引子

早就想做这篇内容了,毕竟做为一个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:

名称类型默认值描述
geometryInstancesArray.GeometryInstance> | GeometryInstance 用于渲染的一组或一个几何图形实例。
appearanceAppearance 用于渲染图元的外观。
depthFailAppearanceAppearance 当图元未通过深度测试时,用于对其进行着色的外观。
showBooleantrue是否显示图元。
modelMatrixMatrix4Matrix4.IDENTITY将图元(所有几何体实例)从模型坐标转换为世界坐标的4x4变换矩阵。
vertexCacheOptimizeBooleanfalse如果为true,几何体顶点将针对顶点前和顶点后着色器缓存进行优化。
interleaveBooleanfalse如果为true,几何体顶点属性将交错,以稍微提高渲染性能,但会增加加载时间。
compressVerticesBooleantrue如果为true,几何体顶点将被压缩,以节省内存。
releaseGeometryInstancesBooleantrue如果为true,则图元不保留对输入几何实例的引用,以节省内存。
allowPickingBooleantrue如果为true,则每个几何体实例将只能使用Scene#pick进行拾取;如果为false,则可节省GPU内存。 
cullBooleantrue如果为true,则渲染器视锥和地平线基于图元的外包围盒剔除其commands;如果要手动剔除图元,将值设置为false可以获得较小的性能增益。
asynchronousBooleantrue确定是选择异步创建图元还是在准备就绪前一直阻塞。
debugShowBoundingVolumeBooleanfalse仅用于调试。是否显示图元commands的外包围盒。
shadowsShadowModeShadowMode.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):

名称类型属性描述
idString<可选的>对象的唯一ID。如果未设置,则会自动生成一个GUID。
nameString<可选的>为用户提供的可读性名称。它不必是唯一的。
availabilityTimeIntervalCollection<可选的>与此对象关联的可用性,如果有的话。
showBoolean<可选的>一个布尔值,是否显示实体及其子实体。
descriptionProperty | string<可选的>实体的HTML描述字符串。
positionPositionProperty | Cartesian3<可选的>实体的位置。
orientationProperty<可选的>实体的方位。
viewFromProperty<可选的>查看此对象的建议初始偏移量。
parentEntity<可选的>与该实体关联的父实体。
billboardBillboardGraphics | BillboardGraphics.ConstructorOptions<可选的>与该实体关联的广告牌。
boxBoxGraphics | BoxGraphics.ConstructorOptions<可选的>与该实体关联的盒子。
corridorCorridorGraphics | CorridorGraphics.ConstructorOptions<可选的>与该实体关联的通道。
cylinderCylinderGraphics | CylinderGraphics.ConstructorOptions<可选的>与该实体关联的圆柱体。
ellipseEllipseGraphics | EllipseGraphics.ConstructorOptions<可选的>与该实体关联的椭圆形。
ellipsoidEllipsoidGraphics | EllipsoidGraphics.ConstructorOptions<可选的>与该实体关联的椭球体。
labelLabelGraphics | LabelGraphics.ConstructorOptions<可选的>与该实体关联的标签。
modelModelGraphics | ModelGraphics.ConstructorOptions<可选的>与该实体关联的模型。
tilesetCesium3DTilesetGraphics | Cesium3DTilesetGraphics.ConstructorOptions<可选的>与该实体关联的3D Tiles数据集。
pathPathGraphics | PathGraphics.ConstructorOptions<可选的>与该实体关联的路径。
planePlaneGraphics | PlaneGraphics.ConstructorOptions<可选的>与该实体关联的平面。
pointPointGraphics | PointGraphics.ConstructorOptions<可选的>与该实体关联的点。
polygonPolygonGraphics | PolygonGraphics.ConstructorOptions<可选的>与该实体关联的多边形。
polylinePolylineGraphics | PolylineGraphics.ConstructorOptions<可选的>与该实体关联的折线。
propertiesPropertyBag | Object.<string, *><可选的>与该实体关联的任意属性。
polylineVolumePolylineVolumeGraphics | PolylineVolumeGraphics.ConstructorOptions<可选的>与该实体关联的polylineVolume。
rectangleRectangleGraphics | RectangleGraphics.ConstructorOptions<可选的>与该实体关联的矩形。
wallWallGraphics | WallGraphics.ConstructorOptions<可选的>与该实体关联的围墙

Entity不愧是比Primitive更为高级的数据格式,功能更强大且封装的也更规范。从API中我们可以清晰地看到Entity所支持的所有数据类型,都是以属性的形式单独存放于options参数中。看描述我们就知道了每个属性的含义,这里就不赘述了。我们还是只关心show属性,也是控制数据显示和隐藏的。还有name属性,也就是数据名称,在我们这里可以理解为图层名称,要注意,这个属性Primitive是没有的,但不代表你不可以给它添加这个属性,大家都知道Javascript的开放性,我们可以自由地为对象扩展属性,毕竟没有图层名称还是很难管理的,所以建议大家添加Primitive的时候为它赋个名称。

ImageryLayer

这个就厉害了,看名字就知道人家是真真正正的图层数据。

构造函数:new Cesium.ImageryLayer(imageryProvider, options)

参数imageryProvider:要显示在椭球体表面的影像提供器,如ArcGisMapServerImageryProvider、BingMapsImageryProvider、GoogleEarthEnterpriseImageryProvider等。

参数options:

名称类型默认值描述
rectangleRectangleimageryProvider.rectangle图层的矩形范围框。这个矩形框可以限制影像提供器的可见部分。
alphaNumber | function1.0图层的alpha混合值,从0.0到1.0。可以是一个简单的数字,也可以是signaturefunction(frameState、layer、x、y、level)函数。函数将传递当前帧的状态、该图层以及需要alpha的影像分块的x、y和level坐标,并返回用于瓦片分块的alpha值。
nightAlphaNumber | function1.0图层在地球夜间的alpha混合值,从0.0到1.0。可以是一个简单的数字,也可以是一个signaturefunction(frameState、layer、x、y、level)函数。仅在enableLighting为true时生效。
dayAlphaNumber | function1.0图层在地球白天一侧的alpha混合值,从0.0到1.0。可以是一个简单的数字,也可以是一个signaturefunction(frameState、layer、x、y、level)函数。仅在enableLighting为true时生效。
brightnessNumber | function1.0图层的亮度。当值为1.0时,使用未修改的图像颜色。当值小于1.0时,图像会变得更暗,而大于1.0会图像会变得更亮。可以是一个简单的数字,也可以是一个signaturefunction(frameState、layer、x、y、level)函数。这个函数是为每帧和每个瓦片执行的,所以它必须是快速执行的。
contrastNumber | function1.0图层的对比度。当值为1.0时,使用未修改的图像颜色。当值小于1.0会降低对比度,大于1.0会增加对比度。可以是一个简单的数字,也可以是一个signaturefunction(frameState、layer、x、y、level)函数。这个函数是为每帧和每个瓦片执行的,所以它必须是快速执行的。
hueNumber | function0.0图层的色调。当值为1.0时,使用未修改的图像颜色。可以是一个简单的数字,也可以是一个signaturefunction(frameState、layer、x、y、level)函数。这个函数是为每帧和每个瓦片执行的,所以它必须是快速执行的。
saturationNumber | function1.0图层的饱和度。当值为1.0时,使用未修改的图像颜色。小于1.0会降低饱和度,大于1.0会增加饱和度。当值小于1.0会降低对比度,大于1.0会增加对比度。可以是一个简单的数字,也可以是一个signaturefunction(frameState、layer、x、y、level)函数。这个函数是为每帧和每个瓦片执行的,所以它必须是快速执行的。
gammaNumber | function1.0图层的伽马校正值。当值为1.0时,使用未修改的图像颜色。可以是一个简单的数字,也可以是一个signaturefunction(frameState、layer、x、y、level)函数。这个函数是为每帧和每个瓦片执行的,所以它必须是快速执行的。
splitDirectionImagerySplitDirection |functionImagerySplitDirection.NONE影像分割方向
minificationFilterTextureMinificationFilterTextureMinificationFilter.LINEAR纹理缩小过滤器。可能的值为TextureMinificationFilter.LINEARTextureMinificationFilter.NEAREST.
magnificationFilterTextureMagnificationFilterTextureMagnificationFilter.LINEAR纹理放大过滤器。可能的值为TextureMinificationFilter.LINEARTextureMinificationFilter.NEAREST.
showBooleantrue是否显示该图层。
maximumAnisotropyNumbermaximum supported用于纹理过滤的最大各向异性级别。如果未指定此参数,则将使用WebGL堆栈支持的最大各向异性。设置较大一点的值可以使影像在水平视图中看起来更好。
minimumTerrainLevelNumber 显示图层的最小地形细节级别,如果未定义则所有级别显示它。零级是最不详细的级别。
maximumTerrainLevelNumber 显示图层的最大地形细节级别,如果未定义则所有级别显示它。零级是最不详细的级别。
cutoutRectangleRectangle 制图矩形,用于剪切影像图层。
colorToAlphaColor 用于alpha的颜色。
colorToAlphaThresholdNumber0.004color-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

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐