Skip to content

Commit f995ffc

Browse files
committed
Merge branch 'dev-0.1.12' of github.com:Loop3D/plugin_loopstructural into dev-0.1.12
2 parents 4a0ba19 + 1fb1539 commit f995ffc

20 files changed

+2028
-268
lines changed

loopstructural/debug_manager.py

Lines changed: 406 additions & 0 deletions
Large diffs are not rendered by default.

loopstructural/gui/dlg_settings.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@ def __init__(self, parent):
7676
self.btn_reset.setIcon(QIcon(QgsApplication.iconPath("mActionUndo.svg")))
7777
self.btn_reset.pressed.connect(self.reset_settings)
7878

79+
if hasattr(self, "btn_browse_debug_directory"):
80+
self.btn_browse_debug_directory.pressed.connect(self._browse_debug_directory)
81+
if hasattr(self, "btn_open_debug_directory"):
82+
self.btn_open_debug_directory.pressed.connect(self._open_debug_directory)
83+
7984
# load previously saved settings
8085
self.load_settings()
8186

@@ -91,6 +96,9 @@ def apply(self):
9196
settings.interpolator_cpw = self.cpw_spin_box.value()
9297
settings.interpolator_regularisation = self.regularisation_spin_box.value()
9398
settings.version = __version__
99+
debug_dir_text = (self.le_debug_directory.text() if hasattr(self, "le_debug_directory") else "") or ""
100+
self.plg_settings.set_debug_directory(debug_dir_text)
101+
settings.debug_directory = debug_dir_text
94102

95103
# dump new settings into QgsSettings
96104
self.plg_settings.save_from_object(settings)
@@ -114,6 +122,8 @@ def load_settings(self):
114122
self.regularisation_spin_box.setValue(settings.interpolator_regularisation)
115123
self.cpw_spin_box.setValue(settings.interpolator_cpw)
116124
self.npw_spin_box.setValue(settings.interpolator_npw)
125+
if hasattr(self, "le_debug_directory"):
126+
self.le_debug_directory.setText(settings.debug_directory or "")
117127

118128
def reset_settings(self):
119129
"""Reset settings in the UI and persisted settings to plugin defaults."""
@@ -125,6 +135,35 @@ def reset_settings(self):
125135
# update the form
126136
self.load_settings()
127137

138+
def _browse_debug_directory(self):
139+
"""Open a directory selector for debug directory."""
140+
from qgis.PyQt.QtWidgets import QFileDialog
141+
142+
start_dir = (self.le_debug_directory.text() if hasattr(self, "le_debug_directory") else "") or ""
143+
chosen = QFileDialog.getExistingDirectory(self, "Select Debug Files Directory", start_dir)
144+
if chosen and hasattr(self, "le_debug_directory"):
145+
self.le_debug_directory.setText(chosen)
146+
147+
def _open_debug_directory(self):
148+
"""Open configured debug directory in the system file manager."""
149+
logger = getattr(self, "log", PlgLogger().log)
150+
target = (
151+
self.le_debug_directory.text()
152+
if hasattr(self, "le_debug_directory")
153+
else self.plg_settings.get_debug_directory()
154+
) or ""
155+
if target:
156+
target_path = Path(target)
157+
if target_path.exists():
158+
QDesktopServices.openUrl(QUrl.fromLocalFile(str(target_path)))
159+
else:
160+
logger(
161+
message=f"[map2loop] Debug directory does not exist: {target}",
162+
log_level=1,
163+
)
164+
else:
165+
logger(message="[map2loop] No debug directory configured.", log_level=1)
166+
128167

129168
class PlgOptionsFactory(QgsOptionsWidgetFactory):
130169
"""Factory for options widget."""

loopstructural/gui/dlg_settings.ui

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,9 @@
7878
<bool>false</bool>
7979
</property>
8080
<layout class="QGridLayout" name="gridLayout">
81-
<item row="8" column="0" colspan="2">
82-
<widget class="QPushButton" name="btn_reset">
83-
<property name="minimumSize">
81+
<item row="9" column="0" colspan="2">
82+
<widget class="QPushButton" name="btn_reset">
83+
<property name="minimumSize">
8484
<size>
8585
<width>200</width>
8686
<height>25</height>
@@ -100,8 +100,8 @@
100100
</property>
101101
</widget>
102102
</item>
103-
<item row="7" column="1">
104-
<widget class="QLabel" name="lbl_version_saved_value">
103+
<item row="7" column="1">
104+
<widget class="QLabel" name="lbl_version_saved_value">
105105
<property name="minimumSize">
106106
<size>
107107
<width>0</width>
@@ -147,8 +147,36 @@
147147
</property>
148148
</widget>
149149
</item>
150-
<item row="0" column="0">
151-
<widget class="QPushButton" name="btn_help">
150+
<item row="8" column="0">
151+
<widget class="QLabel" name="lbl_debug_directory">
152+
<property name="text">
153+
<string>Debug directory</string>
154+
</property>
155+
</widget>
156+
</item>
157+
<item row="8" column="1">
158+
<layout class="QHBoxLayout" name="horizontalLayout">
159+
<item>
160+
<widget class="QLineEdit" name="le_debug_directory"/>
161+
</item>
162+
<item>
163+
<widget class="QPushButton" name="btn_browse_debug_directory">
164+
<property name="text">
165+
<string>Browse...</string>
166+
</property>
167+
</widget>
168+
</item>
169+
<item>
170+
<widget class="QPushButton" name="btn_open_debug_directory">
171+
<property name="text">
172+
<string>Open Folder</string>
173+
</property>
174+
</widget>
175+
</item>
176+
</layout>
177+
</item>
178+
<item row="0" column="0">
179+
<widget class="QPushButton" name="btn_help">
152180
<property name="minimumSize">
153181
<size>
154182
<width>200</width>

loopstructural/gui/map2loop_tools/basal_contacts_widget.py

Lines changed: 132 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import os
44

55
from PyQt5.QtWidgets import QMessageBox, QWidget
6+
from qgis.core import QgsProject, QgsVectorFileWriter
67
from qgis.PyQt import uic
78

89
from ...main.helpers import ColumnMatcher, get_layer_names
@@ -17,7 +18,7 @@ class BasalContactsWidget(QWidget):
1718
from geology layers.
1819
"""
1920

20-
def __init__(self, parent=None, data_manager=None):
21+
def __init__(self, parent=None, data_manager=None, debug_manager=None):
2122
"""Initialize the basal contacts widget.
2223
2324
Parameters
@@ -29,6 +30,7 @@ def __init__(self, parent=None, data_manager=None):
2930
"""
3031
super().__init__(parent)
3132
self.data_manager = data_manager
33+
self._debug = debug_manager
3234

3335
# Load the UI file
3436
ui_path = os.path.join(os.path.dirname(__file__), "basal_contacts_widget.ui")
@@ -62,6 +64,66 @@ def __init__(self, parent=None, data_manager=None):
6264
# Set up field combo boxes
6365
self._setup_field_combo_boxes()
6466

67+
def set_debug_manager(self, debug_manager):
68+
"""Attach a debug manager instance."""
69+
self._debug = debug_manager
70+
71+
def _export_layer_for_debug(self, layer, name_prefix: str):
72+
if not (self._debug and self._debug.is_debug()):
73+
return None
74+
try:
75+
debug_dir = self._debug.get_effective_debug_dir()
76+
out_path = debug_dir / f"{name_prefix}.gpkg"
77+
options = QgsVectorFileWriter.SaveVectorOptions()
78+
options.driverName = "GPKG"
79+
options.layerName = layer.name()
80+
res = QgsVectorFileWriter.writeAsVectorFormatV3(
81+
layer,
82+
str(out_path),
83+
QgsProject.instance().transformContext(),
84+
options,
85+
)
86+
if res[0] == QgsVectorFileWriter.NoError:
87+
return str(out_path)
88+
except Exception as err:
89+
self._debug.plugin.log(
90+
message=f"[map2loop] Failed to export layer '{name_prefix}': {err}",
91+
log_level=2,
92+
)
93+
return None
94+
95+
def _serialize_layer(self, layer, name_prefix: str):
96+
try:
97+
export_path = self._export_layer_for_debug(layer, name_prefix)
98+
return {
99+
"name": layer.name(),
100+
"id": layer.id(),
101+
"provider": layer.providerType() if hasattr(layer, "providerType") else None,
102+
"source": layer.source() if hasattr(layer, "source") else None,
103+
"export_path": export_path,
104+
}
105+
except Exception:
106+
return str(layer)
107+
108+
def _serialize_params_for_logging(self, params, context_label: str):
109+
serialized = {}
110+
for key, value in params.items():
111+
if hasattr(value, "source") or hasattr(value, "id"):
112+
serialized[key] = self._serialize_layer(value, f"{context_label}_{key}")
113+
else:
114+
serialized[key] = value
115+
return serialized
116+
117+
def _log_params(self, context_label: str):
118+
if getattr(self, "_debug", None):
119+
try:
120+
self._debug.log_params(
121+
context_label=context_label,
122+
params=self._serialize_params_for_logging(self.get_parameters(), context_label),
123+
)
124+
except Exception:
125+
pass
126+
65127
def _guess_layers(self):
66128
"""Attempt to auto-select layers based on common naming conventions."""
67129
if not self.data_manager:
@@ -113,53 +175,39 @@ def _on_geology_layer_changed(self):
113175

114176
def _run_extractor(self):
115177
"""Run the basal contacts extraction algorithm."""
178+
self._log_params("basal_contacts_widget_run")
179+
116180
# Validate inputs
117181
if not self.geologyLayerComboBox.currentLayer():
118182
QMessageBox.warning(self, "Missing Input", "Please select a geology layer.")
119183
return
120184

121-
# Parse ignore units
122-
ignore_units = []
123-
if self.ignoreUnitsLineEdit.text().strip():
124-
ignore_units = [
125-
unit.strip() for unit in self.ignoreUnitsLineEdit.text().split(',') if unit.strip()
126-
]
127-
geology = self.geologyLayerComboBox.currentLayer()
128-
unit_name_field = self.unitNameFieldComboBox.currentField()
129-
faults = self.faultsLayerComboBox.currentLayer()
130-
stratigraphic_order = (
131-
self.data_manager.get_stratigraphic_unit_names() if self.data_manager else []
132-
)
133-
134-
# Check if user wants all contacts or just basal contacts
135-
all_contacts = self.allContactsCheckBox.isChecked()
136-
if all_contacts:
137-
stratigraphic_order = list({g[unit_name_field] for g in geology.getFeatures()})
138-
result = extract_basal_contacts(
139-
geology=geology,
140-
stratigraphic_order=stratigraphic_order,
141-
faults=faults,
142-
ignore_units=ignore_units,
143-
unit_name_field=unit_name_field,
144-
all_contacts=all_contacts,
145-
updater=lambda message: QMessageBox.information(self, "Extraction Progress", message),
146-
)
147-
148-
# Show success message based on what was extracted
149-
if all_contacts and result:
150-
addGeoDataFrameToproject(result['all_contacts'], "All contacts")
151-
contact_type = "all contacts and basal contacts"
152-
else:
153-
addGeoDataFrameToproject(result['basal_contacts'], "Basal contacts")
154-
155-
contact_type = "basal contacts"
156-
157-
if result:
158-
QMessageBox.information(
159-
self,
160-
"Success",
161-
f"Successfully extracted {contact_type}!",
162-
)
185+
try:
186+
result, contact_type = self._extract_contacts()
187+
if result:
188+
QMessageBox.information(
189+
self,
190+
"Success",
191+
f"Successfully extracted {contact_type}!",
192+
)
193+
if self._debug and self._debug.is_debug():
194+
try:
195+
self._debug.save_debug_file(
196+
"basal_contacts_result.txt", str(result).encode("utf-8")
197+
)
198+
except Exception as err:
199+
self._debug.plugin.log(
200+
message=f"[map2loop] Failed to save basal contacts debug output: {err}",
201+
log_level=2,
202+
)
203+
except Exception as err:
204+
if self._debug:
205+
self._debug.plugin.log(
206+
message=f"[map2loop] Basal contacts extraction failed: {err}",
207+
log_level=2,
208+
)
209+
raise err
210+
QMessageBox.critical(self, "Error", f"An error occurred: {err}")
163211

164212
def get_parameters(self):
165213
"""Get current widget parameters.
@@ -199,3 +247,44 @@ def set_parameters(self, params):
199247
self.ignoreUnitsLineEdit.setText(', '.join(params['ignore_units']))
200248
if 'all_contacts' in params:
201249
self.allContactsCheckBox.setChecked(params['all_contacts'])
250+
251+
def _extract_contacts(self):
252+
"""Execute basal contacts extraction."""
253+
# Parse ignore units
254+
ignore_units = []
255+
if self.ignoreUnitsLineEdit.text().strip():
256+
ignore_units = [
257+
unit.strip() for unit in self.ignoreUnitsLineEdit.text().split(',') if unit.strip()
258+
]
259+
geology = self.geologyLayerComboBox.currentLayer()
260+
unit_name_field = self.unitNameFieldComboBox.currentField()
261+
faults = self.faultsLayerComboBox.currentLayer()
262+
stratigraphic_order = (
263+
self.data_manager.get_stratigraphic_unit_names() if self.data_manager else []
264+
)
265+
266+
# Check if user wants all contacts or just basal contacts
267+
all_contacts = self.allContactsCheckBox.isChecked()
268+
if all_contacts:
269+
stratigraphic_order = list({g[unit_name_field] for g in geology.getFeatures()})
270+
self.data_manager.logger(f"Extracting all contacts for units: {stratigraphic_order}")
271+
272+
result = extract_basal_contacts(
273+
geology=geology,
274+
stratigraphic_order=stratigraphic_order,
275+
faults=faults,
276+
ignore_units=ignore_units,
277+
unit_name_field=unit_name_field,
278+
all_contacts=all_contacts,
279+
updater=lambda message: QMessageBox.information(self, "Extraction Progress", message),
280+
debug_manager=self._debug,
281+
)
282+
self.data_manager.logger(f'All contacts extracted: {all_contacts}')
283+
contact_type = "basal contacts"
284+
if result:
285+
if all_contacts and result['all_contacts'].empty is False:
286+
addGeoDataFrameToproject(result['all_contacts'], "All contacts")
287+
contact_type = "all contacts and basal contacts"
288+
elif not all_contacts and result['basal_contacts'].empty is False:
289+
addGeoDataFrameToproject(result['basal_contacts'], "Basal contacts")
290+
return result, contact_type

0 commit comments

Comments
 (0)