Skip to content
Snippets Groups Projects
timeseries.py 70.1 KiB
Newer Older
  • Learn to ignore specific revisions
  •             return pickle.dumps(results)
    
            s = TimeSeriesSource.create(source)
            if isinstance(s, TimeSeriesSource):
                results.append(s)
    
        return pickle.dumps(results)
    
    class TimeSeries(QAbstractItemModel):
    
        """
        The sorted list of data sources that specify the time series
        """
    
        sigTimeSeriesDatesAdded = pyqtSignal(list)
        sigTimeSeriesDatesRemoved = pyqtSignal(list)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        sigSensorAdded = pyqtSignal(SensorInstrument)
        sigSensorRemoved = pyqtSignal(SensorInstrument)
    
        sigSourcesAdded = pyqtSignal(list)
        sigSourcesRemoved = pyqtSignal(list)
    
        sigVisibilityChanged = pyqtSignal()
    
            super(TimeSeries, self).__init__()
    
    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
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            self.mCurrentSpatialExtent = None
    
    
            self.cnDate = 'Date'
            self.cnSensor = 'Sensor'
            self.cnNS = 'ns'
            self.cnNL = 'nl'
            self.cnNB = 'nb'
            self.cnCRS = 'CRS'
            self.cnImages = 'Source Image(s)'
            self.mColumnNames = [self.cnDate, self.cnSensor,
                                 self.cnNS, self.cnNL, self.cnNB,
                                 self.cnCRS, self.cnImages]
            self.mRootIndex = QModelIndex()
    
    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
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def setCurrentSpatialExtent(self, spatialExtent:SpatialExtent):
            """
            Sets the spatial extent currently shown
            :param spatialExtent:
            """
            if isinstance(spatialExtent, SpatialExtent) and self.mCurrentSpatialExtent != spatialExtent:
                self.mCurrentSpatialExtent = spatialExtent
    
        def focusVisibilityToExtent(self):
            ext = self.currentSpatialExtent()
            if isinstance(ext, SpatialExtent):
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                for tsd in self:
    
                    assert isinstance(tsd, TimeSeriesDate)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                    b = tsd.hasIntersectingSource(ext)
    
                    if b != tsd.isVisible():
                        changed = True
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                    tsd.setVisibility(b)
    
    
                if changed:
                    ul = self.index(0, 0)
                    lr = self.index(self.rowCount()-1, 0)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                    self.dataChanged.emit(ul, lr, [Qt.CheckStateRole])
    
                    self.sigVisibilityChanged.emit()
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
        def currentSpatialExtent(self)->SpatialExtent:
            """
            Returns the current spatial extent
            :return: SpatialExtent
            """
            return self.mCurrentSpatialExtent
    
        def setVisibleDates(self, tsds:list):
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            """
            Sets the TimeSeriesDates currently shown
    
            :param tsds: [list-of-TimeSeriesDate]
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            """
    
            self.mVisibleDate.clear()
            self.mVisibleDate.extend(tsds)
    
                assert isinstance(tsd, TimeSeriesDate)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                if tsd in self:
                    idx = self.tsdToIdx(tsd)
                    # force reset of background color
                    idx2 = self.index(idx.row(), self.columnCount()-1)
                    self.dataChanged.emit(idx, idx2, [Qt.BackgroundColorRole])
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def sensor(self, sensorID:str)->SensorInstrument:
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            """
            Returns the sensor with sid = sid
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            :param sensorID: str, sensor id
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            :return: SensorInstrument
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            assert isinstance(sensorID, str)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            for sensor in self.mSensors:
                assert isinstance(sensor, SensorInstrument)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                if sensor.id() == sensorID:
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                    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
    
    
        def loadFromFile(self, path, n_max=None, progressDialog:QProgressDialog=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])
    
                images = images[0:n_max]
    
            if isinstance(progressDialog, QProgressDialog):
                progressDialog.setMaximum(len(images))
                progressDialog.setMinimum(0)
                progressDialog.setValue(0)
                progressDialog.setLabelText('Start loading {} images....'.format(len(images)))
    
            self.addSourcesAsync(images, progressDialog=progressDialog)
    
    
    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:
    
                assert isinstance(TSD, TimeSeriesDate)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                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, TimeSeriesDate)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                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 TimeSeriesDate related to an image source
    
            :param pathOfInterest: str, image source uri
    
            :return: TimeSeriesDate
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            for tsd in self.mTSDs:
    
                assert isinstance(tsd, TimeSeriesDate)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                if pathOfInterest in tsd.sourceUris():
    
        def tsd(self, date: np.datetime64, sensor)->TimeSeriesDate:
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            """
    
            Returns the TimeSeriesDate identified by date and sensorID
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            :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
    
            else:
                for tsd in self.mTSDs:
                    if tsd.date() == date:
                        return tsd
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            return None
    
    
        def insertTSD(self, tsd: TimeSeriesDate)->TimeSeriesDate:
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            """
    
            Inserts a TimeSeriesDate
            :param tsd: TimeSeriesDate
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            """
            #insert sorted by time & sensor
            assert tsd not in self.mTSDs
            assert tsd.sensor() in self.mSensors
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            tsd.mTimeSeries = self
    
            tsd.sigRemoveMe.connect(lambda tsd=tsd: self.removeTSDs([tsd]))
    
            tsd.rowsAboutToBeRemoved.connect(lambda p, first, last, tsd=tsd: self.beginRemoveRows(self.tsdToIdx(tsd), first, last))
            tsd.rowsRemoved.connect(self.endRemoveRows)
            tsd.rowsAboutToBeInserted.connect(lambda p, first, last, tsd=tsd: self.beginInsertRows(self.tsdToIdx(tsd), first, last))
            tsd.rowsInserted.connect(self.endInsertRows)
    
            tsd.sigSourcesAdded.connect(self.sigSourcesAdded)
            tsd.sigSourcesRemoved.connect(self.sigSourcesRemoved)
    
    
            row = bisect.bisect(self.mTSDs, tsd)
            self.beginInsertRows(self.mRootIndex, row, row)
            self.mTSDs.insert(row, tsd)
            self.endInsertRows()
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            return tsd
    
    
        def showTSDs(self, tsds:list, b:bool=True):
    
            tsds = [t for t in tsds if t in self]
    
            minRow = None
            maxRow = None
            for t in tsds:
    
                idx = self.tsdToIdx(t)
                if minRow is None:
                    minRow = idx.row()
                    maxRow = idx.row()
                else:
                    minRow = min(minRow, idx.row())
                    maxRow = min(maxRow, idx.row())
    
                assert isinstance(t, TimeSeriesDate)
                t.setVisibility(b)
            if minRow:
                ul = self.index(minRow, 0)
                lr = self.index(maxRow, 0)
    
                self.dataChanged.emit(ul, lr, [Qt.CheckStateRole])
    
                self.sigVisibilityChanged.emit()
    
        def hideTSDs(self, tsds):
            self.showTSDs(tsds, False)
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def removeTSDs(self, tsds):
            """
    
            Removes a list of TimeSeriesDate
            :param tsds: [list-of-TimeSeriesDate]
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            """
            removed = list()
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            toRemove = set()
            for t in tsds:
                if isinstance(t, TimeSeriesDate):
                    toRemove.add(t)
                if isinstance(t, TimeSeriesSource):
                    toRemove.add(t.timeSeriesDate())
    
            for tsd in list(toRemove):
    
                assert isinstance(tsd, TimeSeriesDate)
    
    
                tsd.sigSourcesRemoved.disconnect()
                tsd.sigSourcesAdded.disconnect()
                tsd.sigRemoveMe.disconnect()
    
    
                row = self.mTSDs.index(tsd)
                self.beginRemoveRows(self.mRootIndex, row, row)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                self.mTSDs.remove(tsd)
                tsd.mTimeSeries = None
                removed.append(tsd)
    
            if len(removed) > 0:
                self.sigTimeSeriesDatesRemoved.emit(removed)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
        def tsds(self, date:np.datetime64=None, sensor:SensorInstrument=None)->list:
    
    
            Returns a list of  TimeSeriesDate of the TimeSeries. By default all TimeSeriesDate will be returned.
            :param date: numpy.datetime64 to return the TimeSeriesDate for
            :param sensor: SensorInstrument of interest to return the [list-of-TimeSeriesDate] for.
            :return: [list-of-TimeSeriesDate]
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            tsds = self.mTSDs[:]
            if date:
                tsds = [tsd for tsd in tsds if tsd.date() == date]
            if sensor:
    
                tsds = [tsd for tsd in tsds if tsd.sensor() == sensor]
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            return tsds
    
        def clear(self):
    
            """
            Removes all data sources from the TimeSeries (which will be empty after calling this routine).
            """
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            self.removeTSDs(self[:])
    
        def addSensor(self, sensor:SensorInstrument):
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            Adds a Sensor
            :param sensor: SensorInstrument
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            if not sensor in self.mSensors:
                self.mSensors.append(sensor)
                self.sigSensorAdded.emit(sensor)
                return sensor
            else:
                return None
    
        def checkSensorList(self):
            """
            Removes sensors without linked TSD / no data
            """
            to_remove = []
            for sensor in self.sensors():
                tsds = [tsd for tsd in self.mTSDs if tsd.sensor() == sensor]
                if len(tsds) == 0:
                    to_remove.append(sensor)
            for sensor in to_remove:
                self.removeSensor(sensor)
    
        def removeSensor(self, sensor:SensorInstrument)->SensorInstrument:
            """
            Removes a sensor and all linked images
            :param sensor: SensorInstrument
            :return: SensorInstrument or none, if sensor was not defined in the TimeSeries
            """
            assert isinstance(sensor, SensorInstrument)
            if sensor in self.mSensors:
                tsds = [tsd for tsd in self.mTSDs if tsd.sensor() == sensor]
                self.removeTSDs(tsds)
                self.mSensors.remove(sensor)
                self.sigSensorRemoved.emit(sensor)
                return sensor
            return None
    
        def addSourcesAsync(self, sources:list, nWorkers:int = 1, progressDialog:QProgressDialog=None):
    
    
            tm = QgsApplication.taskManager()
            assert isinstance(tm, QgsTaskManager)
            assert isinstance(nWorkers, int) and nWorkers >= 1
    
            self.mLoadingProgressDialog = progressDialog
    
    
            if True:
                n = len(sources)
                taskDescription = 'Load {} images'.format(n)
                dump = pickle.dumps(sources)
                qgsTask = QgsTask.fromFunction(taskDescription, doLoadTimeSeriesSourcesTask, dump,
                                    on_finished = self.onAddSourcesAsyncFinished)
    
                tid = id(qgsTask)
                qgsTask.taskCompleted.connect(lambda *args, tid=tid: self.onRemoveTask(tid))
                qgsTask.taskTerminated.connect(lambda *args, tid=tid: self.onRemoveTask(tid))
    
                if False:  # for debugging only
    
                    resultDump = doLoadTimeSeriesSourcesTask(qgsTask, dump)
                    self.onAddSourcesAsyncFinished(None, resultDump)
                else:
                    tm.addTask(qgsTask)
    
    
            else:
                # see https://stackoverflow.com/questions/312443/how-do-you-split-a-list-into-evenly-sized-chunks
    
                def chunks(l, n):
                    """Yield successive n-sized chunks from l."""
                    for i in range(0, len(l), n):
                        yield l[i:i + n]
    
                n = int(len(sources) / nWorkers)
                for subset in chunks(sources, 50):
    
                    dump = pickle.dumps(subset)
    
                    taskDescription = 'Load {} images'.format(len(subset))
                    qgsTask = QgsTask.fromFunction(taskDescription, doLoadTimeSeriesSourcesTask, dump, on_finished=self.onAddSourcesAsyncFinished)
                    tid = id(qgsTask)
                    self.mTasks[tid] = qgsTask
                    qgsTask.taskCompleted.connect(lambda *args, tid=tid: self.onRemoveTask(tid))
                    qgsTask.taskTerminated.connect(lambda *args, tid=tid: self.onRemoveTask(tid))
    
                    if False: # for debugging only
                        resultDump = doLoadTimeSeriesSourcesTask(qgsTask, dump)
                        self.onAddSourcesAsyncFinished(None, resultDump)
                    else:
                        tm.addTask(qgsTask)
            s  = ""
    
    
        def onRemoveTask(self, key):
            self.mTasks.pop(key)
    
    
        def onAddSourcesAsyncFinished(self, *args):
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            # print(':: onAddSourcesAsyncFinished')
    
            error = args[0]
            if error is None:
                try:
                    addedDates = []
                    dump = args[1]
                    sources = pickle.loads(dump)
                    for source in sources:
    
                        if isinstance(self.mLoadingProgressDialog, QProgressDialog):
                            self.increaseProgressBar()
    
    
                        newTSD = self._addSource(source)
    
                        if isinstance(newTSD, TimeSeriesDate):
    
                            addedDates.append(newTSD)
    
                    if len(addedDates) > 0:
                        self.sigTimeSeriesDatesAdded.emit(addedDates)
    
                except Exception as ex:
    
            if isinstance(self.mLoadingProgressDialog, QProgressDialog):
                if self.mLoadingProgressDialog.wasCanceled() or self.mLoadingProgressDialog.value() == -1:
                    self.mLoadingProgressDialog = None
    
        def increaseProgressBar(self):
            if isinstance(self.mLoadingProgressDialog, QProgressDialog):
                v = self.mLoadingProgressDialog.value() + 1
                self.mLoadingProgressDialog.setValue(v)
                self.mLoadingProgressDialog.setLabelText('{}/{}'.format(v, self.mLoadingProgressDialog.maximum()))
    
                if v == 1 or v % 25 == 0:
                    QApplication.processEvents()
    
    
        def addSources(self, sources:list, progressDialog:QProgressDialog=None):
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            Adds new data sources to the TimeSeries
            :param sources: [list-of-TimeSeriesSources]
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            assert isinstance(sources, list)
    
            self.mLoadingProgressDialog = progressDialog
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            nMax = len(sources)
            # 1. read sources
            # this could be excluded into a parallel process
            addedDates = []
            for i, source in enumerate(sources):
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                msg = None
    
                if False: #debug
                    newTSD = self._addSource(source)
                else:
                    try:
                        newTSD = self._addSource(source)
                    except Exception as ex:
                        msg = 'Unable to add: {}\n{}'.format(str(source), str(ex))
                        print(msg, file=sys.stderr)
    
                if isinstance(self.mLoadingProgressDialog, QProgressDialog):
                    if self.mLoadingProgressDialog.wasCanceled():
    
                if isinstance(newTSD, TimeSeriesDate):
    
            if isinstance(progressDialog, QProgressDialog):
                progressDialog.setLabelText('Create map widgets...')
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            if len(addedDates) > 0:
                self.sigTimeSeriesDatesAdded.emit(addedDates)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
        def _addSource(self, source:TimeSeriesSource)->TimeSeriesDate:
    
            :return: TimeSeriesDate (if new created)
    
            """
            if isinstance(source, TimeSeriesSource):
                tss = source
            else:
                tss = TimeSeriesSource.create(source)
    
            assert isinstance(tss, TimeSeriesSource)
    
            newTSD = None
    
    
            tsdDate = self.date2date(tss.date())
            tssDate = tss.date()
    
            sid = tss.sid()
            sensor = self.sensor(sid)
            # if necessary, add a new sensor instance
            if not isinstance(sensor, SensorInstrument):
                sensor = self.addSensor(SensorInstrument(sid))
            assert isinstance(sensor, SensorInstrument)
    
            tsd = self.tsd(tsdDate, sensor)
    
            # if necessary, add a new TimeSeriesDate instance
            if not isinstance(tsd, TimeSeriesDate):
                tsd = self.insertTSD(TimeSeriesDate(self, tsdDate, sensor))
    
            assert isinstance(tsd, TimeSeriesDate)
    
            # add the source
            tsd.addSource(tss)
            return newTSD
    
    
        def setDateTimePrecision(self, mode:DateTimePrecision):
            """
            Sets the precision with which the parsed DateTime information will be handled.
            :param mode: TimeSeriesViewer:DateTimePrecision
            :return:
            """
            self.mDateTimePrecision = mode
    
            #do we like to update existing sources?
    
    
    
        def date2date(self, date:np.datetime64)->np.datetime64:
            """
    
            Converts a date of arbitrary precision into the date with precision according to the EOTSV settions.
    
            :param date: numpy.datetime64
            :return: numpy.datetime64
            """
    
            assert isinstance(date, np.datetime64)
            if self.mDateTimePrecision == DateTimePrecision.Original:
                return date
            else:
                date = np.datetime64(date, self.mDateTimePrecision.value)
    
            return date
    
    
        def sources(self) -> list:
            """
            Returns the input sources
            :return: iterator over [list-of-TimeSeriesSources]
            """
    
            for tsd in self:
                for source in tsd:
                    yield source
    
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def sourceUris(self)->list:
            """
            Returns the uris of all sources
            :return: [list-of-str]
            """
            uris = []
            for tsd in self:
    
                assert isinstance(tsd, TimeSeriesDate)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                uris.extend(tsd.sourceUris())
            return uris
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            return len(self.mTSDs)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            return iter(self.mTSDs)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
        def __getitem__(self, slice):
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            return self.mTSDs[slice]
    
    
        def __delitem__(self, slice):
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            self.removeTSDs(slice)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
        def __contains__(self, item):
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            return item in self.mTSDs
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
        def __repr__(self):
            info = []
            info.append('TimeSeries:')
            l = len(self)
            info.append('  Scenes: {}'.format(l))
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            return '\n'.join(info)
    
    
        def headerData(self, section, orientation, role):
            assert isinstance(section, int)
    
            if orientation == Qt.Horizontal and role == Qt.DisplayRole:
    
                if len(self.mColumnNames) > section:
                    return self.mColumnNames[section]
                else:
                    return ''
    
            else:
                return None
    
        def parent(self, index: QModelIndex) -> QModelIndex:
            """
            Returns the parent index of a QModelIndex `index`
            :param index: QModelIndex
            :return: QModelIndex
            """
            if not index.isValid():
                return QModelIndex()
    
            node = index.internalPointer()
            tsd = None
            tss = None
    
    
            if isinstance(node, TimeSeriesDate):
    
                return self.mRootIndex
    
            elif isinstance(node, TimeSeriesSource):
                tss = node
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                tsd = node.timeSeriesDate()
    
                return self.createIndex(self.mTSDs.index(tsd), 0, tsd)
    
        def rowCount(self, index: QModelIndex=None) -> int:
            """
            Return the row-count, i.e. number of child node for a TreeNode as index `index`.
            :param index: QModelIndex
            :return: int
            """
            if index is None:
                index = QModelIndex()
    
            if not index.isValid():
                return len(self)
    
            node = index.internalPointer()
    
            if isinstance(node, TimeSeriesDate):
    
                return len(node)
    
            if isinstance(node, TimeSeriesSource):
                return 0
    
    
        def columnNames(self) -> list:
            """
            Returns the column names
            :return: [list-of-string]
            """
            return self.mColumnNames[:]
    
        def columnCount(self, index:QModelIndex = None) -> int:
            """
            Returns the number of columns
            :param index: QModelIndex
            :return:
            """
    
            return len(self.mColumnNames)
    
    
        def connectTreeView(self, treeView):
            self.mTreeView = treeView
    
        def index(self, row: int, column: int, parent: QModelIndex = None) -> QModelIndex:
            """
            Returns the QModelIndex
            :param row: int
            :param column: int
            :param parent: QModelIndex
            :return: QModelIndex
            """
            if parent is None:
                parent = self.mRootIndex
            else:
                assert isinstance(parent, QModelIndex)
    
            if row < 0 or row >= len(self):
                return QModelIndex()
            if column < 0 or column >= len(self.mColumnNames):
                return QModelIndex()
    
    
            if parent == self.mRootIndex:
                # TSD node
                if row < 0 or row >= len(self):
                    return QModelIndex()
                return self.createIndex(row, column, self[row])
    
            elif parent.parent() == self.mRootIndex:
                # TSS node
                tsd = self.tsdFromIdx(parent)
                if row < 0 or row >= len(tsd):
                    return QModelIndex()
                return self.createIndex(row, column, tsd[row])
    
            return QModelIndex()
    
    
        def tsdToIdx(self, tsd:TimeSeriesDate)->QModelIndex:
    
            Returns an QModelIndex pointing on a TimeSeriesDate of interest
            :param tsd: TimeSeriesDate
    
            :return: QModelIndex
            """
            row = self.mTSDs.index(tsd)
            return self.index(row, 0)
    
    
        def tsdFromIdx(self, index: QModelIndex) -> TimeSeriesDate:
    
            Returns the TimeSeriesDate related to an QModelIndex `index`.
    
            :param index: QModelIndex
            :return: TreeNode
            """
    
            if index.row() == -1 and index.column() == -1:
                return None
            elif not index.isValid():
                return None
            else:
                node = index.internalPointer()
    
                if isinstance(node, TimeSeriesDate):
    
                    return node
                elif isinstance(node, TimeSeriesSource):
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                    return node.timeSeriesDate()
    
    
            return None
    
        def data(self, index, role):
            """
    
            :param index: QModelIndex
            :param role: Qt.ItemRole
            :return: object
            """
            assert isinstance(index, QModelIndex)
            if not index.isValid():
                return None
    
            node = index.internalPointer()
            tsd = None
            tss = None
            if isinstance(node, TimeSeriesSource):
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                tsd = node.timeSeriesDate()
    
            elif isinstance(node, TimeSeriesDate):
    
                tsd = node
    
            if role == Qt.UserRole:
                return node
    
            cName = self.mColumnNames[index.column()]
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            if isinstance(node, TimeSeriesSource):
    
                if role in [Qt.DisplayRole]:
                    if cName == self.cnDate:
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                        return str(tss.date())
    
                    if cName == self.cnImages:
                        return tss.uri()
                    if cName == self.cnNB:
                        return tss.nb
                    if cName == self.cnNL:
                        return tss.nl
                    if cName == self.cnNS:
                        return tss.ns
                    if cName == self.cnCRS:
                        return tss.crs().description()
    
                    if cName == self.cnSensor:
                        return tsd.sensor().name()
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                if role == Qt.DecorationRole and index.column() == 0:
    
                    return None
    
                    ext = tss.spatialExtent()
                    if isinstance(self.mCurrentSpatialExtent, SpatialExtent) and isinstance(ext, SpatialExtent):
                        ext = ext.toCrs(self.mCurrentSpatialExtent.crs())
    
                        b = isinstance(ext, SpatialExtent) and ext.intersects(self.mCurrentSpatialExtent)
                        if b:
                            return QIcon(r':/timeseriesviewer/icons/mapview.svg')
                        else:
                            return QIcon(r':/timeseriesviewer/icons/mapviewHidden.svg')
                    else:
                        print(ext)
                        return None
    
    
                if role == Qt.BackgroundColorRole and tsd in self.mVisibleDate:
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                    return QColor('yellow')
    
    
            if isinstance(node, TimeSeriesDate):
    
                if role in [Qt.DisplayRole]:
                    if cName == self.cnSensor:
                        return tsd.sensor().name()
                    if cName == self.cnImages:
                        return len(tsd)
                    if cName == self.cnDate:
                        return str(tsd.date())
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                if role == Qt.CheckStateRole and index.column() == 0:
    
                    return Qt.Checked if tsd.isVisible() else Qt.Unchecked
    
    
                if role == Qt.BackgroundColorRole and tsd in self.mVisibleDate:
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                    return QColor('yellow')
    
            return None
    
        def setData(self, index: QModelIndex, value: typing.Any, role: int):
    
            if not index.isValid():
                return False
    
            result = False
    
            if isinstance(node, TimeSeriesDate):
    
                if role == Qt.CheckStateRole and index.column() == 0:
                    node.setVisibility(value == Qt.Checked)
                    result = True
    
    
            if result == True:
                self.dataChanged.emit(index, index, [role])
    
    
            if bVisibilityChanged:
                self.sigVisibilityChanged.emit()
    
    
        def findDate(self, date)->TimeSeriesDate:
            """
            Returns a TimeSeriesDate closes to that in date
            :param date: numpy.datetime64 | str | TimeSeriesDate
            :return: TimeSeriesDate
            """
            if isinstance(date, str):
                date = np.datetime64(date)
            if isinstance(date, TimeSeriesDate):
                date = date.date()
            assert isinstance(date, np.datetime64)
    
            if len(self) == 0:
                return None
            dtAbs = np.abs(date - np.asarray([tsd.date() for tsd in self.mTSDs]))
    
            i = np.argmin(dtAbs)
            return self.mTSDs[i]
    
    
    
        def flags(self, index):
            assert isinstance(index, QModelIndex)
            if not index.isValid():
                return Qt.NoItemFlags
            #cName = self.mColumnNames.index(index.column())
            flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable
    
            if isinstance(index.internalPointer(), TimeSeriesDate) and index.column() == 0:
    
                flags = flags | Qt.ItemIsUserCheckable
            return flags
    
    def getSpatialPropertiesFromDataset(ds):
        assert isinstance(ds, gdal.Dataset)
    
        nb = ds.RasterCount
        nl = ds.RasterYSize
        ns = ds.RasterXSize
        proj = ds.GetGeoTransform()
        px_x = float(abs(proj[1]))
        px_y = float(abs(proj[5]))
    
        crs = QgsCoordinateReferenceSystem(ds.GetProjection())
    
        return nb, nl, ns, crs, px_x, px_y
    
    
    def extractWavelengthsFromGDALMetaData(ds:gdal.Dataset)->(list, str):
    
        Reads the wavelength info from standard metadata strings
    
        regWLkey = re.compile('^(center )?wavelength[_ ]*$', re.I)
        regWLUkey = re.compile('^wavelength[_ ]*units?$', re.I)
    
        regNumeric = re.compile(r"([-+]?\d*\.\d+|[-+]?\d+)", re.I)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
        def findKey(d:dict, regex)->str:
            for key in d.keys():
                if regex.search(key):
                    return key
    
        # 1. try band level
        wlu = []
        wl = []
        for b in range(ds.RasterCount):
            band = ds.GetRasterBand(b + 1)
            assert isinstance(band, gdal.Band)
            md = band.GetMetadata_Dict()
    
            keyWLU = findKey(md, regWLUkey)
            keyWL = findKey(md, regWLkey)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
            if isinstance(keyWL, str) and isinstance(keyWLU, str):
                wl.append(float(md[keyWL]))
                wlu.append(LUT_WAVELENGTH_UNITS[md[keyWLU].lower()])
    
        if len(wlu) == len(wl) and len(wl) == ds.RasterCount:
            return wl, wlu[0]
    
        # 2. try data set level
        for domain in ds.GetMetadataDomainList():
            md = ds.GetMetadata_Dict(domain)
    
            keyWLU = findKey(md, regWLUkey)
            keyWL = findKey(md, regWLkey)
    
            if isinstance(keyWL, str) and isinstance(keyWLU, str):
    
    
                wlu = LUT_WAVELENGTH_UNITS[md[keyWLU].lower()]
                matches = regNumeric.findall(md[keyWL])
                wl = [float(n) for n in matches]
    
    
    
                if len(wl) == ds.RasterCount:
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
    def extractWavelengthsFromRapidEyeXML(ds:gdal.Dataset, dom:QDomDocument)->(list, str):
        nodes = dom.elementsByTagName('re:bandSpecificMetadata')
        # see http://schemas.rapideye.de/products/re/4.0/RapidEye_ProductMetadata_GeocorrectedLevel.xsd
        # wavelength and units not given in the XML
        # -> use values from https://www.satimagingcorp.com/satellite-sensors/other-satellite-sensors/rapideye/
        if nodes.count() == ds.RasterCount and ds.RasterCount == 5:
            wlu = r'nm'
            wl = [0.5 * (440 + 510),
                  0.5 * (520 + 590),
                  0.5 * (630 + 685),
                  0.5 * (760 + 850),
                  0.5 * (760 - 850)
                  ]
            return wl, wlu
        return None, None
    
    
    def extractWavelengthsFromDIMAPXML(ds:gdal.Dataset, dom:QDomDocument)->(list, str):
        """
        :param dom: QDomDocument | gdal.Dataset
        :return: (list of wavelengths, str wavelength unit)
        """
        # DIMAP XML metadata?
        assert isinstance(dom, QDomDocument)
        nodes = dom.elementsByTagName('Band_Spectral_Range')
        if nodes.count() > 0:
            candidates = []
            for element in [nodes.item(i).toElement() for i in range(nodes.count())]:
                _band = element.firstChildElement('BAND_ID').text()
                _wlu = element.firstChildElement('MEASURE_UNIT').text()
                wlMin = float(element.firstChildElement('MIN').text())
                wlMax = float(element.firstChildElement('MAX').text())