UNPKG

carthorse

Version:

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

591 lines 28.4 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; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SQLiteExportStrategy = void 0; const better_sqlite3_1 = __importDefault(require("better-sqlite3")); const fs = __importStar(require("fs")); class SQLiteExportStrategy { constructor(pgClient, config, stagingSchema) { this.pgClient = pgClient; this.config = config; this.stagingSchema = stagingSchema; } log(message) { if (this.config.verbose) { console.log(`[SQLite Export] ${message}`); } } /** * Export all data from staging schema to SQLite */ async exportFromStaging() { const result = { trailsExported: 0, nodesExported: 0, edgesExported: 0, dbSizeMB: 0, isValid: false, errors: [] }; try { this.log(`📦 Starting SQLite export to: ${this.config.outputPath}`); // Create SQLite database const db = new better_sqlite3_1.default(this.config.outputPath); // Create tables this.createSqliteTables(db); // Export data if (this.config.includeTrails !== false) { result.trailsExported = await this.exportTrails(db); } if (this.config.includeNodes !== false) { result.nodesExported = await this.exportNodes(db); } if (this.config.includeEdges !== false) { result.edgesExported = await this.exportEdges(db); } if (this.config.includeRecommendations !== false) { result.recommendationsExported = await this.exportRecommendations(db); } // Export route_trails (always include if recommendations are included) if (this.config.includeRecommendations !== false) { result.routeTrailsExported = await this.exportRouteTrails(db); } // Insert metadata await this.insertRegionMetadata(db, result); this.insertSchemaVersion(db); // Close database db.close(); // Calculate file size const stats = fs.statSync(this.config.outputPath); result.dbSizeMB = stats.size / (1024 * 1024); // Defer final summary printing to orchestrator to avoid duplicate logs this.log(`✅ SQLite export completed (summary will be printed by orchestrator)`); result.isValid = true; } catch (error) { const errorMsg = `SQLite export failed: ${error instanceof Error ? error.message : String(error)}`; console.error(`❌ ${errorMsg}`); result.errors.push(errorMsg); result.isValid = false; } return result; } /** * Create SQLite tables */ createSqliteTables(db) { // Create trails table (v14 schema) db.exec(` CREATE TABLE IF NOT EXISTS trails ( id INTEGER PRIMARY KEY AUTOINCREMENT, app_uuid TEXT UNIQUE NOT NULL, name TEXT NOT NULL, region TEXT NOT NULL, osm_id TEXT, osm_type TEXT, length_km REAL CHECK(length_km > 0) NOT NULL, elevation_gain REAL CHECK(elevation_gain >= 0) NOT NULL, elevation_loss REAL CHECK(elevation_loss >= 0) NOT NULL, max_elevation REAL CHECK(max_elevation > 0) NOT NULL, min_elevation REAL CHECK(min_elevation > 0) NOT NULL, avg_elevation REAL CHECK(avg_elevation > 0) NOT NULL, difficulty TEXT CHECK(difficulty IN ('easy', 'moderate', 'hard', 'expert', 'unknown')), surface_type TEXT, trail_type TEXT, geojson TEXT NOT NULL, bbox_min_lng REAL, bbox_max_lng REAL, bbox_min_lat REAL, bbox_max_lat REAL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `); // Create routing_nodes table (v14 schema - integer IDs for efficiency) db.exec(` CREATE TABLE IF NOT EXISTS routing_nodes ( id INTEGER PRIMARY KEY, lat REAL NOT NULL, lng REAL NOT NULL, elevation REAL, node_type TEXT CHECK(node_type IN ('intersection', 'endpoint')) NOT NULL, connected_trails TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `); // Create routing_edges table (v14 schema) db.exec(` CREATE TABLE IF NOT EXISTS routing_edges ( id INTEGER PRIMARY KEY AUTOINCREMENT, source INTEGER NOT NULL, target INTEGER NOT NULL, trail_id TEXT, trail_name TEXT NOT NULL, length_km REAL CHECK(length_km > 0) NOT NULL, elevation_gain REAL CHECK(elevation_gain >= 0), elevation_loss REAL CHECK(elevation_loss >= 0), geojson TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `); // Create route_recommendations table (v14 schema) db.exec(` CREATE TABLE IF NOT EXISTS route_recommendations ( id INTEGER PRIMARY KEY AUTOINCREMENT, route_uuid TEXT UNIQUE NOT NULL, region TEXT NOT NULL, input_length_km REAL CHECK(input_length_km > 0), input_elevation_gain REAL CHECK(input_elevation_gain >= 0), recommended_length_km REAL CHECK(recommended_length_km > 0), recommended_elevation_gain REAL CHECK(recommended_elevation_gain >= 0), route_score REAL CHECK(route_score >= 0 AND route_score <= 100), route_type TEXT CHECK(route_type IN ('out-and-back', 'loop', 'lollipop', 'point-to-point', 'unknown')) NOT NULL, route_name TEXT, route_shape TEXT CHECK(route_shape IN ('loop', 'out-and-back', 'lollipop', 'point-to-point')) NOT NULL, trail_count INTEGER CHECK(trail_count >= 1) NOT NULL, route_path TEXT NOT NULL, route_edges TEXT NOT NULL, similarity_score REAL CHECK(similarity_score >= 0 AND similarity_score <= 1) NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, complete_route_data TEXT ) `); // Create route_trails table (v14 schema) db.exec(` CREATE TABLE IF NOT EXISTS route_trails ( id INTEGER PRIMARY KEY AUTOINCREMENT, route_uuid TEXT NOT NULL, trail_id TEXT NOT NULL, trail_name TEXT NOT NULL, segment_order INTEGER NOT NULL, segment_length_km REAL CHECK(segment_length_km > 0), segment_elevation_gain REAL CHECK(segment_elevation_gain >= 0), trail_type TEXT, surface TEXT, difficulty TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `); // Create region_metadata table (v14 schema) db.exec(` CREATE TABLE IF NOT EXISTS region_metadata ( id INTEGER PRIMARY KEY AUTOINCREMENT, region TEXT UNIQUE NOT NULL, total_trails INTEGER CHECK(total_trails >= 0), total_nodes INTEGER CHECK(total_nodes >= 0), total_edges INTEGER CHECK(total_edges >= 0), total_routes INTEGER CHECK(total_routes >= 0), bbox_min_lat REAL, bbox_max_lat REAL, bbox_min_lng REAL, bbox_max_lng REAL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `); // Create schema_version table (v14 schema) db.exec(` CREATE TABLE IF NOT EXISTS schema_version ( id INTEGER PRIMARY KEY AUTOINCREMENT, version INTEGER NOT NULL, description TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `); // Create indexes for optimal performance db.exec(` CREATE INDEX IF NOT EXISTS idx_trails_app_uuid ON trails(app_uuid); CREATE INDEX IF NOT EXISTS idx_trails_region ON trails(region); CREATE INDEX IF NOT EXISTS idx_trails_name ON trails(name); CREATE INDEX IF NOT EXISTS idx_trails_length ON trails(length_km); CREATE INDEX IF NOT EXISTS idx_trails_elevation ON trails(elevation_gain); CREATE INDEX IF NOT EXISTS idx_routing_nodes_location ON routing_nodes(lat, lng); CREATE INDEX IF NOT EXISTS idx_routing_nodes_coords ON routing_nodes(lat, lng); CREATE INDEX IF NOT EXISTS idx_routing_nodes_elevation ON routing_nodes(elevation); CREATE INDEX IF NOT EXISTS idx_routing_nodes_type ON routing_nodes(node_type); CREATE INDEX IF NOT EXISTS idx_routing_edges_source ON routing_edges(source); CREATE INDEX IF NOT EXISTS idx_routing_edges_target ON routing_edges(target); CREATE INDEX IF NOT EXISTS idx_routing_edges_source_target ON routing_edges(source, target); CREATE INDEX IF NOT EXISTS idx_routing_edges_trail ON routing_edges(trail_id); CREATE INDEX IF NOT EXISTS idx_routing_edges_length ON routing_edges(length_km); CREATE INDEX IF NOT EXISTS idx_route_recommendations_region ON route_recommendations(region); CREATE INDEX IF NOT EXISTS idx_route_recommendations_uuid ON route_recommendations(route_uuid); CREATE INDEX IF NOT EXISTS idx_route_trails_route_uuid ON route_trails(route_uuid); CREATE INDEX IF NOT EXISTS idx_route_trails_trail_id ON route_trails(trail_id); `); // Enable WAL mode for better performance db.exec(` PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL; PRAGMA cache_size = -64000; PRAGMA temp_store = MEMORY; `); } /** * Export trails from staging schema */ async exportTrails(db) { try { // Only export from split_trails - no fallback to original trails const trailsResult = await this.pgClient.query(` SELECT app_uuid, name, osm_id, trail_type, surface as surface_type, CASE WHEN difficulty = 'unknown' THEN 'moderate' ELSE difficulty END as difficulty, length_km, elevation_gain, elevation_loss, max_elevation, min_elevation, avg_elevation, bbox_min_lng, bbox_max_lng, bbox_min_lat, bbox_max_lat, original_trail_id, ST_AsGeoJSON(geometry, 6, 0) as geojson FROM ${this.stagingSchema}.split_trails WHERE geometry IS NOT NULL ORDER BY name `); if (trailsResult.rows.length === 0) { this.log(`❌ No trails found in split_trails table. Trail splitting must be completed before export.`); throw new Error('split_trails table is empty. Trail splitting must be completed before export.'); } // Check if we have valid GeoJSON data const trailsWithGeoJSON = trailsResult.rows.filter(trail => trail.geojson && trail.geojson !== 'null'); const trailsWithoutGeoJSON = trailsResult.rows.filter(trail => !trail.geojson || trail.geojson === 'null'); this.log(`📊 Found ${trailsResult.rows.length} total split trails`); this.log(`📊 Trails with GeoJSON: ${trailsWithGeoJSON.length}`); this.log(`📊 Trails without GeoJSON: ${trailsWithoutGeoJSON.length}`); if (trailsWithoutGeoJSON.length > 0) { this.log(`⚠️ Some trails are missing GeoJSON data. This may cause API validation issues.`); } // Insert trails into SQLite const insertTrails = db.prepare(` INSERT OR REPLACE INTO trails ( app_uuid, name, region, osm_id, trail_type, surface_type, difficulty, length_km, elevation_gain, elevation_loss, max_elevation, min_elevation, avg_elevation, bbox_min_lng, bbox_max_lng, bbox_min_lat, bbox_max_lat, geojson ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); const insertMany = db.transaction((trails) => { for (const trail of trails) { try { // Use the original app_uuid from split_trails to maintain 1:1 mapping with edges insertTrails.run(trail.app_uuid, trail.name || '', this.config.region, trail.osm_id || '', trail.trail_type || 'hiking', trail.surface_type || 'dirt', trail.difficulty || 'moderate', trail.length_km || 0, trail.elevation_gain || 0, trail.elevation_loss || 0, trail.max_elevation || 0, trail.min_elevation || 0, trail.avg_elevation || 0, trail.bbox_min_lng || 0, trail.bbox_max_lng || 0, trail.bbox_min_lat || 0, trail.bbox_max_lat || 0, trail.geojson || '' // Use actual GeoJSON from geometry ); } catch (error) { this.log(`⚠️ Failed to insert trail ${trail.app_uuid}: ${error}`); } } }); insertMany(trailsResult.rows); return trailsResult.rows.length; } catch (error) { this.log(`❌ Error during trails export: ${error}`); throw error; } } /** * Export nodes from staging schema (using ways_noded_vertices_pgr) */ async exportNodes(db) { try { // Get only connected nodes from ways_noded_vertices_pgr const nodesResult = await this.pgClient.query(` SELECT DISTINCT v.id, ST_X(v.the_geom) as lng, ST_Y(v.the_geom) as lat, COALESCE(ST_Z(v.the_geom), 0) as elevation, CASE WHEN v.cnt > 2 THEN 'intersection' ELSE 'endpoint' END as node_type, '' as connected_trails FROM ${this.stagingSchema}.ways_noded_vertices_pgr v WHERE v.id 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 ) ORDER BY v.id `); if (nodesResult.rows.length === 0) { this.log(`⚠️ No nodes found in ways_noded_vertices_pgr`); return 0; } this.log(`📊 Found ${nodesResult.rows.length} routing nodes`); // Insert nodes into SQLite (use integer IDs for efficiency and referential integrity) const insertNodes = db.prepare(` INSERT INTO routing_nodes ( id, lat, lng, elevation, node_type, connected_trails ) VALUES (?, ?, ?, ?, ?, ?) `); const insertMany = db.transaction((nodes) => { for (const node of nodes) { insertNodes.run(node.id, // Use original PostgreSQL ID for referential integrity node.lat, node.lng, node.elevation, node.node_type, node.connected_trails); } }); insertMany(nodesResult.rows); return nodesResult.rows.length; } catch (error) { this.log(`⚠️ ways_noded_vertices_pgr table not found, skipping nodes export: ${error}`); return 0; } } /** * Export edges from staging schema (using ways_noded with simplification) * Also cleans up orphaned nodes as part of the same atomic operation */ async exportEdges(db) { try { // Export edges (duplicates already removed in PostgreSQL) const edgesResult = await this.pgClient.query(` SELECT w.source, w.target, w.app_uuid as trail_id, w.name as trail_name, COALESCE(w.length_km, 0) as length_km, COALESCE(w.elevation_gain, 0) as elevation_gain, COALESCE(w.elevation_loss, 0) as elevation_loss, -- Simplify geometry while preserving start/end points for routing connectivity ST_AsGeoJSON( ST_SimplifyPreserveTopology( ST_Force2D(w.the_geom), -- Convert to 2D for routing edges 0.0001 -- Simplify tolerance (about 10 meters) ) ) as geojson FROM ${this.stagingSchema}.ways_noded w WHERE w.source IS NOT NULL AND w.target IS NOT NULL ORDER BY w.source, w.target `); if (edgesResult.rows.length === 0) { this.log(`⚠️ No edges found in ways_noded`); return 0; } // Check if we have valid GeoJSON data const edgesWithGeoJSON = edgesResult.rows.filter(edge => edge.geojson && edge.geojson !== 'null'); const edgesWithoutGeoJSON = edgesResult.rows.filter(edge => !edge.geojson || edge.geojson === 'null'); this.log(`📊 Found ${edgesResult.rows.length} total edges`); this.log(`📊 Edges with GeoJSON: ${edgesWithGeoJSON.length}`); this.log(`📊 Edges without GeoJSON: ${edgesWithoutGeoJSON.length}`); if (edgesWithoutGeoJSON.length > 0) { this.log(`⚠️ Some edges are missing GeoJSON data. This may cause API validation issues.`); } // Insert edges into SQLite (let SQLite auto-generate id to avoid duplicate ID conflicts) const insertEdges = db.prepare(` INSERT INTO routing_edges ( source, target, trail_id, trail_name, length_km, elevation_gain, elevation_loss, geojson ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `); const insertMany = db.transaction((edges) => { for (const edge of edges) { insertEdges.run(edge.source, edge.target, edge.trail_id, edge.trail_name, edge.length_km, edge.elevation_gain, edge.elevation_loss, edge.geojson); } }); insertMany(edgesResult.rows); return edgesResult.rows.length; } catch (error) { this.log(`⚠️ ways_noded table not found, skipping edges export: ${error}`); return 0; } } /** * Export recommendations from staging schema */ async exportRecommendations(db) { try { this.log(`🔍 Looking for route_recommendations in staging schema: ${this.stagingSchema}`); this.log(`🔍 About to execute query on ${this.stagingSchema}.route_recommendations`); const recommendationsResult = await this.pgClient.query(` SELECT route_uuid, region, input_length_km, input_elevation_gain, recommended_length_km, recommended_elevation_gain, route_score, route_type, route_name, route_shape, trail_count, route_path, route_edges, similarity_score, created_at, complete_route_data FROM ${this.stagingSchema}.route_recommendations ORDER BY created_at DESC `); this.log(`✅ Query executed successfully, found ${recommendationsResult.rows.length} recommendations`); if (recommendationsResult.rows.length === 0) { this.log(`⚠️ No recommendations found in route_recommendations`); return 0; } // Insert recommendations into SQLite this.log(`🔍 Preparing SQLite INSERT statement for route_recommendations`); const insertRecommendations = db.prepare(` INSERT INTO route_recommendations ( route_uuid, region, input_length_km, input_elevation_gain, recommended_length_km, recommended_elevation_gain, route_score, route_type, route_name, route_shape, trail_count, route_path, route_edges, similarity_score, created_at, complete_route_data ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); this.log(`✅ SQLite INSERT statement prepared successfully`); const insertMany = db.transaction((recommendations) => { for (const rec of recommendations) { // Generate complete_route_data in the required format if not already present let completeRouteData = rec.complete_route_data; if (!completeRouteData) { completeRouteData = { routeId: rec.route_uuid, routeName: rec.route_name, routeType: rec.trail_count === 1 ? 'single' : 'multi', totalDistance: rec.recommended_length_km, totalElevationGain: rec.recommended_elevation_gain, routeShape: rec.route_shape, similarityScore: rec.similarity_score, trailSegments: rec.route_edges ? rec.route_edges.map((edge, index) => ({ trailId: edge.trail_id || edge.trail_uuid, appUuid: edge.app_uuid, osmId: edge.osm_id, name: edge.trail_name || edge.name, geometry: edge.geometry || edge.the_geom, distance: edge.distance_km || edge.length_km, elevationGain: edge.elevation_gain, elevationLoss: edge.elevation_loss })) : [], connectivity: { segmentConnections: [], routeContinuity: true, gaps: [] }, combinedPath: rec.route_path, combinedBbox: null, createdAt: rec.created_at ? (typeof rec.created_at === 'string' ? rec.created_at : rec.created_at.toISOString()) : new Date().toISOString(), region: rec.region, inputParameters: { targetDistance: rec.input_length_km, targetElevationGain: rec.input_elevation_gain, distanceTolerance: 10, elevationTolerance: 20 } }; } insertRecommendations.run(rec.route_uuid, rec.region, rec.input_length_km, rec.input_elevation_gain, rec.recommended_length_km, rec.recommended_elevation_gain, rec.route_score, rec.route_type, rec.route_name, rec.route_shape, rec.trail_count, rec.route_path ? JSON.stringify(rec.route_path) : null, rec.route_edges ? JSON.stringify(rec.route_edges) : null, rec.similarity_score, rec.created_at ? (typeof rec.created_at === 'string' ? rec.created_at : rec.created_at.toISOString()) : new Date().toISOString(), typeof completeRouteData === 'string' ? completeRouteData : JSON.stringify(completeRouteData)); } }); insertMany(recommendationsResult.rows); return recommendationsResult.rows.length; } catch (error) { this.log(`❌ Error during route_recommendations export: ${error}`); this.log(`❌ Error details: ${JSON.stringify(error, null, 2)}`); this.log(`⚠️ route_recommendations table not found in schema ${this.stagingSchema}, skipping recommendations export`); return 0; } } /** * Export route_trails from staging schema */ async exportRouteTrails(db) { try { const routeTrailsResult = await this.pgClient.query(` SELECT route_uuid, trail_id, trail_name, segment_order, segment_length_km, segment_elevation_gain, trail_type, surface, difficulty, created_at FROM ${this.stagingSchema}.route_trails ORDER BY route_uuid, segment_order `); if (routeTrailsResult.rows.length === 0) { this.log(`⚠️ No route trails found in route_trails`); return 0; } // Insert route trails into SQLite (let SQLite auto-generate id) const insertRouteTrails = db.prepare(` INSERT INTO route_trails ( route_uuid, trail_id, trail_name, segment_order, segment_length_km, segment_elevation_gain, trail_type, surface, difficulty, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); const insertMany = db.transaction((routeTrails) => { for (const rt of routeTrails) { insertRouteTrails.run(rt.route_uuid, rt.trail_id, rt.trail_name, rt.segment_order, rt.segment_length_km, rt.segment_elevation_gain, rt.trail_type, rt.surface, rt.difficulty, rt.created_at ? (typeof rt.created_at === 'string' ? rt.created_at : rt.created_at.toISOString()) : new Date().toISOString()); } }); insertMany(routeTrailsResult.rows); this.log(`✅ Exported ${routeTrailsResult.rows.length} route trail segments`); return routeTrailsResult.rows.length; } catch (error) { this.log(`⚠️ route_trails table not found in schema ${this.stagingSchema}, skipping route trails export`); return 0; } } /** * Insert region metadata */ async insertRegionMetadata(db, result) { // Get trail statistics from split_trails (no region column needed) const statsResult = await this.pgClient.query(` SELECT COUNT(*) as total_trails, SUM(length_km) as total_length_km, SUM(elevation_gain) as total_elevation_gain, MIN(bbox_min_lng) as bbox_min_lng, MAX(bbox_max_lng) as bbox_max_lng, MIN(bbox_min_lat) as bbox_min_lat, MAX(bbox_max_lat) as bbox_max_lat FROM ${this.stagingSchema}.split_trails `); const stats = statsResult.rows[0]; const insertMetadata = db.prepare(` INSERT OR REPLACE INTO region_metadata ( region, total_trails, total_nodes, total_edges, total_routes, bbox_min_lng, bbox_max_lng, bbox_min_lat, bbox_max_lat, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); insertMetadata.run(this.config.region, result.trailsExported, result.nodesExported, result.edgesExported, result.recommendationsExported || 0, stats.bbox_min_lng || 0, stats.bbox_max_lng || 0, stats.bbox_min_lat || 0, stats.bbox_max_lat || 0, new Date().toISOString()); } /** * Insert schema version */ insertSchemaVersion(db) { const insertVersion = db.prepare(` INSERT OR REPLACE INTO schema_version (version, description, created_at) VALUES (?, ?, ?) `); insertVersion.run(14, 'Carthorse SQLite Export v14.0 (Enhanced Route Recommendations + Trail Composition)', new Date().toISOString()); } } exports.SQLiteExportStrategy = SQLiteExportStrategy; //# sourceMappingURL=sqlite-export-strategy.js.map