基於 HTML5 WebGL 與 GIS 的智慧機場大資料視覺化分析

圖撲軟體發表於2020-04-06

前言:大資料,人工智慧,工業物聯網,5G 已經或者正在潛移默化地改變著我們的生活。在資訊科技快速發展的時代,誰能抓住資料的核心,利用有效的方法對資料做資料探勘和資料分析,從資料中發現趨勢,誰就能做到精準控制,實時分析,有的放矢,從而獲取更快速、更平穩、更長遠地發展。在航空領域,機場、航班和航線資訊是至關重要的資料,本文將介紹以 HT 為平臺,應用 JavaScript、HTML5、GIS 等技術開發的全球航線例項。

介面預覽

- 主介面

- 飛機及飛機陰影動畫
在這裡插入圖片描述
程式碼實現

- 場景搭建

本例項的場景包括 3D 和 2D 場景兩部分,分別是通過 HT 的 3D 和 2D 編輯器構建,該編輯工具基於 HTML5 技術開發,易於上手,而且預定義了許多圖元型別,使用者可以無編碼快速視覺化搭建各種 3D/2D 場景。3D 場景效果如下:

2D 皮膚部分主要包括左側航線表格,右側風暴實時資料表格以及底部的資訊皮膚。左側航線表格展示了不同大洲的航線資訊,大洲可以通過底部的左側按鈕進行切換;右測風暴資訊是模擬生成,實時更新;底部資訊欄包括大洲按鈕及航線詳細資訊。皮膚截圖:

- 航線來源及機場位置的計算

例項的機場和航線源資料來自於開源網站 openflights.org。拿到原始資料之後,我們首先對機場和航線資料進行了初步處理將其存為 JSON 檔案。處理後的機場資料格式如下,每個域對應的資訊依次是緯度、經度、海拔、機場簡稱、大洲、國家、地區和機場名字。

[[-9.443380356,147.2200012, 146,"POM","OC","PG","PG-NCD","Port Moresby"],
[63.98500061,-22.60560036, 171,"KEF","EU","IS","IS-2","Reykjavík"],
[36.001741,117.63201,0,"CN-0083","AS","CN","CN-U-A",""],
… 
]

處理後的航線資料片段格式如下,以第一條資訊為例,航線的起始機場為 MIA,能夠抵達的機場包括[“3201:PUJ”,“24:MSY”,“24:MVD”,“24:NAS”,“24:ORF”,“24:PHL”,“24:PTP”,“24:PTY”,“24:RIC”,“24:SAL”,“24:SAN”,“24:SDQ”,“24:SFO”,“1299:AMS”]。

{"MIA":["3201:PUJ","24:MSY","24:MVD","24:NAS","24:ORF","24:PHL","24:PTP","24:PTY","24:RIC","24:SAL","24:SAN","24:SDQ","24:SFO","1299:AMS"],
"HKG":["3021:SIN","1683:MNL","2994:ICN","15999:PVG","24:JFK","24:LAX","24:NRT","24:SFO","330:YVR","218:KIX","576:KUL","1680:SGN","328:POM"],
"SJU":["3029:SXM","3029:TPA"],
… 
}

通過對處理後的機場、航線資料分析,可以看出機場位置是生成航線的基礎。在處理後的機場資料中,已經具備了機場的經緯度資訊,因此問題的關鍵點在於如何將經緯度轉換為球體座標,轉換程式碼如下:

// 將經緯度轉換為球體位置
getSpherePos(radius, longitude, latitude) {
    let ang1 = Math.PI * (longitude - 90) / 180;
    let ang2 = Math.PI * latitude / 180;
    let x, y, z;
    let s_r = radius;
    x = s_r * Math.sin(ang1) * Math.cos(ang2);
    y = s_r * Math.cos(ang1) * Math.cos(ang2);
    z = s_r * Math.sin(ang2);
    return [x, y, z];
}

對所有機場資料迴圈處理,計算每個機場的球體座標,並將座標資訊與其它既有的機場資訊儲存於全域性陣列中。

- 航線生成

在生成航線時,使用了 ht.Polyline 型別,該型別支援三維空間點描述,而且結合 segments 引數,實現了從二維平面曲線延伸到三維空間曲線的效果。在本例項中,根據航線的起點和終點的位置,利用向量運算構造出中間的控制點,生成貝塞爾曲線來渲染航線。航線建立並新增到 DataModel (通過 add 函式)之後, 呼叫 setHost(host) 函式將其吸附到地球,這樣地球在移動或者旋轉時,航線也會隨之變化。以下為建立一條航線的程式碼實現:

/** 
 * 根據航線起點,終點位置建立航線(貝塞爾曲線)
 * @param {Object} start 起點機場資訊
 * @param {Object} end 終點機場資訊
 */
createEdge(start, end) {
    let edge;
    let distance = ht.Default.getDistance(start.point, end.point);
    let ratio = distance / this.radius;
    let v1 = new ht.Math.Vector3(start.point);
    let v2 = new ht.Math.Vector3(end.point);
    let v3 = v1.clone().add(v2).setLength(distance / 2);
    let v4 = v3.clone().add(v2);
    v3.add(v1);
​
    edge = new ht.Polyline();
​
    // 此處設定 edge 樣式和屬性的程式碼省略
    edge.setPoints([
        { x: start.point[0], y: start.point[2], e: start.point[1] },
        { x: v3.x, y: v3.z, e: v3.y },
        { x: v4.x, y: v4.z, e: v4.y },
        { x: end.point[0], y: end.point[2], e: end.point[1] },
    ]);
    edge.setSegments([1, 4]); 
    this.dm3d.add(edge); 
    edge.setHost(this.earth);
}

這部分的難點在於如何根據航線的起點和終點位置構造中間控制點來生成貝塞爾曲線。下面的示意圖演示了程式碼中向量的計算及各個向量變數的變化。

對所有航線資料迴圈處理,呼叫建立航線的 createEdge(start, end) 函式,就能完成所有航線的繪製生成。如圖所示:

在這裡插入圖片描述

- 2D/3D 互動畫線

在文章的第二幅圖中,有一條黃色的線。這條線的起點對應著表格中選中的航線,終點對應著 3D 空間的航線。當點選表格中某條航線時,如何生成一條線,跨越 2D 和 3D 空間呢?本例項的思路是獲取 3D 空間的位置座標 p3 後,呼叫 g3d.toViewPosition 獲取二維螢幕座標 p,之後通過呼叫 g2d.getLogicalPoint 得到 2D 座標,這個座標就是終點的位置。以下是獲取終點位置的程式碼實現:

// 獲取定位線的終點 -- 3D 球體中選中航線對應的位置
getLineEnd() {
    let p3 = this.g3d.getLineOffset(this.selectedEdge, this.g3d.getLineLength(this.selectedEdge) * 0.5);
    let p = g3d.toViewPosition([p3.point.x, p3.point.y, p3.point.z]);
    p = this.g2d.getLogicalPoint(p);
    this.endPoint = p;
}

線的起點位置程式碼如下,分別計算起點的橫座標和縱座標。

// 獲取定位線的起點 -- 航線表格對應的位置
getLineStart() {
    let offset = this.table.a('ht.translateY');
    let lineStartPoint = {};
    let height = this.table.getHeight();
    let origY = this.table.p().y - height / 2 + this.table.a('ht.headHeight') + this.table.a("ht.rowHeight") / 2;
    lineStartPoint.x = this.table.p().x + this.table.getWidth() / 2;
    lineStartPoint.y = origY + this.rowIndex * this.table.a("ht.rowHeight") + offset;
    this.startPoint = lineStartPoint;
}

- 飛機,飛機陰影動畫及光源移動

在表格中選中某條航線或者雙擊地球上某條航線時,飛機將會沿著航線飛行,飛機上方有光源移動,下方有飛機陰影移動。這部分使用了 HT 內建的 startAni 函式啟動動畫。在 startAni 函式中,action 函式必須提供,實現動畫過程中的屬性變化;finishFunc 為動畫結束後呼叫的函式。一個簡單的動畫例子如下:

ht.Default.startAnim({
    frames: 60,
    interval: 16,
    finishFunc: function() {
        console.log('finish');
    },
    action: function(t) {
        console.log(t);
    }
});

以下為本 Demo 中的 action 函式,該函式完成了動畫過程中飛機、光源及飛機陰影的移動,飛機姿態調整和旋轉。

action: function (v, t) {
    let offset = that.g3d.getLineOffset(that.selectedEdge, length * v); // 偏移量
    let p1 = offset.point; // 3D 座標
    let tangent = offset.tangent; // 切線方向
    let direction = new ht.Math.Vector3(tangent);
    let vp1 = new ht.Math.Vector3(p1);
    direction.multiplyScalar(0.1);
    direction.add(vp1);
    direction.setLength(direction.length() + 2);
    vp1.setLength(vp1.length() + 2);

    that.airPlane.p3(vp1.x, vp1.y, vp1.z);
    that.airPlane.setRotationMode('yxz');
    that.airPlane.lookAtX([0, 0, 0], 'bottom');
    that.airPlane.lookAtX([direction.x, direction.y, direction.z], 'front');

    lightP = new ht.Math.Vector3(p1);
    lightP.setLength(that.radius * 2);
    that.spotLight.p3(lightP.x, lightP.y, lightP.z);

    direction.setLength(that.radius);
    lightP.setLength(that.radius);
    that.planeShadow.p3(lightP.x, lightP.y, lightP.z);
    that.planeShadow.setRotationMode('yxz');
    that.planeShadow.lookAtX([0, 0, 0], 'back');
    that.planeShadow.lookAtX([direction.x, direction.y, direction.z], 'right');
}

- 衛星動畫

例項中,衛星按照橢圓軌道圍繞地球旋轉,Logo 和光暈又圍繞衛星旋轉。橢圓軌道的計算方式採用的是引數方程。假設橢圓的半長軸和半短軸的長度分別為 a 和 b,分別以半長軸和半短軸做橢圓的內切圓和外切圓。通過下圖可以看出橢圓上任意一點 A 與內切圓上的 A1 點有相同的縱座標,與外切圓上的 A2 點有相同的橫座標,所以 A 點的座標就可以描述為 (a * cosθ,b * sinθ),其中 θ 是橢圓內切圓或者外切圓的圓心角。

Logo 和光暈的旋轉使用了 3D 旋轉函式,具體使用方法可以參照 HT 3D 手冊 中的 3D 旋轉函式部分。衛星動畫的程式碼實現如下所示:

// 衛星及 Logo 的旋轉
startSat() {
    let dm = this.dm3d;
    let a = 1226; // 橢圓半長軸
    let b = 698; // 橢圓半短軸

    let x, y, z;
    y = 0;
    let sat_ang = 0; // 衛星初始角度
    let logo_ang = 0; // Logo 初始角度
    setInterval(() => {
        sat_ang = sat_ang + this.satelliteSpeed;
        logo_ang = logo_ang + 0.01
        x = a * Math.cos(-sat_ang); // 衛星當前 x 軸座標
        z = b * Math.sin(-sat_ang); // 衛星當前 z 軸座標
        y = x * Math.sin(Math.PI * 16 / 180); // 衛星當前 y 軸座標
        x = x * Math.cos(Math.PI * 16 / 180); // 衛星軌道面沿 z 軸旋轉之後的新的 x 軸座標

        this.sat.p3(x, y, z);

        this.logo.setRotationY(logo_ang);
        this.logo.setRotationZ(28 / 180 * Math.PI);
        this.logo.setRotationMode('yzx');

        this.sat_p.setRotationY(logo_ang);
        this.sat_p.setRotationZ(-35 / 180 * Math.PI);
        this.sat_p.setRotationMode('yzx');
    }, 16.7);
}

- 風暴動畫

風暴動畫使用 setInterval() 方法重複呼叫風暴動畫部分,模擬風暴的移動,風暴變大及變小。風暴變大及變小的實現思路是設定兩個 Flag 來判斷風暴變大或者變小,風暴變大時,不斷加大風暴在 x,y,z 軸方向的長度,並利用 setSize3d 函式賦值;風暴變小時,不斷減小風暴在 x,y,z 軸方向的長度,並利用 setSize3d 函式賦值。風暴的移動程式碼實現如下:

// 風暴動畫
startStorm() {
    let s_ang = 0;
    let s_ang2 = 0;
    let s_x, s_y, s_z;
    let s_r = 380.07;
    setInterval(() => {
        s_ang = s_ang + 0.002;
        s_ang2 = s_ang2 + 0.002;
        s_x = s_r * Math.sin(s_ang) * Math.cos(s_ang2);
        s_z = s_r * Math.cos(s_ang) * Math.cos(s_ang2);
        s_y = s_r * Math.sin(s_ang2);

        this.storm.p3(s_x, s_y, s_z);
        this.storm.lookAtX([0, 0, 0], 'bottom');
        this.storm.setRotationMode('yzx');
        this.storm.setRotationY(s_ang * 20);
    }, 60);
}

效能優化

為帶來更好的使用者體驗,本例項還進行了一系列的優化,使得例項的執行更加流暢,美觀。

-分批顯示航線

在該例項中共有 2486 條航線,如果一次性顯示在地球上,加上各種樣式,那麼不但載入速度非常緩慢,而且可能會因為記憶體過大而導致程式崩潰。因此,本例項採用了分批載入航線的方式,來提高系統效能。具體實現思路是在初次載入時,設定一個名稱為 display_flag 的樣式來控制航線的顯示與否,然後每隔一定時間(本 Demo 中是每隔 30s)更新一次航線。相關程式碼如下:

this.maxDisplayCount = 300; // 30s 更新一次航線
this.MAX_DISPLAY_COUNT = 6;
edge.s({ // 建立航線時
    'display_flag': parseInt(Math.random() * 10) % this.MAX_DISPLAY_COUNT,
});

start() {
    this.edgeTimer = setInterval(() => {
        this.edges.forEach((val) => {
            let showFlag = this.checkStormDistance(val);
            showFlag = showFlag && (val.s('display_flag') == this.displayFlag);
            val.s('3d.visible', showFlag)
        });
        this.displayCount++;
        if (this.displayCount > this.maxDisplayCount) {
            this.displayFlag = (this.displayFlag + 1) % this.MAX_DISPLAY_COUNT;
            this.displayCount = 0;
        }

    }, 100);

}

- Polyline resolution 動態改變

HT 通過微分段的方式實現曲線,引數 shape3d.resolution 用來控制曲線微分段數,這個引數決定 3D 圖形精度,數值越大麴線越均勻,但同時會影響效能。在本 Demo 中,為防止飛機抖動 shape3d.resolution 設定為 60。但是這樣設定之後,效能影響會很大,因此我們採用了動態調整 resolution 的方式,根據航線是否被選中動態調整,提高效能。程式碼如下。在 updateResolution 中也需要呼叫 g3d.invalidateCachedGeometry(data) 來重置 geometry,更新方法見 “Polyline cache 以及更新方法” 部分。

// 動態改變 resolution
updateResolution(isRestore) {
    if (!this.selectedEdge) { // 沒有航線被選中
        return;
    }
    let res, thickness;
    let len = this.g3d.getLineLength(this.selectedEdge);
    if (isRestore) { // 需要恢復預設值
        res = 30;
        thickness = 0.7;
    } else {
        res = len / 200 * 30;
        if (res < 60) {
            res = 60;
        }
        thickness = 5;
    }
    this.selectedEdge.s('shape3d.resolution', res);
    this.selectedEdge.setThickness(thickness);
}

- Polyline cache 以及更新方法

如前所述,本 Demo 中建立了 2486 條航線,每條航線都是一個 ht.polyLine 型別的 3D 曲線。為提高效能,在建立航線時,將其屬性 geometry.cache 設定為 true。在後續 polyLine 的屬性(例如 points, segments, width)發生變化時,使用 g3d.invalidateCachedGeometry(data) 來重置 geometry。

// 建立航線時設定屬性
edge.s({
    'geometry.cache': true
});
// this.selectedEdge 屬性發生變化時,重置 geometry。
let ui = g3d.getData3dUI(this.selectedEdge);
ui.shapeModel = ui.info = null;
this.g3d.invalidateData(this.selectedEdge);

-有效大洲中心新增輔助定位用的立方體

在有效的大洲中心位置新增一個輔助定位用的立方體,當點選大洲按鈕時,使用 flyTo() 函式調整球體視角。

- 2D/3D 互動畫線呼叫 setTimeout

當 2D/3D 定位線顯示在皮膚後,使用者每次移動介面,定位線都需要重新計算和繪製。考慮到移動介面觸發這個事件的頻率非常高,如果每次都響應,那麼程式將會變得非常繁忙,出現卡頓現象;甚至可能造成事件丟失的情況,比如出現使用者已經停止了移動,線卻沒有畫到位的現象。因此使用 setTimout 保證更新的最短間隔為 50ms,去掉不必要的更新。當然這個間隔可以根據實際情況調整,以降低視覺上的遲鈍感。

this.updateTimer = setTimeout(() => {
    this.updateTimer = null;
    if (this.selectedEdge == null) { // 沒有航線被選中
        return;
    }
    this.getLineEnd(); // 計算 2D/3D 定位線的終點
    this.updateLine(true); // 繪製定位線
}, 50);

有了 2D 和 3D 場景,按照文中介紹的思路和邏輯,就可以完成動畫的生成,航線資料載入,航線視覺化,飛機態勢視覺化和風暴資料的實時顯示,整個過程其樂無窮。

基於航空大資料,在本例項中提到的資料分析和視覺化的基礎上,還可以挖掘更多的應用場景,比如航班執行資料視覺化,飛機資料實時展示,航班歷史資料分析,應急航線排程等。如果想了解更多工業網際網路 2D, 3D 視覺化應用案例,可以到這裡參考更多 http://www.hightopo.com/blog/1103.html 《分享數百個 HT 工業網際網路 2D 3D 視覺化應用案例之 2019 篇》

相關文章