《我的飞机绕着宝岛飞》第二章(高空中和小红飞机加油的冒险旅程)

349 阅读5分钟

前言

人生一迹,谨以此记录Cesium相关系列知识

问题背景:飞机绕着宝岛飞的过程中,警报提示没油了,需要对接“小红加油机”!

上篇回顾《我的飞机绕着宝岛飞》第一章(地面接收器与飞机模型之间的纠缠)

书接上回,上回咱们说到:飞机绕着宝岛飞,通过与地面卫星接收器发出波段连线,告知我的飞机出现了警告。在接到地面控制塔的警告后,我意识到需要尽快处理左侧引擎的异常情况。然而就在此刻,燃油指示灯也开始闪烁,表明剩余的燃油已经无法支撑我返回机场。无奈之下,我请求进行空中加油,这是一次高风险但又必不可少的操作。

幸好,一架专门执行空中加油任务的“小红加油机”恰好在我的航线附近执行任务。我听到地面控制人员焦急但平静的声音:“立即调整航向,准备与加油机对接。”

心跳如鼓在胸腔内敲击,我握紧了操纵杆,将飞机微调至预定位置,目光紧紧锁定着逐渐靠近的加油机。两架飞机在万米高空中,如同两只雄鹰在宝岛上空的云端展开了一场精确的舞蹈。

随着距离的拉近,我能清晰地看到加油机尾部伸出的加油管道,它就像一只等待对接的红色金属脉络。我的飞机缓缓靠近,每一秒都显得异常漫长。终于,在通讯系统中传来一个简洁的“连接成功”,我深吸一口气,放松了紧绷的神经。

现在,我只需保持飞机的稳定,直到加油完毕。空中加油不仅考验着飞行员的技术,更是对心理素质的极大挑战。此刻,我和宝岛之上的天空,共同见证了这惊心动魄的一幕。

recording.gif

一、Cesium加载高德底图

二、场景定位

三、轨迹数据结构

照例由于书接上回,因此部分内容就不作重复叙述了

四、动态模型A与动态模型B(飞机与飞机模拟加油)

4.1 加载动态飞机模型(此处重点在于加载两个动态飞机模型)

具体细节描述,请参考上篇文章。

// 创建一个动态飞机实体对象A
var air1_entity = viewer.entities.add({
    name: 'air1_model',
    model: {
        uri: "Data/airPlane_cesium.gltf",
        show: true,
        scale: 2000,
    }
});
// 创建一个动态飞机实体对象B
var air2_entity = viewer.entities.add({
    name: 'air2_model',
    model: {
        uri: "Data/airPlane_cesium.gltf",
        show: true,
        scale: 2000,
    }
});

此处,先设置好两个飞机的轨迹点位置和时间数组

// 定义一个数组,存储物体运动的时间和位置点
var positionData1 = [
    { time: 0, longitude: 120.49155063, latitude: 25.47218132 },
    { time: 5, longitude: 119.7230824, latitude:23.027839 },
];
var positionData2 = [
    { time: 0, longitude: 119.47517259, latitude: 25.70525443 },
    { time: 10, longitude: 119.60265734, latitude: 23.59670123 },
];

与上一篇文章不一样的是,由于本篇是两个动态飞机,需要两个采样器对象,因此我设置了一个函数用来获取采样器对象,输入的参数是轨迹点数组。

function getPositionSampler(params) {
    // 创建一个采样器对象,用于插值计算物体运动的位置
    let positionSampler = new Cesium.SampledPositionProperty();
    positionSampler.setInterpolationOptions({
        interpolationDegree: 2,
        interpolationAlgorithm: Cesium.HermitePolynomialApproximation
    });
    for (let i = 0; i < params.length; i++) {
        let data = params[i];
        let time = new Cesium.JulianDate.addSeconds(start, data.time, new Cesium.JulianDate());
        let position = new Cesium.Cartesian3.fromDegrees(data.longitude, data.latitude);
        positionSampler.addSample(time, position);
    }
    return positionSampler
}

设置初始时间和两个动态飞机模型的位置信息以及朝向信息

var start = Cesium.JulianDate.fromDate(new Date(2023, 2, 29));
air1_entity.position = getPositionSampler(positionData1);
air1_entity.orientation = new Cesium.VelocityOrientationProperty(getPositionSampler(positionData1)); //根据坐标转头
air2_entity.position = getPositionSampler(positionData2);
air2_entity.orientation = new Cesium.VelocityOrientationProperty(getPositionSampler(positionData2)); //根据坐标转头

4.2 设置加油管线(重点!)

这个步骤的重点是在于CallbackProperty属性的使用,该属性是用来返回某些特定的值,可获取到entity某时刻的position值,因此只需要将两个飞机的动态position值赋值给管线即可。当然本文讲如何去设置两端都在动态变化的管线,大家可以根据自己的需求去更改类型,如:polylineVolume管线、wall墙、polyline线等。绘制类型的代码,请参考(cesium官方提供的案例)[sandcastle.cesium.com/?src=Geomet…]

// 创建一个连接线对象
var line_entity = viewer.entities.add({
    name: 'line',
    polylineVolume: {
        positions: new Cesium.CallbackProperty( (time) =>{
            var startpos = air1_entity.position.getValue(time);
            if(!startpos) return
            var cartographic = Cesium.Ellipsoid.WGS84.cartesianToCartographic(startpos);
            var lon1 = Cesium.Math.toDegrees(cartographic.longitude);
            var lat1 = Cesium.Math.toDegrees(cartographic.latitude);

            var endpos = air2_entity.position.getValue(time);
            if(!endpos) return
            var cartographic = Cesium.Ellipsoid.WGS84.cartesianToCartographic(endpos);
            var lon2 = Cesium.Math.toDegrees(cartographic.longitude);
            var lat2 = Cesium.Math.toDegrees(cartographic.latitude);
            return  Cesium.Cartesian3.fromDegreesArray([lon1,lat1,lon2,lat2])
        }, false),
        shape: computeCircle(2000.0),
        material: Cesium.Color.RED,
    }
})
// 计算轨迹管道函数
function computeCircle(radius) {
    const positions = [];
    for (let i = 0; i < 360; i++) {
        const radians = Cesium.Math.toRadians(i);
        positions.push(
        new Cesium.Cartesian2(
            radius * Math.cos(radians),
            radius * Math.sin(radians)
        )
        );
    }
    return positions;
}

4.3 设置时间

此步骤主要是定义Cesium的时间轴相关参数,包含起始时间、终止时间和当前时间

var stop = Cesium.JulianDate.addSeconds(start, 30, new Cesium.JulianDate());

viewer.clock.startTime = start.clone();
viewer.clock.stopTime = stop.clone();
viewer.clock.currentTime = start.clone();
viewer.clock.shouldAnimate = true;
viewer.clock.clockRange = Cesium.ClockRange.LOOP_STOP;
viewer.clock.multiplier = 1;

五、全部代码及效果图

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>我的飞机绕着宝岛飞</title>
    <!-- <link rel="stylesheet" href="./CesiumUnminified/Widgets/widgets.css">
    <script type="text/javascript" src="./CesiumUnminified/Cesium.js"></script> -->
    <link rel="stylesheet" href="./Cesium/Widgets/widgets.css">
    <script type="text/javascript" src="./Cesium/Cesium.js"></script>
</head>
<body>
    <div id="cesiumContainer"></div>
    <script type="text/javascript">
        let viewer = new Cesium.Viewer('cesiumContainer');
        // 更换底图
        let imageryLayers = viewer.imageryLayers;
        let map = new Cesium.UrlTemplateImageryProvider({
            url: "https://webst02.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}", //高德地图
            minimumLevel: 3,
            maximumLevel: 16,
        });
        imageryLayers.addImageryProvider(map); //添加地图贴图

        // 场景定位
        viewer.camera.flyTo({
            destination : Cesium.Cartesian3.fromDegrees(121.195,21.813,738947.02),
            orientation :{
                heading : Cesium.Math.toRadians(355.1),
                pitch : Cesium.Math.toRadians(-75.3),
                roll :0.0
            }
        });

        // 创建一个动态飞机实体对象A
        var air1_entity = viewer.entities.add({
            name: 'air1_model',
            model: {
                uri: "Data/airPlane_cesium.gltf",
                show: true,
                scale: 2000,
            },
        });
        // 创建一个动态飞机实体对象B
        var air2_entity = viewer.entities.add({
            name: 'air2_model',
            model: {
                uri: "Data/airPlane2.glb",
                show: true,
                scale: 10000,
            },
        });
        // 创建一个连接线对象
        var line_entity = viewer.entities.add({
            name: 'line',
            polylineVolume: {
                positions: new Cesium.CallbackProperty( (time) =>{
                    var startpos = air1_entity.position.getValue(time);
                    if(!startpos) return
                    var cartographic = Cesium.Ellipsoid.WGS84.cartesianToCartographic(startpos);
                    var lon1 = Cesium.Math.toDegrees(cartographic.longitude);
                    var lat1 = Cesium.Math.toDegrees(cartographic.latitude);

                    var endpos = air2_entity.position.getValue(time);
                    if(!endpos) return
                    var cartographic = Cesium.Ellipsoid.WGS84.cartesianToCartographic(endpos);
                    var lon2 = Cesium.Math.toDegrees(cartographic.longitude);
                    var lat2 = Cesium.Math.toDegrees(cartographic.latitude);
                    return  Cesium.Cartesian3.fromDegreesArray([lon1,lat1,lon2,lat2])
                }, false),
                shape: computeCircle(2000.0),
                material: Cesium.Color.RED,
            }
        })
        // 计算轨迹管道函数
        function computeCircle(radius) {
            const positions = [];
            for (let i = 0; i < 360; i++) {
                const radians = Cesium.Math.toRadians(i);
                positions.push(
                new Cesium.Cartesian2(
                    radius * Math.cos(radians),
                    radius * Math.sin(radians)
                )
                );
            }
            return positions;
        }

        // 定义一个数组,存储物体运动的时间和位置点
        var positionData1 = [
            { time: 0, longitude: 120.49155063, latitude: 25.47218132 },
            { time: 5, longitude: 119.7230824, latitude:23.027839 },
        ];
        var positionData2 = [
            { time: 0, longitude: 119.47517259, latitude: 25.70525443 },
            { time: 10, longitude: 119.60265734, latitude: 23.59670123 },
        ];

        
        // 起始时间
        var start = Cesium.JulianDate.fromDate(new Date(2023, 2, 29));

        // 遍历数组,将时间和位置点添加到采样器对象中
        function getPositionSampler(params) {
            // 创建一个采样器对象,用于插值计算物体运动的位置
            let positionSampler = new Cesium.SampledPositionProperty();
            positionSampler.setInterpolationOptions({
                interpolationDegree: 2,
                interpolationAlgorithm: Cesium.HermitePolynomialApproximation
            });
            for (let i = 0; i < params.length; i++) {
                let data = params[i];
                let time = new Cesium.JulianDate.addSeconds(start, data.time, new Cesium.JulianDate());
                let position = new Cesium.Cartesian3.fromDegrees(data.longitude, data.latitude);
                positionSampler.addSample(time, position);
            }
            return positionSampler
        }

        air1_entity.position = getPositionSampler(positionData1);
        air1_entity.orientation = new Cesium.VelocityOrientationProperty(getPositionSampler(positionData1)); //根据坐标转头
        air2_entity.position = getPositionSampler(positionData2);
        air2_entity.orientation = new Cesium.VelocityOrientationProperty(getPositionSampler(positionData2)); //根据坐标转头

        // 定义时钟参数,设置开始时间、结束时间和当前时间
        var stop = Cesium.JulianDate.addSeconds(start, 5, new Cesium.JulianDate());
        viewer.clock.startTime = start.clone();
        viewer.clock.stopTime = stop.clone();
        viewer.clock.currentTime = start.clone();
        viewer.clock.shouldAnimate = true;
        viewer.clock.clockRange = Cesium.ClockRange.LOOP_STOP;
        viewer.clock.multiplier = 1;

    </script>
</body>
</html>