# Copyright (c) 2011-2014, B.I.Stepanov Institute of Physics, National Academy
# of Sciences of Belarus.
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc., 51
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

"""Widgets for visual representation of data used in the aerosol profile
retrieval algorithm."""

import numpy
import os.path

from PyQt4.QtCore import *
from PyQt4.QtGui import *
from PyQt4.Qwt5 import *

from common.utils.third_party import emf_export

from LiricOutput import *
from PreparedLidarInput import *

try:
    from RamanOutput import *
except ImportError:
    ramanOutputPluginLoaded = False
else:
    ramanOutputPluginLoaded = True

try:
    from PolarOutput import *
except ImportError:
    polarOutputPluginLoaded = False
else:
    polarOutputPluginLoaded = True

__all__ = ['LidarPlotWidget', 'DispersionPlotWidget', 'LiricPlotWidget']

if ramanOutputPluginLoaded:
    __all__ += ['RamanPlotWidget']

if polarOutputPluginLoaded:
    __all__ += ['PolarPlotWidget']

# *****************************************************************************
class _PlotDisplayStyle:
    """Common appearance settings, used in construction and population of plot
    widgets.

    To achieve best results both when the data are displayed on the screen and
    saved to image files, several consistent setting sets are provided,
    selected by the 'displayMode' attribute passed to constructor. Thus, to
    save a plot to a file, one has to create a separate instance of the plot
    widget, using an appropriate 'displayMode'."""

    def __init__(self, displayMode):
        """Parameters:
          - 'displayMode': purpose of the plot widget to retrieve the settings
            for. May be 'screen' for an ordinary plot widget, 'image' for a
            special widget instance used to save the data to a raster image
            file, or 'emf' for a special instance used to export the data to
            Windows enhanced metafile format."""

        assert displayMode in ('screen', 'image', 'emf')

        self.displayMode = displayMode

    def getDisplayMode(self):
        return self.displayMode

    def getAxesFont(self, plotWidget):
        """Return 'QFont' used for axes labels."""

        if self.displayMode == 'screen':
            # Derive axis fonts from the widget's default font.
            return plotWidget.font()
        else:
            return QFont('Arial', 8)

    def getTextFont(self, plotWidget):
        """Return 'QFont' used for text messages printed on canvas."""
        return self.getAxesFont(plotWidget)

    def getDpi(self):
        """Return resolution of the image file, in pixels per inch.

        Not to be used with the 'screen' display mode."""

        if self.displayMode == 'image':
            # Small values lead to less detail; large values lead to thinning
            # of cosmetic pens and increased file size.
            return 200

        elif self.displayMode == 'emf':
            # This defines the coordinate grid step. However, very large values
            # seem to slow down office applications when the image is pasted.
            return 1200

    def getScalePenWidth(self):
        """Return width of the pen to draw the axes scales, in pixels."""
        # Qwt will currently fail to align scale components correctly if the
        # pen width is non-zero (i.e., the pen is not cosmetic) and image's
        # resolution is different from that returned by the widget (i.e., the
        # screen resolution by default). Always use cosmetic pen to avoid the
        # problem, as it moreover looks pretty good.
        return 0

    def getPenWidth(self):
        """Return pen width for plots, axes and grid lines, in pixels."""

        if self.displayMode == 'emf':
            penWidthMm = 0.3
            # Pen width in device pixels.
            deviceWidth = round(penWidthMm / 25.4 * self.getDpi())
            # Qwt assumes that pen widths are specified in screen pixels.
            # To make generated pictures independent of the screen resolution,
            # we have to apply the reverse conversion here.
            return (deviceWidth *
                QApplication.desktop().logicalDpiX() / self.getDpi())
        else:
            # Use cosmetic pens in raster images for cleaner look.
            return 0

    def getMajorGridColor(self):
        return QColor(197, 197, 197)

    def getMinorGridColor(self):
        return QColor(231, 231, 231)

# *****************************************************************************
class _QwtPlotForPrinting(QwtPlot):
    """A variation of 'QwtPlot' for special plot widget instances used solely
    to save the data to image files."""

    # ---- Private overridden methods -----------------------------------------
    def metric(self, metric):
        """Redefinition of a virtual function."""
        if metric in (QPaintDevice.PdmDpiX, QPaintDevice.PdmDpiY):
            # Lenghts of the axes ticks are currently specified in widget's
            # pixels in Qwt, and they have to be integers (see
            # 'QwtAbstractScaleDraw.setTickLength'). To make generated pictures
            # truly independent of the screen resolution, we have to redefine
            # the resolution of the widget.
            return 96
        else:
            return QwtPlot.metric(self, metric)

# *****************************************************************************
class LidarPlotWidget(QWidget):
    """Plot of a set of normalized lidar signals at different wavelengths."""

    # ---- Public methods -----------------------------------------------------
    def __init__(self, parent = None):
        QWidget.__init__(self, parent)

        layout = QHBoxLayout()
        self.plot = createPlotWidget(self)
        layout.addWidget(self.plot)

        layout.setContentsMargins(0, 0, 0, 0)
        self.setLayout(layout)

    def clear(self):
        self.plot.clear()
        self.plot.replot()

    def plotData(self, lidarInputs, joinIndex = None):
        """Parameters:
          - 'lidarInputs': a list of valid 'PreparedLidarInput' instances;
          - 'joinIndex': data grid index of the joint point between near and
            far signal zones or 'None' if the value is not known or if the
            signal has not been joined."""

        self.plot.clear()

        if len(lidarInputs) == 0:
            self.plot.replot()
            return

        for lidarInput in lidarInputs:
            addPlot(self.plot, numpy.arange(lidarInput.firstInputIndex,
                lidarInput.lastInputIndex + 1), lidarInput.normalizedSignal,
                getPlotColorByChannelId(lidarInput.getChannelId()))

        addMarker(self.plot, 0.0, 0.0, QwtPlotMarker.Cross)
        # Display a vertical line at the join index.
        if joinIndex is not None:
            addMarker(self.plot, joinIndex, 0.0, QwtPlotMarker.VLine)

        # Use 'addTextMessages' to display a legend in the top right corner of
        # the canvas.
        textMessages = []

        # List only relevant legend entries, in a predefined order.
        channelIds = ('355', '387R', '532', '607R', '1064', '532C')
        channelLabels = ('355 nm', '387 nm R', '532 nm', '607 nm R', '1064 nm',
            u'532 nm \u22a5')

        for i in range(len(channelIds)):
            if any(input.getChannelId() == channelIds[i]
                for input in lidarInputs):
                textMessages.append(getLegendEntryStr(
                    channelLabels[i], getPlotColorByChannelId(channelIds[i])))

        # This will fail if there are no supported channels in 'lidarInputs'.
        assert len(textMessages) > 0
        addTextMessages(self.plot, textMessages)

        self.plot.replot()

# *****************************************************************************
class DispersionPlotWidget(QWidget):
    """Plot of a set of lidar dispersion arrays at different wavelengths."""

    # ---- Public methods -----------------------------------------------------
    def __init__(self, parent = None):
        QWidget.__init__(self, parent)

        layout = QHBoxLayout()
        self.plot = createPlotWidget(self)
        layout.addWidget(self.plot)

        layout.setContentsMargins(0, 0, 0, 0)
        self.setLayout(layout)

    def clear(self):
        self.plot.clear()
        self.plot.replot()

    def plotData(self, lidarInputs, joinIndex = None,
        infiniteDispersion = None):
        """Parameters:
          - 'lidarInputs': a list of valid 'PreparedLidarInput' instances;
          - 'joinIndex': data grid index of the joint point between near and
            far signal zones or 'None' if the value is not known or if the
            signal has not been joined.
          - 'infiniteDispersion': dispersion value that has to be considered as
            an infinite one, and displayed accordingly."""

        self.plot.clear()

        if len(lidarInputs) == 0:
            self.plot.replot()
            return

        for lidarInput in lidarInputs:

            # Replace infinite dispersion values, if any, with zeroes.
            dispersionData = lidarInput.lidarDispersion.copy()
            if infiniteDispersion is not None:
                dispersionData[dispersionData >= infiniteDispersion] = 0.0

            addPlot(self.plot, numpy.arange(lidarInput.firstInputIndex,
                lidarInput.lastInputIndex + 1), dispersionData,
                getPlotColorByChannelId(lidarInput.getChannelId()))

        # If infinite dispersion values are present, they will be replaced now
        # by the upper bound of the remaining data. Infinite points will thus
        # bounce to the top of the plot, but won't disrupt the picture.
        if infiniteDispersion is not None:

            # Calculate the topmost point displayed by the grid.
            self.plot.updateAxes()
            upperBound = self.plot.axisScaleDiv(QwtPlot.yLeft).upperBound()

            # Replace the discarded infinite values with 'upperBound'.
            self.plot.clear()

            for lidarInput in lidarInputs:

                dispersionData = lidarInput.lidarDispersion.copy()
                dispersionData[dispersionData >=
                    infiniteDispersion] = upperBound

                addPlot(self.plot, numpy.arange(lidarInput.firstInputIndex,
                    lidarInput.lastInputIndex + 1), dispersionData,
                    getPlotColorByChannelId(lidarInput.getChannelId()))

        addMarker(self.plot, 0.0, 0.0, QwtPlotMarker.Cross)
        # Display a vertical line at the join index.
        if joinIndex is not None:
            addMarker(self.plot, joinIndex, 0.0, QwtPlotMarker.VLine)

        self.plot.replot()

# *****************************************************************************
class LiricPlotWidget(QWidget):
    """A series of plots visually representing a 'LiricOutput' instance.

    This class may be used to save the data to an image file. In order to do
    so, create its separate instance, passing an appropriate 'displayMode' to
    constructor (see '_PlotDisplayStyle'), call 'plotData', and then call
    'saveAsImage'."""

    # ---- Public methods -----------------------------------------------------
    def __init__(self, displayMode = 'screen', parent = None):
        QWidget.__init__(self, parent)

        self.displayStyle = _PlotDisplayStyle(displayMode)

        layout = QGridLayout()

        self.plot355 = createPlotWidget(self, self.displayStyle)
        layout.addWidget(self.plot355, 0, 0)

        self.plot532 = createPlotWidget(self, self.displayStyle)
        layout.addWidget(self.plot532, 0, 1)

        self.plot1064 = createPlotWidget(self, self.displayStyle)
        layout.addWidget(self.plot1064, 1, 0)

        self.plot532C = createPlotWidget(self, self.displayStyle)

        # Polarimetric plots are available for polarimetric measurements only.
        self.plot532CLabel = QLabel(
            'Polarimetric measurement is not available')
        self.plot532CLabel.setAlignment(Qt.AlignHCenter | Qt.AlignBottom)
        self.plot532CLabel.setWordWrap(True)
        self.plot532CStack = QStackedWidget()
        self.plot532CStack.addWidget(self.plot532CLabel)
        self.plot532CStack.addWidget(self.plot532C)
        self.plot532CStack.setCurrentWidget(self.plot532CLabel)
        layout.addWidget(self.plot532CStack, 1, 1)

        self.concentrationPlot = createPlotWidget(self, self.displayStyle)
        layout.addWidget(self.concentrationPlot, 0, 2, 2, 1)

        layout.setRowStretch(0, 1)
        layout.setRowStretch(1, 1)
        layout.setColumnStretch(0, 1)
        layout.setColumnStretch(1, 1)
        layout.setColumnStretch(2, 1)

        layout.setContentsMargins(0, 0, 0, 0)
        self.setLayout(layout)

    def clear(self):
        self.plot355.clear()
        self.plot532.clear()
        self.plot1064.clear()
        self.plot532C.clear()
        self.concentrationPlot.clear()

        self.plot355.replot()
        self.plot532.replot()
        self.plot1064.replot()
        self.plot532C.replot()
        self.concentrationPlot.replot()

        self.plot532CStack.setCurrentWidget(self.plot532CLabel)

    def plotData(self, liricOutput):
        """Parameters:
          - 'liricOutput': a valid 'LiricOutput' instance."""

        # When the data are saved to a file, horizontal axes must display
        # heights instead of data grid indices.
        if self.displayStyle.getDisplayMode() == 'screen':
            heightConversion = 1.0
        else:
            # Convert meters to kilometers for clarity.
            heightConversion = liricOutput.gridHeightStep * 0.001

        # Update the lidar signal plots.
        for channelId in liricOutput.lidarChannelIds():

            plotWidget = getattr(self, 'plot' + channelId)

            measuredSignal = getattr(liricOutput, 'measuredSignal' + channelId)
            calculatedSignal = getattr(liricOutput,
                'calculatedSignal' + channelId)

            plotWidget.clear()

            firstIndex = getattr(liricOutput, 'firstInputIndex' + channelId)
            if firstIndex is None:
                firstIndex = 0

            addPlot(plotWidget, numpy.arange(
                firstIndex, len(measuredSignal)) * heightConversion,
                measuredSignal[firstIndex :],
                getPlotColorByChannelId(channelId), self.displayStyle)
            addPlot(plotWidget, numpy.arange(
                len(calculatedSignal)) * heightConversion, calculatedSignal,
                Qt.black, self.displayStyle)

            # Display the axes.
            addMarker(plotWidget, 0.0, 0.0, QwtPlotMarker.Cross,
                self.displayStyle)

            # Mark off the lidar signal joining height, if available.
            if liricOutput.joinIndex is not None:
                addMarker(plotWidget, liricOutput.joinIndex, 0.0,
                    QwtPlotMarker.VLine, self.displayStyle)

            # Display the textual information.
            heightStep = liricOutput.gridHeightStep

            # Total retrieved aerosol optical thickness.
            aot = 0.0
            # Integrated retrieved aerosol backscatter coefficient.
            bInt = 0.0

            for modeSuffix in liricOutput.aerosolModeSuffixes():
                backscatter = getattr(liricOutput, 'b' + modeSuffix +
                    channelId)
                attenuation = getattr(liricOutput, 'a' + modeSuffix +
                    (channelId if channelId != '532C' else '532'))

                profile = getattr(liricOutput, 'profile' + modeSuffix)

                aot += attenuation * profile.sum() * heightStep
                bInt += backscatter * profile.sum() * heightStep

            lidarRatio = aot / bInt

            wavelengthStr = channelId
            if channelId == '532C':
                wavelengthStr = u'532,\u22a5'

            addTextMessages(plotWidget, [
                getNameValueStr(u'\u03bb', wavelengthStr),
                getNameValueStr('AOT', '%.3f' % aot),
                getNameValueStr('Bint', '%.2e' % bInt),
                getNameValueStr('P', '%.1f' % lidarRatio)], self.displayStyle)

            plotWidget.replot()

        # Depending on the measurement type, polarimetric plots have to be
        # either manually shown or hidden.
        if '532C' not in liricOutput.lidarChannelIds():
            self.plot532C.clear()
            self.plot532C.replot()
            self.plot532CStack.setCurrentWidget(self.plot532CLabel)
        else:
            self.plot532CStack.setCurrentWidget(self.plot532C)

        # Update the concentrations plot.
        self.concentrationPlot.clear()

        # Use 'addTextMessages' to display a legend in the top right corner of
        # the canvas.
        textMessages = []

        for modeSuffix in liricOutput.aerosolModeSuffixes():
            profile = getattr(liricOutput, 'profile' + modeSuffix)

            addPlot(self.concentrationPlot, profile,
                numpy.arange(len(profile)) * heightConversion,
                getPlotColorByModeSuffix(modeSuffix), self.displayStyle)

            profileDispersion = getattr(liricOutput,
                'profileDispersion' + modeSuffix)

            if profileDispersion is not None:
                addPlot(self.concentrationPlot, profile + profileDispersion,
                    numpy.arange(len(profile)) * heightConversion,
                    getPlotColorByModeSuffix(modeSuffix), self.displayStyle,
                    penStyle = Qt.DashLine)

            textMessages.append(getLegendEntryStr(
                modeSuffix.lower(), getPlotColorByModeSuffix(modeSuffix)))

        # Draw attention to discrepancies in aerosol concentrations by
        # highliting problematic values with various shades of red.
        vFineColor = getValueColor(liricOutput.calculatedVFine,
            liricOutput.vFine, 0.05, 0.1)
        textMessages.append(getNameValueStr('V-F',
            '%.3f' % liricOutput.calculatedVFine, vFineColor))

        vCoarseColor = getValueColor(liricOutput.calculatedVCoarse,
            liricOutput.vCoarse, 0.05, 0.1)
        textMessages.append(getNameValueStr('V-C',
            '%.3f' % liricOutput.calculatedVCoarse, vCoarseColor))

        if (liricOutput.vSpherical is not None and
            liricOutput.calculatedVSpherical is not None):

            vSphericalColor = getValueColor(
                liricOutput.calculatedVSpherical,
                liricOutput.vSpherical, 0.05, 0.1)
            textMessages.append(getNameValueStr('V-Cs',
                '%.3f' % liricOutput.calculatedVSpherical,
                vSphericalColor))

        if (liricOutput.vSpheroid is not None and
            liricOutput.calculatedVSpheroid is not None):

            vSpheroidColor = getValueColor(liricOutput.calculatedVSpheroid,
                liricOutput.vSpheroid, 0.05, 0.1)
            textMessages.append(getNameValueStr('V-Cns',
                '%.3f' % liricOutput.calculatedVSpheroid, vSpheroidColor))

        # Display the axes.
        addMarker(self.concentrationPlot, 0.0, 0.0, QwtPlotMarker.Cross,
            self.displayStyle)

        # Mark off the lidar signal joining height, if available.
        if liricOutput.joinIndex is not None:
            addMarker(self.concentrationPlot, liricOutput.joinIndex, 0.0,
                QwtPlotMarker.VLine, self.displayStyle)

        assert len(textMessages) > 0
        addTextMessages(self.concentrationPlot, textMessages)

        self.concentrationPlot.replot()

    def saveAsImage(self, imageFilePath):
        """Save the plots to an image file at the specified path."""

        # Dimensions of the image in millimeters.
        imageWidthMm = 168.0    # 56 * 3
        imageHeightMm = 84.0    # 42 * 2

        # Distance between a plot and the border of the image, in millimeters.
        outerPlotMarginMm = 1.0
        # Half of a distance between two neighbouring plots, in millimeters.
        innerPlotMarginMm = 1.0

        if self.displayStyle.getDisplayMode() == 'image':
            # Image width in pixels.
            imageWidth = int(round(
                imageWidthMm / 25.4 * self.displayStyle.getDpi()))
            imageHeight = int(round(
                imageHeightMm / 25.4 * self.displayStyle.getDpi()))
            # imageHeight = int(round(imageWidth * imageHeightMm / imageWidthMm))

            image = QImage(QSize(imageWidth, imageHeight),
                QImage.Format_RGB32)

            # Set image resolution so that it fits the required physical size.
            # Font sizes may thus be specified in physical points.
            imageDotsPerMeter = int(round(imageWidth / imageWidthMm * 1000.0))
            image.setDotsPerMeterX(imageDotsPerMeter)
            image.setDotsPerMeterY(imageDotsPerMeter)

            painter = QPainter(image)

            # Draw the background by hand, as 'QwtPlot' seems to fail with it.
            painter.fillRect(0, 0, imageWidth, imageHeight, Qt.white)

        elif self.displayStyle.getDisplayMode() == 'emf':
            # Convert millimeters to inches and set the grain size of the image
            # coordinate grid.
            emfPaintDevice = emf_export.EMFPaintDevice(
                imageWidthMm / 25.4, imageHeightMm / 25.4,
                dpi = self.displayStyle.getDpi())

            imageWidth = emfPaintDevice.width()
            imageHeight = emfPaintDevice.height()

            painter = QPainter(emfPaintDevice)

        # Convert millimeters to pixels.
        outerPlotMargin = int(round(outerPlotMarginMm *
            imageWidth / imageWidthMm))
        innerPlotMargin = int(round(innerPlotMarginMm *
            imageWidth / imageWidthMm))

        plotWidth = imageWidth / 3 - outerPlotMargin - innerPlotMargin
        plotHeight = imageHeight / 2 - outerPlotMargin - innerPlotMargin

        # Bounding rectangles for the 5 plots combined in the image.
        #
        # Use ceiling values for coordinates at the vertical center and at the
        # right side of the image, so that symmetricity is achieved for both
        # even and odd image sizes in pixels.
        rect355 = QRect(
            outerPlotMargin, outerPlotMargin,
            plotWidth, plotHeight)
        rect1064 = QRect(
            outerPlotMargin, (imageHeight + 1) / 2 + innerPlotMargin,
            plotWidth, plotHeight)
        rect532 = QRect(
            imageWidth / 3 + innerPlotMargin, outerPlotMargin,
            plotWidth, plotHeight)
        rect532C = QRect(
            imageWidth / 3 + innerPlotMargin,
            (imageHeight + 1) / 2 + innerPlotMargin,
            plotWidth, plotHeight)

        concentrationRect = QRect(
            (imageWidth * 2 + 2) / 3 + innerPlotMargin, outerPlotMargin,
            plotWidth, imageHeight - outerPlotMargin * 2)

        printFilter = QwtPlotPrintFilter()
        printOptions = QwtPlotPrintFilter.PrintAll
        # Draw bounding rectangles around the plot grids.
        printOptions |= QwtPlotPrintFilter.PrintFrameWithScales
        printFilter.setOptions(printOptions)

        self.plot355.print_(painter, rect355, printFilter)
        self.plot532.print_(painter, rect532, printFilter)
        self.plot1064.print_(painter, rect1064, printFilter)
        if self.plot532CStack.currentWidget() == self.plot532C:
            self.plot532C.print_(painter, rect532C, printFilter)
        self.concentrationPlot.print_(painter, concentrationRect, printFilter)

        painter.end()

        if self.displayStyle.getDisplayMode() == 'image':
            image.save(imageFilePath)

        elif self.displayStyle.getDisplayMode() == 'emf':
            emfPaintDevice.paintEngine().saveFile(imageFilePath)

# *****************************************************************************
if ramanOutputPluginLoaded:
    class RamanPlotWidget(QWidget):
        """A series of plots visually representing a 'RamanOutput' instance."""

        # ---- Public methods -----------------------------------------------------
        def __init__(self, displayMode = 'screen', parent = None):
            QWidget.__init__(self, parent)

            self.displayStyle = _PlotDisplayStyle(displayMode)

            layout = QGridLayout()

            self.plotSource = createPlotWidget(self, self.displayStyle)
            layout.addWidget(self.plotSource, 0, 0)

            self.plotRaman = createPlotWidget(self, self.displayStyle)
            layout.addWidget(self.plotRaman, 1, 0)

            self.plotBackscatter = createPlotWidget(self, self.displayStyle)
            layout.addWidget(self.plotBackscatter, 0, 1, 2, 1)

            self.plotExtinction = createPlotWidget(self, self.displayStyle)
            layout.addWidget(self.plotExtinction, 0, 2, 2, 1)

            self.plotLidar = createPlotWidget(self, self.displayStyle)
            layout.addWidget(self.plotLidar, 0, 3, 2, 1)

            layout.setRowStretch(0, 1)
            layout.setRowStretch(1, 1)
            layout.setColumnStretch(0, 1)
            layout.setColumnStretch(1, 1)
            layout.setColumnStretch(2, 1)
            layout.setColumnStretch(3, 1)

            layout.setContentsMargins(0, 0, 0, 0)
            self.setLayout(layout)

        def clear(self):
            self.plotSource.clear()
            self.plotRaman.clear()
            self.plotBackscatter.clear()
            self.plotExtinction.clear()
            self.plotLidar.clear()

            self.plotSource.replot()
            self.plotRaman.replot()
            self.plotBackscatter.replot()
            self.plotExtinction.replot()
            self.plotLidar.replot()

        def plotData(self, ramanOutput):
            """Parameters:
              - 'ramanOutput': a valid 'RamanOutput' instance."""

            # When the data are saved to a file, horizontal axes must display
            # heights instead of data grid indices.
            if self.displayStyle.getDisplayMode() == 'screen':
                heightConversion = 1.0
            else:
                # Convert meters to kilometers for clarity.
                heightConversion = ramanOutput.gridHeightStep * 0.001

            # Update the lidar signal plots.
            for channelId in ['Source', 'Raman']:

                plotWidget = getattr(self, 'plot' + channelId)

                measuredSignal = getattr(ramanOutput,
                    'measuredSignal' + channelId)
                calculatedSignal = getattr(ramanOutput,
                    'calculatedSignal' + channelId)

                firstInputIndex = getattr(ramanOutput,
                    'firstInputIndex' + channelId)

                plotWidget.clear()

                wavelengthId = '%.0f' % getattr(ramanOutput,
                    'wavelength' + channelId)
                if channelId == 'Raman':
                    wavelengthId += 'R'

                addPlot(plotWidget, numpy.arange(
                    firstInputIndex, len(measuredSignal)) * heightConversion,
                    measuredSignal[firstInputIndex :],
                    getPlotColorByChannelId(wavelengthId), self.displayStyle)
                addPlot(plotWidget, numpy.arange(
                    firstInputIndex, len(calculatedSignal)) * heightConversion,
                    calculatedSignal[firstInputIndex :],
                    Qt.black, self.displayStyle)

                # Display the axes.
                addMarker(plotWidget, 0.0, 0.0, QwtPlotMarker.Cross,
                    self.displayStyle)

                # Display textual information.
                wavelengthStr = '%.0f' % getattr(ramanOutput,
                    'wavelength' + channelId)

                addTextMessages(plotWidget,
                    [getNameValueStr(u'\u03bb', wavelengthStr)],
                    self.displayStyle)

                plotWidget.replot()

            # Update profile plots.
            self.plotBackscatter.clear()
            self.plotExtinction.clear()
            self.plotLidar.clear()

            firstOutputIndex = min(ramanOutput.firstInputIndexSource,
                ramanOutput.firstInputIndexRaman)

            # Update plots of the retrieved profiles.
            backscatterColor = QColor(0, 0, 255)

            addPlot(self.plotBackscatter,
                ramanOutput.profileBackscatter[firstOutputIndex :],
                numpy.arange(firstOutputIndex,
                    len(ramanOutput.profileBackscatter)) * heightConversion,
                backscatterColor, self.displayStyle)

            backscatterDispersion = ramanOutput.profileDispersionBackscatter

            if backscatterDispersion is not None:
                addPlot(self.plotBackscatter,
                    ramanOutput.profileBackscatter[firstOutputIndex :] +
                        backscatterDispersion[firstOutputIndex :],
                    numpy.arange(firstOutputIndex,
                        len(ramanOutput.profileBackscatter)) * heightConversion,
                    backscatterColor, self.displayStyle,
                    penStyle = Qt.DashLine)

            addMarker(self.plotBackscatter, 0.0, 0.0, QwtPlotMarker.Cross,
                self.displayStyle)

            addTextMessages(self.plotBackscatter, [getLegendEntryStr(
                'backscatter ratio minus 1', backscatterColor)])

            self.plotBackscatter.replot()

            addPlot(self.plotExtinction,
                ramanOutput.profileBackscatter[firstOutputIndex :] *
                ramanOutput.profileLidar[firstOutputIndex :],
                numpy.arange(firstOutputIndex,
                    len(ramanOutput.profileLidar)) * heightConversion,
                backscatterColor, self.displayStyle)

            addMarker(self.plotExtinction, 0.0, 0.0, QwtPlotMarker.Cross,
                self.displayStyle)

            addTextMessages(self.plotExtinction, [getLegendEntryStr(
                'extinction', backscatterColor)])

            self.plotExtinction.replot()

            lidarColor = QColor(0, 132, 0)

            addPlot(self.plotLidar,
                ramanOutput.profileLidar[firstOutputIndex :],
                numpy.arange(firstOutputIndex,
                    len(ramanOutput.profileLidar)) * heightConversion,
                lidarColor, self.displayStyle)

            lidarDispersion = ramanOutput.profileDispersionLidar

            if lidarDispersion is not None:
                addPlot(self.plotLidar,
                    ramanOutput.profileLidar[firstOutputIndex :] +
                        lidarDispersion[firstOutputIndex :],
                    numpy.arange(firstOutputIndex,
                        len(ramanOutput.profileLidar)) * heightConversion,
                    lidarColor, self.displayStyle,
                    penStyle = Qt.DashLine)

            addMarker(self.plotLidar, 0.0, 0.0, QwtPlotMarker.Cross,
                self.displayStyle)

            addTextMessages(self.plotLidar, [getLegendEntryStr(
                'lidar ratio', lidarColor)])

            self.plotLidar.replot()

        def saveAsImage(self, imageFilePath):
            """Save the plots to an image file at the specified path."""

            ### TODO: Create a universal approach to plot widgets and image
            # export. (This function is a copy-paste of 'LiricPlotWidget' with
            # minor changes).

            # Dimensions of the image in millimeters.
            imageWidthMm = 224.0    # 56 * 4
            imageHeightMm = 84.0    # 42 * 2

            # Distance between a plot and the border of the image, in millimeters.
            outerPlotMarginMm = 1.0
            # Half of a distance between two neighbouring plots, in millimeters.
            innerPlotMarginMm = 1.0

            if self.displayStyle.getDisplayMode() == 'image':
                # Image width in pixels.
                imageWidth = int(round(
                    imageWidthMm / 25.4 * self.displayStyle.getDpi()))
                imageHeight = int(round(
                    imageHeightMm / 25.4 * self.displayStyle.getDpi()))
                # imageHeight = int(round(imageWidth * imageHeightMm / imageWidthMm))

                image = QImage(QSize(imageWidth, imageHeight),
                    QImage.Format_RGB32)

                # Set image resolution so that it fits the required physical size.
                # Font sizes may thus be specified in physical points.
                imageDotsPerMeter = int(round(imageWidth / imageWidthMm * 1000.0))
                image.setDotsPerMeterX(imageDotsPerMeter)
                image.setDotsPerMeterY(imageDotsPerMeter)

                painter = QPainter(image)

                # Draw the background by hand, as 'QwtPlot' seems to fail with it.
                painter.fillRect(0, 0, imageWidth, imageHeight, Qt.white)

            elif self.displayStyle.getDisplayMode() == 'emf':
                # Convert millimeters to inches and set the grain size of the image
                # coordinate grid.
                emfPaintDevice = emf_export.EMFPaintDevice(
                    imageWidthMm / 25.4, imageHeightMm / 25.4,
                    dpi = self.displayStyle.getDpi())

                imageWidth = emfPaintDevice.width()
                imageHeight = emfPaintDevice.height()

                painter = QPainter(emfPaintDevice)

            # Convert millimeters to pixels.
            outerPlotMargin = int(round(outerPlotMarginMm *
                imageWidth / imageWidthMm))
            innerPlotMargin = int(round(innerPlotMarginMm *
                imageWidth / imageWidthMm))

            plotWidth = imageWidth / 4 - outerPlotMargin - innerPlotMargin
            plotHeight = imageHeight / 2 - outerPlotMargin - innerPlotMargin

            # Bounding rectangles for the 5 plots combined in the image.
            #
            # Use ceiling values for coordinates at the vertical center and
            # at the right side of the image, so that symmetricity is achieved
            # for both even and odd image sizes in pixels.
            rectSource = QRect(
                outerPlotMargin, outerPlotMargin,
                plotWidth, plotHeight)
            rectRaman = QRect(
                outerPlotMargin, (imageHeight + 1) / 2 + innerPlotMargin,
                plotWidth, plotHeight)
            rectBackscatter = QRect(
                (imageWidth + 3) / 4 + innerPlotMargin, outerPlotMargin,
                plotWidth, imageHeight - outerPlotMargin * 2)
            rectExtinction = QRect(
                (imageWidth * 2 + 3) / 4 + innerPlotMargin, outerPlotMargin,
                plotWidth, imageHeight - outerPlotMargin * 2)
            rectLidar = QRect(
                (imageWidth * 3 + 3) / 4 + innerPlotMargin, outerPlotMargin,
                plotWidth, imageHeight - outerPlotMargin * 2)

            printFilter = QwtPlotPrintFilter()
            printOptions = QwtPlotPrintFilter.PrintAll
            # Draw bounding rectangles around the plot grids.
            printOptions |= QwtPlotPrintFilter.PrintFrameWithScales
            printFilter.setOptions(printOptions)

            self.plotSource.print_(painter, rectSource, printFilter)
            self.plotRaman.print_(painter, rectRaman, printFilter)
            self.plotBackscatter.print_(painter, rectBackscatter, printFilter)
            self.plotExtinction.print_(painter, rectExtinction, printFilter)
            self.plotLidar.print_(painter, rectLidar, printFilter)

            painter.end()

            if self.displayStyle.getDisplayMode() == 'image':
                image.save(imageFilePath)

            elif self.displayStyle.getDisplayMode() == 'emf':
                emfPaintDevice.paintEngine().saveFile(imageFilePath)

# *****************************************************************************
if polarOutputPluginLoaded:
    class PolarPlotWidget(QWidget):
        """A series of plots visually representing a 'PolarOutput' instance."""

        # ---- Public methods -----------------------------------------------------
        def __init__(self, displayMode = 'screen', parent = None):
            QWidget.__init__(self, parent)

            self.displayStyle = _PlotDisplayStyle(displayMode)

            layout = QGridLayout()

            self.plotBase = createPlotWidget(self, self.displayStyle)
            layout.addWidget(self.plotBase, 0, 0)

            self.plotCross = createPlotWidget(self, self.displayStyle)
            layout.addWidget(self.plotCross, 1, 0)

            self.plotBsParallel = createPlotWidget(self, self.displayStyle)
            layout.addWidget(self.plotBsParallel, 0, 1)

            self.plotBsCross = createPlotWidget(self, self.displayStyle)
            layout.addWidget(self.plotBsCross, 1, 1)

            self.plotBsTotal = createPlotWidget(self, self.displayStyle)
            layout.addWidget(self.plotBsTotal, 0, 2)

            self.plotDepolar = createPlotWidget(self, self.displayStyle)
            layout.addWidget(self.plotDepolar, 1, 2)

            layout.setRowStretch(0, 1)
            layout.setRowStretch(1, 1)
            layout.setColumnStretch(0, 1)
            layout.setColumnStretch(1, 1)
            layout.setColumnStretch(2, 1)

            layout.setContentsMargins(0, 0, 0, 0)
            self.setLayout(layout)

        def clear(self):
            self.plotBase.clear()
            self.plotCross.clear()
            self.plotBsParallel.clear()
            self.plotBsCross.clear()
            self.plotBsTotal.clear()
            self.plotDepolar.clear()

            self.plotBase.replot()
            self.plotCross.replot()
            self.plotBsParallel.replot()
            self.plotBsCross.replot()
            self.plotBsTotal.replot()
            self.plotDepolar.replot()

        def plotData(self, polarOutput):
            """Parameters:
              - 'polarOutput': a valid 'PolarOutput' instance."""

            # When the data are saved to a file, horizontal axes must display
            # heights instead of data grid indices.
            if self.displayStyle.getDisplayMode() == 'screen':
                heightConversion = 1.0
            else:
                # Convert meters to kilometers for clarity.
                heightConversion = polarOutput.gridHeightStep * 0.001

            # Update the lidar signal plots.
            for channelId in ['Base', 'Cross']:

                plotWidget = getattr(self, 'plot' + channelId)

                measuredSignal = getattr(polarOutput,
                    'measuredSignal' + channelId)
                calculatedSignal = getattr(polarOutput,
                    'calculatedSignal' + channelId)

                firstInputIndex = getattr(polarOutput,
                    'firstInputIndex' + channelId)

                plotWidget.clear()

                addPlot(plotWidget, numpy.arange(
                    firstInputIndex, len(measuredSignal)) * heightConversion,
                    measuredSignal[firstInputIndex :],
                    getPlotColorByChannelId('532'), self.displayStyle)
                addPlot(plotWidget, numpy.arange(
                    firstInputIndex, len(calculatedSignal)) * heightConversion,
                    calculatedSignal[firstInputIndex :],
                    Qt.black, self.displayStyle)

                # Display the axes.
                addMarker(plotWidget, 0.0, 0.0, QwtPlotMarker.Cross,
                    self.displayStyle)

                # Display textual information.
                if channelId == 'Base':
                    wavelengthStr = '532'
                else:
                    wavelengthStr = u'532 \u22a5'

                textMessages = [getNameValueStr(u'\u03bb', wavelengthStr)]

                if channelId == 'Base':
                    textMessages.append(getNameValueStr('R',
                    '%.1e' % polarOutput.aerBackscatterRatioRef))

                if channelId == 'Cross':
                    textMessages.append(getNameValueStr('Q',
                    '%.1e' % polarOutput.lidarCorrection))

                addTextMessages(plotWidget, textMessages, self.displayStyle)

                plotWidget.replot()

            # Update profile plots.
            self.plotBsParallel.clear()
            self.plotBsCross.clear()
            self.plotBsTotal.clear()
            self.plotDepolar.clear()

            firstOutputIndex = min(polarOutput.firstInputIndexBase,
                polarOutput.firstInputIndexCross)

            # Update parallel backscatter plot.
            addPlot(self.plotBsParallel,
                numpy.arange(firstOutputIndex,
                    len(polarOutput.backscatterParallel)) * heightConversion,
                polarOutput.backscatterParallel[firstOutputIndex :],
                getPlotColorByChannelId('532'), self.displayStyle)

            backscatterDispersion = polarOutput.backscatterDispersionParallel

            if backscatterDispersion is not None:
                addPlot(self.plotBsParallel,
                    numpy.arange(firstOutputIndex,
                        len(polarOutput.backscatterParallel)) *
                        heightConversion,
                    polarOutput.backscatterParallel[firstOutputIndex :] +
                        backscatterDispersion[firstOutputIndex :],
                    getPlotColorByChannelId('532'), self.displayStyle,
                    penStyle = Qt.DashLine)

            addMarker(self.plotBsParallel, 0.0, 0.0, QwtPlotMarker.Cross,
                self.displayStyle)

            addTextMessages(self.plotBsParallel, [getLegendEntryStr(
                'parallel backscatter', getPlotColorByChannelId('532'))])

            self.plotBsParallel.replot()

            # Update cross-polarized backscatter plot.
            addPlot(self.plotBsCross,
                numpy.arange(firstOutputIndex,
                    len(polarOutput.backscatterCross)) * heightConversion,
                polarOutput.backscatterCross[firstOutputIndex :],
                getPlotColorByChannelId('532C'), self.displayStyle)

            addMarker(self.plotBsCross, 0.0, 0.0, QwtPlotMarker.Cross,
                self.displayStyle)

            addTextMessages(self.plotBsCross, [getLegendEntryStr(
                'cross backscatter', getPlotColorByChannelId('532C'))])

            self.plotBsCross.replot()

            # Update total backscatter plot.
            totalBackscatter = (
                polarOutput.backscatterParallel[firstOutputIndex :] +
                polarOutput.backscatterCross[firstOutputIndex :])

            addPlot(self.plotBsTotal,
                numpy.arange(firstOutputIndex,
                    len(polarOutput.backscatterParallel)) * heightConversion,
                totalBackscatter,
                QColor(0, 0, 255), self.displayStyle)

            addMarker(self.plotBsTotal, 0.0, 0.0, QwtPlotMarker.Cross,
                self.displayStyle)

            # Calculate integrated backscatter and aerosol optical thickness.
            heightStep = polarOutput.gridHeightStep

            bInt = (totalBackscatter.sum() +
                totalBackscatter[0] * firstOutputIndex) * heightStep

            aot = bInt * polarOutput.lidarRatio

            addTextMessages(self.plotBsTotal, [getLegendEntryStr(
                'total backscatter', QColor(0, 0, 255)),
                getNameValueStr('AOT', '%.3f' % aot),
                getNameValueStr('Bint', '%.2e' % bInt)])

            self.plotBsTotal.replot()

            # Update depolarization ratio plot.
            depolarization = (
                polarOutput.backscatterCross[firstOutputIndex :] /
                polarOutput.backscatterParallel[firstOutputIndex :])

            addPlot(self.plotDepolar,
                numpy.arange(firstOutputIndex,
                    len(polarOutput.backscatterParallel)) * heightConversion,
                depolarization,
                QColor(255, 0, 0), self.displayStyle)

            depolarizationDispersion = polarOutput.depolarizationDispersion

            if depolarizationDispersion is not None:
                addPlot(self.plotDepolar,
                    numpy.arange(firstOutputIndex,
                        len(polarOutput.backscatterParallel)) *
                        heightConversion,
                    depolarization +
                        depolarizationDispersion[firstOutputIndex :],
                    QColor(255, 0, 0), self.displayStyle,
                    penStyle = Qt.DashLine)

            addMarker(self.plotDepolar, 0.0, 0.0, QwtPlotMarker.Cross,
                self.displayStyle)

            addTextMessages(self.plotDepolar, [getLegendEntryStr(
                'depolarization', QColor(255, 0, 0))])

            self.plotDepolar.replot()

        def saveAsImage(self, imageFilePath):
            """Save the plots to an image file at the specified path."""

            ### TODO: Create a universal approach to plot widgets and image
            # export. (This function is a copy-paste of 'LiricPlotWidget' with
            # minor changes).

            # Dimensions of the image in millimeters.
            imageWidthMm = 168.0    # 56 * 3
            imageHeightMm = 84.0    # 42 * 2

            # Distance between a plot and the border of the image, in millimeters.
            outerPlotMarginMm = 1.0
            # Half of a distance between two neighbouring plots, in millimeters.
            innerPlotMarginMm = 1.0

            if self.displayStyle.getDisplayMode() == 'image':
                # Image width in pixels.
                imageWidth = int(round(
                    imageWidthMm / 25.4 * self.displayStyle.getDpi()))
                imageHeight = int(round(
                    imageHeightMm / 25.4 * self.displayStyle.getDpi()))
                # imageHeight = int(round(imageWidth * imageHeightMm / imageWidthMm))

                image = QImage(QSize(imageWidth, imageHeight),
                    QImage.Format_RGB32)

                # Set image resolution so that it fits the required physical size.
                # Font sizes may thus be specified in physical points.
                imageDotsPerMeter = int(round(imageWidth / imageWidthMm * 1000.0))
                image.setDotsPerMeterX(imageDotsPerMeter)
                image.setDotsPerMeterY(imageDotsPerMeter)

                painter = QPainter(image)

                # Draw the background by hand, as 'QwtPlot' seems to fail with it.
                painter.fillRect(0, 0, imageWidth, imageHeight, Qt.white)

            elif self.displayStyle.getDisplayMode() == 'emf':
                # Convert millimeters to inches and set the grain size of the image
                # coordinate grid.
                emfPaintDevice = emf_export.EMFPaintDevice(
                    imageWidthMm / 25.4, imageHeightMm / 25.4,
                    dpi = self.displayStyle.getDpi())

                imageWidth = emfPaintDevice.width()
                imageHeight = emfPaintDevice.height()

                painter = QPainter(emfPaintDevice)

            # Convert millimeters to pixels.
            outerPlotMargin = int(round(outerPlotMarginMm *
                imageWidth / imageWidthMm))
            innerPlotMargin = int(round(innerPlotMarginMm *
                imageWidth / imageWidthMm))

            plotWidth = imageWidth / 3 - outerPlotMargin - innerPlotMargin
            plotHeight = imageHeight / 2 - outerPlotMargin - innerPlotMargin

            # Bounding rectangles for the 6 plots combined in the image.
            #
            # Use ceiling values for coordinates at the vertical center and
            # at the right side of the image, so that symmetricity is achieved
            # for both even and odd image sizes in pixels.
            rectBase = QRect(
                outerPlotMargin, outerPlotMargin,
                plotWidth, plotHeight)
            rectCross = QRect(
                outerPlotMargin, (imageHeight + 1) / 2 + innerPlotMargin,
                plotWidth, plotHeight)
            rectBsParallel = QRect(
                (imageWidth + 4) / 3 + innerPlotMargin, outerPlotMargin,
                plotWidth, plotHeight)
            rectBsCross = QRect(
                (imageWidth + 4) / 3 + innerPlotMargin,
                (imageHeight + 1) / 2 + innerPlotMargin,
                plotWidth, plotHeight)
            rectBsTotal = QRect(
                (imageWidth * 2 + 4) / 3 + innerPlotMargin, outerPlotMargin,
                plotWidth, plotHeight)
            rectDepolar = QRect(
                (imageWidth * 2 + 4) / 3 + innerPlotMargin,
                (imageHeight + 1) / 2 + innerPlotMargin,
                plotWidth, plotHeight)

            printFilter = QwtPlotPrintFilter()
            printOptions = QwtPlotPrintFilter.PrintAll
            # Draw bounding rectangles around the plot grids.
            printOptions |= QwtPlotPrintFilter.PrintFrameWithScales
            printFilter.setOptions(printOptions)

            self.plotBase.print_(painter, rectBase, printFilter)
            self.plotCross.print_(painter, rectCross, printFilter)
            self.plotBsParallel.print_(painter, rectBsParallel, printFilter)
            self.plotBsCross.print_(painter, rectBsCross, printFilter)
            self.plotBsTotal.print_(painter, rectBsTotal, printFilter)
            self.plotDepolar.print_(painter, rectDepolar, printFilter)

            painter.end()

            if self.displayStyle.getDisplayMode() == 'image':
                image.save(imageFilePath)

            elif self.displayStyle.getDisplayMode() == 'emf':
                emfPaintDevice.paintEngine().saveFile(imageFilePath)

# **** Private functions ******************************************************
def createPlotWidget(parent, displayStyle = _PlotDisplayStyle('screen')):
    """Create a 'QwtPlot' with the specified parent widget and apply common
    appearance settings to it."""

    if displayStyle.displayMode == 'screen':
        plotWidget = QwtPlot(parent)
    else:
        plotWidget = _QwtPlotForPrinting(parent)

    axesFont = displayStyle.getAxesFont(plotWidget)

    plotWidget.setCanvasBackground(Qt.white)
    plotWidget.setAxisFont(QwtPlot.xBottom, axesFont)
    plotWidget.setAxisFont(QwtPlot.yLeft, axesFont)

    penWidth = displayStyle.getPenWidth()
    scalePenWidth = displayStyle.getScalePenWidth()

    plotWidget.axisWidget(QwtPlot.yLeft).setPenWidth(scalePenWidth)
    plotWidget.axisWidget(QwtPlot.xBottom).setPenWidth(scalePenWidth)

    plotGrid = QwtPlotGrid()

    majorGridPen = QPen(QBrush(displayStyle.getMajorGridColor()), penWidth)
    minorGridPen = QPen(QBrush(displayStyle.getMinorGridColor()), penWidth)
    plotGrid.setMajPen(majorGridPen)
    plotGrid.setMinPen(minorGridPen)
    plotGrid.enableXMin(True)
    plotGrid.enableYMin(True)

    plotGrid.attach(plotWidget)

    # This is required to assure even distribution of space between the plots.
    plotWidget.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)

    return plotWidget

def addPlot(plotWidget, xArray, yArray, plotColor,
    displayStyle = _PlotDisplayStyle('screen'), penStyle = None):
    """Attach a polyline with the given coordinates and color to an existing
    'QwtPlot' instance."""

    curve = QwtPlotCurve()

    pen = QPen(QBrush(plotColor), displayStyle.getPenWidth())
    if penStyle is not None:
        pen.setStyle(penStyle)

    curve.setPen(pen)

    curve.setData(xArray, yArray)
    curve.attach(plotWidget)

def addMarker(plotWidget, x, y, lineStyle,
    displayStyle = _PlotDisplayStyle('screen')):
    """Add a horizontal or vertical line or both, depending on 'lineStyle',
    to an existing 'QwtPlot' instance.

    'lineStyle' has to be one of 'QwtPlotMarker.LineStyle's."""

    marker = QwtPlotMarker()
    marker.setValue(x, y)
    marker.setLineStyle(lineStyle)
    marker.setLinePen(QPen(QBrush(Qt.black), displayStyle.getPenWidth()))
    marker.attach(plotWidget)

def addTextMessages(plotWidget, strList,
    displayStyle = _PlotDisplayStyle('screen')):
    """Add a marker displaying a set of rich text messages defined by 'strList'
    to the top right corner of the plot's canvas.

    Warning: all the plots have to be added prior to this function call."""

    # All the text lines have to be incorporated into a single marker label, as
    # otherwise their positions would vary depending on the canvas' size in
    # pixels. Use <p> HTML tag to left-align the strings and <br> to separate
    # them. Insert extra spaces before and after each of the lines to generate
    # left and right margins, as there seems to be no other way to do this.
    textString = ('<p align="left">&nbsp;' + '&nbsp;<br>&nbsp;'.join(strList) +
        '&nbsp;</p>')
    text = QwtText(textString, QwtText.RichText)

    # Remove the grid lines under the text and softly outline the boundary to
    # visually hint at the reason for their abrupt disappearance.
    text.setBackgroundBrush(QBrush(Qt.white))
    text.setBackgroundPen(QPen(QBrush(displayStyle.getMinorGridColor()),
        displayStyle.getPenWidth()))

    text.setFont(displayStyle.getTextFont(plotWidget))

    # Calculate the bounds of the widget's canvas.
    plotWidget.updateAxes()

    rightBound = plotWidget.axisScaleDiv(QwtPlot.xBottom).upperBound()
    topBound = plotWidget.axisScaleDiv(QwtPlot.yLeft).upperBound()

    marker = QwtPlotMarker()
    marker.setValue(rightBound, topBound)
    marker.setLabel(text)
    marker.setLineStyle(QwtPlotMarker.NoLine)
    marker.setLabelAlignment(Qt.AlignBottom | Qt.AlignLeft)
    marker.attach(plotWidget)

def getNameValueStr(nameStr, valueStr, valueColor = None):
    """Construct a rich text string representing a name-value pair with value
    marked with the specified color (a 'QColor' instance), if any."""

    if valueColor is not None:
        assert 0 <= valueColor.red() <= 255
        assert 0 <= valueColor.green() <= 255
        assert 0 <= valueColor.blue() <= 255

        valueStr = '<font color="#%02x%02x%02x">%s</font>' % (
            valueColor.red(), valueColor.green(), valueColor.blue(), valueStr)

    return nameStr + ' = ' + valueStr

def getValueColor(value, referenceValue, fitThreshold, semiFitThreshold):
    """Return a 'QColor' instance for the value part of a name-value pair
    string depending on difference between 'value' and 'referenceValue'.

    The returned color is 'None' if absolute value of relative residual between
    'value' and 'relativeValue' is smaller than or equal to 'fitThreshold'. If
    the residual is smaller than or equal to 'semiFitThreshold', the returned
    color is dark red; otherwise it is red."""

    fitColor = None
    semiFitColor = QColor(152, 0, 0)
    nonFitColor = QColor(255, 0, 0)

    if referenceValue == 0.0:
        # Relative residual may not be calculated.
        return fitColor if value == referenceValue else nonFitColor

    residual = abs((referenceValue - value) / referenceValue)

    if residual > semiFitThreshold:
        return nonFitColor
    elif residual > fitThreshold:
        return semiFitColor
    else:
        return fitColor

def getLegendEntryStr(textLabel, lineColor):
    """Construct a rich text string visually resempling a legend entry for a
    line plot of the given color (a 'QColor' instance)."""

    assert 0 <= lineColor.red() <= 255
    assert 0 <= lineColor.green() <= 255
    assert 0 <= lineColor.blue() <= 255

    # Use a strike-through style to display the horizontal line.
    return ('<font color="#%02x%02x%02x">'
        '<s>&nbsp;&nbsp;&nbsp;&nbsp;</s></font> %s' %
        (lineColor.red(), lineColor.green(), lineColor.blue(), textLabel))

def getPlotColorByChannelId(channelId):
    """Return a common color for data related to the given lidar channel."""

    if channelId == '355':
        return QColor(216, 0, 247)
    if channelId == '387R':
        return QColor(106, 0, 213)
    elif channelId == '532':
        return QColor(0, 232, 0)
    elif channelId == '607R':
        return QColor(166, 189, 0)
    elif channelId == '1064':
        return QColor(255, 0, 0)
    elif channelId == '532C':
        return QColor(0, 132, 0)

def getPlotColorByModeSuffix(modeSuffix):
    """Return a common color for data related to the given aerosol mode."""

    if modeSuffix == 'Fine':
        return QColor(0, 0, 255)
    elif modeSuffix == 'Spherical':
        return QColor(255, 0, 0)
    elif modeSuffix == 'Spheroid':
        return QColor(0, 132, 0)
    elif modeSuffix == 'Coarse':
        return QColor(255, 0, 0)
