Map rework

This commit is contained in:
Sacha Weatherstone
2022-01-11 00:04:25 +11:00
parent ce95c070e0
commit 962a45e2fb
6 changed files with 186 additions and 214 deletions

View File

@@ -15,7 +15,7 @@
"@floating-ui/react-dom": "^0.4.3",
"@headlessui/react": "^1.4.2",
"@meshtastic/components": "^1.0.15",
"@meshtastic/meshtasticjs": "^0.6.36",
"@meshtastic/meshtasticjs": "^0.6.37",
"@reduxjs/toolkit": "^1.7.1",
"base64-js": "^1.5.1",
"boring-avatars": "^1.6.1",

8
pnpm-lock.yaml generated
View File

@@ -4,7 +4,7 @@ specifiers:
'@floating-ui/react-dom': ^0.4.3
'@headlessui/react': ^1.4.2
'@meshtastic/components': ^1.0.15
'@meshtastic/meshtasticjs': ^0.6.36
'@meshtastic/meshtasticjs': ^0.6.37
'@reduxjs/toolkit': ^1.7.1
'@types/mapbox-gl': ^2.6.0
'@types/react': ^17.0.38
@@ -62,7 +62,7 @@ dependencies:
'@floating-ui/react-dom': 0.4.3_b3482aaf5744fc7c2aeb7941b0e0a78f
'@headlessui/react': 1.4.2_react-dom@17.0.2+react@17.0.2
'@meshtastic/components': 1.0.15_@types+react@17.0.38
'@meshtastic/meshtasticjs': 0.6.36
'@meshtastic/meshtasticjs': 0.6.37
'@reduxjs/toolkit': 1.7.1_react-redux@7.2.6+react@17.0.2
base64-js: 1.5.1
boring-avatars: 1.6.1
@@ -1535,8 +1535,8 @@ packages:
- '@types/react'
dev: false
/@meshtastic/meshtasticjs/0.6.36:
resolution: {integrity: sha512-Ibd6guVm1qC1P26smIBY+95WdjYFtWIKjDSDTS2Q0xvrvWT3gtL+kuGf+E+XKHqbtxVSAyJQ6RoG1hCEYRByXw==}
/@meshtastic/meshtasticjs/0.6.37:
resolution: {integrity: sha512-HXl8/eTvZAW9b4MfNxoZa2/qrKP4Y2RlyPP8jQOO+FHy5u/BLuMwAEbA69YqsOZMF9SK5/xNwHlrBnMXwWKHDA==}
dependencies:
'@protobuf-ts/runtime': 2.1.0
sub-events: 1.8.9

View File

@@ -0,0 +1,50 @@
import React from 'react';
import ReactDOM from 'react-dom';
import mapbox from 'mapbox-gl';
import ReactDOMServer from 'react-dom/server';
import { useMapbox } from '@hooks/useMapbox';
export interface MarkerProps extends Omit<mapbox.MarkerOptions, 'element'> {
children?: React.ReactNode;
center: mapbox.LngLatLike;
popup: JSX.Element;
}
export const Marker = ({
children,
center,
popup,
...props
}: MarkerProps): JSX.Element => {
const { map } = useMapbox();
const ref = React.useRef<HTMLDivElement>(document.createElement('div'));
const addMarker = React.useCallback((): void => {
if (map) {
const marker = new mapbox.Marker(ref.current, props)
.setLngLat(center)
.setPopup(
new mapbox.Popup().setHTML(ReactDOMServer.renderToString(popup)),
);
marker.addTo(map);
}
}, [map, center, props, popup]);
React.useEffect(() => {
map?.on('load', () => {
addMarker();
});
}, [addMarker, map]);
React.useEffect(() => {
if (map?.loaded()) {
addMarker();
}
}, [addMarker, map]);
<div ref={ref}>{children}</div>;
return ReactDOM.createPortal(children, ref.current);
};

View File

@@ -1,9 +1,5 @@
import React from 'react';
import mapbox from 'mapbox-gl';
import { renderToString } from 'react-dom/server';
import { FiAirplay } from 'react-icons/fi';
import {
setBearing,
setLatLng,
@@ -14,7 +10,6 @@ import {
import { useAppDispatch } from '@hooks/useAppDispatch';
import { useAppSelector } from '@hooks/useAppSelector';
import { useCreateMapbox } from '@hooks/useCreateMapbox';
import { Card } from '@meshtastic/components';
import { MapStyles } from '../Map/styles';
import { MapboxContext } from './mapboxContext';
@@ -28,17 +23,9 @@ export const MapboxProvider = ({
}: MapboxProviderProps): JSX.Element => {
const darkMode = useAppSelector((state) => state.app.darkMode);
const mapState = useAppSelector((state) => state.map);
const nodes = useAppSelector((state) => state.meshtastic.nodes);
const dispatch = useAppDispatch();
const ref = React.useRef<HTMLDivElement>(null);
const [markers, setMarkers] = React.useState<
{ id: number; marker: mapbox.Marker }[]
>([]);
const [markerElements, setMarkerElements] = React.useState<
{ id: number; element: JSX.Element; ref: React.RefObject<HTMLDivElement> }[]
>([]);
const map = useCreateMapbox({
ref,
accessToken:
@@ -52,79 +39,7 @@ export const MapboxProvider = ({
},
});
const updateNodes = React.useCallback(() => {
nodes.map((node) => {
if (map?.loaded() && node.currentPosition) {
const existingMarker = markers.find(
(marker) => marker.id === node.number,
)?.marker;
const tmpRef = React.createRef<HTMLDivElement>();
const markerElement = markerElements.find(
(element) => element.id === node.number,
) ?? {
element: (
<div ref={tmpRef}>
Test
<FiAirplay />
</div>
),
id: node.number,
ref: tmpRef,
};
console.log(markerElement);
const marker =
existingMarker ??
new mapbox.Marker(markerElement.ref.current ?? undefined, {})
.setLngLat([0, 0])
.addTo(map);
marker
.setLngLat([
node.currentPosition.longitudeI / 1e7,
node.currentPosition.latitudeI / 1e7,
])
.setPopup(
new mapbox.Popup().setHTML(
renderToString(
<Card>
<div className="p-2">
<div className="text-xl font-medium">
{node.user?.longName}
</div>
<ul>
<li>ID: {node.number}</li>
</ul>
</div>
</Card>,
),
),
);
if (!existingMarker) {
setMarkers((markers) => [
...markers,
{
id: node.number,
marker,
},
]);
setMarkerElements((markerElements) => [
...markerElements,
markerElement,
]);
}
}
});
}, [markers, markerElements, map, nodes]);
React.useEffect(() => {
map?.on('load', () => {
updateNodes();
});
map?.on('styledata', () => {
if (!map.getSource('mapbox-dem')) {
map.addSource('mapbox-dem', {
@@ -151,7 +66,7 @@ export const MapboxProvider = ({
map?.on('pitch', (e) => {
dispatch(setPitch(e.target.getPitch()));
});
}, [dispatch, map, updateNodes, mapState.exaggeration]);
}, [dispatch, map, mapState.exaggeration]);
React.useEffect(() => {
const center = map?.getCenter();
@@ -209,13 +124,6 @@ export const MapboxProvider = ({
}
}, [map, mapState.style]);
/**
* Markers
*/
React.useEffect(() => {
updateNodes();
}, [nodes, updateNodes]);
return (
<MapboxContext.Provider value={{ map, ref }}>
{children}

View File

@@ -6,6 +6,7 @@ import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
interface MapState {
firstLoad: boolean;
latLng: mapboxgl.LngLat;
zoom: number;
bearing: number;
@@ -17,8 +18,9 @@ interface MapState {
}
const initialState: MapState = {
latLng: new mapboxgl.LngLat(-77.0305, 38.8868),
zoom: 9,
firstLoad: true,
latLng: new mapboxgl.LngLat(0, 0),
zoom: 2,
bearing: 0,
pitch: 0,
accessToken:

View File

@@ -1,8 +1,8 @@
import React from 'react';
import mapboxgl from 'mapbox-gl';
import mapbox from 'mapbox-gl';
import { FaSatellite } from 'react-icons/fa';
import { FiCode } from 'react-icons/fi';
import { FiCode, FiMapPin } from 'react-icons/fi';
import { GiLightningFrequency } from 'react-icons/gi';
import {
MdAccountCircle,
@@ -16,6 +16,7 @@ import {
} from 'react-icons/md';
import TimeAgo from 'timeago-react';
import { Marker } from '@components/Map/Marker';
import type { Node } from '@core/slices/meshtasticSlice';
import { Disclosure } from '@headlessui/react';
import { useMapbox } from '@hooks/useMapbox';
@@ -81,124 +82,135 @@ export const NodeCard = ({ node, myNodeInfo }: NodeCardProps): JSX.Element => {
// }, [node.positions]);
return (
<Disclosure
as="div"
className="m-2 rounded-md shadow-md bg-gray-50 dark:bg-gray-700"
>
<Disclosure.Button
as="div"
className="flex w-full gap-2 p-2 bg-gray-100 rounded-md shadow-md dark:bg-primaryDark"
>
{myNodeInfo ? (
<MdAccountCircle className="my-auto" />
) : (
<div
className={`my-auto w-3 h-3 rounded-full ${
age === 'young'
? 'bg-green-500'
: age === 'aging'
? 'bg-yellow-500'
: age === 'old'
? 'bg-red-500'
: 'bg-gray-500'
}`}
/>
)}
<div className="my-auto">{node.user?.longName}</div>
<div className="my-auto ml-auto text-xs font-semibold">
{!myNodeInfo && (
<span>
{node.lastHeard.getTime() ? (
<TimeAgo datetime={node.lastHeard} />
) : (
'Never'
)}
</span>
)}
</div>
<IconButton
disabled={PositionConfidence === 'none'}
onClick={(): void => {
if (PositionConfidence !== 'none' && node.currentPosition) {
map?.flyTo({
center: new mapboxgl.LngLat(
node.currentPosition.longitudeI / 1e7,
node.currentPosition.latitudeI / 1e7,
),
zoom: 16,
});
}
// if (PositionConfidence !== 'none' && node.currentPosition) {
// dispatch(
// setLatLng(
// new mapboxgl.LngLat(
// node.currentPosition.longitudeI / 1e7,
// node.currentPosition.latitudeI / 1e7,
// ),
// ),
// );
// }
}}
icon={
PositionConfidence === 'high' ? (
<MdGpsFixed />
) : PositionConfidence === 'low' ? (
<MdGpsNotFixed />
) : (
<MdGpsOff />
<>
{node.currentPosition && (
<Marker
center={
new mapbox.LngLat(
node.currentPosition.longitudeI / 1e7,
node.currentPosition.latitudeI / 1e7,
)
}
/>
</Disclosure.Button>
<Disclosure.Panel className="p-2">
{myNodeInfo && (
<div>
<div className="flex justify-between">
<span className="flex">
<MdSdStorage className="my-auto" />
Firmware Ver:
</span>
<span>{myNodeInfo.firmwareVersion}</span>
</div>
<div className="flex justify-between">
<span className="flex">
<GiLightningFrequency className="my-auto" />
Freq Bands:
</span>
<span>{myNodeInfo.numBands}</span>
popup={<div>Popup</div>}
>
<div className="z-50 bg-blue-500 border-2 border-blue-500 rounded-full bg-opacity-30">
<div className="m-4 ">
<FiMapPin className="w-5 h-5" />
</div>
</div>
)}
<div className="flex">
<div className="my-auto">
{Protobuf.HardwareModel[node.user?.hwModel ?? 0]}
</div>
<div className="ml-auto">
<IconButton icon={<FiCode className="w-5 h-5" />} />
</div>
</div>
<div className="flex">
<MdSignalCellularAlt className="my-auto" />
SNR:
{node.snr[node.snr.length - 1] < snrAverage ? (
<MdArrowDropDown className="text-red-500" />
</Marker>
)}
<Disclosure
as="div"
className="m-2 rounded-md shadow-md bg-gray-50 dark:bg-gray-700"
>
<Disclosure.Button
as="div"
className="flex w-full gap-2 p-2 bg-gray-100 rounded-md shadow-md dark:bg-primaryDark"
>
{myNodeInfo ? (
<MdAccountCircle className="my-auto" />
) : (
<MdArrowDropUp className="text-green-500" />
<div
className={`my-auto w-3 h-3 rounded-full ${
age === 'young'
? 'bg-green-500'
: age === 'aging'
? 'bg-yellow-500'
: age === 'old'
? 'bg-red-500'
: 'bg-gray-500'
}`}
/>
)}
{node.snr[node.snr.length - 1]}, Average: {snrAverage}
</div>
<div className="flex">
<FaSatellite className="my-auto" />
Sats:
{(node.currentPosition?.satsInView ?? 0) < satsAverage ? (
<MdArrowDropDown className="text-red-500" />
) : (
<MdArrowDropUp className="text-green-500" />
<div className="my-auto">{node.user?.longName}</div>
<div className="my-auto ml-auto text-xs font-semibold">
{!myNodeInfo && (
<span>
{node.lastHeard.getTime() ? (
<TimeAgo datetime={node.lastHeard} />
) : (
'Never'
)}
</span>
)}
</div>
<IconButton
disabled={PositionConfidence === 'none'}
onClick={(e): void => {
e.stopPropagation();
if (PositionConfidence !== 'none' && node.currentPosition) {
map?.flyTo({
center: new mapbox.LngLat(
node.currentPosition.longitudeI / 1e7,
node.currentPosition.latitudeI / 1e7,
),
zoom: 16,
});
}
}}
icon={
PositionConfidence === 'high' ? (
<MdGpsFixed />
) : PositionConfidence === 'low' ? (
<MdGpsNotFixed />
) : (
<MdGpsOff />
)
}
/>
</Disclosure.Button>
<Disclosure.Panel className="p-2">
{myNodeInfo && (
<div>
<div className="flex justify-between">
<span className="flex">
<MdSdStorage className="my-auto" />
Firmware Ver:
</span>
<span>{myNodeInfo.firmwareVersion}</span>
</div>
<div className="flex justify-between">
<span className="flex">
<GiLightningFrequency className="my-auto" />
Freq Bands:
</span>
<span>{myNodeInfo.numBands}</span>
</div>
</div>
)}
{node.currentPosition?.satsInView ?? 0}, Average: {satsAverage}
</div>
</Disclosure.Panel>
</Disclosure>
<div className="flex">
<div className="my-auto">
{Protobuf.HardwareModel[node.user?.hwModel ?? 0]}
</div>
<div className="ml-auto">
<IconButton icon={<FiCode className="w-5 h-5" />} />
</div>
</div>
<div className="flex">
<MdSignalCellularAlt className="my-auto" />
SNR:
{node.snr[node.snr.length - 1] < snrAverage ? (
<MdArrowDropDown className="text-red-500" />
) : (
<MdArrowDropUp className="text-green-500" />
)}
{node.snr[node.snr.length - 1]}, Average: {snrAverage}
</div>
<div className="flex">
<FaSatellite className="my-auto" />
Sats:
{(node.currentPosition?.satsInView ?? 0) < satsAverage ? (
<MdArrowDropDown className="text-red-500" />
) : (
<MdArrowDropUp className="text-green-500" />
)}
{node.currentPosition?.satsInView ?? 0}, Average: {satsAverage}
</div>
</Disclosure.Panel>
</Disclosure>
</>
);
};