Get clean-building locator map

This commit is contained in:
Eli Ribble 2026-04-03 19:45:12 +00:00
parent 6203e3da75
commit 27fd1faa9c
No known key found for this signature in database
5 changed files with 233 additions and 7 deletions

View file

@ -27,8 +27,8 @@
<script setup lang="ts">
import "maplibre-gl/dist/maplibre-gl.css";
import type { LngLatBoundsLike, Map as MapLibreMap } from "maplibre-gl";
import maplibregl from "maplibre-gl";
import type { LngLatBoundsLike, Map as MapLibreMap } from "maplibre-gl";
import { onMounted, onUnmounted, ref, shallowRef, type Ref } from "vue";
import { Bounds, Marker } from "@/types";
@ -37,7 +37,7 @@ interface Emits {
}
interface Props {
bounds?: Bounds;
markers: Marker[];
markers?: Marker[];
organizationId: number;
tegola: string;
}

View file

@ -0,0 +1,179 @@
<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 { Location, Marker } from "@/types";
// Emits interface
interface Emits {
(e: "update:modelValue", location: Location): void;
(e: "click", location: Location): void;
(e: "load"): void;
(e: "zoomend"): void;
(e: "markerDragEnd", location: Location): void;
}
// Props
interface Props {
modelValue: Location | null;
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[]
>([]);
function _bounds(): LngLatBoundsLike {
if (props.markers) {
return boundsMarkers(props.markers);
} else {
return boundsDefault();
}
}
// Initialize map
const initializeMap = () => {
if (!mapContainer.value) return;
const bounds = _bounds();
map.value = new maplibregl.Map({
bounds: bounds,
container: mapContainer.value,
style: "https://tiles.stadiamaps.com/styles/alidade_smooth.json",
});
map.value.on("click", (e: maplibregl.MapLayerMouseEvent) => {
e.preventDefault();
console.log("internal click", e);
emit("click", {
lat: e.lngLat.lat,
lng: e.lngLat.lng,
});
});
map.value.on("load", () => {
console.log("map loaded");
emit("load");
});
map.value.on("zoomend", () => {
emit("zoomend");
});
};
// 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(markerDef.location)
.addTo(map.value!);
if (markerDef.draggable ?? true) {
marker.on("dragend", () => {
const lngLat = marker.getLngLat();
emit("markerDragEnd", {
lat: lngLat.lat,
lng: lngLat.lng,
});
});
}
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(props.markers[0].location, { duration: 1000 });
} else {
// Multiple markers: fit bounds
const bounds = new maplibregl.LngLatBounds();
props.markers.forEach((marker) => {
bounds.extend([marker.location.lng, marker.location.lat]);
});
map.value.fitBounds(bounds, { padding: 50, duration: 1000 });
}
};
// Watch for modelValue changes to pan to location
watch(
() => props.modelValue,
(newLocation) => {
if (map.value && newLocation) {
map.value.panTo(newLocation, { duration: 1000 });
}
},
{ 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>

22
ts/map-utils.ts Normal file
View file

@ -0,0 +1,22 @@
import type { Marker } from "@/types";
import { LngLat, LngLatBounds } from "maplibre-gl";
export function boundsMarkers(markers: Marker[]): LngLatBounds {
let min_lat = 90;
let min_lng = 180;
let max_lat = -90;
let max_lng = -180;
markers.forEach((marker: Marker) => {
min_lat = Math.min(marker.location.lat, min_lat);
min_lng = Math.min(marker.location.lng, min_lng);
max_lat = Math.min(marker.location.lat, max_lat);
max_lng = Math.min(marker.location.lng, max_lng);
});
return new LngLatBounds(
new LngLat(min_lng, min_lat),
new LngLat(max_lng, max_lat),
);
}
export function boundsDefault(): LngLatBounds {
return new LngLatBounds(new LngLat(-70, 50), new LngLat(-125, 25));
}

View file

@ -156,7 +156,14 @@ select.tall {
You can also click on the map to mark the location precisely
</p>
<div class="map-container">
<map-locator id="map"></map-locator>
<MapLocator
v-model="currentLocation"
:markers="markers"
:initial-zoom="15"
@click="doMapClick"
,
@marker-drag-end="doMapMarkerDragEnd"
/>
</div>
<input type="hidden" id="map-zoom" name="map-zoom" />
<input type="hidden" id="address-country" name="address-country" />
@ -502,16 +509,34 @@ select.tall {
</template>
<script setup lang="ts">
import { ref } from "vue";
import { computed, ref } from "vue";
import AddressSuggestion from "@/components/AddressSuggestion.vue";
import { Address } from "@/type/stadia";
import MapLocator from "@/components/MapLocator.vue";
import type { Location, Marker } from "@/types";
import type { Address } from "@/type/stadia";
const currentLocation = ref<Location | null>(null);
const marker = ref<Marker | null>(null);
const isCollapsed = ref<boolean>(true);
const toggleCollapse = () => {
isCollapsed.value = !isCollapsed.value;
};
const selectedAddress = ref<Address | null>(null);
const markers = computed((): Marker[] => {
if (marker.value) {
return [marker.value];
} else {
return [];
}
});
function doAddressSelected(address: Address) {
console.log("Address selected", address);
}
function doMapClick(location: Location) {
console.log("Map clicked", location);
}
function doMapMarkerDragEnd(location: Location) {
console.log("marker drag end", location);
}
</script>

View file

@ -89,8 +89,8 @@ export interface MapClickEvent {
point: Point;
}
export interface Marker {
color: string;
draggable: boolean;
color?: string;
draggable?: boolean;
id: string;
location: Location;
}