Source code for sec_interp.core.services.export_service

from __future__ import annotations

"""Export service for SecInterp.

This module provides a service to orchestrate all export operations,
including data (Shapefile, CSV) and preview (PNG, PDF, SVG) exports.
"""

from pathlib import Path
from typing import Any

from qgis.core import QgsMapSettings, QgsRectangle

from sec_interp.core.exceptions import DataMissingError, ExportError
from sec_interp.core.services.access_control_service import AccessControlService
from sec_interp.core.types import PreviewParams
from sec_interp.logger_config import get_logger

logger = get_logger(__name__)


[docs] class ExportService: """Service to orchestrate all export operations."""
[docs] def __init__(self, controller: Any | None = None): """Initialize the export service. Args: controller: Optional reference to ProfileController for data access. """ self.controller = controller self.access_control = AccessControlService()
[docs] def export_data( self, output_folder: Path, params: PreviewParams, profile_data: list[tuple], geol_data: list[Any] | None, struct_data: list[Any] | None, drillhole_data: list[Any] | None = None, interp_data: list[Any] | None = None, export_options: dict[str, bool] | None = None, ) -> list[str]: """Export generated data to CSV and Shapefile formats. Args: output_folder: Destination directory for all exported files. params: Correctly validated parameters for the export run. profile_data: Topographic profile points (dist, elevation). geol_data: List of GeologySegment objects. struct_data: List of StructureMeasurement objects. drillhole_data: Optional list of drillhole data. interp_data: Optional list of InterpretationPolygon objects. export_options: Dictionary of flags 'exp_topo', 'exp_geol', etc. """ if export_options is None: # Default to all True if not provided export_options = { "exp_topo": True, "exp_geol": True, "exp_struct": True, "exp_drill": True, "exp_interp": True, } # Ensure we have data to work with if not profile_data: raise DataMissingError("No profile data available for export") line_layer = params.line_layer if not line_layer: raise DataMissingError("Section line layer not found in parameters") line_crs = line_layer.crs() result_msg = ["✓ Saving files..."] from sec_interp.exporters import CSVExporter csv_exporter = CSVExporter({}) # Orchestrate sub-exports # Orchestrate sub-exports if export_options.get("exp_topo", True): self._export_topography(output_folder, profile_data, line_crs, csv_exporter, result_msg) self._export_axes(output_folder, profile_data, line_crs, result_msg) if export_options.get("exp_geol", True): self._export_geology(output_folder, geol_data, line_crs, csv_exporter, result_msg) if export_options.get("exp_struct", True): self._export_structures( output_folder, struct_data, params, line_crs, csv_exporter, result_msg ) if export_options.get("exp_drill", True): self._export_drillholes( output_folder, drillhole_data, line_crs, result_msg, export_options ) if export_options.get("exp_interp", True): self._export_interpretations( output_folder, interp_data, line_layer, line_crs, result_msg ) result_msg.append(f"\n✓ All files saved to:\n{output_folder}") return result_msg
def _export_topography( self, folder: Path, data: list[tuple], crs: Any, csv_exporter: Any, msg: list[str], ) -> None: """Export topographic data.""" from sec_interp.exporters import ProfileLineShpExporter logger.info("✓ Saving topographic profile...") try: csv_exporter.export( folder / "topo_profile.csv", {"headers": ["dist", "elev"], "rows": data} ) ProfileLineShpExporter({}).export( folder / "profile_line.shp", {"profile_data": data, "crs": crs} ) msg.extend([" - topo_profile.csv", " - profile_line.shp"]) except Exception as e: raise ExportError(f"Topography export failed: {e!s}") from e def _export_geology( self, folder: Path, data: list[Any] | None, crs: Any, csv_exporter: Any, msg: list[str], ) -> None: """Export geological data.""" if not data: return from sec_interp.exporters import GeologyShpExporter logger.info("✓ Saving geological profile...") try: rows = [(p[0], p[1], s.unit_name) for s in data for p in s.points] csv_exporter.export( folder / "geol_profile.csv", {"headers": ["dist", "elev", "geology"], "rows": rows}, ) GeologyShpExporter({}).export( folder / "geol_profile.shp", {"geology_data": data, "crs": crs} ) msg.extend([" - geol_profile.csv", " - geol_profile.shp"]) except Exception as e: raise ExportError(f"Geology export failed: {e!s}") from e def _export_structures( self, folder: Path, data: list[Any] | None, params: PreviewParams, crs: Any, csv_exporter: Any, msg: list[str], ) -> None: """Export structural data.""" if not data: return from sec_interp.exporters import StructureShpExporter logger.info("✓ Saving structural profile...") try: rows = [(s.distance, s.apparent_dip) for s in data] csv_exporter.export( folder / "structural_profile.csv", {"headers": ["dist", "apparent_dip"], "rows": rows}, ) raster_res = 1.0 if params.raster_layer: raster_res = params.raster_layer.rasterUnitsPerPixelX() StructureShpExporter({}).export( folder / "structural_profile.shp", { "structural_data": data, "crs": crs, "dip_scale_factor": params.dip_scale_factor, "raster_res": raster_res, }, ) msg.extend([" - structural_profile.csv", " - structural_profile.shp"]) except Exception as e: raise ExportError(f"Structure export failed: {e!s}") from e def _export_drillholes( self, folder: Path, data: list[Any] | None, crs: Any, msg: list[str], options: dict[str, bool] | None = None, ) -> None: """Export drillhole data (2D and optional 3D).""" if not data: return from sec_interp.exporters import ( DrillholeInterval3DExporter, DrillholeIntervalShpExporter, DrillholeTrace3DExporter, DrillholeTraceShpExporter, ) logger.info("✓ Saving drillhole data...") try: # 1. Standard 2D Export DrillholeTraceShpExporter({}).export( folder / "drillhole_traces.shp", {"drillhole_data": data, "crs": crs} ) DrillholeIntervalShpExporter({}).export( folder / "drillhole_intervals.shp", {"drillhole_data": data, "crs": crs} ) msg.extend([" - drillhole_traces.shp", " - drillhole_intervals.shp"]) # 2. Advanced 3D Export if options: # Traces 3D if options.get("drill_3d_traces", False): if options.get("drill_3d_original", True): path = folder / "drillhole_traces_3d_real.shp" DrillholeTrace3DExporter({}).export( path, { "drillhole_data": data, "crs": crs, "use_projected": False, }, ) msg.append(f" - {path.name} (3D Real)") if options.get("drill_3d_projected", False): path = folder / "drillhole_traces_3d_projected.shp" DrillholeTrace3DExporter({}).export( path, {"drillhole_data": data, "crs": crs, "use_projected": True}, ) msg.append(f" - {path.name} (3D Proj)") # Intervals 3D if options.get("drill_3d_intervals", False): if options.get("drill_3d_original", True): path = folder / "drillhole_intervals_3d_real.shp" DrillholeInterval3DExporter({}).export( path, { "drillhole_data": data, "crs": crs, "use_projected": False, }, ) msg.append(f" - {path.name} (3D Real)") if options.get("drill_3d_projected", False): path = folder / "drillhole_intervals_3d_projected.shp" DrillholeInterval3DExporter({}).export( path, {"drillhole_data": data, "crs": crs, "use_projected": True}, ) msg.append(f" - {path.name} (3D Proj)") except Exception as e: raise ExportError(f"Drillhole export failed: {e!s}") from e def _export_interpretations( self, folder: Path, data: list[Any] | None, line_layer: Any, crs: Any, msg: list[str], ) -> None: """Export interpretation data.""" if not data: logger.info("No interpretations provided for export.") return from sec_interp.exporters import Interpretation2DExporter logger.info("✓ Saving interpretation data...") try: # 2D Export (Standard) Interpretation2DExporter({}).export( folder / "interpretations.shp", {"interpretations": data} ) msg.append(" - interpretations.shp") # 3D Export (Restricted Feature) if self.access_control.can_export_3d(): from sec_interp.exporters import Interpretation3DExporter logger.info("✓ Saving 3D interpretation data...") # Get section line geometry if line_layer and line_layer.isValid(): line_geom = next(line_layer.getFeatures()).geometry() Interpretation3DExporter({}).export( folder / "interpretations_3d.shp", { "interpretations": data, "section_line": line_geom, "crs": crs, }, ) msg.append(" - interpretations_3d.shp (3D)") else: logger.warning("Invalid section line layer, skipping 3D export.") else: logger.info("3D Export features are restricted for this user.") except Exception as e: raise ExportError(f"Interpretation export failed: {e!s}") from e def _export_axes(self, folder: Path, data: list[tuple], crs: Any, msg: list[str]) -> None: """Export profile axes.""" from sec_interp.exporters import AxesShpExporter logger.info("✓ Saving profile axes...") try: AxesShpExporter({}).export( folder / "profile_axes.shp", {"profile_data": data, "crs": crs} ) except Exception as e: raise ExportError(f"Profile axes export failed: {e!s}") from e
[docs] def get_map_settings( self, layers: list[Any], extent: QgsRectangle, size: Any | None, background_color: Any, ) -> QgsMapSettings: """Create and configure QgsMapSettings for canvas or image export. Args: layers: List of map layers to be rendered. extent: The spatial extent (bounding box) of the view. size: Optional output size in pixels (QSize). background_color: The background color for the render (QColor). Returns: A configured QgsMapSettings instance ready for rendering. """ map_settings = QgsMapSettings() map_settings.setLayers(layers) map_settings.setExtent(extent) if size is not None: map_settings.setOutputSize(size) map_settings.setBackgroundColor(background_color) return map_settings