【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) { //风杆数为0
        position.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知识,将其运用到了实践中,同时也发现自己对知识的理解其实还存在许多的不足,需要继续努力!

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/239114.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

vxe-table 右键菜单+权限控制(v3)

1.menu-config 是用于配置右键菜单的属性。通过 menu-config 属性&#xff0c;定义右键菜单的内容、显示方式和样式。 通过 menu-config 属性配置了右键菜单&#xff0c;其中的 options 属性定义了右键菜单的选项。用户在表格中右键点击时&#xff0c;将会弹出包含这些选项的自…

md笔记使用加自动备份整理

1、安装使用 TyporaGiteePicGo搭建图床&#xff08;解决使用Typora写的笔记上传csdn图片无法正常显示问题&#xff09; 2、修改主题 文件-》偏好设置-》外观-》打开主题文件夹 将css文件放到里面然后重启typora&#xff08;css文件可以参考参考链接&#xff09; 3、设置自动备份…

数据结构二维数组计算题,以行为主?以列为主?

1.假设以行序为主序存储二维数组Aarray[1..100,1..100]&#xff0c;设每个数据元素占2个存储单元&#xff0c;基地址为10&#xff0c;则LOC[5,5]&#xff08; &#xff09;。 A&#xff0e;808 B&#xff0e;818 C&#xff0e;1010 D&…

包装效果图渲染技巧:怎么用云渲染省钱、省时间

在今天这个市场竞争白热化的时代&#xff0c;一个产品的包装设计往往决定了它在架上是否能够脱颖而出。因此&#xff0c;品牌在推向市场前精心设计的包装效果图显得尤为重要。在这里&#xff0c;我们将探究包装效果图渲染的关键性、渲染技巧及云渲染技术如何在提升渲染品质与降…

Matlab 点云收缩L1中值(Weiszfeld算法)

文章目录 一、简介二、实现代码三、实现效果参考资料一、简介 对于之前的加权均值收缩方式,它存在一个很大的缺点,即容易受到噪声的影响,因此这里我们采用另一种统计学方案:L1中值。其形式如下所示: 其中 x i x_i

DICOM 文件中,VR,VL,Sequence,图像二进制的几个注意点

DICOM 文件中&#xff0c;VR&#xff0c;VL&#xff0c;Sequence&#xff0c;图像二进制的几个注意点 1. 传输语法 DICOM 文件的结构&#xff0c;在网上有很多的学习资料&#xff0c;这里只介绍些容易混淆的概念&#xff0c;作为回看笔记。 每个传输语法&#xff0c;起都是表…

openGauss学习笔记-154 openGauss 数据库运维-备份与恢复-闪回恢复

文章目录 openGauss学习笔记-154 openGauss 数据库运维-备份与恢复-闪回恢复154.1 闪回查询154.1.1 背景信息154.1.2 前提条件154.1.3 语法154.1.4 参数说明154.1.5 使用示例 154.2 闪回表154.2.1 背景信息154.2.2 前提条件154.2.3 语法154.2.4 使用示例 154.3 闪回DROP/TRUNCA…

【清晰明了】Jenkins邮件发送配置

自带邮件插件 首先要知道的是jenkins是自带邮件插件的&#xff0c;且不支持卸载。 下面开始配置自带邮件插件。 配置默认邮件管理员 系统管理 --> 系统配置&#xff0c;进行如下配置&#xff1a; 不配置管理员邮件地址报错如下 jakarta.mail.internet.AddressException:…

网络编程----select 模型总结

为什么要使用select模型&#xff1f; 答&#xff1a;解决基本C/S模型中&#xff0c;accept()、recv()、send()阻塞的问题 select模型与C/S模型的不同点 C/S模型中accept()会阻塞一直傻等socket来链接select模型只解决accept()傻等的问题&#xff0c;不解决recv(),send()执行…

大厂大数据面试题收录(1)

目录 1.java 中 object 类有哪些方法? 2.说一下和equals的区别&#xff1f; 3.为什么要重写 equals 和 hashcode()方法&#xff1f; 4.机器学习中&#xff0c;监督学习 和 无监督学习的区别是啥&#xff1f;&#xff1f; 5.kafka 组件熟悉吗,kafka 如何实现消息的有序的&a…

css 表示具有特定类或者其他属性的某种标签类型的元素

需求 通过 css 选择器获取某种标签&#xff08;如&#xff1a;div、input 等&#xff09;具有某个属性&#xff08;如&#xff1a;class、id 等&#xff09;的元素&#xff0c;从而修改其样式。 代码 通过 [标签].[属性] 的方式来获取 <div class"test">&l…

C++相关闲碎记录(8)

1、预定义的Function adapter 和 binder #include <iostream> #include <functional>int main() {auto plus10 std::bind(std::plus<int>(), std::placeholders::_1, 10);std::cout << "10: " << plus10(6) << std::endl…

基于FPGA的视频接口之高速IO(PCIE)

简介 相对于其他高速IO接口应用&#xff0c;PCIE协议有专门的的IP来进行操作&#xff0c;通过8对输入高速IO&#xff0c;以及输出高速IO&#xff0c;来实现PCIEX8功能。 原理框图 原理图 软件调用

Nginx首页修改及使用Nginx实现端口转发

按照我之前博客给的方法搭建好这样一个CTF靶场 但是呢它默认是在8000端口 如何直接访问IP地址或者域名就可以实现直接访问到靶场呢 我们需要将80端口的内容转发到8000&#xff0c;使用nginx实现端口转发功能 首先我们安装nginx&#xff1a; 安装工具和库 yum -y install gc…

《地理信息系统原理》笔记/期末复习资料(9. 网络地理信息系统)

目录 9. 网络地理信息系统 9.1. 概述 9.1.1. 网络GIS概念 9.1.2. 网络GIS体系结构 9.1.3. 网络GIS内容体系 9.2. 分布式网络GIS 9.2.1. 分布式网络GIS概念 9.2.2. 分布式主要技术 9.3. WebGIS 9.3.1. WebGIS概念 9.3.2. WebGIS分类与特点 9.3.3. WebGIS技术框架 9…

IDEA中的Postman!这款插件:免费,好用!

Postman是大家最常用的API调试工具&#xff0c;那么有没有一种方法可以不用手动写入接口到Postman&#xff0c;即可进行接口调试操作&#xff1f;今天给大家推荐一款IDEA插件&#xff1a;Apipost Helper&#xff0c;写完代码就可以调试接口并一键生成接口文档&#xff01;而且还…

电商早报 | 12月12日| 淘宝公布2023年度商品初选名单入围

淘宝公布2023年度商品初选名单&#xff1a;军大衣、酱香拿铁、熊猫周边入围 又一年临近收官&#xff0c;淘宝如期启动了“2023年度十大商品”评选。 12月11日&#xff0c;淘宝官方发布了初选入围名单&#xff0c;30件最具代表性的商品脱颖而出。据淘宝路边社介绍&#xff0c;…

系统架构设计师教程(三)信息系统基础知识

信息系统基础知识 3.1 信息系统概述3.1.1 信息系统的定义3.1.2 信息系统的发展3.1.3 信息系统的分类3.1.4 信息系统的生命周期3.1.5 信息系统建设原则3.1.6 信息系统开发方法 3.2 业务处理系统 (TPS)3.2.1 业务处理系统的概念3.2.2 业务处理系统的功能3.2.3 业务处理系统的特点…

互联网加竞赛 opencv 图像识别 指纹识别 - python

0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 基于机器视觉的指纹识别系统 &#x1f947;学长这里给一个题目综合评分(每项满分5分) 难度系数&#xff1a;3分工作量&#xff1a;3分创新点&#xff1a;4分 该项目较为新颖&#xff0c;适…

Pearson、Spearman 相关性分析使用

介绍 Pearson 积差相关系数衡量了两个定量变量之间的线性相关程度。 用来衡量两个数据集的线性相关程度&#xff0c;仅当一个变量的变化与另一个变量的比例变化相关时&#xff0c;关系才是线性的。 Spearman等级相关系数则衡量分级定序变量之间的相关程度。斯皮尔曼相关系数不…