Skip to content
Snippets Groups Projects
temporalprofiles.py 64.3 KiB
Newer Older



            # calculate pixel indices to load
            for tpId, wkt in task.mGeometries.items():
                geom = QgsGeometry.fromWkt(wkt)
                assert isinstance(geom, QgsGeometry)
                pt = geom.centroid().asPoint()

                px = geo2px(pt, gt)
                if px.x() < 0 or px.x() > ns or px.y() < 0  or px.y() > nl:
                    task.mERRORS.append('TemporalProfile {} is out of image bounds: {} = pixel {}'.format(tpId, geom, px))
                    continue

                task.mRESULTS[tpId] = {'px_x':px.x(),
                                       'px_y':px.y(),
                                       'geo_x':pt.x(),
                                       'geo_y':px.y(),
                                       'gt':gt}
                pxIndices[tpId] = px

            # todo: implement load balancing

            for j, bandIndex in enumerate([b for b in task.mBandIndices if b >= 0 and b < nb]):

                band = ds.GetRasterBand(bandIndex + 1)
                assert isinstance(band, gdal.Band)
                no_data = band.GetNoDataValue()

                bandName = 'b{}'.format(bandIndex + 1)

                for tpId, px in pxIndices.items():
                    assert isinstance(px, QPoint)

                    value = band.ReadAsArray(px.x(), px.y(), 1, 1).flatten()[0]
                    if no_data and value == no_data:
                        value = np.NaN

                    task.mRESULTS[tpId][bandName] = value


        except Exception as ex:
            task.mERRORS.append('Error source image {}:\n{}'.format(task.mSourcePath, ex))
        results.append(task)
        qgsTask.setProgress(100 * (i + 1) / n)
    return pickle.dumps(results)



class TemporalProfilePlotDataItem(pg.PlotDataItem):

    def __init__(self, plotStyle, parent=None):
        assert isinstance(plotStyle, TemporalProfile2DPlotStyle)


        super(TemporalProfilePlotDataItem, self).__init__([], [], parent=parent)
        self.menu = None
        #self.setFlags(QGraphicsItem.ItemIsSelectable)
        self.mPlotStyle = plotStyle
        self.setAcceptedMouseButtons(Qt.LeftButton | Qt.RightButton)
        self.mPlotStyle.sigUpdated.connect(self.updateDataAndStyle)
        self.updateStyle()

    # On right-click, raise the context menu
    def mouseClickEvent(self, ev):
        if ev.button() == QtCore.Qt.RightButton:
            if self.raiseContextMenu(ev):
                ev.accept()

    def raiseContextMenu(self, ev):
        menu = self.getContextMenus()

        # Let the scene add on to the end of our context menu
        # (this is optional)
        menu = self.scene().addParentContextMenus(self, menu, ev)

        pos = ev.screenPos()
        menu.popup(QtCore.QPoint(pos.x(), pos.y()))
        return True

    # This method will be called when this item's _children_ want to raise
    # a context menu that includes their parents' menus.
    def getContextMenus(self, event=None):
        if self.menu is None:
            self.menu = QMenu()
            self.menu.setTitle(self.name + " options..")

            green = QAction("Turn green", self.menu)
            green.triggered.connect(self.setGreen)
            self.menu.addAction(green)
            self.menu.green = green

            blue = QAction("Turn blue", self.menu)
            blue.triggered.connect(self.setBlue)
            self.menu.addAction(blue)
            self.menu.green = blue

            alpha = QWidgetAction(self.menu)
            alphaSlider = QSlider()
            alphaSlider.setOrientation(QtCore.Qt.Horizontal)
            alphaSlider.setMaximum(255)
            alphaSlider.setValue(255)
            alphaSlider.valueChanged.connect(self.setAlpha)
            alpha.setDefaultWidget(alphaSlider)
            self.menu.addAction(alpha)
            self.menu.alpha = alpha
            self.menu.alphaSlider = alphaSlider
        return self.menu


    def updateDataAndStyle(self):

        TP = self.mPlotStyle.temporalProfile()
        sensor = self.mPlotStyle.sensor()

        if isinstance(TP, TemporalProfile) and isinstance(sensor, SensorInstrument):
Benjamin Jakimow's avatar
Benjamin Jakimow committed
            x, y = TP.dataFromExpression(self.mPlotStyle.sensor(), self.mPlotStyle.expression(), dateType='date')

            if np.any(np.isfinite(y)):
                self.setData(x=x, y=y, connect='finite')
                self.setData(x=[], y=[]) # dummy
        else:
            self.setData(x=[], y=[])  # dummy for empty data
    def updateStyle(self):
        """
        Updates visibility properties
        """
        self.setVisible(self.mPlotStyle.isVisible())
        self.setSymbol(self.mPlotStyle.markerSymbol)
        self.setSymbolSize(self.mPlotStyle.markerSize)
        self.setSymbolBrush(self.mPlotStyle.markerBrush)
        self.setSymbolPen(self.mPlotStyle.markerPen)
        self.setPen(self.mPlotStyle.linePen)
        self.update()



    def setClickable(self, b, width=None):
        assert isinstance(b, bool)
        self.curve.setClickable(b, width=width)

    def setColor(self, color):
        if not isinstance(color, QColor):

            color = QColor(color)
        self.setPen(color)

    def pen(self):
        return fn.mkPen(self.opts['pen'])

    def color(self):
        return self.pen().color()


    def setLineWidth(self, width):
        pen = pg.mkPen(self.opts['pen'])
        assert isinstance(pen, QPen)
        pen.setWidth(width)
        self.setPen(pen)



Benjamin Jakimow's avatar
Benjamin Jakimow committed
VSI_DIR = r'/vsimem/temporalprofiles/'

class TemporalProfileLayer(QgsVectorLayer):
    """
    A collection to store the TemporalProfile data delivered by a PixelLoader
    """

    #sigSensorAdded = pyqtSignal(SensorInstrument)
    #sigSensorRemoved = pyqtSignal(SensorInstrument)
    #sigPixelAdded = pyqtSignal()
    #sigPixelRemoved = pyqtSignal()

    sigTemporalProfilesAdded = pyqtSignal(list)
    sigTemporalProfilesRemoved = pyqtSignal(list)
    sigMaxProfilesChanged = pyqtSignal(int)
Benjamin Jakimow's avatar
Benjamin Jakimow committed


Benjamin Jakimow's avatar
Benjamin Jakimow committed
    def __init__(self, timeSeries:TimeSeries, uri=None, name='Temporal Profiles'):
Benjamin Jakimow's avatar
Benjamin Jakimow committed

        lyrOptions = QgsVectorLayer.LayerOptions(loadDefaultStyle=False, readExtentFromXml=False)

        if uri is None:
            # create a new, empty backend
            # existing_vsi_files = vsiSpeclibs()
            existing_vsi_files = []
            # todo:
            assert isinstance(existing_vsi_files, list)
            i = 0
Benjamin Jakimow's avatar
Benjamin Jakimow committed
            _name = name.replace(' ', '_')
            uri = (pathlib.Path(VSI_DIR) / '{}.gpkg'.format(_name)).as_posix()
            while not ogr.Open(uri) is None:
Benjamin Jakimow's avatar
Benjamin Jakimow committed
                i += 1
Benjamin Jakimow's avatar
Benjamin Jakimow committed
                uri = (pathlib.Path(VSI_DIR) / '{}{:03}.gpkg'.format(_name, i)).as_posix()
Benjamin Jakimow's avatar
Benjamin Jakimow committed

            drv = ogr.GetDriverByName('GPKG')
            assert isinstance(drv, ogr.Driver)
            co = ['VERSION=AUTO']
            dsSrc = drv.CreateDataSource(uri, options=co)
            assert isinstance(dsSrc, ogr.DataSource)
            srs = osr.SpatialReference()
            srs.ImportFromEPSG(4326)
            co = ['GEOMETRY_NAME=geom',
                  'GEOMETRY_NULLABLE=YES',
Benjamin Jakimow's avatar
Benjamin Jakimow committed
                  ]

            lyr = dsSrc.CreateLayer(name, srs=srs, geom_type=ogr.wkbPoint, options=co)

            assert isinstance(lyr, ogr.Layer)
            ldefn = lyr.GetLayerDefn()
            assert isinstance(ldefn, ogr.FeatureDefn)
            dsSrc.FlushCache()
        else:
            dsSrc = ogr.Open(uri)
            assert isinstance(dsSrc, ogr.DataSource)
            names = [dsSrc.GetLayerByIndex(i).GetName() for i in range(dsSrc.GetLayerCount())]
            i = names.index(name)
            lyr = dsSrc.GetLayer(i)


        # consistency check
        uri2 = '{}|{}'.format(dsSrc.GetName(), lyr.GetName())
        assert QgsVectorLayer(uri2).isValid()
        super(TemporalProfileLayer, self).__init__(uri2, name, 'ogr', lyrOptions)


        """
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        assert isinstance(timeSeries, TimeSeries)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        crs = QgsCoordinateReferenceSystem('EPSG:4326')
        uri = 'Point?crs={}'.format(crs.authid())
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        lyrOptions = QgsVectorLayer.LayerOptions(loadDefaultStyle=False, readExtentFromXml=False)
        super(TemporalProfileLayer, self).__init__(uri, name, 'memory', lyrOptions)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        """
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        from collections import OrderedDict
        self.mProfiles = OrderedDict()
        self.mTimeSeries = timeSeries
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        #symbol = QgsFillSymbol.createSimple({'style': 'no', 'color': 'red', 'outline_color': 'black'})
        #self.mLocations.renderer().setSymbol(symbol)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        self.setName('EOTS Temporal Profiles')
        #fields.append(createQgsField(FN_ID, self.mNextID))
        fields.append(createQgsField(FN_NAME, ''))
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        fields.append(createQgsField(FN_X, 0.0, comment='Longitude'))
        fields.append(createQgsField(FN_Y, 0.0, comment='Latitude'))
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        #fields.append(createQgsField(FN_N_TOTAL, 0, comment='Total number of band values'))
        #fields.append(createQgsField(FN_N_NODATA,0, comment='Total of no-data values.'))
        #fields.append(createQgsField(FN_N_LOADED, 0, comment='Loaded valid band values.'))
        #fields.append(createQgsField(FN_N_LOADED_PERCENT,0.0, comment='Loading progress (%)'))
        assert self.startEditing()
        assert self.dataProvider().addAttributes(fields)
        assert self.commitChanges()
        self.initConditionalStyles()

        self.committedFeaturesAdded.connect(self.onFeaturesAdded)
        self.committedFeaturesRemoved.connect(self.onFeaturesRemoved)
        return list(self.mProfiles.values())[slice]
Benjamin Jakimow's avatar
Benjamin Jakimow committed
    def loadMissingData(self, backgroundProcess=False):
        assert isinstance(self.mTimeSeries, TimeSeries)

        # Get or create the TimeSeriesProfiles which will store the loaded values
        tasks = []

        theGeometries = []

        # Define which (new) bands need to be loaded for each sensor
        LUT_bandIndices = dict()
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        for sensor in self.mTimeSeries.sensors():
Benjamin Jakimow's avatar
Benjamin Jakimow committed
                LUT_bandIndices[sensor] = list(range(sensor.nb))

        PL = PixelLoader()
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        PL.sigPixelLoaded.connect(self.addPixelLoaderResult)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        # update new / existing points

        for tsd in self.mTimeSeries:
            assert isinstance(tsd, TimeSeriesDate)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
            requiredIndices = LUT_bandIndices[tsd.mSensor]
Benjamin Jakimow's avatar
Benjamin Jakimow committed
            requiredIndexKeys = [bandIndex2bandKey(b) for b in requiredIndices]
            TPs = []
            missingIndices = set()
            for TP in self.mProfiles.values():
                assert isinstance(TP, TemporalProfile)
                dataValues = TP.mData[tsd]
                existingKeys = list(dataValues.keys())
                missingIdx = [bandKey2bandIndex(k) for k in requiredIndexKeys if k not in existingKeys]
                if len(missingIdx) > 0:
                    TPs.append(TP)
                    missingIndices.union(set(missingIdx))

            if len(TPs) > 0:
                theGeometries = [tp.coordinate() for tp in TPs]
                theIDs = [tp.id() for tp in TPs]
Benjamin Jakimow's avatar
Benjamin Jakimow committed
                for pathImg in tsd.sourceUris():
                    task = PixelLoaderTask(pathImg, theGeometries,
                                           bandIndices=requiredIndices,
                                           temporalProfileIDs=theIDs)
                    tasks.append(task)
Benjamin Jakimow's avatar
Benjamin Jakimow committed


        if len(tasks) > 0:

            if backgroundProcess:
                PL.startLoading(tasks)
            else:
                import eotimeseriesviewer.pixelloader
Benjamin Jakimow's avatar
Benjamin Jakimow committed
                dump = pickle.dumps(tasks)
                tasks =pickle.loads(eotimeseriesviewer.pixelloader.doLoaderTask(eotimeseriesviewer.pixelloader.TaskMock(), dump))
Benjamin Jakimow's avatar
Benjamin Jakimow committed
                for i, task in enumerate(tasks):
Benjamin Jakimow's avatar
Benjamin Jakimow committed
                    PL.sigPixelLoaded.emit(task)
Benjamin Jakimow's avatar
Benjamin Jakimow committed


        else:
            if DEBUG:
                print('Data for geometries already loaded')

        s = ""

Benjamin Jakimow's avatar
Benjamin Jakimow committed
    def saveTemporalProfiles(self, pathVector, sep='\t'):
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        if pathVector is None or len(pathVector) == 0:
            global DEFAULT_SAVE_PATH
            if DEFAULT_SAVE_PATH == None:
                DEFAULT_SAVE_PATH = 'temporalprofiles.shp'
            d = os.path.dirname(DEFAULT_SAVE_PATH)
            filters = QgsProviderRegistry.instance().fileVectorFilters()
            pathVector, filter = QFileDialog.getSaveFileName(None, 'Save {}'.format(self.name()), DEFAULT_SAVE_PATH,
                                                             filter=filters)

            if len(pathVector) == 0:
                return None
            else:
                DEFAULT_SAVE_PATH = pathVector


        drvName = QgsVectorFileWriter.driverForExtension(os.path.splitext(pathVector)[-1])
        QgsVectorFileWriter.writeAsVectorFormat(self, pathVector, 'utf-8', destCRS=self.crs(), driverName=drvName)

        pathCSV = os.path.splitext(pathVector)[0] + '.data.csv'
        # write a flat list of profiles
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        csvLines = ['Temporal Profiles']
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        nBands = max([s.nb for s in self.mTimeSeries.sensors()])
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        csvLines.append(sep.join(['id', 'name', 'sensor', 'date', 'doy', 'sensor'] + ['b{}'.format(b+1) for b in range(nBands)]))
Benjamin Jakimow's avatar
Benjamin Jakimow committed

        for p in list(self.getFeatures()):

            assert isinstance(p, QgsFeature)
            fid = p.id()
            tp = self.mProfiles.get(fid)
            if tp is None:
                continue
            assert isinstance(tp, TemporalProfile)
            name = tp.name()
            for tsd, values in tp.mData.items():
                assert isinstance(tsd, TimeSeriesDate)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
                line = [fid, name, tsd.mSensor.name(), tsd.mDate, tsd.mDOY]
                for b in range(tsd.mSensor.nb):
Benjamin Jakimow's avatar
Benjamin Jakimow committed
                    key = 'b{}'.format(b+1)
                    line.append(values.get(key))

                line = ['' if v == None else str(v) for v in line]
                line = sep.join([str(l) for l in line])
Benjamin Jakimow's avatar
Benjamin Jakimow committed
                csvLines.append(line)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
            s = ""

Benjamin Jakimow's avatar
Benjamin Jakimow committed
        # write CSV file
        with open(pathCSV, 'w', encoding='utf8') as f:
            f.write('\n'.join(csvLines))
Benjamin Jakimow's avatar
Benjamin Jakimow committed

        return [pathVector, pathCSV]

Benjamin Jakimow's avatar
Benjamin Jakimow committed
        """
        Returns the TimeSeries instance.
        :return: TimeSeries
        """
    def onFeaturesAdded(self, layerID, addedFeatures):
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        """
        Create a TemporalProfile object for each QgsFeature added to the backend QgsVectorLayer
        :param layerID:
        :param addedFeatures:
        :return:
        """
            temporalProfiles = []
            for feature in addedFeatures:
                fid = feature.id()
Benjamin Jakimow's avatar
Benjamin Jakimow committed
                if fid < 0:
                    continue
Benjamin Jakimow's avatar
Benjamin Jakimow committed
                tp = TemporalProfile(self, fid, feature.geometry())
                self.mProfiles[fid] = tp
                temporalProfiles.append(tp)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
            if len(temporalProfiles) > 0:
                pass
                #self.sigTemporalProfilesAdded.emit(temporalProfiles)
    def onFeaturesRemoved(self,  layerID, removedFIDs):
    def initConditionalStyles(self):
        styles = self.conditionalStyles()
        assert isinstance(styles, QgsConditionalLayerStyles)
        for fieldName in self.fields().names():
            red = QgsConditionalStyle("@value is NULL")
            red.setTextColor(QColor('red'))
            styles.setFieldStyles(fieldName, [red])
        #styles.setRowStyles([red])
    def createTemporalProfiles(self, coordinates, names:list=None)->list:
        """
        Creates temporal profiles
        :param coordinates:
        :return:
        """

        if isinstance(coordinates, QgsVectorLayer):
            lyr = coordinates
            coordinates = []
            names = []
            trans = QgsCoordinateTransform()
            trans.setSourceCrs(lyr.crs())
            trans.setDestinationCrs(self.crs())

            nameField = None
            if isinstance(names, str) and names in lyr.fields().names():
                nameField = names
            else:
                for name in lyr.fields().names():
                    if re.search('names?', name, re.I):
                        nameField = name
                        break
            if nameField is None:
                nameField = lyr.fields().names()[0]

            for f in lyr.getFeatures():
                assert isinstance(f, QgsFeature)
                g = f.geometry()
                if g.isEmpty():
                    continue
                g = g.centroid()
                assert g.transform(trans) == 0
                coordinates.append(SpatialPoint(self.crs(), g.asPoint()))
                names.append(f.attribute(nameField))

            del trans

        elif not isinstance(coordinates, list):
        assert isinstance(coordinates, list)

        if not isinstance(names, list):
            n = self.featureCount()
            names = []
            for i in range(len(coordinates)):
                names.append('Profile {}'.format(n+i+1))

        assert len(coordinates) == len(names)

Benjamin Jakimow's avatar
Benjamin Jakimow committed
        n = self.dataProvider().featureCount()
        for i, (coordinate, name) in enumerate(zip(coordinates, names)):
            assert isinstance(coordinate, SpatialPoint)
            f = QgsFeature(self.fields())
            f.setGeometry(QgsGeometry.fromPointXY(coordinate.toCrs(self.crs())))
            #f.setAttribute(FN_ID, self.mNextID)
            f.setAttribute(FN_NAME, name)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
            f.setAttribute(FN_X, coordinate.x())
            f.setAttribute(FN_Y, coordinate.y())
Benjamin Jakimow's avatar
Benjamin Jakimow committed
            #f.setAttribute(FN_N_LOADED_PERCENT, 0.0)
            #f.setAttribute(FN_N_LOADED, 0)
            #f.setAttribute(FN_N_TOTAL, 0)
            #f.setAttribute(FN_N_NODATA, 0)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        self.startEditing()

        newFeatures = []
        def onFeaturesAdded(lid, fids):
            newFeatures.extend(fids)

        self.committedFeaturesAdded.connect(onFeaturesAdded)
        self.beginEditCommand('Add {} profile locations'.format(len(features)))
        success = self.addFeatures(features)
        self.committedFeaturesAdded.disconnect(onFeaturesAdded)
        assert self.featureCount() == len(self.mProfiles)
        profiles = [self.mProfiles[f.id()] for f in newFeatures]
        return profiles
Benjamin Jakimow's avatar
Benjamin Jakimow committed
    def saveEdits(self, leaveEditable=False, triggerRepaint=True):
        """
        function to save layer changes-
        :param layer:
        :param leaveEditable:
        :param triggerRepaint:
        """
        if not self.isEditable():
            return
        if not self.commitChanges():
            self.commitErrors()
        if leaveEditable:
            self.startEditing()
        if triggerRepaint:
            self.triggerRepaint()
    def addMissingFields(self, fields):
        missingFields = []
        for field in fields:
            assert isinstance(field, QgsField)
            i = self.dataProvider().fieldNameIndex(field.name())
            if i == -1:
                missingFields.append(field)
        if len(missingFields) > 0:
            b = self.isEditable()
            self.startEditing()
            self.dataProvider().addAttributes(missingFields)
            self.saveEdits(leaveEditable=b)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        return self.dataProvider().featureCount()
    def __iter__(self):
        r = QgsFeatureRequest()
        for f in self.getFeatures(r):
            yield self.mProfiles[f.id()]
    def __contains__(self, item):
        return item in self.mProfiles.values()


    def temporalProfileToLocationFeature(self, tp:TemporalProfile):

        self.mLocations.selectByIds([tp.id()])
        for f in self.mLocations.selectedFeatures():
            assert isinstance(f, QgsFeature)
            return f

        return None

    def fromSpatialPoint(self, spatialPoint):
        """ Tests if a Temporal Profile already exists for the given spatialPoint"""
Benjamin Jakimow's avatar
Benjamin Jakimow committed

        for p in list(self.mProfiles.values()):
            assert isinstance(p, TemporalProfile)
            if p.coordinate() == spatialPoint:
                return p
        """
        spatialPoint = spatialPoint.toCrs(self.crs())
        unit = QgsUnitTypes.toAbbreviatedString(self.crs().mapUnits()).lower()
        x = spatialPoint.x() + 0.00001
        y = spatialPoint.y() + 0.
        if 'degree' in unit:
            dx = dy = 0.000001
            dx = dy = 0.1
        rect = QgsRectangle(x-dx,y-dy, x+dy,y+dy)
        for f  in self.getFeatures(rect):
            return self.mProfiles[f.id()]
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        """

    def removeTemporalProfiles(self, temporalProfiles):
        """
        Removes temporal profiles from this collection
        :param temporalProfile: TemporalProfile
        """

        if isinstance(temporalProfiles, TemporalProfile):
            temporalProfiles = [temporalProfiles]
        assert isinstance(temporalProfiles, list)

        temporalProfiles = [tp for tp in temporalProfiles if isinstance(tp, TemporalProfile) and tp.id() in self.mProfiles.keys()]
            b = self.isEditable()
            assert self.startEditing()

            fids = [tp.mID for tp in temporalProfiles]

            self.deleteFeatures(fids)
            self.saveEdits(leaveEditable=b)

            self.sigTemporalProfilesRemoved.emit(temporalProfiles)

    def loadCoordinatesFromOgr(self, path):
        """Loads the TemporalProfiles for vector geometries in data source 'path' """
        if path is None:
            filters = QgsProviderRegistry.instance().fileVectorFilters()
            defDir = None
            if isinstance(DEFAULT_SAVE_PATH, str) and len(DEFAULT_SAVE_PATH) > 0:
                defDir = os.path.dirname(DEFAULT_SAVE_PATH)
            path, filter = QFileDialog.getOpenFileName(directory=defDir, filter=filters)

        if isinstance(path, str) and len(path) > 0:
            sourceLyr = QgsVectorLayer(path)
            nameAttribute = None

            fieldNames = [n.lower() for n in sourceLyr.fields().names()]
            for candidate in ['name', 'id']:
                if candidate in fieldNames:
                    nameAttribute = sourceLyr.fields().names()[fieldNames.index(candidate)]
                    break

            if len(self.timeSeries()) == 0:
                sourceLyr.selectAll()
            else:
Benjamin Jakimow's avatar
Benjamin Jakimow committed
                extent = self.timeSeries().maxSpatialExtent(sourceLyr.crs())
                sourceLyr.selectByRect(extent)
            newProfiles = []
            for feature in sourceLyr.selectedFeatures():
                assert isinstance(feature, QgsFeature)
                geom = feature.geometry()
                if isinstance(geom, QgsGeometry):
                    point = geom.centroid().constGet()
                    try:
                        TPs = self.createTemporalProfiles(SpatialPoint(sourceLyr.crs(), point))
                        for TP in TPs:
                            if nameAttribute:
                                name = feature.attribute(nameAttribute)
                            else:
                                name = 'FID {}'.format(feature.id())
                            TP.setName(name)
                            newProfiles.append(TP)
                    except Exception as ex:
                        print(ex)

    def addPixelLoaderResult(self, d):
        assert isinstance(d, PixelLoaderTask)
        if d.success():
            for fid in d.temporalProfileIDs:
                TP = self.mProfiles.get(fid)
                if isinstance(TP, TemporalProfile):
                    TP.pullDataUpdate(d)
                else:
Benjamin Jakimow's avatar
Benjamin Jakimow committed
                    pass
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        else:
            s = ""

    def clear(self):
        #todo: remove TS Profiles
        #self.mTemporalProfiles.clear()
        #self.sensorPxLayers.clear()
        pass


class TemporalProfileTableFilterModel(QgsAttributeTableFilterModel):
    def __init__(self, sourceModel, parent=None):
        dummyCanvas = QgsMapCanvas(parent)
        dummyCanvas.setDestinationCrs(DEFAULT_CRS)
        dummyCanvas.setExtent(QgsRectangle(-180,-90,180,90))
        super(TemporalProfileTableFilterModel, self).__init__(dummyCanvas, sourceModel, parent=parent)
class TemporalProfileTableModel(QgsAttributeTableModel):
    #sigPlotStyleChanged = pyqtSignal(SpectralProfile)
    #sigAttributeRemoved = pyqtSignal(str)
    #sigAttributeAdded = pyqtSignal(str)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
    AUTOGENERATES_COLUMNS = [FN_ID, FN_Y, FN_X]
                             #FN_N_LOADED, FN_N_TOTAL, FN_N_NODATA,
                             #FN_N_LOADED_PERCENT

    def __init__(self, temporalProfileLayer=None, parent=None):

        if temporalProfileLayer is None:
            temporalProfileLayer = TemporalProfileLayer()

        cache = QgsVectorLayerCache(temporalProfileLayer, 1000)

        super(TemporalProfileTableModel, self).__init__(cache, parent)
        self.mTemporalProfileLayer = temporalProfileLayer
        self.mCache = cache
        assert self.mCache.layer() == self.mTemporalProfileLayer
        self.loadLayer()

    def columnNames(self):
        return self.mTemporalProfileLayer.fields().names()

    def feature(self, index):

        id = self.rowToId(index.row())
        f = self.layer().getFeature(id)

        return f

    def temporalProfile(self, index):
        feature = self.feature(index)
        return self.mTemporalProfileLayer.temporalProfileFromFeature(feature)

    def data(self, index, role=Qt.DisplayRole):
        Returns Temporal Profile Layer values
        :param index: QModelIndex
        :param role: enum Qt.ItemDataRole
        :return: value
        """
        if role is None or not index.isValid():
            return None

Benjamin Jakimow's avatar
Benjamin Jakimow committed
        result = super(TemporalProfileTableModel, self).data(index, role=role)

    def setData(self, index, value, role=None):
        """
        Sets Temporal Profile Data.
        :param index: QModelIndex()
        :param value: value to set
        :param role: role
        :return: True | False
        """
        if role is None or not index.isValid():
            return False

        f = self.feature(index)
        result = False

        if value == None:
            value = QVariant()
        cname = self.columnNames()[index.column()]
        if role == Qt.EditRole and cname not in TemporalProfileTableModel.AUTOGENERATES_COLUMNS:
            i = f.fieldNameIndex(cname)
            if f.attribute(i) == value:
                return False
            b = self.mTemporalProfileLayer.isEditable()
            self.mTemporalProfileLayer.startEditing()
            self.mTemporalProfileLayer.changeAttributeValue(f.id(), i, value)
            self.mTemporalProfileLayer.saveEdits(leaveEditable=b)
            result = True
            #f = self.layer().getFeature(profile.id())
            #i = f.fieldNameIndex(SpectralProfile.STYLE_FIELD)
            #self.layer().changeAttributeValue(f.id(), i, value)
            #result = super().setData(self.index(index.row(), self.mcnStyle), value, role=Qt.EditRole)
            #if not b:
            #    self.layer().commitChanges()
        if result:
            self.dataChanged.emit(index, index, [role])
        else:
            result = super().setData(index, value, role=role)


        return result


Benjamin Jakimow's avatar
Benjamin Jakimow committed
    def headerData(self, section:int, orientation:Qt.Orientation, role:int):
        data = super(TemporalProfileTableModel, self).headerData(section, orientation, role)
        if role == Qt.ToolTipRole and orientation == Qt.Horizontal:
            #add the field comment to column description
            field = self.layer().fields().at(section)
            assert isinstance(field, QgsField)
            comment = field.comment()
            if len(comment) > 0:
                data = re.sub('</p>$', ' <i>{}</i></p>'.format(comment), data)

        return data

    def supportedDragActions(self):
        return Qt.CopyAction | Qt.MoveAction

    def supportedDropActions(self):
        return Qt.CopyAction | Qt.MoveAction


    def supportedDragActions(self):
        return Qt.CopyAction

    def supportedDropActions(self):
        return Qt.CopyAction

    def flags(self, index):

        if index.isValid():
            columnName = self.columnNames()[index.column()]
            flags = super(TemporalProfileTableModel, self).flags(index) | Qt.ItemIsSelectable
            #if index.column() == 0:
            #    flags = flags | Qt.ItemIsUserCheckable

            if columnName in TemporalProfileTableModel.AUTOGENERATES_COLUMNS:
                flags = flags ^ Qt.ItemIsEditable
            return flags
        return None


class TemporalProfileFeatureSelectionManager(QgsIFeatureSelectionManager):


    def __init__(self, layer, parent=None):
        s =""
        super(TemporalProfileFeatureSelectionManager, self).__init__(parent)
        assert isinstance(layer, QgsVectorLayer)
        self.mLayer = layer
        self.mLayer.selectionChanged.connect(self.selectionChanged)

    def layer(self):
        return self.mLayer

    def deselect(self, ids):

        if len(ids) > 0:
            selected = [id for id in self.selectedFeatureIds() if id not in ids]
            self.mLayer.deselect(ids)

            self.selectionChanged.emit(selected, ids, True)

    def select(self, ids):
        self.mLayer.select(ids)

    def selectFeatures(self, selection, command):

        super(TemporalProfileFeatureSelectionManager, self).selectF
        s = ""
    def selectedFeatureCount(self):
        return self.mLayer.selectedFeatureCount()

    def selectedFeatureIds(self):
        return self.mLayer.selectedFeatureIds()
    def setSelectedFeatures(self, ids):
        self.mLayer.selectByIds(ids)


class TemporalProfileTableView(QgsAttributeTableView):

    def __init__(self, parent=None):
        super(TemporalProfileTableView, self).__init__(parent)


        #self.setSelectionBehavior(QAbstractItemView.SelectRows)
        #self.setSelectionMode(QAbstractItemView.SingleSelection)
        self.horizontalHeader().setSectionsMovable(True)
        self.willShowContextMenu.connect(self.onWillShowContextMenu)
        self.horizontalHeader().setSectionResizeMode(QHeaderView.Interactive)


        self.mSelectionManager = None

    def setModel(self, filterModel):

        super(TemporalProfileTableView, self).setModel(filterModel)


        self.mSelectionManager = TemporalProfileFeatureSelectionManager(self.model().layer())
        self.setFeatureSelectionManager(self.mSelectionManager)
        #self.selectionModel().selectionChanged.connect(self.onSelectionChanged)
        self.mContextMenuActions = []
    def setContextMenuActions(self, actions:list):
        self.mContextMenuActions = actions

    #def contextMenuEvent(self, event):
    def onWillShowContextMenu(self, menu, index):
        assert isinstance(menu, QMenu)
        assert isinstance(index, QModelIndex)

        featureIDs = self.temporalProfileLayer().selectedFeatureIds()

        if len(featureIDs) == 0 and index.isValid():
            if isinstance(self.model(), QgsAttributeTableFilterModel):
                index = self.model().mapToSource(index)
                if index.isValid():
                    featureIDs.append(self.model().sourceModel().feature(index).id())
            elif isinstance(self.model(), QgsAttributeTableFilterModel):
                featureIDs.append(self.model().feature(index).id())

        for a in self.mContextMenuActions:
            menu.addAction(a)

        for a in self.actions():
            menu.addAction(a)

    def temporalProfileLayer(self):
        return self.model().layer()



    def fidsToIndices(self, fids):
        """
        Converts feature ids into FilterModel QModelIndices
        :param fids: [list-of-int]
        :return:
        """
        if isinstance(fids, int):
            fids = [fids]
        assert isinstance(fids, list)
        fmodel = self.model()
        indices = [fmodel.fidToIndex(id) for id in fids]
        return [fmodel.index(idx.row(), 0) for idx in indices]

    def onRemoveFIDs(self, fids):

        layer = self.temporalProfileLayer()
        assert isinstance(layer, TemporalProfileLayer)
        b = layer.isEditable()
        layer.startEditing()
        layer.deleteFeatures(fids)
        layer.saveEdits(leaveEditable=b)


    def dropEvent(self, event):
        assert isinstance(event, QDropEvent)
        mimeData = event.mimeData()

        if self.model().rowCount() == 0:
            index = self.model().createIndex(0,0)
            index = self.indexAt(event.pos())

        #if mimeData.hasFormat(mimedata.MDF_SPECTRALLIBRARY):
         #   self.model().dropMimeData(mimeData, event.dropAction(), index.row(), index.column(), index.parent())
          #  event.accept()





    def dragEnterEvent(self, event):
        assert isinstance(event, QDragEnterEvent)
        #if event.mimeData().hasFormat(mimedata.MDF_SPECTRALLIBRARY):
        #    event.accept()

    def dragMoveEvent(self, event):
        assert isinstance(event, QDragMoveEvent)
        #if event.mimeData().hasFormat(mimedata.MDF_SPECTRALLIBRARY):
        #    event.accept()
        s = ""