MapGIS Client for JavaScript实时动态更新车辆模型的位置与轨迹可视化
数字孪生作为当前 WebGIS 和智慧城市建设中的热点应用之一,被广泛应用于交通、安防、能源等多个领域。它通过虚拟与现实的联动,将传感器采集到的各类信息映射到三维场景中,实现数据的空间化可视、事件的实时化响应、系统的智能化管理。Cesium 作为一个高性能的三维地球引擎,具备良好的可视化能力和开放扩展性,成为构建三维数字孪生场景的主流技术选择。通过 Cesium,我们可以将车辆、人员、设备等实体以
前言
数字孪生作为当前 WebGIS 和智慧城市建设中的热点应用之一,被广泛应用于交通、安防、能源等多个领域。它通过虚拟与现实的联动,将传感器采集到的各类信息映射到三维场景中,实现数据的空间化可视、事件的实时化响应、系统的智能化管理。Cesium 作为一个高性能的三维地球引擎,具备良好的可视化能力和开放扩展性,成为构建三维数字孪生场景的主流技术选择。
通过 Cesium,我们可以将车辆、人员、设备等实体以三维模型的形式呈现在场景中,并结合实时数据动态驱动这些模型运动,直观地反映真实世界的变化过程。本文将介绍如何使用 Cesium 实现车辆模型的位置与轨迹可视化,以及如何动态更新模型的位置和轨迹。
效果预览

主要流程
- 添加一个模型实体和一个线实体;
- 每获取到一个点,即更新模型实体和新线实体的位置;
代码实现
这里我们选择封装一个类(ModelAnnimateTool)用于实现模型动画工具,方便后续扩展。
首先是构造函数,接收一个对象参数,包含 Cesium 的 Viewer 实例、模型 URL、模型参数、默认朝向、额外偏移角、是否显示轨迹等属性。
- defaultHeading:模型默认朝向(弧度),即接收到第一个点时,模型的朝向;
- headingOffset:额外偏移角(弧度),因为后续计算模型朝向时,都默认建模时模型朝向东方,以此为基准计算,如果模型不朝向东方,需要设置偏移量;
class ModelAnnimateTool {
constructor(options) {
this.viewer = options.viewer;
this.id = options.id || Math.random().toString(36).substr(2); // 生成一个随机id
this.modelUrl = options.modelUrl; // 模型的uri
this.modelOptions = options.modelOptions || {}; // 模型的参数
// 模型默认朝向(弧度)
this.defaultHeading = options.defaultHeading || -Math.PI / 2;
// 额外偏移角(弧度),可用于细调
this.headingOffset = options.headingOffset || 0;
this.showTrail = options.showTrail || false;
this._entity = null;
this._polylineEntity = null;
this._isPlaying = false;
this._currentIndex = 0;
this._positions = []; // 保存历史轨迹点
this.updatePosition = this.updatePosition.bind(this); // 可选
this._onTick = options.onTick || function () { };
this._createModelEntity();
if (this.showTrail) {
this._createTrailEntity();
}
}
// ... 方法
}
第一个方法,创建模型实体,即添加一个模型实体,用于显示模型。这里两个重要的参数是:
- position:模型的位置,这里使用了一个回调函数,每次渲染场景时都会调用这个函数,返回 this._positions 数组中的最新位置(最后一个元素);
- orientation:模型的方向,这里也使用了一个回调函数,每次渲染场景时都会调用这个函数,返回 this.updateOrientation() 的结果,即模型的朝向;
// 创建模型
_createModelEntity() {
if (!this.modelUrl) { throw new Error('modelUrl不能为空') };
// 初始化模型,即取position中的第一个点作为模型的初始点
this._entity = this.viewer.entities.add({
id: this.id,
//通过回调函数动态计算属性值,每当 Cesium 渲染场景时都会调用这个函数,使实体的位置始终保持在 this._positions 数组中的最新位置(最后一个元素)
position: new Cesium.CallbackProperty(() => {
return this._positions.length > 0 ? this._positions[this._positions.length - 1] : null;
}, false),
model: {
uri: this.modelUrl,
scale: this.modelOptions.scale || 1.0,
minimumPixelSize: this.modelOptions.minimumPixelSize || 64,
maximumScale: this.modelOptions.maximumScale || 200,
show: this.modelOptions.show || true,
},
//orientation: 一个属性,用于指定相对于地球固定地球中心 (ECEF) 的实体方向。如果未定义,则在实体位置使用 east-north-up。
orientation: new Cesium.CallbackProperty(() => this.updateOrientation(), false)
})
}
第二个函数,创建轨迹实体,即添加一个线实体,用于显示模型的运动轨迹。这里使用了一个回调函数,每次渲染场景时都会调用这个函数,返回 this._positions 数组中的所有位置,即模型的运动轨迹;
// 创建轨迹
_createTrailEntity() {
this._polylineEntity = this.viewer.entities.add({
name: `${this.id}-trail`,
polyline: {
positions: new Cesium.CallbackProperty(() => this._positions, false),
width: 2,
material: Cesium.Color.RED.withAlpha(0.5),
}
});
}
第三个函数,更新模型朝向,即计算模型的方向。这里使用了 Cesium 的 Cesium.Cartographic.fromCartesian() 方法,将笛卡尔坐标转换为地理坐标,然后计算两个点之间的经纬度差,再使用 Math.atan2() 方法计算航向角(heading),最后使用 Cesium.Transforms.headingPitchRollQuaternion() 方法将航向角转换为四元数,即模型的方向;
// 更新模型朝向
updateOrientation() {
if (this._positions.length < 2) {
const offsetHeading = this.defaultHeading - Cesium.Math.PI_OVER_TWO;
const orientation = Cesium.Transforms.headingPitchRollQuaternion(
this._positions[0],
new Cesium.HeadingPitchRoll(offsetHeading, 0, 0)
);
return orientation
}
// 两个及以上点时,计算当前运动方向的朝向
const p1 = this._positions[this._positions.length - 2];
const p2 = this._positions[this._positions.length - 1];
// 计算航向角(heading),pitch和roll保持0(水平)
const carto1 = Cesium.Cartographic.fromCartesian(p1);
const carto2 = Cesium.Cartographic.fromCartesian(p2);
const deltaLon = carto2.longitude - carto1.longitude;
const deltaLat = carto2.latitude - carto1.latitude;
const heading = Math.atan2(deltaLon, deltaLat);
// 如果模型默认朝东,需要调整偏移角度
const offsetHeading = heading - Cesium.Math.PI_OVER_TWO;
const orientation = Cesium.Transforms.headingPitchRollQuaternion(
p2,
new Cesium.HeadingPitchRoll(offsetHeading, 0, 0)
);
return orientation;
}
第四个函数,更新运动轨迹,即添加一个新的位置到 this._positions 数组中,并更新模型朝向;
// 更新运动轨迹
updatePosition = (position) => {
if (!position) {
throw new Error('position为空');
}
if (!position instanceof Cesium.Cartesian3) {
position = Cesium.Cartesian3.fromDegrees(position[0], position[1], position[2]); // 将经纬度转换为笛卡尔坐标
} else {
position = Cesium.Cartesian3.clone(position);
}
this._positions.push(position);
}
其他的一些方法,如获取模型、获取路径、销毁漫游实例等;
// 获取模型
// 用于用户自定义模型的样式属性
getEntity() {
return this._entity;
}
// 获取路径
// 用于用户自定义路径的样式属性
getTrailEntity() {
return this._polylineEntity;
}
// 销毁漫游实例
destroy() {
if (this._entity) this.viewer.entities.remove(this._entity);
if (this._polylineEntity) this.viewer.entities.remove(this._polylineEntity);
}
注意事项
tips1:如何判断模型是否朝向东方
绘制一条朝东的线(中国位于东半球,往东经度增加,因此经度增加,纬度相同的两个点,就是东西朝向),并在线的起点加一个模型,看模型的朝向即可。
const start = Cesium.Cartesian3.fromDegrees(114.408, 30.470);
const end = Cesium.Cartesian3.fromDegrees(114.518, 30.470);
// 添加线实体
viewer.entities.add({
polyline: {
positions: [start, end],
width: 3,
material: Cesium.Color.BLUE,
},
});
const car = viewer.entities.add({
name: 'car1',
position: start,
model: {
uri: './public/police_car.gltf',
minimumPixelSize: 128,
maximumScale: 20000,
},
})
viewer.flyTo(car);
更多推荐



所有评论(0)