目录
前言
一、场景重现
1、遇到问题的代码
2、问题排查
二、通过实验验证猜想
1、排查LayerGroup和FeatureGroup
2、排查Leaflet.canvaslabel.js
三、柳暗花明又一村
1、点聚类的办法
2、歪打正着
总结
前言
在上一篇博客中介绍了基于SpringBoot的全国风景区WebGIS按省展示实践。在这篇博客中,关于旅游景点我是使用Leaflet.canvaslabel.js进行静态标注的,其实在案例中要实现点击风景区点,然后自动弹出它的其它信息,比如景点名称,所在省市县信息,还有设立年份。最开始打算采用bindPopup的方案,在编写代码的过程当中,发现一个有趣的问题。在点击所在省份之后,再把景点信息列表在地图上渲染出来,然后针对每个点绑定popup。但实际的情况是,界面上的景点点击后,没有任何反应,奇怪的是有时候又可以点。当时想着,如果是代码的绑定得有问题,肯定是都出不来,不存在一份代码,两种效果的情况。
遇到了问题,便解决问题。在没有找到具体的问题前,首先检查代码的问题,然后检查相应的组件是否有什么缺陷,最后再梳理解决办法,大致的思路如此。本文在此背景下诞生,博文首先详细介绍了风景区展示时具体的问题,然后分析排查思路和方向,接着使用不同的方案验证思路,最后给出最终的解决方案。解决问题就是对症下药一样,找到了病根基本上就是药到病除,解决了问题的酸爽感一定让很多技术圈朋友深有体会吧。
一、场景重现
我想在讲述一些解决办法之前,还是把碰到的详细问题描述清楚。首先是不至于让看到博文的朋友感到很诧异,第二是对于解决方案的前提条件有一个具象的描述。能让我们知道为什么会这样?出现了什么情况?我们要解决的问题是什么。这里结合代码和实际效果来讲解,这样可能比较有针对性。
1、遇到问题的代码
这里将之前的第一版代码贴出来,肯定有朋友第一眼就能看到问题的所在,但当时折腾了我很久。也许旁观者清,也许是经验比较多,曾经遇到过,一下子就解决了。
/* 展示省份geojson范围*/
function showProvince(id){
var myStyle = {color:"white",weight:5,"opacity":1,fillOpacity: 0};
$.ajax({
type:"get",
url:prefix + "/geojson/" + id,
dataType:"json",
cache:false,
processData:false,
success:function(result){
if(undefined != provinceAreaLayer ){
provinceAreaLayer.removeFrom(mymap);//先移除
}
if(result.code == web_status.SUCCESS){
var geojson = JSON.parse(result.data);
provinceAreaLayer = L.geoJSON(geojson,{style:myStyle}).addTo(mymap);
//同时设置中心位置和级别,一般省份设置为7
mymap.setView(provinceAreaLayer.getBounds().getCenter(),7);
}
},
error:function(){
$.modal.alertWarning("获取空间信息失败");
});
}
/*风景区展示*/
function showScenicSpot(code){
$.ajax({
type:"get",
url:prefix + "/datalist/" + code,
dataType:"json",
success:function(result){
if(result.code == web_status.SUCCESS){
var strokeStyleSet = "#23168d";
var lat,lng,cityInfo;
for(var i=0;i<result.data.length;i++){
var dataInfo = result.data[i];
var geomObj = JSON.parse(dataInfo.geomJson);
if(i == 0){
lat = geomObj.coordinates[1];
lng = geomObj.coordinates[0];
continue;
}
var radiusSize = 5;
var content = "<strong>名称:</strong>"+dataInfo.name + "<br/><strong>级别:</strong>"+ dataInfo.level;
content += "<br/><strong>所属行政区划:</strong>"+ dataInfo.province + "/" + dataInfo.city + "/" + dataInfo.area;
content += "<br/><strong>评定时间:</strong>"+ dataInfo.evaluationTime ;
var latlng = new L.latLng(geomObj.coordinates[1], geomObj.coordinates[0]);
let marker = L.circleMarker(latlng, {
radius: radiusSize,
color: strokeStyleSet,
labelStyle: {
offsetX: 0, //横坐标偏移(像素)
offsetY: 30, //纵坐标偏移(像素)
text: dataInfo.name,
rotation: 0,
zIndex: radiusSize,
minZoom : 5,
strokeStyle: strokeStyleSet
}
}).addTo(showLayerGroup);
marker.bindPopup(content);
}
mymap.addLayer(showLayerGroup);
}
},
error:function(){
$.modal.alertWarning("获取信息失败");
}
});
}
不出意外的话还是出了意外,我们来计划在点击风景区时,通过bindPopup弹出经典的一些附属信息。但是无论点击哪个景点,似乎都没有符合我们的预期,效果如下:
遇到点击了没有效果的情况,第一个想到的原因是看一下网页的控制台,看是否有什么报错信息。根据这个思路,我们打开浏览器的控制台看一下:
很遗憾,控制台没有想要的信息,代码是没有问题的,可以正常运行的。那么问题到底出在哪里呢?
2、问题排查
在明确没有代码语法问题和报错的前提下,怎么解决这个问题呢?代码是严格按照我们的设计去执行的,但是为什么没有按照我们的预期效果来展示呢?当时,博主从下面两个方向进行思考。首先、排查相关组件的引用是否有问题,因为在本案例中,涉及到LayerGroup和FeatureGroup的使用,是否是因为涉及Group的增加和删除导致了标记对象的事件显示不征程。其次想到的是Leaflet.canvaslabel.js这个组件本身是不是有问题,在数量规模和事件绑定上是不是有什么不对的地方。受限于博主的经验,首先想到了这两个方面,因此排查工作也由此展开。
二、通过实验验证猜想
针对问题我们有了两个方面的猜想,要想验证猜想的成立,就必须通过代码实验,通过实验说明问题可以解决,这样才是有说服力的。本节针对上述猜想,阐述两个实验环节。
1、排查LayerGroup和FeatureGroup
怀疑LayerGroup和FeatureGroup有问题的根据是因为,在展示省份地理范围和风景区点位列表时,我们都会设计Group的操作,通过Group,可以实现图层级的隐藏和展示,可以实现某一专题的元素共同的可视化。但是在切换省份时,要想实现只展示当前省份和所属的风景区信息,首先要把之前的省份地理范围清空掉,同时也把之前省份的风景区列表清除掉。所以映入眼帘的第一个嫌疑人出现。添加元素和移除的方法如下:
function preview(code,id){
showLayerGroup.clearLayers();//图层要素清除
showProvince(id);
showScenicSpot(code);
}
要素添加到图层的代码如下:
let marker = L.circleMarker(/*省略*/).addTo(showLayerGroup);
mymap.addLayer(showLayerGroup);
通过在Leaflet的官方网站中查看API文档,查看相关的示例代码,并没有发现什么问题。甚至在当时调试的情况下,已经不采用Group的方案,直接把marker添加到Map当中,问题依然没有解决,至此,排除是LayerGroup或者FeatureGroup引起的问题。
2、排查Leaflet.canvaslabel.js
在把LayerGroup和FeatureGroup排除了嫌疑之后,把目光转向了Leaflet.canvaslabel.js。思考是不是因为Leaflet.canvaslabel.js这个组件自身有什么缺陷(在大规模数据量的时候),或者说我们在API的调用有没有什么问题。带着这个疑问,我们来做两个实验。第一个实验是针对性的看API,是否有什么方法或者属性配置没有正确配置。第二个实验是Leaflet.canvaslabel.js配合大规模数据点,比如5000个点的情况下,能否正常bindPopup。下面逐步来进行验证:
options: {
defaultLabelStyle: {
offsetX: 0, //横坐标偏移(像素)
offsetY: 0, //纵坐标偏移(像素)
scale: 1, //放大比例
rotation: 0, //旋转角度(弧度),可能会导致碰撞检测不准确
text: null, //标注文本内容
minZoom: null, //最小显示级别
maxZoom: null, //最大显示级别
collisionFlg: true, //碰撞检测
center: null, //标注位置,默认为null,会自动计算几何中心
zIndex: 0, //排序
defaultHeight: 20, //文本高度,无法自动计算,所以直接传参手动调整
//文本样式,具体值请参考[canvas](https://www.runoob.com/tags/ref-canvas.html)
font: "10px sans-serif",
fillStyle: "rgba(0,0,0,1)",
lineCap: "round",
lineDash: [],
lineDashOffset: 0,
lineJoin: "round",
strokeStyle: "rgba(0,0,0,1)",
textAlign: "center",
textBaseline: "middle",
lineWidth: 1,
},
},
这是组件的属性配置,基本上与标注的可视化没有关系。再来看事件和执行器:
/**
* 执行侦听器
*/
_executeListeners: function (event) {
if (!this._textBounds) return;
var me = this;
var ret = this.getTextByEvent(event);
if (ret && ret.length > 0) {
me._map._container.style.cursor = "pointer";
if (event.type === "click") {
me._onClickListeners.forEach(function (listener) {
listener(event, ret);
});
}
if (event.type === "mousemove") {
me._onHoverListeners.forEach(function (listener) {
listener(event, ret);
});
}
if (event.type === "mousedown") {
me._onMouseDownListeners.forEach(function (listener) {
listener(event, ret);
});
}
if (event.type === "mouseup") {
me._onMouseUpListeners.forEach(function (listener) {
listener(event, ret);
});
}
} else {
me._map._container.style.cursor = "";
}
},
/**
* 添加click侦听器
*/
addOnClickListener: function (listener) {
this._onClickListeners.push(listener);
},
通过代码发现,组件也并有修改逻辑,只是接收点击事件,然后添加到执行器中。基本排除了组件自身的问题。那么剩下最后一个猜想,大规模数量下组件的事件是不是失效了。因此我们进行大规模数据量的加载试验。
在之前的博文中在Leaflet中点对象使用SVG和Canvas两种模式的对比,对组件的加载能力有较详细的描述,在使用5000甚至10000个点的规模下,展示非常迅速,同时点击对象,也能实现Popup的快速提示。
var count = 30000;
for (let i = 0; i < count; i++) {
let latlng = L.latLng(23.95 + Math.random() * 10, 112.40034 + Math.random() * 15);
var content = "<strong>名称:</strong>城市"+i + "<br/><strong>级别:</strong>"+ i;
content += "<br/><strong>所属行政区划:</strong>"+ i + "/" ;
content += "<br/><strong>评定时间:</strong>"+ i ;
var title = "重要城市" + Math.random();
let c = L.circleMarker(latlng, {
radius: 5,
labelStyle: {
text: title,
rotation: 0,
zIndex: 1,
strokeStyle: "red",
}
}).addTo(showGroup);
c.bindPopup(content);
}
map.addLayer(showGroup);
经过两个方向,几个步骤的排查,依然没有找到具体的原因,也没有找到解决方案。有点懊恼,于是想着换个思路。对组件的多点展示能力有初步怀疑,但没有找到原因。
三、柳暗花明又一村
山重水复疑无路,柳暗花明又一村。这句话用形容当时解决问题的情况非常贴切,在两个方向都明确没有定位最终问题的情况下。我决定用之前遇到的一个点聚类的思路。遇到问题不要放弃,再没有方向的时候,发散思路,解决了问题,经验值就此提升。因此,遇到bug,解决bug才是好的态度。这里分两个步骤描述问题的解决办法。
1、点聚类的办法
带着之前没有找到的根本问题,一直将注意力集中在风景区的点数量上。怀疑是不是点多了,导致了事件响应的问题。所以考虑减少点位信息,在Leaflet当中,可以通过点聚类的办法介绍视野内的对象,通过减少点的渲染,保障Popup的正常可视化。
大家还记得点聚类的组件用什么吗?之前也有文章进行深入讲解,就是用leaflet.markercluster-src。代码就不详细贴了,创建聚合图层,将点添加到图层中。
//创建聚合图层并设置最大聚合半径为 26像素
var markersGroup = L.markerClusterGroup({maxClusterRadius: 26});
通过点聚合,发现确实能解决几个省的展示问题,当时以为找到了解决办法很欣喜,但是在浙江时,发现有风景区不能很好的展示出来。开始对数据的加载机制有个第一次怀疑,这算个歪打正着。本来没有怀疑数据的加载机制。
2、歪打正着
在对数据的加载机制有了怀疑之后,便迅速去看代码。在展示省份地理范围的代码中,我们使用ajax的方式去后台请求数据,ajax请求头如下:
type:"get",
url:prefix + "/geojson/" + id,
data:{},
dataType:"json"
众所周知,在ajax当中,其默认的数据加载方式异步的,也就是async=true。其实到了这里,才是真正的问题根源。就是由于数据请求的异步方式导致了组件的事件响应问题。再来看一下代码:
function preview(code,id){
showLayerGroup.clearLayers();//图层要素清除
showProvince(id);
showScenicSpot(code);
}
在上述代码中,首先异步加载省份的geojson地理范围,再异步加载风景区信息。也就是异步的问题导致的,虽然方法看起来像是先加载地理范围,实际上设置了异步,可能地理范围还没有加载完,风景区信息已经再加载了。也正是这个原因导致了事件不响应。按照这个思路,我们将ajax方法设置为同步的方式,即async=false。
再来看具体的效果,以浙江为例:
杭州西湖风景区
杭州市千岛湖风景区
浙江普陀山风景区
至此,问题得以全部解决,Leaflet.canvaslabel在Ajax异步请求时bindPopup无效的解决办法。导致popup无效的问题是ajax的异步机制导致的。当然在实际开发当中,您可以使用一个解决返回所有的数据,把空间范围和数据列表同时返回就不存在上述问题了。解决办法很多,找到问题根本很重要。遇到问题,慢慢分析,大胆设想,小心验证,一定会有收获,不放弃。
总结
以上就是本文的主要内容,本文分享了作者在排查GIS问题当中的一些思路和方法。在没有找到具体的问题前,首先检查代码的问题,然后检查相应的组件是否有什么缺陷,最后再梳理解决办法,大致的思路如此。本文在此背景下诞生,博文首先详细介绍了风景区展示时具体的问题,然后分析排查思路和方向,接着使用不同的方案验证思路,最后给出最终的解决方案。行文仓促,定有不足之处,案例的排查方法和经验都不足,欢迎有经验的大佬基于批评指正,万分感谢。