Source code for sec_interp.core.types

from __future__ import annotations

"""Core data types and enums for SecInterp."""

from dataclasses import dataclass, field
from enum import IntEnum
from typing import Any

from qgis.core import QgsRasterLayer, QgsVectorLayer

from sec_interp.core.exceptions import ValidationError
from sec_interp.core.performance_metrics import MetricsCollector


[docs] class FieldType(IntEnum): """Core-safe field types mapping to QVariant.Type values. This allows the core module to perform type validation WITHOUT direct dependencies on PyQt components. """ NULL = 0 BOOL = 1 INT = 2 DOUBLE = 6 STRING = 10 LONG_LONG = 4 DATE = 14 DATE_TIME = 16
# Profile data types (Initial aliases) ProfilePoints = list[tuple[float, float]] GeologyPoints = list[tuple[float, float, str]] StructurePoints = list[tuple[float, float]] # Layer collections LayerDict = dict[str, QgsVectorLayer] """Dictionary mapping layer names to QgsVectorLayer objects.""" # Settings and configuration SettingsDict = dict[str, Any] """Dictionary of plugin settings and configuration values.""" ExportSettings = dict[str, Any] """Dictionary of export configuration parameters.""" # Validation results ValidationResult = tuple[bool, str] """Tuple of (is_valid, error_message) from validation functions.""" # Point and Geometry Domain Types Point2D = tuple[float, float] """A 2D point represented as (x, y) or (distance, elevation).""" Point3D = tuple[float, float, float] """A 3D point represented as (x, y, z).""" DomainGeometry = str """A geometry represented in WKT (Well-Known Text) format.""" PointList = list[Point2D] """List of 2D points."""
[docs] @dataclass class StructureMeasurement: """Represents a projected structural measurement on the section plane. Attributes: distance: Horizontal distance from the start of the profile. elevation: Elevation (Z) at the projected point. apparent_dip: Dip angle relative to the section plane. original_dip: True dip measured in the field. original_strike: True strike (azimuth) measured in the field. attributes: Dictionary containing original feature attributes. """ distance: float elevation: float apparent_dip: float original_dip: float original_strike: float attributes: dict[str, Any]
[docs] @dataclass class GeologySegment: """Represents a geological unit segment along the profile. Attributes: unit_name: Name of the geological unit. geometry_wkt: WKT representation of the segment geometry (optional). attributes: Dictionary containing original feature attributes. points: Sampled points (distance, elevation) representing the segment boundary. """ unit_name: str geometry_wkt: DomainGeometry | None attributes: dict[str, Any] points: list[Point2D] points_3d: list[Point3D] = field(default_factory=list) points_3d_projected: list[Point3D] = field(default_factory=list)
[docs] @dataclass class InterpretationPolygon: """Represents a 2D digitized interpretation polygon on the section profile. Attributes: id: Unique identifier for the polygon. name: User-defined name for the interpreted unit/feature. type: Classification (e.g., 'lithology', 'fault', 'alteration'). vertices_2d: List of (distance, elevation) points defining the polygon. attributes: Metadata for the interpretation. color: Visual representation color (HEX). created_at: ISO timestamp of creation. """ id: str name: str type: str vertices_2d: list[tuple[float, float]] attributes: dict[str, Any] = field(default_factory=dict) color: str = "#FF0000" created_at: str = ""
[docs] @dataclass class InterpretationPolygon25D: """Represents a georeferenced 2.5D interpretation geometry (with M coordinates). Attributes: id: Inherited identifier. name: Inherited name. type: Inherited type. geometry_wkt: Domain Geometry in WKT format. attributes: Inherited and calculated attributes. crs_authid: CRS Auth ID (e.g. 'EPSG:4326'). """ id: str name: str type: str geometry_wkt: DomainGeometry attributes: dict[str, Any] crs_authid: str
[docs] @dataclass class GeologyTaskInput: """Data Transfer Object for GeologyGenerationTask. Contains all necessary data to process geological profiles without accessing QGIS layers directly. """ line_geometry_wkt: DomainGeometry line_start_x: float line_start_y: float crs_authid: str master_profile_data: list[Point2D] master_grid_dists: list[tuple[float, Point2D, float]] outcrop_data: list[dict[str, Any]] # List of dicts with 'wkt', 'attrs', 'unit_name' outcrop_name_field: str tolerance: float = 0.001
[docs] @dataclass class DrillholeTaskInput: """Data Transfer Object for DrillholeGenerationTask. Encapsulates all data required to project and process drillholes in a background thread without accessing QGIS API objects. """ # Section Line Info line_geometry_wkt: DomainGeometry line_start_x: float line_start_y: float line_crs_authid: str section_azimuth: float # Parameters buffer_width: float collar_id_field: str use_geometry: bool collar_x_field: str collar_y_field: str collar_z_field: str collar_depth_field: str # Detached Data collar_data: list[dict[str, Any]] # List of dicts with attrs and geometry survey_data: dict[Any, list[tuple[float, float, float]]] # hole_id -> [(depth, azim, incl)] interval_data: dict[Any, list[tuple[float, float, str]]] # hole_id -> [(from, to, lith)] # Optional DEM data for fallback elevation # Since Raster access is not thread safe, we might need to sample beforehand? # Or pass a grid? For simplicity, we assume collars have Z or handled in prepare. # Actually, DrillholeService projects collars which needs Z. # If Z is missing, we need DEM. # Constraint: Thread safe DEM access is tricky. # Solution: Pre-sample collar elevations in 'prepare_task_input' (Main Thread) # and pass 'pre_sampled_z' map. pre_sampled_z: dict[Any, float] = field(default_factory=dict)
# Final type aliases for processed data StructureData = list[StructureMeasurement] GeologyData = list[GeologySegment] ProfileData = list[tuple[float, float]]
[docs] @dataclass class PreviewParams: """Consolidated parameters for profile generation and preview. Attributes: raster_layer: QGIS raster layer for DEM sampling. line_layer: QGIS vector layer for the section orientation. band_num: Raster band number to use for elevation. buffer_dist: Search buffer for projecting data onto the section. outcrop_layer: Optional vector layer with geological outcrops. outcrop_name_field: Field name for geological unit names. struct_layer: Optional vector layer with structural measurements. dip_field: Field name for dip values. strike_field: Field name for strike/azimuth values. dip_scale_factor: Visual scale factor for dip lines. collar_layer: Optional vector layer with drillhole collars. collar_id_field: Field name for drillhole IDs in collar layer. collar_use_geometry: Whether to use layer geometry for collar coordinates. collar_x_field: Field name for X coordinate. collar_y_field: Field name for Y coordinate. collar_z_field: Field name for Z coordinate. collar_depth_field: Field name for total hole depth. survey_layer: Optional vector layer with drillhole surveys. survey_id_field: Field name for drillhole IDs in survey layer. survey_depth_field: Field name for downhole depth in survey. survey_azim_field: Field name for azimuth in survey. survey_incl_field: Field name for inclination in survey. interval_layer: Optional vector layer with drillhole intervals. interval_id_field: Field name for drillhole IDs in interval layer. interval_from_field: Field name for 'from' depth. interval_to_field: Field name for 'to' depth. interval_lith_field: Field name for lithology code/name. max_points: Max number of points for simplified preview (LOD). canvas_width: Width of the preview canvas in pixels. auto_lod: Whether to automatically adjust LOD based on canvas width. """ raster_layer: QgsRasterLayer line_layer: QgsVectorLayer band_num: int buffer_dist: float = 100.0 # Geology params outcrop_layer: QgsVectorLayer | None = None outcrop_name_field: str | None = None # Structure params struct_layer: QgsVectorLayer | None = None dip_field: str | None = None strike_field: str | None = None dip_scale_factor: float = 1.0 # Drillhole params collar_layer: QgsVectorLayer | None = None collar_id_field: str | None = None collar_use_geometry: bool = True collar_x_field: str | None = None collar_y_field: str | None = None collar_z_field: str | None = None collar_depth_field: str | None = None survey_layer: QgsVectorLayer | None = None survey_id_field: str | None = None survey_depth_field: str | None = None survey_azim_field: str | None = None survey_incl_field: str | None = None interval_layer: QgsVectorLayer | None = None interval_id_field: str | None = None interval_from_field: str | None = None interval_to_field: str | None = None interval_lith_field: str | None = None # LOD Params max_points: int = 1000 canvas_width: int = 800 auto_lod: bool = True
[docs] def validate(self) -> None: """Perform native validation of parameters. Raises: ValidationError: If critical parameters are missing or invalid. """ self._validate_core_params() self._validate_geology_params() self._validate_structure_params() self._validate_drillhole_params()
def _validate_core_params(self) -> None: """Validate core raster and line layer parameters.""" if not self.raster_layer or not self.raster_layer.isValid(): raise ValidationError("Raster layer is missing or invalid.") if not self.line_layer or not self.line_layer.isValid(): raise ValidationError("Section line layer is missing or invalid.") if self.band_num < 1: raise ValidationError(f"Invalid band number: {self.band_num}") if self.buffer_dist < 0: raise ValidationError(f"Buffer distance cannot be negative: {self.buffer_dist}") def _validate_geology_params(self) -> None: """Validate geology specific parameters.""" if self.outcrop_layer and self.outcrop_layer.isValid() and not self.outcrop_name_field: raise ValidationError("Outcrop layer selected but no name field provided.") def _validate_structure_params(self) -> None: """Validate structure specific parameters.""" if ( self.struct_layer and self.struct_layer.isValid() and (not self.dip_field or not self.strike_field) ): raise ValidationError("Structural layer selected but dip/strike fields missing.") def _validate_drillhole_params(self) -> None: """Validate drillhole specific parameters.""" if self.collar_layer and self.collar_layer.isValid(): if not self.collar_id_field: raise ValidationError("Collar layer selected but no ID field provided.") self._validate_survey_params() self._validate_interval_params() def _validate_survey_params(self) -> None: """Validate drillhole survey parameters.""" if self.survey_layer and self.survey_layer.isValid(): required = [ self.survey_id_field, self.survey_depth_field, self.survey_azim_field, self.survey_incl_field, ] if not all(required): raise ValidationError("Survey layer selected but some required fields are missing.") def _validate_interval_params(self) -> None: """Validate drillhole interval parameters.""" if self.interval_layer and self.interval_layer.isValid(): required = [ self.interval_id_field, self.interval_from_field, self.interval_to_field, self.interval_lith_field, ] if not all(required): raise ValidationError( "Interval layer selected but some required fields are missing." )
[docs] @dataclass class PreviewResult: """Consolidated result set from profile generation. Attributes: topo: Sampled topographic profile data. geol: List of geological unit segments. struct: List of projected structural measurements. drillhole: Processed drillhole projection data. metrics: Performance metrics collector for the generation cycle. buffer_dist: Buffer distance used for this result. """ topo: ProfileData | None = None geol: GeologyData | None = None struct: StructureData | None = None drillhole: Any | None = None metrics: MetricsCollector = field(default_factory=MetricsCollector) buffer_dist: float = 0.0
[docs] def get_elevation_range(self) -> tuple[float, float]: """Calculate the global minimum and maximum elevation across all layers. Returns: A tuple containing (min_elevation, max_elevation). """ elevations = [] if self.topo: elevations.extend(p[1] for p in self.topo) if self.geol: for segment in self.geol: elevations.extend(p[1] for p in segment.points) if self.struct: elevations.extend(m.elevation for m in self.struct) if self.drillhole: for hole_data in self.drillhole: # drillhole_data is (hole_id, trace_2d, trace_3d, trace_3d_proj, segments) if len(hole_data) >= 5: _, trace, _, _, segments = hole_data else: # Fallback for legacy 3-element tuples if any _, trace, segments = hole_data if trace: elevations.extend(p[1] for p in trace) if segments: for seg in segments: elevations.extend(p[1] for p in seg.points) if not elevations: return 0.0, 0.0 return min(elevations), max(elevations)
[docs] def get_distance_range(self) -> tuple[float, float]: """Calculate the horizontal distance range based on topography. Returns: A tuple containing (min_distance, max_distance). """ if not self.topo: return 0.0, 0.0 return self.topo[0][0], self.topo[-1][0]