# -*- 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)