UNPKG

carthorse

Version:

A geospatial trail data processing pipeline for building 3D trail databases with elevation data

308 lines 14.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.GeometryPreprocessor = void 0; exports.createGeometryPreprocessor = createGeometryPreprocessor; class GeometryPreprocessor { constructor(pgClient) { this.pgClient = pgClient; } /** * Preprocess trail geometries to ensure they are simple, valid, and non-duplicated. * This function can be called repeatedly until no more changes occur. */ async preprocessTrailGeometries(config) { const { schemaName, tableName = 'trails', region, bbox, maxPasses = 5, minLengthMeters = 0.0, tolerance = 0.00001 } = config; const result = { success: false, initialCount: 0, finalCount: 0, droppedCount: 0, passes: 0, errors: [], summary: { invalidGeometries: 0, nonSimpleGeometries: 0, emptyGeometries: 0, tooShortGeometries: 0, duplicateGeometries: 0, complexGeometries: 0 } }; try { console.log(`🔧 Starting geometry preprocessing for ${schemaName}.${tableName}...`); // Get initial count const initialCountResult = await this.pgClient.query(` SELECT COUNT(*) as count FROM ${schemaName}.${tableName} `); result.initialCount = parseInt(initialCountResult.rows[0].count); console.log(`📊 Initial geometry count: ${result.initialCount}`); let lastCount = result.initialCount; let pass = 0; while (pass < maxPasses) { pass++; console.log(`🔄 Pass ${pass}/${maxPasses}: Processing geometries...`); const passResult = await this.performGeometryCleanupPass(schemaName, tableName, region, bbox, minLengthMeters, tolerance); // Update summary result.summary.invalidGeometries += passResult.invalidGeometries; result.summary.nonSimpleGeometries += passResult.nonSimpleGeometries; result.summary.emptyGeometries += passResult.emptyGeometries; result.summary.tooShortGeometries += passResult.tooShortGeometries; result.summary.duplicateGeometries += passResult.duplicateGeometries; result.summary.complexGeometries += passResult.complexGeometries; // Get current count const currentCountResult = await this.pgClient.query(` SELECT COUNT(*) as count FROM ${schemaName}.${tableName} `); const currentCount = parseInt(currentCountResult.rows[0].count); console.log(`📊 Pass ${pass} results: ${currentCount} geometries remaining (dropped ${lastCount - currentCount})`); // Check if we've stabilized if (currentCount === lastCount) { console.log(`✅ Geometry preprocessing stabilized after ${pass} passes`); break; } lastCount = currentCount; } result.passes = pass; result.finalCount = lastCount; result.droppedCount = result.initialCount - result.finalCount; result.success = true; console.log(`✅ Geometry preprocessing completed:`); console.log(` 📊 Initial: ${result.initialCount} geometries`); console.log(` 📊 Final: ${result.finalCount} geometries`); console.log(` 📊 Dropped: ${result.droppedCount} geometries`); console.log(` 🔄 Passes: ${result.passes}`); console.log(` 📋 Summary:`, result.summary); return result; } catch (error) { result.errors.push(error instanceof Error ? error.message : String(error)); console.error(`❌ Geometry preprocessing failed:`, error); return result; } } /** * Perform a single pass of geometry cleanup */ async performGeometryCleanupPass(schemaName, tableName, region, bbox, minLengthMeters = 0.0, tolerance = 0.00001) { const result = { invalidGeometries: 0, nonSimpleGeometries: 0, emptyGeometries: 0, tooShortGeometries: 0, duplicateGeometries: 0, complexGeometries: 0 }; // Build WHERE clause for filtering let whereClause = 'WHERE geometry IS NOT NULL'; if (region) { whereClause += ` AND region = '${region}'`; } if (bbox) { const [minLng, minLat, maxLng, maxLat] = bbox; whereClause += ` AND ST_Intersects(geometry, ST_MakeEnvelope(${minLng}, ${minLat}, ${maxLng}, ${maxLat}, 4326))`; } // Step 1: Handle complex geometries (MultiLineString, GeometryCollection) console.log(' 🔧 Step 1: Processing complex geometries...'); const complexGeomsResult = await this.pgClient.query(` SELECT COUNT(*) as count FROM ${schemaName}.${tableName} ${whereClause} AND ST_GeometryType(geometry) IN ('ST_MultiLineString', 'ST_GeometryCollection') `); result.complexGeometries = parseInt(complexGeomsResult.rows[0].count); if (result.complexGeometries > 0) { await this.pgClient.query(` UPDATE ${schemaName}.${tableName} SET geometry = ST_LineMerge(ST_CollectionHomogenize(ST_MakeValid(geometry))) ${whereClause} AND ST_GeometryType(geometry) IN ('ST_MultiLineString', 'ST_GeometryCollection') `); console.log(` ✅ Processed ${result.complexGeometries} complex geometries`); } // Step 2: Remove invalid geometries console.log(' 🔧 Step 2: Removing invalid geometries...'); const invalidGeomsResult = await this.pgClient.query(` SELECT COUNT(*) as count FROM ${schemaName}.${tableName} ${whereClause} AND NOT ST_IsValid(geometry) `); result.invalidGeometries = parseInt(invalidGeomsResult.rows[0].count); if (result.invalidGeometries > 0) { await this.pgClient.query(` DELETE FROM ${schemaName}.${tableName} ${whereClause} AND NOT ST_IsValid(geometry) `); console.log(` ✅ Removed ${result.invalidGeometries} invalid geometries`); } // Step 3: Remove non-simple geometries console.log(' 🔧 Step 3: Removing non-simple geometries...'); const nonSimpleGeomsResult = await this.pgClient.query(` SELECT COUNT(*) as count FROM ${schemaName}.${tableName} ${whereClause} AND NOT ST_IsSimple(geometry) `); result.nonSimpleGeometries = parseInt(nonSimpleGeomsResult.rows[0].count); if (result.nonSimpleGeometries > 0) { await this.pgClient.query(` DELETE FROM ${schemaName}.${tableName} ${whereClause} AND NOT ST_IsSimple(geometry) `); console.log(` ✅ Removed ${result.nonSimpleGeometries} non-simple geometries`); } // Step 4: Remove empty geometries console.log(' 🔧 Step 4: Removing empty geometries...'); const emptyGeomsResult = await this.pgClient.query(` SELECT COUNT(*) as count FROM ${schemaName}.${tableName} ${whereClause} AND ST_IsEmpty(geometry) `); result.emptyGeometries = parseInt(emptyGeomsResult.rows[0].count); if (result.emptyGeometries > 0) { await this.pgClient.query(` DELETE FROM ${schemaName}.${tableName} ${whereClause} AND ST_IsEmpty(geometry) `); console.log(` ✅ Removed ${result.emptyGeometries} empty geometries`); } // Step 5: Remove geometries that are too short or single points if (minLengthMeters > 0) { console.log(` 🔧 Step 5: Removing geometries shorter than ${minLengthMeters}m...`); const minLengthDegrees = minLengthMeters / 111000.0; // Convert meters to degrees const tooShortGeomsResult = await this.pgClient.query(` SELECT COUNT(*) as count FROM ${schemaName}.${tableName} ${whereClause} AND ST_Length(geometry) < $1 `, [minLengthDegrees]); result.tooShortGeometries = parseInt(tooShortGeomsResult.rows[0].count); if (result.tooShortGeometries > 0) { await this.pgClient.query(` DELETE FROM ${schemaName}.${tableName} ${whereClause} AND ST_Length(geometry) < $1 `, [minLengthDegrees]); console.log(` ✅ Removed ${result.tooShortGeometries} geometries shorter than ${minLengthMeters}m`); } } else { console.log(' 🔧 Step 5: Removing single-point geometries...'); const singlePointGeomsResult = await this.pgClient.query(` SELECT COUNT(*) as count FROM ${schemaName}.${tableName} ${whereClause} AND ST_NPoints(geometry) < 2 `); result.tooShortGeometries = parseInt(singlePointGeomsResult.rows[0].count); if (result.tooShortGeometries > 0) { await this.pgClient.query(` DELETE FROM ${schemaName}.${tableName} ${whereClause} AND ST_NPoints(geometry) < 2 `); console.log(` ✅ Removed ${result.tooShortGeometries} single-point geometries`); } } // Step 6: Remove geometries that are not LineString console.log(' 🔧 Step 6: Removing non-LineString geometries...'); await this.pgClient.query(` DELETE FROM ${schemaName}.${tableName} ${whereClause} AND ST_GeometryType(geometry) != 'ST_LineString' `); // Step 7: Remove exact duplicates console.log(' 🔧 Step 7: Removing duplicate geometries...'); const duplicatesResult = await this.pgClient.query(` SELECT COUNT(*) as count FROM ( SELECT geometry, COUNT(*) as cnt FROM ${schemaName}.${tableName} ${whereClause} GROUP BY geometry HAVING COUNT(*) > 1 ) dupes `); result.duplicateGeometries = parseInt(duplicatesResult.rows[0].count); if (result.duplicateGeometries > 0) { await this.pgClient.query(` DELETE FROM ${schemaName}.${tableName} a USING ${schemaName}.${tableName} b WHERE a.ctid < b.ctid AND ST_Equals(a.geometry, b.geometry) ${region ? `AND a.region = '${region}' AND b.region = '${region}'` : ''} `); console.log(` ✅ Removed ${result.duplicateGeometries} duplicate geometries`); } return result; } /** * Validate that all geometries in a table are clean and ready for routing */ async validateGeometryCleanliness(schemaName, tableName = 'trails') { const result = { isValid: true, issues: [], summary: { total: 0, valid: 0, simple: 0, nonEmpty: 0, lineStrings: 0, reasonableLength: 0 } }; try { // Get overall counts const totalResult = await this.pgClient.query(` SELECT COUNT(*) as count FROM ${schemaName}.${tableName} WHERE geometry IS NOT NULL `); result.summary.total = parseInt(totalResult.rows[0].count); // Check each validation criterion const validResult = await this.pgClient.query(` SELECT COUNT(*) as count FROM ${schemaName}.${tableName} WHERE geometry IS NOT NULL AND ST_IsValid(geometry) `); result.summary.valid = parseInt(validResult.rows[0].count); const simpleResult = await this.pgClient.query(` SELECT COUNT(*) as count FROM ${schemaName}.${tableName} WHERE geometry IS NOT NULL AND ST_IsSimple(geometry) `); result.summary.simple = parseInt(simpleResult.rows[0].count); const nonEmptyResult = await this.pgClient.query(` SELECT COUNT(*) as count FROM ${schemaName}.${tableName} WHERE geometry IS NOT NULL AND NOT ST_IsEmpty(geometry) `); result.summary.nonEmpty = parseInt(nonEmptyResult.rows[0].count); const lineStringsResult = await this.pgClient.query(` SELECT COUNT(*) as count FROM ${schemaName}.${tableName} WHERE geometry IS NOT NULL AND ST_GeometryType(geometry) = 'ST_LineString' `); result.summary.lineStrings = parseInt(lineStringsResult.rows[0].count); const reasonableLengthResult = await this.pgClient.query(` SELECT COUNT(*) as count FROM ${schemaName}.${tableName} WHERE geometry IS NOT NULL AND ST_Length(geometry) >= 1.0 `); result.summary.reasonableLength = parseInt(reasonableLengthResult.rows[0].count); // Check for issues if (result.summary.valid < result.summary.total) { result.issues.push(`${result.summary.total - result.summary.valid} invalid geometries found`); result.isValid = false; } if (result.summary.simple < result.summary.total) { result.issues.push(`${result.summary.total - result.summary.simple} non-simple geometries found`); result.isValid = false; } if (result.summary.nonEmpty < result.summary.total) { result.issues.push(`${result.summary.total - result.summary.nonEmpty} empty geometries found`); result.isValid = false; } if (result.summary.lineStrings < result.summary.total) { result.issues.push(`${result.summary.total - result.summary.lineStrings} non-LineString geometries found`); result.isValid = false; } if (result.summary.reasonableLength < result.summary.total) { result.issues.push(`${result.summary.total - result.summary.reasonableLength} geometries that are too short found`); result.isValid = false; } return result; } catch (error) { result.issues.push(`Validation failed: ${error instanceof Error ? error.message : String(error)}`); result.isValid = false; return result; } } } exports.GeometryPreprocessor = GeometryPreprocessor; /** * Create a GeometryPreprocessor instance */ function createGeometryPreprocessor(pgClient) { return new GeometryPreprocessor(pgClient); } //# sourceMappingURL=geometry-preprocessing.js.map