Source code for sec_interp.core.utils.sampling

from __future__ import annotations

"""Sampling Utilities Module.

This module provides elevation sampling and profile context preparation tools.
"""

from qgis.core import (
    QgsDistanceArea,
    QgsGeometry,
    QgsPointXY,
    QgsRasterLayer,
    QgsVectorLayer,
)

from sec_interp.logger_config import get_logger

logger = get_logger(__name__)


[docs] def sample_elevation_along_line( geometry: QgsGeometry, raster_layer: QgsRasterLayer, band_number: int, distance_area: QgsDistanceArea, reference_point: QgsPointXY | None = None, interval: float | None = None, ) -> list[QgsPointXY]: """Sample elevation values along a line geometry from a raster layer. Densifies the line at raster resolution and samples the elevation at each vertex. Args: geometry: The line geometry to sample along. raster_layer: The source DEM raster layer. band_number: The raster band index to sample. distance_area: Object for geodesic distance calculations. reference_point: Optional start point for distance measurements. interval: Optional sampling interval. If None, uses raster resolution. Returns: A list of QgsPointXY objects where x is horizontal distance and y is elevation. """ from .geometry import densify_line_by_interval # Use raster resolution if no interval provided if interval is None: interval = raster_layer.rasterUnitsPerPixelX() try: densified_geom = densify_line_by_interval(geometry, interval) except (ValueError, RuntimeError): # Fallback to original geometry if densification fails densified_geom = geometry # Get vertices from densified geometry using helper from .geometry import get_line_vertices vertices = get_line_vertices(densified_geom) points = [] current_dist = 0.0 # Optional: If we have a reference point, calculate its distance to the first vertex if reference_point: current_dist = distance_area.measureLine(reference_point, vertices[0]) # Sample elevation at each vertex for i, pt in enumerate(vertices): # Calculate incremental distance if i > 0: # For densified points, Euclidean distance is sufficient and MUCH faster # than geodesic measureLine calls, especially since they are very close. segment_len = distance_area.measureLine(vertices[i - 1], pt) current_dist += segment_len val, ok = raster_layer.dataProvider().sample(pt, band_number) elev = val if ok else 0.0 points.append(QgsPointXY(current_dist, elev)) return points
[docs] def prepare_profile_context( line_lyr: QgsVectorLayer, ) -> tuple[QgsGeometry, QgsPointXY, QgsDistanceArea]: """Prepare a common context for profile calculation operations. Args: line_lyr: The cross-section line vector layer. Returns: A tuple containing: - line_geom: The geometry of the section line. - line_start: The starting point of the line. - distance_area: Fully configured geodesic distance object. Raises: ValueError: If the input layer is empty or has invalid geometry. """ from .spatial import create_distance_area, get_line_start_point line_feat = next(line_lyr.getFeatures(), None) if not line_feat: raise ValueError("Line layer has no features") line_geom = line_feat.geometry() if not line_geom or line_geom.isNull(): raise ValueError("Line geometry is not valid") line_start = get_line_start_point(line_geom) da = create_distance_area(line_lyr.crs()) return line_geom, line_start, da
[docs] def interpolate_elevation(topo_data: list, distance: float) -> float: """Interpolate elevation at given distance. Args: topo_data: List of (distance, elevation) tuples. distance: Distance at which to interpolate elevation. Returns: The interpolated elevation value. """ if not topo_data: return 0.0 import bisect # Extract distances for bisect distances = [pt[0] for pt in topo_data] # Find the insertion point idx = bisect.bisect_left(distances, distance) if idx == 0: return topo_data[0][1] if idx >= len(topo_data): return topo_data[-1][1] # Interpolate dist1, elev1 = topo_data[idx - 1] dist2, elev2 = topo_data[idx] if dist2 == dist1: return elev1 ratio = (distance - dist1) / (dist2 - dist1) return elev1 + (elev2 - elev1) * ratio