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.
Files changed (33) hide show
  1. package/android/build.gradle +1 -1
  2. package/android/src/main/java/expo/modules/gaodemap/ExpoGaodeMapView.kt +40 -1
  3. package/android/src/main/java/expo/modules/gaodemap/overlays/ClusterView.kt +427 -61
  4. package/android/src/main/java/expo/modules/gaodemap/overlays/ClusterViewModule.kt +16 -0
  5. package/android/src/main/java/expo/modules/gaodemap/overlays/HeatMapView.kt +160 -25
  6. package/android/src/main/java/expo/modules/gaodemap/overlays/HeatMapViewModule.kt +13 -1
  7. package/android/src/main/java/expo/modules/gaodemap/overlays/MultiPointView.kt +165 -13
  8. package/android/src/main/java/expo/modules/gaodemap/overlays/MultiPointViewModule.kt +9 -1
  9. package/android/src/main/java/expo/modules/gaodemap/utils/BitmapDescriptorCache.kt +20 -0
  10. package/build/components/overlays/Cluster.d.ts.map +1 -1
  11. package/build/components/overlays/Cluster.js +6 -2
  12. package/build/components/overlays/Cluster.js.map +1 -1
  13. package/build/components/overlays/HeatMap.d.ts.map +1 -1
  14. package/build/components/overlays/HeatMap.js +12 -1
  15. package/build/components/overlays/HeatMap.js.map +1 -1
  16. package/build/index.d.ts +0 -1
  17. package/build/index.d.ts.map +1 -1
  18. package/build/index.js.map +1 -1
  19. package/build/types/overlays.types.d.ts +69 -14
  20. package/build/types/overlays.types.d.ts.map +1 -1
  21. package/build/types/overlays.types.js.map +1 -1
  22. package/build/utils/ModuleLoader.js +1 -1
  23. package/build/utils/ModuleLoader.js.map +1 -1
  24. package/ios/ExpoGaodeMapView.swift +44 -0
  25. package/ios/overlays/ClusterAnnotation.swift +32 -0
  26. package/ios/overlays/ClusterView.swift +251 -45
  27. package/ios/overlays/ClusterViewModule.swift +14 -0
  28. package/ios/overlays/CoordinateQuadTree.swift +291 -0
  29. package/ios/overlays/HeatMapView.swift +119 -5
  30. package/ios/overlays/HeatMapViewModule.swift +13 -1
  31. package/ios/overlays/MultiPointView.swift +160 -2
  32. package/ios/overlays/MultiPointViewModule.swift +22 -0
  33. package/package.json +1 -1
@@ -16,14 +16,34 @@ class HeatMapView: ExpoView {
16
16
  var radius: Int = 50
17
17
  /// 透明度
18
18
  var opacity: Double = 0.6
19
+ /// 渐变配置
20
+ var gradient: [String: Any]?
21
+ /// 是否开启高清适配
22
+ var allowRetinaAdapting: Bool = false
23
+
24
+ private var visible: Bool = true
19
25
 
20
26
  /// 地图视图弱引用
21
27
  private var mapView: MAMapView?
22
28
  /// 热力图图层
23
- private var heatmapOverlay: MAHeatMapTileOverlay?
29
+ var heatmapOverlay: MAHeatMapTileOverlay?
30
+ /// 渲染器
31
+ private var renderer: MATileOverlayRenderer?
32
+
33
+ private func reloadRenderer() {
34
+ guard let mapView = mapView, let overlay = heatmapOverlay else { return }
35
+ if let existing = mapView.renderer(for: overlay) as? MATileOverlayRenderer {
36
+ existing.reloadData()
37
+ mapView.setNeedsDisplay()
38
+ return
39
+ }
40
+ renderer?.reloadData()
41
+ mapView.setNeedsDisplay()
42
+ }
24
43
 
25
44
  required init(appContext: AppContext? = nil) {
26
45
  super.init(appContext: appContext)
46
+ self.backgroundColor = UIColor.clear
27
47
  }
28
48
 
29
49
  /**
@@ -32,15 +52,31 @@ class HeatMapView: ExpoView {
32
52
  */
33
53
  func setMap(_ map: MAMapView) {
34
54
  self.mapView = map
55
+ print("HeatMap: setMap")
35
56
  createOrUpdateHeatMap()
36
57
  }
37
58
 
59
+ /**
60
+ * 获取渲染器
61
+ */
62
+ func getRenderer() -> MAOverlayRenderer {
63
+ if let overlay = heatmapOverlay {
64
+ if renderer == nil || renderer?.overlay !== overlay {
65
+ renderer = MATileOverlayRenderer(tileOverlay: overlay)
66
+ renderer?.reloadData()
67
+ }
68
+ return renderer!
69
+ }
70
+ return MAOverlayRenderer()
71
+ }
72
+
38
73
  /**
39
74
  * 设置热力图数据
40
75
  * @param data 数据点数组,每个点包含 latitude、longitude
41
76
  */
42
77
  func setData(_ data: [[String: Any]]) {
43
78
  self.data = data
79
+ print("HeatMap: setData count=\(data.count)")
44
80
  createOrUpdateHeatMap()
45
81
  }
46
82
 
@@ -50,6 +86,7 @@ class HeatMapView: ExpoView {
50
86
  */
51
87
  func setRadius(_ radius: Int) {
52
88
  self.radius = radius
89
+ print("HeatMap: setRadius \(radius)")
53
90
  createOrUpdateHeatMap()
54
91
  }
55
92
 
@@ -59,6 +96,30 @@ class HeatMapView: ExpoView {
59
96
  */
60
97
  func setOpacity(_ opacity: Double) {
61
98
  self.opacity = opacity
99
+ print("HeatMap: setOpacity \(opacity)")
100
+ createOrUpdateHeatMap()
101
+ }
102
+
103
+ /**
104
+ * 设置渐变配置
105
+ */
106
+ func setGradient(_ gradient: [String: Any]?) {
107
+ self.gradient = gradient
108
+ print("HeatMap: setGradient hasValue=\(gradient != nil)")
109
+ createOrUpdateHeatMap()
110
+ }
111
+
112
+ /**
113
+ * 设置是否开启高清适配
114
+ */
115
+ func setAllowRetinaAdapting(_ allow: Bool) {
116
+ self.allowRetinaAdapting = allow
117
+ print("HeatMap: setAllowRetinaAdapting \(allow)")
118
+ createOrUpdateHeatMap()
119
+ }
120
+
121
+ func setVisible(_ visible: Bool) {
122
+ self.visible = visible
62
123
  createOrUpdateHeatMap()
63
124
  }
64
125
 
@@ -66,12 +127,28 @@ class HeatMapView: ExpoView {
66
127
  * 创建或更新热力图
67
128
  */
68
129
  private func createOrUpdateHeatMap() {
130
+ if !Thread.isMainThread {
131
+ DispatchQueue.main.async { [weak self] in
132
+ self?.createOrUpdateHeatMap()
133
+ }
134
+ return
135
+ }
136
+
69
137
  guard let mapView = mapView else { return }
138
+
139
+ if !visible {
140
+ if let overlay = heatmapOverlay {
141
+ overlay.opacity = 0
142
+ reloadRenderer()
143
+ }
144
+ return
145
+ }
70
146
 
71
147
  // 移除旧的热力图
72
148
  if let oldHeatmap = heatmapOverlay {
73
149
  mapView.remove(oldHeatmap)
74
150
  heatmapOverlay = nil
151
+ renderer = nil
75
152
  }
76
153
 
77
154
  // 验证数据有效性
@@ -90,7 +167,9 @@ class HeatMapView: ExpoView {
90
167
  let node = MAHeatMapNode()
91
168
  node.coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
92
169
  // 支持自定义强度,默认为 1.0
93
- if let intensity = point["intensity"] as? Double {
170
+ if let count = point["count"] as? Double {
171
+ node.intensity = Float(max(0, count))
172
+ } else if let intensity = point["intensity"] as? Double {
94
173
  node.intensity = Float(max(0, min(1, intensity)))
95
174
  } else {
96
175
  node.intensity = 1.0
@@ -98,25 +177,60 @@ class HeatMapView: ExpoView {
98
177
  heatmapData.append(node)
99
178
  }
100
179
 
101
- guard !heatmapData.isEmpty else { return }
180
+ guard !heatmapData.isEmpty else {
181
+ print("HeatMap: No valid data points found")
182
+ return
183
+ }
184
+
185
+ print("HeatMap: Creating overlay with \(heatmapData.count) points")
102
186
 
103
187
  // 创建热力图图层
104
188
  let heatmap = MAHeatMapTileOverlay()
105
189
  heatmap.data = heatmapData
106
- heatmap.radius = max(1, radius) // 确保半径至少为 1
190
+ heatmap.radius = min(200, max(10, radius))
107
191
  heatmap.opacity = CGFloat(max(0, min(1, opacity))) // 限制透明度范围
192
+ heatmap.allowRetinaAdapting = allowRetinaAdapting
108
193
 
109
- mapView.add(heatmap)
194
+ // 配置渐变
195
+ if let gradientConfig = gradient,
196
+ let colorsArray = gradientConfig["colors"] as? [Any],
197
+ let startPointsArray = gradientConfig["startPoints"] as? [NSNumber] {
198
+
199
+ var colors: [UIColor] = []
200
+ for colorValue in colorsArray {
201
+ if let color = ColorParser.parseColor(colorValue) {
202
+ colors.append(color)
203
+ }
204
+ }
205
+
206
+ if !colors.isEmpty && colors.count == startPointsArray.count {
207
+ let gradient = MAHeatMapGradient(color: colors, andWithStartPoints: startPointsArray)
208
+ heatmap.gradient = gradient
209
+ }
210
+ }
211
+
110
212
  heatmapOverlay = heatmap
213
+ renderer = MATileOverlayRenderer(tileOverlay: heatmap)
214
+ renderer?.reloadData()
215
+
216
+ mapView.add(heatmap)
217
+ reloadRenderer()
111
218
  }
112
219
 
113
220
  /**
114
221
  * 移除热力图
115
222
  */
116
223
  func removeHeatMap() {
224
+ if !Thread.isMainThread {
225
+ DispatchQueue.main.async { [weak self] in
226
+ self?.removeHeatMap()
227
+ }
228
+ return
229
+ }
117
230
  guard let mapView = mapView, let heatmap = heatmapOverlay else { return }
118
231
  mapView.remove(heatmap)
119
232
  heatmapOverlay = nil
233
+ renderer = nil
120
234
  }
121
235
 
122
236
  /**
@@ -8,6 +8,10 @@ public class HeatMapViewModule: Module {
8
8
  Prop("data") { (view: HeatMapView, data: [[String: Any]]) in
9
9
  view.setData(data)
10
10
  }
11
+
12
+ Prop("visible") { (view: HeatMapView, visible: Bool) in
13
+ view.setVisible(visible)
14
+ }
11
15
 
12
16
  Prop("radius") { (view: HeatMapView, radius: Int) in
13
17
  view.setRadius(radius)
@@ -16,6 +20,14 @@ public class HeatMapViewModule: Module {
16
20
  Prop("opacity") { (view: HeatMapView, opacity: Double) in
17
21
  view.setOpacity(opacity)
18
22
  }
23
+
24
+ Prop("gradient") { (view: HeatMapView, gradient: [String: Any]?) in
25
+ view.setGradient(gradient)
26
+ }
27
+
28
+ Prop("allowRetinaAdapting") { (view: HeatMapView, allow: Bool) in
29
+ view.setAllowRetinaAdapting(allow)
30
+ }
19
31
  }
20
32
  }
21
- }
33
+ }
@@ -1,11 +1,20 @@
1
1
  import ExpoModulesCore
2
2
  import MAMapKit
3
+ import UIKit
3
4
 
4
5
  class MultiPointView: ExpoView {
6
+ let onMultiPointPress = EventDispatcher()
7
+
5
8
  var points: [[String: Any]] = []
9
+ var iconUri: String?
10
+ var iconWidth: Double?
11
+ var iconHeight: Double?
12
+ var anchorX: Double = 0.5
13
+ var anchorY: Double = 0.5
6
14
 
7
15
  private var mapView: MAMapView?
8
16
  private var multiPointOverlay: MAMultiPointOverlay?
17
+ private var renderer: MAMultiPointOverlayRenderer?
9
18
 
10
19
  required init(appContext: AppContext? = nil) {
11
20
  super.init(appContext: appContext)
@@ -21,6 +30,27 @@ class MultiPointView: ExpoView {
21
30
  updateMultiPoint()
22
31
  }
23
32
 
33
+ func setIcon(_ iconUri: String?) {
34
+ self.iconUri = iconUri
35
+ updateIcon()
36
+ }
37
+
38
+ func setIconWidth(_ width: Double?) {
39
+ self.iconWidth = width
40
+ updateIcon()
41
+ }
42
+
43
+ func setIconHeight(_ height: Double?) {
44
+ self.iconHeight = height
45
+ updateIcon()
46
+ }
47
+
48
+ func setAnchor(x: Double, y: Double) {
49
+ self.anchorX = x
50
+ self.anchorY = y
51
+ renderer?.anchor = CGPoint(x: x, y: y)
52
+ }
53
+
24
54
  private func updateMultiPoint() {
25
55
  guard let mapView = mapView else { return }
26
56
 
@@ -28,13 +58,14 @@ class MultiPointView: ExpoView {
28
58
  if let oldOverlay = multiPointOverlay {
29
59
  mapView.remove(oldOverlay)
30
60
  multiPointOverlay = nil
61
+ renderer = nil
31
62
  }
32
63
 
33
64
  // 验证数据有效性
34
65
  guard !points.isEmpty else { return }
35
66
 
36
67
  var items: [MAMultiPointItem] = []
37
- for point in points {
68
+ for (index, point) in points.enumerated() {
38
69
  guard let latitude = point["latitude"] as? Double,
39
70
  let longitude = point["longitude"] as? Double else {
40
71
  continue
@@ -42,16 +73,143 @@ class MultiPointView: ExpoView {
42
73
 
43
74
  let item = MAMultiPointItem()
44
75
  item.coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
76
+ // 存储 index 和 customerId 到 item 中,这里利用 customID 或者 title 属性
77
+ // MAMultiPointItem 有 customID 属性
78
+ if let customerId = point["customerId"] as? String {
79
+ item.customID = customerId
80
+ } else if let id = point["id"] as? String {
81
+ item.customID = id
82
+ }
83
+
84
+ // 也可以存 title,这里为了方便查找 index,我们可以把 index 存到 title 或者 subtitle
85
+ // 但最好是通过 customID 查找
86
+
45
87
  items.append(item)
46
88
  }
47
89
 
48
90
  guard !items.isEmpty else { return }
49
91
 
50
92
  let overlay = MAMultiPointOverlay(multiPointItems: items)
93
+ self.multiPointOverlay = overlay
51
94
  mapView.add(overlay)
52
- multiPointOverlay = overlay
53
95
  }
54
96
 
97
+ func getRenderer() -> MAMultiPointOverlayRenderer? {
98
+ guard let overlay = multiPointOverlay else { return nil }
99
+
100
+ if renderer == nil {
101
+ renderer = MAMultiPointOverlayRenderer(multiPointOverlay: overlay)
102
+ renderer?.anchor = CGPoint(x: anchorX, y: anchorY)
103
+ updateIcon()
104
+ }
105
+ return renderer
106
+ }
107
+
108
+ private func updateIcon() {
109
+ guard let iconUri = iconUri else { return }
110
+
111
+ // 构建缓存 key
112
+ let w = Int(iconWidth ?? 0)
113
+ let h = Int(iconHeight ?? 0)
114
+ let key = "multipoint|\(iconUri)|\(w)x\(h)"
115
+
116
+ // 1. 尝试从缓存获取
117
+ if let cached = IconBitmapCache.shared.image(forKey: key) {
118
+ self.renderer?.icon = cached
119
+ refreshOverlay()
120
+ return
121
+ }
122
+
123
+ loadIcon(iconUri: iconUri) { [weak self] image in
124
+ guard let self = self, let image = image else { return }
125
+
126
+ var finalImage = image
127
+ if let w = self.iconWidth, let h = self.iconHeight {
128
+ finalImage = self.resizeImage(image: image, targetSize: CGSize(width: w, height: h))
129
+ } else if let w = self.iconWidth {
130
+ let h = image.size.height * (w / image.size.width)
131
+ finalImage = self.resizeImage(image: image, targetSize: CGSize(width: w, height: h))
132
+ } else if let h = self.iconHeight {
133
+ let w = image.size.width * (h / image.size.height)
134
+ finalImage = self.resizeImage(image: image, targetSize: CGSize(width: w, height: h))
135
+ }
136
+
137
+ // 写入缓存
138
+ IconBitmapCache.shared.setImage(finalImage, forKey: key)
139
+
140
+ self.renderer?.icon = finalImage
141
+ self.refreshOverlay()
142
+ }
143
+ }
144
+
145
+ private func refreshOverlay() {
146
+ self.renderer?.setNeedsUpdate()
147
+ // 强制刷新:通过重新添加 overlay 触发更新
148
+ if let mapView = self.mapView, let overlay = self.multiPointOverlay {
149
+ mapView.remove(overlay)
150
+ mapView.add(overlay)
151
+ }
152
+ }
153
+
154
+ private func resizeImage(image: UIImage, targetSize: CGSize) -> UIImage {
155
+ UIGraphicsBeginImageContextWithOptions(targetSize, false, 0.0)
156
+ image.draw(in: CGRect(origin: .zero, size: targetSize))
157
+ let resizedImage = UIGraphicsGetImageFromCurrentImageContext()
158
+ UIGraphicsEndImageContext()
159
+ return resizedImage ?? image
160
+ }
161
+
162
+ private func loadIcon(iconUri: String, completion: @escaping (UIImage?) -> Void) {
163
+ if iconUri.hasPrefix("http://") || iconUri.hasPrefix("https://") {
164
+ guard let url = URL(string: iconUri) else {
165
+ completion(nil)
166
+ return
167
+ }
168
+ URLSession.shared.dataTask(with: url) { data, _, _ in
169
+ guard let data = data, let image = UIImage(data: data) else {
170
+ DispatchQueue.main.async { completion(nil) }
171
+ return
172
+ }
173
+ DispatchQueue.main.async { completion(image) }
174
+ }.resume()
175
+ } else if iconUri.hasPrefix("file://") {
176
+ let path = String(iconUri.dropFirst(7))
177
+ completion(UIImage(contentsOfFile: path))
178
+ } else {
179
+ completion(UIImage(named: iconUri))
180
+ }
181
+ }
182
+
183
+ func handleMultiPointClick(item: MAMultiPointItem) {
184
+ // 查找对应的 point 数据
185
+ var index = -1
186
+ var pointData: [String: Any]?
187
+
188
+ // 优先通过 customID 查找
189
+ if let customID = item.customID {
190
+ index = points.firstIndex { ($0["customerId"] as? String) == customID || ($0["id"] as? String) == customID } ?? -1
191
+ }
192
+
193
+ // 如果没找到,尝试通过坐标查找(不够精确,但作为备选)
194
+ if index == -1 {
195
+ index = points.firstIndex { point in
196
+ guard let lat = point["latitude"] as? Double,
197
+ let lng = point["longitude"] as? Double else { return false }
198
+ return abs(lat - item.coordinate.latitude) < 0.000001 && abs(lng - item.coordinate.longitude) < 0.000001
199
+ } ?? -1
200
+ }
201
+
202
+ if index != -1 {
203
+ pointData = points[index]
204
+ onMultiPointPress([
205
+ "index": index,
206
+ "customerId": pointData?["customerId"] ?? pointData?["id"] ?? "",
207
+ "latitude": item.coordinate.latitude,
208
+ "longitude": item.coordinate.longitude
209
+ ])
210
+ }
211
+ }
212
+
55
213
  /**
56
214
  * 移除多点覆盖物
57
215
  */
@@ -5,9 +5,31 @@ public class MultiPointViewModule: Module {
5
5
  Name("MultiPointView")
6
6
 
7
7
  View(MultiPointView.self) {
8
+ Events("onMultiPointPress")
9
+
8
10
  Prop("points") { (view: MultiPointView, points: [[String: Any]]) in
9
11
  view.setPoints(points)
10
12
  }
13
+
14
+ Prop("icon") { (view: MultiPointView, icon: String?) in
15
+ view.setIcon(icon)
16
+ }
17
+
18
+ Prop("iconWidth") { (view: MultiPointView, width: Double?) in
19
+ view.setIconWidth(width)
20
+ }
21
+
22
+ Prop("iconHeight") { (view: MultiPointView, height: Double?) in
23
+ view.setIconHeight(height)
24
+ }
25
+
26
+ Prop("anchor") { (view: MultiPointView, anchor: [String: Double]?) in
27
+ if let anchor = anchor {
28
+ let x = anchor["x"] ?? 0.5
29
+ let y = anchor["y"] ?? 0.5
30
+ view.setAnchor(x: x, y: y)
31
+ }
32
+ }
11
33
  }
12
34
  }
13
35
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-gaode-map",
3
- "version": "2.2.15",
3
+ "version": "2.2.16",
4
4
  "description": "A full-featured AMap (Gaode Map) React Native component library built with Expo Modules, providing map display, location services, overlays, and more.",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",