a little hacky but it works

This commit is contained in:
Will Freeman
2024-09-30 21:35:40 -05:00
parent acfff6bb40
commit 6d8b3ba42f
7 changed files with 372 additions and 11 deletions

View File

@@ -16,5 +16,9 @@ val PekkoHttpVersion = "1.0.1"
libraryDependencies ++= Seq(
"org.apache.pekko" %% "pekko-actor-typed" % PekkoVersion,
"org.apache.pekko" %% "pekko-stream" % PekkoVersion,
"org.apache.pekko" %% "pekko-http" % PekkoHttpVersion
"org.apache.pekko" %% "pekko-http" % PekkoHttpVersion,
"io.circe" %% "circe-core" % "0.14.7",
"io.circe" %% "circe-generic" % "0.14.9",
"io.circe" %% "circe-parser" % "0.14.9",
"org.apache.pekko" %% "pekko-http-cors" % PekkoHttpVersion
)

View File

@@ -1,6 +1,7 @@
package me.deflock.shotgun
import org.apache.pekko
import org.apache.pekko.http.scaladsl.model.headers.{HttpOrigin, HttpOriginRange, `Access-Control-Allow-Origin`}
import pekko.actor.typed.ActorSystem
import pekko.actor.typed.scaladsl.Behaviors
import pekko.http.scaladsl.Http
@@ -17,16 +18,32 @@ object ShotgunServer {
implicit val system: ActorSystem[Any] = ActorSystem(Behaviors.empty, "my-system")
implicit val executionContext: ExecutionContextExecutor = system.executionContext
val route =
path("oauth2" / "callback") {
get {
parameters(Symbol("code").?) { (code) =>
complete(HttpEntity(ContentTypes.`text/html(UTF-8)`, "<h1>Say hello to Pekko HTTP</h1><p><b>Code: " + code.getOrElse("None") + "</b></p>"))
val routes = {
concat {
path("oauth2" / "callback") {
get {
parameters(Symbol("code").?) { (code) =>
complete(HttpEntity(ContentTypes.`text/html(UTF-8)`, "<h1>Say hello to Pekko HTTP</h1><p><b>Code: " + code.getOrElse("None") + "</b></p>"))
}
}
}
path("alpr") {
get {
parameters("minLat".as[Double], "minLng".as[Double], "maxLat".as[Double], "maxLng".as[Double]) { (minLat, minLng, maxLat, maxLng) =>
val client = new services.OverpassClient() // TODO: make this global
val bBox = services.BoundingBox(minLat, minLng, maxLat, maxLng)
onSuccess(client.getALPRs(bBox)) { json =>
respondWithHeader(`Access-Control-Allow-Origin`.*) {
complete(HttpEntity(ContentTypes.`application/json`, json.toString()))
}
}
}
}
}
}
}
val bindingFuture = Http().newServerAt("localhost", 8080).bind(route)
val bindingFuture = Http().newServerAt("localhost", 8080).bind(routes)
println(s"Server now online. Please navigate to http://localhost:8080\nPress RETURN to stop...")
StdIn.readLine() // let it run until user presses return

View File

@@ -0,0 +1,38 @@
package services
import org.apache.pekko
import pekko.actor.typed.ActorSystem
import pekko.actor.typed.scaladsl.Behaviors
import pekko.http.scaladsl.Http
import pekko.http.scaladsl.model._
import pekko.http.scaladsl.unmarshalling.Unmarshal
import io.circe._
import io.circe.parser._
import org.apache.pekko.http.scaladsl.client.RequestBuilding._
import scala.concurrent.duration.DurationInt
import scala.concurrent.{ExecutionContextExecutor, Future}
import scala.util.{Failure, Success}
case class BoundingBox(minLat: Double, minLng: Double, maxLat: Double, maxLng: Double)
class OverpassClient(implicit val system: ActorSystem[_], implicit val executionContext: ExecutionContextExecutor) {
val baseUrl = "https://overpass-api.de/api/interpreter"
def getALPRs(bBox: BoundingBox): Future[Json] = {
val query = s"""[out:json][bbox:${bBox.minLat},${bBox.minLng},${bBox.maxLat},${bBox.maxLng}];node["man_made"="surveillance"]["surveillance:type"="ALPR"];out body;>;out skel qt;"""
val formData = FormData("data" -> query).toEntity
val request = HttpRequest(
method = HttpMethods.POST,
uri = baseUrl,
entity = formData
)
Http().singleRequest(request).flatMap { response =>
response.entity.toStrict(5.seconds).flatMap { entity =>
Unmarshal(entity).to[String].map(parse(_).getOrElse(Json.Null))
}
}
}
}

152
webapp/package-lock.json generated
View File

@@ -8,6 +8,7 @@
"name": "deflock",
"version": "0.0.0",
"dependencies": {
"axios": "^1.7.7",
"vue": "^3.4.29",
"vue-router": "^4.3.3",
"vuetify": "^3.7.2"
@@ -882,6 +883,21 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "1.7.7",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
"integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -897,6 +913,17 @@
"balanced-match": "^1.0.0"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/computeds": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/computeds/-/computeds-0.0.1.tgz",
@@ -928,6 +955,14 @@
"integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==",
"dev": true
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
@@ -982,6 +1017,38 @@
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
},
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -1043,6 +1110,25 @@
"node": ">= 0.10.0"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
@@ -1174,6 +1260,11 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/read-package-json-fast": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz",
@@ -1967,6 +2058,21 @@
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"dev": true
},
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"axios": {
"version": "1.7.7",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
"integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
"requires": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -1982,6 +2088,14 @@
"balanced-match": "^1.0.0"
}
},
"combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"requires": {
"delayed-stream": "~1.0.0"
}
},
"computeds": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/computeds/-/computeds-0.0.1.tgz",
@@ -2010,6 +2124,11 @@
"integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==",
"dev": true
},
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
},
"entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
@@ -2051,6 +2170,21 @@
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
},
"follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="
},
"form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
}
},
"fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -2096,6 +2230,19 @@
"integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==",
"dev": true
},
"mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
},
"mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"requires": {
"mime-db": "1.52.0"
}
},
"minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
@@ -2170,6 +2317,11 @@
"source-map-js": "^1.2.1"
}
},
"proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"read-package-json-fast": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz",

View File

@@ -11,6 +11,7 @@
"type-check": "vue-tsc --build --force"
},
"dependencies": {
"axios": "^1.7.7",
"vue": "^3.4.29",
"vue-router": "^4.3.3",
"vuetify": "^3.7.2"

View File

@@ -0,0 +1,26 @@
import axios from "axios";
export interface BoundingBox {
minLat: number;
maxLat: number;
minLng: number;
maxLng: number;
}
const apiService = axios.create({
baseURL: "http://localhost:8080",
headers: {
"Content-Type": "application/json",
},
});
export const getALPRs = async (boundingBox: BoundingBox) => {
const queryParams = new URLSearchParams({
minLat: boundingBox.minLat.toString(),
maxLat: boundingBox.maxLat.toString(),
minLng: boundingBox.minLng.toString(),
maxLng: boundingBox.maxLng.toString(),
});
const response = await apiService.get(`/alpr?${queryParams.toString()}`);
return response.data;
}

View File

@@ -1,12 +1,26 @@
<template>
<div class="map-container">
<!-- use-global-leaflet=false is a workaround for a bug in current version of vue-leaflet -->
<l-map v-if="center" ref="map" v-model:zoom="zoom" :center="center" :use-global-leaflet="false">
<l-map
v-if="center"
ref="map"
v-model:zoom="zoom"
v-model:center="center"
:use-global-leaflet="false"
@update:bounds="updateBounds"
@ready="mapLoaded"
>
<l-tile-layer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
layer-type="base"
name="OpenStreetMap"
></l-tile-layer>
<l-marker
@click="console.log('marker clicked')"
v-for="alpr in alprsInView"
:key="alpr.id"
:lat-lng="[alpr.lat, alpr.lon]"
><l-popup>This is an ALPR! Fuck it!</l-popup></l-marker>
</l-map>
<div v-else>
loading...
@@ -16,12 +30,58 @@
<script setup lang="ts">
import 'leaflet/dist/leaflet.css';
import { LMap, LTileLayer, LMarker } from '@vue-leaflet/vue-leaflet';
import { LMap, LTileLayer, LMarker, LPopup } from '@vue-leaflet/vue-leaflet';
import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router'
import type { Ref } from 'vue';
import type { BoundingBox } from '@/services/apiService';
import { getALPRs } from '@/services/apiService';
const zoom: Ref<number> = ref(12);
const center: Ref<[number, number]|null> = ref(null);
const center: Ref<any|null> = ref(null);
const bounds: Ref<BoundingBox|null> = ref(null);
const router = useRouter();
const alprsInView = ref([
{
"type": "node",
"id": 12187369976,
"lat": 34.6616103,
"lon": -86.4870137,
"tags": {
"brand": "Flock Safety",
"brand:wikidata": "Q108485435",
"camera:mount": "pole",
"camera:type": "fixed",
"direction": "335",
"man_made": "surveillance",
"operator": "Flock Safety",
"operator:wikidata": "Q108485435",
"surveillance": "traffic",
"surveillance:type": "ALPR",
"surveillance:zone": "traffic"
}
},
{
"type": "node",
"id": 12187369977,
"lat": 34.6615727,
"lon": -86.4881948,
"tags": {
"brand": "Flock Safety",
"brand:wikidata": "Q108485435",
"camera:mount": "pole",
"camera:type": "fixed",
"direction": "295",
"man_made": "surveillance",
"operator": "Flock Safety",
"operator:wikidata": "Q108485435",
"surveillance": "traffic",
"surveillance:type": "ALPR",
"surveillance:zone": "traffic"
}
}
]);
function getUserLocation(): Promise<[number, number]> {
return new Promise((resolve, reject) => {
@@ -44,10 +104,73 @@ function getUserLocation(): Promise<[number, number]> {
});
};
function mapLoaded(map: any) {
updateBounds(map.getBounds());
}
function updateBounds(newBounds: any) {
bounds.value = {
minLat: newBounds.getSouth(),
maxLat: newBounds.getNorth(),
minLng: newBounds.getWest(),
maxLng: newBounds.getEast(),
};
updateMarkers();
if (center.value) {
updateURL();
}
}
function updateURL() {
if (!center.value) {
return;
}
router.replace({
hash: `#map=${zoom.value}/${center.value.lat.toFixed(6)}/${center.value.lng.toFixed(6)}`
});
}
function updateMarkers() {
// Fetch ALPRs in the current view
if (!bounds.value) {
return;
}
if (zoom.value < 12) {
console.log('zoomed out too far');
return;
}
// getALPRs(bounds.value)
// .then((alprs: any) => {
// alprsInView.value = alprs.elements;
// });
}
onMounted(() => {
const hash = router.currentRoute.value.hash;
if (hash) {
const parts = hash.split('/');
console.log('parts', parts);
if (parts.length === 3 && parts[0].startsWith('#map')) {
const zoomLevelString = parts[0].replace('#map=', '');
zoom.value = parseInt(zoomLevelString, 10);
center.value = {
lat: parseFloat(parts[1]),
lng: parseFloat(parts[2]),
};
console.log('center', center.value);
console.log('zoom', zoom.value);
}
}
getUserLocation()
.then(location => {
center.value = location;
if (!hash)
center.value = { lat: location[0], lng: location[1] };
});
});