Flight queue UI: dotted map line, left-pane Queue sub-section, e2e (ISS-043 part 2)
CI / build + unit + e2e (pull_request) Failing after 6m51s
CI / build + unit + e2e (pull_request) Failing after 6m51s
This commit is contained in:
+64
-1
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { StateSnapshot } from './game/state';
|
||||
import { planesGeoJson, routesGeoJson, hoverRouteGeoJson, hoverTraceGeoJson, greatCircleArc, ROUTE_ARC_SEGMENTS, type AirportCoord } from './mapData';
|
||||
import { planesGeoJson, queuedRoutesGeoJson, routesGeoJson, hoverRouteGeoJson, hoverTraceGeoJson, greatCircleArc, ROUTE_ARC_SEGMENTS, type AirportCoord } from './mapData';
|
||||
import { haversineKm } from './geo';
|
||||
|
||||
const AIRPORTS = new Map<string, AirportCoord>([
|
||||
@@ -140,6 +140,69 @@ describe('routesGeoJson (v0.12.0 — one line per flying plane\'s current leg)',
|
||||
});
|
||||
});
|
||||
|
||||
describe('queuedRoutesGeoJson (ISS-043 v0.26.0 — dotted queue line: dest → queued legs only)', () => {
|
||||
it('draws nothing for a grounded plane, even with a non-empty queue', () => {
|
||||
const fc = queuedRoutesGeoJson(
|
||||
[
|
||||
{ id: 'p1', model: 'atr72', status: 'available', fuel: 1, maxFuel: 2, locationIcao: 'EDDF', locationName: 'F', destIcao: null, destName: null, departureSimTime: null, lat: 50.03, lon: 8.56, queue: ['EHAM'] },
|
||||
],
|
||||
AIRPORTS,
|
||||
);
|
||||
expect(fc.features).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('draws nothing for a flying plane with an empty queue', () => {
|
||||
const fc = queuedRoutesGeoJson(
|
||||
[
|
||||
{ id: 'p1', model: 'atr72', status: 'flying', fuel: 1, maxFuel: 2, locationIcao: 'EDDF', locationName: 'F', destIcao: 'EHAM', destName: 'A', departureSimTime: 0, lat: 51, lon: 9, queue: [] },
|
||||
],
|
||||
AIRPORTS,
|
||||
);
|
||||
expect(fc.features).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('flying plane with a 2-leg queue → one feature chaining dest → q0 → q1 with exact endpoints', () => {
|
||||
const fc = queuedRoutesGeoJson(
|
||||
[
|
||||
// Active leg EDDF→EHAM stays in routesGeoJson; the queue line is ONLY EHAM → EDDV → LEMD.
|
||||
{ id: 'p1', model: 'atr72', status: 'flying', fuel: 1, maxFuel: 2, locationIcao: 'EDDF', locationName: 'F', destIcao: 'EHAM', destName: 'A', departureSimTime: 0, lat: 51, lon: 9, queue: ['EDDV', 'LEMD'] },
|
||||
],
|
||||
AIRPORTS,
|
||||
);
|
||||
expect(fc.features).toHaveLength(1);
|
||||
expect(fc.features[0].properties).toEqual({ planeId: 'p1' });
|
||||
const coords = fc.features[0].geometry.coordinates;
|
||||
// Two concatenated arcs with the shared joint point dropped once: n_legs × SEGMENTS + 1 points.
|
||||
expect(coords.length).toBe(2 * ROUTE_ARC_SEGMENTS + 1);
|
||||
expect(coords[0]).toEqual([4.76, 52.31]); // starts at DEST (EHAM), not at the plane's origin
|
||||
expect(coords[coords.length - 1]).toEqual([-3.56, 40.47]); // ends at the last queued airport (LEMD)
|
||||
expect(coords[ROUTE_ARC_SEGMENTS]).toEqual([9.68, 52.46]); // joint point = first queued airport (EDDV), exact
|
||||
});
|
||||
|
||||
it('skips a plane whose dest or a queued ICAO is missing from the airport index', () => {
|
||||
const fc = queuedRoutesGeoJson(
|
||||
[
|
||||
{ id: 'p1', model: 'atr72', status: 'flying', fuel: 1, maxFuel: 2, locationIcao: 'EDDF', locationName: 'F', destIcao: 'ZZZZ', destName: '?', departureSimTime: 0, lat: 51, lon: 9, queue: ['EHAM'] },
|
||||
{ id: 'p2', model: 'atr72', status: 'flying', fuel: 1, maxFuel: 2, locationIcao: 'EDDF', locationName: 'F', destIcao: 'EHAM', destName: 'A', departureSimTime: 0, lat: 51, lon: 9, queue: ['ZZZZ'] },
|
||||
],
|
||||
AIRPORTS,
|
||||
);
|
||||
expect(fc.features).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('one feature per qualifying plane — grounded and empty-queue planes draw nothing', () => {
|
||||
const fc = queuedRoutesGeoJson(
|
||||
[
|
||||
{ id: 'p1', model: 'atr72', status: 'flying', fuel: 1, maxFuel: 2, locationIcao: 'EDDF', locationName: 'F', destIcao: 'EHAM', destName: 'A', departureSimTime: 0, lat: 51, lon: 9, queue: ['LEMD'] },
|
||||
{ id: 'p2', model: 'atr72', status: 'flying', fuel: 1, maxFuel: 2, locationIcao: 'EHAM', locationName: 'A', destIcao: 'EDDV', destName: 'H', departureSimTime: 0, lat: 52.3, lon: 4.7, queue: [] },
|
||||
{ id: 'p3', model: 'atr72', status: 'available', fuel: 1, maxFuel: 2, locationIcao: 'EDDV', locationName: 'H', destIcao: null, destName: null, departureSimTime: null, lat: 52.46, lon: 9.68, queue: ['LEMD'] },
|
||||
],
|
||||
AIRPORTS,
|
||||
);
|
||||
expect(fc.features.map((f) => f.properties.planeId)).toEqual(['p1']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hoverRouteGeoJson (ISS-022/025/027 v0.22.0 — hover route trace)', () => {
|
||||
it('returns an empty collection for the no-hover "" sentinel', () => {
|
||||
expect(hoverRouteGeoJson('', '', AIRPORTS)).toEqual({ type: 'FeatureCollection', features: [] });
|
||||
|
||||
+29
-1
@@ -1,4 +1,4 @@
|
||||
import type { StateSnapshot } from './game/state';
|
||||
import type { PlaneState, StateSnapshot } from './game/state';
|
||||
import { formatFuel } from './format';
|
||||
import { positionAt } from './sim/movement';
|
||||
|
||||
@@ -164,3 +164,31 @@ export function routesGeoJson(
|
||||
}
|
||||
return { type: 'FeatureCollection', features };
|
||||
}
|
||||
|
||||
// ISS-043 (v0.26.0): the dotted queue line — one LineString per FLYING plane with a non-empty
|
||||
// queue, chaining dest → queue[0] → … → queue[n−1]: ONLY the not-yet-started legs (the active
|
||||
// leg locationIcao→dest stays in routesGeoJson's solid layer). Arcs are concatenated with the
|
||||
// shared joint point dropped once. A plane is skipped — never guessed — when its dest or any
|
||||
// queued ICAO is missing from the airport index; empty collection when nothing qualifies.
|
||||
export function queuedRoutesGeoJson(
|
||||
planes: PlaneState[],
|
||||
airportsByIcao: ReadonlyMap<string, AirportCoord>,
|
||||
): FeatureCollection<MapLineFeature> {
|
||||
const features: MapLineFeature[] = [];
|
||||
for (const p of planes) {
|
||||
if (p.status !== 'flying' || !p.destIcao || p.queue.length === 0) continue;
|
||||
const pts = [p.destIcao, ...p.queue].map((icao) => airportsByIcao.get(icao));
|
||||
if (pts.some((c) => c === undefined)) continue; // an endpoint is missing from the dataset — skip
|
||||
const coords: [number, number][] = [];
|
||||
for (let i = 0; i < pts.length - 1; i++) {
|
||||
const arc = greatCircleArc(pts[i]!, pts[i + 1]!);
|
||||
coords.push(...(i === 0 ? arc : arc.slice(1))); // drop the duplicated joint point
|
||||
}
|
||||
features.push({
|
||||
type: 'Feature',
|
||||
geometry: { type: 'LineString', coordinates: coords },
|
||||
properties: { planeId: p.id },
|
||||
});
|
||||
}
|
||||
return { type: 'FeatureCollection', features };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user