react-native-mapbox-gl
react-native-mapbox-gl copied to clipboard
Help with combining multiple styles in react native
I have a react native application where I have multiple styles that can be categorized into two types. Styles that are used as basemaps and other styles that are used as overlays on top of the basemap styles. The user should be able to choose multiple of those overlays in combination with any single basemap style. I came up with code that combines the layers and sources for each style and combines them into a single master JSON style that is fed to the MapView
. This approach is working for some overlay styles but there are conflicts between shapes. When choosing multiple overlays where each overlay contains shapes and/or vector data, conflicts happen where the last chosen overlay's shapes/vectors disappear when the map is zoomed in. The text is not affected though. Below is the code for the store that manages appends and removes the layers and the MapView component:
Store:
import Geolocation from '@react-native-community/geolocation';
import {Alert} from 'react-native';
import {create} from 'zustand';
import {fetchStyleJSON} from '../utils';
type Basemap = 'nautical' | 'bing' | 'street';
export type Overlay =
| 'canal'
| 'structure'
| 'floridaBackCountry'
| 'proStructureMap'
| 'tampaStructureMap'
| 'navAids'
| 'seaSurfaceTemp'
| 'radar'
| 'lightning'
| 'chlorophyll'
| 'salinity'
| 'currents'
| 'wind'
| 'midAtlantic'
| 'greatLakes'
| 'lakeIstokpoga'
| 'neAtlantic'
| 'fishingSpots'
| 'oysterBeds'
| 'seagrass'
| 'coral'
| 'routes';
type StyleFetchingInfo = {
username: string;
id: string;
};
type SimpleMapboxLayer = {
id: string;
source: string;
layout?: {visibility: 'visible' | 'none'};
};
type SimpleMapboxStyle = {
layers: SimpleMapboxLayer[];
sources: {[key: string]: any};
sprite: any;
version: number;
glyphs: string;
};
interface StoreState {
mapBoxStyle: SimpleMapboxStyle | null;
overlayStyles: StyleFetchingInfo[];
basemapStyle: StyleFetchingInfo;
setBasemapStyle: (style: StyleFetchingInfo) => void;
appendOverlayStyle: (style: StyleFetchingInfo) => void;
removeOverlayStyle: (style: StyleFetchingInfo) => void;
basemaps: Record<Basemap, boolean>;
overlays: Record<Overlay, boolean>;
toggleBasemap: (basemap: Basemap) => void;
toggleOverlay: (overlay: Overlay) => void;
opacity: number;
setOpacity: (value: number) => void;
updateUserLocation: () => void;
userLocation: number[];
setUserLocation: (value: number[]) => void;
centerToDisplay: number[];
setCenterToDisplay: (value: number[]) => void;
showBasemapSelector: boolean;
setShowBasemapSelector: (value: boolean) => void;
addMapBoxStyle: (id: string, username: string, top?: boolean) => void;
removeMapBoxStyle: (id: string, username: string) => void;
init: () => Promise<void>;
resetMapboxStyle: () => void;
renderStyles: () => void;
bbox: number[];
updateBbox: (bbox: number[]) => Promise<void>;
followUserLocation: boolean;
setFollowUserLocation: (v: boolean) => void;
currentWeatherTime: Date;
currentWeatherFrame: number;
}
const useStore = create<StoreState>((set, get) => ({
followUserLocation: false,
setFollowUserLocation: (v: boolean) => {
set({
followUserLocation: v,
});
},
currentWeatherFrame: -1,
currentWeatherTime: new Date(),
mapBoxStyle: {
version: 8,
sources: {},
layers: [],
glyphs: 'mapbox://fonts/mapbox/{fontstack}/{range}.pbf',
sprite:
'mapbox://sprites/n4kexe/clalayzj7001b14rx57xzieq7/82ng0xsookf655np1mbvczlkl',
},
bbox: [-180, -90, 180, 90],
updateBbox: async (bbox: number[]) => {
set({bbox});
},
basemapStyle: {
id: 'streets-v12',
username: 'mapbox',
},
overlayStyles: [],
resetMapboxStyle: () => {
const {mapBoxStyle} = get();
if (mapBoxStyle) {
mapBoxStyle.layers = [];
mapBoxStyle.sources = {};
set({mapBoxStyle});
}
},
setBasemapStyle: basemapStyle => {
set({basemapStyle});
get().resetMapboxStyle();
},
renderStyles: () => {
get().resetMapboxStyle();
get().addMapBoxStyle(get().basemapStyle.id, get().basemapStyle.username);
get().overlayStyles.forEach(s => get().addMapBoxStyle(s.id, s.username));
},
appendOverlayStyle: style =>
set(state => ({overlayStyles: [...state.overlayStyles, style]})),
removeOverlayStyle: style => {
const {overlayStyles} = get();
const filteredOverlayStyles = overlayStyles.filter(
s => s.id !== style.id || s.username !== style.username,
);
set({
overlayStyles: filteredOverlayStyles,
});
},
addMapBoxStyle: (id: string, username: string) => {
fetchStyleJSON(id, username).then(async newStyleJSON => {
const currentStyle = get().mapBoxStyle;
if (!currentStyle) {
return;
}
const prefix = `catch_map_${id}_${username}`;
Object.entries(newStyleJSON.sources).forEach(([source_key, source]) => {
currentStyle.sources[prefix + '_' + source_key] = source;
});
newStyleJSON.layers.forEach((layer: SimpleMapboxLayer) => {
layer.id = prefix + '_' + layer.id;
if (layer.source) {
layer.source = prefix + '_' + layer.source;
currentStyle.layers.push(layer);
}
});
currentStyle.sprite = newStyleJSON.sprite;
set({
mapBoxStyle: currentStyle,
});
});
},
removeMapBoxStyle: (id: string, username: string) => {
const currentStyle = get().mapBoxStyle;
if (!currentStyle) {
return;
}
const prefix = `catch_map_${id}_${username}`;
for (let sourceKey in currentStyle.sources) {
if (sourceKey.startsWith(prefix)) {
const sources = currentStyle.sources;
delete sources[sourceKey];
currentStyle.sources = {...sources};
}
}
currentStyle.layers = currentStyle.layers.filter(
layer => !layer.id.startsWith(prefix),
);
set({
mapBoxStyle: {...currentStyle},
});
},
init: async () => {
const style = await fetchStyleJSON('streets-v12', 'mapbox');
get().updateUserLocation();
set({
mapBoxStyle: style,
});
},
basemaps: {
nautical: false,
bing: false,
street: true,
},
overlays: {
canal: false,
structure: false,
floridaBackCountry: false,
proStructureMap: false,
tampaStructureMap: false,
navAids: false,
chlorophyll: false,
currents: false,
lightning: false,
salinity: false,
seaSurfaceTemp: false,
radar: false,
wind: false,
greatLakes: false,
lakeIstokpoga: false,
midAtlantic: false,
neAtlantic: false,
coral: false,
fishingSpots: false,
oysterBeds: false,
routes: false,
seagrass: false,
},
toggleBasemap: basemap => {
set(state => ({
basemaps: {
street: false,
bing: false,
nautical: false,
[basemap]: !state.basemaps[basemap],
},
}));
const noLayerSelected = Object.values(get().basemaps).every(f => !f);
if (noLayerSelected) {
set(state => ({
basemaps: {
...state.basemaps,
street: true,
},
}));
}
},
toggleOverlay: overlay => {
set(state => ({
overlays: {
...state.overlays,
[overlay]: !state.overlays[overlay],
},
}));
},
opacity: 1,
setOpacity: v => {
set({
opacity: v,
});
},
userLocation: [-80.1918, 25.7617],
centerToDisplay: [-80.1918, 25.7617],
setCenterToDisplay: (value: number[]) => {
set({centerToDisplay: value});
},
updateUserLocation: () => {
try {
set({followUserLocation: true});
Geolocation.getCurrentPosition(
position => {
set({
userLocation: [position.coords.longitude, position.coords.latitude],
});
},
error => {
console.log('err', error.message);
if (error.message === 'No location provider available.') {
Alert.alert('Please turn on device location');
}
console.log(error.code, error.message);
},
);
} catch (error) {
console.error('Error getting current location:', error);
}
},
setUserLocation: value => {
set({userLocation: value});
},
showBasemapSelector: false,
setShowBasemapSelector: (value: boolean) => {
set({showBasemapSelector: value});
},
}));
export default useStore;
import {check, PERMISSIONS, RESULTS, request} from 'react-native-permissions';
import React, {useCallback, useEffect, useState, useRef} from 'react';
import Mapbox, {Camera, UserLocation} from '@rnmapbox/maps';
import {useFocusEffect} from '@react-navigation/native';
import {Dimensions, StyleSheet, View} from 'react-native';
import LatLngToDMS from '../components/LatLngToDMS';
import BasemapSelector from '../components/basemap-selector/BasemapSelector';
import Controls from '../components/Controls';
import useStore from '../stores/map';
import VerticalSlider from '../components/Slider';
import MapCursor from '../components/MapCursor';
import {MB_ACCESS_TOKEN} from '../utils';
import OverlayLayers from '../components/OverlayLayers';
import SeaSurfaceTemperatureDialog from '../components/SeaSurfaceTemperatureDialog';
import OverLaySliders from '../components/OverlaySliders';
Mapbox.setAccessToken(MB_ACCESS_TOKEN);
const Home: React.FC = () => {
const {
userLocation,
setUserLocation,
setCenterToDisplay,
centerToDisplay,
showBasemapSelector,
setShowBasemapSelector,
mapBoxStyle,
init,
overlayStyles,
basemapStyle,
renderStyles,
updateBbox,
overlays,
followUserLocation,
setFollowUserLocation,
} = useStore();
const [locationPermission, setLocationPermission] = useState<boolean | null>(
null,
);
const cameraRef = useRef<Camera>(null);
const mapRef = useRef<Mapbox.MapView>(null);
const screenWidth = Dimensions.get('window').width;
const scaleBarLeftPosition = (screenWidth - 195) / 2;
const toggleBasemapSelector = () =>
setShowBasemapSelector(!showBasemapSelector);
useFocusEffect(
useCallback(() => {
const checkLocationPermission = async () => {
const result = await check(PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION);
if (result === RESULTS.GRANTED) {
setLocationPermission(true);
} else {
setLocationPermission(false);
}
};
checkLocationPermission();
}, []),
);
const requestLocationPermission = async () => {
const result = await request(PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION);
if (result === RESULTS.GRANTED) {
setLocationPermission(true);
} else {
setLocationPermission(false);
}
};
useEffect(() => {
if (locationPermission === null) {
return;
}
if (!locationPermission) {
requestLocationPermission();
}
}, [locationPermission]);
useEffect(() => {
if (followUserLocation) {
cameraRef?.current?.setCamera({
centerCoordinate: [-80.1918, 25.7617],
});
}
}, [followUserLocation, userLocation]);
useEffect(() => {
init();
}, [init]);
useEffect(() => {
renderStyles();
}, [basemapStyle, overlayStyles]);
return (
<View style={styles.page}>
{!followUserLocation && <MapCursor />}
<Mapbox.MapView
attributionEnabled={false}
ref={mapRef}
style={styles.map}
styleJSON={JSON.stringify(mapBoxStyle)}
logoEnabled={false}
scaleBarEnabled={true}
scaleBarPosition={{bottom: 45, left: scaleBarLeftPosition}}
onTouchMove={() => followUserLocation && setFollowUserLocation(false)}
onCameraChanged={e => {
setCenterToDisplay(e.properties.center);
updateBbox([...e.properties.bounds.sw, ...e.properties.bounds.ne]);
}}>
<OverlayLayers />
<UserLocation
visible={true}
animated={true}
minDisplacement={10}
showsUserHeadingIndicator={true}
requestsAlwaysUse={true}
onUpdate={loc => {
if (followUserLocation) {
setUserLocation([loc?.coords?.longitude, loc?.coords.latitude]);
}
}}
/>
<Camera
maxZoomLevel={19}
minZoomLevel={0}
ref={cameraRef}
centerCoordinate={[-80.1918, 25.7617]}
defaultSettings={{
centerCoordinate: [-80.1918, 25.7617],
zoomLevel: 8,
animationDuration: 1,
}}
allowUpdates={true}
animationMode="flyTo"
/>
</Mapbox.MapView>
{showBasemapSelector && <BasemapSelector />}
{overlays.seaSurfaceTemp && <SeaSurfaceTemperatureDialog />}
<Controls
toggleBasemapSelector={toggleBasemapSelector}
isUserLocationOn={true}
/>
<VerticalSlider />
<OverLaySliders />
<LatLngToDMS
longitude={centerToDisplay[0]}
latitude={centerToDisplay[1]}
/>
</View>
);
};
const styles = StyleSheet.create({
page: {
flex: 1,
},
map: {
flex: 1,
},
});
export default Home;
Any ideas?