diff --git a/shotgun/src/main/scala/me/deflock/shotgun/ShotgunServer.scala b/shotgun/src/main/scala/me/deflock/shotgun/ShotgunServer.scala
index 9b6f88e..d25791e 100644
--- a/shotgun/src/main/scala/me/deflock/shotgun/ShotgunServer.scala
+++ b/shotgun/src/main/scala/me/deflock/shotgun/ShotgunServer.scala
@@ -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) =>
diff --git a/shotgun/src/main/scala/services/NominatimClient.scala b/shotgun/src/main/scala/services/NominatimClient.scala
new file mode 100644
index 0000000..585b26b
--- /dev/null
+++ b/shotgun/src/main/scala/services/NominatimClient.scala
@@ -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}"))
+ }
+ }
+ }
+}
diff --git a/webapp/src/services/apiService.ts b/webapp/src/services/apiService.ts
index 097cec1..0561fa9 100644
--- a/webapp/src/services/apiService.ts
+++ b/webapp/src/services/apiService.ts
@@ -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);
+}
diff --git a/webapp/src/views/HomeView.vue b/webapp/src/views/HomeView.vue
index c35919d..9152d42 100644
--- a/webapp/src/views/HomeView.vue
+++ b/webapp/src/views/HomeView.vue
@@ -15,11 +15,31 @@
@ready="mapLoaded"
:options="{ zoomControl: false, attributionControl: false }"
>
+
+
+
+
+ Gomdi-chevron-right
+
+
+
+
+ />
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 = ref(13);
const center: Ref = ref(null);
const bounds: Ref = ref(null);
+const searchField: Ref = ref(null);
+const searchQuery: Ref = ref('');
const router = useRouter();
const canRefreshMarkers = computed(() => zoom.value >= 10);
@@ -58,6 +80,28 @@ const canRefreshMarkers = computed(() => zoom.value >= 10);
const alprsInView: Ref = ref([]);
const bboxForLastRequest: Ref = 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;