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