diff --git a/loopstructural/gui/map2loop_tools/thickness_calculator_widget.py b/loopstructural/gui/map2loop_tools/thickness_calculator_widget.py
index 5250f8e..e993af8 100644
--- a/loopstructural/gui/map2loop_tools/thickness_calculator_widget.py
+++ b/loopstructural/gui/map2loop_tools/thickness_calculator_widget.py
@@ -338,15 +338,30 @@ def _run_calculator(self):
# Validate inputs based on calculator type
calculator_type = self.calculatorTypeComboBox.currentText()
- if calculator_type == "InterpolatedStructure":
- if not self.geologyLayerComboBox.currentLayer():
- QMessageBox.warning(self, "Missing Input", "Please select a geology layer.")
- return False
- if not self.basalContactsComboBox.currentLayer():
- QMessageBox.warning(self, "Missing Input", "Please select a basal contacts layer.")
+ if not self.geologyLayerComboBox.currentLayer():
+ QMessageBox.warning(self, "Missing Input", "Please select a geology layer.")
+ return False
+
+ # A basal contacts layer is optional: when omitted, basal contacts are
+ # extracted automatically from the geology layer using the
+ # stratigraphic column, so a stratigraphic order is required instead.
+ if not self.basalContactsComboBox.currentLayer():
+ stratigraphic_order = (
+ self.data_manager.get_stratigraphic_unit_names()
+ if self.data_manager and hasattr(self.data_manager, 'get_stratigraphic_unit_names')
+ else None
+ )
+ if not stratigraphic_order:
+ QMessageBox.warning(
+ self,
+ "Missing Input",
+ "Please select a basal contacts layer, or define a stratigraphic "
+ "column so basal contacts can be extracted from the geology layer "
+ "automatically.",
+ )
return False
- elif calculator_type == "StructuralPoint":
+ if calculator_type == "StructuralPoint":
if not self.structureLayerComboBox.currentLayer():
QMessageBox.warning(self, "Missing Input", "Please select a structure layer.")
return False
diff --git a/loopstructural/gui/map2loop_tools/thickness_calculator_widget.ui b/loopstructural/gui/map2loop_tools/thickness_calculator_widget.ui
index 405b124..7f20bc8 100644
--- a/loopstructural/gui/map2loop_tools/thickness_calculator_widget.ui
+++ b/loopstructural/gui/map2loop_tools/thickness_calculator_widget.ui
@@ -63,12 +63,16 @@
-
- Basal Contacts Layer:
+ Basal Contacts Layer (optional):
-
-
+
+
+ true
+
+
-
diff --git a/loopstructural/main/m2l_api.py b/loopstructural/main/m2l_api.py
index 725a55b..29467ac 100644
--- a/loopstructural/main/m2l_api.py
+++ b/loopstructural/main/m2l_api.py
@@ -429,9 +429,9 @@ def sample_contacts(
def calculate_thickness(
geology,
- basal_contacts,
sampled_contacts,
structure,
+ basal_contacts=None,
cross_sections=None,
calculator_type="InterpolatedStructure",
dtm=None,
@@ -451,12 +451,15 @@ def calculate_thickness(
----------
geology : QgsVectorLayer or GeoDataFrame
Geology polygon layer.
- basal_contacts : QgsVectorLayer or GeoDataFrame
- Basal contacts line layer.
sampled_contacts : QgsVectorLayer or GeoDataFrame
Sampled contacts point layer.
structure : QgsVectorLayer or GeoDataFrame
Structure point layer with orientation data.
+ basal_contacts : QgsVectorLayer or GeoDataFrame, optional
+ Basal contacts line layer, by default None. When omitted, basal
+ contacts are extracted automatically from ``geology`` using
+ ``stratigraphic_order``, so a separate basal-contacts extraction
+ step is not required.
cross_sections : QgsVectorLayer or GeoDataFrame, optional
Cross-sections line layer, by default None.
calculator_type : str, optional
@@ -474,7 +477,9 @@ def calculate_thickness(
max_line_length : float, optional
Maximum line length for StructuralPoint calculator, by default None.
stratigraphic_order : list, optional
- List of unit names in stratigraphic order, by default None.
+ List of unit names in stratigraphic order, by default None. Required
+ when ``basal_contacts`` is omitted, so basal contacts can be derived
+ from the geology layer.
updater : callable, optional
Callback function for progress updates, by default None.
@@ -486,14 +491,35 @@ def calculate_thickness(
if updater:
updater(f"Calculating thickness using {calculator_type}...")
+ auto_extracted_basal_contacts = basal_contacts is None
+ if auto_extracted_basal_contacts:
+ if not stratigraphic_order:
+ raise ValueError(
+ "A basal contacts layer or a stratigraphic order is required to calculate "
+ "thickness: without a basal contacts layer, the stratigraphic order is "
+ "needed to extract basal contacts from the geology layer."
+ )
+ if updater:
+ updater("No basal contacts layer supplied; extracting basal contacts from geology...")
+ target_crs = geology.crs() if hasattr(geology, 'crs') else None
+ basal_contacts_gdf = extract_basal_contacts(
+ geology=geology,
+ stratigraphic_order=stratigraphic_order,
+ unit_name_field=unit_name_field,
+ updater=updater,
+ debug_manager=debug_manager,
+ target_crs=target_crs,
+ )['basal_contacts']
+
# Convert layers to GeoDataFrames
geology_gdf = qgsLayerToGeoDataFrame(geology)
- basal_contacts_gdf = qgsLayerToGeoDataFrame(basal_contacts)
- basal_contacts_gdf = (
- basal_contacts_gdf.rename(columns={basal_contacts_unit_name: 'basal_unit'})
- if basal_contacts_unit_name
- else basal_contacts_gdf
- )
+ if not auto_extracted_basal_contacts:
+ basal_contacts_gdf = qgsLayerToGeoDataFrame(basal_contacts)
+ basal_contacts_gdf = (
+ basal_contacts_gdf.rename(columns={basal_contacts_unit_name: 'basal_unit'})
+ if basal_contacts_unit_name
+ else basal_contacts_gdf
+ )
sampled_contacts_gdf = qgsLayerToGeoDataFrame(sampled_contacts)
structure_gdf = qgsLayerToGeoDataFrame(structure)
cross_sections_gdf = qgsLayerToGeoDataFrame(cross_sections)
diff --git a/loopstructural/processing/algorithms/thickness_calculator.py b/loopstructural/processing/algorithms/thickness_calculator.py
index de239c8..a7abe4e 100644
--- a/loopstructural/processing/algorithms/thickness_calculator.py
+++ b/loopstructural/processing/algorithms/thickness_calculator.py
@@ -33,6 +33,7 @@
)
# Internal imports
+from ...main.m2l_api import extract_basal_contacts
from ...main.vectorLayerWrapper import (
dataframeToQgsTable,
matrixToDict,
@@ -132,9 +133,9 @@ def initAlgorithm(self, config: Optional[dict[str, Any]] = None) -> None:
self.addParameter(
QgsProcessingParameterFeatureSource(
self.INPUT_BASAL_CONTACTS,
- "Basal Contacts",
+ "Basal Contacts (optional, extracted from geology if omitted)",
[QgsProcessing.TypeVectorLine],
- defaultValue='Basal Contacts',
+ optional=True,
)
)
self.addParameter(
@@ -261,9 +262,9 @@ def processAlgorithm(
sampled_contacts = self.parameterAsSource(parameters, self.INPUT_SAMPLED_CONTACTS, context)
unit_name_field = self.parameterAsString(parameters, self.INPUT_UNIT_NAME_FIELD, context)
+ geology_layer = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context)
if bounding_box_type == 0:
- geology_layer = self.parameterAsVectorLayer(parameters, self.INPUT_GEOLOGY, context)
extent = geology_layer.extent()
bounding_box = {
'minx': extent.xMinimum(),
@@ -315,7 +316,19 @@ def processAlgorithm(
# convert layers to dataframe or geodataframe
units = qgsLayerToDataFrame(geology_data)
geology_data = qgsLayerToGeoDataFrame(geology_data)
- basal_contacts = qgsLayerToGeoDataFrame(basal_contacts)
+ if basal_contacts is not None:
+ basal_contacts = qgsLayerToGeoDataFrame(basal_contacts)
+ else:
+ feedback.pushInfo(
+ "No basal contacts layer supplied; extracting basal contacts from geology layer..."
+ )
+ basal_contacts = extract_basal_contacts(
+ geology=geology_layer,
+ stratigraphic_order=stratigraphic_order,
+ unit_name_field=unit_name_field,
+ updater=feedback.pushInfo,
+ target_crs=geology_layer.crs() if geology_layer else None,
+ )['basal_contacts']
structure_data = qgsLayerToDataFrame(structure_data)
rename_map = {}
missing_fields = []