|
|
|
@ -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); |
|
|
|
|
|
|
|
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 PlaceNodes = (): void => { |
|
|
|
const mapState = useAppSelector((state) => state.map); |
|
|
|
const [markers, setMarkers] = React.useState< |
|
|
|
{ id: number; marker: mapboxgl.Marker }[] |
|
|
|
>([]); |
|
|
|
const mapRef = React.useRef<HTMLDivElement>(null); |
|
|
|
|
|
|
|
const map = useMapbox(mapRef, mapState.accessToken, { |
|
|
|
center: mapState.latLng, |
|
|
|
zoom: mapState.zoom, |
|
|
|
style: mapState.style.url, |
|
|
|
}); |
|
|
|
|
|
|
|
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]); |
|
|
|
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, |
|
|
|
}); |
|
|
|
} |
|
|
|
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(() => { |
|
|
|
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; |
|
|
|
} |
|
|
|
const center = map?.getCenter(); |
|
|
|
if (center !== mapState.latLng) { |
|
|
|
map?.setCenter(mapState.latLng); |
|
|
|
} |
|
|
|
}, [maptype, darkMode, map]); |
|
|
|
}, [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> |
|
|
|
); |
|
|
|
|