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-btn icon>
|
||||
<!-- <v-btn icon>
|
||||
<v-icon @click="toggleTheme">mdi-theme-light-dark</v-icon>
|
||||
</v-btn>
|
||||
</v-btn> -->
|
||||
</v-app-bar>
|
||||
|
||||
<v-navigation-drawer
|
||||
|
||||
@@ -31,3 +31,8 @@ p {
|
||||
.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 {
|
||||
margin: 0.5em 0;
|
||||
margin: 0.8em 0;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<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>
|
||||
<div :class="{ 'fade-in': showFinalAnimation }" class="subheading fade-text">and rapidly growing!</div>
|
||||
</div>
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, watch, type Ref } from 'vue';
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { getALPRCounts } from '@/services/apiService';
|
||||
import { CountUp } from 'countup.js';
|
||||
|
||||
@@ -30,6 +31,7 @@ const counts: Ref<Counts> = ref({
|
||||
worldwide: undefined,
|
||||
});
|
||||
const showFinalAnimation = ref(false);
|
||||
const { xs: isMobile } = useDisplay();
|
||||
|
||||
onMounted(() => {
|
||||
getALPRCounts().then((response) => {
|
||||
@@ -74,4 +76,9 @@ watch(counts, (newCounts: Counts) => {
|
||||
.fade-in {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.mobile {
|
||||
display: block;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<v-icon start>mdi-compass-outline</v-icon> <b>{{ cardinalDirection }}</b>
|
||||
</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">
|
||||
{{ alpr.tags.manufacturer }}
|
||||
</span>
|
||||
@@ -27,7 +27,7 @@
|
||||
</b>
|
||||
</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">
|
||||
{{ alpr.tags.operator }}
|
||||
</span>
|
||||
@@ -39,7 +39,12 @@
|
||||
</v-list>
|
||||
|
||||
<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>
|
||||
</v-sheet>
|
||||
</template>
|
||||
@@ -48,7 +53,7 @@
|
||||
import { defineProps, computed } from 'vue';
|
||||
import type { PropType } from 'vue';
|
||||
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({
|
||||
alpr: {
|
||||
@@ -60,11 +65,11 @@ const props = defineProps({
|
||||
const isFaceRecognition = computed(() => props.alpr.tags.brand === 'Avigilon');
|
||||
|
||||
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 {
|
||||
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>
|
||||
|
||||
@@ -11,23 +11,25 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, h, createApp, watch, ref, type PropType, type Ref } from 'vue';
|
||||
import L, { type LatLngExpression } from 'leaflet';
|
||||
import { onBeforeUnmount, onMounted, h, createApp, watch, ref, type PropType, type Ref } from 'vue';
|
||||
import L, { type LatLngExpression, type FeatureGroup, type MarkerClusterGroup, type Marker, type CircleMarker } from 'leaflet';
|
||||
import type { ALPR } from '@/types';
|
||||
|
||||
import DFMapPopup from './DFMapPopup.vue';
|
||||
|
||||
import { createVuetify } from 'vuetify'
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import 'leaflet.markercluster';
|
||||
import 'leaflet.markercluster/dist/MarkerCluster.Default.css';
|
||||
import 'leaflet.markercluster/dist/MarkerCluster.css';
|
||||
import { createVuetify } from 'vuetify'
|
||||
|
||||
// Color of Marker Circle
|
||||
const MARKER_COLOR = 'rgb(63,84,243)';
|
||||
|
||||
// Internal State Management
|
||||
const markerMap = new Map<string, Marker | CircleMarker>();
|
||||
const isInternalUpdate = ref(false);
|
||||
|
||||
const props = defineProps({
|
||||
center: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
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 circlesLayer: L.FeatureGroup;
|
||||
let clusterLayer: L.MarkerClusterGroup;
|
||||
let currentLocationLayer: L.FeatureGroup;
|
||||
let circlesLayer: FeatureGroup;
|
||||
let clusterLayer: MarkerClusterGroup;
|
||||
let currentLocationLayer: FeatureGroup;
|
||||
|
||||
onMounted(() => {
|
||||
initializeMap();
|
||||
});
|
||||
|
||||
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;
|
||||
// Marker Creation Utilities
|
||||
function createSVGMarker(alpr: ALPR): string {
|
||||
const orientationDegrees = alpr.tags.direction;
|
||||
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.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;">
|
||||
${orientationDegrees ? fovPath : ''}
|
||||
<g transform="matrix(0.906623,0,0,0.906623,23.9045,22.3271)">
|
||||
@@ -105,31 +70,21 @@ function makeSVGMarker(alpr: ALPR): L.Marker {
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
const el = document.createElement('div');
|
||||
el.innerHTML = svgMarker;
|
||||
el.style.width = '50px';
|
||||
el.style.height = '50px';
|
||||
|
||||
function createMarker(alpr: ALPR): Marker | CircleMarker {
|
||||
if (hasPlottableOrientation(alpr.tags.direction)) {
|
||||
const icon = L.divIcon({
|
||||
className: 'leaflet-data-marker',
|
||||
html: svgMarker,
|
||||
html: createSVGMarker(alpr),
|
||||
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;
|
||||
return L.marker([alpr.lat, alpr.lon], { icon });
|
||||
}
|
||||
|
||||
function makeCircleMarker(alpr: ALPR): L.CircleMarker {
|
||||
const { lat, lon: lng } = alpr;
|
||||
const orientationDegrees = alpr.tags.direction;
|
||||
|
||||
const marker = L.circleMarker([lat, lng], {
|
||||
return L.circleMarker([alpr.lat, alpr.lon], {
|
||||
fill: true,
|
||||
fillColor: MARKER_COLOR,
|
||||
fillOpacity: 0.6,
|
||||
@@ -139,9 +94,6 @@ function makeCircleMarker(alpr: ALPR): L.CircleMarker {
|
||||
radius: 8,
|
||||
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 {
|
||||
@@ -151,13 +103,15 @@ function bindPopup(marker: L.CircleMarker | L.Marker, alpr: ALPR): L.CircleMarke
|
||||
const popupContent = document.createElement('div');
|
||||
createApp({
|
||||
render() {
|
||||
return h(DFMapPopup, { alpr: {
|
||||
return h(DFMapPopup, {
|
||||
alpr: {
|
||||
id: alpr.id,
|
||||
lat: alpr.lat,
|
||||
lon: alpr.lon,
|
||||
tags: alpr.tags,
|
||||
type: alpr.type,
|
||||
} });
|
||||
}
|
||||
});
|
||||
}
|
||||
}).use(createVuetify()).mount(popupContent);
|
||||
|
||||
@@ -172,72 +126,124 @@ function hasPlottableOrientation(orientationDegrees: string) {
|
||||
return orientationDegrees && !isNaN(parseInt(orientationDegrees));
|
||||
}
|
||||
|
||||
function populateMap() {
|
||||
const showFov = props.zoom >= 16;
|
||||
// Map State Management
|
||||
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();
|
||||
|
||||
if (clusterLayer) {
|
||||
map.removeLayer(clusterLayer);
|
||||
}
|
||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
||||
}).addTo(map);
|
||||
|
||||
clusterLayer = L.markerClusterGroup({
|
||||
chunkedLoading: true,
|
||||
disableClusteringAtZoom: 16, // showFov threshold
|
||||
disableClusteringAtZoom: 16,
|
||||
removeOutsideVisibleBounds: true,
|
||||
maxClusterRadius: 60,
|
||||
zoomToBoundsOnClick: true,
|
||||
spiderfyOnEveryZoom: false,
|
||||
spiderfyOnMaxZoom: false,
|
||||
});
|
||||
|
||||
circlesLayer = L.featureGroup();
|
||||
currentLocationLayer = L.featureGroup();
|
||||
|
||||
for (const alpr of props.alprs) {
|
||||
const orientationDegrees = alpr.tags.direction;
|
||||
map.addLayer(clusterLayer);
|
||||
registerMapEvents();
|
||||
|
||||
let marker: L.CircleMarker | L.Marker;
|
||||
|
||||
if (hasPlottableOrientation(orientationDegrees)) {
|
||||
marker = makeSVGMarker(alpr);
|
||||
if (props.alprs.length) {
|
||||
updateMarkers(props.alprs);
|
||||
} else {
|
||||
marker = makeCircleMarker(alpr);
|
||||
emit('update:bounds', map.getBounds());
|
||||
}
|
||||
}
|
||||
|
||||
function updateMarkers(newAlprs: ALPR[]): void {
|
||||
const currentIds = new Set(markerMap.keys());
|
||||
const nonexistingAlprs = newAlprs.filter(alpr => !currentIds.has(alpr.id));
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
clusterLayer.addLayer(circlesLayer);
|
||||
map.addLayer(clusterLayer);
|
||||
}
|
||||
|
||||
const emit = defineEmits(['update:center', 'update:zoom', 'update:bounds']);
|
||||
// Update cluster layer
|
||||
clusterLayer.clearLayers();
|
||||
clusterLayer.addLayer(circlesLayer);
|
||||
}
|
||||
|
||||
function registerEvents() {
|
||||
function updateCurrentLocation(): void {
|
||||
currentLocationLayer.clearLayers();
|
||||
|
||||
if (props.currentLocation) {
|
||||
const marker = L.circleMarker([props.currentLocation[0], props.currentLocation[1]], {
|
||||
radius: 10,
|
||||
color: '#ffffff',
|
||||
fillColor: '#007bff',
|
||||
fillOpacity: 1,
|
||||
weight: 4
|
||||
}).bindPopup('Current Location');
|
||||
|
||||
currentLocationLayer.addLayer(marker);
|
||||
map.addLayer(currentLocationLayer);
|
||||
}
|
||||
}
|
||||
|
||||
// Lifecycle Hooks
|
||||
onMounted(() => {
|
||||
initializeMap();
|
||||
|
||||
// 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.zoom, (newZoom: number) => {
|
||||
if (!isInternalUpdate.value) {
|
||||
isInternalUpdate.value = true;
|
||||
map.setZoom(newZoom);
|
||||
setTimeout(() => {
|
||||
isInternalUpdate.value = false;
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
|
||||
watch(() => props.alprs, (newAlprs) => {
|
||||
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());
|
||||
});
|
||||
}
|
||||
|
||||
function registerWatchers() {
|
||||
watch(() => props.center, (newCenter: any, oldCenter: any) => {
|
||||
if (newCenter.lat !== oldCenter.lat || newCenter.lng !== oldCenter.lng) {
|
||||
map.setView(newCenter as LatLngExpression);
|
||||
}
|
||||
});
|
||||
|
||||
watch(() => props.currentLocation, (newLocation, oldLocation) => {
|
||||
console.log("current location watcher triggered!");
|
||||
renderCurrentLocation();
|
||||
});
|
||||
|
||||
watch(() => props.alprs, (newAlprs, oldAlprs) => {
|
||||
populateMap();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
:items="alprBrands"
|
||||
item-title="nickname"
|
||||
return-object
|
||||
label="Manufacturer"
|
||||
variant="solo-filled"
|
||||
label="Choose a Manufacturer"
|
||||
variant="outlined"
|
||||
flat
|
||||
hide-details
|
||||
></v-select>
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
<div>
|
||||
<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 class="mt-4">v1.0</p>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
@@ -101,7 +101,7 @@ const router = createRouter({
|
||||
{
|
||||
path: '/qr',
|
||||
name: 'qr-landing',
|
||||
component: () => import('../views/QRLandingView.vue'),
|
||||
component: () => import('../views/Landing.vue'),
|
||||
meta: {
|
||||
title: 'You Found an ALPR | DeFlock'
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<v-col cols="12" md="10">
|
||||
<h2 class="mb-2">Our Amazing Sponsors</h2>
|
||||
<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>
|
||||
|
||||
<v-row>
|
||||
|
||||
@@ -3,25 +3,24 @@
|
||||
<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 ALPRs!</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 Nearby ALPRs
|
||||
<v-icon end>mdi-map</v-icon>
|
||||
</v-btn>
|
||||
<h1 class="display-1 px-8">You're Being Tracked</h1>
|
||||
|
||||
<ALPRCounter class="my-6" />
|
||||
|
||||
<!-- Featured On Section -->
|
||||
<v-container class="featured-on-section mt-10">
|
||||
<h4 class="mb-8" style="opacity: 0.8">Featured On</h4>
|
||||
<v-container class="featured-on-section my-8">
|
||||
<h4 class="mb-4" style="opacity: 0.8">Featured On</h4>
|
||||
<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-card>
|
||||
</v-row>
|
||||
</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-row>
|
||||
</v-container>
|
||||
@@ -30,14 +29,14 @@
|
||||
<v-container class="py-10 text-center info-section">
|
||||
|
||||
<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.
|
||||
</p>
|
||||
|
||||
<v-divider class="my-8" />
|
||||
|
||||
<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.
|
||||
</p>
|
||||
|
||||
@@ -49,7 +48,7 @@
|
||||
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.
|
||||
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>
|
||||
</v-col>
|
||||
@@ -100,7 +99,7 @@
|
||||
background: url('/flock-camera.jpeg') no-repeat center center;
|
||||
background-size: cover;
|
||||
color: white;
|
||||
padding: 100px 0 50px 0 !important;
|
||||
padding: 60px 0 50px 0 !important;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -111,7 +110,7 @@
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@@ -121,7 +120,7 @@
|
||||
}
|
||||
|
||||
.map-section {
|
||||
background: url('/deflock-screenshot.webp') no-repeat right center;
|
||||
background: url('/deflock-screenshot.webp') no-repeat center center;
|
||||
background-size: cover;
|
||||
color: white;
|
||||
padding: 100px 0;
|
||||
|
||||
@@ -112,7 +112,9 @@ function goToUserLocation() {
|
||||
setCurrentLocation()
|
||||
.then((cl) => {
|
||||
center.value = cl;
|
||||
setTimeout(() => {
|
||||
zoom.value = DEFAULT_ZOOM;
|
||||
}, 10);
|
||||
})
|
||||
.catch(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>
|
||||
<v-container>
|
||||
<h1 class="text-center">Report an ALPR</h1>
|
||||
<h1 class="text-center mt-4">Report a New ALPR</h1>
|
||||
|
||||
<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:
|
||||
@@ -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">
|
||||
<template v-slot:default="{ step }: { step: any }">
|
||||
<v-stepper-vertical-item
|
||||
class="transparent"
|
||||
:complete="step > 1"
|
||||
title="Create an OpenStreetMap Account"
|
||||
value="1"
|
||||
@@ -20,6 +21,7 @@
|
||||
</v-stepper-vertical-item>
|
||||
|
||||
<v-stepper-vertical-item
|
||||
class="transparent"
|
||||
:complete="step > 2"
|
||||
title="Find the ALPR's Location"
|
||||
value="2"
|
||||
@@ -31,6 +33,7 @@
|
||||
</v-stepper-vertical-item>
|
||||
|
||||
<v-stepper-vertical-item
|
||||
class="transparent"
|
||||
:complete="step > 3"
|
||||
title="Add the ALPR to OpenStreetMap"
|
||||
value="3"
|
||||
@@ -39,7 +42,7 @@
|
||||
<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 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">
|
||||
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>
|
||||
@@ -53,10 +56,11 @@
|
||||
<p class="mt-8">
|
||||
After copying the tags, paste them into the <strong>Tags</strong> field in the popup.
|
||||
</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
|
||||
class="transparent"
|
||||
:complete="step > 4"
|
||||
title="Adjust the Direction"
|
||||
value="4"
|
||||
@@ -69,6 +73,7 @@
|
||||
</v-stepper-vertical-item>
|
||||
|
||||
<v-stepper-vertical-item
|
||||
class="transparent"
|
||||
:complete="step > 5"
|
||||
title="Submit Your Changes"
|
||||
value="5"
|
||||
@@ -90,6 +95,7 @@
|
||||
</v-stepper-vertical-item>
|
||||
|
||||
<v-stepper-vertical-item
|
||||
class="transparent"
|
||||
:complete="step > 6"
|
||||
title="Hang a Sign"
|
||||
value="6"
|
||||
@@ -98,9 +104,28 @@
|
||||
<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.
|
||||
</p>
|
||||
|
||||
<template v-slot:next>
|
||||
<v-btn
|
||||
color="primary"
|
||||
disabled
|
||||
>
|
||||
Next
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-stepper-vertical-item>
|
||||
</template>
|
||||
</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>
|
||||
<Footer />
|
||||
</template>
|
||||
@@ -116,4 +141,8 @@ const step = ref(1);
|
||||
|
||||
<style scoped>
|
||||
@import url('@/assets/typography.css');
|
||||
|
||||
.transparent {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -12,5 +12,9 @@ export default defineConfig({
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
},
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 5173,
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user