# -*- coding: utf-8 -*- # # Licensed under the terms of the BSD 3-Clause # (see plotpy/LICENSE for details) # pylint: disable=C0103 """ Annotations ----------- The :mod:`annotation` module provides annotated shape plot items. """ from __future__ import annotations from typing import TYPE_CHECKING, Callable import numpy as np from guidata.configtools import get_icon from guidata.dataset import update_dataset from guidata.utils.misc import assert_interfaces_valid from plotpy.config import CONF, _ from plotpy.coords import canvas_to_axes from plotpy.interfaces import IBasePlotItem, ISerializableType, IShapeItemType from plotpy.items.label import DataInfoLabel from plotpy.items.shape.base import AbstractShape from plotpy.items.shape.ellipse import EllipseShape from plotpy.items.shape.point import PointShape from plotpy.items.shape.polygon import PolygonShape from plotpy.items.shape.rectangle import ObliqueRectangleShape, RectangleShape from plotpy.items.shape.segment import SegmentShape from plotpy.mathutils.geometry import ( compute_angle, compute_center, compute_distance, compute_rect_size, ) from plotpy.styles.label import LabelParam from plotpy.styles.shape import AnnotationParam if TYPE_CHECKING: import guidata.io import qwt.scale_map from qtpy.QtCore import QPointF, QRectF from qtpy.QtGui import QPainter from plotpy.interfaces import IItemType from plotpy.styles.base import ItemParameters class AnnotatedShape(AbstractShape): """ Construct an annotated shape with properties set with *annotationparam* (see :py:class:`.styles.AnnotationParam`) Args: annotationparam: Annotation parameters """ __implements__ = (IBasePlotItem, ISerializableType) SHAPE_CLASS: type[AbstractShape] = RectangleShape # to be overridden LABEL_ANCHOR: str = "" def __init__( self, annotationparam: AnnotationParam | None = None, info_callback: Callable[[AnnotatedShape], str] | None = None, ) -> None: super().__init__() assert self.LABEL_ANCHOR is not None and len(self.LABEL_ANCHOR) != 0 if info_callback is None: def info_callback(annotation: AnnotatedShape) -> str: """Return information on annotation""" return annotation.get_infos() self.info_callback = info_callback self.shape: AbstractShape = self.create_shape() self.label = self.create_label() self.area_computations_visible = True self.subtitle_visible = True if annotationparam is None: self.annotationparam = AnnotationParam( _("Annotation"), icon="annotation.png" ) else: self.annotationparam = annotationparam self.annotationparam.update_item(self) self.setIcon(get_icon("annotation.png")) def types(self) -> tuple[type[IItemType], ...]: """Returns a group or category for this item. This should be a tuple of class objects inheriting from IItemType Returns: tuple: Tuple of class objects inheriting from IItemType """ return (IShapeItemType, ISerializableType) def __reduce__(self) -> tuple[type, tuple, tuple]: """Return a tuple for pickling""" self.annotationparam.update_param(self) state = (self.shape, self.label, self.annotationparam) return (self.__class__, (), state) def __setstate__(self, state: tuple) -> None: """Set state after unpickling""" shape, label, param = state self.shape = shape self.label = label self.annotationparam: AnnotationParam = param self.annotationparam.update_item(self) def serialize( self, writer: guidata.io.HDF5Writer | guidata.io.INIWriter | guidata.io.JSONWriter, ) -> None: """Serialize object to HDF5 writer Args: writer: HDF5, INI or JSON writer """ writer.write(self.annotationparam, group_name="annotationparam") self.shape.serialize(writer) self.label.serialize(writer) def deserialize( self, reader: guidata.io.HDF5Reader | guidata.io.INIReader | guidata.io.JSONReader, ) -> None: """Deserialize object from HDF5 reader Args: reader: HDF5, INI or JSON reader """ self.annotationparam = AnnotationParam(_("Annotation"), icon="annotation.png") reader.read("annotationparam", instance=self.annotationparam) self.annotationparam.update_item(self) self.shape.deserialize(reader) self.label.deserialize(reader) def set_style(self, section: str, option: str) -> None: """Set style for this item Args: section: Section option: Option """ self.shape.set_style(section, option) # ----QwtPlotItem API-------------------------------------------------------- def draw( self, painter: QPainter, xMap: qwt.scale_map.QwtScaleMap, yMap: qwt.scale_map.QwtScaleMap, canvasRect: QRectF, ) -> None: """Draw the item Args: painter: Painter xMap: X axis scale map yMap: Y axis scale map canvasRect: Canvas rectangle """ self.shape.draw(painter, xMap, yMap, canvasRect) if self.label.isVisible(): self.label.draw(painter, xMap, yMap, canvasRect) # ----Public API------------------------------------------------------------- def create_shape(self): """Return the shape object associated to this annotated shape object""" shape = self.SHAPE_CLASS(0, 0, 1, 1) # pylint: disable=not-callable return shape def create_label(self) -> DataInfoLabel: """Return the label object associated to this annotated shape object Returns: Label object """ label_param = LabelParam(_("Label"), icon="label.png") label_param.read_config(CONF, "plot", "shape/label") label_param.anchor = self.LABEL_ANCHOR return DataInfoLabel(label_param, [self]) def is_label_visible(self) -> bool: """Return True if associated label is visible Returns: True if associated label is visible """ return self.label.isVisible() def set_label_visible(self, state: bool) -> None: """Set the annotated shape's label visibility Args: state: True if label should be visible """ self.label.setVisible(state) def update_label(self) -> None: """Update the annotated shape's label contents""" self.label.update_text() def set_info_callback(self, callback: Callable[[AnnotatedShape], str]) -> None: """Set the callback function to get informations on current shape Args: callback: Callback function to get informations on current shape """ self.info_callback = callback def get_text(self) -> str: """Return text associated to current shape (see :py:class:`.label.ObjectInfo`) Returns: Text associated to current shape """ text = "" title = self.title().text() if title: text += f"{title}" subtitle = self.annotationparam.subtitle if subtitle and self.subtitle_visible: if text: text += "
" text += f"{subtitle}" if self.area_computations_visible: infos = self.info_callback(self) if infos: if text: text += "
" text += infos return text def x_to_str(self, x: float) -> str: """Convert x to a string (with associated unit and uncertainty) Args: x: X value Returns: str: Formatted string with x value """ param = self.annotationparam if self.plot() is None: return "" else: xunit = self.plot().get_axis_unit(self.xAxis()) fmt = param.format if param.uncertainty: fmt += " ± " + (fmt % (x * param.uncertainty)) if xunit is not None: return (fmt + " " + xunit) % x else: return (fmt) % x def y_to_str(self, y): """Convert y to a string (with associated unit and uncertainty) Args: y: Y value Returns: str: Formatted string with x value """ param = self.annotationparam if self.plot() is None: return "" else: yunit = self.plot().get_axis_unit(self.yAxis()) fmt = param.format if param.uncertainty: fmt += " ± " + (fmt % (y * param.uncertainty)) if yunit is not None: return (fmt + " " + yunit) % y else: return (fmt) % y def get_center(self): """Return shape center coordinates: (xc, yc)""" return self.shape.get_center() def get_tr_center(self): """Return shape center coordinates after applying transform matrix""" raise NotImplementedError def get_tr_center_str(self): """Return center coordinates as a string (with units)""" xc, yc = self.get_tr_center() return f"( {self.x_to_str(xc)} ; {self.y_to_str(yc)} )" def get_tr_size(self): """Return shape size after applying transform matrix""" raise NotImplementedError def get_tr_size_str(self): """Return size as a string (with units)""" xs, ys = self.get_tr_size() return f"{self.x_to_str(xs)} x {self.y_to_str(ys)}" def get_infos(self) -> str: """Get informations on current shape Returns: str: Formatted string with informations on current shape """ return "" def set_label_position(self): """Set label position, for instance based on shape position""" raise NotImplementedError def apply_transform_matrix(self, x, y): """ :param x: :param y: :return: """ V = np.array([x, y, 1.0]) W = np.dot(V, self.annotationparam.transform_matrix) return W[0], W[1] def get_transformed_coords(self, handle1, handle2): """ :param handle1: :param handle2: :return: """ x1, y1 = self.apply_transform_matrix(*self.shape.points[handle1]) x2, y2 = self.apply_transform_matrix(*self.shape.points[handle2]) return x1, y1, x2, y2 # ----IBasePlotItem API------------------------------------------------------ def hit_test(self, pos: QPointF) -> tuple[float, float, bool, None]: """Return a tuple (distance, attach point, inside, other_object) Args: pos: Position Returns: tuple: Tuple with four elements: (distance, attach point, inside, other_object). Description of the returned values: * distance: distance in pixels (canvas coordinates) to the closest attach point * attach point: handle of the attach point * inside: True if the mouse button has been clicked inside the object * other_object: if not None, reference of the object which will be considered as hit instead of self """ return self.shape.poly_hit_test(self.plot(), self.xAxis(), self.yAxis(), pos) def move_point_to( self, handle: int, pos: tuple[float, float], ctrl: bool = False ) -> None: """Move a handle as returned by hit_test to the new position Args: handle: Handle pos: Position ctrl: True if button is being pressed, False otherwise """ self.shape.move_point_to(handle, pos, ctrl) self.set_label_position() if self.plot(): self.plot().SIG_ANNOTATION_CHANGED.emit(self) def move_shape(self, old_pos: QPointF, new_pos: QPointF) -> None: """Translate the shape such that old_pos becomes new_pos in axis coordinates Args: old_pos: Old position new_pos: New position """ self.shape.move_shape(old_pos, new_pos) self.label.move_local_shape(old_pos, new_pos) def move_local_shape(self, old_pos: QPointF, new_pos: QPointF) -> None: """Translate the shape such that old_pos becomes new_pos in canvas coordinates Args: old_pos: Old position new_pos: New position """ old_pt = canvas_to_axes(self, old_pos) new_pt = canvas_to_axes(self, new_pos) self.shape.move_shape(old_pt, new_pt) self.set_label_position() if self.plot(): self.plot().SIG_ITEM_MOVED.emit(self, *(old_pt + new_pt)) self.plot().SIG_ANNOTATION_CHANGED.emit(self) def move_with_selection(self, delta_x: float, delta_y: float) -> None: """Translate the item together with other selected items Args: delta_x: Translation in plot coordinates along x-axis delta_y: Translation in plot coordinates along y-axis """ self.shape.move_with_selection(delta_x, delta_y) self.label.move_with_selection(delta_x, delta_y) self.plot().SIG_ANNOTATION_CHANGED.emit(self) def select(self) -> None: """ Select the object and eventually change its appearance to highlight the fact that it's selected """ AbstractShape.select(self) self.shape.select() def unselect(self) -> None: """ Unselect the object and eventually restore its original appearance to highlight the fact that it's not selected anymore """ AbstractShape.unselect(self) self.shape.unselect() def get_item_parameters(self, itemparams: ItemParameters) -> None: """ Appends datasets to the list of DataSets describing the parameters used to customize apearance of this item Args: itemparams: Item parameters """ self.shape.get_item_parameters(itemparams) self.label.get_item_parameters(itemparams) self.annotationparam.update_param(self) itemparams.add("AnnotationParam", self, self.annotationparam) def set_item_parameters(self, itemparams: ItemParameters) -> None: """ Change the appearance of this item according to the parameter set provided Args: itemparams: Item parameters """ self.shape.set_item_parameters(itemparams) self.label.set_item_parameters(itemparams) update_dataset( self.annotationparam, itemparams.get("AnnotationParam"), visible_only=True ) self.annotationparam.update_item(self) self.plot().SIG_ANNOTATION_CHANGED.emit(self) # Autoscalable types API def is_empty(self) -> bool: """Return True if the item is empty Returns: True if the item is empty, False otherwise """ return self.shape.is_empty() def boundingRect(self) -> QRectF: """Return the bounding rectangle of the shape Returns: Bounding rectangle of the shape """ return self.shape.boundingRect() assert_interfaces_valid(AnnotatedShape) class AnnotatedPoint(AnnotatedShape): """ Construct an annotated point at coordinates (x, y) with properties set with *annotationparam* (see :py:class:`.styles.AnnotationParam`) """ SHAPE_CLASS = PointShape LABEL_ANCHOR = "TL" def __init__( self, x=0, y=0, annotationparam: AnnotationParam | None = None, info_callback: Callable[[AnnotatedShape], str] | None = None, ) -> None: super().__init__(annotationparam, info_callback) self.shape: PointShape self.set_pos(x, y) self.setIcon(get_icon("point_shape.png")) # ----Public API------------------------------------------------------------- def set_pos(self, x, y): """Set the point coordinates to (x, y)""" self.shape.set_pos(x, y) self.set_label_position() def get_pos(self): """Return the point coordinates""" return self.shape.get_pos() # ----AnnotatedShape API----------------------------------------------------- def create_shape(self): """Return the shape object associated to this annotated shape object""" shape = self.SHAPE_CLASS(0, 0) return shape def set_label_position(self): """Set label position, for instance based on shape position""" x, y = self.shape.points[0] self.label.set_pos(x, y) # ----AnnotatedShape API----------------------------------------------------- def get_tr_position(self): xt, yt = self.apply_transform_matrix(*self.shape.points[0]) return xt, yt def get_infos(self) -> str: """Get informations on current shape Returns: str: Formatted string with informations on current shape """ xt, yt = self.apply_transform_matrix(*self.shape.points[0]) s = "{title} ( {posx} ; {posy} )" s = s.format( title=_("Position:"), posx=self.x_to_str(xt), posy=self.y_to_str(yt) ) return s class AnnotatedSegment(AnnotatedShape): """ Construct an annotated segment between coordinates (x1, y1) and (x2, y2) with properties set with *annotationparam* (see :py:class:`.styles.AnnotationParam`) """ SHAPE_CLASS = SegmentShape LABEL_ANCHOR = "C" def __init__( self, x1=0, y1=0, x2=0, y2=0, annotationparam: AnnotationParam | None = None, info_callback: Callable[[AnnotatedShape], str] | None = None, ) -> None: super().__init__(annotationparam, info_callback) self.shape: SegmentShape self.set_rect(x1, y1, x2, y2) self.setIcon(get_icon("segment.png")) # ----Public API------------------------------------------------------------- def set_rect(self, x1, y1, x2, y2): """ Set the coordinates of the shape's top-left corner to (x1, y1), and of its bottom-right corner to (x2, y2). """ self.shape.set_rect(x1, y1, x2, y2) self.set_label_position() def get_rect(self): """ Return the coordinates of the shape's top-left and bottom-right corners """ return self.shape.get_rect() def get_tr_length(self): """Return segment length after applying transform matrix""" return compute_distance(*self.get_transformed_coords(0, 1)) def get_tr_center(self): """Return segment position (middle) after applying transform matrix""" return compute_center(*self.get_transformed_coords(0, 1)) # ----AnnotatedShape API----------------------------------------------------- def set_label_position(self): """Set label position, for instance based on shape position""" x1, y1, x2, y2 = self.get_rect() self.label.set_pos(*compute_center(x1, y1, x2, y2)) # ----AnnotatedShape API----------------------------------------------------- def get_infos(self) -> str: """Get informations on current shape Returns: str: Formatted string with informations on current shape """ return "
".join( [ _("Center:") + " " + self.get_tr_center_str(), _("Distance:") + " " + self.x_to_str(self.get_tr_length()), ] ) class AnnotatedPolygon(AnnotatedShape): """ Construct an annotated polygon with properties set with *annotationparam* (see :py:class:`.styles.AnnotationParam`) Args: points: List of points closed: True if polygon is closed annotationparam: Annotation parameters """ SHAPE_CLASS = PolygonShape LABEL_ANCHOR = "C" def __init__( self, points: list[tuple[float, float]] | None = None, closed: bool | None = None, annotationparam: AnnotationParam | None = None, info_callback: Callable[[AnnotatedShape], str] | None = None, ) -> None: super().__init__(annotationparam, info_callback) self.shape: PolygonShape if points is not None: self.set_points(points) if closed is not None: self.set_closed(closed) self.setIcon(get_icon("polygon.png")) # ----Public API------------------------------------------------------------- def set_points(self, points: list[tuple[float, float]] | np.ndarray | None) -> None: """Set the polygon points Args: points: List of point coordinates """ self.shape.set_points(points) self.set_label_position() def get_points(self) -> np.ndarray: """Return polygon points Returns: Polygon points (array of shape (N, 2)) """ return self.shape.get_points() def set_closed(self, state: bool) -> None: """Set closed state Args: state: True if the polygon is closed, False otherwise """ self.shape.set_closed(state) def is_closed(self) -> bool: """Return True if the polygon is closed, False otherwise Returns: True if the polygon is closed, False otherwise """ return self.shape.is_closed() def is_empty(self) -> bool: """Return True if the item is empty Returns: True if the item is empty, False otherwise """ return self.shape.is_empty() def add_local_point(self, pos: tuple[float, float]) -> int: """Add a point in canvas coordinates (local coordinates) Args: pos: Position Returns: Handle of the added point """ pt = canvas_to_axes(self, pos) return self.add_point(pt) def add_point(self, pt: tuple[float, float]) -> int: """Add a point in axis coordinates Args: pt: Position Returns: Handle of the added point """ handle = self.shape.add_point(pt) self.set_label_position() return handle def del_point(self, handle: int) -> int: """Delete a point Args: handle: Handle Returns: Handle of the deleted point """ handle = self.shape.del_point(handle) self.set_label_position() return handle def move_local_point_to(self, handle: int, pos: QPointF, ctrl: bool = None) -> None: """Move a handle as returned by hit_test to the new position Args: handle: Handle pos: Position ctrl: True if button is being pressed, False otherwise """ pt = canvas_to_axes(self, pos) self.move_point_to(handle, pt) def move_shape( self, old_pos: tuple[float, float], new_pos: tuple[float, float] ) -> None: """Translate the shape such that old_pos becomes new_pos in axis coordinates Args: old_pos: Old position new_pos: New position """ self.shape.move_shape(old_pos, new_pos) self.set_label_position() # ----AnnotatedShape API----------------------------------------------------- def create_shape(self): """Return the shape object associated to this annotated shape object""" shape = self.SHAPE_CLASS() # pylint: disable=not-callable return shape def set_label_position(self) -> None: """Set label position, for instance based on shape position""" x, y = self.shape.get_center() self.label.set_pos(x, y) def get_tr_center(self) -> tuple[float, float]: """Return shape center coordinates after applying transform matrix""" return self.shape.get_center() def get_infos(self) -> str: """Get informations on current shape Returns: str: Formatted string with informations on current shape """ return "
".join( [ _("Center:") + " " + self.get_tr_center_str(), ] ) class AnnotatedRectangle(AnnotatedShape): """ Construct an annotated rectangle between coordinates (x1, y1) and (x2, y2) with properties set with *annotationparam* (see :py:class:`.styles.AnnotationParam`) """ SHAPE_CLASS = RectangleShape LABEL_ANCHOR = "TL" def __init__( self, x1=0, y1=0, x2=0, y2=0, annotationparam: AnnotationParam | None = None, info_callback: Callable[[AnnotatedShape], str] | None = None, ) -> None: super().__init__(annotationparam, info_callback) self.shape: RectangleShape self.set_rect(x1, y1, x2, y2) self.setIcon(get_icon("rectangle.png")) # ----Public API------------------------------------------------------------- def set_rect(self, x1, y1, x2, y2): """ Set the coordinates of the shape's top-left corner to (x1, y1), and of its bottom-right corner to (x2, y2). """ self.shape.set_rect(x1, y1, x2, y2) self.set_label_position() def get_rect(self) -> tuple[float, float, float, float]: """ Return the coordinates of the shape's top-left and bottom-right corners """ return self.shape.get_rect() # ----AnnotatedShape API----------------------------------------------------- def set_label_position(self): """Set label position, for instance based on shape position""" x_label, y_label = self.shape.points.min(axis=0) self.label.set_pos(x_label, y_label) def get_tr_center(self): """Return shape center coordinates after applying transform matrix""" return compute_center(*self.get_transformed_coords(0, 2)) def get_tr_size(self): """Return shape size after applying transform matrix""" return compute_rect_size(*self.get_transformed_coords(0, 2)) def get_infos(self) -> str: """Get informations on current shape Returns: str: Formatted string with informations on current shape """ return "
".join( [ _("Center:") + " " + self.get_tr_center_str(), _("Size:") + " " + self.get_tr_size_str(), ] ) class AnnotatedObliqueRectangle(AnnotatedRectangle): """ Construct an annotated oblique rectangle between coordinates (x0, y0), (x1, y1), (x2, y2) and (x3, y3) with properties set with *annotationparam* (see :py:class:`.styles.AnnotationParam`) """ SHAPE_CLASS = ObliqueRectangleShape LABEL_ANCHOR = "C" def __init__( self, x0=0, y0=0, x1=0, y1=0, x2=0, y2=0, x3=0, y3=0, annotationparam=None ): AnnotatedShape.__init__(self, annotationparam) self.shape: ObliqueRectangleShape self.set_rect(x0, y0, x1, y1, x2, y2, x3, y3) self.setIcon(get_icon("oblique_rectangle.png")) # ----Public API------------------------------------------------------------- def get_tr_angle(self): """Return X-diameter angle with horizontal direction, after applying transform matrix""" xcoords = self.get_transformed_coords(0, 1) _x, yr1 = self.apply_transform_matrix(1.0, 1.0) _x, yr2 = self.apply_transform_matrix(1.0, 2.0) return (compute_angle(*xcoords, reverse=yr1 > yr2) + 90) % 180 - 90 def get_bounding_rect_coords(self) -> tuple[float, float, float, float]: """Return bounding rectangle coordinates (in plot coordinates) Returns: Bounding rectangle coordinates (in plot coordinates) """ return self.shape.get_bounding_rect_coords() # ----AnnotatedShape API----------------------------------------------------- def create_shape(self): """Return the shape object associated to this annotated shape object""" shape = self.SHAPE_CLASS(0, 0, 0, 0, 0, 0, 0, 0) return shape # ----AnnotatedShape API----------------------------------------------------- def set_label_position(self): """Set label position, for instance based on shape position""" self.label.set_pos(*self.get_center()) # ----RectangleShape API----------------------------------------------------- def set_rect(self, x0, y0, x1, y1, x2, y2, x3, y3): """ Set the rectangle corners coordinates: (x0, y0): top-left corner (x1, y1): top-right corner (x2, y2): bottom-right corner (x3, y3): bottom-left corner :: x: additionnal points (x0, y0)------>(x1, y1) ↑ | | | x x | | | ↓ (x3, y3)<------(x2, y2) """ self.shape.set_rect(x0, y0, x1, y1, x2, y2, x3, y3) self.set_label_position() def get_tr_size(self): """Return shape size after applying transform matrix""" dx = compute_distance(*self.get_transformed_coords(0, 1)) dy = compute_distance(*self.get_transformed_coords(0, 3)) return dx, dy # ----AnnotatedShape API----------------------------------------------------- def get_infos(self) -> str: """Get informations on current shape Returns: str: Formatted string with informations on current shape """ return "
".join( [ _("Center:") + " " + self.get_tr_center_str(), _("Size:") + " " + self.get_tr_size_str(), _("Angle:") + " %.1f°" % self.get_tr_angle(), ] ) def get_tr_center(self): x0, y0, x2, y2 = self.get_transformed_coords(0, 2) return compute_center(x0, y0, x2, y2) class AnnotatedEllipse(AnnotatedShape): """ Construct an annotated ellipse with X-axis diameter between coordinates (x1, y1) and (x2, y2) and properties set with *annotationparam* (see :py:class:`.styles.AnnotationParam`) """ SHAPE_CLASS = EllipseShape LABEL_ANCHOR = "C" def __init__( self, x1=0, y1=0, x2=0, y2=0, annotationparam: AnnotationParam | None = None, info_callback: Callable[[AnnotatedShape], str] | None = None, ) -> None: super().__init__(annotationparam, info_callback) self.shape: EllipseShape self.set_xdiameter(x1, y1, x2, y2) self.setIcon(get_icon("ellipse_shape.png")) self.switch_to_ellipse() # ----Public API------------------------------------------------------------- def switch_to_ellipse(self): """Switch to ellipse mode""" self.shape.switch_to_ellipse() def switch_to_circle(self): """Switch to circle mode""" self.shape.switch_to_circle() def set_xdiameter(self, x0, y0, x1, y1): """Set the coordinates of the ellipse's X-axis diameter Warning: transform matrix is not applied here""" self.shape.set_xdiameter(x0, y0, x1, y1) self.set_label_position() def get_xdiameter(self): """Return the coordinates of the ellipse's X-axis diameter Warning: transform matrix is not applied here""" return self.shape.get_xdiameter() def set_ydiameter(self, x2, y2, x3, y3): """Set the coordinates of the ellipse's Y-axis diameter Warning: transform matrix is not applied here""" self.shape.set_ydiameter(x2, y2, x3, y3) self.set_label_position() def get_ydiameter(self): """Return the coordinates of the ellipse's Y-axis diameter Warning: transform matrix is not applied here""" return self.shape.get_ydiameter() def get_rect(self): """ :return: """ return self.shape.get_rect() def set_rect(self, x0, y0, x1, y1): """ :param x0: :param y0: :param x1: :param y1: """ raise NotImplementedError def get_tr_angle(self): """Return X-diameter angle with horizontal direction, after applying transform matrix""" xcoords = self.get_transformed_coords(0, 1) _x, yr1 = self.apply_transform_matrix(1.0, 1.0) _x, yr2 = self.apply_transform_matrix(1.0, 2.0) return (compute_angle(*xcoords, reverse=yr1 > yr2) + 90) % 180 - 90 # ----AnnotatedShape API----------------------------------------------------- def set_label_position(self): """Set label position, for instance based on shape position""" x_label, y_label = self.shape.points.mean(axis=0) self.label.set_pos(x_label, y_label) def get_tr_center(self): """Return center coordinates: (xc, yc)""" return compute_center(*self.get_transformed_coords(0, 1)) def get_tr_size(self): """Return shape size after applying transform matrix""" xcoords = self.get_transformed_coords(0, 1) ycoords = self.get_transformed_coords(2, 3) dx = compute_distance(*xcoords) dy = compute_distance(*ycoords) if np.fabs(self.get_tr_angle()) > 45: dx, dy = dy, dx return dx, dy def get_infos(self) -> str: """Get informations on current shape Returns: str: Formatted string with informations on current shape """ return "
".join( [ _("Center:") + " " + self.get_tr_center_str(), _("Size:") + " " + self.get_tr_size_str(), _("Angle:") + f" {self.get_tr_angle():.1f}°", ] ) class AnnotatedCircle(AnnotatedEllipse): """ Construct an annotated circle with diameter between coordinates (x1, y1) and (x2, y2) and properties set with *annotationparam* (see :py:class:`.styles.AnnotationParam`) """ def __init__(self, x1=0, y1=0, x2=0, y2=0, annotationparam=None): AnnotatedEllipse.__init__(self, x1, y1, x2, y2, annotationparam) self.shape.switch_to_circle() def get_tr_diameter(self): """Return circle diameter after applying transform matrix""" return compute_distance(*self.get_transformed_coords(0, 1)) # ----AnnotatedShape API------------------------------------------------- def get_infos(self) -> str: """Get informations on current shape Returns: str: Formatted string with informations on current shape """ return "
".join( [ _("Center:") + " " + self.get_tr_center_str(), _("Diameter:") + " " + self.x_to_str(self.get_tr_diameter()), ] ) # ----AnnotatedEllipse API--------------------------------------------------- def set_rect(self, x0, y0, x1, y1): """ :param x0: :param y0: :param x1: :param y1: """ self.shape.set_rect(x0, y0, x1, y1)