WholeProcessPlatform/frontend/docs/地图模块-OpenLayers抽吸与碰撞分析.md
2026-07-01 08:48:39 +08:00

12 KiB
Raw Permalink Blame History

地图模块-OpenLayers抽吸与碰撞分析

1. 背景

当前地图 2D 主引擎已经切换为 OpenLayers项目依赖版本为 ol@^10.8.0
本次关注的问题有两个:

  • 点位“抽吸”逻辑不符合业务预期;
  • 文字碰撞时把整个点位一起隐藏,而不是仅隐藏文字。

典型场景是两个相邻电站,例如“积石峡”和“公伯峡”:

  • 可用代号 12 表示两个相邻点;
  • 默认缩放下只展示 1
  • 放大到某个层级后,1 隐藏,2 展示;
  • 再继续放大后,12 都展示,但 2 需要“抽吸/拉开”显示,避免与 1 重叠;
  • 如果只是文字发生碰撞,希望点图标保留,仅文字隐藏或延后显示。

本文件先整理当前代码现状和问题成因,不直接修改实现。
如果需要查看当前已经落地的抽吸规则、默认参数和调参入口,请同时参考:

  • docs/地图模块-抽吸规则与参数说明.md

2. 当前代码结构

当前前端 GIS 运行链路如下:

  1. src/components/gis/GisView.vue
  2. src/modules/map/application/map-orchestrator.ts
  3. src/components/gis/map.class.ts
  4. src/components/gis/map.ol.ts
  5. src/components/gis/ol/point-layer-manager.ts
  6. src/components/gis/ol/popup-manager.ts

各层职责简述:

  • GisView.vue

    • 地图入口组件;
    • 挂载地图容器、图例、筛选器、控制器;
    • 通过 mapOrchestrator.mountView() 初始化地图;
    • 菜单切换时通过 handlePageChange() 触发页面地图重载。
  • map.class.ts

    • 地图门面类;
    • 当前 2D 实现实际绑定的是 MapOl
    • 3D 实现才会切到 MapCesium
  • map.ol.ts

    • OpenLayers 主实现;
    • 负责地图初始化、底图加载、点位样式、Popup、区域裁切、量算等
    • 当前“点位渲染规则”和“碰撞相关逻辑”主要集中在这个文件。
  • point-layer-manager.ts

    • 负责点图层创建、Feature 灌入、按图层控制显隐;
    • 点位图层统一使用 VectorLayer + VectorSource
  • popup-manager.ts

    • 管理 hover popup 和高缩放下的批量 popup
    • 内部额外做了一套基于边界框的碰撞判断。

3. 当前点位渲染链路

3.1 点图层创建

point-layer-manager.ts 中的 ensureLayer() 会统一创建点图层:

  • 图层类型为 VectorLayer<VectorSource>
  • 图层上开启了 declutter: true
  • 样式函数统一走 map.ol.ts 中的 createPointStyle()

也就是说,当前所有点位的“是否显示”和“图标/文字如何避让”,都收敛到 OpenLayers 的 declutter 机制和样式函数里。

3.2 点样式生成

map.ol.tscreatePointStyle() 当前做了这些事:

  • 读取 _iconUrl_labelText
  • 如果图例不可见、区域不可见,则直接返回 null
  • 如果 distance 不满足当前缩放级别要求,也直接返回 null
  • 图标使用 Icon
  • 文字使用 Text
  • 图标和文字都设置了 declutterMode: 'declutter'
  • 最终把图标和文字一起放进同一个 Style 返回。

这意味着:

  • 点图标和文字不是两个独立的渲染对象;
  • 它们在当前实现中属于同一个样式结果;
  • 只要这组样式参与碰撞判定,图标和文字的命运就是绑定的。

3.3 当前已有的“抽稀”逻辑

当前项目中确实有一层“缩放越小,显示越少”的处理,但它不是精细的“抽吸”。

map.ol.tsshouldRenderFeatureByDistance() 逻辑是:

  • 小缩放下要求 distance 更大才允许显示;
  • 缩放越大,允许显示的点逐渐增多;
  • 本质上是基于后端数据字段 distance 做全局抽稀。

这个逻辑的特点是:

  • 是全局阈值,不是成对点位或同组点位的专属规则;
  • 只能决定“这个点显示还是不显示”;
  • 不能表达“1 和 2 是近邻关系,并且在不同缩放阶段按业务优先级切换显示”。

3.4 当前批量 Popup 的碰撞检测

popup-manager.ts 在高缩放批量 popup 模式下,又额外做了一层碰撞判断:

  • 先估算图标和文字合并后的锚点边界框;
  • 如果边界框碰撞,则当前要素直接不参与 popup 渲染;
  • 随后再判断 popup 矩形之间是否碰撞。

这说明当前“图标 + 文字”被视为一个整体,不仅体现在 OpenLayers 的 declutter 里,也体现在 popup 的辅助判断里。

4. 当前仓库里“抽吸”相关代码现状

仓库中确实存在历史上的“抽吸”实现,但不在当前 OpenLayers 主链路里。

相关文件:

  • src/utils/leaflet/leaflet.inflatable-markers-group.js
  • src/components/gis/map.leaflet.ts

现状判断:

  • leaflet.inflatable-markers-group.js 是旧 Leaflet 时代的抽吸插件;
  • 它内部有 inflateAsManyAsPossible()_zoomend() 等典型抽吸逻辑;
  • 但当前 map.class.ts 实际实例化的是 new MapOl()
  • map.leaflet.ts 已经不在主运行路径上。

结论是:

  • 当前页面上看到的点位行为,不是 Leaflet 抽吸插件在工作;
  • 现在的显示结果主要来自 OpenLayers 的 declutter、当前样式函数和 distance 抽稀逻辑;
  • 因此“已经有抽吸但是不太对”,更准确地说是“历史上有抽吸代码,但现在主链路没有真正使用那套机制”。

5. 问题分析

5.1 为什么当前实现做不到你要的分阶段抽吸

你描述的目标并不是简单“避让”,而是一个明确的分阶段显示策略:

  1. 默认只显示 1
  2. 缩放到阶段 A 时,隐藏 1,改为显示 2
  3. 缩放到阶段 B 时,12 都显示;
  4. 阶段 B 下,2 还要相对 1 做抽吸/偏移,避免完全重叠。

而当前代码缺少以下能力:

  • 没有“近邻点关系”配置

    • 代码里没有看到类似 groupIdneighborIdsprioritydisplayLevel 这样的结构;
    • 系统不知道“积石峡”和“公伯峡”是一组特殊近邻点。
  • 没有“分阶段显示规则”配置

    • 当前只有全局的 distance 阈值判断;
    • 没有按缩放阶段表达“谁替代谁显示、谁先隐藏、谁后展示”。
  • 没有“抽吸偏移”计算

    • 当前点要素坐标就是原始经纬度转换结果;
    • 没有根据缩放级别和邻近关系生成偏移坐标;
    • 也没有“展开后沿圆周/沿固定方向偏移”的逻辑。
  • 当前 declutter 是被动避让,不是主动编排

    • declutter 只会尽量避免重叠;
    • 它不会理解业务上的“1 是主点、2 是附点”;
    • 也不会自动实现“先 1后 2再两个都展示且 2 偏移”的规则。

所以当前看到的结果不稳定,本质原因不是某一个参数不对,而是“现有机制和目标能力不是一类问题”。

5.2 为什么文字碰撞会把整个点隐藏

这是当前实现里最关键的原因:

  • 点图标和文字是在同一个 Style 里返回的;
  • 图层开启了 declutter: true
  • 图标和文字又都设置了 declutterMode: 'declutter'

在这种组织方式下,当前要素的碰撞判定是按“一个整体渲染单元”处理的,而不是“图标一套规则,文字一套规则”。

因此会出现下面的现象:

  • 图标本身其实不冲突;
  • 但文字边界框发生碰撞;
  • 最终被隐藏的是整条样式结果;
  • 用户视觉上就会觉得“明明只是字挤了,为什么点也没了”。

这和你现在观察到的问题是一致的。

5.3 为什么当前逻辑还会进一步放大这个问题

除了 OpenLayers 自身的 declutter 组织方式,代码里还有两层因素会继续放大问题:

  • shouldRenderFeatureByDistance() 会先根据 distance 直接过滤点位;
  • popup-manager.ts 在批量 popup 模式下,也把“图标 + 文字”当作合并边界框做碰撞模拟。

也就是说,当前系统对近邻点位的处理是:

  • 先用全局 distance 规则筛掉一部分;
  • 再用 OpenLayers declutter 继续过滤;
  • 高缩放时 popup 层再做一轮碰撞跳过。

这三层叠加后,显示行为更偏向“谁先撞谁消失”,而不是“按业务规则分阶段展开”。

6. 结合你的案例做具体映射

如果以近邻点 12 为例,当前代码里实际上缺的是下面这些业务语义:

  • 12 属于同一近邻组;
  • 组内有主次优先级;
  • 在不同 zoom 区间内有不同展示策略;
  • 在最大放大阶段需要给 2 一个偏移位;
  • 偏移后图标和文字还要分别控制碰撞策略。

当前实现只能表达:

  • 某个点在当前 zoom 下是否允许渲染;
  • 某个点图例是否可见;
  • 某个点是否被区域裁切隐藏;
  • 某个点的图标和文字整体是否参与 declutter。

它并不能表达:

  • “1 替代 2 展示”;
  • “2 替代 1 展示”;
  • “1 和 2 一起展示,但 2 抽吸展开”;
  • “只隐藏文字,不隐藏图标”。

所以你现在遇到的问题是结构性问题,不是简单调一个 declutter 参数就能彻底解决。

7. 后续修改时建议优先动的层

如果下一步要真正改这块,建议优先从下面几层入手:

7.1 先补“近邻点显示规则”

建议不要直接把业务规则写死在样式函数里,而是先增加一层近邻点编排规则,例如:

  • groupId
  • priority
  • showZoomMin
  • showZoomMax
  • replaceAtZoom
  • expandAtZoom
  • expandOffset

这样才能表达:

  • 默认只显示主点;
  • 中间层级切换成副点;
  • 更大层级两个都显示;
  • 副点在展示时做偏移。

7.2 抽吸不要依赖 declutter 自动完成

抽吸本质上是“主动布局”问题,不是“被动避让”问题。

建议后续把近邻点抽吸拆成单独逻辑:

  • 先根据原始点数据识别近邻组;
  • 再根据当前 zoom 产出“本次真正参与渲染的点集合”;
  • 对需要展开的点,生成偏移后的渲染坐标;
  • 最后再交给图层渲染。

这样会比直接指望 OpenLayers 的 declutter 更可控。

7.3 图标层和文字层要分开控制

要实现“文字碰撞时只隐藏文字,不隐藏点图标”,后续最重要的一点是把图标和文字的渲染职责拆开。

至少要做到下面二选一:

  • 图标层和文字层拆成两个独立图层;
  • 或者把图标与文字拆成可独立决策的渲染单元。

目标是:

  • 图标优先保留;
  • 文字单独避让;
  • 文本碰撞时只压文字,不压图标。

如果仍然维持“同一个样式对象里同时放 image 和 text”的组织方式这个问题大概率还会持续存在。

7.4 Popup 碰撞策略也要和新规则同步

后续如果点位抽吸和文字单独避让改了,popup-manager.ts 里的边界框逻辑也要一起调整。

否则会出现:

  • 图标已经正常显示;
  • 文字也按新规则显示了;
  • 但 popup 层仍然沿用旧的合并边界框;
  • 最终又在 popup 阶段把结果打回去。

8. 建议的改造顺序

为了降低风险,建议后续改造按以下顺序推进:

  1. 先梳理近邻点数据结构

    • 确定近邻点如何分组;
    • 确定主次优先级和缩放阈值从哪里来。
  2. 再实现“显示决策层”

    • 在真正生成 Style 之前,先决定哪些点应该显示;
    • 哪些点应该隐藏;
    • 哪些点需要偏移展开。
  3. 然后拆分图标和文字碰撞

    • 先保证点图标稳定显示;
    • 再单独做文字避让。
  4. 最后调整 popup 逻辑

    • 保证 popup 的碰撞判定和最终可见点保持一致。

9. 涉及文件清单

本次分析重点涉及以下文件:

  • src/components/gis/GisView.vue
  • src/components/gis/map.class.ts
  • src/components/gis/map.ol.ts
  • src/components/gis/ol/point-layer-manager.ts
  • src/components/gis/ol/popup-manager.ts
  • src/utils/leaflet/leaflet.inflatable-markers-group.js
  • src/components/gis/map.leaflet.ts
  • package.json

10. 当前结论

当前地图问题可以归纳为两点:

  1. 现在的 OpenLayers 主链路并没有真正使用旧 Leaflet 的抽吸机制;
  2. 当前点图标和文字被作为一个整体参与 declutter所以文字碰撞时会连点一起隐藏。

所以后续如果要改,方向不应该只是“调避让参数”,而应该是:

  • 增加近邻点分阶段显示规则;
  • 增加主动抽吸/偏移布局;
  • 把图标和文字拆成可独立控制的渲染单元;
  • 同步修正 popup 的碰撞判断。

在这个基础上,再进入代码修改会更稳。