diff --git a/eotimeseriesviewer/__init__.py b/eotimeseriesviewer/__init__.py index 8e05bc99c718e3ae41f54aa64ed110b0ce796312..dc91c11e597194b65227d54e464eccdd09927a37 100644 --- a/eotimeseriesviewer/__init__.py +++ b/eotimeseriesviewer/__init__.py @@ -45,8 +45,6 @@ from qgis.PyQt.QtGui import QIcon mkdir = lambda p: os.makedirs(p, exist_ok=True) - - DIR = os.path.dirname(__file__) DIR_REPO = os.path.dirname(DIR) DIR_UI = jp(DIR, *['ui']) diff --git a/eotimeseriesviewer/pixelloader.py b/eotimeseriesviewer/pixelloader.py index 4eaaea80b7b6ec4280f546559a50e2e507c3b237..e75241b88a4704b0ff1869f60471adb6ccc5e7f3 100644 --- a/eotimeseriesviewer/pixelloader.py +++ b/eotimeseriesviewer/pixelloader.py @@ -199,7 +199,7 @@ def doLoaderTask(taskWrapper:QgsTask, dump): nb, ns, nl = ds.RasterCount, ds.RasterXSize, ds.RasterYSize bandIndices = list(range(nb)) if task.bandIndices is None else list(task.bandIndices) - #ensure to load valid indices only + # ensure to load valid indices only bandIndices = [i for i in bandIndices if i >= 0 and i < nb] task.bandIndices = bandIndices diff --git a/eotimeseriesviewer/profilevisualization.py b/eotimeseriesviewer/profilevisualization.py index eb3c906e8d03c8fb93b7b42f166f32f0b72bcf38..47640c9d376dd4edd0cb97eb399d4148b34fbd05 100644 --- a/eotimeseriesviewer/profilevisualization.py +++ b/eotimeseriesviewer/profilevisualization.py @@ -31,7 +31,7 @@ from qgis.PyQt.QtGui import * from .timeseries import * from .utils import SpatialExtent, SpatialPoint, px2geo, loadUI, nextColor -from .externals.qps.plotstyling.plotstyling import PlotStyle, PlotStyleButton +from .externals.qps.plotstyling.plotstyling import PlotStyle, PlotStyleButton, PlotStyleDialog from .externals import pyqtgraph as pg from .sensorvisualization import SensorListModel from .temporalprofiles2d import * @@ -483,9 +483,17 @@ class PlotSettingsModel2D(QAbstractTableModel): self.columnNames = [self.cnTemporalProfile, self.cnSensor, self.cnStyle, self.cnExpression] self.mPlotSettings = [] + self.mIconSize = QSize(25, 25) self.dataChanged.connect(self.signaler) + def setIconSize(self, size:QSize): + assert isinstance(size, QSize) + self.mIconSize = size + tl = self.createIndex(0, self.columnIndex(self.cnStyle)) + lr = self.createIndex(self.rowCount(), self.columnIndex(self.cnStyle)) + self.dataChanged.emit(tl, lr, [Qt.DecorationRole]) + def __len__(self): return len(self.mPlotSettings) @@ -646,13 +654,13 @@ class PlotSettingsModel2D(QAbstractTableModel): elif columnName == self.cnTemporalProfile: tp = plotStyle.temporalProfile() if isinstance(tp, TemporalProfile): - value = tp.name() + value = '{} {}'.format(tp.id(), tp.name()) else: value = 'undefined' if role == Qt.DecorationRole: if columnName == self.cnStyle: - value = plotStyle.createIcon(QSize(25,25)) + value = plotStyle.createIcon(self.mIconSize) elif role == Qt.CheckStateRole: if columnName == self.cnTemporalProfile: @@ -753,1183 +761,1249 @@ class PlotSettingsModel2D(QAbstractTableModel): -class ProfileViewDockUI(QgsDockWidget, loadUI('profileviewdock.ui')): +class PlotSettingsTableView(QTableView): - def __init__(self, parent=None): - super(ProfileViewDockUI, self).__init__(parent) - self.setupUi(self) + def __init__(self, *args, **kwds): + super(PlotSettingsTableView, self).__init__(*args, **kwds) - self.addActions(self.findChildren(QAction)) + def contextMenuEvent(self, event: QContextMenuEvent): + """ + Creates and shows the QMenu + :param event: QContextMenuEvent + """ - self.mActions2D = [self.actionAddStyle2D, self.actionRemoveStyle2D, self.actionRefresh2D, self.actionReset2DPlot] - self.mActions3D = [self.actionAddStyle3D, self.actionRemoveStyle3D, self.actionRefresh3D, - self.actionReset3DCamera] - self.mActionsTP = [self.actionLoadTPFromOgr, self.actionSaveTemporalProfiles, self.actionToggleEditing, - self.actionRemoveTemporalProfile, self.actionLoadMissingValues] - #TBD. - #self.line.setVisible(False) - #self.listWidget.setVisible(False) - self.baseTitle = self.windowTitle() - self.stackedWidget.currentChanged.connect(self.onStackPageChanged) + indices = self.selectionModel().selectedIndexes() + indices2 = [self.model().mapToSource(idx) for idx in indices] - self.plotWidget3D = None - self.plotWidget3DMPL = None + if len(indices2) > 0: + menu = QMenu(self) + a = menu.addAction('Set Style') + a.triggered.connect(lambda *args, i=indices2: self.onSetStyle(i)) - self.init3DWidgets('gl') + menu.popup(QCursor.pos()) + def onSetStyle(self, indices): - #pi = self.plotWidget2D.plotItem - #ax = DateAxis(orientation='bottom', showValues=True) - #pi.layout.addItem(ax, 3,2) + if len(indices) > 0: + refStyle = None + model = self.model().sourceModel() + assert isinstance(model, PlotSettingsModel2D) - self.TS = None + colStyle = model.columnIndex(model.cnStyle) - self.progressBar.setMinimum(0) - self.progressBar.setMaximum(100) - self.progressBar.setValue(0) - self.progressInfo.setText('') - self.pxViewModel2D = None - self.pxViewModel3D = None + for idx in indices: + style = self.model().sourceModel().idx2plotStyle(idx) + if isinstance(style, PlotStyle): + refStyle = style + break + if isinstance(refStyle, PlotStyle): + newStyle = PlotStyleDialog.getPlotStyle(plotStyle=refStyle) + if isinstance(newStyle, PlotStyle): + for idx in indices: + assert isinstance(idx, QModelIndex) + idxStyle = model.createIndex(idx.row(), colStyle) + model.setData(idxStyle, newStyle, role=Qt.EditRole) - self.tableView2DProfiles.horizontalHeader().setResizeMode(QHeaderView.Interactive) - def init3DWidgets(self, mode='gl'): - assert mode in ['gl'] - l = self.frame3DPlot.layout() - if ENABLE_OPENGL and OPENGL_AVAILABLE and mode == 'gl': +class PlotSettingsModel2DWidgetDelegate(QStyledItemDelegate): + """ - from eotimeseriesviewer.temporalprofiles3dGL import ViewWidget3D - self.plotWidget3D = ViewWidget3D(parent=self.labelDummy3D.parent()) - self.plotWidget3D.setObjectName('plotWidget3D') + """ + def __init__(self, tableView, temporalProfileLayer, parent=None): + assert isinstance(tableView, QTableView) + assert isinstance(temporalProfileLayer, TemporalProfileLayer) + super(PlotSettingsModel2DWidgetDelegate, self).__init__(parent=parent) + self._preferedSize = QgsFieldExpressionWidget().sizeHint() + self.mTableView = tableView + self.mTemporalProfileLayer = temporalProfileLayer + self.mTimeSeries = temporalProfileLayer.timeSeries() - 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]) - self.frameSettings3D.setEnabled(True) - else: - self.frameSettings3D.setEnabled(False) + self.mSensorLayers = {} - def onStackPageChanged(self, i): - w = self.stackedWidget.currentWidget() - title = self.baseTitle - if w == self.page2D: - title = '{} | 2D'.format(title) - for a in self.mActions2D: - a.setVisible(True) - for a in self.mActions3D + self.mActionsTP: - a.setVisible(False) - elif w == self.page3D: - title = '{} | 3D (experimental!)'.format(title) - for a in self.mActions2D + self.mActionsTP: - a.setVisible(False) - for a in self.mActions3D: - a.setVisible(True) - elif w == self.pagePixel: - title = '{} | Coordinates'.format(title) - for a in self.mActions2D + self.mActions3D: - a.setVisible(False) - for a in self.mActionsTP: - a.setVisible(True) + def paint(self, painter: QPainter, option: 'QStyleOptionViewItem', index: QtCore.QModelIndex): - w.update() - self.setWindowTitle(title) + if index.column() == 2: + style = self.style(index) + + h = self.mTableView.verticalHeader().defaultSectionSize() + w = self.mTableView.horizontalHeader().defaultSectionSize() + if h > 0 and w > 0: + px = style.createPixmap(size=QSize(w, h)) + label = QLabel() + label.setPixmap(px) + painter.drawPixmap(option.rect, px) + #QApplication.style().drawControl(QStyle.CE_CustomBase, label, painter) + else: + super(PlotSettingsModel2DWidgetDelegate, self).paint(painter, option, index) + else: + super(PlotSettingsModel2DWidgetDelegate, self).paint(painter, option, index) + def sortFilterProxyModel(self)->QSortFilterProxyModel: + return self.mTableView.model() -class SpectralTemporalVisualization(QObject): + def plotSettingsModel(self)->PlotSettingsModel2D: + return self.sortFilterProxyModel().sourceModel() - sigShowPixel = pyqtSignal(TimeSeriesDatum, QgsPoint, QgsCoordinateReferenceSystem) + def setItemDelegates(self, tableView): + assert isinstance(tableView, QTableView) + model = self.plotSettingsModel() + + assert isinstance(model, PlotSettingsModel2D) + for c in [model.cnSensor, model.cnExpression, model.cnStyle, model.cnTemporalProfile]: + i = model.columnNames.index(c) + tableView.setItemDelegateForColumn(i, self) + def getColumnName(self, index): + assert index.isValid() + model = self.plotSettingsModel() + assert isinstance(model, PlotSettingsModel2D) + return model.columnNames[index.column()] """ - Signalizes to move to specific date of interest + 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 """ - sigMoveToDate = pyqtSignal(np.datetime64) + def exampleLyr(self, sensor): + # if 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') + 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 __init__(self, timeSeries, profileDock): - super(SpectralTemporalVisualization, self).__init__() + def createEditor(self, parent, option, index): + cname = self.getColumnName(index) + model = self.plotSettingsModel() + pmodel = self.sortFilterProxyModel() - assert isinstance(profileDock, ProfileViewDockUI) - self.ui = profileDock + w = None + if index.isValid() and isinstance(model, PlotSettingsModel2D): - import eotimeseriesviewer.pixelloader - if DEBUG: - eotimeseriesviewer.pixelloader.DEBUG = True + plotStyle = model.idx2plotStyle(pmodel.mapToSource(index)) - #the timeseries. will be set later - assert isinstance(timeSeries, TimeSeries) - self.TS = timeSeries - self.plot_initialized = False + if isinstance(plotStyle, TemporalProfile2DPlotStyle): + if cname == model.cnExpression: - self.plot2D = self.ui.plotWidget2D - self.plot2D.getViewBox().sigMoveToDate.connect(self.sigMoveToDate) - self.plot3D = self.ui.plotWidget3D + w = QgsFieldExpressionWidget(parent=parent) + 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())) + w.setExpression(plotStyle.expression()) + plotStyle.sigSensorChanged.connect(lambda s: w.setLayer(self.exampleLyr(s))) + if isinstance(plotStyle.sensor(), SensorInstrument): + w.setLayer(self.exampleLyr(plotStyle.sensor())) - # temporal profile collection to store loaded values - self.mTemporalProfileLayer = TemporalProfileLayer(self.TS) - self.mTemporalProfileLayer.sigTemporalProfilesAdded.connect(self.onTemporalProfilesAdded) - #self.mTemporalProfileLayer.startEditing() - self.mTemporalProfileLayer.selectionChanged.connect(self.onTemporalProfileSelectionChanged) + 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())) - # fix to not loose C++ reference on temporal profile layer in case it is removed from QGIS mapcanvas - self.mMapCanvas = QgsMapCanvas() - self.mMapCanvas.setVisible(False) - self.mMapCanvas.setLayers([self.mTemporalProfileLayer]) - # self.tpCollectionListModel = TemporalProfileCollectionListModel(self.tpCollection) + elif cname == model.cnSensor: + w = QComboBox(parent=parent) + m = SensorListModel(self.mTimeSeries) + w.setModel(m) - assert isinstance(self.ui.mDualView, QgsDualView) - self.ui.mDualView.init(self.mTemporalProfileLayer, self.mMapCanvas) - self.ui.mDualView.setView(QgsDualView.AttributeTable) - # pixel loader to load pixel values in parallel - config = QgsAttributeTableConfig() - config.update(self.mTemporalProfileLayer.fields()) - config.setActionWidgetVisible(True) + elif cname == model.cnTemporalProfile: + w = QgsFeatureListComboBox(parent=parent) + w.setSourceLayer(self.mTemporalProfileLayer) + w.setIdentifierField(FN_ID) + w.setDisplayExpression('to_string("{}")+\' \'+"name"'.format(FN_ID)) + w.setAllowNull(False) + else: + raise NotImplementedError() + return w - hidden = [FN_ID] - for i, columnConfig in enumerate(config.columns()): + def checkData(self, index, w, value): + 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 value == w.expression() + assert w.isExpressionValid(value) == w.isValidExpression() - assert isinstance(columnConfig, QgsAttributeTableConfig.ColumnConfig) - config.setColumnHidden(i, columnConfig.name in hidden) + if w.isValidExpression(): + self.commitData.emit(w) + else: + s = "" + #print(('Delegate commit failed',w.asExpression())) + if isinstance(w, PlotStyleButton): + self.commitData.emit(w) - self.mTemporalProfilesTableConfig = config - self.mTemporalProfileLayer.setAttributeTableConfig(self.mTemporalProfilesTableConfig) + def style(self, proxyIndex:QModelIndex)->PlotStyle: + model = self.plotSettingsModel() + index = self.sortFilterProxyModel().mapToSource(proxyIndex) + return model.data(index, role=Qt.UserRole) - self.pixelLoader = PixelLoader() - self.pixelLoader.sigPixelLoaded.connect(self.onPixelLoaded) - self.pixelLoader.sigLoadingStarted.connect(lambda: self.ui.progressInfo.setText('Start loading...')) - # self.pixelLoader.sigLoadingStarted.connect(self.tpCollection.prune) - self.pixelLoader.sigLoadingFinished.connect(lambda: self.plot2D.enableAutoRange('x', False)) + def setEditorData(self, editor, proxyIndex): - # set the plot models for 2D - self.plotSettingsModel2D = PlotSettingsModel2D() - self.plotSettingsModel2DProxy = QSortFilterProxyModel() - self.plotSettingsModel2DProxy.setSourceModel(self.plotSettingsModel2D) - self.ui.tableView2DProfiles.setModel(self.plotSettingsModel2DProxy) - self.ui.tableView2DProfiles.setSortingEnabled(True) - self.ui.tableView2DProfiles.selectionModel().selectionChanged.connect(self.onPlot2DSelectionChanged) - self.ui.tableView2DProfiles.horizontalHeader().setResizeMode(QHeaderView.Interactive) - self.plotSettingsModel2D.sigDataChanged.connect(self.requestUpdate) - self.plotSettingsModel2D.rowsInserted.connect(self.onRowsInserted2D) - self.delegateTableView2D = PlotSettingsModel2DWidgetDelegate(self.ui.tableView2DProfiles, self.mTemporalProfileLayer) - self.delegateTableView2D.setItemDelegates(self.ui.tableView2DProfiles) + model = self.plotSettingsModel() + index = self.sortFilterProxyModel().mapToSource(proxyIndex) + w = None + if index.isValid() and isinstance(model, PlotSettingsModel2D): + cname = self.getColumnName(proxyIndex) + if cname == model.cnExpression: + lastExpr = model.data(index, Qt.DisplayRole) + assert isinstance(editor, QgsFieldExpressionWidget) + editor.setProperty('lastexpr', lastExpr) + editor.setField(lastExpr) - # set the plot models for 3D - self.plotSettingsModel3D = PlotSettingsModel3D() - self.ui.tableView3DProfiles.setModel(self.plotSettingsModel3D) - self.ui.tableView3DProfiles.setSortingEnabled(False) - self.ui.tableView2DProfiles.selectionModel().selectionChanged.connect(self.onPlot3DSelectionChanged) - self.ui.tableView3DProfiles.horizontalHeader().setResizeMode(QHeaderView.Interactive) - self.plotSettingsModel3D.rowsInserted.connect(self.onRowsInserted3D) - self.delegateTableView3D = PlotSettingsModel3DWidgetDelegate(self.ui.tableView3DProfiles, self.mTemporalProfileLayer) - self.delegateTableView3D.setItemDelegates(self.ui.tableView3DProfiles) + elif cname == model.cnStyle: + style = model.data(index, Qt.UserRole) + assert isinstance(editor, PlotStyleButton) + editor.setPlotStyle(style) - if not ENABLE_OPENGL: - self.ui.listWidget.item(1).setHidden(True) - self.ui.page3D.setHidden(True) + 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, QgsFeatureListComboBox) + value = editor.identifierValue() + if value != QVariant(): + plotStyle = index.data(role=Qt.UserRole) + TP = plotStyle.temporalProfile() + editor.setIdentifierValue(TP.id()) + else: + s = "" + else: + raise NotImplementedError() + def setModelData(self, w, model, proxyIndex): + index = self.sortFilterProxyModel().mapToSource(proxyIndex) + cname = self.getColumnName(proxyIndex) + model = self.plotSettingsModel() + if index.isValid() and isinstance(model, PlotSettingsModel2D): + if cname == model.cnExpression: + assert isinstance(w, QgsFieldExpressionWidget) + expr = w.asExpression() + exprLast = model.data(index, Qt.DisplayRole) - def onTemporalProfilesRemoved(removedProfiles): - #set to valid temporal profile + if w.isValidExpression() and expr != exprLast: + model.setData(index, w.asExpression(), Qt.EditRole) - affectedStyles2D = [p for p in self.plotSettingsModel2D if p.temporalProfile() in removedProfiles] - affectedStyles3D = [p for p in self.plotSettingsModel3D if p.temporalProfile() in removedProfiles] - alternativeProfile = self.mTemporalProfileLayer[-1] if len(self.mTemporalProfileLayer) > 0 else None - for s in affectedStyles2D: - assert isinstance(s, TemporalProfile2DPlotStyle) - m = self.plotSettingsModel2D - idx = m.plotStyle2idx(s) - idx = m.createIndex(idx.row(), m.columnNames.index(m.cnTemporalProfile)) - m.setData(idx, alternativeProfile, Qt.EditRole) + elif cname == model.cnStyle: + if isinstance(w, PlotStyleButton): + style = w.plotStyle() + model.setData(index, style, Qt.EditRole) - for s in affectedStyles3D: - assert isinstance(s, TemporalProfile3DPlotStyle) - m = self.plotSettingsModel3D - idx = m.plotStyle2idx(s) - idx = m.createIndex(idx.row(), m.columnNames.index(m.cnTemporalProfile)) - m.setData(idx, alternativeProfile, Qt.EditRole) + elif cname == model.cnSensor: + assert isinstance(w, QComboBox) + sensor = w.itemData(w.currentIndex(), role=Qt.UserRole) + if isinstance(sensor, SensorInstrument): + model.setData(index, sensor, Qt.EditRole) - self.mTemporalProfileLayer.sigTemporalProfilesRemoved.connect(onTemporalProfilesRemoved) + elif cname == model.cnTemporalProfile: + assert isinstance(w, QgsFeatureListComboBox) + fid = w.identifierValue() + if isinstance(fid, int): + TP = self.mTemporalProfileLayer.mProfiles.get(fid) + model.setData(index, TP, Qt.EditRole) - # remove / add plot style - def on2DPlotStyleRemoved(plotStyles): - for plotStyle in plotStyles: - assert isinstance(plotStyle, PlotStyle) - for pi in plotStyle.mPlotItems: - self.plot2D.plotItem.removeItem(pi) + else: + raise NotImplementedError() - def on3DPlotStyleRemoved(plotStyles): - toRemove = [] - for plotStyle in plotStyles: - assert isinstance(plotStyle, TemporalProfile3DPlotStyle) - toRemove.append(plotStyle.mPlotItems) - self.plot3D.removeItems(toRemove) - self.plotSettingsModel2D.sigPlotStylesRemoved.connect(on2DPlotStyleRemoved) - self.plotSettingsModel3D.sigPlotStylesRemoved.connect(on3DPlotStyleRemoved) +class PlotSettingsModel3DWidgetDelegate(QStyledItemDelegate): + """ - # initialize the update loop - self.updateRequested = True - self.updateTimer = QTimer(self) - self.updateTimer.timeout.connect(self.onDataUpdate) - self.updateTimer.start(2000) + """ + def __init__(self, tableView, temporalProfileLayer, parent=None): + assert isinstance(tableView, QTableView) + assert isinstance(temporalProfileLayer, TemporalProfileLayer) + super(PlotSettingsModel3DWidgetDelegate, self).__init__(parent=parent) + self._preferedSize = QgsFieldExpressionWidget().sizeHint() + self.mTableView = tableView + self.mTimeSeries = temporalProfileLayer.timeSeries() + self.mTemporalProfileLayer = temporalProfileLayer + self.mSensorLayers = {} - self.sigMoveToDate.connect(self.onMoveToDate) + def setItemDelegates(self, tableView): + assert isinstance(tableView, QTableView) + model = tableView.model() + assert isinstance(model, PlotSettingsModel3D) + for c in [model.cnSensor, + model.cnExpression, + # model.cnStyle, + model.cnTemporalProfile]: + i = model.columnNames.index(c) + tableView.setItemDelegateForColumn(i, self) - self.initActions() - #self.ui.stackedWidget.setCurrentPage(self.ui.pagePixel) - self.ui.onStackPageChanged(self.ui.stackedWidget.currentIndex()) + def getColumnName(self, index): + assert index.isValid() + model = index.model() + assert isinstance(model, PlotSettingsModel3D) + return model.columnNames[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.cnExpression: + w = QgsFieldExpressionWidget(parent=parent) + w.setExpression(plotStyle.expression()) + w.setLayer(self.exampleLyr(plotStyle.sensor())) + def onSensorAdded(s): + w.setLayer(self.exampleLyr(s)) + #plotStyle.sigSensorChanged.connect(lambda s : w.setLayer(self.exampleLyr(s))) + plotStyle.sigSensorChanged.connect(onSensorAdded) + 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())) - def temporalProfileLayer(self)->TemporalProfileLayer: - """ - Returns a QgsVectorLayer that is used to store profile coordinates. - :return: - """ - return self.mTemporalProfileLayer + elif cname == model.cnStyle: + w = TemporalProfile3DPlotStyleButton(parent=parent) + w.setPlotStyle(plotStyle) + w.setToolTip('Set plot style') + w.sigPlotStyleChanged.connect(lambda: self.checkData(index, w, w.plotStyle())) - def onTemporalProfilesContextMenu(self, event): - assert isinstance(event, QContextMenuEvent) - tableView = self.ui.tableViewTemporalProfiles - selectionModel = self.ui.tableViewTemporalProfiles.selectionModel() - assert isinstance(selectionModel, QItemSelectionModel) + elif cname == model.cnSensor: + w = QComboBox(parent=parent) + m = SensorListModel(self.mTimeSeries) + w.setModel(m) - model = self.ui.tableViewTemporalProfiles.model() - assert isinstance(model, TemporalProfileLayer) + elif cname == model.cnTemporalProfile: + w = QgsFeatureListComboBox(parent=parent) + w.setSourceLayer(self.mTemporalProfileLayer) + w.setIdentifierField('id') + w.setDisplayExpression('to_string("id")+\' \'+"name"') + w.setAllowNull(False) + else: + raise NotImplementedError() + return w - temporalProfiles = [] + def exampleLyr(self, sensor): - if len(selectionModel.selectedIndexes()) > 0: - for idx in selectionModel.selectedIndexes(): - tp = model.idx2tp(idx) - if isinstance(tp, TemporalProfile) and not tp in temporalProfiles: - temporalProfiles.append(tp) - else: - temporalProfiles = model[:] + if sensor not in self.mSensorLayers.keys(): + crs = QgsCoordinateReferenceSystem('EPSG:4862') + uri = 'Point?crs={}'.format(crs.authid()) + lyr = QgsVectorLayer(uri, 'LOCATIONS', 'memory') + assert sensor is None or isinstance(sensor, SensorInstrument) - spatialPoints = [tp.coordinate() for tp in temporalProfiles] + f = sensorExampleQgsFeature(sensor, singleBandOnly=True) + 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 checkData(self, index, w, value): + 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, QgsFieldExpressionWidget): + assert value == w.expression() + assert w.isExpressionValid(value) == w.isValidExpression() - menu = QMenu() + if w.isValidExpression(): + self.commitData.emit(w) + else: + s = "" + #print(('Delegate commit failed',w.asExpression())) + if isinstance(w, TemporalProfile3DPlotStyleButton): - a = menu.addAction('Load missing') - a.setToolTip('Loads missing band-pixels.') - a.triggered.connect(lambda : self.loadCoordinate(spatialPoints=spatialPoints, mode='all')) - s = "" + self.commitData.emit(w) - a = menu.addAction('Reload') - a.setToolTip('Reloads all band-pixels.') - a.triggered.connect(lambda: self.loadCoordinate(spatialPoints=spatialPoints, mode='reload')) - menu.popup(tableView.viewport().mapToGlobal(event.pos())) - self.menu = menu + def setEditorData(self, editor, index): + cname = self.getColumnName(index) + model = self.mTableView.model() + w = None + if index.isValid() and isinstance(model, PlotSettingsModel3D): + cname = self.getColumnName(index) + style = model.idx2plotStyle(index) - def selected2DPlotStyles(self): - result = [] + if cname == model.cnExpression: + lastExpr = index.model().data(index, Qt.DisplayRole) + assert isinstance(editor, QgsFieldExpressionWidget) + editor.setProperty('lastexpr', lastExpr) + editor.setField(lastExpr) - m = self.ui.tableView2DProfiles.model() - for idx in selectedModelIndices(self.ui.tableView2DProfiles): - result.append(m.data(idx, Qt.UserRole)) - return result + elif cname == model.cnStyle: + assert isinstance(editor, TemporalProfile3DPlotStyleButton) + 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, QgsFeatureListComboBox) + value = editor.identifierValue() + if value != QVariant(): + plotStyle = index.data(role=Qt.UserRole) + TP = plotStyle.temporalProfile() + editor.setIdentifierValue(TP.id()) + else: + s = "" + else: + raise NotImplementedError() - def removePlotStyles2D(self, plotStyles): - m = self.ui.tableView2DProfiles.model() - if isinstance(m.sourceModel(), PlotSettingsModel2D): - m.sourceModel().removePlotStyles(plotStyles) + def setModelData(self, w, model, index): + cname = self.getColumnName(index) + model = self.mTableView.model() + + if index.isValid() and isinstance(model, PlotSettingsModel3D): + 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) - def removeTemporalProfiles(self, fids): + elif cname == model.cnStyle: + assert isinstance(w, TemporalProfile3DPlotStyleButton) + model.setData(index, w.plotStyle(), Qt.EditRole) - self.mTemporalProfileLayer.selectByIds(fids) - b = self.mTemporalProfileLayer.isEditable() - self.mTemporalProfileLayer.startEditing() - self.mTemporalProfileLayer.deleteSelectedFeatures() - self.mTemporalProfileLayer.saveEdits(leaveEditable=b) + 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) - def createNewPlotStyle2D(self): - l = len(self.mTemporalProfileLayer) + s = "" + elif cname == model.cnTemporalProfile: + assert isinstance(w, QgsFeatureListComboBox) + fid = w.identifierValue() + if isinstance(fid, int): + TP = self.mTemporalProfileLayer.mProfiles.get(fid) + model.setData(index, TP, Qt.EditRole) - plotStyle = TemporalProfile2DPlotStyle() - plotStyle.sigExpressionUpdated.connect(self.updatePlot2D) + else: + raise NotImplementedError() - sensors = self.TS.sensors() - if len(sensors) > 0: - plotStyle.setSensor(sensors[0]) - if len(self.mTemporalProfileLayer) > 0: - temporalProfile = self.mTemporalProfileLayer[0] - plotStyle.setTemporalProfile(temporalProfile) +class ProfileViewDockUI(QgsDockWidget, loadUI('profileviewdock.ui')): - if len(self.plotSettingsModel2D) > 0: - lastStyle = self.plotSettingsModel2D[0] #top style in list is the most-recent - assert isinstance(lastStyle, TemporalProfile2DPlotStyle) - markerColor = nextColor(lastStyle.markerBrush.color()) - plotStyle.markerBrush.setColor(markerColor) - self.plotSettingsModel2D.insertPlotStyles([plotStyle], i=0) - pdi = plotStyle.createPlotItem(self.plot2D) - assert isinstance(pdi, TemporalProfilePlotDataItem) - pdi.sigClicked.connect(self.onProfileClicked2D) - pdi.sigPointsClicked.connect(self.onPointsClicked2D) - self.plot2D.plotItem.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() - return plotStyle + def __init__(self, parent=None): + super(ProfileViewDockUI, self).__init__(parent) + self.setupUi(self) + self.addActions(self.findChildren(QAction)) - def createNewPlotStyle3D(self): - if not (ENABLE_OPENGL and OPENGL_AVAILABLE): - return + self.mActions2D = [self.actionAddStyle2D, self.actionRemoveStyle2D, self.actionRefresh2D, self.actionReset2DPlot] + self.mActions3D = [self.actionAddStyle3D, self.actionRemoveStyle3D, self.actionRefresh3D, + self.actionReset3DCamera] + self.mActionsTP = [self.actionLoadTPFromOgr, self.actionSaveTemporalProfiles, self.actionToggleEditing, + self.actionRemoveTemporalProfile, self.actionLoadMissingValues] + #TBD. + #self.line.setVisible(False) + #self.listWidget.setVisible(False) + self.baseTitle = self.windowTitle() + self.stackedWidget.currentChanged.connect(self.onStackPageChanged) - plotStyle = TemporalProfile3DPlotStyle() - plotStyle.sigExpressionUpdated.connect(self.updatePlot3D) + self.plotWidget3D = None + self.plotWidget3DMPL = None - if len(self.mTemporalProfileLayer) > 0: - temporalProfile = self.mTemporalProfileLayer[0] - plotStyle.setTemporalProfile(temporalProfile) - if len(self.plotSettingsModel3D) > 0: - color = self.plotSettingsModel3D[-1].color() - plotStyle.setColor(nextColor(color)) + self.init3DWidgets('gl') - sensors = list(self.TS.mSensors2TSDs.keys()) - if len(sensors) > 0: - plotStyle.setSensor(sensors[0]) + #pi = self.plotWidget2D.plotItem + #ax = DateAxis(orientation='bottom', showValues=True) + #pi.layout.addItem(ax, 3,2) - self.plotSettingsModel3D.insertPlotStyles([plotStyle], i=0) # latest to the top - plotItems = plotStyle.createPlotItem() - self.plot3D.addItems(plotItems) - #self.updatePlot3D() - def onProfileClicked2D(self, pdi): - if isinstance(pdi, TemporalProfilePlotDataItem): - sensor = pdi.mPlotStyle.sensor() - tp = pdi.mPlotStyle.temporalProfile() - if isinstance(tp, TemporalProfile) and isinstance(sensor, SensorInstrument): - c = tp.coordinate() - info = ['Sensor:{}'.format(sensor.name()), - 'Coordinate:{}, {}'.format(c.x(), c.y())] - self.ui.tbInfo2D.setPlainText('\n'.join(info)) + self.TS = None + self.progressBar.setMinimum(0) + self.progressBar.setMaximum(100) + self.progressBar.setValue(0) + self.progressInfo.setText('') + self.pxViewModel2D = None + self.pxViewModel3D = None - 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): - c = tp.coordinate() - info = ['Sensor: {}'.format(sensor.name()), - 'Coordinate: {}, {}'.format(c.x(), c.y())] + self.tableView2DProfiles.horizontalHeader().setResizeMode(QHeaderView.Interactive) - for item in spottedItems: - pos = item.pos() - x = pos.x() - y = pos.y() - date = num2date(x) - info.append('Date: {}\nValue: {}'.format(date, y)) + def init3DWidgets(self, mode='gl'): + assert mode in ['gl'] + l = self.frame3DPlot.layout() - self.ui.tbInfo2D.setPlainText('\n'.join(info)) + if ENABLE_OPENGL and OPENGL_AVAILABLE and mode == 'gl': - def onTemporalProfilesAdded(self, profiles): - # self.mTemporalProfileLayer.prune() - for plotStyle in self.plotSettingsModel3D: - assert isinstance(plotStyle, TemporalProfilePlotStyleBase) - if not isinstance(plotStyle.temporalProfile(), TemporalProfile): + from eotimeseriesviewer.temporalprofiles3dGL import ViewWidget3D + self.plotWidget3D = ViewWidget3D(parent=self.labelDummy3D.parent()) + self.plotWidget3D.setObjectName('plotWidget3D') - r = self.plotSettingsModel3D.plotStyle2idx(plotStyle).row() - c = self.plotSettingsModel3D.columnIndex(self.plotSettingsModel3D.cnTemporalProfile) - idx = self.plotSettingsModel3D.createIndex(r, c) - self.plotSettingsModel3D.setData(idx, self.mTemporalProfileLayer[0]) + 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]) + self.frameSettings3D.setEnabled(True) + else: + self.frameSettings3D.setEnabled(False) - def onTemporalProfileSelectionChanged(self, selectedFIDs, deselectedFIDs): - nSelected = len(selectedFIDs) + def onStackPageChanged(self, i): + w = self.stackedWidget.currentWidget() + title = self.baseTitle + if w == self.page2D: + title = '{} | 2D'.format(title) + for a in self.mActions2D: + a.setVisible(True) + for a in self.mActions3D + self.mActionsTP: + a.setVisible(False) + elif w == self.page3D: + title = '{} | 3D (experimental!)'.format(title) + for a in self.mActions2D + self.mActionsTP: + a.setVisible(False) + for a in self.mActions3D: + a.setVisible(True) + elif w == self.pagePixel: + title = '{} | Coordinates'.format(title) + for a in self.mActions2D + self.mActions3D: + a.setVisible(False) + for a in self.mActionsTP: + a.setVisible(True) - self.ui.actionRemoveTemporalProfile.setEnabled(nSelected > 0) + w.update() + self.setWindowTitle(title) - def onPlot2DSelectionChanged(self, selected, deselected): - self.ui.actionRemoveStyle2D.setEnabled(len(selected) > 0) - def onPlot3DSelectionChanged(self, selected, deselected): +class SpectralTemporalVisualization(QObject): - self.ui.actionRemoveStyle3D.setEnabled(len(selected) > 0) + sigShowPixel = pyqtSignal(TimeSeriesDatum, QgsPoint, QgsCoordinateReferenceSystem) - def initActions(self): + """ + Signalizes to move to specific date of interest + """ + sigMoveToDate = pyqtSignal(np.datetime64) - self.ui.actionRemoveStyle2D.setEnabled(False) - self.ui.actionRemoveTemporalProfile.setEnabled(False) - self.ui.actionAddStyle2D.triggered.connect(self.createNewPlotStyle2D) - self.ui.actionAddStyle3D.triggered.connect(self.createNewPlotStyle3D) - self.ui.actionRefresh2D.triggered.connect(self.updatePlot2D) - self.ui.actionRefresh3D.triggered.connect(self.updatePlot3D) - self.ui.actionRemoveStyle2D.triggered.connect(lambda:self.removePlotStyles2D(self.selected2DPlotStyles())) - self.ui.actionRemoveTemporalProfile.triggered.connect(lambda :self.removeTemporalProfiles(self.mTemporalProfileLayer.selectedFeatureIds())) - self.ui.actionToggleEditing.triggered.connect(self.onToggleEditing) - self.ui.actionReset2DPlot.triggered.connect(self.plot2D.resetViewBox) - self.plot2D.resetTransform() - self.ui.actionReset3DCamera.triggered.connect(self.reset3DCamera) - self.ui.actionLoadTPFromOgr.triggered.connect(lambda : self.mTemporalProfileLayer.loadCoordinatesFromOgr(None)) - self.ui.actionLoadMissingValues.triggered.connect(lambda: self.loadMissingData()) - self.ui.actionSaveTemporalProfiles.triggered.connect(lambda *args : self.mTemporalProfileLayer.saveTemporalProfiles) - #set actions to be shown in the TemporalProfileTableView context menu - ma = [self.ui.actionSaveTemporalProfiles, self.ui.actionLoadMissingValues] + def __init__(self, timeSeries, profileDock): + super(SpectralTemporalVisualization, self).__init__() - self.onEditingToggled() + assert isinstance(profileDock, ProfileViewDockUI) + self.ui = profileDock - def onSaveTemporalProfiles(self): + import eotimeseriesviewer.pixelloader + if DEBUG: + eotimeseriesviewer.pixelloader.DEBUG = True - s = "" + #the timeseries. will be set later + assert isinstance(timeSeries, TimeSeries) + self.TS = timeSeries + self.plot_initialized = False - def onToggleEditing(self, b): + self.plot2D = self.ui.plotWidget2D + self.plot2D.getViewBox().sigMoveToDate.connect(self.sigMoveToDate) + self.plot3D = self.ui.plotWidget3D - if self.mTemporalProfileLayer.isEditable(): - self.mTemporalProfileLayer.saveEdits(leaveEditable=False) - else: - self.mTemporalProfileLayer.startEditing() - self.onEditingToggled() + # temporal profile collection to store loaded values + self.mTemporalProfileLayer = TemporalProfileLayer(self.TS) + self.mTemporalProfileLayer.sigTemporalProfilesAdded.connect(self.onTemporalProfilesAdded) + #self.mTemporalProfileLayer.startEditing() + self.mTemporalProfileLayer.selectionChanged.connect(self.onTemporalProfileSelectionChanged) - def onEditingToggled(self): - lyr = self.mTemporalProfileLayer + # fix to not loose C++ reference on temporal profile layer in case it is removed from QGIS mapcanvas + self.mMapCanvas = QgsMapCanvas() + self.mMapCanvas.setVisible(False) + self.mMapCanvas.setLayers([self.mTemporalProfileLayer]) + # self.tpCollectionListModel = TemporalProfileCollectionListModel(self.tpCollection) - hasSelectedFeatures = lyr.selectedFeatureCount() > 0 - isEditable = lyr.isEditable() - self.ui.actionToggleEditing.blockSignals(True) - self.ui.actionToggleEditing.setChecked(isEditable) - #self.actionSaveTemporalProfiles.setEnabled(isEditable) - #self.actionReload.setEnabled(not isEditable) - self.ui.actionToggleEditing.blockSignals(False) + assert isinstance(self.ui.mDualView, QgsDualView) + self.ui.mDualView.init(self.mTemporalProfileLayer, self.mMapCanvas) + self.ui.mDualView.setView(QgsDualView.AttributeTable) + # pixel loader to load pixel values in parallel + config = QgsAttributeTableConfig() + config.update(self.mTemporalProfileLayer.fields()) + config.setActionWidgetVisible(False) - #self.actionAddAttribute.setEnabled(isEditable) - #self.actionRemoveAttribute.setEnabled(isEditable) - self.ui.actionRemoveTemporalProfile.setEnabled(isEditable and hasSelectedFeatures) - #self.actionPasteFeatures.setEnabled(isEditable) - self.ui.actionToggleEditing.setEnabled(not lyr.readOnly()) + hidden = [] + for i, columnConfig in enumerate(config.columns()): + + assert isinstance(columnConfig, QgsAttributeTableConfig.ColumnConfig) + config.setColumnHidden(i, columnConfig.name in hidden) + self.mTemporalProfilesTableConfig = config + self.mTemporalProfileLayer.setAttributeTableConfig(self.mTemporalProfilesTableConfig) + self.pixelLoader = PixelLoader() + self.pixelLoader.sigPixelLoaded.connect(self.onPixelLoaded) + self.pixelLoader.sigLoadingStarted.connect(lambda: self.ui.progressInfo.setText('Start loading...')) + # self.pixelLoader.sigLoadingStarted.connect(self.tpCollection.prune) + self.pixelLoader.sigLoadingFinished.connect(lambda: self.plot2D.enableAutoRange('x', False)) - def reset3DCamera(self, *args): + # set the plot models for 2D + self.plotSettingsModel2D = PlotSettingsModel2D() + self.plotSettingsModel2DProxy = QSortFilterProxyModel() + self.plotSettingsModel2DProxy.setSourceModel(self.plotSettingsModel2D) + self.ui.tableView2DProfiles.setModel(self.plotSettingsModel2DProxy) + self.ui.tableView2DProfiles.setSortingEnabled(True) + self.ui.tableView2DProfiles.selectionModel().selectionChanged.connect(self.onPlot2DSelectionChanged) + self.ui.tableView2DProfiles.horizontalHeader().setResizeMode(QHeaderView.Interactive) + self.plotSettingsModel2D.sigDataChanged.connect(self.requestUpdate) + self.plotSettingsModel2D.rowsInserted.connect(self.onRowsInserted2D) + self.delegateTableView2D = PlotSettingsModel2DWidgetDelegate(self.ui.tableView2DProfiles, self.mTemporalProfileLayer) + self.delegateTableView2D.setItemDelegates(self.ui.tableView2DProfiles) + #self.ui.tableView2DProfiles.horizontalHeader().sectionResized.connect(self.on2DSettingsTableColumnResize) - if ENABLE_OPENGL and OPENGL_AVAILABLE: - self.ui.actionReset3DCamera.trigger() + # set the plot models for 3D + self.plotSettingsModel3D = PlotSettingsModel3D() + self.ui.tableView3DProfiles.setModel(self.plotSettingsModel3D) + self.ui.tableView3DProfiles.setSortingEnabled(False) + self.ui.tableView2DProfiles.selectionModel().selectionChanged.connect(self.onPlot3DSelectionChanged) + self.ui.tableView3DProfiles.horizontalHeader().setResizeMode(QHeaderView.Interactive) + self.plotSettingsModel3D.rowsInserted.connect(self.onRowsInserted3D) + self.delegateTableView3D = PlotSettingsModel3DWidgetDelegate(self.ui.tableView3DProfiles, self.mTemporalProfileLayer) + self.delegateTableView3D.setItemDelegates(self.ui.tableView3DProfiles) + if not ENABLE_OPENGL: + self.ui.listWidget.item(1).setHidden(True) + self.ui.page3D.setHidden(True) - 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 onTemporalProfilesRemoved(removedProfiles): + #set to valid temporal profile + affectedStyles2D = [p for p in self.plotSettingsModel2D if p.temporalProfile() in removedProfiles] + affectedStyles3D = [p for p in self.plotSettingsModel3D if p.temporalProfile() in removedProfiles] + alternativeProfile = self.mTemporalProfileLayer[-1] if len(self.mTemporalProfileLayer) > 0 else None + for s in affectedStyles2D: + assert isinstance(s, TemporalProfile2DPlotStyle) + m = self.plotSettingsModel2D + idx = m.plotStyle2idx(s) + idx = m.createIndex(idx.row(), m.columnNames.index(m.cnTemporalProfile)) + m.setData(idx, alternativeProfile, Qt.EditRole) - def onPixelLoaded(self, d): + for s in affectedStyles3D: + assert isinstance(s, TemporalProfile3DPlotStyle) + m = self.plotSettingsModel3D + idx = m.plotStyle2idx(s) + idx = m.createIndex(idx.row(), m.columnNames.index(m.cnTemporalProfile)) + m.setData(idx, alternativeProfile, Qt.EditRole) - if isinstance(d, PixelLoaderTask): + self.mTemporalProfileLayer.sigTemporalProfilesRemoved.connect(onTemporalProfilesRemoved) - bn = os.path.basename(d.sourcePath) - if d.success(): + # remove / add plot style + def on2DPlotStyleRemoved(plotStyles): + for plotStyle in plotStyles: + assert isinstance(plotStyle, PlotStyle) + for pi in plotStyle.mPlotItems: + self.plot2D.plotItem.removeItem(pi) - t = 'Loaded {} pixel from {}.'.format(len(d.resProfiles), bn) - self.mTemporalProfileLayer.addPixelLoaderResult(d) - self.updateRequested = True - else: - t = 'Failed loading from {}.'.format(bn) - if d.info and d.info != '': - t += '({})'.format(d.info) + def on3DPlotStyleRemoved(plotStyles): + toRemove = [] + for plotStyle in plotStyles: + assert isinstance(plotStyle, TemporalProfile3DPlotStyle) + toRemove.append(plotStyle.mPlotItems) + self.plot3D.removeItems(toRemove) - # QgsApplication.processEvents() - self.ui.progressInfo.setText(t) - def requestUpdate(self, *args): + self.plotSettingsModel2D.sigPlotStylesRemoved.connect(on2DPlotStyleRemoved) + self.plotSettingsModel3D.sigPlotStylesRemoved.connect(on3DPlotStyleRemoved) + + # initialize the update loop self.updateRequested = True - #next time + self.updateTimer = QTimer(self) + self.updateTimer.timeout.connect(self.onDataUpdate) + self.updateTimer.start(2000) - def onRowsInserted2D(self, parent, start, end): - model = self.ui.tableView2DProfiles.model().sourceModel() - 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.ui.tableView2DProfiles.openPersistentEditor(idxExpr) - self.ui.tableView2DProfiles.openPersistentEditor(idxStyle) - start += 1 + self.sigMoveToDate.connect(self.onMoveToDate) - def onRowsInserted3D(self, parent, start, end): - model = self.ui.tableView3DProfiles.model() - if isinstance(model, PlotSettingsModel3D): - colExpression = model.columnIndex(model.cnExpression) - colStyle = model.columnIndex(model.cnStyle) - while start <= end: - idxStyle = model.createIndex(start, colStyle) - idxExpr = model.createIndex(start, colExpression) - self.ui.tableView3DProfiles.openPersistentEditor(idxStyle) - self.ui.tableView3DProfiles.openPersistentEditor(idxExpr) - start += 1 + self.initActions() + #self.ui.stackedWidget.setCurrentPage(self.ui.pagePixel) + self.ui.onStackPageChanged(self.ui.stackedWidget.currentIndex()) - def onObservationClicked(self, plotDataItem, points): - for p in points: - tsd = p.data() - #print(tsd) + def plotStyles(self): + return self.plotSettingsModel2D[:] - def loadMissingData(self, backgroundProcess=False): + def temporalProfileLayer(self)->TemporalProfileLayer: """ - Loads all band values of collected locations that have not been loaded until now + Returns a QgsVectorLayer that is used to store profile coordinates. + :return: """ + return self.mTemporalProfileLayer - fids = self.mTemporalProfileLayer.selectedFeatureIds() - if len(fids) == 0: - fids = [f.id() for f in self.mTemporalProfileLayer.getFeatures()] + def onTemporalProfilesContextMenu(self, event): + assert isinstance(event, QContextMenuEvent) + tableView = self.ui.tableViewTemporalProfiles + selectionModel = self.ui.tableViewTemporalProfiles.selectionModel() + assert isinstance(selectionModel, QItemSelectionModel) - tps = [self.mTemporalProfileLayer.mProfiles.get(fid) for fid in fids] - spatialPoints = [tp.coordinate() for tp in tps if isinstance(tp, TemporalProfile)] - self.loadCoordinate(spatialPoints=spatialPoints, mode='all', backgroundProcess=backgroundProcess) + model = self.ui.tableViewTemporalProfiles.model() + assert isinstance(model, TemporalProfileLayer) - LOADING_MODES = ['missing', 'reload', 'all'] - def loadCoordinate(self, spatialPoints=None, LUT_bandIndices=None, mode='missing', backgroundProcess = True): - """ - :param spatialPoints: [list-of-geometries] to load pixel values from - :param LUT_bandIndices: dictionary {sensor:[indices]} with band indices to be loaded per sensor - :param mode: - :return: - """ - """ - Loads a temporal profile for a single or multiple geometries. - :param spatialPoints: SpatialPoint | [list-of-SpatialPoints] - """ - assert mode in SpectralTemporalVisualization.LOADING_MODES + temporalProfiles = [] - if isinstance(spatialPoints, SpatialPoint): - spatialPoints = [spatialPoints] + if len(selectionModel.selectedIndexes()) > 0: + for idx in selectionModel.selectedIndexes(): + tp = model.idx2tp(idx) + if isinstance(tp, TemporalProfile) and not tp in temporalProfiles: + temporalProfiles.append(tp) + else: + temporalProfiles = model[:] - assert isinstance(spatialPoints, list) + spatialPoints = [tp.coordinate() for tp in temporalProfiles] - if not isinstance(self.plotSettingsModel2D, PlotSettingsModel2D): - return False - # if not self.pixelLoader.isReadyToLoad(): - # return False + menu = QMenu() - assert isinstance(self.TS, TimeSeries) + a = menu.addAction('Load missing') + a.setToolTip('Loads missing band-pixels.') + a.triggered.connect(lambda : self.loadCoordinate(spatialPoints=spatialPoints, mode='all')) + s = "" - # Get or create the TimeSeriesProfiles which will store the loaded values + a = menu.addAction('Reload') + a.setToolTip('Reloads all band-pixels.') + a.triggered.connect(lambda: self.loadCoordinate(spatialPoints=spatialPoints, mode='reload')) - tasks = [] - TPs = [] - theGeometries = [] + menu.popup(tableView.viewport().mapToGlobal(event.pos())) + self.menu = menu - # Define which (new) bands need to be loaded for each sensor - if LUT_bandIndices is None: - LUT_bandIndices = dict() - for sensor in self.TS.sensors(): - if mode in ['all','reload']: - LUT_bandIndices[sensor] = list(range(sensor.nb)) - else: - LUT_bandIndices[sensor] = self.plotSettingsModel2D.requiredBandsIndices(sensor) - assert isinstance(LUT_bandIndices, dict) - for sensor in self.TS.sensors(): - assert sensor in LUT_bandIndices.keys() + def selected2DPlotStyles(self): + result = [] - # update new / existing points + m = self.ui.tableView2DProfiles.model() + for idx in selectedModelIndices(self.ui.tableView2DProfiles): + result.append(m.data(idx, Qt.UserRole)) + return result - for spatialPoint in spatialPoints: - assert isinstance(spatialPoint, SpatialPoint) - TP = self.mTemporalProfileLayer.fromSpatialPoint(spatialPoint) - # if not TP exists for this point, create an empty one - if not isinstance(TP, TemporalProfile): - TP = self.mTemporalProfileLayer.createTemporalProfiles(spatialPoint)[0] - if len(self.mTemporalProfileLayer) == 1: - if len(self.plotSettingsModel2D) == 0: - self.createNewPlotStyle2D() + def removePlotStyles2D(self, plotStyles): + m = self.ui.tableView2DProfiles.model() + if isinstance(m.sourceModel(), PlotSettingsModel2D): + m.sourceModel().removePlotStyles(plotStyles) - if len(self.plotSettingsModel3D) == 0: - self.createNewPlotStyle3D() + def removeTemporalProfiles(self, fids): - TPs.append(TP) - theGeometries.append(TP.coordinate()) + self.mTemporalProfileLayer.selectByIds(fids) + b = self.mTemporalProfileLayer.isEditable() + self.mTemporalProfileLayer.startEditing() + self.mTemporalProfileLayer.deleteSelectedFeatures() + self.mTemporalProfileLayer.saveEdits(leaveEditable=b) + def createNewPlotStyle2D(self): + l = len(self.mTemporalProfileLayer) - TP_ids = [TP.id() for TP in TPs] - # each TSD is a Task - s = "" - # a Task defines which bands are to be loaded - for tsd in self.TS: - assert isinstance(tsd, TimeSeriesDatum) - # do not load from invisible TSDs - if not tsd.isVisible(): - continue + plotStyle = TemporalProfile2DPlotStyle() + plotStyle.sigExpressionUpdated.connect(self.updatePlot2D) - # which bands do we need to load? - requiredIndices = set(LUT_bandIndices[tsd.sensor()]) - if len(requiredIndices) == 0: - continue + sensors = self.TS.sensors() + if len(sensors) > 0: + plotStyle.setSensor(sensors[0]) - if mode == 'missing': - missingIndices = set() + if len(self.mTemporalProfileLayer) > 0: + temporalProfile = self.mTemporalProfileLayer[0] + plotStyle.setTemporalProfile(temporalProfile) - for TP in TPs: - assert isinstance(TP, TemporalProfile) - need2load = TP.missingBandIndices(tsd, requiredIndices=requiredIndices) - missingIndices = missingIndices.union(need2load) + if len(self.plotSettingsModel2D) > 0: + lastStyle = self.plotSettingsModel2D[0] #top style in list is the most-recent + assert isinstance(lastStyle, TemporalProfile2DPlotStyle) + markerColor = nextColor(lastStyle.markerBrush.color()) + plotStyle.markerBrush.setColor(markerColor) + self.plotSettingsModel2D.insertPlotStyles([plotStyle], i=0) + pdi = plotStyle.createPlotItem(self.plot2D) - missingIndices = sorted(list(missingIndices)) - else: - missingIndices = requiredIndices + assert isinstance(pdi, TemporalProfilePlotDataItem) + pdi.sigClicked.connect(self.onProfileClicked2D) + pdi.sigPointsClicked.connect(self.onPointsClicked2D) + self.plot2D.plotItem.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() + return plotStyle - if len(missingIndices) > 0: - for pathImg in tsd.sourceUris(): - task = PixelLoaderTask(pathImg, theGeometries, - bandIndices=missingIndices, - temporalProfileIDs=TP_ids) - tasks.append(task) - if len(tasks) > 0: - aGoodDefault = 2 if len(self.TS) > 25 else 1 + def createNewPlotStyle3D(self): + if not (ENABLE_OPENGL and OPENGL_AVAILABLE): + return - if DEBUG: - print('Start loading for {} geometries from {} sources...'.format( - len(theGeometries), len(tasks) - )) - if backgroundProcess: - self.pixelLoader.startLoading(tasks) - else: - import eotimeseriesviewer.pixelloader - tasks = [PixelLoaderTask.fromDump(eotimeseriesviewer.pixelloader.doLoaderTask(None, task.toDump())) for task in tasks] - l = len(tasks) - for i, task in enumerate(tasks): - self.pixelLoader.sigPixelLoaded.emit(task) + plotStyle = TemporalProfile3DPlotStyle() + plotStyle.sigExpressionUpdated.connect(self.updatePlot3D) - else: - if DEBUG: - print('Data for geometries already loaded') + if len(self.mTemporalProfileLayer) > 0: + temporalProfile = self.mTemporalProfileLayer[0] + plotStyle.setTemporalProfile(temporalProfile) + if len(self.plotSettingsModel3D) > 0: + color = self.plotSettingsModel3D[-1].color() + plotStyle.setColor(nextColor(color)) + sensors = list(self.TS.mSensors2TSDs.keys()) + if len(sensors) > 0: + plotStyle.setSensor(sensors[0]) - @QtCore.pyqtSlot() - def onDataUpdate(self): + self.plotSettingsModel3D.insertPlotStyles([plotStyle], i=0) # latest to the top + plotItems = plotStyle.createPlotItem() + self.plot3D.addItems(plotItems) + #self.updatePlot3D() - # self.mTemporalProfileLayer.prune() + def onProfileClicked2D(self, pdi): + if isinstance(pdi, TemporalProfilePlotDataItem): + sensor = pdi.mPlotStyle.sensor() + tp = pdi.mPlotStyle.temporalProfile() + if isinstance(tp, TemporalProfile) and isinstance(sensor, SensorInstrument): + c = tp.coordinate() + info = ['Sensor:{}'.format(sensor.name()), + 'Coordinate:{}, {}'.format(c.x(), c.y())] + self.ui.tbInfo2D.setPlainText('\n'.join(info)) - for plotSetting in self.plotSettingsModel2D: - assert isinstance(plotSetting, TemporalProfile2DPlotStyle) - tp = plotSetting.temporalProfile() - for pdi in plotSetting.mPlotItems: - assert isinstance(pdi, TemporalProfilePlotDataItem) - pdi.updateDataAndStyle() - if isinstance(tp, TemporalProfile) and plotSetting.temporalProfile().updated(): - plotSetting.temporalProfile().resetUpdatedFlag() + 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): + c = tp.coordinate() + info = ['Sensor: {}'.format(sensor.name()), + 'Coordinate: {}, {}'.format(c.x(), c.y())] - for i in self.plot2D.plotItem.dataItems: - i.updateItems() + for item in spottedItems: + pos = item.pos() + x = pos.x() + y = pos.y() + date = num2date(x) + info.append('Date: {}\nValue: {}'.format(date, y)) - notInit = [0, 1] == self.plot2D.plotItem.getAxis('bottom').range - if notInit: - 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) + self.ui.tbInfo2D.setPlainText('\n'.join(info)) - if x0 is not None: - self.plot2D.plotItem.setXRange(x0, x1) + def onTemporalProfilesAdded(self, profiles): + # self.mTemporalProfileLayer.prune() + for plotStyle in self.plotSettingsModel3D: + assert isinstance(plotStyle, TemporalProfilePlotStyleBase) + if not isinstance(plotStyle.temporalProfile(), TemporalProfile): - # self.plot2D.xAxisInitialized = True + r = self.plotSettingsModel3D.plotStyle2idx(plotStyle).row() + c = self.plotSettingsModel3D.columnIndex(self.plotSettingsModel3D.cnTemporalProfile) + idx = self.plotSettingsModel3D.createIndex(r, c) + self.plotSettingsModel3D.setData(idx, self.mTemporalProfileLayer[0]) - @QtCore.pyqtSlot() - def updatePlot3D(self): - if ENABLE_OPENGL and OPENGL_AVAILABLE: + def onTemporalProfileSelectionChanged(self, selectedFIDs, deselectedFIDs): + nSelected = len(selectedFIDs) - from eotimeseriesviewer.temporalprofiles3dGL import ViewWidget3D - assert isinstance(self.plot3D, ViewWidget3D) + self.ui.actionRemoveTemporalProfile.setEnabled(nSelected > 0) - # 1. ensure that data from all bands will be loaded - # new loaded values will be shown in the next updatePlot3D call - coordinates = [] - allPlotItems = [] - for plotStyle3D in self.plotSettingsModel3D: - assert isinstance(plotStyle3D, TemporalProfile3DPlotStyle) - if plotStyle3D.isPlotable(): - coordinates.append(plotStyle3D.temporalProfile().coordinate()) - if len(coordinates) > 0: - self.loadCoordinate(coordinates, mode='all') + def onPlot2DSelectionChanged(self, selected, deselected): - toRemove = [item for item in self.plot3D.items if item not in allPlotItems] - self.plot3D.removeItems(toRemove) - toAdd = [item for item in allPlotItems if item not in self.plot3D.items] - self.plot3D.addItems(toAdd) + self.ui.actionRemoveStyle2D.setEnabled(len(selected) > 0) + def onPlot3DSelectionChanged(self, selected, deselected): - """ - # 3. add new plot items - plotItems = [] - for plotStyle3D in self.plotSettingsModel3D: - assert isinstance(plotStyle3D, TemporalProfile3DPlotStyle) - if plotStyle3D.isPlotable(): - items = plotStyle3D.createPlotItem(None) - plotItems.extend(items) + self.ui.actionRemoveStyle3D.setEnabled(len(selected) > 0) - self.plot3D.addItems(plotItems) - self.plot3D.updateDataRanges() - self.plot3D.resetScaling() - """ + def initActions(self): - @QtCore.pyqtSlot() - def updatePlot2D(self): - if isinstance(self.plotSettingsModel2D, PlotSettingsModel2D): + self.ui.actionRemoveStyle2D.setEnabled(False) + self.ui.actionRemoveTemporalProfile.setEnabled(False) + self.ui.actionAddStyle2D.triggered.connect(self.createNewPlotStyle2D) + self.ui.actionAddStyle3D.triggered.connect(self.createNewPlotStyle3D) + self.ui.actionRefresh2D.triggered.connect(self.updatePlot2D) + self.ui.actionRefresh3D.triggered.connect(self.updatePlot3D) + self.ui.actionRemoveStyle2D.triggered.connect(lambda:self.removePlotStyles2D(self.selected2DPlotStyles())) + self.ui.actionRemoveTemporalProfile.triggered.connect(lambda :self.removeTemporalProfiles(self.mTemporalProfileLayer.selectedFeatureIds())) + self.ui.actionToggleEditing.triggered.connect(self.onToggleEditing) + self.ui.actionReset2DPlot.triggered.connect(self.plot2D.resetViewBox) + self.plot2D.resetTransform() + self.ui.actionReset3DCamera.triggered.connect(self.reset3DCamera) + self.ui.actionLoadTPFromOgr.triggered.connect(lambda : self.mTemporalProfileLayer.loadCoordinatesFromOgr(None)) + self.ui.actionLoadMissingValues.triggered.connect(lambda: self.loadMissingData()) + self.ui.actionSaveTemporalProfiles.triggered.connect(lambda *args : self.mTemporalProfileLayer.saveTemporalProfiles) + #set actions to be shown in the TemporalProfileTableView context menu + ma = [self.ui.actionSaveTemporalProfiles, self.ui.actionLoadMissingValues] - locations = set() - for plotStyle in self.plotSettingsModel2D: - assert isinstance(plotStyle, TemporalProfile2DPlotStyle) - if plotStyle.isPlotable(): - locations.add(plotStyle.temporalProfile().coordinate()) - for pdi in plotStyle.mPlotItems: - assert isinstance(pdi, TemporalProfilePlotDataItem) - pdi.updateDataAndStyle() + self.onEditingToggled() - #2. load pixel data - self.loadCoordinate(list(locations)) + def onToggleEditing(self, b): - # https://github.com/pyqtgraph/pyqtgraph/blob/5195d9dd6308caee87e043e859e7e553b9887453/examples/customPlot.py - return + if self.mTemporalProfileLayer.isEditable(): + self.mTemporalProfileLayer.saveEdits(leaveEditable=False) + else: + self.mTemporalProfileLayer.startEditing() + self.onEditingToggled() + def onEditingToggled(self): + lyr = self.mTemporalProfileLayer + hasSelectedFeatures = lyr.selectedFeatureCount() > 0 + isEditable = lyr.isEditable() + self.ui.actionToggleEditing.blockSignals(True) + self.ui.actionToggleEditing.setChecked(isEditable) + #self.actionSaveTemporalProfiles.setEnabled(isEditable) + #self.actionReload.setEnabled(not isEditable) + self.ui.actionToggleEditing.blockSignals(False) -class PlotSettingsModel2DWidgetDelegate(QStyledItemDelegate): - """ + #self.actionAddAttribute.setEnabled(isEditable) + #self.actionRemoveAttribute.setEnabled(isEditable) + self.ui.actionRemoveTemporalProfile.setEnabled(isEditable and hasSelectedFeatures) + #self.actionPasteFeatures.setEnabled(isEditable) + self.ui.actionToggleEditing.setEnabled(not lyr.readOnly()) - """ - def __init__(self, tableView, temporalProfileLayer, parent=None): - assert isinstance(tableView, QTableView) - assert isinstance(temporalProfileLayer, TemporalProfileLayer) - super(PlotSettingsModel2DWidgetDelegate, self).__init__(parent=parent) - self._preferedSize = QgsFieldExpressionWidget().sizeHint() - self.mTableView = tableView - self.mTemporalProfileLayer = temporalProfileLayer - self.mTimeSeries = temporalProfileLayer.timeSeries() - self.mSensorLayers = {} - def sortFilterProxyModel(self)->QSortFilterProxyModel: - return self.mTableView.model() + def reset3DCamera(self, *args): - def plotSettingsModel(self)->PlotSettingsModel2D: - return self.sortFilterProxyModel().sourceModel() + if ENABLE_OPENGL and OPENGL_AVAILABLE: + self.ui.actionReset3DCamera.trigger() - def setItemDelegates(self, tableView): - assert isinstance(tableView, QTableView) - model = self.plotSettingsModel() - assert isinstance(model, PlotSettingsModel2D) - for c in [model.cnSensor, model.cnExpression, model.cnStyle, model.cnTemporalProfile]: - i = model.columnNames.index(c) - tableView.setItemDelegateForColumn(i, self) - def getColumnName(self, index): - assert index.isValid() - model = self.plotSettingsModel() - assert isinstance(model, PlotSettingsModel2D) - return model.columnNames[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): - # if isinstance(sensor, SensorInstrument): - if sensor not in self.mSensorLayers.keys(): + sigMoveToTSD = pyqtSignal(TimeSeriesDatum) - crs = QgsCoordinateReferenceSystem('EPSG:4862') - uri = 'Point?crs={}'.format(crs.authid()) - lyr = QgsVectorLayer(uri, 'LOCATIONS', 'memory') - 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 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 createEditor(self, parent, option, index): - cname = self.getColumnName(index) - model = self.plotSettingsModel() - pmodel = self.sortFilterProxyModel() - w = None - if index.isValid() and isinstance(model, PlotSettingsModel2D): + def onPixelLoaded(self, d): - plotStyle = model.idx2plotStyle(pmodel.mapToSource(index)) + if isinstance(d, PixelLoaderTask): - if isinstance(plotStyle, TemporalProfile2DPlotStyle): - if cname == model.cnExpression: + bn = os.path.basename(d.sourcePath) + if d.success(): - w = QgsFieldExpressionWidget(parent=parent) - 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())) - w.setExpression(plotStyle.expression()) - plotStyle.sigSensorChanged.connect(lambda s: w.setLayer(self.exampleLyr(s))) - if isinstance(plotStyle.sensor(), SensorInstrument): - w.setLayer(self.exampleLyr(plotStyle.sensor())) + t = 'Loaded {} pixel from {}.'.format(len(d.resProfiles), bn) + self.mTemporalProfileLayer.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.ui.tableView2DProfiles.model().sourceModel() + 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.ui.tableView2DProfiles.openPersistentEditor(idxExpr) + self.ui.tableView2DProfiles.openPersistentEditor(idxStyle) + start += 1 - 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())) + def onRowsInserted3D(self, parent, start, end): + model = self.ui.tableView3DProfiles.model() + if isinstance(model, PlotSettingsModel3D): + colExpression = model.columnIndex(model.cnExpression) + colStyle = model.columnIndex(model.cnStyle) + while start <= end: + idxStyle = model.createIndex(start, colStyle) + idxExpr = model.createIndex(start, colExpression) + self.ui.tableView3DProfiles.openPersistentEditor(idxStyle) + self.ui.tableView3DProfiles.openPersistentEditor(idxExpr) + start += 1 - elif cname == model.cnSensor: - w = QComboBox(parent=parent) - m = SensorListModel(self.mTimeSeries) - w.setModel(m) + def onObservationClicked(self, plotDataItem, points): + for p in points: + tsd = p.data() + #print(tsd) - elif cname == model.cnTemporalProfile: - w = QgsFeatureListComboBox(parent=parent) - w.setSourceLayer(self.mTemporalProfileLayer) - w.setIdentifierField('id') - w.setDisplayExpression('to_string("id")+\' \'+"name"') - w.setAllowNull(False) - else: - raise NotImplementedError() - return w - def checkData(self, index, w, value): - 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 value == w.expression() - assert w.isExpressionValid(value) == w.isValidExpression() + def loadMissingData(self, backgroundProcess=False): + """ + Loads all band values of collected locations that have not been loaded until now + """ - if w.isValidExpression(): - self.commitData.emit(w) - else: - s = "" - #print(('Delegate commit failed',w.asExpression())) - if isinstance(w, PlotStyleButton): + fids = self.mTemporalProfileLayer.selectedFeatureIds() + if len(fids) == 0: + fids = [f.id() for f in self.mTemporalProfileLayer.getFeatures()] - self.commitData.emit(w) + tps = [self.mTemporalProfileLayer.mProfiles.get(fid) for fid in fids] + spatialPoints = [tp.coordinate() for tp in tps if isinstance(tp, TemporalProfile)] + self.loadCoordinate(spatialPoints=spatialPoints, mode='all', backgroundProcess=backgroundProcess) - def setEditorData(self, editor, proxyIndex): + LOADING_MODES = ['missing', 'reload', 'all'] + def loadCoordinate(self, spatialPoints=None, LUT_bandIndices=None, mode='missing', backgroundProcess = True): + """ + :param spatialPoints: [list-of-geometries] to load pixel values from + :param LUT_bandIndices: dictionary {sensor:[indices]} with band indices to be loaded per sensor + :param mode: + :return: + """ + """ + Loads a temporal profile for a single or multiple geometries. + :param spatialPoints: SpatialPoint | [list-of-SpatialPoints] + """ + assert mode in SpectralTemporalVisualization.LOADING_MODES - model = self.plotSettingsModel() - index = self.sortFilterProxyModel().mapToSource(proxyIndex) - w = None - if index.isValid() and isinstance(model, PlotSettingsModel2D): - cname = self.getColumnName(proxyIndex) - if cname == model.cnExpression: - lastExpr = 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, QgsFeatureListComboBox) - value = editor.identifierValue() - if value != QVariant(): - plotStyle = index.data(role=Qt.UserRole) - TP = plotStyle.temporalProfile() - editor.setIdentifierValue(TP.id()) - else: - s = "" - else: - raise NotImplementedError() + if isinstance(spatialPoints, SpatialPoint): + spatialPoints = [spatialPoints] - def setModelData(self, w, model, proxyIndex): - index = self.sortFilterProxyModel().mapToSource(proxyIndex) - cname = self.getColumnName(proxyIndex) - model = self.plotSettingsModel() + assert isinstance(spatialPoints, list) - 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 not isinstance(self.plotSettingsModel2D, PlotSettingsModel2D): + return False - if w.isValidExpression() and expr != exprLast: - model.setData(index, w.asExpression(), Qt.EditRole) + # if not self.pixelLoader.isReadyToLoad(): + # return False - elif cname == model.cnStyle: - if isinstance(w, PlotStyleButton): - model.setData(index, w.plotStyle(), Qt.EditRole) + assert isinstance(self.TS, TimeSeries) - elif cname == model.cnSensor: - assert isinstance(w, QComboBox) - sensor = w.itemData(w.currentIndex(), role=Qt.UserRole) - if isinstance(sensor, SensorInstrument): - model.setData(index, sensor, Qt.EditRole) + # Get or create the TimeSeriesProfiles which will store the loaded values - elif cname == model.cnTemporalProfile: - assert isinstance(w, QgsFeatureListComboBox) - fid = w.identifierValue() - if isinstance(fid, int): - TP = self.mTemporalProfileLayer.mProfiles.get(fid) - model.setData(index, TP, Qt.EditRole) + tasks = [] + TPs = [] + theGeometries = [] - else: - raise NotImplementedError() + # Define which (new) bands need to be loaded for each sensor + if LUT_bandIndices is None: + LUT_bandIndices = dict() + for sensor in self.TS.sensors(): + if mode in ['all', 'reload']: + LUT_bandIndices[sensor] = list(range(sensor.nb)) + else: + 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 -class PlotSettingsModel3DWidgetDelegate(QStyledItemDelegate): - """ - """ - def __init__(self, tableView, temporalProfileLayer, parent=None): - assert isinstance(tableView, QTableView) - assert isinstance(temporalProfileLayer, TemporalProfileLayer) - super(PlotSettingsModel3DWidgetDelegate, self).__init__(parent=parent) - self._preferedSize = QgsFieldExpressionWidget().sizeHint() - self.mTableView = tableView - self.mTimeSeries = temporalProfileLayer.timeSeries() - self.mTemporalProfileLayer = temporalProfileLayer - self.mSensorLayers = {} + for spatialPoint in spatialPoints: + assert isinstance(spatialPoint, SpatialPoint) + TP = self.mTemporalProfileLayer.fromSpatialPoint(spatialPoint) + + # if no TemporaProfile existed before, create an empty one + if not isinstance(TP, TemporalProfile): + TP = self.mTemporalProfileLayer.createTemporalProfiles(spatialPoint)[0] + + if len(self.mTemporalProfileLayer) == 1: + if len(self.plotSettingsModel2D) == 0: + self.createNewPlotStyle2D() + + if len(self.plotSettingsModel3D) == 0: + self.createNewPlotStyle3D() + + TPs.append(TP) + theGeometries.append(TP.coordinate()) + TP_ids = [TP.id() for TP in TPs] + # each TSD is a Task + s = "" + # a Task defines which bands are to be loaded + for tsd in self.TS: + assert isinstance(tsd, TimeSeriesDatum) + # 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 + if mode == 'missing': + missingIndices = set() - def setItemDelegates(self, tableView): - assert isinstance(tableView, QTableView) - model = tableView.model() - assert isinstance(model, PlotSettingsModel3D) - for c in [model.cnSensor, model.cnExpression, model.cnStyle, model.cnTemporalProfile]: - i = model.columnNames.index(c) - tableView.setItemDelegateForColumn(i, self) + for TP in TPs: + assert isinstance(TP, TemporalProfile) + need2load = TP.missingBandIndices(tsd, requiredIndices=requiredIndices) + missingIndices = missingIndices.union(need2load) - def getColumnName(self, index): - assert index.isValid() - model = index.model() - assert isinstance(model, PlotSettingsModel3D) - return model.columnNames[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.cnExpression: - w = QgsFieldExpressionWidget(parent=parent) - w.setExpression(plotStyle.expression()) - w.setLayer(self.exampleLyr(plotStyle.sensor())) - def onSensorAdded(s): - w.setLayer(self.exampleLyr(s)) - #plotStyle.sigSensorChanged.connect(lambda s : w.setLayer(self.exampleLyr(s))) - plotStyle.sigSensorChanged.connect(onSensorAdded) - 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())) + missingIndices = sorted(list(missingIndices)) + else: + missingIndices = requiredIndices - elif cname == model.cnStyle: - w = TemporalProfile3DPlotStyleButton(parent=parent) - w.setPlotStyle(plotStyle) - w.setToolTip('Set plot style') - w.sigPlotStyleChanged.connect(lambda: self.checkData(index, w, w.plotStyle())) + if len(missingIndices) > 0: + for pathImg in tsd.sourceUris(): + task = PixelLoaderTask(pathImg, theGeometries, + bandIndices=missingIndices, + temporalProfileIDs=TP_ids) + tasks.append(task) - elif cname == model.cnSensor: - w = QComboBox(parent=parent) - m = SensorListModel(self.mTimeSeries) - w.setModel(m) + if len(tasks) > 0: - elif cname == model.cnTemporalProfile: - w = QgsFeatureListComboBox(parent=parent) - w.setSourceLayer(self.mTemporalProfileLayer) - w.setIdentifierField('id') - w.setDisplayExpression('to_string("id")+\' \'+"name"') - w.setAllowNull(False) - else: - raise NotImplementedError() - return w + if DEBUG: + print('Start loading for {} geometries from {} sources...'.format( + len(theGeometries), len(tasks) + )) + if backgroundProcess: + self.pixelLoader.startLoading(tasks) + else: + import eotimeseriesviewer.pixelloader + tasks = [PixelLoaderTask.fromDump(eotimeseriesviewer.pixelloader.doLoaderTask(None, task.toDump())) for task in tasks] + l = len(tasks) + for i, task in enumerate(tasks): + self.pixelLoader.sigPixelLoaded.emit(task) - def exampleLyr(self, sensor): + else: + if DEBUG: + print('Data for geometries already loaded') - if sensor not in self.mSensorLayers.keys(): - crs = QgsCoordinateReferenceSystem('EPSG:4862') - uri = 'Point?crs={}'.format(crs.authid()) - lyr = QgsVectorLayer(uri, 'LOCATIONS', 'memory') - assert sensor is None or isinstance(sensor, SensorInstrument) - f = sensorExampleQgsFeature(sensor, singleBandOnly=True) - 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 checkData(self, index, w, value): - 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, QgsFieldExpressionWidget): - assert value == w.expression() - assert w.isExpressionValid(value) == w.isValidExpression() + @QtCore.pyqtSlot() + def onDataUpdate(self): - if w.isValidExpression(): - self.commitData.emit(w) - else: - s = "" - #print(('Delegate commit failed',w.asExpression())) - if isinstance(w, TemporalProfile3DPlotStyleButton): + # self.mTemporalProfileLayer.prune() - self.commitData.emit(w) + for plotSetting in self.plotSettingsModel2D: + assert isinstance(plotSetting, TemporalProfile2DPlotStyle) + tp = plotSetting.temporalProfile() + for pdi in plotSetting.mPlotItems: + assert isinstance(pdi, TemporalProfilePlotDataItem) + pdi.updateDataAndStyle() + if isinstance(tp, TemporalProfile) and plotSetting.temporalProfile().updated(): + plotSetting.temporalProfile().resetUpdatedFlag() - def setEditorData(self, editor, index): - cname = self.getColumnName(index) - model = self.mTableView.model() + for i in self.plot2D.plotItem.dataItems: + i.updateItems() - w = None - if index.isValid() and isinstance(model, PlotSettingsModel3D): - cname = self.getColumnName(index) - style = model.idx2plotStyle(index) + notInit = [0, 1] == self.plot2D.plotItem.getAxis('bottom').range + if notInit: + 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 cname == model.cnExpression: - lastExpr = index.model().data(index, Qt.DisplayRole) - assert isinstance(editor, QgsFieldExpressionWidget) - editor.setProperty('lastexpr', lastExpr) - editor.setField(lastExpr) + if x0 is not None: + self.plot2D.plotItem.setXRange(x0, x1) - elif cname == model.cnStyle: - assert isinstance(editor, TemporalProfile3DPlotStyleButton) - editor.setPlotStyle(style) + # self.plot2D.xAxisInitialized = True - 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, QgsFeatureListComboBox) - value = editor.identifierValue() - if value != QVariant(): - plotStyle = index.data(role=Qt.UserRole) - TP = plotStyle.temporalProfile() - editor.setIdentifierValue(TP.id()) - else: - s = "" + @QtCore.pyqtSlot() + def updatePlot3D(self): + if ENABLE_OPENGL and OPENGL_AVAILABLE: - else: - raise NotImplementedError() + from eotimeseriesviewer.temporalprofiles3dGL import ViewWidget3D + assert isinstance(self.plot3D, ViewWidget3D) - def setModelData(self, w, model, index): - cname = self.getColumnName(index) - model = self.mTableView.model() + # 1. ensure that data from all bands will be loaded + # new loaded values will be shown in the next updatePlot3D call + coordinates = [] + allPlotItems = [] + for plotStyle3D in self.plotSettingsModel3D: + assert isinstance(plotStyle3D, TemporalProfile3DPlotStyle) + if plotStyle3D.isPlotable(): + coordinates.append(plotStyle3D.temporalProfile().coordinate()) - if index.isValid() and isinstance(model, PlotSettingsModel3D): - if cname == model.cnExpression: - assert isinstance(w, QgsFieldExpressionWidget) - expr = w.asExpression() - exprLast = model.data(index, Qt.DisplayRole) + if len(coordinates) > 0: + self.loadCoordinate(coordinates, mode='all') - if w.isValidExpression() and expr != exprLast: - model.setData(index, w.asExpression(), Qt.EditRole) + toRemove = [item for item in self.plot3D.items if item not in allPlotItems] + self.plot3D.removeItems(toRemove) + toAdd = [item for item in allPlotItems if item not in self.plot3D.items] + self.plot3D.addItems(toAdd) - elif cname == model.cnStyle: - assert isinstance(w, TemporalProfile3DPlotStyleButton) - 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) + """ + # 3. add new plot items + plotItems = [] + for plotStyle3D in self.plotSettingsModel3D: + assert isinstance(plotStyle3D, TemporalProfile3DPlotStyle) + if plotStyle3D.isPlotable(): + items = plotStyle3D.createPlotItem(None) + plotItems.extend(items) - s = "" + self.plot3D.addItems(plotItems) + self.plot3D.updateDataRanges() + self.plot3D.resetScaling() + """ - elif cname == model.cnTemporalProfile: - assert isinstance(w, QgsFeatureListComboBox) - fid = w.identifierValue() - if isinstance(fid, int): - TP = self.mTemporalProfileLayer.mProfiles.get(fid) - model.setData(index, TP, Qt.EditRole) + @QtCore.pyqtSlot() + def updatePlot2D(self): + if isinstance(self.plotSettingsModel2D, PlotSettingsModel2D): - else: - raise NotImplementedError() + locations = set() + for plotStyle in self.plotSettingsModel2D: + assert isinstance(plotStyle, TemporalProfile2DPlotStyle) + if plotStyle.isPlotable(): + locations.add(plotStyle.temporalProfile().coordinate()) + for pdi in plotStyle.mPlotItems: + assert isinstance(pdi, TemporalProfilePlotDataItem) + pdi.updateDataAndStyle() + #2. load pixel data + self.loadCoordinate(list(locations)) + # https://github.com/pyqtgraph/pyqtgraph/blob/5195d9dd6308caee87e043e859e7e553b9887453/examples/customPlot.py + return def examplePixelLoader(): diff --git a/eotimeseriesviewer/temporalprofiles2d.py b/eotimeseriesviewer/temporalprofiles2d.py index 1a17808a01a16c6e1d85b3b03484b7c17693a0e6..a1e68d4cb6d74b8d98e05a6f7a4aec88c1acd81a 100644 --- a/eotimeseriesviewer/temporalprofiles2d.py +++ b/eotimeseriesviewer/temporalprofiles2d.py @@ -46,7 +46,7 @@ OPENGL_AVAILABLE = False DEFAULT_SAVE_PATH = None DEFAULT_CRS = QgsCoordinateReferenceSystem('EPSG:4326') -FN_ID = 'id' +FN_ID = 'fid' FN_X = 'x' FN_Y = 'y' FN_NAME = 'name' @@ -594,7 +594,7 @@ class TemporalProfile(QObject): assert isinstance(tsd, TimeSeriesDatum) meta = {'doy': tsd.mDOY, 'date': str(tsd.mDate), - 'nodata':False} + 'nodata': False} self.updateData(tsd, meta, skipStatusUpdate=True) #self.updateLoadingStatus() @@ -620,9 +620,13 @@ class TemporalProfile(QObject): def geometry(self): return self.mLayer.getFeature(self.mID).geometry() - def coordinate(self): - x,y = self.geometry().asPoint() - return SpatialPoint(self.mLayer.crs(), x,y) + def coordinate(self)->SpatialPoint: + """ + Returns the profile coordinate + :return: + """ + x, y = self.geometry().asPoint() + return SpatialPoint(self.mLayer.crs(), x, y) def id(self): """Feature ID in connected QgsVectorLayer""" @@ -865,7 +869,7 @@ class TemporalProfile(QObject): return tsd in self.mData.keys() def __repr__(self): - return 'TemporalProfile {}'.format(self.mID) + return 'TemporalProfile {} "{}"'.format(self.id(), self.name()) class TemporalProfilePlotDataItem(pg.PlotDataItem): @@ -1030,7 +1034,7 @@ class TemporalProfileLayer(QgsVectorLayer): srs.ImportFromEPSG(4326) co = ['GEOMETRY_NAME=geom', 'GEOMETRY_NULLABLE=YES', - 'FID=fid' + 'FID={}'.format(FN_ID) ] lyr = dsSrc.CreateLayer(name, srs=srs, geom_type=ogr.wkbPoint, options=co) @@ -1066,13 +1070,13 @@ class TemporalProfileLayer(QgsVectorLayer): self.mTimeSeries = timeSeries #symbol = QgsFillSymbol.createSimple({'style': 'no', 'color': 'red', 'outline_color': 'black'}) #self.mLocations.renderer().setSymbol(symbol) - self.mNextID = 1 + #self.mNextID = 1 self.TS = None self.setName('EOTS Temporal Profiles') fields = QgsFields() - fields.append(createQgsField(FN_ID, self.mNextID)) - fields.append(createQgsField(FN_NAME,'')) + #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')) @@ -1094,8 +1098,6 @@ class TemporalProfileLayer(QgsVectorLayer): assert isinstance(self.mTimeSeries, TimeSeries) # Get or create the TimeSeriesProfiles which will store the loaded values - - tasks = [] theGeometries = [] @@ -1226,6 +1228,8 @@ class TemporalProfileLayer(QgsVectorLayer): :param addedFeatures: :return: """ + if layerID != self.id(): + s = "" if len(addedFeatures) > 0: @@ -1245,14 +1249,17 @@ class TemporalProfileLayer(QgsVectorLayer): def onFeaturesRemoved(self, layerID, removedFIDs): + if layerID != self.id(): + s = "" + if len(removedFIDs) > 0: - toRemove = [] + removed = [] for fid in removedFIDs: - toRemove.append(self.mProfiles.pop(fid)) + removed.append(self.mProfiles.pop(fid)) - self.sigTemporalProfilesRemoved.emit(toRemove) + self.sigTemporalProfilesRemoved.emit(removed) def initConditionalStyles(self): @@ -1266,49 +1273,95 @@ class TemporalProfileLayer(QgsVectorLayer): #styles.setRowStyles([red]) - def createTemporalProfiles(self, coordinates)->list: + def createTemporalProfiles(self, coordinates, names:list=None)->list: """ Creates temporal profiles :param coordinates: :return: """ - if not isinstance(coordinates, list): + + 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] + 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 = [] n = self.dataProvider().featureCount() - for i, coordinate in enumerate(coordinates): + 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()))) - f.setAttribute(FN_ID, self.mNextID) - f.setAttribute(FN_NAME, 'TP {}'.format(self.mNextID)) + #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) - self.mNextID += 1 + #self.mNextID += 1 features.append(f) + if len(features) == 0: + return [] + b = self.isEditable() - tps_before = list(self.mProfiles.values()) self.startEditing() + + newFeatures = [] + def onFeaturesAdded(lid, fids): + newFeatures.extend(fids) + + self.committedFeaturesAdded.connect(onFeaturesAdded) + self.beginEditCommand('Add {} profile locations'.format(len(features))) success = self.addFeatures(features) + self.endEditCommand() self.saveEdits(leaveEditable=b) + self.committedFeaturesAdded.disconnect(onFeaturesAdded) - - if success: - assert n+len(features) == self.featureCount() - assert self.featureCount() == len(self.mProfiles) - profiles = [tp for tp in self.mProfiles.values() if tp not in tps_before] - #for p in profiles: - # p.updateLoadingStatus() - return profiles - else: - return [] + assert self.featureCount() == len(self.mProfiles) + profiles = [self.mProfiles[f.id()] for f in newFeatures] + return profiles def saveEdits(self, leaveEditable=False, triggerRepaint=True): diff --git a/eotimeseriesviewer/ui/profileviewdock.ui b/eotimeseriesviewer/ui/profileviewdock.ui index 728b14f161946aa0459b4e0f022a7da1a5720665..891a53b0b3c724267c8f16186a18c6b03a91ca4d 100644 --- a/eotimeseriesviewer/ui/profileviewdock.ui +++ b/eotimeseriesviewer/ui/profileviewdock.ui @@ -6,19 +6,19 @@ <rect> <x>0</x> <y>0</y> - <width>773</width> - <height>235</height> + <width>799</width> + <height>433</height> </rect> </property> <property name="minimumSize"> <size> - <width>0</width> - <height>0</height> + <width>537</width> + <height>235</height> </size> </property> <property name="maximumSize"> <size> - <width>16777215</width> + <width>524287</width> <height>524287</height> </size> </property> @@ -205,7 +205,7 @@ <number>0</number> </property> <property name="currentIndex"> - <number>2</number> + <number>0</number> </property> <widget class="QWidget" name="page2D"> <layout class="QVBoxLayout" name="verticalLayout_3"> @@ -265,7 +265,7 @@ <number>0</number> </property> <item> - <widget class="QTableView" name="tableView2DProfiles"> + <widget class="PlotSettingsTableView" name="tableView2DProfiles"> <property name="sizePolicy"> <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> <horstretch>1</horstretch> @@ -942,6 +942,11 @@ background-color: rgb(0, 0, 0);</string> <extends>QGraphicsView</extends> <header>eotimeseriesviewer.temporalprofiles2d</header> </customwidget> + <customwidget> + <class>PlotSettingsTableView</class> + <extends>QTableView</extends> + <header>eotimeseriesviewer.profilevisualization</header> + </customwidget> </customwidgets> <resources> <include location="resources.qrc"/> diff --git a/eotimeseriesviewer/ui/timeseriesdock.ui b/eotimeseriesviewer/ui/timeseriesdock.ui index e3ccc43981adc8e67b8fe3e54d32ba803904f4fd..a0856d09d1b01ccae4c3b9aaa67d65eed3cf6579 100644 --- a/eotimeseriesviewer/ui/timeseriesdock.ui +++ b/eotimeseriesviewer/ui/timeseriesdock.ui @@ -7,7 +7,7 @@ <x>0</x> <y>0</y> <width>640</width> - <height>198</height> + <height>261</height> </rect> </property> <property name="sizePolicy"> @@ -278,7 +278,7 @@ <item> <widget class="QLabel" name="summary"> <property name="sizePolicy"> - <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> <horstretch>2</horstretch> <verstretch>0</verstretch> </sizepolicy>