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.
- package/README.md +213 -73
- 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 +10 -11
- package/build/ExpoGaodeMapNavigationModule.d.ts +2 -1
- package/build/index.d.ts +35 -33
- package/build/index.js +70 -106
- package/build/map/ExpoGaodeMapModule.d.ts +2 -201
- package/build/map/ExpoGaodeMapModule.js +586 -18
- 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/index.d.ts +1 -0
- package/build/types/index.js +1 -0
- package/build/types/native-module.types.d.ts +69 -0
- package/build/types/native-module.types.js +2 -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/managers/UIManager.swift +72 -1
- package/ios/map/modules/LocationManager.swift +123 -166
- 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 +3 -2
- package/shared/cpp/ClusterEngine.cpp +110 -0
- package/shared/cpp/ClusterEngine.hpp +20 -0
- package/shared/cpp/ColorParser.cpp +135 -0
- package/shared/cpp/ColorParser.hpp +14 -0
- package/shared/cpp/GeometryEngine.cpp +574 -0
- package/shared/cpp/GeometryEngine.hpp +159 -0
- package/shared/cpp/QuadTree.cpp +92 -0
- package/shared/cpp/QuadTree.hpp +42 -0
- package/shared/cpp/README.md +55 -0
- package/shared/cpp/tests/benchmark_js.js +41 -0
- package/shared/cpp/tests/run.sh +17 -0
- package/shared/cpp/tests/test_main.cpp +276 -0
- 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,85 +1,371 @@
|
|
|
1
1
|
import ExpoModulesCore
|
|
2
2
|
import AMapNaviKit
|
|
3
3
|
|
|
4
|
-
class
|
|
5
|
-
|
|
6
|
-
var
|
|
7
|
-
|
|
4
|
+
class ClusterView: ExpoView {
|
|
5
|
+
// 属性
|
|
6
|
+
var points: [[String: Any]] = [] {
|
|
7
|
+
didSet {
|
|
8
|
+
parsePoints()
|
|
9
|
+
updateClusters()
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
var radius: Int = 100 // 聚合范围 (screen points)
|
|
13
|
+
var minClusterSize: Int = 1
|
|
14
|
+
var clusterBuckets: [[String: Any]]?
|
|
15
|
+
|
|
16
|
+
// 样式属性
|
|
17
|
+
private var clusterBackgroundColor: UIColor = .systemBlue
|
|
18
|
+
private var clusterBorderColor: UIColor = .white
|
|
19
|
+
private var clusterBorderWidth: CGFloat = 2.0
|
|
20
|
+
private var clusterTextColor: UIColor = .white
|
|
21
|
+
private var clusterTextSize: CGFloat = 14.0
|
|
22
|
+
private var clusterSize: CGSize = CGSize(width: 40, height: 40)
|
|
8
23
|
|
|
9
|
-
|
|
10
|
-
|
|
24
|
+
let onClusterPress = EventDispatcher()
|
|
25
|
+
|
|
26
|
+
private weak var mapView: MAMapView?
|
|
27
|
+
// private var quadTree = CoordinateQuadTree() // Removed: using C++ ClusterNative
|
|
28
|
+
private var currentAnnotations: [MAAnnotation] = []
|
|
29
|
+
private let quadTreeQueue = DispatchQueue(label: "com.expo.gaode.quadtree")
|
|
30
|
+
private var isInvalidated = false
|
|
31
|
+
|
|
32
|
+
// 缓存坐标数据以加速 C++ 调用
|
|
33
|
+
private var latitudes: [Double] = []
|
|
34
|
+
private var longitudes: [Double] = []
|
|
11
35
|
|
|
12
36
|
required init(appContext: AppContext? = nil) {
|
|
13
37
|
super.init(appContext: appContext)
|
|
14
38
|
}
|
|
15
39
|
|
|
16
|
-
func
|
|
17
|
-
|
|
18
|
-
|
|
40
|
+
private func parsePoints() {
|
|
41
|
+
var lats: [Double] = []
|
|
42
|
+
var lons: [Double] = []
|
|
43
|
+
|
|
44
|
+
for point in points {
|
|
45
|
+
if let coord = LatLngParser.parseLatLng(point) {
|
|
46
|
+
lats.append(coord.latitude)
|
|
47
|
+
lons.append(coord.longitude)
|
|
48
|
+
} else {
|
|
49
|
+
// 保持索引一致,无效点填 0
|
|
50
|
+
lats.append(0)
|
|
51
|
+
lons.append(0)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
self.latitudes = lats
|
|
56
|
+
self.longitudes = lons
|
|
19
57
|
}
|
|
20
58
|
|
|
59
|
+
// MARK: - Setters for Expo Module
|
|
60
|
+
|
|
21
61
|
func setPoints(_ points: [[String: Any]]) {
|
|
22
62
|
self.points = points
|
|
23
|
-
updateCluster()
|
|
24
63
|
}
|
|
25
64
|
|
|
26
65
|
func setRadius(_ radius: Int) {
|
|
27
66
|
self.radius = radius
|
|
28
|
-
|
|
67
|
+
updateClusters()
|
|
29
68
|
}
|
|
30
69
|
|
|
31
70
|
func setMinClusterSize(_ size: Int) {
|
|
32
71
|
self.minClusterSize = size
|
|
33
|
-
|
|
72
|
+
updateClusters()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
func setMap(_ map: MAMapView) {
|
|
76
|
+
self.mapView = map
|
|
77
|
+
updateClusters()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
func setClusterStyle(_ style: [String: Any]) {
|
|
81
|
+
if let color = ColorParser.parseColor(style["backgroundColor"]) {
|
|
82
|
+
self.clusterBackgroundColor = color
|
|
83
|
+
}
|
|
84
|
+
if let borderColor = ColorParser.parseColor(style["borderColor"]) {
|
|
85
|
+
self.clusterBorderColor = borderColor
|
|
86
|
+
}
|
|
87
|
+
if let borderWidth = style["borderWidth"] as? Double {
|
|
88
|
+
self.clusterBorderWidth = CGFloat(borderWidth)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 尺寸设置
|
|
92
|
+
if let width = style["width"] as? Double {
|
|
93
|
+
self.clusterSize.width = CGFloat(width)
|
|
94
|
+
// 如果只设置了宽度,默认高度等于宽度(正圆)
|
|
95
|
+
if style["height"] == nil {
|
|
96
|
+
self.clusterSize.height = CGFloat(width)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if let height = style["height"] as? Double {
|
|
101
|
+
self.clusterSize.height = CGFloat(height)
|
|
102
|
+
// 如果只设置了高度,默认宽度等于高度
|
|
103
|
+
if style["width"] == nil {
|
|
104
|
+
self.clusterSize.width = CGFloat(height)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
updateClusters()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
func setClusterTextStyle(_ style: [String: Any]) {
|
|
112
|
+
if let color = ColorParser.parseColor(style["color"]) {
|
|
113
|
+
self.clusterTextColor = color
|
|
114
|
+
}
|
|
115
|
+
if let fontSize = style["fontSize"] as? Double {
|
|
116
|
+
self.clusterTextSize = CGFloat(fontSize)
|
|
117
|
+
}
|
|
118
|
+
updateClusters()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
func setClusterBuckets(_ buckets: [[String: Any]]) {
|
|
122
|
+
self.clusterBuckets = buckets
|
|
123
|
+
updateClusters()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
func mapRegionDidChange() {
|
|
127
|
+
updateClusters()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// MARK: - Update Logic
|
|
131
|
+
|
|
132
|
+
private var updateTimer: Timer?
|
|
133
|
+
private let throttleInterval: TimeInterval = 0.3 // 300ms 节流
|
|
134
|
+
|
|
135
|
+
func updateClusters() {
|
|
136
|
+
if isInvalidated { return }
|
|
137
|
+
|
|
138
|
+
// 节流逻辑:取消上一次的 Timer,重新计时
|
|
139
|
+
updateTimer?.invalidate()
|
|
140
|
+
updateTimer = Timer.scheduledTimer(withTimeInterval: throttleInterval, repeats: false) { [weak self] _ in
|
|
141
|
+
self?.performUpdate()
|
|
142
|
+
}
|
|
34
143
|
}
|
|
35
144
|
|
|
36
|
-
private func
|
|
145
|
+
private func performUpdate() {
|
|
146
|
+
if isInvalidated { return }
|
|
37
147
|
guard let mapView = mapView else { return }
|
|
38
148
|
|
|
39
|
-
//
|
|
40
|
-
|
|
149
|
+
// 确保地图已布局
|
|
150
|
+
if mapView.bounds.size.width == 0 { return }
|
|
41
151
|
|
|
42
|
-
|
|
43
|
-
|
|
152
|
+
let visibleRect = mapView.visibleMapRect
|
|
153
|
+
let zoomScale = visibleRect.size.width / Double(mapView.bounds.size.width)
|
|
44
154
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
155
|
+
// 计算当前缩放级别下的物理半径(米)
|
|
156
|
+
// MAMapView 单位投影:1 map point ≈ 1 meter (at equator)
|
|
157
|
+
// 实际上需要根据纬度计算 metersPerMapPoint
|
|
158
|
+
let centerLat = mapView.centerCoordinate.latitude
|
|
159
|
+
let metersPerMapPoint = MAMetersPerMapPointAtLatitude(centerLat)
|
|
160
|
+
let mapPointsPerScreenPoint = zoomScale
|
|
161
|
+
let metersPerScreenPoint = metersPerMapPoint * mapPointsPerScreenPoint
|
|
162
|
+
let radiusMeters = Double(self.radius) * metersPerScreenPoint
|
|
163
|
+
|
|
164
|
+
// 在后台串行队列计算聚合
|
|
165
|
+
quadTreeQueue.async { [weak self] in
|
|
166
|
+
guard let self = self else { return }
|
|
167
|
+
|
|
168
|
+
// 转换为 NSNumber 数组以传递给 Obj-C++
|
|
169
|
+
let latNums = self.latitudes.map { NSNumber(value: $0) }
|
|
170
|
+
let lonNums = self.longitudes.map { NSNumber(value: $0) }
|
|
171
|
+
|
|
172
|
+
// 调用 C++ 聚类算法
|
|
173
|
+
let clusterData = ClusterNative.clusterPoints(latitudes: latNums, longitudes: lonNums, radiusMeters: radiusMeters)
|
|
174
|
+
|
|
175
|
+
var annotations: [ClusterAnnotation] = []
|
|
176
|
+
|
|
177
|
+
if clusterData.count > 0 {
|
|
178
|
+
let clusterCount = clusterData[0].intValue
|
|
179
|
+
var offset = 1
|
|
180
|
+
|
|
181
|
+
for _ in 0..<clusterCount {
|
|
182
|
+
if offset >= clusterData.count { break }
|
|
183
|
+
|
|
184
|
+
let centerIndex = clusterData[offset].intValue
|
|
185
|
+
offset += 1
|
|
186
|
+
|
|
187
|
+
let count = clusterData[offset].intValue
|
|
188
|
+
offset += 1
|
|
189
|
+
|
|
190
|
+
var pois: [[String: Any]] = []
|
|
191
|
+
|
|
192
|
+
for _ in 0..<count {
|
|
193
|
+
if offset < clusterData.count {
|
|
194
|
+
let idx = clusterData[offset].intValue
|
|
195
|
+
if idx >= 0 && idx < self.points.count {
|
|
196
|
+
pois.append(self.points[idx])
|
|
197
|
+
}
|
|
198
|
+
offset += 1
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if centerIndex >= 0 && centerIndex < self.points.count {
|
|
203
|
+
let centerPoint = self.points[centerIndex]
|
|
204
|
+
if let lat = centerPoint["latitude"] as? Double,
|
|
205
|
+
let lon = centerPoint["longitude"] as? Double {
|
|
206
|
+
let coordinate = CLLocationCoordinate2D(latitude: lat, longitude: lon)
|
|
207
|
+
// 只有当聚类点数量 >= minClusterSize 时才显示聚类
|
|
208
|
+
// 但通常 ClusterView 负责显示所有点(聚合或非聚合)
|
|
209
|
+
// 这里如果 count == 1,也是一个 ClusterAnnotation,只是显示样式可能不同
|
|
210
|
+
let annotation = ClusterAnnotation(coordinate: coordinate, count: count, pois: pois)
|
|
211
|
+
annotations.append(annotation)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
49
215
|
}
|
|
50
216
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
217
|
+
DispatchQueue.main.async {
|
|
218
|
+
if self.isInvalidated { return }
|
|
219
|
+
self.updateMapViewAnnotations(with: annotations as [MAAnnotation])
|
|
220
|
+
}
|
|
55
221
|
}
|
|
56
222
|
}
|
|
57
223
|
|
|
58
|
-
|
|
59
|
-
* 移除所有标注
|
|
60
|
-
*/
|
|
61
|
-
private func removeAllAnnotations() {
|
|
224
|
+
private func updateMapViewAnnotations(with newAnnotations: [MAAnnotation]) {
|
|
62
225
|
guard let mapView = mapView else { return }
|
|
63
226
|
|
|
64
|
-
|
|
65
|
-
|
|
227
|
+
// Diff 算法:找出新增、移除和保留的标注
|
|
228
|
+
// 注意:ClusterAnnotation 需要实现 isEqual 和 hash
|
|
229
|
+
|
|
230
|
+
let before = Set(currentAnnotations.compactMap { $0 as? ClusterAnnotation })
|
|
231
|
+
let after = Set(newAnnotations.compactMap { $0 as? ClusterAnnotation })
|
|
232
|
+
|
|
233
|
+
// intersection 返回 before 中的元素(如果相等),这正是我们需要保留的已经在地图上的实例
|
|
234
|
+
let toKeep = before.intersection(after)
|
|
235
|
+
let toAdd = after.subtracting(toKeep)
|
|
236
|
+
let toRemove = before.subtracting(toKeep)
|
|
237
|
+
|
|
238
|
+
// 只有当有变化时才操作
|
|
239
|
+
if !toRemove.isEmpty {
|
|
240
|
+
mapView.removeAnnotations(Array(toRemove))
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if !toAdd.isEmpty {
|
|
244
|
+
mapView.addAnnotations(Array(toAdd))
|
|
66
245
|
}
|
|
67
|
-
|
|
246
|
+
|
|
247
|
+
// 更新 currentAnnotations
|
|
248
|
+
// 关键:必须保留已经在地图上的实例 (toKeep),加上新增的实例 (toAdd)
|
|
249
|
+
// 这样可以保证 currentAnnotations 中的对象始终与地图上的对象一致,避免 KVO 崩溃
|
|
250
|
+
var nextAnnotations: [MAAnnotation] = []
|
|
251
|
+
nextAnnotations.append(contentsOf: Array(toKeep) as [MAAnnotation])
|
|
252
|
+
nextAnnotations.append(contentsOf: Array(toAdd) as [MAAnnotation])
|
|
253
|
+
|
|
254
|
+
currentAnnotations = nextAnnotations
|
|
68
255
|
}
|
|
69
256
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
257
|
+
// MARK: - View Provider
|
|
258
|
+
|
|
259
|
+
func viewForAnnotation(_ annotation: MAAnnotation) -> MAAnnotationView? {
|
|
260
|
+
guard let clusterAnnotation = annotation as? ClusterAnnotation else { return nil }
|
|
261
|
+
|
|
262
|
+
let reuseIdentifier = "ClusterAnnotation"
|
|
263
|
+
var annotationView = mapView?.dequeueReusableAnnotationView(withIdentifier: reuseIdentifier)
|
|
264
|
+
|
|
265
|
+
if annotationView == nil {
|
|
266
|
+
annotationView = MAAnnotationView(annotation: annotation, reuseIdentifier: reuseIdentifier)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
annotationView?.annotation = annotation
|
|
270
|
+
annotationView?.canShowCallout = false
|
|
271
|
+
|
|
272
|
+
// 生成图标
|
|
273
|
+
annotationView?.image = image(for: clusterAnnotation.count)
|
|
274
|
+
annotationView?.centerOffset = CGPoint(x: 0, y: 0)
|
|
275
|
+
annotationView?.zIndex = 100
|
|
276
|
+
|
|
277
|
+
return annotationView
|
|
76
278
|
}
|
|
77
279
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
280
|
+
private func image(for count: Int) -> UIImage? {
|
|
281
|
+
let size = self.clusterSize
|
|
282
|
+
let renderer = UIGraphicsImageRenderer(size: size)
|
|
283
|
+
|
|
284
|
+
return renderer.image { context in
|
|
285
|
+
let rect = CGRect(origin: .zero, size: size)
|
|
286
|
+
|
|
287
|
+
// 基础样式
|
|
288
|
+
var bgColor = self.clusterBackgroundColor
|
|
289
|
+
var borderColor = self.clusterBorderColor
|
|
290
|
+
var borderWidth = self.clusterBorderWidth
|
|
291
|
+
|
|
292
|
+
// 应用分级样式
|
|
293
|
+
if let buckets = self.clusterBuckets {
|
|
294
|
+
var bestBucket: [String: Any]?
|
|
295
|
+
var maxMinPoints = -1
|
|
296
|
+
|
|
297
|
+
for bucket in buckets {
|
|
298
|
+
if let minPoints = bucket["minPoints"] as? Int, minPoints <= count {
|
|
299
|
+
if minPoints > maxMinPoints {
|
|
300
|
+
maxMinPoints = minPoints
|
|
301
|
+
bestBucket = bucket
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if let bucket = bestBucket {
|
|
307
|
+
if let c = ColorParser.parseColor(bucket["backgroundColor"]) {
|
|
308
|
+
bgColor = c
|
|
309
|
+
}
|
|
310
|
+
if let c = ColorParser.parseColor(bucket["borderColor"]) {
|
|
311
|
+
borderColor = c
|
|
312
|
+
}
|
|
313
|
+
if let w = bucket["borderWidth"] as? Double {
|
|
314
|
+
borderWidth = CGFloat(w)
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
bgColor.setFill()
|
|
320
|
+
UIBezierPath(ovalIn: rect).fill()
|
|
321
|
+
|
|
322
|
+
// 绘制边框
|
|
323
|
+
if borderWidth > 0 {
|
|
324
|
+
borderColor.setStroke()
|
|
325
|
+
let path = UIBezierPath(ovalIn: rect.insetBy(dx: borderWidth / 2, dy: borderWidth / 2))
|
|
326
|
+
path.lineWidth = borderWidth
|
|
327
|
+
path.stroke()
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// 绘制文字
|
|
331
|
+
let text = "\(count)"
|
|
332
|
+
let attributes: [NSAttributedString.Key: Any] = [
|
|
333
|
+
.font: UIFont.boldSystemFont(ofSize: self.clusterTextSize),
|
|
334
|
+
.foregroundColor: self.clusterTextColor
|
|
335
|
+
]
|
|
336
|
+
let textSize = text.size(withAttributes: attributes)
|
|
337
|
+
let textRect = CGRect(x: (size.width - textSize.width) / 2,
|
|
338
|
+
y: (size.height - textSize.height) / 2,
|
|
339
|
+
width: textSize.width,
|
|
340
|
+
height: textSize.height)
|
|
341
|
+
text.draw(in: textRect, withAttributes: attributes)
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// MARK: - Event Handling
|
|
346
|
+
|
|
347
|
+
func containsAnnotation(_ annotation: MAAnnotation) -> Bool {
|
|
348
|
+
guard let clusterAnnotation = annotation as? ClusterAnnotation else { return false }
|
|
349
|
+
return currentAnnotations.contains { $0.isEqual(clusterAnnotation) }
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
func handleAnnotationTap(_ annotation: MAAnnotation) {
|
|
353
|
+
guard let clusterAnnotation = annotation as? ClusterAnnotation else { return }
|
|
354
|
+
|
|
355
|
+
onClusterPress([
|
|
356
|
+
"count": clusterAnnotation.count,
|
|
357
|
+
"latitude": clusterAnnotation.coordinate.latitude,
|
|
358
|
+
"longitude": clusterAnnotation.coordinate.longitude,
|
|
359
|
+
"pois": clusterAnnotation.pois
|
|
360
|
+
])
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// MARK: - Lifecycle
|
|
364
|
+
|
|
365
|
+
override func removeFromSuperview() {
|
|
366
|
+
isInvalidated = true
|
|
367
|
+
updateTimer?.invalidate() // 清理定时器
|
|
368
|
+
super.removeFromSuperview()
|
|
369
|
+
mapView?.removeAnnotations(currentAnnotations)
|
|
84
370
|
}
|
|
85
|
-
}
|
|
371
|
+
}
|
|
@@ -1,21 +1,35 @@
|
|
|
1
1
|
import ExpoModulesCore
|
|
2
2
|
|
|
3
|
-
public class
|
|
3
|
+
public class ClusterViewModule: Module {
|
|
4
4
|
public func definition() -> ModuleDefinition {
|
|
5
|
-
Name("
|
|
5
|
+
Name("ClusterView")
|
|
6
6
|
|
|
7
|
-
View(
|
|
8
|
-
|
|
7
|
+
View(ClusterView.self) {
|
|
8
|
+
Events("onClusterPress")
|
|
9
|
+
|
|
10
|
+
Prop("points") { (view: ClusterView, points: [[String: Any]]) in
|
|
9
11
|
view.setPoints(points)
|
|
10
12
|
}
|
|
11
13
|
|
|
12
|
-
Prop("radius") { (view:
|
|
14
|
+
Prop("radius") { (view: ClusterView, radius: Int) in
|
|
13
15
|
view.setRadius(radius)
|
|
14
16
|
}
|
|
15
17
|
|
|
16
|
-
Prop("minClusterSize") { (view:
|
|
18
|
+
Prop("minClusterSize") { (view: ClusterView, size: Int) in
|
|
17
19
|
view.setMinClusterSize(size)
|
|
18
20
|
}
|
|
21
|
+
|
|
22
|
+
Prop("clusterStyle") { (view: ClusterView, style: [String: Any]) in
|
|
23
|
+
view.setClusterStyle(style)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
Prop("clusterTextStyle") { (view: ClusterView, style: [String: Any]) in
|
|
27
|
+
view.setClusterTextStyle(style)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
Prop("clusterBuckets") { (view: ClusterView, buckets: [[String: Any]]) in
|
|
31
|
+
view.setClusterBuckets(buckets)
|
|
32
|
+
}
|
|
19
33
|
}
|
|
20
34
|
}
|
|
21
35
|
}
|