nidus-sync/ts/components/MapLocatorDisplay.vue

178 lines
3.7 KiB
Vue

<style scoped>
@import url("https://unpkg.com/maplibre-gl@5.0.1/dist/maplibre-gl.css");
.map {
width: 100%;
height: 100%;
transition: filter 0.2s ease;
}
/* Ensure map fills container on all devices */
:deep(.maplibregl-map) {
width: 100%;
height: 100%;
}
:deep(.maplibregl-canvas) {
width: 100%;
height: 100%;
}
</style>
<template>
<!-- Map container -->
<div ref="mapContainer" class="map-container"></div>
</template>
<script setup lang="ts">
import maplibregl from "maplibre-gl";
import {
onMounted,
onUnmounted,
ref,
type Ref,
shallowRef,
useTemplateRef,
watch,
} from "vue";
import { boundsMarkers, boundsDefault } from "@/map-utils";
import type { Marker } from "@/types";
import type { Location } from "@/type/api";
import type { Camera, MoveEndEventInternal } from "@/type/map";
// Emits interface
interface Emits {}
// Props
interface Props {
markers?: Marker[];
}
const props = withDefaults(defineProps<Props>(), {
markers: () => [],
});
// Refs
const map: Ref<maplibregl.Map | null> = shallowRef(null);
const mapContainer = ref<HTMLDivElement | null>(null);
const mapMarkers: Ref<Map<string, maplibregl.Marker>> = shallowRef<
Map<string, maplibregl.Marker>
>(new Map());
// Initialize map
function initializeMap() {
if (!mapContainer.value) return;
let bounds = boundsDefault();
if (props.markers.length > 0) {
bounds = boundsMarkers(props.markers);
}
const _map = new maplibregl.Map({
bounds: bounds,
container: mapContainer.value,
style: "https://tiles.stadiamaps.com/styles/alidade_smooth.json",
// Disable interactions by default
doubleClickZoom: false,
dragPan: false,
scrollZoom: false,
touchZoomRotate: false,
});
_map.addControl(new maplibregl.NavigationControl(), "top-left");
map.value = _map;
}
// Update markers on the map
const updateMarkers = () => {
if (!map.value) return;
// Remove markers that no longer exist
const currentMarkerIds = new Set(props.markers.map((m) => m.id));
for (const [id, marker] of mapMarkers.value) {
if (!currentMarkerIds.has(id)) {
marker.remove();
mapMarkers.value.delete(id);
}
}
// Add or update markers
props.markers.forEach((markerData) => {
let marker = mapMarkers.value.get(markerData.id);
if (marker) {
// Update existing marker
marker.setLngLat([
markerData.location.longitude,
markerData.location.latitude,
]);
marker.setDraggable(markerData.draggable ?? false);
} else {
marker = new maplibregl.Marker({
draggable: markerData.draggable ?? false,
})
.setLngLat([
markerData.location.longitude,
markerData.location.latitude,
])
.addTo(map.value!);
mapMarkers.value.set(markerData.id, marker);
}
});
};
// Frame all markers in view
const frameMarkers = () => {
if (!map.value || props.markers.length === 0) return;
if (props.markers.length === 1) {
// Single marker: pan to it
map.value.panTo(
{
lat: props.markers[0].location.latitude,
lng: props.markers[0].location.longitude,
},
{ duration: 1000 },
{ isInternalUpdate: true },
);
} else {
// Multiple markers: fit bounds
const bounds = new maplibregl.LngLatBounds();
props.markers.forEach((marker) => {
bounds.extend([marker.location.longitude, marker.location.latitude]);
});
map.value.fitBounds(
bounds,
{ padding: 10, duration: 1000 },
{ isInternalUpdate: true },
);
}
};
// Watch for markers changes
watch(
() => props.markers,
() => {
updateMarkers();
},
{ deep: true },
);
// Lifecycle hooks
onMounted(() => {
setTimeout(() => {
initializeMap();
updateMarkers();
}, 0);
});
onUnmounted(() => {
// Remove all markers
mapMarkers.value.forEach((marker) => marker.remove());
mapMarkers.value.clear();
// Remove map
if (map.value) {
map.value.remove();
map.value = null;
}
});
</script>