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.
216 lines
6.6 KiB
216 lines
6.6 KiB
import { Subtle } from "@app/components/UI/Typography/Subtle.tsx";
|
|
import { NodeDetail } from "@app/components/PageComponents/Map/NodeDetail";
|
|
import { cn } from "@app/core/utils/cn.ts";
|
|
import { PageLayout } from "@components/PageLayout.tsx";
|
|
import { Sidebar } from "@components/Sidebar.tsx";
|
|
import { SidebarSection } from "@components/UI/Sidebar/SidebarSection.tsx";
|
|
import { SidebarButton } from "@components/UI/Sidebar/sidebarButton.tsx";
|
|
import { useAppStore } from "@core/stores/appStore.ts";
|
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
|
import { Hashicon } from "@emeraldpay/hashicon-react";
|
|
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
|
import { bbox, lineString } from "@turf/turf";
|
|
import {
|
|
BoxSelectIcon,
|
|
MapPinIcon,
|
|
ZoomInIcon,
|
|
ZoomOutIcon,
|
|
} from "lucide-react";
|
|
import { useCallback, useEffect, useState } from "react";
|
|
import { AttributionControl, Marker, Popup, useMap } from "react-map-gl";
|
|
import MapGl from "react-map-gl/maplibre";
|
|
import { Protobuf } from "@meshtastic/js";
|
|
|
|
export const MapPage = (): JSX.Element => {
|
|
const { nodes, waypoints } = useDevice();
|
|
const { rasterSources, darkMode } = useAppStore();
|
|
const { default: map } = useMap();
|
|
|
|
const [zoom, setZoom] = useState(0);
|
|
const [selectedNode, setSelectedNode] =
|
|
useState<Protobuf.Mesh.NodeInfo | null>(null);
|
|
|
|
const allNodes = Array.from(nodes.values());
|
|
|
|
const getBBox = useCallback(() => {
|
|
if (!map) {
|
|
return;
|
|
}
|
|
const nodesWithPosition = allNodes.filter(
|
|
(node) => node.position?.latitudeI,
|
|
);
|
|
if (!nodesWithPosition.length) {
|
|
return;
|
|
}
|
|
if (nodesWithPosition.length === 1) {
|
|
map.easeTo({
|
|
zoom: 12,
|
|
center: [
|
|
(nodesWithPosition[0].position?.longitudeI ?? 0) / 1e7,
|
|
(nodesWithPosition[0].position?.latitudeI ?? 0) / 1e7,
|
|
],
|
|
});
|
|
return;
|
|
}
|
|
const line = lineString(
|
|
nodesWithPosition.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);
|
|
}
|
|
}, [allNodes, map]);
|
|
|
|
useEffect(() => {
|
|
map?.on("zoom", () => {
|
|
setZoom(map?.getZoom() ?? 0);
|
|
});
|
|
}, [map]);
|
|
|
|
useEffect(() => {
|
|
map?.on("load", () => {
|
|
getBBox();
|
|
});
|
|
}, [map, getBBox]);
|
|
|
|
return (
|
|
<>
|
|
<Sidebar>
|
|
<SidebarSection label="Sources">
|
|
{rasterSources.map((source) => (
|
|
<SidebarButton key={source.title} label={source.title} />
|
|
))}
|
|
</SidebarSection>
|
|
</Sidebar>
|
|
<PageLayout
|
|
label="Map"
|
|
noPadding={true}
|
|
actions={[
|
|
{
|
|
icon: ZoomInIcon,
|
|
onClick() {
|
|
map?.zoomIn();
|
|
},
|
|
},
|
|
{
|
|
icon: ZoomOutIcon,
|
|
onClick() {
|
|
map?.zoomOut();
|
|
},
|
|
},
|
|
{
|
|
icon: BoxSelectIcon,
|
|
onClick() {
|
|
getBBox();
|
|
},
|
|
},
|
|
]}
|
|
>
|
|
<MapGl
|
|
mapStyle="https://raw.githubusercontent.com/hc-oss/maplibre-gl-styles/master/styles/osm-mapnik/v8/default.json"
|
|
// onClick={(e) => {
|
|
// const waypoint = new Protobuf.Waypoint({
|
|
// name: "test",
|
|
// description: "test description",
|
|
// latitudeI: Math.trunc(e.lngLat.lat * 1e7),
|
|
// longitudeI: Math.trunc(e.lngLat.lng * 1e7)
|
|
// });
|
|
// addWaypoint(waypoint);
|
|
// connection?.sendWaypoint(waypoint, "broadcast");
|
|
// }}
|
|
|
|
// @ts-ignore
|
|
|
|
attributionControl={false}
|
|
renderWorldCopies={false}
|
|
maxPitch={0}
|
|
style={{
|
|
filter: darkMode
|
|
? "brightness(0.6) invert(1) contrast(3) hue-rotate(200deg) saturate(0.3) brightness(0.7)"
|
|
: "",
|
|
}}
|
|
dragRotate={false}
|
|
touchZoomRotate={false}
|
|
initialViewState={{
|
|
zoom: 1.6,
|
|
latitude: 35,
|
|
longitude: 0,
|
|
}}
|
|
>
|
|
<AttributionControl
|
|
style={{ background: darkMode ? "#ffffff" : "", color: darkMode ? "black" : "" }}
|
|
/>
|
|
{waypoints.map((wp) => (
|
|
<Marker
|
|
key={wp.id}
|
|
longitude={(wp.longitudeI ?? 0) / 1e7}
|
|
latitude={(wp.latitudeI ?? 0) / 1e7}
|
|
anchor="bottom"
|
|
>
|
|
<div>
|
|
<MapPinIcon size={16} />
|
|
</div>
|
|
</Marker>
|
|
))}
|
|
{/* {rasterSources.map((source, index) => (
|
|
<Source key={index} type="raster" {...source}>
|
|
<Layer type="raster" />
|
|
</Source>
|
|
))} */}
|
|
{allNodes.map((node) => {
|
|
if (node.position?.latitudeI && node.num !== selectedNode?.num) {
|
|
return (
|
|
<Marker
|
|
key={node.num}
|
|
longitude={(node.position.longitudeI ?? 0) / 1e7}
|
|
latitude={(node.position.latitudeI ?? 0) / 1e7}
|
|
style={{ filter: darkMode ? "invert(1)" : "" }}
|
|
anchor="bottom"
|
|
onClick={() => {
|
|
setSelectedNode(node);
|
|
map?.easeTo({
|
|
zoom: 12,
|
|
center: [
|
|
(node.position?.longitudeI ?? 0) / 1e7,
|
|
(node.position?.latitudeI ?? 0) / 1e7,
|
|
],
|
|
});
|
|
}}
|
|
>
|
|
<div className="flex cursor-pointer gap-2 rounded-md border bg-backgroundPrimary p-1.5">
|
|
<Hashicon value={node.num.toString()} size={22} />
|
|
<Subtle className={cn(zoom < 12 && "hidden")}>
|
|
{node.user?.longName ||
|
|
`!${numberToHexUnpadded(node.num)}`}
|
|
</Subtle>
|
|
</div>
|
|
</Marker>
|
|
);
|
|
}
|
|
})}
|
|
{selectedNode?.position && (
|
|
<Popup
|
|
longitude={(selectedNode.position.longitudeI ?? 0) / 1e7}
|
|
latitude={(selectedNode.position.latitudeI ?? 0) / 1e7}
|
|
anchor="left"
|
|
closeOnClick={false}
|
|
onClose={() => setSelectedNode(null)}
|
|
>
|
|
<NodeDetail node={selectedNode} />
|
|
</Popup>
|
|
)}
|
|
</MapGl>
|
|
</PageLayout>
|
|
</>
|
|
);
|
|
};
|
|
|