• 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)) + '",
    ';
    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) + '",
    ';
    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来吧,这里是没有任何商业气息的纯技术分享群,队伍不断壮大中,期待你的加入。

  • 相关阅读:
    从程序员到项目经理
    wumii 爆款总结经验
    快速的搭建JFinal的ORM框架示例
    Hibernate all-delete-orphan[转]
    HHvm Apache 2.4 Nginx建站环境搭建方法安装运行WordPress博客
    雷军是如何从程序员蜕变成职业经理人的
    Postgresql数据库数据简单的导入导出
    如何一年看50本好书?
    清除DNS解析缓存
    mysql 下 计算 两点 经纬度 之间的距离
  • 原文地址:https://www.cnblogs.com/HelsingWang/p/14010452.html
Copyright © 2020-2023  润新知