Plane markers teleport 360° when crossing the antimeridian (follow-up to #120) #124

Closed
opened 2026-09-22 16:50:30 +02:00 by kamilkrzywda · 1 comment
Owner

Follow-up from v0.53.0 (#120, PR #123): route lines now unwrap longitudes across ±180 and render continuously, but plane markers still use the snapshot's wrapped lat/lon (planesGeoJson ← positionAt, lib/game/state.ts:124). A plane mid-crossing teleports 360° on its marker at the instant it passes ±180 while its route line stays continuous.

Fix direction (from PR #123 review): pass airport coordinates into planesGeoJson so the client can unwrap each plane's position relative to its leg endpoints (signature change + Map.tsx call site). Unit-testable via lib/mapData; e2e-observable with a crossing package from /api/test/create-package.

Follow-up from v0.53.0 (#120, PR #123): route lines now unwrap longitudes across ±180 and render continuously, but plane markers still use the snapshot's wrapped lat/lon (`planesGeoJson` ← `positionAt`, lib/game/state.ts:124). A plane mid-crossing teleports 360° on its marker at the instant it passes ±180 while its route line stays continuous. Fix direction (from PR #123 review): pass airport coordinates into `planesGeoJson` so the client can unwrap each plane's position relative to its leg endpoints (signature change + Map.tsx call site). Unit-testable via lib/mapData; e2e-observable with a crossing package from /api/test/create-package.
Author
Owner

Fixed in v0.58.0 — PR #132, merge commit c5413af, deployed to https://flight.dev.kkhost.pl (verified /api/version = 0.58.0).

What changed

  • planesGeoJson now takes the airport index and unwraps each flying plane's longitude against its departure airport via a new module-private unwrapLon shared with unwrapLons — the same anchor greatCircleArc keeps for its first sample. A marker is unwrapped iff its route line is drawn (both leg endpoints exist); grounded planes and non-crossing legs are byte-identical to before.
  • Bonus product fix shipped in this release: the SSE silence watchdog now re-arms on every message (d0aec35) — a stream that goes quiet after the first event falls back to polling instead of freezing client state until reload.

Tests

  • Unit +6 in lib/mapData.test.ts (incl. the key invariant: every arc sample fed back as a wrapped snapshot position comes out exactly on that arc's own sample).
  • E2E +1 in e2e/map-interactions.spec.ts: chain-relocates the starter to NZAA, flies the hook-created NZAA→NFTF crossing at ×1000, samples marker lons from the planes source — no consecutive pair may differ by ≥ 180°, and some sample must sit on a world copy beyond ±180 (either side).

Debugging note: five CI runs were needed. The final failure was not in the product — runtime sometimes renders the continuous series in the −360° world copy (e.g. lons [−184.75 … −175.24] instead of [+175.26 … +183.5]), which MapLibre wraps identically on screen, so it was visually correct all along; only the test's assumption about which copy appeared was wrong. The assertion is now copy-agnostic (Math.abs(lon) > 180), and the run-180 diagnostics (server truth per phase + sampled lon/lat arrays + Δlon discriminator) are kept in the test as a tripwire — they're invisible in green runs and would localize any future regression instantly.

Fixed in **v0.58.0** — PR #132, merge commit `c5413af`, deployed to https://flight.dev.kkhost.pl (verified `/api/version` = 0.58.0). **What changed** - `planesGeoJson` now takes the airport index and unwraps each flying plane's longitude against its departure airport via a new module-private `unwrapLon` shared with `unwrapLons` — the same anchor `greatCircleArc` keeps for its first sample. A marker is unwrapped iff its route line is drawn (both leg endpoints exist); grounded planes and non-crossing legs are byte-identical to before. - Bonus product fix shipped in this release: the SSE silence watchdog now re-arms on every message (`d0aec35`) — a stream that goes quiet after the first event falls back to polling instead of freezing client state until reload. **Tests** - Unit +6 in `lib/mapData.test.ts` (incl. the key invariant: every arc sample fed back as a wrapped snapshot position comes out exactly on that arc's own sample). - E2E +1 in `e2e/map-interactions.spec.ts`: chain-relocates the starter to NZAA, flies the hook-created NZAA→NFTF crossing at ×1000, samples marker lons from the planes source — no consecutive pair may differ by ≥ 180°, and some sample must sit on a world copy beyond ±180 (either side). **Debugging note**: five CI runs were needed. The final failure was not in the product — runtime sometimes renders the continuous series in the −360° world copy (e.g. lons [−184.75 … −175.24] instead of [+175.26 … +183.5]), which MapLibre wraps identically on screen, so it was visually correct all along; only the test's assumption about *which* copy appeared was wrong. The assertion is now copy-agnostic (`Math.abs(lon) > 180`), and the run-180 diagnostics (server truth per phase + sampled lon/lat arrays + Δlon discriminator) are kept in the test as a tripwire — they're invisible in green runs and would localize any future regression instantly.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: OpenCode/Flight-Ops#124