Source code for sec_interp.core.validation.layer_validator

from __future__ import annotations

"""Spatial validation for QGIS layers (geometry types, CRS)."""

from qgis.core import (
    QgsMapLayer,
    QgsProject,
    QgsRasterLayer,
    QgsVectorLayer,
    QgsWkbTypes,
)

from sec_interp.core.types import FieldType

from .field_validator import validate_field_exists, validate_field_type


[docs] def validate_layer_exists( layer_name: str | None, ) -> tuple[bool, str, QgsMapLayer | None]: """Validate that a layer with the given name exists in the current QGIS project. Args: layer_name: The name of the layer to search for. Returns: tuple: (is_valid, error_message, layer) - is_valid: True if at least one matching layer was found. - error_message: Error details if no layer was found. - layer: The first matching layer instance if valid, else None. """ if not layer_name: return False, "Layer name is required", None # Prefer lookup by ID if it's a valid ID, otherwise search by name layer = QgsProject.instance().mapLayer(layer_name) if not layer: # Fallback to name search if not an ID for lyr in QgsProject.instance().mapLayers().values(): if lyr.name() == layer_name: layer = lyr break if not layer: return False, f"Layer '{layer_name}' not found in project", None if not layer.isValid(): return False, f"Layer '{layer_name}' is not valid", None return True, "", layer
[docs] def validate_layer_has_features(layer: QgsVectorLayer) -> tuple[bool, str]: """Validate that a vector layer contains at least one feature. Args: layer: The QGIS vector layer to check. Returns: tuple: (is_valid, error_message) - is_valid: True if the layer has features. - error_message: Error details if the layer is empty. """ if not layer: return False, "Layer is None" if not isinstance(layer, QgsVectorLayer): return False, "Layer is not a vector layer" if layer.featureCount() == 0: return False, f"Layer '{layer.name()}' has no features" return True, ""
[docs] def validate_layer_geometry( layer: QgsVectorLayer, expected_type: QgsWkbTypes.GeometryType ) -> tuple[bool, str]: """Validate that a vector layer matches the expected QGIS geometry type. Args: layer: The QGIS vector layer to check. expected_type: The required QgsWkbTypes.GeometryType. Returns: tuple: (is_valid, error_message) - is_valid: True if the geometry type matches. - error_message: Detailed error if types mismatch. """ if not layer: return False, "Layer is None" if not isinstance(layer, QgsVectorLayer): return False, "Layer is not a vector layer" actual_type = QgsWkbTypes.geometryType(layer.wkbType()) if actual_type != expected_type: type_names = { QgsWkbTypes.PointGeometry: "Point", QgsWkbTypes.LineGeometry: "Line", QgsWkbTypes.PolygonGeometry: "Polygon", QgsWkbTypes.UnknownGeometry: "Unknown", QgsWkbTypes.NullGeometry: "Null", } expected_name = type_names.get(expected_type, f"Type {expected_type}") actual_name = type_names.get(actual_type, f"Type {actual_type}") return False, ( f"Geometry type mismatch for layer '{layer.name()}': " f"Found {actual_name}, but expected {expected_name}. " f"Please select a valid {expected_name.lower()} layer." ) return True, ""
[docs] def validate_raster_band(layer: QgsRasterLayer, band_number: int) -> tuple[bool, str]: """Validate that a specified band number exists in the given raster layer. Args: layer: The QGIS raster layer to check. band_number: The 1-based index of the raster band. Returns: tuple: (is_valid, error_message) - is_valid: True if the band exists. - error_message: Error message if the band is out of range. """ if not layer: return False, "Layer is None" if not isinstance(layer, QgsRasterLayer): return False, "Layer is not a raster layer" band_count = layer.bandCount() if band_number < 1 or band_number > band_count: return False, ( f"Band number {band_number} is invalid. Layer '{layer.name()}' has {band_count} band(s)" ) return True, ""
[docs] def validate_structural_requirements( layer: QgsVectorLayer, layer_name: str, dip_field: str | None, strike_field: str | None, ) -> tuple[bool, str]: """Validate structural layer requirements (geometry and attribute fields). Args: layer: The QGIS point layer containing structural data. layer_name: Human-readable name of the layer. dip_field: Name of the attribute field containing dip values. strike_field: Name of the attribute field containing strike values. Returns: tuple: (is_valid, error_message) - is_valid: True if both geometry and fields are valid. - error_message: Detailed error if validation fails. """ if not layer.isValid(): return False, f"Structural layer '{layer_name}' is not valid." # Validate geometry (points) if QgsWkbTypes.geometryType(layer.wkbType()) != QgsWkbTypes.PointGeometry: return False, "Structural layer must be a point layer." if dip_field: is_valid, msg = validate_field_exists(layer, dip_field) if not is_valid: return False, msg is_valid, msg = validate_field_type( layer, dip_field, [FieldType.INT, FieldType.DOUBLE, FieldType.LONG_LONG] ) if not is_valid: return False, f"Dip field error: {msg}" if strike_field: is_valid, msg = validate_field_exists(layer, strike_field) if not is_valid: return False, msg is_valid, msg = validate_field_type( layer, strike_field, [FieldType.INT, FieldType.DOUBLE, FieldType.LONG_LONG] ) if not is_valid: return False, f"Strike field error: {msg}" return True, ""
[docs] def validate_crs_compatibility(layers: list[QgsMapLayer]) -> tuple[bool, str]: """Validate that a list of layers have compatible Coordinate Reference Systems. If layers have different CRSs, it returns a warning message instead of an error. """ valid_layers = [L for L in layers if L and L.isValid()] if not valid_layers: return True, "" ref_layer = valid_layers[0] ref_crs = ref_layer.crs() incompatible = [ f" - {L.name()}: {L.crs().authid()}" for L in valid_layers if L.crs() != ref_crs ] if incompatible: warning = ( f"⚠ CRS mismatch detected!\n\n" f"Reference CRS: {ref_crs.authid()} ({ref_layer.name()})\n" f"Incompatible layers:\n" + "\n".join(incompatible) + "\n\n" "QGIS will reproject on-the-fly, but this may affect accuracy.\n" ) return False, warning return True, ""