UNPKG

carthorse

Version:

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

528 lines β€’ 25.5 kB
"use strict"; 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