基于 HTML5 + WebGL 的 3D 风力发电场可视化系统

前言

风能是一种开发中的洁净能源,它取之不尽、用之不竭。当然,建风力发电场首先应考虑气象条件和社会自然条件。近年来,我国海上和陆上风电发展迅猛。海水、陆地为我们的风力发电提供了很好地质保障。正是这些场地为我们的风力提供了用之不竭的能源。现在我们正在努力探索这些领域。

本文章实现了风力发电场的整体流程。能让大家能够看到一套完整风力发电预览体系。

需要注意的是,本次项目是使用 HightopoHT for Web产品来搭建的。

预览地址:hightopo.com/demo/wind-p…

大致流程

下面是整个项目的流程图。我们从首页可以进入到场区分布页面和集控页面。

场区分布页面又包括两个不同的 3D 场景,分别是陆地风机场和海上风机场。点击两个 3D 风机场最终都会进入到 3D 风机场景。

预览效果

首页:

1. 世界地图效果

2. 中国地图效果

2. 城市地图效果

集控中心页面(没有动画效果):

场区分布页面(没有动画效果)

陆地风机场:

海上风机场:

代码实现

我们可以看到,首页的地球有三种视角状态,世界地图、中国地图、城市地图。点击每个状态相机就会转到对应的位置。在这之前我们要先预先存一下对应的 centereye 。

我们最好新建一个 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.jscameraLocations变量中取到对应的 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 (动画配置) 。然后我们把当前动画返回给 globalAnimmoveCameraAnim属性,方便我们进行清理。

接下来,就是切换页面了,这点需要非常小心谨慎。因为一旦没有把某个属性清除的话,将会导致内存泄漏等问题,性能会越来越慢。将会导致页面卡死的情况!

所以我们需要一个专门用来清除数据模型的函数 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。而且还能学到很多工业领域的知识。

</hr>
原文链接:segmentfault.com

上一篇:base-widget
下一篇:Vue export import 导入导出的多种方式与区别

相关推荐

  • 防止对HTML5的浏览器历史popstate滚动

    Alex Malet de Carteret(https://stackoverflow.com/users/1415096/alexmaletdecarteret)提出了一个问题:Prevent b...

    2 年前
  • 防止HTML5视频被下载(右键保存)?

    Mureinikpython(https://stackoverflow.com/users/2422776/mureinik)提出了一个问题:Prevent HTML5 video from bei...

    2 年前
  • 选择移动Web HTML5框架[关闭]

    Andrew BarberSarfraz(https://stackoverflow.com/users/237838/andrewbarber)提出了一个问题:Choosing Mobile Web...

    2 年前
  • 调整HTML5画布大小以适应窗口

    shekhardevyn(https://stackoverflow.com/users/1087135/shekhar)提出了一个问题:Resize HTML5 canvas to fit wind...

    2 年前
  • 调整HTML5画布中的图像大小

    Telanor(https://stackoverflow.com/users/149986/telanor)提出了一个问题:Resizing an image in an HTML5 canvas,...

    2 年前
  • 详解HTML5游戏玩家流失原因

    对任何类型的电子游戏来说,玩家流失都是一个无法回避的问题。玩家为什么离开游戏?近日,HTML5游戏设定师纳森·洛维托(Nathan Lovato)在游戏开发者网站Gamasutra撰写文章,解读了玩家...

    4 年前
  • 设置自定义HTML5所需的字段验证消息

    G5WSumit Bijvani(https://stackoverflow.com/users/4752675/g5w)提出了一个问题:Set custom HTML5 required field...

    2 年前
  • 设置HTML5音频位置

    katspaugh(https://stackoverflow.com/users/352796/katspaugh)提出了一个问题:Setting HTML5 audio position,或许与您...

    2 年前
  • 让HTML5 localStorage键

    Brian Tompsett 汤莱恩Simone(https://stackoverflow.com/users/4370109/briantompsett%e6%b1%a4%e8%8e%b1%e6...

    2 年前
  • 触发HTML5表单验证

    Drew(https://stackoverflow.com/users/67521/drew)提出了一个问题:Triggering HTML5 Form Validation,或许与您遇到的问题类似...

    2 年前

官方社区

扫码加入 JavaScript 社区