diff --git a/.gitignore b/.gitignore index 44e3da2a..06b7fcc4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # PyCharm project settings .idea +# VSCode project settings +.vscode + # OSX DS files .DS_Store @@ -115,3 +118,6 @@ CMakeCache.txt cmake_install.cmake CMakeFiles/ Makefile + +# Windows soft links +*.lnk diff --git a/Test_Data.lnk b/Test_Data.lnk new file mode 100644 index 00000000..0cd51e8e Binary files /dev/null and b/Test_Data.lnk differ diff --git a/uwsift/__main__.py b/uwsift/__main__.py index e9eeae2b..1c50d5df 100644 --- a/uwsift/__main__.py +++ b/uwsift/__main__.py @@ -76,6 +76,7 @@ from uwsift.workspace import CachingWorkspace, SimpleWorkspace from uwsift.workspace.collector import ResourceSearchPathCollector + LOG = logging.getLogger(__name__) configure_loggers() @@ -1099,6 +1100,11 @@ def _open_wizard(self, *args, **kwargs): if self._wizard_dialog.exec_(): LOG.info("Loading products from open wizard...") + # The selected projection in the main window is the same as the one chosen in the open file wizard + self.ui.projectionComboBox.setCurrentIndex(self._wizard_dialog.ui.projectionComboBox.currentIndex()) + #setting resampling info in layer details pane for displaying the resolution of the Area + self.ui.layerDetailsPane.set_resampling_info(self._wizard_dialog.resampling_info) + # scenes = self._wizard_dialog.scenes reader = self._wizard_dialog.get_reader() diff --git a/uwsift/etc/SIFT/config/area_definitions.yaml b/uwsift/etc/SIFT/config/area_definitions.yaml index 1cdf372f..933081ea 100644 --- a/uwsift/etc/SIFT/config/area_definitions.yaml +++ b/uwsift/etc/SIFT/config/area_definitions.yaml @@ -1,21 +1,54 @@ area_definitions: - MTG FCI FDSS 1km: mtg_fci_fdss_1km - MTG FCI FDSS 2km: mtg_fci_fdss_2km - MTG FCI FDSS 500m: mtg_fci_fdss_500m - MSG SEVIRI FES 3km: msg_seviri_fes_3km - MSG SEVIRI FES 1km: msg_seviri_fes_1km - MSG SEVIRI RSS 3km: msg_seviri_rss_3km - MSG SEVIRI RSS 1km: msg_seviri_rss_1km - MSG SEVIRI IODC 3km: msg_seviri_iodc_3km - MSG SEVIRI IODC 1km: msg_seviri_iodc_1km - GOES-East ABI F 500m: goes_east_abi_f_500m - GOES-East ABI F 1km: goes_east_abi_f_1km - GOES-East ABI F 2km: goes_east_abi_f_2km - GOES-West ABI F 500m: goes_west_abi_f_500m - GOES-West ABI F 1km: goes_west_abi_f_1km - GOES-West ABI F 2km: goes_west_abi_f_2km - EUROL: eurol + MTG FCI FDSS: + { + 1km: mtg_fci_fdss_1km, + 2km: mtg_fci_fdss_2km, + 500m: mtg_fci_fdss_500m, + 32km: mtg_fci_fdss_32km + } + + MSG SEVERI FES: + { + 3km: msg_seviri_fes_3km, + 1km: msg_seviri_fes_1km, + 9km: msg_seviri_fes_9km + } + MSG SEVIRI RSS: + { + 3km: msg_seviri_rss_3km, + 1km: msg_seviri_rss_1km + } + MSG SEVIRI IODC: + { + 3km: msg_seviri_iodc_3km, + 1km: msg_seviri_iodc_1km + } + GOES-East ABI F: + { + 500m: goes_east_abi_f_500m, + 1km: goes_east_abi_f_1km, + 2km: goes_east_abi_f_2km + } + + GOES-West ABI F: + { + 500m: goes_west_abi_f_500m, + 1km: goes_west_abi_f_1km, + 2km: goes_west_abi_f_2km + } + + EUROL: + { + eurol: eurol + } + + + + + + + #---- SIFT doesn't implement proj=eqc correctly/completely ---- # Plate Carree 3km: worldeqc3km diff --git a/uwsift/etc/SIFT/config/logging.yaml b/uwsift/etc/SIFT/config/logging.yaml index 792c571c..f29fd90a 100644 --- a/uwsift/etc/SIFT/config/logging.yaml +++ b/uwsift/etc/SIFT/config/logging.yaml @@ -12,7 +12,8 @@ logging: # for all loggers. loggers: all: - level: DEBUG + #level: DEBUG + level: INFO vispy: level: INFO sqlalchemy: diff --git a/uwsift/model/__init__.py b/uwsift/model/__init__.py index 854ac6a0..303e4384 100644 --- a/uwsift/model/__init__.py +++ b/uwsift/model/__init__.py @@ -23,3 +23,4 @@ """ from uwsift.model.document import Document # noqa + diff --git a/uwsift/model/area_definitions_manager.py b/uwsift/model/area_definitions_manager.py index b27b712c..97373fc3 100644 --- a/uwsift/model/area_definitions_manager.py +++ b/uwsift/model/area_definitions_manager.py @@ -25,9 +25,12 @@ # plane chart) should be available always. It will be only added though, # if there is no other area definition found in configuration which uses the # 'latlong' projection (or its PROJ.4 aliases) - "Plate Carree": "plate_carree" + "Plate Carree": + { + "plate_carree" : "plate_carree" + } } - + DEFAULT_AREA_DEFINITIONS_YAML = """ plate_carree: description: @@ -54,59 +57,113 @@ class AreaDefinitionsManager: """ _available_area_defs_by_id: None | dict[str, AreaDefinition] = None - _available_area_defs_id_by_name: None | dict[str, str] = None + _available_area_defs_group_by_group_name: None | dict[str, dict[str, str]] = None #changed | group_name : { name : id name : id} + + @classmethod def init_available_area_defs(cls) -> None: cls._available_area_defs_by_id = {} - cls._available_area_defs_id_by_name = {} + cls._available_area_defs_group_by_group_name = {} desired_area_defs = config.get("area_definitions", {}) - for area_def_name, area_id in desired_area_defs.items(): - try: - area_def = get_area_def(area_id) - except AreaNotFound as e: - LOG.warning( + for area_def_group_name, area_def_group in desired_area_defs.items(): + + for area_resolution, area_id in area_def_group.items(): + try: + area_def = get_area_def(area_id) + except AreaNotFound as e: + LOG.warning( f"Area definition configured for display name" f" '{area_def_name}' unknown: {e}. Skipping..." - ) - continue + ) + continue + #adding ar + LOG.info(f"Adding area definition: {area_def_group_name} , {area_resolution} -> {area_id}") + cls._available_area_defs_by_id[area_id] = area_def + + cls._available_area_defs_group_by_group_name[area_def_group_name] = area_def_group + - LOG.info(f"Adding area definition: {area_def_name} -> {area_id}") - cls._available_area_defs_by_id[area_id] = area_def - cls._available_area_defs_id_by_name[area_def_name] = area_id + # cls._available_area_defs_group_by_group_name and cls._available_area_defs_by_id are initialised # Check for existence of at least one 'latlong' projection # (https://proj.org/operations/conversions/latlon.html) for area_def in cls._available_area_defs_by_id.values(): if area_def.crs.is_geographic: return - + # Add default area definition(s)? - for area_def_name, area_id in DEFAULT_AREAS.items(): - area_def = load_area_from_string(DEFAULT_AREA_DEFINITIONS_YAML, area_id) + for area_def_group_name, area_def_group in DEFAULT_AREAS.items(): + for area_resolution, area_id in area_def_group.items(): + area_def = load_area_from_string(DEFAULT_AREA_DEFINITIONS_YAML, area_id) + + LOG.info(f"Adding area definition: {area_def_group_name} , {area_resolution} -> {area_id}") + cls._available_area_defs_by_id[area_id] = area_def - LOG.info(f"Adding default area definition:" f" {area_def_name} -> {area_id}") - cls._available_area_defs_by_id[area_id] = area_def - cls._available_area_defs_id_by_name[area_def_name] = area_id + cls._available_area_defs_group_by_group_name[area_def_group_name] = area_def_group + # + #returns all area projections @classmethod def available_area_def_names(cls): - return cls._available_area_defs_id_by_name.keys() - + return cls._available_area_defs_group_by_group_name.keys() + @classmethod def area_def_by_id(cls, id): return cls._available_area_defs_by_id.get(id) + #returns the first area definition from the group @classmethod def area_def_by_name(cls, name): - return cls.area_def_by_id(cls._available_area_defs_id_by_name.get(name)) + area_group = cls._available_area_defs_group_by_group_name.get(name) + first_key = next(iter(area_group)) + area_id = area_group[first_key] + return cls.area_def_by_id(area_id) @classmethod def default_area_def_name(cls): # TODO: take from configuration, make robust # Since nothing has been configured, take the first key (i.e. the # display_name) of the known area definitions - return next(iter(cls._available_area_defs_id_by_name)) + return next(iter(cls._available_area_defs_group_by_group_name)) + + + #returns all possible resolutions for one area projection + @classmethod + def available_area_def_group_resolutions(cls, group_name): + return cls._available_area_defs_group_by_group_name.get(group_name).keys() + + #returns area group by its name --- for example if a group_name = MSG SEVERI FES, a return value will be {3 km: msg_seviri_fes_3km, 1 km: msg_seviri_fes_1km} + @classmethod + def area_group_by_group_name(cls, group_name): + return cls._available_area_defs_group_by_group_name.get(group_name) + + #returns area definition by its name and resolution --- for example if a group_name = MSG SEVERI FES and resolution = 3km , a return value will be area definition for id = msg_seviri_fes_3km + @classmethod + def area_def_by_group_name_and_resolution(cls, group_name, resolution): + return cls.area_def_by_id(cls._available_area_defs_group_by_group_name.get(group_name).get(resolution)) + + #prepares area def for resampling + @classmethod + def prepare_area_def_for_resampling(cls, area_def, width, height): + # when the shape is changed, it also affects other parameters in the area definition + area_def.width = width + area_def.height = height + area_def.pixel_size_x = (area_def.area_extent[2] - area_def.area_extent[0]) / float(area_def.width) + area_def.pixel_size_y = (area_def.area_extent[3] - area_def.area_extent[1]) / float(area_def.height) + area_def.pixel_upper_left = (float(area_def.area_extent[0]) + float(area_def.pixel_size_x) / 2, float(area_def.area_extent[3]) - float(area_def.pixel_size_y) / 2) + area_def.pixel_offset_x = -area_def.area_extent[0] / area_def.pixel_size_x + area_def.pixel_offset_y = area_def.area_extent[3] / area_def.pixel_size_y + + + # calculates pixel_size_x and pixel_size_y for an area definition with custom resolution values + @classmethod + def area_def_custom_resolution_values(cls, area_def, width, height): + if width and height: + pixel_size_x = (area_def.area_extent[2] - area_def.area_extent[0]) / float(width) + pixel_size_y = (area_def.area_extent[3] - area_def.area_extent[1]) / float(height) + return pixel_size_x, pixel_size_y + # TODO: Why does this need to be a class being updated instead of an instance of a class? diff --git a/uwsift/model/document.py b/uwsift/model/document.py index 62d8a3b7..03f73881 100644 --- a/uwsift/model/document.py +++ b/uwsift/model/document.py @@ -208,6 +208,7 @@ def activate_product_uuid_as_new_dataset(self, uuid: UUID, insert_before=0, **im info[Info.FAMILY] = self._family_for_product_or_info(info) presentation = self._insert_dataset_with_info(info, insert_before=insert_before) + # signal updates from the document self.didAddDataset.emit(info, presentation) diff --git a/uwsift/ui/layer_details_widget_ui.py b/uwsift/ui/layer_details_widget_ui.py index 09b1ff6a..cd1f0c8e 100644 --- a/uwsift/ui/layer_details_widget_ui.py +++ b/uwsift/ui/layer_details_widget_ui.py @@ -53,22 +53,28 @@ def setupUi(self, LayerDetailsPane): self.formLayout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.layerWavelengthValue) self.layerColormapLabel = QtWidgets.QLabel(LayerDetailsPane) self.layerColormapLabel.setObjectName("layerColormapLabel") - self.formLayout.setWidget(5, QtWidgets.QFormLayout.LabelRole, self.layerColormapLabel) + self.formLayout.setWidget(6, QtWidgets.QFormLayout.LabelRole, self.layerColormapLabel) self.layerColormapValue = QtWidgets.QLabel(LayerDetailsPane) self.layerColormapValue.setObjectName("layerColormapValue") - self.formLayout.setWidget(5, QtWidgets.QFormLayout.FieldRole, self.layerColormapValue) + self.formLayout.setWidget(6, QtWidgets.QFormLayout.FieldRole, self.layerColormapValue) self.layerColorLimitsLabel = QtWidgets.QLabel(LayerDetailsPane) self.layerColorLimitsLabel.setObjectName("layerColorLimitsLabel") - self.formLayout.setWidget(6, QtWidgets.QFormLayout.LabelRole, self.layerColorLimitsLabel) + self.formLayout.setWidget(7, QtWidgets.QFormLayout.LabelRole, self.layerColorLimitsLabel) self.layerColorLimitsValue = QtWidgets.QLabel(LayerDetailsPane) self.layerColorLimitsValue.setObjectName("layerColorLimitsValue") - self.formLayout.setWidget(6, QtWidgets.QFormLayout.FieldRole, self.layerColorLimitsValue) + self.formLayout.setWidget(7, QtWidgets.QFormLayout.FieldRole, self.layerColorLimitsValue) self.layerResolutionLabel = QtWidgets.QLabel(LayerDetailsPane) self.layerResolutionLabel.setObjectName("layerResolutionLabel") self.formLayout.setWidget(4, QtWidgets.QFormLayout.LabelRole, self.layerResolutionLabel) self.layerResolutionValue = QtWidgets.QLabel(LayerDetailsPane) self.layerResolutionValue.setObjectName("layerResolutionValue") self.formLayout.setWidget(4, QtWidgets.QFormLayout.FieldRole, self.layerResolutionValue) + self.layerAreaResolutionLabel = QtWidgets.QLabel(LayerDetailsPane) + self.layerAreaResolutionLabel.setObjectName("layerAreaResolutionLabel") + self.formLayout.setWidget(5, QtWidgets.QFormLayout.LabelRole, self.layerAreaResolutionLabel) + self.layerAreaResolutionValue = QtWidgets.QLabel(LayerDetailsPane) + self.layerAreaResolutionValue.setObjectName("layerAreaResolutionValue") + self.formLayout.setWidget(5, QtWidgets.QFormLayout.FieldRole, self.layerAreaResolutionValue) self.verticalLayout.addLayout(self.formLayout) self.kindDetailsStackedWidget = QtWidgets.QStackedWidget(LayerDetailsPane) self.kindDetailsStackedWidget.setFrameShape(QtWidgets.QFrame.Box) @@ -195,8 +201,10 @@ def retranslateUi(self, LayerDetailsPane): self.layerColormapValue.setText(_translate("LayerDetailsPane", "Rainbow (IR Default)")) self.layerColorLimitsLabel.setText(_translate("LayerDetailsPane", "Color Limits:")) self.layerColorLimitsValue.setText(_translate("LayerDetailsPane", "-109.00 ~ 55.00°C")) - self.layerResolutionLabel.setText(_translate("LayerDetailsPane", "Resolution:")) + self.layerResolutionLabel.setText(_translate("LayerDetailsPane", "Native Resolution:")) self.layerResolutionValue.setText(_translate("LayerDetailsPane", "1 km")) + self.layerAreaResolutionLabel.setText(_translate("LayerDetailsPane", "Area Resolution:")) + self.layerAreaResolutionValue.setText(_translate("LayerDetailsPane", "N/A")) self.vmin_slider.setToolTip(_translate("LayerDetailsPane", "minimum color limit")) self.vmax_slider.setToolTip(_translate("LayerDetailsPane", "maximum color limit")) self.gammaLabel.setText(_translate("LayerDetailsPane", "Gamma: ")) diff --git a/uwsift/ui/open_file_wizard_ui.py b/uwsift/ui/open_file_wizard_ui.py index d2db3630..8df2456a 100644 --- a/uwsift/ui/open_file_wizard_ui.py +++ b/uwsift/ui/open_file_wizard_ui.py @@ -142,6 +142,21 @@ def setupUi(self, openFileWizard): self.projectionComboBox.setSizeIncrement(QtCore.QSize(1, 0)) self.projectionComboBox.setObjectName("projectionComboBox") self.productSelectionButtonLayout.addWidget(self.projectionComboBox) + self.resolutionLabel = QtWidgets.QLabel(self.productSelectionPage) + self.resolutionLabel.setObjectName("resolutionLabel") + self.productSelectionButtonLayout.addWidget(self.resolutionLabel) + self.resolutionComboBox = QtWidgets.QComboBox(self.productSelectionPage) + sizePolicy1 = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Fixed) + sizePolicy1.setHorizontalStretch(1) + sizePolicy1.setVerticalStretch(0) + sizePolicy1.setHeightForWidth(self.resolutionComboBox.sizePolicy().hasHeightForWidth()) + self.resolutionComboBox.setSizePolicy(sizePolicy1) + self.resolutionComboBox.setSizeIncrement(QtCore.QSize(1, 0)) + self.resolutionComboBox.setObjectName("resolutionComboBox") + self.productSelectionButtonLayout.addWidget(self.resolutionComboBox) + + # + self.resamplingShapeLabel = QtWidgets.QLabel(self.productSelectionPage) self.resamplingShapeLabel.setObjectName("resamplingShapeLabel") self.productSelectionButtonLayout.addWidget(self.resamplingShapeLabel) @@ -207,6 +222,7 @@ def retranslateUi(self, openFileWizard): self.radiusOfInfluenceLabel.setText(_translate("openFileWizard", "Radius of Influence:")) self.radiusOfInfluenceSpinBox.setSuffix(_translate("openFileWizard", " m")) self.projectionLabel.setText(_translate("openFileWizard", "Projection:")) + self.resolutionLabel.setText(_translate("openFileWizard", "Resolution:")) self.resamplingShapeLabel.setText(_translate("openFileWizard", "Shape:")) self.selectIDTable.setSortingEnabled(True) diff --git a/uwsift/view/layer_details.py b/uwsift/view/layer_details.py index 05c74287..ceff1d24 100644 --- a/uwsift/view/layer_details.py +++ b/uwsift/view/layer_details.py @@ -23,6 +23,7 @@ from PyQt5 import QtWidgets from uwsift.common import FALLBACK_RANGE, INVALID_COLOR_LIMITS, Info, Kind +from uwsift.model.area_definitions_manager import AreaDefinitionsManager from uwsift.model.layer_item import LayerItem from uwsift.model.layer_model import LayerModel from uwsift.model.product_dataset import ProductDataset @@ -43,6 +44,7 @@ class SingleLayerInfoPane(QtWidgets.QWidget): """Shows details about one layer that is currently selected.""" _slider_steps = 100 + _resampling_info = None def __init__(self, *args, **kwargs): """Initialise subwidgets and layout. @@ -149,6 +151,7 @@ def _clear_details_pane(self): self._details_pane_ui.layerInstrumentValue.setText("N/A") self._details_pane_ui.layerWavelengthValue.setText("N/A") self._details_pane_ui.layerResolutionValue.setText("N/A") + self._details_pane_ui.layerAreaResolutionValue.setText("N/A") self._details_pane_ui.layerColormapValue.setText("N/A") self._details_pane_ui.layerColorLimitsValue.setText("N/A") self._details_pane_ui.layerColormapVisual.setHtml("") @@ -296,6 +299,9 @@ def _set_new_clims(self, val, is_max): model = self._current_selected_layer.model model.change_color_limits_for_layer(self._current_selected_layer.uuid, new_clims) + def set_resampling_info(self, resampling_info): + self._resampling_info = resampling_info + def _slider_changed(self, value=None, is_max=True): spin_box = self._details_pane_ui.vmax_spinbox if is_max else self._details_pane_ui.vmin_spinbox if value is None: @@ -329,6 +335,7 @@ def _update_displayed_info(self): self._update_displayed_instrument() self._update_displayed_wavelength() self._update_displayed_resolution() + self._update_displayed_area_resolution() self.update_displayed_clims() self._update_displayed_kind_details() @@ -376,6 +383,21 @@ def _update_displayed_resolution(self): self._details_pane_ui.layerResolutionValue.setText(resolution_str) + def _update_displayed_area_resolution(self): + if self._resampling_info is None: + self._details_pane_ui.layerAreaResolutionValue.setText("N/A") + else: + area_def = AreaDefinitionsManager.area_def_by_id(self._resampling_info["area_id"]) + if self._resampling_info["custom"]: + resolution_values = AreaDefinitionsManager.area_def_custom_resolution_values(area_def, self._resampling_info["shape"][0], self._resampling_info["shape"][1]) + else: + resolution_values = (area_def.pixel_size_x, area_def.pixel_size_y) + + resolution_x = resolution_values[0] + resolution_y = resolution_values[1] + + self._details_pane_ui.layerAreaResolutionValue.setText(f"{format_resolution(resolution_x)} / {format_resolution(resolution_y)} ") + def _update_displayed_time(self): active_product_dataset: Optional[ProductDataset] = ( self._current_selected_layer.get_first_active_product_dataset() diff --git a/uwsift/view/open_file_wizard.py b/uwsift/view/open_file_wizard.py index 5d3436ed..e783638e 100644 --- a/uwsift/view/open_file_wizard.py +++ b/uwsift/view/open_file_wizard.py @@ -25,6 +25,8 @@ from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5.QtCore import QPoint from PyQt5.QtWidgets import QMenu +from PyQt5.QtWidgets import QMessageBox + from satpy.readers import find_files_and_readers, group_files from uwsift import config @@ -44,7 +46,6 @@ PAGE_ID_FILE_SELECTION = 0 PAGE_ID_PRODUCT_SELECTION = 1 - class Conf(Enum): # Just to have a well-defined constant to express "skip this resampler" SKIP = 1 @@ -87,6 +88,7 @@ class OpenFileWizard(QtWidgets.QWizard): inputParametersChanged = QtCore.pyqtSignal() directoryChanged = QtCore.pyqtSignal(str) + def __init__(self, base_dir=None, base_reader=None, parent=None): super(OpenFileWizard, self).__init__(parent) super(OpenFileWizard, self).__init__(parent) @@ -120,6 +122,7 @@ def __init__(self, base_dir=None, base_reader=None, parent=None): self.ui = Ui_openFileWizard() self.ui.setupUi(self) + # ------------------------------------------------------------------------------------------ # SIGNAL & SLOT CONNECTIONS # ------------------------------------------------------------------------------------------ @@ -162,13 +165,17 @@ def __init__(self, base_dir=None, base_reader=None, parent=None): self.ui.radiusOfInfluenceSpinBox.valueChanged.connect(self._update_resampling_info) self.ui.projectionComboBox.setModel(parent.ui.projectionComboBox.model()) - self.ui.projectionComboBox.currentIndexChanged.connect(self._update_resampling_info) - + #self.ui.projectionComboBox.currentIndexChanged.connect(self._update_resampling_info) self._update_resampling_shape_spin_boxes() self.ui.projectionComboBox.currentIndexChanged.connect(self._update_resampling_shape_spin_boxes) + self.ui.projectionComboBox.currentIndexChanged.connect(self._update_resampling_info) + self.ui.resamplingShapeRowSpinBox.valueChanged.connect(self._update_resampling_info) self.ui.resamplingShapeColumnSpinBox.valueChanged.connect(self._update_resampling_info) + #connect signals and slots for the resolutionComboBox + self.ui.resolutionComboBox.currentIndexChanged.connect(self._update_resampling_shape_spin_boxes_by_resolution_change) + # GUI has been initialized, make sure we have a consistent # resampling_info self.resampling_info = None @@ -184,12 +191,20 @@ def __init__(self, base_dir=None, base_reader=None, parent=None): # PUBLIC GENERAL WIZARD INTERFACE # ============================================================================================== + def initializeResolutionComboBox(self): + self.ui.resolutionComboBox.blockSignals(True) + self.ui.resolutionComboBox.clear() + self.ui.resolutionComboBox.blockSignals(False) + self.ui.resolutionComboBox.addItems(tuple(AreaDefinitionsManager.available_area_def_group_resolutions(self.ui.projectionComboBox.currentText()))) + self.ui.resolutionComboBox.addItem("custom") + def initializePage(self, page_id: int): if page_id == PAGE_ID_FILE_SELECTION: self._initialize_page_file_selection() elif page_id == PAGE_ID_PRODUCT_SELECTION: self._initialize_page_product_selection() + def validateCurrentPage(self) -> bool: """Check that the current page will generate the necessary data.""" @@ -289,6 +304,9 @@ def _initialize_page_product_selection(self): self.ui.selectIDTable.resizeColumnsToContents() self.ui.projectionComboBox.setCurrentIndex(self.parent().document.current_projection_index()) + + self.initializeResolutionComboBox() + self._update_resampling_method_combobox() self._update_resampling_info() @@ -721,41 +739,86 @@ def _update_activation_of_projection_combobox(self): def _update_resampling_info(self): area_def_name = self.ui.projectionComboBox.currentText() - area_def = AreaDefinitionsManager.area_def_by_name(area_def_name) - + resolution = self.ui.resolutionComboBox.currentText() + custom = False + if(resolution != "custom"): + area_def = AreaDefinitionsManager.area_def_by_group_name_and_resolution(area_def_name, resolution) + else: + area_def = AreaDefinitionsManager.area_def_by_name(area_def_name) + custom = True + resampler = self.ui.resamplingMethodComboBox.currentData() if not resampler or resampler.lower() == "none": # gracefully interpret capitalization variants of 'None' as: # "do not resample" self.resampling_info = None else: + #added custom info to this structure self.resampling_info = { "resampler": resampler, "area_id": area_def.area_id, "projection": area_def.proj_str, "radius_of_influence": self.ui.radiusOfInfluenceSpinBox.value(), "shape": (self.ui.resamplingShapeRowSpinBox.value(), self.ui.resamplingShapeColumnSpinBox.value()), + "custom" : custom } + + def _set_resampling_shape_spin_boxes_disabled(self, is_disabled): + self.ui.resamplingShapeRowSpinBox.setDisabled(is_disabled) + self.ui.resamplingShapeColumnSpinBox.setDisabled(is_disabled) + def _set_opts_disabled(self, is_disabled): self.ui.radiusOfInfluenceSpinBox.setDisabled(is_disabled) + self.ui.projectionComboBox.setDisabled(is_disabled) + self.ui.resolutionComboBox.setDisabled(is_disabled) + #spin boxes should be disabled when is_disabled = True or when resolution != custom + self._set_resampling_shape_spin_boxes_disabled(is_disabled or self.ui.resolutionComboBox.currentData() != "custom") + + ''' + # # The user should not change the projection nor the resampling shape, # thus: self.ui.projectionComboBox.setDisabled(True) # instead of 'is_disabled' self.ui.resamplingShapeRowSpinBox.setDisabled(True) # instead of 'is_disabled' self.ui.resamplingShapeColumnSpinBox.setDisabled(True) # instead of 'is_disabled' + ''' def _reset_fields(self): self.ui.resamplingMethodComboBox.setCurrentIndex(0) self.ui.radiusOfInfluenceSpinBox.setValue(5000) self.ui.projectionComboBox.setCurrentIndex(self.parent().document.current_projection_index()) + self.ui.resolutionComboBox.setCurrentIndex(0) self._set_opts_disabled(True) def _update_resampling_shape_spin_boxes(self): area_def_name = self.ui.projectionComboBox.currentText() area_def = AreaDefinitionsManager.area_def_by_name(area_def_name) + + #clear all existing items in self.ui.resolutionComboBox and then add resolution values for the chosen projection + self.initializeResolutionComboBox() + self.ui.resamplingShapeRowSpinBox.setValue(area_def.shape[0]) self.ui.resamplingShapeColumnSpinBox.setValue(area_def.shape[1]) + + + def _update_resampling_shape_spin_boxes_by_resolution_change(self): + #This function sets the correct values for the shape based on the selected resolution and projection + area_def_name = self.ui.projectionComboBox.currentText() + resolution = self.ui.resolutionComboBox.currentText() + + if(resolution != "custom"): + area_def = AreaDefinitionsManager.area_def_by_group_name_and_resolution(area_def_name, resolution) + self.ui.resamplingShapeRowSpinBox.setValue(area_def.shape[0]) + self.ui.resamplingShapeColumnSpinBox.setValue(area_def.shape[1]) + self._set_resampling_shape_spin_boxes_disabled(True) + else: + self._set_resampling_shape_spin_boxes_disabled(False) + + def _update_parent_projection_combobox(self): + parent = self.parent() + parent.ui.projectionComboBox.setCurrentIndex(self.ui.projectionComboBox.currentIndex()) + def _update_grouping_mode_combobox(self): reader = self.get_reader() diff --git a/uwsift/workspace/importer.py b/uwsift/workspace/importer.py index 3d4fabc8..c7acc066 100644 --- a/uwsift/workspace/importer.py +++ b/uwsift/workspace/importer.py @@ -34,6 +34,7 @@ import satpy.readers.yaml_reader import satpy.resample import yaml +from docutils.nodes import target from pyresample.geometry import AreaDefinition, StackedAreaDefinition, SwathDefinition from satpy import DataQuery, Scene, available_readers from satpy.dataset import DatasetDict @@ -48,6 +49,7 @@ from uwsift.util.common import get_reader_kwargs_dict from uwsift.workspace.guidebook import ABI_AHI_Guidebook + from .metadatabase import ( Content, ContentImage, @@ -1260,21 +1262,37 @@ def _create_unstructured_points_dataset_content(self, dataset, now, prod): def _preprocess_products_with_resampling(self) -> None: resampler: str = self.resampling_info["resampler"] max_area = self.scn.finest_area() - if isinstance(max_area, AreaDefinition) and max_area.area_id == self.resampling_info["area_id"]: + if isinstance(max_area, AreaDefinition) and max_area.area_id == self.resampling_info["area_id"] and self.resampling_info["custom"] == False: + #resampling is not needed LOG.info( f"Source and target area ID are identical:" f" '{self.resampling_info['area_id']}'." f" Skipping resampling." ) else: + #resampling is needed area_name = max_area.area_id if hasattr(max_area, "area_id") else max_area.name - LOG.info( - f"Resampling from area ID/name '{area_name}'" - f" to area ID '{self.resampling_info['area_id']}'" - f" with method '{resampler}'" - ) target_area_def = AreaDefinitionsManager.area_def_by_id(self.resampling_info["area_id"]) + if(self.resampling_info["custom"] == False): + LOG.info( + f"Resampling from area ID/name '{area_name}'" + f" to area ID '{self.resampling_info['area_id']}'" + f" with method '{resampler}'" + ) + else: + #custom resolution + resampler_shape = self.resampling_info["shape"] + resampler_width = resampler_shape[0] + resampler_height = resampler_shape[1] + LOG.info( + f"Resampling area with the characteristics of '{area_name}'" + f" with method '{resampler}'" + f" with CUSTOM SHAPE: '{resampler_width}, {resampler_height}'" + ) + + AreaDefinitionsManager.prepare_area_def_for_resampling(target_area_def, resampler_width, resampler_height) + # About the next strange line of code: keep a reference to the # original scene to work around an issue in the resampling # implementation for NetCDF data: otherwise the original data @@ -1284,14 +1302,13 @@ def _preprocess_products_with_resampling(self) -> None: # deactivating reduce_data, see https://github.com/pytroll/satpy/issues/2476 reduce_data = False if resampler == "native" else True - self.scn = self.scn.resample( target_area_def, resampler=resampler, radius_of_influence=self.resampling_info["radius_of_influence"], reduce_data=reduce_data, ) - + def _get_fci_segment_height(self, segment_number: int, segment_width: int) -> int: try: seg_heights = self.scn._readers[self.reader].segment_heights diff --git a/uwsift/workspace/simple_workspace.py b/uwsift/workspace/simple_workspace.py index 8e3fcaa6..d2536825 100644 --- a/uwsift/workspace/simple_workspace.py +++ b/uwsift/workspace/simple_workspace.py @@ -265,6 +265,7 @@ def import_product_content( return arrays.data truck = aImporter.from_product(prod, workspace_cwd=self.cache_dir, database_session=None, **importer_kwargs) + if not truck: # aImporter.from_product() didn't return an Importer instance # since all files represent data granules, which are already