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

# pylint: disable=C0103

"""
guiqwt.curve
------------

The `curve` module provides curve-related objects:
    * :py:class:`guiqwt.curve.CurvePlot`: a 2d curve plotting widget
    * :py:class:`guiqwt.curve.CurveItem`: a curve plot item
    * :py:class:`guiqwt.curve.ErrorBarCurveItem`: a curve plot item with 
      error bars
    * :py:class:`guiqwt.curve.GridItem`
    * :py:class:`guiqwt.curve.ItemListWidget`: base widget implementing the 
      `plot item list panel`
    * :py:class:`guiqwt.curve.PlotItemList`: the `plot item list panel`

``CurveItem`` and ``GridItem`` objects are plot items (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.image`
        Module providing image-related plot items and plotting widgets
        
    Module :py:mod:`guiqwt.plot`
        Module providing ready-to-use curve and image plotting widgets and 
        dialog boxes

Examples
~~~~~~~~

Create a basic curve plotting widget:
    * before creating any widget, a `QApplication` must be instantiated (that 
      is a `Qt` internal requirement):
          
>>> import guidata
>>> app = guidata.qapplication()

    * that is mostly equivalent to the following (the only difference is that 
      the `guidata` helper function also installs the `Qt` translation 
      corresponding to the system locale):
          
>>> from PyQt4.QtGui import QApplication
>>> app = QApplication([])

    * now that a `QApplication` object exists, we may create the plotting 
      widget:
          
>>> from guiqwt.curve import CurvePlot
>>> plot = CurvePlot(title="Example", xlabel="X", ylabel="Y")

Create a curve item:
    * from the associated plot item class (e.g. `ErrorBarCurveItem` to 
      create a curve with error bars): the item properties are then assigned 
      by creating the appropriate style parameters object
      (e.g. :py:class:`guiqwt.styles.ErrorBarParam`)
      
>>> from guiqwt.curve import CurveItem
>>> from guiqwt.styles import CurveParam
>>> param = CurveParam()
>>> param.label = 'My curve'
>>> curve = CurveItem(param)
>>> curve.set_data(x, y)
      
    * or using the `plot item builder` (see :py:func:`guiqwt.builder.make`):
      
>>> from guiqwt.builder import make
>>> curve = make.curve(x, y, title='My curve')

Attach the curve to the plotting widget:
    
>>> plot.add_item(curve)

Display the plotting widget:
    
>>> plot.show()
>>> app.exec_()

Reference
~~~~~~~~~

.. autoclass:: CurvePlot
   :members:
   :inherited-members:
.. autoclass:: CurveItem
   :members:
   :inherited-members:
.. autoclass:: ErrorBarCurveItem
   :members:
   :inherited-members:
.. autoclass:: PlotItemList
   :members:
"""

from __future__ import with_statement, print_function

import warnings
import numpy as np

from guidata.qt.QtGui import (QMenu, QListWidget, QListWidgetItem, QVBoxLayout,
                              QToolBar, QMessageBox, QBrush, QColor, QPen,
                              QPolygonF)
from guidata.qt.QtCore import Qt, QPointF, QLineF, QRectF, Signal

from guidata.utils import assert_interfaces_valid, update_dataset
from guidata.configtools import get_icon, get_image_layout
from guidata.qthelpers import create_action, add_actions
from guidata.py3compat import is_text_string, maxsize

# Local imports
from guiqwt.transitional import (QwtPlotCurve, QwtPlotGrid, QwtPlotItem,
                                 QwtScaleMap)
from guiqwt.config import CONF, _
from guiqwt.interfaces import (IBasePlotItem, IDecoratorItemType,
                               ISerializableType, ICurveItemType,
                               ITrackableItemType, IPanel)
from guiqwt.panels import PanelWidget, ID_ITEMLIST
from guiqwt.baseplot import BasePlot, canvas_to_axes
from guiqwt.styles import GridParam, CurveParam, ErrorBarParam, SymbolParam
from guiqwt.shapes import Marker

def _simplify_poly(pts, off, scale, bounds):
    ax, bx, ay, by = scale
    xm, ym, xM, yM = bounds
    a = np.array( [[ax, ay]] )
    b = np.array( [[bx, by]] )
    _pts = a*pts+b
    poly = []
    NP = off.shape[0]
    for i in range(off.shape[0]):
        i0 = off[i, 1]
        if i+1<NP:
            i1 = off[i+1, 1]
        else:
            i1 = pts.shape[0]
        poly.append( (_pts[i0:i1], i) )
    return poly

try:
    from gshhs import simplify_poly
except ImportError:
    simplify_poly = _simplify_poly

def seg_dist(P, P0, P1):
    """
    Return distance between point P and segment (P0, P1)
    If P orthogonal projection on (P0, P1) is outside segment bounds, return
    either distance to P0 or to P1 (the closest one)
    P, P0, P1: QPointF instances
    """
    u = QLineF(P0, P).length()
    if P0 == P1:
        return u
    else:
        angle = QLineF(P0, P).angleTo(QLineF(P0, P1))*np.pi/180
        projection = u*np.cos(angle)
        if  projection > QLineF(P0, P1).length():
            return QLineF(P1, P).length()
        elif projection < 0:
            return QLineF(P0, P).length()
        else:
            return abs(u*np.sin(angle))

def test_seg_dist():
    print(seg_dist(QPointF(200, 100), QPointF(150, 196), QPointF(250, 180)))
    print(seg_dist(QPointF(200, 100), QPointF(190, 196), QPointF(210, 180)))
    print(seg_dist(QPointF(201, 105), QPointF(201, 196), QPointF(201, 180)))

def norm2(v):
    return (v**2).sum(axis=1)

def seg_dist_v(P, X0, Y0, X1, Y1):
    """Version vectorielle de seg_dist"""
    V = np.zeros((X0.shape[0], 2), float)
    PP = np.zeros((X0.shape[0], 2), float)
    PP[:, 0] = X0
    PP[:, 1] = Y0    
    V[:, 0] = X1-X0
    V[:, 1] = Y1-Y0
    dP = np.array(P).reshape(1, 2) - PP
    nV = np.sqrt(norm2(V)).clip(1e-12) # clip: avoid division by zero
    w2 = V/nV[:, np.newaxis]
    w = np.array([ -w2[:, 1], w2[:, 0] ]).T
    distances = np.fabs((dP*w).sum(axis=1))
    ix = distances.argmin()
    return ix, distances[ix]

def test_seg_dist_v():
    """Test de seg_dist_v"""
    a=(np.arange(10.)**2).reshape(5, 2)
    ix, dist = seg_dist_v((2.1, 3.3), a[:-1, 0], a[:-1, 1],
                          a[1:, 0], a[1:, 1])
    print(ix, dist)
    assert ix == 0

if __name__ == "__main__":
    test_seg_dist_v()
    test_seg_dist()


SELECTED_SYMBOL_PARAM = SymbolParam()
SELECTED_SYMBOL_PARAM.read_config(CONF, "plot", "selected_curve_symbol")
SELECTED_SYMBOL = SELECTED_SYMBOL_PARAM.build_symbol()


class GridItem(QwtPlotGrid):
    """
    Construct a grid `plot item` with the parameters *gridparam*
    (see :py:class:`guiqwt.styles.GridParam`)
    """
    __implements__ = (IBasePlotItem,)
    
    _readonly = True
    _private = False
    
    def __init__(self, gridparam=None):
        super(GridItem, self).__init__()
        if gridparam is None:
            self.gridparam = GridParam(title=_("Grid"), icon="grid.png")
        else:
            self.gridparam = gridparam
        self.selected = False
        self.immutable = True # set to false to allow moving points around
        self.update_params() # won't work completely because it's not yet
        # attached to plot (actually, only canvas background won't be updated)

    def types(self):
        return (IDecoratorItemType,)
    
    def attach(self, plot):
        """Reimplemented to update plot canvas background"""
        QwtPlotGrid.attach(self, plot)
        self.update_params()

    def set_readonly(self, state):
        """Set object read-only state"""
        self._readonly = state
        
    def is_readonly(self):
        """Return object read-only 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 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 False
    def can_resize(self):
        return False
    def can_rotate(self):
        return False
    def can_move(self):
        return False

    def select(self):
        """Select item"""
        pass
    
    def unselect(self):
        """Unselect item"""
        pass

    def hit_test(self, pos):
        return maxsize, 0, False, None

    def move_local_point_to(self, handle, pos, ctrl=None):
        pass

    def move_local_shape(self, old_pos, new_pos):
        pass
        
    def move_with_selection(self, delta_x, delta_y):
        pass

    def update_params(self):
        self.gridparam.update_grid(self)
        
    def update_item_parameters(self):
        self.gridparam.update_param(self)

    def get_item_parameters(self, itemparams):
        itemparams.add("GridParam", self, self.gridparam)
    
    def set_item_parameters(self, itemparams):
        self.gridparam = itemparams.get("GridParam")
        self.gridparam.update_grid(self)

assert_interfaces_valid(GridItem)


class CurveItem(QwtPlotCurve):
    """
    Construct a curve `plot item` with the parameters *curveparam*
    (see :py:class:`guiqwt.styles.CurveParam`)
    """
    __implements__ = (IBasePlotItem, ISerializableType)
    
    _readonly = False
    _private = False
    
    def __init__(self, curveparam=None):
        super(CurveItem, self).__init__()
        if curveparam is None:
            self.curveparam = CurveParam(_("Curve"), icon='curve.png')
        else:
            self.curveparam = curveparam
        self.selected = False
        self.immutable = True # set to false to allow moving points around
        self._x = None
        self._y = None
        self.update_params()
        
    def _get_visible_axis_min(self, axis_id, axis_data):
        """Return axis minimum excluding zero and negative values when
        corresponding plot axis scale is logarithmic"""
        if self.plot().get_axis_scale(axis_id) == 'log':
            return axis_data[axis_data > 0].min()
        else:
            return axis_data.min()
        
    def boundingRect(self):
        """Return the bounding rectangle of the data"""
        plot = self.plot()
        if plot is not None and 'log' in (plot.get_axis_scale(self.xAxis()),
                                          plot.get_axis_scale(self.yAxis())):
            x, y = self._x, self._y
            xf, yf = x[np.isfinite(x)], y[np.isfinite(y)]
            xmin = self._get_visible_axis_min(self.xAxis(), xf)
            ymin = self._get_visible_axis_min(self.yAxis(), yf)
            return QRectF(xmin, ymin, xf.max()-xmin, yf.max()-ymin)
        else:
            return QwtPlotCurve.boundingRect(self)
        
    def types(self):
        return (ICurveItemType, ITrackableItemType, ISerializableType)

    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 True
    def can_resize(self):
        return False
    def can_rotate(self):
        return False
    def can_move(self):
        return False

    def __reduce__(self):
        state = (self.curveparam, self._x, self._y, self.z())
        res = ( CurveItem, (), state )
        return res

    def __setstate__(self, state):
        param, x, y, z = state
        self.curveparam = param
        self.set_data(x, y)
        self.setZ(z)
        self.update_params()

    def serialize(self, writer):
        """Serialize object to HDF5 writer"""
        writer.write(self._x, group_name='Xdata')
        writer.write(self._y, group_name='Ydata')
        writer.write(self.z(), group_name='z')
        self.curveparam.update_param(self)
        writer.write(self.curveparam, group_name='curveparam')
    
    def deserialize(self, reader):
        """Deserialize object from HDF5 reader"""
        self.curveparam = CurveParam(_("Curve"), icon='curve.png')
        reader.read('curveparam', instance=self.curveparam)
        x = reader.read(group_name='Xdata', func=reader.read_array)
        y = reader.read(group_name='Ydata', func=reader.read_array)
        self.set_data(x, y)
        self.setZ(reader.read('z'))
        self.update_params()
    
    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 invalidate_plot(self):
        plot = self.plot()
        if plot is not None:
            plot.invalidate()

    def select(self):
        """Select item"""
        self.selected = True
        plot = self.plot()
        if plot is not None:
            plot.blockSignals(True)
        self.setSymbol(SELECTED_SYMBOL)
        if plot is not None:
            plot.blockSignals(False)
        self.invalidate_plot()
    
    def unselect(self):
        """Unselect item"""
        self.selected = False
        # Restoring initial curve parameters:
        self.curveparam.update_curve(self)
        self.invalidate_plot()

    def get_data(self):
        """Return curve data x, y (NumPy arrays)"""
        return self._x, self._y

    def set_data(self, x, y):
        """
        Set curve data:
            * x: NumPy array
            * y: NumPy array
        """
        self._x = np.array(x, copy=False)
        self._y = np.array(y, copy=False)
        self.setData(self._x, self._y)
        
    def is_empty(self):
        """Return True if item data is empty"""
        return self._x is None or self._y is None or self._y.size == 0

    def hit_test(self, pos):
        """Calcul de la distance d'un point à une courbe
        renvoie (dist, handle, inside)"""
        if self.is_empty():
            return maxsize, 0, False, None
        plot = self.plot()
        ax = self.xAxis()
        ay = self.yAxis()
        px = plot.invTransform(ax, pos.x())
        py = plot.invTransform(ay, pos.y())
        # On cherche les 4 points qui sont les plus proches en X et en Y
        # avant et après ie tels que p1x < x < p2x et p3y < y < p4y
        tmpx = self._x - px
        tmpy = self._y - py
        if np.count_nonzero(tmpx) != len(tmpx) or\
           np.count_nonzero(tmpy) != len(tmpy):
            # Avoid dividing by zero warning when computing dx or dy
            return maxsize, 0, False, None
        dx = 1/tmpx
        dy = 1/tmpy
        i0 = dx.argmin()
        i1 = dx.argmax()
        i2 = dy.argmin()
        i3 = dy.argmax()
        t = np.array((i0, i1, i2, i3))
        t2 = (t+1).clip(0, self._x.shape[0]-1)
        i, _d = seg_dist_v((px, py), self._x[t], self._y[t],
                           self._x[t2], self._y[t2])
        i = t[i]
        # Recalcule la distance dans le répère du widget
        p0x = plot.transform(ax, self._x[i])
        p0y = plot.transform(ay, self._y[i])
        if i+1 >= self._x.shape[0]:
            p1x = p0x
            p1y = p0y
        else:
            p1x = plot.transform(ax, self._x[i+1])
            p1y = plot.transform(ay, self._y[i+1])
        distance = seg_dist(QPointF(pos), QPointF(p0x, p0y), QPointF(p1x, p1y))
        return distance, i, False, None
    
    def get_closest_coordinates(self, x, y):
        """Renvoie les coordonnées (x',y') du point le plus proche de (x,y)
        Méthode surchargée pour ErrorBarSignalCurve pour renvoyer
        les coordonnées des pointes des barres d'erreur"""
        plot = self.plot()
        ax = self.xAxis()
        ay = self.yAxis()
        xc = plot.transform(ax, x)
        yc = plot.transform(ay, y)
        _distance, i, _inside, _other = self.hit_test(QPointF(xc, yc))
        point = self.sample(i)
        return point.x(), point.y()

    def get_coordinates_label(self, xc, yc):
        title = self.title().text()
        return "%s:<br>x = %g<br>y = %g" % (title, xc, yc)

    def get_closest_x(self, xc):
        # We assume X is sorted, otherwise we'd need :
        # argmin(abs(x-xc))
        i = self._x.searchsorted(xc)
        if i>0:
            if np.fabs(self._x[i-1]-xc) < np.fabs(self._x[i]-xc):
                return self._x[i-1], self._y[i-1]
        return self._x[i], self._y[i]

    def move_local_point_to(self, handle, pos, ctrl=None):
        if self.immutable:
            return
        if handle < 0 or handle > self._x.shape[0]:
            return
        x, y = canvas_to_axes(self, pos)
        self._x[handle] = x
        self._y[handle] = y
        self.setData(self._x, self._y)
        self.plot().replot()

    def move_local_shape(self, old_pos, new_pos):
        """Translate the shape such that old_pos becomes new_pos
        in canvas coordinates"""
        nx, ny = canvas_to_axes(self, new_pos)
        ox, oy = canvas_to_axes(self, old_pos)
        self._x += (nx-ox)
        self._y += (ny-oy)
        self.setData(self._x, self._y)
        
    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._x += delta_x
        self._y += delta_y
        self.setData(self._x, self._y)

    def update_params(self):        
        self.curveparam.update_curve(self)
        if self.selected:
            self.select()

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

    def get_item_parameters(self, itemparams):
        itemparams.add("CurveParam", self, self.curveparam)
    
    def set_item_parameters(self, itemparams):
        update_dataset(self.curveparam, itemparams.get("CurveParam"),
                       visible_only=True)
        self.update_params()

assert_interfaces_valid(CurveItem)


class PolygonMapItem(QwtPlotItem):
    """
    Construct a curve `plot item` with the parameters *curveparam*
    (see :py:class:`guiqwt.styles.CurveParam`)
    """
    __implements__ = (IBasePlotItem, ISerializableType)
    
    _readonly = False
    _private = False
    _can_select = False
    _can_resize = False
    _can_move = False
    _can_rotate = False

    def __init__(self, curveparam=None):
        super(PolygonMapItem, self).__init__()
        if curveparam is None:
            self.curveparam = CurveParam(_("PolygonMap"), icon='curve.png')
        else:
            self.curveparam = curveparam
        self.selected = False
        self.immutable = True # set to false to allow moving points around
        self._pts = None # Array of points Mx2
        self._n = None   # Array of polygon offsets/ends Nx1 (polygon k points are _pts[_n[k-1]:_n[k]])
        self._c = None   # Color of polygon Nx2 [border,background] as RGBA uint32
        self.update_params()
        
    def types(self):
        return (ICurveItemType, ITrackableItemType, ISerializableType)

    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 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 setPen(self, x):
        pass
    def setBrush(self, x):
        pass
    def setSymbol(self, x):
        pass
    def setCurveAttribute(self, x, y):
        pass
    def setStyle(self, x):
        pass
    def setCurveType(self, x):
        pass
    def setBaseline(self, x):
        pass

    def __reduce__(self):
        state = (self.curveparam, self._pts, self._n, self._c, self.z())
        res = ( PolygonMapItem, (), state )
        return res

    def __setstate__(self, state):
        param, pts, n, c, z = state
        self.curveparam = param
        self.set_data(pts, n, c)
        self.setZ(z)
        self.update_params()

    def serialize(self, writer):
        """Serialize object to HDF5 writer"""
        writer.write(self._pts, group_name='Pdata')
        writer.write(self._n, group_name='Ndata')
        writer.write(self._c, group_name='Cdata')
        writer.write(self.z(), group_name='z')
        self.curveparam.update_param(self)
        writer.write(self.curveparam, group_name='curveparam')
    
    def deserialize(self, reader):
        """Deserialize object from HDF5 reader"""
        pts = reader.read(group_name='Pdata', func=reader.read_array)
        n = reader.read(group_name='Ndata', func=reader.read_array)
        c = reader.read(group_name='Cdata', func=reader.read_array)
        self.set_data(pts, n, c)
        self.setZ(reader.read('z'))
        self.curveparam = CurveParam(_("PolygonMap"), icon='curve.png')
        reader.read('curveparam', instance=self.curveparam)
        self.update_params()

    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 invalidate_plot(self):
        plot = self.plot()
        if plot is not None:
            plot.invalidate()

    def select(self):
        """Select item"""
        self.selected = True
        self.setSymbol(SELECTED_SYMBOL)
        self.invalidate_plot()
    
    def unselect(self):
        """Unselect item"""
        self.selected = False
        # Restoring initial curve parameters:
        self.curveparam.update_curve(self)
        self.invalidate_plot()

    def get_data(self):
        """Return curve data x, y (NumPy arrays)"""
        return self._pts, self._n, self._c

    def set_data(self, pts, n, c):
        """
        Set curve data:
            * x: NumPy array
            * y: NumPy array
        """
        self._pts = np.array(pts, copy=False)
        self._n = np.array(n, copy=False)
        self._c = np.array(c, copy=False)
        xmin, ymin = self._pts.min(axis=0)
        xmax, ymax = self._pts.max(axis=0)
        self.bounds = QRectF(xmin, ymin, xmax-xmin, ymax-ymin)
        
    def is_empty(self):
        """Return True if item data is empty"""
        return self._pts is None or self._pts.size == 0

    def hit_test(self, pos):
        """Calcul de la distance d'un point à une courbe
        renvoie (dist, handle, inside)"""
        if self.is_empty():
            return maxsize, 0, False, None
        plot = self.plot()
        # TODO
        return distance, i, False, None
    
    def get_closest_coordinates(self, x, y):
        """Renvoie les coordonnées (x',y') du point le plus proche de (x,y)
        Méthode surchargée pour ErrorBarSignalCurve pour renvoyer
        les coordonnées des pointes des barres d'erreur"""
        # TODO
        return x, y

    def get_coordinates_label(self, xc, yc):
        title = self.title().text()
        return "%s:<br>x = %f<br>y = %f" % (title, xc, yc)

    def move_local_point_to(self, handle, pos, ctrl=None):
        return

    def move_local_shape(self, old_pos, new_pos):
        pass

    def move_with_selection(self, delta_x, delta_y):
        pass

    def update_params(self):
        self.curveparam.update_curve(self)
        if self.selected:
            self.select()

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

    def get_item_parameters(self, itemparams):
        itemparams.add("CurveParam", self, self.curveparam)
    
    def set_item_parameters(self, itemparams):
        update_dataset(self.curveparam, itemparams.get("CurveParam"),
                       visible_only=True)
        self.update_params()

    def draw(self, painter, xMap, yMap, canvasRect):
        #from time import time
        p1x = xMap.p1()
        s1x = xMap.s1()
        ax = (xMap.p2() - p1x)/(xMap.s2()-s1x)
        p1y = yMap.p1()
        s1y = yMap.s1()
        ay = (yMap.p2() - p1y)/(yMap.s2()-s1y)
        bx, by = p1x-s1x*ax, p1y-s1y*ay
        _c = self._c
        _n = self._n
        fgcol = QColor()
        bgcol = QColor()
        #t0 = time()
        polygons = simplify_poly(self._pts, _n, (ax, bx, ay, by),
                                 canvasRect.getCoords() )
        #t1 = time()
        #print len(polygons), t1-t0
        #t2 = time()
        for poly, num in polygons:
            points = []
            for i in range(poly.shape[0]):
                points.append(QPointF(poly[i, 0], poly[i, 1]))
            pg = QPolygonF(points)
            fgcol.setRgba(int(_c[num, 0]))
            bgcol.setRgba(int(_c[num, 1]))
            painter.setPen(QPen(fgcol))
            painter.setBrush(QBrush(bgcol))
            painter.drawPolygon(pg)
        #print "poly:", time()-t2
        
    def boundingRect(self):
        return self.bounds

assert_interfaces_valid(PolygonMapItem)


def _transform(map, v):
    return QwtScaleMap.transform(map, v)
def vmap(map, v):
    """Transform coordinates while handling RuntimeWarning 
    that could be raised by NumPy when trying to transform 
    a zero in logarithmic scale for example"""
    with warnings.catch_warnings():
        warnings.simplefilter("ignore", category=RuntimeWarning)
        output = np.vectorize(_transform)(map, v)
    return output

class ErrorBarCurveItem(CurveItem):
    """
    Construct an error-bar curve `plot item` 
    with the parameters *errorbarparam*
    (see :py:class:`guiqwt.styles.ErrorBarParam`)
    """
    def __init__(self, curveparam=None, errorbarparam=None):
        if errorbarparam is None:
            self.errorbarparam = ErrorBarParam(_("Error bars"),
                                               icon='errorbar.png')
        else:
            self.errorbarparam = errorbarparam
        super(ErrorBarCurveItem, self).__init__(curveparam)
        self._dx = None
        self._dy = None
        self._minmaxarrays = {}

    def serialize(self, writer):
        """Serialize object to HDF5 writer"""
        super(ErrorBarCurveItem, self).serialize(writer)
        writer.write(self._dx, group_name='dXdata')
        writer.write(self._dy, group_name='dYdata')
        self.errorbarparam.update_param(self)
        writer.write(self.errorbarparam, group_name='errorbarparam')
    
    def deserialize(self, reader):
        """Deserialize object from HDF5 reader"""
        self.curveparam = CurveParam(_("Curve"), icon='curve.png')
        reader.read('curveparam', instance=self.curveparam)
        self.errorbarparam = ErrorBarParam(_("Error bars"),
                                           icon='errorbar.png')
        reader.read('errorbarparam', instance=self.errorbarparam)
        x = reader.read(group_name='Xdata', func=reader.read_array)
        y = reader.read(group_name='Ydata', func=reader.read_array)
        dx = reader.read(group_name='dXdata', func=reader.read_array)
        dy = reader.read(group_name='dYdata', func=reader.read_array)
        self.set_data(x, y, dx, dy)
        self.setZ(reader.read('z'))
        self.update_params()
        
    def unselect(self):
        """Unselect item"""
        CurveItem.unselect(self)
        self.errorbarparam.update_curve(self)

    def get_data(self):
        """
        Return error-bar curve data: x, y, dx, dy

            * x: NumPy array
            * y: NumPy array
            * dx: float or NumPy array (non-constant error bars)
            * dy: float or NumPy array (non-constant error bars)
        """
        return self._x, self._y, self._dx, self._dy

    def set_data(self, x, y, dx=None, dy=None):
        """
        Set error-bar curve data:
            
            * x: NumPy array
            * y: NumPy array
            * dx: float or NumPy array (non-constant error bars)
            * dy: float or NumPy array (non-constant error bars)
        """
        CurveItem.set_data(self, x, y)
        if dx is not None:
            dx = np.array(dx, copy=False)
            if dx.size == 0:
                dx = None
        if dy is not None:
            dy = np.array(dy, copy=False)
            if dy.size == 0:
                dy = None
        self._dx = dx
        self._dy = dy
        self._minmaxarrays = {}

    def get_minmax_arrays(self, all_values=True):
        if self._minmaxarrays.get(all_values) is None:
            x = self._x
            y = self._y
            dx = self._dx
            dy = self._dy
            if all_values:
                if dx is None:
                    xmin = xmax = x
                else:
                    xmin, xmax = x - dx, x + dx
                if dy is None:
                    ymin = ymax = y
                else:
                    ymin, ymax = y - dy, y + dy
                self._minmaxarrays.setdefault(all_values,
                                              (xmin, xmax, ymin, ymax))
            else:
                isf = np.logical_and(np.isfinite(x), np.isfinite(y))
                if dx is not None:
                    isf = np.logical_and(isf, np.isfinite(dx))
                if dy is not None:
                    isf = np.logical_and(isf, np.isfinite(dy))
                if dx is None:
                    xmin = xmax = x[isf]
                else:
                    xmin, xmax = x[isf] - dx[isf], x[isf] + dx[isf]
                if dy is None:
                    ymin = ymax = y[isf]
                else:
                    ymin, ymax = y[isf] - dy[isf], y[isf] + dy[isf]
                self._minmaxarrays.setdefault(all_values,
                                              (x[isf], y[isf],
                                               xmin, xmax, ymin, ymax))
        return self._minmaxarrays[all_values]
        
    def get_closest_coordinates(self, x, y):
        # Surcharge d'une méthode de base de CurveItem
        plot = self.plot()
        ax = self.xAxis()
        ay = self.yAxis()
        xc = plot.transform(ax, x)
        yc = plot.transform(ay, y)
        _distance, i, _inside, _other = self.hit_test(QPointF(xc, yc))
        x0, y0 = self.plot().canvas2plotitem(self, xc, yc)
        x = self._x[i]
        y = self._y[i]
        xmin, xmax, ymin, ymax = self.get_minmax_arrays()
        if abs(y0-y) > abs(y0-ymin[i]):
            y = ymin[i]
        elif abs(y0-y) > abs(y0-ymax[i]):
            y = ymax[i]
        if abs(x0-x) > abs(x0-xmin[i]):
            x = xmin[i]
        elif abs(x0-x) > abs(x0-xmax[i]):
            x = xmax[i]
        return x, y

    def boundingRect(self):
        """Return the bounding rectangle of the data, error bars included"""
        xmin, xmax, ymin, ymax = self.get_minmax_arrays()
        if xmin is None or xmin.size == 0:
            return CurveItem.boundingRect(self)
        plot = self.plot()
        xminf, yminf = xmin[np.isfinite(xmin)], ymin[np.isfinite(ymin)]
        xmaxf, ymaxf = xmax[np.isfinite(xmax)], ymax[np.isfinite(ymax)]
        if plot is not None and 'log' in (plot.get_axis_scale(self.xAxis()),
                                          plot.get_axis_scale(self.yAxis())):
            xmin = self._get_visible_axis_min(self.xAxis(), xminf)
            ymin = self._get_visible_axis_min(self.yAxis(), yminf)
        else:
            xmin = xminf.min()
            ymin = yminf.min()
        return QRectF(xmin, ymin, xmaxf.max()-xmin, ymaxf.max()-ymin)
        
    def draw(self, painter, xMap, yMap, canvasRect):
        if self._x is None or self._x.size == 0:
            return
        x, y, xmin, xmax, ymin, ymax = self.get_minmax_arrays(all_values=False)
        tx = vmap(xMap, x)
        ty = vmap(yMap, y)
        RN = list(range(len(tx)))
        if self.errorOnTop:
            QwtPlotCurve.draw(self, painter, xMap, yMap, canvasRect)
        
        painter.save()
        painter.setPen(self.errorPen)
        cap = self.errorCap/2.

        if self._dx is not None and self.errorbarparam.mode == 0:
            txmin = vmap(xMap, xmin)
            txmax = vmap(xMap, xmax)
            # Classic error bars
            lines = []
            for i in RN:
                yi = ty[i]
                lines.append(QLineF(txmin[i], yi, txmax[i], yi))
            painter.drawLines(lines)
            if cap > 0:
                lines = []
                for i in RN:
                    yi = ty[i]
                    lines.append(QLineF(txmin[i], yi-cap, txmin[i], yi+cap))
                    lines.append(QLineF(txmax[i], yi-cap, txmax[i], yi+cap))
            painter.drawLines(lines)
            
        if self._dy is not None:
            tymin = vmap(yMap, ymin)
            tymax = vmap(yMap, ymax)
            if self.errorbarparam.mode == 0:
                # Classic error bars
                lines = []
                for i in RN:
                    xi = tx[i]
                    lines.append(QLineF(xi, tymin[i], xi, tymax[i]))
                painter.drawLines(lines)
                if cap > 0:
                    # Cap
                    lines = []
                    for i in RN:
                        xi = tx[i]
                        lines.append(QLineF(xi-cap, tymin[i], xi+cap, tymin[i]))
                        lines.append(QLineF(xi-cap, tymax[i], xi+cap, tymax[i]))
                painter.drawLines(lines)
            else:
                # Error area
                points = []
                rpoints = []
                for i in RN:
                    xi = tx[i]
                    points.append(QPointF(xi, tymin[i]))
                    rpoints.append(QPointF(xi, tymax[i]))
                points += reversed(rpoints)
                painter.setBrush(QBrush(self.errorBrush))
                painter.drawPolygon(*points)

        painter.restore()

        if not self.errorOnTop:
            QwtPlotCurve.draw(self, painter, xMap, yMap, canvasRect)
        
    def update_params(self):
        self.errorbarparam.update_curve(self)
        CurveItem.update_params(self)

    def update_item_parameters(self):
        CurveItem.update_item_parameters(self)
        self.errorbarparam.update_param(self)

    def get_item_parameters(self, itemparams):
        CurveItem.get_item_parameters(self, itemparams)
        itemparams.add("ErrorBarParam", self, self.errorbarparam)
    
    def set_item_parameters(self, itemparams):
        update_dataset(self.errorbarparam, itemparams.get("ErrorBarParam"),
                       visible_only=True)
        CurveItem.set_item_parameters(self, itemparams)

assert_interfaces_valid( ErrorBarCurveItem )


#===============================================================================
# Plot Widget
#===============================================================================
class ItemListWidget(QListWidget):
    """
    PlotItemList
    List of items attached to plot
    """
    def __init__(self, parent):
        super(ItemListWidget, self).__init__(parent)
        
        self.manager = None
        self.plot = None # the default plot...
        self.items = []
        
        self.currentRowChanged.connect(self.current_row_changed)
        self.itemChanged.connect(self.item_changed)
        self.itemSelectionChanged.connect(self.refresh_actions)
        self.itemSelectionChanged.connect(self.selection_changed)
        
        self.setWordWrap(True)
        self.setMinimumWidth(140)
        self.setSelectionMode(QListWidget.ExtendedSelection)
        
        # Setup context menu
        self.menu = QMenu(self)
        self.menu_actions = self.setup_actions()
        self.refresh_actions()
        add_actions(self.menu, self.menu_actions)

    def register_panel(self, manager):
        self.manager = manager

        for plot in self.manager.get_plots():
            plot.SIG_ITEMS_CHANGED.connect(self.items_changed)
            plot.SIG_ACTIVE_ITEM_CHANGED.connect(self.items_changed)
        self.plot = self.manager.get_plot()

    def contextMenuEvent(self, event):
        """Override Qt method"""
        self.refresh_actions()
        self.menu.popup(event.globalPos())
                     
    def setup_actions(self):
        self.movedown_ac = create_action(self, _("Move to back"),
                                     icon=get_icon('arrow_down.png'),
                                     triggered=lambda: self.move_item("down"))
        self.moveup_ac = create_action(self, _("Move to front"),
                                       icon=get_icon('arrow_up.png'),
                                       triggered=lambda: self.move_item("up"))
        settings_ac = create_action(self, _("Parameters..."),
                    icon=get_icon('settings.png'),
                    triggered=self.edit_plot_parameters )
        self.remove_ac = create_action(self, _("Remove"),
                                       icon=get_icon('trash.png'),
                                       triggered=self.remove_item)
        return [self.moveup_ac, self.movedown_ac, None,
                settings_ac, self.remove_ac]

    def edit_plot_parameters(self):
        self.plot.edit_plot_parameters("item")
    
    def __is_selection_contiguous(self):
        indexes = sorted([self.row(lw_item) for lw_item
                          in self.selectedItems()])
        return len(indexes) <= 1 or list(range(indexes[0], indexes[-1]+1)) == indexes
        
    def get_selected_items(self):
        """Return selected QwtPlot items
        
        .. warning::

            This is not the same as 
            :py:data:`guiqwt.baseplot.BasePlot.get_selected_items`.
            Some items could appear in itemlist without being registered in 
            plot widget items (in particular, some items could be selected in 
            itemlist without being selected in plot widget)
        """
        return [self.items[self.row(lw_item)]
                for lw_item in self.selectedItems()]
        
    def refresh_actions(self):
        is_selection = len(self.selectedItems()) > 0
        for action in self.menu_actions:
            if action is not None:
                action.setEnabled(is_selection)
        if is_selection:
            remove_state = True
            for item in self.get_selected_items():
                remove_state = remove_state and not item.is_readonly()
            self.remove_ac.setEnabled(remove_state)
            for action in [self.moveup_ac, self.movedown_ac]:
                action.setEnabled(self.__is_selection_contiguous())            
        
    def __get_item_icon(self, item):
        from guiqwt.label import LegendBoxItem, LabelItem
        from guiqwt.annotations import (AnnotatedShape, AnnotatedRectangle,
                                        AnnotatedCircle, AnnotatedEllipse,
                                        AnnotatedPoint, AnnotatedSegment)
        from guiqwt.shapes import (SegmentShape, RectangleShape, EllipseShape,
                                   PointShape, PolygonShape, Axes,
                                   XRangeSelection)
        from guiqwt.image import (BaseImageItem, Histogram2DItem,
                                  ImageFilterItem)
        from guiqwt.histogram import HistogramItem

        icon_name = 'item.png'
        for klass, icon in ((HistogramItem, 'histogram.png'),
                            (ErrorBarCurveItem, 'errorbar.png'),
                            (CurveItem, 'curve.png'),
                            (GridItem, 'grid.png'),
                            (LegendBoxItem, 'legend.png'),
                            (LabelItem, 'label.png'),
                            (AnnotatedSegment, 'segment.png'),
                            (AnnotatedPoint, 'point_shape.png'),
                            (AnnotatedCircle, 'circle.png'),
                            (AnnotatedEllipse, 'ellipse_shape.png'),
                            (AnnotatedRectangle, 'rectangle.png'),
                            (AnnotatedShape, 'annotation.png'),
                            (SegmentShape, 'segment.png'),
                            (RectangleShape, 'rectangle.png'),
                            (PointShape, 'point_shape.png'),
                            (EllipseShape, 'ellipse_shape.png'),
                            (Axes, 'gtaxes.png'),
                            (Marker, 'marker.png'),
                            (XRangeSelection, 'xrange.png'),
                            (PolygonShape, 'freeform.png'),
                            (Histogram2DItem, 'histogram2d.png'),
                            (ImageFilterItem, 'funct.png'),
                            (BaseImageItem, 'image.png'),):
            if isinstance(item, klass):
                icon_name = icon
                break
        return get_icon(icon_name)
        
    def items_changed(self, plot):
        """Plot items have changed"""
        active_plot = self.manager.get_active_plot()
        if active_plot is not plot:
            return
        self.plot = plot
        _block = self.blockSignals(True)
        active = plot.get_active_item()
        self.items = plot.get_public_items(z_sorted=True)
        self.clear()
        for item in self.items:
            title = item.title().text()
            lw_item = QListWidgetItem(self.__get_item_icon(item), title, self)
            lw_item.setCheckState(Qt.Checked if item.isVisible()
                                  else Qt.Unchecked)
            lw_item.setSelected(item.selected)
            font = lw_item.font()
            if item is active:
                font.setItalic(True)
            else:
                font.setItalic(False)
            lw_item.setFont(font)
            self.addItem(lw_item)
        self.refresh_actions()
        self.blockSignals(_block)
            
    def current_row_changed(self, index):
        """QListWidget current row has changed"""
        if index == -1:
            return
        item = self.items[index]
        if not item.can_select():
            item = None
        if item is None:
            self.plot.replot()
                
    def selection_changed(self):
        items = self.get_selected_items()
        self.plot.select_some_items(items)
        self.plot.replot()
        
    def item_changed(self, listwidgetitem):
        """QListWidget item has changed"""
        item = self.items[self.row(listwidgetitem)]
        visible = listwidgetitem.checkState() == Qt.Checked
        if visible != item.isVisible():
            self.plot.set_item_visible(item, visible)
    
    def move_item(self, direction):
        """Move item to the background/foreground
        Works only for contiguous selection
        -> 'refresh_actions' method should guarantee that"""
        items = self.get_selected_items()
        if direction == 'up':
            self.plot.move_up(items)
        else:
            self.plot.move_down(items)
        # Re-select items which can't be selected in plot widget but can be 
        # selected in ItemListWidget:
        for item in items:
            lw_item = self.item(self.items.index(item))
            if not lw_item.isSelected():
                lw_item.setSelected(True)
        self.plot.replot()
        
    def remove_item(self):
        if len(self.selectedItems()) == 1:
            message = _("Do you really want to remove this item?")
        else:
            message = _("Do you really want to remove selected items?")
        answer = QMessageBox.warning(self, _("Remove"), message,
                                     QMessageBox.Yes | QMessageBox.No)
        if answer == QMessageBox.Yes:
            items = self.get_selected_items()
            self.plot.del_items(items)
            self.plot.replot()
        

class PlotItemList(PanelWidget):
    """Construct the `plot item list panel`"""
    __implements__ = (IPanel,)
    PANEL_ID = ID_ITEMLIST
    PANEL_TITLE = _("Item list")
    PANEL_ICON = "item_list.png"
    
    def __init__(self, parent):
        super(PlotItemList, self).__init__(parent)
        self.manager = None
        
        vlayout = QVBoxLayout()
        self.setLayout(vlayout)
        
        style = "<span style=\'color: #444444\'><b>%s</b></span>"
        layout, _label = get_image_layout(self.PANEL_ICON,
                                          style % self.PANEL_TITLE,
                                          alignment=Qt.AlignCenter)
        vlayout.addLayout(layout)
        self.listwidget = ItemListWidget(self)
        vlayout.addWidget(self.listwidget)
        
        toolbar = QToolBar(self)
        vlayout.addWidget(toolbar)
        add_actions(toolbar, self.listwidget.menu_actions)

    def register_panel(self, manager):
        """Register panel to plot manager"""
        self.manager = manager
        self.listwidget.register_panel(manager)
                         
    def configure_panel(self):
        """Configure panel"""
        pass

assert_interfaces_valid(PlotItemList)


class CurvePlot(BasePlot):
    """
    Construct a 2D curve plotting widget 
    (this class inherits :py:class:`guiqwt.baseplot.BasePlot`)
    
        * parent: parent widget
        * title: plot title
        * xlabel: (bottom axis title, top axis title) or bottom axis title only
        * ylabel: (left axis title, right axis title) or left axis title only
        * xunit: (bottom axis unit, top axis unit) or bottom axis unit only
        * yunit: (left axis unit, right axis unit) or left axis unit only
        * gridparam: GridParam instance
        * axes_synchronised: keep all x and y axes synchronised when zomming or
          panning
    """
    DEFAULT_ITEM_TYPE = ICurveItemType
    AUTOSCALE_TYPES = (CurveItem, PolygonMapItem)
    
    #: Signal emitted by plot when plot axis has changed, e.g. when panning/zooming (arg: plot))
    SIG_PLOT_AXIS_CHANGED = Signal("PyQt_PyObject")
    
    def __init__(self, parent=None, title=None, xlabel=None, ylabel=None,
                 xunit=None, yunit=None, gridparam=None,
                 section="plot", axes_synchronised=False):
        super(CurvePlot, self).__init__(parent, section)

        self.axes_reverse = [False]*4
        
        self.set_titles(title=title, xlabel=xlabel, ylabel=ylabel,
                        xunit=xunit, yunit=yunit)
                
        self.antialiased = False

        self.set_antialiasing(CONF.get(section, "antialiasing"))
        
        self.axes_synchronised = axes_synchronised
        
        # Installing our own event filter:
        # (qwt's event filter does not fit our needs)
        self.canvas().installEventFilter(self.filter)
        self.canvas().setMouseTracking(True)
    
        self.cross_marker = Marker()
        self.curve_marker = Marker(label_cb=self.get_coordinates_str,
                                   constraint_cb=self.on_active_curve)
        self.cross_marker.set_style(section, "marker/cross")
        self.curve_marker.set_style(section, "marker/curve")
        self.cross_marker.setVisible(False)
        self.curve_marker.setVisible(False)
        self.cross_marker.attach(self)
        self.curve_marker.attach(self)

        # Background color
        self.setCanvasBackground(Qt.white)        
        
        self.curve_pointer = False
        self.canvas_pointer = False
        
        # Setting up grid
        if gridparam is None:
            gridparam = GridParam(title=_("Grid"), icon="grid.png")
            gridparam.read_config(CONF, section, "grid")
        self.grid = GridItem(gridparam)
        self.add_item(self.grid, z=-1)

    #---- Private API ----------------------------------------------------------
    def __del__(self):
        # Sometimes, an obscure exception happens when we quit an application
        # because if we don't remove the eventFilter it can still be called
        # after the filter object has been destroyed by Python.
        canvas = self.canvas()
        if canvas:
            canvas.removeEventFilter(self.filter)

    # generic helper methods        
    def canvas2plotitem(self, plot_item, x_canvas, y_canvas):
        return (self.invTransform(plot_item.xAxis(), x_canvas),
                self.invTransform(plot_item.yAxis(), y_canvas))
    def plotitem2canvas(self, plot_item, x, y):
        return (self.transform(plot_item.xAxis(), x),
                self.transform(plot_item.yAxis(), y))

    def on_active_curve(self, x, y):
        curve = self.get_last_active_item(ITrackableItemType)
        if curve:
            x, y = curve.get_closest_coordinates(x, y)
        return x, y
    
    def get_coordinates_str(self, x, y):
        title = _("Grid")
        item = self.get_last_active_item(ITrackableItemType)
        if item:
            return item.get_coordinates_label(x, y)
        return "<b>%s</b><br>x = %g<br>y = %g" % (title, x, y)

    def set_marker_axes(self):
        curve = self.get_last_active_item(ITrackableItemType)
        if curve:
            self.cross_marker.setAxes(curve.xAxis(), curve.yAxis())
            self.curve_marker.setAxes(curve.xAxis(), curve.yAxis())
    
    def do_move_marker(self, event):
        pos = event.pos()
        self.set_marker_axes()
        if event.modifiers() & Qt.ShiftModifier or self.curve_pointer :
            self.curve_marker.setZ(self.get_max_z()+1)
            self.cross_marker.setVisible(False)
            self.curve_marker.setVisible(True)
            self.curve_marker.move_local_point_to(0, pos)
            self.replot()
            #self.move_curve_marker(self.curve_marker, xc, yc)
        elif event.modifiers() & Qt.AltModifier or self.canvas_pointer:
            self.cross_marker.setZ(self.get_max_z()+1)
            self.cross_marker.setVisible(True)
            self.curve_marker.setVisible(False)
            self.cross_marker.move_local_point_to(0, pos)
            self.replot()
            #self.move_canvas_marker(self.cross_marker, xc, yc)
        else:
            vis_cross = self.cross_marker.isVisible()
            vis_curve = self.curve_marker.isVisible()
            self.cross_marker.setVisible(False)
            self.curve_marker.setVisible(False)
            if vis_cross or vis_curve:
                self.replot()
                
    def get_axes_to_update(self, dx, dy):
        if self.axes_synchronised:
            axes = []
            for axis_name in self.AXIS_NAMES:
                if axis_name in ("left", "right"):
                    d = dy
                else:
                    d = dx
                axes.append((d, self.get_axis_id(axis_name)))
            return axes
        else:
            xaxis, yaxis = self.get_active_axes()
            return [(dx, xaxis), (dy, yaxis)]
        
    def do_pan_view(self, dx, dy):
        """
        Translate the active axes by dx, dy
        dx, dy are tuples composed of (initial pos, dest pos)
        """
        auto = self.autoReplot()
        self.setAutoReplot(False)
        axes_to_update = self.get_axes_to_update(dx, dy)
        
        for (x1, x0, _start, _width), axis_id in axes_to_update:
            lbound, hbound = self.get_axis_limits(axis_id)
            i_lbound = self.transform(axis_id, lbound)
            i_hbound = self.transform(axis_id, hbound)
            delta = x1-x0
            vmin = self.invTransform(axis_id, i_lbound-delta)
            vmax = self.invTransform(axis_id, i_hbound-delta)
            self.set_axis_limits(axis_id, vmin, vmax)
            
        self.setAutoReplot(auto)
        self.replot()
        # the signal MUST be emitted after replot, otherwise
        # we receiver won't see the new bounds (don't know why?)
        self.SIG_PLOT_AXIS_CHANGED.emit(self)

    def do_zoom_view(self, dx, dy, lock_aspect_ratio=False):
        """
        Change the scale of the active axes (zoom/dezoom) according to dx, dy
        dx, dy are tuples composed of (initial pos, dest pos)
        We try to keep initial pos fixed on the canvas as the scale changes
        """
        # See guiqwt/events.py where dx and dy are defined like this:
        #   dx = (pos.x(), self.last.x(), self.start.x(), rct.width())
        #   dy = (pos.y(), self.last.y(), self.start.y(), rct.height())
        # where:
        #   * self.last is the mouse position seen during last event
        #   * self.start is the first mouse position (here, this is the 
        #     coordinate of the point which is at the center of the zoomed area)
        #   * rct is the plot rect contents
        #   * pos is the current mouse cursor position
        auto = self.autoReplot()
        self.setAutoReplot(False)
        dx = (-1,) + dx # adding direction to tuple dx
        dy = (1,) + dy  # adding direction to tuple dy
        if lock_aspect_ratio:
            direction, x1, x0, start, width = dx
            F = 1+3*direction*float(x1-x0)/width
        axes_to_update = self.get_axes_to_update(dx, dy)
        
        for (direction, x1, x0, start, width), axis_id in axes_to_update:
            lbound, hbound = self.get_axis_limits(axis_id)
            if not lock_aspect_ratio:
                F = 1+3*direction*float(x1-x0)/width
            if F*(hbound-lbound) == 0:
                continue
            if self.get_axis_scale(axis_id) == 'lin':
                orig = self.invTransform(axis_id, start)
                vmin = orig-F*(orig-lbound)
                vmax = orig+F*(hbound-orig)
            else: # log scale
                i_lbound = self.transform(axis_id, lbound)
                i_hbound = self.transform(axis_id, hbound)
                imin = start - F*(start-i_lbound)
                imax = start + F*(i_hbound-start)
                vmin = self.invTransform(axis_id, imin)
                vmax = self.invTransform(axis_id, imax)
            self.set_axis_limits(axis_id, vmin, vmax)

        self.setAutoReplot(auto)
        self.replot()
        # the signal MUST be emitted after replot, otherwise
        # we receiver won't see the new bounds (don't know why?)
        self.SIG_PLOT_AXIS_CHANGED.emit(self)
        
    def do_zoom_rect_view(self, start, end):
        # XXX implement the case when axes are synchronised
        x1, y1 = start.x(), start.y()
        x2, y2 = end.x(), end.y()
        xaxis, yaxis = self.get_active_axes()
        active_axes = [ (x1, x2, xaxis),
                        (y1, y2, yaxis) ]
        for h1, h2, k in active_axes:
            o1 = self.invTransform(k, h1)
            o2 = self.invTransform(k, h2)
            if o1 > o2:
                o1, o2 = o2, o1
            if o1 == o2:
                continue
            if self.get_axis_direction(k):
                o1, o2 = o2, o1
            self.setAxisScale(k, o1, o2)
        self.replot()
        self.SIG_PLOT_AXIS_CHANGED.emit(self)

    def get_default_item(self):
        """Return default item, depending on plot's default item type
        (e.g. for a curve plot, this is a curve item type).
        
        Return nothing if there is more than one item matching 
        the default item type."""
        items = self.get_items(item_type=self.DEFAULT_ITEM_TYPE)
        if len(items) == 1:
            return items[0]

    #---- BasePlot API ---------------------------------------------------------
    def add_item(self, item, z=None):
        """
        Add a *plot item* instance to this *plot widget*
        
            * item: :py:data:`qwt.QwtPlotItem` object implementing
              the :py:data:`guiqwt.interfaces.IBasePlotItem` interface
            * z: item's z order (None -> z = max(self.get_items())+1)
        """
        if isinstance(item, QwtPlotCurve):
            item.setRenderHint(QwtPlotItem.RenderAntialiased, self.antialiased)
        BasePlot.add_item(self, item, z)

    def del_all_items(self, except_grid=True):
        """Del all items, eventually (default) except grid"""
        items = [item for item in self.items
                 if not except_grid or item is not self.grid]
        self.del_items(items)
    
    def set_active_item(self, item):
        """Override base set_active_item to change the grid's
        axes according to the selected item"""
        old_active = self.active_item
        BasePlot.set_active_item(self, item)
        if item is not None and old_active is not item:
            self.grid.setAxes(item.xAxis(), item.yAxis())

    def get_plot_parameters(self, key, itemparams):
        if key == "grid":
            self.grid.gridparam.update_param(self.grid)
            itemparams.add("GridParam", self, self.grid.gridparam)
        else:
            BasePlot.get_plot_parameters(self, key, itemparams)

    def set_item_parameters(self, itemparams):
        # Grid style
        dataset = itemparams.get("GridParam")
        if dataset is not None:
            dataset.update_grid(self.grid)
            self.grid.gridparam = dataset
        BasePlot.set_item_parameters(self, itemparams)
    
    def do_autoscale(self, replot=True, axis_id=None):
        """Do autoscale on all axes"""
        auto = self.autoReplot()
        self.setAutoReplot(False)
        # XXX implement the case when axes are synchronised
        for axis_id in self.AXIS_IDS if axis_id is None else [axis_id]:
            vmin, vmax = None, None
            if not self.axisEnabled(axis_id):
                continue
            for item in self.get_items():
                if isinstance(item, self.AUTOSCALE_TYPES) \
                   and not item.is_empty() and item.isVisible():
                    bounds = item.boundingRect()
                    if axis_id == item.xAxis():
                        xmin, xmax = bounds.left(), bounds.right()
                        if vmin is None or xmin < vmin:
                            vmin = xmin
                        if vmax is None or xmax > vmax:
                            vmax = xmax
                    elif axis_id == item.yAxis():
                        ymin, ymax = bounds.top(), bounds.bottom()
                        if vmin is None or ymin < vmin:
                            vmin = ymin
                        if vmax is None or ymax > vmax:
                            vmax = ymax
            if vmin is None or vmax is None:
                continue
            if vmin == vmax: # same behavior as MATLAB
                vmin -= 1
                vmax += 1
            elif self.get_axis_scale(axis_id) == 'lin':
                dv = vmax-vmin
                vmin -= .002*dv
                vmax += .002*dv
            elif vmin > 0 and vmax > 0: # log scale
                dv = np.log10(vmax)-np.log10(vmin)
                vmin = 10**(np.log10(vmin)-.002*dv)
                vmax = 10**(np.log10(vmax)+.002*dv)
            self.set_axis_limits(axis_id, vmin, vmax)
        self.setAutoReplot(auto)
        if replot:
            self.replot()
        self.SIG_PLOT_AXIS_CHANGED.emit(self)

    def set_axis_limits(self, axis_id, vmin, vmax, stepsize=0):
        """Set axis limits (minimum and maximum values)"""
        axis_id = self.get_axis_id(axis_id)
        vmin, vmax = sorted([vmin, vmax])
        if self.get_axis_direction(axis_id):
            BasePlot.set_axis_limits(self, axis_id, vmax, vmin, stepsize)
        else:
            BasePlot.set_axis_limits(self, axis_id, vmin, vmax, stepsize)

    #---- Public API -----------------------------------------------------------
    def get_axis_direction(self, axis_id):
        """
        Return axis direction of increasing values

            * axis_id: axis id (BasePlot.Y_LEFT, BasePlot.X_BOTTOM, ...)
              or string: 'bottom', 'left', 'top' or 'right'
        """
        axis_id = self.get_axis_id(axis_id)
        return self.axes_reverse[axis_id]
            
    def set_axis_direction(self, axis_id, reverse=False):
        """
        Set axis direction of increasing values

            * axis_id: axis id (BasePlot.Y_LEFT, BasePlot.X_BOTTOM, ...)
              or string: 'bottom', 'left', 'top' or 'right'
            * reverse: False (default)
                - x-axis values increase from left to right
                - y-axis values increase from bottom to top
            * reverse: True
                - x-axis values increase from right to left
                - y-axis values increase from top to bottom
        """
        axis_id = self.get_axis_id(axis_id)
        if reverse != self.axes_reverse[axis_id]:
            self.replot()
            self.axes_reverse[axis_id] = reverse
            axis_map = self.canvasMap(axis_id)
            self.setAxisScale(axis_id, axis_map.s2(), axis_map.s1())
            self.updateAxes()
            self.SIG_AXIS_DIRECTION_CHANGED.emit(self, axis_id)
            
    def set_titles(self, title=None, xlabel=None, ylabel=None,
                   xunit=None, yunit=None):
        """
        Set plot and axes titles at once

            * title: plot title
            * xlabel: (bottom axis title, top axis title) 
              or bottom axis title only
            * ylabel: (left axis title, right axis title) 
              or left axis title only
            * xunit: (bottom axis unit, top axis unit) 
              or bottom axis unit only
            * yunit: (left axis unit, right axis unit) 
              or left axis unit only
        """
        if title is not None:
            self.set_title(title)
        if xlabel is not None:
            if is_text_string(xlabel):
                xlabel = (xlabel, "")
            for label, axis in zip(xlabel, ("bottom", "top")):
                if label is not None:
                    self.set_axis_title(axis, label)
        if ylabel is not None:
            if is_text_string(ylabel):
                ylabel = (ylabel, "")
            for label, axis in zip(ylabel, ("left", "right")):
                if label is not None:
                    self.set_axis_title(axis, label)
        if xunit is not None:
            if is_text_string(xunit):
                xunit = (xunit, "")
            for unit, axis in zip(xunit, ("bottom", "top")):
                if unit is not None:
                    self.set_axis_unit(axis, unit)
        if yunit is not None:
            if is_text_string(yunit):
                yunit = (yunit, "")
            for unit, axis in zip(yunit, ("left", "right")):
                if unit is not None:
                    self.set_axis_unit(axis, unit)
    
    def set_pointer(self, pointer_type):
        """
        Set pointer.

        Valid values of `pointer_type`:

            * None: disable pointer
            * "canvas": enable canvas pointer
            * "curve": enable on-curve pointer
        """
        self.canvas_pointer = False
        self.curve_pointer = False
        if pointer_type == "canvas":
            self.canvas_pointer = True
        elif pointer_type == "curve":
            self.curve_pointer = True

    def set_antialiasing(self, checked):
        """Toggle curve antialiasing"""
        self.antialiased = checked
        for curve in self.itemList():
            if isinstance(curve, QwtPlotCurve):
                curve.setRenderHint(QwtPlotItem.RenderAntialiased,
                                    self.antialiased)

    def set_plot_limits(self, x0, x1, y0, y1, xaxis="bottom", yaxis="left"):
        """Set plot scale limits"""
        self.set_axis_limits(yaxis, y0, y1)
        self.set_axis_limits(xaxis, x0, x1)     
        self.updateAxes()
        self.SIG_AXIS_DIRECTION_CHANGED.emit(self, self.get_axis_id(yaxis))
        self.SIG_AXIS_DIRECTION_CHANGED.emit(self, self.get_axis_id(xaxis))
        
    def set_plot_limits_synchronised(self, x0, x1, y0, y1):
        for yaxis, xaxis in (("left", "bottom"), ("right", "top")):
            self.set_plot_limits(x0, x1, y0, y1, xaxis=xaxis, yaxis=yaxis)
        
    def get_plot_limits(self, xaxis="bottom", yaxis="left"):
        """Return plot scale limits"""
        x0, x1 = self.get_axis_limits(xaxis)
        y0, y1 = self.get_axis_limits(yaxis)
        return x0, x1, y0, y1

