Skip to content

Commit

Permalink
Lots more improvements to handling disabling and enabling analysis it…
Browse files Browse the repository at this point in the history
…ems.
  • Loading branch information
timlinux committed Nov 24, 2024
1 parent 3af2e05 commit 21ec4da
Show file tree
Hide file tree
Showing 17 changed files with 457 additions and 238 deletions.
55 changes: 22 additions & 33 deletions geest/core/generate_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,8 @@ def load_spreadsheet(self):
self.dataframe = self.dataframe[
[
"Dimension",
"Dimension Required",
"Default Dimension Analysis Weighting",
"Factor",
"Factor Required",
"Default Factor Dimension Weighting",
"Indicator",
"Default Indicator Factor Weighting",
Expand Down Expand Up @@ -62,8 +60,7 @@ def load_spreadsheet(self):
"Use Nighttime Lights",
"Use Environmental Hazards",
"Use Street Lights",
"Analysis Mode", # New column
"Indicator Required", # New column
"Analysis Mode",
]
]

Expand All @@ -81,25 +78,20 @@ def parse_to_json(self):
"""
Parse the dataframe into the hierarchical JSON structure.
"""
dimension_map = {}
analysis_model = {}

for _, row in self.dataframe.iterrows():
dimension = row["Dimension"]
factor = row["Factor"]

# Prepare dimension data
dimension_id = self.create_id(dimension)
dimension_required = (
row["Dimension Required"]
if not pd.isna(row["Dimension Required"])
else ""
)
default_dimension_analysis_weighting = (
row["Default Dimension Analysis Weighting"]
if not pd.isna(row["Default Dimension Analysis Weighting"])
else ""
)
if dimension_id not in dimension_map:
if dimension_id not in analysis_model:
# Hardcoded descriptions for specific dimensions
description = ""
if dimension_id == "contextual":
Expand All @@ -110,58 +102,60 @@ def parse_to_json(self):
description = "The Place-Characterization Dimension refers to the social, environmental, and infrastructural attributes of geographical locations, such as walkability, safety, and vulnerability to natural hazards. Unlike the Accessibility Dimension, these factors do not involve mobility but focus on the inherent characteristics of a place that influence women’s ability to participate in the workforce."

# If the Dimension doesn't exist yet, create it
if dimension not in dimension_map:
if dimension not in analysis_model:
new_dimension = {
"id": dimension_id,
"name": dimension,
"required": dimension_required,
"default_analysis_weighting": default_dimension_analysis_weighting,
# Initialise the weighting to the default value
"analysis_weighting": default_dimension_analysis_weighting,
"description": description,
"factors": [],
}
self.result["dimensions"].append(new_dimension)
dimension_map[dimension] = new_dimension
analysis_model[dimension] = new_dimension

# Prepare factor data
factor_id = self.create_id(factor)
factor_required = (
row["Factor Required"] if not pd.isna(row["Factor Required"]) else ""
)
default_factor_dimension_weighting = (
row["Default Factor Dimension Weighting"]
if not pd.isna(row["Default Factor Dimension Weighting"])
else ""
)

# If the Factor doesn't exist in the current dimension, add it
factor_map = {f["name"]: f for f in dimension_map[dimension]["factors"]}
factor_map = {f["name"]: f for f in analysis_model[dimension]["factors"]}
if factor not in factor_map:
new_factor = {
"id": factor_id,
"name": factor,
"required": factor_required,
"default_dimension_weighting": default_factor_dimension_weighting,
# Initialise the weighting to the default value
"dimension_weighting": default_factor_dimension_weighting,
"indicators": [],
"description": (
row["Factor Description"]
if not pd.isna(row["Factor Description"])
else ""
),
}
dimension_map[dimension]["factors"].append(new_factor)
analysis_model[dimension]["factors"].append(new_factor)
factor_map[factor] = new_factor

# Add layer data to the current Factor, including new columns
layer_data = {
# Add indicator data to the current Factor, including new columns
default_factor_weighting = (
row["Default Indicator Factor Weighting"]
if not pd.isna(row["Default Indicator Factor Weighting"])
else ""
)
indicator_data = {
# These are all parsed from the spreadsheet
"indicator": row["Indicator"] if not pd.isna(row["Indicator"]) else "",
"id": row["ID"] if not pd.isna(row["ID"]) else "",
"description": "",
"default_indicator_factor_weighting": (
row["Default Indicator Factor Weighting"]
if not pd.isna(row["Default Indicator Factor Weighting"])
else ""
),
"default_factor_weighting": default_factor_weighting,
# Initialise the weighting to the default value
"factor_weighting": default_factor_weighting,
"default_index_score": (
row["Default Index Score"]
if not pd.isna(row["Default Index Score"])
Expand Down Expand Up @@ -291,14 +285,9 @@ def parse_to_json(self):
"analysis_mode": (
row["Analysis Mode"] if not pd.isna(row["Analysis Mode"]) else ""
), # New column
"indicator_required": (
row["Indicator Required"]
if not pd.isna(row["Indicator Required"])
else ""
), # New column
}

factor_map[factor]["indicators"].append(layer_data)
factor_map[factor]["indicators"].append(indicator_data)

def get_json(self):
"""
Expand Down
1 change: 0 additions & 1 deletion geest/core/generate_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ def infer_schema(data):
return {
"type": "object",
"properties": properties,
"required": required_keys,
}
elif isinstance(data, list):
if len(data) > 0:
Expand Down
90 changes: 49 additions & 41 deletions geest/core/json_tree_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,13 +134,10 @@ def disable(self):
data["analysis_mode"] = "Do Not Use"

if self.isDimension():
data["required"] = False
data["analysis_weighting"] = 0.0
if self.isFactor():
data["required"] = False
data["dimension_weighting"] = 0.0
if self.isIndicator():
data["indicator_required"] = False
data["factor_weighting"] = 0.0

def enable(self):
Expand All @@ -150,18 +147,15 @@ def enable(self):
data = self.attributes()
data["analysis_mode"] = ""
if self.isDimension():
data["required"] = True
data["analysis_weighting"] = data["default_analysis_weighting"]
if self.isFactor():
data["required"] = True
data["dimension_weighting"] = data["default_dimension_weighting"]
if self.parent().getStatus() == "Excluded from analysis":
self.parent().attributes()[
"analysis_weighting"
] = self.parent().attribute("default_analysis_weighting")
if self.isIndicator():
data["indicator_required"] = True
data["factor_weighting"] = data["default_indicator_factor_weighting"]
data["factor_weighting"] = data["default_factor_weighting"]
if self.parent().getStatus() == "Excluded from analysis":
self.parent().attributes()[
"dimension_weighting"
Expand Down Expand Up @@ -210,8 +204,7 @@ def getItemTooltip(self):
def getStatusIcon(self):
"""Retrieve the appropriate icon for the item based on its role."""
status = self.getStatus()
if self.isAnalysis():
return None

if status == "Excluded from analysis":
return QIcon(resources_path("resources", "icons", "excluded.svg"))
if status == "Completed successfully":
Expand Down Expand Up @@ -246,7 +239,6 @@ def getStatus(self):
# First check if the item weighting is 0, or its parent factor is zero
# If so, return "Excluded from analysis"
if self.isIndicator():
# Required flag can be overridden by the factor so we dont check it right now
required_by_parent = float(
self.parentItem.attributes().get("dimension_weighting", 0.0)
)
Expand All @@ -258,17 +250,29 @@ def getStatus(self):
# log_message(f"Excluded from analysis: {data.get('id')}")
return "Excluded from analysis"
if self.isFactor():
if not data.get("required", False):
if not float(data.get("dimension_weighting", 0.0)):
return "Excluded from analysis"
if not float(data.get("dimension_weighting", 0.0)):
return "Excluded from analysis"
# If the sum of the indicator weightings is zero, 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 self.isDimension():
# If the analysis weighting is zero, return "Excluded from analysis"
if not float(data.get("analysis_weighting", 0.0)):
return "Excluded from analysis"
# If the sum of the factor weightings is zero, return "Excluded from analysis"
weight_sum = 0
for child in self.childItems:
weight_sum += float(child.attribute("factor_weighting", 0.0))
weight_sum += float(child.attribute("dimension_weighting", 0.0))
if not weight_sum:
return "Excluded from analysis"
if self.isAnalysis():
# If the sum of the dimension weightings is zero, return "Excluded from analysis"
weight_sum = 0
for child in self.childItems:
weight_sum += float(child.attribute("analysis_weighting", 0.0))
if not weight_sum:
return "Excluded from analysis"

Expand All @@ -287,13 +291,17 @@ def getStatus(self):
):
return "Not configured (optional)"
# Item required and not configured
if (data.get("analysis_mode", "") == "") and data.get(
"indicator_required", False
if (
self.isIndicator()
and (data.get("analysis_mode", "") == "")
and data.get("indicator_required", False)
):
return "Required and not configured"
# Item not required but not configured
if (data.get("analysis_mode", "") == "") and not data.get(
"indicator_required", False
if (
self.isIndicator()
and (data.get("analysis_mode", "") == "")
and not data.get("indicator_required", False)
):
return "Not configured (optional)"
if "Not Run" in data.get("result", "") and not data.get("result_file", ""):
Expand Down Expand Up @@ -389,29 +397,6 @@ def getAnalysisDimensionGuids(self):
return guids
# attributes["analysis_mode"] = "dimension_aggregation"

def getAnalysisAttributes(self):
"""Return the dict of dimensions under this analysis."""
attributes = {}
if self.isAnalysis():
attributes["analysis_name"] = self.attribute("analysis_name", "Not Set")
attributes["description"] = self.attribute(
"analysis_description", "Not Set"
)
attributes["working_folder"] = self.attribute("working_folder", "Not Set")
attributes["cell_size_m"] = self.attribute("cell_size_m", 100)

attributes["dimensions"] = [
{
"dimension_no": i,
"dimension_id": child.attribute("id", ""),
"dimension_name": child.data(0),
"dimension_weighting": child.data(2),
"result_file": child.attribute(f"result_file", ""),
}
for i, child in enumerate(self.childItems)
]
return attributes

def getItemByGuid(self, guid):
"""Return the item with the specified guid."""
if self.guid == guid:
Expand Down Expand Up @@ -470,3 +455,26 @@ def updateFactorWeighting(self, factor_guid, new_weighting):
except Exception as e:
# Handle any exceptions and log the error
log_message(f"Error updating weighting: {e}", level=Qgis.Warning)

def updateDimensionWeighting(self, dimension_guid, new_weighting):
"""Update the weighting of a specific dimension by its guid."""
try:
# Search for the factor by name
dimension_item = self.getItemByGuid(dimension_guid)
# If found, update the weighting
if dimension_item:
dimension_item.setData(2, f"{new_weighting:.2f}")
# weighting references the level above (i.e. analysis)
dimension_item.attributes()["analysis_weighting"] = new_weighting

else:
# Log if the factor name is not found
log_message(
f"Factor '{dimension_guid}' not found.",
tag="Geest",
level=Qgis.Warning,
)

except Exception as e:
# Handle any exceptions and log the error
log_message(f"Error updating weighting: {e}", level=Qgis.Warning)
1 change: 0 additions & 1 deletion geest/core/workflow_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ def create_workflow(
return PointPerCellWorkflow(item, cell_size_m, feedback, context)
elif analysis_mode == "use_polyline_per_cell":
return PolylinePerCellWorkflow(item, cell_size_m, feedback, context)
# TODO fix inconsistent abbreviation below for Poly
elif analysis_mode == "use_polygon_per_cell":
return PolygonPerCellWorkflow(item, cell_size_m, feedback, context)
elif analysis_mode == "factor_aggregation":
Expand Down
12 changes: 10 additions & 2 deletions geest/core/workflows/aggregation_workflow_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,10 +189,11 @@ def get_raster_list(self, index) -> list:
item = self.item.getItemByGuid(guid)
status = item.getStatus() == "Completed successfully"
mode = item.attributes().get("analysis_mode", "Do Not Use") == "Do Not Use"
excluded = item.getStatus() == "Excluded from analysis"
id = item.attribute("id").lower()
if not status and not mode:
if not status and not mode and not excluded:
raise ValueError(
f"{id} is not completed successfully and is not set to 'Do Not Use'"
f"{id} is not completed successfully and is not set to 'Do Not Use' or 'Excluded from analysis'"
)

if mode:
Expand All @@ -202,6 +203,13 @@ def get_raster_list(self, index) -> list:
level=Qgis.Info,
)
continue
if excluded:
log_message(
f"Skipping {item.attribute('id')} as it is excluded from analysis",
tag="Geest",
level=Qgis.Info,
)
continue
if not item.attribute("result_file", ""):
log_message(
f"Skipping {id} as it has no result file",
Expand Down
12 changes: 9 additions & 3 deletions geest/core/workflows/analysis_aggregation_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,14 @@ def __init__(
super().__init__(
item, cell_size_m, feedback, context
) # ⭐️ Item is a reference - whatever you change in this item will directly update the tree
self.id = "geest_analysis"
self.aggregation_attributes = self.item.getAnalysisAttributes()
self.layers = self.aggregation_attributes.get(f"dimensions", [])
self.guids = (
self.item.getAnalysisDimensionGuids()
) # get a list of the items to aggregate
self.id = (
self.item.attribute("analysis_name")
.lower()
.replace(" ", "_")
.replace("'", "")
) # should not be needed any more
self.weight_key = "dimension_weighting"
self.workflow_name = "analysis_aggregation"
5 changes: 1 addition & 4 deletions geest/gui/dialogs/analysis_aggregation_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,6 @@ def __init__(self, analysis_item, editing=False, parent=None):
dimension_id = attributes.get("name")
analysis_weighting = float(attributes.get("analysis_weighting", 0.0))
default_analysis_weighting = attributes.get("default_analysis_weighting", 0)
dimension_required = attributes.get("required", 1)

name_item = QTableWidgetItem(dimension_id)
name_item.setFlags(Qt.ItemIsEnabled)
Expand All @@ -148,8 +147,6 @@ def __init__(self, analysis_item, editing=False, parent=None):
# Use checkboxes
checkbox_widget = self.create_checkbox_widget(row, analysis_weighting)
self.table.setCellWidget(row, 2, checkbox_widget)
if dimension_required == 1:
checkbox_widget.setEnabled(False)

# Reset button
reset_button = QPushButton("Reset")
Expand Down Expand Up @@ -226,7 +223,7 @@ def create_checkbox_widget(self, row: int, analysis_weighting: float) -> QWidget
checkbox.stateChanged.connect(
lambda state, r=row: self.toggle_row_widgets(r, state)
)

checkbox.setEnabled(True) # Enable by default
# Create a container widget with a centered layout
container = QWidget()
layout = QHBoxLayout()
Expand Down
Loading

0 comments on commit 21ec4da

Please sign in to comment.