pyQt5结合Cartopy和matplotlib在界面中画micaps欧细数值预报
一、概述
这则帖子主要介绍了如何在pyqt5中利用Cartopy画地图,画一些想要的信息(本文画的是micaps的欧洲数值预报)。大概介绍本文涉及的知识点,供大家参考:
- pyqt5与matplotlib的结合。
- matplotlib和Cartopy的结合。
- micaps欧洲数值预报的读取。
- 数据的绘制和平滑(插值)
- 风杆过密的解决
- 界面 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