replaced leaflet with map libre
This commit is contained in:
parent
ffb27c2e0a
commit
6359ccf9ee
10 changed files with 708 additions and 412 deletions
|
|
@ -5,7 +5,6 @@
|
|||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<link rel="stylesheet" href="%sveltekit.assets%/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="%sveltekit.assets%/css/bootstrap-icons.css" />
|
||||
<link rel="stylesheet" href="%sveltekit.assets%/ext/leaflet-ruler/leaflet-ruler.css" />
|
||||
<link rel="stylesheet" href="%sveltekit.assets%/css/custom.css" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
<script lang="ts">
|
||||
import { onMount, createEventDispatcher } from "svelte";
|
||||
import * as L from "leaflet";
|
||||
import { ruler, Ruler } from "$lib/ext/leaflet-ruler/leaflet-ruler";
|
||||
import type { Map as LeafletMap, LayerGroup } from "leaflet";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import maplibregl from "maplibre-gl";
|
||||
import type { Map as MapLibreMap, Marker, LngLatBoundsLike } from "maplibre-gl";
|
||||
import "maplibre-gl/dist/maplibre-gl.css";
|
||||
import WindVisualization from "$lib/components/WindVisualisation.svelte";
|
||||
import { distHaversine } from "$lib/mathutil";
|
||||
import type { Prediction, Telemetry } from "$lib/types";
|
||||
|
|
@ -11,9 +10,9 @@
|
|||
export let mode: "prediction" | "telemetry" = "prediction";
|
||||
export let data: Prediction | Telemetry | null = null;
|
||||
|
||||
let map: LeafletMap;
|
||||
let map: MapLibreMap;
|
||||
let mapContainer: HTMLDivElement;
|
||||
let plotLayerGroup: LayerGroup;
|
||||
let markers: Marker[] = [];
|
||||
let mouseLat = 0;
|
||||
let mouseLng = 0;
|
||||
let isSelecting = false;
|
||||
|
|
@ -25,30 +24,50 @@
|
|||
onMount(async () => {
|
||||
if (!mapContainer) return;
|
||||
|
||||
map = L.map(mapContainer, { zoomControl: false }).setView([51.505, -0.09], 13);
|
||||
L.control.zoom({ position: "bottomleft" }).addTo(map);
|
||||
map = new maplibregl.Map({
|
||||
container: mapContainer,
|
||||
style: {
|
||||
version: 8,
|
||||
sources: {
|
||||
osm: {
|
||||
type: "raster",
|
||||
tiles: ["https://a.tile.openstreetmap.org/{z}/{x}/{y}.png"],
|
||||
tileSize: 256,
|
||||
attribution:
|
||||
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
||||
},
|
||||
},
|
||||
layers: [
|
||||
{
|
||||
id: "osm",
|
||||
type: "raster",
|
||||
source: "osm",
|
||||
minzoom: 0,
|
||||
maxzoom: 19,
|
||||
},
|
||||
],
|
||||
},
|
||||
center: [-0.09, 51.505],
|
||||
zoom: 13,
|
||||
});
|
||||
|
||||
plotLayerGroup = L.layerGroup().addTo(map);
|
||||
// Add navigation control (zoom buttons)
|
||||
map.addControl(new maplibregl.NavigationControl(), "bottom-left");
|
||||
|
||||
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
||||
}).addTo(map);
|
||||
|
||||
ruler({
|
||||
position: "bottomright",
|
||||
}).addTo(map);
|
||||
// Add scale control
|
||||
map.addControl(new maplibregl.ScaleControl({ maxWidth: 100, unit: "metric" }), "bottom-right");
|
||||
|
||||
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("mousemove", (e: maplibregl.MapMouseEvent) => {
|
||||
mouseLat = e.lngLat.lat;
|
||||
mouseLng = e.lngLat.lng;
|
||||
});
|
||||
|
||||
map.on("click", (e: any) => {
|
||||
map.on("click", (e: maplibregl.MapMouseEvent) => {
|
||||
if (isSelecting) {
|
||||
dispatch("coordinatesSelected", { lat: e.latlng.lat, lng: e.latlng.lng });
|
||||
dispatch("coordinatesSelected", { lat: e.lngLat.lat, lng: e.lngLat.lng });
|
||||
stopSelection();
|
||||
}
|
||||
});
|
||||
|
|
@ -79,15 +98,64 @@
|
|||
};
|
||||
|
||||
export const clearMapLayers = () => {
|
||||
plotLayerGroup?.clearLayers();
|
||||
// Remove all markers
|
||||
markers.forEach((marker) => marker.remove());
|
||||
markers = [];
|
||||
|
||||
// Remove all layers and sources related to flight paths
|
||||
if (map && map.getLayer("flight-path")) {
|
||||
map.removeLayer("flight-path");
|
||||
}
|
||||
if (map && map.getSource("flight-path")) {
|
||||
map.removeSource("flight-path");
|
||||
}
|
||||
if (map && map.getLayer("telemetry-path")) {
|
||||
map.removeLayer("telemetry-path");
|
||||
}
|
||||
if (map && map.getSource("telemetry-path")) {
|
||||
map.removeSource("telemetry-path");
|
||||
}
|
||||
};
|
||||
|
||||
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 createMarker = (
|
||||
lng: number,
|
||||
lat: number,
|
||||
color: string,
|
||||
iconUrl: string,
|
||||
title: string,
|
||||
) => {
|
||||
const el = document.createElement("div");
|
||||
el.className = "custom-marker";
|
||||
el.style.backgroundImage = `url(${iconUrl})`;
|
||||
el.style.width = "10px";
|
||||
el.style.height = "10px";
|
||||
el.style.backgroundSize = "100%";
|
||||
el.title = title;
|
||||
|
||||
const marker = new maplibregl.Marker({ element: el }).setLngLat([lng, lat]).addTo(map);
|
||||
|
||||
markers.push(marker);
|
||||
return marker;
|
||||
};
|
||||
|
||||
const createBurstMarker = (lng: number, lat: number, title: string) => {
|
||||
const el = document.createElement("div");
|
||||
el.className = "custom-marker";
|
||||
el.style.backgroundImage = `url(pop-marker.png)`;
|
||||
el.style.width = "16px";
|
||||
el.style.height = "16px";
|
||||
el.style.backgroundSize = "100%";
|
||||
el.title = title;
|
||||
|
||||
const marker = new maplibregl.Marker({ element: el }).setLngLat([lng, lat]).addTo(map);
|
||||
|
||||
markers.push(marker);
|
||||
return marker;
|
||||
};
|
||||
|
||||
const plotPrediction = (prediction: Prediction) => {
|
||||
clearMapLayers();
|
||||
|
||||
const { launch, landing, burst, flight_path, flight_time } = prediction;
|
||||
|
||||
const range = distHaversine(launch.latlng, landing.latlng, 1);
|
||||
|
|
@ -97,43 +165,154 @@
|
|||
.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);
|
||||
// Helper to extract lat/lng from either format
|
||||
const getLat = (latlng: any) => (Array.isArray(latlng) ? latlng[0] : latlng.lat);
|
||||
const getLng = (latlng: any) => (Array.isArray(latlng) ? latlng[1] : latlng.lng);
|
||||
|
||||
L.polyline(flight_path, { weight: 3, color: "#000000" }).addTo(plotLayerGroup);
|
||||
// Create markers (MapLibre uses [lng, lat] order)
|
||||
createMarker(getLng(launch.latlng), getLat(launch.latlng), "#0000ff", "target-blue.png", "Launch");
|
||||
createMarker(getLng(landing.latlng), getLat(landing.latlng), "#ff0000", "target-red.png", "Landing");
|
||||
createBurstMarker(getLng(burst.latlng), getLat(burst.latlng), "Burst");
|
||||
|
||||
map?.fitBounds(L.latLngBounds(flight_path));
|
||||
// Add flight path as a line (convert [lat, lng] to [lng, lat] for MapLibre)
|
||||
const coordinates = flight_path.map((coord) => {
|
||||
if (Array.isArray(coord)) {
|
||||
return [coord[1], coord[0]]; // [lat, lng, alt?] -> [lng, lat]
|
||||
} else {
|
||||
return [coord.lng, coord.lat]; // {lat, lng} -> [lng, lat]
|
||||
}
|
||||
});
|
||||
|
||||
map.addSource("flight-path", {
|
||||
type: "geojson",
|
||||
data: {
|
||||
type: "Feature",
|
||||
properties: {},
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
coordinates: coordinates,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
map.addLayer({
|
||||
id: "flight-path",
|
||||
type: "line",
|
||||
source: "flight-path",
|
||||
layout: {
|
||||
"line-join": "round",
|
||||
"line-cap": "round",
|
||||
},
|
||||
paint: {
|
||||
"line-color": "#000000",
|
||||
"line-width": 3,
|
||||
},
|
||||
});
|
||||
|
||||
// Fit bounds to show entire path
|
||||
const bounds = coordinates.reduce(
|
||||
(bounds, coord) => {
|
||||
return bounds.extend(coord as [number, number]);
|
||||
},
|
||||
new maplibregl.LngLatBounds(coordinates[0] as [number, number], coordinates[0] as [number, number]),
|
||||
);
|
||||
|
||||
map.fitBounds(bounds as LngLatBoundsLike, { padding: 50 });
|
||||
};
|
||||
|
||||
const plotTelemetry = (telemetry: Telemetry) => {
|
||||
L.marker(telemetry.launch.latlng, { title: `Launch`, icon: launchIcon }).addTo(plotLayerGroup);
|
||||
clearMapLayers();
|
||||
|
||||
// Helper to extract lat/lng from either format
|
||||
const getLat = (latlng: any) => (Array.isArray(latlng) ? latlng[0] : latlng.lat);
|
||||
const getLng = (latlng: any) => (Array.isArray(latlng) ? latlng[1] : latlng.lng);
|
||||
|
||||
// Launch marker (MapLibre uses [lng, lat] order)
|
||||
createMarker(
|
||||
getLng(telemetry.launch.latlng),
|
||||
getLat(telemetry.launch.latlng),
|
||||
"#0000ff",
|
||||
"target-blue.png",
|
||||
"Launch",
|
||||
);
|
||||
|
||||
// Telemetry point markers with popups
|
||||
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);
|
||||
const el = document.createElement("div");
|
||||
el.className = "custom-marker";
|
||||
el.style.backgroundImage = `url(marker-sm-red.png)`;
|
||||
el.style.width = "10px";
|
||||
el.style.height = "10px";
|
||||
el.style.backgroundSize = "100%";
|
||||
|
||||
const popup = new maplibregl.Popup({ offset: 25 }).setHTML(
|
||||
`<b>Telemetry Point</b><br>Lat: ${point.latitude.toFixed(6)}<br>Lon: ${point.longitude.toFixed(6)}`,
|
||||
);
|
||||
|
||||
const marker = new maplibregl.Marker({ element: el })
|
||||
.setLngLat([point.longitude, point.latitude])
|
||||
.setPopup(popup)
|
||||
.addTo(map);
|
||||
|
||||
markers.push(marker);
|
||||
});
|
||||
|
||||
L.polyline(telemetry.flight_path, { weight: 3, color: "#000000" }).addTo(plotLayerGroup);
|
||||
// Add flight path as a line (convert [lat, lng] to [lng, lat] for MapLibre)
|
||||
const coordinates = telemetry.flight_path.map((coord) => {
|
||||
if (Array.isArray(coord)) {
|
||||
return [coord[1], coord[0]]; // [lat, lng, alt?] -> [lng, lat]
|
||||
} else {
|
||||
return [coord.lng, coord.lat]; // {lat, lng} -> [lng, lat]
|
||||
}
|
||||
});
|
||||
|
||||
map?.fitBounds(L.latLngBounds(telemetry.flight_path));
|
||||
map.addSource("telemetry-path", {
|
||||
type: "geojson",
|
||||
data: {
|
||||
type: "Feature",
|
||||
properties: {},
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
coordinates: coordinates,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
map.addLayer({
|
||||
id: "telemetry-path",
|
||||
type: "line",
|
||||
source: "telemetry-path",
|
||||
layout: {
|
||||
"line-join": "round",
|
||||
"line-cap": "round",
|
||||
},
|
||||
paint: {
|
||||
"line-color": "#000000",
|
||||
"line-width": 3,
|
||||
},
|
||||
});
|
||||
|
||||
// Fit bounds to show entire path
|
||||
const bounds = coordinates.reduce(
|
||||
(bounds, coord) => {
|
||||
return bounds.extend(coord as [number, number]);
|
||||
},
|
||||
new maplibregl.LngLatBounds(coordinates[0] as [number, number], coordinates[0] as [number, number]),
|
||||
);
|
||||
|
||||
map.fitBounds(bounds as LngLatBoundsLike, { padding: 50 });
|
||||
};
|
||||
|
||||
export const panTo = (lat: number, lng: number) => {
|
||||
if (map) {
|
||||
map.setView([lat, lng], map.getZoom());
|
||||
map.setCenter([lng, lat]);
|
||||
}
|
||||
};
|
||||
|
||||
export const zoomTo = (lat: number, lng: number, zoomLevel: number) => {
|
||||
if (map) {
|
||||
map.setView([lat, lng], zoomLevel);
|
||||
map.setCenter([lng, lat]);
|
||||
map.setZoom(zoomLevel);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -156,3 +335,31 @@
|
|||
<WindVisualization {map} {windData} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- <style>
|
||||
.map-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.coordinates-display {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
z-index: 1000;
|
||||
padding: 8px 12px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.card-text {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:global(.custom-marker) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style> -->
|
||||
|
|
|
|||
|
|
@ -1,289 +1,129 @@
|
|||
<script>
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import L from "leaflet";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import "leaflet-velocity/dist/leaflet-velocity.css";
|
||||
import "leaflet-velocity/dist/leaflet-velocity";
|
||||
import "leaflet.heat";
|
||||
import "leaflet-timedimension";
|
||||
|
||||
export let map; // принимаем карту из родительского компонента
|
||||
export let map; // MapLibre map instance from parent component
|
||||
export let windData;
|
||||
|
||||
let timeDimension;
|
||||
let timeDimensionControl;
|
||||
let velocityLayer;
|
||||
let heatLayer;
|
||||
let legend;
|
||||
// State for layer toggles
|
||||
let showHeatmap = false;
|
||||
let showVectors = false;
|
||||
|
||||
// Состояние переключателей
|
||||
let showHeatmap = true;
|
||||
let showVectors = true;
|
||||
let layerControl;
|
||||
|
||||
// Преобразование testVelo.json в формат timeData
|
||||
const prepareTimeData = (windData) => {
|
||||
if (!windData || windData.length < 2) return {};
|
||||
|
||||
// Используем дату из header или текущую дату, если не указана
|
||||
const refTime = windData[0]?.header?.refTime || new Date().toISOString();
|
||||
|
||||
return {
|
||||
[refTime]: {
|
||||
u: windData[0].data, // U-компонента (первый объект в массиве)
|
||||
v: windData[1].data, // V-компонента (второй объект)
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// Функция для нормализации данных тепловой карты
|
||||
const prepareHeatData = (windData) => {
|
||||
if (!windData || windData.length < 2) {
|
||||
console.warn("Invalid wind data structure");
|
||||
return [];
|
||||
}
|
||||
|
||||
// Получаем U и V компоненты
|
||||
const uComponent = windData.find((item) => item.header.parameterNumber === 2);
|
||||
const vComponent = windData.find((item) => item.header.parameterNumber === 3);
|
||||
|
||||
if (!uComponent || !vComponent) {
|
||||
console.warn("Missing wind components");
|
||||
return [];
|
||||
}
|
||||
|
||||
const header = uComponent.header; // Используем header из U компоненты
|
||||
const { lo1, la1, dx, dy, nx, ny } = header;
|
||||
const heatData = [];
|
||||
let maxSpeed = 0;
|
||||
|
||||
// Проверяем совпадение размеров данных
|
||||
if (uComponent.data.length !== vComponent.data.length) {
|
||||
console.warn("U and V components have different lengths");
|
||||
return [];
|
||||
}
|
||||
|
||||
// Собираем данные и находим максимальную скорость
|
||||
for (let i = 0; i < uComponent.data.length; i++) {
|
||||
const u = uComponent.data[i];
|
||||
const v = vComponent.data[i];
|
||||
const speed = Math.sqrt(u * u + v * v);
|
||||
|
||||
if (!isNaN(speed)) {
|
||||
// Вычисляем координаты для текущей точки
|
||||
const y = Math.floor(i / nx);
|
||||
const x = i % nx;
|
||||
let lat = la1 - y * dy;
|
||||
let lng = lo1 + x * dx;
|
||||
if (lng >= 180) lng -= 360;
|
||||
heatData.push([lat, lng, speed]);
|
||||
maxSpeed = Math.max(maxSpeed, speed);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Prepared heat data: ${heatData.length} points, max speed: ${maxSpeed}`);
|
||||
|
||||
// Нормализуем значения интенсивности от 0 до 1
|
||||
if (maxSpeed > 0) {
|
||||
return heatData.map(([lat, lng, intensity]) => [lat, lng, intensity / maxSpeed]);
|
||||
}
|
||||
|
||||
return heatData;
|
||||
};
|
||||
|
||||
// Создание тепловой карты
|
||||
const createHeatLayer = (data) => {
|
||||
if (!data || data.length === 0) {
|
||||
console.warn("No valid heat data provided");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return L.heatLayer(data, {
|
||||
radius: 8, // Увеличьте радиус для глобальной карты
|
||||
blur: 20,
|
||||
// maxZoom: 10,
|
||||
minOpacity: 0.7,
|
||||
gradient: {
|
||||
0.1: "blue",
|
||||
0.3: "cyan",
|
||||
0.5: "lime",
|
||||
0.7: "yellow",
|
||||
1.0: "red",
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Failed to create heat layer:", e);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Обновление слоев
|
||||
const updateLayers = () => {
|
||||
if (!map || !windData) return;
|
||||
|
||||
// Удаляем старые слои
|
||||
if (velocityLayer) map.removeLayer(velocityLayer);
|
||||
if (heatLayer) map.removeLayer(heatLayer);
|
||||
if (legend) map.removeControl(legend);
|
||||
|
||||
// Создаем слой векторов ветра
|
||||
if (showVectors) {
|
||||
velocityLayer = L.velocityLayer({
|
||||
displayValues: true,
|
||||
displayOptions: {
|
||||
velocityType: "Wind Speed",
|
||||
position: "bottomright",
|
||||
emptyString: "No wind data",
|
||||
},
|
||||
data: windData,
|
||||
}).addTo(map);
|
||||
}
|
||||
|
||||
// Создаем тепловую карту
|
||||
if (showHeatmap) {
|
||||
const heatData = prepareHeatData(windData);
|
||||
heatLayer = createHeatLayer(heatData);
|
||||
|
||||
if (heatLayer) {
|
||||
heatLayer.addTo(map);
|
||||
createLegend(Math.max(...heatData.map((point) => point[2])));
|
||||
}
|
||||
}
|
||||
|
||||
// Обновляем контроль слоев
|
||||
updateLayerControl();
|
||||
};
|
||||
|
||||
const updateLayerControl = () => {
|
||||
if (layerControl) {
|
||||
map.removeControl(layerControl);
|
||||
}
|
||||
|
||||
const overlays = {};
|
||||
|
||||
if (velocityLayer) {
|
||||
overlays["Векторы ветра"] = velocityLayer;
|
||||
}
|
||||
|
||||
if (heatLayer) {
|
||||
overlays["Тепловая карта"] = heatLayer;
|
||||
}
|
||||
|
||||
layerControl = L.control
|
||||
.layers(null, overlays, {
|
||||
collapsed: false,
|
||||
position: "topright",
|
||||
})
|
||||
.addTo(map);
|
||||
};
|
||||
// Создание легенды с учетом максимальной скорости
|
||||
const createLegend = (maxSpeed) => {
|
||||
if (!map) return;
|
||||
|
||||
legend = L.control({ position: "bottomright" });
|
||||
|
||||
legend.onAdd = () => {
|
||||
const div = L.DomUtil.create("div", "wind-heat-legend");
|
||||
div.innerHTML = `
|
||||
<h4>Wind Speed (m/s)</h4>
|
||||
<div class="legend-scale">
|
||||
<div class="legend-color" style="background: #0000FF;"></div>
|
||||
<div class="legend-color" style="background: #00FFFF;"></div>
|
||||
<div class="legend-color" style="background: #00FF00;"></div>
|
||||
<div class="legend-color" style="background: #FFFF00;"></div>
|
||||
<div class="legend-color" style="background: #FF0000;"></div>
|
||||
</div>
|
||||
<div class="legend-labels">
|
||||
<span>0</span>
|
||||
<span>${(maxSpeed * 0.25).toFixed(1)}</span>
|
||||
<span>${(maxSpeed * 0.5).toFixed(1)}</span>
|
||||
<span>${(maxSpeed * 0.75).toFixed(1)}</span>
|
||||
<span>${maxSpeed.toFixed(1)}</span>
|
||||
</div>
|
||||
`;
|
||||
return div;
|
||||
};
|
||||
|
||||
legend.addTo(map);
|
||||
};
|
||||
// Note: This is a placeholder implementation
|
||||
// MapLibre GL JS does not have direct equivalents for leaflet-velocity and leaflet.heat
|
||||
// These features would need to be implemented using:
|
||||
// 1. Custom WebGL layers for wind visualization
|
||||
// 2. Heatmap layers using MapLibre's native heatmap style
|
||||
// 3. Third-party libraries like deck.gl or mapbox-gl plugins
|
||||
|
||||
onMount(() => {
|
||||
if (!map) return;
|
||||
if (!map || !windData) return;
|
||||
|
||||
// 1. Настройка TimeDimension (добавьте эти строки в начале)
|
||||
// L.TimeDimension.Util.setProxy('https://your-proxy.com/?url='); // Для загрузки больших данных
|
||||
L.TimeDimension.Util.setCacheLimit(10); // Лимит кэшированных кадров
|
||||
console.log("WindVisualization mounted with MapLibre map");
|
||||
console.log("Wind data available:", windData);
|
||||
|
||||
// 1. Подготовка данных
|
||||
const timeData = prepareTimeData(windData);
|
||||
const firstTime = Object.keys(timeData)[0];
|
||||
|
||||
// Инициализация TimeDimension
|
||||
timeDimension = new L.TimeDimension({
|
||||
period: "PT1H", // Интервал 1 час
|
||||
timeInterval: "${firstTime}/${firstTime}",
|
||||
});
|
||||
|
||||
// Добавляем контролы времени
|
||||
timeDimensionControl = new L.Control.TimeDimension({
|
||||
timeDimension,
|
||||
position: "bottomleft",
|
||||
// autoPlay: true,
|
||||
playerOptions: {
|
||||
// transitionTime: 1000,
|
||||
loop: false,
|
||||
minBufferReady: -1,
|
||||
},
|
||||
});
|
||||
map.addControl(timeDimensionControl);
|
||||
|
||||
// 4. Создание слоев
|
||||
const velocityLayer = L.timeDimension.layer
|
||||
.windVelocity({
|
||||
displayValues: true,
|
||||
data: timeData,
|
||||
displayOptions: {
|
||||
velocityType: "Wind Speed",
|
||||
position: "bottomleft",
|
||||
},
|
||||
})
|
||||
.addTo(map);
|
||||
|
||||
// 5. Тепловая карта (адаптируйте под ваш формат)
|
||||
const heatLayer = L.timeDimension.layer
|
||||
.heat({
|
||||
radius: 15,
|
||||
data: prepareTimeHeatData(timeData),
|
||||
})
|
||||
.addTo(map);
|
||||
// TODO: Implement wind visualization using MapLibre GL JS
|
||||
// Possible approaches:
|
||||
// 1. Use MapLibre's native heatmap layer type for heat visualization
|
||||
// 2. Use deck.gl ScreenGridLayer or HeatmapLayer for advanced heatmaps
|
||||
// 3. Use custom WebGL shaders for wind particle animation
|
||||
// 4. Use mapbox-gl-wind plugin (if compatible with MapLibre)
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
// Clean up any layers or resources when component is destroyed
|
||||
if (map) {
|
||||
if (velocityLayer) map.removeLayer(velocityLayer);
|
||||
if (heatLayer) map.removeLayer(heatLayer);
|
||||
if (legend) map.removeControl(legend);
|
||||
// Remove any added layers
|
||||
console.log("WindVisualization destroyed");
|
||||
}
|
||||
});
|
||||
|
||||
// Реактивность на изменение параметров
|
||||
// Reactive statement for layer updates
|
||||
$: if (map && windData) {
|
||||
updateLayers();
|
||||
}
|
||||
|
||||
const updateLayers = () => {
|
||||
if (!map || !windData) return;
|
||||
|
||||
console.log("Updating wind layers:", { showHeatmap, showVectors });
|
||||
|
||||
// TODO: Implement layer toggling
|
||||
// This would involve adding/removing MapLibre layers based on the toggle state
|
||||
};
|
||||
</script>
|
||||
|
||||
<!--
|
||||
IMPORTANT: This is a simplified placeholder implementation.
|
||||
The original Leaflet-based wind visualization used these plugins:
|
||||
- leaflet-velocity: For wind vector visualization
|
||||
- leaflet.heat: For heatmap visualization
|
||||
- leaflet-timedimension: For time-based animation
|
||||
|
||||
To fully implement wind visualization in MapLibre GL JS, you would need to:
|
||||
|
||||
1. For Wind Vectors:
|
||||
- Use a custom WebGL layer with particle animation
|
||||
- Or use deck.gl's ParticleLayer or FlowmapLayer
|
||||
- Or port/adapt the wind-gl-core library
|
||||
|
||||
2. For Heatmap:
|
||||
- Use MapLibre's native 'heatmap' layer type
|
||||
- Convert wind data to GeoJSON point features
|
||||
- Style with appropriate color gradients
|
||||
|
||||
3. For Time Dimension:
|
||||
- Implement custom time controls
|
||||
- Update data sources based on selected time
|
||||
- Use requestAnimationFrame for smooth animation
|
||||
|
||||
Example MapLibre heatmap implementation:
|
||||
|
||||
map.addSource('wind-heat', {
|
||||
type: 'geojson',
|
||||
data: {
|
||||
type: 'FeatureCollection',
|
||||
features: windPoints // Array of GeoJSON point features
|
||||
}
|
||||
});
|
||||
|
||||
map.addLayer({
|
||||
id: 'wind-heatmap',
|
||||
type: 'heatmap',
|
||||
source: 'wind-heat',
|
||||
paint: {
|
||||
'heatmap-weight': ['get', 'intensity'],
|
||||
'heatmap-intensity': 1,
|
||||
'heatmap-color': [
|
||||
'interpolate',
|
||||
['linear'],
|
||||
['heatmap-density'],
|
||||
0, 'rgba(0,0,255,0)',
|
||||
0.2, 'rgb(0,0,255)',
|
||||
0.4, 'rgb(0,255,255)',
|
||||
0.6, 'rgb(0,255,0)',
|
||||
0.8, 'rgb(255,255,0)',
|
||||
1, 'rgb(255,0,0)'
|
||||
],
|
||||
'heatmap-radius': 20,
|
||||
'heatmap-opacity': 0.7
|
||||
}
|
||||
});
|
||||
-->
|
||||
|
||||
<div class="layer-controls">
|
||||
<div class="control-group">
|
||||
<label>
|
||||
<input type="checkbox" bind:checked={showHeatmap} />
|
||||
Тепловая карта
|
||||
<input type="checkbox" bind:checked={showHeatmap} disabled />
|
||||
Тепловая карта (TODO)
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" bind:checked={showVectors} />
|
||||
Векторы ветра
|
||||
<input type="checkbox" bind:checked={showVectors} disabled />
|
||||
Векторы ветра (TODO)
|
||||
</label>
|
||||
</div>
|
||||
<small style="color: #666; font-size: 11px; margin-top: 8px; display: block;">
|
||||
Wind visualization requires MapLibre implementation
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
|
@ -292,7 +132,7 @@
|
|||
bottom: 30px;
|
||||
left: 10px;
|
||||
z-index: 1000;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
|
||||
|
|
@ -311,35 +151,9 @@
|
|||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
:global(.wind-heat-legend) {
|
||||
padding: 8px 10px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 15px rgba(0, 0, 0, 0.2);
|
||||
line-height: 1.2;
|
||||
color: #333;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
:global(.wind-heat-legend h4) {
|
||||
margin: 0 0 5px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
:global(legend-scale) {
|
||||
display: flex;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
:global(legend-color) {
|
||||
height: 12px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
:global(.legend-labels) {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 11px;
|
||||
.control-group label:has(input:disabled) {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { writable } from "svelte/store";
|
||||
import type { LatLngExpression } from "leaflet";
|
||||
import L from "leaflet";
|
||||
import type { LatLngExpression } from "./types";
|
||||
|
||||
import { getCsrfToken } from "./auth";
|
||||
import type { PredictionStage, RawPrediction, Prediction, Point } from "./types";
|
||||
|
|
@ -129,7 +128,7 @@ export function parsePrediction(prediction: PredictionStage[]): Prediction {
|
|||
if (lon > 180.0) {
|
||||
lon -= 360.0;
|
||||
}
|
||||
launch.latlng = L.latLng([launchObj.latitude, lon, launchObj.altitude]);
|
||||
launch.latlng = { lat: launchObj.latitude, lng: lon, alt: launchObj.altitude };
|
||||
launch.datetime = new Date(launchObj.datetime);
|
||||
|
||||
const burstObj = descent[0];
|
||||
|
|
@ -137,7 +136,7 @@ export function parsePrediction(prediction: PredictionStage[]): Prediction {
|
|||
if (lon > 180.0) {
|
||||
lon -= 360.0;
|
||||
}
|
||||
burst.latlng = L.latLng([burstObj.latitude, lon, burstObj.altitude]);
|
||||
burst.latlng = { lat: burstObj.latitude, lng: lon, alt: burstObj.altitude };
|
||||
burst.datetime = new Date(burstObj.datetime);
|
||||
|
||||
const landingObj = descent[descent.length - 1];
|
||||
|
|
@ -145,7 +144,7 @@ export function parsePrediction(prediction: PredictionStage[]): Prediction {
|
|||
if (lon > 180.0) {
|
||||
lon -= 360.0;
|
||||
}
|
||||
landing.latlng = L.latLng([landingObj.latitude, lon, landingObj.altitude]);
|
||||
landing.latlng = { lat: landingObj.latitude, lng: lon, alt: landingObj.altitude };
|
||||
landing.datetime = new Date(landingObj.datetime);
|
||||
|
||||
const profile = prediction[1].stage === "descent" ? "standard_profile" : "float_profile";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { writable } from "svelte/store"
|
||||
import L from "leaflet";
|
||||
|
||||
import type { TelemetryPoint, Telemetry } from "./types";
|
||||
|
||||
|
|
@ -11,7 +10,7 @@ export function parseTelemetry(telemetry: TelemetryPoint[]): Telemetry {
|
|||
]);
|
||||
|
||||
const launch = {
|
||||
latlng: L.latLng(telemetry[0].latitude, telemetry[0].longitude),
|
||||
latlng: { lat: telemetry[0].latitude, lng: telemetry[0].longitude },
|
||||
datetime: new Date(telemetry[0].datetime)
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,10 @@
|
|||
import type { LatLngExpression, LatLngLiteral } from "leaflet";
|
||||
// Define coordinate types (previously from Leaflet)
|
||||
export type LatLngTuple = [number, number];
|
||||
export interface LatLngLiteral {
|
||||
lat: number;
|
||||
lng: number;
|
||||
}
|
||||
export type LatLngExpression = LatLngTuple | LatLngLiteral;
|
||||
|
||||
export const PROFILE_MAP = {
|
||||
"Обычный": "standard_profile",
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@
|
|||
import { PredictionStore } from "$lib/stores";
|
||||
import { addToast, removeToast } from "$lib/components/Toast.svelte";
|
||||
import ToastContainer from '$lib/components/Toast.svelte';
|
||||
import L, { point } from "leaflet";
|
||||
|
||||
let map: Map | null = null;
|
||||
let panelContainer: PanelContainer | null = null;
|
||||
|
|
@ -30,8 +29,13 @@
|
|||
if (panelContainer) {
|
||||
let element = panelContainer.getElement();
|
||||
if (!element) return;
|
||||
L.DomEvent.disableClickPropagation(element);
|
||||
L.DomEvent.disableScrollPropagation(element);
|
||||
|
||||
// Disable click and scroll propagation to prevent map interaction
|
||||
element.addEventListener('click', (e) => e.stopPropagation());
|
||||
element.addEventListener('dblclick', (e) => e.stopPropagation());
|
||||
element.addEventListener('mousedown', (e) => e.stopPropagation());
|
||||
element.addEventListener('touchstart', (e) => e.stopPropagation());
|
||||
element.addEventListener('wheel', (e) => e.stopPropagation());
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue