import sql from './db'; // Open-Meteo Elevation API // https://open-meteo.com/en/docs/elevation-api const API_URL = 'https://api.open-meteo.com/v1/elevation'; async function updateElevations() { try { console.log('Fetching routes without elevation profile...'); // Debug: Check total routes const count = await sql`SELECT count(*) FROM routes`; console.log(`Total routes in DB: ${count[0].count}`); // Debug: Check one route // const sample = await sql`SELECT id, path, elevation_profile FROM routes LIMIT 1`; // if (sample.length > 0) { // console.log('Sample route:', JSON.stringify(sample[0], null, 2)); // console.log('Type of elevation_profile:', typeof sample[0].elevation_profile); // console.log('Value of elevation_profile:', sample[0].elevation_profile); // } // Fetch a batch of routes to update // We order by ID to be deterministic const routes = await sql` SELECT id, name, path FROM routes ORDER BY id ASC LIMIT 50 `; console.log(`Found ${routes.length} routes to update.`); for (const route of routes) { console.log(`Processing route: ${route.name} (ID: ${route.id})`); try { let geojson = route.path; if (typeof geojson === 'string') { geojson = JSON.parse(geojson); } const coordinates = geojson.coordinates; // Array of [lon, lat] if (!coordinates || coordinates.length === 0) { console.log('No coordinates found, skipping.'); continue; } // Open-Meteo accepts max 100 locations per request in free tier usually, // but let's check documentation. It says "Multiple locations can be specified". // To be safe and avoid URL length limits, we might need to chunk if the path is very long. // For now, let's sample or chunk. // Let's limit to 100 points for the profile to keep it simple and fast // If more than 100 points, we sample. const sampledCoordinates = sampleCoordinates(coordinates, 100); const lats = sampledCoordinates.map(c => c[1]); const lngs = sampledCoordinates.map(c => c[0]); const url = `${API_URL}?latitude=${lats.join(',')}&longitude=${lngs.join(',')}`; const response = await fetch(url); if (!response.ok) { throw new Error(`API Error: ${response.statusText} (${response.status})`); } const data = await response.json(); const elevations = data.elevation; if (!elevations || elevations.length !== sampledCoordinates.length) { console.warn('Mismatch in elevation data length'); continue; } // Smooth elevations to avoid noisy data causing huge elevation gains const smoothedElevations = smoothElevations(elevations, 5); // Create elevation profile // We can calculate distance along the path to create a proper profile { distance, elevation } const profile = []; let totalDistance = 0; for (let i = 0; i < sampledCoordinates.length; i++) { if (i > 0) { const prev = sampledCoordinates[i - 1]; const curr = sampledCoordinates[i]; const distKm = getDistanceFromLatLonInKm(prev[1], prev[0], curr[1], curr[0]); totalDistance += distKm * 1000; // Convert to meters } profile.push({ distance: Math.round(totalDistance * 10) / 10, // in meters, rounded elevation: smoothedElevations[i] // in meters }); } // Update the route in the database // We are NOT updating the main 'path' geometry with 3D coordinates here to preserve the original detailed geometry // We are just saving the profile. // postgres.js handles JSON serialization automatically if we pass the object const result = await sql` UPDATE routes SET elevation_profile = ${sql.json(profile)} WHERE id = ${route.id} `; console.log(`Updated elevation profile for route ${route.id}. Points: ${profile.length}. Result: ${result.count}`); // Log the full profile for verification console.log(`Elevation Profile Data for Route ${route.id}:`); console.log(JSON.stringify(profile, null, 2)); // Respect API rate limits // Open-Meteo free tier: < 600 calls/minute (10/sec). // We'll be conservative with 2 second delay. await new Promise(resolve => setTimeout(resolve, 2000)); } catch (err: any) { if (err.message && err.message.includes('429')) { console.warn('Rate limit hit, waiting 10 seconds...'); await new Promise(resolve => setTimeout(resolve, 10000)); } else { console.error(`Error updating route ${route.id}:`, err); } } } console.log('Elevation update completed.'); } catch (err) { console.error('Error in updateElevations:', err); } } function sampleCoordinates(coordinates: number[][], maxPoints: number): number[][] { if (coordinates.length <= maxPoints) return coordinates; const result = []; const step = (coordinates.length - 1) / (maxPoints - 1); for (let i = 0; i < maxPoints; i++) { const index = Math.round(i * step); result.push(coordinates[index]); } return result; } // Haversine formula for distance function getDistanceFromLatLonInKm(lat1: number, lon1: number, lat2: number, lon2: number) { const R = 6371; // Radius of the earth in km const dLat = deg2rad(lat2 - lat1); const dLon = deg2rad(lon2 - lon1); const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); const d = R * c; // Distance in km return d; } function deg2rad(deg: number) { return deg * (Math.PI / 180); } function smoothElevations(elevations: number[], windowSize: number = 3): number[] { const smoothed = []; for (let i = 0; i < elevations.length; i++) { let sum = 0; let count = 0; for (let j = Math.max(0, i - Math.floor(windowSize / 2)); j <= Math.min(elevations.length - 1, i + Math.floor(windowSize / 2)); j++) { sum += elevations[j]; count++; } smoothed.push(sum / count); } return smoothed; } updateElevations();