diff --git a/cmd/server/helpers_test.go b/cmd/server/helpers_test.go index 1b65bd99..9df42610 100644 --- a/cmd/server/helpers_test.go +++ b/cmd/server/helpers_test.go @@ -345,3 +345,29 @@ func TestWriteJSON(t *testing.T) { t.Errorf("expected 'value', got %v", body["key"]) } } + +func TestHaversineKm(t *testing.T) { + // Same point should be 0 + if d := haversineKm(37.0, -122.0, 37.0, -122.0); d != 0 { + t.Errorf("same point: expected 0, got %f", d) + } + + // SF to LA ~559km + d := haversineKm(37.7749, -122.4194, 34.0522, -118.2437) + if d < 550 || d > 570 { + t.Errorf("SF to LA: expected ~559km, got %f", d) + } + + // Symmetry + d1 := haversineKm(37.7749, -122.4194, 34.0522, -118.2437) + d2 := haversineKm(34.0522, -118.2437, 37.7749, -122.4194) + if d1 != d2 { + t.Errorf("not symmetric: %f vs %f", d1, d2) + } + + // Oslo to Stockholm ~415km (old Euclidean dLat*111, dLon*85 would give ~627km) + d = haversineKm(59.9, 10.7, 59.3, 18.0) + if d < 400 || d > 430 { + t.Errorf("Oslo to Stockholm: expected ~415km, got %f", d) + } +} diff --git a/public/analytics.js b/public/analytics.js index fd4eda3a..e0b32565 100644 --- a/public/analytics.js +++ b/public/analytics.js @@ -1488,9 +1488,9 @@ for (let i = 0; i < data.nodes.length - 1; i++) { const a = data.nodes[i], b = data.nodes[i+1]; if (a.lat && a.lon && b.lat && b.lon && !(a.lat===0&&a.lon===0) && !(b.lat===0&&b.lon===0)) { - const dLat = (a.lat - b.lat) * 111; - const dLon = (a.lon - b.lon) * 85; - const km = Math.sqrt(dLat*dLat + dLon*dLon); + const km = window.HopResolver && window.HopResolver.haversineKm + ? window.HopResolver.haversineKm(a.lat, a.lon, b.lat, b.lon) + : (() => { const R=6371, dLat=(b.lat-a.lat)*Math.PI/180, dLon=(b.lon-a.lon)*Math.PI/180, h=Math.sin(dLat/2)**2+Math.cos(a.lat*Math.PI/180)*Math.cos(b.lat*Math.PI/180)*Math.sin(dLon/2)**2; return R*2*Math.atan2(Math.sqrt(h),Math.sqrt(1-h)); })(); total += km; const cls = km > 200 ? 'color:var(--status-red);font-weight:bold' : km > 50 ? 'color:var(--status-yellow)' : 'color:var(--status-green)'; dists.push(`
${km < 1 ? (km*1000).toFixed(0)+'m' : km.toFixed(1)+'km'} ${esc(a.name)} → ${esc(b.name)}
`); diff --git a/public/hop-resolver.js b/public/hop-resolver.js index 8497a047..8e7eaee5 100644 --- a/public/hop-resolver.js +++ b/public/hop-resolver.js @@ -203,5 +203,5 @@ window.HopResolver = (function() { return nodesList.length > 0; } - return { init: init, resolve: resolve, ready: ready }; + return { init: init, resolve: resolve, ready: ready, haversineKm: haversineKm }; })(); diff --git a/public/index.html b/public/index.html index 14671416..5811e408 100644 --- a/public/index.html +++ b/public/index.html @@ -22,9 +22,9 @@ - - - + + + @@ -85,30 +85,30 @@
- - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test-frontend-helpers.js b/test-frontend-helpers.js index 313f6724..15f2ebee 100644 --- a/test-frontend-helpers.js +++ b/test-frontend-helpers.js @@ -564,6 +564,40 @@ console.log('\n=== hop-resolver.js ==='); }); } +// ===== haversineKm exposed from HopResolver (issue #433) ===== +console.log('\n=== haversineKm (hop-resolver.js) ==='); +{ + const ctx = makeSandbox(); + ctx.IATA_COORDS_GEO = {}; + loadInCtx(ctx, 'public/hop-resolver.js'); + const HR = ctx.window.HopResolver; + + test('haversineKm is exported', () => { + assert.strictEqual(typeof HR.haversineKm, 'function'); + }); + + test('haversineKm same point = 0', () => { + assert.strictEqual(HR.haversineKm(37.0, -122.0, 37.0, -122.0), 0); + }); + + test('haversineKm SF to LA ~559km', () => { + // San Francisco (37.7749, -122.4194) to Los Angeles (34.0522, -118.2437) + const d = HR.haversineKm(37.7749, -122.4194, 34.0522, -118.2437); + assert.ok(d > 550 && d < 570, `Expected ~559km, got ${d}`); + }); + + test('haversineKm differs from old Euclidean approximation', () => { + // The old code used dLat*111, dLon*85 which is inaccurate at high latitudes + // Oslo (59.9, 10.7) to Stockholm (59.3, 18.0) + const haversine = HR.haversineKm(59.9, 10.7, 59.3, 18.0); + const dLat = (59.9 - 59.3) * 111; + const dLon = (10.7 - 18.0) * 85; + const euclidean = Math.sqrt(dLat*dLat + dLon*dLon); + // Haversine should give ~415km, Euclidean ~627km (wrong because dLon*85 is wrong at 60° latitude) + assert.ok(Math.abs(haversine - euclidean) > 50, `Expected significant difference, haversine=${haversine.toFixed(1)}, euclidean=${euclidean.toFixed(1)}`); + }); +} + // ===== SNR/RSSI Number casting ===== { // These test the pattern used in observer-detail.js, home.js, traces.js, live.js