前言
風能是一種開發中的潔凈能源,它取之不盡、用之不竭。當然,建風力發電場首先應考慮氣象條件和社會自然條件。近年來,我國海上和陸上風電發展迅猛。海水、陸地為我們的風力發電提供了很好地質保障。正是這些場地為我們的風力提供了用之不竭的能源。現在我們正在努力探索這些領域。
本文章實現了風力發電場的整體流程。能讓大家能夠看到一套完整風力發電預覽體系。
需要注意的是,本次項目是使用 Hightopo 的 HT for Web 產品來搭建的。
預覽地址:https://hightopo.com/demo/wind-power-station/
大致流程
下面是整個項目的流程圖。我們從首頁可以進入到場區分布頁面和集控頁面。
場區分布頁面又包括兩個不同的 3D 場景,分別是陸地風機場和海上風機場。點擊兩個 3D 風機場最終都會進入到 3D 風機場景。
預覽效果
首頁:
1. 世界地圖效果
2. 中國地圖效果
2. 城市地圖效果
集控中心頁面(沒有動畫效果):
場區分布頁面(沒有動畫效果):
陸地風機場:
海上風機場:
代碼實現
我們可以看到,首頁的地球有三種視角狀態,世界地圖、中國地圖、城市地圖。點擊每個狀態相機就會轉到對應的位置。在這之前我們要先預先存一下對應的 center 和 eye 。
我們最好新建一個 data.js 文件,專門用來提供數據。
相關偽代碼如下:
// 記錄位置 var cameraLocations = { earth: { eye: [-73, 448, 2225], center: [0, 0, 0] }, china: { eye: [-91, 476, 916], center: [0, 0, 0] }, tsankiang: { eye: [35, 241, 593], center: [0, 0, 0] } }
好了,有了數據之后。我們接下來該監聽事件了。我們可以點擊按鈕,也可以點擊高亮區域(世界地圖只有按鈕可以點擊)進入到中國地圖視角。
我們可以這樣先獲取這兩個節點,然后對它們的點擊事件進行相同的處理。但是,我覺得這種方式可以進行優化,更換一種思考方式。
我們可以先將事件進行過濾,我們創建兩個數組,一個保存着類似 click、onEnter 這樣可以執行的事件,一個保存着所有可以觸發事件的節點。這樣可以有利於我們維護,也可以使結構更加清晰。
下圖,我們可以看到,如果當前節點沒有事件權限或者當前事件本身就沒有權限的話,就會被過濾掉。如果都可以正確返回,則執行對應的事件。
相關偽代碼如下:
// 權限事件 this.eventMap = { clickData: true, onEnter: true, onLeave: true } // 權限節點 this.nodeMap = { outline: true, outline2: true, earth: true, bubbles: true, circle: true } /** * 監聽事件 */ initMonitor() { var gv = this.gv var self = this
var evntFlow = function (e) { var event = e.kind var tag = e.data && e.data.getTag() // 檢查當前事件或者節點是否能夠被執行 if (!self.eventMap[event] && !self.nodeMap[tag]) return false self.nodeEvent(event, tag) } gv.mi(eventFlow) }
只要我們當前要執行的節點符合要求,我們就會把 event (當前執行的事件) 和 tag (節點標簽) 傳給執行函數 nodeEvent 執行。這樣就不會浪費資源去處理那些無效的事件或者節點了。
我們接下來來看看 nodeEvent 怎么處理吧!
相關偽代碼如下:
/** * 氣泡事件 * @param { string } event 當前事件 * @param { string } propertyName 當前節點標簽 */ bubblesEvent(event, propertyName) { var dm = this.dm var account = dm.getDataByTag('account') var currentNode = dm.getDataByTag(propertyName) var self = this var clickData = function() { // 執行清除動作 self.clearAction() } var onEnter = function() { // do something } var onLeave = function() { // do something } var allEvent = { clickData, onEnter, onLeave } allEvent[event] && allEvent[event]() }
可以看到,我們可以利用 propertyName(節點標簽) 字符串拼接組成一個方法名。比如當前拿到的節點標簽是 bubbles , this[`${ properName }Event`] 之后,拿到就是 this['bubblesEvent'] 這個方法。當然,這個方法是我們事先定義好的。
在具體的節點方法里面,我們創建了對應的事件函數。根據傳過來的 event 來判斷是否擁有對應的方法。如果有的話執行,否則返回 false 。這樣做的好處是:解耦、結構簡潔、出現問題能夠快速定位。
但是,如果我們仔細想想,我們點擊世界地圖和中國地圖的時候,功能都差不多!如果我們可以將他們合並的話,就會方便很多了!!我們來改造一下代碼。
相關偽代碼如下:
/** * 執行節點事件 */ nodeEvent(event, propertyName) { // 過濾是否有可以合並的事件 var filterEvents = function(propertyName) { var isCombine = false
var self = this
if (['earth', 'china'].includes(propertyName)) { self.changeCameraLocaltions(event, propertyName) isCombine = true } return !isCombine } var eventFun = this[`${propertyName}Event`] // 執行對應的節點事件 filterEvents(propertyName) && eventFun
&& eventFun(event, propertyName) }
我們事先判斷當前事件是否能合並,如果能的話返回 false ,不再執行下面的代碼,然后執行自己的函數。
這時候,我們就可以通過對應的節點標簽,從 data.js 的 cameraLocations 變量中取到對應的 center、eye 。
/** * 移動鏡頭動畫 * @param { object } config 坐標對象 */ moveCameraAnim(gv, config) { var eye = config.eye
var center = config.center
// 如果動畫已經存在,進行清空 if(globalAnim.moveCameraAnim) { globalAnim.moveCameraAnim.stop()
globalAnim.moveCameraAnim = null } var animConfig = { duration: 2e3 } globalAnim.moveCameraAnim = gv.moveCamera(eye, center, animConfig) } // 需要改變相機位置 changeCameraLocaltions(event, properName) { var config = cameraLocations[properName] // 移動相機 this.moveCameraAnim(this.gv, config) }
移動鏡頭動畫使用到了 gv 的 moveCamera 方法,該方法接受 3 個參數,eye (相機),center (目標),animConfig (動畫配置) 。然后我們把當前動畫返回給 globalAnim 的 moveCameraAnim 屬性,方便我們進行清理。
接下來,就是切換頁面了,這點需要非常小心謹慎。因為一旦沒有把某個屬性清除的話,將會導致內存泄漏等問題,性能會越來越慢。將會導致頁面卡死的情況!
所以我們需要一個專門用來清除數據模型的函數 clearAction 。我們應該把所有的動畫對象放到一個對象或者數組中。這樣方便切換頁面的時候清理掉。
相關偽代碼如下:
/** * 清除動作 */ clearAction(index) { var { dm, gv } = this var { g3d, d3d } = window allListener.mi3d && g3d.umi(allListener.mi3d) allListener.mi2d && gv.umi(allListener.mi2d) dm.removeScheduleTask(this.schedule) dm && dm.clear() d3d && d3d.clear() window.d3d = null window.dm = null for (var i in globalAnim) { globalAnim[i] && globalAnim[i].pause() globalAnim[i] = null } // 清除對應的 3D 圖紙 ht.Default.removeHTML(g3d) gv.addToDOM() ht.Default.xhrLoad(`displays/HT-project_2019/風電/${index}.json`, function (text) { let json = ht.Default.parse(text) gv.deserialize(json, function(json, dm2, gv2, datas) { if (json.title) document.title = json.title if (json.a['json.background']) { let bgJSON = json.a['json.background'] if (bgJSON.indexOf('scenes') === 0) { var bgG3d if (g3d) { bgG3d = g3d } else { bgG3d = new ht.graph3d.Graph3dView() } var bgG3dStyle = bgG3d.getView() bgG3dStyle.className = index === 1 ? '' : index === 3 ? 'land' : 'offshore' bgG3d.deserialize(bgJSON, function(json, dm3, gv3, datas) { init3d(dm3, gv3) }) bgG3d.addToDOM() gv.addToDOM(bgG3dStyle) } gv.handleScroll = function () {} } init2d(dm2, gv2) }) }) }
首先我們需要把 dm(數據模型) 和 gv(圖紙) 清除掉。還要注意:mi(監聽函數)、schedule(調度任務) 應該在 dm.clear() 之前 remove。所有的動畫進行 stop() 操作,然后將其值設為 null 。這里需要注意的是, 執行 stop 之后,會調用一次 finishFunc 回調函數。
當我們的 2D 圖紙里面包含 3D 背景的情況下,需要判斷是否已經存在了 3D 的實例,如果存在不需要再次創建。有興趣可以了解一下 webGL 的應用內存泄漏問題。
當進入兩個 3D 場景場景的時候,我們需要一個開場動畫,如開頭效果 gif 圖一樣。所以我們,需要把兩個開場動畫的 center 和 eye 都存到我們已經定義好的 cameraLocations 中。
// 記錄位置 var cameraLocations = { earth: { eye: [-73, 448, 2225], center: [0, 0, 0] }, china: { eye: [-91, 476, 916], center: [0, 0, 0] }, tsankiang: { eye: [35, 241, 593], center: [0, 0, 0] }, offshoreStart: { eye: [-849, 15390, -482], center: [0, 0, 0] }, landStart: { eye: [61, 27169, 55], center: [0, 0, 0] }, offshoreEnd: { eye: [-3912, 241, 834], center: [0, 0, 0] }, landEnd: { eye: [4096, 4122, -5798], center: [1261, 2680, -2181] } }
offshoreStart、offshoreEnd、landStart、landEnd 表示海上和陸上發電場的開始位置和結束位置。
我們需要判斷當前加載的是海上發電場還是陸上發電場。我們可以在加載對應圖紙的時候添加 className 。
我們在 clearAction 這個函數已經定義了 index 這個參數,如果點擊的是陸地發電場傳的就是數字3,如果是海上發電場的話,就是數字4。
比如我需要加載陸地發電場,那么就可以通過判斷 g3d.className = index === 3 ? 'land' : 'offshore' 來添加 className 。
然后在 init 里面進行初始化的判斷。
相關偽代碼如下:
init() { var className = g3d.getView().className // 執行單獨的事件 this.selfAnimStart(className)
this.initData() // 監聽事件 this.monitor() }
我們拿到對應的 className ,傳入相對應的類型並且執行對應的初始化事件,通過我們已經定義好的 moveCameraAnim 函數進行相機的動畫。
相關偽代碼如下:
/** * 不同風電場的開場動畫 */ selfAnimStart(type) { var gv = this.gv var { eye, center } = cameraLocations[`${type}End`] var config = { duration: 3000, eye, center, } this.moveCameraAnim(gv, config) }
總結
這個項目讓我們更加了解了風力發電。不管是風力發電場的地區優勢,還是風機的結構、運轉原理。
做完這個項目,自己得到了很多的成長和感悟。對於技術快速成長的一個好方法就是去不斷的摳細節。項目是一件藝術品,需要不斷對其進行打磨,要做到自己滿意為止。每個細微的點都會影響后面的性能。所以,我們應該以匠人的精神去做任何事。
當然,我也希望一些伙伴能夠勇於探索工業互聯網領域。我們能夠實現的遠遠不止於此。這需要發揮我們的想象力,為這個領域增添更多好玩的、實用的 demo。而且還能學到很多工業領域的知識。