Cesium 中的事件与量测工具

Cesium 中的事件与量测工具

本章当中,我们主要来了解一下 Cesium 中的控件重写、事件应用、相机控制、量测工具与调试面板、以及与第三方库的集成

控件重写

本小节当中我们主要来了解一下如何在不修改源码的基础上对界面当中的某些控件进行扩展重写,我们先从 homeButton 组件开始看起

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
)
  • homeButtonviewModel 中添加监听事件
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 的属性 turefalse 来控制显隐,通过选择面板中的底图或地形图来实现对应图层的切换与显示,Cesium 提供的默认选择面板如下图所

这些图层都是在线的资源,如果是离线环境,或者是只显示客户提供的几个图层数据,我们该如何实现呢,要实现这个功能我们首先需要了解一下 BaseLayerPicker 的主要逻辑关系图,如下图

从上图我们可以看出,对于开发者而言,要实现不同的 ImageryProvider,只需要提供不同的 ProviderViewModel,比如 BingMapOSMArcGISGoogleMaps 等,这样在 BaseLayerPickerUI 中,就会有多个 Provider 供用户选择,而交互则由 BaseLayerPickerViewModel 类负责,用户并不需要关心内部的实现,BaseLayerPickerViewModel 类已经帮我们都实现了

下面我们就利用 BaseLayerPicker 的逻辑关系,实现自定义的 ImageryProvider(高德矢量图)和 TerrainPoviderArcGIS 地形),并将其显示在选择器面板中,下面为核心代码

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.onTickviewer.selectedEntityChangedcamera.moveStartcamera.moveEndscene.preRendercesium3DTileset.allTilesLoaded 等这些属性都是 Event 类型
  • 最后一种则是相机控制方面的事件类 screenSpaceCameraController,该类通过与 CameraEventType 类配合实现相机的控制

下面我们来看几个比较常用的事件应用

鼠标事件

鼠标事件可以说是 GIS 系统里面关于事件应用最常用的一个了,点击地图上的某一个 graphic,并获取其属性信息,就是鼠标事件应用最熟悉的一个场景了,Cesium 为实现这一功能,分成了几个过程

首先传递 viewer.canvas 参数实例化 ScreenSpaceEventHandler 类,比如实例化后的名称为 handler,其次为 handler 注册鼠标事件的监听,最后在监听事件的回调方法中获取 event.position,并将其作为参数执行 scene.pick 方法获取对应的选中对象

  • ScreenSpaceEventHandler

ScreenSpaceEventHandler 类进行实例化,注册事件、注销事件代码如下

1
2
3
4
5
6
7
8
9
10
11
var handler = new Cesium.ScreenSpaceEventHandler(viewer.canvas)

let eventType = Cesium.ScreenSpaceEventType.LEFT_CLICK

// 注册事件
handler.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')
}
}
  • Entity 选择

Cesium 针对于通过 Entity 方式添加的几何图形,提供了一个非常方便的属性 selectedEntityChangedviewer 类事件类型的属性)来帮助我们获取选中的 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 那样需要提前实例化,CesiumViewer 类的实例化过程中,也实例化了其他很多类,其中就包括 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,
]
  • 通过键盘控制

主要是通过操作键盘实现相机的漫游,比如前进、后退、向上、向下等等,实现键盘漫游主要是通过键盘调用相机的 moveForwardmoveBackwardmoveLeftmoveRightmoveUpmoveDown 方法,下面为部分核心代码

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

// Coordinate (0.0, 0.0) will be where the mouse was clicked.
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)
}

// Change movement speed based on the distance of the camera to the surface of the ellipsoid.
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 虽然提供了很多种方法用于实现相机的飞行定位,但这些方法都是基于 ViewerCamera 这两个类实现的

Viewer 类

Viewer 类里面有两个方法用于实现相机的飞行定位,分别是 flyTozoomTo 方法

  • 这两个方法第一个参数都是 target,类型可以是 EntityDataSourceImageryLayerCesium3DTileset
  • flyTo 方法的第二个参数是 options,它是一个包含 duration(飞行持续时间)、maximumHeight(飞行中的最大高度)、offsetHeadingPitchRange 类型)的对象
  • zoomTo 方法的第二个参数是 offset,即上面 options 中的 offset,也是一个 HeadingPitchRange 类型的对象

Viewer 类中的相机定位方法如下图所示

Camera类

Camera 类对应的相机定位方法比较多,包括下图当中所示的五个方法,每个方法的参数及参数类型都用不同的颜色区分开来,其中 options 对象参数只列出了主要的属性,更多参数可以查阅官方的 API 文档 来了解更多

不管是 Viewer 类还是 Camera 类中的方法都能实现相机的定位功能,根据实际情况选择其一或组合使用,其中 viewer.flyTo()camera.flyTo()camera.flyToBoundingSphere() 这三个方法会有一个飞行动画的效果,所以会有飞行持续时间参数 duration,默认是 3

相机参数

我们不管使用哪种方式,基本上都是先确定相机要飞到的某一个位置,如点、矩形、包围球等,然后再结合相机的三个参数 headingpitchrollrange 实现某一位置视角的计算,下面主要介绍这三个参数

我们知道 Cesium 使用的是笛卡尔空间直角坐标系,其中 XYZ 三个轴的正方向如下图所示

相机的三个参数 headingpitchroll 的值是针对于坐标轴旋转的弧度数,示意图如下所示

  • heading(偏航角,单位为弧度)

绕负 Z 轴旋转,顺时针为正,默认为正北方向 0,其中正角向东增加,控制机体头的朝向位置,即左右方向的改变

  • pitch(俯仰角,单位为弧度)

绕负 Y 轴旋转,顺时针为正,默认为俯视 -90,正俯仰角在飞机上方,负俯仰角在飞机下方,可简单理解成前空翻、后空翻

  • roll(翻滚角,单位为弧度)

绕正 X 轴旋转,顺时针为正,默认为 0,可简单理解成侧空翻

  • range(翻滚角,单位为米)

代表相机距离目标的距离

示例

比如针对于某一矩形范围 [110.2, 35.6, 112.3, 36.7],要求相机倾斜 25 度,实现矩形位置飞行定位的话,有两种方法实现,如下

  • 方法一 viewer.flyTo()
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),
})

效果是下面这样的

  • 方法二 camera.flyTo()
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 引擎,所以距离量测支持直线距离、水平距离、垂直距离以及地表距离,面积量测支持水平面积、地表面积以及模型表面积等,不管是哪种类型的距离测量还是面积测量,实现思路基本是一样的,都是按照如下思路实现的

  1. 点击按钮开始测量,侦听鼠标 LEFT_CLICK 事件,记录坐标,绘制节点和折线(多边形)
  2. 侦听鼠标移动事件,鼠标点击后即复制一个浮动点,在 MOUSE_MOVE 事件中不断更新最后一个浮动点,动态更新折线(多边形)绘制
  3. 侦听鼠标右击事件,RIGHT_CLICK 触发时销毁测量相关事件句柄(ScreenSpaceEventHandler),删除多余的浮动点
  4. 折线(多边形)的动态绘制通过 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,
})

// 隐藏 logo
viewer._cesiumWidget._creditContainer.style.display = 'none'

var handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas)

// measureAreaSpace(viewer, handler)
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)
// 在三维场景中添加 Label
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
}
// 实时更新 polyline.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 // 角度转化为弧度(rad)
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
}
// 实时更新 polygon.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

  • CesiumInspector

该控件是针对开发人员来说,虽然不能提供功能的实现,但对于了解渲染效果和性能调优是非常有帮助的,特别是解决一些渲染状态下的问题时非常的有价值,使用该控件非常的简单,只需如下一行代码就能实现该控件的加载

1
viewer.extend(Cesium.viewerCesiumInspectorMixin)

控件里面有很多的功能,包括渲染帧数、Primitive 外包围球、Primitive 参考框架、线框模式等等,这里我们就不详细展开了,详细可以参考官方文档当中的 new Cesium.CesiumInspector(container, scene)

  • Cesium3DTilesInspector

面对大场景下的大规模、大体量的 3D Tiles 数据,Cesium 提供了一个监视 3D Tiles 数据的监视器,用于监视、观察 3D Tiles 数据的效果,加载该空间也非常的简单,只需一行代码即可,结果控件展示如下

1
viewer.extend(Cesium.viewerCesium3DTilesInspectorMixin)

主要包括

  • 3D Tiles 瓦片是否可拾取
  • 显示颜色、线框、瓦片边界范围框、瓦片内容边界范围框、观察者请求体、点云渲染等
  • 一些动态屏幕误差设置、最大屏幕误差设置、样式修改等

与第三方库的集成

这里我们主要简单的介绍 CesiumThree.jsEchartsHeatmapTurf 是如何集成使用的,下面我们一个一个来进行了解

集成 Three.js

Three.js 是基于原生 WebGL 封装运行的三维引擎库,在所有 WebGL 引擎中,Three.js 是国内文资料最多、使用最广泛的三维引擎,Three.js 可应用于 Web 3D 的可视化(如产品在线浏览、在线三维可视化等),微信小程序游戏,科教领域,机械领域,WebVRVR 看房、VR 看车等)以及家装室内设计等方面,是一个比较轻量级的跨浏览器 JavaScript 库 ,适合在浏览器中创建和显示动画 3D 计算机图形

Cesium 的行星级渲染和 GIS 功能与 Three.js 广泛而易用的通用 3D API 相结合,为新的 WebGL 体验开启了许多可能性,两者的集成总体思路如下

  1. 创建两个容器,分别用于显示 CesiumThree.js 的场景
  2. 分别初始化两者各自的渲染器
  3. 调整两者的渲染频率保持一致
  4. 调整两者的相机位置角度保持一致
  5. 加入要展示的图形

以下展示了部分核心代码

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 初始化时,要将它的自动渲染关掉(即 useDefaultRenderLoop 属性调整为 false)
cesium.viewer = new Cesium.Viewer('cesiumContainer', {
// 关闭自动渲染
useDefaultRenderLoop: false,
// ...
})

// 第二步,初始化 Three.js
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)
}

// 第三步,调整 Three.js 和 Cesium 的渲染频率
// 手动开启 Cesium 和 Three.js 的渲染,并放进一个渲染频率里
function loop() {
requestAnimationFrame(loop)
renderCesium()
renderThreeObj()

// 第四步,调整相机一致
// 这里使用的 Cesium 的相机为主相机,使 Three.js 的相机与 Cesium 保持一致即可
renderCamera()
}

// 第五步,加入要展示的图形
// 这里加入一个 Cesium 的图形 polygon,再加入一个 Three.js 的球体,以及一个 12 面体
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)

// 将热力图添加到球体上(生成的热力图 canvas 元素类名为 heatmap-canvas)
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)

而下面的截图是通过 TurfCesium 实现的点、线、面缓冲区分析结果,即借助了 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 这个封装好的库,它支持小数据量的 GeoJSONShape 文件矢量动态切片,并且还能实现贴地效果

# GIS

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×