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 的碰撞判断。
|
|
|
|
|
|
|
|
|
|
|
|
在这个基础上,再进入代码修改会更稳。
|