Newer
Older
from qgis.core import QgsReadWriteContext, QgsTextFormat, QgsTextBufferSettings, QgsUnitTypes
from qgis.gui import QgsFileWidget
from eotimeseriesviewer import *
from eotimeseriesviewer import __version__ as EOTSV_VERSION
from eotimeseriesviewer.timeseries import SensorMatching, SensorInstrument

Benjamin Jakimow
committed
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'

Benjamin Jakimow
committed
MapTextFormat = 'map_text_format'

Benjamin Jakimow
committed
SensorMatching = 'sensor_matching'
ScreenShotDirectory = 'screen_shot_directory'
RasterSourceDirectory = 'raster_source_directory'
VectorSourceDirectory = 'vector_source_directory'
MapImageExportDirectory = 'map_image_export_directory'
SettingsVersion = 'settings_version'
Debug = 'debug'
QgsTaskAsync = 'qgs_task_async'
QgsTaskBlockSize = 'qgs_task_block_size'
BandStatsSampleSize = 'band_stats_sample_size'
RasterOverlapSampleSize = 'raster_overlap_sample_size'

Benjamin Jakimow
committed
StartupRestoreProjectSettings = 'startup_load_projectsettings'

Benjamin Jakimow
committed
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
import eotimeseriesviewer
d[Keys.Debug] = eotimeseriesviewer.DEBUG
# map visualization
d[Keys.MapUpdateInterval] = 500 # milliseconds
d[Keys.MapSize] = QSize(150, 150)
d[Keys.MapBackgroundColor] = QColor('black')

Benjamin Jakimow
committed
d[Keys.QgsTaskAsync] = True
d[Keys.QgsTaskBlockSize] = 25
d[Keys.BandStatsSampleSize] = 256

Benjamin Jakimow
committed
d[Keys.StartupRestoreProjectSettings] = True
d[Keys.SettingsVersion] = EOTSV_VERSION

Benjamin Jakimow
committed
textFormat = QgsTextFormat()
textFormat.setColor(QColor('black'))

Benjamin Jakimow
committed
textFormat.setSizeUnit(QgsUnitTypes.RenderPoints)
textFormat.setFont(QFont('Helvetica'))
textFormat.setSize(11)
buffer = QgsTextBufferSettings()
buffer.setColor(QColor('white'))
buffer.setSize(5)

Benjamin Jakimow
committed
buffer.setSizeUnit(QgsUnitTypes.RenderPixels)
buffer.setEnabled(True)
textFormat.setBuffer(buffer)
d[Keys.MapTextFormat] = textFormat
# tbd. other settings
return d
"""
Returns the EOTSV settings.
:return: QSettings
"""
settings = QSettings(QSettings.UserScope, 'HU-Berlin', 'EO-TimeSeriesViewer')
if (not settings().contains(Keys.SettingsVersion.value)) or \
str(settings().value(Keys.SettingsVersion.value)) < '1.10.2020':
# addresses issue https://bitbucket.org/jakimowb/eo-time-series-viewer/issues/103/tsv-crashes-qgis-on-linux
# which was caused by a wrong serialization of a QgsTextFormat object
settings().setValue(Keys.MapTextFormat.value, None)
"""
Provides direct access to a settings value

Benjamin Jakimow
committed
:param key: Keys
:param default: default value, defaults to None
:return: value | None
"""

Benjamin Jakimow
committed
assert isinstance(key, Keys)
assert isinstance(key.value, str)
value = None
try:

Benjamin Jakimow
committed
value = settings().value(key.value, defaultValue=default)
if value == QVariant():
value = None
if value == 'true':
value = True
elif value == 'false':
value = False

Benjamin Jakimow
committed
if key == Keys.MapTextFormat:
if isinstance(value, QByteArray):
doc = QDomDocument()
doc.setContent(value)
value = QgsTextFormat()
value.readXml(doc.documentElement(), QgsReadWriteContext())
if key == Keys.QgsTaskAsync:
value = bool(value)
if key == Keys.QgsTaskBlockSize:
value = int(value)
if key == Keys.BandStatsSampleSize:
value = int(value)
if key == Keys.RasterOverlapSampleSize:
value = int(value)
if key == Keys.MapUpdateInterval:
value = int(value)

Benjamin Jakimow
committed
if key == Keys.SensorSpecs:
# check sensor specs
if value is None:
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

Benjamin Jakimow
committed
settings().setValue(key.value, None)
print(error, file=sys.stderr)
except Exception as otherError:
s = ""
if value is None:
value = default
return value
"""
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(sid: typing.Union[str, SensorInstrument]) -> str:
"""
Retuns the sensor name stored for a certain sensor id
if isinstance(sid, SensorInstrument):
sid = sid.id()
sensorSpecs = value(Keys.SensorSpecs, default=dict())
assert isinstance(sensorSpecs, dict)
return sSpecs.get('name', None)
"""
Shortcut to save a value into the EOTSV settings
:param key: str | Key
:param value: any value
"""

Benjamin Jakimow
committed
assert isinstance(key, Keys)
assert isinstance(key.value, str)

Benjamin Jakimow
committed
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)

Benjamin Jakimow
committed
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():

Benjamin Jakimow
committed
setValue(key, val)
settings().sync()
"""
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:
del self.mSensors[i]
self.endRemoveRows()
def loadSettings(self):
sensorSpecs = value(Keys.SensorSpecs, default={})
sensors = []
for sid, specs in sensorSpecs.items():
sensor = SensorInstrument(sid)
sensor.setName(specs['name'])
sensors.append(sensor)
self.addSensors(sensors)
def removeSensors(self, sensors: typing.List[SensorInstrument]):
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:
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()
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)
"""
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 [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
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
"""
A widget to change settings
"""
def __init__(self, title='<#>', parent=None):
super(SettingsDialog, self).__init__(parent)
loadUi(DIR_UI / 'settingsdialog.ui', self)
from eotimeseriesviewer.timeseries import DateTimePrecision
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)

Benjamin Jakimow
committed
self.cbDebug.stateChanged.connect(self.validate)
self.mFileWidgetScreenshots.setStorageMode(QgsFileWidget.GetDirectory)
self.mFileWidgetRasterSources.setStorageMode(QgsFileWidget.GetDirectory)
self.mFileWidgetVectorSources.setStorageMode(QgsFileWidget.GetDirectory)
self.cbDateTimePrecission.currentIndexChanged.connect(self.validate)
self.sbMapSizeX.valueChanged.connect(self.validate)
self.sbMapSizeY.valueChanged.connect(self.validate)
self.sbMapRefreshIntervall.valueChanged.connect(self.validate)

Benjamin Jakimow
committed
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)
self.mLastValues = values()
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)
def onRemoveSelectedSensors(self):
sm = self.tableViewSensorSettings.selectionModel()
assert isinstance(sm, QItemSelectionModel)
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)
if Keys.Debug in values.keys():
import eotimeseriesviewer
eotimeseriesviewer.DEBUG = values[Keys.Debug]
values = self.values()
setValues(values)
"""
Returns the settings as dictionary
:return: dict
"""
d = dict()
d[Keys.ScreenShotDirectory] = self.mFileWidgetScreenshots.filePath()
d[Keys.RasterSourceDirectory] = self.mFileWidgetRasterSources.filePath()
d[Keys.VectorSourceDirectory] = self.mFileWidgetVectorSources.filePath()
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

Benjamin Jakimow
committed
d[Keys.StartupRestoreProjectSettings] = self.cbStartupRestoreSettings.isChecked()
d[Keys.SensorMatching] = flags

Benjamin Jakimow
committed
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()

Benjamin Jakimow
committed
d[Keys.MapTextFormat] = self.mMapTextFormatButton.textFormat()
# others page
d[Keys.Debug] = self.cbDebug.isChecked()
d[Keys.QgsTaskAsync] = self.cbAsyncQgsTasks.isChecked()
d[Keys.QgsTaskBlockSize] = self.sbQgsTaskBlockSize.value()
d[Keys.BandStatsSampleSize] = self.sbBandStatsSampleSize.value()
d[Keys.RasterOverlapSampleSize] = self.sbRasterOverlapSampleSize.value()
for k in self.mLastValues.keys():
if k not in d.keys():
d[k] = self.mLastValues[k]
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)
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)

Benjamin Jakimow
committed
if checkKey(key, Keys.StartupRestoreProjectSettings):
self.cbStartupRestoreSettings.setChecked(bool(value in [True, 'true', 'True']))

Benjamin Jakimow
committed
if checkKey(key, Keys.SensorMatching):
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
committed
if checkKey(key, Keys.MapTextFormat) and isinstance(value, QgsTextFormat):
self.mMapTextFormatButton.setTextFormat(value)
# others page
if checkKey(key, Keys.Debug) and isinstance(value, bool):
self.cbDebug.setChecked(value)
if checkKey(key, Keys.QgsTaskAsync) and isinstance(value, bool):
self.cbAsyncQgsTasks.setChecked(value)
if checkKey(key, Keys.QgsTaskBlockSize) and isinstance(value, int):
self.sbQgsTaskBlockSize.setValue(value)
if checkKey(key, Keys.BandStatsSampleSize) and isinstance(value, int):
self.sbBandStatsSampleSize.setValue(value)
if checkKey(key, Keys.RasterOverlapSampleSize) and isinstance(value, int):
self.sbRasterOverlapSampleSize.setValue(value)