前言


  随着工业物联网和互联网技术的普及和发展,人工填料的方式已经逐渐被机械设备取代。工业厂商减小误操作、提升设备安全以及追求高效率等制造特点对设备的要求愈加高标准、严要求。同时机械生产以后还需遵从整个项目流程的规范管理,如何实行管理与交接也是一大严峻的挑战。因此,整个生产流程中还应该制定一套关于管理流程的可视化界面。

  在工业过程控制中,按被控对象的实时数据采集的信息与给定值比较产生的误差的比例、积分和微分进行控制的控制系统,简称 PID 控制系统。PID
控制生产环境具有适应性强,鲁棒性强,使用方便等特点。进料系统则涉及到超高压技术,在流水线系统中广泛应用,能够实现设备半自动化或自动化送料作业,解决传统进料方式计量不准、工作环境污染以及工人劳动强度高等问题,从而实现高效的流水线加工。结合
PID 和自动化部署,可以为电力、机械、冶金、化工、食品、纺织等工业或者民用行业供需。本篇文章通过搭建危险废物进料系统的 2D
场景以及数据界面展示,帮助我们了解如何使用 HT 实现一个可视化的 PID 控制进料系统。

  项目地址预览: 基于 HTML5  的 PID-进料系统可视化界面
<https://www.cnblogs.com/htdaydayup/p/基于%20HTML5%20%20的%20PID-进料系统可视化界面> 
http://www.hightopo.com/demo/PID-feed-system/
<http://www.hightopo.com/demo/PID-feed-system/>

 

效果预览

  整体协作场景



  抓斗操作场景



  进料场景



代码构建

  搭建场景

  该文主要实现的是 2D 场景,我们需要用到拓扑组件的相关 api 搭建基础场景:
1 dataModel = new ht.DataModel(); //数据容器,用来存取数据节点Node 2 graphView = new
ht.graph.GraphView(dataModel);//拓扑组件 3 graphView.addToDOM(); //将组件添加到body中
  上述代码添加组件到body中所用的是 addToDom 方法,HT
组件一般会嵌入BorderPane、SplitView和TabView等容器中使用,而最外层的HT组件则需要用户手工将 getView() 返回的底层 div
元素添加到页面的 DOM 元素中,这里需要注意的是,当父容器大小变化时,如果父容器是 BorderPane 和 SplitView
等这些HT预定义的容器组件,则HT的容器会自动递归调用孩子组件 invalidate 函数通知更新。但如果父容器是原生的 html 元素, 则 HT
组件无法获知需要更新,因此最外层的 HT 组件一般需要监听 window 的窗口大小变化事件,调用最外层组件 invalidate 函数进行更新。

  为了最外层组件加载填充满窗口的方便性,HT 的所有组件都有 addToDOM 函数,其实现逻辑如下,其中 iv 是 invalidate 的简写:
1 addToDom = function(){ 2 var self = this, 3 view = self.getView(), //
获取组件的底层div 4 style = view.style; 5 document.body.appendChild(view); //
将组件底层div添加到body中 6 style.left = '0'; //默认所有组件的position都设置为absolute绝对定位 7
style.right = '0'; 8 style.top = '0'; 9 style.bottom = '0'; 10
window.addEventListener('resize',function(){ self.iv(); },false); //
窗口改变大小,调用刷新函数 11 }
  将视图默认方法重置:
1 graphView.setPannable(false); //禁用通过鼠标拖拽进行平移操作 2 graphView.setRectSelectable(
false); //禁用拓扑上进行框选操作 3 graphView.setMovableFunc(()=>{false}); //禁用移动过滤器函数
  在 2D 编辑器上创建 2D 图形会生成 JSON 文件,引入生成场景需要进行反序列化:
1 ht.Default.xhrLoad('displays/industry/PID-进料系统.json',function(text){ 2 var
json = ht.Default.parse(text);//解析为JSON对象 3 dataModel.deserialize(json); //
反序列化为场景 4 })
  在 HT 中,Data 类型对象构造时内部会自动被赋予一个 id 属性,可通过 data.getId() 和 data.setId( id )
获取和设置,Data 对象添加到 DataModel 之后不允许修改 id 值,可通过 dataModel.getDataById (id ) 快速查找
Data 对象。但是一般建议 id 属性由 HT 自动分配,用户业务意义的唯一标示可存在 tag 属性上,通过 Data#setTag( tag )
函数允许任意动态改变 tag 值,通过DataModel#getDataByTag(tag) 可查找到对应的 Data 对象,并支持通过
DataModel#removeDataByTag( tag ) 删除 Data 对象。我们这边通过在 JSON 中设置 Data 对象的 tag
属性,在代码中通过 dataModel.getDataByTag( tag ) 函数来获取该 Data 对象:
1 { 2 "c": "ht.Node", 3 "i": 407, 4 "p": { 5 "displayName": "抓手的结", 6
"parent": { 7 "__i": 403 8 }, 9 "tag": "gripKnot", 10 "image":
"symbols/symbol factory/垃圾处理/抓手的结.json", 11 "position": { 12 "x": -569.62125, 13
"y": -117.0502514 }, 15 "width": 50, 16 "height": 25 17 }, 18 "s": { 19
"select.width": 020 } 21 }, 1 var gripRightPaw =
dataModel.getDataByTag('gripRightPaw'); 2 var girpLeftPaw =
dataModel.getDataByTag('grapLeftPaw'); 3 var gripKnot =
dataModel.getDataByTag('gripKnot');


  展开动画

  HT 对动画封装了 ht.Default.startAnim 函数,通过设置 duration 获取动画时长, action
函数里为执行的动画属性,以及 finishFunc 动画执行后的回调函数,该案例共置8个动画,包含自驱动以及异步动画。下面举第八个动画(循环水流动)为例来理解
ht 内置动画效果:
1 //循环水流动 2 function animation() { 3 var lineJson = {}; 4 var name = ''; 5
var speed = 20, 6 lastTime = Date.now(); 7 //循环获取水流 tag,并设置初始化
shape.dash.offset 为0 8 for (var i = 1; i <= 9; i++ ) { 9 if (i != 8) { 10
name = 'line'+i; 11 lineJson[name] = 0; 12 } 13 } 14 ht.Default.startAnim({
15 duration: 5000, 16 action: function () { 17 var time = Date.now(), 18   
deltaTime = (time - lastTime) / 1000; 19 for (var tags in lineJson) { 20 if
(tags.split('e')[1] % 2) { 21 lineJson[tags] += deltaTime * speed; 22 } else {
23 lineJson[tags] -= deltaTime * speed; 24 } 25 var lines =
dataModel.getDataByTag(tags);26 lines.setStyle('shape.dash.offset'
,lineJson[tags]);27 } 28 lastTime = time 29 }, 30 finishFunc: function () { 31
animation();32 //TODO... 也可以在这里异步调用下一个动画 33 } 34 }) 35 }
  该例首先根据已创建的循环水流(已绑定 tag 标签)通过 for 循环以及 dataModel. getDataByTag 动态获取 Data
节点,通过标签名携带的数字判断水流方向,最终使用 Data.setStyle(可以简写为 Data.s ) 设置虚线部分的偏移距离。



  上面的循环水流为例,如果 lineJson[tags] += value (定值) ,当用户放大视图时图元数量减少,会多调用几次 anim 中的
action 函数,流动速度增快,缩小同理。因此采用 value = speed * deltaTime 的解决方式,解决视图在不同缩放 zoom
的情况下播放速度不一致的问题,具体原理如下:
1 //global 2 var lastTime = Date.now(); 3 var distance = 0; //距离 4 var
speed = 20;//速度 5 //action 6 ht.Default.startAnim({ 7 duration:5000, 8
action:function(){ 9 var time = Date.now(); 10 var deltaTime = (time -
lastTime) / 1000; 11 distance += speed * deltaTime; 12 lastTime = time; 13 },
14 finishFunc:function(){//TODO} 15 })
    ht 实现动画不仅可以使用 startAnim 来驱动,也可以采用按调度 addScheduleTask 进行实现,代码如下:
1 dataModel.addScheleTask({ 2 interval, //调度间隔 3 beforeAction(){}, //调度开始之前的动作
4 action(){}, //调度任务 5 afterAction(){} //调度结束之后的动作 6 })
  也可以使用 callLater 进行实现,ht 内置函数封装了非常多关于动画有趣且实操性强的 api ,有兴趣可以进入官网 (
https://www.hightopo.com <http://www.hightopo.com> )进行了解和学习,也可以线上申请 framework
的试用包。如果想要了解更多HT封装的动画进行操作,可以参考 https://www.cnblogs.com/xhload3d/p/9222549.html
<https://www.cnblogs.com/xhload3d/p/9222549.html> 等其他文章。

  可操作

  当然,HT 也汲取了订阅-发布模式的天然优势,通过驱动数据更改视图,更加直观地感受到数据与视图的绑定过程。以下提供2种 HT
提供的可操作界面,第一种是通过创建面板组件, HT 内部提供了包含 formPane 、borderPane、TablePane
等一系列通用面板组件,此处我们以  formPane 为例,首先在 index.html 主页面中引入 ht-form.js ,该文件封装了 formPane
面板的 api ,相关伪代码如下:
1 var fp = new ht.widget.FormPane(); //创建面板对象 2 fp.setWidth(200); 3
fp.setHeight(100);  4 fp.setRowHeight(30); //面板行高 5 fp.setPadding(16);  6
fp.getView().className ='main'; //节点设置类名后可以直接在 style 中设置属性,说白了 fp.getView()
就是一个普通的 DOM 节点 7 fp.addRow([{ //通过 addRow 方法添加文本以及进度条等内容 8 id:'text', 9
element:'Current Speed === 20', 10 align:'center' 11 }],[0.1]); 12 fp.addRow([{
13 id:'speed', 14 slider:{ //进度条 15 min:0, 16 max:100, 17 value:20, //当前进度值 18
step:1, 19 onValueChanged(){ value改变时触发函数 20 var speed = fp.v('speed'); 21
fp.v('text','Current Speed === ' + speed); 22 } 23 } 24 }],[0.1]); 25
document.body.appendChild(fp.getView());


  此时,我们只要把之前定义的 speed 指向 fp.v('speed') ,就可以简单地实现数据视图绑定:
1 function animation(fp){ 2 var lineJson = {}; 3 var name = ''; 4 var
lastTime = Date.now(); 5 var speed; 6 for (var i = 1; i <= 9; i++ ) { 7 if
(i != 8) { 8 name = 'line'+i; 9 lineJson[name] = 0; 10 } 11 } 12
ht.Default.startAnim({13 duration: 5000, 14 action: function () { 15 speed =
fp.v('speed'); 16 var time = Date.now(), 17 deltaTime = (time - lastTime) / 1000
;18 for (var tags in lineJson) { 19 if (tags.split('e')[1] % 2) { 20
lineJson[tags] += deltaTime * speed; 21 } else { 22 lineJson[tags] -= deltaTime
* speed; 23 } 24 var lines = dataModel.getDataByTag(tags); 25
lines.setStyle('shape.dash.offset',lineJson[tags]); 26 } 27 lastTime = time; 28
},29 finishFunc: function () { 30 animation(fp); 31 } 32 }) 33 }
 

  另一种是通过 HT 的矢量图形库,矢量图形采用点、线或多边形的图形描述方式,解决了 png 、jpg
等格式图片在缩放过程中出现失真现象。创建矢量图形可以通过常规编辑器如 webstorm、webstorm 通过代码编写,也可以通过 HT-2D
编辑器直接创建图形,基本上不需要操作代码就可以简单地创建出图形,有学过 3dmax 或者 CAD
制图的同学对此应该都不陌生。在编辑器的不断完善下,内部已经有许多优秀的图标和组件案例,这边就可以直接引用一些小案例,首先需要创建一张图纸,然后直接拉取一个自制图标,类似
legend 的效果都是绘线画出来的,更改文字部分就可以直接看到效果了。



  关键的还是功能性组件,图标展示显示界面,功能性组件支持事件的触发,首先在控件里面拉取 slider 图标,然后到组件栏拉取 slider
组件,设置控件的最大值、最小值和默认值等一系列参数。



  可以得知我们即将改变的值有两个,一个是 slider ,一个是文本的值,默认20,我们给这两个 Data 对象绑定唯一标签,分别为
sliderValue 以及 textValue,先通过进度条的当前值改变文本的值:
1 var sliderValue = dataModel.getDataByTag('sliderValue'); 2 var textValue =
dataModel.getDataByTag('textValue'); 3 sliderValue.a('ht.onChange',function(){
//value改变触发事件4   textValue.a('textValue',sliderValue.a('ht.value')); 5 })
  然后animation拿到进度条的当前值,指向speed:
1 function animation(data) { 2 var lineJson = {}; 3 var name = ''; 4 var
lastTime = Date.now(); 5 var speed; 6 for (var i = 1; i <= 9; i++ ) { 7 if
(i != 8) { 8 name = 'line'+i; 9 lineJson[name] = 0; 10 } 11 } 12
ht.Default.startAnim({13 duration: 5000, 14 action: function () { 15 speed =
data.a('ht.value'); 16 var time = Date.now(), 17 deltaTime = (time - lastTime)
/ 1000; 18 for (var tags in lineJson) { 19 if (tags.split('e')[1] % 2) { 20
lineJson[tags] += deltaTime * speed; 21 } else { 22 lineJson[tags] -= deltaTime
* speed; 23 } 24 var lines = dataModel.getDataByTag(tags); 25
lines.setStyle('shape.dash.offset',lineJson[tags]); 26 } 27 lastTime = time; 28
},29 finishFunc: function () { 30 animation(data); 31 } 32 }) 33 }
 



  当然也可以自定义多个 slider 分别控制不同的动画,具体如何实现还是全凭需求而定。

  

  不局限于 2D 可视化场景,与 3D 相关生产环境的可视化场景模拟也有许多案例,如下:

  3D水泥工厂工艺流程:http://www.hightopo.com/demo/CementFactory/
<http://www.hightopo.com/demo/CementFactory/>

 

  3D高炉炼铁工业流程:http://www.hightopo.com/demo/large-screen-puddling/
<http://www.hightopo.com/demo/large-screen-puddling/>