124 lines
3.1 KiB
TypeScript
124 lines
3.1 KiB
TypeScript
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<Omit<TrajectoryStyle, 'launchIcon' | 'landingIcon' | 'burstIcon'>> &
|
|
Pick<TrajectoryStyle, 'launchIcon' | 'landingIcon' | 'burstIcon'> = {
|
|
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: `<b>Launch</b><br>${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: `<b>Landing</b><br>${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: `<b>Burst</b><br>${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: `<b>${p.datetime}</b><br>${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,
|
|
});
|
|
}
|