From d0d29659cbb4162234ab92c7c71f6fe2386ea695 Mon Sep 17 00:00:00 2001 From: rouggy Date: Thu, 10 Sep 2026 17:48:39 +0200 Subject: [PATCH] fix(maps): the ground track stops cutting straight across the map MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit splitAtAntimeridian works from which COPY of the world each longitude is in, so it needs a series that runs past ±180 rather than jumping back. greatCirclePoints produces one; a satellite ground track does not — SGP4 reports every longitude inside (−180, 180]. A track leaving Kamchatka at +179.9 and arriving in Alaska at −179.9 therefore looked like a single 359.8° step inside one world, no split was made, and the polyline drew the chord: a straight dashed line clean across the map on every crossing. The series is now unwrapped first. A jump of more than 180° between consecutive points is that wrap and nothing else — no real path steps half the globe between samples — and the unwrap is idempotent, so the great-circle caller is unaffected. Checked on three series: a normalised crossing now yields two segments meeting exactly at ±180 where it used to yield one chord; an already continuous 170→183 still splits at 180; a track that never crosses comes back untouched. --- frontend/src/lib/maidenhead.ts | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/frontend/src/lib/maidenhead.ts b/frontend/src/lib/maidenhead.ts index c740cbd..0c8c738 100644 --- a/frontend/src/lib/maidenhead.ts +++ b/frontend/src/lib/maidenhead.ts @@ -186,8 +186,38 @@ export function greatCirclePoints( // opposite edge, at the SAME latitude, so the line leaves one side of the map // and re-enters the other at the height it left. Leaflet takes the result as a // multi-polyline, so one path is still one layer. -export function splitAtAntimeridian(pts: [number, number][]): [number, number][][] { - if (pts.length === 0) return []; +// unwrapLon makes a longitude series continuous. +// +// splitAtAntimeridian works from which COPY of the world each longitude is in, +// which requires the series to run past ±180 rather than jumping back. A great +// circle built by greatCirclePoints already does; a satellite ground track does +// not — SGP4 reports every longitude inside (−180, 180], so a track leaving +// Kamchatka at +179.9 and arriving in Alaska at −179.9 looked to the splitter +// like one step of 359.8° inside a single world. No split was made and the +// polyline drew the chord: a straight dashed line clean across the map, from +// one side of the planet to the other, on every crossing. +// +// A jump of more than 180° between two consecutive points is that wrap and +// nothing else: no real path steps half the globe between samples. Idempotent +// on a series that was already continuous, so both callers can share it. +function unwrapLon(pts: [number, number][]): [number, number][] { + if (pts.length < 2) return pts; + const out: [number, number][] = [pts[0]]; + let turns = 0; + for (let i = 1; i < pts.length; i++) { + const [lat, lon] = pts[i]; + const prevRaw = pts[i - 1][1]; + const d = lon - prevRaw; + if (d > 180) turns -= 1; + else if (d < -180) turns += 1; + out.push([lat, lon + 360 * turns]); + } + return out; +} + +export function splitAtAntimeridian(input: [number, number][]): [number, number][][] { + if (input.length === 0) return []; + const pts = unwrapLon(input); // Which copy of the world a longitude belongs to: 0 is the map's own. const world = (lon: number) => Math.floor((lon + 180) / 360); const norm = (lon: number) => lon - 360 * world(lon);