From a553923b5d07ad8059639a39313f03b1f5f2d4b0 Mon Sep 17 00:00:00 2001
From: "Benjamin Jakimow benjamin.jakimow@geo.hu-berlin.de"
 <benjamin.jakimow@geo.hu-berlin.de>
Date: Thu, 29 Oct 2020 21:29:42 +0100
Subject: [PATCH] updated QPS

Signed-off-by: Benjamin Jakimow benjamin.jakimow@geo.hu-berlin.de <benjamin.jakimow@geo.hu-berlin.de>
---
 .../externals/qps/cursorlocationvalue.py      | 114 +-
 .../externals/qps/layerconfigwidgets/core.py  |  26 +-
 .../qps/layerconfigwidgets/gdalmetadata.py    | 164 ++-
 .../layerconfigwidgets/vectorlayerfields.py   |  68 +-
 .../externals/qps/layerproperties.py          |  31 +-
 eotimeseriesviewer/externals/qps/maptools.py  |   2 +-
 eotimeseriesviewer/externals/qps/models.py    | 667 +++++++-----
 .../externals/qps/plotstyling/plotstyling.py  |   6 +-
 .../externals/qps/simplewidgets.py            | 133 +++
 .../externals/qps/speclib/__init__.py         |  12 +-
 .../externals/qps/speclib/core.py             | 276 +++--
 .../qps/speclib/function_help/format_py.json  |  12 +
 .../speclib/function_help/spectraldata.json   |   8 +
 .../speclib/function_help/spectralmath.json   |   8 +
 .../externals/qps/speclib/gui.py              | 979 ++++++++++--------
 .../externals/qps/speclib/io/vectorsources.py |   8 +-
 .../externals/qps/speclib/qgsfunctions.py     | 277 ++++-
 .../spectralprofileeditorconfigwidget.ui      |  65 +-
 .../speclib/spectralprofileeditorwidget.ui    | 246 +++--
 .../externals/qps/subdatasets.py              |  19 +-
 eotimeseriesviewer/externals/qps/testing.py   |  11 +-
 .../qps/ui/cursorlocationinfodock.ui          |   6 +-
 .../qps/ui/gdalmetadatamodelwidget.ui         |  28 +-
 eotimeseriesviewer/externals/qps/unitmodel.py |   4 +
 eotimeseriesviewer/externals/qps/utils.py     |  51 +-
 .../externals/qps/vectorlayertools.py         |   6 +-
 26 files changed, 2085 insertions(+), 1142 deletions(-)
 create mode 100644 eotimeseriesviewer/externals/qps/simplewidgets.py
 create mode 100644 eotimeseriesviewer/externals/qps/speclib/function_help/format_py.json
 create mode 100644 eotimeseriesviewer/externals/qps/speclib/function_help/spectraldata.json
 create mode 100644 eotimeseriesviewer/externals/qps/speclib/function_help/spectralmath.json

diff --git a/eotimeseriesviewer/externals/qps/cursorlocationvalue.py b/eotimeseriesviewer/externals/qps/cursorlocationvalue.py
index d4986f08..8967755c 100644
--- a/eotimeseriesviewer/externals/qps/cursorlocationvalue.py
+++ b/eotimeseriesviewer/externals/qps/cursorlocationvalue.py
@@ -100,97 +100,69 @@ class CursorLocationInfoModel(TreeModel):
     REMAINDER = 'reminder'
 
     def __init__(self, parent=None):
-        super(CursorLocationInfoModel, self).__init__(parent)
+        super().__init__(parent=parent)
+
+        self.setColumnNames(['Band/Field', 'Value'])
 
-        self.mColumnNames = ['Band/Field', 'Value', 'Description']
         self.mExpandedNodeRemainder = {}
         self.mNodeExpansion = CursorLocationInfoModel.REMAINDER
 
-    def setNodeExpansion(self, type):
-
-        assert type in [CursorLocationInfoModel.ALWAYS_EXPAND,
-                        CursorLocationInfoModel.NEVER_EXPAND,
-                        CursorLocationInfoModel.REMAINDER]
-        self.mNodeExpansion = type
+    def flags(self, index):
 
-    def setExpandedNodeRemainder(self, node=None):
-        treeView = self.mTreeView
-        assert isinstance(treeView, QTreeView)
-        if node is None:
-            for n in self.mRootNode.childNodes():
-                self.setExpandedNodeRemainder(node=n)
-        else:
-            self.mExpandedNodeRemainder[self.weakNodeId(node)] = self.mTreeView.isExpanded(self.node2idx(node))
-            for n in node.childNodes():
-                self.setExpandedNodeRemainder(node=n)
+        return Qt.ItemIsEnabled | Qt.ItemIsSelectable
 
-    def weakNodeId(self, node):
+    """
+    def weakNodeId(self, node: TreeNode) -> str:
         assert isinstance(node, TreeNode)
         n = node.name()
         while node.parentNode() != self.mRootNode:
             node = node.parentNode()
             n += '{}:{}'.format(node.name(), n)
         return n
+    """
 
-    def flags(self, index):
-
-        return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable
-
-    def addSourceValues(self, sourceValueSet):
+    def addSourceValues(self, sourceValueSet: SourceValueSet):
         if not isinstance(sourceValueSet, SourceValueSet):
             return
-
-        # get-or-create node
-        def gocn(root, name) -> TreeNode:
-            assert isinstance(root, TreeNode)
-            n = TreeNode(root, name)
-            weakId = self.weakNodeId(n)
-
-            expand = False
-            if not isinstance(root.parentNode(), TreeNode):
-                expand = True
-            else:
-                if self.mNodeExpansion == CursorLocationInfoModel.REMAINDER:
-                    expand = self.mExpandedNodeRemainder.get(weakId, False)
-                elif self.mNodeExpansion == CursorLocationInfoModel.NEVER_EXPAND:
-                    expand = False
-                elif self.mNodeExpansion == CursorLocationInfoModel.ALWAYS_EXPAND:
-                    expand = True
-
-            self.mTreeView.setExpanded(self.node2idx(n), expand)
-            return n
-
         bn = os.path.basename(sourceValueSet.source)
 
         if isinstance(sourceValueSet, RasterValueSet):
-            root = gocn(self.mRootNode, name=bn)
+
+            root = TreeNode(bn)
             root.setIcon(QIcon(':/qps/ui/icons/raster.svg'))
 
             # add subnodes
-            n = gocn(root, 'Pixel')
-            n.setValues('{},{}'.format(sourceValueSet.pxPosition.x(), sourceValueSet.pxPosition.y()))
+            pxNode = TreeNode('Pixel')
+            pxNode.setValues('{},{}'.format(sourceValueSet.pxPosition.x(), sourceValueSet.pxPosition.y()))
+
+            subNodes = [pxNode]
 
             for bv in sourceValueSet.bandValues:
                 if isinstance(bv, RasterValueSet.BandInfo):
-                    n = gocn(root, 'Band {}'.format(bv.bandIndex + 1))
+                    n = TreeNode('Band {}'.format(bv.bandIndex + 1))
                     n.setToolTip('Band {} {}'.format(bv.bandIndex + 1, bv.bandName).strip())
                     n.setValues([bv.bandValue, bv.bandName])
+                    subNodes.append(n)
 
                     if isinstance(bv.classInfo, ClassInfo):
-                        nc = gocn(root, 'Class')
+                        nc = TreeNode('Class')
                         nc.setValues(bv.classInfo.name())
                         nc.setIcon(bv.classInfo.icon())
-
+                        n.appendChildNodes(nc)
 
                 elif isinstance(bv, QColor):
-                    n = gocn(root, 'Color')
+                    n = TreeNode('Color')
                     n.setToolTip('Color selected from screen pixel')
                     n.setValues(bv.getRgb())
+                    subNodes.append(n)
+            root.appendChildNodes(subNodes)
+            self.rootNode().appendChildNodes(root)
 
         if isinstance(sourceValueSet, VectorValueSet):
             if len(sourceValueSet.features) == 0:
                 return
-            root = gocn(self.mRootNode, name=bn)
+
+            root = TreeNode(bn)
             refFeature = sourceValueSet.features[0]
             assert isinstance(refFeature, QgsFeature)
             typeName = QgsWkbTypes.displayString(refFeature.geometry().wkbType()).lower()
@@ -201,21 +173,33 @@ class CursorLocationInfoModel(TreeModel):
             if 'point' in typeName:
                 root.setIcon(QIcon(r':/images/themes/default/mIconPointLayer.svg'))
 
+            subNodes = []
             for field in refFeature.fields():
                 assert isinstance(field, QgsField)
 
-                fieldNode = gocn(root, name=field.name())
+                fieldNode = TreeNode(field.name())
 
+                featureNodes = []
                 for i, feature in enumerate(sourceValueSet.features):
                     assert isinstance(feature, QgsFeature)
-                    nf = gocn(fieldNode, name='{}'.format(feature.id()))
+                    nf = TreeNode(name='{}'.format(feature.id()))
                     nf.setValues([feature.attribute(field.name()), field.typeName()])
                     nf.setToolTip('Value of feature "{}" in field with name "{}"'.format(feature.id(), field.name()))
-
+                    featureNodes.append(nf)
+                fieldNode.appendChildNodes(featureNodes)
+                subNodes.append(fieldNode)
+            root.appendChildNodes(subNodes)
+            self.rootNode().appendChildNodes(root)
         s = ""
 
     def clear(self):
-        self.mRootNode.removeChildNodes(0, self.mRootNode.childCount())
+        self.mRootNode.removeAllChildNodes()
+
+
+class CursorLocationInfoTreeView(TreeView):
+
+    def __init__(self, *args, **kwds):
+        super().__init__(*args, **kwds)
 
 
 class ComboBoxOption(object):
@@ -313,8 +297,11 @@ class CursorLocationInfoDock(QDockWidget):
         self.btnCrs.crsChanged.connect(self.setCrs)
         self.btnCrs.setCrs(QgsCoordinateReferenceSystem())
 
-        self.mLocationInfoModel = CursorLocationInfoModel(parent=self.treeView)
-        self.treeView.setModel(self.mLocationInfoModel)
+        self.mLocationInfoModel = CursorLocationInfoModel()
+        self.mTreeView: CursorLocationInfoTreeView
+        assert isinstance(self.mTreeView, CursorLocationInfoTreeView)
+        self.mTreeView.setAutoExpansionDepth(3)
+        self.mTreeView.setModel(self.mLocationInfoModel)
 
         self.mLayerModeModel = ComboBoxOptionModel(LAYERMODES, parent=self)
         self.mLayerTypeModel = ComboBoxOptionModel(LAYERTYPES, parent=self)
@@ -355,6 +342,9 @@ class CursorLocationInfoDock(QDockWidget):
         self.setCanvas(canvas)
         self.reloadCursorLocation()
 
+    def treeView(self) -> CursorLocationInfoTreeView:
+        return self.mTreeView
+
     def reloadCursorLocation(self):
         """
         Call to load / re-load the data for the cursor location
@@ -381,7 +371,7 @@ class CursorLocationInfoDock(QDockWidget):
         for c in self.mCanvases:
             lyrs.extend(layerFilter(c))
 
-        self.mLocationInfoModel.setExpandedNodeRemainder()
+        self.treeView().updateNodeExpansion(False)
         self.mLocationInfoModel.clear()
 
         for l in lyrs:
@@ -473,6 +463,8 @@ class CursorLocationInfoDock(QDockWidget):
 
                 pass
 
+        self.treeView().updateNodeExpansion(True)
+
     def setCursorLocation(self, spatialPoint: SpatialPoint):
         """
         Set the cursor lcation to be loaded.
@@ -535,5 +527,3 @@ class CursorLocationInfoDock(QDockWidget):
             return self.mLocationHistory[0]
         else:
             return None, None
-
-
diff --git a/eotimeseriesviewer/externals/qps/layerconfigwidgets/core.py b/eotimeseriesviewer/externals/qps/layerconfigwidgets/core.py
index 115a8179..042ea3d7 100644
--- a/eotimeseriesviewer/externals/qps/layerconfigwidgets/core.py
+++ b/eotimeseriesviewer/externals/qps/layerconfigwidgets/core.py
@@ -27,8 +27,9 @@ import os
 import pathlib
 import enum
 import re
-from qgis.core import QgsMapLayer, QgsRasterLayer, QgsVectorLayer, QgsFileUtils, QgsSettings, QgsStyle, QgsApplication
-from qgis.gui import QgsRasterHistogramWidget, QgsMapCanvas, QgsMapLayerConfigWidget, \
+from qgis.core import QgsMapLayer, QgsRasterLayer, QgsVectorLayer, QgsFileUtils, QgsSettings, \
+    QgsStyle, QgsMapLayerStyle, QgsApplication
+from qgis.gui import QgsRasterHistogramWidget, QgsMapCanvas, QgsMapLayerConfigWidget,  \
     QgsLayerTreeEmbeddedConfigWidget, QgsMapLayerConfigWidgetFactory, QgsRendererRasterPropertiesWidget, \
     QgsRendererPropertiesDialog, QgsRasterTransparencyWidget, QgsProjectionSelectionWidget
 #
@@ -192,11 +193,11 @@ class SymbologyConfigWidget(QpsMapLayerConfigWidget):
     Emulates the QGS Layer Property Dialogs "Source" page
     """
 
-    def __init__(self, layer: QgsMapLayer, canvas: QgsMapCanvas, style: QgsStyle = QgsStyle(), parent=None):
+    def __init__(self, layer: QgsMapLayer, canvas: QgsMapCanvas, parent=None):
         super().__init__(layer, canvas, parent=parent)
         loadUi(configWidgetUi('symbologyconfigwidget.ui'), self)
         self.mSymbologyWidget = None
-        self.mStyle: QgsStyle = style
+        self.mStyle: QgsMapLayerStyle = None
         self.mDefaultRenderer = None
         if isinstance(layer, (QgsRasterLayer, QgsVectorLayer)):
             self.mDefaultRenderer = layer.renderer().clone()
@@ -206,7 +207,7 @@ class SymbologyConfigWidget(QpsMapLayerConfigWidget):
     def symbologyWidget(self) -> typing.Union[QgsRendererRasterPropertiesWidget, QgsRendererPropertiesDialog]:
         return self.scrollArea.widget()
 
-    def style(self) -> QgsStyle:
+    def style(self) -> QgsMapLayerStyle:
         return self.mStyle
 
     def menuButtonMenu(self) -> QMenu:
@@ -302,6 +303,13 @@ class SymbologyConfigWidget(QpsMapLayerConfigWidget):
         else:
             QMessageBox.information(self, 'Load Style', msg)
 
+    def reset(self):
+
+        lyr = self.mapLayer()
+        if isinstance(lyr, QgsMapLayer):
+            lyr.styleManager().reset()
+            self.syncToLayer()
+
     def saveStyle(self, fileName: str = None):
         lastUsedDir = QgsSettings().value("style/lastStyleDir")
 
@@ -364,6 +372,9 @@ class SymbologyConfigWidget(QpsMapLayerConfigWidget):
 
         w = self.symbologyWidget()
         lyr = self.mapLayer()
+        if isinstance(lyr, QgsMapLayer):
+            self.mStyle = lyr.styleManager().style(lyr.styleManager().currentStyle())
+
         if isinstance(lyr, QgsRasterLayer):
             r = lyr.renderer()
             if isinstance(w, QgsRendererRasterPropertiesWidget):
@@ -377,11 +388,12 @@ class SymbologyConfigWidget(QpsMapLayerConfigWidget):
                 self.setSymbologyWidget(w)
 
         elif isinstance(lyr, QgsVectorLayer):
+            w = None
             if not isinstance(w, QgsRendererPropertiesDialog):
-                w = QgsRendererPropertiesDialog(lyr, self.style(), embedded=True)
+                w = QgsRendererPropertiesDialog(lyr, QgsStyle.defaultStyle(), embedded=True)
                 self.setSymbologyWidget(w)
             else:
-                s = ""
+
                 pass
 
         else:
diff --git a/eotimeseriesviewer/externals/qps/layerconfigwidgets/gdalmetadata.py b/eotimeseriesviewer/externals/qps/layerconfigwidgets/gdalmetadata.py
index f6144aea..dc464879 100644
--- a/eotimeseriesviewer/externals/qps/layerconfigwidgets/gdalmetadata.py
+++ b/eotimeseriesviewer/externals/qps/layerconfigwidgets/gdalmetadata.py
@@ -34,7 +34,7 @@ from qgis.PyQt.QtGui import *
 from qgis.PyQt.QtWidgets import *
 from qgis.core import QgsRasterLayer, QgsVectorLayer, QgsMapLayer, \
     QgsVectorDataProvider, QgsRasterDataProvider, Qgis
-from qgis.gui import QgsMapCanvas, QgsMapLayerConfigWidgetFactory, QgsDoubleSpinBox
+from qgis.gui import QgsMapCanvas, QgsMapLayerConfigWidgetFactory, QgsDoubleSpinBox, QgsMessageBar
 from .core import QpsMapLayerConfigWidget
 from ..classification.classificationscheme import ClassificationScheme, ClassificationSchemeWidget
 from ..utils import loadUi, gdalDataset, parseWavelength, parseFWHM
@@ -115,6 +115,12 @@ class GDALErrorHandler(object):
         self.err_no=err_no
         self.err_msg=err_msg
 
+        if err_level == gdal.CE_Warning:
+            pass
+
+        if err_level > gdal.CE_Warning:
+            raise RuntimeError(err_level, err_no, err_msg)
+
 
 class GDALBandMetadataItem(object):
 
@@ -129,6 +135,7 @@ class GDALBandMetadataItem(object):
 
 class GDALBandMetadataModel(QAbstractTableModel):
     sigWavelengthUnitsChanged = pyqtSignal(str)
+    sigMessage = pyqtSignal(str, Qgis.MessageLevel)
 
     def __init__(self, parent=None):
         super().__init__(parent)
@@ -136,6 +143,8 @@ class GDALBandMetadataModel(QAbstractTableModel):
         self.cnWavelength = 'Wavelength'
         self.cnFWHM = 'FWHM'
 
+        self.mErrorHandler: GDALErrorHandler = GDALErrorHandler()
+
         self.mWavelengthUnitModel = XUnitModel()
         self.mWavelengthUnitModel.mDescription[BAND_INDEX] = 'None'
         self.mWavelengthUnitModel.mToolTips[BAND_INDEX] = 'No wavelength defined'
@@ -323,45 +332,72 @@ class GDALBandMetadataModel(QAbstractTableModel):
     def applyToLayer(self, *args):
         if isinstance(self.mMapLayer, QgsRasterLayer) and self.mMapLayer.isValid():
 
-            def list_or_empty(values):
+            def list_or_empty(values, domain:str=None):
                 for v in values:
-                    if v not in ['', None, 'None']:
-                        return ','.join(values)
+                    if v in ['', None, 'None']:
+                        return ''
 
-                return ''
+                result = ','.join(values)
+                if domain == 'ENVI':
+                    result = f'{{{result}}}'
+                return result
 
             if self.mMapLayer.dataProvider().name() == 'gdal':
+                gdal.PushErrorHandler(self.mErrorHandler.handler)
                 try:
                     ds: gdal.Dataset = gdal.Open(self.mMapLayer.source(), gdal.GA_Update)
                     assert isinstance(ds, gdal.Dataset)
-                except Exception as ex:
-                    print(f'unable to open image in update mode: {self.mMapLayer.source()}')
-                    print(ex, file=sys.stderr)
+                except RuntimeError as ex:
+                    msg = f'{ex}\nMetadata might not get saved for {self.mMapLayer.source()}'
+                    self.sigMessage.emit(msg, Qgis.Warning)
+
                     ds: gdal.Dataset = gdal.Open(self.mMapLayer.source(), gdal.GA_ReadOnly)
 
-                is_envi = ds.GetDriver().ShortName == 'ENVI'
-                if is_envi:
-                    domain = 'ENVI'
-                else:
-                    domain = None
-                if self.mWavelengthUnit not in [None, '', BAND_INDEX]:
-                    ds.SetMetadataItem('wavelength units ', self.mWavelengthUnit, domain)
+                try:
 
-                    wl = [str(item.wavelength) for item in self.mBandMetadata]
-                    ds.SetMetadataItem('wavelength', list_or_empty(wl), domain)
+                    # if ENVI driver, save to 'ENVI' domain
+                    # if other driver, save  to default domain
 
-                    fwhm = [str(item.fwhm) for item in self.mBandMetadata]
-                    ds.SetMetadataItem('fwhm', list_or_empty(fwhm), domain)
+                    is_envi = ds.GetDriver().ShortName == 'ENVI'
+                    if is_envi:
+                        domain = 'ENVI'
+                    else:
+                        domain = None
 
-                for b, item in enumerate(self.mBandMetadata):
-                    assert isinstance(item, GDALBandMetadataItem)
-                    band: gdal.Band = ds.GetRasterBand(b+1)
-                    band.SetDescription(item.name)
+                    if self.mWavelengthUnit not in [None, '', BAND_INDEX]:
+
+                        # remove potential concurrent definitions of wavelength unit and fwhm
+                        for k in ['wavelength_units', 'wavelength units', 'fwhm', 'wavelength', 'wavelengths']:
+                            for d in ['ENVI', None]:
+                                ds.SetMetadataItem(k, None, d)
+                                for b in range(ds.RasterCount):
+                                    band: gdal.Band = ds.GetRasterBand(b+1)
+                                    band.SetMetadataItem(k, None, d)
 
-                    band.FlushCache()
+                        ds.SetMetadataItem('wavelength units', self.mWavelengthUnit, domain)
 
-                ds.FlushCache()
-                del ds
+                        wl = [str(item.wavelength) for item in self.mBandMetadata]
+                        ds.SetMetadataItem('wavelength', list_or_empty(wl), domain)
+
+                        fwhm = [str(item.fwhm) for item in self.mBandMetadata]
+                        ds.SetMetadataItem('fwhm', list_or_empty(fwhm), domain)
+
+                    for b, item in enumerate(self.mBandMetadata):
+                        assert isinstance(item, GDALBandMetadataItem)
+                        band: gdal.Band = ds.GetRasterBand(b+1)
+                        band.SetDescription(item.name)
+
+                        band.FlushCache()
+
+                    ds.FlushCache()
+                    del ds
+
+                except Exception as ex:
+                    msg = f'{ex}'
+                    self.sigMessage.emit(msg, Qgis.Critical)
+                    print(msg, file=sys.stderr)
+                finally:
+                    gdal.PopErrorHandler()
 
     def validate(self) -> typing.List[str]:
         # todo: implement some internal validation and return descriptive error messages
@@ -409,7 +445,6 @@ class GDALBandMetadataModel(QAbstractTableModel):
         self.endResetModel()
 
 
-
 class GDALBandMetadataModelTableViewDelegate(QStyledItemDelegate):
     """
 
@@ -553,6 +588,7 @@ class GDALBandMetadataModelTableView(QTableView):
 class GDALMetadataModel(QAbstractTableModel):
 
     sigEditable = pyqtSignal(bool)
+    sigMessage = pyqtSignal(str, Qgis.MessageLevel)
 
     def __init__(self, parent=None):
         super(GDALMetadataModel, self).__init__(parent)
@@ -566,9 +602,9 @@ class GDALMetadataModel(QAbstractTableModel):
         self.cnValue = 'Value(s)'
 
         self._column_names = [self.cnItem, self.cnDomain, self.cnKey, self.cnValue]
-        self._isEditable = False
+        self._isEditable: bool = False
         self._MDItems: typing.List[GDALMetadataItem] = []
-        self._MOKs:typing.List[str] = []
+        self._MOKs: typing.List[str] = []
 
     def resetChanges(self):
         c = self._column_names.index(self.cnValue)
@@ -653,6 +689,7 @@ class GDALMetadataModel(QAbstractTableModel):
         mdItems, moks = self._read_maplayer()
         self._MDItems.extend(mdItems)
         self._MOKs.extend(moks)
+
         self.endResetModel()
 
     def removeItem(self, item:GDALMetadataItem):
@@ -679,8 +716,17 @@ class GDALMetadataModel(QAbstractTableModel):
 
             if self.mLayer.dataProvider().name() == 'gdal':
                 gdal.PushErrorHandler(self.mErrorHandler.handler)
+
+                try:
+                    ds: gdal.Dataset = gdal.Open(self.mLayer.source(), gdal.GA_Update)
+                    assert isinstance(ds, gdal.Dataset)
+                except RuntimeError as ex:
+                    msg = f'{ex}\nMetadata might not get saved for {self.mLayer.source()}'
+                    self.sigMessage.emit(msg, Qgis.Warning)
+
+                    ds: gdal.Dataset = gdal.Open(self.mLayer.source(), gdal.GA_ReadOnly)
+
                 try:
-                    ds = gdal.Open(self.mLayer.source(), gdal.GA_ReadOnly)
                     if isinstance(ds, gdal.Dataset):
                         for item in changed:
                             mo: gdal.MajorObject = None
@@ -696,11 +742,9 @@ class GDALMetadataModel(QAbstractTableModel):
                         ds.FlushCache()
                         del ds
 
-                    if self.mErrorHandler.err_level >= gdal.CE_Warning:
-                        raise RuntimeError(self.mErrorHandler.err_level,
-                                           self.mErrorHandler.err_no,
-                                           self.mErrorHandler.err_msg)
                 except Exception as ex:
+                    msg = str(ex)
+                    self.sigMessage.emit(msg, Qgis.Critical)
                     print(ex, file=sys.stderr)
                 finally:
                     gdal.PopErrorHandler()
@@ -708,8 +752,10 @@ class GDALMetadataModel(QAbstractTableModel):
         if isinstance(self.mLayer, QgsVectorLayer) and isinstance(self.mLayer.dataProvider(), QgsVectorDataProvider):
             if self.mLayer.dataProvider().name() == 'ogr':
                 path = self.mLayer.source().split('|')[0]
-                gdal.PushErrorHandler(self.mErrorHandler.handler)
+                use_ogr_exception = ogr.GetUseExceptions()
+
                 try:
+                    ogr.UseExceptions()
                     ds: ogr.DataSource = ogr.Open(path, update=1)
                     if isinstance(ds, ogr.DataSource):
                         for item in changed:
@@ -724,14 +770,18 @@ class GDALMetadataModel(QAbstractTableModel):
                                 mo.SetMetadataItem(item.key, item.value, item.domain)
 
                         ds.FlushCache()
-                    if self.mErrorHandler.err_level >= gdal.CE_Warning:
-                        raise RuntimeError(self.mErrorHandler.err_level,
-                                           self.mErrorHandler.err_no,
-                                           self.mErrorHandler.err_msg)
+
                 except Exception as ex:
+                    msg = str(ex)
+                    self.sigMessage.emit(msg, Qgis.Critical)
                     print(ex, file=sys.stderr)
                 finally:
-                    gdal.PopErrorHandler()
+                    if use_ogr_exception:
+                        ogr.UseExceptions()
+                    else:
+                        ogr.DontUseExceptions()
+
+                    #gdal.PopErrorHandler()
 
     def index2MDItem(self, index: QModelIndex) -> GDALMetadataItem:
         """
@@ -819,12 +869,16 @@ class GDALMetadataModel(QAbstractTableModel):
         items = []
         major_objects = []
 
+
+
         if not isinstance(self.mLayer, QgsMapLayer) or not self.mLayer.isValid():
+            self.setIsEditable(False)
             return items, major_objects
 
+        is_editable = False
         if isinstance(self.mLayer, QgsRasterLayer) and self.mLayer.dataProvider().name() == 'gdal':
             ds = gdal.Open(self.mLayer.source())
-
+            is_editable = True
             if isinstance(ds, gdal.Dataset):
                 z = len(str(ds.RasterCount))
                 mok = 'Dataset'
@@ -842,6 +896,8 @@ class GDALMetadataModel(QAbstractTableModel):
         if isinstance(self.mLayer, QgsVectorLayer) and self.mLayer.dataProvider().name() == 'ogr':
             ds = ogr.Open(self.mLayer.source().split('|')[0])
             if isinstance(ds, ogr.DataSource):
+                drv: ogr.Driver = ds.GetDriver()
+                is_editable = False
                 sep = self.mLayer.dataProvider().sublayerSeparator()
                 subLayers = self.mLayer.dataProvider().subLayers()
                 if len(subLayers) > 0:
@@ -862,6 +918,7 @@ class GDALMetadataModel(QAbstractTableModel):
                     for (domain, key, value) in self._read_majorobject(lyr):
                         items.append(GDALMetadataItem(mok, domain, key, value))
 
+        self.setIsEditable(is_editable)
         return items, major_objects
 
 
@@ -875,7 +932,7 @@ class GDALMetadataModelTableView(QTableView):
 
     def contextMenuEvent(self, event: QContextMenuEvent) -> None:
         """
-        Opens a context menue
+        Opens a context menu
         """
         index = self.indexAt(event.pos())
         if index.isValid():
@@ -915,8 +972,6 @@ class GDALMetadataItemDialog(QDialog):
         self.tbValue.textChanged.connect(self.validate)
         self.cbDomain.currentTextChanged.connect(self.validate)
         self.cbMajorObject.currentTextChanged.connect(self.validate)
-
-
         self.validate()
 
     def validate(self, *args):
@@ -984,12 +1039,14 @@ class GDALMetadataModelConfigWidget(QpsMapLayerConfigWidget):
         pathUi = pathlib.Path(__file__).parents[1] / 'ui' / 'gdalmetadatamodelwidget.ui'
         loadUi(pathUi, self)
 
+        self.mMessageBar: QgsMessageBar
         self.tbFilter: QLineEdit
         self.btnMatchCase.setDefaultAction(self.optionMatchCase)
         self.btnRegex.setDefaultAction(self.optionRegex)
         self._cs = None
 
         self.bandMetadataModel = GDALBandMetadataModel()
+        self.bandMetadataModel.sigMessage.connect(self.showMessage)
         self.bandMetadataProxyModel = QSortFilterProxyModel()
         self.bandMetadataProxyModel.setSourceModel(self.bandMetadataModel)
         self.bandMetadataProxyModel.setFilterKeyColumn(-1)
@@ -1002,6 +1059,7 @@ class GDALMetadataModelConfigWidget(QpsMapLayerConfigWidget):
         self.bandMetadataModel.registerWavelengthUnitComboBox(self.cbWavelengthUnits)
 
         self.metadataModel = GDALMetadataModel()
+        self.metadataModel.sigMessage.connect(self.showMessage)
         self.metadataModel.sigEditable.connect(self.onEditableChanged)
         self.metadataProxyModel = QSortFilterProxyModel()
         self.metadataProxyModel.setSourceModel(self.metadataModel)
@@ -1031,6 +1089,18 @@ class GDALMetadataModelConfigWidget(QpsMapLayerConfigWidget):
         self.actionRemoveItem.triggered.connect(self.onRemoveSelectedItems)
         self.onEditableChanged(self.metadataModel.isEditable())
 
+    def showMessage(self, msg:str, level: Qgis.MessageLevel):
+
+        if level == Qgis.Critical:
+            duration = 200
+        else:
+            duration = 50
+        line1 = msg.splitlines()[0]
+        self.messageBar().pushMessage('', line1, msg, level, duration)
+
+    def messageBar(self) -> QgsMessageBar:
+        return self.mMessageBar
+
     def onWavelengthUnitsChanged(self):
         wlu = self.bandMetadataModel.wavelenghtUnit()
 
@@ -1086,7 +1156,7 @@ class GDALMetadataModelConfigWidget(QpsMapLayerConfigWidget):
         if not (isinstance(layer, QgsMapLayer) and layer.isValid()):
             self.is_gdal = self.is_ogr = self.supportsGDALClassification = False
         else:
-
+            self.supportsGDALClassification = False
             self.is_gdal = isinstance(layer, QgsRasterLayer) and layer.dataProvider().name() == 'gdal'
             self.is_ogr = isinstance(layer, QgsVectorLayer) and layer.dataProvider().name() == 'ogr'
 
@@ -1129,6 +1199,8 @@ class GDALMetadataModelConfigWidget(QpsMapLayerConfigWidget):
         self.metadataModel.setLayer(lyr)
         if self.supportsGDALClassification:
             self._cs = ClassificationScheme.fromMapLayer(lyr)
+        else:
+            self._cs = None
 
         if isinstance(self._cs, ClassificationScheme) and len(self._cs) > 0:
             self.gbClassificationScheme.setVisible(True)
@@ -1188,7 +1260,7 @@ class GDALMetadataConfigWidgetFactory(QgsMapLayerConfigWidgetFactory):
 
     def createWidget(self, layer, canvas, dockWidget=True, parent=None) -> GDALMetadataModelConfigWidget:
         w = GDALMetadataModelConfigWidget(layer, canvas, parent=parent)
-        w.metadataModel.setIsEditable(True)
+        #w.metadataModel.setIsEditable(True)
         w.setWindowTitle(self.title())
         w.setWindowIcon(self.icon())
         return w
diff --git a/eotimeseriesviewer/externals/qps/layerconfigwidgets/vectorlayerfields.py b/eotimeseriesviewer/externals/qps/layerconfigwidgets/vectorlayerfields.py
index d2448092..c589fe1c 100644
--- a/eotimeseriesviewer/externals/qps/layerconfigwidgets/vectorlayerfields.py
+++ b/eotimeseriesviewer/externals/qps/layerconfigwidgets/vectorlayerfields.py
@@ -22,19 +22,18 @@
     along with this software. If not, see <http://www.gnu.org/licenses/>.
 ***************************************************************************
 """
-import typing, pathlib, sys
-from qgis.core import QgsRasterLayer, QgsRasterRenderer
-from qgis.core import *
-from qgis.gui import QgsMapCanvas, QgsMapLayerConfigWidget, QgsRasterBandComboBox
-from qgis.gui import *
-from qgis.PyQt.QtWidgets import *
-from qgis.PyQt.QtGui import *
+import sys
+
 from qgis.PyQt.QtCore import *
+from qgis.PyQt.QtGui import *
 from qgis.PyQt.QtGui import QIcon
-from ..utils import loadUi
-import numpy as np
+from qgis.PyQt.QtWidgets import *
+from qgis.core import QgsMapLayer, QgsVectorLayer, QgsField, QgsFieldModel, QgsEditorWidgetSetup
+from qgis.gui import QgsEditorWidgetFactory, QgsCollapsibleGroupBox, QgsEditorConfigWidget, QgsGui, \
+    QgsMapLayerConfigWidgetFactory
 from .core import QpsMapLayerConfigWidget
 from ..layerproperties import AddAttributeDialog
+from ..utils import loadUi
 
 
 class LayerFieldsTableModel(QgsFieldModel):
@@ -53,7 +52,8 @@ class LayerFieldsTableModel(QgsFieldModel):
         self.cnWMS = 'WMS'
         self.cnWFS = 'WFS'
 
-        self.mColumnNames = [self.cnId, self.cnName, self.cnAlias, self.cnType, self.cnTypeName, self.cnLength, self.cnPrecision, self.cnComment, self.cnWMS, self.cnWFS]
+        self.mColumnNames = [self.cnId, self.cnName, self.cnAlias, self.cnType, self.cnTypeName, self.cnLength,
+                             self.cnPrecision, self.cnComment, self.cnWMS, self.cnWFS]
 
     def columnNames(self):
         return self.mColumnNames[:]
@@ -77,10 +77,10 @@ class LayerFieldsTableModel(QgsFieldModel):
             return col
         return None
 
-    def columnCount(self, parent:QModelIndex):
+    def columnCount(self, parent: QModelIndex):
         return len(self.mColumnNames)
 
-    def data(self, index:QModelIndex, role:int):
+    def data(self, index: QModelIndex, role: int):
         if not index.isValid():
             return None
 
@@ -112,7 +112,7 @@ class LayerFieldsTableModel(QgsFieldModel):
                 return None
             if cn == self.cnWFS:
                 return None
-        if role in [QgsFieldModel.FieldNameRole ,
+        if role in [QgsFieldModel.FieldNameRole,
                     QgsFieldModel.FieldIndexRole,
                     QgsFieldModel.ExpressionRole,
                     QgsFieldModel.IsExpressionRole,
@@ -126,12 +126,12 @@ class LayerFieldsTableModel(QgsFieldModel):
             return super().data(index, role)
 
 
-
 class LayerFieldsListModel(QgsFieldModel):
     """
     A model to show the QgsFields of an QgsVectorLayer as vertical list
     Inherits QgsFieldModel and allows to change the name of the 1st column.
     """
+
     def __init__(self, parent=None):
         """
         Constructor
@@ -175,13 +175,14 @@ class LayerFieldsListModel(QgsFieldModel):
             self.headerDataChanged.emit(orientation, col, col)
         return result
 
-class LayerAttributeFormConfigEditorWidget(QWidget):
 
+class LayerAttributeFormConfigEditorWidget(QWidget):
     class ConfigInfo(QStandardItem):
         """
         Describes a QgsEditorWidgetFactory configuration.
         """
-        def __init__(self, key:str, factory:QgsEditorWidgetFactory, configWidget:QgsEditorConfigWidget):
+
+        def __init__(self, key: str, factory: QgsEditorWidgetFactory, configWidget: QgsEditorConfigWidget):
             super(LayerAttributeFormConfigEditorWidget.ConfigInfo, self).__init__()
 
             assert isinstance(key, str)
@@ -194,7 +195,6 @@ class LayerAttributeFormConfigEditorWidget(QWidget):
             self.setToolTip(factory.name())
             self.mInitialConfig = dict(configWidget.config())
 
-
         def resetConfig(self):
             """
             Resets the widget to its initial values
@@ -243,10 +243,9 @@ class LayerAttributeFormConfigEditorWidget(QWidget):
             """
             return QgsEditorWidgetSetup(self.factoryKey(), self.config())
 
-
     sigChanged = pyqtSignal(object)
 
-    def __init__(self, parent, layer:QgsVectorLayer, index:int):
+    def __init__(self, parent, layer: QgsVectorLayer, index: int):
         super(LayerAttributeFormConfigEditorWidget, self).__init__(parent)
 
         self.setLayout(QVBoxLayout())
@@ -309,7 +308,6 @@ class LayerAttributeFormConfigEditorWidget(QWidget):
         self.layout().addStretch()
         self.cbWidgetType.setCurrentIndex(iCurrent)
 
-
         conf = self.currentFieldConfig()
         self.mInitialFactoryKey = None
         self.mInitialConf = None
@@ -317,9 +315,7 @@ class LayerAttributeFormConfigEditorWidget(QWidget):
             self.mInitialFactoryKey = conf.factoryKey()
             self.mInitialConf = conf.config()
 
-
-
-    def setFactory(self, factoryKey:str):
+    def setFactory(self, factoryKey: str):
         """
         Shows the QgsEditorConfigWidget of QgsEditorWidgetFactory `factoryKey`
         :param factoryKey: str
@@ -331,7 +327,6 @@ class LayerAttributeFormConfigEditorWidget(QWidget):
                 self.cbWidgetType.setCurrentIndex(i)
                 break
 
-
     def changed(self) -> bool:
         """
         Returns True if the QgsEditorWidgetFactory or its configuration has been changed
@@ -364,7 +359,6 @@ class LayerAttributeFormConfigEditorWidget(QWidget):
         Resets the widget to its initial status
         """
         if self.changed():
-
             self.setFactory(self.mInitialFactoryKey)
             self.currentEditorConfigWidget().setConfig(self.mInitialConf)
 
@@ -379,7 +373,6 @@ class LayerAttributeFormConfigEditorWidget(QWidget):
         self.stackedWidget.setCurrentIndex(index)
         fieldConfig = self.currentFieldConfig()
         if isinstance(fieldConfig, LayerAttributeFormConfigEditorWidget.ConfigInfo):
-
             self.sigChanged.emit(self)
 
 
@@ -387,6 +380,7 @@ class LayerAttributeFormConfigWidget(QpsMapLayerConfigWidget):
     """
     A widget to set QgsVectorLayer field settings
     """
+
     def __init__(self, *args, **kwds):
         super().__init__(*args, **kwds)
 
@@ -401,14 +395,13 @@ class LayerAttributeFormConfigWidget(QpsMapLayerConfigWidget):
         self.treeView.selectionModel().currentRowChanged.connect(self.onSelectedFieldChanged)
         self.updateFieldWidgets()
 
-
-    def menuButtonMenu(self) ->QMenu:
+    def menuButtonMenu(self) -> QMenu:
         m = QMenu('Reset')
         a = m.addAction('Reset')
         a.triggered.connect(self.onReset)
         return m
 
-    def onSelectedFieldChanged(self, index1:QModelIndex, index2:QModelIndex):
+    def onSelectedFieldChanged(self, index1: QModelIndex, index2: QModelIndex):
         """
         Shows the widget for the selected QgsField
         :param index1:
@@ -420,14 +413,14 @@ class LayerAttributeFormConfigWidget(QpsMapLayerConfigWidget):
                 s = ""
             self.stackedWidget.setCurrentIndex(r)
 
-    def onScrollAreaResize(self, resizeEvent:QResizeEvent):
+    def onScrollAreaResize(self, resizeEvent: QResizeEvent):
         """
         Forces the stackedWidget's width to fit into the scrollAreas viewport
         :param resizeEvent: QResizeEvent
         """
         assert isinstance(resizeEvent, QResizeEvent)
         self.stackedWidget.setMaximumWidth(resizeEvent.size().width())
-        s =""
+        s = ""
 
     def onReset(self):
 
@@ -455,8 +448,6 @@ class LayerAttributeFormConfigWidget(QpsMapLayerConfigWidget):
             w.apply()
         self.onSettingsChanged()
 
-
-
     def updateFieldWidgets(self):
         """
         Empties the stackedWidget and populates it with a FieldConfigEditor
@@ -511,6 +502,7 @@ class LayerAttributeFormConfigWidgetFactory(QgsMapLayerConfigWidgetFactory):
     def supportsStyleDock(self):
         return False
 
+
 class LayerFieldsConfigWidget(QpsMapLayerConfigWidget):
     """
     A widget to edit the fields of a QgsVectorLayer
@@ -543,7 +535,6 @@ class LayerFieldsConfigWidget(QpsMapLayerConfigWidget):
         self.actionFieldCalculator.setEnabled(False)
         self.actionFieldCalculator.setVisible(False)
 
-
         self.validate()
 
         lyr.editingStarted.connect(self.validate)
@@ -554,12 +545,10 @@ class LayerFieldsConfigWidget(QpsMapLayerConfigWidget):
         bEdit = isinstance(self.mapLayer(), QgsVectorLayer) and self.mapLayer().isEditable()
         bSelected = len(self.tableView.selectionModel().selectedRows()) > 0
 
-
         self.actionAddField.setEnabled(bEdit)
         self.actionRemoveField.setEnabled(bEdit and bSelected)
         self.actionToggleEditing.setChecked(bEdit)
 
-
     def onAddField(self):
         lyr = self.mapLayer()
         if isinstance(lyr, QgsVectorLayer) and lyr.isEditable():
@@ -585,7 +574,7 @@ class LayerFieldsConfigWidget(QpsMapLayerConfigWidget):
         else:
             if lyr.isModified():
                 result = QMessageBox.question(self, 'Leaving edit mode', 'Save changes?',
-                                          buttons=QMessageBox.No | QMessageBox.Yes, defaultButton=QMessageBox.Yes)
+                                              buttons=QMessageBox.No | QMessageBox.Yes, defaultButton=QMessageBox.Yes)
                 if result == QMessageBox.Yes:
                     if not lyr.commitChanges():
                         errors = lyr.commitErrors()
@@ -601,7 +590,8 @@ class LayerFieldsConfigWidget(QpsMapLayerConfigWidget):
         lyr = self.mapLayer()
         if not isinstance(lyr, QgsVectorLayer):
             return
-        indices = [self.mProxyModel.mapToSource(idx).data(QgsFieldModel.FieldIndexRole) for idx in self.tableView.selectionModel().selectedRows()]
+        indices = [self.mProxyModel.mapToSource(idx).data(QgsFieldModel.FieldIndexRole) for idx in
+                   self.tableView.selectionModel().selectedRows()]
 
         if not lyr.deleteAttributes(indices):
             errors = lyr.errors()
@@ -638,5 +628,3 @@ class LayerFieldsConfigWidgetFactory(QgsMapLayerConfigWidgetFactory):
 
     def supportsStyleDock(self):
         return False
-
-
diff --git a/eotimeseriesviewer/externals/qps/layerproperties.py b/eotimeseriesviewer/externals/qps/layerproperties.py
index d0be19e2..09a0b2b2 100644
--- a/eotimeseriesviewer/externals/qps/layerproperties.py
+++ b/eotimeseriesviewer/externals/qps/layerproperties.py
@@ -125,6 +125,21 @@ MDF_QGIS_LAYER_STYLE = 'application/qgis.style'
 MDF_TEXT_PLAIN = 'text/plain'
 
 
+class FieldListModel(QAbstractListModel):
+
+    def __init__(self, *args, layer:QgsVectorLayer=None, **kwds):
+
+        super().__init__(*args, **kwds)
+
+    def setLayer(self, layer:QgsVectorLayer):
+
+        self.mLayer = layer
+
+    def flags(self, index:QModelIndex):
+        pass
+
+
+
 class AddAttributeDialog(QDialog):
     """
     A dialog to set up a new QgsField.
@@ -159,6 +174,7 @@ class AddAttributeDialog(QDialog):
             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)
@@ -800,6 +816,7 @@ class LayerPropertiesDialog(QgsOptionsDialogBase):
         assert isinstance(self.mOptionsStackedWidget, QStackedWidget)
         assert isinstance(lyr, QgsMapLayer)
         self.btnConfigWidgetMenu: QPushButton = QPushButton('<menu>')
+        self.btnConfigWidgetMenu.setVisible(False)
         assert isinstance(self.btnConfigWidgetMenu, QPushButton)
         self.mOptionsListWidget.currentRowChanged.connect(self.onPageChanged)
         self.mLayer: QgsMapLayer = lyr
@@ -822,6 +839,9 @@ class LayerPropertiesDialog(QgsOptionsDialogBase):
         self.btnCancel: QPushButton = self.buttonBox.button(QDialogButtonBox.Cancel)
         self.btnOk: QPushButton = self.buttonBox.button(QDialogButtonBox.Ok)
 
+        self.btnHelp: QPushButton = self.buttonBox.button(QDialogButtonBox.Help)
+        # not connected
+        self.btnHelp.setVisible(False)
         s = ""
 
         assert isinstance(self.mOptionsListWidget, QListWidget)
@@ -854,7 +874,7 @@ class LayerPropertiesDialog(QgsOptionsDialogBase):
         menu = None
 
         if isinstance(page, QgsMapLayerConfigWidget):
-            # comes with QIS 3.12
+            # comes with QGIS 3.12
 
             if hasattr(page, 'menuButtonMenu'):
                 menu = page.menuButtonMenu()
@@ -1230,7 +1250,7 @@ class AttributeTableWidget(QMainWindow, QgsExpressionContextGenerator):
                 self.mActionSearchForm.setEnabled(False)
                 self.mActionSearchForm.setToolTip(tr("Search is not supported when using custom UI forms"))
 
-            self.editingToggled();
+            self.editingToggled()
 
         self._hide_unconnected_widgets()
 
@@ -1268,10 +1288,7 @@ class AttributeTableWidget(QMainWindow, QgsExpressionContextGenerator):
         self.mMainView.openConditionalStyles()
 
     def mActionCutSelectedRows_triggered(self):
-
-        pass
-
-        # QgisApp:: instance() -> cutSelectionToClipboard(mLayer);
+        self.vectorLayerTools().cutSelectionToClipboard(self.mLayer)
 
     def mActionCopySelectedRows_triggered(self):
         self.vectorLayerTools().copySelectionToClipboard(self.mLayer)
@@ -1628,7 +1645,7 @@ class AttributeTableWidget(QMainWindow, QgsExpressionContextGenerator):
         self.vectorLayerTools().panToSelected(self.mLayer)
 
     def mActionDeleteSelected_triggered(self):
-        self.vectorLayerTools().deleteSelected(self.mLayer)
+        self.vectorLayerTools().deleteSelection(self.mLayer)
 
     def reloadModel(self):
         """
diff --git a/eotimeseriesviewer/externals/qps/maptools.py b/eotimeseriesviewer/externals/qps/maptools.py
index 5d3ff078..a2e91ec2 100644
--- a/eotimeseriesviewer/externals/qps/maptools.py
+++ b/eotimeseriesviewer/externals/qps/maptools.py
@@ -919,7 +919,7 @@ class QgsMapToolDigitizeFeature(QgsMapToolCapture):
                     g = QgsGeometry(savePoint)
                 elif QgsWkbTypes.isMultiType(layerWKBType) and not QgsWkbTypes.hasZ(layerWKBType):
                     # g = QgsGeometry::fromMultiPointXY( QgsMultiPointXY() << savePoint );
-                    g = QgsGeometry.fromMultiPointXY(savePoint)
+                    #g = QgsGeometry.fromMultiPointXY(savePoint)
                     g = QgsGeometry(savePoint)
                 elif not QgsWkbTypes.isMultiType(layerWKBType) and QgsWkbTypes.hasZ(layerWKBType):
                     g = QgsGeometry(QgsPoint(savePoint.x(), savePoint.y(),
diff --git a/eotimeseriesviewer/externals/qps/models.py b/eotimeseriesviewer/externals/qps/models.py
index a37254ac..449ddfa4 100644
--- a/eotimeseriesviewer/externals/qps/models.py
+++ b/eotimeseriesviewer/externals/qps/models.py
@@ -24,20 +24,14 @@
     along with this software. If not, see <http://www.gnu.org/licenses/>.
 ***************************************************************************
 """
-
-
-import os, pickle, copy, enum
-
-from collections import OrderedDict
-
-from qgis.core import *
-from qgis.gui import *
-
-from PyQt5.QtCore import *
-from PyQt5.QtGui import *
-from PyQt5.QtWidgets import *
-
-from osgeo import gdal, osr
+import warnings
+import copy
+import enum
+import typing
+from qgis.PyQt.QtCore import QModelIndex, QAbstractItemModel, QAbstractListModel, \
+    pyqtSignal, Qt, QObject, QAbstractListModel, QSize
+from qgis.PyQt.QtWidgets import QComboBox, QTreeView
+from qgis.PyQt.QtGui import QIcon
 
 
 def currentComboBoxValue(comboBox):
@@ -49,6 +43,7 @@ def currentComboBoxValue(comboBox):
     else:
         return comboBox.currentData()
 
+
 def setCurrentComboBoxValue(comboBox, value):
     """
     Sets a QComboBox to the value `value`, if it exists in the underlying item list
@@ -125,7 +120,6 @@ class Option(object):
             return other.mValue == self.mValue
 
 
-
 class OptionListModel(QAbstractListModel):
     def __init__(self, options=None, parent=None):
         super(OptionListModel, self).__init__(parent)
@@ -148,6 +142,7 @@ class OptionListModel(QAbstractListModel):
         self.insertOptions(options)
 
     sigOptionsInserted = pyqtSignal(list)
+
     def insertOptions(self, options, i=None):
         if options is None:
             return
@@ -173,7 +168,6 @@ class OptionListModel(QAbstractListModel):
 
             self.sigOptionsInserted.emit(options)
 
-
     def o2o(self, value):
         if not isinstance(value, Option):
             value = Option(value, '{}'.format(value))
@@ -192,6 +186,7 @@ class OptionListModel(QAbstractListModel):
         return [o.mValue for o in self.options()]
 
     sigOptionsRemoved = pyqtSignal(list)
+
     def removeOptions(self, options):
         """
         Removes a list of options from this Options list.
@@ -237,16 +232,12 @@ class OptionListModel(QAbstractListModel):
                 break
         return idx
 
-
     def optionNames(self):
         return [o.mName for o in self.mOptions]
 
     def optionValues(self):
         return [o.mValue for o in self.mOptions]
 
-
-
-
     def data(self, index, role=Qt.DisplayRole):
         if not index.isValid():
             return None
@@ -264,35 +255,37 @@ class OptionListModel(QAbstractListModel):
         elif role == Qt.DecorationRole:
             result = option.mIcon
         elif role == Qt.UserRole:
-            result =  option
+            result = option
         return result
 
 
-
 class TreeNode(QObject):
-
     sigWillAddChildren = pyqtSignal(QObject, int, int)
     sigAddedChildren = pyqtSignal(QObject, int, int)
     sigWillRemoveChildren = pyqtSignal(QObject, int, int)
     sigRemovedChildren = pyqtSignal(QObject, int, int)
     sigUpdated = pyqtSignal(QObject)
-    sigExpandedChanged = pyqtSignal(QObject, bool)
-
-    def __init__(self, parentNode, name=None, value=None, values=None, icon=None, toolTip:str=None, statusTip:str=None, **kwds):
-        super(TreeNode, self).__init__()
-        QObject.__init__(self)
 
-        self.mParentNode = parentNode
-
-        self.mChildren = []
-        self.mName = name
-        self.mValues = []
-        self.mIcon = None
-        self.mToolTip = None
-        self.mCheckState = Qt.Unchecked
-        self.mCheckable = False
-        self.mStatusTip = ''
-        self.mExpanded = False
+    def __init__(self,
+                 name: str = None,
+                 value: any = None,
+                 values=None,
+                 icon: QIcon = None,
+                 toolTip: str = None,
+                 statusTip: str = None,
+                 **kwds):
+
+        super().__init__()
+
+        self.mParentNode: TreeNode = None
+        self.mChildren: typing.List[TreeNode] = []
+        self.mName: str = name
+        self.mValues: list = []
+        self.mIcon: QIcon = None
+        self.mToolTip: str = None
+        self.mCheckState: Qt.CheckState = Qt.Unchecked
+        self.mCheckable: bool = False
+        self.mStatusTip: str = ''
 
         if name:
             self.setName(name)
@@ -302,43 +295,44 @@ class TreeNode(QObject):
             self.setIcon(icon)
         if toolTip:
             self.setToolTip(toolTip)
-
         if statusTip:
             self.setStatusTip(statusTip)
 
         if values is not None:
             self.setValues(values)
 
-        if isinstance(parentNode, TreeNode):
-            parentNode.appendChildNodes([self])
-
-        s = ""
-
-
     def __iter__(self):
         return iter(self.mChildren)
 
     def __len__(self):
         return len(self.mChildren)
 
+    def __contains__(self, item):
+        return item in self.mChildren
+
+    def __getitem__(self, slice):
+        return self.mChildren[slice]
 
-    def setExpanded(self, expanded:bool):
+    def depth(self) -> int:
+        d = 0
+        parent = self.parentNode()
+        while isinstance(parent, TreeNode):
+            d += 1
+            parent = parent.parentNode()
+        return d
+
+    def columnCount(self) -> int:
         """
-        Expands the node
-        :param expanded:
+        A node has at least one column for its name
         :return:
+        :rtype:
         """
-        assert isinstance(expanded, bool)
-        b = self.mExpanded != expanded
-        self.mExpanded = expanded
-
-        if b and not self.signalsBlocked():
-            self.sigExpandedChanged.emit(self, self.mExpanded)
+        return len(self.mValues) + 1
 
     def expanded(self) -> bool:
         return self.mExpanded == True
 
-    def setStatusTip(self, statusTip:str):
+    def setStatusTip(self, statusTip: str):
         """
         Sets the nodes's status tip to the string specified by statusTip.
         :param statusTip: str
@@ -366,11 +360,10 @@ class TreeNode(QObject):
     def isCheckable(self) -> bool:
         return self.mCheckable == True
 
-    def setCheckable(self, b:bool):
+    def setCheckable(self, b: bool):
         assert isinstance(b, bool)
         self.mCheckable = b
 
-
     def clone(self, parent=None):
 
         n = TreeNode(parent)
@@ -384,52 +377,57 @@ class TreeNode(QObject):
             childNode.clone(parent=n)
         return n
 
-
-
-    def nodeIndex(self):
-        return self.mParentNode.mChildren.index(self)
+    def nodeIndex(self) -> int:
+        p = self.parentNode()
+        if isinstance(p, TreeNode):
+            return p.mChildren.index(self)
+        else:
+            None
 
     def next(self):
-        i = self.nodeIndex()
-        if i < len(self.mChildren.mChildren):
-            return self.mParentNode.mChildren[i + 1]
-        else:
-            return None
+        p = self.parentNode()
+        if isinstance(p, TreeNode):
+            i = p.mChildren.index(self)
+            if i < len(p.mChildren) - 1:
+                return p.mChildren[i + 1]
+        return None
 
     def previous(self):
-        i = self.nodeIndex()
-        if i > 0:
-            return self.mParentNode.mChildren[i - 1]
-        else:
-            return None
+        p = self.parentNode()
+        if isinstance(p, TreeNode):
+            i = p.mChildren.index(self)
+            if i > 0:
+                return p.mChildren[i - 1]
+        return None
 
     def detach(self):
         """
         Detaches this TreeNode from its parent TreeNode
         :return:
         """
-        if isinstance(self.mParentNode, TreeNode):
-            self.mParentNode.mChildren.remove(self)
-            self.setParentNode(None)
+        p = self.parent()
+        if isinstance(p, TreeNode):
+            p.removeChildNodes(self)
+
+    def hasChildren(self) -> bool:
+        return len(self.mChildren) > 0
 
-    def appendChildNodes(self, listOfChildNodes):
-        self.insertChildNodes(len(self.mChildren), listOfChildNodes)
+    def appendChildNodes(self, child_nodes):
+        self.insertChildNodes(len(self.mChildren), child_nodes)
 
-    def insertChildNodes(self, index, listOfChildNodes):
+    def insertChildNodes(self, index: int, child_nodes):
         assert index <= len(self.mChildren)
-        if isinstance(listOfChildNodes, TreeNode):
-            listOfChildNodes = [listOfChildNodes]
-        assert isinstance(listOfChildNodes, list)
-        listOfChildNodes = [l for l in listOfChildNodes if l not in self.mChildren]
+        if isinstance(child_nodes, TreeNode):
+            child_nodes = [child_nodes]
+        assert isinstance(child_nodes, list)
+        child_nodes = [l for l in child_nodes if l not in self.mChildren]
 
-        l = len(listOfChildNodes)
+        l = len(child_nodes)
         idxLast = index + l - 1
         self.sigWillAddChildren.emit(self, index, idxLast)
-        if not self.signalsBlocked():
-            self.sigWillAddChildren.emit(self, index, idxLast)
-        for i, node in enumerate(listOfChildNodes):
+
+        for i, node in enumerate(child_nodes):
             assert isinstance(node, TreeNode)
-            node.mParentNode = self
             # connect node signals
             node.sigWillAddChildren.connect(self.sigWillAddChildren)
             node.sigAddedChildren.connect(self.sigAddedChildren)
@@ -437,39 +435,60 @@ class TreeNode(QObject):
             node.sigRemovedChildren.connect(self.sigRemovedChildren)
             node.sigUpdated.connect(self.sigUpdated)
 
+            node.setParentNode(self)
             self.mChildren.insert(index + i, node)
-        import time
-        t0 = time.time()
 
         self.sigAddedChildren.emit(self, index, idxLast)
-        s = ""
-        if not self.signalsBlocked():
-            self.sigAddedChildren.emit(self, index, idxLast)
 
-    def removeChildNode(self, node):
-        assert node in self.mChildren
-        i = self.mChildren.index(node)
-        self.removeChildNodes(i, 1)
+    def removeAllChildNodes(self):
+        self.removeChildNodes(self.childNodes())
 
-    def removeChildNodes(self, row, count):
+    def removeChildNodes(self, child_nodes):
+        """
+        Removes child-nodes
+        :param child_nodes:
+        :type child_nodes:
+        :return:
+        :rtype:
+        """
+        if isinstance(child_nodes, TreeNode):
+            child_nodes = [child_nodes]
+        child_nodes: typing.List[TreeNode]
+        for node in child_nodes:
+            assert isinstance(node, TreeNode)
+            assert node in self.mChildren
 
-        if row < 0 or count <= 0:
-            return False
+        child_nodes = sorted(child_nodes, key=lambda node: node.nodeIndex())
+        while len(child_nodes) > 0:
+            # find neighbored nodes to remove
+            nextNode = child_nodes[0]
+            toRemove = []
+            while isinstance(nextNode, TreeNode) and nextNode in child_nodes:
+                toRemove.append(nextNode)
+                nextNode = nextNode.next()
 
-        rowLast = row + count - 1
+            first = toRemove[0].nodeIndex()
+            last = toRemove[-1].nodeIndex()
 
-        if rowLast >= self.childCount():
-            return False
+            self.sigWillRemoveChildren.emit(self, first, last)
+
+            for node in toRemove:
+                self.mChildren.remove(node)
 
-        self.sigWillRemoveChildren.emit(self, row, rowLast)
-        to_remove = self.childNodes()[row:rowLast + 1]
-        for n in to_remove:
-            self.mChildren.remove(n)
-            # n.mParent = None
+                # disconnect node signals
+                node.sigWillAddChildren.disconnect(self.sigWillAddChildren)
+                node.sigAddedChildren.disconnect(self.sigAddedChildren)
+                node.sigWillRemoveChildren.disconnect(self.sigWillRemoveChildren)
+                node.sigRemovedChildren.disconnect(self.sigRemovedChildren)
+                node.sigUpdated.disconnect(self.sigUpdated)
 
-        self.sigRemovedChildren.emit(self, row, rowLast)
+                node.setParentNode(None)
 
-    def setToolTip(self, toolTip:str):
+                child_nodes.remove(node)
+
+            self.sigRemovedChildren.emit(self, first, last)
+
+    def setToolTip(self, toolTip: str):
         """
         Sets the tooltip
         :param toolTip: str
@@ -483,27 +502,27 @@ class TreeNode(QObject):
         """
         return self.mToolTip
 
+    def setParentNode(self, node):
+        self.mParentNode: TreeNode = node
+
     def parentNode(self):
-        """
-        Returns the parent TreeNode that owns this TreeNode
-        :return: TreeNode
-        """
         return self.mParentNode
 
-    def setParentNode(self, treeNode):
-        """
-        :param treeNode:
-        :return:
-        """
-        assert isinstance(treeNode, TreeNode)
-        self.mParentNode = treeNode
+    def setParent(self, parentNode) -> None:
+        if parentNode is not None:
+            warnings.warn('Use setParentNode', DeprecationWarning)
+            assert isinstance(parentNode, TreeNode)
+        super().setParent(parentNode)
 
-    def setIcon(self, icon:QIcon):
+    def setIcon(self, icon: QIcon):
         """
         Sets the TreeNode icon
         :param icon: QIcon
         """
-        self.mIcon = icon
+        if icon != self.mIcon:
+            self.mIcon = icon
+            self.sigUpdated.emit(self)
+
 
     def icon(self) -> QIcon:
         """
@@ -512,12 +531,14 @@ class TreeNode(QObject):
         """
         return self.mIcon
 
-    def setName(self, name:str):
+    def setName(self, name: str):
         """
         Sets the TreeNodes name
         :param name: str
         """
-        self.mName = name
+        if name != self.mName:
+            self.mName = str(name)
+            self.sigUpdated.emit(self)
 
     def name(self) -> str:
         """
@@ -539,22 +560,18 @@ class TreeNode(QObject):
         else:
             self.setValues([value])
 
-    def setValues(self, listOfValues:list):
+    def setValues(self, values: list):
         """
         Sets the values show by this TreeNode
-        :param listOfValues: [list-of-values]
+        :param values: [list-of-values]
         """
-        old = self.mValues
-        if listOfValues is None:
-            self.mValues = []
-        else:
-            if not isinstance(listOfValues, list):
-                listOfValues = [listOfValues]
-            self.mValues = listOfValues[:]
-        if self.mValues != old:
+        if not isinstance(values, list):
+            values = [values]
+
+        if self.mValues != values:
+            self.mValues.clear()
+            self.mValues.extend(values)
             self.sigUpdated.emit(self)
-            if not self.signalsBlocked():
-                self.sigUpdated.emit(self)
 
     def values(self) -> list:
         """
@@ -573,12 +590,10 @@ class TreeNode(QObject):
         else:
             return None
 
-
     def childCount(self) -> int:
         """Returns the number of child nones"""
         return len(self.mChildren)
 
-
     def childNodes(self) -> list:
         """
         Returns the child nodes
@@ -586,6 +601,21 @@ class TreeNode(QObject):
         """
         return self.mChildren[:]
 
+    def findParentNode(self, nodeType):
+        """
+        Returns the next upper TreeNode of type "nodeType"
+        :param nodeType:
+        :return: TreeNode of type "nodeType" or None
+        """
+
+        parent = self.parentNode()
+        if not isinstance(parent, TreeNode):
+            return None
+        elif isinstance(parent, nodeType):
+            return parent
+        else:
+            return parent.findParentNode(nodeType)
+
     def findChildNodes(self, type, recursive=True):
         """
         Returns a list of child nodes with node-type `type`.
@@ -606,21 +636,32 @@ class TreeModel(QAbstractItemModel):
     """
     A QAbstractItemModel implementation to be used in QTreeViews
     """
-    def __init__(self, parent=None, rootNode=None):
-        super(TreeModel, self).__init__(parent)
 
-        self.mColumnNames = ['Node', 'Value']
-        self.mRootNode = rootNode if isinstance(rootNode, TreeNode) else TreeNode(None)
+    def __init__(self, parent: QObject = None, rootNode: TreeNode = None):
+        super().__init__(parent)
+
+        self.mRootNode: TreeNode
+        if isinstance(rootNode, TreeNode):
+            self.mRootNode = rootNode
+        else:
+            self.mRootNode = TreeNode(name='<root node>')
+
+        self.mRootNode.setValues(['Name', 'Value'])
         self.mRootNode.sigWillAddChildren.connect(self.onNodeWillAddChildren)
         self.mRootNode.sigAddedChildren.connect(self.onNodeAddedChildren)
         self.mRootNode.sigWillRemoveChildren.connect(self.onNodeWillRemoveChildren)
         self.mRootNode.sigRemovedChildren.connect(self.onNodeRemovedChildren)
         self.mRootNode.sigUpdated.connect(self.onNodeUpdated)
 
-        self.mTreeView = None
-        if isinstance(parent, QTreeView):
-            self.connectTreeView(parent)
-        s = ""
+    def setColumnNames(self, names):
+        assert isinstance(names, list)
+        self.mRootNode.setValues(names)
+
+    def __contains__(self, item):
+        return item in self.mRootNode
+
+    def __getitem__(self, slice):
+        return self.mRootNode[slice]
 
     def rootNode(self) -> TreeNode:
         """
@@ -629,42 +670,43 @@ class TreeModel(QAbstractItemModel):
         """
         return self.mRootNode
 
-
-    def onNodeWillAddChildren(self, node, idx1, idxL):
+    def onNodeWillAddChildren(self, node: TreeNode, first: int, last: int):
         idxNode = self.node2idx(node)
-        self.beginInsertRows(idxNode, idx1, idxL)
+        self.beginInsertRows(idxNode, first, last)
 
-    def onNodeAddedChildren(self, *args):
+    def onNodeAddedChildren(self, node: TreeNode, first: int, last: int):
         self.endInsertRows()
-        # for i in range(idx1, idxL+1):
 
+    def maxColumnCount(self, index: QModelIndex) -> int:
+        assert isinstance(index, QModelIndex)
+        cnt = self.columnCount(index)
+        for row in range(self.rowCount(index)):
+            idx = self.index(row, 0, index)
+            cnt = max(cnt, self.maxColumnCount(idx))
+        return cnt
 
-    def onNodeWillRemoveChildren(self, node, idx1, idxL):
+    def onNodeWillRemoveChildren(self, node: TreeNode, first: int, last: int):
         idxNode = self.node2idx(node)
-        self.beginRemoveRows(idxNode, idx1, idxL)
+        self.beginRemoveRows(idxNode, first, last)
 
-    def onNodeRemovedChildren(self, node, idx1, idxL):
+    def onNodeRemovedChildren(self, node: TreeNode, first: int, last: int):
         self.endRemoveRows()
 
-    def onNodeUpdated(self, node):
-        idxNode = self.node2idx(node)
-        self.dataChanged.emit(idxNode, idxNode)
-
+    def onNodeUpdated(self, node: TreeNode):
+        idx = self.node2idx(node)
+        idx2 = self.createIndex(idx.row(), node.columnCount() - 1)
+        self.dataChanged.emit(idx, idx2)
 
     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]
+            if section < len(self.mRootNode.values()):
+                return self.mRootNode.values()[section]
             else:
-                return ''
-
-        else:
-            return None
+                return f'Column {section + 1}'
+        return None
 
-    def parent(self, index:QModelIndex) -> QModelIndex:
+    def parent(self, index: QModelIndex) -> QModelIndex:
         """
         Returns the parent index of a QModelIndex `index`
         :param index: QModelIndex
@@ -672,42 +714,30 @@ class TreeModel(QAbstractItemModel):
         """
         if not index.isValid():
             return QModelIndex()
-        node = self.idx2node(index)
-        if not isinstance(node, TreeNode):
-            return QModelIndex()
-
-        parentNode = node.parentNode()
-        if not isinstance(parentNode, TreeNode):
+        childNode: TreeNode = index.internalPointer()
+        parentNode: TreeNode = childNode.parentNode()
+        if parentNode == self.mRootNode:
             return QModelIndex()
 
-        return self.node2idx(parentNode)
-
-        if node not in parentNode.mChildren:
-            return QModelIndex
-        row = parentNode.mChildren.index(node)
-        return self.createIndex(row, 0, parentNode)
+        return self.createIndex(parentNode.nodeIndex(), 0, parentNode)
 
-    def rowCount(self, index:QModelIndex) -> int:
+    def rowCount(self, parent: QModelIndex = None) -> int:
         """
-        Return the row-count, i.e. number of child node for a TreeNode as index `index`.
+        Return the row-count, i.e. number of child node for a TreeNode at index `index`.
         :param index: QModelIndex
         :return: int
         """
-        if index is None:
-            return len(self.rootNode().mChildren)
-        node = index.internalPointer()
-        if isinstance(node, TreeNode):
-            return node.childCount()
-        else:
-            return len(self.mRootNode)
-        if not index.isValid():
+        if parent is None:
+            parent = QModelIndex()
+
+        if parent.column() > 0:
             return 0
-        #assert isinstance(index, QModelIndex)
-        return index.internalPointer().childCount()
+        if not parent.isValid():
+            parentNode: TreeNode = self.mRootNode
+        else:
+            parentNode: TreeNode = parent.internalPointer()
 
-        node = self.idx2node(index)
-        #node = index.internalPointer()
-        return len(node.mChildren) if isinstance(node, TreeNode) else 0
+        return parentNode.childCount()
 
     def hasChildren(self, index=QModelIndex()) -> bool:
         """
@@ -725,31 +755,50 @@ class TreeModel(QAbstractItemModel):
         """
         return self.mColumnNames[:]
 
-    def idx2columnName(self, index:QModelIndex) -> str:
+    def printModel(self, index: QModelIndex, prefix=''):
         """
-        Returns the column name related to a QModelIndex
-        :param index: QModelIndex
-        :return: str, column name
+        Prints the model oder a sub-node specified by index
+        :param index:
+        :type index:
+        :param prefix:
+        :type prefix:
+        :return:
+        :rtype:
         """
-        if not index.isValid():
-            return None
-        else:
-            return self.mColumnNames[index.column()]
+        if index is None:
+            index = QModelIndex()
+        if isinstance(index, TreeNode):
+            index = self.node2idx(index)
+        print(f'{prefix} {self.data(index, role=Qt.DisplayRole)}')
+        for r in range(self.rowCount(index)):
+            idx = self.index(r, 0, parent=index)
+            self.printModel(idx, prefix=f'{prefix}-')
+
+    def span(self, idx) -> QSize():
 
-    def columnCount(self, index= QModelIndex()) -> int:
+        return super(TreeModel, self).span(idx)
+
+    def columnCount(self, parent: QModelIndex = None) -> int:
         """
         Returns the number of columns
         :param index: QModelIndex
         :return:
         """
-        return len(self.mColumnNames)
+        return len(self.mRootNode.values())
 
-    def connectTreeView(self, treeView):
-        self.mTreeView = treeView
+        """
+        if not isinstance(parent, QModelIndex):
+            parent = QModelIndex()
+        if not parent.isValid():
+            return len(self.rootNode().values())
 
+        parentNode: TreeNode = parent.internalPointer()
+        assert isinstance(parentNode, TreeNode)
 
+        return parentNode.columnCount()
+        """
 
-    def index(self, row:int, column:int, parent:QModelIndex=None) -> QModelIndex:
+    def index(self, row: int, column: int, parent: QModelIndex = None) -> QModelIndex:
         """
         Returns the QModelIndex
         :param row: int
@@ -758,21 +807,19 @@ class TreeModel(QAbstractItemModel):
         :return: QModelIndex
         """
         if parent is None:
-            parentNode = self.mRootNode
-        else:
-            parentNode = self.idx2node(parent)
+            parent = QModelIndex()
 
-        if row < 0 or row >= parentNode.childCount():
-            return QModelIndex()
-        if column < 0 or column >= len(self.mColumnNames):
-            return QModelIndex()
+        if not parent.isValid():
+            parentNode: TreeNode = self.mRootNode
+        else:
+            parentNode: TreeNode = parent.internalPointer()
 
-        if isinstance(parentNode, TreeNode) and row < len(parentNode.mChildren):
+        if len(parentNode.mChildren) > 0:
             return self.createIndex(row, column, parentNode.mChildren[row])
         else:
             return QModelIndex()
 
-    def findParentNode(self, node, parentNodeType) -> TreeNode:
+    def findParentNode(self, node: TreeNode, nodeType) -> TreeNode:
         """
         finds the next parent TreeNode of type `parentNodeType`
         :param node: TreeNode
@@ -780,14 +827,9 @@ class TreeModel(QAbstractItemModel):
         :return: TreeNode instance
         """
         assert isinstance(node, TreeNode)
-        while True:
-            if isinstance(node, parentNodeType):
-                return node
-            if not isinstance(node.parentNode(), TreeNode):
-                return None
-            node = node.parentNode()
+        return node.findParentNode(nodeType)
 
-    def indexes2nodes(self, indexes:list):
+    def indexes2nodes(self, indexes: list):
         """
         Returns the TreeNodes related to a list of QModelIndexes
         :param indexes: [list-of-QModelIndex]
@@ -801,7 +843,23 @@ class TreeModel(QAbstractItemModel):
                 nodes.append(n)
         return nodes
 
-    def nodes2indexes(self, nodes:list):
+    def removeNodes(self, nodes):
+        """
+        Removes nodes from the model
+        :param nodes:
+        :type nodes:
+        :return:
+        :rtype:
+        """
+        if isinstance(nodes, TreeNode):
+            nodes = [nodes]
+
+        for n in nodes:
+            idx = self.node2idx(n)
+            if idx.isValid():
+                n.parentNode().removeChildNodes(n)
+
+    def nodes2indexes(self, nodes: list):
         """
         Converts a list of TreeNodes into the corresponding list of QModelIndexes
         Set indexes2nodes
@@ -810,10 +868,9 @@ class TreeModel(QAbstractItemModel):
         """
         return [self.node2idx(n) for n in nodes]
 
-
     def expandNode(self, node, expand=True, recursive=True):
         assert isinstance(node, TreeNode)
-        if isinstance(self.mTreeView, QTreeView):
+        if False and isinstance(self.mTreeView, QTreeView):
             idx = self.node2idx(node)
             self.mTreeView.setExpanded(idx, expand)
 
@@ -821,42 +878,37 @@ class TreeModel(QAbstractItemModel):
                 for n in node.childNodes():
                     self.expandNode(n, expand=expand, recursive=recursive)
 
-
-    def idx2node(self, index:QModelIndex) -> TreeNode:
+    def idx2node(self, index: QModelIndex) -> TreeNode:
         """
         Returns the TreeNode related to an QModelIndex `index`.
         :param index: QModelIndex
         :return: TreeNode
         """
 
-        if index.row() == -1 and index.column() == -1:
-            return self.mRootNode
-        elif not index.isValid():
+        if not index.isValid():
             return self.mRootNode
         else:
             node = index.internalPointer()
             assert isinstance(node, TreeNode)
             return node
 
-    def node2idx(self, node:TreeNode) -> QModelIndex:
+    def node2idx(self, node: TreeNode) -> QModelIndex:
         """
         Returns a TreeNode's QModelIndex
         :param node: TreeNode
         :return: QModelIndex
         """
-        assert isinstance(node, TreeNode)
-        if node == self.mRootNode:
+
+        if node in [self.mRootNode, None]:
             return QModelIndex()
-            return self.createIndex(-1, -1, node)
         else:
-            parentNode = node.parentNode()
-            assert isinstance(parentNode, TreeNode)
-            if node not in parentNode.mChildren:
+            row: int = node.nodeIndex()
+            if not isinstance(row, int):
                 return QModelIndex()
-            r = parentNode.mChildren.index(node)
-            return self.createIndex(r, 0, node)
+            parentIndex = self.node2idx(node.parentNode())
+            return self.index(row, 0, parent=parentIndex)
 
-    def data(self, index, role):
+    def data(self, index: QModelIndex, role=Qt.DisplayRole):
         """
 
         :param index: QModelIndex
@@ -864,18 +916,16 @@ class TreeModel(QAbstractItemModel):
         :return: object
         """
         assert isinstance(index, QModelIndex)
-        if not index.isValid():
-            return None
-
-        node = self.idx2node(index)
-        #node = self.idx2node(index)
         node = index.internalPointer()
-        assert isinstance(node, TreeNode)
-        col = index.column()
+        if not isinstance(node, TreeNode):
+            return None
+        if node == self.rootNode():
+            s = ""
         if role == Qt.UserRole:
             return node
 
         col = index.column()
+
         if col == 0:
             if role in [Qt.DisplayRole, Qt.EditRole]:
                 return node.name()
@@ -884,35 +934,92 @@ class TreeModel(QAbstractItemModel):
             if role == Qt.ToolTipRole:
                 return node.toolTip()
         if col > 0:
+            # first column is for the node name, other columns are for node values
             i = col - 1
-            if role in [Qt.DisplayRole, Qt.EditRole] and len(node.values()) > i:
-                return str(node.values()[i])
+
+            if len(node.values()) > i:
+
+                if role == Qt.DisplayRole:
+                    return str(node.values()[i])
+                if role == Qt.EditRole:
+                    return node.values()[i]
+                if role == Qt.ToolTipRole:
+                    tt = [f'{i + 1}: {v}' for i, v in enumerate(node.values())]
+                    return '\n'.join(tt)
+        return None
 
     def flags(self, index):
         assert isinstance(index, QModelIndex)
         if not index.isValid():
             return Qt.NoItemFlags
-        node = self.idx2node(index)
-        #node = self.idx2node(index)
         return Qt.ItemIsEnabled | Qt.ItemIsSelectable
 
 
-
 class TreeView(QTreeView):
     """
     A basic QAbstractItemView implementation to realize TreeModels.
     """
+
     def __init__(self, *args, **kwds):
         super(TreeView, self).__init__(*args, **kwds)
 
+        self.mAutoExpansionDepth: int = 1
         self.mModel = None
+        self.mNodeExpansion: typing.Dict[str, bool] = dict()
+
+    def setAutoExpansionDepth(self, depth: int):
+        """
+        Sets the depth until which new TreeNodes will be opened
+        0 = Top nodes not expanded
+        1 = Top nodes expanded
+        2 = Top and Subnodes expanded
+        :param depth: int
+        """
+        assert isinstance(depth, int)
+        self.mAutoExpansionDepth = depth
+
+    def updateNodeExpansion(self, restore: bool,
+                            index: QModelIndex = None, prefix='') -> typing.Dict[str, bool]:
+        """
+        Allows to save and restore the state of node expansion
+        :param restore: bool, set True to save the state, False to restore it
+        :param index: QModelIndex()
+        :param prefix: string to identify the parent nodes
+        :return: Dict[str, bool] that stores the node expansion states
+        """
+        assert isinstance(restore, bool)
+
+        if not isinstance(index, QModelIndex):
+            index = QModelIndex()
+        if False and not (restore or index.isValid()):
+            self.mNodeExpansion.clear()
+
+        model: QAbstractItemModel = self.model()
+        if isinstance(model, QAbstractItemModel):
+            rows = model.rowCount(index)
+            if rows > 0:
+                nodeName = f'{prefix}:{model.data(index, role=Qt.DisplayRole)}'
+                nodeDepth: int = self.nodeDepth(index)
+
+                if restore:
+                    # restore expansion state, if stored in mNodeExpansion
+                    self.setExpanded(index, self.mNodeExpansion.get(nodeName, nodeDepth < self.mAutoExpansionDepth))
+                else:
+                    # save expansion state
+                    self.mNodeExpansion[nodeName] = self.isExpanded(index)
+
+                for row in range(rows):
+                    idx = model.index(row, 0, index)
+                    self.updateNodeExpansion(restore, index=idx, prefix=nodeName)
 
-    def setModel(self, model:QAbstractItemModel):
+        return self.mNodeExpansion
+
+    def setModel(self, model: QAbstractItemModel):
         """
         Sets the TreeModel
         :param model: TreeModel
         """
-        super(TreeView, self).setModel(model)
+        super().setModel(model)
 
         self.mModel = model
         if isinstance(self.mModel, QAbstractItemModel):
@@ -920,19 +1027,32 @@ class TreeView(QTreeView):
             self.mModel.dataChanged.connect(self.onDataChanged)
             self.mModel.rowsInserted.connect(self.onRowsInserted)
 
+        # update column spans
+        self.onModelReset()
+
+
     def onModelReset(self):
         for row in range(self.model().rowCount(QModelIndex())):
             idx = self.model().index(row, 0)
             self.setColumnSpan(idx)
 
-    def onRowsInserted(self, parent:QModelIndex, first:int, last:int):
+    def nodeDepth(self, index: QModelIndex) -> int:
+        if not index.isValid():
+            return 0
+        return 1 + self.nodeDepth(index.parent())
+
+    def onRowsInserted(self, parent: QModelIndex, first: int, last: int):
 
-        for row in range(first, last+1):
+        for row in range(first, last + 1):
             idx = self.model().index(row, 0, parent)
             self.setColumnSpan(idx)
 
-    def onDataChanged(self, tl:QModelIndex, br:QModelIndex, roles):
+        level = self.nodeDepth(parent)
+        if level < self.mAutoExpansionDepth:
+            self.setExpanded(idx, True)
+            s = ""
 
+    def onDataChanged(self, tl: QModelIndex, br: QModelIndex, roles):
 
         parent = tl.parent()
         for row in range(tl.row(), br.row() + 1):
@@ -940,11 +1060,36 @@ class TreeView(QTreeView):
             self.setColumnSpan(idx)
         s = ""
 
-    def setColumnSpan(self, idx:QModelIndex):
+    def setColumnSpan(self, idx: QModelIndex):
         """
         Sets the column span for index `idx` and all child widgets
         :param idx:
         :return:
+        """
+
+        assert isinstance(idx, QModelIndex)
+        if not idx.isValid():
+            return
+
+        colCnt = self.header().count()
+        #span: QSize = model.span(idx)
+        span: QSize = QSize(1, 1)
+        rightIdx = idx
+        while rightIdx.isValid() and rightIdx.column() < colCnt:
+            rightIdx = rightIdx.siblingAtColumn(rightIdx.column() + 1)
+            if rightIdx.data(Qt.DisplayRole) is None:
+                span.setWidth(span.width() + 1)
+            else:
+                break
+
+        if span.width() > 1:
+            self.setFirstColumnSpanned(idx.row(), idx.parent(), True)
+        for row in range(self.model().rowCount(idx)):
+            idx2 = self.model().index(row, 0, idx)
+            self.setColumnSpan(idx2)
+
+        return
+
         """
         assert isinstance(idx, QModelIndex)
         if not idx.isValid():
@@ -962,6 +1107,7 @@ class TreeView(QTreeView):
             for row in range(self.model().rowCount(idx)):
                 idx2 = self.model().index(row, 0, idx)
                 self.setColumnSpan(idx2)
+        """
 
     def selectedNode(self) -> TreeNode:
         """
@@ -975,8 +1121,6 @@ class TreeView(QTreeView):
 
         return None
 
-
-
     def selectedNodes(self) -> list:
         """
         Returns all selected TreeNodes
@@ -988,4 +1132,3 @@ class TreeView(QTreeView):
             if isinstance(node, TreeNode) and node not in nodes:
                 nodes.append(node)
         return nodes
-
diff --git a/eotimeseriesviewer/externals/qps/plotstyling/plotstyling.py b/eotimeseriesviewer/externals/qps/plotstyling/plotstyling.py
index 1d35684a..8c1e942a 100644
--- a/eotimeseriesviewer/externals/qps/plotstyling/plotstyling.py
+++ b/eotimeseriesviewer/externals/qps/plotstyling/plotstyling.py
@@ -643,10 +643,10 @@ class PlotStyle(QObject):
 
             hw, hh = int(w * 0.5), int(h * 0.5)
             w2, h2 = int(w * 0.75), int(h * 0.75)
-            # p.drawLine(x1,y1,x2,y2)
+            #p.drawLine(x1,y1,x2,y2)
 
-            #p.drawLine(2, h - 2, hw, hh)
-            #p.drawLine(hw, hh, w - 2, int(h * 0.3))
+            p.drawLine(2, h - 2, hw, hh)
+            p.drawLine(hw, hh, w - 2, int(h * 0.3))
 
             p.translate(pm.width() / 2, pm.height() / 2)
             drawSymbol(p, self.markerSymbol, self.markerSize, self.markerPen, self.markerBrush)
diff --git a/eotimeseriesviewer/externals/qps/simplewidgets.py b/eotimeseriesviewer/externals/qps/simplewidgets.py
new file mode 100644
index 00000000..6f8a51ac
--- /dev/null
+++ b/eotimeseriesviewer/externals/qps/simplewidgets.py
@@ -0,0 +1,133 @@
+from qgis.PyQt.QtCore import pyqtSignal, Qt
+
+from qgis.PyQt.QtWidgets import QWidget, QAbstractSpinBox, QSpinBox, QDoubleSpinBox, \
+    QHBoxLayout, QVBoxLayout, QSlider
+
+
+class SliderSpinBox(QWidget):
+    sigValueChanged = pyqtSignal(int)
+
+    def __init__(self, *args,
+                 spinbox: QAbstractSpinBox = None,
+                 spinbox_position: Qt.Alignment = Qt.AlignLeft,
+                 **kwds):
+
+        if not isinstance(spinbox, QAbstractSpinBox):
+            spinbox = QSpinBox()
+        assert isinstance(spinbox, QAbstractSpinBox)
+        assert isinstance(spinbox_position, Qt.AlignmentFlag)
+
+        super().__init__(*args, **kwds)
+
+        self.spinbox: QAbstractSpinBox = spinbox
+        self.slider: QSlider = QSlider(Qt.Horizontal)
+        self.slider.valueChanged.connect(self.onSliderValueChanged)
+        self.spinbox.valueChanged.connect(self.onSpinboxValueChanged)
+
+        if spinbox_position in [Qt.AlignLeft, Qt.AlignRight]:
+            l = QHBoxLayout()
+        elif spinbox_position in [Qt.AlignTop, Qt.AlignBottom]:
+            l = QVBoxLayout()
+        else:
+            raise NotImplementedError()
+
+        if spinbox_position in [Qt.AlignLeft, Qt.AlignTop]:
+            l.addWidget(self.spinbox)
+            l.addWidget(self.slider)
+        else:
+            l.addWidget(self.slider)
+            l.addWidget(self.spinbox)
+
+        l.setContentsMargins(0, 0, 0, 0)
+        l.setSpacing(2)
+
+        self.setLayout(l)
+
+    def onSliderValueChanged(self, value):
+        v = self.slider2spinboxvalue(value)
+        self.spinbox.setValue(v)
+
+    def onSpinboxValueChanged(self, value):
+        v = self.spinbox2slidervalue(value)
+        if v != self.slider.value():
+            self.slider.setValue(v)
+
+        self.sigValueChanged.emit(value)
+
+    def setSingleStep(self, value):
+        self.spinbox.setSingleStep(value)
+        self.slider.setSingleStep(value)
+        self.slider.setPageStep(value * 10)
+
+    def singleStep(self):
+        return self.spinbox.singleStep()
+
+    def setMinimum(self, value):
+        self.spinbox.setMinimum(value)
+        self.slider.setMinimum(self.spinbox2slidervalue(value))
+
+    def spinbox2slidervalue(self, value):
+        return value
+
+    def slider2spinboxvalue(self, value):
+        return value
+
+    def setMaximum(self, value):
+        self.spinbox.setMaximum(value)
+        self.slider.setMaximum(self.spinbox2slidervalue(value))
+
+    def maximum(self) -> int:
+        return self.spinbox.maximum()
+
+    def minimum(self) -> int:
+        return self.spinbox.minimum()
+
+    def setValue(self, value: float):
+        self.spinbox.setValue(value)
+
+    def value(self) -> float:
+        return self.spinbox.value()
+
+    def setRange(self, vmin, vmax):
+        vmin = min(vmin, vmax)
+        vmax = max(vmin, vmax)
+        assert vmin <= vmax
+
+        self.setMinimum(vmin)
+        self.setMaximum(vmax)
+
+
+class DoubleSliderSpinBox(SliderSpinBox):
+    sigValueChanged = pyqtSignal(float)
+
+    def __init__(self, *args, **kwds):
+        spinbox = QDoubleSpinBox()
+        super().__init__(*args, spinbox=spinbox, **kwds)
+
+        self.spinbox: QDoubleSpinBox
+        assert isinstance(self.spinbox, QDoubleSpinBox)
+        self.setDecimals(2)
+        self.setMinimum(0)
+        self.setMaximum(1)
+        self.setSingleStep(0.1)
+
+    def spinbox2slidervalue(self, value: float) -> int:
+        v = int(round(10 ** self.decimals() * value))
+        return v
+
+    def slider2spinboxvalue(self, value: int) -> float:
+        v = value / (10 ** self.decimals())
+        return v
+
+    def setDecimals(self, value: int):
+        self.spinbox.setDecimals(value)
+        self.setSingleStep(self.spinbox.singleStep())
+
+    def decimals(self) -> int:
+        return self.spinbox.decimals()
+
+    def setSingleStep(self, value):
+        self.spinbox.setSingleStep(value)
+        m = int(10 ** self.decimals() * value)
+        self.slider.setSingleStep(m)
+        self.slider.setPageStep(m * 10)
\ No newline at end of file
diff --git a/eotimeseriesviewer/externals/qps/speclib/__init__.py b/eotimeseriesviewer/externals/qps/speclib/__init__.py
index ff3a890d..dff57628 100644
--- a/eotimeseriesviewer/externals/qps/speclib/__init__.py
+++ b/eotimeseriesviewer/externals/qps/speclib/__init__.py
@@ -27,13 +27,11 @@
 ***************************************************************************
 """
 import enum
-from qgis.core import *
-
-from qgis.PyQt.QtCore import QSettings
-
+from qgis.core import QgsSettings
 
 EDITOR_WIDGET_REGISTRY_KEY = 'Spectral Profile'
 
+
 class SpectralLibrarySettingsKey(enum.Enum):
     CURRENT_PROFILE_STYLE = 1
     DEFAULT_PROFILE_STYLE = 2
@@ -44,17 +42,15 @@ class SpectralLibrarySettingsKey(enum.Enum):
     SELECTION_COLOR = 7
 
 
-def speclibSettings() -> QSettings:
+def speclibSettings() -> QgsSettings:
     """
     Returns SpectralLibrary relevant QSettings
     :return: QSettings
     """
     return QgsSettings('HUB', 'speclib')
 
+
 try:
     from ..speclib.io.envi import EnviSpectralLibraryIO
 except:
     pass
-
-
-
diff --git a/eotimeseriesviewer/externals/qps/speclib/core.py b/eotimeseriesviewer/externals/qps/speclib/core.py
index 21032653..6b58923b 100644
--- a/eotimeseriesviewer/externals/qps/speclib/core.py
+++ b/eotimeseriesviewer/externals/qps/speclib/core.py
@@ -31,6 +31,7 @@ import enum
 import pickle
 import typing
 import pathlib
+import collections
 import uuid
 from osgeo import osr
 from ..speclib import SpectralLibrarySettingsKey
@@ -100,9 +101,6 @@ class SerializationMode(enum.Enum):
     PICKLE = 2
 
 
-SERIALIZATION = SerializationMode.PICKLE
-
-
 def log(msg: str):
     if DEBUG:
         QgsMessageLog.logMessage(msg, 'spectrallibraries.py')
@@ -390,13 +388,16 @@ def toType(t, arg, empty2None=True):
             return t(arg)
 
 
-def encodeProfileValueDict(d: dict, mode: SerializationMode = SerializationMode.PICKLE) -> str:
+def encodeProfileValueDict(d: dict, mode=None) -> QByteArray:
     """
     Converts a SpectralProfile value dictionary into a compact JSON string, which can be
     extracted with `decodeProfileValueDict`.
     :param d: dict
     :return: str
     """
+    if mode is not None:
+        warnings.warn('keyword "mode" is not not used anymore', DeprecationWarning)
+
     if not isinstance(d, dict):
         return None
     d2 = {}
@@ -405,30 +406,22 @@ def encodeProfileValueDict(d: dict, mode: SerializationMode = SerializationMode.
         # save keys with information only
         if v is not None:
             d2[k] = v
-    if mode == SerializationMode.JSON:
-        return json.dumps(d2, sort_keys=True, separators=(',', ':'))
-    elif mode == SerializationMode.PICKLE:
-        return QByteArray(pickle.dumps(d2))
-    else:
-        raise NotImplementedError()
+    return QByteArray(pickle.dumps(d2))
 
 
-def decodeProfileValueDict(dump, mode: SerializationMode = SerializationMode.PICKLE):
+def decodeProfileValueDict(dump, mode=None):
     """
     Converts a json / pickle dump  into a SpectralProfile value dictionary
     :param dump: str
     :return: dict
     """
+    if mode is not None:
+        warnings.warn('keyword "mode" is not used anymore', DeprecationWarning)
+
     d = EMPTY_PROFILE_VALUES.copy()
 
     if dump not in EMPTY_VALUES:
-        d2 = None
-        if mode == SerializationMode.JSON:
-            d2 = json.loads(dump)
-        elif mode == SerializationMode.PICKLE:
-            d2 = pickle.loads(dump)
-        else:
-            raise NotImplementedError()
+        d2 = pickle.loads(dump)
         d.update(d2)
     return d
 
@@ -477,25 +470,35 @@ LUT_IDL2GDAL = {1: gdal.GDT_Byte,
 
 
 def ogrStandardFields() -> list:
-    """Returns the minimum set of field a Spectral Library has to contain"""
+    """Returns the minimum set of field a Spectral Library contains"""
     fields = [
         ogr.FieldDefn(FIELD_FID, ogr.OFTInteger),
         ogr.FieldDefn(FIELD_NAME, ogr.OFTString),
         ogr.FieldDefn('source', ogr.OFTString),
-        ogr.FieldDefn(FIELD_VALUES, ogr.OFTString) \
-            if SERIALIZATION == SerializationMode.JSON else \
-            ogr.FieldDefn(FIELD_VALUES, ogr.OFTBinary),
+        ogr.FieldDefn(FIELD_VALUES, ogr.OFTBinary),
     ]
     return fields
 
 
-def createStandardFields():
+def spectralValueFields(spectralLibrary: QgsVectorLayer) -> typing.List[QgsField]:
+    """
+    Returns the fields that contains values of SpectralProfiles
+    :param spectralLibrary:
+    :return:
+    """
+    fields = [f for f in spectralLibrary.fields() if
+              f.type() == QVariant.ByteArray and
+              f.editorWidgetSetup().type() == EDITOR_WIDGET_REGISTRY_KEY]
+
+    return fields
+
+
+def createStandardFields() -> QgsFields:
     fields = QgsFields()
     for f in ogrStandardFields():
         assert isinstance(f, ogr.FieldDefn)
         name = f.GetName()
         ogrType = f.GetType()
-
         if ogrType == ogr.OFTString:
             a, b = QVariant.String, 'varchar'
         elif ogrType in [ogr.OFTInteger, ogr.OFTInteger64]:
@@ -681,33 +684,57 @@ class SpectralProfile(QgsFeature):
         return profile
 
     @staticmethod
-    def fromSpecLibFeature(feature: QgsFeature):
+    def fromQgsFeature(feature: QgsFeature, value_field: str = FIELD_VALUES):
         """
         Converts a QgsFeature into a SpectralProfile
         :param feature: QgsFeature
-        :return: SpectralProfile
+        :param value_field: name of QgsField that stores the Spectral Profile BLOB
+        :return:
         """
         assert isinstance(feature, QgsFeature)
-        sp = SpectralProfile(fields=feature.fields())
+        if isinstance(value_field, QgsField):
+            value_field = value_field.name()
+        assert value_field in feature.fields().names(), f'field "{value_field}" does not exist'
+        sp = SpectralProfile(fields=feature.fields(), value_field=value_field)
         sp.setId(feature.id())
         sp.setAttributes(feature.attributes())
         sp.setGeometry(feature.geometry())
         return sp
 
-    def __init__(self, parent=None, fields=None, values: dict = None):
+    @staticmethod
+    def fromSpecLibFeature(feature: QgsFeature):
+        """
+        Converts a QgsFeature into a SpectralProfile
+        :param feature: QgsFeature
+        :return: SpectralProfile
+        """
+        warnings.warn('Use SpectralProfile.fromQgsFeature instead', DeprecationWarning)
+        return SpectralProfile.fromQgsFeature(feature)
+
+    def __init__(self, parent=None,
+                 fields: QgsFields = None,
+                 values: dict = None,
+                 value_field: typing.Union[str, QgsField] = FIELD_VALUES):
+        """
+        :param parent:
+        :param fields:
+        :param values:
+        :param value_field: name or index of field that contains the spectral values information.
+                            Needs to be a BLOB field.
+        """
 
         if fields is None:
             fields = createStandardFields()
         assert isinstance(fields, QgsFields)
-        # QgsFeature.__init__(self, fields)
-        # QObject.__init__(self)
         super(SpectralProfile, self).__init__(fields)
-        # QObject.__init__(self)
-        # fields = self.fields()
 
         assert isinstance(fields, QgsFields)
         self.mValueCache = None
-        # self.setStyle(DEFAULT_SPECTRUM_STYLE)
+        if isinstance(value_field, QgsField):
+            value_field = value_field.name()
+
+        self.mValueField: str = value_field
+
         if isinstance(values, dict):
             self.setValues(**values)
 
@@ -770,7 +797,6 @@ class SpectralProfile(QgsFeature):
     def setName(self, name: str):
         if name != self.name():
             self.setAttribute(FIELD_NAME, name)
-            # self.sigNameChanged.emit(name)
 
     def name(self) -> str:
         return self.metadata(FIELD_NAME)
@@ -788,10 +814,17 @@ class SpectralProfile(QgsFeature):
         elif isinstance(pt, QgsPointXY):
             self.setGeometry(QgsGeometry.fromPointXY(pt))
 
+    def key(self) -> typing.Tuple[int, any]:
+        """
+        Returns a key tuple consisting of the profiles feature id and the columns that stores the profile data
+        :return:
+        """
+        return (self.id(), self.mValueField)
+
     def geoCoordinate(self):
         return self.geometry()
 
-    def updateMetadata(self, metaData):
+    def updateMetadata(self, metaData:dict):
         if isinstance(metaData, dict):
             for key, value in metaData.items():
                 self.setMetadata(key, value)
@@ -853,21 +886,28 @@ class SpectralProfile(QgsFeature):
         """
         return len(self.yValues())
 
+    def isEmpty(self) -> bool:
+        """
+        Returns True if there is not ByteArray stored in the BLOB value field
+        :return:
+        """
+        return self.attribute(self.fields().indexFromName(self.mValueField)) in [None, QVariant()]
+
     def values(self) -> dict:
         """
         Returns a dictionary with 'x', 'y', 'xUnit' and 'yUnit' values.
         :return: {'x':list,'y':list,'xUnit':str,'yUnit':str, 'bbl':list}
         """
         if self.mValueCache is None:
-            jsonStr = self.attribute(self.fields().indexFromName(FIELD_VALUES))
-            d = decodeProfileValueDict(jsonStr)
+            byteArray = self.attribute(self.fields().indexFromName(self.mValueField))
+            d = decodeProfileValueDict(byteArray)
 
             # save a reference to the decoded dictionary
             self.mValueCache = d
 
         return self.mValueCache
 
-    def setValues(self, x=None, y=None, xUnit=None, yUnit=None, bbl=None):
+    def setValues(self, x=None, y=None, xUnit: str = None, yUnit: str = None, bbl=None, **kwds):
 
         d = self.values().copy()
 
@@ -911,7 +951,7 @@ class SpectralProfile(QgsFeature):
         if isinstance(yUnit, str):
             d['yUnit'] = yUnit
 
-        self.setAttribute(FIELD_VALUES, encodeProfileValueDict(d))
+        self.setAttribute(self.mValueField, encodeProfileValueDict(d))
         self.mValueCache = d
 
     def xValues(self) -> list:
@@ -1286,7 +1326,7 @@ class SpectralProfileRenderer(object):
             customStyleNode = customStyleNodes.at(i)
             customStyle = PlotStyle.readXml(customStyleNode)
             if isinstance(customStyle, PlotStyle):
-                fids = customStyleNode.firstChildElement('fids').firstChild().nodeValue().split(',')
+                fids = customStyleNode.firstChildElement('keys').firstChild().nodeValue().split(',')
                 fids = [int(f) for f in fids]
                 renderer.setProfilePlotStyle(customStyle, fids)
 
@@ -1326,7 +1366,7 @@ class SpectralProfileRenderer(object):
             fids = [k for k, s in self.mFID2Style.items() if s == style]
             nodeStyle = doc.createElement('custom_style')
             style.writeXml(nodeStyle, doc)
-            nodeFIDs = doc.createElement('fids')
+            nodeFIDs = doc.createElement('keys')
             nodeFIDs.appendChild(doc.createTextNode(','.join([str(i) for i in fids])))
             nodeStyle.appendChild(nodeFIDs)
             nodeCustomStyles.appendChild(nodeStyle)
@@ -2155,8 +2195,9 @@ class SpectralLibrary(QgsVectorLayer):
                  path: str = None,
                  baseName: str = DEFAULT_NAME,
                  options: QgsVectorLayer.LayerOptions = None,
+                 value_fields=[FIELD_VALUES],
                  uri: str = None,  # deprectated
-                 name: str = None  # deprectated
+                 name: str = None,  # deprectated
                  ):
 
         if isinstance(uri, str):
@@ -2172,6 +2213,9 @@ class SpectralLibrary(QgsVectorLayer):
         if isinstance(path, pathlib.Path):
             path = path.as_posix()
 
+        if not isinstance(options, QgsVectorLayer.LayerOptions):
+            options = QgsVectorLayer.LayerOptions(loadDefaultStyle=True, readExtentFromXml=True)
+
         if path is None:
             # create a new, empty backend
             existing_vsi_files = vsiSpeclibs()
@@ -2204,9 +2248,15 @@ class SpectralLibrary(QgsVectorLayer):
             assert isinstance(lyr, ogr.Layer)
             ldefn = lyr.GetLayerDefn()
             assert isinstance(ldefn, ogr.FeatureDefn)
+            fieldNames = []
             for f in ogrStandardFields():
+                fieldNames.append(f.GetName())
                 lyr.CreateField(f)
 
+            for f in value_fields:
+                if f not in fieldNames:
+                    lyr.CreateField(ogr.FieldDefn(f, ogr.OFTBinary))
+
             try:
                 dsSrc.FlushCache()
             except RuntimeError as rt:
@@ -2216,25 +2266,47 @@ class SpectralLibrary(QgsVectorLayer):
                     raise rt
 
         assert isinstance(path, str)
-        options = QgsVectorLayer.LayerOptions(loadDefaultStyle=False, readExtentFromXml=False)
         super(SpectralLibrary, self).__init__(path, baseName, 'ogr', options)
 
         # consistency check
         field_names = self.fields().names()
         assert FIELD_NAME in field_names
-        assert FIELD_VALUES in field_names
         f = self.fields().at(self.fields().lookupField(FIELD_NAME))
-        assert f.type() == QVariant.String, 'Field {} not of type String / VARCHAR'
-        f = self.fields().at(self.fields().lookupField(FIELD_VALUES))
-        assert f.type() == QVariant.ByteArray, 'Field {} not of type ByteArray / BLOB'
+        assert f.type() == QVariant.String, f'Field {f.name()} not of type String / VARCHAR'
+
+        for n in value_fields:
+            assert n in field_names
+            f = self.fields().at(self.fields().lookupField(n))
+            assert f.type() == QVariant.ByteArray, f'Field {n} not of type ByteArray / BLOB'
 
         # self.beforeCommitChanges.connect(self.onBeforeCommitChanges)
+
         self.committedFeaturesAdded.connect(self.onCommittedFeaturesAdded)
         self.mProfileRenderer: SpectralProfileRenderer = SpectralProfileRenderer()
         self.mProfileRenderer.setInput(self)
+
+        self.mSpectralValueFields: typing.List[QgsField] = []
+        self.attributeAdded.connect(self.onAttributeAdded)
+        self.attributeDeleted.connect(self.onFieldsChanged)
+
+        # set special default editors
+        for f in value_fields:
+            self.setEditorWidgetSetup(self.fields().lookupField(f),
+                                      QgsEditorWidgetSetup(EDITOR_WIDGET_REGISTRY_KEY, {}))
+
         self.initTableConfig()
         self.initProfileRenderer()
 
+    def onAttributeAdded(self, idx:int):
+
+        field: QgsField = self.fields().at(idx)
+        if field.type() == QVariant.ByteArray:
+            # let new ByteArray fields be SpectralProfile columns by default
+            self.setEditorWidgetSetup(idx, QgsEditorWidgetSetup(EDITOR_WIDGET_REGISTRY_KEY, {}))
+
+    def onFieldsChanged(self):
+        self.mSpectralValueFields = spectralValueFields(self)
+
     def onCommittedFeaturesAdded(self, id, features):
 
         if id != self.id():
@@ -2250,6 +2322,10 @@ class SpectralLibrary(QgsVectorLayer):
                 updates[fidNew] = mFID2Style.pop(fidOld)
         mFID2Style.update(updates)
 
+    def setEditorWidgetSetup(self, index: int, setup: QgsEditorWidgetSetup):
+        super().setEditorWidgetSetup(index, setup)
+        self.onFieldsChanged()
+
     def setProfileRenderer(self, profileRenderer: SpectralProfileRenderer):
         assert isinstance(profileRenderer, SpectralProfileRenderer)
         b = profileRenderer != self.mProfileRenderer
@@ -2313,12 +2389,6 @@ class SpectralLibrary(QgsVectorLayer):
 
         self.setAttributeTableConfig(conf)
 
-        # set special default editors
-        # self.setEditorWidgetSetup(self.fields().lookupField(FIELD_STYLE), QgsEditorWidgetSetup(PlotSettingsEditorWidgetKey, {}))
-
-        self.setEditorWidgetSetup(self.fields().lookupField(FIELD_VALUES),
-                                  QgsEditorWidgetSetup(EDITOR_WIDGET_REGISTRY_KEY, {}))
-
     def mimeData(self, formats: list = None) -> QMimeData:
         """
         Wraps this Speclib into a QMimeData object
@@ -2350,36 +2420,23 @@ class SpectralLibrary(QgsVectorLayer):
 
         return mimeData
 
-    def optionalFields(self) -> list:
-        """
-        Returns the list of optional fields that are not part of the standard field set.
-        :return: [list-of-QgsFields]
-        """
-        standardFields = createStandardFields()
-        return [f for f in self.fields() if f not in standardFields]
-
     def optionalFieldNames(self) -> list:
         """
         Returns the names of additions fields / attributes
         :return: [list-of-str]
         """
-        requiredFields = [f.name for f in ogrStandardFields()]
-        return [n for n in self.fields().names() if n not in requiredFields]
+        warnings.warn('Deprecated and desimplemented', DeprecationWarning)
+        # requiredFields = [f.name for f in ogrStandardFields()]
+        return []
 
-    """
-    def initConditionalStyles(self):
-        styles = self.conditionalStyles()
-        assert isinstance(styles, QgsConditionalLayerStyles)
-
-        for fieldName in self.fieldNames():
-            red = QgsConditionalStyle("@value is NULL")
-            red.setTextColor(QColor('red'))
-            styles.setFieldStyles(fieldName, [red])
-
-        red = QgsConditionalStyle('"__serialized__xvalues" is NULL OR "__serialized__yvalues is NULL" ')
-        red.setBackgroundColor(QColor('red'))
-        styles.setRowStyles([red])
-    """
+    def addSpectralProfileAttribute(self, name: str, comment: str = None) -> bool:
+
+        field = QgsField(name, QVariant.ByteArray, 'Binary', comment=comment)
+        b = self.addAttribute(field)
+        if b:
+            self.setEditorWidgetSetup(self.fields().lookupField(field.name()),
+                                      QgsEditorWidgetSetup(EDITOR_WIDGET_REGISTRY_KEY, {}))
+        return b
 
     def addMissingFields(self, fields: QgsFields, copyEditorWidgetSetup: bool = True):
         """
@@ -2559,20 +2616,53 @@ class SpectralLibrary(QgsVectorLayer):
             for fid in fids:
                 assert isinstance(fid, int)
             featureRequest.setFilterFids(fids)
-        # features = [f for f in self.features() if f.id() in fidsToRemove]
+        # features = [f for f in self.features() if f.id() in keys_to_remove]
         return self.getFeatures(featureRequest)
 
-    def profile(self, fid: int) -> SpectralProfile:
-        return SpectralProfile.fromSpecLibFeature(self.getFeature(fid))
-
-    def profiles(self, fids=None) -> typing.Generator[SpectralProfile, None, None]:
-        """
-        Like features(fidsToRemove=None), but converts each returned QgsFeature into a SpectralProfile
+    def profile(self, fid: int, value_field=None) -> SpectralProfile:
+        if value_field is None:
+            value_field = self.spectralValueFields()[0]
+        return SpectralProfile.fromQgsFeature(self.getFeature(fid), value_field=value_field)
+
+    def profiles(self,
+                 fids=None,
+                 value_fields=None,
+                 profile_keys: typing.Tuple[int, str]=None) -> typing.Generator[SpectralProfile, None, None]:
+        """
+        Like features(keys_to_remove=None), but converts each returned QgsFeature into a SpectralProfile.
+        If multiple value fields are set, profiles are returned ordered by (i) fid and (ii) value field.
+        :param value_fields:
+        :type value_fields:
+        :param profile_keys:
+        :type profile_keys:
         :param fids: optional, [int-list-of-feature-ids] to return
         :return: generator of [List-of-SpectralProfiles]
         """
-        for f in self.features(fids=fids):
-            yield SpectralProfile.fromSpecLibFeature(f)
+
+        if profile_keys is None:
+            if value_fields is None:
+                value_fields = [f.name() for f in self.spectralValueFields()]
+            elif not isinstance(value_fields, list):
+                value_fields = [value_fields]
+
+            for f in self.features(fids=fids):
+                for field in value_fields:
+                    yield SpectralProfile.fromQgsFeature(f, value_field=field)
+        else:
+            # sort by FID
+            LUT_FID2KEYS = dict()
+            for pkey in profile_keys:
+                fid, field = pkey
+
+                fields = LUT_FID2KEYS.get(fid, [])
+                fields.append(field)
+                LUT_FID2KEYS[fid] = fields
+
+            for f in self.features(fids=list(LUT_FID2KEYS.keys())):
+                assert isinstance(f, QgsFeature)
+                for field in LUT_FID2KEYS[f.id()]:
+                    yield SpectralProfile.fromQgsFeature(f, value_field=field)
+
 
     def groupBySpectralProperties(self, excludeEmptyProfiles: bool = True):
         """
@@ -2621,10 +2711,9 @@ class SpectralLibrary(QgsVectorLayer):
         msg = super(SpectralLibrary, self).exportNamedStyle(doc, context=context, categories=categories)
         if msg == '':
             qgsNode = doc.documentElement().toElement()
-            # speclibNode = doc.createElement(XMLNODE_PROFILE_RENDERER)
+
             if isinstance(self.mProfileRenderer, SpectralProfileRenderer):
                 self.mProfileRenderer.writeXml(qgsNode, doc)
-            # qgsNode.appendChild(speclibNode)
 
         return msg
 
@@ -2726,6 +2815,9 @@ class SpectralLibrary(QgsVectorLayer):
 
         return []
 
+    def spectralValueFields(self) -> typing.List[QgsField]:
+        return self.mSpectralValueFields
+
     def yRange(self) -> typing.List[float]:
         """
         Returns the maximum y range
@@ -2837,17 +2929,15 @@ class SpectralLibrary(QgsVectorLayer):
         return max(cnt, 0)
 
     def __iter__(self):
-        r = QgsFeatureRequest()
-        for f in self.getFeatures(r):
-            yield SpectralProfile.fromSpecLibFeature(f)
+        return self.profiles()
 
     def __getitem__(self, slice) -> typing.Union[SpectralProfile, typing.List[SpectralProfile]]:
         fids = sorted(self.allFeatureIds())[slice]
-
+        value_field = self.mSpectralValueFields[0].name()
         if isinstance(fids, list):
             return sorted(self.profiles(fids=fids), key=lambda p: p.id())
         else:
-            return SpectralProfile.fromSpecLibFeature(self.getFeature(fids))
+            return SpectralProfile.fromQgsFeature(self.getFeature(fids), value_field=value_field)
 
     def __delitem__(self, slice):
         profiles = self[slice]
diff --git a/eotimeseriesviewer/externals/qps/speclib/function_help/format_py.json b/eotimeseriesviewer/externals/qps/speclib/function_help/format_py.json
new file mode 100644
index 00000000..5a8489a4
--- /dev/null
+++ b/eotimeseriesviewer/externals/qps/speclib/function_help/format_py.json
@@ -0,0 +1,12 @@
+{
+  "name": "format_py",
+  "type": "function",
+  "description": "Creates a string from inputs, using the <a href=\"https://docs.python.org/3/library/string.html#format-examples\">python format syntax</a>.",
+  "arguments": [ {"arg":"fmt","description":"python format string"},
+                 {"arg":"arg1","description":"1st argument to format string", "optional":"true"},
+                 {"arg":"arg2","description":"2nd argument to format string", "optional":"true"},
+                 {"arg":"argN","description":"N argument to format string", "optional":"true"}
+              ],
+  "examples": [ { "expression":"format_py('foobar{}',42)", "returns":"'foobar42'"},
+                { "expression":"format_py('foobar{:04}',42)", "returns":"'foobar0042'"}]
+}
diff --git a/eotimeseriesviewer/externals/qps/speclib/function_help/spectraldata.json b/eotimeseriesviewer/externals/qps/speclib/function_help/spectraldata.json
new file mode 100644
index 00000000..1f7599ca
--- /dev/null
+++ b/eotimeseriesviewer/externals/qps/speclib/function_help/spectraldata.json
@@ -0,0 +1,8 @@
+{
+  "name": "spectralData",
+  "type": "function",
+  "description": "Returns values of a spectral profile as map",
+  "arguments": [ {"arg":"field","description":"binary data column (BLOB)"}
+                 ],
+  "examples": [ { "expression":"spectralData(field=\"values\")", "returns":"a map with spectral data from column \"values\""}]
+}
diff --git a/eotimeseriesviewer/externals/qps/speclib/function_help/spectralmath.json b/eotimeseriesviewer/externals/qps/speclib/function_help/spectralmath.json
new file mode 100644
index 00000000..2eec5570
--- /dev/null
+++ b/eotimeseriesviewer/externals/qps/speclib/function_help/spectralmath.json
@@ -0,0 +1,8 @@
+{
+  "name": "spectralMath",
+  "type": "function",
+  "description": "Create or modifies data of a SpectralProfile with python",
+  "arguments": [ {"arg":"expression","description":"a python expression to set the profile data"},
+                 {"arg":"field","description":"field to load existing spectral profile data from"}],
+  "examples": [ { "expression":"array_prepend(array(1,2,3),0)", "returns":"[ 0, 1, 2, 3 ]"}]
+}
diff --git a/eotimeseriesviewer/externals/qps/speclib/gui.py b/eotimeseriesviewer/externals/qps/speclib/gui.py
index 47187307..c32f4e2d 100644
--- a/eotimeseriesviewer/externals/qps/speclib/gui.py
+++ b/eotimeseriesviewer/externals/qps/speclib/gui.py
@@ -23,6 +23,8 @@
     along with this software. If not, see <http://www.gnu.org/licenses/>.
 ***************************************************************************
 """
+from typing import List, Tuple
+
 import sip
 import textwrap
 from .core import *
@@ -33,24 +35,26 @@ from ..externals import pyqtgraph as pg
 from ..externals.pyqtgraph.graphicsItems.ViewBox.ViewBoxMenu import ViewBoxMenu
 from ..externals.pyqtgraph.graphicsItems.PlotDataItem import PlotDataItem
 from ..layerproperties import AttributeTableWidget
-from ..unitmodel import UnitModel, BAND_INDEX, XUnitModel, UnitConverterFunctionModel
+from ..unitmodel import BAND_INDEX, XUnitModel, UnitConverterFunctionModel
 
-from ..models import Option, OptionListModel
 from ..plotstyling.plotstyling import PlotStyleWidget, PlotStyle, PlotStyleDialog
-from ..layerproperties import AddAttributeDialog
+
 from qgis.core import \
-    QgsFeature, QgsRenderContext, QgsNullSymbolRenderer, \
-    QgsRasterLayer, QgsMapLayer, QgsVectorLayer, \
+    QgsFeature, QgsRenderContext, QgsNullSymbolRenderer, QgsFieldFormatter, QgsApplication, \
+    QgsRasterLayer, QgsMapLayer, QgsVectorLayer, QgsFieldFormatterRegistry, \
     QgsSymbol, QgsMarkerSymbol, QgsLineSymbol, QgsFillSymbol, \
-    QgsAttributeTableConfig, QgsField, QgsMapLayerProxyModel, QgsFileUtils
+    QgsAttributeTableConfig, QgsField, QgsMapLayerProxyModel, QgsFileUtils, \
+    QgsExpression, QgsFieldProxyModel
+
 from qgis.gui import \
     QgsEditorWidgetWrapper, QgsAttributeTableView, \
     QgsActionMenu, QgsEditorWidgetFactory, QgsStatusBar, \
     QgsDualView, QgsGui, QgisInterface, QgsMapCanvas, QgsDockWidget, QgsEditorConfigWidget, \
-    QgsAttributeTableFilterModel
-
+    QgsAttributeTableFilterModel, QgsFieldExpressionWidget
 
 SPECTRAL_PROFILE_EDITOR_WIDGET_FACTORY: None
+SPECTRAL_PROFILE_FIELD_FORMATTER: None
+SPECTRAL_PROFILE_FIELD_REPRESENT_VALUE = 'Profile'
 
 
 class SpectralXAxis(pg.AxisItem):
@@ -181,7 +185,8 @@ class SpectralProfileRendererWidget(QWidget):
 
         self.btnColorSchemeBright.setDefaultAction(self.actionActivateBrightTheme)
         self.btnColorSchemeDark.setDefaultAction(self.actionActivateDarkTheme)
-        self.actionActivateBrightTheme.triggered.connect(lambda: self.setRendererTheme(SpectralProfileRenderer.bright()))
+        self.actionActivateBrightTheme.triggered.connect(
+            lambda: self.setRendererTheme(SpectralProfileRenderer.bright()))
         self.actionActivateDarkTheme.triggered.connect(lambda: self.setRendererTheme(SpectralProfileRenderer.dark()))
 
     def setResetRenderer(self, profileRenderer: SpectralProfileRenderer):
@@ -254,7 +259,7 @@ class SpectralProfileRendererWidget(QWidget):
         cs.infoColor = self.btnColorInfo.color()
         cs.selectionColor = self.btnColorSelection.color()
         cs.profileStyle = self.wDefaultProfileStyle.plotStyle()
-        #if isinstance(self.mLastRenderer, SpectralProfileRenderer):
+        # if isinstance(self.mLastRenderer, SpectralProfileRenderer):
         #    cs.temporaryProfileStyle = self.mLastRenderer.temporaryProfileStyle.clone()
         #    cs.mFID2Style.update(self.mLastRenderer.mFID2Style)
         cs.useRendererColors = self.optionUseColorsFromVectorRenderer.isChecked()
@@ -277,12 +282,12 @@ class SpectralProfilePlotDataItem(PlotDataItem):
         self.curve.mouseClickEvent = self.onCurveMouseClickEvent
         self.scatter.sigClicked.connect(self.onScatterMouseClicked)
 
-        self.mValueConversionIsPossible : bool = True
+        self.mValueConversionIsPossible: bool = True
         self.mXValueConversionFunction = lambda v, *args: v
         self.mYValueConversionFunction = lambda v, *args: v
         self.mSortByXValues: bool = False
 
-        #self.mDefaultStyle = PlotStyle()
+        # self.mDefaultStyle = PlotStyle()
 
         self.mProfileSource = None
 
@@ -471,9 +476,12 @@ class SpectralProfilePlotDataItem(PlotDataItem):
         pw.getPlotItem().addItem(self)
         return pw
 
+    def key(self) -> typing.Tuple[int, int]:
+        return self.mProfile.key()
+
     def id(self) -> int:
         """
-        Returns the profile id
+        Returns the profile fid
         :return: int
         """
         return self.mProfile.id()
@@ -537,7 +545,6 @@ class SpectralProfilePlotDataItem(PlotDataItem):
 
 
 class XAxisWidgetAction(QWidgetAction):
-
     sigUnitChanged = pyqtSignal(str)
 
     def __init__(self, parent, **kwds):
@@ -570,7 +577,8 @@ class XAxisWidgetAction(QWidgetAction):
             lambda: self.setUnit(unitComboBox.currentData(Qt.UserRole))
         )
 
-        self.sigUnitChanged.connect(lambda unit, cb=unitComboBox: cb.setCurrentIndex(self.mUnitModel.unitIndex(unit).row()))
+        self.sigUnitChanged.connect(
+            lambda unit, cb=unitComboBox: cb.setCurrentIndex(self.mUnitModel.unitIndex(unit).row()))
         return unitComboBox
 
     def createWidget(self, parent: QWidget) -> QWidget:
@@ -590,7 +598,6 @@ class XAxisWidgetAction(QWidgetAction):
 
 
 class SpectralProfileRendererWidgetAction(QWidgetAction):
-
     sigProfileRendererChanged = pyqtSignal(SpectralProfileRenderer)
     sigResetRendererChanged = pyqtSignal(SpectralProfileRenderer)
 
@@ -613,7 +620,6 @@ class SpectralProfileRendererWidgetAction(QWidgetAction):
         return self.mProfileRenderer
 
     def createWidget(self, parent: QWidget) -> SpectralProfileRendererWidget:
-
         w = SpectralProfileRendererWidget(parent)
         w.setProfileRenderer(self.profileRenderer())
         w.sigProfileRendererChanged.connect(self.setProfileRenderer)
@@ -623,7 +629,6 @@ class SpectralProfileRendererWidgetAction(QWidgetAction):
 
 
 class MaxNumberOfProfilesWidgetAction(QWidgetAction):
-
     sigMaxNumberOfProfilesChanged = pyqtSignal(int)
 
     def __init__(self, parent, **kwds):
@@ -658,8 +663,8 @@ class SpectralViewBoxMenu(ViewBoxMenu):
     """
     The QMenu that is shown over the profile plot
     """
-    def __init__(self, *args, **kwds):
 
+    def __init__(self, *args, **kwds):
         super().__init__(*args, **kwds)
 
 
@@ -678,7 +683,8 @@ class SpectralViewBox(pg.ViewBox):
         self.mCurrentCursorPosition: typing.Tuple[int, int] = (0, 0)
         # define actions
         self.mActionMaxNumberOfProfiles: MaxNumberOfProfilesWidgetAction = MaxNumberOfProfilesWidgetAction(None)
-        self.mActionSpectralProfileRendering: SpectralProfileRendererWidgetAction = SpectralProfileRendererWidgetAction(None)
+        self.mActionSpectralProfileRendering: SpectralProfileRendererWidgetAction = SpectralProfileRendererWidgetAction(
+            None)
         self.mActionSpectralProfileRendering.setDefaultWidget(self.mActionSpectralProfileRendering.createWidget(None))
 
         self.mOptionUseVectorSymbology: QAction = \
@@ -750,20 +756,22 @@ class SpectralViewBox(pg.ViewBox):
     def updateCurrentPosition(self, x, y):
         self.mCurrentCursorPosition = (x, y)
 
+
 class SpectralLibraryPlotStats(object):
 
     def __init__(self):
-        self.features_speclib: int = 0
-        self.features_speclib_selected: int = 0
-
-        self.filter_mode: QgsAttributeTableFilterModel.FilterMode = QgsAttributeTableFilterModel.ShowAll
+        self.features_total: int = 0
+        self.features_selected: int = 0
         self.features_filtered: int = 0
-        self.features_filtered_selected: int = 0
-
-        self.features_plotted: int = 0
-        self.features_plotted_max: int = 0
+        self.filter_mode: QgsAttributeTableFilterModel.FilterMode = QgsAttributeTableFilterModel.ShowAll
 
-        self.features_with_value_error: int = 0
+        self.profiles_plotted_max: int = 0
+        self.profiles_total: int = 0
+        self.profiles_empty: int = 0
+        self.profiles_plotted: int = 0
+        self.profiles_selected: int = 0
+        self.profiles_filtered: int = 0
+        self.profiles_error: int = 0
 
     def __eq__(self, other) -> bool:
         if not isinstance(other, SpectralLibraryPlotStats):
@@ -773,6 +781,7 @@ class SpectralLibraryPlotStats(object):
                 return False
         return True
 
+
 class SpectralLibraryPlotWidget(pg.PlotWidget):
     """
     A widget to PlotWidget SpectralProfiles
@@ -794,6 +803,9 @@ class SpectralLibraryPlotWidget(pg.PlotWidget):
         self.setMaxProfiles(64)
         self.mDualView = None
 
+        self.mNumberOfValueErrorsProfiles: int = 0
+        self.mNumberOfEmptyProfiles: int = 0
+
         self.mMaxInfoLength: int = 30
 
         # self.centralWidget.setParent(None)
@@ -817,14 +829,11 @@ class SpectralLibraryPlotWidget(pg.PlotWidget):
         # describe functions to convert wavelength units from unit a to unit b
         self.mUnitConverter = UnitConverterFunctionModel()
 
-        self.mPlotDataItems: typing.List[int, SpectralProfilePlotDataItem] = dict()
+        self.mPlotDataItems: typing.List[typing.Tuple[int, str], SpectralProfilePlotDataItem] = dict()
         self.mPlotOverlayItems = []
         self.setAntialiasing(True)
         self.setAcceptDrops(True)
 
-        self.mLastFIDs = []
-        self.mNeedsPlotUpdate = False
-
         self.mCrosshairLineV = pg.InfiniteLine(angle=90, movable=False)
         self.mCrosshairLineH = pg.InfiniteLine(angle=0, movable=False)
 
@@ -849,7 +858,7 @@ class SpectralLibraryPlotWidget(pg.PlotWidget):
         pi.addItem(self.mCrosshairLineH, ignoreBounds=True)
         pi.addItem(self.mInfoScatterPoint)
         self.proxy2D = pg.SignalProxy(self.scene().sigMouseMoved, rateLimit=100, slot=self.onMouseMoved2D)
-        #self.proxy2D2 = pg.SignalProxy(self.scene().sigMouseClicked, rateLimit=100, slot=self.onMouseClicked)
+        # self.proxy2D2 = pg.SignalProxy(self.scene().sigMouseClicked, rateLimit=100, slot=self.onMouseClicked)
 
         # set default axis unit
         self.updateXUnit()
@@ -857,7 +866,7 @@ class SpectralLibraryPlotWidget(pg.PlotWidget):
 
         self.actionXAxis().sigUnitChanged.connect(self.updateXUnit)
         self.mSPECIFIC_PROFILE_STYLES: typing.Dict[int, PlotStyle] = dict()
-        self.mTEMPORARY_HIGHLIGHTED: typing.Set[int] = set()
+        self.mTEMPORARY_HIGHLIGHTED: typing.Set[typing.Tuple[int, str]] = set()
         self.mDefaultProfileRenderer: SpectralProfileRenderer
         self.mDefaultProfileRenderer = SpectralProfileRenderer.default()
 
@@ -873,6 +882,16 @@ class SpectralLibraryPlotWidget(pg.PlotWidget):
         self.setProfileRenderer(self.mDefaultProfileRenderer)
         self.setAcceptDrops(True)
 
+    def currentProfileKeys(self) -> typing.List[typing.Tuple[int, str]]:
+        return sorted(self.mTEMPORARY_HIGHLIGHTED)
+
+    def currentProfileIDs(self) -> typing.List[int]:
+        return list(set([k[0] for k in self.currentProfileKeys()]))
+
+    def currentProfiles(self) -> typing.List[SpectralProfile]:
+        keys = self.currentProfileKeys()
+        return list(self.speclib().profiles(profile_keys=keys))
+
     def onInfoScatterClicked(self, a, b):
         self.mInfoScatterPoint.setVisible(False)
         self.mInfoScatterPointHtml = ""
@@ -948,7 +967,7 @@ class SpectralLibraryPlotWidget(pg.PlotWidget):
         self.mInfoLabelCursor.setHtml(positionInfoHtml)
 
     def onMouseClicked(self, event):
-        #print(event[0].accepted)
+        # print(event[0].accepted)
         s = ""
 
     def onMouseMoved2D(self, evt):
@@ -1052,14 +1071,16 @@ class SpectralLibraryPlotWidget(pg.PlotWidget):
         """
         return [i for i in self.getPlotItem().items if isinstance(i, SpectralProfilePlotDataItem)]
 
-    def removeSpectralProfilePDIs(self, fidsToRemove: typing.List[int], updateScene: bool = True):
+    def removeSpectralProfilePDIs(self, keys_to_remove: typing.List[typing.Tuple[int, str]], updateScene: bool = True):
         """
         :param updateScene:
-        :param fidsToRemove: feature ids to remove
-        :type fidsToRemove:
+        :param keys_to_remove: feature ids to remove
+        :type keys_to_remove:
         :return:
         :rtype:
         """
+        if len(keys_to_remove) == 0:
+            return
 
         def disconnect(sig, slot):
             while True:
@@ -1071,7 +1092,7 @@ class SpectralLibraryPlotWidget(pg.PlotWidget):
 
         plotItem = self.getPlotItem()
         assert isinstance(plotItem, pg.PlotItem)
-        pdisToRemove = [pdi for pdi in self.spectralProfilePlotDataItems() if pdi.id() in fidsToRemove]
+        pdisToRemove = [pdi for pdi in self.spectralProfilePlotDataItems() if pdi.key() in keys_to_remove]
         for pdi in pdisToRemove:
             assert isinstance(pdi, SpectralProfilePlotDataItem)
             pdi.setClickable(False)
@@ -1079,8 +1100,8 @@ class SpectralLibraryPlotWidget(pg.PlotWidget):
             plotItem.removeItem(pdi)
             # QtGui.QGraphicsScene.items(self, *args)
             assert pdi not in plotItem.dataItems
-            if pdi.id() in self.mPlotDataItems.keys():
-                self.mPlotDataItems.pop(pdi.id(), None)
+            if pdi.key() in self.mPlotDataItems.keys():
+                self.mPlotDataItems.pop(pdi.key(), None)
 
         if updateScene:
             self.scene().update()
@@ -1197,21 +1218,29 @@ class SpectralLibraryPlotWidget(pg.PlotWidget):
 
         if layerID != self.speclib().id():
             return
-
+        speclib: SpectralLibrary = self.speclib()
+        fieldIndices = [speclib.fields().indexOf(f.name()) for f in speclib.spectralValueFields()]
         idxF = self.speclib().fields().indexOf(FIELD_VALUES)
-        fids = []
+        fids = set()
 
         for fid, fieldMap in featureMap.items():
-            if idxF in fieldMap.keys():
-                fids.append(fid)
+            for idx in fieldIndices:
+                if idx in fieldMap.keys():
+                    fids.add(fid)
 
         if len(fids) == 0:
             return
+        fids = list(fids)
+        update = False
         for p in self.speclib().profiles(fids):
             assert isinstance(p, SpectralProfile)
-            pdi = self.spectralProfilePlotDataItem(p.id())
+            pdi = self.mPlotDataItems.get(p.key(), None)
             if isinstance(pdi, SpectralProfilePlotDataItem):
                 pdi.resetSpectralProfile(p)
+            else:
+                update = True
+        if update:
+            self.updateSpectralProfilePlotItems()
 
     @pyqtSlot()
     def onProfileRendererChanged(self):
@@ -1315,7 +1344,6 @@ class SpectralLibraryPlotWidget(pg.PlotWidget):
 
     def updateXUnit(self):
 
-
         unit = self.xUnit()
         label = self.xAxisUnitModel().unitData(unit, role=Qt.DisplayRole)
 
@@ -1342,29 +1370,35 @@ class SpectralLibraryPlotWidget(pg.PlotWidget):
     def updateSpectralProfilePlotItems(self):
         pi = self.getPlotItem()
         assert isinstance(pi, SpectralLibraryPlotItem)
+        n_max = self.maxProfiles()
 
-        toBeVisualized: typing.List[int] = self.profileIDsToVisualize()
-        visualized: typing.List[int] = self.plottedProfileIDs()
-        toBeRemoved = [fid for fid in visualized if fid not in toBeVisualized]
-        toBeAdded = [fid for fid in toBeVisualized if fid not in visualized]
-
-        if isinstance(self.speclib(), SpectralLibrary):
-            selectedNow = set(self.speclib().selectedFeatureIds())
-        else:
-            selectedNow = set()
+        self.mNumberOfValueErrorsProfiles = 0
+        self.mNumberOfEmptyProfiles = 0
 
-        selectionChanged = list(selectedNow.symmetric_difference(self.mSelectedIds))
-        self.mSelectedIds = selectedNow
+        keys_visualized: typing.List[typing.Tuple[int, str]] = self.plottedProfileKeys()
+        pdis_to_visualize: typing.List[SpectralProfilePlotDataItem] = []
+        new_pdis: typing.List[SpectralProfilePlotDataItem] = []
+        sort_x_values: bool = self.xUnit() in ['DOI']
+        for pkey in self.profileKeysToVisualize():
+            if len(pdis_to_visualize) >= n_max:
+                break
 
-        if len(toBeRemoved) > 0:
-            self.removeSpectralProfilePDIs(toBeRemoved)
+            fid, field_name = pkey
 
-        if len(toBeAdded) > 0:
-            sort_x_values = self.xUnit() in ['DOI']
-            addedPDIs = []
-            addedProfiles = self.speclib().profiles(toBeAdded)
-            for profile in addedProfiles:
+            pdi: SpectralProfilePlotDataItem = self.mPlotDataItems.get(pkey, None)
+            if isinstance(pdi, SpectralProfilePlotDataItem):
+                if pdi.valueConversionPossible():
+                    pdis_to_visualize.append(pdi)
+                else:
+                    self.mNumberOfValueErrorsProfiles += 1
+            else:
+                # create a new PDI
+                profile = self.speclib().profile(fid, value_field=field_name)
                 assert isinstance(profile, SpectralProfile)
+                if profile.isEmpty():
+                    self.mNumberOfEmptyProfiles += 1
+                    continue
+
                 if not self.mXUnitInitialized:
                     self.setXUnit(profile.xUnit())
                     self.mXUnitInitialized = True
@@ -1377,16 +1411,35 @@ class SpectralLibraryPlotWidget(pg.PlotWidget):
                 pdi.mSortByXValues = sort_x_values
                 pdi.applyMapFunctions()
                 pdi.sigProfileClicked.connect(self.onProfileClicked)
+                if pdi.valueConversionPossible():
+                    new_pdis.append(pdi)
+                    pdis_to_visualize.append(pdi)
+                else:
+                    self.mNumberOfValueErrorsProfiles += 1
+
+        keys_to_visualize = [pdi.key() for pdi in pdis_to_visualize]
+        keys_to_remove = [pkey for pkey in keys_visualized if pkey not in keys_to_visualize]
+        keys_new = [pdi.key() for pdi in new_pdis]
+        if len(keys_to_remove) > 0:
+            s = ""
+        self.removeSpectralProfilePDIs(keys_to_remove)
+        if len(new_pdis) > 0:
+            for pdi in new_pdis:
+                self.mPlotDataItems[pdi.key()] = pdi
+            pi.addItems(new_pdis)
 
-                self.mPlotDataItems[profile.id()] = pdi
-                addedPDIs.append(pdi)
-            pi.addItems(addedPDIs)
+        if isinstance(self.speclib(), SpectralLibrary):
+            selectedNow = set(self.speclib().selectedFeatureIds())
+        else:
+            selectedNow = set()
 
-        update_styles = list(set(toBeAdded + selectionChanged))
-        if len(update_styles) > 0:
-            self.updateProfileStyles(update_styles)
+        selectionChanged = list(selectedNow.symmetric_difference(self.mSelectedIds))
+        self.mSelectedIds = selectedNow
 
-        if len(toBeAdded + toBeRemoved + selectionChanged) > 0:
+        key_to_update_style = [pkey for pkey in keys_to_visualize if pkey[0] in selectionChanged or pkey in keys_new]
+        self.updateProfileStyles(key_to_update_style)
+
+        if len(keys_new) > 0 or len(keys_to_remove) > 0 or len(key_to_update_style) > 0:
             pi.update()
 
     def resetSpectralProfiles(self):
@@ -1401,14 +1454,15 @@ class SpectralLibraryPlotWidget(pg.PlotWidget):
         :param fid: int | QgsFeature | SpectralProfile
         :return: SpectralProfilePlotDataItem
         """
+        warnings.warn('Do not use', DeprecationWarning)
         if isinstance(fid, QgsFeature):
             fid = fid.id()
         return self.mPlotDataItems.get(fid)
 
-    def updateProfileStyles(self, fids: typing.List[int] = None):
+    def updateProfileStyles(self, keys: typing.List[typing.Tuple[int, str]] = None):
         """
-        Updates the styles for a set of SpectralProfilePlotDataItems specified by its feature ids
-        :param fids: profile ids to update
+        Updates the styles for a set of SpectralProfilePlotDataItems specified by its feature keys
+        :param keys: profile ids to update
         """
 
         if not isinstance(self.speclib(), SpectralLibrary):
@@ -1419,10 +1473,10 @@ class SpectralLibraryPlotWidget(pg.PlotWidget):
         pdis = self.spectralProfilePlotDataItems()
 
         # update for requested FIDs only
-        if isinstance(fids, list):
-            pdis = [pdi for pdi in pdis if pdi.id() in fids]
-
-        xUnit = self.xUnit()
+        if isinstance(keys, list):
+            if len(keys) == 0:
+                return
+            pdis = [pdi for pdi in pdis if pdi.key() in keys]
 
         # update line colors
         fids2 = [pdi.id() for pdi in pdis]
@@ -1507,40 +1561,112 @@ class SpectralLibraryPlotWidget(pg.PlotWidget):
         Returns stats related to existing and visualized SpectralProfiles
         """
         stats = SpectralLibraryPlotStats()
-        stats.features_plotted_max = self.maxProfiles()
+        stats.profiles_plotted_max = self.maxProfiles()
+
         if isinstance(self.speclib(), SpectralLibrary) and not sip.isdeleted(self.speclib()):
-            stats.features_speclib = self.speclib().featureCount()
-            stats.features_speclib_selected = self.speclib().selectedFeatureCount()
+            stats.features_total = self.speclib().featureCount()
+            stats.features_selected = self.speclib().selectedFeatureCount()
 
             stats.filter_mode = self.dualView().filterMode()
 
-            if stats.filter_mode != QgsAttributeTableFilterModel.ShowAll:
-                stats.features_filtered = self.dualView().filteredFeatureCount()
-                selected_fids = self.speclib().selectedFeatureIds()
-                for f in self.dualView().filteredFeatures():
-                    if f in selected_fids:
-                        stats.features_filtered_selected += 1
+            filtered_fids = self.dualView().filteredFeatures()
+            selected_fids = self.speclib().selectedFeatureIds()
+
+            stats.features_filtered = len(filtered_fids)
+
+            stats.profiles_total = stats.features_total * len(self.speclib().spectralValueFields())
+            stats.profiles_filtered = stats.features_filtered * len(self.speclib().spectralValueFields())
+            stats.profiles_error = self.mNumberOfValueErrorsProfiles
+            stats.profiles_empty = self.mNumberOfEmptyProfiles
+
+            for pdi in self.allSpectralProfilePlotDataItems():
+                fid, value_field = pdi.key()
+                if pdi.isVisible():
+                    stats.profiles_plotted += 1
 
-        for pdi in self.allSpectralProfilePlotDataItems():
-            if pdi.isVisible():
-                stats.features_plotted += 1
-            elif not pdi.valueConversionPossible():
-                stats.features_with_value_error += 1
+                    if fid in selected_fids:
+                        stats.profiles_selected += 1
 
         return stats
 
+    def plottedProfileKeys(self) -> typing.List[typing.Tuple[int, str]]:
+        return [pdi.key() for pdi in self.mPlotDataItems.values()]
+
     def plottedProfileIDs(self) -> typing.List[int]:
         """
         Returns the feature IDs of visualize SpectralProfiles from the connected SpectralLibrary.
         """
-        return list(self.mPlotDataItems.keys())
+        return [pdi.id() for pdi in self.mPlotDataItems.values()]
 
-    def profileIDsToVisualize(self) -> typing.List[int]:
+    def profileKeysToVisualize(self) -> typing.List[typing.Tuple[int, str]]:
         """
         Returns the list of profile/feature ids to be visualized.
-        The maximum number is determined by self.maxProfiles()
-        Order of returned fids is equal to its importance.
-        1st position = most important, should be plottet on top of all other profiles
+        Order of returned keys is equal to its importance.
+        1st position = most important, should be plotted on top of all other profiles
+        """
+        if not isinstance(self.speclib(), SpectralLibrary):
+            return []
+
+        fieldNames = [f.name() for f in self.speclib().spectralValueFields()]
+
+        if len(fieldNames) == 0:
+            return []
+
+        selectedOnly = self.actionShowSelectedProfilesOnly().isChecked()
+        selectedIds = self.speclib().selectedFeatureIds()
+
+        dualView = self.dualView()
+        if isinstance(dualView, QgsDualView) and dualView.filteredFeatureCount() > 0:
+            allIDs = dualView.filteredFeatures()
+            selectedIds = [fid for fid in allIDs if fid in selectedIds]
+        else:
+            allIDs = self.speclib().allFeatureIds()
+
+        # Order:
+        # 1. visible in table
+        # 2. selected
+        # 3. others
+
+        # overlaid features / current spectral
+        priority0 = self.currentProfileIDs()
+        priority1 = []  # visible features
+        priority2 = []  # selected features
+        priority3 = []  # any other : not visible / not selected
+
+        if isinstance(dualView, QgsDualView):
+            tv = dualView.tableView()
+            assert isinstance(tv, QTableView)
+            if not selectedOnly:
+                rowHeight = tv.rowViewportPosition(1) - tv.rowViewportPosition(0)
+                if rowHeight > 0:
+                    for y in range(0, tv.viewport().height(), rowHeight):
+                        idx = dualView.tableView().indexAt(QPoint(0, y))
+                        if idx.isValid():
+                            fid = tv.model().data(idx, role=Qt.UserRole)
+                            priority1.append(fid)
+            priority2 = self.dualView().masterModel().layer().selectedFeatureIds()
+            if not selectedOnly:
+                priority3 = dualView.filteredFeatures()
+        else:
+            priority2 = selectedIds
+            if not selectedOnly:
+                priority3 = allIDs
+
+        featurePool = np.unique(priority0 + priority1 + priority2 + priority3).tolist()
+        toVisualize = sorted(featurePool,
+                             key=lambda fid: (fid not in priority0, fid not in priority1, fid not in priority2, fid))
+
+        results = []
+        for fid in toVisualize:
+            for n in fieldNames:
+                results.append((fid, n))
+        return results
+
+    def profileIDsToVisualizeOLD(self) -> typing.List[int]:
+        """
+        Returns the list of profile/feature ids to be visualized.
+        Order of returned keys is equal to its importance.
+        1st position = most important, should be plotted on top of all other profiles
         """
         if not isinstance(self.speclib(), SpectralLibrary):
             return []
@@ -1568,10 +1694,8 @@ class SpectralLibraryPlotWidget(pg.PlotWidget):
         # 2. selected
         # 3. others
 
-
-
         # overlaid features / current spectral
-        priority0 = sorted(self.mTEMPORARY_HIGHLIGHTED)
+        priority0 = self.currentProfileIDs()
         priority1 = []  # visible features
         priority2 = []  # selected features
         priority3 = []  # any other : not visible / not selected
@@ -1622,7 +1746,7 @@ class SpectralLibraryPlotWidget(pg.PlotWidget):
         mimeData = event.mimeData()
         if containsSpeclib(mimeData) and isinstance(self.speclib(), SpectralLibrary):
             speclib = SpectralLibrary.readFromMimeData(mimeData)
-            print(f'DROP SPECLIB {speclib}')
+            # print(f'DROP SPECLIB {speclib}')
             if isinstance(speclib, SpectralLibrary) and len(speclib) > 0:
 
                 b = self.speclib().isEditable()
@@ -1635,57 +1759,123 @@ class SpectralLibraryPlotWidget(pg.PlotWidget):
             super().dropEvent(event)
 
 
-class SpectralProfileValueTableModel(QAbstractTableModel):
+class SpectralProfileTableModel(QAbstractTableModel):
     """
     A TableModel to show and edit spectral values of a SpectralProfile
     """
 
+    sigXUnitChanged = pyqtSignal(str)
+    sigYUnitChanged = pyqtSignal(str)
+
     def __init__(self, *args, **kwds):
-        super(SpectralProfileValueTableModel, self).__init__(*args, **kwds)
+        super(SpectralProfileTableModel, self).__init__(*args, **kwds)
+
+        self.mColumnNames = {0: 'x',
+                             1: 'y'}
+        self.mColumnUnits = {0: None,
+                             1: None}
+
+        self.mValuesX: typing.Dict[int, typing.Any] = {}
+        self.mValuesY: typing.Dict[int, typing.Any] = {}
+        self.mValuesBBL: typing.Dict[int, typing.Any] = {}
+
+        self.mLastProfile: SpectralProfile = SpectralProfile()
 
-        self.mColumnDataTypes = [float, float]
-        self.mColumnDataUnits = ['-', '-']
-        self.mValues = EMPTY_PROFILE_VALUES.copy()
+        self.mRows: int = 0
 
-    def setProfileData(self, values):
+    def setBands(self, bands: int):
+        bands = int(bands)
+
+        assert bands >= 0
+
+        if bands > self.bands():
+            self.beginInsertRows(QModelIndex(), self.bands(), bands - 1)
+            self.mRows = bands
+            self.endInsertRows()
+
+        elif bands < self.bands():
+            self.beginRemoveRows(QModelIndex(), bands, self.bands() - 1)
+            self.mRows = bands
+            self.endRemoveRows()
+
+    def bands(self) -> int:
+        return self.rowCount()
+
+    def setProfile(self, profile: SpectralProfile):
         """
         :param values:
         :return:
         """
-        if isinstance(values, SpectralProfile):
-            values = values.values()
-        assert isinstance(values, dict)
+        assert isinstance(profile, SpectralProfile)
 
-        for k in EMPTY_PROFILE_VALUES.keys():
-            assert k in values.keys()
+        self.beginResetModel()
+        self.mValuesX.clear()
+        self.mValuesY.clear()
+        self.mValuesBBL.clear()
+        self.mLastProfile = profile
+        self.mValuesX.update({i: v for i, v in enumerate(profile.xValues())})
+        self.mValuesY.update({i: v for i, v in enumerate(profile.yValues())})
+        self.mValuesBBL.update({i: v for i, v in enumerate(profile.bbl())})
 
-        for i, k in enumerate(['y', 'x']):
-            if values[k] and len(values[k]) > 0:
-                self.setColumnDataType(i, type(values[k][0]))
-            else:
-                self.setColumnDataType(i, float)
-        self.setColumnValueUnit('y', values.get('yUnit', ''))
-        self.setColumnValueUnit('x', values.get('xUnit', ''))
+        self.setBands(len(self.mValuesY))
 
-        self.beginResetModel()
-        self.mValues.update(values)
         self.endResetModel()
+        self.setXUnit(profile.xUnit())
+        self.setYUnit(profile.yUnit())
+
+    def setXUnit(self, unit: str):
+        if self.xUnit() != unit:
+            self.mColumnUnits[0] = unit
+            idx0 = self.index(0, 0)
+            idx1 = self.index(self.rowCount(QModelIndex()) - 1, 0)
+            self.dataChanged.emit(idx0, idx1)
+            # self.headerDataChanged.emit(Qt.Horizontal, 0, self.columnCount(QModelIndex())-1)
+            self.sigXUnitChanged.emit(unit)
+
+    def setYUnit(self, unit: str):
+        if self.yUnit() != unit:
+            self.mColumnUnits[1] = unit
+            # self.headerDataChanged.emit(Qt.Horizontal, 0, self.columnCount(QModelIndex())-1)
+            self.sigYUnitChanged.emit(unit)
 
-    def values(self) -> dict:
+    def xUnit(self) -> str:
+        return self.mColumnUnits[0]
+
+    def yUnit(self) -> str:
+        return self.mColumnUnits[1]
+
+    def profile(self) -> SpectralProfile:
         """
-        Returns the value dictionary of a SpectralProfile
-        :return: dict
+        Return the data as new SpectralProfile
+        :return:
+        :rtype:
         """
-        return self.mValues
+        p = SpectralProfile(fields=self.mLastProfile.fields())
+        nb = self.bands()
 
-    def rowCount(self, QModelIndex_parent=None, *args, **kwargs):
-        if self.mValues['y'] is None:
-            return 0
+        y = [self.mValuesY.get(b, None) for b in range(nb)]
+        if self.xUnit() == BAND_INDEX:
+            x = None
         else:
-            return len(self.mValues['y'])
+            x = [self.mValuesX.get(b, None) for b in range(nb)]
+
+        bbl = [self.mValuesBBL.get(b, None) for b in range(nb)]
+        bbl = np.asarray(bbl, dtype=bool)
+        if np.any(bbl == False) == False:
+            bbl = None
+        p.setValues(x, y, xUnit=self.xUnit(), yUnit=self.yUnit(), bbl=bbl)
+
+        return p
+
+    def resetProfile(self):
+        self.setProfile(self.mLastProfile)
 
-    def columnCount(self, parent=QModelIndex()):
-        return 2
+    def rowCount(self, parent: QModelIndex = None, *args, **kwargs) -> int:
+
+        return self.mRows
+
+    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():
@@ -1697,17 +1887,28 @@ class SpectralProfileValueTableModel(QAbstractTableModel):
         if role in [Qt.DisplayRole, Qt.EditRole]:
             value = None
             if c == 0:
-                value = self.mValues['y'][i]
+                if self.xUnit() != BAND_INDEX:
+                    value = self.mValuesX.get(i, None)
+                    if value:
+                        return str(value)
+                    else:
+                        return None
+                else:
+                    return i + 1
 
             elif c == 1:
-                value = self.mValues['x'][i]
-
-            # log('data: {} {}'.format(type(value), value))
-            return value
-
-        if role == Qt.UserRole:
-            return self.mValues
+                value = self.mValuesY.get(i, None)
+                if value:
+                    return str(value)
+                else:
+                    return None
 
+        elif role == Qt.CheckStateRole:
+            if c == 0:
+                if bool(self.mValuesBBL.get(i, True)):
+                    return Qt.Checked
+                else:
+                    return Qt.Unchecked
         return None
 
     def setData(self, index, value, role=None):
@@ -1717,81 +1918,29 @@ class SpectralProfileValueTableModel(QAbstractTableModel):
         c = index.column()
         i = index.row()
 
-        if role == Qt.EditRole:
-            # cast to correct data type
-            dt = self.mColumnDataTypes[c]
-            value = dt(value)
+        modified = False
+        if role == Qt.CheckStateRole:
+            if c == 0:
+                self.mValuesBBL[i] = value == Qt.Checked
+                modified = True
 
+        if role == Qt.EditRole:
             if c == 0:
-                self.mValues['y'][i] = value
-                return True
+                try:
+                    self.mValuesX[i] = float(value)
+                    modified = True
+                except:
+                    pass
             elif c == 1:
-                self.mValues['x'][i] = value
-                return True
-        return False
-
-    def index2column(self, index) -> int:
-        """
-        Returns a column index
-        :param index: QModelIndex, int or str from  ['x','y']
-        :return: int
-        """
-        if isinstance(index, str):
-            index = ['y', 'x'].index(index.strip().lower())
-        elif isinstance(index, QModelIndex):
-            index = index.column()
-
-        assert isinstance(index, int) and index >= 0
-        return index
-
-    def setColumnValueUnit(self, index, valueUnit: str):
-        """
-        Sets the unit of the value column
-        :param index: 'y','x', respective 0, 1
-        :param valueUnit: str with unit, e.g. 'Reflectance' or 'um'
-        """
-        index = self.index2column(index)
-        if valueUnit is None:
-            valueUnit = '-'
-
-        assert isinstance(valueUnit, str)
-
-        if self.mColumnDataUnits[index] != valueUnit:
-            self.mColumnDataUnits[index] = valueUnit
-            self.headerDataChanged.emit(Qt.Horizontal, index, index)
-            self.sigColumnValueUnitChanged.emit(index, valueUnit)
-
-    sigColumnValueUnitChanged = pyqtSignal(int, str)
-
-    def setColumnDataType(self, index, dataType: type):
-        """
-        Sets the numeric dataType in which spectral values are returned
-        :param index: 'y','x', respective 0, 1
-        :param dataType: int or float (default)
-        """
-        index = self.index2column(index)
-        if isinstance(dataType, str):
-            i = ['Integer', 'Float'].index(dataType)
-            dataType = [int, float][i]
-
-        assert dataType in [int, float]
-
-        if self.mColumnDataTypes[index] != dataType:
-            self.mColumnDataTypes[index] = dataType
-
-            if index == 0:
-                y = self.mValues.get('y')
-                if isinstance(y, list) and len(y) > 0:
-                    self.mValues['y'] = [dataType(v) for v in self.mValues['y']]
-            elif index == 1:
-                x = self.mValues.get('x')
-                if isinstance(x, list) and len(x) > 0:
-                    self.mValues['x'] = [dataType(v) for v in self.mValues['x']]
-
-            self.dataChanged.emit(self.createIndex(0, index), self.createIndex(self.rowCount(), index))
-            self.sigColumnDataTypeChanged.emit(index, dataType)
+                try:
+                    self.mValuesY[i] = float(value)
+                    modified = True
+                except:
+                    pass
 
-    sigColumnDataTypeChanged = pyqtSignal(int, type)
+        if modified:
+            self.dataChanged.emit(index, index, [role])
+        return modified
 
     def flags(self, index):
         if index.isValid():
@@ -1799,52 +1948,68 @@ class SpectralProfileValueTableModel(QAbstractTableModel):
             flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable
 
             if c == 0:
-                flags = flags | Qt.ItemIsEditable
-            elif c == 1 and self.mValues['xUnit']:
+                flags = flags | Qt.ItemIsUserCheckable
+                if self.xUnit() != BAND_INDEX:
+                    flags = flags | Qt.ItemIsEditable
+            elif c == 1:
                 flags = flags | Qt.ItemIsEditable
             return flags
-            # return item.qt_flags(index.column())
         return None
 
-    def headerData(self, col, orientation, role):
-        if Qt is None:
-            return None
+    def headerData(self, col: int, orientation, role):
+
         if orientation == Qt.Horizontal and role in [Qt.DisplayRole, Qt.ToolTipRole]:
-            name = ['Y', 'X'][col]
-            unit = self.mColumnDataUnits[col]
-            if unit in EMPTY_VALUES:
-                unit = '-'
-            return '{} [{}]'.format(name, unit)
-        elif orientation == Qt.Vertical and role == Qt.DisplayRole:
-            return col
+            return self.mColumnNames.get(col, f'{col + 1}')
         return None
 
 
 class SpectralProfileEditorWidget(QWidget):
-    sigProfileValuesChanged = pyqtSignal(dict)
+    sigProfileChanged = pyqtSignal()
 
     def __init__(self, *args, **kwds):
         super(SpectralProfileEditorWidget, self).__init__(*args, **kwds)
         loadUi(speclibUiPath('spectralprofileeditorwidget.ui'), self)
-        self.mDefault = None
-        self.mModel = SpectralProfileValueTableModel(parent=self)
-        self.mModel.dataChanged.connect(lambda: self.sigProfileValuesChanged.emit(self.profileValues()))
-        self.mModel.sigColumnValueUnitChanged.connect(self.onValueUnitChanged)
-        self.mModel.sigColumnDataTypeChanged.connect(self.onDataTypeChanged)
+        self.mDefault: SpectralProfile = None
+        self.mModel: SpectralProfileTableModel = SpectralProfileTableModel()
+        self.mModel.rowsInserted.connect(self.onBandsChanged)
+        self.mModel.rowsRemoved.connect(self.onBandsChanged)
+        self.mModel.dataChanged.connect(lambda *args: self.onProfileChanged())
+        self.mXUnitModel: XUnitModel = XUnitModel()
+        self.cbXUnit.setModel(self.mXUnitModel)
+        self.cbXUnit.currentIndexChanged.connect(
+            lambda *args: self.mModel.setXUnit(self.cbXUnit.currentData(Qt.UserRole)))
+        self.mModel.sigXUnitChanged.connect(self.onXUnitChanged)
+
+        self.tbYUnit.textChanged.connect(self.mModel.setYUnit)
+        self.mModel.sigYUnitChanged.connect(self.tbYUnit.setText)
+        self.mModel.sigYUnitChanged.connect(self.onProfileChanged)
+        self.mModel.sigXUnitChanged.connect(self.onProfileChanged)
+        # self.mModel.sigColumnValueUnitChanged.connect(self.onValueUnitChanged)
+        # self.mModel.sigColumnDataTypeChanged.connect(self.onDataTypeChanged)
+        self.tableView.setModel(self.mModel)
+
+        self.actionReset.triggered.connect(self.resetProfile)
+        self.btnReset.setDefaultAction(self.actionReset)
 
-        self.cbYUnit.currentTextChanged.connect(lambda unit: self.mModel.setColumnValueUnit(0, unit))
-        self.cbXUnit.currentTextChanged.connect(lambda unit: self.mModel.setColumnValueUnit(1, unit))
+        self.sbBands.valueChanged.connect(self.mModel.setBands)
+        # self.onDataTypeChanged(0, float)
+        # self.onDataTypeChanged(1, float)
 
-        self.cbYUnitDataType.currentTextChanged.connect(lambda v: self.mModel.setColumnDataType(0, v))
-        self.cbXUnitDataType.currentTextChanged.connect(lambda v: self.mModel.setColumnDataType(1, v))
+        self.setProfile(SpectralProfile())
 
-        self.actionReset.triggered.connect(self.resetProfileValues)
-        self.btnReset.setDefaultAction(self.actionReset)
+    def onProfileChanged(self):
+        if self.profile() != self.mDefault:
+            self.sigProfileChanged.emit()
 
-        self.onDataTypeChanged(0, float)
-        self.onDataTypeChanged(1, float)
+    def onXUnitChanged(self, unit: str):
+        unit = self.mXUnitModel.findUnit(unit)
+        if unit is None:
+            unit = BAND_INDEX
+        self.cbXUnit.setCurrentIndex(self.mXUnitModel.unitIndex(unit).row())
 
-        self.setProfileValues(EMPTY_PROFILE_VALUES.copy())
+    def onBandsChanged(self, *args):
+        self.sbBands.setValue(self.mModel.bands())
+        self.onProfileChanged()
 
     def initConfig(self, conf: dict):
         """
@@ -1852,111 +2017,89 @@ class SpectralProfileEditorWidget(QWidget):
         :param conf: dict
         """
 
-        if 'xUnitList' in conf.keys():
-            self.cbXUnit.addItems(conf['xUnitList'])
+        pass
 
-        if 'yUnitList' in conf.keys():
-            self.cbYUnit.addItems(conf['yUnitList'])
-
-    def onValueUnitChanged(self, index: int, unit: str):
-        comboBox = [self.cbYUnit, self.cbXUnit][index]
-        setComboboxValue(comboBox, unit)
-
-    def onDataTypeChanged(self, index: int, dataType: type):
-
-        if dataType == int:
-            typeString = 'Integer'
-        elif dataType == float:
-            typeString = 'Float'
-        else:
-            raise NotImplementedError()
-        comboBox = [self.cbYUnitDataType, self.cbXUnitDataType][index]
-
-        setComboboxValue(comboBox, typeString)
-
-    def setProfileValues(self, values):
+    def setProfile(self, profile: SpectralProfile):
         """
         Sets the profile values to be shown
         :param values: dict() or SpectralProfile
         :return:
         """
+        assert isinstance(profile, SpectralProfile)
+        self.mDefault = profile
 
-        if isinstance(values, SpectralProfile):
-            values = values.values()
-
-        assert isinstance(values, dict)
-        import copy
-        self.mDefault = copy.deepcopy(values)
-        self.mModel.setProfileData(values)
+        self.mModel.setProfile(profile)
 
-    def resetProfileValues(self):
-        self.setProfileValues(self.mDefault)
+    def resetProfile(self):
+        self.mModel.setProfile(self.mDefault)
 
-    def profileValues(self) -> dict:
+    def profile(self) -> SpectralProfile:
         """
-        Returns the value dictionary of a SpectralProfile
+        Returns modified SpectralProfile
         :return: dict
         """
-        return self.mModel.values()
+
+        return self.mModel.profile()
 
 
 class SpectralProfileEditorWidgetWrapper(QgsEditorWidgetWrapper):
 
     def __init__(self, vl: QgsVectorLayer, fieldIdx: int, editor: QWidget, parent: QWidget):
         super(SpectralProfileEditorWidgetWrapper, self).__init__(vl, fieldIdx, editor, parent)
-        self.mEditorWidget = None
-        self.mLabel = None
-        self.mDefaultValue = None
+        self.mWidget: QWidget = None
+
+        self.mLastValue = QVariant()
 
     def createWidget(self, parent: QWidget):
         # log('createWidget')
-        w = None
+
         if not self.isInTable(parent):
-            w = SpectralProfileEditorWidget(parent=parent)
+            self.mWidget = SpectralProfileEditorWidget(parent=parent)
         else:
-            # w = PlotStyleButton(parent)
-            w = QWidget(parent)
-            w.setVisible(False)
-        return w
+            self.mWidget = QLabel(' Profile', parent=parent)
+        return self.mWidget
 
     def initWidget(self, editor: QWidget):
         # log(' initWidget')
         conf = self.config()
 
         if isinstance(editor, SpectralProfileEditorWidget):
-            self.mEditorWidget = editor
-            self.mEditorWidget.sigProfileValuesChanged.connect(self.onValueChanged)
-            self.mEditorWidget.initConfig(conf)
 
-        if isinstance(editor, QWidget):
-            self.mLabel = editor
-            self.mLabel.setVisible(False)
-            self.mLabel.setToolTip('Use Form View to edit values')
+            editor.sigProfileChanged.connect(self.onValueChanged)
+            editor.initConfig(conf)
+
+        elif isinstance(editor, QLabel):
+            editor.setText(SPECTRAL_PROFILE_FIELD_REPRESENT_VALUE)
+            editor.setToolTip('Use Form View to edit values')
 
     def onValueChanged(self, *args):
-        self.valueChanged.emit(self.value())
+        self.valuesChanged.emit(self.value())
         s = ""
 
     def valid(self, *args, **kwargs) -> bool:
-        return isinstance(self.mEditorWidget, SpectralProfileEditorWidget) or isinstance(self.mLabel, QWidget)
+        return isinstance(self.mWidget, (SpectralProfileEditorWidget, QLabel))
 
     def value(self, *args, **kwargs):
-        value = self.mDefaultValue
-        if isinstance(self.mEditorWidget, SpectralProfileEditorWidget):
-            v = self.mEditorWidget.profileValues()
-            value = encodeProfileValueDict(v)
+        value = self.mLastValue
+        w = self.widget()
+        if isinstance(w, SpectralProfileEditorWidget):
+            p = w.profile()
+            value = encodeProfileValueDict(p.values())
 
         return value
 
     def setEnabled(self, enabled: bool):
-
-        if self.mEditorWidget:
-            self.mEditorWidget.setEnabled(enabled)
+        w = self.widget()
+        if isinstance(w, SpectralProfileEditorWidget):
+            w.setEnabled(enabled)
 
     def setValue(self, value):
-        if isinstance(self.mEditorWidget, SpectralProfileEditorWidget):
-            self.mEditorWidget.setProfileValues(decodeProfileValueDict(value))
-        self.mDefaultValue = value
+        self.mLastValue = value
+        p = SpectralProfile(values=decodeProfileValueDict(value))
+        w = self.widget()
+        if isinstance(w, SpectralProfileEditorWidget):
+            w.setProfile(p)
+
         # if isinstance(self.mLabel, QLabel):
         #    self.mLabel.setText(value2str(value))
 
@@ -1968,60 +2111,73 @@ class SpectralProfileEditorConfigWidget(QgsEditorConfigWidget):
         super(SpectralProfileEditorConfigWidget, self).__init__(vl, fieldIdx, parent)
         loadUi(speclibUiPath('spectralprofileeditorconfigwidget.ui'), self)
 
-        self.mLastConfig = {}
+        self.mLastConfig: dict = {}
+        self.MYCACHE = dict()
+        self.mFieldExpressionName: QgsFieldExpressionWidget
+        self.mFieldExpressionSource: QgsFieldExpressionWidget
 
-        self.tbXUnits.textChanged.connect(lambda: self.changed.emit())
-        self.tbYUnits.textChanged.connect(lambda: self.changed.emit())
+        self.mFieldExpressionName.setLayer(vl)
+        self.mFieldExpressionSource.setLayer(vl)
 
-        self.tbResetX.setDefaultAction(self.actionResetX)
-        self.tbResetY.setDefaultAction(self.actionResetY)
+        self.mFieldExpressionName.setFilters(QgsFieldProxyModel.String)
+        self.mFieldExpressionSource.setFilters(QgsFieldProxyModel.String)
 
-    def unitTextBox(self, dim: str) -> QPlainTextEdit:
-        if dim == 'x':
-            return self.tbXUnits
-        elif dim == 'y':
-            return self.tbYUnits
-        else:
-            raise NotImplementedError()
-
-    def units(self, dim: str) -> list:
-        textEdit = self.unitTextBox(dim)
-        assert isinstance(textEdit, QPlainTextEdit)
-        values = []
-        for line in textEdit.toPlainText().splitlines():
-            v = line.strip()
-            if len(v) > 0 and v not in values:
-                values.append(v)
-        return values
-
-    def setUnits(self, dim: str, values: list):
-        textEdit = self.unitTextBox(dim)
-        assert isinstance(textEdit, QPlainTextEdit)
-        textEdit.setPlainText('\n'.join(values))
+        self.mFieldExpressionName.fieldChanged[str, bool].connect(self.onFieldChanged)
+        self.mFieldExpressionSource.fieldChanged[str, bool].connect(self.onFieldChanged)
+
+    def onFieldChanged(self, expr: str, valid: bool):
+        if valid:
+            self.changed.emit()
+
+    def expressionName(self) -> QgsExpression:
+        exp = QgsExpression(self.mFieldExpressionName.expression())
+        return exp
+
+    def expressionSource(self) -> QgsExpression:
+        exp = QgsExpression(self.mFieldExpressionSource.expression())
+        return exp
 
     def config(self, *args, **kwargs) -> dict:
-        config = {'xUnitList': self.units('x'),
-                  'yUnitList': self.units('y')
-                  }
+        config = {'expressionName': self.mFieldExpressionName.expression(),
+                  'expressionSource': self.mFieldExpressionSource.expression(),
+                  'mycache': self.MYCACHE}
+
         return config
 
     def setConfig(self, config: dict):
-        if 'xUnitList' in config.keys():
-            self.setUnits('x', config['xUnitList'])
-
-        if 'yUnitList' in config.keys():
-            self.setUnits('y', config['yUnitList'])
-
         self.mLastConfig = config
+        field: QgsField = self.layer().fields().at(self.field())
+        defaultExprName = "format('Profile %1 {}',$id)".format(field.name())
+        defaultExprSource = ""
+        # set some defaults
+        if True:
+            for field in self.layer().fields():
+                assert isinstance(field, QgsField)
+                if field.name() == 'name':
+                    defaultExprName = f'"{field.name()}"'
+                if field.name() == 'source':
+                    defaultExprSource = f'"{field.name()}"'
+
+        self.mFieldExpressionName.setExpression(config.get('expressionName', defaultExprName))
+        self.mFieldExpressionSource.setExpression(config.get('expressionSource', defaultExprSource))
         # print('setConfig')
 
-    def resetUnits(self, dim: str):
 
-        if dim == 'x' and 'xUnitList' in self.mLastConfig.keys():
-            self.setUnit('x', self.mLastConfig['xUnitList'])
+class SpectralProfileFieldFormatter(QgsFieldFormatter):
+
+    def __init__(self, *args, **kwds):
+        super(SpectralProfileFieldFormatter, self).__init__(*args, **kwds)
+
+    def id(self) -> str:
+        return EDITOR_WIDGET_REGISTRY_KEY
 
-        if dim == 'y' and 'yUnitList' in self.mLastConfig.keys():
-            self.setUnit('y', self.mLastConfig['yUnitList'])
+    def representValue(self, layer: QgsVectorLayer, fieldIndex: int, config: dict, cache, value):
+
+        if value not in [None, NULL]:
+            return SPECTRAL_PROFILE_FIELD_REPRESENT_VALUE
+        else:
+            return 'Empty'
+        s = ""
 
 
 class SpectralProfileEditorWidgetFactory(QgsEditorWidgetFactory):
@@ -2044,17 +2200,17 @@ class SpectralProfileEditorWidgetFactory(QgsEditorWidgetFactory):
         w = SpectralProfileEditorConfigWidget(layer, fieldIdx, parent)
         key = self.configKey(layer, fieldIdx)
         w.setConfig(self.readConfig(key))
-        w.changed.connect(lambda: self.writeConfig(key, w.config()))
+        w.changed.connect(lambda *args, ww=w, k=key: self.writeConfig(key, ww.config()))
         return w
 
-    def configKey(self, layer: QgsVectorLayer, fieldIdx: int):
+    def configKey(self, layer: QgsVectorLayer, fieldIdx: int) -> typing.Tuple[str, int]:
         """
         Returns a tuple to be used as dictionary key to identify a layer field configuration.
         :param layer: QgsVectorLayer
         :param fieldIdx: int
         :return: (str, int)
         """
-        return (layer.id(), fieldIdx)
+        return layer.id(), fieldIdx
 
     def create(self, layer: QgsVectorLayer, fieldIdx: int, editor: QWidget,
                parent: QWidget) -> SpectralProfileEditorWidgetWrapper:
@@ -2066,7 +2222,9 @@ class SpectralProfileEditorWidgetFactory(QgsEditorWidgetFactory):
         :param parent: QWidget
         :return: SpectralProfileEditorWidgetWrapper
         """
+
         w = SpectralProfileEditorWidgetWrapper(layer, fieldIdx, editor, parent)
+        # self.editWrapper = w
         return w
 
     def writeConfig(self, key: tuple, config: dict):
@@ -2083,16 +2241,16 @@ class SpectralProfileEditorWidgetFactory(QgsEditorWidgetFactory):
         :param key: tuple (str, int), as created with .configKey(layer, fieldIdx)
         :return: {}
         """
-        if key in self.mConfigurations.keys():
-            conf = self.mConfigurations[key]
-        else:
-            # return the very default configuration
-            conf = {'xUnitList': X_UNITS[:],
-                    'yUnitList': Y_UNITS[:]
-                    }
-        # print('Read config')
-        # print((key, conf))
-        return conf
+        return self.mConfigurations.get(key, {})
+
+    def supportsField(self, vl: QgsVectorLayer, fieldIdx: int) -> bool:
+        """
+        :param vl:
+        :param fieldIdx:
+        :return:
+        """
+        field: QgsField = vl.fields().at(fieldIdx)
+        return field.type() == QVariant.ByteArray
 
     def fieldScore(self, vl: QgsVectorLayer, fieldIdx: int) -> int:
         """
@@ -2108,21 +2266,24 @@ class SpectralProfileEditorWidgetFactory(QgsEditorWidgetFactory):
         # log(' fieldScore()')
         field = vl.fields().at(fieldIdx)
         assert isinstance(field, QgsField)
-        if field.type() == QVariant.String and field.name() == FIELD_VALUES:
+        if field.type() == QVariant.ByteArray:
             return 20
-        elif field.type() == QVariant.String:
-            return 0
         else:
             return 0
 
 
 def registerSpectralProfileEditorWidget():
-    reg = QgsGui.editorWidgetRegistry()
+    widgetRegistry = QgsGui.editorWidgetRegistry()
+    fieldFormaterRegistry = QgsApplication.instance().fieldFormatterRegistry()
 
-    if not EDITOR_WIDGET_REGISTRY_KEY in reg.factories().keys():
+    if not EDITOR_WIDGET_REGISTRY_KEY in widgetRegistry.factories().keys():
         global SPECTRAL_PROFILE_EDITOR_WIDGET_FACTORY
+        global SPECTRAL_PROFILE_FIELD_FORMATTER
         SPECTRAL_PROFILE_EDITOR_WIDGET_FACTORY = SpectralProfileEditorWidgetFactory(EDITOR_WIDGET_REGISTRY_KEY)
-        reg.registerWidget(EDITOR_WIDGET_REGISTRY_KEY, SPECTRAL_PROFILE_EDITOR_WIDGET_FACTORY)
+        SPECTRAL_PROFILE_FIELD_FORMATTER = SpectralProfileFieldFormatter()
+        widgetRegistry.registerWidget(EDITOR_WIDGET_REGISTRY_KEY, SPECTRAL_PROFILE_EDITOR_WIDGET_FACTORY)
+        fieldFormaterRegistry.addFieldFormatter(SPECTRAL_PROFILE_FIELD_FORMATTER)
+        s = ""
 
 
 class SpectralLibraryWidget(AttributeTableWidget):
@@ -2211,7 +2372,7 @@ class SpectralLibraryWidget(AttributeTableWidget):
 
         self.actionImportVectorRasterSource = QAction('Import profiles from raster + vector source')
         self.actionImportVectorRasterSource.setToolTip('Import spectral profiles from a raster image '
-                                                 'based on vector geometries (Points).')
+                                                       'based on vector geometries (Points).')
         self.actionImportVectorRasterSource.setIcon(QIcon(':/images/themes/default/mActionAddOgrLayer.svg'))
         self.actionImportVectorRasterSource.triggered.connect(self.onImportFromRasterSource)
 
@@ -2252,7 +2413,7 @@ class SpectralLibraryWidget(AttributeTableWidget):
 
         self.insertToolBar(self.mToolbar, self.tbSpeclibAction)
 
-        self.actionShowProperties = QAction('Show Spectral Library Poperties')
+        self.actionShowProperties = QAction('Show Spectral Library Properties')
         self.actionShowProperties.setToolTip('Show Spectral Library Properties')
         self.actionShowProperties.setIcon(QIcon(':/images/themes/default/propertyicons/system.svg'))
         self.actionShowProperties.triggered.connect(self.showProperties)
@@ -2371,7 +2532,6 @@ class SpectralLibraryWidget(AttributeTableWidget):
             for iface in separated:
                 iface.addExportActions(self.speclib(), menu)
 
-
     def plotWidget(self) -> SpectralLibraryPlotWidget:
         return self.mPlotWidget
 
@@ -2411,7 +2571,7 @@ class SpectralLibraryWidget(AttributeTableWidget):
         Adds all current spectral profiles to the "persistent" SpectralLibrary
         """
 
-        fids = self.currentProfileIds()
+        fids = self.plotWidget().currentProfileIDs()
         self.plotWidget().mTEMPORARY_HIGHLIGHTED.clear()
         self.plotWidget().updateProfileStyles(fids)
 
@@ -2429,15 +2589,15 @@ class SpectralLibraryWidget(AttributeTableWidget):
         #  stop plot updates
         plotWidget.mUpdateTimer.stop()
         restart_editing = not speclib.startEditing()
-        oldCurrentIds = self.currentProfileIds()
-
+        oldCurrentKeys = self.plotWidget().currentProfileKeys()
+        oldCurrentIDs = self.plotWidget().currentProfileIDs()
         addAuto: bool = self.optionAddCurrentProfilesAutomatically.isChecked()
 
         if not addAuto:
             # delete previous current profiles from speclib
-            speclib.deleteFeatures(oldCurrentIds)
-            plotWidget.removeSpectralProfilePDIs(oldCurrentIds, updateScene=False)
-            # now there should'nt be any PDI or style ref related to an old ID
+            speclib.deleteFeatures(oldCurrentIDs)
+            plotWidget.removeSpectralProfilePDIs(oldCurrentKeys, updateScene=False)
+            # now there shouldn't be any PDI or style ref related to an old ID
         else:
             self.addCurrentSpectraToSpeclib()
 
@@ -2447,7 +2607,7 @@ class SpectralLibraryWidget(AttributeTableWidget):
             p = currentProfiles[i]
             assert isinstance(p, QgsFeature)
             if not isinstance(p, SpectralProfile):
-                p = SpectralProfile.fromSpecLibFeature(p)
+                p = SpectralProfile.fromQgsFeature(p)
                 currentProfiles[i] = p
 
         # add current profiles to speclib
@@ -2459,7 +2619,12 @@ class SpectralLibraryWidget(AttributeTableWidget):
             speclib.startEditing()
 
         addedIDs = sorted(set(speclib.allFeatureIds()).difference(oldIDs))
+        addedKeys = []
+        value_fields = [f.name() for f in self.speclib().spectralValueFields()]
 
+        for id in addedIDs:
+            for n in value_fields:
+                addedKeys.append((id, n))
         # set profile style
         PROFILE2FID = dict()
         for p, fid in zip(currentProfiles, addedIDs):
@@ -2479,19 +2644,12 @@ class SpectralLibraryWidget(AttributeTableWidget):
 
         if not addAuto:
             # give current spectra the current spectral style
-            self.plotWidget().mTEMPORARY_HIGHLIGHTED.update(addedIDs)
+            self.plotWidget().mTEMPORARY_HIGHLIGHTED.update(addedKeys)
 
         plotWidget.mUpdateTimer.start()
 
-    def currentProfileIds(self) -> typing.List[int]:
-        return sorted(self.plotWidget().mTEMPORARY_HIGHLIGHTED)
-
     def currentProfiles(self) -> typing.List[SpectralProfile]:
-        """
-        Returns the SpectralProfiles which are not added to the SpectralLibrary but shown as over-plot items
-        :return: [list-of-SpectralProfiles]
-        """
-        return list(self.speclib().profiles(self.currentProfileIds()))
+        return self.mPlotWidget.currentProfiles()
 
     def canvas(self) -> QgsMapCanvas:
         """
@@ -2560,16 +2718,7 @@ class SpectralLibraryWidget(AttributeTableWidget):
         """
         Removes all SpectralProfiles and additional fields
         """
-        feature_ids = self.speclib().allFeatureIds()
-        self.speclib().startEditing()
-        self.speclib().deleteFeatures(feature_ids)
-        self.speclib().commitChanges()
-
-        for fieldName in self.speclib().optionalFieldNames():
-            index = self.spectralLibrary().fields().indexFromName(fieldName)
-            self.spectralLibrary().startEditing()
-            self.spectralLibrary().deleteAttribute(index)
-            self.spectralLibrary().commitChanges()
+        warnings.warn('Deprectated and desimplemented', DeprecationWarning)
 
 
 class SpectralLibraryInfoLabel(QLabel):
@@ -2603,37 +2752,33 @@ class SpectralLibraryInfoLabel(QLabel):
 
         # total + filtering
         if stats.filter_mode == QgsAttributeTableFilterModel.ShowFilteredList:
-            needed = stats.features_filtered
-            selected = stats.features_filtered_selected
-            msg += f'{stats.features_filtered}f/'
-            ttp += f'{stats.features_filtered} profiles filtered out of {stats.features_speclib}<br/>'
+            msg += f'{stats.profiles_filtered}f'
+            ttp += f'{stats.profiles_filtered} profiles filtered out of {stats.profiles_total}<br/>'
         else:
             # show all
-            needed = stats.features_speclib
-            selected = stats.features_speclib_selected
-            msg += f'{stats.features_speclib}</span>/'
-            ttp += f'{stats.features_speclib} profiles in total<br/>'
+            msg += f'{stats.profiles_total}'
+            ttp += f'{stats.profiles_total} profiles in total<br/>'
 
         # show selected
-        msg += f'{selected}/'
-        ttp += f'{selected} selected in plot/table<br/>'
-
-        exceeds_limit = needed > stats.features_plotted
-
-        if exceeds_limit:
-            msg += f'<span style="color:red">{stats.features_plotted}({needed})</span>'
-            ttp += f'<span style="color:red">' \
-                   f'{stats.features_plotted} of {needed} profiles plotted<br/>' \
-                   f'<br/>Increase plot limit to show more profiles at same time.' \
-                   f'(Might slow-down plot speed)</span>'
+        msg += f'/{stats.profiles_selected}'
+        ttp += f'{stats.profiles_selected} selected in plot<br/>'
+
+        if stats.profiles_empty > 0:
+            msg += f'/<span style="color:red">{stats.profiles_empty}N</span>'
+            ttp += f'<span style="color:red">At least {stats.profiles_empty} profile fields empty (NULL)<br/>'
+
+        if stats.profiles_error > 0:
+            msg += f'/<span style="color:red">{stats.profiles_error}E</span>'
+            ttp += f'<span style="color:red">At least {stats.profiles_error} profiles ' \
+                   f'can not be converted to X axis unit "{self.plotWidget().xUnit()}" (ERROR)</span><br/>'
+
+        if stats.profiles_plotted >= stats.profiles_plotted_max and stats.profiles_total > stats.profiles_plotted_max:
+            msg += f'/<span style="color:red">{stats.profiles_plotted}</span>'
+            ttp += f'<span style="color:red">{stats.profiles_plotted} profiles plotted. Increase plot ' \
+                   f'limit ({stats.profiles_plotted_max}) to show more at same time.</span><br/>'
         else:
-            msg += f'{stats.features_plotted}'
-            ttp += f'{stats.features_plotted} profiles plotted<br/>'
-
-        if stats.features_with_value_error > 0:
-            msg = f'/<span style="color:red">{stats.features_with_value_error}</span>'
-            ttp = f'<br/><span style="color:red">{stats.features_with_value_error} profiles ' \
-                  f'not convertible to {self.plotWidget().xUnit()}'
+            msg += f'/{stats.profiles_plotted}'
+            ttp += f'{stats.profiles_plotted} profiles plotted<br/>'
 
         msg += '</body></html>'
         ttp += '</p></body></html>'
diff --git a/eotimeseriesviewer/externals/qps/speclib/io/vectorsources.py b/eotimeseriesviewer/externals/qps/speclib/io/vectorsources.py
index 477d944e..7229cb65 100644
--- a/eotimeseriesviewer/externals/qps/speclib/io/vectorsources.py
+++ b/eotimeseriesviewer/externals/qps/speclib/io/vectorsources.py
@@ -68,15 +68,13 @@ class VectorSourceFieldValueConverter(QgsVectorFileWriter.FieldValueConverter):
     def convert(self, fieldIndex: int, value: any):
         if fieldIndex in self.mBLOB2TXT:
             dataDict = decodeProfileValueDict(value)
-            json = encodeProfileValueDict(dataDict, mode=SerializationMode.JSON)
-            return json
+            dataJSON = json.dumps(dataDict)
+            return dataJSON
         return value
 
     def fieldDefinition(self, field: QgsField) -> QgsField:
         if field.name() in self.mBLOB2TXT:
-            f = QgsField(FIELD_VALUES, QVariant.String, 'varchar', comment=field.comment())
-
-            return f
+            return QgsField(field.name(), QVariant.String, 'varchar', comment=field.comment())
         return field
 
 
diff --git a/eotimeseriesviewer/externals/qps/speclib/qgsfunctions.py b/eotimeseriesviewer/externals/qps/speclib/qgsfunctions.py
index 7c62e5e9..32d171fb 100644
--- a/eotimeseriesviewer/externals/qps/speclib/qgsfunctions.py
+++ b/eotimeseriesviewer/externals/qps/speclib/qgsfunctions.py
@@ -23,36 +23,267 @@
     along with this software. If not, see <http://www.gnu.org/licenses/>.
 ***************************************************************************
 """
-import pickle
+import typing
+import string
+import pathlib
+import json
+import sys
+import os
+from qgis.PyQt.QtCore import QCoreApplication
+from qgis.core import QgsExpression, QgsFeature, qgsfunction, QgsFeatureRequest, QgsExpressionFunction, \
+    QgsMessageLog, Qgis, QgsExpressionContext
+from qgis.PyQt.QtCore import QByteArray, QVariant, NULL
+from .core import FIELD_VALUES, decodeProfileValueDict, SpectralProfile, encodeProfileValueDict
 
-from qgis.core import *
+QGS_FUNCTION_GROUP = "Spectral Libraries"
 
-from .core import FIELD_VALUES, decodeProfileValueDict
+QGIS_FUNCTION_INSTANCES = dict()
 
-QGS_FUNCTION_GROUP = "Spectral Libraries"
 
-@qgsfunction(0, QGS_FUNCTION_GROUP)
-def spectralValues(values, feature, parent):
-    """
-    Returns the spectral values dictionary
-    :param values:
-    :param feature:
-    :param parent:
-    :return: dict
-    """
-    if isinstance(feature, QgsFeature):
-        i = feature.fieldNameIndex(FIELD_VALUES)
-        if i >= 0:
-            values = decodeProfileValueDict(feature.attribute(i))
-            return values
-    return None
+class HelpStringMaker(object):
+
+    def __init__(self):
+
+        helpDir = pathlib.Path(__file__).parent / 'function_help'
+        self.mHELP = dict()
+
+        assert helpDir.is_dir()
+
+        for e in os.scandir(helpDir):
+            if e.is_file() and e.name.endswith('.json'):
+                with open(e.path, 'r', encoding='utf-8') as f:
+                    data = json.load(f)
+                    if isinstance(data, dict) and 'name' in data.keys():
+                        self.mHELP[data['name']] = data
+
+    def helpText(self, name: str,
+                 parameters: typing.List[QgsExpressionFunction.Parameter] = []) -> str:
+        """
+        re-implementation of QString QgsExpression::helpText( QString name )
+        to generate similar help strings
+        :param name:
+        :param args:
+        :return:
+        """
+        html = [f'<h3>{name}</h3>']
+        LUT_PARAMETERS = dict()
+        for p in parameters:
+            LUT_PARAMETERS[p.name()] = p
+
+        JSON = self.mHELP.get(name, None)
+        ARGUMENT_DESCRIPTIONS = {}
+        ARGUMENT_NAMES = []
+        if isinstance(JSON, dict):
+            for D in JSON.get('arguments', []):
+                if isinstance(D, dict) and 'arg' in D:
+                    ARGUMENT_NAMES.append(D['arg'])
+                    ARGUMENT_DESCRIPTIONS[D['arg']] = D.get('description', '')
+
+        if not isinstance(JSON, dict):
+            print(f'No help found for {name}', file=sys.stderr)
+            return '\n'.join(html)
+
+        description = JSON.get('description', None)
+        if description:
+            html.append(f'<div class="description"><p>{description}</p></div>')
+
+        arguments = JSON.get('arguments', None)
+        if arguments:
+            hasOptionalArgs: bool = False
+            html.append(f'<h4>Syntax</h4>')
+            syntax = f'<div class="syntax">\n<code>{name}('
+
+            if len(parameters) > 0:
+                delim = ''
+                syntaxParameters = set()
+                for P in parameters:
+                    assert isinstance(P, QgsExpressionFunction.Parameter)
+                    syntaxParameters.add(P.name())
+                    optional: bool = P.optional()
+                    if optional:
+                        hasOptionalArgs = True
+                        syntax += '['
+                    syntax += delim
+                    syntax += f'<span class="argument">{P.name()}'
+                    defaultValue = P.defaultValue()
+                    if isinstance(defaultValue, str):
+                        defaultValue = f"'{defaultValue}'"
+                    if defaultValue not in [None, QVariant()]:
+                        syntax += f'={defaultValue}'
+
+                    syntax += '</span>'
+                    if optional:
+                        syntax += ']'
+                    delim = ','
+                # add other optional arguments from help file
+                for a in ARGUMENT_NAMES:
+                    if a not in syntaxParameters:
+                        pass
+
+            syntax += ')</code>'
+
+            if hasOptionalArgs:
+                syntax += '<br/><br/>[ ] marks optional components'
+            syntax += '</div>'
+            html.append(syntax)
+
+            if len(parameters) > 0:
+                html.append(f'<h4>Arguments</h4>')
+                html.append(f'<div class="arguments"><table>')
+
+                for P in parameters:
+                    assert isinstance(P, QgsExpressionFunction.Parameter)
+
+                    description = ARGUMENT_DESCRIPTIONS.get(P.name(), '')
+                    html.append(f'<tr><td class="argument">{P.name()}</td><td>{description}</td></tr>')
+
+            html.append(f'</table></div>')
+
+        return '\n'.join(html)
+
+
+HM = HelpStringMaker()
+
+"""
+@qgsfunction(args='auto', group='String')
+def format_py(fmt: str, *args):
+    assert isinstance(fmt, str)
+    fmtArgs = args[0:-2]
+    feature, parent = args[-2:]
+
+    return fmt.format(*fmtArgs)
+"""
+class Format_Py(QgsExpressionFunction):
+
+    def __init__(self):
+        group = 'String'
+        name = 'format_py'
+
+        args = [
+            QgsExpressionFunction.Parameter('fmt', optional=False, defaultValue=FIELD_VALUES),
+            QgsExpressionFunction.Parameter('arg1', optional=True),
+            QgsExpressionFunction.Parameter('arg2', optional=True),
+            QgsExpressionFunction.Parameter('argN', optional=True),
+        ]
+        helptext = HM.helpText(name, args)
+        super(Format_Py, self).__init__(name, -1, group, helptext)
+
+    def func(self, values, context: QgsExpressionContext, parent, node):
+        if len(values) == 0 or values[0] in (None, NULL):
+            return None
+        assert isinstance(values[0], str)
+        fmt: str = values[0]
+        fmtArgs = values[1:]
+        try:
+            return fmt.format(*fmtArgs)
+        except:
+            return None
+
+    def usesGeometry(self, node) -> bool:
+        return False
+
+    def referencedColumns(self, node) -> typing.List[str]:
+        return [QgsFeatureRequest.ALL_ATTRIBUTES]
+
+    def handlesNull(self) -> bool:
+        return True
+
+
+class SpectralData(QgsExpressionFunction):
+    def __init__(self):
+        group = QGS_FUNCTION_GROUP
+        name = 'spectralData'
+
+        args = [
+            QgsExpressionFunction.Parameter('field', optional=True, defaultValue=FIELD_VALUES)
+        ]
+
+        helptext = HM.helpText(name, args)
+        super().__init__(name, args, group, helptext)
+
+    def func(self, values, context: QgsExpressionContext, parent, node):
+
+        value_field = values[0]
+        feature = None
+        if context:
+            feature = context.feature()
+        if not isinstance(feature, QgsFeature):
+            return None
+        try:
+            profile = SpectralProfile.fromQgsFeature(feature, value_field=value_field)
+            assert isinstance(profile, SpectralProfile)
+            return profile.values()
+        except Exception as ex:
+            parent.setEvalErrorString(str(ex))
+            return None
+
+    def usesGeometry(self, node) -> bool:
+        return False
+
+    def referencedColumns(self, node) -> typing.List[str]:
+        return [QgsFeatureRequest.ALL_ATTRIBUTES]
+
+    def handlesNull(self) -> bool:
+        return False
+
+
+class SpectralMath(QgsExpressionFunction):
+
+    def __init__(self):
+        group = QGS_FUNCTION_GROUP
+        name = 'spectralMath'
+
+        args = [
+            QgsExpressionFunction.Parameter('expression', optional=False, isSubExpression=True),
+            QgsExpressionFunction.Parameter('field', optional=True, defaultValue=FIELD_VALUES)
+        ]
+        helptext = HM.helpText(name, args)
+        super().__init__(name, args, group, helptext)
+
+    def func(self, values, context: QgsExpressionContext, parent, node):
+
+        expression, value_field = values
+        feature = None
+        if context:
+            feature = context.feature()
+        if not isinstance(feature, QgsFeature):
+            return None
+        try:
+            profile = SpectralProfile.fromQgsFeature(feature, value_field=value_field)
+            assert isinstance(profile, SpectralProfile)
+            values = profile.values()
+            exec(expression, values)
+
+            newProfile = SpectralProfile(values=values)
+            return newProfile.attribute(profile.mValueField)
+        except Exception as ex:
+            parent.setEvalErrorString(str(ex))
+            return None
+
+    def usesGeometry(self, node) -> bool:
+        return True
+
+    def referencedColumns(self, node) -> typing.List[str]:
+        return [QgsFeatureRequest.ALL_ATTRIBUTES]
+
+    def handlesNull(self) -> bool:
+        return True
 
 
 def registerQgsExpressionFunctions():
     """
     Registers functions to support SpectraLibrary handling with QgsExpressions
     """
-    #QgsExpression.registerFunction(plotStyleSymbolFillColor)
-    #QgsExpression.registerFunction(plotStyleSymbol)
-    #QgsExpression.registerFunction(plotStyleSymbolSize)
-    QgsExpression.registerFunction(spectralValues)
+    global QGIS_FUNCTION_INSTANCES
+    for func in [Format_Py(), SpectralMath(), SpectralData()]:
+        QGIS_FUNCTION_INSTANCES[func.name()] = func
+        if QgsExpression.isFunctionName(func.name()):
+            if not QgsExpression.unregisterFunction(func.name):
+                msgtitle = QCoreApplication.translate("UserExpressions", "User expressions")
+                msg = QCoreApplication.translate("UserExpressions",
+                                                 "The user expression {0} already exists and could not be unregistered.").format(
+                    func.name)
+                QgsMessageLog.logMessage(msg + "\n", msgtitle, Qgis.Warning)
+                return None
+        else:
+            QgsExpression.registerFunction(func)
diff --git a/eotimeseriesviewer/externals/qps/speclib/spectralprofileeditorconfigwidget.ui b/eotimeseriesviewer/externals/qps/speclib/spectralprofileeditorconfigwidget.ui
index 651c2f9a..3d572d20 100644
--- a/eotimeseriesviewer/externals/qps/speclib/spectralprofileeditorconfigwidget.ui
+++ b/eotimeseriesviewer/externals/qps/speclib/spectralprofileeditorconfigwidget.ui
@@ -6,74 +6,43 @@
    <rect>
     <x>0</x>
     <y>0</y>
-    <width>408</width>
-    <height>245</height>
+    <width>326</width>
+    <height>396</height>
    </rect>
   </property>
   <property name="windowTitle">
    <string>Form</string>
   </property>
   <layout class="QFormLayout" name="formLayout">
-   <item row="1" column="1">
-    <widget class="QPlainTextEdit" name="tbYUnits"/>
-   </item>
-   <item row="3" column="1">
-    <widget class="QPlainTextEdit" name="tbXUnits"/>
-   </item>
-   <item row="0" column="1">
+   <item row="2" column="0">
     <widget class="QLabel" name="label">
      <property name="text">
-      <string>Units Y Axis (spectral value units)</string>
+      <string>Name</string>
      </property>
     </widget>
    </item>
-   <item row="2" column="1">
+   <item row="3" column="0">
     <widget class="QLabel" name="label_2">
      <property name="text">
-      <string>Units X Axis (band  or wavelength units)</string>
+      <string>Source</string>
      </property>
     </widget>
    </item>
-   <item row="1" column="0">
-    <widget class="QToolButton" name="tbResetY">
-     <property name="text">
-      <string>...</string>
-     </property>
-    </widget>
+   <item row="3" column="1">
+    <widget class="QgsFieldExpressionWidget" name="mFieldExpressionSource"/>
    </item>
-   <item row="3" column="0">
-    <widget class="QToolButton" name="tbResetX">
-     <property name="text">
-      <string>...</string>
-     </property>
-    </widget>
+   <item row="2" column="1">
+    <widget class="QgsFieldExpressionWidget" name="mFieldExpressionName"/>
    </item>
   </layout>
-  <action name="actionResetY">
-   <property name="icon">
-    <iconset>
-     <normaloff>:/images/themes/default/mActionUndo.svg</normaloff>:/images/themes/default/mActionUndo.svg</iconset>
-   </property>
-   <property name="text">
-    <string>Reset</string>
-   </property>
-   <property name="toolTip">
-    <string>Reset to previous values</string>
-   </property>
-  </action>
-  <action name="actionResetX">
-   <property name="icon">
-    <iconset>
-     <normaloff>:/images/themes/default/mActionUndo.svg</normaloff>:/images/themes/default/mActionUndo.svg</iconset>
-   </property>
-   <property name="text">
-    <string>Reset</string>
-   </property>
-   <property name="toolTip">
-    <string>Reset to previous values</string>
-   </property>
-  </action>
  </widget>
+ <customwidgets>
+  <customwidget>
+   <class>QgsFieldExpressionWidget</class>
+   <extends>QWidget</extends>
+   <header>qgsfieldexpressionwidget.h</header>
+  </customwidget>
+ </customwidgets>
  <resources/>
  <connections/>
 </ui>
diff --git a/eotimeseriesviewer/externals/qps/speclib/spectralprofileeditorwidget.ui b/eotimeseriesviewer/externals/qps/speclib/spectralprofileeditorwidget.ui
index 5503b9b3..81ac003f 100644
--- a/eotimeseriesviewer/externals/qps/speclib/spectralprofileeditorwidget.ui
+++ b/eotimeseriesviewer/externals/qps/speclib/spectralprofileeditorwidget.ui
@@ -6,15 +6,30 @@
    <rect>
     <x>0</x>
     <y>0</y>
-    <width>383</width>
-    <height>329</height>
+    <width>322</width>
+    <height>408</height>
    </rect>
   </property>
   <property name="windowTitle">
    <string>Form</string>
   </property>
-  <layout class="QGridLayout" name="gridLayout_2" columnstretch="0,0,0">
-   <item row="5" column="0" colspan="3">
+  <layout class="QGridLayout" name="gridLayout">
+   <property name="leftMargin">
+    <number>2</number>
+   </property>
+   <property name="topMargin">
+    <number>2</number>
+   </property>
+   <property name="rightMargin">
+    <number>2</number>
+   </property>
+   <property name="bottomMargin">
+    <number>2</number>
+   </property>
+   <property name="spacing">
+    <number>2</number>
+   </property>
+   <item row="3" column="0">
     <widget class="QTableView" name="tableView">
      <property name="sizePolicy">
       <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
@@ -28,96 +43,175 @@
        <height>150</height>
       </size>
      </property>
-    </widget>
-   </item>
-   <item row="2" column="1">
-    <widget class="QComboBox" name="cbYUnit">
-     <property name="editable">
-      <bool>false</bool>
+     <property name="styleSheet">
+      <string notr="true">QTableView{
+margin:0;
+padding:1px;
+}</string>
      </property>
+     <attribute name="verticalHeaderMinimumSectionSize">
+      <number>10</number>
+     </attribute>
+     <attribute name="verticalHeaderDefaultSectionSize">
+      <number>10</number>
+     </attribute>
     </widget>
    </item>
    <item row="2" column="0">
-    <widget class="QLabel" name="label_2">
-     <property name="text">
-      <string>Y (band value)</string>
+    <layout class="QFormLayout" name="formLayout">
+     <property name="horizontalSpacing">
+      <number>2</number>
      </property>
-    </widget>
-   </item>
-   <item row="0" column="0">
-    <widget class="QToolButton" name="btnReset">
-     <property name="text">
-      <string>...</string>
+     <property name="verticalSpacing">
+      <number>2</number>
      </property>
-     <property name="icon">
-      <iconset>
-       <normaloff>:/qps/ui/icons/undo_orange.svg</normaloff>:/qps/ui/icons/undo_orange.svg</iconset>
+     <property name="topMargin">
+      <number>0</number>
      </property>
-    </widget>
-   </item>
-   <item row="2" column="2">
-    <widget class="QComboBox" name="cbYUnitDataType">
-     <item>
-      <property name="text">
-       <string>Integer</string>
-      </property>
+     <item row="0" column="0">
+      <widget class="QLabel" name="label_3">
+       <property name="text">
+        <string>Bands</string>
+       </property>
+      </widget>
      </item>
-     <item>
-      <property name="text">
-       <string>Float</string>
-      </property>
+     <item row="1" column="0">
+      <widget class="QLabel" name="label_2">
+       <property name="text">
+        <string>Units</string>
+       </property>
+      </widget>
      </item>
-    </widget>
-   </item>
-   <item row="0" column="2">
-    <widget class="QLabel" name="label_4">
-     <property name="text">
-      <string>Data Type</string>
-     </property>
-    </widget>
-   </item>
-   <item row="0" column="1">
-    <widget class="QLabel" name="label_3">
-     <property name="text">
-      <string>Unit</string>
-     </property>
-    </widget>
-   </item>
-   <item row="3" column="0">
-    <widget class="QLabel" name="label">
-     <property name="text">
-      <string>X (wavelength)</string>
-     </property>
-    </widget>
-   </item>
-   <item row="3" column="2">
-    <widget class="QComboBox" name="cbXUnitDataType">
-     <item>
-      <property name="text">
-       <string>Integer</string>
-      </property>
+     <item row="0" column="1">
+      <layout class="QHBoxLayout" name="horizontalLayout">
+       <property name="spacing">
+        <number>2</number>
+       </property>
+       <property name="topMargin">
+        <number>0</number>
+       </property>
+       <item>
+        <widget class="QgsSpinBox" name="sbBands">
+         <property name="minimumSize">
+          <size>
+           <width>100</width>
+           <height>0</height>
+          </size>
+         </property>
+         <property name="minimum">
+          <number>0</number>
+         </property>
+         <property name="maximum">
+          <number>10000</number>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <spacer name="horizontalSpacer">
+         <property name="orientation">
+          <enum>Qt::Horizontal</enum>
+         </property>
+         <property name="sizeHint" stdset="0">
+          <size>
+           <width>0</width>
+           <height>20</height>
+          </size>
+         </property>
+        </spacer>
+       </item>
+       <item>
+        <widget class="QToolButton" name="btnReset">
+         <property name="text">
+          <string>...</string>
+         </property>
+         <property name="icon">
+          <iconset resource="../qpsresources.qrc">
+           <normaloff>:/qps/ui/icons/undo_orange.svg</normaloff>:/qps/ui/icons/undo_orange.svg</iconset>
+         </property>
+        </widget>
+       </item>
+      </layout>
      </item>
-     <item>
-      <property name="text">
-       <string>Float</string>
-      </property>
+     <item row="1" column="1">
+      <layout class="QHBoxLayout" name="horizontalLayout_2">
+       <property name="topMargin">
+        <number>0</number>
+       </property>
+       <item>
+        <widget class="QLabel" name="label_4">
+         <property name="text">
+          <string>X</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QComboBox" name="cbXUnit">
+         <property name="sizePolicy">
+          <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+           <horstretch>0</horstretch>
+           <verstretch>0</verstretch>
+          </sizepolicy>
+         </property>
+         <property name="editable">
+          <bool>false</bool>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QLabel" name="label">
+         <property name="text">
+          <string>Y</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QLineEdit" name="tbYUnit">
+         <property name="sizePolicy">
+          <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+           <horstretch>0</horstretch>
+           <verstretch>0</verstretch>
+          </sizepolicy>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <spacer name="horizontalSpacer_2">
+         <property name="orientation">
+          <enum>Qt::Horizontal</enum>
+         </property>
+         <property name="sizeHint" stdset="0">
+          <size>
+           <width>0</width>
+           <height>20</height>
+          </size>
+         </property>
+        </spacer>
+       </item>
+      </layout>
      </item>
-    </widget>
-   </item>
-   <item row="3" column="1">
-    <widget class="QComboBox" name="cbXUnit">
-     <property name="editable">
-      <bool>false</bool>
-     </property>
-    </widget>
+    </layout>
    </item>
   </layout>
   <action name="actionReset">
+   <property name="icon">
+    <iconset resource="../../../../QGIS/images/images.qrc">
+     <normaloff>:/images/themes/default/mActionReload.svg</normaloff>:/images/themes/default/mActionReload.svg</iconset>
+   </property>
    <property name="text">
     <string>Reset</string>
    </property>
   </action>
  </widget>
- <resources/>
+ <customwidgets>
+  <customwidget>
+   <class>QgsSpinBox</class>
+   <extends>QSpinBox</extends>
+   <header>qgsspinbox.h</header>
+  </customwidget>
+ </customwidgets>
+ <resources>
+  <include location="../qpsresources.qrc"/>
+  <include location="../../../../QGIS/images/images.qrc"/>
+ </resources>
  <connections/>
 </ui>
diff --git a/eotimeseriesviewer/externals/qps/subdatasets.py b/eotimeseriesviewer/externals/qps/subdatasets.py
index 36e904b3..e73cd592 100644
--- a/eotimeseriesviewer/externals/qps/subdatasets.py
+++ b/eotimeseriesviewer/externals/qps/subdatasets.py
@@ -31,7 +31,7 @@ import sys
 import sip
 import pathlib
 import collections
-from qgis.core import *
+from qgis.core import QgsMapLayer, QgsRasterLayer, QgsTaskManager, QgsApplication, QgsTask
 from qgis.gui import *
 from qgis.PyQt.QtCore import *
 from qgis.PyQt.QtWidgets import *
@@ -39,6 +39,7 @@ from osgeo import gdal
 from .utils import loadUi
 from . import DIR_UI_FILES
 
+
 # read https://gdal.org/user/raster_data_model.html#subdatasets-domain
 
 class SubDatasetType(object):
@@ -55,6 +56,7 @@ class SubDatasetType(object):
             return False
         return self.name == other.name
 
+
 class DatasetInfo(object):
 
     @staticmethod
@@ -122,16 +124,16 @@ class DatasetInfo(object):
             return False
         return self.mReferenceFile == other.mReferenceFile
 
-class SubDatasetLoadingTask(QgsTask):
 
+class SubDatasetLoadingTask(QgsTask):
     sigFoundSubDataSets = pyqtSignal(list)
     sigMessage = pyqtSignal(str, bool)
 
     def __init__(self,
                  files: typing.List[str],
                  description: str = "Collect subdata sets",
-                 callback = None,
-                 block_size : int = 10):
+                 callback=None,
+                 block_size: int = 10):
 
         super().__init__(description=description)
         self.mFiles = files
@@ -159,7 +161,7 @@ class SubDatasetLoadingTask(QgsTask):
                 result_block.clear()
             if self.isCanceled():
                 return False
-            self.setProgress(100 * (i+1) / n)
+            self.setProgress(100 * (i + 1) / n)
 
         if len(result_block) > 0:
             self.sigFoundSubDataSets.emit(result_block[:])
@@ -170,6 +172,7 @@ class SubDatasetLoadingTask(QgsTask):
         if self.mCallback is not None:
             self.mCallback(result, self)
 
+
 class SubDatasetDescriptionModel(QAbstractTableModel):
 
     def __init__(self, *args, **kwds):
@@ -264,6 +267,7 @@ class SubDatasetDescriptionModel(QAbstractTableModel):
             subs = [s for s in subs if s.checked == checked]
         return subs
 
+
 class DatasetTableModel(QAbstractTableModel):
 
     def __init__(self, *args, **kwds):
@@ -416,7 +420,7 @@ class SubDatasetSelectionDialog(QDialog):
         [descriptions.extend(i.subdataset_types()) for i in infos]
         self.subDatasetModel.addSubDatasetDescriptions(descriptions)
 
-    def startTask(self, qgsTask:QgsTask):
+    def startTask(self, qgsTask: QgsTask):
         self.setCursor(Qt.WaitCursor)
         self.fileWidget.setEnabled(False)
         self.fileWidget.lineEdit().setShowSpinner(True)
@@ -440,7 +444,7 @@ class SubDatasetSelectionDialog(QDialog):
         if isinstance(task, SubDatasetLoadingTask) and not sip.isdeleted(task):
             self.onRemoveTask(id(task))
 
-    def onTaskMessage(self, msg: str, is_error:bool):
+    def onTaskMessage(self, msg: str, is_error: bool):
         if is_error:
             print(msg, file=sys.stderr)
         self.setInfo(msg)
@@ -475,4 +479,3 @@ class SubDatasetSelectionDialog(QDialog):
         :param filter:
         """
         self.fileWidget.setFilter(filter)
-
diff --git a/eotimeseriesviewer/externals/qps/testing.py b/eotimeseriesviewer/externals/qps/testing.py
index 6b645d4e..406c429c 100644
--- a/eotimeseriesviewer/externals/qps/testing.py
+++ b/eotimeseriesviewer/externals/qps/testing.py
@@ -326,19 +326,22 @@ class QgisMockup(QgisInterface):
         m = self.ui.menuBar().addAction('Add Vector')
         m = self.ui.menuBar().addAction('Add Raster')
 
-    def mapCanvas(self):
+    def mapCanvas(self) -> QgsMapCanvas:
         return self.mCanvas
 
+    def mapCanvases(self) -> typing.List[QgsMapCanvas]:
+        return [self.mCanvas]
+
     def mapNavToolToolBar(self) -> QToolBar:
         return self.mMapNavToolBar
 
-    def messageBar(self, *args, **kwargs):
+    def messageBar(self, *args, **kwargs) -> QgsMessageBar:
         return self.mMessageBar
 
-    def rasterMenu(self):
+    def rasterMenu(self) -> QMenu:
         return self.mRasterMenu
 
-    def vectorMenu(self):
+    def vectorMenu(self) -> QMenu:
         return self.mVectorMenu
 
     def viewMenu(self) -> QMenu:
diff --git a/eotimeseriesviewer/externals/qps/ui/cursorlocationinfodock.ui b/eotimeseriesviewer/externals/qps/ui/cursorlocationinfodock.ui
index 40e74d00..6eb782d3 100644
--- a/eotimeseriesviewer/externals/qps/ui/cursorlocationinfodock.ui
+++ b/eotimeseriesviewer/externals/qps/ui/cursorlocationinfodock.ui
@@ -82,7 +82,7 @@
      </layout>
     </item>
     <item>
-     <widget class="TreeView" name="treeView"/>
+     <widget class="CursorLocationInfoTreeView" name="mTreeView"/>
     </item>
     <item>
      <layout class="QHBoxLayout" name="horizontalLayout_4">
@@ -284,9 +284,9 @@
    <container>1</container>
   </customwidget>
   <customwidget>
-   <class>TreeView</class>
+   <class>CursorLocationInfoTreeView</class>
    <extends>QTreeView</extends>
-   <header>qps.models</header>
+   <header>qps.cursorlocationvalue</header>
   </customwidget>
  </customwidgets>
  <resources>
diff --git a/eotimeseriesviewer/externals/qps/ui/gdalmetadatamodelwidget.ui b/eotimeseriesviewer/externals/qps/ui/gdalmetadatamodelwidget.ui
index dbc954b7..e3da6e9d 100644
--- a/eotimeseriesviewer/externals/qps/ui/gdalmetadatamodelwidget.ui
+++ b/eotimeseriesviewer/externals/qps/ui/gdalmetadatamodelwidget.ui
@@ -17,7 +17,7 @@
    <iconset resource="../qpsresources.qrc">
     <normaloff>:/qps/ui/icons/rasterband_select.svg</normaloff>:/qps/ui/icons/rasterband_select.svg</iconset>
   </property>
-  <layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,0,0">
+  <layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,0,0,0">
    <property name="spacing">
     <number>2</number>
    </property>
@@ -33,6 +33,16 @@
    <property name="bottomMargin">
     <number>2</number>
    </property>
+   <item>
+    <widget class="QgsMessageBar" name="mMessageBar">
+     <property name="frameShape">
+      <enum>QFrame::StyledPanel</enum>
+     </property>
+     <property name="frameShadow">
+      <enum>QFrame::Raised</enum>
+     </property>
+    </widget>
+   </item>
    <item>
     <widget class="QgsCollapsibleGroupBox" name="gbBandNames">
      <property name="sizePolicy">
@@ -231,7 +241,7 @@
            <string>...</string>
           </property>
           <property name="icon">
-           <iconset resource="../../../QGIS/images/images.qrc">
+           <iconset>
             <normaloff>:/images/themes/default/symbologyAdd.svg</normaloff>:/images/themes/default/symbologyAdd.svg</iconset>
           </property>
           <property name="autoRaise">
@@ -245,7 +255,7 @@
            <string>...</string>
           </property>
           <property name="icon">
-           <iconset resource="../../../QGIS/images/images.qrc">
+           <iconset>
             <normaloff>:/images/themes/default/symbologyRemove.svg</normaloff>:/images/themes/default/symbologyRemove.svg</iconset>
           </property>
           <property name="autoRaise">
@@ -325,7 +335,7 @@
   </action>
   <action name="actionAddItem">
    <property name="icon">
-    <iconset resource="../../../QGIS/images/images.qrc">
+    <iconset>
      <normaloff>:/images/themes/default/symbologyAdd.svg</normaloff>:/images/themes/default/symbologyAdd.svg</iconset>
    </property>
    <property name="text">
@@ -337,7 +347,7 @@
   </action>
   <action name="actionRemoveItem">
    <property name="icon">
-    <iconset resource="../../../QGIS/images/images.qrc">
+    <iconset>
      <normaloff>:/images/themes/default/symbologyRemove.svg</normaloff>:/images/themes/default/symbologyRemove.svg</iconset>
    </property>
    <property name="text">
@@ -349,7 +359,7 @@
   </action>
   <action name="actionReset">
    <property name="icon">
-    <iconset resource="../../../QGIS/images/images.qrc">
+    <iconset>
      <normaloff>:/images/themes/default/mIconClearText.svg</normaloff>:/images/themes/default/mIconClearText.svg</iconset>
    </property>
    <property name="text">
@@ -385,6 +395,12 @@
    <extends>QTableView</extends>
    <header>qps.layerconfigwidgets.gdalmetadata</header>
   </customwidget>
+  <customwidget>
+   <class>QgsMessageBar</class>
+   <extends>QFrame</extends>
+   <header>qgis.gui</header>
+   <container>1</container>
+  </customwidget>
  </customwidgets>
  <resources>
   <include location="../qpsresources.qrc"/>
diff --git a/eotimeseriesviewer/externals/qps/unitmodel.py b/eotimeseriesviewer/externals/qps/unitmodel.py
index 6f7fa836..6f181ef9 100644
--- a/eotimeseriesviewer/externals/qps/unitmodel.py
+++ b/eotimeseriesviewer/externals/qps/unitmodel.py
@@ -191,3 +191,7 @@ class XUnitModel(UnitModel):
         self.addUnit('DecimalYear', description='Date [Decimal Year]')
         self.addUnit('DOY', description='Day of Year [DOY]')
 
+    def findUnit(self, unit):
+        if unit in [None, NULL]:
+            unit = BAND_INDEX
+        return super(XUnitModel, self).findUnit(unit)
\ No newline at end of file
diff --git a/eotimeseriesviewer/externals/qps/utils.py b/eotimeseriesviewer/externals/qps/utils.py
index 57eda376..e0e99518 100644
--- a/eotimeseriesviewer/externals/qps/utils.py
+++ b/eotimeseriesviewer/externals/qps/utils.py
@@ -59,6 +59,11 @@ from qgis.PyQt.QtWidgets import *
 from osgeo import gdal, ogr, osr, gdal_array
 import numpy as np
 from qgis.PyQt.QtWidgets import QAction, QMenu, QToolButton, QDialogButtonBox, QLabel, QGridLayout, QMainWindow
+
+try:
+    from .. import qps
+except:
+    import qps
 from . import DIR_UI_FILES
 
 # dictionary to store form classes and avoid multiple calls to read <myui>.i
@@ -66,11 +71,6 @@ QGIS_RESOURCE_WARNINGS = set()
 
 REMOVE_setShortcutVisibleInContextMenu = hasattr(QAction, 'setShortcutVisibleInContextMenu')
 
-try:
-    from .. import qps
-except:
-    import qps
-
 jp = os.path.join
 dn = os.path.dirname
 
@@ -577,6 +577,8 @@ def createQgsField(name: str, exampleValue: typing.Any, comment: str = None) ->
         return QgsField(name, QVariant.String, 'varchar', comment=comment)
     elif isinstance(exampleValue, np.datetime64):
         return QgsField(name, QVariant.String, 'varchar', comment=comment)
+    elif isinstance(exampleValue, (bytes, QByteArray)):
+        return QgsField(name, QVariant.ByteArray, 'Binary', comment=comment)
     elif isinstance(exampleValue, list):
         assert len(exampleValue) > 0, 'need at least one value in provided list'
         v = exampleValue[0]
@@ -682,7 +684,7 @@ def gdalDataset(dataset: typing.Union[str,
                                       QgsRasterLayer,
                                       QgsRasterDataProvider,
                                       gdal.Dataset],
-                eAccess:int = gdal.GA_ReadOnly) -> gdal.Dataset:
+                eAccess: int = gdal.GA_ReadOnly) -> gdal.Dataset:
     """
     Returns a gdal.Dataset object instance
     :param dataset:
@@ -899,6 +901,24 @@ def qgsRasterLayer(source) -> QgsRasterLayer:
     raise Exception('Unable to transform {} into QgsRasterLayer'.format(source))
 
 
+def qgsRasterLayers(sources) -> typing.Iterator[QgsRasterLayer]:
+    """
+    Like qgsRasterLayer, but on multiple inputs and with extraction of sub-layers
+    :param sources:
+    :return:
+    """
+    if not isinstance(sources, list):
+        sources = [sources]
+    assert isinstance(sources, list)
+
+    for source in sources:
+        lyr: QgsRasterLayer = qgsRasterLayer(source)
+        if lyr.isValid():
+            yield lyr
+        for lyr in qgsRasterLayers(lyr.subLayers()):
+            yield lyr
+
+
 def qgsMapLayer(value: typing.Any) -> QgsMapLayer:
     """
     Tries to convert the input into a QgsMapLayer
@@ -1549,19 +1569,14 @@ def parseWavelength(dataset) -> typing.Tuple[np.ndarray, str]:
     def checkWavelengthUnit(key: str, value: str) -> str:
         wlu = None
         value = value.strip()
-        if re.search(r'wavelength.units?', key, re.I):
+        if re.search(r'wavelength[ _]?units?', key, re.I):
             # metric length units
-            if re.search(r'^(Micrometers?|um|μm)$', values, re.I):
-                wlu = 'μm'  # fix with python 3 UTF
-            elif re.search(r'^(Nanometers?|nm)$', values, re.I):
-                wlu = 'nm'
-            elif re.search(r'^(Millimeters?|mm)$', values, re.I):
-                wlu = 'nm'
-            elif re.search(r'^(Centimeters?|cm)$', values, re.I):
-                wlu = 'nm'
-            elif re.search(r'^(Meters?|m)$', values, re.I):
-                wlu = 'nm'
-            elif re.search(r'^Wavenumber$', values, re.I):
+            wlu = UnitLookup.baseUnit(value)
+
+            if wlu is not None:
+                return wlu
+
+            if re.search(r'^Wavenumber$', values, re.I):
                 wlu = '-'
             elif re.search(r'^GHz$', values, re.I):
                 wlu = 'GHz'
diff --git a/eotimeseriesviewer/externals/qps/vectorlayertools.py b/eotimeseriesviewer/externals/qps/vectorlayertools.py
index f21ed959..0ba69a0d 100644
--- a/eotimeseriesviewer/externals/qps/vectorlayertools.py
+++ b/eotimeseriesviewer/externals/qps/vectorlayertools.py
@@ -107,10 +107,6 @@ class VectorLayerTools(QgsVectorLayerTools):
                 and isinstance(qgis.utils.iface, QgisInterface):
                 qgis.utils.iface.pasteFromClipboard(layer)
 
-    def deleteSelected(self, layer: QgsVectorLayer):
-        if isinstance(layer, QgsVectorLayer) and layer.isEditable():
-            layer.deleteSelectedFeatures()
-
     def invertSelection(self, layer: QgsVectorLayer):
         if isinstance(layer, QgsVectorLayer):
             layer.invertSelection()
@@ -125,7 +121,7 @@ class VectorLayerTools(QgsVectorLayerTools):
 
     def deleteSelection(self, layer: QgsVectorLayer):
         if isinstance(layer, QgsVectorLayer) and layer.isEditable():
-            layer
+            layer.deleteSelectedFeatures()
 
     def toggleEditing(self, vlayer: QgsVectorLayer, allowCancel: bool = True) -> bool:
         """
-- 
GitLab