Skip to content
Snippets Groups Projects
mapvisualization.py 70.7 KiB
Newer Older
# 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, CrosshairMapCanvasItem
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'
def equalTextFormats(tf1:QgsTextFormat, tf2:QgsTextFormat)->True:
    return tf1.toMimeData().text() == tf2.toMimeData().text()
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
    """
    #sigVisibilityChanged = pyqtSignal(bool)
    sigCanvasAppearanceChanged = pyqtSignal()
    sigCrosshairChanged = pyqtSignal()
    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)
        from eotimeseriesviewer.settings import DEFAULT_VALUES, Keys
        self.mMapBackgroundColor = DEFAULT_VALUES[Keys.MapBackgroundColor]
        self.mMapTextFormat = DEFAULT_VALUES[Keys.MapTextFormat]
        self.mCurrentLayer = None
        self.mMapWidget = None
        self.mTimeSeries = None
        self.mSensorLayerList = list()
        self.mCrossHairStyle = CrosshairStyle()

        m = QMenu(self.btnToggleCrosshair)
        m.addAction(self.actionSetCrosshairStyle)
        self.btnToggleCrosshair.setMenu(m)
        self.btnToggleCrosshair.setDefaultAction(self.actionToggleCrosshairVisibility)
        self.btnToggleCrosshair.setChecked(self.crosshairStyle().mShow)
        self.btnToggleMapViewVisibility.setDefaultAction(self.actionToggleMapViewHidden)
        self.tbName.textChanged.connect(self.onTitleChanged)
        self.actionSetCrosshairStyle.triggered.connect(self.onChangeCrosshairStyle)
        self.actionToggleMapViewHidden.toggled.connect(self.sigCanvasAppearanceChanged)
        self.actionToggleCrosshairVisibility.toggled.connect(self.setCrosshairVisibility)

        self.actionAddMapLayer.triggered.connect(lambda *args: self.onAddMapLayer())
        self.actionAddVectorLayer.triggered.connect(lambda *args: self.onAddMapLayer(QgsMapLayerProxyModel.VectorLayer))
        self.actionAddRasterLayer.triggered.connect(lambda *args: self.onAddMapLayer(QgsMapLayerProxyModel.RasterLayer))
        self.btnAddLayer.setDefaultAction(self.actionAddMapLayer)
        m = QMenu()
        m.addAction(self.actionAddVectorLayer)
        m.addAction(self.actionAddRasterLayer)
        self.btnAddLayer.setMenu(m)
        self.btnHighlightMapView.setDefaultAction(self.actionHighlightMapView)
        self.actionHighlightMapView.triggered.connect(lambda: self.setHighlighted(True, timeout=500))
        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)

        for action in m.actions():
            action.toggled.connect(self.sigCanvasAppearanceChanged)

        fixMenuButtons(self)


    def setName(self, name:str):
        self.setTitle(name)

    def name(self)->str:
        return self.title()

    def setMapTextFormat(self, textformat:QgsTextFormat)->QgsTextFormat:

        if not equalTextFormats(self.mapTextFormat(), textformat):
            self.mMapTextFormat = textformat
            self.sigCanvasAppearanceChanged.emit()
        return self.mapTextFormat()

    def mapTextFormat(self)->QgsTextFormat:
        return self.mMapTextFormat

    def mapBackgroundColor(self)->QColor:
        """
        Returns the map background color
        :return: QColor
        """
        return self.mMapBackgroundColor

    def setMapBackgroundColor(self, color:QColor)->QColor:
        """
        Sets the map background color
        :param color: QColor
        :return: QColor
        """
        if self.mMapBackgroundColor != color:
            self.mMapBackgroundColor = color
            self.sigCanvasAppearanceChanged.emit()
        return self.mMapBackgroundColor


    def visibleMapCanvases(self)->list:
        """
        Returns the currently visible mapcanvases
        :return: [list-of-MapCanvases]
        """
        return [m for m in self.mapCanvases() if m.isVisibleToViewport()]

    def onAddMapLayer(self, filter:QgsMapLayerProxyModel.Filter=QgsMapLayerProxyModel.All):
        Slot that opens a SelectMapLayersDialog for any kind of layer
        """
        from .externals.qps.utils import SelectMapLayersDialog
        d = SelectMapLayersDialog()

        if filter == QgsMapLayerProxyModel.All:
            title = 'Select Layer'
            text = 'Layer'
        elif filter == QgsMapLayerProxyModel.RasterLayer:
            title = 'Select Raster Layer'
            text = 'Raster'
        elif filter == QgsMapLayerProxyModel.VectorLayer:
            title = 'Select Vector Layer'
            text = 'Vector'
        d.setWindowTitle(title)
        d.addLayerDescription(text, filter)
        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):

        canvases = self.mapCanvases()
        if len(canvases) > 0:
            mapCanvas = canvases[0]
        else:
            mapCanvas = None

        style = getCrosshairStyle(parent=self, crosshairStyle=self.crosshairStyle(), mapCanvas=mapCanvas)
        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

        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.sigCanvasAppearanceChanged.emit()
    def isVisible(self)->bool:
        """
        Returns the map view visibility
        :return: bool
        """
        return not self.actionToggleMapViewHidden.isChecked()
        Returns the MapCanvases related to this map view. Requires that this mapview was added to a MapWidget

        if isinstance(self.mMapWidget, MapWidget):
            return self.mMapWidget.mapViewCanvases(self)
        else:
            return []
    def onTitleChanged(self, *args):
        self.setWindowTitle('Map View "{}"'.format(self.title()))
        self.sigTitleChanged.emit(self.title())
        if self.optionShowMapViewName.isChecked():
            self.sigCanvasAppearanceChanged.emit()

    def setMapWidget(self, w):
        if isinstance(w, MapWidget):
            self.mMapWidget = w
        else:
            self.mMapWidget = None

    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)]
    def title(self, maskNewLines=True)->str:
        """
        Returns the MapView title
        :return: str
        """
        if maskNewLines:
            return self.tbName.text().replace('\\n', ' ').strip()
        else:
            return self.tbName.text().strip()
    def setCrosshairStyle(self, crosshairStyle:CrosshairStyle)->CrosshairStyle:
        :param crosshairStyle: CrosshairStyle
        """
        if self.mCrossHairStyle != crosshairStyle:
            self.mCrossHairStyle = crosshairStyle
            self.sigCrosshairChanged.emit()

        return self.mCrossHairStyle
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
    def setCrosshairVisibility(self, b:bool):
        Enables / diables the map canvas crosshair.
        :param b: bool
        if b != self.actionToggleCrosshairVisibility.isChecked():
            self.actionToggleCrosshairVisibility.setChecked(b)
            self.mCrossHairStyle.setVisibility(b)
            self.sigCrosshairChanged.emit()

    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():
            sensor.sigNameChanged.connect(self.sigCanvasAppearanceChanged)
            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)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        return [c for c in self.mapCanvases() if isinstance(c, MapCanvas) and \
                isinstance(c.tsd(), TimeSeriesDate) 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
        toRemove = []
        for t in self.mSensorLayerList:
                toRemove.append(t)

        for t in toRemove:
            self.mLayerTreeSensorNode.removeLayer(t[1])
            self.mSensorLayerList.remove(t)
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
class MapWidget(QFrame, loadUIFormClass(jp(DIR_UI, 'mapwidget.ui'))):
    """
    This widget contains all maps
    """

    class ViewMode(enum.Enum):

        MapViewByRows = 1,
        MapViewByCols = 2

Benjamin Jakimow's avatar
Benjamin Jakimow committed
    sigSpatialExtentChanged = pyqtSignal(SpatialExtent)
    sigCrosshairPositionChanged = pyqtSignal([SpatialPoint], [SpatialPoint, MapCanvas])
    sigCRSChanged = pyqtSignal(QgsCoordinateReferenceSystem)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
    sigMapSizeChanged = pyqtSignal(QSize)
    sigMapBackgroundColorChanged = pyqtSignal(QColor)
    sigMapTextColorChanged = pyqtSignal(QColor)
    sigMapTextFormatChanged = pyqtSignal(QgsTextFormat)
    sigMapsPerMapViewChanged = pyqtSignal(int)
    sigMapViewsChanged = pyqtSignal()
    sigMapViewAdded = pyqtSignal(MapView)
    sigMapViewRemoved = pyqtSignal(MapView)
    sigCurrentDateChanged = pyqtSignal(TimeSeriesDate)
    sigCurrentLocationChanged = pyqtSignal(SpatialPoint, MapCanvas)
    sigVisibleDatesChanged = pyqtSignal(list)
    sigViewModeChanged = pyqtSignal(ViewMode)
    def __init__(self, *args, **kwds):
        super(MapWidget, self).__init__(*args, **kwds)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        self.setupUi(self)
        self.setContentsMargins(1, 1, 1, 1)
        self.mGrid = self.gridFrame.layout()
        assert isinstance(self.mGrid, QGridLayout)
        self.mGrid.setSpacing(0)
        self.mGrid.setContentsMargins(0, 0, 0, 0)
        self.mMapLayerStore = QgsMapLayerStore()
        self.mMapViews = []
        self.mCanvases = dict()
        self.mCanvasSignals = dict()
        self.mMapToolKey = MapTools.Pan
        self.mViewMode = MapWidget.ViewMode.MapViewByRows
        self.mMpMV = 3
        self.mSpatialExtent = SpatialExtent.world()
        self.mCrs = self.mSpatialExtent.crs()
        self.mCurrentDate = None
        self.mCrosshairPosition = None
        self.mMapSize = QSize(200, 200)
        from eotimeseriesviewer.settings import DEFAULT_VALUES, Keys
        self.mMapTextFormat = DEFAULT_VALUES[Keys.MapTextFormat]
        self.mMapRefreshTimer = QTimer(self)
        self.mMapRefreshTimer.timeout.connect(self.timedRefresh)
        self.mMapRefreshTimer.setInterval(500)
        self.mMapRefreshTimer.start()
        self.btnFirst.setDefaultAction(self.actionFirstDate)
        self.btnLast.setDefaultAction(self.actionLastDate)
        self.btnBackward.setDefaultAction(self.actionBackward)
        self.btnForward.setDefaultAction(self.actionForward)
        self.btnBackwardFast.setDefaultAction(self.actionBackwardFast)
        self.btnForwardFast.setDefaultAction(self.actionForwardFast)

        self.actionFirstDate.triggered.connect(self.moveToFirstTSD)
        self.actionLastDate.triggered.connect(self.moveToLastTSD)
        self.actionBackward.triggered.connect(self.moveToPreviousTSD)
        self.actionForward.triggered.connect(self.moveToNextTSD)
        self.actionBackwardFast.triggered.connect(self.moveToPreviousTSDFast)
        self.actionForwardFast.triggered.connect(self.moveToNextTSDFast)

        self.mTimeSlider.setTickInterval(0)
        self.mTimeSlider.valueChanged.connect(self.onSliderReleased)



    def messageBar(self)->QgsMessageBar:
        """
        Returns the QgsMessageBar
        :return: QgsMessageBar
        """
        return self.mMessageBar

    def refresh(self):
        for c in self.mapCanvases():
            assert isinstance(c, MapCanvas)
            c.timedRefresh()
    def setMapTextFormat(self, textFormat:QgsTextFormat)->QgsTextFormat:

        if not equalTextFormats(textFormat, self.mMapTextFormat):
            self.mMapTextFormat = textFormat
            for mapView in self.mapViews():
                assert isinstance(mapView, MapView)
                mapView.setMapTextFormat(textFormat)
            self.sigMapTextFormatChanged.emit(self.mapTextFormat())
        return self.mapTextFormat()

    def mapTextFormat(self)->QgsTextFormat:
        return self.mMapTextFormat


    def setMapTool(self, mapToolKey:MapTools):
        if self.mMapToolKey != mapToolKey:
            self.mMapToolKey = mapToolKey
            for c in self.mapCanvases():
                assert isinstance(c, MapCanvas)
                mts = c.mapTools()
                mts.activate(self.mMapToolKey)
    def visibleTSDs(self)->list:
        """
        Returns the list of currently shown TimeSeriesDates.
        :return: [list-of-TimeSeriesDates]
        """
        for mv in self.mMapViews:
            tsds = []
            for c in self.mCanvases[mv]:
                if isinstance(c.tsd(), TimeSeriesDate):
                    tsds.append(c.tsd())
    def spatialExtent(self)->SpatialExtent:
        """
        Returns the current SpatialExtent
        :return: SpatialExtent
        """
        return self.mSpatialExtent
    def setSpatialExtent(self, extent:SpatialExtent)->SpatialExtent:
        """
        Sets a SpatialExtent to all MapCanvases.
        :param extent: SpatialExtent
        :return: SpatialExtent the current SpatialExtent
        """
        assert isinstance(extent, SpatialExtent)
        if self.mSpatialExtent != extent:
            self.mSpatialExtent = extent
            for c in self.mapCanvases():
                assert isinstance(c, MapCanvas)
                c.addToRefreshPipeLine(extent)
            self.sigSpatialExtentChanged.emit(self.mSpatialExtent.__copy__())
        return self.spatialExtent()
    def setSpatialCenter(self, centerNew:SpatialPoint):
        """
        Sets the spatial center of all MapCanvases
        :param centerNew: SpatialPoint
        """
        assert isinstance(centerNew, SpatialPoint)
        extent = self.spatialExtent()
        if isinstance(extent, SpatialExtent):
            centerOld = extent.center()
            centerNew = centerNew.toCrs(extent.crs())
            if centerNew != centerOld and isinstance(centerNew, SpatialPoint):
                extent = extent.__copy__()
                extent.setCenter(centerNew)
                self.setSpatialExtent(extent)
    def spatialCenter(self)->SpatialPoint:
        """
        Return the center of all map canvas
        :return: SpatialPoint
        """
        return self.spatialExtent().spatialCenter()
    def setCrs(self, crs:QgsCoordinateReferenceSystem)->QgsCoordinateReferenceSystem:
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        """
        Sets the MapCanvas CRS.
        :param crs: QgsCoordinateReferenceSystem
        :return: QgsCoordinateReferenceSystem
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        """
        self.mCrs = crs
        if isinstance(crs, QgsCoordinateReferenceSystem):
            for c in self.mapCanvases():
                c.setCrs(crs)