@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.
- package/LICENSE +1 -1
- package/NitroMap.podspec +1 -1
- package/README.md +82 -9
- package/android/CMakeLists.txt +4 -1
- package/android/build.gradle +6 -2
- package/android/gradle.properties +4 -4
- package/android/src/main/cpp/ClusterEngineJNI.cpp +198 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/NitroMap.kt +397 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/NitroMapConfig.kt +53 -0
- package/android/src/main/{java → kotlin}/com/margelo/nitro/nitromap/NitroMapPackage.kt +4 -4
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/NitroMapView.kt +73 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/UserLocationManager.kt +295 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/clustering/ClusterIconRenderer.kt +111 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/clustering/ClusteringManager.kt +104 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/clustering/NitroClusterEngine.kt +166 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/markers/MarkerIconFactory.kt +303 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/markers/MarkerSelectionHandler.kt +72 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/markers/PriceMarkerRenderer.kt +159 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/MapProviderFactory.kt +24 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/MapProviderInterface.kt +128 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/google/GoogleMapDelegate.kt +317 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/google/GoogleMapProvider+Clustering.kt +524 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/google/GoogleMapProvider+Markers.kt +358 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/google/GoogleMapProvider+Overlays.kt +272 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/google/GoogleMapProvider+UserLocation.kt +296 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/google/GoogleMapProvider.kt +815 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/google/MarkerTagData.kt +19 -0
- package/ios/Location/NitroLocationManager.swift +116 -0
- package/ios/MarkerRenderer/MarkerIconFactory.swift +1 -3
- package/ios/MarkerRenderer/PriceMarkerRenderer.swift +10 -6
- package/ios/NitroMap.swift +279 -13
- package/ios/NitroMapConfig/NitroMapConfig.swift +45 -0
- package/ios/Providers/{GoogleMapDelegate.swift → Google/GoogleMapDelegate.swift} +48 -23
- package/ios/Providers/Google/GoogleMapProvider+Camera.swift +180 -0
- package/ios/Providers/Google/GoogleMapProvider+Clustering.swift +541 -0
- package/ios/Providers/Google/GoogleMapProvider+Markers.swift +270 -0
- package/ios/Providers/Google/GoogleMapProvider+Overlays.swift +245 -0
- package/ios/Providers/Google/GoogleMapProvider+UserLocation.swift +180 -0
- package/ios/Providers/Google/GoogleMapProvider.swift +342 -0
- package/ios/Providers/MapProviderFactory.swift +17 -0
- package/ios/Providers/MapProviderProtocol.swift +48 -1
- package/ios/Shared/ClusterConfig+Factory.swift +2 -2
- package/ios/Shared/MapStyleProvider.swift +6 -4
- package/ios/Shared/MarkerSelectionHandler.swift +4 -1
- package/ios/Utils/ColorValueExtension.swift +46 -67
- package/lib/module/components/ImageMarker.js +39 -29
- package/lib/module/components/ImageMarker.js.map +1 -1
- package/lib/module/components/Marker.js +118 -0
- package/lib/module/components/Marker.js.map +1 -0
- package/lib/module/components/NitroCircle.js +92 -0
- package/lib/module/components/NitroCircle.js.map +1 -0
- package/lib/module/components/NitroMap.js +216 -76
- package/lib/module/components/NitroMap.js.map +1 -1
- package/lib/module/components/NitroPolygon.js +137 -0
- package/lib/module/components/NitroPolygon.js.map +1 -0
- package/lib/module/components/NitroPolyline.js +115 -0
- package/lib/module/components/NitroPolyline.js.map +1 -0
- package/lib/module/components/PriceMarker.js +16 -29
- package/lib/module/components/PriceMarker.js.map +1 -1
- package/lib/module/context/NitroMapContext.js.map +1 -1
- package/lib/module/hooks/useNitroCircle.js +18 -0
- package/lib/module/hooks/useNitroCircle.js.map +1 -0
- package/lib/module/hooks/useNitroMarker.js +26 -9
- package/lib/module/hooks/useNitroMarker.js.map +1 -1
- package/lib/module/hooks/useNitroOverlay.js +59 -0
- package/lib/module/hooks/useNitroOverlay.js.map +1 -0
- package/lib/module/hooks/useNitroPolygon.js +18 -0
- package/lib/module/hooks/useNitroPolygon.js.map +1 -0
- package/lib/module/hooks/useNitroPolyline.js +18 -0
- package/lib/module/hooks/useNitroPolyline.js.map +1 -0
- package/lib/module/index.js +5 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/types/overlay.js +4 -0
- package/lib/module/types/overlay.js.map +1 -0
- package/lib/module/types/theme.js +4 -0
- package/lib/module/types/theme.js.map +1 -0
- package/lib/module/utils/colors.js +41 -13
- package/lib/module/utils/colors.js.map +1 -1
- package/lib/module/utils/validation.js +45 -0
- package/lib/module/utils/validation.js.map +1 -0
- package/lib/typescript/src/components/ImageMarker.d.ts.map +1 -1
- package/lib/typescript/src/components/Marker.d.ts +34 -0
- package/lib/typescript/src/components/Marker.d.ts.map +1 -0
- package/lib/typescript/src/components/NitroCircle.d.ts +70 -0
- package/lib/typescript/src/components/NitroCircle.d.ts.map +1 -0
- package/lib/typescript/src/components/NitroMap.d.ts +60 -3
- package/lib/typescript/src/components/NitroMap.d.ts.map +1 -1
- package/lib/typescript/src/components/NitroPolygon.d.ts +86 -0
- package/lib/typescript/src/components/NitroPolygon.d.ts.map +1 -0
- package/lib/typescript/src/components/NitroPolyline.d.ts +84 -0
- package/lib/typescript/src/components/NitroPolyline.d.ts.map +1 -0
- package/lib/typescript/src/components/PriceMarker.d.ts +0 -5
- package/lib/typescript/src/components/PriceMarker.d.ts.map +1 -1
- package/lib/typescript/src/context/NitroMapContext.d.ts +2 -0
- package/lib/typescript/src/context/NitroMapContext.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useNitroCircle.d.ts +7 -0
- package/lib/typescript/src/hooks/useNitroCircle.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useNitroMarker.d.ts +20 -0
- package/lib/typescript/src/hooks/useNitroMarker.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useNitroOverlay.d.ts +26 -0
- package/lib/typescript/src/hooks/useNitroOverlay.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useNitroPolygon.d.ts +7 -0
- package/lib/typescript/src/hooks/useNitroPolygon.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useNitroPolyline.d.ts +7 -0
- package/lib/typescript/src/hooks/useNitroPolyline.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +15 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/specs/NitroMap.nitro.d.ts +248 -6
- package/lib/typescript/src/specs/NitroMap.nitro.d.ts.map +1 -1
- package/lib/typescript/src/types/map.d.ts +34 -4
- package/lib/typescript/src/types/map.d.ts.map +1 -1
- package/lib/typescript/src/types/marker.d.ts +24 -36
- package/lib/typescript/src/types/marker.d.ts.map +1 -1
- package/lib/typescript/src/types/overlay.d.ts +84 -0
- package/lib/typescript/src/types/overlay.d.ts.map +1 -0
- package/lib/typescript/src/types/theme.d.ts +93 -0
- package/lib/typescript/src/types/theme.d.ts.map +1 -0
- package/lib/typescript/src/utils/colors.d.ts +6 -8
- package/lib/typescript/src/utils/colors.d.ts.map +1 -1
- package/lib/typescript/src/utils/validation.d.ts +12 -0
- package/lib/typescript/src/utils/validation.d.ts.map +1 -0
- package/nitrogen/generated/android/c++/JCircleData.hpp +94 -0
- package/nitrogen/generated/android/c++/JClusterConfig.hpp +5 -7
- package/nitrogen/generated/android/c++/JCoordinateRing.hpp +77 -0
- package/nitrogen/generated/android/c++/JFunc_void_UserLocationChangeEvent.hpp +79 -0
- package/nitrogen/generated/android/c++/JFunc_void_UserTrackingMode.hpp +77 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__string.hpp +76 -0
- package/nitrogen/generated/android/c++/JHybridNitroMapSpec.cpp +332 -21
- package/nitrogen/generated/android/c++/JHybridNitroMapSpec.hpp +53 -2
- package/nitrogen/generated/android/c++/JMarkerAnimation.hpp +3 -6
- package/nitrogen/generated/android/c++/JMarkerData.hpp +15 -3
- package/nitrogen/generated/android/c++/JPolygonData.hpp +133 -0
- package/nitrogen/generated/android/c++/JPolylineData.hpp +113 -0
- package/nitrogen/generated/android/c++/JUserLocationChangeEvent.hpp +70 -0
- package/nitrogen/generated/android/c++/JUserTrackingMode.hpp +62 -0
- package/nitrogen/generated/android/c++/views/JHybridNitroMapStateUpdater.cpp +72 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/CircleData.kt +62 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/ClusterConfig.kt +4 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/CoordinateRing.kt +38 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/Func_void_UserLocationChangeEvent.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/Func_void_UserTrackingMode.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/Func_void_std__string.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/HybridNitroMapSpec.kt +228 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/MarkerAnimation.kt +1 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/MarkerData.kt +12 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/PolygonData.kt +62 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/PolylineData.kt +62 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/UserLocationChangeEvent.kt +47 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/{ClusterAnimationStyle.kt → UserTrackingMode.kt} +6 -8
- package/nitrogen/generated/android/nitromapOnLoad.cpp +6 -0
- package/nitrogen/generated/ios/NitroMap-Swift-Cxx-Bridge.cpp +24 -0
- package/nitrogen/generated/ios/NitroMap-Swift-Cxx-Bridge.hpp +178 -17
- package/nitrogen/generated/ios/NitroMap-Swift-Cxx-Umbrella.hpp +18 -3
- package/nitrogen/generated/ios/c++/HybridNitroMapSpecSwift.hpp +252 -16
- package/nitrogen/generated/ios/c++/views/HybridNitroMapComponent.mm +90 -5
- package/nitrogen/generated/ios/swift/CircleData.swift +143 -0
- package/nitrogen/generated/ios/swift/ClusterConfig.swift +22 -15
- package/nitrogen/generated/ios/swift/CoordinateRing.swift +48 -0
- package/nitrogen/generated/ios/swift/Func_void_UserLocationChangeEvent.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_UserTrackingMode.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string.swift +47 -0
- package/nitrogen/generated/ios/swift/HybridNitroMapSpec.swift +35 -1
- package/nitrogen/generated/ios/swift/HybridNitroMapSpec_cxx.swift +582 -8
- package/nitrogen/generated/ios/swift/MarkerAnimation.swift +4 -8
- package/nitrogen/generated/ios/swift/MarkerData.swift +54 -2
- package/nitrogen/generated/ios/swift/PolygonData.swift +167 -0
- package/nitrogen/generated/ios/swift/PolylineData.swift +155 -0
- package/nitrogen/generated/ios/swift/UserLocationChangeEvent.swift +69 -0
- package/nitrogen/generated/ios/swift/UserTrackingMode.swift +44 -0
- package/nitrogen/generated/shared/c++/CircleData.hpp +113 -0
- package/nitrogen/generated/shared/c++/ClusterConfig.hpp +5 -8
- package/nitrogen/generated/shared/c++/CoordinateRing.hpp +77 -0
- package/nitrogen/generated/shared/c++/HybridNitroMapSpec.cpp +53 -2
- package/nitrogen/generated/shared/c++/HybridNitroMapSpec.hpp +75 -6
- package/nitrogen/generated/shared/c++/MarkerAnimation.hpp +4 -8
- package/nitrogen/generated/shared/c++/MarkerData.hpp +14 -2
- package/nitrogen/generated/shared/c++/PolygonData.hpp +117 -0
- package/nitrogen/generated/shared/c++/PolylineData.hpp +114 -0
- package/nitrogen/generated/shared/c++/UserLocationChangeEvent.hpp +88 -0
- package/nitrogen/generated/shared/c++/UserTrackingMode.hpp +80 -0
- package/nitrogen/generated/shared/c++/views/HybridNitroMapComponent.cpp +216 -12
- package/nitrogen/generated/shared/c++/views/HybridNitroMapComponent.hpp +23 -1
- package/nitrogen/generated/shared/json/NitroMapConfig.json +18 -1
- package/package.json +36 -5
- package/src/components/ImageMarker.tsx +58 -42
- package/src/components/Marker.tsx +161 -0
- package/src/components/NitroCircle.tsx +183 -0
- package/src/components/NitroMap.tsx +328 -78
- package/src/components/NitroPolygon.tsx +229 -0
- package/src/components/NitroPolyline.tsx +208 -0
- package/src/components/PriceMarker.tsx +23 -48
- package/src/context/NitroMapContext.tsx +4 -0
- package/src/hooks/useNitroCircle.ts +25 -0
- package/src/hooks/useNitroMarker.ts +49 -10
- package/src/hooks/useNitroOverlay.ts +68 -0
- package/src/hooks/useNitroPolygon.ts +25 -0
- package/src/hooks/useNitroPolyline.ts +25 -0
- package/src/index.tsx +28 -2
- package/src/specs/NitroMap.nitro.ts +294 -5
- package/src/types/map.ts +36 -4
- package/src/types/marker.ts +24 -44
- package/src/types/overlay.ts +87 -0
- package/src/types/theme.ts +101 -0
- package/src/utils/colors.ts +48 -16
- package/src/utils/validation.ts +69 -0
- package/android/src/main/java/com/margelo/nitro/nitromap/ClusterIconGenerator.kt +0 -108
- package/android/src/main/java/com/margelo/nitro/nitromap/ColorUtils.kt +0 -63
- package/android/src/main/java/com/margelo/nitro/nitromap/HybridNitroMap.kt +0 -408
- package/android/src/main/java/com/margelo/nitro/nitromap/HybridNitroMapConfig.kt +0 -68
- package/android/src/main/java/com/margelo/nitro/nitromap/MarkerIconCache.kt +0 -176
- package/android/src/main/java/com/margelo/nitro/nitromap/MarkerIconFactory.kt +0 -252
- package/android/src/main/java/com/margelo/nitro/nitromap/clustering/NitroClusterEngine.kt +0 -252
- package/android/src/main/java/com/margelo/nitro/nitromap/clustering/QuadTree.kt +0 -195
- package/android/src/main/java/com/margelo/nitro/nitromap/providers/GoogleMapProvider.kt +0 -912
- package/android/src/main/java/com/margelo/nitro/nitromap/providers/MapProviderInterface.kt +0 -70
- package/cpp/QuadTree.hpp +0 -246
- package/ios/NitroMapConfig/HybridNitroMapConfig.swift +0 -33
- package/ios/Providers/GoogleMapProvider+Camera.swift +0 -164
- package/ios/Providers/GoogleMapProvider.swift +0 -924
- package/nitrogen/generated/android/c++/JClusterAnimationStyle.hpp +0 -68
- package/nitrogen/generated/ios/swift/ClusterAnimationStyle.swift +0 -52
- 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
|
+
}
|