v0.58.0 — plane markers no longer teleport across the antimeridian (ISS-124)
CI / build + unit + e2e (pull_request) Failing after 8m36s
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:
+24
-5
@@ -41,15 +41,30 @@ export interface FeatureCollection<T> {
|
||||
const EMPTY_POINTS: FeatureCollection<MapPointFeature> = { type: 'FeatureCollection', features: [] };
|
||||
const EMPTY_LINES: FeatureCollection<MapLineFeature> = { type: 'FeatureCollection', features: [] };
|
||||
|
||||
export function planesGeoJson(state: StateSnapshot | null): FeatureCollection<MapPointFeature> {
|
||||
// ISS-124 (v0.58.0): positionAt wraps each sample's longitude into [−180, 180], so a marker on a
|
||||
// leg crossing the antimeridian jumps +179.x → −179.x between polls — MapLibre draws that as a 360°
|
||||
// teleport while the route line (greatCircleArc) stays continuous. Unwrap the plane's lon against
|
||||
// its departure airport — the same anchor greatCircleArc keeps for its first sample — so marker and
|
||||
// line agree frame to frame. Only when BOTH endpoints exist, mirroring routesGeoJson's guard: a
|
||||
// flying plane with a missing endpoint draws no line either, so it stays wrapped like before.
|
||||
export function planesGeoJson(
|
||||
state: StateSnapshot | null,
|
||||
airportsByIcao: ReadonlyMap<string, AirportCoord>,
|
||||
): FeatureCollection<MapPointFeature> {
|
||||
if (!state) return EMPTY_POINTS;
|
||||
// Catalog display name ("ATR 72-600") for the popup — raw model id as fallback.
|
||||
const labelByModel = new Map(state.catalog.map((e) => [e.model, e.label]));
|
||||
const features: MapPointFeature[] = state.planes.map((p) => {
|
||||
const flying = p.status === 'flying';
|
||||
let lon = p.lon;
|
||||
if (flying && p.destIcao) {
|
||||
const from = airportsByIcao.get(p.locationIcao);
|
||||
const to = airportsByIcao.get(p.destIcao);
|
||||
if (from && to) lon = unwrapLon(p.lon, from.lon);
|
||||
}
|
||||
return {
|
||||
type: 'Feature',
|
||||
geometry: { type: 'Point', coordinates: [p.lon, p.lat] },
|
||||
geometry: { type: 'Point', coordinates: [lon, p.lat] },
|
||||
properties: {
|
||||
id: p.id,
|
||||
modelLabel: labelByModel.get(p.model) ?? p.model,
|
||||
@@ -80,12 +95,16 @@ export const ROUTE_ARC_SEGMENTS = 24;
|
||||
// sequence into continuous longitudes: keep the first sample's raw lon, then shift each subsequent
|
||||
// one by ±360 relative to the previous UNWRAPPED value until |Δlon| ≤ 180. One line stays one line;
|
||||
// out-of-range values render on MapLibre's adjacent world copies (renderWorldCopies is default-on).
|
||||
function unwrapLon(lon: number, prev: number): number {
|
||||
while (lon - prev > 180) lon -= 360;
|
||||
while (prev - lon > 180) lon += 360;
|
||||
return lon;
|
||||
}
|
||||
|
||||
function unwrapLons(coords: [number, number][]): [number, number][] {
|
||||
let prev = coords[0]![0];
|
||||
for (let i = 1; i < coords.length; i++) {
|
||||
let lon = coords[i]![0];
|
||||
while (lon - prev > 180) lon -= 360;
|
||||
while (prev - lon > 180) lon += 360;
|
||||
const lon = unwrapLon(coords[i]![0], prev);
|
||||
coords[i]![0] = lon;
|
||||
prev = lon;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user