• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Open-MSS / MSS / 23903367204

02 Apr 2026 01:41PM UTC coverage: 69.802% (+0.09%) from 69.714%
23903367204

Pull #3060

github

web-flow
Merge df9c907d7 into 4a2e76e8a
Pull Request #3060: replaced QtCore.QCoreApplication.processEvents()

406 of 505 new or added lines in 6 files covered. (80.4%)

2 existing lines in 1 file now uncovered.

14493 of 20763 relevant lines covered (69.8%)

2.09 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

82.61
/mslib/msui/viewplotter.py
1
# -*- coding: utf-8 -*-
2
"""
3

4
    mslib.msui.viewplotter
5
    ~~~~~~~~~~~~~~~~~~~~~~~
6

7
    Definitions of Matplotlib widgets for Qt Designer.
8

9
    This file is part of MSS.
10

11
    :copyright: Copyright 2008-2014 Deutsches Zentrum fuer Luft- und Raumfahrt e.V.
12
    :copyright: Copyright 2011-2014 Marc Rautenhaus (mr)
13
    :copyright: Copyright 2016-2026 by the MSS team, see AUTHORS.
14
    :license: APACHE-2.0, see LICENSE for details.
15

16
    Licensed under the Apache License, Version 2.0 (the "License");
17
    you may not use this file except in compliance with the License.
18
    You may obtain a copy of the License at
19

20
       http://www.apache.org/licenses/LICENSE-2.0
21

22
    Unless required by applicable law or agreed to in writing, software
23
    distributed under the License is distributed on an "AS IS" BASIS,
24
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
25
    See the License for the specific language governing permissions and
26
    limitations under the License.
27
"""
28

29
# Parts of the code have been adapted from Chapter 6 of Sandro Tosi,
30
# 'Matplotlib for Python Developers'.
31

32

33
import logging
3✔
34
from datetime import datetime
3✔
35

36
import numpy as np
3✔
37
from matplotlib import figure
3✔
38
from matplotlib.lines import Line2D
3✔
39
from metpy.units import units
3✔
40
from mslib.msui import mpl_map
3✔
41
from mslib.utils import thermolib
3✔
42
from mslib.utils.config import config_loader, load_settings_qsettings, save_settings_qsettings
3✔
43
from mslib.utils.loggerdef import configure_mpl_logger
3✔
44

45

46
PIL_IMAGE_ORIGIN = "upper"
3✔
47
LAST_SAVE_DIRECTORY = config_loader(dataset="data_dir")
3✔
48

49
_DEFAULT_SETTINGS_TOPVIEW = {
3✔
50
    "draw_graticule": True,
51
    "draw_coastlines": True,
52
    "fill_waterbodies": True,
53
    "fill_continents": True,
54
    "draw_flighttrack": True,
55
    "draw_marker": True,
56
    "label_flighttrack": True,
57
    "tov_plot_title_size": "default",
58
    "tov_axes_label_size": "default",
59
    "colour_water": ((153 / 255.), (255 / 255.), (255 / 255.), (255 / 255.)),
60
    "colour_land": ((204 / 255.), (153 / 255.), (102 / 255.), (255 / 255.)),
61
    "colour_ft_vertices": (0, 0, 1, 1),
62
    "colour_ft_waypoints": (1, 0, 0, 1)}
63

64
_DEFAULT_SETTINGS_SIDEVIEW = {
3✔
65
    "vertical_extent": (1050, 180),
66
    "vertical_axis": "pressure",
67
    "secondary_axis": "no secondary axis",
68
    "plot_title_size": "default",
69
    "axes_label_size": "default",
70
    "flightlevels": [300],
71
    "draw_flightlevels": True,
72
    "draw_flighttrack": True,
73
    "fill_flighttrack": True,
74
    "label_flighttrack": True,
75
    "draw_verticals": True,
76
    "draw_marker": True,
77
    "draw_ceiling": True,
78
    "colour_ft_vertices": (0, 0, 1, 1),
79
    "colour_ft_waypoints": (1, 0, 0, 1),
80
    "colour_ft_fill": (0, 0, 1, 0.15),
81
    "colour_ceiling": (0, 0, 1, 0.15)}
82

83
_DEFAULT_SETTINGS_LINEARVIEW = {
3✔
84
    "plot_title_size": "default",
85
    "axes_label_size": "default"}
86
mpl_logger = configure_mpl_logger()
3✔
87

88

89
class ViewPlotter:
3✔
90
    def __init__(self, fig=None, ax=None, settings_tag=None, settings=None, layout=None):
3✔
91
        # setup Matplotlib Figure and Axis
92
        self.fig, self.ax = fig, ax
3✔
93
        self.settings = settings
3✔
94
        self.settings_tag = settings_tag
3✔
95
        if self.fig is None:
3✔
96
            assert ax is None
3✔
97
            if layout is not None:
3✔
98
                self.fig = figure.Figure(facecolor="w", figsize=(layout[0] / 100, layout[1] / 100))  # 0.75
3✔
99
            else:
NEW
100
                self.fig = figure.Figure(facecolor="w")
×
101
        if self.ax is None:
3✔
102
            self.ax = self.fig.add_subplot(111, zorder=99)
3✔
103

104
    def draw_metadata(self, title="", init_time=None, valid_time=None,
3✔
105
                      level=None, style=None):
106

107
        if style:
3✔
108
            title += f" ({style})"
3✔
109
        if level:
3✔
110
            title += f" at {level}"
3✔
111
        if isinstance(valid_time, datetime) and isinstance(init_time, datetime):
3✔
NEW
112
            time_step = valid_time - init_time
×
113
        else:
114
            time_step = None
3✔
115
        if isinstance(valid_time, datetime):
3✔
NEW
116
            valid_time = valid_time.strftime('%a %Y-%m-%d %H:%M UTC')
×
117
        if isinstance(init_time, datetime):
3✔
NEW
118
            init_time = init_time.strftime('%a %Y-%m-%d %H:%M UTC')
×
119

120
        # Add valid time / init time information to the title.
121
        if valid_time:
3✔
122
            if init_time:
3✔
123
                if time_step is not None:
3✔
NEW
124
                    title += f"\nValid: {valid_time} (step {((time_step.days * 86400 + time_step.seconds) // 3600):d}" \
×
125
                             f" hrs from {init_time})"
126
                else:
127
                    title += f"\nValid: {valid_time} (initialisation: {init_time})"
3✔
128
            else:
NEW
129
                title += f"\nValid: {valid_time}"
×
130

131
        # Set title.
132
        self.ax.set_title(title, horizontalalignment='left', x=0)
3✔
133

134
    def get_plot_size_in_px(self):
3✔
135
        """Determines the size of the current figure in pixels.
136
        Returns the tuple width, height.
137
        """
138
        # (bounds = left, bottom, width, height)
139
        ax_bounds = self.ax.bbox.bounds
3✔
140
        width = int(round(ax_bounds[2]))
3✔
141
        height = int(round(ax_bounds[3]))
3✔
142
        return width, height
3✔
143

144
    def get_settings(self):
3✔
145
        """Retrieve dictionary of plotting settings.
146

147
        Returns:
148
            dict: dictionary of settings.
149
        """
150
        return self.settings
3✔
151

152
    def set_settings(self, settings, save=False):
3✔
153
        """Update local settings influencing the plotting
154

155
        Args:
156
            settings (dict): Dictionary of string/value pairs
157
        """
158
        if settings is not None:
3✔
159
            self.settings.update(settings)
3✔
160
        if save:
3✔
NEW
161
            self.save_settings()
×
162

163
    def load_settings(self):
3✔
164
        self.settings = load_settings_qsettings(self.settings_tag, self.settings)
3✔
165

166
    def save_settings(self):
3✔
NEW
167
        save_settings_qsettings(self.settings_tag, self.settings)
×
168

169

170
class TopViewPlotter(ViewPlotter):
3✔
171
    def __init__(self, fig=None, ax=None, settings=None):
3✔
172
        super().__init__(fig, ax, settings_tag="topview", settings=_DEFAULT_SETTINGS_TOPVIEW,
3✔
173
                         layout=config_loader(dataset="layout")["topview"])
174
        self.map = None
3✔
175
        self.legimg = None
3✔
176
        self.legax = None
3✔
177
        # stores the  topview plot title size(tov_pts) and topview axes label size(tov_als),initially as None.
178
        self.tov_pts = None
3✔
179
        self.tov_als = None
3✔
180
        # Sets the default fontsize parameters' values for topview from MSSDefaultConfig.
181
        self.topview_size_settings = config_loader(dataset="topview")
3✔
182
        self.load_settings()
3✔
183
        self.set_settings(settings)
3✔
184
        self.ax.figure.canvas.draw()
3✔
185

186
    def init_map(self, **kwargs):
3✔
187
        self.map = mpl_map.MapCanvas(appearance=self.get_settings(),
3✔
188
                                     resolution="l", area_thresh=1000., ax=self.ax,
189
                                     **kwargs)
190

191
        # Sets the selected fontsize only if draw_graticule box from topview options is checked in.
192
        if self.settings["draw_graticule"]:
3✔
193
            try:
3✔
194
                self.map._draw_auto_graticule(self.tov_als)
3✔
NEW
195
            except Exception as ex:
×
NEW
196
                logging.error("ERROR: cannot plot graticule (message: %s - '%s')", type(ex), ex)
×
197
        else:
NEW
198
            self.map.set_graticule_visible(False)
×
199
        self.ax.set_autoscale_on(False)
3✔
200
        self.ax.set_title("Top view", fontsize=self.tov_pts, horizontalalignment="left", x=0)
3✔
201

202
    def getBBOX(self, bbox_units=None):
3✔
203
        axis = self.ax.axis()
3✔
204
        if bbox_units is not None:
3✔
NEW
205
            self.map.bbox_units = bbox_units
×
206
        if self.map.bbox_units == "degree":
3✔
207
            # Convert the current axis corners to lat/lon coordinates.
208
            axis0, axis2 = self.map(axis[0], axis[2], inverse=True)
3✔
209
            axis1, axis3 = self.map(axis[1], axis[3], inverse=True)
3✔
210
            bbox = (axis0, axis2, axis1, axis3)
3✔
211

NEW
212
        elif self.map.bbox_units.startswith("meter"):
×
NEW
213
            center_x, center_y = self.map(
×
214
                *(float(_x) for _x in self.map.bbox_units[6:-1].split(",")))
NEW
215
            bbox = (axis[0] - center_x, axis[2] - center_y, axis[1] - center_x, axis[3] - center_y)
×
216

217
        else:
NEW
218
            bbox = axis[0], axis[2], axis[1], axis[3]
×
219

220
        return bbox
3✔
221

222
    def clear_figure(self):
3✔
223
        logging.debug("Removing image")
3✔
224
        if self.map.image is not None:
3✔
225
            self.map.image.remove()
3✔
226
            self.map.image = None
3✔
227
            self.ax.set_title("Top view", horizontalalignment="left", x=0)
3✔
228
            self.ax.figure.canvas.draw()
3✔
229

230
    def set_settings(self, settings, save=False):
3✔
231
        """Apply settings from dictionary 'settings' to the view.
232
        If settings is None, apply default settings.
233
        """
234
        super().set_settings(settings, save)
3✔
235

236
        # Stores the exact value of fontsize for topview plot title size(tov_pts)
237
        self.tov_pts = (self.topview_size_settings["plot_title_size"]
3✔
238
                        if self.settings["tov_plot_title_size"] == "default"
239
                        else int(self.settings["tov_plot_title_size"]))
240
        # Stores the exact value of fontsize for topview axes label size(tov_als)
241
        self.tov_als = (self.topview_size_settings["axes_label_size"]
3✔
242
                        if self.settings["tov_axes_label_size"] == "default"
243
                        else int(self.settings["tov_axes_label_size"]))
244

245
        ax = self.ax
3✔
246

247
        if self.map is not None:
3✔
248
            self.map.set_coastlines_visible(self.settings["draw_coastlines"])
3✔
249
            self.map.set_fillcontinents_visible(visible=self.settings["fill_continents"],
3✔
250
                                                land_color=self.settings["colour_land"],
251
                                                lake_color=self.settings["colour_water"])
252
            self.map.set_mapboundary_visible(visible=self.settings["fill_waterbodies"],
3✔
253
                                             bg_color=self.settings["colour_water"])
254

255
            # Updates plot title size as selected from combobox labelled plot title size.
256
            ax.set_autoscale_on(False)
3✔
257
            ax.set_title("Top view", fontsize=self.tov_pts, horizontalalignment="left", x=0)
3✔
258

259
            # Updates graticule ticklabels/labels fontsize if draw_graticule is True.
260
            if self.settings["draw_graticule"]:
3✔
261
                self.map.set_graticule_visible(False)
3✔
262
                self.map._draw_auto_graticule(self.tov_als)
3✔
263
            else:
NEW
264
                self.map.set_graticule_visible(self.settings["draw_graticule"])
×
265

266
    def redraw_map(self, kwargs_update=None):
3✔
267
        """Redraw map canvas.
268
        Executed on clicked() of btMapRedraw.
269
        See MapCanvas.update_with_coordinate_change(). After the map redraw,
270
        coordinates of all objects overlain on the map have to be updated.
271
        """
272

273
        # 2) UPDATE MAP.
NEW
274
        self.map.update_with_coordinate_change(kwargs_update)
×
275

276
        # Sets the graticule ticklabels/labels fontsize for topview when map is redrawn.
NEW
277
        if self.settings["draw_graticule"]:
×
NEW
278
            self.map.set_graticule_visible(False)
×
NEW
279
            self.map._draw_auto_graticule(self.tov_als)
×
280
        else:
NEW
281
            self.map.set_graticule_visible(self.settings["draw_graticule"])
×
NEW
282
        self.ax.figure.canvas.draw()  # this one is required to trigger a
×
283
        # drawevent to update the background
284

285
        # self.draw_metadata() ; It is not needed here, since below here already plot title is being set.
286

287
        # Setting fontsize for topview plot title when map is redrawn.
NEW
288
        self.ax.set_title("Top view", fontsize=self.tov_pts, horizontalalignment='left', x=0)
×
NEW
289
        self.ax.figure.canvas.draw()
×
290

291
    def draw_image(self, img):
3✔
292
        """Draw the image img on the current plot.
293
        """
294
        logging.debug("plotting image..")
3✔
295
        self.wms_image = self.map.imshow(img, interpolation="nearest", origin=PIL_IMAGE_ORIGIN)
3✔
296
        # NOTE: imshow always draws the images to the lowest z-level of the
297
        # plot.
298
        # See these mailing list entries:
299
        # http://www.mail-archive.com/matplotlib-devel@lists.sourceforge.net/msg05955.html
300
        # http://old.nabble.com/Re%3A--Matplotlib-users--imshow-zorder-tt19047314.html#a19047314
301
        #
302
        # Question: Is this an issue for us or do we always want the images in the back
303
        # anyhow? At least we need to remove filled continents here.
304
        # self.map.set_fillcontinents_visible(False)
305
        # ** UPDATE 2011/01/14 ** seems to work with version 1.0!
306
        logging.debug("done.")
3✔
307

308
    def draw_legend(self, img):
3✔
309
        """Draw the legend graphics img on the current plot.
310
        Adds new axes to the plot that accommodate the legend.
311
        """
312
        # If the method is called with a "None" image, the current legend
313
        # graphic should be removed (if one exists).
314
        if self.legimg is not None:
3✔
NEW
315
            logging.debug("removing image %s", self.legimg)
×
NEW
316
            self.legimg.remove()
×
NEW
317
            self.legimg = None
×
318

319
        if img is not None:
3✔
320
            # The size of the legend axes needs to be given in relative figure
321
            # coordinates. To determine those from the legend graphics size in
322
            # pixels, we need to determine the size of the currently displayed
323
            # figure in pixels.
NEW
324
            figsize_px = self.fig.get_size_inches() * self.fig.get_dpi()
×
NEW
325
            ax_extent_x = float(img.size[0]) / figsize_px[0]
×
NEW
326
            ax_extent_y = float(img.size[1]) / figsize_px[1]
×
327

328
            # If no legend axes have been created, do so now.
NEW
329
            if self.legax is None:
×
330
                # Main axes instance of mplwidget has zorder 99.
NEW
331
                self.legax = self.fig.add_axes([1 - ax_extent_x, 0.01, ax_extent_x, ax_extent_y],
×
332
                                               frameon=False,
333
                                               xticks=[], yticks=[],
334
                                               label="ax2", zorder=0)
NEW
335
                self.legax.patch.set_facecolor("None")
×
336

337
            # If axes exist, adjust their position.
338
            else:
NEW
339
                self.legax.set_position([1 - ax_extent_x, 0.01, ax_extent_x, ax_extent_y])
×
340
            # Plot the new legimg in the legax axes.
NEW
341
            self.legimg = self.legax.imshow(img, origin=PIL_IMAGE_ORIGIN, aspect="equal", interpolation="nearest")
×
342
        self.ax.figure.canvas.draw()
3✔
343

344
    def draw_flightpath_legend(self, flightpath_dict):
3✔
345
        """
346
        Draw the flight path legend on the plot, attached to the upper-left corner.
347
        """
348
        # Clear any existing legend
349
        if self.ax.get_legend() is not None:
3✔
350
            self.ax.get_legend().remove()
3✔
351

352
        if not flightpath_dict:
3✔
353
            self.ax.figure.canvas.draw()
3✔
354
            return
3✔
355

356
        # Create legend handles
357
        legend_handles = []
3✔
358
        for name, (color, linestyle) in flightpath_dict.items():
3✔
359
            line = Line2D([0], [0], color=color, linestyle=linestyle, linewidth=2)
3✔
360
            legend_handles.append((line, name))
3✔
361

362
        # Add legend directly to the main axis, attached to the upper-left corner
363
        self.ax.legend(
3✔
364
            [handle for handle, _ in legend_handles],
365
            [name for _, name in legend_handles],
366
            loc='upper left',
367
            bbox_to_anchor=(0, 1),  # (x, y) coordinates relative to the figure
368
            bbox_transform=self.fig.transFigure,  # Use figure coordinates
369
            frameon=False
370
        )
371

372
        self.ax.figure.canvas.draw_idle()
3✔
373

374

375
class SideViewPlotter(ViewPlotter):
3✔
376
    _pres_maj = np.concatenate([np.arange(top * 10, top, -top) for top in (10000, 1000, 100, 10, 1, 0.1)] +
3✔
377
                               [[0.1]])
378
    _pres_min = np.concatenate([np.arange(top * 10, top, -top // 10) for top in (10000, 1000, 100, 10, 1, 0.1)] +
3✔
379
                               [[0.1]])
380

381
    def __init__(self, fig=None, ax=None, settings=None, numlabels=None, num_interpolation_points=None):
3✔
382
        """
383
        Arguments:
384
        model -- WaypointsTableModel defining the vertical section.
385
        """
386
        if numlabels is None:
3✔
387
            numlabels = config_loader(dataset='num_labels')
3✔
388
        if num_interpolation_points is None:
3✔
389
            num_interpolation_points = config_loader(dataset='num_interpolation_points')
3✔
390
        super().__init__(fig, ax, settings_tag="sideview", settings=_DEFAULT_SETTINGS_SIDEVIEW,
3✔
391
                         layout=config_loader(dataset="layout")["sideview"])
392
        self.load_settings()
3✔
393
        self.set_settings(settings)
3✔
394

395
        self.numlabels = numlabels
3✔
396
        self.num_interpolation_points = num_interpolation_points
3✔
397
        self.ax2 = self.ax.twinx()
3✔
398
        self.ax.patch.set_facecolor("None")
3✔
399
        self.ax2.patch.set_facecolor("None")
3✔
400
        # Main axes instance of mplwidget has zorder 99.
401
        self.imgax = self.fig.add_axes(
3✔
402
            self.ax.get_position(), frameon=True, xticks=[], yticks=[], label="imgax", zorder=0)
403
        self.vertical_lines = []
3✔
404

405
        # Sets the default value of sideview fontsize settings from MSSDefaultConfig.
406
        self.sideview_size_settings = config_loader(dataset="sideview")
3✔
407
        # Draw a number of flight level lines.
408
        self.flightlevels = []
3✔
409
        self.fl_label_list = []
3✔
410
        self.image = None
3✔
411
        self.update_vertical_extent_from_settings(init=True)
3✔
412

413
    def _determine_ticks_labels(self, typ):
3✔
414
        if typ == "no secondary axis":
3✔
415
            major_ticks = [] * units.pascal
3✔
416
            minor_ticks = [] * units.pascal
3✔
417
            labels = []
3✔
418
            ylabel = ""
3✔
419
        elif typ == "pressure":
3✔
420
            # Compute the position of major and minor ticks. Major ticks are labelled.
421
            major_ticks = self._pres_maj[(self._pres_maj <= self.p_bot) & (self._pres_maj >= self.p_top)]
3✔
422
            minor_ticks = self._pres_min[(self._pres_min <= self.p_bot) & (self._pres_min >= self.p_top)]
3✔
423
            labels = [f"{_x / 100:.0f}" if _x / 100 >= 1 else (
3✔
424
                      f"{_x / 100:.1f}" if _x / 100 >= 0.1 else (
425
                          f"{_x / 100:.2f}" if _x / 100 >= 0.01 else (
426
                              f"{_x / 100:.3f}"))) for _x in major_ticks]
427
            if len(labels) > 40:
3✔
NEW
428
                labels = ["" if any(y in x for y in "9865") else x for x in labels]
×
429
            elif len(labels) > 20:
3✔
NEW
430
                labels = ["" if any(y in x for y in "975") else x for x in labels]
×
431
            elif len(labels) > 10:
3✔
NEW
432
                labels = ["" if "9" in x else x for x in labels]
×
433
            ylabel = "pressure (hPa)"
3✔
434
        elif typ == "pressure altitude":
3✔
435
            bot_km = thermolib.pressure2flightlevel(self.p_bot * units.Pa).to(units.km).magnitude
3✔
436
            top_km = thermolib.pressure2flightlevel(self.p_top * units.Pa).to(units.km).magnitude
3✔
437
            ma_dist, mi_dist = 5, 1.0
3✔
438
            if (top_km - bot_km) <= 20:
3✔
439
                ma_dist, mi_dist = 1, 0.5
3✔
NEW
440
            elif (top_km - bot_km) <= 40:
×
NEW
441
                ma_dist, mi_dist = 2, 0.5
×
NEW
442
            elif (top_km - bot_km) <= 60:
×
NEW
443
                ma_dist, mi_dist = 4, 1.0
×
444
            major_heights = np.arange(0, top_km + 0.1, ma_dist)
3✔
445
            minor_heights = np.arange(0, top_km + 0.1, mi_dist)
3✔
446
            major_ticks = thermolib.flightlevel2pressure(major_heights * units.km).magnitude
3✔
447
            minor_ticks = thermolib.flightlevel2pressure(minor_heights * units.km).magnitude
3✔
448
            labels = major_heights
3✔
449
            ylabel = "pressure altitude (km)"
3✔
450
        elif typ == "flight level":
3✔
451
            bot_km = thermolib.pressure2flightlevel(self.p_bot * units.Pa).to(units.km).magnitude
3✔
452
            top_km = thermolib.pressure2flightlevel(self.p_top * units.Pa).to(units.km).magnitude
3✔
453
            ma_dist, mi_dist = 100, 20
3✔
454
            if (top_km - bot_km) <= 10:
3✔
NEW
455
                ma_dist, mi_dist = 20, 10
×
456
            elif (top_km - bot_km) <= 40:
3✔
457
                ma_dist, mi_dist = 40, 10
3✔
NEW
458
            elif (top_km - bot_km) <= 60:
×
NEW
459
                ma_dist, mi_dist = 50, 10
×
460
            major_fl = np.arange(0, 3248, ma_dist)
3✔
461
            minor_fl = np.arange(0, 3248, mi_dist)
3✔
462
            major_ticks = thermolib.flightlevel2pressure(major_fl * units.hft).magnitude
3✔
463
            minor_ticks = thermolib.flightlevel2pressure(minor_fl * units.hft).magnitude
3✔
464
            labels = major_fl
3✔
465
            ylabel = "flight level (hft)"
3✔
466
        else:
NEW
467
            raise RuntimeError(f"Unsupported vertical axis type: '{typ}'")
×
468
        return ylabel, major_ticks, minor_ticks, labels
3✔
469

470
    def setup_side_view(self):
3✔
471
        """Set up a vertical section view.
472

473
        Vertical cross section code (log-p axis etc.) taken from
474
        mss_batch_production/visualisation/mpl_vsec.py.
475
        """
476

477
        self.ax.set_title("vertical flight profile", horizontalalignment="left", x=0)
3✔
478
        self.ax.grid(visible=True)
3✔
479

480
        self.ax.set_xlabel("lat/lon")
3✔
481

482
        for ax in (self.ax, self.ax2):
3✔
483
            ax.set_yscale("log")
3✔
484
            ax.set_ylim(self.p_bot, self.p_top)
3✔
485

486
        self.redraw_yaxis()
3✔
487

488
    def redraw_yaxis(self):
3✔
489
        """ Redraws the y-axis on map after setting the values from sideview options dialog box
490
        and also updates the sizes for map title and x and y axes labels and ticklabels"""
491

492
        vaxis = self.settings["vertical_axis"]
3✔
493
        vaxis2 = self.settings["secondary_axis"]
3✔
494

495
        # Sets fontsize value for x axis ticklabel.
496
        axes_label_size = (self.sideview_size_settings["axes_label_size"]
3✔
497
                           if self.settings["axes_label_size"] == "default"
498
                           else int(self.settings["axes_label_size"]))
499
        # Sets fontsize value for plot title and axes title/label
500
        plot_title_size = (self.sideview_size_settings["plot_title_size"]
3✔
501
                           if self.settings["plot_title_size"] == "default"
502
                           else int(self.settings["plot_title_size"]))
503
        # Updates the fontsize of the x-axis ticklabels of sideview.
504
        self.ax.tick_params(axis='x', labelsize=axes_label_size)
3✔
505
        # Updates the fontsize of plot title and x-axis title of sideview.
506
        self.ax.set_title("vertical flight profile", fontsize=plot_title_size, horizontalalignment="left", x=0)
3✔
507
        self.ax.set_xlabel("lat/lon", fontsize=plot_title_size)
3✔
508

509
        for ax, typ in zip((self.ax, self.ax2), (vaxis, vaxis2)):
3✔
510
            ylabel, major_ticks, minor_ticks, labels = self._determine_ticks_labels(typ)
3✔
511

512
            major_ticks_units = getattr(major_ticks, "units", None)
3✔
513
            if ax.yaxis.units is None and major_ticks_units is not None:
3✔
514
                ax.yaxis.set_units(major_ticks_units)
3✔
515

516
            ax.set_ylabel(ylabel, fontsize=plot_title_size)
3✔
517
            ax.set_yticks(minor_ticks, minor=True)
3✔
518
            ax.set_yticks(major_ticks, minor=False)
3✔
519
            ax.set_yticklabels([], minor=True)
3✔
520
            ax.set_yticklabels(labels, minor=False, fontsize=axes_label_size)
3✔
521
            ax.set_ylim(self.p_bot, self.p_top)
3✔
522

523
        if vaxis2 == "no secondary axis":
3✔
524
            self.fig.subplots_adjust(left=0.08, right=0.96, top=0.9, bottom=0.14)
3✔
525
            self.imgax.set_position(self.ax.get_position())
3✔
526
        else:
527
            self.fig.subplots_adjust(left=0.08, right=0.92, top=0.9, bottom=0.14)
3✔
528
            self.imgax.set_position(self.ax.get_position())
3✔
529

530
    def redraw_xaxis(self, lats, lons, times, times_visible):
3✔
531
        """Redraw the x-axis of the side view on path changes. Also remove
532
           a vertical section image if one exists, as it is invalid after
533
           a path change.
534
        """
535
        logging.debug("redrawing x-axis")
3✔
536

537
        # Re-label x-axis.
538
        self.ax.set_xlim(0, len(lats) - 1)
3✔
539
        # Set xticks so that they display lat/lon. Plot "numlabels" labels.
540
        lat_inds = np.arange(len(lats))
3✔
541
        tick_index_step = len(lat_inds) // self.numlabels
3✔
542
        self.ax.set_xticks(lat_inds[::tick_index_step])
3✔
543

544
        if times_visible:
3✔
NEW
545
            self.ax.set_xticklabels([f'{d[0]:2.1f}, {d[1]:2.1f}\n{d[2].strftime("%H:%M")}Z'
×
546
                                     for d in zip(lats[::tick_index_step],
547
                                                  lons[::tick_index_step],
548
                                                  times[::tick_index_step])],
549
                                    rotation=25, horizontalalignment="right")
550
        else:
551
            self.ax.set_xticklabels([f"{d[0]:2.1f}, {d[1]:2.1f}"
3✔
552
                                     for d in zip(lats[::tick_index_step],
553
                                                  lons[::tick_index_step])],
554
                                    rotation=25, horizontalalignment="right")
555

556
        self.ax.figure.canvas.draw()
3✔
557

558
    def draw_vertical_lines(self, highlight, lats, lons):
3✔
559
        # Remove all vertical lines
560
        for line in self.vertical_lines:
3✔
561
            try:
3✔
562
                line.remove()
3✔
NEW
563
            except ValueError as e:
×
NEW
564
                logging.debug("Vertical line was somehow already removed:\n%s", e)
×
565
        self.vertical_lines = []
3✔
566

567
        # Add vertical lines
568
        if self.settings["draw_verticals"]:
3✔
569
            ipoint = 0
3✔
570
            for i, (lat, lon) in enumerate(zip(lats, lons)):
3✔
571
                if (ipoint < len(highlight) and
3✔
572
                        np.hypot(lat - highlight[ipoint][0],
573
                                 lon - highlight[ipoint][1]) < 2E-10):
574
                    self.vertical_lines.append(
3✔
575
                        self.ax.axvline(i, color='k', linewidth=2, linestyle='--', alpha=0.5))
576
                    ipoint += 1
3✔
577
        self.fig.canvas.draw()
3✔
578

579
    def getBBOX(self):
3✔
580
        """Get the bounding box of the view (returns a 4-tuple
581
           x1, y1(p_bot[hPa]), x2, y2(p_top[hPa])).
582
        """
583
        # Get the bounding box of the current view
584
        # (bbox = llcrnrlon, llcrnrlat, urcrnrlon, urcrnrlat; i.e. for the side
585
        #  view bbox = x1, y1(p_bot), x2, y2(p_top)).
NEW
586
        axis = self.ax.axis()
×
587

NEW
588
        num_interpolation_points = self.num_interpolation_points
×
NEW
589
        num_labels = self.numlabels
×
590

591
        # Return a tuple (num_interpolation_points, p_bot[hPa],
592
        #                 num_labels, p_top[hPa]) as BBOX.
NEW
593
        bbox = (num_interpolation_points, (axis[2] / 100),
×
594
                num_labels, (axis[3] / 100))
NEW
595
        return bbox
×
596

597
    def clear_figure(self):
3✔
598
        logging.debug("path of side view has changed.. removing invalidated "
3✔
599
                      "image (if existent) and redrawing.")
600
        if self.image is not None:
3✔
601
            self.image.remove()
3✔
602
            self.image = None
3✔
603
            self.ax.set_title("vertical flight profile", horizontalalignment="left", x=0)
3✔
604
            self.ax.figure.canvas.draw()
3✔
605

606
    def draw_image(self, img):
3✔
607
        """Draw the image img on the current plot.
608

609
        NOTE: The image is plotted in a separate axes object that is located
610
        below the axes that display the flight profile. This is necessary
611
        because imshow() does not work with logarithmic axes.
612
        """
613
        logging.debug("plotting vertical section image..")
3✔
614
        ix, iy = img.size
3✔
615
        logging.debug("  image size is %dx%d px, format is '%s'", ix, iy, img.format)
3✔
616

617
        # If an image is currently displayed, remove it from the plot.
618
        if self.image is not None:
3✔
NEW
619
            self.image.remove()
×
620

621
        # Plot the new image in the image axes and adjust the axes limits.
622
        self.image = self.imgax.imshow(
3✔
623
            img, interpolation="nearest", aspect="auto", origin=PIL_IMAGE_ORIGIN)
624
        self.imgax.set_xlim(0, ix - 1)
3✔
625
        self.imgax.set_ylim(iy - 1, 0)
3✔
626
        self.ax.figure.canvas.draw()
3✔
627
        logging.debug("done.")
3✔
628

629
    def update_vertical_extent_from_settings(self, init=False):
3✔
630
        """ Checks for current units of axis and convert the upper and lower limit
631
        to pa(pascals) for the internal computation by code """
632

633
        if not init:
3✔
634
            p_bot_old = self.p_bot
3✔
635
            p_top_old = self.p_top
3✔
636

637
        if self.settings["vertical_axis"] == "pressure altitude":
3✔
NEW
638
            self.p_bot = thermolib.flightlevel2pressure(self.settings["vertical_extent"][0] * units.km).magnitude
×
NEW
639
            self.p_top = thermolib.flightlevel2pressure(self.settings["vertical_extent"][1] * units.km).magnitude
×
640
        elif self.settings["vertical_axis"] == "flight level":
3✔
NEW
641
            self.p_bot = thermolib.flightlevel2pressure(self.settings["vertical_extent"][0] * units.hft).magnitude
×
NEW
642
            self.p_top = thermolib.flightlevel2pressure(self.settings["vertical_extent"][1] * units.hft).magnitude
×
643
        else:
644
            self.p_bot = self.settings["vertical_extent"][0] * 100
3✔
645
            self.p_top = self.settings["vertical_extent"][1] * 100
3✔
646

647
        if not init:
3✔
648
            if (p_bot_old != self.p_bot) or (p_top_old != self.p_top):
3✔
NEW
649
                if self.image is not None:
×
NEW
650
                    self.image.remove()
×
NEW
651
                    self.image = None
×
NEW
652
                self.setup_side_view()
×
653
            else:
654
                self.redraw_yaxis()
3✔
655

656

657
class LinearViewPlotter(ViewPlotter):
3✔
658
    """Specialised MplCanvas that draws a linear view of a
659
       flight track / list of waypoints.
660
    """
661

662
    def __init__(self, model=None, numlabels=None, settings=None):
3✔
663
        """
664
        Arguments:
665
        model -- WaypointsTableModel defining the linear section.
666
        """
667
        if numlabels is None:
3✔
668
            numlabels = config_loader(dataset='num_labels')
3✔
669
        super().__init__(settings_tag="linearview", settings=_DEFAULT_SETTINGS_LINEARVIEW,
3✔
670
                         layout=config_loader(dataset="layout")["linearview"])
671
        self.load_settings()
3✔
672

673
        # Sets the default values of plot sizes from MissionSupportDefaultConfig.
674
        self.linearview_size_settings = config_loader(dataset="linearview")
3✔
675
        self.set_settings(settings)
3✔
676

677
        # Setup the plot.
678
        self.numlabels = numlabels
3✔
679
        self.setup_linear_view()
3✔
680
        # If a waypoints model has been passed, create an interactor on it.
681
        self.waypoints_interactor = None
3✔
682
        self.waypoints_model = None
3✔
683
        self.vertical_lines = []
3✔
684
        self.basename = "linearview"
3✔
685

686
    def setup_linear_view(self):
3✔
687
        """Set up a linear section view.
688
        """
689
        self.fig.subplots_adjust(left=0.08, right=0.96, top=0.9, bottom=0.14)
3✔
690

691
    def clear_figure(self):
3✔
692
        logging.debug("path of linear view has changed.. removing invalidated plots")
3✔
693
        self.fig.clf()
3✔
694
        self.ax = self.fig.add_subplot(111, zorder=99)
3✔
695
        self.ax.figure.patch.set_visible(False)
3✔
696
        self.vertical_lines = []
3✔
697
        self.fig.canvas.draw()
3✔
698

699
    def redraw_xaxis(self, lats, lons):
3✔
700
        # Re-label x-axis.
701
        self.ax.set_xlim(0, len(lats) - 1)
3✔
702
        # Set xticks so that they display lat/lon. Plot "numlabels" labels.
703
        lat_inds = np.arange(len(lats))
3✔
704
        tick_index_step = len(lat_inds) // self.numlabels
3✔
705
        self.ax.set_xticks(lat_inds[::tick_index_step])
3✔
706
        self.ax.set_xticklabels([f'{d[0]:2.1f}, {d[1]:2.1f}'
3✔
707
                                 for d in zip(lats[::tick_index_step],
708
                                              lons[::tick_index_step])],
709
                                rotation=25, horizontalalignment="right")
710

711
        # Remove all vertical lines
712
        for line in self.vertical_lines:
3✔
713
            try:
3✔
714
                line.remove()
3✔
NEW
715
            except ValueError as e:
×
NEW
716
                logging.debug("Vertical line was somehow already removed:\n%s", e)
×
717
        self.vertical_lines = []
3✔
718

719
    def draw_vertical_lines(self, highlight, lats, lons):
3✔
720
        # draw vertical lines
721
        self.vertical_lines = []
3✔
722
        ipoint = 0
3✔
723
        for i, (lat, lon) in enumerate(zip(lats, lons)):
3✔
724
            if (ipoint < len(highlight) and np.hypot(lat - highlight[ipoint][0],
3✔
725
               lon - highlight[ipoint][1]) < 2E-10):
726
                self.vertical_lines.append(self.ax.axvline(i, color='k', linewidth=2, linestyle='--', alpha=0.5))
3✔
727
                ipoint += 1
3✔
728
        self.fig.tight_layout()
3✔
729
        self.fig.subplots_adjust(top=0.85, bottom=0.20)
3✔
730
        self.fig.canvas.draw()
3✔
731

732
    def draw_legend(self, img):
3✔
NEW
733
        if img is not None:
×
NEW
734
            logging.error("Legends not supported in LinearView mode!")
×
NEW
735
            raise NotImplementedError
×
736

737
    def draw_image(self, xmls, colors=None, scales=None):
3✔
738
        title = self.fig._suptitle.get_text()
3✔
739
        self.clear_figure()
3✔
740
        self.fig.suptitle(title, x=0.95, ha='right')
3✔
741
        offset = 40
3✔
742
        self.ax.patch.set_visible(False)
3✔
743

744
        for i, xml in enumerate(xmls):
3✔
745
            data = xml.find("Data")
3✔
746
            values = [float(value) for value in data.text.split(",")]
3✔
747
            unit = data.attrib["unit"]
3✔
748
            numpoints = int(data.attrib["num_waypoints"])
3✔
749

750
            if colors:
3✔
751
                color = colors[i] if len(colors) > i else colors[-1]
3✔
752
            else:
NEW
753
                color = "#00AAFF"
×
754

755
            if scales:
3✔
756
                scale = scales[i] if len(scales) > i else scales[-1]
3✔
757
            else:
NEW
758
                scale = "linear"
×
759

760
            par = self.ax.twinx() if i > 0 else self.ax
3✔
761
            par.set_yscale(scale)
3✔
762

763
            par.plot(range(numpoints), values, color)
3✔
764
            if i > 0:
3✔
NEW
765
                par.spines["right"].set_position(("outward", (i - 1) * offset))
×
766
            if unit:
3✔
767
                par.set_ylabel(unit)
3✔
768

769
            par.yaxis.label.set_color(color.replace("0x", "#"))
3✔
770

771
    def set_settings(self, settings, save=False):
3✔
772
        """
773
        Apply settings from options ui to the linear view
774
        """
775

776
        super().set_settings(settings, save)
3✔
777

778
        pts = (self.linearview_size_settings["plot_title_size"] if self.settings["plot_title_size"] == "default"
3✔
779
               else int(self.settings["plot_title_size"]))
780
        label_size = (self.linearview_size_settings["axes_label_size"] if self.settings["axes_label_size"] == "default"
3✔
781
                      else int(self.settings["axes_label_size"]))
782
        self.ax.tick_params(axis='both', labelsize=label_size)
3✔
783
        self.ax.set_title("Linear flight profile", fontsize=pts, horizontalalignment='left', x=0)
3✔
784
        self.ax.figure.canvas.draw()
3✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc