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

前言

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

效果图

图片描述

http://www.hightopo.com/demo/...前端

代码实现

总体框架

根据上图看得出来,整个界面被分为五个部分,分别为 palette 组件面板,toolbar 工具条,graphView 拓扑组件,propertyPane 属性面板以及 treeView 树组件,这五个部分中的组件须要先建立出来,而后才放到对应的位置上去:node

dataModel = new ht.DataModel();//数据容器 承载Data数据的模型
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 中布局,结合这两种组件,代码看起来会更加清爽。json

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();

布局结束后,就要考虑每个容器中应该放置哪些内容,我将这些内容分别封装到不一样的函数中,经过调用这些函数来进行数据的显示。数组

Palette 组件面板

左侧的 Palette 组件面板须要向其内部添加 group 做为分组,而后再向组内添加节点。可是咱们使用这个组件的最重要的一个缘由是它可以拖拽节点,可是由于咱们拖拽后须要在 graphView 拓扑组件中生成一个新的节点显示在拓扑图上,因此我将拖拽部分的逻辑写在了 graphView 拓扑组件的初始化函数中,这一小节就不作解释。网络

虽说最重要的因素是拖拽,可是不能否认,这个组件在分类上也是很是直观:app

图片描述

如上图,我在 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 拓扑图形中,来看看这个部分是如何实现的。若是 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);//删除顶层Painter
        if(isPoints){
            resetDefault();//重置toolbar导航栏的状态
        }        
    }            
});

总结

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

相关文章
相关标签/搜索