# -*- coding: utf-8 -*- # # Copyright © 2009-2010 CEA # Pierre Raybaut # Licensed under the terms of the CECILL License # (see guiqwt/__init__.py for details) """ guiqwt.annotations ------------------ The `annotations` module provides annotated shapes: * :py:class:`guiqwt.annotations.AnnotatedPoint` * :py:class:`guiqwt.annotations.AnnotatedSegment` * :py:class:`guiqwt.annotations.AnnotatedRectangle` * :py:class:`guiqwt.annotations.AnnotatedObliqueRectangle` * :py:class:`guiqwt.annotations.AnnotatedEllipse` * :py:class:`guiqwt.annotations.AnnotatedCircle` An annotated shape is a plot item (derived from QwtPlotItem) that may be displayed on a 2D plotting widget like :py:class:`guiqwt.curve.CurvePlot` or :py:class:`guiqwt.image.ImagePlot`. .. seealso:: module :py:mod:`guiqwt.shapes` Examples ~~~~~~~~ An annotated shape may be created: * from the associated plot item class (e.g. `AnnotatedCircle` to create an annotated circle): the item properties are then assigned by creating the appropriate style parameters object (:py:class:`guiqwt.styles.AnnotationParam`) >>> from guiqwt.annotations import AnnotatedCircle >>> from guiqwt.styles import AnnotationParam >>> param = AnnotationParam() >>> param.title = 'My circle' >>> circle_item = AnnotatedCircle(0., 2., 4., 0., param) * or using the `plot item builder` (see :py:func:`guiqwt.builder.make`): >>> from guiqwt.builder import make >>> circle_item = make.annotated_circle(0., 2., 4., 0., title='My circle') Reference ~~~~~~~~~ .. autoclass:: AnnotatedPoint :members: :inherited-members: .. autoclass:: AnnotatedSegment :members: :inherited-members: .. autoclass:: AnnotatedRectangle :members: :inherited-members: .. autoclass:: AnnotatedObliqueRectangle :members: :inherited-members: .. autoclass:: AnnotatedEllipse :members: :inherited-members: .. autoclass:: AnnotatedCircle :members: :inherited-members: """ import numpy as np from guidata.utils import update_dataset # Local imports from guiqwt.config import CONF, _ from guiqwt.styles import LabelParam, AnnotationParam from guiqwt.shapes import (AbstractShape, RectangleShape, EllipseShape, SegmentShape, PointShape, ObliqueRectangleShape) from guiqwt.label import DataInfoLabel from guiqwt.interfaces import IShapeItemType, ISerializableType from guiqwt.signals import SIG_ANNOTATION_CHANGED, SIG_ITEM_MOVED from guiqwt.geometry import (compute_center, compute_rect_size, compute_distance, compute_angle) class AnnotatedShape(AbstractShape): """ Construct an annotated shape with properties set with *annotationparam* (see :py:class:`guiqwt.styles.AnnotationParam`) """ SHAPE_CLASS = None LABEL_ANCHOR = None def __init__(self, annotationparam=None): AbstractShape.__init__(self) assert self.LABEL_ANCHOR is not None self.shape = self.create_shape() self.label = self.create_label() self.area_computations_visible = True if annotationparam is None: self.annotationparam = AnnotationParam(_("Annotation"), icon="annotation.png") else: self.annotationparam = annotationparam self.annotationparam.update_annotation(self) def types(self): return (IShapeItemType, ISerializableType) def __reduce__(self): self.annotationparam.update_param(self) state = (self.shape, self.label, self.annotationparam) return (self.__class__, (), state) def __setstate__(self, state): shape, label, param = state self.shape = shape self.label = label self.annotationparam = param self.annotationparam.update_annotation(self) def set_style(self, section, option): self.shape.set_style(section, option) #----QwtPlotItem API-------------------------------------------------------- def draw(self, painter, xMap, yMap, canvasRect): 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) return shape def create_label(self): """Return the label object associated to this annotated shape 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): """Return True if associated label is visible""" return self.label.isVisible() def set_label_visible(self, state): """Set the annotated shape's label visibility""" self.label.setVisible(state) def update_label(self): """Update the annotated shape's label contents""" self.label.update_text() def get_text(self): """ Return text associated to current shape (see :py:class:`guiqwt.label.ObjectInfo`) """ text = "" title = self.title().text() if title: text += "<b>%s</b>" % title subtitle = self.annotationparam.subtitle if subtitle: if text: text += "<br>" text += "<i>%s</i>" % subtitle if self.area_computations_visible: infos = self.get_infos() if infos: if text: text += "<br>" text += infos return text def x_to_str(self, x, k): """ Convert x (float) to a string (with associated unit and uncertainty) k: uncertainty factor Examples: coordinate: k == 1 distance: k == 2 (uncertainty is doubled) """ 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 += u" ± "+(fmt % (.5*x*k*param.uncertainty)) return (fmt+" "+xunit) % x def y_to_str(self, y, k): """ Convert y (float) to a string (with associated unit and uncertainty) k: uncertainty factor Examples: coordinate: k == 1 distance: k == 2 (uncertainty is doubled) """ 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 += u" ± "+(fmt % (.5*y*k*param.uncertainty)) return (fmt+" "+yunit) % 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 "( %s ; %s )" % (self.x_to_str(xc, 1), self.y_to_str(yc, 1)) 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 "%s x %s" % (self.x_to_str(xs, 2), self.y_to_str(ys, 2)) def get_infos(self): """Return formatted string with informations on current shape""" pass def set_label_position(self): """Set label position, for instance based on shape position""" raise NotImplementedError def apply_transform_matrix(self, x, y): V = np.array([x, y, 1.]) W = np.dot(V, self.annotationparam.transform_matrix) return W[0], W[1] def get_transformed_coords(self, handle1, handle2): 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): return self.shape.poly_hit_test(self.plot(), self.xAxis(), self.yAxis(), pos) def move_point_to(self, handle, pos, ctrl=None): self.shape.move_point_to(handle, pos, ctrl) self.set_label_position() if self.plot(): self.plot().emit(SIG_ANNOTATION_CHANGED, self) def move_shape(self, old_pos, new_pos): self.shape.move_shape(old_pos, new_pos) self.label.move_local_shape(old_pos, new_pos) def move_local_shape(self, old_pos, new_pos): old_pt = self.canvas_to_axes(old_pos) new_pt = self.canvas_to_axes(new_pos) self.shape.move_shape(old_pt, new_pt) self.set_label_position() if self.plot(): self.plot().emit(SIG_ITEM_MOVED, self, *(old_pt+new_pt)) self.plot().emit(SIG_ANNOTATION_CHANGED, self) def move_with_selection(self, delta_x, delta_y): """ Translate the shape together with other selected items delta_x, delta_y: translation in plot coordinates """ self.shape.move_with_selection(delta_x, delta_y) self.label.move_with_selection(delta_x, delta_y) self.plot().emit(SIG_ANNOTATION_CHANGED, self) def select(self): """Select item""" AbstractShape.select(self) self.shape.select() def unselect(self): """Unselect item""" AbstractShape.unselect(self) self.shape.unselect() def get_item_parameters(self, itemparams): 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): 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_annotation(self)[docs]class AnnotatedPoint(AnnotatedShape): """ Construct an annotated point at coordinates (x, y) with properties set with *annotationparam* (see :py:class:`guiqwt.styles.AnnotationParam`) """ SHAPE_CLASS = PointShape LABEL_ANCHOR = "TL" def __init__(self, x=0, y=0, annotationparam=None): AnnotatedShape.__init__(self, annotationparam) self.set_pos(x, y) #----Public API-------------------------------------------------------------[docs] def set_pos(self, x, y): """Set the point coordinates to (x, y)""" self.shape.set_pos(x, y) self.set_label_position()[docs] def get_pos(self): """Return the point coordinates""" return self.shape.get_pos() #----AnnotatedShape API-----------------------------------------------------[docs] def create_shape(self): """Return the shape object associated to this annotated shape object""" shape = self.SHAPE_CLASS(0, 0) return shape[docs] 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-----------------------------------------------------[docs] def get_infos(self): """Return formatted string with informations on current shape""" xt, yt = self.apply_transform_matrix(*self.shape.points[0]) return "( %s ; %s )" % (self.x_to_str(xt, 1), self.y_to_str(yt, 1))[docs]class AnnotatedSegment(AnnotatedShape): """ Construct an annotated segment between coordinates (x1, y1) and (x2, y2) with properties set with *annotationparam* (see :py:class:`guiqwt.styles.AnnotationParam`) """ SHAPE_CLASS = SegmentShape LABEL_ANCHOR = "C" def __init__(self, x1=0, y1=0, x2=0, y2=0, annotationparam=None): AnnotatedShape.__init__(self, annotationparam) self.set_rect(x1, y1, x2, y2) #----Public API-------------------------------------------------------------[docs] 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()[docs] def get_rect(self): """ Return the coordinates of the shape's top-left and bottom-right corners """ return self.shape.get_rect()[docs] def get_tr_length(self): """Return segment length after applying transform matrix""" return compute_distance(*self.get_transformed_coords(0, 1)) #----AnnotatedShape API-----------------------------------------------------[docs] 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-----------------------------------------------------[docs] def get_infos(self): """Return formatted string with informations on current shape""" return _("Distance:") + " " + self.x_to_str(self.get_tr_length(), 2)[docs]class AnnotatedRectangle(AnnotatedShape): """ Construct an annotated rectangle between coordinates (x1, y1) and (x2, y2) with properties set with *annotationparam* (see :py:class:`guiqwt.styles.AnnotationParam`) """ SHAPE_CLASS = RectangleShape LABEL_ANCHOR = "TL" def __init__(self, x1=0, y1=0, x2=0, y2=0, annotationparam=None): AnnotatedShape.__init__(self, annotationparam) self.set_rect(x1, y1, x2, y2) #----Public API-------------------------------------------------------------[docs] 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()[docs] def get_rect(self): """ Return the coordinates of the shape's top-left and bottom-right corners """ return self.shape.get_rect() #----AnnotatedShape API-----------------------------------------------------[docs] 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)[docs] def get_computations_text(self): """Return formatted string with informations on current shape""" tdict = self.get_string_dict() return u"%(center_n)s ( %(center)s )<br>%(size_n)s %(size)s" % tdict[docs] def get_tr_center(self): """Return shape center coordinates after applying transform matrix""" return compute_center(*self.get_transformed_coords(0, 2))[docs] def get_tr_size(self): """Return shape size after applying transform matrix""" return compute_rect_size(*self.get_transformed_coords(0, 2))[docs] def get_infos(self): """Return formatted string with informations on current shape""" return "<br>".join([ _("Center:") + " " + self.get_tr_center_str(), _("Size:") + " " + self.get_tr_size_str(), ])[docs]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:`guiqwt.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.set_rect(x0, y0, x1, y1, x2, y2, x3, y3) #----Public API-------------------------------------------------------------[docs] 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., 1.) _x, yr2 = self.apply_transform_matrix(1., 2.) return (compute_angle(reverse=yr1 > yr2, *xcoords)+90)%180-90[docs] def get_bounding_rect_coords(self): """Return bounding rectangle coordinates (in plot coordinates)""" return self.shape.get_bounding_rect_coords() #----AnnotatedShape API-----------------------------------------------------[docs] 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-----------------------------------------------------[docs] def set_label_position(self): """Set label position, for instance based on shape position""" self.label.set_pos(*self.get_center()) #----RectangleShape API-----------------------------------------------------[docs] def set_rect(self, x0, y0, x1, y1, x2, y2, x3, y3): u""" 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()[docs] 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-----------------------------------------------------[docs] def get_infos(self): """Return formatted string with informations on current shape""" return "<br>".join([ _("Center:") + " " + self.get_tr_center_str(), _("Size:") + " " + self.get_tr_size_str(), _(u"Angle:") + u" %.1f°" % self.get_tr_angle(), ])[docs]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:`guiqwt.styles.AnnotationParam`) """ SHAPE_CLASS = EllipseShape LABEL_ANCHOR = "C" def __init__(self, x1=0, y1=0, x2=0, y2=0, ratio=1., annotationparam=None): self.ratio = ratio AnnotatedShape.__init__(self, annotationparam) self.set_xdiameter(x1, y1, x2, y2) #----Public API-------------------------------------------------------------[docs] 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()[docs] 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()[docs] 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()[docs] 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 self.shape.get_rect() def set_rect(self, x0, y0, x1, y1): raise NotImplementedError[docs] 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., 1.) _x, yr2 = self.apply_transform_matrix(1., 2.) return (compute_angle(reverse=yr1 > yr2, *xcoords)+90)%180-90 #----AnnotatedShape API-----------------------------------------------------[docs] def create_shape(self): """Return the shape object associated to this annotated shape object""" shape = self.SHAPE_CLASS(0, 0, 1, 1, ratio=self.ratio) return shape[docs] 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)[docs] def get_tr_center(self): """Return center coordinates: (xc, yc)""" return compute_center(*self.get_transformed_coords(0, 1))[docs] 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[docs] def get_infos(self): """Return formatted string with informations on current shape""" return "<br>".join([ _("Center:") + " " + self.get_tr_center_str(), _("Size:") + " " + self.get_tr_size_str(), _(u"Angle:") + u" %.1f°" % self.get_tr_angle(), ])[docs]class AnnotatedCircle(AnnotatedEllipse): """ Construct an annotated circle with diameter between coordinates (x1, y1) and (x2, y2) and properties set with *annotationparam* (see :py:class:`guiqwt.styles.AnnotationParam`) """ def __init__(self, x1=0, y1=0, x2=0, y2=0, annotationparam=None): AnnotatedEllipse.__init__(self, x1, y1, x2, y2, 1., annotationparam)[docs] def get_tr_diameter(self): """Return circle diameter after applying transform matrix""" return compute_distance(*self.get_transformed_coords(0, 1)) #----AnnotatedShape API-------------------------------------------------[docs] def get_infos(self): """Return formatted string with informations on current shape""" return "<br>".join([ _("Center:")+" "+self.get_tr_center_str(), _("Diameter:")+" "+self.x_to_str(self.get_tr_diameter(), 2), ]) #----AnnotatedEllipse API---------------------------------------------------def set_rect(self, x0, y0, x1, y1): self.shape.set_rect(x0, y0, x1, y1)