WholeProcessPlatform/frontend/docs/地图模块-OpenLayers抽吸与碰撞分析.md

344 lines
12 KiB
Markdown
Raw Permalink Normal View History

2026-06-30 08:41:54 +08:00
# 地图模块-OpenLayers抽吸与碰撞分析
## 1. 背景
当前地图 2D 主引擎已经切换为 OpenLayers项目依赖版本为 `ol@^10.8.0`
本次关注的问题有两个:
- 点位“抽吸”逻辑不符合业务预期;
- 文字碰撞时把整个点位一起隐藏,而不是仅隐藏文字。
典型场景是两个相邻电站,例如“积石峡”和“公伯峡”:
- 可用代号 `1`、`2` 表示两个相邻点;
- 默认缩放下只展示 `1`
- 放大到某个层级后,`1` 隐藏,`2` 展示;
- 再继续放大后,`1` 和 `2` 都展示,但 `2` 需要“抽吸/拉开”显示,避免与 `1` 重叠;
- 如果只是文字发生碰撞,希望点图标保留,仅文字隐藏或延后显示。
2026-07-01 08:48:39 +08:00
本文件先整理当前代码现状和问题成因,不直接修改实现。
如果需要查看**当前已经落地的抽吸规则、默认参数和调参入口**,请同时参考:
- `docs/地图模块-抽吸规则与参数说明.md`
2026-06-30 08:41:54 +08:00
## 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 的碰撞判断。
在这个基础上,再进入代码修改会更稳。