expo-gaode-map-navigation 2.0.12 → 2.0.13

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 (51) hide show
  1. package/README.md +7 -2
  2. package/android/build.gradle +8 -4
  3. package/android/src/main/AndroidManifest.xml +8 -0
  4. package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapOfflineModule.kt +83 -15
  5. package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapView.kt +13 -3
  6. package/android/src/main/java/expo/modules/gaodemap/map/managers/UIManager.kt +36 -39
  7. package/android/src/main/java/expo/modules/gaodemap/map/overlays/ClusterView.kt +5 -2
  8. package/android/src/main/java/expo/modules/gaodemap/map/overlays/HeatMapView.kt +122 -10
  9. package/android/src/main/java/expo/modules/gaodemap/map/overlays/HeatMapViewModule.kt +2 -2
  10. package/android/src/main/java/expo/modules/gaodemap/map/search/ExpoGaodeMapSearchModule.kt +751 -0
  11. package/build/map/ExpoGaodeMapOfflineModule.d.ts +5 -0
  12. package/build/map/ExpoGaodeMapOfflineModule.d.ts.map +1 -1
  13. package/build/map/ExpoGaodeMapOfflineModule.js.map +1 -1
  14. package/build/map/components/overlays/HeatMap.d.ts.map +1 -1
  15. package/build/map/components/overlays/HeatMap.js +21 -2
  16. package/build/map/components/overlays/HeatMap.js.map +1 -1
  17. package/build/map/index.d.ts +3 -0
  18. package/build/map/index.d.ts.map +1 -1
  19. package/build/map/index.js +3 -0
  20. package/build/map/index.js.map +1 -1
  21. package/build/map/search/ExpoGaodeMapSearch.types.d.ts +340 -0
  22. package/build/map/search/ExpoGaodeMapSearch.types.d.ts.map +1 -0
  23. package/build/map/search/ExpoGaodeMapSearch.types.js +19 -0
  24. package/build/map/search/ExpoGaodeMapSearch.types.js.map +1 -0
  25. package/build/map/search/ExpoGaodeMapSearchModule.d.ts +74 -0
  26. package/build/map/search/ExpoGaodeMapSearchModule.d.ts.map +1 -0
  27. package/build/map/search/ExpoGaodeMapSearchModule.js +47 -0
  28. package/build/map/search/ExpoGaodeMapSearchModule.js.map +1 -0
  29. package/build/map/search/index.d.ts +156 -0
  30. package/build/map/search/index.d.ts.map +1 -0
  31. package/build/map/search/index.js +171 -0
  32. package/build/map/search/index.js.map +1 -0
  33. package/build/map/types/map-view.types.d.ts +4 -2
  34. package/build/map/types/map-view.types.d.ts.map +1 -1
  35. package/build/map/types/map-view.types.js.map +1 -1
  36. package/build/map/utils/ErrorHandler.js +11 -11
  37. package/build/map/utils/ErrorHandler.js.map +1 -1
  38. package/build/map/utils/OfflineMapManager.d.ts +4 -0
  39. package/build/map/utils/OfflineMapManager.d.ts.map +1 -1
  40. package/build/map/utils/OfflineMapManager.js +6 -0
  41. package/build/map/utils/OfflineMapManager.js.map +1 -1
  42. package/expo-module.config.json +4 -2
  43. package/ios/ExpoGaodeMapNaviView.swift +16 -17
  44. package/ios/ExpoGaodeMapNavigation.podspec +2 -1
  45. package/ios/map/ExpoGaodeMapOfflineModule.swift +61 -0
  46. package/ios/map/ExpoGaodeMapSearchModule.swift +773 -0
  47. package/ios/map/modules/LocationManager.swift +9 -3
  48. package/ios/map/overlays/PolylineView.swift +6 -12
  49. package/package.json +1 -1
  50. package/plugin/build/withGaodeMap.js +12 -0
  51. package/android/src/main/java/expo/modules/gaodemap/navigation/managers/RouteCalculator.kt +0 -173
package/README.md CHANGED
@@ -5,6 +5,7 @@
5
5
  ## 特性
6
6
 
7
7
  - 🗺️ **地图渲染**:内置完整地图能力,支持 Marker、Polyline、Polygon、Circle、Cluster、HeatMap 等覆盖物。
8
+ - 🔍 **原生搜索**:内置 POI 搜索、周边搜索、沿途搜索、输入提示、逆地理编码等搜索能力。
8
9
  - 🚗 **多模式路径规划**:支持驾车、步行、骑行、电动车、货车、摩托车等多种出行方式。
9
10
  - 🧭 **实时导航 UI**:提供 `NaviView` 官方嵌入视图,并暴露完整事件与原生参数,方便你自行定制导航界面。
10
11
  - 🛣️ **独立路径规划**:支持“先算路、再导航”的高级模式,可实现多路线对比与选择。
@@ -13,7 +14,7 @@
13
14
 
14
15
  ## 安装
15
16
 
16
- 本模块已包含地图与导航的所有能力,**不需要**、也不应同时安装 `expo-gaode-map`。
17
+ 本模块已包含地图、搜索与导航能力,**不需要**、也不应同时安装 `expo-gaode-map` 或 `expo-gaode-map-search`。
17
18
 
18
19
  ```bash
19
20
  # bun
@@ -29,6 +30,10 @@ npm install expo-gaode-map-navigation
29
30
  **⚠️ 重要提示:**
30
31
  如果项目中已安装 `expo-gaode-map`,请务必先卸载,否则会导致 Android 端二进制冲突(`3dmap` vs `navi-3dmap`)。`expo-gaode-map` 和 `expo-gaode-map-navigation` 由于 SDK 冲突不能同时安装,二选一使用。
31
32
 
33
+ `expo-gaode-map-search` 的独立集成只维护到 `2.2.33`。从下个版本开始,搜索能力随 `expo-gaode-map` / `expo-gaode-map-navigation` 一起维护;在导航包中请直接从 `expo-gaode-map-navigation` 导入搜索 API。
34
+
35
+ 高德官方 Android SDK 在 `10.0.700` 之后将远程依赖由“地图 + 定位”调整为“地图 + 定位 + 搜索”,依赖地址从 `com.amap.api:3dmap:latest.integration` 调整为 `com.amap.api:3dmap-location-search:latest.integration`。继续单独维护 search 模块会带来重复合包和依赖冲突成本,因此搜索能力改为随 core / navigation 一起维护。
36
+
32
37
  > ⚠️ **版本兼容性说明**:
33
38
  > - 如果你的项目使用 **Expo SDK 54 及以上**,请安装 默认的 版本。
34
39
  > - 如果你的项目使用 **Expo SDK 53 及以下**(如 50, 51, 52, 53),请使用 **V1** 版本(Tag: `v1`)。
@@ -643,7 +648,7 @@ const result = await calculateTransitRoute({
643
648
 
644
649
  - [在线文档](https://tomwq.github.io/expo-gaode-map/api/navigation.html)
645
650
  - [GitHub 仓库](https://github.com/TomWq/expo-gaode-map/packages/navigation)
646
- - [示例项目(导航)](https://github.com/TomWq/expo-gaode-map-navigation-example)
651
+ - [导航示例工程](../../example-navigation)
647
652
  - [高德地图开放平台](https://lbs.amap.com/)
648
653
  - [Expo Modules API](https://docs.expo.dev/modules/overview/)
649
654
 
@@ -1,7 +1,7 @@
1
1
  apply plugin: 'com.android.library'
2
2
 
3
3
  group = 'expo.modules.gaodemap.navigation'
4
- version = '2.0.0'
4
+ version = '2.0.13'
5
5
 
6
6
  def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
7
7
  apply from: expoModulesCorePlugin
@@ -34,7 +34,7 @@ android {
34
34
  namespace "expo.modules.gaodemap.navigation"
35
35
  defaultConfig {
36
36
  versionCode 1
37
- versionName "2.0.0"
37
+ versionName "2.0.13"
38
38
  }
39
39
  lintOptions {
40
40
  abortOnError false
@@ -60,10 +60,14 @@ dependencies {
60
60
  implementation files(sdkFile)
61
61
  } else {
62
62
  println "ExpoGaodeMapNavigation: Custom SDK path configured but file not found, falling back to Maven dependency."
63
- implementation 'com.amap.api:navi-3dmap:latest.integration'
63
+ implementation 'com.amap.api:navi-3dmap-location-search:10.1.600_3dmap10.1.600_loc6.5.1_sea9.7.4'
64
+
64
65
  }
65
66
  } else {
66
- implementation 'com.amap.api:navi-3dmap:latest.integration'
67
+ // 10.1.600 is the last Android 3D map SDK line that supports offline
68
+ // map downloads without the separate 11.x offline-download permission.
69
+ implementation 'com.amap.api:navi-3dmap-location-search:10.1.600_3dmap10.1.600_loc6.5.1_sea9.7.4'
70
+
67
71
  }
68
72
 
69
73
  // Live Update APIs on NotificationCompat.Builder
@@ -21,4 +21,12 @@
21
21
  <!-- 蓝牙权限 - 用于室内定位 (可选) -->
22
22
  <uses-permission android:name="android.permission.BLUETOOTH" />
23
23
  <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
24
+
25
+ <application>
26
+ <!-- 高德 3D 地图 SDK 官方离线地图 UI 组件 -->
27
+ <activity
28
+ android:name="com.amap.api.maps.offlinemap.OfflineMapActivity"
29
+ android:screenOrientation="portrait"
30
+ android:exported="false" />
31
+ </application>
24
32
  </manifest>
@@ -1,14 +1,19 @@
1
1
  package expo.modules.gaodemap.map
2
2
 
3
+ import android.app.Activity
4
+ import android.content.Context
5
+ import android.content.Intent
3
6
  import android.os.Bundle
4
7
  import android.os.StatFs
5
8
  import android.os.Environment
6
9
  import android.util.Log
10
+ import com.amap.api.maps.offlinemap.OfflineMapActivity
7
11
  import com.amap.api.maps.offlinemap.OfflineMapCity
8
12
  import com.amap.api.maps.offlinemap.OfflineMapManager
9
13
  import com.amap.api.maps.offlinemap.OfflineMapProvince
10
14
  import com.amap.api.maps.offlinemap.OfflineMapStatus
11
15
  import expo.modules.gaodemap.map.modules.SDKInitializer
16
+ import expo.modules.kotlin.exception.CodedException
12
17
  import expo.modules.kotlin.modules.Module
13
18
  import expo.modules.kotlin.modules.ModuleDefinition
14
19
 
@@ -92,12 +97,18 @@ class ExpoGaodeMapOfflineModule : Module() {
92
97
  offlineMapManager = null
93
98
  downloadingCities.clear()
94
99
  }
100
+
101
+ AsyncFunction("openOfflineMapUI") {
102
+ openOfflineMapUI()
103
+ }
95
104
 
96
105
  // ==================== 地图列表管理 ====================
97
106
 
98
107
  AsyncFunction("getAvailableCities") {
99
- val cities = getOfflineMapManager()?.offlineMapCityList ?: emptyList()
100
- cities.map { city -> convertCityToMap(city) }
108
+ val manager = getOfflineMapManager()
109
+ val cities = manager?.offlineMapCityList ?: emptyList()
110
+ val downloadedCityKeys = getDownloadedCityKeys(manager)
111
+ cities.map { city -> convertCityToMap(city, downloadedCityKeys) }
101
112
  }
102
113
 
103
114
  AsyncFunction("getAvailableProvinces") {
@@ -106,15 +117,19 @@ class ExpoGaodeMapOfflineModule : Module() {
106
117
  }
107
118
 
108
119
  AsyncFunction("getCitiesByProvince") { provinceCode: String ->
109
- val province = getOfflineMapManager()?.offlineMapProvinceList?.find {
110
- it.provinceCode == provinceCode
120
+ val manager = getOfflineMapManager()
121
+ val province = manager?.offlineMapProvinceList?.find {
122
+ it.provinceCode == provinceCode
111
123
  }
112
- province?.cityList?.map { city -> convertCityToMap(city) } ?: emptyList()
124
+ val downloadedCityKeys = getDownloadedCityKeys(manager)
125
+ province?.cityList?.map { city -> convertCityToMap(city, downloadedCityKeys) } ?: emptyList()
113
126
  }
114
127
 
115
128
  AsyncFunction("getDownloadedMaps") {
116
- val cities = getOfflineMapManager()?.downloadOfflineMapCityList ?: emptyList()
117
- cities.map { city -> convertCityToMap(city) }
129
+ val manager = getOfflineMapManager()
130
+ val cities = manager?.downloadOfflineMapCityList ?: emptyList()
131
+ val downloadedCityKeys = getDownloadedCityKeys(manager)
132
+ cities.map { city -> convertCityToMap(city, downloadedCityKeys) }
118
133
  }
119
134
 
120
135
  // ==================== 下载管理 ====================
@@ -208,14 +223,17 @@ class ExpoGaodeMapOfflineModule : Module() {
208
223
  // ==================== 状态查询 ====================
209
224
 
210
225
  AsyncFunction("isMapDownloaded") { cityCode: String ->
211
- val city = getOfflineMapManager()?.getItemByCityCode(cityCode)
212
- city?.state == OfflineMapStatus.SUCCESS ||
213
- city?.state == OfflineMapStatus.CHECKUPDATES
226
+ val manager = getOfflineMapManager()
227
+ val city = manager?.getItemByCityCode(cityCode)
228
+ city?.state == OfflineMapStatus.SUCCESS ||
229
+ city?.let { isDownloadedCity(it, getDownloadedCityKeys(manager)) } == true
214
230
  }
215
231
 
216
232
  AsyncFunction("getMapStatus") { cityCode: String ->
217
- val city = getOfflineMapManager()?.getItemByCityCode(cityCode)
218
- city?.let { convertCityToMap(it) } ?: Bundle()
233
+ val manager = getOfflineMapManager()
234
+ val city = manager?.getItemByCityCode(cityCode)
235
+ val downloadedCityKeys = getDownloadedCityKeys(manager)
236
+ city?.let { convertCityToMap(it, downloadedCityKeys) } ?: Bundle()
219
237
  }
220
238
 
221
239
  AsyncFunction("getTotalProgress") {
@@ -463,13 +481,37 @@ class ExpoGaodeMapOfflineModule : Module() {
463
481
  /**
464
482
  * 转换城市对象为 Map
465
483
  */
466
- private fun convertCityToMap(city: OfflineMapCity): Bundle {
484
+ private fun getDownloadedCityKeys(manager: OfflineMapManager?): Set<String> {
485
+ return manager?.downloadOfflineMapCityList
486
+ ?.flatMap { getCityIdentityKeys(it) }
487
+ ?.toSet()
488
+ ?: emptySet()
489
+ }
490
+
491
+ private fun getCityIdentityKeys(city: OfflineMapCity): List<String> {
492
+ return listOfNotNull(
493
+ city.code?.trim()?.takeIf { it.isNotEmpty() }?.let { "code:$it" },
494
+ city.adcode?.trim()?.takeIf { it.isNotEmpty() }?.let { "adcode:$it" },
495
+ city.city?.trim()?.takeIf { it.isNotEmpty() }?.let { "name:$it" }
496
+ )
497
+ }
498
+
499
+ private fun isDownloadedCity(city: OfflineMapCity, downloadedCityKeys: Set<String>): Boolean {
500
+ return getCityIdentityKeys(city).any { downloadedCityKeys.contains(it) }
501
+ }
502
+
503
+ private fun convertCityToMap(
504
+ city: OfflineMapCity,
505
+ downloadedCityKeys: Set<String> = emptySet()
506
+ ): Bundle {
467
507
  val isPaused = synchronized(lock) { pausedCities.contains(city.code) }
468
508
  val isDownloading = synchronized(lock) { downloadingCities.contains(city.code) }
509
+ val isDownloaded = isDownloadedCity(city, downloadedCityKeys)
469
510
 
470
511
  val status = when {
471
512
  isPaused -> "paused"
472
513
  isDownloading -> "downloading"
514
+ isDownloaded -> "downloaded"
473
515
  else -> getStatusString(city.state)
474
516
  }
475
517
 
@@ -501,8 +543,9 @@ class ExpoGaodeMapOfflineModule : Module() {
501
543
  }
502
544
 
503
545
  /**
504
- * 获取状态字符串
505
- * 注意:只有 SUCCESS 状态才表示真正下载完成
546
+ * 获取状态字符串。
547
+ * CHECKUPDATES / NEW_VERSION 在 10.1.600 冷启动时可能出现在普通城市列表,
548
+ * 不能单独作为已下载依据;已下载状态以 downloadOfflineMapCityList 为准。
506
549
  */
507
550
  private fun getStatusString(state: Int): String {
508
551
  return when (state) {
@@ -558,4 +601,29 @@ class ExpoGaodeMapOfflineModule : Module() {
558
601
  null
559
602
  }
560
603
  }
604
+
605
+ private fun getActivityLaunchContext(): Context {
606
+ return appContext.currentActivity
607
+ ?: appContext.reactContext
608
+ ?: throw CodedException("NO_CONTEXT", "React context not available", null)
609
+ }
610
+
611
+ private fun openOfflineMapUI() {
612
+ val context = getActivityLaunchContext()
613
+ SDKInitializer.restorePersistedState(context.applicationContext)
614
+
615
+ if (!SDKInitializer.isPrivacyReady()) {
616
+ throw CodedException(
617
+ "PRIVACY_NOT_AGREED",
618
+ "隐私协议未完成确认,请先调用 setPrivacyConfig",
619
+ null
620
+ )
621
+ }
622
+
623
+ val intent = Intent(context, OfflineMapActivity::class.java)
624
+ if (context !is Activity) {
625
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
626
+ }
627
+ context.startActivity(intent)
628
+ }
561
629
  }
@@ -150,6 +150,7 @@ class ExpoGaodeMapView(context: Context, appContext: AppContext) : ExpoView(cont
150
150
  // 缓存初始相机位置,等待地图加载完成后设置
151
151
  private var pendingCameraPosition: Map<String, Any?>? = null
152
152
  private var isMapLoaded = false
153
+ private var hasAppliedInitialCameraPosition = false
153
154
 
154
155
  init {
155
156
  try {
@@ -192,7 +193,7 @@ class ExpoGaodeMapView(context: Context, appContext: AppContext) : ExpoView(cont
192
193
 
193
194
  val positionToApply = initialCameraPosition ?: pendingCameraPosition
194
195
  positionToApply?.let { position ->
195
- applyInitialCameraPosition(position)
196
+ applyInitialCameraPositionIfNeeded(position)
196
197
  pendingCameraPosition = null
197
198
  }
198
199
 
@@ -351,10 +352,14 @@ class ExpoGaodeMapView(context: Context, appContext: AppContext) : ExpoView(cont
351
352
  fun setInitialCameraPosition(position: Map<String, Any?>) {
352
353
  initialCameraPosition = position
353
354
 
355
+ if (hasAppliedInitialCameraPosition) {
356
+ return
357
+ }
358
+
354
359
  // 如果地图已加载,立即应用;否则缓存等待地图加载完成
355
360
  if (isMapLoaded) {
356
361
  mainHandler.post {
357
- applyInitialCameraPosition(position)
362
+ applyInitialCameraPositionIfNeeded(position)
358
363
  }
359
364
  } else {
360
365
  pendingCameraPosition = position
@@ -365,8 +370,12 @@ class ExpoGaodeMapView(context: Context, appContext: AppContext) : ExpoView(cont
365
370
  * 实际应用相机位置
366
371
  * @param position 相机位置配置
367
372
  */
368
- private fun applyInitialCameraPosition(position: Map<String, Any?>) {
373
+ private fun applyInitialCameraPositionIfNeeded(position: Map<String, Any?>) {
374
+ if (hasAppliedInitialCameraPosition) {
375
+ return
376
+ }
369
377
  cameraManager.setInitialCameraPosition(position)
378
+ hasAppliedInitialCameraPosition = true
370
379
  }
371
380
 
372
381
  // ==================== UI 控件和手势 ====================
@@ -643,6 +652,7 @@ class ExpoGaodeMapView(context: Context, appContext: AppContext) : ExpoView(cont
643
652
 
644
653
  // 销毁地图实例
645
654
  mapView.onDestroy()
655
+ hasAppliedInitialCameraPosition = false
646
656
  } catch (e: Exception) {
647
657
  // 静默处理异常,确保销毁流程不会中断
648
658
  android.util.Log.e("ExpoGaodeMapView", "Error destroying map", e)
@@ -87,36 +87,46 @@ class UIManager(private val aMap: AMap, private val context: Context) : Location
87
87
  // ==================== 图层显示 ====================
88
88
 
89
89
  private var currentLocationStyle: MyLocationStyle? = null
90
+ private var currentFollowUserLocation: Boolean = false
91
+ private var explicitLocationType: Int? = null
92
+
93
+ private fun parseLocationType(locationType: String): Int? {
94
+ return when (locationType) {
95
+ "SHOW" -> MyLocationStyle.LOCATION_TYPE_SHOW
96
+ "LOCATE" -> MyLocationStyle.LOCATION_TYPE_LOCATE
97
+ "FOLLOW" -> MyLocationStyle.LOCATION_TYPE_FOLLOW
98
+ "MAP_ROTATE" -> MyLocationStyle.LOCATION_TYPE_MAP_ROTATE
99
+ "LOCATION_ROTATE" -> MyLocationStyle.LOCATION_TYPE_LOCATION_ROTATE
100
+ "LOCATION_ROTATE_NO_CENTER" -> MyLocationStyle.LOCATION_TYPE_LOCATION_ROTATE_NO_CENTER
101
+ "FOLLOW_NO_CENTER" -> MyLocationStyle.LOCATION_TYPE_FOLLOW_NO_CENTER
102
+ "MAP_ROTATE_NO_CENTER" -> MyLocationStyle.LOCATION_TYPE_MAP_ROTATE_NO_CENTER
103
+ else -> null
104
+ }
105
+ }
106
+
107
+ private fun resolveLocationType(followUserLocation: Boolean): Int {
108
+ explicitLocationType?.let { return it }
109
+ return if (followUserLocation) {
110
+ MyLocationStyle.LOCATION_TYPE_FOLLOW
111
+ } else {
112
+ MyLocationStyle.LOCATION_TYPE_LOCATION_ROTATE_NO_CENTER
113
+ }
114
+ }
90
115
 
91
116
  /**
92
117
  * 设置是否显示用户位置
93
118
  */
94
119
  fun setShowsUserLocation(show: Boolean, followUserLocation: Boolean = false) {
120
+ currentFollowUserLocation = followUserLocation
95
121
  if (show) {
96
- // 创建默认的定位样式
97
122
  if (currentLocationStyle == null) {
98
- currentLocationStyle = MyLocationStyle().apply {
99
- // 根据是否跟随设置定位类型
100
- val locationType = if (followUserLocation) {
101
- MyLocationStyle.LOCATION_TYPE_FOLLOW // 连续定位并跟随
102
- } else {
103
- MyLocationStyle.LOCATION_TYPE_SHOW // 只显示定位点,不跟随
104
- }
105
- myLocationType(locationType)
106
- interval(2000) // 2秒定位一次
107
- showMyLocation(true)
108
- }
109
- } else {
110
- // 更新定位类型
111
- val locationType = if (followUserLocation) {
112
- MyLocationStyle.LOCATION_TYPE_FOLLOW
113
- } else {
114
- MyLocationStyle.LOCATION_TYPE_SHOW
115
- }
116
- currentLocationStyle?.apply {
117
- myLocationType(locationType)
118
- interval(2000)
119
- }
123
+ currentLocationStyle = MyLocationStyle()
124
+ }
125
+
126
+ currentLocationStyle?.apply {
127
+ myLocationType(resolveLocationType(followUserLocation))
128
+ interval(2000) // 2秒定位一次
129
+ showMyLocation(true)
120
130
  }
121
131
 
122
132
  // 监听定位变化(用于通知 React Native)
@@ -171,9 +181,10 @@ class UIManager(private val aMap: AMap, private val context: Context) : Location
171
181
  */
172
182
  @SuppressLint("DiscouragedApi")
173
183
  fun setUserLocationRepresentation(config: Map<String, Any>) {
184
+ explicitLocationType = (config["locationType"] as? String)?.let(::parseLocationType)
185
+
174
186
  if (currentLocationStyle == null) {
175
187
  currentLocationStyle = MyLocationStyle().apply {
176
- myLocationType(MyLocationStyle.LOCATION_TYPE_LOCATION_ROTATE)
177
188
  interval(2000)
178
189
  showMyLocation(true)
179
190
  }
@@ -182,21 +193,7 @@ class UIManager(private val aMap: AMap, private val context: Context) : Location
182
193
  val style = currentLocationStyle!!
183
194
 
184
195
  // 定位蓝点展现模式 (locationType) - Android 支持8种模式
185
- val locationType = config["locationType"] as? String
186
- if (locationType != null) {
187
- val locationTypeValue = when (locationType) {
188
- "SHOW" -> MyLocationStyle.LOCATION_TYPE_SHOW
189
- "LOCATE" -> MyLocationStyle.LOCATION_TYPE_LOCATE
190
- "FOLLOW" -> MyLocationStyle.LOCATION_TYPE_FOLLOW
191
- "MAP_ROTATE" -> MyLocationStyle.LOCATION_TYPE_MAP_ROTATE
192
- "LOCATION_ROTATE" -> MyLocationStyle.LOCATION_TYPE_LOCATION_ROTATE
193
- "LOCATION_ROTATE_NO_CENTER" -> MyLocationStyle.LOCATION_TYPE_LOCATION_ROTATE_NO_CENTER
194
- "FOLLOW_NO_CENTER" -> MyLocationStyle.LOCATION_TYPE_FOLLOW_NO_CENTER
195
- "MAP_ROTATE_NO_CENTER" -> MyLocationStyle.LOCATION_TYPE_MAP_ROTATE_NO_CENTER
196
- else -> MyLocationStyle.LOCATION_TYPE_LOCATION_ROTATE // 默认值
197
- }
198
- style.myLocationType(locationTypeValue)
199
- }
196
+ style.myLocationType(resolveLocationType(currentFollowUserLocation))
200
197
 
201
198
  // 是否显示定位蓝点 (showMyLocation) - Android 5.1.0+ 支持
202
199
  // 注意:这个属性在 iOS 中没有对应项,是 Android 特有的
@@ -141,8 +141,11 @@ class ClusterView(context: Context, appContext: AppContext) : ExpoView(context,
141
141
  * 设置最小聚合数量
142
142
  */
143
143
  fun setMinClusterSize(size: Int) {
144
- Log.d("ClusterView", "setMinClusterSize: $size")
145
- minClusterSize = size
144
+ val nextSize = size.coerceAtLeast(1)
145
+ if (minClusterSize != nextSize) {
146
+ minClusterSize = nextSize
147
+ styleChanged = true
148
+ }
146
149
  updateClusters()
147
150
  }
148
151
 
@@ -1,21 +1,27 @@
1
1
  package expo.modules.gaodemap.map.overlays
2
2
 
3
3
  import android.content.Context
4
+ import android.graphics.Color
4
5
  import android.os.Looper
5
6
  import android.util.Log
6
7
  import com.amap.api.maps.AMap
8
+ import com.amap.api.maps.CameraUpdateFactory
9
+ import com.amap.api.maps.model.Gradient
10
+ import com.amap.api.maps.model.HeatmapTileProvider
7
11
  import com.amap.api.maps.model.LatLng
8
12
  import com.amap.api.maps.model.TileOverlay
9
13
  import com.amap.api.maps.model.TileOverlayOptions
10
- import com.amap.api.maps.model.HeatmapTileProvider
11
- import com.amap.api.maps.CameraUpdateFactory
14
+ import com.amap.api.maps.model.WeightedLatLng
12
15
  import expo.modules.kotlin.AppContext
13
16
  import expo.modules.kotlin.views.ExpoView
17
+ import expo.modules.gaodemap.map.utils.LatLngParser
14
18
  import java.util.concurrent.ExecutorService
15
19
  import java.util.concurrent.Executors
16
- import expo.modules.gaodemap.map.utils.LatLngParser
17
20
 
18
21
  class HeatMapView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
22
+ private companion object {
23
+ const val TAG = "HeatMapView"
24
+ }
19
25
 
20
26
  private val executor: ExecutorService = Executors.newSingleThreadExecutor()
21
27
  private val applyUpdateRunnable = Runnable { applyUpdateOnMain() }
@@ -25,9 +31,10 @@ class HeatMapView(context: Context, appContext: AppContext) : ExpoView(context,
25
31
 
26
32
  private var heatmapOverlay: TileOverlay? = null
27
33
  private var aMap: AMap? = null
28
- private var dataList: MutableList<LatLng> = mutableListOf()
34
+ private var dataList: MutableList<WeightedLatLng> = mutableListOf()
29
35
  private var radius: Int = 50
30
36
  private var opacity: Double = 0.6
37
+ private var gradient: Gradient? = null
31
38
 
32
39
  /**
33
40
  * 设置地图实例
@@ -36,6 +43,7 @@ class HeatMapView(context: Context, appContext: AppContext) : ExpoView(context,
36
43
  fun setMap(map: AMap) {
37
44
  aMap = map
38
45
  needsRebuild = true
46
+ Log.d(TAG, "setMap: map attached")
39
47
  scheduleUpdate()
40
48
  }
41
49
 
@@ -46,7 +54,8 @@ class HeatMapView(context: Context, appContext: AppContext) : ExpoView(context,
46
54
  */
47
55
  fun setData(data: List<Any>?) {
48
56
  dataList.clear()
49
- dataList.addAll(LatLngParser.parseLatLngList(data))
57
+ dataList.addAll(parseWeightedLatLngList(data))
58
+ Log.d(TAG, "setData: raw=${data?.size ?: 0}, parsed=${dataList.size}, ${formatPointStats(dataList)}")
50
59
  needsRebuild = true
51
60
  scheduleUpdate()
52
61
  }
@@ -56,6 +65,7 @@ class HeatMapView(context: Context, appContext: AppContext) : ExpoView(context,
56
65
  */
57
66
  fun setRadius(radiusValue: Int) {
58
67
  radius = radiusValue
68
+ Log.d(TAG, "setRadius: $radiusValue")
59
69
  needsRebuild = true
60
70
  scheduleUpdate()
61
71
  }
@@ -65,10 +75,20 @@ class HeatMapView(context: Context, appContext: AppContext) : ExpoView(context,
65
75
  */
66
76
  fun setOpacity(opacityValue: Double) {
67
77
  opacity = opacityValue
68
- applyOverlayOpacity()
78
+ Log.d(TAG, "setOpacity: $opacityValue")
79
+ needsRebuild = true
80
+ scheduleUpdate()
81
+ }
82
+
83
+ fun setGradient(gradientValue: Map<String, Any>?) {
84
+ gradient = parseGradient(gradientValue)
85
+ Log.d(TAG, "setGradient: hasGradient=${gradient != null}, raw=${gradientValue != null}")
86
+ needsRebuild = true
87
+ scheduleUpdate()
69
88
  }
70
89
 
71
90
  fun setVisible(visibleValue: Boolean) {
91
+ Log.d(TAG, "setVisible: $visibleValue, points=${dataList.size}, hasOverlay=${heatmapOverlay != null}, needsRebuild=$needsRebuild")
72
92
  if (!visibleValue) {
73
93
  visible = false
74
94
  updateToken += 1
@@ -95,6 +115,7 @@ class HeatMapView(context: Context, appContext: AppContext) : ExpoView(context,
95
115
  private fun scheduleUpdate() {
96
116
  updateToken += 1
97
117
  removeCallbacks(applyUpdateRunnable)
118
+ Log.d(TAG, "scheduleUpdate: token=$updateToken, mapAttached=${aMap != null}, visible=$visible, points=${dataList.size}")
98
119
  postDelayed(applyUpdateRunnable, 32)
99
120
  }
100
121
 
@@ -142,8 +163,11 @@ class HeatMapView(context: Context, appContext: AppContext) : ExpoView(context,
142
163
  val map = aMap ?: return
143
164
  val token = updateToken
144
165
  val pointsSnapshot = ArrayList(dataList)
166
+ val latLngSnapshot = pointsSnapshot.map { it.latLng }
145
167
  val radiusValue = radius.coerceIn(10, 200)
146
168
  val opacityValue = opacity.coerceIn(0.0, 1.0)
169
+ val gradientValue = gradient
170
+ Log.d(TAG, "applyUpdate: token=$token, visible=$visible, points=${pointsSnapshot.size}, radius=$radiusValue, opacity=$opacityValue, gradient=${gradientValue != null}, ${formatPointStats(pointsSnapshot)}")
147
171
 
148
172
  if (!visible) {
149
173
  applyOverlayVisibility()
@@ -151,6 +175,7 @@ class HeatMapView(context: Context, appContext: AppContext) : ExpoView(context,
151
175
  }
152
176
 
153
177
  if (pointsSnapshot.isEmpty()) {
178
+ Log.w(TAG, "applyUpdate: no valid heatmap points, removing overlay")
154
179
  heatmapOverlay?.remove()
155
180
  heatmapOverlay = null
156
181
  return
@@ -164,19 +189,25 @@ class HeatMapView(context: Context, appContext: AppContext) : ExpoView(context,
164
189
 
165
190
  executor.execute {
166
191
  try {
167
- val provider = HeatmapTileProvider.Builder()
168
- .data(pointsSnapshot)
192
+ val builder = HeatmapTileProvider.Builder()
193
+ .data(latLngSnapshot)
169
194
  .radius(radiusValue)
170
- .build()
195
+
196
+ gradientValue?.let { builder.gradient(it) }
197
+
198
+ val provider = builder.build()
171
199
 
172
200
  post {
173
201
  if (token != updateToken) {
202
+ Log.d(TAG, "addOverlay skipped: stale token=$token, current=$updateToken")
174
203
  return@post
175
204
  }
176
205
  if (aMap !== map) {
206
+ Log.d(TAG, "addOverlay skipped: map instance changed")
177
207
  return@post
178
208
  }
179
209
  if (!visible) {
210
+ Log.d(TAG, "addOverlay skipped: hidden")
180
211
  return@post
181
212
  }
182
213
 
@@ -200,9 +231,10 @@ class HeatMapView(context: Context, appContext: AppContext) : ExpoView(context,
200
231
  applyOverlayVisibility()
201
232
  applyOverlayOpacity()
202
233
  forceRefresh()
234
+ Log.i(TAG, "addOverlay success: id=${runCatching { heatmapOverlay?.id }.getOrNull()}, points=${pointsSnapshot.size}, radius=$radiusValue, opacity=$opacityValue")
203
235
  }
204
236
  } catch (t: Throwable) {
205
- Log.e("HeatMapView", "Failed to build heatmap", t)
237
+ Log.e(TAG, "Failed to build heatmap", t)
206
238
  }
207
239
  }
208
240
  }
@@ -217,11 +249,91 @@ class HeatMapView(context: Context, appContext: AppContext) : ExpoView(context,
217
249
  fun removeHeatMap() {
218
250
  updateToken += 1
219
251
  removeCallbacks(applyUpdateRunnable)
252
+ Log.d(TAG, "removeHeatMap")
220
253
  heatmapOverlay?.remove()
221
254
  heatmapOverlay = null
222
255
  dataList.clear()
223
256
  needsRebuild = true
224
257
  }
258
+
259
+ private fun parseWeightedLatLngList(data: Any?): List<WeightedLatLng> {
260
+ if (data == null || data !is List<*>) return emptyList()
261
+
262
+ val result = mutableListOf<WeightedLatLng>()
263
+ for (item in data) {
264
+ val point = parseWeightedLatLng(item)
265
+ if (point != null) {
266
+ result.add(point)
267
+ } else if (item is List<*>) {
268
+ result.addAll(parseWeightedLatLngList(item))
269
+ }
270
+ }
271
+ return result
272
+ }
273
+
274
+ private fun parseWeightedLatLng(data: Any?): WeightedLatLng? {
275
+ val latLng = LatLngParser.parseLatLng(data) ?: return null
276
+ val intensity = when (data) {
277
+ is Map<*, *> -> listOf(data["intensity"], data["weight"], data["count"], data["value"])
278
+ .firstOrNull { it is Number }
279
+ is List<*> -> data.getOrNull(2)
280
+ else -> null
281
+ }
282
+ val weight = ((intensity as? Number)?.toDouble() ?: 1.0).takeIf { it.isFinite() && it > 0.0 } ?: 1.0
283
+ return WeightedLatLng(latLng, weight)
284
+ }
285
+
286
+ private fun parseGradient(gradientValue: Map<String, Any>?): Gradient? {
287
+ if (gradientValue == null) return null
288
+ val rawColors = gradientValue["colors"] as? List<*> ?: return null
289
+ val rawStartPoints = gradientValue["startPoints"] as? List<*> ?: return null
290
+ if (rawColors.size < 2 || rawColors.size != rawStartPoints.size) {
291
+ Log.w(TAG, "parseGradient ignored: colors=${rawColors.size}, startPoints=${rawStartPoints.size}")
292
+ return null
293
+ }
294
+
295
+ val colors = IntArray(rawColors.size)
296
+ val startPoints = FloatArray(rawStartPoints.size)
297
+ for (index in rawColors.indices) {
298
+ val color = parseColor(rawColors[index]) ?: run {
299
+ Log.w(TAG, "parseGradient ignored: invalid color at index=$index, value=${rawColors[index]}")
300
+ return null
301
+ }
302
+ val startPoint = (rawStartPoints[index] as? Number)?.toFloat() ?: run {
303
+ Log.w(TAG, "parseGradient ignored: invalid startPoint at index=$index, value=${rawStartPoints[index]}")
304
+ return null
305
+ }
306
+ colors[index] = color
307
+ startPoints[index] = startPoint.coerceIn(0f, 1f)
308
+ }
309
+ return Gradient(colors, startPoints)
310
+ }
311
+
312
+ private fun parseColor(value: Any?): Int? {
313
+ return when (value) {
314
+ is Number -> value.toInt()
315
+ is String -> runCatching { Color.parseColor(value) }.getOrNull()
316
+ else -> null
317
+ }
318
+ }
319
+
320
+ private fun formatPointStats(points: List<WeightedLatLng>): String {
321
+ if (points.isEmpty()) return "bounds=empty"
322
+ var minLat = Double.POSITIVE_INFINITY
323
+ var maxLat = Double.NEGATIVE_INFINITY
324
+ var minLng = Double.POSITIVE_INFINITY
325
+ var maxLng = Double.NEGATIVE_INFINITY
326
+ var totalIntensity = 0.0
327
+ points.forEach { point ->
328
+ val latLng: LatLng = point.latLng
329
+ minLat = minOf(minLat, latLng.latitude)
330
+ maxLat = maxOf(maxLat, latLng.latitude)
331
+ minLng = minOf(minLng, latLng.longitude)
332
+ maxLng = maxOf(maxLng, latLng.longitude)
333
+ totalIntensity += point.intensity
334
+ }
335
+ return "bounds=[$minLat,$minLng]-[$maxLat,$maxLng], totalIntensity=$totalIntensity"
336
+ }
225
337
 
226
338
  override fun onDetachedFromWindow() {
227
339
  super.onDetachedFromWindow()