You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

174 lines
5.3 KiB

import {
fanOutOffsetsPx,
groupNodesByIdenticalCoords,
type PxOffset,
} from "@components/PageComponents/Map/cluster.ts";
import {
generatePrecisionCircles,
SourcePrecisionCircles,
} from "@components/PageComponents/Map/Layers/PrecisionLayer.tsx";
import { NodeMarker } from "@components/PageComponents/Map/Markers/NodeMarker.tsx";
import { StackBadge } from "@components/PageComponents/Map/Markers/StackBadge.tsx";
import { NodeDetail } from "@components/PageComponents/Map/Popups/NodeDetail.tsx";
import type { PopupState } from "@components/PageComponents/Map/Popups/PopupWrapper.tsx";
import { PopupWrapper } from "@components/PageComponents/Map/Popups/PopupWrapper.tsx";
import { useMapFitting } from "@core/hooks/useMapFitting";
import { useNodeDB } from "@core/stores";
import { hasPos, toLngLat } from "@core/utils/geo.ts";
import type { Protobuf } from "@meshtastic/core";
import { useCallback, useMemo } from "react";
import { useTranslation } from "react-i18next";
import type { MapRef } from "react-map-gl/maplibre";
export interface NodeMarkerProps {
mapRef: MapRef | undefined;
filteredNodes: Protobuf.Mesh.NodeInfo[];
myNode: Protobuf.Mesh.NodeInfo | undefined;
expandedCluster: string | undefined;
setExpandedCluster: (key: string | undefined) => void;
popupState: PopupState | undefined;
setPopupState: (state: PopupState | undefined) => void;
isVisible: boolean;
}
export const NodesLayer = ({
mapRef,
filteredNodes,
myNode,
expandedCluster,
setExpandedCluster,
popupState,
setPopupState,
isVisible,
}: NodeMarkerProps): React.ReactNode[] => {
const { t } = useTranslation("map");
const { hasNodeError } = useNodeDB();
const { focusLngLat } = useMapFitting(mapRef);
const selectedNode = useMemo(
() =>
popupState?.type !== "node"
? undefined
: (filteredNodes.find((node) => node.num === popupState.num) ??
undefined),
[popupState, filteredNodes],
);
const onMarkerClick = useCallback(
(num: number, offset: PxOffset, e: { originalEvent: MouseEvent }) => {
e.originalEvent?.stopPropagation();
setPopupState({ type: "node", num, offset });
const node = filteredNodes.find((node) => node.num === num) ?? undefined;
if (node) {
focusLngLat(toLngLat(node.position));
}
},
[filteredNodes, focusLngLat, setPopupState],
);
const clusters = groupNodesByIdenticalCoords(filteredNodes);
const rendered: React.ReactNode[] = [];
for (const [key, nodes] of clusters) {
if (!nodes.length || !nodes[0]?.position) {
continue;
}
const [lng, lat] = toLngLat(nodes[0].position);
const isExpanded = expandedCluster === key;
// Precompute pixel offsets for expanded state
const expandedOffsets = isExpanded
? fanOutOffsetsPx(nodes.length, key)
: undefined;
// Always render all node markers in the cluster
for (const [i, node] of nodes.entries()) {
const isHead = i === 0;
rendered.push(
<NodeMarker
key={`node-${key}-${node.num}`}
id={node.num}
lng={lng}
lat={lat}
offset={expandedOffsets?.[i]}
label={node.user?.shortName ?? t("unknown.shortName")}
tooltipLabel={node.user?.longName ?? t("unknown.longName")}
hasError={hasNodeError(node.num)}
isFavorite={node.isFavorite ?? false}
isVisible={isVisible}
onClick={(num, e) => {
e.originalEvent?.stopPropagation();
if (!isExpanded && !isHead) {
// collapsed: tapping a buried marker expands the stack first
setExpandedCluster(key);
return;
}
onMarkerClick(num, expandedOffsets?.[i] ?? [0, 0], e);
}}
/>,
);
}
if (nodes.length > 1) {
rendered.push(
<StackBadge
key={`stack-badge-${key}`}
lng={lng}
lat={lat}
count={nodes.length - 1}
isVisible={isVisible && !isExpanded}
onClick={(e) => {
e.originalEvent?.stopPropagation();
setExpandedCluster(key);
}}
/>,
);
}
}
if (selectedNode) {
rendered.push(
<SourcePrecisionCircles
key={`precision-circles-selected-${selectedNode.num}`}
data={generatePrecisionCircles([selectedNode])}
id={`precisionCircles-selected-${selectedNode.num}`}
isVisible={true}
/>,
);
const [lng, lat] = toLngLat(selectedNode.position);
rendered.push(
<PopupWrapper
key={`popup-nodeinfo-${selectedNode.num}`}
lng={lng}
lat={lat}
offset={popupState?.type === "node" ? popupState.offset : [0, 0]}
onClose={() => setPopupState(undefined)}
>
<NodeDetail node={selectedNode} />
</PopupWrapper>,
);
}
if (myNode && hasPos(myNode.position)) {
const [lng, lat] = toLngLat(myNode.position);
rendered.push(
<NodeMarker
key={`node-${myNode.num}`}
id={myNode.num}
lng={lng}
lat={lat}
label={myNode.user?.shortName?.toString() ?? String(myNode.num)}
tooltipLabel={t("myNode.tooltip")}
hasError={false}
isFavorite={true}
onClick={(_, e) => onMarkerClick(myNode.num, [0, 0], e)}
/>,
);
}
return rendered;
};