search functionality, kinda hacky

This commit is contained in:
Will Freeman
2024-10-04 15:09:41 -05:00
parent 64da95d76a
commit 0f432e724f
4 changed files with 150 additions and 5 deletions

View File

@@ -13,8 +13,9 @@ import pekko.http.scaladsl.server.Directives.{path, _}
import org.apache.pekko.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._
import org.apache.pekko.http.scaladsl.server.RejectionHandler
import scala.concurrent.{Await, ExecutionContextExecutor, Future}
import scala.concurrent.duration._
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import scala.concurrent.ExecutionContextExecutor
import scala.io.StdIn
object ShotgunServer {
@@ -26,6 +27,7 @@ object ShotgunServer {
val logging = Logging(system, getClass)
val client = new services.OverpassClient()
val nominatim = new services.NominatimClient()
// CORS
val allowedOrigins = List(
@@ -60,6 +62,16 @@ object ShotgunServer {
}
}
},
path("geocode") {
get {
parameters("query".as[String]) { query =>
val encodedQuery = URLEncoder.encode(query, StandardCharsets.UTF_8.toString)
onSuccess(nominatim.geocodePhrase(encodedQuery)) { json =>
complete(json)
}
}
}
},
path("oauth2" / "callback") {
get {
parameters(Symbol("code").?) { (code) =>

View File

@@ -0,0 +1,32 @@
package services
import org.apache.pekko
import org.apache.pekko.actor.ActorSystem
import pekko.http.scaladsl.Http
import pekko.http.scaladsl.model._
import pekko.http.scaladsl.unmarshalling.Unmarshal
import spray.json._
import scala.concurrent.{ExecutionContextExecutor, Future}
class NominatimClient(implicit val system: ActorSystem, implicit val executionContext: ExecutionContextExecutor) {
val baseUrl = "https://nominatim.openstreetmap.org/search"
def geocodePhrase(query: String): Future[JsValue] = {
val request = HttpRequest(
uri = s"$baseUrl?q=$query&format=json",
headers = List(headers.`User-Agent`("DeFlock/1.0"))
)
Http().singleRequest(request).flatMap { response =>
response.status match {
case StatusCodes.OK =>
Unmarshal(response.entity).to[String].map { jsonString =>
jsonString.parseJson
}
case _ =>
response.discardEntityBytes()
Future.failed(new Exception(s"Failed to geocode phrase: ${response.status}"))
}
}
}
}

View File

@@ -54,3 +54,51 @@ export const getALPRs = async (boundingBox: BoundingBox) => {
const response = await apiService.get(`/alpr?${queryParams.toString()}`);
return response.data;
}
export const geocodeQuery = async (query: string, currentLocation: any) => {
const encodedQuery = encodeURIComponent(query);
const results = (await apiService.get(`/geocode?query=${encodedQuery}`)).data;
function findNearestResult(results: any, currentLocation: any) {
console.log(currentLocation, results);
let nearestResult = results[0];
let nearestDistance = Number.MAX_VALUE;
for (const result of results) {
const distance = Math.sqrt(
Math.pow(result.lat - currentLocation.lat, 2) +
Math.pow(result.lon - currentLocation.lng, 2)
);
if (distance < nearestDistance) {
nearestResult = result;
nearestDistance = distance;
}
}
return nearestResult;
}
if (!results.length) return null;
const cityStatePattern = /(.+),\s*(\w{2})/;
const postalCodePattern = /\d{5}/;
if (cityStatePattern.test(query)) {
console.debug("cityStatePattern");
const cityStateResults = results.filter((result: any) =>
["city", "town", "village", "hamlet", "suburb", "quarter", "neighbourhood", "borough"].includes(result.addresstype)
);
if (cityStateResults.length) {
return findNearestResult(cityStateResults, currentLocation);
}
}
if (postalCodePattern.test(query)) {
console.debug("postalCodePattern");
const postalCodeResults = results.filter((result: any) => result.addresstype === "postcode");
if (postalCodeResults.length) {
return findNearestResult(postalCodeResults, currentLocation);
}
}
console.debug("defaultPattern");
return findNearestResult(results, currentLocation);
}

View File

@@ -15,11 +15,31 @@
@ready="mapLoaded"
:options="{ zoomControl: false, attributionControl: false }"
>
<l-control position="topleft">
<v-text-field
class="map-search"
ref="searchField"
prepend-inner-icon="mdi-magnify"
placeholder="Search for a location"
single-line
variant="solo"
clearable
hide-details
v-model="searchQuery"
@keyup.enter="onSearch"
>
<template v-slot:append-inner>
<v-btn :disabled="!searchQuery" variant="text" flat color="#0080BC" @click="onSearch">
Go<v-icon end>mdi-chevron-right</v-icon>
</v-btn>
</template>
</v-text-field>
</l-control>
<l-tile-layer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
layer-type="base"
name="OpenStreetMap"
></l-tile-layer>
/>
<l-control-zoom position="bottomright" />
<l-marker
v-for="alpr in alprsInView"
@@ -41,16 +61,18 @@
<script setup lang="ts">
import 'leaflet/dist/leaflet.css';
import { LMap, LTileLayer, LMarker, LPopup, LControlZoom } from '@vue-leaflet/vue-leaflet';
import { LMap, LTileLayer, LMarker, LPopup, LControlZoom, LControl } from '@vue-leaflet/vue-leaflet';
import { ref, onMounted, computed } from 'vue';
import { useRouter } from 'vue-router'
import type { Ref } from 'vue';
import { BoundingBox } from '@/services/apiService';
import { getALPRs } from '@/services/apiService';
import { getALPRs, geocodeQuery } from '@/services/apiService';
const zoom: Ref<number> = ref(13);
const center: Ref<any|null> = ref(null);
const bounds: Ref<BoundingBox|null> = ref(null);
const searchField: Ref<any|null> = ref(null);
const searchQuery: Ref<string> = ref('');
const router = useRouter();
const canRefreshMarkers = computed(() => zoom.value >= 10);
@@ -58,6 +80,28 @@ const canRefreshMarkers = computed(() => zoom.value >= 10);
const alprsInView: Ref<any[]> = ref([]);
const bboxForLastRequest: Ref<BoundingBox|null> = ref(null);
function onSearch() {
if (searchField.value) {
console.log('Blurring search field');
searchField.value?.blur();
}
if (!searchQuery.value) {
return;
}
geocodeQuery(searchQuery.value, center.value)
.then((result: any) => {
if (!result) {
alert('No results found');
return;
}
console.log('Geocode result:', result);
const { lat, lon: lng } = result;
center.value = { lat, lng };
zoom.value = 13;
searchQuery.value = '';
});
}
function getUserLocation(): Promise<[number, number]> {
return new Promise((resolve, reject) => {
if (navigator.geolocation) {
@@ -171,6 +215,15 @@ onMounted(() => {
overflow: auto;
}
.map-search {
/* position: absolute;
top: 16px;
left: 16px; */
width: calc(100vw - 32px);
max-width: 400px;
z-index: 1000;
}
.map-notif {
position: absolute;
text-align: center;