Files
TREK/client/src/components/Map/flightGeodesy.test.ts
T
Maurice 634210f2b1 fix(map): render date-line-crossing routes as one continuous arc (#1411)
The great-circle sampler normalizes longitudes to [-180,180], so a
transpacific leg jumped +-360 between neighbours and got split into two
polylines pinned to opposite map edges. Unwrap the longitudes instead
(shared flightGeodesy module for both renderers): Leaflet additionally
draws a +-360-shifted copy so both halves show in the standard view, GL
maps repeat world copies themselves.
2026-07-05 00:23:38 +02:00

64 lines
2.4 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import { greatCircle, unwrapLngs, geodesicArcs } from './flightGeodesy'
const YVR: [number, number] = [49.19, -123.18]
const ICN: [number, number] = [37.46, 126.44]
const FRA: [number, number] = [50.03, 8.57]
const JFK: [number, number] = [40.64, -73.78]
const maxConsecutiveDeltaLng = (pts: [number, number][]) =>
pts.reduce((max, p, i) => (i === 0 ? 0 : Math.max(max, Math.abs(p[1] - pts[i - 1][1]))), 0)
describe('flightGeodesy (#1411)', () => {
it('a date-line crossing unwraps into one continuous arc plus a shifted copy for Leaflet', () => {
const arcs = geodesicArcs(YVR, ICN, true)
expect(arcs).toHaveLength(2)
const [base, shifted] = arcs
// continuous: no ±360 jump anywhere
expect(maxConsecutiveDeltaLng(base)).toBeLessThan(180)
// endpoints: starts at YVR, ends at ICN unwrapped westwards (126.44 - 360)
expect(base[0][0]).toBeCloseTo(YVR[0], 5)
expect(base[0][1]).toBeCloseTo(YVR[1], 5)
expect(base[base.length - 1][1]).toBeCloseTo(ICN[1] - 360, 5)
// the copy is the base shifted by exactly +360
expect(shifted).toHaveLength(base.length)
shifted.forEach(([lat, lng], i) => {
expect(lat).toBe(base[i][0])
expect(lng).toBeCloseTo(base[i][1] + 360, 10)
})
})
it('an eastbound crossing unwraps upwards and shifts by -360', () => {
const arcs = geodesicArcs(ICN, YVR, true)
expect(arcs).toHaveLength(2)
const [base, shifted] = arcs
expect(maxConsecutiveDeltaLng(base)).toBeLessThan(180)
expect(base[base.length - 1][1]).toBeCloseTo(YVR[1] + 360, 5)
expect(shifted[0][1]).toBeCloseTo(base[0][1] - 360, 10)
})
it('a non-crossing flight stays a single in-range arc', () => {
const arcs = geodesicArcs(FRA, JFK, true)
expect(arcs).toHaveLength(1)
for (const [, lng] of arcs[0]) {
expect(lng).toBeGreaterThanOrEqual(-180)
expect(lng).toBeLessThanOrEqual(180)
}
})
it('GL mode always returns exactly one continuous arc (world copies handle the wrap)', () => {
const arcs = geodesicArcs(YVR, ICN, false)
expect(arcs).toHaveLength(1)
expect(maxConsecutiveDeltaLng(arcs[0])).toBeLessThan(180)
})
it('a zero-distance leg passes the endpoints through', () => {
expect(greatCircle(YVR, YVR)).toEqual([YVR, YVR])
})
it('unwrapLngs keeps already-continuous input unchanged', () => {
const pts: [number, number][] = [[0, 170], [0, 175], [0, 179]]
expect(unwrapLngs(pts)).toEqual(pts)
})
})