Source code for sec_interp.gui.main_dialog_interpretation

from __future__ import annotations

"""Interpretation management module for SecInterp main dialog.

This module handles interpretation polygons, their persistence, and attribute inheritance,
decoupling this logic from the main dialog class.
"""

import json
from typing import TYPE_CHECKING, Any

from qgis.core import QgsGeometry, QgsPointXY

from sec_interp.core.types import InterpretationPolygon
from sec_interp.logger_config import get_logger, log_critical_operation

if TYPE_CHECKING:
    from .main_dialog import SecInterpDialog

logger = get_logger(__name__)


[docs] class DialogInterpretationManager: """Manages interpretation polygons and their business logic."""
[docs] def __init__(self, dialog: SecInterpDialog): """Initialize interpretation manager. Args: dialog: The main dialog instance. """ self.dialog = dialog self.interpretations: list[InterpretationPolygon] = []
[docs] def load_interpretations(self) -> None: """Load interpretations from the QGIS project.""" if not self.dialog.project: return json_data, ok = self.dialog.project.readEntry("SecInterp", "interpretations", "[]") if not ok or not json_data: return try: data = json.loads(json_data) self.interpretations = [] for item in data: interp = InterpretationPolygon( id=item.get("id", ""), name=item.get("name", ""), type=item.get("type", "lithology"), vertices_2d=[tuple(v) for v in item.get("vertices_2d", [])], attributes=item.get("attributes", {}), color=item.get("color", "#FF0000"), created_at=item.get("created_at", ""), ) self.interpretations.append(interp) logger.info(f"Loaded {len(self.interpretations)} interpretations from project") except Exception: logger.exception("Failed to load interpretations")
[docs] def save_interpretations(self) -> None: """Save interpretations to the QGIS project.""" if not self.dialog.project: return data = [] for interp in self.interpretations: data.append( { "id": interp.id, "name": interp.name, "type": interp.type, "vertices_2d": interp.vertices_2d, "attributes": interp.attributes, "color": interp.color, "created_at": interp.created_at, } ) def json_serial(obj): """JSON serializer for objects not serializable by default json code.""" if hasattr(obj, "isNull"): # Handle QVariant (PyQt5/PyQGIS) if obj.isNull(): return None return obj.value() return str(obj) json_data = json.dumps(data, default=json_serial) self.dialog.project.writeEntry("SecInterp", "interpretations", json_data) logger.debug(f"Saved {len(data)} interpretations to project")
[docs] def handle_interpretation_finished(self, interpretation: InterpretationPolygon) -> None: """Process a finished interpretation polygon. Args: interpretation: The finished interpretation polygon. """ from qgis.PyQt.QtWidgets import QDialog from .dialogs.interpretation_properties_dialog import ( InterpretationPropertiesDialog, ) log_critical_operation( logger, "handle_interpretation_finished", polygon_id=interpretation.id, vertices=len(interpretation.vertices_2d), ) # 1. Prepare for inheritance interp_config = self.dialog.page_interpretation.get_data() # Try to inherit attributes if enabled if interp_config.get("inherit_geology") or interp_config.get("inherit_drillholes"): self.apply_attribute_inheritance(interpretation, interp_config) # 2. Show properties dialog dlg = InterpretationPropertiesDialog( interpretation, interp_config.get("custom_fields"), self.dialog ) if dlg.exec_() != QDialog.Accepted: logger.info(f"Interpretation canceled by user: {interpretation.id}") # Deactivate interpretation tool anyway self.dialog.preview_widget.btn_interpret.setChecked(False) return # Store interpretation self.interpretations.append(interpretation) self.save_interpretations() logger.info( f"Interpretation polygon added: {interpretation.id} " f"({len(interpretation.vertices_2d)} vertices)" ) # Display feedback in results area msg = ( f"<b>{self.dialog.tr('Interpretation Finished')}</b><br>" f"<b>{self.dialog.tr('Name')}:</b> {interpretation.name}<br>" f"<b>{self.dialog.tr('Vertices')}:</b> {len(interpretation.vertices_2d)}<br>" f"<b>ID:</b> {interpretation.id[:8]}..." ) self.dialog.preview_widget.results_text.setHtml(msg) self.dialog.preview_widget.results_group.setCollapsed(False) # Deactivate interpretation tool self.dialog.preview_widget.btn_interpret.setChecked(False) # Update preview to show the new polygon self.dialog.update_preview_from_checkboxes()
[docs] def apply_attribute_inheritance( self, interpretation: InterpretationPolygon, config: dict[str, Any] ) -> None: """Inherit attributes from nearest geology or drillhole data.""" # Use centroid or first vertex as reference point poly_geom = QgsGeometry.fromPolygonXY( [[QgsPointXY(x, y) for x, y in interpretation.vertices_2d]] ) ref_point = poly_geom.centroid().asPoint() best_match = None min_dist = float("inf") # 1. Check Geology Data if config.get("inherit_geology") and self.dialog.preview_manager.cached_data.get("geol"): for segment in self.dialog.preview_manager.cached_data["geol"]: if not segment.points: continue seg_min_dist = float("inf") for p_dist, p_elev in segment.points: d = ref_point.distance(QgsPointXY(p_dist, p_elev)) seg_min_dist = min(d, seg_min_dist) if seg_min_dist < min_dist: min_dist = seg_min_dist best_match = { "name": segment.unit_name, "type": "geology", "attrs": segment.attributes, } # 2. Check Drillhole Data (Intervals) if config.get("inherit_drillholes") and self.dialog.preview_manager.cached_data.get( "drillhole" ): for dh in self.dialog.preview_manager.cached_data["drillhole"]: extracted_intervals = [] if isinstance(dh, tuple): # Drillhole data is a tuple: (hid, trace2d, trace3d, proj3d, geologic_segments) if len(dh) == 5: extracted_intervals = dh[4] elif len(dh) >= 3: # Legacy/Fallback format extracted_intervals = dh[2] elif hasattr(dh, "intervals"): extracted_intervals = dh.intervals if not extracted_intervals: continue for interval in extracted_intervals: # Robust check for points attribute/existence points = getattr(interval, "points", None) if not points: continue int_min_dist = float("inf") for p_dist, p_elev in points: d = ref_point.distance(QgsPointXY(p_dist, p_elev)) int_min_dist = min(d, int_min_dist) if int_min_dist < min_dist: min_dist = int_min_dist unit_name = getattr( interval, "rock_unit", getattr(interval, "unit_name", self.dialog.tr("Unknown")), ) best_match = { "name": unit_name, "type": "drillhole", "attrs": interval.attributes, } if best_match: logger.info(f"Inherited attributes from {best_match['type']}: {best_match['name']}") interpretation.name = best_match["name"] interpretation.type = best_match["type"] if best_match["attrs"]: interpretation.attributes.update(best_match["attrs"]) interpretation.color = self.dialog.layer_factory.get_color_for_unit( best_match["name"] ).name()