# -*- coding: utf-8 -*-
#
# Copyright © 2009-2010 CEA
# Pierre Raybaut
# Licensed under the terms of the CECILL License
# (see guiqwt/__init__.py for details)

"""
guiqwt.shapes
-------------

The `shapes` module provides geometrical shapes:
    * :py:class:`guiqwt.shapes.PolygonShape`
    * :py:class:`guiqwt.shapes.RectangleShape`
    * :py:class:`guiqwt.shapes.ObliqueRectangleShape`
    * :py:class:`guiqwt.shapes.PointShape`
    * :py:class:`guiqwt.shapes.SegmentShape`
    * :py:class:`guiqwt.shapes.EllipseShape`
    * :py:class:`guiqwt.shapes.Axes`
    * :py:class:`guiqwt.shapes.XRangeSelection`

A 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.annotations`

Examples
~~~~~~~~

A shape may be created:
    * from the associated plot item class (e.g. `RectangleShape` to create a
      rectangle): the item properties are then assigned by creating the
      appropriate style parameters object
      (:py:class:`guiqwt.styles.ShapeParam`)

>>> from guiqwt.shapes import RectangleShape
>>> from guiqwt.styles import ShapeParam
>>> param = ShapeParam()
>>> param.title = 'My rectangle'
>>> rect_item = RectangleShape(0., 2., 4., 0., param)

    * or using the `plot item builder` (see :py:func:`guiqwt.builder.make`):

>>> from guiqwt.builder import make
>>> rect_item = make.rectangle(0., 2., 4., 0., title='My rectangle')

Reference
~~~~~~~~~

.. autoclass:: PolygonShape
   :members:
   :inherited-members:
.. autoclass:: RectangleShape
   :members:
   :inherited-members:
.. autoclass:: ObliqueRectangleShape
   :members:
   :inherited-members:
.. autoclass:: PointShape
   :members:
   :inherited-members:
.. autoclass:: SegmentShape
   :members:
   :inherited-members:
.. autoclass:: EllipseShape
   :members:
   :inherited-members:
.. autoclass:: Axes
   :members:
   :inherited-members:
.. autoclass:: XRangeSelection
   :members:
   :inherited-members:
"""

import os
import sys
from math import cos, fabs, pi, sin, sqrt

import numpy as np
from guidata.utils.misc import assert_interfaces_valid
from guidata.dataset import update_dataset
from qtpy.QtCore import QLineF, QPointF, QRectF, Qt
from qtpy.QtGui import QBrush, QPainter, QPen, QPolygonF, QTransform

from guiqwt.baseplot import canvas_to_axes
from guiqwt.config import CONF, _
from guiqwt.geometry import (
    compute_center,
    vector_norm,
    vector_projection,
    vector_rotation,
)
from guiqwt.interfaces import IBasePlotItem, ISerializableType, IShapeItemType
from guiqwt.styles import (
    MARKERSTYLES,
    AxesShapeParam,
    MarkerParam,
    RangeShapeParam,
    ShapeParam,
)

# Local imports
from guiqwt.transitional import QwtPlotItem, QwtPlotMarker, QwtSymbol

QT_API = os.environ["QT_API"]


class AbstractShape(QwtPlotItem):
    """Interface pour les objets manipulables
    il n'est pas nécessaire de dériver de QwtShape si on
    réutilise une autre classe dérivée de QwtPlotItem

    La classe de base
    """

    __implements__ = (IBasePlotItem,)

    _readonly = False
    _private = False
    _can_select = True
    _can_resize = True
    _can_rotate = False  # TODO: implement shape rotation?
    _can_move = True

    def __init__(self):
        super(AbstractShape, self).__init__()
        self.selected = False

    # ------IBasePlotItem API----------------------------------------------------
    def set_selectable(self, state):
        """Set item selectable state"""
        self._can_select = state

    def set_resizable(self, state):
        """Set item resizable state
        (or any action triggered when moving an handle, e.g. rotation)"""
        self._can_resize = state

    def set_movable(self, state):
        """Set item movable state"""
        self._can_move = state

    def set_rotatable(self, state):
        """Set item rotatable state"""
        self._can_rotate = state

    def can_select(self):
        return self._can_select

    def can_resize(self):
        return self._can_resize

    def can_rotate(self):
        return self._can_rotate

    def can_move(self):
        return self._can_move

    def types(self):
        """Returns a group or category for this item
        this should be a class object inheriting from
        IItemType
        """
        return (IShapeItemType,)

    def set_readonly(self, state):
        """Set object readonly state"""
        self._readonly = state

    def is_readonly(self):
        """Return object readonly state"""
        return self._readonly

    def set_private(self, state):
        """Set object as private"""
        self._private = state

    def is_private(self):
        """Return True if object is private"""
        return self._private

    def select(self):
        """Select item"""
        self.selected = True
        self.invalidate_plot()

    def unselect(self):
        """Unselect item"""
        self.selected = False
        self.invalidate_plot()

    def hit_test(self, pos):
        """
        Return a tuple with four elements:
        (distance, attach point, inside, other_object)

        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
        """
        pass

    def update_item_parameters(self):
        """
        Update item parameters (dataset) from object properties
        """
        pass

    def get_item_parameters(self, itemparams):
        """
        Appends datasets to the list of DataSets describing the parameters
        used to customize apearance of this item
        """
        pass

    def set_item_parameters(self, itemparams):
        """
        Change the appearance of this item according
        to the parameter set provided

        params is a list of Datasets of the same types as those returned
        by get_item_parameters
        """
        pass

    def move_local_point_to(self, handle, pos, ctrl=None):
        """Move a handle as returned by hit_test to the new position pos
        ctrl: True if <Ctrl> button is being pressed, False otherwise"""
        pt = canvas_to_axes(self, pos)
        self.move_point_to(handle, pt, ctrl)
        if self.plot():
            self.plot().SIG_ITEM_HANDLE_MOVED.emit(self)

    def move_local_shape(self, old_pos, new_pos):
        """Translate the shape such that old_pos becomes new_pos
        in canvas coordinates"""
        old_pt = canvas_to_axes(self, old_pos)
        new_pt = canvas_to_axes(self, new_pos)
        self.move_shape(old_pt, new_pt)
        if self.plot():
            self.plot().SIG_ITEM_MOVED.emit(self, *(old_pt + new_pt))

    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.move_shape([0, 0], [delta_x, delta_y])

    # ------Public API-----------------------------------------------------------
    def move_point_to(self, handle, pos, ctrl=None):
        pass

    def move_shape(self, old_pos, new_pos):
        """Translate the shape such that old_pos becomes new_pos
        in axis coordinates"""
        pass

    def invalidate_plot(self):
        plot = self.plot()
        if plot is not None:
            plot.invalidate()


assert_interfaces_valid(AbstractShape)


class Marker(QwtPlotMarker):
    """
    A marker that has two callbacks
    for restraining it's coordinates and
    displaying it's label
    we want to derive from QwtPlotMarker so
    we have to reimplement some of AbstractShape's methods
    (and PyQt doesn't really like multiple inheritance...)
    """

    __implements__ = (IBasePlotItem,)
    _readonly = False
    _private = False
    _can_select = True
    _can_resize = True
    _can_rotate = False
    _can_move = True

    def __init__(self, label_cb=None, constraint_cb=None, markerparam=None):
        super(Marker, self).__init__()
        self._pending_center_handle = None
        self.selected = False
        self.label_cb = label_cb
        if constraint_cb is None:
            constraint_cb = self.center_handle
        self.constraint_cb = constraint_cb
        if markerparam is None:
            self.markerparam = MarkerParam(_("Marker"))
            self.markerparam.read_config(CONF, "plot", "marker/cursor")
        else:
            self.markerparam = markerparam
        self.markerparam.update_marker(self)

    def __reduce__(self):
        self.markerparam.update_param(self)
        state = (self.markerparam, self.xValue(), self.yValue(), self.z())
        return (Marker, (), state)

    def __setstate__(self, state):
        self.markerparam, xvalue, yvalue, z = state
        self.setXValue(xvalue)
        self.setYValue(yvalue)
        self.setZ(z)
        self.markerparam.update_marker(self)

    def serialize(self, writer):
        """Serialize object to HDF5 writer"""
        self.markerparam.update_param(self)
        writer.write(self.markerparam, group_name="markerparam")
        writer.write(self.xValue(), group_name="x")
        writer.write(self.yValue(), group_name="y")
        writer.write(self.z(), group_name="z")

    def deserialize(self, reader):
        """Deserialize object from HDF5 reader"""
        self.markerparam = MarkerParam(_("Marker"), icon="marker.png")
        reader.read("markerparam", instance=self.markerparam)
        self.markerparam.update_marker(self)
        self.setXValue(reader.read("x"))
        self.setYValue(reader.read("y"))
        self.setZ(reader.read("z"))

    # ------QwtPlotItem API------------------------------------------------------
    def draw(self, painter, xmap, ymap, canvasrect):
        """Reimplemented to update label and (eventually) center handle"""
        if self._pending_center_handle:
            x, y = self.center_handle(self.xValue(), self.yValue())
            self.setValue(x, y)
        self.update_label()
        QwtPlotMarker.draw(self, painter, xmap, ymap, canvasrect)

    # ------IBasePlotItem API----------------------------------------------------
    def set_selectable(self, state):
        """Set item selectable state"""
        self._can_select = state

    def set_resizable(self, state):
        """Set item resizable state
        (or any action triggered when moving an handle, e.g. rotation)"""
        self._can_resize = state

    def set_movable(self, state):
        """Set item movable state"""
        self._can_move = state

    def set_rotatable(self, state):
        """Set item rotatable state"""
        self._can_rotate = state

    def can_select(self):
        return self._can_select

    def can_resize(self):
        return self._can_resize

    def can_rotate(self):
        return self._can_rotate

    def can_move(self):
        return self._can_move

    def types(self):
        """Returns a group or category for this item
        this should be a class object inheriting from
        IItemType
        """
        return (IShapeItemType,)

    def set_readonly(self, state):
        """Set object readonly state"""
        self._readonly = state

    def is_readonly(self):
        """Return object readonly state"""
        return self._readonly

    def set_private(self, state):
        """Set object as private"""
        self._private = state

    def is_private(self):
        """Return True if object is private"""
        return self._private

    def select(self):
        """
        Select the object and eventually change its appearance to highlight the
        fact that it's selected
        """
        if self.selected:
            # Already selected
            return
        self.selected = True
        self.markerparam.update_marker(self)
        self.invalidate_plot()

    def unselect(self):
        """
        Unselect the object and eventually restore its original appearance to
        highlight the fact that it's not selected anymore
        """
        self.selected = False
        self.markerparam.update_marker(self)
        self.invalidate_plot()

    def hit_test(self, pos):
        """
        Return a tuple with four elements:
        (distance, attach point, inside, other_object)

        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
        """
        plot = self.plot()
        xc, yc = pos.x(), pos.y()
        x = plot.transform(self.xAxis(), self.xValue())
        y = plot.transform(self.yAxis(), self.yValue())
        ms = self.markerparam.markerstyle
        # The following assert has no purpose except reminding that the
        # markerstyle is one of the MARKERSTYLES dictionary values, in case
        # this dictionary evolves in the future (this should not fail):
        assert ms in list(MARKERSTYLES.values())
        if ms == "NoLine":
            return sqrt((x - xc) ** 2 + (y - yc) ** 2), 0, False, None
        elif ms == "HLine":
            return sqrt((y - yc) ** 2), 0, False, None
        elif ms == "VLine":
            return sqrt((x - xc) ** 2), 0, False, None
        elif ms == "Cross":
            return sqrt(min((x - xc) ** 2, (y - yc) ** 2)), 0, False, None

    def update_item_parameters(self):
        self.markerparam.update_param(self)

    def get_item_parameters(self, itemparams):
        """
        Appends datasets to the list of DataSets describing the parameters
        used to customize apearance of this item
        """
        self.update_item_parameters()
        itemparams.add("MarkerParam", self, self.markerparam)

    def set_item_parameters(self, itemparams):
        """
        Change the appearance of this item according
        to the parameter set provided

        params is a list of Datasets of the same types as those returned
        by get_item_parameters
        """
        update_dataset(
            self.markerparam, itemparams.get("MarkerParam"), visible_only=True
        )
        self.markerparam.update_marker(self)
        if self.selected:
            self.select()

    def move_local_point_to(self, handle, pos, ctrl=None):
        """Move a handle as returned by hit_test to the new position pos
        ctrl: True if <Ctrl> button is being pressed, False otherwise"""
        x, y = canvas_to_axes(self, pos)
        self.set_pos(x, y)

    def move_local_shape(self, old_pos, new_pos):
        """Translate the shape such that old_pos becomes new_pos
        in canvas coordinates"""
        old_pt = canvas_to_axes(self, old_pos)
        new_pt = canvas_to_axes(self, new_pos)
        self.move_shape(old_pt, new_pt)

    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.move_shape([0, 0], [delta_x, delta_y])

    # ------Public API-----------------------------------------------------------
    def set_style(self, section, option):
        self.markerparam.read_config(CONF, section, option)
        self.markerparam.update_marker(self)

    def set_pos(self, x=None, y=None):
        if x is None:
            x = self.xValue()
        if y is None:
            y = self.yValue()
        if self.constraint_cb:
            x, y = self.constraint_cb(x, y)
        self.setValue(x, y)
        if self.plot():
            self.plot().SIG_MARKER_CHANGED.emit(self)

    def get_pos(self):
        return self.xValue(), self.yValue()

    def set_markerstyle(self, style):
        param = self.markerparam
        param.set_markerstyle(style)
        param.update_marker(self)

    def is_vertical(self):
        """Return True if this is a vertical cursor"""
        return self.lineStyle() == QwtPlotMarker.VLine

    def is_horizontal(self):
        """Return True if this is an horizontal cursor"""
        return self.lineStyle() == QwtPlotMarker.HLine

    def center_handle(self, x, y):
        """Center cursor handle depending on marker style (|, -)"""
        plot = self.plot()
        if plot is None:
            self._pending_center_handle = True
        else:
            self._pending_center_handle = False
            if self.is_vertical():
                ymap = plot.canvasMap(self.yAxis())
                y_top, y_bottom = ymap.s1(), ymap.s2()
                y = 0.5 * (y_top + y_bottom)
            elif self.is_horizontal():
                xmap = plot.canvasMap(self.xAxis())
                x_left, x_right = xmap.s1(), xmap.s2()
                x = 0.5 * (x_left + x_right)
        return x, y

    def move_shape(self, old_pos, new_pos):
        """Translate the shape such that old_pos becomes new_pos
        in canvas coordinates"""
        dx = new_pos[0] - old_pos[0]
        dy = new_pos[1] - old_pos[1]
        x, y = self.xValue(), self.yValue()
        return self.move_point_to(0, (x + dx, y + dy))

    def invalidate_plot(self):
        plot = self.plot()
        if plot is not None:
            plot.invalidate()

    def update_label(self):
        x, y = self.xValue(), self.yValue()
        if self.label_cb:
            label = self.label_cb(x, y)
            if label is None:
                return
        elif self.is_vertical():
            label = "x = %g" % x
        elif self.is_horizontal():
            label = "y = %g" % y
        else:
            label = "x = %g<br>y = %g" % (x, y)
        text = self.label()
        text.setText(label)
        self.setLabel(text)
        plot = self.plot()
        if plot is not None:
            xaxis = plot.axisScaleDiv(self.xAxis())
            if x < (xaxis.upperBound() + xaxis.lowerBound()) / 2:
                hor_alignment = Qt.AlignRight
            else:
                hor_alignment = Qt.AlignLeft
            yaxis = plot.axisScaleDiv(self.yAxis())
            ymap = plot.canvasMap(self.yAxis())
            y_top, y_bottom = ymap.s1(), ymap.s2()
            if y < 0.5 * (yaxis.upperBound() + yaxis.lowerBound()):
                if y_top > y_bottom:
                    ver_alignment = Qt.AlignBottom
                else:
                    ver_alignment = Qt.AlignTop
            else:
                if y_top > y_bottom:
                    ver_alignment = Qt.AlignTop
                else:
                    ver_alignment = Qt.AlignBottom
            self.setLabelAlignment(hor_alignment | ver_alignment)


assert_interfaces_valid(Marker)


class PolygonShape(AbstractShape):
    __implements__ = (IBasePlotItem, ISerializableType)
    ADDITIONNAL_POINTS = 0  # Number of points which are not part of the shape
    LINK_ADDITIONNAL_POINTS = False  # Link additionnal points with dotted lines
    CLOSED = True

    def __init__(self, points=None, closed=None, shapeparam=None):
        super(PolygonShape, self).__init__()
        self.closed = self.CLOSED if closed is None else closed
        self.selected = False

        if shapeparam is None:
            self.shapeparam = ShapeParam(_("Shape"), icon="rectangle.png")
        else:
            self.shapeparam = shapeparam
            self.shapeparam.update_shape(self)

        self.pen = QPen()
        self.brush = QBrush()
        self.symbol = QwtSymbol.NoSymbol
        self.sel_pen = QPen()
        self.sel_brush = QBrush()
        self.sel_symbol = QwtSymbol.NoSymbol
        self.points = np.zeros((0, 2), float)
        if points is not None:
            self.set_points(points)

    def types(self):
        return (IShapeItemType, ISerializableType)

    def __reduce__(self):
        self.shapeparam.update_param(self)
        state = (self.shapeparam, self.points, self.closed, self.z())
        return (PolygonShape, (), state)

    def __setstate__(self, state):
        self.shapeparam, self.points, self.closed, z = state
        self.setZ(z)
        self.shapeparam.update_shape(self)

    def serialize(self, writer):
        """Serialize object to HDF5 writer"""
        self.shapeparam.update_param(self)
        writer.write(self.shapeparam, group_name="shapeparam")
        writer.write(self.points, group_name="points")
        writer.write(self.closed, group_name="closed")
        writer.write(self.z(), group_name="z")

    def deserialize(self, reader):
        """Deserialize object from HDF5 reader"""
        self.closed = reader.read("closed")
        self.shapeparam = ShapeParam(_("Shape"), icon="rectangle.png")
        reader.read("shapeparam", instance=self.shapeparam)
        self.shapeparam.update_shape(self)
        self.points = reader.read(group_name="points", func=reader.read_array)
        self.setZ(reader.read("z"))

    # ----Public API-------------------------------------------------------------

    def set_style(self, section, option):
        self.shapeparam.read_config(CONF, section, option)
        self.shapeparam.update_shape(self)

    def set_points(self, points):
        self.points = np.array(points, float)
        assert self.points.shape[1] == 2

    def get_points(self):
        """Return polygon points"""
        return self.points

    def get_bounding_rect_coords(self):
        """Return bounding rectangle coordinates (in plot coordinates)"""
        poly = QPolygonF()
        shape_points = self.points[: -self.ADDITIONNAL_POINTS]
        for i in range(shape_points.shape[0]):
            poly.append(QPointF(shape_points[i, 0], shape_points[i, 1]))
        return poly.boundingRect().getCoords()

    def transform_points(self, xMap, yMap):
        points = QPolygonF()
        for i in range(self.points.shape[0]):
            points.append(
                QPointF(
                    xMap.transform(self.points[i, 0]), yMap.transform(self.points[i, 1])
                )
            )
        return points

    def get_reference_point(self):
        if self.points.size:
            return self.points.mean(axis=0)

    def get_pen_brush(self, xMap, yMap):
        if self.selected:
            pen = self.sel_pen
            brush = self.sel_brush
            sym = self.sel_symbol
        else:
            pen = self.pen
            brush = self.brush
            sym = self.symbol
        if self.points.size > 0:
            x0, y0 = self.get_reference_point()
            xx0 = xMap.transform(x0)
            yy0 = yMap.transform(y0)
            try:
                # Optimized version in PyQt >= v4.5
                t0 = QTransform.fromTranslate(xx0, yy0)
            except AttributeError:
                # Fallback for PyQt <= v4.4
                t0 = QTransform().translate(xx0, yy0)
            tr = brush.transform()
            tr = tr * t0
            brush = QBrush(brush)
            brush.setTransform(tr)
        return pen, brush, sym

    def draw(self, painter, xMap, yMap, canvasRect):
        pen, brush, symbol = self.get_pen_brush(xMap, yMap)
        painter.setRenderHint(QPainter.Antialiasing)
        painter.setPen(pen)
        painter.setBrush(brush)
        points = self.transform_points(xMap, yMap)
        if QT_API.startswith("pyside"):
            points = list(points)
        if self.ADDITIONNAL_POINTS:
            shape_points = points[: -self.ADDITIONNAL_POINTS]
            other_points = points[-self.ADDITIONNAL_POINTS :]
        else:
            shape_points = points
            other_points = []
        if self.closed:
            painter.drawPolygon(shape_points)
        else:
            painter.drawPolyline(shape_points)
        if symbol != QwtSymbol.NoSymbol:
            symbol.drawSymbols(painter, points)
        if self.LINK_ADDITIONNAL_POINTS and other_points:
            pen2 = painter.pen()
            pen2.setStyle(Qt.DotLine)
            painter.setPen(pen2)
            painter.drawPolyline(other_points)

    def poly_hit_test(self, plot, ax, ay, pos):
        pos = QPointF(pos)
        dist = sys.maxsize
        handle = -1
        Cx, Cy = pos.x(), pos.y()
        poly = QPolygonF()
        pts = self.points
        for i in range(pts.shape[0]):
            # On calcule la distance dans le repère du canvas
            px = plot.transform(ax, pts[i, 0])
            py = plot.transform(ay, pts[i, 1])
            if i < pts.shape[0] - self.ADDITIONNAL_POINTS:
                poly.append(QPointF(px, py))
            d = (Cx - px) ** 2 + (Cy - py) ** 2
            if d < dist:
                dist = d
                handle = i
        inside = poly.containsPoint(QPointF(Cx, Cy), Qt.OddEvenFill)
        return sqrt(dist), handle, inside, None

    def hit_test(self, pos):
        """return (dist, handle, inside)"""
        if not self.plot():
            return sys.maxsize, 0, False, None
        return self.poly_hit_test(self.plot(), self.xAxis(), self.yAxis(), pos)

    def add_local_point(self, pos):
        pt = canvas_to_axes(self, pos)
        return self.add_point(pt)

    def add_point(self, pt):
        N, _ = self.points.shape
        self.points = np.resize(self.points, (N + 1, 2))
        self.points[N, :] = pt
        return N

    def del_point(self, handle):
        self.points = np.delete(self.points, handle, 0)
        if handle < len(self.points):
            return handle
        else:
            return self.points.shape[0] - 1

    def move_point_to(self, handle, pos, ctrl=None):
        self.points[handle, :] = pos

    def move_shape(self, old_pos, new_pos):
        dx = new_pos[0] - old_pos[0]
        dy = new_pos[1] - old_pos[1]
        self.points += np.array([[dx, dy]])

    def update_item_parameters(self):
        self.shapeparam.update_param(self)

    def get_item_parameters(self, itemparams):
        self.update_item_parameters()
        itemparams.add("ShapeParam", self, self.shapeparam)

    def set_item_parameters(self, itemparams):
        update_dataset(self.shapeparam, itemparams.get("ShapeParam"), visible_only=True)
        self.shapeparam.update_shape(self)


assert_interfaces_valid(PolygonShape)


class PointShape(PolygonShape):
    CLOSED = False

    def __init__(self, x=0, y=0, shapeparam=None):
        super(PointShape, self).__init__(shapeparam=shapeparam)
        self.set_pos(x, y)

    def set_pos(self, x, y):
        """Set the point coordinates to (x, y)"""
        self.set_points([(x, y)])

    def get_pos(self):
        """Return the point coordinates"""
        return tuple(self.points[0])

    def move_point_to(self, handle, pos, ctrl=None):
        nx, ny = pos
        self.points[0] = (nx, ny)

    def __reduce__(self):
        state = (self.shapeparam, self.points, self.z())
        return (self.__class__, (), state)

    def __setstate__(self, state):
        self.shapeparam, self.points, z = state
        self.setZ(z)
        self.shapeparam.update_shape(self)


assert_interfaces_valid(PointShape)


class SegmentShape(PolygonShape):
    CLOSED = False
    ADDITIONNAL_POINTS = 1  # Number of points which are not part of the shape

    def __init__(self, x1=0, y1=0, x2=0, y2=0, shapeparam=None):
        super(SegmentShape, self).__init__(shapeparam=shapeparam)
        self.set_rect(x1, y1, x2, y2)

    def set_rect(self, x1, y1, x2, y2):
        """
        Set the start point of this segment to (x1, y1)
        and the end point of this line to (x2, y2)
        """
        self.set_points([(x1, y1), (x2, y2), (0.5 * (x1 + x2), 0.5 * (y1 + y2))])

    def get_rect(self):
        return tuple(self.points[0]) + tuple(self.points[1])

    def move_point_to(self, handle, pos, ctrl=None):
        nx, ny = pos
        x1, y1, x2, y2 = self.get_rect()
        if handle == 0:
            self.set_rect(nx, ny, x2, y2)
        elif handle == 1:
            self.set_rect(x1, y1, nx, ny)
        elif handle in (2, -1):
            delta = (nx, ny) - self.points.mean(axis=0)
            self.points += delta

    def __reduce__(self):
        state = (self.shapeparam, self.points, self.z())
        return (self.__class__, (), state)

    def __setstate__(self, state):
        param, points, z = state
        # ----------------------------------------------------------------------
        # compatibility with previous version of SegmentShape:
        x1, y1, x2, y2, x3, y3 = points.ravel()
        v12 = np.array((x2 - x1, y2 - y1))
        v13 = np.array((x3 - x1, y3 - y1))
        if np.linalg.norm(v12) < np.linalg.norm(v13):
            # old pickle format
            points = np.flipud(np.roll(points, -1, axis=0))
        # ----------------------------------------------------------------------
        self.points = points
        self.setZ(z)
        self.shapeparam = param
        self.shapeparam.update_shape(self)


assert_interfaces_valid(SegmentShape)


class RectangleShape(PolygonShape):
    CLOSED = True

    def __init__(self, x1=0, y1=0, x2=0, y2=0, shapeparam=None):
        super(RectangleShape, self).__init__(shapeparam=shapeparam)
        self.set_rect(x1, y1, x2, y2)

    def set_rect(self, x1, y1, x2, y2):
        """
        Set the coordinates of the rectangle's top-left corner to (x1, y1),
        and of its bottom-right corner to (x2, y2).
        """
        self.set_points([(x1, y1), (x2, y1), (x2, y2), (x1, y2)])

    def get_rect(self):
        return tuple(self.points[0]) + tuple(self.points[2])

    def get_center(self):
        """Return center coordinates: (xc, yc)"""
        return compute_center(*self.get_rect())

    def move_point_to(self, handle, pos, ctrl=None):
        nx, ny = pos
        x1, y1, x2, y2 = self.get_rect()
        if handle == 0:
            self.set_rect(nx, ny, x2, y2)
        elif handle == 1:
            self.set_rect(x1, ny, nx, y2)
        elif handle == 2:
            self.set_rect(x1, y1, nx, ny)
        elif handle == 3:
            self.set_rect(nx, y1, x2, ny)
        elif handle == -1:
            delta = (nx, ny) - self.points.mean(axis=0)
            self.points += delta

    def __reduce__(self):
        state = (self.shapeparam, self.points, self.z())
        return (self.__class__, (), state)

    def __setstate__(self, state):
        self.shapeparam, self.points, z = state
        self.setZ(z)
        self.shapeparam.update_shape(self)


assert_interfaces_valid(RectangleShape)


def _no_null_vector(x0, y0, x1, y1, x2, y2, x3, y3):
    return (
        vector_norm(x0, y0, x1, y1)
        and vector_norm(x0, y0, x2, y2)
        and vector_norm(x0, y0, x3, y3)
        and vector_norm(x1, y1, x2, y2)
        and vector_norm(x1, y1, x3, y3)
        and vector_norm(x2, y2, x3, y3)
    )


class ObliqueRectangleShape(PolygonShape):
    CLOSED = True
    ADDITIONNAL_POINTS = 2  # Number of points which are not part of the shape
    LINK_ADDITIONNAL_POINTS = True  # Link additionnal points with dotted lines

    def __init__(self, x0=0, y0=0, x1=0, y1=0, x2=0, y2=0, x3=0, y3=0, shapeparam=None):
        super(ObliqueRectangleShape, self).__init__(shapeparam=shapeparam)
        self.set_rect(x0, y0, x1, y1, x2, y2, x3, y3)

    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 (handles used for rotation -- other handles
            being used for rectangle resizing)

            (x0, y0)------>(x1, y1)
                ↑             |
                |             |
                x             x
                |             |
                |             ↓
            (x3, y3)<------(x2, y2)
        """
        self.set_points(
            [
                (x0, y0),
                (x1, y1),
                (x2, y2),
                (x3, y3),
                (0.5 * (x0 + x3), 0.5 * (y0 + y3)),
                (0.5 * (x1 + x2), 0.5 * (y1 + y2)),
            ]
        )

    def get_rect(self):
        return self.points.ravel()[: -self.ADDITIONNAL_POINTS * 2]

    def get_center(self):
        """Return center coordinates: (xc, yc)"""
        rect = tuple(self.points[0]) + tuple(self.points[2])
        return compute_center(*rect)

    def move_point_to(self, handle, pos, ctrl=None):
        nx, ny = pos
        x0, y0, x1, y1, x2, y2, x3, y3 = self.get_rect()
        if handle == 0:
            if vector_norm(x2, y2, x3, y3) and vector_norm(x2, y2, x1, y1):
                v0n = np.array((nx - x0, ny - y0))
                x3, y3 = vector_projection(v0n, x2, y2, x3, y3)
                x1, y1 = vector_projection(v0n, x2, y2, x1, y1)
                x0, y0 = nx, ny
                if _no_null_vector(x0, y0, x1, y1, x2, y2, x3, y3):
                    self.set_rect(x0, y0, x1, y1, x2, y2, x3, y3)
        elif handle == 1:
            if vector_norm(x3, y3, x0, y0) and vector_norm(x3, y3, x2, y2):
                v1n = np.array((nx - x1, ny - y1))
                x0, y0 = vector_projection(v1n, x3, y3, x0, y0)
                x2, y2 = vector_projection(v1n, x3, y3, x2, y2)
                x1, y1 = nx, ny
                if _no_null_vector(x0, y0, x1, y1, x2, y2, x3, y3):
                    self.set_rect(x0, y0, x1, y1, x2, y2, x3, y3)
        elif handle == 2:
            if vector_norm(x0, y0, x1, y1) and vector_norm(x0, y0, x3, y3):
                v2n = np.array((nx - x2, ny - y2))
                x1, y1 = vector_projection(v2n, x0, y0, x1, y1)
                x3, y3 = vector_projection(v2n, x0, y0, x3, y3)
                x2, y2 = nx, ny
                if _no_null_vector(x0, y0, x1, y1, x2, y2, x3, y3):
                    self.set_rect(x0, y0, x1, y1, x2, y2, x3, y3)
        elif handle == 3:
            if vector_norm(x1, y1, x0, y0) and vector_norm(x1, y1, x2, y2):
                v3n = np.array((nx - x3, ny - y3))
                x0, y0 = vector_projection(v3n, x1, y1, x0, y0)
                x2, y2 = vector_projection(v3n, x1, y1, x2, y2)
                x3, y3 = nx, ny
                if _no_null_vector(x0, y0, x1, y1, x2, y2, x3, y3):
                    self.set_rect(x0, y0, x1, y1, x2, y2, x3, y3)
        elif handle == 4:
            x4, y4 = 0.5 * (x0 + x3), 0.5 * (y0 + y3)
            x5, y5 = 0.5 * (x1 + x2), 0.5 * (y1 + y2)
            nx, ny = x0 + nx - x4, y0 + ny - y4  # moving handle #4 to handle #0

            v10 = np.array((x0 - x1, y0 - y1))
            v12 = np.array((x2 - x1, y2 - y1))
            v10n = np.array((nx - x1, ny - y1))
            k = np.linalg.norm(v12) / np.linalg.norm(v10)
            v12n = vector_rotation(-np.pi / 2, *v10n) * k
            x2, y2 = v12n + np.array([x1, y1])
            x3, y3 = v12n + v10n + np.array([x1, y1])
            x0, y0 = nx, ny

            dx = x5 - 0.5 * (x1 + x2)
            dy = y5 - 0.5 * (y1 + y2)
            x0, y0 = x0 + dx, y0 + dy
            x1, y1 = x1 + dx, y1 + dy
            x2, y2 = x2 + dx, y2 + dy
            x3, y3 = x3 + dx, y3 + dy
            self.set_rect(x0, y0, x1, y1, x2, y2, x3, y3)
        elif handle == 5:
            x4, y4 = 0.5 * (x0 + x3), 0.5 * (y0 + y3)
            x5, y5 = 0.5 * (x1 + x2), 0.5 * (y1 + y2)
            nx, ny = x1 + nx - x5, y1 + ny - y5  # moving handle #5 to handle #1

            v01 = np.array((x1 - x0, y1 - y0))
            v03 = np.array((x3 - x0, y3 - y0))
            v01n = np.array((nx - x0, ny - y0))
            k = np.linalg.norm(v03) / np.linalg.norm(v01)
            v03n = vector_rotation(np.pi / 2, *v01n) * k
            x3, y3 = v03n + np.array([x0, y0])
            x2, y2 = v03n + v01n + np.array([x0, y0])
            x1, y1 = nx, ny

            dx = x4 - 0.5 * (x0 + x3)
            dy = y4 - 0.5 * (y0 + y3)
            x0, y0 = x0 + dx, y0 + dy
            x1, y1 = x1 + dx, y1 + dy
            x2, y2 = x2 + dx, y2 + dy
            x3, y3 = x3 + dx, y3 + dy
            self.set_rect(x0, y0, x1, y1, x2, y2, x3, y3)
        elif handle == -1:
            delta = (nx, ny) - self.points.mean(axis=0)
            self.points += delta

    def __reduce__(self):
        state = (self.shapeparam, self.points, self.z())
        return (self.__class__, (), state)

    def __setstate__(self, state):
        self.shapeparam, self.points, z = state
        self.setZ(z)
        self.shapeparam.update_shape(self)


assert_interfaces_valid(ObliqueRectangleShape)


# FIXME: EllipseShape's ellipse drawing is invalid when aspect_ratio != 1
class EllipseShape(PolygonShape):
    CLOSED = True

    def __init__(self, x1=0, y1=0, x2=0, y2=0, shapeparam=None):
        super(EllipseShape, self).__init__(shapeparam=shapeparam)
        self.is_ellipse = False
        self.set_xdiameter(x1, y1, x2, y2)

    def switch_to_ellipse(self):
        self.is_ellipse = True

    def switch_to_circle(self):
        self.is_ellipse = False

    def set_xdiameter(self, x0, y0, x1, y1):
        """Set the coordinates of the ellipse's X-axis diameter"""
        xline = QLineF(x0, y0, x1, y1)
        yline = xline.normalVector()
        yline.translate(xline.pointAt(0.5) - xline.p1())
        if self.is_ellipse:
            yline.setLength(self.get_yline().length())
        else:
            yline.setLength(xline.length())
        yline.translate(yline.pointAt(0.5) - yline.p2())
        self.set_points(
            [(x0, y0), (x1, y1), (yline.x1(), yline.y1()), (yline.x2(), yline.y2())]
        )

    def get_xdiameter(self):
        """Return the coordinates of the ellipse's X-axis diameter"""
        return tuple(self.points[0]) + tuple(self.points[1])

    def set_ydiameter(self, x2, y2, x3, y3):
        """Set the coordinates of the ellipse's Y-axis diameter"""
        yline = QLineF(x2, y2, x3, y3)
        xline = yline.normalVector()
        xline.translate(yline.pointAt(0.5) - yline.p1())
        if self.is_ellipse:
            xline.setLength(self.get_xline().length())
        xline.translate(xline.pointAt(0.5) - xline.p2())
        self.set_points(
            [(xline.x2(), xline.y2()), (xline.x1(), xline.y1()), (x2, y2), (x3, y3)]
        )

    def get_ydiameter(self):
        """Return the coordinates of the ellipse's Y-axis diameter"""
        return tuple(self.points[2]) + tuple(self.points[3])

    def get_rect(self):
        """Circle only!"""
        (x0, y0), (x1, y1) = self.points[0], self.points[1]
        xc, yc = 0.5 * (x0 + x1), 0.5 * (y0 + y1)
        radius = 0.5 * np.sqrt((x1 - x0) ** 2 + (y1 - y0) ** 2)
        return xc - radius, yc - radius, xc + radius, yc + radius

    def get_center(self):
        """Return center coordinates: (xc, yc)"""
        return compute_center(*self.get_xdiameter())

    def set_rect(self, x0, y0, x1, y1):
        """Circle only!"""
        self.set_xdiameter(x0, 0.5 * (y0 + y1), x1, 0.5 * (y0 + y1))

    def compute_elements(self, xMap, yMap):
        """Return points, lines and ellipse rect"""
        points = self.transform_points(xMap, yMap)
        line0 = QLineF(points[0], points[1])
        line1 = QLineF(points[2], points[3])
        rect = QRectF()
        rect.setWidth(line0.length())
        rect.setHeight(line1.length())
        rect.moveCenter(line0.pointAt(0.5))
        return points, line0, line1, rect

    def hit_test(self, pos):
        """return (dist, handle, inside)"""
        if not self.plot():
            return sys.maxsize, 0, False, None
        dist, handle, inside, other = self.poly_hit_test(
            self.plot(), self.xAxis(), self.yAxis(), pos
        )
        if not inside:
            xMap = self.plot().canvasMap(self.xAxis())
            yMap = self.plot().canvasMap(self.yAxis())
            _points, _line0, _line1, rect = self.compute_elements(xMap, yMap)
            inside = rect.contains(QPointF(pos))
        return dist, handle, inside, other

    def draw(self, painter, xMap, yMap, canvasRect):
        points, line0, line1, rect = self.compute_elements(xMap, yMap)
        pen, brush, symbol = self.get_pen_brush(xMap, yMap)
        painter.setRenderHint(QPainter.Antialiasing)
        painter.setPen(pen)
        painter.setBrush(brush)
        painter.drawLine(line0)
        painter.drawLine(line1)
        painter.save()
        painter.translate(rect.center())
        painter.rotate(-line0.angle())
        painter.translate(-rect.center())
        painter.drawEllipse(rect.toRect())
        painter.restore()
        if symbol != QwtSymbol.NoSymbol:
            for i in range(points.size()):
                symbol.drawSymbol(painter, points[i].toPoint())

    def get_xline(self):
        return QLineF(*(tuple(self.points[0]) + tuple(self.points[1])))

    def get_yline(self):
        return QLineF(*(tuple(self.points[2]) + tuple(self.points[3])))

    def move_point_to(self, handle, pos, ctrl=None):
        nx, ny = pos
        if handle == 0:
            x1, y1 = self.points[1]
            if ctrl:
                # When <Ctrl> is pressed, the center position is unchanged
                x0, y0 = self.points[0]
                x1, y1 = x1 + x0 - nx, y1 + y0 - ny
            self.set_xdiameter(nx, ny, x1, y1)
        elif handle == 1:
            x0, y0 = self.points[0]
            if ctrl:
                # When <Ctrl> is pressed, the center position is unchanged
                x1, y1 = self.points[1]
                x0, y0 = x0 + x1 - nx, y0 + y1 - ny
            self.set_xdiameter(x0, y0, nx, ny)
        elif handle == 2:
            x3, y3 = self.points[3]
            if ctrl:
                # When <Ctrl> is pressed, the center position is unchanged
                x2, y2 = self.points[2]
                x3, y3 = x3 + x2 - nx, y3 + y2 - ny
            self.set_ydiameter(nx, ny, x3, y3)
        elif handle == 3:
            x2, y2 = self.points[2]
            if ctrl:
                # When <Ctrl> is pressed, the center position is unchanged
                x3, y3 = self.points[3]
                x2, y2 = x2 + x3 - nx, y2 + y3 - ny
            self.set_ydiameter(x2, y2, nx, ny)
        elif handle == -1:
            delta = (nx, ny) - self.points.mean(axis=0)
            self.points += delta

    def __reduce__(self):
        state = (self.shapeparam, self.points, self.z())
        return (self.__class__, (), state)

    def __setstate__(self, state):
        self.shapeparam, self.points, z = state
        self.setZ(z)
        self.shapeparam.update_shape(self)


assert_interfaces_valid(EllipseShape)


class Axes(PolygonShape):
    """Axes( (0,1), (1,1), (0,0) )"""

    CLOSED = True

    def __init__(
        self, p0=(0, 0), p1=(0, 0), p2=(0, 0), axesparam=None, shapeparam=None
    ):
        super(Axes, self).__init__(shapeparam=shapeparam)
        self.set_rect(p0, p1, p2)
        self.arrow_angle = 15  # degrees
        self.arrow_size = 0.05  # % of axe length
        self.x_pen = self.pen
        self.x_brush = self.brush
        self.y_pen = self.pen
        self.y_brush = self.brush
        if axesparam is None:
            self.axesparam = AxesShapeParam(_("Axes"), icon="gtaxes.png")
        else:
            self.axesparam = axesparam
        self.axesparam.update_param(self)

    def __reduce__(self):
        self.axesparam.update_param(self)
        state = (self.shapeparam, self.axesparam, self.points, self.z())
        return (self.__class__, (), state)

    def __setstate__(self, state):
        shapeparam, axesparam, points, z = state
        self.points = points
        self.setZ(z)
        self.shapeparam = shapeparam
        self.shapeparam.update_shape(self)
        self.axesparam = axesparam
        self.axesparam.update_axes(self)

    def serialize(self, writer):
        """Serialize object to HDF5 writer"""
        super(Axes, self).serialize(writer)
        self.axesparam.update_param(self)
        writer.write(self.axesparam, group_name="axesparam")

    def deserialize(self, reader):
        """Deserialize object from HDF5 reader"""
        super(Axes, self).deserialize(reader)
        self.axesparam = AxesShapeParam(_("Axes"), icon="gtaxes.png")
        reader.read("axesparam", instance=self.axesparam)
        self.axesparam.update_axes(self)

    def get_transform_matrix(self, dx=1.0, dy=1.0):
        p0, p1, _p3, p2 = [np.array([p[0], p[1], 1.0]) for p in self.points]
        matrix = np.array([(p1 - p0) / dx, (p2 - p0) / dy, p0])
        if abs(np.linalg.det(matrix)) > 1e-12:
            return np.linalg.inv(matrix)

    def set_rect(self, p0, p1, p2):
        p3x = p1[0] + p2[0] - p0[0]
        p3y = p1[1] + p2[1] - p0[1]
        self.set_points([p0, p1, (p3x, p3y), p2])

    def set_style(self, section, option):
        PolygonShape.set_style(self, section, option + "/border")
        self.axesparam.read_config(CONF, section, option)
        self.axesparam.update_axes(self)

    def move_point_to(self, handle, pos, ctrl=None):
        _nx, _ny = pos
        p0, p1, _p3, p2 = list(self.points)
        d1x = p1[0] - p0[0]
        d1y = p1[1] - p0[1]
        d2x = p2[0] - p0[0]
        d2y = p2[1] - p0[1]
        if handle == 0:
            pp0 = pos
            pp1 = pos[0] + d1x, pos[1] + d1y
            pp2 = pos[0] + d2x, pos[1] + d2y
        elif handle == 1:
            pp0 = p0
            pp1 = pos
            pp2 = p2
        elif handle == 3:
            pp0 = p0
            pp1 = p1
            pp2 = pos
        elif handle == 2:
            # find (a,b) such that p3 = a*d1 + b*d2 + p0
            d3x = pos[0] - p0[0]
            d3y = pos[1] - p0[1]
            det = d1x * d2y - d2x * d1y
            if abs(det) < 1e-6:
                # reset
                d1x = d2y = 1.0
                d1y = d2x = 0.0
                det = 1.0
            a = (d2y * d3x - d2x * d3y) / det
            b = (-d1y * d3x + d1x * d3y) / det
            _pp3 = pos
            pp1 = p0[0] + a * d1x, p0[1] + a * d1y
            pp2 = p0[0] + b * d2x, p0[1] + b * d2y
            pp0 = p0
        self.set_rect(pp0, pp1, pp2)
        if self.plot():
            self.plot().SIG_AXES_CHANGED.emit(self)

    def move_shape(self, old_pos, new_pos):
        """Overriden to emit the axes_changed signal"""
        PolygonShape.move_shape(self, old_pos, new_pos)
        if self.plot():
            self.plot().SIG_AXES_CHANGED.emit(self)

    def draw(self, painter, xMap, yMap, canvasRect):
        PolygonShape.draw(self, painter, xMap, yMap, canvasRect)
        p0, p1, _, p2 = list(self.points)

        painter.setPen(self.x_pen)
        painter.setBrush(self.x_brush)
        self.draw_arrow(painter, xMap, yMap, p0, p1)
        painter.setPen(self.y_pen)
        painter.setBrush(self.y_brush)
        self.draw_arrow(painter, xMap, yMap, p0, p2)

    def draw_arrow(self, painter, xMap, yMap, p0, p1):
        sz = self.arrow_size
        angle = pi * self.arrow_angle / 180.0
        ca, sa = cos(angle), sin(angle)
        d1x = xMap.transform(p1[0]) - xMap.transform(p0[0])
        d1y = yMap.transform(p1[1]) - yMap.transform(p0[1])
        norm = sqrt(d1x**2 + d1y**2)
        if abs(norm) < 1e-6:
            return
        d1x *= sz / norm
        d1y *= sz / norm
        n1x = -d1y
        n1y = d1x
        # arrow : a0 - a1 == p1 - a2
        a1x = xMap.transform(p1[0])
        a1y = yMap.transform(p1[1])
        a0x = a1x - ca * d1x + sa * n1x
        a0y = a1y - ca * d1y + sa * n1y
        a2x = a1x - ca * d1x - sa * n1x
        a2y = a1y - ca * d1y - sa * n1y

        poly = QPolygonF()
        poly.append(QPointF(a0x, a0y))
        poly.append(QPointF(a1x, a1y))
        poly.append(QPointF(a2x, a2y))
        painter.drawPolygon(poly)

    def update_item_parameters(self):
        self.axesparam.update_param(self)

    def get_item_parameters(self, itemparams):
        PolygonShape.get_item_parameters(self, itemparams)
        self.update_item_parameters()
        itemparams.add("AxesShapeParam", self, self.axesparam)

    def set_item_parameters(self, itemparams):
        PolygonShape.set_item_parameters(self, itemparams)
        update_dataset(
            self.axesparam, itemparams.get("AxesShapeParam"), visible_only=True
        )
        self.axesparam.update_axes(self)


assert_interfaces_valid(Axes)


class XRangeSelection(AbstractShape):
    def __init__(self, _min, _max, shapeparam=None):
        super(XRangeSelection, self).__init__()
        self._min = _min
        self._max = _max
        if shapeparam is None:
            self.shapeparam = RangeShapeParam(_("Range"), icon="xrange.png")
            self.shapeparam.read_config(CONF, "histogram", "range")
        else:
            self.shapeparam = shapeparam
        self.pen = None
        self.sel_pen = None
        self.brush = None
        self.handle = None
        self.symbol = None
        self.sel_symbol = None
        self.shapeparam.update_range(self)  # creates all the above QObjects

    def get_handles_pos(self):
        plot = self.plot()
        rct = plot.canvas().contentsRect()
        y = rct.center().y()
        x0 = plot.transform(self.xAxis(), self._min)
        x1 = plot.transform(self.xAxis(), self._max)
        return x0, x1, y

    def draw(self, painter, xMap, yMap, canvasRect):
        plot = self.plot()
        if not plot:
            return
        if self.selected:
            pen = self.sel_pen
            sym = self.sel_symbol
        else:
            pen = self.pen
            sym = self.symbol

        rct = plot.canvas().contentsRect()
        rct2 = QRectF(rct)
        rct2.setLeft(xMap.transform(self._min))
        rct2.setRight(xMap.transform(self._max))

        painter.fillRect(rct2, self.brush)
        painter.setPen(pen)
        painter.drawLine(rct2.topLeft(), rct2.bottomLeft())
        painter.drawLine(rct2.topRight(), rct2.bottomRight())
        dash = QPen(pen)
        dash.setStyle(Qt.DashLine)
        dash.setWidth(1)
        painter.setPen(dash)
        painter.drawLine(
            QLineF(rct2.center().x(), rct2.top(), rct2.center().x(), rct2.bottom())
        )
        painter.setPen(pen)
        x0, x1, y = self.get_handles_pos()
        sym.drawSymbol(painter, QPointF(x0, y))
        sym.drawSymbol(painter, QPointF(x1, y))

    def hit_test(self, pos):
        x, _y = pos.x(), pos.y()
        x0, x1, _yp = self.get_handles_pos()
        d0 = fabs(x0 - x)
        d1 = fabs(x1 - x)
        d2 = fabs((x0 + x1) / 2 - x)
        z = np.array([d0, d1, d2])
        dist = z.min()
        handle = z.argmin()
        inside = bool(x0 < x < x1)
        return dist, handle, inside, None

    def move_local_point_to(self, handle, pos, ctrl=None):
        """Move a handle as returned by hit_test to the new position pos
        ctrl: True if <Ctrl> button is being pressed, False otherwise"""
        x, _y = canvas_to_axes(self, pos)
        self.move_point_to(handle, (x, 0))

    def move_point_to(self, hnd, pos, ctrl=None):
        val, _ = pos
        if hnd == 0:
            self._min = val
        elif hnd == 1:
            self._max = val
        elif hnd == 2:
            move = val - (self._max + self._min) / 2
            self._min += move
            self._max += move

        self.plot().SIG_RANGE_CHANGED.emit(self, self._min, self._max)
        # self.plot().replot()

    def get_range(self):
        return self._min, self._max

    def set_range(self, _min, _max, dosignal=True):
        self._min = _min
        self._max = _max
        if dosignal:
            self.plot().SIG_RANGE_CHANGED.emit(self, self._min, self._max)

    def move_shape(self, old_pos, new_pos):
        dx = new_pos[0] - old_pos[0]
        self._min += dx
        self._max += dx
        self.plot().SIG_RANGE_CHANGED.emit(self, self._min, self._max)
        self.plot().replot()

    def update_item_parameters(self):
        self.shapeparam.update_param(self)

    def get_item_parameters(self, itemparams):
        self.update_item_parameters()
        itemparams.add("ShapeParam", self, self.shapeparam)

    def set_item_parameters(self, itemparams):
        update_dataset(self.shapeparam, itemparams.get("ShapeParam"), visible_only=True)
        self.shapeparam.update_range(self)
        self.sel_brush = QBrush(self.brush)


assert_interfaces_valid(XRangeSelection)
