feat: add parseGpxFiles and export haversineKm from MapUtils
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user