diff --git a/README.md b/README.md
index 1a4b670..1aabec5 100644
--- a/README.md
+++ b/README.md
@@ -75,6 +75,8 @@ To set up, specify your API key in the application delegate `ios/Runner/AppDeleg
## Usage
+### Add a navigation view
+
You can now add a `NavigationView` component to your application..
The view can be controlled with the `ViewController` (Navigation and MapView) that are retrieved from the `onMapViewControllerCreated` and `onNavigationViewControllerCreated` (respectively).
@@ -82,8 +84,6 @@ The view can be controlled with the `ViewController` (Navigation and MapView) th
The `NavigationView` compoonent should be used within a View with a bounded size. Using it
in an unbounded widget will cause the application to behave unexpectedly.
-### Add a navigation view
-
```tsx
// Permissions must have been granted by this point.
@@ -105,6 +105,17 @@ in an unbounded widget will cause the application to behave unexpectedly.
/>
```
+### Add a map view
+
+You can also add a bare `MapView` that works as a normal map view without navigation functionality. `MapView` only need a `MapViewController` to be controlled.
+
+```tsx
+
+```
+
See the [example](./example) directory for a complete navigation sample app.
### Requesting and handling permissions
diff --git a/android/src/main/java/com/google/android/react/navsdk/CustomTypes.java b/android/src/main/java/com/google/android/react/navsdk/CustomTypes.java
new file mode 100644
index 0000000..909b807
--- /dev/null
+++ b/android/src/main/java/com/google/android/react/navsdk/CustomTypes.java
@@ -0,0 +1,21 @@
+/**
+ * Copyright 2023 Google LLC
+ *
+ *
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *
Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.react.navsdk;
+
+public class CustomTypes {
+ public enum FragmentType {
+ MAP,
+ NAVIGATION
+ }
+}
diff --git a/android/src/main/java/com/google/android/react/navsdk/EnumTranslationUtil.java b/android/src/main/java/com/google/android/react/navsdk/EnumTranslationUtil.java
index 1ae3867..330ea54 100644
--- a/android/src/main/java/com/google/android/react/navsdk/EnumTranslationUtil.java
+++ b/android/src/main/java/com/google/android/react/navsdk/EnumTranslationUtil.java
@@ -80,4 +80,14 @@ public static int getMapTypeFromJsValue(int jsValue) {
return CameraPerspective.TILTED;
}
}
+
+ public static CustomTypes.FragmentType getFragmentTypeFromJsValue(int jsValue) {
+ switch (jsValue) {
+ case 0:
+ default:
+ return CustomTypes.FragmentType.MAP;
+ case 1:
+ return CustomTypes.FragmentType.NAVIGATION;
+ }
+ }
}
diff --git a/android/src/main/java/com/google/android/react/navsdk/IMapViewFragment.java b/android/src/main/java/com/google/android/react/navsdk/IMapViewFragment.java
new file mode 100644
index 0000000..e835afb
--- /dev/null
+++ b/android/src/main/java/com/google/android/react/navsdk/IMapViewFragment.java
@@ -0,0 +1,103 @@
+/**
+ * Copyright 2023 Google LLC
+ *
+ *
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *
Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.react.navsdk;
+
+import android.view.View;
+import com.google.android.gms.maps.GoogleMap;
+import com.google.android.gms.maps.model.Circle;
+import com.google.android.gms.maps.model.GroundOverlay;
+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 java.io.IOException;
+import java.util.Map;
+
+public interface IMapViewFragment {
+ void setStylingOptions(Map stylingOptions);
+
+ void applyStylingOptions();
+
+ void setFollowingPerspective(int jsValue);
+
+ void setNightModeOption(int jsValue);
+
+ void setMapType(int jsValue);
+
+ void clearMapView();
+
+ void resetMinMaxZoomLevel();
+
+ void animateCamera(Map map);
+
+ Circle addCircle(Map optionsMap);
+
+ Marker addMarker(Map optionsMap);
+
+ Polyline addPolyline(Map optionsMap);
+
+ Polygon addPolygon(Map optionsMap);
+
+ void removeMarker(String id);
+
+ void removePolyline(String id);
+
+ void removePolygon(String id);
+
+ void removeCircle(String id);
+
+ void removeGroundOverlay(String id);
+
+ GroundOverlay addGroundOverlay(Map map);
+
+ void setMapStyle(String url);
+
+ String fetchJsonFromUrl(String urlString) throws IOException;
+
+ void moveCamera(Map map);
+
+ void setZoomLevel(int level);
+
+ void setIndoorEnabled(boolean isOn);
+
+ void setTrafficEnabled(boolean isOn);
+
+ void setCompassEnabled(boolean isOn);
+
+ void setRotateGesturesEnabled(boolean isOn);
+
+ void setScrollGesturesEnabled(boolean isOn);
+
+ void setScrollGesturesEnabledDuringRotateOrZoom(boolean isOn);
+
+ void setTiltGesturesEnabled(boolean isOn);
+
+ void setZoomControlsEnabled(boolean isOn);
+
+ void setZoomGesturesEnabled(boolean isOn);
+
+ void setBuildingsEnabled(boolean isOn);
+
+ void setMyLocationEnabled(boolean isOn);
+
+ void setMapToolbarEnabled(boolean isOn);
+
+ void setMyLocationButtonEnabled(boolean isOn);
+
+ GoogleMap getGoogleMap();
+
+ // Fragment
+ boolean isAdded();
+
+ View getView();
+}
diff --git a/android/src/main/java/com/google/android/react/navsdk/INavViewFragment.java b/android/src/main/java/com/google/android/react/navsdk/INavViewFragment.java
new file mode 100644
index 0000000..1abbac8
--- /dev/null
+++ b/android/src/main/java/com/google/android/react/navsdk/INavViewFragment.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright 2023 Google LLC
+ *
+ *
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *
Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.react.navsdk;
+
+public interface INavViewFragment extends IMapViewFragment {
+ void setNavigationUiEnabled(boolean enableNavigationUi);
+
+ void setTripProgressBarEnabled(boolean enabled);
+
+ void setSpeedometerEnabled(boolean enabled);
+
+ void setSpeedLimitIconEnabled(boolean enabled);
+
+ void setTrafficIncidentCardsEnabled(boolean enabled);
+
+ void setEtaCardEnabled(boolean enabled);
+
+ void setHeaderEnabled(boolean enabled);
+
+ void setRecenterButtonEnabled(boolean enabled);
+
+ void showRouteOverview();
+}
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
new file mode 100644
index 0000000..e3e3125
--- /dev/null
+++ b/android/src/main/java/com/google/android/react/navsdk/MapViewFragment.java
@@ -0,0 +1,704 @@
+/**
+ * Copyright 2023 Google LLC
+ *
+ *
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
+ * except in compliance with the License. You may obtain a copy of the License at
+ *
+ *
http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *
Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.react.navsdk;
+
+import android.Manifest.permission;
+import android.annotation.SuppressLint;
+import android.content.pm.PackageManager;
+import android.graphics.Color;
+import android.os.Bundle;
+import android.view.View;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.app.ActivityCompat;
+import com.facebook.react.bridge.Arguments;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.UiThreadUtil;
+import com.facebook.react.bridge.WritableMap;
+import com.facebook.react.uimanager.UIManagerHelper;
+import com.facebook.react.uimanager.events.Event;
+import com.facebook.react.uimanager.events.EventDispatcher;
+import com.google.android.gms.maps.CameraUpdateFactory;
+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.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.GroundOverlay;
+import com.google.android.gms.maps.model.GroundOverlayOptions;
+import com.google.android.gms.maps.model.LatLng;
+import com.google.android.gms.maps.model.MapStyleOptions;
+import com.google.android.gms.maps.model.Marker;
+import com.google.android.gms.maps.model.MarkerOptions;
+import com.google.android.gms.maps.model.Polygon;
+import com.google.android.gms.maps.model.PolygonOptions;
+import com.google.android.gms.maps.model.Polyline;
+import com.google.android.gms.maps.model.PolylineOptions;
+import com.google.android.libraries.navigation.StylingOptions;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executors;
+
+/**
+ * A fragment that displays a view with a Google Map using MapFragment. This fragment's lifecycle is
+ * managed by NavViewManager.
+ */
+@SuppressLint("ValidFragment")
+public class MapViewFragment extends SupportMapFragment implements IMapViewFragment {
+ private static final String TAG = "MapViewFragment";
+ private GoogleMap mGoogleMap;
+ private StylingOptions mStylingOptions;
+
+ private List markerList = new ArrayList<>();
+ private List polylineList = new ArrayList<>();
+ private List polygonList = new ArrayList<>();
+ private List groundOverlayList = new ArrayList<>();
+ private List circleList = new ArrayList<>();
+ private int viewTag; // React native view tag.
+ private ReactApplicationContext reactContext;
+
+ public MapViewFragment(ReactApplicationContext reactContext, int viewTag) {
+ this.reactContext = reactContext;
+ this.viewTag = viewTag;
+ }
+ ;
+
+ private String style = "";
+
+ @SuppressLint("MissingPermission")
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+
+ getMapAsync(
+ new OnMapReadyCallback() {
+ public void onMapReady(GoogleMap googleMap) {
+ mGoogleMap = googleMap;
+
+ emitEvent("onMapReady", null);
+
+ mGoogleMap.setOnMarkerClickListener(
+ new GoogleMap.OnMarkerClickListener() {
+ @Override
+ public boolean onMarkerClick(Marker marker) {
+ emitEvent("onMarkerClick", ObjectTranslationUtil.getMapFromMarker(marker));
+ return false;
+ }
+ });
+ mGoogleMap.setOnPolylineClickListener(
+ new GoogleMap.OnPolylineClickListener() {
+ @Override
+ public void onPolylineClick(Polyline polyline) {
+ emitEvent(
+ "onPolylineClick", ObjectTranslationUtil.getMapFromPolyline(polyline));
+ }
+ });
+ mGoogleMap.setOnPolygonClickListener(
+ new GoogleMap.OnPolygonClickListener() {
+ @Override
+ public void onPolygonClick(Polygon polygon) {
+ emitEvent("onPolygonClick", ObjectTranslationUtil.getMapFromPolygon(polygon));
+ }
+ });
+ mGoogleMap.setOnCircleClickListener(
+ new GoogleMap.OnCircleClickListener() {
+ @Override
+ public void onCircleClick(Circle circle) {
+ emitEvent("onCircleClick", ObjectTranslationUtil.getMapFromCircle(circle));
+ }
+ });
+ mGoogleMap.setOnGroundOverlayClickListener(
+ new GoogleMap.OnGroundOverlayClickListener() {
+ @Override
+ public void onGroundOverlayClick(GroundOverlay groundOverlay) {
+ emitEvent(
+ "onGroundOverlayClick",
+ ObjectTranslationUtil.getMapFromGroundOverlay(groundOverlay));
+ }
+ });
+
+ mGoogleMap.setOnInfoWindowClickListener(
+ new GoogleMap.OnInfoWindowClickListener() {
+ @Override
+ public void onInfoWindowClick(Marker marker) {
+ emitEvent(
+ "onMarkerInfoWindowTapped", ObjectTranslationUtil.getMapFromMarker(marker));
+ }
+ });
+
+ mGoogleMap.setOnMapClickListener(
+ new GoogleMap.OnMapClickListener() {
+ @Override
+ public void onMapClick(LatLng latLng) {
+ emitEvent("onMapClick", ObjectTranslationUtil.getMapFromLatLng(latLng));
+ }
+ });
+ }
+ });
+ }
+
+ public void applyStylingOptions() {}
+
+ public void setStylingOptions(Map stylingOptions) {}
+
+ @SuppressLint("MissingPermission")
+ public void setFollowingPerspective(int jsValue) {
+ if (mGoogleMap == null) {
+ return;
+ }
+
+ mGoogleMap.followMyLocation(EnumTranslationUtil.getCameraPerspectiveFromJsValue(jsValue));
+ }
+
+ public void setNightModeOption(int jsValue) {}
+
+ public void setMapType(int jsValue) {
+ if (mGoogleMap == null) {
+ return;
+ }
+
+ mGoogleMap.setMapType(EnumTranslationUtil.getMapTypeFromJsValue(jsValue));
+ }
+
+ public void clearMapView() {
+ if (mGoogleMap == null) {
+ return;
+ }
+
+ mGoogleMap.clear();
+ }
+
+ public void resetMinMaxZoomLevel() {
+ if (mGoogleMap == null) {
+ return;
+ }
+
+ mGoogleMap.resetMinMaxZoomPreference();
+ }
+
+ public void animateCamera(Map map) {
+ if (mGoogleMap != null) {
+ int zoom = CollectionUtil.getInt("zoom", map, 0);
+ int tilt = CollectionUtil.getInt("tilt", map, 0);
+ int bearing = CollectionUtil.getInt("bearing", map, 0);
+ int animationDuration = CollectionUtil.getInt("duration", map, 0);
+
+ CameraPosition cameraPosition =
+ new CameraPosition.Builder()
+ .target(
+ ObjectTranslationUtil.getLatLngFromMap(
+ (Map) map.get("target"))) // Set the target location
+ .zoom(zoom) // Set the desired zoom level
+ .tilt(tilt) // Set the desired tilt angle (0 for straight down, 90 for straight up)
+ .bearing(bearing) // Set the desired bearing (rotation angle in degrees)
+ .build();
+
+ mGoogleMap.animateCamera(
+ CameraUpdateFactory.newCameraPosition(cameraPosition), animationDuration, null);
+ }
+ }
+
+ public Circle addCircle(Map optionsMap) {
+ if (mGoogleMap == null) {
+ return null;
+ }
+
+ CircleOptions options = new CircleOptions();
+
+ float strokeWidth =
+ Double.valueOf(CollectionUtil.getDouble("strokeWidth", optionsMap, 0)).floatValue();
+ options.strokeWidth(strokeWidth);
+
+ double radius = CollectionUtil.getDouble("radius", optionsMap, 0.0);
+ options.radius(radius);
+
+ boolean visible = CollectionUtil.getBool("visible", optionsMap, true);
+ options.visible(visible);
+
+ options.center(ObjectTranslationUtil.getLatLngFromMap((Map) optionsMap.get("center")));
+
+ boolean clickable = CollectionUtil.getBool("clickable", optionsMap, false);
+ options.clickable(clickable);
+
+ String strokeColor = CollectionUtil.getString("strokeColor", optionsMap);
+ if (strokeColor != null) {
+ options.strokeColor(Color.parseColor(strokeColor));
+ }
+
+ String fillColor = CollectionUtil.getString("fillColor", optionsMap);
+ if (fillColor != null) {
+ options.fillColor(Color.parseColor(fillColor));
+ }
+
+ Circle circle = mGoogleMap.addCircle(options);
+ circleList.add(circle);
+
+ return circle;
+ }
+
+ public Marker addMarker(Map optionsMap) {
+ if (mGoogleMap == null) {
+ return null;
+ }
+
+ String imagePath = CollectionUtil.getString("imgPath", optionsMap);
+ String title = CollectionUtil.getString("title", optionsMap);
+ String snippet = CollectionUtil.getString("snippet", optionsMap);
+ float alpha = Double.valueOf(CollectionUtil.getDouble("alpha", optionsMap, 1)).floatValue();
+ float rotation =
+ Double.valueOf(CollectionUtil.getDouble("rotation", optionsMap, 0)).floatValue();
+ boolean draggable = CollectionUtil.getBool("draggable", optionsMap, false);
+ boolean flat = CollectionUtil.getBool("flat", optionsMap, false);
+ boolean visible = CollectionUtil.getBool("visible", optionsMap, true);
+
+ MarkerOptions options = new MarkerOptions();
+ if (imagePath != null && !imagePath.isEmpty()) {
+ BitmapDescriptor icon = BitmapDescriptorFactory.fromPath(imagePath);
+ options.icon(icon);
+ }
+
+ options.position(ObjectTranslationUtil.getLatLngFromMap((Map) optionsMap.get("position")));
+
+ if (title != null) {
+ options.title(title);
+ }
+
+ if (snippet != null) {
+ options.snippet(snippet);
+ }
+
+ options.flat(flat);
+ options.alpha(alpha);
+ options.rotation(rotation);
+ options.draggable(draggable);
+ options.visible(visible);
+
+ Marker marker = mGoogleMap.addMarker(options);
+
+ markerList.add(marker);
+
+ return marker;
+ }
+
+ public Polyline addPolyline(Map optionsMap) {
+ if (mGoogleMap == null) {
+ return null;
+ }
+
+ float width = Double.valueOf(CollectionUtil.getDouble("width", optionsMap, 0)).floatValue();
+ boolean clickable = CollectionUtil.getBool("clickable", optionsMap, false);
+ boolean visible = CollectionUtil.getBool("visible", optionsMap, true);
+
+ ArrayList latLngArr = (ArrayList) optionsMap.get("points");
+
+ PolylineOptions options = new PolylineOptions();
+ for (int i = 0; i < latLngArr.size(); i++) {
+ Map latLngMap = (Map) latLngArr.get(i);
+ LatLng latLng = createLatLng(latLngMap);
+ options.add(latLng);
+ }
+
+ String color = CollectionUtil.getString("color", optionsMap);
+ if (color != null) {
+ options.color(Color.parseColor(color));
+ }
+
+ options.width(width);
+ options.clickable(clickable);
+ options.visible(visible);
+
+ Polyline polyline = mGoogleMap.addPolyline(options);
+ polylineList.add(polyline);
+
+ return polyline;
+ }
+
+ public Polygon addPolygon(Map optionsMap) {
+ if (mGoogleMap == null) {
+ return null;
+ }
+
+ String strokeColor = CollectionUtil.getString("strokeColor", optionsMap);
+ String fillColor = CollectionUtil.getString("fillColor", optionsMap);
+ float strokeWidth =
+ Double.valueOf(CollectionUtil.getDouble("strokeWidth", optionsMap, 0)).floatValue();
+ boolean clickable = CollectionUtil.getBool("clickable", optionsMap, false);
+ boolean geodesic = CollectionUtil.getBool("geodesic", optionsMap, false);
+ boolean visible = CollectionUtil.getBool("visible", optionsMap, true);
+
+ ArrayList latLngArr = (ArrayList) optionsMap.get("points");
+
+ PolygonOptions options = new PolygonOptions();
+ for (int i = 0; i < latLngArr.size(); i++) {
+ Map latLngMap = (Map) latLngArr.get(i);
+ LatLng latLng = createLatLng(latLngMap);
+ options.add(latLng);
+ }
+
+ ArrayList holesArr = (ArrayList) optionsMap.get("holes");
+
+ for (int i = 0; i < holesArr.size(); i++) {
+ ArrayList arr = (ArrayList) holesArr.get(i);
+
+ List listHoles = new ArrayList<>();
+
+ for (int j = 0; j < arr.size(); j++) {
+ Map latLngMap = (Map) arr.get(j);
+ LatLng latLng = createLatLng(latLngMap);
+
+ listHoles.add(latLng);
+ }
+
+ options.addHole(listHoles);
+ }
+
+ if (fillColor != null) {
+ options.fillColor(Color.parseColor(fillColor));
+ }
+
+ if (strokeColor != null) {
+ options.strokeColor(Color.parseColor(strokeColor));
+ }
+
+ options.strokeWidth(strokeWidth);
+ options.visible(visible);
+ options.geodesic(geodesic);
+ options.clickable(clickable);
+
+ Polygon polygon = mGoogleMap.addPolygon(options);
+ polygonList.add(polygon);
+
+ return polygon;
+ }
+
+ public void removeMarker(String id) {
+ UiThreadUtil.runOnUiThread(
+ () -> {
+ for (Marker m : markerList) {
+ if (m.getId().equals(id)) {
+ m.remove();
+ markerList.remove(m);
+ return;
+ }
+ }
+ });
+ }
+
+ public void removePolyline(String id) {
+ for (Polyline p : polylineList) {
+ if (p.getId().equals(id)) {
+ p.remove();
+ polylineList.remove(p);
+ return;
+ }
+ }
+ }
+
+ public void removePolygon(String id) {
+ for (Polygon p : polygonList) {
+ if (p.getId().equals(id)) {
+ p.remove();
+ polygonList.remove(p);
+ return;
+ }
+ }
+ }
+
+ public void removeCircle(String id) {
+ for (Circle c : circleList) {
+ if (c.getId().equals(id)) {
+ c.remove();
+ circleList.remove(c);
+ return;
+ }
+ }
+ }
+
+ public void removeGroundOverlay(String id) {
+ for (GroundOverlay g : groundOverlayList) {
+ if (g.getId().equals(id)) {
+ g.remove();
+ groundOverlayList.remove(g);
+ return;
+ }
+ }
+ }
+
+ private LatLng createLatLng(Map map) {
+ Double lat = null;
+ Double lng = null;
+ if (map.containsKey("lat") && map.containsKey("lng")) {
+ if (map.get("lat") != null) lat = Double.parseDouble(map.get("lat").toString());
+ if (map.get("lng") != null) lng = Double.parseDouble(map.get("lng").toString());
+ }
+
+ return new LatLng(lat, lng);
+ }
+
+ public GroundOverlay addGroundOverlay(Map map) {
+ if (mGoogleMap == null) {
+ return null;
+ }
+
+ String imagePath = CollectionUtil.getString("imgPath", map);
+ float width = Double.valueOf(CollectionUtil.getDouble("width", map, 0)).floatValue();
+ float height = Double.valueOf(CollectionUtil.getDouble("height", map, 0)).floatValue();
+ float transparency =
+ Double.valueOf(CollectionUtil.getDouble("transparency", map, 0)).floatValue();
+ boolean clickable = CollectionUtil.getBool("clickable", map, false);
+ boolean visible = CollectionUtil.getBool("visible", map, true);
+
+ Double lat = null;
+ Double lng = null;
+ if (map.containsKey("location")) {
+ Map latlng = (Map) map.get("location");
+ if (latlng.get("lat") != null) lat = Double.parseDouble(latlng.get("lat").toString());
+ if (latlng.get("lng") != null) lng = Double.parseDouble(latlng.get("lng").toString());
+ }
+
+ GroundOverlayOptions options = new GroundOverlayOptions();
+ if (imagePath != null && !imagePath.isEmpty()) {
+ BitmapDescriptor bitmapDescriptor = BitmapDescriptorFactory.fromPath(imagePath);
+ options.image(bitmapDescriptor);
+ }
+ options.position(new LatLng(lat, lng), width, height);
+ options.transparency(transparency);
+ options.clickable(clickable);
+ options.visible(visible);
+ GroundOverlay groundOverlay = mGoogleMap.addGroundOverlay(options);
+ groundOverlayList.add(groundOverlay);
+ return groundOverlay;
+ }
+
+ public void setMapStyle(String url) {
+ Executors.newSingleThreadExecutor()
+ .execute(
+ () -> {
+ try {
+ style = fetchJsonFromUrl(url);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ getActivity()
+ .runOnUiThread(
+ (Runnable)
+ () -> {
+ MapStyleOptions options = new MapStyleOptions(style);
+ mGoogleMap.setMapStyle(options);
+ });
+ });
+ }
+
+ public String fetchJsonFromUrl(String urlString) throws IOException {
+ URL url = new URL(urlString);
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+ connection.setRequestMethod("GET");
+
+ int responseCode = connection.getResponseCode();
+ if (responseCode == HttpURLConnection.HTTP_OK) {
+ InputStream inputStream = connection.getInputStream();
+ BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
+ StringBuilder stringBuilder = new StringBuilder();
+ String line;
+ while ((line = reader.readLine()) != null) {
+ stringBuilder.append(line);
+ }
+ reader.close();
+ inputStream.close();
+ return stringBuilder.toString();
+ } else {
+ // Handle error response
+ throw new IOException("Error response: " + responseCode);
+ }
+ }
+
+ /** Moves the position of the camera to hover over Melbourne. */
+ public void moveCamera(Map map) {
+ LatLng latLng = ObjectTranslationUtil.getLatLngFromMap((Map) map.get("target"));
+
+ float zoom = (float) CollectionUtil.getDouble("zoom", map, 0);
+ float tilt = (float) CollectionUtil.getDouble("tilt", map, 0);
+ float bearing = (float) CollectionUtil.getDouble("bearing", map, 0);
+
+ CameraPosition cameraPosition =
+ CameraPosition.builder().target(latLng).zoom(zoom).tilt(tilt).bearing(bearing).build();
+
+ mGoogleMap.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition));
+ }
+
+ public void setZoomLevel(int level) {
+ if (mGoogleMap != null) {
+ mGoogleMap.animateCamera(CameraUpdateFactory.zoomTo(level));
+ }
+ }
+
+ public void setIndoorEnabled(boolean isOn) {
+ if (mGoogleMap != null) {
+ mGoogleMap.setIndoorEnabled(isOn);
+ }
+ }
+
+ public void setTrafficEnabled(boolean isOn) {
+ if (mGoogleMap != null) {
+ mGoogleMap.setTrafficEnabled(isOn);
+ }
+ }
+
+ public void setCompassEnabled(boolean isOn) {
+ if (mGoogleMap != null) {
+ mGoogleMap.getUiSettings().setCompassEnabled(isOn);
+ }
+ }
+
+ public void setRotateGesturesEnabled(boolean isOn) {
+ if (mGoogleMap != null) {
+ mGoogleMap.getUiSettings().setRotateGesturesEnabled(isOn);
+ }
+ }
+
+ public void setScrollGesturesEnabled(boolean isOn) {
+ if (mGoogleMap != null) {
+ mGoogleMap.getUiSettings().setScrollGesturesEnabled(isOn);
+ }
+ }
+
+ public void setScrollGesturesEnabledDuringRotateOrZoom(boolean isOn) {
+ if (mGoogleMap != null) {
+ mGoogleMap.getUiSettings().setScrollGesturesEnabledDuringRotateOrZoom(isOn);
+ }
+ }
+
+ public void setTiltGesturesEnabled(boolean isOn) {
+ if (mGoogleMap != null) {
+ mGoogleMap.getUiSettings().setTiltGesturesEnabled(isOn);
+ }
+ }
+
+ public void setZoomControlsEnabled(boolean isOn) {
+ if (mGoogleMap != null) {
+ mGoogleMap.getUiSettings().setZoomControlsEnabled(isOn);
+ }
+ }
+
+ public void setZoomGesturesEnabled(boolean isOn) {
+ if (mGoogleMap != null) {
+ mGoogleMap.getUiSettings().setZoomGesturesEnabled(isOn);
+ }
+ }
+
+ public void setBuildingsEnabled(boolean isOn) {
+ if (mGoogleMap != null) {
+ mGoogleMap.setBuildingsEnabled(isOn);
+ }
+ }
+
+ public void setMyLocationEnabled(boolean isOn) {
+ if (mGoogleMap != null) {
+ if (ActivityCompat.checkSelfPermission(getActivity(), permission.ACCESS_FINE_LOCATION)
+ == PackageManager.PERMISSION_GRANTED
+ && ActivityCompat.checkSelfPermission(getActivity(), permission.ACCESS_COARSE_LOCATION)
+ == PackageManager.PERMISSION_GRANTED) {
+ mGoogleMap.setMyLocationEnabled(isOn);
+ }
+ }
+ }
+
+ public void setMapToolbarEnabled(boolean isOn) {
+ if (mGoogleMap != null) {
+ mGoogleMap.getUiSettings().setMapToolbarEnabled(isOn);
+ }
+ }
+
+ /** Toggles whether the location marker is enabled. */
+ public void setMyLocationButtonEnabled(boolean isOn) {
+ if (mGoogleMap == null) {
+ return;
+ }
+
+ UiThreadUtil.runOnUiThread(
+ () -> {
+ mGoogleMap.getUiSettings().setMyLocationButtonEnabled(isOn);
+ });
+ }
+
+ private void emitEvent(String eventName, @Nullable WritableMap data) {
+ if (reactContext != null) {
+ EventDispatcher dispatcher =
+ UIManagerHelper.getEventDispatcherForReactTag(reactContext, viewTag);
+
+ if (dispatcher != null) {
+ int surfaceId = UIManagerHelper.getSurfaceId(reactContext);
+ dispatcher.dispatchEvent(new NavViewEvent(surfaceId, viewTag, eventName, data));
+ }
+ }
+ }
+
+ public GoogleMap getGoogleMap() {
+ return mGoogleMap;
+ }
+
+ // Navigation related function of the IViewFragment interface. Not used in this class.
+ public void setNavigationUiEnabled(boolean enableNavigationUi) {}
+
+ public void setTripProgressBarEnabled(boolean enabled) {}
+
+ public void setSpeedometerEnabled(boolean enabled) {}
+
+ public void setSpeedLimitIconEnabled(boolean enabled) {}
+
+ public void setTrafficIncidentCardsEnabled(boolean enabled) {}
+
+ public void setEtaCardEnabled(boolean enabled) {}
+
+ public void setHeaderEnabled(boolean enabled) {}
+
+ public void setRecenterButtonEnabled(boolean enabled) {}
+
+ public void showRouteOverview() {}
+
+ public class NavViewEvent extends Event {
+ private String eventName;
+ private @Nullable WritableMap eventData;
+
+ public NavViewEvent(
+ int surfaceId, int viewTag, String eventName, @Nullable WritableMap eventData) {
+ super(surfaceId, viewTag);
+ this.eventName = eventName;
+ this.eventData = eventData;
+ }
+
+ @Override
+ public String getEventName() {
+ return eventName;
+ }
+
+ @Override
+ public WritableMap getEventData() {
+ if (eventData == null) {
+ return Arguments.createMap();
+ }
+ return eventData;
+ }
+ }
+}
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 a6aab44..c10f9f6 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
@@ -65,7 +65,7 @@
* A fragment that displays a navigation view with a Google Map using SupportNavigationFragment.
* This fragment's lifecycle is managed by NavViewManager.
*/
-public class NavViewFragment extends SupportNavigationFragment {
+public class NavViewFragment extends SupportNavigationFragment implements INavViewFragment {
private static final String TAG = "NavViewFragment";
private GoogleMap mGoogleMap;
private StylingOptions mStylingOptions;
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 738111d..25d7864 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
@@ -14,6 +14,7 @@
package com.google.android.react.navsdk;
import static com.google.android.react.navsdk.Command.*;
+import static com.google.android.react.navsdk.EnumTranslationUtil.getFragmentTypeFromJsValue;
import android.view.Choreographer;
import android.view.View;
@@ -21,6 +22,7 @@
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReadableArray;
@@ -33,13 +35,16 @@
import java.util.Map;
import java.util.Objects;
+// NavViewManager is responsible for managing both the regular map fragment as well as the
+// navigation map view fragment.
+//
public class NavViewManager extends SimpleViewManager {
public static final String REACT_CLASS = "NavViewManager";
private static NavViewManager instance;
- private final HashMap> fragmentMap = new HashMap<>();
+ private final HashMap> fragmentMap = new HashMap<>();
private ReactApplicationContext reactContext;
@@ -117,20 +122,31 @@ public Map getCommandsMap() {
return map;
}
- public NavViewFragment getFragmentForRoot(ViewGroup root) {
+ public INavViewFragment getNavFragmentForRoot(ViewGroup root) {
+ IMapViewFragment fragment = getFragmentForRoot(root);
+
+ // Check if the fragment is an INavigationViewFragment
+ if (fragment instanceof INavViewFragment) {
+ return (INavViewFragment) fragment;
+ } else {
+ throw new IllegalStateException("The fragment is not a nav view fragment");
+ }
+ }
+
+ public IMapViewFragment getFragmentForRoot(ViewGroup root) {
int viewId = root.getId();
return getFragmentForViewId(viewId);
}
- public NavViewFragment getFragmentForViewId(int viewId) {
- WeakReference weakReference = fragmentMap.get(viewId);
+ public IMapViewFragment getFragmentForViewId(int viewId) {
+ WeakReference weakReference = fragmentMap.get(viewId);
if (weakReference == null || weakReference.get() == null) {
throw new IllegalStateException("Fragment not found for the provided viewId.");
}
return weakReference.get();
}
- public NavViewFragment getAnyFragment() {
+ public IMapViewFragment getAnyFragment() {
if (fragmentMap.isEmpty()) {
return null;
}
@@ -139,7 +155,7 @@ public NavViewFragment getAnyFragment() {
}
public void applyStylingOptions() {
- for (WeakReference weakReference : fragmentMap.values()) {
+ for (WeakReference weakReference : fragmentMap.values()) {
if (weakReference.get() != null) {
weakReference.get().applyStylingOptions();
}
@@ -155,16 +171,18 @@ public void receiveCommand(
switch (Command.find(commandIdInt)) {
case CREATE_FRAGMENT:
Map stylingOptions = args.getMap(0).toHashMap();
- createFragment(root, stylingOptions);
+ CustomTypes.FragmentType fragmentType = getFragmentTypeFromJsValue(args.getInt(1));
+ createFragment(root, stylingOptions, fragmentType);
break;
case DELETE_FRAGMENT:
try {
int viewId = root.getId();
FragmentActivity activity = (FragmentActivity) reactContext.getCurrentActivity();
+ IMapViewFragment fragment = Objects.requireNonNull(fragmentMap.remove(viewId)).get();
activity
.getSupportFragmentManager()
.beginTransaction()
- .remove(Objects.requireNonNull(fragmentMap.remove(viewId)).get())
+ .remove((Fragment) fragment)
.commitNowAllowingStateLoss();
} catch (Exception ignored) {
}
@@ -173,22 +191,22 @@ public void receiveCommand(
getFragmentForRoot(root).moveCamera(args.getMap(0).toHashMap());
break;
case SET_TRIP_PROGRESS_BAR_ENABLED:
- getFragmentForRoot(root).setTripProgressBarEnabled(args.getBoolean(0));
+ getNavFragmentForRoot(root).setTripProgressBarEnabled(args.getBoolean(0));
break;
case SET_NAVIGATION_UI_ENABLED:
- getFragmentForRoot(root).setNavigationUiEnabled(args.getBoolean(0));
+ getNavFragmentForRoot(root).setNavigationUiEnabled(args.getBoolean(0));
break;
case SET_FOLLOWING_PERSPECTIVE:
- getFragmentForRoot(root).setFollowingPerspective(args.getInt(0));
+ getNavFragmentForRoot(root).setFollowingPerspective(args.getInt(0));
break;
case SET_NIGHT_MODE:
getFragmentForRoot(root).setNightModeOption(args.getInt(0));
break;
case SET_SPEEDOMETER_ENABLED:
- getFragmentForRoot(root).setSpeedometerEnabled(args.getBoolean(0));
+ getNavFragmentForRoot(root).setSpeedometerEnabled(args.getBoolean(0));
break;
case SET_SPEED_LIMIT_ICON_ENABLED:
- getFragmentForRoot(root).setSpeedLimitIconEnabled(args.getBoolean(0));
+ getNavFragmentForRoot(root).setSpeedLimitIconEnabled(args.getBoolean(0));
break;
case SET_ZOOM_LEVEL:
int level = args.getInt(0);
@@ -249,19 +267,19 @@ public void receiveCommand(
getFragmentForRoot(root).animateCamera(args.getMap(0).toHashMap());
break;
case SET_TRAFFIC_INCIDENT_CARDS_ENABLED:
- getFragmentForRoot(root).setTrafficIncidentCardsEnabled(args.getBoolean(0));
+ getNavFragmentForRoot(root).setTrafficIncidentCardsEnabled(args.getBoolean(0));
break;
case SET_FOOTER_ENABLED:
- getFragmentForRoot(root).setEtaCardEnabled(args.getBoolean(0));
+ getNavFragmentForRoot(root).setEtaCardEnabled(args.getBoolean(0));
break;
case SET_HEADER_ENABLED:
- getFragmentForRoot(root).setHeaderEnabled(args.getBoolean(0));
+ getNavFragmentForRoot(root).setHeaderEnabled(args.getBoolean(0));
break;
case SET_RECENTER_BUTTON_ENABLED:
- getFragmentForRoot(root).setRecenterButtonEnabled(args.getBoolean(0));
+ getNavFragmentForRoot(root).setRecenterButtonEnabled(args.getBoolean(0));
break;
case SHOW_ROUTE_OVERVIEW:
- getFragmentForRoot(root).showRouteOverview();
+ getNavFragmentForRoot(root).showRouteOverview();
break;
case REMOVE_MARKER:
getFragmentForRoot(root).removeMarker(args.getString(0));
@@ -310,19 +328,32 @@ public Map getExportedCustomDirectEventTypeConstants() {
}
/** Replace your React Native view with a custom fragment */
- public void createFragment(FrameLayout root, Map stylingOptions) {
+ public void createFragment(
+ FrameLayout root, Map stylingOptions, CustomTypes.FragmentType fragmentType) {
setupLayout(root);
FragmentActivity activity = (FragmentActivity) reactContext.getCurrentActivity();
if (activity != null) {
int viewId = root.getId();
- NavViewFragment fragment = new NavViewFragment(reactContext, root.getId());
- fragmentMap.put(viewId, new WeakReference(fragment));
+ Fragment fragment;
+ // FragmentType 0 = MAP, 1 = NAVIGATION.
+ if (fragmentType == CustomTypes.FragmentType.MAP) {
+ MapViewFragment mapFragment = new MapViewFragment(reactContext, root.getId());
+ fragmentMap.put(viewId, new WeakReference(mapFragment));
+ fragment = mapFragment;
- if (stylingOptions != null) {
- fragment.setStylingOptions(stylingOptions);
- }
+ if (stylingOptions != null) {
+ mapFragment.setStylingOptions(stylingOptions);
+ }
+ } else {
+ NavViewFragment navFragment = new NavViewFragment(reactContext, root.getId());
+ fragmentMap.put(viewId, new WeakReference(navFragment));
+ fragment = navFragment;
+ if (stylingOptions != null) {
+ navFragment.setStylingOptions(stylingOptions);
+ }
+ }
activity
.getSupportFragmentManager()
.beginTransaction()
@@ -350,7 +381,7 @@ public void doFrame(long frameTimeNanos) {
/** Layout all children properly */
public void manuallyLayoutChildren(FrameLayout view) {
- NavViewFragment fragment = getFragmentForRoot(view);
+ IMapViewFragment fragment = getFragmentForRoot(view);
if (fragment.isAdded()) {
View childView = fragment.getView();
if (childView != null) {
diff --git a/example/src/MultipleMapsScreen.tsx b/example/src/MultipleMapsScreen.tsx
index 4b34dc1..2300a3f 100644
--- a/example/src/MultipleMapsScreen.tsx
+++ b/example/src/MultipleMapsScreen.tsx
@@ -36,10 +36,11 @@ import {
type LatLng,
type NavigationCallbacks,
useNavigation,
+ MapView,
+ NavigationView,
} from '@googlemaps/react-native-navigation-sdk';
import usePermissions from './checkPermissions';
import OverlayModal from './overlayModal';
-import { NavigationView } from '../../src/navigation/navigationView/navigationView';
const showSnackbar = (text: string, duration = Snackbar.LENGTH_SHORT) => {
Snackbar.show({ text, duration });
@@ -61,8 +62,6 @@ const MultipleMapsScreen = () => {
useState(null);
const [navigationViewController1, setNavigationViewController1] =
useState(null);
- const [navigationViewController2, setNavigationViewController2] =
- useState(null);
const [navigationInitialized, setNavigationInitialized] = useState(false);
const { navigationController, addListeners, removeListeners } =
useNavigation();
@@ -92,22 +91,17 @@ const MultipleMapsScreen = () => {
}, []);
const onNavigationReady = useCallback(async () => {
- if (
- navigationViewController1 != null &&
- navigationViewController2 != null
- ) {
+ if (navigationViewController1 != null) {
await navigationViewController1.setNavigationUIEnabled(true);
- await navigationViewController2.setNavigationUIEnabled(true);
console.log('onNavigationReady');
setNavigationInitialized(true);
}
- }, [navigationViewController1, navigationViewController2]);
+ }, [navigationViewController1]);
const onNavigationDispose = useCallback(async () => {
await navigationViewController1?.setNavigationUIEnabled(false);
- await navigationViewController2?.setNavigationUIEnabled(false);
setNavigationInitialized(false);
- }, [navigationViewController1, navigationViewController2]);
+ }, [navigationViewController1]);
const onNavigationInitError = useCallback(
(errorCode: NavigationInitErrorCode) => {
@@ -346,20 +340,9 @@ const MultipleMapsScreen = () => {
onNavigationViewControllerCreated={setNavigationViewController1}
/>
-
{navigationViewController1 != null &&
diff --git a/ios/react-native-navigation-sdk/CustomTypes.h b/ios/react-native-navigation-sdk/CustomTypes.h
new file mode 100644
index 0000000..667fee7
--- /dev/null
+++ b/ios/react-native-navigation-sdk/CustomTypes.h
@@ -0,0 +1,24 @@
+/**
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#ifndef CustomTypes_h
+#define CustomTypes_h
+
+typedef NS_ENUM(NSInteger, FragmentType) {
+ MAP,
+ NAVIGATION,
+};
+
+#endif /* CustomTypes_h */
diff --git a/ios/react-native-navigation-sdk/NavView.h b/ios/react-native-navigation-sdk/NavView.h
index 3de7da9..aaabaf9 100644
--- a/ios/react-native-navigation-sdk/NavView.h
+++ b/ios/react-native-navigation-sdk/NavView.h
@@ -17,6 +17,7 @@
#import
#import
#import
+#import "CustomTypes.h"
#import "INavigationViewCallback.h"
@class NavViewController;
@@ -33,7 +34,8 @@
@property(nonatomic, copy) RCTDirectEventBlock onCircleClick;
@property(nonatomic, copy) RCTDirectEventBlock onGroundOverlayClick;
-- (NavViewController *)initializeViewControllerWithStylingOptions:(NSDictionary *)stylingOptions;
+- (NavViewController *)initializeViewControllerWithStylingOptions:(NSDictionary *)stylingOptions
+ fragmentType:(FragmentType)fragmentType;
- (NavViewController *)getViewController;
@end
diff --git a/ios/react-native-navigation-sdk/NavView.m b/ios/react-native-navigation-sdk/NavView.m
index 9f8162f..c54ba00 100644
--- a/ios/react-native-navigation-sdk/NavView.m
+++ b/ios/react-native-navigation-sdk/NavView.m
@@ -36,8 +36,11 @@ - (void)layoutSubviews {
}
}
-- (NavViewController *)initializeViewControllerWithStylingOptions:(NSDictionary *)stylingOptions {
+- (NavViewController *)initializeViewControllerWithStylingOptions:(NSDictionary *)stylingOptions
+ fragmentType:(FragmentType)fragmentType {
_viewController = [[NavViewController alloc] init];
+ // FragmentType 0 = MAP, 1 = NAVIGATION.
+ _viewController.isNavigationEnabled = fragmentType == NAVIGATION;
// Test if styling options is not nil
if (stylingOptions != nil && [stylingOptions count] > 0) {
[_viewController setStylingOptions:stylingOptions];
diff --git a/ios/react-native-navigation-sdk/NavViewController.h b/ios/react-native-navigation-sdk/NavViewController.h
index 96a4978..e72c54e 100644
--- a/ios/react-native-navigation-sdk/NavViewController.h
+++ b/ios/react-native-navigation-sdk/NavViewController.h
@@ -24,6 +24,7 @@ NS_ASSUME_NONNULL_BEGIN
@interface NavViewController : UIViewController
@property(weak, nonatomic) id callbacks;
+@property(nonatomic, assign) BOOL isNavigationEnabled;
typedef void (^RouteStatusCallback)(GMSRouteStatus routeStatus);
typedef void (^OnStringResult)(NSString *result);
typedef void (^OnBooleanResult)(BOOL result);
diff --git a/ios/react-native-navigation-sdk/NavViewController.m b/ios/react-native-navigation-sdk/NavViewController.m
index 6669380..2af7d5a 100644
--- a/ios/react-native-navigation-sdk/NavViewController.m
+++ b/ios/react-native-navigation-sdk/NavViewController.m
@@ -168,10 +168,7 @@ - (void)applyStylingOptions {
}
- (void)setZoomLevel:(nonnull NSNumber *)level {
- _mapView.camera =
- [GMSMutableCameraPosition cameraWithLatitude:_mapView.myLocation.coordinate.latitude
- longitude:_mapView.myLocation.coordinate.longitude
- zoom:[level floatValue]];
+ [_mapView animateToZoom:[level floatValue]];
}
- (void)setNavigationUIEnabled:(BOOL)isEnabled {
@@ -280,6 +277,9 @@ - (void)setSpeedLimitIconEnabled:(BOOL)isEnabled {
#pragma mark - View Controller functions
- (BOOL)attachToNavigationSession:(GMSNavigationSession *)session {
+ if (!_isNavigationEnabled) {
+ return NO;
+ }
BOOL result = [_mapView enableNavigationWithSession:session];
_mapView.navigationUIDelegate = self;
[self applyStylingOptions];
diff --git a/ios/react-native-navigation-sdk/RCTNavViewManager.m b/ios/react-native-navigation-sdk/RCTNavViewManager.m
index e869822..4a41f66 100644
--- a/ios/react-native-navigation-sdk/RCTNavViewManager.m
+++ b/ios/react-native-navigation-sdk/RCTNavViewManager.m
@@ -16,11 +16,15 @@
#import "RCTNavViewManager.h"
#import
+#import "CustomTypes.h"
#import "NavView.h"
#import "NavViewController.h"
#import "NavViewModule.h"
#import "ObjectTranslationUtil.h"
+// RCTNavViewManager is responsible for managing both the regular map fragment as well as the
+// navigation map view fragment.
+//
@implementation RCTNavViewManager
static NSMutableDictionary *_viewControllers;
static NavViewModule *_navViewModule;
@@ -72,20 +76,21 @@ - (void)unregisterViewControllerForTag:(NSNumber *)reactTag {
RCT_EXPORT_METHOD(createFragment
: (nonnull NSNumber *)reactTag stylingOptions
- : (NSDictionary *)stylingOptions) {
- [self.bridge.uiManager
- addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) {
- NavView *view = (NavView *)viewRegistry[reactTag];
- if (!view || ![view isKindOfClass:[NavView class]]) {
- RCTLogError(@"Cannot find NativeView with tag #%@", reactTag);
- return;
- }
+ : (NSDictionary *)stylingOptions fragmentType
+ : (NSInteger)fragmentType) {
+ [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager,
+ NSDictionary *viewRegistry) {
+ NavView *view = (NavView *)viewRegistry[reactTag];
+ if (!view || ![view isKindOfClass:[NavView class]]) {
+ RCTLogError(@"Cannot find NativeView with tag #%@", reactTag);
+ return;
+ }
- NavViewController *viewController =
- [view initializeViewControllerWithStylingOptions:stylingOptions];
+ NavViewController *viewController =
+ [view initializeViewControllerWithStylingOptions:stylingOptions fragmentType:fragmentType];
- [self registerViewController:viewController forTag:reactTag];
- }];
+ [self registerViewController:viewController forTag:reactTag];
+ }];
}
RCT_EXPORT_METHOD(deleteFragment : (nonnull NSNumber *)reactTag) {
diff --git a/src/maps/mapView/index.ts b/src/maps/mapView/index.ts
index fa4be43..409f57d 100644
--- a/src/maps/mapView/index.ts
+++ b/src/maps/mapView/index.ts
@@ -16,3 +16,4 @@
export * from './types';
export * from './mapViewController';
+export * from './mapView';
diff --git a/src/maps/mapView/mapView.tsx b/src/maps/mapView/mapView.tsx
new file mode 100644
index 0000000..9964a5d
--- /dev/null
+++ b/src/maps/mapView/mapView.tsx
@@ -0,0 +1,148 @@
+/**
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React, { useCallback, useEffect, useRef, useState } from 'react';
+import { StyleSheet, View, findNodeHandle } from 'react-native';
+import {
+ NavViewManager,
+ sendCommand,
+ commands,
+ type LatLng,
+} from '../../shared';
+import {
+ getMapViewController,
+ FragmentType,
+ type Circle,
+ type GroundOverlay,
+ type MapViewProps,
+ type Marker,
+ type Polygon,
+ type Polyline,
+} from '..';
+
+export const MapView = (props: MapViewProps) => {
+ const mapViewRef = useRef(null);
+ const [viewId, setViewId] = useState(null);
+
+ const { onMapViewControllerCreated } = props;
+
+ /**
+ * @param {any} _ref - The reference to the NavViewManager component.
+ */
+ const onRefAssign = (_ref: any) => {
+ if (mapViewRef.current !== _ref) {
+ mapViewRef.current = _ref;
+ }
+ };
+
+ useEffect(() => {
+ if (!mapViewRef.current) {
+ return;
+ }
+ const _viewId = findNodeHandle(mapViewRef.current) || 0;
+ if (viewId !== _viewId) {
+ setViewId(_viewId);
+
+ const stylingOptions = {};
+
+ const args = [stylingOptions, FragmentType.MAP];
+
+ setTimeout(() => {
+ sendCommand(_viewId, commands.createFragment, args);
+ });
+
+ onMapViewControllerCreated(getMapViewController(_viewId));
+ }
+ }, [onMapViewControllerCreated, viewId]);
+
+ const onMapClick = useCallback(
+ ({ nativeEvent: latlng }: { nativeEvent: LatLng }) => {
+ props.mapViewCallbacks?.onMapClick?.(latlng);
+ },
+ [props.mapViewCallbacks]
+ );
+
+ const onMapReady = useCallback(() => {
+ props.mapViewCallbacks?.onMapReady?.();
+ }, [props.mapViewCallbacks]);
+
+ const onMarkerClick = useCallback(
+ ({ nativeEvent: marker }: { nativeEvent: Marker }) => {
+ props.mapViewCallbacks?.onMarkerClick?.(marker);
+ },
+ [props.mapViewCallbacks]
+ );
+
+ const onPolylineClick = useCallback(
+ ({ nativeEvent: polyline }: { nativeEvent: Polyline }) => {
+ props.mapViewCallbacks?.onPolylineClick?.(polyline);
+ },
+ [props.mapViewCallbacks]
+ );
+
+ const onPolygonClick = useCallback(
+ ({ nativeEvent: polygon }: { nativeEvent: Polygon }) => {
+ props.mapViewCallbacks?.onPolygonClick?.(polygon);
+ },
+ [props.mapViewCallbacks]
+ );
+
+ const onCircleClick = useCallback(
+ ({ nativeEvent: circle }: { nativeEvent: Circle }) => {
+ props.mapViewCallbacks?.onCircleClick?.(circle);
+ },
+ [props.mapViewCallbacks]
+ );
+
+ const onGroundOverlayClick = useCallback(
+ ({ nativeEvent: groundOverlay }: { nativeEvent: GroundOverlay }) => {
+ props.mapViewCallbacks?.onGroundOverlayClick?.(groundOverlay);
+ },
+ [props.mapViewCallbacks]
+ );
+
+ const onMarkerInfoWindowTapped = useCallback(
+ ({ nativeEvent: marker }: { nativeEvent: Marker }) => {
+ props.mapViewCallbacks?.onMarkerInfoWindowTapped?.(marker);
+ },
+ [props.mapViewCallbacks]
+ );
+
+ return (
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ defaultStyle: {
+ flex: 1,
+ },
+});
+
+export default MapView;
diff --git a/src/maps/mapView/types.ts b/src/maps/mapView/types.ts
index 2856d70..92e334b 100644
--- a/src/maps/mapView/types.ts
+++ b/src/maps/mapView/types.ts
@@ -123,6 +123,16 @@ export enum MapType {
HYBRID,
}
+/**
+ * Defines the type of the map fragment.
+ */
+export enum FragmentType {
+ /** Regular Google map view without navigation */
+ MAP = 0,
+ /** Google map view with navigation */
+ NAVIGATION,
+}
+
/**
* `MapViewProps` interface provides a set of method definitions
* for managing map events and debug information.
diff --git a/src/maps/types.ts b/src/maps/types.ts
index a217726..642c32a 100644
--- a/src/maps/types.ts
+++ b/src/maps/types.ts
@@ -14,7 +14,9 @@
* limitations under the License.
*/
+import type { StyleProp, ViewStyle } from 'react-native';
import type { LatLng } from '../shared/types';
+import type { MapViewCallbacks, MapViewController } from './mapView/types';
/**
* An immutable class that aggregates all camera position parameters such as
@@ -157,3 +159,14 @@ export interface UISettings {
/** Defines zoom gestures are enabled/disabled on the GoogleMap. */
isZoomGesturesEnabled: boolean;
}
+
+/**
+ * `MapViewProps` interface provides methods focused on managing map events and state changes.
+ */
+export interface MapViewProps {
+ readonly mapViewCallbacks?: MapViewCallbacks;
+
+ readonly style?: StyleProp | undefined;
+
+ onMapViewControllerCreated(mapViewController: MapViewController): void;
+}
diff --git a/src/navigation/navigationView/navigationView.tsx b/src/navigation/navigationView/navigationView.tsx
index 4749175..21d57d4 100644
--- a/src/navigation/navigationView/navigationView.tsx
+++ b/src/navigation/navigationView/navigationView.tsx
@@ -26,6 +26,7 @@ import { getNavigationViewController } from './navigationViewController';
import type { NavigationViewProps } from './types';
import {
getMapViewController,
+ FragmentType,
type Circle,
type GroundOverlay,
type Marker,
@@ -61,11 +62,12 @@ export const NavigationView = (props: NavigationViewProps) => {
if (viewId !== _viewId) {
setViewId(_viewId);
- const args = [
+ const stylingOptions =
(Platform.OS === 'android'
? androidStylingOptions
- : iOSStylingOptions) || {},
- ];
+ : iOSStylingOptions) || {};
+
+ const args = [stylingOptions, FragmentType.NAVIGATION];
setTimeout(() => {
sendCommand(_viewId, commands.createFragment, args);
diff --git a/src/shared/viewManager.ts b/src/shared/viewManager.ts
index 7be0e8f..f03624d 100644
--- a/src/shared/viewManager.ts
+++ b/src/shared/viewManager.ts
@@ -25,6 +25,7 @@ import type { DirectEventHandler } from 'react-native/Libraries/Types/CodegenTyp
import type { LatLng } from '.';
import type { Circle, GroundOverlay, Marker, Polygon, Polyline } from '../maps';
+// NavViewManager is responsible for managing both the regular map fragment as well as the navigation map view fragment.
export const viewManagerName =
Platform.OS === 'android' ? 'NavViewManager' : 'RCTNavView';