659 lines
15 KiB
Markdown
659 lines
15 KiB
Markdown
# 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/
|