目录
实现效果
实现思路
功能点
选择移动路线
加载模型和移动路线
重新运行
指定位置(经纬度点)开始移动
视角切换
到站提示
运行
停止
联动接口
完整代码
html
js逻辑
trainOperation.js
sourceData.js
gitee仓库项目代码
疑问解答
实现效果
三维地图Cesium,加载一个模型,模型沿着给定的一组经纬度
实现思路
- 车的运行使用czml文件的特性来加载小车模型和小车运行的固定路线,同时定义它们是怎么随时间的变化而变化的。
- 路径长度已知,运行速度已知,结合三维cesium的时钟,就可以知道火车移动完成这段路线需要的时间。
- 改变运行速度或改变当前移动的位置(经纬度点)发生变化,就重新计算时间,然后重新加载czml文件。
功能点
目前已经实现的功能:
1、加载指定可自定义火车模型
2、移动的路线可以自定义修改,也可以在地图上自己选择移动路线(取路线代码已有)
3、给定指定的移动速度
4、视角切换(第一视角、自由视角、跟随视角)
5、根据指定的经纬度点为起点开始移动
6、停止移动、开始移动、到站提示、从起点重新移动、默认相机的视角
7、根据实际业务,接口获取获取的速度、移动路线、车的状态(前进、静止、后退)来控制模型的状态
选择移动路线
1、方法:at3DMapGetPointTakingRoute()
2、初始化成功后,执行at3DMapGetPointTakingRoute函数
TO.at3DMapGetPointTakingRoute()
3、 打开页面,用鼠标点击需要的路线,页面会以红点的样式进行标注,每标注一个点,控制台会以数组的形式打印所有标注的点
4、 把打印数据赋值给routePath变化,完成了路线的数据采集
// 路线
export const routePath = [
[116.44411704836098,39.95279202225133],[116.44807033296253,39.956431604742875],
[116.45386442263022,39.952900124343756],[116.45546340934312,39.951275636111355],
[116.45567209160625,39.9494096918938],[116.4556296095857,39.94616403625391],
]
加载模型和移动路线
加载模型和路线的方式有好几种:
1)使用实体单独加载模型和移动路线
2)使用czml格式描述它们两个的关系
我们这里采用第二种,之前打算用第一种,加载起来是很方便,但是需要计算模型与移动路线的关系等细节,就先不打算了。。。
具体如何使用czml文件描述模型和移动路线的关系,网上有很多,可以自行查阅。
let testModel = window.location.origin + '/Apps/SampleData/models/GroundVehicle/GroundVehicle.glb';
export function modelCzml(useRoutePath,startTime,endTime){
let minx = startTime+'/'+endTime;
return [
{
"id": "document",
"version": "1.0"
},
{
"id": "Vehicle",
"availability": minx ,
"label": {
"fillColor": [
{
"rgba": [0, 0, 0, 255]
}
],
"font": "bold 10pt Segoe UI Semibold",
"horizontalOrigin": "CENTER",
"outlineColor": {
"rgba": [0, 0, 0, 255]
},
"pixelOffset": {
"cartesian2": [40.0, 50.0]
},
"scale": 1.0,
"show": [
{
"boolean": true
}
],
"style": "FILL",
"text": "测试火车移动",
"verticalOrigin": "CENTER"
},
"model": {
"gltf": testModel,
"minimumPixelSize": 120,
"maximumScale": 50
},
"orientation": {
"velocityReference": "#position"
},
"viewFrom": {
// "cartesian": [300, 20, 300]
"cartesian": [500, 2000, 779]
},
"properties": {
"fuel_remaining": {
"epoch": startTime,
"number": [0, 22.5, 1500, 21.2]
}
},
"path": {
"material": {
"solidColor": {
"color": {
"rgba": [255, 255, 0, 255]
}
}
},
"width": [
{
"number": 4.0
}
],
"show": [
{
"boolean": true
}
]
},
"position": {
"interpolationAlgorithm": "LAGRANGE",
"interpolationDegree": 1,
"wrap": false,
"epoch": startTime,
"cartographicDegrees": useRoutePath
// useRoutePath:时间、经度、纬度、高度加载显示的格式为[0, 102.23404378554466, 27.825736605050523, 2500,10, 102.23691954070244, 27.82887625908256, 2500,]
}
}
]
}
重新运行
1、使用trainLoadRun()函数
2、该方法接收三个参数:
1)targetPoint:起点的经纬度,也就是路线起点
2)currentVelocity:移动速度
3)tractionMethod:火车移动状态(前进 后退 静止)
trainLoadRun(targetPoint,currentVelocity=this.currentVelocity,tractionMethod=this.tractionMethod){
// 如果当前火车是后退,判断当前火车是否在运行路线上,如果在,则找到当前火车所在点,并反转火车路线数组
let that = this;
if (that.latAndLonPosition(targetPoint).exists) {
let index = that.latAndLonPosition(targetPoint).index
that.byLonAndLatUpdateTrainPosition(index, currentVelocity, tractionMethod)
} else {
console.error('当前经纬度不在轨迹运行路线上:',targetPoint)
}
}
指定位置(经纬度点)开始移动
也是用targetPoint方法,改变下targetPoint的位置
注意:给的经纬度点要是不在移动路线上是不做处理的
视角切换
1、第一视角
1)相机一直在相对小车指定的位置
2)该模式无法通过鼠标或者键盘操作地图视角
3)路线方向变化,车的头部以及方向也随着变化
firstPerspectiveTow() {
let that = this;
try {
let center = that.trainModel.position.getValue(
that.viewer.clock.currentTime
);
let orientation = that.trainModel.orientation.getValue(
that.viewer.clock.currentTime
)
let transform = that.Cesium.Matrix4.fromRotationTranslation(that.Cesium.Matrix3.fromQuaternion(orientation), center);
// viewer.camera.lookAtTransform(transform, new Cesium.Cartesian3(-100, 0, 50))
that.viewer.camera.lookAtTransform(transform, new that.Cesium.Cartesian3(-60, 0, 50))
} catch (e) {
console.log('err in firstPerspectiveTow function')
}
}
2、自由视角
1)将viewer.trackedEntity的值为:undefined即可
viewer.trackedEntity = undefined;
3、跟随视角
viewer.trackedEntity = trainOpera.trainModel;
到站提示
根据当前已经移动的距离与总路线的距离作比较,总路线减去已经移动的路线,误差值在10米为到站,当然,这个误差值可以自己定义
arriveAtStation(clock,callBack){
let that = this;
let cDistance = that.walkedThrough();
let diff = Math.abs(that.fullRoutePathDistance.total - cDistance);
if (that.tractionMethod == '后退'){
diff = that.fullRoutePathDistance.total - diff
}
if (diff < 9){
// 可以在这里分发到站广播
callBack && callBack();
clock.shouldAnimate = false;
that.arriveAatStation()
}
}
运行
停止
设置时钟的shouldAnimate为false即可
clock.shouldAnimate = false;
联动接口
或者的运行速度,运行状态(前进、静止、后退)等信息,通过接口的形式可以进行控制
接口数据更新的时间根据自己的事迹情况来定
如:获取接口数据的方法
// 通过接口实时获取火车运行的相关信息
getTrainInfo() {
let that = this;
let res = {currentlonLat:'112.938333,40.332444',tractionMethod:'前进',speed:23}; //火车当前前进方式,3种状态:前进 后退 静止
if (!res || res === '{}') return false;
let currentSpeed;
if (res.speed && res.speed.includes('km/h')){
currentSpeed = res.speed.split('km/h')[0] //火车运行速度
}
// 获取当前火车所在经纬度
let targetPoint;
if (res.currentlonLat){
let point = res.currentlonLat.split(',');
targetPoint = {lon: point[0],lat: point[1]}
} else {
console.log('经纬度坐标丢失');
return false;
}
if (res.tractionMethod == '静止' || currentSpeed == '0'){
that.tractionMethod = res.tractionMethod;
that.viewer.clock.shouldAnimate = false;
return false;
}
// 如果页面火车已经到了,但是接口没有返回"静止"状态,前端做停止运行动作
if (window.isEmit && res.tractionMethod != '静止'){
window.TO.viewer.clock.shouldAnimate = false;
return false;
} else {
window.isEmit = false;
}
that.viewer.clock.shouldAnimate = true;
// 运行状态不一样,要重新计算
if (that.tractionMethod != res.tractionMethod){
that.currentVelocity = currentSpeed;
that.tractionMethod = res.tractionMethod;
that.trainLoadRun(targetPoint,that.currentVelocity,that.tractionMethod)
} else {
// 运行的速度不变,不做处理
if (that.currentVelocity == currentSpeed){
return false;
}
let diff = Math.abs(Number(currentSpeed) - Number(that.currentVelocity));
if (diff < 1 ){
return false;
}
that.currentVelocity = currentSpeed;
that.tractionMethod = res.tractionMethod;
that.trainLoadRun(targetPoint,that.currentVelocity,that.tractionMethod)
}
}
可以在init()初始化的时候,用定时器的方式去更新数据
init(){
let that = this;
that.getTrainInfo();
window.trainTimer = window.setInterval(function (){
that.getTrainInfo && that.getTrainInfo();
}, 4000);
that.loadComputeData(that.routePath, that.currentVelocity, that.trainRun);
that.viewer.clock.onTick.addEventListener(function (clock){
that.handler(clock,that)
});
}
完整代码
html
<!DOCTYPE html>
<!--suppress ALL -->
<html lang="en">
<head>
<!-- Use correct character set. -->
<meta charset="utf-8"/>
<!-- Tell IE to use the latest, best version. -->
<meta content="IE=edge" http-equiv="X-UA-Compatible"/>
<!-- Make the application on mobile take up the full browser screen and disable user scaling. -->
<meta content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" name="viewport"/>
<title>Hello World!</title>
<script src="../Build/Cesium/Cesium.js"></script>
<style>
@import url(../Build/Cesium/Widgets/widgets.css);
html, body, #cesiumContainer {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
.btn{
position: fixed;
top: 5px;
left: 10px;
z-index: 9999;
display: flex;
align-items: center;
}
.btn .btn-item{
margin: 0 4px;
}
</style>
</head>
<body>
<div class="btn">
<div style="display: flex;align-items: center">
<span style="font-size: 14px;color: white">当前速度:</span>
<input value="20" placeholder="当前速度" id="currentSuDu" style="width: 100px;height: 20px">
</div>
<button class="btn-item" id="runBtn">运行</button>
<button class="btn-item" id="stopBtn">停止</button>
<button class="btn-item" id="restart">重新开始</button>
<button class="btn-item" id="startAtSpecifiedLocation">指定位置开始</button>
<button class="btn-item" id="firstViewBtn">第一视角</button>
<button class="btn-item" id="freeViewBtn">自由视角</button>
<button class="btn-item" id="followViewBtn">跟随视角</button>
</div>
<div id="cesiumContainer"></div>
<script type="module">
import { routePath } from './trainOperation/sourceData.js';
import TrainOpera from './trainOperation/trainOperation.js';
let Cesium = window.Cesium;
Cesium.Ion.defaultAccessToken = '你自己的token'
const viewer = new Cesium.Viewer("cesiumContainer", {});
window.viewer = viewer;
let TO = new TrainOpera(window.viewer.clock,routePath);
TO.init();
TO.addPolyline(TO.routePath);
window.TO = TO; // 存到全局对象
TO.at3DMapGetPointTakingRoute();
</script>
</body>
</html>
js逻辑
trainOperation.js
// noinspection LanguageDetectionInspection
import { modelCzml,operaDomStyle } from './sourceData.js';
function operaDom(trainOpera,clock){
let viewer = window.viewer;
operaDomStyle()
// 获取按钮元素
let currentSuDu = document.getElementById('currentSuDu');
let runBtn = document.getElementById('runBtn');
let stopBtn = document.getElementById('stopBtn');
let startAtSpecifiedLocation = document.getElementById('startAtSpecifiedLocation');
let restart = document.getElementById('restart');
let firstViewBtn = document.getElementById('firstViewBtn');
let freeViewBtn = document.getElementById('freeViewBtn');
let followViewBtn = document.getElementById('followViewBtn');
// 添加点击事件处理程序
currentSuDu.onblur = function (){
let dom = document.getElementById('currentSuDu');
if (!dom) return;
trainOpera.currentVelocity = this.value;
console.log('当前速度:',trainOpera.currentVelocity+'千米每小时');
}
runBtn.addEventListener('click', function (){
console.log('运行。。。')
trainOpera.trainRun()
});
stopBtn.addEventListener('click', function () {
console.log('暂停运行。。。')
clock.shouldAnimate = false;
});
startAtSpecifiedLocation.addEventListener('click', function () {
console.log('指定位置开始');
let targetPoint = {lon:116.44807033296253,lat:39.956431604742875};
trainOpera.trainLoadRun(targetPoint)
});
restart.addEventListener('click', function () {
console.log('从新头开始运行');
let targetPoint = {lon:trainOpera.originalRoute[0][0],lat:trainOpera.originalRoute[0][1]};
trainOpera.trainLoadRun(targetPoint)
});
firstViewBtn.addEventListener('click', function () {
console.log('已切换为第一视角')
clock.firstPerspective = true; // 控制第一视角
viewer.trackedEntity = trainOpera.trainModel;
});
freeViewBtn.addEventListener('click', function () {
console.log('已切换为自由视角')
clock.firstPerspective = false; // 控制第一视角
// 关闭视角跟随
viewer.trackedEntity = undefined;
});
followViewBtn.addEventListener('click', function () {
console.log('已切换为跟随视角')
clock.firstPerspective = false;
viewer.trackedEntity = trainOpera.trainModel;
});
}
export default class TrainOpera {
constructor(clock,routePath) {
window.isEmit = false; // 控制火车到站后,是否重新运行
this.clock = clock;
this.originalRoute = routePath;
this.camera = window.viewer.camera;
this.Cesium = window.Cesium;
this.viewer = window.viewer;
this.routePath = this.addInterpolation(routePath);
operaDom(this,clock);
clock.firstPerspective = false; // 默认不做第一视角
}
originalRoute = null; // 运行的路线,经纬度
trainModel = null; // 实体模型
startTime = '2023-10-27T12:00:00Z'; // 开始运行时间
currentVelocity = 20; //当前速度53千米每小时
tractionMethod = '前进' //火车当前前进方式,3种状态:前进 后退 静止
CameraPositionInfo = {}; // 当前的相机位置参数
newRoutePathIndex = 0; // 记录当前从那个短路开始计算新的数据
finishNeedTime = null;// 根据当前的速度运行,到终点需要多少时间
wagonNumber = '测试火车移动'; // 车号
discrepancyNum = 37; // 时钟秒速相差实际的37秒,不知道为什么。。。?clock.currentTime.secondsOfDay - 37才是现在时钟运行的秒速时间
useRoutePath = []; // 全局的加载好的路线
dataSource = null; // 当前加载的czml
onTickEvent = null;
timer = null
fullRoutePathDistance = { // 当前路程总长,点与点之间的距离多少米
total: 0, // 总的距离
distance:[],
positionBetween:[]
}
// 记录火车轨迹和时间点
saveLastPathAndTime = {
timer:[],
lanLat:[]
};
init(){
let that = this;
// that.getTrainInfo();
// window.trainTimer = window.setInterval(function (){
// that.getTrainInfo && that.getTrainInfo();
// }, 4000);
that.loadComputeData(that.routePath, that.currentVelocity, that.trainRun);
that.viewer.clock.onTick.addEventListener(function (clock){
that.handler(clock,that)
});
}
restartTrainRun(tractionMethod){
// 火车到站后监听重新启动
if (tractionMethod == '静止'){
window.TO.viewer.clock.shouldAnimate = false;
}
if (!window.isEmit) return;
if (tractionMethod == window.TO.tractionMethod) return;
window.isEmit = false;
console.log('restartTrainRun');
window.TO.timer = window.setInterval(function (){
window.TO && window.TO.getTrainInfo && window.TO.getTrainInfo();
}, 3000);
}
// 火车到站
arriveAatStation(){
let that = this;
window.isEmit = true;
console.log('The train has arrived at the station');
// that.viewer.clock.currentTime = new that.Cesium.JulianDate(that.viewer.clock.stopTime.dayNumber,that.viewer.clock.stopTime.secondsOfDay - that.discrepancyNum);
window.clearInterval(that.timer);
window.clearInterval(window.TO.timer);
that.viewer.clock.shouldAnimate = false;
alert('火车到站')
}
// 获取指定范围内的随机数
getRandomInt(min,max){
return Math.floor(Math.random() * (max - min +1)) + min;
}
// 通过接口实时获取火车运行的相关信息
getTrainInfo() {
return ;
let that = this;
let res = {currentlonLat:'112.938333,40.332444',tractionMethod:'前进',speed:23}; //火车当前前进方式,3种状态:前进 后退 静止
if (!res || res === '{}') return false;
let currentSpeed;
if (res.speed && res.speed.includes('km/h')){
currentSpeed = res.speed.split('km/h')[0] //火车运行速度
}
// 获取当前火车所在经纬度
let targetPoint;
if (res.currentlonLat){
let point = res.currentlonLat.split(',');
targetPoint = {lon: point[0],lat: point[1]}
} else {
console.log('经纬度坐标丢失');
return false;
}
if (res.tractionMethod == '静止' || currentSpeed == '0'){
that.tractionMethod = res.tractionMethod;
that.viewer.clock.shouldAnimate = false;
return false;
}
// 如果页面火车已经到了,但是接口没有返回"静止"状态,前端做停止运行动作
if (window.isEmit && res.tractionMethod != '静止'){
window.TO.viewer.clock.shouldAnimate = false;
return false;
} else {
window.isEmit = false;
}
that.viewer.clock.shouldAnimate = true;
// 运行状态不一样,要重新计算
if (that.tractionMethod != res.tractionMethod){
that.currentVelocity = currentSpeed;
that.tractionMethod = res.tractionMethod;
that.trainLoadRun(targetPoint,that.currentVelocity,that.tractionMethod)
} else {
// 运行的速度不变,不做处理
if (that.currentVelocity == currentSpeed){
return false;
}
let diff = Math.abs(Number(currentSpeed) - Number(that.currentVelocity));
if (diff < 1 ){
return false;
}
that.currentVelocity = currentSpeed;
that.tractionMethod = res.tractionMethod;
that.trainLoadRun(targetPoint,that.currentVelocity,that.tractionMethod)
}
}
trainLoadRun(targetPoint,currentVelocity=this.currentVelocity,tractionMethod=this.tractionMethod){
// 如果当前火车是后退,判断当前火车是否在运行路线上,如果在,则找到当前火车所在点,并反转火车路线数组
let that = this;
if (that.latAndLonPosition(targetPoint).exists) {
let index = that.latAndLonPosition(targetPoint).index
that.byLonAndLatUpdateTrainPosition(index, currentVelocity, tractionMethod)
} else {
console.error('当前经纬度不在轨迹运行路线上:',targetPoint)
}
}
byLonAndLatUpdateTrainPosition(index,velocity, method){
let that = this;
that.newRoutePathIndex = index;
that.currentVelocity = velocity;
let useRoutePath = []
if (method == '前进') {
useRoutePath = that.routePath.slice(index);
that.loadComputeData(useRoutePath,that.currentVelocity,that.trainRun);
}
if (method == '后退') {
useRoutePath = that.routePath.slice(0,index).reverse();
that.loadComputeData(useRoutePath,that.currentVelocity,that.trainRun);
}
if (method == '静止') {
window.viewer.clock.shouldAnimate = false;
clearInterval(that.timer)
}
}
// 判断一个经纬度坐标点,是否在某条由经纬度组成的路线,并返回该点所在的位置
latAndLonPosition(targetPoint){
// targetPoint:{lon:'83.486845',lat:'42.514477'}
let that = this;
let ret;
let tPoint = that.Cesium.Cartographic.fromDegrees(targetPoint.lon, targetPoint.lat);
let minDistance = Infinity;
let positionIndex = 0;
if (!that.routePath.length){
return { exists: false }
}
for (let i = 0; i < that.routePath.length; i++) {
const routePoint = that.routePath[i];
const pointCartographic = that.Cesium.Cartographic.fromDegrees(...routePoint);
const distance = that.distance(pointCartographic, tPoint);
if (distance < minDistance) {
minDistance = distance;
positionIndex = i;
}
}
let routePathStart = that.Cesium.Cartographic.fromDegrees(...that.routePath[0]);
let routePathEnd = that.Cesium.Cartographic.fromDegrees(...that.routePath[that.routePath.length - 1]);
let routeDistance = that.distance(routePathStart, routePathEnd);
if (minDistance < routeDistance) {
// 目标点在路线上 positionIndex:所在位置
ret = { index:positionIndex, exists: true }
} else {
// 目标点不在路线上
ret = { exists: false }
}
return ret
}
walkedThrough(){
let that = this;
let setCTimer = new that.Cesium.JulianDate(that.clock.currentTime.dayNumber,that.clock.currentTime.secondsOfDay - that.discrepancyNum)
let center = that.trainModel.position.getValue(setCTimer);
let currentPosition = that.cartesian3_to_WGS84(center);
let positionIndex = that.latAndLonPosition(currentPosition)
let cDistance = 0;
if (!that.fullRoutePathDistance.distance.length) return 0;
for (let i = 0; i < that.fullRoutePathDistance.distance.length; i++) {
if (i === positionIndex.index) break;
cDistance = cDistance + that.fullRoutePathDistance.distance[i]
}
return cDistance // 走了多少距离
}
computerFullRoutePathDistance(routePath){
let that = this;
for (let i = 0; i < routePath.length; i++) {
if (i < routePath.length-1 ){
let startPosition = that.Cesium.Cartographic.fromDegrees(...routePath[i]);
let endPosition = that.Cesium.Cartographic.fromDegrees(...routePath[i+1]);
let towPosition = that.distance(startPosition,endPosition);
that.fullRoutePathDistance.distance.push(towPosition);
that.fullRoutePathDistance.positionBetween.push(routePath[i][0]+'-'+routePath[i+1][0]);
that.fullRoutePathDistance.total = (that.fullRoutePathDistance.total + towPosition)
}
}
}
timeFormat(){
let now = new Date();
let year = now.getFullYear();
let month = ("0" + (now.getMonth() + 1)).slice(-2); // 获取两位数的月份
let day = ("0" + now.getDate()).slice(-2); // 获取两位数的日期
let hours = ("0" + now.getHours()).slice(-2); // 获取两位数的小时
let minutes = ("0" + now.getMinutes()).slice(-2); // 获取两位数的分钟
let seconds = ("0" + now.getSeconds()).slice(-2); // 获取两位数的秒数
// 返回格式:如2023-10-27T12:00:00Z
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}Z`
}
//将秒转换为分钟、小时
convertSecondsToMinutesHours(seconds) {
let minutes = Math.floor(seconds / 60);
let hours = Math.floor(minutes / 60);
return {
minutes,
hours
}
}
//加载czml文件
reloadCzml(czml,callBack) {
let that = this;
that.delModelEntity(that.dataSource); // 更新前,需删除之前的数据
that.viewer.dataSources.add(that.Cesium.CzmlDataSource.load(czml)).then(function (ds){
that.dataSource = ds
callBack && callBack(ds)
})
}
// 根据路程和当前速度,计数走完需要多少时间
countingRate(v,s) {
// 单位是米和秒
// 速度(v)、距离(s)和时间(t)之间的关系可以用以下的数学公式表示:s = v × t
//速度 (km/h) = 速度 (m/s) × 3.6
let t1 = s / (v*1000/3600); // km
let t2 = s / v; // m
return t1
}
trainRun(fn){
let clock = window.viewer.clock;
// clock.firstPerspective = true;
clock.shouldAnimate = true;
clock.multiplier = 1;
fn && fn()
}
// 计算带时间的路径
createDynamicPositions(positions,totalTime) {
let that = this;
// 将秒数,平均分给每个坐标点,每个坐标点应该到的时间
if (!positions || !positions.length) return;
let useRoutePath = [];
let avgT = Number(totalTime)/positions.length;
let cur = 0;
// 重新计数
that.saveLastPathAndTime = { timer: [], lanLat: []};
for (let i = 0; i < positions.length; i++) {
cur = cur + avgT;
useRoutePath.push(cur,positions[i][0],positions[i][1],0);
that.saveLastPathAndTime.timer.push(cur);
that.saveLastPathAndTime.lanLat.push(positions[i][0]);
}
useRoutePath[0] = 0;
that.useRoutePath = useRoutePath;
return {
useRoutePath,
saveLastPathAndTime:that.saveLastPathAndTime,
};
}
// 给一个时间格式字符串和一个添加的秒数,返回添加秒数后的最终时间
addSeconds(timeString, secondsToAdd) {
// var time = "2012-08-04T15:00:00Z"; console.log(addSeconds(time, 5670)); 输出新的时间字符串
// 将字符串转化为Date对象
let date = new Date(timeString);
// 在Date对象上加上秒数
date.setSeconds(date.getSeconds() + secondsToAdd);
return date.toISOString(); // 将新的Date对象转换回字符串
}
//修改当前的速度
setCurrentVelocity(currentVelocity){
let that = this;
// 保存当前相机的位置
that.CameraPositionInfo = that.getCameraPositionInfo();
if (currentVelocity < 0) currentVelocity = 1;
let secondEd = that.clock.currentTime.secondsOfDay - that.discrepancyNum;
let distanceEd = secondEd*(that.currentVelocity*1000 / 3600);
// 找到移动到的经纬度点
let atBetweenIndex = 0;
let totalPathEd = 0;
let originalDistance = that.fullRoutePathDistance.distance;
for (let i = 0; i < originalDistance.length; i++) {
totalPathEd = totalPathEd + originalDistance[i];
if (totalPathEd > distanceEd){
atBetweenIndex = i;
break
}
}
// 已经移动到的经纬度坐标点
let curMeter = 0;
let currentLan = that.fullRoutePathDistance.positionBetween[atBetweenIndex];
for (let i = 0; i < that.routePath.length; i++) {
if (currentLan.includes(that.routePath[i][0])){
curMeter = i // 记录当前从那个短路开始计算新的数据
break
}
}
that.newRoutePathIndex = that.newRoutePathIndex + curMeter;
let newRoutePath = that.routePath.slice(that.newRoutePathIndex);
that.loadComputeData(newRoutePath,currentVelocity,that.trainRun)
}
// 加载最新数据
loadComputeData(routePath,Velocity = this.currentVelocity,fn){
let that = this;
//routePath:由经纬度组成的路程
// currentVelocity = 30; // 每秒30米的速度行驶
let endTime; // 运行结束时间
that.currentVelocity = Velocity;
let finishNeedTime; // 走完需要多少时间 单位是秒s
let distanceMeter; // 起点到终点多少米远
distanceMeter = that.totalJourney(routePath);
finishNeedTime = that.countingRate(Velocity,distanceMeter);
that.finishNeedTime = finishNeedTime;
endTime = that.addSeconds(this.startTime,finishNeedTime);
let ret = that.createDynamicPositions(routePath,finishNeedTime);
that.saveLastPathAndTime = ret.saveLastPathAndTime; // 路程与时间
that.reloadCzml(modelCzml(ret.useRoutePath,that.startTime,endTime),function (ds){
let trainModel = ds.entities.getById('Vehicle');
that.trainModel = trainModel;
that.viewer.trackedEntity = trainModel;
// 设置模型可以随路径方向转向
// trainModel.orientation = new Cesium.VelocityOrientationProperty(trainModel.position);
// trainModel.model.alignedAxis = new Cesium.VelocityVectorProperty(trainModel.position, true);
// 更新相机位置(第一视角)
// tool.firstPerspective()
fn && fn()
})
return finishNeedTime
}
// 找出当前的速度改变前,火车在的位置,利用数组的下标的方式来记录,根据当前时间的秒速去找,对比,取下一个地点
currentMovingLocation(arr, num){
// 添加数字到数组中
arr.push(num);
// 排序数组
arr.sort((a, b) => a - b);
// 查找数字在数组中的位置(下标)
let index = arr.indexOf(num);
return {
index: index,
sortedArray: arr
};
}
// 修改当前时钟的时间,从那个位置开始运动
setClockCurrentTime(second){
let that = this;
that.clock.currentTime = new that.Cesium.JulianDate(that.clock.currentTime.dayNumber, second);
that.clock.multiplier = 1;
that.clock.shouldAnimate = true;
}
// 删除实体和模型
delModelEntity(dataSource){
let that = this;
if (dataSource){
that.viewer.dataSources.remove(dataSource);
}
}
// 第一视角
firstPerspective(){
let that = this;
//window.trainModel实体
const orientation = that.trainModel.orientation;
const position = that.trainModel.position;
function main() {
try {
if (that.viewer.clock.shouldAnimate === true) {
let ori = orientation.getValue(that.viewer.clock.currentTime); // 获取偏向角
let center = position.getValue(that.viewer.clock.currentTime); // 获取位置
// 1、由四元数计算三维旋转矩阵
let mtx3 = that.Cesium.Matrix3.fromQuaternion(ori);
// 2、计算四维转换矩阵:
let mtx4 = that.Cesium.Matrix4.fromRotationTranslation(mtx3, center);
// 3、计算角度:
let hpr = that.Cesium.Transforms.fixedFrameToHeadingPitchRoll(mtx4);
// 获取角度(弧度)
const headingTemp = hpr.heading;
const pitchTemp = hpr.pitch;
// 调整角度为第一人称
const heading = that.Cesium.Math.toRadians(that.Cesium.Math.toDegrees(headingTemp) + 90);
const pitch = that.Cesium.Math.toRadians(that.Cesium.Math.toDegrees(pitchTemp) - 12);
// 视角高度,根据模型大小调整
const range = 140.0;
// 动态改变模型视角
that.viewer.camera.lookAt(center, new that.Cesium.HeadingPitchRange(heading, pitch, range));
}
} catch (e) {
console.log('err in firstPerspective function')
}
}
that.onTickEvent = that.viewer.clock.onTick.addEventListener(main);
}
firstPerspectiveTow() {
let that = this;
try {
let center = that.trainModel.position.getValue(
that.viewer.clock.currentTime
);
let orientation = that.trainModel.orientation.getValue(
that.viewer.clock.currentTime
)
let transform = that.Cesium.Matrix4.fromRotationTranslation(that.Cesium.Matrix3.fromQuaternion(orientation), center);
// viewer.camera.lookAtTransform(transform, new Cesium.Cartesian3(-100, 0, 50))
that.viewer.camera.lookAtTransform(transform, new that.Cesium.Cartesian3(-60, 0, 50))
} catch (e) {
console.log('err in firstPerspectiveTow function')
}
}
// 根据经纬度坐标,计算总的路程有多少米
totalJourney(routePaths){
let that = this;
if (!routePaths || !routePaths.length) return 0;
let totalJourney = 0;
for (let i = 0; i < routePaths.length; i++) {
if (i < routePaths.length-1 ){
let startPosition = that.Cesium.Cartographic.fromDegrees(...routePaths[i]);
let endPosition = that.Cesium.Cartographic.fromDegrees(...routePaths[i+1]);
let towPosition = that.distance(startPosition,endPosition);
totalJourney = totalJourney + towPosition
}
}
return totalJourney
}
arriveAtStation(clock,callBack){
let that = this;
let cDistance = that.walkedThrough();
let diff = Math.abs(that.fullRoutePathDistance.total - cDistance);
if (that.tractionMethod == '后退'){
diff = that.fullRoutePathDistance.total - diff
}
if (diff < 9){
// 可以在这里分发到站广播
callBack && callBack();
clock.shouldAnimate = false;
that.arriveAatStation()
}
}
// 画一条路线
addPolyline(routePath){
let that = this;
if (!routePath || !routePath.length) return;
let positions = [];
routePath.forEach( el =>{
positions.push(that.Cesium.Cartesian3.fromDegrees(...el))
})
return that.viewer.entities.add({
id: 'MY_POLYLINE',
name: 'MY_POLYLINE',
polyline: {
positions: positions, // 设置轨道的位置点
material: that.Cesium.Color.YELLOW.withAlpha(1.0),
width: 4 // 设置轨道的宽度。
}
});
}
getCameraPositionInfo(){
let that = this;
let position = that.viewer.camera.position; // 获取当前相机位置
let offsetHeading = that.viewer.camera.heading; // 获取当前相机偏移角度
let pitch = that.viewer.camera.pitch; // 获取当前相机俯视角
let roll = that.viewer.camera.roll; // 获取当前相机的滚动角
// viewer.camera.setView(cameraPositionInfo); // 设置相机位置
return {
destination: position,// 经度,纬度,高度
orientation: that.Cesium.HeadingPitchRoll.fromDegrees(offsetHeading, pitch, roll) // 偏航角,俯仰角,滚动角
}
}
throttle(func, delay) {
let lastCall = 0;
return function(...args) {
const now = new Date().getTime();
if (now - lastCall < delay) {
return;
}
lastCall = now;
return func.apply(this, args);
};
}
// 两个经纬度之间的插值,返回相差的经纬度坐标
interpolation(startPosition,endPosition){
let that = this;
// 两个参数接收的类型和值:
// startPosition:[83.787568,42.919772]
// endPosition:[83.484661,42.515142]
let nextPath = []; // 存放两个经纬度坐标之间的差值
// nextPath.push(startPosition); // 火车有时候有可能会变成闪动
let sPosition = new that.Cesium.Cartesian3.fromDegrees(...startPosition);
let ePosition = new that.Cesium.Cartesian3.fromDegrees(...endPosition);
let differenceValue = 0;
// Cesium.Cartesian3.lerp(startPosition, endPosition, differenceValue, new Cesium.Cartesian3()) // 计数两点之间的插值
while (differenceValue <= 1){
// 计数两个经纬度之间的差值,分成--份
differenceValue = differenceValue + 0.05; // 0.05
let coordinateD = that.Cesium.Cartesian3.lerp(sPosition, ePosition, differenceValue, new that.Cesium.Cartesian3());
let wgs84Point = that.cartesian3_to_WGS84(coordinateD);
nextPath.push([wgs84Point.lon, wgs84Point.lat])
}
return nextPath
}
// 笛卡尔坐标转化为经纬度坐标
cartesian3_to_WGS84(point) {
let that = this;
let cartesian3 = new that.Cesium.Cartesian3(point.x, point.y, point.z);
let cartographic = that.Cesium.Cartographic.fromCartesian(cartesian3);
let lat = that.Cesium.Math.toDegrees(cartographic.latitude);
let lon = that.Cesium.Math.toDegrees(cartographic.longitude);
let alt = cartographic.height;
return { lat, lon, alt };
}
// 添加插值
addInterpolation(routePath){
let that = this;
if (!routePath || !routePath.length) return;
/**
// bd09towgs84 百度坐标转换为wgs84坐标
let wgs84 = [];
for (let i = 0; i < routePath.length; i++) {
let a = window.coordtransform.bd09togcj02(routePath[i][0],routePath[i][1]);
let b = window.coordtransform.gcj02towgs84(a[0],a[1]);
wgs84.push(b)
}
routePath = wgs84;
**/
let arr = [];
for (let i = 0; i < routePath.length; i++) {
if (i < routePath.length-1){
// 做个优化,如何两个点直接的距离小于15米不做差值计算
let startPosition = that.Cesium.Cartographic.fromDegrees(...routePath[i]);
let endPosition = that.Cesium.Cartographic.fromDegrees(...routePath[i+1]);
let ret = that.distance(startPosition,endPosition);
if (5 > ret) continue;
let opera = that.interpolation(routePath[i],routePath[i+1]);
arr = arr.concat(opera)
}
}
// 原路线的起点和终点要保持和源数据一致,不能丢失
if (arr[0][0] != routePath[0][0]) arr.unshift(routePath[0]);
if (arr[arr.length -1][0] != routePath[routePath.length -1][0]) arr.push(routePath[routePath.length -1]);
that.computerFullRoutePathDistance(arr);
return arr
}
// 计数两个经纬度之间距离多少米
distance(startPosition, endPosition) {
let that = this;
// let startPosition = Cesium.Cartographic.fromDegrees(117.270739, 31.84309);
// let endPosition = Cesium.Cartographic.fromDegrees(117.270739, 31.85309);
let geodesic = new that.Cesium.EllipsoidGeodesic();
geodesic.setEndPoints(startPosition, endPosition);
return geodesic.surfaceDistance; // 返回两点之间的距离
}
// 在三维地图上取点,地图上会打印选择的点坐标,可以用于去移动路线
at3DMapGetPointTakingRoute(){
window.viewer.entities.add({
//点的位置
position : Cesium.Cartesian3.fromDegrees(112.90365587012, 27.810901397823, 0),
//点
point : {
pixelSize : 5,//点的大小
color : Cesium.Color.RED,//点的颜色
outlineColor:Cesium.Color.GREEN,//外圈颜色
outlineWidth:1,//外圈大小
}
});
let coordinate = []
let str = ''
let handler = new window.Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
handler.setInputAction(function(event) {
let cartesian = window.viewer.camera.pickEllipsoid(event.position);
let cartographic = window.Cesium.Cartographic.fromCartesian(cartesian);
let lng = window.Cesium.Math.toDegrees(cartographic.longitude); // 经度
let lat = window.Cesium.Math.toDegrees(cartographic.latitude); // 纬度
let alt = cartographic.height; // 高度,椭球面height永远等于0
// 添加点
window.viewer.entities.add({
//点的位置
position : Cesium.Cartesian3.fromDegrees(lng, lat, 0),
//点
point : {
pixelSize : 5,//点的大小
color : Cesium.Color.RED,//点的颜色
outlineColor:Cesium.Color.GREEN,//外圈颜色
outlineWidth:1,//外圈大小
}
});
str += '['
str += lng
str += ','
str += lat
str += ']'
str += ','
coordinate.push([lng, lat])
console.log(coordinate);
console.log(str);
}, window.Cesium.ScreenSpaceEventType.LEFT_CLICK);
}
handler(clock,TO){
clock.multiplier = 1;
if (clock.firstPerspective){
// tool.throttle(tool.firstPerspectiveTow,50) && tool.throttle(tool.firstPerspectiveTow,50)();
TO.firstPerspectiveTow()
}
clock.shouldAnimate && TO.arriveAtStation(clock);
}
}
sourceData.js
/* eslint-disable */
// 路线
export const routePath = [
[116.44411704836098,39.95279202225133],[116.44807033296253,39.956431604742875],
[116.45386442263022,39.952900124343756],[116.45546340934312,39.951275636111355],
[116.45567209160625,39.9494096918938],[116.4556296095857,39.94616403625391],
]
// let testModel = window.location.origin + '/Apps/trainOperation/model/scene.gltf';
// let testModel = window.location.origin + '/Apps/SampleData/models/GroundVehicle/GroundVehicle.glb';
let testModel = window.location.origin + '/Apps/SampleData/models/GroundVehicle/GroundVehicle.glb';
export function modelCzml(useRoutePath,startTime,endTime){
let minx = startTime+'/'+endTime;
return [
{
"id": "document",
"version": "1.0"
},
{
"id": "Vehicle",
"availability": minx ,
"label": {
"fillColor": [
{
"rgba": [0, 0, 0, 255]
}
],
"font": "bold 10pt Segoe UI Semibold",
"horizontalOrigin": "CENTER",
"outlineColor": {
"rgba": [0, 0, 0, 255]
},
"pixelOffset": {
"cartesian2": [40.0, 50.0]
},
"scale": 1.0,
"show": [
{
"boolean": true
}
],
"style": "FILL",
"text": "测试火车移动",
"verticalOrigin": "CENTER"
},
"model": {
"gltf": testModel,
"minimumPixelSize": 120,
"maximumScale": 50
},
"orientation": {
"velocityReference": "#position"
},
"viewFrom": {
// "cartesian": [300, 20, 300]
"cartesian": [500, 2000, 779]
},
"properties": {
"fuel_remaining": {
"epoch": startTime,
"number": [0, 22.5, 1500, 21.2]
}
},
"path": {
"material": {
"solidColor": {
"color": {
"rgba": [255, 255, 0, 255]
}
}
},
"width": [
{
"number": 4.0
}
],
"show": [
{
"boolean": true
}
]
},
"position": {
"interpolationAlgorithm": "LAGRANGE",
"interpolationDegree": 1,
"wrap": false,
"epoch": startTime,
"cartographicDegrees": useRoutePath
// useRoutePath:时间、经度、纬度、高度加载显示的格式为[0, 102.23404378554466, 27.825736605050523, 2500,10, 102.23691954070244, 27.82887625908256, 2500,]
}
}
]
}
export function operaDomStyle(){
try {
document.getElementsByClassName('cesium-viewer-animationContainer')[0].style.visibility = 'hidden';
document.getElementsByClassName('cesium-viewer-bottom')[0].style.visibility = 'hidden';
document.getElementsByClassName('cesium-viewer-timelineContainer')[0].style.visibility = 'hidden';
document.getElementsByClassName('cesium-viewer-toolbar')[0].style.visibility = 'hidden';
document.getElementsByClassName('cesium-viewer-fullscreenContainer')[0].style.visibility = 'hidden';
} catch (e) {}
}
gitee仓库项目代码
wdn-cesiumCarRoute: 三维地图Cesium,加载一个模型,模型沿着给定的一组经纬度路线、速度、前进状态进行移动
疑问解答
需要解答(项目的运行、代码疑问、新功能开发...),可以给我留言哟。有空随时会解答