智慧城市交通的要素:路口监管可视化系统的解决方案
集成地理信息系统、视频监控系统、交管部门各业务系统数据,对交通路况车流量、事故处理报告等要素进行综合监测,并支持点选查看具体警力、机动目标、交通事件、监控视频等详细信息,帮助管理者实时掌握交通整体运行态势。
支持集成前端视频巡检系统,有效结合视频智能分析、智能定位、智能研判技术,对道路拥堵点位、隐患点位、事故点位等情况进行可视化监测,实现异常事件的实时告警、快速显示,并可智能化调取异常点位周边监控视频,有效提升接处警效率。
支持集成路口信号灯、视频监控等系统数据,对路口交通流量、流速、车辆及道路异常事件、信号灯状态等信息进行实时监测,并可结合专业的模型算法,比对历史最佳通行速度及最佳通行量,对路口交通态势进行可视化分析研判,为信号配时调优和路口交通组织优化提供科学的决策依据,有效提升交通运行效率。
充分整合交管部门现有数据资源,提供多种可视化分析、交互手段,对海量历史违法违章案件数据进行可视化串并分析,深度挖掘案件时空分布规律,为交管部门进行原因分析、主动防范等业务应用提供支持。
loadCar(type) { // 创建车辆新节点 let car = new ht.Node(); // 根据车辆类型创建加载对应车辆模型 switch (type) { case ‘familyCar‘: car.s(‘shape3d‘, ‘models/HT模型库/交通/车辆/家用车.json‘); break; case ‘truck‘: car.s(‘shape3d‘, ‘models/HT模型库/交通/车辆/卡车.json‘); break; case ‘jeep‘: car.s(‘shape3d‘, ‘models/HT模型库/交通/车辆/吉普车.json‘); break; ... default: console.log(‘NO THIS TYPE CAR!‘); break; } // 设置车辆不可选择和不可移动 car.s({ ‘3d.selectable‘: false, ‘3d.movable‘: false }); // 设置锚点 --- 车的头部 car.setAnchor3d(1, 0, 0.5); // 设置初始位置 car.setPosition3d(0, 100000, 0); let typeIndex = 1; // 判断是否此前生成了这种类型的车辆 this.g3dDm.each(data => { if (data.getTag() === type + typeIndex) { typeIndex++; } }) // 设置车辆节点标签 car.setTag(type + typeIndex); // 设置车辆节点的名字 car.setDisplayName(type); // 将车辆节点添加到数据模型中 this.g3dDm.add(car); }
而关于管道动画的实现上,基于 ht.Default.startAnim() 封装了一个 move 的动画函数是节点沿着路径平滑移动的封装函数,主要参数为:
- node:动画节点;
- path:运行路径;
- duration:动画执行调度时间;
- animParams:动画参数;
通过绘制一条运行路线的管道,ht.Default.getLineCacheInfo() 得到这条管道的点位和分割信息 cache,然后管道信息通过 ht.Default.getLineLength() 得到管道的长度,并且通过 ht.Default.getLineOffset() 来获取连线或者管道指定比例的偏移信息,从而达到移动的效果,是为了通过 node.lookAtX() 来获取节点下一个面对的朝向的位置信息,并设置节点此时的位置,从而达到节点沿着路径平滑移动的效果。
move(node, path, duration = 20000, animParams) { // path._cache_ 里面存着管道的节点信息 let cache = path._cache_; // 如果没有缓存信息,则获取 path._cache_ 里面存着管道的节点信息 if (!cache) { cache = path._cache_ = ht.Default.getLineCacheInfo(path.getPoints(), path.getSegments()); } // 获取管道缓存信息的长度 const len = ht.Default.getLineLength(cache); // 设置动画对象初始化 animParams = animParams || {}; // 设置 action 为 animParams 的动画执行函数 const action = animParams.action; // 动画执行部分 animParams.action = (v, t) => { // 获取管道运动的偏移信息 const offset = ht.Default.getLineOffset(cache, len * v); // 获取偏移位置上的点 const point = offset.point; // 设置节点看向的下一个位置 node.lookAtX([point.x, point.y, point.z], "forward "); // 设置节点的位置 node.p3(point.x, point.y, point.z); // 判断动画是否执行完 if (action) action(); }; // 循环调用动画执行函数 return loop(animParams.action, duration); } // 循环动画函数 loop(action, duration) { return ht.Default.startAnim({ duration: duration, action: action }); }
在交互实现上,通过点击选中摄像头后,使这个摄像头的锥形区域变为直线,表示为选中状态同时标记选中的摄像头的选中前后顺序,并且通过派发事件驱使 2D 图纸上显示摄像头弹窗,在弹窗显示的同时,通过计算得到实时变动的中心点位置信息(center),只要实时通过全局派发事件把位置信息传输到摄像头弹窗场景,就能起到摄像头场景视角与主场景中所点击摄像头的视角同步;取消弹窗显示的交互方式是通过双击场景背景,恢复摄像头锥形区域并且派发事件去隐藏 2D图纸上的摄像头弹窗:
// 全局事件派发器 var G = {} window.G = G; G.event = new ht.Notifier(); handleInteractive(e) { const {kind, data} = e; if(kind === ‘clickData‘) { // 判断点击节点是否带有标签,没有标签则 return let tag = data.getTag(); if(!tag) return; // 判断标签名为摄像头 if(tag.indexOf(‘camera‘) >= 0) { // 设置指定上一个点击的摄像头和当前点击的摄像头 this.lastClickCamera = this.nowClickCamera; this.nowClickCamera = data; // 如果之前有点击摄像头,则初始化摄像头锥体的大小 if (this.lastClickCamera !== null) { let clickRangeNode = this.lastClickCamera.getChildren()._as[0]; clickRangeNode.s3(300, 150, 500); } // 如果有点击摄像头,则设定所点击摄像头锥体的大小 if (this.nowClickCamera !== null) { let clickRangeNode = this.nowClickCamera.getChildren()._as[0]; clickRangeNode.s3(5, 5, 500); } // 获取点击摄像头的位置信息 var cameraP3 = nowClickCamera.p3(); // 获取点击摄像头的旋转信息 var cameraR3 = nowClickCamera.r3(); // 获取点击摄像头的大小信息 var cameraS3 = nowClickCamera.s3(); // 当前锥体起始位置 var realP3 = [cameraP3[0], cameraP3[1] + cameraS3[1] / 2, cameraP3[2] + cameraS3[2] / 2]; // 将当前眼睛位置绕着摄像头起始位置旋转得到正确眼睛位置 var realEye = getCenter(cameraP3, realP3, cameraR3); // 全局事件派发至摄像头场景改变视角的眼睛 eye 和中心点 center G.event.fire({ type: ‘videoCreated‘, eye: realEye, center: getCenter(realEye, [realEye[0], realEye[1] ,realEye[2] + 5], cameraR3) }); // 视频弹窗显示派发 event.fire(SHOW_VIDEO, {g3dDm: this.g3dDm, cameraName:tag}); } } // 双击背景隐藏摄像头场景窗口,并初始化摄像头锥体的大小 if(kind === ‘doubleClickBackground‘) { // 视频弹窗隐藏派发 event.fire(HIDE_VIDEO); // 如果之前有点击摄像头,则初始化摄像头锥体的大小 if (this.nowClickCamera !== null) { let clickRangeNode = this.nowClickCamera.getChildren()._as[0]; clickRangeNode.s3(300, 150, 500) } // 设置当前点击摄像头为空 this.nowClickCamera = null; } }
以上所涉及到方法 getCenter(),实际上是通过去获取每个摄像头节点在场景中对应的旋转角度,简化理解就是一个点 A 围绕着另外一个点 B 旋转,即中心点位置(center)围绕着眼睛位置(eye)旋转,而我们则需要去计算点 A 的位置(中心点位置 center),这里通过封装一个 getCenter 方法用于获取 3d 场景中点 A 绕着点 B 旋转 angle 角度之后得到的点 A 在 3d 场景中的位置,方法中采用了 HT 封装的 ht.Math 下面的方法,以下为实现的代码:
实现代码如下:
// pointA 为 pointB 围绕的旋转点 // pointB 为需要旋转的点 // r3 为旋转的角度数组 [xAngle, yAngle, zAngle] 为绕着 x, y, z 轴分别旋转的角度 const getCenter = function(pointA, pointB, r3) { const mtrx = new ht.Math.Matrix4(); const euler = new ht.Math.Euler(); const v1 = new ht.Math.Vector3(); const v2 = new ht.Math.Vector3(); mtrx.makeRotationFromEuler(euler.set(r3[0], r3[1], r3[2])); v1.fromArray(pointB).sub(v2.fromArray(pointA)); v2.copy(v1).applyMatrix4(mtrx); v2.sub(v1); return [pointB[0] + v2.x, pointB[1] + v2.y, pointB[2] + v2.z]; };
2.2 实景摄像头的实现原理
- RTMP (Real Time Messaging Protocol):实时消息传输协议,RTMP 协议中,视频必须是 H264 编码,音频必须是 AAC 或 MP3 编码,且多以 flv 格式封包。因为 RTMP 协议传输的基本是 FLV 格式的流文件,必须使用 flash 播放器才能播放。
- RTSP (Real-Time Stream Protocol):RTSP 实时效果非常好,适合视频聊天、视频监控等方向。
- HLS(Http Live Streaming):由 Apple 公司定义的基于 HTTP 的流媒体实时传输协议。传输内容包括两部分:1.M3U8 描述文件,2.TS 媒体文件。TS 媒体文件中的视频必须是H264编码,音频必须是 AAC 或 MP3 编码。数据通过 HTTP 协议传输。目前 video.js 库支持该格式文件的播放。
- HTTP-FLV:本协议就是 http+flv,将音视频数据封装成FLV格式,然后通过http协议传输到客户端,这个协议大大方便了浏览器客户端播放直播视频流.目前 flv.js 库支持该格式的文件播放。
例如通过一个简单的 RTMP 视频流的对接就可以明白其实现的原理。对于的视频的载入,需要用到 video.js 的插件进行展示,所以先引入插件,然后对接视频流后,也是同样通过全局事件派发到 HT 的渲染元素 renderHTML 将视频流渲染到场景图纸中,以下是实现的伪代码:
// 引入 video.js 插件 <script src="./js/video.js"></script> // 通过全局事件派发到渲染元素 renderHTML 去渲染视频到场景图纸中 G.event.add(function(e){ if(e.type===‘videoCreated‘){ var div=e.div; div.innerHTML=‘<video id="video" class="video-js vjs-default-skin"><source src="rtmp://10.10.70.57/live/test" type="rtmp/flv"></video>‘; window.player = videojs(‘video‘); } });
- ajax:使用 JavaScript 向服务器提出请求并处理响应而不阻塞用户核心对象 XMLHttpRequest;
- axios:基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中;
- WebSocket:HTML5 提供的一种在单个 TCP 连接上进行全双工通讯的协议;
ajax 和 axios 要实时获取接口数据得通过轮询调用接口的形式进行传输,而 WebSocket 可以双向进行数据传输,在选择运用上可以匹配自己的实现需求。本系统是采用通过 axios 调用接口获取实时数据。
示例中的柱状图和折线图,是通过 HT 里的机制下去使用 eEcharts 上一些图表进行自定义配置而实现的,继而通过对 axios 接口轮询调用载入数据,展现了实时的路口监控数据信息:
loadData() { // 获取图纸的数据模型 let dm = this.g2d.dm(); // 获取车流量接口的数据 axios.get(‘/traffic‘).then(res => { // 接入日车流量折线图的数据 this.lineChart1.a({ ‘seriesData1‘: res.lineChartData1, ‘axisData‘ : res.axisData }); // 接入车辆运行高峰折线图的数据 this.lineChart2.a({ ‘seriesData1‘: res.lineChartData2, ‘axisData‘ : res.axisData }) // 采用数字跳动的方式载入一些数据内容 setBindingDatasWithAnim(dm, res, 800, v => Math.round(v)); // 接入运行峰值的时刻 this.peakTime.s(‘text‘, res.peakTime); }); // 载入设备运行状态的数据 axios.get(‘/equipmentStatus‘).then(res => { setBindingDatasWithAnim(dm, res, 800, v => Math.round(v)); }); // 载入事故统计的数据 axios.get(‘/accident‘).then(res => { setBindingDatasWithAnim(dm, res, 800, v => Math.round(v)); // 接入每月事故柱状图的数据 this.accidentBar.a({ axisData: res.axisData, seriesData1: res.seriesData1 }) }); }
addTableRow() { // 获取表格节点 let table = this.table; // 通过 axios 的 promise 请求接口数据 axios.get(‘getEvent‘).then(res => { // 获取表格节点滚动信息的数据绑定 let tableData = table.a(‘dataSource‘); // 通过向 unshift() 方法可向滚动信息数组的开头添加一个或更多元素 tableData.unshift(res); // 初始化表格的纵向偏移 table.a(‘ty‘, -54); // 开启表格滚动动画 ht.Default.startAnim({ duration: 600, // 动画执行函数 action action: (v, t) => { table.a({ // 通过添加数据后,横向滚动 100 ‘firstRowTx‘: 100 * (1 - v), // 第一行行高出现的透明度渐变效果 ‘firstRowOpacity‘: v, // 纵向偏移 54 的高度 ‘ty‘: (v - 1) * 54 }); } }); }); }