import { defaultVisibilityState, MapLayerTool, type VisibilityState, } from "@app/components/PageComponents/Map/Tools/MapLayerTool.tsx"; import { FilterControl } from "@components/generic/Filter/FilterControl.tsx"; import { type FilterState, useFilterNode } from "@components/generic/Filter/useFilterNode.ts"; import { BaseMap } from "@components/Map.tsx"; import { HeatmapLayer, type HeatmapMode, } from "@components/PageComponents/Map/Layers/HeatmapLayer.tsx"; import { NodesLayer } from "@components/PageComponents/Map/Layers/NodesLayer.tsx"; import { PrecisionLayer } from "@components/PageComponents/Map/Layers/PrecisionLayer.tsx"; import { SNRLayer, SNRTooltip, type SNRTooltipProps, } from "@components/PageComponents/Map/Layers/SNRLayer.tsx"; import { WaypointLayer } from "@components/PageComponents/Map/Layers/WaypointLayer.tsx"; import type { PopupState } from "@components/PageComponents/Map/Popups/PopupWrapper.tsx"; import { PageLayout } from "@components/PageLayout.tsx"; import { Sidebar } from "@components/Sidebar.tsx"; import { useMapFitting } from "@core/hooks/useMapFitting.ts"; import { useNodeDB } from "@core/stores"; import { cn } from "@core/utils/cn.ts"; import { hasPos, toLngLat } from "@core/utils/geo.ts"; import type { Protobuf } from "@meshtastic/core"; import { numberToHexUnpadded } from "@noble/curves/abstract/utils"; import { FunnelIcon, LocateFixedIcon } from "lucide-react"; import { useCallback, useDeferredValue, useId, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { type MapLayerMouseEvent, useMap } from "react-map-gl/maplibre"; const NODEDB_DEBOUNCE_MS = 250; const MapPage = () => { const { t } = useTranslation("map"); const { getNode } = useNodeDB(); const { nodes: validNodes, myNode } = useNodeDB( (db) => ({ // only nodes with a position nodes: db.getNodes((n): n is Protobuf.Mesh.NodeInfo => Boolean(n.position?.latitudeI)), myNode: db.getMyNode(), // References to cause re-render on change _errorsRef: db.nodeErrors, _nodeNumRef: db.myNodeNum, }), { debounce: NODEDB_DEBOUNCE_MS }, ); const { nodeFilter, defaultFilterValues, isFilterDirty } = useFilterNode(); const { default: mapRef } = useMap(); const { focusLngLat, fitToNodes } = useMapFitting(mapRef); const hasFitBoundsOnce = useRef(false); const [snrHover, setSnrHover] = useState(); const [expandedCluster, setExpandedCluster] = useState(); const [popupState, setPopupState] = useState(); const [visibilityState, setVisibilityState] = useState( () => defaultVisibilityState, ); const [heatmapMode, setHeatmapMode] = useState("density"); // Filters const [filterState, setFilterState] = useState(() => defaultFilterValues); const deferredFilterState = useDeferredValue(filterState); const filteredNodes = useMemo( () => validNodes.filter((node) => nodeFilter(node, deferredFilterState)), [validNodes, deferredFilterState, nodeFilter], ); // Map fitting const getMapBounds = useCallback(() => { if (!hasFitBoundsOnce.current) { fitToNodes(validNodes); hasFitBoundsOnce.current = true; } }, [fitToNodes, validNodes]); // SNR lines const snrLayerElementId = useId(); const snrLayerElement = useMemo( () => ( ), [filteredNodes, myNode, visibilityState, snrLayerElementId], ); // Heatmap const heatmapLayerElementId = useId(); const heatmapLayerElement = useMemo( () => ( ), [filteredNodes, visibilityState.heatmap, heatmapMode, heatmapLayerElementId], ); const onMouseMove = useCallback( (event: MapLayerMouseEvent) => { const { features, point: { x, y }, } = event; const hoveredFeature = features?.[0]; if (hoveredFeature) { const { from, to, snr, name, shortName, num } = hoveredFeature.properties; // Handle Heatmap Hover if ( hoveredFeature.layer.id === `${heatmapLayerElementId}-interaction` && name !== undefined ) { setSnrHover({ pos: { x, y }, snr: snr, // Single node SNR from: name || shortName || t("fallbackName", { last4: numberToHexUnpadded(num).slice(-4).toUpperCase(), }), to: undefined, // Single node }); return; } // Handle SNR Line Hover const fromLong = getNode(from)?.user?.longName ?? t("fallbackName", { last4: numberToHexUnpadded(from).slice(-4).toUpperCase(), }); const toLong = getNode(to)?.user?.longName ?? t("fallbackName", { last4: numberToHexUnpadded(to).slice(-4).toUpperCase(), }); setSnrHover({ pos: { x, y }, snr, from: fromLong, to: toLong }); } else { setSnrHover(undefined); } }, [getNode, t, heatmapLayerElementId], ); // Node markers & clusters const onMapBackgroundClick = useCallback(() => { setExpandedCluster(undefined); }, []); const markerElements = useMemo( () => ( ), [filteredNodes, expandedCluster, mapRef, myNode, popupState, visibilityState.nodeMarkers], ); // Precision circles const precisionCirclesElementId = useId(); const precisionCirclesElement = useMemo( () => ( ), [filteredNodes, visibilityState.positionPrecision, precisionCirclesElementId], ); // Waypoints const waypointLayerElement = useMemo( () => ( ), [mapRef, myNode, visibilityState.waypoints, popupState], ); return ( }> {heatmapLayerElement} {markerElements} {snrLayerElement} {precisionCirclesElement} {waypointLayerElement} {snrHover && ( )}
{myNode && hasPos(myNode?.position) && ( )} , showTextSearch: true, }} />
); }; export default MapPage;