44
55import geopandas as gpd
66from PyQt5 .QtWidgets import QDialog , QMessageBox
7- from qgis .PyQt import uic
87from qgis .core import QgsMapLayerProxyModel
8+ from qgis .PyQt import uic
99
1010
1111class 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
0 commit comments