main.py 64.1 KB
Newer Older
unknown's avatar
unknown committed
1
2
3
# -*- coding: utf-8 -*-
"""
/***************************************************************************
4
                              EO Time Series Viewer
unknown's avatar
unknown committed
5
6
7
                              -------------------
        begin                : 2015-08-20
        git sha              : $Format:%H$
8
9
        copyright            : (C) 2017 by HU-Berlin
        email                : benjamin.jakimow@geo.hu-berlin.de
unknown's avatar
unknown committed
10
11
12
13
14
15
16
17
18
19
20
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   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.                                   *
 *                                                                         *
 ***************************************************************************/
"""
21
# noinspection PyPep8Naming
22

23

24
r"""
25
26
27
28
File "D:\Programs\OSGeo4W\apps\Python27\lib\multiprocessing\managers.py", line
528, in start
self._address = reader.recv()
EOFError
Benjamin Jakimow's avatar
Benjamin Jakimow committed
29

30
31
see https://github.com/pyinstaller/pyinstaller/wiki/Recipe-Multiprocessing
see https://github.com/CleanCut/green/issues/103 
32

33
"""
34
"""
35
36
37
38
path = os.path.abspath(os.path.join(sys.exec_prefix, '../../bin/pythonw.exe'))
if os.path.exists(path):
    multiprocessing.set_executable(path)
    sys.argv = [ None ]
39
"""
40

41
42
import qgis.utils
from qgis.core import *
Benjamin Jakimow's avatar
Benjamin Jakimow committed
43
from qgis.core import QgsMapLayer, QgsRasterLayer, QgsVectorLayer, QgsMessageOutput, QgsCoordinateReferenceSystem, \
Benjamin Jakimow's avatar
Benjamin Jakimow committed
44
    Qgis, QgsWkbTypes, QgsTask, QgsProviderRegistry, QgsMapLayerStore, QgsFeature, QgsField, \
Benjamin Jakimow's avatar
Benjamin Jakimow committed
45
46
    QgsTextFormat, QgsProject, QgsSingleSymbolRenderer, QgsGeometry, QgsApplication, QgsFillSymbol, \
    QgsProjectArchive, QgsZipUtils
Benjamin Jakimow's avatar
Benjamin Jakimow committed
47

48
from qgis.gui import *
Benjamin Jakimow's avatar
Benjamin Jakimow committed
49
50
51
from qgis.gui import QgsMapCanvas, QgsStatusBar, QgsFileWidget, \
    QgsMessageBar, QgsMessageViewer, QgsDockWidget, QgsTaskManagerWidget, QgisInterface

52
import qgis.utils
Benjamin Jakimow's avatar
Benjamin Jakimow committed
53
from eotimeseriesviewer import LOG_MESSAGE_TAG
54
55
from eotimeseriesviewer.utils import *
from eotimeseriesviewer.timeseries import *
56
from eotimeseriesviewer.mapcanvas import MapCanvas
Benjamin Jakimow's avatar
Benjamin Jakimow committed
57
from eotimeseriesviewer.profilevisualization import ProfileViewDock
58
from eotimeseriesviewer.temporalprofiles import TemporalProfileLayer
Benjamin Jakimow's avatar
Benjamin Jakimow committed
59
from eotimeseriesviewer.mapvisualization import MapView, MapWidget
60
import eotimeseriesviewer.settings as eotsvSettings
61
62
63
64
65
from .externals.qps.speclib.core import SpectralProfile, SpectralLibrary
from .externals.qps.speclib.gui import SpectralLibraryPanel
from .externals.qps.maptools import MapTools, CursorLocationMapTool, QgsMapToolSelect, QgsMapToolSelectionHandler
from .externals.qps.cursorlocationvalue import CursorLocationInfoModel, CursorLocationInfoDock
from .externals.qps.vectorlayertools import VectorLayerTools
66
import eotimeseriesviewer.labeling
Benjamin Jakimow's avatar
Benjamin Jakimow committed
67
from eotimeseriesviewer import debugLog
Benjamin Jakimow's avatar
Benjamin Jakimow committed
68

69
DEBUG = False
Benjamin Jakimow's avatar
Benjamin Jakimow committed
70

71
72
73
74
75
EXTRA_SPECLIB_FIELDS = [
    QgsField('date', QVariant.String, 'varchar'),
    QgsField('doy', QVariant.Int, 'int'),
    QgsField('sensor', QVariant.String, 'varchar')
]
76

77

78
class AboutDialogUI(QDialog):
79
80
81
    def __init__(self, parent=None):
        """Constructor."""
        super(AboutDialogUI, self).__init__(parent)
82
        loadUi(DIR_UI / 'aboutdialog.ui', self)
83
        self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint)
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
        self.init()

    def init(self):
        self.mTitle = self.windowTitle()
        self.listWidget.currentItemChanged.connect(lambda: self.setAboutTitle())
        self.setAboutTitle()

        # page About
        from eotimeseriesviewer import PATH_LICENSE, __version__, PATH_CHANGELOG, PATH_ABOUT
        self.labelVersion.setText('{}'.format(__version__))

        def readTextFile(path):
            if os.path.isfile(path):
                f = open(path, encoding='utf-8')
                txt = f.read()
                f.close()
            else:
                txt = 'unable to read {}'.format(path)
            return txt

        # page Changed
        self.tbAbout.setHtml(readTextFile(PATH_ABOUT))
Benjamin Jakimow's avatar
Benjamin Jakimow committed
106
107
        self.tbChanges.setHtml(readTextFile(PATH_CHANGELOG))
        self.tbLicense.setHtml(readTextFile(PATH_LICENSE))
108
109
110
111
112
113
114
115
116
117
118
119
120

    def setAboutTitle(self, suffix=None):
        item = self.listWidget.currentItem()

        if item:
            title = '{} | {}'.format(self.mTitle, item.text())
        else:
            title = self.mTitle
        if suffix:
            title += ' ' + suffix
        self.setWindowTitle(title)


Benjamin Jakimow's avatar
main.py    
Benjamin Jakimow committed
121
class EOTimeSeriesViewerUI(QMainWindow):
122
123
    sigAboutToBeClosed = pyqtSignal()

124
125
    def __init__(self, parent=None):
        """Constructor."""
Benjamin Jakimow's avatar
main.py    
Benjamin Jakimow committed
126
        super(EOTimeSeriesViewerUI, self).__init__(parent)
127
        loadUi(DIR_UI / 'timeseriesviewer.ui', self)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
128

129
        self.setCentralWidget(self.mMapWidget)
130

131
        self.addActions(self.findChildren(QAction))
132
        from eotimeseriesviewer import TITLE, icon, __version__
133

134
        self.mInitResized = False
135
        self.mMapToolActions = []
136
        self.setWindowTitle('{} ({})'.format(TITLE, __version__))
137
        self.setWindowIcon(icon())
138
139
        if sys.platform == 'darwin':
            self.menuBar().setNativeMenuBar(False)
140

141
142
143
        # set button default actions -> this will show the action icons as well
        # I don't know why this is not possible in the QDesigner when QToolButtons are
        # placed outside a toolbar
144
145
146

        area = Qt.LeftDockWidgetArea

147
        # self.dockRendering = addDockWidget(docks.RenderingDockUI(self))
148

149
        from eotimeseriesviewer.mapvisualization import MapViewDock
Benjamin Jakimow's avatar
Benjamin Jakimow committed
150
        self.dockMapViews = self.addDockWidget(area, MapViewDock(self))
Benjamin Jakimow's avatar
Benjamin Jakimow committed
151

Benjamin Jakimow's avatar
Benjamin Jakimow committed
152
        # self.tabifyDockWidget(self.dockMapViews, self.dockRendering)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
153
        # self.tabifyDockWidget(self.dockSensors, self.dockCursorLocation)
154
155

        area = Qt.BottomDockWidgetArea
Benjamin Jakimow's avatar
Benjamin Jakimow committed
156
157
        # from timeseriesviewer.mapvisualization import MapViewDockUI
        # self.dockMapViews = addDockWidget(MapViewDockUI(self))
158

Benjamin Jakimow's avatar
Benjamin Jakimow committed
159
        self.dockTimeSeries = self.addDockWidget(area, TimeSeriesDock(self))
160
161
        self.dockTimeSeries.initActions(self)

Benjamin Jakimow's avatar
Benjamin Jakimow committed
162
163
        from eotimeseriesviewer.profilevisualization import ProfileViewDock
        self.dockProfiles = self.addDockWidget(area, ProfileViewDock(self))
164

165
        area = Qt.LeftDockWidgetArea
Benjamin Jakimow's avatar
Benjamin Jakimow committed
166
        # self.dockAdvancedDigitizingDockWidget = self.addDockWidget(area,
Benjamin Jakimow's avatar
Benjamin Jakimow committed
167
        #   QgsAdvancedDigitizingDockWidget(self.dockLabeling.labelingWidget().canvas(), self))
Benjamin Jakimow's avatar
Benjamin Jakimow committed
168
        # self.dockAdvancedDigitizingDockWidget.setVisible(False)
169

170
        area = Qt.BottomDockWidgetArea
171
172
        panel = SpectralLibraryPanel(self)

Benjamin Jakimow's avatar
Benjamin Jakimow committed
173
        self.dockSpectralLibrary = self.addDockWidget(area, panel)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
174
175

        self.tabifyDockWidget(self.dockTimeSeries, self.dockSpectralLibrary)
176
        self.tabifyDockWidget(self.dockTimeSeries, self.dockProfiles)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
177
        # self.tabifyDockWidget(self.dockTimeSeries, self.dockLabeling)
178

179
        area = Qt.RightDockWidgetArea
180

Benjamin Jakimow's avatar
Benjamin Jakimow committed
181
182
183
        self.dockTaskManager = QgsDockWidget('Task Manager')
        self.dockTaskManager.setWidget(QgsTaskManagerWidget(QgsApplication.taskManager()))
        self.dockTaskManager.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
184
        self.dockTaskManager = self.addDockWidget(area, self.dockTaskManager)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
185

186
187
        from eotimeseriesviewer.systeminfo import SystemInfoDock
        from eotimeseriesviewer.sensorvisualization import SensorDockUI
Benjamin Jakimow's avatar
Benjamin Jakimow committed
188

Benjamin Jakimow's avatar
Benjamin Jakimow committed
189
        self.dockSystemInfo = self.addDockWidget(area, SystemInfoDock(self))
190
        self.dockSystemInfo.setVisible(False)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
191

Benjamin Jakimow's avatar
Benjamin Jakimow committed
192
193
        self.dockSensors = self.addDockWidget(area, SensorDockUI(self))
        self.dockCursorLocation = self.addDockWidget(area, CursorLocationInfoDock(self))
194

Benjamin Jakimow's avatar
Benjamin Jakimow committed
195
        self.tabifyDockWidget(self.dockTaskManager, self.dockCursorLocation)
196
197
        self.tabifyDockWidget(self.dockTaskManager, self.dockSystemInfo)
        self.tabifyDockWidget(self.dockTaskManager, self.dockSensors)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
198

199
        for dock in self.findChildren(QDockWidget):
200

201
202
203
204
            if len(dock.actions()) > 0:
                s = ""
            self.menuPanels.addAction(dock.toggleViewAction())

205
206
        self.dockTimeSeries.raise_()

Benjamin Jakimow's avatar
Benjamin Jakimow committed
207
208
209
210
211
212
213
214
215
    def addDockWidget(self, area: Qt.DockWidgetArea, dock: QDockWidget) -> QDockWidget:
        """
        shortcut to add a created dock and return it
        :param dock:
        :return:
        """
        dock.setParent(self)
        super().addDockWidget(area, dock)
        return dock
216

Benjamin Jakimow's avatar
Benjamin Jakimow committed
217
    def registerMapToolAction(self, a: QAction) -> QAction:
218
219
220
221
222
223
        """
        Registers this action as map tools action. If triggered, all other mapt tool actions with be set unchecked
        :param a: QAction
        :return: QAction
        """

224
225
226
227
        assert isinstance(a, QAction)
        if a not in self.mMapToolActions:
            self.mMapToolActions.append(a)
        a.setCheckable(True)
228
        a.toggled.connect(lambda b, action=a: self.onMapToolActionToggled(b, action))
229
        return a
230

Benjamin Jakimow's avatar
Benjamin Jakimow committed
231
    def onMapToolActionToggled(self, b: bool, action: QAction):
232
        """
Benjamin Jakimow's avatar
Benjamin Jakimow committed
233
        Reacts on toggling a map tool
234
235
236
        :param b:
        :param action:
        """
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
        assert isinstance(action, QAction)
        otherActions = [a for a in self.mMapToolActions if a != action]

        # enable / disable the other maptool actions
        if b is True:
            for a in otherActions:
                assert isinstance(a, QAction)
                a.setChecked(False)

        else:
            otherSelected = [a for a in otherActions if a.isChecked()]
            if len(otherSelected) == 0:
                action.setChecked(True)

        b = self.actionIdentify.isChecked()
        self.optionIdentifyCursorLocation.setEnabled(b)
        self.optionIdentifySpectralProfile.setEnabled(b)
        self.optionIdentifyTemporalProfile.setEnabled(b)
        self.optionMoveCenter.setEnabled(b)
256

Benjamin Jakimow's avatar
Benjamin Jakimow committed
257
    def closeEvent(self, a0: QCloseEvent):
258
        self.sigAboutToBeClosed.emit()
259

260
261
262
263
    """
    def resizeEvent(self, event:QResizeEvent):

        super(TimeSeriesViewerUI, self).resizeEvent(event)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
264

265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
        if False and not self.mInitResized:
            pass
        w = 0.5
        minH = int(self.size().height() * w)
        print(minH)
        #self.mCentralWidget.setMinimumHeight(minH)
        for d in self.findChildren(QDockWidget):
            w = d.widget()
            assert isinstance(d, QDockWidget)
            print((d.objectName(), d.minimumHeight(), d.sizePolicy().verticalPolicy()))
            d.setMinimumHeight(0)
            s = ""
        #self.mCentralWidget.setMinimumWidth(int(self.size().width() * w))
            #self.mInitResized = True
    """
Benjamin Jakimow's avatar
Benjamin Jakimow committed
280
281


Benjamin Jakimow's avatar
Benjamin Jakimow committed
282
LUT_MESSAGELOGLEVEL = {
Benjamin Jakimow's avatar
Benjamin Jakimow committed
283
284
285
286
287
288
    Qgis.Info: 'INFO',
    Qgis.Critical: 'INFO',
    Qgis.Warning: 'WARNING',
    Qgis.Success: 'SUCCESS',
}

Benjamin Jakimow's avatar
Benjamin Jakimow committed
289

290
291
292
def showMessage(message, title, level):
    v = QgsMessageViewer()
    v.setTitle(title)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
293
294
295
296
    if message.startswith('<html>'):
        v.setMessage(message, QgsMessageOutput.MessageHtml)
    else:
        v.setMessage(message, QgsMessageOutput.MessageText)
297
298
299
    v.showMessage(True)


Benjamin Jakimow's avatar
Benjamin Jakimow committed
300
class TaskManagerStatusButton(QToolButton):
Benjamin Jakimow's avatar
Benjamin Jakimow committed
301

Benjamin Jakimow's avatar
Benjamin Jakimow committed
302
    def __init__(self, parent: QWidget = None):
Benjamin Jakimow's avatar
Benjamin Jakimow committed
303
304
305
306
307
308
309
310
        super().__init__(parent)

        self.mManager = QgsApplication.taskManager()
        self.setAutoRaise(True)
        self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
        self.setLayout(QHBoxLayout())

        from eotimeseriesviewer.temporalprofiles import TemporalProfileLoaderTask
Benjamin Jakimow's avatar
Benjamin Jakimow committed
311
        from eotimeseriesviewer.timeseries import TimeSeriesLoadingTask, TimeSeriesFindOverlapTask
Benjamin Jakimow's avatar
Benjamin Jakimow committed
312
313
314

        self.mTrackedTasks = [
            TemporalProfileLoaderTask,
Benjamin Jakimow's avatar
Benjamin Jakimow committed
315
            TimeSeriesFindOverlapTask,
Benjamin Jakimow's avatar
Benjamin Jakimow committed
316
317
318
319
            TimeSeriesLoadingTask
        ]

        self.mInfoLabel = QLabel('', parent=self)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
320
321
322
        # self.setStyleSheet('background-color:yellow')
        # self.mInfoLabel.setStyleSheet('background-color:#234521;')
        # self.mInfoLabel.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
323
324
325
326
327
328
329
330
331
332

        self.mInfoLabel.setWordWrap(False)
        self.mProgressBar = QProgressBar(parent=self)
        self.mProgressBar.setMinimum(0)
        self.mProgressBar.setMaximum(100)
        self.mProgressBar.setMaximumWidth(100)
        self.mProgressBar.setMaximumHeight(18)
        self.layout().setContentsMargins(0, 0, 0, 0)
        self.layout().addWidget(self.mProgressBar)
        self.layout().addWidget(self.mInfoLabel)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
333
334
335
        #        self.layout().setStretchFactor(self.mInfoLabel, 2)
        # self.layout().addItem(QSpacerItem(0, 0, QSizePolicy.Expanding, QSizePolicy.Minimum))
        # self.clicked.connect(self.toggleDisplay)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
336
337
338
339
340
341
342
        """
        self.mFloatingWidget = QgsTaskManagerFloatingWidget( manager, parent ? parent->window() : nullptr );
        self.mFloatingWidget.setAnchorWidget( this );
        self.mFloatingWidget.setAnchorPoint( QgsFloatingWidget::BottomMiddle );
        self.mFloatingWidget.setAnchorWidgetPoint( QgsFloatingWidget::TopMiddle );
        self.mFloatingWidget.hide();
        """
Benjamin Jakimow's avatar
Benjamin Jakimow committed
343
        # self.hide()
Benjamin Jakimow's avatar
Benjamin Jakimow committed
344
345

        self.mManager.taskAdded.connect(self.onTaskAdded)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
346
347
348
        # self.mManager.allTasksFinished.connect(self.allFinished)
        # self.mManager.finalTaskProgressChanged.connect(self.overallProgressChanged)
        # self.mManager.countActiveTasksChanged.connect(self.countActiveTasksChanged)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
349
350
351
352
353
354

    def onTaskAdded(self, taskID):
        task = self.mManager.task(taskID)
        from eotimeseriesviewer.temporalprofiles import TemporalProfileLoaderTask
        from eotimeseriesviewer.timeseries import TimeSeriesLoadingTask

Benjamin Jakimow's avatar
Benjamin Jakimow committed
355
        if isinstance(task, (TemporalProfileLoaderTask, TimeSeriesFindOverlapTask, TimeSeriesLoadingTask)):
Benjamin Jakimow's avatar
Benjamin Jakimow committed
356
357
358
359
360
361
362
363
364
365
366
367
            task.progressChanged.connect(self.updateTaskInfo)
            task.taskCompleted.connect(self.updateTaskInfo)
            task.taskTerminated.connect(self.updateTaskInfo)

    def sizeHint(self):
        m = self.fontMetrics()
        txt = self.mInfoLabel.text()
        if hasattr(m, 'horizontalAdvance'):
            width = m.horizontalAdvance('X')
        else:
            width = m.width('X')
        width = int(width * 50 * Qgis.UI_SCALE_FACTOR)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
368
369
        # width = super().sizeHint().width()
        # width = width + self.mInfoLabel.sizeHint().width()
Benjamin Jakimow's avatar
Benjamin Jakimow committed
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
        height = super().sizeHint().height() - 5
        return QSize(width, height)

    def activeTasks(self) -> typing.List[QgsTask]:
        results = []
        for t in self.mManager.tasks():
            if isinstance(t, QgsTask):
                for taskType in self.mTrackedTasks:
                    if isinstance(t, taskType):
                        results.append(t)
                        break
        return results

    def updateTaskInfo(self, *args):
        n = 0
        p = 0.0
        activeTasks = self.activeTasks()
        activeTasks = self.activeTasks()
        for t in activeTasks:
            n += 1
            p += t.progress()

        if n == 0:
            p = 0
            self.mInfoLabel.setText('')
        else:
            self.mInfoLabel.setText(activeTasks[-1].description())
            p = int(round(p / n))
        self.mProgressBar.setValue(p)
        self.setVisible(n > 0)


Benjamin Jakimow's avatar
main.py    
Benjamin Jakimow committed
402
class EOTimeSeriesViewer(QgisInterface, QObject):
403
404
405
406
    _instance = None

    @staticmethod
    def instance():
407
408
409
410
        """
        Returns the TimeSeriesViewer instance
        :return:
        """
Benjamin Jakimow's avatar
main.py    
Benjamin Jakimow committed
411
        return EOTimeSeriesViewer._instance
412

413
414
    sigCurrentLocationChanged = pyqtSignal([SpatialPoint],
                                           [SpatialPoint, QgsMapCanvas])
415

416
417
    sigCurrentSpectralProfilesChanged = pyqtSignal(list)
    sigCurrentTemporalProfilesChanged = pyqtSignal(list)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
418
    currentLayerChanged = pyqtSignal(QgsMapLayer)
419
420

    def __init__(self):
unknown's avatar
unknown committed
421
422
423
424
425
426
427
        """Constructor.

        :param iface: An interface instance that will be passed to this class
            which provides the hook by which you can manipulate the QGIS
            application at run time.
        :type iface: QgsInterface
        """
428
429
        QObject.__init__(self)
        QgisInterface.__init__(self)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
430
        # QApplication.processEvents()
431

432
        assert EOTimeSeriesViewer.instance() is None, 'EOTimeSeriesViewer instance already exists.'
Benjamin Jakimow's avatar
main.py    
Benjamin Jakimow committed
433
434
        EOTimeSeriesViewer._instance = self
        self.ui = EOTimeSeriesViewerUI()
Benjamin Jakimow's avatar
Benjamin Jakimow committed
435

Benjamin Jakimow's avatar
Benjamin Jakimow committed
436
        # create status bar
Benjamin Jakimow's avatar
Benjamin Jakimow committed
437
        self.ui.statusBar().setStyleSheet("QStatusBar::item {border: none;}")
438

Benjamin Jakimow's avatar
Benjamin Jakimow committed
439
440
441
442
        # Drop the font size in the status bar by a couple of points
        statusBarFont = self.ui.font()
        fontSize = statusBarFont.pointSize()
        if os.name == 'windows':
Benjamin Jakimow's avatar
Benjamin Jakimow committed
443
            fontSize = max(fontSize - 1, 8)  # bit less on windows, due to poor rendering of small point sizes
Benjamin Jakimow's avatar
Benjamin Jakimow committed
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
        else:
            fontSize = max(fontSize - 2, 6)
        statusBarFont.setPointSize(fontSize)

        self.mStatusBar = QgsStatusBar()
        self.mStatusBar.setParentStatusBar(self.ui.statusBar())
        self.mStatusBar.setFont(statusBarFont)

        self.ui.statusBar().setFont(statusBarFont)
        self.ui.statusBar().addPermanentWidget(self.mStatusBar, 1)

        self.mTaskManagerButton = TaskManagerStatusButton(self.mStatusBar)
        self.mTaskManagerButton.setFont(statusBarFont)
        self.mStatusBar.addPermanentWidget(self.mTaskManagerButton, 10, QgsStatusBar.AnchorLeft)
        self.mTaskManagerButton.clicked.connect(lambda *args: self.ui.dockTaskManager.raise_())
459
460
        mvd = self.ui.dockMapViews
        dts = self.ui.dockTimeSeries
461
        mw = self.ui.mMapWidget
462
463
464
465

        self.mMapLayerStore = self.mapWidget().mMapLayerStore
        import eotimeseriesviewer.utils
        eotimeseriesviewer.utils.MAP_LAYER_STORES.insert(0, self.mapLayerStore())
466
467
468
469
470
471
        from eotimeseriesviewer.timeseries import TimeSeriesDock
        from eotimeseriesviewer.mapvisualization import MapViewDock, MapWidget
        assert isinstance(mvd, MapViewDock)
        assert isinstance(mw, MapWidget)
        assert isinstance(dts, TimeSeriesDock)

472
        def onClosed():
Benjamin Jakimow's avatar
main.py    
Benjamin Jakimow committed
473
            EOTimeSeriesViewer._instance = None
474

475
        self.ui.sigAboutToBeClosed.connect(onClosed)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
476
477
        import qgis.utils
        assert isinstance(qgis.utils.iface, QgisInterface)
478

479
480
        QgsApplication.instance().messageLog().messageReceived.connect(self.logMessage)

Benjamin Jakimow's avatar
main.py    
Benjamin Jakimow committed
481
        self.mapLayerStore().addMapLayer(self.ui.dockSpectralLibrary.speclib())
482

Benjamin Jakimow's avatar
Benjamin Jakimow committed
483
484
        self.mPostDataLoadingArgs: dict = dict()

485
        self.mVectorLayerTools: VectorLayerTools = VectorLayerTools()
Benjamin Jakimow's avatar
Benjamin Jakimow committed
486
487
488
489
        self.mVectorLayerTools.sigMessage.connect(lambda msg, level: self.logMessage(msg, LOG_MESSAGE_TAG, level))
        self.mVectorLayerTools.sigPanRequest.connect(self.setSpatialCenter)
        self.mVectorLayerTools.sigZoomRequest.connect(self.setSpatialExtent)
        self.mVectorLayerTools.sigEditingStarted.connect(self.updateCurrentLayerActions)
490

Benjamin Jakimow's avatar
Benjamin Jakimow committed
491
        # Save reference to the QGIS interface
492

493
        # init empty time series
494
        self.mTimeSeries = TimeSeries()
495
        self.mTimeSeries.setDateTimePrecision(DateTimePrecision.Day)
496
        self.mSpatialMapExtentInitialized = False
497
        self.mTimeSeries.sigTimeSeriesDatesAdded.connect(self.onTimeSeriesChanged)
498
        self.mTimeSeries.sigTimeSeriesDatesRemoved.connect(self.onTimeSeriesChanged)
499
        self.mTimeSeries.sigSensorAdded.connect(self.onSensorAdded)
500

Benjamin Jakimow's avatar
Benjamin Jakimow committed
501
        # self.mTimeSeries.sigMessage.connect(self.setM)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
502

503
        dts.setTimeSeries(self.mTimeSeries)
504
        self.ui.dockSensors.setTimeSeries(self.mTimeSeries)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
505
506
        self.ui.dockProfiles.setTimeSeries(self.mTimeSeries)
        self.ui.dockProfiles.setVectorLayerTools(self.mVectorLayerTools)
507
508
509
510
        mw.setTimeSeries(self.mTimeSeries)
        mvd.setTimeSeries(self.mTimeSeries)
        mvd.setMapWidget(mw)

Benjamin Jakimow's avatar
Benjamin Jakimow committed
511
        self.profileDock: ProfileViewDock = self.ui.dockProfiles
Benjamin Jakimow's avatar
main.py    
Benjamin Jakimow committed
512
        assert isinstance(self, EOTimeSeriesViewer)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
513
        self.profileDock.sigMoveToDate.connect(self.setCurrentDate)
514

515
516
517
        mw.sigSpatialExtentChanged.connect(self.timeSeries().setCurrentSpatialExtent)
        mw.sigVisibleDatesChanged.connect(self.timeSeries().setVisibleDates)
        mw.sigMapViewAdded.connect(self.onMapViewAdded)
518
        mw.sigCurrentLocationChanged.connect(self.setCurrentLocation)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
519
        mw.sigCurrentLayerChanged.connect(self.updateCurrentLayerActions)
520

521
        self.ui.optionSyncMapCenter.toggled.connect(self.mapWidget().setSyncWithQGISMapCanvas)
522
523
524
525
526
527
528
529
        tb = self.ui.toolBarTimeControl
        assert isinstance(tb, QToolBar)
        tb.addAction(mw.actionFirstDate)
        tb.addAction(mw.actionBackwardFast)
        tb.addAction(mw.actionBackward)
        tb.addAction(mw.actionForward)
        tb.addAction(mw.actionForwardFast)
        tb.addAction(mw.actionLastDate)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
530

531
532
        tstv = self.ui.dockTimeSeries.timeSeriesTreeView
        assert isinstance(tstv, TimeSeriesTreeView)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
533
        tstv.sigMoveToDate.connect(self.setCurrentDate)
534
        tstv.sigMoveToSource.connect(self.setCurrentSource)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
535
        tstv.sigMoveToExtent.connect(self.setSpatialExtent)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
536
        tstv.sigSetMapCrs.connect(self.setCrs)
537
538
539
        self.mCurrentMapLocation = None
        self.mCurrentMapSpectraLoading = 'TOP'

540
541
        self.ui.actionLockMapPanelSize.toggled.connect(self.lockCentralWidgetSize)

542
543
        def initMapToolAction(action, key):
            assert isinstance(action, QAction)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
544
545
            assert isinstance(key, MapTools)

546
547
            action.triggered.connect(lambda: self.setMapTool(key))
            action.setProperty('eotsv/maptoolkey', key)
548
549
            self.ui.registerMapToolAction(action)

550
551
552
553
554
555
556
        initMapToolAction(self.ui.actionPan, MapTools.Pan)
        initMapToolAction(self.ui.actionZoomIn, MapTools.ZoomIn)
        initMapToolAction(self.ui.actionZoomOut, MapTools.ZoomOut)
        initMapToolAction(self.ui.actionZoomPixelScale, MapTools.ZoomPixelScale)
        initMapToolAction(self.ui.actionZoomFullExtent, MapTools.ZoomFull)
        initMapToolAction(self.ui.actionIdentify, MapTools.CursorLocation)

Benjamin Jakimow's avatar
Benjamin Jakimow committed
557
558
559
        initMapToolAction(self.ui.mActionSelectFeatures, MapTools.SelectFeature)
        assert isinstance(self.ui.mActionSelectFeatures, QAction)
        initMapToolAction(self.ui.mActionAddFeature, MapTools.AddFeature)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
560

Benjamin Jakimow's avatar
Benjamin Jakimow committed
561
        self.ui.mActionZoomToLayer.triggered.connect(self.onZoomToLayer)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
562
        self.ui.mActionOpenTable.triggered.connect(self.onOpenTable)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
563

Benjamin Jakimow's avatar
Benjamin Jakimow committed
564
565
566
567
        self.ui.optionSelectFeaturesRectangle.triggered.connect(self.onSelectFeatureOptionTriggered)
        self.ui.optionSelectFeaturesPolygon.triggered.connect(self.onSelectFeatureOptionTriggered)
        self.ui.optionSelectFeaturesFreehand.triggered.connect(self.onSelectFeatureOptionTriggered)
        self.ui.optionSelectFeaturesRadius.triggered.connect(self.onSelectFeatureOptionTriggered)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
568
        self.ui.mActionDeselectFeatures.triggered.connect(self.deselectFeatures)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
569
570
571
572
573
574
575

        m = QMenu()
        m.addAction(self.ui.optionSelectFeaturesRectangle)
        m.addAction(self.ui.optionSelectFeaturesPolygon)
        m.addAction(self.ui.optionSelectFeaturesFreehand)
        m.addAction(self.ui.optionSelectFeaturesRadius)

Benjamin Jakimow's avatar
Benjamin Jakimow committed
576
        self.ui.mActionSelectFeatures.setMenu(m)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
577

Benjamin Jakimow's avatar
Benjamin Jakimow committed
578
579
580
581
582
583
        def onEditingToggled(b: bool):
            l = self.currentLayer()
            if b:
                self.mVectorLayerTools.startEditing(l)
            else:
                self.mVectorLayerTools.stopEditing(l, True)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
584

Benjamin Jakimow's avatar
Benjamin Jakimow committed
585
        self.ui.mActionToggleEditing.toggled.connect(onEditingToggled)
586

587
        # create edit toolbar
588
        tb = self.ui.toolBarVectorFeatures
589
590
591
        assert isinstance(tb, QToolBar)

        # set default map tool
592
        self.ui.actionPan.toggle()
593
594
        self.ui.dockCursorLocation.sigLocationRequest.connect(self.ui.actionIdentifyCursorLocationValues.trigger)
        self.ui.dockCursorLocation.mLocationInfoModel.setNodeExpansion(CursorLocationInfoModel.ALWAYS_EXPAND)
595
        self.ui.actionAddMapView.triggered.connect(mvd.createMapView)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
596
597
        self.ui.actionAddTSD.triggered.connect(lambda: self.addTimeSeriesImages(None))
        self.ui.actionAddVectorData.triggered.connect(lambda: self.addVectorData())
Benjamin Jakimow's avatar
main.py    
Benjamin Jakimow committed
598
        self.ui.actionAddSubDatasets.triggered.connect(self.openAddSubdatasetsDialog)
599
600

        # see https://sentinel.esa.int/web/sentinel/user-guides/sentinel-2-msi/data-formats/xsd
Benjamin Jakimow's avatar
main.py    
Benjamin Jakimow committed
601
        self.ui.actionAddSentinel2.triggered.connect(
602
            lambda: self.openAddSubdatasetsDialog(
Benjamin Jakimow's avatar
Benjamin Jakimow committed
603
                title='Open Sentinel-2 Datasets', filter='MTD_MSIL*.xml'))
Benjamin Jakimow's avatar
main.py    
Benjamin Jakimow committed
604

605
606
        self.ui.actionRemoveTSD.triggered.connect(lambda: self.mTimeSeries.removeTSDs(dts.selectedTimeSeriesDates()))
        self.ui.actionRefresh.triggered.connect(mw.refresh)
607
608
609
        self.ui.actionLoadTS.triggered.connect(self.loadTimeSeriesDefinition)
        self.ui.actionClearTS.triggered.connect(self.clearTimeSeries)
        self.ui.actionSaveTS.triggered.connect(self.saveTimeSeriesDefinition)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
610
        self.ui.actionAddTSExample.triggered.connect(lambda: self.loadExampleTimeSeries(loadAsync=True))
611
        self.ui.actionLoadTimeSeriesStack.triggered.connect(self.loadTimeSeriesStack)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
612
        # self.ui.actionShowCrosshair.toggled.connect(mw.setCrosshairVisibility)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
613
        self.ui.actionExportMapsToImages.triggered.connect(lambda: self.exportMapsToImages())
614

Benjamin Jakimow's avatar
Benjamin Jakimow committed
615
616
        self.ui.mActionLayerProperties.triggered.connect(self.onShowLayerProperties)

Benjamin Jakimow's avatar
Benjamin Jakimow committed
617
618
        from qgis.utils import iface
        self.ui.actionLoadProject.triggered.connect(iface.actionOpenProject().trigger)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
619
        self.ui.actionReloadProject.triggered.connect(self.onReloadProject)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
620
621
        self.ui.actionSaveProject.triggered.connect(iface.actionSaveProject().trigger)

Benjamin Jakimow's avatar
Benjamin Jakimow committed
622
        self.profileDock.actionLoadProfileRequest.triggered.connect(self.activateIdentifyTemporalProfileMapTool)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
623
624
        self.ui.dockSpectralLibrary.SLW.actionSelectProfilesFromMap.triggered.connect(
            self.activateIdentifySpectralProfileMapTool)
625

Benjamin Jakimow's avatar
Benjamin Jakimow committed
626
        # connect buttons with actions
627
        self.ui.actionAbout.triggered.connect(lambda: AboutDialogUI(self.ui).exec_())
628
629

        self.ui.actionSettings.triggered.connect(self.onShowSettingsDialog)
630
        import webbrowser
Benjamin Jakimow's avatar
Benjamin Jakimow committed
631
        from eotimeseriesviewer import DOCUMENTATION, SpectralLibrary, SpectralLibraryPanel, SpectralLibraryWidget
Benjamin Jakimow's avatar
Benjamin Jakimow committed
632
        self.ui.actionShowOnlineHelp.triggered.connect(lambda: webbrowser.open(DOCUMENTATION))
633

634
635
        SLW = self.ui.dockSpectralLibrary.spectralLibraryWidget()
        assert isinstance(SLW, SpectralLibraryWidget)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
636

637
638
639
640
641
        SLW.setMapInteraction(True)
        SLW.setCurrentProfilesMode(SpectralLibraryWidget.CurrentProfilesMode.automatically)
        SLW.sigMapExtentRequested.connect(self.setSpatialExtent)
        SLW.sigMapCenterRequested.connect(self.setSpatialCenter)

642
643
        # add time-specific fields
        sl = self.spectralLibrary()
Benjamin Jakimow's avatar
Benjamin Jakimow committed
644

645
646
647
648
649
650
        assert isinstance(sl, SpectralLibrary)
        sl.setName('EOTS Spectral Library')
        sl.startEditing()
        for field in EXTRA_SPECLIB_FIELDS:
            sl.addAttribute(field)
        assert sl.commitChanges()
651

652
        self.mMapLayerStore.addMapLayer(sl)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
653

Benjamin Jakimow's avatar
Benjamin Jakimow committed
654
        temporalProfileLayer = self.profileDock.temporalProfileLayer()
Benjamin Jakimow's avatar
Benjamin Jakimow committed
655
656
        assert isinstance(temporalProfileLayer, QgsVectorLayer)
        temporalProfileLayer.setName('EOTS Temporal Profiles')
657
        self.mMapLayerStore.addMapLayer(temporalProfileLayer)
658

659
660
        eotimeseriesviewer.labeling.MAP_LAYER_STORES.append(self.mMapLayerStore)
        eotimeseriesviewer.labeling.registerLabelShortcutEditorWidget()
661
662
        self.applySettings()

663
664
        self.initQGISConnection()

665
        for toolBar in self.ui.findChildren(QToolBar):
666
            fixMenuButtons(toolBar)
667

Benjamin Jakimow's avatar
Benjamin Jakimow committed
668
669
670
        self.ui.dockTimeSeries.setFloating(True)
        self.ui.dockTimeSeries.setFloating(False)

Benjamin Jakimow's avatar
Benjamin Jakimow committed
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
        QgsProject.instance().writeProject.connect(self.onWriteProject)
        QgsProject.instance().readProject.connect(self.onReadProject)

    def onWriteProject(self, dom: QDomDocument):

        node = dom.createElement('EOTSV')
        root = dom.documentElement()

        # save time series
        self.timeSeries().writeXml(node, dom)

        # save map views
        self.mapWidget().writeXml(node, dom)
        root.appendChild(node)

Benjamin Jakimow's avatar
Benjamin Jakimow committed
686
    def onReloadProject(self, *args):
Benjamin Jakimow's avatar
Benjamin Jakimow committed
687

Benjamin Jakimow's avatar
Benjamin Jakimow committed
688
689
690
691
692
693
694
695
        proj: QgsProject = QgsProject.instance()
        path = proj.fileName()
        if os.path.isfile(path):
            archive = None
            if QgsZipUtils.isZipFile(path):
                archive = QgsProjectArchive()
                archive.unzip(path)
                path = archive.projectFile()
Benjamin Jakimow's avatar
Benjamin Jakimow committed
696

Benjamin Jakimow's avatar
Benjamin Jakimow committed
697
            file = QFile(path)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
698

Benjamin Jakimow's avatar
Benjamin Jakimow committed
699
700
701
702
703
704
705
706
            doc = QDomDocument('qgis')
            doc.setContent(file)
            self.onReadProject(doc)

            if isinstance(archive, QgsProjectArchive):
                archive.clearProjectFile()

    def onReadProject(self, doc: QDomDocument) -> bool:
707
708
709
710
711
        """
        Reads images and visualization settings from a QgsProject QDomDocument
        :param doc: QDomDocument
        :return: bool
        """
Benjamin Jakimow's avatar
Benjamin Jakimow committed
712
713
714
715
        if not isinstance(doc, QDomDocument):
            return False

        root = doc.documentElement()
Benjamin Jakimow's avatar
Benjamin Jakimow committed
716
717
        node = root.firstChildElement('EOTSV')
        if node.nodeName() == 'EOTSV':
718
719
720
721
            self.timeSeries().clear()
            mapviews = self.mapViews()
            for mv in mapviews:
                self.mapWidget().removeMapView(mv)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
722
723
            self.mapWidget().readXml(node)

Benjamin Jakimow's avatar
Benjamin Jakimow committed
724
725
726
727
728
            mwNode = node.firstChildElement('MapWidget')
            if mwNode.nodeName() == 'MapWidget' and mwNode.hasAttribute('mapDate'):
                dt64 = datetime64(mwNode.attribute('mapDate'))
                if isinstance(dt64, np.datetime64):
                    self.mPostDataLoadingArgs['mapDate'] = dt64
729

Benjamin Jakimow's avatar
Benjamin Jakimow committed
730
731
732
            self.timeSeries().sigLoadingTaskFinished.connect(self.onPostDataLoading)
            self.timeSeries().readXml(node)

Benjamin Jakimow's avatar
Benjamin Jakimow committed
733
734
        return True

Benjamin Jakimow's avatar
Benjamin Jakimow committed
735
736
737
738
739
740
741
742
743
744
745
746
    def onPostDataLoading(self):
        """
        Handles actions that can be applied on a filled time series only, i.e. after sigLoadingTaskFinished was called.
        """
        if 'mapDate' in self.mPostDataLoadingArgs.keys():
            mapDate = self.mPostDataLoadingArgs.pop('mapDate')
            tsd = self.timeSeries().tsd(mapDate, None)
            if isinstance(tsd, TimeSeriesDate):
                self.setCurrentDate(tsd)

        self.timeSeries().sigLoadingTaskFinished.disconnect(self.onPostDataLoading)

Benjamin Jakimow's avatar
Benjamin Jakimow committed
747
    def lockCentralWidgetSize(self, b: bool):
748
749
750
751
752
753
754
755
756
757
758
759
760
761
        """
        Locks or release the current central widget size
        :param b:
        """
        w = self.ui.centralWidget()

        size = w.size()
        if b:
            w.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed))
            w.setMinimumSize(size)
        else:
            w.setSizePolicy(QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred))
            w.setMinimumSize(0, 0)

Benjamin Jakimow's avatar
Benjamin Jakimow committed
762
    def sensors(self) -> typing.List[SensorInstrument]:
763
764
765
766
767
768
        """
        Returns the list of Sensors
        :return: [list-of-Sensors]
        """
        return self.mTimeSeries.sensors()

Benjamin Jakimow's avatar
Benjamin Jakimow committed
769
770
771
772
773
774
    def activateIdentifyTemporalProfileMapTool(self, *args):
        """
        Activates the collection of temporal profiles
        """
        self.ui.actionIdentify.trigger()
        self.ui.optionIdentifyTemporalProfile.setChecked(True)
775

Benjamin Jakimow's avatar
Benjamin Jakimow committed
776
777
778
779
780
781
782
    def activateIdentifySpectralProfileMapTool(self, *args):
        """
        Activates the collection of spectral profiles
        """
        self.ui.actionIdentify.trigger()
        self.ui.optionIdentifySpectralProfile.setChecked(True)

Benjamin Jakimow's avatar
Benjamin Jakimow committed
783
    def _createProgressDialog(self, title='Load Data') -> QProgressDialog:
784
785
786
787
788
789
790
791
792
793
794
        """
        Creates a QProgressDialog to load image data
        :return: QProgressDialog
        """
        progressDialog = QProgressDialog(self.ui)
        progressDialog.setWindowTitle(title)
        progressDialog.setMinimumDuration(500)
        progressDialog.setValue(0)
        progressDialog.setWindowFlags(progressDialog.windowFlags() & ~Qt.WindowContextHelpButtonHint)
        return progressDialog

Benjamin Jakimow's avatar
Benjamin Jakimow committed
795
796
797
798
799
800
801
802
803
804
    def deselectFeatures(self):
        """
        Removes all feature selections (across all map canvases)
        """
        for canvas in self.mapCanvases():
            assert isinstance(canvas, QgsMapCanvas)
            for vl in [l for l in canvas.layers() if isinstance(l, QgsVectorLayer)]:
                assert isinstance(vl, QgsVectorLayer)
                vl.removeSelection()

Benjamin Jakimow's avatar
Benjamin Jakimow committed
805
806
807
808
    def exportMapsToImages(self, path=None, format='PNG'):
        """
        Exports the map canvases to local images.
        :param path: directory to save the images in
Benjamin Jakimow's avatar
main.py    
Benjamin Jakimow committed
809
        :param format: raster format, e.g. 'PNG' or 'JPG'
Benjamin Jakimow's avatar
Benjamin Jakimow committed
810
        """
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
        from .mapcanvas import MapCanvas
        from .mapvisualization import MapView
        from .settings import Keys, setValue, value
        import string

        if path is None:
            d = SaveAllMapsDialog()

            path = value(Keys.MapImageExportDirectory, default=None)
            if isinstance(path, str):
                d.setDirectory(path)

            if d.exec() != QDialog.Accepted:
                s = ""
                return

            format = d.fileType().lower()
            path = d.directory()

        else:
            format = format.lower()

        mapCanvases = self.mapCanvases()
        n = len(mapCanvases)
835
        progressDialog = self._createProgressDialog(title='Save Map Images...')
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
        progressDialog.setRange(0, n)

        valid_chars = "-_.() {}{}".format(string.ascii_letters, string.digits)

        for i, mapCanvas in enumerate(mapCanvases):
            if progressDialog.wasCanceled():
                return

            assert isinstance(mapCanvas, MapCanvas)
            mapCanvas.timedRefresh()
            tsd = mapCanvas.tsd()
            mv = mapCanvas.mapView()
            assert isinstance(mv, MapView)
            mapCanvas.waitWhileRendering()
            imgPath = '{}.{}.{}'.format(tsd.date(), mv.title(), format)

            imgPath = ''.join(c for c in imgPath if c in valid_chars)
            imgPath = imgPath.replace(' ', '_')
            imgPath = os.path.join(path, imgPath)

            mapCanvas.saveAsImage(imgPath, None, format)
            progressDialog.setValue(i + 1)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
858
            progressDialog.setLabelText('{}/{} maps saved'.format(i + 1, n))
859
860
861
862
863
864

            if progressDialog.wasCanceled():
                return

        setValue(Keys.MapImageExportDirectory, path)

Benjamin Jakimow's avatar
Benjamin Jakimow committed
865
    def onMapViewAdded(self, mapView: MapView):
866
867
868
869
870
        """

        :param mapView:
        :return:
        """
Benjamin Jakimow's avatar
Benjamin Jakimow committed
871
        mapView.addLayer(self.profileDock.temporalProfileLayer())
872
873
        mapView.addLayer(self.spectralLibrary())

Benjamin Jakimow's avatar
Benjamin Jakimow committed
874
    def temporalProfileLayer(self) -> TemporalProfileLayer:
Benjamin Jakimow's avatar
Benjamin Jakimow committed
875
876
877
878
        """
        Returns the TemporalProfileLayer
        :return:
        """
Benjamin Jakimow's avatar
Benjamin Jakimow committed
879
        return self.profileDock.temporalProfileLayer()
Benjamin Jakimow's avatar
Benjamin Jakimow committed
880

Benjamin Jakimow's avatar
Benjamin Jakimow committed
881
    def spectralLibrary(self) -> SpectralLibrary:
882
883
884
885
        """
        Returns the SpectraLibrary of the SpectralLibrary dock
        :return: SpectraLibrary
        """
886
        from .externals.qps.speclib.gui import SpectralLibraryPanel
887
888
889
890
        if isinstance(self.ui.dockSpectralLibrary, SpectralLibraryPanel):
            return self.ui.dockSpectralLibrary.SLW.speclib()
        else:
            return None
891

Benjamin Jakimow's avatar
main.py    
Benjamin Jakimow committed
892
893
894
895
896
    def openAddSubdatasetsDialog(self, *args,
                                 title: str = 'Open Subdatasets',
                                 filter: str = '*.*'):

        from .externals.qps.subdatasets import SubDatasetSelectionDialog
897

Benjamin Jakimow's avatar
main.py    
Benjamin Jakimow committed
898
899
900
901
902
903
904
905
        d = SubDatasetSelectionDialog()
        d.setWindowTitle(title)
        d.setFileFilter(filter)
        d.exec_()
        if d.result() == QDialog.Accepted:
            files = d.selectedSubDatasets()
            if len(files) > 0:
                self.addTimeSeriesImages(files)
906

907
908
909
    def close(self):
        self.ui.close()

Benjamin Jakimow's avatar
Benjamin Jakimow committed
910
911
912
913
914
915
916
    def actionCopyLayerStyle(self) -> QAction:
        return self.ui.mActionCopyLayerStyle

    def actionOpenTable(self) -> QAction:
        return self.ui.mActionOpenTable

    def actionZoomActualSize(self) -> QAction:
917
918
        return self.ui.actionZoomPixelScale

Benjamin Jakimow's avatar
Benjamin Jakimow committed
919
    def actionZoomFullExtent(self) -> QAction:
920
921
        return self.ui.actionZoomFullExtent

Benjamin Jakimow's avatar
Benjamin Jakimow committed
922
923
924
925
    def actionZoomToLayer(self) -> QAction:
        return self.ui.mActionZoomToLayer

    def actionZoomIn(self) -> QAction:
926
927
        return self.ui.actionZoomIn

Benjamin Jakimow's avatar
Benjamin Jakimow committed
928
    def actionZoomOut(self) -> QAction:
929
930
        return self.ui.actionZoomOut

Benjamin Jakimow's avatar
Benjamin Jakimow committed
931
932
933
934
935
936
937
938
939
    def actionPasteLayerStyle(self) -> QAction:
        return self.ui.mActionPasteLayerStyle

    def actionLayerProperties(self) -> QAction:
        return self.ui.mActionLayerProperties

    def actionToggleEditing(self) -> QAction:
        return self.ui.mActionToggleEditing

Benjamin Jakimow's avatar
Benjamin Jakimow committed
940
    def setCurrentLayer(self, layer: QgsMapLayer):
Benjamin Jakimow's avatar
Benjamin Jakimow committed
941
942
943
        self.mapWidget().setCurrentLayer(layer)
        self.updateCurrentLayerActions()

Benjamin Jakimow's avatar
Benjamin Jakimow committed
944
945
946
    def crs(self) -> QgsCoordinateReferenceSystem:
        return self.mapWidget().crs()

947
948
949
950
951
952
953
    def setCurrentSource(self, tss: TimeSeriesSource):
        """
        Moves the map view to a TimeSeriesSource
        """
        tss = self.timeSeries().findSource(tss)
        if isinstance(tss, TimeSeriesSource):
            self.ui.mMapWidget.setCurrentDate(tss.timeSeriesDate())
Benjamin Jakimow's avatar
Benjamin Jakimow committed
954
955
956
957
958
959
960
961
962
963
964
965
966

            ext = tss.spatialExtent().toCrs(self.crs())
            # set to new extent, but do not change the EOTSV CRS
            if isinstance(ext, SpatialExtent):
                self.setSpatialExtent(ext)
            else:
                # we cannot transform the coordinate. Try to set the EOTSV center only
                center = tss.spatialExtent().spatialCenter().toCrs(self.crs())
                if isinstance(center, SpatialPoint):
                    self.setSpatialCenter(center)
                else:
                    # last resort: we need to change the EOTSV Projection
                    self.setSpatialExtent(tss.spatialExtent())
967

968
    def setCurrentDate(self, tsd: TimeSeriesDate):
Benjamin Jakimow's avatar
Benjamin Jakimow committed
969
        """
970
        Moves the viewport of the scroll window to a specific TimeSeriesDate
971
        :param tsd:  TimeSeriesDate or numpy.datetime64
Benjamin Jakimow's avatar
Benjamin Jakimow committed
972
        """
973
974
975
        tsd = self.timeSeries().findDate(tsd)
        if isinstance(tsd, TimeSeriesDate):
            self.ui.mMapWidget.setCurrentDate(tsd)
976

Benjamin Jakimow's avatar
Benjamin Jakimow committed
977
    def mapCanvases(self) -> typing.List[MapCanvas]:
978
979
980
981
        """
        Returns all MapCanvases of the spatial visualization
        :return: [list-of-MapCanvases]
        """
982
        return self.ui.mMapWidget.mapCanvases()
983

Benjamin Jakimow's avatar
Benjamin Jakimow committed
984
    def mapLayerStore(self) -> QgsMapLayerStore:
985
986
987
988
989
        """
        Returns the QgsMapLayerStore which is used to register QgsMapLayers
        :return: QgsMapLayerStore
        """
        return self.mMapLayerStore
990

Benjamin Jakimow's avatar
Benjamin Jakimow committed
991
992
993
994
995
    def onOpenTable(self):
        c = self.currentLayer()
        if isinstance(c, QgsVectorLayer):
            self.showAttributeTable(c)

Benjamin Jakimow's avatar
Benjamin Jakimow committed
996
997
998
999
1000
    def onZoomToLayer(self):

        c = self.currentLayer()
        if isinstance(c, QgsMapLayer):
            self.setSpatialExtent(SpatialExtent.fromLayer(c))