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


預覽鏈接://www.hightopo.com/demo/tunnel2/index.html
上圖中的各種設備都可以雙擊,此時 camera 的位置會從當前位置移動到雙擊的設備的正前方;隧道入口的展示牌會自動輪播,出現事故時會展示牌中的內容會由「限速80,請開車燈」變為「超車道兩車追尾,請減速慢行」;兩隧道中間的逃生通道上方的指示牌是可以點擊的,點擊切換為藍綠色激活狀態,兩旁的逃生通道門也會打開,再單擊指示牌變為灰色,門關閉;還有一個事故現場模擬,雙擊兩旁變壓器中其中一個,在隧道內會出現一個「事故現場圖標」,單擊此圖標,出現彈出框顯示事故等等等等。
// 數據容器
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);
}
// 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);
}
});
});

// 設置情報板中的文字向左滾動,並且當文字全部顯示時重複閃爍三次
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);
}
}

node.s({
'shape3d': 'billboard',
'shape3d.image': imageUrl,
'shape3d.image.cache': true
});
// 還記得用 shape3d.image.cache 的代價么?
g3d.invalidateShape3dCachedImage(node);

// 設置「目標」位置
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
]);
}
});
};
// 獲取事件對象的三維坐標
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]);
}

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;
}
}
}
});
// 點擊圖元
else if (e.kind === 'clickData'){
timer = setTimeout(function() {
clearTimeout(timer);
// 如果是「事故」圖標節點
if (e.data.getTag() === 'jam') {
// 創建一個對話框
createDialog(e.data);
}
}, 200);
}

// 彈出框右邊的表單
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;
}


