Cesium深入浅出之图层管理器
引子
早就想做这篇内容了,毕竟做为一个GIS平台,没有图层管理器多不方便啊。然而在Cesium中图层这个概念都很模糊,虽然可以加载很多类型的数据,但是每种数据规格都不一样,导致加载进来之后并不能进行统一且有效的管理。熟悉ArcGIS的朋友一定知道,在ArcGIS中几乎所有的数据都是使用图层来承载的,因此想要管理图层数据轻而易举。而在Cesium中,除了影像数据能算的上图层以外,其他的数据压根都和图层扯不上关系,这点从其命名(imageryLayers)上就可以看得出来。但是这并不代表它不能以图层的方式进行管理,我们只要找到每种数据对应的不同载体,再进行分类处理,就可以了。
预期效果
说实话这个效果只能算是差强人意了,但暂时也就只能做成这样了,就当是抛砖引玉吧。
实现原理
关键是要先找到不同类型数据的载体,我总结了下在Cesium中大概分为四类数据:图元数据(Primitive)、实体数据(Entity)、影像数据(Imagery)、地形数据(Terrain),因为这四类数据的形式是截然不同的,它们分别处于四个不同的数据载体中,所以我们在图层管理器中也是划分了对应的四个分组,接下来就是针对不同的数据载体进行不同的操作了。其次是图层管理器的表现形式,本篇中采用Cesium的Mixin规范进行封装的,如果有不熟悉的小伙伴请看我前面一篇文章,是关于插件是如何封装的。
原理就是这么简单几句话,不过在进入具体实现环节之前,我们还是先来简单讲讲这四种类型数据的相关知识点吧。直接上代码来写文章是很快,但是真对不住“深入浅出”这个词啊,所以还是不能偷懒,希望小伙伴们也不要偷懒,直接把代码copy过去就不管不问了,要做到知其然和知其所以然。
Primitive
在这个系列文章的第一篇中我讲过了Primitive和Entity的区别,简单说来就是Primitive更接近底层且效率高,Entity更丰富更强大但效率低,所以我们也是推荐大家加载数据尽量使用Primitive的方式。其实大部分Entity能做到的功能Primitive也能做的到,只是稍微麻烦一点,但为了性能考虑那点小小的麻烦可以忽略不计了。当然了,Entity也不是一无是处的,比如CallbackProperty这个东东,用过的小伙伴都说好,用它来做个动画效果简直易如反掌,所以我们在日常开发中可以将这二者有机的结合,使用Entity进行Feedback,而使用Primitive做最终展现。不过这只是我个人的见解罢了,也许大牛直接Primitive搞定一切也说不定呢呢。其实底层的东西都有类似的特性,就是越深入越强大,我后面还想出一篇Primitive的专题文章,深入挖掘一下Primitive的潜力。
先来看下Primitive的定义:
构造函数:new Cesium.Primitive(options)
参数options:
名称 | 类型 | 默认值 | 描述 |
---|---|---|---|
geometryInstances |
Array.GeometryInstance> | GeometryInstance | 用于渲染的一组或一个几何图形实例。 | |
appearance |
Appearance | 用于渲染图元的外观。 | |
depthFailAppearance |
Appearance | 当图元未通过深度测试时,用于对其进行着色的外观。 | |
show |
Boolean | true |
是否显示图元。 |
modelMatrix |
Matrix4 | Matrix4.IDENTITY |
将图元(所有几何体实例)从模型坐标转换为世界坐标的4x4变换矩阵。 |
vertexCacheOptimize |
Boolean | false |
如果为true,几何体顶点将针对顶点前和顶点后着色器缓存进行优化。 |
interleave |
Boolean | false |
如果为true,几何体顶点属性将交错,以稍微提高渲染性能,但会增加加载时间。 |
compressVertices |
Boolean | true |
如果为true,几何体顶点将被压缩,以节省内存。 |
releaseGeometryInstances |
Boolean | true |
如果为true,则图元不保留对输入几何实例的引用,以节省内存。 |
allowPicking |
Boolean | true |
如果为true,则每个几何体实例将只能使用Scene#pick 进行拾取;如果为false,则可节省GPU内存。 |
cull |
Boolean | true |
如果为true,则渲染器视锥和地平线基于图元的外包围盒剔除其commands;如果要手动剔除图元,将值设置为false可以获得较小的性能增益。 |
asynchronous |
Boolean | true |
确定是选择异步创建图元还是在准备就绪前一直阻塞。 |
debugShowBoundingVolume |
Boolean | false |
仅用于调试。是否显示图元commands的外包围盒。 |
shadows |
ShadowMode | ShadowMode.DISABLED |
确定图元是从光源投射阴影还是从光源接收阴影。 |
上面的表格十分清晰地为我们展现了Primitive的详细定义,可以说看完表格基本就会用了呢,所以API很有用吧。这里插点题外话,API之所以重要,是因为API是所有二次开发的根本,在开发之前最先要做的就是看API,然后才是去百度、看文章、开源代码,也就是说我们应该面向API开发,而不是面向百度开发,在开发之前很有必要梳理一下API,尤其是涉及数据类型的重点API,正所谓磨刀不误砍柴工。通过上面的API,我们对Primitive的构造有了基本的了解,其中的show属性在后面讲到的图层管理器实现中会用到,它是控制数据的显示和隐藏的,其它属性我们在这里不做过多的延申说明了。
不知道大家发现没有,当你使用viewer.scene.primitives去遍历的时候,里面会出现很多奇怪的东东,比如Cesium3DTileset、Model等等,对象结构也和上述API中列的不一样,这是为什么呢?原来啊,PrimitiveCollection中不仅仅可以存储Primitive数据,还可以存储其他非严格意义的Primitive数据。也就是说,在Cesium中,Primitive是比较宽泛的概念,只要具备一定的规范都可以算做是Primitive,而PrimitiveCollection只是一个容器而已。以Model为例,大家可能都加载过GLTF格式的模型数据,你的代码可能是这样的:
1 var model = scene.primitives.add(Cesium.Model.fromGltf({ 2 url : './duck/duck.gltf' 3 }));
也可能是这样的:
1 var model = viewer.entities.add({ 2 model: { 3 uri: './duck/duck.gltf' 4 } 5 });
那么它们有什么区别呢?最大的区别就是数据载体不一样,一个是加载到PrimitiveCollection中,一个是加载到EntityCollection中。那么我们很容易理解了,同样的Model,第一种加载方式数据类型是Primitive,第二种加载方式数据类型就是Entity。那么我们可以延申一下,是不是可以自定义一种Primitive数据然后加载到PrimitiveCollection中呢?这个问题的答案可以在我前面写的关于视频投影的文章中找到答案,我们视频投影类封装好之后加载到PrimitiveCollection中,发现它可以很好的运转。当然了我们必须Primitive特定的规范,比如update()等。
Entity
Cesium对Entity的结构组织不像Primitive那样松散,总体来讲还是比较清晰的。
构造函数:new Cesium.Entity(options)
参数options(Cesium.Entity.ConstructorOptions):
Entity不愧是比Primitive更为高级的数据格式,功能更强大且封装的也更规范。从API中我们可以清晰地看到Entity所支持的所有数据类型,都是以属性的形式单独存放于options参数中。看描述我们就知道了每个属性的含义,这里就不赘述了。我们还是只关心show属性,也是控制数据显示和隐藏的。还有name属性,也就是数据名称,在我们这里可以理解为图层名称,要注意,这个属性Primitive是没有的,但不代表你不可以给它添加这个属性,大家都知道Javascript的开放性,我们可以自由地为对象扩展属性,毕竟没有图层名称还是很难管理的,所以建议大家添加Primitive的时候为它赋个名称。
ImageryLayer
这个就厉害了,看名字就知道人家是真真正正的图层数据。
构造函数:new Cesium.ImageryLayer(imageryProvider, options)
参数imageryProvider:要显示在椭球体表面的影像提供器,如ArcGisMapServerImageryProvider、BingMapsImageryProvider、GoogleEarthEnterpriseImageryProvider等。
参数options:
名称 | 类型 | 默认值 | 描述 |
---|---|---|---|
rectangle |
Rectangle | imageryProvider.rectangle |
图层的矩形范围框。这个矩形框可以限制影像提供器的可见部分。 |
alpha |
Number | function | 1.0 |
图层的alpha混合值,从0.0到1.0。可以是一个简单的数字,也可以是signaturefunction(frameState、layer、x、y、level)函数。函数将传递当前帧的状态、该图层以及需要alpha的影像分块的x、y和level坐标,并返回用于瓦片分块的alpha值。 |
nightAlpha |
Number | function | 1.0 |
图层在地球夜间的alpha混合值,从0.0到1.0。可以是一个简单的数字,也可以是一个signaturefunction(frameState、layer、x、y、level)函数。仅在enableLighting为true时生效。 |
dayAlpha |
Number | function | 1.0 |
图层在地球白天一侧的alpha混合值,从0.0到1.0。可以是一个简单的数字,也可以是一个signaturefunction(frameState、layer、x、y、level)函数。仅在enableLighting为true时生效。 |
brightness |
Number | function | 1.0 |
图层的亮度。当值为1.0时,使用未修改的图像颜色。当值小于1.0时,图像会变得更暗,而大于1.0会图像会变得更亮。可以是一个简单的数字,也可以是一个signaturefunction(frameState、layer、x、y、level)函数。这个函数是为每帧和每个瓦片执行的,所以它必须是快速执行的。 |
contrast |
Number | function | 1.0 |
图层的对比度。当值为1.0时,使用未修改的图像颜色。当值小于1.0会降低对比度,大于1.0会增加对比度。可以是一个简单的数字,也可以是一个signaturefunction(frameState、layer、x、y、level)函数。这个函数是为每帧和每个瓦片执行的,所以它必须是快速执行的。 |
hue |
Number | function | 0.0 |
图层的色调。当值为1.0时,使用未修改的图像颜色。可以是一个简单的数字,也可以是一个signaturefunction(frameState、layer、x、y、level)函数。这个函数是为每帧和每个瓦片执行的,所以它必须是快速执行的。 |
saturation |
Number | function | 1.0 |
图层的饱和度。当值为1.0时,使用未修改的图像颜色。小于1.0会降低饱和度,大于1.0会增加饱和度。当值小于1.0会降低对比度,大于1.0会增加对比度。可以是一个简单的数字,也可以是一个signaturefunction(frameState、layer、x、y、level)函数。这个函数是为每帧和每个瓦片执行的,所以它必须是快速执行的。 |
gamma |
Number | function | 1.0 |
图层的伽马校正值。当值为1.0时,使用未修改的图像颜色。可以是一个简单的数字,也可以是一个signaturefunction(frameState、layer、x、y、level)函数。这个函数是为每帧和每个瓦片执行的,所以它必须是快速执行的。 |
splitDirection |
ImagerySplitDirection |function | ImagerySplitDirection.NONE |
影像分割方向。 |
minificationFilter |
TextureMinificationFilter | TextureMinificationFilter.LINEAR |
纹理缩小过滤器。可能的值为TextureMinificationFilter.LINEAR 或TextureMinificationFilter.NEAREST . |
magnificationFilter |
TextureMagnificationFilter | TextureMagnificationFilter.LINEAR |
纹理放大过滤器。可能的值为TextureMinificationFilter.LINEAR 或TextureMinificationFilter.NEAREST . |
show |
Boolean | true |
是否显示该图层。 |
maximumAnisotropy |
Number | maximum supported |
用于纹理过滤的最大各向异性级别。如果未指定此参数,则将使用WebGL堆栈支持的最大各向异性。设置较大一点的值可以使影像在水平视图中看起来更好。 |
minimumTerrainLevel |
Number | 显示图层的最小地形细节级别,如果未定义则所有级别显示它。零级是最不详细的级别。 | |
maximumTerrainLevel |
Number | 显示图层的最大地形细节级别,如果未定义则所有级别显示它。零级是最不详细的级别。 | |
cutoutRectangle |
Rectangle | 制图矩形,用于剪切影像图层。 | |
colorToAlpha |
Color | 用于alpha的颜色。 | |
colorToAlphaThreshold |
Number | 0.004 |
color-to-alpha的阈值。 |
累!这部分API的翻译把我头疼死了,非常拗口。上面说了,ImageryLayer是真正的图层数据,看了API我们就知道了,里面有各种参数可供我们调节,如alpha、brightness、contrast、hue、saturation、gamma等,我们可以在图层管理器中做除很多滑块来调节,这个功能在沙盒中也有,不过本篇中仅涉及到了最常用alpha值的调节,也就是透明度,其它的你们可以自行扩展。
Terrain
说到地形数据,在Cesium中它算是既简单又复杂的数据了。说它简单是因为结构简单、使用方法简单,而且Cesium同一时间仅允许一个地形数据有效。说它复杂是因为它根本就不像图层数据,一些基本的操作都很难实现,比如地形的隐藏和显示,当然也还是有办法的,只不过要曲线救国,下面具体实现的时候会讲到。下面看一下地形加载方法:
1 viewer.terrainProvider = new Cesium.CesiumTerrainProvider({ 2 url: IonResource.fromAssetId(3956), 3 requestWaterMask: true 4 });
现在我们知道为什么只能加载一个地形数据了,它是viewer的属性直接赋值的,而不是像其它数据那样加载到容器中。我想Cesium之所以这么设计可能是因为地形数据不好叠加吧,不过如果我们有多个地形数据,而且每个数据都是分布在不同的地方,要想把它们同时加载进来就没办法做到了,不得不说好多时候我们还是有这个需求的,后续我或许会做些这方面的研究吧。
具体实现
前面就说过了,我们是按Cesium插件规范来实现图层管理器,照例我会全部代码奉上,以便于大家学习,如果有公共引用的代码这里没列出来,请到github上去获取。
文件结构
▼📂src
▼📂widgets
▼📂LayerControl
LayerControl.css
LayerControl.html
LayerControl.js
LayerControlViewModel.js
viewerLayerControlMixin.js
viewerLayerControlMixin.js
1 import defined from "cesium/Source/Core/defined.js"; 2 import DeveloperError from "cesium/Source/Core/DeveloperError.js"; 3 import LayerControl from "./LayerControl.js"; 4 import "./LayerControl.css" 5 6 /** 7 * A mixin which adds the LayerControl widget to the Viewer widget. 8 * Rather than being called directly, this function is normally passed as 9 * a parameter to {@link Viewer#extend}, as shown in the example below. 10 * 11 * @function 12 * @param {Viewer} viewer The viewer instance. 13 * @param {Object} [options={}] The options. 14 * @exception {DeveloperError} viewer is required. 15 * @demo {@link http://helsing.wang:8888/simple-cesium | LayerControl Demo} 16 * @example 17 * var viewer = new Cesium.Viewer('cesiumContainer'); 18 * viewer.extend(viewerLayerControlMixin); 19 */ 20 function viewerLayerControlMixin(viewer, options = {}) { 21 if (!defined(viewer)) { 22 throw new DeveloperError("viewer is required."); 23 } 24 25 const container = document.createElement("div"); 26 container.className = "sc-widget-container"; 27 const parent = viewer.scWidgetsContainer || viewer.container; 28 parent.appendChild(container); 29 const widget = new LayerControl( 30 viewer, {container: container} 31 ); 32 33 // Remove the layerControl property from viewer. 34 widget.addOnDestroyListener((function (viewer) { 35 return function () { 36 defined(container) && container.parentNode.removeChild(container); 37 delete viewer.scLayerControl; 38 } 39 })(viewer)) 40 41 // Add the layerControl property to viewer. 42 Object.defineProperties(viewer, { 43 scLayerControl: { 44 get: function () { 45 return widget; 46 }, 47 configurable: true 48 }, 49 }); 50 } 51 52 export default viewerLayerControlMixin;
这个没啥好说的,都是插件规范,有不理解的可以参考上一篇关于插件封装的文章。
LayerControl.js
1 import defined from "cesium/Source/Core/defined.js"; 2 import DeveloperError from "cesium/Source/Core/DeveloperError.js"; 3 import destroyObject from "cesium/Source/Core/destroyObject.js"; 4 import knockout from "cesium/Source/ThirdParty/knockout.js"; 5 import {bindEvent,getElement,insertHtml} from "../../common/util.js"; 6 import LayerControlViewModel from "./LayerControlViewModel.js"; 7 import LayerControlHtml from "./LayerControl.html"; 8 9 class LayerControl { 10 11 /** 12 * Gets the parent container. 13 * @memberOf LayerControl.prototype 14 * @type {Element} 15 */ 16 get container() { 17 return this._container; 18 } 19 /** 20 * Gets the view model. 21 * @memberOf LayerControl.prototype 22 * @type {LayerControlViewModel} 23 */ 24 get viewModel() { 25 return this._viewModel; 26 } 27 28 constructor(viewer, options={}) { 29 this._element = undefined; 30 this._container= undefined; 31 this._viewModel= undefined; 32 this._onDestroyListeners= []; 33 34 if (!defined(viewer)) { 35 throw new DeveloperError("viewer is required."); 36 } 37 if (!defined(options)) { 38 throw new DeveloperError("container is required."); 39 } 40 41 const that = this; 42 let container = options.container; 43 typeof options === "string" && (container = options); 44 container = getElement(container); 45 const element = document.createElement("div"); 46 element.className = "sc-widget sc-widget-layerControl"; 47 insertHtml(element, { 48 content: LayerControlHtml, delay:1000, callback: () => { 49 bindEvent(".sc-widget-layerControl .sc-widget-bar-close", "click", function () { 50 that.destroy(); 51 }) 52 bindEvent(".sc-widget-layerControl .sc-widget-updatePrimitiveLayers", "click", function () { 53 that._viewModel._updatePrimitiveLayers(); 54 }) 55 bindEvent(".sc-widget-layerControl .sc-widget-updateEntityLayers", "click", function () { 56 that._viewModel._updateEntityLayers(); 57 }) 58 bindEvent(".sc-widget-layerControl .sc-widget-updateImageryLayers", "click", function () { 59 that._viewModel._updateImageryLayers(); 60 }) 61 bindEvent(".sc-widget-layerControl .sc-widget-updateTerrainLayers", "click", function () { 62 that._viewModel._updateTerrainLayers(); 63 }) 64 } 65 }); 66 container.appendChild(element); 67 const viewModel = new LayerControlViewModel(viewer, element); 68 69 this._viewModel = viewModel; 70 this._element = element; 71 this._container = container; 72 73 // 绑定viewModel和element 74 knockout.applyBindings(viewModel, element); 75 } 76 77 /** 78 * @returns {Boolean} true if the object has been destroyed, false otherwise. 79 */ 80 isDestroyed () { 81 return false; 82 } 83 84 /** 85 * Destroys the widget. Should be called if permanently. 86 * removing the widget from layout. 87 */ 88 destroy () { 89 if (defined(this._element)) { 90 knockout.cleanNode(this._element); 91 defined(this._container) && this._container.removeChild(this._element); 92 } 93 delete this._element; 94 delete this._container; 95 96 defined(this._viewModel) && this._viewModel.destroy(); 97 delete this._viewModel; 98 99 for (let i = 0; i < this._onDestroyListeners.length; i++) { 100 this._onDestroyListeners[i](); 101 } 102 103 return destroyObject(this); 104 } 105 106 addOnDestroyListener(callback) { 107 if (typeof callback === 'function') { 108 this._onDestroyListeners.push(callback) 109 } 110 } 111 } 112 113 export default LayerControl;
这个也基本是规范,没啥好说的,就注意一下插入HTML后绑定刷新按钮的单击事件就行了。
LayerControlViewModel.js
1 import defined from "cesium/Source/Core/defined.js"; 2 import defaultValue from "cesium/Source/Core/defaultValue.js"; 3 import destroyObject from "cesium/Source/Core/destroyObject.js"; 4 import DeveloperError from "cesium/Source/Core/DeveloperError.js"; 5 import EventHelper from "cesium/Source/Core/EventHelper.js"; 6 import Model from "cesium/Source/Scene/Model.js"; 7 import PrimitiveCollection from "cesium/Source/Scene/PrimitiveCollection.js"; 8 import ScreenSpaceEventHandler from "cesium/Source/Core/ScreenSpaceEventHandler.js"; 9 import CesiumTerrainProvider from "cesium/Source/Core/CesiumTerrainProvider.js"; 10 import EllipsoidTerrainProvider from "cesium/Source/Core/EllipsoidTerrainProvider.js"; 11 import IonResource from "cesium/Source/Core/IonResource.js"; 12 import knockout from "cesium/Source/ThirdParty/knockout.js"; 13 14 class LayerControlViewModel { 15 constructor(viewer) { 16 if (!defined(viewer)) { 17 throw new DeveloperError("viewer is required"); 18 } 19 20 const that = this; 21 const scene = viewer.scene; 22 const canvas = scene.canvas; 23 const eventHandler = new ScreenSpaceEventHandler(canvas); 24 25 this._viewer = viewer; 26 this._eventHandler = eventHandler; 27 this._removePostRenderEvent = scene.postRender.addEventListener(function () { 28 that._update(); 29 }); 30 this._subscribes = []; 31 this.primitiveLayers = []; 32 this.entityLayers = []; 33 this.imageryLayers = []; 34 this.terrainLayers = []; 35 36 37 Object.assign(this, { 38 "viewerShadows": defaultValue(viewer.shadows, false), 39 }) 40 knockout.track(this); 41 const props = [ 42 ["viewerShadows", viewer, "shadows"] 43 ]; 44 props.forEach(value => this._subscribe(value[0], value[1], value[2])); 45 46 const helper = new EventHelper(); 47 // 底图加载完成后的事件 48 helper.add(viewer.scene.globe.tileLoadProgressEvent, function (event) { 49 if (event === 0) { 50 that._updatePrimitiveLayers(); 51 that._updateEntityLayers(); 52 that._updateImageryLayers(); 53 that._updateTerrainLayers(); 54 } 55 }); 56 } 57 58 destroy() { 59 this._eventHandler.destroy(); 60 this._viewer.scene.postRender.removeEventListener(this._removePostRenderEvent); 61 for (let i = this._subscribes.length - 1; i >= 0; i--) { 62 this._subscribes[i].dispose(); 63 this._subscribes.pop(); 64 } 65 return destroyObject(this); 66 } 67 68 _update() { 69 70 } 71 72 _subscribe(name, obj, prop) { 73 const that = this; 74 const result = knockout 75 .getObservable(that, name) 76 .subscribe(() => { 77 obj[prop] = that[name]; 78 that._viewer.scene.requestRender(); 79 }); 80 this._subscribes.push(result); 81 } 82 83 _updatePrimitiveLayers() { 84 const layers = this._viewer.scene.primitives; 85 const count = layers.length; 86 this.primitiveLayers.splice(0, this.primitiveLayers.length); 87 for (let i = count - 1; i >= 0; --i) { 88 const layer = layers.get(i); 89 if (!layer.name) { 90 if (layer.isCesium3DTileset) { 91 layer.url && (layer.name = layer.url.substring(0, layer.url.lastIndexOf("/")) 92 .replace(/^(.*[\/\\])?(.*)*$/, '$2')); 93 } else if (layer instanceof Model) { 94 layer._resource && (layer.name = layer._resource.url.replace(/^(.*[\/\\])?(.*)*(\.[^.?]*.*)$/, '$2')); 95 } else if (layer instanceof PrimitiveCollection) { 96 layer.name = `PrimitiveCollection_${layer._guid}`; 97 } 98 } 99 !layer.name && (layer.name = "[未命名]"); 100 this.primitiveLayers.push(layer); 101 knockout.track(layer, ["show", "name"]); 102 } 103 } 104 105 _updateEntityLayers() { 106 const layers = this._viewer.entities.values; 107 const count = layers.length; 108 this.entityLayers.splice(0, this.entityLayers.length); 109 for (let i = count - 1; i >= 0; --i) { 110 const layer = layers[i]; 111 !layer.name && (layer.name = "[未命名]"); 112 layer.name = layer.name.replace(/^(.*[\/\\])?(.*)*(\.[^.?]*.*)$/, '$2') 113 this.entityLayers.push(layer); 114 knockout.track(layer, ["show", "name"]); 115 } 116 } 117 118 _updateImageryLayers() { 119 const layers = this._viewer.imageryLayers; 120 const count = layers.length; 121 this.imageryLayers.splice(0, this.imageryLayers.length); 122 for (let i = count - 1; i >= 0; --i) { 123 const layer = layers.get(i); 124 if (!layer.name) { 125 layer.name = layer.imageryProvider._resource.url; 126 } 127 !layer.name && (layer.name = "[未命名]"); 128 this.imageryLayers.push(layer); 129 knockout.track(layer, ["alpha", "show", "name"]); 130 } 131 } 132 133 _updateTerrainLayers() { 134 const that = this; 135 this.terrainLayers.splice(0, this.terrainLayers.length); 136 const layer = this._viewer.terrainProvider; 137 138 const realLayers = that._viewer.terrainProvider._layers; 139 const realShow = !!(realLayers && realLayers.length > 0); 140 if (!layer.name && realShow) { 141 layer.name = realLayers[0].resource._url + realLayers[0].tileUrlTemplates; 142 } 143 !layer.name && (layer.name = "[默认地形]"); 144 // 定义show属性 145 !defined(layer.show) && Object.defineProperties(layer, { 146 show: { 147 get: function () { 148 return realShow; 149 }, 150 configurable: true 151 }, 152 }); 153 154 if (realShow !== layer.show) { 155 let terrainProvider; 156 if (!layer.show) { 157 // add a simple terain so no terrain shall be preseneted 158 terrainProvider = new EllipsoidTerrainProvider(); 159 } else { 160 // enable the terain 161 terrainProvider = new CesiumTerrainProvider({ 162 url: IonResource.fromAssetId(3956), 163 requestWaterMask: true 164 }); 165 } 166 that._viewer.terrainProvider = terrainProvider; 167 } 168 169 this.terrainLayers.push(layer); 170 knockout.track(layer, ["alpha", "show", "name"]); 171 172 } 173 } 174 175 export default LayerControlViewModel;
这部分封装算是整个插件中的核心部分,其中大部分还是关于knockout封装的代码,也就是上一篇中的通用内容,这里只讲一下不同的地方吧。先要定义四种图层的集合变量,然后在viewer.scene.globe.tileLoadProgressEvent这个事件中添加图层更新代码,图层更新代码分别对应四种类型的数据封装了四个函数,在更新函数中实现了图层数据的获取以及knockout的响应追踪,其实就是双向绑定图层的show、name等属性,以达到数据和界面状态同步。这里再着重讲一下地形数据的更新,因为地形数据没有show这个属性,所以我们需要自行实现。其实核心代码也只有一句:terrainProvider = new EllipsoidTerrainProvider(),它可以清除当前的地形,做下简单的封装我们就可以实现默认地形和清除地形的切换了。这里我只是做了最简单的实现,如果要加载自定的地形数据的话就不适用了,还需要你们自行改造一下。
LayerControl.html
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>LayerControl</title> 6 </head> 7 <body> 8 <div class="sc-widget-title">图层管理 9 <div class="sc-widget-bar"><span class="sc-widget-bar-close">×</span></div> 10 </div> 11 <div class="sc-widget-content"> 12 <ul class="sc-widget-tree"> 13 <li> 14 <div class="sc-widget-group"><span>图元</span> 15 <button class="sc-widget-updatePrimitiveLayers">刷新</button> 16 </div> 17 <dl> 18 <dd data-bind="foreach: primitiveLayers"> 19 <div class="sc-widget-treeNode sc-widget-item"> 20 <label><input type="checkbox" data-bind="checked: show"><span 21 data-bind="text: name, attr: {title: name}"></span></label> 22 </div> 23 </dd> 24 </dl> 25 </li> 26 <li> 27 <div class="sc-widget-group"><span>实体</span> 28 <button class="sc-widget-updateEntityLayers">刷新</button> 29 </div> 30 <dl> 31 <dd data-bind="foreach: entityLayers"> 32 <div class="sc-widget-treeNode sc-widget-item"> 33 <label><input type="checkbox" data-bind="checked: show"><span 34 data-bind="text: name, attr: {title: name}"></span></label> 35 </div> 36 </dd> 37 </dl> 38 </li> 39 <li> 40 <div class="sc-widget-group"><span>影像</span> 41 <button class="sc-widget-updateImageryLayers">刷新</button> 42 </div> 43 <dl> 44 <dd data-bind="foreach: imageryLayers"> 45 <div class="sc-widget-treeNode sc-widget-item"> 46 <label><input type="checkbox" data-bind="checked: show"><span 47 data-bind="text: name, attr: {title: name}"></span></label> 48 <input type="range" min="0" max="1" step="0.01" data-bind="value: alpha, valueUpdate: 'input'"> 49 </div> 50 </dd> 51 </dl> 52 </li> 53 <li> 54 <div class="sc-widget-group"><span>地形</span> 55 <button class="sc-widget-updateTerrainLayers">刷新</button> 56 </div> 57 <dl> 58 <dd data-bind="foreach: terrainLayers"> 59 <div class="sc-widget-treeNode sc-widget-item"> 60 <label><input type="checkbox" data-bind="checked: show"><span 61 data-bind="text: name, attr: {title: name}"></span></label> 62 </div> 63 </dd> 64 </dl> 65 </li> 66 </ul> 67 </div> 68 </body> 69 </html>
LayerControl.css
1 .simpleCesium .sc-widget-layerControl .sc-widget-group button { 2 position: absolute; 3 right: 5px; 4 } 5 .simpleCesium .sc-widget-layerControl .sc-widget-item label{ 6 text-overflow: ellipsis; 7 overflow: hidden; 8 min-width: 120px; 9 /*max-width: 100px;*/ 10 } 11 .simpleCesium .sc-widget-layerControl .sc-widget-tree dd { 12 max-height: 150px; 13 overflow: auto; 14 }
小结
本篇实现了图层控制器的最基本功能:实时展示当前所有的图层数据,控制图层显示和隐藏,以及影像图层的透明度调节。实现原理是利用knockout动态追踪数据的属性状态。回头看一下上面的代码,真是极简单的,这都要归功于插件的基础,所以这里还是强烈建议大家先看一下上一篇关于插件的实现和规范。
相关资源
GitHub地址:https://github.com/HelsingWang/simple-cesium
Demo地址:http://helsing.wang:8888/simple-cesium
Cesium深入浅出系列CSDN地址:https://blog.csdn.net/fywindmoon
Cesium深入浅出系列博客园地址:https://www.cnblogs.com/HelsingWang
交流群:854943530
posted on 2021-01-29 23:16 Helsing·Wang 阅读(6400) 评论(0) 编辑 收藏 举报