1. 程式人生 > >新基建下,智慧交通發展新規劃:智慧隧道監控視覺化系統

新基建下,智慧交通發展新規劃:智慧隧道監控視覺化系統

前言 隨著當代經濟的發展,交通環境日益緊張,加上山區地區的交通運輸的需求,隧道的交通建設開發方興未艾。隧道交通的規劃越來越完備,而對於隧道內監控管理維護卻顯得有些不足。而工業4.0的崛起,逐步進入了智慧化的新時代,伴隨著工業網際網路的新興力量,工控視覺化系統應運而生,不僅能起到日常的監控管理維護,在發現事故或險情時能第一時間採取應急預案;還能通過實時資料的採集反饋,遠端操控裝置執行以及預測裝置的優良效能,從而達到更立體更全面的工控系統的執行。 HT for Web 不止自主研發了強大的基於 HTML5 的 2D、3D 渲染引擎,為視覺化提供了豐富的展示效果。介於 2D 組態和 3D 組態上,Hightopo(以下簡稱 HT )的 HT for Web 產品上的有著豐富的組態化可供選擇,本文將介紹如何運用 HT 豐富的 2/3D 組態搭建出一個隧道監控視覺化系統的解決方案。 監控隧道內的車道堵塞情況、隧道內的車禍現場,在隧道中顯示當前車禍位置並在隧道口給予提示等功能都是非常有必要的。這個隧道監控視覺化系統的主要內容包括:照明、風機、車道指示燈、交通訊號燈、情報板、消防、火災報警、車行橫洞、風向儀、微波車檢、隧道緊急逃生出口的控制以及事故模擬等等。   介面簡介及效果預覽  

預覽連結:http://www.hightopo.com/demo/tunnel2/index.html

上圖中的各種裝置都可以雙擊,此時 camera 的位置會從當前位置移動到雙擊的裝置的正前方;隧道入口的展示牌會自動輪播,出現事故時會展示牌中的內容會由“限速80,請開車燈”變為“超車道兩車追尾,請減速慢行”;兩隧道中間的逃生通道上方的指示牌是可以點選的,點選切換為藍綠色啟用狀態,兩旁的逃生通道門也會開啟,再單擊指示牌變為灰色,門關閉;還有一個事故現場模擬,雙擊兩旁變壓器中其中一個,在隧道內會出現一個“事故現場圖示”,單擊此圖示,出現彈出框顯示事故等等等等。

  程式碼實現 一、場景搭建 整個隧道都是基於 3D 場景上繪製的,先來看看怎麼搭建 3D 場景:
// 資料容器
dm = new ht.DataModel();
// 3d 場景
g3d = new ht.graph3d.Graph3dView(dm);
// 將場景新增到 body 中
g3d.addToDOM();

上面程式碼中的 addToDOM 函式,是一個將元件新增到 body 體中的函式的封裝,定義如下:

addToDOM = function(){
    var self = this,
         // 獲取元件的底層 div
         view = self.getView(),
         style = view.style;
    // 將元件底層div新增進body中
    document.body.appendChild(view);
    // ht 預設將所有的元件的position都設定為absolute絕對定位
    style.left = '0';
    style.right = '0';
    style.top = '0';
    style.bottom = '0';
    // 視窗大小改變事件,呼叫重新整理函式
    window.addEventListener('resize', function () { self.iv(); }, false);
}
  二、JSON反序列化 整個場景是由名為 隧道1.json 的檔案匯出而成的,我只需要用程式碼將 json 檔案中的內容轉換為我需要的部分即可:
// xhrLoad 函式是一個非同步載入檔案的函式
ht.Default.xhrLoad('./scenes/隧道1.json', function(text) {
    // 將 json 檔案中的文字轉為我們需要的 json 格式的內容
    var json = ht.Default.parse(text);
    // 反序列化資料容器,解析用於生成對應的Data物件並新增到資料容器 這裡相當於把 json 檔案中生成的 ht.Node 節點反序列化到資料容器中,這樣資料容器中就有這個節點了
    dm.deserialize(json);
});

由於 xhrLoad 函式是一個非同步載入函式,所以如果 dm 資料容器反序列化未完成就直接呼叫了其中的節點,那麼會造成資料獲取不到的結果,所以一般來說我是將一些邏輯程式碼寫在這個函式內部,或者給邏輯程式碼設定 timeout 錯開時間差。

首先,由於資料都是儲存在 dm 資料容器中的(通過 dm.add(node) 新增的),所以我們要獲取資料除了可以通過 id、tag 等獨立的方式,還可以通過遍歷資料容器來獲取多個元素。由於這個場景比較複雜,模型的面也比較多,鑑於裝置配置,我將能 Batch 批量的元素都進行了批量。

批量是 HT 實現下的一種特有的機制,批量能提高效能的原理在於,當圖元一個個獨立繪製模型時效能較差,而當一批圖元聚合成一個大模型進行一次性的繪製時, 則會極大提高 WebGL 重新整理效能,執行程式碼如下

dm.each(function(data) {
    // 對“電話”進行批量
    if (data.s('front.image') === 'assets/sos電話.png'){
        data.s('batch', 'sosBatch');
    }
    // 逃生通道批量(透明度也會影響效能)
    else if (data.s('all.color') === 'rgba(222,222,222,0.18)') {
        data.s('batch', 'emergencyBatch');
    }
    else if (data.s('shape3d') === 'models/隧道/攝像頭.json' || data.s('shape3d') === 'models/隧道/橫洞.json' || data.s('shape3d') === 'models/隧道/捲簾門.json') {
        // 個別攝像頭染色了 不做批量
        if(!data.s('shape3d.blend'))
            // 基礎批量什麼也不做
            data.s('batch', 'basicBatch');
    }
    else if (data.s('shape3d') === 'models/大型變壓器/變壓器.json') {    
        data.s('batch', 'tileBatch');
        data.setToolTip('單擊漫遊,雙擊車禍地點出現圖示');
    }
    else if (data.getDisplayName() === '地面') {
        // 設定隧道“地面”不可選中
        data.s('3d.selectable', false);
    }
    else if (data.s('shape3d') === 'models/隧道/排風.json') {
        // 排風扇的模型比較複雜,所以做批量
        data.s('batch', 'fanBatch');
    }
    else if (data.getDisplayName() === 'arrow') {
        // 隧道兩旁的箭頭路標
        if (data.getTag() === 'arrowLeft') data.s('shape3d.image', 'displays/abc.png');
        else data.s('shape3d.image', 'displays/abc2.png');
        data.s({
            'shape3d': 'billboard',
            // 快取,設定了 cache 的代價是需要設定 invalidateShape3dCachedImage
            'shape3d.image.cache': true,
            // 設定這個值,圖片上的鋸齒就不會太明顯了(若圖片型別為 json,則設定 shape3d.dynamic.transparent)
            'shape3d.transparent': true 
        });
        g3d.invalidateShape3dCachedImage(data);
    }
    // 隧道入口處的情報板
    else if (data.getTag() === 'board' || data.getTag() === 'board1') {
        // 業務屬性,用來控制文字的位置[x,y,width,height]
        data.a('textRect', [0, 2, 244, 46]); 
        // 業務屬性,設定文字內容
        data.a('limitText', '限速80,請開車燈');
        var min = -245;
        var name = 'board' + data.getId();
        window[name] = setInterval(function() {
            // 設定情報板中的文字向左滾動,並且當文字全部顯示時重複閃爍三次
            circleFunc(data, window[name], min);
        }, 100);
    }

    //給逃生通道上方的指示板 動態設定顏色
    var infos = ['人行橫洞1', '人行橫洞2', '人行橫洞3', '人行橫洞4', '車行橫洞1', '車行橫洞2', '車行橫洞3'];
    infos.forEach(function(info) {
        if(data.getDisplayName() === info) {
            data.a('emergencyColor', 'rgb(138, 138, 138)');
        }
    });

    infos = ['車道指示器', '車道指示器1', '車道指示器2', '車道指示器3'];
    infos.forEach(function(info) {
        if (data.getDisplayName() === info) {
            // 考慮到效能問題 將六面體變換為 billboard 型別元素
            createBillboard(data, 'assets/車道訊號-過.png', 'assets/車道訊號-過.png', info);
        }
    });
});
上面有一處設定了 tooltip 文字提示資訊,在 3d 中,要顯示這個文字提示資訊,就需要設定 g3d.enableToolTip() 函式,預設 3d 元件是關閉這個功能的。   三、邏輯程式碼 情報板滾動條 我就直接按照上面程式碼中提到的方法進行解釋,首先是 circleFunc 情報板文字迴圈移動的函式,在這個函式中我們用到了業務屬性 limitText 設定情報板中的文字屬性以及 textRect 設定情報板中文字的移動位置屬性:
// 設定情報板中的文字向左滾動,並且當文字全部顯示時重複閃爍三次
function circleFunc(data, timer, min) {
    // 獲取當前業務屬性 limitText 的內容
    var text = data.a('limitText');
    // 設定業務屬性 textRect 文字框的座標和大小
    data.a('textRect', [data.a('textRect')[0]-5, 2, 244, 46]); 
    if (parseInt(data.a('textRect')) <= parseInt(min)) {
        data.a('textRect', [255, 2, 244, 46]);
    }
    else if (data.a('textRect')[0] === 0) {
        clearInterval(timer);
        var index = 0;
        // 設定多個 timer 是因為能夠進入這個函式中的不止一個 data,如果在同一時間多個 data 設定同一個 timer,那肯定只會對最後一個節點進行動畫。後面還有很多這種陷阱,要注意
        var testName = 'testTimer' + data.getId();
        window[testName] = setInterval(function() {
            index++;
            // 如果情報板中文字內容為空
            if(data.a('limitText') === '') {
                setTimeout(function() {
                    // 設定為傳入的 text 值
                    data.a('limitText', text);
                }, 100);
            }
            else {
                setTimeout(function() {
                    // 若情報板中的文字內容不為空,則設定為空
                    data.a('limitText', ''); 
                }, 100);
            }
            // 重複三次 
            if(index === 11) { 
                clearInterval(window[testName]);
                data.a('limitText', text);
            }
        }, 100);

        setTimeout(function() {
            timer = setInterval(function() {
                // 回撥函式
                circleFunc(data, timer, min);
            }, 100);
        }, 1500);
    }
} 
由於 WebGL 對瀏覽器的要求不低,為了能儘量多的適應各大瀏覽器,我們將所有的“道路指示器” ht.Node 型別的六面體全部換成 billboard 型別的節點,效能能提升不少。

http://www.hightopo.com 設定 billboard 的方法很簡單,獲取當前的六面體節點,然後給這些節點設定:
node.s({
    'shape3d': 'billboard',
    'shape3d.image': imageUrl,
    'shape3d.image.cache': true
});
// 還記得用 shape3d.image.cache 的代價麼?
g3d.invalidateShape3dCachedImage(node); 
當然,因為 billboard 不能雙面顯示不同的圖片,只是一個“面”,所以我們還得在這個節點的位置建立另一個節點,在這個節點的“背面”顯示圖片,並且跟這個節點的配置一模一樣,不過位置要稍稍偏移一點。   Camera 緩慢偏移 其他動畫部分比較簡單,我就不在這裡多說了,這裡有一個雙擊節點能將視線從當前 camera 位置移動到雙擊節點正前方的位置的動畫我提一下。我封裝了兩個函式 setEye 和 setCenter,分別用來設定 camera 的位置和目標位置的:
// 設定“目標”位置
function setCenter(center, finish) {
    // 獲取當前“目標”位置,為一個數組,而 getCenter 陣列會在視線移動的過程中不斷變化,所以我們先拷貝一份
    var c = g3d.getCenter().slice(0), 
        // 當前x軸位置和目標位置的差值
        dx = center[0] - c[0], 
        dy = center[1] - c[1],
        dz = center[2] - c[2];
    // 啟動 500 毫秒的動畫過度
    ht.Default.startAnim({
        duration: 500,
        action: function(v, t) {
            // 將“目標”位置緩慢從當前位置移動到設定的位置處
            g3d.setCenter([ 
                c[0] + dx * v,
                c[1] + dy * v,
                c[2] + dz * v
            ]);
        }
    });
};

// 設定“眼睛”位置
function setEye(eye, finish) {
    // 獲取當前“眼睛”位置,為一個數組,而 getEye 陣列會在視線移動的過程中不斷變化,所以我們先拷貝一份
    var e = g3d.getEye().slice(0),
        dx = eye[0] - e[0],
        dy = eye[1] - e[1],
        dz = eye[2] - e[2];
    // 啟動 500 毫秒的動畫過度
    ht.Default.startAnim({
        duration: 500,
        // 將 Camera 位置緩慢地從當前位置移動到設定的位置
        action: function(v, t) {
            g3d.setEye([
                e[0] + dx * v,
                e[1] + dy * v,
                e[2] + dz * v
            ]);
        }
    });
};
後期我們要設定的時候就直接呼叫這兩個函式,並設定引數為我們目標的位置即可。比如我這個場景中的各個模型,由於不同視角對應的各個模型的旋轉角度也不同,我只能找幾個比較有代表性的 0°,90°,180°以及360° 這四種比較典型的角度了。所以繪製 3D 場景的時候,我也儘量設定節點的旋轉角度為這四個中的一種(而且對於我們這個場景來說,基本上只在 y 軸上旋轉了):
// 獲取事件物件的三維座標
var p3 = e.data.p3(), 
    // 獲取事件物件的三維尺寸
    s3 = e.data.s3(),
    // 獲取事件物件的三維旋轉值
    r3 = e.data.r3();
// 設定“目標”位置為當前事件物件的三維座標值
setCenter(p3);
// 如果節點的 y 軸旋轉值 不為 0
if (r3[1] !== 0) {
    // 浮點負數得做轉換才能進行比值
    if (parseFloat(r3[1].toFixed(5)) === parseFloat(-3.14159)) { 
        // 設定camera 的目標位置
        setEye([p3[0], p3[1]+s3[1], p3[2] * Math.abs(r3[1]*2.3/6)]);
    }
    else if (parseFloat(r3[1].toFixed(4)) === parseFloat(-1.5708)) {
        setEye([p3[0] * Math.abs(r3[1]/1.8), p3[1]+s3[1], p3[2]]);
    }
    else {
        setEye([p3[0] *r3[1], p3[1]+s3[1], p3[2]]);
    }
}
else {
    setEye([p3[0], p3[1]+s3[1]*2, p3[2]+1000]);
}
  事故模擬現場 最後來說說模擬的事故現場吧,這段還是比較接近實際專案的。操作流程如下:雙擊“變壓器”-->隧道中間某個部分會出現一個“事故現場”圖示-->單擊圖示,彈出對話方塊,顯示當前事故資訊-->點選確定,則事故現場之前的燈都顯示為紅色×,並且隧道入口的情報板上的文字顯示為“超車道兩車追尾,請減速慢行”-->再雙擊一次“變壓器”,場景恢復事故之前的狀態。 在 HT 中,可通過 Graph3dView#addInteractorListener(簡寫為 mi)來監聽互動過程:
g3d.addInteractorListener(function(e) {
    if(e.kind === 'doubleClickData') {
        // 有“事故”圖示節點存在
        if (e.data.getTag() === 'jam') return;
        // 如果雙擊物件是變壓器
        if (e.data.s('shape3d') === 'models/大型變壓器/變壓器.json') {
            index++;
            // 通過唯一標識 tag 標籤獲取“事故”圖示節點物件
            var jam = dm.getDataByTag('jam');
            if(index === 1){
                var jam = dm.getDataByTag('jam');
                jam.s({
                    // 設定節點在 3d 上可見
                    '3d.visible': true,
                    // 設定節點為 billboard 型別
                    'shape3d': 'billboard',
                    // 設定 billboard 的顯示圖片
                    'shape3d.image': 'assets/車禍.png', 
                    // 設定 billboard 圖片是否快取
                    'shape3d.image.cache': true,
                    // 是否始終面向鏡頭
                    'shape3d.autorotate': true,
                    // 預設保持圖片原本大小,設定為陣列模式則可以設定圖片顯示在介面上的大小
                    'shape3d.fixSizeOnScreen': [30, 30],
                });
                // cache 的代價是節點需要設定這個函式
                g3d.invalidateShape3dCachedImage(jam);
             }
             else {
                 jam.s({
                     // 第二次雙擊變壓器就將所有一切恢復“事故”之前的狀態
                     '3d.visible': false
                });
                dm.each(function(data) {
                    var p3 = data.p3();
                    if ((p3[2] < jam.p3()[2]) && data.getDisplayName() === '車道指示器1') {
                        data.s('shape3d.image', 'assets/車道訊號-過.png');
                    }
                    if(data.getTag() === 'board1') {
                        data.a('limitText', '限速80,請開車燈');
                    }
                });
                index = 0;
            }             
        }
    }
});
既然“事故”節點圖標出現了,接著點選圖標出現“事故資訊彈出框”,監聽事件同樣是在 mi(addInteractorListener)中,但是這次監聽的是單擊事件,我們知道,監聽雙擊事件時會觸發一次單擊事件,為了避免這種情況,我在單擊事件裡面做了演示:
// 點選圖元
else if (e.kind === 'clickData'){
    timer = setTimeout(function() {
        clearTimeout(timer);
        // 如果是“事故”圖示節點
        if (e.data.getTag() === 'jam') {
            // 建立一個對話方塊
            createDialog(e.data);
        }
    }, 200);
}
在上面的雙擊事件中我沒有 clearTimeout,怕順序問題給大家造成困擾,要記得加一下。 彈出框如下: 這個彈出框是由兩個 ht.widget.FormPane 表單構成的,左邊的表單只有一行,行高為 140,右邊的表單是由 5 行構成的,點選確定,則“事故”圖示節點之前的道路指示燈都換成紅色×的圖示:
// 彈出框右邊的表單
function createForm4(node, dialog) {
    // 表單元件
    var form = new ht.widget.FormPane();
    // 設定表單元件的寬
    form.setWidth(200);
    // 設定表單元件的高
    form.setHeight(200);
    // 獲取表單元件的底層 div 
    var view = form.getView();
    // 將表單元件新增到 body 中
    document.body.appendChild(view);

    var infos = [
        '編輯框內容為:2輛',
        '編輯框內容為:客車-客車',
        '編輯框內容為:無起火',
        '編輯框內容為:超車道'
    ];
    infos.forEach(function(info) {
        // 向表單中新增行
        form.addRow([ 
            info
        // 第二個引數為行寬度,小於1的值為相對值
        ], [0.1]);
    });
    
    form.addRow([
        {
            // 新增一行的“確認”按鈕
            button: {
                label: '確認',
                // 按鈕點選事件觸發
                onClicked: function() {
                    // 隱藏對話方塊
                    dialog.hide();
                    dm.each(function(data) {
                        var p3 = data.p3();
                        // 改變“車道指示器”的顯示圖片為紅色×,這裡我是根據“事故”圖示節點的座標來判斷“車道顯示器”是在前還是在後的
                        if ((p3[2] < node.p3()[2]) && data.getDisplayName() === '車道指示器1') {
                            data.s('shape3d.image', 'assets/車道訊號-禁止.png');
                        }
                        // 將隧道口的情報板上的文字替換
                        if(data.getTag() === 'board1') {
                            data.a('limitText', '超車道兩車追尾,請減速慢行');
                        }
                    });
                }
            }
        }
    ], [0.1]);
    return form;
}
  總結 伴隨著新基建的建設興起,是以新發展理念為引領,以技術創新為驅動,以資訊網路為基礎,面向高質量發展需要,提供數字轉型、智慧升級、融合創新等服務的基礎設施體系的完備,國家正邁入新時代的建設,也迎來了新時代的挑戰與機遇。隧道交通的監控可以歸納為工控管理與智慧交通建設的產物,同樣具有極為重要的意義。在眾多行業上所積累的經驗,HT 已經實現了許多不同領域建設的案例,例如路口監管視覺化系統,有興趣的話也可以瞭解一下!   2019 我們也更新了數百個工業網際網路 2D/3D 視覺化案例集,在這裡你能發現許多新奇的例項,也能發掘出不一樣的工業網際網路:https://mp.weixin.qq.com/s/ZbhB6LO2kBRPrRIfHlKGQA 同時,你也可以檢視更多案例及效果:https://www.hightopo.com/demos/index.ht