Based on the 3D HTML5 + WebGL wind farm

Foreword

   Wind power is a clean energy development, it is inexhaustible. Of course, to build wind farms should first consider the weather conditions and natural social conditions. In recent years, China's offshore and onshore wind power is developing rapidly. Sea, land provides a good geological guarantee for our wind power. It is these sites provide us with inexhaustible energy of the wind. Now we are trying to explore these areas.

This article achieve the overall flow of wind farms. It allows everyone to be able to see a complete preview of wind power generation system.

It should be noted that this project is the use of  Hightopo  of   HT for Web   products to build.

Preview Address: https://hightopo.com/demo/wind-power-station/

The general process

 Below is a flow chart of the entire project. We can enter the scene area distribution page and centralized control from the home page.

  Distribution page field area also includes two different 3D scenes, which are onshore wind and offshore wind airport airport. Click 3D two airports will eventually wind turbine into the 3D scene.

 

 

Preview

Home:

1. World Map effects

2. China to map effect

2. city map effect

 Centralized control center of the page (no animation):

Field area distribution page (no animation) :

Onshore wind airport: 

Offshore Wind Airport:

 

Code

 We can see that the home state of the Earth's perspective, there are three, world map, China map, city maps. Click on each camera will go to the state corresponding to the position. Prior to this, we must first deposit in advance about the corresponding center and the eye.

 We'd better create a  data.js document, designed to provide data.

 Related pseudo-code 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]
    }
}

 好了,有了数据之后。我们接下来该监听事件了。我们可以点击按钮,也可以点击高亮区域(世界地图只有按钮可以点击)进入到中国地图视角。

 我们可以这样先获取这两个节点,然后对它们的点击事件进行相同的处理。但是,我觉得这种方式可以进行优化,更换一种思考方式。

 我们可以先将事件进行过滤,我们创建两个数组,一个保存着类似 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) }

 移动镜头动画使用到了 gvmoveCamera 方法,该方法接受 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。而且还能学到很多工业领域的知识。

Guess you like

Origin www.cnblogs.com/htdaydayup/p/12121126.html