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
# 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]