Newer
Older
# -*- coding: utf-8 -*-
"""
/***************************************************************************

Benjamin Jakimow
committed
EO Time Series Viewer
-------------------
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
import sys
import re
import fnmatch
import collections
import copy
from qgis.PyQt.QtCore import \
Qt, QSize, pyqtSignal, QModelIndex, QTimer, QAbstractListModel, QMargins
from qgis.PyQt.QtGui import \
QColor, QIcon, QGuiApplication, QMouseEvent
from qgis.PyQt.QtWidgets import \
QWidget, QLayoutItem, QFrame, QLabel, QGridLayout, QSlider, QMenu, \
QToolBox, QDialog, QAction, QSpinBox, QCheckBox, QLineEdit, QWidgetItem, QSpacerItem, QLayout
from qgis.PyQt.QtXml import \
QDomDocument, QDomNode, QDomElement
QgsCoordinateReferenceSystem, QgsVectorLayer, QgsTextFormat, QgsProject, \
QgsRectangle, QgsRasterRenderer, QgsMapLayerStore, QgsMapLayerStyle, \
QgsLayerTreeModel, QgsLayerTreeGroup, QgsPointXY, \
QgsLayerTree, QgsLayerTreeLayer, QgsReadWriteContext, QgsVector, \
QgsRasterLayer, QgsVectorLayer, QgsMapLayer, QgsMapLayerProxyModel, QgsColorRamp, \
QgsSingleBandPseudoColorRenderer, QgsExpressionContextGenerator, QgsExpressionContext, \
QgsExpressionContextUtils, QgsExpression, QgsExpressionContextScope
from qgis.gui import \
QgsDockWidget, QgsMapCanvas, QgsMapTool, QgsCollapsibleGroupBox, QgsLayerTreeView, \
QgisInterface, QgsLayerTreeViewMenuProvider, QgsLayerTreeMapCanvasBridge, \
QgsProjectionSelectionWidget, QgsMessageBar, QgsFieldExpressionWidget, QgsFilterLineEdit, \
QgsExpressionLineEdit, QgsExpressionBuilderDialog, QgsAttributeForm
from .externals.qps.crosshair.crosshair import getCrosshairStyle, CrosshairStyle, CrosshairMapCanvasItem
from .externals.qps.layerproperties import VectorLayerTools
from .externals.qps.maptools import MapTools
from .mapcanvas import MapCanvas, MapCanvasInfoItem, KEY_LAST_CLICKED
from .timeseries import SensorInstrument, TimeSeriesDate, TimeSeries, SensorProxyLayer
from .utils import loadUi, SpatialPoint, SpatialExtent, datetime64
from eotimeseriesviewer import DIR_UI
from eotimeseriesviewer.utils import fixMenuButtons
KEY_LOCKED_LAYER = 'eotsv/locked'
KEY_SENSOR_GROUP = 'eotsv/sensorgroup'
KEY_SENSOR_LAYER = 'eotsv/sensorlayer'
def equalTextFormats(tf1: QgsTextFormat, tf2: QgsTextFormat) -> True:
if not (isinstance(tf1, QgsTextFormat) and isinstance(tf2, QgsTextFormat)):
return False

Benjamin Jakimow
committed
return tf1.toMimeData().text() == tf2.toMimeData().text()
class MapViewLayerTreeModel(QgsLayerTreeModel):
"""
Layer Tree as shown in a MapView
"""
def __init__(self, rootNode, parent=None):
super(MapViewLayerTreeModel, self).__init__(rootNode, parent=parent)
class MapViewExpressionContextGenerator(QgsExpressionContextGenerator):
def __init__(self, *args, **kwds):
super().__init__(*args, **kwds)
self.mMapView: MapView = None
def setMapView(self, mapView):
self.mMapView = mapView
def createExpressionContext(self) -> QgsExpressionContext:
context = QgsExpressionContext([QgsExpressionContextUtils.projectScope(QgsProject.instance())])
if False and isinstance(self.mMapView, MapView):
canvas = self.mMapView.currentMapCanvas()
context.appendScope(canvas.expressionContextScope())
"""
A MapView defines how a single map canvas visualizes sensor specific EOTS data plus additional vector overlays
"""
sigCanvasAppearanceChanged = pyqtSignal()
sigCrosshairChanged = pyqtSignal()
sigTitleChanged = pyqtSignal(str)

benjamin.jakimow@geo.hu-berlin.de
committed
sigSensorRendererChanged = pyqtSignal(SensorInstrument, QgsRasterRenderer)
sigShowProfiles = pyqtSignal(SpatialPoint, MapCanvas, str)

benjamin.jakimow@geo.hu-berlin.de
committed
def __init__(self, name='Map View', parent=None):
super(MapView, self).__init__(parent)

Benjamin Jakimow
committed
from eotimeseriesviewer.settings import values, Keys, defaultValues, value
DEFAULT_VALUES = defaultValues()
self.mMapBackgroundColor: QColor = value(Keys.MapBackgroundColor,
default=DEFAULT_VALUES.get(Keys.MapBackgroundColor, QColor('black')))
self.mMapTextFormat: QgsTextFormat = value(Keys.MapTextFormat,
default=DEFAULT_VALUES.get(Keys.MapTextFormat, QgsTextFormat()))
self.mMapWidget = None
self.mLayerStyleInitialized: typing.Dict[SensorInstrument, bool] = dict()
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)
self.mDummyCanvas = QgsMapCanvas() # dummy map canvas for dummy layers
self.mDummyCanvas.setVisible(False)

benjamin.jakimow@geo.hu-berlin.de
committed
self.mLayerTree = QgsLayerTree()
self.mLayerTreeMapCanvasBridge = QgsLayerTreeMapCanvasBridge(self.mLayerTree, self.mDummyCanvas)

benjamin.jakimow@geo.hu-berlin.de
committed
# 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.mLayerTreeView.currentLayerChanged.connect(self.sigCurrentLayerChanged.emit)
self.mMapLayerTreeViewMenuProvider = MapViewLayerTreeViewMenuProvider(self, self.mLayerTreeView,
self.mDummyCanvas)
# 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
self.mDefaultInfoExpressionToolTip:str = self.tbInfoExpression.toolTip()
self.tbInfoExpression.textChanged.connect(self.onMapInfoExpressionChanged)
self.btnShowInfoExpression.setDefaultAction(self.optionShowInfoExpression)
self.optionShowInfoExpression.toggled.connect(self.tbInfoExpression.setEnabled)
self.optionShowInfoExpression.toggled.connect(self.actionSetInfoExpression.setEnabled)
self.btnSetInfoExpression.setDefaultAction(self.actionSetInfoExpression)
self.actionSetInfoExpression.triggered.connect(self.onSetInfoExpression)
self._fakeLyr: QgsVectorLayer = QgsVectorLayer("point?crs=epsg:4326", "Scratch point layer", "memory")

Benjamin Jakimow
committed
# self.mExpressionContextGenerator = MapViewExpressionContextGenerator()
# self.mExpressionContextGenerator.setMapView(self)
self.tbInfoExpression.setEnabled(self.optionShowInfoExpression.isChecked())
# self.tbInfoExpression.registerExpressionContextGenerator(self.mExpressionContextGenerator)
self.optionShowInfoExpression.toggled.connect(self.sigCanvasAppearanceChanged)
# self.tbInfoExpression.expressionChanged.connect(self.sigCanvasAppearanceChanged)
for action in m.actions():
action.toggled.connect(self.sigCanvasAppearanceChanged)
fixMenuButtons(self)
def setInfoExpressionError(self, error: str):
if error in ['', None]:
self.tbInfoExpression.setStyleSheet('')
self.tbInfoExpression.setToolTip(self.mDefaultInfoExpressionToolTip)
else:
self.tbInfoExpression.setStyleSheet('QLineEdit#tbInfoExpression{color:red; border: 2px solid red;}')
self.tbInfoExpression.setToolTip(f'<span style="color:red">{error}</span>')
def onMapInfoExpressionChanged(self, text: str):
self.sigCanvasAppearanceChanged.emit()
s = ""
def onSetInfoExpression(self, *args):
context = QgsExpressionContext(QgsExpressionContextUtils.globalProjectLayerScopes(self._fakeLyr))
c = self.currentMapCanvas()
if isinstance(c, MapCanvas):
context.appendScope(QgsExpressionContextScope(c.expressionContextScope()))
expression = self.tbInfoExpression.text()
# taken from qgsfeaturefilterwidget.cpp : void QgsFeatureFilterWidget::filterExpressionBuilder()
dlg = QgsExpressionBuilderDialog(self._fakeLyr, expression,
self,
'generic', context)
dlg.setWindowTitle('Expression Based Filter')
# myDa = QgsDistanceArea()
# myDa.setSourceCrs(self.mLayer.crs(), QgsProject.instance().transformContext())
# myDa.setEllipsoid(QgsProject.instance().ellipsoid())
# dlg.setGeomCalculator(myDa)
if dlg.exec() == QDialog.Accepted:
self.tbInfoExpression.setText(dlg.expressionText())
def __iter__(self) -> typing.Iterator[TimeSeriesDate]:
return iter(self.mapCanvases())
@staticmethod
def readXml(node: QDomNode):
if node.nodeName() == 'MapView':
nodeMapView = node
else:
nodeMapView = node.firstChildElement('MapView')
if nodeMapView.nodeName() != 'MapView':
return None
context = QgsReadWriteContext()
mapView = MapView()
def to_bool(value) -> bool:
return str(value).lower() in ['1', 'true']
mapView.setName(nodeMapView.attribute('name'))
mapView.setMapBackgroundColor(QColor(nodeMapView.attribute('bg')))
mapView.setVisibility(to_bool(nodeMapView.attribute('visible')))
# mapView.optionShowDate.setChecked(to_bool(nodeMapView.attribute('showDate')))
# mapView.optionShowSensorName.setChecked(to_bool(nodeMapView.attribute('showSensorName')))
# mapView.optionShowMapViewName.setChecked(to_bool(nodeMapView.attribute('showMapViewName')))
# nodeMapView.setAttribute('showDate', str(self.optionShowDate.checked()))
# nodeMapView.setAttribute('showSensorName', str(self.optionShowSensorName.checked()))
# nodeMapView.setAttribute('showMapViewName', str(self.optionShowMapViewName.checked()))
textFormat = mapView.mapTextFormat()
textFormat.readXml(nodeMapView, context)
lyrNode = node.firstChildElement('MapViewProxyLayer').toElement()
while lyrNode.nodeName() == 'MapViewProxyLayer':
sid = lyrNode.attribute('sensor_id')
styleNode = lyrNode.firstChildElement('LayerStyle')
style = QgsMapLayerStyle()
style.readXml(styleNode)
sensor = SensorInstrument(sid)
mapView.addSensor(sensor)
lyr = mapView.sensorProxyLayer(sensor)
lyr.setMapLayerStyle(style)
lyrNode = lyrNode.nextSiblingElement()
return mapView
def writeXml(self, node: QDomNode, doc: QDomDocument):
nodeMapView = doc.createElement('MapView')
nodeMapView.setAttribute('name', self.name())
nodeMapView.setAttribute('bg', self.mapBackgroundColor().name())
nodeMapView.setAttribute('visible', str(self.isVisible()))
nodeMapView.setAttribute('infoexpression', self.mapInfoExpression())
context = QgsReadWriteContext()
nodeTextStyle = self.mapTextFormat().writeXml(doc, context)
nodeMapView.appendChild(nodeTextStyle)
for sensor in self.sensors():
lyr = self.sensorProxyLayer(sensor)
if isinstance(lyr, SensorProxyLayer):
sensorNode = doc.createElement('MapViewProxyLayer')
sensorNode.setAttribute('sensor_id', sensor.id())
style: QgsMapLayerStyle = lyr.mapLayerStyle()
styleNode = doc.createElement('LayerStyle')
style.writeXml(styleNode)
sensorNode.appendChild(styleNode)
nodeMapView.appendChild(sensorNode)
node.appendChild(nodeMapView)
self.setTitle(name)
return self.title()
def setMapInfoExpression(self, expression: str):
self.tbInfoExpression.setText(expression)
def mapInfoExpression(self) -> str:
return f'{self.tbInfoExpression.text()}'.strip()
def setMapTextFormat(self, textformat: QgsTextFormat) -> QgsTextFormat:

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

Benjamin Jakimow
committed
return self.mMapTextFormat
"""
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
"""
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)
"""
Sets the QgsMapCanvas.currentLayer() that is used by some QgsMapTools
:param layer: QgsMapLayer | None
:return:
"""
assert layer is None or isinstance(layer, QgsMapLayer)
if layer in self.layers():
self.mLayerTreeView.setCurrentLayer(layer)
if layer not in self.mSensorLayerList:
for c in self.mapCanvases():
c.setCurrentLayer(layer)
def addSpectralProfileLayers(self):
"""Adds the EOTSV Spectral Profile Layers"""
from eotimeseriesviewer.main import EOTimeSeriesViewer
tsv = EOTimeSeriesViewer.instance()
if isinstance(tsv, EOTimeSeriesViewer):
for lyr in tsv.spectralLibraries():
if lyr not in self.layers():
self.addLayer(lyr)
def addTemporalProfileLayer(self):
"""Adds the EOTSV Temporal Profile Layer"""
from eotimeseriesviewer.main import EOTimeSeriesViewer
tsv = EOTimeSeriesViewer.instance()
if isinstance(tsv, EOTimeSeriesViewer):
lyr = tsv.temporalProfileLayer()
if lyr not in self.layers():
self.addLayer(lyr)
"""
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
committed
"""
Sets the map view visibility
:param b: bool
"""
if self.actionToggleMapViewHidden.isChecked() == b:
self.actionToggleMapViewHidden.setChecked(not b)
self.sigCanvasAppearanceChanged.emit()

Benjamin Jakimow
committed
"""
Returns the map view visibility
:return: bool
"""
return not self.actionToggleMapViewHidden.isChecked()

Benjamin Jakimow
committed
"""
Returns the MapCanvases related to this map view. Requires that this mapview was added to a MapWidget

Benjamin Jakimow
committed
:return: [list-of-MapCanvases]
"""
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())
self.sigCanvasAppearanceChanged.emit()
def setMapWidget(self, w):
if isinstance(w, MapWidget):
self.mMapWidget = w
else:
self.mMapWidget = None
"""
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)
"""
Returns the TimeSeries this mapview is connected with
:return: TimeSeries
"""
return self.mTimeSeries
"""
Sets the widget title
:param title: str
"""
self.tbName.setText(title)

benjamin.jakimow@geo.hu-berlin.de
committed
"""
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 layers(self) -> typing.List[QgsMapLayer]:
"""
Returns all layers, including invisible or proxy layers for time-series data
:return: [list-of-QgsMapLayers]
"""
nodes = self.mLayerTree.findLayers()
return [n.layer() for n in nodes if isinstance(n.layer(), QgsMapLayer)]
"""
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:
"""

Benjamin Jakimow
committed
Seths the CrosshairStyle of this MapView
:param crosshairStyle: CrosshairStyle
"""
if self.mCrossHairStyle != crosshairStyle:
self.mCrossHairStyle = crosshairStyle
self.sigCrosshairChanged.emit()
return self.mCrossHairStyle

benjamin.jakimow@geo.hu-berlin.de
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
"""

benjamin.jakimow@geo.hu-berlin.de
committed
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))

benjamin.jakimow@geo.hu-berlin.de
committed
else:
for mapCanvas in self.mapCanvases():
mapCanvas.setStyleSheet(styleOff)
"""
Returns the MapCanvas that was clicked / used last
:return: MapCanvas
"""
if not isinstance(self.mMapWidget, MapWidget):
return None
canvases = sorted(self.mMapWidget.mapViewCanvases(self),
if len(canvases) == 0:
return None
else:
return canvases[-1]
Returns the current map layer, i.e. that selected in the map layer tree view.
If this is a proxy layer, the MapView will try to return a "real" layer from a MapCanvas
:return: QgsMapLayer
cl = self.mLayerTreeView.currentLayer()
if isinstance(cl, SensorProxyLayer):
sensor = cl.sensor()

Benjamin Jakimow
committed
canvases = [c for c in self.mapCanvases() if c.tsd().sensor() == sensor]
canvases = sorted(canvases, key=lambda c: c is not self.currentMapCanvas())
for c in canvases:
for l in c.layers():
if isinstance(l, SensorProxyLayer):
return l
return cl

Benjamin Jakimow
committed
"""
Returns the CrosshairStyle
:return: CrosshairStyle

Benjamin Jakimow
committed
"""
return self.mCrossHairStyle

Benjamin Jakimow
committed
"""
Enables / diables the map canvas crosshair.
:param b: bool

Benjamin Jakimow
committed
"""
if b != self.actionToggleCrosshairVisibility.isChecked():
self.actionToggleCrosshairVisibility.setChecked(b)

Benjamin Jakimow
committed
else:
self.mCrossHairStyle.setVisibility(b)
self.sigCrosshairChanged.emit()
def sensorProxyLayers(self) -> typing.List[SensorProxyLayer]:
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:

benjamin.jakimow@geo.hu-berlin.de
committed
"""
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 sensorLayers(self, sensor: SensorInstrument) -> typing.List[SensorProxyLayer]:
"""
:param sensor:
:return:
"""
layers = []
for c in self.mapCanvases():
for l in c.layers():
if isinstance(l, SensorProxyLayer) and l.sensor() == sensor:
layers.append(l)
return layers
def sensors(self) -> typing.List[SensorInstrument]:
"""
Returns a list of SensorsInstruments
:return: [list-of-SensorInstruments]
"""
return [t[0] for t in self.mSensorLayerList]
"""
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)
masterLayer: SensorProxyLayer = sensor.proxyRasterLayer()
assert isinstance(masterLayer.renderer(), QgsRasterRenderer)
self.mSensorLayerList.append((sensor, masterLayer))
masterLayer.styleChanged.connect(lambda *args, v=self, l=masterLayer: self.onMasterStyleChanged(l))
masterLayer.nameChanged.connect(self.onMasterLyrNameChanged)
layerTreeLayer: QgsLayerTreeLayer = self.mLayerTreeSensorNode.addLayer(masterLayer)
layerTreeLayer.setCustomProperty(KEY_LOCKED_LAYER, True)
layerTreeLayer.setCustomProperty(KEY_SENSOR_LAYER, True)
dummyLayers = self.mDummyCanvas.layers() + [masterLayer]
self.mDummyCanvas.setLayers(dummyLayers)
def onMasterLyrNameChanged(self, *args):
lyr = self.sender()
newname = lyr.name()
ltn = self.mLayerTreeSensorNode.findLayer(lyr)
def onMasterStyleChanged(self, masterLayer: SensorProxyLayer):
sensor: SensorInstrument = masterLayer.sensor()
style: QgsMapLayerStyle = masterLayer.mapLayerStyle()
# print('### MASTER-STYLE-CHANGED')
# print(style.xmlData())
for lyr in self.sensorLayers(sensor):
lyr.setMapLayerStyle(style)
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 \
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

benjamin.jakimow@geo.hu-berlin.de
committed
:param sensor:
:return:
"""
if sensor in self.mLayerStyleInitialized.keys():
self.mLayerStyleInitialized.pop(sensor)
toRemove = []
for t in self.mSensorLayerList:
toRemove.append(t)
for t in toRemove:
self.mLayerTreeSensorNode.removeLayer(t[1])
self.mSensorLayerList.remove(t)

benjamin.jakimow@geo.hu-berlin.de
committed
def hasSensor(self, sensor: SensorInstrument) -> bool:
"""
:param sensor:
:return:
"""
assert isinstance(sensor, SensorInstrument)
return sensor in self.sensors()

benjamin.jakimow@geo.hu-berlin.de
committed
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: QgsLayerTreeView = view
self.mDummyCanvas: QgsMapCanvas = 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)
# self.actionAddEOTSVSpectralProfiles = QAction('Add Spectral Profile Layer')
# self.actionAddEOTSVTemporalProfiles = QAction('Add Temporal Profile Layer')
def mapView(self) -> MapView:
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 onRemoveLayers(self):
selected = self.layerTreeView().selectedLayers()
for l in selected:
if not isinstance(l, SensorProxyLayer):
self.mapView().mLayerTree.removeLayer(l)
def onSetCanvasCRS(self):
s = ""
lyr = self.layerTreeView()
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
extent = SpatialExtent.fromLayer(layer)
if isinstance(extent, SpatialExtent):
extent = extent.toCrs(self.mapView().mapWidget().crs())
self.mapView().mapWidget().setSpatialExtent(extent)
def onZoomActualSize(self):
current = self.mapView().currentLayer()
if isinstance(current, QgsRasterLayer):
s = ""
def onStretchToExtent(self):
current = self.mapView().currentLayer()
canvas = self.mapView().currentMapCanvas()
if not isinstance(canvas, MapCanvas):
return
if isinstance(current, SensorProxyLayer):
for l in canvas.layers():
if isinstance(l, SensorProxyLayer) and l.sensor() == current.sensor():
canvas.stretchToExtent(layer=current)
break
elif isinstance(current, QgsRasterLayer):
canvas.stretchToExtent(layer=current)
def createContextMenu(self) -> QMenu:
model = self.layerTreeModel()
ltree = self.layerTree()
view: QgsLayerTreeView = self.layerTreeView()
currentGroup = view.currentGroupNode()
currentLayer = view.currentLayer()
isSensorGroup = isinstance(currentGroup, QgsLayerTreeGroup) and currentGroup.customProperty(
KEY_SENSOR_GROUP) in [True, 'true']
isSensorLayer = isinstance(currentLayer, SensorProxyLayer)
mv: MapView = self.mapView()
mw: MapWidget = mv.mapWidget()
mw.setCurrentMapView(mv)
if isSensorLayer:
# the current layer is an "empty" proxy layer. use one from a visible map canvas instead
pass
from eotimeseriesviewer.main import EOTimeSeriesViewer
eotsv = EOTimeSeriesViewer.instance()
if not isinstance(eotsv, EOTimeSeriesViewer):
return
menu = QMenu(view)
a = menu.addAction('Rename')
a.triggered.connect(lambda *args, cidx=currentIndex: view.edit(cidx))
menu.addSeparator()
# zoom to layer
menu.addAction(eotsv.actionZoomToLayer())
# rename layer
#
# zoom to native resolution
# in this case not using a map tool but the current layer
ref = eotsv.actionZoomActualSize()
a = menu.addAction(ref.text())
a.setIcon(ref.icon())
a.triggered.connect(self.onZoomActualSize)
if isinstance(currentLayer, QgsRasterLayer):
a = menu.addAction('&Stretch Using Current Extent')
a.triggered.connect(self.onStretchToExtent)
# ----
menu.addSeparator()
a = menu.addAction('Add Spectral Library Layer')
a.triggered.connect(self.mapView().addSpectralProfileLayers)
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
a = menu.addAction('Add Temporal Profile Layer')
a.triggered.connect(self.mapView().addTemporalProfileLayer)
# ----
menu.addSeparator()
# duplicate layer
# remove layer
a = menu.addAction('Remove layer')
a.setToolTip('Remove layer(s)')
a.triggered.connect(self.onRemoveLayers)
menu.addSeparator()
if isinstance(currentLayer, QgsVectorLayer):
menu.addAction(eotsv.actionOpenTable())
menu.addAction(eotsv.actionToggleEditing())
if isinstance(currentLayer, QgsRasterLayer) and not isinstance(currentLayer, SensorProxyLayer):
pass
menu.addSeparator()
# ----------
# set CRS
action = menu.addAction('Set layer CRS to map canvas')
action.triggered.connect(self.onSetCanvasCRS)
# ----
# Export...
# ------
menu.addSeparator()
# Styles...
menu.addAction(eotsv.actionPasteLayerStyle())
menu.addAction(eotsv.actionCopyLayerStyle())
# Properties
menu.addSeparator()
menu.addAction(eotsv.actionLayerProperties())
menu.addSeparator()
return menu
def vectorLayerTools(self) -> VectorLayerTools:
from eotimeseriesviewer.main import EOTimeSeriesViewer
return EOTimeSeriesViewer.instance().mVectorLayerTools
def showAttributeTable(self, lyr: QgsVectorLayer):
from eotimeseriesviewer.main import EOTimeSeriesViewer
tsv = EOTimeSeriesViewer.instance()
if isinstance(tsv, EOTimeSeriesViewer):
tsv.showAttributeTable(lyr)
s = ""
def onSetLayerProperties(self, lyr: QgsRasterLayer, canvas: QgsMapCanvas):
if isinstance(canvas, MapCanvas):
canvas.onSetLayerProperties(lyr)
class MapViewListModel(QAbstractListModel):
"""
A model to store a list of map views.

benjamin.jakimow@geo.hu-berlin.de
committed
"""
sigMapViewsAdded = pyqtSignal(list)
sigMapViewsRemoved = pyqtSignal(list)

benjamin.jakimow@geo.hu-berlin.de
committed
def __init__(self, parent=None):
super(MapViewListModel, self).__init__(parent)
self.mMapViewList = []

benjamin.jakimow@geo.hu-berlin.de
committed
def addMapView(self, mapView):
i = len(self.mMapViewList)
self.insertMapView(i, mapView)
def insertMapView(self, i, mapView):
self.insertMapViews(i, [mapView])
def insertMapViews(self, i, mapViews):
assert isinstance(mapViews, list)
assert i >= 0 and i <= len(self.mMapViewList)
self.beginInsertRows(QModelIndex(), i, i + len(mapViews) - 1)

benjamin.jakimow@geo.hu-berlin.de
committed