构建一个定点飞行追踪器

2023-09-27 11:30:57

以可视化从弗朗西斯科到哥本哈根的真实的航班,使用FlightRadar24收集的雷达数据。

本章节可以学习到:

  1. 在Web上设置和部署Cesium应用程序。
  2. 添加全局三维建筑物、地形和图像的基础图层。
  3. 从一系列位置中准确地想象飞机随时间的变化。

在上一篇快速开始当中提到了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>

添加一个飞机模型

  1. 下载这个飞机模型
  2. 将模型拖拽到这个页面https://ion.cesium.com/assets/?
  3. 转换为glTF,并点击Upload
    Upload GLTF
  4. 在资产页可以看到刚刚上传的资源,官方提供了一段示例代码

在上述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;

页面显示效果如下
飞机模型