fix: removed map zoom on marker click. feat: lined up popup with map marker
This commit is contained in:
@@ -30,7 +30,7 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
|
|||||||
].replaceAll("_", " ");
|
].replaceAll("_", " ");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dark:text-black">
|
<div className="dark:text-black p-1">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<div className="flex flex-col items-center gap-2 min-w-6 pt-1">
|
<div className="flex flex-col items-center gap-2 min-w-6 pt-1">
|
||||||
<Avatar text={node.user?.shortName} />
|
<Avatar text={node.user?.shortName} />
|
||||||
|
|||||||
+101
-105
@@ -1,59 +1,94 @@
|
|||||||
import { NodeDetail } from "@app/components/PageComponents/Map/NodeDetail";
|
import { NodeDetail } from "@app/components/PageComponents/Map/NodeDetail";
|
||||||
import { Avatar } from "@app/components/UI/Avatar";
|
import { Avatar } from "@app/components/UI/Avatar";
|
||||||
import { Subtle } from "@app/components/UI/Typography/Subtle.tsx";
|
|
||||||
import { cn } from "@app/core/utils/cn.ts";
|
|
||||||
import { PageLayout } from "@components/PageLayout.tsx";
|
import { PageLayout } from "@components/PageLayout.tsx";
|
||||||
import { Sidebar } from "@components/Sidebar.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 { useAppStore } from "@core/stores/appStore.ts";
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
import type { Protobuf } from "@meshtastic/js";
|
import type { Protobuf } from "@meshtastic/js";
|
||||||
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
|
||||||
import { bbox, lineString } from "@turf/turf";
|
import { bbox, lineString } from "@turf/turf";
|
||||||
import {
|
import { MapPinIcon } from "lucide-react";
|
||||||
BoxSelectIcon,
|
|
||||||
MapPinIcon,
|
|
||||||
ZoomInIcon,
|
|
||||||
ZoomOutIcon,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { type JSX, useCallback, useEffect, useMemo, useState } from "react";
|
import { type JSX, useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { AttributionControl, Marker, Popup, useMap } from "react-map-gl";
|
import {
|
||||||
|
AttributionControl,
|
||||||
|
GeolocateControl,
|
||||||
|
Marker,
|
||||||
|
MarkerEvent,
|
||||||
|
NavigationControl,
|
||||||
|
Popup,
|
||||||
|
ScaleControl,
|
||||||
|
useMap,
|
||||||
|
} from "react-map-gl";
|
||||||
import MapGl from "react-map-gl/maplibre";
|
import MapGl from "react-map-gl/maplibre";
|
||||||
|
|
||||||
|
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 = (): JSX.Element => {
|
const MapPage = (): JSX.Element => {
|
||||||
const { nodes, waypoints } = useDevice();
|
const { nodes, waypoints } = useDevice();
|
||||||
const { rasterSources, darkMode } = useAppStore();
|
const { darkMode } = useAppStore();
|
||||||
const { default: map } = useMap();
|
const { default: map } = useMap();
|
||||||
|
|
||||||
const [zoom, setZoom] = useState(0);
|
|
||||||
const [selectedNode, setSelectedNode] =
|
const [selectedNode, setSelectedNode] =
|
||||||
useState<Protobuf.Mesh.NodeInfo | null>(null);
|
useState<Protobuf.Mesh.NodeInfo | null>(null);
|
||||||
|
|
||||||
const allNodes = useMemo(() => Array.from(nodes.values()), [nodes]);
|
// 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 getBBox = useCallback(() => {
|
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) {
|
if (!map) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const nodesWithPosition = allNodes.filter(
|
|
||||||
(node) => node.position?.latitudeI,
|
if (!validNodes.length) {
|
||||||
);
|
|
||||||
if (!nodesWithPosition.length) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (nodesWithPosition.length === 1) {
|
if (validNodes.length === 1) {
|
||||||
map.easeTo({
|
map.easeTo({
|
||||||
zoom: map.getZoom(),
|
zoom: map.getZoom(),
|
||||||
center: [
|
center: [
|
||||||
(nodesWithPosition[0].position?.longitudeI ?? 0) / 1e7,
|
(validNodes[0].position?.longitudeI ?? 0) / 1e7,
|
||||||
(nodesWithPosition[0].position?.latitudeI ?? 0) / 1e7,
|
(validNodes[0].position?.latitudeI ?? 0) / 1e7,
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const line = lineString(
|
const line = lineString(
|
||||||
nodesWithPosition.map((n) => [
|
validNodes.map((n) => [
|
||||||
(n.position?.latitudeI ?? 0) / 1e7,
|
(n.position?.latitudeI ?? 0) / 1e7,
|
||||||
(n.position?.longitudeI ?? 0) / 1e7,
|
(n.position?.longitudeI ?? 0) / 1e7,
|
||||||
]),
|
]),
|
||||||
@@ -69,65 +104,53 @@ const MapPage = (): JSX.Element => {
|
|||||||
if (center) {
|
if (center) {
|
||||||
map.easeTo(center);
|
map.easeTo(center);
|
||||||
}
|
}
|
||||||
}, [allNodes, map]);
|
}, [validNodes, map]);
|
||||||
|
|
||||||
useEffect(() => {
|
// Generate all markers
|
||||||
map?.on("zoom", () => {
|
const markers = useMemo(
|
||||||
setZoom(map?.getZoom() ?? 0);
|
() =>
|
||||||
});
|
validNodes.map((node) => {
|
||||||
}, [map]);
|
const position = convertToLatLng(node.position);
|
||||||
|
return (
|
||||||
|
<Marker
|
||||||
|
key={`marker-${node.num}`}
|
||||||
|
longitude={position.longitude}
|
||||||
|
latitude={position.latitude}
|
||||||
|
anchor="bottom"
|
||||||
|
onClick={(e) => handleMarkerClick(node, e)}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
text={node.user?.shortName?.toString() ?? node.num.toString()}
|
||||||
|
/>
|
||||||
|
</Marker>
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
[validNodes, handleMarkerClick],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
map?.on("load", () => {
|
map?.on("load", () => {
|
||||||
getBBox();
|
getMapBounds();
|
||||||
});
|
});
|
||||||
}, [map, getBBox]);
|
}, [map, getMapBounds]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Sidebar>
|
<Sidebar />
|
||||||
<SidebarSection label="Sources">
|
<PageLayout label="Map" noPadding={true} actions={[]}>
|
||||||
{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
|
<MapGl
|
||||||
mapStyle="https://raw.githubusercontent.com/hc-oss/maplibre-gl-styles/master/styles/osm-mapnik/v8/default.json"
|
mapStyle="https://raw.githubusercontent.com/hc-oss/maplibre-gl-styles/master/styles/osm-mapnik/v8/default.json"
|
||||||
attributionControl={false}
|
attributionControl={false}
|
||||||
renderWorldCopies={false}
|
renderWorldCopies={false}
|
||||||
maxPitch={0}
|
maxPitch={0}
|
||||||
|
antialias={true}
|
||||||
style={{
|
style={{
|
||||||
filter: darkMode ? "brightness(0.8)" : "",
|
filter: darkMode ? "brightness(0.8)" : "",
|
||||||
}}
|
}}
|
||||||
dragRotate={false}
|
dragRotate={false}
|
||||||
touchZoomRotate={false}
|
touchZoomRotate={false}
|
||||||
initialViewState={{
|
initialViewState={{
|
||||||
zoom: 1.6,
|
zoom: 1.8,
|
||||||
latitude: 35,
|
latitude: 35,
|
||||||
longitude: 0,
|
longitude: 0,
|
||||||
}}
|
}}
|
||||||
@@ -138,6 +161,14 @@ const MapPage = (): JSX.Element => {
|
|||||||
color: darkMode ? "black" : "",
|
color: darkMode ? "black" : "",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<GeolocateControl
|
||||||
|
position="top-right"
|
||||||
|
positionOptions={{ enableHighAccuracy: true }}
|
||||||
|
trackUserLocation
|
||||||
|
/>
|
||||||
|
<NavigationControl position="top-right" showCompass={false} />
|
||||||
|
|
||||||
|
<ScaleControl />
|
||||||
{waypoints.map((wp) => (
|
{waypoints.map((wp) => (
|
||||||
<Marker
|
<Marker
|
||||||
key={wp.id}
|
key={wp.id}
|
||||||
@@ -150,52 +181,17 @@ const MapPage = (): JSX.Element => {
|
|||||||
</div>
|
</div>
|
||||||
</Marker>
|
</Marker>
|
||||||
))}
|
))}
|
||||||
{allNodes.map((node) => {
|
{markers}
|
||||||
if (node.position?.latitudeI && node.num !== selectedNode?.num) {
|
{selectedNode ? (
|
||||||
return (
|
|
||||||
<Marker
|
|
||||||
key={node.num}
|
|
||||||
longitude={(node.position.longitudeI ?? 0) / 1e7}
|
|
||||||
latitude={(node.position.latitudeI ?? 0) / 1e7}
|
|
||||||
anchor="bottom"
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedNode(node);
|
|
||||||
map?.easeTo({
|
|
||||||
zoom: map.getZoom(),
|
|
||||||
center: [
|
|
||||||
(node.position?.longitudeI ?? 0) / 1e7,
|
|
||||||
(node.position?.latitudeI ?? 0) / 1e7,
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex cursor-pointer gap-2 rounded-md bg-transparent p-1.5">
|
|
||||||
<Avatar
|
|
||||||
text={
|
|
||||||
node.user?.shortName.toString() ?? node.num.toString()
|
|
||||||
}
|
|
||||||
size="sm"
|
|
||||||
/>
|
|
||||||
<Subtle className={cn(zoom < 12 && "hidden")}>
|
|
||||||
{node.user?.longName ||
|
|
||||||
`!${numberToHexUnpadded(node.num)}`}
|
|
||||||
</Subtle>
|
|
||||||
</div>
|
|
||||||
</Marker>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
{selectedNode?.position && (
|
|
||||||
<Popup
|
<Popup
|
||||||
longitude={(selectedNode.position.longitudeI ?? 0) / 1e7}
|
anchor="top"
|
||||||
latitude={(selectedNode.position.latitudeI ?? 0) / 1e7}
|
longitude={convertToLatLng(selectedNode.position).longitude}
|
||||||
anchor="left"
|
latitude={convertToLatLng(selectedNode.position).latitude}
|
||||||
closeOnClick={false}
|
|
||||||
onClose={() => setSelectedNode(null)}
|
onClose={() => setSelectedNode(null)}
|
||||||
>
|
>
|
||||||
<NodeDetail node={selectedNode} />
|
<NodeDetail node={selectedNode} />
|
||||||
</Popup>
|
</Popup>
|
||||||
)}
|
) : null}
|
||||||
</MapGl>
|
</MapGl>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
</>
|
</>
|
||||||
|
|||||||
Reference in New Issue
Block a user