Skip to content

Commit 8a3dbeb

Browse files
Copilotlachlangrose
andcommitted
refactor: Convert widgets to dialogs and call map2loop classes directly
- Removed map2loop_tools_tab.py - tools no longer embedded in modelling widget - Reverted modelling_widget.py to remove Map2Loop Tools tab - Created dialogs.py with QDialog wrappers for all 5 tools - Updated sampler_widget.py to call SamplerDecimator/SamplerSpacing directly - Added menu items under Plugins->LoopStructural for each tool: - Sampler - Automatic Stratigraphic Sorter - User-Defined Stratigraphic Column - Extract Basal Contacts - Thickness Calculator - Updated plugin_main.py with dialog show methods and menu cleanup Co-authored-by: lachlangrose <7371904+lachlangrose@users.noreply.github.com>
1 parent bd81685 commit 8a3dbeb

6 files changed

Lines changed: 378 additions & 82 deletions

File tree

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
1-
"""Map2Loop processing tools widgets.
1+
"""Map2Loop processing tools dialogs.
22
3-
This module contains GUI widgets for map2loop processing tools that can be
4-
incorporated into the main dock widget.
3+
This module contains GUI dialogs for map2loop processing tools that can be
4+
accessed from the plugin menu.
55
"""
66

7-
from .basal_contacts_widget import BasalContactsWidget
8-
from .sampler_widget import SamplerWidget
9-
from .sorter_widget import SorterWidget
10-
from .thickness_calculator_widget import ThicknessCalculatorWidget
11-
from .user_defined_sorter_widget import UserDefinedSorterWidget
7+
from .dialogs import (
8+
BasalContactsDialog,
9+
SamplerDialog,
10+
SorterDialog,
11+
ThicknessCalculatorDialog,
12+
UserDefinedSorterDialog,
13+
)
1214

1315
__all__ = [
14-
'BasalContactsWidget',
15-
'SamplerWidget',
16-
'SorterWidget',
17-
'ThicknessCalculatorWidget',
18-
'UserDefinedSorterWidget',
16+
'BasalContactsDialog',
17+
'SamplerDialog',
18+
'SorterDialog',
19+
'ThicknessCalculatorDialog',
20+
'UserDefinedSorterDialog',
1921
]
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
"""Dialog wrappers for map2loop processing tools.
2+
3+
This module provides QDialog wrappers that use map2loop classes directly
4+
instead of QGIS processing algorithms.
5+
"""
6+
7+
from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout
8+
9+
10+
class SamplerDialog(QDialog):
11+
"""Dialog for running samplers using map2loop classes directly."""
12+
13+
def __init__(self, parent=None):
14+
"""Initialize the sampler dialog."""
15+
super().__init__(parent)
16+
self.setWindowTitle("Map2Loop Sampler")
17+
self.setup_ui()
18+
19+
def setup_ui(self):
20+
"""Set up the dialog UI."""
21+
from .sampler_widget import SamplerWidget
22+
23+
layout = QVBoxLayout(self)
24+
self.widget = SamplerWidget(self)
25+
layout.addWidget(self.widget)
26+
27+
# Replace the run button with dialog buttons
28+
self.widget.runButton.hide()
29+
30+
self.button_box = QDialogButtonBox(
31+
QDialogButtonBox.Ok | QDialogButtonBox.Cancel, self
32+
)
33+
self.button_box.accepted.connect(self._run_and_accept)
34+
self.button_box.rejected.connect(self.reject)
35+
layout.addWidget(self.button_box)
36+
37+
def _run_and_accept(self):
38+
"""Run the sampler and accept dialog if successful."""
39+
self.widget._run_sampler()
40+
# Dialog stays open so user can see the result
41+
42+
43+
class SorterDialog(QDialog):
44+
"""Dialog for running stratigraphic sorter using map2loop classes directly."""
45+
46+
def __init__(self, parent=None):
47+
"""Initialize the sorter dialog."""
48+
super().__init__(parent)
49+
self.setWindowTitle("Map2Loop Automatic Stratigraphic Sorter")
50+
self.setup_ui()
51+
52+
def setup_ui(self):
53+
"""Set up the dialog UI."""
54+
from .sorter_widget import SorterWidget
55+
56+
layout = QVBoxLayout(self)
57+
self.widget = SorterWidget(self)
58+
layout.addWidget(self.widget)
59+
60+
# Replace the run button with dialog buttons
61+
self.widget.runButton.hide()
62+
63+
self.button_box = QDialogButtonBox(
64+
QDialogButtonBox.Ok | QDialogButtonBox.Cancel, self
65+
)
66+
self.button_box.accepted.connect(self._run_and_accept)
67+
self.button_box.rejected.connect(self.reject)
68+
layout.addWidget(self.button_box)
69+
70+
def _run_and_accept(self):
71+
"""Run the sorter and accept dialog if successful."""
72+
self.widget._run_sorter()
73+
74+
75+
class UserDefinedSorterDialog(QDialog):
76+
"""Dialog for user-defined stratigraphic column using map2loop classes directly."""
77+
78+
def __init__(self, parent=None):
79+
"""Initialize the user-defined sorter dialog."""
80+
super().__init__(parent)
81+
self.setWindowTitle("Map2Loop User-Defined Stratigraphic Column")
82+
self.setup_ui()
83+
84+
def setup_ui(self):
85+
"""Set up the dialog UI."""
86+
from .user_defined_sorter_widget import UserDefinedSorterWidget
87+
88+
layout = QVBoxLayout(self)
89+
self.widget = UserDefinedSorterWidget(self)
90+
layout.addWidget(self.widget)
91+
92+
# Replace the run button with dialog buttons
93+
self.widget.runButton.hide()
94+
95+
self.button_box = QDialogButtonBox(
96+
QDialogButtonBox.Ok | QDialogButtonBox.Cancel, self
97+
)
98+
self.button_box.accepted.connect(self._run_and_accept)
99+
self.button_box.rejected.connect(self.reject)
100+
layout.addWidget(self.button_box)
101+
102+
def _run_and_accept(self):
103+
"""Run the sorter and accept dialog if successful."""
104+
self.widget._run_sorter()
105+
106+
107+
class BasalContactsDialog(QDialog):
108+
"""Dialog for extracting basal contacts using map2loop classes directly."""
109+
110+
def __init__(self, parent=None):
111+
"""Initialize the basal contacts dialog."""
112+
super().__init__(parent)
113+
self.setWindowTitle("Map2Loop Basal Contacts Extractor")
114+
self.setup_ui()
115+
116+
def setup_ui(self):
117+
"""Set up the dialog UI."""
118+
from .basal_contacts_widget import BasalContactsWidget
119+
120+
layout = QVBoxLayout(self)
121+
self.widget = BasalContactsWidget(self)
122+
layout.addWidget(self.widget)
123+
124+
# Replace the run button with dialog buttons
125+
self.widget.runButton.hide()
126+
127+
self.button_box = QDialogButtonBox(
128+
QDialogButtonBox.Ok | QDialogButtonBox.Cancel, self
129+
)
130+
self.button_box.accepted.connect(self._run_and_accept)
131+
self.button_box.rejected.connect(self.reject)
132+
layout.addWidget(self.button_box)
133+
134+
def _run_and_accept(self):
135+
"""Run the extractor and accept dialog if successful."""
136+
self.widget._run_extractor()
137+
138+
139+
class ThicknessCalculatorDialog(QDialog):
140+
"""Dialog for calculating thickness using map2loop classes directly."""
141+
142+
def __init__(self, parent=None):
143+
"""Initialize the thickness calculator dialog."""
144+
super().__init__(parent)
145+
self.setWindowTitle("Map2Loop Thickness Calculator")
146+
self.setup_ui()
147+
148+
def setup_ui(self):
149+
"""Set up the dialog UI."""
150+
from .thickness_calculator_widget import ThicknessCalculatorWidget
151+
152+
layout = QVBoxLayout(self)
153+
self.widget = ThicknessCalculatorWidget(self)
154+
layout.addWidget(self.widget)
155+
156+
# Replace the run button with dialog buttons
157+
self.widget.runButton.hide()
158+
159+
self.button_box = QDialogButtonBox(
160+
QDialogButtonBox.Ok | QDialogButtonBox.Cancel, self
161+
)
162+
self.button_box.accepted.connect(self._run_and_accept)
163+
self.button_box.rejected.connect(self.reject)
164+
layout.addWidget(self.button_box)
165+
166+
def _run_and_accept(self):
167+
"""Run the calculator and accept dialog if successful."""
168+
self.widget._run_calculator()

loopstructural/gui/map2loop_tools/sampler_widget.py

Lines changed: 117 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,23 @@ def _on_sampler_type_changed(self):
7575
self.geologyLayerComboBox.setAllowEmptyLayer(True)
7676

7777
def _run_sampler(self):
78-
"""Run the sampler algorithm."""
79-
from qgis import processing
80-
from qgis.core import QgsProcessingFeedback
78+
"""Run the sampler algorithm using map2loop classes directly."""
79+
import pandas as pd
80+
from map2loop.sampler import SamplerDecimator, SamplerSpacing
81+
from osgeo import gdal
82+
from qgis.core import (
83+
QgsCoordinateReferenceSystem,
84+
QgsFeature,
85+
QgsField,
86+
QgsFields,
87+
QgsGeometry,
88+
QgsPointXY,
89+
QgsProject,
90+
QgsVectorLayer,
91+
)
92+
from qgis.PyQt.QtCore import QVariant
93+
94+
from ...main.vectorLayerWrapper import qgsLayerToGeoDataFrame
8195

8296
# Validate inputs
8397
if not self.spatialDataLayerComboBox.currentLayer():
@@ -96,26 +110,109 @@ def _run_sampler(self):
96110
QMessageBox.warning(self, "Missing Input", "DTM layer is required for Decimator.")
97111
return
98112

99-
# Prepare parameters
100-
params = {
101-
'SAMPLER_TYPE': self.samplerTypeComboBox.currentIndex(),
102-
'SPATIAL_DATA': self.spatialDataLayerComboBox.currentLayer(),
103-
'DTM': self.dtmLayerComboBox.currentLayer(),
104-
'GEOLOGY': self.geologyLayerComboBox.currentLayer(),
105-
'DECIMATION': self.decimationSpinBox.value(),
106-
'SPACING': self.spacingSpinBox.value(),
107-
'OUTPUT': 'TEMPORARY_OUTPUT',
108-
}
109-
110-
# Run the algorithm
113+
# Get layers and convert to appropriate formats
111114
try:
112-
feedback = QgsProcessingFeedback()
113-
result = processing.run("plugin_map2loop:sampler", params, feedback=feedback)
115+
spatial_data_layer = self.spatialDataLayerComboBox.currentLayer()
116+
spatial_data_gdf = qgsLayerToGeoDataFrame(spatial_data_layer)
117+
118+
dtm_layer = self.dtmLayerComboBox.currentLayer()
119+
dtm_gdal = gdal.Open(dtm_layer.source()) if dtm_layer and dtm_layer.isValid() else None
120+
121+
geology_layer = self.geologyLayerComboBox.currentLayer()
122+
geology_gdf = (
123+
qgsLayerToGeoDataFrame(geology_layer)
124+
if geology_layer and geology_layer.isValid()
125+
else None
126+
)
127+
128+
# Run the appropriate sampler
129+
if sampler_type == "Decimator":
130+
decimation = self.decimationSpinBox.value()
131+
sampler = SamplerDecimator(
132+
decimation=decimation, dtm_data=dtm_gdal, geology_data=geology_gdf
133+
)
134+
samples = sampler.sample(spatial_data_gdf)
135+
else: # Spacing
136+
spacing = self.spacingSpinBox.value()
137+
sampler = SamplerSpacing(
138+
spacing=spacing, dtm_data=dtm_gdal, geology_data=geology_gdf
139+
)
140+
samples = sampler.sample(spatial_data_gdf)
141+
142+
# Convert result back to QGIS layer and add to project
143+
if samples is not None and not samples.empty:
144+
layer_name = f"Sampled Contacts ({sampler_type})"
145+
146+
fields = QgsFields()
147+
for column_name in samples.columns:
148+
if column_name == 'geometry':
149+
continue
150+
dtype = samples[column_name].dtype
151+
dtype_str = str(dtype)
152+
153+
if dtype_str in ['float16', 'float32', 'float64']:
154+
field_type = QVariant.Double
155+
elif dtype_str in ['int8', 'int16', 'int32', 'int64']:
156+
field_type = QVariant.Int
157+
else:
158+
field_type = QVariant.String
114159

115-
if result:
116-
QMessageBox.information(self, "Success", "Sampling completed successfully!")
160+
fields.append(QgsField(column_name, field_type))
161+
162+
crs = None
163+
if spatial_data_gdf is not None and spatial_data_gdf.crs is not None:
164+
crs = QgsCoordinateReferenceSystem.fromWkt(spatial_data_gdf.crs.to_wkt())
165+
166+
# Create layer
167+
geom_type = "PointZ" if 'Z' in samples.columns else "Point"
168+
layer = QgsVectorLayer(
169+
f"{geom_type}?crs={crs.authid() if crs else 'EPSG:4326'}", layer_name, "memory"
170+
)
171+
provider = layer.dataProvider()
172+
provider.addAttributes(fields)
173+
layer.updateFields()
174+
175+
# Add features
176+
for _index, row in samples.iterrows():
177+
feature = QgsFeature(fields)
178+
179+
# Add geometry
180+
if 'Z' in samples.columns and pd.notna(row.get('Z')):
181+
wkt = f"POINT Z ({row['X']} {row['Y']} {row['Z']})"
182+
feature.setGeometry(QgsGeometry.fromWkt(wkt))
183+
else:
184+
feature.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(row['X'], row['Y'])))
185+
186+
# Add attributes
187+
attributes = []
188+
for column_name in samples.columns:
189+
if column_name == 'geometry':
190+
continue
191+
value = row.get(column_name)
192+
dtype = samples[column_name].dtype
193+
194+
if pd.isna(value):
195+
attributes.append(None)
196+
elif dtype in ['float16', 'float32', 'float64']:
197+
attributes.append(float(value))
198+
elif dtype in ['int8', 'int16', 'int32', 'int64']:
199+
attributes.append(int(value))
200+
else:
201+
attributes.append(str(value))
202+
203+
feature.setAttributes(attributes)
204+
provider.addFeature(feature)
205+
206+
layer.updateExtents()
207+
QgsProject.instance().addMapLayer(layer)
208+
209+
QMessageBox.information(
210+
self,
211+
"Success",
212+
f"Sampling completed! Layer '{layer_name}' added with {len(samples)} features.",
213+
)
117214
else:
118-
QMessageBox.warning(self, "Error", "Failed to complete sampling.")
215+
QMessageBox.warning(self, "Warning", "No samples were generated.")
119216

120217
except Exception as e:
121218
QMessageBox.critical(self, "Error", f"An error occurred: {str(e)}")

0 commit comments

Comments
 (0)