import type { Prediction, Telemetry } from '$domain'; import { toLngLat } from '$domain'; import type { IMap, Scene } from './core'; /** * Plot helpers for high-level domain objects. These live outside MapLibreMap * so they can be reused against any IMap implementation. * * Icons are served from /static; pass explicit overrides if a workspace * should use custom markers. */ export interface TrajectoryStyle { color?: string; width?: number; opacity?: number; launchIcon?: string; landingIcon?: string; burstIcon?: string; iconSize?: [number, number]; } const DEFAULT_STYLE: Required> & Pick = { color: '#000000', width: 3, opacity: 1, iconSize: [12, 12], launchIcon: '/target-blue.png', landingIcon: '/target-red.png', burstIcon: '/pop-marker.png', }; export function plotPrediction( scene: Scene, prediction: Prediction, style: TrajectoryStyle = {}, ): void { const s = { ...DEFAULT_STYLE, ...style }; scene.clear(); scene.addLine('path', { coords: prediction.flight_path, color: s.color, width: s.width, opacity: s.opacity, }); scene.addMarker('launch', { lngLat: toLngLat(prediction.launch.latlng), iconUrl: s.launchIcon, iconSize: s.iconSize, popupHtml: `Launch
${prediction.launch.latlng.lat.toFixed(6)}, ${prediction.launch.latlng.lng.toFixed(6)}`, }); scene.addMarker('landing', { lngLat: toLngLat(prediction.landing.latlng), iconUrl: s.landingIcon, iconSize: s.iconSize, popupHtml: `Landing
${prediction.landing.latlng.lat.toFixed(6)}, ${prediction.landing.latlng.lng.toFixed(6)}`, }); scene.addMarker('burst', { lngLat: toLngLat(prediction.burst.latlng), iconUrl: s.burstIcon, iconSize: [s.iconSize[0] + 4, s.iconSize[1] + 4], popupHtml: `Burst
${prediction.burst.latlng.lat.toFixed(6)}, ${prediction.burst.latlng.lng.toFixed(6)}`, }); } export function plotTelemetry( map: IMap, scene: Scene, telemetry: Telemetry, style: TrajectoryStyle = {}, ): void { const s = { ...DEFAULT_STYLE, ...style }; scene.clear(); scene.addLine('path', { coords: telemetry.flight_path, color: s.color, width: s.width, opacity: s.opacity, }); scene.addMarker('launch', { lngLat: toLngLat(telemetry.launch.latlng), iconUrl: s.launchIcon, iconSize: s.iconSize, }); telemetry.datapoints.forEach((p, i) => { scene.addMarker(`point-${i}`, { lngLat: [p.longitude, p.latitude], iconUrl: '/marker-sm-red.png', iconSize: [8, 8], popupHtml: `${p.datetime}
${p.latitude.toFixed(6)}, ${p.longitude.toFixed(6)}`, }); }); if (telemetry.flight_path.length > 0) { map.fitBounds(telemetry.flight_path, 50); } } export function plotAnimatedMarker(scene: Scene, lng: number, lat: number): void { scene.clear(); scene.addCircle('marker-ring', { center: [lng, lat], radiusPx: 14, color: '#FF6B6B', opacity: 0.3, strokeColor: '#FF1744', strokeWidth: 0, }); scene.addCircle('marker-core', { center: [lng, lat], radiusPx: 6, color: '#FF1744', strokeColor: '#ffffff', strokeWidth: 2, }); }