expo-gaode-map 2.2.15 → 2.2.16
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/android/build.gradle +1 -1
- package/android/src/main/java/expo/modules/gaodemap/ExpoGaodeMapView.kt +40 -1
- package/android/src/main/java/expo/modules/gaodemap/overlays/ClusterView.kt +427 -61
- package/android/src/main/java/expo/modules/gaodemap/overlays/ClusterViewModule.kt +16 -0
- package/android/src/main/java/expo/modules/gaodemap/overlays/HeatMapView.kt +160 -25
- package/android/src/main/java/expo/modules/gaodemap/overlays/HeatMapViewModule.kt +13 -1
- package/android/src/main/java/expo/modules/gaodemap/overlays/MultiPointView.kt +165 -13
- package/android/src/main/java/expo/modules/gaodemap/overlays/MultiPointViewModule.kt +9 -1
- package/android/src/main/java/expo/modules/gaodemap/utils/BitmapDescriptorCache.kt +20 -0
- package/build/components/overlays/Cluster.d.ts.map +1 -1
- package/build/components/overlays/Cluster.js +6 -2
- package/build/components/overlays/Cluster.js.map +1 -1
- package/build/components/overlays/HeatMap.d.ts.map +1 -1
- package/build/components/overlays/HeatMap.js +12 -1
- package/build/components/overlays/HeatMap.js.map +1 -1
- package/build/index.d.ts +0 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js.map +1 -1
- package/build/types/overlays.types.d.ts +69 -14
- package/build/types/overlays.types.d.ts.map +1 -1
- package/build/types/overlays.types.js.map +1 -1
- package/build/utils/ModuleLoader.js +1 -1
- package/build/utils/ModuleLoader.js.map +1 -1
- package/ios/ExpoGaodeMapView.swift +44 -0
- package/ios/overlays/ClusterAnnotation.swift +32 -0
- package/ios/overlays/ClusterView.swift +251 -45
- package/ios/overlays/ClusterViewModule.swift +14 -0
- package/ios/overlays/CoordinateQuadTree.swift +291 -0
- package/ios/overlays/HeatMapView.swift +119 -5
- package/ios/overlays/HeatMapViewModule.swift +13 -1
- package/ios/overlays/MultiPointView.swift +160 -2
- package/ios/overlays/MultiPointViewModule.swift +22 -0
- package/package.json +1 -1
package/android/build.gradle
CHANGED
|
@@ -145,6 +145,19 @@ class ExpoGaodeMapView(context: Context, appContext: AppContext) : ExpoView(cont
|
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
// 辅助监听器列表
|
|
149
|
+
private val cameraChangeListeners = mutableListOf<AMap.OnCameraChangeListener>()
|
|
150
|
+
|
|
151
|
+
fun addCameraChangeListener(listener: AMap.OnCameraChangeListener) {
|
|
152
|
+
if (!cameraChangeListeners.contains(listener)) {
|
|
153
|
+
cameraChangeListeners.add(listener)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
fun removeCameraChangeListener(listener: AMap.OnCameraChangeListener) {
|
|
158
|
+
cameraChangeListeners.remove(listener)
|
|
159
|
+
}
|
|
160
|
+
|
|
148
161
|
/**
|
|
149
162
|
* 设置地图事件监听
|
|
150
163
|
*/
|
|
@@ -152,6 +165,9 @@ class ExpoGaodeMapView(context: Context, appContext: AppContext) : ExpoView(cont
|
|
|
152
165
|
// 设置相机移动监听器
|
|
153
166
|
aMap.setOnCameraChangeListener(object : AMap.OnCameraChangeListener {
|
|
154
167
|
override fun onCameraChange(cameraPosition: com.amap.api.maps.model.CameraPosition?) {
|
|
168
|
+
// 通知辅助监听器
|
|
169
|
+
cameraChangeListeners.forEach { it.onCameraChange(cameraPosition) }
|
|
170
|
+
|
|
155
171
|
// 相机移动中 - 应用节流优化
|
|
156
172
|
cameraPosition?.let {
|
|
157
173
|
val currentTime = System.currentTimeMillis()
|
|
@@ -199,6 +215,9 @@ class ExpoGaodeMapView(context: Context, appContext: AppContext) : ExpoView(cont
|
|
|
199
215
|
}
|
|
200
216
|
|
|
201
217
|
override fun onCameraChangeFinish(cameraPosition: com.amap.api.maps.model.CameraPosition?) {
|
|
218
|
+
// 通知辅助监听器
|
|
219
|
+
cameraChangeListeners.forEach { it.onCameraChangeFinish(cameraPosition) }
|
|
220
|
+
|
|
202
221
|
// 相机移动完成
|
|
203
222
|
cameraPosition?.let {
|
|
204
223
|
val visibleRegion = aMap.projection.visibleRegion
|
|
@@ -229,7 +248,13 @@ class ExpoGaodeMapView(context: Context, appContext: AppContext) : ExpoView(cont
|
|
|
229
248
|
|
|
230
249
|
// 设置全局 Marker 点击监听器
|
|
231
250
|
aMap.setOnMarkerClickListener { marker ->
|
|
232
|
-
MarkerView.handleMarkerClick(marker)
|
|
251
|
+
if (MarkerView.handleMarkerClick(marker)) {
|
|
252
|
+
return@setOnMarkerClickListener true
|
|
253
|
+
}
|
|
254
|
+
if (ClusterView.handleMarkerClick(marker)) {
|
|
255
|
+
return@setOnMarkerClickListener true
|
|
256
|
+
}
|
|
257
|
+
false
|
|
233
258
|
}
|
|
234
259
|
|
|
235
260
|
// 设置全局 Marker 拖拽监听器
|
|
@@ -247,6 +272,19 @@ class ExpoGaodeMapView(context: Context, appContext: AppContext) : ExpoView(cont
|
|
|
247
272
|
}
|
|
248
273
|
})
|
|
249
274
|
|
|
275
|
+
// 设置全局 MultiPoint 点击监听器
|
|
276
|
+
aMap.setOnMultiPointClickListener { item ->
|
|
277
|
+
for (i in 0 until childCount) {
|
|
278
|
+
val child = getChildAt(i)
|
|
279
|
+
if (child is MultiPointView) {
|
|
280
|
+
if (child.handleMultiPointClick(item)) {
|
|
281
|
+
return@setOnMultiPointClickListener true
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return@setOnMultiPointClickListener false
|
|
286
|
+
}
|
|
287
|
+
|
|
250
288
|
aMap.setOnMapClickListener { latLng ->
|
|
251
289
|
// 检查声明式 PolylineView
|
|
252
290
|
if (checkDeclarativePolylinePress(latLng)) {
|
|
@@ -465,6 +503,7 @@ class ExpoGaodeMapView(context: Context, appContext: AppContext) : ExpoView(cont
|
|
|
465
503
|
aMap.setOnCameraChangeListener(null)
|
|
466
504
|
aMap.setOnMarkerClickListener(null)
|
|
467
505
|
aMap.setOnMarkerDragListener(null)
|
|
506
|
+
aMap.setOnMultiPointClickListener(null)
|
|
468
507
|
|
|
469
508
|
// 清除所有覆盖物
|
|
470
509
|
aMap.clear()
|
|
@@ -1,53 +1,132 @@
|
|
|
1
1
|
package expo.modules.gaodemap.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
|
+
import expo.modules.gaodemap.utils.BitmapDescriptorCache
|
|
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.ExpoGaodeMapView
|
|
27
|
+
import expo.modules.gaodemap.utils.ColorParser
|
|
9
28
|
import expo.modules.kotlin.AppContext
|
|
10
29
|
import expo.modules.kotlin.viewevent.EventDispatcher
|
|
11
30
|
import expo.modules.kotlin.views.ExpoView
|
|
31
|
+
import kotlinx.coroutines.*
|
|
32
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
33
|
+
|
|
12
34
|
|
|
13
35
|
/**
|
|
14
36
|
* 点聚合视图
|
|
15
|
-
*
|
|
37
|
+
* 实现真正的点聚合逻辑,支持自定义样式和点击事件
|
|
16
38
|
*/
|
|
17
|
-
class ClusterView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
|
|
39
|
+
class ClusterView(context: Context, appContext: AppContext) : ExpoView(context, appContext), AMap.OnCameraChangeListener {
|
|
18
40
|
|
|
19
41
|
private val onPress by EventDispatcher()
|
|
20
42
|
@Suppress("unused")
|
|
21
43
|
private val onClusterPress by EventDispatcher()
|
|
22
44
|
|
|
23
45
|
private var aMap: AMap? = null
|
|
24
|
-
private var markers: MutableList<Marker> = mutableListOf()
|
|
25
|
-
private var points: List<Map<String, Any>> = emptyList()
|
|
26
|
-
@Suppress("unused")
|
|
27
|
-
private var radius: Int = 60
|
|
28
|
-
@Suppress("unused")
|
|
29
|
-
private var minClusterSize: Int = 2
|
|
30
46
|
|
|
47
|
+
// 聚合点数据
|
|
48
|
+
data class ClusterItem(
|
|
49
|
+
val latLng: LatLng,
|
|
50
|
+
val data: Map<String, Any>
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
// 聚合对象
|
|
54
|
+
class Cluster(val center: LatLng) {
|
|
55
|
+
val items = mutableListOf<ClusterItem>()
|
|
56
|
+
|
|
57
|
+
fun add(item: ClusterItem) {
|
|
58
|
+
items.add(item)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
val size: Int get() = items.size
|
|
62
|
+
val position: LatLng get() = center // 简单处理,使用中心点作为聚合点位置,也可以计算平均位置
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private var rawPoints: List<Map<String, Any>> = emptyList()
|
|
66
|
+
private var clusterItems: List<ClusterItem> = emptyList()
|
|
67
|
+
private var clusters: List<Cluster> = emptyList()
|
|
68
|
+
|
|
69
|
+
// 当前显示的 Markers
|
|
70
|
+
private val currentMarkers = mutableListOf<Marker>()
|
|
71
|
+
|
|
72
|
+
// 配置属性
|
|
73
|
+
private var radius: Int = 60 // dp
|
|
74
|
+
private var minClusterSize: Int = 1
|
|
75
|
+
|
|
76
|
+
// 样式属性
|
|
77
|
+
private var clusterStyle: Map<String, Any>? = null
|
|
78
|
+
private var clusterBuckets: List<Map<String, Any>>? = null
|
|
79
|
+
private var clusterTextStyle: Map<String, Any>? = null
|
|
80
|
+
|
|
81
|
+
private val mainHandler = Handler(Looper.getMainLooper())
|
|
82
|
+
|
|
83
|
+
// 协程作用域
|
|
84
|
+
private var scope = CoroutineScope(Dispatchers.Main + Job())
|
|
85
|
+
private var calculationJob: Job? = null
|
|
86
|
+
|
|
87
|
+
// 缓存 BitmapDescriptor
|
|
88
|
+
private val bitmapCache = ConcurrentHashMap<Int, BitmapDescriptor>()
|
|
89
|
+
private var currentIconDescriptor: BitmapDescriptor? = null
|
|
90
|
+
private var pendingIconUri: String? = null
|
|
91
|
+
|
|
31
92
|
/**
|
|
32
93
|
* 设置地图实例
|
|
33
94
|
*/
|
|
34
95
|
@Suppress("unused")
|
|
35
96
|
fun setMap(map: AMap) {
|
|
36
97
|
aMap = map
|
|
37
|
-
|
|
98
|
+
// 注册相机监听
|
|
99
|
+
// 注意:addView 时会自动调用 setMap,此时 parent 已设置
|
|
100
|
+
(parent as? ExpoGaodeMapView)?.addCameraChangeListener(this)
|
|
101
|
+
|
|
102
|
+
// 如果有待处理的 icon,加载它
|
|
103
|
+
pendingIconUri?.let {
|
|
104
|
+
loadAndSetIcon(it)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
updateClusters()
|
|
38
108
|
}
|
|
39
109
|
|
|
40
110
|
/**
|
|
41
111
|
* 设置聚合点数据
|
|
42
112
|
*/
|
|
43
113
|
fun setPoints(pointsList: List<Map<String, Any>>) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
114
|
+
rawPoints = pointsList
|
|
115
|
+
// 预处理数据
|
|
116
|
+
scope.launch(Dispatchers.Default) {
|
|
117
|
+
clusterItems = pointsList.mapNotNull { point: Map<String, Any> ->
|
|
118
|
+
val lat = (point["latitude"] as? Number)?.toDouble()
|
|
119
|
+
val lng = (point["longitude"] as? Number)?.toDouble()
|
|
120
|
+
if (lat != null && lng != null && lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180) {
|
|
121
|
+
ClusterItem(LatLng(lat, lng), point)
|
|
122
|
+
} else {
|
|
123
|
+
null
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
withContext(Dispatchers.Main) {
|
|
127
|
+
updateClusters()
|
|
128
|
+
}
|
|
49
129
|
}
|
|
50
|
-
createOrUpdateCluster()
|
|
51
130
|
}
|
|
52
131
|
|
|
53
132
|
/**
|
|
@@ -55,84 +134,371 @@ class ClusterView(context: Context, appContext: AppContext) : ExpoView(context,
|
|
|
55
134
|
*/
|
|
56
135
|
fun setRadius(radiusValue: Int) {
|
|
57
136
|
radius = radiusValue
|
|
58
|
-
|
|
137
|
+
updateClusters()
|
|
59
138
|
}
|
|
60
139
|
|
|
61
140
|
/**
|
|
62
141
|
* 设置最小聚合数量
|
|
63
142
|
*/
|
|
64
143
|
fun setMinClusterSize(size: Int) {
|
|
144
|
+
Log.d("ClusterView", "setMinClusterSize: $size")
|
|
65
145
|
minClusterSize = size
|
|
66
|
-
|
|
146
|
+
updateClusters()
|
|
67
147
|
}
|
|
68
148
|
|
|
69
149
|
/**
|
|
70
|
-
*
|
|
150
|
+
* 设置聚合样式
|
|
151
|
+
*/
|
|
152
|
+
fun setClusterStyle(style: Map<String, Any>) {
|
|
153
|
+
clusterStyle = style
|
|
154
|
+
bitmapCache.clear() // 样式改变,清除缓存
|
|
155
|
+
updateClusters()
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* 设置聚合文字样式
|
|
160
|
+
*/
|
|
161
|
+
fun setClusterTextStyle(style: Map<String, Any>) {
|
|
162
|
+
clusterTextStyle = style
|
|
163
|
+
bitmapCache.clear() // 样式改变,清除缓存
|
|
164
|
+
updateClusters()
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
fun setClusterBuckets(buckets: List<Map<String, Any>>) {
|
|
168
|
+
clusterBuckets = buckets
|
|
169
|
+
bitmapCache.clear()
|
|
170
|
+
updateClusters()
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 设置图标 (保留接口,目前主要使用 clusterStyle)
|
|
71
175
|
*/
|
|
72
176
|
@Suppress("UNUSED_PARAMETER")
|
|
73
177
|
fun setIcon(iconUri: String?) {
|
|
74
|
-
|
|
75
|
-
|
|
178
|
+
pendingIconUri = iconUri
|
|
179
|
+
if (iconUri != null) {
|
|
180
|
+
loadAndSetIcon(iconUri)
|
|
181
|
+
} else {
|
|
182
|
+
currentIconDescriptor = null
|
|
183
|
+
updateClusters()
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private fun loadAndSetIcon(iconUri: String) {
|
|
188
|
+
// 尝试从缓存获取
|
|
189
|
+
val cacheKey = "cluster|$iconUri"
|
|
190
|
+
BitmapDescriptorCache.get(cacheKey)?.let {
|
|
191
|
+
currentIconDescriptor = it
|
|
192
|
+
updateClusters()
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
scope.launch(Dispatchers.IO) {
|
|
197
|
+
try {
|
|
198
|
+
val descriptor = when {
|
|
199
|
+
iconUri.startsWith("http") -> {
|
|
200
|
+
val url = URL(iconUri)
|
|
201
|
+
val bitmap = BitmapFactory.decodeStream(url.openStream())
|
|
202
|
+
BitmapDescriptorFactory.fromBitmap(bitmap)
|
|
203
|
+
}
|
|
204
|
+
iconUri.startsWith("file://") -> {
|
|
205
|
+
val path = iconUri.substring(7)
|
|
206
|
+
BitmapDescriptorFactory.fromPath(path)
|
|
207
|
+
}
|
|
208
|
+
else -> {
|
|
209
|
+
// 尝试作为资源名称加载
|
|
210
|
+
val resId = context.resources.getIdentifier(iconUri, "drawable", context.packageName)
|
|
211
|
+
if (resId != 0) {
|
|
212
|
+
BitmapDescriptorFactory.fromResource(resId)
|
|
213
|
+
} else {
|
|
214
|
+
// 尝试作为普通文件路径
|
|
215
|
+
BitmapDescriptorFactory.fromPath(iconUri)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (descriptor != null) {
|
|
221
|
+
BitmapDescriptorCache.put(cacheKey, descriptor)
|
|
222
|
+
currentIconDescriptor = descriptor
|
|
223
|
+
withContext(Dispatchers.Main) {
|
|
224
|
+
updateClusters()
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
} catch (e: Exception) {
|
|
228
|
+
e.printStackTrace()
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* 相机移动回调
|
|
235
|
+
*/
|
|
236
|
+
override fun onCameraChange(cameraPosition: CameraPosition?) {
|
|
237
|
+
// 移动过程中不实时重新计算,以免性能问题
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
override fun onCameraChangeFinish(cameraPosition: CameraPosition?) {
|
|
241
|
+
updateClusters()
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
override fun onAttachedToWindow() {
|
|
245
|
+
super.onAttachedToWindow()
|
|
246
|
+
// 重新创建协程作用域(如果已被取消)
|
|
247
|
+
if (!scope.isActive) {
|
|
248
|
+
scope = CoroutineScope(Dispatchers.Main + Job())
|
|
249
|
+
}
|
|
250
|
+
// 重新注册监听器(防止因 detach 导致监听器丢失)
|
|
251
|
+
(parent as? ExpoGaodeMapView)?.addCameraChangeListener(this)
|
|
252
|
+
updateClusters()
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
override fun onDetachedFromWindow() {
|
|
256
|
+
super.onDetachedFromWindow()
|
|
257
|
+
scope.cancel() // 取消协程
|
|
258
|
+
(parent as? ExpoGaodeMapView)?.removeCameraChangeListener(this)
|
|
259
|
+
currentMarkers.forEach {
|
|
260
|
+
it.remove()
|
|
261
|
+
unregisterMarker(it)
|
|
262
|
+
}
|
|
263
|
+
currentMarkers.clear()
|
|
264
|
+
bitmapCache.clear()
|
|
76
265
|
}
|
|
77
266
|
|
|
78
267
|
/**
|
|
79
|
-
*
|
|
80
|
-
*
|
|
268
|
+
* 更新聚合
|
|
269
|
+
* 使用协程在后台计算
|
|
81
270
|
*/
|
|
82
|
-
private fun
|
|
83
|
-
aMap
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
271
|
+
private fun updateClusters() {
|
|
272
|
+
val map = aMap ?: return
|
|
273
|
+
|
|
274
|
+
// 取消上一次计算
|
|
275
|
+
calculationJob?.cancel()
|
|
276
|
+
|
|
277
|
+
// 确保 scope 处于活跃状态
|
|
278
|
+
if (!scope.isActive) {
|
|
279
|
+
scope = CoroutineScope(Dispatchers.Main + Job())
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
calculationJob = scope.launch(Dispatchers.Default) {
|
|
283
|
+
if (clusterItems.isEmpty()) return@launch
|
|
87
284
|
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
285
|
+
// 获取当前比例尺 (米/像素)
|
|
286
|
+
val scalePerPixel = withContext(Dispatchers.Main) {
|
|
287
|
+
// 增加安全性检查
|
|
288
|
+
if (map.mapType != 0) { // 简单检查 map 是否存活
|
|
289
|
+
map.scalePerPixel
|
|
290
|
+
} else {
|
|
291
|
+
0f
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (scalePerPixel <= 0) {
|
|
296
|
+
// 比例尺无效,稍后重试
|
|
297
|
+
withContext(Dispatchers.Main) {
|
|
298
|
+
android.util.Log.w("ClusterView", "Invalid scalePerPixel: $scalePerPixel, retrying...")
|
|
299
|
+
mainHandler.postDelayed({ updateClusters() }, 500)
|
|
300
|
+
}
|
|
301
|
+
return@launch
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// 计算聚合距离 (米)
|
|
305
|
+
// radius 是 dp,需要转 px,再转米
|
|
306
|
+
val density = context.resources.displayMetrics.density
|
|
307
|
+
val radiusPx = radius * density
|
|
308
|
+
val radiusMeters = radiusPx * scalePerPixel
|
|
309
|
+
|
|
310
|
+
val newClusters = mutableListOf<Cluster>()
|
|
311
|
+
val visited = BooleanArray(clusterItems.size)
|
|
312
|
+
|
|
313
|
+
// 简单的距离聚合算法
|
|
314
|
+
for (i in clusterItems.indices) {
|
|
315
|
+
if (visited[i]) continue
|
|
316
|
+
|
|
317
|
+
val item = clusterItems[i]
|
|
318
|
+
val cluster = Cluster(item.latLng)
|
|
319
|
+
cluster.add(item)
|
|
320
|
+
visited[i] = true
|
|
93
321
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
322
|
+
for (j in i + 1 until clusterItems.size) {
|
|
323
|
+
if (visited[j]) continue
|
|
324
|
+
|
|
325
|
+
val other = clusterItems[j]
|
|
326
|
+
val distance = AMapUtils.calculateLineDistance(item.latLng, other.latLng)
|
|
98
327
|
|
|
99
|
-
|
|
100
|
-
|
|
328
|
+
if (distance < radiusMeters) {
|
|
329
|
+
cluster.add(other)
|
|
330
|
+
visited[j] = true
|
|
331
|
+
}
|
|
101
332
|
}
|
|
333
|
+
|
|
334
|
+
newClusters.add(cluster)
|
|
102
335
|
}
|
|
103
336
|
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
337
|
+
// 更新 UI
|
|
338
|
+
withContext(Dispatchers.Main) {
|
|
339
|
+
renderClusters(newClusters)
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* 渲染聚合点
|
|
346
|
+
*/
|
|
347
|
+
private fun renderClusters(newClusters: List<Cluster>) {
|
|
348
|
+
Log.d("ClusterView", "renderClusters: count=${newClusters.size}, minClusterSize=$minClusterSize")
|
|
349
|
+
val map = aMap ?: return
|
|
350
|
+
clusters = newClusters
|
|
351
|
+
|
|
352
|
+
// 清除旧 Markers
|
|
353
|
+
currentMarkers.forEach {
|
|
354
|
+
it.remove()
|
|
355
|
+
unregisterMarker(it)
|
|
356
|
+
}
|
|
357
|
+
currentMarkers.clear()
|
|
358
|
+
|
|
359
|
+
// 添加新 Markers
|
|
360
|
+
newClusters.forEach { cluster ->
|
|
361
|
+
val markerOptions = MarkerOptions()
|
|
362
|
+
.position(cluster.center)
|
|
363
|
+
|
|
364
|
+
if (cluster.size >= minClusterSize) {
|
|
365
|
+
// 聚合点
|
|
366
|
+
markerOptions.icon(generateIcon(cluster.size))
|
|
367
|
+
markerOptions.zIndex(2.0f) // 聚合点层级高一点
|
|
368
|
+
} else {
|
|
369
|
+
// 单个点
|
|
370
|
+
markerOptions.icon(currentIconDescriptor ?: BitmapDescriptorFactory.defaultMarker())
|
|
371
|
+
markerOptions.zIndex(1.0f)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
val marker = map.addMarker(markerOptions)
|
|
375
|
+
if (marker != null) {
|
|
376
|
+
currentMarkers.add(marker)
|
|
377
|
+
registerMarker(marker, this)
|
|
378
|
+
// 存储关联的 cluster 数据,以便点击时获取
|
|
379
|
+
marker.setObject(cluster)
|
|
115
380
|
}
|
|
116
381
|
}
|
|
117
382
|
}
|
|
118
383
|
|
|
119
384
|
/**
|
|
120
|
-
*
|
|
385
|
+
* 生成聚合图标
|
|
386
|
+
*/
|
|
387
|
+
@SuppressLint("UseKtx")
|
|
388
|
+
private fun generateIcon(count: Int): BitmapDescriptor {
|
|
389
|
+
// 检查缓存
|
|
390
|
+
// 简单的缓存策略:只根据数量缓存。如果样式变化,会清空缓存。
|
|
391
|
+
bitmapCache[count]?.let { return it }
|
|
392
|
+
|
|
393
|
+
val density = context.resources.displayMetrics.density
|
|
394
|
+
|
|
395
|
+
// 获取样式配置
|
|
396
|
+
var activeStyle = clusterStyle ?: emptyMap()
|
|
397
|
+
|
|
398
|
+
clusterBuckets?.let { buckets ->
|
|
399
|
+
val bestBucket = buckets
|
|
400
|
+
.filter { (it["minPoints"] as? Number)?.toInt() ?: 0 <= count }
|
|
401
|
+
.maxByOrNull { (it["minPoints"] as? Number)?.toInt() ?: 0 }
|
|
402
|
+
|
|
403
|
+
if (bestBucket != null) {
|
|
404
|
+
activeStyle = activeStyle + bestBucket
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
val bgColorVal = activeStyle["backgroundColor"]
|
|
409
|
+
val borderColorVal = activeStyle["borderColor"]
|
|
410
|
+
val borderWidthVal = (activeStyle["borderWidth"] as? Number)?.toFloat() ?: 2f
|
|
411
|
+
val textSizeVal = (clusterTextStyle?.get("fontSize") as? Number)?.toFloat() ?: 14f
|
|
412
|
+
val textColorVal = clusterTextStyle?.get("color")
|
|
413
|
+
val fontWeightVal = clusterTextStyle?.get("fontWeight") as? String
|
|
414
|
+
|
|
415
|
+
// 解析颜色
|
|
416
|
+
val bgColor = ColorParser.parseColor(bgColorVal ?: "#F54531") // 默认红色
|
|
417
|
+
val borderColor = ColorParser.parseColor(borderColorVal ?: "#FFFFFF") // 默认白色
|
|
418
|
+
val textColor = ColorParser.parseColor(textColorVal ?: "#FFFFFF") // 默认白色
|
|
419
|
+
|
|
420
|
+
// 计算尺寸 (根据 iOS 逻辑:size = 30 + (count.toString().length - 1) * 5)
|
|
421
|
+
// 这里简单处理,或者根据 count 动态调整
|
|
422
|
+
// 基础大小 36dp
|
|
423
|
+
val baseSize = 36
|
|
424
|
+
val extraSize = (count.toString().length - 1) * 6
|
|
425
|
+
val sizeDp = baseSize + extraSize
|
|
426
|
+
val sizePx = (sizeDp * density).toInt()
|
|
427
|
+
|
|
428
|
+
val bitmap = Bitmap.createBitmap(sizePx, sizePx, Bitmap.Config.ARGB_8888)
|
|
429
|
+
val canvas = Canvas(bitmap)
|
|
430
|
+
|
|
431
|
+
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
|
432
|
+
val radius = sizePx / 2f
|
|
433
|
+
|
|
434
|
+
// 绘制边框
|
|
435
|
+
paint.color = borderColor
|
|
436
|
+
paint.style = Paint.Style.FILL
|
|
437
|
+
canvas.drawCircle(radius, radius, radius, paint)
|
|
438
|
+
|
|
439
|
+
// 绘制背景
|
|
440
|
+
paint.color = bgColor
|
|
441
|
+
val borderWidthPx = borderWidthVal * density
|
|
442
|
+
canvas.drawCircle(radius, radius, radius - borderWidthPx, paint)
|
|
443
|
+
|
|
444
|
+
// 绘制文字
|
|
445
|
+
paint.color = textColor
|
|
446
|
+
paint.textSize = textSizeVal * density
|
|
447
|
+
paint.textAlign = Paint.Align.CENTER
|
|
448
|
+
|
|
449
|
+
// 字体粗细
|
|
450
|
+
if (fontWeightVal == "bold") {
|
|
451
|
+
paint.typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// 文字垂直居中
|
|
455
|
+
val fontMetrics = paint.fontMetrics
|
|
456
|
+
val baseline = radius - (fontMetrics.bottom + fontMetrics.top) / 2
|
|
457
|
+
|
|
458
|
+
canvas.drawText(count.toString(), radius, baseline, paint)
|
|
459
|
+
|
|
460
|
+
val descriptor = BitmapDescriptorFactory.fromBitmap(bitmap)
|
|
461
|
+
bitmapCache[count] = descriptor
|
|
462
|
+
return descriptor
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* 处理 Marker 点击
|
|
121
467
|
*/
|
|
122
|
-
fun
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
468
|
+
fun onMarkerClick(marker: Marker) {
|
|
469
|
+
val cluster = marker.getObject() as? Cluster
|
|
470
|
+
if (cluster != null) {
|
|
471
|
+
// 无论聚合数量多少,统一触发 onClusterPress
|
|
472
|
+
// 这样保证用户在 React Native 端监听 onClusterPress 时总能收到事件
|
|
473
|
+
// 如果是单点,count 为 1,pois 包含单个点数据
|
|
474
|
+
val pointsData = cluster.items.map { it.data }
|
|
475
|
+
onClusterPress(mapOf(
|
|
476
|
+
"count" to cluster.size,
|
|
477
|
+
"latitude" to cluster.center.latitude,
|
|
478
|
+
"longitude" to cluster.center.longitude,
|
|
479
|
+
"pois" to pointsData,
|
|
480
|
+
"points" to pointsData // 兼容 iOS 或用户习惯
|
|
481
|
+
))
|
|
482
|
+
}
|
|
126
483
|
}
|
|
127
484
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
485
|
+
companion object {
|
|
486
|
+
private val markerMap = ConcurrentHashMap<Marker, ClusterView>()
|
|
487
|
+
|
|
488
|
+
fun registerMarker(marker: Marker, view: ClusterView) {
|
|
489
|
+
markerMap[marker] = view
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
fun unregisterMarker(marker: Marker) {
|
|
493
|
+
markerMap.remove(marker)
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
fun handleMarkerClick(marker: Marker): Boolean {
|
|
497
|
+
markerMap[marker]?.let { view ->
|
|
498
|
+
view.onMarkerClick(marker)
|
|
499
|
+
return true
|
|
135
500
|
}
|
|
501
|
+
return false
|
|
136
502
|
}
|
|
137
503
|
}
|
|
138
504
|
}
|
|
@@ -24,6 +24,22 @@ class ClusterViewModule : Module() {
|
|
|
24
24
|
Prop<Int>("minClusterSize") { view: ClusterView, size ->
|
|
25
25
|
view.setMinClusterSize(size)
|
|
26
26
|
}
|
|
27
|
+
|
|
28
|
+
Prop<Map<String, Any>>("clusterStyle") { view: ClusterView, style ->
|
|
29
|
+
view.setClusterStyle(style)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
Prop<List<Map<String, Any>>>("clusterBuckets") { view: ClusterView, buckets ->
|
|
33
|
+
view.setClusterBuckets(buckets)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
Prop<Map<String, Any>>("clusterTextStyle") { view: ClusterView, style ->
|
|
37
|
+
view.setClusterTextStyle(style)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
Prop<String>("icon") { view: ClusterView, icon ->
|
|
41
|
+
view.setIcon(icon)
|
|
42
|
+
}
|
|
27
43
|
}
|
|
28
44
|
}
|
|
29
45
|
}
|