pyQt5 combines Cartopy and matplotlib to draw micaps European numerical forecast in the interface

Posted by lulon83 on Wed, 09 Feb 2022 06:34:47 +0100

1, Overview

This post mainly introduces how to use Cartopy to draw maps and draw some desired information in pyqt5 (this article draws the European numerical forecast of micaps). Briefly introduce the knowledge points involved in this article for your reference:

  1. The combination of pyqt5 and matplotlib.
  2. The combination of matplotlib and Cartopy.
  3. Reading of European numerical forecast by micaps.
  4. Data rendering and smoothing (interpolation)
  5. Solution of over dense wind rod
  6. Interface matplotlib image interaction, as well as the refresh of data image and map in the interaction process

First, the last final effect drawing will attract you:

2, Interface drawing

Here we use pyqt5 to make a simple interface. Because it is not the focus of this time, the interface is not even a semi-finished product, but you can improve it. It mainly introduces the combination of pyqt5 and matplotlib. The following is the main function main py

from PyQt5.Qt import *
#A custom class used to combine pyqt5 and matplotlib
# from My_Class import MyDataFigure
class Window(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Give me a compliment")
        self.showMaximized()#Maximize main window
        self.setup_ui()
    def setup_ui(self):
        #Three controls are added, namely the left, middle and right middle controls, which will be used to draw pictures at that time, and various buttons are placed on the left and right sides
        self.left_ql = QLabel(self)
        self.mid_ql = QLabel(self)#Used to draw pictures
        self.right_wt = QWidget(self)
        #In order to make it easy to distinguish, make the left yellow and the right blue
        self.left_ql.setStyleSheet("background-color:yellow")
        self.right_wt.setStyleSheet("background-color:blue")
        #A horizontal dynamic layout is made, which divides the window into ten parts in total, with eight in the middle
        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)#Border is 0
        main_layout.setSpacing(0)#Control interval is 0
        self.setLayout(main_layout)
if __name__ == "__main__":
    import sys
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec_())

The above code will run out of such a window

The interface is simply made like this. The key point is not to introduce how to make the interface. You can add controls on the left and right sides.

2, Combination of pyqt5 and matplotlib

The combination of pyqt5 and matplotlib essentially makes use of a class in matplotlib, FigureCanvasQTAgg, which can be embedded in the QLabel control of pyqt5 and displayed. It can also draw like the canvas of matplotlib. My is provided below_ Class. Py Code:

#None of the packages introduced below can be less
import matplotlib
matplotlib.use("Qt5Agg")
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
import matplotlib.pyplot as plt
from matplotlib.figure import Figure

#The class needed to embed the interface is inherited from figurecanvasqtag
class MyDataFigure(FigureCanvasQTAgg):
    #Constructor
    #The first two parameters QL, mid_ QL is two QLabel controls. It is not necessary. It is added by the author. One is used to display the longitude and latitude of the mouse, and the other is used to make the mouse press to change the shape. Similarly, you can write any parameters you need to pass in__ init__ () medium
    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)#None of the preceding codes should be missing, and the class name should be consistent
        plt.rcParams['font.sans-serif'] = ['SimHei']#Solve Chinese character garbled code
        plt.rcParams['axes.unicode_minus'] = False#The negative sign is not displayed
        print('Created successfully')#Now you can start drawing

After designing the drawing class, you need to instantiate it in the main interface
First, introduce the class

from My_Class import MyDataFigure

Instantiate then

#Instantiate the custom class MyDataFigure and pass in two control parameters, which can be modified by yourself
        self.canvas_data = MyDataFigure(self.left_ql,self.mid_ql)
        #Embed class into QLabel control
        self.hboxlayout = QHBoxLayout(self.mid_ql)
        self.hboxlayout.addWidget(self.canvas_data)
        #Set border to 0
        self.hboxlayout.setContentsMargins(0,0,0,0)
        #Adjust the area boundary of the drawing
        self.canvas_data.figs.subplots_adjust(left=0.05, right=1.3, top=0.9, bottom=0.5)

At this point, the main window main The code quantity of Py has been completed and will be given at the end of the article.

3, Combination of matplotlib and Cartopy

This part is relatively simple. Our main purpose is to use Cartopy to draw a map on matplotlib, and then all the code parts are in my_ Complete above.
Note: the run is still main py

1. Introduce the required package

Related package functions have been commented

#Package required for drawing
import matplotlib.colors as colors#colour
import cartopy.feature as cfeature#Map loading
import cartopy.crs as ccrs#Projection mode
from cartopy.mpl.gridliner import LATITUDE_FORMATTER, LONGITUDE_FORMATTER#Longitude latitude conversion
import matplotlib.ticker as mticker#x. Y-axis scale display

2. Draw a map

The basic settings are divided into 1 Set the display map range; 2. Create subgraph; 3. Load the area into the subgraph.

self.extent = [70,140,20,60]#It shows the area of 70-140 east longitude and 20-60 north latitude
self.axes_map = self.figs.add_axes([0.03, 0, 0.94, 0.95], projection=ccrs.PlateCarree())#Create subgraph, normal projection
self.axes_map.set_extent(self.extent,crs=ccrs.PlateCarree())#Set range, normal projection
#Draw the sea with cartopy's own map
self.axes_map.add_feature(cfeature.OCEAN.with_scale('110m'))
#Using cartopy's own land map
self.axes_map.add_feature(cfeature.LAND.with_scale('110m'))
# #Using cartopy's own map of rivers
self.axes_map.add_feature(cfeature.RIVERS.with_scale('110m'))
#Using cartopy's own map of lakes
self.axes_map.add_feature(cfeature.LAKES.with_scale('110m'))

This part of the code will be modified later. In order to refresh the map, it will be put into a method. It will be introduced below for your understanding.
The resolution of the loaded map is 110m, which is to reduce the requirement of computer speed

3. Draw national boundaries

I don't need to introduce this. It was introduced in previous articles, and the code can be used directly

#Read CN border la Dat file
        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]
        # Draw borders
        for line in self.borders:
            self.axes_map.plot(line[0::2], line[1::2], '-', color='gray',transform=ccrs.PlateCarree())

When refreshing the map, you only need to draw the border, and you don't need to read cn-border-la repeatedly dat.
Note: in fact, the author uses the shp file to draw the national boundaries. In order to interact, it's faster, but after all, it's published. Try not to make mistakes, and use the more formal national boundaries. In this case, once it's wrong, it's too sensitive

4, Reading of European numerical forecast by micaps

1.read_mdfs.py

micaps European numerical forecast is grid point data. Only one is introduced here. The data reading part is copied by others in the meteorological home, which is published here for you to 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. Read

It doesn't matter if you don't understand it. Just use it directly. Next, in My_Class.py

from read_mdfs import MDFS_Grid#Reading grid data using

Simple application

a = MDFS_Grid('ECMWF_HR_HGT_500_21010308.003')
self.lon = a.data['Lon']#longitude
self.lat = a.data['Lat']#latitude
self.var = a.data['Grid']#data

The above code is to introduce the principle of use: and create an MDFS_Grid class, the input parameter is the file path (here is the European numerical forecast reported from 08:00 on January 3, 21, the 3rd hour forecast, and the altitude field of 500hpa)

Practical implementation

The first step, in My_Class.py creates an identifier to judge whether the required data is successfully read in

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

When the data is successfully read in, the corresponding part is changed to Ture.
In the second part, the file to be read is not a file, so create a read method
The longitude and latitude are not read here, because the longitude and latitude are the same. We can produce one by ourselves. Reduce program redundancy

#Data reading method, the first parameter is the path, and the second is the data type
    def read_data(self,filepath,data_type):
        if data_type == 'HGT':#height
            a = MDFS_Grid(filepath)
            self.data_hgt = a.data['Grid']
            self.flg['HGT'] = True#Read successful
        if data_type == 'RH':#humidity
            a = MDFS_Grid(filepath)
            self.data_rh = a.data['Grid']
            self.flg['RH'] = True#Read successful
        if data_type == 'TMP':#temperature
            a = MDFS_Grid(filepath)
            self.data_tmp = a.data['Grid']
            self.flg['TMP'] = True#Read successful
        if data_type == 'UGRD':#wind
            a = MDFS_Grid(filepath)
            self.data_u = a.data['Grid']
            self.flg['UGRD'] = True#Read successful
        if data_type == 'VGRD':#wind
            a = MDFS_Grid(filepath)
            self.data_v = a.data['Grid']
            self.flg['VGRD'] = True#Read successful

This is not the full version, because there is no data filtering and smoothing. The modification will be introduced again later.
The third part, in__ init__ () calling this method

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')

In this way, all the required height, temperature, humidity and wind are read.

5, Drawing of data

Here we introduce the drawing of humidity. The height and temperature are relatively simple. Let's see for ourselves in the final code

1. Create latitude and longitude grid

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

Note here that the latitude is arranged from large to small, so it is a negative arrangement, but it will affect the later interpolation. The solution will be introduced later

2. Preparation before drawing data

In order to make the later interaction faster, we need to separate the map layer from the data layer, so as to ensure that the map will not be refreshed repeatedly. The solution is to create another sub map and overlay it above the map layer:

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

In order to refresh data in real time, create a method of drawing data def drop (self):

    def drow(self):
        #Layer Settings
        self.axes_data.set_extent(self.extent,crs=ccrs.PlateCarree())
        #grid
        gl = self.axes_data.gridlines(crs=ccrs.PlateCarree(), draw_labels=True, linewidth=3, color='k', alpha=0.5,linestyle='--')
        #Coordinate axis setting
        gl.xformatter = LONGITUDE_FORMATTER ##Convert coordinate scale to latitude and longitude style
        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}
        #Simplified print data
        if self.flg['RH']:
            cf_rh = self.axes_data.contourf(self.olon , self.olat,self.data_rh,10,transform=ccrs.PlateCarree())

3. Draw data

In__ init__ Add self. In () Drop () can be used to plot

3. Data filtering and smoothing

In order to make the humidity more like a cloud, we first eliminate the part with humidity less than 70 and turn the part with humidity greater than 100 into 100.
Operation in data reading phase

		if data_type == 'RH':#humidity
            a = MDFS_Grid(filepath)
            self.data_rh = a.data['Grid']
            
            #Filter the data, reject the part less than 70, and normalize the part greater than 100 to 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


It can be found that the image is ugly, so interpolation and smoothing are needed there
Step 1 import and stock in

from scipy.interpolate import interpolate#interpolation

The second step is to interpolate the latitude and longitude from 0.25 to 0.05. You can modify it according to your needs and computer ability

		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)

Here, the latitude is generated with positive value and increasing item by item. The purpose is to interpolate the following data. It is not allowed to use decreasing sequence item by item

Step 3 data interpolation

		if data_type == 'RH':#humidity
            a = MDFS_Grid(filepath)
            data_rh = a.data['Grid']
            #Data interpolation
            spline_rh = interpolate.RectBivariateSpline(self.lat, self.lon,data_rh,)
            data_rh = spline_rh(self.lat_scipy,self.lon_scipy)
            #Data filtering
            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)
            #Invert the Y axis of the data because the latitude has been set from small to large
            self.data_rh = self.data_rh[::-1,:]
            self.flg['RH'] = True


Color changes have also been added when drawing, which was introduced in previous posts.

		if self.flg['RH']:
            clevs = [70.,80.,85.,90.,95.,100.]#Custom color list
            cdict = ['#d8d8d8','#b8b8b8','#989898','#707070','#505050']#Custom color list
            my_cmap = colors.ListedColormap(cdict)#Custom palette
            norm = colors.BoundaryNorm(clevs,my_cmap.N)#normalization
            cf_rh = self.axes_data.contourf(self.lon_scipy,self.lat_scipy,self.data_rh,clevs,transform=ccrs.PlateCarree(),cmap=my_cmap,norm =norm)

The height method is the same as the temperature method. The final code will give that the wind is not smooth because the wind itself is very dense.

6, Solution of over dense wind rod

Draw wind

			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))

resolvent

		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))

In fact, it is to take a value of data interval 10 and draw it again. The wind data is a two-dimensional array, and two should be written.

7, Map data interaction

1. The data is refreshed and the map remains unchanged

This scenario is suitable for refreshing data one by one, and the map size remains unchanged. You only need to add the following codes at the appropriate positions:

self.axes_data.cla()  # Clear drawing area
self.drow()
self.figs.canvas.draw()  # Notice here that the canvas is redrawn, self figs. canvas
self.figs.canvas.flush_events()  # Canvas refresh self figs. canvas

2. Hold down the mouse and drag the map

Here are two questions: 1 Mouse response event of Matplotlib. 2. Dynamic refresh of maps and data
Question 1: mouse response event of matplotlib
In__ init__ Add mouse response event in ():

#Mouse movement        
self.figs.canvas.mpl_connect('motion_notify_event',self.fun_motion_event)
 #Mouse down
 self.figs.canvas.mpl_connect('button_press_event',self.fun_button_press)
 #Mouse release
 self.figs.canvas.mpl_connect('button_release_event',self.fun_button_release)

Here are three corresponding functions:

    def fun_motion_event(self,event):
        if event.button == 1:#1 means the left key is pressed all the time
            str111 = 'X=' + str(event.xdata) + '\n' +  'Y=' + str(event.ydata)
            self.ql.setText(str111)#The QLabel control passed in at the beginning of the constructor displays the latitude and longitude of the mouse
            move_lon = int(event.xdata) - self.mouse_press_lon#Calculate longitude movement
            move_lat = int(event.ydata) - self.mouse_press_lat#Calculate latitude movement
            self.lon_mid = self.lon_mid - move_lon#Modify display area center
            self.lat_mid = self.lat_mid - move_lat#Modify display area center
            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]#Modify display area
            self.extent = self.check_extent(self.extent)#set up
            self.axes_map.cla()  # Clear drawing area
            self.axes_data.cla()  # Clear drawing area
            self.refresh_map()#Refresh map
            self.drow()#refresh data
            self.figs.canvas.draw()  # Notice here that the canvas is redrawn, self figs. canvas
            self.figs.canvas.flush_events()  # Canvas refresh 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)#Display latitude and longitude
            self.mid_ql.setCursor(Qt.SizeAllCursor)#The mouse changes shape when pressed
            self.mouse_press_lon = int(event.xdata)#Record the longitude when the mouse is pressed
            self.mouse_press_lat = int(event.ydata)#Record the latitude when the mouse is pressed
    def fun_button_release(self,event):
        self.mid_ql.unsetCursor()#Release the mouse to reset the shape

The principle is to record the longitude and latitude when the left mouse button is pressed, record the difference of movement in real time during the movement, feed back to the drawing range, and redraw the map and data according to the new drawing range.
To better redefine the drawing range:

# self.extent = [70,140,20,60]#It shows the area of 70-140 east longitude and 20-60 north latitude       
        #Central longitude 70:40 7:4 [70140,20,60]
        self.lon_mid = 105
        #Longitude span
        self.lon_span = 35
        #Central latitude
        self.lat_mid = 40
        #Latitude span
        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]

The principle is to set a center point (105, 40), and then set the upper, lower, left and right spacing. The translation does not change the spacing, but only the center point; Scaling does not change the center point, but changes the spacing.

To implement the refresh map, create the map refresh method self refresh_ map()

		def refresh_map(self):
            self.axes_map.set_extent(self.extent,crs=ccrs.PlateCarree())
            # Draw borders
            for line in self.borders:
                self.axes_map.plot(line[0::2], line[1::2], '-', color='gray',transform=ccrs.PlateCarree())
            #Draw the sea with cartopy's own map
            self.axes_map.add_feature(cfeature.OCEAN.with_scale('110m'))
            #Using cartopy's own land map
            self.axes_map.add_feature(cfeature.LAND.with_scale('110m'))
            # #Using cartopy's own map of rivers
            self.axes_map.add_feature(cfeature.RIVERS.with_scale('110m'))
            #Using cartopy's own map of lakes
            self.axes_map.add_feature(cfeature.LAKES.with_scale('110m'))

In order to prevent errors from being reported when the drawing range exceeds the allowable range in the process of moving, the drawing range detection function def check is created_ 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. Zoom the map and data with the mouse wheel

The principle is to change the range of the drawing area through the roller, scale it in equal proportion, and the center point is the longitude and latitude of the mouse

#Mouse wheel
        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()  # Clear drawing area
                    self.axes_data.cla()  # Clear drawing area
                    self.refresh_map()
                    self.drow()
                    self.figs.canvas.draw()  # Re draw the canvas here figs. canvas
                    self.figs.canvas.flush_events()  # Canvas refresh 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()  # Clear drawing area
                    self.axes_data.cla()  # Clear drawing area
                    self.refresh_map()
                    self.drow()
                    self.figs.canvas.draw()  # Notice here that the canvas is redrawn, self figs. canvas
                    self.figs.canvas.flush_events()  # Canvas refresh self figs. canvas

The following figure shows the enlarged image with the mouse wheel

8, Summary

If you need test data, you can leave a mailbox in the comment area. This article introduces the information of one time. For continuous playback, you only need to modify the reading path. There is no code in this block. At the same time, shp is actually used to read the map, using maskout py

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

If some of the contents are helpful, please give a praise. Here is the complete code:
main.py

from PyQt5.Qt import *
#A custom class used to combine pyqt5 and matplotlib
from My_Class import MyDataFigure

class Window(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Give me a compliment")
        self.showMaximized()#Maximize main window
        self.setup_ui()


    def setup_ui(self):
        #Three controls are added, namely the left, middle and right middle controls, which will be used to draw pictures at that time, and various buttons are placed on the left and right sides
        self.left_ql = QLabel(self)
        self.mid_ql = QLabel(self)#Used to draw pictures
        self.right_wt = QWidget(self)
        #In order to make it easy to distinguish, make the left yellow and the right blue
        self.left_ql.setStyleSheet("background-color:yellow")
        self.right_wt.setStyleSheet("background-color:blue")

        #A horizontal dynamic layout is made, which divides the window into ten parts in total, with eight in the middle
        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)#Border is 0
        main_layout.setSpacing(0)#Control interval is 0
        self.setLayout(main_layout)

        #Instantiate the custom class MyDataFigure and pass in two control parameters, which can be modified by yourself
        self.canvas_data = MyDataFigure(self.left_ql,self.mid_ql)
        #Embed class into QLabel control
        self.hboxlayout = QHBoxLayout(self.mid_ql)
        self.hboxlayout.addWidget(self.canvas_data)
        #Set border to 0
        self.hboxlayout.setContentsMargins(0,0,0,0)
        #Adjust the area boundary of the drawing
        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

#None of the packages introduced below can be less
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 *

#Package required for drawing
import matplotlib.colors as colors#colour
import cartopy.feature as cfeature#Map loading
import cartopy.crs as ccrs#Projection mode
from cartopy.mpl.gridliner import LATITUDE_FORMATTER, LONGITUDE_FORMATTER#Longitude latitude conversion
import matplotlib.ticker as mticker#x. Y-axis scale display


import numpy as np

from read_mdfs import MDFS_Grid#Reading grid data using
from scipy.interpolate import interpolate#interpolation


#The class needed to embed the interface is inherited from figurecanvasqtag
class MyDataFigure(FigureCanvasQTAgg):
    #Constructor
    #The first two parameters QL, mid_ QL is two QLabel controls. It is not necessary. It is added by the author. One is used to display the longitude and latitude of the mouse, and the other is used to make the mouse press to change the shape. Similarly, you can write any parameters you need to pass in__ init__ () medium
    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)#None of the preceding codes should be missing, and the class name should be consistent
        plt.rcParams['font.sans-serif'] = ['SimHei']#Solve Chinese character garbled code
        plt.rcParams['axes.unicode_minus'] = False#The negative sign is not displayed
        print('Created successfully')#Now you can start drawing
        self.ql = ql
        self.mid_ql = mid_ql


        # self.extent = [70,140,20,60]#It shows the area of 70-140 east longitude and 20-60 north latitude
        #Central longitude 70:40 7:4 [70140,20,60]
        self.lon_mid = 105
        #Longitude span
        self.lon_span = 35
        #Central latitude
        self.lat_mid = 40
        #Latitude span
        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())#Create subgraph, normal projection
        self.axes_map.set_extent(self.extent,crs=ccrs.PlateCarree())#Set range, normal projection

        #Read CN border la Dat file
        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
        }
        #Mouse movement
        self.figs.canvas.mpl_connect('motion_notify_event',self.fun_motion_event)
        #Mouse down
        self.figs.canvas.mpl_connect('button_press_event',self.fun_button_press)
        #Mouse release
        self.figs.canvas.mpl_connect('button_release_event',self.fun_button_release)
        #Mouse wheel
        self.figs.canvas.mpl_connect('scroll_event',self.fun_scroll_event)
        #Create latitude grid
        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)
        #Read data
        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()




    #Data reading method, the first parameter is the path, and the second is the data type
    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 ##Convert coordinate scale to latitude and longitude style
        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.]#Custom color list
                cdict = ['#d8d8d8','#b8b8b8','#989898','#707070','#505050']#Custom color list
                my_cmap = colors.ListedColormap(cdict)#Custom palette
                norm = colors.BoundaryNorm(clevs,my_cmap.N)#normalization
                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 means the left key is pressed all the time
            str111 = 'X=' + str(event.xdata) + '\n' +  'Y=' + str(event.ydata)
            self.ql.setText(str111)#The QLabel control passed in at the beginning of the constructor displays the latitude and longitude of the mouse
            move_lon = int(event.xdata) - self.mouse_press_lon#Calculate longitude movement
            move_lat = int(event.ydata) - self.mouse_press_lat#Calculate latitude movement
            self.lon_mid = self.lon_mid - move_lon#Display center modification
            self.lat_mid = self.lat_mid - move_lat#Modify display area center
            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]#Modify display area
            self.extent = self.check_extent(self.extent)#set up
            self.axes_map.cla()  # Clear drawing area
            self.axes_data.cla()  # Clear drawing area
            self.refresh_map()#Refresh map
            self.drow()#refresh data
            self.figs.canvas.draw()  # Notice here that the canvas is redrawn, self figs. canvas
            self.figs.canvas.flush_events()  # Canvas refresh 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)#Display latitude and longitude
            self.mid_ql.setCursor(Qt.SizeAllCursor)#The mouse changes shape when pressed
            self.mouse_press_lon = int(event.xdata)#Record the longitude when the mouse is pressed
            self.mouse_press_lat = int(event.ydata)#Record the latitude when the mouse is pressed
    def fun_button_release(self,event):
        self.mid_ql.unsetCursor()#Release the mouse to reset the shape
    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()  # Clear drawing area
                    self.axes_data.cla()  # Clear drawing area
                    self.refresh_map()
                    self.drow()
                    self.figs.canvas.draw()  # Notice here that the canvas is redrawn, self figs. canvas
                    self.figs.canvas.flush_events()  # Canvas refresh 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()  # Clear drawing area
                    self.axes_data.cla()  # Clear drawing area
                    self.refresh_map()
                    self.drow()
                    self.figs.canvas.draw()  # Notice here that the canvas is redrawn, self figs. canvas
                    self.figs.canvas.flush_events()  # Canvas refresh self figs. canvas
    def refresh_map(self):
            self.axes_map.set_extent(self.extent,crs=ccrs.PlateCarree())
            # Draw borders
            for line in self.borders:
                self.axes_map.plot(line[0::2], line[1::2], '-', color='gray',transform=ccrs.PlateCarree())
            #Draw the sea with cartopy's own map
            self.axes_map.add_feature(cfeature.OCEAN.with_scale('110m'))
            #Using cartopy's own land map
            self.axes_map.add_feature(cfeature.LAND.with_scale('110m'))
            # #Using cartopy's own map of rivers
            self.axes_map.add_feature(cfeature.RIVERS.with_scale('110m'))
            #Using cartopy's own map of lakes
            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

Topics: GUI