Source code for sec_interp.core.validation.project_validator

from __future__ import annotations

"""Validation for QGIS project state and layer presence."""

from dataclasses import dataclass

from qgis.core import QgsRasterLayer, QgsVectorLayer, QgsWkbTypes

from .layer_validator import (
    validate_field_exists,
    validate_layer_geometry,
    validate_layer_has_features,
    validate_raster_band,
    validate_structural_requirements,
)
from .path_validator import validate_output_path
from .validation_helpers import ValidationContext, validate_reasonable_ranges

# validate_reasonable_ranges moved to validation_helpers.py


[docs] @dataclass class ValidationParams: """Data container for all parameters that need cross-layer validation.""" raster_layer: QgsRasterLayer | None = None band_number: int | None = None line_layer: QgsVectorLayer | None = None output_path: str = "" scale: float = 1.0 vert_exag: float = 1.0 buffer_dist: float = 0.0 outcrop_layer: QgsVectorLayer | None = None outcrop_field: str | None = None struct_layer: QgsVectorLayer | None = None struct_dip_field: str | None = None struct_strike_field: str | None = None dip_scale_factor: float = 1.0 # Drillhole params collar_layer: QgsVectorLayer | None = None collar_id: str | None = None collar_use_geom: bool = True collar_x: str | None = None collar_y: str | None = None survey_layer: QgsVectorLayer | None = None survey_id: str | None = None survey_depth: str | None = None survey_azim: str | None = None survey_incl: str | None = None interval_layer: QgsVectorLayer | None = None interval_id: str | None = None interval_from: str | None = None interval_to: str | None = None interval_lith: str | None = None
[docs] class ProjectValidator: """Orchestrates validation of project parameters independent of the GUI. Level 2: Business Logic Validation. Uses ValidationContext to accumulate errors and verify cross-field dependencies. """
[docs] @staticmethod def validate_all(params: ValidationParams) -> bool: """Perform a comprehensive validation of all project parameters. Args: params: The parameters to validate. Returns: True if all checks passed. Raises: ValidationError: If any validation check fails (contains list of errors). """ # Level 2 Validation: Business Context Accumulation context = ValidationContext() # Phase 1: Critical Structural Checks (Dependencies) # Assuming DEM and Section are critical for any operation ProjectValidator._validate_dem(params, context) ProjectValidator._validate_section(params, context) ProjectValidator._validate_output(params, context) # Stop here if critical foundations are missing? # For now, we continue to gather as much info as possible unless it crashes. # Phase 2: Numeric Ranges & Business Rules ProjectValidator._validate_numeric_ranges(params, context) ProjectValidator._validate_geology(params, context) ProjectValidator._validate_structural(params, context) # Phase 3: Drillhole dependencies (Complex) ProjectValidator._validate_drillholes(params, context) # Raise accumulated errors context.raise_if_errors() return True
@staticmethod def _validate_dem(params: ValidationParams, context: ValidationContext) -> None: """Validate Raster DEM layer.""" if not params.raster_layer: context.add_error("Raster DEM layer is required", "raster_layer") elif params.band_number is not None: is_valid, error = validate_raster_band(params.raster_layer, params.band_number) if not is_valid: context.add_error(error, "band_number") @staticmethod def _validate_section(params: ValidationParams, context: ValidationContext) -> None: """Validate Cross-section line layer.""" if not params.line_layer: context.add_error("Cross-section line layer is required", "line_layer") return is_valid, error = validate_layer_geometry(params.line_layer, QgsWkbTypes.LineGeometry) if not is_valid: context.add_error(error, "line_layer") is_valid, error = validate_layer_has_features(params.line_layer) if not is_valid: context.add_error(error, "line_layer") @staticmethod def _validate_output(params: ValidationParams, context: ValidationContext) -> None: """Validate output path.""" if not params.output_path: context.add_error("Output directory path is required", "output_path") else: is_valid, error, _ = validate_output_path(params.output_path) if not is_valid: context.add_error(error, "output_path") @staticmethod def _validate_numeric_ranges(params: ValidationParams, context: ValidationContext) -> None: """Validate numeric parameter ranges.""" # Level 1 logic might handle simple types, but business rules (Project Level) check context consistency here. # Warnings are accumulated too. warnings = validate_reasonable_ranges( { "vert_exag": params.vert_exag, "scale": params.scale, "buffer": params.buffer_dist, "dip_scale": params.dip_scale_factor, } ) for warn in warnings: context.add_warning(warn) if params.scale < 1: context.add_error("Scale must be >= 1", "scale") if params.vert_exag < 0.1: context.add_error("Vertical exaggeration must be >= 0.1", "vert_exag") if params.buffer_dist < 0: context.add_error("Buffer distance must be >= 0", "buffer_dist") if params.dip_scale_factor < 0.1: context.add_error("Dip scale factor must be >= 0.1", "dip_scale_factor") @staticmethod def _validate_geology(params: ValidationParams, context: ValidationContext) -> None: """Validate Geology Inputs using Dependency Rules.""" # Rule: If outcrop layer is present, field is required. if params.outcrop_layer: # Check geometry is_valid, error = validate_layer_geometry( params.outcrop_layer, QgsWkbTypes.PolygonGeometry ) if not is_valid: context.add_error(error, "outcrop_layer") # Check features is_valid, error = validate_layer_has_features(params.outcrop_layer) if not is_valid: context.add_error(error, "outcrop_layer") # Check field dependency if not params.outcrop_field: context.add_error( "Geology unit field is required when geology layer is selected", "outcrop_field", ) else: is_valid, error = validate_field_exists(params.outcrop_layer, params.outcrop_field) if not is_valid: context.add_error(error, "outcrop_field") @staticmethod def _validate_structural(params: ValidationParams, context: ValidationContext) -> None: """Validate Structural Inputs.""" if params.struct_layer: is_valid, error = validate_structural_requirements( params.struct_layer, params.struct_layer.name(), params.struct_dip_field, params.struct_strike_field, ) if not is_valid: context.add_error(error, "struct_layer") @staticmethod def _validate_drillholes(params: ValidationParams, context: ValidationContext) -> None: """Validate Drillhole Complex Dependencies (Level 2).""" from sec_interp.core.validation.validation_helpers import ( DependencyRule, validate_dependencies, ) # Helper to simplify rules has_collar = bool(params.collar_layer) has_survey = bool(params.survey_layer) has_interval = bool(params.interval_layer) # Collar Rules if has_collar: if not params.collar_id: context.add_error("Collar ID field is required", "collar_id") if not params.collar_use_geom: if not params.collar_x: context.add_error( "Collar X field is required (when not using geometry)", "collar_x", ) if not params.collar_y: context.add_error( "Collar Y field is required (when not using geometry)", "collar_y", ) # Survey Rules (Depend on Collar effectively for a valid project, but here we validate internal consistency) if has_survey: rules = [ DependencyRule( lambda: True, lambda: bool(params.survey_id), "Survey ID field is required", "survey_id", ), DependencyRule( lambda: True, lambda: bool(params.survey_depth), "Survey Depth field is required", "survey_depth", ), DependencyRule( lambda: True, lambda: bool(params.survey_azim), "Survey Azimuth field is required", "survey_azim", ), DependencyRule( lambda: True, lambda: bool(params.survey_incl), "Survey Inclination field is required", "survey_incl", ), ] validate_dependencies(rules, context) # Interval Rules if has_interval: rules = [ DependencyRule( lambda: True, lambda: bool(params.interval_id), "Interval ID field is required", "interval_id", ), DependencyRule( lambda: True, lambda: bool(params.interval_from), "Interval From field is required", "interval_from", ), DependencyRule( lambda: True, lambda: bool(params.interval_to), "Interval To field is required", "interval_to", ), DependencyRule( lambda: True, lambda: bool(params.interval_lith), "Interval Lithology field is required", "interval_lith", ), ] validate_dependencies(rules, context)
[docs] @staticmethod def validate_preview_requirements(params: ValidationParams) -> bool: """Validate only the minimum requirements needed to generate a preview.""" context = ValidationContext() if not params.raster_layer: context.add_error("Raster DEM layer is required") if not params.line_layer: context.add_error("Cross-section line layer is required") context.raise_if_errors() return True
# Legacy helper methods for GUI checks (kept for backward compatibility with check_completeness methods) # Ideally these should delegate to the context logic, but for simple booleans, direct checks are fine.
[docs] @staticmethod def is_drillhole_complete(params: ValidationParams) -> bool: """Check if required fields are filled if drillhole layers are selected.""" # Basic presence check if not params.collar_layer or not params.collar_id: return False # Internal consistency check context = ValidationContext() ProjectValidator._validate_drillholes(params, context) return not context.has_errors
[docs] @staticmethod def is_dem_complete(params: ValidationParams) -> bool: if not params.raster_layer: return False context = ValidationContext() ProjectValidator._validate_dem(params, context) return not context.has_errors
[docs] @staticmethod def is_geology_complete(params: ValidationParams) -> bool: if not params.outcrop_layer or not params.outcrop_field: return False context = ValidationContext() ProjectValidator._validate_geology(params, context) return not context.has_errors
[docs] @staticmethod def is_structure_complete(params: ValidationParams) -> bool: if not params.struct_layer or not params.struct_dip_field or not params.struct_strike_field: return False context = ValidationContext() ProjectValidator._validate_structural(params, context) return not context.has_errors