98 lines
3.2 KiB
TypeScript
98 lines
3.2 KiB
TypeScript
import type { StateSnapshot } from './game/state';
|
|
import { formatFuel } from './format';
|
|
|
|
// Pure GeoJSON builders for the map's dynamic layers (v0.11.0, package model
|
|
// v0.12.0): one Point feature per plane, one LineString per FLYING plane's
|
|
// current leg (origin → dest_icao). Kept out of components/Map.tsx so the
|
|
// classification logic is unit-testable without a DOM.
|
|
|
|
export interface PlaneFeatureProps {
|
|
id: string;
|
|
modelLabel: string;
|
|
status: 'available' | 'flying';
|
|
fuelText: string; // "1234 / 6300 L"
|
|
routeText: string; // "Origin → Destination" while flying, '' when grounded
|
|
flying: boolean;
|
|
}
|
|
|
|
export interface RouteFeatureProps {
|
|
planeId: string;
|
|
}
|
|
|
|
export interface MapPointFeature {
|
|
type: 'Feature';
|
|
geometry: { type: 'Point'; coordinates: [number, number] };
|
|
properties: PlaneFeatureProps;
|
|
}
|
|
|
|
export interface MapLineFeature {
|
|
type: 'Feature';
|
|
geometry: { type: 'LineString'; coordinates: [number, number][] };
|
|
properties: RouteFeatureProps;
|
|
}
|
|
|
|
export interface FeatureCollection<T> {
|
|
type: 'FeatureCollection';
|
|
features: 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> {
|
|
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';
|
|
return {
|
|
type: 'Feature',
|
|
geometry: { type: 'Point', coordinates: [p.lon, p.lat] },
|
|
properties: {
|
|
id: p.id,
|
|
modelLabel: labelByModel.get(p.model) ?? p.model,
|
|
status: p.status,
|
|
fuelText: formatFuel(p.fuel, p.maxFuel),
|
|
routeText: flying && p.destName ? `${p.locationName} → ${p.destName}` : '',
|
|
flying,
|
|
},
|
|
};
|
|
});
|
|
return { type: 'FeatureCollection', features };
|
|
}
|
|
|
|
// Airport coordinate lookup for route endpoints (from the /api/airports GeoJSON).
|
|
export interface AirportCoord {
|
|
lat: number;
|
|
lon: number;
|
|
}
|
|
|
|
// One LineString per FLYING plane — its current leg from locationIcao to dest_icao.
|
|
// Grounded planes draw nothing; a flying plane missing either endpoint from the
|
|
// dataset is skipped, never guessed.
|
|
export function routesGeoJson(
|
|
state: StateSnapshot | null,
|
|
airportsByIcao: ReadonlyMap<string, AirportCoord>,
|
|
): FeatureCollection<MapLineFeature> {
|
|
if (!state) return EMPTY_LINES;
|
|
const features: MapLineFeature[] = [];
|
|
for (const p of state.planes) {
|
|
if (p.status !== 'flying' || !p.destIcao) continue;
|
|
const from = airportsByIcao.get(p.locationIcao);
|
|
const to = airportsByIcao.get(p.destIcao);
|
|
if (!from || !to) continue; // endpoint missing from the dataset — skip, don't guess
|
|
features.push({
|
|
type: 'Feature',
|
|
geometry: {
|
|
type: 'LineString',
|
|
coordinates: [
|
|
[from.lon, from.lat],
|
|
[to.lon, to.lat],
|
|
],
|
|
},
|
|
properties: { planeId: p.id },
|
|
});
|
|
}
|
|
return { type: 'FeatureCollection', features };
|
|
}
|