前言:大數據,人工智能,工業物聯網,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 篇》。