本章当中,我们主要来了解一下 Cesium
中的控件重写、事件应用、相机控制、量测工具与调试面板、以及与第三方库的集成
控件重写 本小节当中我们主要来了解一下如何在不修改源码的基础上对界面当中的某些控件进行扩展重写,我们先从 homeButton
组件开始看起
homeButton
功能在实际的应用场景中很常见而且功能也很实用,该组件的主要功能是返回到系统初始化时的位置,默认是整个球的位置,如下图
但是在实际的业务场景中,一般初始化范围都是某一个城市或园区的位置,如果使用 Cesium
自带的 homeButton
组件,就需要对其进行修改,使我们在点击 homeButton
时,相机不是定位到 Cesium
自带的默认位置,而是定位到我们想要的位置,针对于此我们只需要简单的两步
1 2 3 4 5 6 Cesium.Camera.DEFAULT_VIEW_RECTANGLE = Cesium.Rectangle.fromDegrees( 110.15 , 34.54 , 110.25 , 34.56 )
在 homeButton
的 viewModel
中添加监听事件
1 2 3 4 5 6 7 8 9 if (viewer.homeButton) { viewer.homeButton.viewModel.command.beforeExecute.addEventListener(function (e ) { e.cancel = true viewer.camera.flyTo({ destination: Cesium.Cartesian3.fromDegrees(117.16 , 32.71 , 15000.0 ), }) }) }
Geocoder 组件 Geocoder
是地理编码的意思,我们常用的 POI
搜索就是就是 Geocoder
的功劳,通过查看 Geocoder 源码 ,我们发现 Cesium
默认采用的是 Bing
地图服务来实现地理编码的功能,并且是通过 geocode
方法实现的,那么我们就可以通过覆写 geocoder
方法的方式来实现自定义的地理编码服务
下面我们来尝试重写 geocode
方法,将 Cesium
默认的 Bing
地图服务改为 OSM
地图服务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 function OpenStreetMapNominatimGeocoder ( ) {}OpenStreetMapNominatimGeocoder.prototype.geocode = function (input ) { var url = 'https://nominatim.openstreetmap.org/search' var resource = new Cesium.Resource({ url: url, queryParameters: { format: 'json' , q: input, }, }) return resource.fetchJson().then(function (results ) { var bboxDegrees return results.map(function (resultObject ) { bboxDegrees = resultObject.boundingbox return { displayName: resultObject.display_name, destination: Cesium.Rectangle.fromDegrees(bboxDegrees[2 ], bboxDegrees[0 ], bboxDegrees[3 ], bboxDegrees[1 ]), } }) }) } var viewer = new Cesium.Viewer('cesiumContainer' , { geocoder: new OpenStreetMapNominatimGeocoder(), })
BaseLayerPicker 组件 Cesium
为我们提供了默认的底图、地形图的选择面板,通过修改 baseLayerPicker
的属性 ture
或 false
来控制显隐,通过选择面板中的底图或地形图来实现对应图层的切换与显示,Cesium
提供的默认选择面板如下图所
这些图层都是在线的资源,如果是离线环境,或者是只显示客户提供的几个图层数据,我们该如何实现呢,要实现这个功能我们首先需要了解一下 BaseLayerPicker
的主要逻辑关系图,如下图
从上图我们可以看出,对于开发者而言,要实现不同的 ImageryProvider
,只需要提供不同的 ProviderViewModel
,比如 BingMap
、OSM
、ArcGIS
、GoogleMaps
等,这样在 BaseLayerPicker
的 UI
中,就会有多个 Provider
供用户选择,而交互则由 BaseLayerPickerViewModel
类负责,用户并不需要关心内部的实现,BaseLayerPickerViewModel
类已经帮我们都实现了
下面我们就利用 BaseLayerPicker
的逻辑关系,实现自定义的 ImageryProvider
(高德矢量图)和 TerrainPovider
(ArcGIS
地形),并将其显示在选择器面板中,下面为核心代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 var imageProviderVMs = []let gaodeImageProvider = new Cesium.UrlTemplateImageryProvider({ url: 'http://webrd0{s}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}' , subdomains: ['1' , '2' , '3' , '4' ], }) var gaodeVM = new Cesium.ProviderViewModel({ name: '高德矢量' , iconUrl: Cesium.buildModuleUrl('Widgets/Images/ImageryProviders/openStreetMap.png' ), tooltip: '高德矢量 地图服务' , creationFunction: function ( ) { return gaodeImageProvider }, }) imageProviderVMs.push(gaodeVM) viewer.baseLayerPicker.viewModel.imageryProviderViewModels = imageProviderVMs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 var terrainProviderVMs = []var terrainProvider = new Cesium.ArcGISTiledElevationTerrainProvider({ url: 'https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer' , token: 'KED1aF_I4UzXOHy3BnhwyBHU4l5oY6rO6walkmHoYqGp4XyIWUd5YZUC1ZrLAzvV40pR6gBXQayh0eFA8m6vPg..' , }) var arcgisVM = new Cesium.ProviderViewModel({ name: 'ArcGIS地形' , iconUrl: Cesium.buildModuleUrl('Widgets/Images/TerrainProviders/Ellipsoid.png' ), tooltip: 'ArcGIS地形服务' , creationFunction: function ( ) { return terrainProvider }, }) terrainProviderVMs.push(arcgisVM) viewer.baseLayerPicker.viewModel.terrainProviderViewModels = terrainProviderVMs
效果是下面这样的
事件应用 无论是前端系统,还是二维或是三维 GIS
应用系统,都离不开各种事件的应用,尤其是鼠标的单击、双击事件,Cesium
根据事件的类型、用途,将事件应用分成了三大类
一种是以鼠标操作(左键、中键、右键操作等)为主的 ScreenSpaceEventHandler
类
另一种是通用的事件类 Event
,该类通常在容器类内部实例化,并作为某个属性的类型直接被调用
比如 viewer.clock.onTick
、viewer.selectedEntityChanged
、camera.moveStart
、camera.moveEnd
、scene.preRender
、cesium3DTileset.allTilesLoaded
等这些属性都是 Event
类型
最后一种则是相机控制方面的事件类 screenSpaceCameraController
,该类通过与 CameraEventType
类配合实现相机的控制
下面我们来看几个比较常用的事件应用
鼠标事件 鼠标事件可以说是 GIS
系统里面关于事件应用最常用的一个了,点击地图上的某一个 graphic
,并获取其属性信息,就是鼠标事件应用最熟悉的一个场景了,Cesium
为实现这一功能,分成了几个过程
首先传递 viewer.canvas
参数实例化 ScreenSpaceEventHandler
类,比如实例化后的名称为 handler
,其次为 handler
注册鼠标事件的监听,最后在监听事件的回调方法中获取 event.position
,并将其作为参数执行 scene.pick
方法获取对应的选中对象
对 ScreenSpaceEventHandler
类进行实例化,注册事件、注销事件代码如下
1 2 3 4 5 6 7 8 9 10 11 var handler = new Cesium.ScreenSpaceEventHandler(viewer.canvas)let eventType = Cesium.ScreenSpaceEventType.LEFT_CLICKhandler.setInputAction(event => { console .log(event) }, eventType) handler.removeInputAction(eventType)
上面代码中的事件类型 eventType
直接采用了 ScreenSpaceEventType
中的常量,它的取值有以下这些
鼠标左键
事件类型
含义
LEFT_CLICK
单击
LEFT_DOUBLE__CLICK
双击
LEFT_DOWN
左键按下
LEFT UP
左键弹起
鼠标中键
事件类型
含义
MIDDLE_CLICK
单击
MIDDLE_DOWN
按下
MIDDLE_UP
弹起
鼠标右键
事件类型
含义
RIGHT_CLICK
单击
RIGHT_DOWN
按下
RIGHT_UP
弹起
双指触摸
事件类型
含义
PINCH_START
双指开始事件
PINCH_END
双指结束事件
PINCH_MOVE
双指更改事件
其他鼠标事件
事件类型
含义
MOUSE_MOVE
鼠标移动事件
WHEEL
鼠标滚轮事件
假如应用场景是点击要素获取其属性信息,这个时候就需要在鼠标左键的注册事件中获取 event
结果,核心代码如下
1 var picked = viewer.scene.pick(event.position)
这个时候就可以根据获取到的对象类型进行操作了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 if (Cesium.defined(picked)) { if (picked.id && picked.id instanceof Cesium.Entity) { console .log('选中了 Entity' ) } if (picked.primitive instanceof Cesium.Primitive) { console .log('选中了 Primitive' ) } if (picked.primitive instanceof Cesium.Model) { console .log('选中了模型' ) } if (picked instanceof Cesium.Cesium3DTileFeature) { console .log('选中了 3DTile' ) } }
Cesium
针对于通过 Entity
方式添加的几何图形,提供了一个非常方便的属性 selectedEntityChanged
(viewer
类事件类型的属性)来帮助我们获取选中的 Entity
,通过这个属性我们就不需要再注册鼠标事件了,示例代码如下
1 2 3 viewer.selectedEntityChanged.addEventListener(function (entity ) { console .log(entity.id) })
在某些场景中,我们可能需要跟踪某一辆车或某一个人员,这时我们可以把车辆或人员 Entity
赋给 viewer.trackedEntity
,这样一来相机就会自动跟踪我们所绑定的 Entity
了,实际场景中,我们并不是始终跟踪某一个车辆,有时需要切换到另一个车辆,当切换正在跟踪的车辆时,其实我们是触发了 viewer.trackedEntityChanged
事件,这样我们就可以在此事件中实时获取车辆行驶状态了
1 2 3 viewer.trackedEntityChanged.addEventListener(function (entity ) { console .log(entity.id) })
相机事件 相机控制事件类 screenSpaceCameraController
并不是像鼠标事件相关类 ScreenSpaceEventHandler
那样需要提前实例化,Cesium
在 Viewer
类的实例化过程中,也实例化了其他很多类,其中就包括 ScreenSpaceCameraController
类,并把实例化结果赋值给了 viewer.scene.screenSpaceCameraController
,所以我们直接去操作 viewer.scene.screenSpaceCameraController
就可以了
通过鼠标控制相机的方式取决于 CameraEventType
的常量,包括以下几种
事件类型
含义
LEFT_DRAG
按住鼠标左键,然后移动鼠标并释放按钮
MIDDLE_DRAG
按住鼠标中键,然后移动鼠标并释放按钮
PINCH
触摸表面.上的双指触摸
RIGHT_DRAG
按住鼠标右键,然后移动鼠标并释放按钮
WHEEL
滚动鼠标中键
其中,鼠标的默认操作如下
鼠标操作
3D
2D
Columbus 视角
左键 + 拖拽
旋转地球
在地图上移动
在地图上移动
右键 + 拖拽
缩放
缩放
缩放
中键滚轮
缩放
缩放
缩放
中键 + 拖拽
倾斜地球
无操作
倾斜地球
下面我们来尝试着修改默认的鼠标操作,实现中键缩放、右键旋转,核心代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 viewer.scene.screenSpaceCameraController.tiltEventTypes = [ Cesium.CameraEventType.RIGHT_DRAG, Cesium.CameraEventType.PINCH, { eventType: Cesium.CameraEventType.LEFT_DRAG, modifier: Cesium.KeyboardEventModifier.CTRL, }, { eventType: Cesium.CameraEventType.RIGHT_DRAG, modifier: Cesium.KeyboardEventModifier.CTRL, }, ] viewer.scene.screenSpaceCameraController.zoomEventTypes = [ Cesium.CameraEventType.MIDDLE_DRAG, Cesium.CameraEventType.WHEEL, Cesium.CameraEventType.PINCH, ]
主要是通过操作键盘实现相机的漫游,比如前进、后退、向上、向下等等,实现键盘漫游主要是通过键盘调用相机的 moveForward
、moveBackward
、moveLeft
、moveRight
、moveUp
、moveDown
方法,下面为部分核心代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 viewer.clock.onTick.addEventListener(function (clock ) { var camera = viewer.camera if (flags.looking) { var width = canvas.clientWidth var height = canvas.clientHeight var x = (mousePosition.x - startMousePosition.x) / width var y = -(mousePosition.y - startMousePosition.y) / height var lookFactor = 0.05 camera.lookRight(x * lookFactor) camera.lookUp(y * lookFactor) } var cameraHeight = ellipsoid.cartesianToCartographic(camera.position).height var moveRate = cameraHeight / 100.0 if (flags.moveForward) { camera.moveForward(moveRate) } if (flags.moveBackward) { camera.moveBackward(moveRate) } if (flags.moveUp) { camera.moveUp(moveRate) } if (flags.moveDown) { camera.moveDown(moveRate) } if (flags.moveLeft) { camera.moveLeft(moveRate) } if (flags.moveRight) { camera.moveRight(moveRate) } })
场景渲染事件 场景渲染事件主要包括以下四种
scene.preUpdate
- 更新或呈现场景之前将引发的事件
scene.postUpdate
- 场景更新后以及渲染场景之前立即引发的事件
scene.preRender
- 场景更新后以及渲染场景之前将引发的事件
scene.postRender
- 渲染场景后立即引发的事件
事件的添加和移除代码示例如下
1 2 viewer.scene.preUpdate.addEventListender(callbackFunc) viewer.scene.preUpdate.removeEventListender(callbackFunc)
比如我们自己定义一个指北针、标签,都是可以在 scene.preRender
监听事件的回调函数中更新指北针状态或者是标签的位置信息,下面为部分核心代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 viewer.scene.scene.preRender.addEventListener(() => { if (positions instanceof Array && htmlSize instanceof Array ) { positions.map((ele, index ) => { const html = document .getElementById(`infoTip${index} ` ) if (html) { const canvasPosition = ConversionUtil.degreesToCartesian2(ele.x, ele.y, ele.z) if (canvasPosition) { html.style.top = `${canvasPosition.y - htmlSize[index].offsetHeight} px` html.style.left = `${canvasPosition.x - htmlSize[index].offsetWidth} px` } } }) } })
相机控制 相机控制主要是用于相机的飞行定位,例如系统初始化位置定位、视点切换、设备定位、报警事件定位等,这些都是通过对相机进行操作实现的,Cesium
虽然提供了很多种方法用于实现相机的飞行定位,但这些方法都是基于 Viewer
、Camera
这两个类实现的
Viewer 类 在 Viewer
类里面有两个方法用于实现相机的飞行定位,分别是 flyTo
和 zoomTo
方法
这两个方法第一个参数都是 target
,类型可以是 Entity
、DataSource
、ImageryLayer
、Cesium3DTileset
等
flyTo
方法的第二个参数是 options
,它是一个包含 duration
(飞行持续时间)、maximumHeight
(飞行中的最大高度)、offset
(HeadingPitchRange
类型)的对象
而 zoomTo
方法的第二个参数是 offset
,即上面 options
中的 offset
,也是一个 HeadingPitchRange
类型的对象
Viewer
类中的相机定位方法如下图所示
Camera类 Camera
类对应的相机定位方法比较多,包括下图当中所示的五个方法,每个方法的参数及参数类型都用不同的颜色区分开来,其中 options
对象参数只列出了主要的属性,更多参数可以查阅官方的 API 文档 来了解更多
不管是 Viewer
类还是 Camera
类中的方法都能实现相机的定位功能,根据实际情况选择其一或组合使用,其中 viewer.flyTo()
、camera.flyTo()
、camera.flyToBoundingSphere()
这三个方法会有一个飞行动画的效果,所以会有飞行持续时间参数 duration
,默认是 3
秒
相机参数 我们不管使用哪种方式,基本上都是先确定相机要飞到的某一个位置,如点、矩形、包围球等,然后再结合相机的三个参数 heading
、pitch
、roll
或 range
实现某一位置视角的计算,下面主要介绍这三个参数
我们知道 Cesium
使用的是笛卡尔空间直角坐标系,其中 X
、Y
、Z
三个轴的正方向如下图所示
相机的三个参数 heading
、pitch
、roll
的值是针对于坐标轴旋转的弧度数,示意图如下所示
绕负 Z
轴旋转,顺时针为正,默认为正北方向 0
,其中正角向东增加,控制机体头的朝向位置,即左右方向的改变
绕负 Y
轴旋转,顺时针为正,默认为俯视 -90
,正俯仰角在飞机上方,负俯仰角在飞机下方,可简单理解成前空翻、后空翻
绕正 X
轴旋转,顺时针为正,默认为 0
,可简单理解成侧空翻
代表相机距离目标的距离
示例 比如针对于某一矩形范围 [110.2, 35.6, 112.3, 36.7]
,要求相机倾斜 25
度,实现矩形位置飞行定位的话,有两种方法实现,如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 var entity = viewer.entities.add({ rectangle: { coordinates: rect, material: Cesium.Color.GREEN.withAlpha(1.0 ), height: 10.0 , outline: false , }, }) var heading = Cesium.Math.toRadians(0.0 )var pitch = Cesium.Math.toRadians(-25.0 )var roll = Cesium.Math.toRadians(0 )var range = 0 viewer.flyTo(entity, { offset: new Cesium.HeadingPitchRange(heading, pitch, range), })
效果是下面这样的
1 2 3 4 5 6 var rect = Cesium.Rectangle.fromDegrees(110.2 , 35.6 , 112.3 , 36.7 )viewer.camera.flyTo({ destination: rect, orientation: new Cesium.HeadingPitchRoll(heading, pitch, roll), })
效果是下面这样的
但是运行后我们可以发现,第二种方法会显示定位错误,但鼠标稍微往上拖拽一下,也能看到绿色的矩形,那么这是什么原因造成的呢?其实主要问题是出在 pitch
值不是相机的默认值
当相机的 pitch
不是默认值的时候,就会出现相机定位的位置不在屏幕中心的问题,但这时候使用 viewer.flyTo()
方法来实现定位就能解决此问题,所以建议采用方法一实现相机的飞行定位
量测工具 对于量测工具来说,不管在二维 GIS
还是三维 GIS
中都是必须具备的功能,只不过是在空间上是否有贴地、是否有高度上的距离差别之分,Cesium
是三维 GIS
引擎,所以距离量测支持直线距离、水平距离、垂直距离以及地表距离,面积量测支持水平面积、地表面积以及模型表面积等,不管是哪种类型的距离测量还是面积测量,实现思路基本是一样的,都是按照如下思路实现的
点击按钮开始测量,侦听鼠标 LEFT_CLICK
事件,记录坐标,绘制节点和折线(多边形)
侦听鼠标移动事件,鼠标点击后即复制一个浮动点,在 MOUSE_MOVE
事件中不断更新最后一个浮动点,动态更新折线(多边形)绘制
侦听鼠标右击事件,RIGHT_CLICK
触发时销毁测量相关事件句柄(ScreenSpaceEventHandler
),删除多余的浮动点
折线(多边形)的动态绘制通过 CallbackProperty
属性绑定 positions
属性实现
下面是量测的实现代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 <!DOCTYPE html> <head > <title > 距离量测</title > <script src ="../Build/Cesium/Cesium.js" > </script > <link href ="../Build/Cesium/Widgets/widgets.css" rel ="stylesheet" /> <style > html, body, #cesiumContainer { width: 100%; height: 100%; margin: 0; padding: 0; overflow: hidden; } </style > </head > <body > <div id ="cesiumContainer" > </div > <script > var viewer = new Cesium.Viewer('cesiumContainer' , { animation: false , baseLayerPicker: true , fullscreenButton: false , vrButton: false , geocoder: false , homeButton: true , infoBox: false , sceneModePicker: false , selectionIndicator: false , timeline: false , navigationHelpButton: false , navigationInstructionsInitiallyVisible: false , }) viewer._cesiumWidget._creditContainer.style.display = 'none' var handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas) measureLineSpace(viewer, handler) function measureLineSpace (viewer, handler ) { viewer.cesiumWidget.screenSpaceEventHandler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK) handler = new Cesium.ScreenSpaceEventHandler(viewer.scene._imageryLayerCollection) var positions = [] var poly = null var distance = 0 var cartesian = null var floatingPoint handler.setInputAction(function (movement ) { let ray = viewer.camera.getPickRay(movement.endPosition) cartesian = viewer.scene.globe.pick(ray, viewer.scene) if (positions.length >= 2 ) { if (!Cesium.defined(poly)) { poly = new PolyLinePrimitive(positions) } else { positions.pop() positions.push(cartesian) } distance = getSpaceDistance(positions) } }, Cesium.ScreenSpaceEventType.MOUSE_MOVE) handler.setInputAction(function (movement ) { let ray = viewer.camera.getPickRay(movement.position) cartesian = viewer.scene.globe.pick(ray, viewer.scene) if (positions.length == 0 ) { positions.push(cartesian.clone()) } positions.push(cartesian) var textDisance = distance + '米' floatingPoint = viewer.entities.add({ name: '空间直线距离' , position: positions[positions.length - 1], point: { pixelSize: 5, color: Cesium.Color.RED, outlineColor: Cesium.Color.WHITE, outlineWidth: 2, }, label: { text: textDisance, font: '18px sans-serif' , fillColor: Cesium.Color.GOLD, style: Cesium.LabelStyle.FILL_AND_OUTLINE, outlineWidth: 2, verticalOrigin: Cesium.VerticalOrigin.BOTTOM, pixelOffset: new Cesium.Cartesian2(20 , -20 ), }, }) }, Cesium.ScreenSpaceEventType.LEFT_CLICK) handler.setInputAction(function (movement ) { handler.destroy() positions.pop() }, Cesium.ScreenSpaceEventType.RIGHT_CLICK) var PolyLinePrimitive = (function ( ) { function _ (positions ) { this .options = { name: '直线' , polyline: { show: true , positions: [], material: Cesium.Color.CHARTREUSE, width: 10, clampToGround: true , }, } this .positions = positions this ._init() } _.prototype._init = function ( ) { var _self = this var _update = function ( ) { return _self.positions } this .options.polyline.positions = new Cesium.CallbackProperty(_update, false ) viewer.entities.add(this .options) } return _ })() function getSpaceDistance (positions ) { var distance = 0 for (var i = 0 ; i < positions.length - 1 ; i++) { var point1cartographic = Cesium.Cartographic.fromCartesian(positions[i]) var point2cartographic = Cesium.Cartographic.fromCartesian(positions[i + 1 ]) var geodesic = new Cesium.EllipsoidGeodesic() geodesic.setEndPoints(point1cartographic, point2cartographic) var s = geodesic.surfaceDistance s = Math .sqrt(Math .pow(s, 2 ) + Math .pow(point2cartographic.height - point1cartographic.height, 2 )) distance = distance + s } return distance.toFixed(2 ) } } function measureAreaSpace (viewer, handler ) { viewer.cesiumWidget.screenSpaceEventHandler.removeInputAction(Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK) handler = new Cesium.ScreenSpaceEventHandler(viewer.scene._imageryLayerCollection) var positions = [] var tempPoints = [] var polygon = null var cartesian = null var floatingPoint handler.setInputAction(function (movement ) { let ray = viewer.camera.getPickRay(movement.endPosition) cartesian = viewer.scene.globe.pick(ray, viewer.scene) if (positions.length >= 2 ) { if (!Cesium.defined(polygon)) { polygon = new PolygonPrimitive(positions) } else { positions.pop() positions.push(cartesian) } } }, Cesium.ScreenSpaceEventType.MOUSE_MOVE) handler.setInputAction(function (movement ) { let ray = viewer.camera.getPickRay(movement.position) cartesian = viewer.scene.globe.pick(ray, viewer.scene) if (positions.length == 0 ) { positions.push(cartesian.clone()) } positions.push(cartesian) var cartographic = Cesium.Cartographic.fromCartesian(positions[positions.length - 1 ]) var longitudeString = Cesium.Math.toDegrees(cartographic.longitude) var latitudeString = Cesium.Math.toDegrees(cartographic.latitude) var heightString = cartographic.height tempPoints.push({ lon: longitudeString, lat: latitudeString, hei: heightString, }) floatingPoint = viewer.entities.add({ name: '多边形面积' , position: positions[positions.length - 1], point: { pixelSize: 5, color: Cesium.Color.RED, outlineColor: Cesium.Color.WHITE, outlineWidth: 2, heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, }, }) }, Cesium.ScreenSpaceEventType.LEFT_CLICK) handler.setInputAction(function (movement ) { handler.destroy() positions.pop() var textArea = getArea(tempPoints) + '平方公里' viewer.entities.add({ name: '多边形面积' , position: positions[positions.length - 1], label: { text: textArea, font: '18px sans-serif' , fillColor: Cesium.Color.GOLD, style: Cesium.LabelStyle.FILL_AND_OUTLINE, outlineWidth: 2, verticalOrigin: Cesium.VerticalOrigin.BOTTOM, pixelOffset: new Cesium.Cartesian2(20 , -40 ), heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, }, }) }, Cesium.ScreenSpaceEventType.RIGHT_CLICK) var radiansPerDegree = Math .PI / 180.0 var degreesPerRadian = 180.0 / Math .PI function getArea (points ) { var res = 0 for (var i = 0 ; i < points.length - 2 ; i++) { var j = (i + 1 ) % points.length var k = (i + 2 ) % points.length var totalAngle = Angle(points[i], points[j], points[k]) var dis_temp1 = distance(positions[i], positions[j]) var dis_temp2 = distance(positions[j], positions[k]) res += dis_temp1 * dis_temp2 * Math .abs(Math .sin(totalAngle)) console .log(res) } return (res / 1000000.0 ).toFixed(4 ) } function Angle (p1, p2, p3 ) { var bearing21 = Bearing(p2, p1) var bearing23 = Bearing(p2, p3) var angle = bearing21 - bearing23 if (angle < 0 ) { angle += 360 } return angle } function Bearing (from, to ) { var lat1 = from .lat * radiansPerDegree var lon1 = from .lon * radiansPerDegree var lat2 = to.lat * radiansPerDegree var lon2 = to.lon * radiansPerDegree var angle = -Math .atan2( Math .sin(lon1 - lon2) * Math .cos(lat2), Math .cos(lat1) * Math .sin(lat2) - Math .sin(lat1) * Math .cos(lat2) * Math .cos(lon1 - lon2) ) if (angle < 0 ) { angle += Math .PI * 2.0 } angle = angle * degreesPerRadian return angle } var PolygonPrimitive = (function ( ) { function _ (positions ) { this .options = { name: '多边形' , polygon: { hierarchy: [], material: Cesium.Color.GREEN.withAlpha(0.5), }, } this .hierarchy = { positions } this ._init() } _.prototype._init = function ( ) { var _self = this var _update = function ( ) { return _self.hierarchy } this .options.polygon.hierarchy = new Cesium.CallbackProperty(_update, false ) viewer.entities.add(this .options) } return _ })() function distance (point1, point2 ) { var point1cartographic = Cesium.Cartographic.fromCartesian(point1) var point2cartographic = Cesium.Cartographic.fromCartesian(point2) var geodesic = new Cesium.EllipsoidGeodesic() geodesic.setEndPoints(point1cartographic, point2cartographic) var s = geodesic.surfaceDistance s = Math .sqrt(Math .pow(s, 2 ) + Math .pow(point2cartographic.height - point1cartographic.height, 2 )) return s } } </script > </body >
距离测量效果如下图所示
面积测量效果如下图所示
调试面板 Cesium
中比较常用的调试面板是用于了解 Cesium
渲染效果以及性能调优的 CesiumInspector
和用于监视 3D Tiles
数据的监视器 Cesium3DTilesInspector
该控件是针对开发人员来说,虽然不能提供功能的实现,但对于了解渲染效果和性能调优是非常有帮助的,特别是解决一些渲染状态下的问题时非常的有价值,使用该控件非常的简单,只需如下一行代码就能实现该控件的加载
1 viewer.extend(Cesium.viewerCesiumInspectorMixin)
控件里面有很多的功能,包括渲染帧数、Primitive
外包围球、Primitive
参考框架、线框模式等等,这里我们就不详细展开了,详细可以参考官方文档当中的 new Cesium.CesiumInspector(container, scene)
面对大场景下的大规模、大体量的 3D Tiles
数据,Cesium
提供了一个监视 3D Tiles
数据的监视器,用于监视、观察 3D Tiles
数据的效果,加载该空间也非常的简单,只需一行代码即可,结果控件展示如下
1 viewer.extend(Cesium.viewerCesium3DTilesInspectorMixin)
主要包括
3D Tiles
瓦片是否可拾取
显示颜色、线框、瓦片边界范围框、瓦片内容边界范围框、观察者请求体、点云渲染等
一些动态屏幕误差设置、最大屏幕误差设置、样式修改等
与第三方库的集成 这里我们主要简单的介绍 Cesium
与 Three.js
、Echarts
、Heatmap
和 Turf
是如何集成使用的,下面我们一个一个来进行了解
集成 Three.js Three.js
是基于原生 WebGL
封装运行的三维引擎库,在所有 WebGL
引擎中,Three.js
是国内文资料最多、使用最广泛的三维引擎,Three.js
可应用于 Web 3D
的可视化(如产品在线浏览、在线三维可视化等),微信小程序游戏,科教领域,机械领域,WebVR
(VR
看房、VR
看车等)以及家装室内设计等方面,是一个比较轻量级的跨浏览器 JavaScript
库 ,适合在浏览器中创建和显示动画 3D
计算机图形
将 Cesium
的行星级渲染和 GIS
功能与 Three.js
广泛而易用的通用 3D API
相结合,为新的 WebGL
体验开启了许多可能性,两者的集成总体思路如下
创建两个容器,分别用于显示 Cesium
和 Three.js
的场景
分别初始化两者各自的渲染器
调整两者的渲染频率保持一致
调整两者的相机位置角度保持一致
加入要展示的图形
以下展示了部分核心代码
1 2 <div id ="cesiumContainer" > </div > <div id ="ThreeContainer" > </div >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 cesium.viewer = new Cesium.Viewer('cesiumContainer' , { useDefaultRenderLoop: false , }) function initThree ( ) { let fov = 45 let width = window .innerWidth let height = window .innerHeight let aspect = width / height let near = 1 let far = 10 * 1000 * 1000 three.scene = new THREE.Scene() three.camera = new THREE.PerspectiveCamera(fov, aspect, near, far) three.renderer = new THREE.WebGLRenderer({ alpha : true }) ThreeContainer.appendChild(three.renderer.domElement) } function loop ( ) { requestAnimationFrame(loop) renderCesium() renderThreeObj() renderCamera() } function init3DObject ( ) { let entity = { name: 'Polygon' , polygon: { hierarchy: Cesium.Cartesian3.fromDegreesArray([ minWGS84[0 ], minWGS84[1 ], maxWGS84[0 ], minWGS84[1 ], maxWGS84[0 ], maxWGS84[1 ], minWGS84[0 ], maxWGS84[1 ], ]), material: Cesium.Color.RED.withAlpha(0.1 ), }, } let Polypon = cesium.viewer.entities.add(entity) let doubleSideMaterial = new THREE.MeshNormalMaterial({ side: THREE.DoubleSide, }) geometry = new THREE.SphereGeometry(1 , 32 , 32 ) let sphere = new THREE.Mesh( geometry, new THREE.MeshPhongMaterial({ color: 0xffffff , side: THREE.DoubleSide, }) ) }
效果图如下所示
集成 Echarts Echarts
是一个基于 JavaScript
的开源可视化图表库,具有丰富的图表类型,可用于地理数据可视化的地图、热力图、线图等,Cesium
通过与 Echarts
的地理数据可视化能力相结合,大大增强 Cesium
整体的可视化效果
我们这里通过封装 EchartsLayer
来实现迁徙图的效果,需要注意的是在图表的 option
配置项中不需要写 geo
,同时每个 series
数组中元素都必须加 coordinateSystem: 'GLMap'
,部分核心代码如下
1 2 3 4 5 6 7 8 9 10 var EchartsLayer = function (map, options ) { this ._map = map this ._overlay = this ._createChartOverlay() if (options) { this ._registerMap() } this ._overlay.setOption(options || {}) } let _echartLayer = new EchartsLayer(viewer, option)
实现的效果如下所示
集成 Heatmap heatmap.js 是一个轻量级的、最先进的用于表达热力图的可视化前端库,比如人群分布情况、污染物浓度变化情况、信号强度等
两者的集成比较简单,就是把使用 heatmap.js
生成的热力图,以贴图材质的方式赋给某个几何图形贴图属性即可,部分核心代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 var dataRaw = []for (var i = 0 ; i < len; i++) { var point = { lat: latMin + Math .random() * (latMax - latMin), lon: lonMin + Math .random() * (lonMax - lonMin), value: Math .floor(Math .random() * 100 ), } dataRaw.push(point) } for (var i = 0 ; i < len; i++) { var dataItem = dataRaw[i] var point = { x: Math .floor(((dataItem.lat - latMin) / (latMax - latMin)) * width), y: Math .floor(((dataItem.lon - lonMin) / (lonMax - lonMin)) * height), value: Math .floor(dataItem.value), } max = Math .max(max, dataItem.value) points.push(point) } var heatmapInstance = h337.create({ container: document .querySelector('.heatmap' ), }) var data = { max: max, data: points, } heatmapInstance.setData(data) var canvas = document .getElementsByClassName('heatmap-canvas' )viewer.entities.add({ name: 'heatmap' , rectangle: { coordinates: Cesium.Rectangle.fromDegrees(lonMin, latMin, lonMax, latMax), material: new Cesium.ImageMaterialProperty({ image: canvas[0 ], transparent: true , }), }, })
实现的效果如下所示
集成 Turf Cesium
本身更侧重于三维可视化,在空间分析方面会显得薄弱些,当然空间分析能力可以借助开源 postGIS
中的函数去实现,然后将结果通过 Cesium
去呈现,这里我们不对 postGIS
进行介绍,而是使用一个轻量级的用于空间分析的前端库,即 Turf
Turf
的定位是地理空间分析库,处理各种地图算法,特点是离线计算、模块化、快速,下面是一个计算两点之间的距离的简单示例
1 2 3 4 5 var point1 = turf.point([144.834823 , -37.771257 ])var point2 = turf.point([145.14244 , -37.830937 ])var midpoint = turf.midpoint(point1, point2)
而下面的截图是通过 Turf
、Cesium
实现的点、线、面缓冲区分析结果,即借助了 Turf
的空间分析能力和 Cesium
的可视化能力
部分核心代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 function initPointBuffer ( ) { let point = [106.422638966289 , 29.5698367125623 ] addPoint(point) let pointF = turf.point(point) let buffered = turf.buffer(pointF, 60 , { units : 'meters' }) let coordinates = buffered.geometry.coordinates let points = coordinates[0 ] let degreesArray = pointsToDegreesArray(points) addBufferPolyogn(Cesium.Cartesian3.fromDegreesArray(degreesArray)) } function addPoint (point ) { viewer.entities.add({ position: Cesium.Cartesian3.fromDegrees(point[0 ], point[1 ], 0 ), point: { pixelSize: 10 , heightReference: Cesium.HeightReference.CLAMP_TO_GROUND, color: Cesium.Color.YELLOW, outlineWidth: 3 , outlineColor: Cesium.Color.YELLOW.withAlpha(0.4 ), }, }) } function addBufferPolyogn (positions ) { viewer.entities.add({ polygon: { hierarchy: new Cesium.PolygonHierarchy(positions), material: Cesium.Color.RED.withAlpha(0.6 ), classificationType: Cesium.ClassificationType.BOTH, }, }) }
如果觉得比较麻烦的话,可以直接使用 CesiumVectorTile 这个封装好的库,它支持小数据量的 GeoJSON
、Shape
文件矢量动态切片,并且还能实现贴地效果