一种解决Leaflet中Divicon城市气温标注空间重叠的办法

举报
夜郎king 发表于 2024/11/13 12:57:11 2024/11/13
【摘要】 本文即重点介绍如何在Leaflet中进行DidIcon信息标注时,应对空间对象重叠的问题,如何去解决。文章首先介绍一些可能的解决办法,然后介绍如何基于rbush和Leaflet.LayerGroup.Collision.js这两个组件来实现,最后介绍具体的实现方法即实现成果。

目录

前言 

一、一些解决办法

1、marker的聚类

2、使用leaflet-canvas-label

3、使用Zoom和样式控制

二、基于rbush和Leaflet.LayerGroup.Collision的解决办法

1、关于rbush

2、Leaflet.LayerGroup.Collision.js

三、解决标签重叠的具体实现

1、添加元数据

2、添加到collisionLayer中

3、实现效果

 四、总结



前言 

        又是一年的1024,各位程序员朋友们又迎来了一年一度的程序员节。此刻的你又在做什么呢?是在工位上奋力敲击,用键盘创造作品亦或者处理bug呢。下午刚跟产品达成的一致,希望明天不要再变,希望验收要求不要太变态,哎。万一要变,除了响应又有什么办法呢。或者在进行测试,性能测试也好,功能测试也好。跟开发的同学关系还好吗?是不是又在撕扯一些问题究竟要不要改或者优先级要不要定这么高。是否在专注的进行产品设计,将用户的需求转换成产品文档。这又是第几稿了呢?还记得上次远程会议上,用户信誓旦旦的保证,坚决不会再加需求了。望着邮箱中又发过来的紧急邮件,朋友你还好吗?是否在机房紧张的排查问题呢?系统莫名的停服了半个小时,头顶为数不多的细发也在瑟瑟发抖,今晚或许又是一个通宵。哎,作为运维,不就是扮演救火队去专门灭火嘛?先抽根烟,稳稳神,慢慢排查吧。不是增长了经验就是增长了怒火。哎,1024。年年岁岁都有1024,一样的工作,一样的状态。有同学说,已经开始了滴滴和外卖半年了。体力在慢慢转好。调侃也罢,事实也罢。真心的祝所有的程序员朋友们开心快乐,祝大家生活、工作愉快。

        以上皆是赘言,算个起题。记得在之前的一篇博客中曾讲解如何在Leaflet中进行DivIcon的标注开发。虽然成功的实现了需求,但是不知道各位朋友们有没有实际运行过。在地图上进行点标注之后,如果进行缩放,很难免就会有标注的重叠,掌握空间可视化的朋友应该都知道。随着地图的不妨缩小,比例尺也在不断的变化。而我们的DivIcon由于是已经创建出来的html页面元素,正常情况它是不会变化的。因此在进行空间可视化时就会出现以下的情况。各个标签就完全重叠到了一起。密密麻麻的,特别不好看。以我国东南部一些城市的天气情况综合WebGIS可视化效果,来看下面的效果图:

        可以看到,这些网页的标注都重叠到了一起。虽然我们可以放大地图,以此来让这些标签分开。这样子的效果就比较清爽,关键的信息没有被遮挡。城市的气温信息很友好,看起来很直观。效果如下所示: 

        那么有没有一种合理的方式,就像之前我们分享过的标签自动避让的功能,基于Canvas的标注标签自动避让呢,在空间中碰撞之后就会有标签隐藏起来。但是很遗憾,在Leaflet官方的API中,是没有这种功能的,除了针对Marker有点聚合的功能,针对divIcon的聚合貌似没有。本文即重点介绍如何在Leaflet中进行DidIcon信息标注时,应对空间对象重叠的问题,如何去解决。文章首先介绍一些可能的解决办法,然后介绍如何基于rbush和Leaflet.LayerGroup.Collision.js这两个组件来实现,最后介绍具体的实现方法即实现成果。如果您对如何解决DivIcon的空间重叠问题有兴趣,不妨来这里看看。

一、一些解决办法

         在空间中进行大量的信息标注是一种很常见的需求,由空间的相邻特性所决定的其位置的相邻。由此带来的一些空间重叠的问题。也是很常见的一种现象。这里围绕重叠标注的问题,来寻找一些可行的解决方案。

1、marker的聚类

        对marker进行聚类是一种常见的操作。markercluster是将这些点先聚合成一起,然后伴随着地图的方法按照位置来进行展示。如下图所示:

        这其实也是一种解决办法,即将这些空间标注使用聚类的方式来实现,对于聚集在一起的位置信息,就组合在一起,都不进行展示, 但是这样聚类的实现效果带来的就是信息都不展示。只有地图放大后才会进行展示。

2、使用leaflet-canvas-label

        如果想把点信息都展示出来,但是对于中文标注又可以实现一个避让的另一种方法就是使用leaflet-canvas-label。在前面的博客中也介绍过这些知识,实现的效果如下:

        使用leaflet-canvas-label的标签避让实现方式,虽然可以把信息都展示出来,但是其内部是基于Canvas来实现的,因此想实现自定义的div,如divicon的实现方式就不行,因为在canvas中,它的实现原理是在绘制标签时进行宽度判断。在其源码中可以看到如下代码:

//筛选需要碰撞检测的标签图层并安装zIndex排序
let collisionLayers = labelLayers.filter((layer) => {
    var collisionFlg =
        layer.options.labelStyle.collisionFlg != undefined
              ? layer.options.labelStyle.collisionFlg
              : this.options.defaultLabelStyle.collisionFlg;
     return collisionFlg == true;
 });


let textBounds = { minX, minY, maxX, maxY, layer };
if (
   !(
      labelStyle.collisionFlg == true &&
      this._textBounds.collides(textBounds)
   )
) {
   //绘制标注
   ctx.strokeText(labelStyle.text, 0, 0);
   ctx.fillText(labelStyle.text, 0, 0);
   this._textBounds.insert(textBounds);
}

        使用leaflet.canvaslabel来实现标注避让虽然可以实现需求,但是很明显的是。不支持我们的divicon的标绘方式,因此这个方案暂且搁置。

3、使用Zoom和样式控制

        在web页面中,我们可以对样式进行控制,比如我们可以控制在不同的zoom中,我们的标签不显示出来,在zoom=10级的情况下,将标签的display设置为true,将标签展示出来。而在zoom< 10的情况下,调用样式的display为false,这样就可以实现divicon标注的显示与隐藏。同样,缺点也是非常明显的,不能精确的控制,而且不好预估zoom,对于后期的扩展和修复是比较麻烦的。因此我们需要一种更加灵活的方式。既能实现divicon的动态展示,也能实现重叠对象的问题解决。

二、基于rbush和Leaflet.LayerGroup.Collision的解决办法

        既然上面的方案或多或少都有一些缺陷,那么如何来实现这种需求呢?这里分享一种方法。即使用rbush和Leaflet.LayerGroup.Collision.js来进行支持。本节就对这两个外部的组件来进行说明。

1、关于rbush

        RBush 是一个高性能的 JavaScript 库,用于点和矩形的 2D 空间索引。它基于具有批量插入支持的优化 R-tree 数据结构。空间索引是点和矩形的特殊数据结构,它允许你非常有效地执行诸如 “此边界框内的所有项目” 之类的查询(例如,比遍历所有项目快数百倍)。它最常用于地图和数据可视化。RBush,作为一款专门为JavaScript设计的空间索引库,以其卓越的性能和灵活性,在众多开发者中赢得了广泛的好评。它不仅能够高效地处理二维空间中的点和矩形数据,更因其采用了优化的R树数据结构而具备了处理大规模数据集的能力。RBush的一个显著优势在于它的批量插入功能,这使得在处理大量数据时,无需逐个插入即可实现高效的数据索引建立,极大地提高了数据处理的速度与效率。对于那些需要频繁进行数据查询、插入或删除操作的应用场景来说,RBush无疑是一个理想的选择。

        在这里采用rbush,是为了在前端构建一个2d的Rtree来进行空间数据的查询。关于rbush的要求大家可以在官网网站上查询。rbush分享镜像

        这里注意一下,在进行空间重叠数据的分析时,rbush的版本不能使用太高。需要的朋友可以在博客下面留言,看到会发送可用的版本。 关于rbush的核心方法,这里不细细展开,后面可以找时间来进行深入讲解。

2、Leaflet.LayerGroup.Collision.js

        Leaflet.LayerGroup.Collision.js组件与marker的聚类组件差不多,主要作用就是生成前端聚类。Leaflet.LayerGroup.Collision.js的官方地址如下Leaflet.LayerGroup.Collision

        在src中,包含了具体的组件,感兴趣的可以自己打开源码进行学习。 到这里,对使用的rbush和Leaflet.LayerGroup.Collision进行了简单的介绍,下面将结合实际的例子来进行说明如何进行代码的开发。

三、解决标签重叠的具体实现

        在对涉及的技术进行了简单介绍后,下面来对如何使用rbush和Leaflet.LayerGroup.Collision来进行实例开发,以解决标签重叠的问题。实现标签的碰撞检测,满足用户的需求。

1、添加元数据

        介绍完我们的用户场景后,我们来准备用户的基础元数据。这里以城市天气气温信息的展示。首先包括城市的国籍、城市具体信息、时间和温度。当然,这些数据,您可以从自己的后台接口或者从互联网公开的接口来获取。这里我们仅演示如何进行标绘,对如何获取接口不进行赘述。如果接口中的字段有所修改,请在代码实践过程中进行合理调整相关字段。下面给大家分享具体的元数据,代码如下:

var dataJson = [
		{lat:24.251973,lon:123.873596,c_name:"日本",p_name:"丰原",date:"2024-10-18 22:30:15",temp:"23"},
		{lat:24.497146,lon:117.91626,c_name:"中国",p_name:"福建省漳州市",date:"2024-10-19 22:30:15",temp:"27"},
		{lat:24.116675,lon:120.695801,c_name:"中国",p_name:"台湾省台中市",date:"2024-10-16 22:30:15",temp:"21"},
		{lat:22.634293,lon:120.300293,c_name:"中国",p_name:"台湾省高雄市",date:"2024-10-15 22:30:15",temp:"19"},
		{lat:26.046913,lon:119.245605,c_name:"中国",p_name:"福建省福州市",date:"2024-10-13 22:30:15",temp:"23"},
		{lat:23.58979,lon:120.880508,c_name:"中国",p_name:"台湾省同富村",date:"2024-10-12 22:30:15",temp:"24.5"},
		{lat:22.745789,lon:115.378418,c_name:"中国",p_name:"广东省汕尾市",date:"2024-10-11 22:30:15",temp:"23"},
		{lat:22.748322,lon:121.153107,c_name:"中国",p_name:"台湾省台东县",date:"2024-10-19 09:30:15",temp:"26"},
		{lat:23.989076,lon:121.619339,c_name:"中国",p_name:"台湾省花莲县",date:"2024-10-18 09:30:15",temp:"28"},
		{lat:21.965335,lon:120.813732,c_name:"中国",p_name:"台湾省垦丁公园",date:"2024-10-17 09:30:15",temp:"21"},
		{lat:24.284523,lon:116.122742,c_name:"中国",p_name:"广东省梅州市",date:"2024-10-17 09:30:15",temp:"23"},
		{lat:25.078136,lon:117.007141,c_name:"中国",p_name:"广东省龙岩市",date:"2024-10-16 09:30:15",temp:"22"}
	];

2、添加到collisionLayer中

        与前面介绍过的divicon标注方法一致,在这里还是要根据坐标点生成marker,然后在marker中设置divicon元素信息。关键代码如下:

var collisionLayer = L.LayerGroup.collision({margin:5});

for(var i=0;i<dataJson.length;i++){
	var marker = L.marker([dataJson[i].lat, dataJson[i].lon], {
		icon: L.divIcon({
			iconSize: null,
			className: '',
			popupAnchor:[5,5],
			shadowAnchor:[5,5],
			html: "<div class='marsBlackPanel'  animation-spaceInDown><div class='marsBlackPanel-text' style=''>国家名称:"+dataJson[i].c_name +"</div>"
			+"<div class='marsBlackPanel-text' style=''>城市名称:" + dataJson[i].p_name + "</div>"
			+"<div class='marsBlackPanel-text' style=''>采集时间:" + dataJson[i].date + "</div>"
			+"<div class='marsBlackPanel-text' style=''>当前温度:<span class='temperature'>" +dataJson[i].temp+ "</span> ℃</div></div>"
		})
	}).addTo(collisionLayer);
}

collisionLayer.addTo(map);

        首先我们会生成一个包含对象margin距离的图层组,通过图层组来进行marker数据绑定。在实际创建marker对象时,会根据这个margin来进行范围创建,代码如下:

_positionBox: function(offset, box) {
	return [
		box[0] + offset.x - this._margin,
		box[1] + offset.y - this._margin,
		box[2] + offset.x + this._margin,
		box[3] + offset.y + this._margin,
	]
}

        很明显,在这里将我们传入的组件的宽度加上本身组件的偏移量来生成bbox,然后将bbox添加到rbush中进行判断。将当前图层添加到rbush建立的rtree中,见下面的_maybeAddLayerToRBush方法。

addLayer: function(layer) {
		if ( !('options' in layer) || !('icon' in layer.options)) {
			this._staticLayers.push(layer);
			parentClass.prototype.addLayer.call(this, layer);
			return;
		}

		this._originalLayers.push(layer);
		if (this._map) {
			this._maybeAddLayerToRBush( layer );
		}
	}
_maybeAddLayerToRBush: function(layer) {
	var z    = this._map.getZoom();
	var bush = this._rbush;

	var boxes = this._cachedRelativeBoxes[layer._leaflet_id];
	var visible = false;
	if (!boxes) {
		// Add the layer to the map so it's instantiated on the DOM,
		//   in order to fetch its position and size.
		parentClass.prototype.addLayer.call(this, layer);
		var visible = true;
// 		var htmlElement = layer._icon;
		var box = this._getIconBox(layer._icon);
		boxes = this._getRelativeBoxes(layer._icon.children, box);
		boxes.push(box);
		this._cachedRelativeBoxes[layer._leaflet_id] = boxes;
	}

	boxes = this._positionBoxes(this._map.latLngToLayerPoint(layer.getLatLng()),boxes);

	var collision = false;
	for (var i=0; i<boxes.length && !collision; i++) {
		collision = bush.search(boxes[i]).length > 0;
	}

	if (!collision) {
		if (!visible) {
			parentClass.prototype.addLayer.call(this, layer);
		}
		this._visibleLayers.push(layer);
		bush.load(boxes);
	} else {
		parentClass.prototype.removeLayer.call(this, layer);
	}
}

3、实现效果

        实践是检验真理的唯一标准,在经过上面的编程后我们基本实现了对divicon的标注重叠。下面来看一下具体的效果。

        大家在上图的效果中就可以看到,在地图上重叠的divicon数据已经完全分开了。目前只展示了两个城市的信息。接着我们使用鼠标滚轮来进行地图的方法,看随着地图的放大,其它隐藏的标注是否会展示出来。 

        可以很明显的看到,随着地图的放大,原先隐藏的标签现在都展示出来了。进一步放大后看一下实际的效果如何?

         可以直观的看到,经过上面的开发步骤,我们已经成功的实现DivIcon标注的重叠隐藏和展示。非常完美的实现了我们的需求。

 四、总结

        以上就是本文的主要内容,本文即重点介绍如何在Leaflet中进行DidIcon信息标注时,应对空间对象重叠的问题,如何去解决。文章首先介绍一些可能的解决办法,然后介绍如何基于rbush和Leaflet.LayerGroup.Collision.js这两个组件来实现,最后介绍具体的实现方法即实现成果。当然,在本文的实现过程中,我们还可以结合数据检索,将marker进行空间查询过滤,减少数据量的返回,提高渲染的速度。

        当然,rbush这个组件不仅可以在我们这个场景中进行使用,可以在更多的空间查询中来使用更高效的算法,辅助前端进行数据的高效检索。更多好的使用方式,欢迎大家去探索。行文仓促,定有许多不足之处,还肯定各位朋友和专家在评论区留下批评意见,不生感激。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。