【Canvas】记录一次从0到1绘制风场空间分布图的过程

2023-12-12 21:15

本文主要是介绍【Canvas】记录一次从0到1绘制风场空间分布图的过程,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

 📫 大家好,我是南木元元,热衷分享有趣实用的文章,希望大家多多支持,一起进步!

 🍅 个人主页:南木元元


目录

背景

前置知识

风场数据

绘制风场

准备工作

生成二维网格

获取格点风矢位置

风力等级

计算风矢坐标位置

旋转角度

绘制格点风矢

结语


背景

项目里遇到个需求,要求绘制出风场的空间分布图,一开始的想法是:这有什么难的,直接用echarts不就可以了。但当我看完设计图后,不得不感叹一句,好家伙,这还真有点复杂。最终要实现的效果如下图所示:

由于自定义的程度比较高,echarts肯定是不行的,思来想去,于是决定用canvas来从0到1自己实现,同时也可以顺带把canvas的知识巩固一下(温馨提示:全文可能有点长)。

前置知识

首先解释一下什么是风场空间分布图。

风场空间分布图:一种用于展示区域内风速和风向随空间位置变化的图表,这种图表通常以箭头或风矢的形式来表示风的方向和强度。这使我们可以直观地看到风速、风向的变化规律,它常常在气象学、风能工程等领域中被广泛使用。

本文采用风矢的形式来进行风场的可视化。在气象学中,风矢是用于表示风向和风速的符号图标。风矢由2部分组成,分别为风向杆与风羽。

  • 风向杆:表示风的方向
  • 风羽:分别用长划线和短划线或者与风三角组合的方式表示风速的大小。

了解了上面的概念后,我们下面就将使用Canvas来展示如何绘制风场的空间分布图。

风场数据

数据来源于用户自建的气象观测产品库,原始数据一般是netcdf或grib2的格式,需要后端将其解析成json格式的数据,解析后的数据格式大致如下:

{"yaxis": [10, 20, ...],"xaxis": [[39.4, 107.16], [37.286667, 107.72223], ...]"elementDataList": [{"name": "windS","subData": [{"level": "10","data": [8.9,10.3,...]},{"level": "20","data": [4.6,8.1,...]},...},{"name": "windD","subData": [{"level": "10","data": [59.8,65.0,...]},{"level": "20","data": [60.1,58.5,...]},...]}]
}

纵轴yaxis代表不同的高度层,横轴xaxis代表不同的经纬度坐标,要素列表elementDataList中目前只有一个风场要素(还有其它的气象要素如温度、降水量等,这里不展开),由于风场是矢量要素,同时具有大小和方向,所以这里将风的数据拆分成了windS风速列表和windD风向列表,列表中的值分别为每个高度层所对应的数据。

绘制风场

准备工作

定义一个绘制的类,做一些初始化的操作:属性设置,获取canvas的2d渲染上下文。

class drawWind {constructor(data){ //网格属性this.property = {OFFSET_X: 42, //x轴间隔OFFSET_Y: 20, //y轴间隔};//获取2d渲染上下文this.canvas2d = document.getElementById('canvas');this.ctx2d = this.canvas2d.getContext("2d");//后端返回数据this.data = data;this.xaxis = data.xaxis;this.yaxis = data.yaxis;//处理后的数据this.wind10S = [];this.wind10D = [];}//初始化数据init() {//处理一下返回的风速和风向数据,这里不详细展开,最终处理成网格点数据即可this.wind10S = this.handleData("wind10S");this.wind10D = this.handleData("wind10D");}}
}

还需要处理一下后端返回的数据,变成二维网格点数据,如下:

  • 风速数据

  •  风向数据

最终需要的数据就是网格点数据,即每个网格点都对应其风速和风向数据。

生成二维网格

生成风场需要构造二维网格,canvas绘制二维网格的思路很简单,先使用strokeRect设置一个矩形的边框,然后分别遍历横坐标和纵坐标列表,进行虚线的绘制。

draw2dMesh() {//生成矩形边框this.ctx2d.strokeRect(0, 0, this.canvas2d.width, this.canvas2d.height);//设置虚线样式this.ctx2d.lineWidth = 0.6;this.ctx2d.strokeStyle = "rgb(192, 192, 192)";this.ctx2d.beginPath();//遍历绘制纵向虚线for (let i = 1; i <= this.xaxis.length; i++) {this.ctx2d.setLineDash([5, 3]);this.ctx2d.moveTo(this.property.OFFSET_X * i, 0);this.ctx2d.lineTo(this.property.OFFSET_X * i, this.canvas2d.height);}//遍历绘制横向虚线for (let i = 1; i <= this.yaxis.length; i++) {this.ctx2d.setLineDash([5, 3]);this.ctx2d.moveTo(0, this.property.OFFSET_Y * i);this.ctx2d.lineTo(this.canvas2d.width, this.property.OFFSET_Y * i);}this.ctx2d.stroke();
}

绘制的网格如下:

获取格点风矢位置

每个网格点上的风矢形状是下面这样的。

所以在正式绘制前,我们还需要先计算每个风矢中的风杆和风羽数,得到每个点的位置。

风力等级

风力等级的计算公式:

可以参考这两篇文章:风力的级别换算和风力、等级、风速对照表和计算公式。

这里我们采用的是32个等级,可以预先定义好每个等级对应的风杆、长短划线以及风三角的数量。

this.Level = {"TRIANGLE": 20,"LONG": 4,"SHORT": 2,
},
this.Count = {"TRIANGLE": 10,"LONG": 2,"SHORT": 1,
},
//32个风力等级,每个数组中的四个值依次代表风杆数量、短划线数量、长划线数量、风三角数量
this.windLevel = [[0, 1, 0, 0],[1, 1, 0, 0],[1, 0, 1, 0],[1, 1, 1, 0],[1, 0, 2, 0],[1, 1, 2, 0],[1, 0, 3, 0],[1, 1, 3, 0],[1, 0, 4, 0],[1, 1, 4, 0],[1, 0, 0, 1],[1, 1, 0, 1],[1, 0, 1, 1],[1, 1, 1, 1],[1, 0, 2, 1],[1, 1, 2, 1],[1, 0, 3, 1],[1, 1, 3, 1],[1, 0, 4, 1],[1, 1, 4, 1],[1, 0, 0, 2],[1, 1, 0, 2],[1, 0, 1, 2],[1, 1, 1, 2],[1, 0, 2, 2],[1, 1, 2, 2],[1, 0, 3, 2],[1, 1, 3, 2],[1, 0, 4, 2],[1, 1, 4, 2],
],
//风矢属性:风杆长,长划线长,短划线长,划线间隔,风三角边长
this.featherProperty = {poleLength: 10,longLine: 10,shortLine: 5,lineSpace: 1,triangle: 2,
};

定义计算风力等级的方法。

// 根据风速计算风力等级,公式:v = 0.836 * b^(3/2) v:风速 b:风级
calWindLevel(speed) {let triangle = Math.floor(speed / this.Level.TRIANGLE);let long = Math.floor((speed - this.Level.TRIANGLE * triangle) / this.Level.LONG);let short = Math.floor((speed - this.Level.TRIANGLE * triangle - this.Level.LONG * long) / this.Level.SHORT);let idx = triangle * this.Count.TRIANGLE + long * this.Count.LONG + short * this.Count.SHORT;if (idx > 30) {idx = 30;}return idx;
}

计算风矢坐标位置

接下来需要计算得到每个网格点上的风矢中每个点的位置,这部分是整个流程中最为复杂的。

来说说我的思路:定义一个数组,用于存放当前格点的风矢位置,然后获取计算得到的风杆、长短划线等数量,从风杆顶部开始,依次放入风杆、风三角、长划线、短划线的位置。

//用于存放所有网格点风矢的位置
let position = [];
// 计算坐标位置:Num为当前网格点对应的风力等级,包含各种数量
getPointPosition(Num) {//用于存放当前格点风矢的位置let position = [];  let pole = Num[0]; //风杆数量let short = Num[1]; //短划线数量let long = Num[2]; //长划线数量let triangle = Num[3]; //风三角数量//当前顶点纵坐标位置从风杆顶部开始,这里为负是由于canvas坐标系y轴向下为正let yOffset = -this.featherProperty.poleLength;if (pole == 0) { //风杆数为0position.push(0, 0,this.featherProperty.shortLine, 0,this.featherProperty.shortLine, 0  //为了和风三角的三个一组一致,多加了一个点);//把当前格点的风羽位置放入数组position.push(position);return;}//放入风杆位置position.push(0, 0,0, -this.featherProperty.poleLength,    //向上为负0, -this.featherProperty.poleLength);//判断风三角是否为0,不为0向其中添加顶点if (triangle != 0) {for (let i = 0; i < triangle; ++i) {position.push(0, yOffset,0, yOffset + this.featherProperty.triangle,  //triangle为三角形边长this.featherProperty.longLine, yOffset + (this.featherProperty.triangle / 2));//每画完一个三角形,当前y坐标就要下移,由于canvas向下为正,所以即为加上三角形边长再加划线和三角形的间距yOffset = yOffset + this.featherProperty.triangle + this.featherProperty.lineSpace;}}//判断长划线是否为0,不为0向其中添加顶点if (long != 0) {for (let i = 0; i < long; ++i) {position.push(0, yOffset,this.featherProperty.longLine, yOffset,this.featherProperty.longLine, yOffset);yOffset = yOffset + this.featherProperty.lineSpace;}}//判断短划线是否为0,不为0向其中添加顶点if (short != 0) {for (let i = 0; i < short; ++i) {position.push(0, yOffset,this.featherProperty.shortLine, yOffset,this.featherProperty.shortLine, yOffset);yOffset = yOffset + this.featherProperty.lineSpace;}}//把当前格点的风羽位置放入数组position.push(position);
}

得到的风矢各个点的坐标数组大致如下:

旋转角度

风向决定了每个风矢在格点的旋转角度,由于旋转的时候以每个格点坐标为中心,所以记录一下每个格点的坐标位置。

// 获取旋转角度
getRotateData() {// 保存旋转中心点,即网格点坐标let center = [];// 保存风向let angle = [];for (let y = 0; y < this.yaxis.length; y++) {for (let x = 0; x < this.xaxis.length; x++) {// 获取风向let angle_point = this.angle[x + y * this.xaxis.length];// 计算网格点坐标let center = [(x + 1) * this.offsetX, (y + 1) * this.offsetY];center.push(center); angle.push([angle_point]);}}return {angle: angle,center: center,};
}

绘制格点风矢

做完上述操作后,终于可以开始绘制啦。绘制的思路:由于之前在计算位置的时候就统一3个坐标为一组(即画线只需两个坐标点,但我们也多加了一个重复的点,为了和画三角形统一),所以现在只需遍历顶点数组,来绘制每个格点的风矢就可以了。

// 绘制
drawFeather(data, color, size) {// 设置样式this.ctx.lineWidth = size;   this.ctx.strokeStyle = color;    this.ctx.fillStyle = color;// 让虚线变成实线条this.ctx.setLineDash([]);let position = data.position;let center = data.center;let angle = data.angle;// 遍历顶点数组,绘制每个格点的风矢for(let i = 0; i < center.length; i++) {for(let j = 0; j < position[i].length; j += 6) {// 保存画布 (canvas) 的所有状态this.ctx.save(); // 移动canvas原点到此处,使得当前格点为坐标为原点(0,0)this.ctx.translate(center[i][0],center[i][1]);   this.ctx.rotate(angle[i][0] * Math.PI/180);this.ctx.beginPath();// 之前处理后的数据都是三个为一组(包括线条),直接画线即可this.ctx.moveTo(position[i][j], position[i][j+1]);this.ctx.lineTo(position[i][j+2], position[i][j+3]);this.ctx.lineTo(position[i][j+4], position[i][j+5]);this.ctx.fill(); this.ctx.stroke();   // 恢复 canvas 状态this.ctx.restore();  }}
}

注意:在绘制每个格点风矢的时候,都需要save保存一下将当前canvas的状态入栈,绘制完后restore弹出恢复状态,为的是绘制下一个格点的风矢时都可以重新从canvas的坐标原点(0,0)开始平移到网格中心点,然后进行旋转操作。

最终的效果:

现在主要的部分我们都已经完成了,剩下的其实就是绘制横坐标和纵坐标,由于这部分比较简单,其实就是利用canvas绘制文字,这里就不再详细展开了。

结语

本文主要记录了一次自己使用canvas从0到1绘制风场空间分布图的经历,整个过程还是蛮复杂的,不过也刚好巩固了一下自己的canvas知识,将其运用到了实践中,同时也发现自己对知识的理解其实还存在许多的不足,需要继续努力!

🔥如果此文对你有帮助的话,欢迎💗关注、👍点赞、⭐收藏✍️评论支持一下博主~ 

这篇关于【Canvas】记录一次从0到1绘制风场空间分布图的过程的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/485956

相关文章

Oracle查询优化之高效实现仅查询前10条记录的方法与实践

《Oracle查询优化之高效实现仅查询前10条记录的方法与实践》:本文主要介绍Oracle查询优化之高效实现仅查询前10条记录的相关资料,包括使用ROWNUM、ROW_NUMBER()函数、FET... 目录1. 使用 ROWNUM 查询2. 使用 ROW_NUMBER() 函数3. 使用 FETCH FI

Python MySQL如何通过Binlog获取变更记录恢复数据

《PythonMySQL如何通过Binlog获取变更记录恢复数据》本文介绍了如何使用Python和pymysqlreplication库通过MySQL的二进制日志(Binlog)获取数据库的变更记录... 目录python mysql通过Binlog获取变更记录恢复数据1.安装pymysqlreplicat

SpringBoot 整合 Grizzly的过程

《SpringBoot整合Grizzly的过程》Grizzly是一个高性能的、异步的、非阻塞的HTTP服务器框架,它可以与SpringBoot一起提供比传统的Tomcat或Jet... 目录为什么选择 Grizzly?Spring Boot + Grizzly 整合的优势添加依赖自定义 Grizzly 作为

mysql-8.0.30压缩包版安装和配置MySQL环境过程

《mysql-8.0.30压缩包版安装和配置MySQL环境过程》该文章介绍了如何在Windows系统中下载、安装和配置MySQL数据库,包括下载地址、解压文件、创建和配置my.ini文件、设置环境变量... 目录压缩包安装配置下载配置环境变量下载和初始化总结压缩包安装配置下载下载地址:https://d

springboot整合gateway的详细过程

《springboot整合gateway的详细过程》本文介绍了如何配置和使用SpringCloudGateway构建一个API网关,通过实例代码介绍了springboot整合gateway的过程,需要... 目录1. 添加依赖2. 配置网关路由3. 启用Eureka客户端(可选)4. 创建主应用类5. 自定

使用Python绘制蛇年春节祝福艺术图

《使用Python绘制蛇年春节祝福艺术图》:本文主要介绍如何使用Python的Matplotlib库绘制一幅富有创意的“蛇年有福”艺术图,这幅图结合了数字,蛇形,花朵等装饰,需要的可以参考下... 目录1. 绘图的基本概念2. 准备工作3. 实现代码解析3.1 设置绘图画布3.2 绘制数字“2025”3.3

最新版IDEA配置 Tomcat的详细过程

《最新版IDEA配置Tomcat的详细过程》本文介绍如何在IDEA中配置Tomcat服务器,并创建Web项目,首先检查Tomcat是否安装完成,然后在IDEA中创建Web项目并添加Web结构,接着,... 目录配置tomcat第一步,先给项目添加Web结构查看端口号配置tomcat    先检查自己的to

使用Python绘制可爱的招财猫

《使用Python绘制可爱的招财猫》招财猫,也被称为“幸运猫”,是一种象征财富和好运的吉祥物,经常出现在亚洲文化的商店、餐厅和家庭中,今天,我将带你用Python和matplotlib库从零开始绘制一... 目录1. 为什么选择用 python 绘制?2. 绘图的基本概念3. 实现代码解析3.1 设置绘图画

SpringBoot集成SOL链的详细过程

《SpringBoot集成SOL链的详细过程》Solanaj是一个用于与Solana区块链交互的Java库,它为Java开发者提供了一套功能丰富的API,使得在Java环境中可以轻松构建与Solana... 目录一、什么是solanaj?二、Pom依赖三、主要类3.1 RpcClient3.2 Public

Android数据库Room的实际使用过程总结

《Android数据库Room的实际使用过程总结》这篇文章主要给大家介绍了关于Android数据库Room的实际使用过程,详细介绍了如何创建实体类、数据访问对象(DAO)和数据库抽象类,需要的朋友可以... 目录前言一、Room的基本使用1.项目配置2.创建实体类(Entity)3.创建数据访问对象(DAO