OpenLayers:通过自动布局调整解决Overlay重叠问题

article/2025/6/8 0:23:33

一、解决Overlay重叠问题的尝试

我在最近的开发工作中遇到了一个问题。我开发的项目需要给地图上的站点添加Tooltip提示框(即Overlay),但是由于地图上的部分站点比较密集,导致Tooltip的重叠比较严重,部分Tooltip的内容就会被遮挡。业主对此十分不满,要求我们进行优化。

一些解决的尝试

最初我使用了一些 "治标" 的方式进行优化,例如缩小Tooltip的尺寸、添加折叠功能(Tooltip默认只是小尺寸只展示部分数据,鼠标悬停上去后Tooltip展开显示全部的数据)、将当前聚焦的Tooltip移动到最上层等方法,当然也尝试过做分级展示(在默认的缩放级别下只展示部分重要的Tooltip,当层级增大到一定程度后再展示全部的Tooltip)。

可惜的上述的办法都无法彻底解决Tooltip重叠的问题,后来我参考了其它项目后,找到了一种“治本”的解决方案。

手动调整Tooltip的位置

之前Tooltip都被我放置在站点的上方:

但是实际上也可以将Tooltip放置在下方、左边或右边:

因此通过调整Tooltip的方向,就可以让两个邻近的站点的Tooltip错开,从而解决重叠的问题。

于是基于上面的这种思路,我制定了一个解决方案:只展示部分重要站点的Tooltip,然后手动去调整每个Tooltip的方向,尽可能的避免重叠。

我虽然使用了上面的这种方法暂时了应付了业主的需求,但是我对它并不智能。其实原本在我们的系统中是支持用户可以自主配置展示哪些站点的Tooltip,但是如果这么做地图上Tooltip的数量就是不固定的,这样我是没有办法手动调整方向避免重叠的。因此我只能废弃这一功能,只固定展示部分重要站点的Tooltip。......除非有一套自动调整tooltip布局的机制,于是便有了这篇文章 😊。

二、碰撞检测

之前的“手动布局调整”方案主要的步骤就是先人工识别哪些Tooltip出现了重叠,然后再手动对重叠的Tooltip进行位置的调整。现在想要实现“自动布局调整”我认为也要延续这样的思路,只不过要用程序来代替人工。因此第一步就是要实现自动识别哪些Tooltip出现了重叠,这其实就是要做碰撞检测

由于我的目标Tooltip,它本质上是个Element元素,而Element都是矩形,因此我决定使用轴对齐边框算法来进行碰撞检测。

轴对齐边界框 (AABB) 算法

轴对齐边界框(Axis-Aligned Bounding Box,简称 AABB)是一种在计算机图形学、物理引擎和碰撞检测中广泛使用的算法。它的核心思想是用一个与坐标轴对齐的矩形(在 2D 中)或长方体(在 3D 中)来包裹一个物体,从而简化碰撞检测的计算。

算法步骤

构建 AABB

  1. 遍历物体的所有顶点,找到每个维度的最小值和最大值。
  2. 构建边界框:
    • 在 2D 中:minX = min(所有顶点的 x 坐标)maxX = max(所有顶点的 x 坐标)minYmaxY 同理。
    • 在 3D 中:minX = min(所有顶点的 x 坐标)maxX = max(所有顶点的 x 坐标)minYmaxYminZmaxZ 同理。

碰撞检测

  • 判断两个 AABB 是否相交:
    • 在 2D 中:如果 box1.maxX < box2.minXbox1.minX > box2.maxXbox1.maxY < box2.minYbox1.minY > box2.maxY,则两个边界框不相交。
    • 在 3D 中:如果 box1.maxX < box2.minXbox1.minX > box2.maxXbox1.maxY < box2.minYbox1.minY > box2.maxYbox1.maxZ < box2.minZbox1.minZ > box2.maxZ,则两个边界框不相交。

优缺点

优点

  • 计算简单:AABB 的碰撞检测只需要比较边界框的坐标,计算量小,适合快速筛选。
  • 内存占用小:只需要存储两个点(最小点和最大点),内存占用低。
  • 适合动态物体:如果物体移动或旋转,可以快速更新边界框。

缺点

  • 精度较低:AABB 是轴对齐的,对于旋转的物体,边界框会变得很大,导致碰撞检测的精度下降。
  • 不适合复杂形状:对于不规则形状的物体,AABB 可能会包含大量空白区域,导致误判。

代码示例

2D AABB 碰撞检测

function isAABBColliding(box1, box2) {return !(box1.maxX < box2.minX ||box1.minX > box2.maxX ||box1.maxY < box2.minY ||box1.minY > box2.maxY);
}

3D AABB 碰撞检测

function isAABBColliding(box1, box2) {return !(box1.maxX < box2.minX ||box1.minX > box2.maxX ||box1.maxY < box2.minY ||box1.minY > box2.maxY ||box1.maxZ < box2.minZ ||box1.minZ > box2.maxZ);
}

对Tooltip使用AABB算法

Tooltip的边界框

想要使用AABB算法首先要找到Tooltip的边界框。由于Tooltip本质是Element元素因此就可以借助Element.getBoundingClientRect()方法。

BoundingClientRect()方法会返回一个 DOMRect对象,是包含整个元素的最小矩形(包括 paddingborder-width)。

该对象使用 lefttoprightbottomxywidthheight 这几个以像素为单位的只读属性描述整个矩形的位置和大小。除了 widthheight 以外的属性是相对于视图窗口的左上角来计算的。

根据DOMRect对象我们可以计算出Tooltip的边界框,因此在后面我会将DOMRect对象就视为Tooltip的边界框。

const rect = tooltip.getElement().getBoundingClientRect()// 最小X
const minX = rect.x
// 最大X
const maxX = rect.x + rect.width
// 最小Y
const minY = rect.y
// 最大Y
const maxY = rect.y + rect.width

但是注意,上面的代码只能获取某一个方位上Tooltip的边界框,而一个站点位置上我设计了上、下、左、右四个方位,在这四个方位上的Tooltip我都有可能去进行碰撞检测,因此我最终写了一个方法实现通过一个Tooltip来计算四个方位上的边界框。

// 计算提示框在上下左右四个位置的Rect
function getRects(tooltip) {const positioning = tooltip.getPositioning();const offset = 10;const rect = tooltip.getElement().getBoundingClientRect();let cx,cy,w = rect.width,h = rect.height;switch (positioning) {case POSITIONINGS.TOP:cx = rect.x + rect.width / 2;cy = rect.y + rect.height + offset;break;case POSITIONINGS.RIGHT:cx = rect.x - offset;cy = rect.y + rect.height / 2;break;case POSITIONINGS.BOTTOM:cx = rect.x + rect.width / 2;cy = rect.y - offset;break;case POSITIONINGS.LEFT:cx = rect.x + rect.width + offset;cy = rect.y + rect.height / 2;break;}const result = {TOP: {x: cx - w / 2,y: cy - h - offset,w,h,},RIGHT: {x: cx + offset,y: cy - h / 2,w,h,},BOTTOM: {x: cx - w / 2,y: cy + offset,w,h,},LEFT: {x: cx - w - offset,y: cy - h / 2,w,h,},};return result;
}

注意

由于我在四个方位上的Tooltip都有一段10像素的偏移,因此我在计算边界框时将offset也加入了计算当中。注意要将可能的偏移加入到计算当中来,否则会导致碰撞检测的结果失真。

构建判断方法

之后我们就可以构建一个检测Tooltip是否重叠的方法。

/***  @abstract 判断两个矩形是否重叠 (碰撞检测- 使用aabb算法)* @param {{x: number, y: number, w: number, h: number}} a 矩形a* @param {{x: number, y: number, w: number, h: number}} b 矩形b* @returns {boolean} 是否重叠*/
function isRectOverlay_aabb(a, b) {return !(a.x + a.w <= b.x ||b.x + b.w <= a.x ||a.y + a.h <= b.y ||b.y + b.h <= a.y);
}

三、自动布局优化(四个固定位置中选择 + 贪心算法)

在实现了碰撞检测之后,接下来就要进行自动布局优化,也就是我希望可以用程序来自动判断Tooltip放在哪个方位会更好。这本质上其实是一个求最优解的问题,因此我决定使用最简单的贪心算法来实现。

贪心算法(贪心选择)

定义

贪心算法是一种在每一步选择中都力图使局部最优解能够逐步达到全局最优解的算法。与其他算法不同的是,贪心算法在选择过程中不进行回溯,选择的过程往往基于某种局部最优策略。在一些特定的问题中,贪心算法可以通过逐步构建最优解来实现全局最优。

基本思想

贪心算法的核心思想是通过一系列局部最优选择来构建全局最优解。

优缺点

✅ 优点

  • 简单直观,易于实现
  • 通常时间复杂度较低(O(n log n)常见)

❌ 缺点

  • 不能保证所有问题都得到最优解
  • 需要严格证明其正确性

使用贪心算法进行布局优化

既然贪心算法的思路是在每次抉择时都追求局部最优解,最终达到全局最优解。因此我指定了如下的使用方式:

遍历Tooltip列表,每个Tooltip的每个位置都去检测其与其它已添加的Tooltip是否重叠,选择最优的位置(局部最优解)。

局部最优解是什么?

这时候我就遇到了一个问题,我如何去判断哪个位置是最优位置?

我进行了一些尝试,在最初我将重叠数最少作为局部最优解。但是我很快就发现这样明显是不对的,因为很可能就会出现一种情况:

在A位置虽然只与一个Tooltip重叠,但是却完全把这个Tooltip给遮挡住了;在B位置虽然与好几个Tooltp,但是都只是擦边。

上述的情形下明显B位置更符合我们的需求,但根据重叠数最少的原则A位置才是最优的😅。

之后我又尝试将中心点距离最大作为局部最优解,我会去计算某个Tooltip中心点与其它Tooltip中心点之间的总距离,最后选择总距离最大的位置。但是最后我发现这样也不准确,因为我Tooltip是个矩形不是圆形,对于矩形来说它的中心点到各个边的距离是不一样的,因此用中心点距离来表示重叠程度是有误差的。

最后我还是选择用重叠面积最小作为局部最优解。这样最能够反应重叠的程度。

/*** 计算两个矩形之间的重叠面积* @param {{x: number, y: number, w: number, h: number}} a 矩形a* @param {{x: number, y: number, w: number, h: number}} b 矩形b* @returns {number} 重叠面积*/
function calculateOverlapArea(a, b) {const dx = Math.max(0, Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x));const dy = Math.max(0, Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y));return dx * dy;
}

使用算法

const POSITIONINGS = {TOP: "bottom-center",RIGHT: "center-left",BOTTOM: "top-center",LEFT: "center-right",
};// overlay 布局优化
function layoutOptimized(overlays) {const overlaysRects = []; // 所有overlay的矩形const chosenPositions = []; // 所有overlay的最终位置// 遍历所有overlayfor (let i = 0; i < overlays.length; i++) {const overlay = overlays[i]; // 当前overlayconst rects = getRects(overlay); // 当前overlay的矩形 (上下左右四个位置的矩形)let maxWeight = -Infinity; // 最大权重let bestPos = "TOP"; // 最佳位置// 遍历所有位置for (let posIdx = 0; posIdx < Object.keys(POSITIONINGS).length; posIdx++) {const posKey = Object.keys(POSITIONINGS)[posIdx];const rect = rects[posKey]; // 当前位置的矩形let overlayArea = 0; // 当前位置下的重叠面积// 遍历所有已布局的overlay的矩形,计算重叠面积for (let j = 0; j < overlaysRects.length; j++) {overlayArea += isRectOverlay_aabb(rect, overlaysRects[j])? calculateOverlapArea(rect, overlaysRects[j]): 0;}// 根据重叠面积计算权重const posWeight = -overlayArea;// 更新最佳位置if (posWeight > maxWeight) {maxWeight = posWeight;bestPos = posKey;}if (posWeight === 0) break;}overlaysRects.push(rects[bestPos]);chosenPositions.push(bestPos);}overlays.forEach((overlay, idx) => {overlay.setPositioning(POSITIONINGS[chosenPositions[idx]]);});
}

基本思路

最后整体呈现出来的效果还可以

布局优化

优化

我在使用的过程中发现当前的布局调整还存在明显的问题,比如下面的这个例子中 Tooltip3与Tooltip20、Tooltip2与Tooltip17 就明显不是最优解。

造成这种现象主要是因为,我在统计Tooltip的重叠面积的过程中并没有将所有的Tooltip都加入计算,而只将已优化的Tooltip加入了计算。假设一共有20个Tooltip,第一个Tooltip的重叠面积一定为零,因为此时没有其它已优化的Tooltip;在计算第二个Tooltip的重叠面积时,会将第一个Tooltip加入重叠面积的计算;而计算第二十个Tooltip的重叠面积时则会将其它十九个Tooltip加入重叠面积的计算。

了解了问题的根源就可以进行优化了,我准备在每个Tooltip计算重叠面积时都将其它的Tooltip都加入计算,此时的基本流程就是这样:

调整后的代码如下:

const POSITIONINGS = {TOP: "bottom-center",RIGHT: "center-left",BOTTOM: "top-center",LEFT: "center-right",
};function layoutOptimized2(overlays) {const overlaysRects = []; // 所有overlay的矩形const chosenPositions = []; // 所有overlay的最终位置for (let i = 0; i < overlays.length; i++) {const overlay = overlays[i];const rects = getRects(overlay);overlaysRects.push(rects);chosenPositions.push("TOP");}// 遍历所有overlayfor (let i = 0; i < overlays.length; i++) {const overlay = overlays[i];const rects = overlaysRects[i]; // 当前overlay的矩形 (上下左右四个位置的矩形)let maxWeight = -Infinity; // 最大权重let bestPos = "TOP"; // 最佳位置// 遍历所有位置for (let posIdx = 0; posIdx < Object.keys(POSITIONINGS).length; posIdx++) {const posKey = Object.keys(POSITIONINGS)[posIdx];const rect = rects[posKey]; // 当前位置的矩形let overlayArea = 0; // 当前位置下的重叠面积// 遍历所有已布局的overlay的矩形,计算重叠面积for (let j = 0; j < chosenPositions.length; j++) {if (i !== j) {const rect2 = overlaysRects[j][chosenPositions[j]];overlayArea += isRectOverlay_aabb(rect, rect2)? calculateOverlapArea(rect, rect2): 0;}}// 根据重叠面积计算权重const posWeight = -overlayArea;// 更新最佳位置if (posWeight > maxWeight) {maxWeight = posWeight;bestPos = posKey;}if (posWeight === 0) break;}chosenPositions[i] = bestPos;}overlays.forEach((overlay, idx) => {overlay.setPositioning(POSITIONINGS[chosenPositions[idx]]);});
}

使用调整后的方法进行布局优化后,可以看到之前例子中 Tooltip3与Tooltip20、Tooltip2与Tooltip17 的布局更加合理了。

参考资料

  1. 碰撞检测技术详解-CSDN博客
  2. AABB(axis-aligned bounding box)_aabb包围盒-CSDN博客
  3. 算法设计与分析——贪心算法(详解版)_哈夫曼编码 贪心算法-CSDN博客

http://www.hkcw.cn/article/mAutSrnkgF.shtml

相关文章

7.5- Loading a pretrained LLM

Chapter 7-Fine-tuning to follow instructions 7.5- Loading a pretrained LLM 开始微调前&#xff0c;我们先加载GPT2模型&#xff0c;加载 3.55 亿参数的中型版本&#xff0c;因为 1.24 亿模型太小&#xff0c;无法通过指令微调获得定性合理的结果 ​ 加载 gpt2-medium (…

C++:内存管理

一.深入理解C/C的内存分布 以上是一张C/C 程序内存分区示意图&#xff1a; 栈区 存放内容&#xff1a;局部变量&#xff08;如函数内部定义的普通变量 int a 10; &#xff09;、函数的形式参数 。其特点是由编译器自动分配和释放&#xff0c;遵循先进后出原则&#xff0c;…

【结构型模式】装饰器模式

文章目录 装饰器模式装饰器模式当中的角色和职责装饰器模式的代码实现装饰器模式与代理模式有何不同&#xff1f;装饰器模式的优缺点适用场景 装饰器模式 装饰器模式&#xff08;Decorator Pattern&#xff09;&#xff1a;动态地给一个对象增加一些额外的职责&#xff0c;对于…

Ubuntu 挂载新盘

1.磁盘分区 rootljz:/# lsblk NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT loop0 7:0 0 4K 1 loop /snap/bare/5 loop1 7:1 0 104.2M 1 loop /snap/core/17200 loop2 7:2 0 73.9M 1 loop /snap/core22/1908 loop3 7:3 0 104.6M 1 loo…

Flink03-学习-套接字分词流自动写入工具

上一节中通过如下命令启动服务摸来模拟Socket流。 现在我们写一个ServerSocket来模拟让流自动写入不用手动操作。 pom.xml和上一节一致不需要修改 编写代码 同样适用Socket流 // 使用socket流创建一个从 socket 读取文本的数据流&#xff0c;以换行符 \n 作为分隔符DataStre…

2022年 国内税务年鉴PDF电子版Excel

2022年 国内税务年鉴PDF电子版Excelhttps://download.csdn.net/download/2401_84585615/89784658 https://download.csdn.net/download/2401_84585615/89784658 2022年国内税务年鉴是对中国税收政策、税制改革和税务管理实践的全面总结。这份年鉴详细记录了中国税收系统的整体状…

Gitee Wiki:以知识管理赋能 DevSecOps,推动关键领域软件自主演进

关键领域软件研发中的知识管理困境 传统文档管理模式问题显著 关键领域软件研发领域&#xff0c;传统文档管理模式问题显著&#xff1a;文档存储无系统&#xff0c;查找困难&#xff0c;降低效率&#xff1b;更新不及时&#xff0c;与实际脱节&#xff0c;误导开发&#xff1…

Hadoop 3.x 伪分布式 8088端口无法访问问题处理

【Hadoop】YARN ResourceManager 启动后 8088 端口无法访问问题排查与解决(伪分布式启动Hadoop) 在配置和启动 Hadoop YARN 模块时&#xff0c;发现虽然 ResourceManager 正常启动&#xff0c;JPS 进程中也显示无误&#xff0c;但通过浏览器访问 http://主机IP:8088 时却无法打…

【最小生成树】P2573 [SCOI2012] 滑雪

题目 洛谷&#xff1a;P2573 [SCOI2012] 滑雪 分析 题目条件要点分析&#xff1a; 这道题要求 i 能到达 j 的前提是 i 、j 之间有一条连通的边并且i 的高度比 j 高。这意味着本题给出的是一个有向图。时间胶囊可以返回到上一个景点&#xff0c;可以无限使用&#xff0c;意…

2.2.2 06年T2

Stratford的两大对立力量&#xff1a;令人讽刺的居民与令人同情的公司 - 2006年考研英语Text 2精析 本文解析2006年考研英语Text 2&#xff0c;揭示Stratford小镇居民与皇家莎士比亚剧团(RSC)的深层矛盾。 一、原文与翻译 Paragraph 1&#xff1a;对立双方的形成 L1: Stratfor…

基于人工智能算法实现的AI五子棋博弈

1. 项目概述 本项目实现了一个完整的五子棋游戏系统&#xff0c;包含游戏界面、交互逻辑和人工智能对战功能。 系统采用Python语言开发&#xff0c;使用Pygame库进行图形界面渲染&#xff0c;实现了三种游戏模式&#xff1a;人人对战、人机对战和AI对战。 AI算法基于博弈树搜…

在 Ubuntu 系统上使用 Python 的 Matplotlib 库时遇到的字体缺失问题

报错问题 findfont: Font family [SimHei] not found. Falling back to DejaVu Sans. 在现实图片时尝试显示中文字符命令行报错&#xff0c;在图片中显示方框。 最终解决方案 在尝试了各种方法之后&#xff0c;在代码中添加下图中选中行&#xff0c;问题直接解决。

webstrom中git插件勾选提交部分文件时却出现提交全部问题怎么解决

原因是我有个.husky的文件制定了执行提交的时候就是提交所有的文件 修改.husky/pre-commit文件就可以啦 #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh"# 获取通过 WebStorm 提交的暂存文件&#xff08;仅勾选的部分&#xff09; STAGED_FILES$(gi…

KINGCMS被入侵

现象会强制跳转到 一个异常网站,请掉截图代码. 代码中包含经过混淆处理的JavaScript&#xff0c;它使用了一种技术来隐藏其真实功能。代码中使用了eval函数来执行动态生成的代码&#xff0c;这是一种常见的技术&#xff0c;恶意脚本经常使用它来隐藏其真实目的。 这段脚本会检…

CMS32M65xx/67xx系列CoreMark跑分测试

CMS32M65xx/67xx系列CoreMark跑分测试 1、参考资料准备 1.1、STM32官方跑分链接 1.2、官网链接 官方移植文档&#xff0c;如下所示&#xff0c;点击红框处-移植文档: A new whitepaper and video explain how to port CoreMark-Pro to bare-metal 1.3、测试软件git下载链接 …

Vue.js教学第十八章:Vue 与后端交互(二):Axios 拦截器与高级应用

Vue 与后端交互(二):Axios 拦截器与高级应用 在上一篇文章中,我们学习了 Axios 的基本用法,包括如何发送不同类型的 HTTP 请求以及基本的配置选项。本文将深入剖析 Axios 的拦截器功能,探讨请求拦截器和响应拦截器的作用、配置方法和应用场景,通过实例展示如何利用拦截…

【信创-k8s】海光/兆芯+银河麒麟V10离线部署k8s1.31.8+kubesphere4.1.3

❝ KubeSphere V4已经开源半年多&#xff0c;而且v4.1.3也已经出来了&#xff0c;修复了众多bug。介于V4优秀的LuBan架构&#xff0c;核心组件非常少&#xff0c;资源占用也显著降低&#xff0c;同时带来众多功能和便利性。我们决定与时俱进&#xff0c;使用1.30版本的Kubernet…

【判断酒酒花数】2022-3-31

缘由对超长正整数的处理&#xff1f; - C语言论坛 - 编程论坛 void 判断酒酒花数(_int64 n) {//缘由https://bbs.bccn.net/thread-508634-1-1.html_int64 t n; int h 0, j 0;//while (j < 3)h t % 10, t / 10, j;//整数的个位十位百位之和是其前缀while (t > 0)h t…

oauth2.0

OAuth 2.0 的工作原理和流程。 OAuth 2.0 是一个授权框架&#xff0c;它允许第三方应用获取对用户资源的有限访问权限&#xff0c;而无需获取用户的密码。以下是详细说明&#xff1a; 1. OAuth 2.0 的四个主要角色 资源所有者&#xff08;Resource Owner&#xff09; 通常是…

笔记本/台式C盘扩容:删除、压缩、跨分区与重分配—「小白教程」

删除C盘右侧分区以扩展 删除分区&#xff0c;也会删除分区中所有资料&#xff0c;请注意备份所有重要资料。 1.WinX选择磁盘管理&#xff0c;右键点击C盘右侧分区&#xff0c;选择删除卷&#xff0c;原分区会变成黑色的“未分配”空间&#xff1b; 2.此时右键C盘选择“扩展卷…