From 8c46f7b613d2f707c9b8ae32ed9545afcb56e0a7 Mon Sep 17 00:00:00 2001 From: Kelly W Date: Thu, 16 Jan 2025 16:53:30 -0800 Subject: [PATCH 1/9] Allow analyses with AOIs and valley bottoms #561 --- src/model/project.py | 8 +++++++- src/view/frm_analysis_docwidget.py | 2 +- src/view/frm_analysis_properties.py | 20 +++++++++++++++----- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/model/project.py b/src/model/project.py index 67129ed0..0e4b4b39 100644 --- a/src/model/project.py +++ b/src/model/project.py @@ -94,15 +94,21 @@ def __init__(self, project_file: str): self.events = load_events(curs, self.protocols, self.methods, self.layers, self.lookup_tables, self.rasters) self.planning_containers = load_planning_containers(curs, self.events) self.metrics = load_metrics(curs) - self.analyses = load_analyses(curs, self.sample_frames, self.metrics) self.pour_points = load_pour_points(curs) self.stream_gages = load_stream_gages(curs) self.profiles = load_profiles(curs) self.cross_sections = load_cross_sections(curs) self.valley_bottoms = load_sample_frames(curs, sample_frame_type=SampleFrame.VALLEY_BOTTOM_SAMPLE_FRAME_TYPE) + self.analyses = load_analyses(curs, self.analysis_masks(), self.metrics) self.units = load_units(curs) + def analysis_masks(self) -> dict: + masks = self.sample_frames.copy() + masks.update(self.aois) + masks.update(self.valley_bottoms) + return masks + def get_relative_path(self, absolute_path: str) -> str: return parse_posix_path(os.path.relpath(absolute_path, os.path.dirname(self.project_file))) diff --git a/src/view/frm_analysis_docwidget.py b/src/view/frm_analysis_docwidget.py index 26fc3b1f..2bfe18e6 100644 --- a/src/view/frm_analysis_docwidget.py +++ b/src/view/frm_analysis_docwidget.py @@ -375,7 +375,7 @@ def setupUi(self): self.cmdCalculate.setToolTipDuration(2000) self.horizEvent.addWidget(self.cmdCalculate, 0) - self.lblSegment = QtWidgets.QLabel('Sample Frame Label') + self.lblSegment = QtWidgets.QLabel('Mask Polygon') self.grid.addWidget(self.lblSegment, 2, 0, 1, 1) self.cboSampleFrame = QtWidgets.QComboBox() diff --git a/src/view/frm_analysis_properties.py b/src/view/frm_analysis_properties.py index 68b57e71..c6d03d2e 100644 --- a/src/view/frm_analysis_properties.py +++ b/src/view/frm_analysis_properties.py @@ -26,9 +26,10 @@ def __init__(self, parent, project: Project, analysis: Analysis = None): self.setupUi() # Sample Frames - self.sampling_frames = {id: sample_frame for id, sample_frame in project.sample_frames.items()} + self.sampling_frames = {id: sample_frame for id, sample_frame in project.analysis_masks().items()} self.sampling_frames_model = DBItemModel(self.sampling_frames) self.cboSampleFrame.setModel(self.sampling_frames_model) + self.cboSampleFrame.currentIndexChanged.connect(self.on_cboSampleFrame_currentIndexChanged) # Valley Bottoms self.valley_bottoms = {id: valley_bottom for id, valley_bottom in project.valley_bottoms.items()} @@ -142,6 +143,18 @@ def toggle_all_metrics(self, level_id: str): idx = cboStatus.findText(level_id) cboStatus.setCurrentIndex(idx) + def on_cboSampleFrame_currentIndexChanged(self, index): + + # if the sample frame type is Valley Bottom, then set the Valley Bottom combo box to the selected valley bottom as well, then lock that combo box. if not, then unlock the combo box + sample_frame: SampleFrame = self.cboSampleFrame.currentData(QtCore.Qt.UserRole) + if sample_frame is not None: + if sample_frame.sample_frame_type == SampleFrame.VALLEY_BOTTOM_SAMPLE_FRAME_TYPE: + index = self.cboValleyBottom.findData(sample_frame) + self.cboValleyBottom.setCurrentIndex(index) + self.cboValleyBottom.setEnabled(False) + else: + self.cboValleyBottom.setEnabled(True) + def setupUi(self): self.setMinimumSize(500, 500) @@ -159,15 +172,12 @@ def setupUi(self): self.txtName = QtWidgets.QLineEdit() self.grdLayout1.addWidget(self.txtName, 0, 1, 1, 1) - self.lblSampleFrame = QtWidgets.QLabel('Sample Frame') + self.lblSampleFrame = QtWidgets.QLabel('Analysis Masks (Sample Frame)') self.grdLayout1.addWidget(self.lblSampleFrame, 1, 0, 1, 1) self.cboSampleFrame = QtWidgets.QComboBox() self.grdLayout1.addWidget(self.cboSampleFrame, 1, 1, 1, 1) - # self.groupboxInputs = QtWidgets.QGroupBox('Inputs') - # self.vert.addWidget(self.groupboxInputs) - self.lblValleyBottom = QtWidgets.QLabel('Valley Bottom') self.grdLayout1.addWidget(self.lblValleyBottom, 2, 0, 1, 1) From d1c58b361e2f6f90d0b52dac49f9642dc0212e11 Mon Sep 17 00:00:00 2001 From: Kelly W Date: Thu, 16 Jan 2025 16:53:59 -0800 Subject: [PATCH 2/9] prepare sample frame widget to include aoi and valley bottoms --- src/view/widgets/sample_frames.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/view/widgets/sample_frames.py b/src/view/widgets/sample_frames.py index 71ab64b9..a6cb7f67 100644 --- a/src/view/widgets/sample_frames.py +++ b/src/view/widgets/sample_frames.py @@ -15,16 +15,22 @@ class SampleFrameWidget(QtWidgets.QWidget): sample_frame_changed = pyqtSignal() - def __init__(self, parent: QtWidgets.QWidget, qris_project: Project, qris_map_manager: QRisMapManager = None, first_index_empty: bool = False): + def __init__(self, parent: QtWidgets.QWidget, qris_project: Project, qris_map_manager: QRisMapManager = None, first_index_empty: bool = False, sample_frame_types: list = [SampleFrame.SAMPLE_FRAME_TYPE]): super().__init__(parent) self.qris_project = qris_project self.qris_map_manager = qris_map_manager + self.sample_frame_types = sample_frame_types self.setupUi() # Sample Frames self.sample_frames = {id: sample_frame for id, sample_frame in self.qris_project.sample_frames.items()} + if SampleFrame.AOI_SAMPLE_FRAME_TYPE in self.sample_frame_types: + self.sample_frames.update({id: sample_frame for id, sample_frame in self.qris_project.aois.items()}) + if SampleFrame.VALLEY_BOTTOM_SAMPLE_FRAME_TYPE in self.sample_frame_types: + self.sample_frames.update({id: sample_frame for id, sample_frame in self.qris_project.valley_bottoms.items()}) + if first_index_empty: choose_sample_frame = DBItem('None', 0, 'Choose Sample Frame...') else: From 63cc10f9451ef762f2da3c44bdaa27de9b27a2a2 Mon Sep 17 00:00:00 2001 From: Kelly W Date: Fri, 17 Jan 2025 15:34:32 -0800 Subject: [PATCH 3/9] Allow clip to AOI when importing existing feature class into DCE layer #562 --- src/view/frm_import_dce_layer.py | 36 +++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/src/view/frm_import_dce_layer.py b/src/view/frm_import_dce_layer.py index 090dd821..7ef91613 100644 --- a/src/view/frm_import_dce_layer.py +++ b/src/view/frm_import_dce_layer.py @@ -4,7 +4,7 @@ from qgis.utils import iface from ..model.project import Project -from ..model.db_item import DBItem +from ..model.db_item import DBItem, DBItemModel from ..gp.feature_class_functions import get_field_names, get_field_values from ..gp.import_feature_class import ImportFeatureClass, ImportFieldMap from ..gp.import_temp_layer import ImportTemporaryLayer @@ -58,6 +58,15 @@ def __init__(self, parent, project: Project, db_item: DBItem, import_path: str): self.txtTargetFC.setText(self.db_item.layer.fc_name) self.txtEvent.setText(self.qris_event.name) + # Masks (filtered to just AOI) + self.clipping_masks = {id: aoi for id, aoi in self.qris_project.aois.items()} + no_clipping = DBItem('None', 0, 'None - Retain full dataset extent') + self.clipping_masks[0] = no_clipping + self.masks_model = DBItemModel(self.clipping_masks) + self.cboMaskClip.setModel(self.masks_model) + # Default to no mask clipping + self.cboMaskClip.setCurrentIndex(self.masks_model.getItemIndex(no_clipping)) + self.load_fields() def load_fields(self): @@ -212,10 +221,17 @@ def accept(self): try: layer_attributes = {'event_id': self.db_item.event_id, 'event_layer_id': self.db_item.layer.id} + clip_mask = None + clip_item = self.cboMaskClip.currentData(QtCore.Qt.UserRole) + if clip_item is not None: + if clip_item.id > 0: + clip_mask = ('sample_frame_features', 'sample_frame_id', clip_item.id) + + if self.temp_layer is not None: - import_task = ImportTemporaryLayer(self.temp_layer, self.target_path, layer_attributes, field_maps) + import_task = ImportTemporaryLayer(self.temp_layer, self.target_path, layer_attributes, field_maps, clip_mask) else: - import_task = ImportFeatureClass(self.import_path, self.target_path, layer_attributes, field_maps) + import_task = ImportFeatureClass(self.import_path, self.target_path, layer_attributes, field_maps, clip_mask) self.buttonBox.setEnabled(False) # DEBUG # result = import_task.run() @@ -283,17 +299,23 @@ def setupUi(self): self.txtEvent.setReadOnly(True) self.grid.addWidget(self.txtEvent, 1, 1) + self.lblMaskClip = QtWidgets.QLabel('Clip to AOI') + self.grid.addWidget(self.lblMaskClip, 2, 0) + + self.cboMaskClip = QtWidgets.QComboBox() + self.grid.addWidget(self.cboMaskClip, 2, 1) + self.lblTargetFC = QtWidgets.QLabel('Target Layer') - self.grid.addWidget(self.lblTargetFC, 2, 0) + self.grid.addWidget(self.lblTargetFC, 3, 0) self.txtTargetFC = QtWidgets.QLineEdit() self.txtTargetFC.setReadOnly(True) - self.grid.addWidget(self.txtTargetFC, 2, 1) + self.grid.addWidget(self.txtTargetFC, 3, 1) self.lblFields = QtWidgets.QLabel('Fields') - self.grid.addWidget(self.lblFields, 3, 0) + self.grid.addWidget(self.lblFields, 4, 0) self.horiz = QtWidgets.QHBoxLayout() - self.grid.addLayout(self.horiz, 3, 1) + self.grid.addLayout(self.horiz, 4, 1) self.rdoImport = QtWidgets.QRadioButton('Import Fields') self.rdoImport.setChecked(True) From 8b5ccc6c0cc5cdaaeace7f732ec8f2b20f6e811d Mon Sep 17 00:00:00 2001 From: Kelly W Date: Fri, 17 Jan 2025 16:03:58 -0800 Subject: [PATCH 4/9] fix bugs with aoi form (unique name and open properties) --- src/view/frm_dockwidget.py | 2 +- src/view/frm_mask_aoi.py | 33 +++++++++++++++++++++------------ 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/view/frm_dockwidget.py b/src/view/frm_dockwidget.py index 244174b6..896f7842 100644 --- a/src/view/frm_dockwidget.py +++ b/src/view/frm_dockwidget.py @@ -1559,7 +1559,7 @@ def edit_item(self, model_item: QtGui.QStandardItem, db_item: DBItem): frm = FrmEvent(self, self.project, event=db_item, event_type_id=db_item.event_type.id) elif isinstance(db_item, SampleFrame): if db_item.sample_frame_type == SampleFrame.AOI_SAMPLE_FRAME_TYPE: - frm = FrmAOI(self, self.project, None, db_item.sample_frame_type, db_item) + frm = FrmAOI(self, self.project, None, db_item) elif db_item.sample_frame_type == SampleFrame.VALLEY_BOTTOM_SAMPLE_FRAME_TYPE: frm = FrmValleyBottom(self, self.project, None, db_item) else: diff --git a/src/view/frm_mask_aoi.py b/src/view/frm_mask_aoi.py index d664318a..20a5165b 100644 --- a/src/view/frm_mask_aoi.py +++ b/src/view/frm_mask_aoi.py @@ -26,7 +26,6 @@ def __init__(self, parent, project: Project, import_source_path: str, aoi: Sampl self.aoi = aoi self.import_source_path = import_source_path self.attribute_filter = None - self.str_mask_type = "AOI" super(FrmAOI, self).__init__(parent) metadata_json = json.dumps(aoi.metadata) if aoi is not None else None @@ -41,7 +40,7 @@ def __init__(self, parent, project: Project, import_source_path: str, aoi: Sampl self.setWindowTitle(f'Create New AOI') # The attribute picker is only visible when creating a new regular mask - show_attribute_filter = True + show_attribute_filter = False self.lblAttribute.setVisible(show_attribute_filter) self.cboAttribute.setVisible(show_attribute_filter) @@ -62,6 +61,7 @@ def __init__(self, parent, project: Project, import_source_path: str, aoi: Sampl else: # find if import_source_path is shapefile, geopackage, or other self.basepath, self.layer_name, self.layer_id = layer_path_parser(import_source_path) + show_attribute_filter = True self.txtName.setText(self.layer_name) self.txtName.selectAll() @@ -123,6 +123,15 @@ def accept(self): if not validate_name(self, self.txtName): return + + # Check if the name is unique + if self.qris_project.aois is not None: + current_id = self.aoi.id if self.aoi is not None else None + for aoi_id, aoi in self.qris_project.aois.items(): + if aoi.name == self.txtName.text() and aoi.id != current_id: + QtWidgets.QMessageBox.warning(self, 'Duplicate Name', f"An AOI with the name '{self.txtName.text()}' already exists. Please choose a unique name.") + self.txtName.setFocus() + return metadata_json = self.metadata_widget.get_json() metadata = json.loads(metadata_json) if metadata_json is not None else None @@ -135,10 +144,10 @@ def accept(self): self.qris_project.aois[self.aoi.id] = self.aoi except Exception as ex: if 'unique' in str(ex).lower(): - QtWidgets.QMessageBox.warning(self, 'Duplicate Name', f"A {self.str_mask_type} with the name '{self.txtName.text()}' already exists. Please choose a unique name.") + QtWidgets.QMessageBox.warning(self, 'Duplicate Name', f"An AOI with the name '{self.txtName.text()}' already exists. Please choose a unique name.") self.txtName.setFocus() else: - QtWidgets.QMessageBox.warning(self, f'Error Saving {self.str_mask_type}', str(ex)) + QtWidgets.QMessageBox.warning(self, f'Error Saving AOI', str(ex)) return if self.import_source_path is not None: @@ -166,11 +175,11 @@ def accept(self): except Exception as ex: try: self.aoi.delete(self.qris_project.project_file) - QgsApplication.messageLog().logMessage(f'Error Importing {self.str_mask_type}: {str(ex)}', 'QRIS', level=Qgis.Critical) - iface.messageBar().pushMessage(f'Error Importing {self.str_mask_type}', str(ex), level=Qgis.Critical, duration=5) + QgsApplication.messageLog().logMessage(f'Error Importing AOI: {str(ex)}', 'QRIS', level=Qgis.Critical) + iface.messageBar().pushMessage(f'Error Importing AOI', str(ex), level=Qgis.Critical, duration=5) except Exception as ex_delete: - QgsApplication.messageLog().logMessage(f'Error Deleting {self.str_mask_type}: {str(ex_delete)}', 'QRIS', level=Qgis.Critical) - iface.messageBar().pushMessage(f'Error Deleting {self.str_mask_type}', str(ex_delete), level=Qgis.Critical, duration=5) + QgsApplication.messageLog().logMessage(f'Error Deleting AOI: {str(ex_delete)}', 'QRIS', level=Qgis.Critical) + iface.messageBar().pushMessage(f'Error Deleting AOI', str(ex_delete), level=Qgis.Critical, duration=5) return else: super(FrmAOI, self).accept() @@ -178,14 +187,14 @@ def accept(self): def on_import_complete(self, result: bool): if result is True: - iface.messageBar().pushMessage(f'{self.str_mask_type} Imported', f'{self.str_mask_type} "{self.txtName.text()}" has been imported successfully.', level=Qgis.Success, duration=5) + iface.messageBar().pushMessage(f'AOI Imported', f'AOI "{self.txtName.text()}" has been imported successfully.', level=Qgis.Success, duration=5) else: - QgsApplication.messageLog().logMessage(f'Error Importing {self.str_mask_type} Features', 'QRIS', level=Qgis.Critical) + QgsApplication.messageLog().logMessage(f'Error Importing AOI Features', 'QRIS', level=Qgis.Critical) try: self.aoi.delete(self.qris_project.project_file) except Exception as ex: - QgsApplication.messageLog().logMessage(f'Error Deleting {self.str_mask_type}: {str(ex)}', 'QRIS', level=Qgis.Critical) - iface.messageBar().pushMessage(f'Error Deleting {self.str_mask_type}', str(ex), level=Qgis.Critical, duration=5) + QgsApplication.messageLog().logMessage(f'Error Deleting AOI: {str(ex)}', 'QRIS', level=Qgis.Critical) + iface.messageBar().pushMessage(f'Error Deleting AOI', str(ex), level=Qgis.Critical, duration=5) return super(FrmAOI, self).accept() From 0ef5d82c71f60d37a3609b1eefc1e492cc18c7c8 Mon Sep 17 00:00:00 2001 From: Kelly W Date: Fri, 17 Jan 2025 16:57:34 -0800 Subject: [PATCH 5/9] fix right click promote context polygon icon --- src/view/frm_dockwidget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/view/frm_dockwidget.py b/src/view/frm_dockwidget.py index 896f7842..de22c99b 100644 --- a/src/view/frm_dockwidget.py +++ b/src/view/frm_dockwidget.py @@ -477,8 +477,8 @@ def open_menu(self, position): # self.add_context_menu_item(self.menu, 'Generate Centerline', 'gis', lambda: self.generate_centerline(model_data)) promote_menu = self.menu.addMenu('Promote to ...') self.add_context_menu_item(promote_menu, 'AOI', 'mask', lambda: self.add_aoi(model_item, SampleFrame.AOI_SAMPLE_FRAME_TYPE, DB_MODE_PROMOTE)) + self.add_context_menu_item(promote_menu, 'Riverscape Valley Bottom', 'valley_bottom', lambda: self.add_valley_bottom(model_item, DB_MODE_PROMOTE)) self.add_context_menu_item(promote_menu, 'Sample Frame', 'mask_regular', lambda: self.add_sample_frame(model_item, DB_MODE_PROMOTE)) - self.add_context_menu_item(promote_menu, 'Riverscape Valley Bottom', 'polygon', lambda: self.add_valley_bottom(model_item, DB_MODE_PROMOTE)) if QgsVectorLayer(f'{model_data.gpkg_path}|layername={model_data.fc_name}').geometryType() == QgsWkbTypes.LineGeometry: promote_menu = self.menu.addMenu('Promote to ...') self.add_context_menu_item(promote_menu, 'Profile', 'gis', lambda: self.add_profile(model_item, DB_MODE_PROMOTE)) From 123b79f47fa6a34d0c69947f38ce13b6280e665c Mon Sep 17 00:00:00 2001 From: Kelly W Date: Wed, 22 Jan 2025 15:55:08 -0800 Subject: [PATCH 6/9] Add AOIs and Valley Bottoms to Climate Engine #561 --- src/view/frm_climate_engine_download.py | 3 ++- src/view/frm_climate_engine_explorer.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/view/frm_climate_engine_download.py b/src/view/frm_climate_engine_download.py index 19eabdb5..1c68af2d 100644 --- a/src/view/frm_climate_engine_download.py +++ b/src/view/frm_climate_engine_download.py @@ -14,6 +14,7 @@ from .utilities import add_help_button from ..model.project import Project +from ..model.sample_frame import SampleFrame from ..lib.climate_engine import get_datasets, get_dataset_variables, get_dataset_date_range, get_dataset_timeseries_polygon, open_climate_engine_website @@ -27,7 +28,7 @@ def __init__(self, parent: QtWidgets.QWidget = None, qris_project: Project = Non self.iface = iface self.layer_geometry = None self.qris_project = qris_project - self.sample_frame_widget = SampleFrameWidget(self, qris_project) + self.sample_frame_widget = SampleFrameWidget(self, qris_project, sample_frame_types=[SampleFrame.AOI_SAMPLE_FRAME_TYPE, SampleFrame.VALLEY_BOTTOM_SAMPLE_FRAME_TYPE, SampleFrame.SAMPLE_FRAME_TYPE]) self.date_range_widget = DateRangeWidget(self, horizontal=False) self.setWindowTitle('Download Climate Engine Metrics') diff --git a/src/view/frm_climate_engine_explorer.py b/src/view/frm_climate_engine_explorer.py index ef82e4ed..194accfe 100644 --- a/src/view/frm_climate_engine_explorer.py +++ b/src/view/frm_climate_engine_explorer.py @@ -20,7 +20,9 @@ from ..model.project import Project from ..model.db_item import dict_factory +from ..model.sample_frame import SampleFrame from ..model.basin_characteristics_table_view import BasinCharsTableModel + from ..lib.climate_engine import get_datasets, open_climate_engine_website from ..QRiS.qris_map_manager import QRisMapManager @@ -34,7 +36,7 @@ def __init__(self, parent: QtWidgets.QWidget, qris_project: Project, qris_map_ma self.qris_map_manager = qris_map_manager self.datasets = get_datasets() - self.sample_frame_widget = SampleFrameWidget(self, self.qris_project, self.qris_map_manager, first_index_empty=True) + self.sample_frame_widget = SampleFrameWidget(self, self.qris_project, self.qris_map_manager, first_index_empty=True, sample_frame_types=[SampleFrame.AOI_SAMPLE_FRAME_TYPE, SampleFrame.VALLEY_BOTTOM_SAMPLE_FRAME_TYPE, SampleFrame.SAMPLE_FRAME_TYPE]) self.sample_frame_widget.cbo_sample_frame.currentIndexChanged.connect(self.load_climate_engine_metrics) self.sample_frame_widget.sample_frame_changed.connect(self.load_climate_engine_metrics) self.sample_frame_widget.sample_frame_changed.connect(self.create_plot) From 898f9007a392396af5a89de5443558ae71cc4a99 Mon Sep 17 00:00:00 2001 From: Kelly W Date: Thu, 23 Jan 2025 10:41:43 -0800 Subject: [PATCH 7/9] Fix Virtual Area Fields and VB,AOI field fixes #564 --- src/QRiS/qris_map_manager.py | 8 ++- src/QRiS/riverscapes_map_manager.py | 103 +++++++++------------------- 2 files changed, 39 insertions(+), 72 deletions(-) diff --git a/src/QRiS/qris_map_manager.py b/src/QRiS/qris_map_manager.py index 9a70a62c..2f0b7755 100644 --- a/src/QRiS/qris_map_manager.py +++ b/src/QRiS/qris_map_manager.py @@ -76,9 +76,12 @@ def build_aoi_layer(self, aoi: SampleFrame) -> QgsMapLayer: # setup fields self.set_hidden(feature_layer, 'fid', 'AOI Feature ID') self.set_hidden(feature_layer, 'sample_frame_id', 'AOI ID') + self.set_alias(feature_layer, 'display_label', 'Display Label') self.set_alias(feature_layer, 'position', 'Position') self.set_multiline(feature_layer, 'description', 'Description') self.set_hidden(feature_layer, 'metadata', 'Metadata') + self.set_hidden(feature_layer, 'flow_path', 'Flow Path', hide_in_attribute_table=True) + self.set_hidden(feature_layer, 'flows_into', 'Flows Into', hide_in_attribute_table=True) self.set_virtual_dimension(feature_layer, 'area') self.set_metadata_virtual_fields(feature_layer) @@ -172,8 +175,11 @@ def build_valley_bottom_layer(self, valley_bottom: SampleFrame) -> QgsMapLayer: feature_layer = self.create_db_item_feature_layer(self.project.map_guid, group_layer, fc_path, valley_bottom, 'sample_frame_id', 'valley_bottom') # setup fields - self.set_hidden(feature_layer, 'fid', 'Valley Bottom Feature ID') + self.set_hidden(feature_layer, 'fid', 'VB Feature ID') self.set_hidden(feature_layer, 'sample_frame_id', 'Valley Bottom ID') + self.set_alias(feature_layer, 'display_label', 'Display Label') + self.set_hidden(feature_layer, 'flow_path', 'Flow Path', hide_in_attribute_table=True) + self.set_hidden(feature_layer, 'flows_into', 'Flows Into', hide_in_attribute_table=True) self.set_multiline(feature_layer, 'description', 'Description') self.set_hidden(feature_layer, 'metadata', 'Metadata') self.set_virtual_dimension(feature_layer, 'area') diff --git a/src/QRiS/riverscapes_map_manager.py b/src/QRiS/riverscapes_map_manager.py index 4632a4a4..e1628241 100644 --- a/src/QRiS/riverscapes_map_manager.py +++ b/src/QRiS/riverscapes_map_manager.py @@ -518,71 +518,6 @@ def set_metadata_virtual_fields(self, feature_layer: QgsVectorLayer, field_confi widget = QgsEditorWidgetSetup('Hidden', {}) feature_layer.setEditorWidgetSetup(feature_layer.fields().indexFromName(key), widget) - # if field_type == QVariant.Url: - # set the widget to open the url - - # add an action edit the value - # edit_action_text = dedent(""" - # from qgis.PyQt.QtWidgets import QLineEdit, QDialog, QDialogButtonBox, QVBoxLayout - # from qgis.PyQt.QtCore import Qt - # from qgis.PyQt.QtGui import QIntValidator, QDoubleValidator - # from qgis.core import QgsExpressionContextUtils - - # class EditMetadata(QDialog): - # def __init__(self, parent=None): - - # QDialog.__init__(self, parent) - # self.setWindowTitle('Edit Metadata') - # self.setWindowFlags(Qt.WindowStaysOnTopHint) - # self.setLayout(QVBoxLayout()) - # self.layout().setContentsMargins(0, 0, 0, 0) - # self.layout().setSpacing(0) - # self.layout().setAlignment(Qt.AlignTop) - # self.layout().setAlignment(Qt.AlignLeft) - - # self.metadata = QgsExpressionContextUtils.layerScope(iface.activeLayer()).variable('metadata') - # self. - - # self.metadata_edit = QLineEdit() - # self.metadata_edit.setText(self.metadata) - # self.layout().addWidget(self.metadata_edit) - - # self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) - # self.button_box.accepted.connect(self.accept) - # self.button_box.rejected.connect(self.reject) - # self.layout().addWidget(self.button_box) - - # def accept(self): - # self.metadata = self.metadata_edit.text() - # QgsExpressionContextUtils.setLayerVariable(iface.activeLayer(), 'metadata', self.metadata) - # self.super().accept() - - # def reject(self): - # self.super().reject() - - # dialog = EditMetadata() - # dialog.exec_() - # """).strip("\n") - - # editAction = QgsAction(1, 'Edit Metadata', edit_action_text, None, capture=False, shortTitle='Edit Metadata', actionScopes={'Feature'}) - # feature_layer.actions().addAction(editAction) - # editorAction = QgsAttributeEditorAction(editAction) - - # feature_layer.setEditorWidgetSetup(feature_layer.fields().indexFromName(key), QgsEditorWidgetSetup('TextEdit', {'IsMultiline': True, 'UseHtml': False, 'Action': editorAction})) - - # if field_type == QVariant.Url: - # # add an action to open the url - # url_action_text = dedent(""" - # import webbrowser - # url = "[% @url %]" - # webbrowser.open(url, new=2) - # """).strip("\n") - - # urlAction = QgsAction(1, 'Open URL', url_action_text, None, capture=False, shortTitle='Open URL', actionScopes={'Feature'}) - # feature_layer.actions().addAction(urlAction) - # editorAction = QgsAttributeEditorAction(urlAction) - # feature_layer.setEditorWidgetSetup(feature_layer.fields().indexFromName(key), QgsEditorWidgetSetup('OpenUrl', {'Action': editorAction})) - # set the default value for the metadata field feature_layer.setDefaultValueDefinition(field_index, QgsDefaultValue('\'{}\'')) @@ -596,7 +531,7 @@ def set_multiline(self, feature_layer: QgsVectorLayer, field_name: str, field_al form_config.setLabelOnTop(field_index, True) feature_layer.setEditFormConfig(form_config) - def set_hidden(self, feature_layer: QgsVectorLayer, field_name: str, field_alias: str) -> None: + def set_hidden(self, feature_layer: QgsVectorLayer, field_name: str, field_alias: str, hide_in_attribute_table=False) -> None: """Sets a field to hidden, read only, and also sets an alias just in case. Often used on fid, project_id, and event_id""" fields = feature_layer.fields() field_index = fields.indexFromName(field_name) @@ -607,6 +542,16 @@ def set_hidden(self, feature_layer: QgsVectorLayer, field_name: str, field_alias widget_setup = QgsEditorWidgetSetup('Hidden', {}) feature_layer.setEditorWidgetSetup(field_index, widget_setup) + if hide_in_attribute_table: + config = feature_layer.attributeTableConfig() + columns = config.columns() + for column in columns: + if column.name == field_name: + column.hidden = True + break + config.setColumns(columns) + feature_layer.setAttributeTableConfig(config) + def set_alias(self, feature_layer: QgsVectorLayer, field_name: str, field_alias: str, parent_container=None, display_index=None) -> None: """Just provides an alias to the field for display""" fields = feature_layer.fields() @@ -689,15 +634,31 @@ def set_virtual_dimension(self, feature_layer: QgsVectorLayer, dimension: str) - sets the widget type to text sets default value to the dimension expression""" - field_name = 'vrt_' + dimension - field_alias = dimension.capitalize() + ' (m)' - field_expression = 'round(${}, 0)'.format(dimension) - virtual_field = QgsField(field_name, QVariant.Int) + if dimension == 'area': + field_name = 'vrt_area' + field_alias = 'Area (m²)' + if feature_layer.crs().isGeographic(): + # Use transform function to reproject geometry to EPSG:5070 for area calculation + field_expression = 'round(area(transform($geometry, \'EPSG:4326\', \'EPSG:5070\')), 0)' + else: + field_expression = 'round($area, 0)' + elif dimension == 'length': + field_name = 'vrt_length' + field_alias = 'Length (m)' + if feature_layer.crs().isGeographic(): + # Use transform function to reproject geometry to EPSG:5070 for length calculation + field_expression = 'round(length(transform($geometry, \'EPSG:4326\', \'EPSG:5070\')), 0)' + else: + field_expression = 'round($length, 0)' + else: + raise ValueError("Dimension must be 'area' or 'length'") + + virtual_field = QgsField(field_name, QVariant.Double) feature_layer.addExpressionField(field_expression, virtual_field) fields = feature_layer.fields() field_index = fields.indexFromName(field_name) feature_layer.setFieldAlias(field_index, field_alias) - feature_layer.setDefaultValueDefinition(field_index, QgsDefaultValue(field_expression)) + feature_layer.setDefaultValueDefinition(field_index, QgsDefaultValue(field_expression, True)) widget_setup = QgsEditorWidgetSetup('TextEdit', {}) feature_layer.setEditorWidgetSetup(field_index, widget_setup) From 7d4907cbf0c6f93ee0218700849fb7a8559871c5 Mon Sep 17 00:00:00 2001 From: Kelly W Date: Fri, 24 Jan 2025 15:22:10 -0800 Subject: [PATCH 8/9] Mapping multiple fields in an existing feature class to DCE Layer causes error #552 --- src/view/frm_field_value_map.py | 147 +++++++++++++++++++------------ src/view/frm_import_dce_layer.py | 4 +- 2 files changed, 95 insertions(+), 56 deletions(-) diff --git a/src/view/frm_field_value_map.py b/src/view/frm_field_value_map.py index 82b93996..78af9510 100644 --- a/src/view/frm_field_value_map.py +++ b/src/view/frm_field_value_map.py @@ -6,64 +6,99 @@ class FrmFieldValueMap(QtWidgets.QDialog): # signal to send field value map to parent - field_value_map = QtCore.pyqtSignal(str, dict) + field_value_map_signal = QtCore.pyqtSignal(str, dict) - def __init__(self, parent, field: str, values: list, fields: dict): + def __init__(self, input_field: str, values: list, fields: dict, parent=None): + super().__init__(parent) + self.fields = fields # Dictionary to store field values + self.values = values # List to store input values + self.input_field = input_field # Name of the input field - self.field = field - self.values = values - self.fields = fields - - super(FrmFieldValueMap, self).__init__(parent) self.setupUi() self.setWindowTitle('Field Value Map') - self.txtField.setText(self.field) - - self.load_fields() + self.txtInputField.setText(input_field) - def load_fields(self): + # Populate the combo box with field names + self.cmbOutputField.addItems(self.fields.keys()) - # add rows for each value + # Populate the table with input values self.tblFields.setRowCount(len(self.values)) - - # add values to first column for i, value in enumerate(self.values): - self.tblFields.setItem(i, 0, QtWidgets.QTableWidgetItem(str(value))) - # add drop down to each column with the values in self.fields - for j, field in enumerate(self.fields.keys()): - combo = QtWidgets.QComboBox() - combo.addItem('- NULL -', None) - for value in self.fields[field]: - # add the value and display name to the combo box - combo.addItem(str(value), value) - - self.tblFields.setCellWidget(i, j + 1, combo) - combo.setCurrentIndex(0) + item = QtWidgets.QTableWidgetItem(str(value)) + self.tblFields.setItem(i, 0, item) + + def add_output_field(self, field) -> None: + + if not field: + field = self.cmbOutputField.currentText() + if not field: + return + + # Add a new column to the table + self.tblFields.setColumnCount(self.tblFields.columnCount() + 1) + self.tblFields.setHorizontalHeaderItem(self.tblFields.columnCount() - 1, QtWidgets.QTableWidgetItem(field)) + + # Add combo boxes to each row in the new column + for i in range(self.tblFields.rowCount()): + combo = QtWidgets.QComboBox() + combo.addItem('- NULL -', None) + for value in self.fields.get(field, []): + combo.addItem(str(value), value) + self.tblFields.setCellWidget(i, self.tblFields.columnCount() - 1, combo) + combo.setCurrentIndex(0) + + self.cbo_changed(0) + + def remove_output_field(self) -> None: + field = self.cmbOutputField.currentText() + headers = [self.tblFields.horizontalHeaderItem(i).text() for i in range(self.tblFields.columnCount())] + if field in headers: + idx = headers.index(field) + self.tblFields.removeColumn(idx) + + self.cbo_changed(0) + + def cbo_changed(self, idx: int) -> None: + field = self.cmbOutputField.currentText() + headers = [self.tblFields.horizontalHeaderItem(i).text() for i in range(self.tblFields.columnCount())] + if field in headers[1:]: + self.btnAddOutputField.setDisabled(True) + self.btnRemoveOutputField.setDisabled(False) + else: + self.btnAddOutputField.setDisabled(False) + self.btnRemoveOutputField.setDisabled(True) def get_field_value_map(self) -> dict: - field_value_map = {} for i, value in enumerate(self.values): field_value_map[value] = {} - for j, field in enumerate(self.fields.keys()): - combo = self.tblFields.cellWidget(i, j + 1) + for j in range(1, self.tblFields.columnCount()): + field = self.tblFields.horizontalHeaderItem(j).text() + combo: QtWidgets.QComboBox = self.tblFields.cellWidget(i, j) field_value_map[value][field] = combo.currentData() - return field_value_map def load_field_value_map(self, field_value_map: dict) -> None: + # Add columns based on the field_value_map keys + for field in next(iter(field_value_map.values())).keys(): + if field not in [self.tblFields.horizontalHeaderItem(i).text() for i in range(self.tblFields.columnCount())]: + self.add_output_field(field) + # Populate the combo boxes with the values from field_value_map for i, value in enumerate(self.values): - for j, field in enumerate(self.fields.keys()): - combo: QtWidgets.QComboBox = self.tblFields.cellWidget(i, j + 1) - combo.setCurrentIndex(combo.findData(field_value_map[value][field])) + if value in field_value_map: + for j in range(1, self.tblFields.columnCount()): + field = self.tblFields.horizontalHeaderItem(j).text() + if field in field_value_map[value]: + combo: QtWidgets.QComboBox = self.tblFields.cellWidget(i, j) + combo.setCurrentIndex(combo.findData(field_value_map[value][field])) def accept(self) -> None: out_map = self.get_field_value_map() # retain = self.chkRetain.isChecked() - self.field_value_map.emit(self.field, out_map) + self.field_value_map_signal.emit(self.input_field, out_map) return super().accept() @@ -72,39 +107,43 @@ def setupUi(self): # set size self.resize(800, 600) - # vertical layout self.vLayout = QtWidgets.QVBoxLayout(self) + self.grid = QtWidgets.QGridLayout() + self.vLayout.addLayout(self.grid) + + self.lblInputField = QtWidgets.QLabel('Input Field') + self.grid.addWidget(self.lblInputField, 0, 0) - # horizontal layout for field name - self.hLayout = QtWidgets.QHBoxLayout() - self.vLayout.addLayout(self.hLayout) + self.txtInputField = QtWidgets.QLineEdit() + self.txtInputField.setReadOnly(True) + self.grid.addWidget(self.txtInputField, 0, 1) - # label for field name - self.lblField = QtWidgets.QLabel('Input Field') - self.hLayout.addWidget(self.lblField) + self.lblOutputField = QtWidgets.QLabel('Output Field') + self.grid.addWidget(self.lblOutputField, 1, 0) - # text box for field name - self.txtField = QtWidgets.QLineEdit() - self.txtField.setToolTip('The field from the input file to map values for') - self.txtField.setReadOnly(True) - self.hLayout.addWidget(self.txtField) + self.cmbOutputField = QtWidgets.QComboBox() + self.grid.addWidget(self.cmbOutputField, 1, 1) - # # Retain original values as Metadata checkox - # self.chkRetain = QtWidgets.QCheckBox('Retain original values as Metadata') - # self.chkRetain.setChecked(True) - # self.vLayout.addWidget(self.chkRetain) + self.btnAddOutputField = QtWidgets.QPushButton('Add') + self.btnAddOutputField.setToolTip('Add a new output field') + self.grid.addWidget(self.btnAddOutputField, 1, 2) + + self.btnRemoveOutputField = QtWidgets.QPushButton('Remove') + self.btnRemoveOutputField.setToolTip('Remove the selected output field') + self.grid.addWidget(self.btnRemoveOutputField, 1, 3) - # new table with 1 + number of fields columns self.tblFields = QtWidgets.QTableWidget() - self.tblFields.setColumnCount(1 + len(self.fields)) - self.tblFields.setHorizontalHeaderLabels(['Value'] + list(self.fields.keys())) + self.tblFields.setColumnCount(1) + self.tblFields.setHorizontalHeaderLabels(['Input Values']) self.tblFields.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.Stretch) self.tblFields.verticalHeader().setVisible(False) self.tblFields.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) self.tblFields.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) - # add table to layout self.vLayout.addWidget(self.tblFields) - # add standard form buttons self.vLayout.addLayout(add_standard_form_buttons(self, 'field_value_map')) + + self.btnAddOutputField.clicked.connect(self.add_output_field) + self.btnRemoveOutputField.clicked.connect(self.remove_output_field) + self.cmbOutputField.currentIndexChanged.connect(self.cbo_changed) \ No newline at end of file diff --git a/src/view/frm_import_dce_layer.py b/src/view/frm_import_dce_layer.py index 7ef91613..c7270a78 100644 --- a/src/view/frm_import_dce_layer.py +++ b/src/view/frm_import_dce_layer.py @@ -147,11 +147,11 @@ def open_value_map_dialog(self, row: int): fields[target_field_name] = self.qris_project.lookup_values[target_field['lookup']] # open the value map dialog - frm = FrmFieldValueMap(self, input_field, values, fields) + frm = FrmFieldValueMap(input_field, values, fields) if input_field in [field.src_field for field in self.field_maps]: in_field = next((field for field in self.field_maps if field.src_field == input_field), None) frm.load_field_value_map(in_field.map) - frm.field_value_map.connect(self.on_field_value_map) + frm.field_value_map_signal.connect(self.on_field_value_map) frm.exec_() def on_field_value_map(self, field_name: str, field_value_map: dict): From d1c567628dcaf6acbdde4314c7b32bab082e3198 Mon Sep 17 00:00:00 2001 From: Kelly W Date: Fri, 24 Jan 2025 15:37:00 -0800 Subject: [PATCH 9/9] version 1.0.9 --- CHANGELIST.md | 17 +++++++++++++++++ __version__.py | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELIST.md b/CHANGELIST.md index 7b755750..4f93ecec 100644 --- a/CHANGELIST.md +++ b/CHANGELIST.md @@ -1,5 +1,22 @@ # QRiS Plugin +## [1.0.9] 2025 JAN 24 + +### Added +- Allow clip to AOI when importing existing feature class into DCE layer #562 + +### Fixed +- Bugs with AOI properties form (unique name constraints) +- Promote to Valley Bottom Icon +- Fix Virtual Area Fields and VB,AOI field fixes #564 +- Mapping multiple fields in an existing feature class to DCE Layer causes error #552 + +### Changed +- Analysis allow for metrics to be calculated for AOI's and Valley Bottoms #561 +- Add AOI's and Valley Bottoms to Climate Engine #561 +- Import Field Mapping Form improvements #552 + + ## [1.0.8] 2025 JAN 14 ### Added diff --git a/__version__.py b/__version__.py index a7417924..de3670b9 100644 --- a/__version__.py +++ b/__version__.py @@ -1 +1 @@ -__version__ = "1.0.8" \ No newline at end of file +__version__ = "1.0.9" \ No newline at end of file