added mapcore.ts
This commit is contained in:
parent
e984b9730b
commit
2e6177fe74
2 changed files with 270 additions and 193 deletions
|
|
@ -1,8 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount, createEventDispatcher } from "svelte";
|
import { onMount, createEventDispatcher } from "svelte";
|
||||||
import maplibregl from "maplibre-gl";
|
import { MapLibreCore, type IMapCore, type IMapMarker } from "$lib/mapcore";
|
||||||
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 WindVisualization from "$lib/components/WindVisualisation.svelte";
|
||||||
import { distHaversine } from "$lib/mathutil";
|
import { distHaversine } from "$lib/mathutil";
|
||||||
import type { Prediction, Telemetry } from "$lib/types";
|
import type { Prediction, Telemetry } from "$lib/types";
|
||||||
|
|
@ -10,10 +8,10 @@
|
||||||
export let mode: "prediction" | "telemetry" = "prediction";
|
export let mode: "prediction" | "telemetry" = "prediction";
|
||||||
export let data: Prediction | Telemetry | null = null;
|
export let data: Prediction | Telemetry | null = null;
|
||||||
|
|
||||||
let map: MapLibreMap;
|
let mapCore: IMapCore;
|
||||||
let mapContainer: HTMLDivElement;
|
let mapContainer: HTMLDivElement;
|
||||||
let markers: Marker[] = [];
|
let markers: IMapMarker[] = [];
|
||||||
let animatedMarker: Marker | null = null;
|
let animatedMarker: IMapMarker | null = null;
|
||||||
let mouseLat = 0;
|
let mouseLat = 0;
|
||||||
let mouseLng = 0;
|
let mouseLng = 0;
|
||||||
let isSelecting = false;
|
let isSelecting = false;
|
||||||
|
|
@ -25,48 +23,21 @@
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if (!mapContainer) return;
|
if (!mapContainer) return;
|
||||||
|
|
||||||
map = new maplibregl.Map({
|
mapCore = new MapLibreCore();
|
||||||
container: mapContainer,
|
mapCore.init(mapContainer, { center: [-0.09, 51.505], zoom: 13 });
|
||||||
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,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add navigation control (zoom buttons)
|
mapCore.addNavigationControl("bottom-left");
|
||||||
map.addControl(new maplibregl.NavigationControl(), "bottom-left");
|
mapCore.addScaleControl({ maxWidth: 100, unit: "metric" }, "bottom-right");
|
||||||
|
|
||||||
// Add scale control
|
|
||||||
map.addControl(new maplibregl.ScaleControl({ maxWidth: 100, unit: "metric" }), "bottom-right");
|
|
||||||
|
|
||||||
const response = await fetch("src/routes/testVelo.json");
|
const response = await fetch("src/routes/testVelo.json");
|
||||||
windData = await response.json();
|
windData = await response.json();
|
||||||
|
|
||||||
map.on("mousemove", (e: maplibregl.MapMouseEvent) => {
|
mapCore.on("mousemove", (e) => {
|
||||||
mouseLat = e.lngLat.lat;
|
mouseLat = e.lngLat.lat;
|
||||||
mouseLng = e.lngLat.lng;
|
mouseLng = e.lngLat.lng;
|
||||||
});
|
});
|
||||||
|
|
||||||
map.on("click", (e: maplibregl.MapMouseEvent) => {
|
mapCore.on("click", (e) => {
|
||||||
if (isSelecting) {
|
if (isSelecting) {
|
||||||
dispatch("coordinatesSelected", { lat: e.lngLat.lat, lng: e.lngLat.lng });
|
dispatch("coordinatesSelected", { lat: e.lngLat.lat, lng: e.lngLat.lng });
|
||||||
stopSelection();
|
stopSelection();
|
||||||
|
|
@ -74,9 +45,9 @@
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$: if (map && data) {
|
$: if (mapCore && data) {
|
||||||
plotData(data);
|
plotData(data);
|
||||||
} else if (map) {
|
} else if (mapCore) {
|
||||||
clearMapLayers();
|
clearMapLayers();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -99,35 +70,18 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
export const clearMapLayers = () => {
|
export const clearMapLayers = () => {
|
||||||
// Remove all markers
|
|
||||||
markers.forEach((marker) => marker.remove());
|
markers.forEach((marker) => marker.remove());
|
||||||
markers = [];
|
markers = [];
|
||||||
|
|
||||||
// Remove animated marker
|
|
||||||
removeAnimatedMarker();
|
removeAnimatedMarker();
|
||||||
|
|
||||||
// Remove all layers and sources related to flight paths
|
if (mapCore && mapCore.hasLayer("flight-path")) mapCore.removeLayer("flight-path");
|
||||||
if (map && map.getLayer("flight-path")) {
|
if (mapCore && mapCore.hasSource("flight-path")) mapCore.removeSource("flight-path");
|
||||||
map.removeLayer("flight-path");
|
if (mapCore && mapCore.hasLayer("telemetry-path")) mapCore.removeLayer("telemetry-path");
|
||||||
}
|
if (mapCore && mapCore.hasSource("telemetry-path")) mapCore.removeSource("telemetry-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 createMarker = (
|
const createMarker = (lng: number, lat: number, iconUrl: string, title: string) => {
|
||||||
lng: number,
|
|
||||||
lat: number,
|
|
||||||
color: string,
|
|
||||||
iconUrl: string,
|
|
||||||
title: string,
|
|
||||||
) => {
|
|
||||||
const el = document.createElement("div");
|
const el = document.createElement("div");
|
||||||
el.className = "custom-marker";
|
el.className = "custom-marker";
|
||||||
el.style.backgroundImage = `url(${iconUrl})`;
|
el.style.backgroundImage = `url(${iconUrl})`;
|
||||||
|
|
@ -136,23 +90,18 @@
|
||||||
el.style.backgroundSize = "100%";
|
el.style.backgroundSize = "100%";
|
||||||
el.title = title;
|
el.title = title;
|
||||||
|
|
||||||
// Create popup with coordinates
|
const popup = mapCore
|
||||||
const popup = new maplibregl.Popup({ offset: 25, closeButton: false }).setHTML(
|
.createPopup({ offset: 25, closeButton: false })
|
||||||
`<b>${title}</b><br>Lat: ${lat.toFixed(6)}<br>Lng: ${lng.toFixed(6)}`,
|
.setHTML(`<b>${title}</b><br>Lat: ${lat.toFixed(6)}<br>Lng: ${lng.toFixed(6)}`);
|
||||||
);
|
|
||||||
|
|
||||||
const marker = new maplibregl.Marker({ element: el })
|
const marker = mapCore
|
||||||
|
.createMarker({ element: el })
|
||||||
.setLngLat([lng, lat])
|
.setLngLat([lng, lat])
|
||||||
.setPopup(popup)
|
.setPopup(popup)
|
||||||
.addTo(map);
|
.addTo(mapCore);
|
||||||
|
|
||||||
// Show popup on hover
|
el.addEventListener("mouseenter", () => marker.togglePopup());
|
||||||
el.addEventListener("mouseenter", () => {
|
el.addEventListener("mouseleave", () => marker.togglePopup());
|
||||||
marker.togglePopup();
|
|
||||||
});
|
|
||||||
el.addEventListener("mouseleave", () => {
|
|
||||||
marker.togglePopup();
|
|
||||||
});
|
|
||||||
|
|
||||||
markers.push(marker);
|
markers.push(marker);
|
||||||
return marker;
|
return marker;
|
||||||
|
|
@ -167,23 +116,18 @@
|
||||||
el.style.backgroundSize = "100%";
|
el.style.backgroundSize = "100%";
|
||||||
el.title = title;
|
el.title = title;
|
||||||
|
|
||||||
// Create popup with coordinates
|
const popup = mapCore
|
||||||
const popup = new maplibregl.Popup({ offset: 25, closeButton: false }).setHTML(
|
.createPopup({ offset: 25, closeButton: false })
|
||||||
`<b>${title}</b><br>Lat: ${lat.toFixed(6)}<br>Lng: ${lng.toFixed(6)}`,
|
.setHTML(`<b>${title}</b><br>Lat: ${lat.toFixed(6)}<br>Lng: ${lng.toFixed(6)}`);
|
||||||
);
|
|
||||||
|
|
||||||
const marker = new maplibregl.Marker({ element: el })
|
const marker = mapCore
|
||||||
|
.createMarker({ element: el })
|
||||||
.setLngLat([lng, lat])
|
.setLngLat([lng, lat])
|
||||||
.setPopup(popup)
|
.setPopup(popup)
|
||||||
.addTo(map);
|
.addTo(mapCore);
|
||||||
|
|
||||||
// Show popup on hover
|
el.addEventListener("mouseenter", () => marker.togglePopup());
|
||||||
el.addEventListener("mouseenter", () => {
|
el.addEventListener("mouseleave", () => marker.togglePopup());
|
||||||
marker.togglePopup();
|
|
||||||
});
|
|
||||||
el.addEventListener("mouseleave", () => {
|
|
||||||
marker.togglePopup();
|
|
||||||
});
|
|
||||||
|
|
||||||
markers.push(marker);
|
markers.push(marker);
|
||||||
return marker;
|
return marker;
|
||||||
|
|
@ -196,83 +140,49 @@
|
||||||
|
|
||||||
const range = distHaversine(launch.latlng, landing.latlng, 1);
|
const range = distHaversine(launch.latlng, landing.latlng, 1);
|
||||||
const f_hours = Math.floor(flight_time / 3600);
|
const f_hours = Math.floor(flight_time / 3600);
|
||||||
const f_minutes = Math.floor((flight_time % 3600) / 60)
|
const f_minutes = Math.floor((flight_time % 3600) / 60).toString().padStart(2, "0");
|
||||||
.toString()
|
|
||||||
.padStart(2, "0");
|
|
||||||
const flighttime = `${f_hours}hr${f_minutes}`;
|
const flighttime = `${f_hours}hr${f_minutes}`;
|
||||||
|
|
||||||
// Helper to extract lat/lng from either format
|
|
||||||
const getLat = (latlng: any) => (Array.isArray(latlng) ? latlng[0] : latlng.lat);
|
const getLat = (latlng: any) => (Array.isArray(latlng) ? latlng[0] : latlng.lat);
|
||||||
const getLng = (latlng: any) => (Array.isArray(latlng) ? latlng[1] : latlng.lng);
|
const getLng = (latlng: any) => (Array.isArray(latlng) ? latlng[1] : latlng.lng);
|
||||||
|
|
||||||
// Create markers (MapLibre uses [lng, lat] order)
|
createMarker(getLng(launch.latlng), getLat(launch.latlng), "target-blue.png", "Launch");
|
||||||
createMarker(getLng(launch.latlng), getLat(launch.latlng), "#0000ff", "target-blue.png", "Launch");
|
createMarker(getLng(landing.latlng), getLat(landing.latlng), "target-red.png", "Landing");
|
||||||
createMarker(getLng(landing.latlng), getLat(landing.latlng), "#ff0000", "target-red.png", "Landing");
|
|
||||||
createBurstMarker(getLng(burst.latlng), getLat(burst.latlng), "Burst");
|
createBurstMarker(getLng(burst.latlng), getLat(burst.latlng), "Burst");
|
||||||
|
|
||||||
// Add flight path as a line (convert [lat, lng] to [lng, lat] for MapLibre)
|
const coordinates: [number, number][] = flight_path.map((coord) =>
|
||||||
const coordinates = flight_path.map((coord) => {
|
Array.isArray(coord) ? [coord[1], coord[0]] : [coord.lng, coord.lat],
|
||||||
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", {
|
mapCore.addSource("flight-path", {
|
||||||
type: "geojson",
|
type: "geojson",
|
||||||
data: {
|
data: { type: "Feature", properties: {}, geometry: { type: "LineString", coordinates } },
|
||||||
type: "Feature",
|
|
||||||
properties: {},
|
|
||||||
geometry: {
|
|
||||||
type: "LineString",
|
|
||||||
coordinates: coordinates,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
map.addLayer({
|
mapCore.addLayer({
|
||||||
id: "flight-path",
|
id: "flight-path",
|
||||||
type: "line",
|
type: "line",
|
||||||
source: "flight-path",
|
source: "flight-path",
|
||||||
layout: {
|
layout: { "line-join": "round", "line-cap": "round" },
|
||||||
"line-join": "round",
|
paint: { "line-color": "#000000", "line-width": 3 },
|
||||||
"line-cap": "round",
|
|
||||||
},
|
|
||||||
paint: {
|
|
||||||
"line-color": "#000000",
|
|
||||||
"line-width": 3,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fit bounds to show entire path
|
mapCore.fitBounds(coordinates, 50);
|
||||||
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) => {
|
const plotTelemetry = (telemetry: Telemetry) => {
|
||||||
clearMapLayers();
|
clearMapLayers();
|
||||||
|
|
||||||
// Helper to extract lat/lng from either format
|
|
||||||
const getLat = (latlng: any) => (Array.isArray(latlng) ? latlng[0] : latlng.lat);
|
const getLat = (latlng: any) => (Array.isArray(latlng) ? latlng[0] : latlng.lat);
|
||||||
const getLng = (latlng: any) => (Array.isArray(latlng) ? latlng[1] : latlng.lng);
|
const getLng = (latlng: any) => (Array.isArray(latlng) ? latlng[1] : latlng.lng);
|
||||||
|
|
||||||
// Launch marker (MapLibre uses [lng, lat] order)
|
|
||||||
createMarker(
|
createMarker(
|
||||||
getLng(telemetry.launch.latlng),
|
getLng(telemetry.launch.latlng),
|
||||||
getLat(telemetry.launch.latlng),
|
getLat(telemetry.launch.latlng),
|
||||||
"#0000ff",
|
|
||||||
"target-blue.png",
|
"target-blue.png",
|
||||||
"Launch",
|
"Launch",
|
||||||
);
|
);
|
||||||
|
|
||||||
// Telemetry point markers with popups
|
|
||||||
telemetry.datapoints.forEach((point) => {
|
telemetry.datapoints.forEach((point) => {
|
||||||
const el = document.createElement("div");
|
const el = document.createElement("div");
|
||||||
el.className = "custom-marker";
|
el.className = "custom-marker";
|
||||||
|
|
@ -281,86 +191,58 @@
|
||||||
el.style.height = "10px";
|
el.style.height = "10px";
|
||||||
el.style.backgroundSize = "100%";
|
el.style.backgroundSize = "100%";
|
||||||
|
|
||||||
const popup = new maplibregl.Popup({ offset: 25 }).setHTML(
|
const popup = mapCore
|
||||||
`<b>Telemetry Point</b><br>Lat: ${point.latitude.toFixed(6)}<br>Lon: ${point.longitude.toFixed(6)}`,
|
.createPopup({ 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 })
|
const marker = mapCore
|
||||||
|
.createMarker({ element: el })
|
||||||
.setLngLat([point.longitude, point.latitude])
|
.setLngLat([point.longitude, point.latitude])
|
||||||
.setPopup(popup)
|
.setPopup(popup)
|
||||||
.addTo(map);
|
.addTo(mapCore);
|
||||||
|
|
||||||
markers.push(marker);
|
markers.push(marker);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add flight path as a line (convert [lat, lng] to [lng, lat] for MapLibre)
|
const coordinates: [number, number][] = telemetry.flight_path.map((coord) =>
|
||||||
const coordinates = telemetry.flight_path.map((coord) => {
|
Array.isArray(coord) ? [coord[1], coord[0]] : [coord.lng, coord.lat],
|
||||||
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("telemetry-path", {
|
mapCore.addSource("telemetry-path", {
|
||||||
type: "geojson",
|
type: "geojson",
|
||||||
data: {
|
data: { type: "Feature", properties: {}, geometry: { type: "LineString", coordinates } },
|
||||||
type: "Feature",
|
|
||||||
properties: {},
|
|
||||||
geometry: {
|
|
||||||
type: "LineString",
|
|
||||||
coordinates: coordinates,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
map.addLayer({
|
mapCore.addLayer({
|
||||||
id: "telemetry-path",
|
id: "telemetry-path",
|
||||||
type: "line",
|
type: "line",
|
||||||
source: "telemetry-path",
|
source: "telemetry-path",
|
||||||
layout: {
|
layout: { "line-join": "round", "line-cap": "round" },
|
||||||
"line-join": "round",
|
paint: { "line-color": "#000000", "line-width": 3 },
|
||||||
"line-cap": "round",
|
|
||||||
},
|
|
||||||
paint: {
|
|
||||||
"line-color": "#000000",
|
|
||||||
"line-width": 3,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fit bounds to show entire path
|
mapCore.fitBounds(coordinates, 50);
|
||||||
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) => {
|
export const panTo = (lat: number, lng: number) => {
|
||||||
if (map) {
|
if (mapCore) mapCore.setCenter([lng, lat]);
|
||||||
map.setCenter([lng, lat]);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const zoomTo = (lat: number, lng: number, zoomLevel: number) => {
|
export const zoomTo = (lat: number, lng: number, zoomLevel: number) => {
|
||||||
if (map) {
|
if (mapCore) {
|
||||||
map.setCenter([lng, lat]);
|
mapCore.setCenter([lng, lat]);
|
||||||
map.setZoom(zoomLevel);
|
mapCore.setZoom(zoomLevel);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMap = () => {
|
export const getMap = () => mapCore;
|
||||||
return map;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateAnimatedMarker = (lat: number, lng: number) => {
|
export const updateAnimatedMarker = (lat: number, lng: number) => {
|
||||||
if (!map) return;
|
if (!mapCore) return;
|
||||||
|
|
||||||
if (!animatedMarker) {
|
if (!animatedMarker) {
|
||||||
// Create animated marker
|
|
||||||
const el = document.createElement("div");
|
const el = document.createElement("div");
|
||||||
el.className = "animated-marker";
|
el.className = "animated-marker";
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
|
|
@ -370,16 +252,15 @@
|
||||||
</svg>
|
</svg>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
animatedMarker = new maplibregl.Marker({ element: el, anchor: "center" })
|
animatedMarker = mapCore
|
||||||
|
.createMarker({ element: el, anchor: "center" })
|
||||||
.setLngLat([lng, lat])
|
.setLngLat([lng, lat])
|
||||||
.addTo(map);
|
.addTo(mapCore);
|
||||||
} else {
|
} else {
|
||||||
// Update position
|
|
||||||
animatedMarker.setLngLat([lng, lat]);
|
animatedMarker.setLngLat([lng, lat]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pan to marker
|
mapCore.panTo([lng, lat], { duration: 100 });
|
||||||
map.panTo([lng, lat], { duration: 100 });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const removeAnimatedMarker = () => {
|
export const removeAnimatedMarker = () => {
|
||||||
|
|
@ -400,8 +281,8 @@
|
||||||
</p>
|
</p>
|
||||||
</div> -->
|
</div> -->
|
||||||
<slot />
|
<slot />
|
||||||
{#if map && windData}
|
{#if mapCore && windData}
|
||||||
<WindVisualization {map} {windData} />
|
<WindVisualization map={mapCore.getInstance()} {windData} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
196
src/lib/mapcore.ts
Normal file
196
src/lib/mapcore.ts
Normal file
|
|
@ -0,0 +1,196 @@
|
||||||
|
import maplibregl from 'maplibre-gl';
|
||||||
|
import 'maplibre-gl/dist/maplibre-gl.css';
|
||||||
|
|
||||||
|
// ─── Generic interfaces ────────────────────────────────────────────────────────
|
||||||
|
// Implement these with any map library (Leaflet, OpenLayers, etc.)
|
||||||
|
|
||||||
|
export interface IMapPopup {
|
||||||
|
setHTML(html: string): this;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMapMarker {
|
||||||
|
setLngLat(lngLat: [number, number]): IMapMarker;
|
||||||
|
setPopup(popup: IMapPopup): IMapMarker;
|
||||||
|
addTo(core: IMapCore): IMapMarker;
|
||||||
|
remove(): void;
|
||||||
|
togglePopup(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMapCore {
|
||||||
|
init(container: HTMLElement, options: { center: [number, number]; zoom: number }): void;
|
||||||
|
addNavigationControl(position: string): void;
|
||||||
|
addScaleControl(options: { maxWidth: number; unit: string }, position: string): void;
|
||||||
|
on(event: string, handler: (e: { lngLat: { lat: number; lng: number } }) => void): void;
|
||||||
|
hasLayer(id: string): boolean;
|
||||||
|
hasSource(id: string): boolean;
|
||||||
|
addSource(id: string, source: object): void;
|
||||||
|
addLayer(layer: object): void;
|
||||||
|
removeLayer(id: string): void;
|
||||||
|
removeSource(id: string): void;
|
||||||
|
fitBounds(coords: [number, number][], padding: number): void;
|
||||||
|
setCenter(lngLat: [number, number]): void;
|
||||||
|
setZoom(zoom: number): void;
|
||||||
|
panTo(lngLat: [number, number], options?: { duration?: number }): void;
|
||||||
|
createMarker(options?: { element?: HTMLElement; anchor?: string }): IMapMarker;
|
||||||
|
createPopup(options?: { offset?: number; closeButton?: boolean }): IMapPopup;
|
||||||
|
/** Returns the underlying raw map instance (e.g. for library-specific plugins). */
|
||||||
|
getInstance(): unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── MapLibre GL implementation ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class MapLibrePopup implements IMapPopup {
|
||||||
|
private _popup: maplibregl.Popup;
|
||||||
|
|
||||||
|
constructor(options?: { offset?: number; closeButton?: boolean }) {
|
||||||
|
this._popup = new maplibregl.Popup(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
setHTML(html: string): this {
|
||||||
|
this._popup.setHTML(html);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @internal used by MapLibreMarker */
|
||||||
|
raw(): maplibregl.Popup {
|
||||||
|
return this._popup;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MapLibreMarker implements IMapMarker {
|
||||||
|
private _marker: maplibregl.Marker;
|
||||||
|
|
||||||
|
constructor(options?: { element?: HTMLElement; anchor?: string }) {
|
||||||
|
this._marker = new maplibregl.Marker(options as maplibregl.MarkerOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
setLngLat(lngLat: [number, number]): this {
|
||||||
|
this._marker.setLngLat(lngLat);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPopup(popup: IMapPopup): this {
|
||||||
|
this._marker.setPopup((popup as MapLibrePopup).raw());
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
addTo(core: IMapCore): this {
|
||||||
|
this._marker.addTo(core.getInstance() as maplibregl.Map);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(): void {
|
||||||
|
this._marker.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
togglePopup(): void {
|
||||||
|
this._marker.togglePopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MapLibreCore implements IMapCore {
|
||||||
|
private _map!: maplibregl.Map;
|
||||||
|
|
||||||
|
init(container: HTMLElement, options: { center: [number, number]; zoom: number }): void {
|
||||||
|
this._map = new maplibregl.Map({
|
||||||
|
container,
|
||||||
|
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: options.center,
|
||||||
|
zoom: options.zoom,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addNavigationControl(position: string): void {
|
||||||
|
this._map.addControl(
|
||||||
|
new maplibregl.NavigationControl(),
|
||||||
|
position as maplibregl.ControlPosition,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
addScaleControl(options: { maxWidth: number; unit: string }, position: string): void {
|
||||||
|
this._map.addControl(
|
||||||
|
new maplibregl.ScaleControl(options as maplibregl.ScaleControlOptions),
|
||||||
|
position as maplibregl.ControlPosition,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
on(event: string, handler: (e: { lngLat: { lat: number; lng: number } }) => void): void {
|
||||||
|
this._map.on(event as maplibregl.MapEventType, handler as never);
|
||||||
|
}
|
||||||
|
|
||||||
|
hasLayer(id: string): boolean {
|
||||||
|
return !!this._map.getLayer(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
hasSource(id: string): boolean {
|
||||||
|
return !!this._map.getSource(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
addSource(id: string, source: object): void {
|
||||||
|
this._map.addSource(id, source as maplibregl.SourceSpecification);
|
||||||
|
}
|
||||||
|
|
||||||
|
addLayer(layer: object): void {
|
||||||
|
this._map.addLayer(layer as maplibregl.LayerSpecification);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeLayer(id: string): void {
|
||||||
|
this._map.removeLayer(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeSource(id: string): void {
|
||||||
|
this._map.removeSource(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
fitBounds(coords: [number, number][], padding: number): void {
|
||||||
|
const bounds = coords.reduce(
|
||||||
|
(b, coord) => b.extend(coord),
|
||||||
|
new maplibregl.LngLatBounds(coords[0], coords[0]),
|
||||||
|
);
|
||||||
|
this._map.fitBounds(bounds, { padding });
|
||||||
|
}
|
||||||
|
|
||||||
|
setCenter(lngLat: [number, number]): void {
|
||||||
|
this._map.setCenter(lngLat);
|
||||||
|
}
|
||||||
|
|
||||||
|
setZoom(zoom: number): void {
|
||||||
|
this._map.setZoom(zoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
panTo(lngLat: [number, number], options?: { duration?: number }): void {
|
||||||
|
this._map.panTo(lngLat, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
createMarker(options?: { element?: HTMLElement; anchor?: string }): IMapMarker {
|
||||||
|
return new MapLibreMarker(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
createPopup(options?: { offset?: number; closeButton?: boolean }): IMapPopup {
|
||||||
|
return new MapLibrePopup(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
getInstance(): maplibregl.Map {
|
||||||
|
return this._map;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue