引子
早就想做這篇內容了,畢竟做為一個GIS平台,沒有圖層管理器多不方便啊。然而在Cesium中圖層這個概念都很模糊,雖然可以加載很多類型的數據,但是每種數據規格都不一樣,導致加載進來之后並不能進行統一且有效的管理。熟悉ArcGIS的朋友一定知道,在ArcGIS中幾乎所有的數據都是使用圖層來承載的,因此想要管理圖層數據輕而易舉。而在Cesium中,除了影像數據能算的上圖層以外,其他的數據壓根都和圖層扯不上關系,這點從其命名(imageryLayers)上就可以看得出來。但是這並不代表它不能以圖層的方式進行管理,我們只要找到每種數據對應的不同載體,再進行分類處理,就可以了。
預期效果

說實話這個效果只能算是差強人意了,但暫時也就只能做成這樣了,就當是拋磚引玉吧。
實現原理
關鍵是要先找到不同類型數據的載體,我總結了下在Cesium中大概分為四類數據:圖元數據(Primitive)、實體數據(Entity)、影像數據(Imagery)、地形數據(Terrain),因為這四類數據的形式是截然不同的,它們分別處於四個不同的數據載體中,所以我們在圖層管理器中也是划分了對應的四個分組,接下來就是針對不同的數據載體進行不同的操作了。其次是圖層管理器的表現形式,本篇中采用Cesium的Mixin規范進行封裝的,如果有不熟悉的小伙伴請看我前面一篇文章,是關於插件是如何封裝的。
原理就是這么簡單幾句話,不過在進入具體實現環節之前,我們還是先來簡單講講這四種類型數據的相關知識點吧。直接上代碼來寫文章是很快,但是真對不住“深入淺出”這個詞啊,所以還是不能偷懶,希望小伙伴們也不要偷懶,直接把代碼copy過去就不管不問了,要做到知其然和知其所以然。
Primitive
在這個系列文章的第一篇中我講過了Primitive和Entity的區別,簡單說來就是Primitive更接近底層且效率高,Entity更豐富更強大但效率低,所以我們也是推薦大家加載數據盡量使用Primitive的方式。其實大部分Entity能做到的功能Primitive也能做的到,只是稍微麻煩一點,但為了性能考慮那點小小的麻煩可以忽略不計了。當然了,Entity也不是一無是處的,比如CallbackProperty這個東東,用過的小伙伴都說好,用它來做個動畫效果簡直易如反掌,所以我們在日常開發中可以將這二者有機的結合,使用Entity進行Feedback,而使用Primitive做最終展現。不過這只是我個人的見解罷了,也許大牛直接Primitive搞定一切也說不定呢呢。其實底層的東西都有類似的特性,就是越深入越強大,我后面還想出一篇Primitive的專題文章,深入挖掘一下Primitive的潛力。
先來看下Primitive的定義:
構造函數:new Cesium.Primitive(options)
參數options:
| 名稱 | 類型 | 默認值 | 描述 |
|---|---|---|---|
geometryInstances |
Array.GeometryInstance> | GeometryInstance | 用於渲染的一組或一個幾何圖形實例。 | |
appearance |
Appearance | 用於渲染圖元的外觀。 | |
depthFailAppearance |
Appearance | 當圖元未通過深度測試時,用於對其進行着色的外觀。 | |
show |
Boolean | true |
是否顯示圖元。 |
modelMatrix |
Matrix4 | Matrix4.IDENTITY |
將圖元(所有幾何體實例)從模型坐標轉換為世界坐標的4x4變換矩陣。 |
vertexCacheOptimize |
Boolean | false |
如果為true,幾何體頂點將針對頂點前和頂點后着色器緩存進行優化。 |
interleave |
Boolean | false |
如果為true,幾何體頂點屬性將交錯,以稍微提高渲染性能,但會增加加載時間。 |
compressVertices |
Boolean | true |
如果為true,幾何體頂點將被壓縮,以節省內存。 |
releaseGeometryInstances |
Boolean | true |
如果為true,則圖元不保留對輸入幾何實例的引用,以節省內存。 |
allowPicking |
Boolean | true |
如果為true,則每個幾何體實例將只能使用Scene#pick進行拾取;如果為false,則可節省GPU內存。 |
cull |
Boolean | true |
如果為true,則渲染器視錐和地平線基於圖元的外包圍盒剔除其commands;如果要手動剔除圖元,將值設置為false可以獲得較小的性能增益。 |
asynchronous |
Boolean | true |
確定是選擇異步創建圖元還是在准備就緒前一直阻塞。 |
debugShowBoundingVolume |
Boolean | false |
僅用於調試。是否顯示圖元commands的外包圍盒。 |
shadows |
ShadowMode | ShadowMode.DISABLED |
確定圖元是從光源投射陰影還是從光源接收陰影。 |
上面的表格十分清晰地為我們展現了Primitive的詳細定義,可以說看完表格基本就會用了呢,所以API很有用吧。這里插點題外話,API之所以重要,是因為API是所有二次開發的根本,在開發之前最先要做的就是看API,然后才是去百度、看文章、開源代碼,也就是說我們應該面向API開發,而不是面向百度開發,在開發之前很有必要梳理一下API,尤其是涉及數據類型的重點API,正所謂磨刀不誤砍柴工。通過上面的API,我們對Primitive的構造有了基本的了解,其中的show屬性在后面講到的圖層管理器實現中會用到,它是控制數據的顯示和隱藏的,其它屬性我們在這里不做過多的延申說明了。
不知道大家發現沒有,當你使用viewer.scene.primitives去遍歷的時候,里面會出現很多奇怪的東東,比如Cesium3DTileset、Model等等,對象結構也和上述API中列的不一樣,這是為什么呢?原來啊,PrimitiveCollection中不僅僅可以存儲Primitive數據,還可以存儲其他非嚴格意義的Primitive數據。也就是說,在Cesium中,Primitive是比較寬泛的概念,只要具備一定的規范都可以算做是Primitive,而PrimitiveCollection只是一個容器而已。以Model為例,大家可能都加載過GLTF格式的模型數據,你的代碼可能是這樣的:
1 var model = scene.primitives.add(Cesium.Model.fromGltf({ 2 url : './duck/duck.gltf' 3 }));
也可能是這樣的:
1 var model = viewer.entities.add({ 2 model: { 3 uri: './duck/duck.gltf' 4 } 5 });
那么它們有什么區別呢?最大的區別就是數據載體不一樣,一個是加載到PrimitiveCollection中,一個是加載到EntityCollection中。那么我們很容易理解了,同樣的Model,第一種加載方式數據類型是Primitive,第二種加載方式數據類型就是Entity。那么我們可以延申一下,是不是可以自定義一種Primitive數據然后加載到PrimitiveCollection中呢?這個問題的答案可以在我前面寫的關於視頻投影的文章中找到答案,我們視頻投影類封裝好之后加載到PrimitiveCollection中,發現它可以很好的運轉。當然了我們必須Primitive特定的規范,比如update()等。
Entity
Cesium對Entity的結構組織不像Primitive那樣松散,總體來講還是比較清晰的。
構造函數:new Cesium.Entity(options)
參數options(Cesium.Entity.ConstructorOptions):
Entity不愧是比Primitive更為高級的數據格式,功能更強大且封裝的也更規范。從API中我們可以清晰地看到Entity所支持的所有數據類型,都是以屬性的形式單獨存放於options參數中。看描述我們就知道了每個屬性的含義,這里就不贅述了。我們還是只關心show屬性,也是控制數據顯示和隱藏的。還有name屬性,也就是數據名稱,在我們這里可以理解為圖層名稱,要注意,這個屬性Primitive是沒有的,但不代表你不可以給它添加這個屬性,大家都知道Javascript的開放性,我們可以自由地為對象擴展屬性,畢竟沒有圖層名稱還是很難管理的,所以建議大家添加Primitive的時候為它賦個名稱。
ImageryLayer
這個就厲害了,看名字就知道人家是真真正正的圖層數據。
構造函數:new Cesium.ImageryLayer(imageryProvider, options)
參數imageryProvider:要顯示在橢球體表面的影像提供器,如ArcGisMapServerImageryProvider、BingMapsImageryProvider、GoogleEarthEnterpriseImageryProvider等。
參數options:
| 名稱 | 類型 | 默認值 | 描述 |
|---|---|---|---|
rectangle |
Rectangle | imageryProvider.rectangle |
圖層的矩形范圍框。這個矩形框可以限制影像提供器的可見部分。 |
alpha |
Number | function | 1.0 |
圖層的alpha混合值,從0.0到1.0。可以是一個簡單的數字,也可以是signaturefunction(frameState、layer、x、y、level)函數。函數將傳遞當前幀的狀態、該圖層以及需要alpha的影像分塊的x、y和level坐標,並返回用於瓦片分塊的alpha值。 |
nightAlpha |
Number | function | 1.0 |
圖層在地球夜間的alpha混合值,從0.0到1.0。可以是一個簡單的數字,也可以是一個signaturefunction(frameState、layer、x、y、level)函數。僅在enableLighting為true時生效。 |
dayAlpha |
Number | function | 1.0 |
圖層在地球白天一側的alpha混合值,從0.0到1.0。可以是一個簡單的數字,也可以是一個signaturefunction(frameState、layer、x、y、level)函數。僅在enableLighting為true時生效。 |
brightness |
Number | function | 1.0 |
圖層的亮度。當值為1.0時,使用未修改的圖像顏色。當值小於1.0時,圖像會變得更暗,而大於1.0會圖像會變得更亮。可以是一個簡單的數字,也可以是一個signaturefunction(frameState、layer、x、y、level)函數。這個函數是為每幀和每個瓦片執行的,所以它必須是快速執行的。 |
contrast |
Number | function | 1.0 |
圖層的對比度。當值為1.0時,使用未修改的圖像顏色。當值小於1.0會降低對比度,大於1.0會增加對比度。可以是一個簡單的數字,也可以是一個signaturefunction(frameState、layer、x、y、level)函數。這個函數是為每幀和每個瓦片執行的,所以它必須是快速執行的。 |
hue |
Number | function | 0.0 |
圖層的色調。當值為1.0時,使用未修改的圖像顏色。可以是一個簡單的數字,也可以是一個signaturefunction(frameState、layer、x、y、level)函數。這個函數是為每幀和每個瓦片執行的,所以它必須是快速執行的。 |
saturation |
Number | function | 1.0 |
圖層的飽和度。當值為1.0時,使用未修改的圖像顏色。小於1.0會降低飽和度,大於1.0會增加飽和度。當值小於1.0會降低對比度,大於1.0會增加對比度。可以是一個簡單的數字,也可以是一個signaturefunction(frameState、layer、x、y、level)函數。這個函數是為每幀和每個瓦片執行的,所以它必須是快速執行的。 |
gamma |
Number | function | 1.0 |
圖層的伽馬校正值。當值為1.0時,使用未修改的圖像顏色。可以是一個簡單的數字,也可以是一個signaturefunction(frameState、layer、x、y、level)函數。這個函數是為每幀和每個瓦片執行的,所以它必須是快速執行的。 |
splitDirection |
ImagerySplitDirection |function | ImagerySplitDirection.NONE |
影像分割方向。 |
minificationFilter |
TextureMinificationFilter | TextureMinificationFilter.LINEAR |
紋理縮小過濾器。可能的值為TextureMinificationFilter.LINEAR或TextureMinificationFilter.NEAREST. |
magnificationFilter |
TextureMagnificationFilter | TextureMagnificationFilter.LINEAR |
紋理放大過濾器。可能的值為TextureMinificationFilter.LINEAR或TextureMinificationFilter.NEAREST. |
show |
Boolean | true |
是否顯示該圖層。 |
maximumAnisotropy |
Number | maximum supported |
用於紋理過濾的最大各向異性級別。如果未指定此參數,則將使用WebGL堆棧支持的最大各向異性。設置較大一點的值可以使影像在水平視圖中看起來更好。 |
minimumTerrainLevel |
Number | 顯示圖層的最小地形細節級別,如果未定義則所有級別顯示它。零級是最不詳細的級別。 | |
maximumTerrainLevel |
Number | 顯示圖層的最大地形細節級別,如果未定義則所有級別顯示它。零級是最不詳細的級別。 | |
cutoutRectangle |
Rectangle | 制圖矩形,用於剪切影像圖層。 | |
colorToAlpha |
Color | 用於alpha的顏色。 | |
colorToAlphaThreshold |
Number | 0.004 |
color-to-alpha的閾值。 |
累!這部分API的翻譯把我頭疼死了,非常拗口。上面說了,ImageryLayer是真正的圖層數據,看了API我們就知道了,里面有各種參數可供我們調節,如alpha、brightness、contrast、hue、saturation、gamma等,我們可以在圖層管理器中做除很多滑塊來調節,這個功能在沙盒中也有,不過本篇中僅涉及到了最常用alpha值的調節,也就是透明度,其它的你們可以自行擴展。
Terrain
說到地形數據,在Cesium中它算是既簡單又復雜的數據了。說它簡單是因為結構簡單、使用方法簡單,而且Cesium同一時間僅允許一個地形數據有效。說它復雜是因為它根本就不像圖層數據,一些基本的操作都很難實現,比如地形的隱藏和顯示,當然也還是有辦法的,只不過要曲線救國,下面具體實現的時候會講到。下面看一下地形加載方法:
1 viewer.terrainProvider = new Cesium.CesiumTerrainProvider({ 2 url: IonResource.fromAssetId(3956), 3 requestWaterMask: true 4 });
現在我們知道為什么只能加載一個地形數據了,它是viewer的屬性直接賦值的,而不是像其它數據那樣加載到容器中。我想Cesium之所以這么設計可能是因為地形數據不好疊加吧,不過如果我們有多個地形數據,而且每個數據都是分布在不同的地方,要想把它們同時加載進來就沒辦法做到了,不得不說好多時候我們還是有這個需求的,后續我或許會做些這方面的研究吧。
具體實現
前面就說過了,我們是按Cesium插件規范來實現圖層管理器,照例我會全部代碼奉上,以便於大家學習,如果有公共引用的代碼這里沒列出來,請到github上去獲取。
文件結構
▼📂src
▼📂widgets
▼📂LayerControl
LayerControl.css
LayerControl.html
LayerControl.js
LayerControlViewModel.js
viewerLayerControlMixin.js
viewerLayerControlMixin.js
1 import defined from "cesium/Source/Core/defined.js"; 2 import DeveloperError from "cesium/Source/Core/DeveloperError.js"; 3 import LayerControl from "./LayerControl.js"; 4 import "./LayerControl.css" 5 6 /** 7 * A mixin which adds the LayerControl widget to the Viewer widget. 8 * Rather than being called directly, this function is normally passed as 9 * a parameter to {@link Viewer#extend}, as shown in the example below. 10 * 11 * @function 12 * @param {Viewer} viewer The viewer instance. 13 * @param {Object} [options={}] The options. 14 * @exception {DeveloperError} viewer is required. 15 * @demo {@link http://helsing.wang:8888/simple-cesium | LayerControl Demo} 16 * @example 17 * var viewer = new Cesium.Viewer('cesiumContainer'); 18 * viewer.extend(viewerLayerControlMixin); 19 */ 20 function viewerLayerControlMixin(viewer, options = {}) { 21 if (!defined(viewer)) { 22 throw new DeveloperError("viewer is required."); 23 } 24 25 const container = document.createElement("div"); 26 container.className = "sc-widget-container"; 27 const parent = viewer.scWidgetsContainer || viewer.container; 28 parent.appendChild(container); 29 const widget = new LayerControl( 30 viewer, {container: container} 31 ); 32 33 // Remove the layerControl property from viewer. 34 widget.addOnDestroyListener((function (viewer) { 35 return function () { 36 defined(container) && container.parentNode.removeChild(container); 37 delete viewer.scLayerControl; 38 } 39 })(viewer)) 40 41 // Add the layerControl property to viewer. 42 Object.defineProperties(viewer, { 43 scLayerControl: { 44 get: function () { 45 return widget; 46 }, 47 configurable: true 48 }, 49 }); 50 } 51 52 export default viewerLayerControlMixin;
這個沒啥好說的,都是插件規范,有不理解的可以參考上一篇關於插件封裝的文章。
LayerControl.js
1 import defined from "cesium/Source/Core/defined.js"; 2 import DeveloperError from "cesium/Source/Core/DeveloperError.js"; 3 import destroyObject from "cesium/Source/Core/destroyObject.js"; 4 import knockout from "cesium/Source/ThirdParty/knockout.js"; 5 import {bindEvent,getElement,insertHtml} from "../../common/util.js"; 6 import LayerControlViewModel from "./LayerControlViewModel.js"; 7 import LayerControlHtml from "./LayerControl.html"; 8 9 class LayerControl { 10 11 /** 12 * Gets the parent container. 13 * @memberOf LayerControl.prototype 14 * @type {Element} 15 */ 16 get container() { 17 return this._container; 18 } 19 /** 20 * Gets the view model. 21 * @memberOf LayerControl.prototype 22 * @type {LayerControlViewModel} 23 */ 24 get viewModel() { 25 return this._viewModel; 26 } 27 28 constructor(viewer, options={}) { 29 this._element = undefined; 30 this._container= undefined; 31 this._viewModel= undefined; 32 this._onDestroyListeners= []; 33 34 if (!defined(viewer)) { 35 throw new DeveloperError("viewer is required."); 36 } 37 if (!defined(options)) { 38 throw new DeveloperError("container is required."); 39 } 40 41 const that = this; 42 let container = options.container; 43 typeof options === "string" && (container = options); 44 container = getElement(container); 45 const element = document.createElement("div"); 46 element.className = "sc-widget sc-widget-layerControl"; 47 insertHtml(element, { 48 content: LayerControlHtml, delay:1000, callback: () => { 49 bindEvent(".sc-widget-layerControl .sc-widget-bar-close", "click", function () { 50 that.destroy(); 51 }) 52 bindEvent(".sc-widget-layerControl .sc-widget-updatePrimitiveLayers", "click", function () { 53 that._viewModel._updatePrimitiveLayers(); 54 }) 55 bindEvent(".sc-widget-layerControl .sc-widget-updateEntityLayers", "click", function () { 56 that._viewModel._updateEntityLayers(); 57 }) 58 bindEvent(".sc-widget-layerControl .sc-widget-updateImageryLayers", "click", function () { 59 that._viewModel._updateImageryLayers(); 60 }) 61 bindEvent(".sc-widget-layerControl .sc-widget-updateTerrainLayers", "click", function () { 62 that._viewModel._updateTerrainLayers(); 63 }) 64 } 65 }); 66 container.appendChild(element); 67 const viewModel = new LayerControlViewModel(viewer, element); 68 69 this._viewModel = viewModel; 70 this._element = element; 71 this._container = container; 72 73 // 綁定viewModel和element 74 knockout.applyBindings(viewModel, element); 75 } 76 77 /** 78 * @returns {Boolean} true if the object has been destroyed, false otherwise. 79 */ 80 isDestroyed () { 81 return false; 82 } 83 84 /** 85 * Destroys the widget. Should be called if permanently. 86 * removing the widget from layout. 87 */ 88 destroy () { 89 if (defined(this._element)) { 90 knockout.cleanNode(this._element); 91 defined(this._container) && this._container.removeChild(this._element); 92 } 93 delete this._element; 94 delete this._container; 95 96 defined(this._viewModel) && this._viewModel.destroy(); 97 delete this._viewModel; 98 99 for (let i = 0; i < this._onDestroyListeners.length; i++) { 100 this._onDestroyListeners[i](); 101 } 102 103 return destroyObject(this); 104 } 105 106 addOnDestroyListener(callback) { 107 if (typeof callback === 'function') { 108 this._onDestroyListeners.push(callback) 109 } 110 } 111 } 112 113 export default LayerControl;
這個也基本是規范,沒啥好說的,就注意一下插入HTML后綁定刷新按鈕的單擊事件就行了。
LayerControlViewModel.js
1 import defined from "cesium/Source/Core/defined.js"; 2 import defaultValue from "cesium/Source/Core/defaultValue.js"; 3 import destroyObject from "cesium/Source/Core/destroyObject.js"; 4 import DeveloperError from "cesium/Source/Core/DeveloperError.js"; 5 import EventHelper from "cesium/Source/Core/EventHelper.js"; 6 import Model from "cesium/Source/Scene/Model.js"; 7 import PrimitiveCollection from "cesium/Source/Scene/PrimitiveCollection.js"; 8 import ScreenSpaceEventHandler from "cesium/Source/Core/ScreenSpaceEventHandler.js"; 9 import CesiumTerrainProvider from "cesium/Source/Core/CesiumTerrainProvider.js"; 10 import EllipsoidTerrainProvider from "cesium/Source/Core/EllipsoidTerrainProvider.js"; 11 import IonResource from "cesium/Source/Core/IonResource.js"; 12 import knockout from "cesium/Source/ThirdParty/knockout.js"; 13 14 class LayerControlViewModel { 15 constructor(viewer) { 16 if (!defined(viewer)) { 17 throw new DeveloperError("viewer is required"); 18 } 19 20 const that = this; 21 const scene = viewer.scene; 22 const canvas = scene.canvas; 23 const eventHandler = new ScreenSpaceEventHandler(canvas); 24 25 this._viewer = viewer; 26 this._eventHandler = eventHandler; 27 this._removePostRenderEvent = scene.postRender.addEventListener(function () { 28 that._update(); 29 }); 30 this._subscribes = []; 31 this.primitiveLayers = []; 32 this.entityLayers = []; 33 this.imageryLayers = []; 34 this.terrainLayers = []; 35 36 37 Object.assign(this, { 38 "viewerShadows": defaultValue(viewer.shadows, false), 39 }) 40 knockout.track(this); 41 const props = [ 42 ["viewerShadows", viewer, "shadows"] 43 ]; 44 props.forEach(value => this._subscribe(value[0], value[1], value[2])); 45 46 const helper = new EventHelper(); 47 // 底圖加載完成后的事件 48 helper.add(viewer.scene.globe.tileLoadProgressEvent, function (event) { 49 if (event === 0) { 50 that._updatePrimitiveLayers(); 51 that._updateEntityLayers(); 52 that._updateImageryLayers(); 53 that._updateTerrainLayers(); 54 } 55 }); 56 } 57 58 destroy() { 59 this._eventHandler.destroy(); 60 this._viewer.scene.postRender.removeEventListener(this._removePostRenderEvent); 61 for (let i = this._subscribes.length - 1; i >= 0; i--) { 62 this._subscribes[i].dispose(); 63 this._subscribes.pop(); 64 } 65 return destroyObject(this); 66 } 67 68 _update() { 69 70 } 71 72 _subscribe(name, obj, prop) { 73 const that = this; 74 const result = knockout 75 .getObservable(that, name) 76 .subscribe(() => { 77 obj[prop] = that[name]; 78 that._viewer.scene.requestRender(); 79 }); 80 this._subscribes.push(result); 81 } 82 83 _updatePrimitiveLayers() { 84 const layers = this._viewer.scene.primitives; 85 const count = layers.length; 86 this.primitiveLayers.splice(0, this.primitiveLayers.length); 87 for (let i = count - 1; i >= 0; --i) { 88 const layer = layers.get(i); 89 if (!layer.name) { 90 if (layer.isCesium3DTileset) { 91 layer.url && (layer.name = layer.url.substring(0, layer.url.lastIndexOf("/")) 92 .replace(/^(.*[\/\\])?(.*)*$/, '$2')); 93 } else if (layer instanceof Model) { 94 layer._resource && (layer.name = layer._resource.url.replace(/^(.*[\/\\])?(.*)*(\.[^.?]*.*)$/, '$2')); 95 } else if (layer instanceof PrimitiveCollection) { 96 layer.name = `PrimitiveCollection_${layer._guid}`; 97 } 98 } 99 !layer.name && (layer.name = "[未命名]"); 100 this.primitiveLayers.push(layer); 101 knockout.track(layer, ["show", "name"]); 102 } 103 } 104 105 _updateEntityLayers() { 106 const layers = this._viewer.entities.values; 107 const count = layers.length; 108 this.entityLayers.splice(0, this.entityLayers.length); 109 for (let i = count - 1; i >= 0; --i) { 110 const layer = layers[i]; 111 !layer.name && (layer.name = "[未命名]"); 112 layer.name = layer.name.replace(/^(.*[\/\\])?(.*)*(\.[^.?]*.*)$/, '$2') 113 this.entityLayers.push(layer); 114 knockout.track(layer, ["show", "name"]); 115 } 116 } 117 118 _updateImageryLayers() { 119 const layers = this._viewer.imageryLayers; 120 const count = layers.length; 121 this.imageryLayers.splice(0, this.imageryLayers.length); 122 for (let i = count - 1; i >= 0; --i) { 123 const layer = layers.get(i); 124 if (!layer.name) { 125 layer.name = layer.imageryProvider._resource.url; 126 } 127 !layer.name && (layer.name = "[未命名]"); 128 this.imageryLayers.push(layer); 129 knockout.track(layer, ["alpha", "show", "name"]); 130 } 131 } 132 133 _updateTerrainLayers() { 134 const that = this; 135 this.terrainLayers.splice(0, this.terrainLayers.length); 136 const layer = this._viewer.terrainProvider; 137 138 const realLayers = that._viewer.terrainProvider._layers; 139 const realShow = !!(realLayers && realLayers.length > 0); 140 if (!layer.name && realShow) { 141 layer.name = realLayers[0].resource._url + realLayers[0].tileUrlTemplates; 142 } 143 !layer.name && (layer.name = "[默認地形]"); 144 // 定義show屬性 145 !defined(layer.show) && Object.defineProperties(layer, { 146 show: { 147 get: function () { 148 return realShow; 149 }, 150 configurable: true 151 }, 152 }); 153 154 if (realShow !== layer.show) { 155 let terrainProvider; 156 if (!layer.show) { 157 // add a simple terain so no terrain shall be preseneted 158 terrainProvider = new EllipsoidTerrainProvider(); 159 } else { 160 // enable the terain 161 terrainProvider = new CesiumTerrainProvider({ 162 url: IonResource.fromAssetId(3956), 163 requestWaterMask: true 164 }); 165 } 166 that._viewer.terrainProvider = terrainProvider; 167 } 168 169 this.terrainLayers.push(layer); 170 knockout.track(layer, ["alpha", "show", "name"]); 171 172 } 173 } 174 175 export default LayerControlViewModel;
這部分封裝算是整個插件中的核心部分,其中大部分還是關於knockout封裝的代碼,也就是上一篇中的通用內容,這里只講一下不同的地方吧。先要定義四種圖層的集合變量,然后在viewer.scene.globe.tileLoadProgressEvent這個事件中添加圖層更新代碼,圖層更新代碼分別對應四種類型的數據封裝了四個函數,在更新函數中實現了圖層數據的獲取以及knockout的響應追蹤,其實就是雙向綁定圖層的show、name等屬性,以達到數據和界面狀態同步。這里再着重講一下地形數據的更新,因為地形數據沒有show這個屬性,所以我們需要自行實現。其實核心代碼也只有一句:terrainProvider = new EllipsoidTerrainProvider(),它可以清除當前的地形,做下簡單的封裝我們就可以實現默認地形和清除地形的切換了。這里我只是做了最簡單的實現,如果要加載自定的地形數據的話就不適用了,還需要你們自行改造一下。
LayerControl.html
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>LayerControl</title> 6 </head> 7 <body> 8 <div class="sc-widget-title">圖層管理 9 <div class="sc-widget-bar"><span class="sc-widget-bar-close">×</span></div> 10 </div> 11 <div class="sc-widget-content"> 12 <ul class="sc-widget-tree"> 13 <li> 14 <div class="sc-widget-group"><span>圖元</span> 15 <button class="sc-widget-updatePrimitiveLayers">刷新</button> 16 </div> 17 <dl> 18 <dd data-bind="foreach: primitiveLayers"> 19 <div class="sc-widget-treeNode sc-widget-item"> 20 <label><input type="checkbox" data-bind="checked: show"><span 21 data-bind="text: name, attr: {title: name}"></span></label> 22 </div> 23 </dd> 24 </dl> 25 </li> 26 <li> 27 <div class="sc-widget-group"><span>實體</span> 28 <button class="sc-widget-updateEntityLayers">刷新</button> 29 </div> 30 <dl> 31 <dd data-bind="foreach: entityLayers"> 32 <div class="sc-widget-treeNode sc-widget-item"> 33 <label><input type="checkbox" data-bind="checked: show"><span 34 data-bind="text: name, attr: {title: name}"></span></label> 35 </div> 36 </dd> 37 </dl> 38 </li> 39 <li> 40 <div class="sc-widget-group"><span>影像</span> 41 <button class="sc-widget-updateImageryLayers">刷新</button> 42 </div> 43 <dl> 44 <dd data-bind="foreach: imageryLayers"> 45 <div class="sc-widget-treeNode sc-widget-item"> 46 <label><input type="checkbox" data-bind="checked: show"><span 47 data-bind="text: name, attr: {title: name}"></span></label> 48 <input type="range" min="0" max="1" step="0.01" data-bind="value: alpha, valueUpdate: 'input'"> 49 </div> 50 </dd> 51 </dl> 52 </li> 53 <li> 54 <div class="sc-widget-group"><span>地形</span> 55 <button class="sc-widget-updateTerrainLayers">刷新</button> 56 </div> 57 <dl> 58 <dd data-bind="foreach: terrainLayers"> 59 <div class="sc-widget-treeNode sc-widget-item"> 60 <label><input type="checkbox" data-bind="checked: show"><span 61 data-bind="text: name, attr: {title: name}"></span></label> 62 </div> 63 </dd> 64 </dl> 65 </li> 66 </ul> 67 </div> 68 </body> 69 </html>
LayerControl.css
1 .simpleCesium .sc-widget-layerControl .sc-widget-group button { 2 position: absolute; 3 right: 5px; 4 } 5 .simpleCesium .sc-widget-layerControl .sc-widget-item label{ 6 text-overflow: ellipsis; 7 overflow: hidden; 8 min-width: 120px; 9 /*max-width: 100px;*/ 10 } 11 .simpleCesium .sc-widget-layerControl .sc-widget-tree dd { 12 max-height: 150px; 13 overflow: auto; 14 }
小結
本篇實現了圖層控制器的最基本功能:實時展示當前所有的圖層數據,控制圖層顯示和隱藏,以及影像圖層的透明度調節。實現原理是利用knockout動態追蹤數據的屬性狀態。回頭看一下上面的代碼,真是極簡單的,這都要歸功於插件的基礎,所以這里還是強烈建議大家先看一下上一篇關於插件的實現和規范。
相關資源
GitHub地址:https://github.com/HelsingWang/simple-cesium
Demo地址:http://helsing.wang:8888/simple-cesium
Cesium深入淺出系列CSDN地址:https://blog.csdn.net/fywindmoon
Cesium深入淺出系列博客園地址:https://www.cnblogs.com/HelsingWang
交流群:854943530
