Rebuild
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
Reference in New Issue
Block a user