diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b959125..d18434c 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/README.md b/README.md index c3efc33..ef04342 100644 --- a/README.md +++ b/README.md @@ -542,6 +542,8 @@ Both `NavigationView` and `MapView` support the following props. Props marked wi | ----------------------------------- | ------------------------------------------------ | :---: | ------------------------------------------------ | | `onMapReady` | `() => void` | | Called when the map is ready to use | | `onMapClick` | `(latLng: LatLng) => void` | | Called when the map is clicked | +| `onMapDrag` | `(result: DragResult) => void` | | Called when the map is dragged | +| `onMapDragEnd` | `(result: DragResult) => void` | | Called when the map dragging stops | | `onMarkerClick` | `(marker: Marker) => void` | | Called when a marker is clicked | | `onPolylineClick` | `(polyline: Polyline) => void` | | Called when a polyline is clicked | | `onPolygonClick` | `(polygon: Polygon) => void` | | Called when a polygon is clicked | @@ -564,6 +566,10 @@ The `MapViewController` is provided via the `onMapViewControllerCreated` callbac | `addPolyline(options: PolylineOptions)` | `Promise` | Add or update a polyline. If `options.id` matches an existing polyline, it is updated | | `addPolygon(options: PolygonOptions)` | `Promise` | Add or update a polygon. If `options.id` matches an existing polygon, it is updated | | `addCircle(options: CircleOptions)` | `Promise` | Add or update a circle. If `options.id` matches an existing circle, it is updated | +| `coordinateForPoint(point: Point)` | `Promise` | Maps a point coordinate in the map’s view to an Earth coordinate | +| `pointForCoordinate(coordinate: LatLng)` | `Promise` | Maps an Earth coordinate to a point coordinate in the map’s view | +| `fitBounds(boundsOptions: BoundsOptions)` | `Promise` | Transforms the camera such that the specified bounds are centered on screen | +| `getBounds()` | `Promise` | Retrieves the rectangular bounds of the map view | | `addGroundOverlay(options: GroundOverlayOptions)` | `Promise` | Add or update a ground overlay. If `options.id` matches an existing overlay, it is updated | | `removeMarker(id: string)` | `void` | Remove a marker by its ID | | `removePolyline(id: string)` | `void` | Remove a polyline by its ID | 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 5dbe238..c6f87c8 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 939bb6d..b449b33 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 8a10394..cafa802 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 @@ -98,6 +98,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 fc154e5..d60d83a 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.GoogleMapOptions; 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; @@ -137,6 +138,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/NavAutoModule.java b/android/src/main/java/com/google/android/react/navsdk/NavAutoModule.java index 0af029b..02ac849 100644 --- a/android/src/main/java/com/google/android/react/navsdk/NavAutoModule.java +++ b/android/src/main/java/com/google/android/react/navsdk/NavAutoModule.java @@ -163,6 +163,52 @@ public void addCircle(ReadableMap options, final Promise promise) { }); } + @Override + public void coordinateForPoint(String nativeID, ReadableMap point, Promise promise) { + UiThreadUtil.runOnUiThread( + () -> { + if (mMapViewController == null) { + promise.reject(JsErrors.NO_MAP_ERROR_CODE, JsErrors.NO_MAP_ERROR_MESSAGE); + return; + } + + NavViewModule.resolveCoordinateForPoint( + getReactApplicationContext(), mMapViewController.getGoogleMap(), point, promise); + }); + } + + @Override + public void pointForCoordinate(String nativeID, ReadableMap coordinate, Promise promise) { + if (mMapViewController == null) { + promise.reject(JsErrors.NO_MAP_ERROR_CODE, JsErrors.NO_MAP_ERROR_MESSAGE); + return; + } + + NavViewModule.resolvePointForCoordinate( + getReactApplicationContext(), mMapViewController.getGoogleMap(), coordinate, promise); + } + + @Override + public void fitBounds(String nativeID, ReadableMap boundsOptions, Promise promise) { + if (mMapViewController == null) { + promise.reject(JsErrors.NO_MAP_ERROR_CODE, JsErrors.NO_MAP_ERROR_MESSAGE); + return; + } + + NavViewModule.resolveFitBounds( + getReactApplicationContext(), mMapViewController.getGoogleMap(), boundsOptions, promise); + } + + @Override + public void getBounds(String nativeID, Promise promise) { + if (mMapViewController == null) { + promise.reject(JsErrors.NO_MAP_ERROR_CODE, JsErrors.NO_MAP_ERROR_MESSAGE); + return; + } + + NavViewModule.resolveGetBounds(mMapViewController.getGoogleMap(), promise); + } + @Override public void addMarker(ReadableMap options, final Promise promise) { ReadableMap markerOptionsMap = options; 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 16c734b..3365abc 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.GoogleMapOptions; +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; @@ -242,6 +243,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); + } + @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 0090ae1..b779ffd 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 @@ -655,6 +655,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 6cd6f27..9138d8c 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,24 +13,32 @@ */ 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; import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.bridge.WritableArray; 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.GoogleMap; 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 com.google.maps.android.rn.navsdk.NativeNavViewModuleSpec; import java.util.Map; +import java.util.Objects; /** * TurboModule for map view operations. Uses nativeID-based view registry to access view instances. @@ -65,12 +73,7 @@ public void getCameraPosition(String nativeID, 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); }); @@ -101,6 +104,151 @@ public void getMyLocation(String nativeID, final Promise promise) { }); } + public static void resolveCoordinateForPoint( + ReactApplicationContext context, GoogleMap map, ReadableMap pointMap, final Promise promise) { + try { + float density = context.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 = map.getProjection().fromScreenLocation(point); + + promise.resolve(ObjectTranslationUtil.getMapFromLatLng(latLng)); + } catch (Exception e) { + promise.resolve(null); + } + } + + @ReactMethod + public void coordinateForPoint(String nativeID, ReadableMap pointMap, final Promise promise) { + UiThreadUtil.runOnUiThread( + () -> { + IMapViewFragment fragment = mNavViewManager.getFragmentByNativeId(nativeID); + if (fragment == null || fragment.getGoogleMap() == null) { + promise.reject(JsErrors.NO_MAP_ERROR_CODE, JsErrors.NO_MAP_ERROR_MESSAGE); + return; + } + + NavViewModule.resolveCoordinateForPoint( + getReactApplicationContext(), fragment.getGoogleMap(), pointMap, promise); + }); + } + + public static void resolvePointForCoordinate( + ReactApplicationContext context, + GoogleMap map, + ReadableMap latLngMap, + final Promise promise) { + LatLng latLng = ObjectTranslationUtil.getLatLngFromMap(latLngMap.toHashMap()); + Point point = map.getProjection().toScreenLocation(latLng); + float density = context.getResources().getDisplayMetrics().density; + point.x = (int) (point.x / density); + point.y = (int) (point.y / density); + + promise.resolve(ObjectTranslationUtil.getMapFromPoint(point)); + } + + @ReactMethod + public void pointForCoordinate(String nativeID, ReadableMap latLngMap, final Promise promise) { + UiThreadUtil.runOnUiThread( + () -> { + IMapViewFragment fragment = mNavViewManager.getFragmentByNativeId(nativeID); + if (fragment == null || fragment.getGoogleMap() == null) { + promise.reject(JsErrors.NO_MAP_ERROR_CODE, JsErrors.NO_MAP_ERROR_MESSAGE); + return; + } + + NavViewModule.resolvePointForCoordinate( + getReactApplicationContext(), fragment.getGoogleMap(), latLngMap, promise); + }); + } + + public static void resolveFitBounds( + ReactApplicationContext context, + GoogleMap map, + ReadableMap boundsOptions, + final Promise promise) { + try { + LatLng northEast = + ObjectTranslationUtil.getLatLngFromMap( + Objects.requireNonNull( + Objects.requireNonNull(boundsOptions.getMap("bounds")).getMap("northEast")) + .toHashMap()); + LatLng southWest = + ObjectTranslationUtil.getLatLngFromMap( + Objects.requireNonNull( + Objects.requireNonNull(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 = context.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); + map.setPadding(left, top, right, bottom); + } + + CameraUpdate cameraUpdate = + CameraUpdateFactory.newLatLngBounds(new LatLngBounds(southWest, northEast), 0); + map.animateCamera(cameraUpdate); + + promise.resolve(null); + } catch (Exception e) { + promise.resolve(null); + } + } + + @ReactMethod + public void fitBounds(String nativeID, ReadableMap boundsOptions, final Promise promise) { + UiThreadUtil.runOnUiThread( + () -> { + IMapViewFragment fragment = mNavViewManager.getFragmentByNativeId(nativeID); + if (fragment == null || fragment.getGoogleMap() == null) { + promise.reject(JsErrors.NO_MAP_ERROR_CODE, JsErrors.NO_MAP_ERROR_MESSAGE); + return; + } + + NavViewModule.resolveFitBounds( + getReactApplicationContext(), fragment.getGoogleMap(), boundsOptions, promise); + }); + } + + public static void resolveGetBounds(GoogleMap map, final Promise promise) { + VisibleRegion visibleRegion = map.getProjection().getVisibleRegion(); + LatLng northEast = visibleRegion.farRight; + LatLng southWest = visibleRegion.nearLeft; + + WritableMap northEastMap = ObjectTranslationUtil.getMapFromLatLng(northEast); + WritableMap southWestMap = ObjectTranslationUtil.getMapFromLatLng(southWest); + + WritableMap boundsMap = Arguments.createMap(); + boundsMap.putMap("northEast", northEastMap); + boundsMap.putMap("southWest", southWestMap); + + promise.resolve(boundsMap); + } + + @ReactMethod + public void getBounds(String nativeID, final Promise promise) { + UiThreadUtil.runOnUiThread( + () -> { + IMapViewFragment fragment = mNavViewManager.getFragmentByNativeId(nativeID); + if (fragment == null || fragment.getGoogleMap() == null) { + promise.reject(JsErrors.NO_MAP_ERROR_CODE, JsErrors.NO_MAP_ERROR_MESSAGE); + return; + } + + NavViewModule.resolveGetBounds(fragment.getGoogleMap(), promise); + }); + } + @Override public void getUiSettings(String nativeID, 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 bacef7f..392fba4 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 @@ -14,6 +14,7 @@ package com.google.android.react.navsdk; import android.graphics.Color; +import android.graphics.Point; import android.location.Location; import android.os.Build; import android.util.Log; @@ -143,6 +144,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 9d80982..27261a9 100644 --- a/example/src/controls/mapsControls.tsx +++ b/example/src/controls/mapsControls.tsx @@ -374,6 +374,42 @@ const MapsControls: React.FC = ({ setCustomPaddingEnabled(!customPaddingEnabled); }; + const coordinateForPoint = async () => { + const cameraPosition = await mapViewController.getCameraPosition(); + const point = await mapViewController.pointForCoordinate( + cameraPosition.target + ); + const coordinate = await mapViewController.coordinateForPoint(point); + showSnackbar( + `point, corrdinate: ${point.x.toFixed(4)}, ${point.y.toFixed(4)} ${coordinate.lat.toFixed(4)}, ${coordinate.lng.toFixed(4)}` + ); + }; + + const pointForCoordinate = async () => { + const { target: coordinate } = await mapViewController.getCameraPosition(); + const point = await mapViewController.pointForCoordinate(coordinate); + showSnackbar( + `point, corrdinate: ${point.x.toFixed(4)}, ${point.y.toFixed(4)} ${coordinate.lat.toFixed(4)}, ${coordinate.lng.toFixed(4)}` + ); + }; + + 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(); + showSnackbar( + `ne, sw: ${bounds.northEast.lat.toFixed(4)}, ${bounds.northEast.lng.toFixed(4)} ${bounds.southWest.lat.toFixed(4)}, ${bounds.southWest.lng.toFixed(4)}` + ); + }; + const setMapColorScheme = (index: number) => { const scheme = index === 1 @@ -421,6 +457,23 @@ const MapsControls: React.FC = ({ title="Get camera position" onPress={getCameraPositionClicked} /> + + + + + {/* Map Overlays */} diff --git a/example/src/screens/IntegrationTestsScreen.tsx b/example/src/screens/IntegrationTestsScreen.tsx index 56bbd0f..b43af89 100644 --- a/example/src/screens/IntegrationTestsScreen.tsx +++ b/example/src/screens/IntegrationTestsScreen.tsx @@ -22,6 +22,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { type Circle, + type DragResult, type LatLng, type MapViewController, type Marker, @@ -176,6 +177,18 @@ const IntegrationTestsScreen = () => { ); }, []); + const onMapDrag = useCallback((result: DragResult) => { + showSnackbar( + `Camera at ${result.cameraPosition.target.lat.toFixed(4)}, ${result.cameraPosition.target.lng.toFixed(4)}, ${result.cameraPosition.zoom?.toFixed(4)}` + ); + }, []); + + const onMapDragEnd = useCallback((result: DragResult) => { + showSnackbar( + `Camera at ${result.cameraPosition.target.lat.toFixed(4)}, ${result.cameraPosition.target.lng.toFixed(4)}, ${result.cameraPosition.zoom?.toFixed(4)}` + ); + }, []); + const passTest = () => { setTestStatus(TestRunStatus.Finished); setTestResult(TestResult.Success); @@ -326,6 +339,8 @@ const IntegrationTestsScreen = () => { onPolylineClick={onPolylineClick} onMarkerInfoWindowTapped={onMarkerInfoWindowTapped} onMapClick={onMapClick} + onMapDrag={onMapDrag} + onMapDragEnd={onMapDragEnd} onMapViewControllerCreated={setMapViewController} onNavigationViewControllerCreated={setNavigationViewController} compassEnabled={compassEnabled} diff --git a/example/src/screens/MultipleMapsScreen.tsx b/example/src/screens/MultipleMapsScreen.tsx index 42e3c5b..fff67b5 100644 --- a/example/src/screens/MultipleMapsScreen.tsx +++ b/example/src/screens/MultipleMapsScreen.tsx @@ -38,6 +38,7 @@ import { type Polyline, useNavigation, MapView, + type DragResult, } from '@googlemaps/react-native-navigation-sdk'; import MapsControls from '../controls/mapsControls'; import NavigationControls from '../controls/navigationControls'; @@ -215,6 +216,18 @@ const MultipleMapsScreen = () => { ); }, []); + const onMapDrag1 = useCallback((result: DragResult) => { + showSnackbar( + `Map 1: Camera at ${result.cameraPosition.target.lat.toFixed(4)}, ${result.cameraPosition.target.lng.toFixed(4)}, ${result.cameraPosition.zoom?.toFixed(4)}` + ); + }, []); + + const onMapDragEnd1 = useCallback((result: DragResult) => { + showSnackbar( + `Map 1: Camera at ${result.cameraPosition.target.lat.toFixed(4)}, ${result.cameraPosition.target.lng.toFixed(4)}, ${result.cameraPosition.zoom?.toFixed(4)}` + ); + }, []); + // Map 2 callbacks const onMarkerClick2 = useCallback( (marker: Marker) => { @@ -258,6 +271,18 @@ const MultipleMapsScreen = () => { ); }, []); + const onMapDrag2 = useCallback((result: DragResult) => { + showSnackbar( + `Map 2: Camera at ${result.cameraPosition.target.lat.toFixed(4)}, ${result.cameraPosition.target.lng.toFixed(4)}, ${result.cameraPosition.zoom?.toFixed(4)}` + ); + }, []); + + const onMapDragEnd2 = useCallback((result: DragResult) => { + showSnackbar( + `Map 2: Camera at ${result.cameraPosition.target.lat.toFixed(4)}, ${result.cameraPosition.target.lng.toFixed(4)}, ${result.cameraPosition.zoom?.toFixed(4)}` + ); + }, []); + const closeOverlay = (): void => { setOverlayType(OverlayType.None); }; @@ -320,6 +345,8 @@ const MultipleMapsScreen = () => { onPolylineClick={onPolylineClick1} onMarkerInfoWindowTapped={onMarkerInfoWindowTapped1} onMapClick={onMapClick1} + onMapDrag={onMapDrag1} + onMapDragEnd={onMapDragEnd1} onMapViewControllerCreated={setMapViewController1} onNavigationViewControllerCreated={setNavigationViewController1} /> @@ -346,6 +373,8 @@ const MultipleMapsScreen = () => { onPolylineClick={onPolylineClick2} onMarkerInfoWindowTapped={onMarkerInfoWindowTapped2} onMapClick={onMapClick2} + onMapDrag={onMapDrag2} + onMapDragEnd={onMapDragEnd2} onMapViewControllerCreated={setMapViewController2} /> {currentPage === 1 && ( diff --git a/example/src/screens/NavigationScreen.tsx b/example/src/screens/NavigationScreen.tsx index 1b81109..84aafb9 100644 --- a/example/src/screens/NavigationScreen.tsx +++ b/example/src/screens/NavigationScreen.tsx @@ -38,6 +38,7 @@ import { type Polyline, useNavigation, useNavigationAuto, + type DragResult, } from '@googlemaps/react-native-navigation-sdk'; import MapsControls from '../controls/mapsControls'; import AutoControls from '../controls/autoControls'; @@ -323,6 +324,18 @@ const NavigationScreen = () => { ); }, []); + const onMapDrag = useCallback((result: DragResult) => { + showSnackbar( + `Camera at ${result.cameraPosition.target.lat.toFixed(4)}, ${result.cameraPosition.target.lng.toFixed(4)}, ${result.cameraPosition.zoom?.toFixed(4)}` + ); + }, []); + + const onMapDragEnd = useCallback((result: DragResult) => { + showSnackbar( + `Camera at ${result.cameraPosition.target.lat.toFixed(4)}, ${result.cameraPosition.target.lng.toFixed(4)}, ${result.cameraPosition.zoom?.toFixed(4)}` + ); + }, []); + const closeOverlay = (): void => { setOverlayType(OverlayType.None); }; @@ -361,6 +374,8 @@ const NavigationScreen = () => { reportIncidentButtonEnabled={reportIncidentButtonEnabled} onMapReady={onMapReady} onMapClick={onMapClick} + onMapDrag={onMapDrag} + onMapDragEnd={onMapDragEnd} onMarkerClick={onMarkerClick} onPolylineClick={onPolylineClick} onPolygonClick={onPolygonClick} diff --git a/ios/react-native-navigation-sdk/INavigationViewCallback.h b/ios/react-native-navigation-sdk/INavigationViewCallback.h index de2aa68..68d2e1e 100644 --- a/ios/react-native-navigation-sdk/INavigationViewCallback.h +++ b/ios/react-native-navigation-sdk/INavigationViewCallback.h @@ -27,6 +27,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)handleMapReady; - (void)handleMapClick:(NSDictionary *)latLngMap; +- (void)handleMapDrag:(GMSCameraPosition *)cameraPosition; +- (void)handleMapDragEnd:(GMSCameraPosition *)cameraPosition; - (void)handleRecenterButtonClick; - (void)handleMarkerInfoWindowTapped:(GMSMarker *)marker; - (void)handleMarkerClick:(GMSMarker *)marker; diff --git a/ios/react-native-navigation-sdk/NavAutoModule.mm b/ios/react-native-navigation-sdk/NavAutoModule.mm index e12b860..c7640b4 100644 --- a/ios/react-native-navigation-sdk/NavAutoModule.mm +++ b/ios/react-native-navigation-sdk/NavAutoModule.mm @@ -217,6 +217,82 @@ - (void)addCircle:(CircleOptionsSpec &)options } } +- (void)coordinateForPoint:(PointSpec &)point + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject { + PointSpec optionsCopy(point); + if (_viewController) { + dispatch_async(dispatch_get_main_queue(), ^{ + CGPoint cgPoint = CGPointMake(optionsCopy.x(), optionsCopy.y()); + [self->_viewController coordinateForPoint:&cgPoint + result:^(NSDictionary *result) { + resolve(result); + }]; + }); + } else { + reject(@"NO_VIEW_CONTROLLER", @"No viewController found", nil); + } +} + +- (void)pointForCoordinate:(LatLngSpec &)coordinate + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + LatLngSpec optionsCopy(coordinate); + if (_viewController) { + dispatch_async(dispatch_get_main_queue(), ^{ + CLLocationCoordinate2D latLng = + CLLocationCoordinate2DMake(optionsCopy.lat(), optionsCopy.lng()); + [self->_viewController pointForCoordinate:&latLng + result:^(NSDictionary *result) { + resolve(result); + }]; + }); + } else { + reject(@"NO_VIEW_CONTROLLER", @"No viewController found", nil); + } +} + +- (void)fitBounds:(BoundsOptionsSpec &)boundsOptions + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + BoundsOptionsSpec optionsCopy(boundsOptions); + if (_viewController) { + dispatch_async(dispatch_get_main_queue(), ^{ + CLLocationCoordinate2D northEast = CLLocationCoordinate2DMake( + optionsCopy.bounds().northEast().lat(), optionsCopy.bounds().northEast().lng()); + CLLocationCoordinate2D southWest = CLLocationCoordinate2DMake( + optionsCopy.bounds().southWest().lat(), optionsCopy.bounds().southWest().lng()); + + UIEdgeInsets edgeInsets = UIEdgeInsetsMake(optionsCopy.padding().value().top().value_or(0), + optionsCopy.padding().value().left().value_or(0), + optionsCopy.padding().value().bottom().value_or(0), + optionsCopy.padding().value().right().value_or(0)); + [self->_viewController fitBounds:&northEast + southWest:&southWest + edgeInsets:&edgeInsets + result:^(NSDictionary *result) { + resolve(result); + }]; + }); + } else { + reject(@"NO_VIEW_CONTROLLER", @"No viewController found", nil); + } +} + +- (void)getBounds:(NSString *)id + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + if (_viewController) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self->_viewController getBounds:^(NSDictionary *result) { + resolve(result); + }]; + }); + } else { + reject(@"NO_VIEW_CONTROLLER", @"No viewController found", nil); + } +} + - (void)addPolyline:(PolylineOptionsSpec &)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { diff --git a/ios/react-native-navigation-sdk/NavView.mm b/ios/react-native-navigation-sdk/NavView.mm index cac635d..df49607 100644 --- a/ios/react-native-navigation-sdk/NavView.mm +++ b/ios/react-native-navigation-sdk/NavView.mm @@ -339,6 +339,24 @@ - (void)handleMarkerInfoWindowTapped:(GMSMarker *)marker { self.eventEmitter.onMarkerInfoWindowTapped(result); } +- (void)handleMapDrag:(GMSCameraPosition *)cameraPosition { + NavViewEventEmitter::OnMapDrag result = { + {{cameraPosition.target.latitude, cameraPosition.target.longitude}, + cameraPosition.bearing, + cameraPosition.viewingAngle, + cameraPosition.zoom}}; + self.eventEmitter.onMapDrag(result); +} + +- (void)handleMapDragEnd:(GMSCameraPosition *)cameraPosition { + NavViewEventEmitter::OnMapDragEnd result = { + {{cameraPosition.target.latitude, cameraPosition.target.longitude}, + cameraPosition.bearing, + cameraPosition.viewingAngle, + cameraPosition.zoom}}; + self.eventEmitter.onMapDragEnd(result); +} + - (void)handleMarkerClick:(GMSMarker *)marker { NavViewEventEmitter::OnMarkerClick result = { {marker.position.latitude, marker.position.longitude}, diff --git a/ios/react-native-navigation-sdk/NavViewController.h b/ios/react-native-navigation-sdk/NavViewController.h index 796c3a0..4f1d8bf 100644 --- a/ios/react-native-navigation-sdk/NavViewController.h +++ b/ios/react-native-navigation-sdk/NavViewController.h @@ -90,6 +90,14 @@ typedef void (^OnArrayResult)(NSArray *_Nullable result); - (void)addGroundOverlay:(GMSGroundOverlay *)groundOverlay visible:(BOOL)visible result:(OnDictionaryResult)completionBlock; +- (void)coordinateForPoint:(CGPoint *)point result:(OnDictionaryResult)completionBlock; +- (void)pointForCoordinate:(CLLocationCoordinate2D *)coordinate + result:(OnDictionaryResult)completionBlock; +- (void)fitBounds:(CLLocationCoordinate2D *)northEast + southWest:(CLLocationCoordinate2D *)southWest + edgeInsets:(UIEdgeInsets *)edgeInsets + result:(OnDictionaryResult)completionBlock; +- (void)getBounds:(OnDictionaryResult)completionBlock; - (GMSMapView *)mapView; - (void)showRouteOverview; - (void)removeMarker:(NSString *)markerId; diff --git a/ios/react-native-navigation-sdk/NavViewController.mm b/ios/react-native-navigation-sdk/NavViewController.mm index 059e5d5..54f300e 100644 --- a/ios/react-native-navigation-sdk/NavViewController.mm +++ b/ios/react-native-navigation-sdk/NavViewController.mm @@ -17,6 +17,7 @@ #import "NavViewController.h" #import #import +#import #import #import "CustomTypes.h" #import "NavModule.h" @@ -281,6 +282,14 @@ - (void)mapView:(GMSMapView *)mapView didTapAtCoordinate:(CLLocationCoordinate2D handleMapClick:[ObjectTranslationUtil transformCoordinateToDictionary:coordinate]]; } +- (void)mapView:(GMSMapView *)mapView didChangeCameraPosition:(GMSCameraPosition *)cameraPosition { + [_viewCallbacks handleMapDrag:cameraPosition]; +} + +- (void)mapView:(GMSMapView *)mapView idleAtCameraPosition:(GMSCameraPosition *)cameraPosition { + [_viewCallbacks handleMapDragEnd:cameraPosition]; +} + - (BOOL)mapView:(GMSMapView *)mapView didTapMarker:(GMSMarker *)marker { [_viewCallbacks handleMarkerClick:marker]; return FALSE; @@ -456,17 +465,7 @@ - (void)setNavigationUIEnabledPreference:(int)preference { } - (void)getCameraPosition:(OnDictionaryResult)completionBlock { - GMSCameraPosition *cam = _mapView.camera; - CLLocationCoordinate2D cameraPosition = _mapView.camera.target; - - NSMutableDictionary *map = [[NSMutableDictionary alloc] init]; - map[@"bearing"] = @(cam.bearing); - map[@"tilt"] = @(cam.viewingAngle); - map[@"zoom"] = @(cam.zoom); - - map[@"target"] = @{@"lat" : @(cameraPosition.latitude), @"lng" : @(cameraPosition.longitude)}; - - completionBlock(map); + completionBlock([ObjectTranslationUtil transformCameraPositionToDictionary:_mapView.camera]); } - (void)getMyLocation:(OnDictionaryResult)completionBlock { @@ -735,6 +734,60 @@ - (void)addCircle:(GMSCircle *)circle completionBlock([ObjectTranslationUtil transformCircleToDictionary:circle]); } +- (void)coordinateForPoint:(CGPoint *)point result:(OnDictionaryResult)completionBlock { + CLLocationCoordinate2D coordinate = [_mapView.projection coordinateForPoint:*point]; + + NSMutableDictionary *result = [[NSMutableDictionary alloc] init]; + result[@"lat"] = @(coordinate.latitude); + result[@"lng"] = @(coordinate.longitude); + + completionBlock(result); +} + +- (void)pointForCoordinate:(CLLocationCoordinate2D *)coordinate + result:(OnDictionaryResult)completionBlock { + CGPoint point = [_mapView.projection pointForCoordinate:*coordinate]; + + NSMutableDictionary *result = [[NSMutableDictionary alloc] init]; + result[@"x"] = @(point.x); + result[@"y"] = @(point.y); + + completionBlock(result); +} + +- (void)fitBounds:(CLLocationCoordinate2D *)northEast + southWest:(CLLocationCoordinate2D *)southWest + edgeInsets:(UIEdgeInsets *)edgeInsets + result:(OnDictionaryResult)completionBlock { + GMSCoordinateBounds *coordBounds = [[GMSCoordinateBounds alloc] initWithCoordinate:*northEast + coordinate:*southWest]; + [_mapView animateWithCameraUpdate:[GMSCameraUpdate fitBounds:coordBounds + withEdgeInsets:*edgeInsets]]; + + completionBlock(nil); +} + +- (void)getBounds:(OnDictionaryResult)completionBlock { + GMSVisibleRegion visibleRegion = [_mapView.projection visibleRegion]; + GMSCoordinateBounds *bounds = + [[GMSCoordinateBounds alloc] initWithCoordinate:visibleRegion.nearLeft + coordinate:visibleRegion.farRight]; + CLLocationCoordinate2D northEast = bounds.northEast; + CLLocationCoordinate2D southWest = bounds.southWest; + + NSMutableDictionary *result = [[NSMutableDictionary alloc] init]; + result[@"northEast"] = @{ + @"lat" : @(northEast.latitude), + @"lng" : @(northEast.longitude), + }; + result[@"southWest"] = @{ + @"lat" : @(southWest.latitude), + @"lng" : @(southWest.longitude), + }; + + completionBlock(result); +} + - (void)addMarker:(GMSMarker *)marker visible:(BOOL)visible result:(OnDictionaryResult)completionBlock { diff --git a/ios/react-native-navigation-sdk/NavViewModule.mm b/ios/react-native-navigation-sdk/NavViewModule.mm index b80d4f5..ab3f4f9 100644 --- a/ios/react-native-navigation-sdk/NavViewModule.mm +++ b/ios/react-native-navigation-sdk/NavViewModule.mm @@ -278,6 +278,92 @@ - (void)addPolygon:(NSString *)nativeID } } +- (void)coordinateForPoint:(NSString *)nativeID + point:(PointSpec &)point + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + NavViewController *viewController = [self getViewControllerForNativeID:nativeID]; + PointSpec optionsCopy(point); + if (viewController) { + dispatch_async(dispatch_get_main_queue(), ^{ + CGPoint cgPoint = CGPointMake(optionsCopy.x(), optionsCopy.y()); + [viewController coordinateForPoint:&cgPoint + result:^(NSDictionary *result) { + resolve(result); + }]; + }); + } else { + reject(@"NO_VIEW_CONTROLLER", @"No view controller found for the specified nativeID", nil); + } +} + +- (void)pointForCoordinate:(NSString *)nativeID + coordinate:(LatLngSpec &)coordinate + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + NavViewController *viewController = [self getViewControllerForNativeID:nativeID]; + LatLngSpec optionsCopy(coordinate); + if (viewController) { + dispatch_async(dispatch_get_main_queue(), ^{ + CLLocationCoordinate2D latLng = + CLLocationCoordinate2DMake(optionsCopy.lat(), optionsCopy.lng()); + [viewController pointForCoordinate:&latLng + result:^(NSDictionary *result) { + resolve(result); + }]; + }); + } else { + reject(@"NO_VIEW_CONTROLLER", @"No view controller found for the specified nativeID", nil); + } +} + +- (void)fitBounds:(NSString *)nativeID + boundsOptions:(BoundsOptionsSpec &)boundsOptions + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + NavViewController *viewController = [self getViewControllerForNativeID:nativeID]; + BoundsOptionsSpec optionsCopy(boundsOptions); + if (viewController) { + dispatch_async(dispatch_get_main_queue(), ^{ + CLLocationCoordinate2D northEast = CLLocationCoordinate2DMake( + optionsCopy.bounds().northEast().lat(), optionsCopy.bounds().northEast().lng()); + CLLocationCoordinate2D southWest = CLLocationCoordinate2DMake( + optionsCopy.bounds().southWest().lat(), optionsCopy.bounds().southWest().lng()); + + UIEdgeInsets edgeInsets = + optionsCopy.padding().has_value() + ? UIEdgeInsetsMake(optionsCopy.padding().value().top().value_or(0), + optionsCopy.padding().value().left().value_or(0), + optionsCopy.padding().value().bottom().value_or(0), + optionsCopy.padding().value().right().value_or(0)) + : UIEdgeInsetsMake(0, 0, 0, 0); + [viewController fitBounds:&northEast + southWest:&southWest + edgeInsets:&edgeInsets + result:^(NSDictionary *result) { + resolve(result); + }]; + }); + } else { + reject(@"NO_VIEW_CONTROLLER", @"No view controller found for the specified nativeID", nil); + } +} + +- (void)getBounds:(NSString *)nativeID + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + NavViewController *viewController = [self getViewControllerForNativeID:nativeID]; + if (viewController) { + dispatch_async(dispatch_get_main_queue(), ^{ + [viewController getBounds:^(NSDictionary *result) { + resolve(result); + }]; + }); + } else { + reject(@"NO_VIEW_CONTROLLER", @"No view controller found for the specified nativeID", nil); + } +} + - (void)addGroundOverlay:(NSString *)nativeID options:(GroundOverlayOptionsSpec &)options resolve:(RCTPromiseResolveBlock)resolve diff --git a/ios/react-native-navigation-sdk/ObjectTranslationUtil.h b/ios/react-native-navigation-sdk/ObjectTranslationUtil.h index 472dd60..6521f69 100644 --- a/ios/react-native-navigation-sdk/ObjectTranslationUtil.h +++ b/ios/react-native-navigation-sdk/ObjectTranslationUtil.h @@ -32,6 +32,7 @@ + (NSDictionary *)transformCircleToDictionary:(GMSCircle *)circle; + (NSDictionary *)transformGroundOverlayToDictionary:(GMSGroundOverlay *)groundOverlay; + (GMSPath *)transformToPath:(NSArray *)latLngs; ++ (NSDictionary *)transformCameraPositionToDictionary:(GMSCameraPosition *)position; + (CLLocationCoordinate2D)getLocationCoordinateFrom:(NSDictionary *)latLngMap; + (BOOL)isIdOnUserData:(nullable id)userData; diff --git a/ios/react-native-navigation-sdk/ObjectTranslationUtil.mm b/ios/react-native-navigation-sdk/ObjectTranslationUtil.mm index b5de26f..f49476f 100644 --- a/ios/react-native-navigation-sdk/ObjectTranslationUtil.mm +++ b/ios/react-native-navigation-sdk/ObjectTranslationUtil.mm @@ -244,11 +244,15 @@ + (GMSPath *)transformToPath:(NSArray *)latLngs { return path; } -+ (CLLocationCoordinate2D)getLocationCoordinateFrom:(NSDictionary *)latLngMap { - double latitude = [[latLngMap objectForKey:@"lat"] doubleValue]; - double longitude = [[latLngMap objectForKey:@"lng"] doubleValue]; ++ (NSDictionary *)transformCameraPositionToDictionary:(GMSCameraPosition *)cam { + NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init]; + dictionary[@"bearing"] = @(cam.bearing); + dictionary[@"tilt"] = @(cam.viewingAngle); + dictionary[@"zoom"] = @(cam.zoom); + + dictionary[@"target"] = @{@"lat" : @(cam.target.latitude), @"lng" : @(cam.target.longitude)}; - return CLLocationCoordinate2DMake(latitude, longitude); + return dictionary; } + (BOOL)isIdOnUserData:(nullable id)userData { diff --git a/src/auto/useNavigationAuto.ts b/src/auto/useNavigationAuto.ts index c743d32..f82de7d 100644 --- a/src/auto/useNavigationAuto.ts +++ b/src/auto/useNavigationAuto.ts @@ -21,6 +21,9 @@ import { type Location, colorIntToRGBA, processColorValue, + type Point, + type LatLng, + type Bounds, } from '../shared'; import type { MapType, @@ -40,6 +43,7 @@ import type { GroundOverlayBoundsOptions, GroundOverlayPositionOptions, MapColorScheme, + BoundsOptions, } from '../maps'; import type { NavigationNightMode } from '../navigation'; import { useMemo, useCallback, useRef } from 'react'; @@ -205,6 +209,22 @@ export const useNavigationAuto = (): UseNavigationAutoResult => { }; }, + coordinateForPoint: async (point: Point): Promise => { + return await NavAutoModule.coordinateForPoint(point); + }, + + pointForCoordinate: async (coordinate: LatLng): Promise => { + return await NavAutoModule.pointForCoordinate(coordinate); + }, + + fitBounds: async (boundsOptions: BoundsOptions) => { + return await NavAutoModule.fitBounds(boundsOptions); + }, + + getBounds: async (): Promise => { + return await NavAutoModule.getBounds(); + }, + addMarker: async (markerOptions: MarkerOptions): Promise => { return await NavAutoModule.addMarker(markerOptions); }, diff --git a/src/maps/mapView/mapView.tsx b/src/maps/mapView/mapView.tsx index a8cae77..4b755cb 100644 --- a/src/maps/mapView/mapView.tsx +++ b/src/maps/mapView/mapView.tsx @@ -71,6 +71,8 @@ export const MapView = (props: MapViewProps): React.JSX.Element => { // Use the new architecture event callback hook const onMapClick = useNativeEventCallback(props.onMapClick); const onMapReady = useNativeEventCallback(props.onMapReady); + const onMapDrag = useNativeEventCallback(props.onMapDrag); + const onMapDragEnd = useNativeEventCallback(props.onMapDragEnd); const onMarkerClick = useNativeEventCallback(props.onMarkerClick); const onPolylineClick = useNativeEventCallback(props.onPolylineClick); const onPolygonClick = useNativeEventCallback(props.onPolygonClick); @@ -112,6 +114,8 @@ export const MapView = (props: MapViewProps): React.JSX.Element => { maxZoomLevel={props.maxZoomLevel} onMapClick={onMapClick} onMapReady={onMapReady} + onMapDrag={onMapDrag} + onMapDragEnd={onMapDragEnd} onMarkerClick={onMarkerClick} onPolylineClick={onPolylineClick} onPolygonClick={onPolygonClick} diff --git a/src/maps/mapView/mapViewController.ts b/src/maps/mapView/mapViewController.ts index 04f6e93..7aea8a6 100644 --- a/src/maps/mapView/mapViewController.ts +++ b/src/maps/mapView/mapViewController.ts @@ -16,7 +16,7 @@ import NavViewModule from '../../native/NativeNavViewModule'; import { processColorValue, colorIntToRGBA } from '../../shared'; -import type { Location } from '../../shared/types'; +import type { Location, Point, LatLng, Bounds } from '../../shared/types'; import type { CameraPosition, Circle, @@ -27,6 +27,7 @@ import type { UISettings, } from '../types'; import type { + BoundsOptions, CircleOptions, GroundOverlayBoundsOptions, GroundOverlayOptions, @@ -67,6 +68,22 @@ export const getMapViewController = (nativeID: string): MapViewController => { }; }, + coordinateForPoint: async (point: Point): Promise => { + return await NavViewModule.coordinateForPoint(nativeID, point); + }, + + pointForCoordinate: async (coordinate: LatLng): Promise => { + return await NavViewModule.pointForCoordinate(nativeID, coordinate); + }, + + fitBounds: async (boundsOptions: BoundsOptions) => { + return await NavViewModule.fitBounds(nativeID, boundsOptions); + }, + + getBounds: async (): Promise => { + return await NavViewModule.getBounds(nativeID); + }, + addMarker: async (markerOptions: MarkerOptions): Promise => { return await NavViewModule.addMarker(nativeID, markerOptions); }, diff --git a/src/maps/mapView/types.ts b/src/maps/mapView/types.ts index 59ea599..f16367e 100644 --- a/src/maps/mapView/types.ts +++ b/src/maps/mapView/types.ts @@ -15,7 +15,7 @@ */ import type { ColorValue } from 'react-native'; -import type { LatLng, Location } from '../../shared/types'; +import type { LatLng, Location, Point, Bounds } from '../../shared/types'; import type { CameraPosition, Circle, @@ -221,6 +221,16 @@ export interface Padding { right?: number; } +/** + * Defines bounds options for a geographical bounds with padding. + */ +export interface BoundsOptions { + /** Geographical bounds. */ + bounds: Bounds; + /** Padding within the bounds. */ + padding?: Padding; +} + /** * Defines the type of the map view. */ @@ -248,6 +258,30 @@ export interface MapViewController { */ addCircle(circleOptions: CircleOptions): Promise; + /** + * Maps a point coordinate in the map’s view to an Earth coordinate. + * @param point - Object specifying the point. + */ + coordinateForPoint(point: Point): Promise; + + /** + * Maps an Earth coordinate to a point coordinate in the map’s view. + * @param coordinate - Object specifying the coordinate. + */ + pointForCoordinate(coordinate: LatLng): Promise; + + /** + * Transforms the camera such that the specified bounds are centered on screen. + * @param boundsOptions - Object specifying the bounds options. + */ + fitBounds(boundsOptions: BoundsOptions): Promise; + + /** + * Retrieves the rectangular bounds of the map view. + * @param bounds - Object specifying the bounds. + */ + getBounds(): Promise; + /** * Add or update a marker on the map. * If a marker with the same `id` already exists, it will be updated with the new options. diff --git a/src/maps/types.ts b/src/maps/types.ts index 646e24a..2594c63 100644 --- a/src/maps/types.ts +++ b/src/maps/types.ts @@ -194,6 +194,14 @@ export enum MapColorScheme { DARK = 2, } +/** + * Defines the results of a map drag event. + */ +export interface DragResult { + /** Camera position. */ + cameraPosition: CameraPosition; +} + /** * `MapViewProps` interface provides methods focused on managing map events and state changes. */ @@ -239,6 +247,18 @@ export interface MapViewProps { */ readonly onMapClick?: (latLng: LatLng) => void; + /** + * Callback invoked repeated when map is being dragged. + * @param result The drag result at that instant. + */ + readonly onMapDrag?: (result: DragResult) => void; + + /** + * Callback invoked when map drag ends. + * @param result The drag result at that instant. + */ + readonly onMapDragEnd?: (result: DragResult) => void; + readonly style?: StyleProp | undefined; /** diff --git a/src/native/NativeNavAutoModule.ts b/src/native/NativeNavAutoModule.ts index 9b33460..a12c4cd 100644 --- a/src/native/NativeNavAutoModule.ts +++ b/src/native/NativeNavAutoModule.ts @@ -17,7 +17,7 @@ import type { TurboModule } from 'react-native'; import { TurboModuleRegistry } from 'react-native'; -import type { Location } from '../shared'; +import type { Bounds, LatLng, Location, Point } from '../shared'; import type { Circle, Marker, @@ -127,12 +127,43 @@ type CustomNavigationAutoEventSpec = Readonly<{ data?: string | null; }>; +type PointSpec = Readonly<{ + x: Float; + y: Float; +}>; + +type LatLngSpec = Readonly<{ + lat: Float; + lng: Float; +}>; + +type PaddingSpec = Readonly<{ + top?: Float; + left?: Float; + bottom?: Float; + right?: Float; +}>; + +type BoundsSpec = Readonly<{ + northEast: LatLngSpec; + southWest: LatLngSpec; +}>; + +type BoundsOptionsSpec = Readonly<{ + bounds: BoundsSpec; + padding?: PaddingSpec; +}>; + export interface Spec extends TurboModule { isAutoScreenAvailable(): Promise; setMapType(mapType: Double): void; setMapStyle(mapStyle: string): void; clearMapView(): Promise; addCircle(options: CircleOptionsSpec): Promise; + coordinateForPoint(nativeID: string, point: PointSpec): Promise; + pointForCoordinate(nativeID: string, coordinate: LatLngSpec): Promise; + fitBounds(nativeID: string, boundsOptions: BoundsOptionsSpec): Promise; + getBounds(nativeID: string): Promise; addMarker(options: MarkerOptionsSpec): Promise; addPolyline(options: PolylineOptionsSpec): Promise; addPolygon(options: PolygonOptionsSpec): Promise; diff --git a/src/native/NativeNavViewComponent.ts b/src/native/NativeNavViewComponent.ts index a03da68..1561650 100644 --- a/src/native/NativeNavViewComponent.ts +++ b/src/native/NativeNavViewComponent.ts @@ -132,6 +132,22 @@ export interface NativeNavViewProps extends ViewProps { // Event handlers onMapReady?: DirectEventHandler; onMapClick?: DirectEventHandler<{ lat: Float; lng: Float }>; + onMapDrag?: DirectEventHandler<{ + cameraPosition: { + target: { lat: Float; lng: Float }; + bearing?: Float; + tilt?: Float; + zoom?: Float; + }; + }>; + onMapDragEnd?: DirectEventHandler<{ + cameraPosition: { + target: { lat: Float; lng: Float }; + bearing?: Float; + tilt?: Float; + zoom?: Float; + }; + }>; onMarkerClick?: DirectEventHandler<{ position: { lat: Float; lng: Float }; id: string; diff --git a/src/native/NativeNavViewModule.ts b/src/native/NativeNavViewModule.ts index 7828d96..ab679c5 100644 --- a/src/native/NativeNavViewModule.ts +++ b/src/native/NativeNavViewModule.ts @@ -17,7 +17,7 @@ import type { TurboModule } from 'react-native'; import { TurboModuleRegistry } from 'react-native'; -import type { Location } from '../shared'; +import type { Bounds, LatLng, Location, Point } from '../shared'; import type { Circle, Marker, @@ -120,6 +120,33 @@ type GroundOverlayOptionsSpec = Readonly<{ zIndex?: WithDefault; }>; +type PointSpec = Readonly<{ + x: Float; + y: Float; +}>; + +type LatLngSpec = Readonly<{ + lat: Float; + lng: Float; +}>; + +type PaddingSpec = Readonly<{ + top?: Float; + left?: Float; + bottom?: Float; + right?: Float; +}>; + +type BoundsSpec = Readonly<{ + northEast: LatLngSpec; + southWest: LatLngSpec; +}>; + +type BoundsOptionsSpec = Readonly<{ + bounds: BoundsSpec; + padding?: PaddingSpec; +}>; + /** * TurboModule for map view operations. * @@ -129,6 +156,10 @@ type GroundOverlayOptionsSpec = Readonly<{ */ export interface Spec extends TurboModule { addCircle(nativeID: string, options: CircleOptionsSpec): Promise; + coordinateForPoint(nativeID: string, point: PointSpec): Promise; + pointForCoordinate(nativeID: string, coordinate: LatLngSpec): Promise; + fitBounds(nativeID: string, boundsOptions: BoundsOptionsSpec): Promise; + getBounds(nativeID: string): Promise; addMarker(nativeID: string, options: MarkerOptionsSpec): Promise; addPolyline( nativeID: string, diff --git a/src/navigation/navigationView/navigationView.tsx b/src/navigation/navigationView/navigationView.tsx index 22f4d15..f99e44a 100644 --- a/src/navigation/navigationView/navigationView.tsx +++ b/src/navigation/navigationView/navigationView.tsx @@ -198,6 +198,8 @@ export const NavigationView = ( // Use the new architecture event callback hook const onMapClick = useNativeEventCallback(props.onMapClick); const onMapReady = useNativeEventCallback(props.onMapReady); + const onMapDrag = useNativeEventCallback(props.onMapDrag); + const onMapDragEnd = useNativeEventCallback(props.onMapDragEnd); const onMarkerClick = useNativeEventCallback(props.onMarkerClick); const onPolylineClick = useNativeEventCallback(props.onPolylineClick); const onPolygonClick = useNativeEventCallback(props.onPolygonClick); @@ -268,6 +270,8 @@ export const NavigationView = ( maxZoomLevel={props.maxZoomLevel} onMapClick={onMapClick} onMapReady={onMapReady} + onMapDrag={onMapDrag} + onMapDragEnd={onMapDragEnd} onMarkerClick={onMarkerClick} onPolylineClick={onPolylineClick} onPolygonClick={onPolygonClick} diff --git a/src/shared/types.ts b/src/shared/types.ts index 1a6246f..895296d 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -77,3 +77,23 @@ export interface Location { */ time: number; } + +/** + * An immutable class representing a point on the screen. + */ +export interface Point { + /** Value representing the x coordinate. */ + x: number; + /** Value representing the y coordinate. */ + y: number; +} + +/** + * An immutable class representing a rectangular bounding box on the Earth’s surface. + */ +export interface Bounds { + /** The North-East corner of these bounds. */ + northEast: LatLng; + /** The South-West corner of these bounds. */ + southWest: LatLng; +}