Skip to content
Snippets Groups Projects
settings.py 19.2 KiB
Newer Older
  • Learn to ignore specific revisions
  • Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
    import os, enum, pathlib, re, json, pickle
    
    from collections import namedtuple
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    from qgis.core import *
    from qgis.gui import *
    from qgis.PyQt.QtCore import *
    from qgis.PyQt.QtWidgets import *
    from qgis.PyQt.QtGui import *
    
    
    from eotimeseriesviewer import *
    from eotimeseriesviewer.utils import loadUI
    
    from eotimeseriesviewer.timeseries import SensorMatching, SensorInstrument
    
    from osgeo import gdal, gdalconst, gdal_array
    
    
    
    class Keys(enum.Enum):
        """
        Enumeration of settings keys.
        """
        DateTimePrecision = 'date_time_precision'
        MapSize = 'map_size'
        MapUpdateInterval = 'map_update_interval'
        MapBackgroundColor = 'map_background_color'
    
        SensorSpecs = 'sensor_specs'
    
        ScreenShotDirectory = 'screen_shot_directory'
        RasterSourceDirectory = 'raster_source_directory'
        VectorSourceDirectory = 'vector_source_directory'
    
        MapImageExportDirectory = 'map_image_export_directory'
    
    def defaultValues() -> dict:
        """
        Returns the official hard-coded dictionary of default values.
        :return: dict
        """
        d = dict()
    
        from eotimeseriesviewer.timeseries import DateTimePrecision
    
    
        # general settings
        home = pathlib.Path.home()
        d[Keys.ScreenShotDirectory] = str(home)
        d[Keys.RasterSourceDirectory] = str(home)
        d[Keys.VectorSourceDirectory] = str(home)
        d[Keys.DateTimePrecision] = DateTimePrecision.Day
    
        # d[Keys.SensorSpecs] = dict() # no default sensors
        d[Keys.SensorMatching] = SensorMatching.PX_DIMS
    
    
        # map visualization
        d[Keys.MapUpdateInterval] = 500  # milliseconds
    
        d[Keys.MapSize] = QSize(150, 150)
    
        d[Keys.MapBackgroundColor] = QColor('black')
    
        textFormat.setColor(QColor('black'))
    
        textFormat.setSizeUnit(QgsUnitTypes.RenderPoints)
        textFormat.setFont(QFont('Helvetica'))
        textFormat.setSize(11)
    
        buffer = QgsTextBufferSettings()
    
        buffer.setColor(QColor('white'))
        buffer.setSize(5)
    
        buffer.setSizeUnit(QgsUnitTypes.RenderPixels)
        buffer.setEnabled(True)
        textFormat.setBuffer(buffer)
    
        d[Keys.MapTextFormat] = textFormat
    
    
    
        # tbd. other settings
    
        return d
    
    
    def settings()->QSettings:
        """
        Returns the EOTSV settings.
        :return: QSettings
        """
        settings = QSettings(QSettings.UserScope, 'HU-Berlin', 'EO-TimeSeriesViewer')
    
        return settings
    
    
    
    def value(key:Keys, default=None):
        """
        Provides direct access to a settings value
    
        :param default: default value, defaults to None
        :return: value | None
        """
    
        assert isinstance(key.value, str)
    
            value = settings().value(key.value, defaultValue=default)
    
            if value == QVariant():
                value = None
    
    
            if key == Keys.MapTextFormat:
                if isinstance(value, QByteArray):
                    doc = QDomDocument()
                    doc.setContent(value)
                    value = QgsTextFormat()
                    value.readXml(doc.documentElement(), QgsReadWriteContext())
    
    
    
            if key == Keys.SensorSpecs:
                # check sensor specs
                if value is None:
                    value = dict()
    
    
                assert isinstance(value, dict)
    
                from .timeseries import sensorIDtoProperties
                from json.decoder import JSONDecodeError
    
                for sensorID in list(value.keys()):
                    assert isinstance(sensorID, str)
                    try:
                        sensorSpecs = value[sensorID]
                        if not isinstance(sensorSpecs, dict):
                            value[sensorSpecs] = {'name': None}
                    except (AssertionError, JSONDecodeError) as ex:
                        # delete old-style settings
                        del value[sensorID]
    
    
        except TypeError as error:
            value = None
    
            print(error, file=sys.stderr)
    
    def saveSensorName(sensor:SensorInstrument):
        """
        Saves the sensor name
        :param sensor: SensorInstrument
        :return:
        """
        assert isinstance(sensor, SensorInstrument)
    
        sensorSpecs = value(Keys.SensorSpecs, default=dict())
        assert isinstance(sensorSpecs, dict)
    
        sSpecs = sensorSpecs.get(sensor.id(), dict())
        sSpecs['name'] = sensor.name()
    
        sensorSpecs[sensor.id()] = sSpecs
    
        setValue(Keys.SensorSpecs, sensorSpecs)
    
    
    def sensorName(id:typing.Union[str, SensorInstrument])->str:
    
        """
        Retuns the sensor name stored for a certain sensor id
        :param id: str
        :return: str
        """
        if isinstance(id, SensorInstrument):
            id = id.id()
    
        sensorSpecs = value(Keys.SensorSpecs, default=dict())
        assert isinstance(sensorSpecs, dict)
        sSpecs = sensorSpecs.get(id, dict())
        return sSpecs.get('name', None)
    
    
    
    
    
        """
        Shortcut to save a value into the EOTSV settings
        :param key: str | Key
        :param value: any value
        """
    
        assert isinstance(key.value, str)
    
        if isinstance(value, QgsTextFormat):
            # make QgsTextFormat pickable
            doc = QDomDocument()
            doc.appendChild(value.writeXml(doc, QgsReadWriteContext()))
            value = doc.toByteArray()
            
    
        if key == Keys.SensorSpecs:
            s = ""
        #if isinstance(value, dict) and key == Keys.SensorSpecs:
        #   settings().setValue(key.value, value)
    
    
    
    def setValues(values: dict):
        """
        Writes the EOTSV settings
        :param values: dict
        :return:
        """
        assert isinstance(values, dict)
        for key, val in values.items():
    
        settings().sync()
    
    def values()->dict:
        """
        Returns all settings in a dictionary
        :return: dict
        :rtype: dict
        """
        d = dict()
        for key in Keys:
            assert isinstance(key, Keys)
            d[key] = value(key)
        return d
    
    class SensorSettingsTableModel(QAbstractTableModel):
        """
        A table to visualize sensor-specific settings
        """
        def __init__(self):
            super(SensorSettingsTableModel, self).__init__()
    
            self.mSensors = []
    
            self.mCNKey = 'Specification'
    
            self.mCNName = 'Name'
            self.loadSettings()
    
        def clear(self):
            """Removes all entries"""
            self.removeRows(0, self.rowCount())
            assert len(self.mSensors) == 0
    
        def reload(self):
            """
            Reloads the entire table
            :return:
            """
            self.clear()
            self.loadSettings()
    
        def removeRows(self, row: int, count: int, parent: QModelIndex = QModelIndex()) -> bool:
    
            if count > 0:
                self.beginRemoveRows(parent, row, row+count-1)
    
                for i in reversed(range(row, row+count)):
                    del self.mSensors[i]
    
                self.endRemoveRows()
    
        def loadSettings(self):
            sensorSpecs = value(Keys.SensorSpecs, default={})
    
            sensors = []
            for id, specs in sensorSpecs.items():
                sensor = SensorInstrument(id)
                sensor.setName(specs['name'])
                sensors.append(sensor)
            self.addSensors(sensors)
    
        def removeSensors(self, sensors:typing.List[SensorInstrument]):
            assert isinstance(sensors, list)
    
    
            for sensor in sensors:
                assert isinstance(sensor, SensorInstrument)
                idx = self.sensor2idx(sensor)
                self.beginRemoveRows(QModelIndex(), idx.row(), idx.row())
                self.mSensors.remove(sensor)
                self.endRemoveRows()
    
        def sensor2idx(self, sensor:SensorInstrument)->QModelIndex:
    
            if not sensor in self.mSensors:
                return QModelIndex()
            row = self.mSensors.index(sensor)
            return self.createIndex(row, 0, sensor)
    
    
        def addSensors(self, sensors:typing.List[SensorInstrument]):
            assert isinstance(sensors, list)
            n = len(sensors)
    
            if n > 0:
    
                self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount() + n - 1)
    
                self.mSensors.extend(sensors)
                self.endInsertRows()
    
    
        def setSpecs(self, specs:dict):
            sensors = []
            for sid, sensorSpecs in specs.items():
                assert isinstance(sid, str)
                assert isinstance(sensorSpecs, dict)
                sensor = SensorInstrument(sid)
                # apply specs to sensor instance
                sensor.setName(sensorSpecs.get('name', sensor.name()))
                sensors.append(sensor)
            self.clear()
            self.addSensors(sensors)
    
        def specs(self)->dict:
            """
            Returns the specifications for each stored sensor
            :return:
            :rtype:
            """
    
            specs = dict()
            for sensor in self.mSensors:
                assert isinstance(sensor, SensorInstrument)
    
                s = {'name': sensor.name()}
                specs[sensor.id()] = s
            return specs
    
    
        def rowCount(self, parent: QModelIndex = QModelIndex())->int:
            return len(self.mSensors)
    
        def columnNames(self)->typing.List[str]:
            return [self.mCNKey, self.mCNName]
    
        def columnCount(self, parent: QModelIndex):
            return len(self.columnNames())
    
        def flags(self, index: QModelIndex):
            flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable
            cn = self.columnNames()[index.column()]
            if cn == self.mCNName:
                flags = flags | Qt.ItemIsEditable
    
            return flags
    
        def headerData(self, section, orientation, role):
            assert isinstance(section, int)
            if orientation == Qt.Horizontal and role == Qt.DisplayRole:
                return self.columnNames()[section]
            elif orientation == Qt.Vertical and role == Qt.DisplayRole:
                return section + 1
            else:
                return None
    
        def sensor(self, index)->SensorInstrument:
            if isinstance(index, int):
                return self.mSensors[index]
            else:
                return self.mSensors[index.row()]
    
    
        def sensorIDDisplayString(self, sensor:SensorInstrument)->str:
            """
            Returns a short representation of the sensor id, e.g. "6bands(Int16)@30m"
            :param sensor:
            :type sensor:
            :return:
            :rtype:
            """
            assert isinstance(sensor, SensorInstrument)
    
            s = '{}band({})@{}m'.format(sensor.nb, gdal.GetDataTypeName(sensor.dataType), sensor.px_size_x)
            if sensor.wl is not None and sensor.wlu is not None:
                if sensor.nb == 1:
                    s += ',{}{}'.format(sensor.wl[0], sensor.wlu)
                else:
                    s += ',{}-{}{}'.format(sensor.wl[0], sensor.wl[-1], sensor.wlu)
            return s
    
    
        def data(self, index: QModelIndex, role: int):
    
            if not index.isValid():
                return None
    
            sensor = self.sensor(index)
            cn = self.columnNames()[index.column()]
    
            if role in [Qt.DisplayRole, Qt.EditRole]:
                if cn == self.mCNName:
                    return sensor.name()
                if cn == self.mCNKey:
    
                    return self.sensorIDDisplayString(sensor)
    
            if role in [Qt.ToolTipRole]:
                if cn == self.mCNName:
                    return sensor.name()
                if cn == self.mCNKey:
                    return sensor.id().replace(', "', '\n "')
    
    
            if role == Qt.BackgroundColorRole and not (self.flags(index) & Qt.ItemIsEditable):
                return QColor('gray')
    
    
            if role == Qt.UserRole:
                return sensor
    
    
            return None
    
        def setData(self, index: QModelIndex, value: typing.Any, role: int = ...) -> bool:
    
            if not index.isValid():
                return False
    
            changed = False
            sensor = self.sensor(index)
            cn = self.columnNames()[index.column()]
    
            if cn == self.mCNName and isinstance(value, str):
                sensor.setName(value)
                changed = True
    
            if changed:
                self.dataChanged.emit(index, index, [role])
            return changed
    
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    class SettingsDialog(QDialog, loadUI('settingsdialog.ui')):
        """
        A widget to change settings
        """
    
        def __init__(self, title='<#>', parent=None):
            super(SettingsDialog, self).__init__(parent)
            self.setupUi(self)
    
            assert isinstance(self.cbDateTimePrecission, QComboBox)
    
            from eotimeseriesviewer.timeseries import DateTimePrecision
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            for e in DateTimePrecision:
                assert isinstance(e, enum.Enum)
                self.cbDateTimePrecission.addItem(e.name, e)
    
    
            self.cbSensorMatchingPxDims.setToolTip(SensorMatching.tooltip(SensorMatching.PX_DIMS))
            self.cbSensorMatchingWavelength.setToolTip(SensorMatching.tooltip(SensorMatching.WL))
            self.cbSensorMatchingSensorName.setToolTip(SensorMatching.tooltip(SensorMatching.NAME))
            self.cbSensorMatchingPxDims.stateChanged.connect(self.validate)
            self.cbSensorMatchingWavelength.stateChanged.connect(self.validate)
            self.cbSensorMatchingSensorName.stateChanged.connect(self.validate)
    
            self.mFileWidgetScreenshots.setStorageMode(QgsFileWidget.GetDirectory)
            self.mFileWidgetRasterSources.setStorageMode(QgsFileWidget.GetDirectory)
            self.mFileWidgetVectorSources.setStorageMode(QgsFileWidget.GetDirectory)
    
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            self.cbDateTimePrecission.currentIndexChanged.connect(self.validate)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
            self.sbMapSizeX.valueChanged.connect(self.validate)
            self.sbMapSizeY.valueChanged.connect(self.validate)
            self.sbMapRefreshIntervall.valueChanged.connect(self.validate)
    
    
            self.mMapTextFormatButton.changed.connect(self.validate)
    
    
            assert isinstance(self.buttonBox, QDialogButtonBox)
            self.buttonBox.button(QDialogButtonBox.RestoreDefaults).clicked.connect(lambda: self.setValues(defaultValues()))
            self.buttonBox.button(QDialogButtonBox.Ok).clicked.connect(self.onAccept)
            self.buttonBox.button(QDialogButtonBox.Cancel)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
            self.mLastValues = values()
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
            self.mSensorSpecsModel = SensorSettingsTableModel()
            self.mSensorSpecsProxyModel = QSortFilterProxyModel()
            self.mSensorSpecsProxyModel.setSourceModel(self.mSensorSpecsModel)
    
            self.tableViewSensorSettings.setModel(self.mSensorSpecsProxyModel)
            sm = self.tableViewSensorSettings.selectionModel()
            assert isinstance(sm, QItemSelectionModel)
            sm.selectionChanged.connect(self.onSensorSettingsSelectionChanged)
    
            self.btnDeleteSelectedSensors.setDefaultAction(self.actionDeleteSelectedSensors)
            self.btnReloadSensorSettings.setDefaultAction(self.actionRefreshSensorList)
            self.actionRefreshSensorList.triggered.connect(self.mSensorSpecsModel.reload)
    
            self.actionDeleteSelectedSensors.triggered.connect(self.onRemoveSelectedSensors)
            self.actionDeleteSelectedSensors.setEnabled(len(sm.selectedRows()) > 0)
            self.mSensorSpecsModel.clear()
    
            self.setValues(self.mLastValues)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
    
        def onRemoveSelectedSensors(self):
    
            sm = self.tableViewSensorSettings.selectionModel()
            assert isinstance(sm, QItemSelectionModel)
    
    
            for r in sm.selectedRows():
    
                srcIdx = self.tableViewSensorSettings.model().mapToSource(r)
                sensor = srcIdx.data(role=Qt.UserRole)
                if isinstance(sensor, SensorInstrument):
                    toRemove.append(sensor)
            if len(toRemove) > 0:
                self.mSensorSpecsModel.removeSensors(toRemove)
    
    
    
        def onSensorSettingsSelectionChanged(self, selected:QItemSelection, deselected:QItemSelection):
            self.actionDeleteSelectedSensors.setEnabled(len(selected) > 0)
    
    
        def validate(self, *args):
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
            values = self.values()
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
        def onAccept(self):
    
            self.setResult(QDialog.Accepted)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
            values = self.values()
            setValues(values)
    
            #self.mSensorSpecsModel.saveSettings()
    
            if values != self.mLastValues:
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
                pass
    
    
        def values(self)->dict:
            """
            Returns the settings as dictionary
            :return: dict
            """
            d = dict()
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
            d[Keys.ScreenShotDirectory] = self.mFileWidgetScreenshots.filePath()
            d[Keys.RasterSourceDirectory] = self.mFileWidgetRasterSources.filePath()
            d[Keys.VectorSourceDirectory] = self.mFileWidgetVectorSources.filePath()
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
            d[Keys.DateTimePrecision] = self.cbDateTimePrecission.currentData()
    
    
            flags = SensorMatching.PX_DIMS
            if self.cbSensorMatchingWavelength.isChecked():
                flags = flags | SensorMatching.WL
            if self.cbSensorMatchingSensorName.isChecked():
                flags = flags | SensorMatching.NAME
    
            d[Keys.SensorMatching] = flags
            d[Keys.SensorSpecs] = self.mSensorSpecsModel.specs()
    
            d[Keys.MapSize] = QSize(self.sbMapSizeX.value(), self.sbMapSizeY.value())
            d[Keys.MapUpdateInterval] = self.sbMapRefreshIntervall.value()
            d[Keys.MapBackgroundColor] = self.mCanvasColorButton.color()
    
            d[Keys.MapTextFormat] = self.mMapTextFormatButton.textFormat()
    
    
    
            for k in self.mLastValues.keys():
                if k not in d.keys():
                    d[k] = self.mLastValues[k]
    
    
            return d
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
        def setValues(self, values: dict):
            """
            Sets the values as stored in a dictionary or QSettings object
            :param values: dict | QSettings
            """
    
            if isinstance(values, QSettings):
                d = dict()
                for k in values.allKeys():
    
                    try:
                        d[k] = values.value(k)
                    except Exception as ex:
                        s = "" #TypeError: unable to convert a QVariant back to a Python object
    
                values = d
    
            assert isinstance(values, dict)
    
            def checkKey(val, key:Keys):
                assert isinstance(key, Keys)
                return val in [key, key.value, key.name]
    
            for key, value in values.items():
                if checkKey(key, Keys.ScreenShotDirectory) and isinstance(value, str):
                    self.mFileWidgetScreenshots.setFilePath(value)
                if checkKey(key, Keys.RasterSourceDirectory) and isinstance(value, str):
                    self.mFileWidgetRasterSources.setFilePath(value)
                if checkKey(key, Keys.VectorSourceDirectory) and isinstance(value, str):
                    self.mFileWidgetVectorSources.setFilePath(value)
    
                if checkKey(key, Keys.DateTimePrecision):
                    i = self.cbDateTimePrecission.findData(value)
                    if i > -1:
                        self.cbDateTimePrecission.setCurrentIndex(i)
    
                    assert isinstance(value, SensorMatching)
                    self.cbSensorMatchingPxDims.setChecked(bool(value & SensorMatching.PX_DIMS))
                    self.cbSensorMatchingWavelength.setChecked(bool(value & SensorMatching.WL))
                    self.cbSensorMatchingSensorName.setChecked(bool(value & SensorMatching.NAME))
    
                if checkKey(key, Keys.SensorSpecs):
                    assert isinstance(value, dict)
                    self.mSensorSpecsModel.setSpecs(value)
    
    
                if checkKey(key, Keys.MapSize) and isinstance(value, QSize):
                    self.sbMapSizeX.setValue(value.width())
                    self.sbMapSizeY.setValue(value.height())
    
                if checkKey(key, Keys.MapUpdateInterval) and isinstance(value, (float, int)) and value > 0:
                    self.sbMapRefreshIntervall.setValue(value)
    
                if checkKey(key, Keys.MapBackgroundColor) and isinstance(value, QColor):
                    self.mCanvasColorButton.setColor(value)
    
    Benjamin Jakimow's avatar
    Benjamin Jakimow committed
    
    
                if checkKey(key, Keys.MapTextFormat) and isinstance(value, QgsTextFormat):
                    self.mMapTextFormatButton.setTextFormat(value)