Newer
Older
# -*- coding: utf-8 -*-
"""
/***************************************************************************

Benjamin Jakimow
committed
EO Time Series Viewer
-------------------
begin : 2017-08-04
git sha : $Format:%H$
copyright : (C) 2017 by HU-Berlin
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. *
* *
***************************************************************************/
"""
# noinspection PyPep8Naming
import os, sys, pickle, datetime, re, collections
from collections import OrderedDict
from qgis.gui import *
from qgis.PyQt.QtCore import *
from qgis.PyQt.QtGui import *
from qgis.PyQt.QtWidgets import *
import numpy as np
from osgeo import ogr, osr, gdal
from .externals import pyqtgraph as pg
from .externals.pyqtgraph import functions as fn, AxisItem, ScatterPlotItem, SpotItem, GraphicsScene
from .externals.qps.plotstyling.plotstyling import PlotStyle
from .timeseries import TimeSeries, TimeSeriesDate, SensorInstrument, TimeSeriesSource
from .pixelloader import PixelLoader, PixelLoaderTask
from .utils import *
from .externals.qps.speclib.spectrallibraries import createQgsField
LABEL_EXPRESSION_2D = 'DN or Index'
LABEL_TIME = 'Date'
DEBUG = False
DEFAULT_CRS = QgsCoordinateReferenceSystem('EPSG:4326')

Benjamin Jakimow
committed
FN_ID = 'fid'

Benjamin Jakimow
committed
FN_DOY = 'DOY'
FN_DTG = 'DTG'
FN_IS_NODATA ='is_nodata'
FN_GEO_X = 'geo_x'
FN_GEO_Y = 'geo_y'
FN_PX_X = 'px_x'
FN_PX_Y = 'px_y'
#FN_N_TOTAL = 'n'
#FN_N_NODATA = 'no_data'
#FN_N_LOADED = 'loaded'
#FN_N_LOADED_PERCENT = 'percent'
regBandKey = re.compile(r"(?<!\w)b\d+(?!\w)", re.IGNORECASE)
regBandKeyExact = re.compile(r'^' + regBandKey.pattern + '$', re.IGNORECASE)
def temporalProfileFeatureFields(sensor: SensorInstrument, singleBandOnly=False) -> QgsFields:
"""
Returns the fields of a single temporal profile
:return:
"""
assert isinstance(sensor, SensorInstrument)
fields = QgsFields()
fields.append(createQgsField(FN_DTG, '2011-09-12', comment='Date-time-group'))
fields.append(createQgsField(FN_DOY, 42, comment='Day-of-year'))
fields.append(createQgsField(FN_GEO_X, 12.1233, comment='geo-coordinate x/east value'))
fields.append(createQgsField(FN_GEO_Y, 12.1233, comment='geo-coordinate y/north value'))
fields.append(createQgsField(FN_PX_X, 42, comment='pixel-coordinate x index'))
fields.append(createQgsField(FN_PX_Y, 24, comment='pixel-coordinate y index'))
for b in range(sensor.nb):
bandKey = bandIndex2bandKey(b)
fields.append(createQgsField(bandKey, 1.0, comment='value band {}'.format(b+1)))
return fields
def sensorExampleQgsFeature(sensor:SensorInstrument, singleBandOnly=False)->QgsFeature:
"""
Returns an exemplary QgsFeature with value for a specific sensor
:param sensor: SensorInstrument
:param singleBandOnly:
:return:
"""
# populate with exemplary band values (generally stored as floats)
fields = temporalProfileFeatureFields(sensor)
f = QgsFeature(fields)
pt = QgsPointXY(12.34567, 12.34567)
f.setGeometry(QgsGeometry.fromPointXY(pt))
f.setAttribute(FN_GEO_X, pt.x())
f.setAttribute(FN_GEO_Y, pt.y())
f.setAttribute(FN_PX_X, 1)
f.setAttribute(FN_PX_Y, 1)
dtg = datetime.date.today()
doy = dateDOY(dtg)
f.setAttribute(FN_DTG, str(dtg))
f.setAttribute(FN_DOY, doy)
for b in range(sensor.nb):
bandKey = bandIndex2bandKey(b)
f.setAttribute(bandKey, 1.0)
def dateDOY(date):
if isinstance(date, np.datetime64):
date = date.astype(datetime.date)
return date.timetuple().tm_yday
def daysPerYear(year):
if isinstance(year, np.datetime64):
year = year.astype(datetime.date)
if isinstance(year, datetime.date):
year = year.timetuple().tm_year
return dateDOY(datetime.date(year=year, month=12, day=31))
def date2num(d):
#kindly taken from https://stackoverflow.com/questions/6451655/python-how-to-convert-datetime-dates-to-decimal-years
if isinstance(d, np.datetime64):
d = d.astype(datetime.datetime)
if isinstance(d, QDate):
d = datetime.date(d.year(), d.month(), d.day())
assert isinstance(d, datetime.date)
yearDuration = daysPerYear(d)
yearElapsed = d.timetuple().tm_yday
fraction = float(yearElapsed) / float(yearDuration)
if fraction == 1.0:
fraction = 0.9999999
return float(d.year) + fraction
def num2date(n, dt64=True, qDate=False):
n = float(n)
if n < 1:
n += 1
year = int(n)
fraction = n - year
yearDuration = daysPerYear(year)
yearElapsed = fraction * yearDuration
import math
doy = round(yearElapsed)
if doy < 1:
doy = 1
try:
date = datetime.date(year, 1, 1) + datetime.timedelta(days=doy-1)
except:
s = ""
if qDate:
return QDate(date.year, date.month, date.day)
if dt64:
return np.datetime64(date)
else:
return date
#return np.datetime64('{:04}-01-01'.format(year), 'D') + np.timedelta64(int(yearElapsed), 'D')
assert i >= 0
return 'b{}'.format(i + 1)
match = regBandKeyExact.search(key)
assert match
idx = int(match.group()[1:]) - 1
return idx
class DateTimePlotWidget(pg.PlotWidget):
"""
Subclass of PlotWidget
"""
def __init__(self, parent=None):
"""
Constructor of the widget
"""
super(DateTimePlotWidget, self).__init__(parent)
self.plotItem = pg.PlotItem(
axisItems={'bottom':DateTimeAxis(orientation='bottom')}
,viewBox=DateTimeViewBox()
)
self.setCentralItem(self.plotItem)
#self.xAxisInitialized = False
pi = self.getPlotItem()
pi.getAxis('bottom').setLabel(LABEL_TIME)
pi.getAxis('left').setLabel(LABEL_EXPRESSION_2D)
self.mInfoColor = QColor('yellow')
self.mCrosshairLineV = pg.InfiniteLine(angle=90, movable=False)
self.mCrosshairLineH = pg.InfiniteLine(angle=0, movable=False)
self.mInfoLabelCursor = pg.TextItem(text='<cursor position>', anchor=(1.0, 0.0))
self.mInfoLabelCursor.setColor(QColor('yellow'))
self.scene().addItem(self.mInfoLabelCursor)
self.mInfoLabelCursor.setParentItem(self.getPlotItem())
#self.plot2DLabel.setAnchor()
#self.plot2DLabel.anchor(itemPos=(0, 0), parentPos=(0, 0), offset=(0, 0))
pi.addItem(self.mCrosshairLineV, ignoreBounds=True)
pi.addItem(self.mCrosshairLineH, ignoreBounds=True)
self.proxy2D = pg.SignalProxy(self.scene().sigMouseMoved, rateLimit=60, slot=self.onMouseMoved2D)
def resetViewBox(self):
self.plotItem.getViewBox().autoRange()
def onMouseMoved2D(self, evt):
pos = evt[0] ## using signal proxy turns original arguments into a tuple
plotItem = self.getPlotItem()
if plotItem.sceneBoundingRect().contains(pos):

benjamin.jakimow@geo.hu-berlin.de
committed
vb = plotItem.vb
assert isinstance(vb, DateTimeViewBox)
mousePoint = vb.mapSceneToView(pos)
x = mousePoint.x()
if x >= 0:
y = mousePoint.y()
date = num2date(x)
doy = dateDOY(date)
plotItem.vb.updateCurrentDate(num2date(x, dt64=True))
self.mInfoLabelCursor.setText('DN {:0.2f}\nDate {}\nDOY {}'.format(
mousePoint.y(), date, doy),
color=self.mInfoColor)
s = self.size()
pos = QPointF(s.width(), 0)

benjamin.jakimow@geo.hu-berlin.de
committed
self.mInfoLabelCursor.setVisible(vb.mActionShowCursorValues.isChecked())
self.mInfoLabelCursor.setPos(pos)

benjamin.jakimow@geo.hu-berlin.de
committed
b = vb.mActionShowCrosshair.isChecked()
self.mCrosshairLineH.setVisible(b)
self.mCrosshairLineV.setVisible(b)
self.mCrosshairLineH.pen.setColor(self.mInfoColor)
self.mCrosshairLineV.pen.setColor(self.mInfoColor)
self.mCrosshairLineV.setPos(mousePoint.x())
self.mCrosshairLineH.setPos(mousePoint.y())
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
class DateTimeAxis(pg.AxisItem):
def __init__(self, *args, **kwds):
super(DateTimeAxis, self).__init__(*args, **kwds)
self.setRange(1,3000)
self.enableAutoSIPrefix(False)
self.labelAngle = 0
def logTickStrings(self, values, scale, spacing):
s = ""
def tickStrings(self, values, scale, spacing):
strns = []
if len(values) == 0:
return []
#assert isinstance(values[0],
values = [num2date(v) if v > 0 else num2date(1) for v in values]
rng = max(values)-min(values)
ndays = rng.astype(int)
strns = []
for v in values:
if ndays == 0:
strns.append(v.astype(str))
else:
strns.append(v.astype(str))
return strns
def tickValues(self, minVal, maxVal, size):
d = super(DateTimeAxis, self).tickValues(minVal, maxVal, size)
return d
def drawPicture(self, p, axisSpec, tickSpecs, textSpecs):
p.setRenderHint(p.Antialiasing, False)
p.setRenderHint(p.TextAntialiasing, True)
## draw long line along axis
pen, p1, p2 = axisSpec
p.setPen(pen)
p.drawLine(p1, p2)
p.translate(0.5, 0) ## resolves some damn pixel ambiguity
## draw ticks
for pen, p1, p2 in tickSpecs:
p.setPen(pen)
p.drawLine(p1, p2)
## Draw all text
if self.tickFont is not None:
p.setFont(self.tickFont)
p.setPen(self.pen())
#for rect, flags, text in textSpecs:
# p.drawText(rect, flags, text)
# # p.drawRect(rect)
#see https://github.com/pyqtgraph/pyqtgraph/issues/322
for rect, flags, text in textSpecs:
p.save() # save the painter state
p.translate(rect.center()) # move coordinate system to center of text rect
p.rotate(self.labelAngle) # rotate text
p.translate(-rect.center()) # revert coordinate system
p.drawText(rect, flags, text)
p.restore() # restore the painter state
class DateTimeViewBox(pg.ViewBox):
"""
Subclass of ViewBox
"""
sigMoveToDate = pyqtSignal(np.datetime64)
def __init__(self, parent=None):
"""
Constructor of the CustomViewBox
"""
super(DateTimeViewBox, self).__init__(parent)
#self.menu = None # Override pyqtgraph ViewBoxMenu
#self.menu = self.getMenu() # Create the menu
#self.menu = None
self.mCurrentDate = np.datetime64('today')
self.mXAxisUnit = 'date'
xAction = [a for a in self.menu.actions() if a.text() == 'X Axis'][0]
yAction = [a for a in self.menu.actions() if a.text() == 'Y Axis'][0]
menuXAxis = self.menu.addMenu('X Axis')
#define the widget to set X-Axis options
frame = QFrame()
l = QGridLayout()
frame.setLayout(l)
#l.addWidget(self, QWidget, int, int, alignment: Qt.Alignment = 0): not enough arguments
self.rbXManualRange = QRadioButton('Manual')
self.dateEditX0 = QDateEdit()
self.dateEditX0.setDisplayFormat('yyyy-MM-dd')
self.dateEditX0.setToolTip('Start time')
self.dateEditX0.setCalendarPopup(True)
self.dateEditX0.dateChanged.connect(self.updateXRange)
self.dateEditX1 = QDateEdit()
self.dateEditX1.setDisplayFormat('yyyy-MM-dd')
self.dateEditX0.setToolTip('End time')
self.dateEditX1.setCalendarPopup(True)
self.dateEditX1.dateChanged.connect(self.updateXRange)
self.rbXAutoRange = QRadioButton('Auto')
self.rbXAutoRange.setChecked(True)
self.rbXAutoRange.toggled.connect(self.updateXRange)
l.addWidget(self.rbXManualRange, 0,0)
l.addWidget(self.dateEditX0, 0,1)
l.addWidget(self.dateEditX1, 0,2)
l.addWidget(self.rbXAutoRange, 1, 0)
l.setMargin(1)
l.setSpacing(1)
frame.setMinimumSize(l.sizeHint())
wa = QWidgetAction(menuXAxis)
wa.setDefaultWidget(frame)
menuXAxis.addAction(wa)
self.menu.insertMenu(xAction, menuXAxis)
self.menu.removeAction(xAction)
self.mActionMoveToDate = self.menu.addAction('Move to {}'.format(self.mCurrentDate))

Benjamin Jakimow
committed
self.mActionMoveToDate.triggered.connect(lambda *args : self.sigMoveToDate.emit(self.mCurrentDate))
#self.mActionMoveToProfile = self.menu.addAction('Move to profile location')
#self.mActionMoveToProfile.triggered.connect(lambda *args: self.sigM.emit(self.mCurrentDate))

benjamin.jakimow@geo.hu-berlin.de
committed
self.mActionShowCrosshair = self.menu.addAction('Show Crosshair')
self.mActionShowCrosshair.setCheckable(True)
self.mActionShowCrosshair.setChecked(True)
self.mActionShowCursorValues = self.menu.addAction('Show Mouse values')
self.mActionShowCursorValues.setCheckable(True)
self.mActionShowCursorValues.setChecked(True)
sigXAxisUnitChanged = pyqtSignal(str)
def setXAxisUnit(self, unit):
assert unit in ['date', 'doy']
old = self.mXAxisUnit
self.mXAxisUnit = unit
if old != self.mXAxisUnit:
self.sigXAxisUnitChanged.emit(self.mXAxisUnit)
def xAxisUnit(self):
return self.mXAxisUnit
def updateXRange(self, *args):
isAutoRange = self.rbXAutoRange.isChecked()
self.enableAutoRange('x', isAutoRange)
self.dateEditX0.setEnabled(not isAutoRange)
self.dateEditX1.setEnabled(not isAutoRange)
if not isAutoRange:
t0 = date2num(self.dateEditX0.date())
t1 = date2num(self.dateEditX1.date())
t0 = min(t0, t1)
t1 = max(t0, t1)
self.setXRange(t0, t1)
def updateCurrentDate(self, date):
if isinstance(date, np.datetime64):
self.mCurrentDate = date
self.mActionMoveToDate.setData(date)
self.mActionMoveToDate.setText('Move maps to {}'.format(date))
def raiseContextMenu(self, ev):
pt = self.mapDeviceToView(ev.pos())
self.updateCurrentDate(num2date(pt.x(), dt64=True))
plotDataItems = [item for item in self.scene().itemsNearEvent(ev) if isinstance(item, ScatterPlotItem) and isinstance(item.parentItem(), TemporalProfilePlotDataItem)]
xRange, yRange = self.viewRange()
t0 = num2date(xRange[0], qDate=True)
t1 = num2date(xRange[1], qDate=True)
self.dateEditX0.setDate(t0)
self.dateEditX1.setDate(t1)
menu = self.getMenu(ev)
self.scene().addParentContextMenus(self, menu, ev)
menu.exec_(ev.screenPos().toPoint())
class TemporalProfile(QObject):
def __init__(self, layer, fid:int, geometry:QgsGeometry):
super(TemporalProfile, self).__init__()
assert isinstance(layer, TemporalProfileLayer)
assert fid >= 0
self.mID = fid
self.mLayer = layer
self.mTimeSeries = layer.timeSeries()
self.mData = {}
self.mUpdated = False
self.mLoaded = self.mLoadedMax = self.mNoData = 0
assert isinstance(tsd, TimeSeriesDate)
self.updateData(tsd, meta, skipStatusUpdate=True)
def printData(self, sensor:SensorInstrument=None):
"""
Prints the entire temporal profile. For debug purposes.
"""
for tsd in sorted(self.mData.keys()):
assert isinstance(tsd, TimeSeriesDate)
data = self.mData[tsd]
if isinstance(sensor, SensorInstrument) and tsd.sensor() != sensor:
continue
assert isinstance(data, dict)
info = '{}:{}={}'.format(tsd.date(), tsd.sensor().name(), str(data))
print(info)
def __hash__(self):
return hash('{}{}'.format(self.mID, self.mLayer.layerId()))
Two temporal profiles are equal if they have the same feature id and source layer
:param other:
:return:
"""
if not isinstance(other, TemporalProfile):
return False
return other.mID == self.mID and self.mLayer == other.mLayer
def geometry(self, crs:QgsCoordinateReferenceSystem=None)->QgsGeometry:
"""
Returns the geometry
:param crs:
:return: QgsGeometry. usually a QgsPoint
"""
g = self.mLayer.getFeature(self.mID).geometry()
if not isinstance(g, QgsGeometry):
return None
if isinstance(crs, QgsCoordinateReferenceSystem) and crs != self.mLayer.crs():
trans = QgsCoordinateTransform()
trans.setSourceCrs(self.mLayer.crs())
trans.setDestinationCrs(crs)

Benjamin Jakimow
committed
def coordinate(self)->SpatialPoint:
"""
Returns the profile coordinate
:return:
"""
x, y = self.geometry().asPoint()
return SpatialPoint(self.mLayer.crs(), x, y)
"""Feature ID in connected QgsVectorLayer"""
f = self.mLayer.getFeature(self.mID)
return f.attribute(f.fieldNameIndex(key))
b = self.mLayer.isEditable()
self.mLayer.startEditing()
self.mLayer.changeAttributeValue(f.id(), f.fieldNameIndex(key), value)
self.mLayer.saveEdits(leaveEditable=b)
def name(self):
return self.attribute('name')
def setName(self, name:str):
self.setAttribute('name', name)
def data(self):
return self.mData
def timeSeries(self):
return self.mTimeSeries
Loads the missing data for this profile (synchronous eexecution, may take some time).
"""
tasks = []
for tsd in self.mTimeSeries:
assert isinstance(tsd, TimeSeriesDate)
missingIndices = self.missingBandIndices(tsd)
if len(missingIndices) > 0:
for tss in tsd:
assert isinstance(tss, TimeSeriesSource)
if tss.spatialExtent().contains(self.coordinate().toCrs(tss.crs())):
task = TemporalProfileLoaderTask(tss, [self], bandIndices=missingIndices)
tasks.append(task)
results = doLoadTemporalProfileTasks(TaskMock(), pickle.dumps(tasks))
ts = self.timeSeries()
assert isinstance(result, TemporalProfileLoaderTask)
tsd = ts.getTSD(result.mSourcePath)
assert isinstance(tsd, TimeSeriesDate)
for tpId, data in result.mRESULTS.items():
if tpId == self.id():
self.updateData(tsd, data)
def missingBandIndices(self, tsd, requiredIndices=None):
"""
Returns the band indices [0, sensor.nb) that have not been loaded yet.
:param tsd: TimeSeriesDate of interest
:param requiredIndices: optional subset of possible band-indices to return the missing ones from.
:return: [list-of-indices]
"""
assert isinstance(tsd, TimeSeriesDate)
if requiredIndices is None:
requiredIndices = list(range(tsd.mSensor.nb))
requiredIndices = [i for i in requiredIndices if i >= 0 and i < tsd.mSensor.nb]
existingBandIndices = [bandKey2bandIndex(k) for k in self.data(tsd).keys() if regBandKeyExact.search(k)]
if FN_PX_X not in self.data(tsd).keys() and len(requiredIndices) == 0:
requiredIndices.append(0)
return [i for i in requiredIndices if i not in existingBandIndices]
def plot(self):
for sensor in self.mTimeSeries.sensors():
assert isinstance(sensor, SensorInstrument)
plotStyle = TemporalProfile2DPlotStyle(self)
plotStyle.setSensor(sensor)
pi = TemporalProfilePlotDataItem(plotStyle)
pi.setClickable(True)
pw = pg.plot(title=self.name())
pi.setColor('green')
pg.QAPP.exec_()
def updateData(self, tsd, values, skipStatusUpdate=False):
assert isinstance(tsd, TimeSeriesDate)
assert isinstance(values, dict)
if tsd not in self.mData.keys():
self.mData[tsd] = dict()
values2 = self.mData.get(tsd)
assert isinstance(values2, dict)
for k, v in values.items():
if v in [None, np.NaN] and k not in values2.keys():
values2[k] = v
else:
values2[k] = v
#self.mData[tsd].update(values)
def resetUpdatedFlag(self):
self.mUpdated = False
def updated(self):
return self.mUpdated
def dataFromExpression(self, sensor:SensorInstrument, expression:str, dateType='date'):
x = []
y = []
if not isinstance(expression, QgsExpression):
expression = QgsExpression(expression)
assert isinstance(expression, QgsExpression)
expression = QgsExpression(expression)
sensorTSDs = sorted([tsd for tsd in self.mData.keys() if tsd.sensor() == sensor])
# define required QgsFields
fields = temporalProfileFeatureFields(sensor)
geo_x = self.geometry().centroid().get().x()
geo_y = self.geometry().centroid().get().y()
for i, tsd in enumerate(sensorTSDs):
assert isinstance(tsd, TimeSeriesDate)
data = self.mData[tsd]
if dateType == 'date':
xValue = date2num(tsd.mDate)
elif dateType == 'doy':
xValue = tsd.mDOY
context = QgsExpressionContext()
context.setFields(fields)
# set static properties (same for all TSDs)
f.setGeometry(QgsGeometry(self.geometry()))
f.setAttribute(FN_GEO_X, geo_x)
f.setAttribute(FN_GEO_Y, geo_y)
# set TSD specific properties
f.setAttribute(FN_DOY, tsd.doy())
f.setAttribute(FN_DTG, str(tsd.date()))
for fn in fields.names():
if fn in data.keys():
setQgsFieldValue(f, fn, data[fn])

Benjamin Jakimow
committed
yValue = expression.evaluate(context)
if yValue in [None, QVariant()]:
yValue = np.NaN

Benjamin Jakimow
committed
y.append(yValue)
x.append(xValue)
#return np.asarray(x), np.asarray(y)
assert len(x) == len(y)
return x, y
def data(self, tsd):
assert isinstance(tsd, TimeSeriesDate)
if self.hasData(tsd):
return self.mData[tsd]
else:
return {}
def loadingStatus(self):
"""
Returns the loading status in terms of single pixel values.
nLoaded = sum of single band values
nLoadedMax = potential maximum of band values that might be loaded
:return: (nLoaded, nLoadedMax)
"""
return self.mLoaded, self.mNoData, self.mLoadedMax
#def updateLoadingStatus(self):
# """
# Calculates the loading status in terms of single pixel values.
# nMax is the sum of all bands over each TimeSeriesDate and Sensors
for tsd in self.mTimeSeries:
assert isinstance(tsd, TimeSeriesDate)
if self.hasData(tsd):
if self.isNoData(tsd):
self.mNoData += nb
else:
self.mLoaded += len([k for k in self.mData[tsd].keys() if regBandKey.search(k)])
f = self.mLayer.getFeature(self.id())
b = self.mLayer.isEditable()
self.mLayer.startEditing()
# self.mLayer.changeAttributeValue(f.id(), f.fieldNameIndex(FN_N_NODATA), self.mNoData)
# self.mLayer.changeAttributeValue(f.id(), f.fieldNameIndex(FN_N_TOTAL), self.mLoadedMax)
# self.mLayer.changeAttributeValue(f.id(), f.fieldNameIndex(FN_N_LOADED), self.mLoaded)
# if self.mLoadedMax > 0:
# self.mLayer.changeAttributeValue(f.id(), f.fieldNameIndex(FN_N_LOADED_PERCENT), round(100. * float(self.mLoaded + self.mNoData) / self.mLoadedMax, 2))
self.mLayer.saveEdits(leaveEditable=b)
s = ""
assert isinstance(tsd, TimeSeriesDate)
assert isinstance(tsd, TimeSeriesDate)
return tsd in self.mData.keys()
def __repr__(self):

Benjamin Jakimow
committed
return 'TemporalProfile {} "{}"'.format(self.id(), self.name())
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
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
class TemporalProfilePlotStyleBase(PlotStyle):
sigStyleUpdated = pyqtSignal()
sigDataUpdated = pyqtSignal()
sigExpressionUpdated = pyqtSignal()
sigSensorChanged = pyqtSignal(SensorInstrument)
def __init__(self, parent=None, temporalProfile=None):
super(TemporalProfilePlotStyleBase, self).__init__()
self.mSensor = None
self.mTP = None
self.mExpression = 'b1'
self.mPlotItems = []
self.mIsVisible = True
self.mShowLastLocation = True
if isinstance(temporalProfile, TemporalProfile):
self.setTemporalProfile(temporalProfile)
def showLastLocation(self)->bool:
"""
"""
return self.mShowLastLocation
def isPlotable(self):
return self.isVisible() and isinstance(self.temporalProfile(), TemporalProfile) and isinstance(self.sensor(), SensorInstrument)
def createPlotItem(self):
raise NotImplementedError()
def temporalProfile(self):
return self.mTP
def setTemporalProfile(self, temporalPofile):
b = temporalPofile != self.mTP
self.mTP = temporalPofile
if temporalPofile in [None, QVariant()]:
s =""
else:
assert isinstance(temporalPofile, TemporalProfile)
if b:
self.updateDataProperties()
def setSensor(self, sensor):
assert sensor is None or isinstance(sensor, SensorInstrument)
b = sensor != self.mSensor
self.mSensor = sensor
if b:
self.update()
self.sigSensorChanged.emit(sensor)
def sensor(self):
return self.mSensor
def updateStyleProperties(self):
raise NotImplementedError()
def updateDataProperties(self):
raise NotImplementedError()
def update(self):
self.updateDataProperties()
def setExpression(self, exp):
assert isinstance(exp, str)
b = self.mExpression != exp
self.mExpression = exp
self.updateDataProperties()
if b:
self
self.sigExpressionUpdated.emit()
def expression(self):
return self.mExpression
def __reduce_ex__(self, protocol):
return self.__class__, (), self.__getstate__()
def __getstate__(self):
result = super(TemporalProfile2DPlotStyle, self).__getstate__()
#remove
del result['mTP']
del result['mSensor']
return result
def isVisible(self):
return self.mIsVisible
def setVisibility(self, b):
assert isinstance(b, bool)
old = self.isVisible()
self.mIsVisible = b
if b != old:
self.updateStyleProperties()
#self.update()
def copyFrom(self, plotStyle):
if isinstance(plotStyle, PlotStyle):
super(TemporalProfilePlotStyleBase, self).copyFrom(plotStyle)
self.updateStyleProperties()
if isinstance(plotStyle, TemporalProfilePlotStyleBase):
self.setExpression(plotStyle.expression())
self.setSensor(plotStyle.sensor())
self.setTemporalProfile(plotStyle.temporalProfile())
self.updateDataProperties()
class TemporalProfile2DPlotStyle(TemporalProfilePlotStyleBase):
def __init__(self, temporalProfile=None):
super(TemporalProfile2DPlotStyle, self).__init__(temporalProfile=temporalProfile)
#PlotStyle.__init__(self)
#TemporalProfilePlotStyleBase.__init__(self, temporalProfile=temporalProfile)
def createPlotItem(self, plotWidget):
pdi = TemporalProfilePlotDataItem(self)
self.mPlotItems.append(pdi)
return pdi
def updateStyleProperties(self):
for pdi in self.mPlotItems:
assert isinstance(pdi, TemporalProfilePlotDataItem)
pdi.updateStyle()
def updateDataProperties(self):
for pdi in self.mPlotItems:
assert isinstance(pdi, TemporalProfilePlotDataItem)
pdi.updateDataAndStyle()
class TemporalProfileLoaderTask(object):
"""
An object to loading temporal profile values from a *single* raster source.
"""
def __init__(self, tss:TimeSeriesSource, temporalProfiles: list, bandIndices=None):
assert isinstance(temporalProfiles, list)
self.mId = ''
self.mTSS = tss
# assert isinstance(source, str) or isinstance(source, unicode)
self.mSourcePath = tss.uri()
self.mGeometries = {}
self.mRESULTS = {}
self.mERRORS = []
for tp in temporalProfiles:
assert isinstance(tp, TemporalProfile)
# save geometry as WKT to be pickable
self.mGeometries[tp.id()] = tp.geometry(tss.crs()).asWkt()
if bandIndices is None:
bandIndices = list(range(tss.nb))
self.mBandIndices = bandIndices
def doLoadTemporalProfileTasks(qgsTask:QgsTask, dump):
assert isinstance(qgsTask, QgsTask)
tasks = pickle.loads(dump)
assert isinstance(tasks, list)
n = len(tasks)
qgsTask.setProgress(0)
results = []
for i, task in enumerate(tasks):
assert isinstance(task, TemporalProfileLoaderTask)
try:
ds = gdal.Open(task.mSourcePath)
assert isinstance(ds, gdal.Dataset)
nb, ns, nl = ds.RasterCount, ds.RasterXSize, ds.RasterYSize
gt = ds.GetGeoTransform()
pxIndices = {}
# calculate pixel indices to load
for tpId, wkt in task.mGeometries.items():
geom = QgsGeometry.fromWkt(wkt)
assert isinstance(geom, QgsGeometry)
pt = geom.centroid().asPoint()
px = geo2px(pt, gt)
if px.x() < 0 or px.x() > ns or px.y() < 0 or px.y() > nl:
task.mERRORS.append('TemporalProfile {} is out of image bounds: {} = pixel {}'.format(tpId, geom, px))
continue
task.mRESULTS[tpId] = {'px_x':px.x(),
'px_y':px.y(),
'geo_x':pt.x(),
'geo_y':px.y(),
'gt':gt}
pxIndices[tpId] = px
# todo: implement load balancing
for j, bandIndex in enumerate([b for b in task.mBandIndices if b >= 0 and b < nb]):
band = ds.GetRasterBand(bandIndex + 1)
assert isinstance(band, gdal.Band)
no_data = band.GetNoDataValue()
bandName = 'b{}'.format(bandIndex + 1)
for tpId, px in pxIndices.items():
assert isinstance(px, QPoint)
value = band.ReadAsArray(px.x(), px.y(), 1, 1).flatten()[0]
if no_data and value == no_data:
value = np.NaN
task.mRESULTS[tpId][bandName] = value
except Exception as ex:
task.mERRORS.append('Error source image {}:\n{}'.format(task.mSourcePath, ex))
results.append(task)
qgsTask.setProgress(100 * (i + 1) / n)
return pickle.dumps(results)
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
class TemporalProfilePlotDataItem(pg.PlotDataItem):
def __init__(self, plotStyle, parent=None):
assert isinstance(plotStyle, TemporalProfile2DPlotStyle)
super(TemporalProfilePlotDataItem, self).__init__([], [], parent=parent)
self.menu = None
#self.setFlags(QGraphicsItem.ItemIsSelectable)
self.mPlotStyle = plotStyle
self.setAcceptedMouseButtons(Qt.LeftButton | Qt.RightButton)
self.mPlotStyle.sigUpdated.connect(self.updateDataAndStyle)
self.updateStyle()
# On right-click, raise the context menu
def mouseClickEvent(self, ev):
if ev.button() == QtCore.Qt.RightButton:
if self.raiseContextMenu(ev):
ev.accept()
def raiseContextMenu(self, ev):
menu = self.getContextMenus()
# Let the scene add on to the end of our context menu
# (this is optional)
menu = self.scene().addParentContextMenus(self, menu, ev)
pos = ev.screenPos()
menu.popup(QtCore.QPoint(pos.x(), pos.y()))
return True
# This method will be called when this item's _children_ want to raise
# a context menu that includes their parents' menus.
def getContextMenus(self, event=None):
if self.menu is None:
self.menu = QMenu()
self.menu.setTitle(self.name + " options..")
green = QAction("Turn green", self.menu)
green.triggered.connect(self.setGreen)
self.menu.addAction(green)
self.menu.green = green
blue = QAction("Turn blue", self.menu)
blue.triggered.connect(self.setBlue)
self.menu.addAction(blue)
self.menu.green = blue
alpha = QWidgetAction(self.menu)
alphaSlider = QSlider()
alphaSlider.setOrientation(QtCore.Qt.Horizontal)
alphaSlider.setMaximum(255)
alphaSlider.setValue(255)
alphaSlider.valueChanged.connect(self.setAlpha)
alpha.setDefaultWidget(alphaSlider)
self.menu.addAction(alpha)
self.menu.alpha = alpha
self.menu.alphaSlider = alphaSlider
return self.menu
def updateDataAndStyle(self):
TP = self.mPlotStyle.temporalProfile()
sensor = self.mPlotStyle.sensor()
if isinstance(TP, TemporalProfile) and isinstance(sensor, SensorInstrument):
x, y = TP.dataFromExpression(self.mPlotStyle.sensor(), self.mPlotStyle.expression(), dateType='date')

Benjamin Jakimow
committed
if np.any(np.isfinite(y)):
self.setData(x=x, y=y, connect='finite')
else:
self.setData(x=[], y=[]) # dummy
else:
self.setData(x=[], y=[]) # dummy for empty data
self.updateStyle()

Benjamin Jakimow
committed
def updateStyle(self):
"""
Updates visibility properties
"""
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
self.setVisible(self.mPlotStyle.isVisible())
self.setSymbol(self.mPlotStyle.markerSymbol)
self.setSymbolSize(self.mPlotStyle.markerSize)
self.setSymbolBrush(self.mPlotStyle.markerBrush)
self.setSymbolPen(self.mPlotStyle.markerPen)
self.setPen(self.mPlotStyle.linePen)
self.update()
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 TemporalProfileLayer(QgsVectorLayer):
"""
A collection to store the TemporalProfile data delivered by a PixelLoader
"""
#sigSensorAdded = pyqtSignal(SensorInstrument)
#sigSensorRemoved = pyqtSignal(SensorInstrument)
#sigPixelAdded = pyqtSignal()
#sigPixelRemoved = pyqtSignal()
sigTemporalProfilesAdded = pyqtSignal(list)
sigTemporalProfilesRemoved = pyqtSignal(list)
sigMaxProfilesChanged = pyqtSignal(int)
def __init__(self, timeSeries:TimeSeries, uri=None, name='Temporal Profiles'):
lyrOptions = QgsVectorLayer.LayerOptions(loadDefaultStyle=False, readExtentFromXml=False)
if uri is None:
# create a new, empty backend
# existing_vsi_files = vsiSpeclibs()
existing_vsi_files = []
# todo:
assert isinstance(existing_vsi_files, list)
i = 0
_name = name.replace(' ', '_')
uri = (pathlib.Path(VSI_DIR) / '{}.gpkg'.format(_name)).as_posix()
while not ogr.Open(uri) is None:
uri = (pathlib.Path(VSI_DIR) / '{}{:03}.gpkg'.format(_name, i)).as_posix()
drv = ogr.GetDriverByName('GPKG')
assert isinstance(drv, ogr.Driver)
co = ['VERSION=AUTO']
dsSrc = drv.CreateDataSource(uri, options=co)
assert isinstance(dsSrc, ogr.DataSource)
srs = osr.SpatialReference()
srs.ImportFromEPSG(4326)
co = ['GEOMETRY_NAME=geom',
'GEOMETRY_NULLABLE=YES',

Benjamin Jakimow
committed
'FID={}'.format(FN_ID)
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
]
lyr = dsSrc.CreateLayer(name, srs=srs, geom_type=ogr.wkbPoint, options=co)
assert isinstance(lyr, ogr.Layer)
ldefn = lyr.GetLayerDefn()
assert isinstance(ldefn, ogr.FeatureDefn)
dsSrc.FlushCache()
else:
dsSrc = ogr.Open(uri)
assert isinstance(dsSrc, ogr.DataSource)
names = [dsSrc.GetLayerByIndex(i).GetName() for i in range(dsSrc.GetLayerCount())]
i = names.index(name)
lyr = dsSrc.GetLayer(i)
# consistency check
uri2 = '{}|{}'.format(dsSrc.GetName(), lyr.GetName())
assert QgsVectorLayer(uri2).isValid()
super(TemporalProfileLayer, self).__init__(uri2, name, 'ogr', lyrOptions)
"""
uri = 'Point?crs={}'.format(crs.authid())
lyrOptions = QgsVectorLayer.LayerOptions(loadDefaultStyle=False, readExtentFromXml=False)
super(TemporalProfileLayer, self).__init__(uri, name, 'memory', lyrOptions)
self.mProfiles = OrderedDict()
self.mTimeSeries = timeSeries
#symbol = QgsFillSymbol.createSimple({'style': 'no', 'color': 'red', 'outline_color': 'black'})
#self.mLocations.renderer().setSymbol(symbol)

Benjamin Jakimow
committed
#self.mNextID = 1
self.TS = None
fields = QgsFields()

Benjamin Jakimow
committed
#fields.append(createQgsField(FN_ID, self.mNextID))
fields.append(createQgsField(FN_NAME, ''))
fields.append(createQgsField(FN_X, 0.0, comment='Longitude'))
fields.append(createQgsField(FN_Y, 0.0, comment='Latitude'))
#fields.append(createQgsField(FN_N_TOTAL, 0, comment='Total number of band values'))
#fields.append(createQgsField(FN_N_NODATA,0, comment='Total of no-data values.'))
#fields.append(createQgsField(FN_N_LOADED, 0, comment='Loaded valid band values.'))
#fields.append(createQgsField(FN_N_LOADED_PERCENT,0.0, comment='Loading progress (%)'))
assert self.startEditing()
assert self.dataProvider().addAttributes(fields)
assert self.commitChanges()
self.initConditionalStyles()
self.committedFeaturesAdded.connect(self.onFeaturesAdded)
self.committedFeaturesRemoved.connect(self.onFeaturesRemoved)
def __getitem__(self, slice):
return list(self.mProfiles.values())[slice]
def saveTemporalProfiles(self, pathVector, sep='\t'):
if pathVector is None or len(pathVector) == 0:
global DEFAULT_SAVE_PATH
if DEFAULT_SAVE_PATH == None:
DEFAULT_SAVE_PATH = 'temporalprofiles.shp'
d = os.path.dirname(DEFAULT_SAVE_PATH)
filters = QgsProviderRegistry.instance().fileVectorFilters()
pathVector, filter = QFileDialog.getSaveFileName(None, 'Save {}'.format(self.name()), DEFAULT_SAVE_PATH,
filter=filters)
if len(pathVector) == 0:
return None
else:
DEFAULT_SAVE_PATH = pathVector
drvName = QgsVectorFileWriter.driverForExtension(os.path.splitext(pathVector)[-1])
QgsVectorFileWriter.writeAsVectorFormat(self, pathVector, 'utf-8', destCRS=self.crs(), driverName=drvName)
pathCSV = os.path.splitext(pathVector)[0] + '.data.csv'
# write a flat list of profiles
csvLines.append(sep.join(['id', 'name', 'sensor', 'date', 'doy'] + ['b{}'.format(b+1) for b in range(nBands)]))
for p in list(self.getFeatures()):
assert isinstance(p, QgsFeature)
fid = p.id()
tp = self.mProfiles.get(fid)
if tp is None:
continue
assert isinstance(tp, TemporalProfile)
name = tp.name()
for tsd, values in tp.mData.items():
assert isinstance(tsd, TimeSeriesDate)
line = [fid, name, tsd.mSensor.name(), tsd.mDate, tsd.mDOY]
for b in range(tsd.mSensor.nb):
key = 'b{}'.format(b+1)
line.append(values.get(key))
line = ['' if v == None else str(v) for v in line]
line = sep.join([str(l) for l in line])
# write CSV file
with open(pathCSV, 'w', encoding='utf8') as f:
f.write('\n'.join(csvLines))
def timeSeries(self):
"""
Returns the TimeSeries instance.
:return: TimeSeries
"""
return self.mTimeSeries
def onFeaturesAdded(self, layerID, addedFeatures):
"""
Create a TemporalProfile object for each QgsFeature added to the backend QgsVectorLayer
:param layerID:
:param addedFeatures:
:return:
"""

Benjamin Jakimow
committed
if layerID != self.id():
s = ""
if len(addedFeatures) > 0:
temporalProfiles = []
for feature in addedFeatures:
fid = feature.id()
self.mProfiles[fid] = tp
temporalProfiles.append(tp)
if len(temporalProfiles) > 0:
pass
#self.sigTemporalProfilesAdded.emit(temporalProfiles)
def onFeaturesRemoved(self, layerID, removedFIDs):

Benjamin Jakimow
committed
if layerID != self.id():
s = ""
if len(removedFIDs) > 0:

Benjamin Jakimow
committed
removed = []
for fid in removedFIDs:

Benjamin Jakimow
committed
removed.append(self.mProfiles.pop(fid))

Benjamin Jakimow
committed
self.sigTemporalProfilesRemoved.emit(removed)
def initConditionalStyles(self):
styles = self.conditionalStyles()
assert isinstance(styles, QgsConditionalLayerStyles)
for fieldName in self.fields().names():
red = QgsConditionalStyle("@value is NULL")
red.setTextColor(QColor('red'))
styles.setFieldStyles(fieldName, [red])
#styles.setRowStyles([red])

Benjamin Jakimow
committed
def createTemporalProfiles(self, coordinates, names:list=None)->list:
"""
Creates temporal profiles
:param coordinates:
:return:
"""

Benjamin Jakimow
committed
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
if isinstance(coordinates, QgsVectorLayer):
lyr = coordinates
coordinates = []
names = []
trans = QgsCoordinateTransform()
trans.setSourceCrs(lyr.crs())
trans.setDestinationCrs(self.crs())
nameField = None
if isinstance(names, str) and names in lyr.fields().names():
nameField = names
else:
for name in lyr.fields().names():
if re.search('names?', name, re.I):
nameField = name
break
if nameField is None:
nameField = lyr.fields().names()[0]
for f in lyr.getFeatures():
assert isinstance(f, QgsFeature)
g = f.geometry()
if g.isEmpty():
continue
g = g.centroid()
assert g.transform(trans) == 0
coordinates.append(SpatialPoint(self.crs(), g.asPoint()))
names.append(f.attribute(nameField))
del trans
elif not isinstance(coordinates, list):
coordinates = [coordinates]

Benjamin Jakimow
committed
assert isinstance(coordinates, list)
if not isinstance(names, list):
n = self.featureCount()
names = []
for i in range(len(coordinates)):
names.append('Profile {}'.format(n+i+1))
assert len(coordinates) == len(names)
features = []

Benjamin Jakimow
committed
for i, (coordinate, name) in enumerate(zip(coordinates, names)):
assert isinstance(coordinate, SpatialPoint)
f = QgsFeature(self.fields())
f.setGeometry(QgsGeometry.fromPointXY(coordinate.toCrs(self.crs())))

Benjamin Jakimow
committed
#f.setAttribute(FN_ID, self.mNextID)
f.setAttribute(FN_NAME, name)
f.setAttribute(FN_X, coordinate.x())
f.setAttribute(FN_Y, coordinate.y())
#f.setAttribute(FN_N_LOADED_PERCENT, 0.0)
#f.setAttribute(FN_N_LOADED, 0)
#f.setAttribute(FN_N_TOTAL, 0)
#f.setAttribute(FN_N_NODATA, 0)

Benjamin Jakimow
committed
#self.mNextID += 1
features.append(f)

Benjamin Jakimow
committed
if len(features) == 0:
return []
b = self.isEditable()

Benjamin Jakimow
committed
newFeatures = []
def onFeaturesAdded(lid, fids):
newFeatures.extend(fids)
self.committedFeaturesAdded.connect(onFeaturesAdded)
self.beginEditCommand('Add {} profile locations'.format(len(features)))

Benjamin Jakimow
committed
self.endEditCommand()
self.saveEdits(leaveEditable=b)

Benjamin Jakimow
committed
self.committedFeaturesAdded.disconnect(onFeaturesAdded)

Benjamin Jakimow
committed
assert self.featureCount() == len(self.mProfiles)
profiles = [self.mProfiles[f.id()] for f in newFeatures]
return profiles
def saveEdits(self, leaveEditable=False, triggerRepaint=True):
"""
function to save layer changes-
:param layer:
:param leaveEditable:
:param triggerRepaint:
"""
if not self.isEditable():
return
if not self.commitChanges():
self.commitErrors()
if leaveEditable:
self.startEditing()
if triggerRepaint:
self.triggerRepaint()
def addMissingFields(self, fields):
missingFields = []
for field in fields:
assert isinstance(field, QgsField)
i = self.dataProvider().fieldNameIndex(field.name())
if i == -1:
missingFields.append(field)
if len(missingFields) > 0:
b = self.isEditable()
self.startEditing()
self.dataProvider().addAttributes(missingFields)
self.saveEdits(leaveEditable=b)
def __len__(self):
def __iter__(self):
r = QgsFeatureRequest()
for f in self.getFeatures(r):
yield self.mProfiles[f.id()]
def __contains__(self, item):
return item in self.mProfiles.values()
def temporalProfileToLocationFeature(self, tp:TemporalProfile):
self.mLocations.selectByIds([tp.id()])
for f in self.mLocations.selectedFeatures():
assert isinstance(f, QgsFeature)
return f
return None
def fromSpatialPoint(self, spatialPoint):
""" Tests if a Temporal Profile already exists for the given spatialPoint"""
for p in list(self.mProfiles.values()):
assert isinstance(p, TemporalProfile)
if p.coordinate() == spatialPoint:
return p
return None
def removeTemporalProfiles(self, temporalProfiles):
"""
Removes temporal profiles from this collection
:param temporalProfile: TemporalProfile
"""
if isinstance(temporalProfiles, TemporalProfile):
temporalProfiles = [temporalProfiles]
assert isinstance(temporalProfiles, list)
temporalProfiles = [tp for tp in temporalProfiles if isinstance(tp, TemporalProfile) and tp.id() in self.mProfiles.keys()]
if len(temporalProfiles) > 0:
b = self.isEditable()
assert self.startEditing()
fids = [tp.mID for tp in temporalProfiles]
self.deleteFeatures(fids)
self.saveEdits(leaveEditable=b)
self.sigTemporalProfilesRemoved.emit(temporalProfiles)
def loadCoordinatesFromOgr(self, path):
"""Loads the TemporalProfiles for vector geometries in data source 'path' """
if path is None:
filters = QgsProviderRegistry.instance().fileVectorFilters()
defDir = None
if isinstance(DEFAULT_SAVE_PATH, str) and len(DEFAULT_SAVE_PATH) > 0:
defDir = os.path.dirname(DEFAULT_SAVE_PATH)
path, filter = QFileDialog.getOpenFileName(directory=defDir, filter=filters)
if isinstance(path, str) and len(path) > 0:
sourceLyr = QgsVectorLayer(path)
nameAttribute = None
fieldNames = [n.lower() for n in sourceLyr.fields().names()]
for candidate in ['name', 'id']:
if candidate in fieldNames:
nameAttribute = sourceLyr.fields().names()[fieldNames.index(candidate)]
break
if len(self.timeSeries()) == 0:
sourceLyr.selectAll()
else:
extent = self.timeSeries().maxSpatialExtent(sourceLyr.crs())
sourceLyr.selectByRect(extent)
newProfiles = []
for feature in sourceLyr.selectedFeatures():
assert isinstance(feature, QgsFeature)
geom = feature.geometry()
if isinstance(geom, QgsGeometry):
point = geom.centroid().constGet()
try:
TPs = self.createTemporalProfiles(SpatialPoint(sourceLyr.crs(), point))
for TP in TPs:
if nameAttribute:
name = feature.attribute(nameAttribute)
else:
name = 'FID {}'.format(feature.id())
TP.setName(name)
newProfiles.append(TP)
except Exception as ex:
print(ex)
def addPixelLoaderResult(self, d):
assert isinstance(d, PixelLoaderTask)
if d.success():
for fid in d.temporalProfileIDs:
TP = self.mProfiles.get(fid)

benjamin.jakimow@geo.hu-berlin.de
committed
if isinstance(TP, TemporalProfile):
TP.pullDataUpdate(d)
else:

benjamin.jakimow@geo.hu-berlin.de
committed
s = ""
def clear(self):
"""
Removes all temporal profiles
"""
b = self.isEditable()
self.startEditing()
fids = self.allFeatureIds()
self.deleteFeatures(fids)
self.commitChanges()
if b:
self.startEditing()
#todo: remove TS Profiles
#self.mTemporalProfiles.clear()
#self.sensorPxLayers.clear()
pass
class TemporalProfileTableFilterModel(QgsAttributeTableFilterModel):
def __init__(self, sourceModel, parent=None):
dummyCanvas = QgsMapCanvas(parent)
dummyCanvas.setDestinationCrs(DEFAULT_CRS)
dummyCanvas.setExtent(QgsRectangle(-180,-90,180,90))
super(TemporalProfileTableFilterModel, self).__init__(dummyCanvas, sourceModel, parent=parent)
self.mDummyCanvas = dummyCanvas
#self.setSelectedOnTop(True)
class TemporalProfileTableModel(QgsAttributeTableModel):
#sigPlotStyleChanged = pyqtSignal(SpectralProfile)
#sigAttributeRemoved = pyqtSignal(str)
#sigAttributeAdded = pyqtSignal(str)
AUTOGENERATES_COLUMNS = [FN_ID, FN_Y, FN_X]
#FN_N_LOADED, FN_N_TOTAL, FN_N_NODATA,
#FN_N_LOADED_PERCENT
def __init__(self, temporalProfileLayer=None, parent=None):
if temporalProfileLayer is None:
temporalProfileLayer = TemporalProfileLayer()
cache = QgsVectorLayerCache(temporalProfileLayer, 1000)
super(TemporalProfileTableModel, self).__init__(cache, parent)
self.mTemporalProfileLayer = temporalProfileLayer
self.mCache = cache
assert self.mCache.layer() == self.mTemporalProfileLayer
self.loadLayer()
def columnNames(self):
return self.mTemporalProfileLayer.fields().names()
def feature(self, index):
id = self.rowToId(index.row())
f = self.layer().getFeature(id)
return f
def temporalProfile(self, index):
feature = self.feature(index)
return self.mTemporalProfileLayer.temporalProfileFromFeature(feature)
def data(self, index, role=Qt.DisplayRole):
Returns Temporal Profile Layer values
:param index: QModelIndex
:param role: enum Qt.ItemDataRole
:return: value
"""
if role is None or not index.isValid():
return None
result = super(TemporalProfileTableModel, self).data(index, role=role)
return result
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
def setData(self, index, value, role=None):
"""
Sets Temporal Profile Data.
:param index: QModelIndex()
:param value: value to set
:param role: role
:return: True | False
"""
if role is None or not index.isValid():
return False
f = self.feature(index)
result = False
if value == None:
value = QVariant()
cname = self.columnNames()[index.column()]
if role == Qt.EditRole and cname not in TemporalProfileTableModel.AUTOGENERATES_COLUMNS:
i = f.fieldNameIndex(cname)
if f.attribute(i) == value:
return False
b = self.mTemporalProfileLayer.isEditable()
self.mTemporalProfileLayer.startEditing()
self.mTemporalProfileLayer.changeAttributeValue(f.id(), i, value)
self.mTemporalProfileLayer.saveEdits(leaveEditable=b)
result = True
#f = self.layer().getFeature(profile.id())
#i = f.fieldNameIndex(SpectralProfile.STYLE_FIELD)
#self.layer().changeAttributeValue(f.id(), i, value)
#result = super().setData(self.index(index.row(), self.mcnStyle), value, role=Qt.EditRole)
#if not b:
# self.layer().commitChanges()
if result:
self.dataChanged.emit(index, index, [role])
else:
result = super().setData(index, value, role=role)
return result
def headerData(self, section:int, orientation:Qt.Orientation, role:int):
data = super(TemporalProfileTableModel, self).headerData(section, orientation, role)
if role == Qt.ToolTipRole and orientation == Qt.Horizontal:
#add the field comment to column description
field = self.layer().fields().at(section)
assert isinstance(field, QgsField)
comment = field.comment()
if len(comment) > 0:
data = re.sub('</p>$', ' <i>{}</i></p>'.format(comment), data)
return data
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
def supportedDragActions(self):
return Qt.CopyAction | Qt.MoveAction
def supportedDropActions(self):
return Qt.CopyAction | Qt.MoveAction
def supportedDragActions(self):
return Qt.CopyAction
def supportedDropActions(self):
return Qt.CopyAction
def flags(self, index):
if index.isValid():
columnName = self.columnNames()[index.column()]
flags = super(TemporalProfileTableModel, self).flags(index) | Qt.ItemIsSelectable
#if index.column() == 0:
# flags = flags | Qt.ItemIsUserCheckable
if columnName in TemporalProfileTableModel.AUTOGENERATES_COLUMNS:
flags = flags ^ Qt.ItemIsEditable
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
return flags
return None
class TemporalProfileFeatureSelectionManager(QgsIFeatureSelectionManager):
def __init__(self, layer, parent=None):
s =""
super(TemporalProfileFeatureSelectionManager, self).__init__(parent)
assert isinstance(layer, QgsVectorLayer)
self.mLayer = layer
self.mLayer.selectionChanged.connect(self.selectionChanged)
def layer(self):
return self.mLayer
def deselect(self, ids):
if len(ids) > 0:
selected = [id for id in self.selectedFeatureIds() if id not in ids]
self.mLayer.deselect(ids)
self.selectionChanged.emit(selected, ids, True)
def select(self, ids):
self.mLayer.select(ids)
def selectFeatures(self, selection, command):
super(TemporalProfileFeatureSelectionManager, self).selectF
s = ""
def selectedFeatureCount(self):
return self.mLayer.selectedFeatureCount()
def selectedFeatureIds(self):
return self.mLayer.selectedFeatureIds()
def setSelectedFeatures(self, ids):
self.mLayer.selectByIds(ids)
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
class TemporalProfileTableView(QgsAttributeTableView):
def __init__(self, parent=None):
super(TemporalProfileTableView, self).__init__(parent)
#self.setSelectionBehavior(QAbstractItemView.SelectRows)
#self.setSelectionMode(QAbstractItemView.SingleSelection)
self.horizontalHeader().setSectionsMovable(True)
self.willShowContextMenu.connect(self.onWillShowContextMenu)
self.horizontalHeader().setSectionResizeMode(QHeaderView.Interactive)
self.mSelectionManager = None
def setModel(self, filterModel):
super(TemporalProfileTableView, self).setModel(filterModel)
self.mSelectionManager = TemporalProfileFeatureSelectionManager(self.model().layer())
self.setFeatureSelectionManager(self.mSelectionManager)
#self.selectionModel().selectionChanged.connect(self.onSelectionChanged)
self.mContextMenuActions = []
def setContextMenuActions(self, actions:list):
self.mContextMenuActions = actions
#def contextMenuEvent(self, event):
def onWillShowContextMenu(self, menu, index):
assert isinstance(menu, QMenu)
assert isinstance(index, QModelIndex)
featureIDs = self.temporalProfileLayer().selectedFeatureIds()
if len(featureIDs) == 0 and index.isValid():
if isinstance(self.model(), QgsAttributeTableFilterModel):
index = self.model().mapToSource(index)
if index.isValid():
featureIDs.append(self.model().sourceModel().feature(index).id())
elif isinstance(self.model(), QgsAttributeTableFilterModel):
featureIDs.append(self.model().feature(index).id())
for a in self.mContextMenuActions:
menu.addAction(a)
for a in self.actions():
menu.addAction(a)
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
def temporalProfileLayer(self):
return self.model().layer()
def fidsToIndices(self, fids):
"""
Converts feature ids into FilterModel QModelIndices
:param fids: [list-of-int]
:return:
"""
if isinstance(fids, int):
fids = [fids]
assert isinstance(fids, list)
fmodel = self.model()
indices = [fmodel.fidToIndex(id) for id in fids]
return [fmodel.index(idx.row(), 0) for idx in indices]
def onRemoveFIDs(self, fids):
layer = self.temporalProfileLayer()
assert isinstance(layer, TemporalProfileLayer)
b = layer.isEditable()
layer.startEditing()
layer.deleteFeatures(fids)
layer.saveEdits(leaveEditable=b)
def dropEvent(self, event):
assert isinstance(event, QDropEvent)
mimeData = event.mimeData()
if self.model().rowCount() == 0:
index = self.model().createIndex(0,0)
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
index = self.indexAt(event.pos())
#if mimeData.hasFormat(mimedata.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(mimedata.MDF_SPECTRALLIBRARY):
# event.accept()
def dragMoveEvent(self, event):
assert isinstance(event, QDragMoveEvent)
#if event.mimeData().hasFormat(mimedata.MDF_SPECTRALLIBRARY):
# event.accept()
s = ""
def mimeTypes(self):
pass