expo-gaode-map-navigation 1.1.5-next.1 → 1.1.5-next.2
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 +10 -0
- package/android/src/main/cpp/CMakeLists.txt +24 -0
- package/android/src/main/cpp/cluster_jni.cpp +848 -0
- package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapModule.kt +616 -92
- package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapOfflineModule.kt +493 -0
- package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapView.kt +230 -14
- package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapViewModule.kt +37 -27
- package/android/src/main/java/expo/modules/gaodemap/map/MapPreloadManager.kt +494 -0
- package/android/src/main/java/expo/modules/gaodemap/map/companion/BitmapDescriptorCache.kt +30 -0
- package/android/src/main/java/expo/modules/gaodemap/map/companion/IconBitmapCache.kt +37 -0
- package/android/src/main/java/expo/modules/gaodemap/map/managers/UIManager.kt +76 -0
- package/android/src/main/java/expo/modules/gaodemap/map/modules/LocationManager.kt +15 -3
- package/android/src/main/java/expo/modules/gaodemap/map/modules/SDKInitializer.kt +4 -59
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/CircleView.kt +9 -12
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/CircleViewModule.kt +5 -6
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/ClusterView.kt +539 -66
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/ClusterViewModule.kt +17 -1
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/HeatMapView.kt +165 -33
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/HeatMapViewModule.kt +15 -3
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/MarkerView.kt +1249 -672
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/MarkerViewModule.kt +40 -17
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/MultiPointView.kt +177 -22
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/MultiPointViewModule.kt +11 -3
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/PolygonView.kt +57 -14
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/PolygonViewModule.kt +9 -5
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/PolylineView.kt +90 -63
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/PolylineViewModule.kt +7 -3
- package/android/src/main/java/expo/modules/gaodemap/map/services/LocationForegroundService.kt +3 -2
- package/android/src/main/java/expo/modules/gaodemap/map/utils/BitmapDescriptorCache.kt +20 -0
- package/android/src/main/java/expo/modules/gaodemap/map/utils/ClusterNative.kt +13 -0
- package/android/src/main/java/expo/modules/gaodemap/map/utils/ColorParser.kt +20 -0
- package/android/src/main/java/expo/modules/gaodemap/map/utils/GeometryUtils.kt +515 -0
- package/android/src/main/java/expo/modules/gaodemap/map/utils/LatLngParser.kt +91 -0
- package/android/src/main/java/expo/modules/gaodemap/map/utils/PermissionHelper.kt +248 -0
- package/build/ExpoGaodeMapNaviView.d.ts +7 -7
- package/build/ExpoGaodeMapNaviView.js +8 -8
- package/build/index.d.ts +1 -1
- package/build/index.js +2 -2
- package/build/map/ExpoGaodeMapModule.d.ts +2 -201
- package/build/map/ExpoGaodeMapModule.js +584 -14
- package/build/map/ExpoGaodeMapOfflineModule.d.ts +139 -0
- package/build/map/ExpoGaodeMapOfflineModule.js +8 -0
- package/build/map/ExpoGaodeMapView.js +66 -58
- package/build/map/components/FoldableMapView.d.ts +38 -0
- package/build/map/components/FoldableMapView.js +209 -0
- package/build/map/components/MapContext.d.ts +12 -0
- package/build/map/components/MapContext.js +54 -0
- package/build/map/components/MapUI.d.ts +18 -0
- package/build/map/components/MapUI.js +29 -0
- package/build/map/components/overlays/Circle.js +34 -3
- package/build/map/components/overlays/Cluster.d.ts +3 -1
- package/build/map/components/overlays/Cluster.js +31 -2
- package/build/map/components/overlays/HeatMap.d.ts +3 -1
- package/build/map/components/overlays/HeatMap.js +33 -3
- package/build/map/components/overlays/Marker.d.ts +1 -1
- package/build/map/components/overlays/Marker.js +37 -32
- package/build/map/components/overlays/MultiPoint.js +1 -1
- package/build/map/components/overlays/Polygon.js +30 -3
- package/build/map/components/overlays/Polyline.js +36 -3
- package/build/map/index.d.ts +25 -5
- package/build/map/index.js +59 -18
- package/build/map/types/common.types.d.ts +40 -0
- package/build/map/types/common.types.js +0 -4
- package/build/map/types/index.d.ts +3 -2
- package/build/map/types/map-view.types.d.ts +108 -3
- package/build/map/types/native-module.types.d.ts +363 -0
- package/build/map/types/native-module.types.js +5 -0
- package/build/map/types/offline.types.d.ts +132 -0
- package/build/map/types/offline.types.js +5 -0
- package/build/map/types/overlays.types.d.ts +137 -24
- package/build/map/utils/ErrorHandler.d.ts +110 -0
- package/build/map/utils/ErrorHandler.js +421 -0
- package/build/map/utils/GeoUtils.d.ts +20 -0
- package/build/map/utils/GeoUtils.js +76 -0
- package/build/map/utils/OfflineMapManager.d.ts +148 -0
- package/build/map/utils/OfflineMapManager.js +217 -0
- package/build/map/utils/PermissionUtils.d.ts +91 -0
- package/build/map/utils/PermissionUtils.js +255 -0
- package/build/map/utils/PlatformDetector.d.ts +102 -0
- package/build/map/utils/PlatformDetector.js +186 -0
- package/build/types/naviview.types.d.ts +1 -1
- package/expo-module.config.json +12 -10
- package/ios/ExpoGaodeMapNavigation.podspec +9 -0
- package/ios/map/ExpoGaodeMapModule.swift +485 -75
- package/ios/map/ExpoGaodeMapOfflineModule.swift +479 -0
- package/ios/map/ExpoGaodeMapView.swift +611 -62
- package/ios/map/ExpoGaodeMapViewModule.swift +48 -26
- package/ios/map/MapPreloadManager.swift +348 -0
- package/ios/map/cpp/ClusterEngine.cpp +110 -0
- package/ios/map/cpp/ClusterEngine.hpp +20 -0
- package/ios/map/cpp/ColorParser.cpp +135 -0
- package/ios/map/cpp/ColorParser.hpp +14 -0
- package/ios/map/cpp/GeometryEngine.cpp +574 -0
- package/ios/map/cpp/GeometryEngine.hpp +159 -0
- package/ios/map/cpp/QuadTree.cpp +92 -0
- package/ios/map/cpp/QuadTree.hpp +42 -0
- package/ios/map/cpp/README.md +55 -0
- package/ios/map/cpp/tests/benchmark_js.js +41 -0
- package/ios/map/cpp/tests/run.sh +17 -0
- package/ios/map/cpp/tests/test_main.cpp +276 -0
- package/ios/map/managers/UIManager.swift +72 -1
- package/ios/map/modules/LocationManager.swift +114 -165
- package/ios/map/overlays/CircleView.swift +16 -32
- package/ios/map/overlays/CircleViewModule.swift +12 -12
- package/ios/map/overlays/ClusterAnnotation.swift +32 -0
- package/ios/map/overlays/ClusterView.swift +331 -45
- package/ios/map/overlays/ClusterViewModule.swift +20 -6
- package/ios/map/overlays/HeatMapView.swift +135 -32
- package/ios/map/overlays/HeatMapViewModule.swift +20 -8
- package/ios/map/overlays/MarkerView.swift +613 -130
- package/ios/map/overlays/MarkerViewModule.swift +38 -18
- package/ios/map/overlays/MultiPointView.swift +168 -10
- package/ios/map/overlays/MultiPointViewModule.swift +27 -5
- package/ios/map/overlays/PolygonView.swift +62 -23
- package/ios/map/overlays/PolygonViewModule.swift +18 -12
- package/ios/map/overlays/PolylineView.swift +21 -13
- package/ios/map/overlays/PolylineViewModule.swift +18 -12
- package/ios/map/utils/ClusterNative.h +96 -0
- package/ios/map/utils/ClusterNative.mm +377 -0
- package/ios/map/utils/ColorParser.swift +12 -1
- package/ios/map/utils/CppBridging.mm +13 -0
- package/ios/map/utils/GeometryUtils.swift +34 -0
- package/ios/map/utils/LatLngParser.swift +87 -0
- package/ios/map/utils/PermissionManager.swift +135 -6
- package/package.json +1 -1
- package/build/map/ExpoGaodeMap.types.d.ts +0 -41
- package/build/map/ExpoGaodeMap.types.js +0 -24
- package/build/map/utils/EventManager.d.ts +0 -10
- package/build/map/utils/EventManager.js +0 -26
- package/build/map/utils/ModuleLoader.d.ts +0 -73
- 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
|
-
*
|
|
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
|
-
|
|
43
|
+
|
|
21
44
|
@Suppress("unused")
|
|
22
45
|
private val onClusterPress by EventDispatcher()
|
|
23
46
|
|
|
24
47
|
private var aMap: AMap? = null
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
84
|
-
aMap
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
}
|