154 lines
No EOL
5.2 KiB
Svelte
154 lines
No EOL
5.2 KiB
Svelte
<script lang="ts">
|
|
import { onMount, createEventDispatcher } from "svelte";
|
|
import * as L from "leaflet";
|
|
import type { Map as LeafletMap, LayerGroup } from "leaflet";
|
|
import "leaflet/dist/leaflet.css";
|
|
import WindVisualization from './WindVisualisation.svelte';
|
|
import { distHaversine } from "../lib/mathutil.ts";
|
|
import type { PredictionData, TelemetryData } from "../lib/types.ts";
|
|
|
|
|
|
|
|
/**
|
|
* @type {'prediction' | 'telemetry'}
|
|
*/
|
|
export let mode: "prediction" | "telemetry" = "prediction";
|
|
export let data: PredictionData | TelemetryData | null = null;
|
|
|
|
let map: typeof LeafletMap | undefined;
|
|
let mapContainer: HTMLDivElement;
|
|
let plotLayerGroup: typeof LayerGroup;
|
|
let mouseLat = 0;
|
|
let mouseLng = 0;
|
|
let isSelecting = false;
|
|
|
|
let windData;
|
|
|
|
const dispatch = createEventDispatcher<{ coordinatesSelected: { lat: number; lng: number } }>();
|
|
|
|
onMount(async () => {
|
|
if (!mapContainer) return;
|
|
|
|
map = L.map(mapContainer).setView([51.505, -0.09], 13);
|
|
plotLayerGroup = L.layerGroup().addTo(map);
|
|
|
|
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
|
}).addTo(map);
|
|
|
|
const response = await fetch("src/routes/testVelo.json");
|
|
windData = await response.json();
|
|
|
|
map.on("mousemove", (e: any) => {
|
|
mouseLat = e.latlng.lat;
|
|
mouseLng = e.latlng.lng;
|
|
});
|
|
|
|
map.on("click", (e: any) => {
|
|
if (isSelecting) {
|
|
dispatch("coordinatesSelected", { lat: e.latlng.lat, lng: e.latlng.lng });
|
|
stopSelection();
|
|
}
|
|
});
|
|
});
|
|
|
|
$: if (map && data) {
|
|
plotData(data);
|
|
} else if (map) {
|
|
clearMapLayers();
|
|
}
|
|
|
|
export const startSelection = () => {
|
|
isSelecting = true;
|
|
if (mapContainer) mapContainer.style.cursor = "crosshair";
|
|
};
|
|
|
|
export const stopSelection = () => {
|
|
isSelecting = false;
|
|
if (mapContainer) mapContainer.style.cursor = "";
|
|
};
|
|
|
|
export const plotData = (plotData: PredictionData | TelemetryData) => {
|
|
if (mode === "prediction") {
|
|
plotPrediction(plotData as PredictionData);
|
|
} else if (mode === "telemetry") {
|
|
plotTelemetry(plotData as TelemetryData);
|
|
}
|
|
};
|
|
|
|
export const clearMapLayers = () => {
|
|
plotLayerGroup?.clearLayers();
|
|
};
|
|
|
|
const launchIcon = L.icon({ iconUrl: "target-blue.png", iconSize: [10, 10], iconAnchor: [5, 5] });
|
|
const landIcon = L.icon({ iconUrl: "target-red.png", iconSize: [10, 10], iconAnchor: [5, 5] });
|
|
const burstIcon = L.icon({ iconUrl: "pop-marker.png", iconSize: [16, 16], iconAnchor: [8, 8] });
|
|
const telemetryIcon = L.icon({ iconUrl: "marker-sm-red.png", iconSize: [10, 10], iconAnchor: [5, 5] });
|
|
|
|
const plotPrediction = (prediction: PredictionData) => {
|
|
const { launch, landing, burst, flight_path, flight_time } = prediction;
|
|
|
|
const range = distHaversine(launch.latlng, landing.latlng, 1);
|
|
const f_hours = Math.floor(flight_time / 3600);
|
|
const f_minutes = Math.floor((flight_time % 3600) / 60).toString().padStart(2, "0");
|
|
const flighttime = `${f_hours}hr${f_minutes}`;
|
|
|
|
L.marker(launch.latlng, { title: `Launch`, icon: launchIcon }).addTo(plotLayerGroup);
|
|
L.marker(landing.latlng, { title: `Landing`, icon: landIcon }).addTo(plotLayerGroup);
|
|
L.marker(burst.latlng, { title: `Burst`, icon: burstIcon }).addTo(plotLayerGroup);
|
|
|
|
L.polyline(flight_path, { weight: 3, color: "#000000" }).addTo(plotLayerGroup);
|
|
|
|
map?.fitBounds(L.latLngBounds(flight_path));
|
|
};
|
|
|
|
const plotTelemetry = (telemetry: TelemetryData) => {
|
|
L.marker(telemetry.launch.latlng, { title: `Launch`, icon: launchIcon }).addTo(plotLayerGroup);
|
|
|
|
telemetry.datapoints.forEach((point) => {
|
|
L.marker([point.latitude, point.longitude], {
|
|
title: `Telemetry at ${point.datetime}`,
|
|
icon: telemetryIcon,
|
|
})
|
|
.bindPopup(`<b>Telemetry Point</b><br>Lat: ${point.latitude.toFixed(6)}<br>Lon: ${point.longitude.toFixed(6)}`)
|
|
.addTo(plotLayerGroup);
|
|
});
|
|
|
|
L.polyline(telemetry.flight_path, { weight: 3, color: "#000000" }).addTo(plotLayerGroup);
|
|
|
|
map?.fitBounds(L.latLngBounds(telemetry.flight_path));
|
|
};
|
|
|
|
export const panTo = (lat: number, lng: number) => {
|
|
if (map) {
|
|
map.setView([lat, lng], map.getZoom());
|
|
}
|
|
};
|
|
|
|
export const zoomTo = (lat: number, lng: number, zoomLevel: number) => {
|
|
if (map) {
|
|
map.setView([lat, lng], zoomLevel);
|
|
}
|
|
};
|
|
|
|
export const getMap = () => {
|
|
return map;
|
|
};
|
|
</script>
|
|
|
|
<div class="map-container" bind:this={mapContainer}>
|
|
<div class="card coordinates-display">
|
|
<p class="card-text">
|
|
<b>Lat:</b>
|
|
{mouseLat.toFixed(6)},
|
|
<b>Lon:</b>
|
|
{mouseLng.toFixed(6)}
|
|
</p>
|
|
</div>
|
|
<div class="panel-container">
|
|
<slot />
|
|
</div>
|
|
{#if map && windData}
|
|
<WindVisualization {map} windData={windData} />
|
|
{/if}
|
|
</div> |