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.
- package/README.md +7 -2
- package/android/build.gradle +8 -4
- package/android/src/main/AndroidManifest.xml +8 -0
- package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapOfflineModule.kt +83 -15
- package/android/src/main/java/expo/modules/gaodemap/map/ExpoGaodeMapView.kt +13 -3
- package/android/src/main/java/expo/modules/gaodemap/map/managers/UIManager.kt +36 -39
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/ClusterView.kt +5 -2
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/HeatMapView.kt +122 -10
- package/android/src/main/java/expo/modules/gaodemap/map/overlays/HeatMapViewModule.kt +2 -2
- package/android/src/main/java/expo/modules/gaodemap/map/search/ExpoGaodeMapSearchModule.kt +751 -0
- package/build/map/ExpoGaodeMapOfflineModule.d.ts +5 -0
- package/build/map/ExpoGaodeMapOfflineModule.d.ts.map +1 -1
- package/build/map/ExpoGaodeMapOfflineModule.js.map +1 -1
- package/build/map/components/overlays/HeatMap.d.ts.map +1 -1
- package/build/map/components/overlays/HeatMap.js +21 -2
- package/build/map/components/overlays/HeatMap.js.map +1 -1
- package/build/map/index.d.ts +3 -0
- package/build/map/index.d.ts.map +1 -1
- package/build/map/index.js +3 -0
- package/build/map/index.js.map +1 -1
- package/build/map/search/ExpoGaodeMapSearch.types.d.ts +340 -0
- package/build/map/search/ExpoGaodeMapSearch.types.d.ts.map +1 -0
- package/build/map/search/ExpoGaodeMapSearch.types.js +19 -0
- package/build/map/search/ExpoGaodeMapSearch.types.js.map +1 -0
- package/build/map/search/ExpoGaodeMapSearchModule.d.ts +74 -0
- package/build/map/search/ExpoGaodeMapSearchModule.d.ts.map +1 -0
- package/build/map/search/ExpoGaodeMapSearchModule.js +47 -0
- package/build/map/search/ExpoGaodeMapSearchModule.js.map +1 -0
- package/build/map/search/index.d.ts +156 -0
- package/build/map/search/index.d.ts.map +1 -0
- package/build/map/search/index.js +171 -0
- package/build/map/search/index.js.map +1 -0
- package/build/map/types/map-view.types.d.ts +4 -2
- 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/utils/ErrorHandler.js +11 -11
- package/build/map/utils/ErrorHandler.js.map +1 -1
- package/build/map/utils/OfflineMapManager.d.ts +4 -0
- package/build/map/utils/OfflineMapManager.d.ts.map +1 -1
- package/build/map/utils/OfflineMapManager.js +6 -0
- package/build/map/utils/OfflineMapManager.js.map +1 -1
- package/expo-module.config.json +4 -2
- package/ios/ExpoGaodeMapNaviView.swift +16 -17
- package/ios/ExpoGaodeMapNavigation.podspec +2 -1
- package/ios/map/ExpoGaodeMapOfflineModule.swift +61 -0
- package/ios/map/ExpoGaodeMapSearchModule.swift +773 -0
- package/ios/map/modules/LocationManager.swift +9 -3
- package/ios/map/overlays/PolylineView.swift +6 -12
- package/package.json +1 -1
- package/plugin/build/withGaodeMap.js +12 -0
- 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
|
-
|
|
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
|
-
- [
|
|
651
|
+
- [导航示例工程](../../example-navigation)
|
|
647
652
|
- [高德地图开放平台](https://lbs.amap.com/)
|
|
648
653
|
- [Expo Modules API](https://docs.expo.dev/modules/overview/)
|
|
649
654
|
|
package/android/build.gradle
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
apply plugin: 'com.android.library'
|
|
2
2
|
|
|
3
3
|
group = 'expo.modules.gaodemap.navigation'
|
|
4
|
-
version = '2.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.
|
|
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:
|
|
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
|
-
|
|
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
|
|
100
|
-
cities
|
|
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
|
|
110
|
-
|
|
120
|
+
val manager = getOfflineMapManager()
|
|
121
|
+
val province = manager?.offlineMapProvinceList?.find {
|
|
122
|
+
it.provinceCode == provinceCode
|
|
111
123
|
}
|
|
112
|
-
|
|
124
|
+
val downloadedCityKeys = getDownloadedCityKeys(manager)
|
|
125
|
+
province?.cityList?.map { city -> convertCityToMap(city, downloadedCityKeys) } ?: emptyList()
|
|
113
126
|
}
|
|
114
127
|
|
|
115
128
|
AsyncFunction("getDownloadedMaps") {
|
|
116
|
-
val
|
|
117
|
-
cities
|
|
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
|
|
212
|
-
city
|
|
213
|
-
city?.state == OfflineMapStatus.
|
|
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
|
|
218
|
-
city
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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()
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
145
|
-
minClusterSize
|
|
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.
|
|
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<
|
|
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(
|
|
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
|
-
|
|
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
|
|
168
|
-
.data(
|
|
192
|
+
val builder = HeatmapTileProvider.Builder()
|
|
193
|
+
.data(latLngSnapshot)
|
|
169
194
|
.radius(radiusValue)
|
|
170
|
-
|
|
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(
|
|
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()
|