nidus-sync/ts/map/Map.vue
Eli Ribble b68d93ec91
Load communication reports asynchronously
This solves some problems created by making the publicreport part of the
communication API consistent. There are a lot of optimizations still on
the table with this one, but for now I need to get this out.
2026-04-28 14:49:02 +00:00

263 lines
5.7 KiB
Vue

<style scoped>
.map {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
height: 100%;
width: 100%;
}
</style>
<template>
<div ref="mapDiv" class="map" v-bind="$attrs"></div>
<slot />
</template>
<script setup lang="ts">
import "maplibre-gl/dist/maplibre-gl.css";
import maplibregl from "maplibre-gl";
import {
onBeforeUnmount,
onMounted,
provide,
ref,
type Ref,
shallowRef,
watch,
} from "vue";
import { boundsDefault } from "@/map/util";
import type { MapClickEvent, Marker } from "@/types";
import type { Location } from "@/type/api";
export type LngLatLike = maplibregl.LngLatLike;
export type LngLatBounds = maplibregl.LngLatBounds;
interface Emits {
(e: "marker-drag-end", location: Location): void;
}
interface Props {
bounds?: LngLatBounds;
center?: LngLatLike;
cursor?: string;
markers?: Marker[];
zoom?: number;
}
const emit = defineEmits<Emits>();
const props = withDefaults(defineProps<Props>(), {
bounds: boundsDefault,
cursor: "",
markers: () => [],
});
const mapDiv = ref<HTMLElement | null>(null);
const map: Ref<maplibregl.Map | null> = shallowRef(null);
const mapMarkers: Ref<Map<string, maplibregl.Marker>> = shallowRef<
Map<string, maplibregl.Marker>
>(new Map());
// Provide the map instance to children
provide("map", map);
// Registry for tracking child components
const ons = new Map();
const onces = new Map();
const sources = new Map();
const layers = new Map();
type OnCallbackFunc = () => void;
provide(
"registerOn",
(
eventname: keyof maplibregl.MapLayerEventType,
layerid: string,
callback: OnCallbackFunc,
) => {
console.log("register map.on", eventname, layerid);
ons.set(`${eventname}.${layerid}`, {
callback: callback,
eventname: eventname,
layerid: layerid,
});
if (map.value && map.value.loaded()) {
map.value.on(eventname, layerid, callback);
}
},
);
provide(
"registerOnce",
(
eventname: keyof maplibregl.MapLayerEventType,
layerid: string,
callback: OnCallbackFunc,
) => {
console.log("register map.once", eventname, layerid);
onces.set(`${eventname}.${layerid}`, {
callback: callback,
eventname: eventname,
layerid: layerid,
});
if (map.value && map.value.loaded()) {
map.value.once(eventname, layerid, callback);
}
},
);
provide("registerSource", (id: string, config: any) => {
console.log("register source", id, config);
sources.set(id, config);
if (map.value && map.value.loaded()) {
if (!map.value.getSource(id)) {
map.value.addSource(id, config);
}
}
});
provide("unregisterSource", (id: string) => {
console.log("unregister source", id);
/*
sources.delete(id);
if (map.value && map.value?.getSource(id)) {
map.value.removeSource(id);
}
*/
});
provide("registerLayer", (id: string, config: any) => {
console.log("register layer", id, config);
layers.set(id, config);
if (map.value && map.value.loaded()) {
if (!map.value.getLayer(id)) {
map.value.addLayer(config);
}
}
});
provide("unregisterLayer", (id: string) => {
console.log("unregister layer", id);
/*
layers.delete(id);
if (map.value?.getLayer(id)) {
map.value.removeLayer(id);
}
*/
});
function initializeMap() {
if (!mapDiv.value) return;
console.log("initializing map...", props.bounds, props.center, props.zoom);
const _map = new maplibregl.Map({
bounds: props.bounds,
container: mapDiv.value,
center: props.center,
style: "https://tiles.stadiamaps.com/styles/alidade_smooth.json",
zoom: props.zoom,
});
// When map loads, add all registered sources/layers
_map.on("load", () => {
console.log("map loaded.");
sources.forEach((config, id) => {
console.log("adding source", id, config);
if (!_map.getSource(id)) {
_map.addSource(id, config);
}
});
layers.forEach((config, id) => {
console.log("adding layer", id, config);
if (!_map.getLayer(id)) {
_map.addLayer(config);
}
});
ons.forEach((config, id) => {
console.log("adding map.on", config.eventname, config.layerid);
_map.on(config.eventname, config.layerid, config.callback);
});
});
onces.forEach((config, id) => {
console.log("adding map.on", config.eventname, config.layerid);
_map.once(config.eventname, config.layerid, config.callback);
});
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({
color: markerData.color,
draggable: markerData.draggable ?? false,
})
.setLngLat([
markerData.location.longitude,
markerData.location.latitude,
])
.addTo(map.value!);
// Handle marker drag end
if (markerData.draggable) {
marker.on("dragend", () => {
const lngLat = marker!.getLngLat();
const location: Location = {
latitude: lngLat.lat,
longitude: lngLat.lng,
};
emit("marker-drag-end", location);
});
}
mapMarkers.value.set(markerData.id, marker);
}
});
};
onMounted(() => {
initializeMap();
});
onBeforeUnmount(() => {
if (map.value) {
map.value.remove();
}
});
watch(
() => props.cursor,
(newCursor) => {
if (map.value && map.value.loaded()) {
map.value.getCanvas().style.cursor = newCursor;
}
},
);
watch(
() => props.markers,
() => {
updateMarkers();
},
{ deep: true },
);
</script>