Skip to content
Snippets Groups Projects
timeseries.py 69.4 KiB
Newer Older
  • Learn to ignore specific revisions
  •         if taskWrapper.isCanceled():
                return pickle.dumps(results)
    
            s = TimeSeriesSource.create(source)
            if isinstance(s, TimeSeriesSource):
                results.append(s)
    
            taskWrapper.setProgress(float(i+1) / n * 100.0)
    
        return pickle.dumps(results)
    
    class TimeSeries(QAbstractItemModel):
    
        """
        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)
    
        sigSourcesAdded = pyqtSignal(list)
        sigSourcesRemoved = pyqtSignal(list)
    
            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()
    
    
    
            self.mTasks = list()
    
    
    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
    
                #
                #idx1 = self.index(0, 0)
                #idx2 = self.index(self.rowCount()-1, 0)
                #self.dataChanged.emit(idx1, idx2, [Qt.DecorationRole])
    
        def focusVisibilityToExtent(self):
            ext = self.currentSpatialExtent()
            if isinstance(ext, SpatialExtent):
                for tsd in self:
                    assert isinstance(tsd, TimeSeriesDatum)
                    b = tsd.hasIntersectingSource(ext)
                    tsd.setVisibility(b)
    
    
        def currentSpatialExtent(self)->SpatialExtent:
            """
            Returns the current spatial extent
            :return: SpatialExtent
            """
            return self.mCurrentSpatialExtent
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            """
            Sets the TimeSeriesDates currently shown
            :param tsds: [list-of-TimeSeriesDatum]
            """
    
    
    
            self.mCurrentDates.clear()
            self.mCurrentDates.extend(tsds)
            for tsd in tsds:
                assert isinstance(tsd, TimeSeriesDatum)
    
    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])
    
                self.addSourcesAsync(images[0:n_max], progressDialog=progressDialog)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            else:
    
                self.addSourcesAsync(images, progressDialog=progressDialog)
    
    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 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: 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
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            tsd.mTimeSeries = self
            tsd.sigRemoveMe.connect(lambda: self.removeTSDs([tsd]))
    
            tsd.rowsAboutToBeRemoved.connect(self.onSourcesAboutToBeRemoved)
            tsd.rowsRemoved.connect(self.onSourcesRemoved)
            tsd.rowsAboutToBeInserted.connect(self.onSourcesAboutToBeInserted)
            tsd.rowsInserted.connect(self.onSourcesInserted)
    
            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()
            #self.rowsInserted()
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
            return tsd
    
    
        def onSourcesAboutToBeRemoved(self, parent, first, last):
            s = ""
            pass
    
        def onSourcesRemoved(self, parent, first, last):
            s = ""
        
        def onSourcesAboutToBeInserted(self, parent, first, last):
            s = ""
            
        def onSourcesInserted(self, parent, first, last):
            s = ""
            
      
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def removeTSDs(self, tsds):
            """
            Removes a list of TimeSeriesDatum
            :param tsds: [list-of-TimeSeriesDatum]
            """
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            removed = list()
            for tsd in tsds:
                assert isinstance(tsd, TimeSeriesDatum)
    
                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  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.
    
            :return: [list-of-TimeSeriesDatum]
            """
    
    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[:])
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
        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
    
            # 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):
    
            #for source in sources:
                #subset = [source]
    
                dump = pickle.dumps(subset)
                #taskDescription = 'Load EOTSV {} sources {}'.format(len(subset), uuid.uuid4())
    
                taskDescription = 'Load {} images'.format(len(subset))
    
                qgsTask = QgsTask.fromFunction(taskDescription, doLoadTimeSeriesSourcesTask, dump, on_finished=self.onAddSourcesAsyncFinished)
                self.mTasks.append(qgsTask)
    
                if False: # for debugging
                    resultDump = doLoadTimeSeriesSourcesTask(qgsTask, dump)
                    self.onAddSourcesAsyncFinished(None, resultDump)
                else:
                    tm.addTask(qgsTask)
    
        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:
                        newTSD = self._addSource(source)
                        if isinstance(newTSD, TimeSeriesDatum):
                            addedDates.append(newTSD)
    
                    if len(addedDates) > 0:
                        self.sigTimeSeriesDatesAdded.emit(addedDates)
    
    
                except Exception as ex:
                    s = ""
            else:
                s = ""
            #self._cleanTasks()
    
        def _cleanTasks(self):
            toRemove = []
            for task in self.mTasks:
                if isinstance(task, QgsTask):
                    if task.status() in [QgsTask.Complete, QgsTask.Terminated]:
                        toRemove.append(task)
    
            for t in toRemove:
                self.mTasks.remove(t)
    
    
        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)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
            nMax = len(sources)
    
            #self.sigTimeSeriesSourcesAboutToBeChanged.emit()
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
            self.sigLoadingProgress.emit(0, nMax, 'Start loading {} sources...'.format(nMax))
    
            
            if isinstance(progressDialog, QProgressDialog):
                progressDialog.setRange(0, nMax)
                progressDialog.setLabelText('Load rasters...'.format(nMax))
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            # 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(progressDialog, QProgressDialog):
                    if progressDialog.wasCanceled():
                        break
                    progressDialog.setValue(i)
                    progressDialog.setLabelText('{}/{}'.format(i+1, nMax))
    
    
                    self.sigLoadingProgress.emit(i+1, nMax, msg)
    
                if isinstance(newTSD, TimeSeriesDatum):
                    addedDates.append(newTSD)
    
            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)->TimeSeriesDatum:
            """
            :param source:
            :return: TimeSeriesDatum (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 TimeSeriesDatum instance
            if not isinstance(tsd, TimeSeriesDatum):
    
                tsd = self.insertTSD(TimeSeriesDatum(self, tsdDate, sensor))
    
                newTSD = tsd
                # addedDates.append(tsd)
            assert isinstance(tsd, TimeSeriesDatum)
            # 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 precission into the date with precission 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, TimeSeriesDatum)
                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, TimeSeriesDatum):
                return self.mRootIndex
    
            elif isinstance(node, TimeSeriesSource):
                tss = node
                tsd = node.timeSeriesDatum()
                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, TimeSeriesDatum):
                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:TimeSeriesDatum)->QModelIndex:
            """
            Returns an QModelIndex pointing on a TimeSeriesDatum of interest
            :param tsd: TimeSeriesDatum
            :return: QModelIndex
            """
            row = self.mTSDs.index(tsd)
            return self.index(row, 0)
    
        def tsdFromIdx(self, index: QModelIndex) -> TimeSeriesDatum:
            """
            Returns the TimeSeriesDatum 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, TimeSeriesDatum):
                    return node
                elif isinstance(node, TimeSeriesSource):
                    return node.timeSeriesDatum()
    
            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):
                tsd = node.timeSeriesDatum()
                tss = node
            elif isinstance(node, TimeSeriesDatum):
                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()
    
    
    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.mCurrentDates:
                    return QColor('yellow')
    
            if isinstance(node, TimeSeriesDatum):
    
                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
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                if role == Qt.BackgroundColorRole and tsd in self.mCurrentDates:
                    return QColor('yellow')
    
            return None
    
        def setData(self, index: QModelIndex, value: typing.Any, role: int):
    
            if not index.isValid():
                return False
    
            result = False
    
            node = index.internalPointer()
            if isinstance(node, TimeSeriesDatum):
                if role == Qt.CheckStateRole and index.column() == 0:
                    node.setVisibility(value == Qt.Checked)
                    result = True
    
            if result == True:
                self.dataChanged.emit(index, index, [role])
    
            return result
    
        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(), TimeSeriesDatum) and index.column() == 0:
                flags = flags | Qt.ItemIsUserCheckable
            return flags
    
    
    
    class TimeSeriesTableModel(QAbstractTableModel):
    
    
        def __init__(self, TS:TimeSeries, parent=None, *args):
    
    
            super(TimeSeriesTableModel, self).__init__()
            assert isinstance(TS, TimeSeries)
    
            self.cnDate = 'Date'
            self.cnSensor = 'Sensor'
            self.cnNS = 'ns'
            self.cnNL = 'nl'
            self.cnNB = 'nb'
            self.cnCRS = 'CRS'
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            self.cnImages = 'Image(s)'
    
            self.mColumnNames = [self.cnDate, self.cnSensor,
                                 self.cnNS, self.cnNL, self.cnNB,
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                                 self.cnCRS, self.cnImages]
    
            self.mTimeSeries = TS
            self.mSensors = set()
            self.mTimeSeries.sigTimeSeriesDatesRemoved.connect(self.removeTSDs)
            self.mTimeSeries.sigTimeSeriesDatesAdded.connect(self.addTSDs)
    
    
            self.items = []
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
            self.addTSDs([tsd for tsd in self.mTimeSeries])
    
        def timeSeries(self)->TimeSeries:
            """
            :return: TimeSeries
            """
            return self.mTimeSeries
    
    
        def removeTSDs(self, tsds:list):
            """
            Removes TimeSeriesDatum instances
            :param tsds: list
            """
    
            for tsd in tsds:
    
                if tsd in self.mTimeSeries:
                    self.mTimeSeries.removeTSDs([tsd])
    
                elif tsd in self.items:
                    idx = self.getIndexFromDate(tsd)
                    self.removeRows(idx.row(), 1)
    
    
            idx = self.getIndexFromDate(tsd)
            self.dataChanged.emit(idx, idx)
    
    
        def sensorsChanged(self, sensor):
    
            i = self.mColumnNames.index(self.cnSensor)
    
            idx0 = self.createIndex(0, i)
            idx1 = self.createIndex(self.rowCount(), i)
            self.dataChanged.emit(idx0, idx1)
    
    
        def addTSDs(self, tsds):
    
    
            for tsd in tsds:
                assert isinstance(tsd, TimeSeriesDatum)
                row = bisect.bisect_left(self.items, tsd)
                self.beginInsertRows(QModelIndex(), row, row)
                self.items.insert(row, tsd)
                self.endInsertRows()
    
                #self.sort(self.sortColumnIndex, self.sortOrder)
    
    
            for tsd in tsds:
                assert isinstance(tsd, TimeSeriesDatum)
                tsd.sigVisibilityChanged.connect(lambda: self.tsdChanged(tsd))
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            for sensor in set([tsd.sensor() for tsd in tsds]):
    
                if sensor not in self.mSensors:
                    self.mSensors.add(sensor)
    
                    sensor.sigNameChanged.connect(self.sensorsChanged)
    
        def rowCount(self, parent = QModelIndex())->int:
    
            return len(self.items)
    
    
        def removeRows(self, row, count , parent=QModelIndex()):
            self.beginRemoveRows(parent, row, row+count-1)
            toRemove = self.items[row:row+count]
            for tsd in toRemove:
                self.items.remove(tsd)
            self.endRemoveRows()
    
    
        def getIndexFromDate(self, tsd:TimeSeriesDatum)->QModelIndex:
            assert isinstance(tsd, TimeSeriesDatum)
    
            return self.createIndex(self.items.index(tsd),0)
    
    
        def getDateFromIndex(self, index:QModelIndex)->TimeSeriesDatum:
            assert isinstance(index, QModelIndex)
    
            if index.isValid():
                return self.items[index.row()]
            return None
    
    
        def getTimeSeriesDatumFromIndex(self, index:QModelIndex)->TimeSeriesDatum:
            assert isinstance(index, QModelIndex)
    
            if index.isValid():
                i = index.row()
                if i >= 0 and i < len(self.items):
                    return self.items[i]
    
            return None
    
    
        def columnCount(self, parent = QModelIndex())->int:
    
            return len(self.mColumnNames)
    
    
        def data(self, index, role = Qt.DisplayRole):
            if role is None or not index.isValid():
                return None
    
            value = None
    
            columnName = self.mColumnNames[index.column()]
    
    
            TSD = self.getTimeSeriesDatumFromIndex(index)
    
            assert isinstance(TSD, TimeSeriesDatum)
    
            keys = list(TSD.__dict__.keys())
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            tssList = TSD.sources()
    
    
            if role == Qt.DisplayRole or role == Qt.ToolTipRole:
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                if columnName == self.cnSensor:
    
                    if role == Qt.ToolTipRole:
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                        value = TSD.sensor().description()
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                    else:
                        value = TSD.sensor().name()
    
                elif columnName == self.cnDate:
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                    value = '{}'.format(TSD.date())
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                elif columnName == self.cnImages:
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                    value = '\n'.join(TSD.sourceUris())
    
                elif columnName == self.cnCRS:
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                    value = '\n'.join([tss.crs().description() for tss in tssList])
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                elif columnName == self.cnNB:
                    value = TSD.sensor().nb
                elif columnName == self.cnNL:
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                    value = '\n'.join([str(tss.nl) for tss in tssList])
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                elif columnName == self.cnNS:
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                    value = '\n'.join([str(tss.ns) for tss in tssList])
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                elif columnName == self.cnSensor:
                    value = TSD.sensor().name()
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
                elif columnName in keys:
                    value = TSD.__dict__[columnName]
                else:
                    s = ""
            elif role == Qt.CheckStateRole:
    
                if columnName == self.cnDate:
    
                    value = Qt.Checked if TSD.isVisible() else Qt.Unchecked
    
            elif role == Qt.BackgroundColorRole:
                value = None
            elif role == Qt.UserRole:
                value = TSD
    
            return value
    
        def setData(self, index, value, role=None):
            if role is None or not index.isValid():
                return None
    
            if role is Qt.UserRole:
    
                s = ""