@maydon_tech/react-native-nitro-maps 0.1.4 → 0.2.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 +1 -1
- package/NitroMap.podspec +1 -1
- package/README.md +82 -9
- package/android/CMakeLists.txt +4 -1
- 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 +135 -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 +75 -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++/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 +328 -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 +149 -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/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 +175 -17
- package/nitrogen/generated/ios/NitroMap-Swift-Cxx-Umbrella.hpp +15 -3
- package/nitrogen/generated/ios/c++/HybridNitroMapSpecSwift.hpp +249 -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/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 +179 -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++/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 +114 -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 +23 -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 +77 -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,524 @@
|
|
|
1
|
+
package com.margelo.nitro.nitromap.providers.google
|
|
2
|
+
|
|
3
|
+
import android.os.Handler
|
|
4
|
+
import android.os.Looper
|
|
5
|
+
import android.util.Log
|
|
6
|
+
import com.google.android.gms.maps.model.BitmapDescriptorFactory
|
|
7
|
+
import com.google.android.gms.maps.model.LatLng
|
|
8
|
+
import com.google.android.gms.maps.model.LatLngBounds
|
|
9
|
+
import com.google.android.gms.maps.model.Marker
|
|
10
|
+
import com.google.android.gms.maps.model.MarkerOptions
|
|
11
|
+
import com.margelo.nitro.nitromap.*
|
|
12
|
+
import com.margelo.nitro.nitromap.clustering.ClusteringManager
|
|
13
|
+
import com.margelo.nitro.nitromap.clustering.NitroClusterEngine
|
|
14
|
+
import kotlin.math.*
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Clustering pipeline for GoogleMapProvider.
|
|
18
|
+
* Mirrors iOS GoogleMapProvider+Clustering.swift (~445 lines).
|
|
19
|
+
*
|
|
20
|
+
* Pipeline:
|
|
21
|
+
* 1. Query visible region + renderBuffer expansion
|
|
22
|
+
* 2. Call ClusteringManager.cluster() → C++ Supercluster result
|
|
23
|
+
* 3. Build render items (clusters + singles)
|
|
24
|
+
* 4. Apply strategy (supercluster vs hideOnOverlap)
|
|
25
|
+
* 5. Render new markers BEFORE removing old ones (prevents flicker)
|
|
26
|
+
* 6. Handle selected marker exclusion from clusters
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// MARK: - Render Item (internal to clustering pipeline)
|
|
31
|
+
// ============================================================================
|
|
32
|
+
|
|
33
|
+
private data class RenderItem(
|
|
34
|
+
val latitude: Double,
|
|
35
|
+
val longitude: Double,
|
|
36
|
+
val kind: RenderKind,
|
|
37
|
+
val iconWidth: Double,
|
|
38
|
+
val iconHeight: Double,
|
|
39
|
+
val priority: Int // Higher = more important (clusters > singles)
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
private sealed class RenderKind {
|
|
43
|
+
data class Cluster(
|
|
44
|
+
val markerIds: List<String>,
|
|
45
|
+
val count: Int,
|
|
46
|
+
val iconSize: Double
|
|
47
|
+
) : RenderKind()
|
|
48
|
+
|
|
49
|
+
data class Single(val markerId: String) : RenderKind()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ============================================================================
|
|
53
|
+
// MARK: - Clustering Pipeline
|
|
54
|
+
// ============================================================================
|
|
55
|
+
|
|
56
|
+
/** Propagate config to ClusteringManager + re-cluster (mirrors iOS updateClusterConfig) */
|
|
57
|
+
fun GoogleMapProvider.updateClusterConfig() {
|
|
58
|
+
clusteringManager?.clusterConfig = clusterConfig
|
|
59
|
+
performClustering()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Debounced clustering — fires after 100ms of silence */
|
|
63
|
+
fun GoogleMapProvider.performClustering() {
|
|
64
|
+
clusteringManager?.debounce {
|
|
65
|
+
performClusteringImmediate()
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Throttled clustering — fires at most once per interval during gestures */
|
|
70
|
+
fun GoogleMapProvider.performClusteringThrottled() {
|
|
71
|
+
if (throttleActive) return
|
|
72
|
+
|
|
73
|
+
performClusteringImmediate()
|
|
74
|
+
|
|
75
|
+
val intervalMs = (clusterConfig?.throttleInterval ?: 150.0).toLong()
|
|
76
|
+
throttleActive = true
|
|
77
|
+
Handler(Looper.getMainLooper()).postDelayed({
|
|
78
|
+
throttleActive = false
|
|
79
|
+
}, intervalMs)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Full clustering pipeline */
|
|
83
|
+
fun GoogleMapProvider.performClusteringImmediate() {
|
|
84
|
+
val map = googleMap ?: return
|
|
85
|
+
val manager = clusteringManager ?: return
|
|
86
|
+
val mapView = currentMapView ?: return
|
|
87
|
+
|
|
88
|
+
val width = mapView.width.toDouble()
|
|
89
|
+
val height = mapView.height.toDouble()
|
|
90
|
+
if (width <= 0 || height <= 0) return
|
|
91
|
+
|
|
92
|
+
val enabled = clusterConfig?.enabled ?: true
|
|
93
|
+
if (!enabled) {
|
|
94
|
+
clearRenderedClusterMarkers()
|
|
95
|
+
renderAllMarkersIndividually()
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// --- Phase 1: Query region with renderBuffer expansion ---
|
|
100
|
+
val visibleRegion = map.projection.visibleRegion
|
|
101
|
+
val bounds = LatLngBounds.builder()
|
|
102
|
+
.include(visibleRegion.nearLeft)
|
|
103
|
+
.include(visibleRegion.nearRight)
|
|
104
|
+
.include(visibleRegion.farLeft)
|
|
105
|
+
.include(visibleRegion.farRight)
|
|
106
|
+
.build()
|
|
107
|
+
|
|
108
|
+
val zoom = map.cameraPosition.zoom.toDouble()
|
|
109
|
+
val padding = clusterConfig?.renderBuffer ?: 0.0
|
|
110
|
+
val latSpan = bounds.northeast.latitude - bounds.southwest.latitude
|
|
111
|
+
val lonSpan = bounds.northeast.longitude - bounds.southwest.longitude
|
|
112
|
+
|
|
113
|
+
val queryMinLat = bounds.southwest.latitude - latSpan * padding
|
|
114
|
+
val queryMaxLat = bounds.northeast.latitude + latSpan * padding
|
|
115
|
+
val queryMinLon = bounds.southwest.longitude - lonSpan * padding
|
|
116
|
+
val queryMaxLon = bounds.northeast.longitude + lonSpan * padding
|
|
117
|
+
|
|
118
|
+
val result = manager.cluster(queryMinLat, queryMaxLat, queryMinLon, queryMaxLon, zoom, width, height)
|
|
119
|
+
val minClusterSize = (clusterConfig?.minimumClusterSize ?: 2.0).toInt()
|
|
120
|
+
val strategy = clusterConfig?.strategy ?: ClusterStrategy.SUPERCLUSTER
|
|
121
|
+
|
|
122
|
+
// Build the actual visible polygon from the 4 corners of the rotated viewport.
|
|
123
|
+
// The C++ engine returns markers within the axis-aligned bounding box, which is
|
|
124
|
+
// larger than the actual viewport when the map is rotated (bearing ≠ 0).
|
|
125
|
+
// We filter results against this polygon to ensure markers outside the rotated
|
|
126
|
+
// viewport are properly excluded.
|
|
127
|
+
val bearing = map.cameraPosition.bearing
|
|
128
|
+
val isRotated = bearing % 360.0 != 0.0
|
|
129
|
+
|
|
130
|
+
// Expand the visible polygon outward by renderBuffer so that markers near the
|
|
131
|
+
// screen edge (whose icons overlap onto the visible area) are not clipped early.
|
|
132
|
+
val visiblePolygon = if (isRotated) {
|
|
133
|
+
expandPolygon(
|
|
134
|
+
listOf(
|
|
135
|
+
visibleRegion.nearLeft,
|
|
136
|
+
visibleRegion.nearRight,
|
|
137
|
+
visibleRegion.farRight,
|
|
138
|
+
visibleRegion.farLeft
|
|
139
|
+
),
|
|
140
|
+
padding
|
|
141
|
+
)
|
|
142
|
+
} else null // No polygon filter needed at 0° — axis-aligned bounds are exact
|
|
143
|
+
|
|
144
|
+
// --- Phase 2: Build renderable items ---
|
|
145
|
+
val renderItems = mutableListOf<RenderItem>()
|
|
146
|
+
|
|
147
|
+
for (cluster in result.clusters) {
|
|
148
|
+
// When rotated, filter out clusters outside the actual visible polygon
|
|
149
|
+
if (visiblePolygon != null && !pointInConvexQuad(cluster.latitude, cluster.longitude, visiblePolygon)) continue
|
|
150
|
+
|
|
151
|
+
val containsSelected = selectedMarkerId != null && cluster.markerIds.contains(selectedMarkerId)
|
|
152
|
+
val adjustedCount = if (containsSelected) cluster.count - 1 else cluster.count
|
|
153
|
+
if (adjustedCount < minClusterSize) continue
|
|
154
|
+
|
|
155
|
+
val adjustedIds = if (containsSelected) cluster.markerIds.filter { it != selectedMarkerId } else cluster.markerIds
|
|
156
|
+
val iconDiameter = cluster.iconSize
|
|
157
|
+
|
|
158
|
+
renderItems.add(RenderItem(
|
|
159
|
+
latitude = cluster.latitude,
|
|
160
|
+
longitude = cluster.longitude,
|
|
161
|
+
kind = RenderKind.Cluster(adjustedIds, adjustedCount, iconDiameter),
|
|
162
|
+
iconWidth = iconDiameter,
|
|
163
|
+
iconHeight = iconDiameter,
|
|
164
|
+
priority = adjustedCount + 10000
|
|
165
|
+
))
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// L-3: Use density-aware marker dimensions instead of hardcoded 27x43
|
|
169
|
+
val density = context.resources.displayMetrics.density
|
|
170
|
+
val defaultIconW = 27.0 * density // Google Maps default marker width in px
|
|
171
|
+
val defaultIconH = 43.0 * density // Google Maps default marker height in px
|
|
172
|
+
|
|
173
|
+
for (single in result.singleMarkers) {
|
|
174
|
+
// When rotated, filter out singles outside the actual visible polygon
|
|
175
|
+
if (visiblePolygon != null && !pointInConvexQuad(single.latitude, single.longitude, visiblePolygon)) continue
|
|
176
|
+
|
|
177
|
+
if (single.markerId == selectedMarkerId) continue
|
|
178
|
+
if (nonClusteredMarkers.containsKey(single.markerId)) continue
|
|
179
|
+
// H-3: Use markerData (single source of truth) instead of clusterableMarkerData
|
|
180
|
+
val data = markerData[single.markerId] ?: continue
|
|
181
|
+
if (!data.clusteringEnabled) continue // Non-clusterable markers rendered directly
|
|
182
|
+
|
|
183
|
+
renderItems.add(RenderItem(
|
|
184
|
+
latitude = single.latitude,
|
|
185
|
+
longitude = single.longitude,
|
|
186
|
+
kind = RenderKind.Single(single.markerId),
|
|
187
|
+
iconWidth = defaultIconW,
|
|
188
|
+
iconHeight = defaultIconH,
|
|
189
|
+
priority = 0
|
|
190
|
+
))
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// --- Phase 2b: Apply strategy ---
|
|
194
|
+
val visibleItems = if (strategy == ClusterStrategy.HIDEONOVERLAP) {
|
|
195
|
+
resolveOverlaps(renderItems, zoom, width)
|
|
196
|
+
} else {
|
|
197
|
+
renderItems
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// --- Phase 3: Render — add new BEFORE removing old (prevents flicker) ---
|
|
201
|
+
val isFirstRender = renderedClusterMarkers.isEmpty() && renderedSingleMarkers.isEmpty()
|
|
202
|
+
val oldClusterMarkers = renderedClusterMarkers.toList()
|
|
203
|
+
val oldSingleMarkers = renderedSingleMarkers.toMap()
|
|
204
|
+
renderedClusterMarkers.clear()
|
|
205
|
+
renderedSingleMarkers.clear()
|
|
206
|
+
|
|
207
|
+
// Preserve selected marker
|
|
208
|
+
selectedMarkerId?.let { selId ->
|
|
209
|
+
oldSingleMarkers[selId]?.let { selMarker ->
|
|
210
|
+
renderedSingleMarkers[selId] = selMarker
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Restore hidden non-clustered markers
|
|
215
|
+
for (id in hiddenNonClusteredIds) {
|
|
216
|
+
nonClusteredMarkers[id]?.isVisible = true
|
|
217
|
+
}
|
|
218
|
+
hiddenNonClusteredIds.clear()
|
|
219
|
+
|
|
220
|
+
// Render new items
|
|
221
|
+
for ((renderIndex, item) in visibleItems.withIndex()) {
|
|
222
|
+
when (val kind = item.kind) {
|
|
223
|
+
is RenderKind.Cluster -> {
|
|
224
|
+
val icon = manager.clusterIcon(kind.count)
|
|
225
|
+
val opts = MarkerOptions()
|
|
226
|
+
.position(LatLng(item.latitude, item.longitude))
|
|
227
|
+
.anchor(0.5f, 0.5f)
|
|
228
|
+
.zIndex((kind.count + 1000).toFloat())
|
|
229
|
+
|
|
230
|
+
icon?.let { opts.icon(BitmapDescriptorFactory.fromBitmap(it)) }
|
|
231
|
+
|
|
232
|
+
val marker = map.addMarker(opts)
|
|
233
|
+
if (marker != null) {
|
|
234
|
+
marker.tag = MarkerTagData.Cluster(kind.markerIds, kind.count)
|
|
235
|
+
renderedClusterMarkers.add(marker)
|
|
236
|
+
// Animate cluster icons when animatesClusters is enabled.
|
|
237
|
+
// Only animate when cluster structure actually changes:
|
|
238
|
+
// - First render, zoom level change, or markers added/removed.
|
|
239
|
+
// Skip on pure pan at same zoom (same clusters, just repositioned).
|
|
240
|
+
val zoomChanged = Math.floor(zoom) != Math.floor(lastClusteredZoom.toDouble())
|
|
241
|
+
val structureChanged = isFirstRender || zoomChanged || clusteringDirty
|
|
242
|
+
val animateOnReappear = clusterConfig?.animateOnReappear ?: true
|
|
243
|
+
val animEnabled = (clusterConfig?.animatesClusters ?: true)
|
|
244
|
+
&& (isFirstRender || (animateOnReappear && structureChanged))
|
|
245
|
+
if (animEnabled) {
|
|
246
|
+
val duration = clusterConfig?.animationDuration ?: 0.3
|
|
247
|
+
applyMarkerAnimation(marker, MarkerAnimation.APPEAR, duration)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
is RenderKind.Single -> {
|
|
253
|
+
// H-3: Use markerData (single source of truth)
|
|
254
|
+
val data = markerData[kind.markerId] ?: continue
|
|
255
|
+
|
|
256
|
+
val existing = oldSingleMarkers[kind.markerId]
|
|
257
|
+
if (existing != null) {
|
|
258
|
+
// Reuse existing marker — skip full property update.
|
|
259
|
+
// The marker already has correct props from updateMarkerInPlace().
|
|
260
|
+
// Only zIndex (render order) changes between clustering cycles.
|
|
261
|
+
// Re-setting icon was causing flicker on every camera idle
|
|
262
|
+
// (e.g. during user location tracking).
|
|
263
|
+
existing.zIndex = renderIndex.toFloat()
|
|
264
|
+
renderedSingleMarkers[kind.markerId] = existing
|
|
265
|
+
} else {
|
|
266
|
+
val opts = buildMarkerOptions(data)
|
|
267
|
+
opts.zIndex(renderIndex.toFloat())
|
|
268
|
+
val marker = map.addMarker(opts)
|
|
269
|
+
if (marker != null) {
|
|
270
|
+
marker.tag = MarkerTagData.Regular(kind.markerId)
|
|
271
|
+
renderedSingleMarkers[kind.markerId] = marker
|
|
272
|
+
// Animate new markers emerging from clusters (matches iOS behavior)
|
|
273
|
+
if (data.animateOnReappear && data.animation != MarkerAnimation.NONE) {
|
|
274
|
+
applyMarkerAnimation(marker, data.animation, data.animationDuration)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Remove old markers AFTER new ones are visible
|
|
283
|
+
for (marker in oldClusterMarkers) {
|
|
284
|
+
marker.remove()
|
|
285
|
+
}
|
|
286
|
+
for ((id, marker) in oldSingleMarkers) {
|
|
287
|
+
if (renderedSingleMarkers.containsKey(id)) continue
|
|
288
|
+
if (id == selectedMarkerId) continue
|
|
289
|
+
marker.remove()
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Handle non-clustered markers with hideOnOverlap
|
|
293
|
+
if (strategy == ClusterStrategy.HIDEONOVERLAP) {
|
|
294
|
+
hideOverlappingNonClusteredMarkers(visibleItems, zoom, width)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Track state for next cycle's animation decisions
|
|
298
|
+
lastClusteredZoom = zoom.toFloat()
|
|
299
|
+
clusteringDirty = false
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ============================================================================
|
|
303
|
+
// MARK: - Overlap Resolution (hideOnOverlap strategy)
|
|
304
|
+
// ============================================================================
|
|
305
|
+
|
|
306
|
+
private fun resolveOverlaps(
|
|
307
|
+
items: List<RenderItem>,
|
|
308
|
+
zoom: Double,
|
|
309
|
+
mapWidth: Double
|
|
310
|
+
): List<RenderItem> {
|
|
311
|
+
val sorted = items.sortedByDescending { it.priority }
|
|
312
|
+
val visible = mutableListOf<RenderItem>()
|
|
313
|
+
val worldSize = 256.0 * 2.0.pow(zoom)
|
|
314
|
+
|
|
315
|
+
for (item in sorted) {
|
|
316
|
+
var hasOverlap = false
|
|
317
|
+
for (existing in visible) {
|
|
318
|
+
if (itemsOverlap(item, existing, worldSize)) {
|
|
319
|
+
hasOverlap = true
|
|
320
|
+
break
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (!hasOverlap) {
|
|
324
|
+
visible.add(item)
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return visible
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
private fun GoogleMapProvider.hideOverlappingNonClusteredMarkers(
|
|
331
|
+
visibleItems: List<RenderItem>,
|
|
332
|
+
zoom: Double,
|
|
333
|
+
mapWidth: Double
|
|
334
|
+
) {
|
|
335
|
+
val worldSize = 256.0 * 2.0.pow(zoom)
|
|
336
|
+
|
|
337
|
+
for ((id, ncMarker) in nonClusteredMarkers) {
|
|
338
|
+
val ncItem = RenderItem(
|
|
339
|
+
latitude = ncMarker.position.latitude,
|
|
340
|
+
longitude = ncMarker.position.longitude,
|
|
341
|
+
kind = RenderKind.Single(id),
|
|
342
|
+
iconWidth = 44.0,
|
|
343
|
+
iconHeight = 44.0,
|
|
344
|
+
priority = 0
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
var hasOverlap = false
|
|
348
|
+
for (existing in visibleItems) {
|
|
349
|
+
if (itemsOverlap(ncItem, existing, worldSize)) {
|
|
350
|
+
hasOverlap = true
|
|
351
|
+
break
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (hasOverlap) {
|
|
356
|
+
ncMarker.isVisible = false
|
|
357
|
+
hiddenNonClusteredIds.add(id)
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private fun itemsOverlap(a: RenderItem, b: RenderItem, worldSize: Double): Boolean {
|
|
363
|
+
val dx = mercatorX(a.longitude, worldSize) - mercatorX(b.longitude, worldSize)
|
|
364
|
+
val dy = mercatorY(a.latitude, worldSize) - mercatorY(b.latitude, worldSize)
|
|
365
|
+
val minSepX = (a.iconWidth + b.iconWidth) / 2.0
|
|
366
|
+
val minSepY = (a.iconHeight + b.iconHeight) / 2.0
|
|
367
|
+
return abs(dx) < minSepX && abs(dy) < minSepY
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
private fun mercatorX(longitude: Double, worldSize: Double): Double {
|
|
371
|
+
return (longitude + 180.0) / 360.0 * worldSize
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
private fun mercatorY(latitude: Double, worldSize: Double): Double {
|
|
375
|
+
val latRad = latitude * Math.PI / 180.0
|
|
376
|
+
return (1.0 - ln(tan(latRad) + 1.0 / cos(latRad)) / Math.PI) / 2.0 * worldSize
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ============================================================================
|
|
380
|
+
// MARK: - Marker Pool Management
|
|
381
|
+
// ============================================================================
|
|
382
|
+
|
|
383
|
+
fun GoogleMapProvider.clearRenderedClusterMarkers() {
|
|
384
|
+
for (marker in renderedClusterMarkers) {
|
|
385
|
+
marker.remove()
|
|
386
|
+
}
|
|
387
|
+
renderedClusterMarkers.clear()
|
|
388
|
+
|
|
389
|
+
for ((id, marker) in renderedSingleMarkers) {
|
|
390
|
+
if (id == selectedMarkerId) continue
|
|
391
|
+
marker.remove()
|
|
392
|
+
}
|
|
393
|
+
if (selectedMarkerId != null) {
|
|
394
|
+
val selectedMarker = renderedSingleMarkers[selectedMarkerId]
|
|
395
|
+
renderedSingleMarkers.clear()
|
|
396
|
+
selectedMarker?.let { renderedSingleMarkers[selectedMarkerId!!] = it }
|
|
397
|
+
} else {
|
|
398
|
+
renderedSingleMarkers.clear()
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
fun GoogleMapProvider.renderAllMarkersIndividually() {
|
|
403
|
+
val map = googleMap ?: return
|
|
404
|
+
// H-3: Use markerData (single source of truth)
|
|
405
|
+
// When global clustering is disabled, render ALL markers individually —
|
|
406
|
+
// don't filter by per-marker clusteringEnabled, because updateMarker may
|
|
407
|
+
// have already set it to false before this debounced call fires.
|
|
408
|
+
for ((_, data) in markerData) {
|
|
409
|
+
if (renderedSingleMarkers.containsKey(data.id)) continue
|
|
410
|
+
if (nonClusteredMarkers.containsKey(data.id)) continue
|
|
411
|
+
val opts = buildMarkerOptions(data)
|
|
412
|
+
val marker = map.addMarker(opts)
|
|
413
|
+
if (marker != null) {
|
|
414
|
+
marker.tag = MarkerTagData.Regular(data.id)
|
|
415
|
+
renderedSingleMarkers[data.id] = marker
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ============================================================================
|
|
421
|
+
// MARK: - Clustering Control
|
|
422
|
+
// ============================================================================
|
|
423
|
+
|
|
424
|
+
internal fun GoogleMapProvider.setClusteringEnabledInternal(enabled: Boolean) {
|
|
425
|
+
val existingConfig = clusterConfig
|
|
426
|
+
clusterConfig = if (existingConfig != null) {
|
|
427
|
+
ClusterConfig(
|
|
428
|
+
enabled = enabled,
|
|
429
|
+
strategy = existingConfig.strategy,
|
|
430
|
+
radius = existingConfig.radius,
|
|
431
|
+
minimumClusterSize = existingConfig.minimumClusterSize,
|
|
432
|
+
maxZoom = existingConfig.maxZoom,
|
|
433
|
+
backgroundColor = existingConfig.backgroundColor,
|
|
434
|
+
textColor = existingConfig.textColor,
|
|
435
|
+
borderWidth = existingConfig.borderWidth,
|
|
436
|
+
borderColor = existingConfig.borderColor,
|
|
437
|
+
animatesClusters = existingConfig.animatesClusters,
|
|
438
|
+
animateOnReappear = existingConfig.animateOnReappear,
|
|
439
|
+
animationDuration = existingConfig.animationDuration,
|
|
440
|
+
realtimeClustering = existingConfig.realtimeClustering,
|
|
441
|
+
renderBuffer = existingConfig.renderBuffer,
|
|
442
|
+
throttleInterval = existingConfig.throttleInterval
|
|
443
|
+
)
|
|
444
|
+
} else {
|
|
445
|
+
ClusterConfig(
|
|
446
|
+
enabled = enabled,
|
|
447
|
+
strategy = null, radius = null, minimumClusterSize = null,
|
|
448
|
+
maxZoom = null, backgroundColor = null, textColor = null,
|
|
449
|
+
borderWidth = null, borderColor = null, animatesClusters = null,
|
|
450
|
+
animateOnReappear = null, animationDuration = null,
|
|
451
|
+
realtimeClustering = null, renderBuffer = null, throttleInterval = null
|
|
452
|
+
)
|
|
453
|
+
}
|
|
454
|
+
performClustering()
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
internal fun GoogleMapProvider.refreshClustersInternal() {
|
|
458
|
+
performClustering()
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// M-1: buildMarkerOptions and applyMarkerProperties are now shared functions
|
|
462
|
+
// defined in GoogleMapProvider+Markers.kt — no duplicates here.
|
|
463
|
+
|
|
464
|
+
// ============================================================================
|
|
465
|
+
// MARK: - Viewport Polygon Helpers (rotation-aware visibility)
|
|
466
|
+
// ============================================================================
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Point-in-convex-quadrilateral test using cross products.
|
|
470
|
+
* The polygon must have vertices in order (CW or CCW).
|
|
471
|
+
* Returns true if (lat, lon) is inside the quadrilateral.
|
|
472
|
+
*
|
|
473
|
+
* For a convex polygon, a point is inside if it's on the same side
|
|
474
|
+
* of all edges. We check the sign of the cross product for each edge.
|
|
475
|
+
*/
|
|
476
|
+
private fun pointInConvexQuad(lat: Double, lon: Double, quad: List<LatLng>): Boolean {
|
|
477
|
+
val n = quad.size
|
|
478
|
+
var positive = 0
|
|
479
|
+
var negative = 0
|
|
480
|
+
|
|
481
|
+
for (i in 0 until n) {
|
|
482
|
+
val x1 = quad[i].longitude
|
|
483
|
+
val y1 = quad[i].latitude
|
|
484
|
+
val x2 = quad[(i + 1) % n].longitude
|
|
485
|
+
val y2 = quad[(i + 1) % n].latitude
|
|
486
|
+
|
|
487
|
+
// Cross product of edge vector and point vector
|
|
488
|
+
val cross = (x2 - x1) * (lat - y1) - (y2 - y1) * (lon - x1)
|
|
489
|
+
|
|
490
|
+
if (cross > 0) positive++
|
|
491
|
+
else if (cross < 0) negative++
|
|
492
|
+
|
|
493
|
+
// If we have both positive and negative, point is outside
|
|
494
|
+
if (positive > 0 && negative > 0) return false
|
|
495
|
+
}
|
|
496
|
+
return true
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Expands a convex polygon outward from its centroid by a fraction.
|
|
501
|
+
* Used to account for renderBuffer so markers near viewport edges
|
|
502
|
+
* aren't clipped prematurely.
|
|
503
|
+
*/
|
|
504
|
+
private fun expandPolygon(polygon: List<LatLng>, fraction: Double): List<LatLng> {
|
|
505
|
+
if (fraction <= 0.0) return polygon
|
|
506
|
+
|
|
507
|
+
// Compute centroid
|
|
508
|
+
var cLat = 0.0
|
|
509
|
+
var cLon = 0.0
|
|
510
|
+
for (p in polygon) {
|
|
511
|
+
cLat += p.latitude
|
|
512
|
+
cLon += p.longitude
|
|
513
|
+
}
|
|
514
|
+
cLat /= polygon.size
|
|
515
|
+
cLon /= polygon.size
|
|
516
|
+
|
|
517
|
+
// Expand each vertex outward from centroid
|
|
518
|
+
return polygon.map { p ->
|
|
519
|
+
LatLng(
|
|
520
|
+
cLat + (p.latitude - cLat) * (1.0 + fraction),
|
|
521
|
+
cLon + (p.longitude - cLon) * (1.0 + fraction)
|
|
522
|
+
)
|
|
523
|
+
}
|
|
524
|
+
}
|