carthorse
Version:
A geospatial trail data processing pipeline for building 3D trail databases with elevation data
433 lines • 16.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.LoopSplittingHelpers = void 0;
exports.createLoopSplittingHelpers = createLoopSplittingHelpers;
class LoopSplittingHelpers {
constructor(config) {
this.stagingSchema = config.stagingSchema;
this.pgClient = config.pgClient;
this.intersectionTolerance = config.intersectionTolerance ?? 2.0;
}
/**
* Intelligently split loop trails at intersections and apex points
*/
async splitLoopTrails() {
try {
console.log('🔄 Starting intelligent loop trail splitting...');
// Step 1: Identify loop trails (self-intersecting)
const loopTrailsResult = await this.identifyLoopTrails();
if (!loopTrailsResult.success) {
return loopTrailsResult;
}
// Step 2: Find intersection points between loops and other trails
const intersectionResult = await this.findLoopIntersections();
if (!intersectionResult.success) {
return intersectionResult;
}
// Step 3: Find apex points for loops that only intersect once
const apexResult = await this.findLoopApexPoints();
if (!apexResult.success) {
return apexResult;
}
// Step 4: Split loops at both intersection and apex points
const splitResult = await this.splitLoopsAtPoints();
if (!splitResult.success) {
return splitResult;
}
console.log('✅ Loop trail splitting completed successfully');
return {
success: true,
loopCount: loopTrailsResult.loopCount,
splitSegments: splitResult.splitSegments,
intersectionPoints: intersectionResult.intersectionPoints,
apexPoints: apexResult.apexPoints
};
}
catch (error) {
console.error('❌ Loop trail splitting failed:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}
/**
* Identify loop trails (self-intersecting geometries)
*/
async identifyLoopTrails() {
try {
// Create loop trails table
await this.pgClient.query(`
DROP TABLE IF EXISTS ${this.stagingSchema}.loop_trails;
CREATE TABLE ${this.stagingSchema}.loop_trails AS
SELECT
app_uuid,
name,
geometry,
length_km,
elevation_gain,
elevation_loss,
max_elevation,
min_elevation,
avg_elevation,
trail_type,
surface,
difficulty,
source_tags,
osm_id,
region,
bbox_min_lng,
bbox_max_lng,
bbox_min_lat,
bbox_max_lat,
created_at,
updated_at
FROM ${this.stagingSchema}.trails
WHERE NOT ST_IsSimple(geometry)
AND ST_IsValid(geometry)
AND geometry IS NOT NULL
`);
const result = await this.pgClient.query(`
SELECT COUNT(*) as loop_count FROM ${this.stagingSchema}.loop_trails
`);
const loopCount = parseInt(result.rows[0].loop_count);
console.log(`✅ Identified ${loopCount} loop trails`);
return { success: true, loopCount };
}
catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}
/**
* Find intersection points between loop trails and other trails
*/
async findLoopIntersections() {
try {
// Create intersection points table
await this.pgClient.query(`
DROP TABLE IF EXISTS ${this.stagingSchema}.loop_intersections;
CREATE TABLE ${this.stagingSchema}.loop_intersections (
loop_uuid TEXT,
other_trail_uuid TEXT,
intersection_point GEOMETRY(POINT, 4326),
intersection_point_3d GEOMETRY(POINTZ, 4326),
loop_name TEXT,
other_trail_name TEXT,
distance_meters DOUBLE PRECISION
)
`);
// Find intersections between loops and other trails
await this.pgClient.query(`
INSERT INTO ${this.stagingSchema}.loop_intersections (
loop_uuid, other_trail_uuid, intersection_point, intersection_point_3d,
loop_name, other_trail_name, distance_meters
)
SELECT DISTINCT
lt.app_uuid as loop_uuid,
t.app_uuid as other_trail_uuid,
ST_Force2D(ST_Centroid(ST_Intersection(lt.geometry, t.geometry))) as intersection_point,
ST_Force3D(ST_Centroid(ST_Intersection(lt.geometry, t.geometry))) as intersection_point_3d,
lt.name as loop_name,
t.name as other_trail_name,
ST_Distance(lt.geometry::geography, t.geometry::geography) as distance_meters
FROM ${this.stagingSchema}.loop_trails lt
JOIN ${this.stagingSchema}.trails t ON lt.app_uuid != t.app_uuid
WHERE ST_Intersects(lt.geometry, t.geometry)
AND ST_GeometryType(ST_Intersection(lt.geometry, t.geometry)) IN ('ST_Point', 'ST_MultiPoint')
AND ST_Distance(lt.geometry::geography, t.geometry::geography) <= $1
AND ST_Length(lt.geometry::geography) > 5
AND ST_Length(t.geometry::geography) > 5
`, [this.intersectionTolerance]);
const result = await this.pgClient.query(`
SELECT COUNT(*) as intersection_count FROM ${this.stagingSchema}.loop_intersections
`);
const intersectionCount = parseInt(result.rows[0].intersection_count);
console.log(`✅ Found ${intersectionCount} intersection points between loops and other trails`);
return { success: true, intersectionPoints: intersectionCount };
}
catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}
/**
* Find apex points for loops that only intersect once
*/
async findLoopApexPoints() {
try {
// Create apex points table
await this.pgClient.query(`
DROP TABLE IF EXISTS ${this.stagingSchema}.loop_apex_points;
CREATE TABLE ${this.stagingSchema}.loop_apex_points (
loop_uuid TEXT,
apex_point GEOMETRY(POINT, 4326),
apex_point_3d GEOMETRY(POINTZ, 4326),
loop_name TEXT,
intersection_count INTEGER
)
`);
// Find apex points for loops that only intersect once
await this.pgClient.query(`
INSERT INTO ${this.stagingSchema}.loop_apex_points (
loop_uuid, apex_point, apex_point_3d, loop_name, intersection_count
)
WITH loop_intersection_counts AS (
SELECT
loop_uuid,
COUNT(*) as intersection_count
FROM ${this.stagingSchema}.loop_intersections
GROUP BY loop_uuid
),
single_intersection_loops AS (
SELECT
lic.loop_uuid,
lic.intersection_count,
lt.name as loop_name,
lt.geometry as loop_geometry
FROM loop_intersection_counts lic
JOIN ${this.stagingSchema}.loop_trails lt ON lic.loop_uuid = lt.app_uuid
WHERE lic.intersection_count = 1
)
SELECT
sil.loop_uuid,
ST_PointOnSurface(sil.loop_geometry) as apex_point,
ST_Force3D(ST_PointOnSurface(sil.loop_geometry)) as apex_point_3d,
sil.loop_name,
sil.intersection_count
FROM single_intersection_loops sil
`);
const result = await this.pgClient.query(`
SELECT COUNT(*) as apex_count FROM ${this.stagingSchema}.loop_apex_points
`);
const apexCount = parseInt(result.rows[0].apex_count);
console.log(`✅ Found ${apexCount} apex points for single-intersection loops`);
return { success: true, apexPoints: apexCount };
}
catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}
/**
* Split loops at both intersection and apex points
*/
async splitLoopsAtPoints() {
try {
// Create split segments table
await this.pgClient.query(`
DROP TABLE IF EXISTS ${this.stagingSchema}.loop_split_segments;
CREATE TABLE ${this.stagingSchema}.loop_split_segments (
id SERIAL PRIMARY KEY,
original_loop_uuid TEXT,
segment_number INTEGER,
segment_name TEXT,
geometry GEOMETRY(LINESTRING, 4326),
geometry_3d GEOMETRY(LINESTRINGZ, 4326),
length_km DOUBLE PRECISION,
elevation_gain DOUBLE PRECISION,
elevation_loss DOUBLE PRECISION,
max_elevation DOUBLE PRECISION,
min_elevation DOUBLE PRECISION,
avg_elevation DOUBLE PRECISION,
trail_type TEXT,
surface TEXT,
difficulty TEXT,
source_tags JSONB,
osm_id TEXT,
region TEXT,
bbox_min_lng DOUBLE PRECISION,
bbox_max_lng DOUBLE PRECISION,
bbox_min_lat DOUBLE PRECISION,
bbox_max_lat DOUBLE PRECISION,
split_type TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
`);
// Split loops at intersection points
await this.pgClient.query(`
INSERT INTO ${this.stagingSchema}.loop_split_segments (
original_loop_uuid, segment_number, segment_name, geometry, geometry_3d,
length_km, elevation_gain, elevation_loss, max_elevation, min_elevation, avg_elevation,
trail_type, surface, difficulty, source_tags, osm_id, region,
bbox_min_lng, bbox_max_lng, bbox_min_lat, bbox_max_lat, split_type
)
WITH intersection_split_points AS (
SELECT
loop_uuid,
intersection_point,
'intersection' as split_type
FROM ${this.stagingSchema}.loop_intersections
),
apex_split_points AS (
SELECT
loop_uuid,
apex_point as intersection_point,
'apex' as split_type
FROM ${this.stagingSchema}.loop_apex_points
),
all_split_points AS (
SELECT * FROM intersection_split_points
UNION ALL
SELECT * FROM apex_split_points
),
split_segments AS (
SELECT
lt.app_uuid as original_loop_uuid,
lt.name as original_name,
lt.geometry as original_geometry,
lt.length_km,
lt.elevation_gain,
lt.elevation_loss,
lt.max_elevation,
lt.min_elevation,
lt.avg_elevation,
lt.trail_type,
lt.surface,
lt.difficulty,
lt.source_tags,
lt.osm_id,
lt.region,
lt.bbox_min_lng,
lt.bbox_max_lng,
lt.bbox_min_lat,
lt.bbox_max_lat,
dumped.geom as segment_geometry,
dumped.path[1] as segment_order,
sp.split_type
FROM ${this.stagingSchema}.loop_trails lt
JOIN all_split_points sp ON lt.app_uuid = sp.loop_uuid
CROSS JOIN LATERAL ST_Dump(ST_Split(lt.geometry, sp.intersection_point)) as dumped
)
SELECT
original_loop_uuid,
segment_order as segment_number,
original_name || ' (Segment ' || segment_order || ')' as segment_name,
ST_Force2D(segment_geometry) as geometry,
ST_Force3D(segment_geometry) as geometry_3d,
ST_Length(segment_geometry::geography) / 1000 as length_km,
elevation_gain,
elevation_loss,
max_elevation,
min_elevation,
avg_elevation,
trail_type,
surface,
difficulty,
source_tags,
osm_id,
region,
ST_XMin(segment_geometry) as bbox_min_lng,
ST_XMax(segment_geometry) as bbox_max_lng,
ST_YMin(segment_geometry) as bbox_min_lat,
ST_YMax(segment_geometry) as bbox_max_lat,
split_type
FROM split_segments
WHERE ST_GeometryType(segment_geometry) = 'ST_LineString'
AND ST_Length(segment_geometry::geography) > 5 -- Filter out very short segments
`);
const result = await this.pgClient.query(`
SELECT COUNT(*) as segment_count FROM ${this.stagingSchema}.loop_split_segments
`);
const segmentCount = parseInt(result.rows[0].segment_count);
console.log(`✅ Created ${segmentCount} split segments from loop trails`);
return { success: true, splitSegments: segmentCount };
}
catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}
/**
* Replace loop trails with split segments in the main trails table
*/
async replaceLoopTrailsWithSegments() {
try {
console.log('🔄 Replacing loop trails with split segments...');
// Remove original loop trails
await this.pgClient.query(`
DELETE FROM ${this.stagingSchema}.trails
WHERE app_uuid IN (SELECT app_uuid FROM ${this.stagingSchema}.loop_trails)
`);
// Insert split segments as new trails, forcing geometry to 3D
await this.pgClient.query(`
INSERT INTO ${this.stagingSchema}.trails (
app_uuid, name, geometry, length_km, elevation_gain, elevation_loss,
max_elevation, min_elevation, avg_elevation, trail_type, surface, difficulty,
source_tags, osm_id, region, bbox_min_lng, bbox_max_lng, bbox_min_lat, bbox_max_lat
)
SELECT
original_loop_uuid || '_segment_' || segment_number as app_uuid,
segment_name as name,
ST_Force3D(geometry) as geometry,
length_km,
elevation_gain,
elevation_loss,
max_elevation,
min_elevation,
avg_elevation,
trail_type,
surface,
difficulty,
source_tags,
osm_id,
region,
bbox_min_lng,
bbox_max_lng,
bbox_min_lat,
bbox_max_lat
FROM ${this.stagingSchema}.loop_split_segments
`);
const result = await this.pgClient.query(`
SELECT COUNT(*) as replaced_count FROM ${this.stagingSchema}.trails
WHERE app_uuid LIKE '%_segment_%'
`);
const replacedCount = parseInt(result.rows[0].replaced_count);
console.log(`✅ Replaced loop trails with ${replacedCount} split segments`);
return { success: true, splitSegments: replacedCount };
}
catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}
/**
* Get statistics about loop splitting
*/
async getLoopSplittingStats() {
try {
const stats = await this.pgClient.query(`
SELECT
(SELECT COUNT(*) FROM ${this.stagingSchema}.loop_trails) as loop_count,
(SELECT COUNT(*) FROM ${this.stagingSchema}.loop_intersections) as intersection_count,
(SELECT COUNT(*) FROM ${this.stagingSchema}.loop_apex_points) as apex_count,
(SELECT COUNT(*) FROM ${this.stagingSchema}.loop_split_segments) as segment_count,
(SELECT COUNT(*) FROM ${this.stagingSchema}.trails WHERE app_uuid LIKE '%_segment_%') as replaced_count
`);
return stats.rows[0];
}
catch (error) {
console.error('❌ Failed to get loop splitting stats:', error);
return null;
}
}
}
exports.LoopSplittingHelpers = LoopSplittingHelpers;
function createLoopSplittingHelpers(stagingSchema, pgClient, intersectionTolerance) {
return new LoopSplittingHelpers({
stagingSchema,
pgClient,
intersectionTolerance
});
}
//# sourceMappingURL=loop-splitting-helpers.js.map