Get clean-building locator map
This commit is contained in:
parent
6203e3da75
commit
27fd1faa9c
5 changed files with 233 additions and 7 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
179
ts/components/MapLocator.vue
Normal file
179
ts/components/MapLocator.vue
Normal 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
22
ts/map-utils.ts
Normal 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));
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -89,8 +89,8 @@ export interface MapClickEvent {
|
|||
point: Point;
|
||||
}
|
||||
export interface Marker {
|
||||
color: string;
|
||||
draggable: boolean;
|
||||
color?: string;
|
||||
draggable?: boolean;
|
||||
id: string;
|
||||
location: Location;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue