maps icon indicating copy to clipboard operation
maps copied to clipboard

Zoom level not applied correctly when switching from setting camera with `centerCoordinate` to `bounds`

Open RyszardRzepa opened this issue 1 year ago • 6 comments

Mapbox Implementation

Mapbox

Mapbox Version

11.3.0

React Native Version

0.72.4

Platform

iOS, Android

@rnmapbox/maps version

10.1.19

Standalone component to reproduce

import React, { useRef } from "react";
import { Button, StyleSheet, TouchableOpacity, View, Text } from "react-native";
import Mapbox from "@rnmapbox/maps";
import { ShapeSource, CircleLayer, Camera } from "@rnmapbox/maps";


const App = () => {
const realMapRef = useRef<Mapbox.MapView>(null);
const realCameraRef = useRef<Mapbox.Camera>(null);

const currentZoom = 16;
const sheetHeight = 374;

const padding = {
      paddingTop: 60,
      paddingLeft: 24,
      paddingRight: 24,
      paddingBottom: sheetHeight ? sheetHeight - 30 : 30,
    };

// this function is called in the parent component. Left for the reference
const zoomToBounds = (bounds) => {
        realCameraRef.current.camera?.setCamera({
          bounds: {
            ne: [ 10.6701055, 59.9111846 ],
            sw:  [ 10.6270699, 59.9008117 ],
          },
          padding: padding,
          heading: 0.0, // reset to north
          animationMode: 'easeTo',
        });
      };
// this function is called in the parent component. Left for the reference
      const zoomToPosition = (pos) => {
        realCameraRef.current.setCamera({
          centerCoordinate: [ 10.6701055, 59.9008117 ],
          padding: padding,
          zoomLevel: currentZoom < 17 ? 17 : undefined,
          heading: 0.0,
          animationMode: 'easeTo',
        });
      };

  return (
  <Mapbox.MapView
  ref={realMapRef}
          styleURL="mapbox://styles/123"
          projection="globe"
          rotateEnabled={true}
          scaleBarEnabled={false}
          logoEnabled={false}
        >
           <Camera ref={realCameraRef} animationDuration={0} />
       {/*Rest of the code*/}
        </Mapbox.MapView>
  );
}

export default App;

Observed behavior and steps to reproduce

I see warning in Xcode when the bug happens: [Warning, maps-core]: {}[General]: Unable to calculate camera for given bounds/geometry, padding is greater than map's width or height.

We set the camera position and zoom level by using two methods, depends on what the user click. If user click a marker in the map, we call zoomToPosition function where we set centerCoordinate with the marker position and the padding.

If user click on the fence we call zoomToBounds function where we use bounds param to set the camera in the center with the correct zoom.

The problem is that when we call zoomToPosition and the camera zoom in to the marker and later we call zoomToBounds the camera centers correctly but the zoom level is not applied to show all the markers within the bounds.

https://github.com/rnmapbox/maps/assets/13038459/ebb1df5a-201a-4501-b248-9e596c6d1563


const padding = {
  paddingTop: topSafeArea + 10,
  paddingLeft: 24,
  paddingRight: 24,
  paddingBottom: sheetHeight ? sheetHeight - 30 : 30,
};
      
 const zoomToPosition = (pos) => {
        realCameraRef.current.setCamera({
          centerCoordinate: [ 10.6701055, 59.9008117 ],
          padding: padding,
          zoomLevel: currentZoom < 17 ? 17 : undefined,
          heading: 0.0,
          animationMode: 'easeTo',
        });
      };
      
const zoomToBounds = (bounds) => {
        realCameraRef.current.camera?.setCamera({
          bounds: {
            ne: [ 10.6701055, 59.9111846 ],
            sw:  [ 10.6270699, 59.9008117 ],
          },
          padding: padding,
          heading: 0.0, // reset to north
          animationMode: 'easeTo',
        });
      };

Screen recording. Here we can see that clicking on the marker zoom in the camera, when we navigate back the zoomToBounds is called and it should zoom out the camera to show all the markers, but instead the zoom level don't change. https://github.com/rnmapbox/maps/assets/13038459/bc2d5116-047d-49f7-b33b-d01ca54c2ae0

Expected behavior

Zoom level should be applied correctly to show all the markers within the bounds when calling first

  camera?.setCamera({
    centerCoordinate: pos,
    padding: padding,
    zoomLevel: currentZoom < 17 ? 17 : undefined,
    heading: 0.0,
    animationMode: 'easeTo',
  });

and calling after that setCamera with bounds

realCameraRef.current.camera?.setCamera({
          bounds: {
            ne: [ 10.6701055, 59.9111846 ],
            sw:  [ 10.6270699, 59.9008117 ],
          },
          padding: padding,
          heading: 0.0, // reset to north
          animationMode: 'easeTo',
        });
      };

Zoom level is not applied correctly when we use

Notes / preliminary analysis

We render the bottom sheet on the map. We use the bottom sheet height to calculate bottom padding, to make sure we center the camera in the visible part of the map.

It appears that this zoom issue is gone when we set the padding to the static value example: 30, but this will not center the camera in the visible part of the map.

Additional links and references

No response

Related issue #3354

RyszardRzepa avatar May 13 '24 12:05 RyszardRzepa

In RNMBXCamera.siwft, If I reset the camera state before applying new bounds the padding error goes away and the zoom is applied as expected.

Screen recording showing expected behavior: https://github.com/rnmapbox/maps/assets/13038459/47d7ce59-bc9c-430e-80d5-8e81f6e9ce22

Example code that fix the bug. I am not sure if this is the right way of doing it.

func resetCameraState(map: MapView) {
       // Reset camera options to default values
       let defaultCamera = CameraOptions(
           center: nil,
           padding: .zero,
           anchor: nil,
           zoom: nil,
           bearing: nil,
           pitch: nil
       )
       
       // Apply the default camera options to reset the camera state
       map.mapboxMap.setCamera(to: defaultCamera)
   }
   
   private func toUpdateItem(stop: [String: Any]) -> CameraUpdateItem? {
       if stop.isEmpty {
           return nil
       }

       var zoom: CGFloat?
       if let z = stop["zoom"] as? Double {
           zoom = CGFloat(z)
       }

       var pitch: CGFloat?
       if let p = stop["pitch"] as? Double {
           pitch = CGFloat(p)
       }

       var heading: CLLocationDirection?
       if let h = stop["heading"] as? Double {
           heading = CLLocationDirection(h)
       }

       let padding: UIEdgeInsets = UIEdgeInsets(
           top: stop["paddingTop"] as? Double ?? 0,
           left: stop["paddingLeft"] as? Double ?? 0,
           bottom: stop["paddingBottom"] as? Double ?? 0,
           right: stop["paddingRight"] as? Double ?? 0
       )

       var camera: CameraOptions?

       if let feature = stop["centerCoordinate"] as? String {
           let centerFeature: Turf.Feature? = logged("RNMBXCamera.toUpdateItem.decode.cc") {
               try JSONDecoder().decode(Turf.Feature.self, from: feature.data(using: .utf8)!)
           }

           var center: LocationCoordinate2D?

           switch centerFeature?.geometry {
           case .point(let centerPoint):
               center = centerPoint.coordinates
           default:
               Logger.log(level: .error, message: "RNMBXCamera.toUpdateItem: Unexpected geometry: \(String(describing: centerFeature?.geometry))")
               return nil
           }

           camera = CameraOptions(
               center: center,
               padding: padding,
               anchor: nil,
               zoom: zoom,
               bearing: heading,
               pitch: pitch
           )
       } else if let feature = stop["bounds"] as? String {
           let collection: Turf.FeatureCollection? = logged("RNMBXCamera.toUpdateItem.decode.bound") {
               try JSONDecoder().decode(Turf.FeatureCollection.self, from: feature.data(using: .utf8)!)
           }
           let features = collection?.features

           let ne: CLLocationCoordinate2D
           switch features?.first?.geometry {
           case .point(let point):
               ne = point.coordinates
           default:
               Logger.log(level: .error, message: "RNMBXCamera.toUpdateItem: Unexpected geometry: \(String(describing: features?.first?.geometry))")
               return nil
           }

           let sw: CLLocationCoordinate2D
           switch features?.last?.geometry {
           case .point(let point):
               sw = point.coordinates
           default:
               Logger.log(level: .error, message: "RNMBXCamera.toUpdateItem: Unexpected geometry: \(String(describing: features?.last?.geometry))")
               return nil
           }

           withMapView { map in
               // Reset the camera state before applying new bounds
               self.resetCameraState(map: map)

               #if RNMBX_11
               let bounds = [sw, ne]
               #else
               let bounds = CoordinateBounds(southwest: sw, northeast: ne)
               #endif

               camera = map.mapboxMap.camera(
                   for: bounds,
                   padding: padding,
                   bearing: heading ?? map.mapboxMap.cameraState.bearing,
                   pitch: pitch ?? map.mapboxMap.cameraState.pitch
               )
           }
       } else {
           withMapView { map in
               // Reset the camera state before applying new center coordinate
               self.resetCameraState(map: map)

               camera = CameraOptions(
                   center: nil,
                   padding: padding,
                   anchor: nil,
                   zoom: zoom,
                   bearing: heading,
                   pitch: pitch
               )
           }
       }

       guard let camera = camera else {
           return nil
       }

       var duration: TimeInterval?
       if let d = stop["duration"] as? Double {
           duration = toSeconds(d)
       }

       var mode: CameraMode = .flight
       if let m = stop["mode"] as? String, let m = CameraMode(rawValue: m) {
           mode = m
       }

       return CameraUpdateItem(
           camera: camera,
           mode: mode,
           duration: duration
       )
   }

RyszardRzepa avatar May 14 '24 14:05 RyszardRzepa

@mfazekas is it relates to https://github.com/mapbox/mapbox-maps-ios/issues/2170 ? Can you suggest a hot fix? Thanks @RyszardRzepa for a solution, but camera jumps unpleasantly

Crysp avatar Sep 22 '24 08:09 Crysp

@RyszardRzepa fyi after some research I found that replacing deprecated method to the new API is fixed this bug

diff --git a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXCamera.swift b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXCamera.swift
index 261245c..d70b480 100644
--- a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXCamera.swift
+++ b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXCamera.swift
@@ -452,12 +452,21 @@ open class RNMBXCamera : RNMBXMapComponentBase {
         let bounds = CoordinateBounds(southwest: sw, northeast: ne)
         #endif

-        camera = map.mapboxMap.camera(
-          for: bounds,
-          padding: padding,
-          bearing: heading ?? map.mapboxMap.cameraState.bearing,
-          pitch: pitch ?? map.mapboxMap.cameraState.pitch
-        )
+        do {
+          camera = try map.mapboxMap.camera(
+            for: bounds,
+            camera: CameraOptions(
+              padding: padding,
+              bearing: heading ?? map.mapboxMap.cameraState.bearing,
+              pitch: pitch ?? map.mapboxMap.cameraState.pitch
+            ),
+            coordinatesPadding: nil,
+            maxZoom: nil,
+            offset: nil
+          )
+        } catch let error {
+          Logger.log(level: .error, message: "RNMBXCamera.toUpdateItem: MapError: \(error.localizedDescription)")
+        }
       }
     } else {
       camera = CameraOptions(

Crysp avatar Nov 26 '24 12:11 Crysp

Any news here?

Thank you!

swey avatar Feb 23 '25 20:02 swey

Same issue

celian-riboulet avatar Mar 09 '25 10:03 celian-riboulet