Commit 6e22706d authored by Benjamin Jakimow's avatar Benjamin Jakimow
Browse files

timeseries.py


- fixed error when aborting save as time series dialog
- TimeSeries can be saved to QgsProject QDomDocument, including SensorInstrument
Signed-off-by: Benjamin Jakimow's avatarBenjamin Jakimow <benjamin.jakimow@geo.hu-berlin.de>
parent 5796e847
......@@ -42,7 +42,8 @@ import qgis.utils
from qgis.core import *
from qgis.core import QgsMapLayer, QgsRasterLayer, QgsVectorLayer, QgsMessageOutput, QgsCoordinateReferenceSystem, \
Qgis, QgsWkbTypes, QgsTask, QgsProviderRegistry, QgsMapLayerStore, QgsFeature, QgsField, \
QgsTextFormat, QgsProject, QgsSingleSymbolRenderer, QgsGeometry, QgsApplication, QgsFillSymbol
QgsTextFormat, QgsProject, QgsSingleSymbolRenderer, QgsGeometry, QgsApplication, QgsFillSymbol, \
QgsProjectArchive, QgsZipUtils
from qgis.gui import *
from qgis.gui import QgsMapCanvas, QgsStatusBar, QgsFileWidget, \
......@@ -229,7 +230,7 @@ class EOTimeSeriesViewerUI(QMainWindow):
def onMapToolActionToggled(self, b: bool, action: QAction):
"""
Reacts on togglinga map tool
Reacts on toggling a map tool
:param b:
:param action:
"""
......@@ -289,14 +290,15 @@ LUT_MESSAGELOGLEVEL = {
def showMessage(message, title, level):
v = QgsMessageViewer()
v.setTitle(title)
# print('DEBUG MSG: {}'.format(message))
v.setMessage(message, QgsMessageOutput.MessageHtml \
if message.startswith('<html>')
else QgsMessageOutput.MessageText)
if message.startswith('<html>'):
v.setMessage(message, QgsMessageOutput.MessageHtml)
else:
v.setMessage(message, QgsMessageOutput.MessageText)
v.showMessage(True)
class TaskManagerStatusButton(QToolButton):
def __init__(self, parent: QWidget = None):
super().__init__(parent)
......@@ -612,6 +614,7 @@ class EOTimeSeriesViewer(QgisInterface, QObject):
from qgis.utils import iface
self.ui.actionLoadProject.triggered.connect(iface.actionOpenProject().trigger)
self.ui.actionReloadProject.triggered.connect(self.onReloadProject)
self.ui.actionSaveProject.triggered.connect(iface.actionSaveProject().trigger)
self.profileDock.actionLoadProfileRequest.triggered.connect(self.activateIdentifyTemporalProfileMapTool)
......@@ -678,18 +681,39 @@ class EOTimeSeriesViewer(QgisInterface, QObject):
self.mapWidget().writeXml(node, dom)
root.appendChild(node)
def onReadProject(self, dom: QDomDocument):
def onReloadProject(self, *args):
if dom is None:
s = ""
proj: QgsProject = QgsProject.instance()
path = proj.fileName()
if os.path.isfile(path):
archive = None
if QgsZipUtils.isZipFile(path):
archive = QgsProjectArchive()
archive.unzip(path)
path = archive.projectFile()
file = QFile(path)
root = dom.documentElement()
doc = QDomDocument('qgis')
doc.setContent(file)
self.onReadProject(doc)
if isinstance(archive, QgsProjectArchive):
archive.clearProjectFile()
def onReadProject(self, doc: QDomDocument) -> bool:
if not isinstance(doc, QDomDocument):
return False
root = doc.documentElement()
node = root.firstChildElement('EOTSV')
if node.nodeName() == 'EOTSV':
self.timeSeries().readXml(node)
self.mapWidget().readXml(node)
return True
def lockCentralWidgetSize(self, b: bool):
"""
Locks or release the current central widget size
......@@ -1392,9 +1416,9 @@ class EOTimeSeriesViewer(QgisInterface, QObject):
filters = "CSV (*.csv *.txt);;" + \
"All files (*.*)"
path, filter = QFileDialog.getSaveFileName(caption='Save Time Series definition', filter=filters,
directory=defFile)
path = self.mTimeSeries.saveToFile(path)
if path is not None:
directory=defFile)
if path not in [None, '']:
path = self.mTimeSeries.saveToFile(path)
s.setValue('FILE_TS_DEFINITION', path)
def loadTimeSeriesStack(self):
......@@ -1571,7 +1595,7 @@ class EOTimeSeriesViewer(QgisInterface, QObject):
return vectorLayers
def addTimeSeriesImages(self, files: list, loadAsync:bool=True):
def addTimeSeriesImages(self, files: list, loadAsync: bool = True):
"""
Adds images to the time series
:param files:
......
......@@ -34,7 +34,7 @@ from qgis.core import *
from qgis.core import QgsContrastEnhancement, QgsRasterShader, QgsColorRampShader, QgsProject, \
QgsCoordinateReferenceSystem, QgsVector, QgsTextFormat, \
QgsRectangle, QgsRasterRenderer, QgsMapLayerStore, QgsMapLayerStyle, \
QgsLayerTreeModel, QgsLayerTreeGroup, \
QgsLayerTreeModel, QgsLayerTreeGroup, \
QgsLayerTree, QgsLayerTreeLayer, \
QgsRasterLayer, QgsVectorLayer, QgsMapLayer, QgsMapLayerProxyModel, QgsColorRamp, QgsSingleBandPseudoColorRenderer
......@@ -417,7 +417,6 @@ class MapView(QFrame):
else:
return self.tbName.text().strip()
def setCrosshairStyle(self, crosshairStyle: CrosshairStyle) -> CrosshairStyle:
"""
Seths the CrosshairStyle of this MapView
......@@ -558,8 +557,8 @@ class MapView(QFrame):
sensor: SensorInstrument = masterLayer.sensor()
style: QgsMapLayerStyle = masterLayer.mapLayerStyle()
#print('### MASTER-STYLE-CHANGED')
#print(style.xmlData())
# print('### MASTER-STYLE-CHANGED')
# print(style.xmlData())
for lyr in self.sensorLayers(sensor):
lyr.setMapLayerStyle(style)
......@@ -1198,7 +1197,15 @@ class MapWidget(QFrame):
:return:
"""
mwNode = doc.createElement('EOTSV_MAPWIDGET')
context = QgsReadWriteContext()
mapSize = self.mapSize()
mwNode.setAttribute('mapsPerMapView', f'{self.mapsPerMapView()}')
mwNode.setAttribute('mapWidth', f'{mapSize.width()}')
mwNode.setAttribute('mapHeight', f'{mapSize.height()}')
crsNode = doc.createElement('MapExtent')
self.spatialExtent().writeXml(crsNode, doc)
mwNode.appendChild(crsNode)
for mapView in self.mapViews():
mvNode = doc.createElement('MAP_VIEW')
mvNode.setAttribute('name', mapView.name())
......@@ -1217,27 +1224,44 @@ class MapWidget(QFrame):
return True
def readXml(self, node: QDomNode):
if not node.nodeName() == 'EOTSV_MAPWIDGET':
node = node.firstChildElement('EOTSV_MAPWIDGET')
if node.isNull():
return None
context = QgsReadWriteContext()
if node.hasAttribute('mapsPerMapView'):
self.setMapsPerMapView(int(node.attribute('mapsPerMapView')))
if node.hasAttribute('mapWidth') and node.hasAttribute('mapHeight'):
mapSize = QSize(
int(node.attribute('mapWidth')),
int(node.attribute('mapHeight'))
)
self.setMapSize(mapSize)
mvNode = node.firstChildElement('MAP_VIEW')
while not mvNode.isNull():
nodeExtent = node.firstChildElement('MapExtent')
if nodeExtent.nodeName() == 'MapExtent':
extent = SpatialExtent.readXml(nodeExtent)
if isinstance(extent, SpatialExtent):
self.setCrs(extent.crs())
self.setSpatialExtent(extent)
mvNode = node.firstChildElement('MAP_VIEW').toElement()
while mvNode.nodeName() == 'MAP_VIEW':
mvName = mvNode.attribute('name')
mapView: MapView = None
# find existing map view with same name
for mv in self.mapViews():
if mv.name() == mvName:
mapView = mv
# no map view with same name, create a new
if not isinstance(mapView, MapView):
mapView = MapView()
mapView.setName(mvName)
self.addMapView(mapView)
sensorNode = mvNode.firstChildElement('MAP_VIEW_PROXY_LAYER').toElement()
while not sensorNode.isNull():
while not sensorNode.nodeName() == 'MAP_VIEW_PROXY_LAYER':
sid = sensorNode.attribute('sensor_id')
sensor = self.timeSeries().findMatchingSensor(sid)
if sensor:
......@@ -1249,13 +1273,10 @@ class MapWidget(QFrame):
style.readXml(styleNode)
lyr.setMapLayerStyle(style)
s = ""
sensorNode = sensorNode.nextSibling().toElement()
mvNode = mvNode.nextSibling()
s = ""
mvNode = mvNode.nextSibling().toElement()
def usedLayers(self):
def usedLayers(self) -> typing.List[QgsMapLayer]:
layers = set()
for c in self.mapCanvases():
layers = layers.union(set(c.layers()))
......@@ -1265,7 +1286,7 @@ class MapWidget(QFrame):
return self.mCrs
def setTimeSeries(self, ts: TimeSeries) -> TimeSeries:
assert ts == None or isinstance(ts, TimeSeries)
assert ts is None or isinstance(ts, TimeSeries)
self.mTimeSeries = ts
if isinstance(self.mTimeSeries, TimeSeries):
self.mTimeSeries.sigVisibilityChanged.connect(self._updateCanvasDates)
......@@ -1310,7 +1331,7 @@ class MapWidget(QFrame):
i = self.timeSeries()[:].index(tsd)
slider.setValue(i + 1)
def onSliderMoved(self, value:int):
def onSliderMoved(self, value: int):
tsd = self.sliderDate(i=value)
if isinstance(tsd, TimeSeriesDate):
self.tbSliderDate.setText(f'{tsd.date()}')
......@@ -1790,7 +1811,7 @@ class MapWidget(QFrame):
def _layerListKey(self, canvas: MapCanvas) -> typing.Tuple[MapView, TimeSeriesDate]:
return (canvas.mapView(), canvas.tsd())
def _updateCanvasDates(self, updateLayerCache:bool = True):
def _updateCanvasDates(self, updateLayerCache: bool = True):
visibleBefore = self.visibleTSDs()
bTSDChanged = False
......@@ -1871,7 +1892,7 @@ class MapWidget(QFrame):
item.setVisibility(style.mShow)
assert isinstance(item, CrosshairMapCanvasItem)
item.setCrosshairStyle(style)
#canvas.addToRefreshPipeLine(MapCanvas.Command.UpdateMapItems)
# canvas.addToRefreshPipeLine(MapCanvas.Command.UpdateMapItems)
def _updateCanvasAppearance(self, mapView=None):
......@@ -1923,7 +1944,7 @@ class MapWidget(QFrame):
info.setUpperCenter(uc)
info.setLowerCenter(lc)
#canvas.addToRefreshPipeLine(MapCanvas.Command.UpdateMapItems)
# canvas.addToRefreshPipeLine(MapCanvas.Command.UpdateMapItems)
class MapViewDock(QgsDockWidget):
......
......@@ -132,6 +132,8 @@ def getDS(pathOrDataset) -> gdal.Dataset:
def sensorID(nb: int, px_size_x: float, px_size_y: float, dt: int, wl: list, wlu: str, name: str) -> str:
"""
Creates a sensor ID str
:param name:
:param dt:
:param nb: number of bands
:param px_size_x: pixel size x
:param px_size_y: pixel size y
......@@ -144,15 +146,16 @@ def sensorID(nb: int, px_size_x: float, px_size_y: float, dt: int, wl: list, wlu
assert isinstance(px_size_x, (int, float)) and px_size_x > 0
assert isinstance(px_size_y, (int, float)) and px_size_y > 0
if wl != None:
if wl is not None:
assert isinstance(wl, list)
assert len(wl) == nb
if wlu != None:
if wlu is not None:
assert isinstance(wlu, str)
if name != None:
if name is not None:
assert isinstance(name, str)
jsonDict = {'nb': nb,
'px_size_x': px_size_x,
'px_size_y': px_size_y,
......@@ -168,7 +171,7 @@ def sensorIDtoProperties(idString: str) -> tuple:
"""
Reads a sensor id string and returns the sensor properties. See sensorID().
:param idString: str
:return: (ns, px_size_x, px_size_y, [wl], wlu)
:return: (ns, px_size_x, px_size_y, dt, wl, wlu, name)
"""
jsonDict = json.loads(idString)
......@@ -188,11 +191,11 @@ def sensorIDtoProperties(idString: str) -> tuple:
assert isinstance(nb, int)
assert isinstance(px_size_x, (int, float)) and px_size_x > 0
assert isinstance(px_size_y, (int, float)) and px_size_y > 0
if wl != None:
if wl is not None:
assert isinstance(wl, list)
if wlu != None:
if wlu is not None:
assert isinstance(wlu, str)
if name != None:
if name is not None:
assert isinstance(name, str)
return nb, px_size_x, px_size_y, dt, wl, wlu, name
......@@ -204,14 +207,16 @@ class SensorInstrument(QObject):
SensorNameSettingsPrefix = 'SensorName.'
sigNameChanged = pyqtSignal(str)
LUT_Wavelengths = dict({'B': 480,
'G': 570,
'R': 660,
'nIR': 850,
'swIR': 1650,
'swIR1': 1650,
'swIR2': 2150
})
@staticmethod
def readXml(node: QDomNode):
sensor: SensorInstrument = None
nodeId = node.firstChildElement('SensorId').toElement()
if nodeId.nodeName() == 'SensorId':
sid = nodeId.firstChild().nodeValue()
sensor = SensorInstrument(sid)
s = ""
return sensor
def __init__(self, sid: str, band_names: list = None):
super(SensorInstrument, self).__init__()
......@@ -223,16 +228,17 @@ class SensorInstrument(QObject):
self.dataType: int
self.wl: list
self.wlu: str
self.nb, self.px_size_x, self.px_size_y, self.dataType, self.wl, self.wlu, self.mNameOriginal = sensorIDtoProperties(
self.mId)
self.nb, self.px_size_x, self.px_size_y, self.dataType, self.wl, self.wlu, self.mNameOriginal \
= sensorIDtoProperties(self.mId)
if self.mNameOriginal in [None, '']:
self.mNameOriginal = '{}bands@{}m'.format(self.nb, self.px_size_x)
self.mName = '{}bands@{}m'.format(self.nb, self.px_size_x)
else:
self.mName = self.mNameOriginal
self.mName = self.mNameOriginal
import eotimeseriesviewer.settings
storedName = eotimeseriesviewer.settings.sensorName(self.mId)
if isinstance(storedName, str):
self.mName = storedName
#import eotimeseriesviewer.settings
#storedName = eotimeseriesviewer.settings.sensorName(self.mId)
#if isinstance(storedName, str):
# self.mName = storedName
if not isinstance(band_names, list):
band_names = ['Band {}'.format(b + 1) for b in range(self.nb)]
......@@ -263,6 +269,14 @@ class SensorInstrument(QObject):
self.mMockupDS.FlushCache()
s = ""
def writeXml(self, node: QDomNode, doc: QDomDocument):
nodeId = doc.createElement('SensorId')
nodeId.appendChild(doc.createTextNode(self.id()))
nodeName = doc.createElement('SensorName')
nodeName.appendChild(doc.createTextNode(self.name()))
node.appendChild(nodeId)
node.appendChild(nodeName)
def bandIndexClosestToWavelength(self, wl, wl_unit='nm') -> int:
"""
......@@ -367,15 +381,16 @@ class SensorProxyLayer(QgsRasterLayer):
self.mStyleXml = xml
self.styleChanged.emit()
#xml2 = self.mapLayerStyle().xmlData()
#assert xml2 == xml
#assert self.mStyleXml == xml
# xml2 = self.mapLayerStyle().xmlData()
# assert xml2 == xml
# assert self.mStyleXml == xml
def mapLayerStyle(self) -> QgsMapLayerStyle:
style = QgsMapLayerStyle()
style.readFromLayer(self)
return style
def verifyInputImage(datasource):
"""
Checks if an image source can be uses as TimeSeriesDate, i.e. if it can be read by gdal.Open() and
......@@ -1201,9 +1216,9 @@ class TimeSeriesFindOverlapTask(QgsTask):
continue
stats: QgsRasterBandStats = lyr.dataProvider().bandStatistics(1,
stats=QgsRasterBandStats.Range,
extent=targetExtent2,
sampleSize=self.mSampleSize)
stats=QgsRasterBandStats.Range,
extent=targetExtent2,
sampleSize=self.mSampleSize)
del lyr
......@@ -1504,7 +1519,8 @@ class TimeSeries(QAbstractItemModel):
refValues = (nb, px_size_y, px_size_x, dt, wl, wlu, name)
for sensor in self.sensors():
sValues = (
sensor.nb, sensor.px_size_y, sensor.px_size_x, sensor.dataType, sensor.wl, sensor.wlu, sensor.mNameOriginal)
sensor.nb, sensor.px_size_y, sensor.px_size_x, sensor.dataType, sensor.wl, sensor.wlu,
sensor.mNameOriginal)
if refValues == sValues:
return sensor
......@@ -1837,10 +1853,10 @@ class TimeSeries(QAbstractItemModel):
qgsTask = TimeSeriesLoadingTask(sourcePaths,
callback=self.onTaskFinished,
)
#tid = id(qgsTask)
#self.mTasks[tid] = qgsTask
#qgsTask.taskCompleted.connect(lambda *args, tid=tid: self.onRemoveTask(tid))
#qgsTask.taskTerminated.connect(lambda *args, tid=tid: self.onRemoveTask(tid))
# tid = id(qgsTask)
# self.mTasks[tid] = qgsTask
# qgsTask.taskCompleted.connect(lambda *args, tid=tid: self.onRemoveTask(tid))
# qgsTask.taskTerminated.connect(lambda *args, tid=tid: self.onRemoveTask(tid))
qgsTask.sigFoundSources.connect(self.addTimeSeriesSources)
qgsTask.progressChanged.connect(self.sigProgress.emit)
......@@ -1852,14 +1868,14 @@ class TimeSeries(QAbstractItemModel):
qgsTask.finished(qgsTask.run())
def onRemoveTask(self, key):
#print(f'remove {key}', flush=True)
# print(f'remove {key}', flush=True)
if isinstance(key, QgsTask):
key = id(key)
if key in self.mTasks.keys():
self.mTasks.pop(key)
def onTaskFinished(self, success, task: QgsTask):
#print(':: onAddSourcesAsyncFinished')
# print(':: onAddSourcesAsyncFinished')
if isinstance(task, TimeSeriesLoadingTask):
if len(task.mInvalidSources) > 0:
info = ['Unable to load {} data source(s):'.format(len(task.mInvalidSources))]
......@@ -1871,7 +1887,6 @@ class TimeSeries(QAbstractItemModel):
if success and len(task.mIntersections) > 0:
self.onFoundOverlap(task.mIntersections)
def addTimeSeriesSource(self, source: TimeSeriesSource) -> TimeSeriesDate:
"""
:param source:
......@@ -2133,6 +2148,12 @@ class TimeSeries(QAbstractItemModel):
:return:
"""
tsNode = doc.createElement('EOTSV_TIMESERIES')
for sensor in self.sensors():
sensorNode = doc.createElement('Sensor')
sensor.writeXml(sensorNode, doc)
tsNode.appendChild(sensorNode)
for tss in self.sources():
tssNode = doc.createElement('TIME_SERIES_SOURCE')
tssNode.appendChild(doc.createTextNode((tss.uri())))
......@@ -2146,9 +2167,24 @@ class TimeSeries(QAbstractItemModel):
if node.isNull():
return None
sensorNode = node.firstChildElement('Sensor')
while sensorNode.nodeName() == 'Sensor':
sensor = SensorInstrument.readXml(sensorNode)
if isinstance(sensor, SensorInstrument):
sid = sensor.id()
name = sensor.name()
self.addSensor(sensor)
# set name on sensor instance (which might be already there)
sensor = self.sensor(sid)
if isinstance(sensor, SensorInstrument):
sensor.setName(name)
sensorNode = sensorNode.nextSibling()
tssNode = node.firstChildElement('TIME_SERIES_SOURCE')
to_add = []
while not tssNode.isNull():
while tssNode.nodeName() == 'TIME_SERIES_SOURCE':
uri = tssNode.firstChild().nodeValue()
to_add.append(uri)
tssNode = tssNode.nextSibling()
......
......@@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>1035</width>
<width>774</width>
<height>253</height>
</rect>
</property>
......@@ -74,7 +74,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>1035</width>
<width>774</width>
<height>17</height>
</rect>
</property>
......@@ -106,6 +106,7 @@
<addaction name="actionClearTS"/>
<addaction name="separator"/>
<addaction name="actionSaveProject"/>
<addaction name="actionReloadProject"/>
<addaction name="actionLoadProject"/>
<addaction name="separator"/>
<addaction name="actionAddTSExample"/>
......@@ -306,7 +307,7 @@
<normaloff>:/eotimeseriesviewer/icons/mActionSaveTS.svg</normaloff>:/eotimeseriesviewer/icons/mActionSaveTS.svg</iconset>
</property>
<property name="text">
<string>Save time serie</string>
<string>Save Time Series</string>
</property>
<property name="toolTip">
<string>Save time series as CSV file</string>
......@@ -947,6 +948,14 @@
<string>Load from Project</string>
</property>
</action>
<action name="actionReloadProject">
<property name="text">
<string>Reload Project</string>
</property>
<property name="toolTip">
<string>Reloads settings from the QGIS Project</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment