Refactor of map & other components

This commit is contained in:
ThePetrovich 2025-06-27 18:23:50 +08:00
parent 527d4417ff
commit c7df38e6ce
10 changed files with 532 additions and 466 deletions

View file

@ -1,239 +1,173 @@
<script lang="ts">
import { onMount } from 'svelte';
import * as L from 'leaflet';
import type { Map as LeafletMap } from 'leaflet';
import type { Marker } from 'leaflet';
import type { LatLng } from 'leaflet';
import 'leaflet/dist/leaflet.css';
import VelocityLayer from './velocity.svelte';
import { distHaversine } from '../lib/mathutil.ts';
import { onMount, createEventDispatcher } from "svelte";
import * as L from "leaflet";
import type { Map as LeafletMap, LayerGroup } from "leaflet";
type LatLngExpression = [number, number] | { lat: number; lng: number };
type LatLngLiteral = { lat: number; lng: number };
import "leaflet/dist/leaflet.css";
import VelocityLayer from "./velocity.svelte";
import { distHaversine } from "../lib/mathutil.ts";
import { latestPredictionParsed } from '../lib/prediction.ts';
import { latestTelemetryParsed } from '../lib/telemetry.ts';
interface Point {
latlng: LatLngLiteral & { alt: number };
datetime: Date;
}
let map: typeof LeafletMap;
interface PredictionData {
launch: Point;
landing: Point;
burst: Point;
flight_path: LatLngExpression[];
flight_time: number;
}
interface TelemetryPoint {
altitude: number;
datetime: string;
latitude: number;
longitude: number;
}
interface TelemetryData {
launch: Point;
datapoints: TelemetryPoint[];
flight_path: LatLngExpression[];
}
/**
* @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 inputLat = '56.3576';
let inputLng = '39.8666';
let mapContainer;
let isSelecting = false;
let velocityOptions = {
displayValues: true,
displayOptions: {
velocityType: 'Global Wind',
position: 'bottomleft',
emptyString: 'No velocity data',
velocityType: "Global Wind",
position: "bottomleft",
emptyString: "No velocity data",
},
data: null, // здесь будут ваши данные
data: null,
};
let marker: typeof Marker | null = null;
export { mouseLat, mouseLng, inputLat, inputLng, updateMapPosition };
const dispatch = createEventDispatcher<{ coordinatesSelected: { lat: number; lng: number } }>();
const updateMapPosition = () => {
const lat = parseFloat(inputLat);
const lng = parseFloat(inputLng);
if (isNaN(lat)) {
alert("Please enter a valid latitude");
return;
}
if (isNaN(lng)) {
alert("Please enter a valid longitude");
return;
}
if (lat < -90 || lat > 90) {
alert("Latitude must be between -90 and 90");
return;
}
if (lng < -180 || lng > 180) {
alert("Longitude must be between -180 and 180");
return;
}
// Remove existing marker if it exists
if (marker) {
map.removeLayer(marker);
}
// Create new marker
marker = L.marker([lat, lng]).addTo(map)
.bindPopup("Launch Point");
// Center map on new coordinates
map.setView([lat, lng], map.getZoom());
};
onMount(async () => {
map = L.map('map').setView([51.505, -0.09], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
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: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
}).addTo(map);
// Загрузка данных для velocity (пример)
const response = await fetch('src/routes/testVelo.json');
const response = await fetch("src/routes/testVelo.json");
velocityOptions.data = await response.json();
map.on('mousemove', (e: any) => {
mouseLat = e.latlng.lat.toFixed(6);
mouseLng = e.latlng.lng.toFixed(6);
map.on("mousemove", (e: any) => {
mouseLat = e.latlng.lat;
mouseLng = e.latlng.lng;
});
marker = L.marker([56.3576, 39.8666]).addTo(map)
.bindPopup(() => {
return `
<b>Launch Point</b><br>, Lat: ${marker.getLatLng().lat.toFixed(6)}<br>, Long: ${marker.getLatLng().lng.toFixed(6)}<br>
`;
});
latestPredictionParsed.subscribe((prediction) => {
if (prediction) {
plotPrediction(prediction);
map.on("click", (e: any) => {
if (isSelecting) {
dispatch("coordinatesSelected", { lat: e.latlng.lat, lng: e.latlng.lng });
stopSelection();
}
});
});
const plotPrediction = (prediction: any) => {
console.log("Flight data parsed, creating map plot...");
$: if (map && data) {
plotData(data);
} else if (map) {
clearMapLayers();
}
// Clear existing map items
if (marker) {
map.eachLayer((layer: any) => {
if (layer instanceof L.Marker || layer instanceof L.Polyline) {
map.removeLayer(layer);
}
});
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;
// Calculate range and time of flight
const range = distHaversine(launch.latlng, landing.latlng, 1);
const f_hours = Math.floor(flight_time / 3600);
const f_minutes = Math.floor(((flight_time % 86400) % 3600) / 60).toString().padStart(2, '0');
const f_minutes = Math.floor((flight_time % 3600) / 60).toString().padStart(2, "0");
const flighttime = `${f_hours}hr${f_minutes}`;
console.log(`Range: ${range}, Flight Time: ${flighttime}`);
// Create custom icons
const launchIcon = L.icon({
iconUrl: 'target-blue.png',
iconSize: [10, 10],
iconAnchor: [5, 5],
});
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);
const landIcon = L.icon({
iconUrl: 'target-red.png',
iconSize: [10, 10],
iconAnchor: [5, 5],
});
L.polyline(flight_path, { weight: 3, color: "#000000" }).addTo(plotLayerGroup);
const burstIcon = L.icon({
iconUrl: 'pop-marker.png',
iconSize: [16, 16],
iconAnchor: [8, 8],
});
// Add markers to the map
const launchMarker = L.marker(launch.latlng, {
title: `Launch (${launch.latlng.lat.toFixed(4)}, ${launch.latlng.lng.toFixed(4)}) at ${launch.datetime.toUTCString()}`,
icon: launchIcon,
}).addTo(map);
const landMarker = L.marker(landing.latlng, {
title: `Landing (${landing.latlng.lat.toFixed(4)}, ${landing.latlng.lng.toFixed(4)}) at ${landing.datetime.toUTCString()}`,
icon: landIcon,
}).addTo(map);
const burstMarker = L.marker(burst.latlng, {
title: `Burst (${burst.latlng.lat.toFixed(4)}, ${burst.latlng.lng.toFixed(4)} at altitude ${burst.latlng.alt.toFixed(0)}) at ${burst.datetime.toUTCString()}`,
icon: burstIcon,
}).addTo(map);
// Add flight path polyline
const pathPolyline = L.polyline(flight_path, {
weight: 3,
color: "#000000",
}).addTo(map);
// Center the map on the launch point
map.setView(launch.latlng, 8);
map?.fitBounds(L.latLngBounds(flight_path));
};
const plotTelemetryTrack = (telemetry: any) => {
console.log("Telemetry data parsed, creating map plot...");
const plotTelemetry = (telemetry: TelemetryData) => {
L.marker(telemetry.launch.latlng, { title: `Launch`, icon: launchIcon }).addTo(plotLayerGroup);
// Clear existing map items
if (marker) {
map.eachLayer((layer: any) => {
if (layer instanceof L.Marker || layer instanceof L.Polyline) {
map.removeLayer(layer);
}
});
}
// Create custom icons for telemetry markers
const telemetryIcon = L.icon({
iconUrl: 'marker-sm-red.png',
iconSize: [10, 10],
iconAnchor: [5, 5],
});
const launchIcon = L.icon({
iconUrl: 'target-blue.png',
iconSize: [10, 10],
iconAnchor: [5, 5],
});
// Add markers to the map
const launchMarker = L.marker(telemetry.launch.latlng, {
title: `Launch (${telemetry.launch.latlng.lat.toFixed(4)}, ${telemetry.launch.latlng.lng.toFixed(4)}) at ${telemetry.launch.datetime.toUTCString()}`,
icon: launchIcon,
}).addTo(map);
// interface TelemetryPoint {
// altitude: number;
// datetime: string;
// latitude: number;
// longitude: number;
// payload: string;
// }
// Add telemetry markers to the map
telemetry.datapoints.forEach((point: any) => {
const telemetryMarker = L.marker([point.latitude, point.longitude], {
title: `Telemetry (${point.latitude.toFixed(4)}, ${point.longitude.toFixed(4)}) at ${point.datetime}`,
telemetry.datapoints.forEach((point) => {
L.marker([point.latitude, point.longitude], {
title: `Telemetry at ${point.datetime}`,
icon: telemetryIcon,
}).addTo(map)
.bindPopup(() => {
return `
<b>Telemetry Point</b><br>, Lat: ${point.latitude.toFixed(6)}<br>, Lon: ${point.longitude.toFixed(6)}<br>
`;
});
})
.bindPopup(`<b>Telemetry Point</b><br>Lat: ${point.latitude.toFixed(6)}<br>Lon: ${point.longitude.toFixed(6)}`)
.addTo(plotLayerGroup);
});
// Add flight path polyline
const pathPolyline = L.polyline(telemetry.flight_path, {
weight: 3,
color: "#000000",
}).addTo(map);
};
L.polyline(telemetry.flight_path, { weight: 3, color: "#000000" }).addTo(plotLayerGroup);
map?.fitBounds(L.latLngBounds(telemetry.flight_path));
};
</script>
<div class="map-container">
<div id="map"></div>
<div class="map-container" bind:this={mapContainer}>
<div class="card coordinates-display">
<p class="card-text"><b>Lat:</b> {mouseLat}, <b>Lon:</b> {mouseLng}</p>
<p class="card-text">
<b>Lat:</b>
{mouseLat.toFixed(6)},
<b>Lon:</b>
{mouseLng.toFixed(6)}
</p>
</div>
<div class="panel-container">
<slot></slot>
<slot />
</div>
</div>
<div bind:this={mapContainer} style="width: 100%; height: 100vh;">
{#if map}
<VelocityLayer {map} {velocityOptions} />
{/if}
{#if map}
<VelocityLayer {map} {velocityOptions} />
{/if}
</div>
<style>
@ -243,26 +177,19 @@
height: 100vh;
}
#map {
width: 100%;
height: calc(100% - 44px); /* Adjust height to account for navbar */
position: absolute;
top: 40px;
left: 0;
}
.coordinates-display {
position: absolute;
top: 54px;
right: 10px;
background: #fff; /* Remove transparency */
padding: 3px 8px; /* Reduce padding */
background: #fff;
padding: 3px 8px;
font-family: Arial, sans-serif;
font-size: 14px;
z-index: 1000; /* Ensure it's above the map */
z-index: 1000;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
border: 1px solid #ccc; /* Add card border */
width: 150px; /* Fixed width */
border: 1px solid #ccc;
width: auto;
white-space: nowrap;
}
.panel-container {
@ -271,4 +198,4 @@
right: 20px;
z-index: 1000;
}
</style>
</style>