bktest 2020-05-18
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 的动画函数是节点沿着路径平滑移动的封装函数,主要参数为:
通过绘制一条运行路线的管道,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 视频流的对接就可以明白其实现的原理。对于的视频的载入,需要用到 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 和 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 }); } }); }); }