From f8a6b9eb96fc397cc1e8814478c892eb0724d40b Mon Sep 17 00:00:00 2001 From: Mischa Date: Mon, 22 Jun 2026 23:22:48 +0200 Subject: [PATCH] feat: add parseGpxFiles and export haversineKm from MapUtils --- themes/intotheeast/js/maplibre-utils.js | 89 +++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/themes/intotheeast/js/maplibre-utils.js b/themes/intotheeast/js/maplibre-utils.js index c0b3511..4e12689 100644 --- a/themes/intotheeast/js/maplibre-utils.js +++ b/themes/intotheeast/js/maplibre-utils.js @@ -330,9 +330,98 @@ }); } + /* + * Parse one or more GPX files and compute aggregate cycling statistics. + * urls: array of GPX file URL strings + * callback: called once with { distance, eleGain, eleLoss, highest, lowest, movingTime, avgSpeed } + * or { error: 'no files' } if urls is empty. + * + * distance/eleGain/eleLoss in raw units (km / metres). + * movingTime: "H:MM" string. avgSpeed: km/h number. + */ + function parseGpxFiles(urls, callback) { + var pending = urls.length; + var fileResults = new Array(urls.length); + if (pending === 0) { callback({ error: 'no files' }); return; } + + urls.forEach(function (url, idx) { + fetch(url) + .then(function (r) { return r.text(); }) + .then(function (text) { + var xml = new DOMParser().parseFromString(text, 'text/xml'); + var pts = []; + xml.querySelectorAll('trkpt').forEach(function (pt) { + var eleEl = pt.querySelector('ele'); + var timeEl = pt.querySelector('time'); + pts.push({ + lat: parseFloat(pt.getAttribute('lat')), + lon: parseFloat(pt.getAttribute('lon')), + ele: eleEl ? parseFloat(eleEl.textContent) : NaN, + time: timeEl ? timeEl.textContent : null + }); + }); + fileResults[idx] = pts; + if (--pending === 0) computeAndCallback(); + }) + .catch(function (err) { + console.warn('GPX load failed:', url, err); + fileResults[idx] = []; + if (--pending === 0) computeAndCallback(); + }); + }); + + function computeAndCallback() { + var totalDistance = 0, totalEleGain = 0, totalEleLoss = 0; + var globalHighest = NaN, globalLowest = NaN, totalMovingTime = 0; + + fileResults.forEach(function (pts) { + if (!pts || pts.length < 2) return; + /* Include first point of each file in elevation range */ + if (!isNaN(pts[0].ele)) { + if (isNaN(globalHighest) || pts[0].ele > globalHighest) globalHighest = pts[0].ele; + if (isNaN(globalLowest) || pts[0].ele < globalLowest) globalLowest = pts[0].ele; + } + for (var i = 1; i < pts.length; i++) { + var p0 = pts[i - 1], p1 = pts[i]; + totalDistance += haversineKm(p0.lat, p0.lon, p1.lat, p1.lon); + if (!isNaN(p0.ele) && !isNaN(p1.ele)) { + var dEle = p1.ele - p0.ele; + if (dEle > 0) totalEleGain += dEle; + if (dEle < 0) totalEleLoss += (-dEle); + if (isNaN(globalHighest) || p1.ele > globalHighest) globalHighest = p1.ele; + if (isNaN(globalLowest) || p1.ele < globalLowest) globalLowest = p1.ele; + } + if (p0.time && p1.time) { + var dtHrs = (Date.parse(p1.time) - Date.parse(p0.time)) / 3600000; + if (dtHrs > 0 && (haversineKm(p0.lat, p0.lon, p1.lat, p1.lon) / dtHrs) >= 1) { + totalMovingTime += dtHrs; + } + } + } + }); + + var avgSpeed = totalMovingTime > 0 ? totalDistance / totalMovingTime : 0; + var movHours = Math.floor(totalMovingTime); + var movMins = Math.round((totalMovingTime - movHours) * 60); + if (movMins === 60) { movHours++; movMins = 0; } + + callback({ + distance: totalDistance, + eleGain: totalEleGain, + eleLoss: totalEleLoss, + highest: globalHighest, + lowest: globalLowest, + movingTime: movHours + ':' + (movMins < 10 ? '0' : '') + movMins, + avgSpeed: avgSpeed + }); + } + } + global.MapUtils = { MAP_STYLE: MAP_STYLE, ACCENT: ACCENT, + haversineKm: haversineKm, + parseGpxFiles: parseGpxFiles, addJourneyLine: addJourneyLine, addJourneySegments: addJourneySegments, buildJourneySegments: buildJourneySegments,