feat: add parseGpxFiles and export haversineKm from MapUtils

This commit is contained in:
2026-06-22 23:22:48 +02:00
parent 5923ba431a
commit f8a6b9eb96
+89
View File
@@ -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,