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("_", " "); ].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} />

229
src/pages/Map.tsx

@ -1,59 +1,93 @@
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,
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 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) { 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: 12, 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,78 +103,54 @@ 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()}
className="border-[1.5px] border-slate-600 shadow-xl shadow-slate-600"
/>
</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"
// 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} attributionControl={false}
renderWorldCopies={false} renderWorldCopies={false}
maxPitch={0} maxPitch={0}
antialias={true}
style={{ style={{
filter: darkMode ? "brightness(0.8)" : "", filter: darkMode ? "brightness(0.9)" : "",
}} }}
dragRotate={false} dragRotate={false}
touchZoomRotate={false} touchZoomRotate={false}
initialViewState={{ initialViewState={{
zoom: 1.6, zoom: 1.8,
latitude: 35, latitude: 35,
longitude: 0, longitude: 0,
}} }}
@ -151,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}
@ -163,58 +181,17 @@ const MapPage = (): JSX.Element => {
</div> </div>
</Marker> </Marker>
))} ))}
{/* {rasterSources.map((source, index) => ( {markers}
<Source key={index} type="raster" {...source}> {selectedNode ? (
<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 && (
<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>
</> </>

Loading…
Cancel
Save