Files
Crussell/frontend/src/lib/components/ui/map/MapRoute.svelte
T
popertots ad0ad253ad
Backend CI / Lint & vulns (push) Failing after 1m59s
Backend CI / Tests (push) Successful in 2m1s
Backend CI / Race detector (push) Failing after 4m0s
style: apply prettier formatting to frontend
2026-06-25 13:26:26 +01:00

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>