Skip to content
Snippets Groups Projects
spectrallibraries.py 99.7 KiB
Newer Older
  • Learn to ignore specific revisions
  • # -*- coding: utf-8 -*-
    # noinspection PyPep8Naming
    """
    ***************************************************************************
        spectrallibraries.py
    
        Spectral Profiles and Libraries for a GUI context.
        ---------------------
        Date                 : Juli 2017
        Copyright            : (C) 2017 by Benjamin Jakimow
        Email                : benjamin.jakimow@geo.hu-berlin.de
    ***************************************************************************
    *                                                                         *
    *   This program is free software; you can redistribute it and/or modify  *
    *   it under the terms of the GNU General Public License as published by  *
    *   the Free Software Foundation; either version 2 of the License, or     *
    *   (at your option) any later version.                                   *
    *                                                                         *
    ***************************************************************************
    """
    
    #see http://python-future.org/str_literals.html for str issue discussion
    
    import os, re, tempfile, pickle, copy, shutil, locale, uuid, csv, io
    
    from collections import OrderedDict
    from qgis.core import *
    from qgis.gui import *
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    from qgis.PyQt.QtCore import *
    from qgis.PyQt.QtGui import *
    from qgis.PyQt.QtWidgets import *
    
    from qgis.core import QgsField, QgsFields, QgsFeature, QgsMapLayer, QgsVectorLayer, QgsConditionalStyle
    from qgis.gui import QgsMapCanvas, QgsDockWidget
    from pyqtgraph.widgets.PlotWidget import PlotWidget
    from pyqtgraph.graphicsItems.PlotDataItem import PlotDataItem
    
    from pyqtgraph.graphicsItems.PlotItem import PlotItem
    
    from osgeo import gdal, gdal_array, ogr
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    from timeseriesviewer.utils import *
    
    from timeseriesviewer.virtualrasters import describeRawFile
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    from timeseriesviewer.models import *
    
    from timeseriesviewer.plotstyling import PlotStyle, PlotStyleDialog, MARKERSYMBOLS2QGIS_SYMBOLS
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    import timeseriesviewer.mimedata as mimedata
    
    FILTERS = 'ENVI Spectral Library + CSV (*.esl *.sli);;CSV Table (*.csv);;ESRI Shapefile (*.shp)'
    
    PICKLE_PROTOCOL = pickle.HIGHEST_PROTOCOL
    HIDDEN_ATTRIBUTE_PREFIX = '__serialized__'
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    CURRENT_SPECTRUM_STYLE = PlotStyle()
    CURRENT_SPECTRUM_STYLE.linePen.setStyle(Qt.SolidLine)
    CURRENT_SPECTRUM_STYLE.linePen.setColor(Qt.green)
    
    
    
    DEFAULT_SPECTRUM_STYLE = PlotStyle()
    DEFAULT_SPECTRUM_STYLE.linePen.setStyle(Qt.SolidLine)
    DEFAULT_SPECTRUM_STYLE.linePen.setColor(Qt.white)
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    #CURRENT_SPECTRUM_STYLE.linePen
    #pdi.setPen(fn.mkPen(QColor('green'), width=3))
    
    def gdalDataset(pathOrDataset, eAccess=gdal.GA_ReadOnly):
        """
    
        :param pathOrDataset: path or gdal.Dataset
        :return: gdal.Dataset
        """
    
        if isinstance(pathOrDataset, QgsRasterLayer):
            return gdalDataset(pathOrDataset.source())
    
        if not isinstance(pathOrDataset, gdal.Dataset):
            pathOrDataset = gdal.Open(pathOrDataset, eAccess)
    
        assert isinstance(pathOrDataset, gdal.Dataset)
    
        return pathOrDataset
    
    
    
    def findTypeFromString(value:str):
        """
        Returns a fitting basic data type of a string value
        :param value: string
        :return: type
        """
        for t in (int, float):
            try:
                _ = t(value)
            except ValueError:
                continue
            return t
    
        #every values can be converted into a string
        return str
    
    def toType(t, arg, empty2None=True):
        """
        Converts lists or single values into type t.
    
        Examples:
            toType(int, '42') == 42,
            toType(float, ['23.42', '123.4']) == [23.42, 123.4]
    
        :param t: type
        :param arg: value to convert
        :param empty2None: returns None in case arg is an emptry value (None, '', NoneType, ...)
        :return: arg as type t (or None)
        """
        if isinstance(arg, list):
            return [toType(t, a) for a in arg]
        else:
    
            if empty2None and arg in EMPTY_VALUES:
                return None
            else:
                return t(arg)
    
    
    
    @qgsfunction(0, "Spectral Libraries")
    def plotStyleSymbolFillColor(values, feature, parent):
        if isinstance(feature, QgsFeature):
            i = feature.fieldNameIndex(HIDDEN_ATTRIBUTE_PREFIX+'style')
            if i >= 0:
                style = pickle.loads(feature.attribute(i))
                if isinstance(style, PlotStyle):
                    r,g,b,a = style.markerBrush.color().getRgb()
                    return '{},{},{},{}'.format(r,g,b, a)
    
        return None
    
    @qgsfunction(0, "Spectral Libraries")
    def plotStyleSymbol(values, feature, parent):
        if isinstance(feature, QgsFeature):
            i = feature.fieldNameIndex(HIDDEN_ATTRIBUTE_PREFIX+'style')
            if i >= 0:
                style = pickle.loads(feature.attribute(i))
                if isinstance(style, PlotStyle):
                    symbol = style.markerSymbol
    
                    qgisSymbolString =  MARKERSYMBOLS2QGIS_SYMBOLS.get(symbol)
                    if isinstance(qgisSymbolString, str):
                        return qgisSymbolString
    
        return None
    
    @qgsfunction(0, "Spectral Libraries")
    def plotStyleSymbolSize(values, feature, parent):
        if isinstance(feature, QgsFeature):
            i = feature.fieldNameIndex(HIDDEN_ATTRIBUTE_PREFIX+'style')
            if i >= 0:
                style = pickle.loads(feature.attribute(i))
                if isinstance(style, PlotStyle):
                    return style.markerSize
        return None
    
    
    QgsExpression.registerFunction(plotStyleSymbolFillColor)
    QgsExpression.registerFunction(plotStyleSymbol)
    QgsExpression.registerFunction(plotStyleSymbolSize)
    
    
    
    #Lookup table for ENVI IDL DataTypes to GDAL Data Types
    LUT_IDL2GDAL = {1:gdal.GDT_Byte,
                    12:gdal.GDT_UInt16,
                    2:gdal.GDT_Int16,
                    13:gdal.GDT_UInt32,
                    3:gdal.GDT_Int32,
                    4:gdal.GDT_Float32,
                    5:gdal.GDT_Float64,
                    #:gdal.GDT_CInt16,
                    #8:gdal.GDT_CInt32,
                    6:gdal.GDT_CFloat32,
                    9:gdal.GDT_CFloat64}
    
    
    def createStandardFields():
        fields = QgsFields()
    
        """
        Parameters
        name Field name type Field variant type, currently supported: String / Int / Double 
        typeName Field type (e.g., char, varchar, text, int, serial, double). Field types are usually unique to the source and are stored exactly as returned from the data store. 
        len Field length 
        prec Field precision. Usually decimal places but may also be used in conjunction with other fields types (e.g., variable character fields) 
        comment Comment for the field 
        subType If the field is a collection, its element's type. When all the elements don't need to have the same type, leave this to QVariant::Invalid. 
        """
        fields.append(createQgsField('name', ''))
        fields.append(createQgsField('px_x', 0))
        fields.append(createQgsField('px_y', 0))
        fields.append(createQgsField('x_unit', ''))
        fields.append(createQgsField('y_unit', ''))
        fields.append(createQgsField('source', ''))
        fields.append(createQgsField(HIDDEN_ATTRIBUTE_PREFIX + 'xvalues', ''))
        fields.append(createQgsField(HIDDEN_ATTRIBUTE_PREFIX + 'yvalues', ''))
        fields.append(createQgsField(HIDDEN_ATTRIBUTE_PREFIX + 'style', ''))
    
    
        """
        fields.append(QgsField('name', QVariant.String,'varchar', 25))
        fields.append(QgsField('px_x', QVariant.Int, 'int'))
        fields.append(QgsField('px_y', QVariant.Int, 'int'))
        fields.append(QgsField('x_unit', QVariant.String, 'varchar', 5))
        fields.append(QgsField('y_unit', QVariant.String, 'varchar', 5))
        fields.append(QgsField('source', QVariant.String, 'varchar', 5))
        """
        return fields
    
    
    def value2str(value, sep=' '):
        if isinstance(value, list):
    
            value = sep.join([value2str(v, sep=sep) for v in value])
        elif isinstance(value, np.ndarray):
    
            value = value2str(value.astype(list), sep=sep)
    
            value = str(value)
    
    
    class AddAttributeDialog(QDialog):
    
        def __init__(self, layer, parent=None):
            assert isinstance(layer, QgsVectorLayer)
            super(AddAttributeDialog, self).__init__(parent)
    
            assert isinstance(layer, QgsVectorLayer)
            self.mLayer = layer
    
            self.setWindowTitle('Add Field')
            l = QGridLayout()
    
            self.tbName = QLineEdit('Name')
            self.tbName.setPlaceholderText('Name')
            self.tbName.textChanged.connect(self.validate)
    
            l.addWidget(QLabel('Name'), 0,0)
            l.addWidget(self.tbName, 0, 1)
    
            self.tbComment = QLineEdit()
            self.tbComment.setPlaceholderText('Comment')
            l.addWidget(QLabel('Comment'), 1, 0)
            l.addWidget(self.tbComment, 1, 1)
    
            self.cbType = QComboBox()
            self.typeModel = OptionListModel()
    
            for ntype in self.mLayer.dataProvider().nativeTypes():
                assert isinstance(ntype, QgsVectorDataProvider.NativeType)
                o = Option(ntype,name=ntype.mTypeName, tooltip=ntype.mTypeDesc)
                self.typeModel.addOption(o)
            self.cbType.setModel(self.typeModel)
            self.cbType.currentIndexChanged.connect(self.onTypeChanged)
            l.addWidget(QLabel('Type'), 2, 0)
            l.addWidget(self.cbType, 2, 1)
    
            self.sbLength = QSpinBox()
            self.sbLength.setRange(0, 99)
            self.sbLength.valueChanged.connect(lambda : self.setPrecisionMinMax())
            self.lengthLabel = QLabel('Length')
            l.addWidget(self.lengthLabel, 3, 0)
            l.addWidget(self.sbLength, 3, 1)
    
            self.sbPrecision = QSpinBox()
            self.sbPrecision.setRange(0, 99)
            self.precisionLabel = QLabel('Precision')
            l.addWidget(self.precisionLabel, 4, 0)
            l.addWidget(self.sbPrecision, 4, 1)
    
            self.tbValidationInfo = QLabel()
            self.tbValidationInfo.setStyleSheet("QLabel { color : red}")
            l.addWidget(self.tbValidationInfo, 5, 0, 1, 2)
    
    
            self.buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
            self.buttons.button(QDialogButtonBox.Ok).clicked.connect(self.accept)
            self.buttons.button(QDialogButtonBox.Cancel).clicked.connect(self.reject)
            l.addWidget(self.buttons, 6, 1)
            self.setLayout(l)
    
            self.mLayer = layer
    
            self.onTypeChanged()
    
        def accept(self):
    
            msg = self.validate()
    
            if len(msg) > 0:
                QMessageBox.warning(self, "Add Field", msg)
            else:
                super(AddAttributeDialog, self).accept()
    
        def field(self):
            """
            Returns the new QgsField
            :return:
            """
            ntype = self.currentNativeType()
            return QgsField(name=self.tbName.text(),
                            type=QVariant(ntype.mType).type(),
                            typeName=ntype.mTypeName,
                            len=self.sbLength.value(),
                            prec=self.sbPrecision.value(),
                            comment=self.tbComment.text())
    
    
    
    
        def currentNativeType(self):
            return self.cbType.currentData().value()
    
        def onTypeChanged(self, *args):
            ntype = self.currentNativeType()
            vMin , vMax = ntype.mMinLen, ntype.mMaxLen
            assert isinstance(ntype, QgsVectorDataProvider.NativeType)
    
            isVisible = vMin < vMax
            self.sbLength.setVisible(isVisible)
            self.lengthLabel.setVisible(isVisible)
            self.setSpinBoxMinMax(self.sbLength, vMin , vMax)
            self.setPrecisionMinMax()
    
        def setPrecisionMinMax(self):
            ntype = self.currentNativeType()
            vMin, vMax = ntype.mMinPrec, ntype.mMaxPrec
            isVisible = vMin < vMax
            self.sbPrecision.setVisible(isVisible)
            self.precisionLabel.setVisible(isVisible)
    
            vMax = max(ntype.mMinPrec, min(ntype.mMaxPrec, self.sbLength.value()))
            self.setSpinBoxMinMax(self.sbPrecision, vMin, vMax)
    
        def setSpinBoxMinMax(self, sb, vMin, vMax):
            assert isinstance(sb, QSpinBox)
            value = sb.value()
            sb.setRange(vMin, vMax)
    
            if value > vMax:
                sb.setValue(vMax)
            elif value < vMin:
                sb.setValue(vMin)
    
    
        def validate(self):
    
            msg = []
            name = self.tbName.text()
            if name in self.mLayer.fields().names():
                msg.append('Field name "{}" already exists.'.format(name))
            elif name == '':
                msg.append('Missing field name')
            elif name == 'shape':
                msg.append('Field name "{}" already reserved.'.format(name))
    
            msg = '\n'.join(msg)
            self.buttons.button(QDialogButtonBox.Ok).setEnabled(len(msg) == 0)
    
            self.tbValidationInfo.setText(msg)
    
            return msg
    
    
    
    
    
    class SpectralLibraryTableFilterModel(QgsAttributeTableFilterModel):
    
        def __init__(self, sourceModel, parent=None):
    
            dummyCanvas = QgsMapCanvas()
            dummyCanvas.setDestinationCrs(SpectralProfile.crs)
            dummyCanvas.setExtent(QgsRectangle(-180,-90,180,90))
    
            super(SpectralLibraryTableFilterModel, self).__init__(dummyCanvas, sourceModel, parent=parent)
    
            self.mDummyCanvas = dummyCanvas
    
            #self.setSelectedOnTop(True)
    
    class SpectralLibraryTableView(QgsAttributeTableView):
    
    
        def __init__(self, parent=None):
            super(SpectralLibraryTableView, self).__init__(parent)
    
    
    
            #self.setSelectionBehavior(QAbstractItemView.SelectRows)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            #self.setSelectionMode(QAbstractItemView.SingleSelection)
    
            self.horizontalHeader().setSectionsMovable(True)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            self.willShowContextMenu.connect(self.onWillShowContextMenu)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            self.horizontalHeader().setSectionResizeMode(QHeaderView.Interactive)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            self.mSelectionManager = None
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def setModel(self, filterModel):
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            super(SpectralLibraryTableView, self).setModel(filterModel)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            self.mSelectionManager = SpectralLibraryFeatureSelectionManager(self.model().layer())
            self.setFeatureSelectionManager(self.mSelectionManager)
    
            #self.selectionModel().selectionChanged.connect(self.onSelectionChanged)
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        #def contextMenuEvent(self, event):
        def onWillShowContextMenu(self, menu, index):
            assert isinstance(menu, QMenu)
            assert isinstance(index, QModelIndex)
    
    
            featureIDs = self.spectralLibrary().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())
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            if len(featureIDs) > 0:
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                a = m.addAction("Values")
                a.triggered.connect(lambda b, ids=featureIDs, mode=ClipboardIO.WritingModes.VALUES: self.onCopy2Clipboard(ids, mode))
                a = m.addAction("Attributes")
                a.triggered.connect(lambda b, ids=featureIDs, mode=ClipboardIO.WritingModes.ATTRIBUTES: self.onCopy2Clipboard(ids, mode))
                a = m.addAction("Values + Attributes")
                a.triggered.connect(lambda b, ids=featureIDs, mode=ClipboardIO.WritingModes.ALL: self.onCopy2Clipboard(ids, mode))
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            a.triggered.connect(lambda b, ids=featureIDs : self.onSaveToFile(ids))
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            a.triggered.connect(lambda b, ids=featureIDs : self.onSetStyle(ids))
    
            a = menu.addAction('Check')
            a.triggered.connect(lambda : self.setCheckState(featureIDs, Qt.Checked))
            a = menu.addAction('Uncheck')
            a.triggered.connect(lambda: self.setCheckState(featureIDs, Qt.Unchecked))
    
            menu.addSeparator()
            for a in self.actions():
                menu.addAction(a)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def spectralLibrary(self):
            return self.model().layer()
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def onCopy2Clipboard(self, fids, mode):
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            assert mode in ClipboardIO.WritingModes().modes()
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            speclib = self.spectralLibrary()
            assert isinstance(speclib, SpectralLibrary)
            speclib = speclib.speclibFromFeatureIDs(fids)
            ClipboardIO.write(speclib, mode=mode)
    
            s = ""
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def onSaveToFile(self, fids):
            speclib = self.spectralLibrary()
            assert isinstance(speclib, SpectralLibrary)
            speclib.getFeatures(fids)
            speclib.exportProfiles()
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            """
    
            Converts feature ids into FilterModel QModelIndices
            :param fids: [list-of-int]
            :return:
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            """
    
            if isinstance(fids, int):
                fids = [fids]
            assert isinstance(fids, list)
    
            fmodel = self.model()
    
            indices = [fmodel.fidToIndex(id) for id in fids]
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            return [fmodel.index(idx.row(), 0) for idx in indices]
    
        def onRemoveFIDs(self, fids):
    
            speclib = self.spectralLibrary()
            assert isinstance(speclib, SpectralLibrary)
    
    
        def onSetStyle(self, ids):
    
            if len(ids) == 0:
                return
    
            speclib = self.spectralLibrary()
            assert isinstance(speclib, SpectralLibrary)
    
            profiles = speclib.profiles(ids)
            refProfile = profiles[0]
            styleDefault = refProfile.style()
            refStyle = PlotStyleDialog.getPlotStyle(plotStyle=styleDefault)
    
            if isinstance(refStyle, PlotStyle):
                refProfile.setStyle(refStyle)
    
    
            iStyle = speclib.fields().indexFromName(HIDDEN_ATTRIBUTE_PREFIX+'style')
            assert iStyle >= 0
    
    
            if isinstance(refStyle, PlotStyle):
    
    
                speclib.startEditing()
                for f in profiles:
                    assert isinstance(f, SpectralProfile)
                    oldStyle = f.style()
                    refStyle.setVisibility(oldStyle.isVisible())
                    speclib.changeAttributeValue(f.id(), iStyle, pickle.dumps(refStyle), f.attributes()[iStyle])
    
            iStyle = speclib.fields().indexFromName(HIDDEN_ATTRIBUTE_PREFIX + 'style')
    
            setVisible = checkState == Qt.Checked
    
            for p in profiles:
                assert isinstance(p, SpectralProfile)
                oldStyle = p.style()
                assert isinstance(oldStyle, PlotStyle)
    
                if oldStyle.isVisible() != setVisible:
                    newStyle = p.style()
                    newStyle.setVisibility(setVisible)
                    p.setStyle(newStyle)
                    speclib.changeAttributeValue(p.id(), iStyle, p.attributes()[iStyle], oldStyle)
    
    
    
        def dropEvent(self, event):
            assert isinstance(event, QDropEvent)
            mimeData = event.mimeData()
    
            if self.model().rowCount() == 0:
                index = self.model().createIndex(0,0)
            else:
                index = self.indexAt(event.pos())
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            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)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            if event.mimeData().hasFormat(mimedata.MDF_SPECTRALLIBRARY):
    
                event.accept()
    
        def dragMoveEvent(self, event):
            assert isinstance(event, QDragMoveEvent)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            if event.mimeData().hasFormat(mimedata.MDF_SPECTRALLIBRARY):
    
                event.accept()
            s = ""
    
    
        def mimeTypes(self):
            pass
    
    """
    class SpectralProfileMapTool(QgsMapToolEmitPoint):
    
        sigProfileRequest = pyqtSignal(SpatialPoint, QgsMapCanvas)
    
        def __init__(self, canvas, showCrosshair=True):
            self.mShowCrosshair = showCrosshair
            self.mCanvas = canvas
            QgsMapToolEmitPoint.__init__(self, self.mCanvas)
            self.marker = QgsVertexMarker(self.mCanvas)
    
            self.rubberband = QgsRubberBand(self.mCanvas, QgsWkbTypes.PolygonGeometry)
    
    
            color = QColor('red')
    
            self.rubberband.setLineStyle(Qt.SolidLine)
            self.rubberband.setColor(color)
            self.rubberband.setWidth(2)
    
            self.marker.setColor(color)
            self.marker.setPenWidth(3)
            self.marker.setIconSize(5)
            self.marker.setIconType(QgsVertexMarker.ICON_CROSS)  # or ICON_CROSS, ICON_X
    
        def canvasPressEvent(self, e):
            geoPoint = self.toMapCoordinates(e.pos())
            self.marker.setCenter(geoPoint)
            #self.marker.show()
    
        def setStyle(self, color=None, brushStyle=None, fillColor=None, lineStyle=None):
            if color:
                self.rubberband.setColor(color)
            if brushStyle:
                self.rubberband.setBrushStyle(brushStyle)
            if fillColor:
                self.rubberband.setFillColor(fillColor)
            if lineStyle:
                self.rubberband.setLineStyle(lineStyle)
    
        def canvasReleaseEvent(self, e):
    
            pixelPoint = e.pixelPoint()
    
            crs = self.mCanvas.mapSettings().destinationCrs()
            self.marker.hide()
            geoPoint = self.toMapCoordinates(pixelPoint)
            if self.mShowCrosshair:
                #show a temporary crosshair
                ext = SpatialExtent.fromMapCanvas(self.mCanvas)
                cen = geoPoint
                geom = QgsGeometry()
    
                geom.addPart([QgsPointXY(ext.upperLeftPt().x(),cen.y()), QgsPointXY(ext.lowerRightPt().x(), cen.y())],
    
                              Qgis.Line)
    
                geom.addPart([QgsPointXY(cen.x(), ext.upperLeftPt().y()), QgsPointXY(cen.x(), ext.lowerRightPt().y())],
    
                              Qgis.Line)
    
                self.rubberband.addGeometry(geom, None)
                self.rubberband.show()
                #remove crosshair after 0.1 sec
                QTimer.singleShot(100, self.hideRubberband)
    
            self.sigProfileRequest.emit(SpatialPoint(crs, geoPoint), self.mCanvas)
    
        def hideRubberband(self):
            self.rubberband.reset()
    
    """
    
    
    class SpectralProfilePlotDataItem(PlotDataItem):
    
        def __init__(self, spectralProfile):
            assert isinstance(spectralProfile, SpectralProfile)
            super(SpectralProfilePlotDataItem, self).__init__()
            self.mProfile = spectralProfile
    
            self.setData(x=spectralProfile.xValues(), y=spectralProfile.yValues())
            self.setStyle(self.mProfile.style())
    
        def setClickable(self, b, width=None):
            assert isinstance(b, bool)
            self.curve.setClickable(b, width=width)
    
    
    
        def setStyle(self, style):
            assert isinstance(style, PlotStyle)
            self.setVisible(style.isVisible())
    
            self.setSymbol(style.markerSymbol)
            self.setSymbolBrush(style.markerBrush)
            self.setSymbolSize(style.markerSize)
            self.setSymbolPen(style.markerPen)
            self.setPen(style.linePen)
    
    
    
        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):
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            from pyqtgraph.functions import mkPen
            pen = mkPen(self.opts['pen'])
    
            assert isinstance(pen, QPen)
            pen.setWidth(width)
            self.setPen(pen)
    
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    class SpectralProfile(QgsFeature):
    
    
        crs = QgsCoordinateReferenceSystem('EPSG:4326')
    
    
        @staticmethod
        def fromMapCanvas(mapCanvas, position):
            """
            Returns a list of Spectral Profiles the raster layers in QgsMapCanvas mapCanvas.
            :param mapCanvas:
            :param position:
            """
            assert isinstance(mapCanvas, QgsMapCanvas)
    
    
            from timeseriesviewer.mapcanvas import MapCanvas
            if isinstance(mapCanvas, MapCanvas):
                sources = mapCanvas.layerModel().rasterLayerInfos()
                sources = [s.mSrc for s in sources]
            else:
                layers = [l for l in mapCanvas.layers() if isinstance(l, QgsRasterLayer)]
                sources = [l.source() for l in layers]
    
            return SpectralProfile.fromRasterSources(sources, position)
    
        @staticmethod
        def fromRasterSources(sources, position):
            """
            Returns a list of Spectral Profiles
            :param sources: list-of-raster-sources, e.g. file paths, gdal.Datasets, QgsRasterLayers
            :param position:
            :return:
            """
            profiles = [SpectralProfile.fromRasterSource(s, position) for s in sources]
            return [p for p in profiles if isinstance(p, SpectralProfile)]
    
    
        @staticmethod
        def fromRasterSource(source, position):
            """
            Returns the Spectral Profiles from source at position `position`
            :param source: path or gdal.Dataset
            :param position:
            :return: SpectralProfile
            """
    
            ds = gdalDataset(source)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
            files = ds.GetFileList()
            if len(files) > 0:
                baseName = os.path.basename(files[0])
            else:
                baseName = 'Spectrum'
    
            crs = QgsCoordinateReferenceSystem(ds.GetProjection())
            gt = ds.GetGeoTransform()
    
            if isinstance(position, QPoint):
                px = position
            elif isinstance(position, SpatialPoint):
                px = geo2px(position.toCrs(crs), gt)
    
            elif isinstance(position, QgsPointXY):
    
                px = geo2px(position, ds.GetGeoTransform())
            else:
                raise Exception('Unsupported type of argument "position" {}'.format('{}'.format(position)))
            #check out-of-raster
            if px.x() < 0 or px.y() < 0: return None
            if px.x() > ds.RasterXSize - 1 or px.y() > ds.RasterYSize - 1: return None
    
    
            values = ds.ReadAsArray(px.x(), px.y(), 1, 1)
    
            values = values.flatten()
            for b in range(ds.RasterCount):
                band = ds.GetRasterBand(b+1)
                nodata = band.GetNoDataValue()
                if nodata and values[b] == nodata:
                    return None
    
    
            wl = ds.GetMetadataItem(str('wavelength'), str('ENVI'))
            wlu = ds.GetMetadataItem(str('wavelength_units'), str('ENVI'))
    
            if wl is not None and len(wl) > 0:
                wl = re.sub(r'[ {}]','', wl).split(',')
                wl = [float(w) for w in wl]
    
            profile = SpectralProfile()
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            profile.setName('{} x{} y{}'.format(baseName, px.x(), px.y()))
    
            #profile.setValues(values, valuePositions=wl, valuePositionUnit=wlu)
            profile.setYValues(values)
            if wl is not None:
                profile.setXValues(wl, unit=wlu)
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            profile.setCoordinates(px=px, pt=SpatialPoint(crs, px2geo(px, gt)))
    
            profile.setSource('{}'.format(ds.GetFileList()[0]))
            return profile
    
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
    
        @staticmethod
        def fromSpecLibFeature(feature):
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            assert isinstance(feature, QgsFeature)
    
            sp = SpectralProfile(fields=feature.fields())
    
            sp.setId(feature.id())
    
            sp.setAttributes(feature.attributes())
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            sp.setGeometry(feature.geometry())
    
        XVALUES_FIELD = HIDDEN_ATTRIBUTE_PREFIX+'xvalues'
        YVALUES_FIELD = HIDDEN_ATTRIBUTE_PREFIX + 'yvalues'
        STYLE_FIELD = HIDDEN_ATTRIBUTE_PREFIX + 'style'
    
    
        def __init__(self, parent=None, fields=None, xUnit='index', yUnit=None):
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            if fields is None:
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
            #QgsFeature.__init__(self, fields)
            #QObject.__init__(self)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            super(SpectralProfile, self).__init__(fields)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            fields = self.fields()
            assert isinstance(fields, QgsFields)
    
    
            self.setXUnit(xUnit)
            self.setYUnit(yUnit)
    
        def fieldNames(self):
            return self.fields().names()
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def setName(self, name:str):
            if name != self.name():
                self.setAttribute('name', name)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def setSource(self, uri: str):
            self.setAttribute('source', uri)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
        def setCoordinates(self, px=None, pt=None):
    
            if isinstance(px, QPoint):
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
                self.setAttribute('px_x', px.x())
                self.setAttribute('px_y', px.y())
    
            if isinstance(pt, SpatialPoint):
                sp = pt.toCrs(SpectralProfile.crs)
                self.setGeometry(QgsGeometry.fromPointXY(sp))
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            x = self.attribute('px_x')
            y = self.attribute('px_y')
            if x == None or y == None:
                return None
            return QPoint(x, y)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            return self.geometry()
    
    
        def isValid(self):
            return len(self.mValues) > 0 and self.mValueUnit is not None
    
    
    
        def setXValues(self, values, unit=None):
            if isinstance(values, np.ndarray):
                values = values.tolist()
            assert isinstance(values, list)
    
            self.setMetadata(SpectralProfile.XVALUES_FIELD, values)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
            if isinstance(unit, str):
                self.setMetadata('x_unit', unit)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
        def setYValues(self, values, unit=None):
            if isinstance(values, np.ndarray):
                values = values.tolist()
            assert isinstance(values, list)
    
            self.setMetadata(SpectralProfile.YVALUES_FIELD, values)
    
            if isinstance(unit, str):
                self.setMetadata('y_unit', unit)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
            if self.xValues() is None:
                self.setXValues(list(range(len(values))), unit='index')
    
        def style(self):
            return self.metadata(SpectralProfile.STYLE_FIELD)
    
        def setStyle(self, style):
            assert isinstance(style, PlotStyle)
            self.setMetadata(SpectralProfile.STYLE_FIELD, style)
    
    
        def updateMetadata(self, metaData):
    
            if isinstance(metaData, dict):
                for key, value in metaData.items():
                    self.setMetadata(key, value)
    
        def removeField(self, name):
            fields = self.fields()
            values = self.attributes()
            i = self.fieldNameIndex(name)
            if i >= 0:
                fields.remove(i)
                values.pop(i)
                self.setFields(fields)
                self.setAttributes(values)
    
        def setMetadata(self, key: str, value, addMissingFields=False):
            """
    
            :param key: Name of metadata field
            :param value: value to add. Need to be of type None, str, int or float.
            :param addMissingFields: Set on True to add missing fields (in case value is not None)
            :return:
            """
            i = self.fieldNameIndex(key)
    
            if key.startswith('__serialized__'):
                if value is not None:
                    value = pickle.dumps(value)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            if i < 0:
    
                if value is not None and addMissingFields:
    
                    fields = self.fields()
                    values = self.attributes()
                    if key.startswith('__serialized__'):
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                        fields.append(createQgsField(key, ''))
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                        fields.append(createQgsField(key, value))
    
                    values.append(value)
                    self.setFields(fields)
                    self.setAttributes(values)
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
                return False
            else:
                return self.setAttribute(key, value)
    
        def metadata(self, key: str, default=None):
    
    
            assert isinstance(key, str)
    
            i = self.fieldNameIndex(key)
            if i < 0:
                return None
    
            v = self.attribute(i)
            if v == QVariant(None):
                v = None
    
            if key.startswith('__serialized__') and v != None:
                v = pickle.loads(v)
    
    
            return default if v is None else v
    
        def xValues(self):
    
            return self.metadata(SpectralProfile.XVALUES_FIELD)
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
        def yValues(self):
    
            return self.metadata(SpectralProfile.YVALUES_FIELD)
    
    
        def setXUnit(self, unit : str='index'):
            self.setMetadata('x_unit', unit)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            return self.metadata('x_unit', 'index')
    
    
        def setYUnit(self, unit:str=None):
            self.setMetadata('y_unit', unit)
    
        def yUnit(self):
            return self.metadata('y_unit', None)
    
        def copyFieldSubset(self, fields):
    
            sp = SpectralProfile(fields=fields)
    
            fieldsInCommon = [field for field in sp.fields() if field in self.fields()]
    
            sp.setGeometry(self.geometry())
            sp.setId(self.id())
    
            for field in fieldsInCommon:
                assert isinstance(field, QgsField)
                i = sp.fieldNameIndex(field.name())
                sp.setAttribute(i, self.attribute(field.name()))
            return sp
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
            sp = SpectralProfile(fields=self.fields())
            sp.setAttributes(self.attributes())
            return sp
    
    
        def plot(self):
            """
            Plots this profile to an new PyQtGraph window
            :return:
            """
            import pyqtgraph as pg
    
            pi = SpectralProfilePlotDataItem(self)
            pi.setClickable(True)
            pw = pg.plot( title=self.name())