Cesium深入浅出之图层管理器

引子

早就想做这篇内容了,毕竟做为一个GIS平台,没有图层管理器多不方便啊。然而在Cesium中图层这个概念都很模糊,虽然可以加载很多类型的数据,但是每种数据规格都不一样,导致加载进来之后并不能进行统一且有效的管理。熟悉ArcGIS的朋友一定知道,在ArcGIS中几乎所有的数据都是使用图层来承载的,因此想要管理图层数据轻而易举。而在Cesium中,除了影像数据能算的上图层以外,其他的数据压根都和图层扯不上关系,这点从其命名(imageryLayers)上就可以看得出来。但是这并不代表它不能以图层的方式进行管理,我们只要找到每种数据对应的不同载体,再进行分类处理,就可以了。

预期效果

Cesium深入浅出之图层管理器_第1张图片

说实话这个效果只能算是差强人意了,但暂时也就只能做成这样了,就当是抛砖引玉吧。

实现原理

关键是要先找到不同类型数据的载体,我总结了下在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):

名称 类型 属性 描述
id String <可选的> 对象的唯一ID。如果未设置,则会自动生成一个GUID。
name String <可选的> 为用户提供的可读性名称。它不必是唯一的。
availability TimeIntervalCollection <可选的> 与此对象关联的可用性,如果有的话。
show Boolean <可选的> 一个布尔值,是否显示实体及其子实体。
description Property | string <可选的> 实体的HTML描述字符串。
position PositionProperty | Cartesian3 <可选的> 实体的位置。
orientation Property <可选的> 实体的方位。
viewFrom Property <可选的> 查看此对象的建议初始偏移量。
parent Entity <可选的> 与该实体关联的父实体。
billboard BillboardGraphics | BillboardGraphics.ConstructorOptions <可选的> 与该实体关联的广告牌。
box BoxGraphics | BoxGraphics.ConstructorOptions <可选的> 与该实体关联的盒子。
corridor CorridorGraphics | CorridorGraphics.ConstructorOptions <可选的> 与该实体关联的通道。
cylinder CylinderGraphics | CylinderGraphics.ConstructorOptions <可选的> 与该实体关联的圆柱体。
ellipse EllipseGraphics | EllipseGraphics.ConstructorOptions <可选的> 与该实体关联的椭圆形。
ellipsoid EllipsoidGraphics | EllipsoidGraphics.ConstructorOptions <可选的> 与该实体关联的椭球体。
label LabelGraphics | LabelGraphics.ConstructorOptions <可选的> 与该实体关联的标签。
model ModelGraphics | ModelGraphics.ConstructorOptions <可选的> 与该实体关联的模型。
tileset Cesium3DTilesetGraphics | Cesium3DTilesetGraphics.ConstructorOptions <可选的> 与该实体关联的3D Tiles数据集。
path PathGraphics | PathGraphics.ConstructorOptions <可选的> 与该实体关联的路径。
plane PlaneGraphics | PlaneGraphics.ConstructorOptions <可选的> 与该实体关联的平面。
point PointGraphics | PointGraphics.ConstructorOptions <可选的> 与该实体关联的点。
polygon PolygonGraphics | PolygonGraphics.ConstructorOptions <可选的> 与该实体关联的多边形。
polyline PolylineGraphics | PolylineGraphics.ConstructorOptions <可选的> 与该实体关联的折线。
properties PropertyBag | Object. <可选的> 与该实体关联的任意属性。
polylineVolume PolylineVolumeGraphics | PolylineVolumeGraphics.ConstructorOptions <可选的> 与该实体关联的polylineVolume。
rectangle RectangleGraphics | RectangleGraphics.ConstructorOptions <可选的> 与该实体关联的矩形。
wall WallGraphics | WallGraphics.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.LINEARTextureMinificationFilter.NEAREST.
magnificationFilter TextureMagnificationFilter TextureMagnificationFilter.LINEAR 纹理放大过滤器。可能的值为TextureMinificationFilter.LINEARTextureMinificationFilter.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




    
    LayerControl


图层管理
×
  • 图元
  • 实体
  • 影像
  • 地形

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

你可能感兴趣的:(Cesium,3d)