Skip to content

Commit 76aef57

Browse files
committed
fix: sync fault topo calculator with the data manager. Use the same layers and update the adjacency graph
1 parent 8858693 commit 76aef57

File tree

3 files changed

+194
-43
lines changed

3 files changed

+194
-43
lines changed

loopstructural/gui/map2loop_tools/fault_topology_widget.py

Lines changed: 169 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,38 +4,14 @@
44

55
import geopandas as gpd
66
from PyQt5.QtWidgets import QDialog, QMessageBox
7-
from qgis.PyQt import uic
87
from qgis.core import QgsMapLayerProxyModel
8+
from qgis.PyQt import uic
99

1010

1111
class FaultTopologyWidget(QDialog):
12-
def _guess_fault_layer_and_field(self):
13-
"""Attempt to auto-select the fault layer and ID field based on common names."""
14-
try:
15-
from ...main.helpers import ColumnMatcher, get_layer_names
16-
except ImportError:
17-
return
18-
# Guess fault layer
19-
fault_layer_names = get_layer_names(self.faultLayerComboBox)
20-
fault_matcher = ColumnMatcher(fault_layer_names)
21-
fault_layer_match = fault_matcher.find_match('FAULTS')
22-
if fault_layer_match and hasattr(self, 'data_manager') and self.data_manager:
23-
fault_layer = self.data_manager.find_layer_by_name(fault_layer_match)
24-
self.faultLayerComboBox.setLayer(fault_layer)
25-
# Guess ID field
26-
layer = self.faultLayerComboBox.currentLayer()
27-
if layer:
28-
fields = [field.name() for field in layer.fields()]
29-
matcher = ColumnMatcher(fields)
30-
for key in ["ID", "NAME", "FNAME", "id", "name", "fname"]:
31-
match = matcher.find_match(key)
32-
if match:
33-
self.faultIdFieldComboBox.setField(match)
34-
break
35-
3612
"""Widget for calculating fault topology from a fault layer."""
3713

38-
def __init__(self, parent=None, data_manager=None):
14+
def __init__(self, parent=None, data_manager=None, debug_manager=None):
3915
super().__init__(parent)
4016
self.data_manager = data_manager
4117
# Load the UI file
@@ -45,8 +21,16 @@ def __init__(self, parent=None, data_manager=None):
4521

4622
self.faultLayerComboBox.setFilters(QgsMapLayerProxyModel.LineLayer)
4723
self.faultLayerComboBox.layerChanged.connect(self._on_fault_layer_changed)
24+
# react to field changes so we can update the modelling widget via the data manager
25+
try:
26+
# QgsFieldComboBox uses fieldChanged signal
27+
self.faultIdFieldComboBox.fieldChanged.connect(self._on_fault_field_changed)
28+
except Exception:
29+
pass
30+
4831
self.runButton.clicked.connect(self._run_topology)
49-
self._guess_fault_layer_and_field()
32+
# After attempting to guess, synchronise with current data manager state (if any)
33+
self._sync_with_data_manager()
5034

5135
def _on_fault_layer_changed(self):
5236
layer = self.faultLayerComboBox.currentLayer()
@@ -58,6 +42,71 @@ def _on_fault_layer_changed(self):
5842
if name in fields:
5943
self.faultIdFieldComboBox.setField(name)
6044
break
45+
# Inform the data manager / modelling widgets about the change and preserve other settings
46+
self._update_data_manager_fault_layer()
47+
48+
def _on_fault_field_changed(self):
49+
# When the selected ID field changes, update the data manager so the modelling widget updates
50+
self._update_data_manager_fault_layer()
51+
52+
def _sync_with_data_manager(self):
53+
"""Set the widget UI to reflect the current fault traces selection in the data manager."""
54+
if not hasattr(self, 'data_manager') or self.data_manager is None:
55+
print("No data manager to sync with")
56+
return
57+
try:
58+
fault_traces = self.data_manager.get_fault_traces()
59+
except Exception:
60+
fault_traces = None
61+
if not fault_traces:
62+
return
63+
layer = fault_traces.get('layer')
64+
print(f"Syncing fault topology widget with layer: {layer}")
65+
if layer is not None:
66+
try:
67+
self.faultLayerComboBox.setLayer(layer)
68+
except Exception:
69+
pass
70+
# set the name field if available
71+
fault_name_field = fault_traces.get('fault_name_field')
72+
if fault_name_field and layer is not None:
73+
try:
74+
self.faultIdFieldComboBox.setLayer(layer)
75+
self.faultIdFieldComboBox.setField(fault_name_field)
76+
except Exception:
77+
pass
78+
79+
def _update_data_manager_fault_layer(self):
80+
"""Update the ModellingDataManager with the layer/field chosen in this widget.
81+
82+
Preserve any other fault settings already present in the data manager (dip, displacement, use_z).
83+
"""
84+
if not hasattr(self, 'data_manager') or self.data_manager is None:
85+
return
86+
# Gather current selections from this widget
87+
layer = self.faultLayerComboBox.currentLayer()
88+
name_field = None
89+
try:
90+
name_field = self.faultIdFieldComboBox.currentField()
91+
except Exception:
92+
name_field = None
93+
# Preserve existing settings from data manager if present
94+
existing = self.data_manager.get_fault_traces() or {}
95+
fault_dip_field = existing.get('fault_dip_field')
96+
fault_displacement_field = existing.get('fault_displacement_field')
97+
use_z_coordinate = existing.get('use_z_coordinate', False)
98+
# Call data manager to set the fault trace layer which will notify the modelling UI
99+
try:
100+
self.data_manager.set_fault_trace_layer(
101+
layer,
102+
fault_name_field=name_field,
103+
fault_dip_field=fault_dip_field,
104+
fault_displacement_field=fault_displacement_field,
105+
use_z_coordinate=use_z_coordinate,
106+
)
107+
except Exception:
108+
# Fail silently to avoid breaking UI if data_manager is not fully initialised
109+
return
61110

62111
def _run_topology(self):
63112
layer = self.faultLayerComboBox.currentLayer()
@@ -84,10 +133,97 @@ def _run_topology(self):
84133
return
85134
topology = Topology(geology_data=None, fault_data=gdf)
86135
df = topology.fault_fault_relationships
87-
# if self.data_manager is not None:
88-
# self.data_manager.
89-
# Show or add to project
90-
# addGeoDataFrameToproject(gdf, "Input Faults")
91-
# addGeoDataFrameToproject(df, "Fault Topology Table")
92-
QMessageBox.information(self, "Success", f"Calculated fault topology for {len(df)} pairs.")
136+
137+
# Update the modelling FaultTopology (so the Fault Adjacency tab refreshes)
138+
if hasattr(self, 'data_manager') and self.data_manager is not None:
139+
try:
140+
from LoopStructural.modelling.core.fault_topology import FaultRelationshipType
141+
142+
ft = self.data_manager._fault_topology
143+
144+
# Remove existing fault-fault relationships (notify observers)
145+
for f1, f2 in list(ft.adjacency.keys()):
146+
try:
147+
ft.update_fault_relationship(f1, f2, FaultRelationshipType.NONE)
148+
except Exception:
149+
pass
150+
151+
# Remove existing stratigraphy relationships
152+
for unit, fault in list(ft.stratigraphy_fault_relationships.keys()):
153+
try:
154+
ft.update_fault_stratigraphy_relationship(unit, fault, False)
155+
except Exception:
156+
pass
157+
158+
# Determine faults from topology output
159+
new_faults = set()
160+
if df is not None and not df.empty:
161+
# Prefer standard column names
162+
if 'Fault1' in df.columns and 'Fault2' in df.columns:
163+
for _, row in df.iterrows():
164+
print(f"Found fault pair: {row['Fault1']} - {row['Fault2']}")
165+
new_faults.add(str(row['Fault1']))
166+
new_faults.add(str(row['Fault2']))
167+
else:
168+
# Fallback: take first two columns
169+
cols = list(df.columns)
170+
if len(cols) >= 2:
171+
for _, row in df.iterrows():
172+
new_faults.add(str(row[cols[0]]))
173+
new_faults.add(str(row[cols[1]]))
174+
175+
# Add new faults
176+
for f in sorted(new_faults):
177+
if f not in ft.faults:
178+
try:
179+
ft.add_fault(f)
180+
except Exception:
181+
pass
182+
183+
# Remove faults not in new set
184+
for existing in list(ft.faults):
185+
if existing not in new_faults:
186+
try:
187+
ft.remove_fault(existing)
188+
except Exception:
189+
pass
190+
191+
# Add relationships from df (mark as FAULTED)
192+
if df is not None and not df.empty:
193+
for _, row in df.iterrows():
194+
try:
195+
if 'Fault1' in row.index and 'Fault2' in row.index:
196+
f1 = str(row['Fault1'])
197+
f2 = str(row['Fault2'])
198+
else:
199+
f1 = str(row.iloc[0])
200+
f2 = str(row.iloc[1])
201+
ft.update_fault_relationship(f1, f2, FaultRelationshipType.FAULTED)
202+
except Exception:
203+
pass
204+
205+
# Update unit-fault relationships if available from topology
206+
try:
207+
uf = topology.unit_fault_relationships
208+
if uf is not None and not uf.empty:
209+
for _, r in uf.iterrows():
210+
try:
211+
unit = r.get('Unit', r.iloc[0])
212+
fault = r.get('Fault', r.iloc[1])
213+
ft.update_fault_stratigraphy_relationship(unit, str(fault), True)
214+
except Exception:
215+
pass
216+
except Exception:
217+
# unit-fault relationships not available
218+
pass
219+
220+
except Exception:
221+
# If anything fails here, still continue to show success of topology run
222+
pass
223+
224+
QMessageBox.information(
225+
self,
226+
"Success",
227+
f"Calculated fault topology for {len(df) if df is not None else 0} pairs.",
228+
)
93229
return True

loopstructural/gui/modelling/fault_adjacency_tab.py

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -61,15 +61,27 @@ def init_ui(self):
6161
self.data_manager._stratigraphic_column.attach(self.update)
6262
self.data_manager._fault_topology.attach(self.update)
6363

64-
def _update(self, event, *args, **kwargs):
65-
if (
66-
args[0] == "fault_relationship_updated"
67-
or args[0] == "stratigraphy_fault_relationship_updated"
68-
):
69-
return
70-
71-
self.update_fault_adjacency_table()
72-
self.update_stratigraphic_units_table()
64+
def _update(self, observable, event, *args, **kwargs):
65+
"""Observer callback invoked by Observable.notify.
66+
67+
Parameters follow the Observable.notify signature: (observable, event, *args, **kwargs)
68+
Always refresh the tables when a topology or stratigraphy event occurs.
69+
"""
70+
# event is a string describing what changed
71+
try:
72+
ev = event
73+
except Exception:
74+
# defensive: if call used different ordering, try to recover
75+
ev = None
76+
77+
# Refresh the UI for any relevant event
78+
if ev is None or ev.startswith('fault') or 'stratigraphy' in (ev or ''):
79+
self.update_fault_adjacency_table()
80+
self.update_stratigraphic_units_table()
81+
else:
82+
# fallback: update anyway to keep UI in sync
83+
self.update_fault_adjacency_table()
84+
self.update_stratigraphic_units_table()
7385

7486
def change_button_color(self, button, row, col):
7587
"""Cycle the button color and update the fault relationship."""

loopstructural/main/data_manager.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
from collections import defaultdict
33

44
import numpy as np
5-
from LoopStructural.datatypes import BoundingBox
65
from qgis.core import QgsPointXY, QgsProject, QgsVectorLayer
76

87
from LoopStructural import FaultTopology, StratigraphicColumn
8+
from LoopStructural.datatypes import BoundingBox
99
from LoopStructural.modelling.core.stratigraphic_column import StratigraphicColumnElementType
1010

1111
from .vectorLayerWrapper import qgsLayerToGeoDataFrame
@@ -299,7 +299,10 @@ def get_unique_faults(self):
299299
"""Get the unique faults from the fault traces."""
300300
if self._fault_traces is None or self._fault_traces['layer'] is None:
301301
return []
302+
if self._fault_traces['fault_name_field'] is None:
303+
return []
302304
unique_faults = set()
305+
303306
for feature in self._fault_traces['layer'].getFeatures():
304307
fault_name = feature[self._fault_traces['fault_name_field']]
305308
unique_faults.add(str(fault_name))

0 commit comments

Comments
 (0)