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, sys, re, fnmatch, collections, copy, traceback, bisect

benjamin.jakimow@geo.hu-berlin.de
committed
from qgis.core import *
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 *

benjamin.jakimow@geo.hu-berlin.de
committed
import numpy as np
from .utils import *
from .import Option, OptionListModel
from .timeseries import SensorInstrument, TimeSeriesDatum, TimeSeries, SensorProxyLayer
from .utils import loadUI
from .mapviewscrollarea import MapViewScrollArea
from .mapcanvas import MapCanvas, MapTools
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.mMapView = mapView
self.actionAddGroup = self.mDefActions.actionAddGroup()
self.actionRename = self.mDefActions.actionRenameGroupOrLayer()
self.actionRemove = self.mDefActions.actionRemoveGroupOrLayer()

Benjamin Jakimow
committed
#self.actionZoomToLayer = self.mDefActions.actionZoomToGroup(self.mDummyCanvas)
self.actionCheckAndAllChildren = self.mDefActions.actionCheckAndAllChildren()
self.actionShowFeatureCount = self.mDefActions.actionShowFeatureCount()

Benjamin Jakimow
committed
#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):
return self.mMapView
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
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)

Benjamin Jakimow
committed
#menu.addAction(self.actionZoomToGroup)
#menu.addAction(self.actionZoomToLayer)
#menu.addAction(self.actionZoomToSelected)
menu.addSeparator()
menu.addAction(self.actionAddEOTSVSpectralProfiles)
menu.addAction(self.actionAddEOTSVTemporalProfiles)

Benjamin Jakimow
committed
menu.addSeparator()
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:
showLayerPropertiesDialog(lyr, canvas))
a.setEnabled(isinstance(centerCanvas, QgsMapCanvas))

Benjamin Jakimow
committed
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))

Benjamin Jakimow
committed
a = menu.addAction('Paste Style')
a.setEnabled('application/qgis.style' in QApplication.clipboard().mimeData().formats())
a.triggered.connect(lambda *args, lyr=l: pasteStyleFromClipboard(lyr))
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
#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

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

benjamin.jakimow@geo.hu-berlin.de
committed
sigRemoveMapView = pyqtSignal(object)
sigMapViewVisibility = pyqtSignal(bool)
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)
self.setupUi(self)

Benjamin Jakimow
committed
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.actionAddOgrLayer.triggered.connect(self.onAddOgrLayer)
self.btnAddOgrLayer.setDefaultAction(self.actionAddOgrLayer)
self.btnHighlightMapView.setDefaultAction(self.actionHighlightMapView)
self.actionHighlightMapView.triggered.connect(lambda: self.setHighlighted(True, timeout=500))
self.mTimeSeries = None
self.mSensorLayerList = list()
self.mMapCanvases = list()
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.mLayerTreeMapCanvasBridget = 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.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

Benjamin Jakimow
committed
def visibleMapCanvases(self)->list:
"""
Returns the currently visible mapcanvases
:return: [list-of-MapCanvases]
"""
return [m for m in self.mapCanvases() if m.isVisibleToViewport()]
def onAddOgrLayer(self):
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)
else:
s = ""
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 = ""
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)
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
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
committed
"""
Sets the map view visibility
:param b: bool
"""
for mapCanvas in self.mapCanvases():
assert isinstance(mapCanvas, MapCanvas)
if self.actionToggleMapViewHidden.isChecked() == b:
self.actionToggleMapViewHidden.setChecked(not b)
if changed:
self.sigMapViewVisibility.emit(b)

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

Benjamin Jakimow
committed
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)
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
"""
self.tbName.setText(title)

benjamin.jakimow@geo.hu-berlin.de
committed
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)->str:
"""
Returns the MapView title
:return: str
"""
return self.tbName.text()

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

Benjamin Jakimow
committed
def refreshMapView(self, sensor=None):
for mapCanvas in self.mapCanvases():
if isinstance(mapCanvas, MapCanvas):
mapCanvas.refresh()
def setCrosshairStyle(self, crosshairStyle:CrosshairStyle):
"""

Benjamin Jakimow
committed
Seths the CrosshairStyle of this MapView
: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@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))
else:
for mapCanvas in self.mapCanvases():
mapCanvas.setStyleSheet(styleOff)
def registerMapCanvas(self, mapCanvas:MapCanvas):
"""
Registers a new MapCanvas to this MapView
:param sensor:
:param mapCanvas:
:return:
"""
from eotimeseriesviewer.mapcanvas import MapCanvas
assert isinstance(mapCanvas, MapCanvas)
mapCanvas.sigCrosshairVisibilityChanged.connect(self.setCrosshairVisibility)
mapCanvas.sigCrosshairStyleChanged.connect(self.setCrosshairStyle)
self.mMapCanvases.append(mapCanvas)
self.sigMapViewVisibility.connect(mapCanvas.setEnabled)

Benjamin Jakimow
committed
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):

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

Benjamin Jakimow
committed
"""
# set the action checked state first
if self.actionToggleCrosshairVisibility.isChecked() != b:
self.actionToggleCrosshairVisibility.setChecked(b)

Benjamin Jakimow
committed
else:
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.setCrosshairVisibility(b, emitSignal=False)

Benjamin Jakimow
committed
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:

benjamin.jakimow@geo.hu-berlin.de
committed
"""
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
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))

Benjamin Jakimow
committed
#QgsProject.instance().addMapLayer(dummyLayer)
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

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

benjamin.jakimow@geo.hu-berlin.de
committed
class DatumViewUI(QFrame, loadUI('timeseriesdatumview.ui')):
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
"""
Widget to host the MapCanvases of all map views that relate to a single Datum-Sensor combinbation.
"""
def __init__(self, title='<#>', parent=None):
super(DatumViewUI, self).__init__(parent)
self.setupUi(self)
def sizeHint(self):
m = self.layout().contentsMargins()
s = QSize(0, 0)
map = None
widgets = [self.layout().itemAt(i).widget() for i in range(self.layout().count())]
widgets = [w for w in widgets if isinstance(w, QWidget)]
maps = [w for w in widgets if isinstance(w, MapCanvas)]
others = [w for w in widgets if not isinstance(w, MapCanvas)]
s = self.layout().spacing()
m = self.layout().contentsMargins()
def totalHeight(widgetList):
total = QSize(0,0)
for w in widgetList:
ws = w.size()
if ws.width() == 0:
ws = w.sizeHint()
if w.isVisible():
total.setWidth(max([total.width(), ws.width()]))
total.setHeight(total.height() + ws.height())
return total
baseSize = totalHeight(widgets)
if baseSize.width() == 0:
for o in others:
baseSize.setWidth(9999)
s = QSize(baseSize.width() + m.left() + m.right(),
baseSize.height() + m.top() + m.bottom())
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
return s
"""
def sizeHint(self):
if not self.ui.isVisible():
return QSize(0,0)
else:
#return self.ui.sizeHint()
size = self.ui.sizeHint()
s = self.ui.layout().spacing()
m = self.ui.layout().contentsMargins()
dx = m.left() + m.right() + s
dy = self.ui.layout().spacing()
n = len([m for m in self.mapCanvases.keys() if m.isVisible()])
if n > 0:
baseSize = self.mapCanvases.values()[0].size()
size = QSize(baseSize.width()+ dx, \
size.height()+ (n+1)*(dy+2*s))
else:
s = ""
return size
"""

benjamin.jakimow@geo.hu-berlin.de
committed
sigRenderProgress = pyqtSignal(int,int)
sigLoadingStarted = pyqtSignal(MapView, TimeSeriesDatum)
sigLoadingFinished = pyqtSignal(MapView, TimeSeriesDatum)
sigVisibilityChanged = pyqtSignal(bool)
def __init__(self, timeSeriesDatum:TimeSeriesDatum, stv, parent=None):
assert isinstance(timeSeriesDatum, TimeSeriesDatum)

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

benjamin.jakimow@geo.hu-berlin.de
committed
self.ui.create()
self.showLoading(False)
self.wOffset = self.ui.layout().count()-1

benjamin.jakimow@geo.hu-berlin.de
committed
self.minHeight = self.ui.height()

benjamin.jakimow@geo.hu-berlin.de
committed
self.renderProgress = dict()
assert isinstance(stv, SpatialTemporalVisualization)
self.STV = stv
self.mSensor.sigNameChanged.connect(lambda :self.setColumnInfo())
self.TSD.sigVisibilityChanged.connect(self.setVisibility)

benjamin.jakimow@geo.hu-berlin.de
committed
self.setColumnInfo()
self.mRenderState = dict()

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

benjamin.jakimow@geo.hu-berlin.de
committed
def setColumnInfo(self):
labelTxt = '{}\n{}'.format(str(self.TSD.mDate), self.TSD.mSensor.name())
tooltip = '\n'.join([tss.uri()for tss in self.TSD.sources()])

benjamin.jakimow@geo.hu-berlin.de
committed
self.ui.labelTitle.setText(labelTxt)
self.ui.labelTitle.setToolTip(tooltip)

benjamin.jakimow@geo.hu-berlin.de
committed
def setVisibility(self, b):
self.ui.setVisible(b)
self.sigVisibilityChanged.emit(b)

benjamin.jakimow@geo.hu-berlin.de
committed
def setHighlighted(self, b=True, timeout=1000):
styleOn = """.QFrame {
border: 4px solid red;
border-radius: 4px;
}"""
styleOff = """"""
if b is True:
self.ui.setStyleSheet(styleOn)
if timeout > 0:
QTimer.singleShot(timeout, lambda : self.setHighlighted(b=False))
else:
self.ui.setStyleSheet(styleOff)

benjamin.jakimow@geo.hu-berlin.de
committed
def removeMapView(self, mapView):
self.ui.layout().removeWidget(canvas)

benjamin.jakimow@geo.hu-berlin.de
committed
canvas.close()
def mapCanvases(self)->list:
"""
Retuns the MapCanvases of this DataView
:return: [list-of-MapCanvases]
"""
return self.mMapCanvases.values()
def refresh(self):
"""
Refreshes the MapCanvases in this DatumView, if they are not hidden behind a scroll area.
"""

benjamin.jakimow@geo.hu-berlin.de
committed
if self.ui.isVisible():
for c in self.mapCanvases():

benjamin.jakimow@geo.hu-berlin.de
committed
if c.isVisible():

benjamin.jakimow@geo.hu-berlin.de
committed
def insertMapView(self, mapView):
assert isinstance(mapView, MapView)
from eotimeseriesviewer.mapcanvas import MapCanvas

benjamin.jakimow@geo.hu-berlin.de
committed
mapCanvas.setObjectName('MapCanvas {} {}'.format(mapView.title(), self.TSD.mDate))

Benjamin Jakimow
committed
self.registerMapCanvas(mapView, mapCanvas)
mapCanvas.setMapView(mapView)
mapCanvas.setTSD(self.TSD)
mapView.registerMapCanvas(mapCanvas)
mapCanvas.renderComplete.connect(lambda : self.onRenderingChange(False))
mapCanvas.renderStarting.connect(lambda : self.onRenderingChange(True))
#mapCanvas.sigMapRefreshed[float, float].connect(
# lambda dt: self.STV.TSV.ui.dockSystemInfo.addTimeDelta('Map {}'.format(self.mSensor.name()), dt))
#mapCanvas.sigMapRefreshed.connect(
# lambda dt: self.STV.TSV.ui.dockSystemInfo.addTimeDelta('All Sensors', dt))

benjamin.jakimow@geo.hu-berlin.de
committed
def showLoading(self, b):
if b:
self.ui.progressBar.setRange(0, 0)
self.ui.progressBar.setValue(-1)
else:
self.ui.progressBar.setRange(0,1)
self.ui.progressBar.setValue(0)
def onRenderingChange(self, b):
mc = self.sender()
#assert isinstance(mc, QgsMapCanvas)
self.mRenderState[mc] = b
self.showLoading(any(self.mRenderState.values()))
def onRendering(self, *args):
renderFlags = [m.renderFlag() for m in self.mMapCanvases.values()]
drawFlags = [m.isDrawing() for m in self.mMapCanvases.values()]

benjamin.jakimow@geo.hu-berlin.de
committed
# print((renderFlags, drawFlags))
isLoading = any(renderFlags)
self.showLoading(isLoading)
s = ""

benjamin.jakimow@geo.hu-berlin.de
committed
def registerMapCanvas(self, mapView, mapCanvas):
from eotimeseriesviewer.mapcanvas import MapCanvas

Benjamin Jakimow
committed
mapCanvas.setVisible(mapView.isVisible())
#mapView.sigTitleChanged.connect(lambda title : mapCanvas.setSaveFileName('{}_{}'.format(self.TSD.date, title)))
#mapCanvas.mapLayerModel().addMapLayerSources(self.TSD.qgsMimeDataUtilsUris())
#self.ui.layout().insertWidget(self.wOffset + len(self.mapCanvases), mapCanvas)
self.ui.layout().insertWidget(self.ui.layout().count() - 1, mapCanvas)
self.ui.update()
#register signals handled on (this) DV level
mapCanvas.renderStarting.connect(lambda: self.sigLoadingStarted.emit(mapView, self.TSD))
mapCanvas.mapCanvasRefreshed.connect(lambda: self.sigLoadingFinished.emit(mapView, self.TSD))
#mapCanvas.sigShowProfiles.connect(lambda c, t : mapView.sigShowProfiles.emit(c,mapCanvas, t))
#mapCanvas.sigChangeDVRequest.connect(self.onMapCanvasRequest)

benjamin.jakimow@geo.hu-berlin.de
committed
def __lt__(self, other):
assert isinstance(other, DatumView)
return self.TSD < other.TSD

benjamin.jakimow@geo.hu-berlin.de
committed
def __eq__(self, other):
assert isinstance(other, DatumView)
return self.TSD == other.TSD

benjamin.jakimow@geo.hu-berlin.de
committed
sigResizeRequired = pyqtSignal()
sigLoadingStarted = pyqtSignal(MapView, TimeSeriesDatum)
sigLoadingFinished = pyqtSignal(MapView, TimeSeriesDatum)
sigShowProfiles = pyqtSignal(SpatialPoint)
sigSpatialExtentChanged = pyqtSignal(SpatialExtent)
def __init__(self, STViz):
assert isinstance(STViz, SpatialTemporalVisualization)
super(DateViewCollection, self).__init__()
#self.tsv = tsv
#self.timeSeries = tsv.TS
self.mViews = list()
self.STV = STViz
self.ui = self.STV.targetLayout.parentWidget()
self.scrollArea = self.ui.parentWidget().parentWidget()
#potentially there are many more dates than views.
#therefore we implement the addinng/removing of mapviews here
#we reduce the number of layout refresh calls by
#suspending signals, adding the new map view canvases, and sending sigResizeRequired
self.STV.MVC.sigMapViewAdded.connect(self.addMapView)
self.STV.MVC.sigMapViewRemoved.connect(self.removeMapView)

benjamin.jakimow@geo.hu-berlin.de
committed
def tsdFromMapCanvas(self, mapCanvas):
assert isinstance(mapCanvas, MapCanvas)
for view in self.mViews:
assert isinstance(view, DatumView)
if mapCanvas in view.mMapCanvases.values():
return view.TSD
return None
def tsdView(self, tsd):
r = [v for v in self.mViews if v.TSD == tsd]
if len(r) == 1:
return r[0]
else:
raise Exception('TSD not in list')

benjamin.jakimow@geo.hu-berlin.de
committed
def addMapView(self, mapView):
assert isinstance(mapView, MapView)
w = self.ui
#w.setUpdatesEnabled(False)
#for tsdv in self.mViews:
# tsdv.ui.setUpdatesEnabled(False)
#for tsdv in self.mViews:
# tsdv.ui.setUpdatesEnabled(True)
#mapView.sigSensorRendererChanged.connect(lambda *args : self.setRasterRenderer(mapView, *args))
mapView.sigMapViewVisibility.connect(lambda: self.sigResizeRequired.emit())
mapView.sigShowProfiles.connect(self.sigShowProfiles.emit)
#w.setUpdatesEnabled(True)
def removeMapView(self, mapView):
assert isinstance(mapView, MapView)
for tsdv in self.mViews:
tsdv.removeMapView(mapView)
self.sigResizeRequired.emit()
def highlightDate(self, tsd):
"""
Highlights a time series data for a specific time our
:param tsd:
:return:
"""
tsdView = self.tsdView(tsd)
if isinstance(tsdView, DatumView):
tsdView.setHighlight(True)
def orderedViews(self):
#returns the
if self.focusView is not None:
assert isinstance(self.focusView, DatumView)
return sorted(self.mViews, key=lambda v: np.abs(v.TSD.date - self.focusView.TSD.date))
else:
return self.mViews
"""
def setSubsetSize(self, size):
assert isinstance(size, QSize)
self.subsetSize = size
for tsdView in self.orderedViews():
tsdView.blockSignals(True)
for tsdView in self.orderedViews():
tsdView.setSubsetSize(size)
for tsdView in self.orderedViews():
tsdView.blockSignals(False)
"""

Benjamin Jakimow
committed
for tsd in tsdList:
assert isinstance(tsd, TimeSeriesDatum)
DV = DatumView(tsd, self.STV, parent=self.ui)
DV.sigLoadingStarted.connect(self.sigLoadingStarted.emit)
DV.sigLoadingFinished.connect(self.sigLoadingFinished.emit)
DV.sigVisibilityChanged.connect(lambda: self.STV.adjustScrollArea())
for i, mapView in enumerate(self.STV.MVC):
DV.insertMapView(mapView)
bisect.insort(self.mViews, DV)
i = self.mViews.index(DV)
DV.ui.setParent(self.STV.targetLayout.parentWidget())
self.STV.targetLayout.insertWidget(i, DV.ui)
DV.ui.show()

Benjamin Jakimow
committed
def removeDates(self, tsdList):
toRemove = [v for v in self.mViews if v.TSD in tsdList]
removedDates = []
for DV in toRemove:
self.mViews.remove(DV)
for mapCanvas in DV.mMapCanvases.values():
toRemove = mapCanvas.layers()
mapCanvas.setLayers([])
toRemove = [l for l in toRemove if isinstance(l, QgsRasterLayer)]
if len(toRemove) > 0:
QgsProject.instance().removeMapLayers([l.id() for l in toRemove])
DV.ui.parent().layout().removeWidget(DV.ui)
DV.ui.hide()
DV.ui.close()
removedDates.append(DV.TSD)
del DV

Benjamin Jakimow
committed
if len(removedDates) > 0:
self.sigResizeRequired.emit()

Benjamin Jakimow
committed

Benjamin Jakimow
committed

Benjamin Jakimow
committed
def __getitem__(self, slice):
return self.mViews[slice]
def __delitem__(self, slice):
self.removeDates(self.mViews[slice])

benjamin.jakimow@geo.hu-berlin.de
committed
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
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@geo.hu-berlin.de
committed
def doRefresh(self, mapViews):
for mapView in mapViews:
idx = self.mapView2idx(mapView)
self.dataChanged.emit(idx, idx)

benjamin.jakimow@geo.hu-berlin.de
committed
def removeMapView(self, mapView):
self.removeMapViews([mapView])

benjamin.jakimow@geo.hu-berlin.de
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@geo.hu-berlin.de
committed
def rowCount(self, parent=None, *args, **kwargs):
return len(self.mMapViewList)

benjamin.jakimow@geo.hu-berlin.de
committed
def columnCount(self, QModelIndex_parent=None, *args, **kwargs):
return 1
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]
def mapView2idx(self, mapView):
assert isinstance(mapView, MapView)
row = self.mMapViewList.index(mapView)
return self.createIndex(row, 0, mapView)

benjamin.jakimow@geo.hu-berlin.de
committed
def __getitem__(self, slice):
return self.mMapViewList[slice]
def data(self, index, role=Qt.DisplayRole):
if not index.isValid():
return None
if (index.row() >= len(self.mMapViewList)) or (index.row() < 0):
return None
mapView = self.idx2MapView(index)
assert isinstance(mapView, MapView)
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 MapViewDock(QgsDockWidget, loadUI('mapviewdock.ui')):
sigMapViewAdded = pyqtSignal(MapView)
sigMapViewRemoved = pyqtSignal(MapView)
sigShowProfiles = pyqtSignal(SpatialPoint, MapCanvas, str)
sigMapCanvasColorChanged = pyqtSignal(QColor)
sigSpatialExtentChanged = pyqtSignal(SpatialExtent)
sigCrsChanged = pyqtSignal(QgsCoordinateReferenceSystem)
sigMapSizeChanged = pyqtSignal(QSize)
def setTimeSeries(self, timeSeries:TimeSeries):
assert isinstance(timeSeries, TimeSeries)
self.mTimeSeries = timeSeries
self.mTimeSeries.sigSensorAdded.connect(self.addSensor)
self.mTimeSeries.sigSensorRemoved.connect(self.removeSensor)
def __init__(self, parent=None):
super(MapViewDock, self).__init__(parent)
self.setupUi(self)
self.btnAddMapView.setDefaultAction(self.actionAddMapView)
self.btnRemoveMapView.setDefaultAction(self.actionRemoveMapView)
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())))
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)
self.toolBox.currentChanged.connect(self.onToolboxIndexChanged)
self.spinBoxMapSizeX.valueChanged.connect(lambda: self.onMapSizeChanged('X'))
self.spinBoxMapSizeY.valueChanged.connect(lambda: self.onMapSizeChanged('Y'))
self.mLastMapSize = self.mapSize()

benjamin.jakimow@geo.hu-berlin.de
committed
"""
Returns the defined MapViews
:return: [list-of-MapViews]
"""
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
committed
"""
Returns all MapCanvases from all MapViews
:return: [list-of-MapCanvases]

Benjamin Jakimow
committed
"""
maps = []
for mapView in self.mapViews():
assert isinstance(mapView, MapView)
maps.extend(mapView.mapCanvases())
return maps

Benjamin Jakimow
committed
if isinstance(crs, QgsCoordinateReferenceSystem):
old = self.btnCrs.crs()
if old != crs:
self.btnCrs.setCrs(crs)
self.btnCrs.setLayerCrs(crs)
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)
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)
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@geo.hu-berlin.de
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@geo.hu-berlin.de
committed
def mapSize(self):
return QSize(self.spinBoxMapSizeX.value(),
self.spinBoxMapSizeY.value())

benjamin.jakimow@geo.hu-berlin.de
committed
def refreshCurrentMapView(self, *args):
mv = self.currentMapView()
if isinstance(mv, MapView):
mv.refreshMapView()
else:
s =""

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

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

benjamin.jakimow@geo.hu-berlin.de
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@geo.hu-berlin.de
committed
self.actionRemoveMapView.setEnabled(len(self.mMapViews) > 0)

benjamin.jakimow@geo.hu-berlin.de
committed
def mapBackgroundColor(self)->QColor:
"""
Returns the map canvas background color
:return: QColor
"""
return self.btnMapCanvasColor.color()

benjamin.jakimow@geo.hu-berlin.de
committed
def setMapBackgroundColor(self, color:QColor):
"""
Sets the MapCanvas background color
:param color: QColor
"""
if color != self.mapBackgroundColor():
self.btnMapCanvasColor.setColor(color)

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

benjamin.jakimow@geo.hu-berlin.de
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@geo.hu-berlin.de
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@geo.hu-berlin.de
committed
for mapView in mapViews:
self.sigMapViewAdded.emit(mapView)

benjamin.jakimow@geo.hu-berlin.de
committed
def updateButtons(self, *args):
b = len(self.mMapViews) > 0
self.actionRemoveMapView.setEnabled(b)
self.actionApplyStyles.setEnabled(b)
self.actionHighlightMapView.setEnabled(b)

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

Benjamin Jakimow
committed
def createMapView(self, name:str=None)->MapView:
"""
Create a new MapView
:return: MapView
"""

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

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

Benjamin Jakimow
committed
if isinstance(name, str) and len(name) > 0:
title = name
else:

Benjamin Jakimow
committed
while title in [m.title() for m in self.mapViews()]:
n += 1
title = 'Map View {}'.format(n)
mapView.setTitle(title)
mapView.sigShowProfiles.connect(self.sigShowProfiles)

benjamin.jakimow@geo.hu-berlin.de
committed
mapView.setTimeSeries(self.mTimeSeries)
self.addMapView(mapView)
return mapView

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

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

benjamin.jakimow@geo.hu-berlin.de
committed
"""
assert isinstance(mapView, MapView)
mapView.sigTitleChanged.connect(lambda *args, mv=mapView : self.onMapViewUpdated(mv))
mapView.sigMapViewVisibility.connect(lambda *args, mv=mapView : self.onMapViewUpdated(mv))
i = self.toolBox.addItem(mapView, mapView.windowIcon(), mapView.title())
self.toolBox.setCurrentIndex(i)
self.onMapViewUpdated(mapView)
self.sigMapViewAdded.emit(mapView)

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

benjamin.jakimow@geo.hu-berlin.de
committed
b = isinstance(self.toolBox.currentWidget(), MapView)
self.actionRemoveMapView.setEnabled(b)

benjamin.jakimow@geo.hu-berlin.de
committed
def onMapViewUpdated(self, mapView:MapView):
"""
Handles updates that react on MapView changes
:param mapView: MapView to make the update for
"""
numMV = 0
for i in range(self.toolBox.count()):
item = self.toolBox.widget(i)
if mapView.isVisible():
icon = QIcon(":/timeseriesviewer/icons/mapview.svg")
else:
icon = QIcon(":/timeseriesviewer/icons/mapviewHidden.svg")

benjamin.jakimow@geo.hu-berlin.de
committed
self.toolBox.setItemIcon(i, icon)
self.toolBox.setItemText(i, 'Map View {} "{}"'.format(numMV, mapView.title()))
break

benjamin.jakimow@geo.hu-berlin.de
committed
def removeMapView(self, mapView:MapView)->MapView:
"""
Removes a MapView
:param mapView: MapView
:return: MapView
"""
assert mapView in self.mapViews()
for i in range(self.toolBox.count()):
w = self.toolBox.widget(i)
if isinstance(w, MapView) and w == mapView:
self.toolBox.removeItem(i)
mapView.close()
if self.toolBox.count() >= i:
self.toolBox.setCurrentIndex(min(i, self.toolBox.count()-1))

benjamin.jakimow@geo.hu-berlin.de
committed
self.sigMapViewRemoved.emit(mapView)
return mapView

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

benjamin.jakimow@geo.hu-berlin.de
committed
def __len__(self)->int:
"""
Returns the number of MapViews
:return: int
"""
return len(self.mapViews())

benjamin.jakimow@geo.hu-berlin.de
committed
def __iter__(self):
"""
Provides an iterator over all MapViews
:return:
"""
return iter(self.mapViews())

benjamin.jakimow@geo.hu-berlin.de
committed
def __getitem__(self, slice):

benjamin.jakimow@geo.hu-berlin.de
committed
def __contains__(self, mapView):
return mapView in self.mapViews()

benjamin.jakimow@geo.hu-berlin.de
committed
def index(self, mapView):
assert isinstance(mapView, MapView)
return self.mapViews().index(mapView)
def addSensor(self, sensor:SensorInstrument):
"""
Adds a new SensorInstrument
:param sensor: SensorInstrument
"""
for mapView in self.mapViews():
mapView.addSensor(sensor)
def removeSensor(self, sensor:SensorInstrument):
"""
Removes a Sensor
:param sensor: SensorInstrument
"""
for mapView in self.mMapViews:
mapView.removeSensor(sensor)
def applyStyles(self):
for mapView in self.mMapViews:
mapView.applyStyles()
def setCrosshairStyle(self, crosshairStyle):
for mapView in self.mMapViews:
mapView.setCrosshairStyle(crosshairStyle)
def setShowCrosshair(self, b):
for mapView in self.mMapViews:
mapView.setCrosshairVisibility(b)
def index(self, mapView):
assert isinstance(mapView, MapView)
return self.mapViewsDefinitions.index(mapView)
assert isinstance(mapView, MapView) and mapView in self.mapViews()
self.toolBox.setCurrentWidget(mapView)
def updateTitle(self, *args):
# self.btnToggleMapViewVisibility.setChecked(mapView)
mapView = self.currentMapView()
if isinstance(mapView, MapView):
title = '{} | {}'.format(self.baseTitle, mapView.title())
else:
title = self.baseTitle
self.setWindowTitle(title)
def currentMapView(self):
w = self.toolBox.currentWidget()
if isinstance(w, MapView):
return w
return None
"""
sigLoadingStarted = pyqtSignal(DatumView, MapView)
sigLoadingFinished = pyqtSignal(DatumView, MapView)
sigShowProfiles = pyqtSignal(SpatialPoint, MapCanvas, str)
sigShowMapLayerInfo = pyqtSignal(dict)
sigSpatialExtentChanged = pyqtSignal(SpatialExtent)
sigMapSizeChanged = pyqtSignal(QSize)
sigCRSChanged = pyqtSignal(QgsCoordinateReferenceSystem)
sigActivateMapTool = pyqtSignal(str)
sigMapViewAdded = pyqtSignal(MapView)
sigMapViewRemoved = pyqtSignal(MapView)
sigVisibleDatesChanged = pyqtSignal(list)
def __init__(self, timeSeriesViewer):
super(SpatialTemporalVisualization, self).__init__()
# assert isinstance(timeSeriesViewer, TimeSeriesViewer), timeSeriesViewer
# default map settings
self.mSpatialExtent = SpatialExtent.world()
self.mCRS = self.mSpatialExtent.crs()
self.mSize = QSize(200, 200)
self.mColor = Qt.black
self.mMapCanvases = []
self.ui = timeSeriesViewer.ui
self.mVisibleDates = set()
self.mMapToolKey = MapTools.Pan
self.mMapToolMode = None
self.scrollArea = self.ui.scrollAreaSubsets
assert isinstance(self.scrollArea, MapViewScrollArea)
self.scrollArea.horizontalScrollBar().valueChanged.connect(self.onVisibleMapsChanged)
self.scrollArea.horizontalScrollBar().rangeChanged.connect(self.onVisibleMapsChanged)
# self.scrollArea.sigResized.connect(self.onVisibleMapsChanged)
# self.scrollArea.sigResized.connect(self.refresh())
# self.scrollArea.horizontalScrollBar().valueChanged.connect(self.mRefreshTimer.start)
self.TSV = timeSeriesViewer
self.TS = timeSeriesViewer.timeSeries()
self.ui.dockMapViews.setTimeSeries(self.TS)
self.targetLayout = self.ui.scrollAreaSubsetContent.layout()
self.MVC = self.ui.dockMapViews
assert isinstance(self.MVC, MapViewDock)
self.MVC.sigShowProfiles.connect(self.sigShowProfiles.emit)
self.MVC.sigMapViewAdded.connect(self.onMapViewAdded)
self.MVC.sigMapViewAdded.connect(self.sigMapViewAdded.emit)
self.MVC.sigMapViewRemoved.connect(self.sigMapViewRemoved.emit)
self.vectorOverlay = None
self.DVC = DateViewCollection(self)
self.DVC.sigResizeRequired.connect(self.adjustScrollArea)
self.TS.sigTimeSeriesDatesAdded.connect(self.DVC.addDates)
self.TS.sigTimeSeriesDatesRemoved.connect(self.DVC.removeDates)
self.DVC.addDates(self.TS[:])
if len(self.TS) > 0:
self.setSpatialExtent(self.TS.maxSpatialExtent())
#self.setSubsetSize(QSize(100,50))
self.mMapRefreshTimer = QTimer(self)
self.mMapRefreshTimer.timeout.connect(self.timedCanvasRefresh)
self.mMapRefreshTimer.setInterval(500)
self.mMapRefreshTimer.start()
self.mNumberOfHiddenMapsToRefresh = 2
self.mCurrentLayer = None
self.mSyncLock = False
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
def setMapTool(self, mapToolKey):
mode = None
if mapToolKey == MapTools.SelectFeature:
if self.ui.optionSelectFeaturesRectangle.isChecked():
mode = QgsMapToolSelectionHandler.SelectionMode.SelectSimple
elif self.ui.optionSelectFeaturesPolygon.isChecked():
mode = QgsMapToolSelectionHandler.SelectionMode.SelectPolygon
elif self.ui.optionSelectFeaturesFreehand.isChecked():
mode = QgsMapToolSelectionHandler.SelectionMode.SelectFreehand
elif self.ui.optionSelectFeaturesRadius.isChecked():
mode = QgsMapToolSelectionHandler.SelectionMode.SelectRadius
else:
mode = QgsMapToolSelectionHandler.SelectionMode.SelectSimple
self.mMapToolKey = mapToolKey
self.mMapToolMode = mode
from .mapcanvas import MapCanvas, MapCanvasMapTools
for canvas in self.mapCanvases():
if isinstance(canvas, MapCanvas):
mapTools = canvas.mapTools()
mapTools.activate(mapToolKey)
mt = canvas.mapTool()
if isinstance(mt, QgsMapToolSelect):
mt.setSelectionMode(mode)
def setCurrentLayer(self, layer:QgsMapLayer):
"""
Sets the current map layer some map tools can operate on
:param layer: QgsMapLayer
"""
assert layer is None or isinstance(layer, QgsMapLayer)
self.mCurrentLayer = layer
for mapView in self.mapViews():
mapView.setCurrentLayer(self.mCurrentLayer)
def syncQGISCanvasCenter(self, qgisChanged:bool):
if self.mSyncLock:
return
iface = qgis.utils.iface
assert isinstance(iface, QgisInterface)
c = iface.mapCanvas()
if not isinstance(c, QgsMapCanvas):
return
tsvCenter = self.spatialExtent().spatialCenter()
qgsCenter = SpatialExtent.fromMapCanvas(c).spatialCenter()
if qgisChanged:
# change EOTSV
if tsvCenter.crs().isValid():
self.mSyncLock = True
qgsCenter = qgsCenter.toCrs(tsvCenter.crs())
if isinstance(qgsCenter, SpatialPoint):
self.setSpatialCenter(qgsCenter)
else:
# change QGIS
if qgsCenter.crs().isValid():
self.mSyncLock = True
tsvCenter = tsvCenter.toCrs(qgsCenter.crs())
if isinstance(tsvCenter, SpatialPoint):
c.setCenter(tsvCenter)
else:
pass

benjamin.jakimow@geo.hu-berlin.de
committed
def visibleMaps(self)->list:
"""
Returns a list of mapcanvas visible to the user
:return: [list-of-MapCanvases
"""
return [m for m in self.mapCanvases() if m.isVisibleToViewport()]
def onVisibleMapsChanged(self, *args):
visibleDates = set([m.tsd() for m in self.visibleMaps()])
if visibleDates != self.mVisibleDates:
self.mVisibleDates.clear()
self.mVisibleDates.update(visibleDates)
self.sigVisibleDatesChanged.emit(list(self.mVisibleDates))
self.mSyncLock = False
# do refresh maps
visibleMaps = self.visibleMaps()
hiddenMaps = sorted([m for m in self.mapCanvases() if not m.isVisibleToViewport()],
key = lambda c : self.scrollArea.distanceToCenter(c) )
n = 0
# redraw all visible maps
for c in visibleMaps:
assert isinstance(c, MapCanvas)
c.timedRefresh()
n += 1

Benjamin Jakimow
committed
if False and n < 10:
# refresh up to mNumberOfHiddenMapsToRefresh maps which are not visible to the user
i = 0

Benjamin Jakimow
committed
for c in hiddenMaps:
assert isinstance(c, MapCanvas)
c.timedRefresh()
i += 1
if i >= self.mNumberOfHiddenMapsToRefresh and not force:
break
def mapViewFromCanvas(self, mapCanvas:MapCanvas)->MapView:
"""
Returns the MapView a mapCanvas belongs to
:param mapCanvas: MapCanvas
:return: MapView
"""
for mapView in self.MVC:
assert isinstance(mapView, MapView)
if mapCanvas in mapView.mapCanvases():
return mapView
return None
def onMapViewAdded(self, *args):
self.adjustScrollArea()
s = ""

Benjamin Jakimow
committed
def createMapView(self, name:str=None)->MapView:

Benjamin Jakimow
committed
return self.MVC.createMapView(name=name)

Benjamin Jakimow
committed
def registerMapCanvas(self, mapCanvas:MapCanvas):
"""
Connects a MapCanvas and its signals
:param mapCanvas: MapCanvas
"""
from eotimeseriesviewer.mapcanvas import MapCanvas
assert isinstance(mapCanvas, MapCanvas)
mapCanvas.setMapLayerStore(self.TSV.mMapLayerStore)
mapCanvas.sigCrosshairPositionChanged.connect(lambda point, canvas=mapCanvas: self.TSV.setCurrentLocation(point, canvas))

Benjamin Jakimow
committed
# set general canvas properties
mapCanvas.setFixedSize(self.mSize)
mapCanvas.setDestinationCrs(self.mCRS)
mapCanvas.setSpatialExtent(self.mSpatialExtent)

Benjamin Jakimow
committed
mapCanvas.sigSpatialExtentChanged.connect(self.setSpatialExtent)
mapCanvas.sigCrosshairPositionChanged.connect(self.onCrosshairChanged)
# activate the current map tool
mapTools = mapCanvas.mapTools()
mapTools.activate(self.mMapToolKey)
mt = mapCanvas.mapTool()
if isinstance(mt, QgsMapToolSelect):
mt.setSelectionMode(self.mMapToolMode)
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
def onCrosshairChanged(self, spatialPoint:SpatialPoint):
"""
Synchronizes all crosshair positions. Takes care of CRS differences.
:param spatialPoint: SpatialPoint of new Crosshair position
"""
from eotimeseriesviewer import 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()]
if isinstance(spatialPoint, SpatialPoint):
for mapCanvas in dstCanvases:
mapCanvas.setCrosshairPosition(spatialPoint, emitSignal=False)
def setCrosshairStyle(self, crosshairStyle:CrosshairStyle):
"""
Sets a crosshair style to all map canvas
:param crosshairStyle: CrosshairStyle
"""
for mapView in self.mapViews():
assert isinstance(mapView, MapView)
mapView.setCrosshairStyle(crosshairStyle)
def setCrosshairVisibility(self, b:bool):
"""
Sets the Crosshair visiblity
:param b: bool
"""
assert isinstance(b, bool)
self.onCrosshairChanged(b)
def setVectorLayer(self, lyr:QgsVectorLayer):
"""
Sets a QgsVectorLaye to be shown on top of raster images
:param lyr: QgsVectorLayer
"""
self.MVC.setVectorLayer(lyr)

Benjamin Jakimow
committed

Benjamin Jakimow
committed
def setMapSize(self, size:QSize):
"""
Sets the MapCanvas size.
:param size: QSize
"""

Benjamin Jakimow
committed
assert isinstance(size, QSize)
self.mSize = size
from eotimeseriesviewer.mapcanvas import MapCanvas
for mapCanvas in self.mMapCanvases:
assert isinstance(mapCanvas, MapCanvas)
mapCanvas.setFixedSize(size)
self.sigMapSizeChanged.emit(self.mSize)
self.adjustScrollArea()

Benjamin Jakimow
committed
def mapSize(self)->QSize:
"""
Returns the MapCanvas size
:return: QSize
"""
return QSize(self.mSize)

Benjamin Jakimow
committed
def refresh(self):
"""
Refreshes all visible MapCanvases
"""
for c in self.mapCanvases():
assert isinstance(c, MapCanvas)
c.refresh()

Benjamin Jakimow
committed

Benjamin Jakimow
committed
def adjustScrollArea(self):
"""
Adjusts the scroll area widget to fit all visible widgets
"""
m = self.targetLayout.contentsMargins()
nX = len(self.DVC)
w = h = 0
s = QSize()
r = None
tsdViews = [v for v in self.DVC if v.ui.isVisible()]
mapViews = [v for v in self.MVC if v.isVisible()]
nX = len(tsdViews)
nY = len(mapViews)
spacing = self.targetLayout.spacing()
margins = self.targetLayout.contentsMargins()
sizeX = 1
sizeY = 50
if nX > 0:
s = tsdViews[0].ui.sizeHint().width()
s = nX * (s + spacing) + margins.left() + margins.right()
sizeX = s
if nY > 0 and nX > 0:
s = tsdViews[0].ui.sizeHint().height()
s = s + margins.top() + margins.bottom()
sizeY = s
#s = tsdViews[0].ui.sizeHint()
#s = QSize(nX * (s.width() + spacing) + margins.left() + margins.right(),
# s.height() + margins.top() + margins.bottom())
#print(sizeX, sizeY)
self.targetLayout.parentWidget().resize(QSize(sizeX, sizeY))
def onLocationRequest(self, pt:SpatialPoint, canvas:QgsMapCanvas):
def setSpatialCenter(self, center:SpatialPoint, mapCanvas0=None):
Sets the spatial center of all MapCanvases
:param center: SpatialPoint
:param mapCanvas0:
extent = self.spatialExtent()
if isinstance(extent, SpatialExtent):
centerOld = extent.center()
center = center.toCrs(extent.crs())

Benjamin Jakimow
committed
if center != centerOld and isinstance(center, SpatialPoint):
extent = extent.__copy__()
extent.setCenter(center)
self.setSpatialExtent(extent)
def spatialCenter(self)->SpatialPoint:
return self.spatialExtent().spatialCenter()
Sets the spatial extent of all MapCanvases
:param extent: SpatialExtent
:param mapCanvas0:
:return:
lastExtent = self.spatialExtent()
extent = extent.toCrs(self.crs())
if not isinstance(extent, SpatialExtent) \
or extent.isEmpty() or not extent.isFinite() \
or extent.width() <= 0 \
or extent.height() <= 0 \
or extent == self.mSpatialExtent:
return
if self.mSpatialExtent == extent:
return
self.mSpatialExtent = extent
for mapCanvas in self.mapCanvases():
assert isinstance(mapCanvas, MapCanvas)

Benjamin Jakimow
committed
mapCanvas.addToRefreshPipeLine(extent)
if lastExtent != extent:
self.sigSpatialExtentChanged.emit(extent)
Returns the MapViewDock that controls all MapViews
:return: MapViewDock
Sets the MapCanvas background color
:param color: QColor
assert isinstance(self.MVC, MapViewDock)
self.MVC.setMapBackgroundColor(color)
Returns MapCanvases
:param mapView: a MapView to return MapCanvases from only, defaults to None
:return: [list-of-MapCanvas]
if isinstance(mapView, MapView):
s = ""
return self.mMapCanvases[:]
Returns a list of all mapviews
:return [list-of-MapViews]:
def setCrs(self, crs):
assert isinstance(crs, QgsCoordinateReferenceSystem)
if self.mCRS != crs:
transform = QgsCoordinateTransform()
transform.setSourceCrs(self.mCRS)
transform.setDestinationCrs(crs)
if transform.isValid() and not transform.isShortCircuited():
self.mCRS = crs
for mapCanvas in self.mapCanvases():
# print(('STV set CRS {} {}', str(mapCanvas), self.mCRS.description()))
mapCanvas.setDestinationCrs(QgsCoordinateReferenceSystem(crs))
"""
from timeseriesviewer.utils import saveTransform
if saveTransform(self.mSpatialExtent, self.mCRS, crs):
self.mCRS = crs
else:
pass
"""
self.sigCRSChanged.emit(self.crs())
def crs(self)->QgsCoordinateReferenceSystem:
"""
Returns the QgsCoordinateReferenceSystem
:return: QgsCoordinateReferenceSystem
"""
return self.mCRS
def spatialExtent(self)->SpatialExtent:
"""
Returns the SpatialExtent
:return: SpatialExtent
"""

Benjamin Jakimow
committed
return self.mSpatialExtent.__copy__()
def navigateToTSD(self, TSD:TimeSeriesDatum):
"""
Changes the viewport of the scroll window to show the requested TimeSeriesDatum
:param TSD: TimeSeriesDatum
"""
assert isinstance(TSD, TimeSeriesDatum)
#get widget related to TSD
tsdv = self.DVC.tsdView(TSD)
assert isinstance(self.scrollArea, QScrollArea)
self.scrollArea.ensureWidgetVisible(tsdv.ui)