diff --git a/loopstructural/gui/modelling/geological_model_tab.py b/loopstructural/gui/modelling/geological_model_tab.py
deleted file mode 100644
index c7121d4..0000000
--- a/loopstructural/gui/modelling/geological_model_tab.py
+++ /dev/null
@@ -1,92 +0,0 @@
-from PyQt5.QtCore import Qt
-from PyQt5.QtWidgets import (
- QPushButton,
- QSplitter,
- QTreeWidget,
- QTreeWidgetItem,
- QVBoxLayout,
- QWidget,
-)
-
-from loopstructural.gui.modelling.feature_details_panel import (
- FaultFeatureDetailsPanel,
- FoliationFeatureDetailsPanel,
-)
-from LoopStructural.modelling.features import FeatureType
-
-
-class GeologicalModelTab(QWidget):
- def __init__(self, parent=None, *, model_manager=None):
- super().__init__(parent)
- self.model_manager = model_manager
-
- # Main layout
- mainLayout = QVBoxLayout(self)
-
- # Splitter for collapsible layout
- splitter = QSplitter(self)
- mainLayout.addWidget(splitter)
-
- # Feature list panel
- self.featureList = QTreeWidget()
- self.featureList.setHeaderLabel("Geological Features")
- splitter.addWidget(self.featureList)
-
- # Feature details panel
- self.featureDetailsPanel = QWidget()
- splitter.addWidget(self.featureDetailsPanel)
-
- # Limit feature details panel expansion
- splitter.setStretchFactor(0, 1) # Feature list panel
- splitter.setStretchFactor(1, 0) # Feature details panel
- splitter.setOrientation(Qt.Horizontal) # Add horizontal slider
-
- # Initialize Model button
- self.initializeModelButton = QPushButton("Initialize Model")
- mainLayout.insertWidget(0, self.initializeModelButton)
-
- # Action buttons
-
- self.initializeModelButton.clicked.connect(self.initialize_model)
-
- # Connect feature selection to update details panel
- self.featureList.itemClicked.connect(self.on_feature_selected)
-
- def save_changes(self):
- # Logic to save changes
- pass
-
- def reset_parameters(self):
- # Logic to reset parameters
- pass
-
- def initialize_model(self):
- self.model_manager.update_model()
- self.featureList.clear() # Clear the feature list before populating it
- for feature in self.model_manager.features():
- if feature.name.startswith("__"):
- continue
- items = self.featureList.findItems(feature.name, Qt.MatchExactly)
- if items:
- # If the feature already exists, skip adding it again
- continue
- item = QTreeWidgetItem(self.featureList)
- item.setText(0, feature.name)
- item.setData(0, 1, feature)
- self.featureList.addTopLevelItem(item)
- # self.featureList.itemClicked.connect(self.on_feature_selected)
-
- def on_feature_selected(self, item):
- feature = item.data(0, 1)
- if feature.type == FeatureType.FAULT:
- print("Fault feature selected")
- self.featureDetailsPanel = FaultFeatureDetailsPanel(fault=feature)
- elif feature.type == FeatureType.INTERPOLATED:
- self.featureDetailsPanel = FoliationFeatureDetailsPanel(feature=feature)
- else:
- self.featureDetailsPanel = QWidget() # Default empty panel
-
- # Dynamically replace the featureDetailsPanel widget
- splitter = self.layout().itemAt(1).widget()
- splitter.widget(1).deleteLater() # Remove the existing widget
- splitter.addWidget(self.featureDetailsPanel) # Add the new widget
diff --git a/loopstructural/gui/modelling/geological_model_tab/__init__.py b/loopstructural/gui/modelling/geological_model_tab/__init__.py
new file mode 100644
index 0000000..78fa234
--- /dev/null
+++ b/loopstructural/gui/modelling/geological_model_tab/__init__.py
@@ -0,0 +1 @@
+from .geological_model_tab import GeologicalModelTab
diff --git a/loopstructural/gui/modelling/geological_model_tab/add_fault_dialog.py b/loopstructural/gui/modelling/geological_model_tab/add_fault_dialog.py
new file mode 100644
index 0000000..4b0fc30
--- /dev/null
+++ b/loopstructural/gui/modelling/geological_model_tab/add_fault_dialog.py
@@ -0,0 +1,33 @@
+import os
+
+from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout
+from PyQt5.uic import loadUi
+
+
+class AddFaultDialog(QDialog):
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ ui_path = os.path.join(os.path.dirname(__file__), 'add_fault_dialog.ui')
+ loadUi(ui_path, self)
+ self.setWindowTitle('Add Fault Feature')
+ # You can access widgets by their objectName from the .ui file
+ # Example: self.strike_input, self.dip_input, etc.
+
+ def get_fault_data(self):
+ return {
+ 'strike': self.strike_input.value(),
+ 'dip': self.dip_input.value(),
+ 'centre': (
+ self.centre_x_input.value(),
+ self.centre_y_input.value(),
+ self.centre_z_input.value(),
+ ),
+ 'ellipsoid_extents': (
+ self.extent_x_input.value(),
+ self.extent_y_input.value(),
+ self.extent_z_input.value(),
+ ),
+ 'displacement': self.displacement_input.value(),
+ 'pitch': self.pitch_input.value(),
+ 'name': self.name_input.text(),
+ }
diff --git a/loopstructural/gui/modelling/geological_model_tab/add_fault_dialog.ui b/loopstructural/gui/modelling/geological_model_tab/add_fault_dialog.ui
new file mode 100644
index 0000000..7c6d8e3
--- /dev/null
+++ b/loopstructural/gui/modelling/geological_model_tab/add_fault_dialog.ui
@@ -0,0 +1,114 @@
+
+
+ AddFaultDialog
+
+
+ Add Fault Feature
+
+
+ -
+
+
-
+
+
+ Strike (°)
+
+
+
+ -
+
+
+ -
+
+
+ Dip (°)
+
+
+
+ -
+
+
+ -
+
+
+ Centre (X, Y, Z)
+
+
+
+ -
+
+
-
+
+
+ -
+
+
+ -
+
+
+
+
+ -
+
+
+ Ellipsoid Extents (X, Y, Z)
+
+
+
+ -
+
+
-
+
+
+ -
+
+
+ -
+
+
+
+
+ -
+
+
+ Displacement
+
+
+
+ -
+
+
+ -
+
+
+ Pitch (°)
+
+
+
+ -
+
+
+ -
+
+
+ Name
+
+
+
+ -
+
+
+
+
+ -
+
+
+ QDialogButtonBox::Ok|QDialogButtonBox::Cancel
+
+
+
+
+
+
+
+
diff --git a/loopstructural/gui/modelling/geological_model_tab/add_foliation_dialog.py b/loopstructural/gui/modelling/geological_model_tab/add_foliation_dialog.py
new file mode 100644
index 0000000..93088d7
--- /dev/null
+++ b/loopstructural/gui/modelling/geological_model_tab/add_foliation_dialog.py
@@ -0,0 +1,148 @@
+import os
+
+from PyQt5.QtWidgets import QDialog, QDialogButtonBox
+from PyQt5.uic import loadUi
+
+from .layer_selection_table import LayerSelectionTable
+
+
+class AddFoliationDialog(QDialog):
+ def __init__(self, parent=None, *, data_manager=None, model_manager=None):
+ super().__init__(parent)
+ self.data_manager = data_manager
+ self.model_manager = model_manager
+ ui_path = os.path.join(os.path.dirname(__file__), 'add_foliation_dialog.ui')
+ loadUi(ui_path, self)
+ self.setWindowTitle('Add Foliation')
+
+ # Create the layer selection table widget
+ self.layer_table = LayerSelectionTable(
+ data_manager=self.data_manager,
+ feature_name_provider=lambda: self.name,
+ name_validator=lambda: (self.name_valid, self.name_error)
+ )
+
+ # Replace or integrate with existing UI
+ self._integrate_layer_table()
+
+ self.buttonBox.accepted.connect(self.add_foliation)
+ self.buttonBox.rejected.connect(self.cancel)
+
+ self.modelFeatureComboBox.addItems(
+ [f.name for f in self.model_manager.features() if not f.name.startswith("__")]
+ )
+ self.name_valid = False
+ self.name_error = ""
+
+ def validate_name_field(text):
+ """Validate the feature name field."""
+ valid = True
+ old_name = self.name
+ new_name = text.strip()
+
+ if not new_name:
+ valid = False
+ self.name_error = "Feature name cannot be empty."
+ elif new_name in [f.name for f in self.model_manager.features()]:
+ valid = False
+ self.name_error = "Feature name must be unique."
+ elif new_name in self.data_manager.feature_data:
+ valid = False
+ self.name_error = "Layer already exists in the data manager."
+
+ if not valid:
+ self.name_valid = False
+ self.feature_name_input.setStyleSheet("border: 1px solid red;")
+ else:
+ self.feature_name_input.setStyleSheet("")
+ self.name_valid = True
+
+ # Enable/disable the OK button based on validation
+ self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(self.name_valid)
+
+ # If the name changes, update the data manager key and reinitialize table
+ if old_name != new_name and old_name in self.data_manager.feature_data:
+ # Save current table data
+ old_data = self.layer_table.get_table_data()
+
+ # Remove old key and set new key
+ self.data_manager.feature_data.pop(old_name, None)
+ if new_name and valid:
+ self.data_manager.feature_data[new_name] = old_data
+
+ # Update table to reflect new feature name
+ self.layer_table.initialize_feature_data()
+ self.layer_table.restore_table_state()
+
+ self.feature_name_input.textChanged.connect(validate_name_field)
+
+ @property
+ def name(self):
+ return self.feature_name_input.text().strip()
+
+ def add_foliation(self):
+ if not self.name_valid:
+ self.data_manager.logger(f'Name is invalid: {self.name_error}', log_level=2)
+ return
+
+ # Ensure table state is synchronized with data manager
+ self.layer_table.sync_table_with_data()
+
+ # Check if we have any layers selected
+ if not self.layer_table.has_layers():
+ self.data_manager.logger("No layers selected for the foliation.", log_level=2)
+ return
+
+ folded_feature_name = None
+ if self.modelFeatureComboBox.currentText() != "":
+ folded_feature_name = self.modelFeatureComboBox.currentText()
+
+ self.data_manager.add_foliation_to_model(self.name, folded_feature_name=folded_feature_name)
+ self.accept() # Close the dialog
+
+ def cancel(self):
+ # Clean up any temporary data if necessary
+ if self.name in self.data_manager.feature_data:
+ self.data_manager.feature_data.pop(self.name, None)
+ self.reject()
+
+ def _integrate_layer_table(self):
+ """Integrate the layer table widget with the existing UI."""
+ # Try to replace existing table widget if it exists
+ if hasattr(self, 'items_table'):
+ table_parent = self.items_table.parent()
+
+ # Get the position of the original table
+ if hasattr(table_parent, 'layout') and table_parent.layout():
+ layout = table_parent.layout()
+
+ # Find the index of the original table in the layout
+ table_index = -1
+ for i in range(layout.count()):
+ item = layout.itemAt(i)
+ if item and item.widget() == self.items_table:
+ table_index = i
+ break
+
+ # Remove original widgets
+ if hasattr(self, 'items_table'):
+ self.items_table.setParent(None)
+ if hasattr(self, 'add_item_button'):
+ self.add_item_button.setParent(None)
+
+ # Insert new widget at the same position
+ if table_index >= 0:
+ layout.insertWidget(table_index, self.layer_table)
+ else:
+ layout.addWidget(self.layer_table)
+ else:
+ # Fallback: add to parent widget directly
+ if hasattr(table_parent, 'layout') and not table_parent.layout():
+ from PyQt5.QtWidgets import QVBoxLayout
+ layout = QVBoxLayout(table_parent)
+ table_parent.setLayout(layout)
+ layout.addWidget(self.layer_table)
+
+ # If no existing table found, try to add to main layout
+ elif hasattr(self, 'layout') and self.layout():
+ self.layout().addWidget(self.layer_table)
\ No newline at end of file
diff --git a/loopstructural/gui/modelling/geological_model_tab/add_foliation_dialog.ui b/loopstructural/gui/modelling/geological_model_tab/add_foliation_dialog.ui
new file mode 100644
index 0000000..121443e
--- /dev/null
+++ b/loopstructural/gui/modelling/geological_model_tab/add_foliation_dialog.ui
@@ -0,0 +1,82 @@
+
+
+ AddFoliationDialog
+
+
+
+ 0
+ 0
+ 274
+ 426
+
+
+
+ Add Foliation
+
+
+ -
+
+
+ Feature Name
+
+
+
+ -
+
+
+ -
+
+
+ Items
+
+
+
+ -
+
+
+ 0
+
+
+ 2
+
+
+
+ Type
+
+
+
+
+ Delete
+
+
+
+
+ -
+
+
+ <html><head/><body><p>Add data to fold frame</p></body></html>
+
+
+ +
+
+
+
+ -
+
+
+ <html><head/><body><p>Link fold to feature</p></body></html>
+
+
+
+ -
+
+
+ QDialogButtonBox::Cancel|QDialogButtonBox::Ok
+
+
+
+
+
+
+
+
diff --git a/loopstructural/gui/modelling/geological_model_tab/add_unconformity_dialog.py b/loopstructural/gui/modelling/geological_model_tab/add_unconformity_dialog.py
new file mode 100644
index 0000000..978095f
--- /dev/null
+++ b/loopstructural/gui/modelling/geological_model_tab/add_unconformity_dialog.py
@@ -0,0 +1,68 @@
+from PyQt5.QtWidgets import (
+ QComboBox,
+ QDialog,
+ QDialogButtonBox,
+ QDoubleSpinBox,
+ QFormLayout,
+ QVBoxLayout,
+ QWidget,
+)
+from LoopStructural.modelling.features import FeatureType
+class AddUnconformityDialog(QDialog):
+ def __init__(self, parent=None, data_manager=None, model_manager=None):
+ super().__init__(parent)
+ self.data_manager = data_manager
+ self.model_manager = model_manager
+ self.setWindowTitle("Add Unconformity")
+ self.setMinimumWidth(300)
+
+ layout = QVBoxLayout()
+
+ form_layout = QFormLayout()
+
+ self.foliation_combo = QComboBox()
+ foliations = [
+ feature.name
+ for feature in self.model_manager.features()
+ if feature.type == FeatureType.INTERPOLATED
+ ]
+ self.foliation_combo.addItems(foliations)
+ form_layout.addRow("Foliation:", self.foliation_combo)
+
+ self.value_spinbox = QDoubleSpinBox()
+ self.value_spinbox.setRange(-1e6, 1e6)
+ self.value_spinbox.setDecimals(3)
+ self.value_spinbox.setValue(0.0)
+ form_layout.addRow("Value:", self.value_spinbox)
+
+ self.type_combo = QComboBox()
+ self.type_combo.addItems(["Unconformity", "Onlap Unconformity"])
+ form_layout.addRow("Type:", self.type_combo)
+
+ layout.addLayout(form_layout)
+
+ button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
+ button_box.accepted.connect(self.accept)
+ button_box.rejected.connect(self.reject)
+ layout.addWidget(button_box)
+
+ self.setLayout(layout)
+
+ def accept(self):
+ foliation_name = self.foliation_combo.currentText()
+ value = self.value_spinbox.value()
+ type_text = self.type_combo.currentText()
+ if type_text == "Unconformity":
+ feature_type = FeatureType.UNCONFORMITY
+ else:
+ feature_type = FeatureType.ONLAPUNCONFORMITY
+
+ try:
+ self.model_manager.add_unconformity(foliation_name, value, feature_type)
+ super().accept()
+ except ValueError as e:
+ from PyQt5.QtWidgets import QMessageBox
+
+ QMessageBox.critical(self, "Error", str(e))
+ def reject(self):
+ super().reject()
\ No newline at end of file
diff --git a/loopstructural/gui/modelling/feature_details_panel.py b/loopstructural/gui/modelling/geological_model_tab/feature_details_panel.py
similarity index 53%
rename from loopstructural/gui/modelling/feature_details_panel.py
rename to loopstructural/gui/modelling/geological_model_tab/feature_details_panel.py
index c369ae5..359d622 100644
--- a/loopstructural/gui/modelling/feature_details_panel.py
+++ b/loopstructural/gui/modelling/geological_model_tab/feature_details_panel.py
@@ -1,22 +1,29 @@
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
+ QCheckBox,
QComboBox,
QDoubleSpinBox,
QFormLayout,
QLabel,
+ QPushButton,
QScrollArea,
QVBoxLayout,
QWidget,
+ QTableWidget,
)
+from .layer_selection_table import LayerSelectionTable
+from .splot import SPlotDialog
from LoopStructural.modelling.features import StructuralFrame
-from LoopStructural.utils import normal_vector_to_strike_and_dip
+from LoopStructural.utils import normal_vector_to_strike_and_dip, plungeazimuth2vector
class BaseFeatureDetailsPanel(QWidget):
- def __init__(self, parent=None, *, feature=None):
+ def __init__(self, parent=None, *, feature=None, model_manager=None, data_manager=None):
super().__init__(parent)
self.feature = feature
+ self.model_manager = model_manager
+ self.data_manager = data_manager
# Create a scroll area for horizontal scrolling
scroll = QScrollArea(self)
scroll.setWidgetResizable(True)
@@ -70,6 +77,11 @@ def __init__(self, parent=None, *, feature=None):
self.n_elements_spinbox.valueChanged.connect(self.updateNelements)
+ self.layer_table = LayerSelectionTable(
+ data_manager=self.data_manager,
+ feature_name_provider=lambda: self.feature.name,
+ name_validator=lambda: (True, ''), # Always valid in this context
+ )
# Form layout for better organization
form_layout = QFormLayout()
form_layout.addRow(self.interpolator_type_label, self.interpolator_type_combo)
@@ -77,10 +89,10 @@ def __init__(self, parent=None, *, feature=None):
form_layout.addRow('Regularisation', self.regularisation_spin_box)
form_layout.addRow('Contact points weight', self.cpw_spin_box)
form_layout.addRow('Orientation point weight', self.npw_spin_box)
-
QgsCollapsibleGroupBox = QWidget()
QgsCollapsibleGroupBox.setLayout(form_layout)
self.layout.addWidget(QgsCollapsibleGroupBox)
+ self.layout.addWidget(self.layer_table)
# self.layout.addLayout(form_layout)
@@ -112,8 +124,9 @@ def getNelements(self, feature):
class FaultFeatureDetailsPanel(BaseFeatureDetailsPanel):
- def __init__(self, parent=None, *, fault=None):
- super().__init__(parent, feature=fault)
+
+ def __init__(self, parent=None, *, fault=None, model_manager=None, data_manager=None):
+ super().__init__(parent, feature=fault, model_manager=model_manager, data_manager=data_manager)
if fault is None:
raise ValueError("Fault must be provided.")
self.fault = fault
@@ -194,8 +207,160 @@ def __init__(self, parent=None, *, fault=None):
class FoliationFeatureDetailsPanel(BaseFeatureDetailsPanel):
- def __init__(self, parent=None, *, feature=None):
- super().__init__(parent, feature=feature)
+ def __init__(self, parent=None, *, feature=None, model_manager=None, data_manager=None):
+ super().__init__(parent, feature=feature, model_manager=model_manager, data_manager=data_manager)
+ if feature is None:
+ raise ValueError("Feature must be provided.")
+ self.feature = feature
+ form_layout = QFormLayout()
+ fold_frame_combobox = QComboBox()
+ fold_frame_combobox.addItems([""] + [f.name for f in self.model_manager.fold_frames])
+ fold_frame_combobox.currentTextChanged.connect(self.on_fold_frame_changed)
+ form_layout.addRow("Attach fold frame", fold_frame_combobox)
+
+ convert_to_frame_button = QPushButton("Convert to Structural Frame")
+ convert_to_frame_button.clicked.connect(
+ lambda: self.model_manager.convert_feature_to_structural_frame(self.feature.name)
+ )
+ form_layout.addRow(convert_to_frame_button)
+ QgsCollapsibleGroupBox = QWidget()
+ QgsCollapsibleGroupBox.setLayout(form_layout)
+ self.layout.addWidget(QgsCollapsibleGroupBox)
+ # Remove redundant layout setting
+ self.setLayout(self.layout)
+
+ def on_fold_frame_changed(self, text):
+ self.model_manager.add_fold_to_feature(self.feature.name, fold_frame_name=text)
+
+
+class StructuralFrameFeatureDetailsPanel(BaseFeatureDetailsPanel):
+ def __init__(self, parent=None, *, feature=None, model_manager=None, data_manager=None):
+ super().__init__(parent, feature=feature, model_manager=model_manager, data_manager=data_manager)
+
+
+class FoldedFeatureDetailsPanel(BaseFeatureDetailsPanel):
+ def __init__(self, parent=None, *, feature=None, model_manager=None, data_manager=None):
+ super().__init__(parent, feature=feature, model_manager=model_manager, data_manager=data_manager)
# Remove redundant layout setting
# self.setLayout(self.layout)
+ form_layout = QFormLayout()
+ # remove_fold_frame_button = QPushButton("Remove Fold Frame")
+ # remove_fold_frame_button.clicked.connect(self.remove_fold_frame)
+ # form_layout.addRow(remove_fold_frame_button)
+
+ norm_length = QDoubleSpinBox()
+ norm_length.setRange(0, 100000)
+ norm_length.setValue(1) # Set a default value
+ norm_length.valueChanged.connect(
+ lambda value: feature.builder.update_build_arguments(
+ {
+ 'fold_weights': {
+ **feature.builder.build_arguments.get('fold_weights', {}),
+ 'fold_norm': value,
+ }
+ }
+ )
+ )
+ form_layout.addRow("Normal Length", norm_length)
+
+ norm_weight = QDoubleSpinBox()
+ norm_weight.setRange(0, 100000)
+ norm_weight.setValue(1)
+ norm_weight.valueChanged.connect(
+ lambda value: feature.builder.update_build_arguments(
+ {
+ 'fold_weights': {
+ **feature.builder.build_arguments.get('fold_weights', {}),
+ 'fold_normalisation': value,
+ }
+ }
+ )
+ )
+ form_layout.addRow("Normal Weight", norm_weight)
+
+ fold_axis_weight = QDoubleSpinBox()
+ fold_axis_weight.setRange(0, 100000)
+ fold_axis_weight.setValue(1)
+ fold_axis_weight.valueChanged.connect(
+ lambda value: feature.builder.update_build_arguments(
+ {
+ 'fold_weights': {
+ **feature.builder.build_arguments.get('fold_weights', {}),
+ 'fold_axis_w': value,
+ }
+ }
+ )
+ )
+ form_layout.addRow("Fold Axis Weight", fold_axis_weight)
+
+ fold_orientation_weight = QDoubleSpinBox()
+ fold_orientation_weight.setRange(0, 100000)
+ fold_orientation_weight.setValue(1)
+ fold_orientation_weight.valueChanged.connect(
+ lambda value: feature.builder.update_build_arguments(
+ {
+ 'fold_weights': {
+ **feature.builder.build_arguments.get('fold_weights', {}),
+ 'fold_orientation': value,
+ }
+ }
+ )
+ )
+ form_layout.addRow("Fold Orientation Weight", fold_orientation_weight)
+
+ average_fold_axis_checkbox = QCheckBox("Average Fold Axis")
+ average_fold_axis_checkbox.setChecked(True)
+ average_fold_axis_checkbox.stateChanged.connect(
+ lambda state: feature.builder.update_build_arguments(
+ {'av_fold_axis': state != Qt.Checked}
+ )
+ )
+ average_fold_axis_checkbox.stateChanged.connect(
+ lambda state: fold_azimuth.setEnabled(state == Qt.Checked)
+ )
+ average_fold_axis_checkbox.stateChanged.connect(
+ lambda state: fold_plunge.setEnabled(state == Qt.Checked)
+ )
+ fold_plunge = QDoubleSpinBox()
+ fold_plunge.setRange(0, 90)
+ fold_plunge.setValue(0)
+ fold_azimuth = QDoubleSpinBox()
+ fold_azimuth.setRange(0, 360)
+ fold_azimuth.setValue(0)
+ fold_azimuth.setEnabled(False)
+ fold_plunge.setEnabled(False)
+ fold_plunge.valueChanged.connect(self.foldAxisFromPlungeAzimuth)
+ fold_azimuth.valueChanged.connect(self.foldAxisFromPlungeAzimuth)
+ form_layout.addRow(average_fold_axis_checkbox)
+ form_layout.addRow("Fold Plunge", fold_plunge)
+ form_layout.addRow("Fold Azimuth", fold_azimuth)
+ # splot_button = QPushButton("S-Plot")
+ # splot_button.clicked.connect(
+ # lambda: self.open_splot_dialog()
+ # )
+ form_layout.addRow(splot_button)
+ QgsCollapsibleGroupBox = QWidget()
+ QgsCollapsibleGroupBox.setLayout(form_layout)
+ self.layout.addWidget(QgsCollapsibleGroupBox)
+ # Remove redundant layout setting
+ self.setLayout(self.layout)
+ def open_splot_dialog(self):
+ dialog = SPlotDialog(self, data_manager=self.data_manager, model_manager=self.model_manager, feature_name=self.feature.name)
+ if dialog.exec_() == dialog.Accepted:
+ pass
+ def remove_fold_frame(self):
+ pass
+
+ def foldAxisFromPlungeAzimuth(self):
+ """Calculate the fold axis from plunge and azimuth."""
+ if self.feature:
+ plunge = (
+ self.layout().itemAt(0).widget().findChild(QDoubleSpinBox, "fold_plunge").value()
+ )
+ azimuth = (
+ self.layout().itemAt(0).widget().findChild(QDoubleSpinBox, "fold_azimuth").value()
+ )
+ vector = plungeazimuth2vector(plunge, azimuth)[0]
+ if plunge is not None and azimuth is not None:
+ self.feature.builder.update_build_arguments({'fold_axis': vector})
diff --git a/loopstructural/gui/modelling/geological_model_tab/geological_model_tab.py b/loopstructural/gui/modelling/geological_model_tab/geological_model_tab.py
new file mode 100644
index 0000000..139ae06
--- /dev/null
+++ b/loopstructural/gui/modelling/geological_model_tab/geological_model_tab.py
@@ -0,0 +1,151 @@
+from PyQt5.QtCore import Qt
+from PyQt5.QtWidgets import (
+ QMenu,
+ QPushButton,
+ QSplitter,
+ QTreeWidget,
+ QTreeWidgetItem,
+ QVBoxLayout,
+ QWidget,
+)
+
+from .feature_details_panel import (
+ FaultFeatureDetailsPanel,
+ FoldedFeatureDetailsPanel,
+ FoliationFeatureDetailsPanel,
+ StructuralFrameFeatureDetailsPanel,
+)
+from LoopStructural.modelling.features import FeatureType
+
+# Import the AddFaultDialog
+from .add_fault_dialog import AddFaultDialog
+from .add_foliation_dialog import AddFoliationDialog
+from .add_unconformity_dialog import AddUnconformityDialog
+
+
+class GeologicalModelTab(QWidget):
+ def __init__(self, parent=None, *, model_manager=None, data_manager=None):
+ super().__init__(parent)
+ self.model_manager = model_manager
+ self.data_manager = data_manager
+ self.model_manager.observers.append(self.update_feature_list)
+ # Main layout
+ mainLayout = QVBoxLayout(self)
+
+ # Splitter for collapsible layout
+ splitter = QSplitter(self)
+ mainLayout.addWidget(splitter)
+
+ # Feature list panel
+
+ self.featureList = QTreeWidget()
+ self.featureList.setHeaderLabel("Geological Features")
+ side_panel = QVBoxLayout()
+ side_panel.addWidget(self.featureList)
+ add_feature_button = QPushButton("Add Feature")
+
+ add_feature_button.setContextMenuPolicy(Qt.CustomContextMenu)
+ add_feature_button.customContextMenuRequested.connect(self.show_add_feature_menu)
+ add_feature_button.clicked.connect(self.show_add_feature_menu)
+ side_panel.addWidget(add_feature_button)
+ side_panel_widget = QWidget()
+ side_panel_widget.setLayout(side_panel)
+ splitter.addWidget(side_panel_widget)
+ # self.splitter.addWidget(QWidget()) # Placeholder for the feature list panel
+ # Feature details panel
+ self.featureDetailsPanel = QWidget()
+ splitter.addWidget(self.featureDetailsPanel)
+
+ # Limit feature details panel expansion
+ splitter.setStretchFactor(0, 1) # Feature list panel
+ splitter.setStretchFactor(1, 0) # Feature details panel
+ splitter.setOrientation(Qt.Horizontal) # Add horizontal slider
+
+ # Initialize Model button
+ self.initializeModelButton = QPushButton("Initialize Model")
+ mainLayout.insertWidget(0, self.initializeModelButton)
+
+ # Action buttons
+
+ self.initializeModelButton.clicked.connect(self.initialize_model)
+
+ # Connect feature selection to update details panel
+ self.featureList.itemClicked.connect(self.on_feature_selected)
+
+ def show_add_feature_menu(self, *args):
+ menu = QMenu(self)
+ add_fault = menu.addAction("Add Fault")
+ add_foliaton = menu.addAction("Add Foliation")
+ add_unconformity = menu.addAction("Add Unconformity")
+ buttonPosition = self.sender().mapToGlobal(self.sender().rect().bottomLeft())
+ action = menu.exec_(buttonPosition)
+
+ if action == add_fault:
+ self.open_add_fault_dialog()
+ elif action == add_foliaton:
+ self.open_add_foliation_dialog()
+ elif action == add_unconformity:
+ self.open_add_unconformity_dialog()
+ def open_add_fault_dialog(self):
+ dialog = AddFaultDialog(self)
+ if dialog.exec_() == dialog.Accepted:
+ fault_data = dialog.get_fault_data()
+ # TODO: Add logic to use fault_data to add the fault to the model
+ print("Fault data:", fault_data)
+
+ def open_add_foliation_dialog(self):
+ dialog = AddFoliationDialog(
+ self, data_manager=self.data_manager, model_manager=self.model_manager
+ )
+ if dialog.exec_() == dialog.Accepted:
+ pass
+ def open_add_unconformity_dialog(self):
+ dialog = AddUnconformityDialog(
+ self, data_manager=self.data_manager, model_manager=self.model_manager
+ )
+ if dialog.exec_() == dialog.Accepted:
+ pass
+ def initialize_model(self):
+ self.model_manager.update_model()
+
+ def update_feature_list(self):
+ self.featureList.clear() # Clear the feature list before populating it
+ for feature in self.model_manager.features():
+ if feature.name.startswith("__"):
+ continue
+ items = self.featureList.findItems(feature.name, Qt.MatchExactly)
+ if items:
+ # If the feature already exists, skip adding it again
+ continue
+ item = QTreeWidgetItem(self.featureList)
+ item.setText(0, feature.name)
+ item.setData(0, 1, feature)
+ self.featureList.addTopLevelItem(item)
+ # self.featureList.itemClicked.connect(self.on_feature_selected)
+
+ def on_feature_selected(self, item):
+ feature_name = item.text(0)
+ feature = self.model_manager.model.get_feature_by_name(feature_name)
+ if feature.type == FeatureType.FAULT:
+ self.featureDetailsPanel = FaultFeatureDetailsPanel(
+ fault=feature, model_manager=self.model_manager, data_manager=self.data_manager
+ )
+ elif feature.type == FeatureType.INTERPOLATED:
+ self.featureDetailsPanel = FoliationFeatureDetailsPanel(
+ feature=feature, model_manager=self.model_manager, data_manager=self.data_manager
+ )
+ elif feature.type == FeatureType.STRUCTURALFRAME:
+ self.featureDetailsPanel = StructuralFrameFeatureDetailsPanel(
+ feature=feature, model_manager=self.model_manager, data_manager=self.data_manager
+ )
+ elif feature.type == FeatureType.FOLDED:
+ self.featureDetailsPanel = FoldedFeatureDetailsPanel(
+ feature=feature, model_manager=self.model_manager, data_manager=self.data_manager
+ )
+ else:
+ self.featureDetailsPanel = QWidget() # Default empty panel
+
+ # Dynamically replace the featureDetailsPanel widget
+ splitter = self.layout().itemAt(1).widget()
+ splitter.widget(1).deleteLater() # Remove the existing widget
+ splitter.addWidget(self.featureDetailsPanel) # Add the new widget
diff --git a/loopstructural/gui/modelling/geological_model_tab/layer_selection_table.py b/loopstructural/gui/modelling/geological_model_tab/layer_selection_table.py
new file mode 100644
index 0000000..4648f87
--- /dev/null
+++ b/loopstructural/gui/modelling/geological_model_tab/layer_selection_table.py
@@ -0,0 +1,550 @@
+from PyQt5.QtWidgets import (
+ QComboBox,
+ QDialog,
+ QDialogButtonBox,
+ QHBoxLayout,
+ QLabel,
+ QPushButton,
+ QTableWidget,
+ QVBoxLayout,
+ QWidget,
+)
+from qgis.core import QgsMapLayerProxyModel
+from qgis.gui import QgsFieldComboBox, QgsMapLayerComboBox
+
+
+class LayerSelectionTable(QWidget):
+ """
+ Self-contained widget for layer selection table functionality for geological features.
+
+ This widget includes:
+ - A table for displaying selected layers with Type, Layer, and Delete columns
+ - An "Add Data" button at the bottom for adding new layers
+ - Complete data management integration with data_manager.feature_data
+
+ Usage example:
+ # Create the widget
+ layer_table = LayerSelectionTable(
+ data_manager=my_data_manager,
+ feature_name_provider=lambda: "my_feature_name",
+ name_validator=lambda: (True, "") # or your validation logic
+ )
+
+ # Add to your layout
+ layout.addWidget(layer_table)
+
+ # Access data
+ if layer_table.has_layers():
+ data = layer_table.get_table_data()
+ """
+
+ def __init__(self, data_manager, feature_name_provider, name_validator, parent=None):
+ """
+ Initialize the layer selection table widget.
+
+ Args:
+ data_manager: Data manager instance
+ feature_name_provider: Callable that returns the current feature name
+ name_validator: Callable that returns (is_valid, error_message)
+ parent: Parent widget (optional)
+ """
+ super().__init__(parent)
+ self.data_manager = data_manager
+ self.get_feature_name = feature_name_provider
+ self.validate_name = name_validator
+
+ self._setup_ui()
+ self.initialize_feature_data()
+ self.restore_table_state()
+
+ def _setup_ui(self):
+ """Setup the widget UI with table and add button."""
+ layout = QVBoxLayout(self)
+
+ # Create the table widget
+ self.table = QTableWidget()
+ self._setup_table()
+ layout.addWidget(self.table)
+
+ # Create add button
+ self.add_button = QPushButton("Add Data")
+ self.add_button.clicked.connect(self.add_item_row)
+ layout.addWidget(self.add_button)
+
+ def _setup_table(self):
+ """Setup table columns and headers."""
+ self.table.setColumnCount(3)
+ self.table.setHorizontalHeaderLabels(["Type", "Select Layer", "Delete"])
+
+ def initialize_feature_data(self):
+ """Initialize feature data in the data manager if it doesn't exist."""
+ feature_name = self.get_feature_name()
+ if feature_name and feature_name not in self.data_manager.feature_data:
+ self.data_manager.feature_data[feature_name] = {}
+
+ def restore_table_state(self):
+ """Restore table state from data manager."""
+ feature_name = self.get_feature_name()
+ if not feature_name or feature_name not in self.data_manager.feature_data:
+ return
+
+ # Clear existing table rows
+ self.table.setRowCount(0)
+
+ # Restore rows from data
+ feature_data = self.data_manager.feature_data[feature_name]
+ for layer_name, layer_data in feature_data.items():
+ self._add_row_from_data(layer_data)
+
+ def _add_row_from_data(self, layer_data):
+ """Add a row to the table from existing data."""
+ row = self.table.rowCount()
+ self.table.insertRow(row)
+
+ # Type dropdown
+ type_combo = self._create_type_combo()
+ type_combo.setCurrentText(layer_data.get('type', 'Value'))
+ self.table.setCellWidget(row, 0, type_combo)
+
+ # Select Layer button
+ select_layer_btn = self._create_select_layer_button(row, type_combo)
+ self._update_button_with_selection(select_layer_btn, layer_data)
+ self.table.setCellWidget(row, 1, select_layer_btn)
+
+ # Delete button
+ del_btn = self._create_delete_button(row)
+ self.table.setCellWidget(row, 2, del_btn)
+
+ def add_item_row(self):
+ """Add a new row to the table."""
+ self.initialize_feature_data() # Ensure feature data exists
+
+ row = self.table.rowCount()
+ self.table.insertRow(row)
+
+ # Type dropdown
+ type_combo = self._create_type_combo()
+ self.table.setCellWidget(row, 0, type_combo)
+
+ # Select Layer button
+ select_layer_btn = self._create_select_layer_button(row, type_combo)
+ self.table.setCellWidget(row, 1, select_layer_btn)
+
+ # Delete button
+ del_btn = self._create_delete_button(row)
+ self.table.setCellWidget(row, 2, del_btn)
+
+ def _create_type_combo(self):
+ """Create type selection combo box."""
+ combo = QComboBox()
+ combo.addItems(["Value", "Form Line", "Orientation", "Inequality"])
+ return combo
+
+ def _create_select_layer_button(self, row, type_combo):
+ """Create select layer button."""
+ btn = QPushButton("Select Layer")
+
+ def open_layer_dialog():
+ name_valid, name_error = self.validate_name()
+ if not name_valid:
+ self.data_manager.logger(f'Name is invalid: {name_error}', log_level=2)
+ return
+
+ dialog = LayerSelectionDialog(
+ parent=self.table,
+ data_manager=self.data_manager,
+ feature_name=self.get_feature_name(),
+ layer_type=type_combo.currentText(),
+ existing_data=self._get_existing_data_for_button(btn)
+ )
+
+ if dialog.exec_() == QDialog.Accepted:
+ layer_data = dialog.get_layer_data()
+ if layer_data:
+ self._update_button_with_selection(btn, layer_data)
+ self._add_layer_to_data_manager(layer_data)
+
+ btn.clicked.connect(open_layer_dialog)
+ return btn
+
+ def _create_delete_button(self, row):
+ """Create delete button for a row."""
+ btn = QPushButton("Delete")
+ btn.clicked.connect(lambda: self._delete_item_row(row))
+ return btn
+
+ def _delete_item_row(self, row):
+ """Delete a row from the table and update data manager."""
+ # Find the select layer button in the same row to get layer name
+ select_btn = self.table.cellWidget(row, 1)
+ if hasattr(select_btn, 'selected_layer'):
+ layer_name = select_btn.selected_layer
+ feature_name = self.get_feature_name()
+ if feature_name in self.data_manager.feature_data:
+ self.data_manager.feature_data[feature_name].pop(layer_name, None)
+ print(f'Removing layer: {layer_name} for feature: {feature_name}')
+
+ # Remove the row from table
+ self.table.removeRow(row)
+
+ # Update delete button connections for remaining rows
+ self._update_delete_button_connections()
+
+ def _update_delete_button_connections(self):
+ """Update delete button connections after row deletion to maintain correct row indices."""
+ for row in range(self.table.rowCount()):
+ delete_btn = self.table.cellWidget(row, 2)
+ if delete_btn:
+ # Disconnect old connections
+ delete_btn.clicked.disconnect()
+ # Reconnect with correct row index
+ delete_btn.clicked.connect(lambda checked, r=row: self._delete_item_row(r))
+
+ def _get_existing_data_for_button(self, btn):
+ """Get existing data for a button if it has been configured."""
+ if btn.text() != "Select Layer" and hasattr(btn, 'selected_layer'):
+ feature_name = self.get_feature_name()
+ if feature_name and feature_name in self.data_manager.feature_data:
+ return self.data_manager.feature_data[feature_name].get(btn.selected_layer, {})
+ return {}
+
+ def _update_button_with_selection(self, btn, layer_data):
+ """Update button text and store selection data."""
+ layer_name = layer_data.get('layer_name', 'Unknown')
+ btn.setText(layer_name)
+ btn.selected_layer = layer_name
+
+ # Store field information for different types
+ btn.strike_field = layer_data.get('strike_field')
+ btn.dip_field = layer_data.get('dip_field')
+ btn.value_field = layer_data.get('value_field')
+ btn.lower_field = layer_data.get('lower_field')
+ btn.upper_field = layer_data.get('upper_field')
+
+ def _add_layer_to_data_manager(self, layer_data):
+ """Add selected layer data to the data manager."""
+ if not isinstance(layer_data, dict):
+ raise ValueError("layer_data must be a dictionary.")
+ if self.data_manager:
+ feature_name = self.get_feature_name()
+ self.data_manager.update_feature_data(feature_name, layer_data)
+ else:
+ raise RuntimeError("Data manager is not set.")
+
+ def clear_table(self):
+ """Clear all rows from the table and reset feature data."""
+ feature_name = self.get_feature_name()
+ if feature_name and feature_name in self.data_manager.feature_data:
+ self.data_manager.feature_data[feature_name].clear()
+
+ # Clear all table rows
+ self.table.setRowCount(0)
+
+ def get_table_data(self):
+ """Get all table data as a dictionary."""
+ feature_name = self.get_feature_name()
+ if feature_name and feature_name in self.data_manager.feature_data:
+ return self.data_manager.feature_data[feature_name].copy()
+ return {}
+
+ def set_table_data(self, data):
+ """Set table data and restore table state."""
+ feature_name = self.get_feature_name()
+ if not feature_name:
+ return
+
+ # Clear existing table
+ self.clear_table()
+
+ # Update data manager
+ self.initialize_feature_data()
+ self.data_manager.feature_data[feature_name] = data.copy()
+
+ # Restore table state
+ self.restore_table_state()
+
+ def validate_table_state(self):
+ """Validate that table state matches data manager state."""
+ feature_name = self.get_feature_name()
+ if not feature_name or feature_name not in self.data_manager.feature_data:
+ return True
+
+ feature_data = self.data_manager.feature_data[feature_name]
+ table_layers = []
+
+ # Collect layers from table
+ for row in range(self.table.rowCount()):
+ select_btn = self.table.cellWidget(row, 1)
+ if hasattr(select_btn, 'selected_layer'):
+ table_layers.append(select_btn.selected_layer)
+
+ # Compare with data manager
+ data_layers = list(feature_data.keys())
+
+ if set(table_layers) != set(data_layers):
+ print(f"Table state inconsistency detected for feature '{feature_name}':")
+ print(f" Table layers: {table_layers}")
+ print(f" Data layers: {data_layers}")
+ return False
+
+ return True
+
+ def sync_table_with_data(self):
+ """Synchronize table state with data manager state."""
+ if not self.validate_table_state():
+ print("Syncing table with data manager...")
+ self.restore_table_state()
+
+ def get_layer_count(self):
+ """Get the number of layers currently in the table."""
+ feature_name = self.get_feature_name()
+ if feature_name and feature_name in self.data_manager.feature_data:
+ return len(self.data_manager.feature_data[feature_name])
+ return 0
+
+ def has_layers(self):
+ """Check if there are any layers in the table."""
+ return self.get_layer_count() > 0
+
+ def get_layer_names(self):
+ """Get a list of all layer names in the table."""
+ feature_name = self.get_feature_name()
+ if feature_name and feature_name in self.data_manager.feature_data:
+ return list(self.data_manager.feature_data[feature_name].keys())
+ return []
+
+ def get_table_widget(self):
+ """Get the internal table widget for direct access if needed."""
+ return self.table
+
+ def get_add_button(self):
+ """Get the add button widget for customization if needed."""
+ return self.add_button
+
+ def set_add_button_text(self, text):
+ """Set the text of the add button."""
+ self.add_button.setText(text)
+
+ def set_table_headers(self, headers):
+ """Set custom table headers."""
+ if len(headers) == 3:
+ self.table.setHorizontalHeaderLabels(headers)
+ else:
+ raise ValueError("Headers list must contain exactly 3 items")
+
+ def set_add_button_enabled(self, enabled):
+ """Enable or disable the add button."""
+ self.add_button.setEnabled(enabled)
+
+ def is_add_button_enabled(self):
+ """Check if the add button is enabled."""
+ return self.add_button.isEnabled()
+
+
+class LayerSelectionDialog(QDialog):
+ """Dialog for selecting layers and configuring their fields."""
+
+ def __init__(self, parent=None, data_manager=None, feature_name="", layer_type="", existing_data=None):
+ super().__init__(parent)
+ self.data_manager = data_manager
+ self.feature_name = feature_name
+ self.layer_type = layer_type
+ self.existing_data = existing_data or {}
+ self.layer_data = {}
+
+ self.setWindowTitle("Select Layer")
+ self._setup_ui()
+
+ def _setup_ui(self):
+ """Setup the dialog UI."""
+ layout = QVBoxLayout(self)
+
+ # Layer selection
+ layer_label = QLabel("Layer:")
+ layout.addWidget(layer_label)
+
+ self.layer_combo = QgsMapLayerComboBox()
+ self.layer_combo.setFilters(
+ QgsMapLayerProxyModel.LineLayer | QgsMapLayerProxyModel.PointLayer
+ )
+ layout.addWidget(self.layer_combo)
+
+ # Set existing layer if available
+ if 'layer' in self.existing_data:
+ self.layer_combo.setLayer(self.existing_data['layer'])
+
+ # Type-specific field selection
+ self._setup_type_specific_fields(layout)
+
+ # Dialog buttons
+ self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
+ layout.addWidget(self.button_box)
+
+ self.button_box.accepted.connect(self._on_accepted)
+ self.button_box.rejected.connect(self.reject)
+
+ # Validation
+ self.layer_combo.layerChanged.connect(self._validate_layer_selection)
+ self._validate_layer_selection()
+
+ def _setup_type_specific_fields(self, layout):
+ """Setup fields specific to the layer type."""
+ self.field_combos = {}
+
+ if self.layer_type == "Orientation":
+ self._setup_orientation_fields(layout)
+ elif self.layer_type == "Value":
+ self._setup_value_fields(layout)
+ elif self.layer_type == "Inequality":
+ self._setup_inequality_fields(layout)
+
+ def _setup_orientation_fields(self, layout):
+ """Setup fields for orientation data type."""
+ field_layout = QHBoxLayout()
+
+ # Format selection
+ self.format_combo = QComboBox()
+ self.format_combo.addItems(["Strike", "Dip Direction"])
+ if 'orientation_format' in self.existing_data:
+ self.format_combo.setCurrentText(self.existing_data['orientation_format'])
+ field_layout.addWidget(self.format_combo)
+
+ # Strike/Dip Direction field
+ self.strike_field_label = QLabel("Strike:")
+ self.strike_field_combo = QgsFieldComboBox()
+ self.strike_field_combo.setLayer(self.layer_combo.currentLayer())
+ if 'strike_field' in self.existing_data:
+ self.strike_field_combo.setField(self.existing_data['strike_field'])
+
+ field_layout.addWidget(self.strike_field_label)
+ field_layout.addWidget(self.strike_field_combo)
+
+ # Dip field
+ dip_field_label = QLabel("Dip:")
+ self.dip_field_combo = QgsFieldComboBox()
+ self.dip_field_combo.setLayer(self.layer_combo.currentLayer())
+ if 'dip_field' in self.existing_data:
+ self.dip_field_combo.setField(self.existing_data['dip_field'])
+
+ field_layout.addWidget(dip_field_label)
+ field_layout.addWidget(self.dip_field_combo)
+
+ layout.addLayout(field_layout)
+
+ # Update strike label based on format
+ def update_strike_label(text):
+ if text == "Dip Direction":
+ self.strike_field_label.setText("Dip Direction:")
+ else:
+ self.strike_field_label.setText("Strike:")
+
+ self.format_combo.currentTextChanged.connect(update_strike_label)
+ self.layer_combo.layerChanged.connect(self.strike_field_combo.setLayer)
+ self.layer_combo.layerChanged.connect(self.dip_field_combo.setLayer)
+
+ self.field_combos = {
+ 'strike_field': self.strike_field_combo,
+ 'dip_field': self.dip_field_combo,
+ 'format': self.format_combo
+ }
+
+ def _setup_value_fields(self, layout):
+ """Setup fields for value data type."""
+ field_layout = QHBoxLayout()
+
+ value_field_label = QLabel("Value Field:")
+ self.value_field_combo = QgsFieldComboBox()
+ self.value_field_combo.setLayer(self.layer_combo.currentLayer())
+ if 'value_field' in self.existing_data:
+ self.value_field_combo.setField(self.existing_data['value_field'])
+
+ field_layout.addWidget(value_field_label)
+ field_layout.addWidget(self.value_field_combo)
+ layout.addLayout(field_layout)
+
+ self.layer_combo.layerChanged.connect(self.value_field_combo.setLayer)
+
+ self.field_combos = {
+ 'value_field': self.value_field_combo
+ }
+
+ def _setup_inequality_fields(self, layout):
+ """Setup fields for inequality data type."""
+ field_layout = QHBoxLayout()
+
+ lower_field_label = QLabel("Lower:")
+ self.lower_field_combo = QgsFieldComboBox()
+ self.lower_field_combo.setLayer(self.layer_combo.currentLayer())
+ if 'lower_field' in self.existing_data:
+ self.lower_field_combo.setField(self.existing_data['lower_field'])
+
+ upper_field_label = QLabel("Upper:")
+ self.upper_field_combo = QgsFieldComboBox()
+ self.upper_field_combo.setLayer(self.layer_combo.currentLayer())
+ if 'upper_field' in self.existing_data:
+ self.upper_field_combo.setField(self.existing_data['upper_field'])
+
+ field_layout.addWidget(lower_field_label)
+ field_layout.addWidget(self.lower_field_combo)
+ field_layout.addWidget(upper_field_label)
+ field_layout.addWidget(self.upper_field_combo)
+ layout.addLayout(field_layout)
+
+ self.layer_combo.layerChanged.connect(self.lower_field_combo.setLayer)
+ self.layer_combo.layerChanged.connect(self.upper_field_combo.setLayer)
+
+ self.field_combos = {
+ 'lower_field': self.lower_field_combo,
+ 'upper_field': self.upper_field_combo
+ }
+
+ def _validate_layer_selection(self):
+ """Validate the current layer selection."""
+ if self.layer_combo.currentLayer() is None:
+ self.button_box.button(QDialogButtonBox.Ok).setEnabled(False)
+ return False
+
+ layer_name = self.layer_combo.currentLayer().name()
+ if layer_name in self.data_manager.feature_data.get(self.feature_name, {}):
+ self.data_manager.logger("Layer already selected.", log_level=2)
+ self.button_box.button(QDialogButtonBox.Ok).setEnabled(False)
+ return False
+
+ self.button_box.button(QDialogButtonBox.Ok).setEnabled(True)
+ return True
+
+ def _on_accepted(self):
+ """Handle dialog acceptance."""
+ if self.layer_combo.currentLayer() is None:
+ return
+
+ self.layer_data = {
+ 'layer': self.layer_combo.currentLayer(),
+ 'layer_name': self.layer_combo.currentLayer().name(),
+ 'type': self.layer_type
+ }
+
+ # Add type-specific data
+ if self.layer_type == "Orientation":
+ if not self.field_combos['strike_field'].currentField() or not self.field_combos['dip_field'].currentField():
+ return
+ self.layer_data['strike_field'] = self.field_combos['strike_field'].currentField()
+ self.layer_data['dip_field'] = self.field_combos['dip_field'].currentField()
+ self.layer_data['orientation_format'] = self.field_combos['format'].currentText()
+
+ elif self.layer_type == "Value":
+ if not self.field_combos['value_field'].currentField():
+ return
+ self.layer_data['value_field'] = self.field_combos['value_field'].currentField()
+
+ elif self.layer_type == "Inequality":
+ if not self.field_combos['lower_field'].currentField() or not self.field_combos['upper_field'].currentField():
+ return
+ self.layer_data['lower_field'] = self.field_combos['lower_field'].currentField()
+ self.layer_data['upper_field'] = self.field_combos['upper_field'].currentField()
+
+ self.accept()
+
+ def get_layer_data(self):
+ """Get the configured layer data."""
+ return self.layer_data
diff --git a/loopstructural/gui/modelling/geological_model_tab/splot.py b/loopstructural/gui/modelling/geological_model_tab/splot.py
new file mode 100644
index 0000000..710bd3f
--- /dev/null
+++ b/loopstructural/gui/modelling/geological_model_tab/splot.py
@@ -0,0 +1,49 @@
+import numpy as np
+import pyqtgraph as pg
+from qgis.PyQt.QtWidgets import (
+ QComboBox,
+ QDialog,
+ QDialogButtonBox,
+ QDoubleSpinBox,
+ QFormLayout,
+ QVBoxLayout,
+ QWidget,
+)
+from LoopStructural.modelling.features import FeatureType
+
+
+class SPlotDialog(QDialog):
+ def __init__(self, parent=None, *, data_manager=None, model_manager=None, feature_name=None):
+ super().__init__(parent)
+ self.data_manager = data_manager
+ self.model_manager = model_manager
+ self.feature_name = feature_name
+ self.setWindowTitle('S-Plot')
+ self.setMinimumWidth(300)
+ feature = self.model_manager.model.get_feature_by_name(self.feature_name)
+ layout = QVBoxLayout()
+
+
+ fold_frame = feature.fold.fold_limb_rotation.fold_frame_coordinate
+ rotation = feature.fold.fold_limb_rotation.rotation_angle
+ # Placeholder scatter plot using pyqtgraph
+ self.plot_widget = pg.PlotWidget()
+
+ self.plot_widget.plot(
+ fold_frame,
+ rotation,
+ pen=None,
+ symbol='o',
+ symbolSize=6,
+ symbolBrush=(100, 150, 255, 200),
+ )
+ self.plot_widget.setLabel('left', 'Fold limb rotation angle (°)')
+ self.plot_widget.setLabel('bottom', 'Fold frame coordinate')
+ layout.addWidget(self.plot_widget)
+
+ button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
+ button_box.accepted.connect(self.accept)
+ button_box.rejected.connect(self.reject)
+ layout.addWidget(button_box)
+
+ self.setLayout(layout)
diff --git a/loopstructural/gui/modelling/model_setup_tab.py b/loopstructural/gui/modelling/model_setup_tab.py
deleted file mode 100644
index e69de29..0000000
diff --git a/loopstructural/gui/modelling/model_setup_tab.ui b/loopstructural/gui/modelling/model_setup_tab.ui
deleted file mode 100644
index 978b1b9..0000000
--- a/loopstructural/gui/modelling/model_setup_tab.ui
+++ /dev/null
@@ -1,136 +0,0 @@
-
-
- Form
-
-
-
- 0
- 0
- 593
- 364
-
-
-
- Form
-
-
-
-
- 0
- 0
- 591
- 361
-
-
-
- -
-
-
-
-
-
- Initialise Model
-
-
-
- -
-
-
- Run
-
-
-
- -
-
-
- Interpolation Parameters
-
-
-
-
- -1
- 19
- 591
- 263
-
-
-
-
-
-
-
- -
-
-
- Qt::Vertical
-
-
-
- 20
- 40
-
-
-
-
- -
-
-
-
-
-
- Number of elements
-
-
-
- -
-
-
- 10000000
-
-
-
- -
-
-
- Value point weight
-
-
-
- -
-
-
- Norm point weight
-
-
-
- -
-
-
- Regularisation
-
-
-
- -
-
-
- 50.000000000000000
-
-
-
- -
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/loopstructural/gui/modelling/modelling_widget.py b/loopstructural/gui/modelling/modelling_widget.py
index 242c6e7..bfd5380 100644
--- a/loopstructural/gui/modelling/modelling_widget.py
+++ b/loopstructural/gui/modelling/modelling_widget.py
@@ -31,7 +31,7 @@ def __init__(
)
self.fault_adjacency_tab_widget = FaultAdjacencyTab(self, data_manager=self.data_manager)
self.geological_model_tab_widget = GeologicalModelTab(
- self, model_manager=self.model_manager
+ self, model_manager=self.model_manager, data_manager=self.data_manager
)
mainLayout = QVBoxLayout(self)
diff --git a/loopstructural/gui/modelling/modelling_widget_back.py b/loopstructural/gui/modelling/modelling_widget_back.py
deleted file mode 100644
index a0773fe..0000000
--- a/loopstructural/gui/modelling/modelling_widget_back.py
+++ /dev/null
@@ -1,1050 +0,0 @@
-import json
-import os
-import random
-
-import numpy as np
-from LoopStructural.utils import random_hex_colour
-from pyvistaqt import QtInteractor
-from qgis.core import (
- QgsEllipse,
- QgsFeature,
- QgsFieldProxyModel,
- QgsMapLayerProxyModel,
- QgsPoint,
- QgsProject,
- QgsVectorLayer,
-)
-from qgis.PyQt import uic
-from qgis.PyQt.QtGui import QColor
-from qgis.PyQt.QtWidgets import (
- QCheckBox,
- QColorDialog,
- QComboBox,
- QDoubleSpinBox,
- QFileDialog,
- QLabel,
- QLineEdit,
- QListWidgetItem,
- QPushButton,
- QWidget,
-)
-
-from ...main import QgsProcessInputData
-from ...main.callableToLayer import callableToLayer
-from ...main.geometry.calculateLineAzimuth import calculateAverageAzimuth
-from ...main.rasterFromModel import callableToRaster
-
-# from .feature_widget import FeatureWidget
-# from LoopStructural.visualisation import Loop3DView
-# from loopstructural.gui.modelling.stratigraphic_column import StratigraphicColumnWidget
-__title__ = "LoopStructural"
-
-
-class ModellingWidget(QWidget):
- def __init__(self, parent: QWidget = None, mapCanvas=None, logger=None):
- super().__init__(parent)
- uic.loadUi(os.path.join(os.path.dirname(__file__), "modelling_widget.ui"), self)
- self.project = QgsProject.instance()
- self.mapCanvas = mapCanvas
- self.rotationDoubleSpinBox.setValue(mapCanvas.rotation())
- self._set_layer_filters()
- # self.unitNameField.setLayer(self.basalContactsLayer.currentLayer())
- self.logger = logger
- self._basalContacts = None
- self._units = {}
- self._faults = {}
- self._connectSignals()
- self.view = None
- self.model = None
- self.outputPath = ""
- self.activeFeature = None
- self.groups = []
- self.plotter = QtInteractor(parent)
- self.plotter.add_axes()
- self.pyvista_layout.addWidget(self.plotter)
- self.loadFromProject()
-
- def setLayerComboBoxFromProject(self, comboBox: QComboBox, layerKey: str):
- layerName, flag = self.project.readEntry(__title__, layerKey)
- if flag:
-
- layers = self.project.mapLayersByName(layerName)
- print(layerName, layers)
- if len(layers) == 0:
- self.logger(
- message=f"Layer {layerName} not found in project",
- log_level=2,
- push=True,
- )
- return
- comboBox.setLayer(None)
- comboBox.setLayer(layers[0])
-
- def setLayerFieldComboBoxFromProject(
- self, comboBox: QComboBox, fieldKey: str, layer: QgsVectorLayer
- ):
- if layer is None:
- self.logger(message="Layer is None", log_level=2, push=True)
- return
- fieldName, flag = self.project.readEntry(__title__, fieldKey)
- if not flag or not fieldName:
- self.logger(message=f"Field {fieldKey} not found in project", log_level=0, push=True)
- return
- field_names = [field.name() for field in layer.fields()]
- if fieldName not in field_names:
- self.logger(
- message=f"Field {fieldName} not found in layer {layer.name()}",
- log_level=0,
- push=True,
- )
- return
- comboBox.setField(fieldName)
-
- def loadFromProject(self):
- # Load settings from project
- self.setLayerComboBoxFromProject(self.basalContactsLayer, "basal_contacts_layer")
- self.setLayerFieldComboBoxFromProject(
- self.unitNameField, "unitname_field", self.basalContactsLayer.currentLayer()
- )
- self.setLayerComboBoxFromProject(self.structuralDataLayer, "structural_data_layer")
- self.setLayerFieldComboBoxFromProject(
- self.dipField, "dip_field", self.structuralDataLayer.currentLayer()
- )
- self.setLayerFieldComboBoxFromProject(
- self.orientationField, "orientation_field", self.structuralDataLayer.currentLayer()
- )
- self.setLayerFieldComboBoxFromProject(
- self.structuralDataUnitName,
- "structuraldata_unitname_field",
- self.structuralDataLayer.currentLayer(),
- )
- self.setLayerComboBoxFromProject(self.faultTraceLayer, "fault_trace_layer")
- self.setLayerFieldComboBoxFromProject(
- self.faultNameField, "faultname_field", self.faultTraceLayer.currentLayer()
- )
- self.setLayerFieldComboBoxFromProject(
- self.faultDipField, "fault_dip_field", self.faultTraceLayer.currentLayer()
- )
- self.setLayerFieldComboBoxFromProject(
- self.faultDisplacementField,
- "fault_displacement_field",
- self.faultTraceLayer.currentLayer(),
- )
- self.setLayerFieldComboBoxFromProject(
- self.faultPitchField, "fault_pitch_field", self.faultTraceLayer.currentLayer()
- )
- self.setLayerComboBoxFromProject(self.DtmLayer, "dtm_layer")
- self.setLayerComboBoxFromProject(self.roiLayer, "roi_layer")
- label, flag = self.project.readEntry(__title__, "orientation_label", "Strike")
- if flag:
- self.orientationType.setCurrentText(label)
- resp, flag = self.project.readEntry(__title__, "units", "")
- if flag:
- self._units = json.loads(resp)
- if len(self._units) > 0:
- self._initialiseStratigraphicColumn()
- resp, flag = self.project.readEntry(__title__, "faults", "")
- if flag:
- # try:
- self._faults = json.loads(resp)
- self.initFaultNetwork()
- # except:
- # self.logger(message="Faults not loaded", log_level=2, push=True)
-
- def _set_layer_filters(self):
- # Set filters for the layer selection comboboxes
- # basal contacts can be line or points
- self.basalContactsLayer.setFilters(
- QgsMapLayerProxyModel.LineLayer | QgsMapLayerProxyModel.PointLayer
- )
- self.basalContactsLayer.setAllowEmptyLayer(True)
- # Structural data can only be points
- self.structuralDataLayer.setFilters(QgsMapLayerProxyModel.PointLayer)
- self.basalContactsLayer.setAllowEmptyLayer(True)
- # fault traces can be lines or points
- self.faultTraceLayer.setFilters(
- QgsMapLayerProxyModel.LineLayer | QgsMapLayerProxyModel.PointLayer
- )
- self.faultTraceLayer.setAllowEmptyLayer(True)
- # dtm can only be a raster
- self.DtmLayer.setFilters(QgsMapLayerProxyModel.RasterLayer)
- self.DtmLayer.setAllowEmptyLayer(True)
-
- # evaluate on model layer
- self.evaluateModelOnLayerSelector.setFilters(QgsMapLayerProxyModel.PointLayer)
- self.evaluateModelOnLayerSelector.setAllowEmptyLayer(True)
- # evaluate on feature layer
- self.evaluateFeatureLayerSelector.setFilters(QgsMapLayerProxyModel.PointLayer)
- self.evaluateFeatureLayerSelector.setAllowEmptyLayer(True)
- # orientation field can only be double or int
- self.orientationField.setFilters(QgsFieldProxyModel.Numeric)
- self.dipField.setFilters(QgsFieldProxyModel.Numeric)
- # fault dip field can only be double or int
- self.faultDipField.setFilters(QgsFieldProxyModel.Numeric)
- # fault displacement field can only be double or int
- self.faultDisplacementField.setFilters(QgsFieldProxyModel.Numeric)
-
- def saveLayerComboBoxState(self, comboBox: QComboBox, layerKey: str):
- layer = comboBox.currentLayer()
- if layer is not None:
- self.project.writeEntry(__title__, layerKey, layer.name())
-
- def saveLayerFieldComboBoxState(self, comboBox: QComboBox, fieldKey: str):
- field = comboBox.currentField()
- if field is not None:
- self.project.writeEntry(__title__, fieldKey, field)
-
- def saveSettingToProject(self, key: str, value: str):
- self.project.writeEntry(__title__, key, value)
-
- def _connectSignals(self):
- self.basalContactsLayer.layerChanged.connect(self.onBasalContactsChanged)
- self.structuralDataLayer.layerChanged.connect(self.onStructuralDataLayerChanged)
- self.unitNameField.fieldChanged.connect(self.onUnitFieldChanged)
- self.faultTraceLayer.layerChanged.connect(self.onFaultTraceLayerChanged)
- self.faultNameField.fieldChanged.connect(self.onFaultFieldChanged)
- self.faultDipField.fieldChanged.connect(self.onFaultFieldChanged)
- self.faultDisplacementField.fieldChanged.connect(self.onFaultFieldChanged)
- self.orientationType.currentIndexChanged.connect(self.onOrientationTypeChanged)
- self.orientationField.fieldChanged.connect(self.onOrientationFieldChanged)
- self.initModel.clicked.connect(self.onInitialiseModel)
- self.rotationDoubleSpinBox.valueChanged.connect(self.onRotationChanged)
- self.runModelButton.clicked.connect(self.onRunModel)
- self.pathButton.clicked.connect(self.onClickPath)
- self.saveButton.clicked.connect(self.onSaveModel)
- self.path.textChanged.connect(self.onPathTextChanged)
- self.faultSelection.currentIndexChanged.connect(self.onSelectedFaultChanged)
-
- self.basalContactsLayer.layerChanged.connect(
- lambda: self.saveLayerComboBoxState(self.basalContactsLayer, 'basal_contacts_layer')
- )
- self.unitNameField.fieldChanged.connect(
- lambda: self.saveLayerFieldComboBoxState(self.unitNameField, 'unitname_field')
- )
- self.structuralDataLayer.layerChanged.connect(
- lambda: self.saveLayerComboBoxState(self.structuralDataLayer, 'structural_data_layer')
- )
- self.orientationField.fieldChanged.connect(
- lambda: self.saveLayerFieldComboBoxState(self.orientationField, 'orientation_field')
- )
-
- self.dipField.fieldChanged.connect(
- lambda: self.saveLayerFieldComboBoxState(self.dipField, 'dip_field')
- )
- self.roiLayer.layerChanged.connect(
- lambda: self.saveLayerComboBoxState(self.roiLayer, 'roi_layer')
- )
- self.faultTraceLayer.layerChanged.connect(
- lambda: self.saveLayerComboBoxState(self.faultTraceLayer, 'fault_trace_layer')
- )
- self.faultNameField.fieldChanged.connect(
- lambda: self.saveLayerFieldComboBoxState(self.faultNameField, 'faultname_field')
- )
- self.faultDipField.fieldChanged.connect(
- lambda: self.saveLayerFieldComboBoxState(self.faultDipField, 'fault_dip_field')
- )
- self.faultDisplacementField.fieldChanged.connect(
- lambda: self.saveLayerFieldComboBoxState(
- self.faultDisplacementField, 'fault_displacement_field'
- )
- )
- self.faultPitchField.fieldChanged.connect(
- lambda: self.saveLayerFieldComboBoxState(self.faultPitchField, 'fault_pitch_field')
- )
- self.faultDipValue.valueChanged.connect(
- lambda value: self.updateFaultProperty('fault_dip', value)
- )
-
- self.structuralDataUnitName.fieldChanged.connect(
- lambda: self.saveLayerFieldComboBoxState(
- self.structuralDataUnitName, 'structuraldata_unitname_field'
- )
- )
- self.faultPitchValue.valueChanged.connect(
- lambda value: self.updateFaultProperty('fault_pitch', value)
- )
- self.faultDisplacementValue.valueChanged.connect(
- lambda value: self.updateFaultProperty('displacement', value)
- )
- self.faultActiveCheckBox.stateChanged.connect(
- lambda value: self.updateFaultProperty('active', value)
- )
- self.faultMajorAxisLength.valueChanged.connect(
- lambda value: self.updateFaultProperty('major_axis', value)
- )
- self.faultIntermediateAxisLength.valueChanged.connect(
- lambda value: self.updateFaultProperty('intermediate_axis', value)
- )
- self.faultMinorAxisLength.valueChanged.connect(
- lambda value: self.updateFaultProperty('minor_axis', value)
- )
-
- self.orientationType.currentIndexChanged.connect(
- lambda value: self.saveSettingToProject(
- 'orientation_type', self.orientationLabel.text()
- )
- )
-
- # self.faultCentreX.valueChanged.connect(lambda value: self.updateFaultProperty('centre', value))
- # self.faultCentreY.valueChanged.connect(lambda value: self.updateFaultProperty('centre', value))
- # self.faultCentreZ.valueChanged.connect(lambda value: self.updateFaultProperty('centre', value))
- self.addFaultElipseToMap.clicked.connect(self.drawFaultElipse)
- self.addModelContactsToProject.clicked.connect(self.onAddModelContactsToProject)
- self.addFaultDisplacementsToProject.clicked.connect(self.onAddFaultDisplacmentsToProject)
- self.evaluateModelOnLayer.clicked.connect(self.onEvaluateModelOnLayer)
- self.evaluateFeatureOnLayer.clicked.connect(self.onEvaluateFeatureOnLayer)
- self.addMappedLithologiesToProject.clicked.connect(self.onAddModelledLithologiesToProject)
- self.addFaultTracesToProject.clicked.connect(self.onAddFaultTracesToProject)
- self.addScalarFieldToProject.clicked.connect(self.onAddScalarFieldToProject)
- # self.saveThicknessOrderButton.clicked.connect(self.saveThicknessOrder)
- self.addUnitButton.clicked.connect(self.addUnitToStratigraphicColumn)
- self.addBlockModelToPyvistaButton.clicked.connect(self.addBlockModelToPyvista)
- self.clearPyvistaButton.clicked.connect(self.clearPyvista)
- self.addSurfacesToPyvistaButton.clicked.connect(self.addModelSurfacesToPyvista)
- self.addDataToPyvistaButton.clicked.connect(self.addDataToPyvista)
- QgsProject.instance().readProject.connect(self.loadFromProject)
-
- def onModelListItemClicked(self, feature):
- self.activeFeature = self.model[feature.text()]
- self.numberOfElementsSpinBox.setValue(
- self.activeFeature.builder.build_arguments['nelements']
- )
- self.numberOfElementsSpinBox.valueChanged.connect(
- lambda nelements: self.activeFeature.builder.update_build_arguments(
- {'nelements': nelements}
- )
- )
- self.regularisationSpin.setValue(
- self.activeFeature.builder.build_arguments['regularisation']
- )
- self.regularisationSpin.valueChanged.connect(
- lambda regularisation: self.activeFeature.builder.update_builupdate_build_argumentsd_args(
- {'regularisation': regularisation}
- )
- )
- self.npwSpin.setValue(self.activeFeature.builder.build_arguments['npw'])
- self.npwSpin.valueChanged.connect(
- lambda npw: self.activeFeature.builder.update_build_arguments({'npw': npw})
- )
- self.cpwSpin.setValue(self.activeFeature.builder.build_arguments['cpw'])
- self.cpwSpin.valueChanged.connect(
- lambda cpw: self.activeFeature.builder.update_build_arguments({'cpw': cpw})
- )
- # self.updateButton.clicked.connect(lambda : feature.builder.update())
-
- def onInitialiseModel(self):
-
- columnmap = {
- 'unitname': self.unitNameField.currentField(),
- 'faultname': self.faultNameField.currentField(),
- 'dip': self.dipField.currentField(),
- 'orientation': self.orientationField.currentField(),
- 'structure_unitname': self.structuralDataUnitName.currentField(),
- # 'pitch': self.faultPitchField.currentField()
- }
- faultNetwork = np.zeros((len(self._faults), len(self._faults)))
- for i in range(len(self._faults)):
- for j in range(len(self._faults)):
- if i != j:
- item = self.faultNetworkTable.cellWidget(i, j)
- if item.currentText() == 'Abuts':
- faultNetwork[i, j] = 1
- elif item.currentText() == 'Cuts':
- faultNetwork[i, j] = -1
- faultStratigraphy = np.zeros((len(self._faults), len(self.groups)))
- for i in range(len(self._faults)):
- for j in range(len(self.groups)):
- item = self.faultStratigraphyTable.cellWidget(i, j)
- faultStratigraphy[i, j] = item.isChecked()
-
- processor = QgsProcessInputData(
- basal_contacts=self.basalContactsLayer.currentLayer(),
- groups=self.groups,
- fault_trace=self.faultTraceLayer.currentLayer(),
- fault_properties=self._faults,
- structural_data=self.structuralDataLayer.currentLayer(),
- dtm=self.DtmLayer.currentLayer(),
- columnmap=columnmap,
- roi=self.roiLayer.currentLayer(),
- top=self.heightSpinBox.value(),
- bottom=self.depthSpinBox.value(),
- dip_direction=self.orientationType.currentIndex() == 1,
- rotation=self.rotationDoubleSpinBox.value(),
- faultNetwork=faultNetwork,
- faultStratigraphy=faultStratigraphy,
- faultlist=list(self._faults.keys()),
- )
- self.processor = processor
- self.model = processor.get_model()
- self.logger(message="Model initialised", log_level=0, push=True)
- self.modelList.clear()
- for feature in self.model.features:
- if feature.name[0] == '_':
- continue
- item = QListWidgetItem()
- item.setText(feature.name)
- item.setBackground(
- QColor(random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
- )
-
- self.modelList.addItem(item)
- self.modelList.itemClicked.connect(self.onModelListItemClicked)
- self.plotter.add_mesh(self.model.bounding_box.vtk().outline())
-
- def onOrientationTypeChanged(self, index):
- if index == 0:
- self.orientationLabel.setText("Strike")
- else:
- self.orientationLabel.setText("Dip Direction")
-
- def onRotationChanged(self, rotation):
- self.mapCanvas.setRotation(rotation)
-
- def onOrientationFieldChanged(self, field):
- pass
-
- def onStructuralDataLayerChanged(self, layer):
- self.orientationField.setLayer(layer)
- self.dipField.setLayer(layer)
- self.structuralDataUnitName.setLayer(layer)
- # self.saveLayersToProject()
- # self.dipField.setValidator(QDoubleValidator(0.0, 360.0, 2))
- # self.orientationField.setValidator(QDoubleValidator(0.0, 360.0, 2))
-
- def onRunModel(self):
- try:
- self.model.update(progressbar=False)
- self._model_updated()
- self.logger(message="Model run", log_level=0, push=True)
-
- except Exception as e:
- self.logger(
- message=str(e),
- log_level=2,
- push=True,
- )
-
- def _model_updated(self):
- self.addScalarFieldComboBox.clear()
- self.evaluateFeatureFeatureSelector.clear()
- for feature in self.model.features:
- ## make sure that private features are not added to the list
- if feature.name[0] != "_":
- self.addScalarFieldComboBox.addItem(feature.name)
- self.evaluateFeatureFeatureSelector.addItem(feature.name)
- self.addScalarFieldComboBox.setCurrentIndex(0)
- self.evaluateFeatureFeatureSelector.setCurrentIndex(0)
-
- def onAddModelContactsToProject(self):
- pass
-
- def onAddFaultDisplacmentsToProject(self):
- pass
-
- def addBlockModelToPyvista(self):
- if self.model is None:
- self.logger(message="Model not initialised", log_level=2, push=True)
- return
- self.plotter.add_mesh(self.model.get_block_model()[0].vtk(), show_scalar_bar=False)
-
- def addModelSurfacesToPyvista(self):
- if self.model is None:
- self.logger(message="Model not initialised", log_level=2, push=True)
- return
- surfaces = self.model.get_stratigraphic_surfaces()
- for surface in surfaces:
- self.plotter.add_mesh(surface.vtk(), show_scalar_bar=False, color=surface.colour)
- fault_surfaces = self.model.get_fault_surfaces()
- for surface in fault_surfaces:
- self.plotter.add_mesh(surface.vtk(), show_scalar_bar=False, color='black')
-
- def addDataToPyvista(self):
- if self.model is None:
- self.logger(message="Model not initialised", log_level=2, push=True)
- return
- for f in self.model.features:
- if f.name[0] != "_":
- for d in f.get_data():
- self.plotter.add_mesh(d.vtk(), show_scalar_bar=False)
-
- def clearPyvista(self):
- self.plotter.clear()
- if self.model is not None:
- self.plotter.add_mesh(self.model.bounding_box.vtk().outline())
-
- def onEvaluateModelOnLayer(self):
- layer = self.evaluateModelOnLayerSelector.currentLayer()
-
- callableToLayer(
- lambda xyz: self.model.evaluate_model(xyz),
- layer,
- self.DtmLayer.currentLayer(),
- 'unit_id',
- )
-
- def onEvaluateFeatureOnLayer(self):
- feature_name = self.evaluateFeatureFeatureSelector.currentText()
- layer = self.evaluateFeatureLayerSelector.currentLayer()
- callableToLayer(
- lambda xyz: self.model.evaluate_feature_value(feature_name, xyz),
- layer,
- self.DtmLayer.currentLayer(),
- feature_name,
- )
- pass
-
- def onAddModelledLithologiesToProject(self):
- if self.model is None:
- self.logger(message="Model not initialised", log_level=2, push=True)
- return
- bounding_box = self.model.bounding_box
- feature_layer = callableToRaster(
- lambda xyz: self.model.evaluate_model(xyz),
- dtm=self.DtmLayer.currentLayer(),
- bounding_box=bounding_box,
- crs=QgsProject.instance().crs(),
- layer_name='modelled_lithologies',
- )
- if feature_layer.isValid():
- QgsProject.instance().addMapLayer(feature_layer)
- else:
- self.logger(message="Failed to add scalar field to project", log_level=2, push=True)
- pass
-
- def onAddFaultTracesToProject(self):
- pass
-
- def onAddScalarFieldToProject(self):
- feature_name = self.addScalarFieldComboBox.currentText()
- if self.model is None:
- self.logger(message="Model not initialised", log_level=2, push=True)
- return
- bounding_box = self.model.bounding_box
- feature_layer = callableToRaster(
- lambda xyz: self.model.evaluate_feature_value(feature_name, xyz),
- dtm=self.DtmLayer.currentLayer(),
- bounding_box=bounding_box,
- crs=QgsProject.instance().crs(),
- layer_name=f'{feature_name}_scalar_field',
- )
- if feature_layer.isValid():
- QgsProject.instance().addMapLayer(feature_layer)
- else:
- self.logger(message="Failed to add scalar field to project", log_level=2, push=True)
-
- def onBasalContactsChanged(self, layer):
- self.unitNameField.setLayer(layer)
- # self.saveLayersToProject()
-
- def onFaultTraceLayerChanged(self, layer):
- self.faultNameField.setLayer(layer)
- self.faultDipField.setLayer(layer)
- self.faultDisplacementField.setLayer(layer)
- self._faults = {} # reset faults
- self.onSelectedFaultChanged(-1)
- self.initFaultSelector()
- self.initFaultNetwork()
- # self.saveLayersToProject()
-
- def onUnitFieldChanged(self, field):
- if len(self._units) == 0:
-
- unique_values = set()
- attributes = {}
- layer = self.unitNameField.layer()
- if layer:
- fields = {}
- fields['unitname'] = layer.fields().indexFromName(field)
- if '_ls_th' in [field.name() for field in layer.fields()]:
- fields['thickness'] = layer.fields().indexFromName('_ls_th')
- if '_ls_or' in [field.name() for field in layer.fields()]:
- fields['order'] = layer.fields().indexFromName('_ls_or')
- if '_ls_col' in [field.name() for field in layer.fields()]:
- fields['colour'] = layer.fields().indexFromName('_ls_col')
- field_index = layer.fields().indexFromName(field)
-
- for feature in layer.getFeatures():
- unique_values.add(str(feature[field_index]))
- attributes[str(feature[field_index])] = {}
- for k in fields:
- if feature[fields[k]] is not None:
- attributes[str(feature[field_index])][k] = feature[fields[k]]
-
- colours = random_hex_colour(n=len(unique_values))
- self._units = dict(
- zip(
- list(unique_values),
- [
- {
- 'thickness': (
- attributes[u]['thickness'] if 'thickness' in attributes[u] else 10.0
- ),
- 'order': int(attributes[u]['order']) if 'order' in attributes[u] else i,
- 'name': u,
- 'colour': (
- str(attributes[u]['colour'])
- if 'colour' in attributes[u]
- else colours[i]
- ),
- 'contact': (
- str(attributes[u]['contact'])
- if 'contact' in attributes[u]
- else 'Conformable'
- ),
- }
- for i, u in enumerate(unique_values)
- ],
- )
- )
- self._initialiseStratigraphicColumn()
-
- def initFaultSelector(self):
- self.faultSelection.clear()
- self.resetFaultField()
- if self._faults:
- faults = list(self._faults.keys())
- self.faultSelection.addItems(faults)
-
- def initFaultNetwork(self):
- # faultNetwork
- self.faultNetworkTable.clear()
- self.faultNetworkTable.setRowCount(0)
- self.faultNetworkTable.setColumnCount(0)
- self.faultStratigraphyTable.clear()
- self.faultStratigraphyTable.setRowCount(0)
- self.faultStratigraphyTable.setColumnCount(0)
- if not self._faults:
- return
-
- faults = list(self._faults.keys())
- self.faultNetworkTable.setRowCount(len(faults))
- self.faultNetworkTable.setColumnCount(len(faults))
-
- # Set headers
- self.faultNetworkTable.setHorizontalHeaderLabels(faults)
- self.faultNetworkTable.setVerticalHeaderLabels(faults)
-
- # Fill table with empty items
- for i in range(len(faults)):
- for j in range(len(faults)):
- if i == j:
- flag = QLabel()
- flag.setText('')
- else:
- flag = QComboBox()
- flag.addItem('')
- flag.addItem('Abuts')
- flag.addItem('Cuts')
- # item = QTableWidgetItem(flag)
- self.faultNetworkTable.setCellWidget(i, j, flag)
-
- # Make cells more visible
- self.faultNetworkTable.setShowGrid(True)
- self.faultNetworkTable.resizeColumnsToContents()
- self.faultNetworkTable.resizeRowsToContents()
-
- self.faultStratigraphyTable.clear()
-
- faults = list(self._faults.keys())
- groups = [g['name'] for g in self.groups]
- self.faultStratigraphyTable.setRowCount(len(faults))
- self.faultStratigraphyTable.setColumnCount(len(groups))
-
- # Set headers
- self.faultStratigraphyTable.setHorizontalHeaderLabels(groups)
- self.faultStratigraphyTable.setVerticalHeaderLabels(faults)
-
- # Fill table with empty items
- for j in range(len(groups)):
- for i in range(len(faults)):
- flag = QCheckBox()
- flag.setChecked(True)
- self.faultStratigraphyTable.setCellWidget(i, j, flag)
-
- # Make cells more visible
- self.faultStratigraphyTable.setShowGrid(True)
- self.faultStratigraphyTable.resizeColumnsToContents()
- self.faultStratigraphyTable.resizeRowsToContents()
-
- def onFaultFieldChanged(self, field):
- name_field = self.faultNameField.currentField()
- dip_field = self.faultDipField.currentField()
- displacement_field = self.faultDisplacementField.currentField()
- layer = self.faultNameField.layer()
-
- if name_field and layer:
- self._faults = {}
- for feature in layer.getFeatures():
- self._faults[str(feature[name_field])] = {
- 'fault_dip': feature.attributeMap().get(dip_field, 90),
- 'displacement': feature.attributeMap().get(
- displacement_field, 0.1 * feature.geometry().length()
- ),
- 'fault_centre': {
- 'x': feature.geometry().centroid().asPoint().x(),
- 'y': feature.geometry().centroid().asPoint().y(),
- },
- 'major_axis': feature.geometry().length(),
- 'intermediate_axis': feature.geometry().length(),
- 'minor_axis': feature.geometry().length() / 3,
- 'active': True,
- "azimuth": calculateAverageAzimuth(feature.geometry()),
- "fault_pitch": feature.attributeMap().get('pitch', 90),
- "crs": layer.crs().authid(),
- }
- self.initFaultSelector()
- self.initFaultNetwork()
- # self.saveLayersToProject()
-
- def saveLayersToProject(self):
- if self.basalContactsLayer.currentLayer() is not None:
- self.project.writeEntry(
- __title__, "basal_contacts_layer", self.basalContactsLayer.currentLayer().name()
- )
- if self.structuralDataLayer.currentLayer() is not None:
- self.project.writeEntry(
- __title__, "structural_data_layer", self.structuralDataLayer.currentLayer().name()
- )
- if self.faultTraceLayer.currentLayer() is not None:
- self.project.writeEntry(
- __title__, "fault_trace_layer", self.faultTraceLayer.currentLayer().name()
- )
- if self.DtmLayer.currentLayer() is not None:
- self.project.writeEntry(__title__, "dtm_layer", self.DtmLayer.currentLayer().name())
- if self.roiLayer.currentLayer() is not None:
- self.project.writeEntry(__title__, "roi_layer", self.roiLayer.currentLayer().name())
- if self.unitNameField.currentField() is not None:
- self.project.writeEntry(__title__, "unitname_field", self.unitNameField.currentField())
- if self.dipField.currentField() is not None:
- self.project.writeEntry(__title__, "dip_field", self.dipField.currentField())
- if self.orientationField.currentField() is not None:
- self.project.writeEntry(
- __title__, "orientation_field", self.orientationField.currentField()
- )
- if self.faultNameField.currentField() is not None:
- self.project.writeEntry(
- __title__, "faultname_field", self.faultNameField.currentField()
- )
- if self.faultDipField.currentField() is not None:
- self.project.writeEntry(__title__, "fault_dip_field", self.faultDipField.currentField())
- if self.faultDisplacementField.currentField() is not None:
- self.project.writeEntry(
- __title__, "fault_displacement_field", self.faultDisplacementField.currentField()
- )
- if self.faultPitchField.currentField() is not None:
- self.project.writeEntry(
- __title__, "fault_pitch_field", self.faultPitchField.currentField()
- )
- if self._units:
- self.project.writeEntry(__title__, "units", json.dumps(self._units))
- if self._faults:
- self.project.writeEntry(__title__, "faults", json.dumps(self._faults))
-
- def onSelectedFaultChanged(self, index):
- if index >= 0:
- fault = self.faultSelection.currentText()
- self.faultDipValue.setValue(self._faults[fault]['fault_dip'])
- self.faultPitchValue.setValue(self._faults[fault]['fault_pitch'])
- self.faultDisplacementValue.setValue(self._faults[fault]['displacement'])
- self.faultActiveCheckBox.setChecked(self._faults[fault]['active'])
- self.faultMajorAxisLength.setValue(self._faults[fault]['major_axis'])
- self.faultIntermediateAxisLength.setValue(self._faults[fault]['intermediate_axis'])
- self.faultMinorAxisLength.setValue(self._faults[fault]['minor_axis'])
- self.faultCentreX.setValue(self._faults[fault]['fault_centre']['x'])
- self.faultCentreY.setValue(self._faults[fault]['fault_centre']['y'])
- # self.faultCentreZ.setValue(self._faults[fault]['centre'].z())
- self._onActiveFaultChanged(self._faults[fault]['active'])
-
- def saveFaultsToProject(self):
- if self._faults:
- self.project.writeEntry(__title__, "faults", json.dumps(self._faults))
-
- def saveUnitsToProject(self):
- if self._units:
- self.project.writeEntry(__title__, "units", json.dumps(self._units))
-
- def resetFaultField(self):
- self.faultDipValue.setValue(0)
- self.faultPitchValue.setValue(0)
- self.faultDisplacementValue.setValue(0)
- self.faultActiveCheckBox.setChecked(0)
- self.faultMajorAxisLength.setValue(0)
- self.faultIntermediateAxisLength.setValue(0)
- self.faultMinorAxisLength.setValue(0)
- self.faultCentreX.setValue(0)
- self.faultCentreY.setValue(0)
- # self.faultCentreZ.setValue(self._faults[fault]['centre'].z())
- self._onActiveFaultChanged(False)
-
- def _onActiveFaultChanged(self, value):
- self.faultDipValue.setEnabled(value)
- self.faultPitchValue.setEnabled(value)
- self.faultDisplacementValue.setEnabled(value)
- self.faultMajorAxisLength.setEnabled(value)
- self.faultIntermediateAxisLength.setEnabled(value)
- self.faultMinorAxisLength.setEnabled(value)
- self.faultCentreX.setEnabled(value)
- self.faultCentreY.setEnabled(value)
- # self.faultCentreZ.setEnabled(value)
-
- def updateFaultProperty(self, prop, value):
- fault = self.faultSelection.currentText()
- if fault not in self._faults:
- return
- self._faults[fault][prop] = value
- if prop == 'active':
- self._onActiveFaultChanged(value)
- self.saveFaultsToProject()
-
- def drawFaultElipse(self):
- fault = self.faultSelection.currentText()
- if fault:
- centre = self._faults[fault]['centre']
- major_axis = self._faults[fault]['major_axis']
-
- minor_axis = self._faults[fault]['minor_axis']
- azimuth = self._faults[fault].get('azimuth', 0)
- crs = self._faults[fault].get('crs', 'EPSG:4326')
- # Create an ellipsoid centered at the fault center
- ellipsoid = QgsEllipse(
- QgsPoint(centre.x(), centre.y()), major_axis / 2, minor_axis / 2, azimuth
- )
-
- # Add the ellipsoid to the map canvas
- ellipsoid_layer = QgsVectorLayer(f"Polygon?crs={crs}", f"{fault}: Ellipsoid", "memory")
- ellipsoid_layer_provider = ellipsoid_layer.dataProvider()
- ellipsoid_feature = QgsFeature()
- ellipsoid_feature.setGeometry(ellipsoid.toPolygon())
- ellipsoid_layer_provider.addFeatures([ellipsoid_feature])
-
- QgsProject.instance().addMapLayer(ellipsoid_layer)
-
- def _getSortedStratigraphicColumn(self):
-
- return sorted(self._units.items(), key=lambda x: x[1]['order'])
-
- def _initialiseStratigraphicColumn(self):
- while self.stratigraphicColumnContainer.count():
- child = self.stratigraphicColumnContainer.takeAt(0)
- if child.widget():
- child.widget().deleteLater()
-
- def create_lambda(i, direction):
- return lambda: self.onOrderChanged(i, i + direction)
-
- def create_color_picker(unit):
- def pick_color():
- color = QColorDialog.getColor()
- if color.isValid():
- self._units[unit]['colour'] = color.name()
- self._initialiseStratigraphicColumn()
-
- return pick_color
-
- for i, (unit, value) in enumerate(self._getSortedStratigraphicColumn()):
- # Add stretch factor to first column
-
- label = QLineEdit(unit)
- label.editingFinished.connect(
- lambda unit=unit, label=label: self.stratigraphicColumnUnitNameChanged(
- unit, label.text()
- )
- )
- spin_box = QDoubleSpinBox(maximum=100000, minimum=0)
- spin_box.setValue(value['thickness'])
- order = QLabel()
- order.setText(str(value['order']))
- up = QPushButton("↑")
- down = QPushButton("↓")
- color_picker = QPushButton("Pick Colour")
- # Set background color for the row
- background_color = value.get('colour', "#ffffff")
- label.setStyleSheet(f"background-color: {background_color};")
- spin_box.setStyleSheet(f"background-color: {background_color};")
- order.setStyleSheet(f"background-color: {background_color};")
- up.setStyleSheet(f"background-color: {background_color};")
- down.setStyleSheet(f"background-color: {background_color};")
- color_picker.setStyleSheet(f"background-color: {background_color};")
- self.stratigraphicColumnContainer.addWidget(label, i, 0)
- self.stratigraphicColumnContainer.addWidget(spin_box, i, 1)
- self.stratigraphicColumnContainer.addWidget(up, i, 2)
- self.stratigraphicColumnContainer.addWidget(down, i, 3)
- self.stratigraphicColumnContainer.addWidget(color_picker, i, 4)
- unconformity = QComboBox()
- unconformity.addItem('Conformable')
- unconformity.addItem('Erode')
- unconformity.addItem('Onlap')
- if 'contact' in value:
- unconformity.setCurrentText(value['contact'])
-
- unconformity.currentTextChanged.connect(
- lambda text, unit=unit: self.stratigraphicColumnChanged(text, unit)
- )
-
- self.stratigraphicColumnContainer.addWidget(unconformity, i, 5)
- up.clicked.connect(create_lambda(i, -1))
- down.clicked.connect(create_lambda(i, 1))
- color_picker.clicked.connect(create_color_picker(unit))
- spin_box.valueChanged.connect(
- lambda value, unit=unit: self.onThicknessChanged(unit, value)
- )
- remove_button = QPushButton("Remove")
- remove_button.setStyleSheet(f"background-color: {background_color};")
- remove_button.clicked.connect(
- lambda value, unit=unit: self.stratigraphicColumnRemoveClicked(unit)
- )
- self.stratigraphicColumnContainer.addWidget(remove_button, i, 6)
-
- self.updateGroups()
-
- def stratigraphicColumnChanged(self, text, unit):
- self._units[unit]['contact'] = text
- self.updateGroups()
- self.saveUnitsToProject()
-
- def stratigraphicColumnRemoveClicked(self, unit):
- if unit in self._units:
- del self._units[unit]
- self._initialiseStratigraphicColumn()
- self.saveUnitsToProject()
-
- def addUnitToStratigraphicColumn(self):
- name = 'New Unit'
- if len(self._units) > 0:
- name = f'New Unit {len(self._units) + 1}'
- colour = random_hex_colour(n=1)[0]
- self._units[name] = {
- 'thickness': 10.0,
- 'order': len(self._units),
- 'name': name,
- 'colour': colour,
- 'contact': 'Conformable',
- }
- self._initialiseStratigraphicColumn()
- self.saveUnitsToProject()
-
- def stratigraphicColumnUnitNameChanged(self, unit, name):
-
- old_name = unit
- if unit == name:
- return
- if unit not in self._units:
- return
- if name in self._units and name != unit:
- self.logger(message="Cannot rename, unit name already exists", log_level=2, push=True)
- return
- unit = self._units[unit]
- unit['name'] = name
- self._units[name] = unit
- del self._units[old_name]
- self._initialiseStratigraphicColumn()
- self.saveUnitsToProject()
-
- def updateGroups(self):
- columns = self._getSortedStratigraphicColumn()
-
- self.groups = []
- group = []
- ii = 0
- for _i, (_unit, value) in enumerate(columns):
- group.append(value)
- if value['contact'] != 'Conformable':
- self.groups.append({'name': f'group_{ii}', 'units': group})
- ii += 1
- group = []
-
- self.groups.append({'name': f'group_{ii}', 'units': group})
- self.initFaultNetwork()
-
- def onOrderChanged(self, old_index, new_index):
- if new_index < 0 or new_index >= len(self._units):
- return
- units = dict(self._units) # update a copy
- for unit, value in self._units.items():
- if value['order'] == old_index:
- units[unit]['order'] = new_index
- elif value['order'] == new_index:
- units[unit]['order'] = old_index
- self._units = units # set to copy
- self._initialiseStratigraphicColumn()
- self.saveUnitsToProject()
-
- def onThicknessChanged(self, unit, value):
- self._units[unit]['thickness'] = value
- self.saveUnitsToProject()
-
- def onSaveModel(self):
- if self.model is None:
- self.logger(message="Cannot save model, model not initialised", log_level=2, push=True)
- return
- try:
-
- fileFormat = self.fileFormatCombo.currentText()
- path = self.path.text() #
- name = self.modelNameLineEdit.text()
- if fileFormat == 'python':
- fileFormat = 'pkl'
- self.model.to_file(os.path.join(path, name + "." + fileFormat))
- with open(os.path.join(path, name + "." + 'py'), 'w') as f:
- f.write("from loopstructural import GeologicalModel\n")
- f.write(f"model = GeologicalModel.from_file('{name + '.' + fileFormat}')\n")
- return
-
- self.model.save(
- filename=os.path.join(path, name + "." + fileFormat),
- block_model=self.blockModelCheckBox.isChecked(),
- stratigraphic_surfaces=self.stratigraphicSurfacesCheckBox.isChecked(),
- fault_surfaces=self.faultSurfacesCheckBox.isChecked(),
- stratigraphic_data=self.stratigraphicDataCheckBox.isChecked(),
- fault_data=self.faultDataCheckBox.isChecked(),
- )
- self.logger(message=f"Model saved to {path}", log_level=0, push=True)
- except Exception as e:
- self.logger(
- message=str(e),
- log_level=2,
- push=True,
- )
-
- def saveThicknessOrder(self):
- pass
- # if self._units is None:
- # self.logger(message="No units found", log_level=2, push=True)
- # return
- # self.project.writeEntry(
- # "LoopStructural", "units", json.dumps(self._units)
- # )
- # layer = self.basalContactsLayer.currentLayer()
- # layer.startEditing()
- # field_names = ["_ls_th", "_ls_or", "_ls_col"]
- # field_types = [QVariant.Double, QVariant.Int, QVariant.String]
- # for field_name, field_type in zip(field_names, field_types):
-
- # if field_name not in [field.name() for field in layer.fields()]:
- # layer.dataProvider().addAttributes([QgsField(field_name, field_type)])
- # layer.updateFields()
- # for unit, value in self._units.items():
- # for feature in layer.getFeatures():
- # if feature.attributeMap().get(self.unitNameField.currentField()) == unit:
- # feature[field_names[0]] = value['thickness']
- # feature[field_names[1]] = value['order']
- # feature[field_names[2]] = value['colour']
- # layer.updateFeature(feature)
- # layer.commitChanges()
- # layer.updateFields()
- # self.logger(
- # message=f"Thickness, colour and order saved to {layer.name()}", log_level=0, push=True
- # )
-
- def onPathTextChanged(self, text):
- self.outputPath = text
-
- def onClickPath(self):
- self.outputPath = QFileDialog.getExistingDirectory(None, "Select output path for model")
-
- self.path.setText(self.outputPath)
- # if self.path:
- # if os.path.exists(self.gridDirectory):
- # self.output_directory = os.path.split(
- # self.dlg.lineEdit_gridOutputDir.text()
- # )[-1]
diff --git a/loopstructural/gui/modelling/stratigraphic_column/stratigraphic_column.py b/loopstructural/gui/modelling/stratigraphic_column/stratigraphic_column.py
index c7617a3..f7383a6 100644
--- a/loopstructural/gui/modelling/stratigraphic_column/stratigraphic_column.py
+++ b/loopstructural/gui/modelling/stratigraphic_column/stratigraphic_column.py
@@ -67,7 +67,7 @@ def update_display(self):
"""Update the widget display based on the data manager's stratigraphic column."""
self.unitList.clear()
if self.data_manager and self.data_manager._stratigraphic_column:
- for unit in self.data_manager._stratigraphic_column.order:
+ for unit in reversed(self.data_manager._stratigraphic_column.order):
if unit.element_type == StratigraphicColumnElementType.UNIT:
self.add_unit(unit_data=unit.to_dict(), create_new=False)
elif unit.element_type == StratigraphicColumnElementType.UNCONFORMITY:
diff --git a/loopstructural/gui/visualisation/object_list_widget.py b/loopstructural/gui/visualisation/object_list_widget.py
index 7b4eac9..49bf8e7 100644
--- a/loopstructural/gui/visualisation/object_list_widget.py
+++ b/loopstructural/gui/visualisation/object_list_widget.py
@@ -1,4 +1,3 @@
-import geoh5py
import pyvista as pv
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
@@ -106,6 +105,7 @@ def export_selected_object(self):
# Determine available formats based on object type and dependencies
formats = []
try:
+ import geoh5py # noqa: F401
has_geoh5py = True
except ImportError:
has_geoh5py = False
diff --git a/loopstructural/main/__init__.py b/loopstructural/main/__init__.py
index 5ce165f..e69de29 100644
--- a/loopstructural/main/__init__.py
+++ b/loopstructural/main/__init__.py
@@ -1 +0,0 @@
-from .loopstructuralwrapper import QgsProcessInputData
diff --git a/loopstructural/main/data_manager.py b/loopstructural/main/data_manager.py
index fdfac15..58949c4 100644
--- a/loopstructural/main/data_manager.py
+++ b/loopstructural/main/data_manager.py
@@ -1,4 +1,5 @@
import json
+from collections import defaultdict
import numpy as np
from qgis.core import QgsPointXY, QgsProject, QgsVectorLayer
@@ -63,6 +64,7 @@ def __init__(self, *, project=None, mapCanvas=None, logger=None):
self.dem_layer = None
self.use_dem = True
self.dem_callback = None
+ self.feature_data = defaultdict(dict)
def onSaveProject(self):
"""Save project data."""
@@ -345,7 +347,7 @@ def update_stratigraphy(self):
unit_name_field=self._structural_orientations['unitname_field'],
dip_direction=(
True
- if self._structural_orientations['orientation_type'] == "Dip Direction"
+ if self._structural_orientations['orientation_type'] == "Dip Direction/Dip"
else False
),
)
@@ -557,3 +559,27 @@ def find_layer_by_name(self, layer_name):
else:
self.logger(message=f"Layer '{layer_name}' is not a vector layer.", log_level=2)
return None
+
+ def update_feature_data(self, feature_name: str, feature_data: dict):
+ """Update the feature data in the data manager."""
+ if not isinstance(feature_data, dict):
+ raise ValueError("feature_data must be a dictionary.")
+ self.feature_data[feature_name][feature_data['layer_name']] = feature_data
+ self.logger(message=f"Updated feature data for '{feature_name}'.")
+
+ def add_foliation_to_model(self, foliation_name: str, *, folded_feature_name=None):
+ """Add a foliation to the model."""
+ if foliation_name not in self.feature_data:
+ raise ValueError(f"Foliation '{foliation_name}' does not exist in the data manager.")
+ foliation_data = self.feature_data[foliation_name]
+ for layer in foliation_data.values():
+ layer['df'] = qgsLayerToGeoDataFrame(
+ layer['layer']
+ ) # Convert QgsVectorLayer to GeoDataFrame
+ if self._model_manager:
+ self._model_manager.add_foliation(
+ foliation_name, foliation_data, folded_feature_name=folded_feature_name
+ )
+ self.logger(message=f"Added foliation '{foliation_name}' to the model.")
+ else:
+ raise RuntimeError("Model manager is not set.")
diff --git a/loopstructural/main/loopstructuralwrapper.py b/loopstructural/main/loopstructuralwrapper.py
deleted file mode 100644
index 75b742a..0000000
--- a/loopstructural/main/loopstructuralwrapper.py
+++ /dev/null
@@ -1,149 +0,0 @@
-from typing import List
-
-import numpy as np
-import pandas as pd
-from LoopStructural import GeologicalModel
-from LoopStructural.modelling.input import ProcessInputData
-
-from .vectorLayerWrapper import qgsLayerToDataFrame
-
-
-class QgsProcessInputData(ProcessInputData):
- def __init__(
- self,
- basal_contacts,
- groups: List[dict],
- fault_trace,
- fault_properties,
- structural_data,
- dtm,
- columnmap: dict,
- roi,
- top: float,
- bottom: float,
- dip_direction: bool,
- rotation,
- faultNetwork: np.ndarray = None,
- faultStratigraphy: np.ndarray = None,
- faultlist: List[str] = None,
- ):
- i, j = np.where(faultNetwork == 1)
- edges = []
- edgeproperties = []
- for ii, jj in zip(i, j):
- edges.append((faultlist[jj], faultlist[ii]))
- edgeproperties.append({'type': 'abuts'})
-
- contact_locations = qgsLayerToDataFrame(basal_contacts, dtm)
- fault_data = qgsLayerToDataFrame(fault_trace, dtm)
- contact_orientations = qgsLayerToDataFrame(structural_data, dtm)
- thicknesses = {}
- stratigraphic_order = []
- colours = {}
- for g in groups:
- stratigraphic_order.append((g['name'], [u['name'] for u in g['units']]))
- for u in g['units']:
- thicknesses[u['name']] = u['thickness']
- colours[u['name']] = u['colour']
- # for key in stratigraphic_column.keys():
- # thicknesses[key] = stratigraphic_column[key]['thickness']
- # stratigraphic_order = [None] * len(thicknesses)
- # for key in stratigraphic_column.keys():
- # stratigraphic_order[stratigraphic_column[key]['order']] = key
- # print(stratigraphic_column)
- # stratigraphic_order = [('sg', stratigraphic_order)]
- roi_rectangle = roi.extent()
- minx = roi_rectangle.xMinimum()
- maxx = roi_rectangle.xMaximum()
- miny = roi_rectangle.yMinimum()
- maxy = roi_rectangle.yMaximum()
- # transformer = EuclideanTransformation(minx,miny,rotation)
-
- origin = (minx, miny, bottom)
- maximum = (maxx, maxy, top)
- if contact_locations is not None and columnmap['unitname'] in contact_locations:
- contact_locations = contact_locations.rename(columns={columnmap['unitname']: 'name'})[
- ['X', 'Y', 'Z', 'name']
- ]
- else:
- contact_locations = None
- if fault_data is not None and columnmap['faultname'] in fault_data:
- fault_data = fault_data.rename(columns={columnmap['faultname']: 'fault_name'})[
- ['X', 'Y', 'Z', 'fault_name']
- ]
- if np.all(fault_data['fault_name'].isna()):
- raise ValueError('Fault column name is all None. Check the column name')
- else:
- fault_data = None
-
- if (
- contact_orientations is not None
- and columnmap['structure_unitname'] in contact_orientations
- and columnmap['dip'] in contact_orientations
- and columnmap['orientation'] in contact_orientations
- ):
- contact_orientations = contact_orientations.rename(
- columns={
- columnmap['structure_unitname']: 'name',
- columnmap['dip']: 'dip',
- columnmap['orientation']: 'strike',
- }
- )[['X', 'Y', 'Z', 'dip', 'strike', 'name']]
- contact_orientations['dip'] = contact_orientations['dip'].astype(float)
- contact_orientations['strike'] = contact_orientations['strike'].astype(float)
- if np.all(contact_orientations['name'].isna()):
- raise ValueError('Unit column name is all None. Check the column name')
- if np.all(contact_orientations['dip'].isna()):
- raise ValueError('Dip column name is all None. Check the column name')
- if np.all(contact_orientations['strike'].isna()):
- raise ValueError('Strike column name is all None. Check the column name')
- if dip_direction:
- contact_orientations['strike'] = contact_orientations['strike'] - 90
- else:
- contact_orientations = None
- faults = []
- for fault_name in fault_properties.keys():
- fault = fault_properties[fault_name]
- if fault['active']:
- faults.append(
- {
- 'fault_name': fault_name,
- 'fault_dip': fault['fault_dip'],
- 'displacement': fault['displacement'],
- 'major_axis': fault['major_axis'],
- 'intermediate_axis': fault['intermediate_axis'],
- 'minor_axis': fault['minor_axis'],
- 'centreEasting': fault['fault_centre']['x'],
- 'centreNorthing': fault['fault_centre']['y'],
- 'centreElevation': 0, # if fault['centre']fault['centre'].z(),
- 'fault_pitch': fault['fault_pitch'],
- # 'active': fault['active'],
- # 'azimuth': fault['azimuth'],
- # 'crs': fault['crs'],
- }
- )
- fault_properties = None
- if len(faults) > 0:
-
- fault_properties = pd.DataFrame(faults)
- fault_properties['fault_name'] = fault_properties['fault_name'].astype(str)
- fault_properties = fault_properties.set_index('fault_name')
- fault_data['fault_name'] = fault_data['fault_name'].astype(str)
- super().__init__(
- contacts=contact_locations,
- stratigraphic_order=stratigraphic_order,
- thicknesses=thicknesses,
- fault_locations=fault_data,
- contact_orientations=contact_orientations,
- fault_orientations=None,
- fault_properties=fault_properties,
- origin=origin,
- maximum=maximum,
- fault_edges=edges if len(edges) > 0 else None,
- fault_edge_properties=edgeproperties if len(edgeproperties) > 0 else None,
- colours=colours,
- # fault_edges=[(fault,None) for fault in fault_data['fault_name'].unique()],
- )
-
- def get_model(self):
- return GeologicalModel.from_processor(self)
diff --git a/loopstructural/main/model_manager.py b/loopstructural/main/model_manager.py
index e65161e..71eeb3f 100644
--- a/loopstructural/main/model_manager.py
+++ b/loopstructural/main/model_manager.py
@@ -2,12 +2,15 @@
from typing import Callable
import geopandas as gpd
+import numpy as np
import pandas as pd
from LoopStructural import GeologicalModel
from LoopStructural.datatypes import BoundingBox
from LoopStructural.modelling.core.fault_topology import FaultRelationshipType
from LoopStructural.modelling.core.stratigraphic_column import StratigraphicColumn
+from LoopStructural.modelling.features import FeatureType, StructuralFrame
+from LoopStructural.modelling.features.fold import FoldFrame
from loopstructural.toolbelt.preferences import PlgSettingsStructure
@@ -208,8 +211,7 @@ def update_structural_data(
['X', 'Y', 'Z', 'dip', 'strike', 'unit_name']
]
if dip_direction:
- structural_orientations['dip'] = structural_orientations[dip_field]
- structural_orientations['strike'] = structural_orientations[strike_field] - 90
+ structural_orientations['strike'] = structural_orientations['strike'] - 90
for unit_name in structural_orientations['unit_name'].unique():
orientations = structural_orientations.loc[
structural_orientations['unit_name'] == unit_name, ['X', 'Y', 'Z', 'dip', 'strike']
@@ -249,7 +251,7 @@ def update_foliation_features(self):
if 'orientations' in unit_data:
orientations = unit_data['orientations']
if not orientations.empty:
- orientations['val'] = val
+ orientations['val'] = np.nan
orientations['feature_name'] = groupname
data.append(orientations)
@@ -260,7 +262,7 @@ def update_foliation_features(self):
data = pd.concat(data, ignore_index=True)
foliation = self.model.create_and_add_foliation(
groupname,
- series_surface_data=data,
+ data=data,
force_constrained=True,
nelements=PlgSettingsStructure.interpolator_nelements,
npw=PlgSettingsStructure.interpolator_npw,
@@ -299,7 +301,7 @@ def update_fault_features(self):
displacement=displacement,
fault_dip=dip,
fault_pitch=pitch,
- fault_data=data,
+ data=data,
nelements=PlgSettingsStructure.interpolator_nelements,
npw=PlgSettingsStructure.interpolator_npw,
cpw=PlgSettingsStructure.interpolator_cpw,
@@ -344,3 +346,82 @@ def update_model(self):
def features(self):
return self.model.features
+
+ def add_foliation(
+ self,
+ name: str,
+ data: dict,
+ folded_feature_name=None,
+ sampler=AllSampler(),
+ use_z_coordinate=False,
+ ):
+ # for z
+ dfs = []
+ for layer_data in data.values():
+ if layer_data['type'] == 'Orientation':
+ df = sampler(layer_data['df'], self.dem_function, use_z_coordinate)
+ df['strike'] = df[layer_data['strike_field']]
+ df['dip'] = df[layer_data['dip_field']]
+ df['feature_name'] = name
+ dfs.append(df[['X', 'Y', 'Z', 'strike', 'dip', 'feature_name']])
+ elif layer_data['type'] == 'Formline':
+ pass
+ elif layer_data['type'] == 'Value':
+ df = sampler(layer_data['df'], self.dem_function, use_z_coordinate)
+ df['val'] = df[layer_data['value_field']]
+ df['feature_name'] = name
+ dfs.append(df[['X', 'Y', 'Z', 'val', 'feature_name']])
+
+ elif layer_data['type'] == 'Inequality':
+ df = sampler(layer_data['df'], self.dem_function, use_z_coordinate)
+ df['l'] = df[layer_data['lower_field']]
+ df['u'] = df[layer_data['upper_field']]
+ df['feature_name'] = name
+ dfs.append(df[['X', 'Y', 'Z', 'l', 'u', 'feature_name']])
+
+ else:
+ raise ValueError(f"Unknown layer type: {layer_data['type']}")
+ self.model.create_and_add_foliation(name, data=pd.concat(dfs, ignore_index=True))
+ # if folded_feature_name is not None:
+ # from LoopStructural.modelling.features._feature_converters import add_fold_to_feature
+
+ # folded_feature = self.model.get_feature_by_name(folded_feature_name)
+ # folded_feature_name = add_fold_to_feature(frame, folded_feature)
+ # self.model[folded_feature_name] = folded_feature
+ for observer in self.observers:
+ observer()
+ def add_unconformity(self, foliation_name: str, value: float, type: FeatureType = FeatureType.UNCONFORMITY):
+ foliation = self.model.get_feature_by_name(foliation_name)
+ if foliation is None:
+ raise ValueError(f"Foliation '{foliation_name}' not found in the model.")
+ if type == FeatureType.UNCONFORMITY:
+ self.model.add_unconformity(foliation, value)
+ elif type == FeatureType.ONLAPUNCONFORMITY:
+ self.model.add_onlap_unconformity(foliation, value)
+
+ def add_fold_to_feature(self, feature_name: str, fold_frame_name: str, fold_weights={}):
+
+ from LoopStructural.modelling.features._feature_converters import add_fold_to_feature
+
+ fold_frame = self.model.get_feature_by_name(fold_frame_name)
+ if isinstance(fold_frame,StructuralFrame):
+ fold_frame = FoldFrame(fold_frame.name,fold_frame.features, None, fold_frame.model)
+ if fold_frame is None:
+ raise ValueError(f"Fold frame '{fold_frame_name}' not found in the model.")
+ feature = self.model.get_feature_by_name(feature_name)
+ if feature is None:
+ raise ValueError(f"Feature '{feature_name}' not found in the model.")
+ folded_feature = add_fold_to_feature(feature, fold_frame)
+ self.model[feature_name] = folded_feature
+
+ def convert_feature_to_structural_frame(self, feature_name: str):
+ from LoopStructural.modelling.features.builders import StructuralFrameBuilder
+
+ builder = self.model.get_feature_by_name(feature_name).builder
+ new_builder = StructuralFrameBuilder.from_feature_builder(builder)
+ self.model[feature_name] = new_builder.frame
+
+ @property
+ def fold_frames(self):
+ """Return the fold frames in the model."""
+ return [f for f in self.model.features if f.type == FeatureType.STRUCTURALFRAME]
diff --git a/loopstructural/main/projectmanager.py b/loopstructural/main/projectmanager.py
deleted file mode 100644
index 955003a..0000000
--- a/loopstructural/main/projectmanager.py
+++ /dev/null
@@ -1,13 +0,0 @@
-class ProjectManager:
- def __init__(self, project, data_manager=None):
- self.project = project
- self.data_manager = data_manager
-
- def load_from_project(self):
- pass
-
- def save_to_project(self):
- pass
-
- def clear_project(self):
- pass
diff --git a/loopstructural/main/rasterFromModel.py b/loopstructural/main/rasterFromModel.py
deleted file mode 100644
index 1731885..0000000
--- a/loopstructural/main/rasterFromModel.py
+++ /dev/null
@@ -1,66 +0,0 @@
-import os
-import tempfile
-import uuid
-
-import numpy as np
-from osgeo import gdal, osr
-from qgis.core import QgsRasterLayer
-
-from .geometry.mapGrid import createGrid
-
-
-def callableToRaster(callable, dtm, bounding_box, crs, layer_name):
- """Convert a feature to a raster and store it in QGIS as a temporary layer.
-
- :param feature: The object that has an `evaluate_value` method for computing values.
- :param dtm: Digital terrain model (if needed for processing).
- :param bounding_box: Object with `origin`, `maximum`, `step_vector`, and `nsteps`.
- :param crs: Coordinate reference system (QGIS CRS object).
- """
- # Create grid of points based on bounding_box
- points = createGrid(bounding_box, dtm) # This function should return a list of coordinates
-
- # Evaluate feature at each point
- values = callable(points)
- # Reshape values into a 2D NumPy array (Fortran order)
- values = np.array(values).reshape((bounding_box.nsteps[1], bounding_box.nsteps[0]), order='F')
-
- # Define raster metadata
- rows, cols = values.shape
- values = np.flipud(values) # Flip array vertically to match raster orientation
- geotransform = [
- bounding_box.global_origin[0], # x_min
- bounding_box.step_vector[0], # Pixel width (step in X)
- 0, # No rotation (affine transform)
- bounding_box.global_origin[1]
- + bounding_box.step_vector[1] * bounding_box.nsteps[1], # y_min (origin at bottom-left)
- 0, # No rotation
- -bounding_box.step_vector[1], # Pixel height (negative so origin is bottom-left)
- ]
-
- # Create an in-memory raster using `/vsimem/`
- temp_dir = tempfile.gettempdir()
- temp_raster_path = os.path.join(temp_dir, f'temp_raster_{uuid.uuid4().hex}.tif')
- driver = gdal.GetDriverByName("GTiff")
- ds = driver.Create(temp_raster_path, cols, rows, 1, gdal.GDT_Float32)
-
- # Set georeferencing
- ds.SetGeoTransform(geotransform)
- srs = osr.SpatialReference()
- srs.ImportFromWkt(crs.toWkt()) # Convert QGIS CRS to GDAL WKT
- ds.SetProjection(srs.ExportToWkt())
-
- # Write data to raster band
- band = ds.GetRasterBand(1)
- band.WriteArray(values)
- band.SetNoDataValue(-9999) # Optional: Set NoData value
- band.FlushCache()
-
- # Close dataset
- ds = None
-
- # Load raster into QGIS as a temporary layer
- temp_layer = QgsRasterLayer(temp_raster_path, layer_name, "gdal")
- temp_layer.setCustomProperty("temporary", True)
-
- return temp_layer
diff --git a/loopstructural/main/stratigraphic_column.py b/loopstructural/main/stratigraphic_column.py
deleted file mode 100644
index 903c785..0000000
--- a/loopstructural/main/stratigraphic_column.py
+++ /dev/null
@@ -1,304 +0,0 @@
-import enum
-from typing import Dict
-
-
-class UnconformityType(enum.Enum):
- """
- An enumeration for different types of unconformities in a stratigraphic column.
- """
-
- ERODE = 'erode'
- ONLAP = 'onlap'
-
-
-class StratigraphicColumnElementType(enum.Enum):
- """
- An enumeration for different types of elements in a stratigraphic column.
- """
-
- UNIT = 'unit'
- UNCONFORMITY = 'unconformity'
-
-
-class StratigraphicColumnElement:
- """
- A class to represent an element in a stratigraphic column, which can be a unit or a topological object
- for example unconformity.
- """
-
- def __init__(self, uuid=None):
- """
- Initializes the StratigraphicColumnElement with a name and an optional description.
- """
- if uuid is None:
- import uuid as uuid_module
-
- uuid = str(uuid_module.uuid4())
- self.uuid = uuid
-
-
-class StratigraphicUnit(StratigraphicColumnElement):
- """
- A class to represent a stratigraphic unit, which is a distinct layer of rock with specific characteristics.
- """
-
- def __init__(self, *, uuid=None, name=None, colour=None, thickness=None):
- """
- Initializes the StratigraphicUnit with a name and an optional description.
- """
- super().__init__(uuid)
- self.name = name
- self.colour = colour
- self.thickness = thickness
- self.element_type = StratigraphicColumnElementType.UNIT
-
- def to_dict(self):
- """
- Converts the stratigraphic unit to a dictionary representation.
- """
- return {"name": self.name, "colour": self.colour, "thickness": self.thickness}
-
- @classmethod
- def from_dict(cls, data):
- """
- Creates a StratigraphicUnit from a dictionary representation.
- """
- if not isinstance(data, dict):
- raise TypeError("Data must be a dictionary")
- name = data.get("name")
- colour = data.get("colour")
- thickness = data.get("thickness", None)
- uuid = data.get("uuid", None)
- return cls(uuid=uuid, name=name, colour=colour, thickness=thickness)
-
- def __str__(self):
- """
- Returns a string representation of the stratigraphic unit.
- """
- return (
- f"StratigraphicUnit(name={self.name}, colour={self.colour}, thickness={self.thickness})"
- )
-
-
-class StratigraphicUnconformity(StratigraphicColumnElement):
- """
- A class to represent a stratigraphic unconformity, which is a surface of discontinuity in the stratigraphic record.
- """
-
- def __init__(
- self, *, uuid=None, name=None, unconformity_type: UnconformityType = UnconformityType.ERODE
- ):
- """
- Initializes the StratigraphicUnconformity with a name and an optional description.
- """
- super().__init__(uuid)
- self.name = name
- if unconformity_type not in [UnconformityType.ERODE, UnconformityType.ONLAP]:
- raise ValueError("Invalid unconformity type")
- self.unconformity_type = unconformity_type
- self.element_type = StratigraphicColumnElementType.UNCONFORMITY
-
- def to_dict(self):
- """
- Converts the stratigraphic unconformity to a dictionary representation.
- """
- return {
- "uuid": self.uuid,
- "name": self.name,
- "unconformity_type": self.unconformity_type.value,
- }
-
- def __str__(self):
- """
- Returns a string representation of the stratigraphic unconformity.
- """
- return (
- f"StratigraphicUnconformity(name={self.name}, "
- f"unconformity_type={self.unconformity_type.value})"
- )
-
- @classmethod
- def from_dict(cls, data):
- """
- Creates a StratigraphicUnconformity from a dictionary representation.
- """
- if not isinstance(data, dict):
- raise TypeError("Data must be a dictionary")
- name = data.get("name")
- unconformity_type = UnconformityType(
- data.get("unconformity_type", UnconformityType.ERODE.value)
- )
- uuid = data.get("uuid", None)
- return cls(uuid=uuid, name=name, unconformity_type=unconformity_type)
-
-
-class StratigraphicColumn:
- """
- A class to represent a stratigraphic column, which is a vertical section of the Earth's crust
- showing the sequence of rock layers and their relationships.
- """
-
- def __init__(self):
- """
- Initializes the StratigraphicColumn with a name and a list of layers.
- """
- self.order = []
-
- def add_unit(self, name, colour, thickness=None):
- unit = StratigraphicUnit(name=name, colour=colour, thickness=thickness)
-
- self.order.append(unit)
- return unit
-
- def remove_unit(self, uuid):
- """
- Removes a unit or unconformity from the stratigraphic column by its uuid.
- """
- for i, element in enumerate(self.order):
- if element.uuid == uuid:
- del self.order[i]
- return True
- return False
-
- def add_unconformity(self, name, unconformity_type=UnconformityType.ERODE):
- unconformity = StratigraphicUnconformity(
- uuid=None, name=name, unconformity_type=unconformity_type
- )
-
- self.order.append(unconformity)
- return unconformity
-
- def get_element_by_index(self, index):
- """
- Retrieves an element by its index from the stratigraphic column.
- """
- if index < 0 or index >= len(self.order):
- raise IndexError("Index out of range")
- return self.order[index]
-
- def get_unit_by_name(self, name):
- """
- Retrieves a unit by its name from the stratigraphic column.
- """
- for unit in self.order:
- if isinstance(unit, StratigraphicUnit) and unit.name == name:
- return unit
-
- return None
-
- def add_element(self, element):
- """
- Adds a StratigraphicColumnElement to the stratigraphic column.
- """
- if isinstance(element, StratigraphicColumnElement):
- self.order.append(element)
- else:
- raise TypeError("Element must be an instance of StratigraphicColumnElement")
-
- def get_elements(self):
- """
- Returns a list of all elements in the stratigraphic column.
- """
- return self.order
-
- def get_groups(self):
- groups = []
- group = []
- for e in self.order:
- if isinstance(e, StratigraphicUnit):
- group.append(e)
- else:
- if group:
- groups.append(group)
- group = []
- if group:
- groups.append(group)
- return groups
-
- def get_unitname_groups(self):
- groups = []
- group = []
- for e in self.order:
- if isinstance(e, StratigraphicUnit):
- group.append(e.name)
- else:
- if group:
- groups.append(group)
- group = []
- if group:
- groups.append(group)
- return groups
-
- def __getitem__(self, uuid):
- """
- Retrieves an element by its uuid from the stratigraphic column.
- """
- for element in self.order:
- if element.uuid == uuid:
- return element
- raise KeyError(f"No element found with uuid: {uuid}")
-
- def update_order(self, new_order):
- """
- Updates the order of elements in the stratigraphic column based on a new order list.
- """
- if not isinstance(new_order, list):
- raise TypeError("New order must be a list")
- self.order = [
- self.__getitem__(uuid) for uuid in new_order if self.__getitem__(uuid) is not None
- ]
-
- def update_element(self, unit_data: Dict):
- """
- Updates an existing element in the stratigraphic column with new data.
- :param unit_data: A dictionary containing the updated data for the element.
- """
- if not isinstance(unit_data, dict):
- raise TypeError("unit_data must be a dictionary")
- element = self.__getitem__(unit_data['uuid'])
- if isinstance(element, StratigraphicUnit):
- element.name = unit_data.get('name', element.name)
- element.colour = unit_data.get('colour', element.colour)
- element.thickness = unit_data.get('thickness', element.thickness)
- elif isinstance(element, StratigraphicUnconformity):
- element.name = unit_data.get('name', element.name)
- element.unconformity_type = UnconformityType(
- unit_data.get('unconformity_type', element.unconformity_type.value)
- )
-
- def clear(self):
- """
- Clears the stratigraphic column, removing all elements.
- """
- self.order.clear()
-
- def __str__(self):
- """
- Returns a string representation of the stratigraphic column, listing all elements.
- """
- return "\n".join([f"{i+1}. {element}" for i, element in enumerate(self.order)])
-
- def to_dict(self):
- """
- Converts the stratigraphic column to a dictionary representation.
- """
- return {
- "elements": [element.to_dict() for element in self.order],
- }
-
- @classmethod
- def from_dict(cls, data):
- """
- Creates a StratigraphicColumn from a dictionary representation.
- """
- if not isinstance(data, dict):
- raise TypeError("Data must be a dictionary")
- column = cls()
- elements_data = data.get("elements", [])
- for element_data in elements_data:
- if "unconformity_type" in element_data:
- element = StratigraphicUnconformity.from_dict(element_data)
- else:
- element = StratigraphicUnit.from_dict(element_data)
- column.add_element(element)
- return column
diff --git a/loopstructural/requirements.txt b/loopstructural/requirements.txt
index 1c14ece..550f000 100644
--- a/loopstructural/requirements.txt
+++ b/loopstructural/requirements.txt
@@ -1,5 +1,4 @@
pyvistaqt
pyvista
-LoopStructural==1.6.17
-geoh5py
-meshio
+LoopStructural==1.6.20
+pyqtgraph
\ No newline at end of file