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):

名稱 類型 屬性 描述
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.<string, *> <可選的> 與該實體關聯的任意屬性。
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同一時間僅允許一個地形數據有效。說它復雜是因為它根本就不像圖層數據,一些基本的操作都很難實現,比如地形的隱藏和顯示,當然也還是有辦法的,只不過要曲線救國,下面具體實現的時候會講到。下面看一下地形加載方法:

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM