diff --git a/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt b/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt index bcb26fd..d98eb6c 100644 --- a/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt +++ b/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt @@ -12,6 +12,8 @@ import com.google.android.gms.maps.GoogleMap import com.google.android.gms.maps.GoogleMapOptions import com.google.android.gms.maps.MapView import com.google.android.gms.maps.model.CameraPosition +import com.google.android.gms.maps.model.Circle +import com.google.android.gms.maps.model.CircleOptions import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps.model.LatLngBounds import com.google.android.gms.maps.model.MapColorScheme @@ -34,20 +36,26 @@ class GoogleMapsViewImpl( GoogleMap.OnCameraIdleListener, GoogleMap.OnMapClickListener, GoogleMap.OnMarkerClickListener, + GoogleMap.OnPolylineClickListener, + GoogleMap.OnPolygonClickListener, + GoogleMap.OnCircleClickListener, LifecycleEventListener { private var initialized = false private var mapReady = false private var googleMap: GoogleMap? = null private var mapView: MapView? = null - private val pendingPolygons = mutableListOf>() - private val pendingPolylines = mutableListOf>() + private val pendingMarkers = mutableListOf>() - private var cameraMoveReason = -1 + private val pendingPolylines = mutableListOf>() + private val pendingPolygons = mutableListOf>() + private val pendingCircles = mutableListOf>() - private val polygonsById = mutableMapOf() - private val polylinesById = mutableMapOf() private val markersById = mutableMapOf() + private val polylinesById = mutableMapOf() + private val polygonsById = mutableMapOf() + private val circlesById = mutableMapOf() + private var cameraMoveReason = -1 private var lastSubmittedLocation: Location? = null private var lastSubmittedCameraPosition: CameraPosition? = null @@ -112,6 +120,9 @@ class 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) } initLocationCallbacks() @@ -282,6 +293,13 @@ class GoogleMapsViewImpl( } pendingPolygons.clear() } + + if (pendingCircles.isNotEmpty()) { + pendingCircles.forEach { (id, opts) -> + internalAddCircle(id, opts) + } + pendingCircles.clear() + } } var buildingEnabled: Boolean? = null @@ -388,6 +406,9 @@ class GoogleMapsViewImpl( 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 onCameraChangeStart: ((RNRegion, RNCamera, Boolean) -> Unit)? = null var onCameraChange: ((RNRegion, RNCamera, Boolean) -> Unit)? = null var onCameraChangeComplete: ((RNRegion, RNCamera, Boolean) -> Unit)? = null @@ -669,18 +690,76 @@ class GoogleMapsViewImpl( pendingPolygons.clear() } + fun addCircle( + id: String, + opts: CircleOptions, + ) { + if (googleMap == null) { + pendingCircles.add(id to opts) + return + } + + onUi { + circlesById.remove(id)?.remove() + } + internalAddCircle(id, opts) + } + + private fun internalAddCircle( + id: String, + opts: CircleOptions, + ) { + onUi { + val circle = + googleMap?.addCircle(opts).also { + it?.tag = id + } + if (circle != null) { + circlesById[id] = circle + } + } + } + + fun updateCircle( + id: String, + block: (Circle) -> Unit, + ) { + val circle = circlesById[id] ?: return + onUi { + block(circle) + } + } + + fun removeCircle(id: String) { + onUi { + circlesById.remove(id)?.remove() + } + } + + fun clearCircles() { + onUi { + circlesById.values.forEach { it.remove() } + } + circlesById.clear() + pendingCircles.clear() + } + fun destroyInternal() { onUi { markerOptions.cancelAllJobs() clearMarkers() clearPolylines() clearPolygons() + clearCircles() locationHandler.stop() googleMap?.apply { setOnCameraMoveStartedListener(null) setOnCameraMoveListener(null) setOnCameraIdleListener(null) setOnMarkerClickListener(null) + setOnPolylineClickListener(null) + setOnPolygonClickListener(null) + setOnCircleClickListener(null) setOnMapClickListener(null) } googleMap = null @@ -740,6 +819,18 @@ class GoogleMapsViewImpl( return true } + override fun onPolylineClick(polyline: Polyline) { + onPolylinePress?.invoke(polyline.tag?.toString() ?: "unknown") + } + + override fun onPolygonClick(polygon: Polygon) { + onPolygonPress?.invoke(polygon.tag?.toString() ?: "unknown") + } + + override fun onCircleClick(circle: Circle) { + onCirclePress?.invoke(circle.tag?.toString() ?: "unknown") + } + override fun onMapClick(coordinates: LatLng) { onMapPress?.invoke( RNLatLng(coordinates.latitude, coordinates.longitude), diff --git a/android/src/main/java/com/rngooglemapsplus/MapCircle.kt b/android/src/main/java/com/rngooglemapsplus/MapCircle.kt new file mode 100644 index 0000000..eb919b3 --- /dev/null +++ b/android/src/main/java/com/rngooglemapsplus/MapCircle.kt @@ -0,0 +1,29 @@ +package com.rngooglemapsplus + +import com.facebook.react.uimanager.PixelUtil.dpToPx +import com.google.android.gms.maps.model.CircleOptions +import com.google.android.gms.maps.model.LatLng + +class MapCircleOptions { + fun buildCircleOptions(circle: RNCircle): CircleOptions = + CircleOptions().apply { + center(LatLng(circle.center.latitude, circle.center.longitude)) + circle.radius?.let { radius(it) } + circle.strokeWidth?.let { strokeWidth(it.dpToPx()) } + circle.strokeColor?.let { strokeColor(it.toColor()) } + circle.fillColor?.let { fillColor(it.toColor()) } + circle.pressable?.let { clickable(it) } + circle.zIndex?.let { zIndex(it.toFloat()) } + } +} + +fun RNCircle.circleEquals(b: RNCircle): Boolean { + if (zIndex != b.zIndex) return false + if (pressable != b.pressable) return false + if (center != b.center) return false + if (radius != b.radius) return false + if (strokeWidth != b.strokeWidth) return false + if (strokeColor != b.strokeColor) return false + if (fillColor != b.fillColor) return false + return true +} diff --git a/android/src/main/java/com/rngooglemapsplus/MapMarker.kt b/android/src/main/java/com/rngooglemapsplus/MapMarker.kt index 53cc25b..20da17f 100644 --- a/android/src/main/java/com/rngooglemapsplus/MapMarker.kt +++ b/android/src/main/java/com/rngooglemapsplus/MapMarker.kt @@ -36,11 +36,12 @@ class MarkerOptions( m: RNMarker, icon: BitmapDescriptor, ): MarkerOptions = - MarkerOptions() - .position(LatLng(m.coordinate.latitude, m.coordinate.longitude)) - .zIndex(m.zIndex.toFloat()) - .icon(icon) - .anchor((m.anchor?.x ?: 0.5).toFloat(), (m.anchor?.y ?: 0.5).toFloat()) + MarkerOptions().apply { + position(LatLng(m.coordinate.latitude, m.coordinate.longitude)) + anchor((m.anchor?.x ?: 0.5).toFloat(), (m.anchor?.y ?: 0.5).toFloat()) + icon(icon) + m.zIndex?.let { zIndex(it.toFloat()) } + } fun buildIconAsync( id: String, diff --git a/android/src/main/java/com/rngooglemapsplus/MapPolygon.kt b/android/src/main/java/com/rngooglemapsplus/MapPolygon.kt index f2a152e..6c50ee1 100644 --- a/android/src/main/java/com/rngooglemapsplus/MapPolygon.kt +++ b/android/src/main/java/com/rngooglemapsplus/MapPolygon.kt @@ -15,12 +15,14 @@ class MapPolygonOptions { poly.fillColor?.let { fillColor(it.toColor()) } poly.strokeColor?.let { strokeColor(it.toColor()) } poly.strokeWidth?.let { strokeWidth(it.dpToPx()) } - zIndex(poly.zIndex.toFloat()) + poly.pressable?.let { clickable(it) } + poly.zIndex?.let { zIndex(it.toFloat()) } } } fun RNPolygon.polygonEquals(b: RNPolygon): Boolean { if (zIndex != b.zIndex) return false + if (pressable != b.pressable) return false if (strokeWidth != b.strokeWidth) return false if (fillColor != b.fillColor) return false if (strokeColor != b.strokeColor) return false diff --git a/android/src/main/java/com/rngooglemapsplus/MapPolyline.kt b/android/src/main/java/com/rngooglemapsplus/MapPolyline.kt index 86e01b9..1cd22ac 100644 --- a/android/src/main/java/com/rngooglemapsplus/MapPolyline.kt +++ b/android/src/main/java/com/rngooglemapsplus/MapPolyline.kt @@ -22,7 +22,8 @@ class MapPolylineOptions { pl.lineCap?.let { endCap(mapLineCap(it)) } pl.lineJoin?.let { jointType(mapLineJoin(it)) } pl.color?.let { color(it.toColor()) } - zIndex(pl.zIndex.toFloat()) + pl.pressable?.let { clickable(it) } + pl.zIndex?.let { zIndex(it.toFloat()) } } fun mapLineCap(type: RNLineCapType?): Cap = @@ -43,6 +44,7 @@ class MapPolylineOptions { fun RNPolyline.polylineEquals(b: RNPolyline): Boolean { if (zIndex != b.zIndex) return false + if (pressable != b.pressable) return false if ((width ?: 0.0) != (b.width ?: 0.0)) return false if (lineCap != b.lineCap) return false if (lineJoin != b.lineJoin) return false diff --git a/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt b/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt index 55386f2..6d8a90e 100644 --- a/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt +++ b/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt @@ -5,6 +5,7 @@ import com.facebook.react.bridge.UiThreadUtil import com.facebook.react.uimanager.PixelUtil.dpToPx import com.facebook.react.uimanager.ThemedReactContext import com.google.android.gms.maps.model.CameraPosition +import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps.model.MapColorScheme import com.google.android.gms.maps.model.MapStyleOptions import com.margelo.nitro.core.Promise @@ -21,6 +22,7 @@ class RNGoogleMapsPlusView( private val markerOptions = MarkerOptions() private val polylineOptions = MapPolylineOptions() private val polygonOptions = MapPolygonOptions() + private val circleOptions = MapCircleOptions() override val view = GoogleMapsViewImpl(context, locationHandler, playServiceHandler, markerOptions) @@ -101,27 +103,24 @@ class RNGoogleMapsPlusView( } else if (!prev.markerEquals(next)) { view.updateMarker(id) { m -> onUi { - if (prev.coordinate != next.coordinate) { - m.position = - com.google.android.gms.maps.model.LatLng( - next.coordinate.latitude, - next.coordinate.longitude, - ) - } - if (prev.zIndex != next.zIndex) { - m.zIndex = next.zIndex.toFloat() + m.position = + LatLng( + next.coordinate.latitude, + next.coordinate.longitude, + ) + next.zIndex?.let { m.zIndex = it.toFloat() } ?: run { + m.zIndex = 0f } + if (!prev.markerStyleEquals(next)) { markerOptions.buildIconAsync(id, next) { icon -> m.setIcon(icon) } } - if (prev.anchor != next.anchor) { - m.setAnchor( - (next.anchor?.x ?: 0.5).toFloat(), - (next.anchor?.y ?: 0.5).toFloat(), - ) - } + m.setAnchor( + (next.anchor?.x ?: 0.5).toFloat(), + (next.anchor?.y ?: 0.5).toFloat(), + ) } } } @@ -147,8 +146,8 @@ class RNGoogleMapsPlusView( onUi { gms.points = next.coordinates.map { - com.google.android.gms.maps.model - .LatLng(it.latitude, it.longitude) + + LatLng(it.latitude, it.longitude) } next.width?.let { gms.width = it.dpToPx() } next.lineCap?.let { @@ -158,7 +157,7 @@ class RNGoogleMapsPlusView( } next.lineJoin?.let { gms.jointType = polylineOptions.mapLineJoin(it) } next.color?.let { gms.color = it.toColor() } - gms.zIndex = next.zIndex.toFloat() + next.zIndex?.let { gms.zIndex = it.toFloat() } } } } @@ -190,7 +189,36 @@ class RNGoogleMapsPlusView( next.fillColor?.let { gmsPoly.fillColor = it.toColor() } next.strokeColor?.let { gmsPoly.strokeColor = it.toColor() } next.strokeWidth?.let { gmsPoly.strokeWidth = it.dpToPx() } - gmsPoly.zIndex = next.zIndex.toFloat() + next.zIndex?.let { gmsPoly.zIndex = it.toFloat() } + } + } + } + } + field = value + } + + override var circles: Array? = null + set(value) { + val prevById = field?.associateBy { it.id } ?: emptyMap() + val nextById = value?.associateBy { it.id } ?: emptyMap() + + (prevById.keys - nextById.keys).forEach { id -> + view.removeCircle(id) + } + + nextById.forEach { (id, next) -> + val prev = prevById[id] + if (prev == null) { + view.addCircle(id, circleOptions.buildCircleOptions(next)) + } else if (!prev.circleEquals(next)) { + view.updateCircle(id) { gmsCircle -> + onUi { + gmsCircle.center = LatLng(next.center.latitude, next.center.longitude) + next.radius?.let { gmsCircle.radius = it } + next.strokeWidth?.let { gmsCircle.strokeWidth = it.dpToPx() } + next.strokeColor?.let { gmsCircle.strokeColor = it.toColor() } + next.fillColor?.let { gmsCircle.fillColor = it.toColor() } + next.zIndex?.let { gmsCircle.zIndex = it.toFloat() } ?: run { gmsCircle.zIndex = 0f } } } } @@ -227,6 +255,21 @@ class RNGoogleMapsPlusView( view.onMarkerPress = cb } + override var onPolylinePress: ((String) -> Unit)? = null + set(cb) { + view.onPolylinePress = cb + } + + override var onPolygonPress: ((String) -> Unit)? = null + set(cb) { + view.onPolygonPress = cb + } + + override var onCirclePress: ((String) -> Unit)? = null + set(cb) { + view.onCirclePress = cb + } + override var onCameraChangeStart: ((RNRegion, RNCamera, Boolean) -> Unit)? = null set(cb) { view.onCameraChangeStart = cb diff --git a/example/src/App.tsx b/example/src/App.tsx index b295ef3..1018126 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -17,6 +17,7 @@ import type { GoogleMapsViewRef, RNRegion, RNLatLng, + RNCircle, } from '../../src'; import { GoogleMapsView, GoogleMapsModule } from '../../src'; import { callback } from 'react-native-nitro-modules'; @@ -205,6 +206,7 @@ const randomCoordinates = ( const makePolygon = (id: number): RNPolygon => ({ id: id.toString(), zIndex: id, + pressable: true, coordinates: [ randomCoordinates(37.7749, -122.4194, 0.01), randomCoordinates(37.7749, -122.4194, 0.01), @@ -219,6 +221,7 @@ const makePolygon = (id: number): RNPolygon => ({ const makePolyline = (id: number): RNPolyline => ({ id: id.toString(), zIndex: id, + pressable: true, coordinates: [ randomCoordinates(37.7749, -122.4194, 0.02), randomCoordinates(37.7749, -122.4194, 0.02), @@ -227,8 +230,19 @@ const makePolyline = (id: number): RNPolyline => ({ lineCap: id % 2 === 0 ? 'round' : 'square', lineJoin: id % 3 === 0 ? 'bevel' : 'round', - color: id % 2 === 0 ? '#ff0000' : '#0000ff', - width: 1 + (id % 4), + color: id % 2 === 0 ? '#00ff00' : '#ff0000', + width: 2 + (id % 4), +}); + +const makeCircle = (id: number): RNCircle => ({ + id: id.toString(), + zIndex: id, + pressable: true, + center: randomCoordinates(37.7749, -122.4194, 0.02), + radius: 100 + (id % 5), + strokeWidth: 1 + (id % 5), + strokeColor: '#ff0000', + fillColor: '#0000ff', }); export const makeMarker = (id: number): RNMarker => ({ @@ -260,6 +274,10 @@ export default function App() { Array.from({ length: 1 }, (_, i) => makePolyline(i + 1)) ); + const [circles] = useState( + Array.from({ length: 1 }, (_, i) => makeCircle(i + 1)) + ); + useEffect(() => { const interval = setInterval(() => { if (stressTest) { @@ -391,6 +409,21 @@ export default function App() { console.log('Marker pressed', id); }, }} + onPolylinePress={{ + f: function (id: string): void { + console.log('Polyline pressed', id); + }, + }} + onPolygonPress={{ + f: function (id: string): void { + console.log('Polygon pressed', id); + }, + }} + onCirclePress={{ + f: function (id: string): void { + console.log('Circle pressed', id); + }, + }} onCameraChangeStart={{ f: function ( region: RNRegion, @@ -431,6 +464,7 @@ export default function App() { markers={show ? [...markers] : []} polygons={show ? polygons : []} polylines={show ? polylines : []} + circles={show ? circles : []} /> diff --git a/ios/GoogleMapViewImpl.swift b/ios/GoogleMapViewImpl.swift index 3aa0e12..2c76731 100644 --- a/ios/GoogleMapViewImpl.swift +++ b/ios/GoogleMapViewImpl.swift @@ -10,13 +10,15 @@ final class GoogleMapsViewImpl: UIView, GMSMapViewDelegate { private var initialized = false private var mapReady = false - private var pendingPolygons: [(id: String, polygon: GMSPolygon)] = [] - private var pendingPolylines: [(id: String, polyline: GMSPolyline)] = [] private var pendingMarkers: [(id: String, marker: GMSMarker)] = [] + private var pendingPolylines: [(id: String, polyline: GMSPolyline)] = [] + private var pendingPolygons: [(id: String, polygon: GMSPolygon)] = [] + private var pendingCircles: [(id: String, circle: GMSCircle)] = [] - private var polygonsById: [String: GMSPolygon] = [:] - private var polylinesById: [String: GMSPolyline] = [:] private var markersById: [String: GMSMarker] = [:] + private var polylinesById: [String: GMSPolyline] = [:] + private var polygonsById: [String: GMSPolygon] = [:] + private var circlesById: [String: GMSCircle] = [:] private var cameraMoveReasonIsGesture: Bool = false private var lastSubmittedCameraPosition: GMSCameraPosition? @@ -28,6 +30,9 @@ final class GoogleMapsViewImpl: UIView, GMSMapViewDelegate { var onLocationError: ((_ error: RNLocationErrorCode) -> Void)? var onMapPress: ((RNLatLng) -> Void)? var onMarkerPress: ((String) -> Void)? + var onPolylinePress: ((String) -> Void)? + var onPolygonPress: ((String) -> Void)? + var onCirclePress: ((String) -> Void)? var onCameraChangeStart: ((RNRegion, RNCamera, Bool) -> Void)? var onCameraChange: ((RNRegion, RNCamera, Bool) -> Void)? var onCameraChangeComplete: ((RNRegion, RNCamera, Bool) -> Void)? @@ -64,7 +69,7 @@ final class GoogleMapsViewImpl: UIView, GMSMapViewDelegate { @MainActor func initMapView(mapId: String?, liteMode: Bool?, camera: GMSCameraPosition?) { - if(initialized) {return} + if initialized { return } initialized = true let options = GMSMapViewOptions() options.frame = bounds @@ -169,6 +174,12 @@ final class GoogleMapsViewImpl: UIView, GMSMapViewDelegate { } pendingPolygons.removeAll() } + if !pendingCircles.isEmpty { + pendingCircles.forEach { + addCircleInternal(id: $0.id, circle: $0.circle) + } + pendingCircles.removeAll() + } } var currentCamera: GMSCameraPosition? { @@ -420,11 +431,47 @@ final class GoogleMapsViewImpl: UIView, GMSMapViewDelegate { pendingPolygons.removeAll() } + @MainActor + func addCircle(id: String, circle: GMSCircle) { + if mapView == nil { + pendingCircles.append((id, circle)) + return + } + if let old = circlesById.removeValue(forKey: id) { old.map = nil } + addCircleInternal(id: id, circle: circle) + } + + @MainActor + private func addCircleInternal(id: String, circle: GMSCircle) { + circle.map = mapView + circle.userData = id + circlesById[id] = circle + } + + @MainActor + func updateCircle(id: String, block: @escaping (GMSCircle) -> Void) { + guard let circle = circlesById[id] else { return } + block(circle) + } + + @MainActor + func removeCircle(id: String) { + if let circle = circlesById.removeValue(forKey: id) { circle.map = nil } + } + + @MainActor + func clearCircles() { + circlesById.values.forEach { $0.map = nil } + circlesById.removeAll() + pendingCircles.removeAll() + } + func deinitInternal() { markerOptions.cancelAllIconTasks() clearMarkers() clearPolylines() clearPolygons() + clearCircles() locationHandler.stop() mapView?.clear() mapView?.delegate = nil @@ -567,7 +614,8 @@ final class GoogleMapsViewImpl: UIView, GMSMapViewDelegate { RNLatLng( latitude: coordinate.latitude, longitude: coordinate.longitude - )) + ) + ) } func mapView(_ mapView: GMSMapView, didTap marker: GMSMarker) -> Bool { @@ -575,4 +623,23 @@ final class GoogleMapsViewImpl: UIView, GMSMapViewDelegate { onMarkerPress?(id) return true } + + func mapView(_ mapView: GMSMapView, didTap overlay: GMSOverlay) { + switch overlay { + case let circle as GMSCircle: + let id = (circle.userData as? String) ?? "unknown" + onCirclePress?(id) + + case let polygon as GMSPolygon: + let id = (polygon.userData as? String) ?? "unknown" + onPolygonPress?(id) + + case let polyline as GMSPolyline: + let id = (polyline.userData as? String) ?? "unknown" + onPolylinePress?(id) + + default: + break + } + } } diff --git a/ios/MapCircle.swift b/ios/MapCircle.swift new file mode 100644 index 0000000..e565298 --- /dev/null +++ b/ios/MapCircle.swift @@ -0,0 +1,43 @@ +import GoogleMaps + +class MapCircleOptions { + + func buildCircle(_ c: RNCircle) -> GMSCircle { + let circle = GMSCircle() + circle.position = CLLocationCoordinate2D( + latitude: c.center.latitude, + longitude: c.center.longitude + ) + if let r = c.radius { circle.radius = r } + if let fc = c.fillColor?.toUIColor() { circle.fillColor = fc } + if let sc = c.strokeColor?.toUIColor() { circle.strokeColor = sc } + if let sw = c.strokeWidth { circle.strokeWidth = CGFloat(sw) } + if let pr = c.pressable { circle.isTappable = pr } + if let zi = c.zIndex { circle.zIndex = Int32(zi) } + + return circle + } +} + +extension RNCircle { + func updateCircle(_ next: RNCircle, _ c: GMSCircle) { + c.position = CLLocationCoordinate2D( + latitude: next.center.latitude, + longitude: next.center.longitude + ) + if let r = next.radius { c.radius = r } + if let fc = next.fillColor?.toUIColor() { c.fillColor = fc } + if let sc = next.strokeColor?.toUIColor() { c.strokeColor = sc } + if let sw = next.strokeWidth { c.strokeWidth = CGFloat(sw) } + if let pr = next.pressable { c.isTappable = pr } + if let zi = next.zIndex { c.zIndex = Int32(zi) } + } + + func circleEquals(_ b: RNCircle) -> Bool { + zIndex == b.zIndex && pressable == b.pressable + && center.latitude == b.center.latitude + && center.longitude == b.center.longitude && radius == b.radius + && strokeWidth == b.strokeWidth && strokeColor == b.strokeColor + && fillColor == b.fillColor + } +} diff --git a/ios/MapMarker.swift b/ios/MapMarker.swift index 7902b32..4d142e8 100644 --- a/ios/MapMarker.swift +++ b/ios/MapMarker.swift @@ -18,7 +18,6 @@ final class MapMarkerOptions { longitude: m.coordinate.longitude ) ) - marker.zIndex = Int32(m.zIndex) marker.userData = m.id marker.tracksViewChanges = true marker.icon = icon @@ -26,6 +25,7 @@ final class MapMarkerOptions { x: m.anchor?.x ?? 0.5, y: m.anchor?.y ?? 0.5 ) + if let zi = m.zIndex { marker.zIndex = Int32(zi) } DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak marker] in marker?.tracksViewChanges = false } @@ -71,24 +71,17 @@ final class MapMarkerOptions { @MainActor func updateMarker(_ prev: RNMarker, _ next: RNMarker, _ m: GMSMarker) { - if prev.coordinate.latitude != next.coordinate.latitude - || prev.coordinate.longitude != next.coordinate.longitude { - m.position = CLLocationCoordinate2D( - latitude: next.coordinate.latitude, - longitude: next.coordinate.longitude - ) - } + m.position = CLLocationCoordinate2D( + latitude: next.coordinate.latitude, + longitude: next.coordinate.longitude + ) - if prev.zIndex != next.zIndex { - m.zIndex = Int32(next.zIndex) - } + if let zi = next.zIndex { m.zIndex = Int32(zi) } - if prev.anchor?.x != next.anchor?.x || prev.anchor?.y != next.anchor?.y { - m.groundAnchor = CGPoint( - x: next.anchor?.x ?? 0.5, - y: next.anchor?.y ?? 0.5 - ) - } + m.groundAnchor = CGPoint( + x: next.anchor?.x ?? 0.5, + y: next.anchor?.y ?? 0.5 + ) if !prev.markerStyleEquals(next) { buildIconAsync(next.id, next) { img in diff --git a/ios/MapPolygon.swift b/ios/MapPolygon.swift index 7b0036c..5e5f66b 100644 --- a/ios/MapPolygon.swift +++ b/ios/MapPolygon.swift @@ -13,7 +13,8 @@ class MapPolygonOptions { if let fc = p.fillColor?.toUIColor() { pg.fillColor = fc } if let sc = p.strokeColor?.toUIColor() { pg.strokeColor = sc } if let sw = p.strokeWidth { pg.strokeWidth = CGFloat(sw) } - pg.zIndex = Int32(p.zIndex) + if let pr = p.pressable { pg.isTappable = pr } + if let zi = p.zIndex { pg.zIndex = Int32(zi) } return pg } } @@ -34,11 +35,13 @@ extension RNPolygon { if let fc = next.fillColor?.toUIColor() { pg.fillColor = fc } if let sc = next.strokeColor?.toUIColor() { pg.strokeColor = sc } if let sw = next.strokeWidth { pg.strokeWidth = CGFloat(sw) } - pg.zIndex = Int32(next.zIndex) + if let pr = next.pressable { pg.isTappable = pr } + if let zi = next.zIndex { pg.zIndex = Int32(zi) } } func polygonEquals(_ b: RNPolygon) -> Bool { guard zIndex == b.zIndex, + pressable == b.pressable, strokeWidth == b.strokeWidth, fillColor == b.fillColor, strokeColor == b.strokeColor, diff --git a/ios/MapPolyline.swift b/ios/MapPolyline.swift index 5f67ef2..91904fb 100644 --- a/ios/MapPolyline.swift +++ b/ios/MapPolyline.swift @@ -11,13 +11,14 @@ class MapPolylineOptions { let pl = GMSPolyline(path: path) if let w = p.width { pl.strokeWidth = CGFloat(w) } if let c = p.color?.toUIColor() { pl.strokeColor = c } - pl.zIndex = Int32(p.zIndex) if let cap = p.lineCap { /// pl.lineCap = mapLineCap(cap) } if let join = p.lineJoin { /// pl.strokeJoin = mapLineJoin(join) } + if let pr = p.pressable { pl.isTappable = pr } + if let zi = p.zIndex { pl.zIndex = Int32(zi) } return pl } } @@ -61,7 +62,8 @@ extension RNPolyline { if let c = next.color?.toUIColor() { pl.strokeColor = c } - pl.zIndex = Int32(next.zIndex) + if let pr = next.pressable { pl.isTappable = pr } + if let zi = next.zIndex { pl.zIndex = Int32(zi) } } func polylineEquals(_ b: RNPolyline) -> Bool { diff --git a/ios/RNGoogleMapsPlusView.swift b/ios/RNGoogleMapsPlusView.swift index 6568395..e10a320 100644 --- a/ios/RNGoogleMapsPlusView.swift +++ b/ios/RNGoogleMapsPlusView.swift @@ -11,6 +11,7 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { private let markerOptions = MapMarkerOptions() private let polylineOptions = MapPolylineOptions() private let polygonOptions = MapPolygonOptions() + private let circleOptions = MapCircleOptions() private let impl: GoogleMapsViewImpl @@ -154,15 +155,15 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { for (id, next) in nextById { if let prev = prevById[id] { if !prev.polylineEquals(next) { - impl.updatePolyline(id: id) { gms in - prev.updatePolyline(next, gms) + impl.updatePolyline(id: id) { pl in + prev.updatePolyline(next, pl) } - } else { - impl.addPolyline( - id: id, - polyline: polylineOptions.buildPolyline(next) - ) } + } else { + impl.addPolyline( + id: id, + polyline: polylineOptions.buildPolyline(next) + ) } } } @@ -197,6 +198,35 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { } } + @MainActor + var circles: [RNCircle]? { + didSet { + let prevById = Dictionary( + (oldValue ?? []).map { ($0.id, $0) }, + uniquingKeysWith: { _, new in new } + ) + let nextById = Dictionary( + (circles ?? []).map { ($0.id, $0) }, + uniquingKeysWith: { _, new in new } + ) + + let removed = Set(prevById.keys).subtracting(nextById.keys) + removed.forEach { impl.removeCircle(id: $0) } + + for (id, next) in nextById { + if let prev = prevById[id] { + if !prev.circleEquals(next) { + impl.updateCircle(id: id) { circle in + prev.updateCircle(next, circle) + } + } + } else { + impl.addCircle(id: id, circle: circleOptions.buildCircle(next)) + } + } + } + } + func setCamera(camera: RNCamera, animated: Bool?, durationMS: Double?) { let current = impl.currentCamera @@ -258,6 +288,15 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { var onMarkerPress: ((String) -> Void)? { didSet { impl.onMarkerPress = onMarkerPress } } + var onPolylinePress: ((String) -> Void)? { + didSet { impl.onPolylinePress = onPolylinePress } + } + var onPolygonPress: ((String) -> Void)? { + didSet { impl.onPolygonPress = onPolygonPress } + } + var onCirclePress: ((String) -> Void)? { + didSet { impl.onCirclePress = onCirclePress } + } var onCameraChangeStart: ((RNRegion, RNCamera, Bool) -> Void)? { didSet { impl.onCameraChangeStart = onCameraChangeStart } } diff --git a/src/RNGoogleMapsPlusView.nitro.ts b/src/RNGoogleMapsPlusView.nitro.ts index ec84c95..56e0fee 100644 --- a/src/RNGoogleMapsPlusView.nitro.ts +++ b/src/RNGoogleMapsPlusView.nitro.ts @@ -18,6 +18,7 @@ import type { RNMapErrorCode, RNMapType, RNInitialProps, + RNCircle, } from './types'; export interface RNGoogleMapsPlusViewProps extends HybridViewProps { @@ -33,12 +34,16 @@ export interface RNGoogleMapsPlusViewProps extends HybridViewProps { markers?: RNMarker[]; polygons?: RNPolygon[]; polylines?: RNPolyline[]; + circles?: RNCircle[]; onMapError?: (error: RNMapErrorCode) => void; onMapReady?: (ready: boolean) => void; onLocationUpdate?: (location: RNLocation) => void; onLocationError?: (error: RNLocationErrorCode) => void; onMapPress?: (coordinate: RNLatLng) => void; onMarkerPress?: (id: string) => void; + onPolylinePress?: (id: string) => void; + onPolygonPress?: (id: string) => void; + onCirclePress?: (id: string) => void; onCameraChangeStart?: ( region: RNRegion, camera: RNCamera, diff --git a/src/types.ts b/src/types.ts index ffc957b..c439815 100644 --- a/src/types.ts +++ b/src/types.ts @@ -114,7 +114,7 @@ export type RNLineJoinType = 'miter' | 'round' | 'bevel'; export type RNMarker = { id: string; - zIndex: number; + zIndex?: number; coordinate: RNLatLng; anchor?: RNPosition; width: number; @@ -124,7 +124,8 @@ export type RNMarker = { export type RNPolygon = { id: string; - zIndex: number; + zIndex?: number; + pressable?: boolean; coordinates: RNLatLng[]; fillColor?: string; strokeColor?: string; @@ -133,7 +134,8 @@ export type RNPolygon = { export type RNPolyline = { id: string; - zIndex: number; + zIndex?: number; + pressable?: boolean; coordinates: RNLatLng[]; lineCap?: RNLineCapType; lineJoin?: RNLineJoinType; @@ -141,6 +143,17 @@ export type RNPolyline = { width?: number; }; +export type RNCircle = { + id: string; + pressable?: boolean; + zIndex?: number; + center: RNLatLng; + radius?: number; + strokeWidth?: number; + strokeColor?: string; + fillColor?: string; +}; + export type RNLocationPermissionResult = { android?: RNAndroidLocationPermissionResult; ios?: RNIOSPermissionResult;