本教程向您展示如何构建您的第一个 Cesium 应用程序以可视化从旧金山到哥本哈根的真实航班,以及FlightRadar24收集的雷达数据。您将学习如何:
- 在网络上设置和部署您的 Cesium 应用程序
- 添加全球 3D 建筑、地形和图像的基础图层
- 随着时间的推移,根据位置列表准确地可视化飞机
开始之前
我们将从 Cesium ion 获取全球卫星图像、3D 建筑和地形,Cesium ion 是一个用于流式传输和托管 3D 内容的开放平台。 如果您还没有免费的 Cesium ion 帐户,请注册一个。
登录后:
- 转到您的 Access Tokens 选项卡。
- 请注意默认令牌旁边的复制按钮。我们将在下一步中使用此令牌。
Cesium ion 是一个用于流式传输和托管 3D 内容的开放平台,包括全球精选数据,您可以使用这些数据创建自己的真实世界应用程序。
步骤1:设置你的Cesium 应用
我们将使用开源 JavaScript 引擎 CesiumJS 创建我们的应用程序。我们将使用在线 IDE Glitch 来托管我们的应用程序。
- 使用我们放在一起的基本模板创建一个新的 Glitch 项目。
- 单击左侧面板中的 index.html 查看应用程序代码。
- 替换为您的令牌页面
your_token_here
中的访问令牌 。 - 通过单击顶部的 Show 并选择 Next to The Code 来运行应用程序。
到目前为止,index.html 中的代码做了三件事:
- 导入 CesiumJS 库。JavaScript和 CSS 文件在这两行中加载:
<script src="https://cesium.com/downloads/cesiumjs/releases/1.100/Build/Cesium/Cesium.js"></script>
<link
href="https://cesium.com/downloads/cesiumjs/releases/1.100/Build/Cesium/Widgets/widgets.css"
rel="stylesheet"
/>
- 为场景添加一个 HTML 容器:
<div id=cesiumContainer></div>
。 - 初始化查看器:
const viewer = new Cesium.Viewer('cesiumContainer');
。
你现在有一个基本的 CesiumJS 应用程序在你的浏览器中运行,其中包含来自 Cesium ion 的全球卫星图像。
配置自动刷新
每次代码更改时,Glitch 都会自动刷新页面。您可以通过单击左上角的项目名称并取消选中此框来切换它:
使用应用程序窗口顶部的刷新按钮重新运行应用程序:
步骤2:添加全局 3D 建筑和地形
让我们在场景中添加一些全局层。默认情况下,您的Cesium ion帐户可以访问以下资源:
- Cesium World Terrain———高分辨率地形,精度高达 1 米。
- Cesium OSM 建筑物———超过 3.5 亿个建筑物来自 OpenStreetMap 数据。
- Bing Maps Aerial Imagery———分辨率高达 15 厘米的全球卫星图像。
您的应用程序已在使用 Bing 地图图层。
- 用下面的代码替换 index.html 中的 JavaScript 代码,保留之前的访问令牌行。
// Keep your *Cesium.Ion.defaultAccessToken = 'your_token_here'* line from before here.
const viewer = new Cesium.Viewer('cesiumContainer', {
terrainProvider: Cesium.createWorldTerrain()
});
const osmBuildings = viewer.scene.primitives.add(Cesium.createOsmBuildings());
- 通过单击并拖动来探索场景。拖动时按住 CTRL 倾斜相机。
请注意:放大时会加载更高级别的细节。这使我们能够以做出现实世界决策所需的准确性来可视化整个星球。 这是可能的,因为我们正在使用 3D Tiles,这是我们率先将 3D 内容流式传输到任何设备的开放标准。了解如何 将您自己的数据转换为 3D Tiles。
步骤3:可视化单个样本
FlightRadar24 使用多种方法跟踪空中交通,包括雷达。为简单起见,我们将雷达数据直接复制到我们的应用程序中,但您稍后可以扩展此代码以解析原始数据,甚至可以在样本从服务器传递到您的应用程序时可视化实时空中交通。如果您好奇,可以下载为此航班收集的原始数据。
- 添加下面的代码以可视化场景中的单个点并将相机对准它。
- 单击红点以查看附加的说明。此描述可用于附加信息,例如每个点的确切位置或捕获时间。
// STEP 3 CODE (first point)
// This is one of the first radar samples collected for our flight.
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: Cesium.Cartesian3.fromDegrees(
dataPoint.longitude,
dataPoint.latitude,
dataPoint.height
),
point: { pixelSize: 10, color: Cesium.Color.RED }
});
// Fly the camera to this point.
viewer.flyTo(pointEntity);
步骤4:要可视化整个系列的雷达示例, 请将上面的代码(步骤三)替换为以下代码段。
// STEP 3 CODE (all points)
// These are all the radar points from this flight.
const flightData = JSON.parse(
'[{"longitude":-122.39053,"latitude":37.61779,"height":-27.32}...]'
);
// Create a point for each.
for (let i = 0; i < flightData.length; i++) {
const dataPoint = flightData[i];
viewer.entities.add({
description: *Location: (${dataPoint.longitude}, ${dataPoint.latitude}, ${dataPoint.height})*,
position: Cesium.Cartesian3.fromDegrees(
dataPoint.longitude,
dataPoint.latitude,
dataPoint.height
),
point: { pixelSize: 10, color: Cesium.Color.RED }
});
}
现在你可以看到飞机的全系列雷达样本,从登机口,到跑道,一直到降落在哥本哈根的跑道上!
我们使用什么坐标系?CesiumJS中的坐标在ECEF中使用 Cartesian3 类表示 。在这个系统中,原点 (0, 0, 0) 是地球的中心。 这就是为什么我们 Cartesian3.fromDegrees 在 ECEF 中使用将经度、纬度和高度转换为X、Y和 Z 的原因。 CesiumJ 中的高度以米为单位,相对于 WGS84 椭球。我们对雷达数据进行了预处理,将相对于平均海平面的英尺高度转换为相对于椭球体的米。
步骤5:随时间可视化运动
到目前为止,我们已经可视化了各个雷达样本。CesiumJS内置支持在随时间收集的样本之间进行插值,因此我们可以看到飞机在任何给定时刻的位置。我们将创建一个 SampledPositionProperty 来存储每个位置以及时间戳。源数据不包括每个样本的时间戳,但我们知道航班号为 SK936,原定于太平洋标准时间 2020年 3 月 9 日下午 4:10 起飞。我们假设位置样本间隔 30 秒。
- 用下面的代码替换 index.html 中的所有 JavaScript 代码,从一开始就只保留访问令牌行。
- 探索动画飞行:
- 使用左下角的按钮播放和暂停动画。
- 在底部的时间轴中单击并拖动以跳过场景时间。
- 双击地面上的任意位置以将相机与移动实体分离。
// STEP 4 CODE (replaces steps 2 and 3)
// Keep your *Cesium.Ion.defaultAccessToken = 'your_token_here'* line from before here.
const viewer = new Cesium.Viewer('cesiumContainer', {
terrainProvider: Cesium.createWorldTerrain()
});
const osmBuildings = viewer.scene.primitives.add(Cesium.createOsmBuildings());
const flightData = JSON.parse(
'[{"longitude":-122.39053,"latitude":37.61779,"height":-27.32}...]'
);
/* Initialize the viewer clock:
Assume the radar samples are 30 seconds apart, and calculate the entire flight duration based on that assumption.
Get the start and stop date times of the flight, where the start is the known flight departure time (converted from PST
to UTC) and the stop is the start plus the calculated duration. (Note that Cesium uses Julian dates. See
https://simple.wikipedia.org/wiki/Julian_day.)
Initialize the viewer's clock by setting its start and stop to the flight start and stop times we just calculated.
Also, set the viewer's current time to the start time and take the user to that time.
*/
const timeStepInSeconds = 30;
const totalSeconds = timeStepInSeconds * (flightData.length - 1);
const start = Cesium.JulianDate.fromIso8601('2020-03-09T23:10:00Z');
const stop = Cesium.JulianDate.addSeconds(
start,
totalSeconds,
new Cesium.JulianDate()
);
viewer.clock.startTime = start.clone();
viewer.clock.stopTime = stop.clone();
viewer.clock.currentTime = start.clone();
viewer.timeline.zoomTo(start, stop);
// Speed up the playback speed 50x.
viewer.clock.multiplier = 50;
// Start playing the scene.
viewer.clock.shouldAnimate = true;
// The SampledPositionedProperty stores the position and timestamp for each sample along the radar sample series.
const positionProperty = new Cesium.SampledPositionProperty();
for (let i = 0; i < flightData.length; i++) {
const dataPoint = flightData[i];
// Declare the time for this individual sample and store it in a new JulianDate instance.
const time = Cesium.JulianDate.addSeconds(
start,
i * timeStepInSeconds,
new Cesium.JulianDate()
);
const position = Cesium.Cartesian3.fromDegrees(
dataPoint.longitude,
dataPoint.latitude,
dataPoint.height
);
// Store the position along with its timestamp.
// 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: Cesium.Color.RED }
});
}
// STEP 4 CODE (green circle entity)
// Create an entity to both visualize the entire radar sample series with a line and add a point that moves along the samples.
const airplaneEntity = viewer.entities.add({
availability: new Cesium.TimeIntervalCollection([
new Cesium.TimeInterval({ start: start, stop: stop })
]),
position: positionProperty,
point: { pixelSize: 30, color: Cesium.Color.GREEN },
path: new Cesium.PathGraphics({ width: 3 })
});
// Make the camera track this moving entity.
viewer.trackedEntity = airplaneEntity;
跨大西洋飞行是 3D 可视化如何使数据更易于解释的一个很好的例子。实际上,连接雷达样本的线大部分是直的。在 2d 地图上,使用常见的 web 墨卡托投影时会出现弯曲:
步骤6:上传飞机模型
作为最后一步,我们将把飞机的 3D 模型而不是绿点附加到我们的实体。
- 下载飞机的 3D 模型。
- 转到您的帐户仪表板。将模型文件拖放到此页面上。
- 选择 3D Model (Convert to glTF),然后单击上传。
- 完成处理后,通过在仪表板中选择新资源并在右侧的预览窗口下查看来找到资源ID。
步骤7:附上飞机模型
- 删除以下行开头的代码:
// STEP 4 CODE (green circle entity)
- 通过代码的结尾将其替换为以下代码。
- 替换
your_asset_id
为您的资产 ID。
// STEP 6 CODE (airplane entity)
async function loadModel() {
// Load the glTF model from Cesium ion.
const airplaneUri = await Cesium.IonResource.fromAssetId(your_asset_id);
const airplaneEntity = viewer.entities.add({
availability: new Cesium.TimeIntervalCollection([
new Cesium.TimeInterval({ start: start, stop: stop })
]),
position: positionProperty,
// Attach the 3D model instead of the green point.
model: { uri: airplaneUri },
// Automatically compute the orientation from the position.
orientation: new Cesium.VelocityOrientationProperty(positionProperty),
path: new Cesium.PathGraphics({ width: 3 })
});
viewer.trackedEntity = airplaneEntity;
}
loadModel();
你现在有了一个遵循飞行雷达样本的飞机模型!如果您有自己的 3D 模型,请尝试将其添加到您的场景中,而不是提供的模型。
完整的源代码
这是带有 your_token_here
和 your_asset_id
占位符的此应用程序的完整源代码。
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://cesium.com/downloads/cesiumjs/releases/1.100/Build/Cesium/Cesium.js"></script>
<link
href="https://cesium.com/downloads/cesiumjs/releases/1.100/Build/Cesium/Widgets/widgets.css"
rel="stylesheet"
/>
<link
href="style.css"
rel="stylesheet"
/>
</head>
<body>
<div id="cesiumContainer"></div>
<script>
// Your access token can be found at: https://cesium.com/ion/tokens.
// This is the default access token from your ion account
Cesium.Ion.defaultAccessToken =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI3ODdlN2I5OS1iZDMyLTQ2MTItODQwYi01MGE3NGI3NGIzNGUiLCJpZCI6MTE4MjU5LCJpYXQiOjE2NzA5ODI0MDd9.XpfuSxbcNuxNF1jMqI5FxveHXhiq6VVCsww80hIVAME';
// STEP 4 CODE (replaces steps 2 and 3)
// Keep your *Cesium.Ion.defaultAccessToken = 'your_token_here'* line from before here.
const viewer = new Cesium.Viewer('cesiumContainer', {
terrainProvider: Cesium.createWorldTerrain()
});
const osmBuildings = viewer.scene.primitives.add(
Cesium.createOsmBuildings()
);
const flightData = JSON.parse(
'[{"longitude":-122.39053,"latitude":37.61779,"height":-27.32}...]'
);
const timeStepInSeconds = 30;
const totalSeconds = timeStepInSeconds * (flightData.length - 1);
const start = Cesium.JulianDate.fromIso8601('2020-03-09T23:10:00Z');
const stop = Cesium.JulianDate.addSeconds(
start,
totalSeconds,
new Cesium.JulianDate()
);
viewer.clock.startTime = start.clone();
viewer.clock.stopTime = stop.clone();
viewer.clock.currentTime = start.clone();
viewer.timeline.zoomTo(start, stop);
// Speed up the playback speed 50x.
viewer.clock.multiplier = 50;
// Start playing the scene.
viewer.clock.shouldAnimate = true;
// The SampledPositionedProperty stores the position and timestamp for each sample along the radar sample series.
const positionProperty = new Cesium.SampledPositionProperty();
for (let i = 0; i < flightData.length; i++) {
const dataPoint = flightData[i];
// Declare the time for this individual sample and store it in a new JulianDate instance.
const time = Cesium.JulianDate.addSeconds(
start,
i * timeStepInSeconds,
new Cesium.JulianDate()
);
const position = Cesium.Cartesian3.fromDegrees(
dataPoint.longitude,
dataPoint.latitude,
dataPoint.height
);
// Store the position along with its timestamp.
// 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: Cesium.Color.RED }
});
}
// STEP 6 CODE (airplane entity)
async function loadModel() {
// Load the glTF model from Cesium ion.
const airplaneUri = await Cesium.IonResource.fromAssetId(your_asset_id);
const airplaneEntity = viewer.entities.add({
availability: new Cesium.TimeIntervalCollection([
new Cesium.TimeInterval({ start: start, stop: stop })
]),
position: positionProperty,
// Attach the 3D model instead of the green point.
model: { uri: airplaneUri },
// Automatically compute the orientation from the position.
orientation: new Cesium.VelocityOrientationProperty(positionProperty),
path: new Cesium.PathGraphics({ width: 3 })
});
viewer.trackedEntity = airplaneEntity;
}
loadModel();
</script>
</body>
</html>
评论 (0)