def __init__(self, iface: QgisInterface) -> None: super().__init__() self.iface = iface # Import/Export ## Import from 'namelist.wps' import_from_namelist_button = QPushButton("Import from namelist") import_from_namelist_button.setObjectName('import_from_namelist') ## Export to namelist export_geogrid_namelist_button = QPushButton("Export to namelist") export_geogrid_namelist_button.setObjectName( 'export_geogrid_namelist_button') vbox_import_export = QVBoxLayout() vbox_import_export.addWidget(import_from_namelist_button) vbox_import_export.addWidget(export_geogrid_namelist_button) self.gbox_import_export = QGroupBox("Import/Export") self.gbox_import_export.setLayout(vbox_import_export) # Group: Map Type self.group_box_map_type = QGroupBox("Map Type") vbox_map_type = QVBoxLayout() hbox_map_type = QHBoxLayout() self.projection = QComboBox() self.projection.setObjectName('projection') projs = { 'undefined': '-', # do not use a default projection - let the user pick the projection. 'lat-lon': 'Latitude/Longitude', 'lambert': 'Lambert' } for proj_id, proj_label in projs.items(): self.projection.addItem(proj_label, proj_id) hbox_map_type.addWidget(QLabel('GCS/Projection:')) # TODO: when the user select the type of GCS/Projection # we should automatically change the GCS/Projection for the whole # project. This will do an on-the-fly remapping of any of the other CRS # which are different from the one supported by our tool/WRF. # The Project CRS can be accessed in QGIS under the menu `Project` > `Project Properties` > `CRS.` # -> This is only really possible for Lat/Lon as this one is a CRS, where the others are projections # that only become a full CRS with the additional parameters like truelat1. # TODO: fields should be cleared when the user changes the CRS/Projection. hbox_map_type.addWidget(self.projection) vbox_map_type.addLayout(hbox_map_type) ## Lambert only: show 'True Latitudes' field truelat_grid = QGridLayout() self.truelat1 = add_grid_lineedit(truelat_grid, 0, 'True Latitude 1', LAT_VALIDATOR, unit='°', required=True) self.truelat2 = add_grid_lineedit(truelat_grid, 1, 'True Latitude 2', LAT_VALIDATOR, unit='°', required=True) self.widget_true_lats = QWidget() self.widget_true_lats.setLayout(truelat_grid) vbox_map_type.addWidget(self.widget_true_lats) self.domain_pb_set_projection = QPushButton("Set Map CRS") self.domain_pb_set_projection.setObjectName('set_projection_button') vbox_map_type.addWidget(self.domain_pb_set_projection) self.group_box_map_type.setLayout(vbox_map_type) # Group: Horizontal Resolution self.group_box_resol = QGroupBox("Horizontal Grid Spacing") hbox_resol = QHBoxLayout() self.resolution = MyLineEdit(required=True) self.resolution.setValidator(RESOLUTION_VALIDATOR) self.resolution.textChanged.connect( lambda _: update_input_validation_style(self.resolution)) self.resolution.textChanged.emit(self.resolution.text()) hbox_resol.addWidget(self.resolution) self.resolution_label = QLabel() hbox_resol.addWidget(self.resolution_label) self.group_box_resol.setLayout(hbox_resol) # Group: Automatic Domain Generator self.group_box_auto_domain = QGroupBox("Grid Extent Calculator") vbox_auto_domain = QVBoxLayout() hbox_auto_domain = QHBoxLayout() domain_pb_set_canvas_extent = QPushButton("Set to Canvas Extent") domain_pb_set_canvas_extent.setObjectName('set_canvas_extent_button') domain_pb_set_layer_extent = QPushButton("Set to Active Layer Extent") domain_pb_set_layer_extent.setObjectName('set_layer_extent_button') vbox_auto_domain.addLayout(hbox_auto_domain) vbox_auto_domain.addWidget(domain_pb_set_canvas_extent) vbox_auto_domain.addWidget(domain_pb_set_layer_extent) self.group_box_auto_domain.setLayout(vbox_auto_domain) # Group: Manual Domain Configuration ## Subgroup: Centre Point grid_center_point = QGridLayout() self.center_lon = add_grid_lineedit(grid_center_point, 0, 'Longitude', LON_VALIDATOR, '°', required=True) self.center_lat = add_grid_lineedit(grid_center_point, 1, 'Latitude', LAT_VALIDATOR, '°', required=True) group_box_centre_point = QGroupBox("Center Point") group_box_centre_point.setLayout(grid_center_point) ## Subgroup: Advanced configuration grid_dims = QGridLayout() self.cols = add_grid_lineedit(grid_dims, 0, 'Horizontal', DIM_VALIDATOR, required=True) self.rows = add_grid_lineedit(grid_dims, 1, 'Vertical', DIM_VALIDATOR, required=True) group_box_dims = QGroupBox("Grid Extent") group_box_dims.setLayout(grid_dims) vbox_manual_domain = QVBoxLayout() vbox_manual_domain.addWidget(group_box_centre_point) vbox_manual_domain.addWidget(group_box_dims) self.group_box_manual_domain = QGroupBox("Advanced Configuration") # TODO: make this section collapsable (default state collapsed) # and change the checkbox to arrows like `setArrowType(Qt.RightArrow)` # TODO: the style should be disabled when the 'advanced configuration' box is disabled (default). self.group_box_manual_domain.setCheckable(True) self.group_box_manual_domain.setChecked(False) self.group_box_manual_domain.setLayout(vbox_manual_domain) for field in [ self.resolution, self.center_lat, self.center_lon, self.rows, self.cols, self.truelat1, self.truelat2 ]: # editingFinished is only emitted on user input, not via programmatic changes. # This is important as we want to avoid re-drawing the bbox many times when several # fields get changed while using the automatic domain generator. field.editingFinished.connect(self.on_change_any_field) # Group Box: Parent Domain self.group_box_parent_domain = QGroupBox("Enable Parenting") self.group_box_parent_domain.setObjectName('group_box_parent_domain') self.group_box_parent_domain.setCheckable(True) self.group_box_parent_domain.setChecked(False) # TODO: As it is for the single domain case the generation of the domain should be automatic. # For now leave placeholder values of '3' for Child to Parent Ratio and '2' for padding. # use `calc_parent_lonlat_from_child` in `routines.py` to calculate the coordinate of the domain given the child domain coordinate, # grid_ratio and padding. hbox_parent_num = QHBoxLayout() hbox_parent_num.addWidget(QLabel('Number of Parent Domains:')) self.parent_spin = QSpinBox() self.parent_spin.setObjectName('parent_spin') self.parent_spin.setRange(1, MAX_PARENTS) hbox_parent_num.addWidget(self.parent_spin) self.group_box_parent_domain.setLayout(hbox_parent_num) self.parent_domains = [] # type: list self.parent_vbox = QVBoxLayout() self.parent_vbox.setSizeConstraint(QLayout.SetMinimumSize) go_to_data_tab_btn = QPushButton('Continue to Datasets') go_to_data_tab_btn.clicked.connect(self.go_to_data_tab) # Tabs dom_mgr_layout = QVBoxLayout() dom_mgr_layout.addWidget(self.gbox_import_export) dom_mgr_layout.addWidget(self.group_box_map_type) dom_mgr_layout.addWidget(self.group_box_resol) dom_mgr_layout.addWidget(self.group_box_auto_domain) dom_mgr_layout.addWidget(self.group_box_manual_domain) dom_mgr_layout.addWidget(self.group_box_parent_domain) dom_mgr_layout.addLayout(self.parent_vbox) dom_mgr_layout.addWidget(go_to_data_tab_btn) self.setLayout(dom_mgr_layout) QMetaObject.connectSlotsByName(self) # trigger event for initial layout self.projection.currentIndexChanged.emit( self.projection.currentIndex())
class DomainWidget(QWidget): tab_active = pyqtSignal() go_to_data_tab = pyqtSignal() def __init__(self, iface: QgisInterface) -> None: super().__init__() self.iface = iface # Import/Export ## Import from 'namelist.wps' import_from_namelist_button = QPushButton("Import from namelist") import_from_namelist_button.setObjectName('import_from_namelist') ## Export to namelist export_geogrid_namelist_button = QPushButton("Export to namelist") export_geogrid_namelist_button.setObjectName( 'export_geogrid_namelist_button') vbox_import_export = QVBoxLayout() vbox_import_export.addWidget(import_from_namelist_button) vbox_import_export.addWidget(export_geogrid_namelist_button) self.gbox_import_export = QGroupBox("Import/Export") self.gbox_import_export.setLayout(vbox_import_export) # Group: Map Type self.group_box_map_type = QGroupBox("Map Type") vbox_map_type = QVBoxLayout() hbox_map_type = QHBoxLayout() self.projection = QComboBox() self.projection.setObjectName('projection') projs = { 'undefined': '-', # do not use a default projection - let the user pick the projection. 'lat-lon': 'Latitude/Longitude', 'lambert': 'Lambert' } for proj_id, proj_label in projs.items(): self.projection.addItem(proj_label, proj_id) hbox_map_type.addWidget(QLabel('GCS/Projection:')) # TODO: when the user select the type of GCS/Projection # we should automatically change the GCS/Projection for the whole # project. This will do an on-the-fly remapping of any of the other CRS # which are different from the one supported by our tool/WRF. # The Project CRS can be accessed in QGIS under the menu `Project` > `Project Properties` > `CRS.` # -> This is only really possible for Lat/Lon as this one is a CRS, where the others are projections # that only become a full CRS with the additional parameters like truelat1. # TODO: fields should be cleared when the user changes the CRS/Projection. hbox_map_type.addWidget(self.projection) vbox_map_type.addLayout(hbox_map_type) ## Lambert only: show 'True Latitudes' field truelat_grid = QGridLayout() self.truelat1 = add_grid_lineedit(truelat_grid, 0, 'True Latitude 1', LAT_VALIDATOR, unit='°', required=True) self.truelat2 = add_grid_lineedit(truelat_grid, 1, 'True Latitude 2', LAT_VALIDATOR, unit='°', required=True) self.widget_true_lats = QWidget() self.widget_true_lats.setLayout(truelat_grid) vbox_map_type.addWidget(self.widget_true_lats) self.domain_pb_set_projection = QPushButton("Set Map CRS") self.domain_pb_set_projection.setObjectName('set_projection_button') vbox_map_type.addWidget(self.domain_pb_set_projection) self.group_box_map_type.setLayout(vbox_map_type) # Group: Horizontal Resolution self.group_box_resol = QGroupBox("Horizontal Grid Spacing") hbox_resol = QHBoxLayout() self.resolution = MyLineEdit(required=True) self.resolution.setValidator(RESOLUTION_VALIDATOR) self.resolution.textChanged.connect( lambda _: update_input_validation_style(self.resolution)) self.resolution.textChanged.emit(self.resolution.text()) hbox_resol.addWidget(self.resolution) self.resolution_label = QLabel() hbox_resol.addWidget(self.resolution_label) self.group_box_resol.setLayout(hbox_resol) # Group: Automatic Domain Generator self.group_box_auto_domain = QGroupBox("Grid Extent Calculator") vbox_auto_domain = QVBoxLayout() hbox_auto_domain = QHBoxLayout() domain_pb_set_canvas_extent = QPushButton("Set to Canvas Extent") domain_pb_set_canvas_extent.setObjectName('set_canvas_extent_button') domain_pb_set_layer_extent = QPushButton("Set to Active Layer Extent") domain_pb_set_layer_extent.setObjectName('set_layer_extent_button') vbox_auto_domain.addLayout(hbox_auto_domain) vbox_auto_domain.addWidget(domain_pb_set_canvas_extent) vbox_auto_domain.addWidget(domain_pb_set_layer_extent) self.group_box_auto_domain.setLayout(vbox_auto_domain) # Group: Manual Domain Configuration ## Subgroup: Centre Point grid_center_point = QGridLayout() self.center_lon = add_grid_lineedit(grid_center_point, 0, 'Longitude', LON_VALIDATOR, '°', required=True) self.center_lat = add_grid_lineedit(grid_center_point, 1, 'Latitude', LAT_VALIDATOR, '°', required=True) group_box_centre_point = QGroupBox("Center Point") group_box_centre_point.setLayout(grid_center_point) ## Subgroup: Advanced configuration grid_dims = QGridLayout() self.cols = add_grid_lineedit(grid_dims, 0, 'Horizontal', DIM_VALIDATOR, required=True) self.rows = add_grid_lineedit(grid_dims, 1, 'Vertical', DIM_VALIDATOR, required=True) group_box_dims = QGroupBox("Grid Extent") group_box_dims.setLayout(grid_dims) vbox_manual_domain = QVBoxLayout() vbox_manual_domain.addWidget(group_box_centre_point) vbox_manual_domain.addWidget(group_box_dims) self.group_box_manual_domain = QGroupBox("Advanced Configuration") # TODO: make this section collapsable (default state collapsed) # and change the checkbox to arrows like `setArrowType(Qt.RightArrow)` # TODO: the style should be disabled when the 'advanced configuration' box is disabled (default). self.group_box_manual_domain.setCheckable(True) self.group_box_manual_domain.setChecked(False) self.group_box_manual_domain.setLayout(vbox_manual_domain) for field in [ self.resolution, self.center_lat, self.center_lon, self.rows, self.cols, self.truelat1, self.truelat2 ]: # editingFinished is only emitted on user input, not via programmatic changes. # This is important as we want to avoid re-drawing the bbox many times when several # fields get changed while using the automatic domain generator. field.editingFinished.connect(self.on_change_any_field) # Group Box: Parent Domain self.group_box_parent_domain = QGroupBox("Enable Parenting") self.group_box_parent_domain.setObjectName('group_box_parent_domain') self.group_box_parent_domain.setCheckable(True) self.group_box_parent_domain.setChecked(False) # TODO: As it is for the single domain case the generation of the domain should be automatic. # For now leave placeholder values of '3' for Child to Parent Ratio and '2' for padding. # use `calc_parent_lonlat_from_child` in `routines.py` to calculate the coordinate of the domain given the child domain coordinate, # grid_ratio and padding. hbox_parent_num = QHBoxLayout() hbox_parent_num.addWidget(QLabel('Number of Parent Domains:')) self.parent_spin = QSpinBox() self.parent_spin.setObjectName('parent_spin') self.parent_spin.setRange(1, MAX_PARENTS) hbox_parent_num.addWidget(self.parent_spin) self.group_box_parent_domain.setLayout(hbox_parent_num) self.parent_domains = [] # type: list self.parent_vbox = QVBoxLayout() self.parent_vbox.setSizeConstraint(QLayout.SetMinimumSize) go_to_data_tab_btn = QPushButton('Continue to Datasets') go_to_data_tab_btn.clicked.connect(self.go_to_data_tab) # Tabs dom_mgr_layout = QVBoxLayout() dom_mgr_layout.addWidget(self.gbox_import_export) dom_mgr_layout.addWidget(self.group_box_map_type) dom_mgr_layout.addWidget(self.group_box_resol) dom_mgr_layout.addWidget(self.group_box_auto_domain) dom_mgr_layout.addWidget(self.group_box_manual_domain) dom_mgr_layout.addWidget(self.group_box_parent_domain) dom_mgr_layout.addLayout(self.parent_vbox) dom_mgr_layout.addWidget(go_to_data_tab_btn) self.setLayout(dom_mgr_layout) QMetaObject.connectSlotsByName(self) # trigger event for initial layout self.projection.currentIndexChanged.emit( self.projection.currentIndex()) @property def project(self) -> Project: return self._project @project.setter def project(self, val: Project) -> None: ''' Sets the currently active project. See tab_simulation. ''' self._project = val self.populate_ui_from_project() def populate_ui_from_project(self) -> None: project = self.project try: domains = project.data['domains'] except KeyError: return main_domain = domains[0] idx = self.projection.findData(main_domain['map_proj']) self.projection.setCurrentIndex(idx) try: truelat1 = main_domain['truelat1'] self.truelat1.set_value(truelat1) truelat2 = main_domain['truelat2'] self.truelat2.set_value(truelat2) except KeyError: pass self.resolution.set_value(main_domain['cell_size'][0]) lon, lat = main_domain['center_lonlat'] self.center_lat.set_value(lat) self.center_lon.set_value(lon) cols, rows = main_domain['domain_size'] self.rows.set_value(rows) self.cols.set_value(cols) if len(domains) > 1: self.group_box_parent_domain.setChecked(True) self.parent_spin.setValue(len(domains) - 1) # We call the signal handler explicitly as we need the widgets ready immediately # and otherwise this is delayed until the signals are processed (queueing etc.). self.on_parent_spin_valueChanged(len(domains) - 1) for idx, parent_domain in enumerate(domains[1:]): fields, _ = self.parent_domains[idx] fields = fields['inputs'] field_to_key = { 'ratio': 'parent_cell_size_ratio', 'top': 'padding_top', 'left': 'padding_left', 'right': 'padding_right', 'bottom': 'padding_bottom' } for field_name, key in field_to_key.items(): field = fields[field_name] val = parent_domain[key] field.set_value(val) self.draw_bbox_and_grids(zoom_out=True) @pyqtSlot() def on_export_geogrid_namelist_button_clicked(self): if not self.update_project(): raise ValueError('Input invalid, check fields') folder = QFileDialog.getExistingDirectory( caption='Select namelist.wps output folder') if not folder: return self.project.export_namelists(folder) @pyqtSlot() def on_set_projection_button_clicked(self): crs = self.create_domain_crs() qgsProject = QgsProject.instance() # type: QgsProject qgsProject.setCrs(get_qgis_crs(crs.proj4)) def create_domain_crs(self) -> CRS: proj = self.get_proj_kwargs() if proj is None: raise ValueError('Incomplete projection definition') origin_valid = all( map(lambda w: w.is_valid(), [self.center_lat, self.center_lon])) if origin_valid: origin = LonLat(self.center_lon.value(), self.center_lat.value()) else: origin = LonLat(0, 0) if proj['map_proj'] == 'lambert': crs = CRS.create_lambert(proj['truelat1'], proj['truelat2'], origin) elif proj['map_proj'] == 'lat-lon': crs = CRS.create_lonlat() else: raise NotImplementedError('unknown proj: ' + proj['map_proj']) return crs @pyqtSlot() def on_set_canvas_extent_button_clicked(self): if not self.resolution.is_valid(): return canvas = self.iface.mapCanvas() # type: QgsMapCanvas settings = canvas.mapSettings() # type: QgsMapSettings map_crs = settings.destinationCrs( ) # type: QgsCoordinateReferenceSystem extent = canvas.extent() # type: QgsRectangle self.set_domain_to_extent(map_crs, extent) @pyqtSlot() def on_set_layer_extent_button_clicked(self): if not self.resolution.is_valid(): return layer = self.iface.activeLayer() # type: QgsMapLayer layer_crs = layer.crs() # type: QgsCoordinateReferenceSystem extent = layer.extent() # type: QgsRectangle self.set_domain_to_extent(layer_crs, extent) def set_domain_to_extent(self, crs: QgsCoordinateReferenceSystem, extent: QgsRectangle) -> None: resolution = self.resolution.value() bbox = rect_to_bbox(extent) extent_crs = CRS(crs.toProj4()) domain_crs = self.create_domain_crs() domain_srs = domain_crs.srs domain_bbox = domain_crs.transform_bbox(bbox, domain_srs) # TODO disallow creation of bounding box outside projection range (e.g. for lat-lon 360-180) xmin, xmax, ymin, ymax = domain_bbox.minx, domain_bbox.maxx, domain_bbox.miny, domain_bbox.maxy center_x = xmin + (xmax - xmin) / 2 center_y = ymin + (ymax - ymin) / 2 center_lonlat = domain_crs.to_lonlat(Coordinate2D(center_x, center_y)) self.center_lat.set_value(center_lonlat.lat) self.center_lon.set_value(center_lonlat.lon) self.resolution.set_value(resolution) cols = ceil((xmax - xmin) / resolution) rows = ceil((ymax - ymin) / resolution) self.cols.set_value(cols) self.rows.set_value(rows) self.on_change_any_field(zoom_out=True) @pyqtSlot() def on_group_box_parent_domain_clicked(self): if self.group_box_parent_domain.isChecked(): self.add_parent_domain() else: self.parent_spin.setValue(1) while self.parent_domains: self.remove_last_parent_domain() def add_parent_domain(self): idx = len(self.parent_domains) + 1 fields, group_box_parent = create_parent_group_box('Parent ' + str(idx), '?', self.proj_res_unit, required=True) self.parent_vbox.addWidget(group_box_parent) # "If you add a child widget to an already visible widget you must # explicitly show the child to make it visible." # (http://doc.qt.io/qt-5/qwidget.html#QWidget) group_box_parent.show() self.parent_domains.append((fields, group_box_parent)) # After adding/removing widgets, we need to tell Qt to recompute the sizes. # This always has to be done on the widget where the child widgets have been changed, # here self.subtab_parenting (which contains self.parent_vbox). self.adjustSize() for field in fields['inputs'].values(): field.editingFinished.connect(self.on_change_any_field) def remove_last_parent_domain(self): _, group_box_parent = self.parent_domains.pop() group_box_parent.deleteLater() self.parent_vbox.removeWidget(group_box_parent) self.on_change_any_field() @pyqtSlot(int) def on_parent_spin_valueChanged(self, value: int) -> None: count = len(self.parent_domains) for _ in range(value, count): self.remove_last_parent_domain() for _ in range(count, value): self.add_parent_domain() @pyqtSlot(int) def on_projection_currentIndexChanged(self, index: int) -> None: proj_id = self.projection.currentData() is_lambert = proj_id == 'lambert' is_undefined = proj_id == 'undefined' is_lat_lon = proj_id == 'lat-lon' self.group_box_resol.setDisabled(is_undefined) self.group_box_auto_domain.setDisabled(is_undefined) self.group_box_manual_domain.setDisabled(is_undefined) self.group_box_parent_domain.setDisabled(is_undefined) self.widget_true_lats.setVisible(is_lambert) if is_undefined: self.proj_res_unit = '' elif is_lat_lon: self.proj_res_unit = '°' elif is_lambert: self.proj_res_unit = 'm' self.resolution_label.setText(self.proj_res_unit) # If the projection is changed the parent domains are removed self.group_box_parent_domain.setChecked(False) for _ in self.parent_domains: self.remove_last_parent_domain() self.adjustSize() def get_proj_kwargs(self) -> dict: proj_id = self.projection.currentData() kwargs = {'map_proj': proj_id} if proj_id == 'lambert': valid = all( map(lambda w: w.is_valid(), [self.truelat1, self.truelat2])) if not valid: return None kwargs = { 'map_proj': proj_id, 'truelat1': self.truelat1.value(), 'truelat2': self.truelat2.value(), } return kwargs def update_project(self) -> bool: proj_kwargs = self.get_proj_kwargs() if proj_kwargs is None: return False valid = all( map(lambda w: w.is_valid(), [ self.center_lat, self.center_lon, self.resolution, self.cols, self.rows ])) if not valid: return False center_lonlat = LonLat(lon=self.center_lon.value(), lat=self.center_lat.value()) resolution = self.resolution.value() domain_size = (self.cols.value(), self.rows.value()) parent_domains = [] for parent_domain in self.parent_domains: fields, _ = parent_domain inputs = fields['inputs'] valid = all(map(lambda w: w.is_valid(), inputs.values())) if not valid: return False ratio, top, left, right, bottom = [ inputs[name].value() for name in ['ratio', 'top', 'left', 'right', 'bottom'] ] parent_domains.append({ 'parent_cell_size_ratio': ratio, 'padding_left': left, 'padding_right': right, 'padding_bottom': bottom, 'padding_top': top }) self.project.set_domains(cell_size=(resolution, resolution), domain_size=domain_size, center_lonlat=center_lonlat, parent_domains=parent_domains, **proj_kwargs) return True def on_change_any_field(self, zoom_out=False): if not self.update_project(): return domains = self.project.data['domains'] # update main domain size as it may have been adjusted main_domain_size = domains[0]['domain_size'] self.cols.set_value(main_domain_size[0]) self.rows.set_value(main_domain_size[1]) for (fields, _), domain in zip(self.parent_domains, domains[1:]): # update the parent resolutions res_label = fields['other']['resolution'] res_label.setText( HORIZONTAL_RESOLUTION_LABEL.format( resolution=domain['cell_size'][0], unit=self.proj_res_unit)) # update any padding as it may have been adjusted for name in ['left', 'right', 'top', 'bottom']: field = fields['inputs'][name] field.set_value(domain['padding_' + name]) self.draw_bbox_and_grids(zoom_out) def draw_bbox_and_grids(self, zoom_out: bool) -> None: project = self.project update_domain_grid_layers(project) update_domain_outline_layers(self.iface.mapCanvas(), project, zoom_out=zoom_out)