a lot of refactoring and cleaning up

This commit is contained in:
Will Freeman
2024-12-27 20:22:04 -07:00
parent 75e12f43a4
commit 1ba3f9c3bb
15 changed files with 216 additions and 320 deletions
+8 -1
View File
@@ -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">&nbsp;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>
+11 -6
View File
@@ -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>
+128 -122
View File
@@ -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: '&copy; <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: '&copy; <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>
+2 -2
View File
@@ -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>
+1
View File
@@ -57,6 +57,7 @@
<div>
<p>&copy; {{ 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>