fix: compute GPX stats per-file to avoid spurious inter-track segments

Both stats.html.twig and trip.html.twig previously flattened all GPX
trackpoints into a single masterPts array before computing haversine
distance, elevation, and moving time. This caused the junction between
file N's last point and file N+1's first point to be treated as a real
segment — e.g. Florence→coast (~79 km, ~42 h) for Italy's 3-file demo
data, overstating distance and moving time significantly.

Fix: compute all metrics within each file independently and sum the
results. fileResults collection and callback consumption are unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WPJztrVGbwic2xTG7G9fjM
This commit is contained in:
2026-06-19 23:13:08 +02:00
parent 1a247e1889
commit 8152fe79b6
2 changed files with 50 additions and 51 deletions
+9 -12
View File
@@ -162,27 +162,24 @@ if (GPX_URLS.length > 0) {
}); });
fileResults[idx] = pts; fileResults[idx] = pts;
pending--; pending--;
if (pending === 0) { computeDistance(); } if (pending === 0) { computeTotalDistance(); }
}) })
.catch(function(err) { .catch(function(err) {
console.warn('GPX load failed:', url, err); console.warn('GPX load failed:', url, err);
fileResults[idx] = []; fileResults[idx] = [];
pending--; pending--;
if (pending === 0) { computeDistance(); } if (pending === 0) { computeTotalDistance(); }
}); });
}); });
function computeDistance() { function computeTotalDistance() {
var masterPts = [];
fileResults.forEach(function(pts) {
if (pts) { pts.forEach(function(p) { masterPts.push(p); }); }
});
var total = 0; var total = 0;
for (var i = 1; i < masterPts.length; i++) { fileResults.forEach(function(pts) {
total += haversine(masterPts[i-1].lat, masterPts[i-1].lon, for (var i = 1; i < pts.length; i++) {
masterPts[i].lat, masterPts[i].lon); total += haversine(pts[i-1].lat, pts[i-1].lon, pts[i].lat, pts[i].lon);
} }
distEl.textContent = masterPts.length < 2 ? '—' : Math.round(total).toLocaleString(); });
distEl.textContent = total > 0 ? Math.round(total).toLocaleString() : '—';
} }
} else { } else {
// Mode B: sum haversine between consecutive entry lat/lng points // Mode B: sum haversine between consecutive entry lat/lng points
+41 -39
View File
@@ -437,50 +437,52 @@ function parseGpxFiles(urls, callback) {
}); });
function computeAndCallback() { function computeAndCallback() {
var masterPts = []; var totalDistance = 0, totalEleGain = 0, totalEleLoss = 0;
var globalHighest = NaN, globalLowest = NaN, totalMovingTime = 0;
fileResults.forEach(function(pts) { fileResults.forEach(function(pts) {
if (pts) { pts.forEach(function(p) { masterPts.push(p); }); } if (!pts || pts.length < 2) return;
}); for (var i = 1; i < pts.length; i++) {
var n = masterPts.length; var p0 = pts[i-1], p1 = pts[i];
if (n < 2) { callback({ distance: 0 }); return; } var d = haversineKm(p0.lat, p0.lon, p1.lat, p1.lon);
var distance = 0, eleGain = 0, eleLoss = 0; totalDistance += d;
var highest = NaN, lowest = NaN, movingTime = 0;
for (var i = 1; i < n; i++) { if (!isNaN(p0.ele) && !isNaN(p1.ele)) {
var p0 = masterPts[i-1], p1 = masterPts[i]; var dEle = p1.ele - p0.ele;
var d = haversineKm(p0.lat, p0.lon, p1.lat, p1.lon); if (dEle > 1) totalEleGain += dEle - 1;
distance += d; if (dEle < -1) totalEleLoss += (-dEle) - 1;
if (!isNaN(p0.ele) && !isNaN(p1.ele)) { if (isNaN(globalHighest) || p1.ele > globalHighest) globalHighest = p1.ele;
var dEle = p1.ele - p0.ele; if (isNaN(globalLowest) || p1.ele < globalLowest) globalLowest = p1.ele;
if (dEle > 1) eleGain += dEle - 1; }
else if (dEle < -1) eleLoss += (-dEle) - 1;
if (isNaN(highest) || p1.ele > highest) highest = p1.ele; if (p0.time && p1.time) {
if (isNaN(lowest) || p1.ele < lowest) lowest = p1.ele; var dtHrs = (Date.parse(p1.time) - Date.parse(p0.time)) / 3600000;
} if (dtHrs > 0) {
if (p0.time && p1.time) { var speed = d / dtHrs;
var dtHrs = (Date.parse(p1.time) - Date.parse(p0.time)) / 3600000; if (speed >= 1) totalMovingTime += dtHrs;
if (dtHrs > 0) { }
var speed = d / dtHrs;
if (speed >= 1) movingTime += dtHrs;
} }
} }
} // include first point of each file in elevation range
// include first point in elevation range if (pts.length > 0 && !isNaN(pts[0].ele)) {
if (!isNaN(masterPts[0].ele)) { if (isNaN(globalHighest) || pts[0].ele > globalHighest) globalHighest = pts[0].ele;
if (isNaN(highest) || masterPts[0].ele > highest) highest = masterPts[0].ele; if (isNaN(globalLowest) || pts[0].ele < globalLowest) globalLowest = pts[0].ele;
if (isNaN(lowest) || masterPts[0].ele < lowest) lowest = masterPts[0].ele; }
} });
var avgSpeed = movingTime > 0 ? distance / movingTime : 0;
var movHours = Math.floor(movingTime); var avgSpeed = totalMovingTime > 0 ? totalDistance / totalMovingTime : 0;
var movMins = Math.round((movingTime - movHours) * 60); var movHours = Math.floor(totalMovingTime);
var movMins = Math.round((totalMovingTime - movHours) * 60);
if (movMins === 60) { movHours++; movMins = 0; } if (movMins === 60) { movHours++; movMins = 0; }
callback({ callback({
distance: distance, distance: totalDistance,
eleGain: eleGain, eleGain: totalEleGain,
eleLoss: eleLoss, eleLoss: totalEleLoss,
highest: highest, highest: globalHighest,
lowest: lowest, lowest: globalLowest,
movingTime: movHours + ':' + (movMins < 10 ? '0' : '') + movMins, movingTime: movHours + ':' + (movMins < 10 ? '0' : '') + movMins,
avgSpeed: avgSpeed avgSpeed: avgSpeed
}); });
} }
} }