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 bb3a0c2..9f30f82 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 @@ -15,15 +15,18 @@ import android.annotation.SuppressLint; import android.app.Activity; +import android.util.Log; import androidx.core.util.Supplier; import com.facebook.react.bridge.UiThreadUtil; import com.google.android.gms.maps.CameraUpdateFactory; import com.google.android.gms.maps.GoogleMap; +import com.google.android.gms.maps.GoogleMap.CameraPerspective; import com.google.android.gms.maps.model.BitmapDescriptor; import com.google.android.gms.maps.model.BitmapDescriptorFactory; import com.google.android.gms.maps.model.CameraPosition; import com.google.android.gms.maps.model.Circle; import com.google.android.gms.maps.model.CircleOptions; +import com.google.android.gms.maps.model.FollowMyLocationOptions; import com.google.android.gms.maps.model.GroundOverlay; import com.google.android.gms.maps.model.GroundOverlayOptions; import com.google.android.gms.maps.model.LatLng; @@ -42,6 +45,7 @@ import java.util.Map; public class MapViewController implements INavigationViewControllerProperties { + private static final String TAG = "MapViewController"; private GoogleMap mGoogleMap; private Supplier activitySupplier; private INavigationViewCallback mNavigationViewCallback; @@ -883,23 +887,35 @@ public void setMinZoomLevel(float minZoomLevel) { return; } - // Get the effective max zoom for comparison - float maxZoom = + minZoomLevelPreference = minZoomLevel; + + // Reset both preferences first so the new min/max pair is always applied + // atomically. Without this, Fabric can deliver minZoomLevel and maxZoomLevel + // prop updates in any order, causing a transient state where min > max. + mGoogleMap.resetMinMaxZoomPreference(); + + float effectiveMin = (minZoomLevel < 0.0f) ? mGoogleMap.getMinZoomLevel() : minZoomLevel; + float effectiveMax = (maxZoomLevelPreference != null && maxZoomLevelPreference >= 0.0f) ? maxZoomLevelPreference : mGoogleMap.getMaxZoomLevel(); - // Validate that min is not greater than max (unless using -1 sentinel) - if (minZoomLevel >= 0.0f && minZoomLevel > maxZoom) { - throw new IllegalArgumentException( - "Minimum zoom level cannot be greater than maximum zoom level"); + if (effectiveMin > effectiveMax) { + Log.w( + TAG, + "minZoomLevel (" + + effectiveMin + + ") is greater than maxZoomLevel (" + + effectiveMax + + "). Ignoring zoom level constraints."); + return; } - minZoomLevelPreference = minZoomLevel; - - // Use map's current minZoomLevel if -1 is provided - float effectiveMin = (minZoomLevel < 0.0f) ? mGoogleMap.getMinZoomLevel() : minZoomLevel; mGoogleMap.setMinZoomPreference(effectiveMin); + + if (maxZoomLevelPreference != null) { + mGoogleMap.setMaxZoomPreference(effectiveMax); + } } @Override @@ -908,23 +924,35 @@ public void setMaxZoomLevel(float maxZoomLevel) { return; } - // Get the effective min zoom for comparison - float minZoom = + maxZoomLevelPreference = maxZoomLevel; + + // Reset both preferences first so the new min/max pair is always applied + // atomically. Without this, Fabric can deliver minZoomLevel and maxZoomLevel + // prop updates in any order, causing a transient state where min > max. + mGoogleMap.resetMinMaxZoomPreference(); + + float effectiveMax = (maxZoomLevel < 0.0f) ? mGoogleMap.getMaxZoomLevel() : maxZoomLevel; + float effectiveMin = (minZoomLevelPreference != null && minZoomLevelPreference >= 0.0f) ? minZoomLevelPreference : mGoogleMap.getMinZoomLevel(); - // Validate that max is not less than min (unless using -1 sentinel) - if (maxZoomLevel >= 0.0f && maxZoomLevel < minZoom) { - throw new IllegalArgumentException( - "Maximum zoom level cannot be less than minimum zoom level"); + if (effectiveMin > effectiveMax) { + Log.w( + TAG, + "minZoomLevel (" + + effectiveMin + + ") is greater than maxZoomLevel (" + + effectiveMax + + "). Ignoring zoom level constraints."); + return; } - maxZoomLevelPreference = maxZoomLevel; - - // Use map's current maxZoomLevel if -1 is provided - float effectiveMax = (maxZoomLevel < 0.0f) ? mGoogleMap.getMaxZoomLevel() : maxZoomLevel; mGoogleMap.setMaxZoomPreference(effectiveMax); + + if (minZoomLevelPreference != null) { + mGoogleMap.setMinZoomPreference(effectiveMin); + } } public void setZoomGesturesEnabled(boolean enabled) { @@ -1005,16 +1033,27 @@ public void resetMinMaxZoomLevel() { return; } + minZoomLevelPreference = null; + maxZoomLevelPreference = null; mGoogleMap.resetMinMaxZoomPreference(); } @SuppressLint("MissingPermission") - public void setFollowingPerspective(int jsValue) { + public void setFollowingPerspective(int jsValue, Float zoomLevel) { if (mGoogleMap == null) { return; } - mGoogleMap.followMyLocation(EnumTranslationUtil.getCameraPerspectiveFromJsValue(jsValue)); + @CameraPerspective + int perspective = EnumTranslationUtil.getCameraPerspectiveFromJsValue(jsValue); + + if (zoomLevel != null) { + FollowMyLocationOptions options = + FollowMyLocationOptions.builder().setZoomLevel(zoomLevel).build(); + mGoogleMap.followMyLocation(perspective, options); + } else { + mGoogleMap.followMyLocation(perspective); + } } public void setPadding(int top, int left, int bottom, int right) { 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 8c8aae5..d9247ae 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 @@ -393,14 +393,15 @@ public void setMyLocationEnabled(boolean enabled) { } @Override - public void setFollowingPerspective(double perspective) { + public void setFollowingPerspective(double perspective, Double zoomLevel) { int jsValue = (int) perspective; UiThreadUtil.runOnUiThread( () -> { if (mMapViewController == null) { return; } - mMapViewController.setFollowingPerspective(jsValue); + mMapViewController.setFollowingPerspective( + jsValue, zoomLevel == null ? null : zoomLevel.floatValue()); }); } 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..e4f8570 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 @@ -405,7 +405,8 @@ public void setNavigationUIEnabled(String nativeID, boolean enabled, final Promi } @Override - public void setFollowingPerspective(String nativeID, double perspective, final Promise promise) { + public void setFollowingPerspective( + String nativeID, double perspective, Double zoomLevel, final Promise promise) { UiThreadUtil.runOnUiThread( () -> { IMapViewFragment fragment = mNavViewManager.getFragmentByNativeId(nativeID); @@ -415,7 +416,10 @@ public void setFollowingPerspective(String nativeID, double perspective, final P } if (fragment instanceof INavViewFragment) { - fragment.getMapController().setFollowingPerspective((int) perspective); + fragment + .getMapController() + .setFollowingPerspective( + (int) perspective, zoomLevel == null ? null : zoomLevel.floatValue()); promise.resolve(null); } else { promise.reject(JsErrors.NOT_NAV_VIEW_ERROR_CODE, JsErrors.NOT_NAV_VIEW_ERROR_MESSAGE); diff --git a/example/e2e/map.test.js b/example/e2e/map.test.js index dda09a7..e381400 100644 --- a/example/e2e/map.test.js +++ b/example/e2e/map.test.js @@ -89,4 +89,11 @@ describe('Map view tests', () => { await expectNoErrors(); await expectSuccess(); }); + + it('MT10 - test min and max zoom level constraints', async () => { + await selectTestByName('testMinMaxZoomLevels'); + await waitForTestToFinish(); + await expectNoErrors(); + await expectSuccess(); + }); }); diff --git a/example/e2e/navigation.test.js b/example/e2e/navigation.test.js index b7648b8..30dc674 100644 --- a/example/e2e/navigation.test.js +++ b/example/e2e/navigation.test.js @@ -90,4 +90,12 @@ describe('Navigation tests', () => { await expectNoErrors(); await expectSuccess(); }); + + it('NT09 - test setFollowingPerspective with zoom level options', async () => { + await selectTestByName('testSetFollowingPerspective'); + await agreeToTermsAndConditions(); + await waitForTestToFinish(); + await expectNoErrors(); + await expectSuccess(); + }); }); diff --git a/example/src/screens/IntegrationTestsScreen.tsx b/example/src/screens/IntegrationTestsScreen.tsx index 0bc40bf..7540a76 100644 --- a/example/src/screens/IntegrationTestsScreen.tsx +++ b/example/src/screens/IntegrationTestsScreen.tsx @@ -55,6 +55,8 @@ import { testStartGuidanceWithoutDestinations, testRouteTokenOptionsValidation, testMapStyle, + testMinMaxZoomLevels, + testSetFollowingPerspective, NO_ERRORS_DETECTED_LABEL, } from './integration_tests/integration_test'; @@ -127,6 +129,12 @@ const IntegrationTestsScreen = () => { boolean | undefined >(undefined); const [mapStyle, setMapStyle] = useState(undefined); + const [minZoomLevel, setMinZoomLevel] = useState( + undefined + ); + const [maxZoomLevel, setMaxZoomLevel] = useState( + undefined + ); const onMapReady = useCallback(async () => { try { @@ -236,6 +244,8 @@ const IntegrationTestsScreen = () => { setZoomControlsEnabled, setMapToolbarEnabled, setMapStyle, + setMinZoomLevel, + setMaxZoomLevel, }; }; @@ -305,6 +315,12 @@ const IntegrationTestsScreen = () => { case 'testMapStyle': await testMapStyle(getTestTools()); break; + case 'testMinMaxZoomLevels': + await testMinMaxZoomLevels(getTestTools()); + break; + case 'testSetFollowingPerspective': + await testSetFollowingPerspective(getTestTools()); + break; default: resetTestState(); break; @@ -347,6 +363,8 @@ const IntegrationTestsScreen = () => { zoomControlsEnabled={zoomControlsEnabled} mapToolbarEnabled={mapToolbarEnabled} mapStyle={mapStyle} + minZoomLevel={minZoomLevel} + maxZoomLevel={maxZoomLevel} /> @@ -518,6 +536,20 @@ const IntegrationTestsScreen = () => { }} testID="testMapStyle" /> + { + runTest('testMinMaxZoomLevels'); + }} + testID="testMinMaxZoomLevels" + /> + { + runTest('testSetFollowingPerspective'); + }} + testID="testSetFollowingPerspective" + /> ); diff --git a/example/src/screens/integration_tests/integration_test.ts b/example/src/screens/integration_tests/integration_test.ts index f342251..c83dd29 100644 --- a/example/src/screens/integration_tests/integration_test.ts +++ b/example/src/screens/integration_tests/integration_test.ts @@ -16,6 +16,7 @@ import { AudioGuidance, + CameraPerspective, TravelMode, NavigationSessionStatus, RouteStatus, @@ -64,6 +65,8 @@ interface TestTools { setZoomControlsEnabled: (enabled: boolean | undefined) => void; setMapToolbarEnabled: (enabled: boolean | undefined) => void; setMapStyle: (style: string | undefined) => void; + setMinZoomLevel: (level: number | undefined) => void; + setMaxZoomLevel: (level: number | undefined) => void; } const NAVIGATOR_NOT_READY_ERROR_CODE = 'NO_NAVIGATOR_ERROR_CODE'; @@ -1667,3 +1670,199 @@ export const testMapStyle = async (testTools: TestTools) => { failTest(`Failed to set mapStyle: ${error}`); } }; + +export const testMinMaxZoomLevels = async (testTools: TestTools) => { + const { + mapViewController, + passTest, + failTest, + expectFalseError, + setMinZoomLevel, + setMaxZoomLevel, + } = testTools; + + if (!mapViewController) { + return failTest('mapViewController was expected to exist'); + } + + try { + // Test 1: Set valid min and max zoom levels (min < max) + setMinZoomLevel(5); + setMaxZoomLevel(15); + await delay(200); + + // Verify camera zoom is constrained - move to zoom below min + mapViewController.moveCamera({ + target: { lat: 37.7749, lng: -122.4194 }, + bearing: 0, + tilt: 0, + zoom: 3, + }); + await delay(200); + + const posAfterMinClamp = await mapViewController.getCameraPosition(); + if ((posAfterMinClamp.zoom ?? 0) < 5) { + return expectFalseError( + `zoom (${posAfterMinClamp.zoom}) should be >= minZoomLevel (5)` + ); + } + + // Move to zoom above max + mapViewController.moveCamera({ + target: { lat: 37.7749, lng: -122.4194 }, + bearing: 0, + tilt: 0, + zoom: 20, + }); + await delay(200); + + const posAfterMaxClamp = await mapViewController.getCameraPosition(); + if ((posAfterMaxClamp.zoom ?? 0) > 15) { + return expectFalseError( + `zoom (${posAfterMaxClamp.zoom}) should be <= maxZoomLevel (15)` + ); + } + + // Test 2: Set zoom within range - should work normally + mapViewController.moveCamera({ + target: { lat: 37.7749, lng: -122.4194 }, + bearing: 0, + tilt: 0, + zoom: 10, + }); + await delay(200); + + const posInRange = await mapViewController.getCameraPosition(); + if (posInRange.zoom !== 10) { + return expectFalseError( + `zoom (${posInRange.zoom}) should be 10 when within range` + ); + } + + // Test 3: Reset zoom levels + setMinZoomLevel(undefined); + setMaxZoomLevel(undefined); + await delay(200); + + // Test 4: Invalid case - min > max (should not crash, constraints ignored) + setMinZoomLevel(15); + setMaxZoomLevel(5); + await delay(200); + + // Verify app did not crash - camera operations should still work + mapViewController.moveCamera({ + target: { lat: 37.7749, lng: -122.4194 }, + bearing: 0, + tilt: 0, + zoom: 10, + }); + await delay(200); + + const posAfterInvalid = await mapViewController.getCameraPosition(); + if (!posAfterInvalid) { + return failTest('getCameraPosition failed after invalid min/max zoom'); + } + + // Clean up - reset to defaults + setMinZoomLevel(undefined); + setMaxZoomLevel(undefined); + await delay(200); + + passTest(); + } catch (error) { + failTest(`testMinMaxZoomLevels failed: ${error}`); + } +}; + +export const testSetFollowingPerspective = async (testTools: TestTools) => { + const { + navigationController, + mapViewController, + navigationViewController, + setOnNavigationReady, + setOnLocationChanged, + passTest, + failTest, + expectFalseError, + } = testTools; + + // Accept ToS first + if (!(await acceptToS(navigationController, failTest))) { + return; + } + + const startLocation: LatLng = { lat: 37.4195823, lng: -122.0799018 }; + + setOnNavigationReady(async () => { + disableVoiceGuidanceForTests(navigationController); + const located = await simulateAndWaitForLocation( + navigationController, + setOnLocationChanged, + startLocation + ); + if (!located) { + return failTest( + 'Timed out waiting for simulated location to be confirmed' + ); + } + + if (!navigationViewController) { + return failTest('navigationViewController was expected to exist'); + } + + if (!mapViewController) { + return failTest('mapViewController was expected to exist'); + } + + try { + // Test 1: Set following perspective without zoom (default behavior) + await navigationViewController.setFollowingPerspective( + CameraPerspective.TILTED + ); + + // Test 2: Set following perspective with a fixed zoom level + await navigationViewController.setFollowingPerspective( + CameraPerspective.TOP_DOWN_NORTH_UP, + { zoomLevel: 15 } + ); + + const posAfterZoom15 = await waitForCondition( + () => mapViewController.getCameraPosition(), + pos => Math.abs((pos.zoom ?? 0) - 15) <= 0.5 + ); + if (!posAfterZoom15) { + return expectFalseError( + 'Timed out waiting for zoom ~15 after setFollowingPerspective' + ); + } + + // Test 3: Set following perspective with a different zoom level + await navigationViewController.setFollowingPerspective( + CameraPerspective.TOP_DOWN_HEADING_UP, + { zoomLevel: 10 } + ); + + const posAfterZoom10 = await waitForCondition( + () => mapViewController.getCameraPosition(), + pos => Math.abs((pos.zoom ?? 0) - 10) <= 0.5 + ); + if (!posAfterZoom10) { + return expectFalseError( + 'Timed out waiting for zoom ~10 after setFollowingPerspective' + ); + } + + // Test 4: Reset zoom by omitting options (should use default auto-zoom) + await navigationViewController.setFollowingPerspective( + CameraPerspective.TILTED + ); + + navigationController.cleanup(); + passTest(); + } catch (error) { + failTest(`testSetFollowingPerspective failed: ${error}`); + } + }); + + await initializeNavigation(navigationController, failTest); +}; diff --git a/ios/react-native-navigation-sdk/NavAutoModule.mm b/ios/react-native-navigation-sdk/NavAutoModule.mm index e12b860..adfd47a 100644 --- a/ios/react-native-navigation-sdk/NavAutoModule.mm +++ b/ios/react-native-navigation-sdk/NavAutoModule.mm @@ -548,10 +548,11 @@ - (void)setNightMode:(NSInteger)nightMode { }); } -- (void)setFollowingPerspective:(NSInteger)perspective { +- (void)setFollowingPerspective:(NSInteger)perspective zoomLevel:(NSNumber *)zoomLevel { dispatch_async(dispatch_get_main_queue(), ^{ if (self->_viewController) { - [self->_viewController setFollowingPerspective:[NSNumber numberWithInteger:perspective]]; + [self->_viewController setFollowingPerspective:[NSNumber numberWithInteger:perspective] + zoomLevel:zoomLevel]; } }); } diff --git a/ios/react-native-navigation-sdk/NavViewController.h b/ios/react-native-navigation-sdk/NavViewController.h index 796c3a0..3fd222e 100644 --- a/ios/react-native-navigation-sdk/NavViewController.h +++ b/ios/react-native-navigation-sdk/NavViewController.h @@ -45,7 +45,7 @@ typedef void (^OnArrayResult)(NSArray *_Nullable result); - (void)setReportIncidentButtonEnabled:(BOOL)isEnabled; - (void)setNavigationUIEnabled:(BOOL)isEnabled; - (void)setNavigationUIEnabledPreference:(int)preference; -- (void)setFollowingPerspective:(NSNumber *)index; +- (void)setFollowingPerspective:(NSNumber *)index zoomLevel:(NSNumber *)zoomLevel; - (void)setNightMode:(NSNumber *)index; - (void)setSpeedometerEnabled:(BOOL)isEnabled; - (void)setSpeedLimitIconEnabled:(BOOL)isEnabled; diff --git a/ios/react-native-navigation-sdk/NavViewController.mm b/ios/react-native-navigation-sdk/NavViewController.mm index 059e5d5..a662276 100644 --- a/ios/react-native-navigation-sdk/NavViewController.mm +++ b/ios/react-native-navigation-sdk/NavViewController.mm @@ -560,7 +560,9 @@ - (void)setMyLocationEnabled:(BOOL)isEnabled { _mapView.myLocationEnabled = isEnabled; } -- (void)setFollowingPerspective:(NSNumber *)index { +- (void)setFollowingPerspective:(NSNumber *)index zoomLevel:(NSNumber *)zoomLevel { + _mapView.cameraMode = GMSNavigationCameraModeFollowing; + if ([index isEqual:@1]) { [_mapView setFollowingPerspective:GMSNavigationCameraPerspectiveTopDownNorthUp]; } else if ([index isEqual:@2]) { @@ -568,7 +570,12 @@ - (void)setFollowingPerspective:(NSNumber *)index { } else { [_mapView setFollowingPerspective:GMSNavigationCameraPerspectiveTilted]; } - _mapView.cameraMode = GMSNavigationCameraModeFollowing; + + if (zoomLevel != nil) { + _mapView.followingZoomLevel = zoomLevel.floatValue; + } else { + _mapView.followingZoomLevel = GMSNavigationNoFollowingZoomLevel; + } } - (void)setSpeedometerEnabled:(BOOL)isEnabled { @@ -950,6 +957,14 @@ - (void)setMinZoomLevel:(float)minLevel maxZoom:(float)maxLevel { // Use default values if -1 is provided float effectiveMinLevel = (minLevel < 0.0f) ? kGMSMinZoomLevel : minLevel; float effectiveMaxLevel = (maxLevel < 0.0f) ? kGMSMaxZoomLevel : maxLevel; + + if (effectiveMinLevel > effectiveMaxLevel) { + NSLog(@"NavViewController: minZoomLevel (%f) is greater than maxZoomLevel (%f). Ignoring zoom " + @"level constraints.", + effectiveMinLevel, effectiveMaxLevel); + return; + } + [_mapView setMinZoom:effectiveMinLevel maxZoom:effectiveMaxLevel]; } diff --git a/ios/react-native-navigation-sdk/NavViewModule.mm b/ios/react-native-navigation-sdk/NavViewModule.mm index b80d4f5..b319387 100644 --- a/ios/react-native-navigation-sdk/NavViewModule.mm +++ b/ios/react-native-navigation-sdk/NavViewModule.mm @@ -476,13 +476,14 @@ - (void)setNavigationUIEnabled:(NSString *)nativeID } - (void)setFollowingPerspective:(NSString *)nativeID - perspective:(double)perspective + perspective:(NSInteger)perspective + zoomLevel:(NSNumber *)zoomLevel resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { NavViewController *viewController = [self getViewControllerForNativeID:nativeID]; if (viewController) { dispatch_async(dispatch_get_main_queue(), ^{ - [viewController setFollowingPerspective:@((NSInteger)perspective)]; + [viewController setFollowingPerspective:@(perspective) zoomLevel:zoomLevel]; resolve(nil); }); } else { diff --git a/src/auto/types.ts b/src/auto/types.ts index 3f8285e..b83dd98 100644 --- a/src/auto/types.ts +++ b/src/auto/types.ts @@ -15,7 +15,11 @@ */ import type { MapViewController, MapType, MapColorScheme } from '../maps'; -import type { CameraPerspective, NavigationNightMode } from '../navigation'; +import type { + CameraPerspective, + FollowMyLocationOptions, + NavigationNightMode, +} from '../navigation'; /** Defines all callbacks to be emitted by NavViewAuto support. */ export interface NavigationAutoCallbacks { @@ -63,8 +67,12 @@ export interface MapViewAutoController extends MapViewController { * camera following mode. This recenters the camera to follow the user's location. * * @param perspective The camera perspective to use when following. + * @param options Optional settings for camera follow behavior (e.g., fixed zoom level). */ - setFollowingPerspective(perspective: CameraPerspective): void; + setFollowingPerspective( + perspective: CameraPerspective, + options?: FollowMyLocationOptions + ): void; /** * Sends a custom message from React Native to the native auto module. diff --git a/src/auto/useNavigationAuto.ts b/src/auto/useNavigationAuto.ts index c743d32..2dd2759 100644 --- a/src/auto/useNavigationAuto.ts +++ b/src/auto/useNavigationAuto.ts @@ -41,7 +41,10 @@ import type { GroundOverlayPositionOptions, MapColorScheme, } from '../maps'; -import type { NavigationNightMode } from '../navigation'; +import type { + FollowMyLocationOptions, + NavigationNightMode, +} from '../navigation'; import { useMemo, useCallback, useRef } from 'react'; const { NavAutoModule } = NativeModules; @@ -163,8 +166,11 @@ export const useNavigationAuto = (): UseNavigationAutoResult => { return await NavAutoModule.isAutoScreenAvailable(); }, - setFollowingPerspective: (perspective: number) => { - NavAutoModule.setFollowingPerspective(perspective); + setFollowingPerspective: ( + perspective: number, + options?: FollowMyLocationOptions + ) => { + NavAutoModule.setFollowingPerspective(perspective, options?.zoomLevel); }, sendCustomMessage: ( diff --git a/src/maps/mapView/mapView.tsx b/src/maps/mapView/mapView.tsx index a8cae77..3a0d8ec 100644 --- a/src/maps/mapView/mapView.tsx +++ b/src/maps/mapView/mapView.tsx @@ -82,6 +82,19 @@ export const MapView = (props: MapViewProps): React.JSX.Element => { props.onMarkerInfoWindowTapped ); + const { minZoomLevel, maxZoomLevel } = props; + + const hasConflictingZoomLevels = + minZoomLevel != null && maxZoomLevel != null && minZoomLevel > maxZoomLevel; + + useEffect(() => { + if (hasConflictingZoomLevels) { + console.warn( + `minZoomLevel (${minZoomLevel}) must not be greater than maxZoomLevel (${maxZoomLevel}). Zoom level constraints will be ignored.` + ); + } + }, [hasConflictingZoomLevels, minZoomLevel, maxZoomLevel]); + return ( { zoomControlsEnabled={props.zoomControlsEnabled} zoomGesturesEnabled={props.zoomGesturesEnabled} buildingsEnabled={props.buildingsEnabled} - minZoomLevel={props.minZoomLevel} - maxZoomLevel={props.maxZoomLevel} + minZoomLevel={hasConflictingZoomLevels ? undefined : minZoomLevel} + maxZoomLevel={hasConflictingZoomLevels ? undefined : maxZoomLevel} onMapClick={onMapClick} onMapReady={onMapReady} onMarkerClick={onMarkerClick} diff --git a/src/native/NativeNavAutoModule.ts b/src/native/NativeNavAutoModule.ts index 9b33460..8f7a097 100644 --- a/src/native/NativeNavAutoModule.ts +++ b/src/native/NativeNavAutoModule.ts @@ -150,7 +150,10 @@ export interface Spec extends TurboModule { setMyLocationButtonEnabled(enabled: boolean): void; setMapColorScheme(colorScheme: Int32): void; setNightMode(nightMode: Int32): void; - setFollowingPerspective(perspective: Int32): void; + setFollowingPerspective( + perspective: Int32, + zoomLevel?: WithDefault + ): void; setBuildingsEnabled(enabled: boolean): void; setZoomLevel(zoomLevel: Double): Promise; setMapPadding(top: Double, left: Double, bottom: Double, right: Double): void; diff --git a/src/native/NativeNavViewModule.ts b/src/native/NativeNavViewModule.ts index 7828d96..ae26454 100644 --- a/src/native/NativeNavViewModule.ts +++ b/src/native/NativeNavViewModule.ts @@ -139,7 +139,11 @@ export interface Spec extends TurboModule { nativeID: string, options: GroundOverlayOptionsSpec ): Promise; - setFollowingPerspective(nativeID: string, perspective: Int32): Promise; + setFollowingPerspective( + nativeID: string, + perspective: Int32, + zoomLevel?: WithDefault + ): Promise; moveCamera( nativeID: string, cameraPosition: CameraPositionSpec diff --git a/src/navigation/navigationView/navigationView.tsx b/src/navigation/navigationView/navigationView.tsx index 22f4d15..e2b0c32 100644 --- a/src/navigation/navigationView/navigationView.tsx +++ b/src/navigation/navigationView/navigationView.tsx @@ -221,6 +221,19 @@ export const NavigationView = ( [onPromptVisibilityChangedProp] ); + const { minZoomLevel, maxZoomLevel } = props; + + const hasConflictingZoomLevels = + minZoomLevel != null && maxZoomLevel != null && minZoomLevel > maxZoomLevel; + + useEffect(() => { + if (hasConflictingZoomLevels) { + console.warn( + `minZoomLevel (${minZoomLevel}) must not be greater than maxZoomLevel (${maxZoomLevel}). Zoom level constraints will be ignored.` + ); + } + }, [hasConflictingZoomLevels, minZoomLevel, maxZoomLevel]); + return ( { + setFollowingPerspective: async ( + perspective: CameraPerspective, + options?: FollowMyLocationOptions + ) => { try { - await NavViewModule.setFollowingPerspective(nativeID, perspective); + await NavViewModule.setFollowingPerspective( + nativeID, + perspective, + options?.zoomLevel + ); } catch (error) { console.error('Error calling setFollowingPerspective:', error); } diff --git a/src/navigation/navigationView/types.ts b/src/navigation/navigationView/types.ts index 5ac5d4b..e560ca7 100644 --- a/src/navigation/navigationView/types.ts +++ b/src/navigation/navigationView/types.ts @@ -164,6 +164,18 @@ export enum NavigationNightMode { FORCE_NIGHT = 2, } +/** + * Options for controlling camera behavior when following the user's location. + */ +export interface FollowMyLocationOptions { + /** + * A fixed zoom level to use when the camera is following the user's location. + * When set, the Navigation SDK's auto-zoom is overridden with this value. + * Omit or set to undefined to use the default Nav SDK auto-zoom. + */ + zoomLevel?: number; +} + /** * Allows you to access Navigator methods. */ @@ -180,7 +192,12 @@ export interface NavigationViewController { setNavigationUIEnabled(enabled: boolean): Promise; /** - * Sets the camera perspective for navigation. + * Sets the camera perspective for navigation and enables camera following mode. + * @param perspective - The camera perspective to use. + * @param options - Optional settings for camera follow behavior (e.g., fixed zoom level). */ - setFollowingPerspective(perspective: CameraPerspective): Promise; + setFollowingPerspective( + perspective: CameraPerspective, + options?: FollowMyLocationOptions + ): Promise; }