@maydon_tech/react-native-nitro-maps 0.1.4 → 0.2.1

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.
Files changed (222) hide show
  1. package/LICENSE +1 -1
  2. package/NitroMap.podspec +1 -1
  3. package/README.md +82 -9
  4. package/android/CMakeLists.txt +4 -1
  5. package/android/build.gradle +6 -2
  6. package/android/gradle.properties +4 -4
  7. package/android/src/main/cpp/ClusterEngineJNI.cpp +198 -0
  8. package/android/src/main/kotlin/com/margelo/nitro/nitromap/NitroMap.kt +397 -0
  9. package/android/src/main/kotlin/com/margelo/nitro/nitromap/NitroMapConfig.kt +53 -0
  10. package/android/src/main/{java → kotlin}/com/margelo/nitro/nitromap/NitroMapPackage.kt +4 -4
  11. package/android/src/main/kotlin/com/margelo/nitro/nitromap/NitroMapView.kt +73 -0
  12. package/android/src/main/kotlin/com/margelo/nitro/nitromap/UserLocationManager.kt +295 -0
  13. package/android/src/main/kotlin/com/margelo/nitro/nitromap/clustering/ClusterIconRenderer.kt +111 -0
  14. package/android/src/main/kotlin/com/margelo/nitro/nitromap/clustering/ClusteringManager.kt +104 -0
  15. package/android/src/main/kotlin/com/margelo/nitro/nitromap/clustering/NitroClusterEngine.kt +166 -0
  16. package/android/src/main/kotlin/com/margelo/nitro/nitromap/markers/MarkerIconFactory.kt +303 -0
  17. package/android/src/main/kotlin/com/margelo/nitro/nitromap/markers/MarkerSelectionHandler.kt +72 -0
  18. package/android/src/main/kotlin/com/margelo/nitro/nitromap/markers/PriceMarkerRenderer.kt +159 -0
  19. package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/MapProviderFactory.kt +24 -0
  20. package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/MapProviderInterface.kt +128 -0
  21. package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/google/GoogleMapDelegate.kt +317 -0
  22. package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/google/GoogleMapProvider+Clustering.kt +524 -0
  23. package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/google/GoogleMapProvider+Markers.kt +358 -0
  24. package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/google/GoogleMapProvider+Overlays.kt +272 -0
  25. package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/google/GoogleMapProvider+UserLocation.kt +296 -0
  26. package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/google/GoogleMapProvider.kt +815 -0
  27. package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/google/MarkerTagData.kt +19 -0
  28. package/ios/Location/NitroLocationManager.swift +116 -0
  29. package/ios/MarkerRenderer/MarkerIconFactory.swift +1 -3
  30. package/ios/MarkerRenderer/PriceMarkerRenderer.swift +10 -6
  31. package/ios/NitroMap.swift +279 -13
  32. package/ios/NitroMapConfig/NitroMapConfig.swift +45 -0
  33. package/ios/Providers/{GoogleMapDelegate.swift → Google/GoogleMapDelegate.swift} +48 -23
  34. package/ios/Providers/Google/GoogleMapProvider+Camera.swift +180 -0
  35. package/ios/Providers/Google/GoogleMapProvider+Clustering.swift +541 -0
  36. package/ios/Providers/Google/GoogleMapProvider+Markers.swift +270 -0
  37. package/ios/Providers/Google/GoogleMapProvider+Overlays.swift +245 -0
  38. package/ios/Providers/Google/GoogleMapProvider+UserLocation.swift +180 -0
  39. package/ios/Providers/Google/GoogleMapProvider.swift +342 -0
  40. package/ios/Providers/MapProviderFactory.swift +17 -0
  41. package/ios/Providers/MapProviderProtocol.swift +48 -1
  42. package/ios/Shared/ClusterConfig+Factory.swift +2 -2
  43. package/ios/Shared/MapStyleProvider.swift +6 -4
  44. package/ios/Shared/MarkerSelectionHandler.swift +4 -1
  45. package/ios/Utils/ColorValueExtension.swift +46 -67
  46. package/lib/module/components/ImageMarker.js +39 -29
  47. package/lib/module/components/ImageMarker.js.map +1 -1
  48. package/lib/module/components/Marker.js +118 -0
  49. package/lib/module/components/Marker.js.map +1 -0
  50. package/lib/module/components/NitroCircle.js +92 -0
  51. package/lib/module/components/NitroCircle.js.map +1 -0
  52. package/lib/module/components/NitroMap.js +216 -76
  53. package/lib/module/components/NitroMap.js.map +1 -1
  54. package/lib/module/components/NitroPolygon.js +137 -0
  55. package/lib/module/components/NitroPolygon.js.map +1 -0
  56. package/lib/module/components/NitroPolyline.js +115 -0
  57. package/lib/module/components/NitroPolyline.js.map +1 -0
  58. package/lib/module/components/PriceMarker.js +16 -29
  59. package/lib/module/components/PriceMarker.js.map +1 -1
  60. package/lib/module/context/NitroMapContext.js.map +1 -1
  61. package/lib/module/hooks/useNitroCircle.js +18 -0
  62. package/lib/module/hooks/useNitroCircle.js.map +1 -0
  63. package/lib/module/hooks/useNitroMarker.js +26 -9
  64. package/lib/module/hooks/useNitroMarker.js.map +1 -1
  65. package/lib/module/hooks/useNitroOverlay.js +59 -0
  66. package/lib/module/hooks/useNitroOverlay.js.map +1 -0
  67. package/lib/module/hooks/useNitroPolygon.js +18 -0
  68. package/lib/module/hooks/useNitroPolygon.js.map +1 -0
  69. package/lib/module/hooks/useNitroPolyline.js +18 -0
  70. package/lib/module/hooks/useNitroPolyline.js.map +1 -0
  71. package/lib/module/index.js +5 -0
  72. package/lib/module/index.js.map +1 -1
  73. package/lib/module/types/overlay.js +4 -0
  74. package/lib/module/types/overlay.js.map +1 -0
  75. package/lib/module/types/theme.js +4 -0
  76. package/lib/module/types/theme.js.map +1 -0
  77. package/lib/module/utils/colors.js +41 -13
  78. package/lib/module/utils/colors.js.map +1 -1
  79. package/lib/module/utils/validation.js +45 -0
  80. package/lib/module/utils/validation.js.map +1 -0
  81. package/lib/typescript/src/components/ImageMarker.d.ts.map +1 -1
  82. package/lib/typescript/src/components/Marker.d.ts +34 -0
  83. package/lib/typescript/src/components/Marker.d.ts.map +1 -0
  84. package/lib/typescript/src/components/NitroCircle.d.ts +70 -0
  85. package/lib/typescript/src/components/NitroCircle.d.ts.map +1 -0
  86. package/lib/typescript/src/components/NitroMap.d.ts +60 -3
  87. package/lib/typescript/src/components/NitroMap.d.ts.map +1 -1
  88. package/lib/typescript/src/components/NitroPolygon.d.ts +86 -0
  89. package/lib/typescript/src/components/NitroPolygon.d.ts.map +1 -0
  90. package/lib/typescript/src/components/NitroPolyline.d.ts +84 -0
  91. package/lib/typescript/src/components/NitroPolyline.d.ts.map +1 -0
  92. package/lib/typescript/src/components/PriceMarker.d.ts +0 -5
  93. package/lib/typescript/src/components/PriceMarker.d.ts.map +1 -1
  94. package/lib/typescript/src/context/NitroMapContext.d.ts +2 -0
  95. package/lib/typescript/src/context/NitroMapContext.d.ts.map +1 -1
  96. package/lib/typescript/src/hooks/useNitroCircle.d.ts +7 -0
  97. package/lib/typescript/src/hooks/useNitroCircle.d.ts.map +1 -0
  98. package/lib/typescript/src/hooks/useNitroMarker.d.ts +20 -0
  99. package/lib/typescript/src/hooks/useNitroMarker.d.ts.map +1 -1
  100. package/lib/typescript/src/hooks/useNitroOverlay.d.ts +26 -0
  101. package/lib/typescript/src/hooks/useNitroOverlay.d.ts.map +1 -0
  102. package/lib/typescript/src/hooks/useNitroPolygon.d.ts +7 -0
  103. package/lib/typescript/src/hooks/useNitroPolygon.d.ts.map +1 -0
  104. package/lib/typescript/src/hooks/useNitroPolyline.d.ts +7 -0
  105. package/lib/typescript/src/hooks/useNitroPolyline.d.ts.map +1 -0
  106. package/lib/typescript/src/index.d.ts +15 -2
  107. package/lib/typescript/src/index.d.ts.map +1 -1
  108. package/lib/typescript/src/specs/NitroMap.nitro.d.ts +248 -6
  109. package/lib/typescript/src/specs/NitroMap.nitro.d.ts.map +1 -1
  110. package/lib/typescript/src/types/map.d.ts +34 -4
  111. package/lib/typescript/src/types/map.d.ts.map +1 -1
  112. package/lib/typescript/src/types/marker.d.ts +24 -36
  113. package/lib/typescript/src/types/marker.d.ts.map +1 -1
  114. package/lib/typescript/src/types/overlay.d.ts +84 -0
  115. package/lib/typescript/src/types/overlay.d.ts.map +1 -0
  116. package/lib/typescript/src/types/theme.d.ts +93 -0
  117. package/lib/typescript/src/types/theme.d.ts.map +1 -0
  118. package/lib/typescript/src/utils/colors.d.ts +6 -8
  119. package/lib/typescript/src/utils/colors.d.ts.map +1 -1
  120. package/lib/typescript/src/utils/validation.d.ts +12 -0
  121. package/lib/typescript/src/utils/validation.d.ts.map +1 -0
  122. package/nitrogen/generated/android/c++/JCircleData.hpp +94 -0
  123. package/nitrogen/generated/android/c++/JClusterConfig.hpp +5 -7
  124. package/nitrogen/generated/android/c++/JCoordinateRing.hpp +77 -0
  125. package/nitrogen/generated/android/c++/JFunc_void_UserLocationChangeEvent.hpp +79 -0
  126. package/nitrogen/generated/android/c++/JFunc_void_UserTrackingMode.hpp +77 -0
  127. package/nitrogen/generated/android/c++/JFunc_void_std__string.hpp +76 -0
  128. package/nitrogen/generated/android/c++/JHybridNitroMapSpec.cpp +332 -21
  129. package/nitrogen/generated/android/c++/JHybridNitroMapSpec.hpp +53 -2
  130. package/nitrogen/generated/android/c++/JMarkerAnimation.hpp +3 -6
  131. package/nitrogen/generated/android/c++/JMarkerData.hpp +15 -3
  132. package/nitrogen/generated/android/c++/JPolygonData.hpp +133 -0
  133. package/nitrogen/generated/android/c++/JPolylineData.hpp +113 -0
  134. package/nitrogen/generated/android/c++/JUserLocationChangeEvent.hpp +70 -0
  135. package/nitrogen/generated/android/c++/JUserTrackingMode.hpp +62 -0
  136. package/nitrogen/generated/android/c++/views/JHybridNitroMapStateUpdater.cpp +72 -4
  137. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/CircleData.kt +62 -0
  138. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/ClusterConfig.kt +4 -4
  139. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/CoordinateRing.kt +38 -0
  140. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/Func_void_UserLocationChangeEvent.kt +80 -0
  141. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/Func_void_UserTrackingMode.kt +80 -0
  142. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/Func_void_std__string.kt +80 -0
  143. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/HybridNitroMapSpec.kt +228 -2
  144. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/MarkerAnimation.kt +1 -2
  145. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/MarkerData.kt +12 -3
  146. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/PolygonData.kt +62 -0
  147. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/PolylineData.kt +62 -0
  148. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/UserLocationChangeEvent.kt +47 -0
  149. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/{ClusterAnimationStyle.kt → UserTrackingMode.kt} +6 -8
  150. package/nitrogen/generated/android/nitromapOnLoad.cpp +6 -0
  151. package/nitrogen/generated/ios/NitroMap-Swift-Cxx-Bridge.cpp +24 -0
  152. package/nitrogen/generated/ios/NitroMap-Swift-Cxx-Bridge.hpp +178 -17
  153. package/nitrogen/generated/ios/NitroMap-Swift-Cxx-Umbrella.hpp +18 -3
  154. package/nitrogen/generated/ios/c++/HybridNitroMapSpecSwift.hpp +252 -16
  155. package/nitrogen/generated/ios/c++/views/HybridNitroMapComponent.mm +90 -5
  156. package/nitrogen/generated/ios/swift/CircleData.swift +143 -0
  157. package/nitrogen/generated/ios/swift/ClusterConfig.swift +22 -15
  158. package/nitrogen/generated/ios/swift/CoordinateRing.swift +48 -0
  159. package/nitrogen/generated/ios/swift/Func_void_UserLocationChangeEvent.swift +47 -0
  160. package/nitrogen/generated/ios/swift/Func_void_UserTrackingMode.swift +47 -0
  161. package/nitrogen/generated/ios/swift/Func_void_std__string.swift +47 -0
  162. package/nitrogen/generated/ios/swift/HybridNitroMapSpec.swift +35 -1
  163. package/nitrogen/generated/ios/swift/HybridNitroMapSpec_cxx.swift +582 -8
  164. package/nitrogen/generated/ios/swift/MarkerAnimation.swift +4 -8
  165. package/nitrogen/generated/ios/swift/MarkerData.swift +54 -2
  166. package/nitrogen/generated/ios/swift/PolygonData.swift +167 -0
  167. package/nitrogen/generated/ios/swift/PolylineData.swift +155 -0
  168. package/nitrogen/generated/ios/swift/UserLocationChangeEvent.swift +69 -0
  169. package/nitrogen/generated/ios/swift/UserTrackingMode.swift +44 -0
  170. package/nitrogen/generated/shared/c++/CircleData.hpp +113 -0
  171. package/nitrogen/generated/shared/c++/ClusterConfig.hpp +5 -8
  172. package/nitrogen/generated/shared/c++/CoordinateRing.hpp +77 -0
  173. package/nitrogen/generated/shared/c++/HybridNitroMapSpec.cpp +53 -2
  174. package/nitrogen/generated/shared/c++/HybridNitroMapSpec.hpp +75 -6
  175. package/nitrogen/generated/shared/c++/MarkerAnimation.hpp +4 -8
  176. package/nitrogen/generated/shared/c++/MarkerData.hpp +14 -2
  177. package/nitrogen/generated/shared/c++/PolygonData.hpp +117 -0
  178. package/nitrogen/generated/shared/c++/PolylineData.hpp +114 -0
  179. package/nitrogen/generated/shared/c++/UserLocationChangeEvent.hpp +88 -0
  180. package/nitrogen/generated/shared/c++/UserTrackingMode.hpp +80 -0
  181. package/nitrogen/generated/shared/c++/views/HybridNitroMapComponent.cpp +216 -12
  182. package/nitrogen/generated/shared/c++/views/HybridNitroMapComponent.hpp +23 -1
  183. package/nitrogen/generated/shared/json/NitroMapConfig.json +18 -1
  184. package/package.json +36 -5
  185. package/src/components/ImageMarker.tsx +58 -42
  186. package/src/components/Marker.tsx +161 -0
  187. package/src/components/NitroCircle.tsx +183 -0
  188. package/src/components/NitroMap.tsx +328 -78
  189. package/src/components/NitroPolygon.tsx +229 -0
  190. package/src/components/NitroPolyline.tsx +208 -0
  191. package/src/components/PriceMarker.tsx +23 -48
  192. package/src/context/NitroMapContext.tsx +4 -0
  193. package/src/hooks/useNitroCircle.ts +25 -0
  194. package/src/hooks/useNitroMarker.ts +49 -10
  195. package/src/hooks/useNitroOverlay.ts +68 -0
  196. package/src/hooks/useNitroPolygon.ts +25 -0
  197. package/src/hooks/useNitroPolyline.ts +25 -0
  198. package/src/index.tsx +28 -2
  199. package/src/specs/NitroMap.nitro.ts +294 -5
  200. package/src/types/map.ts +36 -4
  201. package/src/types/marker.ts +24 -44
  202. package/src/types/overlay.ts +87 -0
  203. package/src/types/theme.ts +101 -0
  204. package/src/utils/colors.ts +48 -16
  205. package/src/utils/validation.ts +69 -0
  206. package/android/src/main/java/com/margelo/nitro/nitromap/ClusterIconGenerator.kt +0 -108
  207. package/android/src/main/java/com/margelo/nitro/nitromap/ColorUtils.kt +0 -63
  208. package/android/src/main/java/com/margelo/nitro/nitromap/HybridNitroMap.kt +0 -408
  209. package/android/src/main/java/com/margelo/nitro/nitromap/HybridNitroMapConfig.kt +0 -68
  210. package/android/src/main/java/com/margelo/nitro/nitromap/MarkerIconCache.kt +0 -176
  211. package/android/src/main/java/com/margelo/nitro/nitromap/MarkerIconFactory.kt +0 -252
  212. package/android/src/main/java/com/margelo/nitro/nitromap/clustering/NitroClusterEngine.kt +0 -252
  213. package/android/src/main/java/com/margelo/nitro/nitromap/clustering/QuadTree.kt +0 -195
  214. package/android/src/main/java/com/margelo/nitro/nitromap/providers/GoogleMapProvider.kt +0 -912
  215. package/android/src/main/java/com/margelo/nitro/nitromap/providers/MapProviderInterface.kt +0 -70
  216. package/cpp/QuadTree.hpp +0 -246
  217. package/ios/NitroMapConfig/HybridNitroMapConfig.swift +0 -33
  218. package/ios/Providers/GoogleMapProvider+Camera.swift +0 -164
  219. package/ios/Providers/GoogleMapProvider.swift +0 -924
  220. package/nitrogen/generated/android/c++/JClusterAnimationStyle.hpp +0 -68
  221. package/nitrogen/generated/ios/swift/ClusterAnimationStyle.swift +0 -52
  222. package/nitrogen/generated/shared/c++/ClusterAnimationStyle.hpp +0 -88
@@ -0,0 +1,270 @@
1
+ import Foundation
2
+ import GoogleMaps
3
+
4
+ // MARK: - Marker Management (SRP: Marker CRUD and selection only)
5
+
6
+ extension GoogleMapProvider {
7
+
8
+ // MARK: - Marker CRUD
9
+
10
+ func addMarker(_ marker: MarkerData) {
11
+ addMarkerSync(marker)
12
+ clusteringDirty = true
13
+ }
14
+
15
+ func addMarkers(_ markers: [MarkerData]) {
16
+ for marker in markers {
17
+ addMarkerSync(marker)
18
+ }
19
+ clusteringDirty = true
20
+ performClustering()
21
+ }
22
+
23
+ func addMarkerSync(_ markerData: MarkerData) {
24
+ removeMarkerSync(markerData.id)
25
+
26
+ // ALL markers go into the clustering engine so it knows their positions
27
+ // and sizes for overlap prevention
28
+ clusterableMarkerData[markerData.id] = markerData
29
+ clusteringManager.addMarker(markerData)
30
+
31
+ if !markerData.clusteringEnabled || !(clusterConfig?.enabled ?? true) {
32
+ // Non-clustered markers also get a direct GMSMarker on the map
33
+ let marker = GMSMarker()
34
+ configureGMSMarker(marker, from: markerData)
35
+ marker.map = gmsMapView
36
+ nonClusteredMarkers[markerData.id] = marker
37
+ }
38
+ }
39
+
40
+ func updateMarker(_ marker: MarkerData) {
41
+ updateMarkerInPlace(marker)
42
+ }
43
+
44
+ /// Update marker in-place without triggering full re-clustering
45
+ /// This prevents texture allocation exhaustion by reusing existing GMSMarker objects
46
+ private func updateMarkerInPlace(_ markerData: MarkerData) {
47
+ let id = markerData.id
48
+
49
+ // Update stored data
50
+ if let existing = clusterableMarkerData[id] {
51
+ clusterableMarkerData[id] = markerData
52
+
53
+ // Only re-index in the C++ engine if coordinate changed.
54
+ // Cosmetic updates (selection, clusteringEnabled, icon style) don't
55
+ // need a spatial re-index and must NOT set markersChanged_ = true,
56
+ // which would force a full hierarchy rebuild.
57
+ let coordChanged =
58
+ existing.coordinate.latitude != markerData.coordinate.latitude ||
59
+ existing.coordinate.longitude != markerData.coordinate.longitude
60
+ if coordChanged {
61
+ clusteringManager.removeMarker(id)
62
+ clusteringManager.addMarker(markerData)
63
+ }
64
+ }
65
+
66
+ // Update rendered marker in-place (no remove/re-add)
67
+ if let gmsMarker = renderedSingleMarkers[id] {
68
+ updateGMSMarkerProperties(gmsMarker, from: markerData)
69
+ } else if let gmsMarker = nonClusteredMarkers[id] {
70
+ updateGMSMarkerProperties(gmsMarker, from: markerData)
71
+ }
72
+ // If marker not in any dict, it will be created with new data when clustering runs
73
+ }
74
+
75
+ func removeMarker(_ id: String) {
76
+ removeMarkerSync(id)
77
+ clusteringDirty = true
78
+ performClustering()
79
+ }
80
+
81
+ func removeMarkerSync(_ id: String) {
82
+ clusteringManager.removeMarker(id)
83
+ clusterableMarkerData.removeValue(forKey: id)
84
+
85
+ if let marker = renderedSingleMarkers[id] {
86
+ marker.map = nil
87
+ renderedSingleMarkers.removeValue(forKey: id)
88
+ }
89
+
90
+ if let marker = nonClusteredMarkers[id] {
91
+ marker.map = nil
92
+ nonClusteredMarkers.removeValue(forKey: id)
93
+ }
94
+ }
95
+
96
+ func clearMarkers() {
97
+ clearMarkersSync()
98
+ clusteringDirty = true
99
+ }
100
+
101
+ func clearMarkersSync() {
102
+ clusteringManager.clearMarkers()
103
+ clusterableMarkerData.removeAll()
104
+ clearRenderedMarkersToPool()
105
+
106
+ for (_, marker) in nonClusteredMarkers {
107
+ marker.map = nil
108
+ }
109
+ nonClusteredMarkers.removeAll()
110
+ }
111
+
112
+ // MARK: - Marker Selection
113
+
114
+ func deselectCurrentMarker() {
115
+ guard let previousId = selectedMarkerId else { return }
116
+ selectedMarkerId = nil
117
+ gmsMapView.selectedMarker = nil
118
+
119
+ // Revert marker data to unselected state and update icon in-place
120
+ if let data = clusterableMarkerData[previousId],
121
+ let updatedData = selectionHandler.updateSelectionState(for: data, selected: false) {
122
+ clusterableMarkerData[previousId] = updatedData
123
+
124
+ // Update icon in-place on the existing GMSMarker
125
+ if let gmsMarker = renderedSingleMarkers[previousId] {
126
+ gmsMarker.icon = MarkerIconFactory.createIcon(for: updatedData)
127
+ gmsMarker.zIndex = Int32(updatedData.zIndex ?? 0)
128
+ } else if let gmsMarker = nonClusteredMarkers[previousId] {
129
+ gmsMarker.icon = MarkerIconFactory.createIcon(for: updatedData)
130
+ gmsMarker.zIndex = Int32(updatedData.zIndex ?? 0)
131
+ }
132
+
133
+ // Add marker back into clustering engine
134
+ clusteringManager.addMarker(updatedData)
135
+ }
136
+ }
137
+
138
+ func deselectMarker() {
139
+ deselectCurrentMarker()
140
+ }
141
+
142
+ func selectMarker(_ id: String) {
143
+ // Deselect previous marker
144
+ if let previousId = selectedMarkerId, previousId != id {
145
+ deselectCurrentMarker()
146
+ }
147
+
148
+ selectedMarkerId = id
149
+
150
+ // Remove marker FROM clustering engine — it should not participate
151
+ // in clustering while selected. This prevents clustering from
152
+ // destroying the GMSMarker object and dismissing the info window.
153
+ clusteringManager.removeMarker(id)
154
+
155
+ // Update marker data to selected state (without touching clustering engine)
156
+ if let data = clusterableMarkerData[id],
157
+ let updatedData = selectionHandler.updateSelectionState(for: data, selected: true) {
158
+ clusterableMarkerData[id] = updatedData
159
+ // Regenerate icon on existing GMSMarker
160
+ if let gmsMarker = renderedSingleMarkers[id] {
161
+ gmsMarker.icon = MarkerIconFactory.createIcon(for: updatedData)
162
+ } else if let gmsMarker = nonClusteredMarkers[id] {
163
+ gmsMarker.icon = MarkerIconFactory.createIcon(for: updatedData)
164
+ }
165
+ }
166
+
167
+ // Find or create the GMSMarker and select it
168
+ var markerPosition: CLLocationCoordinate2D?
169
+
170
+ if let marker = renderedSingleMarkers[id] {
171
+ marker.zIndex = Int32.max
172
+ gmsMapView.selectedMarker = marker
173
+ markerPosition = marker.position
174
+ } else if let marker = nonClusteredMarkers[id] {
175
+ marker.zIndex = Int32.max
176
+ gmsMapView.selectedMarker = marker
177
+ markerPosition = marker.position
178
+ } else if let markerData = clusterableMarkerData[id] {
179
+ // Marker was inside a cluster — create a single marker for it
180
+ let marker = GMSMarker()
181
+ configureGMSMarker(marker, from: markerData)
182
+ marker.zIndex = Int32.max
183
+ marker.map = gmsMapView
184
+ renderedSingleMarkers[id] = marker
185
+ gmsMapView.selectedMarker = marker
186
+ markerPosition = marker.position
187
+ }
188
+
189
+ // No explicit re-clustering needed — the camera animation below
190
+ // triggers idleAt → performClustering() naturally.
191
+
192
+ // Animate camera to marker position
193
+ if let position = markerPosition {
194
+ let camera = GMSCameraPosition.camera(
195
+ withLatitude: position.latitude,
196
+ longitude: position.longitude,
197
+ zoom: gmsMapView.camera.zoom
198
+ )
199
+ CATransaction.begin()
200
+ CATransaction.setAnimationDuration(0.3)
201
+ gmsMapView.animate(to: camera)
202
+ CATransaction.commit()
203
+ }
204
+ }
205
+
206
+ // MARK: - GMSMarker Configuration Helpers
207
+
208
+ /// Configure a GMSMarker with marker data (for initial setup)
209
+ func configureGMSMarker(_ gmsMarker: GMSMarker, from markerData: MarkerData) {
210
+ gmsMarker.position = CLLocationCoordinate2D(
211
+ latitude: markerData.coordinate.latitude,
212
+ longitude: markerData.coordinate.longitude
213
+ )
214
+ gmsMarker.title = markerData.title
215
+ gmsMarker.snippet = markerData.description
216
+ gmsMarker.isDraggable = markerData.draggable
217
+ gmsMarker.opacity = Float(markerData.opacity)
218
+ gmsMarker.rotation = markerData.rotation
219
+ gmsMarker.zIndex = Int32(markerData.zIndex)
220
+ gmsMarker.groundAnchor = CGPoint(
221
+ x: markerData.anchor.x,
222
+ y: markerData.anchor.y
223
+ )
224
+ gmsMarker.userData = markerData.id
225
+ gmsMarker.icon = MarkerIconFactory.createIcon(for: markerData)
226
+
227
+ // Accessibility — GMSMarker manages its own isAccessibilityElement
228
+ // internally; forcing it to true throws an ObjC exception in some
229
+ // SDK versions. Setting accessibilityLabel alone is sufficient.
230
+ gmsMarker.accessibilityLabel = markerData.accessibilityLabel ?? markerData.title
231
+
232
+ switch markerData.animation {
233
+ case .appear:
234
+ gmsMarker.appearAnimation = .pop
235
+ case .none:
236
+ gmsMarker.appearAnimation = .none
237
+ }
238
+ }
239
+
240
+ /// Update GMSMarker properties in-place without creating a new marker object
241
+ func updateGMSMarkerProperties(_ gmsMarker: GMSMarker, from markerData: MarkerData) {
242
+ gmsMarker.position = CLLocationCoordinate2D(
243
+ latitude: markerData.coordinate.latitude,
244
+ longitude: markerData.coordinate.longitude
245
+ )
246
+ gmsMarker.title = markerData.title
247
+ gmsMarker.snippet = markerData.description
248
+ gmsMarker.isDraggable = markerData.draggable
249
+ gmsMarker.opacity = Float(markerData.opacity)
250
+ gmsMarker.rotation = markerData.rotation
251
+ gmsMarker.zIndex = Int32(markerData.zIndex)
252
+ gmsMarker.groundAnchor = CGPoint(
253
+ x: markerData.anchor.x,
254
+ y: markerData.anchor.y
255
+ )
256
+
257
+ // Accessibility
258
+ gmsMarker.accessibilityLabel = markerData.accessibilityLabel ?? markerData.title
259
+
260
+ // Only regenerate icon if needed - cache will handle deduplication
261
+ if let newIcon = MarkerIconFactory.createIcon(for: markerData) {
262
+ gmsMarker.icon = newIcon
263
+ }
264
+ }
265
+
266
+ /// Apply the platform's native appear animation to a cluster marker.
267
+ func applyClusterAnimation(_ marker: GMSMarker) {
268
+ marker.appearAnimation = .pop
269
+ }
270
+ }
@@ -0,0 +1,245 @@
1
+ import Foundation
2
+ import GoogleMaps
3
+
4
+ // MARK: - Overlay Management (SRP: Polyline, Polygon, Circle only)
5
+
6
+ extension GoogleMapProvider {
7
+
8
+ // MARK: - Polyline Management
9
+
10
+ func addPolyline(_ polyline: PolylineData) {
11
+ // Remove existing polyline with same ID
12
+ if let existing = polylines[polyline.id] {
13
+ existing.map = nil
14
+ polylines.removeValue(forKey: polyline.id)
15
+ }
16
+
17
+ let path = GMSMutablePath()
18
+ for coord in polyline.coordinates {
19
+ path.add(CLLocationCoordinate2D(
20
+ latitude: coord.latitude,
21
+ longitude: coord.longitude
22
+ ))
23
+ }
24
+
25
+ let gmsPolyline = GMSPolyline(path: path)
26
+ gmsPolyline.strokeWidth = CGFloat(polyline.strokeWidth)
27
+ gmsPolyline.geodesic = polyline.geodesic
28
+ gmsPolyline.zIndex = Int32(polyline.zIndex)
29
+
30
+ let color = polyline.strokeColor.toUIColor()
31
+
32
+ if polyline.dashed {
33
+ applyDashSpans(to: gmsPolyline, color: color)
34
+ dashedPolylineData[polyline.id] = polyline
35
+ } else {
36
+ gmsPolyline.strokeColor = color
37
+ dashedPolylineData.removeValue(forKey: polyline.id)
38
+ }
39
+
40
+ gmsPolyline.isTappable = polyline.tappable
41
+ var userData: [String: String] = [GoogleMapDelegate.overlayTypeKey: GoogleMapDelegate.overlayTypePolyline, GoogleMapDelegate.overlayIdKey: polyline.id]
42
+ if let label = polyline.accessibilityLabel { userData["accessibilityLabel"] = label }
43
+ gmsPolyline.userData = userData
44
+ gmsPolyline.map = gmsMapView
45
+ polylines[polyline.id] = gmsPolyline
46
+ }
47
+
48
+ /// Apply dash spans using screen-pixel sizing.
49
+ /// Projects path coordinates to screen points, measures actual screen length,
50
+ /// then creates dashes targeting ~20pt solid / ~10pt gap.
51
+ /// Always starts and ends with a solid span. Bounded to max 100 cycles.
52
+ private func applyDashSpans(to gmsPolyline: GMSPolyline, color: UIColor) {
53
+ guard let path = gmsPolyline.path, path.count() >= 2 else { return }
54
+ let solidStyle = GMSStrokeStyle.solidColor(color)
55
+ let clearStyle = GMSStrokeStyle.solidColor(.clear)
56
+ let pointCount = Int(path.count())
57
+ let totalSegments: Double = Double(pointCount - 1)
58
+
59
+ // --- Step 1: Calculate screen length per segment ---
60
+ var segmentScreenLengths: [Double] = []
61
+ segmentScreenLengths.reserveCapacity(pointCount - 1)
62
+ var totalScreenLength: Double = 0
63
+
64
+ for i in 0..<(pointCount - 1) {
65
+ let p1 = gmsMapView.projection.point(for: path.coordinate(at: UInt(i)))
66
+ let p2 = gmsMapView.projection.point(for: path.coordinate(at: UInt(i + 1)))
67
+ let dx = Double(p2.x - p1.x)
68
+ let dy = Double(p2.y - p1.y)
69
+ let len = sqrt(dx * dx + dy * dy)
70
+ segmentScreenLengths.append(len)
71
+ totalScreenLength += len
72
+ }
73
+
74
+ guard totalScreenLength > 0 else {
75
+ gmsPolyline.spans = [GMSStyleSpan(style: solidStyle)]
76
+ return
77
+ }
78
+
79
+ // --- Step 2: Calculate dash/gap count from screen length ---
80
+ let dashPt: Double = 20.0 // solid dash size in screen points
81
+ let gapPt: Double = 10.0 // gap size in screen points
82
+ let cyclePt: Double = dashPt + gapPt
83
+
84
+ // How many full dash cycles fit? Always at least 1 dash.
85
+ var dashCount = max(Int(totalScreenLength / cyclePt), 1)
86
+ dashCount = min(dashCount, 100) // cap for performance
87
+
88
+ // --- Step 3: Build spans as segment fractions proportional to screen size ---
89
+ // Pattern: solid, gap, solid, gap, ..., solid (2N-1 slots)
90
+ // Each solid = dashPt screen points, each gap = gapPt screen points
91
+ // Total pattern screen length = dashCount * dashPt + (dashCount - 1) * gapPt
92
+ let patternScreenLength = Double(dashCount) * dashPt + Double(dashCount - 1) * gapPt
93
+
94
+ // Convert screen points to segment fractions
95
+ let dashSegments = (dashPt / patternScreenLength) * totalSegments
96
+ let gapSegments = (gapPt / patternScreenLength) * totalSegments
97
+
98
+ var spans: [GMSStyleSpan] = []
99
+ spans.reserveCapacity(dashCount * 2 - 1)
100
+
101
+ for i in 0..<(dashCount * 2 - 1) {
102
+ if i % 2 == 0 {
103
+ spans.append(GMSStyleSpan(style: solidStyle, segments: dashSegments))
104
+ } else {
105
+ spans.append(GMSStyleSpan(style: clearStyle, segments: gapSegments))
106
+ }
107
+ }
108
+ gmsPolyline.spans = spans
109
+ }
110
+
111
+ /// Recalculate dash spans for all dashed polylines at current zoom.
112
+ /// Called from camera idle delegate.
113
+ func refreshDashedPolylines() {
114
+ for (id, data) in dashedPolylineData {
115
+ guard let gmsPolyline = polylines[id] else { continue }
116
+ let color = data.strokeColor.toUIColor()
117
+ applyDashSpans(to: gmsPolyline, color: color)
118
+ }
119
+ }
120
+
121
+ func updatePolyline(_ polyline: PolylineData) {
122
+ // Remove and re-add — GMSPolyline properties like path are not
123
+ // efficiently updatable in-place.
124
+ addPolyline(polyline)
125
+ }
126
+
127
+ func removePolyline(_ id: String) {
128
+ if let existing = polylines[id] {
129
+ existing.map = nil
130
+ polylines.removeValue(forKey: id)
131
+ }
132
+ dashedPolylineData.removeValue(forKey: id)
133
+ }
134
+
135
+ func clearPolylines() {
136
+ for (_, polyline) in polylines {
137
+ polyline.map = nil
138
+ }
139
+ polylines.removeAll()
140
+ dashedPolylineData.removeAll()
141
+ }
142
+
143
+ // MARK: - Polygon Management
144
+
145
+ func addPolygon(_ polygon: PolygonData) {
146
+ // Remove existing polygon with same ID
147
+ if let existing = polygons[polygon.id] {
148
+ existing.map = nil
149
+ }
150
+
151
+ let path = GMSMutablePath()
152
+ for coord in polygon.coordinates {
153
+ path.add(CLLocationCoordinate2D(latitude: coord.latitude, longitude: coord.longitude))
154
+ }
155
+
156
+ let gmsPolygon = GMSPolygon(path: path)
157
+
158
+ gmsPolygon.fillColor = polygon.fillColor.toUIColor()
159
+ gmsPolygon.strokeColor = polygon.strokeColor.toUIColor()
160
+
161
+ gmsPolygon.strokeWidth = CGFloat(polygon.strokeWidth)
162
+ gmsPolygon.zIndex = Int32(polygon.zIndex)
163
+
164
+ // Add holes
165
+ if !polygon.holes.isEmpty {
166
+ var holePaths: [GMSPath] = []
167
+ for ring in polygon.holes {
168
+ let holePath = GMSMutablePath()
169
+ for coord in ring.coordinates {
170
+ holePath.add(CLLocationCoordinate2D(latitude: coord.latitude, longitude: coord.longitude))
171
+ }
172
+ holePaths.append(holePath)
173
+ }
174
+ gmsPolygon.holes = holePaths
175
+ }
176
+
177
+ gmsPolygon.isTappable = polygon.tappable
178
+ var polygonUserData: [String: String] = [GoogleMapDelegate.overlayTypeKey: GoogleMapDelegate.overlayTypePolygon, GoogleMapDelegate.overlayIdKey: polygon.id]
179
+ if let label = polygon.accessibilityLabel { polygonUserData["accessibilityLabel"] = label }
180
+ gmsPolygon.userData = polygonUserData
181
+ gmsPolygon.map = gmsMapView
182
+ polygons[polygon.id] = gmsPolygon
183
+ }
184
+
185
+ func updatePolygon(_ polygon: PolygonData) {
186
+ // Remove and re-add
187
+ addPolygon(polygon)
188
+ }
189
+
190
+ func removePolygon(_ id: String) {
191
+ if let existing = polygons[id] {
192
+ existing.map = nil
193
+ polygons.removeValue(forKey: id)
194
+ }
195
+ }
196
+
197
+ func clearPolygons() {
198
+ for (_, polygon) in polygons {
199
+ polygon.map = nil
200
+ }
201
+ polygons.removeAll()
202
+ }
203
+
204
+ // MARK: - Circle Management
205
+
206
+ func addCircle(_ circle: CircleData) {
207
+ // Remove existing circle with same ID
208
+ if let existing = circles[circle.id] {
209
+ existing.map = nil
210
+ }
211
+
212
+ let center = CLLocationCoordinate2D(latitude: circle.center.latitude, longitude: circle.center.longitude)
213
+ let gmsCircle = GMSCircle(position: center, radius: circle.radius)
214
+
215
+ gmsCircle.fillColor = circle.fillColor.toUIColor()
216
+ gmsCircle.strokeColor = circle.strokeColor.toUIColor()
217
+
218
+ gmsCircle.strokeWidth = CGFloat(circle.strokeWidth)
219
+ gmsCircle.zIndex = Int32(circle.zIndex)
220
+ gmsCircle.isTappable = circle.tappable
221
+ var circleUserData: [String: String] = [GoogleMapDelegate.overlayTypeKey: GoogleMapDelegate.overlayTypeCircle, GoogleMapDelegate.overlayIdKey: circle.id]
222
+ if let label = circle.accessibilityLabel { circleUserData["accessibilityLabel"] = label }
223
+ gmsCircle.userData = circleUserData
224
+ gmsCircle.map = gmsMapView
225
+ circles[circle.id] = gmsCircle
226
+ }
227
+
228
+ func updateCircle(_ circle: CircleData) {
229
+ addCircle(circle)
230
+ }
231
+
232
+ func removeCircle(_ id: String) {
233
+ if let existing = circles[id] {
234
+ existing.map = nil
235
+ circles.removeValue(forKey: id)
236
+ }
237
+ }
238
+
239
+ func clearCircles() {
240
+ for (_, circle) in circles {
241
+ circle.map = nil
242
+ }
243
+ circles.removeAll()
244
+ }
245
+ }
@@ -0,0 +1,180 @@
1
+ import Foundation
2
+ import GoogleMaps
3
+ import UIKit
4
+ import CoreLocation
5
+
6
+ // MARK: - User Location
7
+
8
+ extension GoogleMapProvider {
9
+
10
+ /// Called by facade when CLLocationManager fires a location/heading update.
11
+ /// Updates custom marker position + rotation and handles camera following.
12
+ func updateUserLocation(_ location: CLLocation, heading: CLHeading?) {
13
+ // Store latest location for my-location button
14
+ lastKnownUserLocation = location
15
+
16
+ let marker = customUserLocationMarker
17
+
18
+ // Compute rotation from heading
19
+ let rotation: CLLocationDirection? = {
20
+ if let heading = heading, heading.trueHeading >= 0 {
21
+ return heading.trueHeading
22
+ } else if location.course >= 0 {
23
+ return location.course
24
+ }
25
+ return nil
26
+ }()
27
+
28
+ // Determine if camera should follow
29
+ let shouldFollow = userTrackingMode != nil && userTrackingMode != .none
30
+
31
+ if shouldFollow {
32
+ // Synchronize marker position + camera in one animation transaction.
33
+ // Without this, marker.position jumps instantly while camera animates,
34
+ // causing a visual "teleport forward, then back" artifact.
35
+ CATransaction.begin()
36
+ CATransaction.setAnimationDuration(0.3)
37
+
38
+ // Update marker within the same animation
39
+ if let marker = marker {
40
+ marker.position = location.coordinate
41
+ if let rotation = rotation {
42
+ marker.rotation = rotation
43
+ }
44
+ }
45
+
46
+ // Camera follows — animate in the same transaction
47
+ let target = location.coordinate
48
+ switch userTrackingMode! {
49
+ case .follow:
50
+ let camera = GMSCameraPosition.camera(
51
+ withTarget: target,
52
+ zoom: gmsMapView.camera.zoom
53
+ )
54
+ gmsMapView.animate(to: camera)
55
+ case .followwithheading:
56
+ let bearing: CLLocationDirection = {
57
+ if let heading = heading, heading.trueHeading >= 0 {
58
+ return heading.trueHeading
59
+ } else if location.course >= 0 {
60
+ return location.course
61
+ }
62
+ return gmsMapView.camera.bearing
63
+ }()
64
+ let camera = GMSCameraPosition.camera(
65
+ withTarget: target,
66
+ zoom: gmsMapView.camera.zoom,
67
+ bearing: bearing,
68
+ viewingAngle: gmsMapView.camera.viewingAngle
69
+ )
70
+ gmsMapView.animate(to: camera)
71
+ case .none:
72
+ break
73
+ }
74
+
75
+ CATransaction.commit()
76
+ } else {
77
+ // No camera follow — just update marker position instantly
78
+ if let marker = marker {
79
+ marker.position = location.coordinate
80
+ if let rotation = rotation {
81
+ marker.rotation = rotation
82
+ }
83
+ }
84
+ }
85
+ }
86
+
87
+ // MARK: - Custom User Location Marker
88
+
89
+ func setupCustomUserLocationMarker() {
90
+ // Remove old custom marker
91
+ customUserLocationMarker?.map = nil
92
+ customUserLocationMarker = nil
93
+
94
+ guard !userLocationImage.isEmpty else {
95
+ // No custom image — re-enable default blue dot
96
+ cachedUserLocationImageUrl = ""
97
+ userLocationImageData = nil
98
+ gmsMapView.isMyLocationEnabled = showsUserLocation ?? false
99
+ return
100
+ }
101
+
102
+ // Disable native blue dot — CLLocationManager handles tracking independently
103
+ gmsMapView.isMyLocationEnabled = false
104
+
105
+ // Create custom marker
106
+ let marker = GMSMarker()
107
+ marker.groundAnchor = CGPoint(
108
+ x: userLocationAnchor?.x ?? 0.5,
109
+ y: userLocationAnchor?.y ?? 0.5
110
+ )
111
+ marker.isFlat = true
112
+ marker.zIndex = Int32.max // Always on top
113
+
114
+ // Reuse cached image if URL is the same (only size changed, or marker recreated)
115
+ if let cachedImage = userLocationImageData, cachedUserLocationImageUrl == userLocationImage {
116
+ let size = CGFloat(userLocationSize)
117
+ let resized = resizeImage(cachedImage, to: CGSize(width: size, height: size))
118
+ marker.icon = resized
119
+ }
120
+
121
+ marker.map = gmsMapView
122
+ customUserLocationMarker = marker
123
+
124
+ // Restore position from last known location
125
+ if let location = lastKnownUserLocation {
126
+ marker.position = location.coordinate
127
+ }
128
+
129
+ // Skip download if same URL and we already have cached data
130
+ if cachedUserLocationImageUrl == userLocationImage && userLocationImageData != nil {
131
+ return
132
+ }
133
+
134
+ // Load image
135
+ let size = CGFloat(userLocationSize)
136
+ if userLocationImage.hasPrefix("http://") || userLocationImage.hasPrefix("https://") {
137
+ // Remote image
138
+ let imageUrl = userLocationImage
139
+ cachedUserLocationImageUrl = imageUrl
140
+ DispatchQueue.global().async { [weak self] in
141
+ guard let url = URL(string: imageUrl),
142
+ let data = try? Data(contentsOf: url),
143
+ let image = UIImage(data: data) else { return }
144
+ let resized = self?.resizeImage(image, to: CGSize(width: size, height: size))
145
+ DispatchQueue.main.async { [weak self] in
146
+ self?.userLocationImageData = image // Cache original, not resized
147
+ self?.customUserLocationMarker?.icon = resized
148
+ }
149
+ }
150
+ } else {
151
+ // Local asset
152
+ cachedUserLocationImageUrl = userLocationImage
153
+ if let image = UIImage(named: userLocationImage) {
154
+ let resized = resizeImage(image, to: CGSize(width: size, height: size))
155
+ userLocationImageData = image // Cache original
156
+ marker.icon = resized
157
+ }
158
+ }
159
+ }
160
+
161
+ func resizeImage(_ image: UIImage, to size: CGSize) -> UIImage {
162
+ let renderer = UIGraphicsImageRenderer(size: size)
163
+ return renderer.image { _ in
164
+ image.draw(in: CGRect(origin: .zero, size: size))
165
+ }
166
+ }
167
+
168
+ func centerOnUserLocation() {
169
+ guard let location = lastKnownUserLocation else { return }
170
+ let camera = GMSCameraPosition.camera(
171
+ withLatitude: location.coordinate.latitude,
172
+ longitude: location.coordinate.longitude,
173
+ zoom: gmsMapView.camera.zoom
174
+ )
175
+ CATransaction.begin()
176
+ CATransaction.setAnimationDuration(0.3)
177
+ gmsMapView.animate(to: camera)
178
+ CATransaction.commit()
179
+ }
180
+ }