from __future__ import annotations
"""Specific exporters for profile data (Shapefiles)."""
import math
from pathlib import Path
from typing import Any
from qgis.core import (
QgsFeature,
QgsField,
QgsFields,
QgsGeometry,
QgsPointXY,
)
from qgis.PyQt.QtCore import QMetaType
from sec_interp.core import utils as scu
from sec_interp.logger_config import get_logger
from .base_exporter import BaseExporter
logger = get_logger(__name__)
[docs]
class ProfileLineShpExporter(BaseExporter):
"""Exports the topographic profile line to a Shapefile."""
[docs]
def get_supported_extensions(self) -> list[str]:
"""Get supported file extensions.
Returns:
List of supported extensions.
"""
return [".shp"]
[docs]
def export(self, output_path: Path, data: dict[str, Any]) -> bool:
"""Export the topographic profile line to a Shapefile.
Args:
output_path: Path to the output Shapefile.
data: Dictionary containing 'profile_data' and 'crs'.
"""
profile_data = data.get("profile_data")
crs = data.get("crs")
if not profile_data or not crs:
return False
try:
points = [QgsPointXY(d, e) for d, e in profile_data]
geom = QgsGeometry.fromPolylineXY(points)
if not geom or geom.isNull():
return False
fields = QgsFields()
fields.append(QgsField("id", QMetaType.Type.Int))
writer = scu.create_shapefile_writer(str(output_path), crs, fields)
feat = QgsFeature()
feat.setGeometry(geom)
feat.setAttributes([1])
writer.addFeature(feat)
except Exception:
logger.exception(f"Failed to export profile line to {output_path}")
return False
else:
return True
[docs]
class GeologyShpExporter(BaseExporter):
"""Exports the geological profile to a Shapefile."""
[docs]
def get_supported_extensions(self) -> list[str]:
"""Get supported file extensions.
Returns:
List of supported extensions.
"""
return [".shp"]
[docs]
def export(self, output_path: Path, data: dict[str, Any]) -> bool:
"""Export the geological profile to a Shapefile.
Args:
output_path: Path to the output Shapefile.
data: Dictionary containing 'geology_data' and 'crs'.
"""
geology_data = data.get("geology_data")
crs = data.get("crs")
if not geology_data or not crs:
return False
try:
fields = self._create_geology_fields(geology_data)
writer = scu.create_shapefile_writer(str(output_path), crs, fields)
self._write_geology_features(writer, geology_data, fields)
del writer
except Exception:
logger.exception(f"Failed to export geology profile to {output_path}")
return False
else:
return True
def _write_geology_features(self, writer: Any, geology_data: list, fields: QgsFields) -> None:
"""Write geology segments to the writer.
Args:
writer: The vector file writer.
geology_data: List of geology segments.
fields: The QGIS field collection.
"""
for segment in geology_data:
feat = self._create_geology_feature(segment, fields)
if feat:
writer.addFeature(feat)
def _create_geology_fields(self, geology_data: list) -> QgsFields:
"""Create fields from the first segment's attributes."""
fields = QgsFields()
if geology_data:
# Use attributes from first segment as template
first_attrs = geology_data[0].attributes
for key in first_attrs:
fields.append(QgsField(key, QMetaType.Type.QString))
return fields
def _create_geology_feature(self, segment: Any, fields: QgsFields) -> QgsFeature | None:
"""Create a feature for a geology segment."""
if len(segment.points) < 2:
return None
points = [QgsPointXY(d, e) for d, e in segment.points]
geom = QgsGeometry.fromPolylineXY(points)
feat = QgsFeature(fields)
feat.setGeometry(geom)
# Set attributes
for key, val in segment.attributes.items():
idx = fields.indexOf(key)
if idx >= 0:
feat.setAttribute(idx, val)
return feat
[docs]
class StructureShpExporter(BaseExporter):
"""Exports the structural profile to a Shapefile."""
[docs]
def get_supported_extensions(self) -> list[str]:
"""Get supported file extensions.
Returns:
List of supported extensions.
"""
return [".shp"]
[docs]
def export(self, output_path: Path, data: dict[str, Any]) -> bool:
"""Export the structural profile to a Shapefile.
Args:
output_path: Path to the output Shapefile.
data: Dictionary containing 'structural_data', 'crs', 'dip_scale_factor',
and 'raster_res'.
"""
structural_data = data.get("structural_data")
crs = data.get("crs")
dip_scale_factor = data.get("dip_scale_factor", 4)
raster_res = data.get("raster_res", 1.0)
if not structural_data or not crs:
return False
try:
line_length = raster_res * dip_scale_factor
fields = self._create_structure_fields(structural_data)
writer = scu.create_shapefile_writer(str(output_path), crs, fields)
self._write_structure_features(writer, structural_data, fields, line_length)
del writer
except Exception:
logger.exception(f"Failed to export structural profile to {output_path}")
return False
else:
return True
def _write_structure_features(
self, writer: Any, structural_data: list, fields: QgsFields, line_length: float
) -> None:
"""Write structural measurements to the writer.
Args:
writer: The vector file writer.
structural_data: List of structural measurements.
fields: The QGIS field collection.
line_length: Length of the dip line in plot units.
"""
for m in structural_data:
feat = self._create_structure_feature(m, fields, line_length)
if feat:
writer.addFeature(feat)
def _create_structure_fields(self, structural_data: list) -> QgsFields:
"""Create fields for structural data."""
fields = QgsFields()
if structural_data:
first_attrs = structural_data[0].attributes
for key in first_attrs:
fields.append(QgsField(key, QMetaType.Type.QString))
fields.append(QgsField("app_dip", QMetaType.Type.Double))
fields.append(QgsField("dist", QMetaType.Type.Double))
fields.append(QgsField("elev", QMetaType.Type.Double))
return fields
def _create_structure_feature(
self, m: Any, fields: QgsFields, line_length: float
) -> QgsFeature:
"""Create a feature for a structural measurement."""
geom = self._calculate_dip_geometry(m, line_length)
feat = QgsFeature(fields)
feat.setGeometry(geom)
# Set attributes
for key, val in m.attributes.items():
idx = fields.indexOf(key)
if idx >= 0:
feat.setAttribute(idx, val)
feat["app_dip"] = m.apparent_dip
feat["dist"] = m.distance
feat["elev"] = m.elevation
return feat
def _calculate_dip_geometry(self, m: Any, line_length: float) -> QgsGeometry:
"""Calculate the line geometry for a structural dip."""
rad_dip = math.radians(m.apparent_dip)
dy = -line_length * math.sin(abs(rad_dip))
dx = line_length * math.cos(abs(rad_dip))
if m.apparent_dip < 0:
dx = -dx
p1 = QgsPointXY(m.distance, m.elevation)
p2 = QgsPointXY(m.distance + dx, m.elevation + dy)
return QgsGeometry.fromPolylineXY([p1, p2])
[docs]
class AxesShpExporter(BaseExporter):
"""Exports the profile axes to a Shapefile."""
[docs]
def get_supported_extensions(self) -> list[str]:
"""Get supported file extensions.
Returns:
List of supported extensions.
"""
return [".shp"]
[docs]
def export(self, output_path: Path, data: dict[str, Any]) -> bool:
"""Export the profile axes to a Shapefile.
Args:
output_path: Path to the output Shapefile.
data: Dictionary containing 'profile_data' and 'crs'.
"""
profile_data = data.get("profile_data")
crs = data.get("crs")
if not profile_data or not crs:
return False
try:
dists = [p[0] for p in profile_data]
elevs = [p[1] for p in profile_data]
min_d, max_d = min(dists), max(dists)
min_e, max_e = min(elevs), max(elevs)
if max_d == min_d:
max_d = min_d + 100
if max_e == min_e:
max_e = min_e + 10
e_range = max_e - min_e
min_e_padded = min_e - e_range * 0.05
max_e_padded = max_e + e_range * 0.05
lines = [
[QgsPointXY(min_d, min_e_padded), QgsPointXY(min_d, max_e_padded)],
[QgsPointXY(max_d, min_e_padded), QgsPointXY(max_d, max_e_padded)],
[QgsPointXY(min_d, min_e_padded), QgsPointXY(max_d, min_e_padded)],
]
fields = QgsFields()
fields.append(QgsField("axis", QMetaType.Type.QString))
writer = scu.create_shapefile_writer(str(output_path), crs, fields)
axis_names = ["Left", "Right", "Bottom"]
self._write_axes_features(writer, lines, axis_names)
del writer
except Exception:
logger.exception(f"Failed to export axes to {output_path}")
return False
else:
return True
def _write_axes_features(self, writer: Any, lines: list, axis_names: list[str]) -> None:
"""Write axes lines to the writer.
Args:
writer: The vector file writer.
lines: List of coordinate pairs for each axis.
axis_names: List of axis labels.
"""
for i, points in enumerate(lines):
feat = QgsFeature()
geom = QgsGeometry.fromPolylineXY(points)
feat.setGeometry(geom)
feat.setAttributes([axis_names[i]])
writer.addFeature(feat)