expo-gaode-map 2.2.32 → 2.2.33

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 (91) hide show
  1. package/README.md +2 -3
  2. package/android/src/main/cpp/cluster_jni.cpp +56 -0
  3. package/android/src/main/java/expo/modules/gaodemap/ExpoGaodeMapModule.kt +45 -6
  4. package/android/src/main/java/expo/modules/gaodemap/ExpoGaodeMapOfflineModule.kt +1 -1
  5. package/android/src/main/java/expo/modules/gaodemap/modules/SDKInitializer.kt +23 -17
  6. package/android/src/main/java/expo/modules/gaodemap/overlays/MarkerBitmapRenderer.kt +30 -16
  7. package/android/src/main/java/expo/modules/gaodemap/overlays/MarkerView.kt +36 -31
  8. package/android/src/main/java/expo/modules/gaodemap/overlays/MarkerViewModule.kt +6 -6
  9. package/android/src/main/java/expo/modules/gaodemap/utils/GeometryUtils.kt +103 -0
  10. package/build/ExpoGaodeMapModule.d.ts +9 -259
  11. package/build/ExpoGaodeMapModule.d.ts.map +1 -1
  12. package/build/ExpoGaodeMapModule.js +19 -910
  13. package/build/ExpoGaodeMapModule.js.map +1 -1
  14. package/build/ExpoGaodeMapView.d.ts +3 -4
  15. package/build/ExpoGaodeMapView.d.ts.map +1 -1
  16. package/build/ExpoGaodeMapView.js +28 -25
  17. package/build/ExpoGaodeMapView.js.map +1 -1
  18. package/build/components/overlays/Circle.d.ts.map +1 -1
  19. package/build/components/overlays/Circle.js +1 -30
  20. package/build/components/overlays/Circle.js.map +1 -1
  21. package/build/components/overlays/Cluster.d.ts.map +1 -1
  22. package/build/components/overlays/Cluster.js +1 -42
  23. package/build/components/overlays/Cluster.js.map +1 -1
  24. package/build/components/overlays/HeatMap.d.ts.map +1 -1
  25. package/build/components/overlays/HeatMap.js +1 -20
  26. package/build/components/overlays/HeatMap.js.map +1 -1
  27. package/build/components/overlays/Marker.d.ts.map +1 -1
  28. package/build/components/overlays/Marker.js +14 -80
  29. package/build/components/overlays/Marker.js.map +1 -1
  30. package/build/components/overlays/Polygon.d.ts.map +1 -1
  31. package/build/components/overlays/Polygon.js +1 -25
  32. package/build/components/overlays/Polygon.js.map +1 -1
  33. package/build/components/overlays/Polyline.d.ts.map +1 -1
  34. package/build/components/overlays/Polyline.js +1 -31
  35. package/build/components/overlays/Polyline.js.map +1 -1
  36. package/build/index.d.ts +6 -2
  37. package/build/index.d.ts.map +1 -1
  38. package/build/index.js +6 -2
  39. package/build/index.js.map +1 -1
  40. package/build/module/geometry.d.ts +183 -0
  41. package/build/module/geometry.d.ts.map +1 -0
  42. package/build/module/geometry.js +374 -0
  43. package/build/module/geometry.js.map +1 -0
  44. package/build/module/location.d.ts +49 -0
  45. package/build/module/location.d.ts.map +1 -0
  46. package/build/module/location.js +257 -0
  47. package/build/module/location.js.map +1 -0
  48. package/build/module/nativeModule.d.ts +5 -0
  49. package/build/module/nativeModule.d.ts.map +1 -0
  50. package/build/module/nativeModule.js +34 -0
  51. package/build/module/nativeModule.js.map +1 -0
  52. package/build/module/normalizers.d.ts +9 -0
  53. package/build/module/normalizers.d.ts.map +1 -0
  54. package/build/module/normalizers.js +109 -0
  55. package/build/module/normalizers.js.map +1 -0
  56. package/build/module/privacy.d.ts +20 -0
  57. package/build/module/privacy.d.ts.map +1 -0
  58. package/build/module/privacy.js +59 -0
  59. package/build/module/privacy.js.map +1 -0
  60. package/build/module/sdk.d.ts +20 -0
  61. package/build/module/sdk.d.ts.map +1 -0
  62. package/build/module/sdk.js +82 -0
  63. package/build/module/sdk.js.map +1 -0
  64. package/build/types/index.d.ts +1 -1
  65. package/build/types/index.js.map +1 -1
  66. package/build/types/native-module.types.d.ts +12 -16
  67. package/build/types/native-module.types.d.ts.map +1 -1
  68. package/build/types/native-module.types.js.map +1 -1
  69. package/build/types/overlays.types.d.ts +0 -16
  70. package/build/types/overlays.types.d.ts.map +1 -1
  71. package/build/types/overlays.types.js.map +1 -1
  72. package/build/types/route-playback.types.d.ts +3 -0
  73. package/build/types/route-playback.types.d.ts.map +1 -1
  74. package/build/types/route-playback.types.js.map +1 -1
  75. package/build/utils/RouteUtils.d.ts +1 -1
  76. package/build/utils/RouteUtils.d.ts.map +1 -1
  77. package/build/utils/RouteUtils.js +35 -1
  78. package/build/utils/RouteUtils.js.map +1 -1
  79. package/ios/ExpoGaodeMapModule.swift +38 -6
  80. package/ios/ExpoGaodeMapView.swift +10 -3
  81. package/ios/GaodeMapPrivacyManager.swift +26 -18
  82. package/ios/modules/LocationManager.swift +1 -1
  83. package/ios/overlays/MarkerView.swift +36 -25
  84. package/ios/overlays/MarkerViewModule.swift +4 -4
  85. package/ios/utils/ClusterNative.h +8 -0
  86. package/ios/utils/ClusterNative.mm +27 -0
  87. package/package.json +3 -1
  88. package/scripts/check-expo-modules.js +68 -0
  89. package/shared/cpp/GeometryEngine.cpp +112 -0
  90. package/shared/cpp/GeometryEngine.hpp +21 -0
  91. package/shared/cpp/tests/test_main.cpp +15 -0
@@ -48,12 +48,19 @@ public class ExpoGaodeMapModule: Module {
48
48
  _ = self.trySetupApiKeyFromPlist()
49
49
  }
50
50
 
51
- Function("setPrivacyShow") { (hasShow: Bool, hasContainsPrivacy: Bool) in
52
- GaodeMapPrivacyManager.setPrivacyShow(hasShow, hasContainsPrivacy: hasContainsPrivacy)
53
- }
51
+ Function("setPrivacyConfig") { (config: [String: Any]) in
52
+ let hasShow = config["hasShow"] as? Bool ?? false
53
+ let hasContainsPrivacy = config["hasContainsPrivacy"] as? Bool ?? hasShow
54
+ let hasAgree = config["hasAgree"] as? Bool ?? false
55
+ let privacyVersion = config["privacyVersion"] as? String
54
56
 
55
- Function("setPrivacyAgree") { (hasAgree: Bool) in
56
- GaodeMapPrivacyManager.setPrivacyAgree(hasAgree)
57
+ GaodeMapPrivacyManager.setPrivacyConfig(
58
+ hasShow: hasShow,
59
+ hasContainsPrivacy: hasContainsPrivacy,
60
+ hasAgree: hasAgree,
61
+ privacyVersion: privacyVersion,
62
+ updatesPrivacyVersion: config.keys.contains("privacyVersion")
63
+ )
57
64
  }
58
65
 
59
66
  Function("setPrivacyVersion") { (version: String) in
@@ -76,7 +83,7 @@ public class ExpoGaodeMapModule: Module {
76
83
  */
77
84
  Function("initSDK") { (config: [String: String]) in
78
85
  guard GaodeMapPrivacyManager.isReady else {
79
- throw Exception(name: "PRIVACY_NOT_AGREED", description: "隐私协议未完成确认,请先调用 setPrivacyShow/setPrivacyAgree")
86
+ throw Exception(name: "PRIVACY_NOT_AGREED", description: "隐私协议未完成确认,请先调用 setPrivacyConfig")
80
87
  }
81
88
  GaodeMapPrivacyManager.applyPrivacyState()
82
89
 
@@ -273,6 +280,31 @@ public class ExpoGaodeMapModule: Module {
273
280
  }
274
281
  return ClusterNative.calculateDistance(lat1: coord1.latitude, lon1: coord1.longitude, lat2: coord2.latitude, lon2: coord2.longitude)
275
282
  }
283
+
284
+ /**
285
+ * 根据多个坐标点计算可同时可见的推荐缩放级别
286
+ */
287
+ Function("calculateFitZoom") {
288
+ (points: [Any]?, viewportWidthPx: Double?, viewportHeightPx: Double?, paddingPx: Double?, minZoom: Int?, maxZoom: Int?) -> Double in
289
+ let coords = LatLngParser.parseLatLngList(points)
290
+ let safeMinZoom = minZoom ?? 3
291
+ let safeMaxZoom = maxZoom ?? 20
292
+ if coords.isEmpty {
293
+ return Double(safeMinZoom)
294
+ }
295
+
296
+ let lats = coords.map { NSNumber(value: $0.latitude) }
297
+ let lons = coords.map { NSNumber(value: $0.longitude) }
298
+ return ClusterNative.calculateFitZoom(
299
+ latitudes: lats,
300
+ longitudes: lons,
301
+ viewportWidthPx: viewportWidthPx ?? 390.0,
302
+ viewportHeightPx: viewportHeightPx ?? 844.0,
303
+ paddingPx: paddingPx ?? 48.0,
304
+ minZoom: Int32(safeMinZoom),
305
+ maxZoom: Int32(safeMaxZoom)
306
+ )
307
+ }
276
308
 
277
309
  /**
278
310
  * 计算多边形面积
@@ -1349,17 +1349,24 @@ extension ExpoGaodeMapView {
1349
1349
  isHandlingAnnotationSelect = true
1350
1350
 
1351
1351
  // 🔑 统一从 overlayViews 查找 MarkerView(新旧架构统一)
1352
- for view in overlayViews {
1353
- if let markerView = view as? MarkerView {
1352
+ for overlayView in overlayViews {
1353
+ if let markerView = overlayView as? MarkerView {
1354
1354
  if markerView.annotation === annotation {
1355
1355
  let eventData: [String: Any] = [
1356
1356
  "latitude": annotation.coordinate.latitude,
1357
1357
  "longitude": annotation.coordinate.longitude
1358
1358
  ]
1359
1359
  markerView.onMarkerPress(eventData)
1360
+ // iOS 对“已选中 annotation”再次点击通常不会重复触发 didSelect。
1361
+ // 对自定义 children marker(无系统 callout)这里主动取消选中,
1362
+ // 以保证同一 marker 可以连续触发点击(例如关闭 sheet 后再次点击)。
1363
+ if !markerView.subviews.isEmpty {
1364
+ mapView.deselectAnnotation(annotation, animated: false)
1365
+ isHandlingAnnotationSelect = false
1366
+ }
1360
1367
  return
1361
1368
  }
1362
- } else if let clusterView = view as? ClusterView {
1369
+ } else if let clusterView = overlayView as? ClusterView {
1363
1370
  if clusterView.containsAnnotation(annotation) {
1364
1371
  clusterView.handleAnnotationTap(annotation)
1365
1372
  return
@@ -46,24 +46,6 @@ enum GaodeMapPrivacyManager {
46
46
  applyPrivacyState()
47
47
  }
48
48
 
49
- static func setPrivacyShow(_ show: Bool, hasContainsPrivacy: Bool) {
50
- let previousStatus = status()
51
- hasShow = show
52
- self.hasContainsPrivacy = hasContainsPrivacy
53
- persistState()
54
- applyPrivacyState()
55
- notifyIfNeeded(previousStatus: previousStatus)
56
- }
57
-
58
- static func setPrivacyAgree(_ agree: Bool) {
59
- let previousStatus = status()
60
- hasAgree = agree
61
- agreedPrivacyVersion = agree ? privacyVersion : nil
62
- persistState()
63
- applyPrivacyState()
64
- notifyIfNeeded(previousStatus: previousStatus)
65
- }
66
-
67
49
  static func setPrivacyVersion(_ version: String) {
68
50
  let previousStatus = status()
69
51
  privacyVersion = version.trimmingCharacters(in: .whitespacesAndNewlines)
@@ -83,6 +65,32 @@ enum GaodeMapPrivacyManager {
83
65
  notifyIfNeeded(previousStatus: previousStatus)
84
66
  }
85
67
 
68
+ static func setPrivacyConfig(
69
+ hasShow: Bool,
70
+ hasContainsPrivacy: Bool,
71
+ hasAgree: Bool,
72
+ privacyVersion newPrivacyVersion: String?,
73
+ updatesPrivacyVersion: Bool
74
+ ) {
75
+ let previousStatus = status()
76
+
77
+ if updatesPrivacyVersion {
78
+ privacyVersion = newPrivacyVersion?.trimmingCharacters(in: .whitespacesAndNewlines)
79
+ if privacyVersion?.isEmpty == true {
80
+ privacyVersion = nil
81
+ }
82
+ }
83
+
84
+ self.hasShow = hasShow
85
+ self.hasContainsPrivacy = hasContainsPrivacy
86
+ self.hasAgree = hasAgree
87
+ agreedPrivacyVersion = hasAgree ? privacyVersion : nil
88
+
89
+ persistState()
90
+ applyPrivacyState()
91
+ notifyIfNeeded(previousStatus: previousStatus)
92
+ }
93
+
86
94
  static func resetPrivacyConsent() {
87
95
  let previousStatus = status()
88
96
  clearConsentPersistedState(keepCurrentVersion: false)
@@ -238,7 +238,7 @@ class LocationManager: NSObject, AMapLocationManagerDelegate {
238
238
  */
239
239
  func coordinateConvert(_ coordinate: [String: Double], type: Int, promise: Promise) {
240
240
  guard GaodeMapPrivacyManager.isReady else {
241
- promise.reject("PRIVACY_NOT_AGREED", "隐私协议未完成确认,请先调用 setPrivacyShow/setPrivacyAgree")
241
+ promise.reject("PRIVACY_NOT_AGREED", "隐私协议未完成确认,请先调用 setPrivacyConfig")
242
242
  return
243
243
  }
244
244
 
@@ -36,10 +36,10 @@ class MarkerView: ExpoView {
36
36
  var iconWidth: Double = 40
37
37
  /// 图标高度(用于自定义图标 icon 属性)
38
38
  var iconHeight: Double = 40
39
- /// 自定义视图宽度(用于 children 属性)
40
- var customViewWidth: Double = 0
41
- /// 自定义视图高度(用于 children 属性)
42
- var customViewHeight: Double = 0
39
+ /// 内容宽度(用于 children 属性)
40
+ var contentWidth: Double = 0
41
+ /// 内容高度(用于 children 属性)
42
+ var contentHeight: Double = 0
43
43
  /// 中心偏移
44
44
  var centerOffset: [String: Double]?
45
45
  /// 是否显示动画
@@ -230,7 +230,7 @@ class MarkerView: ExpoView {
230
230
 
231
231
  // 1. 如果有 children,使用自定义视图
232
232
  if self.subviews.count > 0 {
233
- let size = resolvedCustomSubviewSize(defaultSize: CGSize(width: 200, height: 60))
233
+ let size = resolvedContentSubviewSize(defaultSize: CGSize(width: 200, height: 60))
234
234
  let key = childrenCacheKey(for: size)
235
235
  if let cached = IconBitmapCache.shared.image(forKey: key) {
236
236
  annotationView?.image = cached
@@ -243,6 +243,7 @@ class MarkerView: ExpoView {
243
243
  // 异步渲染并设置
244
244
  DispatchQueue.main.async { [weak self, weak annotationView] in
245
245
  guard let self = self, let annotationView = annotationView else { return }
246
+ guard self.isAnnotationView(annotationView, boundTo: annotation) else { return }
246
247
  if let generated = self.createImageFromSubviews() {
247
248
  annotationView.image = generated
248
249
  self.applyCenterOffset(to: annotationView, defaultOffset: .zero)
@@ -265,6 +266,7 @@ class MarkerView: ExpoView {
265
266
  // 异步加载图标
266
267
  loadIcon(iconUri: iconUri) { [weak self, weak annotationView] image in
267
268
  guard let self = self, let image = image, let annotationView = annotationView else { return }
269
+ guard self.isAnnotationView(annotationView, boundTo: annotation) else { return }
268
270
  let size = CGSize(width: self.iconWidth, height: self.iconHeight)
269
271
  UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
270
272
  image.draw(in: CGRect(origin: .zero, size: size))
@@ -305,8 +307,9 @@ class MarkerView: ExpoView {
305
307
 
306
308
  // 🔑 如果有 children,使用自定义视图
307
309
  if self.subviews.count > 0 {
308
- // 使用 class-level reuseId,便于系统复用 view,减少内存
309
- let reuseId = "custom_marker_children" + (growAnimation ? "_grow" : "")
310
+ let reuseToken = cacheKey?.replacingOccurrences(of: "|", with: "_")
311
+ ?? String(ObjectIdentifier(self).hashValue)
312
+ let reuseId = "custom_marker_children_\(reuseToken)" + (growAnimation ? "_grow" : "")
310
313
  var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseId)
311
314
  if annotationView == nil {
312
315
  if growAnimation {
@@ -326,7 +329,7 @@ class MarkerView: ExpoView {
326
329
  self.annotationView = annotationView
327
330
 
328
331
  // 生成 cacheKey 或 fallback 到 identifier
329
- let size = resolvedCustomSubviewSize(defaultSize: CGSize(width: 200, height: 40))
332
+ let size = resolvedContentSubviewSize(defaultSize: CGSize(width: 200, height: 40))
330
333
  let key = childrenCacheKey(for: size)
331
334
 
332
335
  // 1) 如果缓存命中,直接同步返回图像(fast path)
@@ -347,6 +350,7 @@ class MarkerView: ExpoView {
347
350
  // 🔑 修复:延长延迟时间,给 React Native Image 更多加载时间
348
351
  DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self, weak annotationView] in
349
352
  guard let self = self, let annotationView = annotationView else { return }
353
+ guard self.isAnnotationView(annotationView, boundTo: annotation) else { return }
350
354
  // 再次检查缓存(避免重复渲染)
351
355
  if let cached = IconBitmapCache.shared.image(forKey: key) {
352
356
  annotationView.image = cached
@@ -410,12 +414,12 @@ class MarkerView: ExpoView {
410
414
  UIGraphicsEndImageContext()
411
415
 
412
416
  DispatchQueue.main.async {
417
+ guard let annotationView else { return }
418
+ guard self.isAnnotationView(annotationView, boundTo: annotation) else { return }
413
419
  if let img = resizedImage {
414
420
  IconBitmapCache.shared.setImage(img, forKey: key)
415
- annotationView?.image = img
416
- if let annotationView = annotationView {
417
- self.applyCenterOffset(to: annotationView, defaultOffset: CGPoint(x: 0, y: -img.size.height / 2))
418
- }
421
+ annotationView.image = img
422
+ self.applyCenterOffset(to: annotationView, defaultOffset: CGPoint(x: 0, y: -img.size.height / 2))
419
423
  }
420
424
  }
421
425
  }
@@ -496,7 +500,7 @@ class MarkerView: ExpoView {
496
500
  * 将子视图转换为图片
497
501
  */
498
502
  private func createImageFromSubviews() -> UIImage? {
499
- let size = resolvedCustomSubviewSize(defaultSize: CGSize(width: 200, height: 60))
503
+ let size = resolvedContentSubviewSize(defaultSize: CGSize(width: 200, height: 60))
500
504
  let key = childrenCacheKey(for: size)
501
505
 
502
506
  if let cachedImage = IconBitmapCache.shared.image(forKey: key) {
@@ -580,14 +584,21 @@ class MarkerView: ExpoView {
580
584
  return .zero
581
585
  }
582
586
 
583
- private func resolvedCustomSubviewSize(defaultSize: CGSize) -> CGSize {
587
+ private func isAnnotationView(_ annotationView: MAAnnotationView, boundTo annotation: MAAnnotation) -> Bool {
588
+ guard let current = annotationView.annotation else {
589
+ return false
590
+ }
591
+ return (current as AnyObject) === (annotation as AnyObject)
592
+ }
593
+
594
+ private func resolvedContentSubviewSize(defaultSize: CGSize) -> CGSize {
584
595
  guard let firstSubview = subviews.first else {
585
596
  return defaultSize
586
597
  }
587
598
 
588
- if customViewWidth > 0 || customViewHeight > 0 {
589
- let width = customViewWidth > 0 ? CGFloat(customViewWidth) : defaultSize.width
590
- let height = customViewHeight > 0 ? CGFloat(customViewHeight) : defaultSize.height
599
+ if contentWidth > 0 || contentHeight > 0 {
600
+ let width = contentWidth > 0 ? CGFloat(contentWidth) : defaultSize.width
601
+ let height = contentHeight > 0 ? CGFloat(contentHeight) : defaultSize.height
591
602
  return CGSize(width: width, height: height)
592
603
  }
593
604
 
@@ -842,8 +853,8 @@ class MarkerView: ExpoView {
842
853
 
843
854
  private func invalidateCurrentChildrenCache() {
844
855
  let sizes = [
845
- resolvedCustomSubviewSize(defaultSize: CGSize(width: 200, height: 40)),
846
- resolvedCustomSubviewSize(defaultSize: CGSize(width: 200, height: 60))
856
+ resolvedContentSubviewSize(defaultSize: CGSize(width: 200, height: 40)),
857
+ resolvedContentSubviewSize(defaultSize: CGSize(width: 200, height: 60))
847
858
  ]
848
859
 
849
860
  for size in sizes {
@@ -1000,17 +1011,17 @@ class MarkerView: ExpoView {
1000
1011
  }
1001
1012
  }
1002
1013
 
1003
- func setCustomViewWidth(_ width: Double) {
1004
- guard customViewWidth != width else { return }
1005
- customViewWidth = width
1014
+ func setContentWidth(_ width: Double) {
1015
+ guard contentWidth != width else { return }
1016
+ contentWidth = width
1006
1017
  if !subviews.isEmpty {
1007
1018
  refreshAnnotationAppearance(invalidateChildrenCache: true)
1008
1019
  }
1009
1020
  }
1010
1021
 
1011
- func setCustomViewHeight(_ height: Double) {
1012
- guard customViewHeight != height else { return }
1013
- customViewHeight = height
1022
+ func setContentHeight(_ height: Double) {
1023
+ guard contentHeight != height else { return }
1024
+ contentHeight = height
1014
1025
  if !subviews.isEmpty {
1015
1026
  refreshAnnotationAppearance(invalidateChildrenCache: true)
1016
1027
  }
@@ -41,12 +41,12 @@ public class MarkerViewModule: Module {
41
41
  view.setIconHeight(height)
42
42
  }
43
43
 
44
- Prop("customViewWidth") { (view: MarkerView, width: Double) in
45
- view.setCustomViewWidth(width)
44
+ Prop("contentWidth") { (view: MarkerView, width: Double) in
45
+ view.setContentWidth(width)
46
46
  }
47
47
 
48
- Prop("customViewHeight") { (view: MarkerView, height: Double) in
49
- view.setCustomViewHeight(height)
48
+ Prop("contentHeight") { (view: MarkerView, height: Double) in
49
+ view.setContentHeight(height)
50
50
  }
51
51
 
52
52
  Prop("centerOffset") { (view: MarkerView, offset: [String: Double]) in
@@ -55,6 +55,14 @@ NS_ASSUME_NONNULL_BEGIN
55
55
  + (NSDictionary * _Nullable)calculatePathBoundsWithLatitudes:(NSArray<NSNumber *> *)latitudes
56
56
  longitudes:(NSArray<NSNumber *> *)longitudes NS_SWIFT_NAME(calculatePathBounds(latitudes:longitudes:));
57
57
 
58
+ + (double)calculateFitZoomWithLatitudes:(NSArray<NSNumber *> *)latitudes
59
+ longitudes:(NSArray<NSNumber *> *)longitudes
60
+ viewportWidthPx:(double)viewportWidthPx
61
+ viewportHeightPx:(double)viewportHeightPx
62
+ paddingPx:(double)paddingPx
63
+ minZoom:(int)minZoom
64
+ maxZoom:(int)maxZoom NS_SWIFT_NAME(calculateFitZoom(latitudes:longitudes:viewportWidthPx:viewportHeightPx:paddingPx:minZoom:maxZoom:));
65
+
58
66
  + (NSString *)encodeGeoHashWithLat:(double)lat
59
67
  lon:(double)lon
60
68
  precision:(int)precision NS_SWIFT_NAME(encodeGeoHash(lat:lon:precision:));
@@ -242,6 +242,33 @@
242
242
  };
243
243
  }
244
244
 
245
+ + (double)calculateFitZoomWithLatitudes:(NSArray<NSNumber *> *)latitudes
246
+ longitudes:(NSArray<NSNumber *> *)longitudes
247
+ viewportWidthPx:(double)viewportWidthPx
248
+ viewportHeightPx:(double)viewportHeightPx
249
+ paddingPx:(double)paddingPx
250
+ minZoom:(int)minZoom
251
+ maxZoom:(int)maxZoom {
252
+ if (latitudes.count == 0 || latitudes.count != longitudes.count) {
253
+ return (double)minZoom;
254
+ }
255
+
256
+ std::vector<gaodemap::GeoPoint> points;
257
+ points.reserve(latitudes.count);
258
+ for (NSUInteger i = 0; i < latitudes.count; i++) {
259
+ points.push_back({[latitudes[i] doubleValue], [longitudes[i] doubleValue]});
260
+ }
261
+
262
+ return gaodemap::calculateFitZoomForPoints(
263
+ points,
264
+ viewportWidthPx,
265
+ viewportHeightPx,
266
+ paddingPx,
267
+ minZoom,
268
+ maxZoom
269
+ );
270
+ }
271
+
245
272
  + (NSString *)encodeGeoHashWithLat:(double)lat
246
273
  lon:(double)lon
247
274
  precision:(int)precision {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-gaode-map",
3
- "version": "2.2.32",
3
+ "version": "2.2.33",
4
4
  "description": "A full-featured React Native AMap (Gaode Map) library for Expo, including map display, location, overlays, offline maps, and geometry utilities.",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -9,6 +9,7 @@
9
9
  "android",
10
10
  "ios",
11
11
  "shared",
12
+ "scripts",
12
13
  "app.plugin.js",
13
14
  "expo-module.config.json",
14
15
  "plugin/build"
@@ -20,6 +21,7 @@
20
21
  "lint": "expo-module lint",
21
22
  "test": "expo-module test",
22
23
  "prepare": "expo-module prepare && yarn build:plugin",
24
+ "postinstall": "node scripts/check-expo-modules.js",
23
25
  "prepublishOnly": "echo 'Skipping proofread check' && exit 0",
24
26
  "expo-module": "expo-module",
25
27
  "open:ios": "xed example/ios",
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ function resolveProjectRoot() {
7
+ return process.env.INIT_CWD || process.env.npm_config_local_prefix || process.cwd();
8
+ }
9
+
10
+ function hasPackage(pkgName, projectRoot) {
11
+ try {
12
+ require.resolve(`${pkgName}/package.json`, { paths: [projectRoot] });
13
+ return true;
14
+ } catch {
15
+ return false;
16
+ }
17
+ }
18
+
19
+ function printWarning(lines) {
20
+ const prefix = '[expo-gaode-map]';
21
+ console.warn(`${prefix} WARNING: Expo Modules infrastructure was not detected in this React Native project.`);
22
+ for (const line of lines) {
23
+ console.warn(`${prefix} ${line}`);
24
+ }
25
+ }
26
+
27
+ function main() {
28
+ const projectRoot = resolveProjectRoot();
29
+
30
+ // Skip non-React Native consumers.
31
+ if (!hasPackage('react-native', projectRoot)) {
32
+ return;
33
+ }
34
+
35
+ const hasExpo = hasPackage('expo', projectRoot);
36
+ const hasExpoModulesCore = hasPackage('expo-modules-core', projectRoot);
37
+ const podfilePath = path.join(projectRoot, 'ios', 'Podfile');
38
+ const hasPodfile = fs.existsSync(podfilePath);
39
+ const hasUseExpoModules = hasPodfile
40
+ ? /\buse_expo_modules!\b/m.test(fs.readFileSync(podfilePath, 'utf8'))
41
+ : true;
42
+
43
+ if (hasExpo && hasExpoModulesCore && hasUseExpoModules) {
44
+ return;
45
+ }
46
+
47
+ const details = [];
48
+ if (!hasExpo) {
49
+ details.push('Missing dependency: expo');
50
+ }
51
+ if (!hasExpoModulesCore) {
52
+ details.push('Missing dependency: expo-modules-core');
53
+ }
54
+ if (!hasUseExpoModules) {
55
+ details.push('iOS Podfile does not contain use_expo_modules!');
56
+ }
57
+
58
+ printWarning([
59
+ ...details,
60
+ 'This package requires Expo Modules infrastructure even in bare React Native apps.',
61
+ 'Suggested fix:',
62
+ ' npx install-expo-modules@latest',
63
+ hasPodfile ? ' (then) cd ios && pod install' : null,
64
+ 'Docs: https://docs.expo.dev/bare/installing-expo-modules/',
65
+ ].filter(Boolean));
66
+ }
67
+
68
+ main();
@@ -3,6 +3,7 @@
3
3
  #include <cmath>
4
4
  #include <map>
5
5
  #include <algorithm>
6
+ #include <limits>
6
7
 
7
8
  namespace gaodemap {
8
9
 
@@ -19,6 +20,54 @@ static inline double geo_toDegrees(double radians) {
19
20
  return radians * kRadiansToDegrees;
20
21
  }
21
22
 
23
+ static inline double clampMercatorLatitude(double lat) {
24
+ static constexpr double kMaxMercatorLatitude = 85.05112878;
25
+ if (lat > kMaxMercatorLatitude) return kMaxMercatorLatitude;
26
+ if (lat < -kMaxMercatorLatitude) return -kMaxMercatorLatitude;
27
+ return lat;
28
+ }
29
+
30
+ static inline double mercatorX01(double lon) {
31
+ double wrapped = std::fmod(lon + 180.0, 360.0);
32
+ if (wrapped < 0.0) {
33
+ wrapped += 360.0;
34
+ }
35
+ return wrapped / 360.0;
36
+ }
37
+
38
+ static inline double mercatorY01(double lat) {
39
+ const double clampedLat = clampMercatorLatitude(lat);
40
+ const double latRad = geo_toRadians(clampedLat);
41
+ const double y = (1.0 - std::asinh(std::tan(latRad)) / kPi) * 0.5;
42
+ if (y < 0.0) return 0.0;
43
+ if (y > 1.0) return 1.0;
44
+ return y;
45
+ }
46
+
47
+ static double wrappedSpan01(std::vector<double> xs) {
48
+ if (xs.size() <= 1) {
49
+ return 0.0;
50
+ }
51
+
52
+ std::sort(xs.begin(), xs.end());
53
+ double maxGap = 0.0;
54
+
55
+ for (size_t i = 0; i + 1 < xs.size(); ++i) {
56
+ const double gap = xs[i + 1] - xs[i];
57
+ if (gap > maxGap) {
58
+ maxGap = gap;
59
+ }
60
+ }
61
+
62
+ const double endGap = xs.front() + 1.0 - xs.back();
63
+ if (endGap > maxGap) {
64
+ maxGap = endGap;
65
+ }
66
+
67
+ const double span = 1.0 - maxGap;
68
+ return span < 0.0 ? 0.0 : span;
69
+ }
70
+
22
71
  double calculateDistance(double lat1, double lon1, double lat2, double lon2) {
23
72
  const double radLat1 = geo_toRadians(lat1);
24
73
  const double radLat2 = geo_toRadians(lat2);
@@ -512,6 +561,69 @@ GeoPoint pixelToLatLng(double x, double y, int zoom) {
512
561
  return {lat, lon};
513
562
  }
514
563
 
564
+ double calculateFitZoomForPoints(
565
+ const std::vector<GeoPoint>& points,
566
+ double viewportWidthPx,
567
+ double viewportHeightPx,
568
+ double paddingPx,
569
+ int minZoom,
570
+ int maxZoom
571
+ ) {
572
+ if (minZoom > maxZoom) {
573
+ std::swap(minZoom, maxZoom);
574
+ }
575
+ if (points.empty()) {
576
+ return static_cast<double>(minZoom);
577
+ }
578
+ if (points.size() == 1) {
579
+ return static_cast<double>(maxZoom);
580
+ }
581
+
582
+ const double safeViewportWidth = viewportWidthPx > 1.0 ? viewportWidthPx : 390.0;
583
+ const double safeViewportHeight = viewportHeightPx > 1.0 ? viewportHeightPx : 844.0;
584
+ const double safePadding = std::max(0.0, paddingPx);
585
+ const double availableWidth = std::max(1.0, safeViewportWidth - safePadding * 2.0);
586
+ const double availableHeight = std::max(1.0, safeViewportHeight - safePadding * 2.0);
587
+
588
+ std::vector<double> projectedXs;
589
+ projectedXs.reserve(points.size());
590
+
591
+ double minY = std::numeric_limits<double>::max();
592
+ double maxY = -std::numeric_limits<double>::max();
593
+
594
+ for (const auto& p : points) {
595
+ projectedXs.push_back(mercatorX01(p.lon));
596
+ const double y = mercatorY01(p.lat);
597
+ if (y < minY) minY = y;
598
+ if (y > maxY) maxY = y;
599
+ }
600
+
601
+ const double spanX = wrappedSpan01(projectedXs);
602
+ const double spanY = std::max(0.0, maxY - minY);
603
+ static constexpr double kTileSize = 256.0;
604
+ static constexpr double kSpanEpsilon = 1e-12;
605
+
606
+ const double zoomX = spanX <= kSpanEpsilon
607
+ ? static_cast<double>(maxZoom)
608
+ : std::log2(availableWidth / (kTileSize * spanX));
609
+ const double zoomY = spanY <= kSpanEpsilon
610
+ ? static_cast<double>(maxZoom)
611
+ : std::log2(availableHeight / (kTileSize * spanY));
612
+
613
+ double fitZoom = std::min(zoomX, zoomY);
614
+ if (!std::isfinite(fitZoom)) {
615
+ fitZoom = static_cast<double>(minZoom);
616
+ }
617
+
618
+ if (fitZoom < static_cast<double>(minZoom)) {
619
+ fitZoom = static_cast<double>(minZoom);
620
+ } else if (fitZoom > static_cast<double>(maxZoom)) {
621
+ fitZoom = static_cast<double>(maxZoom);
622
+ }
623
+
624
+ return fitZoom;
625
+ }
626
+
515
627
  // --- 批量地理围栏与热力图 ---
516
628
 
517
629
  int findPointInPolygons(double pointLat, double pointLon, const std::vector<std::vector<GeoPoint>>& polygons) {
@@ -125,6 +125,27 @@ PixelResult latLngToPixel(double lat, double lon, int zoom);
125
125
  */
126
126
  GeoPoint pixelToLatLng(double x, double y, int zoom);
127
127
 
128
+ /**
129
+ * 根据一组坐标点和视口尺寸计算“可同时看到所有点”的推荐缩放级别。
130
+ * 使用 Web Mercator 投影,在跨经线场景下会自动取更小经度跨度。
131
+ *
132
+ * @param points 坐标点集合,至少 1 个
133
+ * @param viewportWidthPx 视口宽度(像素)
134
+ * @param viewportHeightPx 视口高度(像素)
135
+ * @param paddingPx 四周预留的内边距(像素)
136
+ * @param minZoom 最小缩放级别
137
+ * @param maxZoom 最大缩放级别
138
+ * @return 推荐 zoom(已在 [minZoom, maxZoom] 范围内)
139
+ */
140
+ double calculateFitZoomForPoints(
141
+ const std::vector<GeoPoint>& points,
142
+ double viewportWidthPx,
143
+ double viewportHeightPx,
144
+ double paddingPx,
145
+ int minZoom,
146
+ int maxZoom
147
+ );
148
+
128
149
  // --- 批量地理围栏与热力图 ---
129
150
 
130
151
  /**
@@ -167,6 +167,21 @@ void testGeometryEngineExtended() {
167
167
  // Test trailing semicolon
168
168
  assert(parsePolyline("116.4074,39.9042;").size() == 1);
169
169
 
170
+ // 10. calculateFitZoomForPoints
171
+ std::vector<GeoPoint> nearby = {
172
+ {39.9042, 116.4074}, // Beijing
173
+ {39.9142, 116.4174}
174
+ };
175
+ std::vector<GeoPoint> farAway = {
176
+ {39.9042, 116.4074}, // Beijing
177
+ {31.2304, 121.4737} // Shanghai
178
+ };
179
+ const double nearZoom = calculateFitZoomForPoints(nearby, 390, 844, 48, 3, 20);
180
+ const double farZoom = calculateFitZoomForPoints(farAway, 390, 844, 48, 3, 20);
181
+ assert(nearZoom > farZoom);
182
+ assert(nearZoom <= 20.0 && nearZoom >= 3.0);
183
+ assert(farZoom <= 20.0 && farZoom >= 3.0);
184
+
170
185
  std::cout << "PASSED" << std::endl;
171
186
  }
172
187