From 7921f4941f6656fe9c588d4f5e9d1f5594632598 Mon Sep 17 00:00:00 2001 From: pinpong Date: Tue, 7 Oct 2025 12:16:25 +0700 Subject: [PATCH] feat: add map ui settings support feat: add location config support feat: add myLocationEnabled support feat: add indoorEnabled support refactor: code optimizations and cleanup chore: updated example --- .../rngooglemapsplus/GoogleMapsViewImpl.kt | 151 +++++++++++--- .../com/rngooglemapsplus/LocationHandler.kt | 88 ++++----- .../{MapCircle.kt => MapCircleBuilder.kt} | 14 +- .../{MapMarker.kt => MapMarkerBuilder.kt} | 2 +- .../{MapPolygon.kt => MapPolygonBuilder.kt} | 20 +- ...apPolyline.kt => MapPolylineBuilder.kt.kt} | 21 +- .../rngooglemapsplus/RNGoogleMapsPlusView.kt | 97 ++++----- .../extensions/RNCameraExtension.kt | 19 ++ .../extensions/RNLocationPriorityExtension.kt | 12 ++ .../extensions/RNMapCircleExtension.kt | 14 ++ .../extensions/RNPolygonExtension.kt | 20 ++ .../extensions/RNPolylineExtension.kt | 21 ++ .../extensions/RNUserInterfaceExtension.kt | 12 ++ .../StringExtension.kt} | 2 +- .../extensions/ThrowableExtension.kt | 38 ++++ example/ios/Podfile.lock | 4 +- example/src/App.tsx | 30 ++- ios/.swiftlint.yml | 4 +- ios/GoogleMapViewImpl.swift | 187 +++++++++++++++--- ios/LocationHandler.swift | 98 +++------ ios/MapCircleBuilder.swift | 20 ++ ...MapMarker.swift => MapMarkerBuilder.swift} | 26 +-- ios/MapPolygonBuilder.swift | 20 ++ ios/MapPolylineBuilder.swift | 24 +++ ios/RNGoogleMapsPlusView.swift | 178 ++++++----------- ios/extensions/RNCamera+Extension.swift | 22 +++ .../RNCircle+Extension.swift} | 19 -- .../RNIOSLocationAccuracy+Extensions.swift | 19 ++ ios/extensions/RNMarker+Extension.swift | 24 +++ .../RNPolygon+Extension.swift.swift} | 19 -- .../RNPolyline+Extension.swift.swift} | 55 ++---- .../RNUserInterface+Extension.swift | 16 ++ .../String+Extensions.swift} | 0 src/RNGoogleMapsPlusView.nitro.ts | 6 + src/types.ts | 45 ++++- 35 files changed, 856 insertions(+), 491 deletions(-) rename android/src/main/java/com/rngooglemapsplus/{MapCircle.kt => MapCircleBuilder.kt} (62%) rename android/src/main/java/com/rngooglemapsplus/{MapMarker.kt => MapMarkerBuilder.kt} (99%) rename android/src/main/java/com/rngooglemapsplus/{MapPolygon.kt => MapPolygonBuilder.kt} (54%) rename android/src/main/java/com/rngooglemapsplus/{MapPolyline.kt => MapPolylineBuilder.kt.kt} (70%) create mode 100644 android/src/main/java/com/rngooglemapsplus/extensions/RNCameraExtension.kt create mode 100644 android/src/main/java/com/rngooglemapsplus/extensions/RNLocationPriorityExtension.kt create mode 100644 android/src/main/java/com/rngooglemapsplus/extensions/RNMapCircleExtension.kt create mode 100644 android/src/main/java/com/rngooglemapsplus/extensions/RNPolygonExtension.kt create mode 100644 android/src/main/java/com/rngooglemapsplus/extensions/RNPolylineExtension.kt create mode 100644 android/src/main/java/com/rngooglemapsplus/extensions/RNUserInterfaceExtension.kt rename android/src/main/java/com/rngooglemapsplus/{Color.kt => extensions/StringExtension.kt} (97%) create mode 100644 android/src/main/java/com/rngooglemapsplus/extensions/ThrowableExtension.kt create mode 100644 ios/MapCircleBuilder.swift rename ios/{MapMarker.swift => MapMarkerBuilder.swift} (86%) create mode 100644 ios/MapPolygonBuilder.swift create mode 100644 ios/MapPolylineBuilder.swift create mode 100644 ios/extensions/RNCamera+Extension.swift rename ios/{MapCircle.swift => extensions/RNCircle+Extension.swift} (59%) create mode 100644 ios/extensions/RNIOSLocationAccuracy+Extensions.swift create mode 100644 ios/extensions/RNMarker+Extension.swift rename ios/{MapPolygon.swift => extensions/RNPolygon+Extension.swift.swift} (66%) rename ios/{MapPolyline.swift => extensions/RNPolyline+Extension.swift.swift} (57%) create mode 100644 ios/extensions/RNUserInterface+Extension.swift rename ios/{Color.swift => extensions/String+Extensions.swift} (100%) diff --git a/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt b/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt index d98eb6c..cb9e084 100644 --- a/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt +++ b/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt @@ -1,5 +1,6 @@ package com.rngooglemapsplus +import android.annotation.SuppressLint import android.location.Location import android.widget.FrameLayout import com.facebook.react.bridge.LifecycleEventListener @@ -24,12 +25,14 @@ 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.rngooglemapsplus.extensions.toGooglePriority +import com.rngooglemapsplus.extensions.toLocationErrorCode class GoogleMapsViewImpl( val reactContext: ThemedReactContext, val locationHandler: LocationHandler, val playServiceHandler: PlayServicesHandler, - val markerOptions: com.rngooglemapsplus.MarkerOptions, + val markerBuilder: MarkerBuilder, ) : FrameLayout(reactContext), GoogleMap.OnCameraMoveStartedListener, GoogleMap.OnCameraMoveListener, @@ -252,12 +255,37 @@ class GoogleMapsViewImpl( it.bottom.dpToPx().toInt(), ) } + + uiSettings?.let { v -> + googleMap?.uiSettings?.apply { + v.allGesturesEnabled?.let { setAllGesturesEnabled(it) } + v.compassEnabled?.let { isCompassEnabled = it } + v.indoorLevelPickerEnabled?.let { isIndoorLevelPickerEnabled = it } + v.mapToolbarEnabled?.let { isMapToolbarEnabled = it } + v.myLocationButtonEnabled?.let { + googleMap?.setLocationSource(locationHandler) + isMyLocationButtonEnabled = it + } + v.rotateEnabled?.let { isRotateGesturesEnabled = it } + v.scrollEnabled?.let { isScrollGesturesEnabled = it } + v.scrollDuringRotateOrZoomEnabled?.let { + isScrollGesturesEnabledDuringRotateOrZoom = it + } + v.tiltEnabled?.let { isTiltGesturesEnabled = it } + v.zoomControlsEnabled?.let { isZoomControlsEnabled = it } + v.zoomGesturesEnabled?.let { isZoomGesturesEnabled = it } + } + } + buildingEnabled?.let { googleMap?.isBuildingsEnabled = it } trafficEnabled?.let { googleMap?.isTrafficEnabled = it } + indoorEnabled?.let { + googleMap?.isIndoorEnabled = it + } googleMap?.setMapStyle(customMapStyle) mapType?.let { googleMap?.mapType = it @@ -273,6 +301,12 @@ class GoogleMapsViewImpl( } } + locationConfig?.let { + locationHandler.priority = it.android?.priority?.toGooglePriority() + locationHandler.interval = it.android?.interval?.toLong() + locationHandler.minUpdateInterval = it.android?.minUpdateInterval?.toLong() + } + if (pendingMarkers.isNotEmpty()) { pendingMarkers.forEach { (id, opts) -> internalAddMarker(id, opts) @@ -302,6 +336,69 @@ class GoogleMapsViewImpl( } } + var uiSettings: RNMapUiSettings? = null + set(value) { + field = value + onUi { + value?.let { v -> + googleMap?.uiSettings?.apply { + v.allGesturesEnabled?.let { setAllGesturesEnabled(it) } + v.compassEnabled?.let { isCompassEnabled = it } + v.indoorLevelPickerEnabled?.let { isIndoorLevelPickerEnabled = it } + v.mapToolbarEnabled?.let { isMapToolbarEnabled = it } + v.myLocationButtonEnabled?.let { + googleMap?.setLocationSource(locationHandler) + isMyLocationButtonEnabled = it + } + v.rotateEnabled?.let { isRotateGesturesEnabled = it } + v.scrollEnabled?.let { isScrollGesturesEnabled = it } + v.scrollDuringRotateOrZoomEnabled?.let { + isScrollGesturesEnabledDuringRotateOrZoom = it + } + v.tiltEnabled?.let { isTiltGesturesEnabled = it } + v.zoomControlsEnabled?.let { isZoomControlsEnabled = it } + v.zoomGesturesEnabled?.let { isZoomGesturesEnabled = it } + } + } + ?: run { + googleMap?.uiSettings?.apply { + setAllGesturesEnabled(true) + isCompassEnabled = false + isIndoorLevelPickerEnabled = false + isMapToolbarEnabled = false + isMyLocationButtonEnabled = false + googleMap?.setLocationSource(null) + isRotateGesturesEnabled = true + isScrollGesturesEnabled = true + isScrollGesturesEnabledDuringRotateOrZoom = true + isTiltGesturesEnabled = true + isZoomControlsEnabled = false + isZoomGesturesEnabled = false + } + } + } + } + + @SuppressLint("MissingPermission") + var myLocationEnabled: Boolean? = null + set(value) { + onUi { + try { + value?.let { + googleMap?.isMyLocationEnabled = it + } + ?: run { + googleMap?.isMyLocationEnabled = false + } + } catch (se: SecurityException) { + onLocationError?.invoke(RNLocationErrorCode.PERMISSION_DENIED) + } catch (ex: Exception) { + val error = ex.toLocationErrorCode(context) + onLocationError?.invoke(error) + } + } + } + var buildingEnabled: Boolean? = null set(value) { field = value @@ -327,6 +424,19 @@ class GoogleMapsViewImpl( } } + var indoorEnabled: Boolean? = null + set(value) { + field = value + onUi { + value?.let { + googleMap?.isIndoorEnabled = it + } + ?: run { + googleMap?.isIndoorEnabled = false + } + } + } + var customMapStyle: MapStyleOptions? = null set(value) { field = value @@ -400,6 +510,14 @@ class GoogleMapsViewImpl( } } + var locationConfig: RNLocationConfig? = null + set(value) { + field = value + locationHandler.priority = value?.android?.priority?.toGooglePriority() + locationHandler.interval = value?.android?.interval?.toLong() + locationHandler.minUpdateInterval = value?.android?.minUpdateInterval?.toLong() + } + var onMapError: ((RNMapErrorCode) -> Unit)? = null var onMapReady: ((Boolean) -> Unit)? = null var onLocationUpdate: ((RNLocation) -> Unit)? = null @@ -414,7 +532,7 @@ class GoogleMapsViewImpl( var onCameraChangeComplete: ((RNRegion, RNCamera, Boolean) -> Unit)? = null fun setCamera( - camera: RNCamera, + cameraPosition: CameraPosition, animated: Boolean, durationMS: Int, ) { @@ -423,33 +541,8 @@ class GoogleMapsViewImpl( if (current == null) { return@onUi } - val camPosBuilder = - CameraPosition.Builder( - current, - ) - - camera.center?.let { - camPosBuilder.target( - LatLng( - it.latitude, - it.longitude, - ), - ) - } - - camera.zoom?.let { - camPosBuilder.zoom(it.toFloat()) - } - camera.bearing?.let { - camPosBuilder.bearing(it.toFloat()) - } - camera.tilt?.let { - camPosBuilder.tilt(it.toFloat()) - } - - val camPos = camPosBuilder.build() - val update = CameraUpdateFactory.newCameraPosition(camPos) + val update = CameraUpdateFactory.newCameraPosition(cameraPosition) if (animated) { googleMap?.animateCamera(update, durationMS, null) @@ -746,7 +839,7 @@ class GoogleMapsViewImpl( fun destroyInternal() { onUi { - markerOptions.cancelAllJobs() + markerBuilder.cancelAllJobs() clearMarkers() clearPolylines() clearPolygons() diff --git a/android/src/main/java/com/rngooglemapsplus/LocationHandler.kt b/android/src/main/java/com/rngooglemapsplus/LocationHandler.kt index 22c9101..32d61ba 100644 --- a/android/src/main/java/com/rngooglemapsplus/LocationHandler.kt +++ b/android/src/main/java/com/rngooglemapsplus/LocationHandler.kt @@ -10,8 +10,6 @@ import com.facebook.react.bridge.ReactContext import com.facebook.react.bridge.UiThreadUtil import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.GoogleApiAvailability -import com.google.android.gms.common.api.ApiException -import com.google.android.gms.common.api.CommonStatusCodes import com.google.android.gms.common.api.ResolvableApiException import com.google.android.gms.location.FusedLocationProviderClient import com.google.android.gms.location.LocationCallback @@ -19,22 +17,43 @@ import com.google.android.gms.location.LocationRequest import com.google.android.gms.location.LocationResult import com.google.android.gms.location.LocationServices import com.google.android.gms.location.LocationSettingsRequest -import com.google.android.gms.location.LocationSettingsStatusCodes import com.google.android.gms.location.Priority +import com.google.android.gms.maps.LocationSource import com.google.android.gms.tasks.OnSuccessListener +import com.rngooglemapsplus.extensions.toLocationErrorCode private const val REQ_LOCATION_SETTINGS = 2001 +private const val PRIORITY_DEFAULT = Priority.PRIORITY_BALANCED_POWER_ACCURACY +private const val INTERVAL_DEFAULT = 600000L +private const val MIN_UPDATE_INTERVAL = 3600000L class LocationHandler( val context: ReactContext, -) { +) : LocationSource { private val fusedLocationClientProviderClient: FusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context) + private var listener: LocationSource.OnLocationChangedListener? = null private var locationRequest: LocationRequest? = null private var locationCallback: LocationCallback? = null - private var priority = Priority.PRIORITY_HIGH_ACCURACY - private var interval: Long = 5000 - private var minUpdateInterval: Long = 5000 + + var priority: Int? = PRIORITY_DEFAULT + set(value) { + field = value + start() + } + + var interval: Long? = INTERVAL_DEFAULT + set(value) { + field = value + buildLocationRequest() + } + + var minUpdateInterval: Long? = MIN_UPDATE_INTERVAL + set(value) { + field = value + buildLocationRequest() + } + var onUpdate: ((Location) -> Unit)? = null var onError: ((RNLocationErrorCode) -> Unit)? = null @@ -90,6 +109,10 @@ class LocationHandler( @Suppress("deprecation") private fun buildLocationRequest() { + val priority = priority ?: Priority.PRIORITY_BALANCED_POWER_ACCURACY + val interval = interval ?: INTERVAL_DEFAULT + val minUpdateInterval = minUpdateInterval ?: MIN_UPDATE_INTERVAL + locationRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { LocationRequest @@ -106,21 +129,6 @@ class LocationHandler( restartLocationUpdates() } - fun setPriority(priority: Int) { - this.priority = priority - buildLocationRequest() - } - - fun setInterval(interval: Int) { - this.interval = interval.toLong() - buildLocationRequest() - } - - fun setFastestInterval(fastestInterval: Int) { - this.minUpdateInterval = fastestInterval.toLong() - buildLocationRequest() - } - private fun restartLocationUpdates() { stop() // 4) Google Play Services checken – früh zurückmelden @@ -146,7 +154,7 @@ class LocationHandler( } }, ).addOnFailureListener { e -> - val error = mapThrowableToCode(e) + val error = e.toLocationErrorCode(context) onError?.invoke(error) } locationCallback = @@ -154,6 +162,7 @@ class LocationHandler( override fun onLocationResult(locationResult: LocationResult) { val location = locationResult.lastLocation if (location != null) { + listener?.onLocationChanged(location) onUpdate?.invoke(location) } else { onError?.invoke(RNLocationErrorCode.POSITION_UNAVAILABLE) @@ -166,40 +175,31 @@ class LocationHandler( locationCallback!!, Looper.getMainLooper(), ).addOnFailureListener { e -> - val error = mapThrowableToCode(e) + val error = e.toLocationErrorCode(context) onError?.invoke(error) } } catch (se: SecurityException) { onError?.invoke(RNLocationErrorCode.PERMISSION_DENIED) } catch (ex: Exception) { - val error = mapThrowableToCode(ex) + val error = ex.toLocationErrorCode(context) onError?.invoke(error) } } - private fun mapThrowableToCode(t: Throwable): RNLocationErrorCode { - if (t is SecurityException) return RNLocationErrorCode.PERMISSION_DENIED - if (t.message?.contains("GoogleApi", ignoreCase = true) == true) { - val gms = GoogleApiAvailability.getInstance() - val status = gms.isGooglePlayServicesAvailable(context) - if (status != ConnectionResult.SUCCESS) return RNLocationErrorCode.PLAY_SERVICE_NOT_AVAILABLE - } - if (t is ApiException) { - when (t.statusCode) { - CommonStatusCodes.NETWORK_ERROR -> return RNLocationErrorCode.POSITION_UNAVAILABLE - LocationSettingsStatusCodes.RESOLUTION_REQUIRED, - LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE, - -> return RNLocationErrorCode.SETTINGS_NOT_SATISFIED - } - return RNLocationErrorCode.INTERNAL_ERROR - } - return RNLocationErrorCode.INTERNAL_ERROR - } - fun stop() { + listener = null if (locationCallback != null) { fusedLocationClientProviderClient.removeLocationUpdates(locationCallback!!) locationCallback = null } } + + override fun activate(listener: LocationSource.OnLocationChangedListener) { + this.listener = listener + start() + } + + override fun deactivate() { + stop() + } } diff --git a/android/src/main/java/com/rngooglemapsplus/MapCircle.kt b/android/src/main/java/com/rngooglemapsplus/MapCircleBuilder.kt similarity index 62% rename from android/src/main/java/com/rngooglemapsplus/MapCircle.kt rename to android/src/main/java/com/rngooglemapsplus/MapCircleBuilder.kt index eb919b3..f4ac789 100644 --- a/android/src/main/java/com/rngooglemapsplus/MapCircle.kt +++ b/android/src/main/java/com/rngooglemapsplus/MapCircleBuilder.kt @@ -3,8 +3,9 @@ package com.rngooglemapsplus import com.facebook.react.uimanager.PixelUtil.dpToPx import com.google.android.gms.maps.model.CircleOptions import com.google.android.gms.maps.model.LatLng +import com.rngooglemapsplus.extensions.toColor -class MapCircleOptions { +class MapCircleBuilder { fun buildCircleOptions(circle: RNCircle): CircleOptions = CircleOptions().apply { center(LatLng(circle.center.latitude, circle.center.longitude)) @@ -16,14 +17,3 @@ class MapCircleOptions { circle.zIndex?.let { zIndex(it.toFloat()) } } } - -fun RNCircle.circleEquals(b: RNCircle): Boolean { - if (zIndex != b.zIndex) return false - if (pressable != b.pressable) return false - if (center != b.center) return false - if (radius != b.radius) return false - if (strokeWidth != b.strokeWidth) return false - if (strokeColor != b.strokeColor) return false - if (fillColor != b.fillColor) return false - return true -} diff --git a/android/src/main/java/com/rngooglemapsplus/MapMarker.kt b/android/src/main/java/com/rngooglemapsplus/MapMarkerBuilder.kt similarity index 99% rename from android/src/main/java/com/rngooglemapsplus/MapMarker.kt rename to android/src/main/java/com/rngooglemapsplus/MapMarkerBuilder.kt index 20da17f..b47584e 100644 --- a/android/src/main/java/com/rngooglemapsplus/MapMarker.kt +++ b/android/src/main/java/com/rngooglemapsplus/MapMarkerBuilder.kt @@ -19,7 +19,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlin.coroutines.coroutineContext -class MarkerOptions( +class MarkerBuilder( private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default), ) { private val iconCache = diff --git a/android/src/main/java/com/rngooglemapsplus/MapPolygon.kt b/android/src/main/java/com/rngooglemapsplus/MapPolygonBuilder.kt similarity index 54% rename from android/src/main/java/com/rngooglemapsplus/MapPolygon.kt rename to android/src/main/java/com/rngooglemapsplus/MapPolygonBuilder.kt index 6c50ee1..4bae53b 100644 --- a/android/src/main/java/com/rngooglemapsplus/MapPolygon.kt +++ b/android/src/main/java/com/rngooglemapsplus/MapPolygonBuilder.kt @@ -2,8 +2,9 @@ package com.rngooglemapsplus import com.facebook.react.uimanager.PixelUtil.dpToPx import com.google.android.gms.maps.model.PolygonOptions +import com.rngooglemapsplus.extensions.toColor -class MapPolygonOptions { +class MapPolygonBuilder { fun buildPolygonOptions(poly: RNPolygon): PolygonOptions = PolygonOptions().apply { poly.coordinates.forEach { pt -> @@ -19,20 +20,3 @@ class MapPolygonOptions { poly.zIndex?.let { zIndex(it.toFloat()) } } } - -fun RNPolygon.polygonEquals(b: RNPolygon): Boolean { - if (zIndex != b.zIndex) return false - if (pressable != b.pressable) return false - if (strokeWidth != b.strokeWidth) return false - if (fillColor != b.fillColor) return false - if (strokeColor != b.strokeColor) return false - val ac = coordinates - val bc = b.coordinates - if (ac.size != bc.size) return false - for (i in ac.indices) { - val p = ac[i] - val q = bc[i] - if (p.latitude != q.latitude || p.longitude != q.longitude) return false - } - return true -} diff --git a/android/src/main/java/com/rngooglemapsplus/MapPolyline.kt b/android/src/main/java/com/rngooglemapsplus/MapPolylineBuilder.kt.kt similarity index 70% rename from android/src/main/java/com/rngooglemapsplus/MapPolyline.kt rename to android/src/main/java/com/rngooglemapsplus/MapPolylineBuilder.kt.kt index 1cd22ac..411c35f 100644 --- a/android/src/main/java/com/rngooglemapsplus/MapPolyline.kt +++ b/android/src/main/java/com/rngooglemapsplus/MapPolylineBuilder.kt.kt @@ -7,8 +7,9 @@ import com.google.android.gms.maps.model.JointType import com.google.android.gms.maps.model.PolylineOptions import com.google.android.gms.maps.model.RoundCap import com.google.android.gms.maps.model.SquareCap +import com.rngooglemapsplus.extensions.toColor -class MapPolylineOptions { +class MapPolylineBuilder { fun buildPolylineOptions(pl: RNPolyline): PolylineOptions = PolylineOptions().apply { pl.coordinates.forEach { pt -> @@ -41,21 +42,3 @@ class MapPolylineOptions { null -> JointType.DEFAULT } } - -fun RNPolyline.polylineEquals(b: RNPolyline): Boolean { - if (zIndex != b.zIndex) return false - if (pressable != b.pressable) return false - if ((width ?: 0.0) != (b.width ?: 0.0)) return false - if (lineCap != b.lineCap) return false - if (lineJoin != b.lineJoin) return false - if (color != b.color) return false - val ac = coordinates - val bc = b.coordinates - if (ac.size != bc.size) return false - for (i in ac.indices) { - val p = ac[i] - val q = bc[i] - if (p.latitude != q.latitude || p.longitude != q.longitude) return false - } - return true -} diff --git a/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt b/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt index 6d8a90e..bc2983b 100644 --- a/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt +++ b/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt @@ -4,11 +4,15 @@ import com.facebook.proguard.annotations.DoNotStrip import com.facebook.react.bridge.UiThreadUtil import com.facebook.react.uimanager.PixelUtil.dpToPx import com.facebook.react.uimanager.ThemedReactContext -import com.google.android.gms.maps.model.CameraPosition import com.google.android.gms.maps.model.LatLng -import com.google.android.gms.maps.model.MapColorScheme import com.google.android.gms.maps.model.MapStyleOptions import com.margelo.nitro.core.Promise +import com.rngooglemapsplus.extensions.circleEquals +import com.rngooglemapsplus.extensions.polygonEquals +import com.rngooglemapsplus.extensions.polylineEquals +import com.rngooglemapsplus.extensions.toCameraPosition +import com.rngooglemapsplus.extensions.toColor +import com.rngooglemapsplus.extensions.toMapColorScheme @DoNotStrip class RNGoogleMapsPlusView( @@ -19,23 +23,33 @@ class RNGoogleMapsPlusView( private var locationHandler = LocationHandler(context) private var playServiceHandler = PlayServicesHandler(context) - private val markerOptions = MarkerOptions() - private val polylineOptions = MapPolylineOptions() - private val polygonOptions = MapPolygonOptions() - private val circleOptions = MapCircleOptions() + private val markerBuilder = MarkerBuilder() + private val polylineBuilder = MapPolylineBuilder() + private val polygonBuilder = MapPolygonBuilder() + private val circleBuilder = MapCircleBuilder() override val view = - GoogleMapsViewImpl(context, locationHandler, playServiceHandler, markerOptions) + GoogleMapsViewImpl(context, locationHandler, playServiceHandler, markerBuilder) override var initialProps: RNInitialProps? = null set(value) { view.initMapView( value?.mapId, value?.liteMode, - mapCameraToCameraPosition(value?.initialCamera), + value?.camera?.toCameraPosition(), ) } + override var uiSettings: RNMapUiSettings? = null + set(value) { + view.uiSettings = value + } + + override var myLocationEnabled: Boolean? = null + set(value) { + view.myLocationEnabled = value + } + override var buildingEnabled: Boolean? = null set(value) { view.buildingEnabled = value @@ -46,6 +60,11 @@ class RNGoogleMapsPlusView( view.trafficEnabled = value } + override var indoorEnabled: Boolean? = null + set(value) { + view.indoorEnabled = value + } + override var customMapStyle: String? = null set(value) { currentCustomMapStyle = value @@ -56,7 +75,7 @@ class RNGoogleMapsPlusView( override var userInterfaceStyle: RNUserInterfaceStyle? = null set(value) { - view.userInterfaceStyle = userInterfaceStyleToMapColorScheme(value) + view.userInterfaceStyle = value.toMapColorScheme() } override var minZoomLevel: Double? = null @@ -87,17 +106,17 @@ class RNGoogleMapsPlusView( val nextById = value?.associateBy { it.id } ?: emptyMap() (prevById.keys - nextById.keys).forEach { id -> - markerOptions.cancelIconJob(id) + markerBuilder.cancelIconJob(id) view.removeMarker(id) } nextById.forEach { (id, next) -> val prev = prevById[id] if (prev == null) { - markerOptions.buildIconAsync(id, next) { icon -> + markerBuilder.buildIconAsync(id, next) { icon -> view.addMarker( id, - markerOptions.build(next, icon), + markerBuilder.build(next, icon), ) } } else if (!prev.markerEquals(next)) { @@ -113,7 +132,7 @@ class RNGoogleMapsPlusView( } if (!prev.markerStyleEquals(next)) { - markerOptions.buildIconAsync(id, next) { icon -> + markerBuilder.buildIconAsync(id, next) { icon -> m.setIcon(icon) } } @@ -140,7 +159,7 @@ class RNGoogleMapsPlusView( nextById.forEach { (id, next) -> val prev = prevById[id] if (prev == null) { - view.addPolyline(id, polylineOptions.buildPolylineOptions(next)) + view.addPolyline(id, polylineBuilder.buildPolylineOptions(next)) } else if (!prev.polylineEquals(next)) { view.updatePolyline(id) { gms -> onUi { @@ -151,11 +170,11 @@ class RNGoogleMapsPlusView( } next.width?.let { gms.width = it.dpToPx() } next.lineCap?.let { - val cap = polylineOptions.mapLineCap(it) + val cap = polylineBuilder.mapLineCap(it) gms.startCap = cap gms.endCap = cap } - next.lineJoin?.let { gms.jointType = polylineOptions.mapLineJoin(it) } + next.lineJoin?.let { gms.jointType = polylineBuilder.mapLineJoin(it) } next.color?.let { gms.color = it.toColor() } next.zIndex?.let { gms.zIndex = it.toFloat() } } @@ -177,7 +196,7 @@ class RNGoogleMapsPlusView( nextById.forEach { (id, next) -> val prev = prevById[id] if (prev == null) { - view.addPolygon(id, polygonOptions.buildPolygonOptions(next)) + view.addPolygon(id, polygonBuilder.buildPolygonOptions(next)) } else if (!prev.polygonEquals(next)) { view.updatePolygon(id) { gmsPoly -> onUi { @@ -209,7 +228,7 @@ class RNGoogleMapsPlusView( nextById.forEach { (id, next) -> val prev = prevById[id] if (prev == null) { - view.addCircle(id, circleOptions.buildCircleOptions(next)) + view.addCircle(id, circleBuilder.buildCircleOptions(next)) } else if (!prev.circleEquals(next)) { view.updateCircle(id) { gmsCircle -> onUi { @@ -226,6 +245,11 @@ class RNGoogleMapsPlusView( field = value } + override var locationConfig: RNLocationConfig? = null + set(value) { + view.locationConfig = value + } + override var onMapError: ((RNMapErrorCode) -> Unit)? = null set(cb) { view.onMapError = cb @@ -290,7 +314,7 @@ class RNGoogleMapsPlusView( animated: Boolean?, durationMS: Double?, ) { - view.setCamera(camera, animated == true, durationMS?.toInt() ?: 3000) + view.setCamera(camera.toCameraPosition(), animated == true, durationMS?.toInt() ?: 3000) } override fun setCameraToCoordinates( @@ -318,41 +342,6 @@ class RNGoogleMapsPlusView( override fun requestLocationPermission(): Promise = permissionHandler.requestLocationPermission() override fun isGooglePlayServicesAvailable(): Boolean = playServiceHandler.isPlayServicesAvailable() - - fun userInterfaceStyleToMapColorScheme(value: RNUserInterfaceStyle?): Int? { - value ?: return null - return when (value) { - RNUserInterfaceStyle.LIGHT -> { - MapColorScheme.LIGHT - } - - RNUserInterfaceStyle.DARK -> { - MapColorScheme.DARK - } - - RNUserInterfaceStyle.DEFAULT -> { - MapColorScheme.FOLLOW_SYSTEM - } - } - } - - fun mapCameraToCameraPosition(camera: RNCamera?): CameraPosition? { - camera ?: return null - val builder = CameraPosition.builder() - camera.center?.let { - builder.target( - com.google.android.gms.maps.model.LatLng( - camera.center.latitude, - camera.center.longitude, - ), - ) - } - camera.zoom?.let { builder.zoom(it.toFloat()) } - camera.bearing?.let { builder.bearing(it.toFloat()) } - camera.tilt?.let { builder.tilt(it.toFloat()) } - - return builder.build() - } } private inline fun onUi(crossinline block: () -> Unit) { diff --git a/android/src/main/java/com/rngooglemapsplus/extensions/RNCameraExtension.kt b/android/src/main/java/com/rngooglemapsplus/extensions/RNCameraExtension.kt new file mode 100644 index 0000000..ed29245 --- /dev/null +++ b/android/src/main/java/com/rngooglemapsplus/extensions/RNCameraExtension.kt @@ -0,0 +1,19 @@ +package com.rngooglemapsplus.extensions + +import com.google.android.gms.maps.model.CameraPosition +import com.google.android.gms.maps.model.LatLng +import com.rngooglemapsplus.RNCamera + +fun RNCamera.toCameraPosition(): CameraPosition { + val builder = CameraPosition.builder() + + center?.let { + builder.target(LatLng(it.latitude, it.longitude)) + } + + zoom?.let { builder.zoom(it.toFloat()) } + bearing?.let { builder.bearing(it.toFloat()) } + tilt?.let { builder.tilt(it.toFloat()) } + + return builder.build() +} diff --git a/android/src/main/java/com/rngooglemapsplus/extensions/RNLocationPriorityExtension.kt b/android/src/main/java/com/rngooglemapsplus/extensions/RNLocationPriorityExtension.kt new file mode 100644 index 0000000..d794703 --- /dev/null +++ b/android/src/main/java/com/rngooglemapsplus/extensions/RNLocationPriorityExtension.kt @@ -0,0 +1,12 @@ +package com.rngooglemapsplus.extensions + +import com.google.android.gms.location.Priority +import com.rngooglemapsplus.RNAndroidLocationPriority + +fun RNAndroidLocationPriority.toGooglePriority(): Int = + when (this) { + RNAndroidLocationPriority.PRIORITY_HIGH_ACCURACY -> Priority.PRIORITY_HIGH_ACCURACY + RNAndroidLocationPriority.PRIORITY_BALANCED_POWER_ACCURACY -> Priority.PRIORITY_BALANCED_POWER_ACCURACY + RNAndroidLocationPriority.PRIORITY_LOW_POWER -> Priority.PRIORITY_LOW_POWER + RNAndroidLocationPriority.PRIORITY_PASSIVE -> Priority.PRIORITY_PASSIVE + } diff --git a/android/src/main/java/com/rngooglemapsplus/extensions/RNMapCircleExtension.kt b/android/src/main/java/com/rngooglemapsplus/extensions/RNMapCircleExtension.kt new file mode 100644 index 0000000..8de84e4 --- /dev/null +++ b/android/src/main/java/com/rngooglemapsplus/extensions/RNMapCircleExtension.kt @@ -0,0 +1,14 @@ +package com.rngooglemapsplus.extensions + +import com.rngooglemapsplus.RNCircle + +fun RNCircle.circleEquals(b: RNCircle): Boolean { + if (zIndex != b.zIndex) return false + if (pressable != b.pressable) return false + if (center != b.center) return false + if (radius != b.radius) return false + if (strokeWidth != b.strokeWidth) return false + if (strokeColor != b.strokeColor) return false + if (fillColor != b.fillColor) return false + return true +} diff --git a/android/src/main/java/com/rngooglemapsplus/extensions/RNPolygonExtension.kt b/android/src/main/java/com/rngooglemapsplus/extensions/RNPolygonExtension.kt new file mode 100644 index 0000000..cc037e8 --- /dev/null +++ b/android/src/main/java/com/rngooglemapsplus/extensions/RNPolygonExtension.kt @@ -0,0 +1,20 @@ +package com.rngooglemapsplus.extensions + +import com.rngooglemapsplus.RNPolygon + +fun RNPolygon.polygonEquals(b: RNPolygon): Boolean { + if (zIndex != b.zIndex) return false + if (pressable != b.pressable) return false + if (strokeWidth != b.strokeWidth) return false + if (fillColor != b.fillColor) return false + if (strokeColor != b.strokeColor) return false + val ac = coordinates + val bc = b.coordinates + if (ac.size != bc.size) return false + for (i in ac.indices) { + val p = ac[i] + val q = bc[i] + if (p.latitude != q.latitude || p.longitude != q.longitude) return false + } + return true +} diff --git a/android/src/main/java/com/rngooglemapsplus/extensions/RNPolylineExtension.kt b/android/src/main/java/com/rngooglemapsplus/extensions/RNPolylineExtension.kt new file mode 100644 index 0000000..61b4ae7 --- /dev/null +++ b/android/src/main/java/com/rngooglemapsplus/extensions/RNPolylineExtension.kt @@ -0,0 +1,21 @@ +package com.rngooglemapsplus.extensions + +import com.rngooglemapsplus.RNPolyline + +fun RNPolyline.polylineEquals(b: RNPolyline): Boolean { + if (zIndex != b.zIndex) return false + if (pressable != b.pressable) return false + if ((width ?: 0.0) != (b.width ?: 0.0)) return false + if (lineCap != b.lineCap) return false + if (lineJoin != b.lineJoin) return false + if (color != b.color) return false + val ac = coordinates + val bc = b.coordinates + if (ac.size != bc.size) return false + for (i in ac.indices) { + val p = ac[i] + val q = bc[i] + if (p.latitude != q.latitude || p.longitude != q.longitude) return false + } + return true +} diff --git a/android/src/main/java/com/rngooglemapsplus/extensions/RNUserInterfaceExtension.kt b/android/src/main/java/com/rngooglemapsplus/extensions/RNUserInterfaceExtension.kt new file mode 100644 index 0000000..8fd4c5a --- /dev/null +++ b/android/src/main/java/com/rngooglemapsplus/extensions/RNUserInterfaceExtension.kt @@ -0,0 +1,12 @@ +package com.rngooglemapsplus.extensions + +import com.google.android.gms.maps.model.MapColorScheme +import com.rngooglemapsplus.RNUserInterfaceStyle + +fun RNUserInterfaceStyle?.toMapColorScheme(): Int? = + when (this) { + RNUserInterfaceStyle.LIGHT -> MapColorScheme.LIGHT + RNUserInterfaceStyle.DARK -> MapColorScheme.DARK + RNUserInterfaceStyle.DEFAULT -> MapColorScheme.FOLLOW_SYSTEM + null -> null + } diff --git a/android/src/main/java/com/rngooglemapsplus/Color.kt b/android/src/main/java/com/rngooglemapsplus/extensions/StringExtension.kt similarity index 97% rename from android/src/main/java/com/rngooglemapsplus/Color.kt rename to android/src/main/java/com/rngooglemapsplus/extensions/StringExtension.kt index dff40bf..f7ae761 100644 --- a/android/src/main/java/com/rngooglemapsplus/Color.kt +++ b/android/src/main/java/com/rngooglemapsplus/extensions/StringExtension.kt @@ -1,4 +1,4 @@ -package com.rngooglemapsplus +package com.rngooglemapsplus.extensions import android.graphics.Color import androidx.core.graphics.toColorInt diff --git a/android/src/main/java/com/rngooglemapsplus/extensions/ThrowableExtension.kt b/android/src/main/java/com/rngooglemapsplus/extensions/ThrowableExtension.kt new file mode 100644 index 0000000..3c29cc4 --- /dev/null +++ b/android/src/main/java/com/rngooglemapsplus/extensions/ThrowableExtension.kt @@ -0,0 +1,38 @@ +package com.rngooglemapsplus.extensions + +import android.content.Context +import com.google.android.gms.common.ConnectionResult +import com.google.android.gms.common.GoogleApiAvailability +import com.google.android.gms.common.api.ApiException +import com.google.android.gms.common.api.CommonStatusCodes +import com.google.android.gms.location.LocationSettingsStatusCodes +import com.rngooglemapsplus.RNLocationErrorCode + +fun Throwable.toLocationErrorCode(context: Context): RNLocationErrorCode { + return when (this) { + is SecurityException -> RNLocationErrorCode.PERMISSION_DENIED + + is ApiException -> + when (statusCode) { + CommonStatusCodes.NETWORK_ERROR -> + RNLocationErrorCode.POSITION_UNAVAILABLE + LocationSettingsStatusCodes.RESOLUTION_REQUIRED, + LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE, + -> + RNLocationErrorCode.SETTINGS_NOT_SATISFIED + else -> + RNLocationErrorCode.INTERNAL_ERROR + } + + else -> { + if (message?.contains("GoogleApi", ignoreCase = true) == true) { + val gms = GoogleApiAvailability.getInstance() + val status = gms.isGooglePlayServicesAvailable(context) + if (status != ConnectionResult.SUCCESS) { + return RNLocationErrorCode.PLAY_SERVICE_NOT_AVAILABLE + } + } + RNLocationErrorCode.INTERNAL_ERROR + } + } +} diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index b8e29d4..7fae52d 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -2356,7 +2356,7 @@ PODS: - React-perflogger (= 0.82.0-rc.4) - React-utils (= 0.82.0-rc.4) - SocketRocket - - RNGoogleMapsPlus (0.1.1): + - RNGoogleMapsPlus (1.0.3-dev.0): - boost - DoubleConversion - fast_float @@ -2708,7 +2708,7 @@ SPEC CHECKSUMS: ReactAppDependencyProvider: 1202b833d8cca6c917dabcf679837c34a9ca5723 ReactCodegen: 19febbd1fdc8928493972f8d5290f2952e14c9d2 ReactCommon: 2caf7281b37aa1ca389e18839dd594099efb1489 - RNGoogleMapsPlus: 8a1a26a15dcd3b87de67a86243f91030fc27e6dc + RNGoogleMapsPlus: f9d77f78190272ae61db89e5e424d735342fcddc SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 SVGKit: 1ad7513f8c74d9652f94ed64ddecda1a23864dea Yoga: 2fb906b2084fd388a52edae73c54c39c3f50e86c diff --git a/example/src/App.tsx b/example/src/App.tsx index 1018126..6ccf5a8 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -7,6 +7,7 @@ import { Text, } from 'react-native'; import { useEffect, useMemo, useRef, useState } from 'react'; +import { RNAndroidLocationPriority, RNIOSLocationAccuracy } from '../../src'; import type { RNCamera, RNMapStyleElement, @@ -374,7 +375,7 @@ export default function App() { initialProps={{ /// mapStyle not working with mapId /// mapId: '111', - initialCamera: { + camera: { center: { latitude: 37.7749, longitude: -122.4194, @@ -382,10 +383,25 @@ export default function App() { zoom: 15, }, }} + uiSettings={{ + allGesturesEnabled: true, + compassEnabled: true, + indoorLevelPickerEnabled: true, + mapToolbarEnabled: true, + myLocationButtonEnabled: true, + rotateEnabled: true, + scrollEnabled: true, + scrollDuringRotateOrZoomEnabled: true, + tiltEnabled: true, + zoomControlsEnabled: true, + zoomGesturesEnabled: true, + }} onMapReady={callback((ready) => console.log('Map is ready! ' + ready))} style={styles.map} + myLocationEnabled={true} buildingEnabled={true} trafficEnabled={true} + indoorEnabled={true} customMapStyle={JSON.stringify( normalStyle ? standardMapStyle : silverMapStyle )} @@ -399,6 +415,18 @@ export default function App() { bottom: 20, right: 20, }} + locationConfig={{ + android: { + priority: + RNAndroidLocationPriority.PRIORITY_BALANCED_POWER_ACCURACY, + interval: 5000, + minUpdateInterval: 5000, + }, + ios: { + desiredAccuracy: RNIOSLocationAccuracy.ACCURACY_BEST, + distanceFilterMeters: 10, + }, + }} onMapPress={{ f: function (coordinate: RNLatLng): void { console.log('Map pressed', coordinate); diff --git a/ios/.swiftlint.yml b/ios/.swiftlint.yml index 3343b1b..86952cf 100644 --- a/ios/.swiftlint.yml +++ b/ios/.swiftlint.yml @@ -1,6 +1,8 @@ disabled_rules: - file_length - type_body_length + - cyclomatic_complexity + - todo identifier_name: min_length: @@ -8,7 +10,7 @@ identifier_name: error: 1 max_length: warning: 40 - error: 50 + error: 100 line_length: warning: 150 diff --git a/ios/GoogleMapViewImpl.swift b/ios/GoogleMapViewImpl.swift index 2c76731..1cc79fa 100644 --- a/ios/GoogleMapViewImpl.swift +++ b/ios/GoogleMapViewImpl.swift @@ -5,7 +5,7 @@ import UIKit final class GoogleMapsViewImpl: UIView, GMSMapViewDelegate { private let locationHandler: LocationHandler - private let markerOptions: MapMarkerOptions + private let markerBuilder: MapMarkerBuilder private var mapView: GMSMapView? private var initialized = false private var mapReady = false @@ -24,26 +24,13 @@ final class GoogleMapsViewImpl: UIView, GMSMapViewDelegate { private var lastSubmittedCameraPosition: GMSCameraPosition? private var lastSubmittedLocation: CLLocation? - var onMapError: ((RNMapErrorCode) -> Void)? - var onMapReady: ((Bool) -> Void)? - var onLocationUpdate: ((RNLocation) -> Void)? - var onLocationError: ((_ error: RNLocationErrorCode) -> Void)? - var onMapPress: ((RNLatLng) -> Void)? - var onMarkerPress: ((String) -> Void)? - var onPolylinePress: ((String) -> Void)? - var onPolygonPress: ((String) -> Void)? - var onCirclePress: ((String) -> Void)? - var onCameraChangeStart: ((RNRegion, RNCamera, Bool) -> Void)? - var onCameraChange: ((RNRegion, RNCamera, Bool) -> Void)? - var onCameraChangeComplete: ((RNRegion, RNCamera, Bool) -> Void)? - init( frame: CGRect = .zero, locationHandler: LocationHandler, - markerOptions: MapMarkerOptions + markerBuilder: MapMarkerBuilder ) { self.locationHandler = locationHandler - self.markerOptions = markerOptions + self.markerBuilder = markerBuilder super.init(frame: frame) setupAppLifecycleObservers() } @@ -132,12 +119,46 @@ final class GoogleMapsViewImpl: UIView, GMSMapViewDelegate { ) } - if let style = customMapStyle { - mapView?.mapStyle = style + if let uiSettings = uiSettings { + if let allGesturesEnabled = uiSettings.allGesturesEnabled { + mapView?.settings.setAllGesturesEnabled(allGesturesEnabled) + } + if let compassEnabled = uiSettings.compassEnabled { + mapView?.settings.compassButton = compassEnabled + } + if let indoorLevelPickerEnabled = uiSettings.indoorLevelPickerEnabled { + mapView?.settings.indoorPicker = indoorLevelPickerEnabled + } + if let mapToolbarEnabled = uiSettings.mapToolbarEnabled { + /// not supported + } + if let myLocationButtonEnabled = uiSettings.myLocationButtonEnabled { + mapView?.settings.myLocationButton = myLocationButtonEnabled + } + if let rotateEnabled = uiSettings.rotateEnabled { + mapView?.settings.rotateGestures = rotateEnabled + } + if let scrollEnabled = uiSettings.scrollEnabled { + mapView?.settings.scrollGestures = scrollEnabled + } + if let scrollDuringRotateOrZoomEnabled = uiSettings + .scrollDuringRotateOrZoomEnabled { + mapView?.settings.allowScrollGesturesDuringRotateOrZoom = + scrollDuringRotateOrZoomEnabled + } + if let tiltEnabled = uiSettings.tiltEnabled { + mapView?.settings.tiltGestures = tiltEnabled + } + if let zoomControlsEnabled = uiSettings.zoomControlsEnabled { + /// not supported + } + if let zoomGesturesEnabled = uiSettings.zoomGesturesEnabled { + mapView?.settings.zoomGestures = zoomGesturesEnabled + } } - if let mapType = mapType { - mapView?.mapType = mapType + if let myLocation = myLocationEnabled { + mapView?.isMyLocationEnabled = myLocation } if let buildings = buildingEnabled { @@ -148,6 +169,18 @@ final class GoogleMapsViewImpl: UIView, GMSMapViewDelegate { mapView?.isTrafficEnabled = traffic } + if let indoor = indoorEnabled { + mapView?.isIndoorEnabled = indoor + } + + if let style = customMapStyle { + mapView?.mapStyle = style + } + + if let mapType = mapType { + mapView?.mapType = mapType + } + if let uiStyle = userInterfaceStyle { mapView?.overrideUserInterfaceStyle = uiStyle } @@ -156,6 +189,13 @@ final class GoogleMapsViewImpl: UIView, GMSMapViewDelegate { mapView?.setMinZoom(Float(minZoom), maxZoom: Float(maxZoom)) } + if let locationConfig = locationConfig { + locationHandler.desiredAccuracy = + locationConfig.ios?.desiredAccuracy?.toCLLocationAccuracy + locationHandler.distanceFilterMeters = + locationConfig.ios?.distanceFilterMeters + } + if !pendingMarkers.isEmpty { pendingMarkers.forEach { addMarkerInternal(id: $0.id, marker: $0.marker) @@ -186,11 +226,80 @@ final class GoogleMapsViewImpl: UIView, GMSMapViewDelegate { mapView?.camera } + @MainActor + var uiSettings: RNMapUiSettings? { + didSet { + guard let mapView = mapView else { return } + let settings = mapView.settings + + if let v = uiSettings { + if let allGesturesEnabled = v.allGesturesEnabled { + settings.setAllGesturesEnabled(allGesturesEnabled) + } + if let compassEnabled = v.compassEnabled { + settings.compassButton = compassEnabled + } + if let indoorLevelPickerEnabled = v.indoorLevelPickerEnabled { + settings.indoorPicker = indoorLevelPickerEnabled + } + if let mapToolbarEnabled = v.mapToolbarEnabled { + /// not supported + } + if let myLocationButtonEnabled = v.myLocationButtonEnabled { + settings.myLocationButton = myLocationButtonEnabled + } + if let rotateEnabled = v.rotateEnabled { + settings.rotateGestures = rotateEnabled + } + if let scrollEnabled = v.scrollEnabled { + settings.scrollGestures = scrollEnabled + } + if let scrollDuringRotateOrZoomEnabled = v + .scrollDuringRotateOrZoomEnabled { + settings.allowScrollGesturesDuringRotateOrZoom = + scrollDuringRotateOrZoomEnabled + } + if let tiltEnabled = v.tiltEnabled { + settings.tiltGestures = tiltEnabled + } + if let zoomControlsEnabled = v.zoomControlsEnabled { + /// not supported + } + if let zoomGesturesEnabled = v.zoomGesturesEnabled { + settings.zoomGestures = zoomGesturesEnabled + } + } else { + settings.setAllGesturesEnabled(true) + settings.compassButton = false + settings.indoorPicker = false + settings.myLocationButton = false + settings.rotateGestures = true + settings.scrollGestures = true + settings.allowScrollGesturesDuringRotateOrZoom = true + settings.tiltGestures = true + settings.zoomGestures = false + } + } + } + + @MainActor + var myLocationEnabled: Bool? { + didSet { + if let value = myLocationEnabled { + mapView?.isMyLocationEnabled = value + } else { + mapView?.isMyLocationEnabled = false + } + } + } + @MainActor var buildingEnabled: Bool? { didSet { if let value = buildingEnabled { mapView?.isBuildingsEnabled = value + } else { + mapView?.isBuildingsEnabled = false } } } @@ -200,6 +309,19 @@ final class GoogleMapsViewImpl: UIView, GMSMapViewDelegate { didSet { if let value = trafficEnabled { mapView?.isTrafficEnabled = value + } else { + mapView?.isTrafficEnabled = false + } + } + } + + @MainActor + var indoorEnabled: Bool? { + didSet { + if let value = indoorEnabled { + mapView?.isIndoorEnabled = value + } else { + mapView?.isIndoorEnabled = false } } } @@ -243,7 +365,6 @@ final class GoogleMapsViewImpl: UIView, GMSMapViewDelegate { @MainActor var mapPadding: RNMapPadding? { didSet { - mapPadding if let padding = mapPadding { mapView?.padding = UIEdgeInsets( top: padding.top, @@ -263,6 +384,28 @@ final class GoogleMapsViewImpl: UIView, GMSMapViewDelegate { } } + @MainActor var locationConfig: RNLocationConfig? { + didSet { + locationHandler.desiredAccuracy = + locationConfig?.ios?.desiredAccuracy?.toCLLocationAccuracy + locationHandler.distanceFilterMeters = + locationConfig?.ios?.distanceFilterMeters + } + } + + var onMapError: ((RNMapErrorCode) -> Void)? + var onMapReady: ((Bool) -> Void)? + var onLocationUpdate: ((RNLocation) -> Void)? + var onLocationError: ((_ error: RNLocationErrorCode) -> Void)? + var onMapPress: ((RNLatLng) -> Void)? + var onMarkerPress: ((String) -> Void)? + var onPolylinePress: ((String) -> Void)? + var onPolygonPress: ((String) -> Void)? + var onCirclePress: ((String) -> Void)? + var onCameraChangeStart: ((RNRegion, RNCamera, Bool) -> Void)? + var onCameraChange: ((RNRegion, RNCamera, Bool) -> Void)? + var onCameraChangeComplete: ((RNRegion, RNCamera, Bool) -> Void)? + func setCamera(camera: GMSCameraPosition, animated: Bool, durationMS: Double) { if animated { withCATransaction( @@ -467,7 +610,7 @@ final class GoogleMapsViewImpl: UIView, GMSMapViewDelegate { } func deinitInternal() { - markerOptions.cancelAllIconTasks() + markerBuilder.cancelAllIconTasks() clearMarkers() clearPolylines() clearPolygons() diff --git a/ios/LocationHandler.swift b/ios/LocationHandler.swift index 9cca06a..176cf5d 100644 --- a/ios/LocationHandler.swift +++ b/ios/LocationHandler.swift @@ -2,43 +2,43 @@ import CoreLocation import Foundation import UIKit +private let kCLLocationAccuracyDefault: CLLocationAccuracy = + kCLLocationAccuracyBest +private let kCLDistanceFilterNoneDefault: CLLocationDistance = + kCLDistanceFilterNone + final class LocationHandler: NSObject, CLLocationManagerDelegate { private let manager = CLLocationManager() - private var priority: Int = Priority.highAccuracy.rawValue - private var interval: TimeInterval = 5.0 - private var minUpdateInterval: TimeInterval = 5.0 - private var distanceFilterMeters: CLLocationDistance = kCLDistanceFilterNone + + var desiredAccuracy: CLLocationAccuracy? = kCLLocationAccuracyDefault { + didSet { + if let desiredAccuracy = desiredAccuracy { + manager.desiredAccuracy = desiredAccuracy + } else { + manager.desiredAccuracy = kCLLocationAccuracyBest + } + } + } + + var distanceFilterMeters: CLLocationDistance? = kCLDistanceFilterNoneDefault { + didSet { + if let distanceFilterMeters = distanceFilterMeters { + manager.distanceFilter = distanceFilterMeters + } else { + manager.distanceFilter = kCLDistanceFilterNone + } + } + } var onUpdate: ((CLLocation) -> Void)? var onError: ((_ error: RNLocationErrorCode) -> Void)? - private var lastEmit: Date? - override init() { super.init() manager.delegate = self manager.pausesLocationUpdatesAutomatically = true manager.activityType = .other - applyPriority() - } - - func setPriority(_ priority: Int) { - self.priority = priority - applyPriority() - } - - func setInterval(_ seconds: Int) { - self.interval = max(0, TimeInterval(seconds)) - } - - func setFastestInterval(_ seconds: Int) { - self.minUpdateInterval = max(0, TimeInterval(seconds)) - } - - func setDistanceFilter(_ meters: Double) { - self.distanceFilterMeters = meters >= 0 ? meters : kCLDistanceFilterNone - manager.distanceFilter = distanceFilterMeters } func showLocationDialog() { @@ -110,36 +110,13 @@ final class LocationHandler: NSObject, CLLocationManagerDelegate { } } - private func applyPriority() { - guard let p = Priority(rawValue: priority) else { - manager.desiredAccuracy = kCLLocationAccuracyBest - return - } - switch p { - case .highAccuracy: - manager.desiredAccuracy = kCLLocationAccuracyBest - case .balanced: - manager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters - case .lowPower: - manager.desiredAccuracy = kCLLocationAccuracyHundredMeters - case .passive: - manager.desiredAccuracy = kCLLocationAccuracyKilometer - } - } - private func startUpdates() { - manager.distanceFilter = distanceFilterMeters + manager.desiredAccuracy = desiredAccuracy ?? kCLLocationAccuracyDefault + manager.distanceFilter = + distanceFilterMeters ?? kCLDistanceFilterNoneDefault manager.startUpdatingLocation() } - private func shouldEmit(now: Date) -> Bool { - if let last = lastEmit { - let delta = now.timeIntervalSince(last) - if delta < minUpdateInterval { return false } - } - return true - } - func locationManager( _ manager: CLLocationManager, didFailWithError error: Error @@ -166,12 +143,7 @@ final class LocationHandler: NSObject, CLLocationManagerDelegate { didUpdateLocations locations: [CLLocation] ) { guard let loc = locations.last else { return } - let now = Date() - - if shouldEmit(now: now) { - lastEmit = now - onUpdate?(loc) - } + onUpdate?(loc) } private static func topMostViewController() -> UIViewController? { @@ -191,15 +163,3 @@ final class LocationHandler: NSObject, CLLocationManagerDelegate { } } - -extension LocationHandler { - enum Priority: Int { - case highAccuracy = 100 - /// Android: PRIORITY_BALANCED_POWER_ACCURACY - case balanced = 102 - /// Android: PRIORITY_LOW_POWER - case lowPower = 104 - /// Android: PRIORITY_PASSIVE - case passive = 105 - } -} diff --git a/ios/MapCircleBuilder.swift b/ios/MapCircleBuilder.swift new file mode 100644 index 0000000..7dc9fe0 --- /dev/null +++ b/ios/MapCircleBuilder.swift @@ -0,0 +1,20 @@ +import GoogleMaps + +final class MapCircleBuilder { + + func buildCircle(_ c: RNCircle) -> GMSCircle { + let circle = GMSCircle() + circle.position = CLLocationCoordinate2D( + latitude: c.center.latitude, + longitude: c.center.longitude + ) + if let r = c.radius { circle.radius = r } + if let fc = c.fillColor?.toUIColor() { circle.fillColor = fc } + if let sc = c.strokeColor?.toUIColor() { circle.strokeColor = sc } + if let sw = c.strokeWidth { circle.strokeWidth = CGFloat(sw) } + if let pr = c.pressable { circle.isTappable = pr } + if let zi = c.zIndex { circle.zIndex = Int32(zi) } + + return circle + } +} diff --git a/ios/MapMarker.swift b/ios/MapMarkerBuilder.swift similarity index 86% rename from ios/MapMarker.swift rename to ios/MapMarkerBuilder.swift index 4d142e8..2c431a4 100644 --- a/ios/MapMarker.swift +++ b/ios/MapMarkerBuilder.swift @@ -2,7 +2,7 @@ import GoogleMaps import SVGKit import UIKit -final class MapMarkerOptions { +final class MapMarkerBuilder { private let iconCache = NSCache() private var tasks: [String: Task] = [:] private let queue = DispatchQueue( @@ -174,27 +174,3 @@ final class MapMarkerOptions { } } - -extension RNMarker { - func markerEquals(_ b: RNMarker) -> Bool { - id == b.id && zIndex == b.zIndex - && coordinate.latitude == b.coordinate.latitude - && coordinate.longitude == b.coordinate.longitude - && anchor?.x == b.anchor?.x && anchor?.y == b.anchor?.y - && markerStyleEquals(b) - } - - func markerStyleEquals(_ b: RNMarker) -> Bool { - width == b.width && height == b.height - && iconSvg == b.iconSvg - } - - func styleHash() -> NSString { - var hasher = Hasher() - hasher.combine(width) - hasher.combine(height) - hasher.combine(iconSvg) - return String(hasher.finalize()) as NSString - } - -} diff --git a/ios/MapPolygonBuilder.swift b/ios/MapPolygonBuilder.swift new file mode 100644 index 0000000..c68c6b2 --- /dev/null +++ b/ios/MapPolygonBuilder.swift @@ -0,0 +1,20 @@ +import GoogleMaps + +final class MapPolygonBuilder { + + func buildPolygon(_ p: RNPolygon) -> GMSPolygon { + let path = GMSMutablePath() + p.coordinates.forEach { + path.add( + CLLocationCoordinate2D(latitude: $0.latitude, longitude: $0.longitude) + ) + } + let pg = GMSPolygon(path: path) + if let fc = p.fillColor?.toUIColor() { pg.fillColor = fc } + if let sc = p.strokeColor?.toUIColor() { pg.strokeColor = sc } + if let sw = p.strokeWidth { pg.strokeWidth = CGFloat(sw) } + if let pr = p.pressable { pg.isTappable = pr } + if let zi = p.zIndex { pg.zIndex = Int32(zi) } + return pg + } +} diff --git a/ios/MapPolylineBuilder.swift b/ios/MapPolylineBuilder.swift new file mode 100644 index 0000000..bbb0b9e --- /dev/null +++ b/ios/MapPolylineBuilder.swift @@ -0,0 +1,24 @@ +import GoogleMaps + +final class MapPolylineBuilder { + func buildPolyline(_ p: RNPolyline) -> GMSPolyline { + let path = GMSMutablePath() + p.coordinates.forEach { + path.add( + CLLocationCoordinate2D(latitude: $0.latitude, longitude: $0.longitude) + ) + } + let pl = GMSPolyline(path: path) + if let w = p.width { pl.strokeWidth = CGFloat(w) } + if let c = p.color?.toUIColor() { pl.strokeColor = c } + if let cap = p.lineCap { + /// pl.lineCap = mapLineCap(cap) + } + if let join = p.lineJoin { + /// pl.strokeJoin = mapLineJoin(join) + } + if let pr = p.pressable { pl.isTappable = pr } + if let zi = p.zIndex { pl.zIndex = Int32(zi) } + return pl + } +} diff --git a/ios/RNGoogleMapsPlusView.swift b/ios/RNGoogleMapsPlusView.swift index e10a320..631be21 100644 --- a/ios/RNGoogleMapsPlusView.swift +++ b/ios/RNGoogleMapsPlusView.swift @@ -8,10 +8,10 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { private let permissionHandler: PermissionHandler private let locationHandler: LocationHandler - private let markerOptions = MapMarkerOptions() - private let polylineOptions = MapPolylineOptions() - private let polygonOptions = MapPolygonOptions() - private let circleOptions = MapCircleOptions() + private let markerBuilder = MapMarkerBuilder() + private let polylineBuilder = MapPolylineBuilder() + private let polygonBuilder = MapPolygonBuilder() + private let circleBuilder = MapCircleBuilder() private let impl: GoogleMapsViewImpl @@ -24,15 +24,15 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { self.locationHandler = LocationHandler() self.impl = GoogleMapsViewImpl( locationHandler: locationHandler, - markerOptions: markerOptions + markerBuilder: markerBuilder ) } /* - /// TODO: prepareForRecycle - override func prepareForRecycle() { - impl.clearAll() - } + /// TODO: prepareForRecycle + override func prepareForRecycle() { + impl.clearAll() + } */ @MainActor @@ -41,11 +41,21 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { impl.initMapView( mapId: initialProps?.mapId, liteMode: initialProps?.liteMode, - camera: mapCameraToGMSCamera(initialProps?.initialCamera) + camera: initialProps?.camera?.toGMSCameraPosition(current: nil) ) } } + @MainActor + var uiSettings: RNMapUiSettings? { + didSet { impl.uiSettings = uiSettings } + } + + @MainActor + var myLocationEnabled: Bool? { + didSet { impl.myLocationEnabled = myLocationEnabled } + } + @MainActor var buildingEnabled: Bool? { didSet { impl.buildingEnabled = buildingEnabled } @@ -56,6 +66,11 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { didSet { impl.trafficEnabled = trafficEnabled } } + @MainActor + var indoorEnabled: Bool? { + didSet { impl.indoorEnabled = indoorEnabled } + } + @MainActor var customMapStyle: String? { didSet { @@ -68,9 +83,7 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { @MainActor var userInterfaceStyle: RNUserInterfaceStyle? { didSet { - impl.userInterfaceStyle = mapUserInterfaceStyleToUIUserInterfaceStyle( - userInterfaceStyle - ) + impl.userInterfaceStyle = userInterfaceStyle?.toUIUserInterfaceStyle } } @@ -115,20 +128,20 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { removed.forEach { impl.removeMarker(id: $0) - markerOptions.cancelIconTask($0) + markerBuilder.cancelIconTask($0) } for (id, next) in nextById { if let prev = prevById[id] { if !prev.markerEquals(next) { impl.updateMarker(id: id) { m in - self.markerOptions.updateMarker(prev, next, m) + self.markerBuilder.updateMarker(prev, next, m) } } } else { - markerOptions.buildIconAsync(next.id, next) { icon in + markerBuilder.buildIconAsync(next.id, next) { icon in guard let icon else { return } - let marker = self.markerOptions.build(next, icon: icon) + let marker = self.markerBuilder.build(next, icon: icon) self.impl.addMarker(id: id, marker: marker) } } @@ -162,7 +175,7 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { } else { impl.addPolyline( id: id, - polyline: polylineOptions.buildPolyline(next) + polyline: polylineBuilder.buildPolyline(next) ) } } @@ -192,7 +205,7 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { } } } else { - impl.addPolygon(id: id, polygon: polygonOptions.buildPolygon(next)) + impl.addPolygon(id: id, polygon: polygonBuilder.buildPolygon(next)) } } } @@ -221,52 +234,15 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { } } } else { - impl.addCircle(id: id, circle: circleOptions.buildCircle(next)) + impl.addCircle(id: id, circle: circleBuilder.buildCircle(next)) } } } } - func setCamera(camera: RNCamera, animated: Bool?, durationMS: Double?) { - let current = impl.currentCamera - - let zoom = Float(camera.zoom ?? Double(current?.zoom ?? 0)) - let bearing = camera.bearing ?? current?.bearing ?? 0 - let viewingAngle = camera.bearing ?? current?.viewingAngle ?? 0 - - let target = CLLocationCoordinate2D( - latitude: camera.center?.latitude ?? current?.target.latitude ?? 0, - longitude: camera.center?.longitude ?? current?.target.longitude ?? 0 - ) - - let cam = GMSCameraPosition.camera( - withTarget: target, - zoom: zoom, - bearing: bearing, - viewingAngle: viewingAngle - ) - onMain { - self.impl.setCamera( - camera: cam, - animated: animated ?? true, - durationMS: durationMS ?? 3000 - ) - } - } - - func setCameraToCoordinates( - coordinates: [RNLatLng], - padding: RNMapPadding?, - animated: Bool?, - durationMS: Double? - ) { - onMain { - self.impl.setCameraToCoordinates( - coordinates: coordinates, - padding: padding ?? RNMapPadding(0, 0, 0, 0), - animated: animated ?? true, - durationMS: durationMS ?? 3000 - ) + @MainActor var locationConfig: RNLocationConfig? { + didSet { + impl.locationConfig = locationConfig } } @@ -307,6 +283,33 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { didSet { impl.onCameraChangeComplete = onCameraChangeComplete } } + func setCamera(camera: RNCamera, animated: Bool?, durationMS: Double?) { + let cam = camera.toGMSCameraPosition(current: impl.currentCamera) + onMain { + self.impl.setCamera( + camera: cam, + animated: animated ?? true, + durationMS: durationMS ?? 3000 + ) + } + } + + func setCameraToCoordinates( + coordinates: [RNLatLng], + padding: RNMapPadding?, + animated: Bool?, + durationMS: Double? + ) { + onMain { + self.impl.setCameraToCoordinates( + coordinates: coordinates, + padding: padding ?? RNMapPadding(0, 0, 0, 0), + animated: animated ?? true, + durationMS: durationMS ?? 3000 + ) + } + } + func showLocationDialog() { locationHandler.showLocationDialog() } @@ -324,61 +327,6 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { /// not supported return true } - - private func mapCameraToGMSCamera(_ c: RNCamera?) -> GMSCameraPosition? { - guard let c = c else { return nil } - - let current = impl.currentCamera - let center = CLLocationCoordinate2D( - latitude: c.center?.latitude ?? current?.target.latitude ?? 0, - longitude: c.center?.longitude ?? current?.target.longitude ?? 0 - ) - let z = Float(c.zoom ?? Double(current?.zoom ?? 0)) - let b = c.bearing ?? current?.bearing ?? 0 - let t = c.tilt ?? current?.viewingAngle ?? 0 - - return GMSCameraPosition.camera( - withTarget: center, - zoom: z, - bearing: b, - viewingAngle: t - ) - } - - func mapUserInterfaceStyleToUIUserInterfaceStyle( - _ style: RNUserInterfaceStyle? - ) -> UIUserInterfaceStyle? { - guard let style = style else { return nil } - - switch style { - case .light: - return .light - case .dark: - return .dark - case .default: - return .unspecified - } - } -} - -extension UIUserInterfaceStyle { - init?(fromString string: String) { - switch string.lowercased() { - case "light": self = .light - case "dark": self = .dark - case "default": self = .unspecified - default: return nil - } - } - - var stringValue: String { - switch self { - case .light: return "light" - case .dark: return "dark" - case .unspecified: return "default" - @unknown default: return "default" - } - } } @inline(__always) diff --git a/ios/extensions/RNCamera+Extension.swift b/ios/extensions/RNCamera+Extension.swift new file mode 100644 index 0000000..c1075ab --- /dev/null +++ b/ios/extensions/RNCamera+Extension.swift @@ -0,0 +1,22 @@ +import CoreLocation +import GoogleMaps + +extension RNCamera { + func toGMSCameraPosition(current: GMSCameraPosition?) -> GMSCameraPosition { + let center = CLLocationCoordinate2D( + latitude: center?.latitude ?? current?.target.latitude ?? 0, + longitude: center?.longitude ?? current?.target.longitude ?? 0 + ) + + let zoom = Float(zoom ?? Double(current?.zoom ?? 0)) + let bearing = bearing ?? current?.bearing ?? 0 + let tilt = tilt ?? current?.viewingAngle ?? 0 + + return GMSCameraPosition.camera( + withTarget: center, + zoom: zoom, + bearing: bearing, + viewingAngle: tilt + ) + } +} diff --git a/ios/MapCircle.swift b/ios/extensions/RNCircle+Extension.swift similarity index 59% rename from ios/MapCircle.swift rename to ios/extensions/RNCircle+Extension.swift index e565298..7364b39 100644 --- a/ios/MapCircle.swift +++ b/ios/extensions/RNCircle+Extension.swift @@ -1,24 +1,5 @@ import GoogleMaps -class MapCircleOptions { - - func buildCircle(_ c: RNCircle) -> GMSCircle { - let circle = GMSCircle() - circle.position = CLLocationCoordinate2D( - latitude: c.center.latitude, - longitude: c.center.longitude - ) - if let r = c.radius { circle.radius = r } - if let fc = c.fillColor?.toUIColor() { circle.fillColor = fc } - if let sc = c.strokeColor?.toUIColor() { circle.strokeColor = sc } - if let sw = c.strokeWidth { circle.strokeWidth = CGFloat(sw) } - if let pr = c.pressable { circle.isTappable = pr } - if let zi = c.zIndex { circle.zIndex = Int32(zi) } - - return circle - } -} - extension RNCircle { func updateCircle(_ next: RNCircle, _ c: GMSCircle) { c.position = CLLocationCoordinate2D( diff --git a/ios/extensions/RNIOSLocationAccuracy+Extensions.swift b/ios/extensions/RNIOSLocationAccuracy+Extensions.swift new file mode 100644 index 0000000..3c2112c --- /dev/null +++ b/ios/extensions/RNIOSLocationAccuracy+Extensions.swift @@ -0,0 +1,19 @@ +import CoreLocation + +extension RNIOSLocationAccuracy { + var toCLLocationAccuracy: CLLocationAccuracy { + switch self { + case .accuracyBest: + return kCLLocationAccuracyBest + + case .accuracyNearestTenMeter: + return kCLLocationAccuracyNearestTenMeters + + case .accuracyNearestHundredMeter: + return kCLLocationAccuracyHundredMeters + + case .accuracyKilometer: + return kCLLocationAccuracyKilometer + } + } +} diff --git a/ios/extensions/RNMarker+Extension.swift b/ios/extensions/RNMarker+Extension.swift new file mode 100644 index 0000000..a039853 --- /dev/null +++ b/ios/extensions/RNMarker+Extension.swift @@ -0,0 +1,24 @@ +import GoogleMaps + +extension RNMarker { + func markerEquals(_ b: RNMarker) -> Bool { + id == b.id && zIndex == b.zIndex + && coordinate.latitude == b.coordinate.latitude + && coordinate.longitude == b.coordinate.longitude + && anchor?.x == b.anchor?.x && anchor?.y == b.anchor?.y + && markerStyleEquals(b) + } + + func markerStyleEquals(_ b: RNMarker) -> Bool { + width == b.width && height == b.height + && iconSvg == b.iconSvg + } + + func styleHash() -> NSString { + var hasher = Hasher() + hasher.combine(width) + hasher.combine(height) + hasher.combine(iconSvg) + return String(hasher.finalize()) as NSString + } +} diff --git a/ios/MapPolygon.swift b/ios/extensions/RNPolygon+Extension.swift.swift similarity index 66% rename from ios/MapPolygon.swift rename to ios/extensions/RNPolygon+Extension.swift.swift index 5e5f66b..c274024 100644 --- a/ios/MapPolygon.swift +++ b/ios/extensions/RNPolygon+Extension.swift.swift @@ -1,24 +1,5 @@ import GoogleMaps -class MapPolygonOptions { - - func buildPolygon(_ p: RNPolygon) -> GMSPolygon { - let path = GMSMutablePath() - p.coordinates.forEach { - path.add( - CLLocationCoordinate2D(latitude: $0.latitude, longitude: $0.longitude) - ) - } - let pg = GMSPolygon(path: path) - if let fc = p.fillColor?.toUIColor() { pg.fillColor = fc } - if let sc = p.strokeColor?.toUIColor() { pg.strokeColor = sc } - if let sw = p.strokeWidth { pg.strokeWidth = CGFloat(sw) } - if let pr = p.pressable { pg.isTappable = pr } - if let zi = p.zIndex { pg.zIndex = Int32(zi) } - return pg - } -} - extension RNPolygon { func updatePolygon(_ next: RNPolygon, _ pg: GMSPolygon) { let path = GMSMutablePath() diff --git a/ios/MapPolyline.swift b/ios/extensions/RNPolyline+Extension.swift.swift similarity index 57% rename from ios/MapPolyline.swift rename to ios/extensions/RNPolyline+Extension.swift.swift index 91904fb..add3364 100644 --- a/ios/MapPolyline.swift +++ b/ios/extensions/RNPolyline+Extension.swift.swift @@ -1,44 +1,5 @@ import GoogleMaps -class MapPolylineOptions { - func buildPolyline(_ p: RNPolyline) -> GMSPolyline { - let path = GMSMutablePath() - p.coordinates.forEach { - path.add( - CLLocationCoordinate2D(latitude: $0.latitude, longitude: $0.longitude) - ) - } - let pl = GMSPolyline(path: path) - if let w = p.width { pl.strokeWidth = CGFloat(w) } - if let c = p.color?.toUIColor() { pl.strokeColor = c } - if let cap = p.lineCap { - /// pl.lineCap = mapLineCap(cap) - } - if let join = p.lineJoin { - /// pl.strokeJoin = mapLineJoin(join) - } - if let pr = p.pressable { pl.isTappable = pr } - if let zi = p.zIndex { pl.zIndex = Int32(zi) } - return pl - } -} - -func mapLineCap(_ t: RNLineCapType?) -> CGLineCap { - switch t { - case .round: return .round - case .square: return .square - default: return .butt - } -} - -func mapLineJoin(_ t: RNLineJoinType?) -> CGLineJoin { - switch t { - case .round: return .round - case .bevel: return .bevel - default: return .miter - } -} - extension RNPolyline { func updatePolyline(_ next: RNPolyline, _ pl: GMSPolyline) { let path = GMSMutablePath() @@ -82,4 +43,20 @@ extension RNPolyline { } return true } + + private func mapLineCap(_ t: RNLineCapType) -> CGLineCap { + switch t { + case .round: return .round + case .square: return .square + default: return .butt + } + } + + private func mapLineJoin(_ t: RNLineJoinType) -> CGLineJoin { + switch t { + case .round: return .round + case .bevel: return .bevel + default: return .miter + } + } } diff --git a/ios/extensions/RNUserInterface+Extension.swift b/ios/extensions/RNUserInterface+Extension.swift new file mode 100644 index 0000000..4ff0571 --- /dev/null +++ b/ios/extensions/RNUserInterface+Extension.swift @@ -0,0 +1,16 @@ +import UIKit + +extension RNUserInterfaceStyle { + var toUIUserInterfaceStyle: UIUserInterfaceStyle { + switch self { + case .light: + return .light + case .dark: + return .dark + case .default: + return .unspecified + @unknown default: + return .unspecified + } + } +} diff --git a/ios/Color.swift b/ios/extensions/String+Extensions.swift similarity index 100% rename from ios/Color.swift rename to ios/extensions/String+Extensions.swift diff --git a/src/RNGoogleMapsPlusView.nitro.ts b/src/RNGoogleMapsPlusView.nitro.ts index 56e0fee..5e19ac6 100644 --- a/src/RNGoogleMapsPlusView.nitro.ts +++ b/src/RNGoogleMapsPlusView.nitro.ts @@ -19,12 +19,17 @@ import type { RNMapType, RNInitialProps, RNCircle, + RNMapUiSettings, + RNLocationConfig, } from './types'; export interface RNGoogleMapsPlusViewProps extends HybridViewProps { initialProps?: RNInitialProps; + uiSettings?: RNMapUiSettings; + myLocationEnabled?: boolean; buildingEnabled?: boolean; trafficEnabled?: boolean; + indoorEnabled?: boolean; customMapStyle?: string; userInterfaceStyle?: RNUserInterfaceStyle; minZoomLevel?: number; @@ -35,6 +40,7 @@ export interface RNGoogleMapsPlusViewProps extends HybridViewProps { polygons?: RNPolygon[]; polylines?: RNPolyline[]; circles?: RNCircle[]; + locationConfig?: RNLocationConfig; onMapError?: (error: RNMapErrorCode) => void; onMapReady?: (ready: boolean) => void; onLocationUpdate?: (location: RNLocation) => void; diff --git a/src/types.ts b/src/types.ts index c439815..59b6952 100644 --- a/src/types.ts +++ b/src/types.ts @@ -6,7 +6,21 @@ export type GoogleMapsViewRef = HybridView; export type RNInitialProps = { mapId?: string; liteMode?: boolean; - initialCamera?: RNCamera; + camera?: RNCamera; +}; + +export type RNMapUiSettings = { + allGesturesEnabled?: boolean; + compassEnabled?: boolean; + indoorLevelPickerEnabled?: boolean; + mapToolbarEnabled?: boolean; + myLocationButtonEnabled?: boolean; + rotateEnabled?: boolean; + scrollEnabled?: boolean; + scrollDuringRotateOrZoomEnabled?: boolean; + tiltEnabled?: boolean; + zoomControlsEnabled?: boolean; + zoomGesturesEnabled?: boolean; }; export type RNLatLng = { latitude: number; longitude: number }; @@ -154,6 +168,35 @@ export type RNCircle = { fillColor?: string; }; +export type RNLocationConfig = { + android?: RNAndroidLocationConfig; + ios?: RNIOSLocationConfig; +}; +export type RNAndroidLocationConfig = { + priority?: RNAndroidLocationPriority; + interval?: number; + minUpdateInterval?: number; +}; + +export enum RNAndroidLocationPriority { + PRIORITY_HIGH_ACCURACY = 0, + PRIORITY_BALANCED_POWER_ACCURACY = 1, + PRIORITY_LOW_POWER = 2, + PRIORITY_PASSIVE = 3, +} + +export type RNIOSLocationConfig = { + desiredAccuracy?: RNIOSLocationAccuracy; + distanceFilterMeters?: number; +}; + +export enum RNIOSLocationAccuracy { + ACCURACY_BEST = 0, + ACCURACY_NEAREST_TEN_METER = 1, + ACCURACY_NEAREST_HUNDRED_METER = 2, + ACCURACY_KILOMETER = 3, +} + export type RNLocationPermissionResult = { android?: RNAndroidLocationPermissionResult; ios?: RNIOSPermissionResult;