New map logic
This commit is contained in:
+142
-124
@@ -1,5 +1,3 @@
|
|||||||
import 'mapbox-gl/dist/mapbox-gl.css';
|
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import mapboxgl from 'mapbox-gl';
|
import mapboxgl from 'mapbox-gl';
|
||||||
@@ -7,185 +5,205 @@ import { renderToString } from 'react-dom/server';
|
|||||||
import { FaDirections, FaGlobeAfrica, FaMountain } from 'react-icons/fa';
|
import { FaDirections, FaGlobeAfrica, FaMountain } from 'react-icons/fa';
|
||||||
import { MdFullscreen, MdRadar, MdWbShade } from 'react-icons/md';
|
import { MdFullscreen, MdRadar, MdWbShade } from 'react-icons/md';
|
||||||
|
|
||||||
import { useAppSelector } from '@app/hooks/redux';
|
import { useMapbox } from '@app/hooks/mapbox';
|
||||||
import { IconButton } from '@meshtastic/components';
|
import { useAppDispatch, useAppSelector } from '@app/hooks/redux';
|
||||||
|
import {
|
||||||
|
setExaggeration,
|
||||||
|
setHillShade,
|
||||||
|
setLatLng,
|
||||||
|
setMapStyle,
|
||||||
|
setZoom,
|
||||||
|
} from '@core/slices/mapSlice';
|
||||||
|
import { Card, IconButton } from '@meshtastic/components';
|
||||||
|
|
||||||
|
import type { MapStyle } from './styles';
|
||||||
import { MapStyles } from './styles';
|
import { MapStyles } from './styles';
|
||||||
|
|
||||||
export const Map = (): JSX.Element => {
|
export const Map = (): JSX.Element => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
const darkMode = useAppSelector((state) => state.app.darkMode);
|
const darkMode = useAppSelector((state) => state.app.darkMode);
|
||||||
const nodes = useAppSelector((state) => state.meshtastic.nodes);
|
const nodes = useAppSelector((state) => state.meshtastic.nodes);
|
||||||
|
const mapState = useAppSelector((state) => state.map);
|
||||||
|
const [markers, setMarkers] = React.useState<
|
||||||
|
{ id: number; marker: mapboxgl.Marker }[]
|
||||||
|
>([]);
|
||||||
|
const mapRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
mapboxgl.accessToken =
|
const map = useMapbox(mapRef, mapState.accessToken, {
|
||||||
'pk.eyJ1Ijoic2FjaGF3IiwiYSI6ImNrNW9meXozZjBsdW0zbHBjM2FnNnV6cmsifQ.3E4n8eFGD9ZOFo-XDVeZnQ';
|
center: mapState.latLng,
|
||||||
const mapDiv = React.useRef<HTMLDivElement>(null);
|
zoom: mapState.zoom,
|
||||||
const [map, setMap] = React.useState(null as mapboxgl.Map | null);
|
style: mapState.style.url,
|
||||||
const [exaggeration, setExaggeration] = React.useState(false);
|
});
|
||||||
const [shading, setShading] = React.useState(false);
|
|
||||||
const [maptype, setMaptype] = React.useState<
|
|
||||||
'Streets' | 'Outdoors' | 'Satellite' | 'Default'
|
|
||||||
>('Default');
|
|
||||||
|
|
||||||
const PlaceNodes = (): void => {
|
const updateNodes = React.useCallback(() => {
|
||||||
nodes.map((node) => {
|
nodes.map((node) => {
|
||||||
if (node.currentPosition && map) {
|
if (map?.loaded() && node.currentPosition) {
|
||||||
new mapboxgl.Marker({})
|
const existingMarker = markers.find(
|
||||||
.setLngLat({
|
(marker) => marker.id === node.number,
|
||||||
lat: node.currentPosition?.latitudeI / 1e7,
|
)?.marker;
|
||||||
lng: node.currentPosition?.longitudeI / 1e7,
|
const marker =
|
||||||
})
|
existingMarker ??
|
||||||
|
new mapboxgl.Marker({}).setLngLat([0, 0]).addTo(map);
|
||||||
|
|
||||||
|
marker
|
||||||
|
.setLngLat([
|
||||||
|
node.currentPosition.longitudeI / 1e7,
|
||||||
|
node.currentPosition.latitudeI / 1e7,
|
||||||
|
])
|
||||||
.setPopup(
|
.setPopup(
|
||||||
new mapboxgl.Popup().setHTML(
|
new mapboxgl.Popup().setHTML(
|
||||||
renderToString(
|
renderToString(
|
||||||
<div>
|
<Card>
|
||||||
<div className="text-xl font-medium">
|
<div className="text-xl font-medium">
|
||||||
{node.user?.longName}
|
{node.user?.longName}
|
||||||
</div>
|
</div>
|
||||||
<ul>
|
<ul>
|
||||||
<li>ID: {node.number}</li>
|
<li>ID: {node.number}</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>,
|
</Card>,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
);
|
||||||
.addTo(map);
|
|
||||||
|
if (!existingMarker) {
|
||||||
|
setMarkers((markers) => [
|
||||||
|
...markers,
|
||||||
|
{
|
||||||
|
id: node.number,
|
||||||
|
marker,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
}, [markers, map, nodes]);
|
||||||
|
|
||||||
|
const ChangeMapStyle = React.useCallback(
|
||||||
|
(styleName: string, style: MapStyle) => {
|
||||||
|
dispatch(
|
||||||
|
setMapStyle(
|
||||||
|
mapState.style.title === styleName
|
||||||
|
? darkMode
|
||||||
|
? MapStyles.Dark
|
||||||
|
: MapStyles.Light
|
||||||
|
: style,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[dispatch, darkMode, mapState.style.title],
|
||||||
|
);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
PlaceNodes();
|
map?.on('load', () => {
|
||||||
}, [nodes]);
|
updateNodes();
|
||||||
|
});
|
||||||
React.useEffect(() => {
|
map?.on('styledata', () => {
|
||||||
if (map?.loaded()) {
|
if (!map.getSource('mapbox-dem')) {
|
||||||
switch (maptype) {
|
map.addSource('mapbox-dem', {
|
||||||
case 'Outdoors':
|
type: 'raster-dem',
|
||||||
map.setStyle(MapStyles.Outdoors.url);
|
url: 'mapbox://mapbox.mapbox-terrain-dem-v1',
|
||||||
break;
|
tileSize: 512,
|
||||||
|
maxzoom: 14,
|
||||||
case 'Satellite':
|
});
|
||||||
map.setStyle(MapStyles.Satellite.url);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'Streets':
|
|
||||||
map.setStyle(MapStyles.Streets.url);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
map.setStyle(darkMode ? MapStyles.Dark.url : MapStyles.Light.url);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
map.setTerrain({
|
||||||
}, [maptype, darkMode, map]);
|
source: 'mapbox-dem',
|
||||||
|
exaggeration: mapState.exaggeration ? 1.5 : 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
map?.on('dragend', (e) => {
|
||||||
|
dispatch(setLatLng(e.target.getCenter()));
|
||||||
|
});
|
||||||
|
map?.on('zoomend', (e) => {
|
||||||
|
dispatch(setZoom(e.target.getZoom()));
|
||||||
|
});
|
||||||
|
}, [dispatch, map, updateNodes, mapState.exaggeration]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const center = map?.getCenter();
|
||||||
|
if (center !== mapState.latLng) {
|
||||||
|
map?.setCenter(mapState.latLng);
|
||||||
|
}
|
||||||
|
}, [map, mapState.latLng]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hill Shading
|
||||||
|
*/
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (map?.loaded()) {
|
if (map?.loaded()) {
|
||||||
if (shading) {
|
if (mapState.hillShade) {
|
||||||
map
|
map.addLayer(
|
||||||
.addSource('mapbox-dem', {
|
{
|
||||||
type: 'raster-dem',
|
id: 'hillshading',
|
||||||
url: 'mapbox://mapbox.mapbox-terrain-dem-v1',
|
source: 'mapbox-dem',
|
||||||
tileSize: 512,
|
type: 'hillshade',
|
||||||
maxzoom: 14,
|
// insert below waterway-river-canal-shadow;
|
||||||
})
|
// where hillshading sits in the Mapbox Outdoors style
|
||||||
.addLayer(
|
},
|
||||||
{
|
'waterway-river-canal-shadow',
|
||||||
id: 'hillshading',
|
);
|
||||||
source: 'mapbox-dem',
|
|
||||||
type: 'hillshade',
|
|
||||||
// insert below waterway-river-canal-shadow;
|
|
||||||
// where hillshading sits in the Mapbox Outdoors style
|
|
||||||
},
|
|
||||||
'waterway-river-canal-shadow',
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
map.removeLayer('hillshading');
|
map.removeLayer('hillshading');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [shading, map]);
|
}, [map, mapState.hillShade]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exaggeration
|
||||||
|
*/
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (map?.loaded()) {
|
if (map?.loaded()) {
|
||||||
if (exaggeration) {
|
map.setTerrain({
|
||||||
map
|
source: 'mapbox-dem',
|
||||||
.addSource('mapbox-dem', {
|
exaggeration: mapState.exaggeration ? 1.5 : 0,
|
||||||
type: 'raster-dem',
|
});
|
||||||
url: 'mapbox://mapbox.mapbox-terrain-dem-v1',
|
|
||||||
tileSize: 512,
|
|
||||||
maxzoom: 14,
|
|
||||||
})
|
|
||||||
.setTerrain({
|
|
||||||
source: 'mapbox-dem',
|
|
||||||
exaggeration: 1.5,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
map.setTerrain();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [exaggeration, map]);
|
}, [map, mapState.exaggeration]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map Style
|
||||||
|
*/
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!map && mapDiv.current) {
|
if (map?.loaded()) {
|
||||||
const map = new mapboxgl.Map({
|
map.setStyle(mapState.style.url);
|
||||||
container: mapDiv.current,
|
|
||||||
style: darkMode ? MapStyles.Dark.url : MapStyles.Light.url,
|
|
||||||
// center: [lng, lat],
|
|
||||||
// zoom: zoom,
|
|
||||||
});
|
|
||||||
setMap(map);
|
|
||||||
|
|
||||||
map.on('load', () => {
|
|
||||||
map.addLayer({
|
|
||||||
id: 'sky',
|
|
||||||
type: 'sky',
|
|
||||||
paint: {
|
|
||||||
'sky-type': 'atmosphere',
|
|
||||||
'sky-atmosphere-sun': [0.0, 0.0],
|
|
||||||
'sky-atmosphere-sun-intensity': 15,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
PlaceNodes();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, [map, darkMode]);
|
}, [map, mapState.style]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Markers
|
||||||
|
*/
|
||||||
|
React.useEffect(() => {
|
||||||
|
updateNodes();
|
||||||
|
}, [nodes, updateNodes]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex w-full h-full">
|
<div className="relative flex w-full h-full">
|
||||||
<div className="fixed right-0 z-20 p-2 m-4 space-y-2 bg-white rounded-md shadow-md md:mx-10 dark:bg-primaryDark">
|
<div className="fixed right-0 z-20 p-2 m-4 space-y-2 bg-white rounded-md shadow-md md:mx-10 dark:bg-primaryDark">
|
||||||
<IconButton
|
<IconButton
|
||||||
active={maptype === 'Satellite'}
|
active={mapState.style.title === 'Satellite'}
|
||||||
onClick={(): void => {
|
onClick={(): void => {
|
||||||
if (maptype === 'Satellite') {
|
ChangeMapStyle('Satellite', MapStyles.Satellite);
|
||||||
setMaptype('Default');
|
|
||||||
} else {
|
|
||||||
setMaptype('Satellite');
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
icon={<FaGlobeAfrica />}
|
icon={<FaGlobeAfrica />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`p-1 -m-1 space-y-2 border-gray-400 rounded-md dark:border-gray-200 ${
|
className={`p-1 -m-1 space-y-2 border-gray-400 rounded-md dark:border-gray-200 ${
|
||||||
maptype === 'Outdoors' ? 'border' : ''
|
mapState.style.title === 'Outdoors' ? 'border' : ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
active={maptype === 'Outdoors'}
|
active={mapState.style.title === 'Outdoors'}
|
||||||
onClick={(): void => {
|
onClick={(): void => {
|
||||||
if (maptype === 'Outdoors') {
|
ChangeMapStyle('Outdoors', MapStyles.Outdoors);
|
||||||
setMaptype('Default');
|
|
||||||
} else {
|
|
||||||
setMaptype('Outdoors');
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
icon={<FaDirections />}
|
icon={<FaDirections />}
|
||||||
/>
|
/>
|
||||||
{maptype === 'Outdoors' && (
|
{mapState.style.title === 'Outdoors' && (
|
||||||
<IconButton
|
<IconButton
|
||||||
active={shading}
|
active={mapState.hillShade}
|
||||||
onClick={(): void => {
|
onClick={(): void => {
|
||||||
setShading(!shading);
|
dispatch(setHillShade(!mapState.hillShade));
|
||||||
}}
|
}}
|
||||||
icon={<MdWbShade />}
|
icon={<MdWbShade />}
|
||||||
/>
|
/>
|
||||||
@@ -194,9 +212,9 @@ export const Map = (): JSX.Element => {
|
|||||||
|
|
||||||
<hr className="text-gray-400 dark:text-gray-200" />
|
<hr className="text-gray-400 dark:text-gray-200" />
|
||||||
<IconButton
|
<IconButton
|
||||||
active={exaggeration}
|
active={mapState.exaggeration}
|
||||||
onClick={(): void => {
|
onClick={(): void => {
|
||||||
setExaggeration(!exaggeration);
|
dispatch(setExaggeration(!mapState.exaggeration));
|
||||||
}}
|
}}
|
||||||
icon={<FaMountain />}
|
icon={<FaMountain />}
|
||||||
/>
|
/>
|
||||||
@@ -204,7 +222,7 @@ export const Map = (): JSX.Element => {
|
|||||||
<IconButton icon={<MdRadar />} />
|
<IconButton icon={<MdRadar />} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full h-full">
|
<div className="flex w-full h-full">
|
||||||
<div className="flex-grow w-full h-full" ref={mapDiv} />
|
<div className="flex-grow w-full h-full" ref={mapRef} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import mapboxgl from 'mapbox-gl';
|
||||||
|
|
||||||
|
import type { MapStyle } from '@app/components/Map/styles';
|
||||||
|
import { MapStyles } from '@app/components/Map/styles';
|
||||||
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
interface MapState {
|
||||||
|
latLng: mapboxgl.LngLat;
|
||||||
|
zoom: number;
|
||||||
|
accessToken: string;
|
||||||
|
style: MapStyle;
|
||||||
|
hillShade: boolean;
|
||||||
|
exaggeration: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: MapState = {
|
||||||
|
latLng: new mapboxgl.LngLat(-77.0305, 38.8868),
|
||||||
|
zoom: 9,
|
||||||
|
accessToken:
|
||||||
|
'pk.eyJ1Ijoic2FjaGF3IiwiYSI6ImNrNW9meXozZjBsdW0zbHBjM2FnNnV6cmsifQ.3E4n8eFGD9ZOFo-XDVeZnQ',
|
||||||
|
style:
|
||||||
|
localStorage.getItem('darkMode') === 'true'
|
||||||
|
? MapStyles.Dark
|
||||||
|
: MapStyles.Light,
|
||||||
|
hillShade: false,
|
||||||
|
exaggeration: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mapSlice = createSlice({
|
||||||
|
name: 'map',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setLatLng: (state, action: PayloadAction<mapboxgl.LngLat>) => {
|
||||||
|
state.latLng = action.payload;
|
||||||
|
},
|
||||||
|
setZoom: (state, action: PayloadAction<number>) => {
|
||||||
|
state.zoom = action.payload;
|
||||||
|
},
|
||||||
|
setMapStyle(state, action: PayloadAction<MapStyle>) {
|
||||||
|
state.style = action.payload;
|
||||||
|
},
|
||||||
|
setHillShade(state, action: PayloadAction<boolean>) {
|
||||||
|
state.hillShade = action.payload;
|
||||||
|
},
|
||||||
|
setExaggeration(state, action: PayloadAction<boolean>) {
|
||||||
|
state.exaggeration = action.payload;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const {
|
||||||
|
setLatLng,
|
||||||
|
setZoom,
|
||||||
|
setMapStyle,
|
||||||
|
setHillShade,
|
||||||
|
setExaggeration,
|
||||||
|
} = mapSlice.actions;
|
||||||
|
|
||||||
|
export default mapSlice.reducer;
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
import { configureStore } from '@reduxjs/toolkit';
|
import { configureStore } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
import appReducer from './slices/appSlice';
|
import appReducer from './slices/appSlice';
|
||||||
|
import mapReducer from './slices/mapSlice';
|
||||||
import meshtasticReducer from './slices/meshtasticSlice';
|
import meshtasticReducer from './slices/meshtasticSlice';
|
||||||
|
|
||||||
export const store = configureStore({
|
export const store = configureStore({
|
||||||
reducer: {
|
reducer: {
|
||||||
app: appReducer,
|
app: appReducer,
|
||||||
meshtastic: meshtasticReducer,
|
meshtastic: meshtasticReducer,
|
||||||
|
map: mapReducer,
|
||||||
},
|
},
|
||||||
middleware: (getDefaultMiddleware) =>
|
middleware: (getDefaultMiddleware) =>
|
||||||
getDefaultMiddleware({
|
getDefaultMiddleware({
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import 'mapbox-gl/dist/mapbox-gl.css';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import mapboxgl from 'mapbox-gl';
|
||||||
|
|
||||||
|
type AccessToken = string;
|
||||||
|
|
||||||
|
export function useMapbox(
|
||||||
|
ref: React.RefObject<HTMLDivElement>,
|
||||||
|
accessToken: AccessToken,
|
||||||
|
options?: Partial<mapboxgl.MapboxOptions>,
|
||||||
|
): mapboxgl.Map | undefined {
|
||||||
|
const [mapInstance, setMapInstance] = React.useState<mapboxgl.Map>();
|
||||||
|
React.useEffect(() => {
|
||||||
|
const container = ref.current;
|
||||||
|
if (mapInstance || !container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mapboxgl.accessToken = accessToken;
|
||||||
|
const mergedOptions = { container, ...options };
|
||||||
|
const map = new mapboxgl.Map(mergedOptions);
|
||||||
|
setMapInstance(map);
|
||||||
|
}, [ref, accessToken, options, mapInstance]);
|
||||||
|
|
||||||
|
return mapInstance;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import mapboxgl from 'mapbox-gl';
|
||||||
import { FaSatellite } from 'react-icons/fa';
|
import { FaSatellite } from 'react-icons/fa';
|
||||||
import { FiCode } from 'react-icons/fi';
|
import { FiCode } from 'react-icons/fi';
|
||||||
import { GiLightningFrequency } from 'react-icons/gi';
|
import { GiLightningFrequency } from 'react-icons/gi';
|
||||||
@@ -15,17 +16,23 @@ import {
|
|||||||
} from 'react-icons/md';
|
} from 'react-icons/md';
|
||||||
import TimeAgo from 'timeago-react';
|
import TimeAgo from 'timeago-react';
|
||||||
|
|
||||||
|
import { setLatLng } from '@app/core/slices/mapSlice.js';
|
||||||
|
import { useAppDispatch } from '@app/hooks/redux.js';
|
||||||
import type { Node } from '@core/slices/meshtasticSlice';
|
import type { Node } from '@core/slices/meshtasticSlice';
|
||||||
import { Disclosure } from '@headlessui/react';
|
import { Disclosure } from '@headlessui/react';
|
||||||
import { IconButton } from '@meshtastic/components';
|
import { IconButton } from '@meshtastic/components';
|
||||||
import { Protobuf } from '@meshtastic/meshtasticjs';
|
import { Protobuf } from '@meshtastic/meshtasticjs';
|
||||||
|
|
||||||
|
type PositionConfidence = 'high' | 'low' | 'none';
|
||||||
|
type NodeAge = 'young' | 'aging' | 'old' | 'dead';
|
||||||
|
|
||||||
export interface NodeCardProps {
|
export interface NodeCardProps {
|
||||||
node: Node;
|
node: Node;
|
||||||
myNodeInfo?: Protobuf.MyNodeInfo;
|
myNodeInfo?: Protobuf.MyNodeInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NodeCard = ({ node, myNodeInfo }: NodeCardProps): JSX.Element => {
|
export const NodeCard = ({ node, myNodeInfo }: NodeCardProps): JSX.Element => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
const [snrAverage, setSnrAverage] = React.useState(0);
|
const [snrAverage, setSnrAverage] = React.useState(0);
|
||||||
const [satsAverage, setSatsAverage] = React.useState(0);
|
const [satsAverage, setSatsAverage] = React.useState(0);
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -35,6 +42,32 @@ export const NodeCard = ({ node, myNodeInfo }: NodeCardProps): JSX.Element => {
|
|||||||
.reduce((a, b) => a + b) / (node.snr.length > 3 ? 3 : node.snr.length),
|
.reduce((a, b) => a + b) / (node.snr.length > 3 ? 3 : node.snr.length),
|
||||||
);
|
);
|
||||||
}, [node.snr]);
|
}, [node.snr]);
|
||||||
|
const [PositionConfidence, setPositionConfidence] =
|
||||||
|
React.useState<PositionConfidence>('none');
|
||||||
|
const [age, setAge] = React.useState<NodeAge>('young');
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setAge(
|
||||||
|
node.lastHeard > new Date(Date.now() - 1000 * 60 * 15)
|
||||||
|
? 'young'
|
||||||
|
: node.lastHeard > new Date(Date.now() - 1000 * 60 * 30)
|
||||||
|
? 'aging'
|
||||||
|
: node.lastHeard > new Date(Date.now() - 1000 * 60 * 60)
|
||||||
|
? 'old'
|
||||||
|
: 'dead',
|
||||||
|
);
|
||||||
|
}, [node.lastHeard]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setPositionConfidence(
|
||||||
|
node.currentPosition
|
||||||
|
? new Date(node.currentPosition.posTimestamp * 1000) >
|
||||||
|
new Date(new Date().getTime() - 1000 * 60 * 30)
|
||||||
|
? 'high'
|
||||||
|
: 'low'
|
||||||
|
: 'none',
|
||||||
|
);
|
||||||
|
}, [node.currentPosition]);
|
||||||
|
|
||||||
// React.useEffect(() => {
|
// React.useEffect(() => {
|
||||||
// setSatsAverage(
|
// setSatsAverage(
|
||||||
@@ -52,17 +85,20 @@ export const NodeCard = ({ node, myNodeInfo }: NodeCardProps): JSX.Element => {
|
|||||||
as="div"
|
as="div"
|
||||||
className="m-2 rounded-md shadow-md bg-gray-50 dark:bg-gray-700"
|
className="m-2 rounded-md shadow-md bg-gray-50 dark:bg-gray-700"
|
||||||
>
|
>
|
||||||
<Disclosure.Button className="flex w-full gap-2 p-2 bg-gray-100 rounded-md shadow-md dark:bg-primaryDark">
|
<Disclosure.Button
|
||||||
|
as="div"
|
||||||
|
className="flex w-full gap-2 p-2 bg-gray-100 rounded-md shadow-md dark:bg-primaryDark"
|
||||||
|
>
|
||||||
{myNodeInfo ? (
|
{myNodeInfo ? (
|
||||||
<MdAccountCircle className="my-auto" />
|
<MdAccountCircle className="my-auto" />
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
className={`my-auto w-3 h-3 rounded-full ${
|
className={`my-auto w-3 h-3 rounded-full ${
|
||||||
node.lastHeard > new Date(Date.now() - 1000 * 60 * 15)
|
age === 'young'
|
||||||
? 'bg-green-500'
|
? 'bg-green-500'
|
||||||
: node.lastHeard > new Date(Date.now() - 1000 * 60 * 30)
|
: age === 'aging'
|
||||||
? 'bg-yellow-500'
|
? 'bg-yellow-500'
|
||||||
: node.lastHeard > new Date(Date.now() - 1000 * 60 * 60)
|
: age === 'old'
|
||||||
? 'bg-red-500'
|
? 'bg-red-500'
|
||||||
: 'bg-gray-500'
|
: 'bg-gray-500'
|
||||||
}`}
|
}`}
|
||||||
@@ -81,17 +117,30 @@ export const NodeCard = ({ node, myNodeInfo }: NodeCardProps): JSX.Element => {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<IconButton
|
||||||
{node.currentPosition ? (
|
disabled={PositionConfidence === 'none'}
|
||||||
new Date(node.positions[0].posTimestamp * 1000) >
|
onClick={() => {
|
||||||
new Date(new Date().getTime() - 1000 * 60 * 30) ? (
|
if (PositionConfidence !== 'none' && node.currentPosition) {
|
||||||
<IconButton icon={<MdGpsFixed />} />
|
dispatch(
|
||||||
) : (
|
setLatLng(
|
||||||
<IconButton icon={<MdGpsNotFixed />} />
|
new mapboxgl.LngLat(
|
||||||
)
|
node.currentPosition.longitudeI / 1e7,
|
||||||
) : (
|
node.currentPosition.latitudeI / 1e7,
|
||||||
<IconButton disabled icon={<MdGpsOff />} />
|
),
|
||||||
)}
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
icon={
|
||||||
|
PositionConfidence === 'high' ? (
|
||||||
|
<MdGpsFixed />
|
||||||
|
) : PositionConfidence === 'low' ? (
|
||||||
|
<MdGpsNotFixed />
|
||||||
|
) : (
|
||||||
|
<MdGpsOff />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Disclosure.Button>
|
</Disclosure.Button>
|
||||||
<Disclosure.Panel className="p-2">
|
<Disclosure.Panel className="p-2">
|
||||||
{myNodeInfo && (
|
{myNodeInfo && (
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import React from 'react';
|
|||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { FiCode, FiMenu } from 'react-icons/fi';
|
import { FiCode, FiMenu } from 'react-icons/fi';
|
||||||
import JSONPretty from 'react-json-pretty';
|
import JSONPretty from 'react-json-pretty';
|
||||||
import ReactSelect, { Theme } from 'react-select';
|
import type { Theme } from 'react-select';
|
||||||
|
import ReactSelect from 'react-select';
|
||||||
|
|
||||||
import { useAppSelector } from '@app/hooks/redux';
|
import { useAppSelector } from '@app/hooks/redux';
|
||||||
import { FormFooter } from '@components/FormFooter';
|
import { FormFooter } from '@components/FormFooter';
|
||||||
|
|||||||
Reference in New Issue
Block a user