Files
pbf/NavSea_Native_Pickup_ReactNative_MapLibre_实现说明.md

659 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# NavSea Native Pickup React Native + MapLibre 实现说明
最后更新:`2026-04-09`
适用项目:`/root/sourceserver/pbf`
目标前端:`React Native + MapLibre Native`
## 1. 文档目的
本文给前端团队一份可以直接实现的 `pickup` 方案。
目标不是再讨论抽象模型,而是回答下面几件事:
1. 在 `native MapLibre` 中如何点击海图并拿到结果
2. 当前 `NavSea 20nm final` 这条线应该怎么消费
3. 哪些层应该能点,哪些层应屏蔽
4. 点击空白水域时如何返回深度上下文
5. 规则字典和解释器怎么接入
## 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:
- [style.navsea-delivery-karatsu-20nm-final.json](/root/sourceserver/pbf/src/pbf/style.navsea-delivery-karatsu-20nm-final.json)
- tile root:
- `/home/wwwroot/pbf-delivery-karatsu-20nm-final`
当前真实瓦片抽样结果已经确认:
- `feature.id` 可直接作为主对象 id 使用
- 当前 `properties` 是瘦载荷,但已经保留一批 pickup 必要字段
典型保留字段包括:
- `navigation_marks`
- `display_code`
- `canonical_object_type`
- `chart_icon_image`
- `chart_label_text`
- `chart_label_subtext`
- `light_color_code`
- `light_sector_mode`
- `name_ja`
- `navigation_hazard_point`
- `class_code`
- `canonical_object_type`
- `chart_icon_image`
- `clearance_limit_point`
- `class_code`
- `clearance_height_m`
- `chart_label_text`
- `name_ja`
- `depth_contour`
- `class_code`
- `least_depth_m`
- `chart_label_text`
## 4. Pickup 的结果类型
pickup 不只返回“对象”,而是返回三类结果之一。
### 4.1 `object`
点中了明确海图对象,例如:
- 航标
- 危险物
- 引航站
- 净空限制点
- 锚地符号
返回:
- 这个对象是什么
- 它有哪些细节
### 4.2 `info`
点中了可见信息,例如:
- 深度数字
- 净空数字
- 灯略记
- 标签
返回:
- 这条信息是什么
- 它属于哪个对象
### 4.3 `context`
点的是空白水域,但这个位置本身有海图意义。
返回:
- 当前深度上下文
- 是否浅水
- 最近等深线信息
- 是否位于锚地/航路/危险区
说明:
- 空白水域不应简单返回“未命中对象”
- 应优先返回水深上下文
## 5. 当前推荐实现结构
前端实现建议直接采用:
- 规则字典:
- [navsea-pickup-rules.v1.json](/root/sourceserver/pbf/src/pbf/navsea-pickup-rules.v1.json)
- JS 解释器:
- [navsea-pickup-interpreter.js](/root/sourceserver/pbf/src/pbf/navsea-pickup-interpreter.js)
- TS 解释器:
- [navsea-pickup-interpreter.ts](/root/sourceserver/pbf/src/pbf/navsea-pickup-interpreter.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.primary`
- `render_layer_groups.area`
- `render_layer_groups.info`
- `render_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`
```json
{
"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`
```json
{
"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`
```json
{
"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 当前解释器职责
解释器只做以下事:
1. 从 feature 中读取:
- `feature.id`
- `feature.sourceLayer`
- `feature.layer.id`
- `feature.properties`
2. 按 `feature.id` 聚合
3. 使用规则字典匹配:
- `source_layer_std`
4. 结合规则字典里的:
- `interaction_role`
- `pickup_priority`
5. 若前端传入点击经纬度:
- `click_lnglat`
解释器会在同分候选之间优先选择距离点击点最近的对象
6. 当同一点击同时命中区域对象与附近点对象时:
- 优先尊重精确点查命中的可见区域表达
7. 输出统一 pickup 结构
### 11.3 当前解释器不负责
解释器不负责:
- 再造对象 identity
- 替代 style 做渲染决策
- 做最终放行审计
## 12. React Native 伪代码
```ts
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 优先级
当对象/信息层都未命中时,按下面顺序尝试:
1. 查深度数字或等深相关 label
2. 查深度分带 / 水域层
3. 查最近等深线
4. 最后返回“无可解释上下文”
### 13.2 当前建议先做的最小版
最小版只需要先返回:
- 最近 `depth_contour`
- 当前是否落在浅水相关面层
- 当前是否落在危险区 / 航路 / 锚地区
后续再升级成更细的:
- `depth_band`
- `nearest_contour_m`
- `shallow_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. 当前建议的落地顺序
建议前端团队按下面顺序实现:
1. 接入规则字典 JSON
2. 接入仓库内的 TS 解释器,或以它为母版迁入 RN 工程
3. 先实现:
- 精确点查 pickup
- 主对象层补查 pickup
- 信息层 pickup
4. 再实现:
- 空白水域深度上下文
5. 最后把 compare 页和 RN 端解释结果做对齐抽查
## 17. 与后续 PBF 演进的关系
当前方案不要求增加 PBF 容量。
后续如果 builder 增补:
- `object_domain`
- `object_class`
- `object_subclass`
前端仍可继续使用同一套结构,只需把规则字典从“主判读层”逐步降为“兼容补充层”。
## 18. 结论
当前最稳的 native pickup 方案是:
- 继续使用当前 `Karatsu 20nm final` PBF/style
- 使用可见层作为点击入口
- 使用 `feature.id` 作为对象主键
- 使用规则字典和解释器做本地判读
- 对空白水域返回深度上下文
这样既不增加 PBF 容量,也能满足:
- 用户点图标看细节
- 用户点水域看水深上下文
- 后续继续做 CPA / 安全判断
## 19. 参考
- MapLibre React Native `MapView` 官方文档:
- `queryRenderedFeaturesAtPoint`
- `queryRenderedFeaturesInRect`
- https://maplibre.org/maplibre-react-native/docs/components/general/map-view/