Routes whose great circle crosses ±180° (e.g. Auckland → Fua'amotu, Tonga) rendered as a line spanning the whole map: greatCircleArc sampled positionAt, which wraps every sample's longitude into [−180, 180], so consecutive samples jumped +179.x → −179.x and MapLibre — which draws LineStrings as-is with no ±180 splitting — connected them across the globe.
Fix (lib/mapData.ts): new module-private unwrapLons re-expresses each arc's sample sequence in continuous longitudes (first sample keeps its raw lon; every later one is shifted by ±360 relative to the previous UNWRAPPED value until |Δlon| ≤ 180), applied inside greatCircleArc so all four line builders (routesGeoJson, queuedRoutesGeoJson, hoverRouteGeoJson, hoverTraceGeoJson) are fixed at once. One line stays one LineString — no splitting; out-of-range values render on MapLibre's adjacent world copies (renderWorldCopies is default-on). queuedRoutesGeoJson gets a second global pass after concatenation, since each leg unwraps relative to its own start airport and the joint can jump ±360. Non-crossing routes are byte-identical (the pass is a no-op when no |Δlon| > 180 exists).
Tests:
Unit +4 (lib/mapData.test.ts, real dataset pairs NZAA/NFTF/NZCH): eastward and westward crossings stay continuous, endpoints exact mod 360 with the first sample still the raw origin, sampled chord length ≈ haversine within 1%, non-crossing lons stay in [−180, 180], and a grounded plane's two-leg queue chain crossing ±180 stays continuous across the joint.
E2E +1 (e2e/hover.spec.ts): hovering an antimeridian-crossing package (hook-created NZAA → NFTF) draws one continuous trace — no rendered segment may contain a ±180 longitude jump. expectHoverRoute's endpoint check now compares longitudes on the circle (mod 360; identical for any pair within ±180, so existing routes are unaffected), and fitToAirports unwraps crossing pairs so their bounds follow the short arc across the date line instead of spanning the wrong side of the globe.
Known limitation (follow-up): plane markers still use the snapshot's wrapped lat/lon (lib/game/state.ts → positionAt), so a plane mid-crossing teleports 360° on its marker at the instant it passes ±180 while its route line stays continuous. Fixing needs airport coordinates inside planesGeoJson (signature change + Map.tsx call site) — deliberately out of scope here.
Verification (local, per AGENTS.md):tsc --noEmit clean; vitest run 359/359 passed (30 files); playwright test --list compiles and lists all 58 tests in 14 files. Full e2e suite runs in CI against the isolated dind stack.
Closes #120
Routes whose great circle crosses ±180° (e.g. Auckland → Fua'amotu, Tonga) rendered as a line spanning the whole map: `greatCircleArc` sampled `positionAt`, which wraps every sample's longitude into [−180, 180], so consecutive samples jumped +179.x → −179.x and MapLibre — which draws LineStrings as-is with no ±180 splitting — connected them across the globe.
**Fix (`lib/mapData.ts`):** new module-private `unwrapLons` re-expresses each arc's sample sequence in continuous longitudes (first sample keeps its raw lon; every later one is shifted by ±360 relative to the previous UNWRAPPED value until |Δlon| ≤ 180), applied inside `greatCircleArc` so all four line builders (`routesGeoJson`, `queuedRoutesGeoJson`, `hoverRouteGeoJson`, `hoverTraceGeoJson`) are fixed at once. One line stays one LineString — no splitting; out-of-range values render on MapLibre's adjacent world copies (`renderWorldCopies` is default-on). `queuedRoutesGeoJson` gets a second global pass after concatenation, since each leg unwraps relative to its own start airport and the joint can jump ±360. Non-crossing routes are byte-identical (the pass is a no-op when no |Δlon| > 180 exists).
**Tests:**
- Unit +4 (`lib/mapData.test.ts`, real dataset pairs NZAA/NFTF/NZCH): eastward and westward crossings stay continuous, endpoints exact mod 360 with the first sample still the raw origin, sampled chord length ≈ haversine within 1%, non-crossing lons stay in [−180, 180], and a grounded plane's two-leg queue chain crossing ±180 stays continuous across the joint.
- E2E +1 (`e2e/hover.spec.ts`): hovering an antimeridian-crossing package (hook-created NZAA → NFTF) draws one continuous trace — no rendered segment may contain a ±180 longitude jump. `expectHoverRoute`'s endpoint check now compares longitudes on the circle (mod 360; identical for any pair within ±180, so existing routes are unaffected), and `fitToAirports` unwraps crossing pairs so their bounds follow the short arc across the date line instead of spanning the wrong side of the globe.
**Known limitation (follow-up):** plane markers still use the snapshot's wrapped lat/lon (`lib/game/state.ts` → `positionAt`), so a plane mid-crossing teleports 360° on its marker at the instant it passes ±180 while its route line stays continuous. Fixing needs airport coordinates inside `planesGeoJson` (signature change + `Map.tsx` call site) — deliberately out of scope here.
**Verification (local, per AGENTS.md):** `tsc --noEmit` clean; `vitest run` 359/359 passed (30 files); `playwright test --list` compiles and lists all 58 tests in 14 files. Full e2e suite runs in CI against the isolated dind stack.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #120
Routes whose great circle crosses ±180° (e.g. Auckland → Fua'amotu, Tonga) rendered as a line spanning the whole map:
greatCircleArcsampledpositionAt, which wraps every sample's longitude into [−180, 180], so consecutive samples jumped +179.x → −179.x and MapLibre — which draws LineStrings as-is with no ±180 splitting — connected them across the globe.Fix (
lib/mapData.ts): new module-privateunwrapLonsre-expresses each arc's sample sequence in continuous longitudes (first sample keeps its raw lon; every later one is shifted by ±360 relative to the previous UNWRAPPED value until |Δlon| ≤ 180), applied insidegreatCircleArcso all four line builders (routesGeoJson,queuedRoutesGeoJson,hoverRouteGeoJson,hoverTraceGeoJson) are fixed at once. One line stays one LineString — no splitting; out-of-range values render on MapLibre's adjacent world copies (renderWorldCopiesis default-on).queuedRoutesGeoJsongets a second global pass after concatenation, since each leg unwraps relative to its own start airport and the joint can jump ±360. Non-crossing routes are byte-identical (the pass is a no-op when no |Δlon| > 180 exists).Tests:
lib/mapData.test.ts, real dataset pairs NZAA/NFTF/NZCH): eastward and westward crossings stay continuous, endpoints exact mod 360 with the first sample still the raw origin, sampled chord length ≈ haversine within 1%, non-crossing lons stay in [−180, 180], and a grounded plane's two-leg queue chain crossing ±180 stays continuous across the joint.e2e/hover.spec.ts): hovering an antimeridian-crossing package (hook-created NZAA → NFTF) draws one continuous trace — no rendered segment may contain a ±180 longitude jump.expectHoverRoute's endpoint check now compares longitudes on the circle (mod 360; identical for any pair within ±180, so existing routes are unaffected), andfitToAirportsunwraps crossing pairs so their bounds follow the short arc across the date line instead of spanning the wrong side of the globe.Known limitation (follow-up): plane markers still use the snapshot's wrapped lat/lon (
lib/game/state.ts→positionAt), so a plane mid-crossing teleports 360° on its marker at the instant it passes ±180 while its route line stays continuous. Fixing needs airport coordinates insideplanesGeoJson(signature change +Map.tsxcall site) — deliberately out of scope here.Verification (local, per AGENTS.md):
tsc --noEmitclean;vitest run359/359 passed (30 files);playwright test --listcompiles and lists all 58 tests in 14 files. Full e2e suite runs in CI against the isolated dind stack.