前言

SVG并非仅仅是一种图像格式, 由于它是一种基于XML的语言,也就意味着它继承了XML的跨平台性和可扩展性,从而在图形可重用性上迈出了一大步。如SVG可以内嵌于其他的XML文档中,而SVG文档中也可以嵌入其他的XML内容,各个不同的SVG图形可以方便地组合, 构成新的SVG图形。这个 Demo 运用的技术基于 HTML5 的技术适应了只能电网调度、配电网运行监控与配电网运维管控,通过移动终端实现 Web SCADA 账上运维的时代需求。由于传统电力行业 CS 桌面监控系统一直到新一代 Web 和移动终端进化中,HT 是实施成本最低,开发和运行效率最高的前端图形技术解决方案。SVG 矢量图形大家都不会陌生了,尤其是在工控电信等领域,但是这篇文章并不是要制作一个新的绘制 SVG 图的编辑器,而是一个可绘制矢量图形并且对这个图形进行数据绑定的更高阶。

效果图

http://www.hightopo.com/demo/2deditor/HT-2D-Editor.html

代码实现

整体框架

根据上图看得出来,整个界面被分为五个部分,分别为 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 拓扑组件

前面说到了 Palette 组件中节点拖拽到 graphView 拓扑图形中,来看看这个部分是如何实现的。graphView 拓扑组件是 HT 非常重要的一个组件,了解它非常有必要。如果 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 类型 参考 HT for Web 样式手册
            });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);//删除顶层Painterif(isPoints){resetDefault();//重置toolbar导航栏的状态
        }        }
});

总结

一开始想说要做这个编辑器还有点怕怕的,就是感觉任务重,但是不上不行,所以总是在拖,但是后来整体分析下来,发现其实一步一步来就好,不要把步骤想得太复杂,什么事情都是从小堆到大的,以前我们用 svg 绘制的图形都可以在这上面绘制,当然,如果有需要拓展也完全 ok,毕竟别人写的编辑器不一定能够完全满足你的要求。这个编辑器虽说在画图上面跟别家无异,但是最重要的是它能够绘制出矢量图形,结合 HT 的数据绑定和动画,我们就可以对这些矢量图形中的每一个部分进行操作,比如灯的闪烁啊,比如人眨眼睛等等操作,至于这些都是后话了。有了这个编辑器我也能够更加快速地进行开发了~

Web SCADA 电力接线图工控组态编辑器相关推荐

  1. Web SCADA 电力接线图工控组态编辑器 1

    前言 SVG并非仅仅是一种图像格式, 由于它是一种基于XML的语言,也就意味着它继承了XML的跨平台性和可扩展性,从而在图形可重用性上迈出了一大步.如SVG可以内嵌于其他的XML文档中,而SVG文档中 ...

  2. 电力 Web SCADA 工控组态编辑器

    SVG 并非仅仅是一种图像格式, 由于它是一种基于 XML 的语言,也就意味着它继承了 XML 的跨平台性和可扩展性,从而在图形可重用性上迈出了一大步.如 SVG 可以内嵌于其他的 XML 文档中,而 ...

  3. WPF工控组态软件之温度计

    WPF以其丰富灵活的控件样式设计,相较于WinForm而言,一直是工控组态软件的宠儿.经过前两文章的学习,已经对WPF开发工控组态软件有了一个基本的了解, 今天继续学习温度计的开发,仅供学习分享使用, ...

  4. 工控 组态王6.60 SP3软件7.5 SP4 授权软件狗 USB 硬件狗 分享 下载

    工控 组态王6.60 SP3软件 授权软件狗 USB 硬件狗 分享 下载 技术咨询QQ:3137723307 组态王6.60是一款非常好用的工业类自动化控制软件,此版本继承了前面几个版本的所有功能,可 ...

  5. WPF工控组态软件之冷却塔和空气压缩机开发

    WPF以其丰富灵活的控件样式设计,相较于WinForm而言,一直是工控组态软件的宠儿.经过上一篇文章的学习,已经对WPF开发工控组态软件有了一个基本的了解, 今天继续学习冷却塔和空气压缩机的开发,仅供 ...

  6. WPF工控组态软件之管道和冷却风扇开发

    WPF以其丰富灵活的控件样式设计,相较于WinForm而言,一直是工控组态软件的宠儿,本文以两个简单的小例子,简述如何通过WPF设计出表示水流的管道,和转动的冷却风扇.仅供学习分享使用,如有不足之处, ...

  7. 适应各种工控组态软件的图库,一款常用的工控图库,包括3D按钮 、箭头、对角线、多边形、鼓风机、锅炉、工厂等多种图形

    适应各种工控组态软件的图库,一款常用的工控图库,包括3D按钮 .箭头.对角线.多边形.鼓风机.锅炉.工厂等多种图形,有四千个图形,涵盖了所有行业的工控图形,种类丰富,可以满足您日常工控设计需求,支持e ...

  8. CTFHub 工控组态分析 WP

    一道力控软件的工控题,不是很难,要对力控软件有一定的了解 这里放上力控软件的百度网盘链接链接: https://pan.baidu.com/s/1YvdJupd_h7yfBRmgFRY6gA?pwd= ...

  9. 快速搭建 物联网电力检测,web SCADA 工控组态

    效果展示: 电力检测所需的硬件拓扑图 设备层: 智能仪表 等 检测设备状态 传输层 :通讯网关 协议转换 交互层: 现场检测 大屏展示 远程维护 硬件部署好之后我们就可以在平台上搭建我们需要的系统了 ...

最新文章

  1. 75. Find Peak Element 【medium】
  2. CZoneSoft出品: 音频视频在线录制系列之 AV留言本 简介
  3. 红外发送管的角度特性测试
  4. linux基础学习7
  5. ASP:FileSystemObject处理文件
  6. PurgeComm()函数_清空缓冲区 收藏
  7. android 清空canvas部分内容_Android自定义View实现圆形头像效果
  8. eltable刷新整个表格方法_利用SSAS实现PBI报表增量刷新
  9. HDU 1495(非常可乐)
  10. latex 符号_sympy: 符号运算-1
  11. 《 自动化测试最佳实践:来自全球的经典自动化测试案例解析》一一1.3 建立自动化策略...
  12. mysql delete|删除 命令的注意点
  13. javaweb功能模块如何合理设计_燃油燃气锅炉烟道如何设计才更合理?
  14. AngularJS开发指南
  15. Loj #2324. 「清华集训 2017」小 Y 和二叉树
  16. 非常流行的版本管理工具Tower for Mac
  17. Microsoft Excel 教程:如何在 Excel 中创建自定义数字格式?
  18. matlab中gui选择一个文件,matlabgui中定义函数
  19. Excel怎么设置下拉选择项
  20. php word 生成图片,PHP导出word文档的简单实现方法(可导出图片)-Go语言中文社区

热门文章

  1. ruby Mixin用法
  2. 如何查看当前Linux的版本
  3. libevent介绍
  4. 在ASP.NET中使用WINDOWS模式登录SQL数据库
  5. Hessian源码分析(java)
  6. PE文件和COFF文件格式分析——签名、COFF文件头和可选文件头2
  7. 从FFmpeg 4. 2源码中提取dshow mjpeg code步骤
  8. 主成分分析(PCA)简介
  9. 逻辑回归(Logistic Regression)简介及C++实现
  10. c语言基础习题下载,C语言基础题目