# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2004-2016 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# ###########################################################################*/
"""A :class:`.PlotWidget` with additionnal toolbars.
The :class:`PlotWindow` is a subclass of :class:`.PlotWidget`.
It provides the plot API fully defined in :class:`.Plot`.
"""
__authors__ = ["V.A. Sole", "T. Vincent"]
__license__ = "MIT"
__date__ = "07/03/2016"
import collections
import logging
from . import PlotWidget
from .PlotActions import *  # noqa
from .PlotTools import PositionInfo, ProfileToolBar
from .LegendSelector import LegendsDockWidget
from .CurvesROIWidget import CurvesROIDockWidget
from .MaskToolsWidget import MaskToolsDockWidget
try:
    from ..console import IPythonDockWidget
except ImportError:
    IPythonDockWidget = None
from .. import qt
_logger = logging.getLogger(__name__)
[docs]class PlotWindow(PlotWidget):
    """Qt Widget providing a 1D/2D plot area and additional tools.
    This widget includes the following QAction as attributes:
    - resetZoomAction: Reset zoom
    - xAxisAutoScaleAction: Toggle X axis autoscale
    - yAxisAutoScaleAction: Toggle Y axis autoscale
    - xAxisLogarithmicAction: Toggle X axis log scale
    - yAxisLogarithmicAction: Toggle Y axis log scale
    - gridAction: Toggle plot grid
    - curveStyleAction: Change curve line and markers style
    - colormapAction: Open a colormap dialog to change active image
      and default colormap.
    - keepDataAspectRatioAction: Toggle keep aspect ratio
    - yAxisInvertedAction: Toggle Y Axis direction
    - copyAction: Copy plot snapshot to clipboard
    - saveAction: Save plot
    - printAction: Print plot
    Initialiser parameters:
    :param parent: The parent of this widget or None.
    :param backend: The backend to use for the plot.
                    The default is to use matplotlib.
    :type backend: str or :class:`BackendBase.BackendBase`
    :param bool resetzoom: Toggle visibility of reset zoom action.
    :param bool autoScale: Toggle visibility of axes autoscale actions.
    :param bool logScale: Toggle visibility of axes log scale actions.
    :param bool grid: Toggle visibility of grid mode action.
    :param bool curveStyle: Toggle visibility of curve style action.
    :param bool colormap: Toggle visibility of colormap action.
    :param bool aspectRatio: Toggle visibility of aspect ration action.
    :param bool yInverted: Toggle visibility of Y axis direction action.
    :param bool copy: Toggle visibility of copy action.
    :param bool save: Toggle visibility of save action.
    :param bool print_: Toggle visibility of print action.
    :param bool control: True to display an Options button with a sub-menu
                         to show legends, toggle crosshair and pan with arrows.
                         (Default: False)
    :param position: True to display widget with (x, y) mouse position
                     (Default: False).
                     It also supports a list of (name, funct(x, y)->value)
                     to customize the displayed values.
                     See :class:`silx.gui.plot.PlotTools.PositionInfo`.
    :param bool roi: Toggle visibilty of ROI action.
    """
    def __init__(self, parent=None, backend=None,
                 resetzoom=True, autoScale=True, logScale=True, grid=True,
                 curveStyle=True, colormap=True,
                 aspectRatio=True, yInverted=True,
                 copy=True, save=True, print_=True,
                 control=False, position=False, roi=True, mask=True):
        super(PlotWindow, self).__init__(parent=parent, backend=backend)
        if parent is None:
            self.setWindowTitle('PlotWindow')
        self._dockWidgets = []
        # Init actions
        self.group = qt.QActionGroup(self)
        self.group.setExclusive(False)
        self.resetZoomAction = self.group.addAction(ResetZoomAction(self))
        self.resetZoomAction.setVisible(resetzoom)
        self.zoomInAction = ZoomInAction(self)
        self.addAction(self.zoomInAction)
        self.zoomOutAction = ZoomOutAction(self)
        self.addAction(self.zoomOutAction)
        self.xAxisAutoScaleAction = self.group.addAction(
            XAxisAutoScaleAction(self))
        self.xAxisAutoScaleAction.setVisible(autoScale)
        self.yAxisAutoScaleAction = self.group.addAction(
            YAxisAutoScaleAction(self))
        self.yAxisAutoScaleAction.setVisible(autoScale)
        self.xAxisLogarithmicAction = self.group.addAction(
            XAxisLogarithmicAction(self))
        self.xAxisLogarithmicAction.setVisible(logScale)
        self.yAxisLogarithmicAction = self.group.addAction(
            YAxisLogarithmicAction(self))
        self.yAxisLogarithmicAction.setVisible(logScale)
        self.gridAction = self.group.addAction(
            GridAction(self, gridMode='both'))
        self.gridAction.setVisible(grid)
        self.curveStyleAction = self.group.addAction(CurveStyleAction(self))
        self.curveStyleAction.setVisible(curveStyle)
        self.colormapAction = self.group.addAction(ColormapAction(self))
        self.colormapAction.setVisible(colormap)
        self.keepDataAspectRatioAction = self.group.addAction(
            KeepAspectRatioAction(self))
        self.keepDataAspectRatioAction.setVisible(aspectRatio)
        self.yAxisInvertedAction = self.group.addAction(
            YAxisInvertedAction(self))
        self.yAxisInvertedAction.setVisible(yInverted)
        self.group.addAction(self.roiAction)
        self.roiAction.setVisible(roi)
        self.group.addAction(self.maskAction)
        self.maskAction.setVisible(mask)
        self._separator = qt.QAction('separator', self)
        self._separator.setSeparator(True)
        self.group.addAction(self._separator)
        self.copyAction = self.group.addAction(CopyAction(self))
        self.copyAction.setVisible(copy)
        self.saveAction = self.group.addAction(SaveAction(self))
        self.saveAction.setVisible(save)
        self.printAction = self.group.addAction(PrintAction(self))
        self.printAction.setVisible(print_)
        if control or position:
            hbox = qt.QHBoxLayout()
            hbox.setSpacing(0)
            hbox.setContentsMargins(0, 0, 0, 0)
            if control:
                self.controlButton = qt.QPushButton("Options")
                self.controlButton.setAutoDefault(False)
                self.controlButton.clicked.connect(self._controlButtonClicked)
                hbox.addWidget(self.controlButton)
            if position:  # Add PositionInfo widget to the bottom of the plot
                if isinstance(position, collections.Iterable):
                    # Use position as a set of converters
                    converters = position
                else:
                    converters = None
                self.positionWidget = PositionInfo(self, converters=converters)
                self.positionWidget.autoSnapToActiveCurve = True
                hbox.addWidget(self.positionWidget)
            hbox.addStretch(1)
            bottomBar = qt.QWidget()
            bottomBar.setLayout(hbox)
            layout = qt.QVBoxLayout()
            layout.setSpacing(0)
            layout.setContentsMargins(0, 0, 0, 0)
            layout.addWidget(self.getWidgetHandle())
            layout.addWidget(bottomBar)
            centralWidget = qt.QWidget()
            centralWidget.setLayout(layout)
            self.setCentralWidget(centralWidget)
        self.addToolBar(self.toolBar())
    @property
    @property
    @property
[docs]    def roiAction(self):
        """QAction toggling curve ROI dock widget"""
        return self.curvesROIDockWidget.toggleViewAction()
 
    @property
    @property
[docs]    def maskAction(self):
        """QAction toggling image mask dock widget"""
        return self.maskToolsDockWidget.toggleViewAction()
 
[docs]    def getSelectionMask(self):
        """Return the current mask handled by :attr:`maskToolsDockWidget`.
        :return: The array of the mask with dimension of the 'active' image.
                 If there is no active image, an empty array is returned.
        :rtype: 2D numpy.ndarray of uint8
        """
        return self.maskToolsDockWidget.getSelectionMask()
 
[docs]    def setSelectionMask(self, mask):
        """Set the mask handled by :attr`maskToolsDockWidget`.
        If the provided mask has not the same dimension as the 'active'
        image, it will by cropped or padded.
        :param mask: The array to use for the mask.
        :type mask: numpy.ndarray of uint8 of dimension 2, C-contiguous.
                    Array of other types are converted.
        :return: True if success, False if failed
        """
        return bool(self.maskToolsDockWidget.setSelectionMask(mask))
 
    @property
    @property
[docs]    def crosshairAction(self):
        """Action toggling crosshair cursor mode (lazy-loaded)."""
        if not hasattr(self, '_crosshairAction'):
            self._crosshairAction = CrosshairAction(self, color='red')
        return self._crosshairAction
 
    @property
[docs]    def panWithArrowKeysAction(self):
        """Action toggling pan with arrow keys (lazy-loaded)."""
        if not hasattr(self, '_panWithArrowKeysAction'):
            self._panWithArrowKeysAction = PanWithArrowKeysAction(self)
        return self._panWithArrowKeysAction
 
    def _controlButtonClicked(self):
        """Display Options button sub-menu."""
        controlMenu = qt.QMenu()
        controlMenu.addAction(self.legendsDockWidget.toggleViewAction())
        controlMenu.addAction(self.roiAction)
        controlMenu.addAction(self.maskAction)
        if self.consoleDockWidget is not None:
            controlMenu.addAction(self.consoleDockWidget.toggleViewAction())
        else:
            disabledConsoleAction = controlMenu.addAction('Console')
            disabledConsoleAction.setCheckable(True)
            disabledConsoleAction.setEnabled(False)
        controlMenu.addSeparator()
        controlMenu.addAction(self.crosshairAction)
        controlMenu.addAction(self.panWithArrowKeysAction)
        controlMenu.exec_(self.cursor().pos())
    def _introduceNewDockWidget(self, dock_widget):
        """Maintain a list of dock widgets, in the order in which they are
        added. Tabify them as soon as there are more than one of them.
        :param dock_widget: Instance of :class:`QDockWidget` to be added.
        """
        if dock_widget not in self._dockWidgets:
            self._dockWidgets.append(dock_widget)
        if len(self._dockWidgets) == 1:
            # The first created dock widget must be added to a Widget area
            width = self.centralWidget().width()
            height = self.centralWidget().height()
            if width > (2.0 * height) and width > 1000:
                area = qt.Qt.RightDockWidgetArea
            else:
                area = qt.Qt.BottomDockWidgetArea
            self.addDockWidget(area, dock_widget)
        else:
            # Other dock widgets are added as tabs to the same widget area
            self.tabifyDockWidget(self._dockWidgets[0],
                                  dock_widget)
 
[docs]class Plot1D(PlotWindow):
    """PlotWindow with tools specific for curves.
    :param parent: The parent of this widget
    """
    def __init__(self, parent=None):
        super(Plot1D, self).__init__(parent=parent, backend=None,
                                     resetzoom=True, autoScale=True,
                                     logScale=True, grid=True,
                                     curveStyle=True, colormap=False,
                                     aspectRatio=False, yInverted=False,
                                     copy=True, save=True, print_=True,
                                     control=True, position=True,
                                     roi=True, mask=False)
        if parent is None:
            self.setWindowTitle('Plot1D')
        self.setGraphXLabel('X')
        self.setGraphYLabel('Y')
 
[docs]class Plot2D(PlotWindow):
    """PlotWindow with a toolbar specific for images.
    :param parent: The parent of this widget
    """
    def __init__(self, parent=None):
        # List of information to display at the bottom of the plot
        posInfo = [
            ('X', lambda x, y: x),
            ('Y', lambda x, y: y),
            ('Data', self._getActiveImageValue)]
        super(Plot2D, self).__init__(parent=parent, backend=None,
                                     resetzoom=True, autoScale=False,
                                     logScale=False, grid=False,
                                     curveStyle=False, colormap=True,
                                     aspectRatio=True, yInverted=True,
                                     copy=True, save=True, print_=True,
                                     control=False, position=posInfo,
                                     roi=False, mask=True)
        if parent is None:
            self.setWindowTitle('Plot2D')
        self.setGraphXLabel('Columns')
        self.setGraphYLabel('Rows')
        self.profile = ProfileToolBar(self)
        """"Profile tools attached to this plot.
        See :class:`silx.gui.plot.PlotTools.ProfileToolBar`
        """
        self.addToolBar(self.profile)
    def _getActiveImageValue(self, x, y):
        """Get value of active image at position (x, y)
        :param float x: X position in plot coordinates
        :param float y: Y position in plot coordinates
        :return: The value at that point or '-'
        """
        image = self.getActiveImage()
        if image is not None:
            data, params = image[0], image[4]
            ox, oy = params['origin']
            sx, sy = params['scale']
            if (y - oy) >= 0 and (x - ox) >= 0:
                # Test positive before cast otherwisr issue with int(-0.5) = 0
                row = int((y - oy) / sy)
                col = int((x - ox) / sx)
                if (row < data.shape[0] and col < data.shape[1]):
                    return data[row, col]
        return '-'
 
[docs]def plot1D(x_or_y=None, y=None, title='', xlabel='X', ylabel='Y'):
    """Plot curves in a dedicated widget.
    Examples:
    The following examples must run with a Qt QApplication initialized.
    First import :func:`plot1D` function:
    >>> from silx.gui.plot import plot1D
    >>> import numpy
    Plot a single curve given some values:
    >>> values = numpy.random.random(100)
    >>> plot_1curve = plot1D(values, title='Random data')
    Plot a single curve given the x and y values:
    >>> angles = numpy.linspace(0, numpy.pi, 100)
    >>> sin_a = numpy.sin(angles)
    >>> plot_sinus = plot1D(angles, sin_a,
    ...                     xlabel='angle (radian)', ylabel='sin(a)')
    Plot many curves by giving a 2D array:
    >>> curves = numpy.random.random(10 * 100).reshape(10, 100)
    >>> plot_curves = plot1D(curves)
    Plot many curves sharing the same x values:
    >>> angles = numpy.linspace(0, numpy.pi, 100)
    >>> values = (numpy.sin(angles), numpy.cos(angles))
    >>> plot = plot1D(angles, values)
    :param x_or_y: x values or y values if y is not provided
    :param y: y values (x_or_y) must be provided
    :param str title: The title of the Plot widget
    :param str xlabel: The label of the X axis
    :param str ylabel: The label of the Y axis
    """
    plot = Plot1D()
    plot.setGraphTitle(title)
    plot.setGraphXLabel(xlabel)
    plot.setGraphYLabel(ylabel)
    # Handle x_or_y and y arguments
    if x_or_y is None and y is not None:
        # Only y is provided, reorder arguments
        x_or_y, y = y, None
    if x_or_y is not None:
        x_or_y = numpy.array(x_or_y, copy=False)
        if y is None:  # x_or_y is y and no x provided, create x values
            y = x_or_y
            x_or_y = numpy.arange(x_or_y.shape[-1], dtype=numpy.float32)
        y = numpy.array(y, copy=False)
        y = y.reshape(-1, y.shape[-1])  # Make it 2D array
        if x_or_y.ndim == 1:
            for index, ycurve in enumerate(y):
                plot.addCurve(x_or_y, ycurve, legend=('curve_%d' % index))
        else:
            # Make x a 2D array as well
            x_or_y = x_or_y.reshape(-1, x_or_y.shape[-1])
            if x_or_y.shape[0] != y.shape[0]:
                raise ValueError(
                    'Not the same dimensions for x and y (%d != %d)' %
                    (x_or_y.shape[0], y.shape[0]))
            for index, (xcurve, ycurve) in enumerate(zip(x_or_y, y)):
                plot.addCurve(xcurve, ycurve, legend=('curve_%d' % index))
    plot.show()
    return plot
 
[docs]def plot2D(data=None, cmap=None, norm='linear',
           vmin=None, vmax=None,
           aspect=False,
           origin=(0., 0.), scale=(1., 1.),
           title='', xlabel='X', ylabel='Y'):
    """Plot an image in a dedicated widget.
    Example to plot an image.
    This example must run with a Qt QApplication initialized.
    >>> from silx.gui.plot import plot2D
    >>> import numpy
    >>> data = numpy.random.random(1024 * 1024).reshape(1024, 1024)
    >>> plot = plot2D(data, title='Random data')
    :param data: data to plot as an image
    :type data: numpy.ndarray-like with 2 dimensions
    :param str cmap: The name of the colormap to use for the plot.
    :param str norm: The normalization of the colormap:
                     'linear' (default) or 'log'
    :param float vmin: The value to use for the min of the colormap
    :param float vmax: The value to use for the max of the colormap
    :param bool aspect: True to keep aspect ratio (Default: False)
    :param origin: (ox, oy) The origin of the image in the plot
    :type origin: 2-tuple of floats
    :param scale: (sx, sy) The scale of the image in the plot
                  (i.e., the size of the image's pixel in plot coordinates)
    :type scale: 2-tuple of floats
    :param str title: The title of the Plot widget
    :param str xlabel: The label of the X axis
    :param str ylabel: The label of the Y axis
    """
    plot = Plot2D()
    plot.setGraphTitle(title)
    plot.setGraphXLabel(xlabel)
    plot.setGraphYLabel(ylabel)
    # Update default colormap with input parameters
    colormap = plot.getDefaultColormap()
    if cmap is not None:
        colormap['name'] = cmap
    colormap['normalization'] = norm
    if vmin is not None:
        colormap['vmin'] = vmin
    if vmax is not None:
        colormap['vmax'] = vmax
    if vmin is not None and vmax is not None:
        colormap['autoscale'] = False
    plot.setDefaultColormap(colormap)
    plot.setKeepDataAspectRatio(aspect)
    if data is not None:
        plot.addImage(data, origin=origin, scale=scale)
    plot.show()
    return plot