CoolFace
Apppublic

mlukac/xrf-explorer-dev

sourceHugging Faceupdated 10mo agoView on Hugging Face
0likes
annotations.py366 linesDownload Raw Back to controllers
1from __future__ import annotations2 3import logging4from enum import IntEnum5from typing import TYPE_CHECKING, Optional6 7import param8 9from xrf_explorer.core.events import (10    AddDepthIntervalAnnotation,11    AddPointAnnotation,12    RemoveDepthIntervalAnnotation,13    RemovePointAnnotation,14    UpdateDepthIntervalAnnotation,15    UpdatePointAnnotation,16)17from xrf_explorer.core.types import ApplicationHandlerProtocol18from xrf_explorer.handlers.annotations import (19    DepthIntervalAnnotationHandler,20    PointAnnotationHandler,21)22from bokeh.models import ColumnDataSource23 24 25class AnnotationSelection(IntEnum):26    """Enum for annotation selection states."""27 28    NONE_SELECTED = -129    CREATE_NEW = -230 31 32class BaseAnnotationController(param.Parameterized):33    """Base class for annotation controllers providing common functionality."""34    35    # Common form inputs36    label = param.String(default="", doc="Annotation label text")37    font_size = param.Selector(38        default="14pt",39        objects=["10pt", "12pt", "14pt", "16pt", "18pt", "20pt", "24pt"],40        doc="Font size for annotation text"41    )42    angle = param.Number(default=0.0, bounds=(0, 90), doc="Text rotation angle in degrees")43    color = param.Color(default="#FF0000", doc="Annotation color")44    45    # Common action buttons46    add_annotation_button = param.Event(label="Add Annotation")47    48    # Common annotation management49    selected_annotation_index = param.Selector(50        default=AnnotationSelection.CREATE_NEW,51        objects=[],52        allow_None=False,53        label="Select Annotation",54    )55    delete_annotation_button = param.Event(label="Delete Selected")56 57    def __init__(self, app: ApplicationHandlerProtocol, handler, **kwargs) -> None:58        super().__init__(**kwargs)59        self._app = app60        self._handler = handler61        self._updating_from_selection: bool = False62        self._ui_initialized: bool = False63 64    def _update_annotation_choices(self) -> None:65        """Update the dropdown choices and handle selection persistence."""66        if not self._ui_initialized:67            return68 69        new_choices = self._get_annotation_choices()70        old_index = self.selected_annotation_index71 72        choice_objects = [choice[1] for choice in new_choices]73        choice_names = dict(new_choices)74 75        try:76            self.param.selected_annotation_index.objects = choice_names77            self.param.selected_annotation_index.names = choice_names78        except (ValueError, AttributeError) as e:79            # Log the error with full context for debugging80            logging.error(81                f"Critical error updating annotation dropdown choices in {self.__class__.__name__}: {e}. "82                f"Choice objects: {choice_objects}, Choice names: {choice_names}"83            )84            # Re-raise the exception as this indicates a serious configuration problem85            # that should not be silently ignored86            raise RuntimeError(87                f"Failed to update annotation dropdown choices: {e}"88            ) from e89 90        # Preserve selection if valid, otherwise default to CREATE_NEW91        if old_index in choice_objects:92            self.selected_annotation_index = old_index93        else:94            self.selected_annotation_index = AnnotationSelection.CREATE_NEW95 96    def initialize_ui(self) -> None:97        """Initialize the UI after all components are set up."""98        self._ui_initialized = True99        self._update_annotation_choices()100 101        # Ensure valid initial selection102        valid_choice_values = [choice[1] for choice in self._get_annotation_choices()]103        if self.selected_annotation_index not in valid_choice_values:104            self.selected_annotation_index = AnnotationSelection.CREATE_NEW105 106    def handle_annotation_data_changed(self) -> None:107        """Called after annotation data changes to update dropdown choices."""108        self._update_annotation_choices()109 110    @param.depends("delete_annotation_button", watch=True)111    def _delete_annotation_clicked(self) -> None:112        """Handle delete annotation button click."""113        if self.selected_annotation_index >= 0:114            self._app.trigger(self._get_remove_event(self.selected_annotation_index))115            self.selected_annotation_index = AnnotationSelection.CREATE_NEW116 117    @param.depends("selected_annotation_index", watch=True)118    def select_annotation(self) -> None:119        """Handle annotation selection change."""120        self._updating_from_selection = True121 122        if self.selected_annotation_index == AnnotationSelection.CREATE_NEW:123            self._reset_form()124        elif self._is_valid_annotation_index():125            self._load_annotation_data()126        else:127            # Fallback for invalid selections128            self.selected_annotation_index = AnnotationSelection.CREATE_NEW129 130        self._updating_from_selection = False131 132    def _is_valid_annotation_index(self) -> bool:133        """Check if selected annotation index is valid."""134        return (135            self.selected_annotation_index >= 0136            and self.selected_annotation_index < len(self._handler.data)137        )138 139    def _get_annotation_choices(self) -> list[tuple[str, int]]:140        """Generate dropdown choices for annotation selection."""141        raise NotImplementedError("Subclasses must implement _get_annotation_choices")142 143    def _get_remove_event(self, index: int):144        """Get the remove event for this annotation type."""145        raise NotImplementedError("Subclasses must implement _get_remove_event")146 147    def _add_annotation_clicked(self) -> None:148        """Handle add annotation button click."""149        raise NotImplementedError("Subclasses must implement _add_annotation_clicked")150 151    def _update_annotation(self) -> None:152        """Handle form field changes to update existing annotation."""153        raise NotImplementedError("Subclasses must implement _update_annotation")154 155    def _reset_form(self) -> None:156        """Reset form fields to default values."""157        raise NotImplementedError("Subclasses must implement _reset_form")158 159    def _load_annotation_data(self) -> None:160        """Load annotation data into form fields."""161        raise NotImplementedError("Subclasses must implement _load_annotation_data")162 163 164class PointAnnotationController(BaseAnnotationController):165    """Controller for managing point annotations with form inputs and selection."""166 167    # Form inputs specific to point annotations168    depth = param.Integer(default=None, bounds=(0, None))169 170    def _get_annotation_choices(self) -> list[tuple[str, int]]:171        """Generate dropdown choices for annotation selection."""172        choices = [("➕ Create New Annotation", int(AnnotationSelection.CREATE_NEW))]173 174        if len(self._handler.data) == 0:175            return choices176 177        for i, (_, row) in enumerate(self._handler.data.iterrows()):178            display_label = f"{i}: {row['label']} ({int(row['depth'])} ft)"179            choices.append((display_label, i))180 181        return choices182 183    def _get_remove_event(self, index: int):184        """Get the remove event for this annotation type."""185        return RemovePointAnnotation(index=index)186 187    @param.depends("add_annotation_button", watch=True)188    def _add_annotation_clicked(self) -> None:189        """Handle add annotation button click."""190        if self.depth is not None and self.label.strip():191            self._app.trigger(192                AddPointAnnotation(193                    depth=float(self.depth), label=self.label, color=self.color,194                    font_size=self.font_size, angle=self.angle195                )196            )197            # Reset form198            self._reset_form()199 200    @param.depends("depth", "label", "font_size", "angle", "color", watch=True)201    def _update_annotation(self) -> None:202        """Handle form field changes to update existing annotation."""203        if self._updating_from_selection or not self._is_valid_annotation_index():204            return205 206        if self.depth is not None:207            self._app.trigger(208                UpdatePointAnnotation(209                    index=self.selected_annotation_index,210                    depth=float(self.depth),211                    label=self.label,212                    color=self.color,213                    font_size=self.font_size,214                    angle=self.angle,215                )216            )217 218    def _reset_form(self) -> None:219        """Reset form fields to default values."""220        self.depth = None221        self.label = ""222        self.font_size = "14pt"223        self.angle = 0.0224        self.color = "#FF0000"225 226    def _load_annotation_data(self) -> None:227        """Load annotation data into form fields."""228        if not self._is_valid_annotation_index():229            logging.error(230                f"Invalid annotation index {self.selected_annotation_index} "231                f"for DataFrame with {len(self._handler.data)} rows. Resetting to CREATE_NEW."232            )233            self.selected_annotation_index = AnnotationSelection.CREATE_NEW234            return235        236        try:237            annotation = self._handler.data.iloc[self.selected_annotation_index]238            self.depth = int(round(annotation["depth"]))239            self.label = annotation["label"]240            self.font_size = annotation["font_size"]241            self.angle = annotation["angle"]242            self.color = annotation["color"]243        except (IndexError, KeyError, ValueError) as e:244            logging.error(245                f"Failed to load annotation data at index {self.selected_annotation_index}: {e}. "246                f"Resetting to CREATE_NEW."247            )248            self.selected_annotation_index = AnnotationSelection.CREATE_NEW249 250 251class DepthIntervalAnnotationController(BaseAnnotationController):252    """Controller for managing depth interval annotations with form inputs and selection."""253 254    # Form inputs specific to depth interval annotations255    depth_start = param.Integer(default=None, bounds=(0, None), doc="Start depth")256    depth_end = param.Integer(default=None, bounds=(0, None), doc="End depth")257    hatch_pattern = param.Selector(258        default="blank",259        objects=["blank", "dot", "ring", "horizontal_line", "vertical_line", "cross", "horizontal_dash", "vertical_dash", "spiral"],260        doc="Hatch pattern for annotation"261    )262 263    def _get_annotation_choices(self) -> list[tuple[str, int]]:264        """Generate dropdown choices for annotation selection."""265        choices = [266            ("➕ Create New Interval Annotation", int(AnnotationSelection.CREATE_NEW))267        ]268 269        if len(self._handler.data) == 0:270            return choices271 272        for i, (_, row) in enumerate(self._handler.data.iterrows()):273            start = int(row["depth_start"])274            end = int(row["depth_end"])275            display_label = f"{i}: {row['label']} ({start}-{end} ft)"276            choices.append((display_label, i))277 278        return choices279 280    def _get_remove_event(self, index: int):281        """Get the remove event for this annotation type."""282        return RemoveDepthIntervalAnnotation(index=index)283 284    @param.depends("add_annotation_button", watch=True)285    def _add_annotation_clicked(self) -> None:286        """Handle add annotation button click."""287        if (288            self.depth_start is not None289            and self.depth_end is not None290            and self.label.strip()291            and self.depth_start < self.depth_end292        ):293            self._app.trigger(294                AddDepthIntervalAnnotation(295                    depth_start=float(self.depth_start),296                    depth_end=float(self.depth_end),297                    label=self.label,298                    color=self.color,299                    hatch_pattern=self.hatch_pattern,300                    font_size=self.font_size,301                    angle=self.angle,302                )303            )304            # Reset form305            self._reset_form()306 307    @param.depends("depth_start", "depth_end", "label", "font_size", "angle", "color", "hatch_pattern", watch=True)308    def _update_annotation(self) -> None:309        """Handle form field changes to update existing annotation."""310        if self._updating_from_selection or not self._is_valid_annotation_index():311            return312 313        if (314            self.depth_start is not None315            and self.depth_end is not None316            and self.depth_start < self.depth_end317        ):318            self._app.trigger(319                UpdateDepthIntervalAnnotation(320                    index=self.selected_annotation_index,321                    depth_start=float(self.depth_start),322                    depth_end=float(self.depth_end),323                    label=self.label,324                    color=self.color,325                    hatch_pattern=self.hatch_pattern,326                    font_size=self.font_size,327                    angle=self.angle,328                )329            )330 331    def _reset_form(self) -> None:332        """Reset form fields to default values."""333        self.depth_start = None334        self.depth_end = None335        self.label = ""336        self.font_size = "14pt"337        self.angle = 0.0338        self.color = "#FF0000"339        self.hatch_pattern = "blank"340 341    def _load_annotation_data(self) -> None:342        """Load annotation data into form fields."""343        if not self._is_valid_annotation_index():344            logging.error(345                f"Invalid annotation index {self.selected_annotation_index} "346                f"for DataFrame with {len(self._handler.data)} rows. Resetting to CREATE_NEW."347            )348            self.selected_annotation_index = AnnotationSelection.CREATE_NEW349            return350        351        try:352            annotation = self._handler.data.iloc[self.selected_annotation_index]353            self.depth_start = int(round(annotation["depth_start"]))354            self.depth_end = int(round(annotation["depth_end"]))355            self.label = annotation["label"]356            self.font_size = annotation["font_size"]357            self.angle = annotation["angle"]358            self.color = annotation["color"]359            self.hatch_pattern = annotation["hatch_pattern"]360        except (IndexError, KeyError, ValueError) as e:361            logging.error(362                f"Failed to load annotation data at index {self.selected_annotation_index}: {e}. "363                f"Resetting to CREATE_NEW."364            )365            self.selected_annotation_index = AnnotationSelection.CREATE_NEW366