Cesium深入淺出之信息彈框


引子

信息彈框種類有很多,今天我們要說的是那種可以釘在地圖上的信息框,它具備一個地圖坐標,可以跟隨地圖移動,超出地圖范圍會被隱藏,讓人感覺它是地圖場景中的一部分。不過它還不是真正的地圖元素,它還只是個網頁元素而已,也就是說它始終是朝向屏幕平面的,而不是那種三維廣告板的效果,那種效果或許后續會做吧。

預期效果

這個效果其實是動態的,從底部到頂部逐漸顯現,不過GIF圖比較大就沒上傳了,看看最終的效果吧。

實現原理

原理真的很簡單,一句話可以描述,就是實時同步笛卡爾坐標(地圖坐標)和畫布(canvas)坐標,讓網頁元素始終保持在地圖坐標的某個點上,其他的操作都是HTML+CSS的基本操作了,來看具體的操作吧。

具體實現

代碼不多,我就直接給出完整的封裝了,不過要注意一下,我使用的是ES6封裝的,而且其中使用了某些新特性,比如私有變量,最好配合eslint轉碼,或者自行修改變量名稱吧。另外Cesium不是全局引用,而是在模塊中分別引用的,引用方式不同的小伙伴請自行添加Cesium前綴。

  1 // InfoTool.js
  2 
  3 // ====================
  4 // 引入模塊
  5 // ====================
  6 import Viewer from "cesium/Source/Widgets/Viewer/Viewer.js";
  7 import CesiumMath from "cesium/Source/Core/Math.js";
  8 import Cesium3DTileFeature from "cesium/Source/Scene/Cesium3DTileFeature.js";
  9 import Cartesian2 from "cesium/Source/Core/Cartesian2.js";
 10 import Cartesian3 from "cesium/Source/Core/Cartesian3.js";
 11 import Cartographic from "cesium/Source/Core/Cartographic.js";
 12 import SceneTransforms from "cesium/Source/Scene/SceneTransforms.js";
 13 import defined from "cesium/Source/Core/defined.js";
 14 import './info.css';
 15 
 16 // ====================
 17 //
 18 // ====================
 19 /**
 20  * 信息工具。
 21  *
 22  * @author Helsing
 23  * @date 2019/12/22
 24  * @alias InfoTool
 25  * @constructor
 26  * @param {Viewer} viewer Cesium視窗。
 27  */
 28 class InfoTool {
 29     /**
 30      * 創建一個動態實體彈窗。
 31      *
 32      * @param {Viewer} viewer Cesium視窗。
 33      * @param {Number} options 選項。
 34      * @param {Cartesian3} options.position 彈出位置。
 35      * @param {HTMLElement} options.element 彈出窗元素容器。
 36      * @param {Function} callback 回調函數。
 37      * @ignore
 38      */
 39     static #createInfoTool(viewer, options, callback = undefined) {
 40         const cartographic = Cartographic.fromCartesian(options.position);
 41         const lon = CesiumMath.toDegrees(cartographic.longitude); //.toFixed(5);
 42         const lat = CesiumMath.toDegrees(cartographic.latitude); //.toFixed(5);
 43 
 44         // 注意,這里不能使用hide()或者display,會導致元素一直重繪。
 45         util.setCss(options.element, "opacity", "0"); 
 46         util.setCss(options.element.querySelector("div:nth-child(1)"), "height", "0");
 47         util.setCss(options.element.querySelector("div:nth-child(2)"), "opacity", "0");
 48 
 49         // 回調
 50         callback();
 51 
 52         // 添加div彈窗
 53         setTimeout(function () {
 54             InfoTool.#popup(viewer, options.element, lon, lat, cartographic.height)
 55         }, 100);
 56     }
 57     /**
 58      * 彈出HTML元素彈窗。
 59      *
 60      * @param {Viewer} viewer Cesium視窗。
 61      * @param {Element|HTMLElement} element 彈窗元素。
 62      * @param {Number} lon 經度。
 63      * @param {Number} lat 緯度。
 64      * @param {Number} height 高度。
 65      * @ignore
 66      */
 67     static #popup(viewer, element, lon, lat, height) {
 68         setTimeout(function () {
 69             // 設置元素效果
 70             util.setCss(element, "opacity", "1");
 71             util.setCss(element.querySelector("div:nth-child(1)"), "transition", "ease 1s");
 72             util.setCss(element.querySelector("div:nth-child(2)"), "transition", "opacity 1s");
 73             util.setCss(element.querySelector("div:nth-child(1)"), "height", "80px");
 74             util.setCss(element.querySelector("div:nth-child(2)"), "pointer-events", "auto");
 75             window.setTimeout(function () {
 76                 util.setCss(element.querySelector("div:nth-child(2)"), "opacity", "1");
 77             }, 500);
 78         }, 100);
 79         const divPosition = Cartesian3.fromDegrees(lon, lat, height);
 80         InfoTool.#hookToGlobe(viewer, element, divPosition, [10, -(parseInt(util.getCss(element, "height")))], true);
 81         viewer.scene.requestRender();
 82     }
 83     /**
 84      * 將HTML彈窗掛接到地球上。
 85      *
 86      * @param {Viewer} viewer Cesium視窗。
 87      * @param {Element} element 彈窗元素。
 88      * @param {Cartesian3} position 地圖坐標點。
 89      * @param {Array} offset 偏移。
 90      * @param {Boolean} hideOnBehindGlobe 當元素在地球背面會自動隱藏,以減輕判斷計算壓力。
 91      * @ignore
 92      */
 93     static #hookToGlobe(viewer, element, position, offset, hideOnBehindGlobe) {
 94         const scene = viewer.scene, camera = viewer.camera;
 95         const cartesian2 = new Cartesian2();
 96         scene.preRender.addEventListener(function () {
 97             const canvasPosition = scene.cartesianToCanvasCoordinates(position, cartesian2); // 笛卡爾坐標到畫布坐標
 98             if (defined(canvasPosition)) {
 99                 util.setCss(element, "left", parseInt(canvasPosition.x + offset[0]) + "px");
100                 util.setCss(element, "top", parseInt(canvasPosition.y + offset[1]) + "px");
101 
102                 // 是否在地球背面隱藏
103                 if (hideOnBehindGlobe) {
104                     const cameraPosition = camera.position;
105                     let height = scene.globe.ellipsoid.cartesianToCartographic(cameraPosition).height;
106                     height += scene.globe.ellipsoid.maximumRadius;
107                     if (!(Cartesian3.distance(cameraPosition, position) > height)) {
108                         util.setCss(element, "display", "flex");
109                     } else {
110                         util.setCss(element, "display", "none");
111                     }
112                 }
113             }
114         });
115     }
116 
117     #element;
118     viewer;
119 
120     constructor(viewer) {
121         this.viewer = viewer;
122 
123         // 在Cesium容器中添加元素
124         this.#element = document.createElement("div");
125         this.#element.id = "infoTool_" + util.getGuid(true);
126         this.#element.name = "infoTool";
127         this.#element.classList.add("helsing-three-plugins-infotool");
128         this.#element.appendChild(document.createElement("div"));
129         this.#element.appendChild(document.createElement("div"));
130         viewer.container.appendChild(this.#element);
131     }
132 
133     /**
134      * 添加。
135      *
136      * @author Helsing
137      * @date 2019/12/22
138      * @param {Object} options 選項。
139      * @param {Element} options.element 彈窗元素。
140      * @param {Cartesian2|Cartesian3} options.position 點擊位置。
141      * @param {Cesium3DTileFeature} [options.inputFeature] 模型要素。
142      * @param {String} options.type 類型(默認值為default,即任意點擊模式;如果設置為info,即信息模式,只有點擊Feature才會響應)。
143      * @param {String} options.content 內容(只有類型為default時才起作用)。
144      * @param {Function} callback 回調函數。
145      */
146     add(options, callback = undefined) {
147         // 判斷參數為空返回
148         if (!options) {
149             return;
150         }
151         //
152         let position, cartesian2d, cartesian3d, inputFeature;
153         if (options instanceof Cesium3DTileFeature) {
154             inputFeature = options;
155             options = {};
156         } else {
157             if (options instanceof Cartesian2 || options instanceof Cartesian3) {
158                 position = options;
159                 options = {};
160             } else {
161                 position = options.position;
162                 inputFeature = options.inputFeature;
163             }
164             // 判斷點位為空返回
165             if (!position) {
166                 return;
167             }
168             if (position instanceof Cartesian2) { // 二維轉三維
169                 // 如果支持拾取模型則取模型值
170                 cartesian3d = (this.viewer.scene.pickPositionSupported && defined(this.viewer.scene.pick(options.position))) ?
171                     this.viewer.scene.pickPosition(position) : this.viewer.camera.pickEllipsoid(position, this.viewer.scene.globe.ellipsoid);
172                 cartesian2d = position;
173             } else {
174                 cartesian3d = position;
175                 cartesian2d = SceneTransforms.wgs84ToWindowCoordinates(this.viewer.scene, cartesian3d);
176             }
177             // 判斷點位為空返回
178             if (!cartesian3d) {
179                 return;
180             }
181         }
182 
183         const that = this;
184 
185         // 1.組織信息
186         let info = '';
187             if (options.type === "info") {
188             // 拾取要素
189             const feature = inputFeature || this.viewer.scene.pick(cartesian2d);
190             // 判斷拾取要素為空返回
191             if (!defined(feature)) {
192                 this.remove();
193                 return;
194             }
195 
196             if (feature instanceof Cesium3DTileFeature) { // 3dtiles
197                 let propertyNames = feature.getPropertyNames();
198                 let length = propertyNames.length;
199                 for (let i = 0; i < length; ++i) {
200                     let propertyName = propertyNames[i];
201                     info += '"' + (propertyName + '": "' + feature.getProperty(propertyName)) + '",\n';
202                 }
203             } else if (feature.id) { // Entity
204                 const properties = feature.id.properties;
205                 if (properties) {
206                     let propertyNames = properties._propertyNames;
207                     let length = propertyNames.length;
208                     for (let i = 0; i < length; ++i) {
209                         let propertyName = propertyNames[i];
210                         //console.log(propertyName + ': ' + properties[propertyName]._value);
211                         info += '"' + (propertyName + '": "' + properties[propertyName]._value) + '",\n';
212                     }
213                 }
214             }
215         } else {
216             options.content && (info = options.content);
217         }
218 
219         // 2.生成特效
220         // 添加之前先移除
221         this.remove();
222 
223         if (!info) {
224             return;
225         }
226 
227         options.position = cartesian3d;
228         options.element = options.element || this.#element;
229 
230         InfoTool.#createInfoTool(this.viewer, options, function () {
231             util.setInnerText(that.#element.querySelector("div:nth-child(2)"), info);
232             typeof callback === "function" && callback();
233         });
234     }
235 
236     /**
237      * 移除。
238      *
239      * @author Helsing
240      * @date 2020/1/18
241      */
242     remove(entityId = undefined) {
243         util.setCss(this.#element, "opacity", "0");
244         util.setCss(this.#element.querySelector("div:nth-child(1)"), "transition", "");
245         util.setCss(this.#element.querySelector("div:nth-child(2)"), "transition", "");
246         util.setCss(this.#element.querySelector("div:nth-child(1)"), "height", "0");
247         util.setCss(this.#element.querySelector("div:nth-child(2)"), "pointer-events", "none");
248     };
249 }
250 
251 export default InfoTool;

上述代碼中用到了util.setCss等函數,都是自己封裝的,小伙伴們可以自己實現也可以用我的。

  1 /**
  2  * 設置CSS。
  3  *
  4  * @author Helsing
  5  * @date 2019/11/12
  6  * @param {Element|HTMLElement|String} srcNodeRef 元素ID、元素或數組。
  7  * @param {String} property 屬性。
  8  * @param {String} value 值。
  9  */
 10 setCss: function (srcNodeRef, property, value) {
 11     if (srcNodeRef) {
 12         if (srcNodeRef instanceof Array && srcNodeRef.length > 0) {
 13             for (let i = 0; i < srcNodeRef.length; i++) {
 14                 srcNodeRef[i].style.setProperty(property, value);
 15             }
 16         } else if (typeof (srcNodeRef) === "string") {
 17             if (srcNodeRef.indexOf("#") < 0 && srcNodeRef.indexOf(".") < 0 && srcNodeRef.indexOf(" ") < 0) {
 18                 const element = document.getElementById(srcNodeRef);
 19                 element && (element.style.setProperty(property, value));
 20             } else {
 21                 const elements = document.querySelectorAll(srcNodeRef);
 22                 for (let i = 0; i < elements.length; i++) {
 23                     elements[i].style.setProperty(property, value);
 24                 }
 25             }
 26         } else if (srcNodeRef instanceof HTMLElement) {
 27             srcNodeRef.style.setProperty(property, value);
 28         }
 29     }
 30 },
 31 
 32 /**
 33  * 設置元素的值。
 34  *
 35  * @author Helsing
 36  * @date 2019/11/12
 37  * @param {String|HTMLElement|Array} srcNodeRef 元素ID、元素或數組。
 38  * @param {String} value 值。
 39  */
 40 setInnerText: function (srcNodeRef, value) {
 41     if (srcNodeRef) {
 42         if (srcNodeRef instanceof Array && srcNodeRef.length > 0) {
 43             const that = this;
 44             for (let i = 0; i < srcNodeRef.length; i++) {
 45                 let element = srcNodeRef[i];
 46                 if (that.isElement(element)) {
 47                     element.innerText = value;
 48                 }
 49             }
 50         } else if (typeof (srcNodeRef) === "string") {
 51             if (srcNodeRef.indexOf("#") < 0 && srcNodeRef.indexOf(".") < 0 && srcNodeRef.indexOf(" ") < 0) {
 52                 let element = document.getElementById(srcNodeRef);
 53                 element && (element.innerText = value);
 54             } else {
 55                 const elements = document.querySelectorAll(srcNodeRef);
 56                 for (let i = 0; i < elements.length; i++) {
 57                     elements[i].innerText = value;
 58                 }
 59             }
 60         } else {
 61             if (this.isElement(srcNodeRef)) {
 62                 srcNodeRef.innerText = value;
 63             }
 64         }
 65     }
 66 },
 67 
 68 /**
 69  * 判斷對象是否為元素。
 70  *
 71  * @author Helsing
 72  * @date 2019/12/24
 73  * @param {Object} obj 對象。
 74  * @returns {Boolean} 是或否。
 75  */
 76 isElement: function (obj) {
 77     return (typeof HTMLElement === 'object')
 78         ? (obj instanceof HTMLElement)
 79         : !!(obj && typeof obj === 'object' && (obj.nodeType === 1 || obj.nodeType === 9) && typeof obj.nodeName === 'string');
 80 },
 81 
 82 /**
 83  * 獲取全球唯一ID。
 84  *
 85  * @author Helsing
 86  * @date 2019/11/21
 87  * @param {Boolean} removeMinus 是否去除“-”號。
 88  * @returns {String} GUID。
 89  */
 90 getGuid: function (removeMinus) {
 91     let d = new Date().getTime();
 92     let uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
 93         const r = (d + Math.random() * 16) % 16 | 0;
 94         d = Math.floor(d / 16);
 95         return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
 96     });
 97     if (removeMinus) {
 98         uuid = uuid.replace(/-/g, "");
 99     }
100     return uuid;
101 }

另外給出css樣式

1 .helsing-three-plugins-infotool { display: none; flex-direction: column-reverse; position: fixed; top: 0; left: 0;min-width: 100px; height: 250px; user-select: none; pointer-events: none; }
2     .helsing-three-plugins-infotool > div:nth-child(1) { left: 0; width: 40px; height: 0; bottom: 0; background: url("popup_line.png") no-repeat center 100%; }
3     .helsing-three-plugins-infotool > div:nth-child(2) { opacity: 0; box-shadow: 0 0 8px 0 rgba(0, 170, 255, .6) inset; padding: 20px; user-select: text; pointer-events: auto; }

上述代碼很簡單,雖然注釋不多,但我相信小伙伴們一眼就能懂了,這里只講兩個關鍵的地方。

第一個地方,hookToGlobe方法,這也是全篇最重要的一個點了。Cesium和網頁元素是兩個不相干的東西,它們的唯一紐帶就是Canvas,因為Canvas也是網頁元素,所以同步div和Canvas的坐標位置即可實現彈窗釘在地圖上,而且這個同步是要實時的,這就須要不斷的刷新,我們使用Cesium的preRender事件來實現。cartesianToCanvasCoordinates將地圖笛卡爾坐標轉換為畫布坐標,然后設置div的top和left樣式,即完成了坐標位置實時同步工作。

第二個地方,add方法。現在彈窗已經有了,那么里面的信息如何獲取呢,有一點基礎的童鞋都知道要使用pick,pick之后會返回一個Feature對象,這個對象里面包含着屬性信息,這里要區分一下模型和實體,它們的獲取方法不同,模型使用feature.getProperty方法獲取,實體使用feature.id.properties[propertyName]._value屬性值獲取。最后遍歷一下字段名稱和屬性值,組織成json格式的數據呈現,或者可以使用表格控件來呈現。

小結

這是一個沒什么難度但很實用的功能,而且樣式可以隨意定制,只要你懂css就行,比Cesium自帶的信息彈框好靈活多了吧。不出意外的話,下一篇會更新模型壓平,說實話現在還沒開始研究呢,等着我現學現賣吧,希望別打臉。

PS

想要了解更多更好玩的東西就到群854943530來吧,這里是沒有任何商業氣息的純技術分享群,隊伍不斷壯大中,期待你的加入。


免責聲明!

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



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