carthorse
Version:
A geospatial trail data processing pipeline for building 3D trail databases with elevation data
528 lines β’ 25.5 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.ExportService = exports.TrailsOnlyExportStrategy = exports.SQLiteExportStrategy = exports.GeoJSONExportStrategy = void 0;
const fs = __importStar(require("fs"));
const export_sql_helpers_1 = require("../sql/export-sql-helpers");
/**
* GeoJSON Export Strategy
*/
class GeoJSONExportStrategy {
async export(pgClient, config) {
try {
console.log('πΊοΈ Starting GeoJSON export...');
const sqlHelpers = new export_sql_helpers_1.ExportSqlHelpers(pgClient, config.stagingSchema);
// Export all data from staging schema
const { trails, nodes, edges } = await sqlHelpers.exportAllDataForGeoJSON();
// Handle route recommendations separately to avoid JSON parsing issues
let routeRecommendations = [];
try {
routeRecommendations = await sqlHelpers.exportRouteRecommendations();
// Add GeoJSON-specific processing for routes
if (routeRecommendations.length > 0) {
// Add constituent trails and geometry for GeoJSON
const enrichedRoutes = await Promise.all(routeRecommendations.map(async (route) => {
// Extract constituent trails from route_edges JSONB (already parsed by PostgreSQL)
const constituentTrails = route.route_edges ?
route.route_edges
.filter((edge) => edge.app_uuid)
.map((edge) => ({
app_uuid: edge.app_uuid,
trail_name: edge.trail_name,
trail_type: edge.trail_type,
surface: edge.surface,
difficulty: edge.difficulty,
length_km: edge.trail_length_km,
elevation_gain: edge.trail_elevation_gain,
elevation_loss: edge.elevation_loss,
max_elevation: edge.max_elevation,
min_elevation: edge.min_elevation,
avg_elevation: edge.avg_elevation
})) : [];
// Use the geojson field from the export query
let geometry = null;
if (route.geojson) {
geometry = route.geojson;
}
return {
...route,
constituent_trails: constituentTrails,
geojson: geometry
};
}));
routeRecommendations = enrichedRoutes;
console.log(`β
Successfully exported ${routeRecommendations.length} routes with GeoJSON geometry`);
}
}
catch (error) {
console.log('π No route recommendations to export (this is normal when no routes are generated)');
console.log('Error details:', error instanceof Error ? error.message : String(error));
routeRecommendations = [];
}
// Create GeoJSON features based on configuration
const trailFeatures = config.includeTrails !== false ? trails.map(row => ({
type: 'Feature',
properties: {
id: row.app_uuid,
name: row.name,
trail_type: row.trail_type,
surface: row.surface,
difficulty: row.difficulty,
length_km: row.length_km,
elevation_gain: row.elevation_gain,
elevation_loss: row.elevation_loss,
max_elevation: row.max_elevation,
min_elevation: row.min_elevation,
avg_elevation: row.avg_elevation,
color: '#00ff00', // Green for trails
size: 2
},
geometry: JSON.parse(row.geojson)
})) : [];
const nodeFeatures = config.includeNodes !== false ? nodes.map(row => {
let color = '#0000ff'; // Blue for trail nodes
let size = 0.5; // Much smaller points (was 1)
if (row.node_type === 'intersection') {
color = '#ff0000'; // Red for intersections
size = 0.8; // Much smaller points (was 1.5)
}
else if (row.node_type === 'endpoint') {
color = '#00ff00'; // Green for endpoints
size = 0.8; // Much smaller points (was 1.5)
}
return {
type: 'Feature',
properties: {
id: row.id,
node_uuid: row.node_uuid,
node_type: row.node_type,
connected_trails: row.connected_trails,
trail_ids: row.trail_ids,
color,
size,
// Point-specific styling for smaller visualization
markerSize: size,
markerSymbol: 'circle',
markerColor: color,
opacity: 0.7, // Slightly transparent
weight: size,
radius: size,
fillColor: color,
fillOpacity: 0.6
},
geometry: JSON.parse(row.geojson)
};
}) : [];
const edgeFeatures = config.includeEdges !== false ? edges.map(row => ({
type: 'Feature',
properties: {
id: row.id,
source: row.source,
target: row.target,
trail_id: row.trail_id,
trail_name: row.trail_name,
length_km: row.length_km,
elevation_gain: row.elevation_gain,
elevation_loss: row.elevation_loss,
is_bidirectional: row.is_bidirectional,
color: '#ff00ff', // Magenta for edges
size: 1
},
geometry: JSON.parse(row.geojson)
})) : [];
const routeFeatures = config.includeRoutes !== false ? routeRecommendations.map(row => ({
type: 'Feature',
properties: {
id: row.route_uuid,
route_name: row.route_name,
route_type: row.route_type,
route_shape: row.route_shape,
recommended_length_km: row.recommended_length_km,
recommended_elevation_gain: row.recommended_elevation_gain,
trail_count: row.trail_count,
route_score: row.route_score,
similarity_score: row.similarity_score,
region: row.region,
constituent_trails: row.constituent_trails || [],
color: '#ff8800', // Orange for route recommendations
size: 50, // Much wider for maximum visibility
lineStyle: 'dotted', // Dotted line style
weight: 50, // Additional weight property for some viewers
strokeWidth: 50, // Stroke width for some viewers
strokeColor: '#ff8800', // Explicit stroke color
strokeOpacity: 1.0, // Full opacity
strokeDasharray: '20,10', // Larger dotted pattern
zIndex: 1000, // Ensure routes are on top
opacity: 1.0, // Full opacity
fillOpacity: 0.8 // Fill opacity for some viewers
},
geometry: JSON.parse(row.geojson)
})) : [];
// Create GeoJSON collection - ROUTES FIRST for top layer visibility
const geojson = {
type: 'FeatureCollection',
features: [...routeFeatures, ...trailFeatures, ...nodeFeatures, ...edgeFeatures]
};
// Write to file
fs.writeFileSync(config.outputPath, JSON.stringify(geojson, null, 2));
console.log(`β
GeoJSON export completed:`);
console.log(` π File: ${config.outputPath}`);
console.log(` πΊοΈ Trails: ${trailFeatures.length}`);
console.log(` π Nodes: ${nodeFeatures.length}`);
console.log(` π€οΈ Edges: ${edgeFeatures.length}`);
console.log(` π£οΈ Routes: ${routeFeatures.length}`);
console.log(` π¨ Colors: Trails (green), Nodes (blue/red), Edges (magenta), Routes (orange, dotted, 3x width)`);
return {
success: true,
message: `GeoJSON export completed successfully`,
data: {
trails: trailFeatures.length,
nodes: nodeFeatures.length,
edges: edgeFeatures.length,
routes: routeFeatures.length
}
};
}
catch (error) {
console.error('β Error during GeoJSON export:', error);
return {
success: false,
message: `GeoJSON export failed: ${error}`
};
}
}
}
exports.GeoJSONExportStrategy = GeoJSONExportStrategy;
/**
* SQLite Export Strategy
*/
class SQLiteExportStrategy {
async export(pgClient, config) {
try {
console.log('ποΈ Starting SQLite export...');
// Import SQLite helpers dynamically to avoid circular dependencies
const { createSqliteTables, insertTrails, insertRoutingNodes, insertRoutingEdges, insertRouteRecommendations, insertSchemaVersion } = await Promise.resolve().then(() => __importStar(require('../sqlite-export-helpers')));
// Export data from staging schema
const sqlHelpers = new export_sql_helpers_1.ExportSqlHelpers(pgClient, config.stagingSchema);
// Get all data from staging schema
const trails = await sqlHelpers.exportTrailsForGeoJSON();
const nodes = await sqlHelpers.exportRoutingNodesForGeoJSON();
const edges = await sqlHelpers.exportRoutingEdgesForGeoJSON();
// Handle route recommendations separately to avoid JSON parsing issues
let routeRecommendations = [];
try {
routeRecommendations = await sqlHelpers.exportRouteRecommendations();
console.log(`β
Successfully exported ${routeRecommendations.length} routes to SQLite`);
}
catch (error) {
console.log('π No route recommendations to export (this is normal when no routes are generated)');
routeRecommendations = [];
}
// Create SQLite database
const db = new (await Promise.resolve().then(() => __importStar(require('better-sqlite3')))).default(config.outputPath);
// Create tables
createSqliteTables(db);
// Insert schema version
const { CARTHORSE_SCHEMA_VERSION } = await Promise.resolve().then(() => __importStar(require('../sqlite-export-helpers')));
insertSchemaVersion(db, CARTHORSE_SCHEMA_VERSION, 'Carthorse SQLite Export v14.0 (Enhanced Route Recommendations + Trail Composition)');
// Export trails from staging schema
const trailsResult = await pgClient.query(`
SELECT
app_uuid, name, region, osm_id, 'way' as osm_type, trail_type, surface as surface_type,
CASE
WHEN difficulty = 'unknown' THEN 'moderate'
ELSE difficulty
END as difficulty,
ST_AsGeoJSON(geometry, 6, 1) as geojson,
length_km, elevation_gain, elevation_loss, max_elevation, min_elevation, avg_elevation,
bbox_min_lng, bbox_max_lng, bbox_min_lat, bbox_max_lat,
created_at, updated_at
FROM ${config.stagingSchema}.trails
WHERE geometry IS NOT NULL
ORDER BY name
`);
// Also get all unique trail IDs from routing edges to ensure we export all referenced trails
const routingTrailsResult = await pgClient.query(`
SELECT DISTINCT trail_id as app_uuid
FROM ${config.stagingSchema}.routing_edges
WHERE trail_id IS NOT NULL AND trail_id != ''
`);
const routingTrailIds = new Set(routingTrailsResult.rows.map(row => row.app_uuid));
const exportedTrailIds = new Set(trailsResult.rows.map(row => row.app_uuid));
// Find trails that are in routing edges but not in the main trails export
const missingTrailIds = Array.from(routingTrailIds).filter(id => !exportedTrailIds.has(id));
if (missingTrailIds.length > 0) {
console.log(`[SQLITE] Found ${missingTrailIds.length} trails referenced in routing edges that need to be exported`);
// Get the missing trails from the routing edges (with trail metadata)
const missingTrailsResult = await pgClient.query(`
SELECT DISTINCT
re.trail_id as app_uuid,
re.trail_name as name,
'unknown' as region,
NULL as osm_id,
'way' as osm_type,
'hiking' as trail_type,
'unknown' as surface_type,
'moderate' as difficulty,
ST_AsGeoJSON(re.geometry, 6, 1) as geojson,
re.length_km,
re.elevation_gain,
COALESCE(re.elevation_loss, 0) as elevation_loss,
0 as max_elevation,
0 as min_elevation,
0 as avg_elevation,
NULL as bbox_min_lng,
NULL as bbox_max_lng,
NULL as bbox_min_lat,
NULL as bbox_max_lat,
NOW() as created_at,
NOW() as updated_at
FROM ${config.stagingSchema}.routing_edges re
WHERE re.trail_id = ANY($1)
`, [missingTrailIds]);
if (missingTrailsResult.rows.length > 0) {
// Combine all trails
const allTrails = [...trailsResult.rows, ...missingTrailsResult.rows];
insertTrails(db, allTrails);
console.log(`[SQLITE] β
Exported ${allTrails.length} total trails (${trailsResult.rows.length} from main table + ${missingTrailsResult.rows.length} from routing edges)`);
}
else {
insertTrails(db, trailsResult.rows);
console.log(`[SQLITE] β
Exported ${trailsResult.rows.length} trails from main table`);
}
}
else {
insertTrails(db, trailsResult.rows);
console.log(`[SQLITE] β
Exported ${trailsResult.rows.length} trails from main table`);
}
insertRoutingNodes(db, nodes.map(n => ({
...n,
geometry: JSON.parse(n.geojson)
})));
insertRoutingEdges(db, edges.map(e => ({
...e,
geometry: JSON.parse(e.geojson)
})));
// Insert route recommendations and constituent trail data
if (routeRecommendations.length > 0) {
const { insertRouteRecommendations, insertRouteTrails } = await Promise.resolve().then(() => __importStar(require('../sqlite-export-helpers')));
// Insert route recommendations
insertRouteRecommendations(db, routeRecommendations);
// Extract and insert constituent trail data for each route
const routeTrails = [];
// Get unique trail UUIDs that need name lookup
const trailUuids = new Set();
for (const route of routeRecommendations) {
if (route.constituent_trails && Array.isArray(route.constituent_trails)) {
route.constituent_trails.forEach((trail) => {
if (trail.app_uuid && (!trail.name || trail.name.startsWith('Trail '))) {
trailUuids.add(trail.app_uuid);
}
});
}
}
// Lookup trail names from public database if needed
let trailNameMap = new Map();
if (trailUuids.size > 0) {
console.log(`π Looking up ${trailUuids.size} trail names from public database...`);
const trailNamesResult = await pgClient.query(`
SELECT app_uuid, name
FROM public.trails
WHERE app_uuid = ANY($1::uuid[])
`, [Array.from(trailUuids)]);
trailNamesResult.rows.forEach((row) => {
trailNameMap.set(row.app_uuid, row.name);
});
console.log(`β
Found ${trailNameMap.size} trail names`);
}
// Get all trail IDs that exist in the SQLite database
const existingTrailIds = new Set(trails.map(t => t.app_uuid));
console.log(`[SQLITE] Found ${existingTrailIds.size} unique trail IDs in database`);
for (const route of routeRecommendations) {
if (route.constituent_trails && Array.isArray(route.constituent_trails)) {
route.constituent_trails.forEach((trail, index) => {
// Only include trails that exist in the SQLite database
if (!existingTrailIds.has(trail.app_uuid)) {
console.log(`[SQLITE] β οΈ Skipping route trail segment for non-existent trail: ${trail.app_uuid}`);
return;
}
// Use existing name, lookup from public DB, or fallback to UUID
let trailName = trail.name;
if (!trailName || trailName.startsWith('Trail ')) {
trailName = trailNameMap.get(trail.app_uuid) || `Trail ${trail.app_uuid || 'Unknown'}`;
}
routeTrails.push({
route_uuid: route.route_uuid,
trail_id: trail.app_uuid, // Use app_uuid consistently since that's what's in the trails table
trail_name: trailName,
segment_order: index + 1,
segment_distance_km: trail.distance_km || trail.length_km,
segment_elevation_gain: trail.elevation_gain,
segment_elevation_loss: trail.elevation_loss || 0,
created_at: new Date().toISOString()
});
});
}
}
if (routeTrails.length > 0) {
insertRouteTrails(db, routeTrails);
}
}
db.close();
console.log(`β
SQLite export completed:`);
console.log(` π File: ${config.outputPath}`);
console.log(` πΊοΈ Trails: ${trails.length}`);
console.log(` π Nodes: ${nodes.length}`);
console.log(` π€οΈ Edges: ${edges.length}`);
console.log(` π£οΈ Routes: ${routeRecommendations.length}`);
return {
success: true,
message: `SQLite export completed successfully`,
data: {
trails: trails.length,
nodes: nodes.length,
edges: edges.length,
routes: routeRecommendations.length
}
};
}
catch (error) {
console.error('β Error during SQLite export:', error);
return {
success: false,
message: `SQLite export failed: ${error}`
};
}
}
}
exports.SQLiteExportStrategy = SQLiteExportStrategy;
/**
* Trails-Only Export Strategy (subset of GeoJSON)
*/
class TrailsOnlyExportStrategy {
async export(pgClient, config) {
try {
console.log('πΊοΈ Starting trails-only export...');
const sqlHelpers = new export_sql_helpers_1.ExportSqlHelpers(pgClient, config.stagingSchema);
// Export only trails from staging schema
const trails = await sqlHelpers.exportTrailSegmentsOnly();
// Create GeoJSON features for trails only
const trailFeatures = trails.map(row => ({
type: 'Feature',
properties: {
id: row.app_uuid,
name: row.name,
trail_type: row.trail_type,
surface: row.surface,
difficulty: row.difficulty,
length_km: row.length_km,
elevation_gain: row.elevation_gain,
elevation_loss: row.elevation_loss,
max_elevation: row.max_elevation,
min_elevation: row.min_elevation,
avg_elevation: row.avg_elevation,
color: '#00ff00', // Green for trails
size: 2
},
geometry: JSON.parse(row.geojson)
}));
// Create GeoJSON collection
const geojson = {
type: 'FeatureCollection',
features: trailFeatures
};
// Write to file
fs.writeFileSync(config.outputPath, JSON.stringify(geojson, null, 2));
console.log(`β
Trails-only export completed:`);
console.log(` π File: ${config.outputPath}`);
console.log(` πΊοΈ Trails: ${trailFeatures.length}`);
console.log(` π¨ Colors: Trails (green)`);
return {
success: true,
message: `Trails-only export completed successfully`,
data: {
trails: trailFeatures.length
}
};
}
catch (error) {
console.error('β Error during trails-only export:', error);
return {
success: false,
message: `Trails-only export failed: ${error}`
};
}
}
}
exports.TrailsOnlyExportStrategy = TrailsOnlyExportStrategy;
/**
* Main Export Service
*/
class ExportService {
constructor() {
this.strategies = new Map();
// Register export strategies
this.strategies.set('geojson', new GeoJSONExportStrategy());
this.strategies.set('sqlite', new SQLiteExportStrategy());
this.strategies.set('trails-only', new TrailsOnlyExportStrategy());
}
/**
* Export data using the specified strategy
*/
async export(format, pgClient, config) {
const strategy = this.strategies.get(format);
if (!strategy) {
return {
success: false,
message: `Unsupported export format: ${format}`
};
}
return await strategy.export(pgClient, config);
}
/**
* Register a new export strategy
*/
registerStrategy(name, strategy) {
this.strategies.set(name, strategy);
}
/**
* Get available export formats
*/
getAvailableFormats() {
return Array.from(this.strategies.keys());
}
}
exports.ExportService = ExportService;
//# sourceMappingURL=export-service.js.map