cesium公交车轨迹漫游

个人博客:CSDN 博客-满分观察网友 z

演示地址:哔哩哔哩-满分观察网友 z

这是一个用 Cesium.js 做的公交车轨迹漫游,实现的功能有加载站点和道路轨迹点数据、监听车辆的实时位置、车辆控制器。滚动屏等等。

文章目录

  • 1. 地图初始化
  • 2. 数据渲染
    • 2.1. 轨迹点
    • 2.2. 车牌
    • 2.3. 站牌标注
  • 3. 计算时间
    • 3.1. 时间总和
    • 3.2. 系统时间赋值
    • 3.3. 采样位置
    • 3.4. 加载公交车
    • 3.5. 气泡窗跟随
  • 4. 面板展示
    • 4.1. 起点与目的地
    • 4.2. 站牌名字
    • 4.3. 车辆控制器
    • 4.4. 视角切换
    • 4.5. 后端数据模拟
    • 4.6. 调整速度
    • 4.7. 滚动屏
  • 5. 事件监听器
  • 6. 移除事件

1. 地图初始化

加载高德地图。

const layer = new Cesium.UrlTemplateImageryProvider({url: "http://webrd02.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}",minimumLevel: 4,maximumLevel: 18,
});

对高德地图进行滤色处理。

onMounted(() => {// viewer是操控地图api的开始viewer = new Cesium.Viewer("cesiumContainer", {imageryProvider: layer,baseLayerPicker: false, //是否显示图层控件animation: false, //是否显示动画控件timeline: false, //是否显示时间轴控件fullscreenButton: false, //是否显示全屏控件geocoder: false, //是否显示搜索控件homeButton: false, //是否显示主页按钮navigationHelpButton: false, //是否显示帮助提示按钮sceneModePicker: false, //是否显示投影方式控件shouldAnimate: true, // 是否自动播放selectionIndicator: false, //隐藏选中框infoBox: false, //隐藏右上角信息框shadows: true,});// 设置滤色modifyMap(viewer, {invertColor: true,filterRGB: [70, 110, 120],});initData();
});

image-20240702224531955

load.json 道路数据分析

image-20240702224752939

front_name:公交站起点

terminal_name:公交站终点

xs,ys:是轨迹点的坐标经纬度,需要进行处理

start_time:开始时间

end_time:结束时间

length:路程(公里)

stations:每个站点

  • name:站点的名字
  • xy_coords:站点的坐标

2. 数据渲染

2.1. 轨迹点

首先处理每个轨迹点的坐标,它们的经纬度坐标分开,需要拼接起来,使用 forEach 循环进行拼接,这里只需要循环一次即可,因为经度和纬度的坐标数组都是一样长的,另一个可以根据循环的索引获得。再将经纬度坐标转为笛卡尔坐标,最后推入一个数组中保存。

let xArr = loadData.xs.split(",");
let yArr = loadData.ys.split(",");
xArr.forEach((item, index) => {positions.push(Cesium.Cartesian3.fromDegrees(Number(item), Number(yArr[index])));xyArr.push([Number(item), Number(yArr[index])]);
});

将轨迹点数据渲染到页面上。

const line = viewer.entities.add({polyline: {positions,width: 10,material: Cesium.Color.WHITE.withAlpha(0.8),clampGround: true,},
});

2.2. 车牌

处理每个车牌的数据,先遍历 loadData.stations,再获得里面 xy_coords 数据,处理完后转为笛卡尔坐标

// 获得站牌坐标 并转为笛卡尔坐标
let position = Cesium.Cartesian3.fromDegrees(...item.xy_coords.split(";").map((a) => Number(a))
);

加载对应的模型,,并改变站牌的大小和方向,进来的时候可以对着我们。

viewer.entities.add({position,orientation,model: {uri: "/src/assets/model.gltf",scale: 0.07,minimumPixelSize: 10,},
});
// 公交车牌的朝向
let orientation = Cesium.Transforms.headingPitchRollQuaternion(position,Cesium.HeadingPitchRoll.fromDegrees(90, 0, 0)
);

2.3. 站牌标注

展示站牌的标注,它的坐标跟站牌一样,但是高度却不一样,需要额外进行处理。

let positionLabel = Cesium.Cartesian3.fromDegrees(...item.xy_coords.split(";").map((a) => Number(a)),24
);

添加站牌标注到地图上。

viewer.entities.add({position: positionLabel,id: "label" + index,label: {text: item.name,font: "10px Helvetica",style: Cesium.LabelStyle.FILL_AND_OUTLINE,fillColor: Cesium.Color.WHITE, //字体颜色backgroundColor: Cesium.Color.BLACK.withAlpha(0.5), //背景颜色showBackground: true, //是否显示背景颜色},
});

image-20240702230605739

3. 计算时间

获得开始的时间和结束时间,开始时间可以随便给,根据公式 s=vt,s 保持不变,v 可以自己设定,这样就可以计算得到结束的时间了。

3.1. 时间总和

这里封装一个数据来计算时间总数和每个轨迹点对应的系统时间,它接受两个参数:pArr(轨迹点数组)和 speed(速度)。函数的实现原理是计算轨迹点之间的距离,利用 Cesium 自带的distance计算,并将这些距离除以速度以得到每个轨迹点所对应的时间。最后,函数返回一个包含时间总和每个轨迹点对应的系统时间。

const getSiteTimes = (pArr, speed) => {let timeSum = 0,times = []; //timeSum花费时间总和,每一个轨迹点对应的时间for (let i = 0; i < pArr.length; i++) {if (i == 0) {times.push(0);continue;}// 计算时间总数timeSum += spaceDistance(pArr[i - 1], pArr[i]) / speed;// 每个轨迹点所对应的系统时间times.push(timeSum);}return { timeSum: timeSum, siteTime: times };
};// 计算两点的距离
const spaceDistance = (a, b) => {return Cesium.Cartesian3.distance(a, b).toFixed(2);
};

设置速度为 20,可以获得时间总和每个轨迹点对应的系统时间。

image-20240702232646501

3.2. 系统时间赋值

这时可以设置开始时间和结束时间。

// 设置时间边界
const start = Cesium.JulianDate.fromDate(new Date(2015, 1, 1, 11));
const stop = Cesium.JulianDate.addSeconds(start,timeObj.timeSum,new Cesium.JulianDate()
);

光是拿到时间没有用,需要给系统时间赋值。

// 设置时间段
viewer.clock.startTime = start.clone();
viewer.clock.stopTime = stop.clone();
viewer.clock.currentTime = start.clone(); //当前时间

3.3. 采样位置

定义了一个名为 getSampleData 的函数,该函数接受三个参数:pArr(轨迹点数组)、start(开始时间)和 siteTime(时间戳数组)。函数的实现原理是将轨迹点数组 pArr 中的每个点按照时间戳数组 siteTime 中的对应时间添加到 position 对象中,从而形成一个采样位置属性。也就是将轨迹点数据转换为 Cesium 可以理解的格式,从而在地图上绘制轨迹。

为什么要传入siteTime时间节点传过来呢?

因为我们算出的时间节点都是从 0 开始算的,但是我们要把时间换算到系统里面去,应该是开始的时间加上时间节点。

这个案例最重点的地方:计算每个轨迹点对应的系统时间

for (let i = 0; i < pArr.length; i++) {//每一个轨迹点所对应的系统时间const time = Cesium.JulianDate.addSeconds(start,siteTime[i],new Cesium.JulianDate());position.addSample(time, pArr[i]);
}
console.log(position);

如果能在_property._values 中拿到 996 个坐标以及 332 个所对应的系统时间,那就是没问题了,因为笛卡尔是三个坐标。

image-20240702234146455

3.4. 加载公交车

根据实体的速度来计算其朝向。

orientation: new Cesium.VelocityOrientationProperty(position);

将创建的实体设置为视图的跟踪实体。跟踪实体是视图中的一个实体,当我们在视图中移动时,它会自动跟随我们的位置。

viewer.trackedEntity = entity;

如何使公交车在站牌停下?

  1. 需要监听时钟
  2. 将当前的公交车坐标和站牌的坐标做比较
  3. 获取它们之间的距离,在一定范围内,则停止
// 监听公交车的运动
viewer.clock.onTick.addEventListener(tickEventHandler);
// 获取当前的时间公交车的坐标
let startPosition = entity.position.getValue(viewer.clock.currentTime);
// console.log(a);
// 获取站牌的坐标 比较第二个站点
let endPosition = Cesium.Cartesian3.fromDegrees(...loadData.stations[data.index + 1].xy_coords.split(";").map((a) => Number(a))
);// 得到他们之间的距离
let distance = spaceDistance(startPosition, endPosition);

这里的距离应该是车的中心点到站牌的距离

如果小于一定的距离,就让公交车停下来,也就是 viewer.clock.shouldAnimate = false;

3.5. 气泡窗跟随

获取当前的坐标,将气泡窗的位置进行更新。

image-20240703085708150

4. 面板展示

第一栏是起点与目的地,第二栏是编号和站牌名字,第三栏是车辆控制器:暂停,减速,加速,重置,跟随视角,车内视角,自由视角,速度以及进度条。

image-20240703085025489

4.1. 起点与目的地

获取站牌的名称,包括起点和目的地。

data.fromName = loadData.stations[data.index].name;
data.toName = loadData.stations[data.index + 1].name;

4.2. 站牌名字

加载站牌名字到面板上,这里有一些动画效果,到站时,展示面板也要高亮显示,并挪动图标。

if (data.index >= 12 && data.index < 24) {img.value.style.top = "195px";img.value.style.left = 17 + (data.index - 12) * 32.85 + "px";
} else if (data.index >= 24) {img.value.style.top = "385px";img.value.style.left = 17 + (data.index - 24) * 32.85 + "px";
} else {img.value.style.left = 17 + data.index * 32.85 + "px";
}

点击站牌,也可以实现跳转到相应的站牌。

// 跳转到站牌
const toStation = (index) => {changeView("free");if (pickLabel) {pickLabel.label.fillColor = Cesium.Color.WHITE;}pickLabel = viewer.entities.getById("label" + index);// console.log(pickLabel);pickLabel.label.fillColor = Cesium.Color.YELLOW;viewer.flyTo(pickLabel);
};

4.3. 车辆控制器

播放按钮,设置播放状态,点击后播放,变为暂停按钮,再次点击,变为播放按钮。

if (!isBegin) {isBegin = true;toBegin();
}
data.control.play = true;
viewer.clock.shouldAnimate = true;

暂停就是把时间进行暂停。

viewer.clock.shouldAnimate = false;

重播时间就是让系统的当前时间再等于一下系统的开始时间,图标,进度条,站牌都还原。

img.value.style.top = "10px";
img.value.style.left = "17px";
data.control.percentage = 0;
data.index = 0;
viewer.clock.currentTime = viewer.clock.startTime;
data.fromName = loadData.stations[0].name;
data.toName = loadData.stations[1].name;

公交车的进度条,根据 s=vt,我们需要计算的是时间,用当前花费的时间除以总时间,可以得到进度条,花费的时间可以用当前时间戳减去开始时间戳。

4.4. 视角切换

自由视角就是取消跟随视角 viewer.trackedEntity = null;和摆脱观察者模式 viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY);

车内视角就是行驶时更新相机的朝向,使其始终朝向前方。

根据车内的坐标系,可以确定相机放的位置。相机的位置不能写(0,0,0),不能跟放的东西是同一个点,会报错的。

viewer.camera.lookAtTransform(transform, new Cesium.Cartesian3(-0.001, 0, 0)); //将相机向后面放一点

image-20240703092327068

不管你如何修改上面的三个值,你都无法实现车内的视角,原因是我们所看向的点永远在车底,我们拿到的实时坐标是没有高度的,永远是 0。我们应该要把中心点提一下,x 轴往后一点,这样就可以实现了。

// 改变视角 公交车内
let cartographic = Cesium.Cartographic.fromCartesian(startPosition);
let lon = Cesium.Math.toDegrees(cartographic.longitude);
let lat = Cesium.Math.toDegrees(cartographic.latitude);
let newPosition = Cesium.Cartesian3.fromDegrees(lon, lat, 2);
if (isInCar) {viewer.trackedEntity = null;var ori = entity.orientation.getValue(viewer.clock.currentTime); //获取偏向角// var center = entity.position.getValue(viewer.clock.currentTime); //获取位置var transform = Cesium.Matrix4.fromRotationTranslation(Cesium.Matrix3.fromQuaternion(ori),newPosition); //将偏向角转为3*3矩阵,利用实时点位转为4*4矩阵viewer.camera.lookAtTransform(transform, new Cesium.Cartesian3(-0.001, 0, 0)); //将相机向后面放一点
}

image-20240703093022682

车内视角参考:cesium 巡逻获取对象实时信息并实现切换第一人称视角

4.5. 后端数据模拟

模拟后端给你一个点,从这个点开始跑,[114.406979, 30.462812]

第一个思路:从所有的轨迹点钟中选出后端的点,但是后端提供的点不一定在轨迹点上,也有可能有轨迹点的连线上。

第二个思路:算出后端给的点跑了多久,就当时间给回系统时间。可以用该点到起始点的距离除以总的路程,再乘以时间综合。

使用 turf 库的线段截取,获取一条线、起点和终点,并返回这些点之间的线段。起止点不需要正好落在直线上。注意都是 geojson 数据!

let xyArr = [];
let xArr = loadData.xs.split(",");
let yArr = loadData.ys.split(",");
xArr.forEach((item, index) => {positions.push(Cesium.Cartesian3.fromDegrees(Number(item), Number(yArr[index])));xyArr.push([Number(item), Number(yArr[index])]);
});
lineGeoJson = turf.lineString(xyArr);
const start = turf.point(lineGeoJson.geometry.coordinates[0]); //起点
const stop = turf.point([114.406979, 30.462812]); //终点
const sliced = turf.lineSlice(start, stop, lineGeoJson); //线
// console.log(sliced);

计算截取的长度/总长度的百分比,并求出相应的时间

return turf.length(sliced) / turf.length(lineGeoJson);
const proportion = getProportion(); //getProportion是返回截取的长度/总长度的百分比
const newTime = Cesium.JulianDate.addSeconds(start,timeObj.timeSum * proportion,new Cesium.JulianDate()
);

同时还要考虑公交车是否到站了

第一个思路;,后端的点需要跟所有的站点进行比较,拿到最小的距离。但是当线路是 U 型弯回来的时候,如果马路对面的站点比较近,就和实际效果不一样。

image-20240703102436588

第二个思路:遍历所有的站点到起始点的距离,用来跟截取的线段长度做比较,如果截取的线段长度小于站点到起始点线段长度,说明找到了更接近的站点,将当前点的索引(i)赋值给 index。

// 截取站牌长度比较
loadData.stations.forEach((item, i) => {const stationsStop = item.xy_coords.split(";").map((a) => Number(a));const stationsSliced = turf.lineSlice(start, stationsStop, lineGeoJson);// console.log(stationsSliced);if (turf.length(stationsSliced) < turf.length(sliced)) {index = i;}
});

4.6. 调整速度

根据 s=vt,s 不变,如果要更改 v 的话,其他都会发生改变,只有当时间变快时,速度就可以变得快。

4.7. 滚动屏

根据公交车与站牌的距离,显示滚动屏的内容,距离小于一定的值,就会显示前方到站 xxx,到了站点起步,就会显示下一站 xxx,然后过一会时间就会恢复初始的内容。

QQ录屏20240703104224 -original-original

5. 事件监听器

当监听网页切走后,公交车将会暂停。

// 监听用户是否离开页面 离开 公交车停止运动
document.addEventListener("visibilitychange", function () {if (document.hidden) {viewer.clock.shouldAnimate = false;data.control.play = false;}
});

6. 移除事件

当公交车到达终点后,会清除所有的事件。

const removeEvent = () => {bubble.windowClose();viewer.clock.onTick.removeEventListener(tickEventHandler);viewer.entities.remove(entity);changeView("free");isBegin = false;data.control.play = false;viewer.clock.shouldAnimate = false;
};

也会弹出到达终点。

if (data.index == loadData.stations.length - 1) {alert("We are arrive the terminal.乘客们,感谢您一路对我们工作理解和支持的,下车前请检查一下自己的行李物品,请不要遗忘在车厢内。");removeEvent();return;
}

参考案例:Cesium 官方车辆轨迹漫游

image-20240702230945514

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/39750.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【高中数学/基本不等式】已知:x,y均为正实数,且xy+2x+y=4 求:x+y的最小值?

【问题】 已知&#xff1a;x,y均为正实数&#xff0c;且xy2xy4 求&#xff1a;xy的最小值&#xff1f; 【来源】 https://www.ixigua.com/7147585275823292942?logTagf25494de7fce23a3a3d0 【解答】 解&#xff1a; 由xy2xy4 两边加二得 xy2xy24 2 分解因式得 (x1)(…

【WEB前端2024】3D智体编程:乔布斯3D纪念馆-第53课-语音指令跳舞

【WEB前端2024】3D智体编程&#xff1a;乔布斯3D纪念馆-第53课-语音指令跳舞 使用dtns.network德塔世界&#xff08;开源的智体世界引擎&#xff09;&#xff0c;策划和设计《乔布斯超大型的开源3D纪念馆》的系列教程。dtns.network是一款主要由JavaScript编写的智体世界引擎&…

面试题springboot面试

文章目录 Spring的依赖注入构造器注入stetter注入属性注入 springboot的优势第一开箱即用约定大于配置内嵌tomcat服务器 javaweb的三大组件springboot的自动配置原理SpringIoc的实现机制springmvcspring如何简化开发 Spring的依赖注入 构造器注入 stetter注入 属性注入 使用…

前端进阶:Vue.js

目录 框架&#xff1a; 助解&#xff1a; 框架&#xff1a; VUE 什么是Vue.js? Vue.js优点 Vue安装 方式一&#xff1a;直接用<script>引入 方式二&#xff1a;命令行工具 第一个Vue程序 代码 代码解释&#xff1a; 运行 Vue指令 v-text v-html v-tex…

Mysql和ES使用汇总

一、mysql和ES在业务上的配合使用 一般使用时使用ES 中存储全文检索的关键字与获取的商品详情的id&#xff0c;通过ES查询获取查询商品的列表中展示的数据&#xff0c;通过展示id 操作去获取展示商品的所有信息。mysql根据id去查询数据库数据是很快的&#xff1b; 为什么ES一般…

10 - Python文件编程和异常

文件和异常 在实际开发中&#xff0c;常常需要对程序中的数据进行持久化操作&#xff0c;而实现数据持久化最直接简单的方式就是将数据保存到文件中。说到“文件”这个词&#xff0c;可能需要先科普一下关于文件系统的知识&#xff0c;对于这个概念&#xff0c;维基百科上给出…

亚马逊云科技AWS免费大热AI应用开发证书(含题库、开卷)

亚马逊云科技AWS官方生成式AI免费证书来了&#xff01;内含免费AI基础课程&#xff01;快速掌握AWS的前沿AI技术&#xff0c;后端开发程序员也可以速成AI专家&#xff0c;了解当下最&#x1f525;的AWS AI架构解决方案&#xff01; 本证书内容包括AWS上的AI基础知识&#xff0c…

剖析DeFi交易产品之UniswapV4:Swap

文章首发于公众号&#xff1a;Keegan小钢 Swap 可分为两种场景&#xff1a;单池交易和跨池交易。在 PoolManager 合约里&#xff0c;要完成交易流程&#xff0c;会涉及到 lock()、swap()、settle()、take() 四个函数。单池交易时只需要调一次 swap() 函数&#xff0c;而跨池交易…

【面向就业的Linux基础】从入门到熟练,探索Linux的秘密(七)-shell语法(5)

shell语法的一些知识和练习&#xff0c;可以当作笔记收藏一下&#xff01;&#xff01; 文章目录 前言 一、shell 二、shell语法 1.文件重定向 2.引入外部脚本 3.作业 总结 前言 shell语法的一些知识和练习&#xff0c;可以当作笔记收藏一下&#xff01;&#xff01; 提示&…

七种大模型微调方法:让你的Offer拿到爽

在当今的人工智能和机器学习领域&#xff0c;大型预训练模型&#xff08;如GPT、BERT等&#xff09;已成为解决自然语言处理&#xff08;NLP&#xff09;任务的强大工具。然而&#xff0c;要让这些模型更好地适应特定任务或领域&#xff0c;往往需要进行微调。本文将详细介绍七…

手把手教你:如何在51建模网免费下载3D模型?

作为国内领先的3D互动展示平台&#xff0c;51建模网不仅汇聚了庞大的3D模型资源库&#xff0c;供用户免费下载&#xff0c;更集成了在线编辑、格式转换、内嵌展示及互动体验等一站式功能&#xff0c;为3D创作者及爱好者搭建起梦想与现实的桥梁。 如何在51建模网免费下载3D模型…

鸿蒙认证值得考吗?

鸿蒙认证值得考吗&#xff1f; 鸿蒙认证&#xff08;HarmonyOS Certification&#xff09;是华为为了培养和认证开发者在鸿蒙操作系统&#xff08;HarmonyOS&#xff09;领域的专业技能而设立的一系列认证项目。这些认证旨在帮助开发者和企业工程师提升在鸿蒙生态中的专业技能…

linux——IPC 进程间通信

IPC 进程间通信 interprocess communicate IPC&#xff08;Inter-Process Communication&#xff09;&#xff0c;即进程间通信&#xff0c;其产生的原因主要可以归纳为以下几点&#xff1a; 进程空间的独立性 资源隔离&#xff1a;在现代操作系统中&#xff0c;每个进程都…

图解 Kafka 架构

写在前面 Kafka 是一个可横向扩展&#xff0c;高可靠的实时消息中间件&#xff0c;常用于服务解耦、流量削峰。 好像是 LinkedIn 团队开发的&#xff0c;后面捐赠给apache基金会了。 kafka 总体架构图 Producer&#xff1a;生产者&#xff0c;消息的产生者&#xff0c;是消息的…

怎么把录音转文字?推荐几个简单易操作的方法

在小暑这个节气里&#xff0c;炎热的天气让人分外渴望效率up&#xff01;Up&#xff01;Up&#xff01; 对于那些在会议或课堂中急需记录信息的朋友们&#xff0c;手写笔记的速度往往难以跟上讲话的节奏。此时&#xff0c;电脑录音转文字软件就像一阵及时雨&#xff0c;让记录…

PHP pwn 学习 (1)

文章目录 A. PHP extensions for C1. 运行环境与工作目录初始化2. 构建与加载3. 关键结构定义PHP_FUNCTIONINTERNAL_FUNCTION_PARAMETERSzend_execute_data等ZEND_PARSE_PARAMETERS_START等zend_parse_arg_stringzend_module_entryzend_function_entry等PHP类相关 原文链接&…

Python 作业题1 (猜数字)

题目 你要根据线索猜出一个三位数。游戏会根据你的猜测给出以下提示之一&#xff1a;如果你猜对一位数字但数字位置不对&#xff0c;则会提示“Pico”&#xff1b;如果你同时猜对了一位数字及其位置&#xff0c;则会提示“Fermi”&#xff1b;如果你猜测的数字及其位置都不对&…

Flower花所:稳定运营的数字货币交易所

Flower花所是一家稳定运营的数字货币交易所&#xff0c;致力于为全球用户提供安全、高效的数字资产交易服务。作为一家长期稳定运营的数字货币交易平台&#xff0c;Flower花所以其可靠的技术基础和优质的客户服务而闻名。 平台稳定性与可靠性&#xff1a; 持续运营&#xff1a;…

Vue前端练习

此练习项目只涉及前端&#xff0c;主要是vue和ElementUI框架的使用。&#xff08;ElementUI官网&#xff1a;Element - The worlds most popular Vue UI framework&#xff09; 一、环境准备 安装idea 安装Node.js 一键式安装(不需要做任何配置) npm -v&#xff08;也可用nod…

mysql-sql-第十五周

学习目标&#xff1a; sql 学习内容&#xff1a; 41.查询没有学全所有课程的同学的信息 select *from students where students.stunm not in (select score.stunm from score group by score.stunm having count(score.counm) (select count(counm) from course)) 42.查询…