From ea558319a929768a447ba231e9ce52980c4e79ef Mon Sep 17 00:00:00 2001 From: Joachim Amoah Date: Thu, 4 Dec 2025 14:01:43 -0500 Subject: [PATCH 1/3] feat: add more MapView events and controls --- CONTRIBUTING.md | 4 +- .../android/react/navsdk/Constants.java | 11 ++ .../react/navsdk/INavigationViewCallback.java | 5 + .../react/navsdk/MapViewController.java | 4 + .../android/react/navsdk/MapViewFragment.java | 19 +++ .../android/react/navsdk/NavViewFragment.java | 19 +++ .../android/react/navsdk/NavViewManager.java | 6 +- .../android/react/navsdk/NavViewModule.java | 122 ++++++++++++++++- .../react/navsdk/ObjectTranslationUtil.java | 19 +++ example/src/controls/mapsControls.tsx | 34 +++++ .../src/screens/IntegrationTestsScreen.tsx | 7 + example/src/screens/MultipleMapsScreen.tsx | 13 ++ example/src/screens/NavigationScreen.tsx | 7 + .../INavigationViewCallback.h | 2 + .../NavAutoModule.m | 117 ++++++++++------ ios/react-native-navigation-sdk/NavModule.m | 89 +++++-------- ios/react-native-navigation-sdk/NavView.h | 3 + ios/react-native-navigation-sdk/NavView.m | 12 ++ .../NavViewController.h | 4 + .../NavViewController.m | 100 +++++++++++--- .../NavViewModule.m | 126 ++++++++++++------ .../ObjectTranslationUtil.h | 1 + .../ObjectTranslationUtil.m | 11 ++ .../RCTNavViewManager.m | 101 ++++++-------- src/auto/useNavigationAuto.ts | 25 +++- src/maps/mapView/mapView.tsx | 21 ++- src/maps/mapView/mapViewController.ts | 19 ++- src/maps/mapView/types.ts | 63 ++++++++- src/maps/types.ts | 3 +- .../navigationView/navigationView.tsx | 17 +++ src/shared/types.ts | 20 +++ src/shared/viewManager.ts | 11 +- 32 files changed, 786 insertions(+), 229 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1cb8a636..189be2d9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -42,14 +42,14 @@ This project follows follow [GitHub's directions](https://help.github.com/articles/generating-ssh-keys/) to generate an SSH key. - `git clone git@github.com:/googlemaps/react-native-navigation-sdk.git` -- `git remote add upstream git@github.com:googlemaps/react-native-sdk.git` (So that you +- `git remote add upstream git@github.com:googlemaps/react-native-navigation-sdk.git` (So that you fetch from the master repository, not your clone, when running `git fetch` et al.) #### Create branch 1. `git fetch upstream` -2. `git checkout upstream/master -b ` +2. `git checkout upstream/main -b ` 3. Start coding! #### Commit changes diff --git a/android/src/main/java/com/google/android/react/navsdk/Constants.java b/android/src/main/java/com/google/android/react/navsdk/Constants.java index 5dbe238d..c6f87c8b 100644 --- a/android/src/main/java/com/google/android/react/navsdk/Constants.java +++ b/android/src/main/java/com/google/android/react/navsdk/Constants.java @@ -16,4 +16,15 @@ public class Constants { public static final String LAT_FIELD_KEY = "lat"; public static final String LNG_FIELD_KEY = "lng"; + + public static final String URI_KEY = "uri"; + + public static final String X_KEY = "x"; + public static final String Y_KEY = "y"; + + public static final String CAMERA_POSITION_KEY = "cameraPosition"; + public static final String TARGET_KEY = "target"; + public static final String BEARING_KEY = "bearing"; + public static final String TILT_KEY = "tilt"; + public static final String ZOOM_KEY = "zoom"; } diff --git a/android/src/main/java/com/google/android/react/navsdk/INavigationViewCallback.java b/android/src/main/java/com/google/android/react/navsdk/INavigationViewCallback.java index 939bb6db..b449b334 100644 --- a/android/src/main/java/com/google/android/react/navsdk/INavigationViewCallback.java +++ b/android/src/main/java/com/google/android/react/navsdk/INavigationViewCallback.java @@ -13,6 +13,7 @@ */ package com.google.android.react.navsdk; +import com.google.android.gms.maps.model.CameraPosition; import com.google.android.gms.maps.model.Circle; import com.google.android.gms.maps.model.GroundOverlay; import com.google.android.gms.maps.model.LatLng; @@ -36,4 +37,8 @@ public interface INavigationViewCallback { void onMarkerInfoWindowTapped(Marker marker); void onMapClick(LatLng latLng); + + void onMapDrag(CameraPosition cameraPosition); + + void onMapDragEnd(CameraPosition cameraPosition); } diff --git a/android/src/main/java/com/google/android/react/navsdk/MapViewController.java b/android/src/main/java/com/google/android/react/navsdk/MapViewController.java index 882b3b1a..0ba1ded2 100644 --- a/android/src/main/java/com/google/android/react/navsdk/MapViewController.java +++ b/android/src/main/java/com/google/android/react/navsdk/MapViewController.java @@ -82,6 +82,10 @@ public void setupMapListeners(INavigationViewCallback navigationViewCallback) { mGoogleMap.setOnInfoWindowClickListener( marker -> mNavigationViewCallback.onMarkerInfoWindowTapped(marker)); mGoogleMap.setOnMapClickListener(latLng -> mNavigationViewCallback.onMapClick(latLng)); + mGoogleMap.setOnCameraMoveListener( + () -> mNavigationViewCallback.onMapDrag(mGoogleMap.getCameraPosition())); + mGoogleMap.setOnCameraIdleListener( + () -> mNavigationViewCallback.onMapDragEnd(mGoogleMap.getCameraPosition())); } public GoogleMap getGoogleMap() { diff --git a/android/src/main/java/com/google/android/react/navsdk/MapViewFragment.java b/android/src/main/java/com/google/android/react/navsdk/MapViewFragment.java index 740555f2..e914c14b 100644 --- a/android/src/main/java/com/google/android/react/navsdk/MapViewFragment.java +++ b/android/src/main/java/com/google/android/react/navsdk/MapViewFragment.java @@ -27,6 +27,7 @@ import com.google.android.gms.maps.GoogleMap; import com.google.android.gms.maps.OnMapReadyCallback; import com.google.android.gms.maps.SupportMapFragment; +import com.google.android.gms.maps.model.CameraPosition; import com.google.android.gms.maps.model.Circle; import com.google.android.gms.maps.model.GroundOverlay; import com.google.android.gms.maps.model.LatLng; @@ -126,6 +127,24 @@ public void onMapClick(LatLng latLng) { emitEvent("onMapClick", ObjectTranslationUtil.getMapFromLatLng(latLng)); } + @Override + public void onMapDrag(CameraPosition cameraPosition) { + WritableMap map = Arguments.createMap(); + map.putMap( + Constants.CAMERA_POSITION_KEY, + ObjectTranslationUtil.getMapFromCameraPosition(cameraPosition)); + emitEvent("onMapDrag", map); + } + + @Override + public void onMapDragEnd(CameraPosition cameraPosition) { + WritableMap map = Arguments.createMap(); + map.putMap( + Constants.CAMERA_POSITION_KEY, + ObjectTranslationUtil.getMapFromCameraPosition(cameraPosition)); + emitEvent("onMapDragEnd", map); + } + public MapViewController getMapController() { return mMapViewController; } diff --git a/android/src/main/java/com/google/android/react/navsdk/NavViewFragment.java b/android/src/main/java/com/google/android/react/navsdk/NavViewFragment.java index 579356c1..1c9e64ca 100644 --- a/android/src/main/java/com/google/android/react/navsdk/NavViewFragment.java +++ b/android/src/main/java/com/google/android/react/navsdk/NavViewFragment.java @@ -26,6 +26,7 @@ import com.facebook.react.uimanager.events.EventDispatcher; import com.google.android.gms.maps.GoogleMap; import com.google.android.gms.maps.OnMapReadyCallback; +import com.google.android.gms.maps.model.CameraPosition; import com.google.android.gms.maps.model.Circle; import com.google.android.gms.maps.model.GroundOverlay; import com.google.android.gms.maps.model.LatLng; @@ -166,6 +167,24 @@ public void onMapClick(LatLng latLng) { emitEvent("onMapClick", ObjectTranslationUtil.getMapFromLatLng(latLng)); } + @Override + public void onMapDrag(CameraPosition cameraPosition) { + WritableMap map = Arguments.createMap(); + map.putMap( + Constants.CAMERA_POSITION_KEY, + ObjectTranslationUtil.getMapFromCameraPosition(cameraPosition)); + emitEvent("onMapDrag", ObjectTranslationUtil.getMapFromCameraPosition(cameraPosition)); + } + + @Override + public void onMapDragEnd(CameraPosition cameraPosition) { + WritableMap map = Arguments.createMap(); + map.putMap( + Constants.CAMERA_POSITION_KEY, + ObjectTranslationUtil.getMapFromCameraPosition(cameraPosition)); + emitEvent("onMapDragEnd", ObjectTranslationUtil.getMapFromCameraPosition(cameraPosition)); + } + @Override public void onDestroy() { super.onDestroy(); diff --git a/android/src/main/java/com/google/android/react/navsdk/NavViewManager.java b/android/src/main/java/com/google/android/react/navsdk/NavViewManager.java index b95bfd39..eb8d21f4 100644 --- a/android/src/main/java/com/google/android/react/navsdk/NavViewManager.java +++ b/android/src/main/java/com/google/android/react/navsdk/NavViewManager.java @@ -26,8 +26,8 @@ import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.common.MapBuilder; -import com.facebook.react.uimanager.SimpleViewManager; import com.facebook.react.uimanager.ThemedReactContext; +import com.facebook.react.uimanager.ViewGroupManager; import com.facebook.react.uimanager.annotations.ReactProp; import com.google.android.gms.maps.GoogleMap; import java.lang.ref.WeakReference; @@ -38,7 +38,7 @@ // NavViewManager is responsible for managing both the regular map fragment as well as the // navigation map view fragment. // -public class NavViewManager extends SimpleViewManager { +public class NavViewManager extends ViewGroupManager { public static final String REACT_CLASS = "NavViewManager"; @@ -528,6 +528,8 @@ public Map getExportedCustomDirectEventTypeConstants() { MapBuilder.of("registrationName", "onPromptVisibilityChanged")) .put("onMapReady", MapBuilder.of("registrationName", "onMapReady")) .put("onMapClick", MapBuilder.of("registrationName", "onMapClick")) + .put("onMapDrag", MapBuilder.of("registrationName", "onMapDrag")) + .put("onMapDragEnd", MapBuilder.of("registrationName", "onMapDragEnd")) .put("onMarkerClick", MapBuilder.of("registrationName", "onMarkerClick")) .put("onPolylineClick", MapBuilder.of("registrationName", "onPolylineClick")) .put("onPolygonClick", MapBuilder.of("registrationName", "onPolygonClick")) diff --git a/android/src/main/java/com/google/android/react/navsdk/NavViewModule.java b/android/src/main/java/com/google/android/react/navsdk/NavViewModule.java index 1fe2e2cf..48dfa35a 100644 --- a/android/src/main/java/com/google/android/react/navsdk/NavViewModule.java +++ b/android/src/main/java/com/google/android/react/navsdk/NavViewModule.java @@ -13,6 +13,7 @@ */ package com.google.android.react.navsdk; +import android.graphics.Point; import android.location.Location; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Promise; @@ -22,14 +23,18 @@ import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.bridge.WritableMap; +import com.google.android.gms.maps.CameraUpdate; +import com.google.android.gms.maps.CameraUpdateFactory; import com.google.android.gms.maps.UiSettings; import com.google.android.gms.maps.model.CameraPosition; import com.google.android.gms.maps.model.Circle; import com.google.android.gms.maps.model.GroundOverlay; import com.google.android.gms.maps.model.LatLng; +import com.google.android.gms.maps.model.LatLngBounds; import com.google.android.gms.maps.model.Marker; import com.google.android.gms.maps.model.Polygon; import com.google.android.gms.maps.model.Polyline; +import com.google.android.gms.maps.model.VisibleRegion; import java.util.HashMap; import java.util.Map; @@ -75,12 +80,7 @@ public void getCameraPosition(Integer viewId, final Promise promise) { return; } - LatLng target = cp.target; - WritableMap map = Arguments.createMap(); - map.putDouble("bearing", cp.bearing); - map.putDouble("tilt", cp.tilt); - map.putDouble("zoom", cp.zoom); - map.putMap("target", ObjectTranslationUtil.getMapFromLatLng(target)); + WritableMap map = ObjectTranslationUtil.getMapFromCameraPosition(cp); promise.resolve(map); }); @@ -110,6 +110,116 @@ public void getMyLocation(Integer viewId, final Promise promise) { }); } + @ReactMethod + public void coordinateForPoint(Integer viewId, ReadableMap pointMap, final Promise promise) { + UiThreadUtil.runOnUiThread( + () -> { + if (mNavViewManager.getGoogleMap(viewId) == null) { + promise.reject(JsErrors.NO_MAP_ERROR_CODE, JsErrors.NO_MAP_ERROR_MESSAGE); + return; + } + + try { + float density = getReactApplicationContext().getResources().getDisplayMetrics().density; + int x = (int) density * CollectionUtil.getInt("x", pointMap.toHashMap(), 0); + int y = (int) density * CollectionUtil.getInt("y", pointMap.toHashMap(), 0); + Point point = new Point(x, y); + LatLng latLng = + mNavViewManager.getGoogleMap(viewId).getProjection().fromScreenLocation(point); + + promise.resolve(ObjectTranslationUtil.getMapFromLatLng(latLng)); + } catch (Exception e) { + promise.resolve(null); + return; + } + }); + } + + @ReactMethod + public void pointForCoordinate(Integer viewId, ReadableMap latLngMap, final Promise promise) { + UiThreadUtil.runOnUiThread( + () -> { + if (mNavViewManager.getGoogleMap(viewId) == null) { + promise.reject(JsErrors.NO_MAP_ERROR_CODE, JsErrors.NO_MAP_ERROR_MESSAGE); + return; + } + + LatLng latLng = ObjectTranslationUtil.getLatLngFromMap(latLngMap.toHashMap()); + Point point = + mNavViewManager.getGoogleMap(viewId).getProjection().toScreenLocation(latLng); + float density = getReactApplicationContext().getResources().getDisplayMetrics().density; + point.x = (int) (point.x / density); + point.y = (int) (point.y / density); + + promise.resolve(ObjectTranslationUtil.getMapFromPoint(point)); + }); + } + + @ReactMethod + public void fitBounds(Integer viewId, ReadableMap boundsOptions, final Promise promise) { + UiThreadUtil.runOnUiThread( + () -> { + if (mNavViewManager.getGoogleMap(viewId) == null) { + promise.reject(JsErrors.NO_MAP_ERROR_CODE, JsErrors.NO_MAP_ERROR_MESSAGE); + return; + } + + LatLng northEast = + ObjectTranslationUtil.getLatLngFromMap( + boundsOptions.getMap("bounds").getMap("northEast").toHashMap()); + LatLng southWest = + ObjectTranslationUtil.getLatLngFromMap( + boundsOptions.getMap("bounds").getMap("southWest").toHashMap()); + + if (northEast == null || southWest == null) { + promise.resolve(null); + return; + } + + ReadableMap paddingMap = boundsOptions.getMap("padding"); + if (paddingMap != null) { + double density = + getReactApplicationContext().getResources().getDisplayMetrics().density; + int left = (int) (paddingMap.getInt("left") * density); + int top = (int) (paddingMap.getInt("top") * density); + int right = (int) (paddingMap.getInt("right") * density); + int bottom = (int) (paddingMap.getInt("bottom") * density); + mNavViewManager.getGoogleMap(viewId).setPadding(left, top, right, bottom); + } + + CameraUpdate cameraUpdate = + CameraUpdateFactory.newLatLngBounds(new LatLngBounds(southWest, northEast), 0); + mNavViewManager.getGoogleMap(viewId).animateCamera(cameraUpdate); + + promise.resolve(null); + }); + } + + @ReactMethod + public void getBounds(Integer viewId, final Promise promise) { + UiThreadUtil.runOnUiThread( + () -> { + if (mNavViewManager.getGoogleMap(viewId) == null) { + promise.reject(JsErrors.NO_MAP_ERROR_CODE, JsErrors.NO_MAP_ERROR_MESSAGE); + return; + } + + VisibleRegion visibleRegion = + mNavViewManager.getGoogleMap(viewId).getProjection().getVisibleRegion(); + LatLng northEast = visibleRegion.farRight; + LatLng southWest = visibleRegion.nearLeft; + + WritableMap northEastMap = ObjectTranslationUtil.getMapFromLatLng(northEast); + WritableMap southWestMap = ObjectTranslationUtil.getMapFromLatLng(southWest); + + WritableMap map = Arguments.createMap(); + map.putMap("northEast", northEastMap); + map.putMap("southWest", southWestMap); + + promise.resolve(map); + }); + } + @ReactMethod public void getUiSettings(Integer viewId, final Promise promise) { UiThreadUtil.runOnUiThread( diff --git a/android/src/main/java/com/google/android/react/navsdk/ObjectTranslationUtil.java b/android/src/main/java/com/google/android/react/navsdk/ObjectTranslationUtil.java index c44cdca6..ead8c2fc 100644 --- a/android/src/main/java/com/google/android/react/navsdk/ObjectTranslationUtil.java +++ b/android/src/main/java/com/google/android/react/navsdk/ObjectTranslationUtil.java @@ -13,11 +13,13 @@ */ package com.google.android.react.navsdk; +import android.graphics.Point; import android.location.Location; import android.os.Build; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; +import com.google.android.gms.maps.model.CameraPosition; import com.google.android.gms.maps.model.Circle; import com.google.android.gms.maps.model.GroundOverlay; import com.google.android.gms.maps.model.LatLng; @@ -79,6 +81,23 @@ public static WritableMap getMapFromLatLng(LatLng latLng) { return map; } + public static WritableMap getMapFromPoint(Point point) { + WritableMap map = Arguments.createMap(); + map.putDouble(Constants.X_KEY, point.x); + map.putDouble(Constants.Y_KEY, point.y); + return map; + } + + public static WritableMap getMapFromCameraPosition(CameraPosition cameraPosition) { + WritableMap map = Arguments.createMap(); + map.putMap(Constants.TARGET_KEY, getMapFromLatLng(cameraPosition.target)); + map.putDouble(Constants.BEARING_KEY, cameraPosition.bearing); + map.putDouble(Constants.TILT_KEY, cameraPosition.tilt); + map.putDouble(Constants.ZOOM_KEY, cameraPosition.zoom); + + return map; + } + public static WritableMap getMapFromWaypoint(Waypoint waypoint) { WritableMap map = Arguments.createMap(); diff --git a/example/src/controls/mapsControls.tsx b/example/src/controls/mapsControls.tsx index 6a13fa62..8729f4ee 100644 --- a/example/src/controls/mapsControls.tsx +++ b/example/src/controls/mapsControls.tsx @@ -212,6 +212,36 @@ const MapsControls: React.FC = ({ mapViewController }) => { setCustomPaddingEnabled(!customPaddingEnabled); }; + const coordinateForPoint = async () => { + const cameraPosition = await mapViewController.getCameraPosition(); + const point = await mapViewController.pointForCoordinate( + cameraPosition.target + ); + const coordinate = await mapViewController.coordinateForPoint(point); + console.log({ point, coordinate }); + }; + + const pointForCoordinate = async () => { + const { target: coordinate } = await mapViewController.getCameraPosition(); + const point = await mapViewController.pointForCoordinate(coordinate); + console.log({ point, coordinate }); + }; + + const fitBounds = async () => { + const bounds = await mapViewController.getBounds(); + bounds.northEast.lat -= 1; + bounds.northEast.lng -= 1; + bounds.southWest.lat += 1; + bounds.southWest.lng += 1; + + await mapViewController.fitBounds({ bounds }); + }; + + const getBounds = async () => { + const bounds = await mapViewController.getBounds(); + console.log(bounds); + }; + return ( = ({ mapViewController }) => { onPress={getIsMyLocationEnabled} />