基于 HTML5 + WebGL 的 3D 风力发电场
前言
风能是一种开发中的洁净能源,它取之不尽、用之不竭。当然,建风力发电场首先应考虑气象条件和社会自然条件。近年来,我国海上和陆上风电发展迅猛。海水、陆地为我们的风力发电提供了很好地质保障。正是这些场地为我们的风力提供了用之不竭的能源。现在我们正在努力探索这些领域。
本文章实现了风力发电场的整体流程。能让大家能够看到一套完整风力发电预览体系。
需要注意的是,本次项目是使用 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。而且还能学到很多工业领域的知识。
基于 HTML5 + WebGL 的 3D 风力发电场的更多相关文章
- 基于 HTML5 WebGL 的 3D 风机 Web 组态工业互联网应用
基于 HTML5 WebGL 的 3D 风机 Web 组态工业互联网应用 前言 在目前大数据时代背景之下,数据可视化的需求也变得越来越庞大,在数据可视化的背景之下,通过智能机器间的链接并最终将人机链接 ...
- 基于 HTML5 + WebGL 的 3D 可视化挖掘机
前言 在工业互联网以及物联网的影响下,人们对于机械的管理,机械的可视化,机械的操作可视化提出了更高的要求.如何在一个系统中完整的显示机械的运行情况,机械的运行轨迹,或者机械的机械动作显得尤为的重要,因 ...
- 基于 HTML5 + WebGL 实现 3D 可视化地铁系统
前言 工业互联网,物联网,可视化等名词在我们现在信息化的大背景下已经是耳熟能详,日常生活的交通,出行,吃穿等可能都可以用信息化的方式来为我们表达,在传统的可视化监控领域,一般都是基于 Web SCAD ...
- 基于 HTML5 + WebGL 实现 3D 挖掘机系统
前言 在工业互联网以及物联网的影响下,人们对于机械的管理,机械的可视化,机械的操作可视化提出了更高的要求.如何在一个系统中完整的显示机械的运行情况,机械的运行轨迹,或者机械的机械动作显得尤为的重要,因 ...
- 基于 HTML5 WebGL 的 3D 科幻风机
前言 许多世纪以来,风力机同水力机械一样,作为动力源替代人力.畜力,对生产力的发展发挥过重要作用.近代机电动力的广泛应用以及二十世纪50年代中东油田的发现,使风机发电机的发展缓慢下来. 70年代初期, ...
- 基于 HTML5 WebGL 构建 3D 智能数字化城市全景
前言 自 2011 年我国城镇化率首次突破 50% 以来,<新型城镇化发展规划>将智慧城市列为我国城市发展的三大目标之一,并提出到 2020 年,建成一批特色鲜明的智慧城市.截至现今,全国 ...
- 基于 HTML5 WebGL 的 3D 网络拓扑图
在数据量很大的2D 场景下,要找到具体的模型比较困难,并且只能显示出模型的的某一部分,显示也不够直观,这种时候能快速搭建出 3D 场景就有很大需求了.但是搭建 3D 应用场景又依赖于通过 3ds Ma ...
- 基于 HTML5 WebGL 的 3D 服务器与客户端的通信
这个例子的初衷是模拟服务器与客户端的通信,我把整个需求简化变成了今天的这个例子.3D 机房方面的模拟一般都是需要鹰眼来辅助的,这样找产品以及整个空间的概括会比较明确,在这个例子中我也加了,这篇文章就算 ...
- 基于 HTML5 WebGL 的 3D SCADA 主站系统
这个例子的初衷是模拟服务器与客户端的通信,我把整个需求简化变成了今天的这个例子.3D 的模拟一般需要鹰眼来辅助的,这样找产品以及整个空间的概括会比较明确,在这个例子中我也加了,这篇文章就算是我对这次项 ...
随机推荐
- 关于 FormData 和 URLSearchParams
一.FormData FormData 接口提供了一种表示表单数据的键值对的构造方式,经过它的数据可以使用 XMLHttpRequest.send() 方法送出,本接口和此方法都相当简单直接.如果送出 ...
- 使用原生JS封装一个动画函数
最近一直在忙项目,很少有时间回顾之前的知识,今天刚好要做一个轮播,因为对兼容性有一定的要求,使用了各种插件和库中的轮播,效果都不是很理想,一怒之下,使用原生JS封装了一个轮播组件,其中重要的功能就是一 ...
- 从零开始学习Kafka
简介 kafka是一个分布式消息队列.具有高性能.持久化.多副本备份.横向扩展能力.生产者往队列里写消息,消费者从队列里取消息进行业务逻辑.一般在架构设计中起到解耦.削峰.异步处理的作用. Kafka ...
- [转]Redis和Memcache区别,优缺点对比
1. Redis和Memcache都是将数据存放在内存中,都是内存数据库.不过memcache还可用于缓存其他东西,例如图片.视频等等. 2.Redis不仅仅支持简单的k/v类型的数据,同时还提供li ...
- 为什么阿里代码规约要求避免使用 Apache BeanUtils 进行属性复制
缘起 有一次开发过程中,刚好看到小伙伴在调用 set 方法,将数据库中查询出来的 Po 对象的属性拷贝到 Vo 对象中,类似这样: 可以看出,Po 和 Vo 两个类的字段绝大部分是一样的,我们一个个地 ...
- linux 从用户空间的 I/O 存取
刚刚描述的这些函数主要打算被设备驱动使用, 但它们也可从用户空间使用, 至少在 PC- 类 的计算机. GNU C 库在 <sys/io.h> 中定义它们. 下列条件应当应用来对于 inb ...
- 2019-3-1-获取-Nuget-版本号
title author date CreateTime categories 获取 Nuget 版本号 lindexi 2019-3-1 9:27:6 +0800 2019-02-25 15:51: ...
- mysql(8.0.16)安装及使用注意事项
1.安装地址:https://dev.mysql.com/downloads/mysql/ 2.在安装路径:D:\mysql\mysql-8.0.16-winx64(安装时的路径,可自己选择)下面新建 ...
- Teleport ultra/IDM(Internet Download Manager)
神器扒网站——teleport ultra IDM(Internet Download Manager) 在平时的开发或者学习的过程中,我们难免会看到一些让人心动的网站,于是自己想把它搞下来,自己手工 ...
- 移动端android上line-height不居中的问题的解决
废话不多话,直接上代码,如下: .btn { width: 1.5rem; max-width: 100px; text-align: center; height: .56rem; font-wei ...