Compare commits
3 Commits
63b823a7a1
...
improvemen
Author | SHA1 | Date | |
---|---|---|---|
2a55bd056f | |||
702da27468 | |||
8e99669487 |
2374
webapp/frontend/package-lock.json
generated
2374
webapp/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -10,7 +10,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/vite": "^4.1.4",
|
"@tailwindcss/vite": "^4.1.4",
|
||||||
"@vueuse/core": "^13.1.0",
|
"@turf/turf": "^7.2.0",
|
||||||
"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 +19,7 @@
|
|||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"pinia": "^3.0.2",
|
"pinia": "^3.0.2",
|
||||||
"pinia-plugin-persistedstate": "^4.2.0",
|
"pinia-plugin-persistedstate": "^4.2.0",
|
||||||
|
"rbush": "^4.0.1",
|
||||||
"tailwindcss": "^4.1.4",
|
"tailwindcss": "^4.1.4",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"vue-router": "^4.5.0"
|
"vue-router": "^4.5.0"
|
||||||
|
@ -143,15 +143,17 @@ export default class LayersControl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_hideLayer(name) {
|
_hideLayer(name) {
|
||||||
console.debug('hideLayer: ', name);
|
if (this._getLayers().includes(name)) {
|
||||||
this._disableLayerTransitions(name);
|
this._disableLayerTransitions(name);
|
||||||
this._map.setLayoutProperty(name, 'visibility', 'none');
|
this._map.setLayoutProperty(name, 'visibility', 'none');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_showLayer(name) {
|
_showLayer(name) {
|
||||||
console.debug('showLayer: ', name);
|
if (this._getLayers().includes(name)) {
|
||||||
this._disableLayerTransitions(name);
|
this._disableLayerTransitions(name);
|
||||||
this._map.setLayoutProperty(name, 'visibility', 'visible');
|
this._map.setLayoutProperty(name, 'visibility', 'visible');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_revertControlAction(config) {
|
_revertControlAction(config) {
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { lastSeenAnnouncementId, CURRENT_ANNOUNCEMENT_ID } from '@/config';
|
import { CURRENT_ANNOUNCEMENT_ID } from '@/config';
|
||||||
import { useUIStore } from '@/stores/uiStore';
|
import { useUIStore } from '@/stores/uiStore';
|
||||||
|
import { useConfigStore } from '@/stores/configStore';
|
||||||
const ui = useUIStore();
|
const ui = useUIStore();
|
||||||
|
const config = useConfigStore();
|
||||||
|
|
||||||
function dismissAnnouncement() {
|
function dismissAnnouncement() {
|
||||||
if (lastSeenAnnouncementId.value != CURRENT_ANNOUNCEMENT_ID) {
|
if (config.lastSeenAnnouncementId != CURRENT_ANNOUNCEMENT_ID) {
|
||||||
lastSeenAnnouncementId.value = CURRENT_ANNOUNCEMENT_ID;
|
config.lastSeenAnnouncementId = CURRENT_ANNOUNCEMENT_ID;
|
||||||
}
|
}
|
||||||
ui.hideAnnouncement();
|
ui.hideAnnouncement();
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { selectedNodeLatestPowerMetric } from '@/store.js';
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
channel: Number, // Channel number (1, 2, or 3)
|
channel: Number, // Channel number (1, 2, or 3),
|
||||||
|
latest: Array,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -10,13 +10,13 @@
|
|||||||
<li class="flex p-3">
|
<li class="flex p-3">
|
||||||
<div class="text-sm font-medium text-gray-900">Channel {{ channel }}</div>
|
<div class="text-sm font-medium text-gray-900">Channel {{ channel }}</div>
|
||||||
<div class="ml-auto text-sm text-gray-700">
|
<div class="ml-auto text-sm text-gray-700">
|
||||||
<span v-if="selectedNodeLatestPowerMetric">
|
<span v-if="latest">
|
||||||
<span v-if="selectedNodeLatestPowerMetric[`ch${channel}_voltage`]" >
|
<span v-if="latest[`ch${channel}_voltage`]" >
|
||||||
{{ Number(selectedNodeLatestPowerMetric[`ch${channel}_voltage`]).toFixed(2) }}V
|
{{ Number(latest[`ch${channel}_voltage`]).toFixed(2) }}V
|
||||||
</span>
|
</span>
|
||||||
<span v-else>???</span>
|
<span v-else>???</span>
|
||||||
<span v-if="selectedNodeLatestPowerMetric[`ch${channel}_current`]">
|
<span v-if="latest[`ch${channel}_current`]">
|
||||||
/ {{ Number(selectedNodeLatestPowerMetric[`ch${channel}_current`]).toFixed(2) }}mA
|
/ {{ Number(latest[`ch${channel}_current`]).toFixed(2) }}mA
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span v-else>???</span>
|
<span v-else>???</span>
|
||||||
|
@ -16,6 +16,8 @@ const search = computed({
|
|||||||
get: () => ui.searchText,
|
get: () => ui.searchText,
|
||||||
set: val => ui.search(val),
|
set: val => ui.search(val),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO: reload button is jumpy when it spins
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { hasSeenInfoModal } from '@/config';
|
import { useConfigStore } from '@/stores/configStore';
|
||||||
import { useUIStore } from '@/stores/uiStore';
|
import { useUIStore } from '@/stores/uiStore';
|
||||||
|
|
||||||
const ui = useUIStore();
|
const ui = useUIStore();
|
||||||
|
const config = useConfigStore();
|
||||||
|
|
||||||
function dismissInfoModal() {
|
function dismissInfoModal() {
|
||||||
if (hasSeenInfoModal.value === false) {
|
if (config.hasSeenInfoModal === false) {
|
||||||
hasSeenInfoModal.value = true;
|
config.hasSeenInfoModal = true;
|
||||||
}
|
}
|
||||||
ui.hideInfoModal();
|
ui.hideInfoModal();
|
||||||
}
|
}
|
||||||
|
@ -20,16 +20,62 @@ import { copyShareLinkForNode, getTimeSpan, buildPath } from '@/utils';
|
|||||||
import { useConfigStore } from '@/stores/configStore';
|
import { useConfigStore } from '@/stores/configStore';
|
||||||
|
|
||||||
const configStore = useConfigStore();
|
const configStore = useConfigStore();
|
||||||
const emit = defineEmits(['showPositionHistory']);
|
const emit = defineEmits(['showPositionHistory', 'showTraceRoute']);
|
||||||
const mapData = useMapStore();
|
const mapData = useMapStore();
|
||||||
|
|
||||||
function showPositionHistory(id) {
|
const mqttMetrics = ref([]);
|
||||||
emit('showPositionHistory', id);
|
const traceroutes = ref([]);
|
||||||
|
const deviceMetrics = ref([]);
|
||||||
|
const environmentMetrics = ref([]);
|
||||||
|
const powerMetrics = ref([]);
|
||||||
|
|
||||||
|
// Generalized function for loading data
|
||||||
|
async function loadData(path, params = {}, targetRef, metrics = false) {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(buildPath(path), { params });
|
||||||
|
|
||||||
|
// Grab the first key from the response (assuming it's always an object with a single data array)
|
||||||
|
const key = Object.keys(response.data)[0];
|
||||||
|
const rawData = response.data[key] ?? [];
|
||||||
|
|
||||||
|
// Reverse if it's metric data, otherwise just assign
|
||||||
|
targetRef.value = metrics ? rawData.slice().reverse() : rawData;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error loading data from ${path}:`, error);
|
||||||
|
targetRef.value = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showTraceRoute(traceroute) {
|
// Utility function to calculate the timeFrom value based on a given time range
|
||||||
emit('showTraceRoute', traceroute);
|
function calculateTimeFrom(timeRange) {
|
||||||
state.selectedTraceRoute = traceroute;
|
const time = getTimeSpan(timeRange).amount;
|
||||||
|
return new Date().getTime() - (time * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load MQTT metrics
|
||||||
|
function loadNodeMqttMetrics(nodeId) {
|
||||||
|
loadData(`/api/v1/nodes/${nodeId}/mqtt-metrics`, {}, mqttMetrics);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load Traceroutes
|
||||||
|
function loadNodeTraceroutes(nodeId) {
|
||||||
|
loadData(`/api/v1/nodes/${nodeId}/traceroutes`, { count: 5 }, traceroutes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load Device Metrics
|
||||||
|
function loadNodeDeviceMetrics(nodeId) {
|
||||||
|
loadData(`/api/v1/nodes/${nodeId}/device-metrics`, { time_from: calculateTimeFrom(configStore.deviceMetricsTimeRange) }, deviceMetrics, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load Environment Metrics
|
||||||
|
function loadNodeEnvironmentMetrics(nodeId) {
|
||||||
|
loadData(`/api/v1/nodes/${nodeId}/environment-metrics`, { time_from: calculateTimeFrom(configStore.environmentMetricsTimeRange) }, environmentMetrics, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load Power Metrics
|
||||||
|
function loadNodePowerMetrics(nodeId) {
|
||||||
|
loadData(`/api/v1/nodes/${nodeId}/power-metrics`, { time_from: calculateTimeFrom(configStore.powerMetricsTimeRange) }, powerMetrics, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadNode(nodeId) {
|
function loadNode(nodeId) {
|
||||||
@ -40,73 +86,6 @@ function loadNode(nodeId) {
|
|||||||
loadNodePowerMetrics(nodeId);
|
loadNodePowerMetrics(nodeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadNodeMqttMetrics(nodeId) {
|
|
||||||
state.selectedNodeMqttMetrics = [];
|
|
||||||
axios.get(buildPath(`/api/v1/nodes/${nodeId}/mqtt-metrics`)).then((response) => {
|
|
||||||
state.selectedNodeMqttMetrics = response.data.mqtt_metrics;
|
|
||||||
}).catch(() => {
|
|
||||||
// do nothing
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadNodeTraceroutes(nodeId) {
|
|
||||||
state.selectedNodeTraceroutes = [];
|
|
||||||
axios.get(buildPath(`/api/v1/nodes/${nodeId}/traceroutes`), {
|
|
||||||
params: {
|
|
||||||
count: 5,
|
|
||||||
},
|
|
||||||
}).then((response) => {
|
|
||||||
state.selectedNodeTraceroutes = response.data.traceroutes;
|
|
||||||
}).catch(() => {
|
|
||||||
// do nothing
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadNodeDeviceMetrics(nodeId) {
|
|
||||||
const time = getTimeSpan(configStore.deviceMetricsTimeRange).amount
|
|
||||||
const timeFrom = new Date().getTime() - (time * 1000);
|
|
||||||
axios.get(buildPath(`/api/v1/nodes/${nodeId}/device-metrics`), {
|
|
||||||
params: {
|
|
||||||
time_from: timeFrom,
|
|
||||||
},
|
|
||||||
}).then((response) => {
|
|
||||||
// reverse response, as it's newest to oldest, but we want oldest to newest
|
|
||||||
state.selectedNodeDeviceMetrics = response.data.device_metrics.reverse();
|
|
||||||
}).catch(() => {
|
|
||||||
state.selectedNodeDeviceMetrics = [];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadNodeEnvironmentMetrics(nodeId) {
|
|
||||||
const time = getTimeSpan(configStore.environmentMetricsTimeRange).amount
|
|
||||||
const timeFrom = new Date().getTime() - (time * 1000);
|
|
||||||
axios.get(buildPath(`/api/v1/nodes/${nodeId}/environment-metrics`), {
|
|
||||||
params: {
|
|
||||||
time_from: timeFrom,
|
|
||||||
},
|
|
||||||
}).then((response) => {
|
|
||||||
// reverse response, as it's newest to oldest, but we want oldest to newest
|
|
||||||
state.selectedNodeEnvironmentMetrics = response.data.environment_metrics.reverse();
|
|
||||||
}).catch(() => {
|
|
||||||
state.selectedNodeEnvironmentMetrics = [];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadNodePowerMetrics(nodeId) {
|
|
||||||
const time = getTimeSpan(configStore.powerMetricsTimeRange).amount
|
|
||||||
const timeFrom = new Date().getTime() - (time * 1000);
|
|
||||||
axios.get(buildPath(`/api/v1/nodes/${nodeId}/power-metrics`), {
|
|
||||||
params: {
|
|
||||||
time_from: timeFrom,
|
|
||||||
},
|
|
||||||
}).then((response) => {
|
|
||||||
// reverse response, as it's newest to oldest, but we want oldest to newest
|
|
||||||
state.selectedNodePowerMetrics = response.data.power_metrics.reverse();
|
|
||||||
}).catch(() => {
|
|
||||||
state.selectedNodePowerMetrics = [];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => state.selectedNode,
|
() => state.selectedNode,
|
||||||
(newValue) => {
|
(newValue) => {
|
||||||
@ -263,17 +242,17 @@ watch(
|
|||||||
<!-- lora config -->
|
<!-- lora config -->
|
||||||
<LoraConfig :node="state.selectedNode"/>
|
<LoraConfig :node="state.selectedNode"/>
|
||||||
<!-- position -->
|
<!-- position -->
|
||||||
<Position @show-position-history="showPositionHistory" :node="state.selectedNode"/>
|
<Position @show-position-history="(nodeId) => $emit('showPositionHistory', nodeId)" :node="state.selectedNode"/>
|
||||||
<!-- device metrics -->
|
<!-- device metrics -->
|
||||||
<DeviceMetricsChart :node="state.selectedNode"/>
|
<DeviceMetricsChart :node="state.selectedNode" :data="deviceMetrics"/>
|
||||||
<!-- environment metrics -->
|
<!-- environment metrics -->
|
||||||
<EnvironmentMetricsChart :node="state.selectedNode"/>
|
<EnvironmentMetricsChart :node="state.selectedNode" :data="environmentMetrics"/>
|
||||||
<!-- power metrics -->
|
<!-- power metrics -->
|
||||||
<PowerMetricsChart/>
|
<PowerMetricsChart :data="powerMetrics"/>
|
||||||
<!-- mqtt -->
|
<!-- mqtt -->
|
||||||
<MqttHistory />
|
<MqttHistory :data="mqttMetrics"/>
|
||||||
<!-- traceroutes -->
|
<!-- traceroutes -->
|
||||||
<Traceroutes @show-trace-route="showTraceRoute"/>
|
<Traceroutes @show-trace-route="(traceroute) => $emit('showTraceRoute', traceroute)" :data="traceroutes"/>
|
||||||
<!-- other -->
|
<!-- other -->
|
||||||
<OtherInfo :node="state.selectedNode"/>
|
<OtherInfo :node="state.selectedNode"/>
|
||||||
<!-- share -->
|
<!-- share -->
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
const props = defineProps(['node']);
|
const props = defineProps(['node', 'data']);
|
||||||
|
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { state } from '@/store.js';
|
|
||||||
import MetricsChart from '@/components/Chart/Metrics.vue';
|
import MetricsChart from '@/components/Chart/Metrics.vue';
|
||||||
|
|
||||||
const chartData = computed(() => {
|
const chartData = computed(() => {
|
||||||
const metrics = state.selectedNodeDeviceMetrics;
|
const metrics = props.data;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
labels: metrics.map(m => m.created_at),
|
labels: metrics.map(m => m.created_at),
|
||||||
|
@ -1,19 +1,14 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
const props = defineProps(['node']);
|
const props = defineProps(['node', 'data']);
|
||||||
|
import { computed } from 'vue';
|
||||||
import { ref, computed, onMounted, watch } from 'vue';
|
import { formatTemperature } from '@/utils';
|
||||||
import { useConfigStore } from '@/stores/configStore';
|
|
||||||
import { state } from '@/store.js';
|
|
||||||
import { formatTemperature } from '@/utils.js';
|
|
||||||
import MetricsChart from '@/components/Chart/Metrics.vue';
|
import MetricsChart from '@/components/Chart/Metrics.vue';
|
||||||
|
|
||||||
const configStore = useConfigStore();
|
|
||||||
|
|
||||||
// Chart data prep
|
// Chart data prep
|
||||||
const labels = computed(() => state.selectedNodeEnvironmentMetrics.map(m => m.created_at));
|
const labels = computed(() => props.data.map(m => m.created_at));
|
||||||
const temperatureMetrics = computed(() => state.selectedNodeEnvironmentMetrics.map(m => m.temperature));
|
const temperatureMetrics = computed(() => props.data.map(m => m.temperature));
|
||||||
const relativeHumidityMetrics = computed(() => state.selectedNodeEnvironmentMetrics.map(m => m.relative_humidity));
|
const relativeHumidityMetrics = computed(() => props.data.map(m => m.relative_humidity));
|
||||||
const barometricPressureMetrics = computed(() => state.selectedNodeEnvironmentMetrics.map(m => m.barometric_pressure));
|
const barometricPressureMetrics = computed(() => props.data.map(m => m.barometric_pressure));
|
||||||
|
|
||||||
const chartData = computed(() => ({
|
const chartData = computed(() => ({
|
||||||
labels: labels.value,
|
labels: labels.value,
|
||||||
@ -112,7 +107,7 @@ const legendData = {
|
|||||||
<li class="flex p-3">
|
<li class="flex p-3">
|
||||||
<div class="text-sm font-medium text-gray-900">Temperature</div>
|
<div class="text-sm font-medium text-gray-900">Temperature</div>
|
||||||
<div class="ml-auto text-sm text-gray-700">
|
<div class="ml-auto text-sm text-gray-700">
|
||||||
<span v-if="state.selectedNode?.temperature">{{ formatTemperature(state.selectedNode.temperature) }}</span>
|
<span v-if="props.node?.temperature">{{ formatTemperature(props.node.temperature) }}</span>
|
||||||
<span v-else>???</span>
|
<span v-else>???</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
@ -120,8 +115,8 @@ const legendData = {
|
|||||||
<li class="flex p-3">
|
<li class="flex p-3">
|
||||||
<div class="text-sm font-medium text-gray-900">Relative Humidity</div>
|
<div class="text-sm font-medium text-gray-900">Relative Humidity</div>
|
||||||
<div class="ml-auto text-sm text-gray-700">
|
<div class="ml-auto text-sm text-gray-700">
|
||||||
<span v-if="state.selectedNode?.relative_humidity">
|
<span v-if="props.node?.relative_humidity">
|
||||||
{{ Number(state.selectedNode.relative_humidity).toFixed(0) }}%
|
{{ Number(props.node.relative_humidity).toFixed(0) }}%
|
||||||
</span>
|
</span>
|
||||||
<span v-else>???</span>
|
<span v-else>???</span>
|
||||||
</div>
|
</div>
|
||||||
@ -130,8 +125,8 @@ const legendData = {
|
|||||||
<li class="flex p-3">
|
<li class="flex p-3">
|
||||||
<div class="text-sm font-medium text-gray-900">Barometric Pressure</div>
|
<div class="text-sm font-medium text-gray-900">Barometric Pressure</div>
|
||||||
<div class="ml-auto text-sm text-gray-700">
|
<div class="ml-auto text-sm text-gray-700">
|
||||||
<span v-if="state.selectedNode?.barometric_pressure">
|
<span v-if="props.node?.barometric_pressure">
|
||||||
{{ Number(state.selectedNode.barometric_pressure).toFixed(1) }} hPa
|
{{ Number(props.node.barometric_pressure).toFixed(1) }} hPa
|
||||||
</span>
|
</span>
|
||||||
<span v-else>???</span>
|
<span v-else>???</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { getRegionFrequencyRange } from '@/utils.js';
|
import { getRegionFrequencyRange } from '@/utils';
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
node: Object,
|
node: Object,
|
||||||
});
|
});
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { state } from '@/store.js';
|
const props = defineProps(['data']);
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
const mqttMetrics = computed(() => state.selectedNodeMqttMetrics);
|
const mqttMetrics = computed(() => props.data);
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { state, selectedNodeLatestPowerMetric } from '@/store.js';
|
|
||||||
import MetricsChart from '@/components/Chart/Metrics.vue';
|
import MetricsChart from '@/components/Chart/Metrics.vue';
|
||||||
import ChannelData from '@/components/Chart/PowerMetrics/ChannelData.vue';
|
import ChannelData from '@/components/Chart/PowerMetrics/ChannelData.vue';
|
||||||
|
const props = defineProps(['data']);
|
||||||
|
|
||||||
const legendData = {
|
const legendData = {
|
||||||
'Channel 1': 'bg-blue-500',
|
'Channel 1': 'bg-blue-500',
|
||||||
@ -10,9 +10,14 @@ const legendData = {
|
|||||||
'Channel 3': 'bg-orange-500',
|
'Channel 3': 'bg-orange-500',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const latestMetric = computed(() => {
|
||||||
|
const [ latest ] = (props.data ?? []).slice(-1);
|
||||||
|
return latest;
|
||||||
|
});
|
||||||
|
|
||||||
// Computed dataset for the chart container
|
// Computed dataset for the chart container
|
||||||
const chartData = computed(() => {
|
const chartData = computed(() => {
|
||||||
const metrics = state.selectedNodePowerMetrics;
|
const metrics = props.data;
|
||||||
const labels = metrics.map(m => m.created_at);
|
const labels = metrics.map(m => m.created_at);
|
||||||
|
|
||||||
const colors = ['#3b82f6', '#22c55e', '#f97316'];
|
const colors = ['#3b82f6', '#22c55e', '#f97316'];
|
||||||
@ -109,6 +114,6 @@ const chartConfig = {
|
|||||||
:legendData="legendData"
|
:legendData="legendData"
|
||||||
:chartConfig="chartConfig"
|
:chartConfig="chartConfig"
|
||||||
>
|
>
|
||||||
<ChannelData v-for="i in [1, 2, 3]" :key="i" :channel="i" />
|
<ChannelData v-for="i in [1, 2, 3]" :key="i" :channel="i" :latest="latestMetric"/>
|
||||||
</MetricsChart>
|
</MetricsChart>
|
||||||
</template>
|
</template>
|
@ -2,7 +2,7 @@
|
|||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
node: Object,
|
node: Object,
|
||||||
});
|
});
|
||||||
import { getShareLinkForNode, copyShareLinkForNode } from '@/utils.js';
|
import { getShareLinkForNode, copyShareLinkForNode } from '@/utils';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
const props = defineProps(['data']);
|
||||||
const emit = defineEmits(['showTraceRoute']);
|
const emit = defineEmits(['showTraceRoute']);
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { state } from '@/store.js';
|
|
||||||
import { useMapStore } from '@/stores/mapStore';
|
import { useMapStore } from '@/stores/mapStore';
|
||||||
const mapData = useMapStore();
|
const mapData = useMapStore();
|
||||||
</script>
|
</script>
|
||||||
@ -12,8 +12,8 @@ const mapData = useMapStore();
|
|||||||
<div class="text-sm text-gray-600">Only 5 most recent are shown</div>
|
<div class="text-sm text-gray-600">Only 5 most recent are shown</div>
|
||||||
</div>
|
</div>
|
||||||
<ul role="list" class="flex-1 divide-y divide-gray-200">
|
<ul role="list" class="flex-1 divide-y divide-gray-200">
|
||||||
<template v-if="state.selectedNodeTraceroutes.length > 0">
|
<template v-if="props.data.length > 0">
|
||||||
<li @click="$emit('showTraceRoute', traceroute)" v-for="traceroute of state.selectedNodeTraceroutes">
|
<li @click="$emit('showTraceRoute', traceroute)" v-for="traceroute of props.data">
|
||||||
<div class="relative flex items-center">
|
<div class="relative flex items-center">
|
||||||
<div class="block flex-1 px-4 py-2">
|
<div class="block flex-1 px-4 py-2">
|
||||||
<div class="relative flex min-w-0 flex-1 items-center">
|
<div class="relative flex min-w-0 flex-1 items-center">
|
||||||
|
@ -4,11 +4,16 @@ import { state } from '@/store';
|
|||||||
import CloseActionButton from '@/components/CloseActionButton.vue';
|
import CloseActionButton from '@/components/CloseActionButton.vue';
|
||||||
|
|
||||||
const emit = defineEmits(['dismiss']);
|
const emit = defineEmits(['dismiss']);
|
||||||
|
const props = defineProps({
|
||||||
|
node: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
validator(value) {
|
||||||
|
return value === null || typeof value === 'object';
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function dismissShowingNodeNeighbours() {
|
|
||||||
state.selectedNodeToShowNeighbours = null;
|
|
||||||
emit('dismiss');
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -21,30 +26,30 @@ function dismissShowingNodeNeighbours() {
|
|||||||
leave-from-class="translate-y-0"
|
leave-from-class="translate-y-0"
|
||||||
leave-to-class="translate-y-full"
|
leave-to-class="translate-y-full"
|
||||||
>
|
>
|
||||||
<div v-show="state.selectedNodeToShowNeighbours" class="fixed left-0 right-0 bottom-0">
|
<div v-show="props.node !== null" class="fixed left-0 right-0 bottom-0">
|
||||||
<div v-if="state.selectedNodeToShowNeighbours" class="mx-auto w-screen max-w-md p-4">
|
<div v-if="props.node !== null" class="mx-auto w-screen max-w-md p-4">
|
||||||
<div class="flex h-full flex-col bg-white shadow-xl rounded-xl border">
|
<div class="flex h-full flex-col bg-white shadow-xl rounded-xl border">
|
||||||
<div class="p-2">
|
<div class="p-2">
|
||||||
<div class="flex items-start justify-between">
|
<div class="flex items-start justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="font-bold">
|
<h2 class="font-bold">
|
||||||
{{ state.selectedNodeToShowNeighbours.short_name }} Neighbors
|
{{ props.node.short_name }} Neighbors
|
||||||
</h2>
|
</h2>
|
||||||
<h3
|
<h3
|
||||||
v-if="state.selectedNodeToShowNeighboursType === 'weHeard'"
|
v-if="state.neighborsModalType === 'weHeard'"
|
||||||
class="text-sm"
|
class="text-sm"
|
||||||
>
|
>
|
||||||
Nodes heard by {{ state.selectedNodeToShowNeighbours.short_name }}
|
Nodes heard by {{ props.node.short_name }}
|
||||||
</h3>
|
</h3>
|
||||||
<h3
|
<h3
|
||||||
v-if="state.selectedNodeToShowNeighboursType === 'theyHeard'"
|
v-if="state.neighborsModalType === 'theyHeard'"
|
||||||
class="text-sm"
|
class="text-sm"
|
||||||
>
|
>
|
||||||
Nodes that heard {{ state.selectedNodeToShowNeighbours.short_name }}
|
Nodes that heard {{ props.node.short_name }}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<CloseActionButton
|
<CloseActionButton
|
||||||
@click="dismissShowingNodeNeighbours"
|
@click="$emit('dismiss')"
|
||||||
class="my-auto ml-3"
|
class="my-auto ml-3"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,13 +1,20 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
const emit = defineEmits(['dismiss']);
|
const emit = defineEmits(['dismiss']);
|
||||||
|
const props = defineProps({
|
||||||
|
node: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
validator(value) {
|
||||||
|
return value === null || typeof value === 'object';
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
import { state } from '@/store';
|
import { state } from '@/store';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { useUIStore } from '@/stores/uiStore';
|
import { useUIStore } from '@/stores/uiStore';
|
||||||
|
|
||||||
const ui = useUIStore();
|
const ui = useUIStore();
|
||||||
function dismissShowingNodePositionHistory() {
|
function dismissShowingNodePositionHistory() {
|
||||||
state.selectedNodePositionHistory = [];
|
|
||||||
state.selectedNodeToShowPositionHistory = null;
|
|
||||||
ui.collapsePositionHistoryModal();
|
ui.collapsePositionHistoryModal();
|
||||||
emit('dismiss');
|
emit('dismiss');
|
||||||
}
|
}
|
||||||
@ -44,8 +51,8 @@ function onPositionHistoryQuickRangeClick(range) {
|
|||||||
leave-active-class="transition duration-300 ease-in-out transform"
|
leave-active-class="transition duration-300 ease-in-out transform"
|
||||||
leave-from-class="translate-y-0"
|
leave-from-class="translate-y-0"
|
||||||
leave-to-class="translate-y-full">
|
leave-to-class="translate-y-full">
|
||||||
<div v-show="state.selectedNodeToShowPositionHistory != null" class="fixed left-0 right-0 bottom-0">
|
<div v-show="props.node != null" class="fixed left-0 right-0 bottom-0">
|
||||||
<div v-if="state.selectedNodeToShowPositionHistory != null" class="mx-auto w-screen max-w-md p-4">
|
<div v-if="props.node != null" class="mx-auto w-screen max-w-md p-4">
|
||||||
<div class="flex h-full flex-col bg-white shadow-xl rounded-xl border">
|
<div class="flex h-full flex-col bg-white shadow-xl rounded-xl border">
|
||||||
<div>
|
<div>
|
||||||
<div class="flex p-2">
|
<div class="flex p-2">
|
||||||
@ -61,7 +68,7 @@ function onPositionHistoryQuickRangeClick(range) {
|
|||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="my-auto mr-auto font-bold">{{ state.selectedNodeToShowPositionHistory.short_name }} Position History</div>
|
<div class="my-auto mr-auto font-bold">{{ props.node.short_name }} Position History</div>
|
||||||
<div class="flex my-auto ml-3 space-x-2">
|
<div class="flex my-auto ml-3 space-x-2">
|
||||||
<a href="javascript:void(0)" class="rounded-full" @click="dismissShowingNodePositionHistory">
|
<a href="javascript:void(0)" class="rounded-full" @click="dismissShowingNodePositionHistory">
|
||||||
<div class="bg-gray-100 hover:bg-gray-200 p-1 rounded-full">
|
<div class="bg-gray-100 hover:bg-gray-200 p-1 rounded-full">
|
||||||
|
@ -131,8 +131,8 @@ function formatDate(date) {
|
|||||||
<br /><br />
|
<br /><br />
|
||||||
<button @click="state.selectedNode = node" class="border border-gray-300 bg-gray-100 p-1 w-full rounded-sm hover:bg-gray-200 mb-1">Show Full Details</button>
|
<button @click="state.selectedNode = node" class="border border-gray-300 bg-gray-100 p-1 w-full rounded-sm hover:bg-gray-200 mb-1">Show Full Details</button>
|
||||||
<br />
|
<br />
|
||||||
<button @click="$emit('showNeighbors', 'theyHeard', node.node_id)" class="border border-gray-300 bg-gray-100 p-1 w-full rounded-sm hover:bg-gray-200 mb-1">Show Neighbours (Heard Us)</button>
|
<button @click="$emit('showNeighbors', node.node_id, 'theyHeard')" class="border border-gray-300 bg-gray-100 p-1 w-full rounded-sm hover:bg-gray-200 mb-1">Show Neighbours (Heard Us)</button>
|
||||||
<br />
|
<br />
|
||||||
<button @click="$emit('showNeighbors', 'weHeard', node.node_id)" class="border border-gray-300 bg-gray-100 p-1 w-full rounded-sm hover:bg-gray-200">Show Neighbours (We Heard)</button>
|
<button @click="$emit('showNeighbors', node.node_id, 'weHeard')" class="border border-gray-300 bg-gray-100 p-1 w-full rounded-sm hover:bg-gray-200">Show Neighbours (We Heard)</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
const emit = defineEmits(['goTo']);
|
const emit = defineEmits(['goTo', 'dismiss']);
|
||||||
|
const props = defineProps(['data']);
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { state } from '@/store';
|
|
||||||
import { useMapStore } from '@/stores/mapStore';
|
import { useMapStore } from '@/stores/mapStore';
|
||||||
import NodeEntry from '@/components/Traceroute/NodeEntry.vue';
|
import NodeEntry from '@/components/Traceroute/NodeEntry.vue';
|
||||||
const mapData = useMapStore();
|
const mapData = useMapStore();
|
||||||
// selected node IDs
|
// selected node IDs
|
||||||
const toNode = computed(() => mapData.findNodeById(state.selectedTraceRoute.to));
|
const toNode = computed(() => mapData.findNodeById(props.data.to));
|
||||||
const fromNode = computed(() => mapData.findNodeById(state.selectedTraceRoute.from));
|
const fromNode = computed(() => mapData.findNodeById(props.data.from));
|
||||||
const gatewayNode = computed(() => mapData.findNodeById(state.selectedTraceRoute.gateway_id));
|
const gatewayNode = computed(() => mapData.findNodeById(props.data.gateway_id));
|
||||||
// pre-resolve the route nodes into an array
|
// pre-resolve the route nodes into an array
|
||||||
const routeNodes = computed(() =>
|
const routeNodes = computed(() =>
|
||||||
state.selectedTraceRoute.route?.map(id => ({
|
props.data.route?.map(id => ({
|
||||||
id,
|
id,
|
||||||
node: mapData.findNodeById(id),
|
node: mapData.findNodeById(id),
|
||||||
})) ?? []
|
})) ?? []
|
||||||
@ -29,7 +29,7 @@ const routeNodes = computed(() =>
|
|||||||
leave-active-class="transition-opacity duration-300 ease-linear"
|
leave-active-class="transition-opacity duration-300 ease-linear"
|
||||||
leave-from-class="opacity-100"
|
leave-from-class="opacity-100"
|
||||||
leave-to-class="opacity-0">
|
leave-to-class="opacity-0">
|
||||||
<div v-show="state.selectedTraceRoute != null" @click="state.selectedTraceRoute = null" class="fixed inset-0 bg-gray-900/75"></div>
|
<div v-show="props.data != null" @click="$emit('dismiss')" class="fixed inset-0 bg-gray-900/75"></div>
|
||||||
</transition>
|
</transition>
|
||||||
|
|
||||||
<!-- sidebar -->
|
<!-- sidebar -->
|
||||||
@ -40,19 +40,19 @@ const routeNodes = computed(() =>
|
|||||||
leave-active-class="transition duration-300 ease-in-out transform"
|
leave-active-class="transition duration-300 ease-in-out transform"
|
||||||
leave-from-class="translate-x-0"
|
leave-from-class="translate-x-0"
|
||||||
leave-to-class="-translate-x-full">
|
leave-to-class="-translate-x-full">
|
||||||
<div v-show="state.selectedTraceRoute != null" class="fixed top-0 left-0 bottom-0">
|
<div v-show="props.data != null" class="fixed top-0 left-0 bottom-0">
|
||||||
<div v-if="state.selectedTraceRoute != null" class="w-screen h-full max-w-md overflow-hidden">
|
<div v-if="props.data != null" class="w-screen h-full max-w-md overflow-hidden">
|
||||||
<div class="flex h-full flex-col bg-white shadow-xl">
|
<div class="flex h-full flex-col bg-white shadow-xl">
|
||||||
|
|
||||||
<!-- slideover header -->
|
<!-- slideover header -->
|
||||||
<div class="p-2 border-b border-gray-200 shadow-sm">
|
<div class="p-2 border-b border-gray-200 shadow-sm">
|
||||||
<div class="flex items-start justify-between">
|
<div class="flex items-start justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="font-bold">Traceroute #{{ state.selectedTraceRoute.id }}</h2>
|
<h2 class="font-bold">Traceroute #{{ props.data.id }}</h2>
|
||||||
<h3 class="text-sm">{{ moment(new Date(state.selectedTraceRoute.updated_at)).fromNow() }} - {{ state.selectedTraceRoute.route.length }} hops {{ state.selectedTraceRoute.channel_id ? `on ${state.selectedTraceRoute.channel_id}` : '' }}</h3>
|
<h3 class="text-sm">{{ moment(new Date(props.data.updated_at)).fromNow() }} - {{ props.data.route.length }} hops {{ props.data.channel_id ? `on ${props.data.channel_id}` : '' }}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="my-auto ml-3 flex h-7 items-center">
|
<div class="my-auto ml-3 flex h-7 items-center">
|
||||||
<a href="javascript:void(0)" class="rounded-full" @click="state.selectedTraceRoute = null">
|
<a href="javascript:void(0)" class="rounded-full" @click="$emit('dismiss')">
|
||||||
<div class="bg-gray-100 hover:bg-gray-200 p-2 rounded-full">
|
<div class="bg-gray-100 hover:bg-gray-200 p-2 rounded-full">
|
||||||
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||||
@ -83,7 +83,7 @@ const routeNodes = computed(() =>
|
|||||||
<div>
|
<div>
|
||||||
<div class="bg-gray-200 p-2 font-semibold">Raw Data</div>
|
<div class="bg-gray-200 p-2 font-semibold">Raw Data</div>
|
||||||
<div class="text-sm text-gray-700">
|
<div class="text-sm text-gray-700">
|
||||||
<pre class="bg-gray-100 rounded-sm p-2 overflow-x-auto">{{ JSON.stringify(state.selectedTraceRoute, null, 4) }}</pre>
|
<pre class="bg-gray-100 rounded-sm p-2 overflow-x-auto">{{ JSON.stringify(props.data, null, 4) }}</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { useMapStore } from '@/stores/mapStore';
|
import { useMapStore } from '@/stores/mapStore';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { nodesMaxAge, nodesOfflineAge } from '@/config'; // TODO: use config store
|
import { useConfigStore } from '@/stores/configStore';
|
||||||
import { icons } from '@/map';
|
import { icons } from '@/map';
|
||||||
import { hasNodeUplinkedToMqttRecently, isValidCoordinates } from '@/utils';
|
import { hasNodeUplinkedToMqttRecently, isValidCoordinates } from '@/utils';
|
||||||
|
|
||||||
export function useNodeProcessor() {
|
export function useNodeProcessor() {
|
||||||
const mapStore = useMapStore(); // Access your mapStore from Pinia
|
const mapStore = useMapStore();
|
||||||
|
const config = useConfigStore();
|
||||||
|
|
||||||
// This function processes new node data
|
// This function processes new node data
|
||||||
const processNewNodes = (newNodes) => {
|
const processNewNodes = (newNodes) => {
|
||||||
@ -15,9 +16,9 @@ export function useNodeProcessor() {
|
|||||||
|
|
||||||
for (const node of newNodes) {
|
for (const node of newNodes) {
|
||||||
// Skip nodes older than configured node max age
|
// Skip nodes older than configured node max age
|
||||||
if (nodesMaxAge.value) {
|
if (config.nodesMaxAge !== null) {
|
||||||
const lastUpdatedAgeInMillis = now.diff(moment(node.updated_at));
|
const lastUpdatedAgeInMillis = now.diff(moment(node.updated_at));
|
||||||
if (lastUpdatedAgeInMillis > nodesMaxAge.value * 1000) {
|
if (lastUpdatedAgeInMillis > config.nodesMaxAge * 1000) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -42,9 +43,9 @@ export function useNodeProcessor() {
|
|||||||
let icon = icons.mqttDisconnected;
|
let icon = icons.mqttDisconnected;
|
||||||
|
|
||||||
// Use offline icon for nodes older than configured node offline age
|
// Use offline icon for nodes older than configured node offline age
|
||||||
if (nodesOfflineAge.value) {
|
if (config.nodesOfflineAge !== null) {
|
||||||
const lastUpdatedAgeInMillis = now.diff(moment(node.updated_at));
|
const lastUpdatedAgeInMillis = now.diff(moment(node.updated_at));
|
||||||
if (lastUpdatedAgeInMillis > nodesOfflineAge.value * 1000) {
|
if (lastUpdatedAgeInMillis > config.nodesOfflineAge * 1000) {
|
||||||
icon = icons.offline;
|
icon = icons.offline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
84
webapp/frontend/src/composables/useWaypointProcessor.js
Normal file
84
webapp/frontend/src/composables/useWaypointProcessor.js
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import moment from 'moment';
|
||||||
|
import { useMapStore } from '@/stores/mapStore';
|
||||||
|
import { useConfigStore } from '@/stores/configStore';
|
||||||
|
import { isValidCoordinates } from '@/utils';
|
||||||
|
|
||||||
|
export function useWaypointProcessor() {
|
||||||
|
const mapStore = useMapStore();
|
||||||
|
const config = useConfigStore();
|
||||||
|
|
||||||
|
// This function processes new waypoint data
|
||||||
|
const processNewWaypoints = (newWaypoints) => {
|
||||||
|
const now = moment();
|
||||||
|
const processedWaypoints = [];
|
||||||
|
const processedMarkers = [];
|
||||||
|
for (const waypoint of newWaypoints) {
|
||||||
|
// skip waypoints older than configured waypoint max age
|
||||||
|
if (config.waypointsMaxAge !== null) {
|
||||||
|
const lastUpdatedAgeInMillis = now.diff(moment(waypoint.updated_at));
|
||||||
|
if (lastUpdatedAgeInMillis > config.waypointsMaxAge * 1000) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip expired waypoints
|
||||||
|
if (waypoint.expire < Date.now() / 1000) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip waypoints without position
|
||||||
|
if (!waypoint.latitude || !waypoint.longitude) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip nodes with invalid position
|
||||||
|
if (isNaN(waypoint.latitude) || isNaN(waypoint.longitude)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// fix lat long
|
||||||
|
waypoint.latitude = waypoint.latitude / 10000000;
|
||||||
|
waypoint.longitude = waypoint.longitude / 10000000;
|
||||||
|
|
||||||
|
// TODO: determine emoji to show as marker icon
|
||||||
|
const emoji = waypoint.icon === 0 ? 128205 : waypoint.icon;
|
||||||
|
const emojiText = String.fromCodePoint(emoji);
|
||||||
|
|
||||||
|
if (!isValidCoordinates(waypoint.latitude, waypoint.longitude)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// create waypoint marker
|
||||||
|
const marker = {
|
||||||
|
type: 'Feature',
|
||||||
|
properties: {
|
||||||
|
layer: 'waypoints',
|
||||||
|
},
|
||||||
|
geometry: {
|
||||||
|
type: 'Point',
|
||||||
|
coordinates: [waypoint.longitude, waypoint.latitude]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// add waypoint & marker to cache
|
||||||
|
processedWaypoints.push(waypoint);
|
||||||
|
processedMarkers.push(marker);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return processed data (waypoints and markers)
|
||||||
|
return { processedWaypoints, processedMarkers };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Process new data and store it (bulk processing)
|
||||||
|
const parseWaypointsResponse = (newWaypoints) => {
|
||||||
|
const { processedWaypoints, processedMarkers } = processNewWaypoints(newWaypoints);
|
||||||
|
|
||||||
|
// Clear old data and update in bulk
|
||||||
|
mapStore.clearWaypoints();
|
||||||
|
mapStore.setWaypoints(processedWaypoints);
|
||||||
|
mapStore.setWaypointMarkers(processedMarkers);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
parseWaypointsResponse,
|
||||||
|
};
|
||||||
|
};
|
@ -1,27 +1,3 @@
|
|||||||
import { useStorage } from '@vueuse/core';
|
|
||||||
|
|
||||||
// static
|
// static
|
||||||
export const CURRENT_ANNOUNCEMENT_ID = 1;
|
export const CURRENT_ANNOUNCEMENT_ID = 1;
|
||||||
export const BASE_PATH = 'http://localhost:9090';
|
export const BASE_PATH = 'http://localhost:9090';
|
||||||
|
|
||||||
// boolean
|
|
||||||
export const autoUpdatePositionInUrl = useStorage('auto-update-url', true);
|
|
||||||
export const enableMapAnimations = useStorage('map-animations', true);
|
|
||||||
export const hasSeenInfoModal = useStorage('seen-info-modal', false);
|
|
||||||
// time in seconds
|
|
||||||
export const nodesMaxAge = useStorage('nodes-max-age', null);
|
|
||||||
export const nodesDisconnectedAge = useStorage('nodes-max-disconnected-age', 604800);
|
|
||||||
export const nodesOfflineAge = useStorage('nodes-offline-age', null);
|
|
||||||
export const waypointsMaxAge = useStorage('waypoints-max-age', 604800);
|
|
||||||
// number
|
|
||||||
export const goToNodeZoomLevel = useStorage('zoom-to-node', 15);
|
|
||||||
export const lastSeenAnnouncementId = useStorage('last-seen-announcement-id', 1);
|
|
||||||
// distance in meters
|
|
||||||
export const neighboursMaxDistance = useStorage('neighbors-distance', null);
|
|
||||||
// device info ranges
|
|
||||||
export const deviceMetricsTimeRange = useStorage('device-metrics-range', '3d');
|
|
||||||
export const powerMetricsTimeRange = useStorage('power-metrics-range', '3d');
|
|
||||||
export const environmentMetricsTimeRange = useStorage('environment-metrics-range', '3d');
|
|
||||||
// map config
|
|
||||||
export const enabledOverlayLayers = useStorage('enabled-overlay-layers', ['Legend', 'Position History']);
|
|
||||||
export const selectedTileLayerName = useStorage('selected-tile-layer', 'OpenStreetMap');
|
|
@ -1,8 +1,6 @@
|
|||||||
export default {
|
export default {
|
||||||
mounted(el, binding) {
|
mounted(el, binding) {
|
||||||
console.log('mounted')
|
|
||||||
el._clickOutsideHandler = (event) => {
|
el._clickOutsideHandler = (event) => {
|
||||||
console.log('handler')
|
|
||||||
if (!(el === event.target || el.contains(event.target))) {
|
if (!(el === event.target || el.contains(event.target))) {
|
||||||
binding.value(event);
|
binding.value(event);
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,9 @@ export function setMap(map) {
|
|||||||
export function getMap() {
|
export function getMap() {
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
export function unsetMap() {
|
||||||
|
instance = null;
|
||||||
|
}
|
||||||
|
|
||||||
export const layerGroups = {
|
export const layerGroups = {
|
||||||
nodes: {},
|
nodes: {},
|
||||||
|
@ -1,25 +1,19 @@
|
|||||||
import { reactive, computed } from 'vue';
|
import { reactive } from 'vue';
|
||||||
|
|
||||||
export const state = reactive({
|
export const state = reactive({
|
||||||
// state
|
// state
|
||||||
searchText: '', // moved to ui store, maybe should not be there though?
|
searchText: '', // moved to ui store, maybe should not be there though?
|
||||||
selectedNodeOutlineCircle: null,
|
selectedNodeOutlineCircle: null,
|
||||||
selectedNodeMqttMetrics: [],
|
|
||||||
selectedNodeTraceroutes: [],
|
// new selected node/data stuff
|
||||||
selectedNodeDeviceMetrics: [],
|
positionHistoryNode: null,
|
||||||
selectedNodePowerMetrics: [],
|
neighborsNode: null,
|
||||||
selectedNodeEnvironmentMetrics: [],
|
traceRouteData: null,
|
||||||
selectedNodeToShowNeighbours: null,
|
|
||||||
selectedNodeToShowNeighbours: null,
|
// new modal specific stuff
|
||||||
selectedNodeToShowNeighboursType: null,
|
neighborsModalType: null,
|
||||||
|
|
||||||
// position history
|
// position history
|
||||||
selectedNodeToShowPositionHistory: null,
|
|
||||||
positionHistoryDateTimeTo: null,
|
positionHistoryDateTimeTo: null,
|
||||||
positionHistoryDateTimeFrom: null,
|
positionHistoryDateTimeFrom: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const selectedNodeLatestPowerMetric = computed(() => {
|
|
||||||
const [ latestPowerMetric ] = state.selectedNodePowerMetrics.slice(-1);
|
|
||||||
return latestPowerMetric;
|
|
||||||
});
|
|
@ -21,7 +21,7 @@ export const useConfigStore = defineStore('config', {
|
|||||||
lastSeenAnnouncementId: 1,
|
lastSeenAnnouncementId: 1,
|
||||||
|
|
||||||
// Distance values (for max distances)
|
// Distance values (for max distances)
|
||||||
neighboursMaxDistance: null,
|
neighborsMaxDistance: null,
|
||||||
|
|
||||||
// Device info ranges (can be persisted)
|
// Device info ranges (can be persisted)
|
||||||
deviceMetricsTimeRange: '3d',
|
deviceMetricsTimeRange: '3d',
|
||||||
|
@ -1,55 +1,111 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
|
import RBush from 'rbush';
|
||||||
|
import { point, bbox, distance } from '@turf/turf';
|
||||||
|
|
||||||
export const useMapStore = defineStore('map', {
|
export const useMapStore = defineStore('map', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
nodes: [],
|
// core stuff
|
||||||
waypoints: [],
|
nodes: [],
|
||||||
nodeMarkers: {},
|
nodeIndex: new RBush(),
|
||||||
waypointMarkers: [],
|
waypoints: [],
|
||||||
}),
|
nodeMarkers: {},
|
||||||
actions: {
|
waypointMarkers: [],
|
||||||
setNodes(nodes) {
|
// temp stuff
|
||||||
this.nodes = nodes;
|
positionHistory: [],
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
// Bulk set nodes and rebuild the spatial index
|
||||||
|
setNodes(nodes) {
|
||||||
|
this.nodes = nodes;
|
||||||
|
this.indexNodes(nodes); // Efficient reindexing after bulk update
|
||||||
|
},
|
||||||
|
|
||||||
|
// Bulk set node markers
|
||||||
|
setNodeMarkers(markers) {
|
||||||
|
this.nodeMarkers = markers;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Clear nodes and markers
|
||||||
|
clearNodes() {
|
||||||
|
this.nodes = [];
|
||||||
|
this.nodeMarkers = {};
|
||||||
|
this.nodeIndex.clear(); // Clear the spatial index when nodes are cleared
|
||||||
|
},
|
||||||
|
|
||||||
|
// Bulk set waypoints
|
||||||
|
setWaypoints(waypoints) {
|
||||||
|
this.waypoints = waypoints;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Bulk set waypoint markers
|
||||||
|
setWaypointMarkers(markers) {
|
||||||
|
this.waypointMarkers = markers;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Clear waypoints and markers
|
||||||
|
clearWaypoints() {
|
||||||
|
this.waypoints = [];
|
||||||
|
this.waypointMarkers = [];
|
||||||
|
},
|
||||||
|
|
||||||
|
// Find a node by ID
|
||||||
|
findNodeById(id) {
|
||||||
|
return this.nodes.find(node => node.node_id.toString() === id.toString()) ?? null;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Find a node marker by ID
|
||||||
|
findNodeMarkerById(id) {
|
||||||
|
return this.nodeMarkers[id] ?? null;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Find a random node
|
||||||
|
findRandomNode() {
|
||||||
|
return this.nodes[Math.floor(Math.random() * this.nodes.length)] ?? null;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Rebuild the spatial index for a set of nodes
|
||||||
|
indexNodes(nodes) {
|
||||||
|
// Clear the previous index
|
||||||
|
this.nodeIndex.clear();
|
||||||
|
|
||||||
|
// Rebuild the index for the new set of nodes
|
||||||
|
nodes.forEach((node) => {
|
||||||
|
const nodeMarker = this.findNodeMarkerById(node.node_id);
|
||||||
|
if (nodeMarker?.geometry?.coordinates) {
|
||||||
|
const coords = nodeMarker.geometry.coordinates;
|
||||||
|
const box = bbox(point(coords)); // Get the bounding box for the point
|
||||||
|
this.nodeIndex.insert({ minX: box[0], minY: box[1], maxX: box[2], maxY: box[3], node });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// Find nearby nodes within a specific distance
|
||||||
|
findNearbyNodes(node, maxDistance, checkForHeard) {
|
||||||
|
const nodeMarker = this.findNodeMarkerById(node.node_id);
|
||||||
|
if (!nodeMarker?.geometry?.coordinates) return [];
|
||||||
|
|
||||||
|
const coords = nodeMarker.geometry.coordinates;
|
||||||
|
const searchBox = bbox(point(coords)); // Get bounding box around the point
|
||||||
|
|
||||||
|
// Query the spatial index to find nearby nodes
|
||||||
|
const nearbyNodes = this.nodeIndex.search({
|
||||||
|
minX: searchBox[0], minY: searchBox[1], maxX: searchBox[2], maxY: searchBox[3]
|
||||||
|
});
|
||||||
|
|
||||||
|
return nearbyNodes.filter(({ node: nearbyNode }) => {
|
||||||
|
const nearbyNodeMarker = this.findNodeMarkerById(nearbyNode.node_id);
|
||||||
|
const distanceToNode = distance(nodeMarker, nearbyNodeMarker, { units: 'meters' });
|
||||||
|
// For 'theyHeard', check if current node is in the nearby node's neighbors
|
||||||
|
if (checkForHeard) {
|
||||||
|
const match = nearbyNode.neighbors?.find(n => n.node_id === node.node_id);
|
||||||
|
return match && distanceToNode <= maxDistance;
|
||||||
|
} else {
|
||||||
|
return distanceToNode <= maxDistance;
|
||||||
|
}
|
||||||
|
}).map(({ node: nearbyNode }) => ({
|
||||||
|
node: nearbyNode,
|
||||||
|
neighborData: nearbyNode.neighbors?.find(n => n.node_id === node.node_id),
|
||||||
|
}));
|
||||||
|
},
|
||||||
},
|
},
|
||||||
setNodeMarkers(markers) {
|
|
||||||
this.nodeMarkers = markers;
|
|
||||||
},
|
|
||||||
addNode(node, marker) {
|
|
||||||
// TODO do validation i.e. does it exist already?
|
|
||||||
this.nodes.push(node);
|
|
||||||
// allow undefined/null marker (nodes don't always have a marker)
|
|
||||||
if (typeof marker === 'object') this.addNodeMarker(marker);
|
|
||||||
},
|
|
||||||
addNodeMarker(marker) {
|
|
||||||
// TODO do validation checking here -- i.e. do we have the node? is marker.proprties.id set? does it exist already?
|
|
||||||
this.nodeMarkers[marker.properties.id] = marker;
|
|
||||||
},
|
|
||||||
clearNodes() {
|
|
||||||
this.nodes = [];
|
|
||||||
this.nodeMarkers = {};
|
|
||||||
},
|
|
||||||
setWaypoints(waypoints) {
|
|
||||||
this.waypoints = waypoints;
|
|
||||||
},
|
|
||||||
addWaypoint(waypoint, marker) {
|
|
||||||
this.waypoints.push(waypoint);
|
|
||||||
this.addWaypointMarker(marker);
|
|
||||||
},
|
|
||||||
addWaypointMarker(marker) {
|
|
||||||
this.waypointMarkers.push(marker);
|
|
||||||
},
|
|
||||||
clearWaypoints() {
|
|
||||||
this.waypoints = [];
|
|
||||||
this.waypointMarkers = [];
|
|
||||||
},
|
|
||||||
findNodeById(id) {
|
|
||||||
return this.nodes.find((node) => node.node_id.toString() === id.toString()) ?? null;
|
|
||||||
},
|
|
||||||
findNodeMarkerById(id) {
|
|
||||||
return this.nodeMarkers[id] ?? null;
|
|
||||||
},
|
|
||||||
findRandomNode() {
|
|
||||||
return this.nodes[Math.floor(Math.random() * this.nodes.length)] ?? null;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
@ -12,15 +12,18 @@ import NodeTooltip from '@/components/NodeTooltip.vue';
|
|||||||
|
|
||||||
import { useUIStore } from '@/stores/uiStore';
|
import { useUIStore } from '@/stores/uiStore';
|
||||||
import { useMapStore } from '@/stores/mapStore';
|
import { useMapStore } from '@/stores/mapStore';
|
||||||
|
import { useConfigStore } from '@/stores/configStore';
|
||||||
import { useNodeProcessor } from '@/composables/useNodeProcessor';
|
import { useNodeProcessor } from '@/composables/useNodeProcessor';
|
||||||
|
import { useWaypointProcessor } from '@/composables/useWaypointProcessor';
|
||||||
|
|
||||||
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import maplibregl from 'maplibre-gl';
|
import maplibregl from 'maplibre-gl';
|
||||||
|
import { point, distance } from '@turf/turf';
|
||||||
import LegendControl from '@/LegendControl';
|
import LegendControl from '@/LegendControl';
|
||||||
import LayerControl from '@/LayerControl';
|
import LayerControl from '@/LayerControl';
|
||||||
import { onMounted, useTemplateRef, ref, watch, markRaw, nextTick, createApp, shallowRef } from 'vue';
|
import { onMounted, useTemplateRef, ref, watch, createApp, shallowRef, onUnmounted } from 'vue';
|
||||||
import { state } from '@/store';
|
import { state } from '@/store';
|
||||||
import {
|
import {
|
||||||
layerGroups,
|
layerGroups,
|
||||||
@ -32,30 +35,15 @@ import {
|
|||||||
clearAllNeighbors,
|
clearAllNeighbors,
|
||||||
clearAllWaypoints,
|
clearAllWaypoints,
|
||||||
clearAllPositionHistory,
|
clearAllPositionHistory,
|
||||||
cleanUpPositionHistory,
|
|
||||||
closeAllTooltips,
|
closeAllTooltips,
|
||||||
closeAllPopups,
|
closeAllPopups,
|
||||||
cleanUpNodeNeighbors,
|
|
||||||
clearNodeOutline,
|
clearNodeOutline,
|
||||||
clearMap,
|
clearMap,
|
||||||
setMap,
|
setMap,
|
||||||
getMap,
|
getMap,
|
||||||
|
unsetMap,
|
||||||
} from '@/map';
|
} from '@/map';
|
||||||
import {
|
import { CURRENT_ANNOUNCEMENT_ID } from '@/config';
|
||||||
nodesMaxAge,
|
|
||||||
nodesDisconnectedAge,
|
|
||||||
nodesOfflineAge,
|
|
||||||
waypointsMaxAge,
|
|
||||||
enableMapAnimations,
|
|
||||||
goToNodeZoomLevel,
|
|
||||||
autoUpdatePositionInUrl,
|
|
||||||
neighboursMaxDistance,
|
|
||||||
enabledOverlayLayers,
|
|
||||||
selectedTileLayerName,
|
|
||||||
hasSeenInfoModal,
|
|
||||||
lastSeenAnnouncementId,
|
|
||||||
CURRENT_ANNOUNCEMENT_ID,
|
|
||||||
} from '@/config';
|
|
||||||
import {
|
import {
|
||||||
getColorForSnr,
|
getColorForSnr,
|
||||||
getPositionPrecisionInMeters,
|
getPositionPrecisionInMeters,
|
||||||
@ -75,18 +63,20 @@ const mapEl = useTemplateRef('appMap');
|
|||||||
const popup = shallowRef(null); // Keep a single popup reference
|
const popup = shallowRef(null); // Keep a single popup reference
|
||||||
const popupTarget = ref(null); // DOM container for Vue teleport
|
const popupTarget = ref(null); // DOM container for Vue teleport
|
||||||
const isTooltipLocked = ref(false); // Locked open (via click)
|
const isTooltipLocked = ref(false); // Locked open (via click)
|
||||||
const selectedNode = ref(null);
|
const selectedNode = ref(null); // related to tooltip only
|
||||||
|
|
||||||
const ui = useUIStore();
|
const ui = useUIStore();
|
||||||
const mapData = useMapStore();
|
const mapData = useMapStore();
|
||||||
|
const config = useConfigStore();
|
||||||
const { parseNodesResponse } = useNodeProcessor();
|
const { parseNodesResponse } = useNodeProcessor();
|
||||||
|
const { parseWaypointsResponse } = useWaypointProcessor();
|
||||||
|
|
||||||
// watchers
|
// watchers
|
||||||
watch(
|
watch(
|
||||||
() => state.positionHistoryDateTimeTo,
|
() => state.positionHistoryDateTimeTo,
|
||||||
(newValue) => {
|
(newValue) => {
|
||||||
if (newValue != null) {
|
if (newValue != null) {
|
||||||
loadNodePositionHistory(state.selectedNodeToShowPositionHistory.node_id);
|
loadNodePositionHistory(state.positionHistoryNode.node_id);
|
||||||
}
|
}
|
||||||
}, {deep: true}
|
}, {deep: true}
|
||||||
);
|
);
|
||||||
@ -94,30 +84,60 @@ watch(
|
|||||||
() => state.positionHistoryDateTimeFrom,
|
() => state.positionHistoryDateTimeFrom,
|
||||||
(newValue) => {
|
(newValue) => {
|
||||||
if (newValue != null) {
|
if (newValue != null) {
|
||||||
loadNodePositionHistory(state.selectedNodeToShowPositionHistory.node_id);
|
loadNodePositionHistory(state.positionHistoryNode.node_id);
|
||||||
}
|
}
|
||||||
}, {deep: true}
|
}, {deep: true}
|
||||||
);
|
);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => mapData.nodeMarkers,
|
() => mapData.nodeMarkers,
|
||||||
(newMarkers, oldMarkers) => {
|
(newMarkers) => {
|
||||||
// This will trigger whenever nodeMarkers change
|
// This will trigger whenever nodeMarkers change
|
||||||
updateMapNodeSource(newMarkers);
|
updateMapSourceData('nodes', newMarkers);
|
||||||
},
|
},
|
||||||
{ deep: true } // Ensure that nested changes are also observed
|
{ deep: true } // Ensure that nested changes are also observed
|
||||||
);
|
);
|
||||||
|
|
||||||
function updateMapNodeSource(markers) {
|
watch(
|
||||||
const source = getMap().getSource('nodes');
|
() => mapData.waypointMarkers,
|
||||||
if (source) {
|
(newMarkers) => {
|
||||||
|
// This will trigger whenever waypointMarkers change
|
||||||
|
updateMapSourceData('waypoints', newMarkers);
|
||||||
|
},
|
||||||
|
{ deep: true } // Ensure that nested changes are also observed
|
||||||
|
);
|
||||||
|
|
||||||
|
function updateMapSourceData(sourceName, features) {
|
||||||
|
const source = getMap().getSource(sourceName);
|
||||||
|
if (source !== null) {
|
||||||
source.setData({
|
source.setData({
|
||||||
type: 'FeatureCollection',
|
type: 'FeatureCollection',
|
||||||
features: Object.values(markers),
|
features: Object.values(features),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resetNodeNeighbors() {
|
||||||
|
state.neighborsNode = null;
|
||||||
|
state.neighborsModalType = null;
|
||||||
|
cleanUpNodeNeighbors();
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanUpNodeNeighbors() {
|
||||||
|
// do map stuff (clean up markers and whatnot)
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetPositionHistory() {
|
||||||
|
state.positionHistoryNode = null; // clear node, closes ui
|
||||||
|
mapStore.positionHistory = []; // clears out position history cache
|
||||||
|
cleanUpPositionHistory();
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanUpPositionHistory() {
|
||||||
|
// do map stuff (clean up markers and whatnot)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: this still scales pretty badly, also colors are off
|
||||||
function showNodeOutline(id) {
|
function showNodeOutline(id) {
|
||||||
// remove any existing node circle
|
// remove any existing node circle
|
||||||
clearNodeOutline();
|
clearNodeOutline();
|
||||||
@ -141,7 +161,6 @@ function showNodeOutline(id) {
|
|||||||
let adjustedRadius = radiusInMeters * zoomFactor;
|
let adjustedRadius = radiusInMeters * zoomFactor;
|
||||||
// Set a minimum radius (e.g., 10 meters) to avoid disappearing circles
|
// Set a minimum radius (e.g., 10 meters) to avoid disappearing circles
|
||||||
adjustedRadius = Math.max(adjustedRadius, 10);
|
adjustedRadius = Math.max(adjustedRadius, 10);
|
||||||
console.log(adjustedRadius)
|
|
||||||
|
|
||||||
// Create a circle as a GeoJSON feature
|
// Create a circle as a GeoJSON feature
|
||||||
const geojsonCircle = {
|
const geojsonCircle = {
|
||||||
@ -151,330 +170,125 @@ function showNodeOutline(id) {
|
|||||||
coordinates: nodeMarker.geometry.coordinates,
|
coordinates: nodeMarker.geometry.coordinates,
|
||||||
},
|
},
|
||||||
properties: {
|
properties: {
|
||||||
radius: adjustedRadius // You can store the radius in the properties if needed
|
radius: adjustedRadius,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
getMap().getSource('node-outlines').setData(geojsonCircle);
|
getMap().getSource('node-outlines').setData(geojsonCircle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showNeighbors(type, id) {
|
function showNodeNeighbors(id, direction = 'weHeard') {
|
||||||
let func = showNodeNeighboursThatHeardUs;
|
|
||||||
if (type === 'weHeard') func = showNodeNeighboursThatWeHeard;
|
|
||||||
func(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
function showNodeNeighboursThatHeardUs(id) {
|
|
||||||
cleanUpNodeNeighbors();
|
cleanUpNodeNeighbors();
|
||||||
|
|
||||||
// find node
|
const node = useMapStore().findNodeById(id);
|
||||||
const node = mapData.findNodeById(id);
|
if (!node) return;
|
||||||
if (!node) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// find node marker
|
const nodeMarker = useMapStore().findNodeMarkerById(node.node_id);
|
||||||
const nodeMarker = mapData.findNodeMarkerById(node.node_id);
|
if (!nodeMarker?.geometry?.coordinates) return;
|
||||||
if (!nodeMarker) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// show overlay
|
state.neighborsNode = node;
|
||||||
state.selectedNodeToShowNeighbours = node;
|
state.neighborsModalType = direction;
|
||||||
state.selectedNodeToShowNeighboursType = 'theyHeard';
|
|
||||||
|
|
||||||
// find all nodes that have us as a neighbour
|
const neighborFeatures = [];
|
||||||
const neighbourNodeInfos = [];
|
const neighbors = direction === 'weHeard' ? node.neighbors ?? [] : mapData.findNearbyNodes(node, config.neighborsMaxDistance, true);
|
||||||
for (const nodeThatMayHaveHeardUs of mapData.nodes) {
|
|
||||||
// find our node in this nodes neighbours
|
|
||||||
const nodeNeighbours = nodeThatMayHaveHeardUs.neighbours ?? [];
|
|
||||||
const neighbour = nodeNeighbours.find(function(neighbour) {
|
|
||||||
return neighbour.node_id.toString() === node.node_id.toString();
|
|
||||||
});
|
|
||||||
|
|
||||||
// we exist as a neighbour
|
// Process neighbors
|
||||||
if (neighbour) {
|
neighbors.forEach((neighborData) => {
|
||||||
neighbourNodeInfos.push({
|
const neighborNode = direction === 'weHeard' ? neighborData : neighborData.node;
|
||||||
node: nodeThatMayHaveHeardUs,
|
const neighbor = direction === 'weHeard' ? neighborData : neighborData.neighborData;
|
||||||
neighbour: neighbour,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensure we have neighbours to show
|
if (neighbor.snr === 0) return;
|
||||||
if (neighbourNodeInfos.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// add node neighbours
|
const neighborNodeMarker = useMapStore().findNodeMarkerById(neighborNode.node_id);
|
||||||
for (const neighbourNodeInfo of neighbourNodeInfos) {
|
if (!neighborNodeMarker?.geometry?.coordinates) return;
|
||||||
|
|
||||||
const neighbourNode = neighbourNodeInfo.node;
|
// Calculate the distance in meters between the current node and the neighbor
|
||||||
const neighbour = neighbourNodeInfo.neighbour;
|
const from = point(neighborNodeMarker.geometry.coordinates);
|
||||||
|
const to = point(nodeMarker.geometry.coordinates);
|
||||||
|
const distanceInMeters = distance(from, to, { units: 'meters' }).toFixed(2);
|
||||||
|
|
||||||
// fixme: skipping zero snr? saw some crazy long neighbours with zero snr...
|
// Enforce max distance for weHeaerd
|
||||||
if (neighbour.snr === 0) {
|
if (config.neighborsMaxDistance != null && parseFloat(distanceInMeters) > config.neighborsMaxDistance && direction === 'weHeard') {
|
||||||
continue;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// find neighbour node marker
|
// Create the neighbor feature
|
||||||
const neighbourNodeMarker = mapData.findNodeMarkerById(neighbourNode.node_id);
|
const feature = createNeighborFeature(node, neighborNode, neighbor.snr, direction, distanceInMeters);
|
||||||
if (!neighbourNodeMarker) {
|
if (feature) {
|
||||||
continue;
|
neighborFeatures.push(feature);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
// calculate distance in meters between nodes (rounded to 2 decimal places)
|
if (neighborFeatures.length > 0) renderNeighborLines(neighborFeatures);
|
||||||
const distanceInMeters = neighbourNodeMarker.getLatLng().distanceTo(nodeMarker.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([
|
|
||||||
nodeMarker.getLatLng(), // from us
|
|
||||||
neighbourNodeMarker.getLatLng(), // to neighbour
|
|
||||||
], {
|
|
||||||
color: getColourForSnr(neighbour.snr),
|
|
||||||
opacity: 1,
|
|
||||||
}).arrowheads({
|
|
||||||
size: '10px',
|
|
||||||
fill: true,
|
|
||||||
offsets: {
|
|
||||||
start: '25px',
|
|
||||||
end: '25px',
|
|
||||||
},
|
|
||||||
}).addTo(layerGroups.nodeNeighbors);
|
|
||||||
|
|
||||||
const tooltip = getNeighbourTooltipContent('theyHeard', 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();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function showNodeNeighboursThatWeHeard(id) {
|
// Create a GeoJSON feature for each neighbor
|
||||||
cleanUpNodeNeighbors();
|
function createNeighborFeature(node, neighborNode, snr, direction, distanceInMeters) {
|
||||||
|
const neighborNodeMarker = useMapStore().findNodeMarkerById(neighborNode.node_id);
|
||||||
|
const nodeMarker = useMapStore().findNodeMarkerById(node.node_id);
|
||||||
|
|
||||||
// find node
|
// Create the GeoJSON line feature for the neighbor connection
|
||||||
const node = mapData.findNodeById(id);
|
return {
|
||||||
if (!node) {
|
id: `line-${node.node_id}-${neighborNode.node_id}`,
|
||||||
return;
|
type: 'Feature',
|
||||||
}
|
geometry: {
|
||||||
|
type: 'LineString',
|
||||||
// find node marker
|
coordinates: [
|
||||||
const nodeMarker = mapData.findNodeMarkerById(node.node_id);
|
neighborNodeMarker.geometry.coordinates, // from neighbor
|
||||||
if (!nodeMarker) {
|
nodeMarker.geometry.coordinates, // to our node
|
||||||
return;
|
],
|
||||||
}
|
},
|
||||||
|
properties: {
|
||||||
// show overlay
|
snr,
|
||||||
state.selectedNodeToShowNeighbours = node;
|
color: getColorForSnr(snr),
|
||||||
state.selectedNodeToShowNeighboursType = 'weHeard';
|
tooltip: getNeighbourTooltipContent(direction, node, neighborNode, distanceInMeters, snr), // TODO
|
||||||
|
},
|
||||||
// ensure we have neighbours to show
|
};
|
||||||
const neighbours = node.neighbours ?? [];
|
|
||||||
if (neighbours.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const neighbour of neighbours) {
|
|
||||||
// fixme: skipping zero snr? saw some crazy long neighbours with zero snr...
|
|
||||||
if (neighbour.snr === 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// find neighbor node
|
|
||||||
const neighbourNode = mapData.findNodeById(neighbour.node_id);
|
|
||||||
if (!neighbourNode) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// find neighbor node marker
|
|
||||||
const neighbourNodeMarker = mapData.findNodeMarkerById(neighbour.node_id);
|
|
||||||
if (!neighbourNodeMarker) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// calculate distance in meters between nodes (rounded to 2 decimal places)
|
|
||||||
const distanceInMeters = nodeMarker.getLatLng().distanceTo(neighbourNodeMarker.getLatLng()).toFixed(2);
|
|
||||||
|
|
||||||
if (neighboursMaxDistance.value != null && parseFloat(distanceInMeters) > neighboursMaxDistance.value) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// add neighbour line to map
|
|
||||||
const line = L.polyline([
|
|
||||||
neighbourNodeMarker.getLatLng(), // from neighbor
|
|
||||||
nodeMarker.getLatLng(), // to us
|
|
||||||
], {
|
|
||||||
color: getColorForSnr(neighbour.snr),
|
|
||||||
opacity: 1,
|
|
||||||
}).arrowheads({
|
|
||||||
size: '10px',
|
|
||||||
fill: true,
|
|
||||||
offsets: {
|
|
||||||
start: '25px',
|
|
||||||
end: '25px',
|
|
||||||
},
|
|
||||||
}).addTo(layerGroups.nodeNeighbors);
|
|
||||||
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onNodesUpdated(nodes) {
|
function renderNeighborLines(features) {
|
||||||
const now = moment();
|
const sourceId = 'node-neighbors';
|
||||||
// clear cach
|
const lineLayerId = 'node-neighbors-line';
|
||||||
mapData.clearNodes();
|
|
||||||
for (const node of nodes) {
|
|
||||||
// skip nodes older than configured node max age
|
|
||||||
if (nodesMaxAge.value) {
|
|
||||||
const lastUpdatedAgeInMillis = now.diff(moment(node.updated_at));
|
|
||||||
if (lastUpdatedAgeInMillis > nodesMaxAge.value * 1000) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// add node to cache
|
// Cleanup any previous layers/sources
|
||||||
mapData.addNode(node);
|
if (map.getLayer(lineLayerId)) map.removeLayer(lineLayerId);
|
||||||
|
if (map.getSource(sourceId)) map.removeSource(sourceId);
|
||||||
|
|
||||||
// skip nodes without position
|
// Add source
|
||||||
if (!node.latitude || !node.longitude) {
|
map.addSource(sourceId, {
|
||||||
continue;
|
type: 'geojson',
|
||||||
}
|
data: {
|
||||||
|
|
||||||
// skip nodes with invalid position
|
|
||||||
if (isNaN(node.latitude) || isNaN(node.longitude)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// fix lat long
|
|
||||||
node.latitude = node.latitude / 10000000;
|
|
||||||
node.longitude = node.longitude / 10000000;
|
|
||||||
|
|
||||||
// icon based on mqtt connection state
|
|
||||||
let icon = icons.mqttDisconnected;
|
|
||||||
|
|
||||||
// use offline icon for nodes older than configured node offline age
|
|
||||||
if (nodesOfflineAge.value) {
|
|
||||||
const lastUpdatedAgeInMillis = now.diff(moment(node.updated_at));
|
|
||||||
if (lastUpdatedAgeInMillis > nodesOfflineAge.value * 1000) {
|
|
||||||
icon = icons.offline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// determine if node was recently heard uplinking packets to mqtt
|
|
||||||
const nodeHasUplinkedToMqttRecently = hasNodeUplinkedToMqttRecently(node);
|
|
||||||
if (nodeHasUplinkedToMqttRecently) {
|
|
||||||
icon = icons.mqttConnected;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isValidCoordinates(node.latitude, node.longitude)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// create node marker
|
|
||||||
const marker = {
|
|
||||||
type: 'Feature',
|
|
||||||
properties: {
|
|
||||||
id: node.node_id,
|
|
||||||
role: node.role_name,
|
|
||||||
layer: 'nodes',
|
|
||||||
color: icon,
|
|
||||||
},
|
|
||||||
geometry: {
|
|
||||||
type: 'Point',
|
|
||||||
coordinates: [node.longitude, node.latitude]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// add marker to cache
|
|
||||||
mapData.addNodeMarker(marker);
|
|
||||||
}
|
|
||||||
// set data
|
|
||||||
const source = getMap().getSource('nodes');
|
|
||||||
if (source) {
|
|
||||||
source.setData({
|
|
||||||
type: 'FeatureCollection',
|
type: 'FeatureCollection',
|
||||||
features: Object.values(mapData.nodeMarkers),
|
features,
|
||||||
});
|
},
|
||||||
}
|
});
|
||||||
}
|
|
||||||
|
|
||||||
function onWaypointsUpdated(waypoints) {
|
// Add solid line layer (no animation, no arrows)
|
||||||
const now = moment();
|
map.addLayer({
|
||||||
// clear cache
|
id: lineLayerId,
|
||||||
mapData.clearWaypoints();
|
type: 'line',
|
||||||
// add waypoints
|
source: sourceId,
|
||||||
for (const waypoint of waypoints) {
|
paint: {
|
||||||
// skip waypoints older than configured waypoint max age
|
'line-color': ['get', 'color'],
|
||||||
if (waypointsMaxAge.value) {
|
'line-width': 2,
|
||||||
const lastUpdatedAgeInMillis = now.diff(moment(waypoint.updated_at));
|
},
|
||||||
if (lastUpdatedAgeInMillis > waypointsMaxAge.value * 1000) {
|
});
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// skip expired waypoints
|
// Interactivity: tooltips/popups
|
||||||
if (waypoint.expire < Date.now() / 1000) {
|
map.on('click', lineLayerId, (e) => {
|
||||||
continue;
|
const feature = e.features[0];
|
||||||
}
|
new maplibregl.Popup()
|
||||||
|
.setLngLat(e.lngLat)
|
||||||
|
.setHTML(feature.properties.tooltip)
|
||||||
|
.addTo(map);
|
||||||
|
});
|
||||||
|
|
||||||
// skip waypoints without position
|
map.on('mouseenter', lineLayerId, () => {
|
||||||
if (!waypoint.latitude || !waypoint.longitude) {
|
map.getCanvas().style.cursor = 'pointer';
|
||||||
continue;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// skip nodes with invalid position
|
map.on('mouseleave', lineLayerId, () => {
|
||||||
if (isNaN(waypoint.latitude) || isNaN(waypoint.longitude)) {
|
map.getCanvas().style.cursor = '';
|
||||||
continue;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// fix lat long
|
|
||||||
waypoint.latitude = waypoint.latitude / 10000000;
|
|
||||||
waypoint.longitude = waypoint.longitude / 10000000;
|
|
||||||
|
|
||||||
// TODO: determine emoji to show as marker icon
|
|
||||||
const emoji = waypoint.icon === 0 ? 128205 : waypoint.icon;
|
|
||||||
const emojiText = String.fromCodePoint(emoji);
|
|
||||||
|
|
||||||
if (!isValidCoordinates(node.latitude, node.longitude)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// create waypoint marker
|
|
||||||
const marker = {
|
|
||||||
type: 'Feature',
|
|
||||||
properties: {
|
|
||||||
layer: 'waypoints',
|
|
||||||
},
|
|
||||||
geometry: {
|
|
||||||
type: 'Point',
|
|
||||||
coordinates: [waypoint.longitude, waypoint.latitude]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// add waypoint & marker to cache
|
|
||||||
mapData.addWaypoint(waypoint, marker);
|
|
||||||
}
|
|
||||||
// set data
|
|
||||||
const source = getMap().getSource('waypoints');
|
|
||||||
if (source) {
|
|
||||||
source.setData({
|
|
||||||
type: 'FeatureCollection',
|
|
||||||
features: Object.values(mapData.waypointMarkers),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
@ -567,7 +381,7 @@ function showNodePositionHistory(nodeId) {
|
|||||||
|
|
||||||
// update ui
|
// update ui
|
||||||
state.selectedNode = null;
|
state.selectedNode = null;
|
||||||
state.selectedNodeToShowPositionHistory = node;
|
state.positionHistoryNode = node;
|
||||||
ui.expandPositionHistoryModal();
|
ui.expandPositionHistoryModal();
|
||||||
|
|
||||||
// close node info tooltip as position history shows under it
|
// close node info tooltip as position history shows under it
|
||||||
@ -583,7 +397,7 @@ function showNodePositionHistory(nodeId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadNodePositionHistory(nodeId) {
|
function loadNodePositionHistory(nodeId) {
|
||||||
state.selectedNodePositionHistory = [];
|
mapStore.positionHistory = [];
|
||||||
axios.get(buildPath(`/api/v1/nodes/${nodeId}/position-history`), {
|
axios.get(buildPath(`/api/v1/nodes/${nodeId}/position-history`), {
|
||||||
params: {
|
params: {
|
||||||
// parse from datetime-local format, and send as unix timestamp in milliseconds
|
// parse from datetime-local format, and send as unix timestamp in milliseconds
|
||||||
@ -591,8 +405,8 @@ function loadNodePositionHistory(nodeId) {
|
|||||||
time_to: moment(state.positionHistoryDateTimeTo, "YYYY-MM-DDTHH:mm").format("x"),
|
time_to: moment(state.positionHistoryDateTimeTo, "YYYY-MM-DDTHH:mm").format("x"),
|
||||||
},
|
},
|
||||||
}).then((response) => {
|
}).then((response) => {
|
||||||
state.selectedNodePositionHistory = response.data.position_history;
|
mapStore.positionHistory = response.data.position_history;
|
||||||
if (state.selectedNodeToShowPositionHistory != null) {
|
if (state.positionHistoryNode != null) {
|
||||||
clearAllPositionHistory();
|
clearAllPositionHistory();
|
||||||
onPositionHistoryUpdated(response.data.position_history);
|
onPositionHistoryUpdated(response.data.position_history);
|
||||||
};
|
};
|
||||||
@ -622,7 +436,7 @@ function goToNode(id, animate, zoom){
|
|||||||
// find node
|
// find node
|
||||||
const node = mapData.findNodeById(id);
|
const node = mapData.findNodeById(id);
|
||||||
if (!node) {
|
if (!node) {
|
||||||
alert("Could not find node: " + id);
|
alert('Could not find node: ' + id);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -641,7 +455,7 @@ function goToNode(id, animate, zoom){
|
|||||||
const coords = nodeMarker.geometry.coordinates;
|
const coords = nodeMarker.geometry.coordinates;
|
||||||
getMap().flyTo({
|
getMap().flyTo({
|
||||||
center: coords,
|
center: coords,
|
||||||
zoom: parseFloat(zoom || goToNodeZoomLevel.value)
|
zoom: parseFloat(zoom || config.goToNodeZoomLevel)
|
||||||
});
|
});
|
||||||
getMap().once('moveend', async () => {
|
getMap().once('moveend', async () => {
|
||||||
// add position bubble for node
|
// add position bubble for node
|
||||||
@ -655,6 +469,14 @@ function goToNode(id, animate, zoom){
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showTraceRoute(traceroute) {
|
||||||
|
state.traceRouteData = traceroute;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetTraceRoute() {
|
||||||
|
state.traceRouteData = null;
|
||||||
|
}
|
||||||
|
|
||||||
function onSearchResultNodeClick(node) {
|
function onSearchResultNodeClick(node) {
|
||||||
// clear search
|
// clear search
|
||||||
ui.search('');
|
ui.search('');
|
||||||
@ -670,6 +492,14 @@ function onSearchResultNodeClick(node) {
|
|||||||
state.selectedNode = node;
|
state.selectedNode = node;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
const map = getMap();
|
||||||
|
if (map !== null) {
|
||||||
|
map.remove();
|
||||||
|
unsetMap();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const bounds = [
|
const bounds = [
|
||||||
[-100, 70], // top left
|
[-100, 70], // top left
|
||||||
@ -686,12 +516,10 @@ onMounted(() => {
|
|||||||
layers: [],
|
layers: [],
|
||||||
glyphs: 'https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf',
|
glyphs: 'https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf',
|
||||||
},
|
},
|
||||||
//center: [-15, 150],
|
|
||||||
center: [0, 0],
|
center: [0, 0],
|
||||||
zoom: 2,
|
zoom: 2,
|
||||||
fadeDuration: 0,
|
fadeDuration: 0,
|
||||||
renderWorldCopies: false
|
renderWorldCopies: false
|
||||||
//maxBounds: [[-180, -85], [180, 85]]
|
|
||||||
});
|
});
|
||||||
setMap(map);
|
setMap(map);
|
||||||
map.addControl(new maplibregl.AttributionControl({ compact: true }), 'bottom-right');
|
map.addControl(new maplibregl.AttributionControl({ compact: true }), 'bottom-right');
|
||||||
@ -704,7 +532,7 @@ onMounted(() => {
|
|||||||
}), 'top-left');
|
}), 'top-left');
|
||||||
const layerControl = new LayerControl({
|
const layerControl = new LayerControl({
|
||||||
maps: tileLayers,
|
maps: tileLayers,
|
||||||
initialMap: 'OpenStreetMap',
|
initialMap: config.selectedTileLayerName,
|
||||||
controls: {
|
controls: {
|
||||||
Nodes: {
|
Nodes: {
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
@ -712,7 +540,7 @@ onMounted(() => {
|
|||||||
layers: {
|
layers: {
|
||||||
'All': {
|
'All': {
|
||||||
type: 'layer_control',
|
type: 'layer_control',
|
||||||
hideAllExcept: ['nodes', 'node-outlines'],
|
hideAllExcept: ['nodes', 'node-outlines', 'node-neighbors-line'],
|
||||||
disableCluster: 'nodes',
|
disableCluster: 'nodes',
|
||||||
},
|
},
|
||||||
'Routers': {
|
'Routers': {
|
||||||
@ -723,7 +551,7 @@ onMounted(() => {
|
|||||||
},
|
},
|
||||||
'Clustered': {
|
'Clustered': {
|
||||||
type: 'layer_control',
|
type: 'layer_control',
|
||||||
hideAllExcept: ['clusters', 'unclustered-points', 'cluster-count', 'node-outlines'],
|
hideAllExcept: ['clusters', 'unclustered-points', 'cluster-count', 'node-outlines', 'node-neighbors-line'],
|
||||||
},
|
},
|
||||||
'None': {
|
'None': {
|
||||||
type: 'layer_control',
|
type: 'layer_control',
|
||||||
@ -733,7 +561,7 @@ onMounted(() => {
|
|||||||
},
|
},
|
||||||
Overlays: {
|
Overlays: {
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
default: ['Legend', 'Position History'],
|
default: config.enabledOverlayLayers,
|
||||||
layers: {
|
layers: {
|
||||||
'Legend': {
|
'Legend': {
|
||||||
type: 'toggle_element',
|
type: 'toggle_element',
|
||||||
@ -927,7 +755,6 @@ function measureTooltipSize(node) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function openLockedTooltipFromNode(feature) {
|
async function openLockedTooltipFromNode(feature) {
|
||||||
console.log('openLockedTooltipFromNode', feature);
|
|
||||||
const nodeId = feature?.properties?.id;
|
const nodeId = feature?.properties?.id;
|
||||||
const node = mapData.findNodeById(nodeId ?? '');
|
const node = mapData.findNodeById(nodeId ?? '');
|
||||||
const coordinates = feature?.geometry?.coordinates?.slice();
|
const coordinates = feature?.geometry?.coordinates?.slice();
|
||||||
@ -1122,10 +949,10 @@ async function determineAnchorForNode(node, coordinates) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (lastSeenAnnouncementId.value !== CURRENT_ANNOUNCEMENT_ID) {
|
if (config.lastSeenAnnouncementId !== CURRENT_ANNOUNCEMENT_ID) {
|
||||||
ui.showAnnouncement();
|
ui.showAnnouncement();
|
||||||
}
|
}
|
||||||
if (!isMobile() && hasSeenInfoModal.value === false) {
|
if (!isMobile() && config.hasSeenInfoModal === false) {
|
||||||
ui.showInfoModal();
|
ui.showInfoModal();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -1148,11 +975,11 @@ onMounted(() => {
|
|||||||
<InfoModal />
|
<InfoModal />
|
||||||
<HardwareModelList />
|
<HardwareModelList />
|
||||||
<Settings />
|
<Settings />
|
||||||
<NodeInfo @show-position-history="showNodePositionHistory"/>
|
<NodeInfo @show-position-history="showNodePositionHistory" @show-trace-route="showTraceRoute"/>
|
||||||
<NodeNeighborsModal @dismiss="cleanUpNodeNeighbors" />
|
<NodeNeighborsModal @dismiss="resetNodeNeighbors" :node="state.neighborsNode"/>
|
||||||
<NodePositionHistoryModal @dismiss="cleanUpPositionHistory" />
|
<NodePositionHistoryModal @dismiss="resetPositionHistory" :node="state.positionHistoryNode"/>
|
||||||
<TracerouteInfo @go-to="goToNode" />
|
<TracerouteInfo @go-to="goToNode" @dismiss="resetTraceRoute" :data="state.traceRouteData"/>
|
||||||
<Teleport v-if="popupTarget && selectedNode" :to="popupTarget">
|
<Teleport v-if="popupTarget && selectedNode" :to="popupTarget">
|
||||||
<NodeTooltip :node="selectedNode" @show-neighbors="showNeighbors"/>
|
<NodeTooltip :node="selectedNode" @show-neighbors="showNodeNeighbors"/>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
</template>
|
</template>
|
Reference in New Issue
Block a user