前言
人生一迹,谨以此记录Cesium相关系列知识
问题提出:加载动态模型+实时往返轨迹线?
(重点在于实时往返轨迹线)
一、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()
}
六、全部代码及效果图
<!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>