基於 WebGL 的 HTML5 低碳工業園區監控系統

圖撲軟體發表於2019-03-22

前言

低碳工業園區的建設與推廣是我國推進工業低碳轉型的重要舉措,低碳工業園區能源與碳排放管控平臺是低碳工業園區建設的關鍵環節。如何對園區內的企業的能源量進行採集、計量、碳排放核算,如何對能源消耗和碳排放進行實時動態監測等問題,涉及多個技術領域,專業性強。其資料不僅要求準確,更要求真實可靠(即可核查、可溯源)。這是低碳工業園區“管控平臺”建設的核心任務,也是當前我國工業園區建設中需要迫切解決的主要問題之一。

在這裡插入圖片描述
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/) 查閱對應的資料。

相關文章