Cesium | 利用Property机制实现轨迹回放

5,377 阅读3分钟

这是我参与更文挑战的第6天,活动详情查看: 更文挑战

前言

我们在前面的文章中介绍过Cesium的Property机制,了解了它的作用以及它的用法,这篇文章我们通过一个实际场景来深入学习和复习一下Property机制。

在智慧城市系统中,必不可少的就是轨迹回放或者让模型沿着指定路径行走,而Cesium提供的Property机制能够在很好的实现这样的场景的同时保证性能。下面简单来讲一下Property,更多的知识可以阅读Cesium的Property机制究竟有多香

property

简单来说,Property就是Cesium提供的一种机制,可以让指定的属性随时间自动变化并赋值,操作便捷的同时能够节省性能。在Cesium中Property可以与时间轴进行关联,并根据时间返回对应的属性,我们再通过返回的值去改变我i们想要修改的状态值。

实现

一样的,首先我们整理思路,想要实现轨迹回放,我们需要一组轨迹坐标串、一个定义好时间点状态的Property,以及一个用来在轨迹上行走的model(本文用billboard代替)。

我们拿到了轨迹坐标串后,需要通过计算获得几个关键节点的距离、时间,生成完整的property作为我们实体的position属性。

基本思路如上,接下来我们开始动手实现。思路只提供大致方向,在实现中可能会对某些方面进行部分修改。一般场景下我们得到的都是经纬度坐标,而操作时需要使用笛卡尔坐标。

let lnglatArr = [
    [121.527589, 38.957547],
    [121.527825, 38.960166],
    [121.536472, 38.959098],
    [121.540442, 38.958464],
    [121.543489, 38.958131],
    [121.542888, 38.955861],
    [121.542266, 38.953325],
]

let positions = lnglatArr.map((item) => {
    return Cesium.Cartesian3.fromDegrees(item[0] , item[1] , 0.5);
})

接下来我们需要获取回放中每段的时间以及总时间,这里我们需要通过速度和坐标点之间的距离算出所需时间(这是小学知识吧,t = l/s)。

我们需要记录每个点位的时间,用来为property添加节点,这里我们将它写成一个方法方便调用。入参就是我们的坐标串与速度。

// 计算每个点位时间和总时间
getSiteTimes(positions, speed) {
    let timeSum = 0;
    let times = [];
    for (let i = 0; i < positions.length; i++) {
        if (i === 0) {
            times.push(0);
            continue;
        }

        timeSum += this.spaceDistance([positions[i - 1], positions[i]]) / speed;
        times.push(timeSum);
    }
    return {
        timeSum,
        siteTimes: times,
    };
},

可以看到我们在循环内又定义了一个方法spaceDisatance,这个方法用来计算传入的两个点之间的距离。Cesium为我们提供了相关方法计算两点之间的距离。

// 计算距离
spaceDistance(positions) {
    let distance = 0;
    for (let i = 0; i < positions.length - 1; i++) {
        let point1cartographic = Cesium.Cartographic.fromCartesian(
            positions[i]
        );
        let point2cartographic = Cesium.Cartographic.fromCartesian(
            positions[i + 1]
        );
        /**根据经纬度计算出距离**/
        let geodesic = new Cesium.EllipsoidGeodesic();
        geodesic.setEndPoints(point1cartographic, point2cartographic);
        let s = geodesic.surfaceDistance;
        //返回两点之间的距离 如果带高度的话
        s = Math.sqrt(
            Math.pow(s, 2) +
            Math.pow(point2cartographic.height - point1cartographic.height, 2)
        );
        distance = distance + s;
    }
    return distance.toFixed(2);
},

现在我们拿到了总时间和每两个点之间的时间,现在可以开始构建property和时间轴了。首先我们需要定义开始时间结束时间并给到viewerclock实例上代表时间段,接着构筑Property实例。

let timeObj = this.getSiteTimes(positions, speed);
let startTime = Cesium.JulianDate.fromDate(new Date());
let stopTime = Cesium.JulianDate.addSeconds(
    startTime,
    timeObj.timeSum,
    new Cesium.JulianDate()
);

viewer.clock.startTime = startTime.clone();
viewer.clock.stopTime = stopTime.clone();
viewer.clock.currentTime = startTime.clone();

// 生成property
var property = new Cesium.SampledPositionProperty();
for (var i = 0; i < positions.length; i++) {
    const time = Cesium.JulianDate.addSeconds(
        startTime,
        timeObj.siteTimes[i],
        new Cesium.JulianDate()
    );
    property.addSample(time, positions[i]);
}

最后一步,我们添加一个Entity实体,做轨迹回放的物体,这里可以使用任何实体。添加到场景中后需要利用viewer.trackedEntity动态追踪实体。我们的轨迹回放就大功告成了。

this.animateEntity = viewer.entities.add({
    // 实体可用性,在指定时间内返回有效数据
    availability: new Cesium.TimeIntervalCollection([
        new Cesium.TimeInterval({
            start: startTime,
            stop: stopTime,
        }),
    ]),
    // 位置信息随时间变化property
    position: property,
    // 实体方向
    orientation: new Cesium.VelocityOrientationProperty(property),
    billboard: {
        image: "./static/blueCamera.png",
        verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
    },
    // 轨迹路径
    path: {
        resolution: 1,
        width: 10,
        material: Cesium.Color.RED,
    },
});

viewer.trackedEntity = this.animateEntity;

效果如下:

animate1.gif

封装

这里简单提一下利用Class进行封装吧,没有什么大的难点,就是把实现步骤拆分,选定参数进行暴露。这里简单书写一下代码结构。

export default class TrackedAnimate {
    constructor(viewer, cb) {
        this.viewer = viewer;
        this.cb = cb;
    }

    startRoam(positions, speed) {
        let newPositions = positions.map(item => {
            return Cesium.Cartesian3.fromDegrees(item[0], item[1], 0.5);
        });
        this.addRoamLine(newPositions, speed);
    }

    //添加漫游路线
    addRoamLine(positions, speed) {
        this.endRoam();

        let timeObj = this.getSiteTimes(positions, speed);
        let startTime = Cesium.JulianDate.fromDate(new Date()); 
        let stopTime = Cesium.JulianDate.addSeconds(
            startTime,
            timeObj.timeSum,
            new Cesium.JulianDate()
        );

        this.viewer.clock.startTime = startTime.clone();
        this.viewer.clock.stopTime = stopTime.clone();
        this.viewer.clock.currentTime = startTime.clone();

        let property = this.getSampledPositionProperty(
            positions,
            startTime,
            timeObj.siteTimes
        );
        this.addModel(startTime, stopTime, property);

        this.viewer.trackedEntity = this.animateEntity;
        this.timoutId = setTimeout(e => {
            this.endRoam();
        }, timeObj.timeSum * 1000);
    }

    addModel(startTime, stopTime, property) {
        //……
    }

    endRoam() {
        //……
    }

    getSampledPositionProperty(positions, startTime, siteTimes) {
        //……
    }

    //计算每个点位时间与总时间
    getSiteTimes(positions, speed) {
        //……
    }

    //计算距离
    spaceDistance(positions) {
        //……
    }
}

调用:

this.trackEntity = new TrackedAnimate(viewer , () => {
    console.log('moe_');
});

this.trackEntity.startRoam(this.movePositions, 25);
this.trackEntity.endRoam();

最后

上述我们简单实现了轨迹回放的场景,可以看到在封装中我们还设置了回调函数的参数,后续可以进行很多扩展比如多视角回放,特定位置做特定功能等等,都可以进行扩展。Property的使用场景有很多,这只是其中一部分,感兴趣的可以深入研究共同探讨。