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;