<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 'leaflet/dist/leaflet';
const L = window.L;
import 'leaflet-geometryutil';
import 'leaflet-arrowheads';
import 'leaflet.markercluster';
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;
            }
        }

        // add node to cache
        state.nodes.push(node);

        // 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.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;
        }

        // 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 marker to cache
        state.nodeMarkers[node.node_id] = marker;

        // show node info tooltip when clicking node marker
        marker.on('click', function(event) {
            // close all other popups and tooltips
            closeAllTooltips();
            closeAllPopups();

            // find node
            const node = findNodeById(event.target.options.tagName);
            if (!node){
                return;
            }

            // show position precision outline
            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>