import { NodeDetail } from "../../components/PageComponents/Map/NodeDetail.tsx"; import { Avatar } from "../../components/UI/Avatar.tsx"; import { useTheme } from "../../core/hooks/useTheme.ts"; import { PageLayout } from "@components/PageLayout.tsx"; import { Sidebar } from "@components/Sidebar.tsx"; import { useDevice } from "@core/stores/deviceStore.ts"; import type { Protobuf } from "@meshtastic/core"; import { bbox, lineString } from "@turf/turf"; import { MapPinIcon } from "lucide-react"; import { useCallback, useEffect, useMemo, useState } from "react"; import { AttributionControl, GeolocateControl, Marker, NavigationControl, Popup, ScaleControl, useMap, } from "react-map-gl/maplibre"; import MapGl from "react-map-gl/maplibre"; import { useNodeFilters } from "@core/hooks/useNodeFilters.ts"; import { FilterControl } from "@pages/Map/FilterControl.tsx"; 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 { nodes, waypoints } = useDevice(); const { theme } = useTheme(); const { default: map } = useMap(); const darkMode = theme === "dark"; const [selectedNode, setSelectedNode] = useState< Protobuf.Mesh.NodeInfo | null >(null); // Filter out nodes without a valid position const validNodes = useMemo( () => Array.from(nodes.values()).filter( (node): node is Protobuf.Mesh.NodeInfo => Boolean(node.position?.latitudeI), ), [nodes], ); const { filteredNodes, filters, onFilterChange, resetFilters, filterConfigs, } = useNodeFilters(validNodes); 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 (!map) { return; } if (!validNodes.length) { return; } if (validNodes.length === 1) { 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); } }, [filteredNodes, map]); // Generate all markers const markers = useMemo( () => filteredNodes.map((node) => { const position = convertToLatLng(node.position); return ( handleMarkerClick(node, e)} > ); }), [filteredNodes, handleMarkerClick], ); useEffect(() => { map?.on("load", () => { getMapBounds(); }); }, [map, getMapBounds]); return ( <> {waypoints.map((wp) => (
))} {markers} {selectedNode ? ( setSelectedNode(null)} > ) : null}
); }; export default MapPage;