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;
pending--;
if (pending === 0) { computeDistance(); }
if (pending === 0) { computeTotalDistance(); }
})
.catch(function(err) {
console.warn('GPX load failed:', url, err);
fileResults[idx] = [];
pending--;
if (pending === 0) { computeDistance(); }
if (pending === 0) { computeTotalDistance(); }
});
});
function computeDistance() {
var masterPts = [];
fileResults.forEach(function(pts) {
if (pts) { pts.forEach(function(p) { masterPts.push(p); }); }
});
function computeTotalDistance() {
var total = 0;
for (var i = 1; i < masterPts.length; i++) {
total += haversine(masterPts[i-1].lat, masterPts[i-1].lon,
masterPts[i].lat, masterPts[i].lon);
}
distEl.textContent = masterPts.length < 2 ? '—' : Math.round(total).toLocaleString();
fileResults.forEach(function(pts) {
for (var i = 1; i < pts.length; i++) {
total += haversine(pts[i-1].lat, pts[i-1].lon, pts[i].lat, pts[i].lon);
}
});
distEl.textContent = total > 0 ? Math.round(total).toLocaleString() : '—';
}
} else {
// Mode B: sum haversine between consecutive entry lat/lng points
+41 -39
View File
@@ -437,50 +437,52 @@ function parseGpxFiles(urls, callback) {
});
function computeAndCallback() {
var masterPts = [];
var totalDistance = 0, totalEleGain = 0, totalEleLoss = 0;
var globalHighest = NaN, globalLowest = NaN, totalMovingTime = 0;
fileResults.forEach(function(pts) {
if (pts) { pts.forEach(function(p) { masterPts.push(p); }); }
});
var n = masterPts.length;
if (n < 2) { callback({ distance: 0 }); return; }
var distance = 0, eleGain = 0, eleLoss = 0;
var highest = NaN, lowest = NaN, movingTime = 0;
for (var i = 1; i < n; i++) {
var p0 = masterPts[i-1], p1 = masterPts[i];
var d = haversineKm(p0.lat, p0.lon, p1.lat, p1.lon);
distance += d;
if (!isNaN(p0.ele) && !isNaN(p1.ele)) {
var dEle = p1.ele - p0.ele;
if (dEle > 1) eleGain += dEle - 1;
else if (dEle < -1) eleLoss += (-dEle) - 1;
if (isNaN(highest) || p1.ele > highest) highest = p1.ele;
if (isNaN(lowest) || p1.ele < lowest) lowest = p1.ele;
}
if (p0.time && p1.time) {
var dtHrs = (Date.parse(p1.time) - Date.parse(p0.time)) / 3600000;
if (dtHrs > 0) {
var speed = d / dtHrs;
if (speed >= 1) movingTime += dtHrs;
if (!pts || pts.length < 2) return;
for (var i = 1; i < pts.length; i++) {
var p0 = pts[i-1], p1 = pts[i];
var d = haversineKm(p0.lat, p0.lon, p1.lat, p1.lon);
totalDistance += d;
if (!isNaN(p0.ele) && !isNaN(p1.ele)) {
var dEle = p1.ele - p0.ele;
if (dEle > 1) totalEleGain += dEle - 1;
if (dEle < -1) totalEleLoss += (-dEle) - 1;
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) {
var speed = d / dtHrs;
if (speed >= 1) totalMovingTime += dtHrs;
}
}
}
}
// include first point in elevation range
if (!isNaN(masterPts[0].ele)) {
if (isNaN(highest) || masterPts[0].ele > highest) highest = masterPts[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 movMins = Math.round((movingTime - movHours) * 60);
// include first point of each file in elevation range
if (pts.length > 0 && !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;
}
});
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: distance,
eleGain: eleGain,
eleLoss: eleLoss,
highest: highest,
lowest: lowest,
movingTime: movHours + ':' + (movMins < 10 ? '0' : '') + movMins,
avgSpeed: avgSpeed
distance: totalDistance,
eleGain: totalEleGain,
eleLoss: totalEleLoss,
highest: globalHighest,
lowest: globalLowest,
movingTime: movHours + ':' + (movMins < 10 ? '0' : '') + movMins,
avgSpeed: avgSpeed
});
}
}