diff --git a/README.md b/README.md
index 3d665e5..9650e97 100644
--- a/README.md
+++ b/README.md
@@ -3,11 +3,11 @@ Official MeshCore Map (frontend)
## Installation
This is fully static and build-free site, cloning it to web location that can serve static content should be enough.
-It uses backend api deployed on https://map.meshcore.dev/api/v1/nodes
+It uses backend api deployed on https://meshcore.dev/api/v1/nodes
## Libraries used
* [Vue3](https://github.com/vuejs/core)
* [Beer.css](https://github.com/beercss/beercss)
* [Leaflet](https://github.com/Leaflet/Leaflet)
* [Leaflet.markercluster](https://github.com/Leaflet/Leaflet.markercluster)
-* [Material icons](https://fonts.google.com/icons)
+* [Material icons](https://fonts.google.com/icons)
\ No newline at end of file
diff --git a/index.html b/index.html
index 789bb20..cd7f099 100644
--- a/index.html
+++ b/index.html
@@ -88,7 +88,7 @@
today
- Inserted from
+ Last updated
@@ -98,6 +98,9 @@
+
+ Clear filters
+
diff --git a/src/map.js b/src/map.js
index 096213c..3b5ad71 100644
--- a/src/map.js
+++ b/src/map.js
@@ -1,18 +1,6 @@
import { createApp, reactive, ref, computed, watch, onMounted, toRaw } from '../lib/vue.esm-browser.js';
import * as ntools from './node-utils.js';
-
const apiUrl = 'https://map.meshcore.dev/api/v1/nodes';
-const keyOrder = ['adv_name', 'type', 'link', 'inserted_date', 'updated_date', 'public_key', 'coords', 'params' ]
-const humanLabel = {
- coords: 'Coordinates',
- adv_name: 'Name',
- inserted_date: 'Inserted',
- updated_date: 'Last updated',
- public_key: 'Public key',
- type: 'Node type',
- params: 'Radio params',
- link: 'Meshcore link',
-};
const types = {
'1': 'Client',
@@ -21,26 +9,39 @@ const types = {
'4': 'Sensor'
};
-const humanValue = {
- inserted_date(val) {
- return new Date(val).toLocaleString();
+const columnOrder = ['adv_name', 'type', 'link', 'inserted_date', 'updated_date', 'public_key', 'coords', 'params' ];
+const columns = {
+ coords: {
+ label: 'Coordinates',
+ value: (val) => `${val} `
},
- updated_date(val) {
- return new Date(val).toLocaleString();
+ adv_name: {
+ label: 'Name'
},
- coords(val) {
- return `${val} `;
+ inserted_date: {
+ label: 'Inserted date',
+ value: (val) => new Date(val).toLocaleString()
},
- type(val) {
- return types[val];
+ updated_date: {
+ label: 'Updated date',
+ value: (val) => new Date(val).toLocaleString()
},
- link(val) {
- return `Copy to clipboard `
+ public_key: {
+ label: 'Public key'
},
- params(val) {
- return Object.entries(val).map(([key, val]) => `${key}=${val}`).join(', ')
- }
-}
+ type: {
+ label: 'Node type',
+ value: (val) => types[val]
+ },
+ params: {
+ label: 'Radio params',
+ value: (val) => Object.entries(val).map(([key, val]) => `${key}=${val}`).join(', ')
+ },
+ link: {
+ label: 'Meshcore link',
+ value: (val) => `Copy to clipboard `
+ },
+};
function getSvgIconUrl(text, color) {
const svg = `
@@ -66,7 +67,7 @@ function clearLocationHash () {
function getTable(node) {
return ''+
- '' + keyOrder.flatMap(key => node[key] ? [`${humanLabel[key]} ${ humanValue[key] ? humanValue[key](node[key]) : node[key] } `] : [] ).join(' ') + ' '+
+ '' + columnOrder.flatMap(key => node[key] ? [`${columns[key].label} ${ columns[key].value ? columns[key].value(node[key]) : node[key] } `] : [] ).join(' ') + ' '+
'
';
}
@@ -102,13 +103,15 @@ const baseMaps = {
}),
};
-let initCoords = { lat: 7, lon: 25, zoom: 3 };
+let params = { lat: 7, lon: 25, zoom: 3 };
const urlParams = Object.fromEntries(new URLSearchParams(location.search));
-if(!(isNaN(urlParams.lat) || isNaN(urlParams.lon) || isNaN(urlParams.zoom))) {
- initCoords = urlParams
+if(Number(urlParams.lat) && Number(urlParams.lon) && Number(urlParams.zoom)) {
+ params = urlParams
}
+// console.log(params);
+
const map = window.leafletMap = leaflet.map('map', {
minZoom: 2,
maxBounds: [
@@ -117,7 +120,7 @@ const map = window.leafletMap = leaflet.map('map', {
],
layers: baseMaps[baseMapSelected],
zoomControl: false
-}).setView([initCoords.lat, initCoords.lon], initCoords.zoom);
+}).setView([params.lat, params.lon], params.zoom);
map.on('baselayerchange', function(ev) {
localStorage.setItem('baseMapSelected', ev.name);
@@ -143,90 +146,12 @@ createApp({
search: '',
link: '',
nodeFilter: [],
- fromDate: '2025-03-01',
+ fromDate: '',
clusteringZoom: 12,
+ urlParams
});
- const filtersActive = computed(() => app.filteredNodes.length && app.nodes.length !== app.filteredNodes.length);
-
- const stats = computed(() => {
- const nodes = app.nodes;
-
- if(!nodes) return [];
-
- const result = [];
- result.push(`
- total: ${nodes.length} |
- person ${nodes.filter(n => n.type === 1).length} |
- cell_tower ${nodes.filter(n => n.type === 2).length} |
- forum ${nodes.filter(n => n.type === 3).length}
- `);
- result.push(`24h: ${app.nodes.filter(n => isNewerThan(n.inserted_date, 1)).length} `);
- result.push(`7d: ${app.nodes.filter(n => isNewerThan(n.inserted_date, 7)).length} `);
- result.push(`30d: ${app.nodes.filter(n => isNewerThan(n.inserted_date, 30)).length} `);
-
- return result;
- });
-
- const searchResults = computed(() => {
- if(!app.search) { return [] }
-
- return app.filteredNodes.filter(
- node => node.adv_name.toLowerCase().includes(app.search.toLowerCase()) || node.public_key.startsWith(app.search)
- ).toSorted(
- (a, b) => a.adv_name.localeCompare(b.adv_name)
- ).slice(0, 20);
- });
-
- watch([
- () => app.nodeFilter,
- () => app.fromDate,
- ],
- () => {
- const fromDate = new Date(app.fromDate);
- app.filteredNodes = app.nodeFilter
- .flatMap(type => app.nodesByType[type])
- .filter(node => node && (node.updatedDate ? node.updatedDate > fromDate : node.insertDate > fromDate));
- console.log('refresh', app.nodeFilter, app.filteredNodes.length);
- refreshMap({ download: false });
- }
- );
-
- watch(() => app.clusteringZoom, () => {
- refreshMap({ download: false, clusteringZoom: app.clusteringZoom });
- });
-
- let markerClusterGroup = L.markerClusterGroup({
- disableClusteringAtZoom: app.clusteringZoom
- });
-
- async function refreshMap({ download = true, clusteringZoom = 0 } = {}) {
- if(download) {
- const nodesReq = await fetch(apiUrl);
- app.nodes = await nodesReq.json();
- for(const node of app.nodes) {
- let icon = icons[node.type.toString()];
- (app.nodesByType[node.type] ??= []).push(node);
-
- if(node.type === 1) {
- const label = ntools.getNameIconLabel(node.adv_name);
- const color = ntools.getColourForName(node.adv_name);
- icon = getSvgIconUrl(label, color);
- }
-
- const marker = node.marker = L.marker(
- [node.adv_lat, node.adv_lon], { icon, title: node.adv_name }
- );
-
- node.coords = `${node.adv_lat.toFixed(4)}, ${node.adv_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);
- const popup = L.popup({ minWidth: 350, maxWidth: 350, content: getTable(node) });
- marker.bindPopup(popup);
- }
- }
-
+ async function refreshMap({ clusteringZoom = 0 } = {}) {
markerClusterGroup.clearLayers();
const nodes = app.filteredNodes.length > 0 ? app.filteredNodes : app.nodes;
@@ -278,16 +203,128 @@ createApp({
return escapedSource.replace(highlightString, `${highlightString} `);
}
+ function clearFilters() {
+ app.nodeFilter = [1,2,3,4];
+ app.fromDate = '2025-03-01';
+ app.cluster = 12;
+ }
+
+ async function downloadNodes() {
+ const nodesReq = await fetch(apiUrl);
+ app.nodes = await nodesReq.json();
+ for(const node of app.nodes) {
+ let icon = icons[node.type.toString()];
+ (app.nodesByType[node.type] ??= []).push(node);
+
+ if(node.type === 1) {
+ const label = ntools.getNameIconLabel(node.adv_name);
+ const color = ntools.getColourForName(node.adv_name);
+ icon = getSvgIconUrl(label, color);
+ }
+
+ const marker = node.marker = L.marker(
+ [node.adv_lat, node.adv_lon], { icon, title: node.adv_name }
+ );
+
+ node.coords = `${node.adv_lat.toFixed(4)}, ${node.adv_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);
+ const popup = L.popup({ minWidth: 350, maxWidth: 350, content: getTable(node) });
+ marker.bindPopup(popup);
+ }
+ }
+
+ clearFilters();
+
+ const filtersActive = computed(() => app.filteredNodes.length && app.nodes.length !== app.filteredNodes.length);
+
+ watch(
+ [
+ () => app.nodeFilter,
+ () => app.fromDate,
+ ],
+ () => {
+ const fromDate = new Date(app.fromDate);
+ app.filteredNodes = app.nodeFilter
+ .flatMap(type => app.nodesByType[type])
+ .filter(node => node && (node.updatedDate ? node.updatedDate > fromDate : node.insertDate > fromDate));
+ console.log('refresh', app.nodeFilter, app.filteredNodes.length);
+ app.urlParams.nodes = app.nodeFilter.join(',');
+ app.urlParams.date = app.fromDate;
+ refreshMap({ download: false });
+ }
+ );
+
+ watch(() => app.clusteringZoom, () => {
+ app.urlParams.cluster = app.clusteringZoom;
+ refreshMap({ download: false, clusteringZoom: app.clusteringZoom });
+ });
+
+ const stats = computed(() => {
+ const nodes = app.nodes;
+
+ if(!nodes) return [];
+
+ const result = [];
+ result.push(`
+ total: ${nodes.length} |
+ person ${nodes.filter(n => n.type === 1).length} |
+ cell_tower ${nodes.filter(n => n.type === 2).length} |
+ forum ${nodes.filter(n => n.type === 3).length}
+ `);
+ result.push(`24h: ${app.nodes.filter(n => isNewerThan(n.inserted_date, 1)).length} `);
+ result.push(`7d: ${app.nodes.filter(n => isNewerThan(n.inserted_date, 7)).length} `);
+ result.push(`30d: ${app.nodes.filter(n => isNewerThan(n.inserted_date, 30)).length} `);
+
+ return result;
+ });
+
+ const searchResults = computed(() => {
+ if(!app.search) { return [] }
+
+ return app.filteredNodes.filter(
+ node => node.adv_name.toLowerCase().includes(app.search.toLowerCase()) || node.public_key.startsWith(app.search)
+ ).toSorted(
+ (a, b) => a.adv_name.localeCompare(b.adv_name)
+ ).slice(0, 20);
+ });
+
+ let markerClusterGroup = L.markerClusterGroup({
+ disableClusteringAtZoom: app.clusteringZoom
+ });
+
+ watch(
+ () => app.urlParams,
+ () => {
+ history.replaceState({}, '', `/?${new URLSearchParams(app.urlParams)}`);
+ },
+ { deep: true }
+ );
+
map.on('moveend', function(e) {
const pos = map.getCenter();
const zoom = map.getZoom();
- history.replaceState({}, '', `/?lat=${pos.lat.toFixed(4)}&lon=${pos.lng.toFixed(4)}&zoom=${zoom}`);
+ app.urlParams.zoom = zoom;
+ app.urlParams.lat = pos.lat.toFixed(4);
+ app.urlParams.lon = pos.lng.toFixed(4);
});
- refreshMap();
-
onMounted(() => {
- app.nodeFilter = ['1', '2', '3', '4'];
+
+ downloadNodes().then(() => {
+ if(urlParams.nodes) {
+ app.nodeFilter = urlParams.nodes.split(',');
+ }
+ if(urlParams.date) {
+ app.fromDate = urlParams.date
+ }
+ if(urlParams.cluster) {
+ app.clusteringZoom = urlParams.cluster;
+ }
+ refreshMap();
+ })
+
if(location.hash === '#add-new-node') {
dialogAddNode.value.showModal();
dialogAddNode.value.addEventListener('close', () => clearLocationHash());
@@ -298,7 +335,8 @@ createApp({
return {
app, refreshMap, addNode,
stats, searchResults, filtersActive,
- showNode, dialogAddNode, highlightString
+ showNode, dialogAddNode, highlightString,
+ clearFilters
}
},
-}).mount('#app')
\ No newline at end of file
+}).mount('#app')