Web SCADA 電力接線圖工控組態編輯器

圖撲軟體發表於2018-04-16

前言

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 樹元件,這五個部分中的元件需要先建立出來,然後才放到對應的位置上去:

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 中佈局,結合這兩種元件,程式碼看起來會更加清爽。

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 拓撲元件的初始化函式中,這一小節就不做解釋。

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



 

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

相關文章