Source code for cruiseplan.interactive.widgets

"""
Custom matplotlib widgets for oceanographic cruise planning interface.
"""

from typing import Callable, Optional

import matplotlib.pyplot as plt
from matplotlib.widgets import Widget


[docs] class ModeIndicator(Widget): """ Visual indicator for current interaction mode. Shows current mode (navigation/point/line/area) with clear styling. Provides visual feedback to users about the current interaction state. Attributes ---------- ax : plt.Axes Matplotlib axes for the widget display. modes : List[str] List of available interaction modes. current_mode : str Currently active interaction mode. callbacks : Dict[str, Callable] Dictionary of callback functions for mode changes. colors : Dict[str, str] Color mapping for different modes. text_obj : Optional[plt.Text] Matplotlib text object for mode display. """
[docs] def __init__( self, ax: plt.Axes, modes: list[str], initial_mode: str = "navigation" ): """ Initialize the mode indicator widget. Parameters ---------- ax : plt.Axes Matplotlib axes for widget placement. modes : List[str] List of available interaction modes. initial_mode : str, optional Initial mode to display (default: "navigation"). """ self.ax = ax self.modes = modes self.current_mode = initial_mode # Callbacks are for external components (like StationPicker) to react to mode changes self.callbacks: dict[str, Callable] = {} # Style configuration aligned with visual distinction self.colors = { "navigation": "#2E8B57", # Sea green "point": "#4169E1", # Royal blue "line": "#FF6347", # Tomato "area": "#9932CC", # Dark orchid } self.text_obj: Optional[plt.Text] = None self._setup_display()
def _setup_display(self): """Initialize the mode indicator display.""" self.ax.set_xticks([]) self.ax.set_yticks([]) self._update_display() def _update_display(self): """Update the visual indicator.""" # Clear the axis content but maintain background properties self.ax.clear() self.ax.set_xlim(0, 1) self.ax.set_ylim(0, 1) # Background color based on mode color = self.colors.get(self.current_mode, "#808080") self.ax.add_patch(plt.Rectangle((0, 0), 1, 1, facecolor=color, alpha=0.3)) # Mode text self.text_obj = self.ax.text( 0.5, 0.5, f"Mode: {self.current_mode.title()}", ha="center", va="center", fontweight="bold", fontsize=10, color=color, ) # Restore necessary aesthetic properties self.ax.set_xticks([]) self.ax.set_yticks([]) self.ax.figure.canvas.draw_idle()
[docs] def set_mode(self, mode: str) -> None: """ Change the current mode. Parameters ---------- mode : str New mode to set. Must be one of the available modes. """ if mode in self.modes: old_mode = self.current_mode self.current_mode = mode self._update_display() # Trigger callbacks registered for the new mode if mode in self.callbacks: self.callbacks[mode](old_mode, mode)
[docs] def on_mode_change(self, mode: str, callback: Callable[[], None]) -> None: """ Register callback for mode changes. Parameters ---------- mode : str Mode for which to register the callback. callback : Callable Function to call when mode changes to the specified mode. """ self.callbacks[mode] = callback
[docs] class StatusDisplay(Widget): """ Real-time status display for coordinates, depth, and operation counts. Shows current mouse coordinates, bathymetric depth, and counts of planned stations, transects, and areas. Attributes ---------- ax : plt.Axes Matplotlib axes for the widget display. status_lines : List[plt.Text] List of matplotlib text objects for status display lines. """
[docs] def __init__(self, ax: plt.Axes): """ Initialize the status display widget. Parameters ---------- ax : plt.Axes Matplotlib axes for widget placement. """ self.ax = ax self.status_lines: list[plt.Text] = [] self._setup_display()
def _setup_display(self): """Initialize the status display.""" self.ax.clear() self.ax.set_xlim(0, 1) self.ax.set_ylim(0, 1) self.ax.set_xticks([]) self.ax.set_yticks([]) # Initialize status text lines self.status_lines = [ self.ax.text( 0.05, 0.85, "Coordinates: --", fontsize=9, transform=self.ax.transAxes ), self.ax.text( 0.05, 0.70, "Depth: --", fontsize=9, transform=self.ax.transAxes ), self.ax.text( 0.05, 0.55, "Stations: 0", fontsize=9, transform=self.ax.transAxes, weight="bold", ), self.ax.text( 0.05, 0.40, "Transects: 0", fontsize=9, transform=self.ax.transAxes, weight="bold", ), self.ax.text( 0.05, 0.25, "Areas: 0", fontsize=9, transform=self.ax.transAxes, weight="bold", ), ]
[docs] def update_coordinates(self, lat: Optional[float], lon: Optional[float]) -> None: """ Update coordinate display, using Degrees Decimal Minutes format. Parameters ---------- lat : Optional[float] Latitude coordinate in decimal degrees. lon : Optional[float] Longitude coordinate in decimal degrees. """ if lat is not None and lon is not None: # Format: DD MM.mmm Dir, DDD MM.mmm Dir # --- Latitude Conversion --- lat_deg, lat_frac = divmod(abs(lat), 1) lat_min = lat_frac * 60 lat_dir = "N" if lat >= 0 else "S" # --- Longitude Conversion --- lon_deg, lon_frac = divmod(abs(lon), 1) lon_min = lon_frac * 60 lon_dir = "E" if lon >= 0 else "W" # Format string for display (e.g., 53° 07.40'N, 050° 34.07'W) coord_str = f"{lat_deg:02.0f}° {lat_min:05.2f}'{lat_dir}, {lon_deg:03.0f}° {lon_min:05.2f}'{lon_dir}" self.status_lines[0].set_text(f"Coordinates: {coord_str}") else: self.status_lines[0].set_text("Coordinates: --")
[docs] def update_depth(self, depth: Optional[float]) -> None: """ Update depth display, handling positive elevation and negative depth. Parameters ---------- depth : Optional[float] Depth/elevation value in meters (negative for depth, positive for elevation). """ if depth is not None: if depth > 0: self.status_lines[1].set_text(f"Elevation: +{depth:.0f} m") else: self.status_lines[1].set_text(f"Depth: {abs(depth):.0f} m") else: self.status_lines[1].set_text("Depth: --")
[docs] def update_counts(self, stations: int, transects: int, areas: int) -> None: """ Update operation counters. Parameters ---------- stations : int Number of planned stations. transects : int Number of planned transects. areas : int Number of planned survey areas. """ self.status_lines[2].set_text(f"Stations: {stations}") self.status_lines[3].set_text(f"Transects: {transects}") self.status_lines[4].set_text(f"Areas: {areas}") # Refresh display self.ax.figure.canvas.draw_idle()