import type { StateSnapshot } from './game/state'; import { formatFuel } from './format'; // Pure GeoJSON builders for the map's dynamic layers (v0.11.0): one Point feature // per plane, one LineString per accepted/in-flight contract. 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 { contractId: string; kind: 'pending' | 'inflight'; } 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 { type: 'FeatureCollection'; features: T[]; } const EMPTY_POINTS: FeatureCollection = { type: 'FeatureCollection', features: [] }; const EMPTY_LINES: FeatureCollection = { type: 'FeatureCollection', features: [] }; export function planesGeoJson(state: StateSnapshot | null): FeatureCollection { if (!state) return EMPTY_POINTS; const contractById = new Map(state.contracts.map((c) => [c.id, c])); // 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'; const contract = p.contractId ? contractById.get(p.contractId) : undefined; 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 && contract ? `${contract.originName} → ${contract.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 contract that is accepted but not yet dispatched ('pending') // or already flying ('inflight' — planeId set and that plane's status is 'flying'). // Open/completed/expired contracts draw nothing. export function routesGeoJson( state: StateSnapshot | null, airportsByIcao: ReadonlyMap, ): FeatureCollection { if (!state) return EMPTY_LINES; const planeById = new Map(state.planes.map((p) => [p.id, p])); const features: MapLineFeature[] = []; for (const c of state.contracts) { if (c.status !== 'accepted') continue; const from = airportsByIcao.get(c.originIcao); const to = airportsByIcao.get(c.destIcao); if (!from || !to) continue; // endpoint missing from the dataset — skip, don't guess const plane = c.planeId ? planeById.get(c.planeId) : undefined; features.push({ type: 'Feature', geometry: { type: 'LineString', coordinates: [ [from.lon, from.lat], [to.lon, to.lat], ], }, properties: { contractId: c.id, kind: plane?.status === 'flying' ? 'inflight' : 'pending' }, }); } return { type: 'FeatureCollection', features }; }