nidus-sync/ts/components/MapLocator.vue

219 lines
4.6 KiB
Vue
Raw Normal View History

2026-04-03 19:45:12 +00:00
<style scoped>
@import url("https://unpkg.com/maplibre-gl@5.0.1/dist/maplibre-gl.css");
.map-container {
height: 100%;
width: 100%;
}
.map-container :deep(img) {
max-width: none;
min-width: 0px;
height: auto;
}
</style>
<template>
<div ref="mapContainer" class="map-container"></div>
</template>
<script setup lang="ts">
import maplibregl from "maplibre-gl";
import type { LngLatBoundsLike, Map as MapLibreMap } from "maplibre-gl";
import { onMounted, onUnmounted, ref, type Ref, shallowRef, 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";
2026-04-03 19:45:12 +00:00
// Emits interface
interface Emits {
(e: "update:modelValue", value: Camera): void;
2026-04-03 19:45:12 +00:00
(e: "click", location: Location): void;
(e: "load"): void;
(e: "markerDragEnd", location: Location): void;
}
// Props
interface Props {
modelValue: Camera | null;
2026-04-03 19:45:12 +00:00
apiKey?: string;
markers?: Marker[];
}
const props = withDefaults(defineProps<Props>(), {
markers: () => [],
});
const emit = defineEmits<Emits>();
// Refs
const mapContainer = ref<HTMLDivElement | null>(null);
const map: Ref<MapLibreMap | null> = shallowRef(null);
const markerInstances: Ref<maplibregl.Marker[]> = shallowRef<
maplibregl.Marker[]
>([]);
// Initialize map
const initializeMap = () => {
if (!mapContainer.value) return;
let bounds = boundsDefault();
if (props.markers.length > 0) {
bounds = boundsMarkers(props.markers);
}
const _map = new maplibregl.Map({
2026-04-03 19:45:12 +00:00
bounds: bounds,
container: mapContainer.value,
style: "https://tiles.stadiamaps.com/styles/alidade_smooth.json",
});
map.value = _map;
_map.on("click", (e: maplibregl.MapLayerMouseEvent) => {
2026-04-03 19:45:12 +00:00
e.preventDefault();
console.log("internal click", e);
emit("click", {
latitude: e.lngLat.lat,
longitude: e.lngLat.lng,
2026-04-03 19:45:12 +00:00
});
});
_map.on("load", () => {
2026-04-03 19:45:12 +00:00
emit("load");
updateModel(_map);
2026-04-03 19:45:12 +00:00
});
_map.on("zoomend", (evt: MoveEndEventInternal) => {
console.log("zoomend", evt);
if (_map && !evt.isInternalUpdate) {
updateModel(_map);
}
});
_map.on("moveend", (evt: MoveEndEventInternal) => {
console.log("moveend", evt);
if (_map && !evt.isInternalUpdate) {
updateModel(_map);
}
2026-04-03 19:45:12 +00:00
});
};
function updateModel(_map: maplibregl.Map) {
const center = _map.getCenter();
const newCamera: Camera = {
location: {
latitude: center.lat,
longitude: center.lng,
},
zoom: _map.getZoom(),
};
emit("update:modelValue", newCamera);
}
2026-04-03 19:45:12 +00:00
// Update markers on the map
const updateMarkers = () => {
if (!map.value) return;
// Remove existing markers
markerInstances.value.forEach((marker) => marker.remove());
markerInstances.value = [];
// Add new markers
props.markers.forEach((markerDef) => {
const marker = new maplibregl.Marker({
color: markerDef.color || "#FF0000",
draggable: markerDef.draggable ?? true,
})
.setLngLat({
lat: markerDef.location.latitude,
lng: markerDef.location.longitude,
})
2026-04-03 19:45:12 +00:00
.addTo(map.value!);
if (markerDef.draggable ?? true) {
marker.on("dragend", () => {
const lngLat = marker.getLngLat();
emit("markerDragEnd", {
latitude: lngLat.lat,
longitude: lngLat.lng,
2026-04-03 19:45:12 +00:00
});
});
}
markerInstances.value.push(marker);
});
// Frame markers if there are any
if (props.markers.length > 0) {
frameMarkers();
}
};
// 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, zoom: props.modelValue?.zoom },
{ isInternalUpdate: true },
);
2026-04-03 19:45:12 +00:00
} else {
// Multiple markers: fit bounds
const bounds = new maplibregl.LngLatBounds();
props.markers.forEach((marker) => {
bounds.extend([marker.location.longitude, marker.location.latitude]);
2026-04-03 19:45:12 +00:00
});
map.value.fitBounds(
bounds,
{ padding: 10, duration: 1000 },
{ isInternalUpdate: true },
);
2026-04-03 19:45:12 +00:00
}
};
// Watch for modelValue changes to pan to location
watch(
() => props.modelValue,
(newLocation) => {
if (map.value && newLocation) {
map.value.panTo(
{
lat: newLocation.location.latitude,
lng: newLocation.location.longitude,
},
{ duration: 1000 },
{ isInternalUpdate: true },
);
2026-04-03 19:45:12 +00:00
}
},
{ deep: true },
);
// Watch for markers changes
watch(
() => props.markers,
() => {
updateMarkers();
},
{ deep: true },
);
// Lifecycle hooks
onMounted(() => {
setTimeout(() => {
initializeMap();
updateMarkers();
}, 0);
});
onUnmounted(() => {
if (map.value) {
map.value.remove();
}
});
</script>