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';