Browse Source

Merge pull request #409 from danditomaso/fix/map-zooms-on-click

Fixed map zooming when clicking on map marker. Fixed popup alignment to Map Marker
pull/429/head
Dan Ditomaso 1 year ago
committed by GitHub
parent
commit
0f0751e4d2
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      src/components/PageComponents/Map/NodeDetail.tsx
  2. 229
      src/pages/Map.tsx

2
src/components/PageComponents/Map/NodeDetail.tsx

@ -31,7 +31,7 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
].replaceAll("_", " ");
return (
<div className="dark:text-black">
<div className="dark:text-black p-1">
<div className="flex gap-2">
<div className="flex flex-col items-center gap-2 min-w-6 pt-1">
<Avatar text={node.user?.shortName} />

229
src/pages/Map.tsx

@ -1,59 +1,93 @@
import { NodeDetail } from "@app/components/PageComponents/Map/NodeDetail";
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 { 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 type { Protobuf } from "@meshtastic/js";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import { bbox, lineString } from "@turf/turf";
import {
BoxSelectIcon,
MapPinIcon,
ZoomInIcon,
ZoomOutIcon,
} from "lucide-react";
import { MapPinIcon } from "lucide-react";
import { type JSX, useCallback, useEffect, useMemo, useState } from "react";
import { AttributionControl, Marker, Popup, useMap } from "react-map-gl";
import {
AttributionControl,
GeolocateControl,
Marker,
NavigationControl,
Popup,
ScaleControl,
useMap,
} from "react-map-gl";
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 { nodes, waypoints } = useDevice();
const { rasterSources, darkMode } = useAppStore();
const { darkMode } = useAppStore();
const { default: map } = useMap();
const [zoom, setZoom] = useState(0);
const [selectedNode, setSelectedNode] =
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 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],
);
const getBBox = useCallback(() => {
// Get the bounds of the map based on the nodes furtherest away from center
const getMapBounds = useCallback(() => {
if (!map) {
return;
}
const nodesWithPosition = allNodes.filter(
(node) => node.position?.latitudeI,
);
if (!nodesWithPosition.length) {
if (!validNodes.length) {
return;
}
if (nodesWithPosition.length === 1) {
if (validNodes.length === 1) {
map.easeTo({
zoom: 12,
zoom: map.getZoom(),
center: [
(nodesWithPosition[0].position?.longitudeI ?? 0) / 1e7,
(nodesWithPosition[0].position?.latitudeI ?? 0) / 1e7,
(validNodes[0].position?.longitudeI ?? 0) / 1e7,
(validNodes[0].position?.latitudeI ?? 0) / 1e7,
],
});
return;
}
const line = lineString(
nodesWithPosition.map((n) => [
validNodes.map((n) => [
(n.position?.latitudeI ?? 0) / 1e7,
(n.position?.longitudeI ?? 0) / 1e7,
]),
@ -69,78 +103,54 @@ const MapPage = (): JSX.Element => {
if (center) {
map.easeTo(center);
}
}, [allNodes, map]);
}, [validNodes, map]);
useEffect(() => {
map?.on("zoom", () => {
setZoom(map?.getZoom() ?? 0);
});
}, [map]);
// Generate all markers
const markers = useMemo(
() =>
validNodes.map((node) => {
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()}
className="border-[1.5px] border-slate-600 shadow-xl shadow-slate-600"
/>
</Marker>
);
}),
[validNodes, handleMarkerClick],
);
useEffect(() => {
map?.on("load", () => {
getBBox();
getMapBounds();
});
}, [map, getBBox]);
}, [map, getMapBounds]);
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();
},
},
]}
>
<Sidebar />
<PageLayout label="Map" noPadding={true} actions={[]}>
<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}
antialias={true}
style={{
filter: darkMode ? "brightness(0.8)" : "",
filter: darkMode ? "brightness(0.9)" : "",
}}
dragRotate={false}
touchZoomRotate={false}
initialViewState={{
zoom: 1.6,
zoom: 1.8,
latitude: 35,
longitude: 0,
}}
@ -151,6 +161,14 @@ const MapPage = (): JSX.Element => {
color: darkMode ? "black" : "",
}}
/>
<GeolocateControl
position="top-right"
positionOptions={{ enableHighAccuracy: true }}
trackUserLocation
/>
<NavigationControl position="top-right" showCompass={false} />
<ScaleControl />
{waypoints.map((wp) => (
<Marker
key={wp.id}
@ -163,58 +181,17 @@ const MapPage = (): JSX.Element => {
</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 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 && (
{markers}
{selectedNode ? (
<Popup
longitude={(selectedNode.position.longitudeI ?? 0) / 1e7}
latitude={(selectedNode.position.latitudeI ?? 0) / 1e7}
anchor="left"
closeOnClick={false}
anchor="top"
longitude={convertToLatLng(selectedNode.position).longitude}
latitude={convertToLatLng(selectedNode.position).latitude}
onClose={() => setSelectedNode(null)}
>
<NodeDetail node={selectedNode} />
</Popup>
)}
) : null}
</MapGl>
</PageLayout>
</>

Loading…
Cancel
Save