125 lines
3.8 KiB
Svelte
125 lines
3.8 KiB
Svelte
<script lang="ts">
|
|
import { onMount, onDestroy } from 'svelte';
|
|
import maplibregl from 'maplibre-gl';
|
|
import 'maplibre-gl/dist/maplibre-gl.css';
|
|
import type { Timeseries } from '../lib/types';
|
|
|
|
export let trackUrl: string;
|
|
export let timeseries: Timeseries | null = null;
|
|
export let bbox: [number, number, number, number] | null = null;
|
|
export let accentColor: string = '#00c8ff';
|
|
export let hoveredIdx: number | null = null;
|
|
|
|
let mapEl: HTMLDivElement;
|
|
let map: any;
|
|
const MarkerClass = maplibregl.Marker;
|
|
let hoverMarker: any;
|
|
let markersAdded = false;
|
|
|
|
const TILE_STYLE = 'https://tiles.openfreemap.org/styles/positron';
|
|
|
|
onMount(() => {
|
|
map = new maplibregl.Map({
|
|
container: mapEl,
|
|
style: TILE_STYLE,
|
|
center: [0, 0],
|
|
zoom: 1,
|
|
attributionControl: false,
|
|
});
|
|
|
|
map.addControl(new maplibregl.AttributionControl({ compact: true }), 'bottom-right');
|
|
|
|
// Hover dot marker — must set lngLat before addTo in MapLibre v5
|
|
const el = document.createElement('div');
|
|
el.style.cssText = `
|
|
width:12px;height:12px;border-radius:50%;
|
|
background:white;border:2px solid ${accentColor};
|
|
box-shadow:0 0 6px ${accentColor};display:none;pointer-events:none;
|
|
`;
|
|
hoverMarker = new maplibregl.Marker({ element: el, anchor: 'center' })
|
|
.setLngLat([0, 0])
|
|
.addTo(map);
|
|
|
|
map.on('load', () => {
|
|
map.addSource('track', {
|
|
type: 'geojson',
|
|
data: trackUrl,
|
|
lineMetrics: true,
|
|
});
|
|
|
|
map.addLayer({
|
|
id: 'track-shadow',
|
|
type: 'line',
|
|
source: 'track',
|
|
paint: { 'line-color': 'rgba(0,0,0,0.3)', 'line-width': 5, 'line-blur': 2 },
|
|
});
|
|
|
|
map.addLayer({
|
|
id: 'track-line',
|
|
type: 'line',
|
|
source: 'track',
|
|
layout: { 'line-cap': 'round', 'line-join': 'round' },
|
|
paint: {
|
|
'line-width': 3,
|
|
'line-gradient': [
|
|
'interpolate', ['linear'], ['line-progress'],
|
|
0, accentColor,
|
|
0.5, '#ff6b35',
|
|
1, accentColor,
|
|
],
|
|
},
|
|
});
|
|
});
|
|
});
|
|
|
|
// Fit to bbox when detail JSON loads (bbox is null at map init)
|
|
$: if (map && bbox) {
|
|
const fit = () => map.fitBounds(
|
|
[[bbox![0], bbox![1]], [bbox![2], bbox![3]]],
|
|
{ padding: 40, animate: true },
|
|
);
|
|
map.loaded() ? fit() : map.once('load', fit);
|
|
}
|
|
|
|
// Add start/end markers when timeseries arrives
|
|
$: if (map && MarkerClass && timeseries && !markersAdded) {
|
|
markersAdded = true;
|
|
const add = () => {
|
|
const lats = (timeseries!.lat ?? []).filter(v => v != null) as number[];
|
|
const lons = (timeseries!.lon ?? []).filter(v => v != null) as number[];
|
|
if (!lats.length) return;
|
|
new MarkerClass({ element: makeDot('#4ade80'), anchor: 'center' })
|
|
.setLngLat([lons[0], lats[0]]).addTo(map);
|
|
new MarkerClass({ element: makeDot('#f87171'), anchor: 'center' })
|
|
.setLngLat([lons[lons.length - 1], lats[lats.length - 1]]).addTo(map);
|
|
};
|
|
map.loaded() ? add() : map.once('load', add);
|
|
}
|
|
|
|
// Hover dot linked to chart crosshair
|
|
$: if (hoverMarker && timeseries && hoveredIdx != null) {
|
|
const lat = timeseries.lat?.[hoveredIdx];
|
|
const lon = timeseries.lon?.[hoveredIdx];
|
|
if (lat != null && lon != null) {
|
|
hoverMarker.getElement().style.display = 'block';
|
|
hoverMarker.setLngLat([lon, lat]);
|
|
}
|
|
} else if (hoverMarker) {
|
|
hoverMarker.getElement().style.display = 'none';
|
|
}
|
|
|
|
function makeDot(color: string): HTMLDivElement {
|
|
const el = document.createElement('div');
|
|
el.style.cssText = `
|
|
width:10px;height:10px;border-radius:50%;
|
|
background:${color};border:2px solid white;
|
|
box-shadow:0 0 4px rgba(0,0,0,0.5);
|
|
`;
|
|
return el;
|
|
}
|
|
|
|
onDestroy(() => map?.remove());
|
|
</script>
|
|
|
|
<div bind:this={mapEl} class="w-full h-full" />
|