导语
最近项目上遇到一个专题树,遇到打开专题的时间轴后,之前打开的图层会被关闭的bug过程是这样的,先打开增城区规划导则地块
,然后再打开上面含有多个年份的历史图层,规划导则地块
,这个时候增城区规划导则地块
的图层会被关闭。

经过代码定位和按行注释,发现是这段代码针对图层的关闭和显示有问题:
// 处理默认年份的显示隐藏
var layer = this.defaultLayer;
if (visible) {
`layer.setVisibleLayers && layer.setVisibleLayers([layerobj.layerindex])`;
layer.setVisibility(true);
} else {
// 关闭
`layer.setVisibleLayers && layer.setVisibleLayers([]);`
layer.setVisibility(false);
}
为什么当前的 layer
会把其他的图层关闭了呢?下面先理解几个概念。
本文主要讲述以下几点内容:
- 什么是图层
- 图层是如何加载的
- 图层的应用:解决专题树的问题
什么是图层
图层是 ArcMap、ArcGlobe 和 ArcScene 等Arcgis 产品套件中地理数据集的显示机制。一个图层引用一个数据集,并指定如何利用符号和文本标注绘制该数据集。向地图添加图层时,要指定它的引用数据集并设定地图符号和标注属性。
包含一个地图控件的每个应用程序是通过一系列图层组装的。显示以特定的顺序显示在地图上,列在最底部的显示在地图的最上面显示,也就是先添加的显示在下面显示(原理类似于“栈”)

所有的图层都是从Layer类型继承而来的,可以参考下载的API中的对象模型图。
Layer
|–TiledMapServiceLayer
|----|–ArcGISTiledMapServiceLayer
|–DynamicLayer
|----|–DynamicMapServiceLayer
|----------|–ArcGISDynamicMapServiceLayer
|----------|–ArcGISImageServiceLayer
|----------|–GPResultImageLayer
|–GraphicsLayer
|----|–FeatureLayer
|–ElementLayer
而图层是怎么加载出来的呢,它是通过地图服务加载出来的。
图层的加载
什么是地图服务
地图服务是一种利用 ArcGIS 使地图可通过 web 进行访问的方法。我们首先在 ArcMap 中制作地图,然后将地图发布到 ArcGIS Server 站点上。当地图服务发布成功后,我们可以通过网址(xxxx/arcgis/rest/services)来查看地图服务所支持的操作,地图服务所包含的数据,以及我们还可以通过网址来测试地图服务的功能。
之后在Web 应用程序、ArcGIS for Desktop、ArcGIS Online 以及其他客户端应用程序中请求该地址使用此地图服务
下面说说常见的两种图层加载模式,实例化一个图层对象,需要传入图层的 url
。
切片服务
原理:切片服务是已经通过比例尺切好地图了,如通常的底图,一般是切片服务加载的,当你通过鼠标放大底图,它会根据当前的比例尺来加载已经切好的图片,加载的方式是通过
export
接口请求已经切好的图片。
由于切片服务已经切好了,所以无法通过类似setVisibleLayers
来控制它的图层显示,只能通过setVisiblity
控制整个图层的显示。导出图片时,export
会把整个当前比例的切片导出来。
一个发布出来的切片如下:


通过ArcGISTiledMapServiceLayer
新建一个切片类实例,然后加载到地图中
var layer = new ArcGISTiledMapServiceLayer(layerobj.url);
map.addLayer(layer);
控制切片图层的显示
// 设置图层显示/隐藏
layerVisibleRefreshByName: function(obj) {
if (obj == null) return;
var serviceid = obj.serviceid;
var layername = obj.layername;
var layervisible = obj.layervisible;
// 如果 serviceid 不存在时,运维赋值为 label 名称
if (serviceid == this.guid || serviceid == this.label) {
this.setVisibility(layervisible);
}
}
当然,如果切片服务在发布时,勾选了可以使用动态服务加载的,那么切片服务也可以通过ArcGISDynamicMapServiceLayer
来加载。
动态服务
原理:动态服务是即时生成的图片,而不是服务器上预存的图片。当用户向服务器请求地图服务时,服务器根据接收到参数调用底层服务,底层服务经过参数计算,实时生成像素点,这些像素点构成图片,返回到服务器,服务器再传递给客户端。
一个动态服务的信息如下:

通过ArcGISDynamicMapServiceLayer
新建一个动态类实例,然后加载到地图中
var layer = new ArcGISDynamicMapServiceLayer(layerobj.url);
map.addLayer(layer);
在我们执行setVisibleLayers
时,会通过 export
方式来把对应的子图层输出图片,然后加载到地图中。setVisibleLayers(-1)
关闭所有子图层。
控制动态图片的显示
/**
* 改变图层的可见性
* @param {Object} dlayer 服务图层
* @param {Number} layerid 子图层 id
* @param {Boolean} layervisible 可见性
*/
changeLayerVisible: function (dlayer, layerid, layervisible) {
if (dlayer == null) return;
if (layerid < 0) return;
var arrc = dlayer.visibleLayers;
arrc = this.dealWithLayerInfos(arrc, dlayer.layerInfos);
if (arrc == null || (arrc.length == 1 && arrc[0] == -1)) {
arrc = [];
}
if (layervisible) {
if (!this.checkLayerId(arrc, layerid)) {
arrc.push(layerid);
}
} else {
if (this.checkLayerId(arrc, layerid)) {
arrc = this.removeLayerId(arrc, layerid);
}
}
this.setVisibleLayers(arrc, true);
},
checkLayerId: function (arrc, layerid) {
if (arrc == null) return false;
for (var i = 0; i < arrc.length; i++) {
if (arrc[i] == layerid) {
return true;
}
}
return false;
},
回归场景
说到历史时间轴,首先要理解专题树里面的节点信息。
专题树
专题树由专题组成,每一个叶子节点都是一个专题,那么专题是什么呢?
专题:专题也就是一个图层服务,每一个专题layer
都是系统初始化时,通过动态服务或切片服务实例化后添加到地图中的,
每个专题图层,都有一个图层组 layers
,这个是该专题服务下的默认显示的子图层集合,所以打开或关闭专题时,如果子图层是通过动态服务加载的,也就是关闭对应的子图层。如果是切片服务的,则是关闭整个服务对象。


历史时间轴的实现
关于历史时间轴的逻辑是这样的

- 勾选某个图层,这个图层可能是一个服务图层 Layer 也可能是子图层。
- 遇到带有时间轴标识的,会打开时间轴面板,初始打开默认的年份。这些图层地址是通过读取配置得来。
{
"type": "规划导则地块",
"layers": [
{
"label": "2019",
"layertype": "dynamic",
"layername": "规划导则地块",
"layerindex": 4,
"serviceName": "控制性规划导则",
"serviceUid": "",
"defaultLayer": true,
"url": "xxxxx/arcgis/rest/services/%E6%8E%A7%E5%88%B6%E6%80%A7%E8%A7%84%E5%88%92%E5%AF%BC%E5%88%99/MapServer/4"
},
{
"label": "2018",
"layertype": "dynamic",
"layername": "规划导则地块",
"layerindex": 4,
"serviceName": "控制性规划导则",
"serviceUid": "",
"defaultLayer": false,
"url": "xxxxx/arcgis/rest/services/%E6%8E%A7%E5%88%B6%E6%80%A7%E8%A7%84%E5%88%92%E5%AF%BC%E5%88%992018/MapServer"
},
{
"label": "2017",
"layertype": "dynamic",
"layername": "规划导则地块",
"layerindex": 4,
"serviceName": "控制性规划导则2017",
"defaultLayer": false,
"url": "xxxxx/arcgis/rest/services/%E6%8E%A7%E5%88%B6%E6%80%A7%E8%A7%84%E5%88%92%E5%AF%BC%E5%88%992017/MapServer"
},
{
"label": "2016",
"layertype": "dynamic",
"layername": "规划导则地块2016",
"layerindex": 4,
"serviceName": "控制性规划导则2016",
"defaultLayer": false,
"url": "xxxxx/arcgis/rest/services/%E6%8E%A7%E5%88%B6%E6%80%A7%E8%A7%84%E5%88%92%E5%AF%BC%E5%88%992016/MapServer"
}
]
},
- 关键的切换代码
/**
* _changeLayerVisible() 改变图层的显示情况
* @param {Object} layerobj 图层信息
* @param {Boolean} visible 是否可见
*/
_changeLayerVisible: function(layerobj, visible) {
this.map = window._map; // 获取到地图
if (!layerobj) {
return;
}
if (layerobj.label != this.clashHisdata.label) {
// 隔离与默认年份相冲突的年份,把默认年份放在else中处理
var layerId =
this.defaultLayer.label + '_historydataservice' + layerobj.label; // 用于同时添加多个图层 多个图层会出现
topic.publish('history-layerIds', layerId); // 历史图层id传送至专题树,由专题书统一管理图层开关
this.historyLayerIds.push(layerId);
switch (layerobj.layertype) {
case 'tiled':
if (visible) {
var layer = this.map.getLayer(layerId);
if (layer) {
this.map.removeLayer(layer);
layer = new ArcGISTiledMapServiceLayer(layerobj.url);
this._currentLayer = layer;
layer.id = layerId;
this.map.addLayer(layer);
this.map.reorderLayer(layer, 1);
layer.setVisibility(true);
} else {
layer = new ArcGISTiledMapServiceLayer(layerobj.url);
this._currentLayer = layer;
layer.id = layerId;
this.map.addLayer(layer);
this.map.reorderLayer(layer, 1);
}
this._currentLayer.setOpacity(this._opacity);
} else {
var layer = this.map.getLayer(layerId);
if (layer) {
//this.map.removeLayer(layer);
layer.setVisibility(false);
}
}
break;
case 'dynamic':
if (visible) {
var layer = this.map.getLayer(layerId);
if (layer) {
if (layer.url == layerobj.url) {
layer.setVisibleLayers([layerobj.layerindex]);
this._currentLayer = layer;
layer.setVisibility(true);
} else {
this.map.removeLayer(layer);
layer = new ArcGISDynamicMapServiceLayer(layerobj.url);
this._currentLayer = layer;
layer.id = layerId;
this.map.addLayer(layer);
this.map.reorderLayer(layer, 1);
layer.setVisibleLayers([layerobj.layerindex]);
layer.setVisibility(true);
}
} else {
var dlayer = new ArcGISDynamicMapServiceLayer(layerobj.url);
this._currentLayer = dlayer;
dlayer.id = layerId;
this.map.addLayer(dlayer);
this.map.reorderLayer(dlayer, 1);
dlayer.setVisibleLayers([layerobj.layerindex]);
}
this._currentLayer.setOpacity(1);
setTimeout(
lang.hitch(this, function() {
this._currentLayer.setOpacity(this._opacity);
}),
200
);
} else {
var layer = this.map.getLayer(layerId);
if (layer) {
layer.setVisibleLayers([]);
layer.setVisibility(false);
}
}
break;
}
} else {
`// 处理默认年份的显示隐藏
var layer = this.defaultLayer;
if (visible) {
layer.setVisibleLayers && layer.setVisibleLayers([layerobj.layerindex])`;
layer.setVisibility(true);
} else {
// 关闭
layer.setVisibleLayers && layer.setVisibleLayers([]);
layer.setVisibility(false);
}
}
},
找出图层被关闭的原因
Layer: 一个图层服务包含了很多子图层
这个是历史面板初始化时的操作。
时间轴打开后,通过serviceid
去获取添加到地图中的当前 layer
对象。
从专题信息中,获取到当前专题里面的图层,默认显示的子图层。
再通过它的serviceUid
获取到加载到地图中的父图层(专题)。

图层关闭由 setVisibility(isVisible)
和 setVisibleLayers(ids, doNotRefresh?)
组合控制,由切片服务生成的图层只有 setVisibility
属性,动态服务生成的图层则由两者组合控制图层的显示。如图,setVisibility
控制整个图层的显示,而 setVisibleLayers
可以更加细粒度地控制图层里面的子图层。

再看看之前的代码实现,通过断点发现,默认图层 layer
的子图层包含了增城导则地块图层
,因此在打开默认图层的某个子图层时,这行代码layer.setVisibleLayers([layerobj.layerindex])
只赋值了当前子图层的索引id,导致把之前的子图层都关闭了。
// 处理默认年份的显示隐藏
var layer = this.defaultLayer;
if (visible) {
`layer.setVisibleLayers && layer.setVisibleLayers([layerobj.layerindex])`;
layer.setVisibility(true);
} else {
// 关闭
`layer.setVisibleLayers && layer.setVisibleLayers([]);`
layer.setVisibility(false);
}
这时候只需要添加对应的图层服务类型判断逻辑,缓存之前的就可以了。
// 处理默认年份的显示隐藏
var layer = this.defaultLayer;
```var visibleLayers = []; // 可见的图层
visibleLayers = visibleLayers.concat(layer.visibleLayers);```
if (visible) {
// 打开
var index = visibleLayers.indexOf(layerobj.layerindex);
if (index === -1 && layerobj.layertype !== 'tiled') {
visibleLayers.push(layerobj.layerindex); // 添加新的图层索引进去,否则传递 -1 会关闭所有图层
layer.setVisibleLayers && layer.setVisibleLayers(visibleLayers); // 注意区分tiled和dynamic
}
layer.setVisibility(true);
} else {
// 满足:
// 1. 需要解决时间轴后面的Layer 被默认的 Layer覆盖问题,
// 2. 但是不能把整个 Layer 关闭了,否则,会影响属于同一个图层服务实例下,其他子图层的显示。
// 3. 当前只能把后面的图层移动 z-index,但是又要满足时间轴的图层作为底图来使用。
`if (layerobj.layertype === 'tiled') {
// 判断是否为切片
layer.setVisibility(false);
} else {
// 动态图片服务的关闭
// 关闭
var index = visibleLayers.indexOf(layerobj.layerindex);
if (index > -1) {
visibleLayers.splice(index, 1);
}
layer.setVisibleLayers && layer.setVisibleLayers(visibleLayers);
}`
}
}
当然,上面的默认图层的显示/隐藏,可以直接把图层对象传递给专题树来统一处理开关,这样就不用写这些判断逻辑了。
(全文完)
总结
- 关键是理解图层的概念以及它们是怎么加载的,这样遇到控制图层的问题时就能快速定位解决了。
- 目前的
arcgis api
是经过优化的,只有地图范围改变了,才会去请求地图服务。所以在切换时间轴时,要注意改变它的范围,才能更好的定位错误。