snowva-backend / src /update_elevation.ts
zerito's picture
Deploy backend update Tue Dec 9 15:53:43 CET 2025
044aa5c
Raw
History Blame Contribute Delete
7.39 kB
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();