diff --git a/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt b/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt index 5979c6a..5a0486d 100644 --- a/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt +++ b/android/src/main/java/com/rngooglemapsplus/GoogleMapsViewImpl.kt @@ -27,8 +27,11 @@ import com.google.android.gms.maps.model.Polyline import com.google.android.gms.maps.model.PolylineOptions 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.rngooglemapsplus.extensions.toGooglePriority import com.rngooglemapsplus.extensions.toLocationErrorCode +import java.io.ByteArrayInputStream +import java.nio.charset.StandardCharsets class GoogleMapsViewImpl( val reactContext: ThemedReactContext, @@ -55,12 +58,14 @@ class GoogleMapsViewImpl( private val pendingPolygons = mutableListOf>() private val pendingCircles = mutableListOf>() private val pendingHeatmaps = mutableListOf>() + private val pendingKmlLayers = mutableListOf>() private val markersById = mutableMapOf() private val polylinesById = mutableMapOf() private val polygonsById = mutableMapOf() private val circlesById = mutableMapOf() private val heatmapsById = mutableMapOf() + private val kmlLayersById = mutableMapOf() private var cameraMoveReason = -1 private var lastSubmittedLocation: Location? = null @@ -343,6 +348,13 @@ class GoogleMapsViewImpl( } pendingHeatmaps.clear() } + + if (pendingKmlLayers.isNotEmpty()) { + pendingKmlLayers.forEach { (id, string) -> + internalAddKmlLayer(id, string) + } + pendingKmlLayers.clear() + } } var uiSettings: RNMapUiSettings? = null @@ -825,6 +837,50 @@ class GoogleMapsViewImpl( pendingHeatmaps.clear() } + fun addKmlLayer( + id: String, + kmlString: String, + ) { + if (googleMap == null) { + pendingKmlLayers.add(id to kmlString) + return + } + onUi { + kmlLayersById.remove(id)?.removeLayerFromMap() + } + internalAddKmlLayer(id, kmlString) + } + + private fun internalAddKmlLayer( + 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 + } + } + } + + fun removeKmlLayer(id: String) { + onUi { + kmlLayersById.remove(id)?.removeLayerFromMap() + } + } + + fun clearKmlLayer() { + onUi { + kmlLayersById.values.forEach { it.removeLayerFromMap() } + } + kmlLayersById.clear() + pendingKmlLayers.clear() + } + fun destroyInternal() { onUi { markerBuilder.cancelAllJobs() @@ -833,6 +889,7 @@ class GoogleMapsViewImpl( clearPolygons() clearCircles() clearHeatmaps() + clearKmlLayer() locationHandler.stop() googleMap?.apply { setOnCameraMoveStartedListener(null) diff --git a/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt b/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt index 40ed111..ede216d 100644 --- a/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt +++ b/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt @@ -234,6 +234,20 @@ class RNGoogleMapsPlusView( } } + override var kmlLayers: 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.removeKmlLayer(id) + } + nextById.forEach { (id, next) -> + view.addKmlLayer(id, next.kmlString) + } + } + override var locationConfig: RNLocationConfig? = null set(value) { if (field == value) return diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 7755d98..60981f1 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -2358,7 +2358,7 @@ PODS: - React-perflogger (= 0.82.0) - React-utils (= 0.82.0) - SocketRocket - - RNGoogleMapsPlus (1.1.0-dev.2): + - RNGoogleMapsPlus (1.1.0-dev.5): - boost - DoubleConversion - fast_float @@ -2713,7 +2713,7 @@ SPEC CHECKSUMS: ReactAppDependencyProvider: c5c4f5280e4ae0f9f4a739c64c4260fe0b3edaf1 ReactCodegen: 3873d7ac09960375f7845384ff47d53e478462dc ReactCommon: f5527f5d97a9957ab46eb5db78875d3579e03b97 - RNGoogleMapsPlus: 9b638ea84ab0231430a8b5a109a20130ad4a722a + RNGoogleMapsPlus: cdea400ea1e69740d91e07dbb5882d93be4c0a77 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 SVGKit: 1ad7513f8c74d9652f94ed64ddecda1a23864dea Yoga: ce55ebb197c21e22b6700cd36e3f36b7ec26e6f8 diff --git a/example/src/App.tsx b/example/src/App.tsx index 4597e37..619f913 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -178,6 +178,78 @@ const silverMapStyle: RNMapStyleElement[] = [ }, ]; +const kmlString = ` + + + + Example KML Data + Example with marker, polygon and circle shifted further northeast of San Francisco + + + Center Point + + -122.4156,37.7781,0 + + + + + Example Polygon + + + + + + -122.4206,37.7826,0 + -122.4106,37.7826,0 + -122.4106,37.7746,0 + -122.4206,37.7746,0 + -122.4206,37.7826,0 + + + + + + + + Approximate Circle + + + + + + -122.4156,37.7801,0 + -122.4136,37.7801,0 + -122.4136,37.7761,0 + -122.4156,37.7761,0 + -122.4176,37.7761,0 + -122.4176,37.7801,0 + -122.4156,37.7801,0 + + + + + + + + +`.trim(); + function makeSvgIcon(width: number, height: number): string { const color = randomColor(); return ` @@ -369,6 +441,8 @@ export default function App() { Array.from({ length: 1 }, (_, i) => makeHeatmap(i + 1)) ); + const [kmlLayers] = useState([{ id: '21', zIndex: 1, kmlString }]); + useEffect(() => { if (!stressTest) return; @@ -543,6 +617,7 @@ export default function App() { polylines={polylines} circles={circles} heatmaps={heatmaps} + kmlLayers={kmlLayers} /> diff --git a/ios/GoogleMapViewImpl.swift b/ios/GoogleMapViewImpl.swift index 91387f2..a800ebe 100644 --- a/ios/GoogleMapViewImpl.swift +++ b/ios/GoogleMapViewImpl.swift @@ -16,12 +16,14 @@ final class GoogleMapsViewImpl: UIView, GMSMapViewDelegate { private var pendingPolygons: [(id: String, polygon: GMSPolygon)] = [] private var pendingCircles: [(id: String, circle: GMSCircle)] = [] private var pendingHeatmaps: [(id: String, heatmap: GMUHeatmapTileLayer)] = [] + private var pendingKmlLayers: [(id: String, kmlString: String)] = [] private var markersById: [String: GMSMarker] = [:] private var polylinesById: [String: GMSPolyline] = [:] private var polygonsById: [String: GMSPolygon] = [:] private var circlesById: [String: GMSCircle] = [:] private var heatmapsById: [String: GMUHeatmapTileLayer] = [:] + private var kmlLayerById: [String: GMUGeometryRenderer] = [:] private var cameraMoveReasonIsGesture: Bool = false private var lastSubmittedCameraPosition: GMSCameraPosition? @@ -177,6 +179,12 @@ final class GoogleMapsViewImpl: UIView, GMSMapViewDelegate { } pendingHeatmaps.removeAll() } + if !pendingKmlLayers.isEmpty { + pendingKmlLayers.forEach { + addKmlLayerInternal(id: $0.id, kmlString: $0.kmlString) + } + pendingKmlLayers.removeAll() + } } var currentCamera: GMSCameraPosition? { @@ -524,6 +532,42 @@ final class GoogleMapsViewImpl: UIView, GMSMapViewDelegate { pendingHeatmaps.removeAll() } + @MainActor + func addKmlLayer(id: String, kmlString: String) { + if mapView == nil { + pendingKmlLayers.append((id, kmlString)) + return + } + kmlLayerById.removeValue(forKey: id).map { $0.clear() } + addKmlLayerInternal(id: id, kmlString: kmlString) + } + + @MainActor + private func addKmlLayerInternal(id: String, kmlString: String) { + guard let data = kmlString.data(using: .utf8) else { return } + let parser = GMUKMLParser(data: data) + parser.parse() + mapView.map { mapView in + let renderer = GMUGeometryRenderer( + map: mapView, + geometries: parser.placemarks + ) + renderer.render() + } + } + + @MainActor + func removeKmlLayer(id: String) { + kmlLayerById.removeValue(forKey: id).map { $0.clear() } + } + + @MainActor + func clearKmlLayers() { + kmlLayerById.values.forEach { $0.clear() } + kmlLayerById.removeAll() + pendingKmlLayers.removeAll() + } + func deinitInternal() { markerBuilder.cancelAllIconTasks() clearMarkers() diff --git a/ios/RNGoogleMapsPlusView.swift b/ios/RNGoogleMapsPlusView.swift index 47edfd3..49c498f 100644 --- a/ios/RNGoogleMapsPlusView.swift +++ b/ios/RNGoogleMapsPlusView.swift @@ -256,6 +256,27 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { } } + @MainActor + var kmlLayers: [RNKMLayer]? { + didSet { + let prevById = Dictionary( + (oldValue ?? []).map { ($0.id, $0) }, + uniquingKeysWith: { _, new in new } + ) + let nextById = Dictionary( + (kmlLayers ?? []).map { ($0.id, $0) }, + uniquingKeysWith: { _, new in new } + ) + + let removed = Set(prevById.keys).subtracting(nextById.keys) + removed.forEach { impl.removeKmlLayer(id: $0) } + + for (id, next) in nextById { + impl.addKmlLayer(id: id, kmlString: next.kmlString) + } + } + } + @MainActor var locationConfig: RNLocationConfig? { didSet { impl.locationConfig = locationConfig diff --git a/src/RNGoogleMapsPlusView.nitro.ts b/src/RNGoogleMapsPlusView.nitro.ts index dc29a66..07f104f 100644 --- a/src/RNGoogleMapsPlusView.nitro.ts +++ b/src/RNGoogleMapsPlusView.nitro.ts @@ -23,6 +23,7 @@ import type { RNLocationConfig, RNMapZoomConfig, RNHeatmap, + RNKMLayer, } from './types'; export interface RNGoogleMapsPlusViewProps extends HybridViewProps { @@ -42,6 +43,7 @@ export interface RNGoogleMapsPlusViewProps extends HybridViewProps { polylines?: RNPolyline[]; circles?: RNCircle[]; heatmaps?: RNHeatmap[]; + kmlLayers?: RNKMLayer[]; locationConfig?: RNLocationConfig; onMapError?: (error: RNMapErrorCode) => void; onMapReady?: (ready: boolean) => void; diff --git a/src/types.ts b/src/types.ts index 5a6d9e5..fedb7b8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -199,6 +199,11 @@ export type RNHeatmapGradient = { colorMapSize: number; }; +export type RNKMLayer = { + id: string; + kmlString: string; +}; + export type RNLocationConfig = { android?: RNAndroidLocationConfig; ios?: RNIOSLocationConfig;