天天看點

Web SCADA 電力接線圖工控組态編輯器前言效果圖代碼實作總結

前言

SVG并非僅僅是一種圖像格式, 由于它是一種基于XML的語言,也就意味着它繼承了XML的跨平台性和可擴充性,進而在圖形可重用性上邁出了一大步。如SVG可以内嵌于其他的XML文檔中,而SVG文檔中也可以嵌入其他的XML内容,各個不同的SVG圖形可以友善地組合, 構成新的SVG圖形。這個 Demo 運用的技術基于 HTML5 的技術适應了隻能電網排程、配電網運作監控與配電網運維管控,通過移動終端實作 Web SCADA 賬上運維的時代需求。由于傳統電力行業 CS 桌面監控系統一直到新一代 Web 和移動終端進化中,HT 是實施成本最低,開發和運作效率最高的前端圖形技術解決方案。SVG 矢量圖形大家都不會陌生了,尤其是在工控電信等領域,但是這篇文章并不是要制作一個新的繪制 SVG 圖的編輯器,而是一個可繪制矢量圖形并且對這個圖形進行資料綁定的更高階。

效果圖

Web SCADA 電力接線圖工控組态編輯器前言效果圖代碼實作總結

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);//刻度尺           
Web SCADA 電力接線圖工控組态編輯器前言效果圖代碼實作總結

這些布局,隻需要結合 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 拓撲元件的初始化函數中,這一小節就不做解釋。

雖然說最重要的因素是拖拽,但是不可否認,這個元件在分類上也是非常直覺:

Web SCADA 電力接線圖工控組态編輯器前言效果圖代碼實作總結

如上圖,我在 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 拓撲元件

Web SCADA 電力接線圖工控組态編輯器前言效果圖代碼實作總結

前面說到了 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 樹元件

Web SCADA 電力接線圖工控組态編輯器前言效果圖代碼實作總結

至于樹元件,樹元件和 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 屬性面闆

Web SCADA 電力接線圖工控組态編輯器前言效果圖代碼實作總結

屬性面闆,即為顯示屬性的一個容器,不同的類型的節點可能在屬性的顯示上有所不同,是以我在 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 工具欄

Web SCADA 電力接線圖工控組态編輯器前言效果圖代碼實作總結

差點忘記說這個部分了,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 的資料綁定和動畫,我們就可以對這些矢量圖形中的每一個部分進行操作,比如燈的閃爍啊,比如人眨眼睛等等操作,至于這些都是後話了。有了這個編輯器我也能夠更加快速地進行開發了~

繼續閱讀