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 { PageLayout } from "@components/PageLayout.tsx"; import { Sidebar } from "@components/Sidebar.tsx"; import { useDevice, useNodeDB } from "@core/stores"; import { cn } from "@core/utils/cn.ts"; import type { Protobuf } from "@meshtastic/core"; import { bbox, lineString } from "@turf/turf"; import { FunnelIcon, MapPinIcon } from "lucide-react"; import { useCallback, useDeferredValue, useMemo, useRef, useState, } from "react"; import { Marker, Popup, useMap } from "react-map-gl/maplibre"; import { NodeDetail } from "../../components/PageComponents/Map/NodeDetail.tsx"; import { Avatar } from "../../components/UI/Avatar.tsx"; const NODEDB_DEBOUNCE_MS = 250; type NodePosition = { latitude: number; longitude: number; }; const convertToLatLng = (position?: { latitudeI?: number; longitudeI?: number; }): NodePosition => ({ latitude: (position?.latitudeI ?? 0) / 1e7, longitude: (position?.longitudeI ?? 0) / 1e7, }); const MapPage = () => { const { waypoints } = useDevice(); const { nodes: validNodes, hasNodeError } = useNodeDB( (db) => ({ // only nodes with a position nodes: db.getNodes((n): n is Protobuf.Mesh.NodeInfo => Boolean(n.position?.latitudeI), ), hasNodeError: db.hasNodeError, // include the Map reference so error badges update when nodeErrors changes _errorsRef: db.nodeErrors, }), { debounce: NODEDB_DEBOUNCE_MS }, ); const { nodeFilter, defaultFilterValues, isFilterDirty } = useFilterNode(); const { default: map } = useMap(); const [selectedNode, setSelectedNode] = useState(null); const [filterState, setFilterState] = useState( () => defaultFilterValues, ); const deferredFilterState = useDeferredValue(filterState); const filteredNodes = useMemo( () => validNodes.filter((node) => nodeFilter(node, deferredFilterState)), [validNodes, deferredFilterState, nodeFilter], ); const hasFitBoundsOnce = useRef(false); const handleMarkerClick = useCallback( (node: Protobuf.Mesh.NodeInfo, event: { originalEvent: MouseEvent }) => { event?.originalEvent?.stopPropagation(); setSelectedNode(node); if (map) { const position = convertToLatLng(node.position); map.easeTo({ center: [position.longitude, position.latitude], zoom: map?.getZoom(), }); } }, [map], ); // Get the bounds of the map based on the nodes furtherest away from center const getMapBounds = useCallback(() => { if (hasFitBoundsOnce.current || !map || validNodes.length === 0) { return; } if (validNodes.length === 1 && validNodes[0]) { map.easeTo({ zoom: map.getZoom(), center: [ (validNodes[0].position?.longitudeI ?? 0) / 1e7, (validNodes[0].position?.latitudeI ?? 0) / 1e7, ], }); return; } const line = lineString( validNodes.map((n) => [ (n.position?.latitudeI ?? 0) / 1e7, (n.position?.longitudeI ?? 0) / 1e7, ]), ); const bounds = bbox(line); const center = map.cameraForBounds( [ [bounds[1], bounds[0]], [bounds[3], bounds[2]], ], { padding: { top: 10, bottom: 10, left: 10, right: 10 } }, ); if (center) { map.easeTo(center); } hasFitBoundsOnce.current = true; }, [map, validNodes]); // Generate all markers const markers = useMemo( () => filteredNodes.map((node) => { const position = convertToLatLng(node.position); return ( handleMarkerClick(node, e)} > ); }), [filteredNodes, handleMarkerClick, hasNodeError], ); return ( }> {waypoints.map((wp) => (
))} {markers} {selectedNode && (() => { const position = convertToLatLng(selectedNode.position); return ( setSelectedNode(null)} className="w-full" > ); })()}
, showTextSearch: true, }} />
); }; export default MapPage;