# -*- coding: utf-8 -*- """ /*************************************************************************** HUB TimeSeriesViewer ------------------- 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 from __future__ import absolute_import import os, sys, pickle, datetime from collections import OrderedDict from qgis.gui import * from qgis.core import * from PyQt4.QtCore import * from PyQt4.QtXml import * from PyQt4.QtGui import * from timeseriesviewer import jp, SETTINGS from timeseriesviewer.timeseries import * from timeseriesviewer.utils import SpatialExtent, SpatialPoint, px2geo from timeseriesviewer.ui.docks import TsvDockWidgetBase, loadUI from timeseriesviewer.plotstyling import PlotStyle, PlotStyleButton from timeseriesviewer.pixelloader import PixelLoader, PixelLoaderTask from timeseriesviewer.sensorvisualization import SensorListModel import pyqtgraph as pg from pyqtgraph import functions as fn from pyqtgraph import AxisItem import datetime from osgeo import gdal, gdal_array import numpy as np DEBUG = False LABEL_DN = 'DN or Index' LABEL_TIME = 'Date' OPENGL_AVAILABLE = False try: import OpenGL OPENGL_AVAILABLE = True from pyqtgraph.opengl import GLViewWidget class ViewWidget3D(GLViewWidget): def paintGL(self, *args, **kwds): GLViewWidget.paintGL(self, *args, **kwds) self.qglColor(Qt.white) self.renderAnnotations() def renderAnnotations(self): self.renderText(0.8, 0.8, 0.8, 'text 3D') self.renderText(5, 10, 'text 2D fixed') """ class TemporalProfileGLLinePlotItem(gl.GLLinePlotItem): def __init__(self, plotStyle, *args, **kwds): assert isinstance(plotStyle, TemporalProfile3DPlotStyle) gl.GLLinePlotItem """ except: if DEBUG: print('unable to import package OpenGL') pass def getTextColorWithContrast(c): assert isinstance(c, QColor) if c.lightness() < 0.5: return QColor('white') else: return QColor('black') def bandIndex2bandKey(i): assert isinstance(i, int) assert i >= 0 return 'b{}'.format(i + 1) def bandKey2bandIndex(key): match = PlotSettingsModel2D.regBandKeyExact.search(key) assert match idx = int(match.group()[1:]) - 1 return idx def selectedModelIndices(tableView): assert isinstance(tableView, QTableView) result = {} sm = tableView.selectionModel() m = tableView.model() if isinstance(sm, QItemSelectionModel) and isinstance(m, QAbstractItemModel): for idx in sm.selectedIndexes(): assert isinstance(idx, QModelIndex) if idx.row() not in result.keys(): result[idx.row()] = idx return result.values() 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 _SensorPoints(pg.PlotDataItem): def __init__(self, *args, **kwds): super(_SensorPoints, self).__init__(*args, **kwds) # menu creation is deferred because it is expensive and often # the user will never see the menu anyway. self.menu = None def boundingRect(self): return super(_SensorPoints,self).boundingRect() def paint(self, p, *args): super(_SensorPoints, self).paint(p, *args) # 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 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()) x = np.asarray(x, dtype=np.float) y = np.asarray(y, dtype=np.float) if len(y) > 0: self.setData(x=x, y=y) else: self.setData(x=[], y=[]) #dummy self.updateStyle() def updateStyle(self): """ Updates visibility properties """ if DEBUG: print('{} updateStyle'.format(self)) from pyqtgraph.graphicsItems.ScatterPlotItem import drawSymbol # path = drawSymbol(p, self.markerSymbol, self.markerSize, self.markerPen, self.markerBrush) # #painter, symbol, size, pen, brush 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() #self.setPen(fn.mkPen(self.mPlotStyle.linePen)) #self.setFillBrush(fn.mkBrush(self.mPlotStyle.mExpression)) #self.setSymbolBrush(fn.mkBrush(self.mPlotStyle.markerBrush)) # self.setFillBrush(self.mPlotStyle.) #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 PlotSettingsModel2DWidgetDelegate(QStyledItemDelegate): """ """ def __init__(self, tableView, timeSeries, temporalProfileListModel, parent=None): super(PlotSettingsModel2DWidgetDelegate, self).__init__(parent=parent) self._preferedSize = QgsFieldExpressionWidget().sizeHint() self.mTableView = tableView self.mTimeSeries = timeSeries self.mTemporalProfileListModel = temporalProfileListModel self.mSensorLayers = {} def setItemDelegates(self, tableView): assert isinstance(tableView, QTableView) model = tableView.model() assert isinstance(model, PlotSettingsModel2D) for c in [model.cnSensor, model.cnExpression, model.cnStyle, model.cnTemporalProfile]: i = model.columNames.index(c) tableView.setItemDelegateForColumn(i, self) def getColumnName(self, index): assert index.isValid() model = index.model() assert isinstance(model, PlotSettingsModel2D) return model.columNames[index.column()] """ def sizeHint(self, options, index): s = super(ExpressionDelegate, self).sizeHint(options, index) exprString = self.tableView.model().data(index) l = QLabel() l.setText(exprString) x = l.sizeHint().width() + 100 s = QSize(x, s.height()) return self._preferedSize """ def exampleLyr(self, sensor): assert isinstance(sensor, SensorInstrument) if sensor not in self.mSensorLayers.keys(): crs = QgsCoordinateReferenceSystem('EPSG:4862') uri = 'Point?crs={}'.format(crs.authid()) lyr = QgsVectorLayer(uri, 'LOCATIONS', 'memory', False) f = sensorExampleQgsFeature(sensor) assert isinstance(f, QgsFeature) assert lyr.startEditing() for field in f.fields(): lyr.addAttribute(field) lyr.addFeature(f) lyr.commitChanges() self.mSensorLayers[sensor] = lyr return self.mSensorLayers[sensor] def createEditor(self, parent, option, index): cname = self.getColumnName(index) model = self.mTableView.model() w = None if index.isValid() and isinstance(model, PlotSettingsModel2D): plotStyle = model.idx2plotStyle(index) if isinstance(plotStyle, TemporalProfile2DPlotStyle): if cname == model.cnExpression: w = QgsFieldExpressionWidget(parent=parent) w.setExpression(plotStyle.expression()) w.setLayer(self.exampleLyr(plotStyle.sensor())) plotStyle.sigSensorChanged.connect(lambda s : w.setLayer(self.exampleLyr(s))) w.setExpressionDialogTitle('Values') w.setToolTip('Set an expression to specify the image band or calculate a spectral index.') w.fieldChanged[str,bool].connect(lambda n, b : self.checkData(index, w, w.expression())) elif cname == model.cnStyle: w = PlotStyleButton(parent=parent) w.setPlotStyle(plotStyle) w.setToolTip('Set style.') w.sigPlotStyleChanged.connect(lambda: self.checkData(index, w, w.plotStyle())) elif cname == model.cnSensor: w = QComboBox(parent=parent) m = SensorListModel(self.mTimeSeries) w.setModel(m) elif cname == model.cnTemporalProfile: w = QComboBox(parent=parent) w.setModel(self.mTemporalProfileListModel) else: raise NotImplementedError() return w def checkData(self, index, w, expression): assert isinstance(index, QModelIndex) model = self.mTableView.model() if index.isValid() and isinstance(model, PlotSettingsModel2D): plotStyle = model.idx2plotStyle(index) assert isinstance(plotStyle, TemporalProfile2DPlotStyle) if isinstance(w, QgsFieldExpressionWidget): assert expression == w.expression() assert w.isExpressionValid(expression) == w.isValidExpression() if w.isValidExpression(): self.commitData.emit(w) else: s = "" #print(('Delegate commit failed',w.asExpression())) if isinstance(w, PlotStyleButton): self.commitData.emit(w) def setEditorData(self, editor, index): cname = self.getColumnName(index) model = self.mTableView.model() w = None if index.isValid() and isinstance(model, PlotSettingsModel2D): cname = self.getColumnName(index) if cname == model.cnExpression: lastExpr = index.model().data(index, Qt.DisplayRole) assert isinstance(editor, QgsFieldExpressionWidget) editor.setProperty('lastexpr', lastExpr) editor.setField(lastExpr) elif cname == model.cnStyle: style = index.data() assert isinstance(editor, PlotStyleButton) editor.setPlotStyle(style) elif cname == model.cnSensor: assert isinstance(editor, QComboBox) m = editor.model() assert isinstance(m, SensorListModel) sensor = index.data(role=Qt.UserRole) if isinstance(sensor, SensorInstrument): idx = m.sensor2idx(sensor) editor.setCurrentIndex(idx.row()) elif cname == model.cnTemporalProfile: assert isinstance(editor, QComboBox) m = editor.model() assert isinstance(m, TemporalProfileCollectionListModel) TP = index.data(role=Qt.UserRole) if isinstance(TP, TemporalProfile): idx = m.tp2idx(TP) editor.setCurrentIndex(idx) else: raise NotImplementedError() def setModelData(self, w, model, index): cname = self.getColumnName(index) model = self.mTableView.model() if index.isValid() and isinstance(model, PlotSettingsModel2D): if cname == model.cnExpression: assert isinstance(w, QgsFieldExpressionWidget) expr = w.asExpression() exprLast = model.data(index, Qt.DisplayRole) if w.isValidExpression() and expr != exprLast: model.setData(index, w.asExpression(), Qt.EditRole) elif cname == model.cnStyle: assert isinstance(w, PlotStyleButton) model.setData(index, w.plotStyle(), Qt.EditRole) elif cname == model.cnSensor: assert isinstance(w, QComboBox) sensor = w.itemData(w.currentIndex(), role=Qt.UserRole) assert isinstance(sensor, SensorInstrument) model.setData(index, sensor, Qt.EditRole) elif cname == model.cnTemporalProfile: assert isinstance(w, QComboBox) TP = w.itemData(w.currentIndex(), role=Qt.UserRole) assert isinstance(TP, TemporalProfile) model.setData(index, TP, Qt.EditRole) else: raise NotImplementedError() class PlotSettingsModel3DWidgetDelegate(QStyledItemDelegate): """ """ def __init__(self, tableView, parent=None): super(PlotSettingsModel3DWidgetDelegate, self).__init__(parent=parent) self._preferedSize = QgsFieldExpressionWidget().sizeHint() self.mTableView = tableView def setItemDelegates(self, tableView): assert isinstance(tableView, QTableView) model = tableView.model() assert isinstance(model, PlotSettingsModel3D) for c in [model.cnStyle]: i = model.columNames.index(c) tableView.setItemDelegateForColumn(i, self) def getColumnName(self, index): assert index.isValid() model = index.model() assert isinstance(model, PlotSettingsModel3D) return model.columNames[index.column()] """ def sizeHint(self, options, index): s = super(ExpressionDelegate, self).sizeHint(options, index) exprString = self.tableView.model().data(index) l = QLabel() l.setText(exprString) x = l.sizeHint().width() + 100 s = QSize(x, s.height()) return self._preferedSize """ def createEditor(self, parent, option, index): cname = self.getColumnName(index) model = self.mTableView.model() w = None if index.isValid() and isinstance(model, PlotSettingsModel3D): plotStyle = model.idx2plotStyle(index) if isinstance(plotStyle, TemporalProfile3DPlotStyle): if cname == model.cnStyle: w = QgsColorButton(parent=parent) w.setColor(plotStyle.color()) w.setToolTip('Set line color') w.colorChanged.connect(lambda: self.checkData(index, w)) return w def checkData(self, index, w): assert isinstance(index, QModelIndex) model = self.mTableView.model() if index.isValid() and isinstance(model, PlotSettingsModel3D): plotStyle = model.idx2plotStyle(index) assert isinstance(plotStyle, TemporalProfile3DPlotStyle) if isinstance(w, QgsColorButton): self.commitData.emit(w) def setEditorData(self, editor, index): cname = self.getColumnName(index) model = self.mTableView.model() w = None if index.isValid() and isinstance(model, PlotSettingsModel3D): style = model.idx2plotStyle(index) assert isinstance(style, TemporalProfile3DPlotStyle) cname = self.getColumnName(index) if cname == model.cnStyle: assert isinstance(editor, QgsColorButton) editor.setColor(style.color()) else: raise NotImplementedError() def setModelData(self, w, model, index): cname = self.getColumnName(index) model = self.mTableView.model() if index.isValid() and isinstance(model, PlotSettingsModel3D): if cname == model.cnStyle: assert isinstance(w, QgsColorButton) model.setData(index, w.color(), Qt.EditRole) else: raise NotImplementedError() class SensorPixelDataMemoryLayer(QgsVectorLayer): def __init__(self, sensor, crs=None): assert isinstance(sensor, SensorInstrument) if crs is None: crs = QgsCoordinateReferenceSystem('EPSG:4862') uri = 'Point?crs={}'.format(crs.authid()) super(SensorPixelDataMemoryLayer, self).__init__(uri, 'Pixels_sensor_' + sensor.name(), 'memory', False) self.mSensor = sensor #initialize fields assert self.startEditing() # standard field names, types, etc. fieldDefs = [('pxid', QVariant.String, 'integer'), ('date', QVariant.String, 'char'), ('doy', QVariant.Int, 'integer'), ('geo_x', QVariant.Double, 'decimal'), ('geo_y', QVariant.Double, 'decimal'), ('px_x', QVariant.Int, 'integer'), ('px_y', QVariant.Int, 'integer'), ] # one field for each band for b in range(sensor.nb): fName = 'b{}'.format(b + 1) fieldDefs.append((fName, QVariant.Double, 'decimal')) # initialize fields for fieldDef in fieldDefs: field = QgsField(fieldDef[0], fieldDef[1], fieldDef[2]) self.addAttribute(field) self.commitChanges() def sensor(self): return self.mSensor def nPixels(self): raise NotImplementedError() def dates(self): raise NotImplementedError() class TemporalProfileCollectionListModel(QAbstractListModel): def __init__(self, temporalProfileCollection, *args, **kwds): super(TemporalProfileCollectionListModel, self).__init__(*args, **kwds) assert isinstance(temporalProfileCollection, TemporalProfileCollection) self.mTPColl = temporalProfileCollection self.mTPColl.rowsAboutToBeInserted.connect(self.rowsAboutToBeInserted) self.mTPColl.rowsInserted.connect(self.rowsInserted.emit) #self.mTPColl.rowsAboutToBeRemoved.connect(self.rowsAboutToBeRemoved) self.mTPColl.rowsRemoved.connect(lambda : self.modelReset.emit()) def idx2tp(self, *args, **kwds): return self.mTPColl.idx2tp(*args, **kwds) def tp2idx(self, *args, **kwds): return self.mTPColl.tp2idx(*args, **kwds) def flags(self, index): if index.isValid(): flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable return flags #return item.qt_flags(index.column()) return Qt.NoItemFlags def rowCount(self, *args, **kwds): return self.mTPColl.rowCount(*args, **kwds) def data(self, index, role=Qt.DisplayRole): if role is None or not index.isValid(): return None TP = self.mTPColl.idx2tp(index) value = None if isinstance(TP, TemporalProfile): if role == Qt.DisplayRole: value = '{}'.format(TP.name()) elif role == Qt.ToolTipRole: value = '#{} "{}" {}'.format(TP.mID, TP.name(), TP.mCoordinate) elif role == Qt.UserRole: value = TP return value class TemporalProfileCollection(QAbstractTableModel): """ 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, ): super(TemporalProfileCollection, self).__init__() #self.sensorPxLayers = dict() #self.memLyrCrs = QgsCoordinateReferenceSystem('EPSG:4326') self.newDataFlag = False self.mcnID = 'id' self.mcnCoordinate = 'Coordinate' self.mcnLoaded = 'Loading' self.mcnName = 'Name' self.mColumNames = [self.mcnName, self.mcnLoaded, self.mcnCoordinate] crs = QgsCoordinateReferenceSystem('EPSG:4862') uri = 'Point?crs={}'.format(crs.authid()) self.TS = None self.mLocations = QgsVectorLayer(uri, 'LOCATIONS', 'memory', False) self.mTemporalProfiles = [] self.mTPLookupSpatialPoint = {} self.mTPLookupID = {} self.mCurrentTPID = 0 self.mMaxProfiles = 10 self.nextID = 0 def __len__(self): return len(self.mTemporalProfiles) def __iter__(self): return iter(self.mTemporalProfiles) def __getitem__(self, slice): return self.mTemporalProfiles[slice] def __contains__(self, item): return item in self.mTemporalProfiles def rowCount(self, parent=None, *args, **kwargs): return len(self.mTemporalProfiles) def columnCount(self, QModelIndex_parent=None, *args, **kwargs): return len(self.mColumNames) def idx2tp(self, index): if index.isValid() and index.row() < len(self.mTemporalProfiles) : return self.mTemporalProfiles[index.row()] return None def tp2idx(self, temporalProfile): assert isinstance(temporalProfile, TemporalProfile) if temporalProfile in self.mTemporalProfiles: row = self.mTemporalProfiles.index(temporalProfile) return self.createIndex(row, 0) else: return QModelIndex() def data(self, index, role = Qt.DisplayRole): if role is None or not index.isValid(): return None value = None columnName = self.mColumNames[index.column()] TP = self.idx2tp(index) if not isinstance(TP, TemporalProfile): return None #self.mColumNames = ['id','coordinate','loaded'] if role == Qt.DisplayRole: if columnName == self.mcnID: value = TP.mID elif columnName == self.mcnName: value = TP.name() elif columnName == self.mcnCoordinate: value = '{}'.format(TP.mCoordinate) elif columnName == self.mcnLoaded: nIs, nMax = TP.loadingStatus() if nMax > 0: value = '{}/{} ({:0.2f} %)'.format(nIs, nMax, float(nIs) / nMax * 100) elif role == Qt.EditRole: if columnName == self.mcnName: value = TP.name() elif role == Qt.ToolTipRole: if columnName == self.mcnID: value = 'ID Temporal Profile' elif columnName == self.mcnName: value = TP.name() elif columnName == self.mcnCoordinate: value = '{}'.format(TP.mCoordinate) elif columnName == self.mcnLoaded: nIs, nMax = TP.loadingStatus() value = '{}'.format(TP.mCoordinate) elif role == Qt.UserRole: value = TP return value def flags(self, index): if index.isValid(): flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable cName = self.mColumNames[index.column()] if cName == self.mcnName: flags = flags | Qt.ItemIsEditable return flags #return item.qt_flags(index.column()) return None def setData(self, index, value, role=None): if role is None or not index.isValid(): return None cName = self.mColumNames[index.column()] TP = self.idx2tp(index) if isinstance(TP, TemporalProfile): if role == Qt.EditRole and cName == self.mcnName: if len(value) == 0: #do not accept empty strings return False else: TP.setName(value) return True return False def headerData(self, col, orientation, role): if Qt is None: return None if role == Qt.DisplayRole: if orientation == Qt.Horizontal: return self.mColumNames[col] elif orientation == Qt.Vertical: return col return None def insertTemporalProfiles(self, temporalProfiles, i=None): if isinstance(temporalProfiles, TemporalProfile): temporalProfiles = [temporalProfiles] assert isinstance(temporalProfiles, list) for temporalProfile in temporalProfiles: assert isinstance(temporalProfile, TemporalProfile) if i is None: i = len(self.mTemporalProfiles) if len(temporalProfiles) > 0: self.beginInsertRows(QModelIndex(), i, i + len(temporalProfiles) - 1) for temporalProfile in temporalProfiles: assert isinstance(temporalProfile, TemporalProfile) id = self.nextID self.nextID += 1 temporalProfile.mID = id self.mTemporalProfiles.insert(i, temporalProfile) self.mTPLookupID[id] = temporalProfile self.mTPLookupSpatialPoint[temporalProfile.mCoordinate] = temporalProfile i += 1 self.endInsertRows() self.sigTemporalProfilesAdded.emit(temporalProfiles) def temporalProfileFromGeometry(self, geometry): if geometry in self.mTPLookupSpatialPoint.keys(): return self.mTPLookupSpatialPoint[geometry] else: return None def temporalProfileFromID(self, id): if id in self.mTPLookupID.keys(): return self.mTPLookupID[id] else: return None def id(self, temporalProfile): """ Returns the id of an TemporalProfile :param temporalProfile: TemporalProfile :return: id or None, inf temporalProfile is not part of this collections """ for k, tp in self.mTPLookupID.items(): if tp == temporalProfile: return k return None def fromID(self, id): if self.mTPLookupID.has_key(id): return self.mTPLookupID[id] else: return None def fromSpatialPoint(self, spatialPoint): if self.mTPLookupSpatialPoint.has_key(spatialPoint): return self.mTPLookupSpatialPoint[spatialPoint] else: 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 in self.mTemporalProfiles] if len(temporalProfiles) > 0: def deleteFromDict(d, value): assert isinstance(d, dict) if value in d.values(): key = d.keys()[d.values().index(value)] d.pop(key) for temporalProfile in temporalProfiles: assert isinstance(temporalProfile, TemporalProfile) idx = self.tp2idx(temporalProfile) row = idx.row() self.beginRemoveRows(QModelIndex(), row, row) self.mTemporalProfiles.remove(temporalProfile) deleteFromDict(self.mTPLookupID, temporalProfile) deleteFromDict(self.mTPLookupSpatialPoint, temporalProfile) self.endRemoveRows() self.sigTemporalProfilesRemoved.emit(temporalProfiles) def connectTimeSeries(self, timeSeries): self.clear() if isinstance(timeSeries, TimeSeries): self.TS = timeSeries #for sensor in self.TS.Sensors: # self.addSensor(sensor) #self.TS.sigSensorAdded.connect(self.addSensor) #self.TS.sigSensorRemoved.connect(self.removeSensor) else: self.TS = None def setMaxProfiles(self, n): """ Sets the maximum number of temporal profiles to be stored in this container. :param n: number of profiles, must be >= 1 """ old = self.mMaxProfiles assert n >= 1 if old != n: self.mMaxProfiles = n self.prune() self.sigMaxProfilesChanged.emit(self.mMaxProfiles) def prune(self): """ Reduces the number of temporal profile to the value n defined with .setMaxProfiles(n) :return: [list-of-removed-TemporalProfiles] """ def getFieldDefn(self, name, values): if isinstance(values, np.ndarray): # add bands if values.dtype in [np.int8, np.int16, np.int32, np.int64, np.uint8, np.uint16, np.uint32, np.uint64]: fType = QVariant.Int fTypeName = 'integer' elif values.dtype in [np.float16, np.float32, np.float64]: fType = QVariant.Double fTypeName = 'decimal' else: raise NotImplementedError() return QgsField(name, fType, fTypeName) def setFeatureAttribute(self, feature, name, value): assert isinstance(feature, QgsFeature) assert isinstance(name, str) i = feature.fieldNameIndex(name) assert i >= 0, 'Field "{}" does not exist'.format(name) field = feature.fields()[i] if field.isNumeric(): if field.type() == QVariant.Int: value = int(value) elif field.type() == QVariant.Double: value = float(value) else: raise NotImplementedError() feature.setAttribute(i, value) def sort(self, col, order): if self.rowCount() == 0: return self.layoutAboutToBeChanged.emit() colName = self.mColumNames[col] r = order != Qt.AscendingOrder if colName == self.mcnName: self.items.sort(key = lambda TP:TP.name(), reverse=r) elif colName == self.mcnCoordinate: self.items.sort(key=lambda TP: str(TP.mCoordinate), reverse=r) elif colName == self.mcnID: self.items.sort(key=lambda TP: TP.mID, reverse=r) elif colName == self.mcnLoaded: self.items.sort(key=lambda TP: TP.loadingStatus(), reverse=r) self.layoutChanged.emit() def addPixelLoaderResult(self, d): assert isinstance(d, PixelLoaderTask) if d.success(): for i, TPid in enumerate(d.temporalProfileIDs): TP = self.temporalProfileFromID(TPid) tsd = self.TS.getTSD(d.sourcePath) assert isinstance(tsd, TimeSeriesDatum) if isinstance(TP, TemporalProfile): profileData = d.resProfiles[i] if not isinstance(profileData, tuple): s = "" vMean, vStd = profileData values = {} validValues = not isinstance(vMean, str) #1. add the pixel values per returned band for iBand, bandIndex in enumerate(d.bandIndices): key = 'b{}'.format(bandIndex + 1) values[key] = vMean[iBand] if validValues else None key = 'std{}'.format(bandIndex + 1) values[key] = vStd[iBand] if validValues else None #indicesY, indicesX = d.imagePixelIndices() #values['px_x'] = indicesX #values['px_y'] = indicesY TP.updateData(tsd, values) def clear(self): #todo: remove TS Profiles #self.mTemporalProfiles.clear() #self.sensorPxLayers.clear() pass class TemporalProfile2DPlotStyle(PlotStyle): sigExpressionUpdated = pyqtSignal() sigSensorChanged = pyqtSignal(SensorInstrument) def __init__(self, temporalProfile): super(TemporalProfile2DPlotStyle, self).__init__() assert isinstance(temporalProfile, TemporalProfile) self.mSensor = None self.mTP = temporalProfile self.mExpression = u'"b1"' self.mPlotItems = [] if isinstance(temporalProfile, TemporalProfile): self.setTemporalProfile(temporalProfile) def createPlotItem(self, plotWidget): pdi = TemporalProfilePlotDataItem(self) self.mPlotItems.append(pdi) return pdi def temporalProfile(self): return self.mTP def setTemporalProfile(self, temporalPofile): assert isinstance(temporalPofile, TemporalProfile) b = temporalPofile != self.mTP self.mTP = temporalPofile if b: self.update() def setSensor(self, sensor): assert isinstance(sensor, SensorInstrument) b = sensor != self.mSensor self.mSensor = sensor if b: self.update() self.sigSensorChanged.emit(sensor) def update(self): super(TemporalProfile2DPlotStyle, self).update() for pdi in self.mPlotItems: assert isinstance(pdi, TemporalProfilePlotDataItem) pdi.updateStyle() #pdi.updateItems() def sensor(self): return self.mSensor def setExpression(self, exp): assert isinstance(exp, unicode) b = self.mExpression != exp self.mExpression = exp if b: self.update() 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 class TemporalProfile3DPlotStyle(PlotStyle): def __init__(self,sensor): super(TemporalProfile3DPlotStyle, self).__init__() #assert isinstance(temporalProfile, TemporalProfile) assert isinstance(sensor, SensorInstrument) self.mSensor = sensor #self.mTP = temporalProfile self.mScale = 1.0 self.mOffset = 0.0 self.mColor = QColor('green') #self.setTemporalProfile(temporalProfile) def setColor(self, color): assert isinstance(color, QColor) old = self.mColor self.mColor = color if old != color: self.update() def color(self): return self.mColor def setScaling(self, scale, offset): scale = float(scale) offset = float(offset) x,y =self.mScale, self.mOffset self.mScale = scale self.mOffset = offset if x != scale or y != offset: self.update() def sensor(self): return self.mSensor def __reduce_ex__(self, protocol): return self.__class__, (), self.__getstate__() def __getstate__(self): result = super(TemporalProfile3DPlotStyle, self).__getstate__() #remove del result['mSensor'] return result 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 """ def raiseContextMenu(self, ev): pt = self.mapDeviceToView(ev.pos()) print(pt.x(), pt.y()) date = num2date(pt.x()) menu = QMenu(None) a = menu.addAction('Move to {}'.format(date)) a.setData(date) a.triggered.connect(lambda : self.sigMoveToDate.emit(date)) self.scene().addParentContextMenus(self, menu, ev) menu.exec_(ev.screenPos().toPoint()) """ class DateTimePlotWidget(pg.PlotWidget): """ Subclass of PlotWidget """ def __init__(self, parent=None): """ Constructor of the widget """ super(DateTimePlotWidget, self).__init__(parent, viewBox=DateTimeViewBox()) self.plotItem = pg.PlotItem( axisItems={'bottom':DateTimeAxis(orientation='bottom')}, viewBox=DateTimeViewBox() ) self.setCentralItem(self.plotItem) self.xAxisInitialized = False class PlotSettingsModel3D(QAbstractTableModel): #sigSensorAdded = pyqtSignal(SensorPlotSettings) sigVisibilityChanged = pyqtSignal(TemporalProfile2DPlotStyle) sigPlotStylesAdded = pyqtSignal(list) sigPlotStylesRemoved = pyqtSignal(list) def __init__(self, parent=None, *args): #assert isinstance(tableView, QTableView) super(PlotSettingsModel3D, self).__init__(parent=parent) self.mTimeSeries = None self.cnSensor = 'sensor' self.cnScale = 'Scale' self.cnOffset = 'Offset' self.cnStyle = 'style' self.columNames = [self.cnSensor, self.cnScale, self.cnOffset, self.cnStyle] self.mPlotSettings = [] #assert isinstance(plotWidget, DateTimePlotWidget) self.sortColumnIndex = 0 self.sortOrder = Qt.AscendingOrder self.sort(0, Qt.AscendingOrder) def connectTimeSeries(self, timeSeries): if isinstance(timeSeries, TimeSeries): self.mTimeSeries = timeSeries self.mTimeSeries.sigSensorAdded.connect(self.createStyle) self.mTimeSeries.sigSensorRemoved.connect(self.onSensorRemoved) for sensor in self.mTimeSeries.sensors(): self.onSensorAdded(sensor) def hasStyleForSensor(self, sensor): assert isinstance(sensor, SensorInstrument) for plotStyle in self.mPlotSettings: assert isinstance(plotStyle, TemporalProfile3DPlotStyle) if plotStyle.sensor() == sensor: return True return False def createStyle(self, sensor): if not self.hasStyleForSensor(sensor): s = TemporalProfile3DPlotStyle(sensor) self.insertPlotStyles([s]) def onSensorRemoved(self, sensor): assert isinstance(sensor, SensorInstrument) self.removePlotStyles([s for s in self.mPlotSettings if s.sensor() == sensor]) def __len__(self): return len(self.mPlotSettings) def __iter__(self): return iter(self.mPlotSettings) def __getitem__(self, slice): return self.mPlotSettings[slice] def __contains__(self, item): return item in self.mPlotSettings def columnIndex(self, name): return self.columNames.index(name) def insertPlotStyles(self, plotStyles, i=None): """ Inserts PlotStyle :param plotStyles: TemporalProfilePlotStyle | [list-of-TemporalProfilePlotStyle] :param i: index to insert, defaults to the last list position """ if isinstance(plotStyles, TemporalProfile3DPlotStyle): plotStyles = [plotStyles] assert isinstance(plotStyles, list) for plotStyle in plotStyles: assert isinstance(plotStyle, TemporalProfile3DPlotStyle) if i is None: i = len(self.mPlotSettings) if len(plotStyles) > 0: self.beginInsertRows(QModelIndex(), i, i + len(plotStyles)-1) for j, plotStyle in enumerate(plotStyles): assert isinstance(plotStyle, TemporalProfile3DPlotStyle) self.mPlotSettings.insert(i+j, plotStyle) self.endInsertRows() self.sigPlotStylesAdded.emit(plotStyles) def removePlotStyles(self, plotStyles): """ Removes PlotStyle instances :param plotStyles: TemporalProfilePlotStyle | [list-of-TemporalProfilePlotStyle] """ if isinstance(plotStyles, TemporalProfile3DPlotStyle): plotStyles = [plotStyles] assert isinstance(plotStyles, list) if len(plotStyles) > 0: for plotStyle in plotStyles: assert isinstance(plotStyle, TemporalProfile3DPlotStyle) if plotStyle in self.mPlotSettings: idx = self.plotStyle2idx(plotStyle) self.beginRemoveRows(QModelIndex(), idx.row(),idx.row()) self.mPlotSettings.remove(plotStyle) self.endRemoveRows() self.sigPlotStylesRemoved.emit(plotStyles) def sort(self, col, order): if self.rowCount() == 0: return colName = self.columnames[col] r = order != Qt.AscendingOrder #self.beginMoveRows(idxSrc, if colName == self.cnSensor: self.mPlotSettings.sort(key = lambda sv:sv.sensor().name(), reverse=r) def rowCount(self, parent = QModelIndex()): return len(self.mPlotSettings) def removeRows(self, row, count , parent = QModelIndex()): self.beginRemoveRows(parent, row, row + count-1) toRemove = self.mPlotSettings[row:row + count] for tsd in toRemove: self.mPlotSettings.remove(tsd) self.endRemoveRows() def plotStyle2idx(self, plotStyle): assert isinstance(plotStyle, TemporalProfile3DPlotStyle) if plotStyle in self.mPlotSettings: i = self.mPlotSettings.index(plotStyle) return self.createIndex(i, 0) else: return QModelIndex() def idx2plotStyle(self, index): if index.isValid() and index.row() < self.rowCount(): return self.mPlotSettings[index.row()] return None def columnCount(self, parent = QModelIndex()): return len(self.columNames) def data(self, index, role = Qt.DisplayRole): if role is None or not index.isValid(): return None value = None columnName = self.columNames[index.column()] plotStyle = self.idx2plotStyle(index) if isinstance(plotStyle, TemporalProfile3DPlotStyle): sensor = plotStyle.sensor() #print(('data', columnName, role)) if role == Qt.DisplayRole: if columnName == self.cnSensor: if isinstance(sensor, SensorInstrument): value = sensor.name() else: value = '<Select Sensor>' elif columnName == self.cnScale: value = plotStyle.mScale elif columnName == self.cnOffset: value = plotStyle.mOffset if role == Qt.EditRole: if columnName == self.cnScale: value = plotStyle.mScale elif columnName == self.cnOffset: value = plotStyle.mOffset elif role == Qt.CheckStateRole: if columnName == self.cnSensor: value = Qt.Checked if plotStyle.isVisible() else Qt.Unchecked elif role == Qt.UserRole: value = plotStyle if columnName == self.cnSensor: value = plotStyle.sensor() elif columnName == self.cnStyle: value = plotStyle else: value = plotStyle #print(('get data',value)) return value def setData(self, index, value, role=None): if role is None or not index.isValid(): return False #print(('Set data', index.row(), index.column(), value, role)) columnName = self.columNames[index.column()] if value is None: return False result = False plotStyle = self.idx2plotStyle(index) if isinstance(plotStyle, TemporalProfile3DPlotStyle): if role in [Qt.DisplayRole]: if columnName == self.cnScale and isinstance(value, float): plotStyle.setScaling(value, plotStyle.mOffset) result = True elif columnName == self.cnOffset and isinstance(value, float): plotStyle.setScaling(plotStyle.mScale, value) result = True elif columnName == self.cnStyle: if isinstance(value, PlotStyle): plotStyle.copyFrom(value) result = True elif isinstance(value, QColor): plotStyle.setColor(value) result = True if role == Qt.CheckStateRole: if columnName == self.cnSensor: plotStyle.setVisibility(value == Qt.Checked) result = True if role == Qt.EditRole: if columnName == self.cnScale: plotStyle.setScaling(value, plotStyle.mOffset) result = True elif columnName == self.cnOffset: plotStyle.setScaling(plotStyle.mScale, value) result = True elif columnName == self.cnStyle: if isinstance(value, QColor): plotStyle.setColor(value) result = True if isinstance(value, TemporalProfile3DPlotStyle): plotStyle.copyFrom(value) result = True return result def flags(self, index): if index.isValid(): columnName = self.columNames[index.column()] flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable if columnName in [self.cnSensor]: flags = flags | Qt.ItemIsUserCheckable if columnName in [self.cnScale, self.cnOffset, self.cnStyle]: #allow check state flags = flags | Qt.ItemIsEditable return flags #return item.qt_flags(index.column()) return Qt.NoItemFlags def headerData(self, col, orientation, role): if Qt is None: return None if orientation == Qt.Horizontal and role == Qt.DisplayRole: return self.columNames[col] elif orientation == Qt.Vertical and role == Qt.DisplayRole: return col return None class PlotSettingsModel2D(QAbstractTableModel): #sigSensorAdded = pyqtSignal(SensorPlotSettings) sigVisibilityChanged = pyqtSignal(TemporalProfile2DPlotStyle) sigDataChanged = pyqtSignal(TemporalProfile2DPlotStyle) sigPlotStylesAdded = pyqtSignal(list) sigPlotStylesRemoved = pyqtSignal(list) regBandKey = re.compile("(?<!\w)b\d+(?!\w)", re.IGNORECASE) regBandKeyExact = re.compile('^' + regBandKey.pattern + '$', re.IGNORECASE) def __init__(self, temporalProfileCollection, plotWidget, parent=None, *args): #assert isinstance(tableView, QTableView) super(PlotSettingsModel2D, self).__init__(parent=parent) assert isinstance(temporalProfileCollection, TemporalProfileCollection) self.cnID = 'ID' self.cnSensor = 'sensor' self.cnExpression = LABEL_DN self.cnStyle = 'style' self.cnTemporalProfile = 'Coordinate' self.columNames = [self.cnTemporalProfile, self.cnSensor, self.cnStyle, self.cnExpression] self.mPlotSettings = [] #assert isinstance(plotWidget, DateTimePlotWidget) self.mPlotWidget = plotWidget self.sortColumnIndex = 0 self.sortOrder = Qt.AscendingOrder self.tpCollection = temporalProfileCollection #self.tpCollection.sigTemporalProfilesRemoved.connect(lambda removedTPs : self.removePlotStyles([p for p in self.mPlotSettings if p.temporalProfile() in removedTPs])) assert isinstance(self.tpCollection.TS, TimeSeries) #self.tpCollection.TS.sigSensorAdded.connect(self.addPlotItem) #self.tpCollection.TS.sigSensorRemoved.connect(self.removeSensor) self.sort(0, Qt.AscendingOrder) self.dataChanged.connect(self.signaler) def __len__(self): return len(self.mPlotSettings) def __iter__(self): return iter(self.mPlotSettings) def __getitem__(self, slice): return self.mPlotSettings[slice] def __contains__(self, item): return item in self.mPlotSettings def testSlot(self, *args): print(('TESTSLOT', args)) def columnIndex(self, name): return self.columNames.index(name) def signaler(self, idxUL, idxLR): if idxUL.isValid(): plotStyle = self.idx2plotStyle(idxUL) cname = self.columNames[idxUL.column()] if cname in [self.cnSensor,self.cnStyle]: self.sigVisibilityChanged.emit(plotStyle) if cname in [self.cnExpression]: self.sigDataChanged.emit(plotStyle) def requiredBandsIndices(self, sensor): """ Returns the band indices required to calculate the values for the different PlotStyle expressions making use of sensor :param sensor: SensorInstrument for which the band indices are to be returned. :return: [list-of-band-indices] """ bandIndices = set() assert isinstance(sensor, SensorInstrument) for p in [p for p in self.mPlotSettings if p.sensor() == sensor]: assert isinstance(p, TemporalProfile2DPlotStyle) expression = p.expression() #remove leading & tailing " bandKeys = PlotSettingsModel2D.regBandKey.findall(expression) for bandIndex in [bandKey2bandIndex(key) for key in bandKeys]: bandIndices.add(bandIndex) return bandIndices def insertPlotStyles(self, plotStyles, i=None): """ Inserts PlotStyle :param plotStyles: TemporalProfilePlotStyle | [list-of-TemporalProfilePlotStyle] :param i: index to insert, defaults to the last list position """ if isinstance(plotStyles, TemporalProfile2DPlotStyle): plotStyles = [plotStyles] assert isinstance(plotStyles, list) for plotStyle in plotStyles: assert isinstance(plotStyle, TemporalProfile2DPlotStyle) if i is None: i = len(self.mPlotSettings) if len(plotStyles) > 0: self.beginInsertRows(QModelIndex(), i, i + len(plotStyles)-1) for j, plotStyle in enumerate(plotStyles): assert isinstance(plotStyle, TemporalProfile2DPlotStyle) self.mPlotSettings.insert(i+j, plotStyle) self.endInsertRows() self.sigPlotStylesAdded.emit(plotStyles) def removePlotStyles(self, plotStyles): """ Removes PlotStyle instances :param plotStyles: TemporalProfilePlotStyle | [list-of-TemporalProfilePlotStyle] """ if isinstance(plotStyles, PlotStyle): plotStyles = [plotStyles] assert isinstance(plotStyles, list) if len(plotStyles) > 0: for plotStyle in plotStyles: assert isinstance(plotStyle, PlotStyle) if plotStyle in self.mPlotSettings: idx = self.plotStyle2idx(plotStyle) self.beginRemoveRows(QModelIndex(), idx.row(),idx.row()) self.mPlotSettings.remove(plotStyle) self.endRemoveRows() if isinstance(plotStyle, TemporalProfile2DPlotStyle): for pi in plotStyle.mPlotItems: self.mPlotWidget.getPlotItem().removeItem(pi) self.sigPlotStylesRemoved.emit(plotStyles) def sort(self, col, order): if self.rowCount() == 0: return colName = self.columnames[col] r = order != Qt.AscendingOrder #self.beginMoveRows(idxSrc, if colName == self.cnSensor: self.mPlotSettings.sort(key = lambda sv:sv.sensor().name(), reverse=r) elif colName == self.cnExpression: self.mPlotSettings.sort(key=lambda sv: sv.expression(), reverse=r) elif colName == self.cnStyle: self.mPlotSettings.sort(key=lambda sv: sv.color, reverse=r) def rowCount(self, parent = QModelIndex()): return len(self.mPlotSettings) def removeRows(self, row, count , parent = QModelIndex()): self.beginRemoveRows(parent, row, row + count-1) toRemove = self.mPlotSettings[row:row + count] for tsd in toRemove: self.mPlotSettings.remove(tsd) self.endRemoveRows() def plotStyle2idx(self, plotStyle): assert isinstance(plotStyle, TemporalProfile2DPlotStyle) if plotStyle in self.mPlotSettings: i = self.mPlotSettings.index(plotStyle) return self.createIndex(i, 0) else: return QModelIndex() def idx2plotStyle(self, index): if index.isValid() and index.row() < self.rowCount(): return self.mPlotSettings[index.row()] return None def columnCount(self, parent = QModelIndex()): return len(self.columNames) def data(self, index, role = Qt.DisplayRole): if role is None or not index.isValid(): return None value = None columnName = self.columNames[index.column()] plotStyle = self.idx2plotStyle(index) if isinstance(plotStyle, TemporalProfile2DPlotStyle): sensor = plotStyle.sensor() #print(('data', columnName, role)) if role == Qt.DisplayRole: if columnName == self.cnSensor: if isinstance(sensor, SensorInstrument): value = sensor.name() else: value = '<Select Sensor>' elif columnName == self.cnExpression: value = plotStyle.expression() elif columnName == self.cnTemporalProfile: value = plotStyle.temporalProfile().name() #elif role == Qt.DecorationRole: # if columnName == self.cnStyle: # value = plotStyle.createIcon(QSize(96,96)) elif role == Qt.CheckStateRole: if columnName == self.cnTemporalProfile: value = Qt.Checked if plotStyle.isVisible() else Qt.Unchecked elif role == Qt.UserRole: value = plotStyle if columnName == self.cnSensor: value = plotStyle.sensor() elif columnName == self.cnExpression: value = plotStyle.expression() elif columnName == self.cnStyle: value = plotStyle elif columnName == self.cnTemporalProfile: value == plotStyle.temporalProfile() else: value = plotStyle #print(('get data',value)) return value def setData(self, index, value, role=None): if role is None or not index.isValid(): return False #print(('Set data', index.row(), index.column(), value, role)) columnName = self.columNames[index.column()] if value is None: return False result = False plotStyle = self.idx2plotStyle(index) if isinstance(plotStyle, TemporalProfile2DPlotStyle): if role in [Qt.DisplayRole]: if columnName == self.cnExpression: plotStyle.setExpression(value) result = True elif columnName == self.cnStyle: if isinstance(value, PlotStyle): plotStyle.copyFrom(value) result = True if role == Qt.CheckStateRole: if columnName == self.cnTemporalProfile: plotStyle.setVisibility(value == Qt.Checked) result = True if role == Qt.EditRole: if columnName == self.cnExpression: plotStyle.setExpression(value) result = True elif columnName == self.cnStyle: plotStyle.copyFrom(value) result = True elif columnName == self.cnSensor: plotStyle.setSensor(value) result = True elif columnName == self.cnTemporalProfile: plotStyle.setTemporalProfile(value) result = True if result: #save plot-style self.savePlotSettings(plotStyle, index='DEFAULT') self.dataChanged.emit(index, index) return result def savePlotSettings(self, sensorPlotSettings, index='DEFAULT'): return #todo assert isinstance(sensorPlotSettings, TemporalProfile2DPlotStyle) #todo: avoid dumps id = 'SPS.{}.{}'.format(index, sensorPlotSettings.sensor().id()) d = pickle.dumps(sensorPlotSettings) SETTINGS.setValue(id, d) def restorePlotSettings(self, sensor, index='DEFAULT'): return None #todo assert isinstance(sensor, SensorInstrument) id = 'SPS.{}.{}'.format(index, sensor.id()) sensorPlotSettings = SETTINGS.value(id) if sensorPlotSettings is not None: try: sensorPlotSettings = pickle.loads(sensorPlotSettings) s = "" except: sensorPlotSettings = None pass if isinstance(sensorPlotSettings, TemporalProfile2DPlotStyle): return sensorPlotSettings else: return None def flags(self, index): if index.isValid(): columnName = self.columNames[index.column()] flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable if columnName in [self.cnTemporalProfile]: flags = flags | Qt.ItemIsUserCheckable if columnName in [self.cnTemporalProfile, self.cnSensor, self.cnExpression, self.cnStyle]: #allow check state flags = flags | Qt.ItemIsEditable return flags #return item.qt_flags(index.column()) return Qt.NoItemFlags def headerData(self, col, orientation, role): if Qt is None: return None if orientation == Qt.Horizontal and role == Qt.DisplayRole: return self.columNames[col] elif orientation == Qt.Vertical and role == Qt.DisplayRole: return col return None class ProfileViewDockUI(QgsDockWidget, loadUI('profileviewdock.ui')): def __init__(self, parent=None): super(ProfileViewDockUI, self).__init__(parent) self.setupUi(self) #TBD. #self.line.setVisible(False) #self.listWidget.setVisible(False) self.stackedWidget.setCurrentWidget(self.page2D) self.plotWidget3D = None if OPENGL_AVAILABLE: l = self.frame3DPlot.layout() #from pyqtgraph.opengl import GLViewWidget #self.plotWidget3D = GLViewWidget(parent=self.page3D) self.plotWidget3D = ViewWidget3D(parent=self.frame3DPlot) self.plotWidget3D.setObjectName('plotWidget3D') size = self.labelDummy3D.size() l.addWidget(self.plotWidget3D) self.plotWidget3D.setSizePolicy(self.labelDummy3D.sizePolicy()) self.labelDummy3D.setVisible(False) l.removeWidget(self.labelDummy3D) self.plotWidget3D.setBaseSize(size) self.splitter3D.setSizes([100, 100]) #pi = self.plotWidget2D.plotItem #ax = DateAxis(orientation='bottom', showValues=True) #pi.layout.addItem(ax, 3,2) self.baseTitle = self.windowTitle() self.TS = None self.progressBar.setMinimum(0) self.progressBar.setMaximum(100) self.progressBar.setValue(0) self.progressInfo.setText('') self.pxViewModel2D = None self.pxViewModel3D = None self.tableView2DProfiles.horizontalHeader().setResizeMode(QHeaderView.ResizeToContents) self.tableView2DProfiles.setSortingEnabled(True) self.tableViewTemporalProfiles.horizontalHeader().setResizeMode(QHeaderView.ResizeToContents) self.tableViewTemporalProfiles.setSortingEnabled(True) def qgsFieldFromKeyValue(fieldName, value): t = type(value) if t in [int, float] or np.isreal(value): fLen = 0 fPrec = 0 fComm = '' fType = '' f = QgsField(fieldName, QVariant.Double, 'double', 40, 5) else: f = QgsField(fieldName, QVariant.String, 'text', 40, 5) return f def sensorExampleQgsFeature(sensor): assert isinstance(sensor, SensorInstrument) #populate with exemplary band values (generally stored as floats) fieldValues = collections.OrderedDict() for b in range(sensor.nb): fn = bandIndex2bandKey(b) fieldValues[fn] = 1.0 date = datetime.date.today() doy = dateDOY(date) fieldValues['doy'] = doy fieldValues['date'] = str(date) fields = QgsFields() for k, v in fieldValues.items(): fields.append(qgsFieldFromKeyValue(k,v)) f = QgsFeature(fields) for k, v in fieldValues.items(): f.setAttribute(k, v) return f 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) 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): 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 dt64: return np.datetime64(date) else: return date #return np.datetime64('{:04}-01-01'.format(year), 'D') + np.timedelta64(int(yearElapsed), 'D') def depr_date2num(d): d2 = d.astype(datetime.datetime) o = d2.toordinal() #assert d == num2date(o) return o def depr_num2date(n): n = int(np.round(n)) if n < 1: n = 1 d = datetime.datetime.fromordinal(n) d = d.date() return np.datetime64('{:04}-{:02}-{:02}'.format(d.year,d.month,d.day), 'D') class TemporalProfile(QObject): _mNextID = 0 @staticmethod def nextID(): n = TemporalProfile._mNextID TemporalProfile._mNextID += 1 return n def __init__(self, timeSeries, spatialPoint): super(TemporalProfile, self).__init__() assert isinstance(timeSeries, TimeSeries) assert isinstance(spatialPoint, SpatialPoint) self.mTimeSeries = timeSeries self.mCoordinate = spatialPoint self.mID = TemporalProfile.nextID() self.mData = {} self.mUpdated = False self.mName = '#{}'.format(self.mID) self.mLoaded = self.mLoadedMax = 0 self.initMetadata() self.updateLoadingStatus() def initMetadata(self): for tsd in self.mTimeSeries: assert isinstance(tsd, TimeSeriesDatum) meta = {'doy':tsd.doy, 'date':str(tsd.date)} self.updateData(tsd, meta) sigNameChanged = pyqtSignal(str) def setName(self, name): if name != self.mName: self.mName = name self.sigNameChanged.emit(self.mName) def name(self): return self.mName def plot(self): import pyqtgraph as pg 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()) pw.getPlotItem().addItem(pi) pi.setColor('green') pg.QAPP.exec_() def updateData(self, tsd, values): assert isinstance(tsd, TimeSeriesDatum) assert isinstance(values, dict) if tsd not in self.mData.keys(): self.mData[tsd] = {} self.mData[tsd].update(values) self.updateLoadingStatus() self.mUpdated = True def resetUpdated(self): self.mUpdated = False def updated(self): return self.mUpdated def dataFromExpression(self, sensor, expression, dateType='date'): assert dateType in ['date','doy'] x = [] y = [] if not isinstance(expression, QgsExpression): expression = QgsExpression(expression) assert isinstance(expression, QgsExpression) expression = QgsExpression(expression) fields = QgsFields() sensorTSDs = sorted([tsd for tsd in self.mData.keys() if tsd.sensor == sensor]) for tsd in sensorTSDs: data = self.mData[tsd] for k, v in data.items(): if v is not None and fields.fieldNameIndex(k) == -1: fields.append(qgsFieldFromKeyValue(k, v)) for i, tsd in enumerate(sensorTSDs): assert isinstance(tsd, TimeSeriesDatum) data = self.mData[tsd] context = QgsExpressionContext() scope = QgsExpressionContextScope() f = QgsFeature() f.setFields(fields) f.setValid(True) for k, v in data.items(): if v is None: continue idx = f.fieldNameIndex(k) field = f.fields().field(idx) if field.typeName() == 'text': v = str(v) else: v = float(v) f.setAttribute(k,v) scope.setFeature(f) context.appendScope(scope) #value = expression.evaluatePrepared(f) value = expression.evaluate(context) if value in [None, NULL]: s = "" else: if dateType == 'date': x.append(date2num(tsd.date)) elif dateType == 'doy': x.append(tsd.doy) y.append(value) #return np.asarray(x), np.asarray(y) assert len(x) == len(y) return x, y def data(self, tsd): assert isinstance(tsd, TimeSeriesDatum) 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.mLoadedMax def updateLoadingStatus(self): """ Calculates and the loading status in terms of single pixel values. nMax is the sum of all bands over each TimeSeriesDatum and Sensors """ self.mLoaded = self.mLoadedMax for tsd in self.mTimeSeries: assert isinstance(tsd, TimeSeriesDatum) self.mLoadedMax += tsd.sensor.nb if self.hasData(tsd): self.mLoaded += len([k for k in self.mData[tsd].keys() if k.startswith('b')]) def hasData(self,tsd): assert isinstance(tsd, TimeSeriesDatum) return tsd in self.mData.keys() def __repr__(self): return 'TemporalProfile {}'.format(self.mCoordinate) class SpectralTemporalVisualization(QObject): sigShowPixel = pyqtSignal(TimeSeriesDatum, QgsPoint, QgsCoordinateReferenceSystem) """ Signalizes to move to specific date of interest """ sigMoveToDate = pyqtSignal(np.datetime64) def __init__(self, ui): super(SpectralTemporalVisualization, self).__init__() assert isinstance(ui, ProfileViewDockUI), 'arg ui of type: {} {}'.format(type(ui), str(ui)) self.ui = ui if DEBUG: import timeseriesviewer.pixelloader timeseriesviewer.pixelloader.DEBUG = True self.TS = None self.pixelLoader = PixelLoader() self.pixelLoader.sigPixelLoaded.connect(self.onPixelLoaded) self.pixelLoader.sigLoadingStarted.connect(lambda: self.ui.progressInfo.setText('Start loading...')) self.plot_initialized = False self.tableView2DProfiles = ui.tableView2DProfiles self.tableView2DProfiles.setSortingEnabled(False) self.tableView2DProfiles.horizontalHeader().setResizeMode(QHeaderView.ResizeToContents) self.plotSettingsModel3D = PlotSettingsModel3D() #self.plotSettingsModel3D.sigPlotStylesRemoved.connect(self.updatePlot3D) #self.plotSettingsModel3D.sigPlotStylesAdded.connect(self.updatePlot3D) #self.plotSettingsModel3D.sigPlotStylesAdded.connect(self.updatePlot3D) self.plotSettingsModel3D.rowsInserted.connect(self.onRowsInserted3D) self.ui.tableView3DProfiles.setModel(self.plotSettingsModel3D) self.ui.tableView3DProfiles.horizontalHeader().setResizeMode(QHeaderView.ResizeToContents) self.delegateTableView3D = PlotSettingsModel3DWidgetDelegate(self.ui.tableView3DProfiles) self.delegateTableView3D.setItemDelegates(self.ui.tableView3DProfiles) # self.mSelectionModel.currentChanged.connect(self.onCurrentSelectionChanged) self.plot2D = ui.plotWidget2D pi = self.plot2D.getPlotItem() pi.getViewBox().sigMoveToDate.connect(self.sigMoveToDate) pi.getAxis('bottom').setLabel(LABEL_TIME) pi.getAxis('left').setLabel(LABEL_DN) self.plot2Dvline = pg.InfiniteLine(angle=90, movable=False) self.plot2Dhline = pg.InfiniteLine(angle=0, movable=False) #self.plot2DLabel = pg.TextItem(text='LABEL') self.plot2DLabel = pg.LabelItem(justify='right') #pi.setContentsMargins(0.1, 0.1, 0.1, 0.1) pi.addItem(self.plot2Dvline, ignoreBounds=True) pi.addItem(self.plot2Dhline, ignoreBounds=True) self.plot2D.addItem(self.plot2DLabel, ignoreBounds=True) self.proxy2D = pg.SignalProxy(self.plot2D.scene().sigMouseMoved, rateLimit=60, slot=self.onMouseMoved2D) self.plot3D = ui.plotWidget3D self.reset3DCamera() ## Add a grid to the view if OPENGL_AVAILABLE: import pyqtgraph.opengl as gl self.glGridItem = gl.GLGridItem() self.glGridItem.setDepthValue(10) # draw grid after surfaces since they may be translucent self.glPlotDataItems = [self.glGridItem] self.plot3D.addItem(self.glGridItem) self.tpCollection = TemporalProfileCollection() self.tpCollectionListModel = TemporalProfileCollectionListModel(self.tpCollection) self.ui.tableViewTemporalProfiles.setModel(self.tpCollection) self.ui.tableViewTemporalProfiles.selectionModel().selectionChanged.connect(self.onTemporalProfileSelectionChanged) self.ui.tableViewTemporalProfiles.horizontalHeader().setResizeMode(QHeaderView.ResizeToContents) self.ui.cbTemporalProfile3D.setModel(self.tpCollectionListModel) #self.pxCollection.sigPixelAdded.connect(self.requestUpdate) #self.pxCollection.sigPixelRemoved.connect(self.clear) self.plotSettingsModel2D = None self.pixelLoader.sigLoadingStarted.connect(self.clear) self.pixelLoader.sigLoadingFinished.connect(lambda : self.plot2D.enableAutoRange('x', False)) # self.VIEW.setItemDelegateForColumn(3, PointStyleDelegate(self.VIEW)) self.plotData2D = dict() self.plotData3D = dict() self.updateRequested = True self.updateTimer = QTimer(self) self.updateTimer.timeout.connect(self.onDataUpdate) self.updateTimer.start(5000) self.sigMoveToDate.connect(self.onMoveToDate) self.initActions() def onMouseMoved2D(self, evt): pos = evt[0] ## using signal proxy turns original arguments into a tuple plotItem = self.plot2D.getPlotItem() if plotItem.sceneBoundingRect().contains(pos): mousePoint = plotItem.vb.mapSceneToView(pos) x = mousePoint.x() y = mousePoint.y() index = int(mousePoint.x()) self.plot2DLabel.setText('Refreshed {}'.format(mousePoint.x(), mousePoint.y())) #if index > 0 and index < len(data1): # label.setText( # "<span style='font-size: 12pt'>x=%0.1f, <span style='color: red'>y1=%0.1f</span>, <span style='color: green'>y2=%0.1f</span>" % ( # mousePoint.x(), data1[index], data2[index])) self.plot2Dvline.setPos(mousePoint.x()) self.plot2Dhline.setPos(mousePoint.y()) return vb = plotItem.vb if plotItem.sceneBoundingRect().contains(pos): mousePoint = vb.mapSceneToView(pos) self.vLine.setPos(mousePoint.x()) self.hLine.setPos(mousePoint.y()) info = '{:0.2f}, {:0.2f}'.format(mousePoint.x(), mousePoint.y()) self.tbCursorLocationValue.setText(info) """ self.mlabel.setText( "<span style='font-size: 12pt'>{}</span>".format(info) """ else: self.tbCursorLocationValue.setText('') self.mlabel.setText('') def selected2DPlotStyles(self): result = [] m = self.ui.tableView2DProfiles.model() for idx in selectedModelIndices(self.ui.tableView2DProfiles): result.append(m.idx2plotStyle(idx)) return result def selectedTemporalProfiles(self): result = [] m = self.ui.tableViewTemporalProfiles.model() for idx in selectedModelIndices(self.ui.tableViewTemporalProfiles): result.append(m.idx2tp(idx)) return result def removePlotStyles(self, plotStyles): m = self.ui.tableView2DProfiles.model() if isinstance(m, PlotSettingsModel2D): m.removePlotStyles(plotStyles) def removeTemporalProfiles(self, temporalProfiles): m = self.ui.tableViewTemporalProfiles.model() if isinstance(m, TemporalProfileCollection): m.removeTemporalProfiles(temporalProfiles) def createNewPlotStyle(self): l = len(self.tpCollection) if l > 0: temporalProfile = self.tpCollection[0] plotStyle = TemporalProfile2DPlotStyle(temporalProfile) plotStyle.sigExpressionUpdated.connect(self.updatePlot2D) sensors = self.TS.Sensors.keys() if len(sensors) > 0: plotStyle.setSensor(sensors[0]) self.plotSettingsModel2D.insertPlotStyles([plotStyle]) pdi = plotStyle.createPlotItem(self.plot2D) assert isinstance(pdi, TemporalProfilePlotDataItem) pdi.sigClicked.connect(self.onProfileClicked2D) pdi.sigPointsClicked.connect(self.onPointsClicked2D) self.plot2D.getPlotItem().addItem(pdi) #self.plot2D.getPlotItem().addItem(pg.PlotDataItem(x=[1, 2, 3], y=[1, 2, 3])) #plotItem.addDataItem(pdi) #plotItem.plot().sigPlotChanged.emit(plotItem) self.updatePlot2D() def onProfileClicked2D(self, pdi): if isinstance(pdi, TemporalProfilePlotDataItem): sensor = pdi.mPlotStyle.sensor() tp = pdi.mPlotStyle.temporalProfile() if isinstance(tp, TemporalProfile) and isinstance(sensor, SensorInstrument): info = ['Sensor:{}'.format(sensor.name()), 'Coordinate:{}, {}'.format(tp.mCoordinate.x(), tp.mCoordinate.y())] self.ui.tbInfo2D.setPlainText('\n'.join(info)) def onPointsClicked2D(self, pdi, spottedItems): if isinstance(pdi, TemporalProfilePlotDataItem) and isinstance(spottedItems, list): sensor = pdi.mPlotStyle.sensor() tp = pdi.mPlotStyle.temporalProfile() if isinstance(tp, TemporalProfile) and isinstance(sensor, SensorInstrument): info = ['Sensor: {}'.format(sensor.name()), 'Coordinate: {}, {}'.format(tp.mCoordinate.x(), tp.mCoordinate.y())] for item in spottedItems: pos = item.pos() x = pos.x() y = pos.y() date = num2date(x) info.append('Date: {}\nValue: {}'.format(date, y)) self.ui.tbInfo2D.setPlainText('\n'.join(info)) def onTemporalProfileSelectionChanged(self, selected, deselected): self.ui.actionRemoveTemporalProfile.setEnabled(len(selected) > 0) def onPlot2DSelectionChanged(self, selected, deselected): self.ui.actionRemoveView.setEnabled(len(selected) > 0) def initActions(self): self.ui.actionRemoveView.setEnabled(False) self.ui.actionRemoveTemporalProfile.setEnabled(False) self.ui.btnAddView.setDefaultAction(self.ui.actionAddView) self.ui.btnRemoveView.setDefaultAction(self.ui.actionRemoveView) self.ui.btnRefresh2D.setDefaultAction(self.ui.actionRefresh2D) self.ui.btnRefresh3D.setDefaultAction(self.ui.actionRefresh3D) self.ui.btnRemoveTemporalProfile.setDefaultAction(self.ui.actionRemoveTemporalProfile) self.ui.btnReset3DCamera.setDefaultAction(self.ui.actionReset3DCamera) self.ui.actionRefresh2D.triggered.connect(self.updatePlot2D) self.ui.actionRefresh3D.triggered.connect(self.updatePlot3D) self.ui.actionAddView.triggered.connect(self.createNewPlotStyle) self.ui.actionRemoveView.triggered.connect(lambda:self.removePlotStyles(self.selected2DPlotStyles())) self.ui.actionRemoveTemporalProfile.triggered.connect(lambda :self.removeTemporalProfiles(self.selectedTemporalProfiles())) self.ui.actionReset3DCamera.triggered.connect(self.reset3DCamera) self.tpCollection.sigMaxProfilesChanged.connect(self.ui.sbMaxTP.setValue) self.ui.sbMaxTP.valueChanged.connect(self.tpCollection.setMaxProfiles) #todo: self.ui.actionRemoveView.triggered.connect(self.plotSettingsModel.createPlotStyle) def reset3DCamera(self, *args): if OPENGL_AVAILABLE: self.plot3D.setCameraPosition((0,0,0), distance=10, elevation=10) def setTimeSeries(self, TS): assert isinstance(TS, TimeSeries) self.TS = TS self.tpCollection.connectTimeSeries(self.TS) self.plotSettingsModel2D = PlotSettingsModel2D(self.tpCollection, self.plot2D, parent=self) self.plotSettingsModel2D.sigVisibilityChanged.connect(self.setVisibility) self.plotSettingsModel2D.sigDataChanged.connect(self.requestUpdate) self.plotSettingsModel2D.rowsInserted.connect(self.onRowsInserted2D) self.plotSettingsModel3D.connectTimeSeries(self.TS) # self.plotSettingsModel.modelReset.connect(self.updatePersistantWidgets) self.tableView2DProfiles.setModel(self.plotSettingsModel2D) #self.tableView2DProfilesSelectionModel = QItemSelectionModel(self.mModel) self.tableView2DProfiles.selectionModel().selectionChanged.connect(self.onPlot2DSelectionChanged) #self.tableView2DProfilesSelectionModel.selectionChanged.connect(self.onPlot2DSelectionChanged) #self.tableView2DProfilesSelectionModel.setSelectionModel(self.mSelectionModel) self.delegateTableView2D = PlotSettingsModel2DWidgetDelegate(self.tableView2DProfiles, self.TS, self.tpCollectionListModel) self.delegateTableView2D.setItemDelegates(self.tableView2DProfiles) sigMoveToTSD = pyqtSignal(TimeSeriesDatum) def onMoveToDate(self, date): dt = np.asarray([np.abs(tsd.date - date) for tsd in self.TS]) i = np.argmin(dt) self.sigMoveToTSD.emit(self.TS[i]) def onPixelLoaded(self, nDone, nMax, d): self.ui.progressBar.setValue(nDone) self.ui.progressBar.setMaximum(nMax) assert isinstance(d, PixelLoaderTask) bn = os.path.basename(d.sourcePath) if d.success(): t = 'Last loaded from {}.'.format(bn) self.tpCollection.addPixelLoaderResult(d) self.updateRequested = True else: t = 'Failed loading from {}.'.format(bn) if d.info and d.info != '': t += '({})'.format(d.info) # QgsApplication.processEvents() self.ui.progressInfo.setText(t) def requestUpdate(self, *args): self.updateRequested = True #next time def onRowsInserted2D(self, parent, start, end): model = self.tableView2DProfiles.model() if isinstance(model, PlotSettingsModel2D): colExpression = model.columnIndex(model.cnExpression) colStyle = model.columnIndex(model.cnStyle) while start <= end: idxExpr = model.createIndex(start, colExpression) idxStyle = model.createIndex(start, colStyle) self.tableView2DProfiles.openPersistentEditor(idxExpr) self.tableView2DProfiles.openPersistentEditor(idxStyle) start += 1 #self.TV.openPersistentEditor(model.createIndex(start, colStyle)) s = "" def onRowsInserted3D(self, parent, start, end): model = self.ui.tableView3DProfiles.model() if isinstance(model, PlotSettingsModel3D): colStyle = model.columnIndex(model.cnStyle) while start <= end: idxStyle = model.createIndex(start, colStyle) self.ui.tableView3DProfiles.openPersistentEditor(idxStyle) start += 1 def onObservationClicked(self, plotDataItem, points): for p in points: tsd = p.data() #print(tsd) def clear(self): #first remove from pixelCollection self.tpCollection.prune() return self.plotData2D.clear() self.plotData3D.clear() pi = self.plot2D.getPlotItem() plotItems = pi.listDataItems() for p in plotItems: p.clear() p.update() if len(self.TS) > 0: rng = [self.TS[0].date, self.TS[-1].date] rng = [date2num(d) for d in rng] self.plot2D.getPlotItem().setRange(xRange=rng) #QApplication.processEvents() if self.plot3D: pass def loadCoordinate(self, spatialPoints=None, LUT_bandIndices=None): """ Loads a temporal profile for a single or multiple geometries. :param spatialPoints: SpatialPoint | [list-of-SpatialPoints] """ if not isinstance(self.plotSettingsModel2D, PlotSettingsModel2D): return False if not self.pixelLoader.isReadyToLoad(): return False assert isinstance(self.TS, TimeSeries) #Get or create the TimeSeriesProfiles which will store the loaded values tasks = [] TPs = [] theGeometries = [] # Define a which (new) bands need to be loaded for each sensor if LUT_bandIndices is None: LUT_bandIndices = dict() for sensor in self.TS.Sensors: LUT_bandIndices[sensor] = self.plotSettingsModel2D.requiredBandsIndices(sensor) assert isinstance(LUT_bandIndices, dict) for sensor in self.TS.Sensors: assert sensor in LUT_bandIndices.keys() #update new / existing points if isinstance(spatialPoints, SpatialPoint): spatialPoints = [spatialPoints] for spatialPoint in spatialPoints: assert isinstance(spatialPoint, SpatialPoint) TP = self.tpCollection.fromSpatialPoint(spatialPoint) if not isinstance(TP, TemporalProfile): TP = TemporalProfile(self.TS, spatialPoint) self.tpCollection.insertTemporalProfiles(TP, i=0) TPs.append(TP) theGeometries.append(TP.mCoordinate) TP_ids = [TP.mID for TP in TPs] #each TSD is a Task #a Task defines which bands are to be loaded for tsd in self.TS: #do not load from invisible TSDs if not tsd.isVisible(): continue #which bands do we need to load? requiredIndices = set(LUT_bandIndices[tsd.sensor]) if len(requiredIndices) == 0: continue else: s = "" missingIndices = set() for TP in TPs: assert isinstance(TP, TemporalProfile) existingBandKeys = [k for k in TP.data(tsd).keys() if PlotSettingsModel2D.regBandKeyExact.search(k)] existingBandIndices = set([bandKey2bandIndex(k) for k in existingBandKeys]) need2load = requiredIndices.difference(existingBandIndices) missingIndices = missingIndices.union(need2load) missingIndices = sorted(list(missingIndices)) if len(missingIndices) > 0: task = PixelLoaderTask(tsd.pathImg, theGeometries, bandIndices=missingIndices, temporalProfileIDs=TP_ids) tasks.append(task) if len(tasks) > 0: aGoodDefault = 2 if len(self.TS) > 25 else 1 self.pixelLoader.setNumberOfProcesses(SETTINGS.value('profileloader_threads', aGoodDefault)) if DEBUG: print('Start loading for {} geometries from {} sources...'.format( len(theGeometries), len(tasks) )) self.pixelLoader.startLoading(tasks) else: if DEBUG: print('Data for geometries already loaded') def setVisibility(self, sensorPlotStyle): assert isinstance(sensorPlotStyle, TemporalProfile2DPlotStyle) self.setVisibility2D(sensorPlotStyle) def setVisibility2D(self, sensorPlotStyle): self.plot2D.update() def addData(self, sensorView = None): if sensorView is None: for sv in self.plotSettingsModel2D.items: self.setData(sv) else: assert isinstance(sensorView, TemporalProfile2DPlotStyle) self.setData2D(sensorView) @QtCore.pyqtSlot() def onDataUpdate(self): for plotSetting in self.plotSettingsModel2D: assert isinstance(plotSetting, TemporalProfile2DPlotStyle) if plotSetting.temporalProfile().updated(): for pdi in plotSetting.mPlotItems: assert isinstance(pdi, TemporalProfilePlotDataItem) pdi.updateDataAndStyle() plotSetting.temporalProfile().resetUpdated() for i in self.plot2D.getPlotItem().dataItems: i.updateItems() if not self.plot2D.xAxisInitialized: x0 = x1 = None for plotSetting in self.plotSettingsModel2D: assert isinstance(plotSetting, TemporalProfile2DPlotStyle) for pdi in plotSetting.mPlotItems: assert isinstance(pdi, TemporalProfilePlotDataItem) if pdi.xData.ndim == 0 or pdi.xData.shape[0] == 0: continue if x0 is None: x0 = pdi.xData.min() x1 = pdi.xData.max() else: x0 = min(pdi.xData.min(), x0) x1 = max(pdi.xData.max(), x1) if x0 is not None: self.plot2D.getPlotItem().setXRange(x0, x1) self.plot2D.xAxisInitialized = True @QtCore.pyqtSlot() def updatePlot3D(self): if OPENGL_AVAILABLE: from pyqtgraph.opengl import GLViewWidget import pyqtgraph.opengl as gl assert isinstance(self.plot3D, GLViewWidget) w = self.plot3D #we need the data from all bands del self.glPlotDataItems[:] for i in w.items: w.removeItem(i) idx = self.ui.cbTemporalProfile3D.currentIndex() if idx >= 0: tp = self.ui.cbTemporalProfile3D.itemData(idx, role=Qt.UserRole) assert isinstance(tp, TemporalProfile) #1. ensure that data from all bands will be loaded LUT_bandIndices = dict() for sensor in self.TS.sensors(): assert isinstance(sensor, SensorInstrument) LUT_bandIndices[sensor] = list(range(sensor.nb)) self.loadCoordinate(tp.mCoordinate, LUT_bandIndices=LUT_bandIndices) #2. visualize already loaded data LUTStyle = {} for style in self.plotSettingsModel3D: assert isinstance(style, TemporalProfile3DPlotStyle) LUTStyle[style.sensor()] = style dataPos = [] x0 = x1 = y0 = y1 = z0 = z1 = 0 for iDate, tsd in enumerate(tp.mTimeSeries): data = tp.data(tsd) bandKeys = sorted([k for k in data.keys() if k.startswith('b') and data[k] != None], key=lambda k: bandKey2bandIndex(k)) if len(bandKeys) < 2: continue t = date2num(tsd.date) x = [] y = [] z = [] for i, k in enumerate(bandKeys): x.append(i) y.append(t) z.append(data[k]) x = np.asarray(x) y = np.asarray(y) z = np.asarray(z) if iDate == 0: x0, x1 = (x.min(), x.max()) y0, y1 = (y.min(), y.max()) z0, z1 = (z.min(), z.max()) else: x0, x1 = (min(x.min(), x0), max(x.max(), x1)) y0, y1 = (min(y.min(), y0), max(y.max(), y1)) z0, z1 = (min(z.min(), z0), max(z.max(), z1)) if tsd.sensor in LUTStyle.keys(): style = LUTStyle[tsd.sensor] else: style = TemporalProfile3DPlotStyle(tsd.sensor) dataPos.append((x,y,z, style)) xyz = [(x0,x1),(y0,y1),(z0,z1)] l = len(dataPos) for iPos, pos in enumerate(dataPos): x,y,z, style = pos assert isinstance(style, TemporalProfile3DPlotStyle) if not style.isVisible(): continue arr = np.asarray((x,y,z), dtype=np.float64).transpose() for i, m in enumerate(xyz): m0, m1 = m arr[:, i] = (arr[:,i] - m0)/(m1-m0) plt = gl.GLLinePlotItem(pos=arr, #color=pg.glColor((i, n * 1.3)), #color=pg.glColor(255,123,123,125), #color=pg.glColor((iPos, l * 1.3)), color=pg.glColor(style.color()), width=1.0, antialias=True) self.glPlotDataItems.append(plt) for i, item in enumerate(self.glPlotDataItems): w.addItem(item) #self.glGridItem.scale(0.1,0.1,0.1, local=False) #w.setBackgroundColor(QColor('black')) #w.setCameraPosition(pos=(0.0, 0.0, 0.0), distance=1.) w.addItem(self.glGridItem) w.update() """ for sensor, values in data.items(): if len(values['z']) > 0: x = values['x'] y = values['y'] z = values['z'] p2 = gl.GLSurfacePlotItem(x=x, y=y, z=z, shader='normalColor') p2.translate(-10, -10, 0) w.addItem(p2) """ @QtCore.pyqtSlot() def updatePlot2D(self): if isinstance(self.plotSettingsModel2D, PlotSettingsModel2D): if DEBUG: print('Update plot...') pi = self.plot2D.getPlotItem() piDataItems = pi.listDataItems() locations = set() for plotSetting in self.plotSettingsModel2D: assert isinstance(plotSetting, TemporalProfile2DPlotStyle) locations.add(plotSetting.temporalProfile().mCoordinate) for pdi in plotSetting.mPlotItems: assert isinstance(pdi, TemporalProfilePlotDataItem) pdi.updateDataAndStyle() #for i in pi.dataItems: # i.updateItems() #self.plot2D.update() #2. load pixel data self.loadCoordinate(list(locations)) # https://github.com/pyqtgraph/pyqtgraph/blob/5195d9dd6308caee87e043e859e7e553b9887453/examples/customPlot.py return def examplePixelLoader(): # prepare QGIS environment if sys.platform == 'darwin': PATH_QGS = r'/Applications/QGIS.app/Contents/MacOS' os.environ['GDAL_DATA'] = r'/usr/local/Cellar/gdal/1.11.3_1/share' else: # assume OSGeo4W startup PATH_QGS = os.environ['QGIS_PREFIX_PATH'] assert os.path.exists(PATH_QGS) qgsApp = QgsApplication([], True) QApplication.addLibraryPath(r'/Applications/QGIS.app/Contents/PlugIns') QApplication.addLibraryPath(r'/Applications/QGIS.app/Contents/PlugIns/qgis') qgsApp.setPrefixPath(PATH_QGS, True) qgsApp.initQgis() gb = QGroupBox() gb.setTitle('Sandbox') PL = PixelLoader() PL.setNumberOfThreads(2) if False: files = ['observationcloud/testdata/2014-07-26_LC82270652014207LGN00_BOA.bsq', 'observationcloud/testdata/2014-08-03_LE72270652014215CUB00_BOA.bsq' ] else: from timeseriesviewer.utils import file_search searchDir = r'H:\LandsatData\Landsat_NovoProgresso' files = file_search(searchDir, '*227065*band4.img', recursive=True) #files = files[0:3] lyr = QgsRasterLayer(files[0]) coord = lyr.extent().center() crs = lyr.crs() l = QVBoxLayout() btnStart = QPushButton() btnStop = QPushButton() prog = QProgressBar() tboxResults = QPlainTextEdit() tboxResults.setMaximumHeight(300) tboxThreads = QPlainTextEdit() tboxThreads.setMaximumHeight(200) label = QLabel() label.setText('Progress') def showProgress(n,m,md): prog.setMinimum(0) prog.setMaximum(m) prog.setValue(n) info = [] for k, v in md.items(): info.append('{} = {}'.format(k,str(v))) tboxResults.setPlainText('\n'.join(info)) #tboxThreads.setPlainText(PL.threadInfo()) qgsApp.processEvents() PL.sigPixelLoaded.connect(showProgress) btnStart.setText('Start loading') btnStart.clicked.connect(lambda : PL.startLoading(files, coord, crs)) btnStop.setText('Cancel') btnStop.clicked.connect(lambda: PL.cancelLoading()) lh = QHBoxLayout() lh.addWidget(btnStart) lh.addWidget(btnStop) l.addLayout(lh) l.addWidget(prog) l.addWidget(tboxThreads) l.addWidget(tboxResults) gb.setLayout(l) gb.show() #rs.setBackgroundStyle('background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #222, stop:1 #333);') #rs.handle.setStyleSheet('background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #282, stop:1 #393);') qgsApp.exec_() qgsApp.exitQgis() if __name__ == '__main__': import site, sys from timeseriesviewer import utils qgsApp = utils.initQgisApplication() DEBUG = False if False: #the ultimative test for floating point division correctness, at least on a DOY-level date1 = np.datetime64('1960-12-31','D') assert date1 == num2date(date2num(date1)) #1960 - 12 - 31 for year in range(1960, 2057): for doy in range(1, daysPerYear(year)+1): dt = datetime.timedelta(days=doy - 1) date1 = np.datetime64('{}-01-01'.format(year)) + np.timedelta64(doy-1,'D') date2 = datetime.date(year=year, month=1, day=1) + datetime.timedelta(days=doy-1) assert date1 == num2date(date2num(date1), dt64=True), 'date1: {}'.format(date1) assert date2 == num2date(date2num(date2), dt64=False), 'date2: {}'.format(date1) ui = ProfileViewDockUI() ui.show() if True: TS = TimeSeries() STVis = SpectralTemporalVisualization(ui) STVis.setTimeSeries(TS) import example.Images from timeseriesviewer import file_search files = file_search(os.path.dirname(example.Images.__file__), '*.tif') TS.addFiles(files) ext = TS.getMaxSpatialExtent() cp1 = SpatialPoint(ext.crs(),ext.center()) cpND = SpatialPoint(ext.crs(), 681151.214,-752388.476) cp2 = SpatialPoint(ext.crs(), ext.center()) cp3 = SpatialPoint(ext.crs(), ext.center().x()+500, ext.center().y()+250) STVis.loadCoordinate(cpND) STVis.loadCoordinate(cp2) STVis.loadCoordinate(cp3) STVis.createNewPlotStyle() if False: for tp in STVis.tpCollection: assert isinstance(tp, TemporalProfile) tp.plot() qgsApp.exec_() qgsApp.exitQgis()