004Cesium专刊:动态模型+实时往返轨迹线

1,991 阅读7分钟

前言

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

问题提出:加载动态模型+实时往返轨迹线?

(重点在于实时往返轨迹线)

image.png

一、Cesium加载高德底图

本步骤的目的呢?是为了解决由于网络问题,导致的cesium地球显示不出来的bug,非常影响观感和调试体验。 本文采用高德源,有机会更一个关于各种地图源切换的文章,毕竟底图是万物之本。

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);

二、场景定位

先定位到数据以及模型的最佳视角范围内,以便于后续调试。此处的中心点坐标以及方向hpr参数,可以通过坐标及视角拾取得到,后续文章会更新。

注意:延迟2s,是为了等待场景跳转动画完毕,以便于观察到模型从起点开始移动,不会因为场景动画而影响观看。

viewer.camera.flyTo({
    destination : Cesium.Cartesian3.fromDegrees(112.402664,34.621038,4798.56),
    orientation :{
        heading : Cesium.Math.toRadians(351.9),
        pitch : Cesium.Math.toRadians(-85.7),
        roll :0.0
    }
});
setTimeout(2000)

三、轨迹数据结构

轨迹点数据,采用数组方式存储,每个元素代表一个轨迹点信息,每个轨迹点信息采用对象形式存储,其中time表示时间秒数, longitude为轨迹点经度,latitude表示轨迹点纬度。

注意:time表示累加时间(即后轨迹点的time必须大于前轨迹点的time),下列数据意思为:A-B耗时5s,A-B-C耗时10s

[
  { time: 0, longitude: 112.40641179342809, latitude: 34.6280090036493 },   //A
  { time: 5, longitude: 112.39561868811835, latitude:34.61691889996022 },  //B
  { time: 10, longitude: 112.38948151752913, latitude: 34.6286049476503 },  //C
  { time: 15, longitude: 112.38948151752913, latitude: 34.6286049476503 },  //C
  { time: 20, longitude: 112.39561868811835, latitude:34.61691889996022 },  //B
  { time: 25, longitude: 112.40641179342809, latitude: 34.6280090036493 },   //A
]

四、动态模型(模型按照轨迹数据进行运动)

4.1 时间模块设置

此步骤主要是Cesium的时间轴相关设置,包含起始时间、终止时间等

// 起始时间(设置为当前时间)
var start = Cesium.JulianDate.fromDate(new Date());
// 终止时间(计算方式,起始时间+30为整个轨迹的总时长)
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;
// 设置到达结束时间后的行为:LOOP_STOP(时间循环);UNBOUNDED(时间继续);CLAMPED(时间暂停)
viewer.clock.clockRange = Cesium.ClockRange.LOOP_STOP;  
// 设置时间轴播放的速度
viewer.clock.multiplier = 1; 

4.2 轨迹位置插值模块

此步骤主要是Cesium中Property大类中的SampledPositionProperty小类的用法,Property作为Cesium重要类,用法颇深,值得深入研究,有兴趣可以查看大佬vtxf的介绍:[Cesium的Property机制总结](zhuanlan.zhihu.com/p/50534090)

注意:由于返程算法问题,请勿使用其他插值算法( Cesium.LagrangePolynomialApproximation,Cesium.HermitePolynomialApproximation),否则会导致返程算法出问题。若只往程为了讲究路线丝滑,可以采取其他插值算法。

// 设置简单位置属性
var positionSampler = new Cesium.SampledPositionProperty();
//差值器(预防拐弯突兀等问题)
positionSampler.setInterpolationOptions({
  interpolationDegree: 1,
  interpolationAlgorithm: Cesium.LinearApproximation
});
// 循环写入时间与位置联系
for (var i = 0; i < positionData.length; i++) {
    var data = positionData[i];
    var time = new Cesium.JulianDate.addSeconds(start, data.time, new Cesium.JulianDate());
    var position = new Cesium.Cartesian3.fromDegrees(data.longitude, data.latitude);
    positionSampler.addSample(time, position);
}

4.3 加载模型(按轨迹数据移动)

加载模型部分,利用简单的Cesium中Entity加载即可。

注意:模型比例大小,模型方向

var run_entity = viewer.entities.add({
    name: 'run',
    model: {
        uri: "Data/chick.glb",
        show: true,
        scale: 1000, //根据不同模型去设置不同放大比例
    },
    position: positionSampler,
    // 设置模型方向(时刻保持路线正前方)
    orientation : new Cesium.VelocityOrientationProperty(positionSampler)
});

五、实时往返轨迹(往:轨迹一直保持新增,返:重复轨迹会消失)

此处逻辑为本文重点,此处也可以进行更改为自己所需的功能,后续有说明。首先讲解下两个算法,一:获取当前时间点下已走过的轨迹点数组(即往程)。二:删除返程时间点下的冗余轨迹点(即返程)

5.1 构建轨迹管线模型

此步骤为重点中的中点,实时轨迹线是通过CallbackProperty类与动态模型联系,和上文中的SampledPositionProperty类同样,都隶属于Property大类。CallbackProperty类就是一个回调函数,因此可以加入自己所需要的任何功能,回调函数中的参数为time时间

  var line_entity = viewer.entities.add({
      name: 'line',
      polylineVolume: {
      positions: new Cesium.CallbackProperty( (time) => {
          var runPos = run_entity.position.getValue(time);
          if(!runPos) return
          var cartographic = Cesium.Ellipsoid.WGS84.cartesianToCartographic(tarpos);
          var lon = Cesium.Math.toDegrees(cartographic.longitude);
          var lat = Cesium.Math.toDegrees(cartographic.latitude);
        
          let linePos = _getLastPoint(time)
          linePos = _isRepeatPoint([lon,lat],pos).flat()

          linePos.push(lon)
          linePos.push(lat)
        
          return  Cesium.Cartesian3.fromDegreesArray(linePos)
      }, false),
          shape: computeCircle(6.0),
          material: Cesium.Color.RED,
      }
  })

5.2 获取当前时间点已走过的轨迹数组

原理:通过输入当前时间,计算起始时刻与当前时间的时间差,利用轨迹点的累计时间去对比,时间差大于轨迹点时间,则证明该点已经走过,否则未走过。

function _getLastPoint(time) {
    const timeDifference = Cesium.JulianDate.secondsDifference(time, start);
    let currentTime = 0
    let lastPointList = []
    for (let i = 1; i < positionData.length; i++) {
        currentTime = positionData[i].time
        if (timeDifference < currentTime) {
            lastPointList.push([positionData[i - 1].longitude,positionData[i - 1].latitude])
            break
        } else {
            lastPointList.push([positionData[i - 1].longitude,positionData[i - 1].latitude])
        }
    }
    return lastPointList
}

5.3 获取当前时间点已走过两遍的轨迹数组(即从返程数组里去除往程数组)

原理:通过输入当前时间,计算起始时刻与当前时间的时间差,利用轨迹点的累计时间去对比,时间差大于轨迹点时间,则证明该点已经走过,否则未走过。 判断方法:首先让轨迹数组组成轨迹线段,然后去判断当前时间点是都在轨迹线段上,若在,则证明该轨迹线段的终点应删除,若不在,则保留(注意该方法部分特殊情况会有瑕疵)

// 判断管线点组中是否有冗余点,有冗余点则删除冗余点后的元素
// 输入:轨迹点,管线点组
function _isRepeatPoint(point, posList) {
    if(posList.length == 1) return posList.flat()
    for (let i = 1; i < posList.length; i++) {
        //斜率不存在 
        if (posList[i][0] == posList[i - 1][0]) {
            if(point[0] == posList[i][0]){
                posList.splice(i)
                return posList.flat()
            }
        } else {
            // 斜率为0
            if(posList[i][1] == posList[i-1][1]){
                if(point[1] == posList[i][1]){
                posList.splice(i)
                return posList.flat()
            }
            } else {
                // 正常斜截式
                k = (posList[i][1]-posList[i-1][1])/(posList[i][0]-posList[i-1][0])
                b = posList[i][1] - k*posList[i][0] 
                if (point[1].toFixed(4) == (k*point[0] + b).toFixed(4)) {
                    posList.splice(i)
                    return posList.flat()
                }
            }
        }

    }
    return posList.flat()
}

六、全部代码及效果图

004实时往返轨迹线.gif

<!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>动态模型+实时往返轨迹线demo</title>
    <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(112.402664,34.621038,4798.56),
            orientation :{
                heading : Cesium.Math.toRadians(351.9),
                pitch : Cesium.Math.toRadians(-85.7),
                roll :0.0
            }
        });
        setTimeout(2000)
        // 定义中转点数据
        var positionData = [
                    { time: 0, longitude: 112.40641179342809, latitude: 34.6280090036493 },   //A
                    { time: 5, longitude: 112.39561868811835, latitude:34.61691889996022 },  //B
                    { time: 10, longitude: 112.38948151752913, latitude: 34.6286049476503 },  //C
                    { time: 15, longitude: 112.38948151752913, latitude: 34.6286049476503 },  //C
                    { time: 20, longitude: 112.39561868811835, latitude:34.61691889996022 },  //B
                    { time: 25, longitude: 112.40641179342809, latitude: 34.6280090036493 },   //A
                ];

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

        // 计算位置差值
        var positionSampler = new Cesium.SampledPositionProperty();
        //差值器(预防拐弯突兀等问题)
         positionSampler.setInterpolationOptions({
            interpolationDegree: 1,
            interpolationAlgorithm: Cesium.LinearApproximation
        });
        for (var i = 0; i < positionData.length; i++) {
            var data = positionData[i];
            var time = new Cesium.JulianDate.addSeconds(start, data.time, new Cesium.JulianDate());
            var position = new Cesium.Cartesian3.fromDegrees(data.longitude, data.latitude);
            positionSampler.addSample(time, position);
        }

        // 创建一个模型对象
        var fireMan_entity = viewer.entities.add({
            name: 'model',
            model: {
                uri: "Data/chick_run.glb",
                show: true,
                scale: 2000,
            },
            position: positionSampler,
            orientation : new Cesium.VelocityOrientationProperty(positionSampler)
        });
        
        // 创建一个轨迹线对象
        var fireHose_entity = viewer.entities.add({
            name: 'line',
            polylineVolume: {
            positions: new Cesium.CallbackProperty( (time) =>{
                var tarpos = fireMan_entity.position.getValue(time);
                if(!tarpos) return
                var cartographic = Cesium.Ellipsoid.WGS84.cartesianToCartographic(tarpos);
                var lon = Cesium.Math.toDegrees(cartographic.longitude);
                var lat = Cesium.Math.toDegrees(cartographic.latitude);

                let pos = _getLastPoint(time)
                pos = _isRepeatPoint([lon,lat],pos).flat()

                pos.push(lon)
                pos.push(lat)
                
                return  Cesium.Cartesian3.fromDegreesArray(pos)
            }, false),
                shape: computeCircle(20.0),
                material: Cesium.Color.RED,
            }
        })
        // 设置视角跟随物体运动,并显示信息框
        // viewer.trackedEntity = fireMan_entity;

        // 定义时钟参数
        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; 

        // 判断当前时间下,经过的中转点,构成管线点组
        function _getLastPoint(time) {
            const timeDifference = Cesium.JulianDate.secondsDifference(time, start);
            let currentTime = 0
            let lastPointList = []
            for (let i = 1; i < positionData.length; i++) {
                currentTime = positionData[i].time
                if (timeDifference < currentTime) {
                    lastPointList.push([positionData[i - 1].longitude,positionData[i - 1].latitude])
                    break
                } else {
                    lastPointList.push([positionData[i - 1].longitude,positionData[i - 1].latitude])
                }
            }
            return lastPointList
        }
        
        // 判断管线点组中是否有冗余点,有冗余点则删除冗余点后的元素
        // 输入:轨迹点,管线点组
        function _isRepeatPoint(point, posList) {
            if(posList.length == 1) return posList.flat()
            for (let i = 1; i < posList.length; i++) {
                //斜率不存在 
                if (posList[i][0] == posList[i - 1][0]) {
                    if(point[0] == posList[i][0]){
                        posList.splice(i)
                        return posList.flat()
                    }
                } else {
                    // 斜率为0
                    if(posList[i][1] == posList[i-1][1]){
                        if(point[1] == posList[i][1]){
                        posList.splice(i)
                        return posList.flat()
                    }
                    } else {
                        // 正常斜截式
                        k = (posList[i][1]-posList[i-1][1])/(posList[i][0]-posList[i-1][0])
                        b = posList[i][1] - k*posList[i][0] 
                        if (point[1].toFixed(4) == (k*point[0] + b).toFixed(4)) {
                            posList.splice(i)
                            return posList.flat()
                        }
                    }
                }
      
            }
            return posList.flat()
        }
        
        // 计算轨迹管道函数
        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;
        }
    </script>
</body>
</html>