Skip to content
Snippets Groups Projects
timeseries.py 45.2 KiB
Newer Older
  • Learn to ignore specific revisions
  • # -*- coding: utf-8 -*-
    """
    /***************************************************************************
    
                                  -------------------
            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.                                   *
     *                                                                         *
     ***************************************************************************/
    """
    # noinspection PyPep8Naming
    
    import sys, re, collections, traceback, time, json, urllib, types, enum
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    from qgis import *
    from qgis.core import *
    from qgis.gui import *
    
    from qgis.PyQt.QtGui import *
    from qgis.PyQt.QtWidgets import *
    from qgis.PyQt.QtCore import *
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    from timeseriesviewer.dateparser import DOYfromDatetime64
    from timeseriesviewer.utils import SpatialExtent, loadUI, px2geo
    
    gdal.SetConfigOption('VRT_SHARED_SOURCE', '0') #!important. really. do not change this.
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    import numpy as np
    
    
    from timeseriesviewer import messageLog
    
    from timeseriesviewer.dateparser import parseDateFromDataSet
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    def transformGeometry(geom, crsSrc, crsDst, trans=None):
        if trans is None:
            assert isinstance(crsSrc, QgsCoordinateReferenceSystem)
            assert isinstance(crsDst, QgsCoordinateReferenceSystem)
            return transformGeometry(geom, None, None, trans=QgsCoordinateTransform(crsSrc, crsDst))
        else:
            assert isinstance(trans, QgsCoordinateTransform)
            return trans.transform(geom)
    
    
    GDAL_DATATYPES = {}
    for var in vars(gdal):
        match = re.search(r'^GDT_(?P<type>.*)$', var)
        if match:
            number = getattr(gdal, var)
            GDAL_DATATYPES[match.group('type')] = number
            GDAL_DATATYPES[match.group()] = number
    
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    METRIC_EXPONENTS = {
    
        "nm": -9, "um": -6, "mm": -3, "cm": -2, "dm": -1, "m": 0, "hm": 2, "km": 3
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    }
    #add synonyms
    METRIC_EXPONENTS['nanometers'] = METRIC_EXPONENTS['nm']
    METRIC_EXPONENTS['micrometers'] = METRIC_EXPONENTS['um']
    METRIC_EXPONENTS['millimeters'] = METRIC_EXPONENTS['mm']
    METRIC_EXPONENTS['centimeters'] = METRIC_EXPONENTS['cm']
    METRIC_EXPONENTS['decimeters'] = METRIC_EXPONENTS['dm']
    METRIC_EXPONENTS['meters'] = METRIC_EXPONENTS['m']
    METRIC_EXPONENTS['hectometers'] = METRIC_EXPONENTS['hm']
    METRIC_EXPONENTS['kilometers'] = METRIC_EXPONENTS['km']
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    def convertMetricUnit(value, u1, u2):
        assert u1 in METRIC_EXPONENTS.keys()
        assert u2 in METRIC_EXPONENTS.keys()
    
        e1 = METRIC_EXPONENTS[u1]
        e2 = METRIC_EXPONENTS[u2]
    
        return value * 10**(e1-e2)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    def getDS(pathOrDataset)->gdal.Dataset:
        """
        Returns a gdal.Dataset
        :param pathOrDataset: str | gdal.Dataset | QgsRasterLayer
        :return:
        """
        if isinstance(pathOrDataset, QgsRasterLayer):
            return getDS(pathOrDataset.source())
        elif isinstance(pathOrDataset, gdal.Dataset):
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        elif isinstance(pathOrDataset, str):
    
            ds = gdal.Open(pathOrDataset)
            assert isinstance(ds, gdal.Dataset)
            return ds
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    def sensorID(nb:int, px_size_x:float, px_size_y:float, dt:int, wl:list, wlu:str)->str:
        """
        Create a sensor ID
        :param nb: number of bands
        :param px_size_x: pixel size x
        :param px_size_y: pixel size y
        :param wl: list of wavelength
        :param wlu: str, wavelength unit
        :return: str
        """
    
        assert dt in GDAL_DATATYPES.values()
        assert isinstance(nb, int) and nb > 0
        assert isinstance(px_size_x, (int, float)) and px_size_x > 0
        assert isinstance(px_size_y, (int, float)) and px_size_y > 0
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
        if wl != None:
            assert isinstance(wl, list)
            assert len(wl) == nb
    
        if wlu != None:
            assert isinstance(wlu, str)
    
        return json.dumps((nb, px_size_x, px_size_y, dt, wl, wlu))
    
    def sensorIDtoProperties(idString:str)->tuple:
        """
        Reads a sensor id string and returns the sensor properties. See sensorID().
        :param idString: str
        :return: (ns, px_size_x, px_size_y, [wl], wlu)
        """
        nb, px_size_x, px_size_y, dt, wl, wlu = json.loads(idString)
        assert isinstance(dt, int) and dt >= 0
        assert isinstance(nb, int)
        assert isinstance(px_size_x, (int,float)) and px_size_x > 0
        assert isinstance(px_size_y, (int, float)) and px_size_y > 0
        if wl != None:
            assert isinstance(wl, list)
        if wlu != None:
            assert isinstance(wlu, str)
    
        return nb, px_size_x, px_size_y, dt, wl, wlu
    
    
    
    class SensorInstrument(QObject):
    
        """
        Describes a Sensor Configuration
        """
    
        SensorNameSettingsPrefix = 'SensorName.'
    
        sigNameChanged = pyqtSignal(str)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
        LUT_Wavelengths = dict({'B':480,
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                                'G':570,
                                'R':660,
                                'nIR':850,
                                'swIR':1650,
                                'swIR1':1650,
                                'swIR2':2150
                                })
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def __init__(self, sid:str, sensor_name:str=None, band_names:list = None):
    
            super(SensorInstrument, self).__init__()
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            self.mId = sid
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            self.nb, self.px_size_x, self.px_size_y, self.dataType, self.wl, self.wlu = sensorIDtoProperties(self.mId)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            if not isinstance(band_names, list):
                band_names = ['Band {}'.format(b+1) for b in range(self.nb)]
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            assert len(band_names) == self.nb
            self.bandNames = band_names
            self.wlu = self.wlu
            if self.wl is None:
                self.wl = None
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                self.wl = np.asarray(self.wl)
    
                sensor_name = '{}bands@{}m'.format(self.nb, self.px_size_x)
    
                import timeseriesviewer.settings
                sensor_name = timeseriesviewer.settings.value(self._sensorSettingsKey(), sensor_name)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            self.mName = ''
    
            self.setName(sensor_name)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            self.hashvalue = hash(self.mId)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            from timeseriesviewer.tests import TestObjects
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            import uuid
            path = '/vsimem/mockupImage.{}.bsq'.format(uuid.uuid4())
            self.mMockupDS = TestObjects.inMemoryImage(path=path, nb=self.nb, eType=self.dataType, ns=2, nl=2)
            self.mMockupLayer = QgsRasterLayer(self.mMockupDS.GetFileList()[0])
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def mockupLayer(self)->QgsRasterLayer:
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            #create an in-memory data set
            return self.mMockupLayer
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def id(self)->str:
            return self.mId
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def _sensorSettingsKey(self):
            return SensorInstrument.SensorNameSettingsPrefix+self.mId
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def setName(self, name:str):
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            """
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            Sets the sensor/product name
            :param name: str
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            """
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            if name != self.mName:
                self.mName = name
    
                import timeseriesviewer.settings
                timeseriesviewer.settings.setValue(self._sensorSettingsKey(), name)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                self.sigNameChanged.emit(self.name())
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def name(self)->str:
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            Returns the sensor name
            :return: str
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            return self.mName
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def __eq__(self, other):
    
            if not isinstance(other, SensorInstrument):
                return False
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            return self.mId == other.mId
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
        def __hash__(self):
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
        def __repr__(self):
    
            return str(self.__class__) +' ' + self.name()
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def description(self)->str:
    
            """
            Returns a human-readable description
            :return: str
            """
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            info = []
    
            info.append(self.name())
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            info.append('{} Bands'.format(self.nb))
            info.append('Band\tName\tWavelength')
            for b in range(self.nb):
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                if self.wl is not None:
                    wl = str(self.wl[b])
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                else:
                    wl = 'unknown'
                info.append('{}\t{}\t{}'.format(b + 1, self.bandNames[b], wl))
    
            return '\n'.join(info)
    
    
    
    def verifyInputImage(datasource):
        """
        Checks if an image source can be uses as TimeSeriesDatum, i.e. if it can be read by gdal.Open() and
        if we can extract an observation date as numpy.datetime64.
        :param datasource: str with data source uri or gdal.Dataset
        :return: bool
        """
    
        if datasource is None:
    
        if isinstance(datasource, str):
            datasource = gdal.Open(datasource)
        if not isinstance(datasource, gdal.Dataset):
    
        if datasource.RasterCount == 0 and len(datasource.GetSubDatasets()) > 0:
    
            #logger.error('Can not open container {}.\nPlease specify a subdataset'.format(path))
    
        if datasource.GetDriver().ShortName == 'VRT':
            files = datasource.GetFileList()
    
            if len(files) > 0:
                for f in files:
                    subDS = gdal.Open(f)
                    if not isinstance(subDS, gdal.Dataset):
                        return False
    
    
        from timeseriesviewer.dateparser import parseDateFromDataSet
    
        date = parseDateFromDataSet(datasource)
    
        if date is None:
            return False
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
    class TimeSeriesSource(object):
        """Provides some information on source images"""
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def create(source):
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            Reads the argument and returns a TimeSeriesSource
            :param source: gdal.Dataset, str, QgsRasterLayer
            :return: TimeSeriesSource
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            ds = None
            if isinstance(source, QgsRasterLayer):
                lyr = source
                provider = lyr.providerType()
    
                if provider == 'gdal':
                    ds = gdal.Open(lyr.source())
                elif provider == 'wcs':
                    parts = urllib.parse.parse_qs(lyr.source())
                    url = re.search(r'^[^?]+', parts['url'][0]).group()
                    identifier = re.search(r'^[^?]+', parts['identifier'][0]).group()
    
                    uri2 = 'WCS:{}?coverage={}'.format(url, identifier)
                    ds = gdal.Open(uri2)
    
                    if not isinstance(ds, gdal.Dataset) or ds.RasterCount == 0:
                        dsGetCoverage = gdal.Open('WCS:{}'.format(url))
                        for subdatasetUrl, id in dsGetCoverage.GetSubDatasets():
                            if id == identifier:
                                ds = gdal.Open(subdatasetUrl)
                                break
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                else:
                    raise Exception('Unsupported raster data provider: {}'.format(provider))
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            elif isinstance(source, str):
                ds = gdal.Open(source)
    
            elif isinstance(source, gdal.Dataset):
                ds = source
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            else:
                raise Exception('Unsupported source: {}'.format(source))
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            return TimeSeriesSource(ds)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def __init__(self, dataset:gdal.Dataset):
    
            assert isinstance(dataset, gdal.Dataset)
            assert dataset.RasterCount > 0
            assert dataset.RasterYSize > 0
            assert dataset.RasterXSize > 0
            self.mUri = dataset.GetFileList()[0]
    
            self.mDate = parseDateFromDataSet(dataset)
            assert self.mDate is not None, 'Unable to find acquisition date of {}'.format(self.mUri)
    
            self.mDrv = dataset.GetDriver().ShortName
            self.mGT = dataset.GetGeoTransform()
            self.mWKT = dataset.GetProjection()
            self.mCRS = QgsCoordinateReferenceSystem(self.mWKT)
    
            self.mWL, self.mWLU = extractWavelengths(dataset)
    
    
            self.nb, self.nl, self.ns = dataset.RasterCount, dataset.RasterYSize, dataset.RasterXSize
            self.mGeoTransform = dataset.GetGeoTransform()
            px_x = float(abs(self.mGeoTransform[1]))
            px_y = float(abs(self.mGeoTransform[5]))
            self.mGSD = (px_x, px_y)
            self.mDataType = dataset.GetRasterBand(1).DataType
            self.mSid = sensorID(self.nb, px_x, px_y, self.mDataType, self.mWL, self.mWLU)
    
            self.mMetaData = collections.OrderedDict()
            for domain in dataset.GetMetadataDomainList():
                self.mMetaData[domain] = dataset.GetMetadata_Dict(domain)
    
            self.mUL = QgsPointXY(*px2geo(QPoint(0, 0), self.mGeoTransform, pxCenter=False))
            self.mLR = QgsPointXY(*px2geo(QPoint(self.ns + 1, self.nl + 1), self.mGeoTransform, pxCenter=False))
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
        def name(self)->str:
            """
            Returns a name for this data source
            :return:
            """
            bn = os.path.basename(self.uri())
            return '{} {}'.format(bn, self.date())
    
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def uri(self)->str:
            """
            URI that can be used with GDAL to open a dataset
            :return: str
            """
            return self.mUri
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def qgsMimeDataUtilsUri(self)->QgsMimeDataUtils.Uri:
            uri = QgsMimeDataUtils.Uri()
            uri.name = self.name()
            uri.providerKey = 'gdal'
            uri.uri = self.uri()
            uri.layerType = 'raster'
            return uri
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def sid(self)->str:
            """
            Returns the sensor id
            :return: str
            """
            return self.mSid
    
        def date(self)->np.datetime64:
            return self.mDate
    
        def crs(self)->QgsCoordinateReferenceSystem:
            return self.mCRS
    
        def spatialExtent(self)->SpatialExtent:
            return SpatialExtent(self.mCRS, self.mUL, self.mLR)
    
        def __eq__(self, other):
            if not isinstance(other, TimeSeriesSource):
                return False
            return self.mUri == other.mUri
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    class TimeSeriesDatum(QObject):
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        sigVisibilityChanged = pyqtSignal(bool)
        sigRemoveMe = pyqtSignal()
        sigSourcesChanged = pyqtSignal()
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def __init__(self, timeSeries, date:np.datetime64, sensor:SensorInstrument):
            """
            Constructor
            :param timeSeries: TimeSeries, parent TimeSeries instance, optional
            :param date: np.datetime64,
            :param sensor: SensorInstrument
            """
            super(TimeSeriesDatum,self).__init__()
            assert isinstance(date, np.datetime64)
            assert isinstance(sensor, SensorInstrument)
            self.mSensor = sensor
            self.mDate = date
            self.mDOY = DOYfromDatetime64(self.mDate)
            self.mSources = []
            self.mMasks = []
    
            self.mVisibility = True
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            self.mTimeSeries = timeSeries
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
        def addSource(self, source):
            """
            Adds an time series source to this TimeSeriesDatum
            :param path: TimeSeriesSource or any argument accepted by TimeSeriesSource.create()
            :return: TimeSeriesSource, if added
            """
    
            if not isinstance(source, TimeSeriesSource):
                return self.addSource(TimeSeriesSource.create(source))
            else:
                assert isinstance(source, TimeSeriesSource)
                assert self.mDate == source.date()
                assert self.mSensor.id() == source.sid()
                if source not in self.mSources:
                    self.mSources.append(source)
                    self.sigSourcesChanged.emit()
                    return source
                else:
                    return None
    
    
    
        def setVisibility(self, b:bool):
    
            """
            Sets the visibility of the TimeSeriesDatum, i.e. whether linked MapCanvases will be shown to the user
            :param b: bool
            """
    
            old = self.mVisibility
            self.mVisibility = b
            if old != self.mVisibility:
                self.sigVisibilityChanged.emit(b)
    
        def isVisible(self):
    
            """
            Returns whether the TimeSeriesDatum is visible as MapCanvas
            :return: bool
            """
    
            return self.mVisibility
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def sensor(self)->SensorInstrument:
            """
            Returns the SensorInstrument
            :return: SensorInsturment
            """
            return self.mSensor
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def sources(self)->list:
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            Returns the source images
            :return: [list-of-TimeSeriesSource]
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            return self.mSources
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def sourceUris(self)->list:
            """
            Returns all source URIs  as list of strings-
            :return: [list-of-str]
            """
            return [tss.uri() for tss in self.sources()]
    
        def qgsMimeDataUtilsUris(self)->list:
            """
            Returns all source URIs as list of QgsMimeDataUtils.Uri
            :return: [list-of-QgsMimedataUtils.Uris]
            """
            return [s.qgsMimeDataUtilsUri() for s in self.sources()]
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def date(self)->np.datetime64:
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            Returns the observation date
            :return: numpy.datetime64
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            return np.datetime64(self.mDate)
    
        def decimalYear(self)->float:
            """
            Returns the observation date as decimal year (year + doy / (366+1) )
            :return: float
            """
    
            return self.year() + self.doy() / (366+1)
    
        def year(self)->int:
            """
            Returns the observation year
            :return: int
            """
            return self.mDate.astype(object).year
    
    
        def doy(self)->int:
            """
            Returns the day of Year (DOY)
            :return: int
            """
            return int(self.mDOY)
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def spatialExtent(self):
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            Returns the SpatialExtent of all data sources
    
            :return: SpatialExtent
            """
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            ext = None
            for i, tss in enumerate(self.sources()):
                assert isinstance(tss, TimeSeriesSource)
                if i == 0:
                    ext = tss.spatialExtent()
                else:
                    ext.combineExtentWith(tss.spatialExtent())
            return ext
    
        def imageBorders(self)->QgsGeometry:
            """
            Retunrs the exact border polygon
            :return: QgsGeometry
            """
    
            return None
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def __repr__(self)->str:
            """
            String representation
            :return:
            """
            return 'TimeSeriesDatum({},{})'.format(str(self.mDate), str(self.mSensor))
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def __eq__(self, other)->bool:
            """
            Tow TimeSeriesDatum instances are equal if they have the same date, sensor and sources.
            :param other: TimeSeriesDatum
            :return: bool
            """
            if not isinstance(other, TimeSeriesDatum):
                return False
            return self.id() == other.id() and self.mSources == other.mSources
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def __len__(self)->int:
            """
            Returns the number of source images.
            :return: int
            """
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            return len(self.mSources)
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def __lt__(self, other)->bool:
            """
            :param other: TimeSeriesDatum
            :return: bool
            """
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            assert isinstance(other, TimeSeriesDatum)
            if self.date() < other.date():
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                return True
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            elif self.date() > other.date():
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            else:
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                return self.sensor().id() < other.sensor().id()
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def id(self)->tuple:
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            """
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            :return: tuple
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            """
            return (self.mDate, self.mSensor.id())
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def mimeDataUris(self)->list:
            """
            Returns the sources of this TSD as list of QgsMimeDataUtils.Uris
            :return: [list-of-QgsMimeDataUtils]
            """
            results = []
            for tss in self.sources():
                assert isinstance(tss, TimeSeriesSource)
    
            [tss.uri() for tss in self.sources()]
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            return hash(self.id())
    
    class TimeSeriesTableView(QTableView):
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        sigMoveToDateRequest = pyqtSignal(TimeSeriesDatum)
    
    
        def __init__(self, parent=None):
            super(TimeSeriesTableView, self).__init__(parent)
    
        def contextMenuEvent(self, event):
    
            """
            Creates and shows an QMenu
            :param event:
            """
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
            idx = self.indexAt(event.pos())
            tsd = self.model().data(idx, role=Qt.UserRole)
    
    
    
            menu = QMenu(self)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            a.triggered.connect(lambda: self.onCopyValues())
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            a.triggered.connect(lambda: self.onSetCheckState(Qt.Checked))
    
            a = menu.addAction('Uncheck')
            a.triggered.connect(lambda: self.onSetCheckState(Qt.Unchecked))
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            if isinstance(tsd, TimeSeriesDatum):
                a = menu.addAction('Show {}'.format(tsd.date()))
                a.triggered.connect(lambda _, tsd=tsd: self.sigMoveToDateRequest.emit(tsd))
    
    
            menu.popup(QCursor.pos())
    
    
            """
            Sets a ChecState to all selected rows
            :param checkState: Qt.CheckState
            """
    
            indices = self.selectionModel().selectedIndexes()
            rows = sorted(list(set([i.row() for i in indices])))
            model = self.model()
            if isinstance(model, TimeSeriesTableModel):
                for r in rows:
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                    idx = model.index(r, 0)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def onCopyValues(self, delimiter='\t'):
    
            """
            Copies selected cell values to the clipboard
            """
    
            indices = self.selectionModel().selectedIndexes()
            model = self.model()
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            if isinstance(model, QSortFilterProxyModel):
    
                from collections import OrderedDict
                R = OrderedDict()
                for idx in indices:
                    if not idx.row() in R.keys():
                        R[idx.row()] = []
                    R[idx.row()].append(model.data(idx, Qt.DisplayRole))
                info = []
                for k, values in R.items():
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                    info.append(delimiter.join([str(v) for v in values]))
    
                info = '\n'.join(info)
                QApplication.clipboard().setText(info)
            s = ""
    
    
    class TimeSeriesDockUI(QgsDockWidget, loadUI('timeseriesdock.ui')):
    
        """
        QgsDockWidget that shows the TimeSeries
        """
    
        def __init__(self, parent=None):
            super(TimeSeriesDockUI, self).__init__(parent)
            self.setupUi(self)
            self.btnAddTSD.setDefaultAction(parent.actionAddTSD)
            self.btnRemoveTSD.setDefaultAction(parent.actionRemoveTSD)
            self.btnLoadTS.setDefaultAction(parent.actionLoadTS)
            self.btnSaveTS.setDefaultAction(parent.actionSaveTS)
            self.btnClearTS.setDefaultAction(parent.actionClearTS)
    
            self.progressBar.setMinimum(0)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            self.setProgressInfo(0, 100, 'Add images to fill time series')
    
            self.progressBar.setValue(0)
            self.progressInfo.setText(None)
            self.frameFilters.setVisible(False)
    
    
    
        def setStatus(self):
    
            """
            Updates the status of the TimeSeries
            """
    
            from timeseriesviewer.timeseries import TimeSeries
            if isinstance(self.TS, TimeSeries):
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                nDates = len(self.TS)
                nSensors = len(self.TS.sensors())
                msg = '{} scene(s) from {} sensor(s)'.format(nDates, nSensors)
                if nDates > 1:
                    msg += ', {} to {}'.format(str(self.TS[0].date()), str(self.TS[-1].date()))
    
                self.progressInfo.setText(msg)
    
    
        def setProgressInfo(self, nDone:int, nMax:int, message=None):
            """
            Sets the progress bar of the TimeSeriesDockUI
            :param nDone: number of added data sources
            :param nMax: total number of data source to be added
            :param message: error / other kind of info message
            """
    
            if self.progressBar.maximum() != nMax:
                self.progressBar.setMaximum(nMax)
            self.progressBar.setValue(nDone)
            self.progressInfo.setText(message)
            QgsApplication.processEvents()
            if nDone == nMax:
                QTimer.singleShot(3000, lambda: self.setStatus())
    
        def onSelectionChanged(self, *args):
    
            """
            Slot to react on user-driven changes of the selected TimeSeriesDatum rows.
            """
    
            self.btnRemoveTSD.setEnabled(self.SM is not None and len(self.SM.selectedRows()) > 0)
    
        def selectedTimeSeriesDates(self):
    
            """
            Returns the TimeSeriesDatum selected by a user.
            :return: [list-of-TimeSeriesDatum]
            """
    
            if self.SM is not None:
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                return [self.mTSModel.data(idx, Qt.UserRole) for idx in self.SM.selectedRows()]
    
            """
            Sets the TimeSeries to be shown in the TimeSeriesDockUI
            :param TS: TimeSeries
            """
    
            from timeseriesviewer.timeseries import TimeSeries
            self.TS = TS
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            self.mTSModel = None
    
            self.SM = None
            self.timeSeriesInitialized = False
    
            if isinstance(TS, TimeSeries):
                from timeseriesviewer.timeseries import TimeSeriesTableModel
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                self.mTSModel = TimeSeriesTableModel(self.TS)
                self.mTSProxyModel = QSortFilterProxyModel(self)
                self.mTSProxyModel.setSourceModel(self.mTSModel)
                self.tableView_TimeSeries.setModel(self.mTSProxyModel)
                self.SM = QItemSelectionModel(self.mTSProxyModel)
    
                self.tableView_TimeSeries.setSelectionModel(self.SM)
                self.SM.selectionChanged.connect(self.onSelectionChanged)
                self.tableView_TimeSeries.horizontalHeader().setResizeMode(QHeaderView.ResizeToContents)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                self.tableView_TimeSeries.verticalHeader().setResizeMode(QHeaderView.ResizeToContents)
    
                TS.sigLoadingProgress.connect(self.setProgressInfo)
    
            self.onSelectionChanged()
    
    
    
    class DateTimePrecision(enum.Enum):
        """
        Describes the precision to pares DateTimeStamps.
        """
    
        Year = 'Y'
        Month = 'M'
        Week = 'W'
        Day = 'D'
        Hour = 'h'
        Minute = 'm'
        Second = 's'
        Milisecond = 'ms'
        Original = 0
    
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    class TimeSeries(QObject):
    
        """
        The sorted list of data sources that specify the time series
        """
    
        sigTimeSeriesDatesAdded = pyqtSignal(list)
        sigTimeSeriesDatesRemoved = pyqtSignal(list)
    
        sigLoadingProgress = pyqtSignal(int, int, str)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        sigSensorAdded = pyqtSignal(SensorInstrument)
        sigSensorRemoved = pyqtSignal(SensorInstrument)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        sigSourcesChanged = pyqtSignal(TimeSeriesDatum)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def __init__(self, imageFiles=None, maskFiles=None):
            QObject.__init__(self)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            self.mTSDs = list()
            self.mSensors = []
            self.mShape = None
    
            self.mDateTimePrecision = DateTimePrecision.Original
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
            if imageFiles is not None:
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                self.addSources(imageFiles)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            if maskFiles is not None:
                self.addMasks(maskFiles)
    
        _sep = ';'
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def sensor(self, sid:str)->SensorInstrument:
            """
            Returns the sensor with sid = sid
            :param sid: str, sensor id
            :return: SensorInstrument
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            assert isinstance(sid, str)
            for sensor in self.mSensors:
                assert isinstance(sensor, SensorInstrument)
                if sensor.id() == sid:
                    return sensor
            return None
    
    
        def sensors(self)->list:
            """
            Returns the list of sensors derived from the TimeSeries data sources
    
            :return: [list-of-SensorInstruments]
            """
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            return self.mSensors[:]
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def loadFromFile(self, path, n_max=None):
    
            """
            Loads a CSV file with source images of a TimeSeries
            :param path: str, Path of CSV file
            :param n_max: optional, maximum number of files to load
            """
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
            images = []
            masks = []
            with open(path, 'r') as f:
                lines = f.readlines()
                for l in lines:
                    if re.match('^[ ]*[;#&]', l):
                        continue
    
                    parts = re.split('[\n'+TimeSeries._sep+']', l)
                    parts = [p for p in parts if p != '']
                    images.append(parts[0])
                    if len(parts) > 1:
                        masks.append(parts[1])
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            if n_max:
                n_max = min([len(images), n_max])
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                self.addSources(images[0:n_max])
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            else:
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                self.addSources(images)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            #self.addMasks(masks)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
        def saveToFile(self, path):
    
            """
            Saves the TimeSeries sources into a CSV file
            :param path: str, path of CSV file
            :return: path of CSV file
            """
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            if path is None or len(path) == 0:
    
    benjamin.jakimow@geo.hu-berlin.de's avatar
    benjamin.jakimow@geo.hu-berlin.de committed
                return None
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
            lines = []
            lines.append('#Time series definition file: {}'.format(np.datetime64('now').astype(str)))
    
            lines.append('#<image path>')
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            for TSD in self.mTSDs:
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                assert isinstance(TSD, TimeSeriesDatum)
                for pathImg in TSD.sourceUris():
                    lines.append(pathImg)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
            lines = [l+'\n' for l in lines]
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            with open(path, 'w') as f:
                f.writelines(lines)
    
                messageLog('Time series source images written to {}'.format(path))
    
    
    benjamin.jakimow@geo.hu-berlin.de's avatar
    benjamin.jakimow@geo.hu-berlin.de committed
            return path
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def pixelSizes(self):
    
            """
            Returns the pixel sizes of all SensorInstruments
            :return: [list-of-QgsRectangles]
            """
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            for sensor in self.mSensors2TSDs.keys():
    
                r.append((QgsRectangle(sensor.px_size_x, sensor.px_size_y)))
            return r
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def maxSpatialExtent(self, crs=None)->SpatialExtent:
    
            """
            Returns the maximum SpatialExtent of all images of the TimeSeries
            :param crs: QgsCoordinateSystem to express the SpatialExtent coordinates.
            :return:
            """
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            extent = None
            for i, tsd in enumerate(self.mTSDs):
                assert isinstance(tsd, TimeSeriesDatum)
                ext = tsd.spatialExtent()
                if isinstance(extent, SpatialExtent):
                    extent = extent.combineExtentWith(ext)
                else:
                    extent = ext
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
            return extent
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
            """
            Returns the TimeSeriesDatum related to an image source
            :param pathOfInterest: str, image source uri
            :return: TimeSeriesDatum
            """
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            for tsd in self.mTSDs:
                assert isinstance(tsd, TimeSeriesDatum)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                if pathOfInterest in tsd.sourceUris():
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def tsd(self, date: np.datetime64, sensor)->TimeSeriesDatum:
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            """
            Returns the TimeSeriesDatum identified by ate nd sensorID
            :param date:
            :param sensor: SensorInstrument | str with sensor id
            :return:
            """
            assert isinstance(date, np.datetime64)
            if isinstance(sensor, str):
                sensor = self.sensor(sensor)
            if isinstance(sensor, SensorInstrument):
                for tsd in self.mTSDs:
                    if tsd.date() == date and tsd.sensor() == sensor:
                        return tsd
            return None
    
    
        def insertTSD(self, tsd: TimeSeriesDatum)->TimeSeriesDatum:
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            """
            Inserts a TimeSeriesDatum
            :param tsd: TimeSeriesDatum
            """
            #insert sorted by time & sensor
            assert tsd not in self.mTSDs
            assert tsd.sensor() in self.mSensors
            bisect.insort(self.mTSDs, tsd)
            tsd.mTimeSeries = self
            tsd.sigRemoveMe.connect(lambda: self.removeTSDs([tsd]))
            tsd.sigSourcesChanged.connect(lambda: self.sigSourcesChanged.emit(tsd))
    
            return tsd
    
        def removeTSDs(self, tsds):
            """
            Removes a list of TimeSeriesDatum
            :param tsds: [list-of-TimeSeriesDatum]
            """
            removed = list()
            for tsd in tsds:
                assert isinstance(tsd, TimeSeriesDatum)
                assert tsd in self.mTSDs
                self.mTSDs.remove(tsd)
                tsd.mTimeSeries = None
                removed.append(tsd)
            self.sigTimeSeriesDatesRemoved.emit(removed)
    
    
    
    
        def tsds(self, date:np.datetime64=None, sensor:SensorInstrument=None)->list:
    
    
            """
            Returns a list of  TimeSeriesDatum of the TimeSeries. By default all TimeSeriesDatum will be returned.
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            :param date: numpy.datetime64 to return the TimeSeriesDatum for
            :param sensor: SensorInstrument of interest to return the [list-of-TimeSeriesDatum] for.