3D wind farm based on HTML5 + WebGL

Preface

Wind energy is a kind of clean energy in development, which is inexhaustible. Of course, meteorological conditions and social and natural conditions should be taken into account first. In recent years, China's offshore and onshore wind power has developed rapidly. Sea water 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 trying to explore these areas.

This paper realizes the whole process of wind farm. We can see a complete preview system of wind power generation.

It should be noted that this project is built with HT for Web product of Hightopo.

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

Approximate process

The following is the flow chart of the whole project. From the home page, we can enter the distribution page and centralized control page in the field area.

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

Preview effect

Home page:

1. World map effect

2. China map effect

2. Urban map effect

Central control center page (no animation):

Field distribution page (no animation):

Land wind Airport:

Offshore wind Airport:

code implementation

We can see that the earth on the home page has three view states: world map, China map and city map. Click on each state and the camera will go to the corresponding position. Before that, we need to save the corresponding center and eye in advance.

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

The relevant pseudo codes are as follows:

// Record location
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]
    }
}

Well, with the data. It's time for us to monitor the event next. We can click the button, or click the highlighted area (only the button of world map can be clicked) to enter the perspective of China map.

In this way, we can get the two nodes first, and then handle their click events the same. However, I think this way can be optimized and a new way of thinking can be changed.

We can filter the events first. We create two arrays. One is to save events that can be executed like click and onEnter, and the other is to save all nodes that can trigger events. This can help us to maintain and also make the structure clearer.

In the figure below, we can see that if the current node has no event permission or the current event itself has no permission, it will be filtered out. If all can be returned correctly, the corresponding event is executed.

The relevant pseudo codes are as follows:

// Permission events
this.eventMap = {
    clickData: true,
    onEnter: true,
    onLeave: true
}

// Permission node
this.nodeMap = {
    outline: true,
    outline2: true,
    earth: true,
    bubbles: true,
    circle: true
}

/**
  * Monitoring events
  */
initMonitor() {
    var gv = this.gv
   var self = this
    var evntFlow = function (e) {
        var event = e.kind
        var tag = e.data && e.data.getTag()

         // Check whether the current event or node can be executed
         if (!self.eventMap[event] && !self.nodeMap[tag]) return false

         self.nodeEvent(event, tag)
    }

    gv.mi(eventFlow)
}    

As long as the currently executing node meets the requirements, we will pass the event (currently executing event) and tag (node tag) to the executing function nodeEvent to execute * *. **In this way, resources will not be wasted to deal with invalid events or nodes.

Let's see what to do with nodeEvent!

The relevant pseudo codes are as follows:

/**
 * Bubble event
 * @param { string } event Current events
 * @param { string } propertyName Current node label
 */
bubblesEvent(event, propertyName) {
    var dm = this.dm
    var account = dm.getDataByTag('account')
    var currentNode = dm.getDataByTag(propertyName)
    var self = this

    var clickData = function() {
        // Perform clear action
        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 string splicing of propertyname (node label) to form a method name. For example, after the current node tag is bubbles, this [` ${propername} event `], this ['bubbleevent '] is the method. Of course, this method is defined in advance.

In the specific node method, we create the corresponding event function. Judge whether there is a corresponding method according to the passed event. If any, false will be returned. The advantages of this method are: decoupling, simple structure, and quick location of problems.

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

The relevant pseudo codes are as follows:

/**
  * Execute node event
  */
nodeEvent(event, propertyName) {
    // Filter whether there are events that can be merged
    var filterEvents = function(propertyName) {
        var isCombine = false     var self = this Copy code
        if (['earth', 'china'].includes(propertyName)) {
            self.changeCameraLocaltions(event, propertyName)
            isCombine = true
        }

        return !isCombine
    }
   var eventFun = this[`${propertyName}Event`]
   // Execute the corresponding node event
   filterEvents(propertyName)
   &&
   eventFun  &&
   eventFun(event, propertyName)
}

We determine in advance whether the current event can be merged, and if so, return * * false. * * will not execute the following code, and then execute its own function.

At this time, we can get the corresponding center and eye from the cameraLocations variable of data.js through the corresponding node label.

The relevant pseudo codes are as follows:

/**
 * Moving shot animation
 * @param { object } config Coordinate object
 */
moveCameraAnim(gv, config) {
  var eye = config.eye  var center = config.center  // Empty if animation already exists
   if(globalAnim.moveCameraAnim) {
      globalAnim.moveCameraAnim.stop()    globalAnim.moveCameraAnim = null
   }

   var animConfig = {
     duration: 2e3
   }

   globalAnim.moveCameraAnim = gv.moveCamera(eye, center, animConfig)
}

// Need to change camera position
changeCameraLocaltions(event, properName) {
    var config = cameraLocations[properName]

    // Mobile camera
    this.moveCameraAnim(this.gv, config)
}

gv's moveCamera method is used in moving shot animation, which takes three parameters, eye (camera), * * center (target), animConfig (animation configuration). **Then we return the current animation to the moveCameraAnim attribute of globalAnim, which is convenient for us to clean up.

The next step is to switch pages, which needs to be very careful. Once a property is not cleared, it will cause memory leakage and other problems, and the performance will be slower and slower. Will lead to page stuck!

So we need a special function * * clearAction to clear the data model. **We should put all animation objects into one object or array. This is convenient to clean up when switching pages.

The relevant pseudo codes are as follows:

/**
 * Scavenging action
 */
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
    }

    // Clear the corresponding 3D drawing
    ht.Default.removeHTML(g3d)

    gv.addToDOM()
    ht.Default.xhrLoad(`displays/HT-project_2019/Wind power/${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 (listening function), schedule (scheduling task) should be removed before dm.clear(). All animations perform a stop() operation, and then set its value to null. Note that after the stop is executed, the finishFunc callback function will be called once.

When our 2D drawing contains 3d background, we need to judge whether there is 3D instance already. If there is, we don't need to create it again. It's interesting to learn about the memory leakage of webGL applications.

When entering two 3D scenes, we need an opening animation, like the gif picture of the opening effect. So we need to save the center and eye of the two opening animations into the cameraLocations we have defined.

// Record location
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 indicate the start and end positions of offshore and onshore power plants * *. * *

We need to determine whether the current load is offshore or onshore. We can add className when loading the corresponding drawing.

We have defined the index parameter in the clearAction function. If you click land power plant, you will get the number 3. If you click sea power plant, you will get the number 4.

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

Then make initialization judgment in init.

The relevant pseudo codes are as follows:

init() {
    var className = g3d.getView().className
    
    // Execute individual events
    this.selfAnimStart(className)
    this.initData()

    // Monitoring events
    this.monitor()
}

We get the corresponding * * className and * * pass in the corresponding type and execute the corresponding initialization event. We can animate the camera through the moveCameraAnim function that we have defined.

The relevant pseudo codes are as follows:

/**
  * Opening animation of different wind farms
  */
selfAnimStart(type) {
    var gv = this.gv
    var { eye, center } = cameraLocations[`${type}End`]
    var config = {
        duration: 3000,
        eye,
        center,
     }

     this.moveCameraAnim(gv, config)
}

* * * summary

This project gives us a better understanding of wind power generation. Whether it is the regional advantage of wind farms, or the structure and operation principle of wind turbines.

After finishing this project, I got a lot of growth and insight. A good way for technology to grow rapidly is to constantly pick out details. The project is a work of art, which needs to be polished continuously until you are satisfied with it. Every tiny point affects the performance later. Therefore, we should do anything in the spirit of craftsman.

Of course, I also hope that some partners have the courage to explore the field of industrial Internet. We can achieve more than that. This requires our imagination to add more interesting and practical demo s to this field. And I can learn a lot about industry.

Tags: Front-end JSON Mobile Attribute

Posted on Sun, 12 Jan 2020 17:53:12 -0800 by soloslinger