v0.58.0 — plane markers no longer teleport across the antimeridian (ISS-124)
CI / build + unit + e2e (pull_request) Failing after 8m36s

planesGeoJson now takes the airport index and unwraps each flying plane's
longitude against its departure airport (unwrapLon, shared with unwrapLons),
mirroring routesGeoJson's both-endpoints guard so a marker is unwrapped iff
its line is drawn. Map.tsx passes airportsIndexRef at both call sites.

Unit +6 in lib/mapData.test.ts; e2e +1 in e2e/map-interactions.spec.ts
(chain-relocate to NZAA, fly the NZAA→NFTF crossing, sample marker lons).
This commit is contained in:
2026-09-22 21:35:48 +00:00
parent 18e5c832e3
commit c06e038130
6 changed files with 268 additions and 9 deletions
+94 -1
View File
@@ -45,7 +45,7 @@ function snapshot(overrides: Partial<StateSnapshot> = {}): StateSnapshot {
describe('planesGeoJson', () => {
it('returns an empty collection for a null snapshot', () => {
expect(planesGeoJson(null)).toEqual({ type: 'FeatureCollection', features: [] });
expect(planesGeoJson(null, AIRPORTS)).toEqual({ type: 'FeatureCollection', features: [] });
});
it('emits one Point per plane at its polled lat/lon with popup properties', () => {
@@ -56,6 +56,7 @@ describe('planesGeoJson', () => {
{ id: 'plane-2', model: 'atr72', status: 'flying', fuel: 4000, maxFuel: 6300, locationIcao: 'EDDF', locationName: 'Frankfurt Main Airport', destIcao: 'EDDV', destName: 'Hannover Airport', departureSimTime: 1000, lat: 51.2, lon: 9.1, health: 100, repairUntilSimTime: null, queue: [] },
],
}),
AIRPORTS, // non-crossing leg — the unwrap is a no-op, positions stay as polled
);
expect(fc.features).toHaveLength(2);
@@ -82,6 +83,7 @@ describe('planesGeoJson', () => {
{ id: 'p1', model: 'mystery', status: 'available', fuel: 1, maxFuel: 2, locationIcao: 'EDDF', locationName: 'x', destIcao: null, destName: null, departureSimTime: null, lat: 0, lon: 0, health: 100, repairUntilSimTime: null, queue: [] },
],
}),
AIRPORTS,
);
expect(fc.features[0].properties.modelLabel).toBe('mystery');
});
@@ -93,11 +95,102 @@ describe('planesGeoJson', () => {
{ id: 'p1', model: 'atr72', status: 'flying', fuel: 1, maxFuel: 2, locationIcao: 'EDDF', locationName: 'x', destIcao: null, destName: null, departureSimTime: 0, lat: 0, lon: 0, health: 100, repairUntilSimTime: null, queue: [] },
],
}),
AIRPORTS, // no destIcao — the unwrap guard skips it, position stays as polled
);
expect(fc.features[0].properties.routeText).toBe('');
});
});
describe('planesGeoJson — antimeridian marker unwrap (ISS-124 v0.58.0)', () => {
const nzaa = AIRPORTS.get('NZAA')!; // Auckland, lon +174.8
const nftf = AIRPORTS.get('NFTF')!; // Fua'amotu, lon −175.1 — NZAA→NFTF crosses ±180 eastward
it('eastward crossing: a mid-leg wrapped lon is unwrapped against the origin airport', () => {
// positionAt wraps into [−180, 180]: past ±180 the snapshot reads −179.5 for a point at +180.5.
const fc = planesGeoJson(
snapshot({
planes: [
{ id: 'p1', model: 'atr72', status: 'flying', fuel: 3000, maxFuel: 6300, locationIcao: 'NZAA', locationName: 'Auckland', destIcao: 'NFTF', destName: "Fua'amotu", departureSimTime: 0, lat: -29.5, lon: -179.5, health: 100, repairUntilSimTime: null, queue: [] },
],
}),
AIRPORTS,
);
const [lon] = fc.features[0].geometry.coordinates;
expect(lon).toBeCloseTo(180.5, 9); // −179.5 + 360 — continuous with the origin's +174.8
expect(Math.abs(lon - nzaa.lon)).toBeLessThan(180); // within half a turn of the anchor
});
it('westward crossing (reversed pair): unwrapped below −180', () => {
const fc = planesGeoJson(
snapshot({
planes: [
{ id: 'p1', model: 'atr72', status: 'flying', fuel: 3000, maxFuel: 6300, locationIcao: 'NFTF', locationName: "Fua'amotu", destIcao: 'NZAA', destName: 'Auckland', departureSimTime: 0, lat: -29.5, lon: 179.5, health: 100, repairUntilSimTime: null, queue: [] },
],
}),
AIRPORTS,
);
const [lon] = fc.features[0].geometry.coordinates;
expect(lon).toBeCloseTo(-180.5, 9); // +179.5 − 360 — continuous with the origin's −175.1
});
it('marker lon equals the route line\'s own sample at every progress point (same world copy)', () => {
// The line is greatCircleArc(NZAA→NFTF) unwrapped from +174.8; feed each of its samples back as a
// WRAPPED snapshot position (what positionAt produces server-side) — the marker must land exactly
// on the line's sample, so marker and line agree frame to frame across the crossing.
const arc = greatCircleArc(nzaa, nftf);
for (const [uLon, uLat] of arc) {
let wLon = uLon; // wrap into positionAt's range as the server snapshot would carry it
while (wLon > 180) wLon -= 360;
while (wLon <= -180) wLon += 360;
const fc = planesGeoJson(
snapshot({
planes: [
{ id: 'p1', model: 'atr72', status: 'flying', fuel: 1, maxFuel: 2, locationIcao: 'NZAA', locationName: 'A', destIcao: 'NFTF', destName: 'B', departureSimTime: 0, lat: uLat, lon: wLon, health: 100, repairUntilSimTime: null, queue: [] },
],
}),
AIRPORTS,
);
expect(fc.features[0].geometry.coordinates[0]).toBeCloseTo(uLon, 9);
}
});
it('grounded planes keep their raw position (no unwrap without a leg)', () => {
const fc = planesGeoJson(
snapshot({
planes: [
{ id: 'p1', model: 'atr72', status: 'available', fuel: 6300, maxFuel: 6300, locationIcao: 'NFTF', locationName: "Fua'amotu", destIcao: null, destName: null, departureSimTime: null, lat: nftf.lat, lon: nftf.lon, health: 100, repairUntilSimTime: null, queue: [] },
],
}),
AIRPORTS,
);
expect(fc.features[0].geometry.coordinates).toEqual([nftf.lon, nftf.lat]); // raw −175.1, untouched
});
it('a flying plane with a missing endpoint keeps its wrapped lon (no line is drawn either)', () => {
const fc = planesGeoJson(
snapshot({
planes: [
{ id: 'p1', model: 'atr72', status: 'flying', fuel: 3000, maxFuel: 6300, locationIcao: 'NZAA', locationName: 'Auckland', destIcao: 'ZZZZ', destName: '?', departureSimTime: 0, lat: -29.5, lon: -179.5, health: 100, repairUntilSimTime: null, queue: [] },
],
}),
AIRPORTS,
);
expect(fc.features[0].geometry.coordinates).toEqual([-179.5, -29.5]); // wrapped — mirrors routesGeoJson's skip
});
it('non-crossing legs are untouched: lons stay as polled', () => {
const fc = planesGeoJson(
snapshot({
planes: [
{ id: 'p1', model: 'atr72', status: 'flying', fuel: 3000, maxFuel: 6300, locationIcao: 'EDDF', locationName: 'F', destIcao: 'LEMD', destName: 'M', departureSimTime: 0, lat: 45.2, lon: -1.9, health: 100, repairUntilSimTime: null, queue: [] },
],
}),
AIRPORTS,
);
expect(fc.features[0].geometry.coordinates).toEqual([-1.9, 45.2]);
});
});
describe('routesGeoJson (v0.12.0 — one line per flying plane\'s current leg)', () => {
it('returns an empty collection for a null snapshot', () => {
expect(routesGeoJson(null, AIRPORTS)).toEqual({ type: 'FeatureCollection', features: [] });