diff --git a/AUTHORS b/AUTHORS index 6215a054f..db108ce63 100644 --- a/AUTHORS +++ b/AUTHORS @@ -19,6 +19,7 @@ in alphabetic order by first name - Matthias Riße - May Bär - Nilupul Manodya +- Preetam Sundar Das - Reimar Bauer - Rishabh Soni - Sakshi Chopkar diff --git a/docs/autoplot_dock_widget.rst b/docs/autoplot_dock_widget.rst new file mode 100644 index 000000000..e824bd50d --- /dev/null +++ b/docs/autoplot_dock_widget.rst @@ -0,0 +1,97 @@ +mssautoplot - UI Setup +====================== + +Autoplot Docking Widget +----------------------- + +**Autoplot Dockwidget Description:** +The Docking Widget offers users a Graphical User Interface (GUI) for downloading plots based on specified configurations. It provides a user-friendly approach, integrating seamlessly with other dock widgets. This widget can be accessed across all three views—Top View, Side View, and Linear View—ensuring flexibility and ease of use for different user preferences. + +**Components:** + +The autoplot docking widget contains the following parameters which can be configured: + +- **Select configuration file button**: This button is used to select the JSON file for uploading the configurations to the dock widget. By default, it uses the `mssautoplot.json` file located in this path - `$HOME/.config/msui`. + +- **Left Treewidget**: The following parameters can be configured or updated based on requirements: + + - Flight + - Map Sections + - Vertical + - Filename + - Initial Time + - Valid Time + +- **Right Treewidget**: The following parameters can be configured or updated: + + - URL + - Layers + - Styles + - Level + - Start Time + - End Time + - Time Interval + +- **ComboBoxes**: These allow configuring the start time, end time, and time interval. Ensure that the start time is always less than the end time. The plots are downloaded from the start time to the end time at the provided time intervals. + +- **Download Plots Button**: This button is used to download plots based on the configurations. The configuration is saved in the `mssautoplot.json` file by default, located at `$HOME/.config/msui`. + +- **Update/Create JSON file**: This button will create or update the JSON file. If not present, it will update the default `mssautoplot.json` file located at `$HOME/.config/msui`. + +How to Use +---------- + +The `mssautoplot.json` file located in the `$HOME/.config/msui` directory contains the default configuration. + +The **left treewidget** is used to configure the automated plotting flights list: + +.. code-block:: json + + "automated_plotting_flights": + [ + ["flight1", "section1", "vertical1", "filename1", "init_time1", "time1"], + ["flight2", "section2", "vertical2", "filename2", "init_time2", "time2"] + ] + +- Flight, filename, and section parameters are configured in the **Top View**. +- The vertical parameter is configured in the **Side View**. + +The **right treewidget** is used to configure the automated plotting flight sections, which are based on the view: + +- For **Top View**, it is `"automated_plotting_hsecs": [["URL", "Layer", "Styles", "Level"]]`. +- For **Side View**, it is `"automated_plotting_vsecs": [["URL", "Layer", "Styles", "Level"]]`. +- For **Linear View**, it is `"automated_plotting_lsecs": [["URL", "Layer", "Styles"]]`. + +Inserting, Updating, and Removing Configuration in the Treewidget +----------------------------------------------------------------- + +There are three buttons— **Add**, **Update**, and **Remove**—under each treewidget: + +- **Add Button**: Inserts a row based on the current configurations. + + - For the left treewidget, the current values of flight, section, vertical, filename, init_time, and time are inserted. + - For the right treewidget, the current values of URL, Layers, Styles, Level, Start Time, End Time, and Time Interval are inserted. + +- **Remove Button**: Removes the selected row. + +- **Update Button**: Updates the selected row with the current values (only active after selecting a row). + +Ensure that the **right tree widget** has at least one row before inserting into the left treewidget. + +Downloading the Plots +--------------------- + +Plots can be downloaded in the following ways: + +1. Upload the configuration JSON file by clicking the **Select configuration file** button, then click the **Download Plots** button to download the plots. +2. Upload the configuration JSON file by clicking the **Select configuration file** button, then make modifications as needed. +3. Download plots with or without flight track from start time to end time at specified time intervals. This ensures that a total of `M x N` plots are downloaded, where `M` is the number of rows in the left treewidget and `N` is the number of rows in the right treewidget. + +Example +------- + +An `mssautoplot.json` file generated after clicking the **Update/Create Configuration file Button** in the path, e.g. “$HOME/.config/msui” by default: + +**/$HOME/.config/msui/mssautoplot.json** + +.. literalinclude:: samples/config/msui/autoplot_dockwidget.json.sample diff --git a/docs/components.rst b/docs/components.rst index d0aeca2d9..a3eb80b40 100644 --- a/docs/components.rst +++ b/docs/components.rst @@ -10,5 +10,6 @@ Components mscolab gentutorials mssautoplot + autoplot_dock_widget conf_sso_test_msscolab sso_via_saml_mscolab diff --git a/docs/samples/config/msui/autoplot_dockwidget.json.sample b/docs/samples/config/msui/autoplot_dockwidget.json.sample new file mode 100644 index 000000000..c929ebf12 --- /dev/null +++ b/docs/samples/config/msui/autoplot_dockwidget.json.sample @@ -0,0 +1,294 @@ +{ + "mscolab_skip_verify_user_token": true, + "filepicker_default": "default", + "data_dir": "~/mssdata", + "layout": { + "topview": [ + 963, + 702 + ], + "sideview": [ + 913, + 557 + ], + "linearview": [ + 913, + 557 + ], + "tableview": [ + 1236, + 424 + ], + "immutable": false + }, + "predefined_map_sections": { + "00 global (cyl)": { + "CRS": "EPSG:4326", + "map": { + "llcrnrlon": -180.0, + "llcrnrlat": -90.0, + "urcrnrlon": 180.0, + "urcrnrlat": 90.0 + } + }, + "01 SADPAP (stereo)": { + "CRS": "EPSG:77890290", + "map": { + "llcrnrlon": -150.0, + "llcrnrlat": -45.0, + "urcrnrlon": -25.0, + "urcrnrlat": -20.0 + } + }, + "02 SADPAP zoom (stereo)": { + "CRS": "EPSG:77890290", + "map": { + "llcrnrlon": -120.0, + "llcrnrlat": -65.0, + "urcrnrlon": -45.0, + "urcrnrlat": -28.0 + } + }, + "03 SADPAP (cyl)": { + "CRS": "EPSG:4326", + "map": { + "llcrnrlon": -100.0, + "llcrnrlat": -75.0, + "urcrnrlon": -30.0, + "urcrnrlat": -30.0 + } + }, + "04 Southern Hemisphere (stereo)": { + "CRS": "EPSG:77889270", + "map": { + "llcrnrlon": 135.0, + "llcrnrlat": 0.0, + "urcrnrlon": -45.0, + "urcrnrlat": 0.0 + } + }, + "05 EDMO-SAL (cyl)": { + "CRS": "EPSG:4326", + "map": { + "llcrnrlon": -40, + "llcrnrlat": 10, + "urcrnrlon": 30, + "urcrnrlat": 60 + } + }, + "06 SAL-BA (cyl)": { + "CRS": "EPSG:4326", + "map": { + "llcrnrlon": -80, + "llcrnrlat": -40, + "urcrnrlon": -10, + "urcrnrlat": 30 + } + }, + "07 Europe (cyl)": { + "CRS": "EPSG:4326", + "map": { + "llcrnrlon": -15.0, + "llcrnrlat": 35.0, + "urcrnrlon": 30.0, + "urcrnrlat": 65.0 + } + }, + "08 Germany (cyl)": { + "CRS": "EPSG:4326", + "map": { + "llcrnrlon": 5.0, + "llcrnrlat": 45.0, + "urcrnrlon": 15.0, + "urcrnrlat": 57.0 + } + }, + "09 Northern Hemisphere (stereo)": { + "CRS": "MSS:stere,0,90,90", + "map": { + "llcrnrlon": -45.0, + "llcrnrlat": 0.0, + "urcrnrlon": 135.0, + "urcrnrlat": 0.0 + } + } + }, + "num_interpolation_points": 201, + "num_labels": 10, + "default_WMS": [ + "http://localhost:8081/", + "https://view.eumetsat.int/geoserver/wms", + "http://eccharts.ecmwf.int/wms/?token=public", + "https://neo.gsfc.nasa.gov/wms/wms" + ], + "default_VSEC_WMS": [ + "http://localhost:8081/" + ], + "default_LSEC_WMS": [ + "http://localhost:8081/" + ], + "default_MSCOLAB": [ + "http://localhost:8083" + ], + "MSCOLAB_auth_user_name": "mscolab", + "MSCOLAB_category": "default", + "MSCOLAB_timeout": [ + 2, + 10 + ], + "MSCOLAB_skip_archived_operations": false, + "MSS_auth": { + "https://forecast.fz-juelich.de/campaigns2019": "username", + "http://localhost:8083": "emailid" + }, + "WMS_request_timeout": 30, + "WMS_preload": [], + "wms_cache": "wms cache path", + "wms_cache_max_size_bytes": 20971520, + "wms_cache_max_age_seconds": 432000, + "wms_prefetch": { + "validtime_fwd": 0, + "validtime_bck": 0, + "level_up": 0, + "level_down": 0 + }, + "locations": { + "EDMO": [ + 48.08, + 11.28 + ], + "Hannover": [ + 52.37, + 9.74 + ], + "Hamburg": [ + 53.55, + 9.99 + ], + "Juelich": [ + 50.92, + 6.36 + ], + "Leipzig": [ + 51.34, + 12.37 + ], + "Muenchen": [ + 48.14, + 11.57 + ], + "Stuttgart": [ + 48.78, + 9.18 + ], + "Wien": [ + 48.20833, + 16.373064 + ], + "Zugspitze": [ + 47.42, + 10.98 + ], + "Kiruna": [ + 67.821, + 20.336 + ], + "Ny-Alesund": [ + 78.928, + 11.986 + ], + "Zhukovsky": [ + 55.6, + 38.116 + ], + "Paphos": [ + 34.775, + 32.425 + ], + "Sharjah": [ + 25.35, + 55.65 + ], + "Brindisi": [ + 40.658, + 17.947 + ], + "Nagpur": [ + 21.15, + 79.083 + ], + "Mumbai": [ + 19.089, + 72.868 + ], + "Delhi": [ + 28.566, + 77.103 + ] + }, + "new_flighttrack_template": [ + "Nagpur", + "Delhi" + ], + "new_flighttrack_flightlevel": 0, + "proxies": {}, + "mscolab_server_url": "http://localhost:8083", + "mss_dir": "~/mss", + "gravatar_ids": [], + "export_plugins": {}, + "import_plugins": {}, + "topview": { + "plot_title_size": 10, + "axes_label_size": 10 + }, + "sideview": { + "plot_title_size": 10, + "axes_label_size": 10 + }, + "linearview": { + "plot_title_size": 10, + "axes_label_size": 10 + }, + "automated_plotting_flights": [ + [ + "ST16", + "00 global (cyl)", + "", + "ST16.ftml", + "", + "2019-07-01T12:00:00Z" + ], + [ + "op2", + "03 SADPAP (cyl)", + "1043.0, 176.0", + "op2", + "", + "2019-09-29T12:00:00Z" + ], + [ + "op2", + "03 SADPAP (cyl)", + "", + "op2", + "", + "2019-09-30T00:00:00Z" + ] + ], + "automated_plotting_hsecs": [ + [ + "https://forecast.fz-juelich.de/campaigns2019", + "CLaMS_TL.mole_fraction_of_H1301_in_air_tl", + "auto", + "330" + ], + [ + "https://forecast.fz-juelich.de/campaigns2019", + "CLaMS_PL.surface_origin_tracer_from_asia_pl", + "auto", + "500.0" + ] + ], + "automated_plotting_vsecs": [], + "automated_plotting_lsecs": [] +} diff --git a/docs/samples/config/msui/mssautoplot.json.sample b/docs/samples/config/msui/mssautoplot.json.sample index a60bdc137..b57a23c76 100644 --- a/docs/samples/config/msui/mssautoplot.json.sample +++ b/docs/samples/config/msui/mssautoplot.json.sample @@ -38,24 +38,96 @@ }, "predefined_map_sections": { - "01 Europe (cyl)": {"CRS": "EPSG:4326", - "map": {"llcrnrlon": -15.0, "llcrnrlat": 35.0, - "urcrnrlon": 30.0, "urcrnrlat": 65.0}}, - "02 Germany (cyl)": {"CRS": "EPSG:4326", - "map": {"llcrnrlon": 5.0, "llcrnrlat": 45.0, - "urcrnrlon": 15.0, "urcrnrlat": 57.0}}, - "03 Global (cyl)": {"CRS": "EPSG:4326", - "map": {"llcrnrlon": -180.0, "llcrnrlat": -90.0, - "urcrnrlon": 180.0, "urcrnrlat": 90.0}}, - "04 Shannon (stereo)": {"CRS": "EPSG:77752350", - "map": {"llcrnrlon": -45.0, "llcrnrlat": 22.0, - "urcrnrlon": 45.0, "urcrnrlat": 63.0}}, - "05 Northern Hemisphere (stereo)": {"CRS": "EPSG:77790000", - "map": {"llcrnrlon": -45.0, "llcrnrlat": 0.0, - "urcrnrlon": 135.0, "urcrnrlat": 0.0}}, - "06 Southern Hemisphere (stereo)": {"CRS": "EPSG:77890000", - "map": {"llcrnrlon": 45.0, "llcrnrlat": 0.0, - "urcrnrlon": -135.0, "urcrnrlat": 0.0}} + "00 global (cyl)": { + "CRS": "EPSG:4326", + "map": { + "llcrnrlon": -180.0, + "llcrnrlat": -90.0, + "urcrnrlon": 180.0, + "urcrnrlat": 90.0 + } + }, + "01 SADPAP (stereo)": { + "CRS": "EPSG:77890290", + "map": { + "llcrnrlon": -150.0, + "llcrnrlat": -45.0, + "urcrnrlon": -25.0, + "urcrnrlat": -20.0 + } + }, + "02 SADPAP zoom (stereo)": { + "CRS": "EPSG:77890290", + "map": { + "llcrnrlon": -120.0, + "llcrnrlat": -65.0, + "urcrnrlon": -45.0, + "urcrnrlat": -28.0 + } + }, + "03 SADPAP (cyl)": { + "CRS": "EPSG:4326", + "map": { + "llcrnrlon": -100.0, + "llcrnrlat": -75.0, + "urcrnrlon": -30.0, + "urcrnrlat": -30.0 + } + }, + "04 Southern Hemisphere (stereo)": { + "CRS": "EPSG:77889270", + "map": { + "llcrnrlon": 135.0, + "llcrnrlat": 0.0, + "urcrnrlon": -45.0, + "urcrnrlat": 0.0 + } + }, + "05 EDMO-SAL (cyl)": { + "CRS": "EPSG:4326", + "map": { + "llcrnrlon": -40, + "llcrnrlat": 10, + "urcrnrlon": 30, + "urcrnrlat": 60 + } + }, + "06 SAL-BA (cyl)": { + "CRS": "EPSG:4326", + "map": { + "llcrnrlon": -80, + "llcrnrlat": -40, + "urcrnrlon": -10, + "urcrnrlat": 30 + } + }, + "07 Europe (cyl)": { + "CRS": "EPSG:4326", + "map": { + "llcrnrlon": -15.0, + "llcrnrlat": 35.0, + "urcrnrlon": 30.0, + "urcrnrlat": 65.0 + } + }, + "08 Germany (cyl)": { + "CRS": "EPSG:4326", + "map": { + "llcrnrlon": 5.0, + "llcrnrlat": 45.0, + "urcrnrlon": 15.0, + "urcrnrlat": 57.0 + } + }, + "09 Northern Hemisphere (stereo)": { + "CRS": "MSS:stere,0,90,90", + "map": { + "llcrnrlon": -45.0, + "llcrnrlat": 0.0, + "urcrnrlon": 135.0, + "urcrnrlat": 0.0 + } + } }, "new_flighttrack_template": ["Kiruna", "Ny-Alesund"], diff --git a/docs/samples/config/msui/msui_settings.json.sample b/docs/samples/config/msui/msui_settings.json.sample index 2f125181e..1f0e5453c 100644 --- a/docs/samples/config/msui/msui_settings.json.sample +++ b/docs/samples/config/msui/msui_settings.json.sample @@ -37,24 +37,96 @@ }, "predefined_map_sections": { - "01 Europe (cyl)": {"CRS": "EPSG:4326", - "map": {"llcrnrlon": -15.0, "llcrnrlat": 35.0, - "urcrnrlon": 30.0, "urcrnrlat": 65.0}}, - "02 Germany (cyl)": {"CRS": "EPSG:4326", - "map": {"llcrnrlon": 5.0, "llcrnrlat": 45.0, - "urcrnrlon": 15.0, "urcrnrlat": 57.0}}, - "03 Global (cyl)": {"CRS": "EPSG:4326", - "map": {"llcrnrlon": -180.0, "llcrnrlat": -90.0, - "urcrnrlon": 180.0, "urcrnrlat": 90.0}}, - "04 Shannon (stereo)": {"CRS": "EPSG:77752350", - "map": {"llcrnrlon": -45.0, "llcrnrlat": 22.0, - "urcrnrlon": 45.0, "urcrnrlat": 63.0}}, - "05 Northern Hemisphere (stereo)": {"CRS": "EPSG:77790000", - "map": {"llcrnrlon": -45.0, "llcrnrlat": 0.0, - "urcrnrlon": 135.0, "urcrnrlat": 0.0}}, - "06 Southern Hemisphere (stereo)": {"CRS": "EPSG:77890000", - "map": {"llcrnrlon": 45.0, "llcrnrlat": 0.0, - "urcrnrlon": -135.0, "urcrnrlat": 0.0}} + "00 global (cyl)": { + "CRS": "EPSG:4326", + "map": { + "llcrnrlon": -180.0, + "llcrnrlat": -90.0, + "urcrnrlon": 180.0, + "urcrnrlat": 90.0 + } + }, + "01 SADPAP (stereo)": { + "CRS": "EPSG:77890290", + "map": { + "llcrnrlon": -150.0, + "llcrnrlat": -45.0, + "urcrnrlon": -25.0, + "urcrnrlat": -20.0 + } + }, + "02 SADPAP zoom (stereo)": { + "CRS": "EPSG:77890290", + "map": { + "llcrnrlon": -120.0, + "llcrnrlat": -65.0, + "urcrnrlon": -45.0, + "urcrnrlat": -28.0 + } + }, + "03 SADPAP (cyl)": { + "CRS": "EPSG:4326", + "map": { + "llcrnrlon": -100.0, + "llcrnrlat": -75.0, + "urcrnrlon": -30.0, + "urcrnrlat": -30.0 + } + }, + "04 Southern Hemisphere (stereo)": { + "CRS": "EPSG:77889270", + "map": { + "llcrnrlon": 135.0, + "llcrnrlat": 0.0, + "urcrnrlon": -45.0, + "urcrnrlat": 0.0 + } + }, + "05 EDMO-SAL (cyl)": { + "CRS": "EPSG:4326", + "map": { + "llcrnrlon": -40, + "llcrnrlat": 10, + "urcrnrlon": 30, + "urcrnrlat": 60 + } + }, + "06 SAL-BA (cyl)": { + "CRS": "EPSG:4326", + "map": { + "llcrnrlon": -80, + "llcrnrlat": -40, + "urcrnrlon": -10, + "urcrnrlat": 30 + } + }, + "07 Europe (cyl)": { + "CRS": "EPSG:4326", + "map": { + "llcrnrlon": -15.0, + "llcrnrlat": 35.0, + "urcrnrlon": 30.0, + "urcrnrlat": 65.0 + } + }, + "08 Germany (cyl)": { + "CRS": "EPSG:4326", + "map": { + "llcrnrlon": 5.0, + "llcrnrlat": 45.0, + "urcrnrlon": 15.0, + "urcrnrlat": 57.0 + } + }, + "09 Northern Hemisphere (stereo)": { + "CRS": "MSS:stere,0,90,90", + "map": { + "llcrnrlon": -45.0, + "llcrnrlat": 0.0, + "urcrnrlon": 135.0, + "urcrnrlat": 0.0 + } + } }, "new_flighttrack_template": ["Kiruna", "Ny-Alesund"], diff --git a/mslib/msui/autoplot_dockwidget.py b/mslib/msui/autoplot_dockwidget.py new file mode 100644 index 000000000..642915624 --- /dev/null +++ b/mslib/msui/autoplot_dockwidget.py @@ -0,0 +1,516 @@ +# -*- coding: utf-8 -*- +""" + + mslib.utils.autoplot_dockwidget + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + This is a docking widget that allows the user to create the + json file or edit the json file which can be used by the CLI for + automatically downloading the plots. + + This file is part of MSS. + + :copyright: Copyright 2024 Preetam Sundar Das + :copyright: Copyright 2024 by the MSS team, see AUTHORS. + :license: APACHE-2.0, see LICENSE for details. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" + +import os +import json +import logging +from datetime import datetime + +import click +from PyQt5.QtWidgets import QWidget, QFileDialog, QTreeWidgetItem, QMessageBox +from PyQt5 import QtCore + +from mslib.utils.mssautoplot import main as cli_tool +from mslib.msui.qt5.ui_mss_autoplot import Ui_AutoplotDockWidget +from mslib.msui import constants as const + + +class AutoplotDockWidget(QWidget, Ui_AutoplotDockWidget): + + treewidget_item_selected = QtCore.pyqtSignal(str, str, str, str) + autoplot_treewidget_item_selected = QtCore.pyqtSignal(str, str) + update_op_flight_treewidget = QtCore.pyqtSignal(str, str) + + def __init__(self, parent=None, parent2=None, view=None, config_settings=None): + super().__init__() + self.setupUi(self) + + self.UploadAutoplotButton.setVisible(False) + self.UploadAutoplotSecsButton.setVisible(False) + self.cpath = "" + self.view = view + self.url = "" + self.layer = "" + self.styles = "" + self.level = "" + self.flight = "" + self.sections = "" + self.vertical = "" + self.filename = "" + self.itime = "" + self.vtime = "" + self.stime = "" + self.etime = "" + self.intv = "" + + self.refresh_sig(config_settings) + + parent.refresh_signal_send.connect(lambda: self.refresh_sig(config_settings)) + + parent.vtime_vals.connect(lambda vtime_vals: self.update_stime_etime(vtime_vals)) + + self.autoplotSecsTreeWidget.itemSelectionChanged.connect(self.autoplotSecsTreeWidget_selected_row) + self.autoplotTreeWidget.itemSelectionChanged.connect(self.autoplotTreeWidget_selected_row) + + # Add to TreeWidget + if self.view == "Top View": + self.addToAutoplotButton.clicked.connect(lambda: self.add_to_treewidget( + parent, parent2, config_settings, self.autoplotTreeWidget, parent.waypoints_model.name, + parent.cbChangeMapSection.currentText(), self.vertical, parent.waypoints_model.name, parent.curritime, + parent.currvtime, "", "", "", "" + )) + elif self.view == "Side View": + self.addToAutoplotButton.clicked.connect(lambda: self.add_to_treewidget( + parent, parent2, config_settings, self.autoplotTreeWidget, parent.waypoints_model.name, "", + parent.currvertical, parent.waypoints_model.name, parent.curritime, parent.currvtime, "", "", + "", "" + )) + else: + self.addToAutoplotButton.clicked.connect(lambda: self.add_to_treewidget( + parent, parent2, config_settings, self.autoplotTreeWidget, parent.waypoints_model.name, "", "", + parent.waypoints_model.name, "", parent.currvtime, "", "", "", "" + )) + + self.addToAutoplotSecsButton.clicked.connect(lambda: self.add_to_treewidget( + parent, parent2, config_settings, self.autoplotSecsTreeWidget, "", "", "", "", "", "", + parent.currurl, parent.currlayer, str(parent.currstyles).strip(), parent.currlevel + )) + + # Remove from Tree Widget + self.RemoveFromAutoplotButton.clicked.connect( + lambda: self.remove_selected_row(parent, self.autoplotTreeWidget, config_settings)) + self.RemoveFromAutoplotSecsButton.clicked.connect( + lambda: self.remove_selected_row(parent, self.autoplotSecsTreeWidget, config_settings)) + + # Update Tree Widget + if self.view == "Top View": + self.UploadAutoplotButton.clicked.connect(lambda: self.update_treewidget( + parent, parent2, config_settings, self.autoplotTreeWidget, parent.waypoints_model.name, + parent.cbChangeMapSection.currentText(), "", parent.waypoints_model.name, parent.curritime, + parent.currvtime, "", "", "", "" + )) + elif self.view == "Side View": + self.UploadAutoplotButton.clicked.connect(lambda: self.update_treewidget( + parent, parent2, config_settings, self.autoplotTreeWidget, parent.waypoints_model.name, "", + parent.currvertical, parent.waypoints_model.name, "", parent.currvtime, "", "", "", "" + )) + else: + self.UploadAutoplotButton.clicked.connect(lambda: self.update_treewidget( + parent, parent2, config_settings, self.autoplotTreeWidget, parent.waypoints_model.name, "", "", + parent.waypoints_model.name, "", parent.currvtime, "", "", "", "" + )) + + self.UploadAutoplotSecsButton.clicked.connect(lambda: self.update_treewidget( + parent, parent2, config_settings, self.autoplotSecsTreeWidget, "", "", "", "", "", "", + parent.currurl, parent.currlayer, str(parent.currstyles).strip(), parent.currlevel + )) + + # config buttons + self.selectConfigButton.clicked.connect(lambda: self.configure_from_path(parent, config_settings)) + self.updateConfigFile.clicked.connect(lambda: self.update_config_file(config_settings)) + self.updateConfigFile.setDefault(True) + + # time interval combobox + self.timeIntervalComboBox.currentIndexChanged.connect( + lambda: self.combo_box_input(self.timeIntervalComboBox)) + # stime/etime + self.stimeComboBox.currentIndexChanged.connect( + lambda: self.combo_box_input(self.stimeComboBox)) + self.etimeComboBox.currentIndexChanged.connect( + lambda: self.combo_box_input(self.etimeComboBox)) + + self.autoplotTreeWidget.itemSelectionChanged.connect(self.on_item_selection_changed) + self.autoplotSecsTreeWidget.itemSelectionChanged.connect(self.on_item_selection_changed_secs) + self.downloadPushButton.clicked.connect(lambda: self.download_plots_cli(config_settings)) + + def download_plots_cli(self, config_settings): + if self.stime > self.etime: + QMessageBox.information( + self, + "WARNING", + "Start time should be before end time" + ) + return + if self.autoplotSecsTreeWidget.topLevelItemCount() == 0: + QMessageBox.information( + self, + "WARNING", + "Cannot download empty treewidget" + ) + return + view = "top" + intv = 0 + if self.intv != "": + index = self.intv.find(' ') + intv = int(self.intv[:index]) + + if self.view == "Top View": + view = "top" + elif self.view == "Side View": + view = "side" + else: + view = "linear" + + # Create the configuration path + config_path = os.path.join(const.MSUI_CONFIG_PATH, "mssautoplot.json") + + # Save the config settings to the file + if config_path: + with open(config_path, 'w') as file: + json.dump(config_settings, file, indent=4) + + args = { + 'cpath': config_path, + 'view': view, + 'ftrack': "", + 'itime': self.itime, + 'vtime': self.vtime, + 'intv': intv, + 'stime': self.stime[:-1], + 'etime': self.etime[:-1] + } + + # Invoke the main method using click from the mssautoplot + try: + ctx = click.Context(cli_tool) + ctx.obj = self + ctx.invoke(cli_tool, **args) + except SystemExit as ex: + logging.error("Can't find given data: %s", ex) + QMessageBox.information( + self, + "Error", + ex.args[0] + ) + ctx.obj = None + return + + def autoplotSecsTreeWidget_selected_row(self): + selected_items = self.autoplotSecsTreeWidget.selectedItems() + if selected_items: + url = selected_items[0].text(0) + layer = selected_items[0].text(1) + styles = selected_items[0].text(2) + level = selected_items[0].text(3) + + self.treewidget_item_selected.emit(url, layer, styles, level) + + def autoplotTreeWidget_selected_row(self): + if self.autoplotSecsTreeWidget.topLevelItemCount() == 0: + QMessageBox.information( + self, + "WARNING", + "Select right tree widget row first." + ) + return + selected_items = self.autoplotTreeWidget.selectedItems() + if selected_items: + flight = selected_items[0].text(0) + filename = selected_items[0].text(3) + section = selected_items[0].text(1) + vtime = selected_items[0].text(5) + if flight != "" and flight == filename: + self.update_op_flight_treewidget.emit("operation", flight) + elif flight != "": + self.update_op_flight_treewidget.emit("flight", flight) + self.autoplot_treewidget_item_selected.emit(section, vtime) + + def update_stime_etime(self, vtime_data): + self.stimeComboBox.clear() + self.etimeComboBox.clear() + self.stimeComboBox.addItem("") + self.etimeComboBox.addItem("") + self.stimeComboBox.addItems(vtime_data) + self.etimeComboBox.addItems(vtime_data) + + def on_item_selection_changed(self): + selected_item = self.autoplotTreeWidget.selectedItems() + if selected_item: + self.UploadAutoplotButton.setVisible(True) + else: + self.UploadAutoplotButton.setVisible(False) + + def on_item_selection_changed_secs(self): + selected_item = self.autoplotSecsTreeWidget.selectedItems() + if selected_item: + self.UploadAutoplotSecsButton.setVisible(True) + else: + self.UploadAutoplotSecsButton.setVisible(False) + + def configure_from_path(self, parent, config_settings): + options = QFileDialog.Options() + options |= QFileDialog.DontUseNativeDialog + + fileName, _ = QFileDialog.getOpenFileName( + self, "Select .json Config File", const.MSUI_CONFIG_PATH, "JSON Files (*.json)", options=options) + + if fileName != "": + self.cpath = fileName + with open(fileName, 'r') as file: + configure = json.load(file) + autoplot_flights = configure["automated_plotting_flights"] + autoplot_hsecs = configure["automated_plotting_hsecs"] + autoplot_vsecs = configure["automated_plotting_vsecs"] + autoplot_lsecs = configure["automated_plotting_lsecs"] + + config_settings["automated_plotting_flights"] = autoplot_flights + config_settings["automated_plotting_hsecs"] = autoplot_hsecs + config_settings["automated_plotting_vsecs"] = autoplot_vsecs + config_settings["automated_plotting_lsecs"] = autoplot_lsecs + + parent.refresh_signal_emit.emit() + self.resize_treewidgets() + + def add_to_treewidget(self, parent, parent2, config_settings, treewidget, flight, sections, vertical, filename, + itime, vtime, url, layer, styles, level): + if treewidget.objectName() == "autoplotTreeWidget": + if self.autoplotSecsTreeWidget.topLevelItemCount() == 0: + QMessageBox.information( + self, + "WARNING", + "Add right tree widget row first." + ) + return + if flight.startswith("new flight track"): + filename = "" + flight = "" + else: + if filename != parent2.mscolab.active_operation_name: + filename += ".ftml" + item = QTreeWidgetItem([flight, sections, vertical, filename, itime, vtime]) + self.autoplotTreeWidget.addTopLevelItem(item) + self.autoplotTreeWidget.setCurrentItem(item) + config_settings["automated_plotting_flights"].append([flight, sections, vertical, filename, itime, vtime]) + parent.refresh_signal_emit.emit() + if treewidget.objectName() == "autoplotSecsTreeWidget": + if url == "": + QMessageBox.information( + self, + "WARNING", + "Please select the URL, layer, styles and level (row information first)" + ) + return + item = QTreeWidgetItem([url, layer, styles, level, self.stime, self.etime, self.intv]) + self.autoplotSecsTreeWidget.addTopLevelItem(item) + self.autoplotSecsTreeWidget.setCurrentItem(item) + + if self.view == "Top View": + config_settings["automated_plotting_hsecs"].append([url, layer, styles, level]) + elif self.view == "Side View": + config_settings["automated_plotting_vsecs"].append([url, layer, styles, level]) + else: + config_settings["automated_plotting_lsecs"].append([url, layer, styles, level]) + self.autoplotSecsTreeWidget.clearSelection() + self.resize_treewidgets() + + def update_treewidget(self, parent, parent2, config_settings, treewidget, flight, sections, vertical, filename, + itime, vtime, url, layer, styles, level): + if flight.startswith("new flight track"): + filename = "" + flight = "" + else: + if filename != parent2.mscolab.active_operation_name: + filename += ".ftml" + if treewidget.objectName() == "autoplotTreeWidget": + selected_item = self.autoplotTreeWidget.currentItem() + selected_item.setText(0, flight) + selected_item.setText(3, filename) + selected_item.setText(5, vtime) + if self.view == "Top View": + selected_item.setText(1, sections) + selected_item.setText(4, itime) + elif self.view == "Side View": + selected_item.setText(2, vertical) + + index = treewidget.indexOfTopLevelItem(selected_item) + settings_list = config_settings["automated_plotting_flights"][index] + if self.view == "Top View": + config_settings["automated_plotting_flights"][index] = [ + flight, sections, settings_list[2], filename, itime, vtime] + elif self.view == "Side View": + config_settings["automated_plotting_flights"][index] = [ + flight, settings_list[1], vertical, filename, settings_list[4], vtime] + else: + config_settings["automated_plotting_flights"][index] = [ + flight, settings_list[1], settings_list[2], filename, settings_list[4], vtime] + parent.refresh_signal_emit.emit() + + if treewidget.objectName() == "autoplotSecsTreeWidget": + if url == "": + QMessageBox.information( + self, + "WARNING", + "Please select the URL, layer, styles and level (row information first)" + ) + return + selected_item = self.autoplotSecsTreeWidget.currentItem() + selected_item.setText(0, url) + selected_item.setText(1, layer) + selected_item.setText(2, styles) + selected_item.setText(3, level) + selected_item.setText(4, self.stime) + selected_item.setText(5, self.etime) + selected_item.setText(6, self.intv) + index = treewidget.indexOfTopLevelItem(selected_item) + if self.view == "Top View": + if index != -1: + config_settings["automated_plotting_hsecs"][index] = [url, layer, styles, level] + elif self.view == "Side View": + if index != -1: + config_settings["automated_plotting_vsecs"][index] = [url, layer, styles, level] + else: + if index != -1: + config_settings["automated_plotting_lsecs"][index] = [url, layer, styles, level] + self.autoplotSecsTreeWidget.clearSelection() + self.resize_treewidgets() + + def refresh_sig(self, config_settings): + autoplot_flights = config_settings["automated_plotting_flights"] + if self.view == "Top View": + autoplot_secs = config_settings["automated_plotting_hsecs"] + elif self.view == "Side View": + autoplot_secs = config_settings["automated_plotting_vsecs"] + else: + autoplot_secs = config_settings["automated_plotting_lsecs"] + + self.autoplotTreeWidget.clear() + for row in autoplot_flights: + item = QTreeWidgetItem(row) + self.autoplotTreeWidget.addTopLevelItem(item) + + self.autoplotSecsTreeWidget.clear() + for row in autoplot_secs: + item = QTreeWidgetItem(row) + self.autoplotSecsTreeWidget.addTopLevelItem(item) + self.autoplotSecsTreeWidget.clearSelection() + self.autoplotTreeWidget.clearSelection() + + def remove_selected_row(self, parent, treewidget, config_settings): + if treewidget.topLevelItemCount() == 0: + QMessageBox.information( + self, + "WARNING", + "Cannot remove from empty treewidget" + ) + return + selected_item = treewidget.currentItem() + if selected_item: + index = treewidget.indexOfTopLevelItem(selected_item) + if index != -1: + treewidget.takeTopLevelItem(index) + if treewidget.objectName() == "autoplotTreeWidget": + config_settings["automated_plotting_flights"].pop(index) + if treewidget.objectName() == "autoplotSecsTreeWidget": + if self.view == "Top View": + config_settings["automated_plotting_hsecs"].pop(index) + elif self.view == "Side View": + config_settings["automated_plotting_vsecs"].pop(index) + else: + config_settings["automated_plotting_lsecs"].pop(index) + else: + parent = selected_item.parent() + if parent: + parent.takeChild(parent.indexOfChild(selected_item)) + parent.refresh_signal_emit.emit() + self.resize_treewidgets() + self.stime = "" + self.etime = "" + + def combo_box_input(self, combo): + comboBoxName = combo.objectName() + currentText = combo.currentText() + if comboBoxName == "timeIntervalComboBox": + if currentText == "": + return + if self.stimeComboBox.count() == 0: + QMessageBox.information( + self, + "WARNING", + "Please select a layer first." + ) + self.timeIntervalComboBox.setCurrentIndex(0) + return + datetime1_str = self.stimeComboBox.itemText(1) + datetime2_str = self.stimeComboBox.itemText(2) + + datetime1 = datetime.strptime(datetime1_str, "%Y-%m-%dT%H:%M:%SZ") + datetime2 = datetime.strptime(datetime2_str, "%Y-%m-%dT%H:%M:%SZ") + time_difference = int((datetime2 - datetime1).total_seconds()) + time_diff = 1 + num = int(currentText.split()[0]) + if currentText.endswith("mins"): + time_diff = time_diff * 60 * num + elif currentText.endswith("hour"): + time_diff = time_diff * 3600 * num + elif currentText.endswith("hours"): + time_diff = time_diff * 3600 * num + elif currentText.endswith("days"): + time_diff = time_diff * 86400 * num + + if time_diff % time_difference != 0: + QMessageBox.information( + self, + "WARNING", + "Please select valid time interval." + ) + self.timeIntervalComboBox.setCurrentIndex(0) + return + + self.intv = currentText + elif comboBoxName == "stimeComboBox": + self.stime = currentText + elif comboBoxName == "etimeComboBox": + self.etime = currentText + + def resize_treewidgets(self): + for i in range(6): + self.autoplotTreeWidget.resizeColumnToContents(i) + for i in range(7): + self.autoplotSecsTreeWidget.resizeColumnToContents(i) + + def update_config_file(self, config_settings): + options = QFileDialog.Options() + options |= QFileDialog.DontUseNativeDialog + + file_path, _ = QFileDialog.getSaveFileName( + self, + "Save JSON File", + const.MSUI_CONFIG_PATH, + "JSON Files (*.json);;All Files (*)", + options=options + ) + if file_path: + with open(file_path, 'w') as file: + json.dump(config_settings, file, indent=4) + + QMessageBox.information( + self, + "SUCCESS", + "Configuration successfully saved." + ) diff --git a/mslib/msui/linearview.py b/mslib/msui/linearview.py index 0fdf6a254..2d413a7d1 100644 --- a/mslib/msui/linearview.py +++ b/mslib/msui/linearview.py @@ -26,15 +26,17 @@ """ from mslib.utils.config import config_loader -from PyQt5 import QtGui, QtWidgets +from PyQt5 import QtGui, QtWidgets, QtCore from mslib.msui.qt5 import ui_linearview_window as ui from mslib.msui.qt5 import ui_linearview_options as ui_opt from mslib.msui.viewwindows import MSUIMplViewWindow from mslib.msui import wms_control as wms from mslib.msui.icons import icons +from mslib.msui import autoplot_dockwidget as apd # Dock window indices. WMS = 0 +AUTOPLOT = 1 class MSUI_LV_Options_Dialog(QtWidgets.QDialog, ui_opt.Ui_LinearViewOptionsDialog): @@ -79,7 +81,13 @@ class MSUILinearViewWindow(MSUIMplViewWindow, ui.Ui_LinearWindow): """ name = "Linear View" - def __init__(self, parent=None, model=None, _id=None, tutorial_mode=False): + refresh_signal_send = QtCore.pyqtSignal() + refresh_signal_emit = QtCore.pyqtSignal() + item_selected = QtCore.pyqtSignal(str, str, str, str) + vtime_vals = QtCore.pyqtSignal([list]) + itemSecs_selected = QtCore.pyqtSignal(str) + + def __init__(self, parent=None, mainwindow=None, model=None, _id=None, config_settings=None, tutorial_mode=False): """ Set up user interface, connect signal/slots. """ @@ -92,16 +100,28 @@ def __init__(self, parent=None, model=None, _id=None, tutorial_mode=False): # Dock windows [WMS]: self.cbTools.clear() - self.cbTools.addItems(["(select to open control)", "Linear Section WMS"]) - self.docks = [None] + self.cbTools.addItems(["(select to open control)", "Linear Section WMS", "Autoplot"]) + self.docks = [None, None] self.setFlightTrackModel(model) + self.currurl = "" + self.currlayer = "" + self.currlevel = "" + self.currstyles = "" + self.currflights = "" + self.currvertical = "" + self.currvtime = "" + # Connect slots and signals. # ========================== + # ToDo review 2026 after EOL of Win 10 if we can use parent again + if mainwindow is not None: + mainwindow.refresh_signal_connect.connect(self.refresh_signal_send.emit) # Tool opener. - self.cbTools.currentIndexChanged.connect(self.openTool) + self.cbTools.currentIndexChanged.connect(lambda ind: self.openTool( + index=ind, parent=mainwindow, config_settings=config_settings)) self.lvoptionbtn.clicked.connect(self.open_settings_dialog) self.openTool(WMS + 1) @@ -112,7 +132,7 @@ def __del__(self): def update_predefined_maps(self, extra): pass - def openTool(self, index): + def openTool(self, index, parent=None, config_settings=None): """ Slot that handles requests to open tool windows. """ @@ -126,12 +146,76 @@ def openTool(self, index): waypoints_model=self.waypoints_model, view=self.mpl.canvas, wms_cache=config_loader(dataset="wms_cache")) + widget.vtime_data.connect(lambda vtime: self.valid_time_vals(vtime)) + widget.base_url_changed.connect(lambda url: self.url_val_changed(url)) + widget.layer_changed.connect(lambda layer: self.layer_val_changed(layer)) + widget.styles_changed.connect(lambda styles: self.styles_val_changed(styles)) + widget.itime_changed.connect(lambda styles: self.itime_val_changed(styles)) + widget.vtime_changed.connect(lambda styles: self.vtime_val_changed(styles)) + self.item_selected.connect(lambda url, layer, style, + level: widget.row_is_selected(url, layer, style, level, "linear")) + self.itemSecs_selected.connect(lambda vtime: widget.leftrow_is_selected(vtime)) self.mpl.canvas.waypoints_interactor.signal_get_lsec.connect(widget.call_get_lsec) + elif index == AUTOPLOT: + title = "Autoplot (Linear View)" + widget = apd.AutoplotDockWidget(parent=self, parent2=parent, + view="Linear View", config_settings=config_settings) + widget.treewidget_item_selected.connect( + lambda url, layer, style, level: self.tree_item_select(url, layer, style, level)) + widget.update_op_flight_treewidget.connect( + lambda opfl, flight: parent.update_treewidget_op_fl(opfl, flight)) else: raise IndexError("invalid control index") # Create the actual dock widget containing . self.createDockWidget(index, title, widget) + @QtCore.pyqtSlot() + def url_val_changed(self, strr): + self.currurl = strr + + @QtCore.pyqtSlot() + def layer_val_changed(self, strr): + self.currlayerobj = strr + layerstring = str(strr) + second_colon_index = layerstring.find(':', layerstring.find(':') + 1) + self.currurl = layerstring[:second_colon_index].strip() if second_colon_index != -1 else layerstring.strip() + self.currlayer = layerstring.split('|')[1].strip() if '|' in layerstring else None + + @QtCore.pyqtSlot() + def level_val_changed(self, strr): + self.currlevel = strr + + @QtCore.pyqtSlot() + def styles_val_changed(self, strr): + if strr is None: + self.currstyles = "" + else: + self.currstyles = strr + + @QtCore.pyqtSlot() + def itime_val_changed(self, strr): + self.curritime = strr + + @QtCore.pyqtSlot() + def tree_item_select(self, url, layer, style, level): + self.item_selected.emit(url, layer, style, level) + + @QtCore.pyqtSlot() + def valid_time_vals(self, vtimes_list): + self.vtime_vals.emit(vtimes_list) + + @QtCore.pyqtSlot() + def treePlot_item_select(self, section, vtime): + self.itemSecs_selected.emit(vtime) + + @QtCore.pyqtSlot() + def vtime_val_changed(self, strr): + self.currvtime = strr + + @QtCore.pyqtSlot() + def vertical_val_changed(self, strr): + self.currvertical = strr + def setFlightTrackModel(self, model): """ Set the QAbstractItemModel instance that the view displays. diff --git a/mslib/msui/msui_mainwindow.py b/mslib/msui/msui_mainwindow.py index 761e79d62..3448d75ca 100644 --- a/mslib/msui/msui_mainwindow.py +++ b/mslib/msui/msui_mainwindow.py @@ -50,8 +50,9 @@ from mslib.msui.icons import icons, python_powered from mslib.utils.qt import get_open_filenames, get_save_filename, show_popup from mslib.utils.config import read_config_file, config_loader -from mslib.utils import release_info from PyQt5 import QtGui, QtCore, QtWidgets +from mslib.utils import release_info + from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas # Add config path to PYTHONPATH so plugins located there may be found @@ -430,17 +431,9 @@ class MSUIMainWindow(QtWidgets.QMainWindow, ui.Ui_MSUIMainWindow): signal_listFlighttrack_doubleClicked = QtCore.pyqtSignal() signal_permission_revoked = QtCore.pyqtSignal(int) signal_render_new_permission = QtCore.pyqtSignal(int, str) + refresh_signal_connect = QtCore.pyqtSignal() def __init__(self, local_operations_data=None, tutorial_mode=False, *args): - """ - This method initializes the main window of the application. - It sets up the user interface, icons, menu actions, and connects signals to slots. - - :param local_operations_data: Base path used by "work asynchronously" to store operations. - :param tutorial_mode: Whether to run the application in tutorial mode. Default is False. - :param args: Additional arguments to pass to the parent class. - - """ super().__init__(*args) self.tutorial_mode = tutorial_mode self.setupUi(self) @@ -457,6 +450,14 @@ def __init__(self, local_operations_data=None, tutorial_mode=False, *args): self.config_editor = None self.local_active = True self.new_flight_track_counter = 0 + edit = editor.ConfigurationEditorWindow(self) + # ToDo review if this can replace of other config_loader() calls + self.config_for_gui = edit.last_saved + # automated_plotting_* parameters must be stored or loaded by the mssautoplot.json file + self.config_for_gui["automated_plotting_flights"].clear() + self.config_for_gui["automated_plotting_hsecs"].clear() + self.config_for_gui["automated_plotting_vsecs"].clear() + self.config_for_gui["automated_plotting_lsecs"].clear() # Reference to the flight track that is currently displayed in the views. self.active_flight_track = None @@ -597,6 +598,22 @@ def add_plugin_submenu(self, name, extension, function, pickertype, plugin_type= menu.addAction(action) setattr(self, action_name, action) + def update_treewidget_op_fl(self, op_fl, flight): + if op_fl == "operation": + for index in range(self.listOperationsMSC.count()): + item = self.listOperationsMSC.item(index) + if flight == item.operation_path: + item = self.listOperationsMSC.item(index) + self.mscolab.set_active_op_id(item) + break + else: + for index in range(self.listFlightTracks.count()): + item = self.listFlightTracks.item(index) + if flight == item.text(): + item = self.listFlightTracks.item(index) + self.activate_flight_track(item) + break + def add_import_plugins(self, picker_default): plugins = config_loader(dataset="import_plugins") for name in plugins: @@ -920,13 +937,17 @@ def create_view(self, _type, model): view_window = topview.MSUITopViewWindow(mainwindow=self, model=model, active_flighttrack=self.active_flight_track, mscolab_server_url=self.mscolab.mscolab_server_url, - token=self.mscolab.token, tutorial_mode=self.tutorial_mode) + token=self.mscolab.token, tutorial_mode=self.tutorial_mode, + config_settings=self.config_for_gui) + view_window.refresh_signal_emit.connect(self.refresh_signal_connect.emit) view_window.mpl.resize(layout['topview'][0], layout['topview'][1]) if layout["immutable"]: view_window.mpl.setFixedSize(layout['topview'][0], layout['topview'][1]) elif _type == "sideview": # Side view. - view_window = sideview.MSUISideViewWindow(model=model, tutorial_mode=self.tutorial_mode) + view_window = sideview.MSUISideViewWindow(mainwindow=self, model=model, tutorial_mode=self.tutorial_mode, + config_settings=self.config_for_gui) + view_window.refresh_signal_emit.connect(self.refresh_signal_connect.emit) view_window.mpl.resize(layout['sideview'][0], layout['sideview'][1]) if layout["immutable"]: view_window.mpl.setFixedSize(layout['sideview'][0], layout['sideview'][1]) @@ -936,7 +957,10 @@ def create_view(self, _type, model): view_window.centralwidget.resize(layout['tableview'][0], layout['tableview'][1]) elif _type == "linearview": # Linear view. - view_window = linearview.MSUILinearViewWindow(model=model, tutorial_mode=self.tutorial_mode) + view_window = linearview.MSUILinearViewWindow(mainwindow=self, model=model, + tutorial_mode=self.tutorial_mode, + config_settings=self.config_for_gui) + view_window.refresh_signal_emit.connect(self.refresh_signal_connect.emit) view_window.mpl.resize(layout['linearview'][0], layout['linearview'][1]) if layout["immutable"]: view_window.mpl.setFixedSize(layout['linearview'][0], layout['linearview'][1]) @@ -1097,8 +1121,6 @@ def closeEvent(self, event): if self.config_editor is not None: self.config_editor.restart_on_save = False self.config_editor.close() - from PyQt5 import QtTest - QtTest.QTest.qWait(5) if self.config_editor is not None: self.statusBar.showMessage("Save your config changes and try closing again") event.ignore() diff --git a/mslib/msui/multilayers.py b/mslib/msui/multilayers.py index e6537e5c9..6231b03e8 100644 --- a/mslib/msui/multilayers.py +++ b/mslib/msui/multilayers.py @@ -38,6 +38,7 @@ class Multilayers(QtWidgets.QDialog, ui.Ui_MultilayersDialog): """ needs_repopulate = QtCore.pyqtSignal() + styles_on_change = QtCore.pyqtSignal(str) def __init__(self, dock_widget): super().__init__(parent=dock_widget) @@ -374,6 +375,7 @@ def style_changed(layer): layer.style_changed() self.multilayer_clicked(layer) self.dock_widget.auto_update() + self.styles_on_change.emit(layer.style) style.currentIndexChanged.connect(lambda: style_changed(widget)) self.listLayers.setItemWidget(widget, 1, style) @@ -797,7 +799,7 @@ def style_changed(self): if self.style != self.styles[0]: self.parent.settings["saved_styles"][str(self)] = self.style else: - self.parent.settings["saved_styles"].pop(str(self)) + self.parent.settings["saved_styles"].pop(str(self), None) save_settings_qsettings("multilayers", self.parent.settings) def color_changed(self, color): diff --git a/mslib/msui/qt5/ui_mss_autoplot.py b/mslib/msui/qt5/ui_mss_autoplot.py new file mode 100644 index 000000000..7bfa49c6d --- /dev/null +++ b/mslib/msui/qt5/ui_mss_autoplot.py @@ -0,0 +1,174 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'mssau.ui' +# +# Created by: PyQt5 UI code generator 5.15.9 +# +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_AutoplotDockWidget(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(900, 243) + self.gridLayout_3 = QtWidgets.QGridLayout(Form) + self.gridLayout_3.setObjectName("gridLayout_3") + self.gridLayout_2 = QtWidgets.QGridLayout() + self.gridLayout_2.setObjectName("gridLayout_2") + self.autoplotTreeWidget = QtWidgets.QTreeWidget(Form) + self.autoplotTreeWidget.setMinimumSize(QtCore.QSize(0, 0)) + self.autoplotTreeWidget.setMaximumSize(QtCore.QSize(16777215, 140)) + self.autoplotTreeWidget.setObjectName("autoplotTreeWidget") + self.autoplotTreeWidget.header().setDefaultSectionSize(150) + self.gridLayout_2.addWidget(self.autoplotTreeWidget, 1, 0, 1, 1) + self.horizontalLayout_5 = QtWidgets.QHBoxLayout() + self.horizontalLayout_5.setObjectName("horizontalLayout_5") + self.addToAutoplotSecsButton = QtWidgets.QPushButton(Form) + self.addToAutoplotSecsButton.setObjectName("addToAutoplotSecsButton") + self.horizontalLayout_5.addWidget(self.addToAutoplotSecsButton) + self.RemoveFromAutoplotSecsButton = QtWidgets.QPushButton(Form) + self.RemoveFromAutoplotSecsButton.setObjectName("RemoveFromAutoplotSecsButton") + self.horizontalLayout_5.addWidget(self.RemoveFromAutoplotSecsButton) + self.UploadAutoplotSecsButton = QtWidgets.QPushButton(Form) + self.UploadAutoplotSecsButton.setObjectName("UploadAutoplotSecsButton") + self.horizontalLayout_5.addWidget(self.UploadAutoplotSecsButton) + self.gridLayout_2.addLayout(self.horizontalLayout_5, 2, 1, 1, 1) + self.selectConfigButton = QtWidgets.QPushButton(Form) + self.selectConfigButton.setObjectName("selectConfigButton") + self.gridLayout_2.addWidget(self.selectConfigButton, 0, 0, 1, 1) + self.horizontalLayout_4 = QtWidgets.QHBoxLayout() + self.horizontalLayout_4.setObjectName("horizontalLayout_4") + self.addToAutoplotButton = QtWidgets.QPushButton(Form) + self.addToAutoplotButton.setObjectName("addToAutoplotButton") + self.horizontalLayout_4.addWidget(self.addToAutoplotButton) + self.RemoveFromAutoplotButton = QtWidgets.QPushButton(Form) + self.RemoveFromAutoplotButton.setObjectName("RemoveFromAutoplotButton") + self.horizontalLayout_4.addWidget(self.RemoveFromAutoplotButton) + self.UploadAutoplotButton = QtWidgets.QPushButton(Form) + self.UploadAutoplotButton.setObjectName("UploadAutoplotButton") + self.horizontalLayout_4.addWidget(self.UploadAutoplotButton) + self.gridLayout_2.addLayout(self.horizontalLayout_4, 2, 0, 1, 1) + self.horizontalLayout_2 = QtWidgets.QHBoxLayout() + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + self.verticalLayout_10 = QtWidgets.QVBoxLayout() + self.verticalLayout_10.setObjectName("verticalLayout_10") + self.timeIntervalLabel = QtWidgets.QLabel(Form) + self.timeIntervalLabel.setObjectName("timeIntervalLabel") + self.verticalLayout_10.addWidget(self.timeIntervalLabel) + self.timeIntervalComboBox = QtWidgets.QComboBox(Form) + self.timeIntervalComboBox.setObjectName("timeIntervalComboBox") + self.timeIntervalComboBox.addItem("") + self.timeIntervalComboBox.setItemText(0, "") + self.timeIntervalComboBox.addItem("") + self.timeIntervalComboBox.addItem("") + self.timeIntervalComboBox.addItem("") + self.timeIntervalComboBox.addItem("") + self.timeIntervalComboBox.addItem("") + self.timeIntervalComboBox.addItem("") + self.timeIntervalComboBox.addItem("") + self.timeIntervalComboBox.addItem("") + self.timeIntervalComboBox.addItem("") + self.timeIntervalComboBox.addItem("") + self.timeIntervalComboBox.addItem("") + self.timeIntervalComboBox.addItem("") + self.timeIntervalComboBox.addItem("") + self.verticalLayout_10.addWidget(self.timeIntervalComboBox) + self.horizontalLayout_2.addLayout(self.verticalLayout_10) + self.verticalLayout = QtWidgets.QVBoxLayout() + self.verticalLayout.setObjectName("verticalLayout") + spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.verticalLayout.addItem(spacerItem) + self.downloadPushButton = QtWidgets.QPushButton(Form) + self.downloadPushButton.setMaximumSize(QtCore.QSize(16777215, 24)) + self.downloadPushButton.setObjectName("downloadPushButton") + self.verticalLayout.addWidget(self.downloadPushButton) + self.horizontalLayout_2.addLayout(self.verticalLayout) + self.verticalLayout_3 = QtWidgets.QVBoxLayout() + self.verticalLayout_3.setObjectName("verticalLayout_3") + spacerItem1 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.verticalLayout_3.addItem(spacerItem1) + self.updateConfigFile = QtWidgets.QPushButton(Form) + self.updateConfigFile.setMaximumSize(QtCore.QSize(16777215, 24)) + self.updateConfigFile.setObjectName("updateConfigFile") + self.verticalLayout_3.addWidget(self.updateConfigFile) + self.horizontalLayout_2.addLayout(self.verticalLayout_3) + self.gridLayout_2.addLayout(self.horizontalLayout_2, 3, 1, 1, 1) + self.horizontalLayout_9 = QtWidgets.QHBoxLayout() + self.horizontalLayout_9.setObjectName("horizontalLayout_9") + self.verticalLayout_2 = QtWidgets.QVBoxLayout() + self.verticalLayout_2.setObjectName("verticalLayout_2") + self.stimeLabel = QtWidgets.QLabel(Form) + self.stimeLabel.setObjectName("stimeLabel") + self.verticalLayout_2.addWidget(self.stimeLabel) + self.stimeComboBox = QtWidgets.QComboBox(Form) + self.stimeComboBox.setEditable(True) + self.stimeComboBox.setObjectName("stimeComboBox") + self.verticalLayout_2.addWidget(self.stimeComboBox) + self.horizontalLayout_9.addLayout(self.verticalLayout_2) + self.verticalLayout_8 = QtWidgets.QVBoxLayout() + self.verticalLayout_8.setObjectName("verticalLayout_8") + self.etimeLabel = QtWidgets.QLabel(Form) + self.etimeLabel.setObjectName("etimeLabel") + self.verticalLayout_8.addWidget(self.etimeLabel) + self.etimeComboBox = QtWidgets.QComboBox(Form) + self.etimeComboBox.setEditable(True) + self.etimeComboBox.setObjectName("etimeComboBox") + self.verticalLayout_8.addWidget(self.etimeComboBox) + self.horizontalLayout_9.addLayout(self.verticalLayout_8) + self.gridLayout_2.addLayout(self.horizontalLayout_9, 3, 0, 1, 1) + self.autoplotSecsTreeWidget = QtWidgets.QTreeWidget(Form) + self.autoplotSecsTreeWidget.setMinimumSize(QtCore.QSize(0, 0)) + self.autoplotSecsTreeWidget.setMaximumSize(QtCore.QSize(16777215, 140)) + self.autoplotSecsTreeWidget.setObjectName("autoplotSecsTreeWidget") + self.autoplotSecsTreeWidget.header().setDefaultSectionSize(200) + self.gridLayout_2.addWidget(self.autoplotSecsTreeWidget, 1, 1, 1, 1) + self.gridLayout_3.addLayout(self.gridLayout_2, 0, 0, 1, 1) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + _translate = QtCore.QCoreApplication.translate + Form.setWindowTitle(_translate("Form", "MSSAUTOPLOT")) + self.autoplotTreeWidget.headerItem().setText(0, _translate("Form", "Flight")) + self.autoplotTreeWidget.headerItem().setText(1, _translate("Form", "Map Sections")) + self.autoplotTreeWidget.headerItem().setText(2, _translate("Form", "Vertical")) + self.autoplotTreeWidget.headerItem().setText(3, _translate("Form", "Filename")) + self.autoplotTreeWidget.headerItem().setText(4, _translate("Form", "Initial Time")) + self.autoplotTreeWidget.headerItem().setText(5, _translate("Form", "Valid Time")) + self.addToAutoplotSecsButton.setText(_translate("Form", "Add")) + self.RemoveFromAutoplotSecsButton.setText(_translate("Form", "Remove")) + self.UploadAutoplotSecsButton.setText(_translate("Form", "Update")) + self.selectConfigButton.setText(_translate("Form", "Select Configuration File")) + self.addToAutoplotButton.setText(_translate("Form", "Add")) + self.RemoveFromAutoplotButton.setText(_translate("Form", "Remove")) + self.UploadAutoplotButton.setText(_translate("Form", "Update")) + self.timeIntervalLabel.setText(_translate("Form", "Time Interval")) + self.timeIntervalComboBox.setItemText(1, _translate("Form", "1 min")) + self.timeIntervalComboBox.setItemText(2, _translate("Form", "5 min")) + self.timeIntervalComboBox.setItemText(3, _translate("Form", "10 min")) + self.timeIntervalComboBox.setItemText(4, _translate("Form", "15 min")) + self.timeIntervalComboBox.setItemText(5, _translate("Form", "30 min")) + self.timeIntervalComboBox.setItemText(6, _translate("Form", "1 hour")) + self.timeIntervalComboBox.setItemText(7, _translate("Form", "2 hours")) + self.timeIntervalComboBox.setItemText(8, _translate("Form", "3 hours")) + self.timeIntervalComboBox.setItemText(9, _translate("Form", "6 hours")) + self.timeIntervalComboBox.setItemText(10, _translate("Form", "12 hours")) + self.timeIntervalComboBox.setItemText(11, _translate("Form", "24 hours")) + self.timeIntervalComboBox.setItemText(12, _translate("Form", "2 days")) + self.timeIntervalComboBox.setItemText(13, _translate("Form", "7 days")) + self.downloadPushButton.setText(_translate("Form", "Download Plots")) + self.updateConfigFile.setText(_translate("Form", "Update/Create Configuration File")) + self.stimeLabel.setText(_translate("Form", "Start Time")) + self.etimeLabel.setText(_translate("Form", "End Time")) + self.autoplotSecsTreeWidget.headerItem().setText(0, _translate("Form", "URL")) + self.autoplotSecsTreeWidget.headerItem().setText(1, _translate("Form", "Layers")) + self.autoplotSecsTreeWidget.headerItem().setText(2, _translate("Form", "Styles")) + self.autoplotSecsTreeWidget.headerItem().setText(3, _translate("Form", "Levels")) + self.autoplotSecsTreeWidget.headerItem().setText(4, _translate("Form", "Start Time")) + self.autoplotSecsTreeWidget.headerItem().setText(5, _translate("Form", "End Time")) + self.autoplotSecsTreeWidget.headerItem().setText(6, _translate("Form", "Time Interval")) diff --git a/mslib/msui/sideview.py b/mslib/msui/sideview.py index 9be6da715..f388ed597 100644 --- a/mslib/msui/sideview.py +++ b/mslib/msui/sideview.py @@ -29,7 +29,7 @@ import logging import functools -from PyQt5 import QtGui, QtWidgets +from PyQt5 import QtGui, QtWidgets, QtCore from mslib.msui.qt5 import ui_sideview_window as ui from mslib.msui.qt5 import ui_sideview_options as ui_opt @@ -39,9 +39,11 @@ from mslib.utils import thermolib from mslib.utils.config import config_loader from mslib.utils.units import units, convert_to +from mslib.msui import autoplot_dockwidget as apd # Dock window indices. WMS = 0 +AUTOPLOT = 1 class MSUI_SV_OptionsDialog(QtWidgets.QDialog, ui_opt.Ui_SideViewOptionsDialog): @@ -252,7 +254,13 @@ class MSUISideViewWindow(MSUIMplViewWindow, ui.Ui_SideViewWindow): """ name = "Side View" - def __init__(self, parent=None, model=None, _id=None, tutorial_mode=False): + refresh_signal_send = QtCore.pyqtSignal() + refresh_signal_emit = QtCore.pyqtSignal() + item_selected = QtCore.pyqtSignal(str, str, str, str) + vtime_vals = QtCore.pyqtSignal([list]) + itemSecs_selected = QtCore.pyqtSignal(str) + + def __init__(self, parent=None, mainwindow=None, model=None, _id=None, config_settings=None, tutorial_mode=False): """ Set up user interface, connect signal/slots. """ @@ -263,19 +271,33 @@ def __init__(self, parent=None, model=None, _id=None, tutorial_mode=False): self.settings_tag = "sideview" # Dock windows [WMS]: self.cbTools.clear() - self.cbTools.addItems(["(select to open control)", "Vertical Section WMS"]) - self.docks = [None] + self.cbTools.addItems(["(select to open control)", "Vertical Section WMS", "Autoplot"]) + self.docks = [None, None] self.setFlightTrackModel(model) + self.currurl = "" + self.currlayer = "" + self.currlevel = self.getView().get_settings()["vertical_axis"] + self.currstyles = "" + self.currflights = "" + self.currvertical = ', '.join(map(str, self.getView().get_settings()["vertical_extent"])) + self.currvtime = "" + self.curritime = "" + self.currlayerobj = None + # Connect slots and signals. # ========================== + # ToDo review 2026 after EOL of Win 10 if we can use parent again + if mainwindow is not None: + mainwindow.refresh_signal_connect.connect(self.refresh_signal_send.emit) # Buttons to set sideview options. self.btOptions.clicked.connect(self.open_settings_dialog) # Tool opener. - self.cbTools.currentIndexChanged.connect(self.openTool) + self.cbTools.currentIndexChanged.connect(lambda ind: self.openTool( + index=ind, parent=mainwindow, config_settings=config_settings)) self.openTool(WMS + 1) def __del__(self): @@ -284,7 +306,7 @@ def __del__(self): def update_predefined_maps(self, extra): pass - def openTool(self, index): + def openTool(self, index, parent=None, config_settings=None): """ Slot that handles requests to open tool windows. """ @@ -298,12 +320,72 @@ def openTool(self, index): waypoints_model=self.waypoints_model, view=self.mpl.canvas, wms_cache=config_loader(dataset="wms_cache")) + widget.vtime_data.connect(lambda vtime: self.valid_time_vals(vtime)) + widget.base_url_changed.connect(lambda url: self.url_val_changed(url)) + widget.layer_changed.connect(lambda layer: self.layer_val_changed(layer)) + widget.styles_changed.connect(lambda styles: self.styles_val_changed(styles)) + widget.itime_changed.connect(lambda styles: self.itime_val_changed(styles)) + widget.vtime_changed.connect(lambda styles: self.vtime_val_changed(styles)) + self.item_selected.connect(lambda url, layer, style, + level: widget.row_is_selected(url, layer, style, level, "side")) + self.itemSecs_selected.connect(lambda vtime: widget.leftrow_is_selected(vtime)) self.mpl.canvas.waypoints_interactor.signal_get_vsec.connect(widget.call_get_vsec) + elif index == AUTOPLOT: + title = "Autoplot (Side View)" + widget = apd.AutoplotDockWidget(parent=self, parent2=parent, + view="Side View", config_settings=config_settings) + widget.treewidget_item_selected.connect( + lambda url, layer, style, level: self.tree_item_select(url, layer, style, level)) + widget.update_op_flight_treewidget.connect( + lambda opfl, flight: parent.update_treewidget_op_fl(opfl, flight)) else: raise IndexError("invalid control index") # Create the actual dock widget containing . self.createDockWidget(index, title, widget) + @QtCore.pyqtSlot() + def url_val_changed(self, strr): + self.currurl = strr + + @QtCore.pyqtSlot() + def layer_val_changed(self, strr): + self.currlayerobj = strr + layerstring = str(strr) + second_colon_index = layerstring.find(':', layerstring.find(':') + 1) + self.currurl = layerstring[:second_colon_index].strip() if second_colon_index != -1 else layerstring.strip() + self.currlayer = layerstring.split('|')[1].strip() if '|' in layerstring else None + + @QtCore.pyqtSlot() + def tree_item_select(self, url, layer, style, level): + self.item_selected.emit(url, layer, style, level) + + @QtCore.pyqtSlot() + def level_val_changed(self, strr): + self.currlevel = strr + + @QtCore.pyqtSlot() + def styles_val_changed(self, strr): + if strr is None: + self.currstyles = "" + else: + self.currstyles = strr + + @QtCore.pyqtSlot() + def vtime_val_changed(self, strr): + self.currvtime = strr + + @QtCore.pyqtSlot() + def itime_val_changed(self, strr): + self.curritime = strr + + @QtCore.pyqtSlot() + def valid_time_vals(self, vtimes_list): + self.vtime_vals.emit(vtimes_list) + + @QtCore.pyqtSlot() + def treePlot_item_select(self, section, vtime): + self.itemSecs_selected.emit(vtime) + def setFlightTrackModel(self, model): """ Set the QAbstractItemModel instance that the view displays. @@ -317,9 +399,13 @@ def open_settings_dialog(self): Slot to open a dialog that lets the user specify sideview options. """ settings = self.getView().get_settings() + self.currvertical = ', '.join(map(str, settings["vertical_extent"])) + self.currlevel = settings["vertical_axis"] dlg = MSUI_SV_OptionsDialog(parent=self, settings=settings) dlg.setModal(True) if dlg.exec_() == QtWidgets.QDialog.Accepted: settings = dlg.get_settings() self.getView().set_settings(settings, save=True) + self.currvertical = ', '.join(map(str, settings["vertical_extent"])) + self.currlevel = settings["vertical_axis"] dlg.destroy() diff --git a/mslib/msui/topview.py b/mslib/msui/topview.py index 0655f7d07..a162fe891 100644 --- a/mslib/msui/topview.py +++ b/mslib/msui/topview.py @@ -44,6 +44,7 @@ from mslib.msui import airdata_dockwidget as ad from mslib.msui import multiple_flightpath_dockwidget as mf from mslib.msui import flighttrack as ft +from mslib.msui import autoplot_dockwidget as apd from mslib.msui.icons import icons from mslib.msui.flighttrack import Waypoint @@ -54,6 +55,7 @@ KMLOVERLAY = 3 AIRDATA = 4 MULTIPLEFLIGHTPATH = 5 +AUTOPLOT = 6 class MSUI_TV_MapAppearanceDialog(QtWidgets.QDialog, ui_ma.Ui_MapAppearanceDialog): @@ -185,9 +187,16 @@ class MSUITopViewWindow(MSUIMplViewWindow, ui.Ui_TopViewWindow): signal_listFlighttrack_doubleClicked = QtCore.pyqtSignal() signal_permission_revoked = QtCore.pyqtSignal(int) signal_render_new_permission = QtCore.pyqtSignal(int, str) + sections_changed = QtCore.pyqtSignal(str) + refresh_signal_emit = QtCore.pyqtSignal() + refresh_signal_send = QtCore.pyqtSignal() + item_selected = QtCore.pyqtSignal(str, str, str, str) + itemSecs_selected = QtCore.pyqtSignal(str) + vtime_vals = QtCore.pyqtSignal([list]) def __init__(self, parent=None, mainwindow=None, model=None, _id=None, - active_flighttrack=None, mscolab_server_url=None, token=None, tutorial_mode=False): + active_flighttrack=None, mscolab_server_url=None, token=None, config_settings=None, + tutorial_mode=False): """ Set up user interface, connect signal/slots. """ @@ -195,6 +204,7 @@ def __init__(self, parent=None, mainwindow=None, model=None, _id=None, logging.debug(_id) self.settings_tag = "topview" self.tutorial_mode = tutorial_mode + # ToDo review 2026 after EOL of Win 10 if we can use parent again self.mainwindow_signal_login_mscolab = mainwindow.signal_login_mscolab self.mainwindow_signal_logout_mscolab = mainwindow.signal_logout_mscolab self.mainwindow_signal_listFlighttrack_doubleClicked = mainwindow.signal_listFlighttrack_doubleClicked @@ -210,7 +220,7 @@ def __init__(self, parent=None, mainwindow=None, model=None, _id=None, self.setWindowIcon(QtGui.QIcon(icons('64x64'))) # Dock windows [WMS, Satellite, Trajectories, Remote Sensing, KML Overlay, Multiple Flightpath]: - self.docks = [None, None, None, None, None, None] + self.docks = [None, None, None, None, None, None, None] # Initialise the GUI elements (map view, items of combo boxes etc.). self.setup_top_view() @@ -228,8 +238,21 @@ def __init__(self, parent=None, mainwindow=None, model=None, _id=None, self.mscolab_server_url = mscolab_server_url self.token = token + self.currurl = "" + self.currlayer = "" + self.currlevel = "" + self.currstyles = "" + self.currsections = "" + self.currflights = "" + self.curritime = "" + self.currvtime = "" + self.currlayerobj = None + # Connect slots and signals. # ========================== + # ToDo review 2026 after EOL of Win 10 if we can use parent again + if mainwindow is not None: + mainwindow.refresh_signal_connect.connect(self.refresh_signal_send.emit) # Map controls. self.btMapRedraw.clicked.connect(self.mpl.canvas.redraw_map) @@ -242,7 +265,8 @@ def __init__(self, parent=None, mainwindow=None, model=None, _id=None, self.btRoundtrip.clicked.connect(self.make_roundtrip) # Tool opener. - self.cbTools.currentIndexChanged.connect(self.openTool) + self.cbTools.currentIndexChanged.connect(lambda ind: self.openTool( + index=ind, parent=mainwindow, config_settings=config_settings)) if mainwindow is not None: # Update flighttrack @@ -289,8 +313,8 @@ def setup_top_view(self): Initialise GUI elements. (This method is called before signals/slots are connected). """ - toolitems = ["(select to open control)", "Web Map Service", "Satellite Tracks", "Remote Sensing", "KML Overlay", - "Airports/Airspaces", "Multiple Flightpath"] + toolitems = ["(select to open control)", "Web Map Service", "Satellite Tracks", "Remote Sensing", + "KML Overlay", "Airports/Airspaces", "Multiple Flightpath", "Autoplot"] self.cbTools.clear() self.cbTools.addItems(toolitems) @@ -323,7 +347,7 @@ def update_predefined_maps(self, extra=None): if current_map_key in predefined_map_sections.keys(): self.cbChangeMapSection.setCurrentText(current_map_key) - def openTool(self, index): + def openTool(self, index, parent=None, config_settings=None): """ Slot that handles requests to open control windows. """ @@ -336,6 +360,16 @@ def openTool(self, index): default_WMS=config_loader(dataset="default_WMS"), view=self.mpl.canvas, wms_cache=config_loader(dataset="wms_cache")) + widget.vtime_data.connect(lambda vtime: self.valid_time_vals(vtime)) + widget.base_url_changed.connect(lambda url: self.url_val_changed(url)) + widget.layer_changed.connect(lambda layer: self.layer_val_changed(layer)) + widget.on_level_changed.connect(lambda level: self.level_val_changed(level)) + widget.styles_changed.connect(lambda styles: self.styles_val_changed(styles)) + widget.itime_changed.connect(lambda styles: self.itime_val_changed(styles)) + widget.vtime_changed.connect(lambda styles: self.vtime_val_changed(styles)) + self.item_selected.connect(lambda url, layer, style, + level: widget.row_is_selected(url, layer, style, level, "top")) + self.itemSecs_selected.connect(lambda vtime: widget.leftrow_is_selected(vtime)) widget.signal_disable_cbs.connect(self.disable_cbs) widget.signal_enable_cbs.connect(self.enable_cbs) elif index == SATELLITE: @@ -370,6 +404,16 @@ def openTool(self, index): lambda op_id, path: self.signal_render_new_permission.emit(op_id, path)) if self.active_op_id is not None: self.signal_activate_operation.emit(self.active_op_id) + elif index == AUTOPLOT: + title = "Autoplot (Top View)" + widget = apd.AutoplotDockWidget(parent=self, parent2=parent, + view="Top View", config_settings=config_settings) + widget.treewidget_item_selected.connect( + lambda url, layer, style, level: self.tree_item_select(url, layer, style, level)) + widget.autoplot_treewidget_item_selected.connect( + lambda section, vtime: self.treePlot_item_select(section, vtime)) + widget.update_op_flight_treewidget.connect( + lambda opfl, flight: parent.update_treewidget_op_fl(opfl, flight)) else: raise IndexError("invalid control index") @@ -384,6 +428,52 @@ def disable_cbs(self): def enable_cbs(self): self.wms_connected = False + @QtCore.pyqtSlot() + def tree_item_select(self, url, layer, style, level): + self.item_selected.emit(url, layer, style, level) + + @QtCore.pyqtSlot() + def treePlot_item_select(self, section, vtime): + self.cbChangeMapSection.setCurrentText(section) + self.changeMapSection() + self.itemSecs_selected.emit(vtime) + + @QtCore.pyqtSlot() + def url_val_changed(self, strr): + self.currurl = strr + + @QtCore.pyqtSlot() + def valid_time_vals(self, vtimes_list): + self.vtime_vals.emit(vtimes_list) + + @QtCore.pyqtSlot() + def layer_val_changed(self, strr): + self.currlayerobj = strr + layerstring = str(strr) + second_colon_index = layerstring.find(':', layerstring.find(':') + 1) + self.currurl = layerstring[:second_colon_index].strip() if second_colon_index != -1 else layerstring.strip() + self.currlayer = layerstring.split('|')[1].strip() if '|' in layerstring else None + + @QtCore.pyqtSlot() + def level_val_changed(self, strr): + self.currlevel = strr.split(' ')[0] + + @QtCore.pyqtSlot() + def styles_val_changed(self, strr): + if strr is None or not str(strr).strip(): + self.currstyles = "" + else: + split_strr = str(strr).strip().split() + self.currstyles = split_strr[0].strip() if split_strr else "" + + @QtCore.pyqtSlot() + def itime_val_changed(self, strr): + self.curritime = strr + + @QtCore.pyqtSlot() + def vtime_val_changed(self, strr): + self.currvtime = strr + def changeMapSection(self, index=0, only_kwargs=False): """ Change the current map section to one of the predefined regions. @@ -394,22 +484,23 @@ def changeMapSection(self, index=0, only_kwargs=False): dataset="predefined_map_sections") current_map = predefined_map_sections.get( current_map_key, {"CRS": current_map_key, "map": {}}) - proj_params = get_projection_params(current_map["CRS"]) - - # Create a keyword arguments dictionary for basemap that contains - # the projection parameters. - kwargs = dict(current_map["map"]) - kwargs.update({"CRS": current_map["CRS"], "BBOX_UNITS": proj_params["bbox"], - "OPERATION_NAME": self.waypoints_model.name}) - kwargs.update(proj_params["basemap"]) - - if only_kwargs: - # Return kwargs dictionary and do NOT redraw the map. - return kwargs - - logging.debug("switching to map section '%s' - '%s'", current_map_key, kwargs) - self.mpl.canvas.redraw_map(kwargs) - self.mpl.navbar.clear_history() + + if current_map["CRS"] != "": + proj_params = get_projection_params(current_map["CRS"]) + # Create a keyword arguments dictionary for basemap that contains + # the projection parameters. + kwargs = dict(current_map["map"]) + kwargs.update({"CRS": current_map["CRS"], "BBOX_UNITS": proj_params["bbox"], + "OPERATION_NAME": self.waypoints_model.name}) + kwargs.update(proj_params["basemap"]) + + if only_kwargs: + # Return kwargs dictionary and do NOT redraw the map. + return kwargs + + logging.debug("switching to map section '%s' - '%s'", current_map_key, kwargs) + self.mpl.canvas.redraw_map(kwargs) + self.mpl.navbar.clear_history() def setIdentifier(self, identifier): super().setIdentifier(identifier) diff --git a/mslib/msui/ui/ui_mss_autoplot.ui b/mslib/msui/ui/ui_mss_autoplot.ui new file mode 100644 index 000000000..4f1a2d952 --- /dev/null +++ b/mslib/msui/ui/ui_mss_autoplot.ui @@ -0,0 +1,374 @@ + + + Form + + + + 0 + 0 + 900 + 243 + + + + MSSAUTOPLOT + + + + + + + + + 0 + 0 + + + + + 16777215 + 140 + + + + 150 + + + + Flight + + + + + Map Sections + + + + + Vertical + + + + + Filename + + + + + Initial Time + + + + + Valid Time + + + + + + + + + + Add + + + + + + + Remove + + + + + + + Update + + + + + + + + + Select Configuration File + + + + + + + + + Add + + + + + + + Remove + + + + + + + Update + + + + + + + + + + + + + Time Interval + + + + + + + + + + + + + 1 min + + + + + 5 min + + + + + 10 min + + + + + 15 min + + + + + 30 min + + + + + 1 hour + + + + + 2 hours + + + + + 3 hours + + + + + 6 hours + + + + + 12 hours + + + + + 24 hours + + + + + 2 days + + + + + 7 days + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + 16777215 + 24 + + + + Download Plots + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + 16777215 + 24 + + + + Update/Create Configuration File + + + + + + + + + + + + + + + Start Time + + + + + + + true + + + + + + + + + + + End Time + + + + + + + true + + + + + + + + + + + + 0 + 0 + + + + + 16777215 + 140 + + + + 200 + + + + URL + + + + + Layers + + + + + Styles + + + + + Levels + + + + + Start Time + + + + + End Time + + + + + Time Interval + + + + + + + + + + + diff --git a/mslib/msui/wms_control.py b/mslib/msui/wms_control.py index d3fdb0502..4a7fadcd4 100644 --- a/mslib/msui/wms_control.py +++ b/mslib/msui/wms_control.py @@ -404,6 +404,13 @@ class WMSControlWidget(QtWidgets.QWidget, ui.Ui_WMSDockWidget): signal_disable_cbs = QtCore.pyqtSignal(name="disable_cbs") signal_enable_cbs = QtCore.pyqtSignal(name="enable_cbs") image_displayed = QtCore.pyqtSignal() + base_url_changed = QtCore.pyqtSignal(str) + layer_changed = QtCore.pyqtSignal(Layer) + on_level_changed = QtCore.pyqtSignal(str) + styles_changed = QtCore.pyqtSignal(str) + itime_changed = QtCore.pyqtSignal(str) + vtime_changed = QtCore.pyqtSignal(str) + vtime_data = QtCore.pyqtSignal([list]) def __init__(self, parent=None, default_WMS=None, wms_cache=None, view=None): """ @@ -416,6 +423,9 @@ def __init__(self, parent=None, default_WMS=None, wms_cache=None, view=None): self.setupUi(self) self.view = view + self.layer_name = None + self.style_name = None + self.current_sel_layer = None # Multilayering things self.multilayers = Multilayers(self) @@ -484,6 +494,7 @@ def __init__(self, parent=None, default_WMS=None, wms_cache=None, view=None): # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ self.multilayers.btGetCapabilities.clicked.connect(self.get_capabilities) self.multilayers.pbViewCapabilities.clicked.connect(self.view_capabilities) + self.multilayers.styles_on_change.connect(lambda styles: self.style_changed_now(styles)) self.btClearMap.clicked.connect(self.clear_map) @@ -536,6 +547,177 @@ def __init__(self, parent=None, default_WMS=None, wms_cache=None, view=None): self.multilayers.cbWMS_URL.setCurrentIndex(0) self.wms_url_changed(self.multilayers.cbWMS_URL.currentText()) + def row_is_selected(self, url, layer, styles, level, view_name): + if url not in self.multilayers.layers: + self.layer_name = layer + self.style_name = styles + self.update_url_layer_styles(url_name=url, layer_name=layer, style_name=styles, level=level) + self.populate_ui(update_level=level) + else: + # Get coordinate reference system and bounding box from the map + # object in the view. + + if view_name == "top": + crs = self.view.get_crs() + elif view_name == "side": + crs = "VERT:LOGP" + else: + crs = "LINE:1" + bbox = self.view.getBBOX() + + # Determine the current size of the vertical section plot on the + # screen in pixels. The image will be retrieved in this size. + width, height = self.view.get_plot_size_in_px() + + for index in range(self.multilayers.listLayers.topLevelItemCount()): + top_item = self.multilayers.listLayers.topLevelItem(index) + # Iterate over the children of the top-level item + for i in range(top_item.childCount()): + child_item = top_item.child(i) + + # Process both columns of the child item + + for column in range(child_item.columnCount()): + + if child_item.text(column).endswith(layer): + self.current_sel_layer = child_item + + self.multilayers.threads += 1 + + if self.multilayers.carry_parameters["level"] in self.current_sel_layer.get_levels(): + self.current_sel_layer.set_level(self.multilayers.carry_parameters["level"]) + if self.multilayers.carry_parameters["itime"] in self.current_sel_layer.get_itimes(): + self.current_sel_layer.set_itime(self.multilayers.carry_parameters["itime"]) + if self.multilayers.carry_parameters["vtime"] in self.current_sel_layer.get_vtimes(): + self.current_sel_layer.set_vtime(self.multilayers.carry_parameters["vtime"]) + if self.multilayers.current_layer != self.current_sel_layer or list( + self.multilayers.layers[self.current_sel_layer.wms_name] + ).index(self.current_sel_layer.text(0)) == 2: + self.multilayers.current_layer = self.current_sel_layer + self.multilayers.listLayers.setCurrentItem(self.current_sel_layer) + index = self.multilayers.cbWMS_URL.findText(self.current_sel_layer.get_wms().url) + if index != -1 and index != self.multilayers.cbWMS_URL.currentIndex(): + self.multilayers.cbWMS_URL.setCurrentIndex(index) + + self.multilayers.threads -= 1 + styles_name = str(styles).strip().split()[0].strip() + + def style_changed(layer): + for style in self.current_sel_layer.styles: + this_style = str(style) + this_style = this_style.strip().split()[0] + this_style = this_style.strip() + if this_style.startswith(styles_name): + self.current_sel_layer.style = style + self.current_sel_layer.style_changed() + self.multilayers.multilayer_clicked(self.current_sel_layer) + self.current_sel_layer.parent.dock_widget.auto_update() + break + + layer.style_changed() + self.multilayers.multilayer_clicked(layer) + self.multilayers.dock_widget.auto_update() + self.multilayers.styles_on_change.emit(layer.style) + + style_changed(self.current_sel_layer) + + if view_name == "top": + layers = [self.current_sel_layer] + args = [] + for i, layer_itr in enumerate(layers): + transparent = self.cbTransparent.isChecked() if i == 0 else True + bbox_tmp = tuple(bbox) + wms = self.multilayers.layers[layer_itr.wms_name]["wms"] + if wms.version == "1.3.0" and crs.startswith("EPSG") and int(crs[5:]) in axisorder_yx: + bbox_tmp = (bbox[1], bbox[0], bbox[3], bbox[2]) + args.extend(self.retrieve_image(layer_itr, crs, bbox_tmp, None, width, height, transparent)) + + self.fetch.emit(args) + elif view_name == "side": + crs = "VERT:LOGP" + bbox = self.view.getBBOX() + + # Get lat/lon coordinates of flight track and convert to string for URL. + path_string = "" + for waypoint in self.waypoints_model.all_waypoint_data(): + path_string += f"{waypoint.lat:.2f},{waypoint.lon:.2f}," + path_string = path_string[:-1] + + # Determine the current size of the vertical section plot on the + # screen in pixels. The image will be retrieved in this size. + width, height = self.view.get_plot_size_in_px() + layers = [self.current_sel_layer] + # Retrieve the image. + if not layers: + layers = [self.multilayers.get_current_layer()] + layers.sort(key=lambda x: self.multilayers.get_multilayer_priority(x)) + + args = [] + for i, layer in enumerate(layers): + transparent = self.cbTransparent.isChecked() if i == 0 else True + args.extend(self.retrieve_image(layer, crs, bbox, path_string, width, height, transparent)) + + self.fetch.emit(args) + else: + crs = "LINE:1" + bbox = self.view.getBBOX() + + # Get lat/lon/alt coordinates of flight track and convert to string for URL. + path_string = "" + for waypoint in self.waypoints_model.all_waypoint_data(): + path_string += f"{waypoint.lat:.2f},{waypoint.lon:.2f},{waypoint.pressure}," + path_string = path_string[:-1] + + layers = [self.current_sel_layer] + # Retrieve the image. + if not layers: + layers = [self.multilayers.get_current_layer()] + layers.sort(key=lambda x: self.multilayers.get_multilayer_priority(x)) + + args = [] + for i, layer in enumerate(layers): + args.extend(self.retrieve_image(layer, crs, bbox, path_string, + transparent=False, format="text/xml")) + + self.fetch.emit(args) + + self.prefet = WMSMapFetcher(self.wms_cache) + self.prefet.moveToThread(self.thread_prefetch) + self.prefetch.connect(self.prefet.fetch_maps) # implicitly uses a queued connection + + self.fet = WMSMapFetcher(self.wms_cache) + self.fet.moveToThread(self.thread_fetch) + self.fetch.connect(self.fet.fetch_maps) # implicitly uses a queued connection + self.fet.finished.connect(self.continue_retrieve_image) # implicitly uses a queued connection + self.fet.exception.connect(self.display_exception) # implicitly uses a queued connection + self.fet.started_request.connect(self.display_progress_dialog) # implicitly uses a queued connection + self.populate_ui(update_level=level) + for i in range(self.cbLevel.count()): + item_text = self.cbLevel.itemText(i) + if item_text.startswith(level): + self.cbLevel.setCurrentText(item_text) + self.level_changed() + break + + def leftrow_is_selected(self, vtime): + if vtime is not None: + self.cbValidTime.setCurrentText(vtime) + self.valid_time_changed() + + layer = self.multilayers.get_current_layer() + crs = layer.get_allowed_crs() + if crs and \ + self.parent() is not None and \ + self.parent().parent() is not None: + logging.debug("Layer offers '%s' projections.", crs) + extra = [_code for _code in crs if _code.startswith("EPSG")] + extra = [_code for _code in sorted(extra) if _code[5:] in basemap.epsg_dict] + logging.debug("Selected '%s' for Combobox.", extra) + self.parent().parent().update_predefined_maps(extra) + + def style_changed_now(self, style): + self.styles_changed.emit(style) + def __del__(self): """Destructor. """ @@ -554,7 +736,7 @@ def get_all_maps(self, disregard_current=False): else: self.get_map([self.multilayers.get_current_layer()]) - def initialise_wms(self, base_url, version="1.3.0"): + def initialise_wms(self, base_url, version="1.3.0", level=None): """Initialises a MSUIWebMapService object with the specified base_url. If the web server returns a '401 Unauthorized', prompt the user for @@ -595,7 +777,7 @@ def on_success(wms): self.multilayers.cbWMS_URL.setEditText(base_url) save_settings_qsettings('wms', {'recent_wms_url': base_url}) - self.activate_wms(wms) + self.activate_wms(wms, level=level) WMS_SERVICE_CACHE[wms.url] = wms self.cpdlg.close() @@ -712,14 +894,53 @@ def clear_map(self): logging.debug("enabling checkboxes in map-options if any") self.signal_enable_cbs.emit() - def get_capabilities(self): + def select_layer_and_style(self, layer_list, layer_name, style_name): + if layer_list is None or layer_name is None: + return + self.current_sel_layer = None + + for index in range(layer_list.topLevelItemCount()): + top_item = layer_list.topLevelItem(index) + + # Iterate over the children of the top-level item + for i in range(top_item.childCount()): + child_item = top_item.child(i) + + for column in range(child_item.columnCount()): + + if child_item.text(column).endswith(layer_name): + self.current_sel_layer = child_item + + if not self.current_sel_layer: + print(f"Layer '{layer_name}' not found.") + return + + # Simulate clicking the layer + self.multilayers.multilayer_clicked(self.current_sel_layer) + for styles in self.current_sel_layer.styles: + selected_style = str(styles) + if selected_style == style_name: + self.current_sel_layer.style = styles + self.current_sel_layer.style_changed() + self.multilayers.multilayer_clicked(self.current_sel_layer) + self.current_sel_layer.parent.dock_widget.auto_update() + break + + def update_url_layer_styles(self, url_name, layer_name, style_name, level=None): + self.multilayers.cbWMS_URL.setEditText(url_name) + self.get_capabilities(level=level) + + def get_capabilities(self, level=None): """ Query the WMS server in the URL combobox for its capabilities. """ # Load new WMS. Only add those layers to the combobox that can provide # the CRS that match the filter of this module. + base_url = self.multilayers.cbWMS_URL.currentText() + self.base_url_changed.emit(base_url) + params = {'service': 'WMS', 'request': 'GetCapabilities'} @@ -731,7 +952,7 @@ def on_success(request): url = url.replace("?service=WMS", "").replace("&service=WMS", "") \ .replace("?request=GetCapabilities", "").replace("&request=GetCapabilities", "") logging.debug("requesting capabilities from %s", url) - self.initialise_wms(url, None) + self.initialise_wms(url, None, level=level) def on_failure(e): try: @@ -753,7 +974,7 @@ def on_failure(e): Worker.create(lambda: requests.get(base_url, params=params, timeout=(5, 60)), on_success, on_failure) - def activate_wms(self, wms, cache=False): + def activate_wms(self, wms, cache=False, level=None): # Parse layer tree of the wms object and discover usable layers. stack = list(wms.contents.values()) filtered_layers = set() @@ -768,6 +989,7 @@ def activate_wms(self, wms, cache=False): len(filtered_layers)) filtered_layers = sorted(filtered_layers) selected = self.multilayers.current_layer.text(0) if self.multilayers.current_layer else None + self.selected_layer = selected if not cache and wms.url in self.multilayers.layers and \ wms.capabilities_document.decode("utf-8") != \ self.multilayers.layers[wms.url]["wms"].capabilities_document.decode("utf-8"): @@ -779,7 +1001,7 @@ def activate_wms(self, wms, cache=False): self.multilayers.update_checkboxes() self.multilayers.pbViewCapabilities.setEnabled(True) if len(filtered_layers) > 0: - self.populate_ui() + self.populate_ui(update_level=level) if self.prefetcher is not None: self.prefetch.disconnect(self.prefetcher.fetch_maps) @@ -799,6 +1021,10 @@ def activate_wms(self, wms, cache=False): # logic to disable fill continents, fill oceans on connection to self.signal_disable_cbs.emit() + if level: + self.select_layer_and_style(layer_list=self.multilayers.listLayers, + layer_name=self.layer_name, style_name=self.style_name) + self.populate_ui(update_level=level) def view_capabilities(self): """Open a WMSCapabilitiesBrowser dialog showing the capabilities @@ -869,7 +1095,7 @@ def disable_ui(self): self.enable_level_elements(False) self.btGetMap.setEnabled(False) - def populate_ui(self): + def populate_ui(self, update_level=None): """ Adds the values of the current layer to the UI comboboxes """ @@ -880,12 +1106,15 @@ def populate_ui(self): active_layers = self.multilayers.get_active_layers() layer = self.multilayers.get_current_layer() + if layer is not None: + self.layer_changed.emit(layer) + currentstyle = layer.get_style() + self.styles_changed.emit(currentstyle) if not layer: self.lLayerName.setText("No Layer selected") self.disable_ui() return - else: self.btGetMap.setEnabled(True) @@ -906,6 +1135,7 @@ def populate_ui(self): if levels: self.cbLevel.addItems(levels) + self.on_level_changed.emit(levels[0]) self.cbLevel.setCurrentIndex(self.cbLevel.findText(layer.get_level())) self.enable_level_elements(len(levels) > 0) @@ -936,6 +1166,13 @@ def populate_ui(self): extra = [_code for _code in sorted(extra) if _code[5:] in basemap.epsg_dict] logging.debug("Selected '%s' for Combobox.", extra) self.parent().parent().update_predefined_maps(extra) + if update_level: + for i in range(self.cbLevel.count()): + item_text = self.cbLevel.itemText(i) + index = item_text.find(' ') + item_text = item_text[:index] + if item_text == update_level: + self.cbLevel.setCurrentIndex(i) self.layerChangeInProgress = False @@ -1094,6 +1331,7 @@ def init_time_changed(self): init time date/time edit. """ init_time = self.cbInitTime.currentText() + self.itime_changed.emit(init_time) if init_time != "": init_time = parse_iso_datetime(init_time) if init_time is not None: @@ -1110,6 +1348,7 @@ def valid_time_changed(self): """Same as initTimeChanged(), but for the valid time elements. """ valid_time = self.cbValidTime.currentText() + self.vtime_changed.emit(valid_time) if valid_time != "": valid_time = parse_iso_datetime(valid_time) if valid_time is not None: @@ -1119,6 +1358,9 @@ def valid_time_changed(self): self.multilayers.get_current_layer().set_vtime(self.cbValidTime.currentText()) self.multilayers.carry_parameters["vtime"] = self.cbValidTime.currentText() + combo_items = [self.cbValidTime.itemText(i) for i in range(self.cbValidTime.count())] + self.vtime_data.emit(combo_items) + self.auto_update() return valid_time == "" or valid_time is not None @@ -1126,6 +1368,8 @@ def level_changed(self): if self.multilayers.threads == 0 and not self.layerChangeInProgress: self.multilayers.get_current_layer().set_level(self.cbLevel.currentText()) self.multilayers.carry_parameters["level"] = self.cbLevel.currentText() + currentlevel = self.cbLevel.currentText() + self.on_level_changed.emit(currentlevel) self.auto_update() def enable_level_elements(self, enable): diff --git a/mslib/utils/config.py b/mslib/utils/config.py index 7faf3bd18..e6766a76d 100644 --- a/mslib/utils/config.py +++ b/mslib/utils/config.py @@ -74,18 +74,96 @@ class MSUIDefaultConfig: # Predefined map regions to be listed in the corresponding topview combobox predefined_map_sections = { - "01 Europe (cyl)": {"CRS": "EPSG:4326", - "map": {"llcrnrlon": -15.0, "llcrnrlat": 35.0, - "urcrnrlon": 30.0, "urcrnrlat": 65.0}}, - "02 Germany (cyl)": {"CRS": "EPSG:4326", - "map": {"llcrnrlon": 5.0, "llcrnrlat": 45.0, - "urcrnrlon": 15.0, "urcrnrlat": 57.0}}, - "03 Global (cyl)": {"CRS": "EPSG:4326", - "map": {"llcrnrlon": -180.0, "llcrnrlat": -90.0, - "urcrnrlon": 180.0, "urcrnrlat": 90.0}}, - "04 Northern Hemisphere (stereo)": {"CRS": "MSS:stere,0,90,90", - "map": {"llcrnrlon": -45.0, "llcrnrlat": 0.0, - "urcrnrlon": 135.0, "urcrnrlat": 0.0}} + "00 global (cyl)": { + "CRS": "EPSG:4326", + "map": { + "llcrnrlon": -180.0, + "llcrnrlat": -90.0, + "urcrnrlon": 180.0, + "urcrnrlat": 90.0 + } + }, + "01 SADPAP (stereo)": { + "CRS": "EPSG:77890290", + "map": { + "llcrnrlon": -150.0, + "llcrnrlat": -45.0, + "urcrnrlon": -25.0, + "urcrnrlat": -20.0 + } + }, + "02 SADPAP zoom (stereo)": { + "CRS": "EPSG:77890290", + "map": { + "llcrnrlon": -120.0, + "llcrnrlat": -65.0, + "urcrnrlon": -45.0, + "urcrnrlat": -28.0 + } + }, + "03 SADPAP (cyl)": { + "CRS": "EPSG:4326", + "map": { + "llcrnrlon": -100.0, + "llcrnrlat": -75.0, + "urcrnrlon": -30.0, + "urcrnrlat": -30.0 + } + }, + "04 Southern Hemisphere (stereo)": { + "CRS": "EPSG:77889270", + "map": { + "llcrnrlon": 135.0, + "llcrnrlat": 0.0, + "urcrnrlon": -45.0, + "urcrnrlat": 0.0 + } + }, + "05 EDMO-SAL (cyl)": { + "CRS": "EPSG:4326", + "map": { + "llcrnrlon": -40, + "llcrnrlat": 10, + "urcrnrlon": 30, + "urcrnrlat": 60 + } + }, + "06 SAL-BA (cyl)": { + "CRS": "EPSG:4326", + "map": { + "llcrnrlon": -80, + "llcrnrlat": -40, + "urcrnrlon": -10, + "urcrnrlat": 30 + } + }, + "07 Europe (cyl)": { + "CRS": "EPSG:4326", + "map": { + "llcrnrlon": -15.0, + "llcrnrlat": 35.0, + "urcrnrlon": 30.0, + "urcrnrlat": 65.0 + } + }, + "08 Germany (cyl)": { + "CRS": "EPSG:4326", + "map": { + "llcrnrlon": 5.0, + "llcrnrlat": 45.0, + "urcrnrlon": 15.0, + "urcrnrlat": 57.0 + } + }, + "09 Northern Hemisphere (stereo)": { + "CRS": "MSS:stere,0,90,90", + "map": { + "llcrnrlon": -45.0, + "llcrnrlat": 0.0, + "urcrnrlon": 135.0, + "urcrnrlat": 0.0 + } + } } # Side View. @@ -292,7 +370,7 @@ class MSUIDefaultConfig: "automated_plotting_flights": [["", "", "", "", "", ""]], "automated_plotting_hsecs": [["http://www.your-wms-server.de", "", "", ""]], "automated_plotting_vsecs": [["http://www.your-wms-server.de", "", "", ""]], - "automated_plotting_lsecs": [["http://www.your-wms-server.de", "", ""]] + "automated_plotting_lsecs": [["http://www.your-wms-server.de", "", "", "pressure"]] } config_descriptions = { diff --git a/mslib/utils/mssautoplot.py b/mslib/utils/mssautoplot.py index efbed08bc..0332ac4f2 100644 --- a/mslib/utils/mssautoplot.py +++ b/mslib/utils/mssautoplot.py @@ -25,22 +25,29 @@ limitations under the License. """ -from datetime import datetime, timedelta +import os import io +import re +import json import logging -import os -import sys +from datetime import datetime, timedelta +from urllib.parse import urljoin +import requests +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QMessageBox, QProgressDialog import click import defusedxml.ElementTree as etree import PIL.Image import matplotlib +from slugify import slugify from fs import open_fs import mslib import mslib.utils import mslib.msui import mslib.msui.mpl_map +import mslib.utils.auth import mslib.utils.qt import mslib.utils.thermolib from mslib.utils.config import config_loader, read_config_file @@ -53,6 +60,7 @@ from mslib.utils import config as conf from mslib.utils.auth import get_auth_from_url_and_name from mslib.utils.loggerdef import configure_mpl_logger +from mslib.utils.verify_user_token import verify_user_token TEXT_CONFIG = { @@ -77,33 +85,191 @@ def load_from_ftml(filename): return data_list, wp_list +def load_from_operation(op_name, msc_url, msc_auth_password, username, password): + """ + Method to load data from an operation in MSColab. + + Parameters: + :op_name: Name of the operation to load data from + :msc_url: URL of the MS Colab server + :msc_auth_password: Password for MS Colab authentication + :username: Username for authentication + :password: Password for authentication + + Returns: + Tuple containing a list of data points and a list of Waypoints if successful, None otherwise + """ + data = { + "email": username, + "password": password + } + session = requests.Session() + msc_auth = ("mscolab", msc_auth_password) + session.auth = msc_auth + session.headers.update({'x-test': 'true'}) + # ToDp fix config_loader it gets a list of two times the entry + response = session.get(urljoin(msc_url, 'status'), timeout=tuple(config_loader(dataset="MSCOLAB_timeout")[0])) + session.close() + if response.status_code == 401: + logging.error("Error", 'Server authentication data were incorrect.') + elif response.status_code == 200: + session = requests.Session() + session.auth = msc_auth + session.headers.update({'x-test': 'true'}) + url = urljoin(msc_url, "token") + try: + # ToDp fix config_loader it gets a list of two times the entry + response = session.post(url, data=data, timeout=tuple(config_loader(dataset="MSCOLAB_timeout")[0])) + response.raise_for_status() + except requests.exceptions.RequestException as ex: + logging.error("unexpected error: %s %s %s", type(ex), url, ex) + return + if response.text != "False": + _json = json.loads(response.text) + token = _json["token"] + msc_url = url + op_id = get_op_id(msc_url=msc_url, token=token, op_name=op_name) + xml_data = get_xml_data(msc_url=msc_url, token=token, op_id=op_id) + wp_list = ft.load_from_xml_data(xml_data) + now = datetime.now() + for wp in wp_list: + wp.utc_time = now + data_list = [ + (wp.lat, wp.lon, wp.flightlevel, wp.location, wp.comments) for wp in wp_list] + return data_list, wp_list + + +def get_xml_data(msc_url, token, op_id): + """ + + Parameters: + :msc_url: The URL of the MSColab Server + :token: The user's token for authentication + :op_id: The id of the operation to retrieve + + Returns: + str: The content of the XML data retrieved from the server + + """ + if verify_user_token(msc_url, token): + data = { + "token": token, + "op_id": op_id + } + url = urljoin(msc_url, "get_operation_by_id") + r = requests.get(url, data=data) + if r.text != "False": + xml_content = json.loads(r.text)["content"] + return xml_content + + +def get_op_id(msc_url, token, op_name): + """ + gets the operation id of the given operation name + + Parameters: + :msc_url: The URL of the MSColab server + :token: The user token for authentication + :op_name:: The name of the operation to retrieve op_id for + + Returns: + :op_id: The op_id of the operation with the specified name + """ + logging.debug('get_recent_op_id') + if verify_user_token(msc_url, token): + """ + get most recent operation's op_id + """ + skip_archived = config_loader(dataset="MSCOLAB_skip_archived_operations") + data = { + "token": token, + "skip_archived": skip_archived + } + url = urljoin(msc_url, "operations") + r = requests.get(url, data=data) + if r.text != "False": + _json = json.loads(r.text) + operations = _json["operations"] + for op in operations: + if op["path"] == op_name: + return op["op_id"] + + class Plotting: - def __init__(self, cpath): + def __init__(self, cpath, msc_url=None, msc_auth_password=None, username=None, password=None, pdlg=None): + """ + Initialize the Plotting object with the provided parameters. + + Parameters: + :cpath: Path to the configuration file + :msc_url: URL for MSColab service + :msc_auth_password: Authentication password for MSColab service + :username: User's username + :password: User's password + """ read_config_file(cpath) + self.pdlg = pdlg self.config = config_loader() self.num_interpolation_points = self.config["num_interpolation_points"] self.num_labels = self.config["num_labels"] self.tick_index_step = self.num_interpolation_points // self.num_labels self.bbox = None + flight = self.config["automated_plotting_flights"][0][0] section = self.config["automated_plotting_flights"][0][1] filename = self.config["automated_plotting_flights"][0][3] - try: - self.params = mslib.utils.coordinate.get_projection_params( - self.config["predefined_map_sections"][section]["CRS"].lower()) - except KeyError as e: - print(e) - sys.exit("Invalid SECTION and/or CRS") - self.params["basemap"].update(self.config["predefined_map_sections"][section]["map"]) - self.bbox_units = self.params["bbox"] - self.read_ftml(filename) + if self.__class__.__name__ == "TopViewPlotting": + try: + self.params = mslib.utils.coordinate.get_projection_params( + self.config["predefined_map_sections"][section]["CRS"].lower()) + except KeyError as e: + print(e) + raise SystemExit("Invalid SECTION and/or CRS") + self.params["basemap"].update(self.config["predefined_map_sections"][section]["map"]) + self.bbox_units = self.params["bbox"] + if filename != "" and filename == flight: + self.read_operation(flight, msc_url, msc_auth_password, username, password) + elif filename != "": + # Todo add the dir to the file in the mssautoplot.json + dirpath = "./" + file_path = os.path.join(dirpath, filename) + exists = os.path.exists(file_path) + if not exists: + print("Filename {} doesn't exist".format(filename)) + self.pdlg.close() + raise SystemExit("Filename {} doesn't exist".format(filename)) + self.read_ftml(filename) + + def setup(self): + pass + + def update_path(self, filename=None): + """ + Update the path by reading the FTML data from the given filename + and redrawing the path based on the updated waypoints model data. + + Parameters: + :filename: The name of the file to read FTML data from. + + Returns: + None + """ + # plot path and label + if filename != "": + self.read_ftml(filename) + self.fig.canvas.draw() + self.plotter.update_from_waypoints(self.wp_model_data) + self.plotter.redraw_path(waypoints_model_data=self.wp_model_data) + + def update_path_ops(self, filename=None): + self.setup() + # plot path and label + if filename != "": + self.read_operation(filename, self.url, self.msc_auth, self.username, self.password) + self.fig.canvas.draw() + self.plotter.update_from_waypoints(self.wp_model_data) + self.plotter.redraw_path(waypoints_model_data=self.wp_model_data) def read_ftml(self, filename): - dirpath = "./" - file_path = os.path.join(dirpath, filename) - exists = os.path.exists(file_path) - if not exists: - print("Filename {} doesn't exist".format(filename)) - sys.exit() self.wps, self.wp_model_data = load_from_ftml(filename) self.wp_lats, self.wp_lons, self.wp_locs = [[x[i] for x in self.wps] for i in [0, 1, 3]] self.wp_press = [mslib.utils.thermolib.flightlevel2pressure(wp[2] * units.hft).to("Pa").m for wp in self.wps] @@ -114,27 +280,47 @@ def read_ftml(self, filename): numpoints=self.num_interpolation_points + 1, connection="greatcircle") + def read_operation(self, op_name, msc_url, msc_auth_password, username, password): + self.wps, self.wp_model_data = load_from_operation(op_name, msc_url=msc_url, + msc_auth_password=msc_auth_password, username=username, + password=password) + self.wp_lats, self.wp_lons, self.wp_locs = [[x[i] for x in self.wps] for i in [0, 1, 3]] + self.wp_press = [mslib.utils.thermolib.flightlevel2pressure(wp[2] * units.hft).to("Pa").m for wp in self.wps] + self.path = [(wp[0], wp[1], datetime.now()) for wp in self.wps] + self.vertices = [list(a) for a in (zip(self.wp_lons, self.wp_lats))] + self.lats, self.lons = mslib.utils.coordinate.path_points([_x[0] for _x in self.path], + [_x[1] for _x in self.path], + numpoints=self.num_interpolation_points + 1, + connection="greatcircle") + class TopViewPlotting(Plotting): - def __init__(self, cpath): - super(TopViewPlotting, self).__init__(cpath) + def __init__(self, cpath, msc_url, msc_auth_password, msc_username, msc_password, pdlg): + super(TopViewPlotting, self).__init__(cpath, msc_url, msc_auth_password, msc_username, msc_password, pdlg) + self.pdlg = pdlg self.myfig = qt.TopViewPlotter() self.myfig.fig.canvas.draw() self.fig, self.ax = self.myfig.fig, self.myfig.ax matplotlib.backends.backend_agg.FigureCanvasAgg(self.fig) self.myfig.init_map(**(self.params["basemap"])) self.plotter = mpath.PathH_Plotter(self.myfig.map) + self.username = msc_username + self.password = msc_password + self.msc_auth = msc_auth_password + self.url = msc_url - def update_path(self, filename=None): - # plot path and label - if filename is not None: - self.read_ftml(filename) - self.fig.canvas.draw() - self.plotter.update_from_waypoints(self.wp_model_data) - self.plotter.redraw_path(waypoints_model_data=self.wp_model_data) + def setup(self): + pass def draw(self, flight, section, vertical, filename, init_time, time, url, layer, style, elevation, no_of_plots): - self.update_path(filename) + if filename != "" and filename == flight: + self.update_path_ops(filename) + elif filename != "": + try: + self.update_path(filename) + except AttributeError as e: + logging.debug(e) + raise SystemExit("No FLIGHT Selected") width, height = self.myfig.get_plot_size_in_px() self.bbox = self.params['basemap'] @@ -156,21 +342,27 @@ def draw(self, flight, section, vertical, filename, init_time, time, url, layer, } auth_username, auth_password = get_auth_from_url_and_name(url, self.config["MSS_auth"]) + # bbox for 1.3.0 needs a fix, swapped order wms = MSUIWebMapService(url, username=auth_username, password=auth_password, - version='1.3.0') + version='1.1.1') img = wms.getmap(**kwargs) image_io = io.BytesIO(img.read()) img = PIL.Image.open(image_io) self.myfig.draw_image(img) - self.myfig.fig.savefig(f"{flight}_{layer}_{no_of_plots}.png") + t = str(time) + date_time = re.sub(r'\W+', '', t) + plot_filename = slugify(f"{flight}_{layer}_{section}_{date_time}_{no_of_plots}_{elevation}") + ".png" + self.myfig.fig.savefig(plot_filename) + print(f"The image is saved at: {os.getcwd()}/{plot_filename}") class SideViewPlotting(Plotting): - def __init__(self, cpath): - super(SideViewPlotting, self).__init__(cpath) + def __init__(self, cpath, msc_url, msc_auth_password, msc_username, msc_password, pdlg): + super(SideViewPlotting, self).__init__(cpath, msc_url, msc_auth_password, msc_username, msc_password, pdlg) + self.pdlg = pdlg self.myfig = qt.SideViewPlotter() self.ax = self.myfig.ax self.fig = self.myfig.fig @@ -178,6 +370,10 @@ def __init__(self, cpath): self.fig.canvas.draw() matplotlib.backends.backend_agg.FigureCanvasAgg(self.myfig.fig) self.plotter = mpath.PathV_Plotter(self.myfig.ax) + self.username = msc_username + self.password = msc_password + self.msc_auth = msc_auth_password + self.url = msc_url def setup(self): self.intermediate_indexes = [] @@ -194,8 +390,18 @@ def setup(self): self.myfig.redraw_xaxis(self.lats, self.lons, times, times_visible) def update_path(self, filename=None): - self.setup() - if filename is not None: + """ + Update the path by reading the FTML data from the given filename + and redrawing the path based on the updated waypoints model data. + + Parameters: + :filename: The name of the file to read FTML data from. + + Returns: + None + """ + # plot path and label + if filename != "": self.read_ftml(filename) self.fig.canvas.draw() self.plotter.update_from_waypoints(self.wp_model_data) @@ -205,8 +411,28 @@ def update_path(self, filename=None): highlight = [[wp[0], wp[1]] for wp in self.wps] self.myfig.draw_vertical_lines(highlight, self.lats, self.lons) + def update_path_ops(self, filename=None): + self.setup() + # plot path and label + if filename != "": + self.read_operation(filename, self.url, self.msc_auth, self.username, self.password) + self.fig.canvas.draw() + self.plotter.update_from_waypoints(self.wp_model_data) + indices = list(zip(self.intermediate_indexes, self.wp_press)) + self.plotter.redraw_path(vertices=indices, + waypoints_model_data=self.wp_model_data) + highlight = [[wp[0], wp[1]] for wp in self.wps] + self.myfig.draw_vertical_lines(highlight, self.lats, self.lons) + def draw(self, flight, section, vertical, filename, init_time, time, url, layer, style, elevation, no_of_plots): - self.update_path(filename) + if filename != "" and filename == flight: + self.update_path_ops(filename) + elif filename != "": + try: + self.update_path(filename) + except AttributeError as e: + logging.debug(e) + raise SystemExit("No FLIGHT Selected") width, height = self.myfig.get_plot_size_in_px() p_bot, p_top = [float(x) * 100 for x in vertical.split(",")] self.bbox = tuple([x for x in (self.num_interpolation_points, @@ -227,84 +453,121 @@ def draw(self, flight, section, vertical, filename, init_time, time, url, layer, "format": "image/png", "size": (width, height) } + auth_username, auth_password = get_auth_from_url_and_name(url, self.config["MSS_auth"]) + # bbox for sideview is correct wms = MSUIWebMapService(url, username=auth_username, password=auth_password, version='1.3.0') img = wms.getmap(**kwargs) - image_io = io.BytesIO(img.read()) img = PIL.Image.open(image_io) + plot_filename = slugify(f"{flight}_{layer}_{time}_{no_of_plots}") + ".png" + self.myfig.setup_side_view() + self.myfig.draw_image(img) + self.ax.set_title(f"{flight}: {layer} \n{time} {no_of_plots}", horizontalalignment="left", x=0) + self.myfig.redraw_xaxis(self.lats, self.lons, None, False) self.myfig.draw_image(img) - self.myfig.fig.savefig(f"{flight}_{layer}_{no_of_plots}.png", bbox_inches='tight') + self.myfig.fig.savefig(plot_filename, bbox_inches='tight') + print(f"The image is saved at: {os.getcwd()}/{plot_filename}") class LinearViewPlotting(Plotting): - def __init__(self): - super(LinearViewPlotting, self).__init__() + # ToDo Implement access of MSColab + def __init__(self, cpath, msc_url, msc_auth_password, msc_username, msc_password, pdlg): + super(LinearViewPlotting, self).__init__(cpath, msc_url, msc_auth_password, msc_username, msc_password) + self.pdlg = pdlg self.myfig = qt.LinearViewPlotter() self.ax = self.myfig.ax matplotlib.backends.backend_agg.FigureCanvasAgg(self.myfig.fig) + self.plotter = mpath.PathV_Plotter(self.myfig.ax) self.fig = self.myfig.fig + self.username = msc_username + self.password = msc_password + self.msc_auth = msc_auth_password + self.url = msc_url def setup(self): - self.bbox = (self.num_interpolation_points,) + linearview_size_settings = config_loader(dataset="linearview") settings_dict = {"plot_title_size": linearview_size_settings["plot_title_size"], "axes_label_size": linearview_size_settings["axes_label_size"]} self.myfig.set_settings(settings_dict) self.myfig.setup_linear_view() - def draw(self): - for flight, section, vertical, filename, init_time, time in \ - self.config["automated_plotting_flights"]: - for url, layer, style in self.config["automated_plotting_lsecs"]: - width, height = self.myfig.get_plot_size_in_px() - - if not init_time: - init_time = None - - auth_username, auth_password = get_auth_from_url_and_name(url, self.config["MSS_auth"]) - wms = MSUIWebMapService(url, - username=auth_username, - password=auth_password, - version='1.3.0') - - path_string = "" - for i, wp in enumerate(self.wps): - path_string += f"{wp[0]:.2f},{wp[1]:.2f},{self.wp_press[i]}," - path_string = path_string[:-1] - - # retrieve and draw image - kwargs = {"layers": [layer], - "styles": [style], - "time": time, - "init_time": init_time, - "exceptions": 'application/vnd.ogc.se_xml', - "srs": "LINE:1", - "path_str": path_string, - "bbox": self.bbox, - "format": "text/xml", - "size": (width, height) - } - - xmls = wms.getmap(**kwargs) - - if not isinstance(xmls, list): - xmls = [xmls] - - xml_objects = [] - for xml_ in xmls: - xml_data = etree.fromstring(xml_.read()) - xml_objects.append(xml_data) - - self.myfig.draw_image(xml_objects, colors=None, scales=None) - self.myfig.redraw_xaxis(self.lats, self.lons) - highlight = [[wp[0], wp[1]] for wp in self.wps] - self.myfig.draw_vertical_lines(highlight, self.lats, self.lons) - self.myfig.fig.savefig(f"{flight}_{layer}.png", bbox_inches='tight') + def update_path(self, filename=None): + self.setup() + if filename != "": + self.read_ftml(filename) + + highlight = [[wp[0], wp[1]] for wp in self.wps] + self.myfig.draw_vertical_lines(highlight, self.lats, self.lons) + + def update_path_ops(self, filename=None): + self.setup() + # plot path and la + # plot path and label + if filename != "": + self.read_operation(filename, self.url, self.msc_auth, self.username, self.password) + highlight = [[wp[0], wp[1]] for wp in self.wps] + self.myfig.draw_vertical_lines(highlight, self.lats, self.lons) + + def draw(self, flight, section, vertical, filename, init_time, time, url, layer, style, elevation, no_of_plots): + if filename != "" and filename == flight: + self.update_path_ops(filename) + elif filename != "": + try: + self.update_path(filename) + except AttributeError as e: + logging.debug(e) + raise SystemExit("No FLIGHT Selected") + width, height = self.myfig.get_plot_size_in_px() + self.bbox = (self.num_interpolation_points,) + + if not init_time: + init_time = None + + auth_username, auth_password = get_auth_from_url_and_name(url, self.config["MSS_auth"]) + wms = MSUIWebMapService(url, + username=auth_username, + password=auth_password, + version='1.3.0') + + path_string = "" + for i, wp in enumerate(self.wps): + path_string += f"{wp[0]:.2f},{wp[1]:.2f},{self.wp_press[i]}," + path_string = path_string[:-1] + kwargs = {"layers": [layer], + "styles": [style], + "time": time, + "init_time": init_time, + "exceptions": 'application/vnd.ogc.se_xml', + "srs": "LINE:1", + "path_str": path_string, + "bbox": self.bbox, + "format": "text/xml", + "size": (width, height) + } + xmls = wms.getmap(**kwargs) + + if not isinstance(xmls, list): + xmls = [xmls] + + xml_objects = [] + for xml_ in xmls: + xml_data = etree.fromstring(xml_.read()) + xml_objects.append(xml_data) + + self.myfig.draw_image(xml_objects, colors=None, scales=None) + self.myfig.redraw_xaxis(self.lats, self.lons) + highlight = [[wp[0], wp[1]] for wp in self.wps] + plot_filename = slugify(f"{flight}_{layer}") + ".png" + self.myfig.draw_vertical_lines(highlight, self.lats, self.lons) + self.myfig.ax.set_title(f"{flight}: {layer} \n{time} {no_of_plots}", horizontalalignment="left", x=0) + self.myfig.fig.savefig(plot_filename, bbox_inches='tight') + print(f"The image is saved at: {os.getcwd()}/{plot_filename}") @click.command() @@ -316,55 +579,109 @@ def draw(self): @click.option('--intv', default=0, help='Time interval.') @click.option('--stime', default="", help='Starting time for downloading multiple plots with a fixed interval.') @click.option('--etime', default="", help='Ending time for downloading multiple plots with a fixed interval.') -def main(cpath, view, ftrack, itime, vtime, intv, stime, etime): +@click.pass_context +def main(ctx, cpath, view, ftrack, itime, vtime, intv, stime, etime): + pdlg = None + + def close_process_dialog(pdlg): + pdlg.close() + + if ctx.obj is not None: + # ToDo find a simpler solution, on a split of the package, QT is expensive for such a progressbar + pdlg = QProgressDialog("Downloading images", "Cancel", 0, 10, parent=ctx.obj) + pdlg.setMinimumDuration(0) + pdlg.repaint() + pdlg.canceled.connect(lambda: close_process_dialog(pdlg)) + pdlg.setWindowModality(Qt.WindowModal) + pdlg.setAutoReset(True) # Close dialog automatically when reaching max value + pdlg.setAutoClose(True) # Automatically close when value reaches maximum + pdlg.setValue(0) # Initial progress value + + # Set window flags to ensure visibility and modality + pdlg.setWindowFlags(pdlg.windowFlags() | Qt.CustomizeWindowHint | Qt.WindowTitleHint) + + pdlg.setValue(0) + conf.read_config_file(path=cpath) config = conf.config_loader() + + # flight_name = config["automated_plotting_flights"][0][0] + # file = config["automated_plotting_flights"][0][3] + if ctx.obj is not None: + pdlg.setValue(1) + + msc_url = config["mscolab_server_url"] + msc_auth_password = mslib.utils.auth.get_password_from_keyring(service_name=f"MSCOLAB_AUTH_{msc_url}", + username="mscolab") + msc_username = config["MSS_auth"][msc_url] + msc_password = mslib.utils.auth.get_password_from_keyring(service_name=msc_url, username=msc_username) + + # Choose view (top or side) if view == "top": - top_view = TopViewPlotting(cpath) + top_view = TopViewPlotting(cpath, msc_url, msc_auth_password, msc_username, msc_password, pdlg) sec = "automated_plotting_hsecs" - else: - side_view = SideViewPlotting(cpath) + elif view == "side": + side_view = SideViewPlotting(cpath, msc_url, msc_auth_password, msc_username, msc_password, pdlg) sec = "automated_plotting_vsecs" + elif view == "linear": + linear_view = LinearViewPlotting(cpath, msc_url, msc_auth_password, msc_username, msc_password, pdlg) + sec = "automated_plotting_lsecs" + else: + print("Invalid view") + + if ctx.obj is not None: + pdlg.setValue(2) def draw(no_of_plots): try: if view == "top": - top_view.draw(flight, section, vertical, filename, init_time, - time, url, layer, style, elevation, no_of_plots=no_of_plots) + top_view.draw(flight, section, vertical, filename, init_time, time, + url, layer, style, elevation, no_of_plots) elif view == "side": - side_view.draw(flight, section, vertical, filename, init_time, - time, url, layer, style, elevation, no_of_plots=no_of_plots) + side_view.draw(flight, section, vertical, filename, init_time, time, + url, layer, style, elevation, no_of_plots=no_of_plots) + elif view == "linear": + linear_view.draw(flight, section, vertical, filename, init_time, time, + url, layer, style, elevation, no_of_plots) + else: + print("View is not available, Plot not created!") + return False except Exception as e: if "times" in str(e): print("Invalid times and/or levels requested") elif "LAYER" in str(e): - print("Invalid LAYER '{}' requested".format(layer)) - elif "404 Client Error" or "NOT FOUND for url" in e: + print(f"Invalid LAYER '{layer}' requested") + elif "404 Client Error" in str(e) or "NOT FOUND for url" in str(e): print("Invalid STYLE and/or URL requested") else: print(str(e)) else: print("Plot downloaded!") - - for flight, section, vertical, filename, init_time, time in \ - config["automated_plotting_flights"]: + return True + return False + if ctx.obj is not None: + pdlg.setValue(4) + flag = False + for flight, section, vertical, filename, init_time, time in config["automated_plotting_flights"]: + if ctx.obj is not None: + pdlg.setValue(8) for url, layer, style, elevation in config[sec]: if vtime == "" and stime == "": no_of_plots = 1 - draw(no_of_plots) + flag = draw(no_of_plots) elif intv == 0: if itime != "": - init_time = datetime.strptime(itime, "%Y-%m-%dT" "%H:%M:%S") - time = datetime.strptime(vtime, "%Y-%m-%dT" "%H:%M:%S") + init_time = datetime.strptime(itime, "%Y-%m-%dT%H:%M:%S") + time = datetime.strptime(vtime, "%Y-%m-%dT%H:%M:%S") if ftrack != "": flight = ftrack no_of_plots = 1 - draw(no_of_plots) + flag = draw(no_of_plots) elif intv > 0: if itime != "": - init_time = datetime.strptime(itime, "%Y-%m-%dT" "%H:%M:%S") - starttime = datetime.strptime(stime, "%Y-%m-%dT" "%H:%M:%S") - endtime = datetime.strptime(etime, "%Y-%m-%dT" "%H:%M:%S") + init_time = datetime.strptime(itime, "%Y-%m-%dT%H:%M:%S") + starttime = datetime.strptime(stime, "%Y-%m-%dT%H:%M:%S") + endtime = datetime.strptime(etime, "%Y-%m-%dT%H:%M:%S") i = 1 time = starttime while time <= endtime: @@ -372,11 +689,26 @@ def draw(no_of_plots): if ftrack != "": flight = ftrack no_of_plots = i - draw(no_of_plots) + flag = draw(no_of_plots) time = time + timedelta(hours=intv) - i = i + 1 + i += 1 else: raise Exception("Invalid interval") + if ctx.obj is not None: + pdlg.setValue(10) + pdlg.close() + if flag: + QMessageBox.information( + ctx.obj, # The parent widget (use `None` if no parent) + "SUCCESS", # Title of the message box + "Plots downloaded successfully." # Message text + ) + else: + QMessageBox.information( + ctx.obj, # The parent widget (use `None` if no parent) + "FAILURE", # Title of the message box + "Plots couldnot be downloaded." # Message text + ) if __name__ == '__main__': diff --git a/tests/_test_msui/test_linearview.py b/tests/_test_msui/test_linearview.py index 2a43fdd13..ea02add4e 100644 --- a/tests/_test_msui/test_linearview.py +++ b/tests/_test_msui/test_linearview.py @@ -33,6 +33,7 @@ from PyQt5 import QtTest, QtCore from mslib.msui import flighttrack as ft import mslib.msui.linearview as tv +from mslib.msui.msui import MSUIMainWindow from mslib.msui.mpl_qtwidget import _DEFAULT_SETTINGS_LINEARVIEW @@ -55,13 +56,14 @@ def test_get(self): class Test_MSSLinearViewWindow: @pytest.fixture(autouse=True) def setup(self, qtbot): + mainwindow = MSUIMainWindow() initial_waypoints = [ft.Waypoint(40., 25., 300), ft.Waypoint(60., -10., 400), ft.Waypoint(40., 10, 300)] waypoints_model = ft.WaypointsTableModel("") waypoints_model.insertRows( 0, rows=len(initial_waypoints), waypoints=initial_waypoints) - self.window = tv.MSUILinearViewWindow(model=waypoints_model) + self.window = tv.MSUILinearViewWindow(model=waypoints_model, parent=mainwindow) self.window.show() QtTest.QTest.qWaitForWindowExposed(self.window) yield @@ -87,6 +89,7 @@ def test_options(self, mockdlg): class Test_LinearViewWMS: @pytest.fixture(autouse=True) def setup(self, qtbot, mswms_server): + mainwindow = MSUIMainWindow() self.url = mswms_server self.tempdir = tempfile.mkdtemp() if not os.path.exists(self.tempdir): @@ -96,7 +99,7 @@ def setup(self, qtbot, mswms_server): waypoints_model = ft.WaypointsTableModel("") waypoints_model.insertRows( 0, rows=len(initial_waypoints), waypoints=initial_waypoints) - self.window = tv.MSUILinearViewWindow(model=waypoints_model) + self.window = tv.MSUILinearViewWindow(model=waypoints_model, parent=mainwindow) self.window.show() QtTest.QTest.qWaitForWindowExposed(self.window) self.window.cbTools.currentIndexChanged.emit(1) diff --git a/tests/_test_msui/test_multiple_flightpath_dockwidget.py b/tests/_test_msui/test_multiple_flightpath_dockwidget.py index d25ffc3cf..c53ba3092 100644 --- a/tests/_test_msui/test_multiple_flightpath_dockwidget.py +++ b/tests/_test_msui/test_multiple_flightpath_dockwidget.py @@ -45,7 +45,7 @@ def setup(self, qtbot): self.waypoints_model.insertRows( 0, rows=len(initial_waypoints), waypoints=initial_waypoints) - self.widget = tv.MSUITopViewWindow(model=self.waypoints_model, mainwindow=self.window) + self.widget = tv.MSUITopViewWindow(model=self.waypoints_model, mainwindow=self.window, parent=self.window) self.window.show() QtTest.QTest.qWaitForWindowExposed(self.window) yield diff --git a/tests/_test_msui/test_sideview.py b/tests/_test_msui/test_sideview.py index 22b3302c7..bd5ae74ff 100644 --- a/tests/_test_msui/test_sideview.py +++ b/tests/_test_msui/test_sideview.py @@ -34,6 +34,7 @@ from PyQt5 import QtTest, QtCore, QtGui from mslib.msui import flighttrack as ft import mslib.msui.sideview as tv +from mslib.msui.msui import MSUIMainWindow from mslib.msui.mpl_qtwidget import _DEFAULT_SETTINGS_SIDEVIEW @@ -74,13 +75,14 @@ def test_setColour(self, mockdlg): class Test_MSSSideViewWindow: @pytest.fixture(autouse=True) def setup(self, qtbot): + mainwindow = MSUIMainWindow() initial_waypoints = [ft.Waypoint(40., 25., 300), ft.Waypoint(60., -10., 400), ft.Waypoint(40., 10, 300)] waypoints_model = ft.WaypointsTableModel("") waypoints_model.insertRows( 0, rows=len(initial_waypoints), waypoints=initial_waypoints) - self.window = tv.MSUISideViewWindow(model=waypoints_model) + self.window = tv.MSUISideViewWindow(model=waypoints_model, parent=mainwindow) self.window.show() QtTest.QTest.qWaitForWindowExposed(self.window) yield @@ -127,6 +129,7 @@ def test_y_axes(self): class Test_SideViewWMS: @pytest.fixture(autouse=True) def setup(self, qtbot, mswms_server): + mainwindow = MSUIMainWindow() self.url = mswms_server self.tempdir = tempfile.mkdtemp() if not os.path.exists(self.tempdir): @@ -136,7 +139,7 @@ def setup(self, qtbot, mswms_server): waypoints_model = ft.WaypointsTableModel("") waypoints_model.insertRows( 0, rows=len(initial_waypoints), waypoints=initial_waypoints) - self.window = tv.MSUISideViewWindow(model=waypoints_model) + self.window = tv.MSUISideViewWindow(model=waypoints_model, parent=mainwindow) self.window.show() QtTest.QTest.qWaitForWindowExposed(self.window) self.window.cbTools.currentIndexChanged.emit(1) diff --git a/tests/_test_msui/test_topview.py b/tests/_test_msui/test_topview.py index 88aaf5e40..b83fc4997 100644 --- a/tests/_test_msui/test_topview.py +++ b/tests/_test_msui/test_topview.py @@ -61,7 +61,7 @@ def setup(self, qtbot): waypoints_model = ft.WaypointsTableModel("") waypoints_model.insertRows( 0, rows=len(initial_waypoints), waypoints=initial_waypoints) - self.window = tv.MSUITopViewWindow(model=waypoints_model, mainwindow=mainwindow) + self.window = tv.MSUITopViewWindow(model=waypoints_model, mainwindow=mainwindow, parent=mainwindow) self.window.show() QtTest.QTest.qWaitForWindowExposed(self.window) yield @@ -211,7 +211,7 @@ def setup(self, qtbot, mswms_server): 0, rows=len(initial_waypoints), waypoints=initial_waypoints) mainwindow = MSUIMainWindow() - self.window = tv.MSUITopViewWindow(model=waypoints_model, mainwindow=mainwindow) + self.window = tv.MSUITopViewWindow(model=waypoints_model, mainwindow=mainwindow, parent=mainwindow) self.window.show() QtTest.QTest.qWaitForWindowExposed(self.window) self.window.cbTools.currentIndexChanged.emit(1) @@ -250,12 +250,12 @@ def test_kwargs_update_does_not_harm(self): waypoints_model = ft.WaypointsTableModel("") waypoints_model.insertRows(0, rows=len(initial_waypoints), waypoints=initial_waypoints) mainwindow = MSUIMainWindow() - self.window = tv.MSUITopViewWindow(model=waypoints_model, mainwindow=mainwindow) + self.window = tv.MSUITopViewWindow(model=waypoints_model, mainwindow=mainwindow, parent=mainwindow) # user_options is a global var from mslib.utils.config import user_options - assert user_options['predefined_map_sections']['01 Europe (cyl)']['map'] == {'llcrnrlat': 35.0, + assert user_options['predefined_map_sections']['07 Europe (cyl)']['map'] == {'llcrnrlat': 35.0, 'llcrnrlon': -15.0, 'urcrnrlat': 65.0, 'urcrnrlon': 30.0} diff --git a/tests/data/msui_settings.json b/tests/data/msui_settings.json index be8ecf5b9..adb90ae94 100644 --- a/tests/data/msui_settings.json +++ b/tests/data/msui_settings.json @@ -1,76 +1,210 @@ - { "data_dir": "~/mssdata", "filepicker_default": "default", - "import_plugins": { - "FliteStar": ["fls", "mslib.plugins.io.flitestar", "load_from_flitestar"], - "Text": ["txt", "mslib.plugins.io.text", "load_from_txt"] + "FliteStar": [ + "fls", + "mslib.plugins.io.flitestar", + "load_from_flitestar" + ], + "Text": [ + "txt", + "mslib.plugins.io.text", + "load_from_txt" + ] }, - "export_plugins": { - "Text": ["txt", "mslib.plugins.io.text", "save_to_txt"], - "KML": ["kml", "mslib.plugins.io.kml", "save_to_kml"], - "GPX": ["gpx", "mslib.plugins.io.gpx", "save_to_gpx"] - }, - - + "Text": [ + "txt", + "mslib.plugins.io.text", + "save_to_txt" + ], + "KML": [ + "kml", + "mslib.plugins.io.kml", + "save_to_kml" + ], + "GPX": [ + "gpx", + "mslib.plugins.io.gpx", + "save_to_gpx" + ] + }, "layout": { - "topview": [963, 702], - "sideview": [913, 557], - "tableview": [1236, 424], - "immutable": false - }, - + "topview": [ + 963, + 702 + ], + "sideview": [ + 913, + 557 + ], + "tableview": [ + 1236, + 424 + ], + "immutable": false + }, "locations": { - "EDMO": [48.08, 11.28], - "Hannover": [52.37, 9.74], - "Hamburg": [53.55, 9.99], - "Juelich": [50.92, 6.36], - "Leipzig": [51.34, 12.37], - "Muenchen": [48.14, 11.57], - "Stuttgart": [48.78, 9.18], - "Wien": [48.20833, 16.373064], - "Zugspitze": [47.42, 10.98], - "Kiruna": [67.821, 20.336], - "Ny-Alesund": [78.928, 11.986] + "EDMO": [ + 48.08, + 11.28 + ], + "Hannover": [ + 52.37, + 9.74 + ], + "Hamburg": [ + 53.55, + 9.99 + ], + "Juelich": [ + 50.92, + 6.36 + ], + "Leipzig": [ + 51.34, + 12.37 + ], + "Muenchen": [ + 48.14, + 11.57 + ], + "Stuttgart": [ + 48.78, + 9.18 + ], + "Wien": [ + 48.20833, + 16.373064 + ], + "Zugspitze": [ + 47.42, + 10.98 + ], + "Kiruna": [ + 67.821, + 20.336 + ], + "Ny-Alesund": [ + 78.928, + 11.986 + ] }, - "predefined_map_sections": { - "01 Europe (cyl)": {"CRS": "EPSG:4326", - "map": {"llcrnrlon": -15.0, "llcrnrlat": 35.0, - "urcrnrlon": 30.0, "urcrnrlat": 65.0}}, - "02 Germany (cyl)": {"CRS": "EPSG:4326", - "map": {"llcrnrlon": 5.0, "llcrnrlat": 45.0, - "urcrnrlon": 15.0, "urcrnrlat": 57.0}}, - "03 Global (cyl)": {"CRS": "EPSG:4326", - "map": {"llcrnrlon": -180.0, "llcrnrlat": -90.0, - "urcrnrlon": 180.0, "urcrnrlat": 90.0}}, - "04 Shannon (stereo)": {"CRS": "EPSG:77752350", - "map": {"llcrnrlon": -45.0, "llcrnrlat": 22.0, - "urcrnrlon": 45.0, "urcrnrlat": 63.0}}, - "05 Northern Hemisphere (stereo)": {"CRS": "EPSG:77790000", - "map": {"llcrnrlon": -45.0, "llcrnrlat": 0.0, - "urcrnrlon": 135.0, "urcrnrlat": 0.0}}, - "06 Southern Hemisphere (stereo)": {"CRS": "EPSG:77890000", - "map": {"llcrnrlon": 45.0, "llcrnrlat": 0.0, - "urcrnrlon": -135.0, "urcrnrlat": 0.0}} + "00 global (cyl)": { + "CRS": "EPSG:4326", + "map": { + "llcrnrlon": -180.0, + "llcrnrlat": -90.0, + "urcrnrlon": 180.0, + "urcrnrlat": 90.0 + } + }, + "01 SADPAP (stereo)": { + "CRS": "EPSG:77890290", + "map": { + "llcrnrlon": -150.0, + "llcrnrlat": -45.0, + "urcrnrlon": -25.0, + "urcrnrlat": -20.0 + } + }, + "02 SADPAP zoom (stereo)": { + "CRS": "EPSG:77890290", + "map": { + "llcrnrlon": -120.0, + "llcrnrlat": -65.0, + "urcrnrlon": -45.0, + "urcrnrlat": -28.0 + } + }, + "03 SADPAP (cyl)": { + "CRS": "EPSG:4326", + "map": { + "llcrnrlon": -100.0, + "llcrnrlat": -75.0, + "urcrnrlon": -30.0, + "urcrnrlat": -30.0 + } + }, + "04 Southern Hemisphere (stereo)": { + "CRS": "EPSG:77889270", + "map": { + "llcrnrlon": 135.0, + "llcrnrlat": 0.0, + "urcrnrlon": -45.0, + "urcrnrlat": 0.0 + } + }, + "05 EDMO-SAL (cyl)": { + "CRS": "EPSG:4326", + "map": { + "llcrnrlon": -40, + "llcrnrlat": 10, + "urcrnrlon": 30, + "urcrnrlat": 60 + } + }, + "06 SAL-BA (cyl)": { + "CRS": "EPSG:4326", + "map": { + "llcrnrlon": -80, + "llcrnrlat": -40, + "urcrnrlon": -10, + "urcrnrlat": 30 + } + }, + "07 Europe (cyl)": { + "CRS": "EPSG:4326", + "map": { + "llcrnrlon": -15.0, + "llcrnrlat": 35.0, + "urcrnrlon": 30.0, + "urcrnrlat": 65.0 + } + }, + "08 Germany (cyl)": { + "CRS": "EPSG:4326", + "map": { + "llcrnrlon": 5.0, + "llcrnrlat": 45.0, + "urcrnrlon": 15.0, + "urcrnrlat": 57.0 + } + }, + "09 Northern Hemisphere (stereo)": { + "CRS": "MSS:stere,0,90,90", + "map": { + "llcrnrlon": -45.0, + "llcrnrlat": 0.0, + "urcrnrlon": 135.0, + "urcrnrlat": 0.0 + } + } }, - - "new_flighttrack_template": ["Kiruna", "Ny-Alesund"], - "new_flighttrack_flightlevel": 250, - "num_interpolation_points": 201, - "num_labels": 10, - - "WMS_request_timeout": 30, - - "default_WMS": ["http://www.your-server.de/forecasts"], - "default_VSEC_WMS": ["http://www.your-server.de/forecasts"], - "default_LSEC_WMS": ["http://www.your-server.de/forecasts"], - - "default_MSCOLAB": ["http://www.your-mscolab-server.de/"], - "MSS_auth": { - "http://www.your-server.de/forecasts" : "authuser", - "http://www.your-mscolab-server.de" : "authuser" - } -} + "new_flighttrack_template": [ + "Kiruna", + "Ny-Alesund" + ], + "new_flighttrack_flightlevel": 250, + "num_interpolation_points": 201, + "num_labels": 10, + "WMS_request_timeout": 30, + "default_WMS": [ + "http://www.your-server.de/forecasts" + ], + "default_VSEC_WMS": [ + "http://www.your-server.de/forecasts" + ], + "default_LSEC_WMS": [ + "http://www.your-server.de/forecasts" + ], + "default_MSCOLAB": [ + "http://www.your-mscolab-server.de/" + ], + "MSS_auth": { + "http://www.your-server.de/forecasts": "authuser", + "http://www.your-mscolab-server.de": "authuser" + } +} \ No newline at end of file diff --git a/tutorials/tutorial_hexagoncontrol.py b/tutorials/tutorial_hexagoncontrol.py index dcf4e0d54..93633c124 100644 --- a/tutorials/tutorial_hexagoncontrol.py +++ b/tutorials/tutorial_hexagoncontrol.py @@ -43,7 +43,7 @@ def automate_hexagoncontrol(): msui_full_screen_and_open_first_view() # Changing map to Global - find_and_click_picture('topviewwindow-01-europe-cyl.png', + find_and_click_picture('topviewwindow-00-global-cyl.png', "Map change dropdown could not be located on the screen") select_listelement(2) # Zooming into the map diff --git a/tutorials/tutorial_kml.py b/tutorials/tutorial_kml.py index 809cc962c..b9626ffd1 100644 --- a/tutorials/tutorial_kml.py +++ b/tutorials/tutorial_kml.py @@ -46,7 +46,7 @@ def automate_kml(): def _switch_to_europe_map(): - find_and_click_picture('topviewwindow-01-europe-cyl.png', "Map change dropdown could not be located on the screen.") + find_and_click_picture('topviewwindow-00-global-cyl.png', "Map change dropdown could not be located on the screen.") select_listelement(2) pag.sleep(1) create_tutorial_images() diff --git a/tutorials/tutorial_satellitetrack.py b/tutorials/tutorial_satellitetrack.py index eb4e2ce54..3e2e379cd 100644 --- a/tutorials/tutorial_satellitetrack.py +++ b/tutorials/tutorial_satellitetrack.py @@ -52,7 +52,7 @@ def automate_rs(): pag.sleep(2) # Changing map to Global - find_and_click_picture('topviewwindow-01-europe-cyl.png', + find_and_click_picture('topviewwindow-00-global-cyl.png', "Map change dropdown could not be located on the screen") select_listelement(2) diff --git a/tutorials/tutorial_views.py b/tutorials/tutorial_views.py index df304ef6e..88890d48a 100644 --- a/tutorials/tutorial_views.py +++ b/tutorials/tutorial_views.py @@ -137,8 +137,8 @@ def automate_views(): topview = load_settings_qsettings('topview', {"os_screen_region": (0, 0, 0, 0)}) pag.sleep(1) - find_and_click_picture('topviewwindow-01-europe-cyl.png', - 'Projection 01-europe-cyl not found', + find_and_click_picture('topviewwindow-00-global-cyl.png', + 'Projection 00-global-cyl not found', region=topview["os_screen_region"]) select_listelement(2) diff --git a/tutorials/tutorial_waypoints.py b/tutorials/tutorial_waypoints.py index 4b1ea63e5..d5e7a4ac0 100644 --- a/tutorials/tutorial_waypoints.py +++ b/tutorials/tutorial_waypoints.py @@ -90,7 +90,7 @@ def automate_waypoints(): pag.sleep(2) # Changing map to Global - find_and_click_picture('topviewwindow-01-europe-cyl.png', + find_and_click_picture('topviewwindow-00-global-cyl.png', "Map change dropdown could not be located.") select_listelement(2) pag.sleep(5)