Skip to content

Commit 48f6296

Browse files
committed
feat: derive basal contacts from geology map in thickness calculators
Basal contacts are now extracted automatically from the geology layer and stratigraphic column when no basal-contacts layer is supplied, so the separate "Extract Basal Contacts" step is no longer required before running the thickness calculator (GUI widget and the Processing Toolbox algorithm).
1 parent a02dffb commit 48f6296

4 files changed

Lines changed: 81 additions & 23 deletions

File tree

‎loopstructural/gui/map2loop_tools/thickness_calculator_widget.py‎

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -338,15 +338,30 @@ def _run_calculator(self):
338338
# Validate inputs based on calculator type
339339
calculator_type = self.calculatorTypeComboBox.currentText()
340340

341-
if calculator_type == "InterpolatedStructure":
342-
if not self.geologyLayerComboBox.currentLayer():
343-
QMessageBox.warning(self, "Missing Input", "Please select a geology layer.")
344-
return False
345-
if not self.basalContactsComboBox.currentLayer():
346-
QMessageBox.warning(self, "Missing Input", "Please select a basal contacts layer.")
341+
if not self.geologyLayerComboBox.currentLayer():
342+
QMessageBox.warning(self, "Missing Input", "Please select a geology layer.")
343+
return False
344+
345+
# A basal contacts layer is optional: when omitted, basal contacts are
346+
# extracted automatically from the geology layer using the
347+
# stratigraphic column, so a stratigraphic order is required instead.
348+
if not self.basalContactsComboBox.currentLayer():
349+
stratigraphic_order = (
350+
self.data_manager.get_stratigraphic_unit_names()
351+
if self.data_manager and hasattr(self.data_manager, 'get_stratigraphic_unit_names')
352+
else None
353+
)
354+
if not stratigraphic_order:
355+
QMessageBox.warning(
356+
self,
357+
"Missing Input",
358+
"Please select a basal contacts layer, or define a stratigraphic "
359+
"column so basal contacts can be extracted from the geology layer "
360+
"automatically.",
361+
)
347362
return False
348363

349-
elif calculator_type == "StructuralPoint":
364+
if calculator_type == "StructuralPoint":
350365
if not self.structureLayerComboBox.currentLayer():
351366
QMessageBox.warning(self, "Missing Input", "Please select a structure layer.")
352367
return False

‎loopstructural/gui/map2loop_tools/thickness_calculator_widget.ui‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,16 @@
6363
<item row="4" column="0">
6464
<widget class="QLabel" name="basalContactsLabel">
6565
<property name="text">
66-
<string>Basal Contacts Layer:</string>
66+
<string>Basal Contacts Layer (optional):</string>
6767
</property>
6868
</widget>
6969
</item>
7070
<item row="4" column="1">
71-
<widget class="QgsMapLayerComboBox" name="basalContactsComboBox"/>
71+
<widget class="QgsMapLayerComboBox" name="basalContactsComboBox">
72+
<property name="allowEmptyLayer">
73+
<bool>true</bool>
74+
</property>
75+
</widget>
7276
</item>
7377
<item row="6" column="0">
7478
<widget class="QLabel" name="sampledContactsLabel">

‎loopstructural/main/m2l_api.py‎

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -429,9 +429,9 @@ def sample_contacts(
429429

430430
def calculate_thickness(
431431
geology,
432-
basal_contacts,
433432
sampled_contacts,
434433
structure,
434+
basal_contacts=None,
435435
cross_sections=None,
436436
calculator_type="InterpolatedStructure",
437437
dtm=None,
@@ -451,12 +451,15 @@ def calculate_thickness(
451451
----------
452452
geology : QgsVectorLayer or GeoDataFrame
453453
Geology polygon layer.
454-
basal_contacts : QgsVectorLayer or GeoDataFrame
455-
Basal contacts line layer.
456454
sampled_contacts : QgsVectorLayer or GeoDataFrame
457455
Sampled contacts point layer.
458456
structure : QgsVectorLayer or GeoDataFrame
459457
Structure point layer with orientation data.
458+
basal_contacts : QgsVectorLayer or GeoDataFrame, optional
459+
Basal contacts line layer, by default None. When omitted, basal
460+
contacts are extracted automatically from ``geology`` using
461+
``stratigraphic_order``, so a separate basal-contacts extraction
462+
step is not required.
460463
cross_sections : QgsVectorLayer or GeoDataFrame, optional
461464
Cross-sections line layer, by default None.
462465
calculator_type : str, optional
@@ -474,7 +477,9 @@ def calculate_thickness(
474477
max_line_length : float, optional
475478
Maximum line length for StructuralPoint calculator, by default None.
476479
stratigraphic_order : list, optional
477-
List of unit names in stratigraphic order, by default None.
480+
List of unit names in stratigraphic order, by default None. Required
481+
when ``basal_contacts`` is omitted, so basal contacts can be derived
482+
from the geology layer.
478483
updater : callable, optional
479484
Callback function for progress updates, by default None.
480485
@@ -486,14 +491,35 @@ def calculate_thickness(
486491
if updater:
487492
updater(f"Calculating thickness using {calculator_type}...")
488493

494+
auto_extracted_basal_contacts = basal_contacts is None
495+
if auto_extracted_basal_contacts:
496+
if not stratigraphic_order:
497+
raise ValueError(
498+
"A basal contacts layer or a stratigraphic order is required to calculate "
499+
"thickness: without a basal contacts layer, the stratigraphic order is "
500+
"needed to extract basal contacts from the geology layer."
501+
)
502+
if updater:
503+
updater("No basal contacts layer supplied; extracting basal contacts from geology...")
504+
target_crs = geology.crs() if hasattr(geology, 'crs') else None
505+
basal_contacts_gdf = extract_basal_contacts(
506+
geology=geology,
507+
stratigraphic_order=stratigraphic_order,
508+
unit_name_field=unit_name_field,
509+
updater=updater,
510+
debug_manager=debug_manager,
511+
target_crs=target_crs,
512+
)['basal_contacts']
513+
489514
# Convert layers to GeoDataFrames
490515
geology_gdf = qgsLayerToGeoDataFrame(geology)
491-
basal_contacts_gdf = qgsLayerToGeoDataFrame(basal_contacts)
492-
basal_contacts_gdf = (
493-
basal_contacts_gdf.rename(columns={basal_contacts_unit_name: 'basal_unit'})
494-
if basal_contacts_unit_name
495-
else basal_contacts_gdf
496-
)
516+
if not auto_extracted_basal_contacts:
517+
basal_contacts_gdf = qgsLayerToGeoDataFrame(basal_contacts)
518+
basal_contacts_gdf = (
519+
basal_contacts_gdf.rename(columns={basal_contacts_unit_name: 'basal_unit'})
520+
if basal_contacts_unit_name
521+
else basal_contacts_gdf
522+
)
497523
sampled_contacts_gdf = qgsLayerToGeoDataFrame(sampled_contacts)
498524
structure_gdf = qgsLayerToGeoDataFrame(structure)
499525
cross_sections_gdf = qgsLayerToGeoDataFrame(cross_sections)

‎loopstructural/processing/algorithms/thickness_calculator.py‎

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
)
3434

3535
# Internal imports
36+
from ...main.m2l_api import extract_basal_contacts
3637
from ...main.vectorLayerWrapper import (
3738
dataframeToQgsTable,
3839
matrixToDict,
@@ -132,9 +133,9 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None:
132133
self.addParameter(
133134
QgsProcessingParameterFeatureSource(
134135
self.INPUT_BASAL_CONTACTS,
135-
"Basal Contacts",
136+
"Basal Contacts (optional, extracted from geology if omitted)",
136137
[QgsProcessing.TypeVectorLine],
137-
defaultValue='Basal Contacts',
138+
optional=True,
138139
)
139140
)
140141
self.addParameter(
@@ -261,9 +262,9 @@ def processAlgorithm(
261262

262263
sampled_contacts = self.parameterAsSource(parameters, self.INPUT_SAMPLED_CONTACTS, context)
263264
unit_name_field = self.parameterAsString(parameters, self.INPUT_UNIT_NAME_FIELD, context)
265+
geology_layer = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context)
264266

265267
if bounding_box_type == 0:
266-
geology_layer = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context)
267268
extent = geology_layer.extent()
268269
bounding_box = {
269270
'minx': extent.xMinimum(),
@@ -315,7 +316,19 @@ def processAlgorithm(
315316
# convert layers to dataframe or geodataframe
316317
units = qgsLayerToDataFrame(geology_data)
317318
geology_data = qgsLayerToGeoDataFrame(geology_data)
318-
basal_contacts = qgsLayerToGeoDataFrame(basal_contacts)
319+
if basal_contacts is not None:
320+
basal_contacts = qgsLayerToGeoDataFrame(basal_contacts)
321+
else:
322+
feedback.pushInfo(
323+
"No basal contacts layer supplied; extracting basal contacts from geology layer..."
324+
)
325+
basal_contacts = extract_basal_contacts(
326+
geology=geology_layer,
327+
stratigraphic_order=stratigraphic_order,
328+
unit_name_field=unit_name_field,
329+
updater=feedback.pushInfo,
330+
target_crs=geology_layer.crs() if geology_layer else None,
331+
)['basal_contacts']
319332
structure_data = qgsLayerToDataFrame(structure_data)
320333
rename_map = {}
321334
missing_fields = []

0 commit comments

Comments
 (0)