expo-gaode-map-navigation 1.1.5 → 1.1.7

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 (146) hide show
  1. package/README.md +213 -73
  2. package/android/build.gradle +10 -0
  3. package/android/src/main/cpp/CMakeLists.txt +24 -0
  4. package/android/src/main/cpp/cluster_jni.cpp +848 -0
  5. package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapModule.kt +616 -92
  6. package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapOfflineModule.kt +493 -0
  7. package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapView.kt +230 -14
  8. package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapViewModule.kt +37 -27
  9. package/android/src/main/java/expo/modules/gaodemap/map/MapPreloadManager.kt +494 -0
  10. package/android/src/main/java/expo/modules/gaodemap/map/companion/BitmapDescriptorCache.kt +30 -0
  11. package/android/src/main/java/expo/modules/gaodemap/map/companion/IconBitmapCache.kt +37 -0
  12. package/android/src/main/java/expo/modules/gaodemap/map/managers/UIManager.kt +76 -0
  13. package/android/src/main/java/expo/modules/gaodemap/map/modules/LocationManager.kt +15 -3
  14. package/android/src/main/java/expo/modules/gaodemap/map/modules/SDKInitializer.kt +4 -59
  15. package/android/src/main/java/expo/modules/gaodemap/map/overlays/CircleView.kt +9 -12
  16. package/android/src/main/java/expo/modules/gaodemap/map/overlays/CircleViewModule.kt +5 -6
  17. package/android/src/main/java/expo/modules/gaodemap/map/overlays/ClusterView.kt +539 -66
  18. package/android/src/main/java/expo/modules/gaodemap/map/overlays/ClusterViewModule.kt +17 -1
  19. package/android/src/main/java/expo/modules/gaodemap/map/overlays/HeatMapView.kt +165 -33
  20. package/android/src/main/java/expo/modules/gaodemap/map/overlays/HeatMapViewModule.kt +15 -3
  21. package/android/src/main/java/expo/modules/gaodemap/map/overlays/MarkerView.kt +1249 -672
  22. package/android/src/main/java/expo/modules/gaodemap/map/overlays/MarkerViewModule.kt +40 -17
  23. package/android/src/main/java/expo/modules/gaodemap/map/overlays/MultiPointView.kt +177 -22
  24. package/android/src/main/java/expo/modules/gaodemap/map/overlays/MultiPointViewModule.kt +11 -3
  25. package/android/src/main/java/expo/modules/gaodemap/map/overlays/PolygonView.kt +57 -14
  26. package/android/src/main/java/expo/modules/gaodemap/map/overlays/PolygonViewModule.kt +9 -5
  27. package/android/src/main/java/expo/modules/gaodemap/map/overlays/PolylineView.kt +90 -63
  28. package/android/src/main/java/expo/modules/gaodemap/map/overlays/PolylineViewModule.kt +7 -3
  29. package/android/src/main/java/expo/modules/gaodemap/map/services/LocationForegroundService.kt +3 -2
  30. package/android/src/main/java/expo/modules/gaodemap/map/utils/BitmapDescriptorCache.kt +20 -0
  31. package/android/src/main/java/expo/modules/gaodemap/map/utils/ClusterNative.kt +13 -0
  32. package/android/src/main/java/expo/modules/gaodemap/map/utils/ColorParser.kt +20 -0
  33. package/android/src/main/java/expo/modules/gaodemap/map/utils/GeometryUtils.kt +515 -0
  34. package/android/src/main/java/expo/modules/gaodemap/map/utils/LatLngParser.kt +91 -0
  35. package/android/src/main/java/expo/modules/gaodemap/map/utils/PermissionHelper.kt +248 -0
  36. package/build/ExpoGaodeMapNaviView.d.ts +7 -7
  37. package/build/ExpoGaodeMapNaviView.js +10 -11
  38. package/build/ExpoGaodeMapNavigationModule.d.ts +2 -1
  39. package/build/index.d.ts +35 -33
  40. package/build/index.js +70 -106
  41. package/build/map/ExpoGaodeMapModule.d.ts +2 -201
  42. package/build/map/ExpoGaodeMapModule.js +586 -18
  43. package/build/map/ExpoGaodeMapOfflineModule.d.ts +139 -0
  44. package/build/map/ExpoGaodeMapOfflineModule.js +8 -0
  45. package/build/map/ExpoGaodeMapView.js +66 -58
  46. package/build/map/components/FoldableMapView.d.ts +38 -0
  47. package/build/map/components/FoldableMapView.js +209 -0
  48. package/build/map/components/MapContext.d.ts +12 -0
  49. package/build/map/components/MapContext.js +54 -0
  50. package/build/map/components/MapUI.d.ts +18 -0
  51. package/build/map/components/MapUI.js +29 -0
  52. package/build/map/components/overlays/Circle.js +34 -3
  53. package/build/map/components/overlays/Cluster.d.ts +3 -1
  54. package/build/map/components/overlays/Cluster.js +31 -2
  55. package/build/map/components/overlays/HeatMap.d.ts +3 -1
  56. package/build/map/components/overlays/HeatMap.js +33 -3
  57. package/build/map/components/overlays/Marker.d.ts +1 -1
  58. package/build/map/components/overlays/Marker.js +37 -32
  59. package/build/map/components/overlays/MultiPoint.js +1 -1
  60. package/build/map/components/overlays/Polygon.js +30 -3
  61. package/build/map/components/overlays/Polyline.js +36 -3
  62. package/build/map/index.d.ts +25 -5
  63. package/build/map/index.js +59 -18
  64. package/build/map/types/common.types.d.ts +40 -0
  65. package/build/map/types/common.types.js +0 -4
  66. package/build/map/types/index.d.ts +3 -2
  67. package/build/map/types/map-view.types.d.ts +108 -3
  68. package/build/map/types/native-module.types.d.ts +363 -0
  69. package/build/map/types/native-module.types.js +5 -0
  70. package/build/map/types/offline.types.d.ts +132 -0
  71. package/build/map/types/offline.types.js +5 -0
  72. package/build/map/types/overlays.types.d.ts +137 -24
  73. package/build/map/utils/ErrorHandler.d.ts +110 -0
  74. package/build/map/utils/ErrorHandler.js +421 -0
  75. package/build/map/utils/GeoUtils.d.ts +20 -0
  76. package/build/map/utils/GeoUtils.js +76 -0
  77. package/build/map/utils/OfflineMapManager.d.ts +148 -0
  78. package/build/map/utils/OfflineMapManager.js +217 -0
  79. package/build/map/utils/PermissionUtils.d.ts +91 -0
  80. package/build/map/utils/PermissionUtils.js +255 -0
  81. package/build/map/utils/PlatformDetector.d.ts +102 -0
  82. package/build/map/utils/PlatformDetector.js +186 -0
  83. package/build/types/index.d.ts +1 -0
  84. package/build/types/index.js +1 -0
  85. package/build/types/native-module.types.d.ts +69 -0
  86. package/build/types/native-module.types.js +2 -0
  87. package/build/types/naviview.types.d.ts +1 -1
  88. package/expo-module.config.json +12 -10
  89. package/ios/ExpoGaodeMapNavigation.podspec +9 -0
  90. package/ios/map/ExpoGaodeMapModule.swift +485 -75
  91. package/ios/map/ExpoGaodeMapOfflineModule.swift +479 -0
  92. package/ios/map/ExpoGaodeMapView.swift +611 -62
  93. package/ios/map/ExpoGaodeMapViewModule.swift +48 -26
  94. package/ios/map/MapPreloadManager.swift +348 -0
  95. package/ios/map/cpp/ClusterEngine.cpp +110 -0
  96. package/ios/map/cpp/ClusterEngine.hpp +20 -0
  97. package/ios/map/cpp/ColorParser.cpp +135 -0
  98. package/ios/map/cpp/ColorParser.hpp +14 -0
  99. package/ios/map/cpp/GeometryEngine.cpp +574 -0
  100. package/ios/map/cpp/GeometryEngine.hpp +159 -0
  101. package/ios/map/cpp/QuadTree.cpp +92 -0
  102. package/ios/map/cpp/QuadTree.hpp +42 -0
  103. package/ios/map/cpp/README.md +55 -0
  104. package/ios/map/managers/UIManager.swift +72 -1
  105. package/ios/map/modules/LocationManager.swift +123 -166
  106. package/ios/map/overlays/CircleView.swift +16 -32
  107. package/ios/map/overlays/CircleViewModule.swift +12 -12
  108. package/ios/map/overlays/ClusterAnnotation.swift +32 -0
  109. package/ios/map/overlays/ClusterView.swift +331 -45
  110. package/ios/map/overlays/ClusterViewModule.swift +20 -6
  111. package/ios/map/overlays/HeatMapView.swift +135 -32
  112. package/ios/map/overlays/HeatMapViewModule.swift +20 -8
  113. package/ios/map/overlays/MarkerView.swift +613 -130
  114. package/ios/map/overlays/MarkerViewModule.swift +38 -18
  115. package/ios/map/overlays/MultiPointView.swift +168 -10
  116. package/ios/map/overlays/MultiPointViewModule.swift +27 -5
  117. package/ios/map/overlays/PolygonView.swift +62 -23
  118. package/ios/map/overlays/PolygonViewModule.swift +18 -12
  119. package/ios/map/overlays/PolylineView.swift +21 -13
  120. package/ios/map/overlays/PolylineViewModule.swift +18 -12
  121. package/ios/map/utils/ClusterNative.h +96 -0
  122. package/ios/map/utils/ClusterNative.mm +377 -0
  123. package/ios/map/utils/ColorParser.swift +12 -1
  124. package/ios/map/utils/CppBridging.mm +13 -0
  125. package/ios/map/utils/GeometryUtils.swift +34 -0
  126. package/ios/map/utils/LatLngParser.swift +87 -0
  127. package/ios/map/utils/PermissionManager.swift +135 -6
  128. package/package.json +3 -2
  129. package/shared/cpp/ClusterEngine.cpp +110 -0
  130. package/shared/cpp/ClusterEngine.hpp +20 -0
  131. package/shared/cpp/ColorParser.cpp +135 -0
  132. package/shared/cpp/ColorParser.hpp +14 -0
  133. package/shared/cpp/GeometryEngine.cpp +574 -0
  134. package/shared/cpp/GeometryEngine.hpp +159 -0
  135. package/shared/cpp/QuadTree.cpp +92 -0
  136. package/shared/cpp/QuadTree.hpp +42 -0
  137. package/shared/cpp/README.md +55 -0
  138. package/shared/cpp/tests/benchmark_js.js +41 -0
  139. package/shared/cpp/tests/run.sh +17 -0
  140. package/shared/cpp/tests/test_main.cpp +276 -0
  141. package/build/map/ExpoGaodeMap.types.d.ts +0 -41
  142. package/build/map/ExpoGaodeMap.types.js +0 -24
  143. package/build/map/utils/EventManager.d.ts +0 -10
  144. package/build/map/utils/EventManager.js +0 -26
  145. package/build/map/utils/ModuleLoader.d.ts +0 -73
  146. package/build/map/utils/ModuleLoader.js +0 -112
@@ -1,33 +1,99 @@
1
1
  package expo.modules.gaodemap.map.overlays
2
2
 
3
+ import android.annotation.SuppressLint
3
4
  import android.content.Context
5
+ import android.util.Log
6
+ import android.graphics.Bitmap
7
+ import android.graphics.BitmapFactory
8
+ import android.graphics.Canvas
9
+ import java.net.URL
10
+
11
+
12
+ import android.graphics.Paint
13
+
14
+ import android.graphics.Typeface
15
+ import android.os.Handler
16
+ import android.os.Looper
17
+
4
18
  import com.amap.api.maps.AMap
19
+ import com.amap.api.maps.AMapUtils
20
+ import com.amap.api.maps.model.BitmapDescriptor
5
21
  import com.amap.api.maps.model.BitmapDescriptorFactory
22
+ import com.amap.api.maps.model.CameraPosition
6
23
  import com.amap.api.maps.model.LatLng
7
24
  import com.amap.api.maps.model.Marker
8
25
  import com.amap.api.maps.model.MarkerOptions
26
+ import expo.modules.gaodemap.map.ExpoGaodeMapView
27
+ import expo.modules.gaodemap.map.utils.ClusterNative
28
+ import expo.modules.gaodemap.map.utils.ColorParser
29
+ import expo.modules.gaodemap.map.utils.LatLngParser
9
30
  import expo.modules.kotlin.AppContext
10
31
  import expo.modules.kotlin.viewevent.EventDispatcher
11
32
  import expo.modules.kotlin.views.ExpoView
33
+ import kotlinx.coroutines.*
34
+ import java.util.concurrent.ConcurrentHashMap
35
+
12
36
 
13
37
  /**
14
38
  * 点聚合视图
15
- * 注意:高德 Android SDK 的点聚合功能需要额外依赖,这里提供基础实现
16
- * 实际使用时可能需要引入 com.amap.api:3dmap-cluster 库
39
+ * 实现真正的点聚合逻辑,支持自定义样式和点击事件
17
40
  */
18
- class ClusterView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
41
+ class ClusterView(context: Context, appContext: AppContext) : ExpoView(context, appContext), AMap.OnCameraChangeListener {
19
42
 
20
- private val onPress by EventDispatcher()
43
+
21
44
  @Suppress("unused")
22
45
  private val onClusterPress by EventDispatcher()
23
46
 
24
47
  private var aMap: AMap? = null
25
- private var markers: MutableList<Marker> = mutableListOf()
26
- private var points: List<Map<String, Any>> = emptyList()
27
- @Suppress("unused")
28
- private var radius: Int = 60
29
- @Suppress("unused")
30
- private var minClusterSize: Int = 2
48
+
49
+ // 聚合点数据
50
+ data class ClusterItem(
51
+ val latLng: LatLng,
52
+ val data: Map<String, Any>
53
+ )
54
+
55
+ // 聚合对象
56
+ class Cluster(val center: LatLng) {
57
+ val items = mutableListOf<ClusterItem>()
58
+
59
+ fun add(item: ClusterItem) {
60
+ items.add(item)
61
+ }
62
+
63
+ val size: Int get() = items.size
64
+
65
+ }
66
+
67
+ private var rawPoints: List<Map<String, Any>> = emptyList()
68
+ private var clusterItems: List<ClusterItem> = emptyList()
69
+ private var clusters: List<Cluster> = emptyList()
70
+
71
+ // 当前显示的 Markers
72
+ private val currentMarkers = mutableListOf<Marker>()
73
+
74
+ // 配置属性
75
+ private var radius: Int = 60 // dp
76
+ private var minClusterSize: Int = 1
77
+
78
+ // 样式属性
79
+ private var clusterStyle: Map<String, Any>? = null
80
+ private var clusterBuckets: List<Map<String, Any>>? = null
81
+ private var clusterTextStyle: Map<String, Any>? = null
82
+
83
+ private val mainHandler = Handler(Looper.getMainLooper())
84
+
85
+ // 协程作用域
86
+ private var scope = CoroutineScope(Dispatchers.Main + Job())
87
+ private var calculationJob: Job? = null
88
+
89
+ // 缓存 BitmapDescriptor
90
+ private val bitmapCache = ConcurrentHashMap<Int, BitmapDescriptor>()
91
+ private var currentIconDescriptor: BitmapDescriptor? = null
92
+ private var customIconBitmap: Bitmap? = null
93
+ private var pendingIconUri: String? = null
94
+
95
+ // 标记样式是否发生变化,用于强制更新图标
96
+ private var styleChanged = false
31
97
 
32
98
  /**
33
99
  * 设置地图实例
@@ -35,20 +101,32 @@ class ClusterView(context: Context, appContext: AppContext) : ExpoView(context,
35
101
  @Suppress("unused")
36
102
  fun setMap(map: AMap) {
37
103
  aMap = map
38
- createOrUpdateCluster()
104
+ // 注册相机监听
105
+ // 注意:addView 时会自动调用 setMap,此时 parent 已设置
106
+ (parent as? ExpoGaodeMapView)?.addCameraChangeListener(this)
107
+
108
+ // 如果有待处理的 icon,加载它
109
+ pendingIconUri?.let {
110
+ loadAndSetIcon(it)
111
+ }
112
+
113
+ updateClusters()
39
114
  }
40
115
 
41
116
  /**
42
- * 设置聚合点数据
117
+ * 设置聚合点
43
118
  */
44
- fun setPoints(pointsList: List<Map<String, Any>>) {
45
- // 过滤无效坐标
46
- points = pointsList.filter { point ->
47
- val lat = (point["latitude"] as? Number)?.toDouble()
48
- val lng = (point["longitude"] as? Number)?.toDouble()
49
- lat != null && lng != null && lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180
119
+ fun setPoints(points: List<Map<String, Any>>) {
120
+ rawPoints = points
121
+ clusterItems = points.mapNotNull { pointData ->
122
+ LatLngParser.parseLatLng(pointData)?.let { latLng ->
123
+ ClusterItem(latLng, pointData)
124
+ }
50
125
  }
51
- createOrUpdateCluster()
126
+
127
+ // 强制重新计算
128
+ styleChanged = true
129
+ updateClusters()
52
130
  }
53
131
 
54
132
  /**
@@ -56,84 +134,479 @@ class ClusterView(context: Context, appContext: AppContext) : ExpoView(context,
56
134
  */
57
135
  fun setRadius(radiusValue: Int) {
58
136
  radius = radiusValue
59
- createOrUpdateCluster()
137
+ updateClusters()
60
138
  }
61
139
 
62
140
  /**
63
141
  * 设置最小聚合数量
64
142
  */
65
143
  fun setMinClusterSize(size: Int) {
144
+ Log.d("ClusterView", "setMinClusterSize: $size")
66
145
  minClusterSize = size
67
- createOrUpdateCluster()
146
+ updateClusters()
68
147
  }
69
148
 
70
149
  /**
71
- * 设置图标
150
+ * 设置聚合样式
151
+ */
152
+ fun setClusterStyle(style: Map<String, Any>) {
153
+ clusterStyle = style
154
+ bitmapCache.clear() // 样式改变,清除缓存
155
+ styleChanged = true
156
+ updateClusters()
157
+ }
158
+
159
+ /**
160
+ * 设置聚合文字样式
161
+ */
162
+ fun setClusterTextStyle(style: Map<String, Any>) {
163
+ clusterTextStyle = style
164
+ bitmapCache.clear() // 样式改变,清除缓存
165
+ styleChanged = true
166
+ updateClusters()
167
+ }
168
+
169
+ fun setClusterBuckets(buckets: List<Map<String, Any>>) {
170
+ clusterBuckets = buckets
171
+ bitmapCache.clear()
172
+ styleChanged = true
173
+ updateClusters()
174
+ }
175
+
176
+ /**
177
+ * 设置图标 (保留接口,目前主要使用 clusterStyle)
72
178
  */
73
179
  @Suppress("UNUSED_PARAMETER")
74
180
  fun setIcon(iconUri: String?) {
75
- // 简化处理,实际需要实现图片加载
76
- createOrUpdateCluster()
181
+ pendingIconUri = iconUri
182
+ if (iconUri != null) {
183
+ loadAndSetIcon(iconUri)
184
+ } else {
185
+ currentIconDescriptor = null
186
+ updateClusters()
187
+ }
188
+ }
189
+
190
+ private fun loadAndSetIcon(iconUri: String) {
191
+ // 尝试从缓存获取 (这里只缓存 Descriptor,Bitmap 需要重新加载或者另外缓存)
192
+ // 为了简单起见,如果设置了 icon,我们总是重新加载 Bitmap 以便支持绘制文字
193
+ // 实际生产中应该也缓存 Bitmap
194
+
195
+ scope.launch(Dispatchers.IO) {
196
+ try {
197
+ val bitmap = when {
198
+ iconUri.startsWith("http") -> {
199
+ val url = URL(iconUri)
200
+ BitmapFactory.decodeStream(url.openStream())
201
+ }
202
+ iconUri.startsWith("file://") -> {
203
+ val path = iconUri.substring(7)
204
+ BitmapFactory.decodeFile(path)
205
+ }
206
+ else -> {
207
+ // 尝试作为资源名称加载
208
+ val resId = context.resources.getIdentifier(iconUri, "drawable", context.packageName)
209
+ if (resId != 0) {
210
+ BitmapFactory.decodeResource(context.resources, resId)
211
+ } else {
212
+ // 尝试作为普通文件路径
213
+ BitmapFactory.decodeFile(iconUri)
214
+ }
215
+ }
216
+ }
217
+
218
+ if (bitmap != null) {
219
+ customIconBitmap = bitmap
220
+ val descriptor = BitmapDescriptorFactory.fromBitmap(bitmap)
221
+ currentIconDescriptor = descriptor
222
+
223
+ // 清空缓存,因为基础图标变了,所有生成的带数字的图标都需要重新生成
224
+ bitmapCache.clear()
225
+
226
+ withContext(Dispatchers.Main) {
227
+ updateClusters()
228
+ }
229
+ }
230
+ } catch (e: Exception) {
231
+ e.printStackTrace()
232
+ }
233
+ }
234
+ }
235
+
236
+ /**
237
+ * 相机移动回调
238
+ */
239
+ override fun onCameraChange(cameraPosition: CameraPosition?) {
240
+ // 移动过程中不实时重新计算,以免性能问题
241
+ }
242
+
243
+ override fun onCameraChangeFinish(cameraPosition: CameraPosition?) {
244
+ updateClusters()
245
+ }
246
+
247
+ override fun onAttachedToWindow() {
248
+ super.onAttachedToWindow()
249
+ // 重新创建协程作用域(如果已被取消)
250
+ if (!scope.isActive) {
251
+ scope = CoroutineScope(Dispatchers.Main + Job())
252
+ }
253
+ // 重新注册监听器(防止因 detach 导致监听器丢失)
254
+ (parent as? ExpoGaodeMapView)?.addCameraChangeListener(this)
255
+ updateClusters()
256
+ }
257
+
258
+ override fun onDetachedFromWindow() {
259
+ super.onDetachedFromWindow()
260
+ scope.cancel() // 取消协程
261
+ (parent as? ExpoGaodeMapView)?.removeCameraChangeListener(this)
262
+ currentMarkers.forEach {
263
+ it.remove()
264
+ unregisterMarker(it)
265
+ }
266
+ currentMarkers.clear()
267
+ bitmapCache.clear()
77
268
  }
78
269
 
79
270
  /**
80
- * 创建或更新聚合
81
- * 注意:这是简化实现,完整的点聚合需要使用专门的聚合库
271
+ * 更新聚合
272
+ * 使用协程在后台计算
82
273
  */
83
- private fun createOrUpdateCluster() {
84
- aMap?.let { map ->
85
- // 清除旧的标记
86
- markers.forEach { it.remove() }
87
- markers.clear()
274
+ private fun updateClusters() {
275
+ val map = aMap ?: return
276
+
277
+ // 取消上一次计算
278
+ calculationJob?.cancel()
279
+
280
+ // 确保 scope 处于活跃状态
281
+ if (!scope.isActive) {
282
+ scope = CoroutineScope(Dispatchers.Main + Job())
283
+ }
284
+
285
+ calculationJob = scope.launch(Dispatchers.Default) {
286
+ if (clusterItems.isEmpty()) return@launch
88
287
 
89
- // 简化实现:直接添加所有点作为标记
90
- // 实际应用中应该使用点聚合算法
91
- points.forEach { point ->
92
- val lat = (point["latitude"] as? Number)?.toDouble()
93
- val lng = (point["longitude"] as? Number)?.toDouble()
94
-
95
- if (lat != null && lng != null) {
96
- val markerOptions = MarkerOptions()
97
- .position(LatLng(lat, lng))
98
- .icon(BitmapDescriptorFactory.defaultMarker())
99
-
100
- val marker = map.addMarker(markerOptions)
101
- marker?.let { markers.add(it) }
288
+ // 获取当前比例尺 (米/像素)
289
+ val scalePerPixel = withContext(Dispatchers.Main) {
290
+ // 增加安全性检查
291
+ if (map.mapType != 0) { // 简单检查 map 是否存活
292
+ map.scalePerPixel
293
+ } else {
294
+ 0f
102
295
  }
103
296
  }
297
+
298
+ if (scalePerPixel <= 0) {
299
+ // 比例尺无效,稍后重试
300
+ withContext(Dispatchers.Main) {
301
+ Log.w("ClusterView", "Invalid scalePerPixel: $scalePerPixel, retrying...")
302
+ mainHandler.postDelayed({ updateClusters() }, 500)
303
+ }
304
+ return@launch
305
+ }
104
306
 
105
- // 设置点击监听
106
- map.setOnMarkerClickListener { clickedMarker ->
107
- if (markers.contains(clickedMarker)) {
108
- onPress(mapOf(
109
- "latitude" to clickedMarker.position.latitude,
110
- "longitude" to clickedMarker.position.longitude
111
- ))
112
- true
113
- } else {
114
- false
307
+ // 计算聚合距离 (米)
308
+ // radius dp,需要转 px,再转米
309
+ val density = context.resources.displayMetrics.density
310
+ val radiusPx = radius * density
311
+ val radiusMeters = radiusPx * scalePerPixel
312
+
313
+ val newClusters = buildClustersFromNative(radiusMeters.toDouble()) ?: buildClustersFallback(radiusMeters.toDouble())
314
+
315
+ // 更新 UI
316
+ withContext(Dispatchers.Main) {
317
+ renderClusters(newClusters)
318
+ }
319
+ }
320
+ }
321
+
322
+ private fun buildClustersFromNative(radiusMeters: Double): List<Cluster>? {
323
+ return try {
324
+ val latitudes = DoubleArray(clusterItems.size)
325
+ val longitudes = DoubleArray(clusterItems.size)
326
+ for (i in clusterItems.indices) {
327
+ val item = clusterItems[i]
328
+ latitudes[i] = item.latLng.latitude
329
+ longitudes[i] = item.latLng.longitude
330
+ }
331
+
332
+ val encoded = ClusterNative.clusterPoints(latitudes, longitudes, radiusMeters)
333
+ if (encoded.isEmpty()) return null
334
+
335
+ var cursor = 0
336
+ val clusterCount = encoded[cursor++]
337
+ if (clusterCount <= 0) return emptyList()
338
+
339
+ val newClusters = mutableListOf<Cluster>()
340
+ for (c in 0 until clusterCount) {
341
+ if (cursor + 1 >= encoded.size) break
342
+ val centerIndex = encoded[cursor++]
343
+ val size = encoded[cursor++]
344
+ if (centerIndex < 0 || centerIndex >= clusterItems.size) {
345
+ cursor += size
346
+ continue
115
347
  }
348
+ val cluster = Cluster(clusterItems[centerIndex].latLng)
349
+ for (k in 0 until size) {
350
+ if (cursor >= encoded.size) break
351
+ val itemIndex = encoded[cursor++]
352
+ if (itemIndex >= 0 && itemIndex < clusterItems.size) {
353
+ cluster.add(clusterItems[itemIndex])
354
+ }
355
+ }
356
+ newClusters.add(cluster)
116
357
  }
358
+ newClusters
359
+ } catch (_: Throwable) {
360
+ null
117
361
  }
118
362
  }
363
+
364
+ private fun buildClustersFallback(radiusMeters: Double): List<Cluster> {
365
+ val newClusters = mutableListOf<Cluster>()
366
+ val visited = BooleanArray(clusterItems.size)
367
+
368
+ for (i in clusterItems.indices) {
369
+ if (visited[i]) continue
370
+
371
+ val item = clusterItems[i]
372
+ val cluster = Cluster(item.latLng)
373
+ cluster.add(item)
374
+ visited[i] = true
375
+
376
+ for (j in i + 1 until clusterItems.size) {
377
+ if (visited[j]) continue
378
+
379
+ val other = clusterItems[j]
380
+ val distance = AMapUtils.calculateLineDistance(item.latLng, other.latLng).toDouble()
381
+
382
+ if (distance < radiusMeters) {
383
+ cluster.add(other)
384
+ visited[j] = true
385
+ }
386
+ }
387
+
388
+ newClusters.add(cluster)
389
+ }
390
+
391
+ return newClusters
392
+ }
393
+
394
+ /**
395
+ * 渲染聚合点
396
+ * 使用 Diff 算法优化渲染,避免全量刷新导致的闪烁
397
+ */
398
+ private fun renderClusters(newClusters: List<Cluster>) {
399
+ Log.d("ClusterView", "renderClusters: count=${newClusters.size}, minClusterSize=$minClusterSize")
400
+ val map = aMap ?: return
401
+ clusters = newClusters
402
+
403
+ // 1. 建立新数据的索引 (基于位置 lat_lng)
404
+ // 注意:Double 比较存在精度问题,这里简单处理,实际可使用 GeoHash 或容差比较
405
+ val newClusterMap = newClusters.associateBy { "${it.center.latitude}_${it.center.longitude}" }
406
+
407
+ // 2. 建立当前 Markers 的索引
408
+ val currentMarkerMap = mutableMapOf<String, Marker>()
409
+ val markersToRemove = mutableListOf<Marker>()
410
+
411
+ currentMarkers.forEach { marker ->
412
+ val key = "${marker.position.latitude}_${marker.position.longitude}"
413
+ // 如果当前 Marker 的位置在新数据中存在,且尚未被匹配(处理位置重叠的罕见情况)
414
+ if (newClusterMap.containsKey(key) && !currentMarkerMap.containsKey(key)) {
415
+ currentMarkerMap[key] = marker
416
+ } else {
417
+ markersToRemove.add(marker)
418
+ }
419
+ }
420
+
421
+ // 3. 移除不再存在的 Markers
422
+ markersToRemove.forEach {
423
+ it.remove()
424
+ unregisterMarker(it)
425
+ currentMarkers.remove(it)
426
+ }
427
+
428
+ // 4. 更新或添加 Markers
429
+ newClusters.forEach { cluster ->
430
+ val key = "${cluster.center.latitude}_${cluster.center.longitude}"
431
+ val existingMarker = currentMarkerMap[key]
432
+
433
+ if (existingMarker != null) {
434
+ // --- 更新逻辑 ---
435
+ // 检查数据是否变化(例如聚合数量变化导致图标变化)
436
+ val oldCluster = existingMarker.getObject() as? Cluster
437
+ if (oldCluster?.size != cluster.size || styleChanged) {
438
+ // 只有数量变化时才更新图标,减少开销
439
+ if (cluster.size >= minClusterSize) {
440
+ existingMarker.setIcon(generateIcon(cluster.size))
441
+ existingMarker.zIndex = 2.0f
442
+ } else {
443
+ existingMarker.setIcon(currentIconDescriptor ?: BitmapDescriptorFactory.defaultMarker())
444
+ existingMarker.zIndex = 1.0f
445
+ }
446
+ }
447
+ // 总是更新 title,以防 enableCallout 变化
448
+ existingMarker.title = "${cluster.size}个点"
449
+
450
+ // 更新关联数据
451
+ existingMarker.setObject(cluster)
452
+ } else {
453
+ // --- 新增逻辑 ---
454
+ val markerOptions = MarkerOptions()
455
+ .position(cluster.center)
456
+
457
+
458
+
459
+
460
+ if (cluster.size >= minClusterSize) {
461
+ markerOptions.icon(generateIcon(cluster.size))
462
+ markerOptions.zIndex(2.0f)
463
+ } else {
464
+ markerOptions.icon(currentIconDescriptor ?: BitmapDescriptorFactory.defaultMarker())
465
+ markerOptions.zIndex(1.0f)
466
+ }
467
+
468
+ val marker = map.addMarker(markerOptions)
469
+ if (marker != null) {
470
+ currentMarkers.add(marker)
471
+ registerMarker(marker, this)
472
+ marker.setObject(cluster)
473
+ }
474
+ }
475
+ }
476
+
477
+ // 重置样式变化标记
478
+ styleChanged = false
479
+ }
480
+
481
+ /**
482
+ * 生成聚合图标
483
+ */
484
+ @SuppressLint("UseKtx")
485
+ private fun generateIcon(count: Int): BitmapDescriptor {
486
+ // 检查缓存
487
+ // 简单的缓存策略:只根据数量缓存。如果样式变化,会清空缓存。
488
+ bitmapCache[count]?.let { return it }
489
+
490
+ val density = context.resources.displayMetrics.density
491
+
492
+ // 获取样式配置
493
+ var activeStyle = clusterStyle ?: emptyMap()
494
+
495
+ clusterBuckets?.let { buckets ->
496
+ val bestBucket = buckets
497
+ .filter { ((it["minPoints"] as? Number)?.toInt() ?: 0) <= count }
498
+ .maxByOrNull { (it["minPoints"] as? Number)?.toInt() ?: 0 }
499
+
500
+ if (bestBucket != null) {
501
+ activeStyle = activeStyle + bestBucket
502
+ }
503
+ }
504
+
505
+ val bgColorVal = activeStyle["backgroundColor"]
506
+ val borderColorVal = activeStyle["borderColor"]
507
+ val borderWidthVal = (activeStyle["borderWidth"] as? Number)?.toFloat() ?: 2f
508
+ val textSizeVal = (clusterTextStyle?.get("fontSize") as? Number)?.toFloat() ?: 14f
509
+ val textColorVal = clusterTextStyle?.get("color")
510
+ val fontWeightVal = clusterTextStyle?.get("fontWeight") as? String
511
+
512
+ // 解析颜色
513
+ val bgColor = ColorParser.parseColor(bgColorVal ?: "#F54531") // 默认红色
514
+ val borderColor = ColorParser.parseColor(borderColorVal ?: "#FFFFFF") // 默认白色
515
+ val textColor = ColorParser.parseColor(textColorVal ?: "#FFFFFF") // 默认白色
516
+
517
+ // 计算尺寸 (根据 iOS 逻辑:size = 30 + (count.toString().length - 1) * 5)
518
+ // 这里简单处理,或者根据 count 动态调整
519
+ // 基础大小 36dp
520
+ val baseSize = 36
521
+ val extraSize = (count.toString().length - 1) * 6
522
+ val sizeDp = baseSize + extraSize
523
+ val sizePx = (sizeDp * density).toInt()
524
+
525
+ val bitmap = if (customIconBitmap != null) {
526
+ // 如果有自定义图标,将其缩放到目标大小
527
+ val scaled = Bitmap.createScaledBitmap(customIconBitmap!!, sizePx, sizePx, true)
528
+ // 复制为可变 Bitmap 以便绘制文字
529
+ scaled.copy(Bitmap.Config.ARGB_8888, true)
530
+ } else {
531
+ Bitmap.createBitmap(sizePx, sizePx, Bitmap.Config.ARGB_8888)
532
+ }
533
+
534
+ val canvas = Canvas(bitmap)
535
+
536
+ val paint = Paint(Paint.ANTI_ALIAS_FLAG)
537
+ val radius = sizePx / 2f
538
+
539
+ // 如果没有自定义图标,绘制默认的圆形背景
540
+ if (customIconBitmap == null) {
541
+ // 绘制边框
542
+ paint.color = borderColor
543
+ paint.style = Paint.Style.FILL
544
+ canvas.drawCircle(radius, radius, radius, paint)
545
+
546
+ // 绘制背景
547
+ paint.color = bgColor
548
+ val borderWidthPx = borderWidthVal * density
549
+ canvas.drawCircle(radius, radius, radius - borderWidthPx, paint)
550
+ }
551
+
552
+ // 绘制文字
553
+ paint.color = textColor
554
+ paint.textSize = textSizeVal * density
555
+ paint.textAlign = Paint.Align.CENTER
556
+
557
+ // 字体粗细
558
+ if (fontWeightVal == "bold") {
559
+ paint.typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
560
+ }
561
+
562
+ // 文字垂直居中
563
+ val fontMetrics = paint.fontMetrics
564
+ val baseline = radius - (fontMetrics.bottom + fontMetrics.top) / 2
565
+
566
+ canvas.drawText(count.toString(), radius, baseline, paint)
567
+
568
+ val descriptor = BitmapDescriptorFactory.fromBitmap(bitmap)
569
+ bitmapCache[count] = descriptor
570
+ return descriptor
571
+ }
119
572
 
120
573
  /**
121
- * 移除聚合
574
+ * 处理 Marker 点击
122
575
  */
123
- fun removeCluster() {
124
- markers.forEach { it.remove() }
125
- markers.clear()
126
- points = emptyList()
576
+ fun onMarkerClick(marker: Marker) {
577
+ val cluster = marker.getObject() as? Cluster
578
+ if (cluster != null) {
579
+ // 无论聚合数量多少,统一触发 onClusterPress
580
+ // 这样保证用户在 React Native 端监听 onClusterPress 时总能收到事件
581
+ // 如果是单点,count 为 1,pois 包含单个点数据
582
+ val pointsData = cluster.items.map { it.data }
583
+ onClusterPress(mapOf(
584
+ "count" to cluster.size,
585
+ "latitude" to cluster.center.latitude,
586
+ "longitude" to cluster.center.longitude,
587
+ "pois" to pointsData,
588
+ "points" to pointsData // 兼容 iOS 或用户习惯
589
+ ))
590
+ }
127
591
  }
128
592
 
129
- override fun onDetachedFromWindow() {
130
- super.onDetachedFromWindow()
131
- // 🔑 关键修复:使用 post 延迟检查
132
- post {
133
- if (parent == null) {
134
- removeCluster()
135
- aMap = null
593
+ companion object {
594
+ private val markerMap = ConcurrentHashMap<Marker, ClusterView>()
595
+
596
+ fun registerMarker(marker: Marker, view: ClusterView) {
597
+ markerMap[marker] = view
598
+ }
599
+
600
+ fun unregisterMarker(marker: Marker) {
601
+ markerMap.remove(marker)
602
+ }
603
+
604
+ fun handleMarkerClick(marker: Marker): Boolean {
605
+ markerMap[marker]?.let { view ->
606
+ view.onMarkerClick(marker)
607
+ return true
136
608
  }
609
+ return false
137
610
  }
138
611
  }
139
612
  }