2026-04-09 17:21:35 +00:00
|
|
|
<style scoped>
|
|
|
|
|
#address-input {
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
}
|
|
|
|
|
.map-container {
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
|
|
|
|
height: 500px;
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
margin-top: 20px;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
/* Prevent touch scrolling issues */
|
|
|
|
|
touch-action: pan-y pinch-zoom;
|
|
|
|
|
}
|
|
|
|
|
#map {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Mobile-specific adjustments */
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
.map-container {
|
|
|
|
|
height: 400px;
|
|
|
|
|
margin-bottom: 15px;
|
|
|
|
|
margin-top: 15px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Extra small devices */
|
|
|
|
|
@media (max-width: 576px) {
|
|
|
|
|
.map-container {
|
|
|
|
|
height: 350px;
|
|
|
|
|
border-radius: 5px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
<template>
|
|
|
|
|
<div class="mb-4">
|
|
|
|
|
<AddressSuggestion
|
2026-04-27 19:40:24 +00:00
|
|
|
v-model="modelValue.address"
|
2026-04-09 17:21:35 +00:00
|
|
|
placeholder="Start typing an address (min 3 characters)"
|
|
|
|
|
@suggestion-selected="doAddressSuggestionSelected"
|
|
|
|
|
>
|
|
|
|
|
</AddressSuggestion>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Map Placeholder -->
|
|
|
|
|
<div class="mb-4">
|
|
|
|
|
<label class="form-label fw-semibold">Location Preview</label>
|
|
|
|
|
<div class="map-container">
|
|
|
|
|
<MapLocator
|
2026-04-10 14:20:04 +00:00
|
|
|
:initialCamera="initialCamera"
|
2026-04-09 17:21:35 +00:00
|
|
|
:markers="markers"
|
|
|
|
|
@click="doMapClick"
|
|
|
|
|
@marker-drag-end="doMapMarkerDragEnd"
|
2026-04-10 14:20:04 +00:00
|
|
|
v-model="currentCamera"
|
2026-04-09 17:21:35 +00:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
<script setup lang="ts">
|
2026-04-27 19:40:24 +00:00
|
|
|
import { computed, onMounted, ref } from "vue";
|
2026-04-09 17:21:35 +00:00
|
|
|
import AddressSuggestion from "@/components/AddressSuggestion.vue";
|
|
|
|
|
import MapLocator from "@/components/MapLocator.vue";
|
2026-04-10 16:59:29 +00:00
|
|
|
import { Address } from "@/type/api";
|
|
|
|
|
import type { Geocode, GeocodeSuggestion, Location } from "@/type/api";
|
2026-04-14 18:40:54 +00:00
|
|
|
import { useStoreGeocode } from "@/store/geocode";
|
2026-04-27 19:40:24 +00:00
|
|
|
import { useStoreLocation } from "@/store/location";
|
|
|
|
|
import { Camera, Locator } from "@/type/map";
|
2026-04-16 04:47:41 +00:00
|
|
|
import type { MapClickEvent, Marker } from "@/types";
|
2026-04-09 17:21:35 +00:00
|
|
|
|
|
|
|
|
interface Emits {
|
2026-04-27 19:40:24 +00:00
|
|
|
(e: "update:modelValue", value: Locator): void;
|
2026-04-09 17:21:35 +00:00
|
|
|
}
|
|
|
|
|
interface Props {
|
2026-04-10 14:20:04 +00:00
|
|
|
initialCamera?: Camera;
|
2026-04-27 19:40:24 +00:00
|
|
|
modelValue: Locator;
|
2026-04-09 17:21:35 +00:00
|
|
|
}
|
2026-04-10 14:20:04 +00:00
|
|
|
const currentCamera = ref<Camera>(new Camera());
|
2026-04-09 17:21:35 +00:00
|
|
|
const emit = defineEmits<Emits>();
|
2026-04-14 18:40:54 +00:00
|
|
|
const geocode = useStoreGeocode();
|
2026-04-09 17:21:35 +00:00
|
|
|
const markers = computed((): Marker[] => {
|
2026-04-27 19:40:24 +00:00
|
|
|
if (!props.modelValue.address.location) {
|
2026-04-09 17:21:35 +00:00
|
|
|
return [];
|
|
|
|
|
}
|
2026-04-09 22:29:26 +00:00
|
|
|
if (
|
2026-04-27 19:40:24 +00:00
|
|
|
props.modelValue.address.location.latitude == 0.0 ||
|
|
|
|
|
props.modelValue.address.location.longitude == 0.0
|
2026-04-09 22:29:26 +00:00
|
|
|
) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
2026-04-09 22:22:27 +00:00
|
|
|
const marker = {
|
|
|
|
|
color: "#FF0000",
|
|
|
|
|
draggable: true,
|
|
|
|
|
id: "x",
|
2026-04-27 19:40:24 +00:00
|
|
|
location: props.modelValue.address.location,
|
2026-04-09 22:22:27 +00:00
|
|
|
};
|
|
|
|
|
return [marker];
|
2026-04-09 17:21:35 +00:00
|
|
|
});
|
|
|
|
|
const props = defineProps<Props>();
|
2026-04-27 19:40:24 +00:00
|
|
|
const storeLocation = useStoreLocation();
|
2026-04-09 17:21:35 +00:00
|
|
|
function doAddressSuggestionSelected(suggestion: GeocodeSuggestion) {
|
|
|
|
|
console.log("Address suggestion selected", suggestion);
|
|
|
|
|
|
|
|
|
|
doAddressSuggestionDetails(suggestion);
|
|
|
|
|
}
|
|
|
|
|
async function doAddressSuggestionDetails(suggestion: GeocodeSuggestion) {
|
|
|
|
|
// Fetch full details for the selected suggestion
|
2026-04-27 19:40:24 +00:00
|
|
|
updateModel(
|
|
|
|
|
suggestion.gid,
|
|
|
|
|
suggestion.detail,
|
|
|
|
|
props.modelValue.address.location,
|
|
|
|
|
);
|
2026-04-09 17:21:35 +00:00
|
|
|
const url = `/api/geocode/by-gid/${suggestion.gid}`;
|
|
|
|
|
const response = await fetch(url);
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
console.error("Failed to get suggestion detail", response.statusText);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const data = (await response.json()) as Geocode;
|
|
|
|
|
|
|
|
|
|
if (currentCamera.value) {
|
|
|
|
|
console.log("suggestion located, zooming", data);
|
|
|
|
|
currentCamera.value.zoom = 15;
|
|
|
|
|
}
|
2026-04-14 01:32:32 +00:00
|
|
|
updateModel(data.address.gid, data.address.raw, data.address.location);
|
2026-04-09 17:21:35 +00:00
|
|
|
}
|
2026-04-16 04:47:41 +00:00
|
|
|
function doMapClick(event: MapClickEvent) {
|
2026-04-27 19:40:24 +00:00
|
|
|
updateModel(
|
|
|
|
|
props.modelValue.address.gid,
|
|
|
|
|
props.modelValue.address.raw,
|
|
|
|
|
event.location,
|
|
|
|
|
);
|
2026-04-09 17:21:35 +00:00
|
|
|
geocode
|
2026-04-27 19:40:24 +00:00
|
|
|
.reverseClosest(event.location)
|
2026-04-09 17:21:35 +00:00
|
|
|
.then((code: Geocode) => {
|
2026-04-09 22:22:27 +00:00
|
|
|
updateModel(
|
|
|
|
|
code.address.gid,
|
|
|
|
|
code.address.raw,
|
2026-04-27 19:40:24 +00:00
|
|
|
props.modelValue.address.location,
|
2026-04-09 22:22:27 +00:00
|
|
|
);
|
2026-04-09 17:21:35 +00:00
|
|
|
console.log("reverse geocoded", code);
|
|
|
|
|
})
|
|
|
|
|
.catch((e) => {
|
|
|
|
|
console.error("failed to reverse geocode after map click", e);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
function doMapMarkerDragEnd(location: Location) {
|
2026-04-27 19:40:24 +00:00
|
|
|
updateModel(
|
|
|
|
|
props.modelValue.address.gid,
|
|
|
|
|
props.modelValue.address.raw,
|
|
|
|
|
location,
|
|
|
|
|
);
|
2026-04-09 17:21:35 +00:00
|
|
|
}
|
2026-04-09 22:22:27 +00:00
|
|
|
function updateModel(
|
|
|
|
|
address_gid: string,
|
|
|
|
|
address_raw: string,
|
2026-04-10 16:59:29 +00:00
|
|
|
location?: Location,
|
2026-04-09 22:22:27 +00:00
|
|
|
) {
|
2026-04-10 16:59:29 +00:00
|
|
|
const newAddress = new Address(
|
|
|
|
|
"",
|
|
|
|
|
address_gid,
|
|
|
|
|
"",
|
|
|
|
|
"",
|
|
|
|
|
"",
|
|
|
|
|
address_raw,
|
|
|
|
|
"",
|
|
|
|
|
"",
|
|
|
|
|
"",
|
|
|
|
|
location,
|
|
|
|
|
);
|
2026-04-27 19:40:24 +00:00
|
|
|
const newLocator = new Locator(newAddress, props.modelValue.location);
|
|
|
|
|
emit("update:modelValue", newLocator);
|
2026-04-09 17:21:35 +00:00
|
|
|
}
|
2026-04-27 19:40:24 +00:00
|
|
|
onMounted(() => {
|
2026-05-08 22:48:51 +00:00
|
|
|
const geo_config = {
|
|
|
|
|
enableHighAccuracy: true,
|
|
|
|
|
maximumAge: Infinity,
|
|
|
|
|
timeout: 10000,
|
|
|
|
|
};
|
2026-04-27 19:40:24 +00:00
|
|
|
storeLocation
|
2026-05-08 22:48:51 +00:00
|
|
|
.get(geo_config)
|
2026-04-27 19:40:24 +00:00
|
|
|
.then((loc: GeolocationPosition) => {
|
|
|
|
|
console.log("user geolocation", loc);
|
|
|
|
|
const coords = loc.coords;
|
|
|
|
|
// If we don't already have an address then zoom on the users location
|
|
|
|
|
// because an address signals they've typed something or the report came
|
|
|
|
|
// pre-populated with something
|
|
|
|
|
if (props.modelValue.address.gid == "") {
|
|
|
|
|
currentCamera.value = {
|
|
|
|
|
location: coords,
|
|
|
|
|
zoom: 15,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.catch((e) => {
|
|
|
|
|
console.log("failed to get location", e);
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-04-09 17:21:35 +00:00
|
|
|
</script>
|