mirror of
https://github.com/FoggedLens/deflock.git
synced 2026-02-12 15:02:45 +00:00
dockerize
This commit is contained in:
23
Dockerfile
Normal file
23
Dockerfile
Normal file
@@ -0,0 +1,23 @@
|
||||
# Stage 1: Build the frontend
|
||||
FROM node:18 AS frontend-build
|
||||
WORKDIR /app/frontend
|
||||
COPY webapp/package*.json ./
|
||||
RUN npm install
|
||||
COPY webapp ./
|
||||
RUN npm run build
|
||||
|
||||
# Stage 2: Build the Scala app
|
||||
FROM hseeberger/scala-sbt:8u222_1.3.5_2.13.1 AS scala-build
|
||||
WORKDIR /app
|
||||
COPY shotgun/project/ ./project
|
||||
COPY shotgun/build.sbt ./build.sbt
|
||||
COPY shotgun/ ./
|
||||
COPY --from=frontend-build /app/frontend/dist/ ../webapp/dist/
|
||||
RUN sbt assembly
|
||||
|
||||
# Stage 3: Run the Scala app
|
||||
FROM openjdk:11-jre-slim
|
||||
WORKDIR /app
|
||||
COPY --from=scala-build /app/target/scala-2.12/shotgun-assembly-0.1.0-SNAPSHOT.jar ./shotgun-assembly-0.1.0-SNAPSHOT.jar
|
||||
COPY --from=frontend-build /app/frontend/dist/ ../webapp/dist/
|
||||
CMD ["java", "-jar", "shotgun-assembly-0.1.0-SNAPSHOT.jar"]
|
||||
31
docker-compose.prod.yml
Normal file
31
docker-compose.prod.yml
Normal file
@@ -0,0 +1,31 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
deflock:
|
||||
image: public.ecr.aws/w2o0b9g0/deflock.me:latest
|
||||
expose:
|
||||
- 8080
|
||||
#restart: always
|
||||
stdin_open: true
|
||||
|
||||
nginx:
|
||||
image: nginx:latest
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/nginx.conf
|
||||
- /etc/letsencrypt:/etc/letsencrypt
|
||||
environment:
|
||||
- SSL_CERTIFICATE=/etc/letsencrypt/live/deflock.me/fullchain.pem
|
||||
- SSL_CERTIFICATE_KEY=/etc/letsencrypt/live/deflock.me/privkey.pem
|
||||
restart: always
|
||||
|
||||
certbot:
|
||||
image: certbot/certbot
|
||||
environment:
|
||||
- CERTBOT_EMAIL
|
||||
volumes:
|
||||
- /etc/letsencrypt:/etc/letsencrypt
|
||||
- /var/lib/letsencrypt:/var/lib/letsencrypt
|
||||
command: certonly --webroot -w /var/www/certbot -d deflock.me --email ${CERTBOT_EMAIL} --agree-tos --no-eff-email
|
||||
43
nginx.conf
Normal file
43
nginx.conf
Normal file
@@ -0,0 +1,43 @@
|
||||
user nginx;
|
||||
worker_processes 1;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
sendfile on;
|
||||
keepalive_timeout 65;
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name deflock.me www.deflock.me;
|
||||
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
server_name deflock.me www.deflock.me;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/deflock.me/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/deflock.me/privkey.pem;
|
||||
|
||||
location / {
|
||||
proxy_pass http://deflock:8080;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
{"name":"sbt","version":"1.9.1","bspVersion":"2.1.0-M1","languages":["scala"],"argv":["/Users/willfreeman/Library/Caches/Coursier/arc/https/github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.10%252B7/OpenJDK17U-jdk_x64_mac_hotspot_17.0.10_7.tar.gz/jdk-17.0.10+7/Contents/Home/bin/java","-Xms100m","-Xmx100m","-classpath","/Users/willfreeman/Library/Caches/Coursier/arc/https/github.com/sbt/sbt/releases/download/v1.8.2/sbt-1.8.2.zip/sbt/bin/sbt-launch.jar","-Dsbt.script=/Users/willfreeman/Library/Caches/Coursier/arc/https/github.com/sbt/sbt/releases/download/v1.8.2/sbt-1.8.2.zip/sbt/bin/sbt","xsbt.boot.Boot","-bsp"]}
|
||||
{"name":"sbt","version":"1.9.1","bspVersion":"2.1.0-M1","languages":["scala"],"argv":["/Users/willfreeman/Library/Java/JavaVirtualMachines/corretto-11.0.19/Contents/Home/bin/java","-Xms100m","-Xmx100m","-classpath","/Users/willfreeman/Library/Application Support/JetBrains/IdeaIC2024.1/plugins/Scala/launcher/sbt-launch.jar","-Dsbt.script=/Users/willfreeman/Library/Application%20Support/Coursier/bin/sbt","xsbt.boot.Boot","-bsp"]}
|
||||
@@ -17,8 +17,7 @@ libraryDependencies ++= Seq(
|
||||
"org.apache.pekko" %% "pekko-actor-typed" % PekkoVersion,
|
||||
"org.apache.pekko" %% "pekko-stream" % PekkoVersion,
|
||||
"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
|
||||
"org.apache.pekko" %% "pekko-http-spray-json" % PekkoHttpVersion,
|
||||
"org.apache.pekko" %% "pekko-http-cors" % PekkoHttpVersion,
|
||||
"org.apache.pekko" %% "pekko-slf4j" % PekkoVersion,
|
||||
)
|
||||
|
||||
1
shotgun/project/plugins.sbt
Normal file
1
shotgun/project/plugins.sbt
Normal file
@@ -0,0 +1 @@
|
||||
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "1.1.0")
|
||||
@@ -1,25 +1,65 @@
|
||||
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 org.apache.pekko.actor.ActorSystem
|
||||
import org.apache.pekko.event.Logging
|
||||
import org.apache.pekko.http.cors.scaladsl.CorsDirectives.cors
|
||||
import org.apache.pekko.http.cors.scaladsl.model.HttpOriginMatcher
|
||||
import org.apache.pekko.http.cors.scaladsl.settings.CorsSettings
|
||||
import org.apache.pekko.http.scaladsl.model.headers.{HttpOrigin, `Access-Control-Allow-Origin`}
|
||||
import pekko.http.scaladsl.Http
|
||||
import pekko.http.scaladsl.model._
|
||||
import pekko.http.scaladsl.server.Directives._
|
||||
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.ExecutionContextExecutor
|
||||
import scala.concurrent.{Await, ExecutionContextExecutor, Future}
|
||||
import scala.concurrent.duration._
|
||||
import scala.io.StdIn
|
||||
|
||||
object ShotgunServer {
|
||||
|
||||
def main(args: Array[String]): Unit = {
|
||||
|
||||
implicit val system: ActorSystem[Any] = ActorSystem(Behaviors.empty, "my-system")
|
||||
implicit val executionContext: ExecutionContextExecutor = system.executionContext
|
||||
implicit val system: ActorSystem = ActorSystem("my-system")
|
||||
implicit val executionContext: ExecutionContextExecutor = system.dispatcher
|
||||
val logging = Logging(system, getClass)
|
||||
|
||||
val routes = {
|
||||
concat {
|
||||
val client = new services.OverpassClient()
|
||||
|
||||
// CORS
|
||||
val allowedOrigins = List(
|
||||
"http://localhost:8080",
|
||||
"http://localhost:5173",
|
||||
"https://deflock.me",
|
||||
"https://www.deflock.me",
|
||||
).map(HttpOrigin(_)) // TODO: make this a config setting
|
||||
val corsSettings = CorsSettings.default
|
||||
.withAllowedOrigins(HttpOriginMatcher(allowedOrigins: _*))
|
||||
.withExposedHeaders(List(`Access-Control-Allow-Origin`.name))
|
||||
|
||||
val rejectionHandler = RejectionHandler.newBuilder()
|
||||
.handleNotFound {
|
||||
complete((StatusCodes.NotFound, "The requested resource could not be found."))
|
||||
}
|
||||
.handle {
|
||||
case corsRejection: org.apache.pekko.http.cors.scaladsl.CorsRejection =>
|
||||
complete((StatusCodes.Forbidden, "CORS rejection: Invalid origin"))
|
||||
}
|
||||
.result()
|
||||
|
||||
val apiRoutes = pathPrefix("api") {
|
||||
concat (
|
||||
path("alpr") {
|
||||
get {
|
||||
parameters("minLat".as[Double], "minLng".as[Double], "maxLat".as[Double], "maxLng".as[Double]) { (minLat, minLng, maxLat, maxLng) =>
|
||||
val bBox = services.BoundingBox(minLat, minLng, maxLat, maxLng)
|
||||
onSuccess(client.getALPRs(bBox)) { json =>
|
||||
complete(json)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
path("oauth2" / "callback") {
|
||||
get {
|
||||
parameters(Symbol("code").?) { (code) =>
|
||||
@@ -27,23 +67,20 @@ object ShotgunServer {
|
||||
}
|
||||
}
|
||||
}
|
||||
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 spaRoutes = pathSingleSlash {
|
||||
getFromFile("../webapp/dist/index.html")
|
||||
} ~ getFromDirectory("../webapp/dist")
|
||||
|
||||
val routes = handleRejections(rejectionHandler) {
|
||||
cors(corsSettings) {
|
||||
concat(apiRoutes, spaRoutes)
|
||||
}
|
||||
}
|
||||
|
||||
val bindingFuture = Http().newServerAt("localhost", 8080).bind(routes)
|
||||
val bindingFuture = Http().newServerAt("0.0.0.0", 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
|
||||
|
||||
@@ -1,25 +1,20 @@
|
||||
package services
|
||||
|
||||
import org.apache.pekko
|
||||
import pekko.actor.typed.ActorSystem
|
||||
import pekko.actor.typed.scaladsl.Behaviors
|
||||
import org.apache.pekko.actor.ActorSystem
|
||||
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 spray.json._
|
||||
import DefaultJsonProtocol._
|
||||
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) {
|
||||
class OverpassClient(implicit val system: ActorSystem, implicit val executionContext: ExecutionContextExecutor) {
|
||||
val baseUrl = "https://overpass-api.de/api/interpreter"
|
||||
|
||||
def getALPRs(bBox: BoundingBox): Future[Json] = {
|
||||
def getALPRs(bBox: BoundingBox): Future[JsValue] = {
|
||||
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(
|
||||
@@ -29,8 +24,14 @@ class OverpassClient(implicit val system: ActorSystem[_], implicit val execution
|
||||
)
|
||||
|
||||
Http().singleRequest(request).flatMap { response =>
|
||||
response.entity.toStrict(5.seconds).flatMap { entity =>
|
||||
Unmarshal(entity).to[String].map(parse(_).getOrElse(Json.Null))
|
||||
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 get ALPRs: ${response.status}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
webapp/public/adjust-angle.png
Normal file
BIN
webapp/public/adjust-angle.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 381 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 23 KiB |
BIN
webapp/public/edit-map.png
Normal file
BIN
webapp/public/edit-map.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 489 KiB |
BIN
webapp/public/paste-tags.png
Normal file
BIN
webapp/public/paste-tags.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 344 KiB |
@@ -20,16 +20,10 @@ const drawer = ref(false)
|
||||
<v-app-bar-nav-icon variant="text" @click.stop="drawer = !drawer"></v-app-bar-nav-icon>
|
||||
|
||||
<v-toolbar-title>
|
||||
<v-img height="36" width="200" src="/deflock-logo.svg" />
|
||||
<v-img height="36" width="130" src="/deflock-logo.svg" />
|
||||
</v-toolbar-title>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<template v-if="$vuetify.display.mdAndUp">
|
||||
<v-btn disabled icon="mdi-magnify" variant="text"></v-btn>
|
||||
|
||||
<v-btn disabled icon="mdi-filter" variant="text"></v-btn>
|
||||
</template>
|
||||
</v-app-bar>
|
||||
|
||||
<v-navigation-drawer
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
a {
|
||||
font-weight: bold;
|
||||
color: rgb(18, 151, 195);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
39
webapp/src/components/DFCode.vue
Normal file
39
webapp/src/components/DFCode.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div style="position: relative">
|
||||
<v-btn @click="copyToClipboard" variant="plain" flat class="copy-button">
|
||||
<v-icon>mdi-content-copy</v-icon>
|
||||
</v-btn>
|
||||
<code ref="codeContent">
|
||||
<slot></slot>
|
||||
</code>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
const codeContent = ref<HTMLElement | null>(null);
|
||||
|
||||
function copyToClipboard() {
|
||||
if (codeContent.value) {
|
||||
navigator.clipboard.writeText(codeContent.value.innerText);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
code {
|
||||
background-color: #f4f4f4;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
display: block;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.copy-button {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
}
|
||||
</style>
|
||||
@@ -1,14 +1,44 @@
|
||||
import axios from "axios";
|
||||
|
||||
export interface BoundingBox {
|
||||
export interface BoundingBoxLiteral {
|
||||
minLat: number;
|
||||
maxLat: number;
|
||||
minLng: number;
|
||||
maxLng: number;
|
||||
}
|
||||
|
||||
export class BoundingBox implements BoundingBoxLiteral {
|
||||
minLat: number;
|
||||
maxLat: number;
|
||||
minLng: number;
|
||||
maxLng: number;
|
||||
|
||||
constructor({minLat, maxLat, minLng, maxLng}: BoundingBoxLiteral) {
|
||||
this.minLat = minLat;
|
||||
this.maxLat = maxLat;
|
||||
this.minLng = minLng;
|
||||
this.maxLng = maxLng;
|
||||
}
|
||||
|
||||
updateFromOther(boundingBoxLiteral: BoundingBoxLiteral) {
|
||||
this.minLat = boundingBoxLiteral.minLat;
|
||||
this.maxLat = boundingBoxLiteral.maxLat;
|
||||
this.minLng = boundingBoxLiteral.minLng;
|
||||
this.maxLng = boundingBoxLiteral.maxLng;
|
||||
}
|
||||
|
||||
isSubsetOf(other: BoundingBoxLiteral) {
|
||||
return (
|
||||
this.minLat >= other.minLat &&
|
||||
this.maxLat <= other.maxLat &&
|
||||
this.minLng >= other.minLng &&
|
||||
this.maxLng <= other.maxLng
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const apiService = axios.create({
|
||||
baseURL: "http://localhost:8080",
|
||||
baseURL: window.location.hostname === "localhost" ? "http://localhost:8080/api" : "/api",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
|
||||
@@ -20,11 +20,14 @@
|
||||
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! More data (such as direction) coming soon.</l-popup></l-marker>
|
||||
><l-popup>
|
||||
<h2>ALPR</h2>
|
||||
<p v-if="alpr.tags.brand || alpr.tags.operator"><strong>Brand: </strong><a target="_blank" :href="`https://www.wikidata.org/wiki/${alpr.tags['brand:wikidata'] || alpr.tags['operator:wikidata']}`">{{ alpr.tags.brand || alpr.tags.operator || 'Unknown' }}</a></p>
|
||||
<p v-if="alpr.tags.direction"><strong>Faces: {{ degreesToCardinal(alpr.tags.direction) }}</strong></p>
|
||||
</l-popup></l-marker>
|
||||
</l-map>
|
||||
<div v-else>
|
||||
loading...
|
||||
@@ -38,7 +41,7 @@ import { LMap, LTileLayer, LMarker, LPopup } from '@vue-leaflet/vue-leaflet';
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { useRouter } from 'vue-router'
|
||||
import type { Ref } from 'vue';
|
||||
import type { BoundingBox } from '@/services/apiService';
|
||||
import { BoundingBox } from '@/services/apiService';
|
||||
import { getALPRs } from '@/services/apiService';
|
||||
|
||||
const zoom: Ref<number> = ref(12);
|
||||
@@ -48,46 +51,8 @@ const router = useRouter();
|
||||
|
||||
const canRefreshMarkers = computed(() => zoom.value >= 10);
|
||||
|
||||
const alprsInView: Ref<any[]> = 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"
|
||||
// }
|
||||
// }
|
||||
]);
|
||||
const alprsInView: Ref<any[]> = ref([]);
|
||||
const bboxForLastRequest: Ref<BoundingBox|null> = ref(null);
|
||||
|
||||
function getUserLocation(): Promise<[number, number]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -115,18 +80,22 @@ function mapLoaded(map: any) {
|
||||
}
|
||||
|
||||
function updateBounds(newBounds: any) {
|
||||
bounds.value = {
|
||||
updateURL();
|
||||
|
||||
const newBoundingBox = new BoundingBox({
|
||||
minLat: newBounds.getSouth(),
|
||||
maxLat: newBounds.getNorth(),
|
||||
minLng: newBounds.getWest(),
|
||||
maxLng: newBounds.getEast(),
|
||||
};
|
||||
});
|
||||
bounds.value = newBoundingBox;
|
||||
|
||||
if (bboxForLastRequest.value && newBoundingBox.isSubsetOf(bboxForLastRequest.value)) {
|
||||
console.debug('new bounds are a subset of the last request, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
updateMarkers();
|
||||
|
||||
if (center.value) {
|
||||
updateURL();
|
||||
}
|
||||
}
|
||||
|
||||
function updateURL() {
|
||||
@@ -153,14 +122,19 @@ function updateMarkers() {
|
||||
getALPRs(bounds.value)
|
||||
.then((alprs: any) => {
|
||||
alprsInView.value = alprs.elements;
|
||||
bboxForLastRequest.value = bounds.value;
|
||||
});
|
||||
}
|
||||
|
||||
function degreesToCardinal(degrees: number): string {
|
||||
const cardinals = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'];
|
||||
return cardinals[Math.round(degrees / 45) % 8];
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -168,8 +142,6 @@ onMounted(() => {
|
||||
lat: parseFloat(parts[1]),
|
||||
lng: parseFloat(parts[2]),
|
||||
};
|
||||
console.log('center', center.value);
|
||||
console.log('zoom', zoom.value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,9 +168,9 @@ onMounted(() => {
|
||||
left: 32px;
|
||||
width: calc(100% - 64px);
|
||||
z-index: 1000;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
border-radius: 4px;
|
||||
padding: 4px;
|
||||
color: #333;
|
||||
color: #eee;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,33 +2,85 @@
|
||||
<v-container>
|
||||
<h2>How to Report an ALPR</h2>
|
||||
<p>
|
||||
If you've spotted an ALPR in your area, you can help us track it by reporting it to our database. Here's how you can do it:
|
||||
If you've spotted an ALPR in your area, you can help us track it by reporting it to OpenStreetMap, where we source our information. Here's how you can do it:
|
||||
</p>
|
||||
|
||||
<h3>Coming Soon</h3>
|
||||
<p>
|
||||
We're working on a way for you to report ALPRs directly from this site. Check back soon for updates!
|
||||
</p>
|
||||
<p>
|
||||
Until then, you can report them on OpenStreetMap. <a href="https://wiki.openstreetmap.org/wiki/Tag:man_made%3Dsurveillance" target="_blank">Learn how to add ALPRs to OpenStreetMap</a>.
|
||||
</p>
|
||||
<v-alert
|
||||
variant="tonal"
|
||||
type="warning"
|
||||
class="my-6"
|
||||
title="Are you sure it's an ALPR?"
|
||||
>
|
||||
<p>
|
||||
Before you report an ALPR, please read our <router-link style="color: unset !important" to="/what-is-an-alpr">guide on what ALPRs look like</router-link> to make sure you're reporting the right thing.
|
||||
</p>
|
||||
</v-alert>
|
||||
|
||||
<!-- <p>
|
||||
If you can do so safely, take a picture of the ALPR camera. Make sure the camera is visible in the image, and try to capture any identifying information, such as the brand name or any logos.
|
||||
</p>
|
||||
<div class="ml-4 mt-4">
|
||||
<h3>1. Create an OpenStreetMap Account</h3>
|
||||
<p>
|
||||
<a href="https://www.openstreetmap.org/user/new" target="_blank">Sign up for an OpenStreetMap account</a> in order to submit changes.
|
||||
</p>
|
||||
|
||||
<h3>Step 2: Note the Location</h3>
|
||||
<p>
|
||||
Record the location of the ALPR camera. You can use your phone's GPS to get the coordinates, or note the nearest address or intersection.
|
||||
</p>
|
||||
<h3>2. Find the ALPR's Location on OpenStreetMap</h3>
|
||||
<p>
|
||||
<a href="https://www.openstreetmap.org" target="_blank">Launch OpenStreetMap</a> and search for the location of the ALPR. You can use the search bar at the top of the page to find the location.
|
||||
</p>
|
||||
|
||||
<h3>Step 3: Submit the Report</h3>
|
||||
<p>
|
||||
Click the "Report" button below to submit your report. You'll be asked to upload the picture you took and provide the location information. Your report will help us build a more comprehensive database of ALPRs in the United States.
|
||||
</p> -->
|
||||
<h3>3. Add the ALPR to OpenStreetMap</h3>
|
||||
<p>
|
||||
Once you've found the location of the ALPR, click the <strong>Edit</strong> button in the top left corner of the page. This will open the OpenStreetMap editor, where you can add the ALPR to the map.
|
||||
</p>
|
||||
|
||||
<v-img src="/edit-map.png" />
|
||||
|
||||
<p>
|
||||
To add the ALPR, click the <strong>Point</strong> button in the top left corner of the editor, then click on the location of the ALPR on the map. In the popup that appears, paste the following tags:
|
||||
</p>
|
||||
|
||||
<DFCode>
|
||||
man_made=surveillance<br>
|
||||
surveillance:type=ALPR<br>
|
||||
camera:mount=pole<br>
|
||||
camera:type=fixed<br>
|
||||
surveillance=traffic<br>
|
||||
surveillance:zone=traffic<br>
|
||||
</DFCode>
|
||||
|
||||
<v-img class="my-4" src="/paste-tags.png" />
|
||||
|
||||
<p>
|
||||
If you've identified the brand of the ALPR as Flock Safety, then you can also add the following tags:
|
||||
</p>
|
||||
<DFCode>
|
||||
operator=Flock Safety<br>
|
||||
operator:wikidata=Q108485435<br>
|
||||
brand=Flock Safety<br>
|
||||
brand:wikidata=Q108485435<br>
|
||||
</DFCode>
|
||||
|
||||
<h3>4. Adjust the Direction</h3>
|
||||
|
||||
<v-img src="/adjust-angle.png" />
|
||||
|
||||
<p>
|
||||
If you know the direction that the ALPR is facing, you can use the up and down arrows to set the direction it faces.
|
||||
</p>
|
||||
|
||||
<h3>5. Submit Your Changes</h3>
|
||||
<p>
|
||||
Once you've added the ALPR to the map, click the <strong>Save</strong> button in the top left corner of the editor. You'll be asked to provide a brief description of your changes. Once you've submitted your changes, the ALPR will be added to OpenStreetMap.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import DFCode from '@/components/DFCode.vue';
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* TODO: put this all in one place, also in what-is view */
|
||||
h2 {
|
||||
@@ -50,16 +102,11 @@ p {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--v-primary-base);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: var(--v-primary-base);
|
||||
code {
|
||||
background-color: #f4f4f4;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
display: block;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user