186 lines
4.7 KiB
Svelte
186 lines
4.7 KiB
Svelte
<script lang="ts">
|
|
import { getContext } from 'svelte';
|
|
import MapLibreGL from 'maplibre-gl';
|
|
import { generateUUID } from '$lib/utils/uuid';
|
|
|
|
interface Props {
|
|
/** Optional unique identifier for the route layer */
|
|
id?: string;
|
|
/** Array of [longitude, latitude] coordinate pairs defining the route */
|
|
coordinates: [number, number][];
|
|
/** Line color as CSS color value (default: "#4285F4") */
|
|
color?: string;
|
|
/** Line width in pixels (default: 3) */
|
|
width?: number;
|
|
/** Line opacity from 0 to 1 (default: 0.8) */
|
|
opacity?: number;
|
|
/** Dash pattern [dash length, gap length] for dashed lines */
|
|
dashArray?: [number, number];
|
|
/** Callback when the route line is clicked */
|
|
onclick?: () => void;
|
|
/** Callback when mouse enters the route line */
|
|
onmouseenter?: () => void;
|
|
/** Callback when mouse leaves the route line */
|
|
onmouseleave?: () => void;
|
|
/** Whether the route is interactive - shows pointer cursor on hover (default: true) */
|
|
interactive?: boolean;
|
|
}
|
|
|
|
let {
|
|
coordinates,
|
|
color = '#4285F4',
|
|
width = 3,
|
|
opacity = 0.8,
|
|
dashArray,
|
|
onclick,
|
|
onmouseenter,
|
|
onmouseleave,
|
|
interactive = true,
|
|
id = generateUUID()
|
|
}: Props = $props();
|
|
|
|
const mapCtx = getContext<{
|
|
getMap: () => MapLibreGL.Map | null;
|
|
isStyleReady: () => boolean;
|
|
}>('map');
|
|
|
|
const sourceId = $derived(`route-source-${id}`);
|
|
const layerId = $derived(`route-layer-${id}`);
|
|
|
|
// Add route when map is ready
|
|
$effect(() => {
|
|
const map = mapCtx.getMap();
|
|
const loaded = mapCtx.isStyleReady();
|
|
|
|
if (!loaded || !map || coordinates.length < 2) return;
|
|
|
|
// Remove existing layer and source if they exist
|
|
if (map.getLayer(layerId)) map.removeLayer(layerId);
|
|
if (map.getSource(sourceId)) map.removeSource(sourceId);
|
|
|
|
// Add source
|
|
map.addSource(sourceId, {
|
|
type: 'geojson',
|
|
data: {
|
|
type: 'Feature',
|
|
properties: {},
|
|
geometry: {
|
|
type: 'LineString',
|
|
coordinates
|
|
}
|
|
}
|
|
});
|
|
|
|
// Build paint options with transition definitions
|
|
// Use default values here - they'll be updated by the paint property effect
|
|
const paint: MapLibreGL.LineLayerSpecification['paint'] = {
|
|
'line-color': '#94a3b8', // Start with gray (unselected color)
|
|
'line-width': 5, // Start with unselected width
|
|
'line-opacity': 0.6, // Start with unselected opacity
|
|
'line-color-transition': { duration: 300, delay: 0 },
|
|
'line-width-transition': { duration: 300, delay: 0 },
|
|
'line-opacity-transition': { duration: 300, delay: 0 }
|
|
};
|
|
|
|
if (dashArray) {
|
|
paint['line-dasharray'] = dashArray;
|
|
}
|
|
|
|
// Add layer
|
|
map.addLayer({
|
|
id: layerId,
|
|
type: 'line',
|
|
source: sourceId,
|
|
layout: {
|
|
'line-join': 'round',
|
|
'line-cap': 'round'
|
|
},
|
|
paint
|
|
});
|
|
|
|
return () => {
|
|
try {
|
|
if (map.getLayer(layerId)) map.removeLayer(layerId);
|
|
if (map.getSource(sourceId)) map.removeSource(sourceId);
|
|
} catch {
|
|
// Ignore errors during cleanup
|
|
}
|
|
};
|
|
});
|
|
|
|
// Update route data when coordinates change
|
|
$effect(() => {
|
|
const map = mapCtx.getMap();
|
|
const loaded = mapCtx.isStyleReady();
|
|
|
|
if (!loaded || !map || coordinates.length < 2) return;
|
|
|
|
const source = map.getSource(sourceId) as MapLibreGL.GeoJSONSource | undefined;
|
|
if (source) {
|
|
source.setData({
|
|
type: 'Feature',
|
|
properties: {},
|
|
geometry: {
|
|
type: 'LineString',
|
|
coordinates
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// Update paint properties when they change
|
|
$effect(() => {
|
|
const map = mapCtx.getMap();
|
|
const loaded = mapCtx.isStyleReady();
|
|
|
|
if (!loaded || !map || !map.getLayer(layerId)) return;
|
|
|
|
map.setPaintProperty(layerId, 'line-color', color);
|
|
map.setPaintProperty(layerId, 'line-width', width);
|
|
map.setPaintProperty(layerId, 'line-opacity', opacity);
|
|
|
|
if (dashArray) {
|
|
map.setPaintProperty(layerId, 'line-dasharray', dashArray);
|
|
}
|
|
|
|
// Move selected routes to top (when opacity is 1, it's selected)
|
|
if (opacity === 1) {
|
|
try {
|
|
map.moveLayer(layerId);
|
|
} catch {
|
|
// Layer might not exist yet
|
|
}
|
|
}
|
|
});
|
|
|
|
// Handle click and hover events
|
|
$effect(() => {
|
|
const map = mapCtx.getMap();
|
|
const loaded = mapCtx.isStyleReady();
|
|
|
|
if (!loaded || !map || !interactive) return;
|
|
|
|
const handleClick = () => {
|
|
onclick?.();
|
|
};
|
|
const handleMouseEnter = () => {
|
|
map.getCanvas().style.cursor = 'pointer';
|
|
onmouseenter?.();
|
|
};
|
|
const handleMouseLeave = () => {
|
|
map.getCanvas().style.cursor = '';
|
|
onmouseleave?.();
|
|
};
|
|
|
|
map.on('click', layerId, handleClick);
|
|
map.on('mouseenter', layerId, handleMouseEnter);
|
|
map.on('mouseleave', layerId, handleMouseLeave);
|
|
|
|
return () => {
|
|
map.off('click', layerId, handleClick);
|
|
map.off('mouseenter', layerId, handleMouseEnter);
|
|
map.off('mouseleave', layerId, handleMouseLeave);
|
|
};
|
|
});
|
|
</script>
|