DEV Community

Ajmal Hasan
Ajmal Hasan

Posted on • Edited on

React Native Google Map Optimisation

Image description

Video Url

Library used for map:
https://www.npmjs.com/package/react-native-maps

The post is about how to make app crash free and optimised when huge amount of marker/polygon is to be shown on map. There is lot of performance issue when we use map with multiple data and we need to update Custom marker as per zoom level. So I will walk you through each step needed to make the app run on both platform easily.

Sample data we are using:

export const kmlData = {
  status: true,
  data: [
    {
      id: 3,
      polygons: [
        {latitude: 24.79672061085053, longitude: 46.69632228737188},
        {latitude: 24.79730088563427, longitude: 46.6960244924467},
        {latitude: 24.7969867813524, longitude: 46.69528762847696},
        {latitude: 24.796409228271, longitude: 46.69558319234557},
        {latitude: 24.79672061085053, longitude: 46.69632228737188},
      ],
      center: {latitude: 24.796853402304418, longitude: 46.69580273788693},
    },
    {
      id: 5,
      polygons: [
        {latitude: 24.79478092109267, longitude: 46.69287860779153},
        {latitude: 24.79491840277788, longitude: 46.69320194403617},
        {latitude: 24.79511421069013, longitude: 46.6931029170428},
        {latitude: 24.79497672880431, longitude: 46.69277958136081},
        {latitude: 24.79478092109267, longitude: 46.69287860779153},
      ],
      center: {latitude: 24.794971095661616, longitude: 46.693035073221495},
    },
*
*
*
*
]}

Enter fullscreen mode Exit fullscreen mode

Explanation:

  1. Always remember trackViewChanges prop of Map should always be kept false if we want to run map without crash/lag mainly for android it rerenders. Enabling it will let marker change content of info window after first render pass, but will lead to decreased performance, so it's recommended to disable it whenever you don't need it.
  2. For iOS we are setting trackViewChanges true only when app region's zoom level(Latitudedelta) is changed then again after some delay we make it false, though with this fix android will not work properly.
  3. For android we are giving specific key attribute which makes marker rerender whenever region is changed and also we setting trackViewChanges as false always.

Extras:

  1. getCenterOfAllPolygon() gives the center of polygon so that we can place marker on center of polygon as we cannot put any view inside Map's polygon and showing any view over polygon we have to use marker over it.
  2. animateToRegion(coordinate,delay) is used to move to certain region.
  3. fitToCoordinates(polygonsArray, {edgePadding,animated}) is used when we want to click on polygon zoom to polygon level without cropping any side.

File
Map.js

export default function MapPlots({}) {
  const mapRef = useRef(null);
  const [markerType, setMarkerType] = useState(0);
  const [clickedPolygon, setClickedPolygon] = useState();
  const [mapType, setmapType] = useState(SATELLITE_MAP_MODE);
  const [trackViewChanges, settrackViewChanges] = useState(false);
  const [regionChanged, setregionChanged] = useState(null);
  const {data} = kmlData;
  const isIOS = Platform.OS === 'ios' ? true : false;
  const coordinatesData = data.map((v, i) => ({
    ...v,
    color:
      i % 2 === 0
        ? [COLOR_GREEN_OPACITY, COLOR_GREEN]
        : [COLOR_RED_OPACITY, COLOR_RED],
  }));
  const navigation = useNavigation();
  const initialRegion = {
    ...coordinatesData[0]?.polygons[0],
    latitudeDelta: 1,
    longitudeDelta: 50,
  };

  console.log('coordinatesDatacoordinatesData', coordinatesData);

  const onMapReady = () => {
    InteractionManager.runAfterInteractions(() => goToLocation());
    // getCenterOfAllPolygon();
  };

  const goToLocation = () => {
    let initCoordinates = coordinatesData[0]?.polygons[0] || [];
    let c2 = {
      ...initCoordinates,
      latitudeDelta: 0.005,
      longitudeDelta: 0.005,
    };
    mapRef?.current?.animateToRegion(c2, 5 * 1000);
  };

  // const getCenterOfAllPolygon = () => {
  //   let centers = [];
  //   polygonsCoordinates.map((v, i) => {
  //     let c = getCenterPolygon(v);
  //     centers.push(c);
  //   });
  //   setPolygonCenters(centers);
  //   console.log('centers', centers);
  // };

  // let getCenterPolygon = coordinates => {
  //   let x = coordinates.map(c => c.latitude);
  //   let y = coordinates.map(c => c.longitude);

  //   let minX = Math.min.apply(null, x);
  //   let maxX = Math.max.apply(null, x);

  //   let minY = Math.min.apply(null, y);
  //   let maxY = Math.max.apply(null, y);

  //   return {
  //     latitude: (minX + maxX) / 2,
  //     longitude: (minY + maxY) / 2,
  //   };
  // };

  const onPressPolygon = (polygons, index = 0) => {
    console.log('onPressPolygononPressPolygon', polygons, index);
    setClickedPolygon(index);
    mapRef?.current?.fitToCoordinates(polygons, {
      edgePadding: {top: 2, right: 2, bottom: 2, left: 2},
      animated: true,
    });
  };

    const onRegionChangeComplete = param => {
    console.log('onRegionChangeComplete0', param);
    if (parseInt(param?.latitudeDelta.toFixed(2)) < 0.03) {
      //50//1
      if (param?.latitudeDelta.toFixed(5) !== regionChanged?.latitudeDelta) {
        setregionChanged({
          ...param,
          latitudeDelta: param?.latitudeDelta.toFixed(5),
          longitudeDelta: param?.longitudeDelta.toFixed(5),
        });
        let toFixedLat = param?.latitudeDelta;
        console.log('onRegionChangeComplete1', param);
        if (toFixedLat > 0.005) {
          setMarkerType(1);
        } else if (toFixedLat < 0.005) {
          setMarkerType(2);
        }
        //region change 0=nothing show
        //region change 1=dot show
        //region change 2=marker show
        settrackViewChanges(true);
        setTimeout(() => {
          settrackViewChanges(false);
        }, 100);
      }
    }
  };

  const setMapTypeFunc = () => {
    setmapType(v =>
      v === STANDARD_MAP_MODE ? SATELLITE_MAP_MODE : STANDARD_MAP_MODE,
    );
  };

  return (
    <View style={styles.container}>
      <TouchableOpacity
        style={styles.mapType(mapType)}
        onPress={setMapTypeFunc}>
        <CustomIcon
          color={
            mapType === STANDARD_MAP_MODE ? COLOR_GRAY_70 : COLOR_SECONDARY
          }
          name={ICONOGRAPHY.FLOORS}
          size={20}
        />
      </TouchableOpacity>

      {markerType ? null : (
        <View
          style={{
            ...styles.map,
            ...styles.loader,
          }}>
          <ActivityIndicator color={COLOR_PRIMARY} size="large" />
        </View>
      )}

      <MapView
        provider={MapView.PROVIDER_GOOGLE}
        ref={mapRef}
        style={styles.map}
        onMapReady={onMapReady}
        initialRegion={initialRegion}
        mapType={mapType}
        onRegionChangeComplete={onRegionChangeComplete}>
        {!markerType
          ? null
          : coordinatesData.map((item, index) => (
              <View key={index}>
                <Polygon
                  coordinates={item?.polygons}
                  strokeColor={
                    index !== clickedPolygon ? COLOR_PLOT_BORDER : COLOR_BLACK
                  } // fallback for when `strokeColors` is not supported by the map-provider
                  fillColor={item?.color[index !== clickedPolygon ? 0 : 1]}
                  strokeWidth={index !== clickedPolygon ? 0.3 : 2}
                  tappable
                  geodesic
                  onPress={() => onPressPolygon(item?.polygons, index)}
                />
                <Marker
                  key={
                    isIOS() ? index : `${index}${trackViewChanges}`
                    // isIOS() ? index : `${index}${trackViewChanges}${Date.now()}`
                  }
                  opacity={markerType ? 1 : 0}
                  anchor={{x: 0.5, y: 0.5}}
                  centerOffset={{x: 0.5, y: 0.5}}
                  onPress={() => onPressPolygon(item?.polygons, index)}
                  coordinate={item?.center}
                  tracksViewChanges={isIOS() ? trackViewChanges : false}>
                  {!markerType ? (
                    <View />
                  ) : markerType === 1 ? (
                    <View style={styles.dot} />
                  ) : (
                    <Text style={styles.markerText}>1001</Text>
                  )}
                </Marker>
              </View>
            ))}
      </MapView>
      {clickedPolygon === null || clickedPolygon === undefined ? null : (
        <PopupBottom
          navigation={navigation}
          clickedPolygon={clickedPolygon}
          onPressCloseButton={() => setClickedPolygon(null)}
        />
      )}
    </View>
  );
}
const styles = StyleSheet.create({
  container: {
    ...StyleSheet.absoluteFillObject,
    flex: 1,
    justifyContent: 'flex-end',
    alignItems: 'center',
  },
  back: {
    height: 20,
    width: 20,
    backgroundColor: 'red',
    position: 'absolute',
    zIndex: 1,
    left: 20,
    top: 50,
  },
  map: {
    ...StyleSheet.absoluteFillObject,
  },
  markerText: {color: '#000', fontSize: 12},
  mapType: mapType => ({
    position: 'absolute',
    zIndex: 1,
    top: 160,
    left: 16,
    height: 30,
    width: 30,
    borderRadius: 4,
    backgroundColor: COLOR_WHITE,
    borderWidth: 1,
    borderColor: mapType === STANDARD_MAP_MODE ? COLOR_WHITE : COLOR_SECONDARY,
    justifyContent: 'center',
    alignItems: 'center',
    ...shadow.shadow,
  }),
  loader: {
    justifyContent: 'center',
    alignItems: 'center',
    zIndex: 100,
  },
  dot: {
    height: 10,
    width: 10,
    borderRadius: 10,
    backgroundColor: COLOR_WHITE,
  },
});

Enter fullscreen mode Exit fullscreen mode

Top comments (0)