Skip to content
Snippets Groups Projects
mapvisualization.py 62.7 KiB
Newer Older
  • Learn to ignore specific revisions
  • # noinspection PyPep8Naming
    
    """
    /***************************************************************************
    
                                  -------------------
            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.                                   *
     *                                                                         *
     ***************************************************************************/
    """
    
    
    import os, sys, re, fnmatch, collections, copy, traceback, bisect
    
    from qgis.core import QgsContrastEnhancement, QgsRasterShader, QgsColorRampShader,  QgsProject, QgsCoordinateReferenceSystem, \
        QgsRasterLayer, QgsVectorLayer, QgsMapLayer, QgsMapLayerProxyModel, QgsColorRamp, QgsSingleBandPseudoColorRenderer
    
    
    from qgis.gui import *
    from qgis.gui import QgsDockWidget, QgsMapCanvas, QgsMapTool, QgsCollapsibleGroupBox
    
    from PyQt5.QtXml import *
    from PyQt5.QtCore import *
    from PyQt5.QtGui import *
    
    from .utils import *
    from .import Option, OptionListModel
    
    from .timeseries import SensorInstrument, TimeSeriesDate, TimeSeries, SensorProxyLayer
    
    from .utils import loadUI
    from .mapviewscrollarea import MapViewScrollArea
    
    from .mapcanvas import MapCanvas, MapTools, MapCanvasInfoItem, MapCanvasMapTools
    
    from .externals.qps.crosshair.crosshair import getCrosshairStyle, CrosshairStyle
    from .externals.qps.layerproperties import showLayerPropertiesDialog
    from .externals.qps.maptools import *
    
    
    #assert os.path.isfile(dummyPath)
    #lyr = QgsRasterLayer(dummyPath)
    #assert lyr.isValid()
    DUMMY_RASTERINTERFACE = QgsSingleBandGrayRenderer(None, 0)
    
    
    KEY_LOCKED_LAYER = 'eotsv/locked'
    KEY_SENSOR_GROUP = 'eotsv/sensorgroup'
    KEY_SENSOR_LAYER = 'eotsv/sensorlayer'
    
    class MapViewLayerTreeViewMenuProvider(QgsLayerTreeViewMenuProvider):
    
    
        def __init__(self, mapView, view: QgsLayerTreeView, canvas: QgsMapCanvas):
    
            super(MapViewLayerTreeViewMenuProvider, self).__init__()
            assert isinstance(view, QgsLayerTreeView)
            assert isinstance(canvas, QgsMapCanvas)
            self.mLayerTreeView = view
            self.mDummyCanvas = canvas
            self.mDefActions = QgsLayerTreeViewDefaultActions(self.mLayerTreeView)
    
    
            self.actionAddGroup = self.mDefActions.actionAddGroup()
            self.actionRename = self.mDefActions.actionRenameGroupOrLayer()
            self.actionRemove = self.mDefActions.actionRemoveGroupOrLayer()
    
            #self.actionZoomToLayer = self.mDefActions.actionZoomToGroup(self.mDummyCanvas)
    
            self.actionCheckAndAllChildren = self.mDefActions.actionCheckAndAllChildren()
            self.actionShowFeatureCount = self.mDefActions.actionShowFeatureCount()
    
            #self.actionZoomToLayer = self.mDefActions.actionZoomToLayer(self.mDummyCanvas)
            #self.actionZoomToSelected = self.mDefActions.actionZoomToSelection(self.mDummyCanvas)
            #self.actionZoomToGroup = self.mDefActions.actionZoomToGroup(self.mDummyCanvas)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            self.actionAddEOTSVSpectralProfiles = QAction('Add Spectral Profile Layer')
    
            self.actionAddEOTSVTemporalProfiles = QAction('Add Temporal Profile Layer')
    
        def mapView(self):
            return self.mMapView
    
    
        def layerTreeView(self)->QgsLayerTreeView:
            return self.mLayerTreeView
    
        def layerTree(self)->QgsLayerTree:
            return self.layerTreeModel().rootGroup()
    
        def layerTreeModel(self)->QgsLayerTreeModel:
            return self.layerTreeView().model()
    
        def createContextMenu(self)->QMenu:
    
            model = self.layerTreeModel()
            ltree = self.layerTree()
            view = self.layerTreeView()
            g = view.currentGroupNode()
            l = view.currentLayer()
            i = view.currentIndex()
            #fixedNodes = len([l for l in view.selectedLayersRecursive() if l.property(KEY_LOCKED_LAYER) == True]) > 0 or \
            #             isinstance(g, QgsLayerTreeGroup) and g.property(KEY_LOCKED_LAYER) == True
    
            # disable actions
            #self.actionRemove.setEnabled(fixedNodes == False)
    
            menu = QMenu(view)
            isSensorGroup = isinstance(g, QgsLayerTreeGroup) and g.customProperty(KEY_SENSOR_GROUP) in [True, 'true']
            isSensorLayer = isinstance(l, QgsRasterLayer) and l.customProperty(KEY_SENSOR_LAYER) in [True, 'true']
            self.actionRemove.setEnabled(not (isSensorGroup or isSensorLayer))
            self.actionAddGroup.setEnabled(not (isSensorGroup or isSensorLayer))
            menu.addAction(self.actionAddGroup)
            menu.addAction(self.actionRename)
            menu.addAction(self.actionRemove)
    
    
            #menu.addAction(self.actionZoomToGroup)
            #menu.addAction(self.actionZoomToLayer)
            #menu.addAction(self.actionZoomToSelected)
    
            menu.addSeparator()
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            menu.addAction(self.actionAddEOTSVSpectralProfiles)
            menu.addAction(self.actionAddEOTSVTemporalProfiles)
    
    
            centerCanvas = None
            if isinstance(self.mapView(), MapView):
                visibleCanvases = self.mapView().visibleMapCanvases()
                if len(visibleCanvases) > 0:
                    i = int(len(visibleCanvases) / 2)
                    centerCanvas = visibleCanvases[i]
    
    
            a = menu.addAction('Set Properties')
    
    
            a.triggered.connect(lambda *args,
                                       canvas = centerCanvas,
                                       lyr = l,
                                       b = not isinstance(l, SensorProxyLayer):
    
                                showLayerPropertiesDialog(lyr, canvas, useQGISDialog=b))
    
            a.setEnabled(isinstance(centerCanvas, QgsMapCanvas))
    
    
            from .externals.qps.layerproperties import pasteStyleFromClipboard, pasteStyleToClipboard
            a = menu.addAction('Copy Style')
            a.setToolTip('Copy the current layer style to clipboard')
            a.triggered.connect(lambda *args, lyr=l: pasteStyleToClipboard(lyr))
    
            a = menu.addAction('Paste Style')
            a.setEnabled('application/qgis.style' in QApplication.clipboard().mimeData().formats())
            a.triggered.connect(lambda *args, lyr=l: pasteStyleFromClipboard(lyr))
    
            #a = menu.addAction('Settings')
            #from qps.layerproperties import showLayerPropertiesDialog
            #a.triggered.connect(lambda *args, lyr=l:showLayerPropertiesDialog(lyr, self._canvas))
    
            return menu
    
    class MapViewLayerTreeModel(QgsLayerTreeModel):
        """
        Layer Tree as shown in a MapView
        """
        def __init__(self, rootNode, parent=None):
            super(MapViewLayerTreeModel, self).__init__(rootNode, parent=parent)
    
        def dataXXX(self, index:QModelIndex, role=Qt.DisplayRole):
            node = self.index2node(index)
            # if node.name() == 'testlayer':
            #     s = ""
    
            if True:
                if isinstance(node, QgsLayerTreeGroup) and node.customProperty(KEY_SENSOR_GROUP) in ['true', True]:
                    if role == Qt.FontRole:
                        f = super(MapViewLayerTreeModel, self).data(index, role=role)
                        f.setBold(True)
                        return f
                if isinstance(node, QgsLayerTreeLayer) and node.customProperty(KEY_SENSOR_LAYER) in ['true', True]:
    
                    if role == Qt.FontRole:
                        f = super(MapViewLayerTreeModel, self).data(index, role=role)
                        assert isinstance(f, QFont)
                        f.setItalic(True)
                        return f
    
                    if role == Qt.DecorationRole:
                        return QIcon(':/timeseriesviewer/icons/icon.svg')
    
            return super(MapViewLayerTreeModel, self).data(index, role=role)
    
        def flagsXXX(self, index:QModelIndex):
    
            f = super(MapViewLayerTreeModel, self).flags(index)
    
            node = self.index2node(index)
            if isinstance(node, QgsLayerTreeNode) and ( \
                    node.customProperty(KEY_SENSOR_LAYER) in ['true', True] or \
                    node.customProperty(KEY_SENSOR_GROUP) in ['true', True]):
                f = f ^ Qt.ItemIsDragEnabled
                f = f ^ Qt.ItemIsDropEnabled
    
            return f
    
    
    class MapView(QFrame, loadUIFormClass(jp(DIR_UI, 'mapview.ui'))):
        """
        A MapView defines how a single map canvas visualizes sensor specific EOTS data plus additional vector overlays
        """
    
        sigMapViewVisibilityChanged = pyqtSignal(bool)
        sigCrosshairVisibilityChanged = pyqtSignal(bool)
    
        sigTitleChanged = pyqtSignal(str)
    
        sigSensorRendererChanged = pyqtSignal(SensorInstrument, QgsRasterRenderer)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
        sigShowProfiles = pyqtSignal(SpatialPoint, MapCanvas, str)
    
        def __init__(self, name='Map View', parent=None):
            super(MapView, self).__init__(parent)
            self.setupUi(self)
    
            m = QMenu(self.btnToggleCrosshair)
            m.addAction(self.actionSetCrosshairStyle)
    
            self.btnToggleCrosshair.setMenu(m)
            self.btnToggleCrosshair.setDefaultAction(self.actionToggleCrosshairVisibility)
            self.btnToggleMapViewVisibility.setDefaultAction(self.actionToggleMapViewHidden)
            self.tbName.textChanged.connect(self.onTitleChanged)
            self.actionSetCrosshairStyle.triggered.connect(self.onChangeCrosshairStyle)
    
            self.actionToggleMapViewHidden.toggled.connect(lambda isHidden: self.setVisibility(not isHidden))
    
            self.actionToggleCrosshairVisibility.toggled.connect(self.setCrosshairVisibility)
    
    
            self.actionAddVectorLayer.triggered.connect(self.onAddVectorLayer)
            self.actionAddRasterLayer.triggered.connect(self.onAddRasterLayer)
            self.btnAddVectorLayer.setDefaultAction(self.actionAddVectorLayer)
            self.btnAddRasterLayer.setDefaultAction(self.actionAddRasterLayer)
    
            self.btnHighlightMapView.setDefaultAction(self.actionHighlightMapView)
            self.actionHighlightMapView.triggered.connect(lambda: self.setHighlighted(True, timeout=500))
    
            self.mCurrentLayer = None
    
    
            self.mSensorLayerList = list()
            self.mMapCanvases = list()
            assert isinstance(self.mLayerTreeView, QgsLayerTreeView)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
            self.mDummyCanvas = QgsMapCanvas() # dummy map canvas for dummy layers
    
            self.mDummyCanvas.setVisible(False)
    
            self.mLayerTree = QgsLayerTree()
            self.mLayerTreeMapCanvasBridget = QgsLayerTreeMapCanvasBridge(self.mLayerTree, self.mDummyCanvas)
    
            # self.mLayerTreeModel = QgsLayerTreeModel(self.mLayerTree)
            self.mLayerTreeModel = MapViewLayerTreeModel(self.mLayerTree)
    
            self.mLayerTreeModel.setFlags(QgsLayerTreeModel.AllowNodeChangeVisibility |
                                          QgsLayerTreeModel.AllowNodeRename |
                                          QgsLayerTreeModel.AllowNodeReorder)
    
            self._createSensorNode()
    
            self.mLayerTreeView.setModel(self.mLayerTreeModel)
    
            self.mMapLayerTreeViewMenuProvider = MapViewLayerTreeViewMenuProvider(self, self.mLayerTreeView, self.mDummyCanvas)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
            # register some actions that interact with other GUI elements
            self.mMapLayerTreeViewMenuProvider.actionAddEOTSVSpectralProfiles.triggered.connect(self.addSpectralProfileLayer)
            self.mMapLayerTreeViewMenuProvider.actionAddEOTSVTemporalProfiles.triggered.connect(self.addTemporalProfileLayer)
    
    
            self.mLayerTreeView.setMenuProvider(self.mMapLayerTreeViewMenuProvider)
    
            self.mLayerTreeView.currentLayerChanged.connect(self.setCurrentLayer)
    
            self.mLayerTree.removedChildren.connect(self.onChildNodesRemoved)
    
            self.mIsVisible = True
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            self.setTitle(name)
    
            m = QMenu()
            m.addAction(self.optionShowDate)
            m.addAction(self.optionShowSensorName)
            m.addAction(self.optionShowMapViewName)
            self.btnInfoOptions.setMenu(m)
    
            fixMenuButtons(self)
    
    
        def setName(self, name:str):
            self.setTitle(name)
    
        def name(self)->str:
            return self.title()
    
    
        def visibleMapCanvases(self)->list:
            """
            Returns the currently visible mapcanvases
            :return: [list-of-MapCanvases]
            """
            return [m for m in self.mapCanvases() if m.isVisibleToViewport()]
    
    
        def onAddVectorLayer(self):
            """
            Slot that opens a SelectMapLayersDialog to add a vector layer
            """
    
            from .externals.qps.utils import SelectMapLayersDialog
            d = SelectMapLayersDialog()
            d.setWindowTitle('Select Vector Layer')
            d.addLayerDescription('Vector Layer', QgsMapLayerProxyModel.VectorLayer)
    
            if d.exec() == QDialog.Accepted:
                for l in d.mapLayers():
                    self.addLayer(l)
    
        def onAddRasterLayer(self):
            """
            Slot that opens a SelectMapLayersDialog to add a vector layer
            """
            from .externals.qps.utils import SelectMapLayersDialog
            d = SelectMapLayersDialog()
            d.setWindowTitle('Select Raster Layer')
            d.addLayerDescription('Raster Layer', QgsMapLayerProxyModel.RasterLayer)
    
            if d.exec() == QDialog.Accepted:
                for l in d.mapLayers():
                    self.addLayer(l)
    
    
        def setCurrentLayer(self, layer:QgsMapLayer):
            """
            Sets the QgsMapCanvas.currentLayer() that is used by some QgsMapTools
            :param layer: QgsMapLayer | None
            :return:
            """
    
            assert layer is None or isinstance(layer, QgsMapLayer)
    
            self.mCurrentLayer = layer
    
            if layer not in self.mSensorLayerList:
                for c in self.mapCanvases():
                    c.setCurrentLayer(layer)
            else:
                s = ""
    
    
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def addSpectralProfileLayer(self):
            """Adds the EOTSV Spectral Profile Layer"""
            from eotimeseriesviewer.main import TimeSeriesViewer
            tsv = TimeSeriesViewer.instance()
            if isinstance(tsv, TimeSeriesViewer):
                lyr = tsv.spectralLibrary()
                if lyr not in self.layers():
                    self.addLayer(lyr)
    
        def addTemporalProfileLayer(self):
            """Adds the EOTSV Temporal Profile Layer"""
            from eotimeseriesviewer.main import TimeSeriesViewer
            tsv = TimeSeriesViewer.instance()
            if isinstance(tsv, TimeSeriesViewer):
                lyr = tsv.temporalProfileLayer()
                if lyr not in self.layers():
                    self.addLayer(lyr)
    
    
    
        def addLayer(self, layer:QgsMapLayer):
            """
            Add a QgsMapLayer to the MapView layer tree
            :param layer: QgsMapLayer
            """
            if isinstance(layer, QgsVectorLayer):
                self.mLayerTree.insertLayer(0, layer)
            else:
                self.mLayerTree.addLayer(layer)
    
        def _createSensorNode(self):
            self.mLayerTreeSensorNode = QgsLayerTreeGroup(name='Raster Time Series', checked=True)
            self.mLayerTreeSensorNode.setCustomProperty(KEY_LOCKED_LAYER, True)
            self.mLayerTreeSensorNode.setCustomProperty(KEY_SENSOR_GROUP, True)
            self.mLayerTree.addChildNode(self.mLayerTreeSensorNode)
    
        def _containsSensorNode(self, root:QgsLayerTreeGroup)->bool:
            assert isinstance(root, QgsLayerTreeGroup)
            if root.customProperty(KEY_SENSOR_GROUP) in [True, 'true']:
                return True
            for grp in root.findGroups():
                if self._containsSensorNode(grp):
                    return True
            return False
    
        def onChildNodesRemoved(self, node, idxFrom, idxTo):
            if not self._containsSensorNode(self.mLayerTreeModel.rootGroup()):
                self._createSensorNode()
    
        def onChangeCrosshairStyle(self):
            style = getCrosshairStyle(parent=self, crosshairStyle=self.crosshairStyle())
            if isinstance(style, CrosshairStyle):
                self.setCrosshairStyle(style)
    
    
        def setVisibility(self, b: bool):
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            assert isinstance(b, bool)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
    benjamin.jakimow@geo.hu-berlin.de's avatar
    benjamin.jakimow@geo.hu-berlin.de committed
            for mapCanvas in self.mapCanvases():
                assert isinstance(mapCanvas, MapCanvas)
    
    benjamin.jakimow@geo.hu-berlin.de's avatar
    benjamin.jakimow@geo.hu-berlin.de committed
                if not mapCanvas.isVisible() == b:
    
    benjamin.jakimow@geo.hu-berlin.de's avatar
    benjamin.jakimow@geo.hu-berlin.de committed
                    mapCanvas.setVisible(b)
    
    
            if self.actionToggleMapViewHidden.isChecked() == b:
                self.actionToggleMapViewHidden.setChecked(not b)
    
    benjamin.jakimow@geo.hu-berlin.de's avatar
    benjamin.jakimow@geo.hu-berlin.de committed
            if changed:
    
                self.sigMapViewVisibilityChanged.emit(b)
    
        def isVisible(self)->bool:
            """
            Returns the map view visibility
            :return: bool
            """
    
            return not self.actionToggleMapViewHidden.isChecked()
    
        def mapCanvases(self)->list:
            """
            Returns the MapCanvases related to this map view
            :return: [list-of-MapCanvases]
            """
    
            return self.mMapCanvases[:]
    
        def onTitleChanged(self, *args):
            self.setWindowTitle('Map View "{}"'.format(self.title()))
    
            self.sigTitleChanged.emit(self.title())
    
        def setTimeSeries(self, timeSeries:TimeSeries):
            """
            Conntects the MapView with a TimeSeries.
            :param timeSeries: TimeSeries
            """
            assert isinstance(timeSeries, TimeSeries)
    
            for s in self.sensors():
                self.removeSensor(s)
    
            self.mTimeSeries = timeSeries
            self.mTimeSeries.sigSensorAdded.connect(self.addSensor)
            self.mTimeSeries.sigSensorRemoved.connect(self.removeSensor)
            for s in timeSeries.sensors():
                self.addSensor(s)
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def timeSeries(self)->TimeSeries:
            """
            Returns the TimeSeries this mapview is connected with
            :return: TimeSeries
            """
            return self.mTimeSeries
    
        def setTitle(self, title:str):
            """
            Sets the widget title
            :param title: str
            """
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            old = self.title()
            if old != title:
    
                self.tbName.setText(title)
    
        def layers(self)->list:
            """
            Returns the visible layers, including proxy layer for time-series data
            :return: [list-of-QgsMapLayers]
            """
    
            return [l for l in self.mLayerTree.checkedLayers() if isinstance(l, QgsMapLayer)]
    
            """
            Returns the MapView title
            :return: str
            """
            return self.tbName.text()
    
            for mapCanvas in self.mapCanvases():
                if isinstance(mapCanvas, MapCanvas):
                    mapCanvas.refresh()
    
        def setCrosshairStyle(self, crosshairStyle:CrosshairStyle):
            """
    
            :param crosshairStyle: CrosshairStyle
            """
    
            from eotimeseriesviewer import CrosshairStyle
            assert isinstance(crosshairStyle, CrosshairStyle)
            srcCanvas = self.sender()
            if isinstance(srcCanvas, MapCanvas):
                dstCanvases = [c for c in self.mapCanvases() if c != srcCanvas]
            else:
                dstCanvases = [c for c in self.mapCanvases()]
    
            for mapCanvas in dstCanvases:
                mapCanvas.setCrosshairStyle(crosshairStyle, emitSignal=False)
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
        def setHighlighted(self, b=True, timeout=1000):
    
            """
            Activates or deactivates a red-line border of the MapCanvases
            :param b: True | False to activate / deactivate the highlighted lines-
            :param timeout: int, milliseconds how long the highlighted frame should appear
            """
    
            styleOn = """.MapCanvas {
                        border: 4px solid red;
                        border-radius: 4px;
                    }"""
            styleOff = """"""
            if b is True:
                for mapCanvas in self.mapCanvases():
                    mapCanvas.setStyleSheet(styleOn)
                if timeout > 0:
                    QTimer.singleShot(timeout, lambda : self.setHighlighted(False))
            else:
                for mapCanvas in self.mapCanvases():
                    mapCanvas.setStyleSheet(styleOff)
    
    
        def crosshairStyle(self)->CrosshairStyle:
            """
            Returns the CrosshairStyle
            :return:
            """
            for c in self.mapCanvases():
                assert isinstance(c, MapCanvas)
                style = c.crosshairStyle()
                if isinstance(style, CrosshairStyle):
                    return style
            return None
    
        def setCrosshairVisibility(self, b:bool):
    
            Enables / diables the map canvas crosshair.
            :param b: bool
    
            # set the action checked state first
            if self.actionToggleCrosshairVisibility.isChecked() != b:
                self.actionToggleCrosshairVisibility.setChecked(b)
    
                self.sigCrosshairVisibilityChanged.emit(b)
    
        def sensorProxyLayers(self)->list:
            layers = [n.layer() for n in self.mLayerTreeSensorNode.findLayers()]
            return [l for l in layers if isinstance(l, SensorProxyLayer)]
    
        def sensorProxyLayer(self, sensor:SensorInstrument)->SensorProxyLayer:
    
            Returns the proxy layer related to a SensorInstrument
            :param sensor: SensorInstrument
            :return: SensorLayer
            """
            for l in self.sensorProxyLayers():
                if l.sensor() == sensor:
                    return l
            return None
    
        def sensors(self)->list:
            """
            Returns a list of SensorsInstruments
            :return: [list-of-SensorInstruments]
            """
    
            return [t[0] for t in self.mSensorLayerList]
    
        def addSensor(self, sensor:SensorInstrument):
            """
            Adds a SensorInstrument to be shown in this MapView. Each sensor will be represented as a Raster Layer in the
            Tree Model.
            :param sensor: SensorInstrument
            """
            assert isinstance(sensor, SensorInstrument)
            if sensor not in self.sensors():
                dummyLayer = sensor.proxyLayer()
    
                assert isinstance(dummyLayer.renderer(), QgsRasterRenderer)
    
                dummyLayer.rendererChanged.connect(lambda sensor=sensor: self.onSensorRendererChanged(sensor))
    
                layerTreeLayer = self.mLayerTreeSensorNode.addLayer(dummyLayer)
                assert isinstance(layerTreeLayer, QgsLayerTreeLayer)
                layerTreeLayer.setCustomProperty(KEY_LOCKED_LAYER, True)
                layerTreeLayer.setCustomProperty(KEY_SENSOR_LAYER, True)
                self.mSensorLayerList.append((sensor, dummyLayer))
    
    
        def onSensorRendererChanged(self, sensor:SensorInstrument):
            for c in self.sensorCanvases(sensor):
                assert isinstance(c, MapCanvas)
                c.addToRefreshPipeLine(MapCanvas.Command.RefreshRenderer)
    
        def sensorCanvases(self, sensor:SensorInstrument)->list:
            """
            Returns the MapCanvases that show a layer with data for the given ``sensor``
            :param sensor: SensorInstrument
            :return:
            """
            assert isinstance(sensor, SensorInstrument)
            return [c for c in self.mapCanvases() if isinstance(c, MapCanvas) and c.tsd().sensor() == sensor]
    
    
    
        def sensorLayer(self, sensor: SensorInstrument):
            """
            Returns the QgsRasterLayer that is used a proxy to specify the QgsRasterRenderer for a sensor
            :param sensor: SensorInstrument
            :return: QgsRasterLayer
            """
            assert isinstance(sensor, SensorInstrument)
            for t in self.mSensorLayerList:
                s, l = t
                assert isinstance(s, SensorInstrument)
                assert isinstance(l, QgsRasterLayer)
                if s == sensor:
                    return l
            raise Exception('Sensor "{}" not registered to MapView "{}"'.format(sensor.name(), self.title()))
    
        def removeSensor(self, sensor:SensorInstrument):
            """
            Removes a sensor from this map view
    
            pair = None
            for i, t in enumerate(self.mSensorLayerList):
                if t[0] == sensor:
                    pair = t
                    break
            assert pair is not None, 'Sensor "{}" not found'.format(sensor.name())
            self.mLayerTreeSensorNode.removeLayer(pair[1])
            self.mSensorLayerList.remove(pair)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
        def hasSensor(self, sensor)->bool:
            """
            :param sensor:
            :return:
            """
            assert isinstance(sensor, SensorInstrument)
            return sensor in self.sensors()
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    class MapViewListModel(QAbstractListModel):
        """
        A model to store a list of map views.
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        """
        sigMapViewsAdded = pyqtSignal(list)
        sigMapViewsRemoved = pyqtSignal(list)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def __init__(self, parent=None):
            super(MapViewListModel, self).__init__(parent)
            self.mMapViewList = []
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def addMapView(self, mapView):
            i = len(self.mMapViewList)
            self.insertMapView(i, mapView)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def insertMapView(self, i, mapView):
            self.insertMapViews(i, [mapView])
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def insertMapViews(self, i, mapViews):
            assert isinstance(mapViews, list)
            assert i >= 0 and i <= len(self.mMapViewList)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            self.beginInsertRows(QModelIndex(), i, i + len(mapViews) - 1)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            for j in range(len(mapViews)):
                mapView = mapViews[j]
                assert isinstance(mapView, MapView)
                mapView.sigTitleChanged.connect(
                    lambda : self.doRefresh([mapView])
                )
                self.mMapViewList.insert(i + j, mapView)
            self.endInsertRows()
            self.sigMapViewsAdded.emit(mapViews)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def doRefresh(self, mapViews):
            for mapView in mapViews:
                idx = self.mapView2idx(mapView)
                self.dataChanged.emit(idx, idx)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def removeMapView(self, mapView):
            self.removeMapViews([mapView])
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def removeMapViews(self, mapViews):
            assert isinstance(mapViews, list)
            for mv in mapViews:
                assert mv in self.mMapViewList
                idx = self.mapView2idx(mv)
                self.beginRemoveRows(idx.parent(), idx.row(), idx.row())
                self.mMapViewList.remove(mv)
                self.endRemoveRows()
            self.sigMapViewsRemoved.emit(mapViews)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def rowCount(self, parent=None, *args, **kwargs):
            return len(self.mMapViewList)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def columnCount(self, QModelIndex_parent=None, *args, **kwargs):
            return 1
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def idx2MapView(self, index):
            if isinstance(index, QModelIndex):
                if index.isValid():
                    index = index.row()
                else:
                    return None
            assert index >= 0 and index < len(self.mMapViewList)
            return self.mMapViewList[index]
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def mapView2idx(self, mapView):
            assert isinstance(mapView, MapView)
            row = self.mMapViewList.index(mapView)
            return self.createIndex(row, 0, mapView)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def __len__(self):
            return len(self.mMapViewList)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def __iter__(self):
            return iter(self.mMapViewList)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def __getitem__(self, slice):
            return self.mMapViewList[slice]
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def data(self, index, role=Qt.DisplayRole):
            if not index.isValid():
                return None
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            if (index.row() >= len(self.mMapViewList)) or (index.row() < 0):
                return None
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            mapView = self.idx2MapView(index)
            assert isinstance(mapView, MapView)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            value = None
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            if role == Qt.DisplayRole:
                value = '{} {}'.format(index.row() +1 , mapView.title())
            #if role == Qt.DecorationRole:
                #value = classInfo.icon(QSize(20,20))
            if role == Qt.UserRole:
                value = mapView
            return value
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    class MapViewDock(QgsDockWidget, loadUI('mapviewdock.ui')):
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        sigMapViewAdded = pyqtSignal(MapView)
        sigMapViewRemoved = pyqtSignal(MapView)
        sigShowProfiles = pyqtSignal(SpatialPoint, MapCanvas, str)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        sigMapCanvasColorChanged = pyqtSignal(QColor)
        sigSpatialExtentChanged = pyqtSignal(SpatialExtent)
        sigCrsChanged = pyqtSignal(QgsCoordinateReferenceSystem)
        sigMapSizeChanged = pyqtSignal(QSize)
    
        sigMapsPerMapViewChanged = pyqtSignal(int)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def setTimeSeries(self, timeSeries:TimeSeries):
            assert isinstance(timeSeries, TimeSeries)
            self.mTimeSeries = timeSeries
            self.mTimeSeries.sigSensorAdded.connect(self.addSensor)
            self.mTimeSeries.sigSensorRemoved.connect(self.removeSensor)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def __init__(self, parent=None):
            super(MapViewDock, self).__init__(parent)
            self.setupUi(self)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            self.baseTitle = self.windowTitle()
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            self.btnAddMapView.setDefaultAction(self.actionAddMapView)
            self.btnRemoveMapView.setDefaultAction(self.actionRemoveMapView)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            self.btnCrs.crsChanged.connect(self.sigCrsChanged)
            self.btnMapCanvasColor.colorChanged.connect(self.onMapCanvasBackgroundColorChanged)
            self.btnApplySizeChanges.clicked.connect(lambda : self.sigMapSizeChanged.emit(QSize(self.spinBoxMapSizeX.value(),self.spinBoxMapSizeY.value())))
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            self.actionAddMapView.triggered.connect(self.createMapView)
            self.actionRemoveMapView.triggered.connect(lambda : self.removeMapView(self.currentMapView()) if self.currentMapView() else None)
            self.actionApplyStyles.triggered.connect(self.refreshCurrentMapView)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            self.toolBox.currentChanged.connect(self.onToolboxIndexChanged)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            self.spinBoxMapSizeX.valueChanged.connect(lambda: self.onMapSizeChanged('X'))
            self.spinBoxMapSizeY.valueChanged.connect(lambda: self.onMapSizeChanged('Y'))
            self.mLastMapSize = self.mapSize()
    
            self.sbMpMV.valueChanged.connect(self.sigMapsPerMapViewChanged)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            self.mTimeSeries = None
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def mapViews(self)->list:
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            Returns the defined MapViews
            :return: [list-of-MapViews]
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            assert isinstance(self.toolBox, QToolBox)
            mapViews = []
            for i in range(self.toolBox.count()):
                item = self.toolBox.widget(i)
                if isinstance(item, MapView):
                    mapViews.append(item)
            return mapViews
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def mapCanvases(self)->list:
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            Returns all MapCanvases from all MapViews
            :return: [list-of-MapCanvases]
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            maps = []
            for mapView in self.mapViews():
                assert isinstance(mapView, MapView)
                maps.extend(mapView.mapCanvases())
            return maps
    
        def setCrs(self, crs):
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            if isinstance(crs, QgsCoordinateReferenceSystem):
                old = self.btnCrs.crs()
                if old != crs:
                    self.btnCrs.setCrs(crs)
                    self.btnCrs.setLayerCrs(crs)
    
        def mapsPerMapView(self)->int:
            return self.sbMpMV.value()
    
        def setMapsPerMapView(self, n:int):
            assert n > 0
            self.sbMpMV.setValue(n)
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def setMapSize(self, size):
            assert isinstance(size, QSize)
            ws = [self.spinBoxMapSizeX, self.spinBoxMapSizeY]
            oldSize = self.mapSize()
            b = oldSize != size
            for w in ws:
                w.blockSignals(True)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            self.spinBoxMapSizeX.setValue(size.width()),
            self.spinBoxMapSizeY.setValue(size.height())
            self.mLastMapSize = QSize(size)
            for w in ws:
                w.blockSignals(False)
            self.mLastMapSize = QSize(size)
            if b:
                self.sigMapSizeChanged.emit(size)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def onMapSizeChanged(self, dim):
            newSize = self.mapSize()
            #1. set size of other dimension accordingly
            if dim is not None:
                if self.checkBoxKeepSubsetAspectRatio.isChecked():
                    if dim == 'X':
                        vOld = self.mLastMapSize.width()
                        vNew = newSize.width()
                        targetSpinBox = self.spinBoxMapSizeY
                    elif dim == 'Y':
                        vOld = self.mLastMapSize.height()
                        vNew = newSize.height()
                        targetSpinBox = self.spinBoxMapSizeX
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                    oldState = targetSpinBox.blockSignals(True)
                    targetSpinBox.setValue(int(round(float(vNew) / vOld * targetSpinBox.value())))
                    targetSpinBox.blockSignals(oldState)
                    newSize = self.mapSize()
                if newSize != self.mLastMapSize:
                    self.btnApplySizeChanges.setEnabled(True)
            else:
                self.sigMapSizeChanged.emit(self.mapSize())
                self.btnApplySizeChanges.setEnabled(False)
            self.setMapSize(newSize)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            return QSize(self.spinBoxMapSizeX.value(),
                         self.spinBoxMapSizeY.value())
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def refreshCurrentMapView(self, *args):
            mv = self.currentMapView()
            if isinstance(mv, MapView):
                mv.refreshMapView()
            else:
                s  =""
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def dummySlot(self):
            s  =""
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def onMapViewsRemoved(self, mapViews):
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            for mapView in mapViews:
                idx = self.stackedWidget.indexOf(mapView.ui)
                if idx >= 0:
                    self.stackedWidget.removeWidget(mapView.ui)
                    mapView.ui.close()
                else:
                    s = ""
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            self.actionRemoveMapView.setEnabled(len(self.mMapViews) > 0)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def mapBackgroundColor(self)->QColor:
            """
            Returns the map canvas background color
            :return: QColor
            """
            return self.btnMapCanvasColor.color()
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def setMapBackgroundColor(self, color:QColor):
            """
            Sets the MapCanvas background color
            :param color: QColor
            """
            if color != self.mapBackgroundColor():
                self.btnMapCanvasColor.setColor(color)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def onMapCanvasBackgroundColorChanged(self, color:QColor):
            """
            Reacts on a changes map color
            :param color: QColor
            """
            assert isinstance(color, QColor)
            self.mColor = color
            for mapCanvas in self.mapCanvases():
                assert isinstance(mapCanvas, MapCanvas)
                mapCanvas.addToRefreshPipeLine(color)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            self.sigMapCanvasColorChanged.emit(color)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def onMapViewsAdded(self, mapViews):
            nextShown = None
            for mapView in mapViews:
                mapView.sigTitleChanged.connect(self.updateTitle)
                self.stackedWidget.addWidget(mapView.ui)
                if nextShown is None:
                    nextShown = mapView
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                contents = mapView.ui.scrollAreaWidgetContents
                size = contents.size()
                hint = contents.sizeHint()
                #mapView.ui.scrollArea.update()
                s = ""
                #setMinimumSize(mapView.ui.scrollAreaWidgetContents.sizeHint())
                #hint = contents.sizeHint()
                #contents.setMinimumSize(hint)
            if isinstance(nextShown, MapView):
                self.setCurrentMapView(nextShown)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            for mapView in mapViews:
                self.sigMapViewAdded.emit(mapView)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def updateButtons(self, *args):
            b = len(self.mMapViews) > 0
            self.actionRemoveMapView.setEnabled(b)
            self.actionApplyStyles.setEnabled(b)
            self.actionHighlightMapView.setEnabled(b)
    
        def createMapView(self, name:str=None)->MapView:
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            """
            Create a new MapView
            :return: MapView
            """
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            mapView = MapView()
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            n = len(self.mapViews()) + 1
    
            if isinstance(name, str) and len(name) > 0:
                title = name
            else:
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                title = 'Map View {}'.format(n)
    
                while title in [m.title() for m in self.mapViews()]:
                    n += 1
                    title = 'Map View {}'.format(n)
    
            if n == 1:
                mapView.optionShowDate.setChecked(True)
                mapView.optionShowSensorName.setChecked(True)
    
            mapView.setTitle(title)
            mapView.sigShowProfiles.connect(self.sigShowProfiles)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            mapView.setTimeSeries(self.mTimeSeries)
            self.addMapView(mapView)
            return mapView
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
        def addMapView(self, mapView:MapView):
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            Adds a MapView
            :param mapView: MapView
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            assert isinstance(mapView, MapView)
            mapView.sigTitleChanged.connect(lambda *args, mv=mapView : self.onMapViewUpdated(mv))