mirror of
https://github.com/FoggedLens/deflock.git
synced 2026-07-06 12:28:11 +02:00
works with segmenting, now adjust angle and whitelisted tags
This commit is contained in:
@@ -13,6 +13,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, h, createApp, watch, type PropType } from 'vue';
|
import { onMounted, h, createApp, watch, type PropType } from 'vue';
|
||||||
import L, { type LatLngExpression } from 'leaflet';
|
import L, { type LatLngExpression } from 'leaflet';
|
||||||
|
import type { ALPR } from '@/types';
|
||||||
|
|
||||||
import DFMapPopup from './DFMapPopup.vue';
|
import DFMapPopup from './DFMapPopup.vue';
|
||||||
|
|
||||||
@@ -33,7 +34,10 @@ const props = defineProps({
|
|||||||
type: Number,
|
type: Number,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
alprs: Array,
|
alprs: {
|
||||||
|
type: Array as PropType<ALPR[]>,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
currentLocation: {
|
currentLocation: {
|
||||||
type: Object as PropType<[number, number] | null>,
|
type: Object as PropType<[number, number] | null>,
|
||||||
default: null,
|
default: null,
|
||||||
@@ -99,10 +103,9 @@ function populateMap() {
|
|||||||
clusterLayer = L.markerClusterGroup();
|
clusterLayer = L.markerClusterGroup();
|
||||||
circlesLayer = L.featureGroup();
|
circlesLayer = L.featureGroup();
|
||||||
|
|
||||||
for (let i = 0; i < 200; i++) {
|
for (const alpr of props.alprs) {
|
||||||
const lat = 51.505 + (Math.random() - 0.5) * 0.1;
|
const { lat, lon: lng } = alpr;
|
||||||
const lng = -0.09 + (Math.random() - 0.5) * 0.1;
|
const orientationDegrees = alpr.tags?.direction || 0; // TODO: make sure this works with nodes w/o orientation
|
||||||
const orientationDegrees = Math.random() * 360;
|
|
||||||
|
|
||||||
let marker: L.CircleMarker | L.Marker;
|
let marker: L.CircleMarker | L.Marker;
|
||||||
|
|
||||||
@@ -129,7 +132,7 @@ function populateMap() {
|
|||||||
html: svgMarker,
|
html: svgMarker,
|
||||||
iconSize: [60, 60],
|
iconSize: [60, 60],
|
||||||
iconAnchor: [30, 30],
|
iconAnchor: [30, 30],
|
||||||
popupAnchor: [0, -5],
|
popupAnchor: [0, -8],
|
||||||
});
|
});
|
||||||
|
|
||||||
marker = L.marker([lat, lng], { icon: icon });
|
marker = L.marker([lat, lng], { icon: icon });
|
||||||
@@ -155,11 +158,11 @@ function populateMap() {
|
|||||||
createApp({
|
createApp({
|
||||||
render() {
|
render() {
|
||||||
return h(DFMapPopup, { alpr: {
|
return h(DFMapPopup, { alpr: {
|
||||||
id: `id-${i}`,
|
id: alpr.id,
|
||||||
lat: lat,
|
lat: lat,
|
||||||
lon: lng,
|
lon: lng,
|
||||||
tags: { type: 'car' },
|
tags: alpr.tags,
|
||||||
type: "node",
|
type: alpr.type,
|
||||||
} });
|
} });
|
||||||
}
|
}
|
||||||
}).use(createVuetify()).mount(popupContent);
|
}).use(createVuetify()).mount(popupContent);
|
||||||
@@ -221,6 +224,10 @@ function registerWatchers() {
|
|||||||
console.log("current location watcher triggered!");
|
console.log("current location watcher triggered!");
|
||||||
renderCurrentLocation();
|
renderCurrentLocation();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(() => props.alprs, (newAlprs, oldAlprs) => {
|
||||||
|
populateMap();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { onBeforeMount, computed, ref, type Ref } from 'vue';
|
||||||
|
import type { ALPR } from '@/types';
|
||||||
|
import axios from 'axios';
|
||||||
|
import type { BoundingBox } from '@/services/apiService'; // TODO: this is a strange place to hold this type
|
||||||
|
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL: 'https://cdn.deflock.me/regions',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useTilesStore = defineStore('tiles', () => {
|
||||||
|
// Key: "lat/lng", Value: ALPR[]
|
||||||
|
const tiles: Ref<Record<string, ALPR[]>> = ref({});
|
||||||
|
const availableTiles: Ref<string[]> = ref([]);
|
||||||
|
const expirationDateUtc: Ref<Date | null> = ref(null);
|
||||||
|
let tileUrlTemplate: string|undefined = undefined;
|
||||||
|
let tileSizeDegrees: number|undefined = undefined;
|
||||||
|
|
||||||
|
const fetchIndex = async (): Promise<void> => {
|
||||||
|
if (expirationDateUtc.value && expirationDateUtc.value > new Date()) {
|
||||||
|
console.debug('Index is not expired, using cached index');
|
||||||
|
} else {
|
||||||
|
console.debug('Index is expired or not set, fetching new index');
|
||||||
|
const response = await api.get('/index.json');
|
||||||
|
expirationDateUtc.value = new Date(response.data.expiration_utc);
|
||||||
|
availableTiles.value = response.data.regions;
|
||||||
|
tileUrlTemplate = response.data.tile_url;
|
||||||
|
tileSizeDegrees = response.data.tile_size_degrees;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchAndAddTile = async (lat: number, lng: number): Promise<void> => {
|
||||||
|
const key = `${lat}/${lng}`;
|
||||||
|
|
||||||
|
if (tiles.value[key]) {
|
||||||
|
console.debug(`Tile ${key} is already cached, skipping fetch`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tileUrlTemplate) {
|
||||||
|
console.warn('Tile URL template is not set, skipping fetch');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = tileUrlTemplate.replace('{lat}/{lon}', key);
|
||||||
|
const tile = await api.get(url);
|
||||||
|
|
||||||
|
tiles.value[key] = tile.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchVisibleTiles = async (boundingBox: BoundingBox): Promise<void> => {
|
||||||
|
if (!(tileUrlTemplate && tileSizeDegrees)) {
|
||||||
|
console.warn('Tile URL template is not set, skipping fetch');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { minLat: south, minLng: west, maxLat: north, maxLng: east } = boundingBox;
|
||||||
|
|
||||||
|
// Determine tiles in viewport
|
||||||
|
const visibleTiles = [];
|
||||||
|
for (let lat = Math.floor(south / tileSizeDegrees) * tileSizeDegrees; lat <= Math.ceil(north / tileSizeDegrees) * tileSizeDegrees; lat += tileSizeDegrees) {
|
||||||
|
for (let lng = Math.floor(west / tileSizeDegrees) * tileSizeDegrees; lng <= Math.ceil(east / tileSizeDegrees) * tileSizeDegrees; lng += tileSizeDegrees) {
|
||||||
|
const key = `${lat}/${lng}`;
|
||||||
|
if (!tiles.value[key] && availableTiles.value.includes(key)) {
|
||||||
|
visibleTiles.push({ lat, lng });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch missing tiles
|
||||||
|
const fetchPromises = visibleTiles.map(({ lat, lng }) =>
|
||||||
|
fetchAndAddTile(lat, lng)
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(fetchPromises);
|
||||||
|
}
|
||||||
|
|
||||||
|
const allNodes = computed(() => Object.values(tiles.value).flat());
|
||||||
|
|
||||||
|
onBeforeMount(fetchIndex);
|
||||||
|
|
||||||
|
return {
|
||||||
|
fetchVisibleTiles,
|
||||||
|
allNodes,
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
v-model:zoom="zoom"
|
v-model:zoom="zoom"
|
||||||
:current-location="currentLocation"
|
:current-location="currentLocation"
|
||||||
@update:bounds="updateBounds"
|
@update:bounds="updateBounds"
|
||||||
|
:alprs
|
||||||
>
|
>
|
||||||
<!-- SEARCH -->
|
<!-- SEARCH -->
|
||||||
<template v-slot:topleft>
|
<template v-slot:topleft>
|
||||||
@@ -59,6 +60,7 @@ import type { Cluster } from '@/services/apiService';
|
|||||||
import { getALPRs, geocodeQuery, getClusters } from '@/services/apiService';
|
import { getALPRs, geocodeQuery, getClusters } from '@/services/apiService';
|
||||||
import { useDisplay, useTheme } from 'vuetify';
|
import { useDisplay, useTheme } from 'vuetify';
|
||||||
import { useGlobalStore } from '@/stores/global';
|
import { useGlobalStore } from '@/stores/global';
|
||||||
|
import { useTilesStore } from '@/stores/tiles';
|
||||||
import type { ALPR } from '@/types';
|
import type { ALPR } from '@/types';
|
||||||
import L from 'leaflet';
|
import L from 'leaflet';
|
||||||
globalThis.L = L;
|
globalThis.L = L;
|
||||||
@@ -75,6 +77,10 @@ const center: Ref<any|null> = ref(null);
|
|||||||
const bounds: Ref<BoundingBox|null> = ref(null);
|
const bounds: Ref<BoundingBox|null> = ref(null);
|
||||||
const searchField: Ref<any|null> = ref(null);
|
const searchField: Ref<any|null> = ref(null);
|
||||||
const searchQuery: Ref<string> = ref('');
|
const searchQuery: Ref<string> = ref('');
|
||||||
|
const tilesStore = useTilesStore();
|
||||||
|
|
||||||
|
const { fetchVisibleTiles } = tilesStore;
|
||||||
|
const alprs = computed(() => tilesStore.allNodes);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { xs } = useDisplay();
|
const { xs } = useDisplay();
|
||||||
@@ -85,8 +91,6 @@ const mapTileUrl = computed(() =>
|
|||||||
'https://tiles.stadiamaps.com/tiles/alidade_smooth_dark/{z}/{x}/{y}{r}.png' :
|
'https://tiles.stadiamaps.com/tiles/alidade_smooth_dark/{z}/{x}/{y}{r}.png' :
|
||||||
'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
|
'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
|
||||||
); // TODO: implement dark mode in LeafletMap.vue
|
); // TODO: implement dark mode in LeafletMap.vue
|
||||||
|
|
||||||
const alprs: Ref<ALPR[]> = ref([]);
|
|
||||||
const clusters: Ref<Cluster[]> = ref([]);
|
const clusters: Ref<Cluster[]> = ref([]);
|
||||||
const bboxForLastRequest: Ref<BoundingBox|null> = ref(null);
|
const bboxForLastRequest: Ref<BoundingBox|null> = ref(null);
|
||||||
const showClusters = computed(() => zoom.value <= CLUSTER_ZOOM_THRESHOLD);
|
const showClusters = computed(() => zoom.value <= CLUSTER_ZOOM_THRESHOLD);
|
||||||
@@ -148,12 +152,7 @@ function updateBounds(newBounds: any) {
|
|||||||
});
|
});
|
||||||
bounds.value = newBoundingBox;
|
bounds.value = newBoundingBox;
|
||||||
|
|
||||||
if (bboxForLastRequest.value && newBoundingBox.isSubsetOf(bboxForLastRequest.value)) {
|
updateMarkers();
|
||||||
console.debug('new bounds are a subset of the last request, skipping');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// updateMarkers();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateURL() {
|
function updateURL() {
|
||||||
@@ -166,46 +165,17 @@ function updateURL() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(zoom, (newZoom, oldZoom) => {
|
|
||||||
|
|
||||||
if (newZoom <= CLUSTER_ZOOM_THRESHOLD && oldZoom > CLUSTER_ZOOM_THRESHOLD) {
|
|
||||||
bboxForLastRequest.value = bounds.value;
|
|
||||||
} else if (newZoom < CLUSTER_ZOOM_THRESHOLD) {
|
|
||||||
alprs.value = [];
|
|
||||||
bboxForLastRequest.value = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function updateMarkers() {
|
function updateMarkers() {
|
||||||
// Fetch ALPRs in the current view
|
// Fetch ALPRs in the current view
|
||||||
if (!bounds.value) {
|
if (!bounds.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showClusters.value || !canRefreshMarkers.value) {
|
console.log('Fetching visible tiles');
|
||||||
return;
|
fetchVisibleTiles(bounds.value);
|
||||||
}
|
|
||||||
|
|
||||||
isLoadingALPRs.value = true;
|
|
||||||
getALPRs(bounds.value)
|
|
||||||
.then((result: any) => {
|
|
||||||
// merge incoming with existing, so that moving the map doesn't remove markers
|
|
||||||
const existingIds = new Set(alprs.value.map(alpr => alpr.id));
|
|
||||||
const newAlprs = result.elements.filter((alpr: any) => !existingIds.has(alpr.id));
|
|
||||||
alprs.value = [...alprs.value, ...newAlprs];
|
|
||||||
bboxForLastRequest.value = bounds.value;
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
isLoadingALPRs.value = false;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getClusters()
|
|
||||||
.then((result: any) => {
|
|
||||||
clusters.value = result.clusters;
|
|
||||||
});
|
|
||||||
|
|
||||||
const hash = router.currentRoute.value.hash;
|
const hash = router.currentRoute.value.hash;
|
||||||
if (hash) {
|
if (hash) {
|
||||||
const parts = hash.split('/');
|
const parts = hash.split('/');
|
||||||
|
|||||||
Reference in New Issue
Block a user