diff --git a/android/src/main/java/com/rnmapbox/rnmbx/components/camera/CameraStop.kt b/android/src/main/java/com/rnmapbox/rnmbx/components/camera/CameraStop.kt index bea5d40e2..868a838dd 100644 --- a/android/src/main/java/com/rnmapbox/rnmbx/components/camera/CameraStop.kt +++ b/android/src/main/java/com/rnmapbox/rnmbx/components/camera/CameraStop.kt @@ -2,17 +2,19 @@ package com.rnmapbox.rnmbx.components.camera import android.animation.Animator import android.content.Context -import com.rnmapbox.rnmbx.utils.GeoJSONUtils.toPointGeometry -import com.rnmapbox.rnmbx.utils.GeoJSONUtils.toLatLng -import com.rnmapbox.rnmbx.utils.GeoJSONUtils.toLatLngBounds -import com.rnmapbox.rnmbx.utils.LatLngBounds -import com.rnmapbox.rnmbx.components.mapview.RNMBXMapView -import com.mapbox.maps.CameraOptions import com.facebook.react.bridge.ReadableMap import com.mapbox.geojson.FeatureCollection +import com.mapbox.geojson.Point +import com.mapbox.maps.CameraOptions import com.mapbox.maps.EdgeInsets +import com.mapbox.maps.ScreenCoordinate import com.rnmapbox.rnmbx.components.camera.constants.CameraMode +import com.rnmapbox.rnmbx.components.mapview.RNMBXMapView +import com.rnmapbox.rnmbx.utils.GeoJSONUtils.toLatLng +import com.rnmapbox.rnmbx.utils.GeoJSONUtils.toLatLngBounds +import com.rnmapbox.rnmbx.utils.GeoJSONUtils.toPointGeometry import com.rnmapbox.rnmbx.utils.LatLng +import com.rnmapbox.rnmbx.utils.LatLngBounds class CameraStop { private var mBearing: Double? = null @@ -124,14 +126,22 @@ class CameraStop { if (mLatLng != null) { builder.center(mLatLng!!.point) } else if (mBounds != null) { + val coordinates = listOf( + mBounds!!.toBounds().northeast, + mBounds!!.toBounds().southwest + ) val tilt = if (mTilt != null) mTilt!! else currentCamera.pitch val bearing = if (mBearing != null) mBearing!! else currentCamera.bearing - val boundsCamera = map.cameraForCoordinateBounds( - mBounds!!.toBounds(), + val boundsCamera = map.cameraForCoordinates( + coordinates, + CameraOptions.Builder() + .pitch(tilt) + .bearing(bearing) + .build(), cameraPaddingEdgeInsets, - bearing, - tilt + null, + ScreenCoordinate(0.0, 0.0) ) builder.center(boundsCamera.center) builder.anchor(boundsCamera.anchor) diff --git a/android/src/main/java/com/rnmapbox/rnmbx/components/camera/CameraUpdateItem.kt b/android/src/main/java/com/rnmapbox/rnmbx/components/camera/CameraUpdateItem.kt index 6b18caa34..faa212637 100644 --- a/android/src/main/java/com/rnmapbox/rnmbx/components/camera/CameraUpdateItem.kt +++ b/android/src/main/java/com/rnmapbox/rnmbx/components/camera/CameraUpdateItem.kt @@ -76,10 +76,11 @@ class CameraUpdateItem( } } val map = mMap.get() - if (map == null) { + if (map == null || mCameraUpdate.center == null) { isCameraActionCancelled = true return } + val animationOptions = MapAnimationOptions.Builder(); // animateCamera / easeCamera only allows positive duration diff --git a/ios/RNMBX/RNMBXCamera.swift b/ios/RNMBX/RNMBXCamera.swift index 261245ca3..4136818c6 100644 --- a/ios/RNMBX/RNMBXCamera.swift +++ b/ios/RNMBX/RNMBXCamera.swift @@ -18,7 +18,7 @@ public enum RemovalReason { public protocol RNMBXMapComponent: AnyObject { func addToMap(_ map: RNMBXMapView, style: Style) func removeFromMap(_ map: RNMBXMapView, reason: RemovalReason) -> Bool - + func waitForStyleLoad() -> Bool } @@ -34,22 +34,24 @@ struct CameraUpdateItem { var camera: CameraOptions var mode: CameraMode var duration: TimeInterval? - + func execute(map: RNMBXMapView, cameraAnimator: inout BasicCameraAnimator?) { logged("CameraUpdateItem.execute") { if let center = camera.center { try center.validate() } - switch mode { - case .flight: - map.mapView.camera.fly(to: camera, duration: duration) - case .ease: - map.mapView.camera.ease(to: camera, duration: duration ?? 0, curve: .easeInOut, completion: nil) - case .linear: - map.mapView.camera.ease(to: camera, duration: duration ?? 0, curve: .linear, completion: nil) - default: - map.mapboxMap.setCamera(to: camera) + map.withMapView { mapView in + switch mode { + case .flight: + mapView.camera.fly(to: camera, duration: duration) + case .ease: + mapView.camera.ease(to: camera, duration: duration ?? 0, curve: .easeInOut, completion: nil) + case .linear: + mapView.camera.ease(to: camera, duration: duration ?? 0, curve: .linear, completion: nil) + default: + mapView.mapboxMap.setCamera(to: camera) + } } } } @@ -57,23 +59,23 @@ struct CameraUpdateItem { class CameraUpdateQueue { var queue: [CameraUpdateItem] = []; - + func dequeue() -> CameraUpdateItem? { guard !queue.isEmpty else { return nil } return queue.removeFirst() } - + func enqueue(stop: CameraUpdateItem) { queue.append(stop) } - + func execute(map: RNMBXMapView, cameraAnimator: inout BasicCameraAnimator?) { guard let stop = dequeue() else { return } - + stop.execute(map: map, cameraAnimator: &cameraAnimator) } } @@ -81,14 +83,16 @@ class CameraUpdateQueue { open class RNMBXMapComponentBase : UIView, RNMBXMapComponent { private weak var _map: RNMBXMapView! = nil private var _mapCallbacks: [(RNMBXMapView) -> Void] = [] - + weak var map : RNMBXMapView? { return _map; } func withMapView(_ callback: @escaping (_ mapView: MapView) -> Void) { - withRNMBXMapView { mapView in - callback(mapView.mapView) + withRNMBXMapView { map in + map.withMapView { mapView in + callback(mapView) + } } } @@ -99,11 +103,11 @@ open class RNMBXMapComponentBase : UIView, RNMBXMapComponent { _mapCallbacks.append(callback) } } - + public func waitForStyleLoad() -> Bool { return false } - + public func addToMap(_ map: RNMBXMapView, style: Style) { _mapCallbacks.forEach { callback in callback(map) @@ -111,7 +115,7 @@ open class RNMBXMapComponentBase : UIView, RNMBXMapComponent { _mapCallbacks = [] _map = map } - + public func removeFromMap(_ map: RNMBXMapView, reason: RemovalReason) -> Bool { _mapCallbacks = [] _map = nil @@ -123,67 +127,67 @@ open class RNMBXMapComponentBase : UIView, RNMBXMapComponent { open class RNMBXCamera : RNMBXMapComponentBase { var cameraAnimator: BasicCameraAnimator? let cameraUpdateQueue = CameraUpdateQueue() - + // MARK: React properties - + @objc public var animationDuration: NSNumber? - + @objc public var animationMode: NSString? - + @objc public var defaultStop: [String: Any]? - + @objc public var followUserLocation : Bool = false { didSet { _updateCameraFromTrackingMode() } } - + @objc public var followUserMode: String? { didSet { _updateCameraFromTrackingMode() } } - + @objc public var followZoomLevel: NSNumber? { didSet { _updateCameraFromTrackingMode() } } - + @objc public var followPitch: NSNumber? { didSet { _updateCameraFromTrackingMode() } } - + @objc public var followHeading: NSNumber? { didSet { _updateCameraFromTrackingMode() } } - + @objc public var followPadding: NSDictionary? { didSet { _updateCameraFromTrackingMode() } } - + @objc public var maxZoomLevel: NSNumber? { didSet { _updateMaxBounds() } } - + @objc public var minZoomLevel: NSNumber? { didSet { _updateMaxBounds() } } - + @objc public var onUserTrackingModeChange: RCTBubblingEventBlock? = nil - + @objc public var stop: [String: Any]? { didSet { _updateCamera() } } - + @objc public var maxBounds: String? { didSet { if let maxBounds = maxBounds { @@ -197,18 +201,18 @@ open class RNMBXCamera : RNMBXMapComponentBase { } } var maxBoundsFeature : FeatureCollection? = nil - + // MARK: Update methods func _updateCameraFromJavascript() { guard !followUserLocation else { return } - + guard let stop = stop else { return } - + /* V10 TODO if let map = map, map.userTrackingMode != .none { @@ -232,22 +236,22 @@ open class RNMBXCamera : RNMBXMapComponentBase { cameraUpdateQueue.execute(map: map, cameraAnimator: &cameraAnimator) } } - + func _disableUserTracking(_ map: MapView) { map.viewport.idle() } - + @objc public func updateCameraStop(_ stop: [String: Any]) { self.stop = stop } - + func _toCoordinateBounds(_ bounds: FeatureCollection) throws -> CoordinateBounds { guard bounds.features.count == 2 else { throw RNMBXError.paramError("Expected two Points in FeatureColletion") } let swFeature = bounds.features[0] let neFeature = bounds.features[1] - + guard case let .point(sw) = swFeature.geometry, case let .point(ne) = neFeature.geometry else { throw RNMBXError.paramError("Expected two Points in FeatureColletion") @@ -255,11 +259,11 @@ open class RNMBXCamera : RNMBXMapComponentBase { return CoordinateBounds(southwest: sw.coordinates, northeast: ne.coordinates) } - + func _updateMaxBounds() { withMapView { map in var options = CameraBoundsOptions() - + if let maxBounds = self.maxBoundsFeature { logged("RNMBXCamera._updateMaxBounds._toCoordinateBounds") { options.bounds = try self._toCoordinateBounds(maxBounds) @@ -269,7 +273,7 @@ open class RNMBXCamera : RNMBXMapComponentBase { } options.minZoom = self.minZoomLevel?.CGFloat options.maxZoom = self.maxZoomLevel?.CGFloat - + logged("RNMBXCamera._updateMaxBounds") { try map.mapboxMap.setCameraBounds(with: options) } @@ -311,23 +315,23 @@ open class RNMBXCamera : RNMBXMapComponentBase { followOptions.bearing = nil trackingModeChanged = true } - + if let onUserTrackingModeChange = self.onUserTrackingModeChange { if (trackingModeChanged) { let event = RNMBXEvent(type: .onUserTrackingModeChange, payload: ["followUserMode": self.followUserMode ?? "normal", "followUserLocation": self.followUserLocation]) onUserTrackingModeChange(event.toJSON()) } } - + var _camera = CameraOptions() - + if let zoom = self.followZoomLevel as? CGFloat { if (zoom >= 0.0) { _camera.zoom = zoom followOptions.zoom = zoom } } - + if let followPitch = self.followPitch as? CGFloat { if (followPitch >= 0.0) { _camera.pitch = followPitch @@ -341,7 +345,7 @@ open class RNMBXCamera : RNMBXMapComponentBase { } else { followOptions.pitch = nil } - + if let followHeading = self.followHeading as? CGFloat { if (followHeading >= 0.0) { _camera.bearing = followHeading @@ -351,7 +355,7 @@ open class RNMBXCamera : RNMBXMapComponentBase { _camera.bearing = stopHeading } } - + if let padding = self.followPadding { let edgeInsets = UIEdgeInsets( top: padding["paddingTop"] as? Double ?? 0, @@ -361,20 +365,20 @@ open class RNMBXCamera : RNMBXMapComponentBase { ) followOptions.padding = edgeInsets } - + let followState = map.viewport.makeFollowPuckViewportState(options: followOptions) - + map.viewport.transition(to: followState) map.viewport.addStatusObserver(self) map.mapboxMap.setCamera(to: _camera) } } - + 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) @@ -384,28 +388,28 @@ open class RNMBXCamera : RNMBXMapComponentBase { if let p = stop["pitch"] as? Double { pitch = CGFloat(p) } - + var heading: CLLocationDirection? if let h = stop["heading"] as? Double { heading = CLLocationDirection(h) } - + var 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 @@ -413,7 +417,7 @@ open class RNMBXCamera : RNMBXMapComponentBase { Logger.log(level: .error, message: "RNMBXCamera.toUpdateItem: Unexpected geometry: \(String(describing: centerFeature?.geometry))") return nil } - + camera = CameraOptions( center: center, padding: padding, @@ -426,7 +430,7 @@ open class RNMBXCamera : RNMBXMapComponentBase { 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): @@ -435,7 +439,7 @@ open class RNMBXCamera : RNMBXMapComponentBase { 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): @@ -444,20 +448,36 @@ open class RNMBXCamera : RNMBXMapComponentBase { Logger.log(level: .error, message: "RNMBXCamera.toUpdateItem: Unexpected geometry: \(String(describing: features?.last?.geometry))") return nil } - + withMapView { map in #if RNMBX_11 - let bounds = [sw, ne] + do { + let bounds: [CLLocationCoordinate2D] = [sw, ne] + camera = try map.mapboxMap.camera( + for: bounds, + camera: .init(cameraState: .init( + center: .init(), + padding: .zero, + zoom: zoom ?? 0, + bearing: heading ?? map.mapboxMap.cameraState.bearing, + pitch: pitch ?? map.mapboxMap.cameraState.pitch + )), + coordinatesPadding: padding, + maxZoom: nil, + offset: nil + ) + } catch { + Logger.log(level: .error, message: "RNMBXCamera.toUpdateItem: Failed to build camera configuration: \(error)") + } #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 ) + #endif } } else { camera = CameraOptions( @@ -478,7 +498,7 @@ open class RNMBXCamera : RNMBXMapComponentBase { 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 @@ -490,7 +510,7 @@ open class RNMBXCamera : RNMBXMapComponentBase { duration: duration ) } - + func _updateCamera() { if let _ = map { if followUserLocation { @@ -500,36 +520,37 @@ open class RNMBXCamera : RNMBXMapComponentBase { } } } - + func _setInitialCamera() { guard let stop = self.defaultStop, let map = map else { return } - + if var updateItem = toUpdateItem(stop: stop) { updateItem.mode = .none updateItem.duration = 0 updateItem.execute(map: map, cameraAnimator: &cameraAnimator) } } - + func initialLayout() { _setInitialCamera() _updateCamera() } - + public override func addToMap(_ map: RNMBXMapView, style: Style) { super.addToMap(map, style: style) map.reactCamera = self } - + public override func removeFromMap(_ map: RNMBXMapView, reason: RemovalReason) -> Bool { if (reason == .StyleChange) { return false } - map.mapView.viewport.removeStatusObserver(self) - return super.removeFromMap(map, reason:reason) + map._mapView.viewport.removeStatusObserver(self) + + return super.removeFromMap(map, reason: reason) } } @@ -568,12 +589,12 @@ extension RNMBXCamera : ViewportStatusObserver { return "compass" case .course: return "course" - case .some(let bearing): + case .some(_): return "constant" case .none: return "normal" } - } else if let state = state as? OverviewViewportState { + } else if let _ = state as? OverviewViewportState { return "overview" } else { return "custom"