from __future__ import annotations
"""Reusable API contracts for focused map/box selection GUIs.
This module intentionally contains no Qt code. It defines the data contracts
that a purpose-built visualization GUI (for example, a post-download
FOV/box-selector) can use to communicate geometry changes back to ``pyAMPP``.
"""
from dataclasses import dataclass
from enum import Enum
from typing import Any, Protocol
[docs]
class CoordMode(str, Enum):
"""Coordinate representation used by the GUI center fields."""
@dataclass(slots=True)
[docs]
class BoxGeometrySelection:
"""Workflow-relevant box geometry values in the same shape as pyAMPP GUI fields.
These map directly to the existing ``PyAmppGUI`` input widgets:
- ``coord_x`` / ``coord_y`` -> ``coord_x_edit`` / ``coord_y_edit``
- ``grid_x`` / ``grid_y`` / ``grid_z`` -> ``grid_*_edit``
- ``dx_km`` -> ``res_edit`` (CLI ``--dx-km``)
"""
[docs]
def as_gui_text_fields(self) -> dict[str, str]:
"""Return values formatted for direct assignment to QLineEdit widgets."""
return {
"coord_x_edit": f"{self.coord_x:.2f}",
"coord_y_edit": f"{self.coord_y:.2f}",
"grid_x_edit": str(int(self.grid_x)),
"grid_y_edit": str(int(self.grid_y)),
"grid_z_edit": str(int(self.grid_z)),
"res_edit": f"{float(self.dx_km):.2f}",
}
@dataclass(slots=True)
@dataclass(slots=True)
[docs]
class DisplayFovSelection:
"""Observer-plane image FOV used for synthetic image framing and canvas centering.
This is intentionally separate from the model box geometry. Values are expressed
in helioprojective (observer-plane) arcsec, regardless of the red-box input
coordinate mode.
"""
[docs]
def as_gui_text_fields(self) -> dict[str, str]:
return {
"fov_x_edit": f"{self.center_x_arcsec:.2f}",
"fov_y_edit": f"{self.center_y_arcsec:.2f}",
"fov_w_edit": f"{self.width_arcsec:.2f}",
"fov_h_edit": f"{self.height_arcsec:.2f}",
}
@dataclass(slots=True)
[docs]
class DisplayFovBoxSelection:
"""Observer-aligned 3D FOV box.
The x/y footprint matches the image-plane FOV rectangle in helioprojective
arcsec. The z extent is stored in observer-centric heliocentric coordinates
(Mm) along the line of sight, where larger values are closer to the
observer.
"""
[docs]
corners_local_mm: tuple[tuple[float, float, float], ...] | None = None
[docs]
observer_key: str = "earth"
@classmethod
[docs]
def from_display_fov(
cls,
fov: "DisplayFovSelection",
z_min_mm: float,
z_max_mm: float,
*,
observer_key: str = "earth",
) -> "DisplayFovBoxSelection":
return cls(
center_x_arcsec=float(fov.center_x_arcsec),
center_y_arcsec=float(fov.center_y_arcsec),
width_arcsec=float(fov.width_arcsec),
height_arcsec=float(fov.height_arcsec),
z_min_mm=float(z_min_mm),
z_max_mm=float(z_max_mm),
observer_key=str(observer_key or "earth"),
)
@dataclass(slots=True)
[docs]
class SelectorDialogResult:
"""Accepted result returned by the standalone selector dialog."""
[docs]
geometry: BoxGeometrySelection
[docs]
fov: "DisplayFovSelection | None" = None
[docs]
square_fov: bool = False
[docs]
class GeometrySelectionConsumer(Protocol):
"""Callback protocol for receiving accepted geometry selections."""
[docs]
def apply_geometry_selection(self, selection: BoxGeometrySelection) -> None:
"""Apply an accepted geometry selection to the host GUI/application."""
__all__ = [
"CoordMode",
"BoxGeometrySelection",
"DisplayFovSelection",
"DisplayFovBoxSelection",
"SelectorDialogResult",
"SelectorSessionInput",
"GeometrySelectionConsumer",
]