电力 Web SCADA 工控组态编辑器
前言
SVG 并非仅仅是一种图像格式, 由于它是一种基于 XML 的语言,也就意味着它继承了 XML 的跨平台性和可扩展性,从而在图形可重用性上迈出了一大步。如 SVG 可以内嵌于其他的 XML 文档中,而 SVG 文档中也可以嵌入其他的 XML 内容,各个不同的 SVG 图形可以方便地组合, 构成新的 SVG 图形。这个 Demo 运用的技术基于 HTML5 的技术适应了只能电网调度、配电网运行监控与配电网运维管控,通过移动终端实现 Web SCADA 账上运维的时代需求。由于传统电力行业 CS 桌面监控系统一直到新一代 Web 和移动终端进化中,HT 是实施成本最低,开发和运行效率最高的前端图形技术解决方案。SVG 矢量图形大家都不会陌生了,尤其是在工控电信等领域,但是这篇文章并不是要制作一个新的绘制 SVG 图的编辑器,而是一个可绘制矢量图形并且对这个图形进行数据绑定的更高阶。
效果图
代码实现
整体框架
根据上图看得出来,整个界面被分为五个部分,分别为 palette 组件面板,toolbar 工具条,graphView 拓扑组件,propertyPane 属性面板以及 treeView 树组件,这五个部分中的组件需要先创建出来,然后才放到对应的位置上去:
- palette = new ht.widget.Palette();// 组件面板
- toolbar = new ht.widget.Toolbar(toolbar_config);// 工具条
- g2d = new ht.graph.GraphView(dataModel);// 拓扑组件
- treeView = new ht.widget.TreeView(dataModel);// 树组件
- propertyPane = new ht.widget.PropertyPane(dataModel);// 属性面板
- propertyView = propertyPane.getPropertyView();// 属性组件
- rulerFrame = new ht.widget.RulerFrame(g2d);// 刻度尺
这些布局,只需要结合 splitView 和 borderPane 进行布局即可轻松完成~其中 splitView 为 HT 中的 分割组件,参数1为放置在前面的 view 组件(可为左边的,或者上面的);参数2为放置在后面的 view 组件(可为右边的,或者下面的);参数3为可选值,默认为 h,表示左右分割,若设置为 v 则为上下分割;参数4即为分割的比例。borderPane 跟 splitView 的作用有些相似,但是在这个 Demo 中布局,结合这两种组件,代码看起来会更加清爽。
- borderPane = new ht.widget.BorderPane();// 边框面板
- leftSplit = new ht.widget.SplitView(palette, borderPane, 'h', 260);// 分割组件,h表示左右分割,v表示上下分割
- rightSplit = new ht.widget.SplitView(propertyPane, treeView, 'v', 0.4);
- mainSplit = new ht.widget.SplitView(leftSplit, rightSplit, 'h', -260);
- borderPane.setTopView(toolbar);// 设置边框面板的顶部组件为 toolbar
- borderPane.setTopHeight(30);
- borderPane.setCenterView(rulerFrame);// 设置边框面板的中间组件为 rulerframe
- mainSplit.addToDOM();// 将 mainSplit 的底层 div 添加进 body 体中
- dataModel.deserialize(datamodel_config);// 反序列化 datamodel_config 的内容,将json内容转为拓扑图场景内容
- g2d.fitContent(true);
布局结束后,就要考虑每一个容器中应该放置哪些内容,我将这些内容分别封装到不同的函数中,通过调用这些函数来进行数据的显示。
Palette 组件面板
左侧的 Palette 组件面板需要向其内部添加 group 作为分组,然后再向组内添加节点。但是我们使用这个组件的最重要的一个原因是它能够拖拽节点,但是因为我们拖拽后需要在 graphView 拓扑组件中生成一个新的节点显示在拓扑图上,所以我将拖拽部分的逻辑写在了 graphView 拓扑组件的初始化函数中,这一小节就不做解释。
虽然说最重要的因素是拖拽,但是不可否认,这个组件在分类上也是非常直观:
如上图,我在 Palette 中做了三个分组:电力、食品加工厂以及污水处理。并在这些分组下面填充了很多属于该组类型的节点。我将这些分组的信息存储在 palette_config.js 文件中,由于三组中的信息量太大,这里只将一小部分的信息展示出来,看看是如何通过 json 对象来对分组进行数据显示的:
- palette_config = {
- scene: {
- name: '电力',
- items: [
- { name: '文字', image: '__text__', type: ht.Text },
- { name: '箭头', image: 'symbols/arrow.json' },
- { name: '地线', image: 'symbols/earthwire.json' }
- ]
- },
- food: {
- name: '食品加工厂',
- items: [
- { name: '间歇式流化床处理器', image: 'symbols/food/Batch fluid bed processor.json'},
- { name: '啤酒瓶', image: 'symbols/food/Beer bottle.json'},
- { name: '台式均质机', image: 'symbols/food/Batch fluid bed processor.json'}
- ]
- },
- pumps: {
- name: '污水处理',
- items: [
- { name: '3维泵', image: 'symbols/pumps/3-D Pump.json'},
- { name: '18-惠勒卡车', image: 'symbols/pumps/18-wheeler truck 1.json'}
- ]
- }
- };
通过遍历这个对象获取内部数据,显示不同的数据信息。当然,在获取对象的信息的时候,我们需要创建 ht.Group 类的对象,以及分组内部的 ht.Node 类的元素(这些元素都为组的孩子),然后将这些获取来的数据赋值到这两种类型的节点上,并且将这些节点添加到 Palette 的数据容器中:
- function initPalette(){// 初始化组件面板中的内容
- for(var name in palette_config){// 从 palette_config.js 文件中获取数据
- var info = palette_config[name];
- var group = new ht.Group();// 组件面板用ht.Group展示分组,ht.Node展示按钮元素
- group.setName(info.name);
- group.setExpanded(false);// 设置group默认关闭
- palette.dm().add(group);// 将节点添加到 palette 的数据容器中
- info.items.forEach(function(item){
- var node = new ht.Node();// 新建 ht.Node 类型节点
- node.setName(item.name);// 设置名称 用于显示在 palette 面板中节点下方说明文字
- node.setImage(item.image);// 设置节点在 palette 面板中的显示图片
- // 文本类型
- if (item.type === ht.Text) {// 通过 json 对象中设置的 type 信息来获取当前信息为何种类型的节点,不同类型的节点有些属性设置不同
- node.s({
- 'text': 'Text',// 文本类型的节点需要设置这个属性显示文本的内容
- 'text.align': 'center',// 文本对齐方式
- 'text.vAlign': 'middle',// 文本垂直对齐方式
- 'text.font': '32px Arial'// 文本字体
- });
- }
- node.item = item;
- node.s({
- 'image.stretch': item.stretch || 'centerUniform',// 设置节点显示图片为填充的方式,这样不同比例的图片也不会因为拉伸而导致变形
- 'draggable': item.draggable === undefined ? true : item.draggable,// 设置节点是否可被拖拽
- });
- group.addChild(node);// 将节点设置为 group 组的孩子
- palette.dm().add(node);// 节点同样也得添加到 palette 的数据容器中进行存储
- });
- }
- }
graphView 拓扑组件
http://www.hightopo.com/demo/2deditor/HT-2D-Editor.html
前面说到了 Palette 组件中节点拖拽到 graphView 拓扑图形中,来看看这个部分是如何实现的。如果 Palette 中的 Node 的 draggable 属性设置为 true ,那么 Palette 可以自动处理 dragstart ,但是 dragover 和 dragdrop 事件需要我们处理,我们知道 IOS 和 Android 设备上并不支持 dragover 和 dragdrop 这类事件,所以 Palette 插件还提供了模拟的拖拽事件 handleDragAndDrop,可以完美兼容 PC 和手持终端。
- function initGraphView(){
- if(ht.Default.isTouchable){// 判断是否为触屏可Touch方式交互
- palette.handleDragAndDrop = function(e, state) {// 重写此方法可以禁用HTML5原生的Drag和Drop事件并启用模拟的拖拽事件
- if(ht.Default.containedInView(e, g2d)){// 判断交互事件所处位置是否在View组件之上
- if(state === 'between'){
- e.preventDefault();// 取消事件的默认动作。
- }
- else if(state === 'end'){// 当state为end时,判断e是否在graphView的范围内,如果是,则创建Node
- handleDrop(e);
- }
- }
- };
- }
- else{
- g2d.getView().addEventListener("dragover", function(e) {
- e.dataTransfer.dropEffect = "copy";
- e.preventDefault();
- });
- g2d.getView().addEventListener("drop", function(e) {
- handleDrop(e);
- });
- }
- }
- function handleDrop(e){// 被拖拽的元素在目标元素上同时鼠标放开触发的事件
- e.preventDefault();
- var paletteNode = palette.dm().sm().ld();// 获取 palette 面板上最后选中的节点
- if (paletteNode) {
- var item = paletteNode.item,
- image = item.image;
- data = g2d.getDataAt(e, null, 5);// 获取事件下的节点
- var node = new (item.type || ht.Node)();
- node.setImage(image);// 设置节点图片
- node.setName(item.name);// 设置节点名称
- node.p(g2d.lp(e));// 设置节点的坐标为拓扑中的逻辑坐标 lp函数为将事件坐标转换为拓扑中的逻辑坐标
- node.s('label', '');// 设置节点在 graphView 中底部不显示 setName 中的说明。因为 label 的优先级大于 name
- if(data instanceof ht.Group){// 如果拖拽到“组类型”的节点上,那么直接设置父亲孩子关系
- node.setParent(data);// 设置节点的父亲
- data.setExpanded(true);// 展开分组
- }else{
- node.setParent(g2d.getCurrentSubGraph());
- }
- g2d.dm().add(node);
- g2d.sm().ss(node);
- }
- }
我在 graphView 拓扑图的场景中央添加了一个 json 场景,通过 dm.deserialize(datamodel_config) 反序列化 json 场景内容导出的一个电信行业的图纸。HT 独特的矢量引擎功能满足电力行业设备种类繁多、设备图元和线路网络需无极缩放、绑定量测数据实时刷新等需求;三维呈现技术使得电力厂站和变压器等设备 3D 可视化监控成为可能。
treeView 树组件
至于树组件,树组件和 graphView 拓扑组件共用同一个 dataModl 数据容器,本来只需要创建出一个树组件对象,然后将其添加进布局容器中即可显示当前拓扑图形中的所有的数据节点,一般 HT 会将树组件上的节点分为几种类型进行显示,ht.Edge、ht.Group、ht.Node、ht.SubGraph、ht.Shape 等类型进行显示,但是这样做有一个问题,如果创建的节点非常多的话,那么无法分辨出那个节点是哪一个,也就无法快速地定位和修改该节点,会给绘图人员带来很大的困扰,所以我在 treeView 的 label 和 icon 的显示上做了一些处理:
- // 初始化树组件
- function initTreeView() {
- // 重载树组件上的文本显示
- treeView.getLabel = function (data) {
- if (data instanceof ht.Text) {
- return data.s('text');
- }
- else if (data instanceof ht.Shape) {
- return data.getName() || '不规则图形'
- }
- return data.getName() || '节点'
- };
- // 重载树组件上的图标显示
- var oldGetIconFunc = treeView.getIcon;
- treeView.getIcon = function (data) {
- if (data instanceof ht.Text) {
- return 'symbols/text.json';
- }
- var img = data.getImage();
- return img ? img : oldGetIconFunc.apply(this, arguments);
- }
- }
propertyPane 属性面板
属性面板,即为显示属性的一个容器,不同的类型的节点可能在属性的显示上有所不同,所以我在 properties_config.js 文件中将几个比较常见的类型的属性存储到数组中,主要有几种属性: text_properties 用于显示文本类型的节点的属性、data_properties 所有的 data 节点均显示的属性、node_properties 用于显示 ht.Node 类型的节点的属性、group_properties 用于显示 ht.Group 类型的节点的属性以及 edge_properties 用于显示 ht.Edge 类型的节点的属性。通过将这些属性分类,我们可以对在 graphView 中选中的不同的节点类型来对属性进行过滤:
- function initPropertyView(){// 初始化属性组件
- dataModel.sm().ms(function(e){// 监听选中变化事件
- propertyView.setProperties(null);
- var data = dataModel.sm().ld();
- // 针对不同类型的节点设置不同的属性内容
- if (data instanceof ht.Text) {// 文本类型
- propertyView.addProperties(text_properties);
- return;
- }
- if(data instanceof ht.Data){// data 类型,所有的节点都基于这个类型
- propertyView.addProperties(data_properties);
- }
- if(data instanceof ht.Node){// node 类型
- propertyView.addProperties(node_properties);
- }
- if(data instanceof ht.Group){// 组类型
- propertyView.addProperties(group_properties);
- }
- if(data instanceof ht.Edge){// 连线类型
- propertyView.addProperties(edge_properties);
- }
- });
- }
数据绑定在属性栏中也有体现,拿 data_properties 中的“标签”和“可编辑”作为演示:
- {
- name: 'name',// 设置了 name 属性,如果没有设置 accessType 则默认通过 get/setName 来获取和设置 name 值
- displayName: '名称',// 用于存取属性名的显示文本值,若为空则显示name属性值
- editable: true// 设置该属性是否可编辑
- },
- {
- name: '2d.editable',// 结合 accessType,则通过 node.s('2d.editable') 获取和设置该属性
- accessType: 'style',// 操作存取属性类型
- displayName: '可编辑',// 用于存取属性名的显示文本值,若为空则显示name属性值
- valueType: 'boolean',// 布尔类型,显示为勾选框
- editable: true// 设置该属性是否可编辑
- }
这两个属性比较有代表性,一个是直接通过 get/set 来设置 name 属性值,一个是通过结合属性的类型来控制 name 的属性值。只要在属性栏中操作“名称”和“可编辑”两个属性,就可以直接在拓扑图中看到对应的节点的显示情况,这就是数据绑定。当然,还可以对矢量图形进行局部的数据绑定,但不是本文的重点,有兴趣的可以参考我的这篇文章 WebGL 3D 电信机架实战之数据绑定。
toolbar 工具栏
差点忘记说这个部分了,toolbar 上总共有 8 种功能,分别是选中编辑、连线、直角连线、不规则图形、刻度尺显示、场景放大、场景缩小以及场景内容导出 json。这 8 种功能都是存储在 toolbar_config.js 文件中的,通过绘制 toolbar 中的元素给每一个元素都添加上了对应的点击触发的内容,主要讲讲 CreateEdgeInteractor.js 创建连线的内容。
我们通过 ht.Default.def 自定义了 CreateEdgeInteractor 类,然后通过 graphView.setInteractors([ new CreateEdgeInteractor(graphView, 'points')]) 这种方式来添加 graphView 拓扑图中的交互器,可以实现创建连线的交互功能。
在 CreateEdgeInteractor 类中通过监听 touchend 放手后事件向 graphView 拓扑图中添加一个 edge 连线,可以通过在 CreateEdgeInteractor 函数中传参来绘制不同的连线类型,比如 “ortho” 则为折线类型:
- var CreateEdgeInteractor = function (graphView, type) {
- CreateEdgeInteractor.superClass.constructor.call(this, graphView);
- this._type = type;
- };
- ht.Default.def(CreateEdgeInteractor, DNDInteractor, {// 自定义类,继承 DNDInteractor,此交互器有一些基本的交互功能
- handleWindowTouchEnd: function (e) {
- this.redraw();
- var isPoints = false;
- if(this._target){
- var edge = new ht.Edge(this._source, this._target);// 创建一条连线,传入起始点和终点
- edge.s({
- 'edge.type': this._type// 设置连线类型 为传入的参数 type 类型
- });
- isPoints = this._type === 'points';// 如果没有设置则默认为 points 连线方式
- if(isPoints){
- edge.s({
- 'edge.points': [{// 设置连线的点
- x: (this._source.p().x + this._target.p().x)/2,
- y: (this._source.p().y + this._target.p().y)/2
- }]
- });
- }
- edge.setParent(this._graphView.getCurrentSubGraph());// 设置连线的父亲节点为当前子网
- this._graphView.getDataModel().add(edge);// 将连线添加到拓扑图的数据容器中
- this._graphView.getSelectionModel().setSelection(edge);// 设置选中该节点
- }
- this._graphView.removeTopPainter(this);// 删除顶层Painter
- if(isPoints){
- resetDefault();// 重置toolbar导航栏的状态
- }
- }
- });
总结
一开始想说要做这个编辑器还有点怕怕的,感觉任务重,但是不上不行,所以总是在拖,但是后来整体分析下来,发现其实一步一步来就好,不要把步骤想得太复杂,什么事情都是从小堆到大的,以前我们用 svg 绘制的图形都可以在这上面绘制,当然,如果有需要拓展也完全 ok,毕竟别人写的编辑器不一定能够完全满足你的要求。这个编辑器虽说在画图上面跟别家无异,但是最重要的是它能够绘制出矢量图形,结合 HT 的数据绑定和动画,我们就可以对这些矢量图形中的每一个部分进行操作,比如灯的闪烁啊,比如人眨眼睛等等操作,至于这些都是后话了。有了这个编辑器我也能够更加快速地进行开发了~
电力 Web SCADA 工控组态编辑器的更多相关文章
- 基于HTML5的Web SCADA工控移动应用
在电力.油田燃气.供水管网等工业自动化领域Web SCADA的概念已经提出了多年,早先年的Web SCADA前端技术大部分还是基于Flex.Silverlight甚至Applet这样的重客户端方案,在 ...
- 基于HT for Web的Web SCADA工控移动应用
在电力.油田燃气.供水管网等工业自动化领域Web SCADA的概念已经提出了多年,早先年的Web SCADA前端技术大部分还是基于Flex.Silverlight甚至Applet这样的重客户端方案,在 ...
- Web SCADA 电力接线图工控组态编辑器
前言 SVG并非仅仅是一种图像格式, 由于它是一种基于XML的语言,也就意味着它继承了XML的跨平台性和可扩展性,从而在图形可重用性上迈出了一大步.如SVG可以内嵌于其他的XML文档中,而SVG文档中 ...
- PCHMI工控组态开发视频教程
PCHMI是一款适合所有PLC工程师快速上手工控组态开发的控件 下面是视频教程链接 PCHMI工控组态 02-按钮的使用 PCHMI工控组态 03-数据显示器使用 PCHMI工控组态 04-标签控件的 ...
- 分享一款免费的工控组态软件(PCHMI)
PCHMI严格的讲它并不是一款组态软件,也不是一款SCADA软件,而是一个基于.NET构架的DLL文件,开发者可以使用微软的Visual Studio将PCHMI.DLL加载到工具箱里面进行二次开发. ...
- 两款工控控件对比评测:Iocomp和ProEssentials
对于程序员来说,要凭一己之力开发出漂亮逼真的工控仪表和工控图表是非常耗时间和精力的,那么使用专业的第三方控件就是不错的选择,不仅节约开发时间,降低了项目风险,最重要的是第三方控件写的程序更专业,工控图 ...
- 基于 HTML5 WebGL 的计量站三维可视化监控系统 Web 组态工控应用
得益于 HTML5 WebGL 技术的成熟,从技术上对工控管理的可视化,数据可视化变得简单易行!完成对工控设备的管理效率,资源管理,风险管理等的大幅度提高,同时也对国家工业4.0计划作出有力响应! 如 ...
- 开源纯C#工控网关+组态软件(七)数据采集与归档
一. 引子 在当前自动化.信息化.智能化的时代背景下,数据的作用日渐凸显.而工业发展到如今,科技含量和自动化水平均显著提高,但对数据的采集.利用才开始起步. 对工业企业而言,数据采集日益受到重视, ...
- 开源纯C#工控网关+组态软件(八)表达式编译器
一. 引子 监控画面的主要功能之一就是跟踪下位机变量变化,并将这些变化展现为动画.大部分时候,界面上一个图元组件的某个状态,与单一变量Tag绑定,比如电机的运行态,绑定一个MotorRunning ...
随机推荐
- 简述Java三大特性
1.面向对象有三大特性,分别是:封装.继承和多态.2.封装:面向对象的封装就是把描述一个对象的属性和行为的代码封装在一个类中,有些属性是不希望公开的,或者说被其他对象访问的,所以我们使用private ...
- CAS 之 Apereo CAS 简介(一)
CAS 之 Apereo CAS 简介(一) Background(背景) 随着公司业务的不断扩展,后台接入子系统不断增多,那么我们将针对不同的平台进行拆分为各自对应的子系统, 权限是不变的,那么我们 ...
- java 单向链表实现
1 class Node{//Node类 2 private String data; 3 private Node next; 4 public Node(String data){ 5 this. ...
- ava集合---LinkedList源码解析
一.源码解析 public class LinkedList<E> extends AbstractSequentialList<E> implements List<E ...
- sql with as 用法-Z
以下内容转自:http:.com/ 一.WITH AS的含义 WITH AS短语,也叫做子查询部分(subquery factoring),可以让你做很多事情,定义一个SQL片断,该SQL片断会被整个 ...
- Beta版本敏捷冲刺每日报告——Day3
1.情况简述 Beta阶段第三次Scrum Meeting 敏捷开发起止时间 2017.11.4 08:00 -- 2017.11.4 22:00 讨论时间地点 2017.11.4晚9:00,软工所实 ...
- 简单的C语言编译器--词法分析器
1. 定义词法单元Tag 首先要将可能出现的词进行分类,可以有不同的分类方式.如多符一类:将所有逗号.分号.括号等都归为一类,或者一符一类,将一个符号归为一类.我这里采用的是一符一类的方式.C代码 ...
- 关于安装wordpress出现500 Internal Server Error的解决方案
安装新版本wordpress出现500 Internal Server Error的问题: 在./wp-includes/class-http.php的268行,改成 $request_order = ...
- 如何使用ILAsm与ILDasm修改.Net exe(dll)文件
一.背景 最近项目组新上项目,交付的时间比较急迫,原本好的分支管理习惯没有遵守好,于是出现下面状况: 多个小伙伴在不同的分支上开发. 原本QA环境也存在一个阻碍性的bug A 一位同事在QA环境发布了 ...
- WebApi一个控制器中定义多个Get方法。
问题:怎样解决一个ApiController中定义多个Get方法或者Post方法? 答:要想实现一个ApiController中定义多个Get方法或者Post方法,则需要在WebApiConfig类中 ...