地图显示兴趣点(POI)
本指南:通过 DynamicPOISearchController 注入搜索引擎,随相机移动在地图上展示并刷新 POI。
功能介绍
DynamicPOISearchController 提供 动态 POI 搜索 能力:业务方实现 DynamicSearchEngine 提供数据,SDK 监听相机移动触发搜索,支持 ID 追踪、增量更新 与可配置的 定时刷新(如 EV 充电桩实时状态)。
效果示意:

| 能力 |
说明 |
| 相机联动 |
相机移动后按可视区域自动搜索 |
| 增量更新 |
通过稳定的 POIAnnotationData.id 增删改标注 |
| 刷新策略 |
fixedInterval 定时刷新或 DISABLED 仅随相机搜索 |
| 自定义调度 |
可注入 CoroutineDispatcher 控制搜索线程 |
获取入口(单块 MapView):
| val dynamicCtrl = mapView.getDynamicPOISearchController() ?: return
|
核心接口一览
| 方法 |
说明 |
injectSearchEngine(searchEngine, dispatcher?) |
注入 DynamicSearchEngine |
setRefreshStrategy(strategy?) |
设置 POI 刷新策略 |
displayPOI(displayContent) |
按类别/关键词展示 POI |
clear() |
清除当前屏 POI 标注;并清空 SDK 共享搜索缓存(多屏下任意一屏调用即可) |
接口详细说明
1. injectSearchEngine — 注入动态搜索引擎
| fun injectSearchEngine(
searchEngine: DynamicSearchEngine,
dispatcher: CoroutineDispatcher? = null
)
|
| 参数 |
类型 |
说明 |
searchEngine |
DynamicSearchEngine |
业务实现的搜索接口;多次注入仅最后一次生效 |
dispatcher |
CoroutineDispatcher? |
搜索任务协程调度器;默认 Dispatchers.IO |
DynamicSearchEngine(业务方实现):
| fun search(
searchUnitParams: SearchUnitParams,
originalResult: List<POIAnnotationData>? = null
): List<POIAnnotationData>?
|
| 参数 |
类型 |
说明 |
searchUnitParams |
SearchUnitParams |
SDK 根据相机位置生成的搜索单元(可视区域、类别、语言等) |
originalResult |
List<POIAnnotationData>? |
已有结果;null 表示首次搜索,非 null 表示刷新 |
| 返回值 |
List<POIAnnotationData>? |
更新后的 POI 列表;null 视为空列表 |
SearchUnitParams 主要字段:
| 字段 |
类型 |
说明 |
poiLayer |
PoiLayer |
POI 图层,与缩放级别相关 |
searchBox |
List<GeoPoint> |
当前可视区域多边形 |
displayContent |
List<String> |
POI 类别或关键词(与 displayPOI 传入一致) |
languageString |
String |
结果语言 |
maxResultCount |
Int |
最大返回数量 |
lat / lon |
Double |
搜索中心经纬度 |
抛出 TimeoutException 时 SDK 会重试;抛出其他 Exception 不会 以相同参数重试。
POIAnnotationData(搜索结果项):
| 字段 |
类型 |
说明 |
id |
String |
必须稳定且唯一,用于增量更新与移除 |
styleKey |
String |
TSS 样式 key(isUserGraphic = false 时) |
location |
Location |
POI 坐标 |
text |
String |
标注文字 |
isUserGraphic / userGraphic |
— |
自定义图标时使用 |
addStringValue / addFloatValue |
— |
刷新时更新标注属性,无需重建整条 POI |
示例代码:
1
2
3
4
5
6
7
8
9
10
11
12 | dynamicCtrl.injectSearchEngine(object : DynamicSearchEngine {
override fun search(
searchUnitParams: SearchUnitParams,
originalResult: List<POIAnnotationData>?
): List<POIAnnotationData>? {
return if (originalResult == null) {
mySearchService.searchInBox(searchUnitParams)
} else {
mySearchService.refresh(searchUnitParams, originalResult)
}
}
})
|
2. setRefreshStrategy — 设置刷新策略
| fun setRefreshStrategy(strategy: PoiRefreshStrategy? = null)
|
| 参数 |
类型 |
说明 |
strategy |
PoiRefreshStrategy? |
刷新策略;null 等同 DISABLED |
| 策略 |
说明 |
PoiRefreshStrategies.fixedInterval(ms) |
固定间隔刷新已有 POI;最小 30 秒(30 * 1000L) |
PoiRefreshStrategies.DISABLED |
不自动刷新,仅在相机移动时触发新搜索 |
示例代码:
| // EV 充电桩等需周期性更新状态
dynamicCtrl.setRefreshStrategy(PoiRefreshStrategies.fixedInterval(30 * 1000L))
// 仅随地图拖动搜索,不后台刷新
dynamicCtrl.setRefreshStrategy(PoiRefreshStrategies.DISABLED)
|
3. displayPOI — 展示 POI
| fun displayPOI(displayContent: List<String>)
|
| 参数 |
类型 |
说明 |
displayContent |
List<String> |
POI 类别或关键词列表;须先 injectSearchEngine |
调用后 SDK 监听相机移动,在可视区域内自动触发 DynamicSearchEngine.search 并渲染标注。
示例代码:
| dynamicCtrl.displayPOI(listOf("ev_charging", "restaurant"))
|
4. clear — 清除 POI
清除当前地图上由动态 POI 搜索展示的标注,并清空 SDK 内部的 共享搜索缓存。
主要使用场景:切换 POI 类别——在调用 displayPOI(新类别) 之前先 clear(),避免旧类别的缓存结果干扰新类别展示。退出 POI 浏览页、页面销毁时也可调用。
示例代码:
| // 从「餐厅」切换到「充电桩」:先清缓存与当前标注,再展示新类别
dynamicCtrl.clear()
dynamicCtrl.displayPOI(listOf("ev_charging"))
|
单屏集成示例
以下示例面向最常见的 单块 MapView 场景,展示完整接入流程:MapView 就绪后注入搜索引擎 → 配置刷新策略 → 按类别展示 POI → 处理点击与生命周期清理。数据层以 Entity SDK 为例,亦可替换为自有 HTTP 服务。
多屏(如主屏 + 仪表盘)同时展示 POI 的接入方式见下文 多屏同时显示 POI。
1. 初始化与注入
在 MapView.onReady 之后获取 DynamicPOISearchController,注入 DynamicSearchEngine 并设置刷新策略(须在 displayPOI 之前完成)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 | class PoiSearchHostFragment : Fragment() {
private lateinit var mapView: MapView
private var dynamicPoiCtrl: DynamicPOISearchController? = null
private val searchEngine = EvPoiSearchEngine() // 见下文实现
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
mapView.setOnReadyListener {
dynamicPoiCtrl = mapView.getDynamicPOISearchController() ?: return@setOnReadyListener
// 可选:自定义协程调度器,避免与主线程或其他 IO 任务争抢
val dispatcher = Executors.newFixedThreadPool(4).asCoroutineDispatcher()
dynamicPoiCtrl?.injectSearchEngine(searchEngine, dispatcher)
// EV 等需周期性更新状态的 POI:最小间隔 30 秒
dynamicPoiCtrl?.setRefreshStrategy(
PoiRefreshStrategies.fixedInterval(30 * 1000L)
)
registerPoiTouchListener()
}
}
}
|
2. 实现 DynamicSearchEngine
originalResult == null 时做首次搜索;非 null 时做定时刷新(在 fixedInterval 策略下由 SDK 回调)。POIAnnotationData.id 须使用业务侧稳定 ID(如 Entity entity.id)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61 | class EvPoiSearchEngine : DynamicSearchEngine {
override fun search(
searchUnitParams: SearchUnitParams,
originalResult: List<POIAnnotationData>?
): List<POIAnnotationData>? {
return if (originalResult == null) {
searchVisibleArea(searchUnitParams)
} else {
refreshExistingPois(searchUnitParams, originalResult)
}
}
private fun searchVisibleArea(params: SearchUnitParams): List<POIAnnotationData> {
val polygon = Polygon.builder()
.setPoints(params.searchBox)
.build()
val filters = SearchFilters.builder()
.setCategoryFilter(CategoryFilter.builder().setCategories(params.displayContent).build())
.setGeoFilter(PolygonGeoFilter.builder(polygon).build())
.build()
val response = EntityService.getClient().searchRequest()
.setFilters(filters)
.setLimit(params.maxResultCount)
.setLocation(params.lat, params.lon)
.execute()
return response.results?.mapNotNull { entity -> entity.toPoiAnnotationData() } ?: emptyList()
}
/** 刷新:在 originalResult 上更新图标/状态,保持 id 不变 */
private fun refreshExistingPois(
params: SearchUnitParams,
original: List<POIAnnotationData>
): List<POIAnnotationData> {
return original.map { poi ->
poi.copy().apply {
// 示例:根据后端最新状态更新 TSS 动态属性
val available = fetchChargerStatus(poi.id)
addFloatValue("available", if (available) 1f else 0f)
}
}
}
private fun Entity.toPoiAnnotationData(): POIAnnotationData? {
if (type != EntityType.PLACE) return null
val loc = Location("poi").apply {
latitude = place.address.geoCoordinates.latitude
longitude = place.address.geoCoordinates.longitude
}
return POIAnnotationData(
id = id, // 稳定 ID,勿用随机值
isUserGraphic = false,
userGraphic = null,
styleKey = "poi_annotations.ev_charging", // 须与当前 TSS 一致
location = loc,
text = place.name
)
}
}
|
3. 开启、关闭与切换 POI 类别
用户进入 POI 浏览、切换搜索类别,或关闭 POI 图层时调用 displayPOI。切换类别时建议先 clear() 再 displayPOI,确保旧类别缓存被清空。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | // 展示 EV 充电桩 + 餐厅
fun showEvAndRestaurantPoi() {
dynamicPoiCtrl?.displayPOI(listOf("ev_charging", "restaurant"))
}
// 切换到仅展示充电桩
fun switchToEvChargingOnly() {
dynamicPoiCtrl?.clear()
dynamicPoiCtrl?.displayPOI(listOf("ev_charging"))
}
// UI 开关:关闭 POI 图层
fun hidePoiByCategory() {
dynamicPoiCtrl?.displayPOI(emptyList())
}
|
4. 注册 POI 点击
POI 标注的触摸事件通过 MapView.setOnPOITouchListener 接收,与动态搜索控制器独立注册。
| private fun registerPoiTouchListener() {
mapView.setOnPOITouchListener { touchType, position, poiDescription ->
if (touchType == TouchType.Click) {
// poiDescription 为被点击 POI 的描述信息,可与 POIAnnotationData.extrasInfo 对应
poiDescription?.let { openPoiDetail(it) }
}
}
}
|
5. 生命周期清理
页面销毁或彻底退出 POI 功能时清除标注并释放缓存。
| override fun onDestroyView() {
dynamicPoiCtrl?.clear()
mapView.setOnPOITouchListener(null)
super.onDestroyView()
}
|
调用顺序小结
| MapView.onReady
→ injectSearchEngine(engine)
→ setRefreshStrategy(...) // 可选
→ displayPOI(categories) // 开始展示
↳ 相机移动 → engine.search(params, null)
↳ 定时刷新 → engine.search(params, originalResult)
→ clear() + displayPOI(新类别) // 切换类别
→ clear() // 退出 POI 功能 / 页面销毁
|
多屏同时显示 POI
除单屏外,HMI 也可在 多块地图(如中控 TnMapView + 仪表盘 TnClusterMapView,或两个 TnMapView)上同时展示相同类别的 POI。
多屏地图实例、Surface 生命周期与数据同步总览见 多屏显示。本节说明 POI 在多屏下的共享与接入逻辑。
设计原则
| 组件 |
是否共享 |
说明 |
DynamicSearchEngine |
共享(推荐单例) |
多屏注入同一实例,避免重复发起相同搜索 |
| 搜索缓存与进行中请求 |
共享(SDK 内部) |
全局 LRU 缓存(默认 500 个 search unit);相同 search unit 的并发请求会合并 |
DynamicPOISearchController |
不共享 |
每块地图各有一个,分别 injectSearchEngine / displayPOI |
| POI Annotation |
不共享 |
各屏在自己的 AnnotationsController 上独立 add / update / remove |
| 相机 / 可视区域 |
不共享 |
各屏按自身视野触发搜索;两屏 POI 数量可能不同 |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 | HMI App
|
+-- MapView(主屏)
| +-- DynamicPOISearchController
| +-- Annotations(本屏渲染)
|
+-- ClusterMapView(仪表盘)
| +-- DynamicPOISearchController
| +-- Annotations(本屏渲染)
|
+-- DynamicSearchEngine(单例 / 长生命周期) ← 多屏共享
|
v
SDK 内部 DynamicPOISearchRepository
- 共享 LRU cache
- 相同 search unit 的 in-flight 请求合并(JOINED_IN_FLIGHT)
|
接入步骤
1. 创建共享 DynamicSearchEngine(App 层单例)
| private var sharedDynamicSearchEngine: DynamicSearchEngine? = null
private fun getOrCreateSharedSearchEngine(context: Context): DynamicSearchEngine {
return sharedDynamicSearchEngine ?: MyDynamicSearchEngine(
context.applicationContext
).also { sharedDynamicSearchEngine = it }
}
|
2. 各屏 ready 后分别获取 Controller,注入同一 Engine
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 | private var mainDynamicPoiCtrl: DynamicPOISearchController? = null
private var clusterDynamicPoiCtrl: DynamicPOISearchController? = null
private val poiCategories = listOf("ev_charging", "restaurant")
private val dynamicPoiDispatcher =
Executors.newFixedThreadPool(6).asCoroutineDispatcher()
private fun configureDynamicPoiSearch(controller: DynamicPOISearchController?) {
controller ?: return
val engine = getOrCreateSharedSearchEngine(requireContext())
controller.injectSearchEngine(engine, dynamicPoiDispatcher)
controller.setRefreshStrategy(PoiRefreshStrategies.fixedInterval(30 * 1000L))
}
// 主屏 MapView onReady
mainDynamicPoiCtrl = mapView.getDynamicPOISearchController()
configureDynamicPoiSearch(mainDynamicPoiCtrl)
// Cluster 屏 onReady
clusterDynamicPoiCtrl = clusterMapView.getDynamicPOISearchController()
configureDynamicPoiSearch(clusterDynamicPoiCtrl)
|
3. 各屏分别调用 displayPOI(相同类别列表)
| fun updateShowPoiOnAllMaps(enabled: Boolean) {
val categories = if (enabled) poiCategories else emptyList()
mainDynamicPoiCtrl?.displayPOI(categories)
clusterDynamicPoiCtrl?.displayPOI(categories)
}
|
注意:仅 injectSearchEngine 不会自动显示 POI;每一块地图都必须各自调用 displayPOI()。共享的是搜索数据,不是某一屏的显示状态。
4. 车辆位置写入共享 Engine(一次更新,多屏受益)
若 DynamicSearchEngine 实现依赖自车位置(如沿路线或距离排序),在定位回调中更新 Engine 即可,无需分别写入各 Controller:
| fun onVehicleLocationUpdated(location: Location) {
(sharedDynamicSearchEngine as? MyDynamicSearchEngine)?.updateVehicleLocation(location)
}
|
多屏样式建议
多屏场景推荐 POIAnnotationData 使用 styleKey(isUserGraphic = false),由各屏自己的 TSS 控制图标样式:
| POIAnnotationData(
id = entity.id, // 须全局唯一且稳定
isUserGraphic = false,
styleKey = "poi_annotations.ev_charging",
location = loc,
text = entity.name
)
|
主屏与 Cluster 屏可能加载不同样式文件(如 styles/default/ 与 styles/cluster/),但 styleKey 须在各自 TSS 中存在,否则该屏 POI 不可见。详见 地图配色方案。
共享缓存与 clear()
多屏共用 DynamicSearchEngine 时,SDK 内部维护全局共享搜索缓存。clear() 行为须特别注意:
| 行为 |
说明 |
| 清空共享缓存 |
任意一块地图的 Controller 调用一次 clear() 即可清空全局缓存,无需各屏都调 |
| 清除 POI 标注 |
仅清除调用方所在屏已展示的 POI 标注;其他屏上的标注不会因此自动移除 |
| 典型用途 |
切换 POI 类别:先由任意一屏 clear() 清缓存,再在各屏 displayPOI(新类别) |
| // 多屏切换 POI 类别:任意一屏 clear() 清共享缓存即可
fun switchPoiCategoryOnAllMaps(newCategories: List<String>) {
mainDynamicPoiCtrl?.clear() // 清全局缓存;也可由 cluster 屏调用,效果相同
mainDynamicPoiCtrl?.displayPOI(newCategories)
clusterDynamicPoiCtrl?.displayPOI(newCategories)
}
|
注意:若切换类别后某屏仍短暂显示旧 POI 标注,可在该屏补调一次 clear() 后再 displayPOI,或依赖 displayPOI 触发的搜索刷新覆盖。
其他缓存相关行为:
| 场景 |
行为 |
| 屏 A 先搜到某 search unit |
结果写入全局共享缓存 |
| 屏 B 视野命中同一 search unit |
直接读缓存,不再重复调用 DynamicSearchEngine.search |
| 两屏同时请求同一 search unit |
进行中的请求合并 |
最后一个 Controller dispose() |
活跃 Controller 计数归零时,SDK 清空共享缓存 |
功能模块整体退出时,各屏按需 clear() 移除标注,并释放 sharedDynamicSearchEngine 引用:
| mainDynamicPoiCtrl?.clear()
clusterDynamicPoiCtrl?.clear()
mainDynamicPoiCtrl = null
clusterDynamicPoiCtrl = null
sharedDynamicSearchEngine = null
|
多屏集成检查清单
| 检查项 |
说明 |
多屏共用同一个 DynamicSearchEngine |
核心要求,避免重复搜索 |
每屏各自 getDynamicPOISearchController() |
Controller 不可跨屏复用 |
每屏各自 displayPOI(sameCategories) |
inject 不等于显示 |
Cluster 屏已 initialize 且 onReady |
与地图其他能力相同 |
各屏 TSS 含对应 poi_annotations.* |
样式按屏独立配置 |
POIAnnotationData.id 全局唯一 |
跨屏增量更新依赖稳定 ID |
切换类别时任意一屏 clear() 即可 |
清空共享缓存,无需每屏都调 |
常见问题(多屏)
Q1:主屏有 POI,Cluster 屏没有?
- Cluster 屏是否调用了
displayPOI()(仅 inject 不够)
- Cluster Controller 是否注入了同一个
DynamicSearchEngine 实例
- Cluster 屏当前 zoom 是否满足搜索触发条件(过低 zoom 时不搜索)
- Cluster 屏 TSS 是否包含对应
styleKey
Q2:两屏 POI 数量不一致?
属正常现象。各屏相机视野不同,命中的 search unit 不同;共享缓存只避免对同一 search unit 重复搜索,不保证两屏 POI 列表完全相同。
Q3:是否需要每屏各实现一个 Search Engine?
不需要。一个 DynamicSearchEngine 实例即可,由 HMI 在 App 层维护单例或长生命周期对象。
Q4:多屏切换 POI 类别时,是否每屏都要 clear()?
不需要。clear() 会清空全局共享搜索缓存,任意一屏调用一次即可;主要目的是切换类别前丢弃旧缓存。之后在各屏分别 displayPOI(新类别)。clear() 只移除调用方所在屏的标注,若需立刻去掉其他屏旧标注,可在对应屏补调 clear()。
注意事项
- 须先
injectSearchEngine,再调用 displayPOI,否则不生效。
POIAnnotationData.id 必须在多次搜索/刷新间保持稳定,否则无法正确增量更新。
- 搜索数据由 Entity SDK 或自有后端提供,地图模块负责 展示与相机/刷新联动。
- POI 触摸使用
POITouchListener,见 触摸与手势。
- 使用
styleKey 时须与当前 TSS 中 POI 样式一致。
- 多屏场景另有接入要求,见 多屏同时显示 POI。
相关指南