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 eotimeseriesviewer.utils import *
from eotimeseriesviewer import Option, OptionListModel
from eotimeseriesviewer.timeseries import SensorInstrument, TimeSeriesDatum, TimeSeries, SensorProxyLayer
from eotimeseriesviewer.utils import loadUI
from eotimeseriesviewer.mapviewscrollarea import MapViewScrollArea
from eotimeseriesviewer.mapcanvas import MapCanvas
from eotimeseriesviewer.externals.qps.crosshair.crosshair import getCrosshairStyle, CrosshairStyle
#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 RendererWidgetModifications(object):
def __init__(self, *args):

Benjamin Jakimow
committed
self.initWidgetNames()

Benjamin Jakimow
committed
s = ""
gridLayoutOld = self.layout().children()[0]
self.gridLayout = QGridLayout()
while gridLayoutOld.count() > 0:
w = gridLayoutOld.takeAt(0)
w = w.widget()
gridLayoutOld.removeWidget(w)
w.setVisible(False)
72
73
74
75
76
77
78
79
80
81
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
l = self.layout()
l.removeItem(gridLayoutOld)
if isinstance(l, QBoxLayout):
l.insertItem(0, self.gridLayout)
self.layout().addStretch()
elif isinstance(l, QGridLayout):
l.addItem(self.gridLayout, 0, 0)
minMaxWidget = self.minMaxWidget()
if isinstance(minMaxWidget, QWidget):
minMaxWidget.layout().itemAt(0).widget().collapsedStateChanged.connect(self.onCollapsed)
def initWidgetNames(self, parent=None):
"""
Create a python variables to access QObjects which are child of parent
:param parent: QObject, self by default
"""
if parent is None:
parent = self
for c in parent.children():
setattr(parent, c.objectName(), c)
def onCollapsed(self, b):
hint = self.sizeHint()
self.parent().adjustSize()
# self.parent().setFixedSize(hint)
self.parent().parent().adjustSize()
def connectSliderWithBandComboBox(self, slider, combobox):
"""
Connects a band-selection slider with a band-selection combobox
:param widget: QgsRasterRendererWidget
:param slider: QSlider to show the band number
:param combobox: QComboBox to show the band name
:return:
"""
assert isinstance(self, QgsRasterRendererWidget)
assert isinstance(slider, QSlider)
assert isinstance(combobox, QComboBox)
# init the slider
lyr = self.rasterLayer()
if lyr.isValid():
nb = lyr.dataProvider().bandCount()
else:
ds = gdal.Open(lyr.source())
if isinstance(ds, gdal.Dataset):
nb = ds.RasterCount
else:
nb = 1
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
slider.setTickPosition(QSlider.TicksAbove)
slider.valueChanged.connect(combobox.setCurrentIndex)
slider.setMinimum(1)
slider.setMaximum(nb)
intervals = [1, 2, 5, 10, 25, 50]
for interval in intervals:
if nb / interval < 10:
break
slider.setTickInterval(interval)
slider.setPageStep(interval)
def onBandValueChanged(self, idx, slider):
assert isinstance(self, QgsRasterRendererWidget)
assert isinstance(idx, int)
assert isinstance(slider, QSlider)
# i = slider.value()
slider.blockSignals(True)
slider.setValue(idx)
slider.blockSignals(False)
# self.minMaxWidget().setBands(myBands)
# self.widgetChanged.emit()
if self.comboBoxWithNotSetItem(combobox):
combobox.currentIndexChanged[int].connect(lambda idx: onBandValueChanged(self, idx, slider))
else:
combobox.currentIndexChanged[int].connect(lambda idx: onBandValueChanged(self, idx + 1, slider))
s = ""
def comboBoxWithNotSetItem(self, cb)->bool:
assert isinstance(cb, QComboBox)
data = cb.itemData(0, role=Qt.DisplayRole)
return re.search(r'^(not set|none|nonetype)$', str(data).strip(), re.I) is not None
def setLayoutItemVisibility(self, grid, isVisible):
assert isinstance(self, QgsRasterRendererWidget)
for i in range(grid.count()):
item = grid.itemAt(i)
if isinstance(item, QLayout):
s = ""
elif isinstance(item, QWidgetItem):
item.widget().setVisible(isVisible)
item.widget().setParent(self)
else:
s = ""
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
def setBandSelection(self, key):
key = key.upper()
if key == 'DEFAULT':
bandIndices = defaultBands(self.rasterLayer())
else:
colors = re.split('[ ,;:]', key)
bandIndices = [bandClosestToWavelength(self.rasterLayer(), c) for c in colors]
n = min(len(bandIndices), len(self.mBandComboBoxes))
for i in range(n):
cb = self.mBandComboBoxes[i]
bandIndex = bandIndices[i]
if self.comboBoxWithNotSetItem(cb):
cb.setCurrentIndex(bandIndex+1)
else:
cb.setCurrentIndex(bandIndex)
def fixBandNames(self, comboBox):
"""
Changes the QGIS default bandnames ("Band 001") to more meaningfull information including gdal.Dataset.Descriptions.
:param widget:
:param comboBox:
"""
nb = self.rasterLayer().bandCount()
assert isinstance(self, QgsRasterRendererWidget)
assert isinstance(comboBox, QComboBox)
#comboBox.clear()
m = comboBox.model()
assert isinstance(m, QStandardItemModel)
bandNames = displayBandNames(self.rasterLayer())
b = 1 if nb < comboBox.count() else 0
for i in range(nb):
item = m.item(i+b,0)
assert isinstance(item, QStandardItem)
item.setData(bandNames[i], Qt.DisplayRole)
item.setData('Band {} "{}"'.format(i+1, bandNames[i]), Qt.ToolTipRole)
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
def displayBandNames(provider_or_dataset, bands=None):
results = None
if isinstance(provider_or_dataset, QgsRasterLayer):
return displayBandNames(provider_or_dataset.dataProvider())
elif isinstance(provider_or_dataset, QgsRasterDataProvider):
if provider_or_dataset.name() == 'gdal':
ds = gdal.Open(provider_or_dataset.dataSourceUri())
results = displayBandNames(ds, bands=bands)
else:
# same as in QgsRasterRendererWidget::displayBandName
results = []
if bands is None:
bands = range(1, provider_or_dataset.bandCount() + 1)
for band in bands:
result = provider_or_dataset.generateBandName(band)
colorInterp ='{}'.format(provider_or_dataset.colorInterpretationName(band))
if colorInterp != 'Undefined':
result += '({})'.format(colorInterp)
results.append(result)
elif isinstance(provider_or_dataset, gdal.Dataset):
results = []
if bands is None:
bands = range(1, provider_or_dataset.RasterCount+1)
for band in bands:
b = provider_or_dataset.GetRasterBand(band)
descr = b.GetDescription()
if len(descr) == 0:
descr = 'Band {}'.format(band)
results.append(descr)
return results
class SingleBandPseudoColorRendererWidget(QgsSingleBandPseudoColorRendererWidget, RendererWidgetModifications):
@staticmethod
def create(layer, extent):
return SingleBandPseudoColorRendererWidget(layer, extent)
def __init__(self, layer, extent):
super(SingleBandPseudoColorRendererWidget, self).__init__(layer, extent)

Benjamin Jakimow
committed
self.gridLayout = self.layout().children()[0]
assert isinstance(self.gridLayout, QGridLayout)
for i in range(self.gridLayout.count()):
w = self.gridLayout.itemAt(i)
w = w.widget()
if isinstance(w, QWidget):
setattr(self, w.objectName(), w)

Benjamin Jakimow
committed
toReplace = [self.mBandComboBox, self.mMinLabel, self.mMaxLabel, self.mMinLineEdit, self.mMaxLineEdit]
for w in toReplace:
self.gridLayout.removeWidget(w)
w.setVisible(False)

Benjamin Jakimow
committed
self.mBandSlider = QSlider(Qt.Horizontal, self)
self.mBandComboBoxes.append(self.mBandComboBox)
self.fixBandNames(self.mBandComboBox)
self.connectSliderWithBandComboBox(self.mBandSlider, self.mBandComboBox)

Benjamin Jakimow
committed
self.mBtnBar = QFrame(self)

Benjamin Jakimow
committed
grid = QGridLayout(self)
grid.addWidget(self.mBtnBar, 0, 0, 1, 4, Qt.AlignLeft)
grid.addWidget(self.mBandSlider, 1, 0, 1, 2)
grid.addWidget(self.mBandComboBox, 1, 2, 1, 2)
grid.addWidget(self.mMinLabel, 2, 0)
grid.addWidget(self.mMinLineEdit, 2, 1)
grid.addWidget(self.mMaxLabel, 2, 2)
grid.addWidget(self.mMaxLineEdit, 2, 3)

Benjamin Jakimow
committed
grid.setColumnStretch(0, 0)
grid.setColumnStretch(1, 2)
grid.setColumnStretch(2, 0)
grid.setColumnStretch(3, 2)
grid.setSpacing(2)

Benjamin Jakimow
committed
self.gridLayout.addItem(grid, 0, 1, 2, 4)
self.gridLayout.setSpacing(2)
self.setLayoutItemVisibility(grid, True)
def initActionButtons(self):
wl, wlu = parseWavelength(self.rasterLayer())
self.wavelengths = wl
self.wavelengthUnit = wlu

Benjamin Jakimow
committed
self.mBtnBar.setLayout(QHBoxLayout(self.mBtnBar))
self.mBtnBar.layout().addStretch()
self.mBtnBar.layout().setContentsMargins(0, 0, 0, 0)
self.mBtnBar.layout().setSpacing(2)
self.actionSetDefault = QAction('Default', self)
self.actionSetRed = QAction('R', self)
self.actionSetGreen = QAction('G', self)
self.actionSetBlue = QAction('B', self)
self.actionSetNIR = QAction('nIR', self)
self.actionSetSWIR = QAction('swIR', self)
self.actionSetDefault.triggered.connect(lambda: self.setBandSelection('default'))
self.actionSetRed.triggered.connect(lambda: self.setBandSelection('R'))
self.actionSetGreen.triggered.connect(lambda: self.setBandSelection('G'))
self.actionSetBlue.triggered.connect(lambda: self.setBandSelection('B'))
self.actionSetNIR.triggered.connect(lambda: self.setBandSelection('nIR'))
self.actionSetSWIR.triggered.connect(lambda: self.setBandSelection('swIR'))
def addBtnAction(action):

Benjamin Jakimow
committed
btn = QToolButton(self.mBtnBar)
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
btn.setDefaultAction(action)
self.mBtnBar.layout().addWidget(btn)
self.insertAction(None, action)
return btn
self.btnDefault = addBtnAction(self.actionSetDefault)
self.btnRed = addBtnAction(self.actionSetRed)
self.btnGreen = addBtnAction(self.actionSetGreen)
self.btnBlue = addBtnAction(self.actionSetRed)
self.btnNIR = addBtnAction(self.actionSetNIR)
self.btnSWIR = addBtnAction(self.actionSetSWIR)
b = self.wavelengths is not None
for a in [self.actionSetRed, self.actionSetGreen, self.actionSetBlue, self.actionSetNIR, self.actionSetSWIR]:
a.setEnabled(b)
def displayBandNames(provider_or_dataset, bands=None):
results = None
if isinstance(provider_or_dataset, QgsRasterLayer):
return displayBandNames(provider_or_dataset.dataProvider())
elif isinstance(provider_or_dataset, QgsRasterDataProvider):
if provider_or_dataset.name() == 'gdal':
ds = gdal.Open(provider_or_dataset.dataSourceUri())
results = displayBandNames(ds, bands=bands)
else:
# same as in QgsRasterRendererWidget::displayBandName
results = []
if bands is None:
bands = range(1, provider_or_dataset.bandCount() + 1)
for band in bands:
result = provider_or_dataset.generateBandName(band)
colorInterp ='{}'.format(provider_or_dataset.colorInterpretationName(band))
if colorInterp != 'Undefined':
result += '({})'.format(colorInterp)
results.append(result)
elif isinstance(provider_or_dataset, gdal.Dataset):
results = []
if bands is None:
bands = range(1, provider_or_dataset.RasterCount+1)
for band in bands:
b = provider_or_dataset.GetRasterBand(band)
descr = b.GetDescription()
if len(descr) == 0:
descr = 'Band {}'.format(band)
results.append(descr)
return results

Benjamin Jakimow
committed
class SingleBandGrayRendererWidget(QgsSingleBandGrayRendererWidget, RendererWidgetModifications):
@staticmethod
def create(layer, extent):
return SingleBandGrayRendererWidget(layer, extent)
def __init__(self, layer, extent):
super(SingleBandGrayRendererWidget, self).__init__(layer, extent)
self.modifyGridLayout()

Benjamin Jakimow
committed
self.mGrayBandSlider = QSlider(Qt.Horizontal, self)
self.mBandComboBoxes.append(self.mGrayBandComboBox)
self.fixBandNames(self.mGrayBandComboBox)
self.connectSliderWithBandComboBox(self.mGrayBandSlider, self.mGrayBandComboBox)

Benjamin Jakimow
committed
self.mBtnBar = QFrame(self)
self.initActionButtons()
self.gridLayout.addWidget(self.mGrayBandLabel, 0, 0)
self.gridLayout.addWidget(self.mBtnBar, 0, 1, 1, 4, Qt.AlignLeft)
self.gridLayout.addWidget(self.mGrayBandSlider, 1, 1, 1, 2)

Benjamin Jakimow
committed
self.gridLayout.addWidget(self.mGrayBandComboBox, 1, 3, 1, 2)
self.gridLayout.addWidget(self.label, 2, 0)
self.gridLayout.addWidget(self.mGradientComboBox, 2, 1, 1, 4)
self.gridLayout.addWidget(self.mMinLabel, 3, 1)
self.gridLayout.addWidget(self.mMinLineEdit, 3, 2)
self.gridLayout.addWidget(self.mMaxLabel, 3, 3)
self.gridLayout.addWidget(self.mMaxLineEdit, 3, 4)
self.gridLayout.addWidget(self.mContrastEnhancementLabel, 4, 0)

Benjamin Jakimow
committed
self.gridLayout.addWidget(self.mContrastEnhancementComboBox, 4, 1, 1, 4)
self.gridLayout.setSpacing(2)
self.setLayoutItemVisibility(self.gridLayout, True)
self.mDefaultRenderer = layer.renderer()
self.setFromRenderer(self.mDefaultRenderer)
def initActionButtons(self):
wl, wlu = parseWavelength(self.rasterLayer())
self.wavelengths = wl
self.wavelengthUnit = wlu

Benjamin Jakimow
committed
self.mBtnBar.setLayout(QHBoxLayout(self))
self.mBtnBar.layout().addStretch()
self.mBtnBar.layout().setContentsMargins(0, 0, 0, 0)
self.mBtnBar.layout().setSpacing(2)
self.actionSetDefault = QAction('Default', self)
self.actionSetRed = QAction('R', self)
self.actionSetGreen = QAction('G', self)
self.actionSetBlue = QAction('B', self)
self.actionSetNIR = QAction('nIR', self)
self.actionSetSWIR = QAction('swIR', self)
self.actionSetDefault.triggered.connect(lambda: self.setBandSelection('default'))
self.actionSetRed.triggered.connect(lambda: self.setBandSelection('R'))
self.actionSetGreen.triggered.connect(lambda: self.setBandSelection('G'))
self.actionSetBlue.triggered.connect(lambda: self.setBandSelection('B'))
self.actionSetNIR.triggered.connect(lambda: self.setBandSelection('nIR'))
self.actionSetSWIR.triggered.connect(lambda: self.setBandSelection('swIR'))
def addBtnAction(action):

Benjamin Jakimow
committed
btn = QToolButton(self.mBtnBar)
btn.setDefaultAction(action)
self.mBtnBar.layout().addWidget(btn)
self.insertAction(None, action)
return btn
self.btnDefault = addBtnAction(self.actionSetDefault)
self.btnBlue = addBtnAction(self.actionSetBlue)
self.btnGreen = addBtnAction(self.actionSetGreen)
self.btnRed = addBtnAction(self.actionSetRed)
self.btnNIR = addBtnAction(self.actionSetNIR)
self.btnSWIR = addBtnAction(self.actionSetSWIR)
b = self.wavelengths is not None
for a in [self.actionSetRed, self.actionSetGreen, self.actionSetBlue, self.actionSetNIR, self.actionSetSWIR]:
a.setEnabled(b)
class PalettedRendererWidget(QgsPalettedRendererWidget, RendererWidgetModifications):
@staticmethod
def create(layer, extent):
return PalettedRendererWidget(layer, extent)
def __init__(self, layer, extent):
super(PalettedRendererWidget, self).__init__(layer, extent)
#self.modifyGridLayout()
self.fixBandNames(self.mBandComboBox)
self.mTreeView.setMinimumSize(QSize(10,10))
s = ""
class MultiBandColorRendererWidget(QgsMultiBandColorRendererWidget, RendererWidgetModifications):
@staticmethod
def create(layer, extent):
return MultiBandColorRendererWidget(layer, extent)
def __init__(self, layer, extent):
super(MultiBandColorRendererWidget, self).__init__(layer, extent)
self.modifyGridLayout()

Benjamin Jakimow
committed
self.mRedBandSlider = QSlider(Qt.Horizontal, self)
self.mGreenBandSlider = QSlider(Qt.Horizontal, self)
self.mBlueBandSlider = QSlider(Qt.Horizontal, self)
self.mBandComboBoxes.extend([self.mRedBandComboBox, self.mGreenBandComboBox, self.mBlueBandComboBox])
self.mSliders = [self.mRedBandSlider, self.mGreenBandSlider, self.mBlueBandSlider]
for cbox, slider in zip(self.mBandComboBoxes, self.mSliders):
self.connectSliderWithBandComboBox(slider, cbox)
self.fixBandNames(self.mRedBandComboBox)
self.fixBandNames(self.mGreenBandComboBox)
self.fixBandNames(self.mBlueBandComboBox)

Benjamin Jakimow
committed
self.mBtnBar = QFrame(self)
self.mBtnBar.setLayout(QHBoxLayout(self.mBtnBar))
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
self.initActionButtons()
self.mBtnBar.layout().addStretch()
self.mBtnBar.layout().setContentsMargins(0, 0, 0, 0)
self.mBtnBar.layout().setSpacing(2)
#self.gridLayout.deleteLater()
# self.gridLayout = newGrid
self.gridLayout.addWidget(self.mBtnBar, 0, 1, 1, 3)
self.gridLayout.addWidget(self.mRedBandLabel, 1, 0)
self.gridLayout.addWidget(self.mRedBandSlider, 1, 1)
self.gridLayout.addWidget(self.mRedBandComboBox, 1, 2)
self.gridLayout.addWidget(self.mRedMinLineEdit, 1, 3)
self.gridLayout.addWidget(self.mRedMaxLineEdit, 1, 4)
self.gridLayout.addWidget(self.mGreenBandLabel, 2, 0)
self.gridLayout.addWidget(self.mGreenBandSlider, 2, 1)
self.gridLayout.addWidget(self.mGreenBandComboBox, 2, 2)
self.gridLayout.addWidget(self.mGreenMinLineEdit, 2, 3)
self.gridLayout.addWidget(self.mGreenMaxLineEdit, 2, 4)
self.gridLayout.addWidget(self.mBlueBandLabel, 3, 0)
self.gridLayout.addWidget(self.mBlueBandSlider, 3, 1)
self.gridLayout.addWidget(self.mBlueBandComboBox, 3, 2)
self.gridLayout.addWidget(self.mBlueMinLineEdit, 3, 3)
self.gridLayout.addWidget(self.mBlueMaxLineEdit, 3, 4)
self.gridLayout.addWidget(self.mContrastEnhancementAlgorithmLabel, 4, 0, 1, 2)
self.gridLayout.addWidget(self.mContrastEnhancementAlgorithmComboBox, 4, 2, 1, 3)
self.setLayoutItemVisibility(self.gridLayout, True)
self.mRedBandLabel.setText('R')
self.mGreenBandLabel.setText('G')
self.mBlueBandLabel.setText('B')
self.mDefaultRenderer = layer.renderer()
def initActionButtons(self):
wl, wlu = parseWavelength(self.rasterLayer())
self.wavelengths = wl
self.wavelengthUnit = wlu
self.actionSetDefault = QAction('Default', self)
self.actionSetTrueColor = QAction('RGB', self)
self.actionSetCIR = QAction('nIR', self)
self.actionSet453 = QAction('swIR', self)
self.actionSetDefault.triggered.connect(lambda: self.setBandSelection('default'))
self.actionSetTrueColor.triggered.connect(lambda: self.setBandSelection('R,G,B'))
self.actionSetCIR.triggered.connect(lambda: self.setBandSelection('nIR,R,G'))
self.actionSet453.triggered.connect(lambda: self.setBandSelection('nIR,swIR,R'))
def addBtnAction(action):

Benjamin Jakimow
committed
btn = QToolButton(self.mBtnBar)
btn.setDefaultAction(action)
self.mBtnBar.layout().addWidget(btn)
self.insertAction(None, action)
return btn
self.btnDefault = addBtnAction(self.actionSetDefault)
self.btnTrueColor = addBtnAction(self.actionSetTrueColor)
self.btnCIR = addBtnAction(self.actionSetCIR)
self.btn453 = addBtnAction(self.actionSet453)
b = self.wavelengths is not None
for a in [self.actionSetCIR, self.actionSet453, self.actionSetTrueColor]:
a.setEnabled(b)
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
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
663
664
665
666
667
668
669
670
671
672
673
class MapViewLayerTreeViewMenuProvider(QgsLayerTreeViewMenuProvider):
def __init__(self, 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)
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)
#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.sigTitleChanged)
self.tbName.textChanged.connect(self.onTitleChanged)
self.actionSetCrosshairStyle.triggered.connect(self.onChangeCrosshairStyle)
self.mSensorLayerList = list()
self.mMapCanvases = list()
assert isinstance(self.mLayerTreeView, QgsLayerTreeView)
self.mDummyCanvas = QgsMapCanvas()
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.mLayerTreeView, self.mDummyCanvas)
self.mLayerTreeView.setMenuProvider(self.mMapLayerTreeViewMenuProvider)
self.mLayerTree.removedChildren.connect(self.onChildNodesRemoved)
self.mIsVisible = True

Benjamin Jakimow
committed
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
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)

Benjamin Jakimow
committed
def setIsVisible(self, b: bool):
"""
Sets the map view visibility
:param b: bool
"""
for mapCanvas in self.mapCanvases():
assert isinstance(mapCanvas, MapCanvas)
mapCanvas.setVisible(b)
if self.ui.actionToggleMapViewHidden.isChecked() == b:
self.ui.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[:]

benjamin.jakimow@geo.hu-berlin.de
committed
def applyStyles(self):
"""Applies all style changes to all sensor views."""
for sensorView in self.mSensorViews.values():

benjamin.jakimow@geo.hu-berlin.de
committed
sensorView.applyStyle()
def onTitleChanged(self, *args):
self.setWindowTitle('Map View "{}"'.format(self.title()))
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 self.mLayerTree.checkedLayers()
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
"""

Benjamin Jakimow
committed
self.onCrosshairChanged(crosshairStyle)

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)

Benjamin Jakimow
committed
mapCanvas.sigCrosshairVisibilityChanged.connect(self.onCrosshairChanged)
mapCanvas.sigCrosshairStyleChanged.connect(self.onCrosshairChanged)
self.mMapCanvases.append(mapCanvas)
self.sigMapViewVisibility.connect(mapCanvas.refresh)

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

Benjamin Jakimow
committed
def onCrosshairChanged(self, obj):
"""
Synchronizes all crosshair positions. Takes care of CRS differences.
:param spatialPoint: SpatialPoint of the new Crosshair position
"""
from eotimeseriesviewer import CrosshairStyle

Benjamin Jakimow
committed
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(obj, bool):
for mapCanvas in dstCanvases:
mapCanvas.setCrosshairVisibility(obj, emitSignal=False)

Benjamin Jakimow
committed
if isinstance(obj, CrosshairStyle):
for mapCanvas in dstCanvases:
mapCanvas.setCrosshairStyle(obj, emitSignal=False)
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
"""
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
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()
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 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)