# -*- coding: utf-8 -*- # noinspection PyPep8Naming """ /*************************************************************************** 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 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 * 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 from eotimeseriesviewer.externals.qps.layerproperties import showLayerPropertiesDialog #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, 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) self.actionAddEOTSVSpectralProfiles = QAction('Add Spectral Profile Layer') self.actionAddEOTSVTemporalProfiles = QAction('Add Temporal Profile Layer') def layerTreeView(self)->QgsLayerTreeView: return self.mLayerTreeView def layerTree(self)->QgsLayerTree: return self.layerTreeModel().rootGroup() def layerTreeModel(self)->QgsLayerTreeModel: return self.layerTreeView().model() def createContextMenu(self)->QMenu: model = self.layerTreeModel() ltree = self.layerTree() view = self.layerTreeView() g = view.currentGroupNode() l = view.currentLayer() i = view.currentIndex() #fixedNodes = len([l for l in view.selectedLayersRecursive() if l.property(KEY_LOCKED_LAYER) == True]) > 0 or \ # isinstance(g, QgsLayerTreeGroup) and g.property(KEY_LOCKED_LAYER) == True # disable actions #self.actionRemove.setEnabled(fixedNodes == False) menu = QMenu(view) isSensorGroup = isinstance(g, QgsLayerTreeGroup) and g.customProperty(KEY_SENSOR_GROUP) in [True, 'true'] isSensorLayer = isinstance(l, QgsRasterLayer) and l.customProperty(KEY_SENSOR_LAYER) in [True, 'true'] self.actionRemove.setEnabled(not (isSensorGroup or isSensorLayer)) self.actionAddGroup.setEnabled(not (isSensorGroup or isSensorLayer)) menu.addAction(self.actionAddGroup) menu.addAction(self.actionRename) menu.addAction(self.actionRemove) #menu.addAction(self.actionZoomToGroup) #menu.addAction(self.actionZoomToLayer) #menu.addAction(self.actionZoomToSelected) menu.addSeparator() menu.addAction(self.actionAddEOTSVSpectralProfiles) menu.addAction(self.actionAddEOTSVTemporalProfiles) menu.addSeparator() a = menu.addAction('Set Properties') a.triggered.connect(lambda *args, lyr=l, canvas=self.mDummyCanvas: showLayerPropertiesDialog(lyr, canvas)) a.setEnabled(not isinstance(l, SensorProxyLayer)) #a = menu.addAction('Settings') #from qps.layerproperties import showLayerPropertiesDialog #a.triggered.connect(lambda *args, lyr=l:showLayerPropertiesDialog(lyr, self._canvas)) return menu class MapViewLayerTreeModel(QgsLayerTreeModel): """ Layer Tree as shown in a MapView """ def __init__(self, rootNode, parent=None): super(MapViewLayerTreeModel, self).__init__(rootNode, parent=parent) def dataXXX(self, index:QModelIndex, role=Qt.DisplayRole): node = self.index2node(index) # if node.name() == 'testlayer': # s = "" if True: if isinstance(node, QgsLayerTreeGroup) and node.customProperty(KEY_SENSOR_GROUP) in ['true', True]: if role == Qt.FontRole: f = super(MapViewLayerTreeModel, self).data(index, role=role) f.setBold(True) return f if isinstance(node, QgsLayerTreeLayer) and node.customProperty(KEY_SENSOR_LAYER) in ['true', True]: if role == Qt.FontRole: f = super(MapViewLayerTreeModel, self).data(index, role=role) assert isinstance(f, QFont) f.setItalic(True) return f if role == Qt.DecorationRole: return QIcon(':/timeseriesviewer/icons/icon.svg') return super(MapViewLayerTreeModel, self).data(index, role=role) def flagsXXX(self, index:QModelIndex): f = super(MapViewLayerTreeModel, self).flags(index) node = self.index2node(index) if isinstance(node, QgsLayerTreeNode) and ( \ node.customProperty(KEY_SENSOR_LAYER) in ['true', True] or \ node.customProperty(KEY_SENSOR_GROUP) in ['true', True]): f = f ^ Qt.ItemIsDragEnabled f = f ^ Qt.ItemIsDropEnabled return f class MapView(QFrame, loadUIFormClass(jp(DIR_UI, 'mapview.ui'))): """ A MapView defines how a single map canvas visualizes sensor specific EOTS data plus additional vector overlays """ sigRemoveMapView = pyqtSignal(object) sigMapViewVisibility = pyqtSignal(bool) sigTitleChanged = pyqtSignal(str) sigSensorRendererChanged = pyqtSignal(SensorInstrument, QgsRasterRenderer) sigShowProfiles = pyqtSignal(SpatialPoint, MapCanvas, str) def __init__(self, name='Map View', parent=None): super(MapView, self).__init__(parent) self.setupUi(self) 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.mCurrentLayer = None 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) self.mLayerTree = QgsLayerTree() self.mLayerTreeMapCanvasBridget = QgsLayerTreeMapCanvasBridge(self.mLayerTree, self.mDummyCanvas) # self.mLayerTreeModel = QgsLayerTreeModel(self.mLayerTree) self.mLayerTreeModel = MapViewLayerTreeModel(self.mLayerTree) self.mLayerTreeModel.setFlags(QgsLayerTreeModel.AllowNodeChangeVisibility | QgsLayerTreeModel.AllowNodeRename | QgsLayerTreeModel.AllowNodeReorder) self._createSensorNode() self.mLayerTreeView.setModel(self.mLayerTreeModel) self.mMapLayerTreeViewMenuProvider = MapViewLayerTreeViewMenuProvider(self.mLayerTreeView, self.mDummyCanvas) # register some actions that interact with other GUI elements self.mMapLayerTreeViewMenuProvider.actionAddEOTSVSpectralProfiles.triggered.connect(self.addSpectralProfileLayer) self.mMapLayerTreeViewMenuProvider.actionAddEOTSVTemporalProfiles.triggered.connect(self.addTemporalProfileLayer) self.mLayerTreeView.setMenuProvider(self.mMapLayerTreeViewMenuProvider) self.mLayerTreeView.currentLayerChanged.connect(self.setCurrentLayer) self.mLayerTree.removedChildren.connect(self.onChildNodesRemoved) self.mIsVisible = True self.setTitle(name) 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): 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) 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): """ Sets the map view visibility :param b: bool """ assert isinstance(b, bool) changed = False for mapCanvas in self.mapCanvases(): assert isinstance(mapCanvas, MapCanvas) if not mapCanvas.isVisible() == b: changed = True mapCanvas.setVisible(b) if self.actionToggleMapViewHidden.isChecked() == b: self.actionToggleMapViewHidden.setChecked(not b) if changed: self.sigMapViewVisibility.emit(b) def isVisible(self)->bool: """ Returns the map view visibility :return: bool """ return not self.actionToggleMapViewHidden.isChecked() 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 setTitle(self, title:str): """ Sets the widget title :param title: str """ old = self.title() if old != title: self.tbName.setText(title) def layers(self)->list: """ Returns the visible layers, including proxy layer for time-series data :return: [list-of-QgsMapLayers] """ return [l for l in self.mLayerTree.checkedLayers() if isinstance(l, QgsMapLayer)] def title(self)->str: """ Returns the MapView title :return: str """ return self.tbName.text() def refreshMapView(self, sensor=None): for mapCanvas in self.mapCanvases(): if isinstance(mapCanvas, MapCanvas): mapCanvas.refresh() def setCrosshairStyle(self, crosshairStyle:CrosshairStyle): """ 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) def setHighlighted(self, b=True, timeout=1000): """ Activates or deactivates a red-line border of the MapCanvases :param b: True | False to activate / deactivate the highlighted lines- :param timeout: int, milliseconds how long the highlighted frame should appear """ styleOn = """.MapCanvas { border: 4px solid red; border-radius: 4px; }""" styleOff = """""" if b is True: for mapCanvas in self.mapCanvases(): mapCanvas.setStyleSheet(styleOn) if timeout > 0: QTimer.singleShot(timeout, lambda : self.setHighlighted(False)) else: for mapCanvas in self.mapCanvases(): mapCanvas.setStyleSheet(styleOff) def 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.setMapView(self) mapCanvas.sigCrosshairVisibilityChanged.connect(self.setCrosshairVisibility) mapCanvas.sigCrosshairStyleChanged.connect(self.setCrosshairStyle) self.mMapCanvases.append(mapCanvas) self.sigMapViewVisibility.connect(mapCanvas.setEnabled) 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): """ Enables / diables the map canvas crosshair. :param b: bool """ 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) def sensorProxyLayers(self)->list: layers = [n.layer() for n in self.mLayerTreeSensorNode.findLayers()] return [l for l in layers if isinstance(l, SensorProxyLayer)] def sensorProxyLayer(self, sensor:SensorInstrument)->SensorProxyLayer: """ Returns the proxy layer related to a SensorInstrument :param sensor: SensorInstrument :return: SensorLayer """ for l in self.sensorProxyLayers(): if l.sensor() == sensor: return l return None def sensors(self)->list: """ Returns a list of SensorsInstruments :return: [list-of-SensorInstruments] """ return [t[0] for t in self.mSensorLayerList] def addSensor(self, sensor:SensorInstrument): """ Adds a SensorInstrument to be shown in this MapView. Each sensor will be represented as a Raster Layer in the Tree Model. :param sensor: SensorInstrument """ assert isinstance(sensor, SensorInstrument) if sensor not in self.sensors(): dummyLayer = sensor.proxyLayer() dummyLayer.rendererChanged.connect(lambda sensor=sensor: self.onSensorRendererChanged(sensor)) #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 :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) def hasSensor(self, sensor)->bool: """ :param sensor: :return: """ assert isinstance(sensor, SensorInstrument) return sensor in self.sensors() class DatumViewUI(QFrame, loadUI('timeseriesdatumview.ui')): """ 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()) #print(s) 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 """ class DatumView(QObject): 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) assert isinstance(stv, SpatialTemporalVisualization) super(DatumView, self).__init__() self.ui = DatumViewUI(parent=parent) self.ui.create() self.showLoading(False) self.wOffset = self.ui.layout().count()-1 self.minHeight = self.ui.height() #self.minWidth = 50 self.renderProgress = dict() assert isinstance(stv, SpatialTemporalVisualization) self.STV = stv self.TSD = timeSeriesDatum self.scrollArea = stv.scrollArea self.mSensor = self.TSD.mSensor self.mSensor.sigNameChanged.connect(lambda :self.setColumnInfo()) self.TSD.sigVisibilityChanged.connect(self.setVisibility) self.setColumnInfo() self.MVC = stv.MVC self.DVC = stv.DVC self.mMapCanvases = dict() self.mRenderState = dict() 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()]) self.ui.labelTitle.setText(labelTxt) self.ui.labelTitle.setToolTip(tooltip) def setVisibility(self, b): self.ui.setVisible(b) self.sigVisibilityChanged.emit(b) 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) def removeMapView(self, mapView): canvas = self.mMapCanvases.pop(mapView) self.ui.layout().removeWidget(canvas) 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. """ if self.ui.isVisible(): for c in self.mapCanvases(): if c.isVisible(): c.refresh() def insertMapView(self, mapView): assert isinstance(mapView, MapView) from eotimeseriesviewer.mapcanvas import MapCanvas mapCanvas = MapCanvas(self.ui) mapCanvas.setObjectName('MapCanvas {} {}'.format(mapView.title(), self.TSD.mDate)) self.registerMapCanvas(mapView, mapCanvas) mapCanvas.setMapView(mapView) mapCanvas.setTSD(self.TSD) mapView.registerMapCanvas(mapCanvas) self.STV.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)) 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()] # print((renderFlags, drawFlags)) isLoading = any(renderFlags) self.showLoading(isLoading) s = "" def registerMapCanvas(self, mapView, mapCanvas): from eotimeseriesviewer.mapcanvas import MapCanvas assert isinstance(mapCanvas, MapCanvas) assert isinstance(mapView, MapView) self.mMapCanvases[mapView] = mapCanvas 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) def onMapCanvasRequest(self, mapCanvas, key): if key == 'hide_date': self.TSD.setVisibility(False) if key == 'copy_sensor': QApplication.clipboard().setText(self.TSD.mSensor.name()) if key == 'copy_date': QApplication.clipboard().setText(str(self.TSD.date())) if key == 'copy_path': QApplication.clipboard().setText('\n'.join(self.TSD.sourceUris())) def __lt__(self, other): assert isinstance(other, DatumView) return self.TSD < other.TSD def __eq__(self, other): """ :param other: :return: """ assert isinstance(other, DatumView) return self.TSD == other.TSD class DateViewCollection(QObject): 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) self.setFocusView(None) 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') 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.insertMapView(mapView) #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) self.sigResizeRequired.emit() 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 setFocusView(self, tsd): self.focusView = tsd 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) """ def addDates(self, tsdList): """ Create a new TSDView :param tsdList: :return: """ 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() if len(tsdList) > 0: self.sigResizeRequired.emit() 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 if len(removedDates) > 0: self.sigResizeRequired.emit() def __len__(self): return len(self.mViews) def __iter__(self): return iter(self.mViews) def __getitem__(self, slice): return self.mViews[slice] def __delitem__(self, slice): self.removeDates(self.mViews[slice]) class MapViewListModel(QAbstractListModel): """ A model to store a list of map views. """ sigMapViewsAdded = pyqtSignal(list) sigMapViewsRemoved = pyqtSignal(list) def __init__(self, parent=None): super(MapViewListModel, self).__init__(parent) self.mMapViewList = [] 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) 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) def doRefresh(self, mapViews): for mapView in mapViews: idx = self.mapView2idx(mapView) self.dataChanged.emit(idx, idx) def removeMapView(self, mapView): self.removeMapViews([mapView]) 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) def rowCount(self, parent=None, *args, **kwargs): return len(self.mMapViewList) 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) def __len__(self): return len(self.mMapViewList) def __iter__(self): return iter(self.mMapViewList) 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) value = None 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.baseTitle = self.windowTitle() 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() self.mTimeSeries = None def mapViews(self)->list: """ 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 def mapCanvases(self)->list: """ Returns all MapCanvases from all MapViews :return: [list-of-MapCanvases] """ maps = [] for mapView in self.mapViews(): assert isinstance(mapView, MapView) maps.extend(mapView.mapCanvases()) return maps def setCrs(self, crs): 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 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) def mapSize(self): return QSize(self.spinBoxMapSizeX.value(), self.spinBoxMapSizeY.value()) def refreshCurrentMapView(self, *args): mv = self.currentMapView() if isinstance(mv, MapView): mv.refreshMapView() else: s ="" def dummySlot(self): s ="" def onMapViewsRemoved(self, mapViews): for mapView in mapViews: idx = self.stackedWidget.indexOf(mapView.ui) if idx >= 0: self.stackedWidget.removeWidget(mapView.ui) mapView.ui.close() else: s = "" self.actionRemoveMapView.setEnabled(len(self.mMapViews) > 0) def mapBackgroundColor(self)->QColor: """ Returns the map canvas background color :return: QColor """ return self.btnMapCanvasColor.color() def setMapBackgroundColor(self, color:QColor): """ Sets the MapCanvas background color :param color: QColor """ if color != self.mapBackgroundColor(): self.btnMapCanvasColor.setColor(color) 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) self.sigMapCanvasColorChanged.emit(color) 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 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) for mapView in mapViews: self.sigMapViewAdded.emit(mapView) def updateButtons(self, *args): b = len(self.mMapViews) > 0 self.actionRemoveMapView.setEnabled(b) self.actionApplyStyles.setEnabled(b) self.actionHighlightMapView.setEnabled(b) def createMapView(self)->MapView: """ Create a new MapView :return: MapView """ mapView = MapView() n = len(self.mapViews()) + 1 title = 'Map View {}'.format(n) 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) mapView.setTimeSeries(self.mTimeSeries) self.addMapView(mapView) return mapView def addMapView(self, mapView:MapView): """ Adds a MapView :param mapView: MapView """ 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) def onToolboxIndexChanged(self): b = isinstance(self.toolBox.currentWidget(), MapView) self.actionRemoveMapView.setEnabled(b) 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 isinstance(item, MapView): numMV += 1 if item == mapView: if mapView.isVisible(): icon = QIcon(":/timeseriesviewer/icons/mapview.svg") else: icon = QIcon(":/timeseriesviewer/icons/mapviewHidden.svg") self.toolBox.setItemIcon(i, icon) self.toolBox.setItemText(i, 'Map View {} "{}"'.format(numMV, mapView.title())) break 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)) self.sigMapViewRemoved.emit(mapView) return mapView def __len__(self)->int: """ Returns the number of MapViews :return: int """ return len(self.mapViews()) def __iter__(self): """ Provides an iterator over all MapViews :return: """ return iter(self.mapViews()) def __getitem__(self, slice): return self.mapViews()[slice] def __contains__(self, mapView): return mapView in self.mapViews() 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) def setCurrentMapView(self, mapView): assert isinstance(mapView, MapView) and mapView in self.mMapViews idx = self.stackedWidget.indexOf(mapView.ui) if idx >= 0: self.stackedWidget.setCurrentIndex(idx) self.cbMapView.setCurrentIndex(self.mMapViews.mapView2idx(mapView).row()) self.updateTitle() def updateTitle(self, *args): # self.btnToggleMapViewVisibility.setChecked(mapView) mapView = self.currentMapView() if isinstance(mapView, MapView): if mapView in self.mMapViews: i = str(self.mMapViews.mapView2idx(mapView).row()+1) else: i = '' #title = '{} | {} "{}"'.format(self.baseTitle, i, mapView.title()) title = '{} | {}'.format(self.baseTitle, i) self.setWindowTitle(title) def currentMapView(self): w = self.toolBox.currentWidget() if isinstance(w, MapView): return w return None class SpatialTemporalVisualization(QObject): """ """ 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() # map-tool handling self.mMapTools = [] 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 def setCurrentLayer(self, layer): assert layer is None or isinstance(layer, QgsMapLayer) self.mCurrentLayer = layer for canvas in self.mapCanvases(): assert isinstance(canvas, QgsMapCanvas) canvas.setCurrentLayer(layer) def syncQGISCanvasCenter(self, qgisChanged:bool): if self.mSyncLock: return iface = qgis.utils.iface assert isinstance(iface, QgisInterface) c = iface.mapCanvas() assert isinstance(c, QgsMapCanvas) tsvCenter = self.spatialExtent().spatialCenter() qgsCenter = SpatialExtent.fromMapCanvas(c).spatialCenter() if qgisChanged: # change TSV if tsvCenter.crs().isValid(): self.mSyncLock = True qgsCenter = qgsCenter.toCrs(tsvCenter.crs()) self.setSpatialCenter(qgsCenter) else: # change QGIS if qgsCenter.crs().isValid(): self.mSyncLock = True tsvCenter = tsvCenter.toCrs(qgsCenter.crs()) c.setCenter(tsvCenter) else: pass 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)) def timedCanvasRefresh(self, *args, force:bool=False): self.mSyncLock = False # do refresh maps assert isinstance(self.scrollArea, MapViewScrollArea) 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 if n < 10: # refresh up to mNumberOfHiddenMapsToRefresh maps which are not visible to the user i = 0 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 = "" def createMapView(self)->MapView: """ Create a new MapWiew :return: MapView """ return self.MVC.createMapView() 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)) self.mMapCanvases.append(mapCanvas) # set general canvas properties mapCanvas.setFixedSize(self.mSize) mapCanvas.setDestinationCrs(self.mCRS) mapCanvas.setSpatialExtent(self.mSpatialExtent) # register on map canvas signals def onChanged(e, mapCanvas0=None): #self.setSpatialExtent(e, mapCanvas0=mapCanvas0) self.setSpatialExtent(e) #mapCanvas.sigSpatialExtentChanged.connect(lambda e: self.setSpatialExtent(e, mapCanvas0=mapCanvas)) #mapCanvas.sigSpatialExtentChanged.connect(lambda e: onChanged(e, mapCanvas0=mapCanvas)) mapCanvas.sigSpatialExtentChanged.connect(self.setSpatialExtent) mapCanvas.sigCrosshairPositionChanged.connect(self.onCrosshairChanged) 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) def setMapSize(self, size): 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() def mapSize(self)->QSize: """ Returns the MapCanvas size :return: QSize """ return QSize(self.mSize) def refresh(self): """ Refreshes all visible MapCanvases """ for c in self.mapCanvases(): assert isinstance(c, MapCanvas) c.refresh() #self.mMapRefreshTimer.stop() 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 setMapTool(self, mapToolKey, *args, **kwds): """ Create a maptool instance to each MapCanvas :param mapToolKey: str which MapTool is to create, or QgsMapTool instance :param args: optional maptool arguments :param kwds: optional maptool keywords :return: [list-of-QgsMapTools] """ del self.mMapTools[:] from eotimeseriesviewer import MapTools, CursorLocationMapTool, SpectralProfileMapTool, TemporalProfileMapTool for canvas in self.mMapCanvases: mt = None if mapToolKey in MapTools.mapToolKeys(): mt = MapTools.create(mapToolKey, canvas, *args, **kwds) if isinstance(mapToolKey, QgsMapTool): mt = MapTools.copy(mapToolKey, canvas, *args, **kwds) if isinstance(mt, QgsMapTool): canvas.setMapTool(mt) self.mMapTools.append(mt) # if required, link map-tool with specific slots if isinstance(mt, CursorLocationMapTool): mt.sigLocationRequest[SpatialPoint, QgsMapCanvas].connect(self.onLocationRequest) return self.mMapTools def onLocationRequest(self, pt:SpatialPoint, canvas:QgsMapCanvas): self.sigShowProfiles.emit(pt, canvas, "") def setSpatialCenter(self, center:SpatialPoint, mapCanvas0=None): """ Sets the spatial center of all MapCanvases :param center: SpatialPoint :param mapCanvas0: """ assert isinstance(center, SpatialPoint) extent = self.spatialExtent() if isinstance(extent, SpatialExtent): centerOld = extent.center() center = center.toCrs(extent.crs()) if center != centerOld: extent = extent.__copy__() extent.setCenter(center) self.setSpatialExtent(extent) def spatialCenter(self)->SpatialPoint: return self.spatialExtent().spatialCenter() def setSpatialExtent(self, extent, mapCanvas0=None): """ Sets the spatial extent of all MapCanvases :param extent: SpatialExtent :param mapCanvas0: :return: """ lastExtent = self.spatialExtent() assert isinstance(extent, 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) mapCanvas.addToRefreshPipeLine(extent) if lastExtent != extent: self.sigSpatialExtentChanged.emit(extent) def mapViewDock(self)->MapViewDock: """ Returns the MapViewDock that controls all MapViews :return: MapViewDock """ return self.MVC def setMapBackgroundColor(self, color:QColor): """ Sets the MapCanvas background color :param color: QColor """ assert isinstance(self.MVC, MapViewDock) self.MVC.setMapBackgroundColor(color) def mapCanvases(self, mapView=None)->list: """ 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[:] def mapViews(self)->list: """ Returns a list of all mapviews :return [list-of-MapViews]: """ return self.MVC[:] 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 """ return self.mSpatialExtent 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)