Remove direct calls to stadia API from geocoding

This commit is contained in:
Eli Ribble 2026-04-06 16:54:48 +00:00
parent 43dce16fbd
commit 9ef6aaa406
No known key found for this signature in database
20 changed files with 213 additions and 274 deletions

View file

@ -61,13 +61,13 @@
<div v-if="suggestions.length > 0" class="suggestions-container list-group">
<div
v-for="(suggestion, index) in suggestions"
:key="suggestion.properties.gid || index"
:key="suggestion.gid"
class="suggestion-item list-group-item"
@click="selectSuggestion(suggestion)"
>
<div class="main-address">{{ suggestion.properties.name }}</div>
<div class="main-address">{{ suggestion.detail }}</div>
<div class="place-info">
{{ suggestion.properties.coarse_location }}
{{ suggestion.type }} {{ suggestion.locality }}
</div>
</div>
</div>
@ -76,41 +76,36 @@
<script setup lang="ts">
import { ref, watch } from "vue";
import { Address } from "@/type/stadia";
import { GeocodeSuggestion } from "@/type/api";
// Props
interface Props {
modelValue?: Address | null;
modelValue?: string;
organizationID?: string;
placeholder?: string;
apiKey?: string;
}
const props = withDefaults(defineProps<Props>(), {
modelValue: null,
modelValue: "",
placeholder: "Enter address",
apiKey: "",
});
// Emits
const emit = defineEmits<{
"update:modelValue": [value: Address | null];
"address-selected": [address: Address];
"update:modelValue": [value: string];
"suggestion-selected": [address: GeocodeSuggestion];
}>();
// State
const searchText = ref("");
const suggestions = ref<Address[]>([]);
const suggestions = ref<GeocodeSuggestion[]>([]);
const debounceTimer = ref<ReturnType<typeof setTimeout> | null>(null);
// Watch for external changes to modelValue
watch(
() => props.modelValue,
(newValue) => {
if (newValue) {
searchText.value = formatAddressDisplay(newValue);
} else {
searchText.value = "";
}
searchText.value = newValue;
},
{ immediate: true },
);
@ -134,61 +129,41 @@ function handleInput() {
debounceTimer.value = setTimeout(async () => {
await fetchAddressSuggestions(text);
}, 300);
// Update the model
emit("update:modelValue", text);
}
async function fetchAddressSuggestions(text: string) {
try {
const q = encodeURIComponent(text);
//const url = `https://api.stadiamaps.com/geocoding/v2/autocomplete?text=${encodeURIComponent(
//text,
//)}&focus.point.lat=35&focus.point.lon=-115`;
const url = `/api/geocode/suggestion?query=${q}`;
let url = `/api/geocode/suggestion?query=${q}`;
if (props.organizationID) {
url = url + "&org=" + props.organizationID;
}
const response = await fetch(url);
const data = await response.json();
suggestions.value = data.features || [];
suggestions.value = data || [];
} catch (error) {
console.error("Error fetching geocoding suggestions:", error);
suggestions.value = [];
}
}
async function selectSuggestion(suggestion: Address) {
async function selectSuggestion(suggestion: GeocodeSuggestion) {
try {
// Fetch full details for the selected suggestion
const url = `https://api.stadiamaps.com/geocoding/v2/place_details?ids=${suggestion.properties.gid}`;
const response = await fetch(url);
const data = await response.json();
const fullAddress: Address = data.features[0];
// Update display text
searchText.value = formatAddressDisplay(fullAddress);
searchText.value = suggestion.detail + ", " + suggestion.locality;
// Clear suggestions
suggestions.value = [];
// Emit the full address object
emit("update:modelValue", fullAddress);
emit("address-selected", fullAddress);
//emit("update:modelValue", search);
emit("suggestion-selected", suggestion);
} catch (error) {
console.error("Error fetching place details:", error);
}
}
function formatAddressDisplay(address: Address): string {
const props = address.properties;
if (props.formatted_address_line) {
return props.formatted_address_line;
} else if (props.address_components) {
const num = props.address_components.number ?? "";
const street = props.address_components.street ?? "";
const location = props.coarse_location ?? "";
return `${num} ${street}, ${location}`.trim();
} else if (props.name != "") {
return `${props.name ?? ""}, ${props.coarse_location ?? ""}`.trim();
} else {
return "???";
}
}
</script>

View file

@ -16,7 +16,8 @@
<script setup lang="ts">
import MapProxiedArcgisTile from "@/components/MapProxiedArcgisTile.vue";
import { Location, Marker } from "@/types";
import { Location } from "@/type/api";
import { Marker } from "@/types";
import { useSessionStore } from "@/store/session";
interface Props {

View file

@ -47,8 +47,8 @@ const props = withDefaults(defineProps<Props>(), {
// default bounds cover a bunch of the continental US
bounds: () => {
return {
max: { lng: -70, lat: 50 },
min: { lng: -125, lat: 25 },
max: { longitude: -70, latitude: 50 },
min: { longitude: -125, latitude: 25 },
};
},
});
@ -59,8 +59,8 @@ const map: Ref<MapLibreMap | null> = shallowRef(null);
function _bounds(): LngLatBoundsLike {
return new maplibregl.LngLatBounds(
new maplibregl.LngLat(boundsSafe.min.lng, boundsSafe.min.lat),
new maplibregl.LngLat(boundsSafe.max.lng, boundsSafe.max.lat),
new maplibregl.LngLat(boundsSafe.min.longitude, boundsSafe.min.latitude),
new maplibregl.LngLat(boundsSafe.max.longitude, boundsSafe.max.latitude),
);
}
const initializeMap = () => {

View file

@ -22,7 +22,8 @@ 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";
import type { Marker } from "@/types";
import type { Location } from "@/type/api";
import type { Camera, MoveEndEventInternal } from "@/type/map";
// Emits interface
@ -71,8 +72,8 @@ const initializeMap = () => {
e.preventDefault();
console.log("internal click", e);
emit("click", {
lat: e.lngLat.lat,
lng: e.lngLat.lng,
latitude: e.lngLat.lat,
longitude: e.lngLat.lng,
});
});
@ -99,7 +100,10 @@ const initializeMap = () => {
function updateModel(_map: maplibregl.Map) {
const center = _map.getCenter();
const newCamera: Camera = {
location: center,
location: {
latitude: center.lat,
longitude: center.lng,
},
zoom: _map.getZoom(),
};
emit("update:modelValue", newCamera);
@ -118,15 +122,18 @@ const updateMarkers = () => {
color: markerDef.color || "#FF0000",
draggable: markerDef.draggable ?? true,
})
.setLngLat(markerDef.location)
.setLngLat({
lat: markerDef.location.latitude,
lng: markerDef.location.longitude,
})
.addTo(map.value!);
if (markerDef.draggable ?? true) {
marker.on("dragend", () => {
const lngLat = marker.getLngLat();
emit("markerDragEnd", {
lat: lngLat.lat,
lng: lngLat.lng,
latitude: lngLat.lat,
longitude: lngLat.lng,
});
});
}
@ -147,7 +154,10 @@ const frameMarkers = () => {
if (props.markers.length === 1) {
// Single marker: pan to it
map.value.panTo(
props.markers[0].location,
{
lat: props.markers[0].location.latitude,
lng: props.markers[0].location.longitude,
},
{ duration: 1000, zoom: props.modelValue?.zoom },
{ isInternalUpdate: true },
);
@ -155,7 +165,7 @@ const frameMarkers = () => {
// Multiple markers: fit bounds
const bounds = new maplibregl.LngLatBounds();
props.markers.forEach((marker) => {
bounds.extend([marker.location.lng, marker.location.lat]);
bounds.extend([marker.location.longitude, marker.location.latitude]);
});
map.value.fitBounds(
bounds,
@ -171,7 +181,10 @@ watch(
(newLocation) => {
if (map.value && newLocation) {
map.value.panTo(
newLocation.location,
{
lat: newLocation.location.latitude,
lng: newLocation.location.longitude,
},
{ duration: 1000 },
{ isInternalUpdate: true },
);

View file

@ -46,8 +46,8 @@ const props = withDefaults(defineProps<Props>(), {
// default bounds cover a bunch of the continental US
bounds: () => {
return {
max: { lng: -70, lat: 50 },
min: { lng: -125, lat: 25 },
max: { longitude: -70, latitude: 50 },
min: { longitude: -125, latitude: 25 },
};
},
});
@ -61,8 +61,8 @@ 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),
new maplibregl.LngLat(newBounds.min.longitude, newBounds.min.latitude),
new maplibregl.LngLat(newBounds.max.longitude, newBounds.max.latitude),
);
if (map.value == null) {
return;
@ -83,8 +83,8 @@ watch(
function _bounds(): LngLatBoundsLike {
return new maplibregl.LngLatBounds(
new maplibregl.LngLat(boundsSafe.min.lng, boundsSafe.min.lat),
new maplibregl.LngLat(boundsSafe.max.lng, boundsSafe.max.lat),
new maplibregl.LngLat(boundsSafe.min.longitude, boundsSafe.min.latitude),
new maplibregl.LngLat(boundsSafe.max.longitude, boundsSafe.max.latitude),
);
}
@ -163,7 +163,10 @@ function updateMarkers(markers: Marker[]) {
if (markerInstances.value.has(markerData.id)) {
// Update existing marker position
const marker = markerInstances.value.get(markerData.id)!;
marker.setLngLat([markerData.location.lng, markerData.location.lat]);
marker.setLngLat([
markerData.location.longitude,
markerData.location.latitude,
]);
console.log("updated", markerData);
} else {
// Create a new marker
@ -171,7 +174,10 @@ function updateMarkers(markers: Marker[]) {
color: markerData.color,
draggable: markerData.draggable,
})
.setLngLat([markerData.location.lng, markerData.location.lat])
.setLngLat([
markerData.location.longitude,
markerData.location.latitude,
])
.addTo(map.value!);
markerInstances.value.set(markerData.id, marker);

View file

@ -31,7 +31,8 @@ import {
shallowRef,
type Ref,
} from "vue";
import { Location, MapClickEvent, Marker, Point } from "@/types";
import { MapClickEvent, Marker, Point } from "@/types";
import type { Location } from "@/type/api";
interface Emits {
(e: "map-click", event: MapClickEvent): void;
@ -58,7 +59,7 @@ watch(
([newLocation]) => {
if (map.value) {
map.value.jumpTo({
center: [newLocation.lng, newLocation.lat],
center: [newLocation.longitude, newLocation.latitude],
zoom: 19,
});
}
@ -70,7 +71,7 @@ const initializeMap = () => {
try {
map.value = new maplibregl.Map({
center: [props.location.lng, props.location.lat],
center: [props.location.longitude, props.location.latitude],
container: mapContainer.value,
style: "https://tiles.stadiamaps.com/styles/osm_bright.json",
zoom: 19,
@ -110,8 +111,8 @@ const initializeMap = () => {
mapInstance.on("click", (e) => {
emit("map-click", {
location: {
lat: e.lngLat.lat,
lng: e.lngLat.lng,
latitude: e.lngLat.lat,
longitude: e.lngLat.lng,
},
map: mapInstance,
point: e.point,

View file

@ -73,7 +73,8 @@ import PlanningColumnDetailEntry from "@/components/PlanningColumnDetailEntry.vu
import TimeRelative from "@/components/TimeRelative.vue";
import { shortAddress } from "@/format";
import { useSessionStore } from "@/store/session";
import { Location, MapClickEvent, Marker, Signal } from "@/types";
import { MapClickEvent, Marker, Signal } from "@/types";
import type { Location } from "@/type/api";
interface Props {
markers: Marker[];
@ -125,8 +126,8 @@ const selectedSignalLocation = (): Location => {
const loc = first_pool?.location;
return (
loc || {
lat: 0,
lng: 0,
latitude: 0,
longitude: 0,
}
);
};

View file

@ -44,7 +44,7 @@ function location(signal: Signal): string {
if (signal.address != null) {
return shortAddress(signal.address);
} else {
return `${signal.location.lat}, ${signal.location.lng}`;
return `${signal.location.latitude}, ${signal.location.longitude}`;
}
}
function title(signal: Signal): string {

View file

@ -34,10 +34,11 @@
<input
type="text"
class="form-control"
v-model="poolLocation.lng"
v-model="poolLocation.longitude"
:class="{
'border-warning':
poolLocation.lng !== selectedTask.pool?.location.lng,
poolLocation.longitude !==
selectedTask.pool?.location.longitude,
}"
/>
</div>
@ -49,10 +50,11 @@
<input
type="text"
class="form-control"
v-model="poolLocation.lat"
v-model="poolLocation.latitude"
:class="{
'border-warning':
poolLocation.lat !== selectedTask.pool?.location?.lat,
poolLocation.latitude !==
selectedTask.pool?.location?.latitude,
}"
/>
</div>
@ -150,13 +152,13 @@ import { useSessionStore } from "@/store/session";
import {
Bounds,
Contact,
Location,
MapClickEvent,
Marker,
Pool,
ReviewTask,
User,
} from "@/types";
import type { Location } from "@/type/api";
interface Props {
loading: boolean;
@ -169,8 +171,8 @@ interface Props {
const props = defineProps<Props>();
const poolCondition = ref<string>("unknown");
const poolLocation = ref<Location>({
lat: 0,
lng: 0,
latitude: 0,
longitude: 0,
});
const siteOwner = ref<Contact>({
has_email: false,