Skip to content
Snippets Groups Projects
temporalprofiles2d.py 56 KiB
Newer Older
  • Learn to ignore specific revisions
  • # -*- coding: utf-8 -*-
    """
    /***************************************************************************
    
                                  -------------------
            begin                : 2017-08-04
            git sha              : $Format:%H$
            copyright            : (C) 2017 by HU-Berlin
            email                : benjamin.jakimow@geo.hu-berlin.de
     ***************************************************************************/
    
    /***************************************************************************
     *                                                                         *
     *   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.                                   *
     *                                                                         *
     ***************************************************************************/
    """
    # noinspection PyPep8Naming
    
    import os, sys, pickle, datetime, re, collections
    
    from collections import OrderedDict
    from qgis.gui import *
    
    from qgis.core import *
    
    from qgis.PyQt.QtCore import *
    from qgis.PyQt.QtGui import *
    from qgis.PyQt.QtWidgets import *
    
    import numpy as np
    import pyqtgraph as pg
    from pyqtgraph import functions as fn
    from pyqtgraph import AxisItem
    
    from osgeo import ogr, osr, gdal
    
    from timeseriesviewer.timeseries import TimeSeries, TimeSeriesDatum, SensorInstrument
    from timeseriesviewer.plotstyling import PlotStyle
    from timeseriesviewer.pixelloader import PixelLoader, PixelLoaderTask
    
    from timeseriesviewer.utils import *
    
    from timeseriesviewer.speclib.spectrallibraries import createQgsField
    
    from timeseriesviewer.models import OptionListModel, Option
    LABEL_EXPRESSION_2D = 'DN or Index'
    
    OPENGL_AVAILABLE = False
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    DEFAULT_SAVE_PATH = None
    
    DEFAULT_CRS = QgsCoordinateReferenceSystem('EPSG:4326')
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    FN_ID = 'id'
    FN_X = 'x'
    FN_Y = 'y'
    FN_NAME = 'name'
    
    FN_N_TOTAL = 'n'
    FN_N_NODATA = 'no_data'
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    FN_N_LOADED = 'loaded'
    
    FN_N_LOADED_PERCENT = 'percent'
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    regBandKey = re.compile(r"(?<!\w)b\d+(?!\w)", re.IGNORECASE)
    regBandKeyExact = re.compile(r'^' + regBandKey.pattern + '$', re.IGNORECASE)
    
    
        OPENGL_AVAILABLE = True
    
    def sensorExampleQgsFeature(sensor, singleBandOnly=False):
    
        # populate with exemplary band values (generally stored as floats)
    
        if sensor is None:
            singleBandOnly = True
    
    
        fieldValues = collections.OrderedDict()
    
        if singleBandOnly:
            fieldValues['b'] = 1.0
        else:
    
            assert isinstance(sensor, SensorInstrument)
    
            for b in range(sensor.nb):
                fn = bandIndex2bandKey(b)
                fieldValues[fn] = 1.0
    
    
        date = datetime.date.today()
        doy = dateDOY(date)
        fieldValues['doy'] = doy
        fieldValues['date'] = str(date)
    
    
        fields = QgsFields()
        for k, v in fieldValues.items():
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            fields.append(createQgsField(k,v))
    
        f = QgsFeature(fields)
        for k, v in fieldValues.items():
            f.setAttribute(k, v)
        return f
    
    
    def dateDOY(date):
        if isinstance(date, np.datetime64):
            date = date.astype(datetime.date)
        return date.timetuple().tm_yday
    
    def daysPerYear(year):
    
        if isinstance(year, np.datetime64):
            year = year.astype(datetime.date)
        if isinstance(year, datetime.date):
            year = year.timetuple().tm_year
    
        return dateDOY(datetime.date(year=year, month=12, day=31))
    
    def date2num(d):
        #kindly taken from https://stackoverflow.com/questions/6451655/python-how-to-convert-datetime-dates-to-decimal-years
        if isinstance(d, np.datetime64):
            d = d.astype(datetime.datetime)
    
    
        if isinstance(d, QDate):
            d = datetime.date(d.year(), d.month(), d.day())
    
    
        assert isinstance(d, datetime.date)
    
        yearDuration = daysPerYear(d)
        yearElapsed = d.timetuple().tm_yday
        fraction = float(yearElapsed) / float(yearDuration)
        if fraction == 1.0:
            fraction = 0.9999999
        return float(d.year) + fraction
    
    
    def num2date(n, dt64=True, qDate=False):
    
        n = float(n)
        if n < 1:
            n += 1
    
        year = int(n)
        fraction = n - year
        yearDuration = daysPerYear(year)
        yearElapsed = fraction * yearDuration
    
        import math
        doy = round(yearElapsed)
        if doy < 1:
            doy = 1
        try:
            date = datetime.date(year, 1, 1) + datetime.timedelta(days=doy-1)
        except:
            s = ""
    
        if qDate:
            return QDate(date.year, date.month, date.day)
    
        if dt64:
            return np.datetime64(date)
        else:
            return date
    
    
    
        #return np.datetime64('{:04}-01-01'.format(year), 'D') + np.timedelta64(int(yearElapsed), 'D')
    
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    def bandIndex2bandKey(i : int):
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    def bandKey2bandIndex(key: str):
    
        match = regBandKeyExact.search(key)
        assert match
        idx = int(match.group()[1:]) - 1
        return idx
    
    
    
    
    class DateTimePlotWidget(pg.PlotWidget):
        """
        Subclass of PlotWidget
        """
        def __init__(self, parent=None):
            """
            Constructor of the widget
            """
            super(DateTimePlotWidget, self).__init__(parent)
            self.plotItem = pg.PlotItem(
                axisItems={'bottom':DateTimeAxis(orientation='bottom')}
                ,viewBox=DateTimeViewBox()
            )
            self.setCentralItem(self.plotItem)
            #self.xAxisInitialized = False
    
            pi = self.getPlotItem()
            pi.getAxis('bottom').setLabel(LABEL_TIME)
    
            pi.getAxis('left').setLabel(LABEL_EXPRESSION_2D)
    
    
            self.mInfoColor = QColor('yellow')
            self.mCrosshairLineV = pg.InfiniteLine(angle=90, movable=False)
            self.mCrosshairLineH = pg.InfiniteLine(angle=0, movable=False)
            self.mInfoLabelCursor = pg.TextItem(text='<cursor position>', anchor=(1.0, 0.0))
            self.mInfoLabelCursor.setColor(QColor('yellow'))
    
            self.scene().addItem(self.mInfoLabelCursor)
            self.mInfoLabelCursor.setParentItem(self.getPlotItem())
            #self.plot2DLabel.setAnchor()
            #self.plot2DLabel.anchor(itemPos=(0, 0), parentPos=(0, 0), offset=(0, 0))
            pi.addItem(self.mCrosshairLineV, ignoreBounds=True)
            pi.addItem(self.mCrosshairLineH, ignoreBounds=True)
    
            self.proxy2D = pg.SignalProxy(self.scene().sigMouseMoved, rateLimit=60, slot=self.onMouseMoved2D)
    
    
        def resetViewBox(self):
            self.plotItem.getViewBox().autoRange()
    
    
    
        def onMouseMoved2D(self, evt):
            pos = evt[0]  ## using signal proxy turns original arguments into a tuple
    
            plotItem = self.getPlotItem()
            if plotItem.sceneBoundingRect().contains(pos):
    
                vb = plotItem.vb
                assert isinstance(vb, DateTimeViewBox)
                mousePoint = vb.mapSceneToView(pos)
    
                if x >= 0:
                    y = mousePoint.y()
                    date = num2date(x)
                    doy = dateDOY(date)
                    plotItem.vb.updateCurrentDate(num2date(x, dt64=True))
                    self.mInfoLabelCursor.setText('DN {:0.2f}\nDate {}\nDOY {}'.format(
                                                  mousePoint.y(), date, doy),
                                                  color=self.mInfoColor)
    
                    s = self.size()
                    pos = QPointF(s.width(), 0)
    
                    self.mInfoLabelCursor.setVisible(vb.mActionShowCursorValues.isChecked())
    
                    self.mInfoLabelCursor.setPos(pos)
    
    
                    b = vb.mActionShowCrosshair.isChecked()
                    self.mCrosshairLineH.setVisible(b)
                    self.mCrosshairLineV.setVisible(b)
    
                    self.mCrosshairLineH.pen.setColor(self.mInfoColor)
                    self.mCrosshairLineV.pen.setColor(self.mInfoColor)
                    self.mCrosshairLineV.setPos(mousePoint.x())
                    self.mCrosshairLineH.setPos(mousePoint.y())
    
    
    
    class DateTimeAxis(pg.AxisItem):
    
        def __init__(self, *args, **kwds):
            super(DateTimeAxis, self).__init__(*args, **kwds)
            self.setRange(1,3000)
            self.enableAutoSIPrefix(False)
            self.labelAngle = 0
    
        def logTickStrings(self, values, scale, spacing):
            s = ""
    
    
        def tickStrings(self, values, scale, spacing):
            strns = []
    
            if len(values) == 0:
                return []
            #assert isinstance(values[0],
    
            values = [num2date(v) if v > 0 else num2date(1) for v in values]
            rng = max(values)-min(values)
            ndays = rng.astype(int)
    
            strns = []
    
            for v in values:
                if ndays == 0:
                    strns.append(v.astype(str))
                else:
                    strns.append(v.astype(str))
    
            return strns
    
        def tickValues(self, minVal, maxVal, size):
            d = super(DateTimeAxis, self).tickValues(minVal, maxVal, size)
    
            return d
    
    
        def drawPicture(self, p, axisSpec, tickSpecs, textSpecs):
    
    
            p.setRenderHint(p.Antialiasing, False)
            p.setRenderHint(p.TextAntialiasing, True)
    
            ## draw long line along axis
            pen, p1, p2 = axisSpec
            p.setPen(pen)
            p.drawLine(p1, p2)
            p.translate(0.5, 0)  ## resolves some damn pixel ambiguity
    
            ## draw ticks
            for pen, p1, p2 in tickSpecs:
                p.setPen(pen)
                p.drawLine(p1, p2)
    
    
            ## Draw all text
            if self.tickFont is not None:
                p.setFont(self.tickFont)
            p.setPen(self.pen())
    
            #for rect, flags, text in textSpecs:
            #    p.drawText(rect, flags, text)
            #    # p.drawRect(rect)
    
            #see https://github.com/pyqtgraph/pyqtgraph/issues/322
            for rect, flags, text in textSpecs:
                p.save()  # save the painter state
                p.translate(rect.center())   # move coordinate system to center of text rect
                p.rotate(self.labelAngle)  # rotate text
                p.translate(-rect.center())  # revert coordinate system
                p.drawText(rect, flags, text)
                p.restore()  # restore the painter state
    
    
    
    class DateTimeViewBox(pg.ViewBox):
        """
        Subclass of ViewBox
        """
        sigMoveToDate = pyqtSignal(np.datetime64)
        def __init__(self, parent=None):
            """
            Constructor of the CustomViewBox
            """
            super(DateTimeViewBox, self).__init__(parent)
            #self.menu = None # Override pyqtgraph ViewBoxMenu
            #self.menu = self.getMenu() # Create the menu
            #self.menu = None
            self.mCurrentDate = np.datetime64('today')
    
            xAction = [a for a in self.menu.actions() if a.text() == 'X Axis'][0]
            yAction = [a for a in self.menu.actions() if a.text() == 'Y Axis'][0]
    
            menuXAxis = self.menu.addMenu('X Axis')
            #define the widget to set X-Axis options
            frame = QFrame()
            l = QGridLayout()
    
            frame.setLayout(l)
            #l.addWidget(self, QWidget, int, int, alignment: Qt.Alignment = 0): not enough arguments
    
            self.rbXManualRange = QRadioButton('Manual')
    
            self.dateEditX0 = QDateEdit()
            self.dateEditX0.setDisplayFormat('yyyy-MM-dd')
            self.dateEditX0.setToolTip('Start time')
            self.dateEditX0.setCalendarPopup(True)
            self.dateEditX0.dateChanged.connect(self.updateXRange)
            self.dateEditX1 = QDateEdit()
            self.dateEditX1.setDisplayFormat('yyyy-MM-dd')
            self.dateEditX0.setToolTip('End time')
            self.dateEditX1.setCalendarPopup(True)
            self.dateEditX1.dateChanged.connect(self.updateXRange)
    
    
            self.rbXAutoRange = QRadioButton('Auto')
            self.rbXAutoRange.setChecked(True)
            self.rbXAutoRange.toggled.connect(self.updateXRange)
    
    
            l.addWidget(self.rbXManualRange, 0,0)
            l.addWidget(self.dateEditX0, 0,1)
            l.addWidget(self.dateEditX1, 0,2)
            l.addWidget(self.rbXAutoRange, 1, 0)
    
    
            l.setMargin(1)
            l.setSpacing(1)
            frame.setMinimumSize(l.sizeHint())
            wa = QWidgetAction(menuXAxis)
            wa.setDefaultWidget(frame)
            menuXAxis.addAction(wa)
    
    
            self.menu.insertMenu(xAction, menuXAxis)
            self.menu.removeAction(xAction)
    
            self.mActionMoveToDate = self.menu.addAction('Move to {}'.format(self.mCurrentDate))
            self.mActionMoveToDate.triggered.connect(lambda : self.sigMoveToDate.emit(self.mCurrentDate))
    
            self.mActionShowCrosshair = self.menu.addAction('Show Crosshair')
            self.mActionShowCrosshair.setCheckable(True)
            self.mActionShowCrosshair.setChecked(True)
            self.mActionShowCursorValues = self.menu.addAction('Show Mouse values')
            self.mActionShowCursorValues.setCheckable(True)
            self.mActionShowCursorValues.setChecked(True)
    
        sigXAxisUnitChanged = pyqtSignal(str)
        def setXAxisUnit(self, unit):
            assert unit in ['date', 'doy']
            old = self.mXAxisUnit
            self.mXAxisUnit = unit
            if old != self.mXAxisUnit:
                self.sigXAxisUnitChanged.emit(self.mXAxisUnit)
    
        def xAxisUnit(self):
            return self.mXAxisUnit
    
    
        def updateXRange(self, *args):
            isAutoRange = self.rbXAutoRange.isChecked()
            self.enableAutoRange('x', isAutoRange)
    
    
            self.dateEditX0.setEnabled(not isAutoRange)
            self.dateEditX1.setEnabled(not isAutoRange)
    
    
                t0 = date2num(self.dateEditX0.date())
                t1 = date2num(self.dateEditX1.date())
                t0 = min(t0, t1)
                t1 = max(t0, t1)
    
                self.setXRange(t0, t1)
    
    
        def updateCurrentDate(self, date):
            if isinstance(date, np.datetime64):
                self.mCurrentDate = date
                self.mActionMoveToDate.setData(date)
                self.mActionMoveToDate.setText('Move maps to {}'.format(date))
    
    
    
        def raiseContextMenu(self, ev):
    
            pt = self.mapDeviceToView(ev.pos())
            self.updateCurrentDate(num2date(pt.x(), dt64=True))
    
    
            xRange, yRange = self.viewRange()
            t0 = num2date(xRange[0], qDate=True)
            t1 = num2date(xRange[1], qDate=True)
            self.dateEditX0.setDate(t0)
            self.dateEditX1.setDate(t1)
    
    
            menu = self.getMenu(ev)
            self.scene().addParentContextMenus(self, menu, ev)
            menu.exec_(ev.screenPos().toPoint())
    
    
    
    class TemporalProfilePlotStyleBase(PlotStyle):
    
        sigStyleUpdated = pyqtSignal()
        sigDataUpdated = pyqtSignal()
    
        sigExpressionUpdated = pyqtSignal()
        sigSensorChanged = pyqtSignal(SensorInstrument)
    
    
        def __init__(self, parent=None, temporalProfile=None):
            super(TemporalProfilePlotStyleBase, self).__init__()
    
            self.mTP = None
            self.mExpression = 'b1'
    
            self.mIsVisible = True
    
    
            if isinstance(temporalProfile, TemporalProfile):
                self.setTemporalProfile(temporalProfile)
    
    
        def isPlotable(self):
            return self.isVisible() and isinstance(self.temporalProfile(), TemporalProfile) and isinstance(self.sensor(), SensorInstrument)
    
    
        def createPlotItem(self):
            raise NotImplementedError()
    
    
        def temporalProfile(self):
            return self.mTP
    
        def setTemporalProfile(self, temporalPofile):
    
            b = temporalPofile != self.mTP
            self.mTP = temporalPofile
    
            if temporalPofile in [None, QVariant()]:
    
                s  =""
            else:
                assert isinstance(temporalPofile, TemporalProfile)
            if b:
    
            assert sensor is None or isinstance(sensor, SensorInstrument)
    
            b = sensor != self.mSensor
            self.mSensor = sensor
            if b:
                self.update()
                self.sigSensorChanged.emit(sensor)
    
        def sensor(self):
            return self.mSensor
    
    
        def updateStyleProperties(self):
            raise NotImplementedError()
    
            raise NotImplementedError()
    
    
            assert isinstance(exp, str)
    
            b = self.mExpression != exp
            self.mExpression = exp
    
                self.sigExpressionUpdated.emit()
    
        def expression(self):
            return self.mExpression
    
        def __reduce_ex__(self, protocol):
            return self.__class__, (), self.__getstate__()
    
        def __getstate__(self):
            result = super(TemporalProfile2DPlotStyle, self).__getstate__()
            #remove
            del result['mTP']
            del result['mSensor']
    
            return result
    
    
        def isVisible(self):
            return self.mIsVisible
    
        def setVisibility(self, b):
            assert isinstance(b, bool)
            old = self.isVisible()
            self.mIsVisible = b
    
            if b != old:
    
    
        def copyFrom(self, plotStyle):
    
                super(TemporalProfilePlotStyleBase, self).copyFrom(plotStyle)
    
                self.updateStyleProperties()
    
            if isinstance(plotStyle, TemporalProfilePlotStyleBase):
    
                self.setExpression(plotStyle.expression())
                self.setSensor(plotStyle.sensor())
                self.setTemporalProfile(plotStyle.temporalProfile())
    
    
    class TemporalProfile2DPlotStyle(TemporalProfilePlotStyleBase):
    
    
        def __init__(self, temporalProfile=None):
            super(TemporalProfile2DPlotStyle, self).__init__(temporalProfile=temporalProfile)
            #PlotStyle.__init__(self)
           #TemporalProfilePlotStyleBase.__init__(self, temporalProfile=temporalProfile)
    
        def createPlotItem(self, plotWidget):
            pdi = TemporalProfilePlotDataItem(self)
            self.mPlotItems.append(pdi)
            return pdi
    
    
            for pdi in self.mPlotItems:
                assert isinstance(pdi, TemporalProfilePlotDataItem)
                pdi.updateStyle()
    
    
        def updateDataProperties(self):
            for pdi in self.mPlotItems:
                assert isinstance(pdi, TemporalProfilePlotDataItem)
                pdi.updateDataAndStyle()
    
    
        sigNameChanged = pyqtSignal(str)
        sigDataChanged = pyqtSignal()
    
    
        def __init__(self, layer, fid):
    
            super(TemporalProfile, self).__init__()
    
            assert isinstance(layer, TemporalProfileLayer)
            assert fid >= 0
    
            self.mID = fid
            self.mLayer = layer
            self.mTimeSeries = layer.timeSeries()
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            assert isinstance(self.mTimeSeries, TimeSeries)
    
            self.mLoaded = self.mLoadedMax = self.mNoData = 0
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
            for tsd in self.mTimeSeries:
                assert isinstance(tsd, TimeSeriesDatum)
                meta = {'doy': tsd.doy,
                        'date': str(tsd.date),
                        'nodata':False}
    
                self.updateData(tsd, meta, skipStatusUpdate=True)
            #self.updateLoadingStatus()
            s = ""
    
    
    
        def __hash__(self):
            return hash('{}{}'.format(self.mID, self.mLayer.layerId()))
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def __eq__(self, other):
            """
    
            Two temporal profiles are equal if they have the same feature id and source layer
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            :param other:
            :return:
            """
    
            if not isinstance(other, TemporalProfile):
                return False
    
    
            return other.mID == self.mID and self.mLayer == other.mLayer
    
        def geometry(self):
            return self.mLayer.getFeature(self.mID).geometry()
    
    
        def coordinate(self):
    
            x,y = self.geometry().asPoint()
            return SpatialPoint(self.mLayer.crs(), x,y)
    
            """Feature ID in connected QgsVectorLayer"""
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def attribute(self, key:str):
    
            f = self.mLayer.getFeature(self.mID)
            return f.attribute(f.fieldNameIndex(key))
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def setAttribute(self, key:str, value):
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            f = self.mLayer.getFeature(self.id())
    
    
            b = self.mLayer.isEditable()
            self.mLayer.startEditing()
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            self.mLayer.changeAttributeValue(f.id(), f.fieldNameIndex(key), value)
    
            self.mLayer.saveEdits(leaveEditable=b)
    
        def name(self):
            return self.attribute('name')
    
        def setName(self, name:str):
            self.setAttribute('name', name)
    
    
        def data(self):
            return self.mData
    
        def timeSeries(self):
            return self.mTimeSeries
    
    
        def pullDataUpdate(self, d):
            assert isinstance(d, PixelLoaderTask)
            if d.success() and self.mID in d.temporalProfileIDs:
                i = d.temporalProfileIDs.index(self.mID)
                tsd = self.mTimeSeries.getTSD(d.sourcePath)
                assert isinstance(tsd, TimeSeriesDatum)
    
                if d.validPixelValues(i):
                    profileData = d.resProfiles[i]
    
    
                    vMean, vStd = profileData
    
                    validValues = not isinstance(vMean, str)
                    # 1. add the pixel values per returned band
    
                    for iBand, bandIndex in enumerate(d.bandIndices):
                        key = 'b{}'.format(bandIndex + 1)
                        values[key] = vMean[iBand] if validValues else None
                        key = 'std{}'.format(bandIndex + 1)
                        values[key] = vStd[iBand] if validValues else None
    
                else:
                    values['nodata'] = True
    
                self.updateData(tsd, values)
    
    
    
        def loadMissingData(self, showGUI=False):
            """
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            Loads the missing data for this profile.
    
            from timeseriesviewer.pixelloader import PixelLoaderTask, doLoaderTask
    
            tasks = []
            for tsd in self.mTimeSeries:
                missingIndices = self.missingBandIndices(tsd)
    
                if len(missingIndices) > 0:
    
                    task = PixelLoaderTask(tsd.pathImg, [self.coordinate()],
    
                                           bandIndices=missingIndices,
                                           temporalProfileIDs=[self.mID])
                    tasks.append(task)
    
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                result = PixelLoaderTask.fromDump(doLoaderTask(None, task))
    
                assert isinstance(result, PixelLoaderTask)
                self.pullDataUpdate(result)
    
    
        def missingBandIndices(self, tsd, requiredIndices=None):
            """
            Returns the band indices [0, sensor.nb) that have not been loaded yet.
            :param tsd: TimeSeriesDatum of interest
            :param requiredIndices: optional subset of possible band-indices to return the missing ones from.
            :return: [list-of-indices]
            """
            assert isinstance(tsd, TimeSeriesDatum)
            if requiredIndices is None:
                requiredIndices = list(range(tsd.sensor.nb))
            requiredIndices = [i for i in requiredIndices if i >= 0 and i < tsd.sensor.nb]
            existingBandIndices = [bandKey2bandIndex(k) for k in self.data(tsd).keys() if regBandKeyExact.search(k)]
            return [i for i in requiredIndices if i not in existingBandIndices]
    
    
        def plot(self):
    
            import pyqtgraph as pg
    
            for sensor in self.mTimeSeries.sensors():
                assert isinstance(sensor, SensorInstrument)
    
                plotStyle = TemporalProfile2DPlotStyle(self)
                plotStyle.setSensor(sensor)
    
                pi = TemporalProfilePlotDataItem(plotStyle)
                pi.setClickable(True)
                pw = pg.plot(title=self.name())
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                pw.plotItem().addItem(pi)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def updateData(self, tsd, values, skipStatusUpdate=False):
    
            assert isinstance(tsd, TimeSeriesDatum)
            assert isinstance(values, dict)
    
            if tsd not in self.mData.keys():
                self.mData[tsd] = {}
    
            self.mData[tsd].update(values)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            if not skipStatusUpdate:
                self.updateLoadingStatus()
                self.mUpdated = True
                self.sigDataChanged.emit()
    
        def resetUpdatedFlag(self):
    
            self.mUpdated = False
    
        def updated(self):
            return self.mUpdated
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def dataFromExpression(self, sensor, expression:str, dateType='date'):
    
            assert dateType in ['date','doy']
            x = []
            y = []
    
    
            if not isinstance(expression, QgsExpression):
                expression = QgsExpression(expression)
            assert isinstance(expression, QgsExpression)
            expression = QgsExpression(expression)
    
    
            #define required QgsFields
    
            fields = QgsFields()
            sensorTSDs = sorted([tsd for tsd in self.mData.keys() if tsd.sensor == sensor])
            for tsd in sensorTSDs:
                data = self.mData[tsd]
                for k, v in data.items():
    
                    if v is not None and fields.indexFromName(k) == -1:
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                        fields.append(createQgsField(k, v))
    
    
            for i, tsd in enumerate(sensorTSDs):
                assert isinstance(tsd, TimeSeriesDatum)
                data = self.mData[tsd]
                context = QgsExpressionContext()
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                context.setFields(fields)
    
                #scope = QgsExpressionContextScope()
                f = QgsFeature(fields)
    
                    setQgsFieldValue(f, k, v)
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                context.setFeature(f)
               # scope.setFeature(f)
                #context.appendScope(scope)
    
                #value = expression.evaluatePrepared(f)
                value = expression.evaluate(context)
    
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                if value in [None, QVariant()]:
    
                    s = ""
                else:
                    if dateType == 'date':
                        x.append(date2num(tsd.date))
                    elif dateType == 'doy':
                        x.append(tsd.doy)
                    y.append(value)
    
            #return np.asarray(x), np.asarray(y)
            assert len(x) == len(y)
            return x, y
    
        def data(self, tsd):
            assert isinstance(tsd, TimeSeriesDatum)
            if self.hasData(tsd):
                return self.mData[tsd]
            else:
                return {}
    
    
        def loadingStatus(self):
            """
            Returns the loading status in terms of single pixel values.
            nLoaded = sum of single band values
            nLoadedMax = potential maximum of band values that might be loaded
            :return: (nLoaded, nLoadedMax)
            """
    
            return self.mLoaded, self.mNoData, self.mLoadedMax
    
            Calculates the loading status in terms of single pixel values.
    
            nMax is the sum of all bands over each TimeSeriesDatum and Sensors
            """
    
    
            self.mLoaded = 0
            self.mLoadedMax = 0
    
            self.mNoData = 0
    
    
            for tsd in self.mTimeSeries:
                assert isinstance(tsd, TimeSeriesDatum)
    
                nb = tsd.sensor.nb
    
                self.mLoadedMax += nb
    
                    if self.isNoData(tsd):
                        self.mNoData += nb
                    else:
                        self.mLoaded += len([k for k in self.mData[tsd].keys() if regBandKey.search(k)])
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            f = self.mLayer.getFeature(self.id())
    
            b = self.mLayer.isEditable()
            self.mLayer.startEditing()
            self.mLayer.changeAttributeValue(f.id(), f.fieldNameIndex(FN_N_NODATA), self.mNoData)
            self.mLayer.changeAttributeValue(f.id(), f.fieldNameIndex(FN_N_TOTAL), self.mLoadedMax)
            self.mLayer.changeAttributeValue(f.id(), f.fieldNameIndex(FN_N_LOADED), self.mLoaded)
    
            if self.mLoadedMax > 0:
                self.mLayer.changeAttributeValue(f.id(), f.fieldNameIndex(FN_N_LOADED_PERCENT), round(100. * float(self.mLoaded + self.mNoData) / self.mLoadedMax, 2))
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
            self.mLayer.saveEdits(leaveEditable=b)
            s = ""
    
        def isNoData(self, tsd):
            assert isinstance(tsd, TimeSeriesDatum)
            return self.mData[tsd]['nodata']
    
        def hasData(self, tsd):
    
            assert isinstance(tsd, TimeSeriesDatum)
            return tsd in self.mData.keys()
    
        def __repr__(self):
    
            return 'TemporalProfile {}'.format(self.mID)
    
    
    
    class TemporalProfilePlotDataItem(pg.PlotDataItem):
    
        def __init__(self, plotStyle, parent=None):
            assert isinstance(plotStyle, TemporalProfile2DPlotStyle)
    
    
            super(TemporalProfilePlotDataItem, self).__init__([], [], parent=parent)
            self.menu = None
            #self.setFlags(QGraphicsItem.ItemIsSelectable)
            self.mPlotStyle = plotStyle
            self.setAcceptedMouseButtons(Qt.LeftButton | Qt.RightButton)
            self.mPlotStyle.sigUpdated.connect(self.updateDataAndStyle)
            self.updateStyle()
    
        # On right-click, raise the context menu
        def mouseClickEvent(self, ev):
            if ev.button() == QtCore.Qt.RightButton:
                if self.raiseContextMenu(ev):
                    ev.accept()
    
        def raiseContextMenu(self, ev):
            menu = self.getContextMenus()
    
            # Let the scene add on to the end of our context menu
            # (this is optional)
            menu = self.scene().addParentContextMenus(self, menu, ev)
    
            pos = ev.screenPos()
            menu.popup(QtCore.QPoint(pos.x(), pos.y()))
            return True
    
        # This method will be called when this item's _children_ want to raise
        # a context menu that includes their parents' menus.
        def getContextMenus(self, event=None):
            if self.menu is None:
                self.menu = QMenu()
                self.menu.setTitle(self.name + " options..")
    
                green = QAction("Turn green", self.menu)
                green.triggered.connect(self.setGreen)
                self.menu.addAction(green)
                self.menu.green = green
    
                blue = QAction("Turn blue", self.menu)
                blue.triggered.connect(self.setBlue)
                self.menu.addAction(blue)
                self.menu.green = blue
    
                alpha = QWidgetAction(self.menu)
                alphaSlider = QSlider()
                alphaSlider.setOrientation(QtCore.Qt.Horizontal)
                alphaSlider.setMaximum(255)
                alphaSlider.setValue(255)
                alphaSlider.valueChanged.connect(self.setAlpha)
                alpha.setDefaultWidget(alphaSlider)
                self.menu.addAction(alpha)
                self.menu.alpha = alpha
                self.menu.alphaSlider = alphaSlider
            return self.menu
    
    
        def updateDataAndStyle(self):
    
            TP = self.mPlotStyle.temporalProfile()
            sensor = self.mPlotStyle.sensor()
    
            if isinstance(TP, TemporalProfile) and isinstance(sensor, SensorInstrument):
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                x, y = TP.dataFromExpression(self.mPlotStyle.sensor(), self.mPlotStyle.expression(), dateType='date')
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                    x = np.asarray(x, dtype=np.float)
                    y = np.asarray(y, dtype=np.float)
    
                    self.setData(x=[], y=[]) # dummy
            else:
                self.setData(x=[], y=[])  # dummy for empty data
    
        def updateStyle(self):
            """
            Updates visibility properties
            """
            from pyqtgraph.graphicsItems.ScatterPlotItem import drawSymbol
    #        path = drawSymbol(p, self.markerSymbol, self.markerSize, self.markerPen, self.markerBrush)
        #                    #painter, symbol, size, pen, brush
            self.setVisible(self.mPlotStyle.isVisible())
            self.setSymbol(self.mPlotStyle.markerSymbol)
            self.setSymbolSize(self.mPlotStyle.markerSize)
            self.setSymbolBrush(self.mPlotStyle.markerBrush)
            self.setSymbolPen(self.mPlotStyle.markerPen)
            self.setPen(self.mPlotStyle.linePen)
            self.update()
    
            #self.setPen(fn.mkPen(self.mPlotStyle.linePen))
            #self.setFillBrush(fn.mkBrush(self.mPlotStyle.mExpression))
            #self.setSymbolBrush(fn.mkBrush(self.mPlotStyle.markerBrush))
    
            # self.setFillBrush(self.mPlotStyle.)
    
            #self.update()
    
        def setClickable(self, b, width=None):
            assert isinstance(b, bool)
            self.curve.setClickable(b, width=width)
    
        def setColor(self, color):
            if not isinstance(color, QColor):
    
                color = QColor(color)
            self.setPen(color)
    
        def pen(self):
            return fn.mkPen(self.opts['pen'])
    
        def color(self):
            return self.pen().color()
    
    
        def setLineWidth(self, width):
            pen = pg.mkPen(self.opts['pen'])
            assert isinstance(pen, QPen)
            pen.setWidth(width)
            self.setPen(pen)
    
    
    
    
    class TemporalProfileLayer(QgsVectorLayer):
    
        """
        A collection to store the TemporalProfile data delivered by a PixelLoader
        """
    
        #sigSensorAdded = pyqtSignal(SensorInstrument)