mirror of
https://github.com/FoggedLens/deflock.git
synced 2026-02-12 15:02:45 +00:00
a lot of refactoring and cleaning up
This commit is contained in:
@@ -57,9 +57,9 @@ watch(() => theme.global.name.value, (newTheme) => {
|
|||||||
|
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
|
|
||||||
<v-btn icon>
|
<!-- <v-btn icon>
|
||||||
<v-icon @click="toggleTheme">mdi-theme-light-dark</v-icon>
|
<v-icon @click="toggleTheme">mdi-theme-light-dark</v-icon>
|
||||||
</v-btn>
|
</v-btn> -->
|
||||||
</v-app-bar>
|
</v-app-bar>
|
||||||
|
|
||||||
<v-navigation-drawer
|
<v-navigation-drawer
|
||||||
|
|||||||
@@ -31,3 +31,8 @@ p {
|
|||||||
.serif {
|
.serif {
|
||||||
font-family: "PT Serif", serif;
|
font-family: "PT Serif", serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Prevent Scrolling Horizontally */
|
||||||
|
html, body {
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 0.5em 0;
|
margin: 0.8em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul, ol {
|
ul, ol {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="counter">
|
<div class="counter">
|
||||||
<span ref="counterEl" class="font-weight-bold">0</span>
|
<span :class="{ mobile: isMobile }" ref="counterEl" class="font-weight-bold">0</span>
|
||||||
<span class="caption"> ALPRs Reported Worldwide</span>
|
<span class="caption"> ALPRs Reported Worldwide</span>
|
||||||
<div :class="{ 'fade-in': showFinalAnimation }" class="subheading fade-text">and rapidly growing!</div>
|
<div :class="{ 'fade-in': showFinalAnimation }" class="subheading fade-text">and rapidly growing!</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref, watch, type Ref } from 'vue';
|
import { onMounted, ref, watch, type Ref } from 'vue';
|
||||||
|
import { useDisplay } from 'vuetify'
|
||||||
import { getALPRCounts } from '@/services/apiService';
|
import { getALPRCounts } from '@/services/apiService';
|
||||||
import { CountUp } from 'countup.js';
|
import { CountUp } from 'countup.js';
|
||||||
|
|
||||||
@@ -30,6 +31,7 @@ const counts: Ref<Counts> = ref({
|
|||||||
worldwide: undefined,
|
worldwide: undefined,
|
||||||
});
|
});
|
||||||
const showFinalAnimation = ref(false);
|
const showFinalAnimation = ref(false);
|
||||||
|
const { xs: isMobile } = useDisplay();
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getALPRCounts().then((response) => {
|
getALPRCounts().then((response) => {
|
||||||
@@ -74,4 +76,9 @@ watch(counts, (newCounts: Counts) => {
|
|||||||
.fade-in {
|
.fade-in {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mobile {
|
||||||
|
display: block;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<v-icon start>mdi-compass-outline</v-icon> <b>{{ cardinalDirection }}</b>
|
<v-icon start>mdi-compass-outline</v-icon> <b>{{ cardinalDirection }}</b>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
<v-list-item>
|
<v-list-item>
|
||||||
<v-icon start>mdi-factory</v-icon> <b>
|
<v-icon start>mdi-domain</v-icon> <b>
|
||||||
<span v-if="alpr.tags.manufacturer">
|
<span v-if="alpr.tags.manufacturer">
|
||||||
{{ alpr.tags.manufacturer }}
|
{{ alpr.tags.manufacturer }}
|
||||||
</span>
|
</span>
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
</b>
|
</b>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
<v-list-item>
|
<v-list-item>
|
||||||
<v-icon start>mdi-police-badge</v-icon> <b>
|
<v-icon start>mdi-account-tie</v-icon> <b>
|
||||||
<span v-if="alpr.tags.operator">
|
<span v-if="alpr.tags.operator">
|
||||||
{{ alpr.tags.operator }}
|
{{ alpr.tags.operator }}
|
||||||
</span>
|
</span>
|
||||||
@@ -39,7 +39,12 @@
|
|||||||
</v-list>
|
</v-list>
|
||||||
|
|
||||||
<div class="text-center text-grey-darken-1">
|
<div class="text-center text-grey-darken-1">
|
||||||
node/{{ alpr.id }}
|
|
||||||
|
<v-tooltip open-delay="500" text="OSM Node ID" location="bottom">
|
||||||
|
<template #activator="{ props }">
|
||||||
|
<span style="cursor: default" v-bind="props">node/{{ alpr.id }}</span>
|
||||||
|
</template>
|
||||||
|
</v-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</v-sheet>
|
</v-sheet>
|
||||||
</template>
|
</template>
|
||||||
@@ -48,7 +53,7 @@
|
|||||||
import { defineProps, computed } from 'vue';
|
import { defineProps, computed } from 'vue';
|
||||||
import type { PropType } from 'vue';
|
import type { PropType } from 'vue';
|
||||||
import type { ALPR } from '@/types';
|
import type { ALPR } from '@/types';
|
||||||
import { VIcon, VList, VSheet, VListItem, VChip } from 'vuetify/components';
|
import { VIcon, VList, VSheet, VListItem, VTooltip } from 'vuetify/components';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
alpr: {
|
alpr: {
|
||||||
@@ -60,11 +65,11 @@ const props = defineProps({
|
|||||||
const isFaceRecognition = computed(() => props.alpr.tags.brand === 'Avigilon');
|
const isFaceRecognition = computed(() => props.alpr.tags.brand === 'Avigilon');
|
||||||
|
|
||||||
const cardinalDirection = computed(() =>
|
const cardinalDirection = computed(() =>
|
||||||
props.alpr.tags.direction === undefined ? 'Unknown' : degreesToCardinal(parseInt(props.alpr.tags.direction))
|
props.alpr.tags.direction === undefined ? 'Unknown Direction' : degreesToCardinal(parseInt(props.alpr.tags.direction))
|
||||||
);
|
);
|
||||||
|
|
||||||
function degreesToCardinal(degrees: number): string {
|
function degreesToCardinal(degrees: number): string {
|
||||||
const cardinals = ['North', 'Northeast', 'East', 'Southeast', 'South', 'Southwest', 'West', 'Northwest'];
|
const cardinals = ['North', 'Northeast', 'East', 'Southeast', 'South', 'Southwest', 'West', 'Northwest'];
|
||||||
return cardinals[Math.round(degrees / 45) % 8];
|
return 'Faces ' + cardinals[Math.round(degrees / 45) % 8];
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -11,23 +11,25 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, h, createApp, watch, ref, type PropType, type Ref } from 'vue';
|
import { onBeforeUnmount, onMounted, h, createApp, watch, ref, type PropType, type Ref } from 'vue';
|
||||||
import L, { type LatLngExpression } from 'leaflet';
|
import L, { type LatLngExpression, type FeatureGroup, type MarkerClusterGroup, type Marker, type CircleMarker } from 'leaflet';
|
||||||
import type { ALPR } from '@/types';
|
import type { ALPR } from '@/types';
|
||||||
|
|
||||||
import DFMapPopup from './DFMapPopup.vue';
|
import DFMapPopup from './DFMapPopup.vue';
|
||||||
|
import { createVuetify } from 'vuetify'
|
||||||
import 'leaflet/dist/leaflet.css';
|
import 'leaflet/dist/leaflet.css';
|
||||||
import 'leaflet.markercluster';
|
import 'leaflet.markercluster';
|
||||||
import 'leaflet.markercluster/dist/MarkerCluster.Default.css';
|
import 'leaflet.markercluster/dist/MarkerCluster.Default.css';
|
||||||
import 'leaflet.markercluster/dist/MarkerCluster.css';
|
import 'leaflet.markercluster/dist/MarkerCluster.css';
|
||||||
import { createVuetify } from 'vuetify'
|
|
||||||
|
|
||||||
// Color of Marker Circle
|
|
||||||
const MARKER_COLOR = 'rgb(63,84,243)';
|
const MARKER_COLOR = 'rgb(63,84,243)';
|
||||||
|
|
||||||
|
// Internal State Management
|
||||||
|
const markerMap = new Map<string, Marker | CircleMarker>();
|
||||||
|
const isInternalUpdate = ref(false);
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
center: {
|
center: {
|
||||||
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
zoom: {
|
zoom: {
|
||||||
@@ -44,59 +46,22 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:center', 'update:zoom', 'update:bounds']);
|
||||||
|
|
||||||
|
// Map instance and layers
|
||||||
let map: L.Map;
|
let map: L.Map;
|
||||||
let circlesLayer: L.FeatureGroup;
|
let circlesLayer: FeatureGroup;
|
||||||
let clusterLayer: L.MarkerClusterGroup;
|
let clusterLayer: MarkerClusterGroup;
|
||||||
let currentLocationLayer: L.FeatureGroup;
|
let currentLocationLayer: FeatureGroup;
|
||||||
|
|
||||||
onMounted(() => {
|
// Marker Creation Utilities
|
||||||
initializeMap();
|
function createSVGMarker(alpr: ALPR): string {
|
||||||
});
|
|
||||||
|
|
||||||
function initializeMap() {
|
|
||||||
map = L.map('map', { zoomControl: false, maxZoom: 18 /* max for OSM tiles */ }).setView(props.center, props.zoom);
|
|
||||||
|
|
||||||
registerEvents();
|
|
||||||
registerWatchers();
|
|
||||||
|
|
||||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
||||||
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
|
||||||
}).addTo(map);
|
|
||||||
|
|
||||||
emit('update:bounds', map.getBounds()); // XXX: this event populates the map
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderCurrentLocation() {
|
|
||||||
if (!props.currentLocation)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (currentLocationLayer) {
|
|
||||||
map.removeLayer(currentLocationLayer);
|
|
||||||
}
|
|
||||||
|
|
||||||
currentLocationLayer = L.featureGroup();
|
|
||||||
const clMarker = L.circleMarker([props.currentLocation[0], props.currentLocation[1]], {
|
|
||||||
radius: 10,
|
|
||||||
color: '#ffffff',
|
|
||||||
fillColor: '#007bff',
|
|
||||||
fillOpacity: 1,
|
|
||||||
weight: 4
|
|
||||||
});
|
|
||||||
|
|
||||||
clMarker.bindPopup('Current Location');
|
|
||||||
|
|
||||||
currentLocationLayer.addLayer(clMarker);
|
|
||||||
map.addLayer(currentLocationLayer);
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeSVGMarker(alpr: ALPR): L.Marker {
|
|
||||||
const { lat, lon: lng } = alpr;
|
|
||||||
const orientationDegrees = alpr.tags.direction;
|
const orientationDegrees = alpr.tags.direction;
|
||||||
const fovPath = `
|
const fovPath = `
|
||||||
<path class="someSVGpath" d="M215.248,221.461L99.696,43.732C144.935,16.031 198.536,0 256,0C313.464,0 367.065,16.031 412.304,43.732L296.752,221.461C287.138,209.593 272.448,202.001 256,202.001C239.552,202.001 224.862,209.593 215.248,221.461Z" style="fill:rgb(87,87,87);fill-opacity:0.46;"/>
|
<path class="someSVGpath" d="M215.248,221.461L99.696,43.732C144.935,16.031 198.536,0 256,0C313.464,0 367.065,16.031 412.304,43.732L296.752,221.461C287.138,209.593 272.448,202.001 256,202.001C239.552,202.001 224.862,209.593 215.248,221.461Z" style="fill:rgb(87,87,87);fill-opacity:0.46;"/>
|
||||||
<path class="someSVGpath" d="M215.248,221.461L99.696,43.732C144.935,16.031 198.536,0 256,0C313.464,0 367.065,16.031 412.304,43.732L296.752,221.461C287.138,209.593 272.448,202.001 256,202.001C239.552,202.001 224.862,209.593 215.248,221.461ZM217.92,200.242C228.694,192.652 241.831,188.195 256,188.195C270.169,188.195 283.306,192.652 294.08,200.242C294.08,200.242 392.803,48.4 392.803,48.4C352.363,26.364 305.694,13.806 256,13.806C206.306,13.806 159.637,26.364 119.197,48.4L217.92,200.242Z" style="fill:rgb(137,135,135);"/>
|
<path class="someSVGpath" d="M215.248,221.461L99.696,43.732C144.935,16.031 198.536,0 256,0C313.464,0 367.065,16.031 412.304,43.732L296.752,221.461C287.138,209.593 272.448,202.001 256,202.001C239.552,202.001 224.862,209.593 215.248,221.461ZM217.92,200.242C228.694,192.652 241.831,188.195 256,188.195C270.169,188.195 283.306,192.652 294.08,200.242C294.08,200.242 392.803,48.4 392.803,48.4C352.363,26.364 305.694,13.806 256,13.806C206.306,13.806 159.637,26.364 119.197,48.4L217.92,200.242Z" style="fill:rgb(137,135,135);"/>
|
||||||
`;
|
`;
|
||||||
const svgMarker = `
|
return `
|
||||||
<svg style="transform:rotate(${orientationDegrees}deg)" class="svgMarker" width="100%" height="100%" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
<svg style="transform:rotate(${orientationDegrees}deg)" class="svgMarker" width="100%" height="100%" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
${orientationDegrees ? fovPath : ''}
|
${orientationDegrees ? fovPath : ''}
|
||||||
<g transform="matrix(0.906623,0,0,0.906623,23.9045,22.3271)">
|
<g transform="matrix(0.906623,0,0,0.906623,23.9045,22.3271)">
|
||||||
@@ -105,31 +70,21 @@ function makeSVGMarker(alpr: ALPR): L.Marker {
|
|||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const el = document.createElement('div');
|
|
||||||
el.innerHTML = svgMarker;
|
|
||||||
el.style.width = '50px';
|
|
||||||
el.style.height = '50px';
|
|
||||||
|
|
||||||
const icon = L.divIcon({
|
|
||||||
className: 'leaflet-data-marker',
|
|
||||||
html: svgMarker,
|
|
||||||
iconSize: [60, 60],
|
|
||||||
iconAnchor: [30, 30],
|
|
||||||
popupAnchor: [0, 0],
|
|
||||||
});
|
|
||||||
|
|
||||||
const marker = L.marker([lat, lng], { icon: icon });
|
|
||||||
const markerWithPopup = bindPopup(marker, alpr);
|
|
||||||
|
|
||||||
return markerWithPopup as L.Marker;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeCircleMarker(alpr: ALPR): L.CircleMarker {
|
function createMarker(alpr: ALPR): Marker | CircleMarker {
|
||||||
const { lat, lon: lng } = alpr;
|
if (hasPlottableOrientation(alpr.tags.direction)) {
|
||||||
const orientationDegrees = alpr.tags.direction;
|
const icon = L.divIcon({
|
||||||
|
className: 'leaflet-data-marker',
|
||||||
|
html: createSVGMarker(alpr),
|
||||||
|
iconSize: [60, 60],
|
||||||
|
iconAnchor: [30, 30],
|
||||||
|
popupAnchor: [0, 0],
|
||||||
|
});
|
||||||
|
return L.marker([alpr.lat, alpr.lon], { icon });
|
||||||
|
}
|
||||||
|
|
||||||
const marker = L.circleMarker([lat, lng], {
|
return L.circleMarker([alpr.lat, alpr.lon], {
|
||||||
fill: true,
|
fill: true,
|
||||||
fillColor: MARKER_COLOR,
|
fillColor: MARKER_COLOR,
|
||||||
fillOpacity: 0.6,
|
fillOpacity: 0.6,
|
||||||
@@ -139,9 +94,6 @@ function makeCircleMarker(alpr: ALPR): L.CircleMarker {
|
|||||||
radius: 8,
|
radius: 8,
|
||||||
weight: 3,
|
weight: 3,
|
||||||
});
|
});
|
||||||
|
|
||||||
const markerWithPopup = bindPopup(marker, alpr);
|
|
||||||
return markerWithPopup as L.CircleMarker;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function bindPopup(marker: L.CircleMarker | L.Marker, alpr: ALPR): L.CircleMarker | L.Marker {
|
function bindPopup(marker: L.CircleMarker | L.Marker, alpr: ALPR): L.CircleMarker | L.Marker {
|
||||||
@@ -151,13 +103,15 @@ function bindPopup(marker: L.CircleMarker | L.Marker, alpr: ALPR): L.CircleMarke
|
|||||||
const popupContent = document.createElement('div');
|
const popupContent = document.createElement('div');
|
||||||
createApp({
|
createApp({
|
||||||
render() {
|
render() {
|
||||||
return h(DFMapPopup, { alpr: {
|
return h(DFMapPopup, {
|
||||||
id: alpr.id,
|
alpr: {
|
||||||
lat: alpr.lat,
|
id: alpr.id,
|
||||||
lon: alpr.lon,
|
lat: alpr.lat,
|
||||||
tags: alpr.tags,
|
lon: alpr.lon,
|
||||||
type: alpr.type,
|
tags: alpr.tags,
|
||||||
} });
|
type: alpr.type,
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}).use(createVuetify()).mount(popupContent);
|
}).use(createVuetify()).mount(popupContent);
|
||||||
|
|
||||||
@@ -172,72 +126,124 @@ function hasPlottableOrientation(orientationDegrees: string) {
|
|||||||
return orientationDegrees && !isNaN(parseInt(orientationDegrees));
|
return orientationDegrees && !isNaN(parseInt(orientationDegrees));
|
||||||
}
|
}
|
||||||
|
|
||||||
function populateMap() {
|
// Map State Management
|
||||||
const showFov = props.zoom >= 16;
|
function initializeMap() {
|
||||||
|
map = L.map('map', {
|
||||||
|
zoomControl: false,
|
||||||
|
maxZoom: 18, // max for OSM tiles
|
||||||
|
minZoom: 3, // don't overload the browser
|
||||||
|
}).setView(props.center, props.zoom);
|
||||||
|
|
||||||
renderCurrentLocation();
|
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
||||||
if (clusterLayer) {
|
}).addTo(map);
|
||||||
map.removeLayer(clusterLayer);
|
|
||||||
}
|
|
||||||
|
|
||||||
clusterLayer = L.markerClusterGroup({
|
clusterLayer = L.markerClusterGroup({
|
||||||
chunkedLoading: true,
|
chunkedLoading: true,
|
||||||
disableClusteringAtZoom: 16, // showFov threshold
|
disableClusteringAtZoom: 16,
|
||||||
removeOutsideVisibleBounds: true,
|
removeOutsideVisibleBounds: true,
|
||||||
maxClusterRadius: 60,
|
maxClusterRadius: 60,
|
||||||
zoomToBoundsOnClick: true,
|
|
||||||
spiderfyOnEveryZoom: false,
|
spiderfyOnEveryZoom: false,
|
||||||
spiderfyOnMaxZoom: false,
|
spiderfyOnMaxZoom: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
circlesLayer = L.featureGroup();
|
circlesLayer = L.featureGroup();
|
||||||
|
currentLocationLayer = L.featureGroup();
|
||||||
|
|
||||||
for (const alpr of props.alprs) {
|
map.addLayer(clusterLayer);
|
||||||
const orientationDegrees = alpr.tags.direction;
|
registerMapEvents();
|
||||||
|
|
||||||
let marker: L.CircleMarker | L.Marker;
|
if (props.alprs.length) {
|
||||||
|
updateMarkers(props.alprs);
|
||||||
|
} else {
|
||||||
|
emit('update:bounds', map.getBounds());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (hasPlottableOrientation(orientationDegrees)) {
|
function updateMarkers(newAlprs: ALPR[]): void {
|
||||||
marker = makeSVGMarker(alpr);
|
const currentIds = new Set(markerMap.keys());
|
||||||
} else {
|
const nonexistingAlprs = newAlprs.filter(alpr => !currentIds.has(alpr.id));
|
||||||
marker = makeCircleMarker(alpr);
|
|
||||||
|
// Add markers
|
||||||
|
for (const alpr of nonexistingAlprs) {
|
||||||
|
if (!currentIds.has(alpr.id)) {
|
||||||
|
// Add new marker
|
||||||
|
const marker = createMarker(alpr);
|
||||||
|
bindPopup(marker, alpr);
|
||||||
|
markerMap.set(alpr.id, marker);
|
||||||
|
circlesLayer.addLayer(marker);
|
||||||
}
|
}
|
||||||
|
|
||||||
circlesLayer.addLayer(marker);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update cluster layer
|
||||||
|
clusterLayer.clearLayers();
|
||||||
clusterLayer.addLayer(circlesLayer);
|
clusterLayer.addLayer(circlesLayer);
|
||||||
map.addLayer(clusterLayer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const emit = defineEmits(['update:center', 'update:zoom', 'update:bounds']);
|
function updateCurrentLocation(): void {
|
||||||
|
currentLocationLayer.clearLayers();
|
||||||
|
|
||||||
function registerEvents() {
|
if (props.currentLocation) {
|
||||||
map.on('moveend', () => {
|
const marker = L.circleMarker([props.currentLocation[0], props.currentLocation[1]], {
|
||||||
emit('update:center', map.getCenter());
|
radius: 10,
|
||||||
emit('update:zoom', map.getZoom());
|
color: '#ffffff',
|
||||||
emit('update:bounds', map.getBounds());
|
fillColor: '#007bff',
|
||||||
});
|
fillOpacity: 1,
|
||||||
|
weight: 4
|
||||||
|
}).bindPopup('Current Location');
|
||||||
|
|
||||||
|
currentLocationLayer.addLayer(marker);
|
||||||
|
map.addLayer(currentLocationLayer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerWatchers() {
|
// Lifecycle Hooks
|
||||||
watch(() => props.center, (newCenter: any, oldCenter: any) => {
|
onMounted(() => {
|
||||||
if (newCenter.lat !== oldCenter.lat || newCenter.lng !== oldCenter.lng) {
|
initializeMap();
|
||||||
map.setView(newCenter as LatLngExpression);
|
|
||||||
|
// Watch for prop changes
|
||||||
|
watch(() => props.center, (newCenter: any) => {
|
||||||
|
if (!isInternalUpdate.value) {
|
||||||
|
isInternalUpdate.value = true;
|
||||||
|
map.setView(newCenter, map.getZoom(), { animate: false });
|
||||||
|
setTimeout(() => {
|
||||||
|
isInternalUpdate.value = false;
|
||||||
|
}, 0);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(() => props.currentLocation, (newLocation, oldLocation) => {
|
watch(() => props.zoom, (newZoom: number) => {
|
||||||
console.log("current location watcher triggered!");
|
if (!isInternalUpdate.value) {
|
||||||
renderCurrentLocation();
|
isInternalUpdate.value = true;
|
||||||
|
map.setZoom(newZoom);
|
||||||
|
setTimeout(() => {
|
||||||
|
isInternalUpdate.value = false;
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(() => props.alprs, (newAlprs, oldAlprs) => {
|
watch(() => props.alprs, (newAlprs) => {
|
||||||
populateMap();
|
updateMarkers(newAlprs);
|
||||||
});
|
}, { deep: true });
|
||||||
|
|
||||||
|
watch(() => props.currentLocation, () => {
|
||||||
|
updateCurrentLocation();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
map?.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
function registerMapEvents() {
|
||||||
|
map.on('moveend', () => {
|
||||||
|
if (!isInternalUpdate.value) {
|
||||||
|
emit('update:center', map.getCenter());
|
||||||
|
emit('update:zoom', map.getZoom());
|
||||||
|
emit('update:bounds', map.getBounds());
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
:items="alprBrands"
|
:items="alprBrands"
|
||||||
item-title="nickname"
|
item-title="nickname"
|
||||||
return-object
|
return-object
|
||||||
label="Manufacturer"
|
label="Choose a Manufacturer"
|
||||||
variant="solo-filled"
|
variant="outlined"
|
||||||
flat
|
flat
|
||||||
hide-details
|
hide-details
|
||||||
></v-select>
|
></v-select>
|
||||||
|
|||||||
@@ -57,6 +57,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<p>© {{ currentYear }} DeFlock. All Rights Reserved</p>
|
<p>© {{ currentYear }} DeFlock. All Rights Reserved</p>
|
||||||
<p>Map data © <a href="https://www.openstreetmap.org/copyright" target="_blank" style="color: unset; font-weight: normal;">OpenStreetMap contributors</a></p>
|
<p>Map data © <a href="https://www.openstreetmap.org/copyright" target="_blank" style="color: unset; font-weight: normal;">OpenStreetMap contributors</a></p>
|
||||||
|
<p class="mt-4">v1.0</p>
|
||||||
</div>
|
</div>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ const router = createRouter({
|
|||||||
{
|
{
|
||||||
path: '/qr',
|
path: '/qr',
|
||||||
name: 'qr-landing',
|
name: 'qr-landing',
|
||||||
component: () => import('../views/QRLandingView.vue'),
|
component: () => import('../views/Landing.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: 'You Found an ALPR | DeFlock'
|
title: 'You Found an ALPR | DeFlock'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<v-col cols="12" md="10">
|
<v-col cols="12" md="10">
|
||||||
<h2 class="mb-2">Our Amazing Sponsors</h2>
|
<h2 class="mb-2">Our Amazing Sponsors</h2>
|
||||||
<p class="mb-8">
|
<p class="mb-8">
|
||||||
Want to see your name here? <a href="https://github.com/sponsors/frillweeman">Become a sponsor</a>, and your name will appear on this page!
|
Want to see your name here? <a target="_blank" href="https://github.com/sponsors/frillweeman">Become a sponsor</a>, and your name will appear on this page!
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<v-row>
|
<v-row>
|
||||||
|
|||||||
@@ -3,25 +3,24 @@
|
|||||||
<v-container fluid class="hero-section">
|
<v-container fluid class="hero-section">
|
||||||
<v-row justify="center">
|
<v-row justify="center">
|
||||||
<v-col cols="12" md="8" class="text-center">
|
<v-col cols="12" md="8" class="text-center">
|
||||||
<h1 class="display-1 px-8">You're Being Tracked by ALPRs!</h1>
|
<h1 class="display-1 px-8">You're Being Tracked</h1>
|
||||||
<ALPRCounter class="mt-4" />
|
|
||||||
<p class="subtitle-1 px-8 mt-6 mb-12 bigger">
|
<ALPRCounter class="my-6" />
|
||||||
Automated License Plate Readers (ALPRs) are monitoring your every move. Learn more about how they work and how you can protect your privacy.
|
|
||||||
</p>
|
|
||||||
<v-btn color="rgb(18, 151, 195)" large @click="goToMap({ withCurrentLocation: true })">
|
|
||||||
Find Nearby ALPRs
|
|
||||||
<v-icon end>mdi-map</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
|
|
||||||
<!-- Featured On Section -->
|
<!-- Featured On Section -->
|
||||||
<v-container class="featured-on-section mt-10">
|
<v-container class="featured-on-section my-8">
|
||||||
<h4 class="mb-8" style="opacity: 0.8">Featured On</h4>
|
<h4 class="mb-4" style="opacity: 0.8">Featured On</h4>
|
||||||
<v-row justify="center" align-items="center">
|
<v-row justify="center" align-items="center">
|
||||||
<v-card flat v-for="site in featuredOn" class="mx-4" :width="site.wide ? 200 : 100" height="50" :href="site.url" target="_blank" style="background: rgba(0,0,0,0)">
|
<v-card flat v-for="site in featuredOn" class="mx-4" :width="site.wide ? 200 : 100" height="50" style="background: rgba(0,0,0,0)">
|
||||||
<v-img contain :src="site.logo" :alt="site.name" class="featured-logo" style="display: flex; align-items: center; height: 100%;" />
|
<v-img contain :src="site.logo" :alt="site.name" class="featured-logo" style="display: flex; align-items: center; height: 100%;" />
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-container>
|
</v-container>
|
||||||
|
|
||||||
|
<v-btn size="large" color="rgb(18, 151, 195)" large @click="goToMap({ withCurrentLocation: true })">
|
||||||
|
Explore the Map
|
||||||
|
<v-icon end>mdi-map</v-icon>
|
||||||
|
</v-btn>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-container>
|
</v-container>
|
||||||
@@ -30,14 +29,14 @@
|
|||||||
<v-container class="py-10 text-center info-section">
|
<v-container class="py-10 text-center info-section">
|
||||||
|
|
||||||
<h2 class="mb-4">What is an ALPR</h2>
|
<h2 class="mb-4">What is an ALPR</h2>
|
||||||
<p class="text-left">
|
<p class="text-left px-6">
|
||||||
Automated License Plate Readers (ALPRs) are cameras that capture images of all passing license plates, storing details like the car's location, date, and time. These cameras collect data on millions of vehicles—regardless of whether the driver is suspected of a crime. While these systems can be useful for tracking stolen cars or wanted individuals, they are mostly used to track the movements of innocent people.
|
Automated License Plate Readers (ALPRs) are cameras that capture images of all passing license plates, storing details like the car's location, date, and time. These cameras collect data on millions of vehicles—regardless of whether the driver is suspected of a crime. While these systems can be useful for tracking stolen cars or wanted individuals, they are mostly used to track the movements of innocent people.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<v-divider class="my-8" />
|
<v-divider class="my-8" />
|
||||||
|
|
||||||
<h2 class="display-2 mb-4">The Dangers of ALPRs</h2>
|
<h2 class="display-2 mb-4">The Dangers of ALPRs</h2>
|
||||||
<p class="subtitle-1 px-8">
|
<p class="px-6">
|
||||||
ALPRs are a threat to your privacy and civil liberties. They can be used to track your movements and profile you, and even stalk you. Learn more about the dangers of ALPRs and how you can protect yourself.
|
ALPRs are a threat to your privacy and civil liberties. They can be used to track your movements and profile you, and even stalk you. Learn more about the dangers of ALPRs and how you can protect yourself.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -49,7 +48,7 @@
|
|||||||
Privacy Violations
|
Privacy Violations
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
ALPRs track your movements and store your data for long periods of time, creating a detailed record of your location history. They surveil mostly innocent people while claiming to target criminals.
|
ALPRs track your movements and store your data for long periods of time, creating a detailed record of your location history.
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
@@ -100,7 +99,7 @@
|
|||||||
background: url('/flock-camera.jpeg') no-repeat center center;
|
background: url('/flock-camera.jpeg') no-repeat center center;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
color: white;
|
color: white;
|
||||||
padding: 100px 0 50px 0 !important;
|
padding: 60px 0 50px 0 !important;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +110,7 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: rgba(0, 0, 0, 0.5);
|
background: rgba(0, 0, 0, 0.55);
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,7 +120,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.map-section {
|
.map-section {
|
||||||
background: url('/deflock-screenshot.webp') no-repeat right center;
|
background: url('/deflock-screenshot.webp') no-repeat center center;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
color: white;
|
color: white;
|
||||||
padding: 100px 0;
|
padding: 100px 0;
|
||||||
|
|||||||
@@ -112,7 +112,9 @@ function goToUserLocation() {
|
|||||||
setCurrentLocation()
|
setCurrentLocation()
|
||||||
.then((cl) => {
|
.then((cl) => {
|
||||||
center.value = cl;
|
center.value = cl;
|
||||||
zoom.value = DEFAULT_ZOOM;
|
setTimeout(() => {
|
||||||
|
zoom.value = DEFAULT_ZOOM;
|
||||||
|
}, 10);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.debug('Error getting user location.', error);
|
console.debug('Error getting user location.', error);
|
||||||
|
|||||||
@@ -1,162 +0,0 @@
|
|||||||
<template>
|
|
||||||
<!-- Hero Section -->
|
|
||||||
<v-container fluid class="hero-section">
|
|
||||||
<v-row justify="center">
|
|
||||||
<v-col cols="12" md="8" class="text-center">
|
|
||||||
<h1 class="display-1 px-8">You're Being Tracked by an ALPR!</h1>
|
|
||||||
<ALPRCounter class="mt-4" />
|
|
||||||
<p class="subtitle-1 px-8 mt-6 mb-12 bigger">
|
|
||||||
Automated License Plate Readers (ALPRs) are monitoring your every move. Learn more about how they work and how you can protect your privacy.
|
|
||||||
</p>
|
|
||||||
<v-btn color="rgb(18, 151, 195)" large @click="goToMap({ withCurrentLocation: true })">
|
|
||||||
Find the Nearest ALPR
|
|
||||||
<v-icon end>mdi-map</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
</v-container>
|
|
||||||
|
|
||||||
<!-- Dangers Section -->
|
|
||||||
<v-container class="py-10 text-center info-section">
|
|
||||||
<h2 class="display-2 mb-4">The Dangers of ALPRs</h2>
|
|
||||||
<p class="subtitle-1 px-8">
|
|
||||||
ALPRs are a threat to your privacy and civil liberties. They can be used to track your movements, profile you, and even stalk you. Learn more about the dangers of ALPRs and how you can protect yourself.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<v-row class="align-center mt-4">
|
|
||||||
<v-col cols="12" md="4" class="text-center">
|
|
||||||
<v-card>
|
|
||||||
<v-card-title class="headline">
|
|
||||||
<v-icon x-large class="mr-2">mdi-shield-alert</v-icon>
|
|
||||||
Privacy Violations
|
|
||||||
</v-card-title>
|
|
||||||
<v-card-text>
|
|
||||||
ALPRs track your movements and store your data for long periods of time, creating a detailed record of your location history. They surveil mostly innocent people while claiming to target criminals.
|
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
|
||||||
</v-col>
|
|
||||||
<v-col cols="12" md="4" class="text-center">
|
|
||||||
<v-card>
|
|
||||||
<v-card-title class="headline">
|
|
||||||
<v-icon x-large class="mr-2">mdi-alert-circle</v-icon>
|
|
||||||
Risk of Misuse
|
|
||||||
</v-card-title>
|
|
||||||
<v-card-text>
|
|
||||||
Data from ALPRs has led to <a target="_blank" href="https://www.newsobserver.com/news/state/north-carolina/article287381160.html">wrongful arrests</a>, profiling, and <a target="_blank" href="https://www.kwch.com/2022/10/31/kechi-police-lieutenant-arrested-using-police-technology-stalk-wife/">stalking ex-partners</a> by police officers.
|
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
|
||||||
</v-col>
|
|
||||||
<v-col cols="12" md="4" class="text-center">
|
|
||||||
<v-card>
|
|
||||||
<v-card-title class="headline">
|
|
||||||
<v-icon x-large class="mr-2">mdi-handcuffs</v-icon>
|
|
||||||
Limited Benefits
|
|
||||||
</v-card-title>
|
|
||||||
<v-card-text>
|
|
||||||
There's no substantial evidence that ALPRs effectively prevent crime, despite <a target="_blank" href="https://www.404media.co/researcher-who-oversaw-flock-surveillance-study-now-has-concerns-about-it/">Flock's unethical attempts</a> to prove otherwise.
|
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
|
|
||||||
<v-btn class="mt-8" color="rgb(18, 151, 195)" large to="/dangers">
|
|
||||||
See All Dangers
|
|
||||||
<v-icon end>mdi-shield-alert</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</v-container>
|
|
||||||
|
|
||||||
<!-- Map Section -->
|
|
||||||
<v-container class="map-section py-10 text-center">
|
|
||||||
<h2 class="display-2 mb-4">Explore ALPR Locations Near You</h2>
|
|
||||||
<v-btn color="white" large @click="goToMap">
|
|
||||||
View the Map
|
|
||||||
<v-icon end>mdi-map</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</v-container>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.hero-section {
|
|
||||||
background: url('/flock-camera.jpeg') no-repeat center center;
|
|
||||||
background-size: cover;
|
|
||||||
color: white;
|
|
||||||
padding: 100px 0 !important;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-section::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-section > * {
|
|
||||||
position: relative;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-section {
|
|
||||||
background: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.map-section {
|
|
||||||
background: url('/deflock-screenshot.webp') no-repeat center center;
|
|
||||||
background-size: cover;
|
|
||||||
color: white;
|
|
||||||
padding: 100px 0;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.map-section::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: rgba(0, 0, 0, 0.4);
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.map-section > * {
|
|
||||||
position: relative;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bigger {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import ALPRCounter from '@/components/ALPRCounter.vue';
|
|
||||||
import { useGlobalStore } from '@/stores/global';
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const { setCurrentLocation } = useGlobalStore();
|
|
||||||
|
|
||||||
interface GoToMapOptions {
|
|
||||||
withCurrentLocation?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function goToMap(options: GoToMapOptions = {}) {
|
|
||||||
if (options.withCurrentLocation) {
|
|
||||||
setCurrentLocation()
|
|
||||||
.then((currentLocation) => {
|
|
||||||
const [lat, lon] = currentLocation;
|
|
||||||
router.push({ path: '/map', hash: `#map=16/${lat.toFixed(6)}/${lon.toFixed(6)}` });
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
router.push({ path: '/map' });
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
router.push({ path: '/map' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-container>
|
<v-container>
|
||||||
<h1 class="text-center">Report an ALPR</h1>
|
<h1 class="text-center mt-4">Report a New ALPR</h1>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
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:
|
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:
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
<v-stepper-vertical color="rgb(18, 151, 195)" v-model="step" flat non-linear class="my-8" edit-icon="mdi-home">
|
<v-stepper-vertical color="rgb(18, 151, 195)" v-model="step" flat non-linear class="my-8" edit-icon="mdi-home">
|
||||||
<template v-slot:default="{ step }: { step: any }">
|
<template v-slot:default="{ step }: { step: any }">
|
||||||
<v-stepper-vertical-item
|
<v-stepper-vertical-item
|
||||||
|
class="transparent"
|
||||||
:complete="step > 1"
|
:complete="step > 1"
|
||||||
title="Create an OpenStreetMap Account"
|
title="Create an OpenStreetMap Account"
|
||||||
value="1"
|
value="1"
|
||||||
@@ -20,6 +21,7 @@
|
|||||||
</v-stepper-vertical-item>
|
</v-stepper-vertical-item>
|
||||||
|
|
||||||
<v-stepper-vertical-item
|
<v-stepper-vertical-item
|
||||||
|
class="transparent"
|
||||||
:complete="step > 2"
|
:complete="step > 2"
|
||||||
title="Find the ALPR's Location"
|
title="Find the ALPR's Location"
|
||||||
value="2"
|
value="2"
|
||||||
@@ -31,6 +33,7 @@
|
|||||||
</v-stepper-vertical-item>
|
</v-stepper-vertical-item>
|
||||||
|
|
||||||
<v-stepper-vertical-item
|
<v-stepper-vertical-item
|
||||||
|
class="transparent"
|
||||||
:complete="step > 3"
|
:complete="step > 3"
|
||||||
title="Add the ALPR to OpenStreetMap"
|
title="Add the ALPR to OpenStreetMap"
|
||||||
value="3"
|
value="3"
|
||||||
@@ -39,7 +42,7 @@
|
|||||||
<p>
|
<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.
|
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>
|
</p>
|
||||||
<v-img max-width="450" src="/edit-map.png" class="my-8 mx-auto" />
|
<v-img max-width="450" src="/edit-map.png" class="my-8" />
|
||||||
<p class="mt-16 mb-8">
|
<p class="mt-16 mb-8">
|
||||||
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 one of the following sets of tags based on the brand of the ALPR:
|
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 one of the following sets of tags based on the brand of the ALPR:
|
||||||
</p>
|
</p>
|
||||||
@@ -53,10 +56,11 @@
|
|||||||
<p class="mt-8">
|
<p class="mt-8">
|
||||||
After copying the tags, paste them into the <strong>Tags</strong> field in the popup.
|
After copying the tags, paste them into the <strong>Tags</strong> field in the popup.
|
||||||
</p>
|
</p>
|
||||||
<v-img max-width="450" class="my-8 mx-auto" src="/paste-tags.png" />
|
<v-img max-width="450" class="my-8" src="/paste-tags.png" />
|
||||||
</v-stepper-vertical-item>
|
</v-stepper-vertical-item>
|
||||||
|
|
||||||
<v-stepper-vertical-item
|
<v-stepper-vertical-item
|
||||||
|
class="transparent"
|
||||||
:complete="step > 4"
|
:complete="step > 4"
|
||||||
title="Adjust the Direction"
|
title="Adjust the Direction"
|
||||||
value="4"
|
value="4"
|
||||||
@@ -69,6 +73,7 @@
|
|||||||
</v-stepper-vertical-item>
|
</v-stepper-vertical-item>
|
||||||
|
|
||||||
<v-stepper-vertical-item
|
<v-stepper-vertical-item
|
||||||
|
class="transparent"
|
||||||
:complete="step > 5"
|
:complete="step > 5"
|
||||||
title="Submit Your Changes"
|
title="Submit Your Changes"
|
||||||
value="5"
|
value="5"
|
||||||
@@ -90,6 +95,7 @@
|
|||||||
</v-stepper-vertical-item>
|
</v-stepper-vertical-item>
|
||||||
|
|
||||||
<v-stepper-vertical-item
|
<v-stepper-vertical-item
|
||||||
|
class="transparent"
|
||||||
:complete="step > 6"
|
:complete="step > 6"
|
||||||
title="Hang a Sign"
|
title="Hang a Sign"
|
||||||
value="6"
|
value="6"
|
||||||
@@ -98,9 +104,28 @@
|
|||||||
<p>
|
<p>
|
||||||
Download our <a href="/deflock-poster.pdf" target="_blank">ALPR sign</a> and hang it near the ALPR to help raise awareness about the device. Be sure to follow all local laws and regulations when hanging signs.
|
Download our <a href="/deflock-poster.pdf" target="_blank">ALPR sign</a> and hang it near the ALPR to help raise awareness about the device. Be sure to follow all local laws and regulations when hanging signs.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<template v-slot:next>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
</v-stepper-vertical-item>
|
</v-stepper-vertical-item>
|
||||||
</template>
|
</template>
|
||||||
</v-stepper-vertical>
|
</v-stepper-vertical>
|
||||||
|
|
||||||
|
<h2 class="text-center mt-16">Edit an Existing ALPR</h2>
|
||||||
|
<p>
|
||||||
|
If you find an ALPR that's missing information and would like to update it, you can follow the same steps as above. Each ALPR on DeFlock has a Node ID that you can use to find it on OpenStreetMap.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="mb-16">
|
||||||
|
Simply click on the ALPR with missing information, and find the Node ID (e.g. <code>node/1237489334</code>) at the bottom of the popup. In the OSM editor search field, paste the <i>numerical portion</i> of the Node ID to find the ALPR and make your changes.
|
||||||
|
</p>
|
||||||
|
|
||||||
</v-container>
|
</v-container>
|
||||||
<Footer />
|
<Footer />
|
||||||
</template>
|
</template>
|
||||||
@@ -116,4 +141,8 @@ const step = ref(1);
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@import url('@/assets/typography.css');
|
@import url('@/assets/typography.css');
|
||||||
|
|
||||||
|
.transparent {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -12,5 +12,9 @@ export default defineConfig({
|
|||||||
alias: {
|
alias: {
|
||||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
host: '0.0.0.0',
|
||||||
|
port: 5173,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user