@ -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 ) / 1 e7 ,
longitude : ( position . longitudeI ? ? 0 ) / 1 e7 ,
} ) ;
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 ) / 1 e7 ,
( nodesWithPosition [ 0 ] . position ? . latitudeI ? ? 0 ) / 1 e7 ,
( validNodes [ 0 ] . position ? . longitudeI ? ? 0 ) / 1 e7 ,
( validNodes [ 0 ] . position ? . latitudeI ? ? 0 ) / 1 e7 ,
] ,
} ) ;
return ;
}
const line = lineString (
nodesWithPosition . map ( ( n ) = > [
validNodes . map ( ( n ) = > [
( n . position ? . latitudeI ? ? 0 ) / 1 e7 ,
( n . position ? . longitudeI ? ? 0 ) / 1 e7 ,
] ) ,
@ -69,78 +103,54 @@ const MapPage = (): JSX.Element => {
if ( center ) {
map . easeTo ( center ) ;
}
} , [ all Nodes, map ] ) ;
} , [ valid Nodes, 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 >
) ) }
{ / * { r a s t e r S o u r c e s . m a p ( ( s o u r c e , i n d e x ) = > (
< 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 ) / 1 e7 }
latitude = { ( node . position . latitudeI ? ? 0 ) / 1 e7 }
// style={{ filter: darkMode ? "invert(1)" : "" }}
anchor = "bottom"
onClick = { ( ) = > {
setSelectedNode ( node ) ;
map ? . easeTo ( {
zoom : 12 ,
center : [
( node . position ? . longitudeI ? ? 0 ) / 1 e7 ,
( node . position ? . latitudeI ? ? 0 ) / 1 e7 ,
] ,
} ) ;
} }
>
< 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 ) / 1 e7 }
latitude = { ( selectedNode . position . latitudeI ? ? 0 ) / 1 e7 }
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 >
< / >