前言
SVG 並不是僅僅是一種圖像格式, 因爲它是一種基於 XML 的語言,也就意味着它繼承了 XML 的跨平臺性和可擴展性,從而在圖形可重用性上邁出了一大步。如 SVG 能夠內嵌於其餘的 XML 文檔中,而 SVG 文檔中也能夠嵌入其餘的 XML 內容,各個不一樣的 SVG 圖形能夠方便地組合, 構成新的 SVG 圖形。這個 Demo 運用的技術基於 HTML5 的技術適應了只能電網調度、配電網運行監控與配電網運維管控,經過移動終端實現 Web SCADA 帳上運維的時代需求。因爲傳統電力行業 CS 桌面監控系統一直到新一代 Web 和移動終端進化中,HT 是實施成本最低,開發和運行效率最高的前端圖形技術解決方案。SVG 矢量圖形你們都不會陌生了,尤爲是在工控電信等領域,可是這篇文章並非要製做一個新的繪製 SVG 圖的編輯器,而是一個可繪製矢量圖形而且對這個圖形進行數據綁定的更高階。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 中佈局,結合這兩種組件,代碼看起來會更加清爽。node
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);
佈局結束後,就要考慮每個容器中應該放置哪些內容,我將這些內容分別封裝到不一樣的函數中,經過調用這些函數來進行數據的顯示。json
Palette 組件面板
左側的 Palette 組件面板須要向其內部添加 group 做爲分組,而後再向組內添加節點。可是咱們使用這個組件的最重要的一個緣由是它可以拖拽節點,可是由於咱們拖拽後須要在 graphView 拓撲組件中生成一個新的節點顯示在拓撲圖上,因此我將拖拽部分的邏輯寫在了 graphView 拓撲組件的初始化函數中,這一小節就不作解釋。數組
雖說最重要的因素是拖拽,可是不能否認,這個組件在分類上也是很是直觀:網絡
如上圖,我在 Palette 中作了三個分組:電力、食品加工廠以及污水處理。並在這些分組下面填充了不少屬於該組類型的節點。我將這些分組的信息存儲在 palette_config.js 文件中,因爲三組中的信息量太大,這裏只將一小部分的信息展現出來,看看是如何經過 json 對象來對分組進行數據顯示的:app
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 的數據綁定和動畫,咱們就能夠對這些矢量圖形中的每個部分進行操做,好比燈的閃爍啊,好比人眨眼睛等等操做,至於這些都是後話了。有了這個編輯器我也可以更加快速地進行開發了~