3D Wind Farm Visualization System Based on HTML5 + WebGL

Foreword

Wind energy is a kind of clean energy under development, which is inexhaustible and inexhaustible. Of course, the construction of wind farms should first consider meteorological conditions and social natural conditions. In recent years, China's offshore and onshore wind power has developed rapidly. Seawater and land provide a good geological guarantee for our wind power generation. It is these sites that provide inexhaustible energy for our wind power. Now we are working hard to explore these areas.

This article implements the overall process of the wind farm. It allows everyone to see a complete wind power preview system.

It should be noted that this project was built using  Hightopo  's  HT for Web  product.

Preview address: hightopo.com/demo/wind-p ...

General process

The following is a flowchart of the entire project. We can enter the site distribution page and centralized control page from the home page.

The site distribution page includes two different 3D scenes, namely the land-wind airport and the sea-wind airport. Clicking on two 3D wind airports will eventually enter the 3D wind turbine scene.

Preview effect

Home:

1. World map effect

2. China map effect

2. City map effect

Central control center page (without animation effect):

Site distribution page (without animation effect) :

Landwind Airport:

Seawind Airport:

Code

We can see that the earth on the homepage has three perspective states: world map, China map, and city map. Click each status camera to go to the corresponding position. Before that, we have to pre-store the corresponding  center  and  eye.

We'd better create a new  data.js  file, specifically used to provide data.

The relevant pseudocode is as follows:

// 记录位置
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]
    }
}

Ok, after having the data. We should listen to the event next. We can click on the button or click on the highlighted area (only the button can be clicked on the world map) to enter the perspective of China map.

We can get these two nodes first, and then do the same to their click events. However, I think this method can be optimized and replaced with a way of thinking.

We can filter the events first. We create two arrays, one holds  events that can be executed like  click and onEnter , and one holds all nodes that can trigger events. This can help us maintain and make the structure clearer.

In the figure below, we can see that if the current node does not have event permissions or the current event itself has no permissions, it will be filtered out. If both can return correctly, the corresponding event is executed.

The relevant pseudocode is as follows:

// 权限事件
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)
}    

As long as the node we want to execute currently meets the requirements, we will pass the  event  (currently executed event)  and  tag (node ​​tag)  to the execution function  nodeEvent for  execution **. ** This will not waste resources to deal with those invalid events or nodes.

Let's take a look at   how to handle nodeEvent !

The relevant pseudocode is as follows:

/**
 * 气泡事件
 * @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]()
}

As you can see, we can use the  propertyName (node ​​label)  string to form a method name. For example, the current node label is  bubbles. After  this [`$ {properName} Event`]  ,  the method is  this ['bubblesEvent'] . Of course, this method is defined in advance.

In the specific node method, we created the corresponding event function. According to the passed  event  to determine whether there is a corresponding method. Execute if there is, otherwise return  false  . The advantages of this are: decoupling, simple structure, and quick positioning of problems.

However, if we think about it carefully, when we click on the world map and the China map, the functions are almost the same! If we can merge them, it will be much more convenient! ! Let's modify the code.

The relevant pseudocode is as follows:

/**
  * 执行节点事件
  */
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)
}

We determine in advance whether the current event can be merged, if it returns ** false **, no longer execute the following code, and then execute our own function.

At this time, we can pass the corresponding node label, from  data.js  the  cameraLocations  taken to the corresponding variable  center, eye.

The relevant pseudocode is as follows:

/**
 * 移动镜头动画
 * @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)
}

The moving camera animation uses  gv  's  moveCamera  method, which accepts 3 parameters, eye (camera) , ** center (target), and animConfig (animation configuration). ** Then we return the current animation to   the moveCameraAnim  property of globalAnim to  facilitate our cleanup.

Next, it's time to switch pages, which requires great care. Because once a certain attribute is not cleared, it will cause problems such as memory leaks, and performance will become slower and slower. It will cause the page to freeze!

So we need a function ** clearAction specifically for clearing the data model. ** We should put all the animation objects in one object or array. This is convenient for cleaning up when switching pages.

The relevant pseudocode is as follows:

/**
 * 清除动作
 */
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)
        })
    })
}

First, we need to remove  dm (data model)  and  gv (drawing)  . Also note: mi (monitor function) , Schedule (scheduled tasks)  should  dm.clear ()  before  the Remove . All animations perform a  stop ()  operation, and then set its value to  null  . It should be noted here that  after executing  stop , the finishFunc  callback function will be called once  .

When our 2D drawing contains a 3D background, we need to determine whether a 3D instance already exists, and if it does exist, we do not need to create it again. If you are interested, you can learn about the webGL application memory leak problem.

When entering two 3D scenes, we need an opening animation, like the gif at the beginning. Therefore, we need to save the center and eye of the two opening animations to the cameraLocations we have defined   .

// 记录位置
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, and landEnd  represent the start and end positions of offshore and onshore power plants **. **

We need to determine whether the current load is an offshore power plant or an onshore power plant. We can add className when loading the corresponding drawings  .

We  have defined the index parameter in the  clearAction function. If you click on the land power plant, it will be the number 3, if it is the offshore power plant, it will be the number 4.

For example, if I need to load a land farm, then I can  add  className by judging  g3d.className = index === 3? 'Land': 'offshore' .

Then make initialization judgment in init.

The relevant pseudocode is as follows:

init() {
    var className = g3d.getView().className
    
    // 执行单独的事件
    this.selfAnimStart(className)
    this.initData()

    // 监听事件
    this.monitor()
}

We get the corresponding ** className, ** pass in the corresponding type and execute the corresponding initialization event,  and the camera animation is performed by the moveCameraAnim function we have defined  .

The relevant pseudocode is as follows:

/**
  * 不同风电场的开场动画
  */
selfAnimStart(type) {
    var gv = this.gv
    var { eye, center } = cameraLocations[`${type}End`]
    var config = {
        duration: 3000,
        eye,
        center,
     }

     this.moveCameraAnim(gv, config)
}

to sum up


This project gave us a better understanding of wind power. Whether it is the regional advantages of the wind farm, or the structure and operation principle of the wind turbine.

After finishing this project, I have gained a lot of growth and insight. A good way for the rapid growth of technology is to constantly pick the details. The project is a work of art, which needs to be polished continuously until it is satisfactory. Each subtle point will affect the performance behind. Therefore, we should do anything with the spirit of a craftsman.

Of course, I also hope that some partners will have the courage to explore the field of industrial Internet. We can achieve much more than that. This requires our imagination to add more fun and practical demos to this field. And can also learn a lot of knowledge in industry.

Published 314 original articles · Like 138 · Visit 390,000+

Guess you like

Origin blog.csdn.net/u013161495/article/details/104276250