Source code for sec_interp.core.utils.geometry_utils.optimization

from __future__ import annotations

"""Geometry optimization utilities for SecInterp preview.

Handles simplification and sampling of geometric data to improve rendering performance.
"""

import math

from qgis.core import QgsGeometry, QgsPointXY

from sec_interp.logger_config import get_logger

logger = get_logger(__name__)


[docs] class PreviewOptimizer: """Provides geometric optimization and sampling logic for preview rendering."""
[docs] @staticmethod def decimate( data: list[tuple[float, float]], tolerance: float | None = None, max_points: int = 1000, ) -> list[tuple[float, float]]: """Decimate line data using Douglas-Peucker algorithm. Args: data: List of (x, y) tuples tolerance: Simplification tolerance (if provided, overrides max_points heuristic) max_points: Maximum points to keep (approximate target if tolerance is None) Returns: Decimated list of (x, y) tuples """ if not data or len(data) <= max_points: return data try: # Create QgsGeometry from points points = [QgsPointXY(x, y) for x, y in data] line = QgsGeometry.fromPolylineXY(points) # Determine tolerance if tolerance is None: # Auto-calculate tolerance based on max_points heuristic extent = line.boundingBox() diag = math.sqrt(extent.width() ** 2 + extent.height() ** 2) calculated_tolerance = diag / max_points else: calculated_tolerance = tolerance # Simplify simplified = line.simplify(calculated_tolerance) # Extract points if simplified.isMultipart(): result_points = simplified.asMultiPolyline()[0] else: result_points = simplified.asPolyline() result = [(p.x(), p.y()) for p in result_points] logger.debug( f"LOD Decimation: {len(data)} -> {len(result)} points " f"(tol={calculated_tolerance:.2f})" ) except Exception as e: logger.warning(f"LOD decimation failed: {e}") return data else: return result
[docs] @staticmethod def calculate_curvature(data: list[tuple[float, float]]) -> list[float]: """Calculate a simple curvature metric for each point in a line. This approximates curvature by the angle deviation between successive segments. High values indicate sharper turns. Args: data: List of (x, y) tuples. Returns: List of curvature values (angles in degrees), same length as data. """ if len(data) < 3: return [0.0] * len(data) curvatures = [0.0] # First point has no preceding segment for i in range(1, len(data) - 1): p_prev = data[i - 1] p_curr = data[i] p_next = data[i + 1] # Vectors for segments v1_x = p_curr[0] - p_prev[0] v1_y = p_curr[1] - p_prev[1] v2_x = p_next[0] - p_curr[0] v2_y = p_next[1] - p_curr[1] # Dot product and magnitudes dot_product = v1_x * v2_x + v1_y * v2_y mag_v1 = math.sqrt(v1_x**2 + v1_y**2) mag_v2 = math.sqrt(v2_x**2 + v2_y**2) if mag_v1 == 0 or mag_v2 == 0: angle = 0.0 else: # Angle between vectors cosine_angle = dot_product / (mag_v1 * mag_v2) # Clamp to avoid NaN from floating point inaccuracies cosine_angle = max(-1.0, min(1.0, cosine_angle)) angle = math.degrees(math.acos(cosine_angle)) # Curvature is the angle deviation between segments curvatures.append(angle) curvatures.append(0.0) # Last point has no succeeding segment return curvatures
[docs] @classmethod def adaptive_sample( cls, data: list[tuple[float, float]], min_tolerance: float = 0.1, max_tolerance: float = 10.0, max_points: int = 1000, ) -> list[tuple[float, float]]: """Adaptively sample data based on local curvature. Args: data: List of (x, y) tuples min_tolerance: Minimum tolerance for high-detail areas max_tolerance: Maximum tolerance for low-detail areas max_points: Maximum points to keep (approximate target) Returns: Adaptively sampled data """ if len(data) <= max_points: return data # Calculate local curvature curvatures = cls.calculate_curvature(data) # Use average curvature to set a general tolerance for the entire line avg_curvature = sum(curvatures) / len(curvatures) # Scale tolerance based on average curvature: # Normalize avg_curvature (assuming max possible is 180) normalized_curvature = avg_curvature / 180.0 # Invert for tolerance: higher curvature means lower tolerance tolerance_factor = 1.0 - normalized_curvature # Linearly interpolate tolerance tolerance = min_tolerance + (max_tolerance - min_tolerance) * tolerance_factor tolerance = max(min_tolerance, min(max_tolerance, tolerance)) logger.debug( f"Adaptive sampling: Avg curvature={avg_curvature:.2f}, " f"calculated tolerance={tolerance:.2f}" ) # Now use the calculated tolerance for decimation return cls.decimate(data, tolerance=tolerance, max_points=max_points)