# -*- coding: utf-8 -*-
"""
/***************************************************************************
                              EO Time Series Viewer
                              -------------------
        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
from collections import OrderedDict
from qgis.gui import *
from qgis.core import *
from qgis.PyQt.QtCore import *
from qgis.PyQt.QtXml import *
from qgis.PyQt.QtGui import *


from .timeseries import *
from .utils import SpatialExtent, SpatialPoint, px2geo, loadUI, nextColor
from .externals.qps.plotstyling.plotstyling import PlotStyle, PlotStyleButton, PlotStyleDialog
from .externals.pyqtgraph import ScatterPlotItem, SpotItem, GraphicsScene
from .externals.qps.externals.pyqtgraph.GraphicsScene.mouseEvents import MouseClickEvent, MouseDragEvent
from .externals import pyqtgraph as pg
from .sensorvisualization import SensorListModel
from .temporalprofiles import *
from .temporalprofiles3d import *
from .pixelloader import PixelLoaderTask, doLoaderTask


import numpy as np

DEBUG = False
OPENGL_AVAILABLE = False
ENABLE_OPENGL = False

try:

    #import OpenGL
    OPENGL_AVAILABLE = True
    from eotimeseriesviewer.temporalprofiles3d import *

    #t = ViewWidget3D()
    #del t


except Exception as ex:
    print('unable to import OpenGL based packages:\n{}'.format(ex))



def getTextColorWithContrast(c):
    assert isinstance(c, QColor)
    if c.lightness() < 0.5:
        return QColor('white')
    else:
        return QColor('black')

def selectedModelIndices(tableView):
    assert isinstance(tableView, QTableView)
    result = {}

    sm = tableView.selectionModel()
    m = tableView.model()
    if isinstance(sm, QItemSelectionModel) and isinstance(m, QAbstractItemModel):
        for idx in sm.selectedIndexes():
            assert isinstance(idx, QModelIndex)
            if idx.row() not in result.keys():
                result[idx.row()] = idx
    return result.values()



class _SensorPoints(pg.PlotDataItem):
    def __init__(self, *args, **kwds):
        super(_SensorPoints, self).__init__(*args, **kwds)
        # menu creation is deferred because it is expensive and often
        # the user will never see the menu anyway.
        self.menu = None

    def boundingRect(self):
        return super(_SensorPoints,self).boundingRect()

    def paint(self, p, *args):
        super(_SensorPoints, self).paint(p, *args)


    # 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



class SensorPixelDataMemoryLayer(QgsVectorLayer):

    def __init__(self, sensor, crs=None):
        assert isinstance(sensor, SensorInstrument)
        if crs is None:
            crs = QgsCoordinateReferenceSystem('EPSG:4862')
        uri = 'Point?crs={}'.format(crs.authid())
        super(SensorPixelDataMemoryLayer, self).__init__(uri, 'Pixels_sensor_' + sensor.name(), 'memory', False)
        self.mSensor = sensor

        #initialize fields
        assert self.startEditing()
        # standard field names, types, etc.
        fieldDefs = [('pxid', QVariant.String, 'integer'),
                     ('date', QVariant.String, 'char'),
                     ('doy', QVariant.Int, 'integer'),
                     ('geo_x', QVariant.Double, 'decimal'),
                     ('geo_y', QVariant.Double, 'decimal'),
                     ('px_x', QVariant.Int, 'integer'),
                     ('px_y', QVariant.Int, 'integer'),
                     ]
        # one field for each band
        for b in range(sensor.nb):
            fName = 'b{}'.format(b + 1)
            fieldDefs.append((fName, QVariant.Double, 'decimal'))

        # initialize fields
        for fieldDef in fieldDefs:
            field = QgsField(fieldDef[0], fieldDef[1], fieldDef[2])
            self.addAttribute(field)
        self.commitChanges()

    def sensor(self):
        return self.mSensor

    def nPixels(self):
        raise NotImplementedError()

    def dates(self):
        raise NotImplementedError()




class PlotSettingsModel3D(QAbstractTableModel):

    #sigSensorAdded = pyqtSignal(SensorPlotSettings)
    sigVisibilityChanged = pyqtSignal(TemporalProfile2DPlotStyle)
    sigPlotStylesAdded = pyqtSignal(list)
    sigPlotStylesRemoved = pyqtSignal(list)

    def __init__(self, parent=None, *args):

        #assert isinstance(tableView, QTableView)

        super(PlotSettingsModel3D, self).__init__(parent=parent)
        self.mTimeSeries = None
        self.cnID = 'ID'
        self.cnExpression = LABEL_EXPRESSION_3D
        self.cnTemporalProfile = 'Coordinate'
        self.cnStyle = 'Style'
        self.cnSensor = 'Sensor'
        self.columnNames = [self.cnTemporalProfile, self.cnSensor, self.cnStyle, self.cnExpression]
        self.mPlotSettings = []
        #assert isinstance(plotWidget, DateTimePlotWidget)

        self.sortColumnIndex = 0
        self.sortOrder = Qt.AscendingOrder


        self.sort(0, Qt.AscendingOrder)

    def hasStyleForSensor(self, sensor):
        assert isinstance(sensor, SensorInstrument)
        for plotStyle in self.mPlotSettings:
            assert isinstance(plotStyle, TemporalProfile3DPlotStyle)
            if plotStyle.sensor() == sensor:
                return True
        return False



    def onSensorRemoved(self, sensor):
        assert isinstance(sensor, SensorInstrument)
        self.removePlotStyles([s for s in self.mPlotSettings if s.sensor() == sensor])



    def __len__(self):
        return len(self.mPlotSettings)

    def __iter__(self):
        return iter(self.mPlotSettings)

    def __getitem__(self, slice):
        return self.mPlotSettings[slice]

    def __contains__(self, item):
        return item in self.mPlotSettings


    def columnIndex(self, name):
        return self.columnNames.index(name)


    def insertPlotStyles(self, plotStyles, i=None):
        """
        Inserts PlotStyle
        :param plotStyles: TemporalProfilePlotStyle | [list-of-TemporalProfilePlotStyle]
        :param i: index to insert, defaults to the last list position
        """
        if isinstance(plotStyles, TemporalProfile3DPlotStyle):
            plotStyles = [plotStyles]
        assert isinstance(plotStyles, list)
        for plotStyle in plotStyles:
            assert isinstance(plotStyle, TemporalProfile3DPlotStyle)

        if i is None:
            i = len(self.mPlotSettings)

        if len(plotStyles) > 0:
            self.beginInsertRows(QModelIndex(), i, i + len(plotStyles)-1)
            for j, plotStyle in enumerate(plotStyles):
                assert isinstance(plotStyle, TemporalProfile3DPlotStyle)
                self.mPlotSettings.insert(i+j, plotStyle)
            self.endInsertRows()
            self.sigPlotStylesAdded.emit(plotStyles)

    def removePlotStyles(self, plotStyles):
        """
        Removes PlotStyle instances
        :param plotStyles: TemporalProfilePlotStyle | [list-of-TemporalProfilePlotStyle]
        """
        if isinstance(plotStyles, TemporalProfile3DPlotStyle):
            plotStyles = [plotStyles]
        assert isinstance(plotStyles, list)

        if len(plotStyles) > 0:
            for plotStyle in plotStyles:
                assert isinstance(plotStyle, TemporalProfile3DPlotStyle)
                if plotStyle in self.mPlotSettings:
                    idx = self.plotStyle2idx(plotStyle)
                    self.beginRemoveRows(QModelIndex(), idx.row(),idx.row())
                    self.mPlotSettings.remove(plotStyle)
                    self.endRemoveRows()
            self.sigPlotStylesRemoved.emit(plotStyles)

    def sort(self, col, order):
        if self.rowCount() == 0:
            return


        colName = self.columnames[col]
        r = order != Qt.AscendingOrder

        #self.beginMoveRows(idxSrc,

        if colName == self.cnSensor:
            self.mPlotSettings.sort(key = lambda sv:sv.sensor().name(), reverse=r)

    def rowCount(self, parent = QModelIndex()):
        return len(self.mPlotSettings)


    def removeRows(self, row, count , parent = QModelIndex()):

        self.beginRemoveRows(parent, row, row + count-1)

        toRemove = self.mPlotSettings[row:row + count]

        for tsd in toRemove:
            self.mPlotSettings.remove(tsd)

        self.endRemoveRows()

    def plotStyle2idx(self, plotStyle):

        assert isinstance(plotStyle, TemporalProfile3DPlotStyle)

        if plotStyle in self.mPlotSettings:
            i = self.mPlotSettings.index(plotStyle)
            return self.createIndex(i, 0)
        else:
            return QModelIndex()

    def idx2plotStyle(self, index):

        if index.isValid() and index.row() < self.rowCount():
            return self.mPlotSettings[index.row()]

        return None

    def columnCount(self, parent = QModelIndex()):
        return len(self.columnNames)

    def data(self, index, role = Qt.DisplayRole):
        if role is None or not index.isValid():
            return None

        value = None
        columnName = self.columnNames[index.column()]
        plotStyle = self.idx2plotStyle(index)
        if isinstance(plotStyle, TemporalProfile3DPlotStyle):
            sensor = plotStyle.sensor()
            #print(('data', columnName, role))
            if role == Qt.DisplayRole:
                if columnName == self.cnSensor:
                    if isinstance(sensor, SensorInstrument):
                        value = sensor.name()
                    else:
                        value = '<Select Sensor>'
                elif columnName == self.cnExpression:
                    value = plotStyle.expression()
                elif columnName == self.cnTemporalProfile:
                    tp = plotStyle.temporalProfile()
                    if isinstance(tp, TemporalProfile):
                        value = tp.name()
                    else:
                        value = 'undefined'

            elif role == Qt.EditRole:
                if columnName == self.cnExpression:
                    value = plotStyle.expression()

            elif role == Qt.CheckStateRole:
                if columnName == self.cnTemporalProfile:
                    value = Qt.Checked if plotStyle.isVisible() else Qt.Unchecked

            elif role == Qt.UserRole:
                value = plotStyle
                if columnName == self.cnSensor:
                    value = plotStyle.sensor()
                elif columnName == self.cnStyle:
                    value = plotStyle
                else:
                    value = plotStyle
        #print(('get data',value))
        return value

    def setData(self, index, value, role=None):
        if role is None or not index.isValid():
            return False
        #print(('Set data', index.row(), index.column(), value, role))
        columnName = self.columnNames[index.column()]

        if value is None:
            return False

        result = False
        plotStyle = self.idx2plotStyle(index)
        if isinstance(plotStyle, TemporalProfile3DPlotStyle):
            """
            if role in [Qt.DisplayRole]:
                if columnName == self.cnExpression and isinstance(value, str):
                    plotStyle.setExpression(value)
                    result = True
                elif columnName == self.cnStyle:
                    if isinstance(value, PlotStyle):
                        plotStyle.copyFrom(value)
                        result = True
            """

            if role == Qt.CheckStateRole:
                if columnName == self.cnTemporalProfile:
                    plotStyle.setVisibility(value == Qt.Checked)
                    result = True

            if role == Qt.EditRole:
                if columnName == self.cnSensor:
                    plotStyle.setSensor(value)
                    result = True
                elif columnName == self.cnExpression:
                    plotStyle.setExpression(value)
                    result = True
                elif columnName == self.cnTemporalProfile:
                    plotStyle.setTemporalProfile(value)
                    result = True
                elif columnName == self.cnStyle:
                    #set the style and trigger an update
                    lastItemType = plotStyle.itemType()
                    lastExpression = plotStyle.expression()
                    plotStyle.copyFrom(value)

                    if lastItemType != plotStyle.itemType() or \
                        lastExpression != plotStyle.expression():
                        plotStyle.updateDataProperties()
                    else:
                        plotStyle.updateStyleProperties()

                    result = True

        return result


    def flags(self, index):
        if index.isValid():
            stype = self.idx2plotStyle(index)
            columnName = self.columnNames[index.column()]
            flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable
            if columnName in [self.cnTemporalProfile]:
                flags = flags | Qt.ItemIsUserCheckable
            if columnName in [self.cnTemporalProfile, self.cnSensor, self.cnExpression, self.cnStyle]: #allow check state
                flags = flags | Qt.ItemIsEditable
            return flags
            #return item.qt_flags(index.column())
        return Qt.NoItemFlags

    def headerData(self, col, orientation, role):
        if Qt is None:
            return None
        if orientation == Qt.Horizontal and role == Qt.DisplayRole:
            return self.columnNames[col]
        elif orientation == Qt.Vertical and role == Qt.DisplayRole:
            return col
        return None


class PlotSettingsModel2D(QAbstractTableModel):

    # sigSensorAdded = pyqtSignal(SensorPlotSettings)
    sigVisibilityChanged = pyqtSignal(TemporalProfile2DPlotStyle)
    sigDataChanged = pyqtSignal(TemporalProfile2DPlotStyle)
    sigPlotStylesAdded = pyqtSignal(list)
    sigPlotStylesRemoved = pyqtSignal(list)


    def __init__(self, parent=None, *args):


        super(PlotSettingsModel2D, self).__init__(parent=parent)

        self.cnID = 'ID'
        self.cnSensor = 'Sensor'
        self.cnExpression = LABEL_EXPRESSION_2D
        self.cnStyle = 'Style'
        self.cnTemporalProfile = 'Coordinate'
        self.columnNames = [self.cnTemporalProfile, self.cnSensor, self.cnStyle, self.cnExpression]

        self.mPlotSettings = []
        self.mIconSize = QSize(25, 25)

        self.dataChanged.connect(self.signaler)

    def __len__(self):
        return len(self.mPlotSettings)

    def __iter__(self):
        return iter(self.mPlotSettings)

    def __getitem__(self, slice):
        return self.mPlotSettings[slice]

    def __contains__(self, item):
        return item in self.mPlotSettings


    def testSlot(self, *args):
        print(('TESTSLOT', args))

    def columnIndex(self, name):
        return self.columnNames.index(name)

    def signaler(self, idxUL, idxLR):
        if idxUL.isValid():

            plotStyle = self.idx2plotStyle(idxUL)
            cname = self.columnNames[idxUL.column()]
            if cname in [self.cnSensor,self.cnStyle]:
                self.sigVisibilityChanged.emit(plotStyle)
            if cname in [self.cnExpression]:
                self.sigDataChanged.emit(plotStyle)




    def requiredBandsIndices(self, sensor)->list:
        """
        Returns the band indices required to calculate the values for
        the different PlotStyle expressions making use of sensor
        :param sensor: SensorInstrument for which the band indices are to be returned.
        :return: [list-of-band-indices]
        """
        bandIndices = set()
        assert isinstance(sensor, SensorInstrument)
        for p in [p for p in self.mPlotSettings if p.sensor() == sensor]:
            assert isinstance(p, TemporalProfile2DPlotStyle)
            expression = p.expression()
            #remove leading & tailing "
            bandKeys = regBandKey.findall(expression)
            for bandIndex in [bandKey2bandIndex(key) for key in bandKeys]:
                bandIndices.add(bandIndex)

        return bandIndices


    def insertPlotStyles(self, plotStyles, i=None):
        """
        Inserts PlotStyle
        :param plotStyles: TemporalProfilePlotStyle | [list-of-TemporalProfilePlotStyle]
        :param i: index to insert, defaults to the last list position
        """
        if isinstance(plotStyles, TemporalProfile2DPlotStyle):
            plotStyles = [plotStyles]
        assert isinstance(plotStyles, list)
        for plotStyle in plotStyles:
            assert isinstance(plotStyle, TemporalProfile2DPlotStyle)

        if i is None:
            i = len(self.mPlotSettings)

        if len(plotStyles) > 0:
            self.beginInsertRows(QModelIndex(), i, i + len(plotStyles)-1)
            for j, plotStyle in enumerate(plotStyles):
                assert isinstance(plotStyle, TemporalProfile2DPlotStyle)
                plotStyle.sigExpressionUpdated.connect(lambda s = plotStyle: self.onStyleUpdated(s))
                self.mPlotSettings.insert(i+j, plotStyle)
            self.endInsertRows()
            self.sigPlotStylesAdded.emit(plotStyles)

    def onStyleUpdated(self, style):

        idx = self.plotStyle2idx(style)
        r = idx.row()
        self.dataChanged.emit(self.createIndex(r, 0), self.createIndex(r, self.columnCount()))

        s = ""


    def removePlotStyles(self, plotStyles):
        """
        Removes PlotStyle instances
        :param plotStyles: TemporalProfilePlotStyle | [list-of-TemporalProfilePlotStyle]
        """
        if isinstance(plotStyles, PlotStyle):
            plotStyles = [plotStyles]
        assert isinstance(plotStyles, list)

        if len(plotStyles) > 0:
            for plotStyle in plotStyles:
                assert isinstance(plotStyle, PlotStyle)
                if plotStyle in self.mPlotSettings:
                    idx = self.plotStyle2idx(plotStyle)
                    self.beginRemoveRows(QModelIndex(), idx.row(),idx.row())
                    self.mPlotSettings.remove(plotStyle)
                    self.endRemoveRows()
            self.sigPlotStylesRemoved.emit(plotStyles)

    def rowCount(self, parent = QModelIndex()):
        return len(self.mPlotSettings)


    def removeRows(self, row, count , parent = QModelIndex()):

        self.beginRemoveRows(parent, row, row + count-1)

        toRemove = self.mPlotSettings[row:row + count]

        for tsd in toRemove:
            self.mPlotSettings.remove(tsd)

        self.endRemoveRows()

    def plotStyle2idx(self, plotStyle):

        assert isinstance(plotStyle, TemporalProfile2DPlotStyle)

        if plotStyle in self.mPlotSettings:
            i = self.mPlotSettings.index(plotStyle)
            return self.createIndex(i, 0)
        else:
            return QModelIndex()

    def idx2plotStyle(self, index):

        if index.isValid() and index.row() < self.rowCount():
            return self.mPlotSettings[index.row()]

        return None

    def columnCount(self, parent = QModelIndex()):
        return len(self.columnNames)

    def data(self, index, role = Qt.DisplayRole):
        if role is None or not index.isValid():
            return None

        value = None
        columnName = self.columnNames[index.column()]
        plotStyle = self.idx2plotStyle(index)
        if isinstance(plotStyle, TemporalProfile2DPlotStyle):
            sensor = plotStyle.sensor()
            #print(('data', columnName, role))
            if role == Qt.DisplayRole:
                if columnName == self.cnSensor:
                    if isinstance(sensor, SensorInstrument):
                        value = sensor.name()
                    else:
                        value = '<Select Sensor>'
                elif columnName == self.cnExpression:
                    value = plotStyle.expression()
                elif columnName == self.cnTemporalProfile:
                    tp = plotStyle.temporalProfile()
                    if isinstance(tp, TemporalProfile):
                        value = '{} "{}"'.format(tp.id(), tp.name())
                    else:
                        value = 'undefined'

            elif role == Qt.CheckStateRole:
                if columnName == self.cnTemporalProfile:
                    value = Qt.Checked if plotStyle.isVisible() else Qt.Unchecked

            elif role == Qt.UserRole:
                value = plotStyle
                if columnName == self.cnSensor:
                    value = plotStyle.sensor()
                elif columnName == self.cnExpression:
                    value = plotStyle.expression()
                elif columnName == self.cnStyle:
                    value = plotStyle
                elif columnName == self.cnTemporalProfile:
                    value == plotStyle.temporalProfile()
                else:
                    value = plotStyle
        #print(('get data',value))
        return value

    def setData(self, index, value, role=None):
        if role is None or not index.isValid():
            return False
        #print(('Set data', index.row(), index.column(), value, role))
        columnName = self.columnNames[index.column()]


        result = False
        plotStyle = self.idx2plotStyle(index)
        if isinstance(plotStyle, TemporalProfile2DPlotStyle):
            if role in [Qt.DisplayRole]:
                if columnName == self.cnExpression:
                    plotStyle.setExpression(value)
                    plotStyle.updateDataProperties()
                    result = True
                elif columnName == self.cnStyle:
                    if isinstance(value, PlotStyle):
                        plotStyle.copyFrom(value)
                        plotStyle.updateStyleProperties()
                        result = True

            if role == Qt.CheckStateRole:
                if columnName == self.cnTemporalProfile:
                    plotStyle.setVisibility(value == Qt.Checked)
                    result = True

            if role == Qt.EditRole:
                if columnName == self.cnExpression:
                    plotStyle.setExpression(value)

                    result = True
                elif columnName == self.cnStyle:
                    plotStyle.copyFrom(value)
                    result = True
                elif columnName == self.cnSensor:
                    plotStyle.setSensor(value)
                    result = True
                elif columnName == self.cnTemporalProfile:
                    plotStyle.setTemporalProfile(value)
                    result = True

        if result:
            #save plot-style
            self.savePlotSettings(plotStyle, index='DEFAULT')
            self.dataChanged.emit(index, index)

        return result

    def savePlotSettings(self, sensorPlotSettings, index='DEFAULT'):
        return

    def restorePlotSettings(self, sensor, index='DEFAULT'):
        return None


    def flags(self, index):
        if index.isValid():
            columnName = self.columnNames[index.column()]
            flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable
            if columnName in [self.cnTemporalProfile]:
                flags = flags | Qt.ItemIsUserCheckable
            if columnName in [self.cnTemporalProfile, self.cnSensor, self.cnExpression, self.cnStyle]: #allow check state
                flags = flags | Qt.ItemIsEditable
            return flags
            #return item.qt_flags(index.column())
        return Qt.NoItemFlags

    def headerData(self, col, orientation, role):
        if Qt is None:
            return None
        if orientation == Qt.Horizontal and role == Qt.DisplayRole:
            return self.columnNames[col]
        elif orientation == Qt.Vertical and role == Qt.DisplayRole:
            return col
        return None




class PlotSettingsTableView(QTableView):

    def __init__(self, *args, **kwds):
        super(PlotSettingsTableView, self).__init__(*args, **kwds)

        pal = self.palette()
        cSelected = pal.color(QPalette.Active, QPalette.Highlight)
        pal.setColor(QPalette.Inactive, QPalette.Highlight, cSelected)
        self.setPalette(pal)

    def contextMenuEvent(self, event: QContextMenuEvent):
        """
        Creates and shows the QMenu
        :param event: QContextMenuEvent
        """

        indices = self.selectionModel().selectedIndexes()

        indices2 = [self.model().mapToSource(idx) for idx in indices]

        if len(indices2) > 0:
            menu = QMenu(self)
            a = menu.addAction('Set Style')
            a.triggered.connect(lambda *args, i=indices2: self.onSetStyle(i))

            menu.popup(QCursor.pos())

    def onSetStyle(self, indices):

        if len(indices) > 0:
            refStyle = None

            model = self.model().sourceModel()
            assert isinstance(model, PlotSettingsModel2D)

            colStyle = model.columnIndex(model.cnStyle)

            for idx in indices:
                style = self.model().sourceModel().idx2plotStyle(idx)
                if isinstance(style, PlotStyle):
                    refStyle = style
                    break
            if isinstance(refStyle, PlotStyle):
                newStyle = PlotStyleDialog.getPlotStyle(plotStyle=refStyle)
                if isinstance(newStyle, PlotStyle):
                    for idx in indices:
                        assert isinstance(idx, QModelIndex)
                        idxStyle = model.createIndex(idx.row(), colStyle)
                        model.setData(idxStyle, newStyle, role=Qt.EditRole)



class PlotSettingsModel2DWidgetDelegate(QStyledItemDelegate):
    """

    """
    def __init__(self, tableView, temporalProfileLayer, parent=None):
        assert isinstance(tableView, QTableView)
        assert isinstance(temporalProfileLayer, TemporalProfileLayer)
        super(PlotSettingsModel2DWidgetDelegate, self).__init__(parent=parent)
        self._preferedSize = QgsFieldExpressionWidget().sizeHint()
        self.mTableView = tableView
        self.mTemporalProfileLayer = temporalProfileLayer
        self.mTimeSeries = temporalProfileLayer.timeSeries()

        self.mSensorLayers = {}

    def paint(self, painter: QPainter, option: 'QStyleOptionViewItem', index: QtCore.QModelIndex):

        if index.column() == 2:
            style = self.style(index)

            h = self.mTableView.verticalHeader().defaultSectionSize()
            w = self.mTableView.horizontalHeader().defaultSectionSize()
            if h > 0 and w > 0:
                px = style.createPixmap(size=QSize(w, h))
                label = QLabel()
                label.setPixmap(px)
                painter.drawPixmap(option.rect, px)
                #QApplication.style().drawControl(QStyle.CE_CustomBase, label, painter)
            else:
                super(PlotSettingsModel2DWidgetDelegate, self).paint(painter, option, index)
        else:
            super(PlotSettingsModel2DWidgetDelegate, self).paint(painter, option, index)

    def sortFilterProxyModel(self)->QSortFilterProxyModel:
        return self.mTableView.model()

    def plotSettingsModel(self)->PlotSettingsModel2D:
        return self.sortFilterProxyModel().sourceModel()

    def setItemDelegates(self, tableView):
        assert isinstance(tableView, QTableView)
        model = self.plotSettingsModel()

        assert isinstance(model, PlotSettingsModel2D)
        for c in [model.cnSensor, model.cnExpression, model.cnStyle, model.cnTemporalProfile]:
            i = model.columnNames.index(c)
            tableView.setItemDelegateForColumn(i, self)

    def getColumnName(self, index):
        assert index.isValid()
        model = self.plotSettingsModel()
        assert isinstance(model, PlotSettingsModel2D)
        return model.columnNames[index.column()]
    """
    def sizeHint(self, options, index):
        s = super(ExpressionDelegate, self).sizeHint(options, index)
        exprString = self.tableView.model().data(index)
        l = QLabel()
        l.setText(exprString)
        x = l.sizeHint().width() + 100
        s = QSize(x, s.height())
        return self._preferedSize
    """
    def exampleLyr(self, sensor):
        # if isinstance(sensor, SensorInstrument):
        if sensor not in self.mSensorLayers.keys():

            crs = QgsCoordinateReferenceSystem('EPSG:4862')
            uri = 'Point?crs={}'.format(crs.authid())
            lyr = QgsVectorLayer(uri, 'LOCATIONS', 'memory')
            f = sensorExampleQgsFeature(sensor)
            assert isinstance(f, QgsFeature)
            assert lyr.startEditing()
            for field in f.fields():
                lyr.addAttribute(field)
            lyr.addFeature(f)
            lyr.commitChanges()
            self.mSensorLayers[sensor] = lyr
        return self.mSensorLayers[sensor]

    def createEditor(self, parent, option, index):
        cname = self.getColumnName(index)
        model = self.plotSettingsModel()
        pmodel = self.sortFilterProxyModel()

        w = None
        if index.isValid() and isinstance(model, PlotSettingsModel2D):

            plotStyle = model.idx2plotStyle(pmodel.mapToSource(index))

            if isinstance(plotStyle, TemporalProfile2DPlotStyle):
                if cname == model.cnExpression:

                    w = QgsFieldExpressionWidget(parent=parent)
                    w.setExpressionDialogTitle('Values')
                    w.setToolTip('Set an expression to specify the image band or calculate a spectral index.')
                    w.fieldChanged[str, bool].connect(lambda n, b: self.checkData(index, w, w.expression()))
                    w.setExpression(plotStyle.expression())

                    if isinstance(plotStyle.sensor(), SensorInstrument):
                        w.setLayer(self.exampleLyr(plotStyle.sensor()))

                elif cname == model.cnStyle:
                    w = PlotStyleButton(parent=parent)
                    w.setPlotStyle(plotStyle)
                    w.setToolTip('Set style.')
                    w.sigPlotStyleChanged.connect(lambda: self.checkData(index, w, w.plotStyle()))

                elif cname == model.cnSensor:
                    w = QComboBox(parent=parent)
                    m = SensorListModel(self.mTimeSeries)
                    w.setModel(m)

                elif cname == model.cnTemporalProfile:
                    w = QgsFeatureListComboBox(parent=parent)
                    w.setSourceLayer(self.mTemporalProfileLayer)
                    w.setIdentifierField(FN_ID)
                    w.setDisplayExpression('to_string("{}")+\'  \'+"name"'.format(FN_ID))
                    w.setAllowNull(False)
                else:
                    raise NotImplementedError()
        return w

    def checkData(self, index, w, value):
        assert isinstance(index, QModelIndex)
        model = self.mTableView.model()
        if index.isValid() and isinstance(model, PlotSettingsModel2D):
            plotStyle = model.idx2plotStyle(index)
            assert isinstance(plotStyle, TemporalProfile2DPlotStyle)
            if isinstance(w, QgsFieldExpressionWidget):
                assert value == w.expression()
                assert w.isExpressionValid(value) == w.isValidExpression()

                if w.isValidExpression():
                    self.commitData.emit(w)
                else:
                    s = ""
                    #print(('Delegate commit failed',w.asExpression()))
            if isinstance(w, PlotStyleButton):

                self.commitData.emit(w)

    def style(self, proxyIndex:QModelIndex)->PlotStyle:
        model = self.plotSettingsModel()
        index = self.sortFilterProxyModel().mapToSource(proxyIndex)
        return model.data(index, role=Qt.UserRole)

    def setEditorData(self, editor, proxyIndex):

        model = self.plotSettingsModel()
        index = self.sortFilterProxyModel().mapToSource(proxyIndex)
        w = None
        if index.isValid() and isinstance(model, PlotSettingsModel2D):
            cname = self.getColumnName(proxyIndex)
            if cname == model.cnExpression:
                lastExpr = model.data(index, Qt.DisplayRole)
                assert isinstance(editor, QgsFieldExpressionWidget)
                editor.setProperty('lastexpr', lastExpr)
                editor.setField(lastExpr)

            elif cname == model.cnStyle:
                style = model.data(index, Qt.UserRole)
                assert isinstance(editor, PlotStyleButton)
                editor.setPlotStyle(style)

            elif cname == model.cnSensor:
                assert isinstance(editor, QComboBox)
                m = editor.model()
                assert isinstance(m, SensorListModel)
                sensor = index.data(role=Qt.UserRole)
                if isinstance(sensor, SensorInstrument):
                    idx = m.sensor2idx(sensor)
                    editor.setCurrentIndex(idx.row())

            elif cname == model.cnTemporalProfile:
                assert isinstance(editor, QgsFeatureListComboBox)
                value = editor.identifierValue()
                if value != QVariant():
                    plotStyle = index.data(role=Qt.UserRole)
                    TP = plotStyle.temporalProfile()
                    editor.setIdentifierValue(TP.id())
                else:
                    s  = ""
            else:
                raise NotImplementedError()

    def setModelData(self, w, model, proxyIndex):
        index = self.sortFilterProxyModel().mapToSource(proxyIndex)
        cname = self.getColumnName(proxyIndex)
        model = self.plotSettingsModel()

        if index.isValid() and isinstance(model, PlotSettingsModel2D):
            if cname == model.cnExpression:
                assert isinstance(w, QgsFieldExpressionWidget)
                expr = w.asExpression()
                exprLast = model.data(index, Qt.DisplayRole)

                if w.isValidExpression() and expr != exprLast:
                    model.setData(index, w.asExpression(), Qt.EditRole)

            elif cname == model.cnStyle:
                if isinstance(w, PlotStyleButton):
                    style = w.plotStyle()
                    model.setData(index, style, Qt.EditRole)

            elif cname == model.cnSensor:
                assert isinstance(w, QComboBox)
                sensor = w.itemData(w.currentIndex(), role=Qt.UserRole)
                if isinstance(sensor, SensorInstrument):
                    model.setData(index, sensor, Qt.EditRole)

            elif cname == model.cnTemporalProfile:
                assert isinstance(w, QgsFeatureListComboBox)
                fid = w.identifierValue()
                if isinstance(fid, int):
                    # once set manually, do not update to last temporal profile any more
                    oldStyle = index.data(role=Qt.UserRole)
                    if isinstance(oldStyle, TemporalProfile2DPlotStyle):
                        oldStyle.mShowLastLocation = False
                    TP = self.mTemporalProfileLayer.mProfiles.get(fid)
                    model.setData(index, TP, Qt.EditRole)

            else:
                raise NotImplementedError()



class PlotSettingsModel3DWidgetDelegate(QStyledItemDelegate):
    """

    """
    def __init__(self, tableView, temporalProfileLayer, parent=None):
        assert isinstance(tableView, QTableView)
        assert isinstance(temporalProfileLayer, TemporalProfileLayer)
        super(PlotSettingsModel3DWidgetDelegate, self).__init__(parent=parent)
        self._preferedSize = QgsFieldExpressionWidget().sizeHint()
        self.mTableView = tableView
        self.mTimeSeries = temporalProfileLayer.timeSeries()
        self.mTemporalProfileLayer = temporalProfileLayer
        self.mSensorLayers = {}

    def setItemDelegates(self, tableView):
        assert isinstance(tableView, QTableView)
        model = tableView.model()
        assert isinstance(model, PlotSettingsModel3D)
        for c in [model.cnSensor,
                  model.cnExpression,
                  # model.cnStyle,
                  model.cnTemporalProfile]:
            i = model.columnNames.index(c)
            tableView.setItemDelegateForColumn(i, self)

    def getColumnName(self, index):
        assert index.isValid()
        model = index.model()
        assert isinstance(model, PlotSettingsModel3D)
        return model.columnNames[index.column()]
    """
    def sizeHint(self, options, index):
        s = super(ExpressionDelegate, self).sizeHint(options, index)
        exprString = self.tableView.model().data(index)
        l = QLabel()
        l.setText(exprString)
        x = l.sizeHint().width() + 100
        s = QSize(x, s.height())
        return self._preferedSize
    """
    def createEditor(self, parent, option, index):
        cname = self.getColumnName(index)
        model = self.mTableView.model()
        w = None
        if index.isValid() and isinstance(model, PlotSettingsModel3D):
            plotStyle = model.idx2plotStyle(index)
            if isinstance(plotStyle, TemporalProfile3DPlotStyle):
                if cname == model.cnExpression:
                    w = QgsFieldExpressionWidget(parent=parent)
                    w.setExpression(plotStyle.expression())
                    w.setLayer(self.exampleLyr(plotStyle.sensor()))
                    def onSensorAdded(s):
                        w.setLayer(self.exampleLyr(s))
                    #plotStyle.sigSensorChanged.connect(lambda s : w.setLayer(self.exampleLyr(s)))
                    plotStyle.sigSensorChanged.connect(onSensorAdded)
                    w.setExpressionDialogTitle('Values')
                    w.setToolTip('Set an expression to specify the image band or calculate a spectral index.')
                    w.fieldChanged[str, bool].connect(lambda n, b : self.checkData(index, w, w.expression()))

                elif cname == model.cnStyle:
                    w = TemporalProfile3DPlotStyleButton(parent=parent)
                    w.setPlotStyle(plotStyle)
                    w.setToolTip('Set plot style')
                    w.sigPlotStyleChanged.connect(lambda: self.checkData(index, w, w.plotStyle()))

                elif cname == model.cnSensor:
                    w = QComboBox(parent=parent)
                    m = SensorListModel(self.mTimeSeries)
                    w.setModel(m)

                elif cname == model.cnTemporalProfile:
                    w = QgsFeatureListComboBox(parent=parent)
                    w.setSourceLayer(self.mTemporalProfileLayer)
                    w.setIdentifierField('id')
                    w.setDisplayExpression('to_string("id")+\'  \'+"name"')
                    w.setAllowNull(False)
                else:
                    raise NotImplementedError()
        return w

    def exampleLyr(self, sensor):

        if sensor not in self.mSensorLayers.keys():
            crs = QgsCoordinateReferenceSystem('EPSG:4862')
            uri = 'Point?crs={}'.format(crs.authid())
            lyr = QgsVectorLayer(uri, 'LOCATIONS', 'memory')
            assert sensor is None or isinstance(sensor, SensorInstrument)

            f = sensorExampleQgsFeature(sensor, singleBandOnly=True)
            assert isinstance(f, QgsFeature)
            assert lyr.startEditing()
            for field in f.fields():
                lyr.addAttribute(field)
            lyr.addFeature(f)
            lyr.commitChanges()
            self.mSensorLayers[sensor] = lyr
        return self.mSensorLayers[sensor]

    def checkData(self, index, w, value):
        assert isinstance(index, QModelIndex)
        model = self.mTableView.model()
        if index.isValid() and isinstance(model, PlotSettingsModel3D):
            plotStyle = model.idx2plotStyle(index)
            assert isinstance(plotStyle, TemporalProfile3DPlotStyle)
            if isinstance(w, QgsFieldExpressionWidget):
                assert value == w.expression()
                assert w.isExpressionValid(value) == w.isValidExpression()

                if w.isValidExpression():
                    self.commitData.emit(w)
                else:
                    s = ""
                    #print(('Delegate commit failed',w.asExpression()))
            if isinstance(w, TemporalProfile3DPlotStyleButton):

                self.commitData.emit(w)


    def setEditorData(self, editor, index):
        cname = self.getColumnName(index)
        model = self.mTableView.model()

        w = None
        if index.isValid() and isinstance(model, PlotSettingsModel3D):
            cname = self.getColumnName(index)
            style = model.idx2plotStyle(index)

            if cname == model.cnExpression:
                lastExpr = index.model().data(index, Qt.DisplayRole)
                assert isinstance(editor, QgsFieldExpressionWidget)
                editor.setProperty('lastexpr', lastExpr)
                editor.setField(lastExpr)

            elif cname == model.cnStyle:
                assert isinstance(editor, TemporalProfile3DPlotStyleButton)
                editor.setPlotStyle(style)

            elif cname == model.cnSensor:
                assert isinstance(editor, QComboBox)
                m = editor.model()
                assert isinstance(m, SensorListModel)
                sensor = index.data(role=Qt.UserRole)
                if isinstance(sensor, SensorInstrument):
                    idx = m.sensor2idx(sensor)
                    editor.setCurrentIndex(idx.row())
            elif cname == model.cnTemporalProfile:
                assert isinstance(editor, QgsFeatureListComboBox)
                value = editor.identifierValue()
                if value != QVariant():
                    plotStyle = index.data(role=Qt.UserRole)
                    TP = plotStyle.temporalProfile()
                    editor.setIdentifierValue(TP.id())
                else:
                    s  = ""

            else:
                raise NotImplementedError()

    def setModelData(self, w, model, index):
        cname = self.getColumnName(index)
        model = self.mTableView.model()

        if index.isValid() and isinstance(model, PlotSettingsModel3D):
            if cname == model.cnExpression:
                assert isinstance(w, QgsFieldExpressionWidget)
                expr = w.asExpression()
                exprLast = model.data(index, Qt.DisplayRole)

                if w.isValidExpression() and expr != exprLast:
                    model.setData(index, w.asExpression(), Qt.EditRole)

            elif cname == model.cnStyle:
                assert isinstance(w, TemporalProfile3DPlotStyleButton)
                model.setData(index, w.plotStyle(), Qt.EditRole)

            elif cname == model.cnSensor:
                assert isinstance(w, QComboBox)
                sensor = w.itemData(w.currentIndex(), role=Qt.UserRole)
                assert isinstance(sensor, SensorInstrument)
                model.setData(index, sensor, Qt.EditRole)

                s = ""

            elif cname == model.cnTemporalProfile:
                assert isinstance(w, QgsFeatureListComboBox)
                fid = w.identifierValue()
                if isinstance(fid, int):
                    TP = self.mTemporalProfileLayer.mProfiles.get(fid)
                    model.setData(index, TP, Qt.EditRole)

            else:
                raise NotImplementedError()



class ProfileViewDockUI(QgsDockWidget, loadUI('profileviewdock.ui')):


    def __init__(self, parent=None):
        super(ProfileViewDockUI, self).__init__(parent)
        self.setupUi(self)

        self.addActions(self.findChildren(QAction))

        self.mActions2D = [self.actionAddStyle2D, self.actionRemoveStyle2D, self.actionRefresh2D, self.actionReset2DPlot]
        self.mActions3D = [self.actionAddStyle3D, self.actionRemoveStyle3D, self.actionRefresh3D,
                           self.actionReset3DCamera]
        self.mActionsTP = [self.actionLoadTPFromOgr, self.actionSaveTemporalProfiles, self.actionToggleEditing,
                           self.actionRemoveTemporalProfile, self.actionLoadMissingValues]
        #TBD.
        #self.line.setVisible(False)
        #self.listWidget.setVisible(False)
        self.baseTitle = self.windowTitle()
        self.stackedWidget.currentChanged.connect(self.onStackPageChanged)


        self.plotWidget3D = None
        self.plotWidget3DMPL = None

        self.init3DWidgets('gl')


        #pi = self.plotWidget2D.plotItem
        #ax = DateAxis(orientation='bottom', showValues=True)
        #pi.layout.addItem(ax, 3,2)


        self.TS = None

        self.progressBar.setMinimum(0)
        self.progressBar.setMaximum(100)
        self.progressBar.setValue(0)
        self.progressInfo.setText('')
        self.pxViewModel2D = None
        self.pxViewModel3D = None

        self.tableView2DProfiles.horizontalHeader().setResizeMode(QHeaderView.Interactive)

    def init3DWidgets(self, mode='gl'):
        assert mode in ['gl']
        l = self.frame3DPlot.layout()

        if ENABLE_OPENGL and OPENGL_AVAILABLE and mode == 'gl':

            from eotimeseriesviewer.temporalprofiles3dGL import ViewWidget3D
            self.plotWidget3D = ViewWidget3D(parent=self.labelDummy3D.parent())
            self.plotWidget3D.setObjectName('plotWidget3D')

            size = self.labelDummy3D.size()
            l.addWidget(self.plotWidget3D)
            self.plotWidget3D.setSizePolicy(self.labelDummy3D.sizePolicy())
            self.labelDummy3D.setVisible(False)
            l.removeWidget(self.labelDummy3D)
            #self.plotWidget3D.setBaseSize(size)
            self.splitter3D.setSizes([100, 100])
            self.frameSettings3D.setEnabled(True)
        else:
            self.frameSettings3D.setEnabled(False)

    def onStackPageChanged(self, i):
        w = self.stackedWidget.currentWidget()
        title = self.baseTitle
        if w == self.page2D:
            title = '{} | 2D'.format(title)
            for a in self.mActions2D:
                a.setVisible(True)
            for a in self.mActions3D + self.mActionsTP:
                a.setVisible(False)
        elif w == self.page3D:
            title = '{} | 3D (experimental!)'.format(title)
            for a in self.mActions2D + self.mActionsTP:
                a.setVisible(False)
            for a in self.mActions3D:
                a.setVisible(True)
        elif w == self.pagePixel:
            title = '{} | Coordinates'.format(title)
            for a in self.mActions2D + self.mActions3D:
                a.setVisible(False)
            for a in self.mActionsTP:
                a.setVisible(True)

        w.update()
        self.setWindowTitle(title)




class SpectralTemporalVisualization(QObject):

    sigShowPixel = pyqtSignal(TimeSeriesDate, QgsPoint, QgsCoordinateReferenceSystem)

    """
    Signalizes to move to specific date of interest
    """
    sigMoveToDate = pyqtSignal(np.datetime64)


    def __init__(self, timeSeries, profileDock):
        super(SpectralTemporalVisualization, self).__init__()

        assert isinstance(profileDock, ProfileViewDockUI)
        self.ui = profileDock
        self.mTasks = dict()
        #import eotimeseriesviewer.pixelloader
        #if DEBUG:
        #    eotimeseriesviewer.pixelloader.DEBUG = True

        #the timeseries. will be set later
        assert isinstance(timeSeries, TimeSeries)
        self.TS = timeSeries
        self.plot_initialized = False

        self.plot2D = self.ui.plotWidget2D
        assert isinstance(self.plot2D, DateTimePlotWidget)
        self.plot2D.getViewBox().sigMoveToDate.connect(self.sigMoveToDate)
        self.plot2D.getViewBox().scene().sigMouseClicked.connect(self.onPointsClicked2D)
        self.plot3D = self.ui.plotWidget3D
        self.mLast2DMouseClickPosition = None

        # temporal profile collection to store loaded values
        self.mTemporalProfileLayer = TemporalProfileLayer(self.TS)
        self.mTemporalProfileLayer.sigTemporalProfilesAdded.connect(self.onTemporalProfilesAdded)
        self.mTemporalProfileLayer.selectionChanged.connect(self.onTemporalProfileSelectionChanged)

        # fix to not loose C++ reference on temporal profile layer in case it is removed from QGIS mapcanvas
        self.mMapCanvas = QgsMapCanvas()
        self.mMapCanvas.setVisible(False)
        self.mMapCanvas.setLayers([self.mTemporalProfileLayer])
        # self.tpCollectionListModel = TemporalProfileCollectionListModel(self.tpCollection)

        assert isinstance(self.ui.mDualView, QgsDualView)
        self.ui.mDualView.init(self.mTemporalProfileLayer, self.mMapCanvas)
        self.ui.mDualView.setView(QgsDualView.AttributeTable)
        # pixel loader to load pixel values in parallel
        config = QgsAttributeTableConfig()
        config.update(self.mTemporalProfileLayer.fields())
        config.setActionWidgetVisible(False)

        hidden = []
        for i, columnConfig in enumerate(config.columns()):

            assert isinstance(columnConfig, QgsAttributeTableConfig.ColumnConfig)
            config.setColumnHidden(i, columnConfig.name in hidden)


        self.mTemporalProfilesTableConfig = config
        self.mTemporalProfileLayer.setAttributeTableConfig(self.mTemporalProfilesTableConfig)

        #self.pixelLoader = PixelLoader()
        #self.pixelLoader.sigPixelLoaded.connect(self.onPixelLoaded)
        #self.pixelLoader.sigLoadingStarted.connect(self.onLoadingStarted)
        #self.pixelLoader.sigLoadingFinished.connect(self.onLoadingFinished)
        #self.pixelLoader.sigProgressChanged.connect(self.onLoadingProgressChanged)

        # set the plot models for 2D
        self.plotSettingsModel2D = PlotSettingsModel2D()
        self.plotSettingsModel2DProxy = QSortFilterProxyModel()
        self.plotSettingsModel2DProxy.setSourceModel(self.plotSettingsModel2D)
        self.ui.tableView2DProfiles.setModel(self.plotSettingsModel2DProxy)
        self.ui.tableView2DProfiles.setSortingEnabled(True)
        self.ui.tableView2DProfiles.selectionModel().selectionChanged.connect(self.onPlot2DSelectionChanged)
        self.ui.tableView2DProfiles.horizontalHeader().setResizeMode(QHeaderView.Interactive)
        self.plotSettingsModel2D.sigDataChanged.connect(self.requestUpdate)
        self.plotSettingsModel2D.rowsInserted.connect(self.onRowsInserted2D)
        self.delegateTableView2D = PlotSettingsModel2DWidgetDelegate(self.ui.tableView2DProfiles, self.mTemporalProfileLayer)
        self.delegateTableView2D.setItemDelegates(self.ui.tableView2DProfiles)
        #self.ui.tableView2DProfiles.horizontalHeader().sectionResized.connect(self.on2DSettingsTableColumnResize)

        # set the plot models for 3D
        self.plotSettingsModel3D = PlotSettingsModel3D()
        self.ui.tableView3DProfiles.setModel(self.plotSettingsModel3D)
        self.ui.tableView3DProfiles.setSortingEnabled(False)
        self.ui.tableView2DProfiles.selectionModel().selectionChanged.connect(self.onPlot3DSelectionChanged)
        self.ui.tableView3DProfiles.horizontalHeader().setResizeMode(QHeaderView.Interactive)
        self.plotSettingsModel3D.rowsInserted.connect(self.onRowsInserted3D)
        self.delegateTableView3D = PlotSettingsModel3DWidgetDelegate(self.ui.tableView3DProfiles, self.mTemporalProfileLayer)
        self.delegateTableView3D.setItemDelegates(self.ui.tableView3DProfiles)

        if not ENABLE_OPENGL:
            self.ui.listWidget.item(1).setHidden(True)
            self.ui.page3D.setHidden(True)


        def onTemporalProfilesRemoved(removedProfiles):
            #set to valid temporal profile

            affectedStyles2D = [p for p in self.plotSettingsModel2D if p.temporalProfile() in removedProfiles]
            affectedStyles3D = [p for p in self.plotSettingsModel3D if p.temporalProfile() in removedProfiles]
            alternativeProfile = self.mTemporalProfileLayer[-1] if len(self.mTemporalProfileLayer) > 0 else None
            for s in affectedStyles2D:
                assert isinstance(s, TemporalProfile2DPlotStyle)
                m = self.plotSettingsModel2D
                idx = m.plotStyle2idx(s)
                idx = m.createIndex(idx.row(), m.columnNames.index(m.cnTemporalProfile))
                m.setData(idx, alternativeProfile, Qt.EditRole)

            for s in affectedStyles3D:
                assert isinstance(s, TemporalProfile3DPlotStyle)
                m = self.plotSettingsModel3D
                idx = m.plotStyle2idx(s)
                idx = m.createIndex(idx.row(), m.columnNames.index(m.cnTemporalProfile))
                m.setData(idx, alternativeProfile, Qt.EditRole)

        self.mTemporalProfileLayer.sigTemporalProfilesRemoved.connect(onTemporalProfilesRemoved)

        # remove / add plot style
        def on2DPlotStyleRemoved(plotStyles):
            for plotStyle in plotStyles:
                assert isinstance(plotStyle, PlotStyle)
                for pi in plotStyle.mPlotItems:
                    self.plot2D.plotItem.removeItem(pi)

        def on3DPlotStyleRemoved(plotStyles):
            toRemove = []
            for plotStyle in plotStyles:
                assert isinstance(plotStyle, TemporalProfile3DPlotStyle)
                toRemove.append(plotStyle.mPlotItems)
            self.plot3D.removeItems(toRemove)


        self.plotSettingsModel2D.sigPlotStylesRemoved.connect(on2DPlotStyleRemoved)
        self.plotSettingsModel3D.sigPlotStylesRemoved.connect(on3DPlotStyleRemoved)

        # initialize the update loop
        self.updateRequested = True
        self.updateTimer = QTimer(self)
        self.updateTimer.timeout.connect(self.onDataUpdate)
        self.updateTimer.start(2000)

        self.sigMoveToDate.connect(self.onMoveToDate)

        self.initActions()
        #self.ui.stackedWidget.setCurrentPage(self.ui.pagePixel)
        self.ui.onStackPageChanged(self.ui.stackedWidget.currentIndex())

    def onLoadingStarted(self):
        self.ui.progressInfo.setText('Start loading...')
        self.ui.progressBar.setRange(0, 0)
        self.ui.progressBar.setValue(0)

    def onLoadingProgressChanged(self, progress):
        if self.ui.progressBar.maximum() == 0:
            self.ui.progressBar.setRange(0, 100)

        value = int(progress)
        self.ui.progressBar.setValue(value)

        if value == 100:
            self.plot2D.enableAutoRange('x', False)
            self.ui.progressInfo.setText('')
            self.ui.progressBar.setValue(0)


    def plotStyles(self):
        return self.plotSettingsModel2D[:]

    def temporalProfileLayer(self)->TemporalProfileLayer:
        """
        Returns a QgsVectorLayer that is used to store profile coordinates.
        :return:
        """
        return self.mTemporalProfileLayer

    def selected2DPlotStyles(self):
        result = []

        m = self.ui.tableView2DProfiles.model()
        for idx in selectedModelIndices(self.ui.tableView2DProfiles):
            result.append(m.data(idx, Qt.UserRole))
        return result

    def removePlotStyles2D(self, plotStyles):
        m = self.ui.tableView2DProfiles.model()
        if isinstance(m.sourceModel(), PlotSettingsModel2D):
            m.sourceModel().removePlotStyles(plotStyles)

    def removeTemporalProfiles(self, fids):

        self.mTemporalProfileLayer.selectByIds(fids)
        b = self.mTemporalProfileLayer.isEditable()
        self.mTemporalProfileLayer.startEditing()
        self.mTemporalProfileLayer.deleteSelectedFeatures()
        self.mTemporalProfileLayer.saveEdits(leaveEditable=b)

    def createNewPlotStyle2D(self):
        l = len(self.mTemporalProfileLayer)


        plotStyle = TemporalProfile2DPlotStyle()
        plotStyle.sigExpressionUpdated.connect(self.updatePlot2D)

        sensors = self.TS.sensors()
        if len(sensors) > 0:
            plotStyle.setSensor(sensors[0])

        if len(self.mTemporalProfileLayer) > 0:
            temporalProfile = self.mTemporalProfileLayer[0]
            plotStyle.setTemporalProfile(temporalProfile)

        if len(self.plotSettingsModel2D) > 0:
            lastStyle = self.plotSettingsModel2D[0] #top style in list is the most-recent
            assert isinstance(lastStyle, TemporalProfile2DPlotStyle)
            markerColor = nextColor(lastStyle.markerBrush.color())
            plotStyle.markerBrush.setColor(markerColor)
        self.plotSettingsModel2D.insertPlotStyles([plotStyle], i=0)
        pdi = plotStyle.createPlotItem(self.plot2D)

        assert isinstance(pdi, TemporalProfilePlotDataItem)
        #pdi.sigClicked.connect(self.onProfileClicked2D)
        #pdi.sigPointsClicked.connect(self.onPointsClicked2D)
        self.plot2D.plotItem.addItem(pdi)
        #self.plot2D.getPlotItem().addItem(pg.PlotDataItem(x=[1, 2, 3], y=[1, 2, 3]))
        #plotItem.addDataItem(pdi)
        #plotItem.plot().sigPlotChanged.emit(plotItem)
        self.updatePlot2D()
        return plotStyle


    def createNewPlotStyle3D(self):
        if not (ENABLE_OPENGL and OPENGL_AVAILABLE):
            return


        plotStyle = TemporalProfile3DPlotStyle()
        plotStyle.sigExpressionUpdated.connect(self.updatePlot3D)

        if len(self.mTemporalProfileLayer) > 0:
            temporalProfile = self.mTemporalProfileLayer[0]
            plotStyle.setTemporalProfile(temporalProfile)
            if len(self.plotSettingsModel3D) > 0:
                color = self.plotSettingsModel3D[-1].color()
                plotStyle.setColor(nextColor(color))

        sensors = list(self.TS.mSensors2TSDs.keys())
        if len(sensors) > 0:
            plotStyle.setSensor(sensors[0])


        self.plotSettingsModel3D.insertPlotStyles([plotStyle], i=0) # latest to the top
        plotItems = plotStyle.createPlotItem()
        self.plot3D.addItems(plotItems)
        #self.updatePlot3D()


    def onPointsClicked2D(self, event: MouseClickEvent):
        info = []
        assert isinstance(event, MouseClickEvent)
        for item in self.plot2D.scene().itemsNearEvent(event):

            if isinstance(item, ScatterPlotItem) and isinstance(item.parentItem(), TemporalProfilePlotDataItem):
                pdi = item.parentItem()
                assert isinstance(pdi, TemporalProfilePlotDataItem)
                tp = pdi.mPlotStyle.temporalProfile()
                assert isinstance(tp, TemporalProfile)
                c = tp.coordinate()

                spottedItems = item.pointsAt(event.pos())
                if len(spottedItems) > 0:
                    info.append('Sensor: {}'.format(pdi.mPlotStyle.sensor().name()))
                    info.append('Coordinate: {}, {}'.format(c.x(), c.y()))
                    for item in spottedItems:
                        if isinstance(item, SpotItem):
                            brush1 = item.brush()
                            brush2 = item.brush()
                            brush2.setColor(QColor('yellow'))
                            item.setBrush(brush2)
                            QTimer.singleShot(500, lambda *args, spotItem=item, brush=brush1: spotItem.setBrush(brush))

                            pos = item.pos()
                            self.mLast2DMouseClickPosition = pos
                            x = pos.x()
                            y = pos.y()
                            date = num2date(x)
                            info.append('{};{}'.format(date, y))

        self.ui.tbInfo2D.setPlainText('\n'.join(info))


    def onTemporalProfilesAdded(self, profiles):
        # self.mTemporalProfileLayer.prune()
        for plotStyle in self.plotSettingsModel3D:
            assert isinstance(plotStyle, TemporalProfilePlotStyleBase)
            if not isinstance(plotStyle.temporalProfile(), TemporalProfile):

                r = self.plotSettingsModel3D.plotStyle2idx(plotStyle).row()
                c = self.plotSettingsModel3D.columnIndex(self.plotSettingsModel3D.cnTemporalProfile)
                idx = self.plotSettingsModel3D.createIndex(r, c)
                self.plotSettingsModel3D.setData(idx, self.mTemporalProfileLayer[0])

    def onTemporalProfileSelectionChanged(self, selectedFIDs, deselectedFIDs):
        nSelected = len(selectedFIDs)

        self.ui.actionRemoveTemporalProfile.setEnabled(nSelected > 0)


    def onPlot2DSelectionChanged(self, selected, deselected):

        self.ui.actionRemoveStyle2D.setEnabled(len(selected) > 0)

        # todo: highlight selected profiles in plot


    def onPlot3DSelectionChanged(self, selected, deselected):

        self.ui.actionRemoveStyle3D.setEnabled(len(selected) > 0)

    def initActions(self):

        self.ui.actionRemoveStyle2D.setEnabled(False)
        self.ui.actionRemoveTemporalProfile.setEnabled(False)
        self.ui.actionAddStyle2D.triggered.connect(self.createNewPlotStyle2D)
        self.ui.actionAddStyle3D.triggered.connect(self.createNewPlotStyle3D)
        self.ui.actionRefresh2D.triggered.connect(self.updatePlot2D)
        self.ui.actionRefresh3D.triggered.connect(self.updatePlot3D)
        self.ui.actionRemoveStyle2D.triggered.connect(lambda:self.removePlotStyles2D(self.selected2DPlotStyles()))
        self.ui.actionRemoveTemporalProfile.triggered.connect(lambda :self.removeTemporalProfiles(self.mTemporalProfileLayer.selectedFeatureIds()))
        self.ui.actionToggleEditing.triggered.connect(self.onToggleEditing)
        self.ui.actionReset2DPlot.triggered.connect(self.plot2D.resetViewBox)
        self.plot2D.resetTransform()
        self.ui.actionReset3DCamera.triggered.connect(self.reset3DCamera)
        self.ui.actionLoadTPFromOgr.triggered.connect(self.onLoadFromVector)
        self.ui.actionLoadMissingValues.triggered.connect(lambda: self.loadMissingData())
        self.ui.actionSaveTemporalProfiles.triggered.connect(lambda *args : self.mTemporalProfileLayer.saveTemporalProfiles(None))
        #set actions to be shown in the TemporalProfileTableView context menu
        ma = [self.ui.actionSaveTemporalProfiles, self.ui.actionLoadMissingValues]


        self.onEditingToggled()

    def onLoadFromVector(self):

        from .externals.qps.layerproperties import SelectMapLayersDialog

        d = SelectMapLayersDialog()
        d.addLayerDescription('Vector Layer', QgsMapLayerProxyModel.VectorLayer)
        d.setWindowTitle('Select Vector Layer')
        if d.exec() == QDialog.Accepted:
            for l in d.mapLayers():
                self.mTemporalProfileLayer.loadCoordinatesFromOgr(l.source())
                break


    def onToggleEditing(self, b):

        if self.mTemporalProfileLayer.isEditable():
            self.mTemporalProfileLayer.saveEdits(leaveEditable=False)
        else:
            self.mTemporalProfileLayer.startEditing()
        self.onEditingToggled()

    def onEditingToggled(self):
        lyr = self.mTemporalProfileLayer

        hasSelectedFeatures = lyr.selectedFeatureCount() > 0
        isEditable = lyr.isEditable()
        self.ui.actionToggleEditing.blockSignals(True)
        self.ui.actionToggleEditing.setChecked(isEditable)
        #self.actionSaveTemporalProfiles.setEnabled(isEditable)
        #self.actionReload.setEnabled(not isEditable)
        self.ui.actionToggleEditing.blockSignals(False)

        #self.actionAddAttribute.setEnabled(isEditable)
        #self.actionRemoveAttribute.setEnabled(isEditable)
        self.ui.actionRemoveTemporalProfile.setEnabled(isEditable and hasSelectedFeatures)
        #self.actionPasteFeatures.setEnabled(isEditable)
        self.ui.actionToggleEditing.setEnabled(not lyr.readOnly())




    def reset3DCamera(self, *args):

        if ENABLE_OPENGL and OPENGL_AVAILABLE:
            self.ui.actionReset3DCamera.trigger()



    sigMoveToTSD = pyqtSignal(TimeSeriesDate)

    def onMoveToDate(self, date):
        dt = np.asarray([np.abs(tsd.date() - date) for tsd in self.TS])
        i = np.argmin(dt)
        self.sigMoveToTSD.emit(self.TS[i])


    def onPixelLoaded(self, qgsTask ,dump)->typing.List[TemporalProfile]:
        """
        Updates TemporalProfiles
        :param qgsTask:
        :param dump:
        :return: [updated TemporalProfiles]
        """
        tasks = pickle.loads(dump)
        assert isinstance(tasks, list)
        s = ""
        t0 = time.time()
        updatedTemporalProfiles = []
        for task in tasks:
            assert isinstance(task, TemporalProfileLoaderTask)

            if len(task.mRESULTS) > 0:
                for tpId, data in task.mRESULTS.items():
                    tp = self.mTemporalProfileLayer.mProfiles.get(tpId)

                    if isinstance(tp, TemporalProfile):
                        tsd = tp.timeSeries().getTSD(task.mSourcePath)
                        if isinstance(tsd, TimeSeriesDate):
                            tp.updateData(tsd, data)
                        if tp not in updatedTemporalProfiles:
                            updatedTemporalProfiles.append(tp)

            if len(task.mERRORS) > 0:


                s = ""
        return updatedTemporalProfiles
        if False:
            print('WRESULTS {}'.format(time.time() - t0))

    def requestUpdate(self, *args):
        self.updateRequested = True
        #next time

    def onRowsInserted2D(self, parent, start, end):
        model = self.ui.tableView2DProfiles.model().sourceModel()
        if isinstance(model, PlotSettingsModel2D):
            colExpression = model.columnIndex(model.cnExpression)
            colStyle = model.columnIndex(model.cnStyle)
            while start <= end:
                idxExpr = model.createIndex(start, colExpression)
                idxStyle = model.createIndex(start, colStyle)
                self.ui.tableView2DProfiles.openPersistentEditor(idxExpr)
                self.ui.tableView2DProfiles.openPersistentEditor(idxStyle)
                start += 1

    def onRowsInserted3D(self, parent, start, end):
        model = self.ui.tableView3DProfiles.model()
        if isinstance(model, PlotSettingsModel3D):
            colExpression = model.columnIndex(model.cnExpression)
            colStyle = model.columnIndex(model.cnStyle)
            while start <= end:
                idxStyle = model.createIndex(start, colStyle)
                idxExpr = model.createIndex(start, colExpression)
                self.ui.tableView3DProfiles.openPersistentEditor(idxStyle)
                self.ui.tableView3DProfiles.openPersistentEditor(idxExpr)
                start += 1

    def loadMissingData(self, backgroundProcess=True):
        """
        Loads all band values of collected locations that have not been loaded until now
        """

        fids = self.mTemporalProfileLayer.selectedFeatureIds()
        if len(fids) == 0:
            fids = [f.id() for f in self.mTemporalProfileLayer.getFeatures()]

        tps = [self.mTemporalProfileLayer.mProfiles.get(fid) for fid in fids]
        spatialPoints = [tp.coordinate() for tp in tps if isinstance(tp, TemporalProfile)]
        self.loadCoordinate(spatialPoints=spatialPoints, mode='all', backgroundProcess=backgroundProcess)

    LOADING_MODES = ['missing', 'reload', 'all']
    def loadCoordinate(self, spatialPoints=None, LUT_bandIndices=None, mode='missing', backgroundProcess = True):
        """
        :param spatialPoints: [list-of-geometries] to load pixel values from
        :param LUT_bandIndices: dictionary {sensor:[indices]} with band indices to be loaded per sensor
        :param mode:
        :return:
        """
        """
        Loads a temporal profile for a single or multiple geometries.
        :param spatialPoints: SpatialPoint | [list-of-SpatialPoints]
        """
        assert mode in SpectralTemporalVisualization.LOADING_MODES
        if isinstance(spatialPoints, SpatialPoint):
            spatialPoints = [spatialPoints]

        assert isinstance(spatialPoints, list)

        if not isinstance(self.plotSettingsModel2D, PlotSettingsModel2D):
            return False

        # if not self.pixelLoader.isReadyToLoad():
        #    return False

        assert isinstance(self.TS, TimeSeries)

        # Get or create the TimeSeriesProfiles which will store the loaded values
        tasks = []
        TemporalProfiles = []

        # Define which (new) bands need to be loaded for each sensor
        if LUT_bandIndices is None:
            LUT_bandIndices = dict()
            for sensor in self.TS.sensors():
                if mode in ['all', 'reload']:
                    LUT_bandIndices[sensor] = list(range(sensor.nb))
                else:
                    LUT_bandIndices[sensor] = self.plotSettingsModel2D.requiredBandsIndices(sensor)

        assert isinstance(LUT_bandIndices, dict)
        for sensor in self.TS.sensors():
            assert sensor in LUT_bandIndices.keys()

        # update new / existing points

        for spatialPoint in spatialPoints:
            assert isinstance(spatialPoint, SpatialPoint)
            TP = self.mTemporalProfileLayer.fromSpatialPoint(spatialPoint)

            # if no TemporalProfile existed before, create an empty one
            if not isinstance(TP, TemporalProfile):
                TP = self.mTemporalProfileLayer.createTemporalProfiles(spatialPoint)[0]

                # set existing plot style to current coordinate
                for plotStyle in self.plotSettingsModel2D:
                    assert isinstance(plotStyle, TemporalProfile2DPlotStyle)
                    if plotStyle.showLastLocation():
                        r = self.plotSettingsModel2D.plotStyle2idx(plotStyle).row()
                        c = self.plotSettingsModel2D.columnIndex(self.plotSettingsModel2D.cnTemporalProfile)
                        idx = self.plotSettingsModel2D.index(r, c)
                        self.plotSettingsModel2D.setData(idx, TP, role=Qt.EditRole)
                        plotStyle.setTemporalProfile(TP)

                # create at least 1 plot style
                if len(self.mTemporalProfileLayer) == 1:
                    if len(self.plotSettingsModel2D) == 0:
                        self.createNewPlotStyle2D()

                    if len(self.plotSettingsModel3D) == 0:
                        self.createNewPlotStyle3D()

            TemporalProfiles.append(TP)

        # each TSD is a Task
        s = ""
        # a Task defines which bands are to be loaded
        for tsd in self.TS:
            assert isinstance(tsd, TimeSeriesDate)

            # do not load from invisible TSDs
            if not tsd.isVisible():
                continue

            # which bands do we need to load?
            requiredIndices = set(LUT_bandIndices[tsd.sensor()])
            if len(requiredIndices) == 0:
                continue

            if mode == 'missing':
                missingIndices = set()

                for TP in TemporalProfiles:
                    assert isinstance(TP, TemporalProfile)
                    need2load = TP.missingBandIndices(tsd, requiredIndices=requiredIndices)
                    missingIndices = missingIndices.union(need2load)

                missingIndices = sorted(list(missingIndices))
            else:
                missingIndices = requiredIndices

            if len(missingIndices) > 0:
                for tss in tsd.sources():
                    assert isinstance(tss, TimeSeriesSource)

                    intersectingTPs = []
                    tssExtent = tss.spatialExtent()
                    for TP in TemporalProfiles:
                        assert isinstance(TP, TemporalProfile)
                        if tssExtent.contains(TP.coordinate().toCrs(tssExtent.crs())):
                            intersectingTPs.append(TP)

                    if len(intersectingTPs) > 0:
                        task = TemporalProfileLoaderTask(tss, intersectingTPs)
                        tasks.append(task)


                    # pickle.loads(doLoaderTask(mock, pickle.dumps([t])))[0]
        if len(tasks) > 0:
            self.loadTemporalProfileTasks(tasks, runAsync=backgroundProcess)

        else:
            if DEBUG:
                print('Data for geometries already loaded')

    def loadTemporalProfileTasks(self, tasks:typing.Iterable[TemporalProfileLoaderTask], runAsync=True)->typing.List[TemporalProfile]:
        """
        Loads data into TemporalProfiles
        :param tasks:
        :param runAsync:
        :return: [list-of-updated TemporalProfiles], empty if runAsyn
        """
        dump = pickle.dumps(tasks)
        if runAsync:
            qgsTask = QgsTask.fromFunction('Load Profiles', doLoadTemporalProfileTasks, dump,
                                           on_finished=self.onPixelLoaded)
        else:
            qgsTask = TaskMock()

        tid = id(qgsTask)
        qgsTask.progressChanged.connect(self.onLoadingProgressChanged)
        qgsTask.taskCompleted.connect(lambda *args, tid=tid: self.onRemoveTask(tid))
        qgsTask.taskTerminated.connect(lambda *args, tid=tid: self.onRemoveTask(tid))
        self.mTasks[tid] = qgsTask

        if runAsync:
            tm = QgsApplication.taskManager()
            assert isinstance(tm, QgsTaskManager)
            tm.addTask(qgsTask, 1000)
            return []
        else:
            return self.onPixelLoaded(qgsTask, doLoadTemporalProfileTasks(qgsTask, dump))

    def onRemoveTask(self, tid):
        if tid in self.mTasks.keys():
            del self.mTasks[tid]

    @QtCore.pyqtSlot()
    def onDataUpdate(self):

        # self.mTemporalProfileLayer.prune()

        for plotSetting in self.plotSettingsModel2D:
            assert isinstance(plotSetting, TemporalProfile2DPlotStyle)
            tp = plotSetting.temporalProfile()
            for pdi in plotSetting.mPlotItems:
                assert isinstance(pdi, TemporalProfilePlotDataItem)
                pdi.updateDataAndStyle()
            if isinstance(tp, TemporalProfile) and plotSetting.temporalProfile().updated():
                plotSetting.temporalProfile().resetUpdatedFlag()


        for i in self.plot2D.plotItem.dataItems:
            i.updateItems()

        notInit = [0, 1] == self.plot2D.plotItem.getAxis('bottom').range
        if notInit:
            x0 = x1 = None
            for plotSetting in self.plotSettingsModel2D:
                assert isinstance(plotSetting, TemporalProfile2DPlotStyle)
                for pdi in plotSetting.mPlotItems:
                    assert isinstance(pdi, TemporalProfilePlotDataItem)
                    if pdi.xData.ndim == 0 or pdi.xData.shape[0] == 0:
                        continue
                    if x0 is None:
                        x0 = pdi.xData.min()
                        x1 = pdi.xData.max()
                    else:
                        x0 = min(pdi.xData.min(), x0)
                        x1 = max(pdi.xData.max(), x1)

            if x0 is not None:
                self.plot2D.plotItem.setXRange(x0, x1)

                # self.plot2D.xAxisInitialized = True

    @QtCore.pyqtSlot()
    def updatePlot3D(self):
        if ENABLE_OPENGL and OPENGL_AVAILABLE:

            from eotimeseriesviewer.temporalprofiles3dGL import ViewWidget3D
            assert isinstance(self.plot3D, ViewWidget3D)

            # 1. ensure that data from all bands will be loaded
            #    new loaded values will be shown in the next updatePlot3D call
            coordinates = []
            allPlotItems = []
            for plotStyle3D in self.plotSettingsModel3D:
                assert isinstance(plotStyle3D, TemporalProfile3DPlotStyle)
                if plotStyle3D.isPlotable():
                    coordinates.append(plotStyle3D.temporalProfile().coordinate())

            if len(coordinates) > 0:
                self.loadCoordinate(coordinates, mode='all')

            toRemove = [item for item in self.plot3D.items if item not in allPlotItems]
            self.plot3D.removeItems(toRemove)
            toAdd = [item for item in allPlotItems if item not in self.plot3D.items]
            self.plot3D.addItems(toAdd)


            """
            # 3. add new plot items
            plotItems = []
            for plotStyle3D in self.plotSettingsModel3D:
                assert isinstance(plotStyle3D, TemporalProfile3DPlotStyle)
                if plotStyle3D.isPlotable():
                    items = plotStyle3D.createPlotItem(None)
                    plotItems.extend(items)

            self.plot3D.addItems(plotItems)
            self.plot3D.updateDataRanges()
            self.plot3D.resetScaling()
            """

    @QtCore.pyqtSlot()
    def updatePlot2D(self):
        if isinstance(self.plotSettingsModel2D, PlotSettingsModel2D):

            locations = set()
            for plotStyle in self.plotSettingsModel2D:
                assert isinstance(plotStyle, TemporalProfile2DPlotStyle)
                if plotStyle.isPlotable():
                    locations.add(plotStyle.temporalProfile().coordinate())

                    for pdi in plotStyle.mPlotItems:
                        assert isinstance(pdi, TemporalProfilePlotDataItem)
                        pdi.updateDataAndStyle()

            #2. load pixel data
            self.loadCoordinate(list(locations))

            # https://github.com/pyqtgraph/pyqtgraph/blob/5195d9dd6308caee87e043e859e7e553b9887453/examples/customPlot.py
            return


def examplePixelLoader():

    # prepare QGIS environment
    if sys.platform == 'darwin':
        PATH_QGS = r'/Applications/QGIS.app/Contents/MacOS'
        os.environ['GDAL_DATA'] = r'/usr/local/Cellar/gdal/1.11.3_1/share'
    else:
        # assume OSGeo4W startup
        PATH_QGS = os.environ['QGIS_PREFIX_PATH']
    assert os.path.exists(PATH_QGS)

    qgsApp = QgsApplication([], True)
    QApplication.addLibraryPath(r'/Applications/QGIS.app/Contents/PlugIns')
    QApplication.addLibraryPath(r'/Applications/QGIS.app/Contents/PlugIns/qgis')
    qgsApp.setPrefixPath(PATH_QGS, True)
    qgsApp.initQgis()


    gb = QGroupBox()
    gb.setTitle('Sandbox')

    PL = PixelLoader()


    if False:
        files = ['observationcloud/testdata/2014-07-26_LC82270652014207LGN00_BOA.bsq',
                 'observationcloud/testdata/2014-08-03_LE72270652014215CUB00_BOA.bsq'
                 ]
    else:
        from eotimeseriesviewer.utils import file_search
        searchDir = r'H:\LandsatData\Landsat_NovoProgresso'
        files = list(file_search(searchDir, '*227065*band4.img', recursive=True))
        #files = files[0:3]

    lyr = QgsRasterLayer(files[0])
    coord = lyr.extent().center()
    crs = lyr.crs()

    l = QVBoxLayout()

    btnStart = QPushButton()
    btnStop = QPushButton()
    prog = QProgressBar()
    tboxResults = QPlainTextEdit()
    tboxResults.setMaximumHeight(300)
    tboxThreads = QPlainTextEdit()
    tboxThreads.setMaximumHeight(200)
    label = QLabel()
    label.setText('Progress')

    def showProgress(n,m,md):
        prog.setMinimum(0)
        prog.setMaximum(m)
        prog.setValue(n)

        info = []
        for k, v in md.items():
            info.append('{} = {}'.format(k,str(v)))
        tboxResults.setPlainText('\n'.join(info))
        #tboxThreads.setPlainText(PL.threadInfo())
        qgsApp.processEvents()

    PL.sigPixelLoaded.connect(showProgress)
    btnStart.setText('Start loading')
    btnStart.clicked.connect(lambda : PL.startLoading(files, coord, crs))
    btnStop.setText('Cancel')
    btnStop.clicked.connect(lambda: PL.cancelLoading())
    lh = QHBoxLayout()
    lh.addWidget(btnStart)
    lh.addWidget(btnStop)
    l.addLayout(lh)
    l.addWidget(prog)
    l.addWidget(tboxThreads)
    l.addWidget(tboxResults)

    gb.setLayout(l)
    gb.show()
    #rs.setBackgroundStyle('background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #222, stop:1 #333);')
    #rs.handle.setStyleSheet('background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #282, stop:1 #393);')
    qgsApp.exec_()
    qgsApp.exitQgis()
