This commit is contained in:
Sacha Weatherstone
2022-02-07 00:00:55 +11:00
parent e43b04990b
commit 8d184578f0
109 changed files with 3495 additions and 3632 deletions
+90
View File
@@ -0,0 +1,90 @@
import React from 'react';
import { FaDirections, FaGlobeAfrica, FaMountain } from 'react-icons/fa';
import { MdFullscreen, MdRadar, MdWbShade } from 'react-icons/md';
import {
setExaggeration,
setHillShade,
setMapStyle,
} from '@core/slices/mapSlice';
import { useAppDispatch } from '@hooks/useAppDispatch';
import { useAppSelector } from '@hooks/useAppSelector';
import { useMapbox } from '@hooks/useMapbox';
import { IconButton } from '@meshtastic/components';
import type { MapStyle } from './styles';
import { MapStyles } from './styles';
export const MapContainer = (): JSX.Element => {
const dispatch = useAppDispatch();
const darkMode = useAppSelector((state) => state.app.darkMode);
const mapState = useAppSelector((state) => state.map);
const { ref } = useMapbox();
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],
);
return (
<div className="relative flex h-full w-full">
<div className="absolute right-0 z-20 m-4 space-y-2 rounded-md border border-gray-300 bg-white p-2 shadow-md dark:border-gray-600 dark:bg-primaryDark">
<IconButton
active={mapState.style.title === 'Satellite'}
onClick={(): void => {
ChangeMapStyle('Satellite', MapStyles.Satellite);
}}
icon={<FaGlobeAfrica />}
/>
<div
className={`-m-1 space-y-2 rounded-md border-gray-300 p-1 dark:border-gray-600 ${
mapState.style.title === 'Outdoors' ? 'border' : ''
}`}
>
<IconButton
active={mapState.style.title === 'Outdoors'}
onClick={(): void => {
ChangeMapStyle('Outdoors', MapStyles.Outdoors);
}}
icon={<FaDirections />}
/>
{mapState.style.title === 'Outdoors' && (
<IconButton
active={mapState.hillShade}
onClick={(): void => {
dispatch(setHillShade(!mapState.hillShade));
}}
icon={<MdWbShade />}
/>
)}
</div>
<hr className="text-gray-400 dark:text-gray-200" />
<IconButton
active={mapState.exaggeration}
onClick={(): void => {
dispatch(setExaggeration(!mapState.exaggeration));
}}
icon={<FaMountain />}
/>
<IconButton icon={<MdFullscreen />} />
<IconButton icon={<MdRadar />} />
</div>
<div className="h-full w-full flex-grow" ref={ref} />
</div>
);
};
+43
View File
@@ -0,0 +1,43 @@
import React from 'react';
import ReactDOM from 'react-dom';
import mapbox from 'mapbox-gl';
import { useMapbox } from '@hooks/useMapbox';
export interface MarkerProps extends Omit<mapbox.MarkerOptions, 'element'> {
children?: React.ReactNode;
center: mapbox.LngLatLike;
}
export const Marker = ({
children,
center,
...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);
marker.addTo(map);
}
}, [map, center, props]);
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);
};
+85
View File
@@ -0,0 +1,85 @@
import React from 'react';
import mapboxgl from 'mapbox-gl';
import { FiMapPin } from 'react-icons/fi';
import { RiRoadMapLine } from 'react-icons/ri';
import { Layout } from '@app/components/layout';
import { MapboxProvider } from '@app/components/MapBox/MapboxProvider';
import type { Node } from '@app/core/slices/meshtasticSlice';
import { useAppSelector } from '@app/hooks/useAppSelector';
import { NodeCard } from '../Nodes/NodeCard';
import { MapContainer } from './MapContainer';
import { Marker } from './Marker';
export const Map = (): JSX.Element => {
const [selectedNode, setSelectedNode] = React.useState<Node>();
const nodes = useAppSelector((state) => state.meshtastic.nodes);
const myNodeNum = useAppSelector(
(state) => state.meshtastic.radio.hardware.myNodeNum,
);
return (
<MapboxProvider>
{nodes.map((node) => {
return (
node.currentPosition && (
<Marker
key={node.number}
center={
new mapboxgl.LngLat(
node.currentPosition.longitudeI / 1e7,
node.currentPosition.latitudeI / 1e7,
)
}
>
<div
onClick={(): void => {
setSelectedNode(node);
}}
className={`z-50 rounded-full border-2 bg-opacity-30 ${
node.number === selectedNode?.number
? 'border-green-500 bg-green-500'
: 'border-blue-500 bg-blue-500'
}`}
>
<div className="m-4 ">
<FiMapPin className="h-5 w-5" />
</div>
</div>
</Marker>
)
);
})}
<Layout
title="Nodes"
icon={<RiRoadMapLine />}
sidebarContents={
<div className="flex flex-col gap-2">
{!nodes.length && (
<span className="p-4 text-sm text-gray-400 dark:text-gray-600">
No nodes found.
</span>
)}
{nodes.map((node) => (
<NodeCard
key={node.number}
node={node}
isMyNode={node.number === myNodeNum}
selected={selectedNode?.number === node.number}
setSelected={(): void => {
setSelectedNode(node);
}}
/>
))}
</div>
}
>
<MapContainer />
</Layout>
</MapboxProvider>
);
};
+28
View File
@@ -0,0 +1,28 @@
export interface MapStyle {
title: string;
url: string;
}
export const MapStyles = {
Streets: {
title: 'Streets',
url: 'mapbox://styles/mapbox/streets-v11?optimize=true',
} as MapStyle,
Outdoors: {
title: 'Outdoors',
url: 'mapbox://styles/mapbox/outdoors-v11?optimize=true',
} as MapStyle,
Light: {
title: 'Light',
url: 'mapbox://styles/mapbox/light-v10?optimize=true',
} as MapStyle,
Dark: {
title: 'Dark',
url: 'mapbox://styles/sachaw/ckwzwm92e1oep14pjunjqlqbo?optimize=true',
} as MapStyle,
Satellite: {
title: 'Satellite',
url: 'mapbox://styles/mapbox/satellite-v9?optimize=true',
} as MapStyle,
};