mirror of
https://github.com/FoggedLens/deflock.git
synced 2026-02-12 15:02:45 +00:00
search functionality, kinda hacky
This commit is contained in:
@@ -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) =>
|
||||
|
||||
32
shotgun/src/main/scala/services/NominatimClient.scala
Normal file
32
shotgun/src/main/scala/services/NominatimClient.scala
Normal 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}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user