carthorse
Version:
A geospatial trail data processing pipeline for building 3D trail databases with elevation data
787 lines (785 loc) ⢠41.7 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.KspRouteGenerator = void 0;
class KspRouteGenerator {
constructor(pgClient, stagingSchema) {
this.pgClient = pgClient;
this.stagingSchema = stagingSchema;
}
async generateRouteRecommendations() {
console.log('š¤ļø Starting KSP route recommendation generation...');
try {
// Step 1: Load route patterns (only out-and-back for now)
console.log('š Loading out-and-back route patterns...');
const patterns = await this.loadRoutePatterns();
console.log(`ā
Loaded ${patterns.length} out-and-back route patterns`);
if (patterns.length === 0) {
console.log('ā ļø No out-and-back patterns found');
return [];
}
// Step 2: Add length and elevation columns to ways_noded for KSP routing
console.log('š Adding length and elevation columns to ways_noded...');
await this.addLengthAndElevationColumns();
// Step 3: Skip connectivity fixes to preserve trail-only routing (like working script)
console.log('āļø Skipping connectivity fixes to preserve trail-only routing');
// Step 4: Generate routes for each pattern using native pgRouting algorithms
const allRecommendations = [];
for (const pattern of patterns) {
console.log(`\nšÆ Processing pattern: ${pattern.pattern_name} (${pattern.target_distance_km}km, ${pattern.target_elevation_gain}m, ${pattern.route_shape})`);
let patternRoutes = [];
const targetRoutes = 5;
// Use different pgRouting algorithms based on route shape
console.log(`š DEBUG: Pattern ${pattern.pattern_name} has route_shape: "${pattern.route_shape}"`);
if (pattern.route_shape === 'loop') {
console.log(`š Using pgr_dijkstra for loop routes`);
patternRoutes = await this.generateLoopRoutes(pattern, targetRoutes);
}
else if (pattern.route_shape === 'point-to-point') {
console.log(`š Using pgr_dijkstra for point-to-point routes`);
patternRoutes = await this.generatePointToPointRoutes(pattern, targetRoutes);
}
else {
// Default to out-and-back using existing KSP logic
console.log(`š Using pgr_ksp for out-and-back routes`);
patternRoutes = await this.generateOutAndBackRoutes(pattern, targetRoutes);
}
// Also try withPoints for more flexible routing
if (patternRoutes.length < targetRoutes) {
console.log(`š Trying pgr_withPoints for additional flexible routes`);
const withPointsRoutes = await this.generateWithPointsRoutes(pattern, targetRoutes - patternRoutes.length);
patternRoutes.push(...withPointsRoutes);
}
console.log(`ā
Generated ${patternRoutes.length} routes for ${pattern.pattern_name}`);
allRecommendations.push(...patternRoutes);
}
// Store recommendations in the staging schema for SQLite export
if (allRecommendations.length > 0) {
console.log(`š¾ Storing ${allRecommendations.length} route recommendations in staging schema...`);
await this.storeRecommendationsInDatabase(allRecommendations);
console.log(`ā
Successfully stored ${allRecommendations.length} route recommendations`);
}
else {
console.log('ā ļø No route recommendations generated');
}
return allRecommendations;
}
catch (error) {
console.error('ā KSP route generation failed:', error);
throw error;
}
}
async loadRoutePatterns() {
const result = await this.pgClient.query(`
SELECT pattern_name, target_distance_km, target_elevation_gain, route_shape, route_type, tolerance_percent
FROM public.route_patterns
WHERE route_shape IN ('out-and-back', 'loop', 'point-to-point')
ORDER BY target_distance_km, route_shape
`);
return result.rows;
}
async addLengthAndElevationColumns() {
// Add length_km column
await this.pgClient.query(`
ALTER TABLE ${this.stagingSchema}.ways_noded
ADD COLUMN IF NOT EXISTS length_km DOUBLE PRECISION
`);
// Calculate length in kilometers
await this.pgClient.query(`
UPDATE ${this.stagingSchema}.ways_noded
SET length_km = ST_Length(the_geom::geography) / 1000
`);
// Add elevation_gain column
await this.pgClient.query(`
ALTER TABLE ${this.stagingSchema}.ways_noded
ADD COLUMN IF NOT EXISTS elevation_gain DOUBLE PRECISION DEFAULT 0
`);
// Calculate elevation gain by joining with trail data
await this.pgClient.query(`
UPDATE ${this.stagingSchema}.ways_noded w
SET elevation_gain = COALESCE(t.elevation_gain, 0)
FROM ${this.stagingSchema}.trails t
WHERE w.old_id = t.id
`);
console.log('ā
Added length_km and elevation_gain columns to ways_noded');
}
async getRegionFromStagingSchema() {
// Get the region from the staging schema by checking the region column in trails table
const result = await this.pgClient.query(`
SELECT DISTINCT region
FROM ${this.stagingSchema}.trails
WHERE region IS NOT NULL
LIMIT 1
`);
if (result.rows.length > 0) {
return result.rows[0].region;
}
// Fallback: try to get region from public.trails based on bbox overlap
const bboxResult = await this.pgClient.query(`
SELECT DISTINCT t.region
FROM public.trails t
WHERE EXISTS (
SELECT 1 FROM ${this.stagingSchema}.trails s
WHERE ST_Intersects(s.geometry, t.geometry)
)
LIMIT 1
`);
if (bboxResult.rows.length > 0) {
return bboxResult.rows[0].region;
}
// Final fallback
return 'unknown';
}
async storeRecommendationsInDatabase(recommendations) {
console.log(`š¾ Storing ${recommendations.length} route recommendations in ${this.stagingSchema}.route_recommendations...`);
// Clear existing recommendations
await this.pgClient.query(`DELETE FROM ${this.stagingSchema}.route_recommendations`);
// Insert new recommendations
for (const recommendation of recommendations) {
await this.pgClient.query(`
INSERT INTO ${this.stagingSchema}.route_recommendations (
route_uuid, route_name, route_type, route_shape,
input_length_km, input_elevation_gain,
recommended_length_km, recommended_elevation_gain,
route_path, route_edges, trail_count,
route_score, similarity_score, region
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
`, [
recommendation.route_uuid,
recommendation.route_name,
recommendation.route_type,
recommendation.route_shape,
recommendation.input_length_km,
recommendation.input_elevation_gain,
recommendation.recommended_length_km,
recommendation.recommended_elevation_gain,
JSON.stringify(recommendation.route_path),
JSON.stringify(recommendation.route_edges),
recommendation.trail_count,
recommendation.route_score,
recommendation.similarity_score,
recommendation.region
]);
}
console.log(`ā
Successfully stored ${recommendations.length} route recommendations in database`);
}
/**
* Generate out-and-back routes using KSP algorithm
* For out-and-back routes, we target half the distance since we'll double it for the return journey
*/
async generateOutAndBackRoutes(pattern, targetRoutes = 5) {
console.log(`\nšÆ Generating out-and-back routes for: ${pattern.pattern_name} (${pattern.target_distance_km}km, ${pattern.target_elevation_gain}m)`);
// For out-and-back routes, we target half the distance since we'll double it
const halfTargetDistance = pattern.target_distance_km / 2;
const halfTargetElevation = pattern.target_elevation_gain / 2;
console.log(`š Targeting half-distance: ${halfTargetDistance.toFixed(1)}km, half-elevation: ${halfTargetElevation.toFixed(0)}m`);
// Get intersection nodes for routing
const nodesResult = await this.pgClient.query(`
SELECT pg_id as id, node_type, connection_count
FROM ${this.stagingSchema}.node_mapping
WHERE node_type IN ('intersection', 'endpoint')
ORDER BY connection_count DESC
LIMIT 20
`);
if (nodesResult.rows.length < 2) {
console.log('ā ļø Not enough nodes for routing');
return [];
}
const patternRoutes = [];
// Try different tolerance levels to get target routes
const toleranceLevels = [
{ name: 'strict', distance: pattern.tolerance_percent, elevation: pattern.tolerance_percent, quality: 1.0 },
{ name: 'medium', distance: 50, elevation: 50, quality: 0.8 },
{ name: 'wide', distance: 100, elevation: 100, quality: 0.6 }
];
for (const tolerance of toleranceLevels) {
if (patternRoutes.length >= targetRoutes)
break;
console.log(`š Trying ${tolerance.name} tolerance (${tolerance.distance}% distance, ${tolerance.elevation}% elevation)`);
// Generate out-and-back routes from each node
for (let i = 0; i < Math.min(nodesResult.rows.length, 10); i++) {
if (patternRoutes.length >= targetRoutes)
break;
const startNode = nodesResult.rows[i].id;
// Find reachable nodes using proper path-based discovery
// Use pgr_dijkstra to find nodes within a reasonable distance (2x half target for safety)
const maxSearchDistance = halfTargetDistance * 2;
console.log(` š Finding nodes reachable within ${maxSearchDistance.toFixed(1)}km from node ${startNode}...`);
const reachableNodes = await this.pgClient.query(`
SELECT DISTINCT end_vid as node_id, agg_cost as distance_km
FROM pgr_dijkstra(
'SELECT id, source, target, length_km as cost FROM ${this.stagingSchema}.ways_noded',
$1::bigint,
(SELECT array_agg(pg_id) FROM ${this.stagingSchema}.node_mapping WHERE node_type IN ('intersection', 'endpoint')),
false
)
WHERE agg_cost <= $2
AND end_vid != $1
ORDER BY agg_cost DESC
LIMIT 10
`, [startNode, maxSearchDistance]);
if (reachableNodes.rows.length === 0) {
console.log(` ā No reachable nodes found from node ${startNode} within ${maxSearchDistance.toFixed(1)}km`);
continue;
}
console.log(` ā
Found ${reachableNodes.rows.length} reachable nodes from node ${startNode}`);
// Try each reachable node as a destination for out-and-back route
for (const reachableNode of reachableNodes.rows) {
if (patternRoutes.length >= targetRoutes)
break;
const endNode = reachableNode.node_id;
const oneWayDistance = reachableNode.distance_km;
console.log(` š¤ļø Trying out-and-back route: ${startNode} ā ${endNode} ā ${startNode} (one-way: ${oneWayDistance.toFixed(2)}km)`);
// Check if the one-way distance is reasonable for our target
const minDistance = halfTargetDistance * (1 - tolerance.distance / 100);
const maxDistance = halfTargetDistance * (1 + tolerance.distance / 100);
if (oneWayDistance < minDistance || oneWayDistance > maxDistance) {
console.log(` ā One-way distance ${oneWayDistance.toFixed(2)}km outside tolerance range [${minDistance.toFixed(2)}km, ${maxDistance.toFixed(2)}km]`);
continue;
}
try {
// Verify we can return from endNode to startNode (should be same distance)
const returnPathCheck = await this.pgClient.query(`
SELECT
COUNT(*) as path_exists,
MAX(agg_cost) as return_distance_km
FROM pgr_dijkstra(
'SELECT id, source, target, length_km as cost FROM ${this.stagingSchema}.ways_noded',
$1::bigint, $2::bigint, false
)
`, [endNode, startNode]);
const canReturn = returnPathCheck.rows[0].path_exists > 0;
const returnDistance = returnPathCheck.rows[0].return_distance_km || 0;
if (!canReturn) {
console.log(` ā Cannot return from node ${endNode} to ${startNode}`);
continue;
}
// Check if return distance is similar to outbound distance (within 10%)
const distanceDiff = Math.abs(oneWayDistance - returnDistance);
const distanceDiffPercent = (distanceDiff / oneWayDistance) * 100;
if (distanceDiffPercent > 10) {
console.log(` ā Return distance ${returnDistance.toFixed(2)}km differs too much from outbound ${oneWayDistance.toFixed(2)}km (${distanceDiffPercent.toFixed(1)}% difference)`);
continue;
}
console.log(` ā
Return path verified: ${returnDistance.toFixed(2)}km (${distanceDiffPercent.toFixed(1)}% difference)`);
// Use KSP to find multiple routes for the outbound journey
const kspResult = await this.pgClient.query(`
SELECT * FROM pgr_ksp(
'SELECT id, source, target, length_km as cost FROM ${this.stagingSchema}.ways_noded',
$1::bigint, $2::bigint, 3, false, false
)
`, [startNode, endNode]);
console.log(`ā
KSP found ${kspResult.rows.length} routes`);
// Process each KSP route
const routeGroups = new Map();
for (const row of kspResult.rows) {
if (!routeGroups.has(row.path_id)) {
routeGroups.set(row.path_id, []);
}
routeGroups.get(row.path_id).push(row);
}
for (const [pathId, routeSteps] of routeGroups) {
if (patternRoutes.length >= targetRoutes)
break;
// Extract edge IDs from the route steps (skip -1 which means no edge)
const edgeIds = routeSteps.map((step) => step.edge).filter((edge) => edge !== -1);
if (edgeIds.length === 0) {
console.log(` ā ļø No valid edges found for path ${pathId}`);
continue;
}
// Get the edges for this route
const routeEdges = await this.pgClient.query(`
SELECT * FROM ${this.stagingSchema}.ways_noded
WHERE id = ANY($1::integer[])
ORDER BY id
`, [edgeIds]);
if (routeEdges.rows.length === 0) {
console.log(` ā ļø No edges found for route path`);
continue;
}
// Calculate route metrics (one-way)
let totalDistance = 0;
let totalElevationGain = 0;
for (const edge of routeEdges.rows) {
totalDistance += edge.length_km || 0;
totalElevationGain += edge.elevation_gain || 0;
}
// For out-and-back routes, double the distance and elevation for the return journey
const outAndBackDistance = totalDistance * 2;
const outAndBackElevation = totalElevationGain * 2;
console.log(` š Route metrics: ${totalDistance.toFixed(2)}km ā ${outAndBackDistance.toFixed(2)}km (out-and-back), ${totalElevationGain.toFixed(0)}m ā ${outAndBackElevation.toFixed(0)}m elevation`);
// Check if route meets tolerance criteria (using full out-and-back distance)
const distanceOk = outAndBackDistance >= pattern.target_distance_km * (1 - tolerance.distance / 100) && outAndBackDistance <= pattern.target_distance_km * (1 + tolerance.distance / 100);
const elevationOk = outAndBackElevation >= pattern.target_elevation_gain * (1 - tolerance.elevation / 100) && outAndBackElevation <= pattern.target_elevation_gain * (1 + tolerance.elevation / 100);
if (distanceOk && elevationOk) {
// Calculate quality score based on tolerance level
const finalScore = tolerance.quality * (1.0 - Math.abs(outAndBackDistance - pattern.target_distance_km) / pattern.target_distance_km);
console.log(` ā
Route meets criteria! Score: ${finalScore.toFixed(3)}`);
// Store the route
const recommendation = {
route_uuid: `ksp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
route_name: `${pattern.pattern_name} - KSP Route`,
route_type: 'custom',
route_shape: pattern.route_shape,
input_length_km: pattern.target_distance_km,
input_elevation_gain: pattern.target_elevation_gain,
recommended_length_km: outAndBackDistance,
recommended_elevation_gain: outAndBackElevation,
route_path: { path_id: pathId, steps: routeSteps },
route_edges: routeEdges.rows,
trail_count: routeEdges.rows.length,
route_score: Math.floor(finalScore * 100),
similarity_score: finalScore,
region: await this.getRegionFromStagingSchema()
};
patternRoutes.push(recommendation);
if (patternRoutes.length >= targetRoutes) {
console.log(` šÆ Reached ${targetRoutes} routes for this pattern`);
break;
}
}
else {
console.log(` ā Route doesn't meet criteria (distance: ${distanceOk}, elevation: ${elevationOk})`);
}
}
}
catch (error) {
console.log(`ā KSP routing failed: ${error.message}`);
}
}
}
}
// Sort by score and take top routes
const bestRoutes = patternRoutes
.sort((a, b) => b.route_score - a.route_score)
.slice(0, targetRoutes);
console.log(`ā
Generated ${bestRoutes.length} out-and-back routes for ${pattern.pattern_name}`);
return bestRoutes;
}
async fixConnectivityIssues() {
const connectionTolerance = 0.000045; // ~5 meters in degrees
// 1. Connect edges that come within 5m of nodes but aren't properly connected
const edgeToNodeConnections = await this.pgClient.query(`
WITH nearby_edges_nodes AS (
SELECT
e.old_id as edge_id,
e.source as current_source,
e.target as current_target,
n.id as nearby_node_id,
ST_Distance(ST_EndPoint(e.the_geom), n.the_geom) as distance_to_node,
CASE
WHEN ST_Distance(ST_StartPoint(e.the_geom), n.the_geom) < ST_Distance(ST_EndPoint(e.the_geom), n.the_geom)
THEN 'start'
ELSE 'end'
END as connection_point
FROM ${this.stagingSchema}.ways_noded e
CROSS JOIN ${this.stagingSchema}.ways_noded_vertices_pgr n
WHERE ST_DWithin(ST_EndPoint(e.the_geom), n.the_geom, $1)
OR ST_DWithin(ST_StartPoint(e.the_geom), n.the_geom, $1)
)
SELECT
edge_id,
nearby_node_id,
distance_to_node,
connection_point
FROM nearby_edges_nodes
WHERE distance_to_node <= $1
AND (current_source != nearby_node_id AND current_target != nearby_node_id)
ORDER BY distance_to_node
`, [connectionTolerance]);
if (edgeToNodeConnections.rows.length > 0) {
console.log(`š Found ${edgeToNodeConnections.rows.length} edges to connect to nearby nodes`);
// Update edge connections
for (const connection of edgeToNodeConnections.rows) {
if (connection.connection_point === 'start') {
await this.pgClient.query(`
UPDATE ${this.stagingSchema}.ways_noded
SET source = $1
WHERE old_id = $2
`, [connection.nearby_node_id, connection.edge_id]);
}
else {
await this.pgClient.query(`
UPDATE ${this.stagingSchema}.ways_noded
SET target = $1
WHERE old_id = $2
`, [connection.nearby_node_id, connection.edge_id]);
}
}
console.log(`ā
Connected ${edgeToNodeConnections.rows.length} edges to nearby nodes`);
}
// 2. Connect nearby endpoints (within 5m) to create intersection nodes
const endpointConnections = await this.pgClient.query(`
WITH endpoint_pairs AS (
SELECT
v1.id as node1_id,
v2.id as node2_id,
ST_Distance(v1.the_geom, v2.the_geom) as distance,
v1.the_geom as geom1,
v2.the_geom as geom2
FROM ${this.stagingSchema}.ways_noded_vertices_pgr v1
CROSS JOIN ${this.stagingSchema}.ways_noded_vertices_pgr v2
WHERE v1.id < v2.id
AND v1.cnt = 1 -- Both are endpoints
AND v2.cnt = 1
AND ST_DWithin(v1.the_geom, v2.the_geom, $1)
AND NOT EXISTS (
SELECT 1 FROM ${this.stagingSchema}.ways_noded e
WHERE (e.source = v1.id AND e.target = v2.id)
OR (e.source = v2.id AND e.target = v1.id)
)
)
SELECT
node1_id,
node2_id,
distance,
ST_MakeLine(geom1, geom2) as bridge_geom
FROM endpoint_pairs
WHERE distance <= $1
ORDER BY distance
LIMIT 100 -- Limit to prevent too many connections
`, [connectionTolerance]);
if (endpointConnections.rows.length > 0) {
console.log(`š Found ${endpointConnections.rows.length} endpoint pairs to connect`);
// Add virtual bridge edges between endpoints
for (const connection of endpointConnections.rows) {
await this.pgClient.query(`
INSERT INTO ${this.stagingSchema}.ways_noded (old_id, source, target, the_geom, length_km, elevation_gain)
VALUES (
(SELECT COALESCE(MAX(old_id), 0) + 1 FROM ${this.stagingSchema}.ways_noded),
$1, $2, $3, $4, 0
)
`, [
connection.node1_id,
connection.node2_id,
connection.bridge_geom,
connection.distance * 111.32 // Convert degrees to km
]);
}
console.log(`ā
Added ${endpointConnections.rows.length} bridge edges between endpoints`);
}
// 3. Recalculate node connectivity after connections
await this.pgClient.query(`
UPDATE ${this.stagingSchema}.ways_noded_vertices_pgr
SET cnt = (
SELECT COUNT(*)
FROM ${this.stagingSchema}.ways_noded e
WHERE e.source = ways_noded_vertices_pgr.id OR e.target = ways_noded_vertices_pgr.id
)
`);
console.log('ā
Recalculated node connectivity after connections');
}
/**
* Generate loop routes using pgRouting's native algorithms
* Uses pgr_dijkstra to find paths that return to the start point
*/
async generateLoopRoutes(pattern, targetRoutes = 5) {
console.log(`š Generating TRUE loop routes for pattern: ${pattern.pattern_name}`);
const recommendations = [];
const region = await this.getRegionFromStagingSchema();
// Use pgRouting's cycle detection to find actual loops
// A true loop starts and ends at the same node
console.log(`š Debugging loop detection for pattern: ${pattern.pattern_name} (target: ${pattern.target_distance_km}km, ${pattern.target_elevation_gain}m)`);
// First, let's check what nodes we have
const nodeCountResult = await this.pgClient.query(`
SELECT COUNT(*) as total_nodes,
COUNT(CASE WHEN cnt >= 2 THEN 1 END) as connected_nodes
FROM ${this.stagingSchema}.ways_noded_vertices_pgr
`);
console.log(`š Node stats: ${nodeCountResult.rows[0].total_nodes} total, ${nodeCountResult.rows[0].connected_nodes} with 2+ connections`);
// Check edge connectivity
const edgeCountResult = await this.pgClient.query(`
SELECT COUNT(*) as total_edges,
COUNT(DISTINCT source) as unique_sources,
COUNT(DISTINCT target) as unique_targets
FROM ${this.stagingSchema}.ways_noded
`);
console.log(`š¤ļø Edge stats: ${edgeCountResult.rows[0].total_edges} edges, ${edgeCountResult.rows[0].unique_sources} sources, ${edgeCountResult.rows[0].unique_targets} targets`);
// Try a simpler cycle detection first
const simpleCycleResult = await this.pgClient.query(`
WITH node_pairs AS (
SELECT DISTINCT v1.id as node1, v2.id as node2
FROM ${this.stagingSchema}.ways_noded_vertices_pgr v1
JOIN ${this.stagingSchema}.ways_noded_vertices_pgr v2 ON v1.id != v2.id
WHERE v1.cnt >= 2 AND v2.cnt >= 2
AND ST_DWithin(v1.the_geom, v2.the_geom, 0.01) -- Within ~1km
)
SELECT
np.node1,
np.node2,
pgr_dijkstra(
'SELECT id, source, target, length_km as cost FROM ${this.stagingSchema}.ways_noded',
np.node1, np.node2, false
) as path
FROM node_pairs np
LIMIT 10
`);
console.log(`š Found ${simpleCycleResult.rows.length} potential node pairs for cycle detection`);
// Now try the recursive cycle detection
const cycleResult = await this.pgClient.query(`
WITH RECURSIVE cycle_search AS (
-- Start with nodes that have multiple connections
SELECT
v.id as start_node,
v.id as current_node,
ARRAY[v.id] as path,
0 as distance,
0 as elevation_gain,
ARRAY[]::integer[] as edges
FROM ${this.stagingSchema}.ways_noded_vertices_pgr v
WHERE v.cnt >= 2
AND v.id IN (
SELECT DISTINCT source FROM ${this.stagingSchema}.ways_noded
UNION
SELECT DISTINCT target FROM ${this.stagingSchema}.ways_noded
)
UNION ALL
-- Recursively explore connected nodes
SELECT
cs.start_node,
e.target as current_node,
cs.path || e.target,
cs.distance + e.length_km,
cs.elevation_gain + COALESCE(e.elevation_gain, 0),
cs.edges || e.id
FROM cycle_search cs
JOIN ${this.stagingSchema}.ways_noded e ON cs.current_node = e.source
WHERE e.target != ALL(cs.path[1:array_length(cs.path, 1)-1]) -- Don't revisit nodes except start
AND cs.distance < $1 * 1.5 -- Limit search depth
AND array_length(cs.path, 1) < 20 -- Limit path length
)
SELECT
start_node,
path,
distance,
elevation_gain,
edges,
array_length(path, 1) as path_length
FROM cycle_search
WHERE current_node = start_node -- True loop: ends where it starts
AND array_length(path, 1) > 2 -- Must have at least 3 nodes
AND distance >= $2 * 0.5 -- Minimum distance
AND distance <= $1 * 1.2 -- Maximum distance
AND elevation_gain >= $3 * 0.5 -- Minimum elevation
AND elevation_gain <= $3 * 1.2 -- Maximum elevation
ORDER BY distance
LIMIT 50
`, [pattern.target_distance_km, pattern.target_distance_km * 0.5, pattern.target_elevation_gain]);
console.log(`š Found ${cycleResult.rows.length} potential cycles for loops`);
for (const cycle of cycleResult.rows) {
if (recommendations.length >= targetRoutes)
break;
try {
// Get the edges for this cycle
const routeEdges = await this.pgClient.query(`
SELECT * FROM ${this.stagingSchema}.ways_noded
WHERE id = ANY($1::integer[])
ORDER BY id
`, [cycle.edges]);
if (routeEdges.rows.length === 0)
continue;
// Calculate metrics
let totalDistance = 0;
let totalElevationGain = 0;
for (const edge of routeEdges.rows) {
totalDistance += edge.length_km || 0;
totalElevationGain += edge.elevation_gain || 0;
}
// Check if this meets our target criteria
const distanceOk = totalDistance >= pattern.target_distance_km * 0.8 && totalDistance <= pattern.target_distance_km * 1.2;
const elevationOk = totalElevationGain >= pattern.target_elevation_gain * 0.8 && totalElevationGain <= pattern.target_elevation_gain * 1.2;
if (distanceOk && elevationOk) {
const recommendation = {
route_uuid: `true-loop-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
route_name: `${pattern.pattern_name} - TRUE Loop Route`,
route_type: 'loop',
route_shape: 'loop',
input_length_km: pattern.target_distance_km,
input_elevation_gain: pattern.target_elevation_gain,
recommended_length_km: totalDistance,
recommended_elevation_gain: totalElevationGain,
route_path: {
start_node: cycle.start_node,
path: cycle.path,
edges: cycle.edges
},
route_edges: routeEdges.rows,
trail_count: routeEdges.rows.length,
route_score: Math.floor((1.0 - Math.abs(totalDistance - pattern.target_distance_km) / pattern.target_distance_km) * 100),
similarity_score: 0,
region: region
};
recommendations.push(recommendation);
console.log(`ā
Found TRUE loop route: ${totalDistance.toFixed(2)}km, ${totalElevationGain.toFixed(0)}m elevation, starts/ends at node ${cycle.start_node}`);
}
}
catch (error) {
console.log(`ā Failed to generate true loop route: ${error}`);
}
}
console.log(`ā
Generated ${recommendations.length} TRUE loop routes`);
return recommendations;
}
/**
* Generate point-to-point routes using pgRouting's pgr_dijkstra
*/
async generatePointToPointRoutes(pattern, targetRoutes = 5) {
console.log(`š Generating point-to-point routes for pattern: ${pattern.pattern_name}`);
const recommendations = [];
const region = await this.getRegionFromStagingSchema();
// Get potential start and end nodes
const nodesResult = await this.pgClient.query(`
SELECT id, the_geom, cnt
FROM ${this.stagingSchema}.ways_noded_vertices_pgr
WHERE cnt >= 2
ORDER BY RANDOM()
LIMIT 50
`);
console.log(`š Found ${nodesResult.rows.length} potential nodes for point-to-point routes`);
// Try different node pairs
for (let i = 0; i < nodesResult.rows.length - 1; i += 2) {
if (recommendations.length >= targetRoutes)
break;
const startNode = nodesResult.rows[i];
const endNode = nodesResult.rows[i + 1];
console.log(`š Trying point-to-point from node ${startNode.id} to ${endNode.id}`);
try {
// Use pgr_dijkstra for point-to-point routing
const dijkstraResult = await this.pgClient.query(`
SELECT * FROM pgr_dijkstra(
'SELECT id, source, target, length_km as cost FROM ${this.stagingSchema}.ways_noded',
$1::integer, $2::integer, false
)
`, [startNode.id, endNode.id]);
if (dijkstraResult.rows.length === 0)
continue;
// Calculate metrics
let totalDistance = 0;
let totalElevationGain = 0;
const edgeIds = dijkstraResult.rows.map((row) => row.edge).filter((edge) => edge !== -1);
if (edgeIds.length === 0)
continue;
const routeEdges = await this.pgClient.query(`
SELECT * FROM ${this.stagingSchema}.ways_noded
WHERE id = ANY($1::integer[])
`, [edgeIds]);
for (const edge of routeEdges.rows) {
totalDistance += edge.length_km || 0;
totalElevationGain += edge.elevation_gain || 0;
}
// Check criteria
const distanceOk = totalDistance >= pattern.target_distance_km * 0.8 && totalDistance <= pattern.target_distance_km * 1.2;
const elevationOk = totalElevationGain >= pattern.target_elevation_gain * 0.8 && totalElevationGain <= pattern.target_elevation_gain * 1.2;
if (distanceOk && elevationOk) {
const recommendation = {
route_uuid: `ptp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
route_name: `${pattern.pattern_name} - Point-to-Point Route`,
route_type: 'point-to-point',
route_shape: 'point-to-point',
input_length_km: pattern.target_distance_km,
input_elevation_gain: pattern.target_elevation_gain,
recommended_length_km: totalDistance,
recommended_elevation_gain: totalElevationGain,
route_path: { path: dijkstraResult.rows },
route_edges: routeEdges.rows,
trail_count: routeEdges.rows.length,
route_score: Math.floor((1.0 - Math.abs(totalDistance - pattern.target_distance_km) / pattern.target_distance_km) * 100),
similarity_score: 0,
region: region
};
recommendations.push(recommendation);
console.log(`ā
Found point-to-point route: ${totalDistance.toFixed(2)}km, ${totalElevationGain.toFixed(0)}m elevation`);
}
}
catch (error) {
console.log(`ā Failed to generate point-to-point route: ${error}`);
}
}
console.log(`ā
Generated ${recommendations.length} point-to-point routes`);
return recommendations;
}
/**
* Generate routes using pgr_withPoints for more flexible routing
* This allows starting/ending anywhere on edges, not just at nodes
*/
async generateWithPointsRoutes(pattern, targetRoutes = 5) {
console.log(`š Generating withPoints routes for pattern: ${pattern.pattern_name}`);
const recommendations = [];
const region = await this.getRegionFromStagingSchema();
// Get random points along edges for more flexible routing
const randomPointsResult = await this.pgClient.query(`
SELECT
id as edge_id,
ST_LineInterpolatePoint(the_geom, 0.3) as start_point,
ST_LineInterpolatePoint(the_geom, 0.7) as end_point
FROM ${this.stagingSchema}.ways_noded
WHERE length_km >= $1 * 0.5
ORDER BY RANDOM()
LIMIT 20
`, [pattern.target_distance_km]);
console.log(`š Found ${randomPointsResult.rows.length} potential edge points for withPoints routing`);
for (const edgePoint of randomPointsResult.rows) {
if (recommendations.length >= targetRoutes)
break;
try {
// Use pgr_withPoints for flexible routing
const withPointsResult = await this.pgClient.query(`
SELECT * FROM pgr_withPoints(
'SELECT id, source, target, length_km as cost FROM ${this.stagingSchema}.ways_noded',
'SELECT 1 as pid, $1::geometry as edge_id, $2::float as fraction',
'SELECT 2 as pid, $3::geometry as edge_id, $4::float as fraction',
-1, -2, false
)
`, [
edgePoint.edge_id, 0.3, // Start point
edgePoint.edge_id, 0.7 // End point
]);
if (withPointsResult.rows.length === 0)
continue;
// Calculate metrics
let totalDistance = 0;
let totalElevationGain = 0;
const edgeIds = withPointsResult.rows.map((row) => row.edge).filter((edge) => edge !== -1);
if (edgeIds.length === 0)
continue;
const routeEdges = await this.pgClient.query(`
SELECT * FROM ${this.stagingSchema}.ways_noded
WHERE id = ANY($1::integer[])
`, [edgeIds]);
for (const edge of routeEdges.rows) {
totalDistance += edge.length_km || 0;
totalElevationGain += edge.elevation_gain || 0;
}
// Check criteria
const distanceOk = totalDistance >= pattern.target_distance_km * 0.8 && totalDistance <= pattern.target_distance_km * 1.2;
const elevationOk = totalElevationGain >= pattern.target_elevation_gain * 0.8 && totalElevationGain <= pattern.target_elevation_gain * 1.2;
if (distanceOk && elevationOk) {
const recommendation = {
route_uuid: `wp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
route_name: `${pattern.pattern_name} - Flexible Route`,
route_type: pattern.route_type,
route_shape: pattern.route_shape,
input_length_km: pattern.target_distance_km,
input_elevation_gain: pattern.target_elevation_gain,
recommended_length_km: totalDistance,
recommended_elevation_gain: totalElevationGain,
route_path: { path: withPointsResult.rows },
route_edges: routeEdges.rows,
trail_count: routeEdges.rows.length,
route_score: Math.floor((1.0 - Math.abs(totalDistance - pattern.target_distance_km) / pattern.target_distance_km) * 100),
similarity_score: 0,
region: region
};
recommendations.push(recommendation);
console.log(`ā
Found withPoints route: ${totalDistance.toFixed(2)}km, ${totalElevationGain.toFixed(0)}m elevation`);
}
}
catch (error) {
console.log(`ā Failed to generate withPoints route: ${error}`);
}
}
console.log(`ā
Generated ${recommendations.length} withPoints routes`);
return recommendations;
}
}
exports.KspRouteGenerator = KspRouteGenerator;
//# sourceMappingURL=ksp-route-generator.js.map