Track user location with map and address data

This is useful because everywhere that we use the AddressAndMapLocator
component we also want to use the user's location and we want to zoom
the map based on their location. Instead of tracking this externally in
3 places we just pull it into the component.
This commit is contained in:
Eli Ribble 2026-04-27 19:40:24 +00:00
parent 3867737fcc
commit a2b8527d91
No known key found for this signature in database
6 changed files with 113 additions and 90 deletions

View file

@ -38,7 +38,7 @@
<template>
<div class="mb-4">
<AddressSuggestion
v-model="modelValue"
v-model="modelValue.address"
placeholder="Start typing an address (min 3 characters)"
@suggestion-selected="doAddressSuggestionSelected"
>
@ -60,33 +60,33 @@
</div>
</template>
<script setup lang="ts">
import { computed, ref } from "vue";
import { computed, onMounted, ref } from "vue";
import AddressSuggestion from "@/components/AddressSuggestion.vue";
import MapLocator from "@/components/MapLocator.vue";
import { Address } from "@/type/api";
import type { Geocode, GeocodeSuggestion, Location } from "@/type/api";
import { useStoreGeocode } from "@/store/geocode";
import { Camera } from "@/type/map";
import { useStoreLocation } from "@/store/location";
import { Camera, Locator } from "@/type/map";
import type { MapClickEvent, Marker } from "@/types";
interface Emits {
(e: "update:modelValue", value: Address): void;
(e: "update:modelValue", value: Locator): void;
}
interface Props {
initialCamera?: Camera;
modelValue: Address;
modelValue: Locator;
}
const address = ref<string>("");
const currentCamera = ref<Camera>(new Camera());
const emit = defineEmits<Emits>();
const geocode = useStoreGeocode();
const markers = computed((): Marker[] => {
if (!props.modelValue.location) {
if (!props.modelValue.address.location) {
return [];
}
if (
props.modelValue.location.latitude == 0.0 ||
props.modelValue.location.longitude == 0.0
props.modelValue.address.location.latitude == 0.0 ||
props.modelValue.address.location.longitude == 0.0
) {
return [];
}
@ -94,15 +94,12 @@ const markers = computed((): Marker[] => {
color: "#FF0000",
draggable: true,
id: "x",
location: props.modelValue.location,
location: props.modelValue.address.location,
};
return [marker];
});
const modelValue = computed({
get: () => props.modelValue,
set: (value: Address) => emit("update:modelValue", value),
});
const props = defineProps<Props>();
const storeLocation = useStoreLocation();
function doAddressSuggestionSelected(suggestion: GeocodeSuggestion) {
console.log("Address suggestion selected", suggestion);
@ -110,7 +107,11 @@ function doAddressSuggestionSelected(suggestion: GeocodeSuggestion) {
}
async function doAddressSuggestionDetails(suggestion: GeocodeSuggestion) {
// Fetch full details for the selected suggestion
updateModel(suggestion.gid, suggestion.detail, props.modelValue.location);
updateModel(
suggestion.gid,
suggestion.detail,
props.modelValue.address.location,
);
const url = `/api/geocode/by-gid/${suggestion.gid}`;
const response = await fetch(url);
if (!response.ok) {
@ -126,14 +127,18 @@ async function doAddressSuggestionDetails(suggestion: GeocodeSuggestion) {
updateModel(data.address.gid, data.address.raw, data.address.location);
}
function doMapClick(event: MapClickEvent) {
updateModel(props.modelValue.gid, props.modelValue.raw, event.location);
updateModel(
props.modelValue.address.gid,
props.modelValue.address.raw,
event.location,
);
geocode
.reverse(event.location)
.reverseClosest(event.location)
.then((code: Geocode) => {
updateModel(
code.address.gid,
code.address.raw,
props.modelValue.location,
props.modelValue.address.location,
);
console.log("reverse geocoded", code);
})
@ -142,7 +147,11 @@ function doMapClick(event: MapClickEvent) {
});
}
function doMapMarkerDragEnd(location: Location) {
updateModel(props.modelValue.gid, props.modelValue.raw, location);
updateModel(
props.modelValue.address.gid,
props.modelValue.address.raw,
location,
);
}
function updateModel(
address_gid: string,
@ -161,6 +170,27 @@ function updateModel(
"",
location,
);
emit("update:modelValue", newAddress);
const newLocator = new Locator(newAddress, props.modelValue.location);
emit("update:modelValue", newLocator);
}
onMounted(() => {
storeLocation
.get()
.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);
});
});
</script>

View file

@ -125,7 +125,7 @@ select.tall {
<p class="small text-muted mb-2">
You can also click on the map to mark the location precisely
</p>
<AddressAndMapLocator v-model="address" />
<AddressAndMapLocator v-model="locator" />
<!-- Mosquito Activity Section -->
<div class="form-section">
@ -470,21 +470,17 @@ import MapLocator from "@/components/MapLocator.vue";
import AddressAndMapLocator from "@/rmo/components/AddressAndMapLocator.vue";
import { useStoreGeocode } from "@/store/geocode";
import { useStoreLocal } from "@/store/local";
import { useStoreLocation } from "@/store/location";
import { useStorePublicReport } from "@/store/publicreport";
import type { Marker } from "@/types";
import {
Address,
type Geocode,
type GeocodeSuggestion,
type Location,
type PublicReport,
} from "@/type/api";
import { Locator } from "@/type/map";
import type { Camera } from "@/type/map";
const address = ref<Address>(new Address());
const currentCamera = ref<Camera | null>(null);
const currentLocation = ref<Location | null>(null);
const locator = ref<Locator>(new Locator());
const errorMessage = ref("");
const formElement = ref<HTMLFormElement | null>(null);
const images = ref<Image[]>([]);
@ -492,7 +488,6 @@ const isSubmitting = ref(false);
const showMore = ref<boolean>(false);
const storeLocal = useStoreLocal();
const storeLocation = useStoreLocation();
const storePublicReport = useStorePublicReport();
const geocode = useStoreGeocode();
const router = useRouter();
@ -504,32 +499,28 @@ async function doSubmit() {
try {
const client_id = storeLocal.getClientID();
const formData = new FormData(formElement.value);
formData.append("address.gid", locator.value.address.gid);
formData.append("address.raw", locator.value.address.raw);
formData.append(
"address.location.latitude",
locator.value.address.location?.latitude?.toString() ?? "0",
);
formData.append(
"address.location.longitude",
locator.value.address.location?.longitude?.toString() ?? "0",
);
formData.append("client_id", client_id);
if (address.value) {
formData.append("address.gid", address.value.gid);
formData.append("address.raw", address.value.raw);
if (address.value.location) {
formData.append(
"address.location.latitude",
address.value.location.latitude.toString(),
);
formData.append(
"address.location.longitude",
address.value.location.longitude.toString(),
);
}
}
formData.append(
"location.accuracy",
currentLocation.value?.accuracy?.toString() ?? "0",
locator.value.location.accuracy?.toString() ?? "0",
);
formData.append(
"location.latitude",
currentLocation.value?.latitude.toString() ?? "0",
locator.value.location.latitude?.toString() ?? "0",
);
formData.append(
"location.longitude",
currentLocation.value?.longitude.toString() ?? "0",
locator.value.location.longitude?.toString() ?? "0",
);
images.value.forEach((image, index) => {
formData.append(`image[${index}]`, image.file, image.name);
@ -549,20 +540,4 @@ async function doSubmit() {
isSubmitting.value = false;
}
}
onMounted(() => {
storeLocation
.get()
.then((loc: GeolocationPosition) => {
console.log("user geolocation", loc);
const coords = loc.coords;
currentLocation.value = coords;
currentCamera.value = {
location: coords,
zoom: 15,
};
})
.catch((e) => {
console.log("failed to get location", e);
});
});
</script>

View file

@ -210,7 +210,7 @@ select.tall {
<p class="small text-muted mb-2">
You can also click on the map to mark the location precisely
</p>
<AddressAndMapLocator v-model="address" />
<AddressAndMapLocator v-model="locator" />
</div>
<button
@ -595,15 +595,13 @@ import { useStoreLocation } from "@/store/location";
import { useStorePublicReport } from "@/store/publicreport";
import type { Marker } from "@/types";
import {
Address,
type Geocode,
type GeocodeSuggestion,
type Location,
type PublicReport,
} from "@/type/api";
import type { Camera } from "@/type/map";
import { type Camera, Locator } from "@/type/map";
const address = ref<Address>(new Address());
const currentCamera = ref<Camera | null>(null);
const currentLocation = ref<Location | null>(null);
const errorMessage = ref("");
@ -611,6 +609,7 @@ const formElement = ref<HTMLFormElement | null>(null);
const geocode = useStoreGeocode();
const images = ref<Image[]>([]);
const isSubmitting = ref(false);
const locator = ref<Locator>(new Locator());
const marker = ref<Marker | null>(null);
const markers = computed((): Marker[] => {
if (marker.value) {
@ -633,31 +632,29 @@ async function doSubmit() {
const client_id = storeLocal.getClientID();
const formData = new FormData(formElement.value);
formData.append("client_id", client_id);
if (address.value) {
formData.append("address.gid", address.value.gid);
formData.append("address.raw", address.value.raw);
if (address.value.location) {
formData.append(
"address.location.latitude",
address.value.location.latitude.toString(),
);
formData.append(
"address.location.longitude",
address.value.location.longitude.toString(),
);
}
formData.append("address.gid", locator.value.address.gid);
formData.append("address.raw", locator.value.address.raw);
if (locator.value.address.location) {
formData.append(
"address.location.latitude",
locator.value.address.location.latitude.toString(),
);
formData.append(
"address.location.longitude",
locator.value.address.location.longitude.toString(),
);
}
formData.append(
"location.accuracy",
currentLocation.value?.accuracy?.toString() ?? "0",
locator.value.location?.accuracy?.toString() ?? "0",
);
formData.append(
"location.latitude",
currentLocation.value?.latitude.toString() ?? "0",
locator.value.location?.latitude?.toString() ?? "0",
);
formData.append(
"location.longitude",
currentLocation.value?.longitude.toString() ?? "0",
locator.value.location?.longitude?.toString() ?? "0",
);
images.value.forEach((image, index) => {
formData.append(`image[${index}]`, image.file, image.name);

View file

@ -10,10 +10,7 @@
Please enter the address so we can match your response with our records.
</p>
<AddressAndMapLocator
:initialCamera="initialCamera"
v-model="modelValue.address"
/>
<AddressAndMapLocator :initialCamera="initialCamera" v-model="locator" />
<div class="d-flex gap-2 mt-4">
<RouterLink
@ -30,14 +27,14 @@
</div>
</template>
<script setup lang="ts">
import { computed, ref } from "vue";
import { computed, onMounted, ref } from "vue";
import { router } from "@/rmo/route/config";
import type { District, PublicReportCompliance } from "@/type/api";
import HeaderCompliance from "@/rmo/components/HeaderCompliance.vue";
import ProgressBarCompliance from "@/rmo/components/ProgressBarCompliance.vue";
import AddressAndMapLocator from "@/rmo/components/AddressAndMapLocator.vue";
import { Camera } from "@/type/map";
import { Camera, Locator } from "@/type/map";
import { useRoutes } from "@/rmo/route/use";
interface Emits {
@ -50,7 +47,9 @@ interface Props {
publicID: string;
}
const emit = defineEmits<Emits>();
const error = ref<string>("");
const locator = ref<Locator>(new Locator());
const props = defineProps<Props>();
const initialCamera = computed((): Camera | undefined => {
if (props.modelValue.location) {
@ -63,6 +62,8 @@ const initialCamera = computed((): Camera | undefined => {
});
const routes = useRoutes();
function doContinue() {
props.modelValue.address = locator.value.address;
props.modelValue.location = locator.value.location;
emit("update:modelValue", props.modelValue);
emit("doAddress");
if (props.modelValue.concerns.length > 0) {
@ -71,4 +72,7 @@ function doContinue() {
router.push(routes.ComplianceEvidence(props.publicID));
}
}
onMounted(() => {
locator.value.address = props.modelValue.address;
});
</script>

View file

@ -7,14 +7,12 @@ export const useStoreGeocode = defineStore("geocode", () => {
const loading = ref(false);
const error = ref(null);
// Actions
async function reverse(location: Location): Promise<Geocode> {
async function doReverse(url: string, location: Location): Promise<Geocode> {
loading.value = true;
error.value = null;
try {
//const url = `https://api.stadiamaps.com/geocoding/v2/reverse?point.lat=${location.lat}&point.lon=${location.lng}`;
const url = "/api/geocode/reverse";
const response = await fetch(url, {
body: JSON.stringify(location),
method: "POST",
@ -29,9 +27,17 @@ export const useStoreGeocode = defineStore("geocode", () => {
throw err;
}
}
// Actions
async function reverse(location: Location): Promise<Geocode> {
return doReverse("/api/geocode/reverse", location);
}
async function reverseClosest(location: Location): Promise<Geocode> {
return doReverse("/api/geocode/reverse/closest", location);
}
return {
// Actions
reverse,
reverseClosest,
};
});

View file

@ -9,6 +9,17 @@ export class Camera {
this.zoom = zoom;
}
}
export class Locator {
address: Address;
location: Location;
constructor(
address: Address = new Address(),
location: Location = new Location(),
) {
this.address = address;
this.location = location;
}
}
export type MoveEndEventInternal = maplibregl.MapLibreEvent<
| maplibregl.MapMouseEvent
| maplibregl.MapTouchEvent