diff --git a/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt b/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt index 09d4a31..dbfa009 100644 --- a/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt +++ b/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt @@ -41,8 +41,6 @@ import com.google.android.gms.maps.model.TileOverlayOptions import com.google.maps.android.data.kml.KmlLayer import com.margelo.nitro.core.Promise import com.rngooglemapsplus.extensions.encode -import com.rngooglemapsplus.extensions.onUi -import com.rngooglemapsplus.extensions.onUiSync import com.rngooglemapsplus.extensions.toGooglePriority import com.rngooglemapsplus.extensions.toLatLng import com.rngooglemapsplus.extensions.toLocationErrorCode @@ -53,7 +51,6 @@ import com.rngooglemapsplus.extensions.toRnCamera import com.rngooglemapsplus.extensions.toRnLatLng import com.rngooglemapsplus.extensions.toRnLocation import com.rngooglemapsplus.extensions.toRnRegion -import com.rngooglemapsplus.extensions.withPaddingPixels import idTag import tagData import java.io.ByteArrayInputStream @@ -451,27 +448,25 @@ class GoogleMapsViewImpl( ) = onUi { if (coordinates.isEmpty()) return@onUi - val w = mapView?.width ?: 0 - val h = mapView?.height ?: 0 + val bounds = + LatLngBounds + .builder() + .apply { + coordinates.forEach { include(it.toLatLng()) } + }.build() - val builder = LatLngBounds.builder() - coordinates.forEach { coord -> builder.include(coord.toLatLng()) } + val previousMapPadding = mapPadding + mapPadding = padding - val baseBounds = builder.build() - val paddedBounds = baseBounds.withPaddingPixels(w, h, padding) - - val adjustedWidth = - (w - padding.left.dpToPx() - padding.right.dpToPx()).toInt().coerceAtLeast(0) - val adjustedHeight = - (h - padding.top.dpToPx() - padding.bottom.dpToPx()).toInt().coerceAtLeast(0) - - val update = CameraUpdateFactory.newLatLngBounds(paddedBounds, adjustedWidth, adjustedHeight, 0) + val update = CameraUpdateFactory.newLatLngBounds(bounds, 0) if (animated) { googleMap?.animateCamera(update, durationMs, null) } else { googleMap?.moveCamera(update) } + + mapPadding = previousMapPadding } fun setCameraBounds(bounds: LatLngBounds?) = @@ -752,7 +747,7 @@ class GoogleMapsViewImpl( kmlLayersById[id] = layer layer.addLayerToMap() } catch (_: Exception) { - // ignore + mapsLog("kml layer parse failed: id=$id") } } @@ -837,9 +832,15 @@ class GoogleMapsViewImpl( setOnMyLocationClickListener(null) setOnMyLocationButtonClickListener(null) setInfoWindowAdapter(null) + isTrafficEnabled = false + isIndoorEnabled = false + myLocationEnabled = false + setLocationSource(null) + setLatLngBoundsForCameraTarget(null) } googleMap = null mapView?.removeAllViews() + mapView = null super.removeAllViews() reactContext.unregisterComponentCallbacks(componentCallbacks) } @@ -968,5 +969,5 @@ class GoogleMapsViewImpl( override fun getInfoContents(marker: Marker): View? = null - override fun getInfoWindow(marker: Marker): View? = markerBuilder.buildInfoWindow(marker.tagData.iconSvg) + override fun getInfoWindow(marker: Marker): View? = markerBuilder.buildInfoWindow(marker.tagData) } diff --git a/android/src/main/java/com/rngooglemapsplus/LocationHandler.kt b/android/src/main/java/com/rngooglemapsplus/LocationHandler.kt index fd7d517..08ec1af 100644 --- a/android/src/main/java/com/rngooglemapsplus/LocationHandler.kt +++ b/android/src/main/java/com/rngooglemapsplus/LocationHandler.kt @@ -64,8 +64,8 @@ class LocationHandler( } fun showLocationDialog() { - UiThreadUtil.runOnUiThread { - val activity = context.currentActivity ?: run { return@runOnUiThread } + onUi { + val activity = context.currentActivity ?: run { return@onUi } val lr = if (Build.VERSION.SDK_INT >= 31) { diff --git a/android/src/main/java/com/rngooglemapsplus/MapCircleBuilder.kt b/android/src/main/java/com/rngooglemapsplus/MapCircleBuilder.kt index 6c06f49..d945372 100644 --- a/android/src/main/java/com/rngooglemapsplus/MapCircleBuilder.kt +++ b/android/src/main/java/com/rngooglemapsplus/MapCircleBuilder.kt @@ -5,7 +5,6 @@ import com.facebook.react.uimanager.PixelUtil.dpToPx import com.google.android.gms.maps.model.Circle import com.google.android.gms.maps.model.CircleOptions import com.rngooglemapsplus.extensions.centerEquals -import com.rngooglemapsplus.extensions.onUi import com.rngooglemapsplus.extensions.toColor import com.rngooglemapsplus.extensions.toLatLng diff --git a/android/src/main/java/com/rngooglemapsplus/MapHelper.kt b/android/src/main/java/com/rngooglemapsplus/MapHelper.kt index 98ff2e3..ee0646a 100644 --- a/android/src/main/java/com/rngooglemapsplus/MapHelper.kt +++ b/android/src/main/java/com/rngooglemapsplus/MapHelper.kt @@ -1,4 +1,4 @@ -package com.rngooglemapsplus.extensions +package com.rngooglemapsplus import com.facebook.react.bridge.UiThreadUtil import kotlinx.coroutines.CompletableDeferred @@ -20,3 +20,16 @@ inline fun onUiSync(crossinline block: () -> T): T { } return runBlocking { result.await() } } + +private const val MAPS_LOG_TAG = "react-native-google-maps-plus" + +fun mapsLog(msg: String) { + android.util.Log.w(MAPS_LOG_TAG, msg) +} + +fun mapsLog( + msg: String, + t: Throwable, +) { + android.util.Log.w(MAPS_LOG_TAG, msg, t) +} diff --git a/android/src/main/java/com/rngooglemapsplus/MapMarkerBuilder.kt b/android/src/main/java/com/rngooglemapsplus/MapMarkerBuilder.kt index 55ba7bf..20085e4 100644 --- a/android/src/main/java/com/rngooglemapsplus/MapMarkerBuilder.kt +++ b/android/src/main/java/com/rngooglemapsplus/MapMarkerBuilder.kt @@ -13,6 +13,7 @@ import android.widget.LinearLayout import androidx.core.graphics.createBitmap import com.caverock.androidsvg.SVG import com.caverock.androidsvg.SVGExternalFileResolver +import com.caverock.androidsvg.SVGParseException import com.facebook.react.uimanager.PixelUtil.dpToPx import com.facebook.react.uimanager.ThemedReactContext import com.google.android.gms.maps.model.BitmapDescriptor @@ -24,13 +25,13 @@ import com.rngooglemapsplus.extensions.coordinatesEquals import com.rngooglemapsplus.extensions.infoWindowAnchorEquals import com.rngooglemapsplus.extensions.markerInfoWindowStyleEquals import com.rngooglemapsplus.extensions.markerStyleEquals -import com.rngooglemapsplus.extensions.onUi import com.rngooglemapsplus.extensions.styleHash import com.rngooglemapsplus.extensions.toLatLng import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.ensureActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -38,7 +39,7 @@ import java.net.HttpURLConnection import java.net.URL import java.net.URLDecoder import java.util.concurrent.ConcurrentHashMap -import kotlin.coroutines.coroutineContext +import kotlin.coroutines.cancellation.CancellationException class MapMarkerBuilder( val context: ThemedReactContext, @@ -117,6 +118,8 @@ class MapMarkerBuilder( else -> null } + }.onFailure { + mapsLog("external svg resolve failed") }.getOrNull() } @@ -140,7 +143,7 @@ class MapMarkerBuilder( try { return Typeface.createFromAsset(assetManager, path) } catch (_: Throwable) { - // / ignore + mapsLog("font resolve failed: $path") } } @@ -264,32 +267,40 @@ class MapMarkerBuilder( scope.launch { try { ensureActive() - val bmp = renderBitmap(m) + val renderResult = renderBitmap(m.iconSvg, m.id) - if (bmp == null) { - withContext(Dispatchers.Main) { onReady(null) } + if (renderResult?.bitmap == null) { + withContext(Dispatchers.Main) { + ensureActive() + onReady(createFallbackDescriptor()) + } return@launch } + ensureActive() - val desc = BitmapDescriptorFactory.fromBitmap(bmp) + val desc = BitmapDescriptorFactory.fromBitmap(renderResult.bitmap) - iconCache.put(key, desc) - bmp.recycle() + if (!renderResult.isFallback) { + iconCache.put(key, desc) + } + renderResult.bitmap.recycle() withContext(Dispatchers.Main) { ensureActive() onReady(desc) } } catch (_: OutOfMemoryError) { + mapsLog("markerId=${m.id} buildIconAsync out of memory") clearIconCache() withContext(Dispatchers.Main) { ensureActive() - onReady(null) + onReady(createFallbackDescriptor()) } } catch (_: Throwable) { + mapsLog("markerId=${m.id} buildIconAsync failed") withContext(Dispatchers.Main) { ensureActive() - onReady(null) + onReady(createFallbackDescriptor()) } } finally { jobsById.remove(m.id) @@ -317,8 +328,22 @@ class MapMarkerBuilder( iconCache.evictAll() } - fun buildInfoWindow(iconSvg: RNMarkerSvg?): ImageView? { - val iconSvg = iconSvg ?: return null + fun buildInfoWindow(markerTag: MarkerTag): ImageView? { + val iconSvg = markerTag.iconSvg ?: return null + + val wPx = + markerTag.iconSvg.width + .dpToPx() + .toInt() + val hPx = + markerTag.iconSvg.height + .dpToPx() + .toInt() + + if (wPx <= 0 || hPx <= 0) { + mapsLog("markerId=${markerTag.id} invalid svg size") + return ImageView(context) + } val svgView = ImageView(context).apply { @@ -330,40 +355,73 @@ class MapMarkerBuilder( } try { - val svg = SVG.getFromString(iconSvg.svgString) - svg.setDocumentWidth(iconSvg.width.dpToPx()) - svg.setDocumentHeight(iconSvg.height.dpToPx()) + val svg = + SVG.getFromString(iconSvg.svgString).apply { + documentWidth = wPx.toFloat() + documentHeight = hPx.toFloat() + } val drawable = PictureDrawable(svg.renderToPicture()) svgView.setImageDrawable(drawable) - } catch (e: Exception) { - return null + } catch (_: Exception) { + mapsLog("markerId=${markerTag.id} infoWindow: svg render failed") + return ImageView(context) } return svgView } - private suspend fun renderBitmap(m: RNMarker): Bitmap? { - m.iconSvg ?: return null + private fun createFallbackBitmap(): Bitmap = + createBitmap(1, 1, Bitmap.Config.ARGB_8888).apply { + setHasAlpha(true) + } + + private fun createFallbackDescriptor(): BitmapDescriptor { + val bmp = createFallbackBitmap() + return BitmapDescriptorFactory.fromBitmap(bmp).also { + bmp.recycle() + } + } + + private data class RenderBitmapResult( + val bitmap: Bitmap, + val isFallback: Boolean, + ) + + private suspend fun renderBitmap( + iconSvg: RNMarkerSvg, + markerId: String, + ): RenderBitmapResult? { + val wPx = + iconSvg.width + .dpToPx() + .toInt() + val hPx = + iconSvg.height + .dpToPx() + .toInt() + + if (wPx <= 0 || hPx <= 0) { + mapsLog("markerId=$markerId invalid svg size") + return RenderBitmapResult(createFallbackBitmap(), true) + } var bmp: Bitmap? = null try { - coroutineContext.ensureActive() - val svg = SVG.getFromString(m.iconSvg.svgString) - - val wPx = - m.iconSvg.width - .dpToPx() - .toInt() - val hPx = - m.iconSvg.height - .dpToPx() - .toInt() - - coroutineContext.ensureActive() - svg.setDocumentWidth(wPx.toFloat()) - svg.setDocumentHeight(hPx.toFloat()) - - coroutineContext.ensureActive() + val svg = + try { + SVG.getFromString(iconSvg.svgString).apply { + documentWidth = wPx.toFloat() + documentHeight = hPx.toFloat() + } + } catch (_: SVGParseException) { + mapsLog("markerId=$markerId icon: svg parse failed") + return RenderBitmapResult(createFallbackBitmap(), true) + } catch (_: IllegalArgumentException) { + mapsLog("markerId=$markerId icon: svg invalid") + return RenderBitmapResult(createFallbackBitmap(), true) + } + + currentCoroutineContext().ensureActive() bmp = createBitmap(wPx, hPx, Bitmap.Config.ARGB_8888).apply { density = context.resources.displayMetrics.densityDpi @@ -372,13 +430,13 @@ class MapMarkerBuilder( } } - return bmp - } catch (t: Throwable) { - try { - bmp?.recycle() - } catch (_: Throwable) { - } - throw t + currentCoroutineContext().ensureActive() + + return RenderBitmapResult(bmp, false) + } catch (e: Exception) { + if (e is CancellationException) throw e + bmp?.recycle() + throw e } } } diff --git a/android/src/main/java/com/rngooglemapsplus/MapPolygonBuilder.kt b/android/src/main/java/com/rngooglemapsplus/MapPolygonBuilder.kt index c3f0f87..70bf82d 100644 --- a/android/src/main/java/com/rngooglemapsplus/MapPolygonBuilder.kt +++ b/android/src/main/java/com/rngooglemapsplus/MapPolygonBuilder.kt @@ -6,7 +6,6 @@ import com.google.android.gms.maps.model.Polygon import com.google.android.gms.maps.model.PolygonOptions import com.rngooglemapsplus.extensions.coordinatesEquals import com.rngooglemapsplus.extensions.holesEquals -import com.rngooglemapsplus.extensions.onUi import com.rngooglemapsplus.extensions.toColor import com.rngooglemapsplus.extensions.toLatLng import com.rngooglemapsplus.extensions.toMapsPolygonHoles diff --git a/android/src/main/java/com/rngooglemapsplus/MapPolylineBuilder.kt.kt b/android/src/main/java/com/rngooglemapsplus/MapPolylineBuilder.kt.kt index abe3958..94a9762 100644 --- a/android/src/main/java/com/rngooglemapsplus/MapPolylineBuilder.kt.kt +++ b/android/src/main/java/com/rngooglemapsplus/MapPolylineBuilder.kt.kt @@ -5,7 +5,6 @@ import com.facebook.react.uimanager.PixelUtil.dpToPx import com.google.android.gms.maps.model.Polyline import com.google.android.gms.maps.model.PolylineOptions import com.rngooglemapsplus.extensions.coordinatesEquals -import com.rngooglemapsplus.extensions.onUi import com.rngooglemapsplus.extensions.toColor import com.rngooglemapsplus.extensions.toLatLng import com.rngooglemapsplus.extensions.toMapJointType diff --git a/android/src/main/java/com/rngooglemapsplus/MapUrlTileOverlayBuilder.kt b/android/src/main/java/com/rngooglemapsplus/MapUrlTileOverlayBuilder.kt index 36359fa..ebd6003 100644 --- a/android/src/main/java/com/rngooglemapsplus/MapUrlTileOverlayBuilder.kt +++ b/android/src/main/java/com/rngooglemapsplus/MapUrlTileOverlayBuilder.kt @@ -25,6 +25,7 @@ class MapUrlTileOverlayBuilder { return try { URL(url) } catch (e: Exception) { + mapsLog("tile url invalid: $url", e) null } } diff --git a/android/src/main/java/com/rngooglemapsplus/extensions/BitmapExtension.kt b/android/src/main/java/com/rngooglemapsplus/extensions/BitmapExtension.kt index 9a366bb..019f7ce 100644 --- a/android/src/main/java/com/rngooglemapsplus/extensions/BitmapExtension.kt +++ b/android/src/main/java/com/rngooglemapsplus/extensions/BitmapExtension.kt @@ -5,6 +5,7 @@ import android.graphics.Bitmap import android.util.Base64 import android.util.Size import androidx.core.graphics.scale +import com.rngooglemapsplus.mapsLog import java.io.ByteArrayOutputStream import java.io.File import java.io.FileOutputStream @@ -30,6 +31,7 @@ fun Bitmap.encode( } else { "data:image/$format;base64," + Base64.encodeToString(bytes, Base64.NO_WRAP) } - } catch (_: Exception) { + } catch (e: Exception) { + mapsLog("snapshot export failed", e) null } diff --git a/android/src/main/java/com/rngooglemapsplus/extensions/LatLngBoundsExtension.kt b/android/src/main/java/com/rngooglemapsplus/extensions/LatLngBoundsExtension.kt index 04a82e0..74adeb8 100644 --- a/android/src/main/java/com/rngooglemapsplus/extensions/LatLngBoundsExtension.kt +++ b/android/src/main/java/com/rngooglemapsplus/extensions/LatLngBoundsExtension.kt @@ -1,41 +1,10 @@ package com.rngooglemapsplus.extensions -import com.facebook.react.uimanager.PixelUtil.dpToPx -import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps.model.LatLngBounds import com.rngooglemapsplus.RNLatLngBounds -import com.rngooglemapsplus.RNMapPadding fun LatLngBounds.toRnLatLngBounds(): RNLatLngBounds = RNLatLngBounds( northeast = northeast.toRnLatLng(), southwest = southwest.toRnLatLng(), ) - -fun LatLngBounds.withPaddingPixels( - mapWidthPx: Int, - mapHeightPx: Int, - padding: RNMapPadding, -): LatLngBounds { - val latSpan = northeast.latitude - southwest.latitude - val lngSpan = northeast.longitude - southwest.longitude - if (latSpan == 0.0 && lngSpan == 0.0) return this - - val latPerPixel = if (mapHeightPx != 0) latSpan / mapHeightPx else 0.0 - val lngPerPixel = if (mapWidthPx != 0) lngSpan / mapWidthPx else 0.0 - - val builder = LatLngBounds.builder() - builder.include( - LatLng( - northeast.latitude + (padding.top.dpToPx() * latPerPixel), - northeast.longitude + (padding.right.dpToPx() * lngPerPixel), - ), - ) - builder.include( - LatLng( - southwest.latitude - (padding.bottom.dpToPx() * latPerPixel), - southwest.longitude - (padding.left.dpToPx() * lngPerPixel), - ), - ) - return builder.build() -} diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 856bd0b..3ead8f8 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -2504,7 +2504,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - RNGoogleMapsPlus (1.8.7): + - RNGoogleMapsPlus (1.10.0): - boost - DoubleConversion - fast_float @@ -3120,7 +3120,7 @@ SPEC CHECKSUMS: ReactCodegen: 0bce2d209e2e802589f4c5ff76d21618200e74cb ReactCommon: 801eff8cb9c940c04d3a89ce399c343ee3eff654 RNGestureHandler: 67501c6d447027581aa1d8e5a7a3ea5a7f0a89ff - RNGoogleMapsPlus: d64028210f2a3b74aa6a7f440ac773c3f1247c93 + RNGoogleMapsPlus: 9561d3fa02c06f374b38dd95c2321b27c71b55c6 RNReanimated: 05c5a85c3ee54ac68d60c8a9b42dbc441e3326ca RNScreens: 98771ad898d1c0528fc8139606bbacf5a2e9d237 RNWorklets: ab618bf7d1c7fd2cb793b9f0f39c3e29274b3ebf diff --git a/ios/GoogleMapViewImpl.swift b/ios/GoogleMapViewImpl.swift index 6f77f7a..9bfc6fb 100644 --- a/ios/GoogleMapViewImpl.swift +++ b/ios/GoogleMapViewImpl.swift @@ -20,7 +20,8 @@ GMSIndoorDisplayDelegate { private var pendingCircles: [(id: String, circle: GMSCircle)] = [] private var pendingHeatmaps: [(id: String, heatmap: GMUHeatmapTileLayer)] = [] private var pendingKmlLayers: [(id: String, kmlString: String)] = [] - private var pendingUrlTileOverlays: [(id: String, urlTileOverlay: GMSURLTileLayer)] = [] + private var pendingUrlTileOverlays: + [(id: String, urlTileOverlay: GMSURLTileLayer)] = [] private var markersById: [String: GMSMarker] = [:] private var polylinesById: [String: GMSPolyline] = [:] @@ -42,13 +43,10 @@ GMSIndoorDisplayDelegate { super.init(frame: frame) } - @MainActor private var lifecycleAttached = false - @MainActor private var lifecycleTasks = [Task]() - @MainActor private func attachLifecycleObservers() { if lifecycleAttached { return } lifecycleAttached = true @@ -83,7 +81,6 @@ GMSIndoorDisplayDelegate { ) } - @MainActor private func detachLifecycleObservers() { if !lifecycleAttached { return } lifecycleAttached = false @@ -95,34 +92,39 @@ GMSIndoorDisplayDelegate { fatalError("init(coder:) has not been implemented") } - @MainActor func initMapView() { - if mapViewInitialized { return } - mapViewInitialized = true - googleMapOptions.frame = bounds + onMain { + if self.mapViewInitialized { return } + self.mapViewInitialized = true + self.googleMapOptions.frame = self.bounds - mapView = GMSMapView.init(options: googleMapOptions) - mapView?.delegate = self - mapView?.autoresizingMask = [.flexibleWidth, .flexibleHeight] - mapView?.paddingAdjustmentBehavior = .never - mapView.map { addSubview($0) } - applyProps() - initLocationCallbacks() - onMapReady?(true) + self.mapView = GMSMapView.init(options: self.googleMapOptions) + self.mapView?.delegate = self + self.mapView?.autoresizingMask = [.flexibleWidth, .flexibleHeight] + self.mapView?.paddingAdjustmentBehavior = .never + self.mapView.map { self.addSubview($0) } + self.applyProps() + self.initLocationCallbacks() + self.onMapReady?(true) + } } - @MainActor private func initLocationCallbacks() { locationHandler.onUpdate = { [weak self] loc in - guard let self = self else { return } - self.onLocationUpdate?(loc.toRnLocation()) + onMain { [weak self] in + guard let self = self else { return } + self.onLocationUpdate?(loc.toRnLocation()) + } } + locationHandler.onError = { [weak self] error in - self?.onLocationError?(error) + onMain { [weak self] in + guard let self = self else { return } + self.onLocationError?(error) + } } } - @MainActor private func applyProps() { ({ self.uiSettings = self.uiSettings })() ({ self.mapPadding = self.mapPadding })() @@ -176,110 +178,124 @@ GMSIndoorDisplayDelegate { } } - @MainActor var currentCamera: GMSCameraPosition? { - mapView?.camera + return mapView?.camera } - @MainActor var googleMapOptions: GMSMapViewOptions = GMSMapViewOptions() - @MainActor var uiSettings: RNMapUiSettings? { didSet { - mapView?.settings.setAllGesturesEnabled( - uiSettings?.allGesturesEnabled ?? true - ) - mapView?.settings.compassButton = uiSettings?.compassEnabled ?? false - mapView?.settings.indoorPicker = - uiSettings?.indoorLevelPickerEnabled ?? false - mapView?.settings.myLocationButton = - uiSettings?.myLocationButtonEnabled ?? false - mapView?.settings.rotateGestures = uiSettings?.rotateEnabled ?? true - mapView?.settings.scrollGestures = uiSettings?.scrollEnabled ?? true - mapView?.settings.allowScrollGesturesDuringRotateOrZoom = - uiSettings?.scrollDuringRotateOrZoomEnabled ?? true - mapView?.settings.tiltGestures = uiSettings?.tiltEnabled ?? true - mapView?.settings.zoomGestures = uiSettings?.zoomGesturesEnabled ?? true + onMain { + self.mapView?.settings.setAllGesturesEnabled( + self.uiSettings?.allGesturesEnabled ?? true + ) + self.mapView?.settings.compassButton = + self.uiSettings?.compassEnabled ?? false + self.mapView?.settings.indoorPicker = + self.uiSettings?.indoorLevelPickerEnabled ?? false + self.mapView?.settings.myLocationButton = + self.uiSettings?.myLocationButtonEnabled ?? false + self.mapView?.settings.rotateGestures = + self.uiSettings?.rotateEnabled ?? true + self.mapView?.settings.scrollGestures = + self.uiSettings?.scrollEnabled ?? true + self.mapView?.settings.allowScrollGesturesDuringRotateOrZoom = + self.uiSettings?.scrollDuringRotateOrZoomEnabled ?? true + self.mapView?.settings.tiltGestures = + self.uiSettings?.tiltEnabled ?? true + self.mapView?.settings.zoomGestures = + self.uiSettings?.zoomGesturesEnabled ?? true + } } } - @MainActor var myLocationEnabled: Bool? { didSet { - mapView?.isMyLocationEnabled = myLocationEnabled ?? false + onMain { + self.mapView?.isMyLocationEnabled = self.myLocationEnabled ?? false + } } } - @MainActor var buildingEnabled: Bool? { didSet { - mapView?.isBuildingsEnabled = buildingEnabled ?? false + onMain { + self.mapView?.isBuildingsEnabled = self.buildingEnabled ?? false + } } } - @MainActor var trafficEnabled: Bool? { didSet { - mapView?.isTrafficEnabled = trafficEnabled ?? false + onMain { + self.mapView?.isTrafficEnabled = self.trafficEnabled ?? false + } } } - @MainActor var indoorEnabled: Bool? { didSet { - mapView?.isIndoorEnabled = indoorEnabled ?? false - mapView?.indoorDisplay.delegate = indoorEnabled == true ? self : nil + onMain { + self.mapView?.isIndoorEnabled = self.indoorEnabled ?? false + self.mapView?.indoorDisplay.delegate = + self.indoorEnabled == true ? self : nil + } } } - @MainActor var customMapStyle: GMSMapStyle? { didSet { - mapView?.mapStyle = customMapStyle + onMain { + self.mapView?.mapStyle = self.customMapStyle + } } } - @MainActor var userInterfaceStyle: UIUserInterfaceStyle? { didSet { - mapView?.overrideUserInterfaceStyle = userInterfaceStyle ?? .unspecified + onMain { + self.mapView?.overrideUserInterfaceStyle = + self.userInterfaceStyle ?? .unspecified + } } } - @MainActor var mapZoomConfig: RNMapZoomConfig? { didSet { - mapView?.setMinZoom( - Float(mapZoomConfig?.min ?? 2), - maxZoom: Float(mapZoomConfig?.max ?? 21) - ) + onMain { + self.mapView?.setMinZoom( + Float(self.mapZoomConfig?.min ?? 2), + maxZoom: Float(self.mapZoomConfig?.max ?? 21) + ) + } } } - @MainActor var mapPadding: RNMapPadding? { didSet { - mapView?.padding = - mapPadding.map { - UIEdgeInsets( - top: $0.top, - left: $0.left, - bottom: $0.bottom, - right: $0.right - ) - } ?? .zero + onMain { + self.mapView?.padding = + self.mapPadding.map { + UIEdgeInsets( + top: $0.top, + left: $0.left, + bottom: $0.bottom, + right: $0.right + ) + } ?? .zero + } } } - @MainActor var mapType: GMSMapViewType? { didSet { - mapView?.mapType = mapType ?? .normal + onMain { + self.mapView?.mapType = self.mapType ?? .normal + } } } - @MainActor var locationConfig: RNLocationConfig? { didSet { locationHandler.updateConfig( @@ -317,7 +333,6 @@ GMSIndoorDisplayDelegate { var onCameraChange: ((RNRegion, RNCamera, Bool) -> Void)? var onCameraChangeComplete: ((RNRegion, RNCamera, Bool) -> Void)? - @MainActor func showMarkerInfoWindow(id: String) { onMain { guard let marker = self.markersById[id] else { return } @@ -326,7 +341,6 @@ GMSIndoorDisplayDelegate { } } - @MainActor func hideMarkerInfoWindow(id: String) { onMain { guard let marker = self.markersById[id] else { return } @@ -336,81 +350,87 @@ GMSIndoorDisplayDelegate { } } - @MainActor func setCamera(camera: GMSCameraPosition, animated: Bool, durationMs: Double) { - if animated { - withCATransaction( - disableActions: false, - duration: durationMs / 1000.0 - ) { - self.mapView?.animate(to: camera) + onMain { + if animated { + withCATransaction( + disableActions: false, + duration: durationMs / 1000.0 + ) { + self.mapView?.animate(to: camera) + } + } else { + let update = GMSCameraUpdate.setCamera(camera) + self.mapView?.moveCamera(update) } - } else { - let update = GMSCameraUpdate.setCamera(camera) - mapView?.moveCamera(update) } } - @MainActor func setCameraToCoordinates( coordinates: [RNLatLng], padding: RNMapPadding, animated: Bool, durationMs: Double ) { - guard let firstCoordinates = coordinates.first else { - return - } - var bounds = GMSCoordinateBounds( - coordinate: firstCoordinates.toCLLocationCoordinate2D(), - coordinate: firstCoordinates.toCLLocationCoordinate2D() - ) + onMain { + guard let firstCoordinates = coordinates.first else { + return + } + var bounds = GMSCoordinateBounds( + coordinate: firstCoordinates.toCLLocationCoordinate2D(), + coordinate: firstCoordinates.toCLLocationCoordinate2D() + ) - for coord in coordinates.dropFirst() { - bounds = bounds.includingCoordinate(coord.toCLLocationCoordinate2D()) - } + for coordinate in coordinates.dropFirst() { + bounds = bounds.includingCoordinate( + coordinate.toCLLocationCoordinate2D() + ) + } - let insets = UIEdgeInsets( - top: padding.top, - left: padding.left, - bottom: padding.bottom, - right: padding.right - ) + let insets = UIEdgeInsets( + top: padding.top, + left: padding.left, + bottom: padding.bottom, + right: padding.right + ) - let update = GMSCameraUpdate.fit(bounds, with: insets) - if animated { - withCATransaction( - disableActions: false, - duration: durationMs / 1000.0 - ) { - self.mapView?.animate(with: update) + let update = GMSCameraUpdate.fit(bounds, with: insets) + + if animated { + withCATransaction( + disableActions: false, + duration: durationMs / 1000.0 + ) { + self.mapView?.animate(with: update) + } + } else { + self.mapView?.moveCamera(update) } - } else { - mapView?.moveCamera(update) } } - @MainActor func setCameraBounds(_ bounds: GMSCoordinateBounds?) { - mapView?.cameraTargetBounds = bounds + onMain { + self.mapView?.cameraTargetBounds = bounds + } } - @MainActor func animateToBounds( _ bounds: GMSCoordinateBounds, padding: Double, durationMs: Double, lockBounds: Bool ) { - if lockBounds { - mapView?.cameraTargetBounds = bounds - } + onMain { + if lockBounds { + self.mapView?.cameraTargetBounds = bounds + } - let update = GMSCameraUpdate.fit(bounds, withPadding: CGFloat(padding)) - mapView?.animate(with: update) + let update = GMSCameraUpdate.fit(bounds, withPadding: CGFloat(padding)) + self.mapView?.animate(with: update) + } } - @MainActor func snapshot( size: CGSize?, format: String, @@ -420,7 +440,7 @@ GMSIndoorDisplayDelegate { ) -> NitroModules.Promise { let promise = Promise() - onMainAsync { + onMain { guard let mapView = self.mapView else { promise.resolve(withResult: nil) return @@ -447,256 +467,289 @@ GMSIndoorDisplayDelegate { return promise } - @MainActor func addMarker(id: String, marker: GMSMarker) { - if mapView == nil { - pendingMarkers.append((id, marker)) - return + onMain { + if self.mapView == nil { + self.pendingMarkers.append((id, marker)) + return + } + self.markersById.removeValue(forKey: id).map { $0.map = nil } + self.addMarkerInternal(id: id, marker: marker) } - markersById.removeValue(forKey: id).map { $0.map = nil } - addMarkerInternal(id: id, marker: marker) } - @MainActor private func addMarkerInternal(id: String, marker: GMSMarker) { - marker.map = mapView - markersById[id] = marker + onMain { + marker.map = self.mapView + self.markersById[id] = marker + } } - @MainActor func updateMarker(id: String, block: @escaping (GMSMarker) -> Void) { - markersById[id].map { - block($0) - if let mapView, mapView.selectedMarker == $0 { - mapView.selectedMarker = nil - mapView.selectedMarker = $0 + onMain { + self.markersById[id].map { + block($0) + if let mapView = self.mapView, mapView.selectedMarker == $0 { + mapView.selectedMarker = nil + mapView.selectedMarker = $0 + } } } } - @MainActor func removeMarker(id: String) { - markersById.removeValue(forKey: id).map { - $0.icon = nil - $0.map = nil + onMain { + self.markersById.removeValue(forKey: id).map { + $0.icon = nil + $0.map = nil + } } } - @MainActor func clearMarkers() { - markersById.values.forEach { $0.map = nil } - markersById.removeAll() - pendingMarkers.removeAll() + onMain { + self.markersById.values.forEach { $0.map = nil } + self.markersById.removeAll() + self.pendingMarkers.removeAll() + } } - @MainActor func addPolyline(id: String, polyline: GMSPolyline) { - if mapView == nil { - pendingPolylines.append((id, polyline)) - return + onMain { + if self.mapView == nil { + self.pendingPolylines.append((id, polyline)) + return + } + self.polylinesById.removeValue(forKey: id).map { $0.map = nil } + self.addPolylineInternal(id: id, polyline: polyline) } - polylinesById.removeValue(forKey: id).map { $0.map = nil } - addPolylineInternal(id: id, polyline: polyline) } - @MainActor private func addPolylineInternal(id: String, polyline: GMSPolyline) { - polyline.tagData = PolylineTag(id: id) - polyline.map = mapView - polylinesById[id] = polyline + onMain { + polyline.tagData = PolylineTag(id: id) + polyline.map = self.mapView + self.polylinesById[id] = polyline + } } - @MainActor func updatePolyline(id: String, block: @escaping (GMSPolyline) -> Void) { - polylinesById[id].map { block($0) } + onMain { + self.polylinesById[id].map { block($0) } + } } - @MainActor func removePolyline(id: String) { - polylinesById.removeValue(forKey: id).map { $0.map = nil } + onMain { + self.polylinesById.removeValue(forKey: id).map { $0.map = nil } + } } - @MainActor func clearPolylines() { - polylinesById.values.forEach { $0.map = nil } - polylinesById.removeAll() - pendingPolylines.removeAll() + onMain { + self.polylinesById.values.forEach { $0.map = nil } + self.polylinesById.removeAll() + self.pendingPolylines.removeAll() + } } - @MainActor func addPolygon(id: String, polygon: GMSPolygon) { - if mapView == nil { - pendingPolygons.append((id, polygon)) - return + onMain { + if self.mapView == nil { + self.pendingPolygons.append((id, polygon)) + return + } + self.polygonsById.removeValue(forKey: id).map { $0.map = nil } + self.addPolygonInternal(id: id, polygon: polygon) } - polygonsById.removeValue(forKey: id).map { $0.map = nil } - addPolygonInternal(id: id, polygon: polygon) } - @MainActor private func addPolygonInternal(id: String, polygon: GMSPolygon) { - polygon.tagData = PolygonTag(id: id) - polygon.map = mapView - polygonsById[id] = polygon + onMain { + polygon.tagData = PolygonTag(id: id) + polygon.map = self.mapView + self.polygonsById[id] = polygon + } } - @MainActor func updatePolygon(id: String, block: @escaping (GMSPolygon) -> Void) { - polygonsById[id].map { block($0) } + onMain { + self.polygonsById[id].map { block($0) } + } } - @MainActor func removePolygon(id: String) { - polygonsById.removeValue(forKey: id).map { $0.map = nil } + onMain { + self.polygonsById.removeValue(forKey: id).map { $0.map = nil } + } } - @MainActor func clearPolygons() { - polygonsById.values.forEach { $0.map = nil } - polygonsById.removeAll() - pendingPolygons.removeAll() + onMain { + self.polygonsById.values.forEach { $0.map = nil } + self.polygonsById.removeAll() + self.pendingPolygons.removeAll() + } } - @MainActor func addCircle(id: String, circle: GMSCircle) { - if mapView == nil { - pendingCircles.append((id, circle)) - return + onMain { + if self.mapView == nil { + self.pendingCircles.append((id, circle)) + return + } + self.circlesById.removeValue(forKey: id).map { $0.map = nil } + self.addCircleInternal(id: id, circle: circle) } - circlesById.removeValue(forKey: id).map { $0.map = nil } - addCircleInternal(id: id, circle: circle) } - @MainActor private func addCircleInternal(id: String, circle: GMSCircle) { - circle.tagData = CircleTag(id: id) - circle.map = mapView - circlesById[id] = circle + onMain { + circle.tagData = CircleTag(id: id) + circle.map = self.mapView + self.circlesById[id] = circle + } } - @MainActor func updateCircle(id: String, block: @escaping (GMSCircle) -> Void) { - circlesById[id].map { block($0) } + onMain { + self.circlesById[id].map { block($0) } + } } - @MainActor func removeCircle(id: String) { - circlesById.removeValue(forKey: id).map { $0.map = nil } + onMain { + self.circlesById.removeValue(forKey: id).map { $0.map = nil } + } } - @MainActor func clearCircles() { - circlesById.values.forEach { $0.map = nil } - circlesById.removeAll() - pendingCircles.removeAll() + onMain { + self.circlesById.values.forEach { $0.map = nil } + self.circlesById.removeAll() + self.pendingCircles.removeAll() + } } - @MainActor func addHeatmap(id: String, heatmap: GMUHeatmapTileLayer) { - if mapView == nil { - pendingHeatmaps.append((id, heatmap)) - return + onMain { + if self.mapView == nil { + self.pendingHeatmaps.append((id, heatmap)) + return + } + self.heatmapsById.removeValue(forKey: id).map { $0.map = nil } + self.addHeatmapInternal(id: id, heatmap: heatmap) } - heatmapsById.removeValue(forKey: id).map { $0.map = nil } - addHeatmapInternal(id: id, heatmap: heatmap) } - @MainActor private func addHeatmapInternal(id: String, heatmap: GMUHeatmapTileLayer) { - heatmap.map = mapView - heatmapsById[id] = heatmap + onMain { + heatmap.map = self.mapView + self.heatmapsById[id] = heatmap + } } - @MainActor func removeHeatmap(id: String) { - heatmapsById.removeValue(forKey: id).map { - $0.clearTileCache() - $0.map = nil + onMain { + self.heatmapsById.removeValue(forKey: id).map { + $0.clearTileCache() + $0.map = nil + } } } - @MainActor func clearHeatmaps() { - heatmapsById.values.forEach { - $0.clearTileCache() - $0.map = nil + onMain { + self.heatmapsById.values.forEach { + $0.clearTileCache() + $0.map = nil + } + self.heatmapsById.removeAll() + self.pendingHeatmaps.removeAll() } - heatmapsById.removeAll() - pendingHeatmaps.removeAll() } - @MainActor func addKmlLayer(id: String, kmlString: String) { - if mapView == nil { - pendingKmlLayers.append((id, kmlString)) - return + onMain { + if self.mapView == nil { + self.pendingKmlLayers.append((id, kmlString)) + return + } + self.kmlLayerById.removeValue(forKey: id).map { $0.clear() } + self.addKmlLayerInternal(id: id, kmlString: kmlString) } - kmlLayerById.removeValue(forKey: id).map { $0.clear() } - addKmlLayerInternal(id: id, kmlString: kmlString) } - @MainActor private func addKmlLayerInternal(id: String, kmlString: String) { - guard let data = kmlString.data(using: .utf8) else { return } - let parser = GMUKMLParser(data: data) - parser.parse() - mapView.map { mapView in - let renderer = GMUGeometryRenderer( - map: mapView, - geometries: parser.placemarks - ) - renderer.render() - kmlLayerById[id] = renderer + onMain { + guard let data = kmlString.data(using: .utf8) else { return } + let parser = GMUKMLParser(data: data) + parser.parse() + + self.mapView.map { mapView in + let renderer = GMUGeometryRenderer( + map: mapView, + geometries: parser.placemarks + ) + renderer.render() + self.kmlLayerById[id] = renderer + } } } - @MainActor func removeKmlLayer(id: String) { - kmlLayerById.removeValue(forKey: id).map { $0.clear() } + onMain { + self.kmlLayerById.removeValue(forKey: id).map { $0.clear() } + } } - @MainActor func clearKmlLayers() { - kmlLayerById.values.forEach { $0.clear() } - kmlLayerById.removeAll() - pendingKmlLayers.removeAll() + onMain { + self.kmlLayerById.values.forEach { $0.clear() } + self.kmlLayerById.removeAll() + self.pendingKmlLayers.removeAll() + } } - @MainActor func addUrlTileOverlay(id: String, urlTileOverlay: GMSURLTileLayer) { - if mapView == nil { - pendingUrlTileOverlays.append((id, urlTileOverlay)) - return + onMain { + if self.mapView == nil { + self.pendingUrlTileOverlays.append((id, urlTileOverlay)) + return + } + self.urlTileOverlays.removeValue(forKey: id).map { $0.map = nil } + self.addUrlTileOverlayInternal(id: id, urlTileOverlay: urlTileOverlay) } - urlTileOverlays.removeValue(forKey: id).map { $0.map = nil } - addUrlTileOverlayInternal(id: id, urlTileOverlay: urlTileOverlay) } - @MainActor private func addUrlTileOverlayInternal( id: String, urlTileOverlay: GMSURLTileLayer ) { - urlTileOverlay.map = mapView - urlTileOverlays[id] = urlTileOverlay + onMain { + urlTileOverlay.map = self.mapView + self.urlTileOverlays[id] = urlTileOverlay + } } - @MainActor func removeUrlTileOverlay(id: String) { - urlTileOverlays.removeValue(forKey: id).map { - $0.clearTileCache() - $0.map = nil + onMain { + self.urlTileOverlays.removeValue(forKey: id).map { + $0.clearTileCache() + $0.map = nil + } } } - @MainActor func clearUrlTileOverlay() { - urlTileOverlays.values.forEach { - $0.clearTileCache() - $0.map = nil + onMain { + self.urlTileOverlays.values.forEach { + $0.clearTileCache() + $0.map = nil + } + self.urlTileOverlays.removeAll() + self.pendingUrlTileOverlays.removeAll() } - urlTileOverlays.removeAll() - pendingUrlTileOverlays.removeAll() } func deinitInternal() { @@ -714,6 +767,11 @@ GMSIndoorDisplayDelegate { self.clearKmlLayers() self.clearUrlTileOverlay() self.mapView?.clear() + self.mapView?.isTrafficEnabled = false + self.mapView?.isIndoorEnabled = false + self.mapView?.isMyLocationEnabled = false + self.mapView?.cameraTargetBounds = nil + self.mapView?.layer.removeAllAnimations() self.mapView?.indoorDisplay.delegate = nil self.mapView?.delegate = nil self.mapView = nil @@ -752,11 +810,14 @@ GMSIndoorDisplayDelegate { func mapViewDidFinishTileRendering(_ mapView: GMSMapView) { guard !mapViewLoaded else { return } - mapViewLoaded = true - let visibleRegion = mapView.projection.visibleRegion().toRNRegion() - let camera = mapView.camera.toRNCamera() + onMain { + self.mapViewLoaded = true - self.onMapLoaded?(visibleRegion, camera) + let visibleRegion = mapView.projection.visibleRegion().toRNRegion() + let camera = mapView.camera.toRNCamera() + + self.onMapLoaded?(visibleRegion, camera) + } } func mapView(_ mapView: GMSMapView, willMove gesture: Bool) { @@ -944,11 +1005,10 @@ GMSIndoorDisplayDelegate { } func mapView(_ mapView: GMSMapView, markerInfoWindow marker: GMSMarker) -> UIView? { - return markerBuilder.buildInfoWindow(iconSvg: marker.tagData.iconSvg) + return markerBuilder.buildInfoWindow(markerTag: marker.tagData) } - func mapView(_ mapView: GMSMapView, markerInfoContents marker: GMSMarker) - -> UIView? { + func mapView(_ mapView: GMSMapView, markerInfoContents marker: GMSMarker) -> UIView? { return nil } } diff --git a/ios/LocationHandler.swift b/ios/LocationHandler.swift index dca4a3e..e2bd0b6 100644 --- a/ios/LocationHandler.swift +++ b/ios/LocationHandler.swift @@ -45,9 +45,7 @@ final class LocationHandler: NSObject, CLLocationManagerDelegate { } func showLocationDialog() { - onMainAsync { [weak self] in - guard let self = self else { return } - + onMain { guard let vc = Self.topMostViewController() else { return } let title = Bundle.main.object(forInfoDictionaryKey: "LocationNotAvailableTitle") @@ -96,7 +94,7 @@ final class LocationHandler: NSObject, CLLocationManagerDelegate { } func openLocationSettings() { - onMainAsync { + onMain { let openSettings = { if #available(iOS 18.3, *) { guard diff --git a/ios/MapCircleBuilder.swift b/ios/MapCircleBuilder.swift index 4dbe5a8..1d5ec6e 100644 --- a/ios/MapCircleBuilder.swift +++ b/ios/MapCircleBuilder.swift @@ -1,7 +1,6 @@ import GoogleMaps final class MapCircleBuilder { - @MainActor func build(_ c: RNCircle) -> GMSCircle { let circle = GMSCircle() circle.position = c.center.toCLLocationCoordinate2D() @@ -15,34 +14,35 @@ final class MapCircleBuilder { return circle } - @MainActor func update(_ prev: RNCircle, _ next: RNCircle, _ c: GMSCircle) { - if !prev.centerEquals(next) { - c.position = next.center.toCLLocationCoordinate2D() - } - - if prev.radius != next.radius { - c.radius = next.radius - } - - if prev.fillColor != next.fillColor { - c.fillColor = next.fillColor?.toUIColor() ?? .clear - } - - if prev.strokeColor != next.strokeColor { - c.strokeColor = next.strokeColor?.toUIColor() ?? .black - } - - if prev.strokeWidth != next.strokeWidth { - c.strokeWidth = CGFloat(next.strokeWidth ?? 1.0) - } - - if prev.pressable != next.pressable { - c.isTappable = next.pressable ?? false - } - - if prev.zIndex != next.zIndex { - c.zIndex = Int32(next.zIndex ?? 0) + onMain { + if !prev.centerEquals(next) { + c.position = next.center.toCLLocationCoordinate2D() + } + + if prev.radius != next.radius { + c.radius = next.radius + } + + if prev.fillColor != next.fillColor { + c.fillColor = next.fillColor?.toUIColor() ?? .clear + } + + if prev.strokeColor != next.strokeColor { + c.strokeColor = next.strokeColor?.toUIColor() ?? .black + } + + if prev.strokeWidth != next.strokeWidth { + c.strokeWidth = CGFloat(next.strokeWidth ?? 1.0) + } + + if prev.pressable != next.pressable { + c.isTappable = next.pressable ?? false + } + + if prev.zIndex != next.zIndex { + c.zIndex = Int32(next.zIndex ?? 0) + } } } } diff --git a/ios/MapHeatmapBuilder.swift b/ios/MapHeatmapBuilder.swift index e5437da..a4207d0 100644 --- a/ios/MapHeatmapBuilder.swift +++ b/ios/MapHeatmapBuilder.swift @@ -4,7 +4,6 @@ import GoogleMapsUtils import UIKit final class MapHeatmapBuilder { - @MainActor func build(_ h: RNHeatmap) -> GMUHeatmapTileLayer { let heatmap = GMUHeatmapTileLayer() heatmap.weightedData = h.weightedData.toWeightedLatLngs() diff --git a/ios/MapHelper.swift b/ios/MapHelper.swift index 43c0573..5bd359f 100644 --- a/ios/MapHelper.swift +++ b/ios/MapHelper.swift @@ -1,12 +1,12 @@ import QuartzCore -@MainActor @inline(__always) +@inline(__always) func withCATransaction( disableActions: Bool = true, duration: CFTimeInterval? = nil, timingFunction: CAMediaTimingFunction? = nil, completion: (() -> Void)? = nil, - _ body: @escaping @MainActor () -> Void + _ body: @escaping () -> Void ) { onMain { CATransaction.begin() @@ -21,20 +21,29 @@ func withCATransaction( } } -@MainActor @inline(__always) -func onMain(_ block: @escaping @MainActor () -> Void) { +@inline(__always) +func onMain( + _ block: @escaping () -> Void +) { if Thread.isMainThread { block() } else { - Task { @MainActor in block() } + Task { @MainActor in + block() + } } } @inline(__always) -func onMainAsync( - _ block: @escaping @MainActor () async -> Void -) { - Task { @MainActor in - await block() - } +func mapsLog(_ message: String) { + NSLog("[react-native-google-maps-plus] %@", message) +} + +@inline(__always) +func mapsLog(_ message: String, _ error: Error) { + NSLog( + "[react-native-google-maps-plus] %@ | %@", + message, + String(describing: error) + ) } diff --git a/ios/MapMarkerBuilder.swift b/ios/MapMarkerBuilder.swift index a6e2a0a..9e6d9f0 100644 --- a/ios/MapMarkerBuilder.swift +++ b/ios/MapMarkerBuilder.swift @@ -10,7 +10,10 @@ final class MapMarkerBuilder { }() private var tasks: [String: Task] = [:] - @MainActor + init() { + warmupSVGKit() + } + func build(_ m: RNMarker, icon: UIImage?) -> GMSMarker { let marker = GMSMarker( position: m.coordinate.toCLLocationCoordinate2D() @@ -44,22 +47,37 @@ final class MapMarkerBuilder { return marker } - @MainActor func update(_ prev: RNMarker, _ next: RNMarker, _ m: GMSMarker) { - withCATransaction(disableActions: true) { + onMain { + withCATransaction(disableActions: true) { - var tracksViewChanges = false - var tracksInfoWindowChanges = false + var tracksViewChanges = false + var tracksInfoWindowChanges = false - if !prev.coordinateEquals(next) { - m.position = next.coordinate.toCLLocationCoordinate2D() - } - - if !prev.markerStyleEquals(next) { - self.buildIconAsync(next) { img in - tracksViewChanges = true - m.icon = img + if !prev.coordinateEquals(next) { + m.position = next.coordinate.toCLLocationCoordinate2D() + } + if !prev.markerStyleEquals(next) { + self.buildIconAsync(next) { img in + tracksViewChanges = true + m.icon = img + + if !prev.anchorEquals(next) { + m.groundAnchor = CGPoint( + x: next.anchor?.x ?? 0.5, + y: next.anchor?.y ?? 1 + ) + } + + if !prev.infoWindowAnchorEquals(next) { + m.infoWindowAnchor = CGPoint( + x: next.infoWindowAnchor?.x ?? 0.5, + y: next.infoWindowAnchor?.y ?? 0 + ) + } + } + } else { if !prev.anchorEquals(next) { m.groundAnchor = CGPoint( x: next.anchor?.x ?? 0.5, @@ -74,72 +92,54 @@ final class MapMarkerBuilder { ) } } - } else { - if !prev.anchorEquals(next) { - m.groundAnchor = CGPoint( - x: next.anchor?.x ?? 0.5, - y: next.anchor?.y ?? 1 - ) - } - if !prev.infoWindowAnchorEquals(next) { - m.infoWindowAnchor = CGPoint( - x: next.infoWindowAnchor?.x ?? 0.5, - y: next.infoWindowAnchor?.y ?? 0 - ) + if prev.title != next.title { + tracksInfoWindowChanges = true + m.title = next.title } - } - - if prev.title != next.title { - tracksInfoWindowChanges = true - m.title = next.title - } - if prev.snippet != next.snippet { - tracksInfoWindowChanges = true - m.snippet = next.snippet - } - - if prev.opacity != next.opacity { - let opacity = Float(next.opacity ?? 1) - m.opacity = opacity - m.iconView?.alpha = CGFloat(opacity) - } + if prev.snippet != next.snippet { + tracksInfoWindowChanges = true + m.snippet = next.snippet + } - if prev.flat != next.flat { - m.isFlat = next.flat ?? false - } + if prev.opacity != next.opacity { + let opacity = Float(next.opacity ?? 1) + m.opacity = opacity + m.iconView?.alpha = CGFloat(opacity) + } - if prev.draggable != next.draggable { - m.isDraggable = next.draggable ?? false - } + if prev.flat != next.flat { + m.isFlat = next.flat ?? false + } - if prev.rotation != next.rotation { - m.rotation = next.rotation ?? 0 - } + if prev.draggable != next.draggable { + m.isDraggable = next.draggable ?? false + } - if prev.zIndex != next.zIndex { - m.zIndex = Int32(next.zIndex ?? 0) - } + if prev.rotation != next.rotation { + m.rotation = next.rotation ?? 0 + } - if !prev.markerInfoWindowStyleEquals(next) { - m.tagData = MarkerTag( - id: next.id, - iconSvg: next.infoWindowIconSvg - ) - } + if prev.zIndex != next.zIndex { + m.zIndex = Int32(next.zIndex ?? 0) + } - if tracksViewChanges { - m.tracksViewChanges = tracksViewChanges - } - if tracksInfoWindowChanges { - m.tracksInfoWindowChanges = tracksInfoWindowChanges - } + if !prev.markerInfoWindowStyleEquals(next) { + m.tagData = MarkerTag( + id: next.id, + iconSvg: next.infoWindowIconSvg + ) + } - if tracksViewChanges || tracksInfoWindowChanges { - onMain { [weak m] in - guard let m = m else { return } + if tracksViewChanges { + m.tracksViewChanges = tracksViewChanges + } + if tracksInfoWindowChanges { + m.tracksInfoWindowChanges = tracksInfoWindowChanges + } + if tracksViewChanges || tracksInfoWindowChanges { if tracksViewChanges { m.tracksViewChanges = false } @@ -158,7 +158,7 @@ final class MapMarkerBuilder { ) { tasks[m.id]?.cancel() - if m.iconSvg == nil { + guard let iconSvg = m.iconSvg else { onReady(nil) return } @@ -177,27 +177,35 @@ final class MapMarkerBuilder { Task { @MainActor in self.tasks.removeValue(forKey: m.id) } } - let img = self.renderUIImage(m, scale) - guard let img, !Task.isCancelled else { return } + let renderResult = self.renderUIImage(iconSvg, m.id, scale) + guard !Task.isCancelled else { return } - self.iconCache.setObject(img, forKey: key) + guard let renderResult = renderResult else { + await MainActor.run { + guard !Task.isCancelled else { return } + onReady(self.createFallbackUIImage()) + } + return + } + + if !renderResult.isFallback { + self.iconCache.setObject(renderResult.image, forKey: key) + } await MainActor.run { guard !Task.isCancelled else { return } - onReady(img) + onReady(renderResult.image) } } tasks[m.id] = task } - @MainActor func cancelIconTask(_ id: String) { tasks[id]?.cancel() tasks.removeValue(forKey: id) } - @MainActor func cancelAllIconTasks() { let ids = Array(tasks.keys) for id in ids { @@ -208,28 +216,36 @@ final class MapMarkerBuilder { CATransaction.flush() } - @MainActor - func buildInfoWindow(iconSvg: RNMarkerSvg?) -> UIImageView? { - guard let iconSvg = iconSvg else { + func buildInfoWindow(markerTag: MarkerTag) -> UIImageView? { + guard let iconSvg = markerTag.iconSvg else { return nil } + let w = CGFloat(iconSvg.width) + let h = CGFloat(iconSvg.height) + + if w <= 0 || h <= 0 { + mapsLog("markerId=\(markerTag.id) icon: invalid svg size") + return createFallbackImageView() + } + guard let data = iconSvg.svgString.data(using: .utf8), let svgImg = SVGKImage(data: data) else { - return nil + mapsLog("markerId=\(markerTag.id) infoWindow: svg utf8 decode failed") + return createFallbackImageView() } - let size = CGSize( - width: max(1, CGFloat(iconSvg.width)), - height: max(1, CGFloat(iconSvg.height)) - ) + let size = CGSize(width: w, height: h) svgImg.size = size guard let finalImage = SVGKExporterUIImage.export(asUIImage: svgImg) else { + mapsLog( + "markerId=\(markerTag.id) infoWindow: svg export to UIImage failed" + ) svgImg.clear() - return nil + return createFallbackImageView() } svgImg.clear() @@ -241,20 +257,63 @@ final class MapMarkerBuilder { return imageView } - private func renderUIImage(_ m: RNMarker, _ scale: CGFloat) -> UIImage? { + private func warmupSVGKit() { + autoreleasepool { + let emptySvg = """ + + """ + guard let data = emptySvg.data(using: .utf8), + let svgImg = SVGKImage(data: data) + else { return } + svgImg.clear() + } + } + + private func createFallbackUIImage() -> UIImage { + let size = CGSize(width: 1, height: 1) + let renderer = UIGraphicsImageRenderer(size: size) + return renderer.image { _ in } + } + + private func createFallbackImageView() -> UIImageView { + let iv = UIImageView(image: createFallbackUIImage()) + iv.contentMode = .scaleAspectFit + iv.backgroundColor = .clear + return iv + } + + private func renderUIImage( + _ iconSvg: RNMarkerSvg, + _ markerId: String, + _ scale: CGFloat + ) -> ( + image: UIImage, isFallback: Bool + )? { + + let w = CGFloat(iconSvg.width) + let h = CGFloat(iconSvg.height) + + if w <= 0 || h <= 0 { + mapsLog("markerId=\(markerId) icon: invalid svg size") + return (createFallbackUIImage(), true) + } + guard - let iconSvg = m.iconSvg, let data = iconSvg.svgString.data(using: .utf8) - else { return nil } + else { + mapsLog("markerId=\(markerId) icon: svg utf8 decode failed") + return (createFallbackUIImage(), true) + } - let size = CGSize( - width: max(1, CGFloat(iconSvg.width)), - height: max(1, CGFloat(iconSvg.height)) - ) + let size = CGSize(width: w, height: h) - return autoreleasepool { () -> UIImage? in + return autoreleasepool { () -> (image: UIImage, isFallback: Bool)? in guard !Task.isCancelled else { return nil } - guard let svgImg = SVGKImage(data: data) else { return nil } + + guard let svgImg = SVGKImage(data: data) else { + mapsLog("markerId=\(markerId) icon: SVGKImage init failed") + return (createFallbackUIImage(), true) + } svgImg.size = size @@ -265,8 +324,14 @@ final class MapMarkerBuilder { let uiImage = SVGKExporterUIImage.export(asUIImage: svgImg) svgImg.clear() - return uiImage + + guard !Task.isCancelled else { return nil } + + if let uiImage = uiImage { + return (uiImage, false) + } else { + return (createFallbackUIImage(), true) + } } } - } diff --git a/ios/MapPolygonBuilder.swift b/ios/MapPolygonBuilder.swift index a5d6bb5..2dac71b 100644 --- a/ios/MapPolygonBuilder.swift +++ b/ios/MapPolygonBuilder.swift @@ -1,7 +1,6 @@ import GoogleMaps final class MapPolygonBuilder { - @MainActor func build(_ p: RNPolygon) -> GMSPolygon { let path = p.coordinates.toGMSPath() let pg = GMSPolygon(path: path) @@ -17,38 +16,39 @@ final class MapPolygonBuilder { return pg } - @MainActor func update(_ prev: RNPolygon, _ next: RNPolygon, _ pg: GMSPolygon) { - if !prev.coordinatesEquals(next) { - pg.path = next.coordinates.toGMSPath() - } - - if !prev.holesEquals(next) { - pg.holes = next.holes.toMapPolygonHoles() - } - - if prev.fillColor != next.fillColor { - pg.fillColor = next.fillColor?.toUIColor() ?? .clear - } - - if prev.strokeColor != next.strokeColor { - pg.strokeColor = next.strokeColor?.toUIColor() ?? .black - } - - if prev.strokeWidth != next.strokeWidth { - pg.strokeWidth = CGFloat(next.strokeWidth ?? 1.0) - } - - if prev.pressable != next.pressable { - pg.isTappable = next.pressable ?? false - } - - if prev.geodesic != next.geodesic { - pg.geodesic = next.geodesic ?? false - } - - if prev.zIndex != next.zIndex { - pg.zIndex = Int32(next.zIndex ?? 0) + onMain { + if !prev.coordinatesEquals(next) { + pg.path = next.coordinates.toGMSPath() + } + + if !prev.holesEquals(next) { + pg.holes = next.holes.toMapPolygonHoles() + } + + if prev.fillColor != next.fillColor { + pg.fillColor = next.fillColor?.toUIColor() ?? .clear + } + + if prev.strokeColor != next.strokeColor { + pg.strokeColor = next.strokeColor?.toUIColor() ?? .black + } + + if prev.strokeWidth != next.strokeWidth { + pg.strokeWidth = CGFloat(next.strokeWidth ?? 1.0) + } + + if prev.pressable != next.pressable { + pg.isTappable = next.pressable ?? false + } + + if prev.geodesic != next.geodesic { + pg.geodesic = next.geodesic ?? false + } + + if prev.zIndex != next.zIndex { + pg.zIndex = Int32(next.zIndex ?? 0) + } } } } diff --git a/ios/MapPolylineBuilder.swift b/ios/MapPolylineBuilder.swift index 45087a1..3d4efc5 100644 --- a/ios/MapPolylineBuilder.swift +++ b/ios/MapPolylineBuilder.swift @@ -1,7 +1,6 @@ import GoogleMaps final class MapPolylineBuilder { - @MainActor func build(_ p: RNPolyline) -> GMSPolyline { let path = GMSMutablePath() p.coordinates.forEach { @@ -23,30 +22,31 @@ final class MapPolylineBuilder { return pl } - @MainActor func update(_ prev: RNPolyline, _ next: RNPolyline, _ pl: GMSPolyline) { - if !prev.coordinatesEquals(next) { - pl.path = next.coordinates.toGMSPath() - } - - if prev.width != next.width { - pl.strokeWidth = CGFloat(next.width ?? 1.0) - } - - if prev.color != next.color { - pl.strokeColor = next.color?.toUIColor() ?? .black - } - - if prev.pressable != next.pressable { - pl.isTappable = next.pressable ?? false - } - - if prev.geodesic != next.geodesic { - pl.geodesic = next.geodesic ?? false - } - - if prev.zIndex != next.zIndex { - pl.zIndex = Int32(next.zIndex ?? 0) + onMain { + if !prev.coordinatesEquals(next) { + pl.path = next.coordinates.toGMSPath() + } + + if prev.width != next.width { + pl.strokeWidth = CGFloat(next.width ?? 1.0) + } + + if prev.color != next.color { + pl.strokeColor = next.color?.toUIColor() ?? .black + } + + if prev.pressable != next.pressable { + pl.isTappable = next.pressable ?? false + } + + if prev.geodesic != next.geodesic { + pl.geodesic = next.geodesic ?? false + } + + if prev.zIndex != next.zIndex { + pl.zIndex = Int32(next.zIndex ?? 0) + } } } } diff --git a/ios/MapUrlTileOverlayBuilder.swift b/ios/MapUrlTileOverlayBuilder.swift index 59f1869..eb03174 100644 --- a/ios/MapUrlTileOverlayBuilder.swift +++ b/ios/MapUrlTileOverlayBuilder.swift @@ -1,7 +1,6 @@ import GoogleMaps class MapUrlTileOverlayBuilder { - @MainActor func build(_ t: RNUrlTileOverlay) -> GMSURLTileLayer { let constructor: GMSTileURLConstructor = { (x: UInt, y: UInt, zoom: UInt) in diff --git a/ios/RNGoogleMapsPlusView.swift b/ios/RNGoogleMapsPlusView.swift index a3673ac..3d30763 100644 --- a/ios/RNGoogleMapsPlusView.swift +++ b/ios/RNGoogleMapsPlusView.swift @@ -30,12 +30,10 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { ) } - @MainActor func dispose() { impl.deinitInternal() } - @MainActor var initialProps: RNInitialProps? { didSet { let options = GMSMapViewOptions() @@ -52,32 +50,26 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { } } - @MainActor var uiSettings: RNMapUiSettings? { didSet { impl.uiSettings = uiSettings } } - @MainActor var myLocationEnabled: Bool? { didSet { impl.myLocationEnabled = myLocationEnabled } } - @MainActor var buildingEnabled: Bool? { didSet { impl.buildingEnabled = buildingEnabled } } - @MainActor var trafficEnabled: Bool? { didSet { impl.trafficEnabled = trafficEnabled } } - @MainActor var indoorEnabled: Bool? { didSet { impl.indoorEnabled = indoorEnabled } } - @MainActor var customMapStyle: String? { didSet { if let value = customMapStyle { @@ -86,31 +78,26 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { } } - @MainActor var userInterfaceStyle: RNUserInterfaceStyle? { didSet { impl.userInterfaceStyle = userInterfaceStyle?.toUIUserInterfaceStyle } } - @MainActor var mapZoomConfig: RNMapZoomConfig? { didSet { impl.mapZoomConfig = mapZoomConfig } } - @MainActor var mapPadding: RNMapPadding? { didSet { impl.mapPadding = mapPadding } } - @MainActor var mapType: RNMapType? { didSet { impl.mapType = mapType?.toGMSMapViewType } } - @MainActor var markers: [RNMarker]? { didSet { let prevById = Dictionary( @@ -148,7 +135,6 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { } } - @MainActor var polylines: [RNPolyline]? { didSet { let prevById = Dictionary( @@ -181,7 +167,6 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { } } - @MainActor var polygons: [RNPolygon]? { didSet { let prevById = Dictionary( @@ -211,7 +196,6 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { } } - @MainActor var circles: [RNCircle]? { didSet { let prevById = Dictionary( @@ -241,7 +225,6 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { } } - @MainActor var heatmaps: [RNHeatmap]? { didSet { let prevById = Dictionary( @@ -262,7 +245,6 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { } } - @MainActor var kmlLayers: [RNKMLayer]? { didSet { let prevById = Dictionary( @@ -283,7 +265,6 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { } } - @MainActor var urlTileOverlays: [RNUrlTileOverlay]? { didSet { let prevById = Dictionary( @@ -307,135 +288,107 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { } } - @MainActor var locationConfig: RNLocationConfig? { didSet { impl.locationConfig = locationConfig } } - @MainActor var onMapError: ((RNMapErrorCode) -> Void)? { didSet { impl.onMapError = onMapError } } - @MainActor var onMapReady: ((Bool) -> Void)? { didSet { impl.onMapReady = onMapReady } } - @MainActor var onMapLoaded: ((RNRegion, RNCamera) -> Void)? { didSet { impl.onMapLoaded = onMapLoaded } } - @MainActor var onLocationUpdate: ((RNLocation) -> Void)? { didSet { impl.onLocationUpdate = onLocationUpdate } } - @MainActor var onLocationError: ((_ error: RNLocationErrorCode) -> Void)? { didSet { impl.onLocationError = onLocationError } } - @MainActor var onMapPress: ((RNLatLng) -> Void)? { didSet { impl.onMapPress = onMapPress } } - @MainActor var onMapLongPress: ((RNLatLng) -> Void)? { didSet { impl.onMapLongPress = onMapLongPress } } - @MainActor var onPoiPress: ((String, String, RNLatLng) -> Void)? { didSet { impl.onPoiPress = onPoiPress } } - @MainActor var onMarkerPress: ((String) -> Void)? { didSet { impl.onMarkerPress = onMarkerPress } } - @MainActor var onPolylinePress: ((String) -> Void)? { didSet { impl.onPolylinePress = onPolylinePress } } - @MainActor var onPolygonPress: ((String) -> Void)? { didSet { impl.onPolygonPress = onPolygonPress } } - @MainActor var onCirclePress: ((String) -> Void)? { didSet { impl.onCirclePress = onCirclePress } } - @MainActor var onMarkerDragStart: ((String, RNLatLng) -> Void)? { didSet { impl.onMarkerDragStart = onMarkerDragStart } } - @MainActor var onMarkerDrag: ((String, RNLatLng) -> Void)? { didSet { impl.onMarkerDrag = onMarkerDrag } } - @MainActor var onMarkerDragEnd: ((String, RNLatLng) -> Void)? { didSet { impl.onMarkerDragEnd = onMarkerDragEnd } } - @MainActor var onIndoorBuildingFocused: ((RNIndoorBuilding) -> Void)? { didSet { impl.onIndoorBuildingFocused = onIndoorBuildingFocused } } - @MainActor var onIndoorLevelActivated: ((RNIndoorLevel) -> Void)? { didSet { impl.onIndoorLevelActivated = onIndoorLevelActivated } } - @MainActor var onInfoWindowPress: ((String) -> Void)? { didSet { impl.onInfoWindowPress = onInfoWindowPress } } - @MainActor var onInfoWindowClose: ((String) -> Void)? { didSet { impl.onInfoWindowClose = onInfoWindowClose } } - @MainActor var onInfoWindowLongPress: ((String) -> Void)? { didSet { impl.onInfoWindowLongPress = onInfoWindowLongPress } } - @MainActor var onMyLocationPress: ((RNLocation) -> Void)? { didSet { impl.onMyLocationPress = onMyLocationPress } } - @MainActor var onMyLocationButtonPress: ((Bool) -> Void)? { didSet { impl.onMyLocationButtonPress = onMyLocationButtonPress } } - @MainActor var onCameraChangeStart: ((RNRegion, RNCamera, Bool) -> Void)? { didSet { impl.onCameraChangeStart = onCameraChangeStart } } - @MainActor var onCameraChange: ((RNRegion, RNCamera, Bool) -> Void)? { didSet { impl.onCameraChange = onCameraChange } } - @MainActor var onCameraChangeComplete: ((RNRegion, RNCamera, Bool) -> Void)? { didSet { impl.onCameraChangeComplete = onCameraChangeComplete } } - @MainActor func showMarkerInfoWindow(id: String) { impl.showMarkerInfoWindow(id: id) } - @MainActor func hideMarkerInfoWindow(id: String) { impl.hideMarkerInfoWindow(id: id) } - @MainActor func setCamera(camera: RNCameraUpdate, animated: Bool?, durationMs: Double?) { - let cam = camera.toGMSCameraPosition(current: impl.currentCamera) - impl.setCamera( - camera: cam, - animated: animated ?? true, - durationMs: durationMs ?? 3000 - ) + onMain { + let cam = camera.toGMSCameraPosition(current: self.impl.currentCamera) + self.impl.setCamera( + camera: cam, + animated: animated ?? true, + durationMs: durationMs ?? 3000 + ) + } } - @MainActor func setCameraToCoordinates( coordinates: [RNLatLng], padding: RNMapPadding?, @@ -450,12 +403,10 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { ) } - @MainActor func setCameraBounds(bounds: RNLatLngBounds?) { impl.setCameraBounds(bounds?.toCoordinateBounds()) } - @MainActor func animateToBounds( bounds: RNLatLngBounds, padding: Double?, @@ -470,7 +421,6 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { ) } - @MainActor func snapshot( options: RNSnapshotOptions, ) -> NitroModules.Promise { @@ -484,25 +434,21 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { } - @MainActor func showLocationDialog() { locationHandler.showLocationDialog() } - @MainActor func openLocationSettings() { locationHandler.openLocationSettings() } - @MainActor func requestLocationPermission() -> NitroModules.Promise { return permissionHandler.requestLocationPermission() } - @MainActor func isGooglePlayServicesAvailable() -> Bool { /// not supported - return true + return false } } diff --git a/ios/extensions/UIImage+Extension.swift b/ios/extensions/UIImage+Extension.swift index 3dcb358..774e689 100644 --- a/ios/extensions/UIImage+Extension.swift +++ b/ios/extensions/UIImage+Extension.swift @@ -35,6 +35,7 @@ extension UIImage { try imageData.write(to: fileURL) return fileURL.path } catch { + mapsLog("snapshot write failed", error) return nil } } else {