Remove direct calls to stadia API from geocoding
This commit is contained in:
parent
43dce16fbd
commit
9ef6aaa406
20 changed files with 213 additions and 274 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Address } from "./types";
|
||||
import { Address } from "@/type/api";
|
||||
|
||||
export function formatAddress(address?: Address): string {
|
||||
if (!address) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type { Marker } from "@/types";
|
||||
import { LngLat, LngLatBounds } from "maplibre-gl";
|
||||
import type { Marker } from "@/types";
|
||||
import type { Location } from "@/type/api";
|
||||
|
||||
export function boundsMarkers(markers: Marker[]): LngLatBounds {
|
||||
let min_lat = 90;
|
||||
|
|
@ -7,10 +8,10 @@ export function boundsMarkers(markers: Marker[]): LngLatBounds {
|
|||
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);
|
||||
min_lat = Math.min(marker.location.latitude, min_lat);
|
||||
min_lng = Math.min(marker.location.longitude, min_lng);
|
||||
max_lat = Math.min(marker.location.latitude, max_lat);
|
||||
max_lng = Math.min(marker.location.longitude, max_lng);
|
||||
});
|
||||
return new LngLatBounds(
|
||||
new LngLat(min_lng, min_lat),
|
||||
|
|
@ -20,3 +21,27 @@ export function boundsMarkers(markers: Marker[]): LngLatBounds {
|
|||
export function boundsDefault(): LngLatBounds {
|
||||
return new LngLatBounds(new LngLat(-125, 50), new LngLat(-70, 25));
|
||||
}
|
||||
|
||||
// Helper functions (outside component)
|
||||
const getBoundingBox = (points: Location[]) => {
|
||||
if (!points || points.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let minLat = points[0].latitude;
|
||||
let maxLat = points[0].latitude;
|
||||
let minLng = points[0].longitude;
|
||||
let maxLng = points[0].longitude;
|
||||
|
||||
for (const point of points) {
|
||||
if (point.latitude < minLat) minLat = point.latitude;
|
||||
if (point.latitude > maxLat) maxLat = point.latitude;
|
||||
if (point.longitude < minLng) minLng = point.longitude;
|
||||
if (point.longitude > maxLng) maxLng = point.longitude;
|
||||
}
|
||||
|
||||
return new window.maplibregl.LngLatBounds(
|
||||
new window.maplibregl.LngLat(minLng, minLat),
|
||||
new window.maplibregl.LngLat(maxLng, maxLat),
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -142,9 +142,9 @@ select.tall {
|
|||
<div class="col-md-6">
|
||||
<div class="mb-3 position-relative">
|
||||
<AddressSuggestion
|
||||
v-model="selectedAddress"
|
||||
v-model="address"
|
||||
placeholder="Start typing an address (min 3 characters)"
|
||||
@address-selected="doAddressSelected"
|
||||
@suggestion-selected="doAddressSuggestionSelected"
|
||||
>
|
||||
</AddressSuggestion>
|
||||
</div>
|
||||
|
|
@ -503,11 +503,13 @@ import ImageUpload, { Image } from "@/components/ImageUpload.vue";
|
|||
import MapLocator from "@/components/MapLocator.vue";
|
||||
import { useGeocodeStore } from "@/store/geocode";
|
||||
import { useLocationStore } from "@/store/location";
|
||||
import type { Location, Marker } from "@/types";
|
||||
import type { Marker } from "@/types";
|
||||
import type { Address, Geocode, GeocodeSuggestion, Location } from "@/type/api";
|
||||
import type { Camera } from "@/type/map";
|
||||
import type { Address, Geocode } from "@/type/stadia";
|
||||
|
||||
const address = ref<string>("");
|
||||
const currentCamera = ref<Camera | null>(null);
|
||||
const currentLocation = ref<Location | null>(null);
|
||||
const errorMessage = ref("");
|
||||
const formElement = ref<HTMLFormElement | null>(null);
|
||||
const images = ref<Image[]>([]);
|
||||
|
|
@ -515,7 +517,7 @@ const isSubmitting = ref(false);
|
|||
const marker = ref<Marker | null>(null);
|
||||
|
||||
const showMore = ref<boolean>(false);
|
||||
const selectedAddress = ref<Address | null>(null);
|
||||
const selectedSuggestion = ref<GeocodeSuggestion | null>(null);
|
||||
const locationStore = useLocationStore();
|
||||
const geocode = useGeocodeStore();
|
||||
const markers = computed((): Marker[] => {
|
||||
|
|
@ -525,13 +527,18 @@ const markers = computed((): Marker[] => {
|
|||
return [];
|
||||
}
|
||||
});
|
||||
function doAddressSelected(address: Address) {
|
||||
console.log("Address selected", address);
|
||||
const geom = address.geometry;
|
||||
if (!geom) {
|
||||
console.error("No geometry on address", address);
|
||||
return;
|
||||
}
|
||||
function doAddressSuggestionSelected(suggestion: GeocodeSuggestion) {
|
||||
console.log("Address suggestion selected", suggestion);
|
||||
|
||||
doAddressSuggestionDetails(suggestion);
|
||||
}
|
||||
async function doAddressSuggestionDetails(suggestion: GeocodeSuggestion) {
|
||||
// Fetch full details for the selected suggestion
|
||||
//const url = `https://api.stadiamaps.com/geocoding/v2/place_details?ids=${suggestion.properties.gid}`;
|
||||
const url = `/api/geocode/by-gid/${suggestion.gid}`;
|
||||
const response = await fetch(url);
|
||||
const data = (await response.json()) as Geocode;
|
||||
|
||||
if (currentCamera.value) {
|
||||
currentCamera.value.zoom = 15;
|
||||
}
|
||||
|
|
@ -539,10 +546,7 @@ function doAddressSelected(address: Address) {
|
|||
color: "#FF0000",
|
||||
draggable: true,
|
||||
id: "x",
|
||||
location: {
|
||||
lat: geom.coordinates[1],
|
||||
lng: geom.coordinates[0],
|
||||
},
|
||||
location: data.location,
|
||||
};
|
||||
}
|
||||
function doMapClick(location: Location) {
|
||||
|
|
@ -576,50 +580,18 @@ async function doSubmit() {
|
|||
errorMessage.value = "";
|
||||
try {
|
||||
const formData = new FormData(formElement.value);
|
||||
if (selectedAddress.value) {
|
||||
const address = selectedAddress.value;
|
||||
const props = address.properties;
|
||||
const context = props.context || {};
|
||||
|
||||
formData.append("address-country", context.iso_3166_a3);
|
||||
if (selectedSuggestion.value) {
|
||||
formData.append("address-gid", selectedSuggestion.value.gid);
|
||||
}
|
||||
if (currentLocation.value) {
|
||||
formData.append(
|
||||
"address-locality",
|
||||
context.whosonfirst?.locality?.name ?? "",
|
||||
);
|
||||
formData.append("address-number", props.address_components?.number ?? "");
|
||||
formData.append(
|
||||
"address-postalcode",
|
||||
props.address_components?.postal_code ?? "",
|
||||
"location_latitude",
|
||||
currentLocation.value.latitude.toString(),
|
||||
);
|
||||
formData.append(
|
||||
"address-region",
|
||||
context.whosonfirst?.region?.abbreviation ?? "",
|
||||
"location_longitude",
|
||||
currentLocation.value.longitude.toString(),
|
||||
);
|
||||
formData.append("address-street", props.address_components?.street ?? "");
|
||||
formData.append(
|
||||
"latitude",
|
||||
address.geometry?.coordinates[1].toString() ?? "0",
|
||||
);
|
||||
formData.append(
|
||||
"longitude",
|
||||
address.geometry?.coordinates[0].toString() ?? "0",
|
||||
);
|
||||
formData.append("latlng-accuracy-type", props.precision ?? "");
|
||||
formData.append(
|
||||
"latlng-accuracy-value",
|
||||
props.distance?.toString() ?? "",
|
||||
);
|
||||
} else {
|
||||
formData.append("address-country", "");
|
||||
formData.append("address-locality", "");
|
||||
formData.append("address-number", "");
|
||||
formData.append("address-postalcode", "");
|
||||
formData.append("address-region", "");
|
||||
formData.append("address-street", "");
|
||||
formData.append("latitude", "0");
|
||||
formData.append("longitude", "0");
|
||||
formData.append("latlng-accuracy-type", "");
|
||||
formData.append("latlng-accuracy-value", "");
|
||||
}
|
||||
images.value.forEach((image, index) => {
|
||||
formData.append(`image[${index}]`, image.file, image.name);
|
||||
|
|
@ -640,7 +612,12 @@ onMounted(() => {
|
|||
locationStore
|
||||
.get()
|
||||
.then((loc: GeolocationPosition) => {
|
||||
console.log("got location");
|
||||
const coords = loc.coords;
|
||||
currentLocation.value = coords;
|
||||
if (currentCamera.value) {
|
||||
currentCamera.value.location = coords;
|
||||
currentCamera.value.zoom = 15;
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log("failed to get location", e);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import type { Geocode } from "@/type/stadia";
|
||||
import type { Location } from "@/types";
|
||||
import type { Geocode, Location } from "@/type/api";
|
||||
|
||||
export const useGeocodeStore = defineStore("geocode", () => {
|
||||
// State
|
||||
|
|
@ -13,8 +12,16 @@ export const useGeocodeStore = defineStore("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 response = await fetch(url);
|
||||
//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",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const data = (await response.json()) as Geocode;
|
||||
return data;
|
||||
} catch (err) {
|
||||
|
|
|
|||
26
ts/type/api.ts
Normal file
26
ts/type/api.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
export interface Address {
|
||||
country: string;
|
||||
gid: string;
|
||||
locality: string;
|
||||
number: string;
|
||||
postal_code: string;
|
||||
raw: string;
|
||||
region: string;
|
||||
street: string;
|
||||
unit: string;
|
||||
}
|
||||
export interface Location {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}
|
||||
export interface GeocodeSuggestion {
|
||||
detail: string;
|
||||
gid: string;
|
||||
locality: string;
|
||||
type: string;
|
||||
}
|
||||
export interface Geocode {
|
||||
address: Address;
|
||||
cell: number;
|
||||
location: Location;
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import maplibregl from "maplibre-gl";
|
||||
import type { Location } from "@/types";
|
||||
import type { Location } from "@/type/api";
|
||||
|
||||
export interface Camera {
|
||||
location: Location;
|
||||
|
|
|
|||
|
|
@ -1,64 +0,0 @@
|
|||
// Interface definitions
|
||||
interface AddressComponents {
|
||||
number?: string;
|
||||
postal_code?: string;
|
||||
street?: string;
|
||||
}
|
||||
|
||||
interface AddressContext {
|
||||
iso_3166_a2: string; // "US"
|
||||
iso_3166_a3: string; // "USA"
|
||||
whosonfirst?: AddressContextWhosOnFirst;
|
||||
}
|
||||
interface AddressContextWhosOnFirst {
|
||||
country: WhosOnFirstEntry;
|
||||
county: WhosOnFirstEntry;
|
||||
locality: WhosOnFirstEntry;
|
||||
region: WhosOnFirstEntry;
|
||||
}
|
||||
interface WhosOnFirstEntry {
|
||||
abbreviation?: string; // "SL" or "UT"
|
||||
gid: string; // "whosonfirst:county:102082877"
|
||||
name: string; // "Salt Lake County"
|
||||
}
|
||||
interface Properties {
|
||||
address_components?: AddressComponents;
|
||||
coarse_location?: string;
|
||||
context: AddressContext;
|
||||
coordinates?: {
|
||||
lat: number;
|
||||
lon: number;
|
||||
};
|
||||
distance?: number;
|
||||
formatted_address_line?: string;
|
||||
gid: string;
|
||||
precision?: string; // "centroid"
|
||||
name: string;
|
||||
}
|
||||
export interface Geometry {
|
||||
type: string;
|
||||
coordinates: [number, number];
|
||||
}
|
||||
export interface Address {
|
||||
geometry?: Geometry;
|
||||
properties: Properties;
|
||||
}
|
||||
export interface GeocodeFeature {
|
||||
geometry: Geometry;
|
||||
properties: Properties;
|
||||
type: string; // "Feature"
|
||||
}
|
||||
export interface Query {
|
||||
"point.lat": number;
|
||||
"point.lng": number;
|
||||
}
|
||||
export interface Geocoding {
|
||||
attribution: string; // https://stadiamaps.com/attribution
|
||||
query: Query;
|
||||
}
|
||||
export interface Geocode {
|
||||
bbox: [number, number, number, number];
|
||||
features: GeocodeFeature[];
|
||||
geocoding: Geocoding;
|
||||
type: string; // "FeatureCollection"
|
||||
}
|
||||
15
ts/types.ts
15
ts/types.ts
|
|
@ -1,15 +1,6 @@
|
|||
import type { Map as MapLibreMap } from "maplibre-gl";
|
||||
import { Address, Location } from "@/type/api";
|
||||
|
||||
export interface Address {
|
||||
country: string;
|
||||
locality: string;
|
||||
number: string;
|
||||
postal_code: string;
|
||||
raw: string;
|
||||
region: string;
|
||||
street: string;
|
||||
unit: string;
|
||||
}
|
||||
export interface Bounds {
|
||||
min: Location;
|
||||
max: Location;
|
||||
|
|
@ -71,10 +62,6 @@ export interface Lead {
|
|||
id: number;
|
||||
title: string;
|
||||
}
|
||||
export interface Location {
|
||||
lat: number;
|
||||
lng: number;
|
||||
}
|
||||
export interface LogEntry {
|
||||
created: string;
|
||||
id: number;
|
||||
|
|
|
|||
|
|
@ -279,28 +279,32 @@ function updateMap() {
|
|||
let max = loc;
|
||||
|
||||
for (const i of selectedCommunication.value?.public_report?.images ?? []) {
|
||||
if (i.location != null && i.location.lat != 0 && i.location.lng != 0) {
|
||||
if (
|
||||
i.location != null &&
|
||||
i.location.latitude != 0 &&
|
||||
i.location.longitude != 0
|
||||
) {
|
||||
mapMarkers.value.push({
|
||||
color: "#00FF00",
|
||||
draggable: false,
|
||||
id: new Date().toISOString(),
|
||||
location: i.location,
|
||||
});
|
||||
min.lat = Math.min(min.lat, i.location.lat);
|
||||
min.lng = Math.min(min.lng, i.location.lng);
|
||||
max.lat = Math.max(max.lat, i.location.lat);
|
||||
max.lng = Math.max(max.lng, i.location.lng);
|
||||
min.latitude = Math.min(min.latitude, i.location.latitude);
|
||||
min.longitude = Math.min(min.longitude, i.location.longitude);
|
||||
max.latitude = Math.max(max.latitude, i.location.latitude);
|
||||
max.longitude = Math.max(max.longitude, i.location.longitude);
|
||||
}
|
||||
}
|
||||
|
||||
mapBounds.value = {
|
||||
max: {
|
||||
lat: max.lat + 0.01,
|
||||
lng: max.lng + 0.01,
|
||||
latitude: max.latitude + 0.01,
|
||||
longitude: max.longitude + 0.01,
|
||||
},
|
||||
min: {
|
||||
lat: min.lat - 0.01,
|
||||
lng: min.lng - 0.01,
|
||||
latitude: min.latitude - 0.01,
|
||||
longitude: min.longitude - 0.01,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,7 +67,8 @@ import ThreeColumn from "@/components/layout/ThreeColumn.vue";
|
|||
import TimeRelative from "@/components/TimeRelative.vue";
|
||||
import { useSignalStore } from "@/store/signal";
|
||||
import { useSessionStore } from "@/store/session";
|
||||
import { Lead, Location, Point, Signal } from "@/types";
|
||||
import { Lead, Point, Signal } from "@/types";
|
||||
import type { Location } from "@/type/api";
|
||||
|
||||
// Refs
|
||||
const mapTile = ref(null);
|
||||
|
|
@ -110,29 +111,6 @@ function doSendToOperations() {
|
|||
function doSplitLead() {
|
||||
console.log("doSplitLead");
|
||||
}
|
||||
// Helper functions (outside component)
|
||||
const getBoundingBox = (points: Location[]) => {
|
||||
if (!points || points.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let minLat = points[0].lat;
|
||||
let maxLat = points[0].lat;
|
||||
let minLng = points[0].lng;
|
||||
let maxLng = points[0].lng;
|
||||
|
||||
for (const point of points) {
|
||||
if (point.lat < minLat) minLat = point.lat;
|
||||
if (point.lat > maxLat) maxLat = point.lat;
|
||||
if (point.lng < minLng) minLng = point.lng;
|
||||
if (point.lng > maxLng) maxLng = point.lng;
|
||||
}
|
||||
|
||||
return new window.maplibregl.LngLatBounds(
|
||||
new window.maplibregl.LngLat(minLng, minLat),
|
||||
new window.maplibregl.LngLat(maxLng, maxLat),
|
||||
);
|
||||
};
|
||||
const markers = computed(() => {
|
||||
return [];
|
||||
});
|
||||
|
|
@ -151,7 +129,7 @@ const updateMap = (signals: Signal[]) => {
|
|||
new window.maplibregl.Marker({
|
||||
color: "#FF0000",
|
||||
draggable: false,
|
||||
}).setLngLat([l.lng, l.lat]),
|
||||
}).setLngLat([l.longitude, l.latitude]),
|
||||
);
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -102,11 +102,11 @@ import {
|
|||
Bounds,
|
||||
Changes,
|
||||
Contact,
|
||||
Location,
|
||||
MapClickEvent,
|
||||
Marker,
|
||||
ReviewTask,
|
||||
} from "@/types";
|
||||
import type { Location } from "@/type/api";
|
||||
|
||||
interface FormData {
|
||||
latitude: number;
|
||||
|
|
@ -147,7 +147,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||
|
||||
// State
|
||||
const newPoolCondition = ref<string>("");
|
||||
const newPoolLocation = ref<Location>({ lat: 0, lng: 0 });
|
||||
const newPoolLocation = ref<Location>({ latitude: 0, longitude: 0 });
|
||||
const newOwnerName = ref<string>("");
|
||||
const newResidentName = ref<string>("");
|
||||
const error = ref<string | null>(null);
|
||||
|
|
@ -186,12 +186,12 @@ const changes = computed<Changes>(() => {
|
|||
} else {
|
||||
unchanged.push("condition");
|
||||
}
|
||||
if (newPoolLocation.value.lat != pool.site.location.lat) {
|
||||
if (newPoolLocation.value.latitude != pool.site.location.latitude) {
|
||||
updated.push("latitude");
|
||||
} else {
|
||||
unchanged.push("latitude");
|
||||
}
|
||||
if (newPoolLocation.value.lng != pool.site.location.lng) {
|
||||
if (newPoolLocation.value.longitude != pool.site.location.longitude) {
|
||||
updated.push("longitude");
|
||||
} else {
|
||||
unchanged.push("longitude");
|
||||
|
|
@ -260,14 +260,14 @@ function updateMap(task: ReviewTask): void {
|
|||
new maplibregl.Marker({
|
||||
color: "#FF0000",
|
||||
draggable: false,
|
||||
}).setLngLat([loc.lng, loc.lat]),
|
||||
}).setLngLat([loc.longitude, loc.latitude]),
|
||||
];
|
||||
|
||||
map.SetMarkers(markers);
|
||||
|
||||
const bounds = new maplibregl.LngLatBounds(
|
||||
new maplibregl.LngLat(loc.lng - 0.005, loc.lat - 0.005),
|
||||
new maplibregl.LngLat(loc.lng + 0.005, loc.lat + 0.005),
|
||||
new maplibregl.LngLat(loc.longitude - 0.005, loc.latitude - 0.005),
|
||||
new maplibregl.LngLat(loc.longitude + 0.005, loc.latitude + 0.005),
|
||||
);
|
||||
|
||||
map.FitBounds(bounds, {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue