# 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 { 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/