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来吧,这里是没有任何商业气息的纯技术分享群,队伍不断壮大中,期待你的加入。
posted on 2020-11-20 13:45 Helsing·Wang 阅读(5625) 评论(5) 编辑 收藏 举报