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

344 lines
12 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.

# 地图模块-OpenLayers抽吸与碰撞分析
## 1. 背景
当前地图 2D 主引擎已经切换为 OpenLayers项目依赖版本为 `ol@^10.8.0`
本次关注的问题有两个:
- 点位“抽吸”逻辑不符合业务预期;
- 文字碰撞时把整个点位一起隐藏,而不是仅隐藏文字。
典型场景是两个相邻电站,例如“积石峡”和“公伯峡”:
- 可用代号 `1`、`2` 表示两个相邻点;
- 默认缩放下只展示 `1`
- 放大到某个层级后,`1` 隐藏,`2` 展示;
- 再继续放大后,`1` 和 `2` 都展示,但 `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.ts``createPointStyle()` 当前做了这些事:
- 读取 `_iconUrl``_labelText`
- 如果图例不可见、区域不可见,则直接返回 `null`
- 如果 `distance` 不满足当前缩放级别要求,也直接返回 `null`
- 图标使用 `Icon`
- 文字使用 `Text`
- 图标和文字都设置了 `declutterMode: 'declutter'`
- 最终把图标和文字一起放进同一个 `Style` 返回。
这意味着:
- 点图标和文字不是两个独立的渲染对象;
- 它们在当前实现中属于同一个样式结果;
- 只要这组样式参与碰撞判定,图标和文字的命运就是绑定的。
### 3.3 当前已有的“抽稀”逻辑
当前项目中确实有一层“缩放越小,显示越少”的处理,但它不是精细的“抽吸”。
`map.ol.ts``shouldRenderFeatureByDistance()` 逻辑是:
- 小缩放下要求 `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 时,`1` 和 `2` 都显示;
4. 阶段 B 下,`2` 还要相对 `1` 做抽吸/偏移,避免完全重叠。
而当前代码缺少以下能力:
- 没有“近邻点关系”配置
- 代码里没有看到类似 `groupId`、`neighborIds`、`priority`、`displayLevel` 这样的结构;
- 系统不知道“积石峡”和“公伯峡”是一组特殊近邻点。
- 没有“分阶段显示规则”配置
- 当前只有全局的 `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. 结合你的案例做具体映射
如果以近邻点 `1`、`2` 为例,当前代码里实际上缺的是下面这些业务语义:
- `1``2` 属于同一近邻组;
- 组内有主次优先级;
- 在不同 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 的碰撞判断。
在这个基础上,再进入代码修改会更稳。