Get map markers working on communication page

This commit is contained in:
Eli Ribble 2026-03-22 19:30:11 +00:00
parent 354c07f2bf
commit 8d5fb1ef0b
No known key found for this signature in database
3 changed files with 162 additions and 193 deletions

View file

@ -47,7 +47,8 @@
<div class="map-container">
<MapMultipoint
id="map"
ref="mapRef"
:bounds="mapBounds"
:markers="mapMarkers"
:organization-id="user.organization.id"
:tegola="user.urls.tegola"
:xmin="user.organization.service_area?.min.x ?? 0"
@ -412,6 +413,8 @@ interface Emits {
}
interface Props {
loading: boolean;
mapBounds?: Bounds;
mapMarkers: Marker[];
selectedCommunication: Communication | null;
user: User | null;
}

View file

@ -2,91 +2,110 @@
<div ref="mapContainer" class="map-multipoint"></div>
</template>
<script setup>
<script setup lang="ts">
import {
ref,
onMounted,
onUnmounted,
defineProps,
defineEmits,
defineExpose,
onMounted,
onUnmounted,
ref,
watch,
} from "vue";
import maplibregl from "maplibre-gl";
const props = defineProps({
xmin: {
type: Number,
default: 0,
},
ymin: {
type: Number,
default: 0,
},
xmax: {
type: Number,
default: 0,
},
ymax: {
type: Number,
default: 0,
},
organizationId: {
type: Number,
default: 0,
},
tegola: {
type: String,
default: "",
interface Point {
lat: Number;
lng: Number;
}
interface Bounds {
min: Point;
max: Point;
}
interface Emits {
(e: "load"): void;
}
interface Props {
bounds?: Bounds;
markers: Marker[];
"organization-id": int;
tegola: string;
}
interface Marker {
color: string;
draggable: boolean;
id: string;
lng: Number;
lat: Number;
}
const emit = defineEmits<Emits>();
const props = withDefaults(defineProps<Props>(), {
// default bounds cover a bunch of the continental US
bounds: {
max: { x: -70, y: 50 },
min: { x: -125, y: 25 },
},
});
const emit = defineEmits(["load"]);
const mapContainer = ref(null);
const _map = ref(null);
const _markers = ref([]);
const _preOns = ref([]);
const mapContainer = ref<HTMLElement | null>();
const map = ref<maplibregl.Map | null>(null);
const markerInstances = ref<Map<string, maplibregl.Marker>>(new Map());
const markers = ref<Map<string, maplibregl.Marker>>(new Map());
watch(
() => props.bounds,
(newBounds) => {
const bounds = new maplibregl.LngLatBounds(
new maplibregl.LngLat(newBounds.min.lng, newBounds.min.lat),
new maplibregl.LngLat(newBounds.max.lng, newBounds.max.lat),
);
if (map.value == null) {
return;
}
map.value.fitBounds(bounds, {
padding: 50,
});
},
{ deep: true },
);
watch(
() => props.markers,
(newMarkers) => {
updateMarkers(newMarkers);
},
{ deep: true },
);
const _bounds = () => {
let bounds = [
[props.xmin, props.ymin],
[props.xmax, props.ymax],
return [
[props.bounds.min.x, props.bounds.min.y],
[props.bounds.max.y, props.bounds.max.y],
];
if (
props.xmin === 0 ||
props.xmax === 0 ||
props.ymin === 0 ||
props.ymax === 0
) {
bounds = [
[-125, 25],
[-70, 50],
];
}
return bounds;
};
const _initializeMap = () => {
const _initializeMap = () => {};
// Lifecycle
onMounted(() => {
if (!mapContainer.value) return;
const bounds = _bounds();
_map.value = new maplibregl.Map({
map.value = new maplibregl.Map({
bounds: bounds,
container: mapContainer.value,
style: "https://tiles.stadiamaps.com/styles/osm_bright.json",
});
_map.value.on("load", () => {
// Wait for map to load, then add the markers
map.value.on("load", () => {
if (props.organizationId !== 0) {
_map.value.addSource("tegola", {
map.value.addSource("tegola", {
type: "vector",
tiles: [
`${props.tegola}maps/nidus/{z}/{x}/{y}?id=${props.organizationId}&organization_id=${props.organizationId}`,
],
});
_map.value.addLayer({
map.value.addLayer({
id: "service-area",
source: "tegola",
"source-layer": "service-area-bounds",
@ -96,117 +115,57 @@ const _initializeMap = () => {
},
});
}
emit("load", { map: _map.value });
updateMarkers(props.markers);
});
for (const on of _preOns.value) {
_map.value.on(...on);
}
};
// Map wrapper methods
const addLayer = (a) => {
return _map.value?.addLayer(a);
};
const addSource = (a, b) => {
return _map.value?.addSource(a, b);
};
const flyTo = (a, b) => {
return _map.value?.flyTo(a, b);
};
const getCanvas = (...args) => {
return _map.value?.getCanvas(...args);
};
const getContainer = (...args) => {
return _map.value?.getContainer(...args);
};
const jumpTo = (args) => {
return _map.value?.jumpTo(args);
};
const on = (...args) => {
if (_map.value != null) {
return _map.value.on(...args);
} else {
_preOns.value.push(args);
}
};
const once = (a, b) => {
return _map.value?.once(a, b);
};
const panTo = (a, b) => {
return _map.value?.panTo(a, b);
};
const queryRenderedFeatures = (a) => {
return _map.value?.queryRenderedFeatures(a);
};
const ClearMarkers = () => {
_markers.value.forEach((marker) => marker.remove());
};
const FitBounds = (bounds, options) => {
return _map.value?.fitBounds(bounds, options);
};
const ResetCamera = () => {
const bounds = _bounds();
FitBounds(bounds, {
linear: false,
});
};
const SetLayoutProperty = (layout, property, value) => {
return _map.value?.setLayoutProperty(layout, property, value);
};
const SetMarkers = (markers) => {
console.log("Setting map markers", markers);
_markers.value.forEach((marker) => marker.remove());
_markers.value = markers;
for (let m of markers) {
m.addTo(_map.value);
}
};
// Lifecycle
onMounted(() => {
setTimeout(() => _initializeMap(), 0);
});
onUnmounted(() => {
if (_map.value) {
_map.value.remove();
}
// Remove all markers
markerInstances.value.forEach((marker) => marker.remove());
markerInstances.value.clear();
// Free OpenGL context
map.value?.remove();
map.value = null;
});
// Expose methods to parent component
defineExpose({
addLayer,
addSource,
flyTo,
getCanvas,
getContainer,
jumpTo,
on,
once,
panTo,
queryRenderedFeatures,
ClearMarkers,
FitBounds,
ResetCamera,
SetLayoutProperty,
SetMarkers,
});
function updateMarkers(markers: Marker[]) {
const newMarkerIds = new Set(markers.map((m) => m.id));
if (map.value == null) {
console.log("refusing to add markers until map is set");
return;
}
// Remove markers that no longer exist
markerInstances.value.forEach((marker, id) => {
if (!newMarkerIds.has(id)) {
marker.remove();
markerInstances.value.delete(id);
}
});
// Add or update markers
markers.forEach((markerData) => {
if (markerInstances.value.has(markerData.id)) {
// Update existing marker position
const marker = markerInstances.value.get(markerData.id)!;
marker.setLngLat([markerData.lng, markerData.lat]);
console.log("updated", markerData);
} else {
// Create a new marker
const marker = new maplibregl.Marker({
color: markerData.color,
draggable: markerData.draggable,
})
.setLngLat([markerData.lng, markerData.lat])
.addTo(map.value!);
markerInstances.value.set(markerData.id, marker);
console.log("added", markerData);
}
});
}
</script>
<style scoped>

View file

@ -24,6 +24,8 @@
<template #center>
<CommunicationColumnDetail
:loading="loading"
:mapBounds="mapBounds || undefined"
:mapMarkers="mapMarkers"
:selectedCommunication="selectedCommunication"
:user="user"
@viewImage="openPhotoViewer"
@ -75,15 +77,16 @@ onMounted(() => {
});
// Refs
const showPhotoModal = ref(false);
const selectedId = ref<string | null>(null);
const currentPhotoIndex = ref(0);
const error = ref(null);
const loading = ref(true);
const mapBounds = ref<Bounds | null>(null);
const mapMarkers = ref<Marker[]>([]);
const selectedId = ref<string | null>(null);
const showPhotoModal = ref(false);
const toastMessage = ref("");
const toastShow = ref(false);
const toastTitle = ref("");
const loading = ref(true);
const error = ref(null);
const mapRef = ref(null);
const currentPhoto = computed(() => {
const comm = selectedCommunication.value;
@ -108,9 +111,11 @@ const selectedCommunication = computed<Communication | null>(() => {
});
const handleDeselect = (id: string) => {
selectedId.value = null;
updateMap();
};
const handleSelect = (id: string) => {
selectedId.value = id;
updateMap();
};
async function fetchCommunications() {
await communication.fetchAll();
@ -251,23 +256,23 @@ function showNotification(title, message) {
}
function updateMap() {
if (!mapRef.value) return;
const map = mapRef.value.$el || mapRef.value;
const loc = selectedCommunication.value.public_report.location;
const loc = selectedCommunication.value?.public_report?.location;
console.log("updating for loc", loc);
if (loc == null) {
map.ClearMarkers();
map.ResetCamera();
mapMarkers.value = [];
return;
}
let markers = [
new maplibregl.Marker({
mapMarkers.value = [
{
color: "#FF0000",
draggable: false,
}).setLngLat([loc.longitude, loc.latitude]),
id: String(Date.now()),
lng: loc.longitude,
lat: loc.latitude,
},
];
console.log("markers now", mapMarkers.value);
let min = { lat: loc.latitude, lng: loc.longitude };
let max = { lat: loc.latitude, lng: loc.longitude };
@ -278,12 +283,12 @@ function updateMap() {
i.location.latitude != 0 &&
i.location.longitude != 0
) {
markers.push(
new maplibregl.Marker({
color: "#00FF00",
draggable: false,
}).setLngLat([i.location.longitude, i.location.latitude]),
);
mapMarkers.value.push({
color: "#00FF00",
draggable: false,
lat: i.location.latitude,
lng: i.location.longitude,
});
min.lat = Math.min(min.lat, i.location.latitude);
min.lng = Math.min(min.lng, i.location.longitude);
max.lat = Math.max(max.lat, i.location.latitude);
@ -291,16 +296,16 @@ function updateMap() {
}
}
map.SetMarkers(markers);
const bounds = new maplibregl.LngLatBounds(
new maplibregl.LngLat(min.lng - 0.01, min.lat - 0.01),
new maplibregl.LngLat(max.lng + 0.01, max.lat + 0.01),
);
map.FitBounds(bounds, {
padding: 50,
});
mapBounds.value = {
max: {
lat: max.lat + 0.01,
lng: max.lng + 0.01,
},
min: {
lat: min.lat - 0.01,
lng: min.lng - 0.01,
},
};
}
function onFilterChange(filters) {
console.log("Filters changed");
@ -321,6 +326,7 @@ onMounted(async () => {
// Setup map layer after next tick to ensure map is mounted
await nextTick();
/*
if (mapRef.value) {
const mapEl = mapRef.value.$el || mapRef.value;
mapEl.addEventListener("load", () => {
@ -336,5 +342,6 @@ onMounted(async () => {
});
});
}
*/
});
</script>