@maydon_tech/react-native-nitro-maps 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/NitroMap.podspec +42 -0
- package/README.md +172 -0
- package/android/CMakeLists.txt +27 -0
- package/android/build.gradle +141 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/cpp/cpp-adapter.cpp +6 -0
- package/android/src/main/java/com/margelo/nitro/nitromap/ClusterIconGenerator.kt +108 -0
- package/android/src/main/java/com/margelo/nitro/nitromap/ColorUtils.kt +63 -0
- package/android/src/main/java/com/margelo/nitro/nitromap/HybridNitroMap.kt +408 -0
- package/android/src/main/java/com/margelo/nitro/nitromap/HybridNitroMapConfig.kt +68 -0
- package/android/src/main/java/com/margelo/nitro/nitromap/MarkerIconCache.kt +176 -0
- package/android/src/main/java/com/margelo/nitro/nitromap/MarkerIconFactory.kt +252 -0
- package/android/src/main/java/com/margelo/nitro/nitromap/NitroMapPackage.kt +33 -0
- package/android/src/main/java/com/margelo/nitro/nitromap/clustering/NitroClusterEngine.kt +252 -0
- package/android/src/main/java/com/margelo/nitro/nitromap/clustering/QuadTree.kt +195 -0
- package/android/src/main/java/com/margelo/nitro/nitromap/providers/GoogleMapProvider.kt +912 -0
- package/android/src/main/java/com/margelo/nitro/nitromap/providers/MapProviderInterface.kt +70 -0
- package/cpp/ClusterEngine.hpp +411 -0
- package/cpp/KDBush.hpp +238 -0
- package/cpp/QuadTree.hpp +246 -0
- package/ios/Clustering/ClusterEngineWrapper.h +58 -0
- package/ios/Clustering/ClusterEngineWrapper.mm +142 -0
- package/ios/Clustering/ClusterIconRenderer.swift +80 -0
- package/ios/Clustering/NitroClusterEngine.swift +117 -0
- package/ios/Clustering/NitroClusterIconGenerator.swift +35 -0
- package/ios/MarkerRenderer/MarkerIconFactory.swift +322 -0
- package/ios/MarkerRenderer/PriceMarkerRenderer.swift +140 -0
- package/ios/NitroMap.swift +332 -0
- package/ios/NitroMapConfig/HybridNitroMapConfig.swift +33 -0
- package/ios/Providers/GoogleMapDelegate.swift +310 -0
- package/ios/Providers/GoogleMapProvider+Camera.swift +164 -0
- package/ios/Providers/GoogleMapProvider.swift +924 -0
- package/ios/Providers/MapProviderProtocol.swift +78 -0
- package/ios/Shared/ClusterConfig+Factory.swift +58 -0
- package/ios/Shared/ClusteringManager.swift +211 -0
- package/ios/Shared/MapStyleProvider.swift +135 -0
- package/ios/Shared/MarkerSelectionHandler.swift +116 -0
- package/ios/Utils/ColorValueExtension.swift +54 -0
- package/lib/module/components/ImageMarker.js +146 -0
- package/lib/module/components/ImageMarker.js.map +1 -0
- package/lib/module/components/NitroMap.js +320 -0
- package/lib/module/components/NitroMap.js.map +1 -0
- package/lib/module/components/PriceMarker.js +165 -0
- package/lib/module/components/PriceMarker.js.map +1 -0
- package/lib/module/context/NitroMapContext.js +15 -0
- package/lib/module/context/NitroMapContext.js.map +1 -0
- package/lib/module/hooks/useNitroMarker.js +104 -0
- package/lib/module/hooks/useNitroMarker.js.map +1 -0
- package/lib/module/index.js +21 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/modules/index.js +4 -0
- package/lib/module/modules/index.js.map +1 -0
- package/lib/module/modules/module.js +30 -0
- package/lib/module/modules/module.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/specs/NitroMap.nitro.js +4 -0
- package/lib/module/specs/NitroMap.nitro.js.map +1 -0
- package/lib/module/specs/NitroMapConfig.nitro.js +4 -0
- package/lib/module/specs/NitroMapConfig.nitro.js.map +1 -0
- package/lib/module/types/map.js +2 -0
- package/lib/module/types/map.js.map +1 -0
- package/lib/module/types/marker.js +4 -0
- package/lib/module/types/marker.js.map +1 -0
- package/lib/module/utils/colors.js +147 -0
- package/lib/module/utils/colors.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/components/ImageMarker.d.ts +70 -0
- package/lib/typescript/src/components/ImageMarker.d.ts.map +1 -0
- package/lib/typescript/src/components/NitroMap.d.ts +14 -0
- package/lib/typescript/src/components/NitroMap.d.ts.map +1 -0
- package/lib/typescript/src/components/PriceMarker.d.ts +88 -0
- package/lib/typescript/src/components/PriceMarker.d.ts.map +1 -0
- package/lib/typescript/src/context/NitroMapContext.d.ts +16 -0
- package/lib/typescript/src/context/NitroMapContext.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useNitroMarker.d.ts +78 -0
- package/lib/typescript/src/hooks/useNitroMarker.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +12 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/modules/index.d.ts +2 -0
- package/lib/typescript/src/modules/index.d.ts.map +1 -0
- package/lib/typescript/src/modules/module.d.ts +22 -0
- package/lib/typescript/src/modules/module.d.ts.map +1 -0
- package/lib/typescript/src/specs/NitroMap.nitro.d.ts +227 -0
- package/lib/typescript/src/specs/NitroMap.nitro.d.ts.map +1 -0
- package/lib/typescript/src/specs/NitroMapConfig.nitro.d.ts +10 -0
- package/lib/typescript/src/specs/NitroMapConfig.nitro.d.ts.map +1 -0
- package/lib/typescript/src/types/map.d.ts +154 -0
- package/lib/typescript/src/types/map.d.ts.map +1 -0
- package/lib/typescript/src/types/marker.d.ts +248 -0
- package/lib/typescript/src/types/marker.d.ts.map +1 -0
- package/lib/typescript/src/utils/colors.d.ts +82 -0
- package/lib/typescript/src/utils/colors.d.ts.map +1 -0
- package/nitro.json +21 -0
- package/nitrogen/generated/android/c++/JCamera.hpp +74 -0
- package/nitrogen/generated/android/c++/JClusterAnimationStyle.hpp +68 -0
- package/nitrogen/generated/android/c++/JClusterConfig.hpp +121 -0
- package/nitrogen/generated/android/c++/JClusterPressEvent.hpp +86 -0
- package/nitrogen/generated/android/c++/JClusterStrategy.hpp +59 -0
- package/nitrogen/generated/android/c++/JColorValue.cpp +26 -0
- package/nitrogen/generated/android/c++/JColorValue.hpp +70 -0
- package/nitrogen/generated/android/c++/JCoordinate.hpp +61 -0
- package/nitrogen/generated/android/c++/JEdgePadding.hpp +69 -0
- package/nitrogen/generated/android/c++/JFunc_void.hpp +75 -0
- package/nitrogen/generated/android/c++/JFunc_void_ClusterPressEvent.hpp +81 -0
- package/nitrogen/generated/android/c++/JFunc_void_MapError.hpp +78 -0
- package/nitrogen/generated/android/c++/JFunc_void_MapPressEvent.hpp +81 -0
- package/nitrogen/generated/android/c++/JFunc_void_MarkerDragEvent.hpp +80 -0
- package/nitrogen/generated/android/c++/JFunc_void_MarkerPressEvent.hpp +80 -0
- package/nitrogen/generated/android/c++/JFunc_void_RegionChangeEvent.hpp +79 -0
- package/nitrogen/generated/android/c++/JHybridNitroMapConfigSpec.cpp +59 -0
- package/nitrogen/generated/android/c++/JHybridNitroMapConfigSpec.hpp +66 -0
- package/nitrogen/generated/android/c++/JHybridNitroMapSpec.cpp +593 -0
- package/nitrogen/generated/android/c++/JHybridNitroMapSpec.hpp +125 -0
- package/nitrogen/generated/android/c++/JImageMarkerConfig.hpp +86 -0
- package/nitrogen/generated/android/c++/JMapBoundaries.hpp +62 -0
- package/nitrogen/generated/android/c++/JMapError.hpp +61 -0
- package/nitrogen/generated/android/c++/JMapPressEvent.hpp +64 -0
- package/nitrogen/generated/android/c++/JMapProvider.hpp +62 -0
- package/nitrogen/generated/android/c++/JMapStyleElement.hpp +87 -0
- package/nitrogen/generated/android/c++/JMapStyler.hpp +78 -0
- package/nitrogen/generated/android/c++/JMapType.hpp +62 -0
- package/nitrogen/generated/android/c++/JMarkerAnimation.hpp +62 -0
- package/nitrogen/generated/android/c++/JMarkerColor.hpp +69 -0
- package/nitrogen/generated/android/c++/JMarkerConfig.hpp +77 -0
- package/nitrogen/generated/android/c++/JMarkerData.hpp +121 -0
- package/nitrogen/generated/android/c++/JMarkerDragEvent.hpp +63 -0
- package/nitrogen/generated/android/c++/JMarkerPressEvent.hpp +63 -0
- package/nitrogen/generated/android/c++/JMarkerStyle.hpp +62 -0
- package/nitrogen/generated/android/c++/JPoint.hpp +61 -0
- package/nitrogen/generated/android/c++/JPriceMarkerStyle.hpp +102 -0
- package/nitrogen/generated/android/c++/JRegion.hpp +69 -0
- package/nitrogen/generated/android/c++/JRegionChangeEvent.hpp +62 -0
- package/nitrogen/generated/android/c++/JVariant_String_MarkerColor.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_String_MarkerColor.hpp +70 -0
- package/nitrogen/generated/android/c++/views/JHybridNitroMapStateUpdater.cpp +144 -0
- package/nitrogen/generated/android/c++/views/JHybridNitroMapStateUpdater.hpp +49 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/Camera.kt +50 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/ClusterAnimationStyle.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/ClusterConfig.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/ClusterPressEvent.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/ClusterStrategy.kt +21 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/ColorValue.kt +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/Coordinate.kt +41 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/EdgePadding.kt +47 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/Func_void.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/Func_void_ClusterPressEvent.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/Func_void_MapError.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/Func_void_MapPressEvent.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/Func_void_MarkerDragEvent.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/Func_void_MarkerPressEvent.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/Func_void_RegionChangeEvent.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/HybridNitroMapConfigSpec.kt +61 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/HybridNitroMapSpec.kt +342 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/ImageMarkerConfig.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/MapBoundaries.kt +41 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/MapError.kt +41 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/MapPressEvent.kt +41 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/MapProvider.kt +22 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/MapStyleElement.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/MapStyler.kt +53 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/MapType.kt +22 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/MarkerAnimation.kt +22 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/MarkerColor.kt +47 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/MarkerConfig.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/MarkerData.kt +71 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/MarkerDragEvent.kt +41 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/MarkerPressEvent.kt +41 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/MarkerStyle.kt +22 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/Point.kt +41 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/PriceMarkerStyle.kt +68 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/Region.kt +47 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/RegionChangeEvent.kt +41 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/Variant_String_MarkerColor.kt +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/nitromapOnLoad.kt +35 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/views/HybridNitroMapManager.kt +50 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/views/HybridNitroMapStateUpdater.kt +23 -0
- package/nitrogen/generated/android/nitromap+autolinking.cmake +87 -0
- package/nitrogen/generated/android/nitromap+autolinking.gradle +27 -0
- package/nitrogen/generated/android/nitromapOnLoad.cpp +70 -0
- package/nitrogen/generated/android/nitromapOnLoad.hpp +25 -0
- package/nitrogen/generated/ios/NitroMap+autolinking.rb +60 -0
- package/nitrogen/generated/ios/NitroMap-Swift-Cxx-Bridge.cpp +130 -0
- package/nitrogen/generated/ios/NitroMap-Swift-Cxx-Bridge.hpp +793 -0
- package/nitrogen/generated/ios/NitroMap-Swift-Cxx-Umbrella.hpp +132 -0
- package/nitrogen/generated/ios/NitroMapAutolinking.mm +41 -0
- package/nitrogen/generated/ios/NitroMapAutolinking.swift +40 -0
- package/nitrogen/generated/ios/c++/HybridNitroMapConfigSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridNitroMapConfigSpecSwift.hpp +84 -0
- package/nitrogen/generated/ios/c++/HybridNitroMapSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridNitroMapSpecSwift.hpp +410 -0
- package/nitrogen/generated/ios/c++/views/HybridNitroMapComponent.mm +206 -0
- package/nitrogen/generated/ios/swift/Camera.swift +80 -0
- package/nitrogen/generated/ios/swift/ClusterAnimationStyle.swift +52 -0
- package/nitrogen/generated/ios/swift/ClusterConfig.swift +268 -0
- package/nitrogen/generated/ios/swift/ClusterPressEvent.swift +70 -0
- package/nitrogen/generated/ios/swift/ClusterStrategy.swift +40 -0
- package/nitrogen/generated/ios/swift/ColorValue.swift +18 -0
- package/nitrogen/generated/ios/swift/Coordinate.swift +47 -0
- package/nitrogen/generated/ios/swift/EdgePadding.swift +69 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_Camera.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_ClusterPressEvent.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_MapBoundaries.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_MapError.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_MapPressEvent.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_MarkerDragEvent.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_MarkerPressEvent.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_RegionChangeEvent.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +47 -0
- package/nitrogen/generated/ios/swift/HybridNitroMapConfigSpec.swift +57 -0
- package/nitrogen/generated/ios/swift/HybridNitroMapConfigSpec_cxx.swift +142 -0
- package/nitrogen/generated/ios/swift/HybridNitroMapSpec.swift +93 -0
- package/nitrogen/generated/ios/swift/HybridNitroMapSpec_cxx.swift +953 -0
- package/nitrogen/generated/ios/swift/ImageMarkerConfig.swift +166 -0
- package/nitrogen/generated/ios/swift/MapBoundaries.swift +47 -0
- package/nitrogen/generated/ios/swift/MapError.swift +47 -0
- package/nitrogen/generated/ios/swift/MapPressEvent.swift +47 -0
- package/nitrogen/generated/ios/swift/MapProvider.swift +44 -0
- package/nitrogen/generated/ios/swift/MapStyleElement.swift +108 -0
- package/nitrogen/generated/ios/swift/MapStyler.swift +177 -0
- package/nitrogen/generated/ios/swift/MapType.swift +44 -0
- package/nitrogen/generated/ios/swift/MarkerAnimation.swift +44 -0
- package/nitrogen/generated/ios/swift/MarkerColor.swift +69 -0
- package/nitrogen/generated/ios/swift/MarkerConfig.swift +82 -0
- package/nitrogen/generated/ios/swift/MarkerData.swift +195 -0
- package/nitrogen/generated/ios/swift/MarkerDragEvent.swift +47 -0
- package/nitrogen/generated/ios/swift/MarkerPressEvent.swift +47 -0
- package/nitrogen/generated/ios/swift/MarkerStyle.swift +44 -0
- package/nitrogen/generated/ios/swift/Point.swift +47 -0
- package/nitrogen/generated/ios/swift/PriceMarkerStyle.swift +374 -0
- package/nitrogen/generated/ios/swift/Region.swift +69 -0
- package/nitrogen/generated/ios/swift/RegionChangeEvent.swift +47 -0
- package/nitrogen/generated/ios/swift/Variant_String_MarkerColor.swift +18 -0
- package/nitrogen/generated/shared/c++/Camera.hpp +92 -0
- package/nitrogen/generated/shared/c++/ClusterAnimationStyle.hpp +88 -0
- package/nitrogen/generated/shared/c++/ClusterConfig.hpp +140 -0
- package/nitrogen/generated/shared/c++/ClusterPressEvent.hpp +86 -0
- package/nitrogen/generated/shared/c++/ClusterStrategy.hpp +76 -0
- package/nitrogen/generated/shared/c++/Coordinate.hpp +79 -0
- package/nitrogen/generated/shared/c++/EdgePadding.hpp +87 -0
- package/nitrogen/generated/shared/c++/HybridNitroMapConfigSpec.cpp +22 -0
- package/nitrogen/generated/shared/c++/HybridNitroMapConfigSpec.hpp +65 -0
- package/nitrogen/generated/shared/c++/HybridNitroMapSpec.cpp +82 -0
- package/nitrogen/generated/shared/c++/HybridNitroMapSpec.hpp +173 -0
- package/nitrogen/generated/shared/c++/ImageMarkerConfig.hpp +103 -0
- package/nitrogen/generated/shared/c++/MapBoundaries.hpp +80 -0
- package/nitrogen/generated/shared/c++/MapError.hpp +79 -0
- package/nitrogen/generated/shared/c++/MapPressEvent.hpp +83 -0
- package/nitrogen/generated/shared/c++/MapProvider.hpp +80 -0
- package/nitrogen/generated/shared/c++/MapStyleElement.hpp +87 -0
- package/nitrogen/generated/shared/c++/MapStyler.hpp +96 -0
- package/nitrogen/generated/shared/c++/MapType.hpp +80 -0
- package/nitrogen/generated/shared/c++/MarkerAnimation.hpp +80 -0
- package/nitrogen/generated/shared/c++/MarkerColor.hpp +87 -0
- package/nitrogen/generated/shared/c++/MarkerConfig.hpp +91 -0
- package/nitrogen/generated/shared/c++/MarkerData.hpp +131 -0
- package/nitrogen/generated/shared/c++/MarkerDragEvent.hpp +81 -0
- package/nitrogen/generated/shared/c++/MarkerPressEvent.hpp +81 -0
- package/nitrogen/generated/shared/c++/MarkerStyle.hpp +80 -0
- package/nitrogen/generated/shared/c++/Point.hpp +79 -0
- package/nitrogen/generated/shared/c++/PriceMarkerStyle.hpp +119 -0
- package/nitrogen/generated/shared/c++/Region.hpp +87 -0
- package/nitrogen/generated/shared/c++/RegionChangeEvent.hpp +80 -0
- package/nitrogen/generated/shared/c++/views/HybridNitroMapComponent.cpp +351 -0
- package/nitrogen/generated/shared/c++/views/HybridNitroMapComponent.hpp +141 -0
- package/nitrogen/generated/shared/json/NitroMapConfig.json +32 -0
- package/package.json +176 -0
- package/react-native.config.js +16 -0
- package/src/components/ImageMarker.tsx +254 -0
- package/src/components/NitroMap.tsx +433 -0
- package/src/components/PriceMarker.tsx +311 -0
- package/src/context/NitroMapContext.tsx +33 -0
- package/src/hooks/useNitroMarker.ts +198 -0
- package/src/index.tsx +62 -0
- package/src/modules/index.ts +6 -0
- package/src/modules/module.ts +45 -0
- package/src/specs/NitroMap.nitro.ts +292 -0
- package/src/specs/NitroMapConfig.nitro.ts +8 -0
- package/src/types/map.ts +166 -0
- package/src/types/marker.ts +267 -0
- package/src/utils/colors.ts +159 -0
|
@@ -0,0 +1,924 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import GoogleMaps
|
|
3
|
+
import GoogleMapsUtils
|
|
4
|
+
import UIKit
|
|
5
|
+
|
|
6
|
+
/// Google Maps implementation of MapProviderProtocol
|
|
7
|
+
class GoogleMapProvider: MapProviderProtocol {
|
|
8
|
+
|
|
9
|
+
// MARK: - Constants
|
|
10
|
+
|
|
11
|
+
private static let defaultLatitude: Double = 41.2995
|
|
12
|
+
private static let defaultLongitude: Double = 69.2401
|
|
13
|
+
|
|
14
|
+
// MARK: - Properties
|
|
15
|
+
|
|
16
|
+
let gmsMapView: GMSMapView
|
|
17
|
+
var mapView: UIView { return gmsMapView }
|
|
18
|
+
|
|
19
|
+
private var mapDelegate: GoogleMapDelegate?
|
|
20
|
+
|
|
21
|
+
// MARK: - Injected Dependencies (DIP)
|
|
22
|
+
|
|
23
|
+
private let clusteringManager: ClusteringManagerProtocol
|
|
24
|
+
private let selectionHandler: MarkerSelectionHandling
|
|
25
|
+
|
|
26
|
+
// Rendered markers
|
|
27
|
+
private(set) var renderedClusterMarkers: [GMSMarker] = []
|
|
28
|
+
private var renderedSingleMarkers: [String: GMSMarker] = [:]
|
|
29
|
+
private var nonClusteredMarkers: [String: GMSMarker] = [:]
|
|
30
|
+
private var hiddenNonClusteredIds: Set<String> = [] // Non-clustered markers hidden due to overlap
|
|
31
|
+
private var clusterableMarkerData: [String: MarkerData] = [:]
|
|
32
|
+
|
|
33
|
+
private var selectedMarkerId: String?
|
|
34
|
+
|
|
35
|
+
private static let defaultMarkerSize: CGFloat = 44
|
|
36
|
+
|
|
37
|
+
// MARK: - Clustering Render Item
|
|
38
|
+
|
|
39
|
+
/// A renderable item produced by the clustering pipeline.
|
|
40
|
+
/// Used by both `supercluster` and `hideOnOverlap` strategies.
|
|
41
|
+
private struct RenderItem {
|
|
42
|
+
enum Kind {
|
|
43
|
+
case cluster(NitroClusterEngine.ClusterDataResult)
|
|
44
|
+
case single(markerId: String)
|
|
45
|
+
}
|
|
46
|
+
let coordinate: CLLocationCoordinate2D
|
|
47
|
+
let kind: Kind
|
|
48
|
+
let iconSize: CGSize
|
|
49
|
+
let priority: Int // Higher = more important (clusters > singles)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// MARK: - Protocol Properties
|
|
53
|
+
|
|
54
|
+
var initialRegion: Region? = Region(latitude: defaultLatitude, longitude: defaultLongitude, latitudeDelta: 0.15, longitudeDelta: 0.15) {
|
|
55
|
+
didSet {
|
|
56
|
+
// Only move to initial region on first set, not on every re-render
|
|
57
|
+
guard oldValue == nil else { return }
|
|
58
|
+
updateCameraToInitialRegion()
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
var showsUserLocation: Bool? {
|
|
63
|
+
didSet { gmsMapView.isMyLocationEnabled = showsUserLocation ?? false }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
var zoomEnabled: Bool? {
|
|
67
|
+
didSet { gmsMapView.settings.zoomGestures = zoomEnabled ?? true }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
var scrollEnabled: Bool? {
|
|
71
|
+
didSet { gmsMapView.settings.scrollGestures = scrollEnabled ?? true }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
var rotateEnabled: Bool? {
|
|
75
|
+
didSet { gmsMapView.settings.rotateGestures = rotateEnabled ?? true }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
var pitchEnabled: Bool? {
|
|
79
|
+
didSet { gmsMapView.settings.tiltGestures = pitchEnabled ?? true }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
var mapType: MapType? {
|
|
83
|
+
didSet { gmsMapView.mapType = convertMapType(mapType) }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
var showsMyLocationButton: Bool? {
|
|
87
|
+
didSet {
|
|
88
|
+
gmsMapView.settings.myLocationButton = showsMyLocationButton ?? false
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
var clusterConfig: ClusterConfig? {
|
|
93
|
+
didSet {
|
|
94
|
+
#if DEBUG
|
|
95
|
+
NSLog("[Clustering] didSet clusterConfig strategy=%@",
|
|
96
|
+
clusterConfig?.strategy.stringValue ?? "nil")
|
|
97
|
+
#endif
|
|
98
|
+
updateClusterConfig()
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
var customMapStyle: [MapStyleElement]? {
|
|
103
|
+
didSet { applyMapStyle() }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
var darkMode: Bool? {
|
|
107
|
+
didSet { applyDarkMode() }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// MARK: - Callbacks
|
|
111
|
+
|
|
112
|
+
var onPress: ((MapPressEvent) -> Void)?
|
|
113
|
+
var onLongPress: ((MapPressEvent) -> Void)?
|
|
114
|
+
var onMapReady: (() -> Void)?
|
|
115
|
+
var onRegionChange: ((RegionChangeEvent) -> Void)?
|
|
116
|
+
var onRegionChangeComplete: ((RegionChangeEvent) -> Void)?
|
|
117
|
+
var onMarkerPress: ((MarkerPressEvent) -> Void)?
|
|
118
|
+
var onMarkerDragStart: ((MarkerDragEvent) -> Void)?
|
|
119
|
+
var onMarkerDrag: ((MarkerDragEvent) -> Void)?
|
|
120
|
+
var onMarkerDragEnd: ((MarkerDragEvent) -> Void)?
|
|
121
|
+
var onClusterPress: ((ClusterPressEvent) -> Void)?
|
|
122
|
+
var onError: ((MapError) -> Void)?
|
|
123
|
+
|
|
124
|
+
// MARK: - Initialization
|
|
125
|
+
|
|
126
|
+
/// Creates a GoogleMapProvider with injected dependencies.
|
|
127
|
+
/// Uses default production implementations if none provided.
|
|
128
|
+
///
|
|
129
|
+
/// - Parameters:
|
|
130
|
+
/// - clusteringManager: Clustering logic handler (default: ClusteringManager)
|
|
131
|
+
/// - selectionHandler: Marker selection handler (default: MarkerSelectionHandler.shared)
|
|
132
|
+
init(
|
|
133
|
+
clusteringManager: ClusteringManagerProtocol = ClusteringManager(),
|
|
134
|
+
selectionHandler: MarkerSelectionHandling = MarkerSelectionHandler.shared
|
|
135
|
+
) {
|
|
136
|
+
self.clusteringManager = clusteringManager
|
|
137
|
+
self.selectionHandler = selectionHandler
|
|
138
|
+
|
|
139
|
+
let camera = GMSCameraPosition.camera(
|
|
140
|
+
withLatitude: Self.defaultLatitude,
|
|
141
|
+
longitude: Self.defaultLongitude,
|
|
142
|
+
zoom: 10
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
// Use GMSMapViewOptions (required for Google Maps SDK v10.0+)
|
|
146
|
+
let options = GMSMapViewOptions()
|
|
147
|
+
options.camera = camera
|
|
148
|
+
options.frame = .zero
|
|
149
|
+
|
|
150
|
+
let mapView = GMSMapView(options: options)
|
|
151
|
+
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
152
|
+
self.gmsMapView = mapView
|
|
153
|
+
|
|
154
|
+
setupIconLoadNotification()
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
deinit {
|
|
158
|
+
NotificationCenter.default.removeObserver(self)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// MARK: - Lifecycle
|
|
162
|
+
|
|
163
|
+
func setup() {
|
|
164
|
+
mapDelegate = GoogleMapDelegate(provider: self)
|
|
165
|
+
gmsMapView.delegate = mapDelegate
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/// Listen for async icon load completion to update markers
|
|
169
|
+
private func setupIconLoadNotification() {
|
|
170
|
+
NotificationCenter.default.addObserver(
|
|
171
|
+
self,
|
|
172
|
+
selector: #selector(handleIconLoaded(_:)),
|
|
173
|
+
name: Notification.Name("MarkerIconLoaded"),
|
|
174
|
+
object: nil
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
@objc private func handleIconLoaded(_ notification: Notification) {
|
|
179
|
+
guard notification.userInfo?["icon"] is UIImage else {
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// The cache has already been updated, so just refresh visible markers
|
|
184
|
+
DispatchQueue.main.async { [weak self] in
|
|
185
|
+
self?.refreshVisibleMarkerIcons()
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/// Refresh icons for visible markers (used after async icon load)
|
|
190
|
+
private func refreshVisibleMarkerIcons() {
|
|
191
|
+
// Only refresh image-style markers as they use async loading
|
|
192
|
+
for (id, gmsMarker) in renderedSingleMarkers {
|
|
193
|
+
if let markerData = clusterableMarkerData[id],
|
|
194
|
+
markerData.config.style == MarkerStyle.image {
|
|
195
|
+
// Re-fetch icon from cache (now should have the loaded image)
|
|
196
|
+
if let icon = MarkerIconFactory.createIcon(for: markerData) {
|
|
197
|
+
gmsMarker.icon = icon
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
for (id, gmsMarker) in nonClusteredMarkers {
|
|
203
|
+
if let markerData = clusterableMarkerData[id],
|
|
204
|
+
markerData.config.style == MarkerStyle.image {
|
|
205
|
+
if let icon = MarkerIconFactory.createIcon(for: markerData) {
|
|
206
|
+
gmsMarker.icon = icon
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
func updateSettings() {
|
|
213
|
+
// Props are already applied via didSet. Just trigger clustering
|
|
214
|
+
// to reconcile any pending state.
|
|
215
|
+
performClustering()
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// MARK: - Cluster Configuration
|
|
219
|
+
|
|
220
|
+
private func updateClusterConfig() {
|
|
221
|
+
#if DEBUG
|
|
222
|
+
NSLog("[Clustering] updateClusterConfig called")
|
|
223
|
+
#endif
|
|
224
|
+
clusteringManager.clusterConfig = clusterConfig
|
|
225
|
+
performClustering()
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// MARK: - Clustering
|
|
229
|
+
|
|
230
|
+
/// Debounced clustering — fires after 100ms of silence (used on idle / config changes)
|
|
231
|
+
func performClustering() {
|
|
232
|
+
// FIX #4: Use the single debounce in ClusteringManager
|
|
233
|
+
// instead of a second timer here.
|
|
234
|
+
clusteringManager.debounce { [weak self] in
|
|
235
|
+
self?.performClusteringImmediate()
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/// Throttled clustering — fires at most once per interval during continuous gestures.
|
|
240
|
+
/// Unlike debounce, throttle fires immediately then blocks subsequent calls for the interval.
|
|
241
|
+
private var throttleTimer: Timer?
|
|
242
|
+
|
|
243
|
+
func performClusteringThrottled() {
|
|
244
|
+
// If throttle timer is active, skip — we already have a pending call
|
|
245
|
+
guard throttleTimer == nil else { return }
|
|
246
|
+
|
|
247
|
+
// Fire immediately
|
|
248
|
+
performClusteringImmediate()
|
|
249
|
+
|
|
250
|
+
// Read interval from config (ms → seconds), default 150ms
|
|
251
|
+
let intervalMs = clusterConfig?.throttleInterval ?? 150
|
|
252
|
+
let intervalSec = intervalMs / 1000.0
|
|
253
|
+
|
|
254
|
+
// Block subsequent calls for the interval
|
|
255
|
+
throttleTimer = Timer.scheduledTimer(withTimeInterval: intervalSec, repeats: false) { [weak self] _ in
|
|
256
|
+
self?.throttleTimer = nil
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private func performClusteringImmediate() {
|
|
261
|
+
guard gmsMapView.frame.size.width > 0 && gmsMapView.frame.size.height > 0
|
|
262
|
+
else {
|
|
263
|
+
return
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
let enabled = clusterConfig?.enabled ?? true
|
|
267
|
+
guard enabled else {
|
|
268
|
+
clearRenderedMarkersToPool()
|
|
269
|
+
renderAllMarkersIndividually()
|
|
270
|
+
return
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// --- Phase 1: Spatial grouping (C++ Supercluster engine) ---
|
|
274
|
+
let visibleRegion = gmsMapView.projection.visibleRegion()
|
|
275
|
+
let zoom = gmsMapView.camera.zoom
|
|
276
|
+
let mapSize = gmsMapView.frame.size
|
|
277
|
+
|
|
278
|
+
let bounds = GMSCoordinateBounds(region: visibleRegion)
|
|
279
|
+
|
|
280
|
+
// Expand bounds by renderBuffer (fraction of viewport size)
|
|
281
|
+
let padding = clusterConfig?.renderBuffer ?? 0
|
|
282
|
+
let latSpan = bounds.northEast.latitude - bounds.southWest.latitude
|
|
283
|
+
let lonSpan = bounds.northEast.longitude - bounds.southWest.longitude
|
|
284
|
+
let queryMinLat = bounds.southWest.latitude - latSpan * padding
|
|
285
|
+
let queryMaxLat = bounds.northEast.latitude + latSpan * padding
|
|
286
|
+
let queryMinLon = bounds.southWest.longitude - lonSpan * padding
|
|
287
|
+
let queryMaxLon = bounds.northEast.longitude + lonSpan * padding
|
|
288
|
+
|
|
289
|
+
#if DEBUG
|
|
290
|
+
NSLog("[Clustering] zoom=%.6f SW=(%.6f,%.6f) NE=(%.6f,%.6f) padding=%.2f",
|
|
291
|
+
Double(zoom),
|
|
292
|
+
bounds.southWest.latitude, bounds.southWest.longitude,
|
|
293
|
+
bounds.northEast.latitude, bounds.northEast.longitude,
|
|
294
|
+
padding)
|
|
295
|
+
#endif
|
|
296
|
+
|
|
297
|
+
let result = clusteringManager.cluster(
|
|
298
|
+
minLat: queryMinLat,
|
|
299
|
+
maxLat: queryMaxLat,
|
|
300
|
+
minLon: queryMinLon,
|
|
301
|
+
maxLon: queryMaxLon,
|
|
302
|
+
zoom: Double(zoom),
|
|
303
|
+
mapSize: mapSize
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
let minClusterSize = Int(clusterConfig?.minimumClusterSize ?? 2)
|
|
307
|
+
let strategy = clusterConfig?.strategy ?? .supercluster
|
|
308
|
+
|
|
309
|
+
// --- Phase 2: Build renderable items from engine output ---
|
|
310
|
+
var renderItems: [RenderItem] = []
|
|
311
|
+
renderItems.reserveCapacity(result.clusters.count + result.singleMarkers.count)
|
|
312
|
+
|
|
313
|
+
// Process clusters
|
|
314
|
+
for cluster in result.clusters {
|
|
315
|
+
let containsSelected = selectedMarkerId != nil && cluster.markerIds.contains(selectedMarkerId!)
|
|
316
|
+
let adjustedCount = containsSelected ? cluster.count - 1 : cluster.count
|
|
317
|
+
guard adjustedCount >= minClusterSize else { continue }
|
|
318
|
+
|
|
319
|
+
let adjustedCluster = containsSelected
|
|
320
|
+
? NitroClusterEngine.ClusterDataResult(
|
|
321
|
+
coordinate: cluster.coordinate,
|
|
322
|
+
markerIds: cluster.markerIds.filter { $0 != selectedMarkerId },
|
|
323
|
+
count: adjustedCount,
|
|
324
|
+
iconSize: cluster.iconSize
|
|
325
|
+
)
|
|
326
|
+
: cluster
|
|
327
|
+
|
|
328
|
+
let icon = clusteringManager.clusterIcon(forCount: adjustedCount)
|
|
329
|
+
let size = icon?.size ?? CGSize(width: 44, height: 44)
|
|
330
|
+
|
|
331
|
+
renderItems.append(RenderItem(
|
|
332
|
+
coordinate: cluster.coordinate,
|
|
333
|
+
kind: .cluster(adjustedCluster),
|
|
334
|
+
iconSize: size,
|
|
335
|
+
priority: adjustedCount + 10000
|
|
336
|
+
))
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Process single markers
|
|
340
|
+
for single in result.singleMarkers {
|
|
341
|
+
let id = single.markerId
|
|
342
|
+
if id == selectedMarkerId { continue }
|
|
343
|
+
if nonClusteredMarkers[id] != nil { continue }
|
|
344
|
+
guard let data = clusterableMarkerData[id] else { continue }
|
|
345
|
+
|
|
346
|
+
let icon = MarkerIconFactory.createIcon(for: data)
|
|
347
|
+
let size = icon?.size ?? CGSize(width: 27, height: 43)
|
|
348
|
+
|
|
349
|
+
renderItems.append(RenderItem(
|
|
350
|
+
coordinate: CLLocationCoordinate2D(
|
|
351
|
+
latitude: single.latitude,
|
|
352
|
+
longitude: single.longitude
|
|
353
|
+
),
|
|
354
|
+
kind: .single(markerId: id),
|
|
355
|
+
iconSize: size,
|
|
356
|
+
priority: 0
|
|
357
|
+
))
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// --- Phase 2b: Apply strategy ---
|
|
361
|
+
var visibleItems = renderItems
|
|
362
|
+
|
|
363
|
+
#if DEBUG
|
|
364
|
+
var clusterDetails: [String] = []
|
|
365
|
+
for item in renderItems {
|
|
366
|
+
if case .cluster(let c) = item.kind {
|
|
367
|
+
clusterDetails.append(String(format: "%d@(%.4f,%.4f)", c.count, c.coordinate.latitude, c.coordinate.longitude))
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
let singleCount = renderItems.count - clusterDetails.count
|
|
371
|
+
let detailStr = clusterDetails.joined(separator: " ")
|
|
372
|
+
NSLog("[Clustering] items=%d singles=%d clusters: %@",
|
|
373
|
+
renderItems.count, singleCount, detailStr)
|
|
374
|
+
#endif
|
|
375
|
+
|
|
376
|
+
if strategy == .hideonoverlap {
|
|
377
|
+
visibleItems = resolveOverlaps(
|
|
378
|
+
items: renderItems,
|
|
379
|
+
zoom: Double(zoom),
|
|
380
|
+
mapWidth: mapSize.width
|
|
381
|
+
)
|
|
382
|
+
#if DEBUG
|
|
383
|
+
NSLog("[Clustering] hideOnOverlap: %d → %d items",
|
|
384
|
+
renderItems.count, visibleItems.count)
|
|
385
|
+
#endif
|
|
386
|
+
}
|
|
387
|
+
// For .supercluster strategy: render all items as-is (overlaps tolerated)
|
|
388
|
+
|
|
389
|
+
// --- Phase 3: Render — add new markers BEFORE removing old ones ---
|
|
390
|
+
// This prevents the visual flash that occurs when markers are destroyed
|
|
391
|
+
// then recreated. By adding first, the map is never empty.
|
|
392
|
+
let isFirstRender = renderedClusterMarkers.isEmpty && renderedSingleMarkers.isEmpty
|
|
393
|
+
|
|
394
|
+
// Stash old markers for removal after new ones are on the map
|
|
395
|
+
let oldClusterMarkers = renderedClusterMarkers
|
|
396
|
+
let oldSingleMarkers = renderedSingleMarkers
|
|
397
|
+
renderedClusterMarkers = []
|
|
398
|
+
renderedSingleMarkers = [:]
|
|
399
|
+
|
|
400
|
+
// Restore previously hidden non-clustered markers
|
|
401
|
+
for id in hiddenNonClusteredIds {
|
|
402
|
+
nonClusteredMarkers[id]?.map = gmsMapView
|
|
403
|
+
}
|
|
404
|
+
hiddenNonClusteredIds.removeAll()
|
|
405
|
+
|
|
406
|
+
// Render new items (add to map)
|
|
407
|
+
for (renderIndex, item) in visibleItems.enumerated() {
|
|
408
|
+
switch item.kind {
|
|
409
|
+
case .cluster(let clusterData):
|
|
410
|
+
let marker = GMSMarker()
|
|
411
|
+
marker.position = clusterData.coordinate
|
|
412
|
+
marker.groundAnchor = CGPoint(x: 0.5, y: 0.5)
|
|
413
|
+
|
|
414
|
+
if let icon = clusteringManager.clusterIcon(forCount: clusterData.count) {
|
|
415
|
+
marker.icon = icon
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
marker.userData = ClusterUserData(
|
|
419
|
+
markerIds: clusterData.markerIds,
|
|
420
|
+
count: clusterData.count
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
// Larger clusters render on top for consistent visual ordering
|
|
424
|
+
marker.zIndex = Int32(clusterData.count + 1000)
|
|
425
|
+
|
|
426
|
+
// Only animate on the very first render, not on pan/zoom re-renders
|
|
427
|
+
if isFirstRender, let animationStyle = clusterConfig?.animationStyle {
|
|
428
|
+
applyClusterAnimation(marker, style: animationStyle)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
marker.map = gmsMapView
|
|
432
|
+
renderedClusterMarkers.append(marker)
|
|
433
|
+
|
|
434
|
+
case .single(let markerId):
|
|
435
|
+
guard let data = clusterableMarkerData[markerId] else { continue }
|
|
436
|
+
// Reuse the existing GMSMarker if this markerId was already rendered.
|
|
437
|
+
// This avoids the remove+add cycle that causes visual flashing.
|
|
438
|
+
if let existing = oldSingleMarkers[markerId] {
|
|
439
|
+
updateGMSMarkerProperties(existing, from: data)
|
|
440
|
+
existing.zIndex = Int32(renderIndex)
|
|
441
|
+
renderedSingleMarkers[markerId] = existing
|
|
442
|
+
} else {
|
|
443
|
+
let marker = GMSMarker()
|
|
444
|
+
configureGMSMarker(marker, from: data)
|
|
445
|
+
marker.zIndex = Int32(renderIndex)
|
|
446
|
+
marker.map = gmsMapView
|
|
447
|
+
renderedSingleMarkers[markerId] = marker
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// NOW remove old markers (after new ones are visible)
|
|
453
|
+
for marker in oldClusterMarkers {
|
|
454
|
+
marker.map = nil
|
|
455
|
+
}
|
|
456
|
+
for (id, marker) in oldSingleMarkers {
|
|
457
|
+
// Skip markers that were reused in the new render
|
|
458
|
+
if renderedSingleMarkers[id] != nil { continue }
|
|
459
|
+
marker.map = nil
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Handle non-clustered markers with hideOnOverlap
|
|
463
|
+
if strategy == .hideonoverlap {
|
|
464
|
+
hideOverlappingNonClusteredMarkers(
|
|
465
|
+
visibleItems: visibleItems,
|
|
466
|
+
zoom: Double(zoom),
|
|
467
|
+
mapWidth: mapSize.width
|
|
468
|
+
)
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// MARK: - Overlap Resolution (hideOnOverlap strategy)
|
|
473
|
+
|
|
474
|
+
/// Removes lower-priority items that visually overlap higher-priority items.
|
|
475
|
+
/// Uses Mercator projection math — no GMSProjection dependency.
|
|
476
|
+
private func resolveOverlaps(
|
|
477
|
+
items: [RenderItem],
|
|
478
|
+
zoom: Double,
|
|
479
|
+
mapWidth: CGFloat
|
|
480
|
+
) -> [RenderItem] {
|
|
481
|
+
let sorted = items.sorted { $0.priority > $1.priority }
|
|
482
|
+
var visible: [RenderItem] = []
|
|
483
|
+
visible.reserveCapacity(sorted.count)
|
|
484
|
+
|
|
485
|
+
let worldSize = 256.0 * pow(2.0, zoom)
|
|
486
|
+
|
|
487
|
+
for item in sorted {
|
|
488
|
+
var hasOverlap = false
|
|
489
|
+
for existing in visible {
|
|
490
|
+
if itemsOverlap(item, existing, worldSize: worldSize) {
|
|
491
|
+
hasOverlap = true
|
|
492
|
+
break
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
if !hasOverlap {
|
|
496
|
+
visible.append(item)
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return visible
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/// Hides non-clustered markers that overlap with already-rendered items.
|
|
504
|
+
private func hideOverlappingNonClusteredMarkers(
|
|
505
|
+
visibleItems: [RenderItem],
|
|
506
|
+
zoom: Double,
|
|
507
|
+
mapWidth: CGFloat
|
|
508
|
+
) {
|
|
509
|
+
let worldSize = 256.0 * pow(2.0, zoom)
|
|
510
|
+
|
|
511
|
+
for (id, ncMarker) in nonClusteredMarkers {
|
|
512
|
+
let data = clusterableMarkerData[id]
|
|
513
|
+
let icon = data.flatMap { MarkerIconFactory.createIcon(for: $0) }
|
|
514
|
+
let size = icon?.size ?? CGSize(width: Self.defaultMarkerSize, height: Self.defaultMarkerSize)
|
|
515
|
+
|
|
516
|
+
let ncItem = RenderItem(
|
|
517
|
+
coordinate: ncMarker.position,
|
|
518
|
+
kind: .single(markerId: id),
|
|
519
|
+
iconSize: size,
|
|
520
|
+
priority: 0
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
var hasOverlap = false
|
|
524
|
+
for existing in visibleItems {
|
|
525
|
+
if itemsOverlap(ncItem, existing, worldSize: worldSize) {
|
|
526
|
+
hasOverlap = true
|
|
527
|
+
break
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if hasOverlap {
|
|
532
|
+
ncMarker.map = nil
|
|
533
|
+
hiddenNonClusteredIds.insert(id)
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/// Checks if two render items overlap in screen space using Mercator projection.
|
|
539
|
+
private func itemsOverlap(_ a: RenderItem, _ b: RenderItem, worldSize: Double) -> Bool {
|
|
540
|
+
let dx: Double = mercatorX(a.coordinate.longitude, worldSize: worldSize)
|
|
541
|
+
- mercatorX(b.coordinate.longitude, worldSize: worldSize)
|
|
542
|
+
let dy: Double = mercatorY(a.coordinate.latitude, worldSize: worldSize)
|
|
543
|
+
- mercatorY(b.coordinate.latitude, worldSize: worldSize)
|
|
544
|
+
let minSepX: Double = Double(a.iconSize.width + b.iconSize.width) / 2.0
|
|
545
|
+
let minSepY: Double = Double(a.iconSize.height + b.iconSize.height) / 2.0
|
|
546
|
+
return Swift.abs(dx) < minSepX && Swift.abs(dy) < minSepY
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// MARK: - Mercator Math Helpers
|
|
550
|
+
|
|
551
|
+
private func mercatorX(_ longitude: Double, worldSize: Double) -> Double {
|
|
552
|
+
return (longitude + 180.0) / 360.0 * worldSize
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
private func mercatorY(_ latitude: Double, worldSize: Double) -> Double {
|
|
556
|
+
let latRad = latitude * .pi / 180.0
|
|
557
|
+
return (1.0 - log(tan(latRad) + 1.0 / cos(latRad)) / .pi) / 2.0 * worldSize
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/// Always create a fresh GMSMarker.
|
|
561
|
+
/// Google Maps SDK has a known bug where reused markers render at incorrect
|
|
562
|
+
/// positions after being removed and re-added to the map. Creating fresh
|
|
563
|
+
/// markers avoids this issue entirely.
|
|
564
|
+
private func getMarkerFromPool() -> GMSMarker {
|
|
565
|
+
return GMSMarker()
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/// Dispose of a marker by removing it from the map.
|
|
569
|
+
/// We intentionally do NOT pool markers due to the Google Maps SDK reuse bug.
|
|
570
|
+
private func returnMarkerToPool(_ marker: GMSMarker) {
|
|
571
|
+
marker.map = nil
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
private func clearRenderedMarkersToPool() {
|
|
575
|
+
for marker in renderedClusterMarkers {
|
|
576
|
+
returnMarkerToPool(marker)
|
|
577
|
+
}
|
|
578
|
+
renderedClusterMarkers.removeAll()
|
|
579
|
+
|
|
580
|
+
for (_, marker) in renderedSingleMarkers {
|
|
581
|
+
returnMarkerToPool(marker)
|
|
582
|
+
}
|
|
583
|
+
renderedSingleMarkers.removeAll()
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
private func renderAllMarkersIndividually() {
|
|
587
|
+
for (_, markerData) in clusterableMarkerData {
|
|
588
|
+
if renderedSingleMarkers[markerData.id] == nil {
|
|
589
|
+
let marker = getMarkerFromPool()
|
|
590
|
+
configureGMSMarker(marker, from: markerData)
|
|
591
|
+
marker.map = gmsMapView
|
|
592
|
+
renderedSingleMarkers[markerData.id] = marker
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/// Configure a GMSMarker with marker data (for initial setup)
|
|
598
|
+
private func configureGMSMarker(_ gmsMarker: GMSMarker, from markerData: MarkerData) {
|
|
599
|
+
gmsMarker.position = CLLocationCoordinate2D(
|
|
600
|
+
latitude: markerData.coordinate.latitude,
|
|
601
|
+
longitude: markerData.coordinate.longitude
|
|
602
|
+
)
|
|
603
|
+
gmsMarker.title = markerData.title
|
|
604
|
+
gmsMarker.snippet = markerData.description
|
|
605
|
+
gmsMarker.isDraggable = markerData.draggable
|
|
606
|
+
gmsMarker.opacity = Float(markerData.opacity)
|
|
607
|
+
gmsMarker.rotation = markerData.rotation
|
|
608
|
+
gmsMarker.zIndex = Int32(markerData.zIndex)
|
|
609
|
+
gmsMarker.groundAnchor = CGPoint(
|
|
610
|
+
x: markerData.anchor.x,
|
|
611
|
+
y: markerData.anchor.y
|
|
612
|
+
)
|
|
613
|
+
gmsMarker.userData = markerData.id
|
|
614
|
+
gmsMarker.icon = MarkerIconFactory.createIcon(for: markerData)
|
|
615
|
+
|
|
616
|
+
switch markerData.animation {
|
|
617
|
+
case .pop:
|
|
618
|
+
gmsMarker.appearAnimation = .pop
|
|
619
|
+
case .fadein:
|
|
620
|
+
gmsMarker.appearAnimation = .fadeIn
|
|
621
|
+
case .none:
|
|
622
|
+
gmsMarker.appearAnimation = .none
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
private func applyClusterAnimation(
|
|
627
|
+
_ marker: GMSMarker,
|
|
628
|
+
style: ClusterAnimationStyle
|
|
629
|
+
) {
|
|
630
|
+
switch style {
|
|
631
|
+
case .bounce, .spring:
|
|
632
|
+
marker.appearAnimation = .pop
|
|
633
|
+
case .fade:
|
|
634
|
+
marker.appearAnimation = .fadeIn
|
|
635
|
+
case .scale, .default:
|
|
636
|
+
marker.appearAnimation = .pop
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// MARK: - Marker Management
|
|
641
|
+
|
|
642
|
+
func addMarker(_ marker: MarkerData) {
|
|
643
|
+
// FIX #3: NitroMap dispatches to main. Provider assumes main thread.
|
|
644
|
+
addMarkerSync(marker)
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
func addMarkers(_ markers: [MarkerData]) {
|
|
648
|
+
for marker in markers {
|
|
649
|
+
addMarkerSync(marker)
|
|
650
|
+
}
|
|
651
|
+
performClustering()
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
private func addMarkerSync(_ markerData: MarkerData) {
|
|
655
|
+
removeMarkerSync(markerData.id)
|
|
656
|
+
|
|
657
|
+
// ALL markers go into the clustering engine so it knows their positions
|
|
658
|
+
// and sizes for overlap prevention
|
|
659
|
+
clusterableMarkerData[markerData.id] = markerData
|
|
660
|
+
clusteringManager.addMarker(markerData)
|
|
661
|
+
|
|
662
|
+
if !markerData.clusteringEnabled || !(clusterConfig?.enabled ?? true) {
|
|
663
|
+
// Non-clustered markers also get a direct GMSMarker on the map
|
|
664
|
+
let marker = getMarkerFromPool()
|
|
665
|
+
configureGMSMarker(marker, from: markerData)
|
|
666
|
+
marker.map = gmsMapView
|
|
667
|
+
nonClusteredMarkers[markerData.id] = marker
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
func updateMarker(_ marker: MarkerData) {
|
|
672
|
+
updateMarkerInPlace(marker)
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/// Update marker in-place without triggering full re-clustering
|
|
676
|
+
/// This prevents texture allocation exhaustion by reusing existing GMSMarker objects
|
|
677
|
+
private func updateMarkerInPlace(_ markerData: MarkerData) {
|
|
678
|
+
let id = markerData.id
|
|
679
|
+
|
|
680
|
+
// Update stored data
|
|
681
|
+
if let existing = clusterableMarkerData[id] {
|
|
682
|
+
clusterableMarkerData[id] = markerData
|
|
683
|
+
|
|
684
|
+
// Only re-index in the C++ engine if coordinate changed.
|
|
685
|
+
// Cosmetic updates (selection, clusteringEnabled, icon style) don't
|
|
686
|
+
// need a spatial re-index and must NOT set markersChanged_ = true,
|
|
687
|
+
// which would force a full hierarchy rebuild.
|
|
688
|
+
let coordChanged =
|
|
689
|
+
existing.coordinate.latitude != markerData.coordinate.latitude ||
|
|
690
|
+
existing.coordinate.longitude != markerData.coordinate.longitude
|
|
691
|
+
if coordChanged {
|
|
692
|
+
clusteringManager.removeMarker(id)
|
|
693
|
+
clusteringManager.addMarker(markerData)
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// Update rendered marker in-place (no remove/re-add)
|
|
698
|
+
if let gmsMarker = renderedSingleMarkers[id] {
|
|
699
|
+
updateGMSMarkerProperties(gmsMarker, from: markerData)
|
|
700
|
+
} else if let gmsMarker = nonClusteredMarkers[id] {
|
|
701
|
+
updateGMSMarkerProperties(gmsMarker, from: markerData)
|
|
702
|
+
}
|
|
703
|
+
// If marker not in any dict, it will be created with new data when clustering runs
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/// Update GMSMarker properties in-place without creating a new marker object
|
|
707
|
+
private func updateGMSMarkerProperties(_ gmsMarker: GMSMarker, from markerData: MarkerData) {
|
|
708
|
+
gmsMarker.position = CLLocationCoordinate2D(
|
|
709
|
+
latitude: markerData.coordinate.latitude,
|
|
710
|
+
longitude: markerData.coordinate.longitude
|
|
711
|
+
)
|
|
712
|
+
gmsMarker.title = markerData.title
|
|
713
|
+
gmsMarker.snippet = markerData.description
|
|
714
|
+
gmsMarker.isDraggable = markerData.draggable
|
|
715
|
+
gmsMarker.opacity = Float(markerData.opacity)
|
|
716
|
+
gmsMarker.rotation = markerData.rotation
|
|
717
|
+
gmsMarker.zIndex = Int32(markerData.zIndex)
|
|
718
|
+
gmsMarker.groundAnchor = CGPoint(
|
|
719
|
+
x: markerData.anchor.x,
|
|
720
|
+
y: markerData.anchor.y
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
// Only regenerate icon if needed - cache will handle deduplication
|
|
724
|
+
if let newIcon = MarkerIconFactory.createIcon(for: markerData) {
|
|
725
|
+
gmsMarker.icon = newIcon
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
func removeMarker(_ id: String) {
|
|
730
|
+
removeMarkerSync(id)
|
|
731
|
+
performClustering()
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
private func removeMarkerSync(_ id: String) {
|
|
735
|
+
clusteringManager.removeMarker(id)
|
|
736
|
+
clusterableMarkerData.removeValue(forKey: id)
|
|
737
|
+
|
|
738
|
+
if let marker = renderedSingleMarkers[id] {
|
|
739
|
+
marker.map = nil
|
|
740
|
+
renderedSingleMarkers.removeValue(forKey: id)
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
if let marker = nonClusteredMarkers[id] {
|
|
744
|
+
marker.map = nil
|
|
745
|
+
nonClusteredMarkers.removeValue(forKey: id)
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
func clearMarkers() {
|
|
750
|
+
clearMarkersSync()
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
private func clearMarkersSync() {
|
|
754
|
+
clusteringManager.clearMarkers()
|
|
755
|
+
clusterableMarkerData.removeAll()
|
|
756
|
+
clearRenderedMarkersToPool()
|
|
757
|
+
|
|
758
|
+
for (_, marker) in nonClusteredMarkers {
|
|
759
|
+
returnMarkerToPool(marker)
|
|
760
|
+
}
|
|
761
|
+
nonClusteredMarkers.removeAll()
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
func selectMarker(_ id: String) {
|
|
765
|
+
// Deselect previous marker if exists
|
|
766
|
+
if let previousId = selectedMarkerId, previousId != id {
|
|
767
|
+
updateMarkerSelectionState(previousId, selected: false)
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// Select new marker
|
|
771
|
+
selectedMarkerId = id
|
|
772
|
+
updateMarkerSelectionState(id, selected: true)
|
|
773
|
+
|
|
774
|
+
// Get marker position and animate to it
|
|
775
|
+
var markerPosition: CLLocationCoordinate2D?
|
|
776
|
+
|
|
777
|
+
if let marker = renderedSingleMarkers[id] {
|
|
778
|
+
gmsMapView.selectedMarker = marker
|
|
779
|
+
markerPosition = marker.position
|
|
780
|
+
} else if let marker = nonClusteredMarkers[id] {
|
|
781
|
+
gmsMapView.selectedMarker = marker
|
|
782
|
+
markerPosition = marker.position
|
|
783
|
+
} else if let markerData = clusterableMarkerData[id] {
|
|
784
|
+
// Marker is inside a cluster — create a temporary single marker
|
|
785
|
+
let marker = GMSMarker()
|
|
786
|
+
configureGMSMarker(marker, from: markerData)
|
|
787
|
+
marker.map = gmsMapView
|
|
788
|
+
renderedSingleMarkers[id] = marker
|
|
789
|
+
gmsMapView.selectedMarker = marker
|
|
790
|
+
markerPosition = marker.position
|
|
791
|
+
performClustering()
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// Animate camera to marker position
|
|
795
|
+
if let position = markerPosition {
|
|
796
|
+
let camera = GMSCameraPosition.camera(
|
|
797
|
+
withLatitude: position.latitude,
|
|
798
|
+
longitude: position.longitude,
|
|
799
|
+
zoom: gmsMapView.camera.zoom
|
|
800
|
+
)
|
|
801
|
+
CATransaction.begin()
|
|
802
|
+
CATransaction.setAnimationDuration(0.3)
|
|
803
|
+
gmsMapView.animate(to: camera)
|
|
804
|
+
CATransaction.commit()
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/// Update a marker's visual selection state by regenerating its icon
|
|
809
|
+
private func updateMarkerSelectionState(_ id: String, selected: Bool) {
|
|
810
|
+
guard let data = clusterableMarkerData[id] else { return }
|
|
811
|
+
|
|
812
|
+
// Use injected handler to create updated marker data with selection state
|
|
813
|
+
guard let updatedData = selectionHandler.updateSelectionState(for: data, selected: selected) else {
|
|
814
|
+
return
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// Update stored data
|
|
818
|
+
clusterableMarkerData[id] = updatedData
|
|
819
|
+
clusteringManager.removeMarker(id)
|
|
820
|
+
clusteringManager.addMarker(updatedData)
|
|
821
|
+
|
|
822
|
+
// Regenerate icon and apply to GMSMarker
|
|
823
|
+
if let gmsMarker = renderedSingleMarkers[id] {
|
|
824
|
+
gmsMarker.icon = MarkerIconFactory.createIcon(for: updatedData)
|
|
825
|
+
} else if let gmsMarker = nonClusteredMarkers[id] {
|
|
826
|
+
gmsMarker.icon = MarkerIconFactory.createIcon(for: updatedData)
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// MARK: - Clustering Control
|
|
831
|
+
|
|
832
|
+
func setClusteringEnabled(_ enabled: Bool) {
|
|
833
|
+
self.clusterConfig = ClusterConfig.withEnabled(enabled, existing: clusterConfig)
|
|
834
|
+
performClustering()
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
func refreshClusters() {
|
|
838
|
+
performClustering()
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// MARK: - Map Style
|
|
842
|
+
|
|
843
|
+
func setMapStyle(_ style: [MapStyleElement]?) {
|
|
844
|
+
customMapStyle = style
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
func setDarkMode(_ enabled: Bool) {
|
|
848
|
+
darkMode = enabled
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
private func applyMapStyle() {
|
|
852
|
+
guard let styleElements = customMapStyle, !styleElements.isEmpty else {
|
|
853
|
+
gmsMapView.mapStyle = nil
|
|
854
|
+
return
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
do {
|
|
858
|
+
let jsonArray = styleElements.map { element -> [String: Any] in
|
|
859
|
+
var dict: [String: Any] = [:]
|
|
860
|
+
if let featureType = element.featureType {
|
|
861
|
+
dict["featureType"] = featureType
|
|
862
|
+
}
|
|
863
|
+
if let elementType = element.elementType {
|
|
864
|
+
dict["elementType"] = elementType
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
dict["stylers"] = element.stylers.map { styler -> [String: Any] in
|
|
868
|
+
var s: [String: Any] = [:]
|
|
869
|
+
if let color = styler.color { s["color"] = color }
|
|
870
|
+
if let visibility = styler.visibility { s["visibility"] = visibility }
|
|
871
|
+
if let weight = styler.weight { s["weight"] = weight }
|
|
872
|
+
if let saturation = styler.saturation { s["saturation"] = saturation }
|
|
873
|
+
if let lightness = styler.lightness { s["lightness"] = lightness }
|
|
874
|
+
if let gamma = styler.gamma { s["gamma"] = gamma }
|
|
875
|
+
return s
|
|
876
|
+
}
|
|
877
|
+
return dict
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
let jsonData = try JSONSerialization.data(withJSONObject: jsonArray)
|
|
881
|
+
let jsonString = String(data: jsonData, encoding: .utf8) ?? "[]"
|
|
882
|
+
gmsMapView.mapStyle = try GMSMapStyle(jsonString: jsonString)
|
|
883
|
+
} catch {
|
|
884
|
+
// FIX #12: Surface errors via onError instead of silent print
|
|
885
|
+
onError?(MapError(code: "STYLE_ERROR", message: "Failed to apply map style: \(error.localizedDescription)"))
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
private func applyDarkMode() {
|
|
890
|
+
if let isDark = darkMode {
|
|
891
|
+
gmsMapView.overrideUserInterfaceStyle = isDark ? .dark : .light
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// MARK: - Helper Methods
|
|
896
|
+
|
|
897
|
+
private func updateCameraToInitialRegion() {
|
|
898
|
+
guard let region = initialRegion else { return }
|
|
899
|
+
|
|
900
|
+
let camera = GMSCameraPosition.camera(
|
|
901
|
+
withLatitude: region.latitude,
|
|
902
|
+
longitude: region.longitude,
|
|
903
|
+
zoom: calculateZoom(for: region)
|
|
904
|
+
)
|
|
905
|
+
gmsMapView.animate(to: camera)
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
func calculateZoom(for region: Region) -> Float {
|
|
909
|
+
// FIX #5: Use MapStyleProvider's logarithmic calculation
|
|
910
|
+
return Float(MapStyleProvider.shared.zoomFromDeltas(
|
|
911
|
+
latDelta: region.latitudeDelta,
|
|
912
|
+
lonDelta: region.longitudeDelta
|
|
913
|
+
))
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
private func convertMapType(_ type: MapType?) -> GMSMapViewType {
|
|
917
|
+
switch type {
|
|
918
|
+
case .satellite: return .satellite
|
|
919
|
+
case .hybrid: return .hybrid
|
|
920
|
+
case .standard, .none: return .normal
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
}
|