mirror of
https://github.com/FoggedLens/deflock.git
synced 2026-06-02 12:31:36 +02:00
a lot of refactoring and cleaning up
This commit is contained in:
@@ -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';
|
||||
|
||||
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 {
|
||||
const { lat, lon: lng } = alpr;
|
||||
const orientationDegrees = alpr.tags.direction;
|
||||
function createMarker(alpr: ALPR): Marker | CircleMarker {
|
||||
if (hasPlottableOrientation(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,
|
||||
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: {
|
||||
id: alpr.id,
|
||||
lat: alpr.lat,
|
||||
lon: alpr.lon,
|
||||
tags: alpr.tags,
|
||||
type: alpr.type,
|
||||
} });
|
||||
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 (props.alprs.length) {
|
||||
updateMarkers(props.alprs);
|
||||
} else {
|
||||
emit('update:bounds', map.getBounds());
|
||||
}
|
||||
}
|
||||
|
||||
if (hasPlottableOrientation(orientationDegrees)) {
|
||||
marker = makeSVGMarker(alpr);
|
||||
} else {
|
||||
marker = makeCircleMarker(alpr);
|
||||
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);
|
||||
}
|
||||
|
||||
circlesLayer.addLayer(marker);
|
||||
}
|
||||
|
||||
// Update cluster layer
|
||||
clusterLayer.clearLayers();
|
||||
clusterLayer.addLayer(circlesLayer);
|
||||
map.addLayer(clusterLayer);
|
||||
}
|
||||
|
||||
const emit = defineEmits(['update:center', 'update:zoom', 'update:bounds']);
|
||||
function updateCurrentLocation(): void {
|
||||
currentLocationLayer.clearLayers();
|
||||
|
||||
function registerEvents() {
|
||||
map.on('moveend', () => {
|
||||
emit('update:center', map.getCenter());
|
||||
emit('update:zoom', map.getZoom());
|
||||
emit('update:bounds', map.getBounds());
|
||||
});
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
function registerWatchers() {
|
||||
watch(() => props.center, (newCenter: any, oldCenter: any) => {
|
||||
if (newCenter.lat !== oldCenter.lat || newCenter.lng !== oldCenter.lng) {
|
||||
map.setView(newCenter as LatLngExpression);
|
||||
// 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.currentLocation, (newLocation, oldLocation) => {
|
||||
console.log("current location watcher triggered!");
|
||||
renderCurrentLocation();
|
||||
watch(() => props.zoom, (newZoom: number) => {
|
||||
if (!isInternalUpdate.value) {
|
||||
isInternalUpdate.value = true;
|
||||
map.setZoom(newZoom);
|
||||
setTimeout(() => {
|
||||
isInternalUpdate.value = false;
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
|
||||
watch(() => props.alprs, (newAlprs, oldAlprs) => {
|
||||
populateMap();
|
||||
});
|
||||
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());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user