15 KiB
NavSea Native Pickup React Native + MapLibre 实现说明
最后更新:2026-04-09
适用项目:/root/sourceserver/pbf
目标前端:React Native + MapLibre Native
1. 文档目的
本文给前端团队一份可以直接实现的 pickup 方案。
目标不是再讨论抽象模型,而是回答下面几件事:
- 在
native MapLibre中如何点击海图并拿到结果 - 当前
NavSea 20nm final这条线应该怎么消费 - 哪些层应该能点,哪些层应屏蔽
- 点击空白水域时如何返回深度上下文
- 规则字典和解释器怎么接入
2. 当前正式口径
当前项目已经收口为以下原则:
- 对象主键统一使用:
feature.id
feature.id视为当前fid的 native 可消费形式- 当前
Karatsu 20nm final不拆 query 数据源 - pickup、渲染、CPA / 安全判断继续消费同一个
feature的同一套属性 - 只要用户看不到的东西,就不应该成为正式 pickup 的点击结果
3. 当前数据基线
当前固定视觉与 PBF 基线:
- compare 页面:
http://192.168.200.184/newpec/navsea-compare-karatsu-20nm.html
- style:
- tile root:
/home/wwwroot/pbf-delivery-karatsu-20nm-final
当前真实瓦片抽样结果已经确认:
feature.id可直接作为主对象 id 使用- 当前
properties是瘦载荷,但已经保留一批 pickup 必要字段
典型保留字段包括:
navigation_marksdisplay_codecanonical_object_typechart_icon_imagechart_label_textchart_label_subtextlight_color_codelight_sector_modename_ja
navigation_hazard_pointclass_codecanonical_object_typechart_icon_image
clearance_limit_pointclass_codeclearance_height_mchart_label_textname_ja
depth_contourclass_codeleast_depth_mchart_label_text
4. Pickup 的结果类型
pickup 不只返回“对象”,而是返回三类结果之一。
4.1 object
点中了明确海图对象,例如:
- 航标
- 危险物
- 引航站
- 净空限制点
- 锚地符号
返回:
- 这个对象是什么
- 它有哪些细节
4.2 info
点中了可见信息,例如:
- 深度数字
- 净空数字
- 灯略记
- 标签
返回:
- 这条信息是什么
- 它属于哪个对象
4.3 context
点的是空白水域,但这个位置本身有海图意义。
返回:
- 当前深度上下文
- 是否浅水
- 最近等深线信息
- 是否位于锚地/航路/危险区
说明:
- 空白水域不应简单返回“未命中对象”
- 应优先返回水深上下文
5. 当前推荐实现结构
前端实现建议直接采用:
- 规则字典:
- JS 解释器:
- TS 解释器:
对于 React Native 团队,推荐两种接法:
方案 A:直接复用解释器逻辑
- 把现有 JS 解释器迁入 RN 工程
- 把规则字典作为静态 JSON 资源加载
适合:
- JS / TS 代码统一
- Web compare 和 RN 共享判读逻辑
方案 B:规则字典保留,解释器改写为 TypeScript
- 保留同一份 JSON 规则字典
- RN 端直接使用仓库里的 TS 解释器,或以它为母版迁入工程
适合:
- RN 工程更强调类型约束
- 前端团队不想直接引用仓库脚本文件
建议优先使用方案 B。
6. MapLibre Native 查询方式
官方 MapLibre React Native 的 MapView 提供:
queryRenderedFeaturesAtPoint(point, [filter], [layerIDs])queryRenderedFeaturesInRect(bbox, [filter], [layerIDs])
其中更推荐用于 pickup 的是:
queryRenderedFeaturesInRect
原因:
- 海图符号小
- 单点容易漏
- 小范围矩形更稳
建议默认点击窗口:
8px
例如:
- 点击点
(x, y) - 查询框:
[x - 8, y - 8, x + 8, y + 8]
7. 交互原则
7.1 只点“看得见”的东西
用户通常不是在做数据检索,而是在解释当前画面。
因此:
- 可见对象可以点
- 可见信息可以点
- 不可见辅助层不应可点
7.2 看到的表达可以是入口,返回结果必须归到主对象
例如:
- 点到航标标签
- 点到净空数字
- 点到灯略记
可以作为 pickup 入口,但最终结果应尽量回到同一个主对象 fid。
7.3 空白水域优先返回海图上下文
当没有主对象命中时,不要立刻返回空。
应继续尝试:
- 深度上下文
- 水域分带
- 最近等深线
8. 哪些层应参与 pickup
当前不建议在 RN 工程里手写 layer 名单。
统一做法是直接读取规则字典中的:
render_layer_groups.primaryrender_layer_groups.arearender_layer_groups.inforender_layer_groups.ignore
这样当 style / pickup 规则更新时,前端无需同步改动硬编码数组。
8.1 主对象层
主对象层从规则字典读取:
pickupRules.render_layer_groups.primary
这组层是正式 pickup 主入口。
8.2 信息层
信息层从规则字典读取:
pickupRules.render_layer_groups.info
这些层允许点击,但应尽量回归到主对象解释。
8.3 可见区域层
可见区域层从规则字典读取:
pickupRules.render_layer_groups.area
这些层本身不是小图标,但它们是用户肉眼明确能看到的主表达。
典型包括:
- 渔业/定置渔具面
- 危险区面
- 锚地区
- 设施区
- 桥区
对这类层,原则是:
- 如果用户精确点在可见区域填充上,应优先返回该区域对象
- 不应让附近的小点符号把它抢走
8.4 默认屏蔽层
默认屏蔽层从规则字典读取:
pickupRules.render_layer_groups.ignore
说明:
- 这些层多为辅助表达
- 用户通常不是想点它们本身
- 其中包括 flare、arc、outline、support 与 background/container 类层
9. 推荐查询流程
推荐前端固定按 4 步做。
第一步:先做精确点查
使用:
queryRenderedFeaturesAtPoint(point, undefined, exactLayers)
其中:
exactLayers = [...renderGroups.area, ...renderGroups.primary, ...renderGroups.info]
目的:
- 优先解释用户当前像素下真正看见的表达
- 特别是:
- 渔网斜线面
- 危险区面
- 锚地区
- 设施区
如果精确点查命中:
- 先保留渲染顺序最靠上的候选
- 再按
feature.id聚合并解释
第二步:精确点查未命中,再查主对象层小范围补捞
使用:
queryRenderedFeaturesInRect(bbox, undefined, pickupRules.render_layer_groups.primary)
如果命中:
- 按
feature.id聚合 - 用规则字典解释
- 返回主对象结果
说明:
- 这一步主要用于补捞:
- 很小的灯标
- 浮标
- 危险物点
- 净空点
第三步:主对象层未命中,再查信息层
使用:
queryRenderedFeaturesInRect(bbox, undefined, pickupRules.render_layer_groups.info)
如果命中:
- 按
feature.id聚合 - 若能回到同一主对象,则返回主对象结果
- 同时标记:
result_type = info
第四步:仍未命中,则返回上下文
执行:
- 位置深度上下文解释
- 水域/浅水/危险区上下文解释
返回:
result_type = context
10. 推荐返回结构
10.1 object
{
"result_type": "object",
"fid": "4101640307",
"source_layer_std": "navigation_marks",
"geometry_type": "Point",
"title": "若宮灯台",
"object_domain": "navigation_mark",
"object_class": "lighthouse",
"object_subclass": "coastal_lighthouse_over_15m",
"canonical_object_type": "coastal_lighthouse_over_15m",
"details": [
{ "field": "display_code", "label": "表示用编号", "value": "30000004" },
{ "field": "light_color_code", "label": "灯色", "value": "white" },
{ "field": "light_sector_mode", "label": "灯弧模式", "value": "sector" }
],
"render": {
"source_layer_std": "navigation_marks",
"render_hit_layers": ["nav-marks-harbor-lighthouses", "nav-mark-labels"],
"chart_icon_image": "symbol-daytime-301",
"chart_symbol_code": "lighthouse"
}
}
10.2 info
{
"result_type": "info",
"fid": "2689086947",
"source_layer_std": "depth_contour",
"title": "2",
"object_domain": "depth",
"object_class": "depth_contour",
"object_subclass": "depth_contour_line",
"details": [
{ "field": "least_depth_m", "label": "最浅/深度(m)", "value": 2.0 }
]
}
10.3 context
{
"result_type": "context",
"context": {
"depth_band": "2-5m",
"nearest_contour_m": 5,
"shallow_water": true,
"in_hazard_area": false,
"in_route_area": false
}
}
11. 前端解释器接法
11.1 输入
输入给解释器的是:
queryRenderedFeatures...返回的候选 feature 数组- 规则字典 JSON
11.2 当前解释器职责
解释器只做以下事:
- 从 feature 中读取:
feature.idfeature.sourceLayerfeature.layer.idfeature.properties
- 按
feature.id聚合 - 使用规则字典匹配:
source_layer_std
- 结合规则字典里的:
interaction_rolepickup_priority
- 若前端传入点击经纬度:
click_lnglat解释器会在同分候选之间优先选择距离点击点最近的对象
- 当同一点击同时命中区域对象与附近点对象时:
- 优先尊重精确点查命中的可见区域表达
- 输出统一 pickup 结构
11.3 当前解释器不负责
解释器不负责:
- 再造对象 identity
- 替代 style 做渲染决策
- 做最终放行审计
12. React Native 伪代码
import pickupRules from "./navsea-pickup-rules.v1.json";
import {
getRenderLayerGroups,
interpretRenderedFeatures,
} from "./navsea-pickup-interpreter";
type PickupResult =
| { result_type: "object"; primary: any; candidates: any[] }
| { result_type: "info"; primary: any; candidates: any[] }
| { result_type: "context"; context: any };
async function runPickup(
mapRef: any,
screenPoint: [number, number],
clickLngLat: { lng: number; lat: number },
pickupRules: any
): Promise<PickupResult> {
const radius = 8;
const bbox: [number, number, number, number] = [
screenPoint[0] - radius,
screenPoint[1] - radius,
screenPoint[0] + radius,
screenPoint[1] + radius,
];
const renderGroups = getRenderLayerGroups(pickupRules);
const exactLayers = [
...renderGroups.area,
...renderGroups.primary,
...renderGroups.info,
];
const exactFeatures =
(await mapRef.current?.queryRenderedFeaturesAtPoint(
screenPoint,
undefined,
exactLayers
)) || [];
if (exactFeatures.length > 0) {
const interpreted = interpretRenderedFeatures(exactFeatures, pickupRules, {
click_lnglat: clickLngLat,
});
const exactPrimary = interpreted.primary;
if (exactPrimary) {
return {
result_type: exactPrimary.interaction_role === "info" ? "info" : "object",
primary: exactPrimary,
candidates: interpreted.candidates,
};
}
}
const primaryFeatures =
(await mapRef.current?.queryRenderedFeaturesInRect(
bbox,
undefined,
renderGroups.primary
)) || [];
if (primaryFeatures.length > 0) {
const interpreted = interpretRenderedFeatures(primaryFeatures, pickupRules, {
click_lnglat: clickLngLat,
});
return {
result_type: "object",
primary: interpreted.primary,
candidates: interpreted.candidates,
};
}
const infoFeatures =
(await mapRef.current?.queryRenderedFeaturesInRect(
bbox,
undefined,
renderGroups.info
)) || [];
if (infoFeatures.length > 0) {
const interpreted = interpretRenderedFeatures(infoFeatures, pickupRules, {
click_lnglat: clickLngLat,
});
return {
result_type: "info",
primary: interpreted.primary,
candidates: interpreted.candidates,
};
}
return {
result_type: "context",
context: await buildDepthContext(mapRef, screenPoint),
};
}
13. 深度上下文实现建议
补充说明:
- 对点状对象,如果不把
clickLngLat传给解释器 - 当同一小框里命中多个同类型、同分对象时
- 解释器只能按原始返回顺序选一个
- 这会导致:
- popup 标题是对的类型
- 但返回的对象坐标可能偏到附近另一根灯标/浮标
13.1 优先级
当对象/信息层都未命中时,按下面顺序尝试:
- 查深度数字或等深相关 label
- 查深度分带 / 水域层
- 查最近等深线
- 最后返回“无可解释上下文”
13.2 当前建议先做的最小版
最小版只需要先返回:
- 最近
depth_contour - 当前是否落在浅水相关面层
- 当前是否落在危险区 / 航路 / 锚地区
后续再升级成更细的:
depth_bandnearest_contour_mshallow_water
14. 规则字典的更新规则
规则字典不是每次纯视觉微调都要更新。
必须同步更新的情况:
- PBF 标准层变化
- 某层关键分类字段变化
- style 主渲染字段依赖变化
- 新增正式 pickup 层
- render layer 分组变化
也就是说:
- 纯颜色、字号、间距微调,通常不需要改
- 但只要改了“这一层怎么解释、怎么归类、怎么点”,就必须改
- 前端若直接读取
render_layer_groups,通常不需要再手改 layer 白名单 - 前端若已实现:
- 精确点查
area + primary + info - 小范围补查
primary + info则能明显减少“看起来像渔网,却返回附近灯标”的误判
- 精确点查
15. 当前不建议做的事
当前前端不建议:
- 直接查询所有 style 层
- 让 flare / arc / outline 抢 primary
- 点空白水域直接返回 null
- 再造一套独立 query API 替代当前统一对象载荷
- 在前端代码里硬写大量分支而不保留规则字典
16. 当前建议的落地顺序
建议前端团队按下面顺序实现:
- 接入规则字典 JSON
- 接入仓库内的 TS 解释器,或以它为母版迁入 RN 工程
- 先实现:
- 精确点查 pickup
- 主对象层补查 pickup
- 信息层 pickup
- 再实现:
- 空白水域深度上下文
- 最后把 compare 页和 RN 端解释结果做对齐抽查
17. 与后续 PBF 演进的关系
当前方案不要求增加 PBF 容量。
后续如果 builder 增补:
object_domainobject_classobject_subclass
前端仍可继续使用同一套结构,只需把规则字典从“主判读层”逐步降为“兼容补充层”。
18. 结论
当前最稳的 native pickup 方案是:
- 继续使用当前
Karatsu 20nm finalPBF/style - 使用可见层作为点击入口
- 使用
feature.id作为对象主键 - 使用规则字典和解释器做本地判读
- 对空白水域返回深度上下文
这样既不增加 PBF 容量,也能满足:
- 用户点图标看细节
- 用户点水域看水深上下文
- 后续继续做 CPA / 安全判断
19. 参考
- MapLibre React Native
MapView官方文档:queryRenderedFeaturesAtPointqueryRenderedFeaturesInRect- https://maplibre.org/maplibre-react-native/docs/components/general/map-view/