// api_v1/scanner/parsers/parseGpx.js const fsp = require('fs/promises'); const xml2js = require('xml2js'); // npm install xml2js function toMillis(date) { if (!date) return null; const d = new Date(date); return isNaN(d.getTime()) ? null : d.getTime(); } function buildTrackPreviewFromGpxPoints(points, maxPoints = 300) { if (!Array.isArray(points)) return []; const clean = []; for (const p of points) { const lat = p.$?.lat != null ? Number(p.$.lat) : null; const lng = p.$?.lon != null ? Number(p.$.lon) : null; const alt = p.ele != null ? Number(p.ele) : null; if (!Number.isFinite(lat) || !Number.isFinite(lng)) continue; const point = { lat, lng }; if (Number.isFinite(alt)) { point.alt = alt; } clean.push(point); } if (clean.length <= maxPoints) return clean; const step = Math.ceil(clean.length / maxPoints); const preview = []; for (let i = 0; i < clean.length; i += step) { preview.push(clean[i]); } const last = clean[clean.length - 1]; const currentLast = preview[preview.length - 1]; if ( !currentLast || currentLast.lat !== last.lat || currentLast.lng !== last.lng ) { preview.push(last); } return preview; } async function parseGpx(absPath) { const xml = await fsp.readFile(absPath, 'utf8'); const parser = new xml2js.Parser({ explicitArray: false }); const gpx = await parser.parseStringPromise(xml); const root = gpx.gpx || gpx; const trk = Array.isArray(root.trk) ? root.trk[0] : root.trk; const name = trk?.name || null; const segments = trk?.trkseg ? (Array.isArray(trk.trkseg) ? trk.trkseg : [trk.trkseg]) : []; const points = []; for (const seg of segments) { const segPts = seg.trkpt ? (Array.isArray(seg.trkpt) ? seg.trkpt : [seg.trkpt]) : []; points.push(...segPts); } let minLat = null; let maxLat = null; let minLon = null; let maxLon = null; let minElev = null; let maxElev = null; let startTime = null; let endTime = null; let distance = null; for (const p of points) { const lat = p.$?.lat != null ? Number(p.$.lat) : null; const lon = p.$?.lon != null ? Number(p.$.lon) : null; const ele = p.ele != null ? Number(p.ele) : null; const time = p.time || null; if (Number.isFinite(lat) && Number.isFinite(lon)) { minLat = minLat == null ? lat : Math.min(minLat, lat); maxLat = maxLat == null ? lat : Math.max(maxLat, lat); minLon = minLon == null ? lon : Math.min(minLon, lon); maxLon = maxLon == null ? lon : Math.max(maxLon, lon); } if (Number.isFinite(ele)) { minElev = minElev == null ? ele : Math.min(minElev, ele); maxElev = maxElev == null ? ele : Math.max(maxElev, ele); } if (time) { if (!startTime) startTime = time; endTime = time; } } const start_time_millis = toMillis(startTime); const end_time_millis = toMillis(endTime); const duration_millis = start_time_millis && end_time_millis ? end_time_millis - start_time_millis : null; const centerLat = minLat != null && maxLat != null ? (minLat + maxLat) / 2 : null; const centerLon = minLon != null && maxLon != null ? (minLon + maxLon) / 2 : null; const trackPreview = buildTrackPreviewFromGpxPoints(points, 300); return { format: 'GPX', display_name: name, route_name: name, title: name, description: root.metadata?.desc || null, notes: null, sport: root.metadata?.type || null, activity_type: null, device_manufacturer: root.metadata?.author?.name || null, device_model: null, source_app: root.metadata?.author?.name || null, start_time_millis, end_time_millis, duration_millis, distance_meters: distance, elevation_gain_meters: null, elevation_loss_meters: null, min_elevation_meters: minElev, max_elevation_meters: maxElev, file_distance_meters: distance, file_elevation_gain_meters: null, computed_distance_meters: distance, computed_elevation_gain_meters: null, average_speed_mps: null, max_speed_mps: null, average_pace_sec_per_km: null, average_heart_rate: null, max_heart_rate: null, average_cadence: null, max_cadence: null, calories: null, power_avg: null, power_max: null, point_count: points.length, segment_count: segments.length, lap_count: 0, min_latitude: minLat, max_latitude: maxLat, min_longitude: minLon, max_longitude: maxLon, center_latitude: centerLat, center_longitude: centerLon, // Preview ridotta del percorso per i thumb della gallery track_preview: trackPreview, tags: [], diagnostics: { source: 'GPX', segments: segments.length, points: points.length, track_preview_points: trackPreview.length } }; } module.exports = parseGpx;