Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Help with combining multiple styles in react native #1739

Open
MahmoudMousaHamad opened this issue Sep 27, 2024 · 0 comments
Open

Help with combining multiple styles in react native #1739

MahmoudMousaHamad opened this issue Sep 27, 2024 · 0 comments

Comments

@MahmoudMousaHamad
Copy link

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant