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 mapboxgl from 'mapbox-gl';
|
||||
@@ -7,185 +5,205 @@ import { renderToString } from 'react-dom/server';
|
||||
import { FaDirections, FaGlobeAfrica, FaMountain } from 'react-icons/fa';
|
||||
import { MdFullscreen, MdRadar, MdWbShade } from 'react-icons/md';
|
||||
|
||||
import { useAppSelector } from '@app/hooks/redux';
|
||||
import { IconButton } from '@meshtastic/components';
|
||||
import { useMapbox } from '@app/hooks/mapbox';
|
||||
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';
|
||||
|
||||
export const Map = (): JSX.Element => {
|
||||
const dispatch = useAppDispatch();
|
||||
const darkMode = useAppSelector((state) => state.app.darkMode);
|
||||
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 =
|
||||
'pk.eyJ1Ijoic2FjaGF3IiwiYSI6ImNrNW9meXozZjBsdW0zbHBjM2FnNnV6cmsifQ.3E4n8eFGD9ZOFo-XDVeZnQ';
|
||||
const mapDiv = React.useRef<HTMLDivElement>(null);
|
||||
const [map, setMap] = React.useState(null as mapboxgl.Map | null);
|
||||
const [exaggeration, setExaggeration] = React.useState(false);
|
||||
const [shading, setShading] = React.useState(false);
|
||||
const [maptype, setMaptype] = React.useState<
|
||||
'Streets' | 'Outdoors' | 'Satellite' | 'Default'
|
||||
>('Default');
|
||||
const map = useMapbox(mapRef, mapState.accessToken, {
|
||||
center: mapState.latLng,
|
||||
zoom: mapState.zoom,
|
||||
style: mapState.style.url,
|
||||
});
|
||||
|
||||
const PlaceNodes = (): void => {
|
||||
const updateNodes = React.useCallback(() => {
|
||||
nodes.map((node) => {
|
||||
if (node.currentPosition && map) {
|
||||
new mapboxgl.Marker({})
|
||||
.setLngLat({
|
||||
lat: node.currentPosition?.latitudeI / 1e7,
|
||||
lng: node.currentPosition?.longitudeI / 1e7,
|
||||
})
|
||||
if (map?.loaded() && node.currentPosition) {
|
||||
const existingMarker = markers.find(
|
||||
(marker) => marker.id === node.number,
|
||||
)?.marker;
|
||||
const marker =
|
||||
existingMarker ??
|
||||
new mapboxgl.Marker({}).setLngLat([0, 0]).addTo(map);
|
||||
|
||||
marker
|
||||
.setLngLat([
|
||||
node.currentPosition.longitudeI / 1e7,
|
||||
node.currentPosition.latitudeI / 1e7,
|
||||
])
|
||||
.setPopup(
|
||||
new mapboxgl.Popup().setHTML(
|
||||
renderToString(
|
||||
<div>
|
||||
<Card>
|
||||
<div className="text-xl font-medium">
|
||||
{node.user?.longName}
|
||||
</div>
|
||||
<ul>
|
||||
<li>ID: {node.number}</li>
|
||||
</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(() => {
|
||||
PlaceNodes();
|
||||
}, [nodes]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (map?.loaded()) {
|
||||
switch (maptype) {
|
||||
case 'Outdoors':
|
||||
map.setStyle(MapStyles.Outdoors.url);
|
||||
break;
|
||||
|
||||
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?.on('load', () => {
|
||||
updateNodes();
|
||||
});
|
||||
map?.on('styledata', () => {
|
||||
if (!map.getSource('mapbox-dem')) {
|
||||
map.addSource('mapbox-dem', {
|
||||
type: 'raster-dem',
|
||||
url: 'mapbox://mapbox.mapbox-terrain-dem-v1',
|
||||
tileSize: 512,
|
||||
maxzoom: 14,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [maptype, darkMode, map]);
|
||||
map.setTerrain({
|
||||
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(() => {
|
||||
if (map?.loaded()) {
|
||||
if (shading) {
|
||||
map
|
||||
.addSource('mapbox-dem', {
|
||||
type: 'raster-dem',
|
||||
url: 'mapbox://mapbox.mapbox-terrain-dem-v1',
|
||||
tileSize: 512,
|
||||
maxzoom: 14,
|
||||
})
|
||||
.addLayer(
|
||||
{
|
||||
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',
|
||||
);
|
||||
if (mapState.hillShade) {
|
||||
map.addLayer(
|
||||
{
|
||||
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 {
|
||||
map.removeLayer('hillshading');
|
||||
}
|
||||
}
|
||||
}, [shading, map]);
|
||||
}, [map, mapState.hillShade]);
|
||||
|
||||
/**
|
||||
* Exaggeration
|
||||
*/
|
||||
React.useEffect(() => {
|
||||
if (map?.loaded()) {
|
||||
if (exaggeration) {
|
||||
map
|
||||
.addSource('mapbox-dem', {
|
||||
type: 'raster-dem',
|
||||
url: 'mapbox://mapbox.mapbox-terrain-dem-v1',
|
||||
tileSize: 512,
|
||||
maxzoom: 14,
|
||||
})
|
||||
.setTerrain({
|
||||
source: 'mapbox-dem',
|
||||
exaggeration: 1.5,
|
||||
});
|
||||
} else {
|
||||
map.setTerrain();
|
||||
}
|
||||
map.setTerrain({
|
||||
source: 'mapbox-dem',
|
||||
exaggeration: mapState.exaggeration ? 1.5 : 0,
|
||||
});
|
||||
}
|
||||
}, [exaggeration, map]);
|
||||
}, [map, mapState.exaggeration]);
|
||||
|
||||
/**
|
||||
* Map Style
|
||||
*/
|
||||
React.useEffect(() => {
|
||||
if (!map && mapDiv.current) {
|
||||
const map = new mapboxgl.Map({
|
||||
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();
|
||||
});
|
||||
if (map?.loaded()) {
|
||||
map.setStyle(mapState.style.url);
|
||||
}
|
||||
}, [map, darkMode]);
|
||||
}, [map, mapState.style]);
|
||||
|
||||
/**
|
||||
* Markers
|
||||
*/
|
||||
React.useEffect(() => {
|
||||
updateNodes();
|
||||
}, [nodes, updateNodes]);
|
||||
|
||||
return (
|
||||
<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">
|
||||
<IconButton
|
||||
active={maptype === 'Satellite'}
|
||||
active={mapState.style.title === 'Satellite'}
|
||||
onClick={(): void => {
|
||||
if (maptype === 'Satellite') {
|
||||
setMaptype('Default');
|
||||
} else {
|
||||
setMaptype('Satellite');
|
||||
}
|
||||
ChangeMapStyle('Satellite', MapStyles.Satellite);
|
||||
}}
|
||||
icon={<FaGlobeAfrica />}
|
||||
/>
|
||||
|
||||
<div
|
||||
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
|
||||
active={maptype === 'Outdoors'}
|
||||
active={mapState.style.title === 'Outdoors'}
|
||||
onClick={(): void => {
|
||||
if (maptype === 'Outdoors') {
|
||||
setMaptype('Default');
|
||||
} else {
|
||||
setMaptype('Outdoors');
|
||||
}
|
||||
ChangeMapStyle('Outdoors', MapStyles.Outdoors);
|
||||
}}
|
||||
icon={<FaDirections />}
|
||||
/>
|
||||
{maptype === 'Outdoors' && (
|
||||
{mapState.style.title === 'Outdoors' && (
|
||||
<IconButton
|
||||
active={shading}
|
||||
active={mapState.hillShade}
|
||||
onClick={(): void => {
|
||||
setShading(!shading);
|
||||
dispatch(setHillShade(!mapState.hillShade));
|
||||
}}
|
||||
icon={<MdWbShade />}
|
||||
/>
|
||||
@@ -194,9 +212,9 @@ export const Map = (): JSX.Element => {
|
||||
|
||||
<hr className="text-gray-400 dark:text-gray-200" />
|
||||
<IconButton
|
||||
active={exaggeration}
|
||||
active={mapState.exaggeration}
|
||||
onClick={(): void => {
|
||||
setExaggeration(!exaggeration);
|
||||
dispatch(setExaggeration(!mapState.exaggeration));
|
||||
}}
|
||||
icon={<FaMountain />}
|
||||
/>
|
||||
@@ -204,7 +222,7 @@ export const Map = (): JSX.Element => {
|
||||
<IconButton icon={<MdRadar />} />
|
||||
</div>
|
||||
<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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user