前言
低碳工業園區的建設與推廣是我國推進工業低碳轉型的重要舉措,低碳工業園區能源與碳排放管控平台是低碳工業園區建設的關鍵環節。如何對園區內的企業的能源量進行采集、計量、碳排放核算,如何對能源消耗和碳排放進行實時動態監測等問題,涉及多個技術領域,專業性強。其數據不僅要求准確,更要求真實可靠(即可核查、可溯源)。這是低碳工業園區“管控平台”建設的核心任務,也是當前我國工業園區建設中需要迫切解決的主要問題之一。
http://www.hightopo.com/demo/HTBuilding/index.html
這個 gif 圖中顯示的是一個 2D 3D 結合而成的低碳工業園區的能源監控系統,主要對各個樓宇以及園區整體的水、電等的使用量的實時監控。
代碼實現
搭建場景
要創建出一個 3D 的低碳工業園區場景並不難,但是如何在同一個界面上同時顯示 2D 和 3D 的場景呢?想要做出炫酷的效果,這種方式在很多情況下是非常有用的。
整個低碳工業園區的場景是搭建在 2D 上的,我們知道,HTML 給 DOM 元素設置圖片只能用傳統的柵格位圖,但是如果怕圖片被拉伸而導致圖片模糊或者變形等結果,用 json 格式的矢量圖片來實現是最好的,柵格位圖在拉伸放大或縮小時會出現圖形模糊,線條變粗出現鋸齒等問題。 而矢量圖片通過點、線和多邊形來描述圖形,因此在無限放大和縮小圖片的情況下依然能保持一致的精確度。
首先我搭建了一個 2D 的場景用來放置我們的 json 矢量圖,利用 ht.Default.xhrLoad 函數將 json 矢量背景圖反序列化顯示在 gv 上,這個 json 矢量背景圖中除了作為背景的 node 還有另外兩個節點,如下圖,紅線框起來的比較大的這個節點是用來裝 3D 場景的,而右邊框起來的比較小的節點是用來放置另外一個 gv 的(暫時還用不到,后期需要添加類似 form 表單的功能,所以我需要固定位置):
ht.Default.xhrLoad('displays/background.json', function(text) { dm.deserialize(text);// 反序列化數據到數據模型 gv.addToDOM();// 將 2D 場景添加到 body 體中 });
這個 2D 場景作為背景的部分就設置完畢,接下來看看如何在 2D 場景的基礎下放上 3D 場景。
2D 中添加 3D 場景
向 2D 中添加 3D 也是非常容易,問題是如何使 3D 場景根據 2D 場景縮放和平移來進行自適應變化,使 3D 場景始終保持在 2D 場景的某個固定的位置?我是通過監聽 gv 的屬性變化事件,監聽到 zoom、translate 等屬性,對 3D 場景進行自動布局的操作:
var g3dInfo = create3D('g3dNode'); gv.mp(function(e) {// 監聽 gv 的屬性變化事件 if (e.property === 'zoom' || e.property === 'translateX' || e.property === 'translateY' ) { layout(g3dInfo); } }); function layout(info) { var rect = info.node.getRect(),// 獲取場景依賴的節點的 矩形區域 zoom = gv.getZoom(),// 獲取當前 gv 的縮放值 tx = gv.tx(),// 獲取當前 gv 的水平平移值 ty = gv.ty();// 獲取當前 gv 的垂直平移值 // 依賴的節點的大小根據 zoom 縮放值來進行縮放 rect.x *= zoom, rect.y *= zoom, rect.width *= zoom, rect.height *= zoom; var x = rect.x + tx, y = rect.y + ty; // 設置場景自動布局 if (info.g3d) info.g3d.layout(x, y, rect.width, rect.height); }
眼尖的同學應該已經注意到了,我沒有寫出 create3D 函數的聲明,就展示的效果而言,這個方法只是將場景 json 圖紙反序列化到 3D 場景中,並追加了一個對象 info,將 3D 場景所依賴的 node 和 3D 場景的變量傳進去:
function create3D(tag) { var g3d = new ht.graph3d.Graph3dView();// 3D 組件 var dataModel = g3d.dm();// 獲取 3D 場景的數據容器 gv.getView().appendChild(g3d.getView());// 將 3D 場景添加到 2D 場景中 ht.Default.xhrLoad('scenes/電雲維.json', function(text) {// 加載 3D 場景的 json 矢量圖紙 dataModel.deserialize(text);// 反序列化數據到數據模型 }); // 停止事件的傳播,阻止它被分派到其他 Document 節點 g3d.getView().addEventListener('mousedown', function(e) { e.stopPropagation()}); g3d.getView().addEventListener('mousewheel', function(e) { e.stopPropagation()}); if (isFirefox=navigator.userAgent.indexOf("Firefox") > 0) { g3d.getView().addEventListener('DOMMouseScroll', function(e) { e.stopPropagation()}); } var info = { g3d: g3d, node: dm.getDataByTag(tag), }; return info; }
2D 和 3D 在鼠標事件上有很多相同的點,但是我們並不希望在操作 3D 場景的同時 2D 場景也跟着變化,所以上面代碼中禁止了鼠標按下和滾輪的事件傳播。
樓宇信息顯示
低碳工業園區監控系統實現的其中一個功能:點擊樓宇視線移到樓宇顯示到一個比較合適的位置,並且樓宇頂部顯示一個面板用來展示當前樓宇的信息。這里我直接創建了一個節點,通過設置節點的 shape3d 屬性為 billboard 即可顯示為一個“面片”,面板非常好用,首先它只有一個面,在 3D 場景中如果需要大量的顯示數據的節點,推薦用這個 billboard 類型,非常省性能。
// 創建在建築上面的顯示面板 var billboard = new ht.Node(); billboard.setScaleX(2);// 將節點 X 軸上放大 2 倍 billboard.setScaleTall(2);// 將節點 Y 軸上放大 2 倍 billboard.s({ 'shape3d': 'billboard',// 此類型為一個面片 'shape3d.image': 'symbols/nodeForm.json',// 設置面片的顯示圖片為矢量圖片 'shape3d.autorotate': true,// 始終面向相機 'shape3d.vector.dynamic': true,// 設置矢量圖形 '3d.visible': false// 不可見 }); billboard.setTag('billboard');// 設置節點的 tag 唯一屬性 dataModel.add(billboard);// 將節點添加到數據容器中
通過點擊不同的樓宇則將信息面板展示在當前點擊的樓宇上方, 並根據不同的選中情況對 billboard 進行顯隱的控制:
dataModel.sm().ms(function(e) {// 監聽選中變化事件 if (e.kind === 'set' || e.kind === 'append') {// 設置選中 及 追加選中 billboard.s('3d.visible', true); var data = dataModel.sm().ld();// 獲取當前選中的最后一個節點 if (!data) return; billboard.p3(data.getPosition().x, data.getTall() + 200, data.getPosition().y);// 設置 billboard 的位置為當前選中的節點的上方 } else if (e.kind === 'remove') {// 選中移除 var data = dataModel.sm().ld();// 獲取當前最后選中的節點 if (data) { billboard.setPosition(data.getPosition().x, data.getPosition().y); billboard.setElevation(data.getTall() + 200); } else billboard.s('3d.visible', false); } else if (e.kind === 'clear') billboard.s('3d.visible', false);// 清除所有的選中后設置 billboard 不可見 });
(其他例子參考)
http://www.hightopo.com/demo/large-screen-photovoltaic/
至於點擊樓宇,從當前視線位置推到節點位置是通過 flyTo 函數,此函數在 6.2.2 版本是有三個參數,參數一為目標節點,參數二為是否動畫,參數三為眼睛跟目標節點中心距離的計算,比如下面代碼設置 0.5,表示眼睛在上述方向上動態計算距離以將目標適配到屏幕 0.5 里容納。信息面板上方顯示了當前點擊的樓宇的名稱,我是在設計 3D 場景的圖紙時給對應的樓宇設置上 displayName 屬性,當前顯示則根據這個 displayName 來進行顯示。
g3d.mi(function(e) {// 增加交互事件監聽器 if(e.kind === 'clickData'){ g3d.flyTo(e.data, true, 0.5);// 將 eye 和 center 從當前位置“飛到”目標節點的位置 第二個參數若是1 則占滿全屏。 6.2.2 版本以上有此方法 var name = e.data.getDisplayName(); // 由於 3D 中不能將模型組合到一起,所以我用追加選中的方法來解決 dataModel.each(function(node) { if(node.getDisplayName() !== name) return;// 我將同一類型的節點的 displayName 設置相同 dataModel.sm().appendSelection(node); }) } });
那么,只有一個 billboard,我們如何讓這個 billboard 根據不同的樓宇顯示不同的信息?這個時候矢量圖標的優勢又多了一個,通過對矢量圖標中的某個部分進行數據綁定進行數據的動態變化,這邊我三言兩語也講不完整,我就簡單提一下如何實現,剩下的可以去官網中的數據綁定手冊中查閱相關資料和具體實現。
前面給 billboard 設置了一個 shape3d.image 屬性,設置的圖片為 nodeForm.json,這個 json 中有四行文本顯示,頂部的文本用來顯示當前點擊的樓宇的名稱。
根據手冊我們知道數據綁定的格式分為兩種,一種是綁定 function 類型,另一種是綁定 string 類型,如下:
也就是說如果 HT 中沒有定義我們需要的屬性或者說一個矢量圖上有多個相同的屬性需要更改為不同的值,就可以通過 attr 來自定義屬性,這里我用的就是這個方法:
"text": { "func": "attr@buildingName", "value": "賽普健身學院學生宿舍" }
數據綁定完成后,我們只需要根據這個綁定數據對當前引用這個 json 矢量圖標的節點的業務屬性變化即可:
// 不同的樓宇上顯示的內容不同 billboard.a('buildingName', name); billboard.a('electricUsage', (Math.random()*300).toFixed(2)); billboard.a('waterUsage', (Math.random()*300).toFixed(2)); billboard.a('gasUsage', (Math.random()*300).toFixed(2));
右側數據顯示
3D 場景創建完畢,接下來如何在 3D 上面再加右邊的兩個數據顯示面板?這里我是在前面 2D json 場景中已排布好位置的節點上添加了另外一個 2D 場景,用來顯示整體場景數據。因為這個 gv 上有兩個信息面板,所以我直接在 graphView 上添加了兩個節點,並將節點添加到這個 graphView 的 dataModel 數據容器上,其他部分我就不再做解釋了,都是基礎的代碼:
function createGV(tag) { var g2d = new ht.graph.GraphView();// 2D 拓撲場景 var dataModel = g2d.dm();// 獲取當前拓撲場景的數據容器 gv.getView().appendChild(g2d.getView());// 將此拓撲場景添加到底層背景圖上 g2d.setInteractors([]);// 清除此組件上的交互 // 添加兩個節點到拓撲場景上 var node = new ht.Node(); node.setImage('symbols/form.json'); node.setPosition(0, 0); dataModel.add(node); var node1 = new ht.Node(); node1.setImage('symbols/form1.json'); node1.setPosition(0, dm.getDataByTag(tag).getHeight()/3); dataModel.add(node1); g2d.fitContent(); setInterval(function() {// form表單數據動態變化 node.a('electricUse', (Math.random()*300).toFixed(2)); node.a('waterUse', (Math.random()*300).toFixed(2)); node.a('gasUse', (Math.random()*300).toFixed(2)); node.a('tempUse', (RandomNumBoth(10, 40))+''); node.a('wetUse', (Math.floor((Math.random()*100)))+''); }, 3000); var info = { g2d: g2d, node: dm.getDataByTag(tag) } return info; }
以上,整個低碳工業園區監控系統的實現全部結束,有問題的或者建議都可以給我留言,或者直接訪問官網(http://hightopo.com/)查閱對應的資料。