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