Newer
Older
import os, enum, pathlib, re, json, pickle
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

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'

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

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
def settings()->QSettings:
"""
Returns the EOTSV settings.
:return: QSettings
"""
settings = QSettings(QSettings.UserScope, 'HU-Berlin', 'EO-TimeSeriesViewer')
def value(key:Keys, default=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 key == Keys.MapTextFormat:
if isinstance(value, QByteArray):
doc = QDomDocument()
doc.setContent(value)
value = QgsTextFormat()
value.readXml(doc.documentElement(), QgsReadWriteContext())

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 = ""
return value
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)

Benjamin Jakimow
committed
def setValue(key:Keys, value):
"""
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()
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'
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
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
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
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
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
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.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)
values = self.values()
setValues(values)
#self.mSensorSpecsModel.saveSettings()
def values(self)->dict:
"""
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
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()

Benjamin Jakimow
committed
d[Keys.MapTextFormat] = self.mMapTextFormatButton.textFormat()
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)
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)

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)