diff --git a/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt b/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt index 45b7c4b..8128ccc 100644 --- a/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt +++ b/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt @@ -1,13 +1,16 @@ package com.rngooglemapsplus +import CircleTag +import MarkerTag +import PolygonTag +import PolylineTag import android.annotation.SuppressLint import android.graphics.Bitmap -import android.util.Base64 +import android.location.Location import android.util.Size +import android.view.View import android.widget.FrameLayout -import androidx.core.graphics.scale import com.facebook.react.bridge.LifecycleEventListener -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.CameraUpdateFactory @@ -24,6 +27,7 @@ import com.google.android.gms.maps.model.MapColorScheme import com.google.android.gms.maps.model.MapStyleOptions import com.google.android.gms.maps.model.Marker import com.google.android.gms.maps.model.MarkerOptions +import com.google.android.gms.maps.model.PointOfInterest import com.google.android.gms.maps.model.Polygon import com.google.android.gms.maps.model.PolygonOptions import com.google.android.gms.maps.model.Polyline @@ -32,6 +36,9 @@ import com.google.android.gms.maps.model.TileOverlay 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 @@ -42,10 +49,10 @@ 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 -import java.io.ByteArrayOutputStream -import java.io.File -import java.io.FileOutputStream import java.nio.charset.StandardCharsets class GoogleMapsViewImpl( @@ -58,25 +65,34 @@ class GoogleMapsViewImpl( GoogleMap.OnCameraMoveListener, GoogleMap.OnCameraIdleListener, GoogleMap.OnMapClickListener, + GoogleMap.OnMapLongClickListener, + GoogleMap.OnPoiClickListener, GoogleMap.OnMarkerClickListener, GoogleMap.OnPolylineClickListener, GoogleMap.OnPolygonClickListener, GoogleMap.OnCircleClickListener, GoogleMap.OnMarkerDragListener, GoogleMap.OnIndoorStateChangeListener, + GoogleMap.OnInfoWindowClickListener, + GoogleMap.OnInfoWindowCloseListener, + GoogleMap.OnInfoWindowLongClickListener, + GoogleMap.OnMyLocationClickListener, + GoogleMap.OnMyLocationButtonClickListener, + GoogleMap.InfoWindowAdapter, LifecycleEventListener { private var initialized = false - private var mapReady = false + private var loaded = false private var destroyed = false private var googleMap: GoogleMap? = null private var mapView: MapView? = null - private val pendingMarkers = mutableListOf>() + private val pendingMarkers = mutableListOf>() private val pendingPolylines = mutableListOf>() private val pendingPolygons = mutableListOf>() private val pendingCircles = mutableListOf>() private val pendingHeatmaps = mutableListOf>() private val pendingKmlLayers = mutableListOf>() + private val pendingUrlTilesOverlays = mutableListOf>() private val markersById = mutableMapOf() private val polylinesById = mutableMapOf() @@ -84,112 +100,111 @@ class GoogleMapsViewImpl( private val circlesById = mutableMapOf() private val heatmapsById = mutableMapOf() private val kmlLayersById = mutableMapOf() + private val urlTileOverlaysById = mutableMapOf() private var cameraMoveReason = -1 - private var lastSubmittedCameraPosition: CameraPosition? = null init { reactContext.addLifecycleEventListener(this) } - fun initMapView(googleMapsOptions: GoogleMapOptions) { - if (initialized) return - initialized = true - val result = playServiceHandler.playServicesAvailability() - val errorCode = result.toRNMapErrorCodeOrNull() + fun initMapView(googleMapsOptions: GoogleMapOptions) = + onUi { + if (initialized) return@onUi + initialized = true - if (errorCode != null) { - onMapError?.invoke(errorCode) + val result = playServiceHandler.playServicesAvailability() + val errorCode = result.toRNMapErrorCodeOrNull() + if (errorCode != null) { + onMapError?.invoke(errorCode) + if (errorCode == RNMapErrorCode.PLAY_SERVICES_MISSING || + errorCode == RNMapErrorCode.PLAY_SERVICES_INVALID + ) { + return@onUi + } + } - if (errorCode == RNMapErrorCode.PLAY_SERVICES_MISSING || - errorCode == RNMapErrorCode.PLAY_SERVICES_INVALID - ) { - return + mapView = MapView(reactContext, googleMapsOptions) + super.addView(mapView) + + mapView?.onCreate(null) + mapView?.getMapAsync { map -> + googleMap = map + googleMap?.setOnMapLoadedCallback { + googleMap?.setOnCameraMoveStartedListener(this@GoogleMapsViewImpl) + googleMap?.setOnCameraMoveListener(this@GoogleMapsViewImpl) + googleMap?.setOnCameraIdleListener(this@GoogleMapsViewImpl) + googleMap?.setOnMarkerClickListener(this@GoogleMapsViewImpl) + googleMap?.setOnPolylineClickListener(this@GoogleMapsViewImpl) + googleMap?.setOnPolygonClickListener(this@GoogleMapsViewImpl) + googleMap?.setOnCircleClickListener(this@GoogleMapsViewImpl) + googleMap?.setOnMapClickListener(this@GoogleMapsViewImpl) + googleMap?.setOnMapLongClickListener(this@GoogleMapsViewImpl) + googleMap?.setOnPoiClickListener(this@GoogleMapsViewImpl) + googleMap?.setOnMarkerDragListener(this@GoogleMapsViewImpl) + googleMap?.setOnInfoWindowClickListener(this@GoogleMapsViewImpl) + googleMap?.setOnInfoWindowCloseListener(this@GoogleMapsViewImpl) + googleMap?.setOnInfoWindowLongClickListener(this@GoogleMapsViewImpl) + googleMap?.setOnMyLocationClickListener(this@GoogleMapsViewImpl) + googleMap?.setOnMyLocationButtonClickListener(this@GoogleMapsViewImpl) + googleMap?.setInfoWindowAdapter(this@GoogleMapsViewImpl) + loaded = true + onMapLoaded?.invoke( + map.projection.visibleRegion.toRnRegion(), + map.cameraPosition.toRnCamera(), + ) + } + applyProps() + initLocationCallbacks() + onMapReady?.invoke(true) } } - mapView = - MapView( - reactContext, - googleMapsOptions, + override fun onCameraMoveStarted(reason: Int) = + onUi { + if (!loaded) return@onUi + cameraMoveReason = reason + val visibleRegion = googleMap?.projection?.visibleRegion ?: return@onUi + val cameraPosition = googleMap?.cameraPosition ?: return@onUi + onCameraChangeStart?.invoke( + visibleRegion.toRnRegion(), + cameraPosition.toRnCamera(), + GoogleMap.OnCameraMoveStartedListener.REASON_GESTURE == reason, ) - - super.addView(mapView) - - mapView?.onCreate(null) - mapView?.getMapAsync { map -> - googleMap = map - googleMap?.setOnMapLoadedCallback { - googleMap?.setOnCameraMoveStartedListener(this@GoogleMapsViewImpl) - googleMap?.setOnCameraMoveListener(this@GoogleMapsViewImpl) - googleMap?.setOnCameraIdleListener(this@GoogleMapsViewImpl) - googleMap?.setOnMarkerClickListener(this@GoogleMapsViewImpl) - googleMap?.setOnPolylineClickListener(this@GoogleMapsViewImpl) - googleMap?.setOnPolygonClickListener(this@GoogleMapsViewImpl) - googleMap?.setOnCircleClickListener(this@GoogleMapsViewImpl) - googleMap?.setOnMapClickListener(this@GoogleMapsViewImpl) - googleMap?.setOnMarkerDragListener(this@GoogleMapsViewImpl) - } - applyProps() - initLocationCallbacks() - mapReady = true - onMapReady?.invoke(true) } - } - - override fun onCameraMoveStarted(reason: Int) { - lastSubmittedCameraPosition = null - cameraMoveReason = reason - val bounds = googleMap?.projection?.visibleRegion?.latLngBounds ?: return - val cameraPosition = googleMap?.cameraPosition ?: return - val isGesture = GoogleMap.OnCameraMoveStartedListener.REASON_GESTURE == reason - - onCameraChangeStart?.invoke( - bounds.toRnRegion(), - cameraPosition.toRnCamera(), - isGesture, - ) - } - - override fun onCameraMove() { - val bounds = googleMap?.projection?.visibleRegion?.latLngBounds ?: return - val cameraPosition = googleMap?.cameraPosition ?: return - - if (cameraPosition == lastSubmittedCameraPosition) { - return + override fun onCameraMove() = + onUi { + if (!loaded) return@onUi + val visibleRegion = googleMap?.projection?.visibleRegion ?: return@onUi + val cameraPosition = googleMap?.cameraPosition ?: return@onUi + val gesture = GoogleMap.OnCameraMoveStartedListener.REASON_GESTURE == cameraMoveReason + onCameraChange?.invoke( + visibleRegion.toRnRegion(), + cameraPosition.toRnCamera(), + gesture, + ) } - lastSubmittedCameraPosition = cameraPosition - - val isGesture = GoogleMap.OnCameraMoveStartedListener.REASON_GESTURE == cameraMoveReason - - onCameraChange?.invoke( - bounds.toRnRegion(), - cameraPosition.toRnCamera(), - isGesture, - ) - } - override fun onCameraIdle() { - val bounds = googleMap?.projection?.visibleRegion?.latLngBounds ?: return - val cameraPosition = googleMap?.cameraPosition ?: return - - val isGesture = GoogleMap.OnCameraMoveStartedListener.REASON_GESTURE == cameraMoveReason - - onCameraChangeComplete?.invoke( - bounds.toRnRegion(), - cameraPosition.toRnCamera(), - isGesture, - ) - } + override fun onCameraIdle() = + onUi { + if (!loaded) return@onUi + val visibleRegion = googleMap?.projection?.visibleRegion ?: return@onUi + val cameraPosition = googleMap?.cameraPosition ?: return@onUi + val gesture = GoogleMap.OnCameraMoveStartedListener.REASON_GESTURE == cameraMoveReason + onCameraChangeComplete?.invoke( + visibleRegion.toRnRegion(), + cameraPosition.toRnCamera(), + gesture, + ) + } fun initLocationCallbacks() { locationHandler.onUpdate = { location -> - onLocationUpdate?.invoke(location.toRnLocation()) + onUi { onLocationUpdate?.invoke(location.toRnLocation()) } } - locationHandler.onError = { error -> - onLocationError?.invoke(error) + onUi { onLocationError?.invoke(error) } } locationHandler.start() } @@ -208,50 +223,37 @@ class GoogleMapsViewImpl( locationConfig = locationConfig if (pendingMarkers.isNotEmpty()) { - pendingMarkers.forEach { (id, opts) -> - internalAddMarker(id, opts) - } + pendingMarkers.forEach { (id, opts, markerTag) -> addMarkerInternal(id, opts, markerTag) } pendingMarkers.clear() } - if (pendingPolylines.isNotEmpty()) { - pendingPolylines.forEach { (id, opts) -> - internalAddPolyline(id, opts) - } + pendingPolylines.forEach { (id, opts) -> addPolylineInternal(id, opts) } pendingPolylines.clear() } - if (pendingPolygons.isNotEmpty()) { - pendingPolygons.forEach { (id, opts) -> - internalAddPolygon(id, opts) - } + pendingPolygons.forEach { (id, opts) -> addPolygonInternal(id, opts) } pendingPolygons.clear() } - if (pendingCircles.isNotEmpty()) { - pendingCircles.forEach { (id, opts) -> - internalAddCircle(id, opts) - } + pendingCircles.forEach { (id, opts) -> addCircleInternal(id, opts) } pendingCircles.clear() } - if (pendingHeatmaps.isNotEmpty()) { - pendingHeatmaps.forEach { (id, opts) -> - internalAddHeatmap(id, opts) - } + pendingHeatmaps.forEach { (id, opts) -> addHeatmapInternal(id, opts) } pendingHeatmaps.clear() } - if (pendingKmlLayers.isNotEmpty()) { - pendingKmlLayers.forEach { (id, string) -> - internalAddKmlLayer(id, string) - } + pendingKmlLayers.forEach { (id, str) -> addKmlLayerInternal(id, str) } pendingKmlLayers.clear() } + if (pendingUrlTilesOverlays.isNotEmpty()) { + pendingUrlTilesOverlays.forEach { (id, opts) -> addUrlTileOverlayInternal(id, opts) } + pendingUrlTilesOverlays.clear() + } } val currentCamera: CameraPosition? - get() = googleMap?.cameraPosition + get() = onUiSync { googleMap?.cameraPosition } var initialProps: RNInitialProps? = null @@ -287,8 +289,8 @@ class GoogleMapsViewImpl( onUi { try { googleMap?.isMyLocationEnabled = value ?: false - } catch (se: SecurityException) { - onLocationError?.invoke(RNLocationErrorCode.PERMISSION_DENIED) + } catch (_: SecurityException) { + onLocationError?.let { cb -> cb(RNLocationErrorCode.PERMISSION_DENIED) } } catch (ex: Exception) { val error = ex.toLocationErrorCode(context) onLocationError?.invoke(error) @@ -299,41 +301,31 @@ class GoogleMapsViewImpl( var buildingEnabled: Boolean? = null set(value) { field = value - onUi { - googleMap?.isBuildingsEnabled = value ?: false - } + onUi { googleMap?.isBuildingsEnabled = value ?: false } } var trafficEnabled: Boolean? = null set(value) { field = value - onUi { - googleMap?.isTrafficEnabled = value ?: false - } + onUi { googleMap?.isTrafficEnabled = value ?: false } } var indoorEnabled: Boolean? = null set(value) { field = value - onUi { - googleMap?.isIndoorEnabled = value ?: false - } + onUi { googleMap?.isIndoorEnabled = value ?: false } } var customMapStyle: MapStyleOptions? = null set(value) { field = value - onUi { - googleMap?.setMapStyle(value) - } + onUi { googleMap?.setMapStyle(value) } } var userInterfaceStyle: Int? = null set(value) { field = value - onUi { - googleMap?.mapColorScheme = value ?: MapColorScheme.FOLLOW_SYSTEM - } + onUi { googleMap?.mapColorScheme = value ?: MapColorScheme.FOLLOW_SYSTEM } } var mapZoomConfig: RNMapZoomConfig? = null @@ -361,9 +353,7 @@ class GoogleMapsViewImpl( var mapType: Int? = null set(value) { field = value - onUi { - googleMap?.mapType = value ?: 1 - } + onUi { googleMap?.mapType = value ?: 1 } } var locationConfig: RNLocationConfig? = null @@ -378,35 +368,52 @@ class GoogleMapsViewImpl( var onMapError: ((RNMapErrorCode) -> Unit)? = null var onMapReady: ((Boolean) -> Unit)? = null + var onMapLoaded: ((RNRegion, RNCamera) -> Unit)? = null var onLocationUpdate: ((RNLocation) -> Unit)? = null var onLocationError: ((RNLocationErrorCode) -> Unit)? = null var onMapPress: ((RNLatLng) -> Unit)? = null - var onMarkerPress: ((String?) -> Unit)? = null - var onPolylinePress: ((String?) -> Unit)? = null - var onPolygonPress: ((String?) -> Unit)? = null - var onCirclePress: ((String?) -> Unit)? = null - var onMarkerDragStart: ((String?, RNLatLng) -> Unit)? = null - var onMarkerDrag: ((String?, RNLatLng) -> Unit)? = null - var onMarkerDragEnd: ((String?, RNLatLng) -> Unit)? = null + var onMapLongPress: ((RNLatLng) -> Unit)? = null + var onPoiPress: ((String, String, RNLatLng) -> Unit)? = null + var onMarkerPress: ((String) -> Unit)? = null + var onPolylinePress: ((String) -> Unit)? = null + var onPolygonPress: ((String) -> Unit)? = null + var onCirclePress: ((String) -> Unit)? = null + var onMarkerDragStart: ((String, RNLatLng) -> Unit)? = null + var onMarkerDrag: ((String, RNLatLng) -> Unit)? = null + var onMarkerDragEnd: ((String, RNLatLng) -> Unit)? = null var onIndoorBuildingFocused: ((RNIndoorBuilding) -> Unit)? = null var onIndoorLevelActivated: ((RNIndoorLevel) -> Unit)? = null + var onInfoWindowPress: ((String) -> Unit)? = null + var onInfoWindowClose: ((String) -> Unit)? = null + var onInfoWindowLongPress: ((String) -> Unit)? = null + var onMyLocationPress: ((RNLocation) -> Unit)? = null + var onMyLocationButtonPress: ((Boolean) -> Unit)? = null var onCameraChangeStart: ((RNRegion, RNCamera, Boolean) -> Unit)? = null var onCameraChange: ((RNRegion, RNCamera, Boolean) -> Unit)? = null var onCameraChangeComplete: ((RNRegion, RNCamera, Boolean) -> Unit)? = null + fun showMarkerInfoWindow(id: String) = + onUi { + val marker = markersById[id] ?: return@onUi + marker.showInfoWindow() + } + + fun hideMarkerInfoWindow(id: String) = + onUi { + val marker = markersById[id] ?: return@onUi + marker.hideInfoWindow() + } + fun setCamera( cameraPosition: CameraPosition, animated: Boolean, durationMs: Int, - ) { - onUi { - val update = CameraUpdateFactory.newCameraPosition(cameraPosition) - - if (animated) { - googleMap?.animateCamera(update, durationMs, null) - } else { - googleMap?.moveCamera(update) - } + ) = onUi { + val update = CameraUpdateFactory.newCameraPosition(cameraPosition) + if (animated) { + googleMap?.animateCamera(update, durationMs, null) + } else { + googleMap?.moveCamera(update) } } @@ -415,93 +422,48 @@ class GoogleMapsViewImpl( padding: RNMapPadding, animated: Boolean, durationMs: Int, - ) { - if (coordinates.isEmpty()) { - return - } - onUi { - val builder = LatLngBounds.Builder() - coordinates.forEach { coord -> - builder.include(coord.toLatLng()) - } - val bounds = builder.build() + ) = onUi { + if (coordinates.isEmpty()) return@onUi - val latSpan = bounds.northeast.latitude - bounds.southwest.latitude - val lngSpan = bounds.northeast.longitude - bounds.southwest.longitude + val w = mapView?.width ?: 0 + val h = mapView?.height ?: 0 - val latPerPixel = latSpan / (mapView?.height ?: 0) - val lngPerPixel = lngSpan / (mapView?.width ?: 0) + val builder = LatLngBounds.builder() + coordinates.forEach { coord -> builder.include(coord.toLatLng()) } - builder.include( - LatLng( - bounds.northeast.latitude + (padding.top.dpToPx() * latPerPixel), - bounds.northeast.longitude, - ), - ) - builder.include( - LatLng( - bounds.southwest.latitude - (padding.bottom.dpToPx() * latPerPixel), - bounds.southwest.longitude, - ), - ) - builder.include( - LatLng( - bounds.northeast.latitude, - bounds.northeast.longitude + (padding.right.dpToPx() * lngPerPixel), - ), - ) - builder.include( - LatLng( - bounds.southwest.latitude, - bounds.southwest.longitude - (padding.left.dpToPx() * lngPerPixel), - ), - ) + val baseBounds = builder.build() + val paddedBounds = baseBounds.withPaddingPixels(w, h, padding) - val paddedBounds = builder.build() + 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 adjustedWidth = - ((mapView?.width ?: 0) - padding.left.dpToPx() - padding.right.dpToPx()).toInt() - val adjustedHeight = - ((mapView?.height ?: 0) - padding.top.dpToPx() - padding.bottom.dpToPx()).toInt() + val update = CameraUpdateFactory.newLatLngBounds(paddedBounds, adjustedWidth, adjustedHeight, 0) - val update = - CameraUpdateFactory.newLatLngBounds( - paddedBounds, - adjustedWidth, - adjustedHeight, - 0, - ) - if (animated) { - googleMap?.animateCamera(update, durationMs, null) - } else { - googleMap?.moveCamera(update) - } + if (animated) { + googleMap?.animateCamera(update, durationMs, null) + } else { + googleMap?.moveCamera(update) } } - fun setCameraBounds(bounds: LatLngBounds?) { + fun setCameraBounds(bounds: LatLngBounds?) = onUi { googleMap?.setLatLngBoundsForCameraTarget(bounds) } - } fun animateToBounds( bounds: LatLngBounds, padding: Int, durationMs: Int, lockBounds: Boolean, - ) { - onUi { - if (lockBounds) { - googleMap?.setLatLngBoundsForCameraTarget(bounds) - } - val update = - CameraUpdateFactory.newLatLngBounds( - bounds, - padding, - ) - googleMap?.animateCamera(update, durationMs, null) + ) = onUi { + if (lockBounds) { + googleMap?.setLatLngBoundsForCameraTarget(bounds) } + val update = CameraUpdateFactory.newLatLngBounds(bounds, padding) + googleMap?.animateCamera(update, durationMs, null) } fun snapshot( @@ -514,349 +476,314 @@ class GoogleMapsViewImpl( val promise = Promise() onUi { googleMap?.snapshot { bitmap -> - try { - if (bitmap == null) { - promise.resolve(null) - return@snapshot - } - - val scaledBitmap = - size?.let { - bitmap.scale(it.width, it.height) - } ?: bitmap - - val output = ByteArrayOutputStream() - scaledBitmap.compress(compressFormat, (quality * 100).toInt().coerceIn(0, 100), output) - val bytes = output.toByteArray() - - if (resultIsFile) { - val file = File(context.cacheDir, "map_snapshot_${System.currentTimeMillis()}.$format") - FileOutputStream(file).use { it.write(bytes) } - promise.resolve(file.absolutePath) - } else { - val base64 = Base64.encodeToString(bytes, Base64.NO_WRAP) - promise.resolve("data:image/$format;base64,$base64") - } - - if (scaledBitmap != bitmap) { - scaledBitmap.recycle() - } - bitmap.recycle() - } catch (e: Exception) { - promise.resolve(null) - } + bitmap + ?.encode(context, size, format, compressFormat, quality, resultIsFile) + ?.let(promise::resolve) ?: promise.resolve(null) } } - return promise } fun addMarker( id: String, opts: MarkerOptions, - ) { + markerTag: MarkerTag, + ) = onUi { if (googleMap == null) { - pendingMarkers.add(id to opts) - return + pendingMarkers.add(Triple(id, opts, markerTag)) + return@onUi } - onUi { - markersById.remove(id)?.remove() - } - internalAddMarker(id, opts) + markersById.remove(id)?.remove() + addMarkerInternal(id, opts, markerTag) } - private fun internalAddMarker( + private fun addMarkerInternal( id: String, opts: MarkerOptions, - ) { - onUi { - val marker = - googleMap?.addMarker(opts).also { - it?.tag = id - } - if (marker != null) { - markersById[id] = marker + markerTag: MarkerTag, + ) = onUi { + val marker = + googleMap?.addMarker(opts)?.apply { + tag = markerTag } + + if (marker != null) { + markersById[id] = marker } } fun updateMarker( id: String, block: (Marker) -> Unit, - ) { - val marker = markersById[id] ?: return - onUi { - block(marker) + ) = onUi { + val marker = markersById[id] ?: return@onUi + block(marker) + if (marker.isInfoWindowShown) { + marker.hideInfoWindow() + marker.showInfoWindow() } } - fun removeMarker(id: String) { + fun removeMarker(id: String) = onUi { - val marker = markersById.remove(id) - marker?.remove() + markersById.remove(id)?.remove() } - } - fun clearMarkers() { + fun clearMarkers() = onUi { markersById.values.forEach { it.remove() } + markersById.clear() + pendingMarkers.clear() } - markersById.clear() - pendingMarkers.clear() - } fun addPolyline( id: String, opts: PolylineOptions, - ) { + ) = onUi { if (googleMap == null) { pendingPolylines.add(id to opts) - return - } - onUi { - polylinesById.remove(id)?.remove() + return@onUi } - internalAddPolyline(id, opts) + polylinesById.remove(id)?.remove() + addPolylineInternal(id, opts) } - private fun internalAddPolyline( + private fun addPolylineInternal( id: String, opts: PolylineOptions, - ) { - onUi { - val polyline = - googleMap?.addPolyline(opts).also { - it?.tag = id - } - if (polyline != null) { - polylinesById[id] = polyline + ) = onUi { + val pl = + googleMap?.addPolyline(opts).also { + it?.tag = PolylineTag(id = id) } - } + if (pl != null) polylinesById[id] = pl } fun updatePolyline( id: String, block: (Polyline) -> Unit, - ) { - val pl = polylinesById[id] ?: return - onUi { - block(pl) - } + ) = onUi { + val pl = polylinesById[id] ?: return@onUi + block(pl) } - fun removePolyline(id: String) { + fun removePolyline(id: String) = onUi { polylinesById.remove(id)?.remove() } - } - fun clearPolylines() { + fun clearPolylines() = onUi { polylinesById.values.forEach { it.remove() } + polylinesById.clear() + pendingPolylines.clear() } - polylinesById.clear() - pendingPolylines.clear() - } fun addPolygon( id: String, opts: PolygonOptions, - ) { + ) = onUi { if (googleMap == null) { pendingPolygons.add(id to opts) - return + return@onUi } - - onUi { - polygonsById.remove(id)?.remove() - } - internalAddPolygon(id, opts) + polygonsById.remove(id)?.remove() + addPolygonInternal(id, opts) } - private fun internalAddPolygon( + private fun addPolygonInternal( id: String, opts: PolygonOptions, - ) { - onUi { - val polygon = - googleMap?.addPolygon(opts).also { - it?.tag = id - } - if (polygon != null) { - polygonsById[id] = polygon + ) = onUi { + val polygon = + googleMap?.addPolygon(opts).also { + it?.tag = PolygonTag(id = id) } - } + if (polygon != null) polygonsById[id] = polygon } fun updatePolygon( id: String, block: (Polygon) -> Unit, - ) { - val polygon = polygonsById[id] ?: return - onUi { - block(polygon) - } + ) = onUi { + val polygon = polygonsById[id] ?: return@onUi + block(polygon) } - fun removePolygon(id: String) { + fun removePolygon(id: String) = onUi { polygonsById.remove(id)?.remove() } - } - fun clearPolygons() { + fun clearPolygons() = onUi { polygonsById.values.forEach { it.remove() } + polygonsById.clear() + pendingPolygons.clear() } - polygonsById.clear() - pendingPolygons.clear() - } fun addCircle( id: String, opts: CircleOptions, - ) { + ) = onUi { if (googleMap == null) { pendingCircles.add(id to opts) - return + return@onUi } - - onUi { - circlesById.remove(id)?.remove() - } - internalAddCircle(id, opts) + circlesById.remove(id)?.remove() + addCircleInternal(id, opts) } - private fun internalAddCircle( + private fun addCircleInternal( id: String, opts: CircleOptions, - ) { - onUi { - val circle = - googleMap?.addCircle(opts).also { - it?.tag = id - } - if (circle != null) { - circlesById[id] = circle + ) = onUi { + val circle = + googleMap?.addCircle(opts).also { + it?.tag = CircleTag(id = id) } - } + if (circle != null) circlesById[id] = circle } fun updateCircle( id: String, block: (Circle) -> Unit, - ) { - val circle = circlesById[id] ?: return - onUi { - block(circle) - } + ) = onUi { + val circle = circlesById[id] ?: return@onUi + block(circle) } - fun removeCircle(id: String) { + fun removeCircle(id: String) = onUi { circlesById.remove(id)?.remove() } - } - fun clearCircles() { + fun clearCircles() = onUi { circlesById.values.forEach { it.remove() } + circlesById.clear() + pendingCircles.clear() } - circlesById.clear() - pendingCircles.clear() - } fun addHeatmap( id: String, opts: TileOverlayOptions, - ) { + ) = onUi { if (googleMap == null) { pendingHeatmaps.add(id to opts) - return - } - - onUi { - heatmapsById.remove(id)?.remove() + return@onUi } - internalAddHeatmap(id, opts) + heatmapsById.remove(id)?.remove() + addHeatmapInternal(id, opts) } - private fun internalAddHeatmap( + private fun addHeatmapInternal( id: String, opts: TileOverlayOptions, - ) { - onUi { - val heatmap = - googleMap?.addTileOverlay(opts) - if (heatmap != null) { - heatmapsById[id] = heatmap - } - } + ) = onUi { + val overlay = googleMap?.addTileOverlay(opts) + if (overlay != null) heatmapsById[id] = overlay } - fun removeHeatmap(id: String) { + fun removeHeatmap(id: String) = onUi { - heatmapsById.remove(id)?.remove() + heatmapsById.remove(id)?.let { heatMap -> + heatMap.clearTileCache() + heatMap.remove() + } } - } - fun clearHeatmaps() { + fun clearHeatmaps() = onUi { - heatmapsById.values.forEach { it.remove() } + heatmapsById.values.forEach { + it.clearTileCache() + it.remove() + } + heatmapsById.clear() + pendingHeatmaps.clear() } - heatmapsById.clear() - pendingHeatmaps.clear() - } fun addKmlLayer( id: String, kmlString: String, - ) { + ) = onUi { if (googleMap == null) { pendingKmlLayers.add(id to kmlString) - return + return@onUi } - onUi { - kmlLayersById.remove(id)?.removeLayerFromMap() - } - internalAddKmlLayer(id, kmlString) + kmlLayersById.remove(id)?.removeLayerFromMap() + addKmlLayerInternal(id, kmlString) } - private fun internalAddKmlLayer( + private fun addKmlLayerInternal( id: String, kmlString: String, - ) { - onUi { - try { - val inputStream = ByteArrayInputStream(kmlString.toByteArray(StandardCharsets.UTF_8)) - val layer = KmlLayer(googleMap, inputStream, context) - kmlLayersById[id] = layer - layer.addLayerToMap() - } catch (e: Exception) { - // / ignore - } + ) = onUi { + try { + val inputStream = ByteArrayInputStream(kmlString.toByteArray(StandardCharsets.UTF_8)) + val layer = KmlLayer(googleMap, inputStream, context) + kmlLayersById[id] = layer + layer.addLayerToMap() + } catch (_: Exception) { + // ignore } } - fun removeKmlLayer(id: String) { + fun removeKmlLayer(id: String) = onUi { kmlLayersById.remove(id)?.removeLayerFromMap() } - } - fun clearKmlLayer() { + fun clearKmlLayer() = onUi { kmlLayersById.values.forEach { it.removeLayerFromMap() } + kmlLayersById.clear() + pendingKmlLayers.clear() } - kmlLayersById.clear() - pendingKmlLayers.clear() + + fun addUrlTileOverlay( + id: String, + opts: TileOverlayOptions, + ) = onUi { + if (googleMap == null) { + pendingUrlTilesOverlays.add(id to opts) + return@onUi + } + urlTileOverlaysById.remove(id)?.remove() + addUrlTileOverlayInternal(id, opts) + } + + private fun addUrlTileOverlayInternal( + id: String, + opts: TileOverlayOptions, + ) = onUi { + val overlay = googleMap?.addTileOverlay(opts) + if (overlay != null) urlTileOverlaysById[id] = overlay } - fun destroyInternal() { - if (destroyed) return - destroyed = true + fun removeUrlTileOverlay(id: String) = + onUi { + urlTileOverlaysById.remove(id)?.let { urlTileOverlay -> + urlTileOverlay.clearTileCache() + urlTileOverlay.remove() + } + } + + fun clearUrlTileOverlays() = onUi { + urlTileOverlaysById.values.forEach { + it.clearTileCache() + it.remove() + } + urlTileOverlaysById.clear() + pendingUrlTilesOverlays.clear() + } + + fun destroyInternal() = + onUi { + if (destroyed) return@onUi + destroyed = true locationHandler.stop() markerBuilder.cancelAllJobs() clearMarkers() @@ -865,6 +792,7 @@ class GoogleMapsViewImpl( clearCircles() clearHeatmaps() clearKmlLayer() + clearUrlTileOverlays() googleMap?.apply { setOnCameraMoveStartedListener(null) setOnCameraMoveListener(null) @@ -874,7 +802,15 @@ class GoogleMapsViewImpl( setOnPolygonClickListener(null) setOnCircleClickListener(null) setOnMapClickListener(null) + setOnMapLongClickListener(null) + setOnPoiClickListener(null) setOnMarkerDragListener(null) + setOnInfoWindowClickListener(null) + setOnInfoWindowCloseListener(null) + setOnInfoWindowLongClickListener(null) + setOnMyLocationClickListener(null) + setOnMyLocationButtonClickListener(null) + setInfoWindowAdapter(null) } googleMap = null mapView?.apply { @@ -887,7 +823,6 @@ class GoogleMapsViewImpl( reactContext.removeLifecycleEventListener(this) initialized = false } - } override fun requestLayout() { super.requestLayout() @@ -901,99 +836,129 @@ class GoogleMapsViewImpl( } } - override fun onAttachedToWindow() { - super.onAttachedToWindow() - locationHandler.start() - } + override fun onAttachedToWindow() = + onUi { + super.onAttachedToWindow() + locationHandler.start() + } - override fun onDetachedFromWindow() { - super.onDetachedFromWindow() - locationHandler.stop() - } + override fun onDetachedFromWindow() = + onUi { + super.onDetachedFromWindow() + locationHandler.stop() + } - override fun onHostResume() { + override fun onHostResume() = onUi { locationHandler.start() mapView?.onResume() } - } - override fun onHostPause() { + override fun onHostPause() = onUi { locationHandler.stop() mapView?.onPause() } - } override fun onHostDestroy() { destroyInternal() } override fun onMarkerClick(marker: Marker): Boolean { - marker.showInfoWindow() - onMarkerPress?.invoke(marker.tag?.toString()) - return true + onUi { + onMarkerPress?.invoke(marker.idTag) + } + return uiSettings?.consumeOnMarkerPress ?: false } - override fun onPolylineClick(polyline: Polyline) { - onPolylinePress?.invoke(polyline.tag?.toString()) - } + override fun onPolylineClick(polyline: Polyline) = + onUi { + onPolylinePress?.invoke(polyline.idTag) + } - override fun onPolygonClick(polygon: Polygon) { - onPolygonPress?.invoke(polygon.tag?.toString()) - } + override fun onPolygonClick(polygon: Polygon) = + onUi { + onPolygonPress?.invoke(polygon.idTag) + } - override fun onCircleClick(circle: Circle) { - onCirclePress?.invoke(circle.tag?.toString()) - } + override fun onCircleClick(circle: Circle) = + onUi { + onCirclePress?.invoke(circle.idTag) + } - override fun onMapClick(coordinates: LatLng) { - onMapPress?.invoke( - coordinates.toRnLatLng(), - ) - } + override fun onMapClick(coordinates: LatLng) = + onUi { + onMapPress?.invoke(coordinates.toRnLatLng()) + } - override fun onMarkerDragStart(marker: Marker) { - onMarkerDragStart?.invoke( - marker.tag?.toString(), - marker.position.toRnLatLng(), - ) - } + override fun onMapLongClick(coordinates: LatLng) = + onUi { + onMapLongPress?.invoke(coordinates.toRnLatLng()) + } - override fun onMarkerDrag(marker: Marker) { - onMarkerDrag?.invoke( - marker.tag?.toString(), - marker.position.toRnLatLng(), - ) - } + override fun onMarkerDragStart(marker: Marker) = + onUi { + onMarkerDragStart?.invoke(marker.idTag, marker.position.toRnLatLng()) + } - override fun onMarkerDragEnd(marker: Marker) { - onMarkerDragEnd?.invoke( - marker.tag?.toString(), - marker.position.toRnLatLng(), - ) - } + override fun onMarkerDrag(marker: Marker) = + onUi { + onMarkerDrag?.invoke(marker.idTag, marker.position.toRnLatLng()) + } - override fun onIndoorBuildingFocused() { - val building = googleMap?.focusedBuilding ?: return - onIndoorBuildingFocused?.invoke(building.toRNIndoorBuilding()) - } + override fun onMarkerDragEnd(marker: Marker) = + onUi { + onMarkerDragEnd?.invoke(marker.idTag, marker.position.toRnLatLng()) + } - override fun onIndoorLevelActivated(indoorBuilding: IndoorBuilding) { - val activeLevel = indoorBuilding.levels.getOrNull(indoorBuilding.activeLevelIndex) ?: return - onIndoorLevelActivated?.invoke( - activeLevel.toRNIndoorLevel( - indoorBuilding.activeLevelIndex, - true, - ), - ) - } -} + override fun onIndoorBuildingFocused() = + onUi { + val building = googleMap?.focusedBuilding ?: return@onUi + onIndoorBuildingFocused?.invoke(building.toRNIndoorBuilding()) + } + + override fun onIndoorLevelActivated(indoorBuilding: IndoorBuilding) = + onUi { + val activeLevel = + indoorBuilding.levels.getOrNull(indoorBuilding.activeLevelIndex) ?: return@onUi + onIndoorLevelActivated?.invoke( + activeLevel.toRNIndoorLevel(indoorBuilding.activeLevelIndex, true), + ) + } + + override fun onPoiClick(poi: PointOfInterest) = + onUi { + onPoiPress?.invoke(poi.placeId, poi.name, poi.latLng.toRnLatLng()) + } + + override fun onInfoWindowClick(marker: Marker) = + onUi { + onInfoWindowPress?.invoke(marker.idTag) + } -private inline fun onUi(crossinline block: () -> Unit) { - if (UiThreadUtil.isOnUiThread()) { - block() - } else { - UiThreadUtil.runOnUiThread { block() } + override fun onInfoWindowClose(marker: Marker) = + onUi { + onInfoWindowClose?.invoke(marker.idTag) + } + + override fun onInfoWindowLongClick(marker: Marker) = + onUi { + onInfoWindowLongPress?.invoke(marker.idTag) + } + + override fun onMyLocationClick(location: Location) = + onUi { + onMyLocationPress?.invoke(location.toRnLocation()) + } + + override fun onMyLocationButtonClick(): Boolean { + onUi { + onMyLocationButtonPress?.invoke(true) + } + return uiSettings?.consumeOnMyLocationButtonPress ?: false } + + override fun getInfoContents(marker: Marker): View? = null + + override fun getInfoWindow(marker: Marker): View? = markerBuilder.buildInfoWindow(marker.tagData.iconSvg) } diff --git a/android/src/main/java/com/rngooglemapsplus/LocationHandler.kt b/android/src/main/java/com/rngooglemapsplus/LocationHandler.kt index 62bc806..ce9fa7a 100644 --- a/android/src/main/java/com/rngooglemapsplus/LocationHandler.kt +++ b/android/src/main/java/com/rngooglemapsplus/LocationHandler.kt @@ -38,7 +38,6 @@ class LocationHandler( private var priority: Int = PRIORITY_DEFAULT private var interval: Long = INTERVAL_DEFAULT private var minUpdateInterval: Long = MIN_UPDATE_INTERVAL - private var lastSubmittedLocation: Location? = null private var isActive = false var onUpdate: ((Location) -> Unit)? = null @@ -57,6 +56,8 @@ class LocationHandler( this.interval = interval ?: INTERVAL_DEFAULT this.minUpdateInterval = minUpdateInterval ?: MIN_UPDATE_INTERVAL buildLocationRequest(this.priority, this.interval, this.minUpdateInterval) + stop() + start() } fun showLocationDialog() { @@ -143,9 +144,8 @@ class LocationHandler( fusedLocationClientProviderClient.lastLocation .addOnSuccessListener( OnSuccessListener { location -> - if (location != null && location != lastSubmittedLocation) { + if (location != null) { onUpdate?.invoke(location) - lastSubmittedLocation = location } }, ).addOnFailureListener { e -> @@ -157,11 +157,8 @@ class LocationHandler( override fun onLocationResult(locationResult: LocationResult) { val location = locationResult.lastLocation if (location != null) { - if (location != lastSubmittedLocation) { - lastSubmittedLocation = location - listener?.onLocationChanged(location) - onUpdate?.invoke(location) - } + listener?.onLocationChanged(location) + onUpdate?.invoke(location) } else { onError?.invoke(RNLocationErrorCode.POSITION_UNAVAILABLE) } diff --git a/android/src/main/java/com/rngooglemapsplus/MapCircleBuilder.kt b/android/src/main/java/com/rngooglemapsplus/MapCircleBuilder.kt index a934ae7..6079629 100644 --- a/android/src/main/java/com/rngooglemapsplus/MapCircleBuilder.kt +++ b/android/src/main/java/com/rngooglemapsplus/MapCircleBuilder.kt @@ -4,6 +4,7 @@ import android.graphics.Color 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.onUi import com.rngooglemapsplus.extensions.toColor import com.rngooglemapsplus.extensions.toLatLng @@ -23,7 +24,7 @@ class MapCircleBuilder { prev: RNCircle, next: RNCircle, circle: Circle, - ) { + ) = onUi { if (prev.center.latitude != next.center.latitude || prev.center.longitude != next.center.longitude ) { diff --git a/android/src/main/java/com/rngooglemapsplus/MapHeatmapBuilder.kt b/android/src/main/java/com/rngooglemapsplus/MapHeatmapBuilder.kt index 4346c14..bee587a 100644 --- a/android/src/main/java/com/rngooglemapsplus/MapHeatmapBuilder.kt +++ b/android/src/main/java/com/rngooglemapsplus/MapHeatmapBuilder.kt @@ -19,7 +19,7 @@ class MapHeatmapBuilder { heatmap.gradient?.let { val colors = it.colors.map { c -> c.toColor() }.toIntArray() val startPoints = it.startPoints.map { p -> p.toFloat() }.toFloatArray() - gradient(Gradient(colors, startPoints)) + gradient(Gradient(colors, startPoints, it.colorMapSize.toInt())) } }.build() diff --git a/android/src/main/java/com/rngooglemapsplus/MapHelper.kt b/android/src/main/java/com/rngooglemapsplus/MapHelper.kt new file mode 100644 index 0000000..98ff2e3 --- /dev/null +++ b/android/src/main/java/com/rngooglemapsplus/MapHelper.kt @@ -0,0 +1,22 @@ +package com.rngooglemapsplus.extensions + +import com.facebook.react.bridge.UiThreadUtil +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.runBlocking + +inline fun onUi(crossinline block: () -> Unit) { + if (UiThreadUtil.isOnUiThread()) { + block() + } else { + UiThreadUtil.runOnUiThread { block() } + } +} + +inline fun onUiSync(crossinline block: () -> T): T { + if (UiThreadUtil.isOnUiThread()) return block() + val result = CompletableDeferred() + UiThreadUtil.runOnUiThread { + runCatching(block).onSuccess(result::complete).onFailure(result::completeExceptionally) + } + return runBlocking { result.await() } +} diff --git a/android/src/main/java/com/rngooglemapsplus/MapMarkerBuilder.kt b/android/src/main/java/com/rngooglemapsplus/MapMarkerBuilder.kt index 1f9b515..aaa924d 100644 --- a/android/src/main/java/com/rngooglemapsplus/MapMarkerBuilder.kt +++ b/android/src/main/java/com/rngooglemapsplus/MapMarkerBuilder.kt @@ -1,16 +1,26 @@ package com.rngooglemapsplus +import MarkerTag import android.graphics.Bitmap +import android.graphics.BitmapFactory import android.graphics.Canvas +import android.graphics.Typeface +import android.graphics.drawable.PictureDrawable +import android.util.Base64 import android.util.LruCache +import android.widget.ImageView +import android.widget.LinearLayout import androidx.core.graphics.createBitmap import com.caverock.androidsvg.SVG +import com.caverock.androidsvg.SVGExternalFileResolver import com.facebook.react.uimanager.PixelUtil.dpToPx +import com.facebook.react.uimanager.ThemedReactContext import com.google.android.gms.maps.model.BitmapDescriptor import com.google.android.gms.maps.model.BitmapDescriptorFactory import com.google.android.gms.maps.model.Marker import com.google.android.gms.maps.model.MarkerOptions import com.rngooglemapsplus.extensions.markerStyleEquals +import com.rngooglemapsplus.extensions.onUi import com.rngooglemapsplus.extensions.styleHash import com.rngooglemapsplus.extensions.toLatLng import kotlinx.coroutines.CoroutineScope @@ -20,9 +30,14 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.ensureActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.net.HttpURLConnection +import java.net.URL +import java.net.URLDecoder +import java.util.concurrent.ConcurrentHashMap import kotlin.coroutines.coroutineContext class MapMarkerBuilder( + val context: ThemedReactContext, private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default), ) { private val iconCache = @@ -33,7 +48,103 @@ class MapMarkerBuilder( ): Int = 1 } - private val jobsById = mutableMapOf() + private val jobsById = ConcurrentHashMap() + + init { + // / TODO: refactor with androidsvg 1.5 release + SVG.registerExternalFileResolver( + object : SVGExternalFileResolver() { + override fun resolveImage(filename: String?): Bitmap? { + if (filename.isNullOrBlank()) return null + + return runCatching { + when { + filename.startsWith("data:image/svg+xml") -> { + val svgContent = + if ("base64," in filename) { + val base64 = filename.substringAfter("base64,") + String(Base64.decode(base64, Base64.DEFAULT), Charsets.UTF_8) + } else { + URLDecoder.decode(filename.substringAfter(","), "UTF-8") + } + + val svg = SVG.getFromString(svgContent) + val width = (svg.documentWidth.takeIf { it > 0 } ?: 128f).toInt() + val height = (svg.documentHeight.takeIf { it > 0 } ?: 128f).toInt() + + createBitmap(width, height).apply { + Canvas(this).also(svg::renderToCanvas) + } + } + + filename.startsWith("http://") || filename.startsWith("https://") -> { + val conn = + (URL(filename).openConnection() as HttpURLConnection).apply { + connectTimeout = 5000 + readTimeout = 5000 + requestMethod = "GET" + instanceFollowRedirects = true + } + conn.connect() + + val contentType = conn.contentType ?: "" + val result = + if (contentType.contains("svg") || filename.endsWith(".svg")) { + val svgText = conn.inputStream.bufferedReader().use { it.readText() } + val innerSvg = SVG.getFromString(svgText) + val w = innerSvg.documentWidth.takeIf { it > 0 } ?: 128f + val h = innerSvg.documentHeight.takeIf { it > 0 } ?: 128f + val bmp = createBitmap(w.toInt(), h.toInt()) + val canvas = Canvas(bmp) + innerSvg.renderToCanvas(canvas) + bmp + } else { + conn.inputStream.use { BitmapFactory.decodeStream(it) } + } + + conn.disconnect() + result + } + + else -> null + } + }.getOrNull() + } + + override fun resolveFont( + fontFamily: String?, + fontWeight: Int, + fontStyle: String?, + ): Typeface? { + if (fontFamily.isNullOrBlank()) return null + + return runCatching { + val assetManager = context.assets + + val candidates = + listOf( + "fonts/$fontFamily.ttf", + "fonts/$fontFamily.otf", + ) + + for (path in candidates) { + try { + return Typeface.createFromAsset(assetManager, path) + } catch (_: Throwable) { + // / ignore + } + } + + Typeface.create(fontFamily, Typeface.NORMAL) + }.getOrElse { + Typeface.create(fontFamily, fontWeight) + } + } + + override fun isFormatSupported(mimeType: String?): Boolean = mimeType?.startsWith("image/") == true + }, + ) + } fun build( m: RNMarker, @@ -57,7 +168,7 @@ class MapMarkerBuilder( prev: RNMarker, next: RNMarker, marker: Marker, - ) { + ) = onUi { if (prev.coordinate.latitude != next.coordinate.latitude || prev.coordinate.longitude != next.coordinate.longitude ) { @@ -132,6 +243,10 @@ class MapMarkerBuilder( if (prev.zIndex != next.zIndex) { marker.zIndex = next.zIndex?.toFloat() ?: 0f } + + if (prev.infoWindowIconSvg != next.infoWindowIconSvg) { + marker.tag = MarkerTag(id = next.id, iconSvg = next.infoWindowIconSvg) + } } fun buildIconAsync( @@ -189,6 +304,31 @@ class MapMarkerBuilder( iconCache.evictAll() } + fun buildInfoWindow(iconSvg: RNMarkerSvg?): ImageView? { + val iconSvg = iconSvg ?: return null + + val svgView = + ImageView(context).apply { + layoutParams = + LinearLayout.LayoutParams( + iconSvg.width.dpToPx().toInt(), + iconSvg.height.dpToPx().toInt(), + ) + } + + try { + val svg = SVG.getFromString(iconSvg.svgString) + svg.setDocumentWidth(iconSvg.width.dpToPx()) + svg.setDocumentHeight(iconSvg.height.dpToPx()) + val drawable = PictureDrawable(svg.renderToPicture()) + svgView.setImageDrawable(drawable) + } catch (e: Exception) { + return null + } + + return svgView + } + private suspend fun renderBitmap(m: RNMarker): Bitmap? { m.iconSvg ?: return null diff --git a/android/src/main/java/com/rngooglemapsplus/MapPolygonBuilder.kt b/android/src/main/java/com/rngooglemapsplus/MapPolygonBuilder.kt index 9601d2c..b8079bd 100644 --- a/android/src/main/java/com/rngooglemapsplus/MapPolygonBuilder.kt +++ b/android/src/main/java/com/rngooglemapsplus/MapPolygonBuilder.kt @@ -4,6 +4,7 @@ import android.graphics.Color import com.facebook.react.uimanager.PixelUtil.dpToPx import com.google.android.gms.maps.model.Polygon import com.google.android.gms.maps.model.PolygonOptions +import com.rngooglemapsplus.extensions.onUi import com.rngooglemapsplus.extensions.toColor import com.rngooglemapsplus.extensions.toLatLng @@ -30,7 +31,7 @@ class MapPolygonBuilder { prev: RNPolygon, next: RNPolygon, poly: Polygon, - ) { + ) = onUi { val coordsChanged = prev.coordinates.size != next.coordinates.size || !prev.coordinates.zip(next.coordinates).all { (a, b) -> diff --git a/android/src/main/java/com/rngooglemapsplus/MapPolylineBuilder.kt.kt b/android/src/main/java/com/rngooglemapsplus/MapPolylineBuilder.kt.kt index 2802570..681682a 100644 --- a/android/src/main/java/com/rngooglemapsplus/MapPolylineBuilder.kt.kt +++ b/android/src/main/java/com/rngooglemapsplus/MapPolylineBuilder.kt.kt @@ -9,6 +9,7 @@ import com.google.android.gms.maps.model.Polyline 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.onUi import com.rngooglemapsplus.extensions.toColor import com.rngooglemapsplus.extensions.toLatLng @@ -34,7 +35,7 @@ class MapPolylineBuilder { prev: RNPolyline, next: RNPolyline, polyline: Polyline, - ) { + ) = onUi { val coordsChanged = prev.coordinates.size != next.coordinates.size || !prev.coordinates.zip(next.coordinates).all { (a, b) -> diff --git a/android/src/main/java/com/rngooglemapsplus/MapUrlTileOverlayBuilder.kt b/android/src/main/java/com/rngooglemapsplus/MapUrlTileOverlayBuilder.kt new file mode 100644 index 0000000..36359fa --- /dev/null +++ b/android/src/main/java/com/rngooglemapsplus/MapUrlTileOverlayBuilder.kt @@ -0,0 +1,40 @@ +package com.rngooglemapsplus + +import com.google.android.gms.maps.model.TileOverlayOptions +import com.google.android.gms.maps.model.UrlTileProvider +import java.net.URL + +class MapUrlTileOverlayBuilder { + fun build(t: RNUrlTileOverlay): TileOverlayOptions { + val provider = + object : UrlTileProvider( + t.tileSize.toInt(), + t.tileSize.toInt(), + ) { + override fun getTileUrl( + x: Int, + y: Int, + zoom: Int, + ): URL? { + val url = + t.url + .replace("{x}", x.toString()) + .replace("{y}", y.toString()) + .replace("{z}", zoom.toString()) + + return try { + URL(url) + } catch (e: Exception) { + null + } + } + } + + val opts = TileOverlayOptions().tileProvider(provider) + + t.fadeIn?.let { opts.fadeIn(it) } + t.zIndex?.let { opts.zIndex(it.toFloat()) } + t.opacity?.let { opts.transparency(1f - it.toFloat()) } + return opts + } +} diff --git a/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt b/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt index f03f538..556f385 100644 --- a/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt +++ b/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt @@ -1,7 +1,7 @@ package com.rngooglemapsplus +import MarkerTag import com.facebook.proguard.annotations.DoNotStrip -import com.facebook.react.bridge.UiThreadUtil import com.facebook.react.uimanager.ThemedReactContext import com.google.android.gms.maps.GoogleMapOptions import com.google.android.gms.maps.model.MapStyleOptions @@ -14,6 +14,7 @@ import com.rngooglemapsplus.extensions.polylineEquals import com.rngooglemapsplus.extensions.toCameraPosition import com.rngooglemapsplus.extensions.toCompressFormat import com.rngooglemapsplus.extensions.toFileExtension +import com.rngooglemapsplus.extensions.toGoogleMapType import com.rngooglemapsplus.extensions.toLatLngBounds import com.rngooglemapsplus.extensions.toMapColorScheme import com.rngooglemapsplus.extensions.toSize @@ -28,11 +29,12 @@ class RNGoogleMapsPlusView( private var locationHandler = LocationHandler(context) private var playServiceHandler = PlayServicesHandler(context) - private val markerBuilder = MapMarkerBuilder() + private val markerBuilder = MapMarkerBuilder(context) private val polylineBuilder = MapPolylineBuilder() private val polygonBuilder = MapPolygonBuilder() private val circleBuilder = MapCircleBuilder() private val heatmapBuilder = MapHeatmapBuilder() + private val urlTileOverlayBuilder = MapUrlTileOverlayBuilder() override val view = GoogleMapsViewImpl(context, locationHandler, playServiceHandler, markerBuilder) @@ -128,9 +130,7 @@ class RNGoogleMapsPlusView( set(value) { if (field == value) return field = value - value?.let { - view.mapType = it.value - } + view.mapType = value?.toGoogleMapType() } override var markers: Array? = null @@ -150,12 +150,19 @@ class RNGoogleMapsPlusView( when { prev == null -> markerBuilder.buildIconAsync(id, next) { icon -> - view.addMarker(id, markerBuilder.build(next, icon)) + view.addMarker( + id, + markerBuilder.build(next, icon), + MarkerTag( + id = id, + iconSvg = next.infoWindowIconSvg, + ), + ) } !prev.markerEquals(next) -> view.updateMarker(id) { marker -> - onUi { markerBuilder.update(prev, next, marker) } + markerBuilder.update(prev, next, marker) } } } @@ -179,7 +186,7 @@ class RNGoogleMapsPlusView( !prev.polylineEquals(next) -> view.updatePolyline(id) { polyline -> - onUi { polylineBuilder.update(prev, next, polyline) } + polylineBuilder.update(prev, next, polyline) } } } @@ -204,7 +211,7 @@ class RNGoogleMapsPlusView( !prev.polygonEquals(next) -> view.updatePolygon(id) { polygon -> - onUi { polygonBuilder.update(prev, next, polygon) } + polygonBuilder.update(prev, next, polygon) } } } @@ -229,7 +236,7 @@ class RNGoogleMapsPlusView( !prev.circleEquals(next) -> view.updateCircle(id) { circle -> - onUi { circleBuilder.update(prev, next, circle) } + circleBuilder.update(prev, next, circle) } } } @@ -264,6 +271,21 @@ class RNGoogleMapsPlusView( } } + override var urlTileOverlays: Array? = null + set(value) { + if (field.contentEquals(value)) return + val prevById = field?.associateBy { it.id } ?: emptyMap() + val nextById = value?.associateBy { it.id } ?: emptyMap() + field = value + (prevById.keys - nextById.keys).forEach { id -> + view.removeUrlTileOverlay(id) + } + + nextById.forEach { (id, next) -> + view.addUrlTileOverlay(id, urlTileOverlayBuilder.build(next)) + } + } + override var locationConfig: RNLocationConfig? = null set(value) { if (field == value) return @@ -281,6 +303,11 @@ class RNGoogleMapsPlusView( view.onMapReady = cb } + override var onMapLoaded: ((RNRegion, RNCamera) -> Unit)? = null + set(cb) { + view.onMapLoaded = cb + } + override var onLocationUpdate: ((RNLocation) -> Unit)? = null set(cb) { view.onLocationUpdate = cb @@ -296,37 +323,47 @@ class RNGoogleMapsPlusView( view.onMapPress = cb } - override var onMarkerPress: ((String?) -> Unit)? = null + override var onMapLongPress: ((RNLatLng) -> Unit)? = null + set(cb) { + view.onMapLongPress = cb + } + + override var onMarkerPress: ((String) -> Unit)? = null set(cb) { view.onMarkerPress = cb } - override var onPolylinePress: ((String?) -> Unit)? = null + override var onPoiPress: ((String, String, RNLatLng) -> Unit)? = null + set(cb) { + view.onPoiPress = cb + } + + override var onPolylinePress: ((String) -> Unit)? = null set(cb) { view.onPolylinePress = cb } - override var onPolygonPress: ((String?) -> Unit)? = null + override var onPolygonPress: ((String) -> Unit)? = null set(cb) { view.onPolygonPress = cb } - override var onCirclePress: ((String?) -> Unit)? = null + override var onCirclePress: ((String) -> Unit)? = null set(cb) { view.onCirclePress = cb } - override var onMarkerDragStart: ((String?, RNLatLng) -> Unit)? = null + override var onMarkerDragStart: ((String, RNLatLng) -> Unit)? = null set(cb) { view.onMarkerDragStart = cb } - override var onMarkerDrag: ((String?, RNLatLng) -> Unit)? = null + override var onMarkerDrag: ((String, RNLatLng) -> Unit)? = null set(cb) { view.onMarkerDrag = cb } - override var onMarkerDragEnd: ((String?, RNLatLng) -> Unit)? = null + override var onMarkerDragEnd: ((String, RNLatLng) -> Unit)? = null set(cb) { view.onMarkerDragEnd = cb } @@ -341,6 +378,31 @@ class RNGoogleMapsPlusView( view.onIndoorLevelActivated = cb } + override var onInfoWindowPress: ((String) -> Unit)? = null + set(cb) { + view.onInfoWindowPress = cb + } + + override var onInfoWindowClose: ((String) -> Unit)? = null + set(cb) { + view.onInfoWindowClose = cb + } + + override var onInfoWindowLongPress: ((String) -> Unit)? = null + set(cb) { + view.onInfoWindowLongPress = cb + } + + override var onMyLocationPress: ((RNLocation) -> Unit)? = null + set(cb) { + view.onMyLocationPress = cb + } + + override var onMyLocationButtonPress: ((Boolean) -> Unit)? = null + set(cb) { + view.onMyLocationButtonPress = cb + } + override var onCameraChangeStart: ((RNRegion, RNCamera, Boolean) -> Unit)? = null set(cb) { view.onCameraChangeStart = cb @@ -356,19 +418,25 @@ class RNGoogleMapsPlusView( view.onCameraChangeComplete = cb } + override fun showMarkerInfoWindow(id: String) { + view.showMarkerInfoWindow(id) + } + + override fun hideMarkerInfoWindow(id: String) { + view.hideMarkerInfoWindow(id) + } + override fun setCamera( camera: RNCamera, animated: Boolean?, durationMs: Double?, ) { - onUi { - val current = view.currentCamera - view.setCamera( - camera.toCameraPosition(current), - animated == true, - durationMs?.toInt() ?: 3000, - ) - } + val current = view.currentCamera + view.setCamera( + camera.toCameraPosition(current), + animated == true, + durationMs?.toInt() ?: 3000, + ) } override fun setCameraToCoordinates( @@ -426,11 +494,3 @@ class RNGoogleMapsPlusView( override fun isGooglePlayServicesAvailable(): Boolean = playServiceHandler.isPlayServicesAvailable() } - -private inline fun onUi(crossinline block: () -> Unit) { - if (UiThreadUtil.isOnUiThread()) { - block() - } else { - UiThreadUtil.runOnUiThread { block() } - } -} diff --git a/android/src/main/java/com/rngooglemapsplus/extensions/BitmapExtension.kt b/android/src/main/java/com/rngooglemapsplus/extensions/BitmapExtension.kt new file mode 100644 index 0000000..9a366bb --- /dev/null +++ b/android/src/main/java/com/rngooglemapsplus/extensions/BitmapExtension.kt @@ -0,0 +1,35 @@ +package com.rngooglemapsplus.extensions + +import android.content.Context +import android.graphics.Bitmap +import android.util.Base64 +import android.util.Size +import androidx.core.graphics.scale +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.FileOutputStream + +fun Bitmap.encode( + context: Context, + targetSize: Size?, + format: String, + compressFormat: Bitmap.CompressFormat, + quality: Double, + asFile: Boolean, +): String? = + try { + targetSize?.let { scale(it.width, it.height) } + val output = ByteArrayOutputStream() + compress(compressFormat, (quality * 100).toInt().coerceIn(0, 100), output) + val bytes = output.toByteArray() + + if (asFile) { + val file = File(context.cacheDir, "snapshot_${System.currentTimeMillis()}.$format") + FileOutputStream(file).use { it.write(bytes) } + file.absolutePath + } else { + "data:image/$format;base64," + Base64.encodeToString(bytes, Base64.NO_WRAP) + } + } catch (_: Exception) { + null + } diff --git a/android/src/main/java/com/rngooglemapsplus/extensions/LatLngBounds.kt b/android/src/main/java/com/rngooglemapsplus/extensions/LatLngBounds.kt deleted file mode 100644 index a178c79..0000000 --- a/android/src/main/java/com/rngooglemapsplus/extensions/LatLngBounds.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.rngooglemapsplus.extensions - -import com.google.android.gms.maps.model.LatLngBounds -import com.rngooglemapsplus.RNRegion - -fun LatLngBounds.toRnRegion(): RNRegion { - val latDelta = northeast.latitude - southwest.latitude - val lngDelta = northeast.longitude - southwest.longitude - - return RNRegion( - center = center.toRnLatLng(), - latitudeDelta = latDelta, - longitudeDelta = lngDelta, - ) -} diff --git a/android/src/main/java/com/rngooglemapsplus/extensions/LatLngBoundsExtension.kt b/android/src/main/java/com/rngooglemapsplus/extensions/LatLngBoundsExtension.kt new file mode 100644 index 0000000..04a82e0 --- /dev/null +++ b/android/src/main/java/com/rngooglemapsplus/extensions/LatLngBoundsExtension.kt @@ -0,0 +1,41 @@ +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/android/src/main/java/com/rngooglemapsplus/extensions/MapObjectTagExtensions.kt b/android/src/main/java/com/rngooglemapsplus/extensions/MapObjectTagExtensions.kt new file mode 100644 index 0000000..d14e16c --- /dev/null +++ b/android/src/main/java/com/rngooglemapsplus/extensions/MapObjectTagExtensions.kt @@ -0,0 +1,84 @@ +import android.util.Log +import com.google.android.gms.maps.model.Circle +import com.google.android.gms.maps.model.Marker +import com.google.android.gms.maps.model.Polygon +import com.google.android.gms.maps.model.Polyline +import com.rngooglemapsplus.RNMarkerSvg + +sealed class MapObjectTag( + open val id: String, +) + +data class MarkerTag( + override val id: String, + val iconSvg: RNMarkerSvg? = null, +) : MapObjectTag(id) + +data class PolylineTag( + override val id: String, +) : MapObjectTag(id) + +data class PolygonTag( + override val id: String, +) : MapObjectTag(id) + +data class CircleTag( + override val id: String, +) : MapObjectTag(id) + +val Marker.tagData: MarkerTag + get() = + (tag as? MarkerTag) ?: run { + Log.w("MapTag", "Marker without tag detected at $position") + val fallback = MarkerTag(id = "unknown") + tag = fallback + fallback + } + +val Marker.idTag: String + get() = tagData.id + +var Polyline.tagData: PolylineTag + get() = + (tag as? PolylineTag) ?: run { + Log.w("MapTag", "Polyline without tag detected") + val fallback = PolylineTag(id = "unknown") + tag = fallback + fallback + } + set(value) { + tag = value + } + +val Polyline.idTag: String + get() = tagData.id + +var Polygon.tagData: PolygonTag + get() = + (tag as? PolygonTag) ?: run { + Log.w("MapTag", "Polygon without tag detected") + val fallback = PolygonTag(id = "unknown") + tag = fallback + fallback + } + set(value) { + tag = value + } + +val Polygon.idTag: String + get() = tagData.id + +var Circle.tagData: CircleTag + get() = + (tag as? CircleTag) ?: run { + Log.w("MapTag", "Circle without tag detected") + val fallback = CircleTag(id = "unknown") + tag = fallback + fallback + } + set(value) { + tag = value + } + +val Circle.idTag: String + get() = tagData.id diff --git a/android/src/main/java/com/rngooglemapsplus/extensions/RNLatLngBoundsExtension.kt b/android/src/main/java/com/rngooglemapsplus/extensions/RNLatLngBoundsExtension.kt index b073d0f..a5b2a66 100644 --- a/android/src/main/java/com/rngooglemapsplus/extensions/RNLatLngBoundsExtension.kt +++ b/android/src/main/java/com/rngooglemapsplus/extensions/RNLatLngBoundsExtension.kt @@ -6,12 +6,6 @@ import com.rngooglemapsplus.RNLatLngBounds fun RNLatLngBounds.toLatLngBounds(): LatLngBounds = LatLngBounds( - LatLng( - southWest.latitude, - southWest.longitude, - ), - LatLng( - northEast.latitude, - northEast.longitude, - ), + southwest.toLatLng(), + northeast.toLatLng(), ) diff --git a/android/src/main/java/com/rngooglemapsplus/extensions/RNMapTypeExtension.kt b/android/src/main/java/com/rngooglemapsplus/extensions/RNMapTypeExtension.kt new file mode 100644 index 0000000..f39e4db --- /dev/null +++ b/android/src/main/java/com/rngooglemapsplus/extensions/RNMapTypeExtension.kt @@ -0,0 +1,13 @@ +package com.rngooglemapsplus.extensions + +import com.google.android.gms.maps.GoogleMap +import com.rngooglemapsplus.RNMapType + +fun RNMapType.toGoogleMapType(): Int = + when (this) { + RNMapType.NONE -> GoogleMap.MAP_TYPE_NONE + RNMapType.NORMAL -> GoogleMap.MAP_TYPE_NORMAL + RNMapType.HYBRID -> GoogleMap.MAP_TYPE_HYBRID + RNMapType.SATELLITE -> GoogleMap.MAP_TYPE_SATELLITE + RNMapType.TERRAIN -> GoogleMap.MAP_TYPE_TERRAIN + } diff --git a/android/src/main/java/com/rngooglemapsplus/extensions/RNSize.kt b/android/src/main/java/com/rngooglemapsplus/extensions/RNSizeExtension.kt similarity index 100% rename from android/src/main/java/com/rngooglemapsplus/extensions/RNSize.kt rename to android/src/main/java/com/rngooglemapsplus/extensions/RNSizeExtension.kt diff --git a/android/src/main/java/com/rngooglemapsplus/extensions/RNSnapshotFormat.kt b/android/src/main/java/com/rngooglemapsplus/extensions/RNSnapshotFormatExtension.kt similarity index 100% rename from android/src/main/java/com/rngooglemapsplus/extensions/RNSnapshotFormat.kt rename to android/src/main/java/com/rngooglemapsplus/extensions/RNSnapshotFormatExtension.kt diff --git a/android/src/main/java/com/rngooglemapsplus/extensions/RNSnapshotResultType.kt b/android/src/main/java/com/rngooglemapsplus/extensions/RNSnapshotResultTypeExtension.kt similarity index 100% rename from android/src/main/java/com/rngooglemapsplus/extensions/RNSnapshotResultType.kt rename to android/src/main/java/com/rngooglemapsplus/extensions/RNSnapshotResultTypeExtension.kt diff --git a/android/src/main/java/com/rngooglemapsplus/extensions/VisibleRegionExtension.kt b/android/src/main/java/com/rngooglemapsplus/extensions/VisibleRegionExtension.kt new file mode 100644 index 0000000..9e8689c --- /dev/null +++ b/android/src/main/java/com/rngooglemapsplus/extensions/VisibleRegionExtension.kt @@ -0,0 +1,13 @@ +package com.rngooglemapsplus.extensions + +import com.google.android.gms.maps.model.VisibleRegion +import com.rngooglemapsplus.RNRegion + +fun VisibleRegion.toRnRegion(): RNRegion = + RNRegion( + nearLeft = nearLeft.toRnLatLng(), + nearRight = nearRight.toRnLatLng(), + farLeft = farLeft.toRnLatLng(), + farRight = farRight.toRnLatLng(), + latLngBounds = latLngBounds.toRnLatLngBounds(), + ) diff --git a/eslint.config.mjs b/eslint.config.mjs index e09360e..ae5fa81 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -21,6 +21,17 @@ export default defineConfig([ rules: { 'react/react-in-jsx-scope': 'off', 'prettier/prettier': 'error', + 'no-restricted-imports': [ + 'error', + { + patterns: [ + { + group: ['**/lib/**'], + message: 'Import only from: "react-native-google-maps-plus".', + }, + ], + }, + ], }, }, { diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index e80c4eb..158b3fe 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -13,7 +13,8 @@ android:allowBackup="false" android:theme="@style/AppTheme" android:usesCleartextTraffic="${usesCleartextTraffic}" - android:supportsRtl="true"> + android:supportsRtl="true" + android:hardwareAccelerated="true"> + + diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 11c296b..b75ae1d 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -2475,7 +2475,7 @@ PODS: - React-perflogger (= 0.82.1) - React-utils (= 0.82.1) - SocketRocket - - RNGestureHandler (2.28.0): + - RNGestureHandler (2.29.0): - boost - DoubleConversion - fast_float @@ -2629,7 +2629,7 @@ PODS: - RNWorklets - SocketRocket - Yoga - - RNScreens (4.17.1): + - RNScreens (4.18.0): - boost - DoubleConversion - fast_float @@ -2656,10 +2656,10 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNScreens/common (= 4.17.1) + - RNScreens/common (= 4.18.0) - SocketRocket - Yoga - - RNScreens/common (4.17.1): + - RNScreens/common (4.18.0): - boost - DoubleConversion - fast_float @@ -3119,10 +3119,10 @@ SPEC CHECKSUMS: ReactAppDependencyProvider: a45ef34bb22dc1c9b2ac1f74167d9a28af961176 ReactCodegen: 0bce2d209e2e802589f4c5ff76d21618200e74cb ReactCommon: 801eff8cb9c940c04d3a89ce399c343ee3eff654 - RNGestureHandler: e1cc4de7646eb557ad62d1271d8eac73c304a896 + RNGestureHandler: 332c71177cd5c856673c7e7875fa3f92ba5a5cc2 RNGoogleMapsPlus: b08637cb23bb9592c6e12d1497148e9fd98c35f2 RNReanimated: 8f0185df21f0dea34ee8c9611ba88c17a290ed9a - RNScreens: ccfcc2f7d9c0d458b7fc41b3f4f0bea054602b3a + RNScreens: 98771ad898d1c0528fc8139606bbacf5a2e9d237 RNWorklets: ab618bf7d1c7fd2cb793b9f0f39c3e29274b3ebf SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 SVGKit: 1ad7513f8c74d9652f94ed64ddecda1a23864dea diff --git a/example/package.json b/example/package.json index 095191f..b404cc6 100644 --- a/example/package.json +++ b/example/package.json @@ -12,18 +12,18 @@ }, "dependencies": { "@react-navigation/native": "7.1.18", - "@react-navigation/native-stack": "7.3.28", - "@react-navigation/stack": "7.4.10", + "@react-navigation/native-stack": "7.5.1", + "@react-navigation/stack": "7.5.0", "react": "19.1.1", "react-hook-form": "7.65.0", "react-native": "0.82.1", "react-native-clusterer": "5.0.1", - "react-native-gesture-handler": "2.28.0", + "react-native-gesture-handler": "2.29.0", "react-native-google-maps-plus": "workspace:*", "react-native-nitro-modules": "0.30.2", "react-native-reanimated": "4.1.3", "react-native-safe-area-context": "5.6.1", - "react-native-screens": "4.17.1", + "react-native-screens": "4.18.0", "react-native-worklets": "0.6.1", "superstruct": "2.0.2" }, diff --git a/example/src/App.tsx b/example/src/App.tsx index 310e45c..16e85b2 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -25,6 +25,8 @@ import CameraTestScreen from './screens/CameraTestScreen'; import type { RootStackParamList } from './types/navigation'; import SnapshotTestScreen from './screens/SnaptshotTestScreen'; import ClusteringScreen from './screens/ClsuteringScreen'; +import SvgMarkersScreen from './screens/SvgMarkersScreen'; +import UrlTileOverlay from './screens/UrlTileOverlay'; const Stack = createStackNavigator(); @@ -63,6 +65,11 @@ export default function App() { component={MarkersScreen} options={{ title: 'Markers' }} /> + + void>( + propCallback: T | undefined, + fallback?: (...args: Parameters) => void +) { + return callback({ + f: ((...args: Parameters) => { + propCallback?.(...args); + fallback?.(...args); + }) as T, + }); +} + export default function MapWrapper(props: Props) { const { children, ...rest } = props; const theme = useTheme(); const styles = useMemo(() => getThemedStyles(theme), [theme]); const layout = useSafeAreaInsets(); - const [mapReady, setMapReady] = React.useState(false); - const initialProps = useMemo( + const [mapLoaded, setMapLoaded] = React.useState(false); + const initialProps: RNInitialProps = useMemo( () => ({ camera: { center: { latitude: 37.7749, longitude: -122.4194 }, @@ -45,7 +62,7 @@ export default function MapWrapper(props: Props) { [] ); - const uiSettings = useMemo( + const uiSettings: RNMapUiSettings = useMemo( () => ({ allGesturesEnabled: true, compassEnabled: true, @@ -58,19 +75,24 @@ export default function MapWrapper(props: Props) { tiltEnabled: true, zoomControlsEnabled: true, zoomGesturesEnabled: true, + consumeOnMarkerPress: false, + consumeOnMyLocationButtonPress: false, }), [] ); - const mapPadding = useMemo(() => { + const mapPadding: RNMapPadding = useMemo(() => { return props.children ? { top: 20, left: 20, bottom: layout.bottom + 80, right: 20 } : { top: 20, left: 20, bottom: layout.bottom, right: 20 }; }, [layout.bottom, props.children]); - const mapZoomConfig = useMemo(() => ({ min: 0, max: 20 }), []); + const mapZoomConfig: RNMapZoomConfig = useMemo( + () => ({ min: 0, max: 20 }), + [] + ); - const locationConfig = useMemo( + const locationConfig: RNLocationConfig = useMemo( () => ({ android: { priority: RNAndroidLocationPriority.PRIORITY_HIGH_ACCURACY, @@ -96,6 +118,9 @@ export default function MapWrapper(props: Props) { }} initialProps={props.initialProps ?? initialProps} uiSettings={props.uiSettings ?? uiSettings} + myLocationEnabled={props.myLocationEnabled ?? true} + trafficEnabled={props.trafficEnabled ?? false} + indoorEnabled={props.indoorEnabled ?? false} style={[styles.map, props.style]} userInterfaceStyle={ props.userInterfaceStyle ?? (theme.dark ? 'dark' : 'light') @@ -104,105 +129,111 @@ export default function MapWrapper(props: Props) { mapZoomConfig={props.mapZoomConfig ?? mapZoomConfig} mapPadding={props.mapPadding ?? mapPadding} locationConfig={props.locationConfig ?? locationConfig} - onMapReady={callback( - props.onMapReady ?? { - f: (ready: boolean) => { - console.log('Map is ready! ' + ready); - setMapReady(true); - }, - } + onMapError={wrapCallback(props.onMapError, (e: RNMapErrorCode) => + console.log('Map error:', e) )} - onMapError={callback( - props.onMapError ?? { - f: (error: RNMapErrorCode) => console.log('Map error:', error), - } + onMapReady={wrapCallback(props.onMapReady, (ready: boolean) => + console.log('Map is ready:', ready) )} - onMapPress={callback( - props.onMapPress ?? { - f: (c: RNLatLng) => console.log('Map press:', c), + onMapLoaded={wrapCallback( + props.onMapLoaded, + (region: RNRegion, camera: RNCamera) => { + console.log('Map is loaded:', region, camera); + setMapLoaded(true); } )} - onMarkerPress={callback( - props.onMarkerPress ?? { - f: (id: string | undefined) => console.log('Marker press:', id), - } + onMapPress={wrapCallback(props.onMapPress, (c: RNLatLng) => + console.log('Map press:', c) )} - onPolylinePress={callback( - props.onPolylinePress ?? { - f: (id: string | undefined) => console.log('Polyline press:', id), - } + onMapLongPress={wrapCallback(props.onMapLongPress, (c: RNLatLng) => + console.log('Map long press:', c) )} - onPolygonPress={callback( - props.onPolygonPress ?? { - f: (id: string | undefined) => console.log('Polygon press:', id), - } + onPoiPress={wrapCallback( + props.onPoiPress, + (placeId: string, name: string, coordinate: RNLatLng) => + console.log('Poi press:', placeId, name, coordinate) )} - onCirclePress={callback( - props.onCirclePress ?? { - f: (id: string | undefined) => console.log('Circle press:', id), - } + onMarkerPress={wrapCallback(props.onMarkerPress, (id: string) => + console.log('Marker press:', id) )} - onMarkerDragStart={callback( - props.onMarkerDragStart ?? { - f: (id: string | undefined, latLng: RNLatLng) => - console.log('Marker drag start', id, latLng), - } + onPolylinePress={wrapCallback(props.onPolylinePress, (id: string) => + console.log('Polyline press:', id) )} - onMarkerDrag={callback( - props.onMarkerDrag ?? { - f: (id: string | undefined, latLng: RNLatLng) => - console.log('Marker drag', id, latLng), - } + onPolygonPress={wrapCallback(props.onPolygonPress, (id: string) => + console.log('Polygon press:', id) )} - onMarkerDragEnd={callback( - props.onMarkerDragEnd ?? { - f: (id: string | undefined, latLng: RNLatLng) => - console.log('Marker drag end', id, latLng), - } + onCirclePress={wrapCallback(props.onCirclePress, (id: string) => + console.log('Circle press:', id) )} - onIndoorBuildingFocused={callback( - props.onIndoorBuildingFocused ?? { - f: (building: RNIndoorBuilding) => - console.log('Indoor building focused', building), - } + onMarkerDragStart={wrapCallback( + props.onMarkerDragStart, + (id: string, latLng: RNLatLng) => + console.log('Marker drag start:', id, latLng) )} - onIndoorLevelActivated={callback( - props.onIndoorLevelActivated ?? { - f: (level: RNIndoorLevel) => - console.log('Indoor level activated', level), - } + onMarkerDrag={wrapCallback( + props.onMarkerDrag, + (id: string, latLng: RNLatLng) => + console.log('Marker drag:', id, latLng) )} - onCameraChangeStart={callback( - props.onCameraChangeStart ?? { - f: (r: RNRegion, cam: RNCamera, g: boolean) => - console.log('Cam start', r, cam, g), - } + onMarkerDragEnd={wrapCallback( + props.onMarkerDragEnd, + (id: string, latLng: RNLatLng) => + console.log('Marker drag end:', id, latLng) )} - onCameraChange={callback( - props.onCameraChange ?? { - f: (r: RNRegion, cam: RNCamera, g: boolean) => - console.log('Cam', r, cam, g), - } + onIndoorBuildingFocused={wrapCallback( + props.onIndoorBuildingFocused, + (building: RNIndoorBuilding) => + console.log('Indoor building focused:', building) )} - onCameraChangeComplete={callback( - props.onCameraChangeComplete ?? { - f: (r: RNRegion, cam: RNCamera, g: boolean) => - console.log('Cam complete', r, cam, g), - } + onIndoorLevelActivated={wrapCallback( + props.onIndoorLevelActivated, + (level: RNIndoorLevel) => + console.log('Indoor level activated:', level) )} - onLocationUpdate={callback( - props.onLocationUpdate ?? { - f: (l: RNLocation) => console.log('Location', l), - } + onInfoWindowPress={wrapCallback(props.onInfoWindowPress, (id: string) => + console.log('InfoWindow press:', id) )} - onLocationError={callback( - props.onLocationError ?? { - f: (e: RNLocationErrorCode) => console.log('Location error', e), - } + onInfoWindowClose={wrapCallback(props.onInfoWindowClose, (id: string) => + console.log('InfoWindow close:', id) + )} + onInfoWindowLongPress={wrapCallback( + props.onInfoWindowLongPress, + (id: string) => console.log('InfoWindow long press:', id) + )} + onMyLocationPress={wrapCallback( + props.onMyLocationPress, + (location: RNLocation) => console.log('MyLocation press:', location) + )} + onMyLocationButtonPress={wrapCallback( + props.onMyLocationButtonPress, + (pressed: boolean) => console.log('MyLocation button press:', pressed) + )} + onCameraChangeStart={wrapCallback( + props.onCameraChangeStart, + (r: RNRegion, cam: RNCamera, g: boolean) => + console.log('Camera start:', r, cam, g) + )} + onCameraChange={wrapCallback( + props.onCameraChange, + (r: RNRegion, cam: RNCamera, g: boolean) => + console.log('Camera changed:', r, cam, g) + )} + onCameraChangeComplete={wrapCallback( + props.onCameraChangeComplete, + (r: RNRegion, cam: RNCamera, g: boolean) => + console.log('Camera complete:', r, cam, g) + )} + onLocationUpdate={wrapCallback( + props.onLocationUpdate, + (l: RNLocation) => console.log('Location:', l) + )} + onLocationError={wrapCallback( + props.onLocationError, + (e: RNLocationErrorCode) => console.log('Location error:', e) )} /> {children} - {!mapReady && ( + {!mapLoaded && ( diff --git a/example/src/components/maptConfigDialog/MapConfigDialog.tsx b/example/src/components/maptConfigDialog/MapConfigDialog.tsx index 7be3737..a348214 100644 --- a/example/src/components/maptConfigDialog/MapConfigDialog.tsx +++ b/example/src/components/maptConfigDialog/MapConfigDialog.tsx @@ -1,13 +1,14 @@ -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { - Modal, - Pressable, - ScrollView, - StyleSheet, + View, Text, TextInput, - View, + StyleSheet, Alert, + Pressable, + Modal, + KeyboardAvoidingView, + Platform, } from 'react-native'; import { type Struct, validate } from 'superstruct'; import { useAppTheme } from '../../hooks/useAppTheme'; @@ -47,29 +48,32 @@ export default function MapConfigDialog({ setError(null); }, [initialData]); - const handleChange = (value: string) => { - setText(value); - try { - const parsed = parseWithUndefined(value); + const handleChange = useCallback( + (value: string) => { + setText(value); + try { + const parsed = parseWithUndefined(value); - if (validator) { - const [err] = validate(parsed, validator); - if (err) { - setIsValid(false); - setError(formatSuperstructError(err, validator)); - return; + if (validator) { + const [err] = validate(parsed, validator); + if (err) { + setIsValid(false); + setError(formatSuperstructError(err, validator)); + return; + } } - } - setIsValid(true); - setError(null); - } catch (e: any) { - setIsValid(false); - setError(e.message); - } - }; + setIsValid(true); + setError(null); + } catch (e: any) { + setIsValid(false); + setError(e.message); + } + }, + [validator] + ); - const handleSave = () => { + const handleSave = useCallback(() => { if (!isValid) { Alert.alert('Invalid JSON', error ?? 'Please fix JSON before saving.'); return; @@ -96,39 +100,54 @@ export default function MapConfigDialog({ } catch (e: any) { Alert.alert('Invalid JSON', e.message); } - }; + }, [isValid, error, text, validator, onSave, onClose]); + + const handleCancel = useCallback(() => { + setText(stringifyWithUndefined(initialData)); + setIsValid(true); + setError(null); + onClose(); + }, [initialData, onClose]); return ( - - + + - {title} - + + {title} + + + Cancel + + + Save + + + + + {!isValid && ( - + {error ?? 'Invalid JSON or schema mismatch'} )} - - - - Cancel - - - Save - - + ); } @@ -137,24 +156,64 @@ const getThemedStyles = (theme: any) => StyleSheet.create({ overlay: { flex: 1, - backgroundColor: theme.overlay, justifyContent: 'center', alignItems: 'center', + backgroundColor: theme.overlay, + paddingHorizontal: 16, }, dialog: { - width: '85%', - maxHeight: '85%', + width: '100%', + maxWidth: 700, backgroundColor: theme.bgPrimary, - borderRadius: 12, + borderRadius: 16, + maxHeight: '85%', flexShrink: 1, + overflow: 'visible', + }, + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingHorizontal: 16, + paddingVertical: 12, + borderBottomWidth: StyleSheet.hairlineWidth, + borderColor: theme.border, + backgroundColor: theme.bgSecondary, + }, + headerActions: { flexDirection: 'row', gap: 8 }, + headerButtonText: { + color: theme.textPrimary, + fontWeight: '600', + fontSize: 14, }, - scroll: { padding: 12 }, + cancelButton: { + paddingVertical: 8, + paddingHorizontal: 14, + borderRadius: 8, + backgroundColor: theme.cancelBg, + marginRight: 8, + }, + saveButton: { + paddingVertical: 8, + paddingHorizontal: 14, + borderRadius: 8, + backgroundColor: theme.bgAccent, + }, + buttonText: { color: theme.textOnAccent, fontWeight: '500' }, title: { - padding: 12, - fontSize: 18, + fontSize: 17, fontWeight: '600', color: theme.textPrimary, }, + body: { + flexShrink: 1, + padding: 16, + }, + bodyWithError: { + flexShrink: 1, + padding: 16, + paddingBottom: 50, + }, input: { borderWidth: 1, borderRadius: 8, @@ -164,34 +223,13 @@ const getThemedStyles = (theme: any) => color: theme.textPrimary, backgroundColor: theme.inputBg, fontFamily: 'monospace', + minHeight: 100, }, - multiline: { minHeight: 250, textAlignVertical: 'top' }, errorText: { marginTop: 6, color: theme.errorBorder, fontSize: 12, fontFamily: 'monospace', }, - error: { - borderColor: theme.errorBorder, - }, - actions: { - flexDirection: 'row', - justifyContent: 'flex-end', - padding: 12, - }, - cancelButton: { - paddingVertical: 8, - paddingHorizontal: 14, - borderRadius: 8, - backgroundColor: theme.cancelBg, - marginRight: 8, - }, - saveButton: { - paddingVertical: 8, - paddingHorizontal: 14, - borderRadius: 8, - backgroundColor: theme.bgAccent, - }, - buttonText: { color: theme.textOnAccent, fontWeight: '500' }, + error: { borderColor: theme.errorBorder }, }); diff --git a/example/src/components/maptConfigDialog/validator.ts b/example/src/components/maptConfigDialog/validator.ts index e017bc6..7c22f09 100644 --- a/example/src/components/maptConfigDialog/validator.ts +++ b/example/src/components/maptConfigDialog/validator.ts @@ -128,6 +128,8 @@ export const RNMapUiSettingsValidator = object({ tiltEnabled: optional(boolean()), zoomControlsEnabled: optional(boolean()), zoomGesturesEnabled: optional(boolean()), + consumeOnMarkerPress: optional(boolean()), + consumeOnMyLocationButtonPress: optional(boolean()), }); export const RNMapZoomConfigValidator = object({ @@ -167,6 +169,7 @@ export const RNMarkerValidator = object({ rotation: optional(number()), infoWindowAnchor: optional(RNPositionValidator), iconSvg: optional(RNMarkerSvgValidator), + infoWindowIconSvg: optional(RNMarkerSvgValidator), }); export const RNPolygonHoleValidator = object({ @@ -235,6 +238,15 @@ export const RNKMLayerValidator = object({ kmlString: string(), }); +export const RNUrlTileOverlayValidator = object({ + id: string(), + zIndex: optional(number()), + url: string(), + tileSize: number(), + opacity: optional(number()), + fadeIn: optional(boolean()), +}); + export const RNIndoorLevelValidator = object({ index: number(), name: optional(string()), @@ -342,6 +354,3 @@ if ( { type: 'literal', schema: 'default' }, ]; } - -export type RNBasicMapConfigType = - typeof RNBasicMapConfigValidator extends Struct ? O : never; diff --git a/example/src/screens/BasicMapScreen.tsx b/example/src/screens/BasicMapScreen.tsx index 526cdac..8f144b8 100644 --- a/example/src/screens/BasicMapScreen.tsx +++ b/example/src/screens/BasicMapScreen.tsx @@ -35,6 +35,8 @@ export default function BasicMapScreen() { tiltEnabled: true, zoomControlsEnabled: true, zoomGesturesEnabled: true, + consumeOnMarkerPress: false, + consumeOnMyLocationButtonPress: false, }, myLocationEnabled: true, buildingEnabled: undefined, diff --git a/example/src/screens/CameraTestScreen.tsx b/example/src/screens/CameraTestScreen.tsx index 91e2a22..1bff6de 100644 --- a/example/src/screens/CameraTestScreen.tsx +++ b/example/src/screens/CameraTestScreen.tsx @@ -23,8 +23,8 @@ export default function CameraTestScreen() { const bounds = useMemo( () => ({ - southWest: { latitude: 37.703, longitude: -122.527 }, - northEast: { latitude: 37.833, longitude: -122.356 }, + southwest: { latitude: 37.703, longitude: -122.527 }, + northeast: { latitude: 37.833, longitude: -122.356 }, }), [] ); diff --git a/example/src/screens/CirclesScreen.tsx b/example/src/screens/CirclesScreen.tsx index 3379c0a..f3be49e 100644 --- a/example/src/screens/CirclesScreen.tsx +++ b/example/src/screens/CirclesScreen.tsx @@ -13,23 +13,23 @@ import { useHeaderButton } from '../hooks/useHeaderButton'; export default function CirclesScreen() { const mapRef = useRef(null); const navigation = useNavigation(); - const [circle, setCircle] = useState(undefined); + const [circles, setCircles] = useState(undefined); const [dialogVisible, setDialogVisible] = useState(true); - useHeaderButton(navigation, circle ? 'Edit' : 'Add', () => + useHeaderButton(navigation, circles ? 'Edit' : 'Add', () => setDialogVisible(true) ); return ( <> - + visible={dialogVisible} title="Edit circle" initialData={makeCircle(1)} validator={RNCircleValidator} onClose={() => setDialogVisible(false)} - onSave={(c) => setCircle(c)} + onSave={(c) => setCircles([c])} /> ); diff --git a/example/src/screens/ClsuteringScreen.tsx b/example/src/screens/ClsuteringScreen.tsx index 4fdbd0e..6344fc7 100644 --- a/example/src/screens/ClsuteringScreen.tsx +++ b/example/src/screens/ClsuteringScreen.tsx @@ -5,10 +5,12 @@ import type { GoogleMapsViewRef, RNMarker, RNMarkerSvg, + RNRegion, } from 'react-native-google-maps-plus'; import type { Supercluster } from 'react-native-clusterer'; import { useClusterer } from 'react-native-clusterer'; import { randomCoordinates } from '../utils/mapGenerators'; +import { rnRegionToRegion } from '../utils/mapUtils'; export default function ClusteringScreen() { const mapRef = useRef(null); @@ -17,14 +19,8 @@ export default function ClusteringScreen() { randomCoordinates(37.7749, -122.4194, 0.2) ) ); - const [region, setRegion] = useState({ - center: { - latitude: 37.7749, - longitude: -122.4194, - }, - latitudeDelta: 0.4, - longitudeDelta: 0.4, - }); + + const [region, setRegion] = useState(null); const mapDimensions = useMemo(() => ({ width: 400, height: 800 }), []); @@ -66,15 +62,7 @@ export default function ClusteringScreen() { [coordinates] ); - const clusterRegion = useMemo( - () => ({ - latitude: region.center.latitude, - longitude: region.center.longitude, - latitudeDelta: region.latitudeDelta, - longitudeDelta: region.longitudeDelta, - }), - [region] - ); + const clusterRegion = useMemo(() => rnRegionToRegion(region), [region]); const clusterOptions = useMemo( () => ({ radius: 60, maxZoom: 16, minZoom: 0 }), @@ -89,11 +77,14 @@ export default function ClusteringScreen() { ); const markers: RNMarker[] = useMemo(() => { - return points.map((feature, i) => { + return points.map((feature) => { const [lng, lat] = feature.geometry.coordinates as [number, number]; const isCluster = 'cluster' in feature.properties; - // @ts-ignore + const id = isCluster + ? `cluster-${feature.properties.cluster_id}` + : feature.properties.id; const count = feature.properties?.point_count ?? 0; + const icon = isCluster ? { width: 36, @@ -107,10 +98,8 @@ export default function ClusteringScreen() { } : feature.properties.svgIcon; - console.log(feature); - return { - id: feature.id?.toString() ?? i.toString(), + id, coordinate: { latitude: lat, longitude: lng }, iconSvg: icon, } as RNMarker; @@ -118,7 +107,12 @@ export default function ClusteringScreen() { }, [points]); return ( - + setRegion(r)} + onCameraChange={(r: RNRegion) => setRegion(r)} + > ); diff --git a/example/src/screens/HeatmapScreen.tsx b/example/src/screens/HeatmapScreen.tsx index ba6b35a..e7b683a 100644 --- a/example/src/screens/HeatmapScreen.tsx +++ b/example/src/screens/HeatmapScreen.tsx @@ -13,23 +13,23 @@ import { useHeaderButton } from '../hooks/useHeaderButton'; export default function HeatmapScreen() { const mapRef = useRef(null); const navigation = useNavigation(); - const [heatmap, setHeatmap] = useState(undefined); + const [heatmaps, setHeatmaps] = useState(undefined); const [dialogVisible, setDialogVisible] = useState(true); - useHeaderButton(navigation, heatmap ? 'Edit' : 'Add', () => + useHeaderButton(navigation, heatmaps ? 'Edit' : 'Add', () => setDialogVisible(true) ); return ( <> - + visible={dialogVisible} title="Edit heatmap" initialData={makeHeatmap(1)} validator={RNHeatmapValidator} onClose={() => setDialogVisible(false)} - onSave={(c) => setHeatmap(c)} + onSave={(c) => setHeatmaps([c])} /> ); diff --git a/example/src/screens/HomeScreen.tsx b/example/src/screens/HomeScreen.tsx index 166f537..580ed4a 100644 --- a/example/src/screens/HomeScreen.tsx +++ b/example/src/screens/HomeScreen.tsx @@ -7,11 +7,13 @@ import { useAppTheme } from '../hooks/useAppTheme'; const screens = [ { name: 'BasicMap', title: 'Basic Map' }, { name: 'Markers', title: 'Markers' }, + { name: 'SvgMarkers', title: 'SVG Markers' }, { name: 'Polygons', title: 'Polygons' }, { name: 'Polylines', title: 'Polylines' }, { name: 'Circles', title: 'Circles' }, { name: 'Heatmap', title: 'Heatmap' }, { name: 'KmlLayer', title: 'KML Layer' }, + { name: 'UrlTileOverlay', title: 'Url Tile Overlay' }, { name: 'Location', title: 'Location & Permissions' }, { name: 'CustomStyle', title: 'Custom Map Style' }, { name: 'IndoorLevelMap', title: 'Indoor Level Map' }, diff --git a/example/src/screens/KmlLayerScreen.tsx b/example/src/screens/KmlLayerScreen.tsx index f3cf25a..cd4f376 100644 --- a/example/src/screens/KmlLayerScreen.tsx +++ b/example/src/screens/KmlLayerScreen.tsx @@ -13,23 +13,25 @@ import { useHeaderButton } from '../hooks/useHeaderButton'; export default function KmlLayerScreen() { const mapRef = useRef(null); const navigation = useNavigation(); - const [kmlLayer, setKmlLayer] = useState(undefined); + const [kmlLayers, setKmlLayers] = useState( + undefined + ); const [dialogVisible, setDialogVisible] = useState(true); - useHeaderButton(navigation, kmlLayer ? 'Edit' : 'Add', () => + useHeaderButton(navigation, kmlLayers ? 'Edit' : 'Add', () => setDialogVisible(true) ); return ( <> - + visible={dialogVisible} title="Edit KML layer" initialData={{ id: '1', kmlString: kmlString }} validator={RNKMLayerValidator} onClose={() => setDialogVisible(false)} - onSave={(c) => setKmlLayer(c)} + onSave={(c) => setKmlLayers([c])} /> ); diff --git a/example/src/screens/MarkersScreen.tsx b/example/src/screens/MarkersScreen.tsx index 880c7df..0c240c2 100644 --- a/example/src/screens/MarkersScreen.tsx +++ b/example/src/screens/MarkersScreen.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useState } from 'react'; +import React, { useMemo, useRef, useState } from 'react'; import MapWrapper from '../components/MapWrapper'; import { makeMarker } from '../utils/mapGenerators'; import type { @@ -9,27 +9,52 @@ import MapConfigDialog from '../components/maptConfigDialog/MapConfigDialog'; import { useNavigation } from '@react-navigation/native'; import { RNMarkerValidator } from '../components/maptConfigDialog/validator'; import { useHeaderButton } from '../hooks/useHeaderButton'; +import type { RNMapUiSettings } from 'react-native-google-maps-plus'; export default function MarkersScreen() { const mapRef = useRef(null); const navigation = useNavigation(); - const [marker, setMarker] = useState(undefined); + const [markers, setMarkers] = useState(undefined); const [dialogVisible, setDialogVisible] = useState(true); - useHeaderButton(navigation, marker ? 'Edit' : 'Add', () => + const uiSettings: RNMapUiSettings = useMemo( + () => ({ + allGesturesEnabled: true, + compassEnabled: true, + indoorLevelPickerEnabled: true, + mapToolbarEnabled: true, + myLocationButtonEnabled: true, + rotateEnabled: true, + scrollEnabled: true, + scrollDuringRotateOrZoomEnabled: true, + tiltEnabled: true, + zoomControlsEnabled: true, + zoomGesturesEnabled: true, + consumeOnMarkerPress: true, + consumeOnMyLocationButtonPress: false, + }), + [] + ); + + useHeaderButton(navigation, markers ? 'Edit' : 'Add', () => setDialogVisible(true) ); return ( <> - + mapRef.current?.showMarkerInfoWindow(id)} + /> visible={dialogVisible} title="Edit marker" initialData={makeMarker(1)} validator={RNMarkerValidator} onClose={() => setDialogVisible(false)} - onSave={(c) => setMarker(c)} + onSave={(c) => setMarkers([c])} /> ); diff --git a/example/src/screens/PolygonsScreen.tsx b/example/src/screens/PolygonsScreen.tsx index dfd2ec9..3096e64 100644 --- a/example/src/screens/PolygonsScreen.tsx +++ b/example/src/screens/PolygonsScreen.tsx @@ -13,23 +13,23 @@ import { useHeaderButton } from '../hooks/useHeaderButton'; export default function PolygonsScreen() { const mapRef = useRef(null); const navigation = useNavigation(); - const [polygon, setPolygon] = useState(undefined); + const [polygons, setPolygons] = useState(undefined); const [dialogVisible, setDialogVisible] = useState(true); - useHeaderButton(navigation, polygon ? 'Edit' : 'Add', () => + useHeaderButton(navigation, polygons ? 'Edit' : 'Add', () => setDialogVisible(true) ); return ( <> - + visible={dialogVisible} title="Edit polygon" initialData={makePolygon(1)} validator={RNPolygonValidator} onClose={() => setDialogVisible(false)} - onSave={(c) => setPolygon(c)} + onSave={(c) => setPolygons([c])} /> ); diff --git a/example/src/screens/PolylinesScreen.tsx b/example/src/screens/PolylinesScreen.tsx index e697fe5..f50c96c 100644 --- a/example/src/screens/PolylinesScreen.tsx +++ b/example/src/screens/PolylinesScreen.tsx @@ -13,23 +13,25 @@ import { useHeaderButton } from '../hooks/useHeaderButton'; export default function PolylinesScreen() { const mapRef = useRef(null); const navigation = useNavigation(); - const [polyline, setPolyline] = useState(undefined); + const [polylines, setPolylines] = useState( + undefined + ); const [dialogVisible, setDialogVisible] = useState(true); - useHeaderButton(navigation, polyline ? 'Edit' : 'Add', () => + useHeaderButton(navigation, polylines ? 'Edit' : 'Add', () => setDialogVisible(true) ); return ( <> - + visible={dialogVisible} title="Edit polyline" initialData={makePolyline(1)} validator={RNPolylineValidator} onClose={() => setDialogVisible(false)} - onSave={(c) => setPolyline(c)} + onSave={(c) => setPolylines([c])} /> ); diff --git a/example/src/screens/SvgMarkersScreen.tsx b/example/src/screens/SvgMarkersScreen.tsx new file mode 100644 index 0000000..b1d42d6 --- /dev/null +++ b/example/src/screens/SvgMarkersScreen.tsx @@ -0,0 +1,217 @@ +import React, { useRef, useState } from 'react'; +import MapWrapper from '../components/MapWrapper'; +import { randomCoordinates } from '../utils/mapGenerators'; +import type { + GoogleMapsViewRef, + RNMarker, +} from 'react-native-google-maps-plus'; + +function buildText(text: string) { + return ` + + ${text} + `; +} + +const MARKER_P_IMAGE_DATA_URI = ` + + + ${buildText('A')} + +`; + +const MARKER_P_IMAGE_SVG_BASE64 = ` + + + ${buildText('B')} + +`; + +const MARKER_P_IMAGE_BASE64_PNG = ` + + + ${buildText('C')} + +`; + +const MARKER_P_IMAGE_BASE64_JPG = ` + + + ${buildText('D')} + +`; + +const MARKER_P_IMAGE_INLINE_SVG = ` + + + ${buildText('E')} + +`; + +const MARKER_P_IMAGE_REMOTE_SVG = ` + + + ${buildText('F')} + +`; + +const MARKER_P_IMAGE_REMOTE_PNG = ` + + + ${buildText('G')} + + +`; + +const MARKER_P_IMAGE_REMOTE_JPG = ` + + + ${buildText('H')} + + +`; + +const MARKER_P_IMAGE_USE_SYMBOL = ` + + + + ${buildText('I')} + + + + +`; + +const MARKER_P_IMAGE_GRADIENT = ` + + + + + + + + + ${buildText('J')} + + +`; + +const MARKER_P_IMAGE_TRANSPARENT = ` + + + + ${buildText('K')} + +`; + +const MARKER_P_IMAGE_ICON = ` + + + + +`; + +const MARKERS = [ + MARKER_P_IMAGE_BASE64_JPG, + MARKER_P_IMAGE_DATA_URI, + MARKER_P_IMAGE_SVG_BASE64, + MARKER_P_IMAGE_BASE64_PNG, + MARKER_P_IMAGE_INLINE_SVG, + MARKER_P_IMAGE_REMOTE_SVG, + MARKER_P_IMAGE_REMOTE_PNG, + MARKER_P_IMAGE_REMOTE_JPG, + MARKER_P_IMAGE_USE_SYMBOL, + MARKER_P_IMAGE_GRADIENT, + MARKER_P_IMAGE_TRANSPARENT, + MARKER_P_IMAGE_ICON, +]; + +export default function SvgMarkersScreen() { + const mapRef = useRef(null); + + const [markers] = useState(() => + MARKERS.map((svg, i) => ({ + id: i.toString(), + zIndex: i, + coordinate: randomCoordinates(37.7749, -122.4194, 0.01), + iconSvg: { + width: 64, + height: 64, + svgString: svg, + }, + })) + ); + + return ; +} diff --git a/example/src/screens/UrlTileOverlay.tsx b/example/src/screens/UrlTileOverlay.tsx new file mode 100644 index 0000000..65883c1 --- /dev/null +++ b/example/src/screens/UrlTileOverlay.tsx @@ -0,0 +1,42 @@ +import React, { useRef, useState } from 'react'; +import MapWrapper from '../components/MapWrapper'; +import type { + GoogleMapsViewRef, + RNUrlTileOverlay, +} from 'react-native-google-maps-plus'; +import MapConfigDialog from '../components/maptConfigDialog/MapConfigDialog'; +import { useNavigation } from '@react-navigation/native'; +import { RNUrlTileOverlayValidator } from '../components/maptConfigDialog/validator'; +import { useHeaderButton } from '../hooks/useHeaderButton'; +import { makeUrlTileOverlay } from '../utils/mapGenerators'; + +export default function UrlTileOverlay() { + const mapRef = useRef(null); + const navigation = useNavigation(); + const [urlTileOverlays, setUrlTileOverlays] = useState< + RNUrlTileOverlay[] | undefined + >(undefined); + const [dialogVisible, setDialogVisible] = useState(true); + + useHeaderButton(navigation, urlTileOverlays ? 'Edit' : 'Add', () => + setDialogVisible(true) + ); + + return ( + <> + + + visible={dialogVisible} + title="Edit KML layer" + initialData={makeUrlTileOverlay(1)} + validator={RNUrlTileOverlayValidator} + onClose={() => setDialogVisible(false)} + onSave={(c) => setUrlTileOverlays([c])} + /> + + ); +} diff --git a/example/src/types/navigation.ts b/example/src/types/navigation.ts index 05f262b..c3ace83 100644 --- a/example/src/types/navigation.ts +++ b/example/src/types/navigation.ts @@ -3,11 +3,13 @@ export type RootStackParamList = { Blank: undefined; BasicMap: undefined; Markers: undefined; + SvgMarkers: undefined; Polygons: undefined; Polylines: undefined; Circles: undefined; Heatmap: undefined; KmlLayer: undefined; + UrlTileOverlay: undefined; Location: undefined; CustomStyle: undefined; IndoorLevelMap: undefined; diff --git a/example/src/utils/heatMapWeightData.ts b/example/src/utils/heatMapWeightData.ts new file mode 100644 index 0000000..2b76d0a --- /dev/null +++ b/example/src/utils/heatMapWeightData.ts @@ -0,0 +1,2502 @@ +export const weightData = [ + { + latitude: 37.77378784256098, + longitude: -122.406822975178, + weight: 6, + }, + { + latitude: 37.772382638833356, + longitude: -122.42028303792827, + weight: 7, + }, + { + latitude: 37.77246955046206, + longitude: -122.44123595449986, + weight: 7, + }, + { + latitude: 37.78684571321374, + longitude: -122.41794620531822, + weight: 8, + }, + { + latitude: 37.76072798737629, + longitude: -122.39587727487438, + weight: 3, + }, + { + latitude: 37.78915631885641, + longitude: -122.42045391817004, + weight: 7, + }, + { + latitude: 37.79674482993917, + longitude: -122.41951478398167, + weight: 7, + }, + { + latitude: 37.775719368270764, + longitude: -122.41830642754984, + weight: 8, + }, + { + latitude: 37.783897811571265, + longitude: -122.41271676790814, + weight: 5, + }, + { + latitude: 37.77945057371386, + longitude: -122.43029006756925, + weight: 7, + }, + { + latitude: 37.785920342042864, + longitude: -122.40684783269433, + weight: 5, + }, + { + latitude: 37.77531432154933, + longitude: -122.41759870629535, + weight: 3, + }, + { + latitude: 37.772267007585256, + longitude: -122.40883427340484, + weight: 6, + }, + { + latitude: 37.77924997509112, + longitude: -122.42541084363361, + weight: 6, + }, + { + latitude: 37.76529312874848, + longitude: -122.41711279231356, + weight: 4, + }, + { + latitude: 37.77543259331078, + longitude: -122.41868388020679, + weight: 1, + }, + { + latitude: 37.78535523749449, + longitude: -122.42222878732422, + weight: 3, + }, + { + latitude: 37.78914951751782, + longitude: -122.41607378637525, + weight: 6, + }, + { + latitude: 37.77435542679204, + longitude: -122.40154488683095, + weight: 7, + }, + { + latitude: 37.77708188945511, + longitude: -122.43864372168898, + weight: 3, + }, + { + latitude: 37.76692025473545, + longitude: -122.42040305138073, + weight: 2, + }, + { + latitude: 37.77375659389661, + longitude: -122.41953031403979, + weight: 1, + }, + { + latitude: 37.775133352105755, + longitude: -122.41576345775457, + weight: 8, + }, + { + latitude: 37.77965890950117, + longitude: -122.41143869871763, + weight: 6, + }, + { + latitude: 37.7763445544547, + longitude: -122.40242731738915, + weight: 5, + }, + { + latitude: 37.76374256967339, + longitude: -122.44042176836479, + weight: 6, + }, + { + latitude: 37.76815654031476, + longitude: -122.43264965541167, + weight: 5, + }, + { + latitude: 37.775208823267505, + longitude: -122.4237593915028, + weight: 8, + }, + { + latitude: 37.77129284849992, + longitude: -122.43620300396586, + weight: 7, + }, + { + latitude: 37.777091335416216, + longitude: -122.41240242484699, + weight: 2, + }, + { + latitude: 37.76202428167494, + longitude: -122.41852729235949, + weight: 3, + }, + { + latitude: 37.76714425480956, + longitude: -122.40936835762443, + weight: 3, + }, + { + latitude: 37.796235647066716, + longitude: -122.43286077855122, + weight: 6, + }, + { + latitude: 37.770647133415515, + longitude: -122.4202371630475, + weight: 2, + }, + { + latitude: 37.77241022432617, + longitude: -122.42764677745423, + weight: 2, + }, + { + latitude: 37.77325692619807, + longitude: -122.40406654566557, + weight: 8, + }, + { + latitude: 37.77520426246093, + longitude: -122.3999119229916, + weight: 6, + }, + { + latitude: 37.77090048647177, + longitude: -122.42359115852152, + weight: 1, + }, + { + latitude: 37.76936489133284, + longitude: -122.4397653487486, + weight: 6, + }, + { + latitude: 37.77277607864193, + longitude: -122.42809532840914, + weight: 4, + }, + { + latitude: 37.76368080461889, + longitude: -122.42099063572512, + weight: 3, + }, + { + latitude: 37.776268569865515, + longitude: -122.41565252565371, + weight: 2, + }, + { + latitude: 37.77858057812171, + longitude: -122.41624303297456, + weight: 2, + }, + { + latitude: 37.77753741787459, + longitude: -122.39905145380082, + weight: 4, + }, + { + latitude: 37.774501468863825, + longitude: -122.41250199688922, + weight: 5, + }, + { + latitude: 37.77237680493907, + longitude: -122.44381749526661, + weight: 2, + }, + { + latitude: 37.76438483266645, + longitude: -122.4193719108095, + weight: 7, + }, + { + latitude: 37.76872636724646, + longitude: -122.41300476351361, + weight: 7, + }, + { + latitude: 37.767042317328986, + longitude: -122.39320352609664, + weight: 8, + }, + { + latitude: 37.788463527463655, + longitude: -122.42406814579718, + weight: 4, + }, + { + latitude: 37.7891395268203, + longitude: -122.41470923252051, + weight: 2, + }, + { + latitude: 37.747822547256405, + longitude: -122.41640171921802, + weight: 3, + }, + { + latitude: 37.77621802925263, + longitude: -122.405338540677, + weight: 7, + }, + { + latitude: 37.76681693493331, + longitude: -122.4203896643528, + weight: 8, + }, + { + latitude: 37.77220492787513, + longitude: -122.4207521696673, + weight: 6, + }, + { + latitude: 37.78710250999388, + longitude: -122.43598582721208, + weight: 7, + }, + { + latitude: 37.76020179523137, + longitude: -122.40755715913652, + weight: 4, + }, + { + latitude: 37.7798251566263, + longitude: -122.41640713457474, + weight: 6, + }, + { + latitude: 37.776836277017665, + longitude: -122.41284892680926, + weight: 6, + }, + { + latitude: 37.78332542190169, + longitude: -122.42760640389221, + weight: 2, + }, + { + latitude: 37.78902464440884, + longitude: -122.40163879062129, + weight: 7, + }, + { + latitude: 37.777404286212175, + longitude: -122.40709713805302, + weight: 8, + }, + { + latitude: 37.781658717411645, + longitude: -122.41624820272416, + weight: 8, + }, + { + latitude: 37.771468383358325, + longitude: -122.43083375679574, + weight: 5, + }, + { + latitude: 37.77655116227659, + longitude: -122.42534822006057, + weight: 6, + }, + { + latitude: 37.779116810391606, + longitude: -122.41729655864313, + weight: 2, + }, + { + latitude: 37.786882283418606, + longitude: -122.42010033438022, + weight: 8, + }, + { + latitude: 37.77813712863214, + longitude: -122.43064531350824, + weight: 5, + }, + { + latitude: 37.76921190976568, + longitude: -122.41904840451664, + weight: 2, + }, + { + latitude: 37.76481169875827, + longitude: -122.41905588811476, + weight: 6, + }, + { + latitude: 37.77099144699575, + longitude: -122.43369810596451, + weight: 2, + }, + { + latitude: 37.77337887441396, + longitude: -122.41510771654166, + weight: 2, + }, + { + latitude: 37.77944163124869, + longitude: -122.4177372489261, + weight: 4, + }, + { + latitude: 37.75938190876475, + longitude: -122.42538179141081, + weight: 1, + }, + { + latitude: 37.767201866575974, + longitude: -122.42507241385496, + weight: 5, + }, + { + latitude: 37.760983262169695, + longitude: -122.41900265851676, + weight: 6, + }, + { + latitude: 37.78443763787066, + longitude: -122.42107197910327, + weight: 8, + }, + { + latitude: 37.773205852507516, + longitude: -122.437880068291, + weight: 5, + }, + { + latitude: 37.774862379058874, + longitude: -122.4326684934662, + weight: 2, + }, + { + latitude: 37.787219532378955, + longitude: -122.41834057094432, + weight: 4, + }, + { + latitude: 37.77439712023427, + longitude: -122.4039786656755, + weight: 2, + }, + { + latitude: 37.78252141546728, + longitude: -122.3965976914881, + weight: 8, + }, + { + latitude: 37.78915674723329, + longitude: -122.41564486570415, + weight: 6, + }, + { + latitude: 37.766232403255295, + longitude: -122.43011771256712, + weight: 5, + }, + { + latitude: 37.788263606240456, + longitude: -122.42349048093213, + weight: 4, + }, + { + latitude: 37.76823769966861, + longitude: -122.39864889903659, + weight: 3, + }, + { + latitude: 37.78905801368192, + longitude: -122.41890704715946, + weight: 2, + }, + { + latitude: 37.77300381322372, + longitude: -122.40848916062254, + weight: 6, + }, + { + latitude: 37.77349194482733, + longitude: -122.41748789449255, + weight: 7, + }, + { + latitude: 37.79003935810247, + longitude: -122.41936681858763, + weight: 6, + }, + { + latitude: 37.77052849925007, + longitude: -122.42016158683074, + weight: 6, + }, + { + latitude: 37.771215730497495, + longitude: -122.41811030929483, + weight: 4, + }, + { + latitude: 37.769236421135645, + longitude: -122.41838543588442, + weight: 2, + }, + { + latitude: 37.77215722020251, + longitude: -122.40150653399333, + weight: 2, + }, + { + latitude: 37.768089451361604, + longitude: -122.42826901447678, + weight: 3, + }, + { + latitude: 37.77604957729983, + longitude: -122.40120054758232, + weight: 3, + }, + { + latitude: 37.76600913737562, + longitude: -122.42344771573018, + weight: 3, + }, + { + latitude: 37.754018238823186, + longitude: -122.42002203006572, + weight: 7, + }, + { + latitude: 37.769225210913625, + longitude: -122.40099342890765, + weight: 6, + }, + { + latitude: 37.76067821945878, + longitude: -122.43250839283246, + weight: 5, + }, + { + latitude: 37.78077330446003, + longitude: -122.43612494823368, + weight: 8, + }, + { + latitude: 37.77050804291955, + longitude: -122.44801477443299, + weight: 6, + }, + { + latitude: 37.77145202746839, + longitude: -122.40692560212393, + weight: 7, + }, + { + latitude: 37.77284243174961, + longitude: -122.44313395518337, + weight: 6, + }, + { + latitude: 37.79146780201731, + longitude: -122.4270672111361, + weight: 4, + }, + { + latitude: 37.780212776234904, + longitude: -122.41883559850638, + weight: 3, + }, + { + latitude: 37.775241349640055, + longitude: -122.42000341966245, + weight: 3, + }, + { + latitude: 37.77939968318167, + longitude: -122.41663857640305, + weight: 1, + }, + { + latitude: 37.76553734268154, + longitude: -122.42247249291606, + weight: 3, + }, + { + latitude: 37.77346285054466, + longitude: -122.42461540333103, + weight: 7, + }, + { + latitude: 37.771117925022295, + longitude: -122.39756371311134, + weight: 7, + }, + { + latitude: 37.780903262140875, + longitude: -122.42204917900384, + weight: 4, + }, + { + latitude: 37.770098932433655, + longitude: -122.40797946096677, + weight: 3, + }, + { + latitude: 37.77623777338929, + longitude: -122.4385516580119, + weight: 7, + }, + { + latitude: 37.7907609646096, + longitude: -122.42349236643356, + weight: 6, + }, + { + latitude: 37.77686834755806, + longitude: -122.44168241405801, + weight: 5, + }, + { + latitude: 37.77398574422116, + longitude: -122.41129138097678, + weight: 7, + }, + { + latitude: 37.77489651001695, + longitude: -122.40366291538425, + weight: 6, + }, + { + latitude: 37.7564394608279, + longitude: -122.40771713393492, + weight: 3, + }, + { + latitude: 37.79215701595138, + longitude: -122.4327016682376, + weight: 5, + }, + { + latitude: 37.77210182473012, + longitude: -122.42579972689546, + weight: 1, + }, + { + latitude: 37.77524080222549, + longitude: -122.41032358680381, + weight: 4, + }, + { + latitude: 37.775814676791846, + longitude: -122.41385100221216, + weight: 6, + }, + { + latitude: 37.76245462706144, + longitude: -122.4059754844752, + weight: 4, + }, + { + latitude: 37.7748711838286, + longitude: -122.42063819053479, + weight: 6, + }, + { + latitude: 37.77973007387581, + longitude: -122.41783122718292, + weight: 2, + }, + { + latitude: 37.77699794820249, + longitude: -122.42679315338748, + weight: 2, + }, + { + latitude: 37.76511641443803, + longitude: -122.41095652409605, + weight: 5, + }, + { + latitude: 37.754939618476605, + longitude: -122.41616408191831, + weight: 5, + }, + { + latitude: 37.76014855738702, + longitude: -122.41589677720197, + weight: 6, + }, + { + latitude: 37.77313457147888, + longitude: -122.40341203983286, + weight: 3, + }, + { + latitude: 37.75886098581633, + longitude: -122.40283508803914, + weight: 6, + }, + { + latitude: 37.779125402297005, + longitude: -122.41231946094958, + weight: 3, + }, + { + latitude: 37.79133475474445, + longitude: -122.40553391953364, + weight: 4, + }, + { + latitude: 37.77591532783482, + longitude: -122.42073394136638, + weight: 3, + }, + { + latitude: 37.77326486479802, + longitude: -122.42246426124566, + weight: 6, + }, + { + latitude: 37.76564310759751, + longitude: -122.4361916551665, + weight: 3, + }, + { + latitude: 37.77636475689126, + longitude: -122.41743961079997, + weight: 1, + }, + { + latitude: 37.78638532428621, + longitude: -122.3924007136553, + weight: 3, + }, + { + latitude: 37.77880373018017, + longitude: -122.42319440705296, + weight: 4, + }, + { + latitude: 37.77719306190045, + longitude: -122.4127527662035, + weight: 2, + }, + { + latitude: 37.7690318045568, + longitude: -122.4236308183235, + weight: 6, + }, + { + latitude: 37.77634939780875, + longitude: -122.40906714169704, + weight: 5, + }, + { + latitude: 37.769478748657455, + longitude: -122.4041167545572, + weight: 4, + }, + { + latitude: 37.787263463699, + longitude: -122.42109647311885, + weight: 5, + }, + { + latitude: 37.767039533415776, + longitude: -122.4080301836325, + weight: 1, + }, + { + latitude: 37.77037756295241, + longitude: -122.43036334476311, + weight: 6, + }, + { + latitude: 37.77995621828489, + longitude: -122.43996938689068, + weight: 4, + }, + { + latitude: 37.77096993497601, + longitude: -122.40820465449968, + weight: 1, + }, + { + latitude: 37.791349194837856, + longitude: -122.419692287977, + weight: 5, + }, + { + latitude: 37.783726806049025, + longitude: -122.41939979241718, + weight: 4, + }, + { + latitude: 37.77221660372711, + longitude: -122.40405135066732, + weight: 3, + }, + { + latitude: 37.78604696372756, + longitude: -122.42337324599973, + weight: 2, + }, + { + latitude: 37.77038284783241, + longitude: -122.40698903656225, + weight: 5, + }, + { + latitude: 37.77759714556636, + longitude: -122.41447420848982, + weight: 1, + }, + { + latitude: 37.78055838898453, + longitude: -122.41905140655503, + weight: 3, + }, + { + latitude: 37.76688926586382, + longitude: -122.42461865747163, + weight: 2, + }, + { + latitude: 37.772566138061975, + longitude: -122.41190608506723, + weight: 4, + }, + { + latitude: 37.786232464688666, + longitude: -122.43149749163913, + weight: 4, + }, + { + latitude: 37.76998106453905, + longitude: -122.41746560769238, + weight: 4, + }, + { + latitude: 37.76751366582306, + longitude: -122.41996836601373, + weight: 8, + }, + { + latitude: 37.776270046369994, + longitude: -122.41949171745347, + weight: 5, + }, + { + latitude: 37.773809439748874, + longitude: -122.41315525564832, + weight: 6, + }, + { + latitude: 37.7752642343008, + longitude: -122.41615149462142, + weight: 2, + }, + { + latitude: 37.773226966840554, + longitude: -122.40255581651505, + weight: 6, + }, + { + latitude: 37.80122259292333, + longitude: -122.4243973203371, + weight: 3, + }, + { + latitude: 37.77573565550098, + longitude: -122.43077920793104, + weight: 6, + }, + { + latitude: 37.77357538122093, + longitude: -122.39669550908745, + weight: 3, + }, + { + latitude: 37.785684962509855, + longitude: -122.41923559922965, + weight: 1, + }, + { + latitude: 37.772235244936354, + longitude: -122.4202738658983, + weight: 6, + }, + { + latitude: 37.7574056340351, + longitude: -122.42407158344758, + weight: 3, + }, + { + latitude: 37.77096046665833, + longitude: -122.42699547006588, + weight: 5, + }, + { + latitude: 37.772520972206266, + longitude: -122.39446378239838, + weight: 2, + }, + { + latitude: 37.77483764162071, + longitude: -122.43744181699942, + weight: 4, + }, + { + latitude: 37.77857712823298, + longitude: -122.41815949239376, + weight: 2, + }, + { + latitude: 37.77369062900582, + longitude: -122.41452082958732, + weight: 6, + }, + { + latitude: 37.792876106284574, + longitude: -122.41580663486758, + weight: 3, + }, + { + latitude: 37.76514936313108, + longitude: -122.40903848075463, + weight: 2, + }, + { + latitude: 37.78341426590201, + longitude: -122.41831933057122, + weight: 7, + }, + { + latitude: 37.766739962036084, + longitude: -122.41051348562411, + weight: 3, + }, + { + latitude: 37.76282611131889, + longitude: -122.41418843699087, + weight: 5, + }, + { + latitude: 37.7998468433663, + longitude: -122.4102648517167, + weight: 2, + }, + { + latitude: 37.785363938399094, + longitude: -122.44056411066789, + weight: 5, + }, + { + latitude: 37.78305418602706, + longitude: -122.42451197874415, + weight: 8, + }, + { + latitude: 37.78640814512251, + longitude: -122.42886876340884, + weight: 7, + }, + { + latitude: 37.776760441470394, + longitude: -122.41765768879885, + weight: 7, + }, + { + latitude: 37.76309568984235, + longitude: -122.41928045846745, + weight: 6, + }, + { + latitude: 37.763146493271975, + longitude: -122.41738563777112, + weight: 7, + }, + { + latitude: 37.79497566943161, + longitude: -122.40981912786175, + weight: 3, + }, + { + latitude: 37.775171765405275, + longitude: -122.4350395884532, + weight: 8, + }, + { + latitude: 37.7761920137096, + longitude: -122.41468885230816, + weight: 4, + }, + { + latitude: 37.79508197483878, + longitude: -122.41298715088702, + weight: 2, + }, + { + latitude: 37.775944747654, + longitude: -122.40315886277943, + weight: 3, + }, + { + latitude: 37.76608972417146, + longitude: -122.43084069288484, + weight: 2, + }, + { + latitude: 37.7741960189644, + longitude: -122.41487136133895, + weight: 6, + }, + { + latitude: 37.784023350812525, + longitude: -122.39574242876768, + weight: 7, + }, + { + latitude: 37.778134957704516, + longitude: -122.40159550905531, + weight: 6, + }, + { + latitude: 37.772137842655894, + longitude: -122.40379613080636, + weight: 2, + }, + { + latitude: 37.77449710276983, + longitude: -122.39936903525388, + weight: 6, + }, + { + latitude: 37.77675841363019, + longitude: -122.41805844377792, + weight: 2, + }, + { + latitude: 37.775045082290866, + longitude: -122.404145083743, + weight: 8, + }, + { + latitude: 37.77250179350299, + longitude: -122.44812542808684, + weight: 7, + }, + { + latitude: 37.751223202016575, + longitude: -122.41900945927101, + weight: 2, + }, + { + latitude: 37.76841454895914, + longitude: -122.40998668104, + weight: 3, + }, + { + latitude: 37.77710376623778, + longitude: -122.43244551108448, + weight: 8, + }, + { + latitude: 37.77851062254193, + longitude: -122.41227849370621, + weight: 6, + }, + { + latitude: 37.77929427523975, + longitude: -122.41799669373864, + weight: 2, + }, + { + latitude: 37.796527556682555, + longitude: -122.42436740989613, + weight: 7, + }, + { + latitude: 37.77469654556772, + longitude: -122.41360098542646, + weight: 2, + }, + { + latitude: 37.74775445439251, + longitude: -122.42766892047354, + weight: 7, + }, + { + latitude: 37.77819135230904, + longitude: -122.41566346287705, + weight: 2, + }, + { + latitude: 37.78371575172997, + longitude: -122.40765308455259, + weight: 3, + }, + { + latitude: 37.78653671029188, + longitude: -122.40873424533699, + weight: 5, + }, + { + latitude: 37.77234107270425, + longitude: -122.42919107827942, + weight: 1, + }, + { + latitude: 37.777385078255584, + longitude: -122.41748137598996, + weight: 6, + }, + { + latitude: 37.77771157082447, + longitude: -122.41808024827536, + weight: 3, + }, + { + latitude: 37.74997058786855, + longitude: -122.41595350569808, + weight: 1, + }, + { + latitude: 37.78878838148139, + longitude: -122.42915000021907, + weight: 7, + }, + { + latitude: 37.76817490521571, + longitude: -122.41704521909944, + weight: 8, + }, + { + latitude: 37.7718981780505, + longitude: -122.42594186898218, + weight: 5, + }, + { + latitude: 37.76068144746727, + longitude: -122.42672649156114, + weight: 7, + }, + { + latitude: 37.785592409399605, + longitude: -122.4168079466871, + weight: 2, + }, + { + latitude: 37.79052488050082, + longitude: -122.41936218133623, + weight: 6, + }, + { + latitude: 37.7719629776494, + longitude: -122.4234137883915, + weight: 4, + }, + { + latitude: 37.77550712813202, + longitude: -122.43162125251617, + weight: 1, + }, + { + latitude: 37.77707428587479, + longitude: -122.43629710839507, + weight: 4, + }, + { + latitude: 37.7602180857071, + longitude: -122.41614920775933, + weight: 3, + }, + { + latitude: 37.76167023816209, + longitude: -122.41057309327171, + weight: 6, + }, + { + latitude: 37.78835250096297, + longitude: -122.41811310926747, + weight: 8, + }, + { + latitude: 37.77653357722768, + longitude: -122.41195003737698, + weight: 8, + }, + { + latitude: 37.7637176123525, + longitude: -122.41193498612587, + weight: 7, + }, + { + latitude: 37.796135533113784, + longitude: -122.42460224602047, + weight: 6, + }, + { + latitude: 37.775586996057605, + longitude: -122.41696341957409, + weight: 3, + }, + { + latitude: 37.780305009956045, + longitude: -122.42149790132709, + weight: 4, + }, + { + latitude: 37.77666119409575, + longitude: -122.41439240537842, + weight: 3, + }, + { + latitude: 37.7684546076077, + longitude: -122.4094172764022, + weight: 5, + }, + { + latitude: 37.79200946260774, + longitude: -122.41284489527432, + weight: 8, + }, + { + latitude: 37.78547994490607, + longitude: -122.41693707274464, + weight: 5, + }, + { + latitude: 37.77935534124923, + longitude: -122.44675821932218, + weight: 7, + }, + { + latitude: 37.770744396335985, + longitude: -122.41425008000054, + weight: 3, + }, + { + latitude: 37.77937562903279, + longitude: -122.4403979757335, + weight: 1, + }, + { + latitude: 37.77534895965078, + longitude: -122.42959465980073, + weight: 5, + }, + { + latitude: 37.772377587311674, + longitude: -122.43052308463766, + weight: 2, + }, + { + latitude: 37.788692966512336, + longitude: -122.43037527715093, + weight: 7, + }, + { + latitude: 37.77504402802339, + longitude: -122.43078689009961, + weight: 1, + }, + { + latitude: 37.77243601088833, + longitude: -122.42015705549609, + weight: 3, + }, + { + latitude: 37.77274526588314, + longitude: -122.42190423498118, + weight: 6, + }, + { + latitude: 37.76664530608675, + longitude: -122.40113398307366, + weight: 8, + }, + { + latitude: 37.76792793439583, + longitude: -122.42854936155135, + weight: 7, + }, + { + latitude: 37.775940095614786, + longitude: -122.4276288609928, + weight: 1, + }, + { + latitude: 37.77580910279941, + longitude: -122.42718609011861, + weight: 2, + }, + { + latitude: 37.74835596256392, + longitude: -122.41675953208474, + weight: 3, + }, + { + latitude: 37.78920221089107, + longitude: -122.43179049363602, + weight: 7, + }, + { + latitude: 37.77063152529854, + longitude: -122.42287909036055, + weight: 3, + }, + { + latitude: 37.77623250000308, + longitude: -122.42494685237841, + weight: 6, + }, + { + latitude: 37.79213082754877, + longitude: -122.41115606795891, + weight: 6, + }, + { + latitude: 37.774791657041796, + longitude: -122.42593451692399, + weight: 4, + }, + { + latitude: 37.77709347197662, + longitude: -122.42323677968345, + weight: 2, + }, + { + latitude: 37.77052910976417, + longitude: -122.42154415709635, + weight: 4, + }, + { + latitude: 37.78263451642938, + longitude: -122.41741443545125, + weight: 4, + }, + { + latitude: 37.77928858658292, + longitude: -122.41752721198586, + weight: 7, + }, + { + latitude: 37.76896496702166, + longitude: -122.41594146842827, + weight: 3, + }, + { + latitude: 37.77066190387805, + longitude: -122.41717526621028, + weight: 6, + }, + { + latitude: 37.77747242528472, + longitude: -122.41662773140601, + weight: 3, + }, + { + latitude: 37.77810978313104, + longitude: -122.42499501369456, + weight: 2, + }, + { + latitude: 37.78543692503737, + longitude: -122.41140533210495, + weight: 1, + }, + { + latitude: 37.769821363423475, + longitude: -122.4136464233395, + weight: 1, + }, + { + latitude: 37.76921807596031, + longitude: -122.41969621709319, + weight: 7, + }, + { + latitude: 37.78164060418575, + longitude: -122.40913214507725, + weight: 6, + }, + { + latitude: 37.75917322056645, + longitude: -122.42003399353172, + weight: 3, + }, + { + latitude: 37.766311797935906, + longitude: -122.4210841034049, + weight: 4, + }, + { + latitude: 37.77050566643732, + longitude: -122.41644211945335, + weight: 7, + }, + { + latitude: 37.769927665815686, + longitude: -122.40470274516751, + weight: 6, + }, + { + latitude: 37.77373169111256, + longitude: -122.42018421892432, + weight: 6, + }, + { + latitude: 37.761605058811476, + longitude: -122.42022963386542, + weight: 4, + }, + { + latitude: 37.78021920639477, + longitude: -122.39407147563546, + weight: 2, + }, + { + latitude: 37.77593507845645, + longitude: -122.43699961398347, + weight: 3, + }, + { + latitude: 37.759962735062174, + longitude: -122.42751323153449, + weight: 7, + }, + { + latitude: 37.771425303798445, + longitude: -122.4462331412101, + weight: 7, + }, + { + latitude: 37.78066146588017, + longitude: -122.42278862667528, + weight: 7, + }, + { + latitude: 37.77286370246437, + longitude: -122.4140085198048, + weight: 6, + }, + { + latitude: 37.76908496075501, + longitude: -122.42389544751236, + weight: 5, + }, + { + latitude: 37.76694317042424, + longitude: -122.42061081956044, + weight: 2, + }, + { + latitude: 37.7750383625157, + longitude: -122.43995614653278, + weight: 3, + }, + { + latitude: 37.785011885743856, + longitude: -122.42630514318634, + weight: 2, + }, + { + latitude: 37.774251562323975, + longitude: -122.41852355457083, + weight: 5, + }, + { + latitude: 37.755144373103505, + longitude: -122.4188863746868, + weight: 4, + }, + { + latitude: 37.789872745145125, + longitude: -122.4379804385467, + weight: 3, + }, + { + latitude: 37.77458406900455, + longitude: -122.41986785675799, + weight: 7, + }, + { + latitude: 37.766429680837796, + longitude: -122.40105749940282, + weight: 4, + }, + { + latitude: 37.77576576610448, + longitude: -122.41436814243876, + weight: 4, + }, + { + latitude: 37.778672867329355, + longitude: -122.41280775768428, + weight: 7, + }, + { + latitude: 37.76821160575987, + longitude: -122.39677188405327, + weight: 3, + }, + { + latitude: 37.77846039766879, + longitude: -122.43589700301662, + weight: 5, + }, + { + latitude: 37.77096427628243, + longitude: -122.40757756944338, + weight: 8, + }, + { + latitude: 37.779490173202106, + longitude: -122.41737604922255, + weight: 2, + }, + { + latitude: 37.778354007652055, + longitude: -122.42775779936284, + weight: 2, + }, + { + latitude: 37.794459026966614, + longitude: -122.41081598210467, + weight: 1, + }, + { + latitude: 37.77336690298013, + longitude: -122.42138872679698, + weight: 5, + }, + { + latitude: 37.78706135588538, + longitude: -122.40673812623395, + weight: 6, + }, + { + latitude: 37.77868416719929, + longitude: -122.41880585369353, + weight: 4, + }, + { + latitude: 37.78160149346374, + longitude: -122.4274108499156, + weight: 7, + }, + { + latitude: 37.78860653070716, + longitude: -122.41771515288234, + weight: 7, + }, + { + latitude: 37.78992110780817, + longitude: -122.42633712541854, + weight: 4, + }, + { + latitude: 37.76984641859194, + longitude: -122.40420838775012, + weight: 1, + }, + { + latitude: 37.76462312723779, + longitude: -122.40686174296462, + weight: 6, + }, + { + latitude: 37.76831186347533, + longitude: -122.39769996406197, + weight: 6, + }, + { + latitude: 37.77593532830477, + longitude: -122.39876771659095, + weight: 2, + }, + { + latitude: 37.77048069332476, + longitude: -122.41936838069364, + weight: 3, + }, + { + latitude: 37.76428467409966, + longitude: -122.41500352126354, + weight: 8, + }, + { + latitude: 37.773308663975534, + longitude: -122.41626556392686, + weight: 5, + }, + { + latitude: 37.77537332277024, + longitude: -122.4210032524865, + weight: 4, + }, + { + latitude: 37.77061320888391, + longitude: -122.41898974480587, + weight: 1, + }, + { + latitude: 37.762046382291565, + longitude: -122.41080508408292, + weight: 4, + }, + { + latitude: 37.77352105744451, + longitude: -122.42403374886301, + weight: 4, + }, + { + latitude: 37.77476967690934, + longitude: -122.43073737941214, + weight: 2, + }, + { + latitude: 37.77078774750744, + longitude: -122.4303677790037, + weight: 4, + }, + { + latitude: 37.78415799405039, + longitude: -122.43940649547905, + weight: 6, + }, + { + latitude: 37.76506513883444, + longitude: -122.4146954388348, + weight: 2, + }, + { + latitude: 37.78385772157598, + longitude: -122.41660808245864, + weight: 6, + }, + { + latitude: 37.77118770712783, + longitude: -122.4069701900946, + weight: 5, + }, + { + latitude: 37.77561803073022, + longitude: -122.42657790899646, + weight: 3, + }, + { + latitude: 37.78822845607223, + longitude: -122.42405443919625, + weight: 7, + }, + { + latitude: 37.773867146356615, + longitude: -122.41416509375259, + weight: 2, + }, + { + latitude: 37.77705754948342, + longitude: -122.42987241214037, + weight: 6, + }, + { + latitude: 37.78942513266261, + longitude: -122.41786683953863, + weight: 7, + }, + { + latitude: 37.77909351232253, + longitude: -122.42205566935479, + weight: 4, + }, + { + latitude: 37.787408661642104, + longitude: -122.43726003319377, + weight: 4, + }, + { + latitude: 37.78891022246514, + longitude: -122.41710738705864, + weight: 3, + }, + { + latitude: 37.77883665177344, + longitude: -122.42532342530885, + weight: 7, + }, + { + latitude: 37.77826050419845, + longitude: -122.43464076863681, + weight: 5, + }, + { + latitude: 37.77087709822634, + longitude: -122.43088192465686, + weight: 2, + }, + { + latitude: 37.777822832251694, + longitude: -122.4462977977772, + weight: 4, + }, + { + latitude: 37.776729075589984, + longitude: -122.41549531037523, + weight: 5, + }, + { + latitude: 37.789423815731745, + longitude: -122.43932460102573, + weight: 5, + }, + { + latitude: 37.75032936490263, + longitude: -122.43304635967871, + weight: 6, + }, + { + latitude: 37.775210702075654, + longitude: -122.40682517179853, + weight: 2, + }, + { + latitude: 37.779838712001336, + longitude: -122.41542462399204, + weight: 4, + }, + { + latitude: 37.77822331630282, + longitude: -122.41741970188777, + weight: 7, + }, + { + latitude: 37.76962743230699, + longitude: -122.42184854778628, + weight: 7, + }, + { + latitude: 37.74791832276605, + longitude: -122.43340896168682, + weight: 6, + }, + { + latitude: 37.77046322338052, + longitude: -122.39547326484539, + weight: 4, + }, + { + latitude: 37.767130063530935, + longitude: -122.43673253141684, + weight: 1, + }, + { + latitude: 37.757451278974926, + longitude: -122.43888754014193, + weight: 7, + }, + { + latitude: 37.77258298948454, + longitude: -122.41000308295601, + weight: 5, + }, + { + latitude: 37.79232774810211, + longitude: -122.41749567382821, + weight: 2, + }, + { + latitude: 37.79872682560711, + longitude: -122.41766936922964, + weight: 6, + }, + { + latitude: 37.79193139176204, + longitude: -122.41970043191085, + weight: 4, + }, + { + latitude: 37.7997810156384, + longitude: -122.39468611998196, + weight: 5, + }, + { + latitude: 37.78060719689347, + longitude: -122.41103222892957, + weight: 3, + }, + { + latitude: 37.78744349440565, + longitude: -122.41714089560584, + weight: 5, + }, + { + latitude: 37.78357306450333, + longitude: -122.4120321798084, + weight: 4, + }, + { + latitude: 37.77666548403877, + longitude: -122.41595070385814, + weight: 7, + }, + { + latitude: 37.7763323940471, + longitude: -122.42598291392854, + weight: 8, + }, + { + latitude: 37.763581460699946, + longitude: -122.41454021905446, + weight: 5, + }, + { + latitude: 37.78506003024406, + longitude: -122.43422138145192, + weight: 3, + }, + { + latitude: 37.784626801270996, + longitude: -122.40072670950322, + weight: 5, + }, + { + latitude: 37.758453230599486, + longitude: -122.4237890210368, + weight: 3, + }, + { + latitude: 37.768056011053865, + longitude: -122.43809992026151, + weight: 4, + }, + { + latitude: 37.77803983251135, + longitude: -122.42287019703372, + weight: 2, + }, + { + latitude: 37.78870515461774, + longitude: -122.43732584359384, + weight: 6, + }, + { + latitude: 37.777501543513694, + longitude: -122.42953274044035, + weight: 4, + }, + { + latitude: 37.77837834956944, + longitude: -122.4183512005129, + weight: 5, + }, + { + latitude: 37.780667122985975, + longitude: -122.42498875797118, + weight: 3, + }, + { + latitude: 37.796385381519855, + longitude: -122.43381819065323, + weight: 2, + }, + { + latitude: 37.76827418729752, + longitude: -122.42116661804484, + weight: 7, + }, + { + latitude: 37.773795391806864, + longitude: -122.39893399914776, + weight: 2, + }, + { + latitude: 37.78448195011017, + longitude: -122.41901497123025, + weight: 7, + }, + { + latitude: 37.79129419340574, + longitude: -122.41786640080032, + weight: 8, + }, + { + latitude: 37.76819246881858, + longitude: -122.41728159025809, + weight: 7, + }, + { + latitude: 37.78085670525712, + longitude: -122.40328128001802, + weight: 2, + }, + { + latitude: 37.776559101737426, + longitude: -122.41704599942867, + weight: 3, + }, + { + latitude: 37.77384909308133, + longitude: -122.4320478284274, + weight: 3, + }, + { + latitude: 37.78068789421225, + longitude: -122.42333968893689, + weight: 6, + }, + { + latitude: 37.77094406361879, + longitude: -122.42190969405058, + weight: 4, + }, + { + latitude: 37.773988065878164, + longitude: -122.4288867614361, + weight: 6, + }, + { + latitude: 37.773802528049, + longitude: -122.41761223177045, + weight: 3, + }, + { + latitude: 37.77255836934094, + longitude: -122.41924402191287, + weight: 3, + }, + { + latitude: 37.77435341348203, + longitude: -122.44605436736062, + weight: 6, + }, + { + latitude: 37.777233457603145, + longitude: -122.4288624392171, + weight: 4, + }, + { + latitude: 37.77912104162733, + longitude: -122.42135092860931, + weight: 7, + }, + { + latitude: 37.78125457670743, + longitude: -122.41821923246235, + weight: 7, + }, + { + latitude: 37.77494090334377, + longitude: -122.4222937723055, + weight: 8, + }, + { + latitude: 37.75562720261027, + longitude: -122.42176493591572, + weight: 4, + }, + { + latitude: 37.7646305288907, + longitude: -122.40896151438625, + weight: 8, + }, + { + latitude: 37.75074094343304, + longitude: -122.41774896601376, + weight: 2, + }, + { + latitude: 37.78337962949414, + longitude: -122.41607698423365, + weight: 3, + }, + { + latitude: 37.76390544354026, + longitude: -122.42197142224369, + weight: 8, + }, + { + latitude: 37.74777004421375, + longitude: -122.41960284318984, + weight: 5, + }, + { + latitude: 37.79131839139827, + longitude: -122.4132565393816, + weight: 7, + }, + { + latitude: 37.795683097766066, + longitude: -122.4202650693788, + weight: 1, + }, + { + latitude: 37.7760246075837, + longitude: -122.43832489626324, + weight: 3, + }, + { + latitude: 37.77752831939996, + longitude: -122.42033996271188, + weight: 7, + }, + { + latitude: 37.77329472328071, + longitude: -122.42217543104807, + weight: 6, + }, + { + latitude: 37.76784522281833, + longitude: -122.42613728640386, + weight: 2, + }, + { + latitude: 37.774992557180916, + longitude: -122.39778506472854, + weight: 7, + }, + { + latitude: 37.77137248558644, + longitude: -122.41938595055771, + weight: 3, + }, + { + latitude: 37.794506700316006, + longitude: -122.44219870819892, + weight: 4, + }, + { + latitude: 37.7815404072938, + longitude: -122.40432971101613, + weight: 1, + }, + { + latitude: 37.76023340425924, + longitude: -122.41754469748713, + weight: 7, + }, + { + latitude: 37.771006341835154, + longitude: -122.42102183726769, + weight: 7, + }, + { + latitude: 37.78545118873739, + longitude: -122.42650045217142, + weight: 2, + }, + { + latitude: 37.79367655775864, + longitude: -122.39831983918475, + weight: 8, + }, + { + latitude: 37.76236174138733, + longitude: -122.41523722403848, + weight: 3, + }, + { + latitude: 37.775732760740986, + longitude: -122.40603491509582, + weight: 5, + }, + { + latitude: 37.77460269812679, + longitude: -122.4044659149845, + weight: 4, + }, + { + latitude: 37.77438459522959, + longitude: -122.439369505132, + weight: 8, + }, + { + latitude: 37.766552322371226, + longitude: -122.4396372517053, + weight: 5, + }, + { + latitude: 37.77523830967655, + longitude: -122.41605423109756, + weight: 5, + }, + { + latitude: 37.77514892800511, + longitude: -122.4177993707635, + weight: 5, + }, + { + latitude: 37.76338605476149, + longitude: -122.43355558326282, + weight: 7, + }, + { + latitude: 37.77738102123909, + longitude: -122.42023998516754, + weight: 5, + }, + { + latitude: 37.76610639149833, + longitude: -122.43629106710257, + weight: 5, + }, + { + latitude: 37.75861990783103, + longitude: -122.41070490859092, + weight: 2, + }, + { + latitude: 37.767687181136594, + longitude: -122.42742339668396, + weight: 8, + }, + { + latitude: 37.77450227903349, + longitude: -122.41303544391674, + weight: 6, + }, + { + latitude: 37.776081673516956, + longitude: -122.42492153761222, + weight: 1, + }, + { + latitude: 37.760407757771624, + longitude: -122.40872838560603, + weight: 4, + }, + { + latitude: 37.76554853352769, + longitude: -122.41608403294005, + weight: 7, + }, + { + latitude: 37.779820009786015, + longitude: -122.42784032020111, + weight: 5, + }, + { + latitude: 37.771438944607134, + longitude: -122.42068513300084, + weight: 8, + }, + { + latitude: 37.777118467188714, + longitude: -122.43807286877927, + weight: 5, + }, + { + latitude: 37.77463162941818, + longitude: -122.43114020959605, + weight: 5, + }, + { + latitude: 37.78315655259015, + longitude: -122.4166199812002, + weight: 5, + }, + { + latitude: 37.783527620456695, + longitude: -122.41958145526111, + weight: 7, + }, + { + latitude: 37.78973563939098, + longitude: -122.40442711816974, + weight: 7, + }, + { + latitude: 37.771580951448385, + longitude: -122.42831282451446, + weight: 5, + }, + { + latitude: 37.77537297532229, + longitude: -122.41943415127064, + weight: 3, + }, + { + latitude: 37.79018733473478, + longitude: -122.43303000216369, + weight: 5, + }, + { + latitude: 37.77280018762938, + longitude: -122.41629021319567, + weight: 4, + }, + { + latitude: 37.75551599862878, + longitude: -122.41871593616905, + weight: 3, + }, + { + latitude: 37.7758834556498, + longitude: -122.41376739124377, + weight: 4, + }, + { + latitude: 37.78896631085146, + longitude: -122.42819113819775, + weight: 8, + }, + { + latitude: 37.783631771146865, + longitude: -122.42035181277596, + weight: 2, + }, + { + latitude: 37.766318664423366, + longitude: -122.41378935842144, + weight: 5, + }, + { + latitude: 37.767122739054756, + longitude: -122.4213428586804, + weight: 7, + }, + { + latitude: 37.782670038658935, + longitude: -122.39090602273309, + weight: 2, + }, + { + latitude: 37.776729114696145, + longitude: -122.41667777242783, + weight: 6, + }, + { + latitude: 37.77452751807454, + longitude: -122.41914371992794, + weight: 5, + }, + { + latitude: 37.77499717657243, + longitude: -122.41848926720246, + weight: 8, + }, + { + latitude: 37.779374308203884, + longitude: -122.41496533374993, + weight: 6, + }, + { + latitude: 37.77381829193447, + longitude: -122.41991883714269, + weight: 4, + }, + { + latitude: 37.78078516950132, + longitude: -122.41931328735681, + weight: 4, + }, + { + latitude: 37.77043749747692, + longitude: -122.41945923630516, + weight: 4, + }, + { + latitude: 37.78298935931121, + longitude: -122.42197467062977, + weight: 1, + }, + { + latitude: 37.77638566577817, + longitude: -122.44464316294368, + weight: 1, + }, + { + latitude: 37.787167857070386, + longitude: -122.41032402864874, + weight: 7, + }, + { + latitude: 37.77987444440249, + longitude: -122.41377519266743, + weight: 2, + }, + { + latitude: 37.7717997414449, + longitude: -122.41684488483594, + weight: 3, + }, + { + latitude: 37.77459229484577, + longitude: -122.41922799233299, + weight: 5, + }, + { + latitude: 37.76937433092144, + longitude: -122.41824402774478, + weight: 4, + }, + { + latitude: 37.76482803025363, + longitude: -122.4014285865162, + weight: 6, + }, + { + latitude: 37.78681537191843, + longitude: -122.40195885400526, + weight: 5, + }, + { + latitude: 37.77116713481662, + longitude: -122.4177047612868, + weight: 8, + }, + { + latitude: 37.77487349090085, + longitude: -122.41893518120916, + weight: 7, + }, + { + latitude: 37.7607470239056, + longitude: -122.41836086634899, + weight: 4, + }, + { + latitude: 37.782772379171895, + longitude: -122.42814404841938, + weight: 5, + }, + { + latitude: 37.76416288130574, + longitude: -122.42699015603034, + weight: 2, + }, + { + latitude: 37.768582880134666, + longitude: -122.44137601345619, + weight: 2, + }, + { + latitude: 37.77335451873783, + longitude: -122.41867543339905, + weight: 4, + }, + { + latitude: 37.77397906323175, + longitude: -122.4158458650662, + weight: 1, + }, + { + latitude: 37.77510281432576, + longitude: -122.40636813844081, + weight: 5, + }, + { + latitude: 37.782987384926756, + longitude: -122.42076351360521, + weight: 3, + }, + { + latitude: 37.76698249316283, + longitude: -122.41466986758864, + weight: 4, + }, + { + latitude: 37.78785158867734, + longitude: -122.40522778261693, + weight: 3, + }, + { + latitude: 37.77788897366828, + longitude: -122.4215581292945, + weight: 6, + }, + { + latitude: 37.762530391155, + longitude: -122.40738657797878, + weight: 7, + }, + { + latitude: 37.77643327304928, + longitude: -122.41422276225094, + weight: 4, + }, + { + latitude: 37.77194999012115, + longitude: -122.40461833650195, + weight: 7, + }, + { + latitude: 37.77702254115794, + longitude: -122.41568875094308, + weight: 8, + }, + { + latitude: 37.77501163502845, + longitude: -122.42103652029425, + weight: 5, + }, + { + latitude: 37.77050918238645, + longitude: -122.4265362771051, + weight: 3, + }, + { + latitude: 37.76823122340263, + longitude: -122.41723922738575, + weight: 5, + }, + { + latitude: 37.78556000420903, + longitude: -122.44007798924542, + weight: 6, + }, + { + latitude: 37.78441159308756, + longitude: -122.44546951366708, + weight: 7, + }, + { + latitude: 37.77439205727964, + longitude: -122.42588878485795, + weight: 2, + }, + { + latitude: 37.767175806495885, + longitude: -122.41951279113702, + weight: 6, + }, + { + latitude: 37.77412094745167, + longitude: -122.41732407018087, + weight: 8, + }, + { + latitude: 37.785925720462984, + longitude: -122.42425440311816, + weight: 2, + }, + { + latitude: 37.77031477683615, + longitude: -122.43184086583078, + weight: 7, + }, + { + latitude: 37.7806346585776, + longitude: -122.39456468761311, + weight: 5, + }, + { + latitude: 37.766936970134864, + longitude: -122.41938684048533, + weight: 3, + }, + { + latitude: 37.77680887795725, + longitude: -122.40352462277508, + weight: 5, + }, + { + latitude: 37.77144066101369, + longitude: -122.42928754677936, + weight: 2, + }, + { + latitude: 37.78926332018626, + longitude: -122.40706562661444, + weight: 5, + }, + { + latitude: 37.77668112053309, + longitude: -122.42417535147608, + weight: 2, + }, + { + latitude: 37.77450595427296, + longitude: -122.42026210707498, + weight: 8, + }, + { + latitude: 37.76669843982016, + longitude: -122.40024524557364, + weight: 6, + }, + { + latitude: 37.76967336546115, + longitude: -122.42556576959156, + weight: 6, + }, + { + latitude: 37.7739100366508, + longitude: -122.42637692211132, + weight: 5, + }, + { + latitude: 37.78814319604354, + longitude: -122.41936798689886, + weight: 2, + }, + { + latitude: 37.773069175117705, + longitude: -122.42264290515187, + weight: 6, + }, + { + latitude: 37.77127131864964, + longitude: -122.42560859162313, + weight: 4, + }, + { + latitude: 37.77912279431457, + longitude: -122.41256236841711, + weight: 1, + }, + { + latitude: 37.77269606075684, + longitude: -122.42588368814496, + weight: 8, + }, + { + latitude: 37.77874571578268, + longitude: -122.40999670811188, + weight: 1, + }, + { + latitude: 37.76987755154968, + longitude: -122.43840182024853, + weight: 5, + }, + { + latitude: 37.770688829311574, + longitude: -122.42498345278484, + weight: 4, + }, + { + latitude: 37.77547535053348, + longitude: -122.4415149242562, + weight: 3, + }, + { + latitude: 37.76590642441266, + longitude: -122.40945815510715, + weight: 1, + }, +]; diff --git a/example/src/utils/mapGenerators.ts b/example/src/utils/mapGenerators.ts index 0b47179..79bd672 100644 --- a/example/src/utils/mapGenerators.ts +++ b/example/src/utils/mapGenerators.ts @@ -4,7 +4,9 @@ import type { RNMarker, RNPolygon, RNPolyline, + RNUrlTileOverlay, } from 'react-native-google-maps-plus'; +import { weightData } from './heatMapWeightData'; export function randomColor() { return ( @@ -29,6 +31,59 @@ export function makeSvgIcon( `; } +export function makeInfoWindowIconSvg( + width: number, + height: number, + color?: string, + text?: string +): string { + color = color ?? randomColor(); + const label = text ?? 'Google Maps Plus'; + const corner = 12; + const pointerHeight = 12; + + const rectHeight = height - pointerHeight; + const textY = rectHeight / 2 + 5; + + return ` + + + + + + + + ${label} + +`.trim(); +} + export const randomCoordinates = ( baseLat: number, baseLng: number, @@ -124,52 +179,27 @@ export const makeCircle = (id: number): RNCircle => ({ export const makeHeatmap = (id: number): RNHeatmap => ({ id: id.toString(), zIndex: id, - weightedData: [ - { - latitude: 37.777714074525925, - longitude: -122.42099587858186, - weight: 1, - }, - { - latitude: 37.785184052875735, - longitude: -122.42914114591328, - weight: 1, - }, - { - latitude: 37.769334961755526, - longitude: -122.41418426583697, - weight: 5, - }, - { - latitude: 37.7717263096532, - longitude: -122.41931954914673, - weight: 4, - }, - { - latitude: 37.78589459403588, - longitude: -122.40573314204349, - weight: 3, - }, - { - latitude: 37.78664297332888, - longitude: -122.42602082474453, - weight: 2, - }, - { - latitude: 37.74874321698208, - longitude: -122.44390470794693, - weight: 1, - }, - ], + weightedData: weightData, gradient: { colors: ['#00f', '#0ff', '#0f0', '#ff0', '#f00'], - startPoints: [0.0, 0.25, 0.5, 0.75, 1.0], - colorMapSize: 1024, + startPoints: [0.1, 0.2, 0.45, 0.7, 1.0], + colorMapSize: 256, }, - radius: 100, + radius: 50, opacity: 1, }); +export function makeUrlTileOverlay(id: number): RNUrlTileOverlay { + return { + id: id.toString(), + zIndex: id, + fadeIn: false, + opacity: 1, + tileSize: 256, + url: '', + }; +} + export function makeMarker(id: number): RNMarker { return { id: id.toString(), @@ -194,6 +224,11 @@ export function makeMarker(id: number): RNMarker { height: 44, svgString: makeSvgIcon(32, 44, '#2D6BE9'), }, + infoWindowIconSvg: { + width: 150, + height: 50, + svgString: makeInfoWindowIconSvg(150, 50, '#2D6BE9'), + }, }; } diff --git a/example/src/utils/mapUtils.ts b/example/src/utils/mapUtils.ts new file mode 100644 index 0000000..92140ab --- /dev/null +++ b/example/src/utils/mapUtils.ts @@ -0,0 +1,21 @@ +import type { RNRegion } from 'react-native-google-maps-plus'; + +export function rnRegionToRegion(rn: RNRegion | null): { + latitude: number; + longitude: number; + latitudeDelta: number; + longitudeDelta: number; +} { + if (rn == null) { + return { latitude: 0, longitude: 0, latitudeDelta: 0, longitudeDelta: 0 }; + } + const { northeast, southwest } = rn.latLngBounds; + + const latitude = (northeast.latitude + southwest.latitude) / 2; + const longitude = (northeast.longitude + southwest.longitude) / 2; + + const latitudeDelta = Math.abs(northeast.latitude - southwest.latitude); + const longitudeDelta = Math.abs(northeast.longitude - southwest.longitude); + + return { latitude, longitude, latitudeDelta, longitudeDelta }; +} diff --git a/ios/GoogleMapViewImpl.swift b/ios/GoogleMapViewImpl.swift index 514b5f9..a0d55bc 100644 --- a/ios/GoogleMapViewImpl.swift +++ b/ios/GoogleMapViewImpl.swift @@ -10,7 +10,7 @@ GMSIndoorDisplayDelegate { private let markerBuilder: MapMarkerBuilder private var mapView: GMSMapView? private var initialized = false - private var mapReady = false + private var loaded = false private var deInitialized = false private var pendingMarkers: [(id: String, marker: GMSMarker)] = [] @@ -19,6 +19,7 @@ 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 markersById: [String: GMSMarker] = [:] private var polylinesById: [String: GMSPolyline] = [:] @@ -26,9 +27,9 @@ GMSIndoorDisplayDelegate { private var circlesById: [String: GMSCircle] = [:] private var heatmapsById: [String: GMUHeatmapTileLayer] = [:] private var kmlLayerById: [String: GMUGeometryRenderer] = [:] + private var urlTileOverlays: [String: GMSURLTileLayer] = [:] private var cameraMoveReasonIsGesture: Bool = false - private var lastSubmittedCameraPosition: GMSCameraPosition? init( frame: CGRect = .zero, @@ -75,7 +76,6 @@ GMSIndoorDisplayDelegate { applyProps() initLocationCallbacks() onMapReady?(true) - mapReady = true } @MainActor @@ -136,6 +136,12 @@ GMSIndoorDisplayDelegate { } pendingKmlLayers.removeAll() } + if !pendingUrlTileOverlays.isEmpty { + pendingUrlTileOverlays.forEach { + addUrlTileOverlayInternal(id: $0.id, urlTileOverlay: $0.urlTileOverlay) + } + pendingUrlTileOverlays.removeAll() + } } @MainActor @@ -255,22 +261,49 @@ GMSIndoorDisplayDelegate { var onMapError: ((RNMapErrorCode) -> Void)? var onMapReady: ((Bool) -> Void)? + var onMapLoaded: ((RNRegion, RNCamera) -> 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 onMarkerDragStart: ((String?, RNLatLng) -> Void)? - var onMarkerDrag: ((String?, RNLatLng) -> Void)? - var onMarkerDragEnd: ((String?, RNLatLng) -> Void)? + var onMapLongPress: ((RNLatLng) -> Void)? + var onPoiPress: ((String, String, RNLatLng) -> Void)? + var onMarkerPress: ((String) -> Void)? + var onPolylinePress: ((String) -> Void)? + var onPolygonPress: ((String) -> Void)? + var onCirclePress: ((String) -> Void)? + var onMarkerDragStart: ((String, RNLatLng) -> Void)? + var onMarkerDrag: ((String, RNLatLng) -> Void)? + var onMarkerDragEnd: ((String, RNLatLng) -> Void)? var onIndoorBuildingFocused: ((RNIndoorBuilding) -> Void)? var onIndoorLevelActivated: ((RNIndoorLevel) -> Void)? + var onInfoWindowPress: ((String) -> Void)? + var onInfoWindowClose: ((String) -> Void)? + var onInfoWindowLongPress: ((String) -> Void)? + var onMyLocationPress: ((RNLocation) -> Void)? + var onMyLocationButtonPress: ((Bool) -> Void)? var onCameraChangeStart: ((RNRegion, RNCamera, Bool) -> Void)? 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 } + self.mapView?.selectedMarker = nil + self.mapView?.selectedMarker = marker + } + } + + @MainActor + func hideMarkerInfoWindow(id: String) { + onMain { + guard let marker = self.markersById[id] else { return } + if self.mapView?.selectedMarker == marker { + self.mapView?.selectedMarker = nil + } + } + } + @MainActor func setCamera(camera: GMSCameraPosition, animated: Bool, durationMs: Double) { if animated { @@ -366,42 +399,16 @@ GMSIndoorDisplayDelegate { mapView.layer.render(in: ctx.cgContext) } - var finalImage = image - - size.map { - UIGraphicsBeginImageContextWithOptions($0, false, 0.0) - image.draw(in: CGRect(origin: .zero, size: $0)) - finalImage = UIGraphicsGetImageFromCurrentImageContext() ?? image - UIGraphicsEndImageContext() - } - - let data: Data? - switch imageFormat { - case .jpeg: - data = finalImage.jpegData(compressionQuality: quality) - case .png: - data = finalImage.pngData() - } - - guard let imageData = data else { - promise.resolve(withResult: nil) - return - } - - if resultIsFile { - let filename = - "map_snapshot_\(Int(Date().timeIntervalSince1970)).\(format)" - let fileURL = URL(fileURLWithPath: NSTemporaryDirectory()) - .appendingPathComponent(filename) - do { - try imageData.write(to: fileURL) - promise.resolve(withResult: fileURL.path) - } catch { - promise.resolve(withResult: nil) - } + if let result = image.encode( + targetSize: size, + format: format, + imageFormat: imageFormat, + quality: quality, + resultIsFile: resultIsFile + ) { + promise.resolve(withResult: result) } else { - let base64 = imageData.base64EncodedString() - promise.resolve(withResult: "data:image/\(format);base64,\(base64)") + promise.resolve(withResult: nil) } } @@ -420,14 +427,19 @@ GMSIndoorDisplayDelegate { @MainActor private func addMarkerInternal(id: String, marker: GMSMarker) { - marker.userData = id marker.map = mapView markersById[id] = marker } @MainActor func updateMarker(id: String, block: @escaping (GMSMarker) -> Void) { - markersById[id].map { block($0) } + markersById[id].map { + block($0) + if let mapView, mapView.selectedMarker == $0 { + mapView.selectedMarker = nil + mapView.selectedMarker = $0 + } + } } @MainActor @@ -454,8 +466,8 @@ GMSIndoorDisplayDelegate { @MainActor private func addPolylineInternal(id: String, polyline: GMSPolyline) { + polyline.tagData = PolylineTag(id: id) polyline.map = mapView - polyline.userData = id polylinesById[id] = polyline } @@ -488,8 +500,8 @@ GMSIndoorDisplayDelegate { @MainActor private func addPolygonInternal(id: String, polygon: GMSPolygon) { + polygon.tagData = PolygonTag(id: id) polygon.map = mapView - polygon.userData = id polygonsById[id] = polygon } @@ -522,8 +534,8 @@ GMSIndoorDisplayDelegate { @MainActor private func addCircleInternal(id: String, circle: GMSCircle) { + circle.tagData = CircleTag(id: id) circle.map = mapView - circle.userData = id circlesById[id] = circle } @@ -562,7 +574,10 @@ GMSIndoorDisplayDelegate { @MainActor func removeHeatmap(id: String) { - heatmapsById.removeValue(forKey: id).map { $0.map = nil } + heatmapsById.removeValue(forKey: id).map { + $0.clearTileCache() + $0.map = nil + } } @MainActor @@ -596,6 +611,7 @@ GMSIndoorDisplayDelegate { geometries: parser.placemarks ) renderer.render() + kmlLayerById[id] = renderer } } @@ -611,6 +627,43 @@ GMSIndoorDisplayDelegate { pendingKmlLayers.removeAll() } + @MainActor + func addUrlTileOverlay(id: String, urlTileOverlay: GMSURLTileLayer) { + if mapView == nil { + pendingUrlTileOverlays.append((id, urlTileOverlay)) + return + } + 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 + } + + @MainActor + func removeUrlTileOverlay(id: String) { + urlTileOverlays.removeValue(forKey: id).map { + $0.clearTileCache() + $0.map = nil + } + } + + @MainActor + func clearUrlTileOverlay() { + urlTileOverlays.values.forEach { + $0.clearTileCache() + $0.map = nil + } + urlTileOverlays.removeAll() + pendingUrlTileOverlays.removeAll() + } + func deinitInternal() { guard !deInitialized else { return } deInitialized = true @@ -623,6 +676,7 @@ GMSIndoorDisplayDelegate { self.clearCircles() self.clearHeatmaps() self.clearKmlLayers() + self.clearUrlTileOverlay() self.mapView?.clear() self.mapView?.indoorDisplay.delegate = nil self.mapView?.delegate = nil @@ -655,50 +709,46 @@ GMSIndoorDisplayDelegate { deinitInternal() } + func mapViewDidFinishTileRendering(_ mapView: GMSMapView) { + guard !loaded else { return } + loaded = true + let visibleRegion = mapView.projection.visibleRegion().toRNRegion() + let camera = mapView.camera.toRNCamera() + + self.onMapLoaded?(visibleRegion, camera) + } + func mapView(_ mapView: GMSMapView, willMove gesture: Bool) { + if !loaded { return } onMain { self.cameraMoveReasonIsGesture = gesture - let visibleRegion = mapView.projection.visibleRegion() - let bounds = GMSCoordinateBounds(region: visibleRegion) - let region = bounds.toRNRegion() + let visibleRegion = mapView.projection.visibleRegion().toRNRegion() let camera = mapView.camera.toRNCamera() - self.onCameraChangeStart?(region, camera, gesture) + self.onCameraChangeStart?(visibleRegion, camera, gesture) } } func mapView(_ mapView: GMSMapView, didChange position: GMSCameraPosition) { + if !loaded { return } onMain { - if let last = self.lastSubmittedCameraPosition, - last.target.latitude == position.target.latitude, - last.target.longitude == position.target.longitude, - last.zoom == position.zoom, - last.bearing == position.bearing, - last.viewingAngle == position.viewingAngle { - return - } - - self.lastSubmittedCameraPosition = position - let visibleRegion = mapView.projection.visibleRegion() - let bounds = GMSCoordinateBounds(region: visibleRegion) - - let region = bounds.toRNRegion() + let visibleRegion = mapView.projection.visibleRegion().toRNRegion() let camera = mapView.camera.toRNCamera() + let gesture = self.cameraMoveReasonIsGesture - self.onCameraChange?(region, camera, self.cameraMoveReasonIsGesture) + self.onCameraChange?(visibleRegion, camera, gesture) } } func mapView(_ mapView: GMSMapView, idleAt position: GMSCameraPosition) { + if !loaded { return } onMain { - let visibleRegion = mapView.projection.visibleRegion() - let bounds = GMSCoordinateBounds(region: visibleRegion) - - let region = bounds.toRNRegion() + let visibleRegion = mapView.projection.visibleRegion().toRNRegion() let camera = mapView.camera.toRNCamera() + let gesture = self.cameraMoveReasonIsGesture - self.onCameraChangeComplete?(region, camera, self.cameraMoveReasonIsGesture) + self.onCameraChangeComplete?(visibleRegion, camera, gesture) } } @@ -713,25 +763,46 @@ GMSIndoorDisplayDelegate { } } + func mapView( + _ mapView: GMSMapView, + didLongPressAt coordinate: CLLocationCoordinate2D + ) { + onMain { + self.onMapLongPress?( + coordinate.toRNLatLng(), + ) + } + } + + func mapView( + _ mapView: GMSMapView, + didTapPOIWithPlaceID placeID: String, + name: String, + location: CLLocationCoordinate2D + ) { + onMain { + self.onPoiPress?(placeID, name, location.toRNLatLng()) + } + } + func mapView(_ mapView: GMSMapView, didTap marker: GMSMarker) -> Bool { onMain { - mapView.selectedMarker = marker - self.onMarkerPress?(marker.userData as? String, ) + self.onMarkerPress?(marker.idTag) } - return true + return uiSettings?.consumeOnMarkerPress ?? false } func mapView(_ mapView: GMSMapView, didTap overlay: GMSOverlay) { onMain { switch overlay { case let circle as GMSCircle: - self.onCirclePress?(circle.userData as? String, ) + self.onCirclePress?(circle.idTag) case let polygon as GMSPolygon: - self.onPolygonPress?(polygon.userData as? String, ) + self.onPolygonPress?(polygon.idTag) case let polyline as GMSPolyline: - self.onPolylinePress?(polyline.userData as? String, ) + self.onPolylinePress?(polyline.idTag) default: break @@ -742,7 +813,7 @@ GMSIndoorDisplayDelegate { func mapView(_ mapView: GMSMapView, didBeginDragging marker: GMSMarker) { onMain { self.onMarkerDragStart?( - marker.userData as? String, + marker.idTag, marker.position.toRNLatLng() ) } @@ -751,7 +822,7 @@ GMSIndoorDisplayDelegate { func mapView(_ mapView: GMSMapView, didDrag marker: GMSMarker) { onMain { self.onMarkerDrag?( - marker.userData as? String, + marker.idTag, marker.position.toRNLatLng() ) } @@ -760,7 +831,7 @@ GMSIndoorDisplayDelegate { func mapView(_ mapView: GMSMapView, didEndDragging marker: GMSMarker) { onMain { self.onMarkerDragEnd?( - marker.userData as? String, + marker.idTag, marker.position.toRNLatLng() ) } @@ -791,4 +862,52 @@ GMSIndoorDisplayDelegate { ) } } + + func mapView(_ mapView: GMSMapView, didTapInfoWindowOf marker: GMSMarker) { + onMain { + self.onInfoWindowPress?(marker.idTag) + } + } + + func mapView(_ mapView: GMSMapView, didCloseInfoWindowOf marker: GMSMarker) { + onMain { + self.onInfoWindowClose?(marker.idTag) + } + } + + func mapView( + _ mapView: GMSMapView, + didLongPressInfoWindowOf marker: GMSMarker + ) { + onMain { + self.onInfoWindowLongPress?(marker.idTag) + } + } + + func mapView( + _ mapView: GMSMapView, + didTapMyLocation location: CLLocationCoordinate2D + ) { + onMain { + self.mapView?.myLocation.map { + self.onMyLocationPress?($0.toRnLocation()) + } + } + } + + func didTapMyLocationButton(for mapView: GMSMapView) -> Bool { + onMain { + self.onMyLocationButtonPress?(true) + } + return uiSettings?.consumeOnMyLocationButtonPress ?? false + } + + func mapView(_ mapView: GMSMapView, markerInfoWindow marker: GMSMarker) -> UIView? { + return markerBuilder.buildInfoWindow(iconSvg: marker.tagData.iconSvg) + } + + func mapView(_ mapView: GMSMapView, markerInfoContents marker: GMSMarker) + -> UIView? { + return nil + } } diff --git a/ios/MapCircleBuilder.swift b/ios/MapCircleBuilder.swift index 148012f..2c70cbe 100644 --- a/ios/MapCircleBuilder.swift +++ b/ios/MapCircleBuilder.swift @@ -1,6 +1,7 @@ import GoogleMaps final class MapCircleBuilder { + @MainActor func build(_ c: RNCircle) -> GMSCircle { let circle = GMSCircle() circle.position = c.center.toCLLocationCoordinate2D() @@ -14,6 +15,7 @@ final class MapCircleBuilder { return circle } + @MainActor func update(_ prev: RNCircle, _ next: RNCircle, _ c: GMSCircle) { if prev.center.latitude != next.center.latitude || prev.center.longitude != next.center.longitude { diff --git a/ios/MapHeatmapBuilder.swift b/ios/MapHeatmapBuilder.swift index 07ea637..e5437da 100644 --- a/ios/MapHeatmapBuilder.swift +++ b/ios/MapHeatmapBuilder.swift @@ -4,6 +4,7 @@ import GoogleMapsUtils import UIKit final class MapHeatmapBuilder { + @MainActor func build(_ h: RNHeatmap) -> GMUHeatmapTileLayer { let heatmap = GMUHeatmapTileLayer() heatmap.weightedData = h.weightedData.toWeightedLatLngs() @@ -18,7 +19,7 @@ final class MapHeatmapBuilder { heatmap.gradient = GMUGradient( colors: colors, startPoints: startPoints, - colorMapSize: 256 + colorMapSize: UInt(g.colorMapSize) ) } diff --git a/ios/MapMarkerBuilder.swift b/ios/MapMarkerBuilder.swift index 1141e70..a27ecb1 100644 --- a/ios/MapMarkerBuilder.swift +++ b/ios/MapMarkerBuilder.swift @@ -10,16 +10,19 @@ final class MapMarkerBuilder { }() private var tasks: [String: Task] = [:] + @MainActor func build(_ m: RNMarker, icon: UIImage?) -> GMSMarker { let marker = GMSMarker( position: m.coordinate.toCLLocationCoordinate2D() ) - marker.userData = m.id marker.tracksViewChanges = true marker.icon = icon m.title.map { marker.title = $0 } m.snippet.map { marker.snippet = $0 } - m.opacity.map { marker.iconView?.alpha = CGFloat($0) } + m.opacity.map { + marker.opacity = Float($0) + marker.iconView?.alpha = CGFloat($0) + } m.flat.map { marker.isFlat = $0 } m.draggable.map { marker.isDraggable = $0 } m.rotation.map { marker.rotation = $0 } @@ -34,6 +37,11 @@ final class MapMarkerBuilder { } m.zIndex.map { marker.zIndex = Int32($0) } + marker.tagData = MarkerTag( + id: m.id, + iconSvg: m.infoWindowIconSvg + ) + onMainAsync { [weak marker] in try? await Task.sleep(nanoseconds: 250_000_000) marker?.tracksViewChanges = false @@ -119,6 +127,12 @@ final class MapMarkerBuilder { y: next.infoWindowAnchor?.y ?? 0 ) } + + m.tagData = MarkerTag( + id: next.id, + iconSvg: next.infoWindowIconSvg + ) + } } @@ -160,11 +174,13 @@ final class MapMarkerBuilder { tasks[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 { @@ -174,6 +190,47 @@ final class MapMarkerBuilder { iconCache.removeAllObjects() } + @MainActor + func buildInfoWindow(iconSvg: RNMarkerSvg?) -> UIImageView? { + guard let iconSvg = iconSvg else { + return nil + } + + guard let data = iconSvg.svgString.data(using: .utf8), + let svgImg = SVGKImage(data: data) + else { + return nil + } + + let size = CGSize( + width: max(1, CGFloat(iconSvg.width)), + height: max(1, CGFloat(iconSvg.height)) + ) + + svgImg.size = size + + guard let base = svgImg.uiImage else { + return nil + } + + let fmt = UIGraphicsImageRendererFormat.default() + fmt.opaque = false + fmt.scale = UIScreen.main.scale + let renderer = UIGraphicsImageRenderer(size: size, format: fmt) + + let finalImage = renderer.image { _ in + base.draw(in: CGRect(origin: .zero, size: size)) + } + + let imageView = UIImageView(image: finalImage) + imageView.frame = CGRect(origin: .zero, size: size) + imageView.contentMode = .scaleAspectFit + imageView.backgroundColor = .clear + + return imageView + } + + @MainActor private func renderUIImage(_ m: RNMarker, _ scale: CGFloat) async -> UIImage? { guard let iconSvg = m.iconSvg, let data = iconSvg.svgString.data(using: .utf8) diff --git a/ios/MapPolygonBuilder.swift b/ios/MapPolygonBuilder.swift index 0ef595e..d74696b 100644 --- a/ios/MapPolygonBuilder.swift +++ b/ios/MapPolygonBuilder.swift @@ -1,6 +1,7 @@ import GoogleMaps final class MapPolygonBuilder { + @MainActor func build(_ p: RNPolygon) -> GMSPolygon { let path = GMSMutablePath() p.coordinates.forEach { @@ -28,6 +29,7 @@ final class MapPolygonBuilder { return pg } + @MainActor func update(_ prev: RNPolygon, _ next: RNPolygon, _ pg: GMSPolygon) { let coordsChanged = prev.coordinates.count != next.coordinates.count diff --git a/ios/MapPolylineBuilder.swift b/ios/MapPolylineBuilder.swift index da2f31d..cb2ae8a 100644 --- a/ios/MapPolylineBuilder.swift +++ b/ios/MapPolylineBuilder.swift @@ -1,6 +1,7 @@ import GoogleMaps final class MapPolylineBuilder { + @MainActor func build(_ p: RNPolyline) -> GMSPolyline { let path = GMSMutablePath() p.coordinates.forEach { @@ -22,6 +23,7 @@ final class MapPolylineBuilder { return pl } + @MainActor func update(_ prev: RNPolyline, _ next: RNPolyline, _ pl: GMSPolyline) { let coordsChanged = prev.coordinates.count != next.coordinates.count diff --git a/ios/MapUrlTileOverlayBuilder.swift b/ios/MapUrlTileOverlayBuilder.swift new file mode 100644 index 0000000..59f1869 --- /dev/null +++ b/ios/MapUrlTileOverlayBuilder.swift @@ -0,0 +1,24 @@ +import GoogleMaps + +class MapUrlTileOverlayBuilder { + @MainActor + func build(_ t: RNUrlTileOverlay) -> GMSURLTileLayer { + + let constructor: GMSTileURLConstructor = { (x: UInt, y: UInt, zoom: UInt) in + let urlString = t.url + .replacingOccurrences(of: "{x}", with: "\(x)") + .replacingOccurrences(of: "{y}", with: "\(y)") + .replacingOccurrences(of: "{z}", with: "\(zoom)") + return URL(string: urlString) + } + + let layer = GMSURLTileLayer(urlConstructor: constructor) + + layer.tileSize = Int(t.tileSize) + t.opacity.map { layer.opacity = Float($0) } + t.zIndex.map { layer.zIndex = Int32($0) } + t.fadeIn.map { layer.fadeIn = $0 } + + return layer + } +} diff --git a/ios/RNGoogleMapsPlusView.swift b/ios/RNGoogleMapsPlusView.swift index fe1c589..4fa4fc5 100644 --- a/ios/RNGoogleMapsPlusView.swift +++ b/ios/RNGoogleMapsPlusView.swift @@ -14,6 +14,7 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { private let polygonBuilder = MapPolygonBuilder() private let circleBuilder = MapCircleBuilder() private let heatmapBuilder = MapHeatmapBuilder() + private let urlTileOverlayBuilder = MapUrlTileOverlayBuilder() private let impl: GoogleMapsViewImpl @@ -110,9 +111,7 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { @MainActor var mapType: RNMapType? { didSet { - impl.mapType = mapType.map { - GMSMapViewType(rawValue: UInt($0.rawValue)) ?? .normal - } + impl.mapType = mapType?.toGMSMapViewType } } @@ -286,6 +285,30 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { } } + @MainActor + var urlTileOverlays: [RNUrlTileOverlay]? { + didSet { + let prevById = Dictionary( + (oldValue ?? []).map { ($0.id, $0) }, + uniquingKeysWith: { _, new in new } + ) + let nextById = Dictionary( + (urlTileOverlays ?? []).map { ($0.id, $0) }, + uniquingKeysWith: { _, new in new } + ) + + let removed = Set(prevById.keys).subtracting(nextById.keys) + removed.forEach { impl.removeUrlTileOverlay(id: $0) } + + for (id, next) in nextById { + impl.addUrlTileOverlay( + id: id, + urlTileOverlay: urlTileOverlayBuilder.build(next) + ) + } + } + } + @MainActor var locationConfig: RNLocationConfig? { didSet { @@ -302,6 +325,10 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { didSet { impl.onMapReady = onMapReady } } @MainActor + var onMapLoaded: ((RNRegion, RNCamera) -> Void)? { + didSet { impl.onMapLoaded = onMapLoaded } + } + @MainActor var onLocationUpdate: ((RNLocation) -> Void)? { didSet { impl.onLocationUpdate = onLocationUpdate } } @@ -314,31 +341,39 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { didSet { impl.onMapPress = onMapPress } } @MainActor - var onMarkerPress: ((String?) -> Void)? { + 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)? { + var onPolylinePress: ((String) -> Void)? { didSet { impl.onPolylinePress = onPolylinePress } } @MainActor - var onPolygonPress: ((String?) -> Void)? { + var onPolygonPress: ((String) -> Void)? { didSet { impl.onPolygonPress = onPolygonPress } } @MainActor - var onCirclePress: ((String?) -> Void)? { + var onCirclePress: ((String) -> Void)? { didSet { impl.onCirclePress = onCirclePress } } @MainActor - var onMarkerDragStart: ((String?, RNLatLng) -> Void)? { + var onMarkerDragStart: ((String, RNLatLng) -> Void)? { didSet { impl.onMarkerDragStart = onMarkerDragStart } } @MainActor - var onMarkerDrag: ((String?, RNLatLng) -> Void)? { + var onMarkerDrag: ((String, RNLatLng) -> Void)? { didSet { impl.onMarkerDrag = onMarkerDrag } } @MainActor - var onMarkerDragEnd: ((String?, RNLatLng) -> Void)? { + var onMarkerDragEnd: ((String, RNLatLng) -> Void)? { didSet { impl.onMarkerDragEnd = onMarkerDragEnd } } @MainActor @@ -350,6 +385,26 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { 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 } } @@ -362,6 +417,16 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { 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: RNCamera, animated: Bool?, durationMs: Double?) { let cam = camera.toGMSCameraPosition(current: impl.currentCamera) diff --git a/ios/extensions/GMSCoordinateBounds+Extension.swift b/ios/extensions/GMSCoordinateBounds+Extension.swift index 8676b88..83f5053 100644 --- a/ios/extensions/GMSCoordinateBounds+Extension.swift +++ b/ios/extensions/GMSCoordinateBounds+Extension.swift @@ -1,19 +1,10 @@ import GoogleMaps extension GMSCoordinateBounds { - func toRNRegion() -> RNRegion { - let center = CLLocationCoordinate2D( - latitude: (northEast.latitude + southWest.latitude) / 2.0, - longitude: (northEast.longitude + southWest.longitude) / 2.0 - ) - - let latDelta = northEast.latitude - southWest.latitude - let lngDelta = northEast.longitude - southWest.longitude - - return RNRegion( - center: center.toRNLatLng(), - latitudeDelta: latDelta, - longitudeDelta: lngDelta + func toRNLatLngBounds() -> RNLatLngBounds { + return RNLatLngBounds( + southwest: southWest.toRNLatLng(), + northeast: northEast.toRNLatLng() ) } } diff --git a/ios/extensions/GMSVisibleRegion+Extension.swift b/ios/extensions/GMSVisibleRegion+Extension.swift new file mode 100644 index 0000000..4a478dd --- /dev/null +++ b/ios/extensions/GMSVisibleRegion+Extension.swift @@ -0,0 +1,14 @@ +import GoogleMaps + +extension GMSVisibleRegion { + func toRNRegion() -> RNRegion { + let bounds = GMSCoordinateBounds(region: self) + return RNRegion( + nearLeft: nearLeft.toRNLatLng(), + nearRight: nearRight.toRNLatLng(), + farLeft: farLeft.toRNLatLng(), + farRight: farRight.toRNLatLng(), + latLngBounds: bounds.toRNLatLngBounds() + ) + } +} diff --git a/ios/extensions/MapObjectTag+Extension.swift b/ios/extensions/MapObjectTag+Extension.swift new file mode 100644 index 0000000..b668290 --- /dev/null +++ b/ios/extensions/MapObjectTag+Extension.swift @@ -0,0 +1,93 @@ +import GoogleMaps +import Foundation + +protocol MapObjectTag { + var id: String { get } +} + +struct MarkerTag: MapObjectTag { + let id: String + let iconSvg: RNMarkerSvg? + init(id: String, iconSvg: RNMarkerSvg? = nil) { + self.id = id + self.iconSvg = iconSvg + } +} + +struct PolylineTag: MapObjectTag { let id: String } +struct PolygonTag: MapObjectTag { let id: String } +struct CircleTag: MapObjectTag { let id: String } + +extension GMSMarker { + var tagData: MarkerTag { + get { + if let tag = userData as? MarkerTag { + return tag + } else { + print("[MapTag] Marker without tag detected at \(position)") + let fallback = MarkerTag(id: "unknown") + userData = fallback + return fallback + } + } + set { + userData = newValue + } + } + + var idTag: String { tagData.id } +} + +extension GMSPolyline { + var tagData: PolylineTag { + get { + if let tag = userData as? PolylineTag { + return tag + } else { + print("[MapTag] Polyline without tag detected") + let fallback = PolylineTag(id: "unknown") + userData = fallback + return fallback + } + } + set { userData = newValue } + } + + var idTag: String { tagData.id } +} + +extension GMSPolygon { + var tagData: PolygonTag { + get { + if let tag = userData as? PolygonTag { + return tag + } else { + print("[MapTag] Polygon without tag detected") + let fallback = PolygonTag(id: "unknown") + userData = fallback + return fallback + } + } + set { userData = newValue } + } + + var idTag: String { tagData.id } +} + +extension GMSCircle { + var tagData: CircleTag { + get { + if let tag = userData as? CircleTag { + return tag + } else { + print("[MapTag] Circle without tag detected") + let fallback = CircleTag(id: "unknown") + userData = fallback + return fallback + } + } + set { userData = newValue } + } + + var idTag: String { tagData.id } +} diff --git a/ios/extensions/RNLatLngBounds+Extension.swift b/ios/extensions/RNLatLngBounds+Extension.swift index 2d5ca16..72bd45c 100644 --- a/ios/extensions/RNLatLngBounds+Extension.swift +++ b/ios/extensions/RNLatLngBounds+Extension.swift @@ -4,12 +4,12 @@ extension RNLatLngBounds { func toCoordinateBounds() -> GMSCoordinateBounds { return GMSCoordinateBounds( coordinate: CLLocationCoordinate2D( - latitude: southWest.latitude, - longitude: southWest.longitude + latitude: southwest.latitude, + longitude: southwest.longitude ), coordinate: CLLocationCoordinate2D( - latitude: northEast.latitude, - longitude: northEast.longitude + latitude: northeast.latitude, + longitude: northeast.longitude ) ) } diff --git a/ios/extensions/RNMapType+Extension.swift b/ios/extensions/RNMapType+Extension.swift new file mode 100644 index 0000000..2d5ff48 --- /dev/null +++ b/ios/extensions/RNMapType+Extension.swift @@ -0,0 +1,18 @@ +import GoogleMaps + +extension RNMapType { + var toGMSMapViewType: GMSMapViewType { + switch self { + case .none: + return .none + case .normal: + return .normal + case .hybrid: + return .hybrid + case .satellite: + return .satellite + case .terrain: + return .terrain + } + } +} diff --git a/ios/extensions/UIImage+Extension.swift b/ios/extensions/UIImage+Extension.swift new file mode 100644 index 0000000..3dcb358 --- /dev/null +++ b/ios/extensions/UIImage+Extension.swift @@ -0,0 +1,45 @@ +import UIKit + +extension UIImage { + func encode( + targetSize: CGSize? = nil, + format: String = "png", + imageFormat: ImageFormat = .png, + quality: CGFloat = 1.0, + resultIsFile: Bool = false + ) -> String? { + var imageToEncode = self + + if let targetSize = targetSize { + UIGraphicsBeginImageContextWithOptions(targetSize, false, 0.0) + self.draw(in: CGRect(origin: .zero, size: targetSize)) + imageToEncode = UIGraphicsGetImageFromCurrentImageContext() ?? self + UIGraphicsEndImageContext() + } + + let data: Data? + switch imageFormat { + case .jpeg: + data = imageToEncode.jpegData(compressionQuality: quality) + case .png: + data = imageToEncode.pngData() + } + + guard let imageData = data else { return nil } + + if resultIsFile { + let filename = "snapshot_\(Int(Date().timeIntervalSince1970)).\(format)" + let fileURL = FileManager.default.temporaryDirectory + .appendingPathComponent(filename) + do { + try imageData.write(to: fileURL) + return fileURL.path + } catch { + return nil + } + } else { + let base64 = imageData.base64EncodedString() + return "data:image/\(format);base64,\(base64)" + } + } +} diff --git a/lefthook.yml b/lefthook.yml index f683cc3..8fd6d30 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -5,6 +5,9 @@ pre-commit: run: yarn format stage_fixed: true + - name: typeCheck + run: yarn typecheck + - name: lint-js run: yarn lint:js diff --git a/package.json b/package.json index 494cd53..77b4582 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,9 @@ "source": "src/index", "react-native": "src/index", "scripts": { - "typecheck": "tsc --noEmit", + "typecheck:lib": "tsc --noEmit -p tsconfig.json", + "typecheck:example": "tsc --noEmit -p example/tsconfig.json", + "typecheck": "yarn typecheck:lib && yarn typecheck:example", "lint": "yarn lint:js && yarn lint:android && yarn lint:ios", "lint:js": "eslint . --max-warnings 0 && yarn prettier --check .", "lint:android": "cd android && ktlint -F ./**/*.kt*", @@ -52,6 +54,7 @@ "android/fix-prefab.gradle", "android/gradle.properties", "android/CMakeLists.txt", + "android/proguard-rules.pro", "android/src", "ios/**/*.h", "ios/**/*.m", @@ -88,8 +91,8 @@ "@semantic-release/git": "10.0.1", "@semantic-release/npm": "13.1.1", "@types/jest": "30.0.0", - "@types/react": "19.2.2", - "clang-format-node": "2.0.2", + "@types/react": "19.1.1", + "clang-format-node": "2.0.3", "conventional-changelog-conventionalcommits": "9.1.0", "del-cli": "7.0.0", "eslint": "9.38.0", @@ -100,7 +103,7 @@ "lefthook": "2.0.0", "nitrogen": "0.30.2", "prettier": "3.6.2", - "react": "19.2.0", + "react": "19.1.1", "react-native": "0.82.1", "react-native-builder-bob": "0.40.13", "react-native-nitro-modules": "0.30.2", diff --git a/src/RNGoogleMapsPlusView.nitro.ts b/src/RNGoogleMapsPlusView.nitro.ts index 24e6cda..06f9b0f 100644 --- a/src/RNGoogleMapsPlusView.nitro.ts +++ b/src/RNGoogleMapsPlusView.nitro.ts @@ -28,6 +28,7 @@ import type { RNIndoorLevel, RNLatLngBounds, RNSnapshotOptions, + RNUrlTileOverlay, } from './types'; export interface RNGoogleMapsPlusViewProps extends HybridViewProps { @@ -48,21 +49,30 @@ export interface RNGoogleMapsPlusViewProps extends HybridViewProps { circles?: RNCircle[]; heatmaps?: RNHeatmap[]; kmlLayers?: RNKMLayer[]; + urlTileOverlays?: RNUrlTileOverlay[]; locationConfig?: RNLocationConfig; onMapError?: (error: RNMapErrorCode) => void; onMapReady?: (ready: boolean) => void; + onMapLoaded?: (region: RNRegion, camera: RNCamera) => void; onLocationUpdate?: (location: RNLocation) => void; onLocationError?: (error: RNLocationErrorCode) => void; onMapPress?: (coordinate: RNLatLng) => void; - onMarkerPress?: (id?: string | undefined) => void; - onPolylinePress?: (id?: string | undefined) => void; - onPolygonPress?: (id?: string | undefined) => void; - onCirclePress?: (id?: string | undefined) => void; - onMarkerDragStart?: (id: string | undefined, location: RNLatLng) => void; - onMarkerDrag?: (id: string | undefined, location: RNLatLng) => void; - onMarkerDragEnd?: (id: string | undefined, location: RNLatLng) => void; + onMapLongPress?: (coordinate: RNLatLng) => void; + onPoiPress?: (placeId: string, name: string, coordinate: RNLatLng) => void; + onMarkerPress?: (id: string) => void; + onPolylinePress?: (id: string) => void; + onPolygonPress?: (id: string) => void; + onCirclePress?: (id: string) => void; + onMarkerDragStart?: (id: string, location: RNLatLng) => void; + onMarkerDrag?: (id: string, location: RNLatLng) => void; + onMarkerDragEnd?: (id: string, location: RNLatLng) => void; onIndoorBuildingFocused?: (indoorBuilding: RNIndoorBuilding) => void; onIndoorLevelActivated?: (indoorLevel: RNIndoorLevel) => void; + onInfoWindowPress?: (id: string) => void; + onInfoWindowClose?: (id: string) => void; + onInfoWindowLongPress?: (id: string) => void; + onMyLocationPress?: (location: RNLocation) => void; + onMyLocationButtonPress?: (pressed: boolean) => void; onCameraChangeStart?: ( region: RNRegion, camera: RNCamera, @@ -81,6 +91,10 @@ export interface RNGoogleMapsPlusViewProps extends HybridViewProps { } export interface RNGoogleMapsPlusViewMethods extends HybridViewMethods { + showMarkerInfoWindow(id: string): void; + + hideMarkerInfoWindow(id: string): void; + setCamera(camera: RNCamera, animated?: boolean, durationMs?: number): void; setCameraToCoordinates( diff --git a/src/types.ts b/src/types.ts index 9b18036..d7d941f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -21,6 +21,8 @@ export type RNMapUiSettings = { tiltEnabled?: boolean; zoomControlsEnabled?: boolean; zoomGesturesEnabled?: boolean; + consumeOnMarkerPress?: boolean; + consumeOnMyLocationButtonPress?: boolean; }; export type RNLatLng = { @@ -29,8 +31,8 @@ export type RNLatLng = { }; export type RNLatLngBounds = { - northEast: RNLatLng; - southWest: RNLatLng; + southwest: RNLatLng; + northeast: RNLatLng; }; export type RNSnapshotOptions = { @@ -135,9 +137,11 @@ export type RNCamera = { }; export type RNRegion = { - center: RNLatLng; - latitudeDelta: number; - longitudeDelta: number; + nearLeft: RNLatLng; + nearRight: RNLatLng; + farLeft: RNLatLng; + farRight: RNLatLng; + latLngBounds: RNLatLngBounds; }; export type RNPosition = { @@ -167,6 +171,7 @@ export type RNMarker = { rotation?: number; infoWindowAnchor?: RNPosition; iconSvg?: RNMarkerSvg; + infoWindowIconSvg?: RNMarkerSvg; }; export type RNMarkerSvg = { @@ -241,6 +246,15 @@ export type RNKMLayer = { kmlString: string; }; +export type RNUrlTileOverlay = { + id: string; + zIndex?: number; + url: string; + tileSize: number; + opacity?: number; + fadeIn?: boolean; +}; + export type RNIndoorBuilding = { activeLevelIndex?: number; defaultLevelIndex?: number; diff --git a/yarn.lock b/yarn.lock index 08c9751..af5991a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3406,9 +3406,9 @@ __metadata: languageName: node linkType: hard -"@react-navigation/elements@npm:^2.6.5": - version: 2.6.5 - resolution: "@react-navigation/elements@npm:2.6.5" +"@react-navigation/elements@npm:^2.7.0": + version: 2.7.0 + resolution: "@react-navigation/elements@npm:2.7.0" dependencies: color: ^4.2.3 use-latest-callback: ^0.2.4 @@ -3422,15 +3422,17 @@ __metadata: peerDependenciesMeta: "@react-native-masked-view/masked-view": optional: true - checksum: ed6542b9dfaf04693445bb847651cc6bfdac5c2ffb687c1a5dcc473a05d12cd5e951c3ef5df854978aa93b6ced0bab1bbe94390df10cf24f2e3f9b72688661fb + checksum: 7d5abb2e1b459cb260177754813e22ffc067fe387440a65968615c49537abf538e8b359da082c6a168fcdc6ce37c2c17d95e0b5c5ac027162f1684cbfc06475c languageName: node linkType: hard -"@react-navigation/native-stack@npm:7.3.28": - version: 7.3.28 - resolution: "@react-navigation/native-stack@npm:7.3.28" +"@react-navigation/native-stack@npm:7.5.1": + version: 7.5.1 + resolution: "@react-navigation/native-stack@npm:7.5.1" dependencies: - "@react-navigation/elements": ^2.6.5 + "@react-navigation/elements": ^2.7.0 + color: ^4.2.3 + sf-symbols-typescript: ^2.1.0 warn-once: ^0.1.1 peerDependencies: "@react-navigation/native": ^7.1.18 @@ -3438,7 +3440,7 @@ __metadata: react-native: "*" react-native-safe-area-context: ">= 4.0.0" react-native-screens: ">= 4.0.0" - checksum: 7cefd3b10f531de8abacb77e5152727a320e5d68e41c721bb82bb8202184fe0850f17416dd1be2229bc513577f8dbcfd6f3c131f92ce7f3f62842c76995bfe6b + checksum: 88cce395335deb2d8d27bc76c19c48cd606c849d35220db8b5e94af15853792d8d5e9e277020a3817557486f85653cf3abcc282a85411c333e06a54345dbfe84 languageName: node linkType: hard @@ -3467,11 +3469,11 @@ __metadata: languageName: node linkType: hard -"@react-navigation/stack@npm:7.4.10": - version: 7.4.10 - resolution: "@react-navigation/stack@npm:7.4.10" +"@react-navigation/stack@npm:7.5.0": + version: 7.5.0 + resolution: "@react-navigation/stack@npm:7.5.0" dependencies: - "@react-navigation/elements": ^2.6.5 + "@react-navigation/elements": ^2.7.0 color: ^4.2.3 peerDependencies: "@react-navigation/native": ^7.1.18 @@ -3480,7 +3482,7 @@ __metadata: react-native-gesture-handler: ">= 2.0.0" react-native-safe-area-context: ">= 4.0.0" react-native-screens: ">= 4.0.0" - checksum: 4516f6263be71df7060cd590ba763c768aa86aee723fae46a95cbda22bb3737143bf73597bb2f2f1633ca4c88e782f070d8a8db61ef8016b82d5a046b734be77 + checksum: 577f73df3ced14bb989538215943635c9ce94e8cebf34d0a3e70974fbba4b6e01c25809caa1177918bff87f3912e9e3af9778f738ea092103d3da61a698a053a languageName: node linkType: hard @@ -3946,15 +3948,6 @@ __metadata: languageName: node linkType: hard -"@types/react@npm:19.2.2": - version: 19.2.2 - resolution: "@types/react@npm:19.2.2" - dependencies: - csstype: ^3.0.2 - checksum: 7eb2d316dd5a6c02acb416524b50bae932c38d055d26e0f561ca23c009c686d16a2b22fcbb941eecbe2ecb167f119e29b9d0142d9d056dd381352c43413b60da - languageName: node - linkType: hard - "@types/stack-utils@npm:^2.0.0, @types/stack-utils@npm:^2.0.3": version: 2.0.3 resolution: "@types/stack-utils@npm:2.0.3" @@ -5277,13 +5270,13 @@ __metadata: languageName: node linkType: hard -"clang-format-node@npm:2.0.2": - version: 2.0.2 - resolution: "clang-format-node@npm:2.0.2" +"clang-format-node@npm:2.0.3": + version: 2.0.3 + resolution: "clang-format-node@npm:2.0.3" bin: clang-format: build/cli.js clang-format-node: build/cli.js - checksum: cbb34e5e5e016fce30242e0a76b9a78c1e5f7058cc97005d018301f2ac0ebca8150e7cb71fef86926f5d82fbeb5c89cad18f81f0fcd6f4ce5132da30b2e1852e + checksum: 15d708e6887cc9a4045c5403826a925c8f2c4ff6700f447cc31fce49a979b299388d3ea7790428d33130fc8d0c550a429f92a927dc5852b20fdcfbee3cc03e7c languageName: node linkType: hard @@ -11806,9 +11799,9 @@ __metadata: languageName: node linkType: hard -"react-native-gesture-handler@npm:2.28.0": - version: 2.28.0 - resolution: "react-native-gesture-handler@npm:2.28.0" +"react-native-gesture-handler@npm:2.29.0": + version: 2.29.0 + resolution: "react-native-gesture-handler@npm:2.29.0" dependencies: "@egjs/hammerjs": ^2.0.17 hoist-non-react-statics: ^3.3.0 @@ -11816,7 +11809,7 @@ __metadata: peerDependencies: react: "*" react-native: "*" - checksum: 7bcd7db784b12565fdd5916bbebc2d3511a63159ca553d33e430008940ba7d209f1e85ef02968a920ed19c414fabe7d2c18cc0e967dd4889aae266788562d1e9 + checksum: 78bc800ccc792a6e09ca2b17fd1f85e12c25892338e9336864b4cd12eceab9c4db8fb95d7e91fbe4796a7743e6d17cbe13d7877217030703d56236ff2d0e122c languageName: node linkType: hard @@ -11834,21 +11827,21 @@ __metadata: "@react-native/metro-config": 0.82.1 "@react-native/typescript-config": 0.82.1 "@react-navigation/native": 7.1.18 - "@react-navigation/native-stack": 7.3.28 - "@react-navigation/stack": 7.4.10 + "@react-navigation/native-stack": 7.5.1 + "@react-navigation/stack": 7.5.0 "@types/react": 19.1.1 react: 19.1.1 react-hook-form: 7.65.0 react-native: 0.82.1 react-native-builder-bob: 0.40.13 react-native-clusterer: 5.0.1 - react-native-gesture-handler: 2.28.0 + react-native-gesture-handler: 2.29.0 react-native-google-maps-plus: "workspace:*" react-native-monorepo-config: 0.2.2 react-native-nitro-modules: 0.30.2 react-native-reanimated: 4.1.3 react-native-safe-area-context: 5.6.1 - react-native-screens: 4.17.1 + react-native-screens: 4.18.0 react-native-worklets: 0.6.1 superstruct: 2.0.2 languageName: unknown @@ -11871,8 +11864,8 @@ __metadata: "@semantic-release/git": 10.0.1 "@semantic-release/npm": 13.1.1 "@types/jest": 30.0.0 - "@types/react": 19.2.2 - clang-format-node: 2.0.2 + "@types/react": 19.1.1 + clang-format-node: 2.0.3 conventional-changelog-conventionalcommits: 9.1.0 del-cli: 7.0.0 eslint: 9.38.0 @@ -11883,7 +11876,7 @@ __metadata: lefthook: 2.0.0 nitrogen: 0.30.2 prettier: 3.6.2 - react: 19.2.0 + react: 19.1.1 react-native: 0.82.1 react-native-builder-bob: 0.40.13 react-native-nitro-modules: 0.30.2 @@ -11965,16 +11958,16 @@ __metadata: languageName: node linkType: hard -"react-native-screens@npm:4.17.1": - version: 4.17.1 - resolution: "react-native-screens@npm:4.17.1" +"react-native-screens@npm:4.18.0": + version: 4.18.0 + resolution: "react-native-screens@npm:4.18.0" dependencies: react-freeze: ^1.0.0 warn-once: ^0.1.0 peerDependencies: react: "*" react-native: "*" - checksum: 7c17118bc1313acd6001e63bf1d6c6a95ca5250c9a06450cceec50768571648d2d5f3e17ed19fb757d176a65bbe80fcba142b937a92cbbc795a6da71243c375e + checksum: b7942efe7bf316ad66aabf6e3b8b999268d3b88b3d23affb0f90f627d8dd980172f79b48abf476d10c3466ba5123240ee3f18f8d0ff7db5b79b9772cb520afa0 languageName: node linkType: hard @@ -12066,13 +12059,6 @@ __metadata: languageName: node linkType: hard -"react@npm:19.2.0": - version: 19.2.0 - resolution: "react@npm:19.2.0" - checksum: 33dd01bf699e1c5040eb249e0f552519adf7ee90b98c49d702a50bf23af6852ea46023a5f7f93966ab10acd7a45428fa0f193c686ecdaa7a75a03886e53ec3fe - languageName: node - linkType: hard - "read-cmd-shim@npm:^5.0.0": version: 5.0.0 resolution: "read-cmd-shim@npm:5.0.0" @@ -12615,6 +12601,13 @@ __metadata: languageName: node linkType: hard +"sf-symbols-typescript@npm:^2.1.0": + version: 2.1.0 + resolution: "sf-symbols-typescript@npm:2.1.0" + checksum: 63ddd79f660268e82889618bcf73d111e013b11d8795c6c89232c21b6770e58a37c7e103590caa4d2a6077b1e211fbaeaf39b665cfaf25f5507e03365e0faebe + languageName: node + linkType: hard + "shebang-command@npm:^2.0.0": version: 2.0.0 resolution: "shebang-command@npm:2.0.0"