以可视化从弗朗西斯科到哥本哈根的真实的航班,使用FlightRadar24收集的雷达数据。
本章节可以学习到:
- 在Web上设置和部署Cesium应用程序。
- 添加全局三维建筑物、地形和图像的基础图层。
- 从一系列位置中准确地想象飞机随时间的变化。
在上一篇快速开始当中提到了Cesium ion
,我们可以从这个里面获得全球卫星图像、3D建筑物和地形。
介绍3D建筑物和地形
默认情况下Cesium ion
可以访问到以下地图资源
Cesium World Terrain
高分辨率地形。
Terrain 直译为地形,创建一个Cesium
应用,需要在视图当中指定一个地形。Cesium World Terrain
是将多个数据源融合到一个瓦片地图当中,并且针对3D地图进行了一些优化。根据官方的解读,这个地形当中精度只在新西兰、英格兰、美国部分国家提供了精度为1米的地形,而在大多地方精度30-60米甚至更低。
js
var viewer = new Cesium.Viewer('container', {
terrainProvider : Cesium.createWorldTerrain()
});
这个地形函数额外提供了一些扩展的功能
js
var viewer = new Cesium.Viewer('container', {
terrainProvider : Cesium.createWorldTerrain({
// 一个水效果蒙版,用于渲染水效果和海岸线数据
requestWaterMask : true,
// 用于对地形进行照明,这对于在不使用高分辨率图像时亮显曲面曲率特别有用。
requestVertexNormals : true
})
});
Cesium OSM Buildings
-超过3.5亿座建筑物来自OpenStreetMap数据。
Cesium OSM Buildings
是一个覆盖全世界的3D建筑物的瓦片图层,是由Cesium ion
这个服务进行提供。
它源自OpenStreetMap,包含超过3.5亿栋建筑物,每栋建筑物都有元数据。这包括基本信息,如建筑物名称和高度,地址,开放时间,甚至建筑物个别部分的材料类型。
官方演示地址:https://ion.cesium.com/stories/viewer/?id=2f0131ab-3948-4467-947c-411d5705a116
js
var viewer = new Cesium.Viewer('cesiumContainer');
viewer.scene.primitives.add(Cesium.createOsmBuildings());
Bing Maps Aerial imagery
-分辨率高达15厘米的全球卫星图像。
js
var viewer = new Cesium.Viewer("cesiumContainer", {
imageryProvider: Cesium.createWorldImagery({
// 显示地图标签
style: Cesium.IonWorldImageryStyle.AERIAL_WITH_LABELS,
// 显示地图的路网图
style: Cesium.IonWorldImageryStyle.ROAD,
}),
});
在2023.07.03 1.107 版本的更新当中,这个选项被 baseLayer所替代, 而且用法
js
const imageryProvider = await createWorldImageryAsync({
style: IonWorldImageryStyle.ROAD
});
var viewer = new Cesium.Viewer("cesiumContainer", {
baseLayer: new ImageryLayer(
imageryProvider, {}
)
});
代码示例
vue3
<script setup lang="ts">
// 这里的 Ion 是上边介绍的存储地图的服务器
import { createOsmBuildingsAsync, Ion, Terrain, Viewer } from 'cesium';
import "cesium/Build/Cesium/Widgets/widgets.css";
import { onMounted } from 'vue';
// 静态文件路径配置
window.CESIUM_BASE_URL = 'CesiumUnminified';
Ion.defaultAccessToken = '你的 token';
// 等待dom初始化完成
onMounted(async () => {
// 先创建一个视图,在ID为 cesiumContainer 的html 元素当中 初始化 Cesium 视图区域
const viewer = new Viewer('cesiumContainer', {
terrain: Terrain.fromWorldTerrain({
requestVertexNormals: true,
})
});
// OsmBuildings 是一个全球3D建筑物视图, 添加一个OSM建筑物视图
const buildingTileset = await createOsmBuildingsAsync();
viewer.scene.primitives.add(buildingTileset);
})
</script>
<template>
<div id="cesiumContainer" style="width: 100%;height: 90vh;"></div>
</template>
这个时候在页面上看到渲染出来的地球资源,鼠标滚动可以放大具体的位置,嗯住键盘
ctrl
键,并拖动鼠标左键,可以变换相机的视角。
放大会加载更加详细的地图内容
如果想要将自己的数据转换为Cesium的数据,可以学习https://cesium.com/learn/ion/
js
// 添加一个点标记
// 先定义一个简单的雷达样本数据,经度、纬度、俯视高度
const dataPoint = { longitude: -122.38985, latitude: 37.61864, height: -27.32 };
// Mark this location with a red point.
const pointEntity = viewer.entities.add({
description: `First data point at (${dataPoint.longitude}, ${dataPoint.latitude})`,
position: Cartesian3.fromDegrees(dataPoint.longitude, dataPoint.latitude, dataPoint.height),
point: { pixelSize: 10, color: Color.RED } // 点的大小和颜色
});
// 视图飞行到这个点.
viewer.flyTo(pointEntity);
如果要支撑更多的雷达样本点
// 一组雷达数据.
const flightData = JSON.parse(
'[{"longitude":-122.39053,"latitude":37.61779,"height":-27.32},{"longitude":-122.39035,"latitude":37.61803,"height":-27.32},{"longitude":-122.39019,"latitude":37.61826,"height":-27.32},{"longitude":-122.39006,"latitude":37.6185,"height":-27.32},{"longitude":-122.38985,"latitude":37.61864,"height":-27.32},{"longitude":-122.39005,"latitude":37.61874,"height":-27.32},{"longitude":-122.39027,"latitude":37.61884,"height":-27.32},{"longitude":-122.39057,"latitude":37.61898,"height":-27.32},{"longitude":-122.39091,"latitude":37.61912,"height":-27.32},{"longitude":-122.39121,"latitude":37.61926,"height":-27.32},{"longitude":-122.39153,"latitude":37.61937,"height":-27.32},{"longitude":-122.39175,"latitude":37.61948,"height":-27.32},{"longitude":-122.39207,"latitude":37.6196,"height":-27.32},{"longitude":-122.39247,"latitude":37.61983,"height":-27.32},{"longitude":-122.39253,"latitude":37.62005,"height":-27.32},{"longitude":-122.39226,"latitude":37.62048,"height":-27.32},{"longitude":-122.39207,"latitude":37.62075,"height":-27.32},{"longitude":-122.39186,"latitude":37.62106,"height":-27.32},{"longitude":-122.39172,"latitude":37.62127,"height":-27.32},{"longitude":-122.39141,"latitude":37.62174,"height":-27.32},{"longitude":-122.39097,"latitude":37.62227,"height":-27.32},{"longitude":-122.39061,"latitude":37.6224,"height":-27.31},{"longitude":-122.39027,"latitude":37.62249,"height":-27.31},{"longitude":-122.38993,"latitude":37.62256,"height":-27.31},{"longitude":-122.3895,"latitude":37.62267,"height":-27.31},{"longitude":12.64936,"latitude":55.6247,"height":40.94}]');
// 创建一个点标记循环
for (let i = 0; i < flightData.length; i++) {
const dataPoint = flightData[i];
viewer.entities.add({
description: `Location: (${dataPoint.longitude}, ${dataPoint.latitude}, ${dataPoint.height})`,
position: Cartesian3.fromDegrees(dataPoint.longitude, dataPoint.latitude, dataPoint.height),
point: { pixelSize: 10, color: Color.RED }
});
}
CesiumJS 使用的是ECEF(地心地固坐标系), 默认中心点为
(0,0,0)
Cartesian3.fromDegrees
是将经度、纬度、高度转换为ECEF坐标。
CesiumJS
中的高度是相对于WGS84椭球体的米。CesiumJS
已经对雷达数据进行了预处理,将高度从相对于平均海平面的英尺转换为相对于椭球体的米。
定点飞行代码示例
vue3
<script setup lang="ts">
// 这里的 Ion 是上边介绍的存储地图的服务器
import { Cartesian3, Color, createOsmBuildingsAsync, createWorldTerrainAsync, Ion, JulianDate, PathGraphics, SampledPositionProperty, Terrain, TimeInterval, TimeIntervalCollection, Viewer } from 'cesium';
import "cesium/Build/Cesium/Widgets/widgets.css";
import { onMounted } from 'vue';
// 静态文件路径配置
window.CESIUM_BASE_URL = 'CesiumUnminified';
Ion.defaultAccessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI5NzI4Mjk2ZC1jMjFiLTQxZGQtYTU0My1jNjBmNmQ3OWQ5YzkiLCJpZCI6MTI3NjY3LCJpYXQiOjE2NzgxOTM1NjZ9.vxjp1CF33ZavOR5fMT-LJ6g2ylQDN7oex0lSBx8OwiA';
// 等待dom初始化完成
onMounted(async () => {
// STEP 4 CODE (replaces steps 2 and 3)
// Keep your `Cesium.Ion.defaultAccessToken = 'your_token_here'` line from before here.
const viewer = new Viewer('cesiumContainer', {
terrainProvider: await createWorldTerrainAsync(),
// animation: false,
// timeline: false,
});
viewer.scene.primitives.add(await createOsmBuildingsAsync());
const flightData = JSON.parse(
'[{"longitude":-122.39053,"latitude":37.61779,"height":-27.32},{"longitude":-122.39035,"latitude":37.61803,"height":-27.32},{"longitude":-122.39019,"latitude":37.61826,"height":-27.32},{"longitude":-122.39006,"latitude":37.6185,"height":-27.32},{"longitude":-122.38985,"latitude":37.61864,"height":-27.32},{"longitude":-122.39005,"latitude":37.61874,"height":-27.32},{"longitude":-122.39027,"latitude":37.61884,"height":-27.32},{"longitude":-122.39057,"latitude":37.61898,"height":-27.32},{"longitude":-122.39091,"latitude":37.61912,"height":-27.32},{"longitude":-122.39121,"latitude":37.61926,"height":-27.32},{"longitude":-122.39153,"latitude":37.61937,"height":-27.32},{"longitude":-122.39175,"latitude":37.61948,"height":-27.32},{"longitude":-122.39207,"latitude":37.6196,"height":-27.32},{"longitude":-122.39247,"latitude":37.61983,"height":-27.32},{"longitude":-122.39253,"latitude":37.62005,"height":-27.32},{"longitude":-122.39226,"latitude":37.62048,"height":-27.32},{"longitude":-122.39207,"latitude":37.62075,"height":-27.32},{"longitude":-122.39186,"latitude":37.62106,"height":-27.32},{"longitude":-122.39172,"latitude":37.62127,"height":-27.32},{"longitude":-122.39141,"latitude":37.62174,"height":-27.32},{"longitude":-122.39097,"latitude":37.62227,"height":-27.32},{"longitude":-122.39061,"latitude":37.6224,"height":-27.31},{"longitude":-122.39027,"latitude":37.62249,"height":-27.31},{"longitude":-122.38993,"latitude":37.62256,"height":-27.31},{"longitude":-122.3895,"latitude":37.62267,"height":-27.31},{"longitude":-122.3889,"latitude":37.62256,"height":-27.31},{"longitude":-122.38854,"latitude":37.62242,"height":-27.31},{"longitude":-122.38814,"latitude":37.62225,"height":-27.31},{"longitude":-122.38778,"latitude":37.62209,"height":-27.31},{"longitude":-122.38744,"latitude":37.62195,"height":-27.31},{"longitude":-122.38671,"latitude":37.62164,"height":-27.31},{"longitude":-122.38638,"latitude":37.62151,"height":-27.31},{"longitude":-122.38604,"latitude":37.62136,"height":-27.31},{"longitude":-122.38571,"latitude":37.62123,"height":-27.31},{"longitude":-122.38512,"latitude":37.62098,"height":-27.31},{"longitude":12.64936,"latitude":55.6247,"height":40.94}]'
);
/* 初始化一个视图时钟:
假设雷达数据的样本间隔是30秒,并根据这个假设计算整个的飞行时间,也就是下面的 totalSeconds。
假设航班的起飞时间是"2020-03-09T23:10:00Z",(PST转换成UTC,北京时间=UTC+8h= PST+8+8=PST+16h。),并计算出结束时间
在Cesium时间使用的是儒略时间,可以自行搜索了解下这个时间。
需要将查看器的当前时间设置为开始时间
*/
const timeStepInSeconds = 30;
const totalSeconds = timeStepInSeconds * (flightData.length - 1); // 整体的飞行时间
const start = JulianDate.fromIso8601("2020-03-09T23:10:00Z");
const stop = JulianDate.addSeconds(start, totalSeconds, new JulianDate());
viewer.clock.startTime = start.clone();
viewer.clock.stopTime = stop.clone();
viewer.clock.currentTime = start.clone();
viewer.timeline.zoomTo(start, stop);
// 设置播放速度为 50 倍.
viewer.clock.multiplier = 50;
// 开始播放当前场景.
viewer.clock.shouldAnimate = true;
// SampledPositionProperty 用于存储时间和雷达位置的对应关系
const positionProperty = new SampledPositionProperty();
for (let i = 0; i < flightData.length; i++) {
const dataPoint = flightData[i];
// 声明一个和上边雷达数据一样的时间间隔为30s的样本,并存储在一个JulianDate当中
const time = JulianDate.addSeconds(start, i * timeStepInSeconds, new JulianDate());
const position = Cartesian3.fromDegrees(dataPoint.longitude, dataPoint.latitude, dataPoint.height);
// 存储位置信息和时间线的对应关系.
// Here we add the positions all upfront, but these can be added at run-time as samples are received from a server.
positionProperty.addSample(time, position);
viewer.entities.add({
description: `Location: (${dataPoint.longitude}, ${dataPoint.latitude}, ${dataPoint.height})`,
position: position,
point: { pixelSize: 10, color: Color.RED }
});
}
// STEP 4 CODE 生成一个绿色的实体
// 根据雷达样本数据,创建一个视角漫游的实体.
const airplaneEntity = viewer.entities.add({
availability: new TimeIntervalCollection([new TimeInterval({ start: start, stop: stop })]),
position: positionProperty,
point: { pixelSize: 30, color: Color.GREEN },
path: new PathGraphics({ width: 3 })
});
// 跟踪实体
viewer.trackedEntity = airplaneEntity;
})
</script>
<template>
<div id="cesiumContainer" style="width: 100%;height: 90vh;"></div>
</template>
添加一个飞机模型
- 下载这个飞机模型
- 将模型拖拽到这个页面https://ion.cesium.com/assets/?
- 转换为
glTF
,并点击Upload
- 在资产页可以看到刚刚上传的资源,官方提供了一段示例代码
在上述STEP 4 CODE 当中添加这段代码
vue3
// 根据资源ID查找模型
const resource = await IonResource.fromAssetId(2292263);
// STEP 4 CODE 生成一个绿色的实体
// 根据雷达样本数据,创建一个视角漫游的实体.
const airplaneEntity = viewer.entities.add({
availability: new TimeIntervalCollection([new TimeInterval({ start: start, stop: stop })]),
position: positionProperty,
point: { pixelSize: 30, color: Color.GREEN },
path: new PathGraphics({ width: 3 }),
model: { uri: resource },
});
// 跟踪实体
viewer.trackedEntity = airplaneEntity;
页面显示效果如下