Cesium深入浅出之图层管理器
引子早就想做这篇内容了,毕竟做为一个GIS平台,没有图层管理器多不方便啊。然而在Cesium中图层这个概念都很模糊,虽然可以加载很多类型的数据,但是每种数据规格都不一样,导致加载进来之后并不能进行统一且有效的管理。熟悉ArcGIS的朋友一定知道,在ArcGIS中几乎所有的数据都是使用图层来承载的,因此想要管理图层数据轻而易举。而在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格式的模型数据,你的代码可能是这样的:
var model = scene.primitives.add(Cesium.Model.fromGltf({
url : './duck/duck.gltf'
}));
也可能是这样的:
var model = viewer.entities.add({
model: {
uri: './duck/duck.gltf'
}
});
那么它们有什么区别呢?最大的区别就是数据载体不一样,一个是加载到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同一时间仅允许一个地形数据有效。说它复杂是因为它根本就不像图层数据,一些基本的操作都很难实现,比如地形的隐藏和显示,当然也还是有办法的,只不过要曲线救国,下面具体实现的时候会讲到。下面看一下地形加载方法:
viewer.terrainProvider = new Cesium.CesiumTerrainProvider({
url: IonResource.fromAssetId(3956),
requestWaterMask: true
});
现在我们知道为什么只能加载一个地形数据了,它是viewer的属性直接赋值的,而不是像其它数据那样加载到容器中。我想Cesium之所以这么设计可能是因为地形数据不好叠加吧,不过如果我们有多个地形数据,而且每个数据都是分布在不同的地方,要想把它们同时加载进来就没办法做到了,不得不说好多时候我们还是有这个需求的,后续我或许会做些这方面的研究吧。
具体实现
前面就说过了,我们是按Cesium插件规范来实现图层管理器,照例我会全部代码奉上,以便于大家学习,如果有公共引用的代码这里没列出来,请到github上去获取。
文件结构
▼📂src
▼📂widgets
▼📂LayerControl
LayerControl.css
LayerControl.html
LayerControl.js
LayerControlViewModel.js
viewerLayerControlMixin.js
viewerLayerControlMixin.js
import defined from "cesium/Source/Core/defined.js";
import DeveloperError from "cesium/Source/Core/DeveloperError.js";
import LayerControl from "./LayerControl.js";
import "./LayerControl.css"
/**
* A mixin which adds the LayerControl widget to the Viewer widget.
* Rather than being called directly, this function is normally passed as
* a parameter to {@link Viewer#extend}, as shown in the example below.
*
* @function
* @param {Viewer} viewer The viewer instance.
* @param {Object} [options={}] The options.
* @exception {DeveloperError} viewer is required.
* @demo {@link http://helsing.wang:8888/simple-cesium | LayerControl Demo}
* @example
* var viewer = new Cesium.Viewer('cesiumContainer');
* viewer.extend(viewerLayerControlMixin);
*/
function viewerLayerControlMixin(viewer, options = {}) {
if (!defined(viewer)) {
throw new DeveloperError("viewer is required.");
}
const container = document.createElement("div");
container.className = "sc-widget-container";
const parent = viewer.scWidgetsContainer || viewer.container;
parent.appendChild(container);
const widget = new LayerControl(
viewer, {container: container}
);
// Remove the layerControl property from viewer.
widget.addOnDestroyListener((function (viewer) {
return function () {
defined(container) && container.parentNode.removeChild(container);
delete viewer.scLayerControl;
}
})(viewer))
// Add the layerControl property to viewer.
Object.defineProperties(viewer, {
scLayerControl: {
get: function () {
return widget;
},
configurable: true
},
});
}
export default viewerLayerControlMixin;
这个没啥好说的,都是插件规范,有不理解的可以参考上一篇关于插件封装的文章。
LayerControl.js
import defined from "cesium/Source/Core/defined.js";
import DeveloperError from "cesium/Source/Core/DeveloperError.js";
import destroyObject from "cesium/Source/Core/destroyObject.js";
import knockout from "cesium/Source/ThirdParty/knockout.js";
import {bindEvent,getElement,insertHtml} from "../../common/util.js";
import LayerControlViewModel from "./LayerControlViewModel.js";
import LayerControlHtml from "./LayerControl.html";
class LayerControl {
/**
* Gets the parent container.
* @memberOf LayerControl.prototype
* @type {Element}
*/
get container() {
return this._container;
}
/**
* Gets the view model.
* @memberOf LayerControl.prototype
* @type {LayerControlViewModel}
*/
get viewModel() {
return this._viewModel;
}
constructor(viewer, options={}) {
this._element = undefined;
this._container= undefined;
this._viewModel= undefined;
this._onDestroyListeners= [];
if (!defined(viewer)) {
throw new DeveloperError("viewer is required.");
}
if (!defined(options)) {
throw new DeveloperError("container is required.");
}
const that = this;
let container = options.container;
typeof options === "string" && (container = options);
container = getElement(container);
const element = document.createElement("div");
element.className = "sc-widget sc-widget-layerControl";
insertHtml(element, {
content: LayerControlHtml, delay:1000, callback: () => {
bindEvent(".sc-widget-layerControl .sc-widget-bar-close", "click", function () {
that.destroy();
})
bindEvent(".sc-widget-layerControl .sc-widget-updatePrimitiveLayers", "click", function () {
that._viewModel._updatePrimitiveLayers();
})
bindEvent(".sc-widget-layerControl .sc-widget-updateEntityLayers", "click", function () {
that._viewModel._updateEntityLayers();
})
bindEvent(".sc-widget-layerControl .sc-widget-updateImageryLayers", "click", function () {
that._viewModel._updateImageryLayers();
})
bindEvent(".sc-widget-layerControl .sc-widget-updateTerrainLayers", "click", function () {
that._viewModel._updateTerrainLayers();
})
}
});
container.appendChild(element);
const viewModel = new LayerControlViewModel(viewer, element);
this._viewModel = viewModel;
this._element = element;
this._container = container;
// 绑定viewModel和element
knockout.applyBindings(viewModel, element);
}
/**
* @returns {Boolean} true if the object has been destroyed, false otherwise.
*/
isDestroyed () {
return false;
}
/**
* Destroys the widget. Should be called if permanently.
* removing the widget from layout.
*/
destroy () {
if (defined(this._element)) {
knockout.cleanNode(this._element);
defined(this._container) && this._container.removeChild(this._element);
}
delete this._element;
delete this._container;
defined(this._viewModel) && this._viewModel.destroy();
delete this._viewModel;
for (let i = 0; i < this._onDestroyListeners.length; i++) {
this._onDestroyListeners[i]();
}
return destroyObject(this);
}
addOnDestroyListener(callback) {
if (typeof callback === 'function') {
this._onDestroyListeners.push(callback)
}
}
}
export default LayerControl;
这个也基本是规范,没啥好说的,就注意一下插入HTML后绑定刷新按钮的单击事件就行了。
LayerControlViewModel.js
import defined from "cesium/Source/Core/defined.js";
import defaultValue from "cesium/Source/Core/defaultValue.js";
import destroyObject from "cesium/Source/Core/destroyObject.js";
import DeveloperError from "cesium/Source/Core/DeveloperError.js";
import EventHelper from "cesium/Source/Core/EventHelper.js";
import Model from "cesium/Source/Scene/Model.js";
import PrimitiveCollection from "cesium/Source/Scene/PrimitiveCollection.js";
import ScreenSpaceEventHandler from "cesium/Source/Core/ScreenSpaceEventHandler.js";
import CesiumTerrainProvider from "cesium/Source/Core/CesiumTerrainProvider.js";
import EllipsoidTerrainProvider from "cesium/Source/Core/EllipsoidTerrainProvider.js";
import IonResource from "cesium/Source/Core/IonResource.js";
import knockout from "cesium/Source/ThirdParty/knockout.js";
class LayerControlViewModel {
constructor(viewer) {
if (!defined(viewer)) {
throw new DeveloperError("viewer is required");
}
const that = this;
const scene = viewer.scene;
const canvas = scene.canvas;
const eventHandler = new ScreenSpaceEventHandler(canvas);
this._viewer = viewer;
this._eventHandler = eventHandler;
this._removePostRenderEvent = scene.postRender.addEventListener(function () {
that._update();
});
this._subscribes = [];
this.primitiveLayers = [];
this.entityLayers = [];
this.imageryLayers = [];
this.terrainLayers = [];
Object.assign(this, {
"viewerShadows": defaultValue(viewer.shadows, false),
})
knockout.track(this);
const props = [
["viewerShadows", viewer, "shadows"]
];
props.forEach(value => this._subscribe(value[0], value[1], value[2]));
const helper = new EventHelper();
// 底图加载完成后的事件
helper.add(viewer.scene.globe.tileLoadProgressEvent, function (event) {
if (event === 0) {
that._updatePrimitiveLayers();
that._updateEntityLayers();
that._updateImageryLayers();
that._updateTerrainLayers();
}
});
}
destroy() {
this._eventHandler.destroy();
this._viewer.scene.postRender.removeEventListener(this._removePostRenderEvent);
for (let i = this._subscribes.length - 1; i >= 0; i--) {
this._subscribes[i].dispose();
this._subscribes.pop();
}
return destroyObject(this);
}
_update() {
}
_subscribe(name, obj, prop) {
const that = this;
const result = knockout
.getObservable(that, name)
.subscribe(() => {
obj[prop] = that[name];
that._viewer.scene.requestRender();
});
this._subscribes.push(result);
}
_updatePrimitiveLayers() {
const layers = this._viewer.scene.primitives;
const count = layers.length;
this.primitiveLayers.splice(0, this.primitiveLayers.length);
for (let i = count - 1; i >= 0; --i) {
const layer = layers.get(i);
if (!layer.name) {
if (layer.isCesium3DTileset) {
layer.url && (layer.name = layer.url.substring(0, layer.url.lastIndexOf("/"))
.replace(/^(.*[\/\\])?(.*)*$/, '$2'));
} else if (layer instanceof Model) {
layer._resource && (layer.name = layer._resource.url.replace(/^(.*[\/\\])?(.*)*(\.[^.?]*.*)$/, '$2'));
} else if (layer instanceof PrimitiveCollection) {
layer.name = `PrimitiveCollection_${layer._guid}`;
}
}
!layer.name && (layer.name = "[未命名]");
this.primitiveLayers.push(layer);
knockout.track(layer, ["show", "name"]);
}
}
_updateEntityLayers() {
const layers = this._viewer.entities.values;
const count = layers.length;
this.entityLayers.splice(0, this.entityLayers.length);
for (let i = count - 1; i >= 0; --i) {
const layer = layers[i];
!layer.name && (layer.name = "[未命名]");
layer.name = layer.name.replace(/^(.*[\/\\])?(.*)*(\.[^.?]*.*)$/, '$2')
this.entityLayers.push(layer);
knockout.track(layer, ["show", "name"]);
}
}
_updateImageryLayers() {
const layers = this._viewer.imageryLayers;
const count = layers.length;
this.imageryLayers.splice(0, this.imageryLayers.length);
for (let i = count - 1; i >= 0; --i) {
const layer = layers.get(i);
if (!layer.name) {
layer.name = layer.imageryProvider._resource.url;
}
!layer.name && (layer.name = "[未命名]");
this.imageryLayers.push(layer);
knockout.track(layer, ["alpha", "show", "name"]);
}
}
_updateTerrainLayers() {
const that = this;
this.terrainLayers.splice(0, this.terrainLayers.length);
const layer = this._viewer.terrainProvider;
const realLayers = that._viewer.terrainProvider._layers;
const realShow = !!(realLayers && realLayers.length > 0);
if (!layer.name && realShow) {
layer.name = realLayers[0].resource._url + realLayers[0].tileUrlTemplates;
}
!layer.name && (layer.name = "[默认地形]");
// 定义show属性
!defined(layer.show) && Object.defineProperties(layer, {
show: {
get: function () {
return realShow;
},
configurable: true
},
});
if (realShow !== layer.show) {
let terrainProvider;
if (!layer.show) {
// add a simple terain so no terrain shall be preseneted
terrainProvider = new EllipsoidTerrainProvider();
} else {
// enable the terain
terrainProvider = new CesiumTerrainProvider({
url: IonResource.fromAssetId(3956),
requestWaterMask: true
});
}
that._viewer.terrainProvider = terrainProvider;
}
this.terrainLayers.push(layer);
knockout.track(layer, ["alpha", "show", "name"]);
}
}
export default LayerControlViewModel;
这部分封装算是整个插件中的核心部分,其中大部分还是关于knockout封装的代码,也就是上一篇中的通用内容,这里只讲一下不同的地方吧。先要定义四种图层的集合变量,然后在viewer.scene.globe.tileLoadProgressEvent这个事件中添加图层更新代码,图层更新代码分别对应四种类型的数据封装了四个函数,在更新函数中实现了图层数据的获取以及knockout的响应追踪,其实就是双向绑定图层的show、name等属性,以达到数据和界面状态同步。这里再着重讲一下地形数据的更新,因为地形数据没有show这个属性,所以我们需要自行实现。其实核心代码也只有一句:terrainProvider = new EllipsoidTerrainProvider(),它可以清除当前的地形,做下简单的封装我们就可以实现默认地形和清除地形的切换了。这里我只是做了最简单的实现,如果要加载自定的地形数据的话就不适用了,还需要你们自行改造一下。
LayerControl.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>LayerControl</title>
</head>
<body>
<div class="sc-widget-title">图层管理
<div class="sc-widget-bar"><span class="sc-widget-bar-close">×</span></div>
</div>
<div class="sc-widget-content">
<ul class="sc-widget-tree">
<li>
<div class="sc-widget-group"><span>图元</span>
<button class="sc-widget-updatePrimitiveLayers">刷新</button>
</div>
<dl>
<dd data-bind="foreach: primitiveLayers">
<div class="sc-widget-treeNode sc-widget-item">
<label><input type="checkbox" data-bind="checked: show"><span
data-bind="text: name, attr: {title: name}"></span></label>
</div>
</dd>
</dl>
</li>
<li>
<div class="sc-widget-group"><span>实体</span>
<button class="sc-widget-updateEntityLayers">刷新</button>
</div>
<dl>
<dd data-bind="foreach: entityLayers">
<div class="sc-widget-treeNode sc-widget-item">
<label><input type="checkbox" data-bind="checked: show"><span
data-bind="text: name, attr: {title: name}"></span></label>
</div>
</dd>
</dl>
</li>
<li>
<div class="sc-widget-group"><span>影像</span>
<button class="sc-widget-updateImageryLayers">刷新</button>
</div>
<dl>
<dd data-bind="foreach: imageryLayers">
<div class="sc-widget-treeNode sc-widget-item">
<label><input type="checkbox" data-bind="checked: show"><span
data-bind="text: name, attr: {title: name}"></span></label>
<input type="range" min="0" max="1" step="0.01" data-bind="value: alpha, valueUpdate: 'input'">
</div>
</dd>
</dl>
</li>
<li>
<div class="sc-widget-group"><span>地形</span>
<button class="sc-widget-updateTerrainLayers">刷新</button>
</div>
<dl>
<dd data-bind="foreach: terrainLayers">
<div class="sc-widget-treeNode sc-widget-item">
<label><input type="checkbox" data-bind="checked: show"><span
data-bind="text: name, attr: {title: name}"></span></label>
</div>
</dd>
</dl>
</li>
</ul>
</div>
</body>
</html>
LayerControl.css
.simpleCesium .sc-widget-layerControl .sc-widget-group button {
position: absolute;
right: 5px;
}
.simpleCesium .sc-widget-layerControl .sc-widget-item label{
text-overflow: ellipsis;
overflow: hidden;
min-width: 120px;
/*max-width: 100px;*/
}
.simpleCesium .sc-widget-layerControl .sc-widget-tree dd {
max-height: 150px;
overflow: auto;
}
小结
本篇实现了图层控制器的最基本功能:实时展示当前所有的图层数据,控制图层显示和隐藏,以及影像图层的透明度调节。实现原理是利用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
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)