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