pyQt5结合Cartopy和matplotlib在界面中画micaps欧洲数值预报

一、概述

这则帖子主要介绍了如何在pyqt5中利用Cartopy画地图,画一些想要的信息(本文画的是micaps的欧洲数值预报)。大概介绍本文涉及的知识点,供大家参考:

  1. pyqt5与matplotlib的结合。
  2. matplotlib和Cartopy的结合。
  3. micaps欧洲数值预报的读取。
  4. 数据的绘制和平滑(插值)
  5. 风杆过密的解决
  6. 界面 matplotlib图像的交互,以及交互过程中数据图像和地图的刷新

先上一张最终效果图吸引一下大家:
在这里插入图片描述

二、界面绘制

这里利用pyqt5做了一个简单的界面,因为不是本次的重点,所以界面连半成品都算不上,只是大家可以完善,主要是介绍pyqt5和matplotlib的结合,下面是主函数main.py

from PyQt5.Qt import *
#自定义的一个类,用来做pyqt5和matplotlib的结合
# from My_Class import MyDataFigure
class Window(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("给个赞呗")
        self.showMaximized()#将主窗口最大化
        self.setup_ui()
    def setup_ui(self):
        #添加了三个控件  分别是左中右  中间控件到时候用来画图,左右两边摆各种按钮
        self.left_ql = QLabel(self)
        self.mid_ql = QLabel(self)#用来画图
        self.right_wt = QWidget(self)
        #为了方便区分,左边弄成黄色,右边弄成蓝色
        self.left_ql.setStyleSheet("background-color:yellow")
        self.right_wt.setStyleSheet("background-color:blue")
        #做了一个水平的动态布局,总共将窗口分成十份,中间占八份
        main_layout = QHBoxLayout()
        main_layout.addWidget(self.left_ql,1)
        main_layout.addWidget(self.mid_ql,8)
        main_layout.addWidget(self.right_wt,1)
        main_layout.setContentsMargins(0,0,0,0)#边框为0
        main_layout.setSpacing(0)#控件间隔为0
        self.setLayout(main_layout)
if __name__ == "__main__":
    import sys
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec_())

上面代码会跑出这样一个窗口
在这里插入图片描述
界面就简单做成这个样子了,重点不在于介绍怎么做界面,左右两边大家可以自己加控件。

二、pyqt5和matplotlib的结合

使pyqt5和matplotlib的结合,实质上是利用了matplotlib中的一个类FigureCanvasQTAgg,可嵌入到pyqt5的QLabel控件中展示出来,也可像matplotlib的画布一样画图,下面提供My_Class.py的代码:

#以下引入的包一个都不能少
import matplotlib
matplotlib.use("Qt5Agg")
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
import matplotlib.pyplot as plt
from matplotlib.figure import Figure

#需要用来嵌入界面的类,继承自FigureCanvasQTAgg
class MyDataFigure(FigureCanvasQTAgg):
    #构造函数
    #前面两个参数ql ,mid_ql是两个QLabel控件,不是必须的,是作者自行添加的,一个用来显示鼠标经纬度,一个用来使鼠标按下变换形状的,同理,大家需要传入什么参数,都可以写在__init__()中
    def __init__(self, ql ,mid_ql,width=100, height=100, dpi=30):
        self.figs = Figure(figsize=(width, height), dpi=dpi)
        super(MyDataFigure, self).__init__(self.figs)#前面的代码一条都不能少,类名要保持一致
        plt.rcParams['font.sans-serif'] = ['SimHei']#解决汉字乱码
        plt.rcParams['axes.unicode_minus'] = False#解决负号不显示
        print('创建成功')#下面就可以开始画图了

设计好画图类之后,需要在主界面实例化该类
首先引入类

from My_Class import MyDataFigure

然后实例化

#实例化自定义的类MyDataFigure,传入了两个控件参数,可以自己修改
        self.canvas_data = MyDataFigure(self.left_ql,self.mid_ql)
        #将类嵌入QLabel控件
        self.hboxlayout = QHBoxLayout(self.mid_ql)
        self.hboxlayout.addWidget(self.canvas_data)
        #设置边框为0
        self.hboxlayout.setContentsMargins(0,0,0,0)
        #调节画图的区域边界
        self.canvas_data.figs.subplots_adjust(left=0.05, right=1.3, top=0.9, bottom=0.5)

至此,主窗口main.py的代码量已全部完成,会在文末给出。

三、matplotlib和Cartopy的结合

这一部分相对简单一点,我们主要的目的是利用Cartopy在matplotlib上面画地图,之后所有的代码部分就在My_Class上面完成。
注意:运行仍是main.py

1.引入需要的包

相关包作用已经注释

#画图需要的包
import matplotlib.colors as colors#颜色
import cartopy.feature as cfeature#地图加载
import cartopy.crs as ccrs#投影方式
from cartopy.mpl.gridliner import LATITUDE_FORMATTER, LONGITUDE_FORMATTER#经纬度转化
import matplotlib.ticker as mticker#x,y轴刻度显示

2.画地图

基础设置分为1.设置显示地图范围;2.创建子图;3.把区域加载到子图中。

self.extent = [70,140,20,60]#显示东经70-140,北纬20-60的区域
self.axes_map = self.figs.add_axes([0.03, 0, 0.94, 0.95], projection=ccrs.PlateCarree())#创建子图,投影普通投影
self.axes_map.set_extent(self.extent,crs=ccrs.PlateCarree())#设置范围,投影普通投影
#利用cartopy自带地图画海
self.axes_map.add_feature(cfeature.OCEAN.with_scale('110m'))
#利用cartopy自带地图陆地
self.axes_map.add_feature(cfeature.LAND.with_scale('110m'))
# #利用cartopy自带地图河流
self.axes_map.add_feature(cfeature.RIVERS.with_scale('110m'))
#利用cartopy自带地图湖泊
self.axes_map.add_feature(cfeature.LAKES.with_scale('110m'))

这一部分代码在后期会修改,为了刷新地图,会放到一个方法内,下文会介绍,这里是方便大家理解。
加载的地图分辨率为110m,是为了降低对计算机速度的要求
在这里插入图片描述

3.画国界

这个不多做介绍了,在以前的文章中有介绍,直接上代码

#读取CN-border-La.dat文件
        with open('CN-border-La.dat') as src:
            context = src.read()
            blocks = [cnt for cnt in context.split('>') if len(cnt) > 0]
            self.borders = [np.fromstring(block, dtype=float, sep=' ') for block in blocks]
        # 画国界
        for line in self.borders:
            self.axes_map.plot(line[0::2], line[1::2], '-', color='gray',transform=ccrs.PlateCarree())

刷新地图时只需要执行画国界这部分就可以,不需要反复读取CN-border-La.dat。
注意:实际上作者是用了shp文件画的国界,为了交互,那样更快一些,但是毕竟是发表出来的,还是尽量不出错,就用了比较正式的,国界这种情况,一旦错了,就太敏感了
在这里插入图片描述

四、micaps欧洲数值预报的读取

1.read_mdfs.py

micaps欧洲数值预报是格点数据,这里只介绍一个,数据读取部分作者是在气象家园抄的别人的,在这里公布给大家read_mdfs.py

import struct
import datetime
import numpy as np

class MDFS_Grid:
    def __init__(self, filepath):
        f = open(filepath, 'rb')
        if f.read(4).decode() != 'mdfs':
            raise ValueError('Not valid mdfs data')
        self.datatype = struct.unpack('h', f.read(2))[0]
        self.model_name = f.read(20).decode('gbk').replace('\x00', '')
        self.element = f.read(50).decode('gbk').replace('\x00', '')
        self.data_dsc = f.read(30).decode('gbk').replace('\x00', '')
        self.level = struct.unpack('f', f.read(4))
        year, month, day, hour, tz = struct.unpack('5i', f.read(20))
        self.utc_time = datetime.datetime(year, month, day, hour) - datetime.timedelta(hours=tz)
        self.period = struct.unpack('i', f.read(4))
        start_lon, end_lon, lon_spacing, lon_number = struct.unpack('3fi', f.read(16))
        start_lat, end_lat, lat_spacing, lat_number = struct.unpack('3fi', f.read(16))
        lon_array = np.arange(start_lon, end_lon + lon_spacing, lon_spacing)
        lat_array = np.arange(start_lat, end_lat + lat_spacing, lat_spacing)
        isoline_start_value, isoline_end_value, isoline_space = struct.unpack('3f', f.read(12))
        f.seek(100, 1)
        block_num = lat_number * lon_number
        data = {
    
    }
        data['Lon'] = lon_array
        data['Lat'] = lat_array
        if self.datatype == 4:
            # Grid form
            grid = struct.unpack('{}f'.format(block_num), f.read(block_num * 4))
            grid_array = np.array(grid).reshape(lat_number, lon_number)
            data['Grid'] = grid_array
        elif self.datatype == 11:
            # Vector form
            norm = struct.unpack('{}f'.format(block_num), f.read(block_num * 4))
            angle = struct.unpack('{}f'.format(block_num), f.read(block_num * 4))
            norm_array = np.array(norm).reshape(lat_number, lon_number)
            angle_array = np.array(angle).reshape(lat_number, lon_number)
            # Convert stupid self-defined angle into correct direction angle
            corr_angle_array = 270 - angle_array
            corr_angle_array[corr_angle_array < 0] += 360
            data['Norm'] = norm_array
            data['Direction'] = corr_angle_array
        self.data = data

2.读取

看不懂没有关系,拿来直接用就可以了,接下来在My_Class.py中引入

from read_mdfs import MDFS_Grid#读取格点数据使用

简单应用

a = MDFS_Grid('ECMWF_HR_HGT_500_21010308.003')
self.lon = a.data['Lon']#经度
self.lat = a.data['Lat']#纬度
self.var = a.data['Grid']#数据

上述的代码是为了给大家介绍使用原理:及创建一个MDFS_Grid类,传入参数即文件路径(这里是21年1月3日08时起报的欧洲数值预报,第3小时预报,500hpa的高度场)

实际实现

第一步,在My_Class.py创建一个标识,用来判断所需数据是否成功读入

self.flg = {
    
    
            'HGT':False,
            'RH':False,
            'TMP':False,
            'UGRD':False,
            'VGRD':False
        }

当数据成功读入以后,相应部分改为Ture。
第二部分,要读取的不是一个文件,所以创建一个读取方法
这里没有读取经纬度,因为经纬度都一样,我们可以自己生产一个。减少程序冗余

#数据读取方法,第一个参数是路径,第二个是数据类型
    def read_data(self,filepath,data_type):
        if data_type == 'HGT':#高度
            a = MDFS_Grid(filepath)
            self.data_hgt = a.data['Grid']
            self.flg['HGT'] = True#读取成功
        if data_type == 'RH':#湿度
            a = MDFS_Grid(filepath)
            self.data_rh = a.data['Grid']
            self.flg['RH'] = True#读取成功
        if data_type == 'TMP':#温度
            a = MDFS_Grid(filepath)
            self.data_tmp = a.data['Grid']
            self.flg['TMP'] = True#读取成功
        if data_type == 'UGRD':#风
            a = MDFS_Grid(filepath)
            self.data_u = a.data['Grid']
            self.flg['UGRD'] = True#读取成功
        if data_type == 'VGRD':#风
            a = MDFS_Grid(filepath)
            self.data_v = a.data['Grid']
            self.flg['VGRD'] = True#读取成功

这里不是完整版,因为没有数据的筛选,平滑,后面会再次介绍修改。
第三部分,在__init__()中调用这个方法

self.read_data('ECMWF_HR_HGT_500_21010308.003','HGT')
self.read_data('ECMWF_HR_TMP_500_21010308.003','TMP')
self.read_data('ECMWF_HR_RH_500_21010308.003','RH')
self.read_data('ECMWF_HR_UGRD_500_21010308.003','UGRD')
self.read_data('ECMWF_HR_VGRD_500_21010308.003','VGRD')

这样所有需要的高度、温度、湿度、风就读取完成了。

五、数据的绘制

这里介绍一下湿度的绘制,高度和温度比较简单,大家在最终的代码里面自己看吧

1.创建经纬度网格

self.lon = np.arange(60.0,150.01,0.25)#经度
self.lat = np.arange(60.1,0,-0.25)#纬度
self.olon , self.olat= np.meshgrid(self.lon,self.lat)

这里注意,纬度是从大到小排列的,所以是负的排列,但是会影响后面的插值,后面介绍解决方法

2.画数据前准备

为了后面交互可以更快一点,我们需要把地图层和数据层分开,这样才能保证不会反复的刷新地图。解决方法是再创建一个子图,覆盖在地图层上方:

self.axes_data = self.figs.add_axes([0.03, 0, 0.94, 0.95],projection=ccrs.PlateCarree())

为了方便实时刷新数据,创建一个画数据的方法def drow(self):

    def drow(self):
        #图层设置
        self.axes_data.set_extent(self.extent,crs=ccrs.PlateCarree())
        #网格
        gl = self.axes_data.gridlines(crs=ccrs.PlateCarree(), draw_labels=True, linewidth=3, color='k', alpha=0.5,linestyle='--')
        #坐标轴设置
        gl.xformatter = LONGITUDE_FORMATTER ##坐标刻度转换为经纬度样式
        gl.yformatter = LATITUDE_FORMATTER
        gl.xlocator = mticker.FixedLocator(np.arange(self.extent[0], self.extent[1], 20))
        gl.ylocator = mticker.FixedLocator(np.arange(self.extent[2], self.extent[3], 10))
        gl.xlabel_style={
    
    'size':35}
        gl.ylabel_style={
    
    'size':35}
        #简化版画数据
        if self.flg['RH']:
            cf_rh = self.axes_data.contourf(self.olon , self.olat,self.data_rh,10,transform=ccrs.PlateCarree())

3.画数据

在__init__()中添加self.drow()就可以出图
在这里插入图片描述

3.数据筛选,平滑

为了让湿度更像云,我们先剔除湿度小于70的部分,并把大于100的部分变成100。
在数据读取阶段操作

		if data_type == 'RH':#湿度
            a = MDFS_Grid(filepath)
            self.data_rh = a.data['Grid']
            
            #筛选数据,小于70的部分剔除,大于100的归一化到100
            e = (self.data_rh <= 100)
            data_rh  = np.where(e,self.data_rh,100.0)
            e = (data_rh >= 70)
            self.data_rh  = np.where(e,self.data_rh,np.nan)

            self.flg['RH'] = True

在这里插入图片描述
可以发现,图像难看,那边就需要进行插值和平滑
第一步 导入库

from scipy.interpolate import interpolate#插值

第二步 插值经纬度从0.25到0.05,大家根据需求和计算机能力修改

		self.lon_scipy = np.arange(60.0,150.01,0.05)
        self.lat_scipy = np.arange(0,60.1,0.05)
        self.olon , self.olat= np.meshgrid(self.lon_scipy,self.lat_scipy)

这里纬度的生成用了正值,逐项递增,目的是为了后面的数据插值,不允许使用逐项递减的数列

第三步 数据插值

		if data_type == 'RH':#湿度
            a = MDFS_Grid(filepath)
            data_rh = a.data['Grid']
            #数据插值
            spline_rh = interpolate.RectBivariateSpline(self.lat, self.lon,data_rh,)
            data_rh = spline_rh(self.lat_scipy,self.lon_scipy)
            #数据筛选
            e = (data_rh <= 100)
            data_rh  = np.where(e,data_rh,100.0)
            e = (data_rh >= 70)
            self.data_rh  = np.where(e,data_rh,np.nan)
            #反转数据的Y轴,因为纬度已经被设置成从小到大
            self.data_rh = self.data_rh[::-1,:]
            self.flg['RH'] = True

在这里插入图片描述
画图时还增加了颜色变化,之前帖子有介绍。

		if self.flg['RH']:
            clevs = [70.,80.,85.,90.,95.,100.]#自定义颜色列表
            cdict = ['#d8d8d8','#b8b8b8','#989898','#707070','#505050']#自定义颜色列表
            my_cmap = colors.ListedColormap(cdict)#自定义色板
            norm = colors.BoundaryNorm(clevs,my_cmap.N)#归一化
            cf_rh = self.axes_data.contourf(self.lon_scipy,self.lat_scipy,self.data_rh,clevs,transform=ccrs.PlateCarree(),cmap=my_cmap,norm =norm)

高度和温度方法一样,最终代码会给出,风不做平滑,因为风本身就很密。

六、风杆过密的解决

绘制风

			if self.flg['UGRD'] and self.flg['VGRD']:
                self.axes_data.barbs(self.lon,self.lat, self.data_u,self.data_v,barbcolor=['b'],linewidth=3, length=10, barb_increments=dict(half=2, full=4, flag=20))

在这里插入图片描述

解决方法

		if self.flg['UGRD'] and self.flg['VGRD']:
                self.axes_data.barbs(self.lon[::10],self.lat[::10], self.data_u[::10,::10],self.data_v[::10,::10],barbcolor=['b'],linewidth=3, length=10, barb_increments=dict(half=2, full=4, flag=20))

实际上就是把数据间隔10取一个值再画,风数据是二维数组,要写两个。
在这里插入图片描述

七、地图数据交互

1.数据刷新,地图不变

该场景适用于一张一张的刷新数据,地图尺寸不变的情况,只需要在适当的位置添加下列代码:

self.axes_data.cla()  # 清除绘图区
self.drow()
self.figs.canvas.draw()  # 这里注意是画布重绘,self.figs.canvas
self.figs.canvas.flush_events()  # 画布刷新self.figs.canvas

2.按住鼠标拖动地图

这里有两个问题:1.matplotlib的鼠标响应事件。2.地图和数据的动态刷新
问题1:matplotlib的鼠标响应事件
在__init__()中添加鼠标响应事件:

#鼠标移动        
self.figs.canvas.mpl_connect('motion_notify_event',self.fun_motion_event)
 #鼠标按下
 self.figs.canvas.mpl_connect('button_press_event',self.fun_button_press)
 #鼠标释放
 self.figs.canvas.mpl_connect('button_release_event',self.fun_button_release)

下面是三个对应的函数:

    def fun_motion_event(self,event):
        if event.button == 1:#1代表左键一直按着
            str111 = 'X=' + str(event.xdata) + '\n' +  'Y=' + str(event.ydata)
            self.ql.setText(str111)#最开始构造函数时传入的QLabel控件,显示鼠标经纬度
            move_lon = int(event.xdata) - self.mouse_press_lon#计算经度移动量
            move_lat = int(event.ydata) - self.mouse_press_lat#计算纬度移动量
            self.lon_mid = self.lon_mid - move_lon#修改显示区域中心
            self.lat_mid = self.lat_mid - move_lat#修改显示区域中心
            self.extent = [self.lon_mid - self.lon_span, self.lon_mid + self.lon_span, self.lat_mid - self.lat_span, self.lat_mid+self.lat_span]#修改显示区域
            self.extent = self.check_extent(self.extent)#设置
            self.axes_map.cla()  # 清除绘图区
            self.axes_data.cla()  # 清除绘图区
            self.refresh_map()#刷新地图
            self.drow()#刷新数据
            self.figs.canvas.draw()  # 这里注意是画布重绘,self.figs.canvas
            self.figs.canvas.flush_events()  # 画布刷新self.figs.canvas
    def fun_button_press(self,event):
        if event.button == 1:
            str111 = 'X=' + str(event.xdata) + '\n' +  'Y=' + str(event.ydata)
            self.ql.setText(str111)#显示经纬度
            self.mid_ql.setCursor(Qt.SizeAllCursor)#按下时鼠标变形状
            self.mouse_press_lon = int(event.xdata)#记录鼠标按下时的经度
            self.mouse_press_lat = int(event.ydata)#记录鼠标按下时的纬度
    def fun_button_release(self,event):
        self.mid_ql.unsetCursor()#松开鼠标,重置形状

原理就是,记录鼠标左键按下时的经纬度,移动过程中实时记录移动的差值,反馈到画图范围,根据新的画图范围来重新画地图和数据。
为了更好的重新定义画图范围:

# self.extent = [70,140,20,60]#显示东经70-140,北纬20-60的区域       
        #中心经度70:40   7:4   [70,140,20,60]
        self.lon_mid = 105
        #经度跨度
        self.lon_span = 35
        #中心纬度
        self.lat_mid = 40
        #纬度跨度
        self.lat_span = 20
        self.extent = [self.lon_mid - self.lon_span, self.lon_mid + self.lon_span, self.lat_mid - self.lat_span, self.lat_mid+self.lat_span]

原理就是设置一个中心点(105,40),再设置上下左右间距,平移不改变间距,只改变中心点;缩放不改变中心点,改变间距。

为了实施刷新地图,创建地图刷新方法self.refresh_map()

		def refresh_map(self):
            self.axes_map.set_extent(self.extent,crs=ccrs.PlateCarree())
            # 画国界
            for line in self.borders:
                self.axes_map.plot(line[0::2], line[1::2], '-', color='gray',transform=ccrs.PlateCarree())
            #利用cartopy自带地图画海
            self.axes_map.add_feature(cfeature.OCEAN.with_scale('110m'))
            #利用cartopy自带地图陆地
            self.axes_map.add_feature(cfeature.LAND.with_scale('110m'))
            # #利用cartopy自带地图河流
            self.axes_map.add_feature(cfeature.RIVERS.with_scale('110m'))
            #利用cartopy自带地图湖泊
            self.axes_map.add_feature(cfeature.LAKES.with_scale('110m'))

为了防止移动的过程中画图范围超出允许范围导致报错,创建了画图范围检测函数def check_extent(self,extent):

def check_extent(self,extent):
        if extent[3] >= 90:
            extent[3] = 90
            extent[2] = 90 - 2*self.lat_span
        if extent[2] <= -90:
            extent[2] = -90
            extent[3] = -90 + 2*self.lat_span
        if extent[0] <= -180:
            extent[0] = -180
            extent[1] = -180 +2*self.lon_span
        if extent[1] >= 180:
            extent[1] = 180
            extent[0] = 180-2*self.lon_span
        return extent

3.鼠标滚轮缩放地图和数据

原理就是通过滚轮来改变画图区域的范围,等比例缩放,中心点为鼠标所在经纬度

#鼠标滚轮
        self.figs.canvas.mpl_connect('scroll_event',self.fun_scroll_event)
    def fun_scroll_event(self,event):
            if event.button == 'up':#[70,140,20,60]
                if  self.lon_span >=14 and self.lat_span >=8:
                    self.lon_span -=7
                    self.lat_span -= 4
                    self.lon_mid = int(event.xdata)
                    self.lat_mid = int(event.ydata)
                    self.extent = [self.lon_mid - self.lon_span, self.lon_mid + self.lon_span, self.lat_mid - self.lat_span, self.lat_mid+self.lat_span]
                    self.extent = self.check_extent(self.extent)
                    self.axes_map.cla()  # 清除绘图区
                    self.axes_data.cla()  # 清除绘图区
                    self.refresh_map()
                    self.drow()
                    self.figs.canvas.draw()  # 这里注意是画布重绘,self.figs.canvas
                    self.figs.canvas.flush_events()  # 画布刷新self.figs.canvas
            if event.button == 'down':  #35  20
                if  self.lon_span <=28 and self.lat_span <=16:
                    self.lon_span +=7
                    self.lat_span += 4
                    self.lon_mid = int(event.xdata)
                    self.lat_mid = int(event.ydata)
                    self.extent = [self.lon_mid - self.lon_span, self.lon_mid + self.lon_span, self.lat_mid - self.lat_span, self.lat_mid+self.lat_span]
                    self.extent = self.check_extent(self.extent)
                    self.axes_map.cla()  # 清除绘图区
                    self.axes_data.cla()  # 清除绘图区
                    self.refresh_map()
                    self.drow()
                    self.figs.canvas.draw()  # 这里注意是画布重绘,self.figs.canvas
                    self.figs.canvas.flush_events()  # 画布刷新self.figs.canvas

下图为利用鼠标滚轮放大后的图像
在这里插入图片描述

八、小结

需要测试数据的可以再评论区留下邮箱,本文就介绍了一个时次的资料,连续播放只需要再修改读取路径就好,这一块没有些代码,同时,读取地图实际使用的是shp,用的maskout.py

maskout.readshapefile('bou2_4l.shp',linewidth=3,ax=self.axes_map)

里面部分内容有帮助的请点个赞,下面是完整代码:
main.py

from PyQt5.Qt import *
#自定义的一个类,用来做pyqt5和matplotlib的结合
from My_Class import MyDataFigure

class Window(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("给个赞呗")
        self.showMaximized()#将主窗口最大化
        self.setup_ui()


    def setup_ui(self):
        #添加了三个控件  分别是左中右  中间控件到时候用来画图,左右两边摆各种按钮
        self.left_ql = QLabel(self)
        self.mid_ql = QLabel(self)#用来画图
        self.right_wt = QWidget(self)
        #为了方便区分,左边弄成黄色,右边弄成蓝色
        self.left_ql.setStyleSheet("background-color:yellow")
        self.right_wt.setStyleSheet("background-color:blue")

        #做了一个水平的动态布局,总共将窗口分成十份,中间占八份
        main_layout = QHBoxLayout()
        main_layout.addWidget(self.left_ql,1)
        main_layout.addWidget(self.mid_ql,8)
        main_layout.addWidget(self.right_wt,1)
        main_layout.setContentsMargins(0,0,0,0)#边框为0
        main_layout.setSpacing(0)#控件间隔为0
        self.setLayout(main_layout)

        #实例化自定义的类MyDataFigure,传入了两个控件参数,可以自己修改
        self.canvas_data = MyDataFigure(self.left_ql,self.mid_ql)
        #将类嵌入QLabel控件
        self.hboxlayout = QHBoxLayout(self.mid_ql)
        self.hboxlayout.addWidget(self.canvas_data)
        #设置边框为0
        self.hboxlayout.setContentsMargins(0,0,0,0)
        #调节画图的区域边界
        self.canvas_data.figs.subplots_adjust(left=0.05, right=1.3, top=0.9, bottom=0.5)



if __name__ == "__main__":
    import sys
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec_())

My_Class.py

#以下引入的包一个都不能少
import matplotlib
matplotlib.use("Qt5Agg")
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
import matplotlib.pyplot as plt
from matplotlib.figure import Figure
from PyQt5.Qt import *

#画图需要的包
import matplotlib.colors as colors#颜色
import cartopy.feature as cfeature#地图加载
import cartopy.crs as ccrs#投影方式
from cartopy.mpl.gridliner import LATITUDE_FORMATTER, LONGITUDE_FORMATTER#经纬度转化
import matplotlib.ticker as mticker#x,y轴刻度显示


import numpy as np

from read_mdfs import MDFS_Grid#读取格点数据使用
from scipy.interpolate import interpolate#插值


#需要用来嵌入界面的类,继承自FigureCanvasQTAgg
class MyDataFigure(FigureCanvasQTAgg):
    #构造函数
    #前面两个参数ql ,mid_ql是两个QLabel控件,不是必须的,是作者自行添加的,一个用来显示鼠标经纬度,一个用来使鼠标按下变换形状的,同理,大家需要传入什么参数,都可以写在__init__()中
    def __init__(self, ql ,mid_ql,width=100, height=100, dpi=30):
        self.figs = Figure(figsize=(width, height), dpi=dpi)
        super(MyDataFigure, self).__init__(self.figs)#前面的代码一条都不能少,类名要保持一致
        plt.rcParams['font.sans-serif'] = ['SimHei']#解决汉字乱码
        plt.rcParams['axes.unicode_minus'] = False#解决负号不显示
        print('创建成功')#下面就可以开始画图了
        self.ql = ql
        self.mid_ql = mid_ql


        # self.extent = [70,140,20,60]#显示东经70-140,北纬20-60的区域
        #中心经度70:40   7:4   [70,140,20,60]
        self.lon_mid = 105
        #经度跨度
        self.lon_span = 35
        #中心纬度
        self.lat_mid = 40
        #纬度跨度
        self.lat_span = 20
        self.extent = [self.lon_mid - self.lon_span, self.lon_mid + self.lon_span, self.lat_mid - self.lat_span, self.lat_mid+self.lat_span]


        self.axes_map = self.figs.add_axes([0.03, 0, 0.94, 0.95], projection=ccrs.PlateCarree())#创建子图,投影普通投影
        self.axes_map.set_extent(self.extent,crs=ccrs.PlateCarree())#设置范围,投影普通投影

        #读取CN-border-La.dat文件
        with open('CN-border-La.dat') as src:
            context = src.read()
            blocks = [cnt for cnt in context.split('>') if len(cnt) > 0]
            self.borders = [np.fromstring(block, dtype=float, sep=' ') for block in blocks]
        self.refresh_map()
        self.flg = {
    
    
            'HGT':False,
            'RH':False,
            'TMP':False,
            'UGRD':False,
            'VGRD':False
        }
        #鼠标移动
        self.figs.canvas.mpl_connect('motion_notify_event',self.fun_motion_event)
        #鼠标按下
        self.figs.canvas.mpl_connect('button_press_event',self.fun_button_press)
        #鼠标释放
        self.figs.canvas.mpl_connect('button_release_event',self.fun_button_release)
        #鼠标滚轮
        self.figs.canvas.mpl_connect('scroll_event',self.fun_scroll_event)
        #创建纬度网格
        self.lon = np.arange(60.0,150.01,0.25)
        self.lat = np.arange(0,60.1,0.25)
        self.lon_scipy = np.arange(60.0,150.01,0.05)
        self.lat_scipy = np.arange(0,60.1,0.05)
        self.olon , self.olat= np.meshgrid(self.lon_scipy,self.lat_scipy)
        #读取数据
        self.read_data(r'D:\project\readEC\21010308\ECMWF_HR_HGT_500_21010308.003','HGT')
        self.read_data(r'D:\project\readEC\21010308\ECMWF_HR_TMP_500_21010308.003','TMP')
        self.read_data(r'D:\project\readEC\21010308\ECMWF_HR_RH_500_21010308.003','RH')
        self.read_data(r'D:\project\readEC\21010308\ECMWF_HR_UGRD_500_21010308.003','UGRD')
        self.read_data(r'D:\project\readEC\21010308\ECMWF_HR_VGRD_500_21010308.003','VGRD')
        self.axes_data = self.figs.add_axes([0.03, 0, 0.94, 0.95],projection=ccrs.PlateCarree())
        self.drow()




    #数据读取方法,第一个参数是路径,第二个是数据类型
    def read_data(self,filepath,data_type):
        if data_type == 'HGT':
            a = MDFS_Grid(filepath)
            self.data_hgt = a.data['Grid']
            spline_hgt = interpolate.RectBivariateSpline(self.lat, self.lon,self.data_hgt,)
            self.data_hgt = spline_hgt(self.lat_scipy,self.lon_scipy)
            self.data_hgt = self.data_hgt[::-1,:]
            self.flg['HGT'] = True
        if data_type == 'RH':
            a = MDFS_Grid(filepath)
            data_rh = a.data['Grid']
            spline_rh = interpolate.RectBivariateSpline(self.lat, self.lon,data_rh,)
            data_rh = spline_rh(self.lat_scipy,self.lon_scipy)
            e = (data_rh <= 100)
            data_rh  = np.where(e,data_rh,100.0)
            e = (data_rh >= 70)
            self.data_rh  = np.where(e,data_rh,np.nan)
            self.data_rh = self.data_rh[::-1,:]
            self.flg['RH'] = True
        if data_type == 'TMP':
            a = MDFS_Grid(filepath)
            self.data_tmp = a.data['Grid']
            spline_tmp = interpolate.RectBivariateSpline(self.lat, self.lon,self.data_tmp,)
            self.data_tmp = spline_tmp(self.lat_scipy,self.lon_scipy)
            self.data_tmp = self.data_tmp[::-1,:]
            self.flg['TMP'] = True#'UGRD')VGRD
        if data_type == 'UGRD':
            a = MDFS_Grid(filepath)
            self.data_u = a.data['Grid']
            self.data_u = self.data_u[::-1,:]
            self.flg['UGRD'] = True
        if data_type == 'VGRD':
            a = MDFS_Grid(filepath)
            self.data_v = a.data['Grid']
            self.data_v = self.data_v[::-1,:]
            self.flg['VGRD'] = True
    def drow(self):
        self.axes_data.set_extent(self.extent,crs=ccrs.PlateCarree())
        gl = self.axes_data.gridlines(crs=ccrs.PlateCarree(), draw_labels=True, linewidth=3, color='k', alpha=0.5,linestyle='--')
        gl.xformatter = LONGITUDE_FORMATTER ##坐标刻度转换为经纬度样式
        gl.yformatter = LATITUDE_FORMATTER
        if int(self.lon_span/5)==0 or int(self.lat_span/5) == 0:
            gl.xlocator = mticker.FixedLocator(np.arange(self.extent[0], self.extent[1], 1))
            gl.ylocator = mticker.FixedLocator(np.arange(self.extent[2], self.extent[3], 1))
        else:
            gl.xlocator = mticker.FixedLocator(np.arange(self.extent[0], self.extent[1], int(self.lon_span/5)))
            gl.ylocator = mticker.FixedLocator(np.arange(self.extent[2], self.extent[3], int(self.lat_span/5)))


        gl.xlabel_style={
    
    'size':35}
        gl.ylabel_style={
    
    'size':35}

        if self.flg['HGT'] or self.flg['RH'] or self.flg['TMP'] or (self.flg['UGRD'] and self.flg['VGRD']):
            if self.flg['HGT']:
                ct_hgt = self.axes_data.contour(self.lon_scipy,self.lat_scipy,self.data_hgt,10,colors='black',linewidths=5)
                self.axes_data.clabel(ct_hgt, inline=True, fontsize=30,fmt='%d')
            if self.flg['TMP']:
                ct_tmp = self.axes_data.contour(self.lon_scipy,self.lat_scipy,self.data_tmp,10,colors='red',linewidths=4)
                self.axes_data.clabel(ct_tmp, inline=True, fontsize=30,fmt='%.1f')
            if self.flg['RH']:
                clevs = [70.,80.,85.,90.,95.,100.]#自定义颜色列表
                cdict = ['#d8d8d8','#b8b8b8','#989898','#707070','#505050']#自定义颜色列表
                my_cmap = colors.ListedColormap(cdict)#自定义色板
                norm = colors.BoundaryNorm(clevs,my_cmap.N)#归一化
                cf_rh = self.axes_data.contourf(self.lon_scipy,self.lat_scipy,self.data_rh,clevs,transform=ccrs.PlateCarree(),cmap=my_cmap,norm =norm)
            if self.flg['UGRD'] and self.flg['VGRD']:
                self.axes_data.barbs(self.lon[::10],self.lat[::10], self.data_u[::10,::10],self.data_v[::10,::10],barbcolor=['b'],linewidth=3, length=10, barb_increments=dict(half=2, full=4, flag=20))
    def fun_motion_event(self,event):
        if event.button == 1:#1代表左键一直按着
            str111 = 'X=' + str(event.xdata) + '\n' +  'Y=' + str(event.ydata)
            self.ql.setText(str111)#最开始构造函数时传入的QLabel控件,显示鼠标经纬度
            move_lon = int(event.xdata) - self.mouse_press_lon#计算经度移动量
            move_lat = int(event.ydata) - self.mouse_press_lat#计算纬度移动量
            self.lon_mid = self.lon_mid - move_lon#修改显示区域中心
            self.lat_mid = self.lat_mid - move_lat#修改显示区域中心
            self.extent = [self.lon_mid - self.lon_span, self.lon_mid + self.lon_span, self.lat_mid - self.lat_span, self.lat_mid+self.lat_span]#修改显示区域
            self.extent = self.check_extent(self.extent)#设置
            self.axes_map.cla()  # 清除绘图区
            self.axes_data.cla()  # 清除绘图区
            self.refresh_map()#刷新地图
            self.drow()#刷新数据
            self.figs.canvas.draw()  # 这里注意是画布重绘,self.figs.canvas
            self.figs.canvas.flush_events()  # 画布刷新self.figs.canvas
    def fun_button_press(self,event):
        if event.button == 1:
            str111 = 'X=' + str(event.xdata) + '\n' +  'Y=' + str(event.ydata)
            self.ql.setText(str111)#显示经纬度
            self.mid_ql.setCursor(Qt.SizeAllCursor)#按下时鼠标变形状
            self.mouse_press_lon = int(event.xdata)#记录鼠标按下时的经度
            self.mouse_press_lat = int(event.ydata)#记录鼠标按下时的纬度
    def fun_button_release(self,event):
        self.mid_ql.unsetCursor()#松开鼠标,重置形状
    def fun_scroll_event(self,event):
            if event.button == 'up':#[70,140,20,60]
                if  self.lon_span >=14 and self.lat_span >=8:
                    self.lon_span -=7
                    self.lat_span -= 4
                    self.lon_mid = int(event.xdata)
                    self.lat_mid = int(event.ydata)
                    self.extent = [self.lon_mid - self.lon_span, self.lon_mid + self.lon_span, self.lat_mid - self.lat_span, self.lat_mid+self.lat_span]
                    self.extent = self.check_extent(self.extent)
                    self.axes_map.cla()  # 清除绘图区
                    self.axes_data.cla()  # 清除绘图区
                    self.refresh_map()
                    self.drow()
                    self.figs.canvas.draw()  # 这里注意是画布重绘,self.figs.canvas
                    self.figs.canvas.flush_events()  # 画布刷新self.figs.canvas
            if event.button == 'down':  #35  20
                if  self.lon_span <=28 and self.lat_span <=16:
                    self.lon_span +=7
                    self.lat_span += 4
                    self.lon_mid = int(event.xdata)
                    self.lat_mid = int(event.ydata)
                    self.extent = [self.lon_mid - self.lon_span, self.lon_mid + self.lon_span, self.lat_mid - self.lat_span, self.lat_mid+self.lat_span]
                    self.extent = self.check_extent(self.extent)
                    self.axes_map.cla()  # 清除绘图区
                    self.axes_data.cla()  # 清除绘图区
                    self.refresh_map()
                    self.drow()
                    self.figs.canvas.draw()  # 这里注意是画布重绘,self.figs.canvas
                    self.figs.canvas.flush_events()  # 画布刷新self.figs.canvas
    def refresh_map(self):
            self.axes_map.set_extent(self.extent,crs=ccrs.PlateCarree())
            # 画国界
            for line in self.borders:
                self.axes_map.plot(line[0::2], line[1::2], '-', color='gray',transform=ccrs.PlateCarree())
            #利用cartopy自带地图画海
            self.axes_map.add_feature(cfeature.OCEAN.with_scale('110m'))
            #利用cartopy自带地图陆地
            self.axes_map.add_feature(cfeature.LAND.with_scale('110m'))
            # #利用cartopy自带地图河流
            self.axes_map.add_feature(cfeature.RIVERS.with_scale('110m'))
            #利用cartopy自带地图湖泊
            self.axes_map.add_feature(cfeature.LAKES.with_scale('110m'))
    def check_extent(self,extent):
        if extent[3] >= 90:
            extent[3] = 90
            extent[2] = 90 - 2*self.lat_span
        if extent[2] <= -90:
            extent[2] = -90
            extent[3] = -90 + 2*self.lat_span
        if extent[0] <= -180:
            extent[0] = -180
            extent[1] = -180 +2*self.lon_span
        if extent[1] >= 180:
            extent[1] = 180
            extent[0] = 180-2*self.lon_span
        return extent

maskout.py

from matplotlib.collections import LineCollection
from shapefile import Reader

def readshapefile(shapefile,drawbounds=True,zorder=None,
                      linewidth=0.5,color='k',ax=None,city = None
                      ):
    shf = Reader(shapefile, encoding='utf-8')
    coords = []
    shptype = shf.shapes()[0].shapeType
    for shprec in shf.shapeRecords():
        shp = shprec.shape
        if shptype != shp.shapeType:
            raise ValueError('readshapefile can only handle a single shape type per file')
        if shptype not in [1,3,5,8]:
            raise ValueError('readshapefile can only handle 2D shape types')
        verts = shp.points
        if shptype in [1,8]: # a Point or MultiPoint shape.
            lons, lats = list(zip(*verts))
                # if latitude is slightly greater than 90, truncate to 90
            lats = [max(min(lat, 90.0), -90.0) for lat in lats]
            if len(verts) > 1: # MultiPoint
                x,y = lons, lats
                coords.append(list(zip(x,y)))
            else: # single Point
                x,y = lons[0], lats[0]
                coords.append((x,y))
        else: # a Polyline or Polygon shape.
            parts = shp.parts.tolist()
            for indx1,indx2 in zip(parts,parts[1:]+[len(verts)]):
                lons, lats = list(zip(*verts[indx1:indx2]))
                    # if latitude is slightly greater than 90, truncate to 90
                lats = [max(min(lat, 90.0), -90.0) for lat in lats]
                x, y = lons, lats
                coords.append(list(zip(x,y)))
        # draw shape boundaries for polylines, polygons  using LineCollection.
    if shptype not in [1,8] and drawbounds:
            # get current axes instance (if none specified).
        ax = ax
        lines = LineCollection(coords,antialiaseds=(1,))
        lines.set_color(color)
        lines.set_linewidth(linewidth)
        if zorder is not None:
            lines.set_zorder(zorder)
        ax.add_collection(lines)

        if city != None:
            line = LineCollection(coords[4:5],antialiaseds=(1,))
            line.set_color('r')
            line.set_linewidth(2)
            ax.add_collection(line)

read_mdfs.py

import struct
import datetime
import numpy as np
class MDFS_Grid:
    def __init__(self, filepath):
        f = open(filepath, 'rb')
        if f.read(4).decode() != 'mdfs':
            raise ValueError('Not valid mdfs data')
        self.datatype = struct.unpack('h', f.read(2))[0]
        self.model_name = f.read(20).decode('gbk').replace('\x00', '')
        self.element = f.read(50).decode('gbk').replace('\x00', '')
        self.data_dsc = f.read(30).decode('gbk').replace('\x00', '')
        self.level = struct.unpack('f', f.read(4))
        year, month, day, hour, tz = struct.unpack('5i', f.read(20))
        self.utc_time = datetime.datetime(year, month, day, hour) - datetime.timedelta(hours=tz)
        self.period = struct.unpack('i', f.read(4))
        start_lon, end_lon, lon_spacing, lon_number = struct.unpack('3fi', f.read(16))
        start_lat, end_lat, lat_spacing, lat_number = struct.unpack('3fi', f.read(16))
        lon_array = np.arange(start_lon, end_lon + lon_spacing, lon_spacing)
        lat_array = np.arange(start_lat, end_lat + lat_spacing, lat_spacing)
        isoline_start_value, isoline_end_value, isoline_space = struct.unpack('3f', f.read(12))
        f.seek(100, 1)
        block_num = lat_number * lon_number
        data = {
    
    }
        data['Lon'] = lon_array
        data['Lat'] = lat_array
        if self.datatype == 4:
            # Grid form
            grid = struct.unpack('{}f'.format(block_num), f.read(block_num * 4))
            grid_array = np.array(grid).reshape(lat_number, lon_number)
            data['Grid'] = grid_array
        elif self.datatype == 11:
            # Vector form
            norm = struct.unpack('{}f'.format(block_num), f.read(block_num * 4))
            angle = struct.unpack('{}f'.format(block_num), f.read(block_num * 4))
            norm_array = np.array(norm).reshape(lat_number, lon_number)
            angle_array = np.array(angle).reshape(lat_number, lon_number)
            # Convert stupid self-defined angle into correct direction angle
            corr_angle_array = 270 - angle_array
            corr_angle_array[corr_angle_array < 0] += 360
            data['Norm'] = norm_array
            data['Direction'] = corr_angle_array
        self.data = data

猜你喜欢

转载自blog.csdn.net/weixin_42372313/article/details/116999761
今日推荐