Skip to content
Snippets Groups Projects
mapvisualization.py 50.9 KiB
Newer Older
# -*- coding: utf-8 -*-
"""
/***************************************************************************
                              HUB TimeSeriesViewer
                              -------------------
        begin                : 2015-08-20
        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
from __future__ import absolute_import
import os, sys, re, fnmatch, collections, copy, traceback, six, bisect
import logging
logger = logging.getLogger(__name__)
from qgis.core import *
from PyQt4.QtXml import *
from PyQt4.QtCore import *
from PyQt4.QtGui import *
import numpy as np
from timeseriesviewer.utils import *
from timeseriesviewer.main import TimeSeriesViewer
from timeseriesviewer.timeseries import SensorInstrument, TimeSeriesDatum
from timeseriesviewer.ui.docks import TsvDockWidgetBase, loadUi
from timeseriesviewer.ui.widgets import TsvMimeDataUtils, maxWidgetSizes
from timeseriesviewer.mapcanvas import MapCanvas
class MapView(QObject):

    sigRemoveMapView = pyqtSignal(object)
    sigMapViewVisibility = pyqtSignal(bool)
    sigVectorVisibility = pyqtSignal(bool)

    sigTitleChanged = pyqtSignal(str)
    sigSensorRendererChanged = pyqtSignal(SensorInstrument, QgsRasterRenderer)
    from timeseriesviewer.crosshair import CrosshairStyle
    sigCrosshairStyleChanged = pyqtSignal(CrosshairStyle)
    sigShowCrosshair = pyqtSignal(bool)
    sigVectorLayerChanged = pyqtSignal()


    def __init__(self, mapViewCollection, recommended_bands=None, parent=None):
        super(MapView, self).__init__()
        assert isinstance(mapViewCollection, MapViewCollection)
        self.mapViewCollection = mapViewCollection
        self.sensorViews = collections.OrderedDict()
        self.spatTempVis = mapViewCollection.STV
        self.ui = MapViewDefinitionUI(self, parent=parent)
        self.ui.create()

        self.setVisibility(True)

        self.setVectorLayer(None)

        #forward actions with reference to this band view
        self.ui.actionRemoveMapView.triggered.connect(lambda: self.sigRemoveMapView.emit(self))
        self.ui.actionApplyStyles.triggered.connect(self.applyStyles)
        self.ui.actionShowCrosshair.toggled.connect(self.setShowCrosshair)
        self.ui.sigShowMapView.connect(lambda: self.sigMapViewVisibility.emit(True))
        self.ui.sigHideMapView.connect(lambda: self.sigMapViewVisibility.emit(False))
        self.ui.sigVectorVisibility.connect(self.sigVectorVisibility.emit)



    def mapCanvases(self):
        m = []
        for sensor, sensorView in self.sensorViews.items():
            m.extend(sensorView.mapCanvases())
        return m

    def vectorLayerRenderer(self):
        if isinstance(self.mVectorLayer, QgsVectorLayer):
            return self.mVectorLayer.rendererV2()
        return None

    def setVectorLayerRenderer(self, renderer):
        if isinstance(renderer, QgsFeatureRendererV2) and \
            isinstance(self.mVectorLayer, QgsVectorLayer):
            self.mVectorLayer.setRendererV2(renderer)

    def setVectorLayer(self, lyr):
        if isinstance(lyr, QgsVectorLayer):
            #add vector layer
            self.mVectorLayer = lyr
            self.mVectorLayer.rendererChanged.connect(self.sigVectorLayerChanged)
            for mapCanvas in self.mapCanvases():
                assert isinstance(mapCanvas, MapCanvas)
                mapCanvas.setLayers([l for l in mapCanvas.layers() if isinstance(l, QgsRasterLayer)])
                mapCanvas.setLazyVectorSources([lyr])
            #remove vector layers
            self.mVectorLayer = None
            self.ui.btnVectorOverlayVisibility.setEnabled(False)
            for mapCanvas in self.mapCanvases():
                mapCanvas.setLayers([l for l in mapCanvas.mLayers if not isinstance(l, QgsVectorLayer)])
                #mapCanvas.refresh()
    def applyStyles(self):
        for sensorView in self.sensorViews.values():
            sensorView.applyStyle()
        s = ""


    def setVisibility(self, isVisible):
        self.ui.setVisibility(isVisible)

    def visibility(self):
        return self.ui.visibility()

    def visibleVectorOverlay(self):
        return isinstance(self.mVectorLayer, QgsVectorLayer) and \
               self.ui.btnVectorOverlayVisibility.isChecked()



    def setTitle(self, title):
        self.mTitle = title
        #self.ui.setTitle('Map View' + title)
        self.sigTitleChanged.emit(self.mTitle)

    def title(self):
        return self.mTitle

    def setCrosshairStyle(self, crosshairStyle):
        self.sigCrosshairStyleChanged.emit(crosshairStyle)
    def setShowCrosshair(self, b):
        self.sigShowCrosshair.emit(b)

    def removeSensor(self, sensor):
        assert type(sensor) is SensorInstrument
        if sensor in self.sensorViews.keys():
            w = self.sensorViews.pop(sensor)
            assert isinstance(w, MapViewSensorSettings)
            l = self.ui.sensorList.layout()
            l.removeWidget(w.ui)
            w.ui.close()
            self.ui.adjustSize()
            return True
        else:
            return False

    def hasSensor(self, sensor):
        assert type(sensor) is SensorInstrument
        return sensor in self.sensorViews.keys()

    def registerMapCanvas(self, sensor, mapCanvas):
        from timeseriesviewer.mapcanvas import MapCanvas
        assert isinstance(mapCanvas, MapCanvas)
        assert isinstance(sensor, SensorInstrument)

        #set basic settings
        sensorView = self.sensorViews[sensor]
        assert isinstance(sensorView, MapViewSensorSettings)
        sensorView.registerMapCanvas(mapCanvas)

        #register signals sensor specific signals
        mapCanvas.setRenderer(sensorView.rasterLayerRenderer())
        #register non-sensor specific signals for this mpa view
        self.sigMapViewVisibility.connect(mapCanvas.refresh)
        self.sigCrosshairStyleChanged.connect(mapCanvas.setCrosshairStyle)
        self.sigShowCrosshair.connect(mapCanvas.setShowCrosshair)
        self.sigVectorLayerChanged.connect(mapCanvas.refresh)
        self.sigVectorVisibility.connect(mapCanvas.refresh)
    def addSensor(self, sensor):
        """
        :param sensor:
        :return:
        """
        assert type(sensor) is SensorInstrument
        assert sensor not in self.sensorViews.keys()
        w = MapViewSensorSettings(sensor)

        #w.showSensorName(False)
        self.sensorViews[sensor] = w
        l = self.ui.sensorList.layout()
        lastWidgetIndex = 0
        for i in range(l.count()):
            if isinstance(l.itemAt(i), QWidget):
                lastWidgetIndex = i
        l.insertWidget(lastWidgetIndex, w.ui)

    def getSensorWidget(self, sensor):
        assert type(sensor) is SensorInstrument
        return self.sensorViews[sensor]



class MapViewRenderSettingsUI(QGroupBox, loadUi('mapviewrendersettings.ui')):

    def __init__(self, parent=None):
        """Constructor."""
        super(MapViewRenderSettingsUI, self).__init__(parent)
        # Set up the user interface from Designer.
        # After setupUI you can access any designer object by doing
        # self.<objectname>, and you can use autoconnect slots - see
        # http://qt-project.org/doc/qt-4.8/designer-using-a-ui-file.html
        # #widgets-and-dialogs-with-auto-connect

        self.setupUi(self)

        self.btnDefaultMB.setDefaultAction(self.actionSetDefaultMB)
        self.btnTrueColor.setDefaultAction(self.actionSetTrueColor)
        self.btnCIR.setDefaultAction(self.actionSetCIR)
        self.btn453.setDefaultAction(self.actionSet453)

        self.btnSingleBandDef.setDefaultAction(self.actionSetDefaultSB)
        self.btnSingleBandBlue.setDefaultAction(self.actionSetB)
        self.btnSingleBandGreen.setDefaultAction(self.actionSetG)
        self.btnSingleBandRed.setDefaultAction(self.actionSetR)
        self.btnSingleBandNIR.setDefaultAction(self.actionSetNIR)
        self.btnSingleBandSWIR.setDefaultAction(self.actionSetSWIR)

        self.btnPasteStyle.setDefaultAction(self.actionPasteStyle)
        self.btnCopyStyle.setDefaultAction(self.actionCopyStyle)
        self.btnApplyStyle.setDefaultAction(self.actionApplyStyle)



class MapViewSensorSettings(QObject):
    """
    Describes the rendering of images of one Sensor
    """

    #sigSensorRendererChanged = pyqtSignal(QgsRasterRenderer)

    def __init__(self, sensor, parent=None):
        """Constructor."""
        super(MapViewSensorSettings, self).__init__(parent)
        from timeseriesviewer.timeseries import SensorInstrument
        assert isinstance(sensor, SensorInstrument)
        self.sensor = sensor

        self.ui = MapViewRenderSettingsUI(parent)
        self.ui.create()
        self.ui.stackedWidget.currentChanged.connect(self.updateUi)
        self.sensor.sigNameChanged.connect(self.onSensorNameChanged)
        self.onSensorNameChanged(self.sensor.name())
        self.ui.bandNames = sensor.bandNames

        self.multiBandMinValues = [self.ui.tbRedMin, self.ui.tbGreenMin, self.ui.tbBlueMin]
        self.multiBandMaxValues = [self.ui.tbRedMax, self.ui.tbGreenMax, self.ui.tbBlueMax]
        self.multiBandSliders = [self.ui.sliderRed, self.ui.sliderGreen, self.ui.sliderBlue]

        for tb in self.multiBandMinValues + self.multiBandMaxValues + [self.ui.tbSingleBandMin, self.ui.tbSingleBandMax]:
            tb.setValidator(QDoubleValidator())
        for sl in self.multiBandSliders + [self.ui.sliderSingleBand]:
            sl.setMinimum(1)
            sl.setMaximum(sensor.nb)
            sl.valueChanged.connect(self.updateUi)

        self.ceAlgs = collections.OrderedDict()

        self.ceAlgs["No enhancement"] = QgsContrastEnhancement.NoEnhancement
        self.ceAlgs["Stretch to MinMax"] = QgsContrastEnhancement.StretchToMinimumMaximum
        self.ceAlgs["Stretch and clip to MinMax"] = QgsContrastEnhancement.StretchAndClipToMinimumMaximum
        self.ceAlgs["Clip to MinMax"] = QgsContrastEnhancement.ClipToMinimumMaximum

        self.colorRampType = collections.OrderedDict()
        self.colorRampType['Interpolated'] = QgsColorRampShader.INTERPOLATED
        self.colorRampType['Discrete'] = QgsColorRampShader.DISCRETE
        self.colorRampType['Exact'] = QgsColorRampShader.EXACT

        self.colorRampClassificationMode = collections.OrderedDict()
        self.colorRampClassificationMode['Continuous'] = 1
        self.colorRampClassificationMode['Equal Interval'] = 2
        self.colorRampClassificationMode['Quantile'] = 3

        def populateCombobox(cb, d):
            for key, value in d.items():
                cb.addItem(key, value)
            cb.setCurrentIndex(0)

        populateCombobox(self.ui.comboBoxContrastEnhancement, self.ceAlgs)
        populateCombobox(self.ui.cbSingleBandColorRampType, self.colorRampType)
        populateCombobox(self.ui.cbSingleBandMode, self.colorRampClassificationMode)

        self.ui.cbSingleBandColorRamp.populate(QgsStyleV2.defaultStyle())


        nb = self.sensor.nb
        lyr = QgsRasterLayer(self.sensor.pathImg)

        #define default renderers:
        bands = [min([b,nb-1]) for b in range(3)]
        extent = lyr.extent()

        bandStats = [lyr.dataProvider().bandStatistics(b, QgsRasterBandStats.All, extent, 500) for b in range(nb)]

        def createEnhancement(bandIndex):
            bandIndex = min([nb - 1, bandIndex])
            e = QgsContrastEnhancement(self.sensor.bandDataType)
            e.setMinimumValue(bandStats[bandIndex].Min)
            e.setMaximumValue(bandStats[bandIndex].Max)
            e.setContrastEnhancementAlgorithm(QgsContrastEnhancement.StretchToMinimumMaximum)
            return e

        self.defaultMB = QgsMultiBandColorRenderer(lyr.dataProvider(), bands[0], bands[1], bands[2])
        self.defaultMB.setRedContrastEnhancement(createEnhancement(bands[0]))
        self.defaultMB.setGreenContrastEnhancement(createEnhancement(bands[1]))
        self.defaultMB.setBlueContrastEnhancement(createEnhancement(bands[2]))

        self.defaultSB = QgsSingleBandPseudoColorRenderer(lyr.dataProvider(), 0, None)

        colorRamp = self.ui.cbSingleBandColorRamp.currentColorRamp()

        #fix: QGIS 3.0 constructor
        shaderFunc = QgsColorRampShader(bandStats[0].Min, bandStats[0].Max)
        shaderFunc.setColorRampType(QgsColorRampShader.INTERPOLATED)
        shaderFunc.setClip(True)
        nSteps = 5
        colorRampItems = []
        diff = bandStats[0].Max - bandStats[0].Min
        for  i in range(nSteps+1):
            f = float(i) / nSteps
            color = colorRamp.color(f)
            value = bandStats[0].Min + diff * f
            colorRampItems.append(QgsColorRampShader.ColorRampItem(value, color))
        shaderFunc.setColorRampItemList(colorRampItems)
        shader = QgsRasterShader()
        shader.setMaximumValue(bandStats[0].Max)
        shader.setMinimumValue(bandStats[0].Min)
        shader.setRasterShaderFunction(shaderFunc)
        self.defaultSB.setShader(shader)
        self.defaultSB.setClassificationMin(shader.minimumValue())
        self.defaultSB.setClassificationMax(shader.maximumValue())
        #init connect signals
        self.ui.actionSetDefaultMB.triggered.connect(lambda : self.setBandSelection('defaultMB'))
        self.ui.actionSetTrueColor.triggered.connect(lambda: self.setBandSelection('TrueColor'))
        self.ui.actionSetCIR.triggered.connect(lambda: self.setBandSelection('CIR'))
        self.ui.actionSet453.triggered.connect(lambda: self.setBandSelection('453'))

        self.ui.actionSetDefaultSB.triggered.connect(lambda: self.setBandSelection('defaultSB'))
        self.ui.actionSetB.triggered.connect(lambda: self.setBandSelection('B'))
        self.ui.actionSetG.triggered.connect(lambda: self.setBandSelection('G'))
        self.ui.actionSetR.triggered.connect(lambda: self.setBandSelection('R'))
        self.ui.actionSetNIR.triggered.connect(lambda: self.setBandSelection('nIR'))
        self.ui.actionSetSWIR.triggered.connect(lambda: self.setBandSelection('swIR'))


        self.ui.actionApplyStyle.triggered.connect(self.applyStyle)
        self.ui.actionCopyStyle.triggered.connect(lambda : QApplication.clipboard().setMimeData(self.mimeDataStyle()))
        self.ui.actionPasteStyle.triggered.connect(lambda : self.pasteStyleFromClipboard())

        #self.ui.stackedWidget

        if not self.sensor.wavelengthsDefined():
            self.ui.btnTrueColor.setEnabled(False)
            self.ui.btnCIR.setEnabled(False)
            self.ui.btn453.setEnabled(False)

            self.ui.btnSingleBandBlue.setEnabled(False)
            self.ui.btnSingleBandGreen.setEnabled(False)
            self.ui.btnSingleBandRed.setEnabled(False)
            self.ui.btnSingleBandNIR.setEnabled(False)
            self.ui.btnSingleBandSWIR.setEnabled(False)

        #apply recent or default renderer
        renderer = lyr.renderer()

        #set defaults
        self.setLayerRenderer(self.defaultSB)
        self.setLayerRenderer(self.defaultMB)

        if type(renderer) in [QgsMultiBandColorRenderer, QgsSingleBandPseudoColorRenderer]:
            self.setLayerRenderer(renderer)


        QApplication.clipboard().dataChanged.connect(self.onClipboardChange)
        self.onClipboardChange()

    def mapCanvases(self):
        return self.mMapCanvases[:]

    def registerMapCanvas(self, mapCanvas):

        assert isinstance(mapCanvas, MapCanvas)
        self.mMapCanvases.append(mapCanvas)
        mapCanvas.sigChangeSVRequest.connect(self.onMapCanvasRendererChangeRequest)


    def onSensorNameChanged(self, newName):
        self.sensor.sigNameChanged.connect(self.ui.labelTitle.setText)
        self.ui.labelTitle.setText(self.sensor.name())
        self.ui.actionApplyStyle.setToolTip('Apply style to all map view images from "{}"'.format(self.sensor.name()))

    def pasteStyleFromClipboard(self):
        utils = TsvMimeDataUtils(QApplication.clipboard().mimeData())
        if utils.hasRasterStyle():
            renderer = utils.rasterStyle(self.sensor.bandDataType)
            if renderer is not None:
                self.setLayerRenderer(renderer)

        for mapCanvas in self.mMapCanvases:
            assert isinstance(mapCanvas, MapCanvas)
            mapCanvas.setRenderer(r)

    def onClipboardChange(self):
        utils = TsvMimeDataUtils(QApplication.clipboard().mimeData())
        self.ui.btnPasteStyle.setEnabled(utils.hasRasterStyle())

    def onMapCanvasRendererChangeRequest(self, mapCanvas, renderer):
        self.setLayerRenderer(renderer)

    def setBandSelection(self, key):


        if key == 'defaultMB':
            bands = [self.defaultMB.redBand(), self.defaultMB.greenBand(), self.defaultMB.blueBand()]
        elif key == 'defaultSB':
            bands = [self.defaultSB.band()]

        else:
            if key in ['R','G','B','nIR','swIR']:
                colors = [key]
            elif key == 'TrueColor':
                colors = ['R','G','B']
            elif key == 'CIR':
                colors = ['nIR', 'R', 'G']
            elif key == '453':
                colors = ['nIR','swIR', 'R']
            bands = [self.sensor.bandClosestToWavelength(c) for c in colors]

        if len(bands) == 1:
            self.ui.sliderSingleBand.setValue(bands[0]+1)
        elif len(bands) == 3:
            for i, b in enumerate(bands):
                self.multiBandSliders[i].setValue(b+1)


        cw = self.ui.stackedWidget.currentWidget()
        text = ''
        if cw == self.ui.pageMultiBand:
            text = 'RGB {} {} {}'.format(
                self.ui.sliderRed.value(),
                self.ui.sliderGreen.value(),
                self.ui.sliderBlue.value()
            )
        elif cw == self.ui.pageSingleBand:
            text = 'Band {}'.format(self.ui.sliderSingleBand.value())


        self.ui.labelSummary.setText(text)


    def setLayerRenderer(self, renderer):
        ui = self.ui
        assert isinstance(renderer, QgsRasterRenderer)
        from timeseriesviewer.utils import niceNumberString
        updated = False
        if isinstance(renderer, QgsMultiBandColorRenderer):
            self.ui.cbRenderType.setCurrentIndex(0)
            #self.ui.stackedWidget.setcurrentWidget(self.ui.pageMultiBand)

            for s in self.multiBandSliders:
                s.blockSignals(True)
            ui.sliderRed.setValue(renderer.redBand())
            ui.sliderGreen.setValue(renderer.greenBand())
            ui.sliderBlue.setValue(renderer.blueBand())
            for s in self.multiBandSliders:
                s.blockSignals(False)

            ceRed = renderer.redContrastEnhancement()
            ceGreen = renderer.greenContrastEnhancement()
            ceBlue = renderer.blueContrastEnhancement()

            if ceRed is None:
                ceRed = ceGreen = ceBlue = QgsContrastEnhancement(self.sensor.bandDataType)
                s = ""
            for i, ce in enumerate([ceRed, ceGreen, ceBlue]):
                vMin = ce.minimumValue()
                vMax = ce.maximumValue()
                self.multiBandMinValues[i].setText(niceNumberString(vMin))
                self.multiBandMaxValues[i].setText(niceNumberString(vMax))

            idx = self.ceAlgs.values().index(ceRed.contrastEnhancementAlgorithm())
            ui.comboBoxContrastEnhancement.setCurrentIndex(idx)
            #self.updateUi()
            updated = True

        if isinstance(renderer, QgsSingleBandPseudoColorRenderer):
            self.ui.cbRenderType.setCurrentIndex(1)
            #self.ui.stackedWidget.setCurrentWidget(self.ui.pageSingleBand)

            self.ui.sliderSingleBand.setValue(renderer.band())
            shader = renderer.shader()
            cmin = shader.minimumValue()
            cmax = shader.maximumValue()
            self.ui.tbSingleBandMin.setText(str(cmin))
            self.ui.tbSingleBandMax.setText(str(cmax))

            shaderFunc = shader.rasterShaderFunction()
            self.ui.cbSingleBandColorRampType.setCurrentIndex(shaderFunc.colorRampType())
            updated = True

        self.updateUi()
        if updated and MapViewSensorSettings.SignalizeImmediately:
            #self.sigSensorRendererChanged.emit(renderer.clone())
            self.applyStyle()
        doc = QDomDocument()
        root = doc.createElement('qgis')

        return None

    def currentComboBoxItem(self, cb):
        d = cb.itemData(cb.currentIndex(), Qt.UserRole)
        return d

        ui = self.ui
        r = None
        if ui.stackedWidget.currentWidget() == ui.pageMultiBand:
            r = QgsMultiBandColorRenderer(None,
                ui.sliderRed.value(), ui.sliderGreen.value(), ui.sliderBlue.value())

            i = self.ui.comboBoxContrastEnhancement.currentIndex()
            alg = self.ui.comboBoxContrastEnhancement.itemData(i)

            if alg == QgsContrastEnhancement.NoEnhancement:
                r.setRedContrastEnhancement(None)
                r.setGreenContrastEnhancement(None)
                r.setBlueContrastEnhancement(None)
            else:
                rgbEnhancements = []
                for i in range(3):
                    e = QgsContrastEnhancement(self.sensor.bandDataType)
                    minmax = [float(self.multiBandMinValues[i].text()), float(self.multiBandMaxValues[i].text())]
                    cmin = min(minmax)
                    cmax = max(minmax)
                    e.setMinimumValue(cmin)
                    e.setMaximumValue(cmax)
                    e.setContrastEnhancementAlgorithm(alg)
                    rgbEnhancements.append(e)
                r.setRedContrastEnhancement(rgbEnhancements[0])
                r.setGreenContrastEnhancement(rgbEnhancements[1])
                r.setBlueContrastEnhancement(rgbEnhancements[2])

        if ui.stackedWidget.currentWidget() == ui.pageSingleBand:
            r = QgsSingleBandPseudoColorRenderer(None, ui.sliderSingleBand.value(), None)
            minmax = [float(ui.tbSingleBandMin.text()), float(ui.tbSingleBandMax.text())]
            cmin = min(minmax)
            cmax = max(minmax)
            r.setClassificationMin(cmin)
            r.setClassificationMax(cmax)
            colorRamp = self.ui.cbSingleBandColorRamp.currentColorRamp()

            # fix: QGIS 3.0 constructor
            shaderFunc = QgsColorRampShader(cmin, cmax)
            shaderFunc.setColorRampType(self.currentComboBoxItem(ui.cbSingleBandColorRampType))
            shaderFunc.setClip(True)
            nSteps = 10
            colorRampItems = []
            diff = cmax - cmin
            for i in range(nSteps + 1):
                f = float(i) / nSteps
                color = colorRamp.color(f)
                value = cmin + diff * f
                colorRampItems.append(QgsColorRampShader.ColorRampItem(value, color))
            shaderFunc.setColorRampItemList(colorRampItems)
            shader = QgsRasterShader()
            shader.setMaximumValue(cmax)
            shader.setMinimumValue(cmin)
            shader.setRasterShaderFunction(shaderFunc)
            r.setShader(shader)

            s = ""
        return r

class DatumView(QObject):

    sigRenderProgress = pyqtSignal(int,int)
    sigLoadingStarted = pyqtSignal(MapView, TimeSeriesDatum)
    sigLoadingFinished = pyqtSignal(MapView, TimeSeriesDatum)
    sigVisibilityChanged = pyqtSignal(bool)

    def __init__(self, timeSeriesDatum, timeSeriesDateViewCollection, mapViewCollection, parent=None):
        assert isinstance(timeSeriesDatum, TimeSeriesDatum)
        assert isinstance(timeSeriesDateViewCollection, DateViewCollection)
        assert isinstance(mapViewCollection, MapViewCollection)

        super(DatumView, self).__init__()
        from timeseriesviewer.ui.widgets import TimeSeriesDatumViewUI
        self.ui = TimeSeriesDatumViewUI(parent=parent)
        self.ui.create()
        self.showLoading(False)
        self.L = self.ui.layout()
        self.wOffset = self.L.count()-1
        self.minHeight = self.ui.height()
        self.minWidth = 50
        self.renderProgress = dict()
        assert isinstance(mapViewCollection.STV, SpatialTemporalVisualization)
        self.STV = mapViewCollection.STV
        self.TSD = timeSeriesDatum
        self.scrollArea = timeSeriesDateViewCollection.scrollArea
        self.Sensor = self.TSD.sensor
        self.Sensor.sigNameChanged.connect(lambda :self.setColumnInfo())
        self.TSD.sigVisibilityChanged.connect(self.setVisibility)
        self.DVC = timeSeriesDateViewCollection
        self.mRenderState = dict()
        labelTxt = '{}\n{}'.format(str(self.TSD.date), self.TSD.sensor.name())
        tooltip = '{}'.format(self.TSD.pathImg)

        self.ui.labelTitle.setText(labelTxt)
        self.ui.labelTitle.setToolTip(tooltip)

    def setVisibility(self, b):
        self.ui.setVisible(b)
        self.sigVisibilityChanged.emit(b)


    def setMapViewVisibility(self, bandView, isVisible):
        self.mapCanvases[bandView].setVisible(isVisible)

    def sizeHint(self):
        if not self.ui.isVisible():
            return QSize(0,0)

        size = self.ui.sizeHint()
        m = self.ui.layout().contentsMargins()
        dx = m.left() + m.right()
        dy = self.ui.layout().spacing()

        n = len(self.mapCanvases)
        if n > 0:
            baseSize = self.mapCanvases.values()[0].size()
            size = QSize(baseSize.width()+ dx, \
                         size.height()+ n*dy)
        return size


    def removeMapView(self, mapView):
        canvas = self.mapCanvases.pop(mapView)
        self.L.removeWidget(canvas)
        canvas.close()
        self.adjustBaseMinSize()

    def refresh(self):
        if self.ui.isVisible():
            for c in self.mapCanvases.values():
                if c.isVisible():

    def insertMapView(self, mapView):
        assert isinstance(mapView, MapView)
        from timeseriesviewer.mapcanvas import MapCanvas
        mapCanvas = MapCanvas(self.ui)
        mapCanvas.setObjectName('MapCanvas {} {}'.format(mapView.title(), self.TSD.date))
        mapCanvas.blockSignals(True)
        self.registerMapCanvas(mapView, mapCanvas)
        # register MapCanvas on MV level
        mapView.registerMapCanvas(self.Sensor, mapCanvas)
        # register MapCanvas on STV level
        self.STV.registerMapCanvas(mapCanvas)
        mapCanvas.blockSignals(False)
        mapCanvas.renderComplete.connect(lambda : self.onRenderingChange(False))
        mapCanvas.renderStarting.connect(lambda : self.onRenderingChange(True))

    def showLoading(self, b):
        if b:
            self.ui.progressBar.setRange(0,0)
            self.ui.progressBar.setValue(-1)
        else:
            self.ui.progressBar.setRange(0,1)
            self.ui.progressBar.setValue(0)

    def onRenderingChange(self, b):
        mc = self.sender()
        #assert isinstance(mc, QgsMapCanvas)
        self.mRenderState[mc] = b
        self.showLoading(any(self.mRenderState.values()))
    def onRendering(self, *args):
        renderFlags = [m.renderFlag() for m in self.mapCanvases.values()]
        drawFlags = [m.isDrawing() for m in self.mapCanvases.values()]
        print((renderFlags, drawFlags))
        isLoading = any(renderFlags)

        self.showLoading(isLoading)

        s = ""
    def registerMapCanvas(self, mapView, mapCanvas):
        from timeseriesviewer.mapcanvas import MapCanvas
        assert isinstance(mapCanvas, MapCanvas)
        self.mapCanvases[mapView] = mapCanvas
        mapCanvas.setLazyRasterSources([self.TSD.pathImg])
        #mapCanvas.setLayers([QgsRasterLayer(self.TSD.pathImg)])
        self.L.insertWidget(self.wOffset + len(self.mapCanvases), mapCanvas)
        self.ui.update()

        #register signals handled on (this) DV level
        mapCanvas.renderStarting.connect(lambda: self.sigLoadingStarted.emit(mapView, self.TSD))
        mapCanvas.mapCanvasRefreshed.connect(lambda: self.sigLoadingFinished.emit(mapView, self.TSD))
        mapCanvas.sigShowProfiles.connect(mapView.sigShowProfiles.emit)
        mapCanvas.sigChangeDVRequest.connect(self.onMapCanvasRequest)
    def onMapCanvasRequest(self, mapCanvas, key):
        if key == 'hide_date':
            self.TSD.setVisibility(False)
        if key == 'copy_sensor':
            QApplication.clipboard().setText(self.TSD.sensor.name())
        if key == 'copy_date':
            QApplication.clipboard().setText(str(self.TSD.date))
        if key == 'copy_path':
            QApplication.clipboard().setText(str(self.TSD.pathImg))
    def __lt__(self, other):
        assert isinstance(other, DatumView)
        return self.TSD < other.TSD
        assert isinstance(other, DatumView)
        return self.TSD == other.TSD
    sigLoadingStarted = pyqtSignal(DatumView, MapView)
    sigLoadingFinished = pyqtSignal(DatumView, MapView)
    sigShowMapLayerInfo = pyqtSignal(dict)
    sigSpatialExtentChanged = pyqtSignal(SpatialExtent)
    sigMapSizeChanged = pyqtSignal(QSize)
    sigCRSChanged = pyqtSignal(QgsCoordinateReferenceSystem)
    sigActivateMapTool = pyqtSignal(str)

    def __init__(self, timeSeriesViewer):
        super(SpatialTemporalVisualization, self).__init__()
        #assert isinstance(timeSeriesViewer, TimeSeriesViewer), timeSeriesViewer
        self.mBlockCanvasSignals = False
        self.mSpatialExtent = SpatialExtent.world()
        self.mCRS = self.mSpatialExtent.crs()
        self.mSize = QSize(200,200)
        self.mColor = Qt.black
        self.mMapCanvases = []
        from timeseriesviewer.ui.widgets import TsvScrollArea
        assert isinstance(self.scrollArea, TsvScrollArea)


        self.mRefreshTimer = QTimer(self)
        self.mRefreshTimer.setInterval(1000)
        self.mRefreshTimer.timeout.connect(self.refresh)

        self.scrollArea.sigResized.connect(self.mRefreshTimer.start)
        self.scrollArea.horizontalScrollBar().valueChanged.connect(self.mRefreshTimer.start)
        self.TSV = timeSeriesViewer
        self.TS = timeSeriesViewer.TS
        self.targetLayout = self.ui.scrollAreaSubsetContent.layout()
        self.dockMapViews = self.ui.dockMapViews
        self.MVC = MapViewCollection(self)
        self.MVC.sigShowProfiles.connect(self.sigShowProfiles.emit)

        self.vectorOverlay = None

        self.DVC = DateViewCollection(self)
        self.DVC.sigResizeRequired.connect(self.adjustScrollArea)
        self.DVC.sigLoadingStarted.connect(self.ui.dockRendering.addStartedWork)
        self.DVC.sigLoadingFinished.connect(self.ui.dockRendering.addFinishedWork)
        #self.timeSeriesDateViewCollection.sigSpatialExtentChanged.connect(self.setSpatialExtent)
        self.TS.sigTimeSeriesDatesAdded.connect(self.DVC.addDates)
        self.TS.sigTimeSeriesDatesRemoved.connect(self.DVC.removeDates)
        self.DVC.addDates(self.TS[:])
        if len(self.TS) > 0:
            self.setSpatialExtent(self.TS.getMaxSpatialExtent())
        #self.setSubsetSize(QSize(100,50))
    def registerMapCanvas(self, mapCanvas):
        from timeseriesviewer.mapcanvas import MapCanvas
        assert isinstance(mapCanvas, MapCanvas)

        self.mMapCanvases.append(mapCanvas)

        #set general canvas properties
        mapCanvas.setFixedSize(self.mSize)
        mapCanvas.setCrs(self.mCRS)
        mapCanvas.setSpatialExtent(self.mSpatialExtent)


        #register on map canvas signals
        mapCanvas.sigSpatialExtentChanged.connect(lambda e: self.setSpatialExtent(e, mapCanvas))



        from timeseriesviewer.mapcanvas import MapCanvas
        for mapCanvas in self.mMapCanvases:
            assert isinstance(mapCanvas, MapCanvas)
            mapCanvas.setCrosshairStyle(crosshairStyle)

        #self.MVC.setCrosshairStyle(crosshairStyle)

    def setShowCrosshair(self, b):
        self.MVC.setShowCrosshair(b)

    def setVectorLayer(self, lyr):
        self.MVC.setVectorLayer(lyr)

    def createMapView(self):
        self.MVC.createMapView()

    def activateMapTool(self, key):
        from timeseriesviewer.mapcanvas import MapCanvas
        for mapCanvas in self.mMapCanvases:
            assert isinstance(mapCanvas, MapCanvas)
            mapCanvas.activateMapTool(key)
    def setMapSize(self, size):
        self.mSize = size
        from timeseriesviewer.mapcanvas import MapCanvas
        for mapCanvas in self.mMapCanvases:
            assert isinstance(mapCanvas, MapCanvas)
            mapCanvas.setFixedSize(size)
        self.sigMapSizeChanged.emit(self.mSize)
    def subsetSize(self):
        return QSize(self.mSize)

        #print('STV REFRESH')
        for tsdView in self.DVC:
        self.mRefreshTimer.stop()

    def adjustScrollArea(self):
        #adjust scroll area widget to fit all visible widgets
        m = self.targetLayout.contentsMargins()
        n = len(self.DVC)
        for TSDView in [v for v in self.DVC if v.ui.isVisible()]:
            s = s + TSDView.sizeHint()
                r = TSDView.sizeHint()
        if r:
            if isinstance(self.targetLayout, QHBoxLayout):

                s = QSize(s.width(), r.height())
            else:
                s = QSize(r.width(), s.height())

            s = s + QSize(m.left() + m.right(), m.top() + m.bottom())
            self.targetLayout.parentWidget().setFixedSize(s)




    def setMaxTSDViews(self, n=-1):
        self.nMaxTSDViews = n
        #todo: remove views

    def setSpatialCenter(self, center, mapCanvas0=None):
        if self.mBlockCanvasSignals:
            return True

        assert isinstance(center, SpatialPoint)
        center = center.toCrs(self.mCRS)
        if not isinstance(center, SpatialPoint):
            return

        self.mBlockCanvasSignals = True
        self.mSpatialExtent.setCenter(center)
        for mapCanvas in self.mMapCanvases:
            if mapCanvas != mapCanvas0:
                oldState = mapCanvas.blockSignals(True)
                mapCanvas.setCenter(center)
                mapCanvas.blockSignals(oldState)
        self.mBlockCanvasSignals = False
        self.sigSpatialExtentChanged.emit(self.mSpatialExtent)


    def setSpatialExtent(self, extent, mapCanvas0=None):
        if self.mBlockCanvasSignals:
            return True
        assert isinstance(extent, SpatialExtent)
        extent = extent.toCrs(self.mCRS)
        if not isinstance(extent, SpatialExtent) \
            or extent.isEmpty() or not extent.isFinite() \
            or extent.width() <= 0 \
            or extent.height() <= 0 \
            or extent == self.mSpatialExtent:

        self.mBlockCanvasSignals = True
        self.mSpatialExtent = extent
        for mapCanvas in self.mMapCanvases:
            if mapCanvas != mapCanvas0:
                oldState = mapCanvas.blockSignals(True)
                mapCanvas.setExtent(extent)
                mapCanvas.blockSignals(oldState)
        self.mBlockCanvasSignals = False
        #for mapCanvas in self.mMapCanvases:
        #    mapCanvas.refresh()
        self.mRefreshTimer.start()
        self.sigSpatialExtentChanged.emit(extent)
    def setBackgroundColor(self, color):
        assert isinstance(color, QColor)
        self.mColor = color

    def backgroundColor(self):
        return self.mColor

    def mapCanvasIterator(self):
        return self.mMapCanvases[:]
    def setCrs(self, crs):
        assert isinstance(crs, QgsCoordinateReferenceSystem)

        if self.mCRS != crs:
            from timeseriesviewer.utils import saveTransform
            if saveTransform(self.mSpatialExtent, self.mCRS, crs):
                self.mCRS = crs
                for mapCanvas in self.mapCanvasIterator():
                    #print(('STV set CRS {} {}', str(mapCanvas), self.mCRS.description()))
                    mapCanvas.setCrs(crs)
            else:
                pass
            self.sigCRSChanged.emit(self.mCRS)