Files
map/webapp/frontend/src/views/HomeView.vue

911 lines
30 KiB
Vue

<script setup>
import Header from '../components/Header.vue';
import InfoModal from '../components/InfoModal.vue';
import HardwareModelList from '../components/HardwareModelList.vue';
import Settings from '../components/Settings.vue';
import NodeInfo from '../components/NodeInfo.vue';
import NodeNeighborsModal from '../components/NodeNeighborsModal.vue';
import NodePositionHistoryModal from '../components/NodePositionHistoryModal.vue';
import TracerouteInfo from '../components/TracerouteInfo.vue';
import Announcement from '../components/Announcement.vue';
import axios from 'axios';
import moment from 'moment';
import L from 'leaflet/dist/leaflet.js';
import 'leaflet.markercluster/dist/leaflet.markercluster.js';
import 'leaflet-groupedlayercontrol/dist/leaflet.groupedlayercontrol.min.js';
import { onMounted, useTemplateRef, ref, watch, markRaw } from 'vue';
import { state } from '../store.js';
import {
layerGroups,
tileLayers,
icons,
getTooltipContentForWaypoint,
getTooltipContentForNode,
getNeighbourTooltipContent,
clearAllNodes,
clearAllNeighbors,
clearAllWaypoints,
clearAllPositionHistory,
cleanUpPositionHistory,
closeAllTooltips,
closeAllPopups,
cleanUpNodeNeighbors,
clearNodeOutline,
clearMap,
setMap,
getMap,
} from '../map.js';
import {
nodesMaxAge,
nodesDisconnectedAge,
nodesOfflineAge,
waypointsMaxAge,
enableMapAnimations,
goToNodeZoomLevel,
autoUpdatePositionInUrl,
neighboursMaxDistance,
enabledOverlayLayers,
selectedTileLayerName,
hasSeenInfoModal,
lastSeenAnnouncementId,
CURRENT_ANNOUNCEMENT_ID,
} from '../config.js';
import {
getColorForSnr,
getPositionPrecisionInMeters,
getTerrainProfileImage,
getRegionFrequencyRange,
escapeString,
formatPositionPrecision,
isMobile,
elementOrAnyAncestorHasClass,
findNodeById,
findNodeMarkerById,
hasNodeUplinkedToMqttRecently,
buildPath,
} from '../utils.js';
const mapEl = useTemplateRef('appMap');
// watchers
watch(
() => state.positionHistoryDateTimeTo,
(newValue) => {
if (newValue != null) {
loadNodePositionHistory(state.selectedNodeToShowPositionHistory.node_id);
}
}, {deep: true}
);
watch(
() => state.positionHistoryDateTimeFrom,
(newValue) => {
if (newValue != null) {
loadNodePositionHistory(state.selectedNodeToShowPositionHistory.node_id);
}
}, {deep: true}
);
function showNodeOutline(id) {
// remove any existing node circle
clearNodeOutline();
// find node marker by id
const nodeMarker = state.nodeMarkers[id];
if (!nodeMarker) {
return;
}
// find node by id
const node = findNodeById(id);
if (!node) {
return;
}
// add position precision circle around node
if(node.position_precision != null && node.position_precision > 0 && node.position_precision < 32){
state.selectedNodeOutlineCircle = L.circle(nodeMarker.getLatLng(), {
radius: getPositionPrecisionInMeters(node.position_precision),
}).addTo(getMap());
}
}
window.showNodeDetails = function(id) {
// find node
const node = findNodeById(id);
if (!node) {
return;
}
state.selectedNode = node;
}
window.showNodeNeighboursThatHeardUs = function(id) {
cleanUpNodeNeighbors();
// find node
const node = findNodeById(id);
if (!node) {
return;
}
// find node marker
const nodeMarker = findNodeMarkerById(node.node_id);
if (!nodeMarker) {
return;
}
// show overlay
state.selectedNodeToShowNeighbours = node;
state.selectedNodeToShowNeighboursType = 'theyHeard';
// find all nodes that have us as a neighbour
const neighbourNodeInfos = [];
for (const nodeThatMayHaveHeardUs of state.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
if (neighbour) {
neighbourNodeInfos.push({
node: nodeThatMayHaveHeardUs,
neighbour: neighbour,
});
}
}
// ensure we have neighbours to show
if (neighbourNodeInfos.length === 0) {
return;
}
// add node neighbours
for (const neighbourNodeInfo of neighbourNodeInfos) {
const neighbourNode = neighbourNodeInfo.node;
const neighbour = neighbourNodeInfo.neighbour;
// fixme: skipping zero snr? saw some crazy long neighbours with zero snr...
if (neighbour.snr === 0) {
continue;
}
// find neighbour node marker
const neighbourNodeMarker = findNodeMarkerById(neighbourNode.node_id);
if (!neighbourNodeMarker) {
continue;
}
// calculate distance in meters between nodes (rounded to 2 decimal places)
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();
});
}
}
window.showNodeNeighboursThatWeHeard = function(id) {
cleanUpNodeNeighbors();
// find node
const node = findNodeById(id);
if (!node) {
return;
}
// find node marker
const nodeMarker = findNodeMarkerById(node.node_id);
if (!nodeMarker) {
return;
}
// show overlay
state.selectedNodeToShowNeighbours = node;
state.selectedNodeToShowNeighboursType = 'weHeard';
// 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 = findNodeById(neighbour.node_id);
if (!neighbourNode) {
continue;
}
// find neighbor node marker
const neighbourNodeMarker = 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) {
const now = moment();
state.nodes = [];
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;
}
}
// skip nodes without position
if (!node.latitude || !node.longitude) {
continue;
}
// 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;
// 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
let icon = icons.mqttDisconnected;
// use offline icon for nodes older than configured node offline age
if (nodesOfflineAge) {
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;
}
// create node marker
const marker = L.marker([node.latitude, longitude], {
icon: icon,
tagName: node.node_id,
// we want to show online nodes above offline, but without needing to use separate layer groups
zIndexOffset: nodeHasUplinkedToMqttRecently ? 1000 : -1000,
}).on('click', function(event) {
// close tooltip on click to prevent tooltip and popup showing at same time
event.target.closeTooltip();
});
// add marker to node layer groups
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 and marker to cache
state.nodes.push(node);
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
showNodeOutline(node.node_id);
// 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) {
// find current node
const currentNode = findNodeMarkerById(node.node_id);
if (!currentNode) {
continue;
}
// 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();
});
}
}
}
}
function onWaypointsUpdated(waypoints) {
state.waypoints = [];
const now = moment();
// add nodes
for (const waypoint of waypoints) {
// skip waypoints older than configured waypoint max age
if (waypointsMaxAge.value) {
const lastUpdatedAgeInMillis = now.diff(moment(waypoint.updated_at));
if (lastUpdatedAgeInMillis > waypointsMaxAge.value * 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;
// 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
const emoji = waypoint.icon === 0 ? 128205 : waypoint.icon;
const emojiText = String.fromCodePoint(emoji);
let tooltip = getTooltipContentForWaypoint(waypoint);
// 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);
}
}
function onPositionHistoryUpdated(updatedPositionHistories) {
let positionHistoryLinesCords = [];
// add nodes
for (const positionHistory of updatedPositionHistories) {
// skip position history without position
if (!positionHistory.latitude || !positionHistory.longitude) {
continue;
}
// find node this position is for
const node = findNodeById(positionHistory.node_id);
if (!node) {
continue;
}
// skip position history without position
if (!positionHistory.latitude || !positionHistory.longitude) {
continue;
}
// skip nodes with invalid position
if (isNaN(positionHistory.latitude) || isNaN(positionHistory.longitude)) {
continue;
}
// fix lat long
positionHistory.latitude = positionHistory.latitude / 10000000;
positionHistory.longitude = positionHistory.longitude / 10000000;
// wrap longitude for shortest path, everything to left of australia should be shown on the right
let longitude = parseFloat(positionHistory.longitude);
if (longitude <= 100) {
longitude += 360;
}
positionHistoryLinesCords.push([positionHistory.latitude, longitude]);
let tooltip = "";
if(positionHistory.type === "position"){
tooltip += `<b>Position</b>`;
} else if(positionHistory.type === "map_report"){
tooltip += `<b>Map Report</b>`;
}
tooltip += `<br/>[${escapeString(node.short_name)}] ${escapeString(node.long_name)}`;
tooltip += `<br/>${positionHistory.latitude}, ${positionHistory.longitude}`;
tooltip += `<br/>Heard on: ${moment(new Date(positionHistory.created_at)).format("DD/MM/YYYY hh:mm A")}`;
// add gateway info if available
if (positionHistory.gateway_id) {
const gatewayNode = findNodeById(positionHistory.gateway_id);
const gatewayNodeInfo = gatewayNode ? `[${gatewayNode.short_name}] ${gatewayNode.long_name}` : "???";
tooltip += `<br/>Heard by: <a href="javascript:void(0);" onclick="goToNode(${positionHistory.gateway_id})">${gatewayNodeInfo}</a>`;
}
// create position history marker
const marker = L.marker([positionHistory.latitude, longitude],{
icon: iconPositionHistory,
}).bindTooltip(tooltip).bindPopup(tooltip).on('click', function(event) {
// close tooltip on click to prevent tooltip and popup showing at same time
event.target.closeTooltip();
});
// add marker to position history layer group
marker.addTo(layerGroups.nodePositionHistory);
}
// show lines between position history markers
L.polyline(positionHistoryLinesCords).addTo(layerGroups.nodePositionHistory);
}
function goToRandomNode() {
if (state.nodes.length > 0) {
const randomNode = state.nodes[Math.floor(Math.random() * state.nodes.length)];
if (randomNode) {
// go to node
if (goToNode(randomNode.node_id)) {
return;
}
// fallback to showing node details since we can't go to the node
window.showNodeDetails(randomNode.node_id);
}
}
}
function showNodePositionHistory(nodeId) {
// find node
const node = findNodeById(nodeId);
if (!node) {
return;
}
// update ui
state.selectedNode = null;
state.selectedNodeToShowPositionHistory = node;
state.positionHistoryModalExpanded = true;
// close node info tooltip as position history shows under it
closeAllTooltips();
// reset default time range when opening position history ui
// YYYY-MM-DDTHH:mm is the format expected by the datetime-local input type
state.positionHistoryDateTimeFrom = moment().subtract(1, "hours").format('YYYY-MM-DDTHH:mm');
state.positionHistoryDateTimeTo = moment().format('YYYY-MM-DDTHH:mm');
// load position history
loadNodePositionHistory(nodeId);
}
function loadNodePositionHistory(nodeId) {
state.selectedNodePositionHistory = [];
axios.get(buildPath(`/api/v1/nodes/${nodeId}/position-history`), {
params: {
// parse from datetime-local format, and send as unix timestamp in milliseconds
time_from: moment(state.positionHistoryDateTimeFrom, "YYYY-MM-DDTHH:mm").format("x"),
time_to: moment(state.positionHistoryDateTimeTo, "YYYY-MM-DDTHH:mm").format("x"),
},
}).then((response) => {
state.selectedNodePositionHistory = response.data.position_history;
if (state.selectedNodeToShowPositionHistory != null) {
clearAllPositionHistory();
onPositionHistoryUpdated(response.data.position_history);
};
}).catch(() => {
// do nothing
});
}
function reload(goToNodeId, zoom) {
// show loading
state.loading = true;
// clear previous data
clearMap();
axios.get(buildPath('/api/v1/nodes')).then(response => {
// update nodes
onNodesUpdated(response.data.nodes);
// hide loading
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);
}
});
}
function goToNode(id, animate, zoom){
// find node
const node = findNodeById(id);
if (!node) {
alert("Could not find node: " + id);
return false;
}
// find node marker by id
const nodeMarker = findNodeMarkerById(id);
if (!nodeMarker) {
return false;
}
// close all popups and tooltips
closeAllPopups();
closeAllTooltips();
// select node
showNodeOutline(id);
// fly to node marker
const shouldAnimate = animate != null ? animate : true;
getMap().flyTo(nodeMarker.getLatLng(), parseFloat(zoom || goToNodeZoomLevel.value), {
animate: enableMapAnimations.value ? shouldAnimate : false,
});
// open tooltip for node
getMap().openTooltip(getTooltipContentForNode(node), nodeMarker.getLatLng(), {
interactive: true, // allow clicking buttons inside tooltip
permanent: true, // don't auto dismiss when clicking buttons inside tooltip
});
// successfully went to node
return true;
}
function onSearchResultNodeClick(node) {
// clear search
state.searchText = '';
// hide search
state.mobileSearchVisible = false;
// go to node
if (goToNode(node.node_id) ){
return;
}
// fallback to showing node details since we can't go to the node
window.showNodeDetails(node.node_id);
}
onMounted(() => {
// set map bounds to be a little more than full size to prevent panning off screen
const bounds = [
[-100, 70], // top left
[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 selectedTileLayer = tileLayers[selectedTileLayerName.value] || tileLayers['OpenStreetMap'];
selectedTileLayer.addTo(getMap());
// handle baselayerchange to update tile layer preference
getMap().on('baselayerchange', function(event) {
selectedTileLayerName.value = event.name;
});
// create legend
const legend = L.control({position: 'bottomleft'});
legend.onAdd = function (map) {
const div = L.DomUtil.create('div', 'leaflet-control-layers');
div.style.backgroundColor = 'white';
div.style.padding = '12px';
div.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>`;
return div;
};
// 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;
});
});
getMap().on('overlayadd', function(event) {
const layerName = event.name;
if (!enabledOverlayLayers.value.includes(layerName)) {
enabledOverlayLayers.value.push(layerName);
}
});
getMap().on('click', function(event) {
// remove outline when map clicked
clearNodeOutline();
// clear search
state.searchText = '';
state.mobileSearchVisible = false;
// do nothing when clicking inside tooltip
const clickedElement = event.originalEvent.target;
if (elementOrAnyAncestorHasClass(clickedElement, 'leaflet-tooltip')) {
return;
}
closeAllTooltips();
closeAllPopups();
});
// auto update url when lat/lng/zoom changes
getMap().on('moveend zoomend', function() {
// check if user enabled auto updating position in url
if (!autoUpdatePositionInUrl.value) {
return;
}
// get map info
const latLng = getMap().getCenter();
const zoom = getMap().getZoom();
// construct new url
const url = new URL(window.location.href);
url.searchParams.set('lat', latLng.lat);
url.searchParams.set('lng', latLng.lng);
url.searchParams.set('zoom', zoom);
// update current url
if(window.history.replaceState){
window.history.replaceState(null, null, url.toString());
}
});
// parse url params
const queryParams = new URLSearchParams(location.search);
const queryNodeId = queryParams.get('node_id');
const queryLat = queryParams.get('lat');
const queryLng = queryParams.get('lng');
const queryZoom = queryParams.get('zoom');
// go to lat/lng if provided
if(queryLat && queryLng){
const zoomLevel = queryZoom || goToNodeZoomLevel.value
getMap().flyTo([queryLat, queryLng], parseFloat(zoomLevel), {
animate: false,
});
}
// reload and go to provided node id
reload(queryNodeId, queryZoom);
});
onMounted(() => {
if (lastSeenAnnouncementId.value !== CURRENT_ANNOUNCEMENT_ID) {
state.announcementVisible = true;
}
if (!isMobile() && hasSeenInfoModal.value === false) {
state.infoModalVisible = true;
}
})
</script>
<template>
<div class="flex flex-col h-full w-full overflow-hidden">
<div class="flex flex-col h-full">
<Announcement />
<Header
@reload="reload"
@random-node="goToRandomNode"
@search-click="onSearchResultNodeClick"
/>
<div id="map" style="width:100%;height:100%;" ref="appMap"></div>
</div>
</div>
<InfoModal />
<HardwareModelList />
<Settings />
<NodeInfo @show-position-history="showNodePositionHistory"/>
<NodeNeighborsModal @dismiss="cleanUpNodeNeighbors" />
<NodePositionHistoryModal @dismiss="cleanUpPositionHistory" />
<TracerouteInfo @go-to="goToNode" />
</template>