Skip to content
Snippets Groups Projects
spectrallibraries.py 64.7 KiB
Newer Older
  • Learn to ignore specific revisions
  • # -*- coding: utf-8 -*-
    # noinspection PyPep8Naming
    """
    ***************************************************************************
        spectrallibraries.py
    
        Spectral Profiles and Libraries for a GUI context.
        ---------------------
        Date                 : Juli 2017
        Copyright            : (C) 2017 by Benjamin Jakimow
        Email                : benjamin.jakimow@geo.hu-berlin.de
    ***************************************************************************
    *                                                                         *
    *   This program is free software; you can redistribute it and/or modify  *
    *   it under the terms of the GNU General Public License as published by  *
    *   the Free Software Foundation; either version 2 of the License, or     *
    *   (at your option) any later version.                                   *
    *                                                                         *
    ***************************************************************************
    """
    from __future__ import absolute_import, unicode_literals
    #see http://python-future.org/unicode_literals.html for unicode issue discussion
    from future.utils import text_to_native_str
    import os, re, tempfile, pickle, copy, shutil, unicodedata,six
    from collections import OrderedDict
    from qgis.core import *
    from qgis.gui import *
    import pyqtgraph as pg
    from pyqtgraph import functions as fn
    from PyQt4.QtCore import *
    from PyQt4.QtGui import *
    import numpy as np
    from osgeo import gdal, gdal_array
    
    
    from timeseriesviewer.utils import geo2px, px2geo, SpatialExtent, SpatialPoint, loadUI, settings
    from timeseriesviewer.virtualrasters import describeRawFile
    
    FILTERS = 'ENVI Spectral Library (*.esl *.sli);;CSV Table (*.csv)'
    
    39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863
    
    def gdalDataset(pathOrDataset, eAccess=gdal.GA_ReadOnly):
        """
    
        :param pathOrDataset: path or gdal.Dataset
        :return: gdal.Dataset
        """
    
        if isinstance(pathOrDataset, QgsRasterLayer):
            return gdalDataset(pathOrDataset.source())
    
        if not isinstance(pathOrDataset, gdal.Dataset):
            pathOrDataset = gdal.Open(pathOrDataset, eAccess)
    
        assert isinstance(pathOrDataset, gdal.Dataset)
    
        return pathOrDataset
    
    
    #Lookup table for ENVI IDL DataTypes to GDAL Data Types
    LUT_IDL2GDAL = {1:gdal.GDT_Byte,
                    12:gdal.GDT_UInt16,
                    2:gdal.GDT_Int16,
                    13:gdal.GDT_UInt32,
                    3:gdal.GDT_Int32,
                    4:gdal.GDT_Float32,
                    5:gdal.GDT_Float64,
                    #:gdal.GDT_CInt16,
                    #8:gdal.GDT_CInt32,
                    6:gdal.GDT_CFloat32,
                    9:gdal.GDT_CFloat64}
    
    def value2str(value, sep=' '):
        if isinstance(value, list):
            value = sep.join([unicode(v) for v in value])
        elif isinstance(value, np.array):
            value = value2str(value.astype(list), sep=sep)
        elif value is None:
            value = unicode('')
        else:
            value = unicode(value)
        return value
    
    class SpectralLibraryTableView(QTableView):
    
        def __init__(self, parent=None):
            super(SpectralLibraryTableView, self).__init__(parent)
            s = ""
    
        def contextMenuEvent(self, event):
    
            menu = QMenu(self)
    
            m = menu.addMenu('Copy...')
            a = m.addAction("Cell Values")
            a.triggered.connect(lambda :self.onCopy2Clipboard('CELLVALUES', separator=';'))
            a = m.addAction("Spectral Values")
            a.triggered.connect(lambda: self.onCopy2Clipboard('YVALUES', separator=';'))
            a = m.addAction("Spectral Values + Metadata")
            a.triggered.connect(lambda: self.onCopy2Clipboard('ALL', separator=';'))
    
            a = m.addAction("Spectral Values (Excel)")
            a.triggered.connect(lambda: self.onCopy2Clipboard('YVALUES', separator='\t'))
            a = m.addAction("Spectral Values + Metadata (Excel)")
            a.triggered.connect(lambda: self.onCopy2Clipboard('ALL', separator='\t'))
    
            a = menu.addAction('Save to file')
            a.triggered.connect(self.onSaveToFile)
    
            a = menu.addAction('Set color')
            a.triggered.connect(self.onSetColor)
    
            m.addSeparator()
    
            a = menu.addAction('Check')
            a.triggered.connect(lambda : self.setCheckState(Qt.Checked))
            a = menu.addAction('Uncheck')
            a.triggered.connect(lambda: self.setCheckState(Qt.Unchecked))
    
            menu.addSeparator()
            a = menu.addAction('Remove')
            a.triggered.connect(lambda : self.model().removeProfiles(self.selectedSpectra()))
            menu.popup(QCursor.pos())
    
        def onCopy2Clipboard(self, key, separator='\t'):
            assert key in ['CELLVALUES', 'ALL', 'YVALUES']
            txt = None
            if key == 'CELLVALUES':
                lines = []
                line = []
                row = None
                for idx in self.selectionModel().selectedIndexes():
                    if row is None:
                        row = idx.row()
                    elif row != idx.row():
                        lines.append(line)
                        line = []
                    line.append(self.model().data(idx, role=Qt.DisplayRole))
                lines.append(line)
                lines = [value2str(l, sep=separator) for l in lines]
                QApplication.clipboard().setText('\n'.join(lines))
            else:
                sl = SpectralLibrary(profiles=self.selectedSpectra())
                txt = None
                if key == 'ALL':
                    lines = CSVSpectralLibraryIO.asTextLines(sl, separator=separator)
                    txt = '\n'.join(lines)
                elif key == 'YVALUES':
                    lines = []
                    for p in sl:
                        assert isinstance(p, SpectralProfile)
                        lines.append(separator.join([unicode(v) for v in p.yValues()]))
                    txt = '\n'.join(lines)
                if txt:
                    QApplication.clipboard().setText(txt)
    
        def onSaveToFile(self, *args):
            sl = SpectralLibrary(profiles=self.selectedSpectra())
            sl.exportProfiles()
    
    
    
    
        def selectedSpectra(self):
            rows = self.selectedRowsIndexes()
            m = self.model()
            return [m.idx2profile(m.createIndex(r, 0)) for r in rows]
    
        def onSetColor(self):
            c = QColorDialog.getColor()
            if isinstance(c, QColor):
                model = self.model()
                for idx in self.selectedRowsIndexes():
                    model.setData(model.createIndex(idx, 1), c, Qt.BackgroundRole)
    
        def setCheckState(self, checkState):
            model = self.model()
    
            for idx in self.selectedRowsIndexes():
                model.setData(model.createIndex(idx, 0), checkState, Qt.CheckStateRole)
    
            selectionModel = self.selectionModel()
            assert isinstance(selectionModel, QItemSelectionModel)
            selectionModel.clearSelection()
    
        def selectedRowsIndexes(self):
            selectionModel = self.selectionModel()
            assert isinstance(selectionModel, QItemSelectionModel)
            return sorted(list(set([i.row() for i in self.selectionModel().selectedIndexes()])))
    
        def dropEvent(self, event):
            assert isinstance(event, QDropEvent)
            mimeData = event.mimeData()
    
            if self.model().rowCount() == 0:
                index = self.model().createIndex(0,0)
            else:
                index = self.indexAt(event.pos())
    
            if mimeData.hasFormat(MimeDataHelper.MDF_SPECTRALLIBRARY):
                self.model().dropMimeData(mimeData, event.dropAction(), index.row(), index.column(), index.parent())
                event.accept()
    
    
    
    
    
        def dragEnterEvent(self, event):
            assert isinstance(event, QDragEnterEvent)
            if event.mimeData().hasFormat(MimeDataHelper.MDF_SPECTRALLIBRARY):
                event.accept()
    
        def dragMoveEvent(self, event):
            assert isinstance(event, QDragMoveEvent)
            if event.mimeData().hasFormat(MimeDataHelper.MDF_SPECTRALLIBRARY):
                event.accept()
            s = ""
    
    
        def mimeTypes(self):
            pass
    
    """
    class SpectralProfileMapTool(QgsMapToolEmitPoint):
    
        sigProfileRequest = pyqtSignal(SpatialPoint, QgsMapCanvas)
    
        def __init__(self, canvas, showCrosshair=True):
            self.mShowCrosshair = showCrosshair
            self.mCanvas = canvas
            QgsMapToolEmitPoint.__init__(self, self.mCanvas)
            self.marker = QgsVertexMarker(self.mCanvas)
            self.rubberband = QgsRubberBand(self.mCanvas, QGis.Polygon)
    
            color = QColor('red')
    
            self.rubberband.setLineStyle(Qt.SolidLine)
            self.rubberband.setColor(color)
            self.rubberband.setWidth(2)
    
            self.marker.setColor(color)
            self.marker.setPenWidth(3)
            self.marker.setIconSize(5)
            self.marker.setIconType(QgsVertexMarker.ICON_CROSS)  # or ICON_CROSS, ICON_X
    
        def canvasPressEvent(self, e):
            geoPoint = self.toMapCoordinates(e.pos())
            self.marker.setCenter(geoPoint)
            #self.marker.show()
    
        def setStyle(self, color=None, brushStyle=None, fillColor=None, lineStyle=None):
            if color:
                self.rubberband.setColor(color)
            if brushStyle:
                self.rubberband.setBrushStyle(brushStyle)
            if fillColor:
                self.rubberband.setFillColor(fillColor)
            if lineStyle:
                self.rubberband.setLineStyle(lineStyle)
    
        def canvasReleaseEvent(self, e):
    
            pixelPoint = e.pixelPoint()
    
            crs = self.mCanvas.mapSettings().destinationCrs()
            self.marker.hide()
            geoPoint = self.toMapCoordinates(pixelPoint)
            if self.mShowCrosshair:
                #show a temporary crosshair
                ext = SpatialExtent.fromMapCanvas(self.mCanvas)
                cen = geoPoint
                geom = QgsGeometry()
                geom.addPart([QgsPoint(ext.upperLeftPt().x(),cen.y()), QgsPoint(ext.lowerRightPt().x(), cen.y())],
                              QGis.Line)
                geom.addPart([QgsPoint(cen.x(), ext.upperLeftPt().y()), QgsPoint(cen.x(), ext.lowerRightPt().y())],
                              QGis.Line)
                self.rubberband.addGeometry(geom, None)
                self.rubberband.show()
                #remove crosshair after 0.1 sec
                QTimer.singleShot(100, self.hideRubberband)
    
            self.sigProfileRequest.emit(SpatialPoint(crs, geoPoint), self.mCanvas)
    
        def hideRubberband(self):
            self.rubberband.reset()
    
    """
    
    class SpectralProfilePlotDataItem(pg.PlotDataItem):
    
        def __init__(self, spectralProfle):
            assert isinstance(spectralProfle, SpectralProfile)
            super(SpectralProfilePlotDataItem, self).__init__(spectralProfle.xValues(), spectralProfle.yValues())
            self.mProfile = spectralProfle
    
        def setClickable(self, b, width=None):
            assert isinstance(b, bool)
            self.curve.setClickable(b, width=width)
    
        def setColor(self, color):
            if not isinstance(color, QColor):
    
                color = QColor(color)
            self.setPen(color)
    
        def pen(self):
    
            return fn.mkPen(self.opts['pen'])
    
        def color(self):
            return self.pen().color()
    
        def setLineWidth(self, width):
            pen = pg.mkPen(self.opts['pen'])
            assert isinstance(pen, QPen)
            pen.setWidth(width)
            self.setPen(pen)
    
    
    class SpectralProfile(QObject):
    
        @staticmethod
        def fromMapCanvas(mapCanvas, position):
            """
            Returns a list of Spectral Profiles the raster layers in QgsMapCanvas mapCanvas.
            :param mapCanvas:
            :param position:
            """
            assert isinstance(mapCanvas, QgsMapCanvas)
    
            layers = [l for l in mapCanvas.layers() if isinstance(l, QgsRasterLayer)]
            sources = [l.source() for l in layers]
            return SpectralProfile.fromRasterSources(sources, position)
    
        @staticmethod
        def fromRasterSources(sources, position):
            """
            Returns a list of Spectral Profiles
            :param sources: list-of-raster-sources, e.g. file paths, gdal.Datasets, QgsRasterLayers
            :param position:
            :return:
            """
            profiles = [SpectralProfile.fromRasterSource(s, position) for s in sources]
            return [p for p in profiles if isinstance(p, SpectralProfile)]
    
    
        @staticmethod
        def fromRasterSource(source, position):
            """
            Returns the Spectral Profiles from source at position `position`
            :param source: path or gdal.Dataset
            :param position:
            :return: SpectralProfile
            """
    
            ds = gdalDataset(source)
            crs = QgsCoordinateReferenceSystem(ds.GetProjection())
            gt = ds.GetGeoTransform()
    
            if isinstance(position, QPoint):
                px = position
            elif isinstance(position, SpatialPoint):
                px = geo2px(position.toCrs(crs), gt)
            elif isinstance(position, QgsPoint):
                px = geo2px(position, ds.GetGeoTransform())
            else:
                raise Exception('Unsupported type of argument "position" {}'.format('{}'.format(position)))
            #check out-of-raster
            if px.x() < 0 or px.y() < 0: return None
            if px.x() > ds.RasterXSize - 1 or px.y() > ds.RasterYSize - 1: return None
    
    
            values = ds.ReadAsArray(px.x(), px.y(), 1, 1)
    
            values = values.flatten()
            for b in range(ds.RasterCount):
                band = ds.GetRasterBand(b+1)
                nodata = band.GetNoDataValue()
                if nodata and values[b] == nodata:
                    return None
    
            wl = ds.GetMetadataItem(str('wavelength'),str('ENVI'))
            wlu = ds.GetMetadataItem(str('wavelength_units'),str('ENVI'))
            if wl is not None and len(wl) > 0:
                wl = re.sub(r'[ {}]','', wl).split(',')
                wl = [float(w) for w in wl]
    
            profile = SpectralProfile()
            profile.setValues(values, valuePositions=wl, valuePositionUnit=wlu)
            profile.setCoordinates(px=px, spatialPoint=SpatialPoint(crs,px2geo(px, gt)))
            profile.setSource('{}'.format(ds.GetFileList()[0]))
            return profile
    
        def __init__(self, parent=None):
            super(SpectralProfile, self).__init__(parent)
            self.mName = ''
            self.mValues = []
            self.mValueUnit = None
            self.mValuePositions = []
            self.mValuePositionUnit = None
            self.mMetadata = dict()
            self.mSource = None
            self.mPxCoordinate = None
            self.mGeoCoordinate = None
    
        sigNameChanged = pyqtSignal(unicode)
        def setName(self, name):
            assert isinstance(name, unicode)
    
            if name != self.mName:
                self.mName = name
                self.sigNameChanged.emit(name)
    
        def name(self):
            return self.mName
    
        def setSource(self, uri):
            assert isinstance(uri, unicode)
            self.mSource = uri
    
        def source(self):
            return self.mSource
    
        def setCoordinates(self, px=None, spatialPoint=None):
            if isinstance(px, QPoint):
                self.mPxCoordinate = px
            if isinstance(spatialPoint, SpatialPoint):
                self.mGeoCoordinate = spatialPoint
    
        def pxCoordinate(self):
            return self.mPxCoordinate
    
        def geoCoordinate(self):
            return self.mGeoCoordinate
    
        def isValid(self):
            return len(self.mValues) > 0 and self.mValueUnit is not None
    
        def setValues(self, values, valueUnit=None,
                      valuePositions=None, valuePositionUnit=None):
            n = len(values)
            self.mValues = values[:]
    
            if valuePositions is None:
                valuePositions = list(range(n))
                valuePositionUnit = 'Index'
            self.setValuePositions(valuePositions, unit=valuePositionUnit)
    
        def setValuePositions(self, positions, unit=None):
            assert len(positions) == len(self.mValues)
            self.mValuePositions = positions[:]
            self.mValuePositionUnit = unit
    
        def updateMetadata(self, metaData):
            assert isinstance(metaData, dict)
            self.mMetadata.update(metaData)
    
        def setMetadata(self, key, value):
    
            assert isinstance(key, unicode)
    
            self.mMetadata[key] = value
    
        def metadata(self, key, default=None):
            assert isinstance(key, unicode)
            v = self.mMetadata.get(key)
            return default if v is None else v
    
        def yValues(self):
            return self.mValues[:]
    
        def yUnit(self):
            return self.mValueUnit
    
        def xValues(self):
            return self.mValuePositions[:]
    
        def xUnit(self):
            return self.mValuePositionUnit
    
        def valueIndexes(self):
            return np.arange(len(self.yValues()))
    
        def clone(self):
            return copy.copy(self)
    
        def plot(self):
            """
            Plots this profile to an new PyQtGraph window
            :return:
            """
            import pyqtgraph as pg
    
            pi = SpectralProfilePlotDataItem(self)
            pi.setClickable(True)
            pw = pg.plot( title=self.name())
            pw.getPlotItem().addItem(pi)
    
            pi.setColor('green')
            pg.QAPP.exec_()
    
    
        def __reduce_ex__(self, protocol):
    
            return self.__class__, (), self.__getstate__()
    
        def __getstate__(self):
            return self.__dict__.copy()
    
        def __setstate__(self, state):
            self.__dict__.update(state)
    
        def __copy__(self):
            return copy.deepcopy(self)
    
        def isEqual(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
    
        """
        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
    
        def __ne__(self, other):
            return not self.__eq__(other)
        """
        def __len__(self):
            return len(self.mValues)
    
    class SpectralLibraryWriter(object):
    
        @staticmethod
        def writeSpeclib(speclib):
            assert isinstance(speclib, SpectralLibrary)
    
    
    
    class SpectralLibraryIO(object):
        """
        Abstract Class to define I/O operations for spectral libraries
        Overwrite the canRead and read From routines.
        """
        @staticmethod
        def canRead(path):
            """
            Returns true if it can reath the source definded by path
            :param path: source uri
            :return: True, if source is readibly
            """
            return False
    
        @staticmethod
        def readFrom(path):
            """
            Returns the SpectralLibrary read from "path"
            :param path: source of SpectralLibrary
            :return: SpectralLibrary
            """
            return None
    
        @staticmethod
        def write(speclib, path):
            """Writes the SpectralLibrary speclib to path, returns a list of written files"""
            assert isinstance(speclib, SpectralLibrary)
            return None
    
    
    class CSVSpectralLibraryIO(SpectralLibraryIO):
    
        @staticmethod
        def write(speclib, path, separator='\t'):
    
            assert isinstance(speclib, SpectralLibrary)
            lines = ['Spectral Library {}'.format(speclib.name())]
    
            lines.extend(
                CSVSpectralLibraryIO.asTextLines(speclib, separator=separator)
            )
    
            file = open(path, 'w')
            for line in lines:
                file.write(line+'\n')
            file.flush()
            file.close()
    
        @staticmethod
        def asTextLines(speclib, separator='\t'):
            lines = []
            attributes = speclib.metadataAttributes()
            grouping = speclib.groupBySpectralProperties()
            for profiles in grouping.values():
                wlU = profiles[0].xUnit()
                wavelength = profiles[0].xValues()
    
                columns = ['n', 'name', 'geo', 'px', 'src']+attributes
                if wlU in [None, 'Index']:
                    columns.extend(['b{}'.format(i + 1) for i in range(len(wavelength))])
                else:
                    for i, wl in enumerate(wavelength):
                        columns.append('b{}_{}'.format(i + 1, wl))
                lines.append(value2str(columns, sep=separator))
    
                for i, p in enumerate(profiles):
                    line = [i + 1, p.name(), p.geoCoordinate(), p.pxCoordinate(), p.source()]
                    line.extend([p.metadata(a) for a in attributes])
                    line.extend(p.yValues())
                    lines.append(value2str(line, sep=separator))
                lines.append('')
            return lines
    
    
    class EnviSpectralLibraryIO(SpectralLibraryIO):
    
        @staticmethod
        def canRead(pathESL):
            """
            Checks if a file can be read as SpectraLibrary
            :param pathESL: path to ENVI Spectral Library (ESL)
            :return: True, if pathESL can be read as Spectral Library.
            """
            assert isinstance(pathESL, unicode)
            if not os.path.isfile(pathESL):
                return False
            hdr = EnviSpectralLibraryIO.readENVIHeader(pathESL, typeConversion=False)
            if hdr is None or hdr[u'file type'] != u'ENVI Spectral Library':
                return False
            return True
    
        @staticmethod
        def readFrom(pathESL, tmpVrt=None):
            """
            Reads an ENVI Spectral Library (ESL).
            :param pathESL: path ENVI Spectral Library
            :param tmpVrt: (optional) path of GDAL VRt that is used to read the ESL
            :return: SpectralLibrary
            """
            assert isinstance(pathESL, unicode)
            md = EnviSpectralLibraryIO.readENVIHeader(pathESL, typeConversion=True)
            data = None
            try:
                to_delete = []
                if tmpVrt is None:
                    tmpVrt = tempfile.mktemp(prefix='tmpESLVrt', suffix='.esl.vrt')
                    to_delete.append(tmpVrt)
                ds = EnviSpectralLibraryIO.esl2vrt(pathESL, tmpVrt)
                data = ds.ReadAsArray()
                ds = None
    
                #remove the temporary VRT, as it was created internally only
                for file in to_delete:
                    os.remove(file)
    
            except Exception as ex:
            #if False:
                pathHdr = EnviSpectralLibraryIO.findENVIHeader(pathESL)
    
                pathTmpBin = tempfile.mktemp(prefix='tmpESL', suffix='.esl.bsq')
                pathTmpHdr = re.sub('bsq$','hdr',pathTmpBin)
                shutil.copyfile(pathESL, pathTmpBin)
                shutil.copyfile(pathHdr, pathTmpHdr)
                assert os.path.isfile(pathTmpBin)
                assert os.path.isfile(pathTmpHdr)
    
                import codecs
                hdr = codecs.open(pathTmpHdr, encoding='utf-8').readlines()
                for iLine in range(len(hdr)):
                    if re.search('file type =', hdr[iLine]):
                        hdr[iLine] = 'file type = ENVI Standard\n'
                        break
                file = codecs.open(pathTmpHdr, 'w', encoding='utf-8')
                file.writelines(hdr)
                file.flush()
                file.close()
                assert os.path.isfile(pathTmpHdr)
                hdr = EnviSpectralLibraryIO.readENVIHeader(pathTmpBin)
                ds = gdal.Open(pathTmpBin)
                data = ds.ReadAsArray()
                ds = None
    
                try:
                    os.remove(pathTmpBin)
                except:
                    pass
                try:
                    os.remove(pathTmpHdr)
                except:
                    pass
            assert data is not None
    
    
    
    
            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'
    
            spectraNames = md.get('spectra names', ['Spectrum {}'.format(i+1) for i in range(nSpectra)])
            listAttributes = [(k, v) for k,v in md.items() \
                              if k not in ['spectra names','wavelength'] and \
                              isinstance(v, list) and len(v) == nSpectra]
    
    
            profiles = []
            for i, name in enumerate(spectraNames):
                p = SpectralProfile()
                p.setValues(data[i,:],
                            valueUnit=valueUnit,
                            valuePositions=valuePositions,
                            valuePositionUnit=valuePositionUnit)
                p.setName(name.strip())
                for listAttribute in listAttributes:
                    p.setMetadata(listAttribute[0], listAttribute[1][i])
                p.setSource(pathESL)
                profiles.append(p)
    
    
            SLIB = SpectralLibrary()
            SLIB.addProfiles(profiles)
            return SLIB
    
        @staticmethod
        def write(speclib, path, ext='sli'):
            assert isinstance(path, unicode)
            dn = os.path.dirname(path)
            bn = os.path.basename(path)
    
            writtenFiles = []
    
            if bn.lower().endswith(ext.lower()):
                bn = os.path.splitext(bn)[0]
    
            if not os.path.isdir(dn):
                os.makedirs(dn)
    
            def value2hdrString(values):
                s = None
                maxwidth = 75
    
                if isinstance(values, list):
                    lines = ['{']
                    values = ['{}'.format(v).replace(',','-') for v in values]
                    line = ' '
                    l = len(values)
                    for i, v in enumerate(values):
                        line += v
                        if i < l-1: line += ', '
                        if len(line) > maxwidth:
                            lines.append(line)
                            line = ' '
                    line += '}'
                    lines.append(line)
                    s = '\n'.join(lines)
    
                else:
                    s = '{}'.format(values)
    
                #unicodedata.normalize('NFKD', title).encode('ascii','ignore')
                #return s
                return u2s(s)
    
    
            for iGrp, grp in enumerate(speclib.groupBySpectralProperties().values()):
    
                wl = grp[0].xValues()
                wlu = grp[0].xUnit()
    
    
                # stack profiles
                pData = [np.asarray(p.yValues()) for p in grp]
                pData = np.vstack(pData)
                pNames = [p.name() for p in grp]
    
                if iGrp == 0:
                    pathDst = os.path.join(dn, '{}.{}'.format(bn, ext))
                else:
                    pathDst = os.path.join(dn, '{}.{}.{}'.format(bn, iGrp, ext))
    
                drv = gdal.GetDriverByName(str('ENVI'))
                assert isinstance(drv, gdal.Driver)
    
    
                eType = gdal_array.NumericTypeCodeToGDALTypeCode(pData.dtype)
                """Create(utf8_path, int xsize, int ysize, int bands=1, GDALDataType eType, char ** options=None) -> Dataset"""
                ds = drv.Create(pathDst, pData.shape[1], pData.shape[0], 1, eType)
                band = ds.GetRasterBand(1)
                assert isinstance(band, gdal.Band)
                band.WriteArray(pData)
    
                #ds = gdal_array.SaveArray(pData, pathDst, format='ENVI')
    
                assert isinstance(ds, gdal.Dataset)
                ds.SetDescription(str(speclib.name()))
                ds.SetMetadataItem(str('band names'), str('Spectral Library'), str('ENVI'))
                ds.SetMetadataItem(str('spectra names'),value2hdrString(pNames), str('ENVI'))
                ds.SetMetadataItem(str('wavelength'), value2hdrString(wl), str('ENVI'))
                ds.SetMetadataItem(str('wavelength units'), str(wlu), str('ENVI'))
    
    
                for a in speclib.metadataAttributes():
                    v = value2hdrString([p.metadata(a) for p in grp])
                    ds.SetMetadataItem(u2s(a), v, str('ENVI'))
    
                pathHdr = ds.GetFileList()[1]
                ds = None
    
                # last step: change ENVI Hdr
                hdr = open(pathHdr).readlines()
                for iLine in range(len(hdr)):
                    if re.search('file type =', hdr[iLine]):
                        hdr[iLine] = 'file type = ENVI Spectral Library\n'
                        break
                file = open(pathHdr, 'w')
                file.writelines(hdr)
                file.flush()
                file.close()
                writtenFiles.append(pathDst)
    
            return writtenFiles
    
    
        @staticmethod
        def esl2vrt(pathESL, pathVrt=None):
            """
            Creates a GDAL Virtual Raster (VRT) that allows to read an ENVI Spectral Library file
            :param pathESL: path ENVI Spectral Library file (binary part)
            :param pathVrt: (optional) path of created GDAL VRT.
            :return: GDAL VRT
            """
    
            hdr = EnviSpectralLibraryIO.readENVIHeader(pathESL, typeConversion=False)
            assert hdr is not None and hdr['file type'] == 'ENVI Spectral Library'
    
            if hdr.get('file compression') == '1':
                raise Exception('Can not read compressed spectral libraries')
    
            eType = LUT_IDL2GDAL[int(hdr['data type'])]
            xSize = int(hdr['samples'])
            ySize = int(hdr['lines'])
            bands = int(hdr['bands'])
            byteOrder = 'MSB' if hdr['byte order'] == 0 else 'LSB'
    
            if pathVrt is None:
                pathVrt = tempfile.mktemp(prefix='tmpESLVrt', suffix='.esl.vrt')
    
    
            ds = describeRawFile(pathESL, pathVrt, xSize, ySize, bands=bands, eType=eType, byteOrder=byteOrder)
            for key, value in hdr.items():
                if isinstance(value, list):
                    value = u','.join(v for v in value)
                ds.SetMetadataItem(u2s(key), text_to_native_str(value), str('ENVI'))
            ds.FlushCache()
            return ds
    
    
        @staticmethod
        def readENVIHeader(pathESL, typeConversion=False):
            """
            Reads an ENVI Header File (*.hdr) and returns its values in a dictionary
            :param pathESL: path to ENVI Header
            :param typeConversion: Set on True to convert header keys with numeric
            values into numeric data types (int / float)
            :return: dict
            """
            assert isinstance(pathESL, unicode)
            if not os.path.isfile(pathESL):
                return None
    
            pathHdr = EnviSpectralLibraryIO.findENVIHeader(pathESL)
            if pathHdr is None:
                return None
    
            import codecs
            #hdr = open(pathHdr).readlines()
            hdr = codecs.open(pathHdr, encoding='utf-8').readlines()
            i = 0
            while i < len(hdr):
                if '{' in hdr[i]:
                    while not '}' in hdr[i]:
                        hdr[i] = hdr[i] + hdr.pop(i + 1)
                i += 1
    
            hdr = [''.join(re.split('\n[ ]*', line)).strip() for line in hdr]
            # keep lines with <tag>=<value> structure only
            hdr = [line for line in hdr if re.search('^[^=]+=', line)]
    
            # restructure into dictionary of type
            # md[key] = single value or
            # md[key] = [list-of-values]
            md = dict()
            for line in hdr:
                tmp = line.split('=')
                key, value = tmp[0].strip(), '='.join(tmp[1:]).strip()
                if value.startswith('{') and value.endswith('}'):
                    value = [v.strip() for v in value.strip('{}').split(',')]
                md[key] = value
    
            # check required metadata tegs
            for k in ['byte order', 'data type', 'header offset', 'lines', 'samples', 'bands']:
                if not k in md.keys():
                    return None
    
            #todo: transform known strings into int/floats?
            def toType(t, arg):
                if isinstance(arg, list):
                    return [toType(t, a) for a  in arg]
                else:
                    return t(arg)
    
            if typeConversion:
                to_int = ['bands','lines','samples','data type','header offset','byte order']
                to_float = ['fwhm','wavelength', 'reflectance scale factor']
                for k in to_int:
                    if k in md.keys():
                        md[k] = toType(int, md[k])
                for k in to_float:
                    if k in md.keys():
                        md[k] = toType(float, md[k])
    
    
            return md
    
        @staticmethod
        def findENVIHeader(pathESL):
            paths = [os.path.splitext(pathESL)[0] + '.hdr', pathESL + '.hdr']
            pathHdr = None
            for p in paths:
                if os.path.exists(p):
                    pathHdr = p
            return pathHdr
    
    
    class SpectralLibraryPanel(QgsDockWidget):
        sigLoadFromMapRequest = None
        def __init__(self, parent=None):
            super(SpectralLibraryPanel, self).__init__(parent)
            self.setWindowTitle('Spectral Library')
            self.SLW = SpectralLibraryWidget(self)
            self.setWidget(self.SLW)
    
    class SpectralLibraryVectorLayer(QgsVectorLayer):
    
        def __init__(self, speclib, crs=None):
            assert isinstance(speclib, SpectralLibrary)
            if crs is None:
                crs = QgsCoordinateReferenceSystem('EPSG:4326')
    
            uri = 'Point?crs={}'.format(crs.authid())
            super(SpectralLibraryVectorLayer, self).__init__(uri, speclib.name(), 'memory', False)
            self.mCrs = crs
            self.mSpeclib = speclib
            self.mSpeclib.sigNameChanged.connect(self.setName)
            self.nameChanged.connect(self.mSpeclib.setName)
    
            #todo QGIS3: use QgsFieldContraint instead self.mOIDs
            self.mOIDs = dict()
    
    
            #initialize fields
            assert self.startEditing()
            # standard field names, types, etc.
            fieldDefs = [('oid', QVariant.Int, 'integer'),
                         ('name', QVariant.String, 'string'),
                         ('geo_x', QVariant.Double, 'decimal'),
                         ('geo_y', QVariant.Double, 'decimal'),
                         ('px_x', QVariant.Int, 'integer'),
                         ('px_y', QVariant.Int, 'integer'),
                         ('source', QVariant.String, 'string'),
                         ]
            # initialize fields
            for fieldDef in fieldDefs:
                field = QgsField(fieldDef[0], fieldDef[1], fieldDef[2])
                self.addAttribute(field)
            self.commitChanges()
    
            self.mSpeclib.sigProfilesAdded.connect(self.onProfilesAdded)
            self.mSpeclib.sigProfilesRemoved.connect(self.onProfilesRemoved)
            self.onProfilesAdded(self.mSpeclib[:])
    
        def onProfilesAdded(self, profiles):
            for p in [p for p in profiles if p.geoCoordinate() is not None]:
                assert isinstance(p, SpectralProfile)