From b2ed8993d648d3c2211d3138f4fbcc05f916da3f Mon Sep 17 00:00:00 2001 From: Tim Sutton Date: Fri, 22 Nov 2024 23:31:12 +0000 Subject: [PATCH] Implementation details for dimension and factor aggregation weighting logic --- geest/core/json_tree_item.py | 8 ++++ .../dialogs/dimension_aggregation_dialog.py | 48 +++++++++++++------ .../gui/dialogs/factor_aggregation_dialog.py | 41 +++++++++------- geest/gui/panels/tree_panel.py | 4 +- 4 files changed, 67 insertions(+), 34 deletions(-) diff --git a/geest/core/json_tree_item.py b/geest/core/json_tree_item.py index 8543d5ab..ffe8dea4 100644 --- a/geest/core/json_tree_item.py +++ b/geest/core/json_tree_item.py @@ -214,6 +214,14 @@ def getStatus(self): if not data.get("required", False): if not float(data.get("dimension_weighting", 0.0)): return "Excluded from analysis" + if self.isDimension(): + # if the sum of the factor weightings is 0, return "Excluded from analysis" + weight_sum = 0 + for child in self.childItems: + weight_sum += float(child.attribute("factor_weighting", 0.0)) + if not weight_sum: + return "Excluded from analysis" + if "Error" in data.get("result", ""): return "Workflow failed" if "Failed" in data.get("result", ""): diff --git a/geest/gui/dialogs/dimension_aggregation_dialog.py b/geest/gui/dialogs/dimension_aggregation_dialog.py index 763e97df..60c5c802 100644 --- a/geest/gui/dialogs/dimension_aggregation_dialog.py +++ b/geest/gui/dialogs/dimension_aggregation_dialog.py @@ -146,7 +146,7 @@ def __init__( item = self.tree_item.getItemByGuid(guid) attributes = item.attributes() factor_id = attributes.get("name") - dimension_weighting = attributes.get("dimension_weighting", 0) + dimension_weighting = float(attributes.get("dimension_weighting", 0.0)) default_dimension_weighting = attributes.get( "default_dimension_weighting", 0 ) @@ -160,14 +160,14 @@ def __init__( weighting_item = QDoubleSpinBox(self) weighting_item.setRange(0.0, 1.0) weighting_item.setDecimals(4) - weighting_item.setValue(float(dimension_weighting)) + weighting_item.setValue(dimension_weighting) weighting_item.setSingleStep(0.01) weighting_item.valueChanged.connect(self.validate_weightings) self.table.setCellWidget(row, 1, weighting_item) self.weightings[guid] = weighting_item # Use checkboxes - checkbox_widget = self.create_checkbox_widget(row) + checkbox_widget = self.create_checkbox_widget(row, dimension_weighting) self.table.setCellWidget(row, 2, checkbox_widget) if factor_required == 1: checkbox_widget.setEnabled(False) @@ -235,12 +235,15 @@ def toggle_guid_column(self): self.guid_column_visible = not self.guid_column_visible self.table.setColumnHidden(4, not self.guid_column_visible) - def create_checkbox_widget(self, row: int) -> QWidget: + def create_checkbox_widget(self, row: int, dimension_weighting: float) -> QWidget: """ Create a QWidget containing a QCheckBox for a specific row and center it. """ checkbox = QCheckBox() - checkbox.setChecked(True) # Initially checked + if dimension_weighting > 0: + checkbox.setChecked(True) # Initially checked + else: + checkbox.setChecked(False) checkbox.stateChanged.connect( lambda state, r=row: self.toggle_row_widgets(r, state) ) @@ -293,7 +296,12 @@ def auto_calculate_weightings(self): log_message("No enabled rows found, skipping auto-calculation") return # No enabled rows, avoid division by zero - equal_weighting = 1.0 / len(enabled_rows) # Divide equally among enabled rows + if len(enabled_rows) == 0: + equal_weighting = 0.0 + else: + equal_weighting = 1.0 / len( + enabled_rows + ) # Divide equally among enabled rows # Set the weighting for each enabled row for row in enabled_rows: @@ -305,6 +313,7 @@ def auto_calculate_weightings(self): log_message(f"Setting zero weighting for row: {row}") widget = self.table.cellWidget(row, 1) # Assuming weight is in column 1 widget.setValue(0) + self.validate_weightings() def is_checkbox_checked(self, row: int) -> bool: """ @@ -333,7 +342,7 @@ def get_checkbox_in_row(self, row: int) -> QCheckBox: return checkbox return None - def assignWeightings(self): + def saveWeightingsToModel(self): """Assign new weightings to the dimensions's factors.""" for factor_guid, spin_box in self.weightings.items(): try: @@ -353,12 +362,23 @@ def update_preview(self): def validate_weightings(self): """Validate weightings to ensure they sum to 1 and are within range.""" - total_weighting = sum( - float(spin_box.value() or 0) for spin_box in self.weightings.values() - ) - valid_sum = ( - abs(total_weighting - 1.0) < 0.001 - ) # Allow slight floating-point tolerance + try: + total_weighting = sum( + float(spin_box.value() or 0) for spin_box in self.weightings.values() + ) + valid_sum = ( + abs(total_weighting - 1.0) < 0.001 + ) # Allow slight floating-point tolerance + except ValueError: + valid_sum = False + + # In the case that all rows are disabled, the sum is valid + enabled_rows = [ + row for row in range(self.table.rowCount()) if self.is_checkbox_checked(row) + ] + enabled_rows_count = len(enabled_rows) + if enabled_rows_count == 0: + valid_sum = True # Update button state and font color for validation for spin_box in self.weightings.values(): @@ -372,7 +392,7 @@ def validate_weightings(self): def accept_changes(self): """Handle the OK button by applying changes and closing the dialog.""" - self.assignWeightings() # Assign weightings when changes are accepted + self.saveWeightingsToModel() # Assign weightings when changes are accepted if self.editing: updated_data = self.dimension_data updated_data["description"] = self.text_edit_left.toPlainText() diff --git a/geest/gui/dialogs/factor_aggregation_dialog.py b/geest/gui/dialogs/factor_aggregation_dialog.py index ade3cb0b..2e295bf4 100644 --- a/geest/gui/dialogs/factor_aggregation_dialog.py +++ b/geest/gui/dialogs/factor_aggregation_dialog.py @@ -196,9 +196,12 @@ def switch_page(self): current_index = self.stacked_widget.currentIndex() self.stacked_widget.setCurrentIndex(1 - current_index) # Toggle between 0 and 1 - def create_checkbox_widget(self, row: int) -> QWidget: + def create_checkbox_widget(self, row: int, weighting_value: float) -> QWidget: checkbox = QCheckBox() - checkbox.setChecked(True) + if weighting_value > 0: + checkbox.setChecked(True) + else: + checkbox.setChecked(False) checkbox.stateChanged.connect( lambda state, r=row: self.toggle_row_widgets(r, state) ) @@ -245,18 +248,18 @@ def populate_table(self): self.table.setItem(row, 1, name_item) # Weighting - weighting_value = attributes.get("factor_weighting", 0) + weighting_value = float(attributes.get("factor_weighting", 0.0)) weighting_item = QDoubleSpinBox() weighting_item.setRange(0.0, 1.0) weighting_item.setDecimals(4) weighting_item.setSingleStep(0.01) - weighting_item.setValue(float(weighting_value)) + weighting_item.setValue(weighting_value) weighting_item.valueChanged.connect(self.validate_weightings) self.table.setCellWidget(row, 2, weighting_item) self.weightings[guid] = weighting_item # Use (Checkbox) - checkbox_widget = self.create_checkbox_widget(row) + checkbox_widget = self.create_checkbox_widget(row, weighting_value) self.table.setCellWidget(row, 3, checkbox_widget) # GUID @@ -292,8 +295,10 @@ def auto_calculate_weightings(self): row for row in range(self.table.rowCount()) if self.is_checkbox_checked(row) ] if not enabled_rows: - return - equal_weighting = 1.0 / len(enabled_rows) + equal_weighting = 0.0 + else: + equal_weighting = 1.0 / len(enabled_rows) + for row in enabled_rows: widget = self.table.cellWidget(row, 2) # Weight column widget.setValue(equal_weighting) @@ -301,6 +306,7 @@ def auto_calculate_weightings(self): if row not in enabled_rows: widget = self.table.cellWidget(row, 2) widget.setValue(0) + self.validate_weightings() def is_checkbox_checked(self, row: int) -> bool: checkbox = self.get_checkbox_in_row(row) @@ -316,7 +322,7 @@ def get_checkbox_in_row(self, row: int) -> QCheckBox: return checkbox return None - def assignWeightings(self): + def saveWeightingsToModel(self): """Assign new weightings to the factor's indicators.""" for indicator_guid, spin_box in self.weightings.items(): try: @@ -332,18 +338,9 @@ def assignWeightings(self): log_message(traceback.format_exc(), tag="Geest", level=Qgis.Warning) - def validate_weightings(self): - total_weighting = sum( - float(spin_box.value()) for spin_box in self.weightings.values() - ) - valid_sum = abs(total_weighting - 1.0) < 0.001 - for spin_box in self.weightings.values(): - spin_box.setStyleSheet("color: black;" if valid_sum else "color: red;") - self.button_box.button(QDialogButtonBox.Ok).setEnabled(valid_sum) - def accept_changes(self): """Handle the OK button by applying changes and closing the dialog.""" - self.assignWeightings() + self.saveWeightingsToModel() if self.editing: updated_data = self.factor_data updated_data["description"] = self.text_edit_left.toPlainText() @@ -367,6 +364,14 @@ def validate_weightings(self): except ValueError: valid_sum = False + # In the case that all rows are disabled, the sum is valid + enabled_rows = [ + row for row in range(self.table.rowCount()) if self.is_checkbox_checked(row) + ] + enabled_rows_count = len(enabled_rows) + if enabled_rows_count == 0: + valid_sum = True + # Update button state and cell highlighting for spin_box in self.weightings.values(): if valid_sum: diff --git a/geest/gui/panels/tree_panel.py b/geest/gui/panels/tree_panel.py index 5b5c880f..2def83ca 100644 --- a/geest/gui/panels/tree_panel.py +++ b/geest/gui/panels/tree_panel.py @@ -816,7 +816,7 @@ def edit_dimension_aggregation(self, dimension_item): dimension_name, dimension_data, dimension_item, editing=editing, parent=self ) if dialog.exec_(): # If OK was clicked - dialog.assignWeightings() + dialog.saveWeightingsToModel() self.save_json_to_working_directory() # Save changes to the JSON if necessary def edit_factor_aggregation(self, factor_item): @@ -830,7 +830,7 @@ def edit_factor_aggregation(self, factor_item): factor_name, factor_data, factor_item, editing=editing, parent=self ) if dialog.exec_(): # If OK was clicked - dialog.assignWeightings() + dialog.saveWeightingsToModel() self.save_json_to_working_directory() # Save changes to the JSON if necessary def show_layer_properties(self, item):