From cdbafe633d9fcba652c6b4dae1e016ed7e559f0f Mon Sep 17 00:00:00 2001 From: Naftali Beder Date: Mon, 18 Apr 2022 16:25:07 -0500 Subject: [PATCH 01/84] Clean up warnings --- ios/RCTMGL-v10/RCTMGLMapView.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ios/RCTMGL-v10/RCTMGLMapView.swift b/ios/RCTMGL-v10/RCTMGLMapView.swift index 9d9810590..e700e072c 100644 --- a/ios/RCTMGL-v10/RCTMGLMapView.swift +++ b/ios/RCTMGL-v10/RCTMGLMapView.swift @@ -143,7 +143,7 @@ open class RCTMGLMapView : MapView { @objc func setReactStyleURL(_ value: String?) { if let value = value { - if let url = URL(string: value) { + if let _ = URL(string: value) { mapView.mapboxMap.loadStyleURI(StyleURI(rawValue: value)!) } else { if RCTJSONParse(value, nil) != nil { @@ -168,6 +168,7 @@ open class RCTMGLMapView : MapView { self.reactOnMapChange = value self.mapView.mapboxMap.onEvery(.cameraChanged, handler: { cameraEvent in + print("Hi????") let event = RCTMGLEvent(type:.regionDidChange, payload: self._makeRegionPayload()); self.fireEvent(event: event, callback: self.reactOnMapChange!) }) @@ -425,7 +426,7 @@ extension RCTMGLMapView { } let features = hitFeatures.map { try! dictionaryFrom($0.feature) } let location = self.mapboxMap.coordinate(for: tapPoint) - let event = try! RCTMGLEvent( + let event = RCTMGLEvent( type: (source is RCTMGLVectorSource) ? .vectorSourceLayerPress : .shapeSourceLayerPress, payload: [ "features": features, From b0b07934c23cd3579040ff44ee608853823c9241 Mon Sep 17 00:00:00 2001 From: Naftali Beder Date: Mon, 18 Apr 2022 16:25:44 -0500 Subject: [PATCH 02/84] Remove log --- ios/RCTMGL-v10/RCTMGLMapView.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/ios/RCTMGL-v10/RCTMGLMapView.swift b/ios/RCTMGL-v10/RCTMGLMapView.swift index e700e072c..a22c68699 100644 --- a/ios/RCTMGL-v10/RCTMGLMapView.swift +++ b/ios/RCTMGL-v10/RCTMGLMapView.swift @@ -168,7 +168,6 @@ open class RCTMGLMapView : MapView { self.reactOnMapChange = value self.mapView.mapboxMap.onEvery(.cameraChanged, handler: { cameraEvent in - print("Hi????") let event = RCTMGLEvent(type:.regionDidChange, payload: self._makeRegionPayload()); self.fireEvent(event: event, callback: self.reactOnMapChange!) }) From 6fcaa7e49db200871a8e4140684f1290fc4ea920 Mon Sep 17 00:00:00 2001 From: Naftali Beder Date: Tue, 19 Apr 2022 10:58:19 -0500 Subject: [PATCH 03/84] Listen for and return map drag information in callbacks --- ios/RCTMGL-v10/MGLModule.swift | 1 + ios/RCTMGL-v10/RCTMGLEvent.swift | 1 + ios/RCTMGL-v10/RCTMGLMapView.swift | 52 ++++++++++++++++++++---------- 3 files changed, 37 insertions(+), 17 deletions(-) diff --git a/ios/RCTMGL-v10/MGLModule.swift b/ios/RCTMGL-v10/MGLModule.swift index 475aa58aa..c62e55914 100644 --- a/ios/RCTMGL-v10/MGLModule.swift +++ b/ios/RCTMGL-v10/MGLModule.swift @@ -42,6 +42,7 @@ class MGLModule : NSObject { ], "EventTypes": [ + "RegionIsChanging" : RCTMGLEvent.EventType.regionIsChanging.rawValue, "RegionDidChange" : RCTMGLEvent.EventType.regionDidChange.rawValue, "DidFinishLoadingMap": RCTMGLEvent.EventType.didFinishLoadingMap.rawValue ], diff --git a/ios/RCTMGL-v10/RCTMGLEvent.swift b/ios/RCTMGL-v10/RCTMGLEvent.swift index 5f3bb6281..1638a4bb4 100644 --- a/ios/RCTMGL-v10/RCTMGLEvent.swift +++ b/ios/RCTMGL-v10/RCTMGLEvent.swift @@ -20,6 +20,7 @@ class RCTMGLEvent : NSObject, RCTMGLEventProtocol { enum EventType : String { case tap = "press" + case regionIsChanging = "regionischanging" case regionDidChange = "regiondidchange" case imageMissing = "imagesmissingimage" case didFinishLoadingMap = "didfinishloadingmap" diff --git a/ios/RCTMGL-v10/RCTMGLMapView.swift b/ios/RCTMGL-v10/RCTMGLMapView.swift index a22c68699..fd4838435 100644 --- a/ios/RCTMGL-v10/RCTMGLMapView.swift +++ b/ios/RCTMGL-v10/RCTMGLMapView.swift @@ -1,12 +1,6 @@ import MapboxMaps import Turf -private extension MapboxMaps.PointAnnotationManager { - // func doHandleTap(_ tap: UITapGestureRecognizer) { - // self.handleTap(tap) - // } -} - class PointAnnotationManager : AnnotationInteractionDelegate { weak var selected : RCTMGLPointAnnotation? = nil @@ -124,7 +118,9 @@ open class RCTMGLMapView : MapView { var onStyleLoadedComponents: [RCTMGLMapComponent]? = [] var _pendingInitialLayout = true - + var _isUserInteraction = false + var _isAnimatingFromUserInteraction = false + var layerWaiters : [String:[(String) -> Void]] = [:] lazy var pointAnnotationManager : PointAnnotationManager = { @@ -156,18 +152,19 @@ open class RCTMGLMapView : MapView { @objc func setReactOnPress(_ value: @escaping RCTBubblingEventBlock) { self.reactOnPress = value - /* - let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap)) - self.addGestureRecognizer(tapGesture) - */ - mapView.gestures.singleTapGestureRecognizer.removeTarget( pointAnnotationManager.manager, action: nil) - mapView.gestures.singleTapGestureRecognizer.addTarget(self, action: #selector(doHandleTap(_:))) + self.mapView.gestures.singleTapGestureRecognizer.removeTarget( pointAnnotationManager.manager, action: nil) + self.mapView.gestures.singleTapGestureRecognizer.addTarget(self, action: #selector(doHandleTap(_:))) } @objc func setReactOnMapChange(_ value: @escaping RCTBubblingEventBlock) { self.reactOnMapChange = value - + self.mapView.mapboxMap.onEvery(.cameraChanged, handler: { cameraEvent in + let event = RCTMGLEvent(type:.regionIsChanging, payload: self._makeRegionPayload()); + self.fireEvent(event: event, callback: self.reactOnMapChange!) + }) + + self.mapView.mapboxMap.onEvery(.mapIdle, handler: { cameraEvent in let event = RCTMGLEvent(type:.regionDidChange, payload: self._makeRegionPayload()); self.fireEvent(event: event, callback: self.reactOnMapChange!) }) @@ -219,7 +216,7 @@ open class RCTMGLMapView : MapView { return result } - func _makeRegionPayload() -> [String:Any] { + func _makeRegionPayload() -> [String: Any] { return toJSON( geometry: .point(Point(mapView.cameraState.center)), properties: [ @@ -227,7 +224,9 @@ open class RCTMGLMapView : MapView { "heading": Double(mapView.cameraState.bearing), "bearing": Double(mapView.cameraState.bearing), "pitch": Double(mapView.cameraState.pitch), - "visibleBounds": _toArray(bounds: mapView.mapboxMap.cameraBounds.bounds) + "visibleBounds": _toArray(bounds: mapView.mapboxMap.cameraBounds.bounds), + "isUserInteraction": _isUserInteraction, + "isAnimatingFromUserInteraction": _isAnimatingFromUserInteraction ] ) } @@ -262,6 +261,8 @@ open class RCTMGLMapView : MapView { let resourceOptions = ResourceOptions(accessToken: MGLModule.accessToken!) super.init(frame: frame, mapInitOptions: MapInitOptions(resourceOptions: resourceOptions)) + self.mapView.gestures.delegate = self + setupEvents() } @@ -273,6 +274,7 @@ open class RCTMGLMapView : MapView { Logger.log(level: .error, message: "MapLoad error \(event)") } }) + self.mapboxMap.onEvery(.styleImageMissing) { (event) in if let data = event.data as? [String:Any] { if let imageName = data["id"] as? String { @@ -360,7 +362,7 @@ open class RCTMGLMapView : MapView { // MARK: - Touch -extension RCTMGLMapView { +extension RCTMGLMapView: GestureManagerDelegate { func touchableSources() -> [RCTMGLSource] { return sources.filter { $0.isTouchable() } } @@ -457,6 +459,22 @@ extension RCTMGLMapView { } } } + + public func gestureManager(_ gestureManager: GestureManager, didBegin gestureType: GestureType) { + _isUserInteraction = true + } + + public func gestureManager(_ gestureManager: GestureManager, didEnd gestureType: GestureType, willAnimate: Bool) { + _isUserInteraction = false + if willAnimate { + _isAnimatingFromUserInteraction = true + } + } + + public func gestureManager(_ gestureManager: GestureManager, didEndAnimatingFor gestureType: GestureType) { + _isUserInteraction = false + _isAnimatingFromUserInteraction = false + } } // MARK: - queryTerrainElevation From 4e14afd6c302533ed7251066e8b6a399f105e431 Mon Sep 17 00:00:00 2001 From: Naftali Beder Date: Tue, 19 Apr 2022 11:01:58 -0500 Subject: [PATCH 04/84] Add example showing map gesture handling --- .../src/examples/V10/MapGestureHandlers.js | 53 +++++++++++++++++++ example/src/scenes/Home.js | 2 + 2 files changed, 55 insertions(+) create mode 100644 example/src/examples/V10/MapGestureHandlers.js diff --git a/example/src/examples/V10/MapGestureHandlers.js b/example/src/examples/V10/MapGestureHandlers.js new file mode 100644 index 000000000..bb8676705 --- /dev/null +++ b/example/src/examples/V10/MapGestureHandlers.js @@ -0,0 +1,53 @@ +import React, {useState} from 'react'; +import {SafeAreaView, View} from 'react-native'; +import {MapView, Camera, Logger} from '@rnmapbox/maps'; +import {Text, Divider} from 'react-native-elements'; + +import Page from '../common/Page'; + +Logger.setLogLevel('verbose'); + +const styles = { + map: { + flex: 1, + }, + info: { + flex: 0, + padding: 10, + }, + divider: { + marginVertical: 10, + }, +}; + +const MapGestureHandlers = props => { + const [region, setRegion] = useState({}); + + const properties = region?.properties; + + return ( + + setRegion(_region)} + onRegionIsChanging={_region => setRegion(_region)} + onRegionDidChange={_region => setRegion(_region)}> + + + + + + Interacting: + {properties?.isUserInteraction ? 'Yes' : 'No'} + + Animating from interaction: + + {properties?.isAnimatingFromUserInteraction ? 'Yes' : 'No'} + + + + + ); +}; + +export default MapGestureHandlers; diff --git a/example/src/scenes/Home.js b/example/src/scenes/Home.js index 2c659288a..9dcad55b3 100644 --- a/example/src/scenes/Home.js +++ b/example/src/scenes/Home.js @@ -69,6 +69,7 @@ import CacheManagement from '../examples/CacheManagement'; import SkyAndTerran from '../examples/V10/SkyAndTerran'; import QueryTerrainElevation from '../examples/V10/QueryTerrainElevation'; import CameraAnimation from '../examples/V10/CameraAnimation'; +import MapGestureHandlers from '../examples/V10/MapGestureHandlers'; const styles = StyleSheet.create({ exampleList: { @@ -121,6 +122,7 @@ const Examples = [ new ExampleItem('Sky and Terrain', SkyAndTerran), new ExampleItem('Query Terrain Elevation', QueryTerrainElevation), new ExampleItem('Camera Animation', CameraAnimation), + new ExampleItem('Map Gesture Handlers', MapGestureHandlers), ]), new ExampleGroup('Map', [ new ExampleItem('Show Map', ShowMap), From c47fbd6e70466112ec5da5fffac7eaab6dcdfa6a Mon Sep 17 00:00:00 2001 From: Naftali Beder Date: Tue, 19 Apr 2022 12:26:13 -0500 Subject: [PATCH 05/84] Remove unneeded enum string values --- ios/RCTMGL-v10/RCTMGLEvent.swift | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/ios/RCTMGL-v10/RCTMGLEvent.swift b/ios/RCTMGL-v10/RCTMGLEvent.swift index 1638a4bb4..4a4fd09eb 100644 --- a/ios/RCTMGL-v10/RCTMGLEvent.swift +++ b/ios/RCTMGL-v10/RCTMGLEvent.swift @@ -19,16 +19,17 @@ class RCTMGLEvent : NSObject, RCTMGLEventProtocol { } enum EventType : String { - case tap = "press" - case regionIsChanging = "regionischanging" - case regionDidChange = "regiondidchange" - case imageMissing = "imagesmissingimage" - case didFinishLoadingMap = "didfinishloadingmap" - case offlineProgress = "offlinestatus" - case offlineError = "offlineerror" - case offlineTileLimit = "offlinetileLimit" - case vectorSourceLayerPress = "vectorsourcelayerpress" - case shapeSourceLayerPress = "shapesourcelayerpress" + case tap + case regionWillChange + case regionIsChanging + case regionDidChange + case imageMissing + case didFinishLoadingMap + case offlineProgress + case offlineError + case offlineTileLimit + case vectorSourceLayerPress + case shapeSourceLayerPress } init(type: EventType, payload: [String:Any]?) { @@ -37,4 +38,3 @@ class RCTMGLEvent : NSObject, RCTMGLEventProtocol { } } - From de50625a70f95b39d9129865001fd25cc32264c1 Mon Sep 17 00:00:00 2001 From: Naftali Beder Date: Tue, 19 Apr 2022 13:41:21 -0500 Subject: [PATCH 06/84] Add new region callbacks to mirror v10 native API --- docs/MapView.md | 8 +++--- docs/docs.json | 40 +++++++++++++++++++++++++++--- index.d.ts | 6 +++++ ios/RCTMGL-v10/MGLModule.swift | 4 +-- ios/RCTMGL-v10/RCTMGLEvent.swift | 2 ++ ios/RCTMGL-v10/RCTMGLMapView.swift | 5 ++-- javascript/components/MapView.js | 38 ++++++++++++++++++++++++++-- 7 files changed, 91 insertions(+), 12 deletions(-) diff --git a/docs/MapView.md b/docs/MapView.md index 1503b1050..d5a14d797 100644 --- a/docs/MapView.md +++ b/docs/MapView.md @@ -26,9 +26,11 @@ | surfaceView | `bool` | `false` | `false` | [Android only] Enable/Disable use of GLSurfaceView insted of TextureView. | | onPress | `func` | `none` | `false` | Map press listener, gets called when a user presses the map | | onLongPress | `func` | `none` | `false` | Map long press listener, gets called when a user long presses the map | -| onRegionWillChange | `func` | `none` | `false` | This event is triggered whenever the currently displayed map region is about to change. | -| onRegionIsChanging | `func` | `none` | `false` | This event is triggered whenever the currently displayed map region is changing. | -| onRegionDidChange | `func` | `none` | `false` | This event is triggered whenever the currently displayed map region finished changing | +| onRegionWillChange | `func` | `none` | `false` |
This event is triggered whenever the currently displayed map region is about to change. | +| onRegionIsChanging | `func` | `none` | `false` |
This event is triggered whenever the currently displayed map region is changing. | +| onRegionDidChange | `func` | `none` | `false` |
This event is triggered whenever the currently displayed map region finished changing. | +| onCameraChanged | `func` | `none` | `false` | v10 only

Called when the currently displayed map area changes. | +| onMapIdle | `func` | `none` | `false` | v10 only

Called when the currently displayed map area stops changing. | | onWillStartLoadingMap | `func` | `none` | `false` | This event is triggered when the map is about to start loading a new map style. | | onDidFinishLoadingMap | `func` | `none` | `false` | This is triggered when the map has successfully loaded a new map style. | | onDidFailLoadingMap | `func` | `none` | `false` | This event is triggered when the map has failed to load a new map style. | diff --git a/docs/docs.json b/docs/docs.json index 2b13910b5..61a94fa3f 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -3044,7 +3044,7 @@ "required": false, "type": "func", "default": "none", - "description": "This event is triggered whenever the currently displayed map region is about to change.", + "description": ", ) => void; + onCameraChanged?: ( + feature: GeoJSON.Feature, + ) => void; + onMapIdle?: ( + feature: GeoJSON.Feature, + ) => void; onUserLocationUpdate?: (feature: MapboxGL.Location) => void; onWillStartLoadingMap?: () => void; onDidFinishLoadingMap?: () => void; diff --git a/ios/RCTMGL-v10/MGLModule.swift b/ios/RCTMGL-v10/MGLModule.swift index c62e55914..4fc46d432 100644 --- a/ios/RCTMGL-v10/MGLModule.swift +++ b/ios/RCTMGL-v10/MGLModule.swift @@ -42,8 +42,8 @@ class MGLModule : NSObject { ], "EventTypes": [ - "RegionIsChanging" : RCTMGLEvent.EventType.regionIsChanging.rawValue, - "RegionDidChange" : RCTMGLEvent.EventType.regionDidChange.rawValue, + "CameraChanged" : RCTMGLEvent.EventType.cameraChanged.rawValue, + "MapIdle" : RCTMGLEvent.EventType.mapIdle.rawValue, "DidFinishLoadingMap": RCTMGLEvent.EventType.didFinishLoadingMap.rawValue ], "OfflineCallbackName": diff --git a/ios/RCTMGL-v10/RCTMGLEvent.swift b/ios/RCTMGL-v10/RCTMGLEvent.swift index 4a4fd09eb..e7c5e86ed 100644 --- a/ios/RCTMGL-v10/RCTMGLEvent.swift +++ b/ios/RCTMGL-v10/RCTMGLEvent.swift @@ -23,6 +23,8 @@ class RCTMGLEvent : NSObject, RCTMGLEventProtocol { case regionWillChange case regionIsChanging case regionDidChange + case cameraChanged + case mapIdle case imageMissing case didFinishLoadingMap case offlineProgress diff --git a/ios/RCTMGL-v10/RCTMGLMapView.swift b/ios/RCTMGL-v10/RCTMGLMapView.swift index fd4838435..e53422799 100644 --- a/ios/RCTMGL-v10/RCTMGLMapView.swift +++ b/ios/RCTMGL-v10/RCTMGLMapView.swift @@ -1,5 +1,6 @@ import MapboxMaps import Turf +import MapKit class PointAnnotationManager : AnnotationInteractionDelegate { weak var selected : RCTMGLPointAnnotation? = nil @@ -160,12 +161,12 @@ open class RCTMGLMapView : MapView { self.reactOnMapChange = value self.mapView.mapboxMap.onEvery(.cameraChanged, handler: { cameraEvent in - let event = RCTMGLEvent(type:.regionIsChanging, payload: self._makeRegionPayload()); + let event = RCTMGLEvent(type:.cameraChanged, payload: self._makeRegionPayload()); self.fireEvent(event: event, callback: self.reactOnMapChange!) }) self.mapView.mapboxMap.onEvery(.mapIdle, handler: { cameraEvent in - let event = RCTMGLEvent(type:.regionDidChange, payload: self._makeRegionPayload()); + let event = RCTMGLEvent(type:.mapIdle, payload: self._makeRegionPayload()); self.fireEvent(event: event, callback: self.reactOnMapChange!) }) } diff --git a/javascript/components/MapView.js b/javascript/components/MapView.js index ab6a70a62..17e1aef34 100644 --- a/javascript/components/MapView.js +++ b/javascript/components/MapView.js @@ -179,6 +179,8 @@ class MapView extends NativeBridgeComponent(React.Component) { onLongPress: PropTypes.func, /** + * Date: Tue, 19 Apr 2022 13:42:24 -0500 Subject: [PATCH 07/84] Display additional callback info --- .../src/examples/V10/MapGestureHandlers.js | 36 +++++++++++++++---- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/example/src/examples/V10/MapGestureHandlers.js b/example/src/examples/V10/MapGestureHandlers.js index bb8676705..b79658081 100644 --- a/example/src/examples/V10/MapGestureHandlers.js +++ b/example/src/examples/V10/MapGestureHandlers.js @@ -18,9 +18,13 @@ const styles = { divider: { marginVertical: 10, }, + fadedText: { + color: 'gray', + }, }; const MapGestureHandlers = props => { + const [lastCallback, setLastCallback] = useState(''); const [region, setRegion] = useState({}); const properties = region?.properties; @@ -29,19 +33,37 @@ const MapGestureHandlers = props => { setRegion(_region)} - onRegionIsChanging={_region => setRegion(_region)} - onRegionDidChange={_region => setRegion(_region)}> + onPress={e => { + console.log(e); + }} + onLongPress={e => { + console.log(e); + }} + onCameraChanged={_region => { + setLastCallback('onCameraChanged'); + setRegion(_region); + }} + onMapIdle={_region => { + setLastCallback('onMapIdle'); + setRegion(_region); + }}> - Interacting: - {properties?.isUserInteraction ? 'Yes' : 'No'} + lastCallback + {lastCallback} + + + + isUserInteraction + {properties?.isUserInteraction ? 'Yes' : 'No'} + - Animating from interaction: - + + isAnimatingFromUserInteraction + {properties?.isAnimatingFromUserInteraction ? 'Yes' : 'No'} From ecf6048bdcf25eb359f1ffca56ec210bbc0cc312 Mon Sep 17 00:00:00 2001 From: Naftali Beder Date: Tue, 19 Apr 2022 13:52:25 -0500 Subject: [PATCH 08/84] Use instant camera transition --- example/src/examples/V10/MapGestureHandlers.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/example/src/examples/V10/MapGestureHandlers.js b/example/src/examples/V10/MapGestureHandlers.js index b79658081..9572e6728 100644 --- a/example/src/examples/V10/MapGestureHandlers.js +++ b/example/src/examples/V10/MapGestureHandlers.js @@ -47,7 +47,11 @@ const MapGestureHandlers = props => { setLastCallback('onMapIdle'); setRegion(_region); }}> - + From 9fd16b080016570df495be252b78be6ca7fe5352 Mon Sep 17 00:00:00 2001 From: Naftali Beder Date: Tue, 19 Apr 2022 14:19:02 -0500 Subject: [PATCH 09/84] Add long-press handler --- ios/RCTMGL-v10/RCTMGLEvent.swift | 1 + ios/RCTMGL-v10/RCTMGLMapView.swift | 36 ++++++++++++++++++++++----- ios/RCTMGL-v10/RCTMGLMapViewManager.m | 1 + 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/ios/RCTMGL-v10/RCTMGLEvent.swift b/ios/RCTMGL-v10/RCTMGLEvent.swift index e7c5e86ed..cbb995191 100644 --- a/ios/RCTMGL-v10/RCTMGLEvent.swift +++ b/ios/RCTMGL-v10/RCTMGLEvent.swift @@ -20,6 +20,7 @@ class RCTMGLEvent : NSObject, RCTMGLEventProtocol { enum EventType : String { case tap + case longPress case regionWillChange case regionIsChanging case regionDidChange diff --git a/ios/RCTMGL-v10/RCTMGLMapView.swift b/ios/RCTMGL-v10/RCTMGLMapView.swift index e53422799..abe5b53d9 100644 --- a/ios/RCTMGL-v10/RCTMGLMapView.swift +++ b/ios/RCTMGL-v10/RCTMGLMapView.swift @@ -109,10 +109,11 @@ public func dictionaryFrom(_ from: Turf.Feature?) throws -> [String:Any]? { @objc(RCTMGLMapView) open class RCTMGLMapView : MapView { - var reactOnPress : RCTBubblingEventBlock? = nil - var reactOnMapChange : RCTBubblingEventBlock? = nil + var reactOnPress : RCTBubblingEventBlock? + var reactOnLongPress : RCTBubblingEventBlock? + var reactOnMapChange : RCTBubblingEventBlock? - var reactCamera : RCTMGLCamera? = nil + var reactCamera : RCTMGLCamera? var images : [RCTMGLImages] = [] var sources : [RCTMGLSource] = [] @@ -149,14 +150,21 @@ open class RCTMGLMapView : MapView { } } } - + @objc func setReactOnPress(_ value: @escaping RCTBubblingEventBlock) { self.reactOnPress = value - + self.mapView.gestures.singleTapGestureRecognizer.removeTarget( pointAnnotationManager.manager, action: nil) self.mapView.gestures.singleTapGestureRecognizer.addTarget(self, action: #selector(doHandleTap(_:))) } + @objc func setReactOnLongPress(_ value: @escaping RCTBubblingEventBlock) { + self.reactOnLongPress = value + + let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(doHandleLongPress(_:))) + self.mapView.addGestureRecognizer(longPressGestureRecognizer) + } + @objc func setReactOnMapChange(_ value: @escaping RCTBubblingEventBlock) { self.reactOnMapChange = value @@ -443,7 +451,7 @@ extension RCTMGLMapView: GestureManagerDelegate { ] ) self.fireEvent(event: event, callback: onPress) - + } else { if let reactOnPress = self.reactOnPress { let location = self.mapboxMap.coordinate(for: tapPoint) @@ -461,6 +469,22 @@ extension RCTMGLMapView: GestureManagerDelegate { } } + @objc + func doHandleLongPress(_ sender: UILongPressGestureRecognizer) { + let position = sender.location(in: self) + + if let reactOnLongPress = self.reactOnLongPress, sender.state == .began { + let coordinate = self.mapboxMap.coordinate(for: position) + var geojson = Feature(geometry: .point(Point(coordinate))); + geojson.properties = [ + "screenPointX": .number(Double(position.x)), + "screenPointY": .number(Double(position.y)) + ] + let event = try! RCTMGLEvent(type:.longPress, payload: dictionaryFrom(geojson)!) + self.fireEvent(event: event, callback: reactOnLongPress) + } + } + public func gestureManager(_ gestureManager: GestureManager, didBegin gestureType: GestureType) { _isUserInteraction = true } diff --git a/ios/RCTMGL-v10/RCTMGLMapViewManager.m b/ios/RCTMGL-v10/RCTMGLMapViewManager.m index 0f08a9ba3..1e4a2333c 100644 --- a/ios/RCTMGL-v10/RCTMGLMapViewManager.m +++ b/ios/RCTMGL-v10/RCTMGLMapViewManager.m @@ -5,6 +5,7 @@ @interface RCT_EXTERN_REMAP_MODULE(RCTMGLMapView, RCTMGLMapViewManager, RCTViewM RCT_REMAP_VIEW_PROPERTY(styleURL, reactStyleURL, NSString) RCT_REMAP_VIEW_PROPERTY(onPress, reactOnPress, RCTBubblingEventBlock) +RCT_REMAP_VIEW_PROPERTY(onLongPress, reactOnLongPress, RCTBubblingEventBlock) RCT_REMAP_VIEW_PROPERTY(onMapChange, reactOnMapChange, RCTBubblingEventBlock) RCT_REMAP_VIEW_PROPERTY(zoomEnabled, reactZoomEnabled, BOOL) From 054cf0aee69029c69d9f989880d4cec3c8a27e1b Mon Sep 17 00:00:00 2001 From: Naftali Beder Date: Tue, 19 Apr 2022 14:42:18 -0500 Subject: [PATCH 10/84] Incorporate tap and long-press into example --- .../src/examples/V10/MapGestureHandlers.js | 55 +++++++++++++++++-- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/example/src/examples/V10/MapGestureHandlers.js b/example/src/examples/V10/MapGestureHandlers.js index 9572e6728..145a2be3a 100644 --- a/example/src/examples/V10/MapGestureHandlers.js +++ b/example/src/examples/V10/MapGestureHandlers.js @@ -1,9 +1,16 @@ import React, {useState} from 'react'; import {SafeAreaView, View} from 'react-native'; -import {MapView, Camera, Logger} from '@rnmapbox/maps'; +import { + MapView, + Camera, + CircleLayer, + ShapeSource, + Logger, +} from '@rnmapbox/maps'; import {Text, Divider} from 'react-native-elements'; import Page from '../common/Page'; +import colors from '../../styles/colors'; Logger.setLogLevel('verbose'); @@ -26,18 +33,32 @@ const styles = { const MapGestureHandlers = props => { const [lastCallback, setLastCallback] = useState(''); const [region, setRegion] = useState({}); + const [features, setFeatures] = useState([]); const properties = region?.properties; + const buildShape = feature => { + return { + type: 'Point', + coordinates: feature.geometry.coordinates, + }; + }; + + const addFeature = (feature, kind) => { + const _feature = {...feature}; + _feature.properties.kind = kind; + setFeatures(prev => [...prev, _feature]); + }; + return ( { - console.log(e); + onPress={_feature => { + addFeature(_feature, 'press'); }} - onLongPress={e => { - console.log(e); + onLongPress={_feature => { + addFeature(_feature, 'longPress'); }} onCameraChanged={_region => { setLastCallback('onCameraChanged'); @@ -52,10 +73,34 @@ const MapGestureHandlers = props => { zoomLevel={12} animationDuration={0} /> + {features.map((f, i) => { + const id = JSON.stringify(f.geometry.coordinates); + const circleStyle = + f.properties.kind === 'press' + ? { + circleColor: colors.primary.blue, + circleRadius: 6, + } + : { + circleColor: colors.primary.pink, + circleRadius: 12, + }; + return ( + + + + ); + })} + + Tap or long-press to create a marker. + + + + lastCallback {lastCallback} From a31cc0d78e677bd997b5ed346f7d9c6497f34230 Mon Sep 17 00:00:00 2001 From: Naftali Beder Date: Wed, 20 Apr 2022 10:59:35 -0500 Subject: [PATCH 11/84] Ignore Ruby gems and local bundler files, cleanup --- .gitignore | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index d1a4b3635..74642f657 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# System +.DS_Store + # Xcode !**/*.xcodeproj !**/*.pbxproj @@ -58,9 +61,6 @@ yarn.lock # Expo .expo -# OS X -.DS_Store - # Test generated files /ReactAndroid/src/androidTest/assets/AndroidTestBundle.js *.js.meta @@ -82,10 +82,14 @@ RNTester/build /React/CoreModules/**/*.xcodeproj /packages/react-native-codegen/**/*.xcodeproj +# Ruby gems/bundle +Gemfile +Gemfile.lock +vendor/ + # CocoaPods /template/ios/Pods/ /template/ios/Podfile.lock -/RNTester/Gemfile.lock # Ignore RNTester specific Pods, but keep the __offline_mirrors__ here. RNTester/Pods/* @@ -99,7 +103,7 @@ RNTester/Pods/* .vscode .vs -# project specific +# Project specific ios/Mapbox.framework ios/temp.zip ios/.framework_version From 0c258b80fbbb079d4498ec72c0e2b2774fc084df Mon Sep 17 00:00:00 2001 From: Naftali Beder Date: Wed, 27 Apr 2022 14:10:13 -0500 Subject: [PATCH 12/84] Convert Camera to typescript --- docs/docs.json | 607 ---------------------------- javascript/components/Camera.js | 673 ------------------------------- javascript/components/Camera.tsx | 420 +++++++++++++++++++ javascript/index.js | 4 +- 4 files changed, 422 insertions(+), 1282 deletions(-) delete mode 100644 javascript/components/Camera.js create mode 100644 javascript/components/Camera.tsx diff --git a/docs/docs.json b/docs/docs.json index b4fce8893..6a679066c 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -208,613 +208,6 @@ ], "name": "Callout" }, - "Camera": { - "description": "", - "displayName": "Camera", - "methods": [ - { - "name": "fitBounds", - "docblock": "Map camera transitions to fit provided bounds\n\n@example\nthis.camera.fitBounds([lng, lat], [lng, lat])\nthis.camera.fitBounds([lng, lat], [lng, lat], 20, 1000) // padding for all sides\nthis.camera.fitBounds([lng, lat], [lng, lat], [verticalPadding, horizontalPadding], 1000)\nthis.camera.fitBounds([lng, lat], [lng, lat], [top, right, bottom, left], 1000)\n\n@param {Array} northEastCoordinates - North east coordinate of bound\n@param {Array} southWestCoordinates - South west coordinate of bound\n@param {Number=} padding - Camera padding for bound\n@param {Number=} animationDuration - Duration of camera animation\n@return {void}", - "modifiers": [], - "params": [ - { - "name": "northEastCoordinates", - "description": "North east coordinate of bound", - "type": { - "name": "Array", - "elements": [ - { - "name": "Number" - } - ] - }, - "optional": false - }, - { - "name": "southWestCoordinates", - "description": "South west coordinate of bound", - "type": { - "name": "Array", - "elements": [ - { - "name": "Number" - } - ] - }, - "optional": false - }, - { - "name": "padding", - "description": "Camera padding for bound", - "type": { - "name": "Number" - }, - "optional": true - }, - { - "name": "animationDuration", - "description": "Duration of camera animation", - "type": { - "name": "Number" - }, - "optional": true - } - ], - "returns": { - "description": null, - "type": { - "name": "void" - } - }, - "description": "Map camera transitions to fit provided bounds", - "examples": [ - "\nthis.camera.fitBounds([lng, lat], [lng, lat])\nthis.camera.fitBounds([lng, lat], [lng, lat], 20, 1000) // padding for all sides\nthis.camera.fitBounds([lng, lat], [lng, lat], [verticalPadding, horizontalPadding], 1000)\nthis.camera.fitBounds([lng, lat], [lng, lat], [top, right, bottom, left], 1000)\n\n" - ] - }, - { - "name": "flyTo", - "docblock": "Map camera will fly to new coordinate\n\n@example\nthis.camera.flyTo([lng, lat])\nthis.camera.flyTo([lng, lat], 12000)\n\n @param {Array} coordinates - Coordinates that map camera will jump too\n @param {Number=} animationDuration - Duration of camera animation\n @return {void}", - "modifiers": [], - "params": [ - { - "name": "coordinates", - "description": "Coordinates that map camera will jump too", - "type": { - "name": "Array", - "elements": [ - { - "name": "Number" - } - ] - }, - "optional": false - }, - { - "name": "animationDuration", - "description": "Duration of camera animation", - "type": { - "name": "Number" - }, - "optional": true - } - ], - "returns": { - "description": null, - "type": { - "name": "void" - } - }, - "description": "Map camera will fly to new coordinate", - "examples": [ - "\nthis.camera.flyTo([lng, lat])\nthis.camera.flyTo([lng, lat], 12000)\n\n " - ] - }, - { - "name": "moveTo", - "docblock": "Map camera will move to new coordinate at the same zoom level\n\n@example\nthis.camera.moveTo([lng, lat], 200) // eases camera to new location based on duration\nthis.camera.moveTo([lng, lat]) // snaps camera to new location without any easing\n\n @param {Array} coordinates - Coordinates that map camera will move too\n @param {Number=} animationDuration - Duration of camera animation\n @return {void}", - "modifiers": [], - "params": [ - { - "name": "coordinates", - "description": "Coordinates that map camera will move too", - "type": { - "name": "Array", - "elements": [ - { - "name": "Number" - } - ] - }, - "optional": false - }, - { - "name": "animationDuration", - "description": "Duration of camera animation", - "type": { - "name": "Number" - }, - "optional": true - } - ], - "returns": { - "description": null, - "type": { - "name": "void" - } - }, - "description": "Map camera will move to new coordinate at the same zoom level", - "examples": [ - "\nthis.camera.moveTo([lng, lat], 200) // eases camera to new location based on duration\nthis.camera.moveTo([lng, lat]) // snaps camera to new location without any easing\n\n " - ] - }, - { - "name": "zoomTo", - "docblock": "Map camera will zoom to specified level\n\n@example\nthis.camera.zoomTo(16)\nthis.camera.zoomTo(16, 100)\n\n@param {Number} zoomLevel - Zoom level that the map camera will animate too\n@param {Number=} animationDuration - Duration of camera animation\n@return {void}", - "modifiers": [], - "params": [ - { - "name": "zoomLevel", - "description": "Zoom level that the map camera will animate too", - "type": { - "name": "Number" - }, - "optional": false - }, - { - "name": "animationDuration", - "description": "Duration of camera animation", - "type": { - "name": "Number" - }, - "optional": true - } - ], - "returns": { - "description": null, - "type": { - "name": "void" - } - }, - "description": "Map camera will zoom to specified level", - "examples": [ - "\nthis.camera.zoomTo(16)\nthis.camera.zoomTo(16, 100)\n\n" - ] - }, - { - "name": "setCamera", - "docblock": "Map camera will perform updates based on provided config. Advanced use only!\n\n@example\nthis.camera.setCamera({\n centerCoordinate: [lng, lat],\n zoomLevel: 16,\n animationDuration: 2000,\n})\n\nthis.camera.setCamera({\n stops: [\n { pitch: 45, animationDuration: 200 },\n { heading: 180, animationDuration: 300 },\n ]\n})\n\n @param {Object} config - Camera configuration", - "modifiers": [], - "params": [ - { - "name": "config", - "description": "Camera configuration", - "type": { - "name": "Object" - }, - "optional": false - } - ], - "returns": null, - "description": "Map camera will perform updates based on provided config. Advanced use only!", - "examples": [ - "\nthis.camera.setCamera({\n centerCoordinate: [lng, lat],\n zoomLevel: 16,\n animationDuration: 2000,\n})\n\nthis.camera.setCamera({\n stops: [\n { pitch: 45, animationDuration: 200 },\n { heading: 180, animationDuration: 300 },\n ]\n})\n\n " - ] - } - ], - "props": [ - { - "name": "allowUpdates", - "required": false, - "type": "bool", - "default": "true", - "description": "If false, the camera will not send any props to the native module. Intended to be used to prevent unnecessary tile fetching and improve performance when the map is not visible. Defaults to true." - }, - { - "name": "animationDuration", - "required": false, - "type": "number", - "default": "2000", - "description": "The duration a camera update takes (in ms)" - }, - { - "name": "animationMode", - "required": false, - "type": "enum", - "default": "'easeTo'", - "description": "The animation style when the camara updates. One of:\n`flyTo`: A complex flight animation, affecting both position and zoom.\n`easeTo`: A standard damped curve.\n`linearTo`: An even linear transition.\n`none`: An instantaneous change (v10 only).\n`moveTo`: An instantaneous change (} northEastCoordinates - North east coordinate of bound - * @param {Array} southWestCoordinates - South west coordinate of bound - * @param {Number=} padding - Camera padding for bound - * @param {Number=} animationDuration - Duration of camera animation - * @return {void} - */ - fitBounds( - northEastCoordinates, - southWestCoordinates, - padding = 0, - animationDuration = 0.0, - ) { - const pad = { - paddingLeft: 0, - paddingRight: 0, - paddingTop: 0, - paddingBottom: 0, - }; - - if (Array.isArray(padding)) { - if (padding.length === 2) { - pad.paddingTop = padding[0]; - pad.paddingBottom = padding[0]; - pad.paddingLeft = padding[1]; - pad.paddingRight = padding[1]; - } else if (padding.length === 4) { - pad.paddingTop = padding[0]; - pad.paddingRight = padding[1]; - pad.paddingBottom = padding[2]; - pad.paddingLeft = padding[3]; - } - } else { - pad.paddingLeft = padding; - pad.paddingRight = padding; - pad.paddingTop = padding; - pad.paddingBottom = padding; - } - - return this.setCamera({ - bounds: { - ne: northEastCoordinates, - sw: southWestCoordinates, - }, - padding: pad, - animationDuration, - animationMode: Camera.Mode.Ease, - }); - } - - /** - * Map camera will fly to new coordinate - * - * @example - * this.camera.flyTo([lng, lat]) - * this.camera.flyTo([lng, lat], 12000) - * - * @param {Array} coordinates - Coordinates that map camera will jump too - * @param {Number=} animationDuration - Duration of camera animation - * @return {void} - */ - flyTo(coordinates, animationDuration = 2000) { - return this.setCamera({ - centerCoordinate: coordinates, - animationDuration, - animationMode: Camera.Mode.Flight, - }); - } - - /** - * Map camera will move to new coordinate at the same zoom level - * - * @example - * this.camera.moveTo([lng, lat], 200) // eases camera to new location based on duration - * this.camera.moveTo([lng, lat]) // snaps camera to new location without any easing - * - * @param {Array} coordinates - Coordinates that map camera will move too - * @param {Number=} animationDuration - Duration of camera animation - * @return {void} - */ - moveTo(coordinates, animationDuration = 0) { - return this.setCamera({ - centerCoordinate: coordinates, - animationDuration, - }); - } - - /** - * Map camera will zoom to specified level - * - * @example - * this.camera.zoomTo(16) - * this.camera.zoomTo(16, 100) - * - * @param {Number} zoomLevel - Zoom level that the map camera will animate too - * @param {Number=} animationDuration - Duration of camera animation - * @return {void} - */ - zoomTo(zoomLevel, animationDuration = 2000) { - return this.setCamera({ - zoomLevel, - animationDuration, - animationMode: Camera.Mode.Flight, - }); - } - - /** - * Map camera will perform updates based on provided config. Advanced use only! - * - * @example - * this.camera.setCamera({ - * centerCoordinate: [lng, lat], - * zoomLevel: 16, - * animationDuration: 2000, - * }) - * - * this.camera.setCamera({ - * stops: [ - * { pitch: 45, animationDuration: 200 }, - * { heading: 180, animationDuration: 300 }, - * ] - * }) - * - * @param {Object} config - Camera configuration - */ - setCamera(config = {}) { - this._setCamera(config); - } - - _setCamera(config = {}) { - let cameraConfig = {}; - - if (config.stops) { - cameraConfig.stops = []; - - for (const stop of config.stops) { - cameraConfig.stops.push(this._createStopConfig(stop)); - } - } else { - cameraConfig = this._createStopConfig(config); - } - - this.refs.camera.setNativeProps({ stop: cameraConfig }); - } - - _createDefaultCamera() { - if (this.defaultCamera) { - return this.defaultCamera; - } - if (!this.props.defaultSettings) { - return null; - } - - this.defaultCamera = this._createStopConfig( - { - ...this.props.defaultSettings, - animationMode: Camera.Mode.Move, - }, - true, - ); - return this.defaultCamera; - } - - _createStopConfig(config = {}, ignoreFollowUserLocation = false) { - if (this.props.followUserLocation && !ignoreFollowUserLocation) { - return null; - } - - const stopConfig = { - mode: this._getNativeCameraMode(config), - pitch: config.pitch, - heading: config.heading, - duration: config.animationDuration || 0, - zoom: config.zoomLevel, - }; - - if (config.centerCoordinate) { - stopConfig.centerCoordinate = toJSONString( - geoUtils.makePoint(config.centerCoordinate), - ); - } - - if (config.bounds && config.bounds.ne && config.bounds.sw) { - const { ne, sw } = config.bounds; - stopConfig.bounds = toJSONString(geoUtils.makeLatLngBounds(ne, sw)); - } - - stopConfig.paddingTop = - config.padding?.paddingTop || config.bounds?.paddingTop || 0; - stopConfig.paddingRight = - config.padding?.paddingRight || config.bounds?.paddingRight || 0; - stopConfig.paddingBottom = - config.padding?.paddingBottom || config.bounds?.paddingBottom || 0; - stopConfig.paddingLeft = - config.padding?.paddingLeft || config.bounds?.paddingLeft || 0; - - return stopConfig; - } - - _getNativeCameraMode(config) { - switch (config.animationMode) { - case Camera.Mode.Flight: - return MapboxGL.CameraModes.Flight; - case Camera.Mode.Ease: - return MapboxGL.CameraModes.Ease; - case Camera.Mode.Linear: - return MapboxGL.CameraModes.Linear; - case Camera.Mode.None: - return MapboxGL.CameraModes.None; - case Camera.Mode.Move: - default: - return MapboxGL.CameraModes.Move; - } - } - - _getMaxBounds() { - const bounds = this.props.maxBounds; - if (!bounds || !bounds.ne || !bounds.sw) { - return null; - } - return toJSONString(geoUtils.makeLatLngBounds(bounds.ne, bounds.sw)); - } - - render() { - const props = Object.assign({}, this.props); - - const callbacks = { - onUserTrackingModeChange: props.onUserTrackingModeChange, - }; - - return ( - - ); - } -} - -const RCTMGLCamera = requireNativeComponent(NATIVE_MODULE_NAME, Camera, { - nativeOnly: { - stop: true, - }, -}); - -Camera.UserTrackingModes = { - Follow: 'normal', - FollowWithHeading: 'compass', - FollowWithCourse: 'course', -}; - -export default Camera; diff --git a/javascript/components/Camera.tsx b/javascript/components/Camera.tsx new file mode 100644 index 000000000..ad830f149 --- /dev/null +++ b/javascript/components/Camera.tsx @@ -0,0 +1,420 @@ +import React, { memo, useCallback, useMemo, useRef } from 'react'; +import { Position } from 'geojson'; +import { NativeModules, requireNativeComponent } from 'react-native'; + +import * as geoUtils from '../utils/geoUtils'; + +const MapboxGL = NativeModules.MGLModule; + +export const NATIVE_MODULE_NAME = 'RCTMGLCamera'; + +const Mode = { + Flight: 'flyTo', + Ease: 'easeTo', + Linear: 'linearTo', + None: 'none', + Move: 'moveTo', +}; + +export const UserTrackingModes = { + Follow: 'normal', + FollowWithHeading: 'compass', + FollowWithCourse: 'course', +}; + +type AnimationMode = 'flyTo' | 'easeTo' | 'linearTo' | 'none' | 'moveTo'; + +interface CameraStop { + centerCoordinate: Position; + bounds: { + ne: Position; + sw: Position; + paddingLeft?: number; // deprecated + paddingRight?: number; // deprecated + paddingTop?: number; // deprecated + paddingBottom?: number; // deprecated + }; + heading: number; + pitch: number; + zoomLevel: number; + padding: { + paddingLeft: number; + paddingRight: number; + paddingTop: number; + paddingBottom: number; + }; + animationDuration: number; + animationMode: AnimationMode; +} + +interface CameraFollowConfig { + followUserLocation: boolean; + followUserMode?: 'normal' | 'compass' | 'course'; + followZoomLevel?: number; + followPitch?: number; + followHeading?: number; +} + +interface CameraMinMaxConfig { + minZoomLevel: number; + maxZoomLevel: number; + maxBounds: { + ne: Position; + sw: Position; + }; +} + +/** + * @param {Position} centerCoordinate + * @param {TODO} bounds + * @param {number} heading + * @param {number} pitch + * @param {number} zoomLevel + * @param {TODO} padding + * @param {number} animationDuration + * @param {AnimationMode} animationMode + * @param {boolean} followUserLocation + * @param {'normal' | 'compass' | 'course'} followUserMode + * @param {number} followZoomLevel + * @param {number} followPitch + * @param {number} followHeading + * @param {number} minZoomLevel + * @param {number} maxZoomLevel + * @param {TODO} maxBounds + */ +interface CameraProps + extends CameraStop, + CameraFollowConfig, + CameraMinMaxConfig { + defaultSettings?: CameraStop; + allowUpdates: boolean; + triggerKey: any; + onUserTrackingModeChange: () => void; +} + +/** + * + * @param {CameraProps} props + */ +const Camera = memo((props: CameraProps) => { + const { + centerCoordinate, + bounds, + heading, + pitch, + zoomLevel, + padding, + allowUpdates = true, + animationDuration = 2000, + animationMode = 'easeTo', + defaultSettings, + minZoomLevel, + maxZoomLevel, + maxBounds, + followUserLocation, + followUserMode, + followZoomLevel, + followPitch, + followHeading, + triggerKey, + onUserTrackingModeChange, + } = props; + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const camera: React.RefObject = useRef(null); + + /** + * Map camera transitions to fit provided bounds + * + * @example + * camera.fitBounds([lng, lat], [lng, lat]) + * camera.fitBounds([lng, lat], [lng, lat], 20, 1000) // padding for all sides + * camera.fitBounds([lng, lat], [lng, lat], [verticalPadding, horizontalPadding], 1000) + * camera.fitBounds([lng, lat], [lng, lat], [top, right, bottom, left], 1000) + * + * @param {Array} northEastCoordinates - North east coordinate of bound + * @param {Array} southWestCoordinates - South west coordinate of bound + * @param {Number=} padding - Camera padding for bound + * @param {Number=} animationDuration - Duration of camera animation + * @return {void} + */ + // const fitBounds = ( + // northEastCoordinates: Position, + // southWestCoordinates: Position, + // padding = 0, + // animationDuration = 0.0, + // ) => { + // const pad = { + // paddingLeft: 0, + // paddingRight: 0, + // paddingTop: 0, + // paddingBottom: 0, + // }; + + // if (Array.isArray(padding)) { + // if (padding.length === 2) { + // pad.paddingTop = padding[0]; + // pad.paddingBottom = padding[0]; + // pad.paddingLeft = padding[1]; + // pad.paddingRight = padding[1]; + // } else if (padding.length === 4) { + // pad.paddingTop = padding[0]; + // pad.paddingRight = padding[1]; + // pad.paddingBottom = padding[2]; + // pad.paddingLeft = padding[3]; + // } + // } else { + // pad.paddingLeft = padding; + // pad.paddingRight = padding; + // pad.paddingTop = padding; + // pad.paddingBottom = padding; + // } + + // return setCamera({ + // bounds: { + // ne: northEastCoordinates, + // sw: southWestCoordinates, + // }, + // padding: pad, + // animationDuration, + // animationMode: Mode.Ease, + // }); + // }; + + /** + * Map camera will fly to new coordinate + * + * @example + * camera.flyTo([lng, lat]) + * camera.flyTo([lng, lat], 12000) + * + * @param {Array} coordinates - Coordinates that map camera will jump too + * @param {Number=} animationDuration - Duration of camera animation + * @return {void} + */ + // const flyTo = (coordinates: Position, animationDuration = 2000) => { + // return setCamera({ + // centerCoordinate: coordinates, + // animationDuration, + // animationMode: Mode.Flight, + // }); + // }; + + /** + * Map camera will move to new coordinate at the same zoom level + * + * @example + * camera.moveTo([lng, lat], 200) // eases camera to new location based on duration + * camera.moveTo([lng, lat]) // snaps camera to new location without any easing + * + * @param {Array} coordinates - Coordinates that map camera will move too + * @param {Number=} animationDuration - Duration of camera animation + * @return {void} + */ + // const moveTo = (coordinates: Position, animationDuration = 0) => { + // return setCamera({ + // centerCoordinate: coordinates, + // animationDuration, + // }); + // }; + + /** + * Map camera will zoom to specified level + * + * @example + * camera.zoomTo(16) + * camera.zoomTo(16, 100) + * + * @param {Number} zoomLevel - Zoom level that the map camera will animate too + * @param {Number=} animationDuration - Duration of camera animation + * @return {void} + */ + // const zoomTo = (zoomLevel: number, animationDuration = 2000) => { + // return setCamera({ + // zoomLevel, + // animationDuration, + // animationMode: Mode.Flight, + // }); + // }; + + /** + * Map camera will perform updates based on provided config. Advanced use only! + * + * @example + * camera.setCamera({ + * centerCoordinate: [lng, lat], + * zoomLevel: 16, + * animationDuration: 2000, + * }) + * + * camera.setCamera({ + * stops: [ + * { pitch: 45, animationDuration: 200 }, + * { heading: 180, animationDuration: 300 }, + * ] + * }) + * + * @param {Object} config - Camera configuration + */ + // const setCamera = (config: CameraStop) => { + // _setCamera(config); + // }; + + // const _setCamera = (config = {}) => { + // let cameraConfig = {}; + + // if (config.stops) { + // cameraConfig.stops = []; + + // for (const stop of config.stops) { + // cameraConfig.stops.push(_createStop(stop)); + // } + // } else { + // cameraConfig = _createStop(config); + // } + + // refs.camera.setNativeProps({ stop: cameraConfig }); + // } + + const _createDefaultStop = (): NativeCameraStop | null => { + return null; + }; + + const _createStop = useCallback( + ( + stop: CameraStop, + ignoreFollowUserLocation = false, + ): NativeCameraStop | null => { + if (props.followUserLocation && !ignoreFollowUserLocation) { + return null; + } + + const stopConfig: NativeCameraStop = { + mode: _getNativeCameraMode(stop), + pitch: stop.pitch, + heading: stop.heading, + duration: stop.animationDuration || 0, + zoom: stop.zoomLevel, + }; + + if (stop.centerCoordinate) { + stopConfig.centerCoordinate = JSON.stringify( + geoUtils.makePoint(stop.centerCoordinate), + ); + } + + if (stop.bounds && stop.bounds.ne && stop.bounds.sw) { + const { ne, sw } = stop.bounds; + stopConfig.bounds = JSON.stringify(geoUtils.makeLatLngBounds(ne, sw)); + } + + stopConfig.paddingTop = + stop.padding?.paddingTop ?? stop.bounds?.paddingTop ?? 0; + stopConfig.paddingRight = + stop.padding?.paddingRight ?? stop.bounds?.paddingRight ?? 0; + stopConfig.paddingBottom = + stop.padding?.paddingBottom ?? stop.bounds?.paddingBottom ?? 0; + stopConfig.paddingLeft = + stop.padding?.paddingLeft ?? stop.bounds?.paddingLeft ?? 0; + + return stopConfig; + }, + [props.followUserLocation], + ); + + const _getNativeCameraMode = (config: { animationMode: AnimationMode }) => { + switch (config.animationMode) { + case Mode.Flight: + return MapboxGL.CameraModes.Flight; + case Mode.Ease: + return MapboxGL.CameraModes.Ease; + case Mode.Linear: + return MapboxGL.CameraModes.Linear; + case Mode.None: + return MapboxGL.CameraModes.None; + case Mode.Move: + default: + return MapboxGL.CameraModes.Move; + } + }; + + const nativeMaxBounds = useMemo(() => { + if (!maxBounds?.ne || !maxBounds?.sw) { + return null; + } + return JSON.stringify( + geoUtils.makeLatLngBounds(maxBounds.ne, maxBounds.sw), + ); + }, [maxBounds]); + + const stop = useMemo(() => { + return _createStop({ + centerCoordinate, + bounds, + heading, + pitch, + zoomLevel, + padding, + animationDuration, + animationMode, + }); + }, [ + centerCoordinate, + bounds, + heading, + pitch, + zoomLevel, + padding, + animationDuration, + animationMode, + _createStop, + ]); + + return ( + + ); +}); + +interface NativeCameraProps extends CameraFollowConfig { + testID: string; + stop: NativeCameraStop | null; + defaultStop: NativeCameraStop | null; + minZoomLevel: number; + maxZoomLevel: number; + maxBounds: string | null; + onUserTrackingModeChange: () => void; +} + +interface NativeCameraStop { + centerCoordinate?: string; + bounds?: string; + heading: number; + pitch: number; + zoom: number; + paddingLeft?: number; + paddingRight?: number; + paddingTop?: number; + paddingBottom?: number; + duration: number; + mode: AnimationMode; +} + +const RCTMGLCamera = + requireNativeComponent(NATIVE_MODULE_NAME); + +export default Camera; diff --git a/javascript/index.js b/javascript/index.js index 193534a43..42c9244f3 100644 --- a/javascript/index.js +++ b/javascript/index.js @@ -7,7 +7,7 @@ import PointAnnotation from './components/PointAnnotation'; import Annotation from './components/annotations/Annotation'; import Callout from './components/Callout'; import UserLocation from './components/UserLocation'; -import Camera from './components/Camera'; +import Camera, { UserTrackingModes } from './components/Camera'; import VectorSource from './components/VectorSource'; import ShapeSource from './components/ShapeSource'; import RasterSource from './components/RasterSource'; @@ -64,7 +64,7 @@ MapboxGL.requestAndroidLocationPermissions = async function () { throw new Error('You should only call this method on Android!'); }; -MapboxGL.UserTrackingModes = Camera.UserTrackingModes; +MapboxGL.UserTrackingModes = UserTrackingModes; // components MapboxGL.MapView = MapView; From fefa4dda147cc925876d73077fb5a22414d15796 Mon Sep 17 00:00:00 2001 From: Naftali Beder Date: Wed, 27 Apr 2022 14:10:35 -0500 Subject: [PATCH 13/84] Fix import --- __tests__/components/Camera.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/__tests__/components/Camera.test.js b/__tests__/components/Camera.test.js index 7ec50012a..3acca9b1c 100644 --- a/__tests__/components/Camera.test.js +++ b/__tests__/components/Camera.test.js @@ -1,7 +1,7 @@ import React from 'react'; import { render } from '@testing-library/react-native'; -import Camera from '../../javascript/components/Camera'; +import Camera, { UserTrackingModes } from '../../javascript/components/Camera'; const cameraWithoutFollowDefault = { ...Camera.defaultProps, @@ -91,7 +91,7 @@ describe('Camera', () => { describe('class', () => { test('correct "UserTrackingModes" statics', () => { - expect(Camera.UserTrackingModes).toStrictEqual({ + expect(UserTrackingModes).toStrictEqual({ Follow: 'normal', FollowWithCourse: 'course', FollowWithHeading: 'compass', From 4d74a6c6835744066f74d203348de3cb5f1f46cd Mon Sep 17 00:00:00 2001 From: Naftali Beder Date: Wed, 27 Apr 2022 14:28:49 -0500 Subject: [PATCH 14/84] Clean up and expand stop methods --- javascript/components/Camera.tsx | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/javascript/components/Camera.tsx b/javascript/components/Camera.tsx index ad830f149..00dcb90dc 100644 --- a/javascript/components/Camera.tsx +++ b/javascript/components/Camera.tsx @@ -93,7 +93,6 @@ interface CameraProps } /** - * * @param {CameraProps} props */ const Camera = memo((props: CameraProps) => { @@ -104,10 +103,8 @@ const Camera = memo((props: CameraProps) => { pitch, zoomLevel, padding, - allowUpdates = true, animationDuration = 2000, animationMode = 'easeTo', - defaultSettings, minZoomLevel, maxZoomLevel, maxBounds, @@ -116,6 +113,8 @@ const Camera = memo((props: CameraProps) => { followZoomLevel, followPitch, followHeading, + defaultSettings, + allowUpdates = true, triggerKey, onUserTrackingModeChange, } = props; @@ -277,11 +276,7 @@ const Camera = memo((props: CameraProps) => { // refs.camera.setNativeProps({ stop: cameraConfig }); // } - const _createDefaultStop = (): NativeCameraStop | null => { - return null; - }; - - const _createStop = useCallback( + const buildNativeStop = useCallback( ( stop: CameraStop, ignoreFollowUserLocation = false, @@ -291,10 +286,10 @@ const Camera = memo((props: CameraProps) => { } const stopConfig: NativeCameraStop = { - mode: _getNativeCameraMode(stop), + mode: nativeAnimationMode(stop.animationMode), pitch: stop.pitch, heading: stop.heading, - duration: stop.animationDuration || 0, + duration: stop.animationDuration ?? 0, zoom: stop.zoomLevel, }; @@ -323,8 +318,8 @@ const Camera = memo((props: CameraProps) => { [props.followUserLocation], ); - const _getNativeCameraMode = (config: { animationMode: AnimationMode }) => { - switch (config.animationMode) { + const nativeAnimationMode = (_mode: AnimationMode) => { + switch (_mode) { case Mode.Flight: return MapboxGL.CameraModes.Flight; case Mode.Ease: @@ -349,7 +344,7 @@ const Camera = memo((props: CameraProps) => { }, [maxBounds]); const stop = useMemo(() => { - return _createStop({ + return buildNativeStop({ centerCoordinate, bounds, heading, @@ -368,15 +363,22 @@ const Camera = memo((props: CameraProps) => { padding, animationDuration, animationMode, - _createStop, + buildNativeStop, ]); + const defaultStop = useMemo((): NativeCameraStop | null => { + if (!defaultSettings) { + return null; + } + return buildNativeStop(defaultSettings); + }, [defaultSettings, buildNativeStop]); + return ( Date: Wed, 27 Apr 2022 14:32:22 -0500 Subject: [PATCH 15/84] Add memoization --- javascript/components/Camera.tsx | 34 ++++++++++++++++---------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/javascript/components/Camera.tsx b/javascript/components/Camera.tsx index 00dcb90dc..061746843 100644 --- a/javascript/components/Camera.tsx +++ b/javascript/components/Camera.tsx @@ -276,6 +276,22 @@ const Camera = memo((props: CameraProps) => { // refs.camera.setNativeProps({ stop: cameraConfig }); // } + const nativeAnimationMode = useCallback((_mode: AnimationMode) => { + switch (_mode) { + case Mode.Flight: + return MapboxGL.CameraModes.Flight; + case Mode.Ease: + return MapboxGL.CameraModes.Ease; + case Mode.Linear: + return MapboxGL.CameraModes.Linear; + case Mode.None: + return MapboxGL.CameraModes.None; + case Mode.Move: + default: + return MapboxGL.CameraModes.Move; + } + }, []); + const buildNativeStop = useCallback( ( stop: CameraStop, @@ -315,25 +331,9 @@ const Camera = memo((props: CameraProps) => { return stopConfig; }, - [props.followUserLocation], + [props.followUserLocation, nativeAnimationMode], ); - const nativeAnimationMode = (_mode: AnimationMode) => { - switch (_mode) { - case Mode.Flight: - return MapboxGL.CameraModes.Flight; - case Mode.Ease: - return MapboxGL.CameraModes.Ease; - case Mode.Linear: - return MapboxGL.CameraModes.Linear; - case Mode.None: - return MapboxGL.CameraModes.None; - case Mode.Move: - default: - return MapboxGL.CameraModes.Move; - } - }; - const nativeMaxBounds = useMemo(() => { if (!maxBounds?.ne || !maxBounds?.sw) { return null; From 8d649ae2b9e7aa07ad52defe9c4fc88ba9c4f74a Mon Sep 17 00:00:00 2001 From: Naftali Beder Date: Wed, 27 Apr 2022 14:33:41 -0500 Subject: [PATCH 16/84] Rename --- javascript/components/Camera.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/javascript/components/Camera.tsx b/javascript/components/Camera.tsx index 061746843..3febe9dc5 100644 --- a/javascript/components/Camera.tsx +++ b/javascript/components/Camera.tsx @@ -343,7 +343,7 @@ const Camera = memo((props: CameraProps) => { ); }, [maxBounds]); - const stop = useMemo(() => { + const nativeStop = useMemo(() => { return buildNativeStop({ centerCoordinate, bounds, @@ -366,7 +366,7 @@ const Camera = memo((props: CameraProps) => { buildNativeStop, ]); - const defaultStop = useMemo((): NativeCameraStop | null => { + const nativeDefaultStop = useMemo((): NativeCameraStop | null => { if (!defaultSettings) { return null; } @@ -377,8 +377,8 @@ const Camera = memo((props: CameraProps) => { Date: Wed, 27 Apr 2022 16:46:08 -0500 Subject: [PATCH 17/84] Build out primary imperative camera method --- javascript/components/Camera.tsx | 655 +++++++++++++++++-------------- 1 file changed, 360 insertions(+), 295 deletions(-) diff --git a/javascript/components/Camera.tsx b/javascript/components/Camera.tsx index 3febe9dc5..122a3d01e 100644 --- a/javascript/components/Camera.tsx +++ b/javascript/components/Camera.tsx @@ -1,4 +1,11 @@ -import React, { memo, useCallback, useMemo, useRef } from 'react'; +import React, { + forwardRef, + memo, + useCallback, + useImperativeHandle, + useMemo, + useRef, +} from 'react'; import { Position } from 'geojson'; import { NativeModules, requireNativeComponent } from 'react-native'; @@ -25,6 +32,7 @@ export const UserTrackingModes = { type AnimationMode = 'flyTo' | 'easeTo' | 'linearTo' | 'none' | 'moveTo'; interface CameraStop { + readonly type: 'CameraStop'; centerCoordinate: Position; bounds: { ne: Position; @@ -34,17 +42,17 @@ interface CameraStop { paddingTop?: number; // deprecated paddingBottom?: number; // deprecated }; - heading: number; - pitch: number; - zoomLevel: number; - padding: { + heading?: number; + pitch?: number; + zoomLevel?: number; + padding?: { paddingLeft: number; paddingRight: number; paddingTop: number; paddingBottom: number; }; - animationDuration: number; - animationMode: AnimationMode; + animationDuration?: number; + animationMode?: AnimationMode; } interface CameraFollowConfig { @@ -64,6 +72,11 @@ interface CameraMinMaxConfig { }; } +interface CameraStops { + readonly type: 'CameraStops'; + stops: CameraStop[]; +} + /** * @param {Position} centerCoordinate * @param {TODO} bounds @@ -95,256 +108,276 @@ interface CameraProps /** * @param {CameraProps} props */ -const Camera = memo((props: CameraProps) => { - const { - centerCoordinate, - bounds, - heading, - pitch, - zoomLevel, - padding, - animationDuration = 2000, - animationMode = 'easeTo', - minZoomLevel, - maxZoomLevel, - maxBounds, - followUserLocation, - followUserMode, - followZoomLevel, - followPitch, - followHeading, - defaultSettings, - allowUpdates = true, - triggerKey, - onUserTrackingModeChange, - } = props; - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const camera: React.RefObject = useRef(null); - - /** - * Map camera transitions to fit provided bounds - * - * @example - * camera.fitBounds([lng, lat], [lng, lat]) - * camera.fitBounds([lng, lat], [lng, lat], 20, 1000) // padding for all sides - * camera.fitBounds([lng, lat], [lng, lat], [verticalPadding, horizontalPadding], 1000) - * camera.fitBounds([lng, lat], [lng, lat], [top, right, bottom, left], 1000) - * - * @param {Array} northEastCoordinates - North east coordinate of bound - * @param {Array} southWestCoordinates - South west coordinate of bound - * @param {Number=} padding - Camera padding for bound - * @param {Number=} animationDuration - Duration of camera animation - * @return {void} - */ - // const fitBounds = ( - // northEastCoordinates: Position, - // southWestCoordinates: Position, - // padding = 0, - // animationDuration = 0.0, - // ) => { - // const pad = { - // paddingLeft: 0, - // paddingRight: 0, - // paddingTop: 0, - // paddingBottom: 0, - // }; - - // if (Array.isArray(padding)) { - // if (padding.length === 2) { - // pad.paddingTop = padding[0]; - // pad.paddingBottom = padding[0]; - // pad.paddingLeft = padding[1]; - // pad.paddingRight = padding[1]; - // } else if (padding.length === 4) { - // pad.paddingTop = padding[0]; - // pad.paddingRight = padding[1]; - // pad.paddingBottom = padding[2]; - // pad.paddingLeft = padding[3]; - // } - // } else { - // pad.paddingLeft = padding; - // pad.paddingRight = padding; - // pad.paddingTop = padding; - // pad.paddingBottom = padding; - // } - - // return setCamera({ - // bounds: { - // ne: northEastCoordinates, - // sw: southWestCoordinates, - // }, - // padding: pad, - // animationDuration, - // animationMode: Mode.Ease, - // }); - // }; - - /** - * Map camera will fly to new coordinate - * - * @example - * camera.flyTo([lng, lat]) - * camera.flyTo([lng, lat], 12000) - * - * @param {Array} coordinates - Coordinates that map camera will jump too - * @param {Number=} animationDuration - Duration of camera animation - * @return {void} - */ - // const flyTo = (coordinates: Position, animationDuration = 2000) => { - // return setCamera({ - // centerCoordinate: coordinates, - // animationDuration, - // animationMode: Mode.Flight, - // }); - // }; - - /** - * Map camera will move to new coordinate at the same zoom level - * - * @example - * camera.moveTo([lng, lat], 200) // eases camera to new location based on duration - * camera.moveTo([lng, lat]) // snaps camera to new location without any easing - * - * @param {Array} coordinates - Coordinates that map camera will move too - * @param {Number=} animationDuration - Duration of camera animation - * @return {void} - */ - // const moveTo = (coordinates: Position, animationDuration = 0) => { - // return setCamera({ - // centerCoordinate: coordinates, - // animationDuration, - // }); - // }; - - /** - * Map camera will zoom to specified level - * - * @example - * camera.zoomTo(16) - * camera.zoomTo(16, 100) - * - * @param {Number} zoomLevel - Zoom level that the map camera will animate too - * @param {Number=} animationDuration - Duration of camera animation - * @return {void} - */ - // const zoomTo = (zoomLevel: number, animationDuration = 2000) => { - // return setCamera({ - // zoomLevel, - // animationDuration, - // animationMode: Mode.Flight, - // }); - // }; - - /** - * Map camera will perform updates based on provided config. Advanced use only! - * - * @example - * camera.setCamera({ - * centerCoordinate: [lng, lat], - * zoomLevel: 16, - * animationDuration: 2000, - * }) - * - * camera.setCamera({ - * stops: [ - * { pitch: 45, animationDuration: 200 }, - * { heading: 180, animationDuration: 300 }, - * ] - * }) - * - * @param {Object} config - Camera configuration - */ - // const setCamera = (config: CameraStop) => { - // _setCamera(config); - // }; - - // const _setCamera = (config = {}) => { - // let cameraConfig = {}; - - // if (config.stops) { - // cameraConfig.stops = []; - - // for (const stop of config.stops) { - // cameraConfig.stops.push(_createStop(stop)); - // } - // } else { - // cameraConfig = _createStop(config); - // } - - // refs.camera.setNativeProps({ stop: cameraConfig }); - // } - - const nativeAnimationMode = useCallback((_mode: AnimationMode) => { - switch (_mode) { - case Mode.Flight: - return MapboxGL.CameraModes.Flight; - case Mode.Ease: - return MapboxGL.CameraModes.Ease; - case Mode.Linear: - return MapboxGL.CameraModes.Linear; - case Mode.None: - return MapboxGL.CameraModes.None; - case Mode.Move: - default: - return MapboxGL.CameraModes.Move; - } - }, []); - - const buildNativeStop = useCallback( - ( - stop: CameraStop, - ignoreFollowUserLocation = false, - ): NativeCameraStop | null => { - if (props.followUserLocation && !ignoreFollowUserLocation) { +const Camera = memo( + forwardRef((props: CameraProps, ref) => { + const { + centerCoordinate, + bounds, + heading, + pitch, + zoomLevel, + padding, + animationDuration, + animationMode, + minZoomLevel, + maxZoomLevel, + maxBounds, + followUserLocation, + followUserMode, + followZoomLevel, + followPitch, + followHeading, + defaultSettings, + allowUpdates = true, + triggerKey, + onUserTrackingModeChange, + } = props; + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const camera: React.RefObject = useRef(null); + + /** + * Map camera transitions to fit provided bounds + * + * @example + * camera.fitBounds([lng, lat], [lng, lat]) + * camera.fitBounds([lng, lat], [lng, lat], 20, 1000) // padding for all sides + * camera.fitBounds([lng, lat], [lng, lat], [verticalPadding, horizontalPadding], 1000) + * camera.fitBounds([lng, lat], [lng, lat], [top, right, bottom, left], 1000) + * + * @param {Array} northEastCoordinates - North east coordinate of bound + * @param {Array} southWestCoordinates - South west coordinate of bound + * @param {Number=} padding - Camera padding for bound + * @param {Number=} animationDuration - Duration of camera animation + * @return {void} + */ + // const fitBounds = ( + // northEastCoordinates: Position, + // southWestCoordinates: Position, + // padding = 0, + // animationDuration = 0.0, + // ) => { + // const pad = { + // paddingLeft: 0, + // paddingRight: 0, + // paddingTop: 0, + // paddingBottom: 0, + // }; + + // if (Array.isArray(padding)) { + // if (padding.length === 2) { + // pad.paddingTop = padding[0]; + // pad.paddingBottom = padding[0]; + // pad.paddingLeft = padding[1]; + // pad.paddingRight = padding[1]; + // } else if (padding.length === 4) { + // pad.paddingTop = padding[0]; + // pad.paddingRight = padding[1]; + // pad.paddingBottom = padding[2]; + // pad.paddingLeft = padding[3]; + // } + // } else { + // pad.paddingLeft = padding; + // pad.paddingRight = padding; + // pad.paddingTop = padding; + // pad.paddingBottom = padding; + // } + + // return setCamera({ + // bounds: { + // ne: northEastCoordinates, + // sw: southWestCoordinates, + // }, + // padding: pad, + // animationDuration, + // animationMode: Mode.Ease, + // }); + // }; + + /** + * Map camera will fly to new coordinate + * + * @example + * camera.flyTo([lng, lat]) + * camera.flyTo([lng, lat], 12000) + * + * @param {Array} coordinates - Coordinates that map camera will jump too + * @param {Number=} animationDuration - Duration of camera animation + * @return {void} + */ + // const flyTo = (coordinates: Position, animationDuration = 2000) => { + // return setCamera({ + // centerCoordinate: coordinates, + // animationDuration, + // animationMode: Mode.Flight, + // }); + // }; + + /** + * Map camera will move to new coordinate at the same zoom level + * + * @example + * camera.moveTo([lng, lat], 200) // eases camera to new location based on duration + * camera.moveTo([lng, lat]) // snaps camera to new location without any easing + * + * @param {Array} coordinates - Coordinates that map camera will move too + * @param {Number=} animationDuration - Duration of camera animation + * @return {void} + */ + // const moveTo = (coordinates: Position, animationDuration = 0) => { + // return setCamera({ + // centerCoordinate: coordinates, + // animationDuration, + // }); + // }; + + /** + * Map camera will zoom to specified level + * + * @example + * camera.zoomTo(16) + * camera.zoomTo(16, 100) + * + * @param {Number} zoomLevel - Zoom level that the map camera will animate too + * @param {Number=} animationDuration - Duration of camera animation + * @return {void} + */ + // const zoomTo = (zoomLevel: number, animationDuration = 2000) => { + // return setCamera({ + // zoomLevel, + // animationDuration, + // animationMode: Mode.Flight, + // }); + // }; + + /** + * Map camera will perform updates based on provided config. Advanced use only! + * + * @example + * camera.setCamera({ + * centerCoordinate: [lng, lat], + * zoomLevel: 16, + * animationDuration: 2000, + * }) + * + * camera.setCamera({ + * stops: [ + * { pitch: 45, animationDuration: 200 }, + * { heading: 180, animationDuration: 300 }, + * ] + * }) + * + * @param {Object} config - Camera configuration + */ + // const setCamera = (config: CameraStop) => { + // _setCamera(config); + // }; + + const nativeAnimationMode = useCallback( + (_mode?: AnimationMode): NativeAnimationMode | undefined => { + switch (_mode) { + case Mode.Flight: + return MapboxGL.CameraModes.Flight; + case Mode.Ease: + return MapboxGL.CameraModes.Ease; + case Mode.Linear: + return MapboxGL.CameraModes.Linear; + case Mode.None: + return MapboxGL.CameraModes.None; + case Mode.Move: + return MapboxGL.CameraModes.Move; + default: + return undefined; + } + }, + [], + ); + + const nativeDefaultStop = useMemo((): NativeCameraStop | null => { + if (!defaultSettings) { return null; } - - const stopConfig: NativeCameraStop = { - mode: nativeAnimationMode(stop.animationMode), - pitch: stop.pitch, - heading: stop.heading, - duration: stop.animationDuration ?? 0, - zoom: stop.zoomLevel, + const _defaultStop: NativeCameraStop = { + centerCoordinate: JSON.stringify(defaultSettings.centerCoordinate), + bounds: JSON.stringify(defaultSettings.bounds), + heading: defaultSettings.heading ?? 0, + pitch: defaultSettings.pitch ?? 0, + zoom: defaultSettings.zoomLevel ?? 11, + paddingTop: defaultSettings.padding?.paddingTop ?? 0, + paddingBottom: defaultSettings.padding?.paddingBottom ?? 0, + paddingLeft: defaultSettings.padding?.paddingLeft ?? 0, + paddingRight: defaultSettings.padding?.paddingRight ?? 0, + duration: defaultSettings.animationDuration ?? 2000, + mode: nativeAnimationMode(defaultSettings.animationMode) ?? 'flight', }; - - if (stop.centerCoordinate) { - stopConfig.centerCoordinate = JSON.stringify( - geoUtils.makePoint(stop.centerCoordinate), - ); - } - - if (stop.bounds && stop.bounds.ne && stop.bounds.sw) { - const { ne, sw } = stop.bounds; - stopConfig.bounds = JSON.stringify(geoUtils.makeLatLngBounds(ne, sw)); - } - - stopConfig.paddingTop = - stop.padding?.paddingTop ?? stop.bounds?.paddingTop ?? 0; - stopConfig.paddingRight = - stop.padding?.paddingRight ?? stop.bounds?.paddingRight ?? 0; - stopConfig.paddingBottom = - stop.padding?.paddingBottom ?? stop.bounds?.paddingBottom ?? 0; - stopConfig.paddingLeft = - stop.padding?.paddingLeft ?? stop.bounds?.paddingLeft ?? 0; - - return stopConfig; - }, - [props.followUserLocation, nativeAnimationMode], - ); - - const nativeMaxBounds = useMemo(() => { - if (!maxBounds?.ne || !maxBounds?.sw) { - return null; - } - return JSON.stringify( - geoUtils.makeLatLngBounds(maxBounds.ne, maxBounds.sw), + return _defaultStop; + }, [defaultSettings, nativeAnimationMode]); + + const buildNativeStop = useCallback( + ( + stop: CameraStop, + ignoreFollowUserLocation = false, + ): NativeCameraStop | null => { + stop = { + ...stop, + type: 'CameraStop', + }; + + if (props.followUserLocation && !ignoreFollowUserLocation) { + return null; + } + + const _nativeStop: NativeCameraStop = { ...nativeDefaultStop }; + + if (stop.pitch !== undefined) _nativeStop.pitch = stop.pitch; + if (stop.heading !== undefined) _nativeStop.heading = stop.heading; + if (stop.zoomLevel !== undefined) _nativeStop.zoom = stop.zoomLevel; + if (stop.animationMode !== undefined) + _nativeStop.mode = nativeAnimationMode(stop.animationMode); + if (stop.animationDuration !== undefined) + _nativeStop.duration = stop.animationDuration; + + if (stop.centerCoordinate) { + _nativeStop.centerCoordinate = JSON.stringify( + geoUtils.makePoint(stop.centerCoordinate), + ); + } + + if (stop.bounds && stop.bounds.ne && stop.bounds.sw) { + const { ne, sw } = stop.bounds; + _nativeStop.bounds = JSON.stringify( + geoUtils.makeLatLngBounds(ne, sw), + ); + } + + _nativeStop.paddingTop = + stop.padding?.paddingTop ?? stop.bounds?.paddingTop ?? 0; + _nativeStop.paddingRight = + stop.padding?.paddingRight ?? stop.bounds?.paddingRight ?? 0; + _nativeStop.paddingBottom = + stop.padding?.paddingBottom ?? stop.bounds?.paddingBottom ?? 0; + _nativeStop.paddingLeft = + stop.padding?.paddingLeft ?? stop.bounds?.paddingLeft ?? 0; + + return _nativeStop; + }, + [props.followUserLocation, nativeDefaultStop, nativeAnimationMode], ); - }, [maxBounds]); - const nativeStop = useMemo(() => { - return buildNativeStop({ + const nativeStop = useMemo(() => { + return buildNativeStop({ + type: 'CameraStop', + centerCoordinate, + bounds, + heading, + pitch, + zoomLevel, + padding, + animationDuration, + animationMode, + }); + }, [ centerCoordinate, bounds, heading, @@ -353,44 +386,74 @@ const Camera = memo((props: CameraProps) => { padding, animationDuration, animationMode, - }); - }, [ - centerCoordinate, - bounds, - heading, - pitch, - zoomLevel, - padding, - animationDuration, - animationMode, - buildNativeStop, - ]); - - const nativeDefaultStop = useMemo((): NativeCameraStop | null => { - if (!defaultSettings) { - return null; - } - return buildNativeStop(defaultSettings); - }, [defaultSettings, buildNativeStop]); - - return ( - - ); -}); + buildNativeStop, + ]); + + const nativeMaxBounds = useMemo(() => { + if (!maxBounds?.ne || !maxBounds?.sw) { + return null; + } + return JSON.stringify( + geoUtils.makeLatLngBounds(maxBounds.ne, maxBounds.sw), + ); + }, [maxBounds]); + + const setCamera = useCallback( + (config: CameraStop | CameraStops) => { + if (!config.type) + config = { + // @ts-expect-error The compiler doesn't understand that the `config` union type is guaranteed + // to be an object type + ...config, + // @ts-expect-error Allows JS files to pass in an invalid config (lacking the `type` property), + // which would raise a compilation error in TS files. + type: config.stops ? 'CameraStops' : 'CameraStop', + }; + + if (config.type === 'CameraStops') { + for (const _stop of config.stops) { + let _nativeStops: NativeCameraStop[] = []; + const _nativeStop = buildNativeStop(_stop); + if (_nativeStop) { + _nativeStops = [..._nativeStops, _nativeStop]; + } + camera.current.setNativeProps({ + stop: { stops: _nativeStops }, + }); + } + } else if (config.type === 'CameraStop') { + const _nativeStop = buildNativeStop(config); + if (_nativeStop) { + camera.current.setNativeProps({ stop: _nativeStop }); + } + } + }, + [buildNativeStop], + ); + + useImperativeHandle(ref, () => ({ + setCamera, + })); + + return ( + + ); + }), +); interface NativeCameraProps extends CameraFollowConfig { testID: string; @@ -405,17 +468,19 @@ interface NativeCameraProps extends CameraFollowConfig { interface NativeCameraStop { centerCoordinate?: string; bounds?: string; - heading: number; - pitch: number; - zoom: number; + heading?: number; + pitch?: number; + zoom?: number; paddingLeft?: number; paddingRight?: number; paddingTop?: number; paddingBottom?: number; - duration: number; - mode: AnimationMode; + duration?: number; + mode?: NativeAnimationMode; } +type NativeAnimationMode = 'flight' | 'ease' | 'linear' | 'none' | 'move'; + const RCTMGLCamera = requireNativeComponent(NATIVE_MODULE_NAME); From 62cb5792bc0e11029da63a57156f74dbfaa9f58f Mon Sep 17 00:00:00 2001 From: Naftali Beder Date: Thu, 28 Apr 2022 10:56:38 -0500 Subject: [PATCH 18/84] Implement imperative methods, refactor and expand types --- ...CameraAnimation.js => CameraAnimation.tsx} | 0 javascript/components/Camera.tsx | 368 +++++++++--------- 2 files changed, 188 insertions(+), 180 deletions(-) rename example/src/examples/V10/{CameraAnimation.js => CameraAnimation.tsx} (100%) diff --git a/example/src/examples/V10/CameraAnimation.js b/example/src/examples/V10/CameraAnimation.tsx similarity index 100% rename from example/src/examples/V10/CameraAnimation.js rename to example/src/examples/V10/CameraAnimation.tsx diff --git a/javascript/components/Camera.tsx b/javascript/components/Camera.tsx index 122a3d01e..0bda6336d 100644 --- a/javascript/components/Camera.tsx +++ b/javascript/components/Camera.tsx @@ -6,8 +6,9 @@ import React, { useMemo, useRef, } from 'react'; -import { Position } from 'geojson'; import { NativeModules, requireNativeComponent } from 'react-native'; +import { Position } from 'geojson'; +import { MapboxGLEvent } from '@rnmapbox/maps'; import * as geoUtils from '../utils/geoUtils'; @@ -15,7 +16,7 @@ const MapboxGL = NativeModules.MGLModule; export const NATIVE_MODULE_NAME = 'RCTMGLCamera'; -const Mode = { +const Mode: Record = { Flight: 'flyTo', Ease: 'easeTo', Linear: 'linearTo', @@ -23,86 +24,132 @@ const Mode = { Move: 'moveTo', }; -export const UserTrackingModes = { +export const UserTrackingModes: Record = { Follow: 'normal', FollowWithHeading: 'compass', FollowWithCourse: 'course', }; -type AnimationMode = 'flyTo' | 'easeTo' | 'linearTo' | 'none' | 'moveTo'; +// Component types. + +export type AnimationMode = 'flyTo' | 'easeTo' | 'linearTo' | 'none' | 'moveTo'; + +type UserTrackingMode = 'normal' | 'compass' | 'course'; + +type UserTrackingModeChangeCallback = ( + event: MapboxGLEvent< + 'usertrackingmodechange', + { + followUserLocation: boolean; + followUserMode: UserTrackingMode | null; + } + >, +) => void; + +/** + * @param {Position} centerCoordinate + * @param {CameraBounds} bounds + * @param {number} heading + * @param {number} pitch + * @param {number} zoomLevel + * @param {CameraPadding} padding + * @param {number} animationDuration + * @param {AnimationMode} animationMode + * @param {boolean} followUserLocation + * @param {UserTrackingMode} followUserMode + * @param {number} followZoomLevel + * @param {number} followPitch + * @param {number} followHeading + * @param {number} minZoomLevel + * @param {number} maxZoomLevel + * @param {CameraBounds} maxBounds + */ +interface CameraProps + extends Omit, + CameraFollowConfig, + CameraMinMaxConfig { + defaultSettings?: CameraStop; + allowUpdates?: boolean; + triggerKey?: any; + onUserTrackingModeChange?: UserTrackingModeChangeCallback; +} interface CameraStop { readonly type: 'CameraStop'; - centerCoordinate: Position; - bounds: { - ne: Position; - sw: Position; - paddingLeft?: number; // deprecated - paddingRight?: number; // deprecated - paddingTop?: number; // deprecated - paddingBottom?: number; // deprecated - }; + centerCoordinate?: Position; + bounds?: CameraBoundsWithPadding; // With padding for backwards compatibility. heading?: number; pitch?: number; zoomLevel?: number; - padding?: { - paddingLeft: number; - paddingRight: number; - paddingTop: number; - paddingBottom: number; - }; + padding?: CameraPadding; animationDuration?: number; animationMode?: AnimationMode; } interface CameraFollowConfig { - followUserLocation: boolean; - followUserMode?: 'normal' | 'compass' | 'course'; + followUserLocation?: boolean; + followUserMode?: UserTrackingMode; followZoomLevel?: number; followPitch?: number; followHeading?: number; } interface CameraMinMaxConfig { - minZoomLevel: number; - maxZoomLevel: number; - maxBounds: { + minZoomLevel?: number; + maxZoomLevel?: number; + maxBounds?: { ne: Position; sw: Position; }; } +interface CameraBounds { + ne: Position; + sw: Position; +} + +interface CameraPadding { + paddingLeft: number; + paddingRight: number; + paddingTop: number; + paddingBottom: number; +} + +interface CameraBoundsWithPadding + extends CameraBounds, + Partial {} + interface CameraStops { readonly type: 'CameraStops'; stops: CameraStop[]; } -/** - * @param {Position} centerCoordinate - * @param {TODO} bounds - * @param {number} heading - * @param {number} pitch - * @param {number} zoomLevel - * @param {TODO} padding - * @param {number} animationDuration - * @param {AnimationMode} animationMode - * @param {boolean} followUserLocation - * @param {'normal' | 'compass' | 'course'} followUserMode - * @param {number} followZoomLevel - * @param {number} followPitch - * @param {number} followHeading - * @param {number} minZoomLevel - * @param {number} maxZoomLevel - * @param {TODO} maxBounds - */ -interface CameraProps - extends CameraStop, - CameraFollowConfig, - CameraMinMaxConfig { - defaultSettings?: CameraStop; - allowUpdates: boolean; - triggerKey: any; - onUserTrackingModeChange: () => void; +// Native module types. + +type NativeAnimationMode = 'flight' | 'ease' | 'linear' | 'none' | 'move'; + +interface NativeCameraProps extends CameraFollowConfig { + testID?: string; + stop: NativeCameraStop | null; + defaultStop?: NativeCameraStop | null; + minZoomLevel?: number; + maxZoomLevel?: number; + maxBounds?: string | null; + onUserTrackingModeChange?: UserTrackingModeChangeCallback; +} + +interface NativeCameraStop { + centerCoordinate?: string; + bounds?: string; + heading?: number; + pitch?: number; + zoom?: number; + paddingLeft?: number; + paddingRight?: number; + paddingTop?: number; + paddingBottom?: number; + duration?: number; + mode?: NativeAnimationMode; } /** @@ -138,141 +185,124 @@ const Camera = memo( const camera: React.RefObject = useRef(null); /** - * Map camera transitions to fit provided bounds + * Map camera transitions to fit provided bounds. * * @example * camera.fitBounds([lng, lat], [lng, lat]) - * camera.fitBounds([lng, lat], [lng, lat], 20, 1000) // padding for all sides + * camera.fitBounds([lng, lat], [lng, lat], 20, 1000) * camera.fitBounds([lng, lat], [lng, lat], [verticalPadding, horizontalPadding], 1000) * camera.fitBounds([lng, lat], [lng, lat], [top, right, bottom, left], 1000) * - * @param {Array} northEastCoordinates - North east coordinate of bound - * @param {Array} southWestCoordinates - South west coordinate of bound - * @param {Number=} padding - Camera padding for bound - * @param {Number=} animationDuration - Duration of camera animation - * @return {void} + * @param {Position} ne Northeast coordinate of bounds + * @param {Position} sw Southwest coordinate of bounds + * @param {number} paddingConfig Camera padding configuration + * @param {number} animationDuration Duration of camera animation */ - // const fitBounds = ( - // northEastCoordinates: Position, - // southWestCoordinates: Position, - // padding = 0, - // animationDuration = 0.0, - // ) => { - // const pad = { - // paddingLeft: 0, - // paddingRight: 0, - // paddingTop: 0, - // paddingBottom: 0, - // }; - - // if (Array.isArray(padding)) { - // if (padding.length === 2) { - // pad.paddingTop = padding[0]; - // pad.paddingBottom = padding[0]; - // pad.paddingLeft = padding[1]; - // pad.paddingRight = padding[1]; - // } else if (padding.length === 4) { - // pad.paddingTop = padding[0]; - // pad.paddingRight = padding[1]; - // pad.paddingBottom = padding[2]; - // pad.paddingLeft = padding[3]; - // } - // } else { - // pad.paddingLeft = padding; - // pad.paddingRight = padding; - // pad.paddingTop = padding; - // pad.paddingBottom = padding; - // } - - // return setCamera({ - // bounds: { - // ne: northEastCoordinates, - // sw: southWestCoordinates, - // }, - // padding: pad, - // animationDuration, - // animationMode: Mode.Ease, - // }); - // }; + const fitBounds = ( + ne: Position, + sw: Position, + paddingConfig: number | number[] = 0, + animationDuration = 0, + ) => { + let padding = { + paddingTop: 0, + paddingBottom: 0, + paddingLeft: 0, + paddingRight: 0, + }; + + if (Array.isArray(paddingConfig)) { + if (paddingConfig.length === 2) { + padding = { + paddingTop: paddingConfig[0], + paddingBottom: paddingConfig[0], + paddingLeft: paddingConfig[1], + paddingRight: paddingConfig[1], + }; + } else if (paddingConfig.length === 4) { + padding = { + paddingTop: paddingConfig[0], + paddingBottom: paddingConfig[2], + paddingLeft: paddingConfig[3], + paddingRight: paddingConfig[1], + }; + } + } else { + padding = { + paddingTop: paddingConfig, + paddingBottom: paddingConfig, + paddingLeft: paddingConfig, + paddingRight: paddingConfig, + }; + } + + return setCamera({ + type: 'CameraStop', + bounds: { + ne, + sw, + }, + padding, + animationDuration, + animationMode: 'easeTo', + }); + }; /** - * Map camera will fly to new coordinate + * Map camera will fly to new coordinate. * * @example * camera.flyTo([lng, lat]) * camera.flyTo([lng, lat], 12000) * - * @param {Array} coordinates - Coordinates that map camera will jump too - * @param {Number=} animationDuration - Duration of camera animation - * @return {void} + * @param {Position} centerCoordinate Coordinates that map camera will center around + * @param {number} animationDuration Duration of camera animation */ - // const flyTo = (coordinates: Position, animationDuration = 2000) => { - // return setCamera({ - // centerCoordinate: coordinates, - // animationDuration, - // animationMode: Mode.Flight, - // }); - // }; + const flyTo = (centerCoordinate: Position, animationDuration = 2000) => { + setCamera({ + type: 'CameraStop', + centerCoordinate, + animationDuration, + }); + }; /** - * Map camera will move to new coordinate at the same zoom level + * Map camera will move to new coordinate at the same zoom level. * * @example * camera.moveTo([lng, lat], 200) // eases camera to new location based on duration * camera.moveTo([lng, lat]) // snaps camera to new location without any easing * - * @param {Array} coordinates - Coordinates that map camera will move too - * @param {Number=} animationDuration - Duration of camera animation - * @return {void} + * @param {Position} centerCoordinate Coordinates that map camera will move too + * @param {number} animationDuration Duration of camera animation */ - // const moveTo = (coordinates: Position, animationDuration = 0) => { - // return setCamera({ - // centerCoordinate: coordinates, - // animationDuration, - // }); - // }; + const moveTo = (centerCoordinate: Position, animationDuration = 0) => { + setCamera({ + type: 'CameraStop', + centerCoordinate, + animationDuration, + animationMode: 'easeTo', + }); + }; /** - * Map camera will zoom to specified level + * Map camera will zoom to specified level. * * @example * camera.zoomTo(16) * camera.zoomTo(16, 100) * - * @param {Number} zoomLevel - Zoom level that the map camera will animate too - * @param {Number=} animationDuration - Duration of camera animation - * @return {void} + * @param {number} zoomLevel Zoom level that the map camera will animate too + * @param {number} animationDuration Duration of camera animation */ - // const zoomTo = (zoomLevel: number, animationDuration = 2000) => { - // return setCamera({ - // zoomLevel, - // animationDuration, - // animationMode: Mode.Flight, - // }); - // }; - - /** - * Map camera will perform updates based on provided config. Advanced use only! - * - * @example - * camera.setCamera({ - * centerCoordinate: [lng, lat], - * zoomLevel: 16, - * animationDuration: 2000, - * }) - * - * camera.setCamera({ - * stops: [ - * { pitch: 45, animationDuration: 200 }, - * { heading: 180, animationDuration: 300 }, - * ] - * }) - * - * @param {Object} config - Camera configuration - */ - // const setCamera = (config: CameraStop) => { - // _setCamera(config); - // }; + const zoomTo = (zoomLevel: number, animationDuration = 2000) => { + return setCamera({ + type: 'CameraStop', + zoomLevel, + animationDuration, + animationMode: 'flyTo', + }); + }; const nativeAnimationMode = useCallback( (_mode?: AnimationMode): NativeAnimationMode | undefined => { @@ -433,6 +463,10 @@ const Camera = memo( useImperativeHandle(ref, () => ({ setCamera, + fitBounds, + flyTo, + moveTo, + zoomTo, })); return ( @@ -455,32 +489,6 @@ const Camera = memo( }), ); -interface NativeCameraProps extends CameraFollowConfig { - testID: string; - stop: NativeCameraStop | null; - defaultStop: NativeCameraStop | null; - minZoomLevel: number; - maxZoomLevel: number; - maxBounds: string | null; - onUserTrackingModeChange: () => void; -} - -interface NativeCameraStop { - centerCoordinate?: string; - bounds?: string; - heading?: number; - pitch?: number; - zoom?: number; - paddingLeft?: number; - paddingRight?: number; - paddingTop?: number; - paddingBottom?: number; - duration?: number; - mode?: NativeAnimationMode; -} - -type NativeAnimationMode = 'flight' | 'ease' | 'linear' | 'none' | 'move'; - const RCTMGLCamera = requireNativeComponent(NATIVE_MODULE_NAME); From 3e0b987977f45a89606d4a2ac531e47239e28d30 Mon Sep 17 00:00:00 2001 From: Naftali Beder Date: Thu, 28 Apr 2022 10:57:15 -0500 Subject: [PATCH 19/84] Update camera imports and exports --- index.d.ts | 72 ++++++++------------------------------------- javascript/index.js | 8 ++++- 2 files changed, 19 insertions(+), 61 deletions(-) diff --git a/index.d.ts b/index.d.ts index b6b5f2e04..29bdcba05 100644 --- a/index.d.ts +++ b/index.d.ts @@ -240,18 +240,18 @@ declare namespace MapboxGL { } type Padding = number | [number, number] | [number, number, number, number]; - class Camera extends Component { - fitBounds( - northEastCoordinates: GeoJSON.Position, - southWestCoordinates: GeoJSON.Position, - padding?: Padding, - duration?: number, - ): void; - flyTo(coordinates: GeoJSON.Position, duration?: number): void; - moveTo(coordinates: GeoJSON.Position, duration?: number): void; - zoomTo(zoomLevel: number, duration?: number): void; - setCamera(config: CameraSettings): void; - } + // class Camera extends Component { + // fitBounds( + // northEastCoordinates: GeoJSON.Position, + // southWestCoordinates: GeoJSON.Position, + // padding?: Padding, + // duration?: number, + // ): void; + // flyTo(coordinates: GeoJSON.Position, duration?: number): void; + // moveTo(coordinates: GeoJSON.Position, duration?: number): void; + // zoomTo(zoomLevel: number, duration?: number): void; + // setCamera(config: CameraSettings): void; + // } class UserLocation extends Component {} @@ -574,54 +574,6 @@ export interface MapViewProps extends ViewProps { onUserTrackingModeChange?: () => void; } -export interface CameraProps extends CameraSettings, ViewProps { - allowUpdates?: boolean; - animationDuration?: number; - animationMode?: 'flyTo' | 'easeTo' | 'linearTo' | 'moveTo'; - defaultSettings?: CameraSettings; - minZoomLevel?: number; - maxZoomLevel?: number; - maxBounds?: { ne: [number, number]; sw: [number, number] }; - followUserLocation?: boolean; - followUserMode?: 'normal' | 'compass' | 'course'; - followZoomLevel?: number; - followPitch?: number; - followHeading?: number; - triggerKey?: any; - alignment?: number[]; - onUserTrackingModeChange?: ( - event: MapboxGLEvent< - 'usertrackingmodechange', - { - followUserLocation: boolean; - followUserMode: 'normal' | 'compass' | 'course' | null; - } - >, - ) => void; -} - -export interface CameraPadding { - paddingLeft?: number; - paddingRight?: number; - paddingTop?: number; - paddingBottom?: number; -} - -export interface CameraSettings { - centerCoordinate?: GeoJSON.Position; - heading?: number; - pitch?: number; - padding?: CameraPadding; - bounds?: CameraPadding & { - ne: GeoJSON.Position; - sw: GeoJSON.Position; - }; - zoomLevel?: number; - animationDuration?: number; - animationMode?: 'flyTo' | 'easeTo' | 'linearTo' | 'moveTo'; - stops?: CameraSettings[]; -} - export interface UserLocationProps { androidRenderMode?: 'normal' | 'compass' | 'gps'; animated?: boolean; diff --git a/javascript/index.js b/javascript/index.js index 42c9244f3..22e1dddd4 100644 --- a/javascript/index.js +++ b/javascript/index.js @@ -7,7 +7,11 @@ import PointAnnotation from './components/PointAnnotation'; import Annotation from './components/annotations/Annotation'; import Callout from './components/Callout'; import UserLocation from './components/UserLocation'; -import Camera, { UserTrackingModes } from './components/Camera'; +import Camera, { + CameraProps, + UserTrackingModes, + AnimationMode, +} from './components/Camera'; import VectorSource from './components/VectorSource'; import ShapeSource from './components/ShapeSource'; import RasterSource from './components/RasterSource'; @@ -126,6 +130,8 @@ export { Callout, UserLocation, Camera, + CameraProps, + AnimationMode, Annotation, MarkerView, VectorSource, From 7682ee47d363d1883370bfa1914603cfe453d14f Mon Sep 17 00:00:00 2001 From: Naftali Beder Date: Thu, 28 Apr 2022 10:58:15 -0500 Subject: [PATCH 20/84] Refactor and add types --- example/src/examples/V10/CameraAnimation.tsx | 118 ++++++++++--------- 1 file changed, 60 insertions(+), 58 deletions(-) diff --git a/example/src/examples/V10/CameraAnimation.tsx b/example/src/examples/V10/CameraAnimation.tsx index c5d988977..9fdc98010 100644 --- a/example/src/examples/V10/CameraAnimation.tsx +++ b/example/src/examples/V10/CameraAnimation.tsx @@ -1,28 +1,20 @@ import React, { useMemo, useState } from 'react'; -import { Button, SafeAreaView, View } from 'react-native'; -import { - MapView, - Camera, - ShapeSource, - CircleLayer, - Logger, -} from '@rnmapbox/maps'; +import { Button, SafeAreaView, StyleSheet, View } from 'react-native'; +import MapboxGL from '@rnmapbox/maps'; +const { MapView, ShapeSource, CircleLayer } = MapboxGL; import bbox from '@turf/bbox'; +import { Feature, LineString, Point, Position } from '@turf/helpers'; import { Text, Divider } from 'react-native-elements'; +import { Camera, CameraProps, AnimationMode } from '../../../../javascript'; import Page from '../common/Page'; import colors from '../../styles/colors'; -Logger.setLogLevel('verbose'); - -const styles = { +const styles = StyleSheet.create({ map: { flex: 1, }, - circle: { - circleRadius: 6, - circleColor: colors.primary.blue, - }, + sheet: { paddingTop: 10, paddingHorizontal: 10, @@ -41,6 +33,13 @@ const styles = { fadedText: { color: 'gray', }, +}); + +const mapStyles = { + circle: { + circleRadius: 6, + circleColor: colors.primary.blue, + }, }; const zeroPadding = { @@ -72,76 +71,78 @@ const randPadding = () => { }; }; -const toPosition = (coordinate) => { - return [coordinate.longitude, coordinate.latitude]; -}; - -const CameraAnimation = (props) => { - const initialCoordinate = { - latitude: 40.759211, - longitude: -73.984638, - }; +const CameraAnimation = (props: any) => { + const initialPosition = [-73.984638, 40.759211]; - const [animationMode, setAnimationMode] = useState('moveTo'); - const [coordinates, setCoordinates] = useState([initialCoordinate]); + const [animationMode, setAnimationMode] = useState('moveTo'); + const [positions, setPositions] = useState([initialPosition]); const [padding, setPadding] = useState(zeroPadding); const paddingDisplay = useMemo(() => { return `L ${padding.paddingLeft} | R ${padding.paddingRight} | T ${padding.paddingTop} | B ${padding.paddingBottom}`; }, [padding]); - const move = (_animationMode, shouldCreateMultiple) => { + const move = ( + _animationMode: AnimationMode, + shouldCreateMultiple: boolean, + ) => { setAnimationMode(_animationMode); if (shouldCreateMultiple) { - const _centerCoordinate = { - latitude: initialCoordinate.latitude + Math.random() * 0.2, - longitude: initialCoordinate.longitude + Math.random() * 0.2, - }; - const _coordinates = Array(10) + const _centerPosition = [ + initialPosition[0] + Math.random() * 0.2, + initialPosition[1] + Math.random() * 0.2, + ]; + const _positions = Array(10) .fill(0) .map((_) => { - return { - latitude: _centerCoordinate.latitude + Math.random() * 0.2, - longitude: _centerCoordinate.longitude + Math.random() * 0.2, - }; + return [ + _centerPosition[0] + Math.random() * 0.2, + _centerPosition[1] + Math.random() * 0.2, + ]; }); - setCoordinates(_coordinates); + setPositions(_positions); } else { - setCoordinates([ - { - latitude: initialCoordinate.latitude + Math.random() * 0.2, - longitude: initialCoordinate.longitude + Math.random() * 0.2, - }, + setPositions([ + [ + initialPosition[0] + Math.random() * 0.2, + initialPosition[1] + Math.random() * 0.2, + ], ]); } }; - const features = useMemo(() => { - return coordinates.map((p) => { + const features = useMemo((): Feature[] => { + return positions.map((p: Position) => { return { type: 'Feature', geometry: { type: 'Point', - coordinates: toPosition(p), + coordinates: p, }, + properties: {}, }; }); - }, [coordinates]); + }, [positions]); - const centerOrBounds = useMemo(() => { - if (coordinates.length === 1) { + const centerOrBounds = useMemo((): Pick< + CameraProps, + 'centerCoordinate' | 'bounds' + > => { + if (positions.length === 1) { return { - centerCoordinate: toPosition(coordinates[0]), + centerCoordinate: positions[0], }; } else { - const positions = coordinates.map(toPosition); - const lineString = { + console.log('pos:', positions); + + const lineString: Feature = { type: 'Feature', geometry: { type: 'LineString', coordinates: positions, }, + properties: {}, }; const _bbox = bbox(lineString); return { @@ -151,19 +152,20 @@ const CameraAnimation = (props) => { }, }; } - }, [coordinates]); + }, [positions]); const locationDisplay = useMemo(() => { - if (coordinates.length > 1) { + if (positions.length > 1) { const ne = centerOrBounds.bounds?.ne.map((n) => n.toFixed(3)); const sw = centerOrBounds.bounds?.sw.map((n) => n.toFixed(3)); return `ne ${ne} | sw ${sw}`; - } else if (coordinates.length === 1) { - const lon = coordinates[0].longitude.toFixed(4); - const lat = coordinates[0].latitude.toFixed(4); + } else if (positions.length === 1) { + const position = positions[0]; + const lon = position[0].toFixed(4); + const lat = position[1].toFixed(4); return `lon ${lon} | lat ${lat}`; } - }, [coordinates, centerOrBounds]); + }, [positions, centerOrBounds]); return ( @@ -182,7 +184,7 @@ const CameraAnimation = (props) => { const id = JSON.stringify(f.geometry.coordinates); return ( - + ); })} From 735341207ede5d30c46aec6df59700f302665cdb Mon Sep 17 00:00:00 2001 From: Naftali Beder Date: Thu, 28 Apr 2022 11:09:00 -0500 Subject: [PATCH 21/84] Clean up --- example/src/examples/V10/CameraAnimation.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/example/src/examples/V10/CameraAnimation.tsx b/example/src/examples/V10/CameraAnimation.tsx index 9fdc98010..8a0d0ded4 100644 --- a/example/src/examples/V10/CameraAnimation.tsx +++ b/example/src/examples/V10/CameraAnimation.tsx @@ -132,6 +132,7 @@ const CameraAnimation = (props: any) => { if (positions.length === 1) { return { centerCoordinate: positions[0], + bounds: undefined, }; } else { console.log('pos:', positions); @@ -146,6 +147,7 @@ const CameraAnimation = (props: any) => { }; const _bbox = bbox(lineString); return { + centerCoordinate: undefined, bounds: { ne: [_bbox[0], _bbox[1]], sw: [_bbox[2], _bbox[3]], @@ -156,13 +158,13 @@ const CameraAnimation = (props: any) => { const locationDisplay = useMemo(() => { if (positions.length > 1) { - const ne = centerOrBounds.bounds?.ne.map((n) => n.toFixed(3)); - const sw = centerOrBounds.bounds?.sw.map((n) => n.toFixed(3)); + const ne = centerOrBounds.bounds?.ne.map((n: number) => n.toFixed(3)); + const sw = centerOrBounds.bounds?.sw.map((n: number) => n.toFixed(3)); return `ne ${ne} | sw ${sw}`; } else if (positions.length === 1) { - const position = positions[0]; - const lon = position[0].toFixed(4); - const lat = position[1].toFixed(4); + const [first] = positions; + const lon = first[0].toFixed(4); + const lat = first[1].toFixed(4); return `lon ${lon} | lat ${lat}`; } }, [positions, centerOrBounds]); From 7f1df7d5514a46b374fb2d001ef2e777ce58692d Mon Sep 17 00:00:00 2001 From: Naftali Beder Date: Thu, 28 Apr 2022 11:09:29 -0500 Subject: [PATCH 22/84] Remove log --- example/src/examples/V10/CameraAnimation.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/example/src/examples/V10/CameraAnimation.tsx b/example/src/examples/V10/CameraAnimation.tsx index 8a0d0ded4..bb04fd256 100644 --- a/example/src/examples/V10/CameraAnimation.tsx +++ b/example/src/examples/V10/CameraAnimation.tsx @@ -135,8 +135,6 @@ const CameraAnimation = (props: any) => { bounds: undefined, }; } else { - console.log('pos:', positions); - const lineString: Feature = { type: 'Feature', geometry: { From 1c3297de3d15c18bf2c355215c92d499abdbf8f0 Mon Sep 17 00:00:00 2001 From: Naftali Beder Date: Thu, 28 Apr 2022 13:15:32 -0500 Subject: [PATCH 23/84] Provide camera ref types and comments --- javascript/components/Camera.tsx | 165 ++++++++++++++++++++----------- 1 file changed, 106 insertions(+), 59 deletions(-) diff --git a/javascript/components/Camera.tsx b/javascript/components/Camera.tsx index 0bda6336d..df6dec4bb 100644 --- a/javascript/components/Camera.tsx +++ b/javascript/components/Camera.tsx @@ -75,7 +75,7 @@ interface CameraProps } interface CameraStop { - readonly type: 'CameraStop'; + readonly type?: 'CameraStop'; centerCoordinate?: Position; bounds?: CameraBoundsWithPadding; // With padding for backwards compatibility. heading?: number; @@ -152,11 +152,93 @@ interface NativeCameraStop { mode?: NativeAnimationMode; } +export interface CameraRef { + /** + * Sets any camera properties, with default fallbacks if unspecified. + */ + setCamera: (config: CameraStop | CameraStops) => void; + + /** + * Set the camera position to enclose the provided bounds, with optional + * padding and duration. + * + * @example + * camera.fitBounds([lon, lat], [lon, lat]); + * camera.fitBounds([lon, lat], [lon, lat], 20, 1000); + * camera.fitBounds([lon, lat], [lon, lat], [verticalPadding, horizontalPadding], 1000); + * camera.fitBounds([lon, lat], [lon, lat], [top, right, bottom, left], 1000); + * + * @param {Position} ne Northeast coordinate of bounding box + * @param {Position} sw Southwest coordinate of bounding box + * @param {number} paddingConfig + * @param {number} animationDuration + */ + fitBounds: ( + ne: Position, + sw: Position, + paddingConfig?: number | number[], + animationDuration?: number, + ) => void; + + /** + * Sets the camera to center around the provided coordinate using a realistic 'travel' + * animation, with optional duration. + * + * @example + * camera.flyTo([lon, lat]); + * camera.flyTo([lon, lat], 12000); + * + * @param {Position} centerCoordinate + * @param {number} animationDuration + */ + flyTo: (centerCoordinate: Position, animationDuration?: number) => void; + + /** + * Sets the camera to center around the provided coordinate, with optional duration. + * + * @example + * camera.moveTo([lon, lat], 200); + * camera.moveTo([lon, lat]); + * + * @param {Position} centerCoordinate + * @param {number} animationDuration + */ + moveTo: (centerCoordinate: Position, animationDuration?: number) => void; + + /** + * Zooms the camera to the provided level, with optional duration. + * + * @example + * camera.zoomTo(16); + * camera.zoomTo(16, 100); + * + * @param {number} zoomLevel + * @param {number} animationDuration + */ + zoomTo: (zoomLevel: number, animationDuration?: number) => void; +} + /** * @param {CameraProps} props + * + * @example + * To use imperative methods, pass in a ref object: + * ``` + * const camera = useRef(null); + * + * useEffect(() => { + * camera.current?.setCamera({ + * centerCoordinate: [lon, lat], + * }); + * }, []); + * + * return ( + * + * ); + * ``` */ const Camera = memo( - forwardRef((props: CameraProps, ref) => { + forwardRef((props: CameraProps, ref: React.ForwardedRef) => { const { centerCoordinate, bounds, @@ -184,24 +266,10 @@ const Camera = memo( // @ts-ignore const camera: React.RefObject = useRef(null); - /** - * Map camera transitions to fit provided bounds. - * - * @example - * camera.fitBounds([lng, lat], [lng, lat]) - * camera.fitBounds([lng, lat], [lng, lat], 20, 1000) - * camera.fitBounds([lng, lat], [lng, lat], [verticalPadding, horizontalPadding], 1000) - * camera.fitBounds([lng, lat], [lng, lat], [top, right, bottom, left], 1000) - * - * @param {Position} ne Northeast coordinate of bounds - * @param {Position} sw Southwest coordinate of bounds - * @param {number} paddingConfig Camera padding configuration - * @param {number} animationDuration Duration of camera animation - */ - const fitBounds = ( - ne: Position, - sw: Position, - paddingConfig: number | number[] = 0, + const fitBounds: CameraRef['fitBounds'] = ( + ne, + sw, + paddingConfig = 0, animationDuration = 0, ) => { let padding = { @@ -236,7 +304,7 @@ const Camera = memo( }; } - return setCamera({ + setCamera({ type: 'CameraStop', bounds: { ne, @@ -248,17 +316,10 @@ const Camera = memo( }); }; - /** - * Map camera will fly to new coordinate. - * - * @example - * camera.flyTo([lng, lat]) - * camera.flyTo([lng, lat], 12000) - * - * @param {Position} centerCoordinate Coordinates that map camera will center around - * @param {number} animationDuration Duration of camera animation - */ - const flyTo = (centerCoordinate: Position, animationDuration = 2000) => { + const flyTo: CameraRef['flyTo'] = ( + centerCoordinate, + animationDuration = 2000, + ) => { setCamera({ type: 'CameraStop', centerCoordinate, @@ -266,17 +327,10 @@ const Camera = memo( }); }; - /** - * Map camera will move to new coordinate at the same zoom level. - * - * @example - * camera.moveTo([lng, lat], 200) // eases camera to new location based on duration - * camera.moveTo([lng, lat]) // snaps camera to new location without any easing - * - * @param {Position} centerCoordinate Coordinates that map camera will move too - * @param {number} animationDuration Duration of camera animation - */ - const moveTo = (centerCoordinate: Position, animationDuration = 0) => { + const moveTo: CameraRef['moveTo'] = ( + centerCoordinate, + animationDuration = 0, + ) => { setCamera({ type: 'CameraStop', centerCoordinate, @@ -285,18 +339,11 @@ const Camera = memo( }); }; - /** - * Map camera will zoom to specified level. - * - * @example - * camera.zoomTo(16) - * camera.zoomTo(16, 100) - * - * @param {number} zoomLevel Zoom level that the map camera will animate too - * @param {number} animationDuration Duration of camera animation - */ - const zoomTo = (zoomLevel: number, animationDuration = 2000) => { - return setCamera({ + const zoomTo: CameraRef['zoomTo'] = ( + zoomLevel, + animationDuration = 2000, + ) => { + setCamera({ type: 'CameraStop', zoomLevel, animationDuration, @@ -428,12 +475,12 @@ const Camera = memo( ); }, [maxBounds]); - const setCamera = useCallback( - (config: CameraStop | CameraStops) => { + const setCamera: CameraRef['setCamera'] = useCallback( + (config) => { if (!config.type) + // @ts-expect-error The compiler doesn't understand that the `config` union type is guaranteed + // to be an object type. config = { - // @ts-expect-error The compiler doesn't understand that the `config` union type is guaranteed - // to be an object type ...config, // @ts-expect-error Allows JS files to pass in an invalid config (lacking the `type` property), // which would raise a compilation error in TS files. From 6c4decd8bd4735475ba9ee4d5b49ec9197a70264 Mon Sep 17 00:00:00 2001 From: Naftali Beder Date: Thu, 28 Apr 2022 13:16:00 -0500 Subject: [PATCH 24/84] Fix zoom force unwrap potential crash --- ios/RCTMGL-v10/RCTMGLCamera.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ios/RCTMGL-v10/RCTMGLCamera.swift b/ios/RCTMGL-v10/RCTMGLCamera.swift index 841219870..2a3fae0be 100644 --- a/ios/RCTMGL-v10/RCTMGLCamera.swift +++ b/ios/RCTMGL-v10/RCTMGLCamera.swift @@ -263,11 +263,11 @@ class RCTMGLCamera : RCTMGLMapComponentBase, LocationConsumer { return .flight }() - if let z1 = minZoomLevel, let z2 = CGFloat(exactly: z1), zoom! < z2 { + if let z1 = minZoomLevel, let z2 = CGFloat(exactly: z1), let zCurrent = zoom, zCurrent < z2 { zoom = z2 } - if let z1 = maxZoomLevel, let z2 = CGFloat(exactly: z1), zoom! > z2 { + if let z1 = maxZoomLevel, let z2 = CGFloat(exactly: z1), let zCurrent = zoom, zCurrent > z2 { zoom = z2 } From 18c974e78ab8a7d5217dbb1b0d729514cff8e2fe Mon Sep 17 00:00:00 2001 From: Naftali Beder Date: Thu, 28 Apr 2022 13:16:38 -0500 Subject: [PATCH 25/84] Add ref to exports --- javascript/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/javascript/index.js b/javascript/index.js index 22e1dddd4..55d931a90 100644 --- a/javascript/index.js +++ b/javascript/index.js @@ -9,6 +9,7 @@ import Callout from './components/Callout'; import UserLocation from './components/UserLocation'; import Camera, { CameraProps, + CameraRef, UserTrackingModes, AnimationMode, } from './components/Camera'; @@ -130,6 +131,7 @@ export { Callout, UserLocation, Camera, + CameraRef, CameraProps, AnimationMode, Annotation, From 7429ff89667de8b8d1970bf341ec6f51e1f8c330 Mon Sep 17 00:00:00 2001 From: Naftali Beder Date: Thu, 28 Apr 2022 13:17:00 -0500 Subject: [PATCH 26/84] Expand to include imperative methods --- example/src/examples/V10/CameraAnimation.tsx | 234 +++++++++++++------ 1 file changed, 158 insertions(+), 76 deletions(-) diff --git a/example/src/examples/V10/CameraAnimation.tsx b/example/src/examples/V10/CameraAnimation.tsx index bb04fd256..18c5ea7fc 100644 --- a/example/src/examples/V10/CameraAnimation.tsx +++ b/example/src/examples/V10/CameraAnimation.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useState } from 'react'; +import React, { useMemo, useRef, useState } from 'react'; import { Button, SafeAreaView, StyleSheet, View } from 'react-native'; import MapboxGL from '@rnmapbox/maps'; const { MapView, ShapeSource, CircleLayer } = MapboxGL; @@ -6,7 +6,12 @@ import bbox from '@turf/bbox'; import { Feature, LineString, Point, Position } from '@turf/helpers'; import { Text, Divider } from 'react-native-elements'; -import { Camera, CameraProps, AnimationMode } from '../../../../javascript'; +import { + Camera, + CameraRef, + CameraProps, + AnimationMode, +} from '../../../../javascript'; import Page from '../common/Page'; import colors from '../../styles/colors'; @@ -42,6 +47,7 @@ const mapStyles = { }, }; +const initialPosition = [-73.984638, 40.759211]; const zeroPadding = { paddingTop: 0, paddingBottom: 0, @@ -57,6 +63,10 @@ const evenPadding = { const minZoomLevel = 8; const maxZoomLevel = 16; +const randPosition = (around: Position): Position => { + return [around[0] + Math.random() * 0.2, around[1] + Math.random() * 0.2]; +}; + const randPadding = () => { const randNum = () => { const items = [0, 150, 300]; @@ -72,7 +82,10 @@ const randPadding = () => { }; const CameraAnimation = (props: any) => { - const initialPosition = [-73.984638, 40.759211]; + const [inputKind, setInputKind] = useState<'declarative' | 'imperative'>( + 'declarative', + ); + const camera = useRef(null); const [animationMode, setAnimationMode] = useState('moveTo'); const [positions, setPositions] = useState([initialPosition]); @@ -89,26 +102,13 @@ const CameraAnimation = (props: any) => { setAnimationMode(_animationMode); if (shouldCreateMultiple) { - const _centerPosition = [ - initialPosition[0] + Math.random() * 0.2, - initialPosition[1] + Math.random() * 0.2, - ]; + const _centerPosition = randPosition(initialPosition); const _positions = Array(10) .fill(0) - .map((_) => { - return [ - _centerPosition[0] + Math.random() * 0.2, - _centerPosition[1] + Math.random() * 0.2, - ]; - }); + .map((_) => randPosition(_centerPosition)); setPositions(_positions); } else { - setPositions([ - [ - initialPosition[0] + Math.random() * 0.2, - initialPosition[1] + Math.random() * 0.2, - ], - ]); + setPositions([randPosition(initialPosition)]); } }; @@ -167,80 +167,162 @@ const CameraAnimation = (props: any) => { } }, [positions, centerOrBounds]); + const declarativeContent = () => { + return ( + + centerCoordinate + +