mirror of
https://github.com/meshcore-dev/map.meshcore.dev.git
synced 2026-04-20 22:13:50 +00:00
added filter by frequency, optimizations, bugfixes
This commit is contained in:
parent
b85c616511
commit
1e5f13a80a
3 changed files with 181 additions and 70 deletions
|
|
@ -232,6 +232,11 @@ table.node-info tr td:last-child {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.svg-node-icon {
|
||||||
|
background: none !important;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
time {
|
time {
|
||||||
cursor: help;
|
cursor: help;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
|
|
||||||
11
index.html
11
index.html
|
|
@ -29,8 +29,8 @@
|
||||||
</a>
|
</a>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<form class="search no-margin" action="javascript:;">
|
<form class="search no-margin" data-ui action="javascript:;">
|
||||||
<menu class="left no-wrap" id="node-filter" data-ui="#node-filter">
|
<menu class="left no-wrap" id="node-filter">
|
||||||
<li>
|
<li>
|
||||||
<label class="checkbox">
|
<label class="checkbox">
|
||||||
<input type="checkbox" value="1" v-model="app.nodeFilter"><span>Clients</span>
|
<input type="checkbox" value="1" v-model="app.nodeFilter"><span>Clients</span>
|
||||||
|
|
@ -79,6 +79,13 @@
|
||||||
<span></span>
|
<span></span>
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
|
<li v-if="app.availableFreqs.length > 0" style="flex-direction:column;gap:4px">
|
||||||
|
<span>Frequency</span>
|
||||||
|
<label class="checkbox" v-for="freq in app.availableFreqs" :key="freq">
|
||||||
|
<input type="checkbox" :value="freq" v-model="app.freqFilter">
|
||||||
|
<span>{{ freq }} MHz</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
<li v-if="filtersActive">
|
<li v-if="filtersActive">
|
||||||
<button class="small max" @click="clearFilters">Clear filters</button>
|
<button class="small max" @click="clearFilters">Clear filters</button>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
||||||
235
src/map.js
235
src/map.js
|
|
@ -1,13 +1,13 @@
|
||||||
import { unpack } from 'https://cdn.jsdelivr.net/npm/msgpackr@1.11.8/+esm';
|
import { unpack } from 'https://cdn.jsdelivr.net/npm/msgpackr@1.11.8/+esm';
|
||||||
import { createApp, reactive, ref, computed, watch, onMounted, toRaw } from '../lib/vue.esm-browser.js';
|
import { createApp, reactive, ref, computed, watch, onMounted, markRaw, shallowRef } from '../lib/vue.esm-browser.js';
|
||||||
import * as ntools from './node-utils.js';
|
import * as ntools from './node-utils.js';
|
||||||
const apiUrl = 'https://map.meshcore.dev/api/v1/nodes?binary=1&short=1';
|
const apiUrl = 'https://map.meshcore.dev/api/v1/nodes?binary=1&short=1';
|
||||||
|
|
||||||
function uint8ArrayToHex(uint8arr) {
|
function uint8ArrayToHex(uint8arr) {
|
||||||
const hexOctets = new Array(uint8arr.length); // is even faster (preallocates necessary array size), then use hexOctets[i] instead of .push()
|
const hexOctets = new Array(uint8arr.length);
|
||||||
|
|
||||||
for (let i = 0; i < uint8arr.length; ++i)
|
for (let i = 0; i < uint8arr.length; ++i)
|
||||||
hexOctets.push(ntools.byteToHex[uint8arr[i]]);
|
hexOctets[i] = ntools.byteToHex[uint8arr[i]];
|
||||||
|
|
||||||
return hexOctets.join('');
|
return hexOctets.join('');
|
||||||
}
|
}
|
||||||
|
|
@ -163,22 +163,21 @@ function escape(html) {
|
||||||
return html.replace(/[&<>"']/g, c => `&#${c.charCodeAt(0)};`)
|
return html.replace(/[&<>"']/g, c => `&#${c.charCodeAt(0)};`)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSvgIconUrl(text, color) {
|
const svgIconCache = new Map();
|
||||||
const svg = `
|
function getSvgIcon(text, color) {
|
||||||
<svg width="512" height="512" xmlns="http://www.w3.org/2000/svg" >
|
const cacheKey = text + '|' + color;
|
||||||
<style>
|
let icon = svgIconCache.get(cacheKey);
|
||||||
text { font: bold 150pt sans-serif; fill: #fff; }
|
if (icon) return icon;
|
||||||
</style>
|
|
||||||
<ellipse cx="50%" cy="50%" rx="50%" ry="50%" fill="${color}"/>
|
|
||||||
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle">${text}</text>
|
|
||||||
</svg>`;
|
|
||||||
|
|
||||||
return L.icon({
|
icon = L.divIcon({
|
||||||
iconUrl: URL.createObjectURL(new Blob([svg], { type: 'image/svg+xml' })),
|
html: `<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><ellipse cx="256" cy="256" rx="256" ry="256" fill="${color}"/><text x="256" y="256" dominant-baseline="central" text-anchor="middle" fill="#fff" font-size="150" font-weight="bold" font-family="sans-serif">${text}</text></svg>`,
|
||||||
|
className: 'svg-node-icon',
|
||||||
iconSize: [32, 32],
|
iconSize: [32, 32],
|
||||||
iconAnchor: [17, 17],
|
iconAnchor: [17, 17],
|
||||||
popupAnchor: [0, -16],
|
popupAnchor: [0, -16],
|
||||||
});
|
});
|
||||||
|
svgIconCache.set(cacheKey, icon);
|
||||||
|
return icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearLocationHash () {
|
function clearLocationHash () {
|
||||||
|
|
@ -282,12 +281,9 @@ const baseMaps = {
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
let params = { lat: 7, lon: 25, zoom: 3 };
|
|
||||||
let params = { lat: 7, lon: 25, zoom: 3 };
|
let params = { lat: 7, lon: 25, zoom: 3 };
|
||||||
|
|
||||||
const urlParams = Object.fromEntries(new URLSearchParams(location.search));
|
const urlParams = Object.fromEntries(new URLSearchParams(location.search));
|
||||||
if(Number(urlParams.lat) && Number(urlParams.lon) && Number(urlParams.zoom)) {
|
|
||||||
params = urlParams
|
|
||||||
if(Number(urlParams.lat) && Number(urlParams.lon) && Number(urlParams.zoom)) {
|
if(Number(urlParams.lat) && Number(urlParams.lon) && Number(urlParams.zoom)) {
|
||||||
params = urlParams
|
params = urlParams
|
||||||
}
|
}
|
||||||
|
|
@ -324,15 +320,11 @@ const icons = Object.fromEntries(['none', 'recent', 'stale', 'old', 'extinct'].m
|
||||||
createApp({
|
createApp({
|
||||||
setup() {
|
setup() {
|
||||||
const app = window.app = reactive({
|
const app = window.app = reactive({
|
||||||
nodes: [],
|
|
||||||
nodesByType: {},
|
|
||||||
filteredNodes: [],
|
|
||||||
nodes: [],
|
|
||||||
nodesByType: {},
|
|
||||||
filteredNodes: [],
|
|
||||||
search: '',
|
search: '',
|
||||||
link: '',
|
link: '',
|
||||||
nodeFilter: [],
|
nodeFilter: [],
|
||||||
|
freqFilter: [],
|
||||||
|
availableFreqs: [],
|
||||||
fromDate: '',
|
fromDate: '',
|
||||||
fromInsertDate: '',
|
fromInsertDate: '',
|
||||||
clusteringZoom: 12,
|
clusteringZoom: 12,
|
||||||
|
|
@ -342,6 +334,35 @@ createApp({
|
||||||
loading: false,
|
loading: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Keep large arrays outside Vue's deep reactivity
|
||||||
|
const nodesRef = shallowRef([]);
|
||||||
|
const nodesByTypeRef = shallowRef({});
|
||||||
|
const filteredNodesRef = shallowRef([]);
|
||||||
|
|
||||||
|
// Proxy access so templates/watchers can use app.nodes etc.
|
||||||
|
Object.defineProperty(app, 'nodes', {
|
||||||
|
get: () => nodesRef.value,
|
||||||
|
set: (v) => { nodesRef.value = v; },
|
||||||
|
});
|
||||||
|
Object.defineProperty(app, 'nodesByType', {
|
||||||
|
get: () => nodesByTypeRef.value,
|
||||||
|
set: (v) => { nodesByTypeRef.value = v; },
|
||||||
|
});
|
||||||
|
Object.defineProperty(app, 'filteredNodes', {
|
||||||
|
get: () => filteredNodesRef.value,
|
||||||
|
set: (v) => { filteredNodesRef.value = v; },
|
||||||
|
});
|
||||||
|
|
||||||
|
function attachClusterClickHandler(group) {
|
||||||
|
group.on('click', function(e) {
|
||||||
|
const marker = e.layer;
|
||||||
|
if(marker) {
|
||||||
|
ensurePopup(marker);
|
||||||
|
marker.openPopup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function refreshMap({ clusteringZoom = 0 } = {}) {
|
async function refreshMap({ clusteringZoom = 0 } = {}) {
|
||||||
markerClusterGroup.clearLayers();
|
markerClusterGroup.clearLayers();
|
||||||
const nodes = app.filteredNodes.length > 0 ? app.filteredNodes : app.nodes;
|
const nodes = app.filteredNodes.length > 0 ? app.filteredNodes : app.nodes;
|
||||||
|
|
@ -350,18 +371,23 @@ createApp({
|
||||||
|
|
||||||
if(clusteringZoom) {
|
if(clusteringZoom) {
|
||||||
markerClusterGroup = L.markerClusterGroup({
|
markerClusterGroup = L.markerClusterGroup({
|
||||||
disableClusteringAtZoom: clusteringZoom
|
disableClusteringAtZoom: clusteringZoom,
|
||||||
|
chunkedLoading: true,
|
||||||
});
|
});
|
||||||
|
attachClusterClickHandler(markerClusterGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
for(const node of nodes) {
|
const markers = new Array(nodes.length);
|
||||||
markerClusterGroup.addLayer(toRaw(node.marker));
|
for(let i = 0; i < nodes.length; i++) {
|
||||||
|
markers[i] = nodes[i].marker;
|
||||||
}
|
}
|
||||||
|
markerClusterGroup.addLayers(markers);
|
||||||
|
|
||||||
map.addLayer(markerClusterGroup);
|
map.addLayer(markerClusterGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showNode(node) {
|
function showNode(node) {
|
||||||
|
ensurePopup(node.marker);
|
||||||
node.marker.openPopup();
|
node.marker.openPopup();
|
||||||
map.flyTo(node.marker.getLatLng(), 19);
|
map.flyTo(node.marker.getLatLng(), 19);
|
||||||
app.search = '';
|
app.search = '';
|
||||||
|
|
@ -376,6 +402,7 @@ createApp({
|
||||||
|
|
||||||
function clearFilters() {
|
function clearFilters() {
|
||||||
app.nodeFilter = [1, 2, 3, 4];
|
app.nodeFilter = [1, 2, 3, 4];
|
||||||
|
app.freqFilter = [];
|
||||||
app.fromDate = '2025-03-01';
|
app.fromDate = '2025-03-01';
|
||||||
app.fromInsertDate = '2025-03-01';
|
app.fromInsertDate = '2025-03-01';
|
||||||
app.cluster = 12;
|
app.cluster = 12;
|
||||||
|
|
@ -386,13 +413,17 @@ createApp({
|
||||||
return days * 24 * 60 * 60 * 1000;
|
return days * 24 * 60 * 60 * 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const _now = Date.now();
|
||||||
|
const _extinct = _now - getDaysEpochMsec(20);
|
||||||
|
const _old = _now - getDaysEpochMsec(10);
|
||||||
|
const _stale = _now - getDaysEpochMsec(5);
|
||||||
|
|
||||||
function getNodeUpdateStatus(node) {
|
function getNodeUpdateStatus(node) {
|
||||||
if(node.source[0] !== 'u') return 'none';
|
if(node.source[0] !== 'u') return 'none';
|
||||||
const updateEpoch = new Date(node.updated_date).getTime();
|
const updateEpoch = new Date(node.updated_date).getTime();
|
||||||
if(updateEpoch < Date.now() - getDaysEpochMsec(20)) return 'extinct';
|
if(updateEpoch < _extinct) return 'extinct';
|
||||||
else if(updateEpoch < Date.now() - getDaysEpochMsec(10)) return 'old';
|
if(updateEpoch < _old) return 'old';
|
||||||
else if(updateEpoch < Date.now() - getDaysEpochMsec(5)) return 'stale';
|
if(updateEpoch < _stale) return 'stale';
|
||||||
|
|
||||||
return 'recent';
|
return 'recent';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -411,39 +442,58 @@ createApp({
|
||||||
app.loading = true;
|
app.loading = true;
|
||||||
const nodesReq = await fetch(apiUrl);
|
const nodesReq = await fetch(apiUrl);
|
||||||
const nodesBlob = await nodesReq.blob();
|
const nodesBlob = await nodesReq.blob();
|
||||||
app.nodes = unpack(await nodesBlob.arrayBuffer());
|
const nodes = unpack(await nodesBlob.arrayBuffer());
|
||||||
|
|
||||||
getPresets().then((presets) => {
|
getPresets().then((presets) => {
|
||||||
app.presets = presets;
|
app.presets = presets;
|
||||||
});
|
});
|
||||||
|
|
||||||
for(const node of app.nodes) {
|
const byType = {};
|
||||||
inflateNode(node);
|
const freqSet = new Set();
|
||||||
const updateStatus = getNodeUpdateStatus(node);
|
const CHUNK_SIZE = 2000;
|
||||||
|
|
||||||
let icon = icons[updateStatus][node.type.toString()];
|
for(let offset = 0; offset < nodes.length; offset += CHUNK_SIZE) {
|
||||||
|
const end = Math.min(offset + CHUNK_SIZE, nodes.length);
|
||||||
|
|
||||||
(app.nodesByType[node.type] ??= []).push(node);
|
// yield to browser between chunks so UI stays responsive
|
||||||
|
if(offset > 0) {
|
||||||
if(node.type === 1) {
|
await new Promise(r => setTimeout(r, 0));
|
||||||
const label = ntools.getNameIconLabel(node.adv_name);
|
|
||||||
const color = ntools.getColourForName(node.adv_name);
|
|
||||||
icon = getSvgIconUrl(label, color);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const marker = node.marker = L.marker(
|
for(let i = offset; i < end; i++) {
|
||||||
[node.lat, node.lon], { icon, title: node.adv_name }
|
const node = nodes[i];
|
||||||
);
|
inflateNode(node);
|
||||||
|
const updateStatus = getNodeUpdateStatus(node);
|
||||||
|
|
||||||
node.status = updateStatus;
|
let icon = icons[updateStatus][node.type.toString()];
|
||||||
node.preset = node.params;
|
|
||||||
node.coords = `${node.lat.toFixed(4)}, ${node.lon.toFixed(4)}`;
|
(byType[node.type] ??= []).push(node);
|
||||||
node.lastAdvertDate = new Date(node.last_advert);
|
|
||||||
node.insertDate = new Date(node.inserted_date);
|
if(node.type === 1) {
|
||||||
node.updatedDate = node.updated_date && new Date(node.updated_date);
|
const label = ntools.getNameIconLabel(node.adv_name);
|
||||||
const popup = L.popup({ minWidth: 350, maxWidth: 350, content: () => getNodePopupHTML(node) });
|
const color = ntools.getColourForName(node.adv_name);
|
||||||
marker.bindPopup(popup);
|
icon = getSvgIcon(label, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
const marker = node.marker = markRaw(L.marker(
|
||||||
|
[node.lat, node.lon], { icon, title: node.adv_name }
|
||||||
|
));
|
||||||
|
|
||||||
|
node.status = updateStatus;
|
||||||
|
node.preset = node.params;
|
||||||
|
node.coords = `${node.lat.toFixed(4)}, ${node.lon.toFixed(4)}`;
|
||||||
|
node.lastAdvertDate = new Date(node.last_advert);
|
||||||
|
node.insertDate = new Date(node.inserted_date);
|
||||||
|
node.updatedDate = node.updated_date && new Date(node.updated_date);
|
||||||
|
markerToNode.set(marker, node);
|
||||||
|
|
||||||
|
if(node.params?.freq) freqSet.add(Math.floor(node.params.freq));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nodesByTypeRef.value = byType;
|
||||||
|
nodesRef.value = nodes;
|
||||||
|
app.availableFreqs = [...freqSet].sort((a, b) => a - b);
|
||||||
}
|
}
|
||||||
catch(e) {
|
catch(e) {
|
||||||
alert('There was an error loading map nodes:', e);
|
alert('There was an error loading map nodes:', e);
|
||||||
|
|
@ -461,18 +511,33 @@ createApp({
|
||||||
watch(
|
watch(
|
||||||
[
|
[
|
||||||
() => app.nodeFilter,
|
() => app.nodeFilter,
|
||||||
|
() => app.freqFilter,
|
||||||
() => app.fromDate,
|
() => app.fromDate,
|
||||||
() => app.fromInsertDate,
|
() => app.fromInsertDate,
|
||||||
],
|
],
|
||||||
() => {
|
() => {
|
||||||
const fromDate = new Date(app.fromDate);
|
const fromDate = new Date(app.fromDate);
|
||||||
const fromInsertDate = new Date(app.fromInsertDate);
|
const fromInsertDate = new Date(app.fromInsertDate);
|
||||||
app.filteredNodes = app.nodeFilter
|
const byType = nodesByTypeRef.value;
|
||||||
.flatMap(type => app.nodesByType[type])
|
const hasFreqFilter = app.freqFilter.length > 0;
|
||||||
.filter(node => node && (node.updatedDate ? node.updatedDate > fromDate : node.insertDate > fromDate))
|
const freqSet = hasFreqFilter ? new Set(app.freqFilter) : null;
|
||||||
.filter(node => node && (node.insertDate > fromInsertDate));
|
|
||||||
console.log('refresh', app.nodeFilter, app.filteredNodes.length);
|
const result = [];
|
||||||
|
for(const type of app.nodeFilter) {
|
||||||
|
const typeNodes = byType[type];
|
||||||
|
if(!typeNodes) continue;
|
||||||
|
for(let i = 0; i < typeNodes.length; i++) {
|
||||||
|
const node = typeNodes[i];
|
||||||
|
if(node.updatedDate ? node.updatedDate <= fromDate : node.insertDate <= fromDate) continue;
|
||||||
|
if(node.insertDate <= fromInsertDate) continue;
|
||||||
|
if(hasFreqFilter && !(node.params?.freq && freqSet.has(Math.floor(node.params.freq)))) continue;
|
||||||
|
result.push(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
filteredNodesRef.value = result;
|
||||||
|
console.log('refresh', app.nodeFilter, result.length);
|
||||||
app.urlParams.nodes = app.nodeFilter.join(',');
|
app.urlParams.nodes = app.nodeFilter.join(',');
|
||||||
|
app.urlParams.freq = app.freqFilter.join(',');
|
||||||
app.urlParams.date = app.fromDate;
|
app.urlParams.date = app.fromDate;
|
||||||
app.urlParams.dateInsert = app.fromInsertDate;
|
app.urlParams.dateInsert = app.fromInsertDate;
|
||||||
refreshMap({ download: false });
|
refreshMap({ download: false });
|
||||||
|
|
@ -487,18 +552,33 @@ createApp({
|
||||||
const stats = computed(() => {
|
const stats = computed(() => {
|
||||||
const nodes = app.nodes;
|
const nodes = app.nodes;
|
||||||
|
|
||||||
if(!nodes) return [];
|
if(!nodes || !nodes.length) return [];
|
||||||
|
|
||||||
|
const byType = app.nodesByType;
|
||||||
|
const now = Date.now();
|
||||||
|
const msPerDay = 86400000;
|
||||||
|
const t1 = now - msPerDay;
|
||||||
|
const t7 = now - 7 * msPerDay;
|
||||||
|
const t30 = now - 30 * msPerDay;
|
||||||
|
let c1 = 0, c7 = 0, c30 = 0;
|
||||||
|
|
||||||
|
for(let i = 0; i < nodes.length; i++) {
|
||||||
|
const insertMs = nodes[i].insertDate.getTime();
|
||||||
|
if(insertMs > t1) c1++;
|
||||||
|
if(insertMs > t7) c7++;
|
||||||
|
if(insertMs > t30) c30++;
|
||||||
|
}
|
||||||
|
|
||||||
const result = [];
|
const result = [];
|
||||||
result.push(`
|
result.push(`
|
||||||
<span>total: <b>${nodes.length}</b></span> |
|
<span>total: <b>${nodes.length}</b></span> |
|
||||||
<i class="node-type pointer-help" title="Total client nodes">person</i><b>${nodes.filter(n => n.type === 1).length}</b> |
|
<i class="node-type pointer-help" title="Total client nodes">person</i><b>${(byType[1] || []).length}</b> |
|
||||||
<i class="node-type pointer-help" title="Total repeater nodes">cell_tower</i><b>${nodes.filter(n => n.type === 2).length}</b> |
|
<i class="node-type pointer-help" title="Total repeater nodes">cell_tower</i><b>${(byType[2] || []).length}</b> |
|
||||||
<i class="node-type pointer-help" title="Total room server nodes">forum</i><b>${nodes.filter(n => n.type === 3).length}</b>
|
<i class="node-type pointer-help" title="Total room server nodes">forum</i><b>${(byType[3] || []).length}</b>
|
||||||
`);
|
`);
|
||||||
result.push(`<span class="pointer-help" title="Nodes added in last 24 hours">24h: <b>${app.nodes.filter(n => isNewerThan(n.inserted_date, 1)).length}</b></span>`);
|
result.push(`<span class="pointer-help" title="Nodes added in last 24 hours">24h: <b>${c1}</b></span>`);
|
||||||
result.push(`<span class="pointer-help" title="Nodes added in last 7 days">7d: <b>${app.nodes.filter(n => isNewerThan(n.inserted_date, 7)).length}</b></span>`);
|
result.push(`<span class="pointer-help" title="Nodes added in last 7 days">7d: <b>${c7}</b></span>`);
|
||||||
result.push(`<span class="pointer-help" title="Nodes added in last 30 days">30d: <b>${app.nodes.filter(n => isNewerThan(n.inserted_date, 30)).length}</b></span>`);
|
result.push(`<span class="pointer-help" title="Nodes added in last 30 days">30d: <b>${c30}</b></span>`);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
|
|
@ -517,9 +597,26 @@ createApp({
|
||||||
});
|
});
|
||||||
|
|
||||||
let markerClusterGroup = L.markerClusterGroup({
|
let markerClusterGroup = L.markerClusterGroup({
|
||||||
disableClusteringAtZoom: app.clusteringZoom
|
disableClusteringAtZoom: app.clusteringZoom,
|
||||||
|
chunkedLoading: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Map markers back to nodes for lazy popup binding
|
||||||
|
const markerToNode = new WeakMap();
|
||||||
|
|
||||||
|
// Lazy popup binding — only create popup when a marker is clicked
|
||||||
|
function ensurePopup(marker) {
|
||||||
|
if(!marker._popupBound) {
|
||||||
|
const node = markerToNode.get(marker);
|
||||||
|
if(node) {
|
||||||
|
marker.bindPopup(
|
||||||
|
markRaw(L.popup({ minWidth: 350, maxWidth: 350, content: () => getNodePopupHTML(node) }))
|
||||||
|
);
|
||||||
|
marker._popupBound = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => app.urlParams,
|
() => app.urlParams,
|
||||||
() => {
|
() => {
|
||||||
|
|
@ -528,15 +625,14 @@ createApp({
|
||||||
{ deep: true }
|
{ deep: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
attachClusterClickHandler(markerClusterGroup);
|
||||||
|
|
||||||
map.on('moveend', function(e) {
|
map.on('moveend', function(e) {
|
||||||
const pos = map.getCenter();
|
const pos = map.getCenter();
|
||||||
const zoom = map.getZoom();
|
const zoom = map.getZoom();
|
||||||
app.urlParams.zoom = zoom;
|
app.urlParams.zoom = zoom;
|
||||||
app.urlParams.lat = pos.lat.toFixed(4);
|
app.urlParams.lat = pos.lat.toFixed(4);
|
||||||
app.urlParams.lon = pos.lng.toFixed(4);
|
app.urlParams.lon = pos.lng.toFixed(4);
|
||||||
app.urlParams.zoom = zoom;
|
|
||||||
app.urlParams.lat = pos.lat.toFixed(4);
|
|
||||||
app.urlParams.lon = pos.lng.toFixed(4);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|
@ -544,6 +640,9 @@ createApp({
|
||||||
if(urlParams.nodes) {
|
if(urlParams.nodes) {
|
||||||
app.nodeFilter = urlParams.nodes.split(',');
|
app.nodeFilter = urlParams.nodes.split(',');
|
||||||
}
|
}
|
||||||
|
if(urlParams.freq) {
|
||||||
|
app.freqFilter = urlParams.freq.split(',').map(Number);
|
||||||
|
}
|
||||||
if(urlParams.date) {
|
if(urlParams.date) {
|
||||||
app.fromDate = urlParams.date
|
app.fromDate = urlParams.date
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue