From c4adfccbb23d014da38edf5923d78fd1c80e377f Mon Sep 17 00:00:00 2001
From: "benjamin.jakimow" <benjamin.jakimow@geo.hu-berlin.de>
Date: Tue, 22 May 2018 18:32:37 +0200
Subject: [PATCH] initQgisApplication - added
 QgsGui.editorWidgetRegistry().initEditors() SpectralProfile inherits
 QgsFeature SpectraLibrary inherits QgsVectorLayer

---
 test/test_spectrallibraries.py        | 165 ++++++++--
 timeseriesviewer/spectrallibraries.py | 416 ++++++++++++++++----------
 timeseriesviewer/utils.py             |   2 +
 3 files changed, 396 insertions(+), 187 deletions(-)

diff --git a/test/test_spectrallibraries.py b/test/test_spectrallibraries.py
index 81a2c36b..e99f35d9 100644
--- a/test/test_spectrallibraries.py
+++ b/test/test_spectrallibraries.py
@@ -19,6 +19,7 @@
 # noinspection PyPep8Naming
 import os, sys, unittest, tempfile
 from timeseriesviewer.utils import initQgisApplication
+from example.Images import Img_2014_06_16_LE72270652014167CUB00_BOA, re_2014_06_25
 qapp = initQgisApplication()
 
 from timeseriesviewer.spectrallibraries import *
@@ -29,65 +30,175 @@ class TestInit(unittest.TestCase):
 
         self.SP = None
         self.SPECLIB = None
+        self.lyr1 = QgsRasterLayer(Img_2014_06_16_LE72270652014167CUB00_BOA)
+        self.lyr2 = QgsRasterLayer(re_2014_06_25)
+        self.layers = [self.lyr1, self.lyr2]
+        QgsProject.instance().addMapLayers(self.layers)
 
     def test_spectralprofile(self):
-        spec1 = SpectralProfile()
 
+        canvas = QgsMapCanvas()
+        canvas.setLayers(self.layers)
+        canvas.setExtent(self.lyr2.extent())
+        canvas.setDestinationCrs(self.lyr1.crs())
+        pos = SpatialPoint(self.lyr2.crs(), *self.lyr2.extent().center())
+        profiles = SpectralProfile.fromMapCanvas(canvas, pos)
+        self.assertIsInstance(profiles, list)
+        self.assertEqual(len(profiles), 2)
+        for p in profiles:
+            self.assertIsInstance(p, SpectralProfile)
 
-        spec1.setValues([0,4,3,2,1],['-'], [450,500,750, 1000, 1500], 'nm')
+        sp1 = SpectralProfile()
+        yVal = [0.23, 0.4, 0.3, 0.8, 0.7]
+        xVal = [300,400, 600, 1200, 2500]
 
+        #default: empty profile
+        self.assertEqual(sp1.xUnit(), 'index')
+        self.assertEqual(sp1.yUnit(), None)
+
+        sp1.setYValues(yVal)
+        self.assertTrue(np.array_equal(sp1.yValues(), np.asarray(yVal)))
+        self.assertTrue(np.array_equal(sp1.xValues(), np.arange(len(yVal))))
+        self.assertEqual(sp1.xUnit(), 'index')
+        self.assertEqual(sp1.yUnit(), None)
+
+        sp1.setYUnit('reflectance')
+        self.assertEqual(sp1.yUnit(), 'reflectance')
+
+
+
+
+
+        sp1.setXValues(xVal)
+        sp1.setYValues(yVal)
+        name = 'missingAttribute'
+        sp1.setMetadata(name, 'myvalue')
+        self.assertTrue(name not in sp1.fieldNames())
+        sp1.setMetadata(name, 'myvalue', addMissingFields=True)
+        self.assertTrue(name in sp1.fieldNames())
+        self.assertEqual(sp1.metadata(name), 'myvalue')
+        sp1.removeField(name)
+        self.assertTrue(name not in sp1.fieldNames())
+        self.assertIsInstance(sp1.xValues(), list)
+        self.assertIsInstance(sp1.yValues(), list)
+
+        sp1.setXUnit('nm')
+        self.assertEqual(sp1.xUnit(), 'nm')
+        self.assertTrue(np.array_equal(xVal, sp1.xValues()))
+
+        self.assertEqual(sp1, sp1)
+
+
+        for sp2 in[sp1.clone(), copy.copy(sp1), sp1.__copy__()]:
+            self.assertIsInstance(sp2, SpectralProfile)
+            self.assertEqual(sp1, sp2)
+
+
+        dump = pickle.dumps(sp1)
+        sp2 = pickle.loads(dump)
+
+        self.assertEqual(sp1, sp2)
+
+        sp2 = SpectralProfile(xUnit='nm')
+        #sp2.setValues(yVal, xValues=xVal)
+        sp2.setXValues(xVal)
+        sp2.setYValues(yVal)
+
+        self.assertNotEqual(sp1, sp2)
+
+        sp2.setYUnit('reflectance')
+        self.assertEqual(sp1, sp2)
 
         values = [('key','value'),('key', 100),('Üä','ÜmlÄute')]
         for md in values:
             k, v = md
-            spec1.setMetadata(k,v)
-            v2 = spec1.metadata(k)
-            self.assertEqual(v,v2)
+            sp1.setMetadata(k,v)
+            v2 = sp1.metadata(k)
+            self.assertEqual(v2, None)
 
-        self.SP = spec1
+        for md in values:
+            k, v = md
+            sp1.setMetadata(k, v, addMissingFields=True)
+            v2 = sp1.metadata(k)
+            self.assertEqual(v, v2)
+
+        self.SP = sp1
 
     def test_spectralLibrary(self):
 
-        spec1 = SpectralProfile()
-        spec1.setValues([0, 4, 3, 2, 1], ['-'], [450, 500, 750, 1000, 1500], 'nm')
+        sp1 = SpectralProfile()
+        sp1.setYValues([0, 4, 3, 2, 1])
+        sp1.setXValues([450, 500, 750, 1000, 1500])
+
+
+        sp2 = SpectralProfile()
+        sp2.setYValues([3, 2, 1, 0, 1])
+        sp2.setXValues([450, 500, 750, 1000, 1500])
+
+        sl1 = SpectralLibrary()
+
+        self.assertEqual(sl1.name(), 'SpectralLibrary')
+        sl1.setName('MySpecLib')
+        self.assertEqual(sl1.name(), 'MySpecLib')
+
+        sl1.addProfiles([sp1, sp2])
+        self.assertEqual(len(sl1),2)
+        t = sl1[0:1]
+        self.assertIsInstance(sl1[0], SpectralProfile)
+        self.assertEqual(sl1[0], sp1)
+        self.assertEqual(sl1[1], sp2)
+        self.assertNotEqual(sl1[0], sp2)
+
+        dump = pickle.dumps(sl1)
+
+        sl2 = pickle.loads(dump)
+        self.assertIsInstance(sl2, SpectralLibrary)
+        self.assertEqual(sl1, sl2)
 
-        spec2 = SpectralProfile()
-        spec2.setValues([3, 2, 1, 0, 1], ['-'], [450, 500, 750, 1000, 1500], 'nm')
+        sl2.addProfiles([sp2])
+        self.assertNotEqual(sl1, sl2)
+        self.assertEqual(sl2[2], sp2)
 
-        sl = SpectralLibrary()
-        sl.addProfiles([spec1, spec2])
-        self.assertEqual(len(sl),2)
-        self.assertEqual(sl[0], spec1)
 
+        #read from image
+
+        #sl1.plot()
 
         tempDir = tempfile.gettempdir()
         pathESL = tempfile.mktemp(prefix='speclib.', suffix='.esl')
         pathCSV = tempfile.mktemp(prefix='speclib.', suffix='.csv')
-        try:
-            sl.exportProfiles(pathESL)
-        except Exception as ex:
-            self.fail('Unable to write ESL. {}'.format(ex))
 
+        #test ENVI Spectral Library
         try:
-            sl2 = SpectralLibrary.readFrom(pathESL)
+            writtenFiles = sl1.exportProfiles(pathESL)
         except Exception as ex:
-            self.fail('Unable to read ESL. {}'.format(ex))
-
+            self.fail('Unable to write ESL. {}'.format(ex))
+        for f in writtenFiles:
+            self.assertTrue(os.path.isfile(f))
+            try:
+                self.assertTrue(EnviSpectralLibraryIO.canRead(f))
+                sl = EnviSpectralLibraryIO.readFrom(f)
+                self.assertIsInstance(sl, SpectralLibrary)
+                sl = SpectralLibrary.readFrom(f)
+            except Exception as ex:
+                self.fail('Failed SpectralLibrary.readFrom(p) p = {}\n{}'.format(f, ex))
+            self.assertIsInstance(sl, SpectralLibrary)
 
         try:
-            sl.exportProfiles(pathCSV)
+            writtenFiles = sl1.exportProfiles(pathCSV)
         except Exception as ex:
             self.fail('Unable to write CSV. {}'.format(ex))
 
-        try:
-            sl2 = SpectralLibrary.readFrom(pathCSV)
-        except Exception as ex:
-            self.fail('Unable to read CSV. {}'.format(ex))
+        for f in writtenFiles:
+            try:
+                sl1 = SpectralLibrary.readFrom(f)
+            except Exception as ex:
+                self.fail('Unable to read CSV. {}'.format(ex))
 
 
 
 
-        self.SPECLIB = sl
+        self.SPECLIB = sl1
 
     def test_speclibWidget(self):
         p = SpectralLibraryWidget()
diff --git a/timeseriesviewer/spectrallibraries.py b/timeseriesviewer/spectrallibraries.py
index 2c80c9e5..b2a26b3a 100644
--- a/timeseriesviewer/spectrallibraries.py
+++ b/timeseriesviewer/spectrallibraries.py
@@ -303,10 +303,11 @@ class SpectralProfileMapTool(QgsMapToolEmitPoint):
 
 class SpectralProfilePlotDataItem(pg.PlotDataItem):
 
-    def __init__(self, spectralProfle):
-        assert isinstance(spectralProfle, SpectralProfile)
-        super(SpectralProfilePlotDataItem, self).__init__(spectralProfle.xValues(), spectralProfle.yValues())
-        self.mProfile = spectralProfle
+    def __init__(self, spectralProfile):
+        assert isinstance(spectralProfile, SpectralProfile)
+        super(SpectralProfilePlotDataItem, self).__init__()
+        self.mProfile = spectralProfile
+        self.setData(x=spectralProfile.xValues(), y=spectralProfile.yValues())
 
     def setClickable(self, b, width=None):
         assert isinstance(b, bool)
@@ -314,12 +315,10 @@ class SpectralProfilePlotDataItem(pg.PlotDataItem):
 
     def setColor(self, color):
         if not isinstance(color, QColor):
-
             color = QColor(color)
         self.setPen(color)
 
     def pen(self):
-
         return fn.mkPen(self.opts['pen'])
 
     def color(self):
@@ -383,7 +382,7 @@ class SpectralProfile(QgsFeature):
             px = position
         elif isinstance(position, SpatialPoint):
             px = geo2px(position.toCrs(crs), gt)
-        elif isinstance(position, QgsPoint):
+        elif isinstance(position, QgsPointXY):
             px = geo2px(position, ds.GetGeoTransform())
         else:
             raise Exception('Unsupported type of argument "position" {}'.format('{}'.format(position)))
@@ -401,14 +400,18 @@ class SpectralProfile(QgsFeature):
             if nodata and values[b] == nodata:
                 return None
 
-        wl = ds.GetMetadataItem(str('wavelength'),str('ENVI'))
-        wlu = ds.GetMetadataItem(str('wavelength_units'),str('ENVI'))
+        wl = ds.GetMetadataItem(str('wavelength'), str('ENVI'))
+        wlu = ds.GetMetadataItem(str('wavelength_units'), str('ENVI'))
         if wl is not None and len(wl) > 0:
             wl = re.sub(r'[ {}]','', wl).split(',')
             wl = [float(w) for w in wl]
 
         profile = SpectralProfile()
-        profile.setValues(values, valuePositions=wl, valuePositionUnit=wlu)
+        #profile.setValues(values, valuePositions=wl, valuePositionUnit=wlu)
+        profile.setYValues(values)
+        if wl is not None:
+            profile.setXValues(wl, unit=wlu)
+
         profile.setCoordinates(px=px, pt=SpatialPoint(crs, px2geo(px, gt)))
         profile.setSource('{}'.format(ds.GetFileList()[0]))
         return profile
@@ -416,68 +419,90 @@ class SpectralProfile(QgsFeature):
     @staticmethod
     def createQgsField(name : str, exampleValue):
         if isinstance(exampleValue, str):
-            return QgsField(name, QVariant.String, 'varchar', 10)
+            return QgsField(name, QVariant.String, 'varchar')
         elif isinstance(exampleValue, int):
             return QgsField(name, QVariant.String, 'int')
         elif isinstance(exampleValue, float):
             return QgsField(name, QVariant.String, 'float')
+        elif isinstance(exampleValue, np.ndarray):
+            return QgsField(name, QVariant.String, 'varchar')
         else:
             raise NotImplemented()
 
-    def __init__(self, parent=None, fields=None):
+    @staticmethod
+    def standardFields():
+        fields = QgsFields()
+
+        """
+        Parameters
+        name Field name type Field variant type, currently supported: String / Int / Double 
+        typeName Field type (e.g., char, varchar, text, int, serial, double). Field types are usually unique to the source and are stored exactly as returned from the data store. 
+        len Field length 
+        prec Field precision. Usually decimal places but may also be used in conjunction with other fields types (e.g., variable character fields) 
+        comment Comment for the field 
+        subType If the field is a collection, its element's type. When all the elements don't need to have the same type, leave this to QVariant::Invalid. 
+        """
+        fields.append(SpectralProfile.createQgsField('name', ''))
+        fields.append(SpectralProfile.createQgsField('px_x', 0))
+        fields.append(SpectralProfile.createQgsField('px_y', 0))
+        fields.append(SpectralProfile.createQgsField('x_unit', ''))
+        fields.append(SpectralProfile.createQgsField('y_unit', ''))
+        fields.append(SpectralProfile.createQgsField('source', ''))
+        fields.append(SpectralProfile.createQgsField('__serialized__xvalues', ''))
+        fields.append(SpectralProfile.createQgsField('__serialized__yvalues', ''))
+
 
 
+        """
+        fields.append(QgsField('name', QVariant.String,'varchar', 25))
+        fields.append(QgsField('px_x', QVariant.Int, 'int'))
+        fields.append(QgsField('px_y', QVariant.Int, 'int'))
+        fields.append(QgsField('x_unit', QVariant.String, 'varchar', 5))
+        fields.append(QgsField('y_unit', QVariant.String, 'varchar', 5))
+        fields.append(QgsField('source', QVariant.String, 'varchar', 5))
+        """
+        return fields
+
+    @staticmethod
+    def fromSpecLibFeature(feature):
+
+        sp = SpectralProfile(fields=feature.fields())
+        sp.setAttributes(feature.attributes())
+        return sp
+
+    def __init__(self, parent=None, fields=None, xUnit='index', yUnit=None):
+
         if fields is None:
-            fields = QgsFields()
-
-            """
-            Parameters
-            name Field name type Field variant type, currently supported: String / Int / Double 
-            typeName Field type (e.g., char, varchar, text, int, serial, double). Field types are usually unique to the source and are stored exactly as returned from the data store. 
-            len Field length 
-            prec Field precision. Usually decimal places but may also be used in conjunction with other fields types (e.g., variable character fields) 
-            comment Comment for the field 
-            subType If the field is a collection, its element's type. When all the elements don't need to have the same type, leave this to QVariant::Invalid. 
-            """
-            fields.append(SpectralProfile.createQgsField('name', ''))
-            fields.append(SpectralProfile.createQgsField('px_x', 0))
-            fields.append(SpectralProfile.createQgsField('px_y', 0))
-            fields.append(SpectralProfile.createQgsField('x_unit', ''))
-            fields.append(SpectralProfile.createQgsField('y_unit', ''))
-            fields.append(SpectralProfile.createQgsField('source', ''))
-
-            """
-            fields.append(QgsField('name', QVariant.String,'varchar', 25))
-            fields.append(QgsField('px_x', QVariant.Int, 'int'))
-            fields.append(QgsField('px_y', QVariant.Int, 'int'))
-            fields.append(QgsField('x_unit', QVariant.String, 'varchar', 5))
-            fields.append(QgsField('y_unit', QVariant.String, 'varchar', 5))
-            fields.append(QgsField('source', QVariant.String, 'varchar', 5))
-            """
+            fields = SpectralProfile.standardFields()
 
+        #QgsFeature.__init__(self, fields)
+        #QObject.__init__(self)
         super(SpectralProfile, self).__init__(fields)
-
+        #QObject.__init__(self)
         fields = self.fields()
         assert isinstance(fields, QgsFields)
 
-        self.mXValues = []
-        self.mYValues = []
+        self.setXUnit(xUnit)
+        self.setYUnit(yUnit)
+
+    def fieldNames(self):
+        return self.fields().names()
+
 
 
-    sigNameChanged = pyqtSignal(str)
     def setName(self, name:str):
         if name != self.name():
             self.setAttribute('name', name)
-            self.sigNameChanged.emit(name)
+            #self.sigNameChanged.emit(name)
 
     def name(self):
-        return self.attribute('name')
+        return self.metadata('name')
 
     def setSource(self, uri: str):
         self.setAttribute('source', uri)
 
     def source(self):
-        return self.attribute('source')
+        return self.metadata('source')
 
     def setCoordinates(self, px=None, pt=None):
         if isinstance(px, QPoint):
@@ -498,30 +523,70 @@ class SpectralProfile(QgsFeature):
     def isValid(self):
         return len(self.mValues) > 0 and self.mValueUnit is not None
 
-    def setValues(self, xValues=None, yValues=None):
-        if xValues is not None and yValues is not None:
-            assert len(xValues) == len(yValues)
 
+    def setXValues(self, values, unit=None):
+        if isinstance(values, np.ndarray):
+            values = values.tolist()
+        assert isinstance(values, list)
 
-        if yValues is not None:
-            self.mYValues = yValues
+        self.setMetadata('__serialized__xvalues', values)
 
-        if xValues is not None:
-            self.mXValues = xValues
+        if isinstance(unit, str):
+            self.setMetadata('x_unit', unit)
 
-        assert len(self.mXValues) == len(self.mYValues)
+    def setYValues(self, values, unit=None):
+        if isinstance(values, np.ndarray):
+            values = values.tolist()
+        assert isinstance(values, list)
+        self.setMetadata('__serialized__yvalues', values)
+        if isinstance(unit, str):
+            self.setMetadata('y_unit', unit)
 
+        if self.xValues() is None:
+            self.setXValues(list(range(len(values))), unit='index')
 
     def updateMetadata(self, metaData):
-        assert isinstance(metaData, dict)
+        if isinstance(metaData, dict):
+            for key, value in metaData.items():
+                self.setMetadata(key, value)
 
-        s = ""
+    def removeField(self, name):
+        fields = self.fields()
+        values = self.attributes()
+        i = self.fieldNameIndex(name)
+        if i >= 0:
+            fields.remove(i)
+            values.pop(i)
+            self.setFields(fields)
+            self.setAttributes(values)
+
+    def setMetadata(self, key: str, value, addMissingFields=False):
+        """
 
+        :param key: Name of metadata field
+        :param value: value to add. Need to be of type None, str, int or float.
+        :param addMissingFields: Set on True to add missing fields (in case value is not None)
+        :return:
+        """
+        i = self.fieldNameIndex(key)
 
+        if key.startswith('__serialized__'):
+            if value is not None:
+                value = pickle.dumps(value)
 
-    def setMetadata(self, key: str, value):
-        i = self.fieldNameIndex(key)
         if i < 0:
+            if value is not None and addMissingFields:
+
+                fields = self.fields()
+                values = self.attributes()
+                if key.startswith('__serialized__'):
+                    fields.append(SpectralProfile.createQgsField(key, ''))
+                else:
+                    fields.append(SpectralProfile.createQgsField(key, value))
+                values.append(value)
+                self.setFields(fields)
+                self.setAttributes(values)
+
             return False
         else:
             return self.setAttribute(key, value)
@@ -529,28 +594,40 @@ class SpectralProfile(QgsFeature):
     def metadata(self, key: str, default=None):
 
         assert isinstance(key, str)
-        v = self.mMetadata.get(key)
+        i = self.fieldNameIndex(key)
+        if i < 0:
+            return None
+
+        v = self.attribute(i)
+        if v == QVariant(None):
+            v = None
+
+        if key.startswith('__serialized__') and v != None:
+            v = pickle.loads(v)
+
         return default if v is None else v
 
     def xValues(self):
-        return self.mXValues[:]
+        return self.metadata('__serialized__xvalues')
 
     def yValues(self):
-        return self.mYValues[:]
+        return self.metadata('__serialized__yvalues')
+
+    def setXUnit(self, unit : str='index'):
+        self.setMetadata('x_unit', unit)
 
     def xUnit(self):
         return self.metadata('x_unit', 'index')
 
-    def yUnit(self):
-        return self.metadata('y_unit')
+    def setYUnit(self, unit:str=None):
+        self.setMetadata('y_unit', unit)
 
-    def valueIndexes(self):
-        return np.arange(len(self.yValues()))
+    def yUnit(self):
+        return self.metadata('y_unit', None)
 
     def clone(self):
 
         sp = SpectralProfile(fields=self.fields())
-        sp.setValues(self.xValues(), self.yValues())
         sp.setAttributes(self.attributes())
         return sp
 
@@ -575,25 +652,28 @@ class SpectralProfile(QgsFeature):
         return self.__class__, (), self.__getstate__()
 
     def __getstate__(self):
-        return self.__dict__.copy()
+        r = QVariant(None)
+        attributes = [None if v == r else v for v  in self.attributes()]
+
+        state = (self.__dict__, attributes)
+        return pickle.dumps(state)
 
     def __setstate__(self, state):
-        self.__dict__.update(state)
+        state = pickle.loads(state)
+        d, a = state
+
+        self.__dict__.update(d)
+        self.setAttributes(a)
 
     def __copy__(self):
-        return copy.deepcopy(self)
+        sp = SpectralProfile(fields=self.fields())
+        sp.setAttributes(self.attributes())
+        return sp
 
-    def isEqual(self, other):
+    def __eq__(self, other):
         if not isinstance(other, SpectralProfile):
             return False
-        if len(self.mValues) != len(other.mValues):
-            return False
-        return all(a == b for a, b in zip(self.mValues, other.mValues)) \
-               and self.mValuePositions == other.mValuePositions \
-               and self.mValueUnit == other.mValueUnit \
-               and self.mValuePositionUnit == other.mValuePositionUnit \
-               and self.mGeoCoordinate == other.mGeoCoordinate \
-               and self.mPxCoordinate == other.mPxCoordinate
+        return np.array_equal(self.attributes(), other.attributes())
 
     """
     def __eq__(self, other):
@@ -672,8 +752,9 @@ class CSVSpectralLibraryIO(SpectralLibraryIO):
 
     @staticmethod
     def asTextLines(speclib, separator='\t'):
+        assert isinstance(speclib, SpectralLibrary)
         lines = []
-        attributes = speclib.metadataAttributes()
+        attributes = speclib.fieldNames()
         grouping = speclib.groupBySpectralProperties()
         for profiles in grouping.values():
             wlU = profiles[0].xUnit()
@@ -731,6 +812,7 @@ class EnviSpectralLibraryIO(SpectralLibraryIO):
                 to_delete.append(tmpVrt)
             ds = EnviSpectralLibraryIO.esl2vrt(pathESL, tmpVrt)
             data = ds.ReadAsArray()
+           # md = ds.GetMetadata_Dict()
             ds = None
 
             #remove the temporary VRT, as it was created internally only
@@ -738,7 +820,6 @@ class EnviSpectralLibraryIO(SpectralLibraryIO):
                 os.remove(file)
 
         except Exception as ex:
-        #if False:
             pathHdr = EnviSpectralLibraryIO.findENVIHeader(pathESL)
 
             pathTmpBin = tempfile.mktemp(prefix='tmpESL', suffix='.esl.bsq')
@@ -765,6 +846,7 @@ class EnviSpectralLibraryIO(SpectralLibraryIO):
             hdr = EnviSpectralLibraryIO.readENVIHeader(pathTmpBin)
             ds = gdal.Open(pathTmpBin)
             data = ds.ReadAsArray()
+            #md = ds.GetMetadata_Dict()
             ds = None
 
             try:
@@ -781,12 +863,12 @@ class EnviSpectralLibraryIO(SpectralLibraryIO):
 
 
         nSpectra, nbands = data.shape
-        valueUnit = ''
-        valuePositionUnit = md.get('wavelength units')
-        valuePositions = md.get('wavelength')
-        if valuePositions is None:
-            valuePositions = list(range(1, nbands+1))
-            valuePositionUnit = 'Band'
+        yUnit = None
+        xUnit = md.get('wavelength units')
+        xValues = md.get('wavelength')
+        if xValues is None:
+            xValues = list(range(1, nbands + 1))
+            xUnit = 'index'
 
         spectraNames = md.get('spectra names', ['Spectrum {}'.format(i+1) for i in range(nSpectra)])
         listAttributes = [(k, v) for k,v in md.items() \
@@ -797,10 +879,8 @@ class EnviSpectralLibraryIO(SpectralLibraryIO):
         profiles = []
         for i, name in enumerate(spectraNames):
             p = SpectralProfile()
-            p.setValues(data[i,:],
-                        valueUnit=valueUnit,
-                        valuePositions=valuePositions,
-                        valuePositionUnit=valuePositionUnit)
+            p.setYValues(data[i,:], unit=yUnit)
+            p.setXValues(xValues, unit=xUnit)
             p.setName(name.strip())
             for listAttribute in listAttributes:
                 p.setMetadata(listAttribute[0], listAttribute[1][i])
@@ -889,8 +969,12 @@ class EnviSpectralLibraryIO(SpectralLibraryIO):
             ds.SetMetadataItem(str('wavelength'), value2hdrString(wl), str('ENVI'))
             ds.SetMetadataItem(str('wavelength units'), str(wlu), str('ENVI'))
 
+            fieldNames = ds.GetMetadata_Dict('ENVI').keys()
+            fieldNames = [n for n in speclib.fields().names() if n not in fieldNames and not n.startswith('__')]
+
 
-            for a in speclib.metadataAttributes():
+
+            for a in fieldNames:
                 v = value2hdrString([p.metadata(a) for p in grp])
                 ds.SetMetadataItem(a, v, str('ENVI'))
 
@@ -1178,54 +1262,45 @@ class SpectralLibrary(QgsVectorLayer):
     sigProfilesRemoved = pyqtSignal([list], [list, list])
     sigNameChanged = pyqtSignal(str)
 
-    def __init__(self, parent=None, profiles=None):
-        super(SpectralLibrary, self).__init__(parent)
+    def __init__(self, name='SpectralLibrary'):
+        crs = SpectralProfile.crs
+        uri = 'Point?crs={}'.format(crs.authid())
+        lyrOptions = QgsVectorLayer.LayerOptions(loadDefaultStyle=False, readExtentFromXml=False)
+        super(SpectralLibrary, self).__init__(uri, name, 'memory', lyrOptions)
 
-        self.mProfiles = []
-        self.mName = ''
-        if profiles is not None:
-            self.mProfiles.extend(profiles[:])
+        defaultFields = SpectralProfile.standardFields()
+
+        assert self.startEditing()
+        assert self.dataProvider().addAttributes(defaultFields)
+        assert self.commitChanges()
 
-    def setName(self, name):
-        if name != self.mName:
-            self.mName = name
-            self.sigNameChanged.emit(name)
 
-    def name(self):
-        return self.mName
 
     def addSpeclib(self, speclib):
         assert isinstance(speclib, SpectralLibrary)
         self.addProfiles([p for p in speclib])
 
+    def addProfiles(self, profiles, index : QModelIndex=QModelIndex()):
 
-    def addProfile(self, profile):
-        self.addProfiles([profile])
-
-    def addProfiles(self, profiles, index=None):
-        to_add = self.extractProfileList(profiles)
-        to_add = [p for p in to_add if p not in self.mProfiles]
-        if len(to_add) > 0:
-            if index is None:
-                index = len(self.mProfiles)
-            self.mProfiles[index:index] = to_add
-
-            indices = [self.mProfiles.index(p) for p in to_add]
-            self.sigProfilesAdded[list].emit(to_add)
-            self.sigProfilesAdded[list, list].emit(to_add, indices)
-
-
-    def extractProfileList(self, profiles):
         if isinstance(profiles, SpectralProfile):
             profiles = [profiles]
-        if isinstance(profiles, list):
-            profiles = [p for p in profiles if isinstance(p, SpectralProfile)]
         elif isinstance(profiles, SpectralLibrary):
-            profiles = profiles.mProfiles[:]
+            profiles = profiles[:]
+
+        assert isinstance(profiles, list)
+        for p in profiles:
+            assert isinstance(p, SpectralProfile)
+        a = self.startEditing()
+        #b, l = self.dataProvider().addFeatures(profiles)
+        b = self.addFeatures(profiles)
+        if b:
+            c = self.commitChanges()
         else:
-            raise Exception('Unknown type {}'.format(type(profiles)))
-        return profiles
+            c = self.rollBack()
+        s = ""
 
+    def profiles(self):
+        return self[:]
 
     def groupBySpectralProperties(self):
         """
@@ -1235,7 +1310,7 @@ class SpectralLibrary(QgsVectorLayer):
         """
 
         d = dict()
-        for p in self.mProfiles:
+        for p in self.profiles():
             #assert isinstance(p, SpectralProfile)
             id = (str(p.xValues()), str(p.xUnit()), str(p.yUnit()))
             if id not in d.keys():
@@ -1243,21 +1318,6 @@ class SpectralLibrary(QgsVectorLayer):
             d[id].append(p)
         return d
 
-    def metadataAttributes(self):
-        attributes = set()
-        for p in self:
-            for k in p.mMetadata.keys():
-                attributes.add(k)
-        return sorted(list(attributes))
-
-    def renameMetadataAttribute(self,oldName, newName):
-        assert oldName in self.metadataAttributes()
-        assert isinstance(oldName, str)
-        assert isinstance(newName, str)
-
-        for p in self:
-            if oldName in p.mMetadata.keys:
-                p.mMetadata[newName] = p.mMetadata.pop(oldName)
 
     def asTextLines(self, separator='\t'):
         return CSVSpectralLibraryIO.asTextLines(self, separator=separator)
@@ -1276,13 +1336,13 @@ class SpectralLibrary(QgsVectorLayer):
         if len(path) > 0:
             ext = os.path.splitext(path)[-1].lower()
             if ext in ['.sli','.esl']:
-                EnviSpectralLibraryIO.write(self, path)
+                return EnviSpectralLibraryIO.write(self, path)
 
             if ext in ['.csv']:
-                CSVSpectralLibraryIO.write(self, path, separator='\t')
+                return CSVSpectralLibraryIO.write(self, path, separator='\t')
 
-            s = ""
 
+        return []
 
     def removeProfiles(self, profiles):
         """
@@ -1306,9 +1366,10 @@ class SpectralLibrary(QgsVectorLayer):
 
 
     def yRange(self):
-        minY = min([min(p.yValues()) for p in self.mProfiles])
-        maxY = max([max(p.yValues()) for p in self.mProfiles])
-        return  minY, maxY
+        profiles = self.profiles()
+        minY = min([min(p.yValues()) for p in profiles])
+        maxY = max([max(p.yValues()) for p in profiles])
+        return minY, maxY
 
     def plot(self):
         import pyqtgraph as pg
@@ -1335,24 +1396,40 @@ class SpectralLibrary(QgsVectorLayer):
     def index(self, obj):
         return self.mProfiles.index(obj)
 
+    def fieldNames(self):
+        return self.fields().names()
+
     def __reduce_ex__(self, protocol):
         return self.__class__, (), self.__getstate__()
 
     def __getstate__(self):
-        return self.__dict__.copy()
+        profiles = self[:]
+        dump = pickle.dumps((self.name(),profiles))
+        return dump
+        #return self.__dict__.copy()
 
     def __setstate__(self, state):
-        self.__dict__.update(state)
+        name, profiles = pickle.loads(state)
+
+        self.setName(name)
+        self.addProfiles(profiles)
+
 
 
     def __len__(self):
-        return len(self.mProfiles)
+        return self.featureCount()
 
     def __iter__(self):
-        return iter(self.mProfiles)
+        r = QgsFeatureRequest()
+        for f in self.getFeatures(r):
+            yield SpectralProfile.fromSpecLibFeature(f)
 
     def __getitem__(self, slice):
-        return self.mProfiles[slice]
+        features = list(self.getFeatures())[slice]
+        if isinstance(features, list):
+            return [SpectralProfile.fromSpecLibFeature(f) for f in features]
+        else:
+            return SpectralProfile.fromSpecLibFeature(features)
 
     def __delitem__(self, slice):
         profiles = self[slice]
@@ -1366,7 +1443,7 @@ class SpectralLibrary(QgsVectorLayer):
             return False
 
         for p1, p2 in zip(self.__iter__(), other.__iter__()):
-            if not p1.isEqual(p2):
+            if not p1 == p2:
                 return False
         return True
 
@@ -2073,10 +2150,13 @@ if __name__ == "__main__":
 
     sp1b = sp1.clone()
 
+    from example.Images import Img_2014_08_11_LC82270652014223LGN00_BOA
+
+
     sp2 = SpectralProfile()
     sp2.setCoordinates(px=QPoint(10, 30), pt=SpatialPoint(QgsCoordinateReferenceSystem('EPSG:32641'), 30.000, 57.500))
-    sp2.setMetadata('attr1', 42)
-    sp2.setMetadata('attr2', 'bar')
+    sp2.setMetadata('attr1', 42, addMissingFields=True)
+    sp2.setMetadata('attr2', 'bar', addMissingFields=True)
 
     sp3 = SpectralProfile()
     sp3.setCoordinates(px=QPoint(10, 10), pt=SpatialPoint(QgsCoordinateReferenceSystem('EPSG:32641'), 30.000, 57.600))
@@ -2084,19 +2164,35 @@ if __name__ == "__main__":
     sp3.setMetadata('attr2', 23)
 
     sl = SpectralLibrary()
-    sl.addProfile(sp1)
-    sl.addProfile(sp2)
-    sl.addProfile(sp3)
+    sl.addProfiles(sp1)
+    sl.addProfiles(sp2)
+    sl.addProfiles(sp3)
 
     w = QFrame()
     w.setLayout(QHBoxLayout())
 
     cache = QgsVectorLayerCache(sl, 1000)
-    view = QgsAttributeTableView()
+
+
     dmodel = QgsAttributeTableModel(cache)
-    fmodel = QgsAttributeTableFilterModel(QgsMapCanvas(), dmodel)
-    view.setModel(fmodel)
+    dmodel.loadLayer()
+    dmodel.updatedFields()
+
+    dummycanvas = QgsMapCanvas()
+    dummycanvas.setDestinationCrs(SpectralProfile.crs)
+    dummycanvas.setCenter(QgsPointXY(0,0))
+    dummycanvas.setExtent(QgsRectangle(-180, -90,180, 90))
+
+    fmodel = QgsAttributeTableFilterModel(dummycanvas, dmodel)
     fmodel.setSourceModel(dmodel)
+
+    if False:
+        view = QgsAttributeTableView()
+        view.setModel(fmodel)
+    else:
+        view = QTableView()
+        view.setModel(dmodel)
+
     w.layout().addWidget(view)
     w.show()
     w.resize(QSize(300,200))
diff --git a/timeseriesviewer/utils.py b/timeseriesviewer/utils.py
index 49211d69..22160458 100644
--- a/timeseriesviewer/utils.py
+++ b/timeseriesviewer/utils.py
@@ -1052,6 +1052,8 @@ def initQgisApplication(pythonPlugins=None, PATH_QGIS=None, qgisDebug=False, qgi
         qgsApp = QgsApplication([], True)
         qgsApp.setPrefixPath(PATH_QGIS, True)
         qgsApp.initQgis()
+        from qgis.gui import QgsGui
+        QgsGui.editorWidgetRegistry().initEditors()
 
         def printQgisLog(tb, error, level):
             if error not in ['Python warning']:
-- 
GitLab