Compare commits

2 Commits

Author SHA1 Message Date
5ec119b6c4 dont build on branch pushes 2025-04-16 23:09:46 -04:00
629825b53d commit work on map transition 2025-04-16 23:09:19 -04:00
11 changed files with 1721 additions and 494 deletions

View File

@ -1,5 +1,8 @@
name: Build Docker containers name: Build Docker containers
on: [push] on:
push:
branches:
- master
jobs: jobs:
Build: Build:

View File

@ -107,11 +107,6 @@ const optionsList = [
type: Boolean, type: Boolean,
description: "This option will drop all packets that have 'OK to MQTT' set to false.", description: "This option will drop all packets that have 'OK to MQTT' set to false.",
}, },
{
name: "debug-incoming-packets",
type: Boolean,
description: "This option will print out all known packets as they arrive.",
},
{ {
name: "drop-portnums-without-bitfield", name: "drop-portnums-without-bitfield",
type: Number, type: Number,
@ -230,7 +225,7 @@ const collectorEnabled = {
const decryptionKeys = options["decryption-keys"] ?? [ const decryptionKeys = options["decryption-keys"] ?? [
"1PG7OiApB1nwvP+rz05pAQ==", // add default "AQ==" decryption key "1PG7OiApB1nwvP+rz05pAQ==", // add default "AQ==" decryption key
]; ];
const logKnownPacketTypes = options["debug-incoming-packets"] ?? false; const logKnownPacketTypes = false;
const dropPacketsNotOkToMqtt = options["drop-packets-not-ok-to-mqtt"] ?? false; const dropPacketsNotOkToMqtt = options["drop-packets-not-ok-to-mqtt"] ?? false;
const dropPortnumsWithoutBitfield = options["drop-portnums-without-bitfield"] ?? null; const dropPortnumsWithoutBitfield = options["drop-portnums-without-bitfield"] ?? null;
const oldFirmwarePositionPrecision = options["old-firmware-position-precision"] ?? null; const oldFirmwarePositionPrecision = options["old-firmware-position-precision"] ?? null;
@ -792,7 +787,7 @@ client.on('message', async (topic, message) => {
break; break;
case Portnums.PortNum.NODEINFO_APP: case Portnums.PortNum.NODEINFO_APP:
callback = onNodeInfo; callback = onNodeInfo;
schema = Mesh.UserSchema; schema = Mesh.NodeInfoSchema;
break; break;
case Portnums.PortNum.WAYPOINT_APP: case Portnums.PortNum.WAYPOINT_APP:
callback = onWaypoint; callback = onWaypoint;
@ -1223,28 +1218,25 @@ async function onRouteDiscovery(envelope, payload) {
} }
async function onTelemetry(envelope, payload) { async function onTelemetry(envelope, payload) {
// we need to do some work on the packet to log it properly
const telemetryType = payload.variant.case
if(logKnownPacketTypes) { if(logKnownPacketTypes) {
console.log('TELEMETRY_APP', { console.log('TELEMETRY_APP', {
from: envelope.packet.from.toString(16), from: envelope.packet.from.toString(16),
type: telemetryType, telemetry: payload,
telemetry: payload.variant.value,
}); });
} }
payload = payload.variant.value
// data to update // data to update
const data = {}; const data = {};
// handle device metrics // handle device metrics
if (telemetryType === 'deviceMetrics'){ if(payload.deviceMetrics){
data.battery_level = payload.batteryLevel !== 0 ? payload.batteryLevel : null; data.battery_level = payload.deviceMetrics.batteryLevel !== 0 ? payload.deviceMetrics.batteryLevel : null;
data.voltage = payload.voltage !== 0 ? payload.voltage : null; data.voltage = payload.deviceMetrics.voltage !== 0 ? payload.deviceMetrics.voltage : null;
data.channel_utilization = payload.channelUtilization !== 0 ? payload.channelUtilization : null; data.channel_utilization = payload.deviceMetrics.channelUtilization !== 0 ? payload.deviceMetrics.channelUtilization : null;
data.air_util_tx = payload.airUtilTx !== 0 ? payload.airUtilTx : null; data.air_util_tx = payload.deviceMetrics.airUtilTx !== 0 ? payload.deviceMetrics.airUtilTx : null;
data.uptime_seconds = payload.uptimeSeconds !== 0 ? payload.uptimeSeconds : null; data.uptime_seconds = payload.deviceMetrics.uptimeSeconds !== 0 ? payload.deviceMetrics.uptimeSeconds : null;
// create device metric // create device metric
try { try {
@ -1283,20 +1275,20 @@ async function onTelemetry(envelope, payload) {
} }
// handle environment metrics // handle environment metrics
if(telemetryType === 'environmentMetrics'){ if(payload.environmentMetrics){
// get metric values // get metric values
const temperature = payload.temperature !== 0 ? payload.temperature : null; const temperature = payload.environmentMetrics.temperature !== 0 ? payload.environmentMetrics.temperature : null;
const relativeHumidity = payload.relativeHumidity !== 0 ? payload.relativeHumidity : null; const relativeHumidity = payload.environmentMetrics.relativeHumidity !== 0 ? payload.environmentMetrics.relativeHumidity : null;
const barometricPressure = payload.barometricPressure !== 0 ? payload.barometricPressure : null; const barometricPressure = payload.environmentMetrics.barometricPressure !== 0 ? payload.environmentMetrics.barometricPressure : null;
const gasResistance = payload.gasResistance !== 0 ? payload.gasResistance : null; const gasResistance = payload.environmentMetrics.gasResistance !== 0 ? payload.environmentMetrics.gasResistance : null;
const voltage = payload.voltage !== 0 ? payload.voltage : null; const voltage = payload.environmentMetrics.voltage !== 0 ? payload.environmentMetrics.voltage : null;
const current = payload.current !== 0 ? payload.current : null; const current = payload.environmentMetrics.current !== 0 ? payload.environmentMetrics.current : null;
const iaq = payload.iaq !== 0 ? payload.iaq : null; const iaq = payload.environmentMetrics.iaq !== 0 ? payload.environmentMetrics.iaq : null;
const windDirection = payload.windDirection; const windDirection = payload.environmentMetrics.windDirection;
const windSpeed = payload.windSpeed; const windSpeed = payload.environmentMetrics.windSpeed;
const windGust = payload.windGust; const windGust = payload.environmentMetrics.windGust;
const windLull = payload.windLull; const windLull = payload.environmentMetrics.windLull;
// set metrics to update on node table // set metrics to update on node table
data.temperature = temperature; data.temperature = temperature;
@ -1345,15 +1337,15 @@ async function onTelemetry(envelope, payload) {
} }
// handle power metrics // handle power metrics
if(telemetryType === 'powerMetrics'){ if(payload.powerMetrics){
// get metric values // get metric values
const ch1Voltage = payload.ch1Voltage !== 0 ? payload.ch1Voltage : null; const ch1Voltage = payload.powerMetrics.ch1Voltage !== 0 ? payload.powerMetrics.ch1Voltage : null;
const ch1Current = payload.ch1Current !== 0 ? payload.ch1Current : null; const ch1Current = payload.powerMetrics.ch1Current !== 0 ? payload.powerMetrics.ch1Current : null;
const ch2Voltage = payload.ch2Voltage !== 0 ? payload.ch2Voltage : null; const ch2Voltage = payload.powerMetrics.ch2Voltage !== 0 ? payload.powerMetrics.ch2Voltage : null;
const ch2Current = payload.ch2Current !== 0 ? payload.ch2Current : null; const ch2Current = payload.powerMetrics.ch2Current !== 0 ? payload.powerMetrics.ch2Current : null;
const ch3Voltage = payload.ch3Voltage !== 0 ? payload.ch3Voltage : null; const ch3Voltage = payload.powerMetrics.ch3Voltage !== 0 ? payload.powerMetrics.ch3Voltage : null;
const ch3Current = payload.ch3Current !== 0 ? payload.ch3Current : null; const ch3Current = payload.powerMetrics.ch3Current !== 0 ? payload.powerMetrics.ch3Current : null;
// create power metric // create power metric
try { try {

View File

@ -8,8 +8,10 @@
"name": "frontend", "name": "frontend",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@mapbox-controls/zoom": "^3.0.0",
"@tailwindcss/vite": "^4.1.4", "@tailwindcss/vite": "^4.1.4",
"@vueuse/core": "^13.1.0", "@vueuse/core": "^13.1.0",
"@watergis/mapbox-gl-legend": "^1.2.6",
"axios": "^1.8.4", "axios": "^1.8.4",
"chart.js": "^4.4.8", "chart.js": "^4.4.8",
"chartjs-adapter-moment": "^1.0.1", "chartjs-adapter-moment": "^1.0.1",
@ -19,6 +21,11 @@
"leaflet-geometryutil": "^0.10.3", "leaflet-geometryutil": "^0.10.3",
"leaflet-groupedlayercontrol": "^0.6.1", "leaflet-groupedlayercontrol": "^0.6.1",
"leaflet.markercluster": "^1.5.3", "leaflet.markercluster": "^1.5.3",
"mapbox-gl-controls": "^2.3.5",
"mapbox-gl-infobox": "^1.0.9",
"maplibre-gl": "^5.3.1",
"maplibre-gl-basemaps": "^0.1.3",
"maplibre-gl-opacity": "^1.8.0",
"moment": "^2.30.1", "moment": "^2.30.1",
"tailwindcss": "^4.1.4", "tailwindcss": "^4.1.4",
"vue": "^3.5.13", "vue": "^3.5.13",
@ -958,6 +965,163 @@
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@mapbox-controls/helpers": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@mapbox-controls/helpers/-/helpers-3.0.0.tgz",
"integrity": "sha512-XxCCKHNaxR8WeJM3syiXxBH/7Yfz7WytnDvhe/aQMgxYhJ68Z9aKAu6E4PtV0AI5dxyQb6UkdhQs8mg7Py0tjA==",
"license": "MIT",
"peerDependencies": {
"mapbox-gl": ">=1.0.0 <4.0.0"
}
},
"node_modules/@mapbox-controls/zoom": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@mapbox-controls/zoom/-/zoom-3.0.0.tgz",
"integrity": "sha512-3uGd53kedOnN9sUbz3Befup8MJAaMtSeSarVa8qTrWlilwacKcNSModowWjBNlPdp1K8iEmkh7gjwzrF+IEgcQ==",
"license": "MIT",
"dependencies": {
"@mapbox-controls/helpers": "3.0.0"
},
"peerDependencies": {
"mapbox-gl": ">=1.0.0 <4.0.0"
}
},
"node_modules/@mapbox/geojson-rewind": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz",
"integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==",
"license": "ISC",
"dependencies": {
"get-stream": "^6.0.1",
"minimist": "^1.2.6"
},
"bin": {
"geojson-rewind": "geojson-rewind"
}
},
"node_modules/@mapbox/geojson-rewind/node_modules/get-stream": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
"integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@mapbox/geojson-types": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@mapbox/geojson-types/-/geojson-types-1.0.2.tgz",
"integrity": "sha512-e9EBqHHv3EORHrSfbR9DqecPNn+AmuAoQxV6aL8Xu30bJMJR1o8PZLZzpk1Wq7/NfCbuhmakHTPYRhoqLsXRnw==",
"license": "ISC"
},
"node_modules/@mapbox/jsonlint-lines-primitives": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz",
"integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/@mapbox/mapbox-gl-style-spec": {
"version": "13.28.0",
"resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-style-spec/-/mapbox-gl-style-spec-13.28.0.tgz",
"integrity": "sha512-B8xM7Fp1nh5kejfIl4SWeY0gtIeewbuRencqO3cJDrCHZpaPg7uY+V8abuR+esMeuOjRl5cLhVTP40v+1ywxbg==",
"license": "ISC",
"dependencies": {
"@mapbox/jsonlint-lines-primitives": "~2.0.2",
"@mapbox/point-geometry": "^0.1.0",
"@mapbox/unitbezier": "^0.0.0",
"csscolorparser": "~1.0.2",
"json-stringify-pretty-compact": "^2.0.0",
"minimist": "^1.2.6",
"rw": "^1.3.3",
"sort-object": "^0.3.2"
},
"bin": {
"gl-style-composite": "bin/gl-style-composite.js",
"gl-style-format": "bin/gl-style-format.js",
"gl-style-migrate": "bin/gl-style-migrate.js",
"gl-style-validate": "bin/gl-style-validate.js"
}
},
"node_modules/@mapbox/mapbox-gl-style-spec/node_modules/@mapbox/unitbezier": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.0.tgz",
"integrity": "sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA==",
"license": "BSD-2-Clause"
},
"node_modules/@mapbox/mapbox-gl-style-spec/node_modules/json-stringify-pretty-compact": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-2.0.0.tgz",
"integrity": "sha512-WRitRfs6BGq4q8gTgOy4ek7iPFXjbra0H3PmDLKm2xnZ+Gh1HUhiKGgCZkSPNULlP7mvfu6FV/mOLhCarspADQ==",
"license": "MIT"
},
"node_modules/@mapbox/mapbox-gl-supported": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-2.0.1.tgz",
"integrity": "sha512-HP6XvfNIzfoMVfyGjBckjiAOQK9WfX0ywdLubuPMPv+Vqf5fj0uCbgBQYpiqcWZT6cbyyRnTSXDheT1ugvF6UQ==",
"license": "BSD-3-Clause",
"peer": true
},
"node_modules/@mapbox/point-geometry": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz",
"integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==",
"license": "ISC"
},
"node_modules/@mapbox/tiny-sdf": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.6.tgz",
"integrity": "sha512-qMqa27TLw+ZQz5Jk+RcwZGH7BQf5G/TrutJhspsca/3SHwmgKQ1iq+d3Jxz5oysPVYTGP6aXxCo5Lk9Er6YBAA==",
"license": "BSD-2-Clause"
},
"node_modules/@mapbox/unitbezier": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz",
"integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==",
"license": "BSD-2-Clause"
},
"node_modules/@mapbox/vector-tile": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz",
"integrity": "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==",
"license": "BSD-3-Clause",
"dependencies": {
"@mapbox/point-geometry": "~0.1.0"
}
},
"node_modules/@mapbox/whoots-js": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz",
"integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==",
"license": "ISC",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@maplibre/maplibre-gl-style-spec": {
"version": "23.1.0",
"resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-23.1.0.tgz",
"integrity": "sha512-R6/ihEuC5KRexmKIYkWqUv84Gm+/QwsOUgHyt1yy2XqCdGdLvlBWVWIIeTZWN4NGdwmY6xDzdSGU2R9oBLNg2w==",
"license": "ISC",
"dependencies": {
"@mapbox/jsonlint-lines-primitives": "~2.0.2",
"@mapbox/unitbezier": "^0.0.1",
"json-stringify-pretty-compact": "^4.0.0",
"minimist": "^1.2.8",
"quickselect": "^3.0.0",
"rw": "^1.3.3",
"tinyqueue": "^3.0.0"
},
"bin": {
"gl-style-format": "dist/gl-style-format.mjs",
"gl-style-migrate": "dist/gl-style-migrate.mjs",
"gl-style-validate": "dist/gl-style-validate.mjs"
}
},
"node_modules/@polka/url": { "node_modules/@polka/url": {
"version": "1.0.0-next.29", "version": "1.0.0-next.29",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
@ -1522,12 +1686,90 @@
"vite": "^5.2.0 || ^6" "vite": "^5.2.0 || ^6"
} }
}, },
"node_modules/@turf/distance": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/@turf/distance/-/distance-6.3.0.tgz",
"integrity": "sha512-basi24ssNFnH3iXPFjp/aNUrukjObiFWoIyDRqKyBJxVwVOwAWvfk4d38QQyBj5nDo5IahYRq/Q+T47/5hSs9w==",
"license": "MIT",
"dependencies": {
"@turf/helpers": "^6.3.0",
"@turf/invariant": "^6.3.0"
}
},
"node_modules/@turf/helpers": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-6.5.0.tgz",
"integrity": "sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw==",
"license": "MIT",
"funding": {
"url": "https://opencollective.com/turf"
}
},
"node_modules/@turf/invariant": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-6.5.0.tgz",
"integrity": "sha512-Wv8PRNCtPD31UVbdJE/KVAWKe7l6US+lJItRR/HOEW3eh+U/JwRCSUl/KZ7bmjM/C+zLNoreM2TU6OoLACs4eg==",
"license": "MIT",
"dependencies": {
"@turf/helpers": "^6.5.0"
},
"funding": {
"url": "https://opencollective.com/turf"
}
},
"node_modules/@types/estree": { "node_modules/@types/estree": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/geojson": {
"version": "7946.0.16",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
"license": "MIT"
},
"node_modules/@types/geojson-vt": {
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/@types/geojson-vt/-/geojson-vt-3.2.5.tgz",
"integrity": "sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==",
"license": "MIT",
"dependencies": {
"@types/geojson": "*"
}
},
"node_modules/@types/mapbox__point-geometry": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz",
"integrity": "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==",
"license": "MIT"
},
"node_modules/@types/mapbox__vector-tile": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@types/mapbox__vector-tile/-/mapbox__vector-tile-1.3.4.tgz",
"integrity": "sha512-bpd8dRn9pr6xKvuEBQup8pwQfD4VUyqO/2deGjfpe6AwC8YRlyEipvefyRJUSiCJTZuCb8Pl1ciVV5ekqJ96Bg==",
"license": "MIT",
"dependencies": {
"@types/geojson": "*",
"@types/mapbox__point-geometry": "*",
"@types/pbf": "*"
}
},
"node_modules/@types/pbf": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.5.tgz",
"integrity": "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==",
"license": "MIT"
},
"node_modules/@types/supercluster": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz",
"integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==",
"license": "MIT",
"dependencies": {
"@types/geojson": "*"
}
},
"node_modules/@types/web-bluetooth": { "node_modules/@types/web-bluetooth": {
"version": "0.0.21", "version": "0.0.21",
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
@ -1827,6 +2069,135 @@
"vue": "^3.5.0" "vue": "^3.5.0"
} }
}, },
"node_modules/@watergis/legend-symbol": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/@watergis/legend-symbol/-/legend-symbol-0.2.3.tgz",
"integrity": "sha512-bxW2nGUvL80/iLTAstvP0QRcpprKKyTOLFueDW+dD0g9PDIFYx4De6MrKp4QJPx9SmrlidPxslOB0mXZrENmUg==",
"dependencies": {
"@mapbox/mapbox-gl-style-spec": "^13.16.0"
}
},
"node_modules/@watergis/mapbox-gl-legend": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/@watergis/mapbox-gl-legend/-/mapbox-gl-legend-1.2.6.tgz",
"integrity": "sha512-0/AjT10uobNPxHntBYM7HjeoGj6fpOWsu5aBrnD3312Gn/63ko/XpjVY6P1YJ3Un4NhulTAtD4GOmpJyWDrEag==",
"license": "MIT",
"dependencies": {
"@mapbox/mapbox-gl-style-spec": "^13.25.0",
"@watergis/legend-symbol": "^0.2.3",
"axios": "^0.27.2",
"mapbox-gl": "^1.13.2"
}
},
"node_modules/@watergis/mapbox-gl-legend/node_modules/@mapbox/mapbox-gl-supported": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-1.5.0.tgz",
"integrity": "sha512-/PT1P6DNf7vjEEiPkVIRJkvibbqWtqnyGaBz3nfRdcxclNSnSdaLU5tfAgcD7I8Yt5i+L19s406YLl1koLnLbg==",
"license": "BSD-3-Clause",
"peerDependencies": {
"mapbox-gl": ">=0.32.1 <2.0.0"
}
},
"node_modules/@watergis/mapbox-gl-legend/node_modules/@mapbox/tiny-sdf": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-1.2.5.tgz",
"integrity": "sha512-cD8A/zJlm6fdJOk6DqPUV8mcpyJkRz2x2R+/fYcWDYG3oWbG7/L7Yl/WqQ1VZCjnL9OTIMAn6c+BC5Eru4sQEw==",
"license": "BSD-2-Clause"
},
"node_modules/@watergis/mapbox-gl-legend/node_modules/@mapbox/unitbezier": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.0.tgz",
"integrity": "sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA==",
"license": "BSD-2-Clause"
},
"node_modules/@watergis/mapbox-gl-legend/node_modules/axios": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.14.9",
"form-data": "^4.0.0"
}
},
"node_modules/@watergis/mapbox-gl-legend/node_modules/earcut": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz",
"integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==",
"license": "ISC"
},
"node_modules/@watergis/mapbox-gl-legend/node_modules/geojson-vt": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-3.2.1.tgz",
"integrity": "sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg==",
"license": "ISC"
},
"node_modules/@watergis/mapbox-gl-legend/node_modules/kdbush": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/kdbush/-/kdbush-3.0.0.tgz",
"integrity": "sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew==",
"license": "ISC"
},
"node_modules/@watergis/mapbox-gl-legend/node_modules/mapbox-gl": {
"version": "1.13.3",
"resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-1.13.3.tgz",
"integrity": "sha512-p8lJFEiqmEQlyv+DQxFAOG/XPWN0Wp7j/Psq93Zywz7qt9CcUKFYDBOoOEKzqe6gudHVJY8/Bhqw6VDpX2lSBg==",
"license": "SEE LICENSE IN LICENSE.txt",
"dependencies": {
"@mapbox/geojson-rewind": "^0.5.2",
"@mapbox/geojson-types": "^1.0.2",
"@mapbox/jsonlint-lines-primitives": "^2.0.2",
"@mapbox/mapbox-gl-supported": "^1.5.0",
"@mapbox/point-geometry": "^0.1.0",
"@mapbox/tiny-sdf": "^1.1.1",
"@mapbox/unitbezier": "^0.0.0",
"@mapbox/vector-tile": "^1.3.1",
"@mapbox/whoots-js": "^3.1.0",
"csscolorparser": "~1.0.3",
"earcut": "^2.2.2",
"geojson-vt": "^3.2.1",
"gl-matrix": "^3.2.1",
"grid-index": "^1.1.0",
"murmurhash-js": "^1.0.0",
"pbf": "^3.2.1",
"potpack": "^1.0.1",
"quickselect": "^2.0.0",
"rw": "^1.3.3",
"supercluster": "^7.1.0",
"tinyqueue": "^2.0.3",
"vt-pbf": "^3.1.1"
},
"engines": {
"node": ">=6.4.0"
}
},
"node_modules/@watergis/mapbox-gl-legend/node_modules/potpack": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz",
"integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==",
"license": "ISC"
},
"node_modules/@watergis/mapbox-gl-legend/node_modules/quickselect": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz",
"integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==",
"license": "ISC"
},
"node_modules/@watergis/mapbox-gl-legend/node_modules/supercluster": {
"version": "7.1.5",
"resolved": "https://registry.npmjs.org/supercluster/-/supercluster-7.1.5.tgz",
"integrity": "sha512-EulshI3pGUM66o6ZdH3ReiFcvHpM3vAigyK+vcxdjpJyEbIIrtbmBdY23mGgnI24uXiGFvrGq9Gkum/8U7vJWg==",
"license": "ISC",
"dependencies": {
"kdbush": "^3.0.0"
}
},
"node_modules/@watergis/mapbox-gl-legend/node_modules/tinyqueue": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz",
"integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==",
"license": "ISC"
},
"node_modules/asynckit": { "node_modules/asynckit": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@ -2009,6 +2380,12 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/csscolorparser": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/csscolorparser/-/csscolorparser-1.0.3.tgz",
"integrity": "sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==",
"license": "MIT"
},
"node_modules/csstype": { "node_modules/csstype": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
@ -2108,6 +2485,12 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/earcut": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.1.tgz",
"integrity": "sha512-0l1/0gOjESMeQyYaK5IDiPNvFeu93Z/cO0TjZh9eZ1vyCtZnA7KMZ8rQggpsJHIbGSdrqYq9OhuveadOVHCshw==",
"license": "ISC"
},
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.137", "version": "1.5.137",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.137.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.137.tgz",
@ -2377,6 +2760,12 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/geojson-vt": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-4.0.2.tgz",
"integrity": "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==",
"license": "ISC"
},
"node_modules/get-intrinsic": { "node_modules/get-intrinsic": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@ -2431,6 +2820,50 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/gl-matrix": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz",
"integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==",
"license": "MIT"
},
"node_modules/global-prefix": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-4.0.0.tgz",
"integrity": "sha512-w0Uf9Y9/nyHinEk5vMJKRie+wa4kR5hmDbEhGGds/kG1PwGLLHKRoNMeJOyCQjjBkANlnScqgzcFwGHgmgLkVA==",
"license": "MIT",
"dependencies": {
"ini": "^4.1.3",
"kind-of": "^6.0.3",
"which": "^4.0.0"
},
"engines": {
"node": ">=16"
}
},
"node_modules/global-prefix/node_modules/isexe": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz",
"integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==",
"license": "ISC",
"engines": {
"node": ">=16"
}
},
"node_modules/global-prefix/node_modules/which": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz",
"integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==",
"license": "ISC",
"dependencies": {
"isexe": "^3.1.1"
},
"bin": {
"node-which": "bin/which.js"
},
"engines": {
"node": "^16.13.0 || >=18.0.0"
}
},
"node_modules/globals": { "node_modules/globals": {
"version": "11.12.0", "version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
@ -2459,6 +2892,12 @@
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/grid-index": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/grid-index/-/grid-index-1.1.0.tgz",
"integrity": "sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==",
"license": "ISC"
},
"node_modules/has-symbols": { "node_modules/has-symbols": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
@ -2515,6 +2954,35 @@
"node": ">=18.18.0" "node": ">=18.18.0"
} }
}, },
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "BSD-3-Clause"
},
"node_modules/ini": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz",
"integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==",
"license": "ISC",
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
"node_modules/install": { "node_modules/install": {
"version": "0.13.0", "version": "0.13.0",
"resolved": "https://registry.npmjs.org/install/-/install-0.13.0.tgz", "resolved": "https://registry.npmjs.org/install/-/install-0.13.0.tgz",
@ -2663,6 +3131,12 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/json-stringify-pretty-compact": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz",
"integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==",
"license": "MIT"
},
"node_modules/json5": { "node_modules/json5": {
"version": "2.2.3", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
@ -2689,6 +3163,21 @@
"graceful-fs": "^4.1.6" "graceful-fs": "^4.1.6"
} }
}, },
"node_modules/kdbush": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz",
"integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==",
"license": "ISC"
},
"node_modules/kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/kolorist": { "node_modules/kolorist": {
"version": "1.8.0", "version": "1.8.0",
"resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz",
@ -2986,6 +3475,242 @@
"@jridgewell/sourcemap-codec": "^1.5.0" "@jridgewell/sourcemap-codec": "^1.5.0"
} }
}, },
"node_modules/mapbox-gl": {
"version": "2.15.0",
"resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-2.15.0.tgz",
"integrity": "sha512-fjv+aYrd5TIHiL7wRa+W7KjtUqKWziJMZUkK5hm8TvJ3OLeNPx4NmW/DgfYhd/jHej8wWL+QJBDbdMMAKvNC0A==",
"license": "SEE LICENSE IN LICENSE.txt",
"peer": true,
"dependencies": {
"@mapbox/geojson-rewind": "^0.5.2",
"@mapbox/jsonlint-lines-primitives": "^2.0.2",
"@mapbox/mapbox-gl-supported": "^2.0.1",
"@mapbox/point-geometry": "^0.1.0",
"@mapbox/tiny-sdf": "^2.0.6",
"@mapbox/unitbezier": "^0.0.1",
"@mapbox/vector-tile": "^1.3.1",
"@mapbox/whoots-js": "^3.1.0",
"csscolorparser": "~1.0.3",
"earcut": "^2.2.4",
"geojson-vt": "^3.2.1",
"gl-matrix": "^3.4.3",
"grid-index": "^1.1.0",
"kdbush": "^4.0.1",
"murmurhash-js": "^1.0.0",
"pbf": "^3.2.1",
"potpack": "^2.0.0",
"quickselect": "^2.0.0",
"rw": "^1.3.3",
"supercluster": "^8.0.0",
"tinyqueue": "^2.0.3",
"vt-pbf": "^3.1.3"
}
},
"node_modules/mapbox-gl-controls": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/mapbox-gl-controls/-/mapbox-gl-controls-2.3.5.tgz",
"integrity": "sha512-FP4c7WhNoE+5JicMkq8TSqJwaxEpkhJlsq4Ye1lIGBy7ubS/l6EZ15ZRumrICpDZGOMMlfKKCpaCo+Kt5omkTg==",
"deprecated": "migrated to monorepo: https://github.com/korywka/mapbox-controls",
"license": "MIT",
"dependencies": {
"@turf/distance": "6.3.0"
},
"peerDependencies": {
"mapbox-gl": ">=1.0.0 <3.0.0"
}
},
"node_modules/mapbox-gl-infobox": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/mapbox-gl-infobox/-/mapbox-gl-infobox-1.0.9.tgz",
"integrity": "sha512-vl4u308UMexH3qC883rsqMJ0AEP+wdoTOSqP+8QhLCTwGKZTpCMnQh/75saFXIJrG97CwXr74JR/lq2RAze87w==",
"license": "GPL-3.0",
"dependencies": {
"mapbox-gl": "^1.13.0"
}
},
"node_modules/mapbox-gl-infobox/node_modules/@mapbox/mapbox-gl-supported": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-1.5.0.tgz",
"integrity": "sha512-/PT1P6DNf7vjEEiPkVIRJkvibbqWtqnyGaBz3nfRdcxclNSnSdaLU5tfAgcD7I8Yt5i+L19s406YLl1koLnLbg==",
"license": "BSD-3-Clause",
"peerDependencies": {
"mapbox-gl": ">=0.32.1 <2.0.0"
}
},
"node_modules/mapbox-gl-infobox/node_modules/@mapbox/tiny-sdf": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-1.2.5.tgz",
"integrity": "sha512-cD8A/zJlm6fdJOk6DqPUV8mcpyJkRz2x2R+/fYcWDYG3oWbG7/L7Yl/WqQ1VZCjnL9OTIMAn6c+BC5Eru4sQEw==",
"license": "BSD-2-Clause"
},
"node_modules/mapbox-gl-infobox/node_modules/@mapbox/unitbezier": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.0.tgz",
"integrity": "sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA==",
"license": "BSD-2-Clause"
},
"node_modules/mapbox-gl-infobox/node_modules/earcut": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz",
"integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==",
"license": "ISC"
},
"node_modules/mapbox-gl-infobox/node_modules/geojson-vt": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-3.2.1.tgz",
"integrity": "sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg==",
"license": "ISC"
},
"node_modules/mapbox-gl-infobox/node_modules/kdbush": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/kdbush/-/kdbush-3.0.0.tgz",
"integrity": "sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew==",
"license": "ISC"
},
"node_modules/mapbox-gl-infobox/node_modules/mapbox-gl": {
"version": "1.13.3",
"resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-1.13.3.tgz",
"integrity": "sha512-p8lJFEiqmEQlyv+DQxFAOG/XPWN0Wp7j/Psq93Zywz7qt9CcUKFYDBOoOEKzqe6gudHVJY8/Bhqw6VDpX2lSBg==",
"license": "SEE LICENSE IN LICENSE.txt",
"dependencies": {
"@mapbox/geojson-rewind": "^0.5.2",
"@mapbox/geojson-types": "^1.0.2",
"@mapbox/jsonlint-lines-primitives": "^2.0.2",
"@mapbox/mapbox-gl-supported": "^1.5.0",
"@mapbox/point-geometry": "^0.1.0",
"@mapbox/tiny-sdf": "^1.1.1",
"@mapbox/unitbezier": "^0.0.0",
"@mapbox/vector-tile": "^1.3.1",
"@mapbox/whoots-js": "^3.1.0",
"csscolorparser": "~1.0.3",
"earcut": "^2.2.2",
"geojson-vt": "^3.2.1",
"gl-matrix": "^3.2.1",
"grid-index": "^1.1.0",
"murmurhash-js": "^1.0.0",
"pbf": "^3.2.1",
"potpack": "^1.0.1",
"quickselect": "^2.0.0",
"rw": "^1.3.3",
"supercluster": "^7.1.0",
"tinyqueue": "^2.0.3",
"vt-pbf": "^3.1.1"
},
"engines": {
"node": ">=6.4.0"
}
},
"node_modules/mapbox-gl-infobox/node_modules/potpack": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz",
"integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==",
"license": "ISC"
},
"node_modules/mapbox-gl-infobox/node_modules/quickselect": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz",
"integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==",
"license": "ISC"
},
"node_modules/mapbox-gl-infobox/node_modules/supercluster": {
"version": "7.1.5",
"resolved": "https://registry.npmjs.org/supercluster/-/supercluster-7.1.5.tgz",
"integrity": "sha512-EulshI3pGUM66o6ZdH3ReiFcvHpM3vAigyK+vcxdjpJyEbIIrtbmBdY23mGgnI24uXiGFvrGq9Gkum/8U7vJWg==",
"license": "ISC",
"dependencies": {
"kdbush": "^3.0.0"
}
},
"node_modules/mapbox-gl-infobox/node_modules/tinyqueue": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz",
"integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==",
"license": "ISC"
},
"node_modules/mapbox-gl/node_modules/earcut": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz",
"integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==",
"license": "ISC",
"peer": true
},
"node_modules/mapbox-gl/node_modules/geojson-vt": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-3.2.1.tgz",
"integrity": "sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg==",
"license": "ISC",
"peer": true
},
"node_modules/mapbox-gl/node_modules/quickselect": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz",
"integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==",
"license": "ISC",
"peer": true
},
"node_modules/mapbox-gl/node_modules/tinyqueue": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz",
"integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==",
"license": "ISC",
"peer": true
},
"node_modules/maplibre-gl": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.3.1.tgz",
"integrity": "sha512-Ihx+oUUSsZkjMou1Cw5J6silE+5OtFFQSPslWF9+7v4yFC/XDHrpsORYO9lWE4KZI0djCEUpZQJpkpnMArAbeA==",
"license": "BSD-3-Clause",
"dependencies": {
"@mapbox/geojson-rewind": "^0.5.2",
"@mapbox/jsonlint-lines-primitives": "^2.0.2",
"@mapbox/point-geometry": "^0.1.0",
"@mapbox/tiny-sdf": "^2.0.6",
"@mapbox/unitbezier": "^0.0.1",
"@mapbox/vector-tile": "^1.3.1",
"@mapbox/whoots-js": "^3.1.0",
"@maplibre/maplibre-gl-style-spec": "^23.1.0",
"@types/geojson": "^7946.0.16",
"@types/geojson-vt": "3.2.5",
"@types/mapbox__point-geometry": "^0.1.4",
"@types/mapbox__vector-tile": "^1.3.4",
"@types/pbf": "^3.0.5",
"@types/supercluster": "^7.1.3",
"earcut": "^3.0.1",
"geojson-vt": "^4.0.2",
"gl-matrix": "^3.4.3",
"global-prefix": "^4.0.0",
"kdbush": "^4.0.2",
"murmurhash-js": "^1.0.0",
"pbf": "^3.3.0",
"potpack": "^2.0.0",
"quickselect": "^3.0.0",
"supercluster": "^8.0.1",
"tinyqueue": "^3.0.0",
"vt-pbf": "^3.1.3"
},
"engines": {
"node": ">=16.14.0",
"npm": ">=8.1.0"
},
"funding": {
"url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1"
}
},
"node_modules/maplibre-gl-basemaps": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/maplibre-gl-basemaps/-/maplibre-gl-basemaps-0.1.3.tgz",
"integrity": "sha512-7+PF7yBoPYo7hU+a81JWuikrRA757HA6YqsLgEL47syaSiwjSLicfZQk8kGlaRS3NFI7Q7fBSO3T14C8tqbQTQ==",
"license": "ISC",
"peerDependencies": {
"maplibre-gl": ">=1"
}
},
"node_modules/maplibre-gl-opacity": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/maplibre-gl-opacity/-/maplibre-gl-opacity-1.8.0.tgz",
"integrity": "sha512-qPQ0LL3kTx+77bkrO81QLdkazxoxxxOahpPhx7ALhgGvgugaHe5G+V7yACCdf9RWdOKCJXGxhZf80G0TKEbbPA==",
"license": "MIT"
},
"node_modules/math-intrinsics": { "node_modules/math-intrinsics": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@ -3016,6 +3741,15 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/mitt": { "node_modules/mitt": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
@ -3049,6 +3783,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/murmurhash-js": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz",
"integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==",
"license": "MIT"
},
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.11", "version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@ -3153,6 +3893,19 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/pbf": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/pbf/-/pbf-3.3.0.tgz",
"integrity": "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==",
"license": "BSD-3-Clause",
"dependencies": {
"ieee754": "^1.1.12",
"resolve-protobuf-schema": "^2.1.0"
},
"bin": {
"pbf": "bin/pbf"
}
},
"node_modules/perfect-debounce": { "node_modules/perfect-debounce": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
@ -3207,6 +3960,12 @@
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
} }
}, },
"node_modules/potpack": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/potpack/-/potpack-2.0.0.tgz",
"integrity": "sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw==",
"license": "ISC"
},
"node_modules/pretty-ms": { "node_modules/pretty-ms": {
"version": "9.2.0", "version": "9.2.0",
"resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.2.0.tgz", "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.2.0.tgz",
@ -3223,12 +3982,33 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/protocol-buffers-schema": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz",
"integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==",
"license": "MIT"
},
"node_modules/proxy-from-env": { "node_modules/proxy-from-env": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/quickselect": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz",
"integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==",
"license": "ISC"
},
"node_modules/resolve-protobuf-schema": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz",
"integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==",
"license": "MIT",
"dependencies": {
"protocol-buffers-schema": "^3.3.1"
}
},
"node_modules/rfdc": { "node_modules/rfdc": {
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
@ -3288,6 +4068,12 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/rw": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
"integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
"license": "BSD-3-Clause"
},
"node_modules/semver": { "node_modules/semver": {
"version": "6.3.1", "version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
@ -3349,6 +4135,34 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/sort-asc": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/sort-asc/-/sort-asc-0.1.0.tgz",
"integrity": "sha512-jBgdDd+rQ+HkZF2/OHCmace5dvpos/aWQpcxuyRs9QUbPRnkEJmYVo81PIGpjIdpOcsnJ4rGjStfDHsbn+UVyw==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/sort-desc": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/sort-desc/-/sort-desc-0.1.1.tgz",
"integrity": "sha512-jfZacW5SKOP97BF5rX5kQfJmRVZP5/adDUTY8fCSPvNcXDVpUEe2pr/iKGlcyZzchRJZrswnp68fgk3qBXgkJw==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/sort-object": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/sort-object/-/sort-object-0.3.2.tgz",
"integrity": "sha512-aAQiEdqFTTdsvUFxXm3umdo04J7MRljoVGbBlkH7BgNsMvVNAJyGj7C/wV1A8wHWAJj/YikeZbfuCKqhggNWGA==",
"dependencies": {
"sort-asc": "^0.1.0",
"sort-desc": "^0.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-js": { "node_modules/source-map-js": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@ -3381,6 +4195,15 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/supercluster": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz",
"integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==",
"license": "ISC",
"dependencies": {
"kdbush": "^4.0.2"
}
},
"node_modules/superjson": { "node_modules/superjson": {
"version": "2.2.2", "version": "2.2.2",
"resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.2.tgz", "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.2.tgz",
@ -3409,6 +4232,12 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/tinyqueue": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz",
"integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==",
"license": "ISC"
},
"node_modules/totalist": { "node_modules/totalist": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
@ -3632,6 +4461,17 @@
"vite": "^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0" "vite": "^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0"
} }
}, },
"node_modules/vt-pbf": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz",
"integrity": "sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==",
"license": "MIT",
"dependencies": {
"@mapbox/point-geometry": "0.1.0",
"@mapbox/vector-tile": "^1.3.1",
"pbf": "^3.2.1"
}
},
"node_modules/vue": { "node_modules/vue": {
"version": "3.5.13", "version": "3.5.13",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz", "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz",

View File

@ -9,8 +9,10 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@mapbox-controls/zoom": "^3.0.0",
"@tailwindcss/vite": "^4.1.4", "@tailwindcss/vite": "^4.1.4",
"@vueuse/core": "^13.1.0", "@vueuse/core": "^13.1.0",
"@watergis/mapbox-gl-legend": "^1.2.6",
"axios": "^1.8.4", "axios": "^1.8.4",
"chart.js": "^4.4.8", "chart.js": "^4.4.8",
"chartjs-adapter-moment": "^1.0.1", "chartjs-adapter-moment": "^1.0.1",
@ -20,6 +22,11 @@
"leaflet-geometryutil": "^0.10.3", "leaflet-geometryutil": "^0.10.3",
"leaflet-groupedlayercontrol": "^0.6.1", "leaflet-groupedlayercontrol": "^0.6.1",
"leaflet.markercluster": "^1.5.3", "leaflet.markercluster": "^1.5.3",
"mapbox-gl-controls": "^2.3.5",
"mapbox-gl-infobox": "^1.0.9",
"maplibre-gl": "^5.3.1",
"maplibre-gl-basemaps": "^0.1.3",
"maplibre-gl-opacity": "^1.8.0",
"moment": "^2.30.1", "moment": "^2.30.1",
"tailwindcss": "^4.1.4", "tailwindcss": "^4.1.4",
"vue": "^3.5.13", "vue": "^3.5.13",

View File

@ -0,0 +1,304 @@
export default class LayersControl {
constructor(options) {
// This div will hold all the checkboxes and their labels
this._container = document.createElement('div');
this._container.style.padding = '12px';
this._container.classList.add(
// Built-in classes for consistency
'maplibregl-ctrl',
'maplibregl-ctrl-group',
// Custom class, see later
'layers-control',
);
// Options
this._options = options;
// Map inputs
this._mapInputs = {};
this.activeMap = null;
// Control inputs
this._controlInputs = {};
this._activeControl = {};
this._previousLayers = [];
}
_createGroupHeader(name) {
const label = document.createElement('label');
const text = document.createElement('span');
text.innerText = name;
text.classList.add('layer-group-name');
label.appendChild(text);
return label;
}
_createGroupContainer() {
const div = document.createElement('div');
div.classList.add('layer-group');
return div;
}
_createGroup(name, data) {
const container = this._createGroupContainer();
const header = this._createGroupHeader(name);
container.appendChild(header);
for (const key of Object.keys(data.layers)) {
let input = null;
if (data.type === 'radio') {
input = this._createLabeledRadio(`${name}_${key}`, key, '_control_selection');
} else if (data.type === 'checkbox') {
input = this._createLabeledCheckbox(`${name}_${key}`, key, '_control_selection');
}
container.appendChild(input);
}
return container;
}
applyDefaults() {
for (const name of Object.keys(this._options.controls)) {
const config = this._options.controls[name];
if (typeof config.default === 'string') {
this._controlInputs[name][config.default].checked = true;
this._doControlAction(this._options.controls[name].layers[config.default]);
} else if (Array.isArray(config.default)) {
for (const id of config.default) {
this._controlInputs[name][id].checked = true;
this._doControlAction(this._options.controls[name].layers[id], true);
}
}
}
}
_createSeparator() {
const div = document.createElement('div');
div.classList.add('layers-control-separator');
return div;
}
_getLayers() {
return this._map.getLayersOrder().filter(name => !this._mapInputs.hasOwnProperty(name));
}
_getVisibleLayers() {
return this._getLayers().filter(layer => {
const visibility = this._map.getLayer(layer).getLayoutProperty('visibility') ?? 'visible';
return visibility === 'visible';
});
}
_doControlAction(config, value) {
switch(config.type) {
case 'source_filter':
const source = this._map.getSource(config.source);
const data = config.getter().filter(config.filter);
source.setData({
type: 'FeatureCollection',
features: data,
});
break;
case 'toggle_element':
if (config.element ?? false) {
document.querySelector(config.element).style.display = value ? 'block' : 'none';
}
break;
case 'layer_toggle':
if (value) {
this._showLayer(config.layer);
} else {
this._hideLayer(config.layer);
}
break;
case 'layer_control':
this._previousLayers = this._getVisibleLayers();
if (config.hideAllExcept ?? false) {
const layersToRemove = this._getVisibleLayers().filter(name => !config.hideAllExcept.includes(name));
for (const id of layersToRemove) {
this._hideLayer(id);
}
for (const id of config.hideAllExcept) {
this._showLayer(id);
}
}
if (config.disableCluster ?? false) {
const style = this._map.getStyle()
style.sources[config.disableCluster].cluster = false;
this._map.setStyle(style);
}
break;
}
}
_disableLayerTransitions(name) {
const layerType = this._map.getLayer(name)?.type;
if (layerType === 'fill' || layerType === 'line' || layerType === 'circle') {
this._map.setPaintProperty(name, `${layerType}-opacity-transition`, { duration: 0 });
} else if (layerType === 'symbol') {
this._map.setPaintProperty(name, 'text-opacity-transition', { duration: 0 });
this._map.setPaintProperty(name, 'icon-opacity-transition', { duration: 0 });
this._map.setPaintProperty(name, 'text-opacity', 1);
this._map.setPaintProperty(name, 'icon-opacity', 1);
const layout = this._map.getLayoutProperty(name, 'text-field');
if (layout) {
this._map.setLayoutProperty(name, 'text-optional', false);
}
}
}
_hideLayer(name) {
console.debug('hideLayer: ', name);
this._disableLayerTransitions(name);
this._map.setLayoutProperty(name, 'visibility', 'none');
}
_showLayer(name) {
console.debug('showLayer: ', name);
this._disableLayerTransitions(name);
this._map.setLayoutProperty(name, 'visibility', 'visible');
}
_revertControlAction(config) {
switch(config.type) {
case 'source_filter':
const source = this._map.getSource(config.source);
source.setData({
type: 'FeatureCollection',
features: config.getter(),
});
break;
case 'toggle_element':
if (config.element ?? false) {
const el = document.querySelector(config.element).style.display;
document.querySelector(config.element).style.display = (el === 'block' ? 'none' : 'block');
}
break;
case 'layer_toggle':
const visibility = this._map.getLayer(config.layer).getLayoutProperty('visibility') ?? 'visible';
if (visibility === 'visible') {
this._hideLayer(config.layer);
} else {
this._showLayer(config.layer);
}
break;
case 'layer_control':
// i want to hide all layers in current layers that weren't in previous layers, so all layers in current layers that are not in previous layers
const toHide = this._getVisibleLayers().filter(layer => !this._previousLayers.includes(layer));
const toShow = this._getLayers().filter(layer => this._previousLayers.includes(layer));
for (const id of toShow) {
this._showLayer(id);
}
for (const id of toHide) {
this._hideLayer(id);
}
this._previousLayers = [];
if (config.disableCluster ?? false) {
const style = this._map.getStyle()
style.sources[config.disableCluster].cluster = true;
this._map.setStyle(style);
}
break;
}
}
_createLabeledRadio(id, display, type) {
const label = document.createElement('label');
const text = document.createElement('span');
text.innerText = ` ${display}`;
const input = document.createElement('input');
input.type = 'radio';
input.name = type;
input.value = id;
input.addEventListener('change', (e) => {
if (type === '_map_control_selection') {
if (this.activeMap != e.target.value) {
this._map.setLayoutProperty(this.activeMap, 'visibility', 'none');
this._map.setLayoutProperty(e.target.value, 'visibility', 'visible');
this.activeMap = e.target.value;
}
} else if (type === '_control_selection') {
const [ parent, child ] = id.split('_');
const layerConfig = this._options.controls[parent].layers[child];
this._revertControlAction(this._activeControl[parent] ?? {type: ''});
this._doControlAction(layerConfig);
this._activeControl[parent] = layerConfig;
}
});
if (type === '_map_control_selection') {
this._mapInputs[id] = input;
} else if (type === '_control_selection') {
const [ parent, child ] = id.split('_');
this._controlInputs[parent][child] = input;
}
label.appendChild(input);
label.appendChild(text);
return label;
}
_createLabeledCheckbox(id, display, type) {
const label = document.createElement('label');
const text = document.createElement('span');
text.innerText = ` ${display}`;
const input = document.createElement('input');
input.type = 'checkbox';
input.name = id;
input.addEventListener('change', (e) => {
if (type === '_control_selection') {
const [ parent, child ] = id.split('_');
const layerConfig = this._options.controls[parent].layers[child];
this._revertControlAction(this._activeControl[parent] ?? {type: ''});
this._doControlAction(layerConfig, e.target.checked);
this._activeControl[parent] = layerConfig;
}
});
if (type === '_control_selection') {
const [ parent, child ] = id.split('_');
this._controlInputs[parent][child] = input;
}
label.appendChild(input);
label.appendChild(text);
return label;
}
onAdd(map) {
this._map = map;
map.on('load', () => {
// create map changer
for (const map of this._options.maps) {
const radioSelection = this._createLabeledRadio(map.id, map.display, '_map_control_selection');
this._container.appendChild(radioSelection);
}
this._container.appendChild(this._createSeparator());
// Create the checkboxes and add them to the container
for (const key of Object.keys(this._options.controls)) {
this._controlInputs[key] = {};
this._activeControl[parent] = {};
const group = this._createGroup(key, this._options.controls[key]);
this._container.appendChild(group);
}
this._options.maps.forEach(({ id, tiles, sourceExtraParams = {}, layerExtraParams = {} }) => {
map.addSource(id, {
...sourceExtraParams,
type: 'raster',
tiles,
});
map.addLayer({ ...layerExtraParams, id, source: id, type: 'raster' });
if (this._options.initialMap === id) {
map.setLayoutProperty(id, 'visibility', 'visible');
this._mapInputs[id].checked = true;
this.activeMap = id;
} else {
map.setLayoutProperty(id, 'visibility', 'none');
}
});
});
return this._container;
}
onRemove(map) {
// Not sure why we have to do this ourselves since we are not the ones
// adding us to the map.
// Copied from their example so keeping it in.
this._container.parentNode.removeChild(this._container);
// This might be to help garbage collection? Also from their example.
// Or perhaps to ensure calls to this object do not change the map still
// after removal.
this._map = undefined;
}
}

View File

@ -0,0 +1,19 @@
export default class LegendControl {
onAdd(map) {
this._map = map;
this._container = document.createElement('div');
this._container.style.backgroundColor = 'white';
this._container.style.padding = '12px';
this._container.innerHTML = `<div style="margin-bottom:6px;"><strong>Legend</strong></div>`
+ `<div style="display:flex"><div class="icon-mqtt-connected" style="width:12px;height:12px;margin-right:4px;margin-top:auto;margin-bottom:auto;"></div> MQTT Connected</div>`
+ `<div style="display:flex"><div class="icon-mqtt-disconnected" style="width:12px;height:12px;margin-right:4px;margin-top:auto;margin-bottom:auto;"></div> MQTT Disconnected</div>`
+ `<div style="display:flex"><div class="icon-offline" style="width:12px;height:12px;margin-right:4px;margin-top:auto;margin-bottom:auto;"></div> Offline Too Long</div>`;
this._container.className = 'maplibregl-ctrl maplibregl-ctrl-group legend-control';
return this._container;
}
onRemove() {
this._container.parentNode.removeChild(this._container);
this._map = undefined;
}
}

View File

@ -1,13 +1,50 @@
@import "tailwindcss"; @import "tailwindcss";
@import "leaflet/dist/leaflet.css"; @import "maplibre-gl/dist/maplibre-gl.css";
@import "leaflet.markercluster/dist/MarkerCluster.css"; @import 'maplibre-gl-basemaps/lib/basemaps.css';
@import "leaflet.markercluster/dist/MarkerCluster.Default.css"; @import '@mapbox-controls/zoom/src/index.css';
@import "leaflet-groupedlayercontrol/dist/leaflet.groupedlayercontrol.min.css"; @import "mapbox-gl/dist/mapbox-gl.css";
@import "mapbox-gl-controls/lib/controls.css";
.layers-control {
line-height: 1.5;
}
.layer-group-name {
margin-bottom: .2em;
margin-left: 3px;
font-weight: 700;
}
.layer-group {
margin-bottom: .5em;
}
.layers-control label {
font-size: 1.08333em;
display: block;
}
.layers-control radio {
margin-top: 2px;
position: relative;
top: 1px;
}
.layers-control-separator {
border-top: 1px solid #ddd;
height: 0;
margin: 5px -10px 5px -6px;
}
/* used to prevent ui flicker before vuejs loads */ /* used to prevent ui flicker before vuejs loads */
[v-cloak] { [v-cloak] {
display: none; display: none;
} }
.maplibregl-ctrl-basemaps:not(.closed) > .hidden {
display: unset !important;
}
.icon-mqtt-connected { .icon-mqtt-connected {
background-color: #16a34a; background-color: #16a34a;
border-radius: 25px; border-radius: 25px;

View File

@ -2,7 +2,7 @@ import { useStorage } from '@vueuse/core';
// static // static
export const CURRENT_ANNOUNCEMENT_ID = 1; export const CURRENT_ANNOUNCEMENT_ID = 1;
export const BASE_PATH = ''; export const BASE_PATH = 'http://localhost:9090';
// string // string
export const temperatureFormat = useStorage('temperature-display', 'fahrenheit'); export const temperatureFormat = useStorage('temperature-display', 'fahrenheit');

View File

@ -1,18 +1,6 @@
import moment from 'moment';
import 'leaflet/dist/leaflet.js';
const L = window.L;
import 'leaflet.markercluster/dist/leaflet.markercluster.js';
import 'leaflet-groupedlayercontrol/dist/leaflet.groupedlayercontrol.min.js';
import {
escapeString,
findNodeById,
getRegionFrequencyRange,
hasNodeUplinkedToMqttRecently,
formatPositionPrecision,
getTerrainProfileImage,
} from './utils.js';
import { state } from './store.js';
import { markRaw } from 'vue'; import { markRaw } from 'vue';
import moment from 'moment';
import { hasNodeUplinkedToMqttRecently, getRegionFrequencyRange, escapeString, formatPositionPrecision } from './utils.js';
// state/config // state/config
let instance = null; let instance = null;
@ -24,67 +12,60 @@ export function getMap() {
} }
export const layerGroups = { export const layerGroups = {
nodes: new L.LayerGroup(), nodes: {},
neighbors: new L.LayerGroup(), neighbors: {},
waypoints: new L.LayerGroup(), waypoints: {},
nodePositionHistory: new L.LayerGroup(), nodePositionHistory: {},
nodeNeighbors: new L.LayerGroup(), nodeNeighbors: {},
nodesRouter: L.markerClusterGroup({ nodesRouter: {},
showCoverageOnHover: false, nodesClustered: {},
disableClusteringAtZoom: 10, // zoom level where node clustering is disabled legend: {},
}), none: {},
nodesClustered: L.markerClusterGroup({
showCoverageOnHover: false,
disableClusteringAtZoom: 10, // zoom level where node clustering is disabled
}),
legend: new L.LayerGroup(),
none: new L.LayerGroup(),
};
export const tileLayers = {
"OpenStreetMap": L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 22, // increase from 18 to 22
attribution: 'Tiles &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> | Data from <a target="_blank" href="https://meshtastic.org/docs/software/integrations/mqtt/">Meshtastic</a>',
}),
"OpenTopoMap": L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', {
maxZoom: 17, // open topo map doesn't have tiles closer than this
attribution: 'Tiles &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
}),
"Esri Satellite": L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
maxZoom: 21, // esri doesn't have tiles closer than this
attribution: 'Tiles &copy; <a href="https://developers.arcgis.com/documentation/mapping-apis-and-services/deployment/basemap-attribution/">Esri</a> | Data from <a target="_blank" href="https://meshtastic.org/docs/software/integrations/mqtt/">Meshtastic</a>'
}),
"Google Satellite": L.tileLayer('https://{s}.google.com/vt/lyrs=s&x={x}&y={y}&z={z}', {
maxZoom: 21,
subdomains: ['mt0', 'mt1', 'mt2', 'mt3'],
attribution: 'Tiles &copy; Google | Data from <a target="_blank" href="https://meshtastic.org/docs/software/integrations/mqtt/">Meshtastic</a>'
}),
"Google Hybrid": L.tileLayer('https://{s}.google.com/vt/lyrs=s,h&x={x}&y={y}&z={z}', {
maxZoom: 21,
subdomains: ['mt0', 'mt1', 'mt2', 'mt3'],
attribution: 'Tiles &copy; Google | Data from <a target="_blank" href="https://meshtastic.org/docs/software/integrations/mqtt/">Meshtastic</a>'
}),
};
export const icons = {
mqttConnected: L.divIcon({
className: 'icon-mqtt-connected',
iconSize: [16, 16], // increase from 12px to 16px to make hover easier
}),
mqttDisconnected: L.divIcon({
className: 'icon-mqtt-disconnected',
iconSize: [16, 16], // increase from 12px to 16px to make hover easier
}),
offline: L.divIcon({
className: 'icon-offline',
iconSize: [16, 16], // increase from 12px to 16px to make hover easier
}),
positionHistory: L.divIcon({
className: 'icon-position-history',
iconSize: [16, 16], // increase from 12px to 16px to make hover easier
}),
}; };
export const tileLayers = [
{
id: 'OpenStreetMap',
display: 'Open Street Map',
tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'],
sourceExtraParams: {
tileSize: 256,
attribution: 'Tiles &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> | Data from <a target="_blank" href="https://meshtastic.org/docs/software/integrations/mqtt/">Meshtastic</a>',
maxzoom: 22
}
},
{
id: 'OpenTopoMap',
display: 'Open Topo Map',
tiles: ['https://tile.opentopomap.org/{z}/{x}/{y}.png'],
sourceExtraParams: {
tileSize: 256,
attribution: 'Tiles &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>"',
maxzoom: 17
}
},
{
id: 'esriSatellite',
display: 'ESRI Satellite',
tiles: ['https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}'],
sourceExtraParams: {
tileSize: 256,
attribution: 'Tiles &copy; <a href="https://developers.arcgis.com/documentation/mapping-apis-and-services/deployment/basemap-attribution/">Esri</a> | Data from <a target="_blank" href="https://meshtastic.org/docs/software/integrations/mqtt/">Meshtastic</a>',
maxzoom: 21
}
},
{
id: 'googleSatellite',
display: 'Google Satellite',
tiles: ['https://mt0.google.com/vt/lyrs=s&x={x}&y={y}&z={z}'],
sourceExtraParams: {
tileSize: 256,
attribution: 'Tiles &copy; Google | Data from <a target="_blank" href="https://meshtastic.org/docs/software/integrations/mqtt/">Meshtastic</a>',
maxzoom: 21
}
}
];
export const icons = {mqttConnected: '#16a34a', mqttDisconnected: '#2563eb', offline: '#dc2626', positionHistory: '#a855f7'};
// tooltips // tooltips
export function getTooltipContentForWaypoint(waypoint) { export function getTooltipContentForWaypoint(waypoint) {
@ -182,109 +163,47 @@ export function getTooltipContentForNode(node) {
} }
export function getNeighbourTooltipContent(type, node, neighbourNode, distanceInMeters, snr) { export function getNeighbourTooltipContent(type, node, neighbourNode, distanceInMeters, snr) {
// default to showing distance in meters return '';
let distance = `${distanceInMeters} meters`;
// scale to distance in kms
if (distanceInMeters >= 1000) {
const distanceInKilometers = (distanceInMeters / 1000).toFixed(2);
distance = `${distanceInKilometers} kilometers`;
}
const terrainImageUrl = type === 'weHeard' ? getTerrainProfileImage(node, neighbourNode) : getTerrainProfileImage(neighbourNode, node);
const templates = {
'weHeard': `<b>[${escapeString(node.short_name)}] ${escapeString(node.long_name)}</b> heard <b>[${escapeString(neighbourNode.short_name)}] ${escapeString(neighbourNode.long_name)}</b>`
+ `<br/>SNR: ${snr}dB`
+ `<br/>Distance: ${distance}`
+ `<br/><br/>ID: ${node.node_id} heard ${neighbourNode.node_id}`
+ `<br/>Hex ID: ${node.node_id_hex} heard ${neighbourNode.node_id_hex}`
+ (node.neighbours_updated_at ? `<br/>Updated: ${moment(new Date(node.neighbours_updated_at)).fromNow()}` : '')
+ `<br/><br/>Terrain images from <a href="http://www.heywhatsthat.com" target="_blank">HeyWhatsThat.com</a>`
+ `<br/><a href="${terrainImageUrl}" target="_blank"><img src="${terrainImageUrl}" width="100%"></a>`,
'theyHeard': `<b>[${escapeString(neighbourNode.short_name)}] ${escapeString(neighbourNode.long_name)}</b> heard <b>[${escapeString(node.short_name)}] ${escapeString(node.long_name)}</b>`
+ `<br/>SNR: ${snr}dB`
+ `<br/>Distance: ${distance}`
+ `<br/><br/>ID: ${neighbourNode.node_id} heard ${node.node_id}`
+ `<br/>Hex ID: ${neighbourNode.node_id_hex} heard ${node.node_id_hex}`
+ (neighbourNode.neighbours_updated_at ? `<br/>Updated: ${moment(new Date(neighbourNode.neighbours_updated_at)).fromNow()}` : '')
+ `<br/><br/>Terrain images from <a href="http://www.heywhatsthat.com" target="_blank">HeyWhatsThat.com</a>`
+ `<br/><a href="${terrainImageUrl}" target="_blank"><img src="${terrainImageUrl}" width="100%"></a>`,
}
return templates[type];
} }
// cleanup // cleanup
export function clearAllNodes() { export function clearAllNodes() {
layerGroups.nodes.clearLayers();
layerGroups.nodesClustered.clearLayers();
layerGroups.nodesRouter.clearLayers();
}; };
export function clearAllNeighbors() { export function clearAllNeighbors() {
layerGroups.neighbors.clearLayers();
}; };
export function clearAllWaypoints() { export function clearAllWaypoints() {
layerGroups.waypoints.clearLayers();
}; };
export function clearAllPositionHistory() { export function clearAllPositionHistory() {
layerGroups.nodePositionHistory.clearLayers();
}; };
export function cleanUpPositionHistory() { export function cleanUpPositionHistory() {
// close tooltips and popups
closeAllPopups();
closeAllTooltips();
// setup node neighbours layer
layerGroups.nodePositionHistory.clearLayers();
layerGroups.nodePositionHistory.removeFrom(getMap());
layerGroups.nodePositionHistory.addTo(getMap());
}; };
export function closeAllTooltips() { export function closeAllTooltips() {
getMap().eachLayer(function(layer) {
if (layer.options.pane === 'tooltipPane') {
layer.removeFrom(getMap());
}
});
}; };
export function closeAllPopups() { export function closeAllPopups() {
getMap().eachLayer(function(layer) {
if (layer.options.pane === 'popupPane') {
layer.removeFrom(getMap());
}
});
}; };
export function cleanUpNodeNeighbors() { export function cleanUpNodeNeighbors() {
// close tooltips and popups
closeAllPopups();
closeAllTooltips();
// setup node neighbours layer
layerGroups.nodeNeighbors.clearLayers();
layerGroups.nodeNeighbors.removeFrom(getMap());
layerGroups.nodeNeighbors.addTo(getMap());
}; };
export function clearNodeOutline() { export function clearNodeOutline() {
if (state.selectedNodeOutlineCircle) {
state.selectedNodeOutlineCircle.removeFrom(getMap());
state.selectedNodeOutlineCircle = null;
}
}; };
export function clearMap() { export function clearMap() {
closeAllPopups();
closeAllTooltips();
clearAllNodes();
clearAllNeighbors();
clearAllWaypoints();
clearNodeOutline();
cleanUpNodeNeighbors();
}; };

View File

@ -5,6 +5,7 @@ export const state = reactive({
nodes: [], nodes: [],
waypoints: [], waypoints: [],
nodeMarkers: {}, nodeMarkers: {},
waypointMarkers: [],
// state // state
searchText: '', searchText: '',
@ -17,6 +18,7 @@ export const state = reactive({
selectedNodeToShowNeighbours: null, selectedNodeToShowNeighbours: null,
selectedNodeToShowNeighbours: null, selectedNodeToShowNeighbours: null,
selectedNodeToShowNeighboursType: null, selectedNodeToShowNeighboursType: null,
currentPopup: null,
// position history // position history
selectedNodeToShowPositionHistory: null, selectedNodeToShowPositionHistory: null,

View File

@ -11,12 +11,11 @@ import Announcement from '../components/Announcement.vue';
import axios from 'axios'; import axios from 'axios';
import moment from 'moment'; import moment from 'moment';
import 'leaflet/dist/leaflet'; import maplibregl from 'maplibre-gl';
const L = window.L; import BasemapsControl from 'maplibre-gl-basemaps';
import 'leaflet-geometryutil'; import OpacityControl from 'maplibre-gl-opacity';
import 'leaflet-arrowheads'; import LegendControl from '../LegendControl.js';
import 'leaflet.markercluster'; import LayerControl from '../LayerControl.js';
import 'leaflet-groupedlayercontrol/dist/leaflet.groupedlayercontrol.min.js';
import { onMounted, useTemplateRef, ref, watch, markRaw } from 'vue'; import { onMounted, useTemplateRef, ref, watch, markRaw } from 'vue';
import { state } from '../store.js'; import { state } from '../store.js';
import { import {
@ -290,6 +289,10 @@ window.showNodeNeighboursThatWeHeard = function(id) {
} }
} }
function isValidCoordinates(lat, lng) {
return lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180;
}
function onNodesUpdated(nodes) { function onNodesUpdated(nodes) {
const now = moment(); const now = moment();
state.nodes = []; state.nodes = [];
@ -319,12 +322,6 @@ function onNodesUpdated(nodes) {
node.latitude = node.latitude / 10000000; node.latitude = node.latitude / 10000000;
node.longitude = node.longitude / 10000000; node.longitude = node.longitude / 10000000;
// wrap longitude for shortest path, everything to left of australia should be shown on the right
let longitude = parseFloat(node.longitude);
if (longitude <= 100) {
longitude += 360;
}
// icon based on mqtt connection state // icon based on mqtt connection state
let icon = icons.mqttDisconnected; let icon = icons.mqttDisconnected;
@ -342,116 +339,35 @@ function onNodesUpdated(nodes) {
icon = icons.mqttConnected; icon = icons.mqttConnected;
} }
if (!isValidCoordinates(node.latitude, node.longitude)) {
continue;
}
// create node marker // create node marker
const marker = L.marker([node.latitude, longitude], { const marker = {
icon: icon, type: 'Feature',
tagName: node.node_id, properties: {
// we want to show online nodes above offline, but without needing to use separate layer groups id: node.node_id,
zIndexOffset: nodeHasUplinkedToMqttRecently ? 1000 : -1000, role: node.role_name,
}).on('click', function(event) { layer: 'nodes',
// close tooltip on click to prevent tooltip and popup showing at same time color: icon,
event.target.closeTooltip(); },
}); geometry: {
type: 'Point',
// add marker to node layer groups coordinates: [node.longitude, node.latitude]
marker.addTo(layerGroups.nodes);
layerGroups.nodesClustered.addLayer(marker);
// add markers for routers and repeaters to routers layer group
if (node.role_name === 'ROUTER'
|| node.role_name === 'ROUTER_CLIENT'
|| node.role_name === 'ROUTER_LATE'
|| node.role_name === 'REPEATER') {
layerGroups.nodesRouter.addLayer(marker);
}
// show tooltip on desktop only
if (!isMobile()) {
marker.bindTooltip(getTooltipContentForNode(node), {
interactive: true,
});
}
// Push node marker to cache
state.nodeMarkers[node.node_id] = marker;
// show node info tooltip when clicking node marker
marker.on('click', function(event) {
// close all other popups and tooltips
closeAllTooltips();
closeAllPopups();
// find node
const node = findNodeById(event.target.options.tagName);
if (!node){
return;
} }
};
// show position precision outline // add maker to cache
showNodeOutline(node.node_id); state.nodeMarkers[node.node_id] = marker;
// open tooltip for node
getMap().openTooltip(getTooltipContentForNode(node), event.target.getLatLng(), {
interactive: true, // allow clicking buttons inside tooltip
permanent: true, // don't auto dismiss when clicking buttons inside tooltip
});
});
} }
for(const node of nodes) { // set data
// find current node const source = getMap().getSource('nodes');
const currentNode = findNodeMarkerById(node.node_id); if (source) {
if (!currentNode) { source.setData({
continue; type: 'FeatureCollection',
} features: Object.values(state.nodeMarkers),
});
// add node neighbours
var polylineOffset = 0;
const neighbours = node.neighbours ?? [];
for( const neighbour of neighbours) {
// fixme: skipping zero snr? saw some crazy long neighbours with zero snr...
if(neighbour.snr === 0){
continue;
}
const neighbourNode = findNodeById(neighbour.node_id);
if (!neighbourNode) {
continue;
}
const neighbourNodeMarker = findNodeMarkerById(neighbour.node_id);
if (neighbourNodeMarker) {
// calculate distance in meters between nodes (rounded to 2 decimal places)
const distanceInMeters = currentNode.getLatLng().distanceTo(neighbourNodeMarker.getLatLng()).toFixed(2);
// don't show this neighbour connection if further than config allows
if (neighboursMaxDistance.value != null && parseFloat(distanceInMeters) > neighboursMaxDistance.value) {
continue;
}
// add neighbour line to map
const line = L.polyline([
currentNode.getLatLng(),
neighbourNodeMarker.getLatLng(),
], {
color: '#2563eb',
opacity: 0.75,
offset: polylineOffset,
}).addTo(layerGroups.neighbors);
// increase offset so next neighbour does not overlay other neighbours from self
polylineOffset += 2;
const tooltip = getNeighbourTooltipContent('weHeard', node, neighbourNode, distanceInMeters, neighbour.snr);
line.bindTooltip(tooltip, {
sticky: true,
opacity: 1,
interactive: true,
}).bindPopup(tooltip).on('click', function(event) {
// close tooltip on click to prevent tooltip and popup showing at same time
event.target.closeTooltip();
});
}
}
} }
} }
@ -487,42 +403,37 @@ function onWaypointsUpdated(waypoints) {
waypoint.latitude = waypoint.latitude / 10000000; waypoint.latitude = waypoint.latitude / 10000000;
waypoint.longitude = waypoint.longitude / 10000000; waypoint.longitude = waypoint.longitude / 10000000;
// wrap longitude for shortest path, everything to left of australia should be shown on the right
let longitude = parseFloat(waypoint.longitude);
if (longitude <= 100) {
longitude += 360;
}
// determine emoji to show as marker icon // determine emoji to show as marker icon
const emoji = waypoint.icon === 0 ? 128205 : waypoint.icon; const emoji = waypoint.icon === 0 ? 128205 : waypoint.icon;
const emojiText = String.fromCodePoint(emoji); const emojiText = String.fromCodePoint(emoji);
let tooltip = getTooltipContentForWaypoint(waypoint); if (!isValidCoordinates(node.latitude, node.longitude)) {
continue;
// create waypoint marker
const marker = L.marker([waypoint.latitude, longitude], {
icon: L.divIcon({
className: 'waypoint-label',
iconSize: [26, 26], // increase from 12px to 26px
html: emojiText,
}),
}).bindPopup(tooltip).on('click', function(event) {
// close tooltip on click to prevent tooltip and popup showing at same time
event.target.closeTooltip();
});
// show tooltip on desktop only
if (!isMobile()) {
marker.bindTooltip(tooltip, {
interactive: true,
});
} }
// add marker to waypoints layer groups
marker.addTo(layerGroups.waypoints)
// add to cache
state.waypoints.push(waypoint); state.waypoints.push(waypoint);
// create waypoint marker
const marker = {
type: 'Feature',
properties: {
layer: 'waypoints',
},
geometry: {
type: 'Point',
coordinates: [waypoint.longitude, waypoint.latitude]
}
};
// add maker to cache
state.waypointMarkers.push(marker);
}
// set data
const source = getMap().getSource('waypoints');
if (source) {
source.setData({
type: 'FeatureCollection',
features: Object.values(state.waypointMarkers),
});
} }
} }
@ -661,19 +572,6 @@ function reload(goToNodeId, zoom) {
onNodesUpdated(response.data.nodes); onNodesUpdated(response.data.nodes);
// hide loading // hide loading
state.loading = false; state.loading = false;
// fetch waypoints (after awaiting nodes, so we can use nodes cache in waypoint tooltips)
axios.get(buildPath('/api/v1/waypoints')).then(response => {
onWaypointsUpdated(response.data.waypoints);
});
// go to node id if provided
if (goToNodeId) {
// go to node
if(goToNode(goToNodeId, false, zoom)) {
return;
}
// fallback to showing node details since we can't go to the node
window.showNodeDetails(goToNodeId);
}
}); });
} }
@ -732,159 +630,363 @@ function onSearchResultNodeClick(node) {
} }
onMounted(() => { onMounted(() => {
// set map bounds to be a little more than full size to prevent panning off screen
const bounds = [ const bounds = [
[-100, 70], // top left [-100, 70], // top left
[100, 500], // bottom right [100, 500], // bottom right
]; ];
// create map positioned over AU and NZ
setMap(L.map(mapEl.value, {
maxBounds: bounds,
}));
// set view
getMap().setView([-15, 150], 2);
// remove leaflet link
getMap().attributionControl.setPrefix('');
// use tile layer based on config const map = new maplibregl.Map({
const selectedTileLayer = tileLayers[selectedTileLayerName.value] || tileLayers['OpenStreetMap']; container: mapEl.value,
selectedTileLayer.addTo(getMap()); attributionControl: false,
glyphs: 'https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf',
// handle baselayerchange to update tile layer preference style: {
getMap().on('baselayerchange', function(event) { version: 8,
selectedTileLayerName.value = event.name; sources: {},
layers: [],
glyphs: 'https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf',
},
//center: [-15, 150],
center: [0, 0],
zoom: 2,
fadeDuration: 0,
renderWorldCopies: false
//maxBounds: [[-180, -85], [180, 85]]
}); });
setMap(map);
map.addControl(new maplibregl.AttributionControl({ compact: true }), 'bottom-right');
// Add zoom and rotation controls to the map.
map.addControl(new maplibregl.NavigationControl({
visualizePitch: false,
visualizeRoll: false,
showZoom: true,
showCompass: false,
}), 'top-left');
const layerControl = new LayerControl({
maps: tileLayers,
initialMap: 'OpenStreetMap',
controls: {
Nodes: {
type: 'radio',
default: 'Clustered',
layers: {
'All': {
type: 'layer_control',
hideAllExcept: ['nodes'],
disableCluster: 'nodes',
},
'Routers': {
type: 'source_filter',
source: 'nodes',
getter: function() { return Object.values(state.nodeMarkers) },
filter: function (node) { return ['ROUTER', 'ROUTER_CLIENT', 'ROUTER_LATE', 'REPEATER'].includes(node.properties.role); }
},
'Clustered': {
type: 'layer_control',
hideAllExcept: ['clusters', 'unclustered-points', 'cluster-count'],
},
'None': {
type: 'layer_control',
hideAllExcept: [],
},
},
},
Overlays: {
type: 'checkbox',
default: ['Legend', 'Position History'],
layers: {
'Legend': {
type: 'toggle_element',
element: '.legend-control',
},
'Neighbors': {},
'Waypoints': {
type: 'layer_toggle',
layer: 'waypoints',
},
'Position History': {},
}
},
}
});
map.addControl(layerControl, 'top-right');
map.addControl(new LegendControl(), 'bottom-left');
// create legend map.doubleClickZoom.disable(); // optional but recommended for clean UX
const legend = L.control({position: 'bottomleft'});
legend.onAdd = function (map) { map.on('load', () => {
const div = L.DomUtil.create('div', 'leaflet-control-layers'); map.addSource('nodes', {
div.style.backgroundColor = 'white'; type: 'geojson',
div.style.padding = '12px'; cluster: true,
div.innerHTML = `<div style="margin-bottom:6px;"><strong>Legend</strong></div>` clusterMaxZoom: 14,
+ `<div style="display:flex"><div class="icon-mqtt-connected" style="width:12px;height:12px;margin-right:4px;margin-top:auto;margin-bottom:auto;"></div> MQTT Connected</div>` clusterRadius: 50,
+ `<div style="display:flex"><div class="icon-mqtt-disconnected" style="width:12px;height:12px;margin-right:4px;margin-top:auto;margin-bottom:auto;"></div> MQTT Disconnected</div>` data: {
+ `<div style="display:flex"><div class="icon-offline" style="width:12px;height:12px;margin-right:4px;margin-top:auto;margin-bottom:auto;"></div> Offline Too Long</div>`; type: 'FeatureCollection',
return div; features: [],
};
// handle adding/remove legend on map (can't use L.Control as an overlay, so we toggle an empty L.LayerGroup)
getMap().on('overlayadd overlayremove', function(event) {
if (event.name === 'Legend') {
if (event.type === 'overlayadd') {
getMap().addControl(legend);
} else if(event.type === 'overlayremove') {
getMap().removeControl(legend);
} }
}
});
// add layers to control ui
L.control.groupedLayers(tileLayers, {
'Nodes': {
'All': layerGroups.nodes,
'Routers': layerGroups.nodesRouter,
'Clustered': layerGroups.nodesClustered,
'None': layerGroups.none,
},
'Overlays': {
'Legend': layerGroups.legend,
'Neighbors': layerGroups.neighbors,
'Waypoints': layerGroups.waypoints,
'Position History': layerGroups.nodePositionHistory,
},
}, {
// make the "Nodes" group exclusive (use radio inputs instead of checkbox)
exclusiveGroups: ['Nodes'],
}).addTo(getMap());
// enable base layers
layerGroups.nodesClustered.addTo(getMap());
// enable overlay layers based on config
if (enabledOverlayLayers.value.includes('Legend')) {
layerGroups.legend.addTo(getMap());
}
if (enabledOverlayLayers.value.includes('Neighbors')) {
layerGroups.neighbors.addTo(getMap());
}
if (enabledOverlayLayers.value.includes('Waypoints')) {
layerGroups.waypoints.addTo(getMap());
}
if (enabledOverlayLayers.value.includes('Position History')) {
layerGroups.nodePositionHistory.addTo(getMap());
}
// update config when map overlay is added/removed
getMap().on('overlayremove', function(event) {
const layerName = event.name;
enabledOverlayLayers.value = enabledOverlayLayers.value.filter(function(enabledOverlayLayer) {
return enabledOverlayLayer !== layerName;
}); });
}); map.addSource('waypoints', {
type: 'geojson',
getMap().on('overlayadd', function(event) { data: {
const layerName = event.name; type: 'FeatureCollection',
if (!enabledOverlayLayers.value.includes(layerName)) { features: [],
enabledOverlayLayers.value.push(layerName); }
} });
}); map.addLayer({
id: 'clusters',
getMap().on('click', function(event) { type: 'circle',
// remove outline when map clicked source: 'nodes',
clearNodeOutline(); filter: ['has', 'point_count'],
// clear search paint: {
state.searchText = ''; 'circle-color': [
state.mobileSearchVisible = false; 'step',
// do nothing when clicking inside tooltip ['get', 'point_count'],
const clickedElement = event.originalEvent.target; '#6ecc3999', // small clusters
if (elementOrAnyAncestorHasClass(clickedElement, 'leaflet-tooltip')) { 10, '#f0c20c99',
return; 30, '#f1801799' // larger clusters
} ],
'circle-radius': [
closeAllTooltips(); 'step',
closeAllPopups(); ['get', 'point_count'],
}); 15, 10, 20, 30, 25
]
// auto update url when lat/lng/zoom changes }
getMap().on('moveend zoomend', function() { });
// check if user enabled auto updating position in url map.addLayer({
if (!autoUpdatePositionInUrl.value) { id: 'cluster-count',
return; type: 'symbol',
} source: 'nodes',
filter: ['has', 'point_count'],
// get map info layout: {
const latLng = getMap().getCenter(); 'text-field': '{point_count_abbreviated}',
const zoom = getMap().getZoom(); 'text-font': ['Open Sans Regular','Arial Unicode MS Regular'],
'text-size': 12
// construct new url }
const url = new URL(window.location.href); });
url.searchParams.set('lat', latLng.lat); map.addLayer({
url.searchParams.set('lng', latLng.lng); id: 'unclustered-points',
url.searchParams.set('zoom', zoom); type: 'circle',
source: 'nodes',
// update current url filter: ['!', ['has', 'point_count']],
if(window.history.replaceState){ paint: {
window.history.replaceState(null, null, url.toString()); 'circle-radius': 6,
} 'circle-color': ['coalesce', ['get', 'color'], '#cccccc'],
}); 'circle-stroke-width': 1,
'circle-stroke-color': '#ffffff'
// parse url params }
const queryParams = new URLSearchParams(location.search); });
const queryNodeId = queryParams.get('node_id'); map.addLayer({
const queryLat = queryParams.get('lat'); id: 'nodes',
const queryLng = queryParams.get('lng'); type: 'circle',
const queryZoom = queryParams.get('zoom'); source: 'nodes',
layout: {
// go to lat/lng if provided visibility: 'none',
if(queryLat && queryLng){ },
const zoomLevel = queryZoom || goToNodeZoomLevel.value paint: {
getMap().flyTo([queryLat, queryLng], parseFloat(zoomLevel), { 'circle-radius': 6,
animate: false, 'circle-color': ['coalesce', ['get', 'color'], '#cccccc'],
'circle-stroke-width': 1,
'circle-stroke-color': '#ffffff'
}
});
map.addLayer({
id: 'waypoints',
type: 'circle',
source: 'waypoints',
layout: {
visibility: 'none',
},
paint: {
'circle-radius': 6,
'circle-color': ['coalesce', ['get', 'color'], '#cccccc'],
'circle-stroke-width': 1,
'circle-stroke-color': '#ffffff'
}
}); });
}
// reload and go to provided node id map.on('mouseenter', 'clusters', () => {
reload(queryNodeId, queryZoom); map.getCanvas().style.cursor = 'pointer';
});
map.on('mouseleave', 'clusters', () => {
map.getCanvas().style.cursor = '';
});
map.on('click', 'clusters', async (e) => {
const features = map.queryRenderedFeatures(e.point, {
layers: ['clusters'] // Ensure you are targeting the 'clusters' layer
});
if (!features.length) return;
const cluster = features[0]; // Get the clicked cluster feature
const clusterId = cluster.properties.cluster_id;
const zoom = await map.getSource('nodes').getClusterExpansionZoom(clusterId);
map.easeTo({
center: features[0].geometry.coordinates,
zoom: zoom,
});
});
// When a click event occurs on a feature in
// the unclustered-point layer, open a popup at
// the location of the feature, with
// description HTML from its properties.
map.on('click', 'unclustered-points', showPopupForEvent);
map.on('mouseenter', 'unclustered-points', showPopupForEvent);
map.on('click', 'nodes', showPopupForEvent);
map.on('mouseenter', 'nodes', showPopupForEvent);
layerControl.applyDefaults();
reload();
})
}); });
function measurePopupSize(htmlContent) {
const popupContainer = document.createElement('div');
popupContainer.className = 'maplibregl-popup maplibregl-popup-anchor-bottom';
popupContainer.style.position = 'absolute';
popupContainer.style.top = '-9999px';
popupContainer.style.left = '-9999px';
popupContainer.style.visibility = 'hidden';
const content = document.createElement('div');
content.className = 'maplibregl-popup-content';
content.innerHTML = htmlContent;
popupContainer.appendChild(content);
document.body.appendChild(popupContainer);
const width = popupContainer.offsetWidth;
const height = popupContainer.offsetHeight;
document.body.removeChild(popupContainer);
return { width, height };
}
function showPopupForEvent(e) {
if (e.features[0].popup) {
return; // already has a popup open
}
const map = getMap();
const coordinates = e.features[0].geometry.coordinates.slice();
const nodeId = e.features[0].properties.id;
const html = getTooltipContentForNode(findNodeById(nodeId));
const mapContainer = map.getContainer();
const mapWidth = mapContainer.offsetWidth;
const mapHeight = mapContainer.offsetHeight;
// Get screen point of the marker
const screenPoint = map.project(coordinates);
// Measure the popup size
const { width: popupWidth, height: popupHeight } = measurePopupSize(html);
// Calculate available space around the marker
const padding = 10;
const headerHeight = document.querySelector('header')?.offsetHeight || 0;
const space = {
left: screenPoint.x - padding,
right: mapWidth - screenPoint.x - padding,
top: screenPoint.y - headerHeight - padding,
bottom: mapHeight - screenPoint.y - padding,
};
const popupOptions = {
'Top-Left': { // popup below, caret top left
anchor: 'top-left',
veriticalDeficit: Math.max(popupHeight - space.bottom, 0),
horizontalDeficit: Math.max(popupWidth - space.right, 0),
},
'Top-Right': { // popup below, caret top right
anchor: 'top-right',
veriticalDeficit: Math.max(popupHeight - space.bottom, 0),
horizontalDeficit: Math.max(popupWidth - space.left, 0),
},
'Bottom-Right': { // popup above, caret bottom right
anchor: 'bottom-right',
veriticalDeficit: Math.max(popupHeight - space.top, 0),
horizontalDeficit: Math.max(popupWidth - space.left, 0),
},
'Bottom-Left': { // popup above, caret bottom left
anchor: 'bottom-left',
veriticalDeficit: Math.max(popupHeight - space.top, 0),
horizontalDeficit: Math.max(popupWidth - space.right, 0),
},
'Left': {
anchor: 'right',
veriticalDeficit: Math.max(Math.max(((popupHeight / 2) - space.bottom), 0) + Math.max(((popupHeight/2) - space.top), 0), 0),
horizontalDeficit: Math.max(popupWidth - space.left, 0),
},
'Right': {
anchor: 'left',
veriticalDeficit: Math.max(Math.max(((popupHeight / 2) - space.bottom), 0) + Math.max(((popupHeight/2) - space.top), 0), 0),
horizontalDeficit: Math.max(popupWidth - space.right, 0),
},
'Above': {
anchor: 'bottom',
veriticalDeficit: Math.max(popupHeight - space.top, 0),
horizontalDeficit: Math.max(Math.max(((popupWidth / 2) - space.left), 0) + Math.max(((popupWidth / 2) - space.right), 0), 0),
},
'Below': {
anchor: 'top',
veriticalDeficit: Math.max(popupHeight - space.bottom, 0),
horizontalDeficit: Math.max(Math.max(((popupWidth / 2) - space.left), 0) + Math.max(((popupWidth / 2) - space.right), 0), 0),
},
};
let bestOption = null;
let bestScore = -Infinity;
const preferredAnchors = ['Left', 'Right', 'Above', 'Below'];
Object.entries(popupOptions).forEach(([key, option]) => {
const verticalCutoff = option.veriticalDeficit / popupHeight;
const horizontalCutoff = option.horizontalDeficit / popupWidth;
const totalCutoff = verticalCutoff + horizontalCutoff;
const visibilityScore = 1 - totalCutoff;
option.visibilityScore = visibilityScore; // for debugging/logging
if (
visibilityScore > bestScore ||
(
Math.abs(visibilityScore - bestScore) < 0.01 && // within tolerance
bestOption && preferredAnchors.includes(key) && !preferredAnchors.includes(bestOption)
)
) {
bestScore = visibilityScore;
bestOption = key;
}
});
const popup = new maplibregl.Popup({
closeButton: true,
closeOnClick: true,
anchor: popupOptions[bestOption].anchor,
});
popup
.setLngLat(coordinates)
.setHTML(html)
.addTo(map);
if (e.type === 'mouseenter') {
const removePopup = () => {
popup.remove();
getMap().off('mousemove', onMouseMove);
getMap().getCanvas().style.cursor = '';
};
const onMouseMove = (e) => {
const features = getMap().queryRenderedFeatures(e.point, { layers: ['unclustered-points'] });
if (!features.length) {
removePopup();
}
};
getMap().on('mousemove', onMouseMove);
}
e.features[0].popup = popup;
}
onMounted(() => { onMounted(() => {
if (lastSeenAnnouncementId.value !== CURRENT_ANNOUNCEMENT_ID) { if (lastSeenAnnouncementId.value !== CURRENT_ANNOUNCEMENT_ID) {
state.announcementVisible = true; state.announcementVisible = true;
@ -898,12 +1000,14 @@ onMounted(() => {
<template> <template>
<div class="flex flex-col h-full w-full overflow-hidden"> <div class="flex flex-col h-full w-full overflow-hidden">
<div class="flex flex-col h-full"> <div class="flex flex-col h-full">
<Announcement /> <header>
<Header <Announcement />
@reload="reload" <Header
@random-node="goToRandomNode" @reload="reload"
@search-click="onSearchResultNodeClick" @random-node="goToRandomNode"
/> @search-click="onSearchResultNodeClick"
/>
</header>
<div id="map" style="width:100%;height:100%;" ref="appMap"></div> <div id="map" style="width:100%;height:100%;" ref="appMap"></div>
</div> </div>
</div> </div>