前言:
储油罐是采油、炼油企业储存油品的重要设备,储油罐液位、温度的精确计量对企业的库存和安全管理有着重大意义。这种可视化的模型储油罐液位、温度在线监测系统改变了传统采用可视化模型界面,实现了油液的实时动态监测,为生产操作和管理决策提供了准确的数据依据,大大避免了安全事故的发生。
HT for Web 官方网站:http://www.hightopo.com/ 感兴趣的朋友可以去官网看看,里面有很多很有趣的项目。
代码部分:
先来看左边的油罐模型,它是由阀门,罐体,和管道若干构成的。这里我使用了 HT 编辑器绘制了这些组件,并将需要的数据进行了绑定。比如油罐罐体上的百分比,就是在编辑器内部绑定了一个方法,再由编辑器自动生成罐体的 JSON 文件,之后再将油罐图标和其他组件的图标共同构成一个图纸,用户只需要将图纸 JSON 文件反序列化,就可以在页面中显示图纸的内容了。如下是编辑器自动生成的图纸 JSON 文件:
{
"v": "6.2.5",
"p": {
"layers": [
{
"name": "0",
"visible": true,
"selectable": true,
"movable": true,
"editable": true
},
{
"name": "b",
"visible": true,
"selectable": true,
"movable": true,
"editable": true
}
],
"autoAdjustIndex": true,
"hierarchicalRendering": true
},
"a": {
"connectActionType": null
},
"d": [
{
"c": "ht.Node",
"i": 43243,
"p": {
"displayName": "阀门",
"layer": "b",
"tag": "1003",
"image": "json/tap.json",
"position": {
"x": 107.04556,
"y": 116.30496
},
"width": 22.2555,
"height": 15.89827
}
},
{
"c": "ht.Shape",
"i": 43244,
"p": {
"position": {
"x": 71.2996,
"y": 118.12154
},
"width": 53.6712,
"height": 1,
"segments": {
"__a": [
1,
2
]
},
"points": {
"__a": [
{
"x": 44.464,
"y": 118.12154
},
{
"x": 98.1352,
"y": 118.12154
}
]
}
},
"s": {
"shape.background": null,
"shape.border.color": "rgb(217,217,217)",
"shape.border.width": 2
}
},
{
"c": "ht.Shape",
"i": 43245,
"p": {
"position": {
"x": 142.37464,
"y": 117.89698
},
"width": 53.44663,
"height": 1,
"segments": {
"__a": [
1,
2
]
},
"points": {
"__a": [
{
"x": 115.65132,
"y": 118.12154
},
{
"x": 169.09795,
"y": 117.67241
}
]
}
},
"s": {
"shape.background": null,
"shape.border.color": "rgb(217,217,217)",
"shape.border.width": 2
}
},
{
"c": "ht.Shape",
"i": 43246,
"p": {
"position": {
"x": 211.09173,
"y": 115.87589
},
"width": 45.8114,
"height": 35.03224,
"segments": {
"__a": [
1,
2,
2,
2,
2
]
},
"points": {
"__a": [
{
"x": 188.18603,
"y": 117.44785
},
{
"x": 198.51605,
"y": 117.22328
},
{
"x": 198.51605,
"y": 133.39201
},
{
"x": 198.74062,
"y": 98.8089
},
{
"x": 233.99743,
"y": 98.35976
}
]
}
},
"s": {
"shape.background": null,
"shape.border.color": "rgb(217,217,217)",
"shape.border.width": 2
}
},
{
"c": "ht.Shape",
"i": 43247,
"p": {
"position": {
"x": 123.6234,
"y": 125.75678
},
"width": 1,
"height": 15.7196,
"segments": {
"__a": [
1,
2
]
},
"points": {
"__a": [
{
"x": 123.51112,
"y": 117.89698
},
{
"x": 123.73568,
"y": 133.61657
}
]
}
},
"s": {
"shape.background": null,
"shape.border.color": "rgb(217,217,217)",
"shape.border.width": 2
}
},
{
"c": "ht.Node",
"i": 43248,
"p": {
"displayName": "油罐",
"layer": "b",
"tag": "1001",
"dataBindings": {
"p": {}
},
"image": "json/oil.json",
"position": {
"x": 123.02107,
"y": 185.15576
}
}
},
{
"c": "ht.Text",
"i": 43249,
"p": {
"position": {
"x": 107.04556,
"y": 106.11657
},
"width": 17.82073,
"height": 7.48388
},
"s": {
"text": "OPEN",
"text.font": "5px arial, sans-serif"
}
},
{
"c": "ht.Text",
"i": 43250,
"p": {
"position": {
"x": 177.7828,
"y": 106.11657
},
"width": 24.84864,
"height": 9.50497
},
"s": {
"text": "CLOSED",
"text.font": "5px arial, sans-serif",
"text.color": "rgb(217,217,217)"
}
},
{
"c": "ht.Shape",
"i": 43251,
"p": {
"position": {
"x": 123.02107,
"y": 224.35038
},
"width": 1,
"height": 53.08572,
"segments": {
"__a": [
1,
2
]
},
"points": {
"__a": [
{
"x": 122.91873,
"y": 197.80752
},
{
"x": 123.1234,
"y": 250.89324
}
]
}
},
"s": {
"shape.background": null,
"shape.border.color": "rgb(217,217,217)",
"shape.border.width": 2
}
},
{
"c": "ht.Text",
"i": 43252,
"p": {
"position": {
"x": 283.74301,
"y": 250.89324
},
"width": 32.25391,
"height": 9.83265
},
"s": {
"text": "Dye Tank 2",
"text.font": "5px arial, sans-serif",
"text.align": "center",
"text.color": "rgb(247,247,247)"
}
},
{
"c": "ht.Shape",
"i": 43253,
"p": {
"position": {
"x": 198.11968,
"y": 224.48984
},
"width": 1,
"height": 53.08572,
"segments": {
"__a": [
1,
2
]
},
"points": {
"__a": [
{
"x": 198.01734,
"y": 197.94698
},
{
"x": 198.22201,
"y": 251.0327
}
]
}
},
"s": {
"shape.background": null,
"shape.border.color": "rgb(217,217,217)",
"shape.border.width": 2
}
},
{
"c": "ht.Shape",
"i": 43254,
"p": {
"position": {
"x": 106.17145,
"y": 258.47692
},
"width": 183.90411,
"height": 16.4313,
"segments": {
"__a": [
1,
2,
2,
2
]
},
"points": {
"__a": [
{
"x": 14.21939,
"y": 250.26127
},
{
"x": 123.23472,
"y": 250.26127
},
{
"x": 198.1235,
"y": 250.26127
},
{
"x": 198.1235,
"y": 266.69256
}
]
}
},
"s": {
"shape.background": null,
"shape.border.color": "rgb(217,217,217)",
"shape.border.width": 2
}
},
{
"c": "ht.Node",
"i": 43255,
"p": {
"displayName": "油罐",
"tag": "1002",
"layer": "b",
"image": "json/oil.json",
"position": {
"x": 198.1235,
"y": 186.11658
}
}
},
{
"c": "ht.Node",
"i": 43256,
"p": {
"displayName": "阀门",
"tag": "1004",
"layer": "b",
"image": "json/tap.json",
"position": {
"x": 177.7828,
"y": 115.65773
},
"width": 24.84864,
"height": 15.04013
}
}
],
"contentRect": {
"x": 0,
"y": 0
}
}
如下是油罐图标的自动生成的 JSON 文件,油罐里包含了罐体和其下方的数据标识,里面的 function 为了在解析时区别出来,编辑器自动转成 __ht__function 的格式:
{
"width": 47,
"height": 105,
"comps": [
{
"type": "rect",
"background": "#D8D8D8",
"borderWidth": 1,
"borderColor": "rgb(204,202,202)",
"rect": [
6.54899,
0.6234,
35,
64
]
},
{
"type": "rect",
"background": {
"func": "attr@oilColor",
"value": "rgb(124,235,128)"
},
"borderColor": "#979797",
"rect": [
10.54899,
4.59114,
27,
56
]
},
{
"type": "rect",
"background": "rgb(247,247,247)",
"borderColor": "#979797",
"anchorX": 0.47277,
"rect": {
"func": "__ht__function(data, view) {\nreturn [10.54899,4.59114,27,56 - data.a(\'percent\') * 0.56];\n}",
"value": [
10.54899,
4.59114,
27,
35
]
}
},
{
"type": "text",
"text": {
"func": "__ht__function(data, view) {\nreturn data.a(\'percent\') + \" %\";\n}",
"value": "100%"
},
"align": "center",
"font": "8px arial, sans-serif",
"rect": [
10.54899,
39.59114,
27,
15.3783
]
},
{
"type": "rect",
"background": "rgb(57,137,173)",
"borderColor": "#979797",
"rect": [
1.09799,
69.25901,
45.90201,
10.69561
]
},
{
"type": "text",
"rect": [
-7.40554,
49.60682,
50,
50
]
},
{
"type": "text",
"text": {
"func": "attr@tankNum",
"value": "Dye Tank 1"
},
"align": "center",
"color": "rgb(247,247,247)",
"font": "5px arial, sans-serif",
"rect": [
1.09799,
69.25901,
45.90201,
10.69561
]
},
{
"type": "rect",
"background": "rgb(57,137,173)",
"borderColor": "#979797",
"rect": [
1.09799,
82.0579,
45.90201,
10.69561
]
},
{
"type": "text",
"text": {
"func": "__ht__function(data, view) {\nreturn data.a(\'percent\') + \" %\";\n}",
"value": "48 %"
},
"align": "center",
"color": "rgb(48,242,120)",
"font": "5px arial, sans-serif",
"rect": [
1.09799,
82.0579,
45.90201,
10.69561
]
},
{
"type": "rect",
"background": "rgb(57,137,173)",
"borderColor": "#979797",
"rect": [
1.09799,
94.25901,
45.90201,
10.69561
]
},
{
"type": "text",
"text": {
"func": "__ht__function(data, view) {\nreturn data.a(\'temp\') + \' ℉\';\n}",
"value": "100"
},
"align": "center",
"color": {
"func": "__ht__function(data, view) {\nreturn data.a(\'temp\') > 75 ? \"rgb(242, 83, 75)\" : \"rgb(48, 242, 120)\";\n}",
"value": "rgb(48,242,120)"
},
"font": "5px arial, sans-serif",
"rect": [
15.44196,
94.30439,
16.66947,
10.69561
]
},
{
"type": "text",
"color": {
"func": "__ht__function(data, view) {\nreturn data.a(\'temp\') > 75 ? \"rgb(242, 83, 75)\" : \"rgb(48, 242, 120)\";\n}",
"value": "rgb(48,242,120)"
},
"font": "5px arial, sans-serif",
"rect": [
22.59446,
94.30439,
11.5503,
10.69561
]
},
{
"type": "oval",
"background": "rgb(212,0,0)",
"borderColor": "#979797",
"visible": {
"func": "__ht__function(data, view) {\nreturn data.a(\'temp\') > 75 ? true : false;\n}",
"value": true
},
"editable": false,
"rect": [
34.3647,
95.70692,
8.51093,
7.89055
]
}
]
}
需要导入一些 js 的类库,如下:
<script src="../ht/lib/ht.js"></script> <script src="../ht/lib/ht-ui.js"></script>
之后我用了 ht.Default 的工具函数 xhrLoad 异步向服务器发送请求,获得了这段 JSON ,将 JSON 反序列化得到数据存进数据模型中,并且给油罐对象的绑定属性赋初始值。这里注意 xhrLoad 请求是异步的,所以给对象赋初始值的时候要在 xhrLoad 的返回函数内部执行,否则可能在执行时还没反序列化完成,就开始获取对象的属性并赋值,就会出现获取不到对象的错误。
dataModel = new ht.DataModel();
ht.Default.xhrLoad(\'oilTank.json\', function(text) {
var json = ht.Default.parse(text);
dataModel.deserialize(json);
dataModel.getDataByTag(\'1004\').a(\'tapColor\', \'rgb(217, 217, 217)\');
dataModel.getDataByTag(\'1002\').a(\'oilColor\', \'rgb(247, 213, 161)\');
for(var i = 1; i < 3; ++i){
let data = dataModel.getDataByTag(\'100\' + i);
data.a(\'percent\', 10);
data.a(\'temp\', 37);
data.a(\'tankNum\', \'Dye Tank \' + i);
data.a(\'presure\', 132);
data.a(\'alarms\', \'- -\');
data.a(\'status\', status[0]);
}
});
再来看右边的表格,我采用的是在外部用一个 Panel 将表格放到里面,再设计表格的列,因为表格内容实际上是对油罐状态的数据显示,他们可以共享同一个 DataModel ,但由于图纸上每一个组件都会对应一个数据,这在获取油罐数据时会造成麻烦,于是我在表格获取数据之前增加了一个过滤器,用来过滤掉一些不需要的数据:
tableView.setVisibleFunc(function(data) {
if(data.getTag() === \'1001\' || data.getTag() === \'1002\') {
return true;
}
return false;
});
然后再设计表格的列,从 DataModel 获取数据并添加到列中:
tablePane = new ht.ui.TablePane(dataModel);
tableView = tablePane.getTableView();
columnModel = tableView.getColumnModel();
var status = [
\'ATUO\',
\'OFF\'
];
var column = new ht.ui.Column();
column.setName(\'tankNum\');
column.setDisplayName(\'Name\');
column.setAccessType(\'attr\');
columnModel.add(column);
column = new ht.ui.Column();
column.setName(\'temp\');
column.setAccessType(\'attr\');
column.setDisplayName(\'TEMP\');
column.setAlign(\'center\');
column.formatValue = function(value, data) {
return data.a(\'temp\') + \' ℉\';
};
column.setEditable(true);
column.setEditorClass(\'ht.ui.editor.NumberEditor\');
columnModel.add(column);
column = new ht.ui.Column();
column.setName(\'presure\');
column.setAccessType(\'attr\');
column.setDisplayName(\'Presure\');
column.formatValue = function(value, data) {
return data.a(\'presure\') + \' PSI\';
}
column.setEditable(true);
column.setEditorClass(\'ht.ui.editor.IntEditor\');
columnModel.add(column);
column = new ht.ui.Column();
column.setName(\'alarms\');
column.setDisplayName(\'Alarms\');
column.setAccessType(\'attr\');
column.setValue = function(value, data) {
return data.a(\'alarms\');
}
columnModel.add(column);
column = new ht.ui.EnumColumn();
column.setDatas(status);
column.setName(\'status\');
column.setDisplayName(\'Status\');
column.setAccessType(\'attr\');
column.setEditable(true);
columnModel.add(column);
在表格中,为了增加表格的效果,我在温度过高时将该行的颜色进行了调整,对工作人员进行提醒,再把表格加入到到 Panel 组件中,并给 Panel 加上了标题
tableView.drawRowBackground = function(drawable, x, y, width, height, data) {
var g = tableView.getRootContext();
if(tableView.isSelected(data)) {
g.fillStyle = \'#87A6CB\';
}
else if(data.a(\'temp\') < 65) {
g.fillStyle = \'rgb(247, 247, 247)\';
}
else if(data.a(\'temp\') >= 65 && data.a(\'temp\') < 90) {
g.fillStyle = \'rgb(247, 204, 139)\';
}
else {
g.fillStyle = \'rgb(242, 83, 75)\';
}
g.beginPath();
g.rect(x, y, width, height);
g.fill();
};
panel.setTitle(\'TANK STATUS OVERVIEW\');
panel.setContentView(tablePane);
最后来说说模型的总体布局,这个模型的主体是左边的油罐模型和右边的表格,表格的数据全都来源于油罐,于是他们共同属于同一个数据容器。那么首先先来看这个模型的整体布局,我采用的是 ht-ui.js 库里面的相对布局器,并且设置了左边油罐模型的位置让整个结构看起来更合理,于是整个的布局代码如下:
relativeLayout = new ht.ui.RelativeLayout();
graphView = new ht.graph.GraphView(dataModel);
graphView.setZoom(2.292);
graphView.setTranslate(297, -178);
panel = new ht.ui.Panel();
panel.setBorder(0);
relativeLayout.addView(new ht.ui.HTView(graphView), {
align:\'left\',
vAlign: \'middle\',
width: \'match_parent\',
height: \'match_parent\'
});
relativeLayout.addView(panel, {
align: \'right\',
vAlign: \'top\',
marginTop: 30,
marginRight: 30,
width: 500,
height: 150
});
总结:在写这个小例子的时候遇到了问题,编辑器使用的不够熟练,一些属性不了解,每一种都得测试完才知道它是干嘛用的,花费了不少时间;还有属性面板上的简化属性按钮,在写的时候没取消,导致了很多本来可以直接设置的属性,自己手动进行添加。还有很多操作都是异步的,例如 xhrLoad 加载 JSON 等等...以后必须得更加留意异步问题。数据模型的问题:思考不同图形是否能加在同一个 Node 下,例如罐体和罐体下方的数据条;思考组件是否能加载同一个 DataModel ,例如左侧油罐模型和右侧的数据表格。通过这个小例子也确实学习到了很多东西,希望在未来的日子里努力变得越来越优秀,与大家共勉。