Skip to content
Snippets Groups Projects
mapvisualization.py 68.9 KiB
Newer Older
        if isinstance(crs, QgsCoordinateReferenceSystem):
            for c in self.mapCanvases():
                c.setCrs(crs)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        """
        Calls the timedRefresh() routine for all MapCanvases
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        """
        for c in self.mapCanvases():
            assert isinstance(c, MapCanvas)
            c.timedRefresh()

    def currentLayers(self):

        layers = set()
        for c in self.mapCanvases():
            layers = layers.union(set(c.layers()))
        return list(layers)


    def crs(self)->QgsCoordinateReferenceSystem:
        return self.mCrs
    def setTimeSeries(self, ts:TimeSeries)->TimeSeries:
        assert ts == None or isinstance(ts, TimeSeries)
        self.mTimeSeries = ts
        if isinstance(self.mTimeSeries, TimeSeries):
            self.mTimeSeries.sigVisibilityChanged.connect(self._updateCanvasDates)
            self.mTimeSeries.sigTimeSeriesDatesRemoved.connect(self._updateCanvasDates)
            self.mTimeSeries.sigTimeSeriesDatesAdded.connect(self._updateSliderRange)
            self.mTimeSeries.sigTimeSeriesDatesRemoved.connect(self._updateSliderRange)

            self._updateSliderRange()

    def _updateSliderRange(self):

        n = len(self.timeSeries())
        self.mTimeSlider.setRange(0, n)
        self.mTimeSlider.setEnabled(n > 0)
        if n > 0:
            tsd = self.currentDate()

            if isinstance(tsd, TimeSeriesDate) and tsd in self.timeSeries():
                i = self.timeSeries()[:].index(tsd)
                self.mTimeSlider.setValue(i+1)

    def onSliderReleased(self):

        i = self.mTimeSlider.value() - 1
        if isinstance(self.mTimeSeries, TimeSeries) and len(self.mTimeSeries) > 0:
            i = min(i, len(self.mTimeSeries)-1)
            i = max(i,  0)
            tsd = self.mTimeSeries[i]
            self.setCurrentDate(tsd)


    def timeSeries(self)->TimeSeries:
        return self.mTimeSeries
    def setMode(self, mode:ViewMode):
        if mode != self.mViewMode:
            self.mViewMode = mode
            self._updateGrid()
            self.sigViewModeChanged.emit(self.mViewMode)

    def setMapsPerMapView(self, n:int):
        assert n >= 0
        if n != self.mMpMV:
            self.mMpMV = n
            self._updateGrid()
            self.timeSlider().setPageStep(max(1, n))
            self.sigMapsPerMapViewChanged.emit(n)
    def setMapSize(self, size:QSize)->QSize:
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        """
        Sets the MapCanvas size
        :param size: QSite
        :return: QSize
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        """
        if size != self.mMapSize:
            for canvas in self.mapCanvases():
                canvas.setFixedSize(size)
            self.mMapSize = size
            self._updateWidgetSize()
            self.sigMapSizeChanged.emit(size)
    def mapSize(self)->QSize:
        """
        Returns the MapCanvas size
        :return: QSize
        """
        return self.mMapSize
    def mapCanvases(self)->list:
        """
        Returns all MapCanvases
        :return: [list-of-MapCanvases]
        """
        return self.findChildren(MapCanvas)
Benjamin Jakimow's avatar
Benjamin Jakimow committed

    def mapViewCanvases(self, mapView:MapView)->list:
        Returns the MapCanvases related to a MapView
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        :param mapView: MapView
        :return: [list-of-MapCanvases]
        return self.mCanvases[mapView]
    def moveToNextTSD(self):
        for tsd in self.timeSeries()[:]:
            assert isinstance(tsd, TimeSeriesDate)
            if tsd > self.currentDate() and tsd.isVisible():
                self.setCurrentDate(tsd)
                return
        s = ""
    def moveToPreviousTSD(self):
        for tsd in reversed(self.timeSeries()[:]):
            if tsd < self.currentDate() and tsd.isVisible():
                self.setCurrentDate(tsd)
                return
        s = ""
    def moveToNextTSDFast(self):
        visible = list([tsd for tsd in self.timeSeries() if tsd.isVisible() and tsd > self.currentDate()])
        if len(visible) > 0 and self.mMpMV > 0:
            i = min(self.mMpMV-1, len(visible)-1)
            self.setCurrentDate(visible[i])
Benjamin Jakimow's avatar
Benjamin Jakimow committed

    def moveToPreviousTSDFast(self):
        visible = list(reversed([tsd for tsd in self.timeSeries() if tsd.isVisible() and tsd < self.currentDate()]))
        if len(visible) > 0 and self.mMpMV > 0:
            i = min(self.mMpMV - 1, len(visible)-1)
            self.setCurrentDate(visible[i])
    def moveToFirstTSD(self):
        for tsd in self.timeSeries()[:]:
            if tsd.isVisible():
                self.setCurrentDate(tsd)
                return
        s = ""


    def moveToLastTSD(self):
        for tsd in reversed(self.timeSeries()[:]):
            if tsd.isVisible():
                self.setCurrentDate(tsd)
                return
        s  = ""

    def setCurrentDate(self, tsd:TimeSeriesDate)->TimeSeriesDate:
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        """
        Sets the current TimeSeriesDate, i.e. the "center" date of all dates to be shown
        :param tsd: TimeSeriesDate
        :return: TimeSeriesDate
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        """
        assert isinstance(tsd, TimeSeriesDate)
        b = tsd != self.mCurrentDate
        self.mCurrentDate = tsd

            i = self.mTimeSeries[:].index(self.mCurrentDate) + 1

            if self.mTimeSlider.value() != i:
                self.mTimeSlider.setValue(i)
            self.sigCurrentDateChanged.emit(self.mCurrentDate)


        if isinstance(self.currentDate(), TimeSeriesDate):
            i = self.timeSeries()[:].index(self.currentDate())
            canForward = i < len(self.mTimeSeries) - 1
            canBackward = i > 0
        else:
            canForward = canBackward = False

        for a in [self.actionForward, self.actionForwardFast, self.actionLastDate]:
            a.setEnabled(canForward)

        for a in [self.actionBackward, self.actionBackwardFast, self.actionFirstDate]:
            a.setEnabled(canBackward)

    def currentDate(self)->TimeSeriesDate:
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        """
        Returns the current TimeSeriesDate
        :return: TimeSeriesDate
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        """
    def addMapView(self, mapView:MapView)->MapView:
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        """
        Adds a MapView
        :param mapView: MapView
        :return: MapView
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        """
        assert isinstance(mapView, MapView)
        if mapView not in self.mMapViews:
            self.mMapViews.append(mapView)
            # connect signals
            mapView.sigCanvasAppearanceChanged.connect(self._updateCanvasAppearance)
            mapView.sigCrosshairChanged.connect(self._updateCrosshair)
Benjamin Jakimow's avatar
Benjamin Jakimow committed

            self._updateGrid()
            self._updateCrosshair(mapView=mapView)
            self.sigMapViewsChanged.emit()
            self.sigMapViewAdded.emit(mapView)

        return mapView

    def removeMapView(self, mapView:MapView)->MapView:
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        """
        Removes a MapView
        :param mapView: Mapview
        :return: MapView
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        """
        if mapView in self.mMapViews:
            self.mMapViews.remove(mapView)
            mapView.setMapWidget(None)
            # disconnect signals
            self._updateGrid()
            self.sigMapViewsChanged.emit()
            self.sigMapViewRemoved.emit(mapView)
        return mapView

    def mapViews(self)->list:
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        """
        Returns a list of all MapViews
        :return: [list-of-MapViews]
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        """
Benjamin Jakimow's avatar
Benjamin Jakimow committed

    def syncQGISCanvasCenter(self, qgisChanged:bool):
Benjamin Jakimow's avatar
Benjamin Jakimow committed

        iface = qgis.utils.iface
        assert isinstance(iface, QgisInterface)
Benjamin Jakimow's avatar
Benjamin Jakimow committed

        c = iface.mapCanvas()
        if not isinstance(c, QgsMapCanvas):
            return
Benjamin Jakimow's avatar
Benjamin Jakimow committed

        tsvCenter = self.spatialExtent().spatialCenter()
        qgsCenter = SpatialExtent.fromMapCanvas(c).spatialCenter()
Benjamin Jakimow's avatar
Benjamin Jakimow committed

        if qgisChanged:
            # change EOTSV
            if tsvCenter.crs().isValid():
                self.mSyncLock = True
                qgsCenter = qgsCenter.toCrs(tsvCenter.crs())
                if isinstance(qgsCenter, SpatialPoint):
                    self.setSpatialCenter(qgsCenter)
                QApplication.processEvents()
                self.mSyncLock = False
Benjamin Jakimow's avatar
Benjamin Jakimow committed

        else:
            # change QGIS
            if qgsCenter.crs().isValid():
                self.mSyncLock = True
                tsvCenter = tsvCenter.toCrs(qgsCenter.crs())
                if isinstance(tsvCenter, SpatialPoint):
                    c.setCenter(tsvCenter)
                QApplication.processEvents()
                self.mSyncLock = False
    def _createMapCanvas(self)->MapCanvas:
        mapCanvas = MapCanvas()
        mapCanvas.setMapLayerStore(self.mMapLayerStore)
        mapCanvas.mInfoItem.setTextFormat(self.mapTextFormat())

        # set general canvas properties
        mapCanvas.setFixedSize(self.mMapSize)
        mapCanvas.setDestinationCrs(self.mCrs)
        mapCanvas.setSpatialExtent(self.mSpatialExtent)

        # activate the current map tool
        mapTools = mapCanvas.mapTools()
        mapTools.activate(self.mMapToolKey)

        mt = mapCanvas.mapTool()
        if isinstance(mt, QgsMapToolSelect):
            mt.setSelectionMode(self.mMapToolMode)

        # connect signals
        self._connectCanvasSignals(mapCanvas)
        return mapCanvas

    def _connectCanvasSignals(self, mapCanvas:MapCanvas):
        mapCanvas.sigSpatialExtentChanged.connect(self.setSpatialExtent)
        mapCanvas.sigDestinationCrsChanged.connect(self.setCrs)
        mapCanvas.sigCrosshairPositionChanged.connect(self.onCrosshairPositionChanged)
        mapCanvas.mapTools().mtCursorLocation.sigLocationRequest[SpatialPoint, QgsMapCanvas].connect(self.sigCurrentLocationChanged)

    def _disconnectCanvasSignals(self, mapCanvas:MapCanvas):
        mapCanvas.sigSpatialExtentChanged.disconnect(self.setSpatialExtent)
        mapCanvas.sigDestinationCrsChanged.disconnect(self.setCrs)
        mapCanvas.sigCrosshairPositionChanged.disconnect(self.onCrosshairPositionChanged)
        mapCanvas.mapTools().mtCursorLocation.sigLocationRequest[SpatialPoint, QgsMapCanvas].disconnect(
            self.sigCurrentLocationChanged)

    def onCrosshairPositionChanged(self, spatialPoint:SpatialPoint):
        if self.mCrosshairPosition != spatialPoint:
            self.setCrosshairPosition(spatialPoint)
            self.sigCrosshairPositionChanged[SpatialPoint, MapCanvas].emit(self.mCrosshairPosition, canvas)
    def setCrosshairPosition(self, spatialPoint)->SpatialPoint:
        spatialPoint = spatialPoint.toCrs(self.crs())
        if self.mCrosshairPosition != spatialPoint:
            self.mCrosshairPosition = spatialPoint

            for canvas in self.mapCanvases():
                assert isinstance(canvas, MapCanvas)
                canvas.setCrosshairPosition(spatialPoint)
            self.sigCrosshairPositionChanged[SpatialPoint].emit(self.mCrosshairPosition)
        return self.crosshairPosition()

    def crosshairPosition(self)->SpatialPoint:
        return self.mCrosshairPosition

    def _updateGrid(self):
        import time
        t0 = time.time()
        # crop grid
        if self.mViewMode == MapWidget.ViewMode.MapViewByRows:

            nc = self.mMpMV
            nr = len(self.mapViews())
        else:
            raise NotImplementedError()

        toRemove = []
        for row in range(nr, self.mGrid.rowCount()):
            for col in range(self.mGrid.columnCount()):
                item = self.mGrid.itemAtPosition(row, col)
                if isinstance(item, QLayoutItem) and isinstance(item.widget(), QWidget):
                    toRemove.append(item.widget())

        for col in range(nc, self.mGrid.columnCount()):
            for row in range(self.mGrid.rowCount()):
                item = self.mGrid.itemAtPosition(row, col)
                if isinstance(item, QLayoutItem) and isinstance(item.widget(), QWidget):
                    toRemove.append(item.widget())

        for w in toRemove:
            self.mGrid.removeWidget(w)
            w.setParent(None)
            w.setVisible(False)
        self.mCanvases.clear()
Benjamin Jakimow's avatar
Benjamin Jakimow committed

        if self.mViewMode == MapWidget.ViewMode.MapViewByRows:
            for row, mv in enumerate(self.mMapViews):
                assert isinstance(mv, MapView)
                self.mCanvases[mv] = []
                for col in range(self.mMpMV):
                    item = self.mGrid.itemAtPosition(row, col)
                    if isinstance(item, QLayoutItem) and isinstance(item.widget(), MapCanvas):
                        c = item.widget()
                    else:
                        self.mGrid.addWidget(c, row, col)
                    assert isinstance(c, MapCanvas)
                    #c.setFixedSize(self.mMapSize)
                    c.setTSD(None)
                    c.setMapView(mv)
                    usedCanvases.append(c)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
                    self.mCanvases[mv].append(c)
        else:
            raise NotImplementedError()
        t2 = time.time()
        self._updateWidgetSize()
        t3 = time.time()

        s = ""
        # remove old canvases
        for c in oldCanvases:
            if c not in usedCanvases:
                self._disconnectCanvasSignals(c)
        t4 = time.time()

            print('t1: {}'.format(t1 - t0))
            print('t2: {}'.format(t2 - t1))
            print('t3: {}'.format(t3 - t2))
            print('t4: {}'.format(t4 - t3))

    def _updateWidgetSize(self):

        self.mGrid.update()
        # self.resize(self.sizeHint())
        # self.setMaximumSize(self.sizeHint())
        self.setFixedSize(self.sizeHint())
            assert isinstance(w, QWidget)

            rect = QGuiApplication.primaryScreen().geometry()

            maxw, maxh = 0.66*rect.width(), 0.66*rect.height()
            hint = self.sizeHint()
            minw, minh = min(hint.width(), maxw), min(hint.height(), maxh)

            w.setMinimumSize(minw, minh)
            #w.setFixedSize(self.sizeHint())
            w.layout().update()
            w.update()

    def _updateLayerCache(self)->list:
        canvases = self.findChildren(MapCanvas)
        for c in canvases:
            assert isinstance(c, MapCanvas)
            self.mMapLayerCache[self._layerListKey(c)] = c.layers()
        return canvases
    def _layerListKey(self, canvas:MapCanvas):
        return (canvas.mapView(), canvas.tsd())
    def _updateCanvasDates(self, updateLayerCache=True):

        visibleBefore = self.visibleTSDs()
        bTSDChanged = False
        if not (isinstance(self.mCurrentDate, TimeSeriesDate) and isinstance(self.timeSeries(), TimeSeries)):
            for c in self.findChildren(MapCanvas):
                assert isinstance(c, MapCanvas)
                c.setTSD(None)
            bTSDChanged = True
        else:

            visible = [tsd for tsd in self.timeSeries() if tsd.isVisible()]

            t = self.mCurrentDate.date()
            visible = sorted(visible, key=lambda tsd: abs(tsd.date() - t))
            visible = visible[0:min(len(visible), self.mMpMV)]
            visible = sorted(visible)

            # set TSD of remaining canvases to None
            while len(visible) < self.mMpMV:
                visible.append(None)

            for mapView in self.mapViews():
                for tsd, canvas in zip(visible, self.mCanvases[mapView]):
                    assert isinstance(tsd, TimeSeriesDate) or tsd is None
                    assert isinstance(canvas, MapCanvas)
                    if canvas.tsd() != tsd:
                        canvas.setTSD(tsd)

                        key = self._layerListKey(canvas)
                        if key in self.mMapLayerCache.keys():
                            canvas.setLayers(self.mMapLayerCache.pop(key))
                        bTSDChanged = True

                    # canvas.setLayers()
        if bTSDChanged:
            self._updateCanvasAppearance()

        visible2 = self.visibleTSDs()
        if visible2 != visibleBefore:
            self.sigVisibleDatesChanged.emit(visible2)

        self._freeUnusedMapLayers()

    def _freeUnusedMapLayers(self):

        layers = [l for l in self.mMapLayerStore.mapLayers().values() if isinstance(l, SensorProxyLayer)]
        needed = self.currentLayers()
        toRemove = [l for l in layers if isinstance(l, SensorProxyLayer) and l not in needed]

        # todo: use a kind of caching

        # remove layers from MapLayerCache and MapLayerStore
        for mv in self.mMapLayerCache.keys():
            layers = [l for l in self.mMapLayerCache[mv] if l not in toRemove]
            self.mMapLayerCache[mv] = layers
        self.mMapLayerStore.removeMapLayers(toRemove)




    def _updateCrosshair(self, mapView=None):

        if isinstance(mapView, MapView):
            mapViews = [mapView]
        else:
            mapViews = self.mapViews()

        for mapView in mapViews:
            assert isinstance(mapView, MapView)
            style = mapView.crosshairStyle()
            assert isinstance(style, CrosshairStyle)

            for canvas in self.mCanvases[mapView]:
                assert isinstance(canvas, MapCanvas)

                item = canvas.mCrosshairItem
                item.setVisibility(style.mShow)
                assert isinstance(item, CrosshairMapCanvasItem)
                item.setCrosshairStyle(style)
                canvas.addToRefreshPipeLine(MapCanvas.Command.UpdateMapItems)

    def _updateCanvasAppearance(self, mapView=None):

        if isinstance(mapView, MapView):
            mapViews = [mapView]
        else:
            mapViews = self.mapViews()

        for mapView in mapViews:
            assert isinstance(mapView, MapView)
            v = mapView.isVisible()
            bg = mapView.mapBackgroundColor()

            showDate = mapView.optionShowDate.isChecked()
            showName = mapView.optionShowMapViewName.isChecked()
            showSensor = mapView.optionShowSensorName.isChecked()

            for canvas in self.mCanvases[mapView]:
                assert isinstance(canvas, MapCanvas)

                # set overall visibility
                if canvas.isVisible() != v:
                    canvas.setVisible(v)

                tsd = canvas.tsd()

                if canvas.canvasColor() != bg:
                    canvas.addToRefreshPipeLine(mapView.mapBackgroundColor())

                # set info text
                info = canvas.infoItem()
                assert isinstance(info, MapCanvasInfoItem)

                uc = []
                lc = []
                if isinstance(tsd, TimeSeriesDate):
                    if showDate:
                        uc += ['{}'.format(tsd.date())]
                    if showName:
                        lc += ['{}'.format(mapView.title(maskNewLines=False))]
                    if showSensor:
                        uc += ['{}'.format(tsd.sensor().name())]

                uc = '\n'.join(uc)
                lc = '\n'.join(lc)


                canvas.addToRefreshPipeLine(MapCanvas.Command.UpdateMapItems)




class MapViewDock(QgsDockWidget, loadUI('mapviewdock.ui')):

    sigMapViewAdded = pyqtSignal(MapView)
    sigMapViewRemoved = pyqtSignal(MapView)
    sigShowProfiles = pyqtSignal(SpatialPoint, MapCanvas, str)

    sigMapCanvasColorChanged = pyqtSignal(QColor)
    sigMapCanvasTextFormatChanged = pyqtSignal(QgsTextFormat)
    sigSpatialExtentChanged = pyqtSignal(SpatialExtent)
    sigCrsChanged = pyqtSignal(QgsCoordinateReferenceSystem)
    sigMapSizeChanged = pyqtSignal(QSize)
    sigMapsPerMapViewChanged = pyqtSignal(int)
    sigMapTextFormatChanged = pyqtSignal(QgsTextFormat)

    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.sigMapCanvasColorChanged)
        self.btnTextFormat.changed.connect(lambda *args: self.sigMapTextFormatChanged.emit(self.mapTextFormat()))
        self.btnApplySizeChanges.clicked.connect(self.onApplyButtonClicked)

        self.actionAddMapView.triggered.connect(self.createMapView)
        self.actionRemoveMapView.triggered.connect(lambda: self.removeMapView(self.currentMapView()) if self.currentMapView() else None)

        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
        self.mMapWidget = None

    def onApplyButtonClicked(self):
        self.sigMapSizeChanged.emit(QSize(self.spinBoxMapSizeX.value(), self.spinBoxMapSizeY.value()))
        self.sigMapsPerMapViewChanged.emit(self.mapsPerMapView())



    def setMapWidget(self, mw)->MapWidget:
        """
        Connects this MapViewDock with a MapWidget
        :param mw: MapWidget
        :return:
        """
        assert isinstance(mw, MapWidget)

        assert mw.timeSeries() == self.mTimeSeries, 'Set the time series first!'
        self.mMapWidget = mw

        self.sigCrsChanged.connect(mw.setCrs)
        mw.sigCRSChanged.connect(self.setCrs)

        self.sigMapSizeChanged.connect(mw.setMapSize)
        mw.sigMapSizeChanged.connect(self.setMapSize)

        self.sigMapTextFormatChanged.connect(mw.setMapTextFormat)
        mw.sigMapTextFormatChanged.connect(self.setMapTextFormat)

        self.sigMapsPerMapViewChanged.connect(mw.setMapsPerMapView)
        mw.sigMapsPerMapViewChanged.connect(self.setMapsPerMapView)

        self.sigMapViewAdded.connect(mw.addMapView)
        self.sigMapViewRemoved.connect(mw.removeMapView)
        mw.sigMapViewAdded.connect(self.addMapView)
        mw.sigMapViewRemoved.connect(self.removeMapView)

        for mapView in mw.mapViews():
            self.addMapView(mapView)

        return self.mMapWidget

    def mapWidget(self):
        """
        Returns the connected MapWidget
        :return: MapWidget
        """
        return self.mMapWidget

    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 mapsPerMapView(self)->int:
        return self.sbMpMV.value()

    def setMapsPerMapView(self, n:int):
        assert n >= 0
        if self.sbMpMV.value != n:
            self.sbMpMV.setValue(n)
            self.mLastNDatesPerMapView = n

    def setMapTextFormat(self, textFormat:QgsTextFormat):

        if not equalTextFormats(textFormat, self.mapTextFormat()):
            self.btnTextFormat.setTextFormat(textFormat)

    def mapTextFormat(self)->QgsTextFormat:
        return self.btnTextFormat.textFormat()

    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)->QSize:
        return QSize(self.spinBoxMapSizeX.value(),
                     self.spinBoxMapSizeY.value())


    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 setMapTextColor(self, color:QColor):
        """
        Sets the map text color
        :param color: QColor
        :return: QColor
        """
        if color != self.mapTextColor():
            self.btnMapTextColor.setColor(color)
        return self.mapTextColor()

    def mapTextColor(self)->QColor:
        """
        Returns the map text color.
        :return: QColor
        """
        return self.btnMapTextColor.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, name:str=None)->MapView:
        """
        Create a new MapView
        :return: MapView
        """

        mapView = MapView()

        n = len(self.mapViews()) + 1
        if isinstance(name, str) and len(name) > 0:
            title = name
        else:
            title = 'Map View {}'.format(n)
            while title in [m.title() for m in self.mapViews()]:
                n += 1
                title = 'Map View {}'.format(n)

        if n == 1:
            mapView.optionShowDate.setChecked(True)
            mapView.optionShowSensorName.setChecked(True)

        mapView.setTitle(title)
        mapView.sigShowProfiles.connect(self.sigShowProfiles)
        mapView.setTimeSeries(self.mTimeSeries)
        self.addMapView(mapView)
        return mapView
    def onInfoOptionToggled(self):
        self.sigMapInfoChanged.emit()
        s = ""
    def addMapView(self, mapView:MapView):
        """
        Adds a MapView
        :param mapView: MapView
        """
        assert isinstance(mapView, MapView)
        if mapView not in self:
            mapView.sigTitleChanged.connect(lambda *args, mv=mapView: self.onMapViewUpdated(mv))
            #mapView.sigVisibilityChanged.connect(lambda *args, mv=mapView: self.onMapViewUpdated(mv))
            mapView.sigCanvasAppearanceChanged.connect(lambda *args, mv=mapView: self.onMapViewUpdated(mv))
            self.sigMapCanvasColorChanged.connect(mapView.setMapBackgroundColor)
            self.sigMapCanvasTextFormatChanged.connect(mapView.setMapTextFormat)
            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
        """
        if 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)