fix(maps): the ground track stops cutting straight across the map

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.
This commit is contained in:
2026-09-10 17:48:39 +02:00
parent 6f9b996db8
commit d0d29659cb
+32 -2
View File
@@ -186,8 +186,38 @@ export function greatCirclePoints(
// opposite edge, at the SAME latitude, so the line leaves one side of the map // 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 // 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. // multi-polyline, so one path is still one layer.
export function splitAtAntimeridian(pts: [number, number][]): [number, number][][] { // unwrapLon makes a longitude series continuous.
if (pts.length === 0) return []; //
// 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. // 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 world = (lon: number) => Math.floor((lon + 180) / 360);
const norm = (lon: number) => lon - 360 * world(lon); const norm = (lon: number) => lon - 360 * world(lon);