carthorse
Version:
A geospatial trail data processing pipeline for building 3D trail databases with elevation data
268 lines โข 13.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.TrailSplitter = void 0;
class TrailSplitter {
constructor(pgClient, stagingSchema, config) {
this.pgClient = pgClient;
this.stagingSchema = stagingSchema;
this.config = config;
}
/**
* Perform comprehensive trail splitting at intersections
*/
async splitTrails(sourceQuery, params) {
console.log('๐ DEBUG: Starting comprehensive trail splitting...');
if (this.config.verbose) {
console.log(`๐ Trail splitting configuration:`);
console.log(` - Minimum trail length: ${this.config.minTrailLengthMeters}m`);
console.log(` - Staging schema: ${this.stagingSchema}`);
console.log(` - Source query: ${sourceQuery}`);
// Show sample of trails being processed
const sampleTrails = await this.pgClient.query(`
SELECT name, ST_Length(geometry::geography) as length_m,
ST_GeometryType(geometry) as geom_type
FROM (${sourceQuery}) as source_trails
WHERE geometry IS NOT NULL AND ST_IsValid(geometry)
LIMIT 5
`);
console.log(`๐ Sample trails to be processed:`);
sampleTrails.rows.forEach((trail, i) => {
console.log(` ${i + 1}. "${trail.name}" (${trail.length_m.toFixed(1)}m, ${trail.geom_type})`);
});
}
// Step 1: Create a temporary table for original trails to avoid conflicts
console.log('๐ Step 1: Creating temporary table for original trails...');
await this.pgClient.query(`
CREATE TEMP TABLE temp_original_trails AS
SELECT * FROM (${sourceQuery}) as source_trails
WHERE geometry IS NOT NULL AND ST_IsValid(geometry)
`);
const originalCountResult = await this.pgClient.query(`SELECT COUNT(*) FROM temp_original_trails`);
const originalCount = parseInt(originalCountResult.rows[0].count);
console.log(`โ
Created temporary table with ${originalCount} original trails`);
// Step 1.5: Create spatial index for performance
console.log('๐ง Creating spatial index for performance...');
await this.pgClient.query(`
CREATE INDEX IF NOT EXISTS idx_temp_original_trails_geometry
ON temp_original_trails USING GIST (geometry)
`);
console.log('โ
Spatial index created');
// Step 2: Perform comprehensive splitting using ST_Node()
console.log('๐ Step 2: Performing comprehensive trail splitting...');
// Step 2.5: Clean up orphaned nodes after splitting
console.log('๐งน Step 2.5: Cleaning up orphaned nodes...');
await this.cleanupOrphanedNodes();
if (this.config.verbose) {
console.log(`๐ง Using ST_Node() to split trails at intersection points...`);
console.log(`๐ง Using ST_Dump() to extract individual segments...`);
console.log(`๐ง Filtering segments shorter than ${this.config.minTrailLengthMeters}m...`);
// OPTIMIZED: Use spatial index for intersection detection
console.log('๐ Detecting trails with intersections (optimized)...');
const trailsWithIntersections = await this.pgClient.query(`
WITH trail_bboxes AS (
SELECT id, name, geometry,
ST_Envelope(geometry) as bbox
FROM temp_original_trails
WHERE geometry IS NOT NULL AND ST_IsValid(geometry)
),
intersection_pairs AS (
SELECT DISTINCT t1.id, t1.name as trail_name,
COUNT(*) as intersection_count,
ST_Length(t1.geometry::geography) as length_m
FROM trail_bboxes t1
JOIN trail_bboxes t2 ON t1.id < t2.id
WHERE ST_Intersects(t1.bbox, t2.bbox) -- Use bbox first for performance
AND ST_Intersects(t1.geometry, t2.geometry) -- Then precise intersection
AND ST_GeometryType(ST_Intersection(t1.geometry, t2.geometry)) IN ('ST_Point', 'ST_MultiPoint')
GROUP BY t1.id, t1.name, t1.geometry
)
SELECT trail_name, intersection_count, length_m
FROM intersection_pairs
ORDER BY intersection_count DESC
LIMIT 10
`);
if (trailsWithIntersections.rows.length > 0) {
console.log(`๐ Trails with intersections (likely to be split):`);
trailsWithIntersections.rows.forEach((trail, i) => {
console.log(` ${i + 1}. "${trail.trail_name}" (${trail.intersection_count} intersections, ${trail.length_m.toFixed(1)}m)`);
});
}
else {
console.log(`๐ No trails with intersections detected`);
}
}
// Step 2.5: Delete original trails from staging schema to replace with split segments
console.log('๐๏ธ Deleting original trails...');
await this.pgClient.query(`DELETE FROM ${this.stagingSchema}.trails`);
console.log('โ
Deleted original trails');
const comprehensiveSplitSql = `
INSERT INTO ${this.stagingSchema}.trails (
app_uuid, name, region, trail_type, surface, difficulty,
bbox_min_lng, bbox_max_lng, bbox_min_lat, bbox_max_lat,
length_km, elevation_gain, elevation_loss, max_elevation, min_elevation, avg_elevation,
geometry
)
SELECT
gen_random_uuid() as app_uuid,
name, region, trail_type, surface, difficulty,
ST_XMin(dumped.geom) as bbox_min_lng, ST_XMax(dumped.geom) as bbox_max_lng,
ST_YMin(dumped.geom) as bbox_min_lat, ST_YMax(dumped.geom) as bbox_max_lat,
ST_Length(dumped.geom::geography) / 1000.0 as length_km,
elevation_gain, elevation_loss, max_elevation, min_elevation, avg_elevation,
dumped.geom as geometry
FROM temp_original_trails t,
LATERAL ST_Dump(ST_Node(t.geometry)) as dumped
WHERE ST_IsValid(dumped.geom)
AND dumped.geom IS NOT NULL
AND ST_NumPoints(dumped.geom) >= 2
AND ST_StartPoint(dumped.geom) != ST_EndPoint(dumped.geom)
`;
const splitResult = await this.pgClient.query(comprehensiveSplitSql, []);
const splitCount = splitResult.rowCount || 0;
console.log(`โ
Comprehensive splitting complete: ${splitCount} segments`);
if (this.config.verbose) {
console.log(`๐ Splitting summary:`);
console.log(` - Original trails: ${originalCount}`);
console.log(` - Split segments: ${splitCount}`);
console.log(` - Segments per trail: ${originalCount > 0 ? (splitCount / originalCount).toFixed(2) : '0.00'}`);
}
// Step 3: Recreate spatial index for split segments
console.log('๐ง Recreating spatial index for split segments...');
await this.pgClient.query(`
CREATE INDEX IF NOT EXISTS idx_${this.stagingSchema}_trails_geometry
ON ${this.stagingSchema}.trails USING GIST (geometry)
`);
console.log('โ
Spatial index recreated');
// Step 4: Calculate final statistics
console.log('๐ Calculating final statistics...');
const finalCount = await this.pgClient.query(`SELECT COUNT(*) FROM ${this.stagingSchema}.trails`);
console.log(`โ
Final segment count: ${finalCount.rows[0].count}`);
// Count remaining intersections (optimized)
console.log('๐ Counting remaining intersections (optimized)...');
const remainingIntersections = await this.pgClient.query(`
WITH trail_bboxes AS (
SELECT id, name, geometry,
ST_Envelope(geometry) as bbox
FROM ${this.stagingSchema}.trails
WHERE geometry IS NOT NULL AND ST_IsValid(geometry)
)
SELECT COUNT(*) as intersection_count
FROM trail_bboxes t1
JOIN trail_bboxes t2 ON t1.id < t2.id
WHERE ST_Intersects(t1.bbox, t2.bbox)
AND ST_Intersects(t1.geometry, t2.geometry)
AND ST_GeometryType(ST_Intersection(t1.geometry, t2.geometry)) IN ('ST_Point', 'ST_MultiPoint')
`);
console.log(`โ
Remaining intersections: ${remainingIntersections.rows[0].intersection_count}`);
// Clean up temporary table
await this.pgClient.query(`DROP TABLE IF EXISTS temp_original_trails`);
return {
iterations: 1,
finalSegmentCount: parseInt(finalCount.rows[0].count),
intersectionCount: parseInt(remainingIntersections.rows[0].intersection_count)
};
}
/**
* Check if there are any intersections between trails
*/
async hasIntersections() {
const lengthFilter = this.config.minTrailLengthMeters > 0 ?
`AND ST_Length(t1.geometry::geography) > ${this.config.minTrailLengthMeters}
AND ST_Length(t2.geometry::geography) > ${this.config.minTrailLengthMeters}` : '';
const result = await this.pgClient.query(`
SELECT COUNT(*) as intersection_count
FROM ${this.stagingSchema}.trails t1
JOIN ${this.stagingSchema}.trails t2 ON t1.id < t2.id
WHERE ST_Intersects(t1.geometry, t2.geometry)
AND ST_GeometryType(ST_Intersection(t1.geometry, t2.geometry)) IN ('ST_Point', 'ST_MultiPoint')
${lengthFilter}
`);
return parseInt(result.rows[0].intersection_count) > 0;
}
/**
* Get statistics about the current trail network
*/
async getStatistics() {
const statsResult = await this.pgClient.query(`
SELECT
COUNT(*) as total_trails,
AVG(ST_Length(geometry::geography)) as avg_length,
COUNT(*) FILTER (WHERE ST_Length(geometry::geography) > 0) as valid_trails
FROM ${this.stagingSchema}.trails
WHERE geometry IS NOT NULL AND ST_IsValid(geometry)
`);
const lengthFilter = this.config.minTrailLengthMeters > 0 ?
`AND ST_Length(t1.geometry::geography) > ${this.config.minTrailLengthMeters}
AND ST_Length(t2.geometry::geography) > ${this.config.minTrailLengthMeters}` : '';
const intersectionResult = await this.pgClient.query(`
SELECT COUNT(*) as intersection_count
FROM ${this.stagingSchema}.trails t1
JOIN ${this.stagingSchema}.trails t2 ON t1.id < t2.id
WHERE ST_Intersects(t1.geometry, t2.geometry)
AND ST_GeometryType(ST_Intersection(t1.geometry, t2.geometry)) IN ('ST_Point', 'ST_MultiPoint')
${lengthFilter}
`);
return {
totalTrails: parseInt(statsResult.rows[0].total_trails),
intersectionCount: parseInt(intersectionResult.rows[0].intersection_count),
averageTrailLength: parseFloat(statsResult.rows[0].avg_length) || 0
};
}
/**
* Clean up orphaned nodes after trail splitting
* Removes nodes that are not connected to any edges in the routing network
*/
async cleanupOrphanedNodes() {
try {
// Check if ways_noded_vertices_pgr table exists (created by pgr_nodeNetwork)
const tableExists = await this.pgClient.query(`
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = $1
AND table_name = 'ways_noded_vertices_pgr'
)
`, [this.stagingSchema]);
if (!tableExists.rows[0].exists) {
console.log('โ ๏ธ ways_noded_vertices_pgr table not found, skipping orphaned node cleanup');
return;
}
// Find orphaned nodes (nodes not connected to any edges)
const orphanedNodesResult = await this.pgClient.query(`
SELECT COUNT(*) as orphaned_count
FROM ${this.stagingSchema}.ways_noded_vertices_pgr v
WHERE v.id NOT IN (
SELECT DISTINCT source FROM ${this.stagingSchema}.ways_noded
WHERE source IS NOT NULL AND target IS NOT NULL
UNION
SELECT DISTINCT target FROM ${this.stagingSchema}.ways_noded
WHERE source IS NOT NULL AND target IS NOT NULL
)
`);
const orphanedCount = parseInt(orphanedNodesResult.rows[0].orphaned_count);
if (orphanedCount > 0) {
console.log(`๐งน Found ${orphanedCount} orphaned nodes, cleaning up...`);
// Remove orphaned nodes from ways_noded_vertices_pgr
await this.pgClient.query(`
DELETE FROM ${this.stagingSchema}.ways_noded_vertices_pgr v
WHERE v.id NOT IN (
SELECT DISTINCT source FROM ${this.stagingSchema}.ways_noded
WHERE source IS NOT NULL AND target IS NOT NULL
UNION
SELECT DISTINCT target FROM ${this.stagingSchema}.ways_noded
WHERE source IS NOT NULL AND target IS NOT NULL
)
`);
console.log(`โ
Cleaned up ${orphanedCount} orphaned nodes`);
}
else {
console.log('โ
No orphaned nodes found');
}
}
catch (error) {
console.log(`โ ๏ธ Orphaned node cleanup failed: ${error}`);
// Don't throw error, just log it as this is a cleanup operation
}
}
}
exports.TrailSplitter = TrailSplitter;
//# sourceMappingURL=trail-splitter.js.map