From 278db6e6a0c8a57e29117d194f44cb6b0d2df684 Mon Sep 17 00:00:00 2001
From: "benjamin.jakimow@geo.hu-berlin.de" <q8DTkxUg-BB>
Date: Thu, 9 Feb 2017 17:19:13 +0100
Subject: [PATCH] various changes to handel profile visualization DateAxis
 ProfilePoint test cases

---
 timeseriesviewer/pixelloader.py          |  326 ------
 timeseriesviewer/profilevisualization.py | 1143 ++++++++++++++++++++++
 2 files changed, 1143 insertions(+), 326 deletions(-)
 delete mode 100644 timeseriesviewer/pixelloader.py
 create mode 100644 timeseriesviewer/profilevisualization.py

diff --git a/timeseriesviewer/pixelloader.py b/timeseriesviewer/pixelloader.py
deleted file mode 100644
index a2b95c13..00000000
--- a/timeseriesviewer/pixelloader.py
+++ /dev/null
@@ -1,326 +0,0 @@
-import os
-import sys
-
-from qgis.gui import *
-from qgis.core import *
-from PyQt4.QtCore import *
-from PyQt4.QtXml import *
-from PyQt4.QtGui import *
-
-
-from osgeo import gdal, gdal_array
-import numpy as np
-
-
-
-class PixelLoadWorker(QObject):
-    #qRegisterMetaType
-    sigPixelLoaded = pyqtSignal(dict)
-
-    sigWorkStarted = pyqtSignal(int)
-
-    sigWorkFinished = pyqtSignal()
-
-    def __init__(self, files, parent=None):
-        super(PixelLoadWorker, self).__init__(parent)
-        assert isinstance(files, list)
-        self.files = files
-
-    def info(self):
-        return 'recent file {}'.format(self.recentFile)
-
-    @pyqtSlot(str, str)
-    def doWork(self, theGeometryWkt, theCrsDefinition):
-
-        g = QgsGeometry.fromWkt(theGeometryWkt)
-        if g.wkbType() == QgsWKBTypes.Point:
-            g = g.asPoint()
-        elif g.wkbType() == QgsWKBTypes.Polygon:
-            g = g.asPolygon()
-        else:
-            raise NotImplementedError()
-
-
-
-        crs = QgsCoordinateReferenceSystem(theCrsDefinition)
-        assert isinstance(crs, QgsCoordinateReferenceSystem)
-        paths = self.files
-        self.sigWorkStarted.emit(len(paths))
-
-        for i, path in enumerate(paths):
-            self.recentFile = path
-
-            lyr = QgsRasterLayer(path)
-            dp = lyr.dataProvider()
-
-            trans = QgsCoordinateTransform(crs, dp.crs())
-            #todo: add with QGIS 3.0
-            #if not trans.isValid():
-            #    self.sigPixelLoaded.emit({})
-            #    continue
-
-            try:
-                geo = trans.transform(g)
-            except(QgsCsException):
-                self.sigPixelLoaded.emit({})
-                continue
-
-            ns = dp.xSize()  # ns = number of samples = number of image columns
-            nl = dp.ySize()  # nl = number of lines
-            ex = dp.extent()
-
-            xres = ex.width() / ns  # pixel size
-            yres = ex.height() / nl
-
-            if not ex.contains(geo):
-                self.sigPixelLoaded.emit({})
-                continue
-
-            def geo2px(x, y):
-                x = int(np.floor((x - ex.xMinimum()) / xres).astype(int))
-                y = int(np.floor((ex.yMaximum() - y) / yres).astype(int))
-                return x, y
-
-            if isinstance(geo, QgsPoint):
-                px_x, px_y = geo2px(geo.x(), geo.y())
-
-                size_x = 1
-                size_y = 1
-                UL = geo
-            elif isinstance(geo, QgsRectangle):
-
-                px_x, px_y = geo2px(geo.xMinimum(), geo.yMaximum())
-                px_x2, px_y2 = geo2px(geo.xMaximum(), geo.yMinimum())
-                size_x = px_x2 - px_x
-                size_y = px_y2 - px_y
-                UL = QgsPoint(geo.xMinimum(), geo.yMaximum())
-
-            ds = gdal.Open(path)
-            if ds is None:
-                self.sigPixelLoaded.emit({})
-                continue
-            nb = ds.RasterCount
-            values = gdal_array.DatasetReadAsArray(ds, px_x, px_y, win_xsize=size_x, win_ysize=size_y)
-
-            nodata = [ds.GetRasterBand(b+1).GetNoDataValue() for b in range(nb)]
-
-
-            md = dict()
-            md['_worker_'] = self.objectName()
-            md['_thread_'] = QThread.currentThread().objectName()
-            md['_wkt_'] = theGeometryWkt
-            md['path'] = path
-            md['xres'] = xres
-            md['yres'] = xres
-            md['geo_ul_x'] = UL.x()
-            md['geo_ul_y'] = UL.y()
-            md['px_ul_x'] = px_x
-            md['px_ul_y'] = px_y
-            md['values'] = values
-            md['nodata'] = nodata
-
-            self.sigPixelLoaded.emit(md)
-        self.recentFile = None
-        self.sigWorkFinished.emit()
-
-
-
-
-class PixelLoader(QObject):
-
-
-    sigPixelLoaded = pyqtSignal(int, int, dict)
-    sigLoadingDone = pyqtSignal()
-    sigLoadingFinished = pyqtSignal()
-    sigLoadingCanceled = pyqtSignal()
-    sigLoadCoordinate = pyqtSignal(str, str)
-
-    def __init__(self, *args, **kwds):
-        super(PixelLoader, self).__init__(*args, **kwds)
-
-        self.nThreads = 1
-        self.nMax = 0
-        self.nDone = 0
-        self.threadsAndWorkers = []
-
-    @QtCore.pyqtSlot(dict)
-    def onPixelLoaded(self, d):
-        self.nDone += 1
-        self.sigPixelLoaded.emit(self.nDone, self.nMax, d)
-
-        if self.nDone == self.nMax:
-            self.sigLoadingFinished.emit()
-
-
-    def setNumberOfThreads(self, nThreads):
-        assert nThreads >= 1
-        self.nThreads = nThreads
-
-    def threadInfo(self):
-        info = []
-        info.append('done: {}/{}'.format(self.nDone, self.nMax))
-        for i, t in enumerate(self.threads):
-            info.append('{}: {}'.format(i, t.info() ))
-
-        return '\n'.join(info)
-
-    def cancelLoading(self):
-        for t in self.threadsAndWorkers:
-            thread, worker = t
-            thread.quit()
-        del self.threadsAndWorkers[:]
-
-        for t,w in self.workerThreads.items():
-            w.stop()
-            t.quit()
-            t.deleteLater()
-            self.workerThreads.pop(t)
-        self.nMax = self.nDone = None
-        self.sigLoadingCanceled.emit()
-
-    def removeFinishedThreads(self):
-
-        toRemove = []
-        for i, t in enumerate(self.threadsAndWorkers):
-            thread, worker = t
-            if thread.isFinished():
-                thread.quit()
-                toRemove.append(t)
-        for t in toRemove:
-            self.threadsAndWorkers.remove(t)
-
-    def startLoading(self, pathList, theGeometry, crs):
-        assert isinstance(pathList, list)
-
-        if isinstance(theGeometry, QgsPoint):
-            theGeometry = QgsPointV2(theGeometry)
-        elif isinstance(theGeometry, QgsRectangle):
-            theGeometry = QgsPolygonV2(theGeometry.asWktPolygon())
-        assert type(theGeometry) in [QgsPointV2, QgsPolygonV2]
-
-
-        wkt = theGeometry.asWkt(50)
-
-
-        l = len(pathList)
-        self.nMax = l
-        self.nFailed = 0
-        self.nDone = 0
-
-        nThreads = self.nThreads
-        filesPerThread = int(np.ceil(float(l) / nThreads))
-
-        if True:
-            worker = PixelLoadWorker(pathList[0:1])
-            worker.doWork(wkt, str(crs.authid()))
-
-        n = 0
-        files = pathList[:]
-        while len(files) > 0:
-            n += 1
-
-            i = min([filesPerThread, len(files)])
-            thread = QThread()
-            thread.setObjectName('Thread {}'.format(n))
-            thread.finished.connect(self.removeFinishedThreads)
-            thread.terminated.connect(self.removeFinishedThreads)
-
-            worker = PixelLoadWorker(files[0:i])
-            worker.setObjectName('W {}'.format(n))
-            worker.moveToThread(thread)
-            worker.sigPixelLoaded.connect(self.onPixelLoaded)
-            worker.sigWorkFinished.connect(thread.quit)
-            self.sigLoadCoordinate.connect(worker.doWork)
-            thread.start()
-            self.threadsAndWorkers.append((thread, worker))
-            del files[0:i]
-
-        #stark the workers
-
-        self.sigLoadCoordinate.emit(theGeometry.asWkt(50), str(crs.authid()))
-
-
-
-
-if __name__ == '__main__':
-
-    # 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()
-    PL.setNumberOfThreads(1)
-
-    if False:
-        files = ['observationcloud/testdata/2014-07-26_LC82270652014207LGN00_BOA.bsq',
-                 'observationcloud/testdata/2014-08-03_LE72270652014215CUB00_BOA.bsq'
-                 ]
-    else:
-        from utils import file_search
-        searchDir = r'H:\LandsatData\Landsat_NovoProgresso'
-        files = 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()
-
diff --git a/timeseriesviewer/profilevisualization.py b/timeseriesviewer/profilevisualization.py
new file mode 100644
index 00000000..0b03f7b1
--- /dev/null
+++ b/timeseriesviewer/profilevisualization.py
@@ -0,0 +1,1143 @@
+import os
+import sys
+
+from qgis.gui import *
+from qgis.core import *
+from PyQt4.QtCore import *
+from PyQt4.QtXml import *
+from PyQt4.QtGui import *
+
+from timeseriesviewer import jp, SETTINGS
+from timeseriesviewer.timeseries import *
+from timeseriesviewer.ui.docks import TsvDockWidgetBase, load
+import pyqtgraph as pg
+from osgeo import gdal, gdal_array
+import numpy as np
+
+
+class DateAxis(pg.AxisItem):
+
+    def __init__(self, *args, **kwds):
+        super(DateAxis, self).__init__(*args, **kwds)
+
+    def logTickStrings(self, values, scale, spacing):
+        s = ""
+
+    def tickStrings(self, values, scale, spacing):
+        strns = []
+        rng = max(values)-min(values)
+        #if rng < 120:
+        #    return pg.AxisItem.tickStrings(self, values, scale, spacing)
+        if rng < 3600*24:
+            string = '%H:%M:%S'
+            label1 = '%b %d -'
+            label2 = ' %b %d, %Y'
+        elif rng >= 3600*24 and rng < 3600*24*30:
+            string = '%d'
+            label1 = '%b - '
+            label2 = '%b, %Y'
+        elif rng >= 3600*24*30 and rng < 3600*24*30*24:
+            string = '%b'
+            label1 = '%Y -'
+            label2 = ' %Y'
+        elif rng >=3600*24*30*24:
+            string = '%Y'
+            label1 = ''
+            label2 = ''
+        for x in values:
+            try:
+                strns.append(time.strftime(string, time.localtime(x)))
+            except ValueError:  ## Windows can't handle dates before 1970
+                strns.append('')
+        try:
+            label = time.strftime(label1, time.localtime(min(values)))+time.strftime(label2, time.localtime(max(values)))
+        except ValueError:
+            label = ''
+        #self.setLabel(text=label)
+        return strns
+
+
+
+class PixelLoadWorker(QObject):
+    #qRegisterMetaType
+    sigPixelLoaded = pyqtSignal(dict)
+
+    sigWorkStarted = pyqtSignal(int)
+
+    sigWorkFinished = pyqtSignal()
+
+    def __init__(self, files, parent=None):
+        super(PixelLoadWorker, self).__init__(parent)
+        assert isinstance(files, list)
+        self.files = files
+
+    def info(self):
+        return 'recent file {}'.format(self.recentFile)
+
+    @pyqtSlot(str, str)
+    def doWork(self, theGeometryWkt, theCrsDefinition):
+
+        g = QgsGeometry.fromWkt(theGeometryWkt)
+        if g.wkbType() == QgsWKBTypes.Point:
+            g = g.asPoint()
+        elif g.wkbType() == QgsWKBTypes.Polygon:
+            g = g.asPolygon()
+        else:
+            raise NotImplementedError()
+
+
+
+        crs = QgsCoordinateReferenceSystem(theCrsDefinition)
+        assert isinstance(crs, QgsCoordinateReferenceSystem)
+        paths = self.files
+        self.sigWorkStarted.emit(len(paths))
+
+        for i, path in enumerate(paths):
+            self.recentFile = path
+
+            lyr = QgsRasterLayer(path)
+            dp = lyr.dataProvider()
+
+            trans = QgsCoordinateTransform(crs, dp.crs())
+            #todo: add with QGIS 3.0
+            #if not trans.isValid():
+            #    self.sigPixelLoaded.emit({})
+            #    continue
+
+            try:
+                geo = trans.transform(g)
+            except(QgsCsException):
+                self.sigPixelLoaded.emit({})
+                continue
+
+            ns = dp.xSize()  # ns = number of samples = number of image columns
+            nl = dp.ySize()  # nl = number of lines
+            ex = dp.extent()
+
+            xres = ex.width() / ns  # pixel size
+            yres = ex.height() / nl
+
+            if not ex.contains(geo):
+                self.sigPixelLoaded.emit({})
+                continue
+
+            def geo2px(x, y):
+                x = int(np.floor((x - ex.xMinimum()) / xres).astype(int))
+                y = int(np.floor((ex.yMaximum() - y) / yres).astype(int))
+                return x, y
+
+            if isinstance(geo, QgsPoint):
+                px_x, px_y = geo2px(geo.x(), geo.y())
+
+                size_x = 1
+                size_y = 1
+                UL = geo
+            elif isinstance(geo, QgsRectangle):
+
+                px_x, px_y = geo2px(geo.xMinimum(), geo.yMaximum())
+                px_x2, px_y2 = geo2px(geo.xMaximum(), geo.yMinimum())
+                size_x = px_x2 - px_x
+                size_y = px_y2 - px_y
+                UL = QgsPoint(geo.xMinimum(), geo.yMaximum())
+
+            ds = gdal.Open(path)
+            if ds is None:
+                self.sigPixelLoaded.emit({})
+                continue
+            nb = ds.RasterCount
+            values = gdal_array.DatasetReadAsArray(ds, px_x, px_y, win_xsize=size_x, win_ysize=size_y)
+
+            nodata = [ds.GetRasterBand(b+1).GetNoDataValue() for b in range(nb)]
+
+
+            md = dict()
+            md['_worker_'] = self.objectName()
+            md['_thread_'] = QThread.currentThread().objectName()
+            md['_wkt_'] = theGeometryWkt
+            md['path'] = path
+            md['xres'] = xres
+            md['yres'] = xres
+            md['geo_ul_x'] = UL.x()
+            md['geo_ul_y'] = UL.y()
+            md['px_ul_x'] = px_x
+            md['px_ul_y'] = px_y
+            md['values'] = values
+            md['nodata'] = nodata
+
+            self.sigPixelLoaded.emit(md)
+        self.recentFile = None
+        self.sigWorkFinished.emit()
+
+
+
+
+class PixelLoader(QObject):
+
+
+    sigPixelLoaded = pyqtSignal(int, int, dict)
+    sigLoadingStarted = pyqtSignal()
+    sigLoadingDone = pyqtSignal()
+    sigLoadingFinished = pyqtSignal()
+    sigLoadingCanceled = pyqtSignal()
+    _sigLoadCoordinate = pyqtSignal(str, str)
+
+    def __init__(self, *args, **kwds):
+        super(PixelLoader, self).__init__(*args, **kwds)
+
+        self.nThreads = 1
+        self.nMax = 0
+        self.nDone = 0
+        self.threadsAndWorkers = []
+
+    @QtCore.pyqtSlot(dict)
+    def onPixelLoaded(self, d):
+        self.nDone += 1
+        self.sigPixelLoaded.emit(self.nDone, self.nMax, d)
+
+        if self.nDone == self.nMax:
+            self.sigLoadingFinished.emit()
+
+
+    def setNumberOfThreads(self, nThreads):
+        assert nThreads >= 1
+        self.nThreads = nThreads
+
+    def threadInfo(self):
+        info = []
+        info.append('done: {}/{}'.format(self.nDone, self.nMax))
+        for i, t in enumerate(self.threads):
+            info.append('{}: {}'.format(i, t.info() ))
+
+        return '\n'.join(info)
+
+    def cancelLoading(self):
+        for t in self.threadsAndWorkers:
+            thread, worker = t
+            thread.quit()
+        del self.threadsAndWorkers[:]
+
+        for t,w in self.workerThreads.items():
+            w.stop()
+            t.quit()
+            t.deleteLater()
+            self.workerThreads.pop(t)
+        self.nMax = self.nDone = None
+        self.sigLoadingCanceled.emit()
+
+    def removeFinishedThreads(self):
+
+        toRemove = []
+        for i, t in enumerate(self.threadsAndWorkers):
+            thread, worker = t
+            if thread.isFinished():
+                thread.quit()
+                toRemove.append(t)
+        for t in toRemove:
+            self.threadsAndWorkers.remove(t)
+
+    def startLoading(self, pathList, theGeometry, crs):
+        self.removeFinishedThreads()
+        self.sigLoadingStarted.emit()
+
+        assert isinstance(pathList, list)
+
+        if isinstance(theGeometry, QgsPoint):
+            theGeometry = QgsPointV2(theGeometry)
+        elif isinstance(theGeometry, QgsRectangle):
+            theGeometry = QgsPolygonV2(theGeometry.asWktPolygon())
+        assert type(theGeometry) in [QgsPointV2, QgsPolygonV2]
+
+
+        wkt = theGeometry.asWkt(50)
+
+
+        l = len(pathList)
+        self.nMax = l
+        self.nFailed = 0
+        self.nDone = 0
+
+        nThreads = self.nThreads
+        filesPerThread = int(np.ceil(float(l) / nThreads))
+
+        if True:
+            worker = PixelLoadWorker(pathList[0:1])
+            worker.doWork(wkt, str(crs.authid()))
+
+        n = 0
+        files = pathList[:]
+
+        while len(files) > 0:
+            n += 1
+
+            i = min([filesPerThread, len(files)])
+            thread = QThread()
+            thread.setObjectName('Thread {}'.format(n))
+            thread.finished.connect(self.removeFinishedThreads)
+            thread.terminated.connect(self.removeFinishedThreads)
+
+            worker = PixelLoadWorker(files[0:i])
+            worker.setObjectName('W {}'.format(n))
+            worker.moveToThread(thread)
+            worker.sigPixelLoaded.connect(self.onPixelLoaded)
+            worker.sigWorkFinished.connect(thread.quit)
+            self._sigLoadCoordinate.connect(worker.doWork)
+            thread.start()
+            self.threadsAndWorkers.append((thread, worker))
+            del files[0:i]
+
+        #stark the workers
+
+        self._sigLoadCoordinate.emit(theGeometry.asWkt(50), str(crs.authid()))
+
+
+class ProfilePoint(pg.GraphicsObject):
+    """
+    This class draws a rectangular area. Right-clicking inside the area will
+    raise a custom context menu which also includes the context menus of
+    its parents.
+    """
+
+    def __init__(self, name):
+        self.name = name
+        self.pen = pg.mkPen('r')
+
+        # menu creation is deferred because it is expensive and often
+        # the user will never see the menu anyway.
+        self.menu = None
+
+        # note that the use of super() is often avoided because Qt does not
+        # allow to inherit from multiple QObject subclasses.
+        pg.GraphicsObject.__init__(self)
+
+
+        # All graphics items must have paint() and boundingRect() defined.
+
+    def boundingRect(self):
+        return QtCore.QRectF(0, 0, 10, 10)
+
+    def paint(self, p, *args):
+        p.setPen(self.pen)
+        p.drawRect(self.boundingRect())
+
+    # 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 = QtGui.QMenu()
+            self.menu.setTitle(self.name + " options..")
+
+            green = QtGui.QAction("Turn green", self.menu)
+            green.triggered.connect(self.setGreen)
+            self.menu.addAction(green)
+            self.menu.green = green
+
+            blue = QtGui.QAction("Turn blue", self.menu)
+            blue.triggered.connect(self.setBlue)
+            self.menu.addAction(blue)
+            self.menu.green = blue
+
+            alpha = QtGui.QWidgetAction(self.menu)
+            alphaSlider = QtGui.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 PlotSettingsWidgetDelegate(QStyledItemDelegate):
+
+    def __init__(self, tableView, parent=None):
+
+        super(PlotSettingsWidgetDelegate, self).__init__(parent=parent)
+        self._preferedSize = QgsFieldExpressionWidget().sizeHint()
+        self.tableView = tableView
+
+    def getColumnName(self, index):
+        assert index.isValid()
+        assert isinstance(index.model(), PlotSettingsModel)
+        return index.model().columnames[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)
+        if cname == 'y-value':
+            w = QgsFieldExpressionWidget(parent)
+            sv = self.tableView.model().data(index, Qt.UserRole)
+            w.setLayer(sv.memLyr)
+            w.setExpressionDialogTitle('Values sensor {}'.format(sv.sensor.sensorName))
+            w.setToolTip('Set values shown for sensor {}'.format(sv.sensor.sensorName))
+            w.fieldChanged.connect(lambda : self.commitExpression(w, w.expression()))
+        elif cname == 'style':
+            sv = self.tableView.model().data(index, Qt.UserRole)
+            sn = sv.sensor.sensorName
+            w = QgsColorButton(parent, 'Point color {}'.format(sn))
+            w.setColor(QColor(index.data()))
+            w.colorChanged.connect(lambda: self.commitData.emit(w))
+        else:
+            raise NotImplementedError()
+        return w
+
+    def commitExpression(self, w, expression):
+
+        assert expression == w.expression()
+        assert w.isExpressionValid(expression) == w.isValidExpression()
+
+        if w.isValidExpression():
+            self.commitData.emit(w)
+        else:
+            print(('Delegate commit failed',w.asExpression()))
+
+
+    def setEditorData(self, editor, index):
+        cname = self.getColumnName(index)
+        if cname == 'y-value':
+            lastExpr = index.model().data(index, Qt.DisplayRole)
+            assert isinstance(editor, QgsFieldExpressionWidget)
+            #print(('Set expr2editor', lastExpr))
+            editor.setProperty('lastexpr', lastExpr)
+            editor.setField(lastExpr)
+        elif cname == 'style':
+            lastColor = index.data()
+            assert isinstance(editor, QgsColorButton)
+            assert isinstance(lastColor, QColor)
+            editor.setColor(QColor(lastColor))
+        else:
+            raise NotImplementedError()
+
+    def setModelData(self, w, model, index):
+        cname = self.getColumnName(index)
+        if cname == 'y-value':
+            assert isinstance(w, QgsFieldExpressionWidget)
+            assert w.isValidExpression()
+            expr = w.expression()
+            exprLast = model.data(index, Qt.DisplayRole)
+
+            if expr != exprLast:
+                model.setData(index, w.expression(), Qt.DisplayRole)
+        elif cname == 'style':
+            assert isinstance(w, QgsColorButton)
+            if index.data() != w.color():
+                model.setData(index, w.color(), Qt.DisplayRole)
+        else:
+            raise NotImplementedError()
+
+class PixelCollection(QObject):
+    """
+    Object to store pixel data returned by PixelLoader
+    """
+
+    sigSensorAdded = pyqtSignal(SensorInstrument)
+    sigSensorRemoved = pyqtSignal(SensorInstrument)
+    sigPixelAdded = pyqtSignal()
+    sigPixelRemoved = pyqtSignal()
+
+
+
+    def __init__(self, timeSeries):
+        assert isinstance(timeSeries, TimeSeries)
+        super(PixelCollection, self).__init__()
+
+        self.TS = timeSeries
+        self.sensors = []
+        self.sensorPxLayers = dict()
+
+
+    def getFieldDefn(self, name, values):
+        if isinstance(values, np.ndarray):
+            # add bands
+            if values.dtype in [np.int8, np.int16, np.int32, np.int64,
+                                np.uint8, np.uint16, np.uint32, np.uint64]:
+                fType = QVariant.Int
+                fTypeName = 'integer'
+            elif values.dtype in [np.float16, np.float32, np.float64]:
+                fType = QVariant.Double
+                fTypeName = 'decimal'
+        else:
+            raise NotImplementedError()
+
+        return QgsField(name, fType, fTypeName)
+
+    def setFeatureAttribute(self, feature, name, value):
+        assert isinstance(feature, QgsFeature)
+        assert isinstance(name, str)
+        i = feature.fieldNameIndex(name)
+        assert i >= 0
+        field = feature.fields()[i]
+        if field.isNumeric():
+            if field.type() == QVariant.Int:
+                value = int(value)
+            elif field.type() == QVariant.Double:
+                value = float(value)
+            else:
+                raise NotImplementedError()
+        feature.setAttribute(i, value)
+
+
+    def addPixel(self, d):
+        assert isinstance(d, dict)
+        if len(d) > 0:
+            tsd = self.TS.getTSD(d['path'])
+            values = d['values']
+            nodata = np.asarray(d['nodata'])
+
+            nb, nl, ns = values.shape
+            assert nb >= 1
+
+            assert isinstance(tsd, TimeSeriesDatum)
+            if tsd.sensor not in self.sensorPxLayers.keys():
+                #create new temp layer
+                mem = QgsVectorLayer('point', 'Pixels_sensor_'+tsd.sensor.sensorName, 'memory')
+
+                self.sensorPxLayers[tsd.sensor] = mem
+                assert mem.startEditing()
+
+                #standard field names, types, etc.
+                fieldDefs = [('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'),
+                             ]
+                for fieldDef in fieldDefs:
+                    field = QgsField(fieldDef[0], fieldDef[1], fieldDef[2])
+                    mem.addAttribute(field)
+
+                for i in range(nb):
+                    fName = 'b{}'.format(i+1)
+                    mem.addAttribute(self.getFieldDefn(fName, values))
+                assert mem.commitChanges()
+
+
+
+                self.sigSensorAdded.emit(tsd.sensor)
+                s = ""
+
+            mem = self.sensorPxLayers[tsd.sensor]
+
+
+            #insert each single pixel, line by line
+            xres = d['xres']
+            yres = d['yres']
+            geo_ul_x = d['geo_ul_x']
+            geo_ul_y = d['geo_ul_y']
+            px_ul_x = d['px_ul_x']
+            px_ul_y = d['px_ul_y']
+
+            doy = tsd.doy
+            for i in range(ns):
+                geo_x = geo_ul_x + xres * i
+                px_x = px_ul_x + i
+                for j in range(nl):
+                    geo_y = geo_ul_y + yres * j
+                    px_y = px_ul_y + j
+                    profile = values[:,j,i]
+
+                    if np.any(np.any(profile == nodata)):
+                        continue
+
+
+                    geometry = QgsPointV2(geo_x, geo_y)
+
+                    feature = QgsFeature(mem.fields())
+
+                    #fnames = [f.name() for f in mem.fields()]
+
+                    feature.setGeometry(QgsGeometry(geometry))
+                    feature.setAttribute('date', str(tsd.date))
+                    feature.setAttribute('doy', doy)
+                    feature.setAttribute('geo_x', geo_x)
+                    feature.setAttribute('geo_y', geo_y)
+                    feature.setAttribute('px_x', px_x)
+                    feature.setAttribute('px_y', px_y)
+                    for b in range(nb):
+                        name ='b{}'.format(b+1)
+                        self.setFeatureAttribute(feature, name, profile[b])
+                    mem.startEditing()
+                    assert mem.addFeature(feature)
+                    assert mem.commitChanges()
+
+            #each pixel is a new feature
+            self.sigPixelAdded.emit()
+
+        pass
+
+
+
+    def clear(self):
+        for sensor in self.sensorPxLayers.keys():
+            self.sigSensorRemoved.emit(sensor)
+        self.sigPixelRemoved.emit()
+
+    def dateValues(self, sensor, expression):
+        if sensor not in self.sensorPxLayers.keys():
+            return []
+        mem = self.sensorPxLayers[sensor]
+        dp = mem.dataProvider()
+        exp = QgsExpression(expression)
+        exp.prepare(dp.fields())
+
+        possibleTsds = self.TS.getTSDs(sensorOfInterest=sensor)
+
+
+        tsds = []
+        values =  []
+
+        if exp.isValid():
+            mem.selectAll()
+            for feature in mem.selectedFeatures():
+                date = np.datetime64(feature.attribute('date'))
+                y = exp.evaluatePrepared(feature)
+                if y is not None:
+                    tsd = next(tsd for tsd in possibleTsds if tsd.date == date)
+                    tsds.append(tsd)
+                    values.append(y)
+                else:
+                    print(exp.evalErrorString())
+
+        return tsds, values
+
+class SensorPlotSettings(object):
+    def __init__(self, sensor, memoryLyr):
+
+        assert isinstance(sensor, SensorInstrument)
+        assert isinstance(memoryLyr, QgsVectorLayer)
+        self.sensor = sensor
+        self.expression = u'"b1"'
+        self.color = QColor('green')
+        self.isVisible = True
+        self.memLyr = memoryLyr
+
+
+class PlotSettingsModel(QAbstractTableModel):
+
+    sigSensorAdded = pyqtSignal(SensorPlotSettings)
+    sigVisiblityChanged = pyqtSignal(SensorPlotSettings)
+
+    columnames = ['sensor','nb','style','y-value']
+    def __init__(self, pxCollection, parent=None, *args):
+
+        #assert isinstance(tableView, QTableView)
+
+        super(PlotSettingsModel, self).__init__()
+
+        self.items = []
+
+        self.sortColumnIndex = 0
+        self.sortOrder = Qt.AscendingOrder
+        self.pxCollection = pxCollection
+        self.pxCollection.sigSensorAdded.connect(self.addSensor)
+        #self.pxCollection.sigSensorRemoved.connect(self.removeSensor)
+
+        self.sort(0, Qt.AscendingOrder)
+        s = ""
+        self.dataChanged.connect(self.signaler)
+
+    def testSlot(self, *args):
+        print('TESTSLOT')
+        s = ""
+
+    def signaler(self, idxUL, idxLR):
+        if idxUL.isValid():
+            cname = self.columnames[idxUL.column()]
+            if cname in ['style', 'sensor','y-value']:
+                sensorView = self.getSensorFromIndex(idxUL)
+                self.sigVisiblityChanged.emit(sensorView)
+
+
+    def addSensor(self, sensor):
+        assert isinstance(sensor, SensorInstrument)
+        assert sensor in self.pxCollection.sensorPxLayers.keys()
+        sensorSettings = SensorPlotSettings(sensor, self.pxCollection.sensorPxLayers[sensor])
+
+        i = len(self.items)
+        idx = self.createIndex(i,i, sensorSettings)
+        self.beginInsertRows(QModelIndex(),i,i)
+        self.items.append(sensorSettings)
+        self.endInsertRows()
+        #self.sort(self.sortColumnIndex, self.sortOrder)
+
+        self.sigSensorAdded.emit(sensorSettings)
+
+
+    def sort(self, col, order):
+        if self.rowCount() == 0:
+            return
+
+
+        colName = self.columnames[col]
+        r = order != Qt.AscendingOrder
+
+        #self.beginMoveRows(idxSrc,
+
+        if colName == 'sensor':
+            self.items.sort(key = lambda sv:sv.sensor.sensorName, reverse=r)
+        elif colName == 'nb':
+            self.items.sort(key=lambda sv: sv.sensor.nb, reverse=r)
+        elif colName == 'y-value':
+            self.items.sort(key=lambda sv: sv.expression, reverse=r)
+        elif colName == 'style':
+            self.items.sort(key=lambda sv: sv.color, reverse=r)
+
+
+
+
+
+    def rowCount(self, parent = QModelIndex()):
+        return len(self.items)
+
+
+    def removeRows(self, row, count , parent=QModelIndex()):
+        self.beginRemoveRows(parent, row, row+count-1)
+        toRemove = self.items[row:row+count]
+        for tsd in toRemove:
+            self.items.remove(tsd)
+        self.endRemoveRows()
+
+    def getIndexFromSensor(self, sensor):
+        sensorViews = [i for i, s in enumerate(self.items) if s.sensor == sensor]
+        assert len(sensorViews) == 1
+        return self.createIndex(sensorViews[0],0)
+
+    def getSensorFromIndex(self, index):
+        if index.isValid():
+            return self.items[index.row()]
+        return None
+
+    def columnCount(self, parent = QModelIndex()):
+        return len(self.columnames)
+
+    def data(self, index, role = Qt.DisplayRole):
+        if role is None or not index.isValid():
+            return None
+
+        value = None
+        columnName = self.columnames[index.column()]
+        sw = self.getSensorFromIndex(index)
+        #print(('data', columnName, role))
+        if role == Qt.DisplayRole:
+            if columnName == 'sensor':
+                value = sw.sensor.sensorName
+            elif columnName == 'nb':
+                value = str(sw.sensor.nb)
+            elif columnName == 'y-value':
+                value = sw.expression
+            elif columnName == 'style':
+                value = QColor(sw.color)
+
+        elif role == Qt.CheckStateRole:
+            if columnName == 'sensor':
+                value = Qt.Checked if sw.isVisible else Qt.Unchecked
+        elif role == Qt.UserRole:
+            value = sw
+        #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.columnames[index.column()]
+
+        result = False
+        sw = self.getSensorFromIndex(index)
+        if role in [Qt.DisplayRole, Qt.EditRole]:
+            if columnName == 'y-value':
+                sw.expression = value
+                result = True
+            elif columnName == 'style':
+                if isinstance(value, QColor):
+                    sw.color = value
+                    result = True
+
+        if role == Qt.CheckStateRole:
+            if columnName == 'sensor':
+                sw.isVisible = value == Qt.Checked
+                result = True
+
+
+        if result:
+            self.dataChanged.emit(index, index)
+
+        return result
+
+    def flags(self, index):
+        if index.isValid():
+            columnName = self.columnames[index.column()]
+            flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable
+            if columnName == 'sensor':
+                flags = flags | Qt.ItemIsUserCheckable
+
+            if columnName in ['y-value']: #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.columnames[col]
+        elif orientation == Qt.Vertical and role == Qt.DisplayRole:
+            return col
+        return None
+
+
+
+class ProfileViewDockUI(TsvDockWidgetBase, load('profileviewdock.ui')):
+    def __init__(self, parent=None):
+        super(ProfileViewDockUI, self).__init__(parent)
+        self.setupUi(self)
+        from timeseriesviewer import OPENGL_AVAILABLE, SETTINGS
+        if OPENGL_AVAILABLE:
+            l = self.page3D.layout()
+            l.removeWidget(self.labelDummy3D)
+            from pyqtgraph.opengl import GLViewWidget
+            self.plotWidget3D = GLViewWidget(self.page3D)
+            l.addWidget(self.plotWidget3D)
+        else:
+            self.plotWidget3D = None
+
+        pi = self.plotWidget2D.plotItem
+        ax = DateAxis(orientation='bottom', showValues=True)
+        pi.layout.addItem(ax, 3,2)
+
+        self.baseTitle = self.windowTitle()
+        self.TS = None
+        self.pxCollection = None
+        self.progressBar.setMinimum(0)
+        self.progressBar.setMaximum(100)
+        self.progressBar.setValue(0)
+        self.progressInfo.setText('')
+        self.pxViewModel2D = None
+        self.pxViewModel3D = None
+        self.pixelLoader = PixelLoader()
+        self.pixelLoader.sigPixelLoaded.connect(self.onPixelLoaded)
+        self.pixelCollection = None
+        self.tableView2DBands.horizontalHeader().setResizeMode(QHeaderView.ResizeToContents)
+        self.tableView2DBands.setSortingEnabled(True)
+        self.btnRefresh2D.setDefaultAction(self.actionRefresh2D)
+
+    def connectTimeSeries(self, TS):
+
+        assert isinstance(TS, TimeSeries)
+        self.TS = TS
+        self.pixelCollection = PixelCollection(self.TS)
+        self.spectralTempVis = SpectralTemporalVisualization(self.pixelCollection, self.tableView2DBands, self.plotWidget2D)
+        self.actionRefresh2D.triggered.connect(lambda : self.spectralTempVis.refresh())
+        self.pixelLoader.sigLoadingStarted.connect(self.pixelCollection.clear)
+        self.pixelLoader.sigLoadingFinished.connect(lambda : QTimer.singleShot(200, lambda : self.spectralTempVis.refresh()))
+
+
+    def onPixelLoaded(self, nDone, nMax, d):
+        self.progressBar.setValue(nDone)
+        self.progressBar.setMaximum(nMax)
+        t = ''
+        if len(d) > 0:
+            t = 'Last loaded from {}.'.format(os.path.basename(d['path']))
+            QgsApplication.processEvents()
+            if self.pixelCollection is not None:
+                self.pixelCollection.addPixel(d)
+        self.progressInfo.setText(t)
+
+
+
+    def loadCoordinate(self, coordinate, crs):
+
+        assert isinstance(coordinate, QgsPoint)
+        assert isinstance(crs, QgsCoordinateReferenceSystem)
+        from timeseriesviewer.timeseries import TimeSeries
+        assert isinstance(self.TS, TimeSeries)
+
+        self.setWindowTitle('{} | {} {}'.format(self.baseTitle, str(coordinate), crs.authid()))
+        self.pixelLoader.setNumberOfThreads(SETTINGS.value('n_threads', 1))
+        #shoudl we allow to keep pixels in memory, e.g. limited by buffer?
+        if self.pxCollection is not None:
+            self.pxCollection.clear()
+        files = [d.pathImg for d in self.TS.data]
+        self.progressInfo.setText('Start loading from {} images...'.format(len(files)))
+        self.pixelLoader.startLoading(files, coordinate, crs)
+
+
+class SpectralTemporalVisualization(QObject):
+
+    sigShowPixel = pyqtSignal(TimeSeriesDatum, QgsPoint, QgsCoordinateReferenceSystem)
+
+    def __init__(self, pixelCollection, tableView, graphic2D):
+        super(SpectralTemporalVisualization, self).__init__()
+        #assert isinstance(timeSeries, TimeSeries)
+        assert isinstance(tableView, QTableView)
+        assert isinstance(graphic2D, pg.PlotWidget)
+        assert isinstance(pixelCollection, PixelCollection)
+        #self.TS = timeSeries
+        self.TV = tableView
+        self.TV.setSortingEnabled(False)
+        self.plot2D = graphic2D
+        self.pxCollection = pixelCollection
+        self.plotSettingsModel = PlotSettingsModel(self.pxCollection)
+        self.plotSettingsModel.sigSensorAdded.connect(self.refresh)
+        #self.plotSettingsModel.sigVisiblityChanged.connect(self.setVisibility)
+
+        self.plotSettingsModel.sigVisiblityChanged.connect(self.refresh)
+        self.plotSettingsModel.rowsInserted.connect(self.onRowsInserted)
+        self.TV.setModel(self.plotSettingsModel)
+        self.delegate = PlotSettingsWidgetDelegate(self.TV)
+        self.TV.setItemDelegateForColumn(2, self.delegate)
+        self.TV.setItemDelegateForColumn(3, self.delegate)
+        #self.TV.setItemDelegateForColumn(3, PointStyleDelegate(self.TV))
+        for s in self.pxCollection.sensorPxLayers.keys():
+            self.plotSettingsModel.addSensor(s)
+
+
+
+
+
+        """
+        def setPersistentWidgets(self, index, start, end):
+            self.VIEW.openPersistentEditor(self.createIndex(index.row(), 2))
+            self.VIEW.openPersistentEditor(self.createIndex(index.row(), 3))
+        """
+        # self.VIEW.setItemDelegateForColumn(3, PointStyleDelegate(self.VIEW))
+        self.plotData2D = dict()
+        self.plotData3D = dict()
+        self.refresh()
+
+    def onRowsInserted(self, parent, start, end):
+        model = self.TV.model()
+        if model:
+            colExpression = model.columnames.index('y-value')
+            colStyle = model.columnames.index('style')
+            while start <= end:
+                idxExpr = model.createIndex(start, colExpression)
+                idxStyle = model.createIndex(start, colStyle)
+                self.TV.openPersistentEditor(idxExpr)
+                self.TV.openPersistentEditor(idxStyle)
+                start += 1
+                #self.TV.openPersistentEditor(model.createIndex(start, colStyle))
+            s = ""
+
+
+    def setVisibility(self, sensorView):
+        assert isinstance(sensorView, SensorPlotSettings)
+        self.setVisibility2D(sensorView)
+
+    def setVisibility2D(self, sensorView):
+        assert isinstance(sensorView, SensorPlotSettings)
+        scatter = self.plotData2D[sensorView.sensor]
+        scatter.setVisible(sensorView.isVisible)
+        scatter.setData(brush=sensorView.color)
+
+        self.plot2D.removeItem(scatter)
+        self.plot2D.addItem(scatter)
+
+    def refresh(self, sensorView = None):
+
+        if sensorView is None:
+            for sv in self.plotSettingsModel.items:
+                self.refresh(sv)
+        else:
+            assert isinstance(sensorView, SensorPlotSettings)
+            self.refresh2D(sensorView)
+        pass
+
+    def refresh2D(self, sensorView):
+        assert isinstance(sensorView, SensorPlotSettings)
+        pi = self.plot2D
+        assert isinstance(pi, pg.PlotWidget)
+
+        if sensorView.sensor not in self.plotData2D.keys():
+            #init scatter plot item
+            scatter = pg.ScatterPlotItem()
+            scatter.setToolTip('Values {}'.format(sensorView.sensor.sensorName))
+            pi.addItem(scatter)
+            self.plotData2D[sensorView.sensor] = scatter
+
+        scatter = self.plotData2D[sensorView.sensor]
+        scatter.clear()
+        scatter.setVisible(sensorView.isVisible)
+
+        #https://github.com/pyqtgraph/pyqtgraph/blob/5195d9dd6308caee87e043e859e7e553b9887453/examples/customPlot.py
+
+        if sensorView.isVisible:
+
+            tsds, values = self.pxCollection.dateValues(sensorView.sensor, sensorView.expression)
+            if len(tsds) > 0:
+
+                dates = np.asarray([tsd.date for tsd in tsds])
+                values = np.asarray(values)
+
+                scatter.setData(x=dates, y=values, data=tsds, brush=sensorView.color)
+
+                pi.addItem(scatter)
+
+            s = ""
+        s = ""
+
+    def refresh3D(self, *arg):
+        pass
+    #
+    def loadCoordinate(self, point, crs):
+        assert isinstance(point, QgsPoint)
+        assert isinstance(crs, QgsCoordinateReferenceSystem)
+        pass
+
+
+
+
+
+def testPixelLoader():
+
+    # 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()
+    PL.setNumberOfThreads(1)
+
+    if False:
+        files = ['observationcloud/testdata/2014-07-26_LC82270652014207LGN00_BOA.bsq',
+                 'observationcloud/testdata/2014-08-03_LE72270652014215CUB00_BOA.bsq'
+                 ]
+    else:
+        from utils import file_search
+        searchDir = r'H:\LandsatData\Landsat_NovoProgresso'
+        files = 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()
+
+if __name__ == '__main__':
+    import site, sys
+    #add site-packages to sys.path as done by enmapboxplugin.py
+
+    from timeseriesviewer import DIR_SITE_PACKAGES
+    site.addsitedir(DIR_SITE_PACKAGES)
+
+    #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()
+
+    #run tests
+    #d = AboutDialogUI()
+    #d.show()
+
+    from timeseriesviewer.tests import *
+
+    TS = TestObjects.TimeSeries(100)
+    ext = TS.getMaxSpatialExtent()
+
+    d = ProfileViewDockUI()
+    d.connectTimeSeries(TS)
+    d.show()
+    d.loadCoordinate(ext.center(), ext.crs())
+
+    #close QGIS
+    try:
+        qgsApp.exec_()
+    except:
+        s = ""
+    qgsApp.exitQgis()
-- 
GitLab