前言:大數據,人工智能,工業物聯網,5G 已經或者正在潛移默化地改變着咱們的生活。在信息技術快速發展的時代,誰能抓住數據的核心,利用有效的方法對數據作數據挖掘和數據分析,從數據中發現趨勢,誰就能作到精準控制,實時分析,有的放矢,從而獲取更快速、更平穩、更長遠地發展。在航空領域,機場、航班和航線信息是相當重要的數據,本文將介紹以 HT 爲平臺,應用 JavaScript、HTML五、GIS 等技術開發的全球航線實例。html
界面預覽git
- 主界面segmentfault
- 飛機及飛機陰影動畫數組
代碼實現性能優化
- 場景搭建dom
本實例的場景包括 3D 和 2D 場景兩部分,分別是經過 HT 的 3D 和 2D 編輯器構建,該編輯工具基於 HTML5 技術開發,易於上手,並且預約義了許多圖元類型,用戶能夠無編碼快速可視化搭建各類 3D/2D 場景。3D 場景效果以下:編輯器
2D 面板部分主要包括左側航線表格,右側風暴實時數據表格以及底部的信息面板。左側航線表格展現了不一樣大洲的航線信息,大洲能夠經過底部的左側按鈕進行切換;右測風暴信息是模擬生成,實時更新;底部信息欄包括大洲按鈕及航線詳細信息。面板截圖:ide
- 航線來源及機場位置的計算svg
實例的機場和航線源數據來自於開源網站 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 篇》。