From a553923b5d07ad8059639a39313f03b1f5f2d4b0 Mon Sep 17 00:00:00 2001 From: "Benjamin Jakimow benjamin.jakimow@geo.hu-berlin.de" <benjamin.jakimow@geo.hu-berlin.de> Date: Thu, 29 Oct 2020 21:29:42 +0100 Subject: [PATCH] updated QPS Signed-off-by: Benjamin Jakimow benjamin.jakimow@geo.hu-berlin.de <benjamin.jakimow@geo.hu-berlin.de> --- .../externals/qps/cursorlocationvalue.py | 114 +- .../externals/qps/layerconfigwidgets/core.py | 26 +- .../qps/layerconfigwidgets/gdalmetadata.py | 164 ++- .../layerconfigwidgets/vectorlayerfields.py | 68 +- .../externals/qps/layerproperties.py | 31 +- eotimeseriesviewer/externals/qps/maptools.py | 2 +- eotimeseriesviewer/externals/qps/models.py | 667 +++++++----- .../externals/qps/plotstyling/plotstyling.py | 6 +- .../externals/qps/simplewidgets.py | 133 +++ .../externals/qps/speclib/__init__.py | 12 +- .../externals/qps/speclib/core.py | 276 +++-- .../qps/speclib/function_help/format_py.json | 12 + .../speclib/function_help/spectraldata.json | 8 + .../speclib/function_help/spectralmath.json | 8 + .../externals/qps/speclib/gui.py | 979 ++++++++++-------- .../externals/qps/speclib/io/vectorsources.py | 8 +- .../externals/qps/speclib/qgsfunctions.py | 277 ++++- .../spectralprofileeditorconfigwidget.ui | 65 +- .../speclib/spectralprofileeditorwidget.ui | 246 +++-- .../externals/qps/subdatasets.py | 19 +- eotimeseriesviewer/externals/qps/testing.py | 11 +- .../qps/ui/cursorlocationinfodock.ui | 6 +- .../qps/ui/gdalmetadatamodelwidget.ui | 28 +- eotimeseriesviewer/externals/qps/unitmodel.py | 4 + eotimeseriesviewer/externals/qps/utils.py | 51 +- .../externals/qps/vectorlayertools.py | 6 +- 26 files changed, 2085 insertions(+), 1142 deletions(-) create mode 100644 eotimeseriesviewer/externals/qps/simplewidgets.py create mode 100644 eotimeseriesviewer/externals/qps/speclib/function_help/format_py.json create mode 100644 eotimeseriesviewer/externals/qps/speclib/function_help/spectraldata.json create mode 100644 eotimeseriesviewer/externals/qps/speclib/function_help/spectralmath.json diff --git a/eotimeseriesviewer/externals/qps/cursorlocationvalue.py b/eotimeseriesviewer/externals/qps/cursorlocationvalue.py index d4986f08..8967755c 100644 --- a/eotimeseriesviewer/externals/qps/cursorlocationvalue.py +++ b/eotimeseriesviewer/externals/qps/cursorlocationvalue.py @@ -100,97 +100,69 @@ class CursorLocationInfoModel(TreeModel): REMAINDER = 'reminder' def __init__(self, parent=None): - super(CursorLocationInfoModel, self).__init__(parent) + super().__init__(parent=parent) + + self.setColumnNames(['Band/Field', 'Value']) - self.mColumnNames = ['Band/Field', 'Value', 'Description'] self.mExpandedNodeRemainder = {} self.mNodeExpansion = CursorLocationInfoModel.REMAINDER - def setNodeExpansion(self, type): - - assert type in [CursorLocationInfoModel.ALWAYS_EXPAND, - CursorLocationInfoModel.NEVER_EXPAND, - CursorLocationInfoModel.REMAINDER] - self.mNodeExpansion = type + def flags(self, index): - def setExpandedNodeRemainder(self, node=None): - treeView = self.mTreeView - assert isinstance(treeView, QTreeView) - if node is None: - for n in self.mRootNode.childNodes(): - self.setExpandedNodeRemainder(node=n) - else: - self.mExpandedNodeRemainder[self.weakNodeId(node)] = self.mTreeView.isExpanded(self.node2idx(node)) - for n in node.childNodes(): - self.setExpandedNodeRemainder(node=n) + return Qt.ItemIsEnabled | Qt.ItemIsSelectable - def weakNodeId(self, node): + """ + def weakNodeId(self, node: TreeNode) -> str: assert isinstance(node, TreeNode) n = node.name() while node.parentNode() != self.mRootNode: node = node.parentNode() n += '{}:{}'.format(node.name(), n) return n + """ - def flags(self, index): - - return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable - - def addSourceValues(self, sourceValueSet): + def addSourceValues(self, sourceValueSet: SourceValueSet): if not isinstance(sourceValueSet, SourceValueSet): return - - # get-or-create node - def gocn(root, name) -> TreeNode: - assert isinstance(root, TreeNode) - n = TreeNode(root, name) - weakId = self.weakNodeId(n) - - expand = False - if not isinstance(root.parentNode(), TreeNode): - expand = True - else: - if self.mNodeExpansion == CursorLocationInfoModel.REMAINDER: - expand = self.mExpandedNodeRemainder.get(weakId, False) - elif self.mNodeExpansion == CursorLocationInfoModel.NEVER_EXPAND: - expand = False - elif self.mNodeExpansion == CursorLocationInfoModel.ALWAYS_EXPAND: - expand = True - - self.mTreeView.setExpanded(self.node2idx(n), expand) - return n - bn = os.path.basename(sourceValueSet.source) if isinstance(sourceValueSet, RasterValueSet): - root = gocn(self.mRootNode, name=bn) + + root = TreeNode(bn) root.setIcon(QIcon(':/qps/ui/icons/raster.svg')) # add subnodes - n = gocn(root, 'Pixel') - n.setValues('{},{}'.format(sourceValueSet.pxPosition.x(), sourceValueSet.pxPosition.y())) + pxNode = TreeNode('Pixel') + pxNode.setValues('{},{}'.format(sourceValueSet.pxPosition.x(), sourceValueSet.pxPosition.y())) + + subNodes = [pxNode] for bv in sourceValueSet.bandValues: if isinstance(bv, RasterValueSet.BandInfo): - n = gocn(root, 'Band {}'.format(bv.bandIndex + 1)) + n = TreeNode('Band {}'.format(bv.bandIndex + 1)) n.setToolTip('Band {} {}'.format(bv.bandIndex + 1, bv.bandName).strip()) n.setValues([bv.bandValue, bv.bandName]) + subNodes.append(n) if isinstance(bv.classInfo, ClassInfo): - nc = gocn(root, 'Class') + nc = TreeNode('Class') nc.setValues(bv.classInfo.name()) nc.setIcon(bv.classInfo.icon()) - + n.appendChildNodes(nc) elif isinstance(bv, QColor): - n = gocn(root, 'Color') + n = TreeNode('Color') n.setToolTip('Color selected from screen pixel') n.setValues(bv.getRgb()) + subNodes.append(n) + root.appendChildNodes(subNodes) + self.rootNode().appendChildNodes(root) if isinstance(sourceValueSet, VectorValueSet): if len(sourceValueSet.features) == 0: return - root = gocn(self.mRootNode, name=bn) + + root = TreeNode(bn) refFeature = sourceValueSet.features[0] assert isinstance(refFeature, QgsFeature) typeName = QgsWkbTypes.displayString(refFeature.geometry().wkbType()).lower() @@ -201,21 +173,33 @@ class CursorLocationInfoModel(TreeModel): if 'point' in typeName: root.setIcon(QIcon(r':/images/themes/default/mIconPointLayer.svg')) + subNodes = [] for field in refFeature.fields(): assert isinstance(field, QgsField) - fieldNode = gocn(root, name=field.name()) + fieldNode = TreeNode(field.name()) + featureNodes = [] for i, feature in enumerate(sourceValueSet.features): assert isinstance(feature, QgsFeature) - nf = gocn(fieldNode, name='{}'.format(feature.id())) + nf = TreeNode(name='{}'.format(feature.id())) nf.setValues([feature.attribute(field.name()), field.typeName()]) nf.setToolTip('Value of feature "{}" in field with name "{}"'.format(feature.id(), field.name())) - + featureNodes.append(nf) + fieldNode.appendChildNodes(featureNodes) + subNodes.append(fieldNode) + root.appendChildNodes(subNodes) + self.rootNode().appendChildNodes(root) s = "" def clear(self): - self.mRootNode.removeChildNodes(0, self.mRootNode.childCount()) + self.mRootNode.removeAllChildNodes() + + +class CursorLocationInfoTreeView(TreeView): + + def __init__(self, *args, **kwds): + super().__init__(*args, **kwds) class ComboBoxOption(object): @@ -313,8 +297,11 @@ class CursorLocationInfoDock(QDockWidget): self.btnCrs.crsChanged.connect(self.setCrs) self.btnCrs.setCrs(QgsCoordinateReferenceSystem()) - self.mLocationInfoModel = CursorLocationInfoModel(parent=self.treeView) - self.treeView.setModel(self.mLocationInfoModel) + self.mLocationInfoModel = CursorLocationInfoModel() + self.mTreeView: CursorLocationInfoTreeView + assert isinstance(self.mTreeView, CursorLocationInfoTreeView) + self.mTreeView.setAutoExpansionDepth(3) + self.mTreeView.setModel(self.mLocationInfoModel) self.mLayerModeModel = ComboBoxOptionModel(LAYERMODES, parent=self) self.mLayerTypeModel = ComboBoxOptionModel(LAYERTYPES, parent=self) @@ -355,6 +342,9 @@ class CursorLocationInfoDock(QDockWidget): self.setCanvas(canvas) self.reloadCursorLocation() + def treeView(self) -> CursorLocationInfoTreeView: + return self.mTreeView + def reloadCursorLocation(self): """ Call to load / re-load the data for the cursor location @@ -381,7 +371,7 @@ class CursorLocationInfoDock(QDockWidget): for c in self.mCanvases: lyrs.extend(layerFilter(c)) - self.mLocationInfoModel.setExpandedNodeRemainder() + self.treeView().updateNodeExpansion(False) self.mLocationInfoModel.clear() for l in lyrs: @@ -473,6 +463,8 @@ class CursorLocationInfoDock(QDockWidget): pass + self.treeView().updateNodeExpansion(True) + def setCursorLocation(self, spatialPoint: SpatialPoint): """ Set the cursor lcation to be loaded. @@ -535,5 +527,3 @@ class CursorLocationInfoDock(QDockWidget): return self.mLocationHistory[0] else: return None, None - - diff --git a/eotimeseriesviewer/externals/qps/layerconfigwidgets/core.py b/eotimeseriesviewer/externals/qps/layerconfigwidgets/core.py index 115a8179..042ea3d7 100644 --- a/eotimeseriesviewer/externals/qps/layerconfigwidgets/core.py +++ b/eotimeseriesviewer/externals/qps/layerconfigwidgets/core.py @@ -27,8 +27,9 @@ import os import pathlib import enum import re -from qgis.core import QgsMapLayer, QgsRasterLayer, QgsVectorLayer, QgsFileUtils, QgsSettings, QgsStyle, QgsApplication -from qgis.gui import QgsRasterHistogramWidget, QgsMapCanvas, QgsMapLayerConfigWidget, \ +from qgis.core import QgsMapLayer, QgsRasterLayer, QgsVectorLayer, QgsFileUtils, QgsSettings, \ + QgsStyle, QgsMapLayerStyle, QgsApplication +from qgis.gui import QgsRasterHistogramWidget, QgsMapCanvas, QgsMapLayerConfigWidget, \ QgsLayerTreeEmbeddedConfigWidget, QgsMapLayerConfigWidgetFactory, QgsRendererRasterPropertiesWidget, \ QgsRendererPropertiesDialog, QgsRasterTransparencyWidget, QgsProjectionSelectionWidget # @@ -192,11 +193,11 @@ class SymbologyConfigWidget(QpsMapLayerConfigWidget): Emulates the QGS Layer Property Dialogs "Source" page """ - def __init__(self, layer: QgsMapLayer, canvas: QgsMapCanvas, style: QgsStyle = QgsStyle(), parent=None): + def __init__(self, layer: QgsMapLayer, canvas: QgsMapCanvas, parent=None): super().__init__(layer, canvas, parent=parent) loadUi(configWidgetUi('symbologyconfigwidget.ui'), self) self.mSymbologyWidget = None - self.mStyle: QgsStyle = style + self.mStyle: QgsMapLayerStyle = None self.mDefaultRenderer = None if isinstance(layer, (QgsRasterLayer, QgsVectorLayer)): self.mDefaultRenderer = layer.renderer().clone() @@ -206,7 +207,7 @@ class SymbologyConfigWidget(QpsMapLayerConfigWidget): def symbologyWidget(self) -> typing.Union[QgsRendererRasterPropertiesWidget, QgsRendererPropertiesDialog]: return self.scrollArea.widget() - def style(self) -> QgsStyle: + def style(self) -> QgsMapLayerStyle: return self.mStyle def menuButtonMenu(self) -> QMenu: @@ -302,6 +303,13 @@ class SymbologyConfigWidget(QpsMapLayerConfigWidget): else: QMessageBox.information(self, 'Load Style', msg) + def reset(self): + + lyr = self.mapLayer() + if isinstance(lyr, QgsMapLayer): + lyr.styleManager().reset() + self.syncToLayer() + def saveStyle(self, fileName: str = None): lastUsedDir = QgsSettings().value("style/lastStyleDir") @@ -364,6 +372,9 @@ class SymbologyConfigWidget(QpsMapLayerConfigWidget): w = self.symbologyWidget() lyr = self.mapLayer() + if isinstance(lyr, QgsMapLayer): + self.mStyle = lyr.styleManager().style(lyr.styleManager().currentStyle()) + if isinstance(lyr, QgsRasterLayer): r = lyr.renderer() if isinstance(w, QgsRendererRasterPropertiesWidget): @@ -377,11 +388,12 @@ class SymbologyConfigWidget(QpsMapLayerConfigWidget): self.setSymbologyWidget(w) elif isinstance(lyr, QgsVectorLayer): + w = None if not isinstance(w, QgsRendererPropertiesDialog): - w = QgsRendererPropertiesDialog(lyr, self.style(), embedded=True) + w = QgsRendererPropertiesDialog(lyr, QgsStyle.defaultStyle(), embedded=True) self.setSymbologyWidget(w) else: - s = "" + pass else: diff --git a/eotimeseriesviewer/externals/qps/layerconfigwidgets/gdalmetadata.py b/eotimeseriesviewer/externals/qps/layerconfigwidgets/gdalmetadata.py index f6144aea..dc464879 100644 --- a/eotimeseriesviewer/externals/qps/layerconfigwidgets/gdalmetadata.py +++ b/eotimeseriesviewer/externals/qps/layerconfigwidgets/gdalmetadata.py @@ -34,7 +34,7 @@ from qgis.PyQt.QtGui import * from qgis.PyQt.QtWidgets import * from qgis.core import QgsRasterLayer, QgsVectorLayer, QgsMapLayer, \ QgsVectorDataProvider, QgsRasterDataProvider, Qgis -from qgis.gui import QgsMapCanvas, QgsMapLayerConfigWidgetFactory, QgsDoubleSpinBox +from qgis.gui import QgsMapCanvas, QgsMapLayerConfigWidgetFactory, QgsDoubleSpinBox, QgsMessageBar from .core import QpsMapLayerConfigWidget from ..classification.classificationscheme import ClassificationScheme, ClassificationSchemeWidget from ..utils import loadUi, gdalDataset, parseWavelength, parseFWHM @@ -115,6 +115,12 @@ class GDALErrorHandler(object): self.err_no=err_no self.err_msg=err_msg + if err_level == gdal.CE_Warning: + pass + + if err_level > gdal.CE_Warning: + raise RuntimeError(err_level, err_no, err_msg) + class GDALBandMetadataItem(object): @@ -129,6 +135,7 @@ class GDALBandMetadataItem(object): class GDALBandMetadataModel(QAbstractTableModel): sigWavelengthUnitsChanged = pyqtSignal(str) + sigMessage = pyqtSignal(str, Qgis.MessageLevel) def __init__(self, parent=None): super().__init__(parent) @@ -136,6 +143,8 @@ class GDALBandMetadataModel(QAbstractTableModel): self.cnWavelength = 'Wavelength' self.cnFWHM = 'FWHM' + self.mErrorHandler: GDALErrorHandler = GDALErrorHandler() + self.mWavelengthUnitModel = XUnitModel() self.mWavelengthUnitModel.mDescription[BAND_INDEX] = 'None' self.mWavelengthUnitModel.mToolTips[BAND_INDEX] = 'No wavelength defined' @@ -323,45 +332,72 @@ class GDALBandMetadataModel(QAbstractTableModel): def applyToLayer(self, *args): if isinstance(self.mMapLayer, QgsRasterLayer) and self.mMapLayer.isValid(): - def list_or_empty(values): + def list_or_empty(values, domain:str=None): for v in values: - if v not in ['', None, 'None']: - return ','.join(values) + if v in ['', None, 'None']: + return '' - return '' + result = ','.join(values) + if domain == 'ENVI': + result = f'{{{result}}}' + return result if self.mMapLayer.dataProvider().name() == 'gdal': + gdal.PushErrorHandler(self.mErrorHandler.handler) try: ds: gdal.Dataset = gdal.Open(self.mMapLayer.source(), gdal.GA_Update) assert isinstance(ds, gdal.Dataset) - except Exception as ex: - print(f'unable to open image in update mode: {self.mMapLayer.source()}') - print(ex, file=sys.stderr) + except RuntimeError as ex: + msg = f'{ex}\nMetadata might not get saved for {self.mMapLayer.source()}' + self.sigMessage.emit(msg, Qgis.Warning) + ds: gdal.Dataset = gdal.Open(self.mMapLayer.source(), gdal.GA_ReadOnly) - is_envi = ds.GetDriver().ShortName == 'ENVI' - if is_envi: - domain = 'ENVI' - else: - domain = None - if self.mWavelengthUnit not in [None, '', BAND_INDEX]: - ds.SetMetadataItem('wavelength units ', self.mWavelengthUnit, domain) + try: - wl = [str(item.wavelength) for item in self.mBandMetadata] - ds.SetMetadataItem('wavelength', list_or_empty(wl), domain) + # if ENVI driver, save to 'ENVI' domain + # if other driver, save to default domain - fwhm = [str(item.fwhm) for item in self.mBandMetadata] - ds.SetMetadataItem('fwhm', list_or_empty(fwhm), domain) + is_envi = ds.GetDriver().ShortName == 'ENVI' + if is_envi: + domain = 'ENVI' + else: + domain = None - for b, item in enumerate(self.mBandMetadata): - assert isinstance(item, GDALBandMetadataItem) - band: gdal.Band = ds.GetRasterBand(b+1) - band.SetDescription(item.name) + if self.mWavelengthUnit not in [None, '', BAND_INDEX]: + + # remove potential concurrent definitions of wavelength unit and fwhm + for k in ['wavelength_units', 'wavelength units', 'fwhm', 'wavelength', 'wavelengths']: + for d in ['ENVI', None]: + ds.SetMetadataItem(k, None, d) + for b in range(ds.RasterCount): + band: gdal.Band = ds.GetRasterBand(b+1) + band.SetMetadataItem(k, None, d) - band.FlushCache() + ds.SetMetadataItem('wavelength units', self.mWavelengthUnit, domain) - ds.FlushCache() - del ds + wl = [str(item.wavelength) for item in self.mBandMetadata] + ds.SetMetadataItem('wavelength', list_or_empty(wl), domain) + + fwhm = [str(item.fwhm) for item in self.mBandMetadata] + ds.SetMetadataItem('fwhm', list_or_empty(fwhm), domain) + + for b, item in enumerate(self.mBandMetadata): + assert isinstance(item, GDALBandMetadataItem) + band: gdal.Band = ds.GetRasterBand(b+1) + band.SetDescription(item.name) + + band.FlushCache() + + ds.FlushCache() + del ds + + except Exception as ex: + msg = f'{ex}' + self.sigMessage.emit(msg, Qgis.Critical) + print(msg, file=sys.stderr) + finally: + gdal.PopErrorHandler() def validate(self) -> typing.List[str]: # todo: implement some internal validation and return descriptive error messages @@ -409,7 +445,6 @@ class GDALBandMetadataModel(QAbstractTableModel): self.endResetModel() - class GDALBandMetadataModelTableViewDelegate(QStyledItemDelegate): """ @@ -553,6 +588,7 @@ class GDALBandMetadataModelTableView(QTableView): class GDALMetadataModel(QAbstractTableModel): sigEditable = pyqtSignal(bool) + sigMessage = pyqtSignal(str, Qgis.MessageLevel) def __init__(self, parent=None): super(GDALMetadataModel, self).__init__(parent) @@ -566,9 +602,9 @@ class GDALMetadataModel(QAbstractTableModel): self.cnValue = 'Value(s)' self._column_names = [self.cnItem, self.cnDomain, self.cnKey, self.cnValue] - self._isEditable = False + self._isEditable: bool = False self._MDItems: typing.List[GDALMetadataItem] = [] - self._MOKs:typing.List[str] = [] + self._MOKs: typing.List[str] = [] def resetChanges(self): c = self._column_names.index(self.cnValue) @@ -653,6 +689,7 @@ class GDALMetadataModel(QAbstractTableModel): mdItems, moks = self._read_maplayer() self._MDItems.extend(mdItems) self._MOKs.extend(moks) + self.endResetModel() def removeItem(self, item:GDALMetadataItem): @@ -679,8 +716,17 @@ class GDALMetadataModel(QAbstractTableModel): if self.mLayer.dataProvider().name() == 'gdal': gdal.PushErrorHandler(self.mErrorHandler.handler) + + try: + ds: gdal.Dataset = gdal.Open(self.mLayer.source(), gdal.GA_Update) + assert isinstance(ds, gdal.Dataset) + except RuntimeError as ex: + msg = f'{ex}\nMetadata might not get saved for {self.mLayer.source()}' + self.sigMessage.emit(msg, Qgis.Warning) + + ds: gdal.Dataset = gdal.Open(self.mLayer.source(), gdal.GA_ReadOnly) + try: - ds = gdal.Open(self.mLayer.source(), gdal.GA_ReadOnly) if isinstance(ds, gdal.Dataset): for item in changed: mo: gdal.MajorObject = None @@ -696,11 +742,9 @@ class GDALMetadataModel(QAbstractTableModel): ds.FlushCache() del ds - if self.mErrorHandler.err_level >= gdal.CE_Warning: - raise RuntimeError(self.mErrorHandler.err_level, - self.mErrorHandler.err_no, - self.mErrorHandler.err_msg) except Exception as ex: + msg = str(ex) + self.sigMessage.emit(msg, Qgis.Critical) print(ex, file=sys.stderr) finally: gdal.PopErrorHandler() @@ -708,8 +752,10 @@ class GDALMetadataModel(QAbstractTableModel): if isinstance(self.mLayer, QgsVectorLayer) and isinstance(self.mLayer.dataProvider(), QgsVectorDataProvider): if self.mLayer.dataProvider().name() == 'ogr': path = self.mLayer.source().split('|')[0] - gdal.PushErrorHandler(self.mErrorHandler.handler) + use_ogr_exception = ogr.GetUseExceptions() + try: + ogr.UseExceptions() ds: ogr.DataSource = ogr.Open(path, update=1) if isinstance(ds, ogr.DataSource): for item in changed: @@ -724,14 +770,18 @@ class GDALMetadataModel(QAbstractTableModel): mo.SetMetadataItem(item.key, item.value, item.domain) ds.FlushCache() - if self.mErrorHandler.err_level >= gdal.CE_Warning: - raise RuntimeError(self.mErrorHandler.err_level, - self.mErrorHandler.err_no, - self.mErrorHandler.err_msg) + except Exception as ex: + msg = str(ex) + self.sigMessage.emit(msg, Qgis.Critical) print(ex, file=sys.stderr) finally: - gdal.PopErrorHandler() + if use_ogr_exception: + ogr.UseExceptions() + else: + ogr.DontUseExceptions() + + #gdal.PopErrorHandler() def index2MDItem(self, index: QModelIndex) -> GDALMetadataItem: """ @@ -819,12 +869,16 @@ class GDALMetadataModel(QAbstractTableModel): items = [] major_objects = [] + + if not isinstance(self.mLayer, QgsMapLayer) or not self.mLayer.isValid(): + self.setIsEditable(False) return items, major_objects + is_editable = False if isinstance(self.mLayer, QgsRasterLayer) and self.mLayer.dataProvider().name() == 'gdal': ds = gdal.Open(self.mLayer.source()) - + is_editable = True if isinstance(ds, gdal.Dataset): z = len(str(ds.RasterCount)) mok = 'Dataset' @@ -842,6 +896,8 @@ class GDALMetadataModel(QAbstractTableModel): if isinstance(self.mLayer, QgsVectorLayer) and self.mLayer.dataProvider().name() == 'ogr': ds = ogr.Open(self.mLayer.source().split('|')[0]) if isinstance(ds, ogr.DataSource): + drv: ogr.Driver = ds.GetDriver() + is_editable = False sep = self.mLayer.dataProvider().sublayerSeparator() subLayers = self.mLayer.dataProvider().subLayers() if len(subLayers) > 0: @@ -862,6 +918,7 @@ class GDALMetadataModel(QAbstractTableModel): for (domain, key, value) in self._read_majorobject(lyr): items.append(GDALMetadataItem(mok, domain, key, value)) + self.setIsEditable(is_editable) return items, major_objects @@ -875,7 +932,7 @@ class GDALMetadataModelTableView(QTableView): def contextMenuEvent(self, event: QContextMenuEvent) -> None: """ - Opens a context menue + Opens a context menu """ index = self.indexAt(event.pos()) if index.isValid(): @@ -915,8 +972,6 @@ class GDALMetadataItemDialog(QDialog): self.tbValue.textChanged.connect(self.validate) self.cbDomain.currentTextChanged.connect(self.validate) self.cbMajorObject.currentTextChanged.connect(self.validate) - - self.validate() def validate(self, *args): @@ -984,12 +1039,14 @@ class GDALMetadataModelConfigWidget(QpsMapLayerConfigWidget): pathUi = pathlib.Path(__file__).parents[1] / 'ui' / 'gdalmetadatamodelwidget.ui' loadUi(pathUi, self) + self.mMessageBar: QgsMessageBar self.tbFilter: QLineEdit self.btnMatchCase.setDefaultAction(self.optionMatchCase) self.btnRegex.setDefaultAction(self.optionRegex) self._cs = None self.bandMetadataModel = GDALBandMetadataModel() + self.bandMetadataModel.sigMessage.connect(self.showMessage) self.bandMetadataProxyModel = QSortFilterProxyModel() self.bandMetadataProxyModel.setSourceModel(self.bandMetadataModel) self.bandMetadataProxyModel.setFilterKeyColumn(-1) @@ -1002,6 +1059,7 @@ class GDALMetadataModelConfigWidget(QpsMapLayerConfigWidget): self.bandMetadataModel.registerWavelengthUnitComboBox(self.cbWavelengthUnits) self.metadataModel = GDALMetadataModel() + self.metadataModel.sigMessage.connect(self.showMessage) self.metadataModel.sigEditable.connect(self.onEditableChanged) self.metadataProxyModel = QSortFilterProxyModel() self.metadataProxyModel.setSourceModel(self.metadataModel) @@ -1031,6 +1089,18 @@ class GDALMetadataModelConfigWidget(QpsMapLayerConfigWidget): self.actionRemoveItem.triggered.connect(self.onRemoveSelectedItems) self.onEditableChanged(self.metadataModel.isEditable()) + def showMessage(self, msg:str, level: Qgis.MessageLevel): + + if level == Qgis.Critical: + duration = 200 + else: + duration = 50 + line1 = msg.splitlines()[0] + self.messageBar().pushMessage('', line1, msg, level, duration) + + def messageBar(self) -> QgsMessageBar: + return self.mMessageBar + def onWavelengthUnitsChanged(self): wlu = self.bandMetadataModel.wavelenghtUnit() @@ -1086,7 +1156,7 @@ class GDALMetadataModelConfigWidget(QpsMapLayerConfigWidget): if not (isinstance(layer, QgsMapLayer) and layer.isValid()): self.is_gdal = self.is_ogr = self.supportsGDALClassification = False else: - + self.supportsGDALClassification = False self.is_gdal = isinstance(layer, QgsRasterLayer) and layer.dataProvider().name() == 'gdal' self.is_ogr = isinstance(layer, QgsVectorLayer) and layer.dataProvider().name() == 'ogr' @@ -1129,6 +1199,8 @@ class GDALMetadataModelConfigWidget(QpsMapLayerConfigWidget): self.metadataModel.setLayer(lyr) if self.supportsGDALClassification: self._cs = ClassificationScheme.fromMapLayer(lyr) + else: + self._cs = None if isinstance(self._cs, ClassificationScheme) and len(self._cs) > 0: self.gbClassificationScheme.setVisible(True) @@ -1188,7 +1260,7 @@ class GDALMetadataConfigWidgetFactory(QgsMapLayerConfigWidgetFactory): def createWidget(self, layer, canvas, dockWidget=True, parent=None) -> GDALMetadataModelConfigWidget: w = GDALMetadataModelConfigWidget(layer, canvas, parent=parent) - w.metadataModel.setIsEditable(True) + #w.metadataModel.setIsEditable(True) w.setWindowTitle(self.title()) w.setWindowIcon(self.icon()) return w diff --git a/eotimeseriesviewer/externals/qps/layerconfigwidgets/vectorlayerfields.py b/eotimeseriesviewer/externals/qps/layerconfigwidgets/vectorlayerfields.py index d2448092..c589fe1c 100644 --- a/eotimeseriesviewer/externals/qps/layerconfigwidgets/vectorlayerfields.py +++ b/eotimeseriesviewer/externals/qps/layerconfigwidgets/vectorlayerfields.py @@ -22,19 +22,18 @@ along with this software. If not, see <http://www.gnu.org/licenses/>. *************************************************************************** """ -import typing, pathlib, sys -from qgis.core import QgsRasterLayer, QgsRasterRenderer -from qgis.core import * -from qgis.gui import QgsMapCanvas, QgsMapLayerConfigWidget, QgsRasterBandComboBox -from qgis.gui import * -from qgis.PyQt.QtWidgets import * -from qgis.PyQt.QtGui import * +import sys + from qgis.PyQt.QtCore import * +from qgis.PyQt.QtGui import * from qgis.PyQt.QtGui import QIcon -from ..utils import loadUi -import numpy as np +from qgis.PyQt.QtWidgets import * +from qgis.core import QgsMapLayer, QgsVectorLayer, QgsField, QgsFieldModel, QgsEditorWidgetSetup +from qgis.gui import QgsEditorWidgetFactory, QgsCollapsibleGroupBox, QgsEditorConfigWidget, QgsGui, \ + QgsMapLayerConfigWidgetFactory from .core import QpsMapLayerConfigWidget from ..layerproperties import AddAttributeDialog +from ..utils import loadUi class LayerFieldsTableModel(QgsFieldModel): @@ -53,7 +52,8 @@ class LayerFieldsTableModel(QgsFieldModel): self.cnWMS = 'WMS' self.cnWFS = 'WFS' - self.mColumnNames = [self.cnId, self.cnName, self.cnAlias, self.cnType, self.cnTypeName, self.cnLength, self.cnPrecision, self.cnComment, self.cnWMS, self.cnWFS] + self.mColumnNames = [self.cnId, self.cnName, self.cnAlias, self.cnType, self.cnTypeName, self.cnLength, + self.cnPrecision, self.cnComment, self.cnWMS, self.cnWFS] def columnNames(self): return self.mColumnNames[:] @@ -77,10 +77,10 @@ class LayerFieldsTableModel(QgsFieldModel): return col return None - def columnCount(self, parent:QModelIndex): + def columnCount(self, parent: QModelIndex): return len(self.mColumnNames) - def data(self, index:QModelIndex, role:int): + def data(self, index: QModelIndex, role: int): if not index.isValid(): return None @@ -112,7 +112,7 @@ class LayerFieldsTableModel(QgsFieldModel): return None if cn == self.cnWFS: return None - if role in [QgsFieldModel.FieldNameRole , + if role in [QgsFieldModel.FieldNameRole, QgsFieldModel.FieldIndexRole, QgsFieldModel.ExpressionRole, QgsFieldModel.IsExpressionRole, @@ -126,12 +126,12 @@ class LayerFieldsTableModel(QgsFieldModel): return super().data(index, role) - class LayerFieldsListModel(QgsFieldModel): """ A model to show the QgsFields of an QgsVectorLayer as vertical list Inherits QgsFieldModel and allows to change the name of the 1st column. """ + def __init__(self, parent=None): """ Constructor @@ -175,13 +175,14 @@ class LayerFieldsListModel(QgsFieldModel): self.headerDataChanged.emit(orientation, col, col) return result -class LayerAttributeFormConfigEditorWidget(QWidget): +class LayerAttributeFormConfigEditorWidget(QWidget): class ConfigInfo(QStandardItem): """ Describes a QgsEditorWidgetFactory configuration. """ - def __init__(self, key:str, factory:QgsEditorWidgetFactory, configWidget:QgsEditorConfigWidget): + + def __init__(self, key: str, factory: QgsEditorWidgetFactory, configWidget: QgsEditorConfigWidget): super(LayerAttributeFormConfigEditorWidget.ConfigInfo, self).__init__() assert isinstance(key, str) @@ -194,7 +195,6 @@ class LayerAttributeFormConfigEditorWidget(QWidget): self.setToolTip(factory.name()) self.mInitialConfig = dict(configWidget.config()) - def resetConfig(self): """ Resets the widget to its initial values @@ -243,10 +243,9 @@ class LayerAttributeFormConfigEditorWidget(QWidget): """ return QgsEditorWidgetSetup(self.factoryKey(), self.config()) - sigChanged = pyqtSignal(object) - def __init__(self, parent, layer:QgsVectorLayer, index:int): + def __init__(self, parent, layer: QgsVectorLayer, index: int): super(LayerAttributeFormConfigEditorWidget, self).__init__(parent) self.setLayout(QVBoxLayout()) @@ -309,7 +308,6 @@ class LayerAttributeFormConfigEditorWidget(QWidget): self.layout().addStretch() self.cbWidgetType.setCurrentIndex(iCurrent) - conf = self.currentFieldConfig() self.mInitialFactoryKey = None self.mInitialConf = None @@ -317,9 +315,7 @@ class LayerAttributeFormConfigEditorWidget(QWidget): self.mInitialFactoryKey = conf.factoryKey() self.mInitialConf = conf.config() - - - def setFactory(self, factoryKey:str): + def setFactory(self, factoryKey: str): """ Shows the QgsEditorConfigWidget of QgsEditorWidgetFactory `factoryKey` :param factoryKey: str @@ -331,7 +327,6 @@ class LayerAttributeFormConfigEditorWidget(QWidget): self.cbWidgetType.setCurrentIndex(i) break - def changed(self) -> bool: """ Returns True if the QgsEditorWidgetFactory or its configuration has been changed @@ -364,7 +359,6 @@ class LayerAttributeFormConfigEditorWidget(QWidget): Resets the widget to its initial status """ if self.changed(): - self.setFactory(self.mInitialFactoryKey) self.currentEditorConfigWidget().setConfig(self.mInitialConf) @@ -379,7 +373,6 @@ class LayerAttributeFormConfigEditorWidget(QWidget): self.stackedWidget.setCurrentIndex(index) fieldConfig = self.currentFieldConfig() if isinstance(fieldConfig, LayerAttributeFormConfigEditorWidget.ConfigInfo): - self.sigChanged.emit(self) @@ -387,6 +380,7 @@ class LayerAttributeFormConfigWidget(QpsMapLayerConfigWidget): """ A widget to set QgsVectorLayer field settings """ + def __init__(self, *args, **kwds): super().__init__(*args, **kwds) @@ -401,14 +395,13 @@ class LayerAttributeFormConfigWidget(QpsMapLayerConfigWidget): self.treeView.selectionModel().currentRowChanged.connect(self.onSelectedFieldChanged) self.updateFieldWidgets() - - def menuButtonMenu(self) ->QMenu: + def menuButtonMenu(self) -> QMenu: m = QMenu('Reset') a = m.addAction('Reset') a.triggered.connect(self.onReset) return m - def onSelectedFieldChanged(self, index1:QModelIndex, index2:QModelIndex): + def onSelectedFieldChanged(self, index1: QModelIndex, index2: QModelIndex): """ Shows the widget for the selected QgsField :param index1: @@ -420,14 +413,14 @@ class LayerAttributeFormConfigWidget(QpsMapLayerConfigWidget): s = "" self.stackedWidget.setCurrentIndex(r) - def onScrollAreaResize(self, resizeEvent:QResizeEvent): + def onScrollAreaResize(self, resizeEvent: QResizeEvent): """ Forces the stackedWidget's width to fit into the scrollAreas viewport :param resizeEvent: QResizeEvent """ assert isinstance(resizeEvent, QResizeEvent) self.stackedWidget.setMaximumWidth(resizeEvent.size().width()) - s ="" + s = "" def onReset(self): @@ -455,8 +448,6 @@ class LayerAttributeFormConfigWidget(QpsMapLayerConfigWidget): w.apply() self.onSettingsChanged() - - def updateFieldWidgets(self): """ Empties the stackedWidget and populates it with a FieldConfigEditor @@ -511,6 +502,7 @@ class LayerAttributeFormConfigWidgetFactory(QgsMapLayerConfigWidgetFactory): def supportsStyleDock(self): return False + class LayerFieldsConfigWidget(QpsMapLayerConfigWidget): """ A widget to edit the fields of a QgsVectorLayer @@ -543,7 +535,6 @@ class LayerFieldsConfigWidget(QpsMapLayerConfigWidget): self.actionFieldCalculator.setEnabled(False) self.actionFieldCalculator.setVisible(False) - self.validate() lyr.editingStarted.connect(self.validate) @@ -554,12 +545,10 @@ class LayerFieldsConfigWidget(QpsMapLayerConfigWidget): bEdit = isinstance(self.mapLayer(), QgsVectorLayer) and self.mapLayer().isEditable() bSelected = len(self.tableView.selectionModel().selectedRows()) > 0 - self.actionAddField.setEnabled(bEdit) self.actionRemoveField.setEnabled(bEdit and bSelected) self.actionToggleEditing.setChecked(bEdit) - def onAddField(self): lyr = self.mapLayer() if isinstance(lyr, QgsVectorLayer) and lyr.isEditable(): @@ -585,7 +574,7 @@ class LayerFieldsConfigWidget(QpsMapLayerConfigWidget): else: if lyr.isModified(): result = QMessageBox.question(self, 'Leaving edit mode', 'Save changes?', - buttons=QMessageBox.No | QMessageBox.Yes, defaultButton=QMessageBox.Yes) + buttons=QMessageBox.No | QMessageBox.Yes, defaultButton=QMessageBox.Yes) if result == QMessageBox.Yes: if not lyr.commitChanges(): errors = lyr.commitErrors() @@ -601,7 +590,8 @@ class LayerFieldsConfigWidget(QpsMapLayerConfigWidget): lyr = self.mapLayer() if not isinstance(lyr, QgsVectorLayer): return - indices = [self.mProxyModel.mapToSource(idx).data(QgsFieldModel.FieldIndexRole) for idx in self.tableView.selectionModel().selectedRows()] + indices = [self.mProxyModel.mapToSource(idx).data(QgsFieldModel.FieldIndexRole) for idx in + self.tableView.selectionModel().selectedRows()] if not lyr.deleteAttributes(indices): errors = lyr.errors() @@ -638,5 +628,3 @@ class LayerFieldsConfigWidgetFactory(QgsMapLayerConfigWidgetFactory): def supportsStyleDock(self): return False - - diff --git a/eotimeseriesviewer/externals/qps/layerproperties.py b/eotimeseriesviewer/externals/qps/layerproperties.py index d0be19e2..09a0b2b2 100644 --- a/eotimeseriesviewer/externals/qps/layerproperties.py +++ b/eotimeseriesviewer/externals/qps/layerproperties.py @@ -125,6 +125,21 @@ MDF_QGIS_LAYER_STYLE = 'application/qgis.style' MDF_TEXT_PLAIN = 'text/plain' +class FieldListModel(QAbstractListModel): + + def __init__(self, *args, layer:QgsVectorLayer=None, **kwds): + + super().__init__(*args, **kwds) + + def setLayer(self, layer:QgsVectorLayer): + + self.mLayer = layer + + def flags(self, index:QModelIndex): + pass + + + class AddAttributeDialog(QDialog): """ A dialog to set up a new QgsField. @@ -159,6 +174,7 @@ class AddAttributeDialog(QDialog): assert isinstance(ntype, QgsVectorDataProvider.NativeType) o = Option(ntype, name=ntype.mTypeName, toolTip=ntype.mTypeDesc) self.typeModel.addOption(o) + self.cbType.setModel(self.typeModel) self.cbType.currentIndexChanged.connect(self.onTypeChanged) l.addWidget(QLabel('Type'), 2, 0) @@ -800,6 +816,7 @@ class LayerPropertiesDialog(QgsOptionsDialogBase): assert isinstance(self.mOptionsStackedWidget, QStackedWidget) assert isinstance(lyr, QgsMapLayer) self.btnConfigWidgetMenu: QPushButton = QPushButton('<menu>') + self.btnConfigWidgetMenu.setVisible(False) assert isinstance(self.btnConfigWidgetMenu, QPushButton) self.mOptionsListWidget.currentRowChanged.connect(self.onPageChanged) self.mLayer: QgsMapLayer = lyr @@ -822,6 +839,9 @@ class LayerPropertiesDialog(QgsOptionsDialogBase): self.btnCancel: QPushButton = self.buttonBox.button(QDialogButtonBox.Cancel) self.btnOk: QPushButton = self.buttonBox.button(QDialogButtonBox.Ok) + self.btnHelp: QPushButton = self.buttonBox.button(QDialogButtonBox.Help) + # not connected + self.btnHelp.setVisible(False) s = "" assert isinstance(self.mOptionsListWidget, QListWidget) @@ -854,7 +874,7 @@ class LayerPropertiesDialog(QgsOptionsDialogBase): menu = None if isinstance(page, QgsMapLayerConfigWidget): - # comes with QIS 3.12 + # comes with QGIS 3.12 if hasattr(page, 'menuButtonMenu'): menu = page.menuButtonMenu() @@ -1230,7 +1250,7 @@ class AttributeTableWidget(QMainWindow, QgsExpressionContextGenerator): self.mActionSearchForm.setEnabled(False) self.mActionSearchForm.setToolTip(tr("Search is not supported when using custom UI forms")) - self.editingToggled(); + self.editingToggled() self._hide_unconnected_widgets() @@ -1268,10 +1288,7 @@ class AttributeTableWidget(QMainWindow, QgsExpressionContextGenerator): self.mMainView.openConditionalStyles() def mActionCutSelectedRows_triggered(self): - - pass - - # QgisApp:: instance() -> cutSelectionToClipboard(mLayer); + self.vectorLayerTools().cutSelectionToClipboard(self.mLayer) def mActionCopySelectedRows_triggered(self): self.vectorLayerTools().copySelectionToClipboard(self.mLayer) @@ -1628,7 +1645,7 @@ class AttributeTableWidget(QMainWindow, QgsExpressionContextGenerator): self.vectorLayerTools().panToSelected(self.mLayer) def mActionDeleteSelected_triggered(self): - self.vectorLayerTools().deleteSelected(self.mLayer) + self.vectorLayerTools().deleteSelection(self.mLayer) def reloadModel(self): """ diff --git a/eotimeseriesviewer/externals/qps/maptools.py b/eotimeseriesviewer/externals/qps/maptools.py index 5d3ff078..a2e91ec2 100644 --- a/eotimeseriesviewer/externals/qps/maptools.py +++ b/eotimeseriesviewer/externals/qps/maptools.py @@ -919,7 +919,7 @@ class QgsMapToolDigitizeFeature(QgsMapToolCapture): g = QgsGeometry(savePoint) elif QgsWkbTypes.isMultiType(layerWKBType) and not QgsWkbTypes.hasZ(layerWKBType): # g = QgsGeometry::fromMultiPointXY( QgsMultiPointXY() << savePoint ); - g = QgsGeometry.fromMultiPointXY(savePoint) + #g = QgsGeometry.fromMultiPointXY(savePoint) g = QgsGeometry(savePoint) elif not QgsWkbTypes.isMultiType(layerWKBType) and QgsWkbTypes.hasZ(layerWKBType): g = QgsGeometry(QgsPoint(savePoint.x(), savePoint.y(), diff --git a/eotimeseriesviewer/externals/qps/models.py b/eotimeseriesviewer/externals/qps/models.py index a37254ac..449ddfa4 100644 --- a/eotimeseriesviewer/externals/qps/models.py +++ b/eotimeseriesviewer/externals/qps/models.py @@ -24,20 +24,14 @@ along with this software. If not, see <http://www.gnu.org/licenses/>. *************************************************************************** """ - - -import os, pickle, copy, enum - -from collections import OrderedDict - -from qgis.core import * -from qgis.gui import * - -from PyQt5.QtCore import * -from PyQt5.QtGui import * -from PyQt5.QtWidgets import * - -from osgeo import gdal, osr +import warnings +import copy +import enum +import typing +from qgis.PyQt.QtCore import QModelIndex, QAbstractItemModel, QAbstractListModel, \ + pyqtSignal, Qt, QObject, QAbstractListModel, QSize +from qgis.PyQt.QtWidgets import QComboBox, QTreeView +from qgis.PyQt.QtGui import QIcon def currentComboBoxValue(comboBox): @@ -49,6 +43,7 @@ def currentComboBoxValue(comboBox): else: return comboBox.currentData() + def setCurrentComboBoxValue(comboBox, value): """ Sets a QComboBox to the value `value`, if it exists in the underlying item list @@ -125,7 +120,6 @@ class Option(object): return other.mValue == self.mValue - class OptionListModel(QAbstractListModel): def __init__(self, options=None, parent=None): super(OptionListModel, self).__init__(parent) @@ -148,6 +142,7 @@ class OptionListModel(QAbstractListModel): self.insertOptions(options) sigOptionsInserted = pyqtSignal(list) + def insertOptions(self, options, i=None): if options is None: return @@ -173,7 +168,6 @@ class OptionListModel(QAbstractListModel): self.sigOptionsInserted.emit(options) - def o2o(self, value): if not isinstance(value, Option): value = Option(value, '{}'.format(value)) @@ -192,6 +186,7 @@ class OptionListModel(QAbstractListModel): return [o.mValue for o in self.options()] sigOptionsRemoved = pyqtSignal(list) + def removeOptions(self, options): """ Removes a list of options from this Options list. @@ -237,16 +232,12 @@ class OptionListModel(QAbstractListModel): break return idx - def optionNames(self): return [o.mName for o in self.mOptions] def optionValues(self): return [o.mValue for o in self.mOptions] - - - def data(self, index, role=Qt.DisplayRole): if not index.isValid(): return None @@ -264,35 +255,37 @@ class OptionListModel(QAbstractListModel): elif role == Qt.DecorationRole: result = option.mIcon elif role == Qt.UserRole: - result = option + result = option return result - class TreeNode(QObject): - sigWillAddChildren = pyqtSignal(QObject, int, int) sigAddedChildren = pyqtSignal(QObject, int, int) sigWillRemoveChildren = pyqtSignal(QObject, int, int) sigRemovedChildren = pyqtSignal(QObject, int, int) sigUpdated = pyqtSignal(QObject) - sigExpandedChanged = pyqtSignal(QObject, bool) - - def __init__(self, parentNode, name=None, value=None, values=None, icon=None, toolTip:str=None, statusTip:str=None, **kwds): - super(TreeNode, self).__init__() - QObject.__init__(self) - self.mParentNode = parentNode - - self.mChildren = [] - self.mName = name - self.mValues = [] - self.mIcon = None - self.mToolTip = None - self.mCheckState = Qt.Unchecked - self.mCheckable = False - self.mStatusTip = '' - self.mExpanded = False + def __init__(self, + name: str = None, + value: any = None, + values=None, + icon: QIcon = None, + toolTip: str = None, + statusTip: str = None, + **kwds): + + super().__init__() + + self.mParentNode: TreeNode = None + self.mChildren: typing.List[TreeNode] = [] + self.mName: str = name + self.mValues: list = [] + self.mIcon: QIcon = None + self.mToolTip: str = None + self.mCheckState: Qt.CheckState = Qt.Unchecked + self.mCheckable: bool = False + self.mStatusTip: str = '' if name: self.setName(name) @@ -302,43 +295,44 @@ class TreeNode(QObject): self.setIcon(icon) if toolTip: self.setToolTip(toolTip) - if statusTip: self.setStatusTip(statusTip) if values is not None: self.setValues(values) - if isinstance(parentNode, TreeNode): - parentNode.appendChildNodes([self]) - - s = "" - - def __iter__(self): return iter(self.mChildren) def __len__(self): return len(self.mChildren) + def __contains__(self, item): + return item in self.mChildren + + def __getitem__(self, slice): + return self.mChildren[slice] - def setExpanded(self, expanded:bool): + def depth(self) -> int: + d = 0 + parent = self.parentNode() + while isinstance(parent, TreeNode): + d += 1 + parent = parent.parentNode() + return d + + def columnCount(self) -> int: """ - Expands the node - :param expanded: + A node has at least one column for its name :return: + :rtype: """ - assert isinstance(expanded, bool) - b = self.mExpanded != expanded - self.mExpanded = expanded - - if b and not self.signalsBlocked(): - self.sigExpandedChanged.emit(self, self.mExpanded) + return len(self.mValues) + 1 def expanded(self) -> bool: return self.mExpanded == True - def setStatusTip(self, statusTip:str): + def setStatusTip(self, statusTip: str): """ Sets the nodes's status tip to the string specified by statusTip. :param statusTip: str @@ -366,11 +360,10 @@ class TreeNode(QObject): def isCheckable(self) -> bool: return self.mCheckable == True - def setCheckable(self, b:bool): + def setCheckable(self, b: bool): assert isinstance(b, bool) self.mCheckable = b - def clone(self, parent=None): n = TreeNode(parent) @@ -384,52 +377,57 @@ class TreeNode(QObject): childNode.clone(parent=n) return n - - - def nodeIndex(self): - return self.mParentNode.mChildren.index(self) + def nodeIndex(self) -> int: + p = self.parentNode() + if isinstance(p, TreeNode): + return p.mChildren.index(self) + else: + None def next(self): - i = self.nodeIndex() - if i < len(self.mChildren.mChildren): - return self.mParentNode.mChildren[i + 1] - else: - return None + p = self.parentNode() + if isinstance(p, TreeNode): + i = p.mChildren.index(self) + if i < len(p.mChildren) - 1: + return p.mChildren[i + 1] + return None def previous(self): - i = self.nodeIndex() - if i > 0: - return self.mParentNode.mChildren[i - 1] - else: - return None + p = self.parentNode() + if isinstance(p, TreeNode): + i = p.mChildren.index(self) + if i > 0: + return p.mChildren[i - 1] + return None def detach(self): """ Detaches this TreeNode from its parent TreeNode :return: """ - if isinstance(self.mParentNode, TreeNode): - self.mParentNode.mChildren.remove(self) - self.setParentNode(None) + p = self.parent() + if isinstance(p, TreeNode): + p.removeChildNodes(self) + + def hasChildren(self) -> bool: + return len(self.mChildren) > 0 - def appendChildNodes(self, listOfChildNodes): - self.insertChildNodes(len(self.mChildren), listOfChildNodes) + def appendChildNodes(self, child_nodes): + self.insertChildNodes(len(self.mChildren), child_nodes) - def insertChildNodes(self, index, listOfChildNodes): + def insertChildNodes(self, index: int, child_nodes): assert index <= len(self.mChildren) - if isinstance(listOfChildNodes, TreeNode): - listOfChildNodes = [listOfChildNodes] - assert isinstance(listOfChildNodes, list) - listOfChildNodes = [l for l in listOfChildNodes if l not in self.mChildren] + if isinstance(child_nodes, TreeNode): + child_nodes = [child_nodes] + assert isinstance(child_nodes, list) + child_nodes = [l for l in child_nodes if l not in self.mChildren] - l = len(listOfChildNodes) + l = len(child_nodes) idxLast = index + l - 1 self.sigWillAddChildren.emit(self, index, idxLast) - if not self.signalsBlocked(): - self.sigWillAddChildren.emit(self, index, idxLast) - for i, node in enumerate(listOfChildNodes): + + for i, node in enumerate(child_nodes): assert isinstance(node, TreeNode) - node.mParentNode = self # connect node signals node.sigWillAddChildren.connect(self.sigWillAddChildren) node.sigAddedChildren.connect(self.sigAddedChildren) @@ -437,39 +435,60 @@ class TreeNode(QObject): node.sigRemovedChildren.connect(self.sigRemovedChildren) node.sigUpdated.connect(self.sigUpdated) + node.setParentNode(self) self.mChildren.insert(index + i, node) - import time - t0 = time.time() self.sigAddedChildren.emit(self, index, idxLast) - s = "" - if not self.signalsBlocked(): - self.sigAddedChildren.emit(self, index, idxLast) - def removeChildNode(self, node): - assert node in self.mChildren - i = self.mChildren.index(node) - self.removeChildNodes(i, 1) + def removeAllChildNodes(self): + self.removeChildNodes(self.childNodes()) - def removeChildNodes(self, row, count): + def removeChildNodes(self, child_nodes): + """ + Removes child-nodes + :param child_nodes: + :type child_nodes: + :return: + :rtype: + """ + if isinstance(child_nodes, TreeNode): + child_nodes = [child_nodes] + child_nodes: typing.List[TreeNode] + for node in child_nodes: + assert isinstance(node, TreeNode) + assert node in self.mChildren - if row < 0 or count <= 0: - return False + child_nodes = sorted(child_nodes, key=lambda node: node.nodeIndex()) + while len(child_nodes) > 0: + # find neighbored nodes to remove + nextNode = child_nodes[0] + toRemove = [] + while isinstance(nextNode, TreeNode) and nextNode in child_nodes: + toRemove.append(nextNode) + nextNode = nextNode.next() - rowLast = row + count - 1 + first = toRemove[0].nodeIndex() + last = toRemove[-1].nodeIndex() - if rowLast >= self.childCount(): - return False + self.sigWillRemoveChildren.emit(self, first, last) + + for node in toRemove: + self.mChildren.remove(node) - self.sigWillRemoveChildren.emit(self, row, rowLast) - to_remove = self.childNodes()[row:rowLast + 1] - for n in to_remove: - self.mChildren.remove(n) - # n.mParent = None + # disconnect node signals + node.sigWillAddChildren.disconnect(self.sigWillAddChildren) + node.sigAddedChildren.disconnect(self.sigAddedChildren) + node.sigWillRemoveChildren.disconnect(self.sigWillRemoveChildren) + node.sigRemovedChildren.disconnect(self.sigRemovedChildren) + node.sigUpdated.disconnect(self.sigUpdated) - self.sigRemovedChildren.emit(self, row, rowLast) + node.setParentNode(None) - def setToolTip(self, toolTip:str): + child_nodes.remove(node) + + self.sigRemovedChildren.emit(self, first, last) + + def setToolTip(self, toolTip: str): """ Sets the tooltip :param toolTip: str @@ -483,27 +502,27 @@ class TreeNode(QObject): """ return self.mToolTip + def setParentNode(self, node): + self.mParentNode: TreeNode = node + def parentNode(self): - """ - Returns the parent TreeNode that owns this TreeNode - :return: TreeNode - """ return self.mParentNode - def setParentNode(self, treeNode): - """ - :param treeNode: - :return: - """ - assert isinstance(treeNode, TreeNode) - self.mParentNode = treeNode + def setParent(self, parentNode) -> None: + if parentNode is not None: + warnings.warn('Use setParentNode', DeprecationWarning) + assert isinstance(parentNode, TreeNode) + super().setParent(parentNode) - def setIcon(self, icon:QIcon): + def setIcon(self, icon: QIcon): """ Sets the TreeNode icon :param icon: QIcon """ - self.mIcon = icon + if icon != self.mIcon: + self.mIcon = icon + self.sigUpdated.emit(self) + def icon(self) -> QIcon: """ @@ -512,12 +531,14 @@ class TreeNode(QObject): """ return self.mIcon - def setName(self, name:str): + def setName(self, name: str): """ Sets the TreeNodes name :param name: str """ - self.mName = name + if name != self.mName: + self.mName = str(name) + self.sigUpdated.emit(self) def name(self) -> str: """ @@ -539,22 +560,18 @@ class TreeNode(QObject): else: self.setValues([value]) - def setValues(self, listOfValues:list): + def setValues(self, values: list): """ Sets the values show by this TreeNode - :param listOfValues: [list-of-values] + :param values: [list-of-values] """ - old = self.mValues - if listOfValues is None: - self.mValues = [] - else: - if not isinstance(listOfValues, list): - listOfValues = [listOfValues] - self.mValues = listOfValues[:] - if self.mValues != old: + if not isinstance(values, list): + values = [values] + + if self.mValues != values: + self.mValues.clear() + self.mValues.extend(values) self.sigUpdated.emit(self) - if not self.signalsBlocked(): - self.sigUpdated.emit(self) def values(self) -> list: """ @@ -573,12 +590,10 @@ class TreeNode(QObject): else: return None - def childCount(self) -> int: """Returns the number of child nones""" return len(self.mChildren) - def childNodes(self) -> list: """ Returns the child nodes @@ -586,6 +601,21 @@ class TreeNode(QObject): """ return self.mChildren[:] + def findParentNode(self, nodeType): + """ + Returns the next upper TreeNode of type "nodeType" + :param nodeType: + :return: TreeNode of type "nodeType" or None + """ + + parent = self.parentNode() + if not isinstance(parent, TreeNode): + return None + elif isinstance(parent, nodeType): + return parent + else: + return parent.findParentNode(nodeType) + def findChildNodes(self, type, recursive=True): """ Returns a list of child nodes with node-type `type`. @@ -606,21 +636,32 @@ class TreeModel(QAbstractItemModel): """ A QAbstractItemModel implementation to be used in QTreeViews """ - def __init__(self, parent=None, rootNode=None): - super(TreeModel, self).__init__(parent) - self.mColumnNames = ['Node', 'Value'] - self.mRootNode = rootNode if isinstance(rootNode, TreeNode) else TreeNode(None) + def __init__(self, parent: QObject = None, rootNode: TreeNode = None): + super().__init__(parent) + + self.mRootNode: TreeNode + if isinstance(rootNode, TreeNode): + self.mRootNode = rootNode + else: + self.mRootNode = TreeNode(name='<root node>') + + self.mRootNode.setValues(['Name', 'Value']) self.mRootNode.sigWillAddChildren.connect(self.onNodeWillAddChildren) self.mRootNode.sigAddedChildren.connect(self.onNodeAddedChildren) self.mRootNode.sigWillRemoveChildren.connect(self.onNodeWillRemoveChildren) self.mRootNode.sigRemovedChildren.connect(self.onNodeRemovedChildren) self.mRootNode.sigUpdated.connect(self.onNodeUpdated) - self.mTreeView = None - if isinstance(parent, QTreeView): - self.connectTreeView(parent) - s = "" + def setColumnNames(self, names): + assert isinstance(names, list) + self.mRootNode.setValues(names) + + def __contains__(self, item): + return item in self.mRootNode + + def __getitem__(self, slice): + return self.mRootNode[slice] def rootNode(self) -> TreeNode: """ @@ -629,42 +670,43 @@ class TreeModel(QAbstractItemModel): """ return self.mRootNode - - def onNodeWillAddChildren(self, node, idx1, idxL): + def onNodeWillAddChildren(self, node: TreeNode, first: int, last: int): idxNode = self.node2idx(node) - self.beginInsertRows(idxNode, idx1, idxL) + self.beginInsertRows(idxNode, first, last) - def onNodeAddedChildren(self, *args): + def onNodeAddedChildren(self, node: TreeNode, first: int, last: int): self.endInsertRows() - # for i in range(idx1, idxL+1): + def maxColumnCount(self, index: QModelIndex) -> int: + assert isinstance(index, QModelIndex) + cnt = self.columnCount(index) + for row in range(self.rowCount(index)): + idx = self.index(row, 0, index) + cnt = max(cnt, self.maxColumnCount(idx)) + return cnt - def onNodeWillRemoveChildren(self, node, idx1, idxL): + def onNodeWillRemoveChildren(self, node: TreeNode, first: int, last: int): idxNode = self.node2idx(node) - self.beginRemoveRows(idxNode, idx1, idxL) + self.beginRemoveRows(idxNode, first, last) - def onNodeRemovedChildren(self, node, idx1, idxL): + def onNodeRemovedChildren(self, node: TreeNode, first: int, last: int): self.endRemoveRows() - def onNodeUpdated(self, node): - idxNode = self.node2idx(node) - self.dataChanged.emit(idxNode, idxNode) - + def onNodeUpdated(self, node: TreeNode): + idx = self.node2idx(node) + idx2 = self.createIndex(idx.row(), node.columnCount() - 1) + self.dataChanged.emit(idx, idx2) def headerData(self, section, orientation, role): assert isinstance(section, int) - if orientation == Qt.Horizontal and role == Qt.DisplayRole: - - if len(self.mColumnNames) > section: - return self.mColumnNames[section] + if section < len(self.mRootNode.values()): + return self.mRootNode.values()[section] else: - return '' - - else: - return None + return f'Column {section + 1}' + return None - def parent(self, index:QModelIndex) -> QModelIndex: + def parent(self, index: QModelIndex) -> QModelIndex: """ Returns the parent index of a QModelIndex `index` :param index: QModelIndex @@ -672,42 +714,30 @@ class TreeModel(QAbstractItemModel): """ if not index.isValid(): return QModelIndex() - node = self.idx2node(index) - if not isinstance(node, TreeNode): - return QModelIndex() - - parentNode = node.parentNode() - if not isinstance(parentNode, TreeNode): + childNode: TreeNode = index.internalPointer() + parentNode: TreeNode = childNode.parentNode() + if parentNode == self.mRootNode: return QModelIndex() - return self.node2idx(parentNode) - - if node not in parentNode.mChildren: - return QModelIndex - row = parentNode.mChildren.index(node) - return self.createIndex(row, 0, parentNode) + return self.createIndex(parentNode.nodeIndex(), 0, parentNode) - def rowCount(self, index:QModelIndex) -> int: + def rowCount(self, parent: QModelIndex = None) -> int: """ - Return the row-count, i.e. number of child node for a TreeNode as index `index`. + Return the row-count, i.e. number of child node for a TreeNode at index `index`. :param index: QModelIndex :return: int """ - if index is None: - return len(self.rootNode().mChildren) - node = index.internalPointer() - if isinstance(node, TreeNode): - return node.childCount() - else: - return len(self.mRootNode) - if not index.isValid(): + if parent is None: + parent = QModelIndex() + + if parent.column() > 0: return 0 - #assert isinstance(index, QModelIndex) - return index.internalPointer().childCount() + if not parent.isValid(): + parentNode: TreeNode = self.mRootNode + else: + parentNode: TreeNode = parent.internalPointer() - node = self.idx2node(index) - #node = index.internalPointer() - return len(node.mChildren) if isinstance(node, TreeNode) else 0 + return parentNode.childCount() def hasChildren(self, index=QModelIndex()) -> bool: """ @@ -725,31 +755,50 @@ class TreeModel(QAbstractItemModel): """ return self.mColumnNames[:] - def idx2columnName(self, index:QModelIndex) -> str: + def printModel(self, index: QModelIndex, prefix=''): """ - Returns the column name related to a QModelIndex - :param index: QModelIndex - :return: str, column name + Prints the model oder a sub-node specified by index + :param index: + :type index: + :param prefix: + :type prefix: + :return: + :rtype: """ - if not index.isValid(): - return None - else: - return self.mColumnNames[index.column()] + if index is None: + index = QModelIndex() + if isinstance(index, TreeNode): + index = self.node2idx(index) + print(f'{prefix} {self.data(index, role=Qt.DisplayRole)}') + for r in range(self.rowCount(index)): + idx = self.index(r, 0, parent=index) + self.printModel(idx, prefix=f'{prefix}-') + + def span(self, idx) -> QSize(): - def columnCount(self, index= QModelIndex()) -> int: + return super(TreeModel, self).span(idx) + + def columnCount(self, parent: QModelIndex = None) -> int: """ Returns the number of columns :param index: QModelIndex :return: """ - return len(self.mColumnNames) + return len(self.mRootNode.values()) - def connectTreeView(self, treeView): - self.mTreeView = treeView + """ + if not isinstance(parent, QModelIndex): + parent = QModelIndex() + if not parent.isValid(): + return len(self.rootNode().values()) + parentNode: TreeNode = parent.internalPointer() + assert isinstance(parentNode, TreeNode) + return parentNode.columnCount() + """ - def index(self, row:int, column:int, parent:QModelIndex=None) -> QModelIndex: + def index(self, row: int, column: int, parent: QModelIndex = None) -> QModelIndex: """ Returns the QModelIndex :param row: int @@ -758,21 +807,19 @@ class TreeModel(QAbstractItemModel): :return: QModelIndex """ if parent is None: - parentNode = self.mRootNode - else: - parentNode = self.idx2node(parent) + parent = QModelIndex() - if row < 0 or row >= parentNode.childCount(): - return QModelIndex() - if column < 0 or column >= len(self.mColumnNames): - return QModelIndex() + if not parent.isValid(): + parentNode: TreeNode = self.mRootNode + else: + parentNode: TreeNode = parent.internalPointer() - if isinstance(parentNode, TreeNode) and row < len(parentNode.mChildren): + if len(parentNode.mChildren) > 0: return self.createIndex(row, column, parentNode.mChildren[row]) else: return QModelIndex() - def findParentNode(self, node, parentNodeType) -> TreeNode: + def findParentNode(self, node: TreeNode, nodeType) -> TreeNode: """ finds the next parent TreeNode of type `parentNodeType` :param node: TreeNode @@ -780,14 +827,9 @@ class TreeModel(QAbstractItemModel): :return: TreeNode instance """ assert isinstance(node, TreeNode) - while True: - if isinstance(node, parentNodeType): - return node - if not isinstance(node.parentNode(), TreeNode): - return None - node = node.parentNode() + return node.findParentNode(nodeType) - def indexes2nodes(self, indexes:list): + def indexes2nodes(self, indexes: list): """ Returns the TreeNodes related to a list of QModelIndexes :param indexes: [list-of-QModelIndex] @@ -801,7 +843,23 @@ class TreeModel(QAbstractItemModel): nodes.append(n) return nodes - def nodes2indexes(self, nodes:list): + def removeNodes(self, nodes): + """ + Removes nodes from the model + :param nodes: + :type nodes: + :return: + :rtype: + """ + if isinstance(nodes, TreeNode): + nodes = [nodes] + + for n in nodes: + idx = self.node2idx(n) + if idx.isValid(): + n.parentNode().removeChildNodes(n) + + def nodes2indexes(self, nodes: list): """ Converts a list of TreeNodes into the corresponding list of QModelIndexes Set indexes2nodes @@ -810,10 +868,9 @@ class TreeModel(QAbstractItemModel): """ return [self.node2idx(n) for n in nodes] - def expandNode(self, node, expand=True, recursive=True): assert isinstance(node, TreeNode) - if isinstance(self.mTreeView, QTreeView): + if False and isinstance(self.mTreeView, QTreeView): idx = self.node2idx(node) self.mTreeView.setExpanded(idx, expand) @@ -821,42 +878,37 @@ class TreeModel(QAbstractItemModel): for n in node.childNodes(): self.expandNode(n, expand=expand, recursive=recursive) - - def idx2node(self, index:QModelIndex) -> TreeNode: + def idx2node(self, index: QModelIndex) -> TreeNode: """ Returns the TreeNode related to an QModelIndex `index`. :param index: QModelIndex :return: TreeNode """ - if index.row() == -1 and index.column() == -1: - return self.mRootNode - elif not index.isValid(): + if not index.isValid(): return self.mRootNode else: node = index.internalPointer() assert isinstance(node, TreeNode) return node - def node2idx(self, node:TreeNode) -> QModelIndex: + def node2idx(self, node: TreeNode) -> QModelIndex: """ Returns a TreeNode's QModelIndex :param node: TreeNode :return: QModelIndex """ - assert isinstance(node, TreeNode) - if node == self.mRootNode: + + if node in [self.mRootNode, None]: return QModelIndex() - return self.createIndex(-1, -1, node) else: - parentNode = node.parentNode() - assert isinstance(parentNode, TreeNode) - if node not in parentNode.mChildren: + row: int = node.nodeIndex() + if not isinstance(row, int): return QModelIndex() - r = parentNode.mChildren.index(node) - return self.createIndex(r, 0, node) + parentIndex = self.node2idx(node.parentNode()) + return self.index(row, 0, parent=parentIndex) - def data(self, index, role): + def data(self, index: QModelIndex, role=Qt.DisplayRole): """ :param index: QModelIndex @@ -864,18 +916,16 @@ class TreeModel(QAbstractItemModel): :return: object """ assert isinstance(index, QModelIndex) - if not index.isValid(): - return None - - node = self.idx2node(index) - #node = self.idx2node(index) node = index.internalPointer() - assert isinstance(node, TreeNode) - col = index.column() + if not isinstance(node, TreeNode): + return None + if node == self.rootNode(): + s = "" if role == Qt.UserRole: return node col = index.column() + if col == 0: if role in [Qt.DisplayRole, Qt.EditRole]: return node.name() @@ -884,35 +934,92 @@ class TreeModel(QAbstractItemModel): if role == Qt.ToolTipRole: return node.toolTip() if col > 0: + # first column is for the node name, other columns are for node values i = col - 1 - if role in [Qt.DisplayRole, Qt.EditRole] and len(node.values()) > i: - return str(node.values()[i]) + + if len(node.values()) > i: + + if role == Qt.DisplayRole: + return str(node.values()[i]) + if role == Qt.EditRole: + return node.values()[i] + if role == Qt.ToolTipRole: + tt = [f'{i + 1}: {v}' for i, v in enumerate(node.values())] + return '\n'.join(tt) + return None def flags(self, index): assert isinstance(index, QModelIndex) if not index.isValid(): return Qt.NoItemFlags - node = self.idx2node(index) - #node = self.idx2node(index) return Qt.ItemIsEnabled | Qt.ItemIsSelectable - class TreeView(QTreeView): """ A basic QAbstractItemView implementation to realize TreeModels. """ + def __init__(self, *args, **kwds): super(TreeView, self).__init__(*args, **kwds) + self.mAutoExpansionDepth: int = 1 self.mModel = None + self.mNodeExpansion: typing.Dict[str, bool] = dict() + + def setAutoExpansionDepth(self, depth: int): + """ + Sets the depth until which new TreeNodes will be opened + 0 = Top nodes not expanded + 1 = Top nodes expanded + 2 = Top and Subnodes expanded + :param depth: int + """ + assert isinstance(depth, int) + self.mAutoExpansionDepth = depth + + def updateNodeExpansion(self, restore: bool, + index: QModelIndex = None, prefix='') -> typing.Dict[str, bool]: + """ + Allows to save and restore the state of node expansion + :param restore: bool, set True to save the state, False to restore it + :param index: QModelIndex() + :param prefix: string to identify the parent nodes + :return: Dict[str, bool] that stores the node expansion states + """ + assert isinstance(restore, bool) + + if not isinstance(index, QModelIndex): + index = QModelIndex() + if False and not (restore or index.isValid()): + self.mNodeExpansion.clear() + + model: QAbstractItemModel = self.model() + if isinstance(model, QAbstractItemModel): + rows = model.rowCount(index) + if rows > 0: + nodeName = f'{prefix}:{model.data(index, role=Qt.DisplayRole)}' + nodeDepth: int = self.nodeDepth(index) + + if restore: + # restore expansion state, if stored in mNodeExpansion + self.setExpanded(index, self.mNodeExpansion.get(nodeName, nodeDepth < self.mAutoExpansionDepth)) + else: + # save expansion state + self.mNodeExpansion[nodeName] = self.isExpanded(index) + + for row in range(rows): + idx = model.index(row, 0, index) + self.updateNodeExpansion(restore, index=idx, prefix=nodeName) - def setModel(self, model:QAbstractItemModel): + return self.mNodeExpansion + + def setModel(self, model: QAbstractItemModel): """ Sets the TreeModel :param model: TreeModel """ - super(TreeView, self).setModel(model) + super().setModel(model) self.mModel = model if isinstance(self.mModel, QAbstractItemModel): @@ -920,19 +1027,32 @@ class TreeView(QTreeView): self.mModel.dataChanged.connect(self.onDataChanged) self.mModel.rowsInserted.connect(self.onRowsInserted) + # update column spans + self.onModelReset() + + def onModelReset(self): for row in range(self.model().rowCount(QModelIndex())): idx = self.model().index(row, 0) self.setColumnSpan(idx) - def onRowsInserted(self, parent:QModelIndex, first:int, last:int): + def nodeDepth(self, index: QModelIndex) -> int: + if not index.isValid(): + return 0 + return 1 + self.nodeDepth(index.parent()) + + def onRowsInserted(self, parent: QModelIndex, first: int, last: int): - for row in range(first, last+1): + for row in range(first, last + 1): idx = self.model().index(row, 0, parent) self.setColumnSpan(idx) - def onDataChanged(self, tl:QModelIndex, br:QModelIndex, roles): + level = self.nodeDepth(parent) + if level < self.mAutoExpansionDepth: + self.setExpanded(idx, True) + s = "" + def onDataChanged(self, tl: QModelIndex, br: QModelIndex, roles): parent = tl.parent() for row in range(tl.row(), br.row() + 1): @@ -940,11 +1060,36 @@ class TreeView(QTreeView): self.setColumnSpan(idx) s = "" - def setColumnSpan(self, idx:QModelIndex): + def setColumnSpan(self, idx: QModelIndex): """ Sets the column span for index `idx` and all child widgets :param idx: :return: + """ + + assert isinstance(idx, QModelIndex) + if not idx.isValid(): + return + + colCnt = self.header().count() + #span: QSize = model.span(idx) + span: QSize = QSize(1, 1) + rightIdx = idx + while rightIdx.isValid() and rightIdx.column() < colCnt: + rightIdx = rightIdx.siblingAtColumn(rightIdx.column() + 1) + if rightIdx.data(Qt.DisplayRole) is None: + span.setWidth(span.width() + 1) + else: + break + + if span.width() > 1: + self.setFirstColumnSpanned(idx.row(), idx.parent(), True) + for row in range(self.model().rowCount(idx)): + idx2 = self.model().index(row, 0, idx) + self.setColumnSpan(idx2) + + return + """ assert isinstance(idx, QModelIndex) if not idx.isValid(): @@ -962,6 +1107,7 @@ class TreeView(QTreeView): for row in range(self.model().rowCount(idx)): idx2 = self.model().index(row, 0, idx) self.setColumnSpan(idx2) + """ def selectedNode(self) -> TreeNode: """ @@ -975,8 +1121,6 @@ class TreeView(QTreeView): return None - - def selectedNodes(self) -> list: """ Returns all selected TreeNodes @@ -988,4 +1132,3 @@ class TreeView(QTreeView): if isinstance(node, TreeNode) and node not in nodes: nodes.append(node) return nodes - diff --git a/eotimeseriesviewer/externals/qps/plotstyling/plotstyling.py b/eotimeseriesviewer/externals/qps/plotstyling/plotstyling.py index 1d35684a..8c1e942a 100644 --- a/eotimeseriesviewer/externals/qps/plotstyling/plotstyling.py +++ b/eotimeseriesviewer/externals/qps/plotstyling/plotstyling.py @@ -643,10 +643,10 @@ class PlotStyle(QObject): hw, hh = int(w * 0.5), int(h * 0.5) w2, h2 = int(w * 0.75), int(h * 0.75) - # p.drawLine(x1,y1,x2,y2) + #p.drawLine(x1,y1,x2,y2) - #p.drawLine(2, h - 2, hw, hh) - #p.drawLine(hw, hh, w - 2, int(h * 0.3)) + p.drawLine(2, h - 2, hw, hh) + p.drawLine(hw, hh, w - 2, int(h * 0.3)) p.translate(pm.width() / 2, pm.height() / 2) drawSymbol(p, self.markerSymbol, self.markerSize, self.markerPen, self.markerBrush) diff --git a/eotimeseriesviewer/externals/qps/simplewidgets.py b/eotimeseriesviewer/externals/qps/simplewidgets.py new file mode 100644 index 00000000..6f8a51ac --- /dev/null +++ b/eotimeseriesviewer/externals/qps/simplewidgets.py @@ -0,0 +1,133 @@ +from qgis.PyQt.QtCore import pyqtSignal, Qt + +from qgis.PyQt.QtWidgets import QWidget, QAbstractSpinBox, QSpinBox, QDoubleSpinBox, \ + QHBoxLayout, QVBoxLayout, QSlider + + +class SliderSpinBox(QWidget): + sigValueChanged = pyqtSignal(int) + + def __init__(self, *args, + spinbox: QAbstractSpinBox = None, + spinbox_position: Qt.Alignment = Qt.AlignLeft, + **kwds): + + if not isinstance(spinbox, QAbstractSpinBox): + spinbox = QSpinBox() + assert isinstance(spinbox, QAbstractSpinBox) + assert isinstance(spinbox_position, Qt.AlignmentFlag) + + super().__init__(*args, **kwds) + + self.spinbox: QAbstractSpinBox = spinbox + self.slider: QSlider = QSlider(Qt.Horizontal) + self.slider.valueChanged.connect(self.onSliderValueChanged) + self.spinbox.valueChanged.connect(self.onSpinboxValueChanged) + + if spinbox_position in [Qt.AlignLeft, Qt.AlignRight]: + l = QHBoxLayout() + elif spinbox_position in [Qt.AlignTop, Qt.AlignBottom]: + l = QVBoxLayout() + else: + raise NotImplementedError() + + if spinbox_position in [Qt.AlignLeft, Qt.AlignTop]: + l.addWidget(self.spinbox) + l.addWidget(self.slider) + else: + l.addWidget(self.slider) + l.addWidget(self.spinbox) + + l.setContentsMargins(0, 0, 0, 0) + l.setSpacing(2) + + self.setLayout(l) + + def onSliderValueChanged(self, value): + v = self.slider2spinboxvalue(value) + self.spinbox.setValue(v) + + def onSpinboxValueChanged(self, value): + v = self.spinbox2slidervalue(value) + if v != self.slider.value(): + self.slider.setValue(v) + + self.sigValueChanged.emit(value) + + def setSingleStep(self, value): + self.spinbox.setSingleStep(value) + self.slider.setSingleStep(value) + self.slider.setPageStep(value * 10) + + def singleStep(self): + return self.spinbox.singleStep() + + def setMinimum(self, value): + self.spinbox.setMinimum(value) + self.slider.setMinimum(self.spinbox2slidervalue(value)) + + def spinbox2slidervalue(self, value): + return value + + def slider2spinboxvalue(self, value): + return value + + def setMaximum(self, value): + self.spinbox.setMaximum(value) + self.slider.setMaximum(self.spinbox2slidervalue(value)) + + def maximum(self) -> int: + return self.spinbox.maximum() + + def minimum(self) -> int: + return self.spinbox.minimum() + + def setValue(self, value: float): + self.spinbox.setValue(value) + + def value(self) -> float: + return self.spinbox.value() + + def setRange(self, vmin, vmax): + vmin = min(vmin, vmax) + vmax = max(vmin, vmax) + assert vmin <= vmax + + self.setMinimum(vmin) + self.setMaximum(vmax) + + +class DoubleSliderSpinBox(SliderSpinBox): + sigValueChanged = pyqtSignal(float) + + def __init__(self, *args, **kwds): + spinbox = QDoubleSpinBox() + super().__init__(*args, spinbox=spinbox, **kwds) + + self.spinbox: QDoubleSpinBox + assert isinstance(self.spinbox, QDoubleSpinBox) + self.setDecimals(2) + self.setMinimum(0) + self.setMaximum(1) + self.setSingleStep(0.1) + + def spinbox2slidervalue(self, value: float) -> int: + v = int(round(10 ** self.decimals() * value)) + return v + + def slider2spinboxvalue(self, value: int) -> float: + v = value / (10 ** self.decimals()) + return v + + def setDecimals(self, value: int): + self.spinbox.setDecimals(value) + self.setSingleStep(self.spinbox.singleStep()) + + def decimals(self) -> int: + return self.spinbox.decimals() + + def setSingleStep(self, value): + self.spinbox.setSingleStep(value) + m = int(10 ** self.decimals() * value) + self.slider.setSingleStep(m) + self.slider.setPageStep(m * 10) \ No newline at end of file diff --git a/eotimeseriesviewer/externals/qps/speclib/__init__.py b/eotimeseriesviewer/externals/qps/speclib/__init__.py index ff3a890d..dff57628 100644 --- a/eotimeseriesviewer/externals/qps/speclib/__init__.py +++ b/eotimeseriesviewer/externals/qps/speclib/__init__.py @@ -27,13 +27,11 @@ *************************************************************************** """ import enum -from qgis.core import * - -from qgis.PyQt.QtCore import QSettings - +from qgis.core import QgsSettings EDITOR_WIDGET_REGISTRY_KEY = 'Spectral Profile' + class SpectralLibrarySettingsKey(enum.Enum): CURRENT_PROFILE_STYLE = 1 DEFAULT_PROFILE_STYLE = 2 @@ -44,17 +42,15 @@ class SpectralLibrarySettingsKey(enum.Enum): SELECTION_COLOR = 7 -def speclibSettings() -> QSettings: +def speclibSettings() -> QgsSettings: """ Returns SpectralLibrary relevant QSettings :return: QSettings """ return QgsSettings('HUB', 'speclib') + try: from ..speclib.io.envi import EnviSpectralLibraryIO except: pass - - - diff --git a/eotimeseriesviewer/externals/qps/speclib/core.py b/eotimeseriesviewer/externals/qps/speclib/core.py index 21032653..6b58923b 100644 --- a/eotimeseriesviewer/externals/qps/speclib/core.py +++ b/eotimeseriesviewer/externals/qps/speclib/core.py @@ -31,6 +31,7 @@ import enum import pickle import typing import pathlib +import collections import uuid from osgeo import osr from ..speclib import SpectralLibrarySettingsKey @@ -100,9 +101,6 @@ class SerializationMode(enum.Enum): PICKLE = 2 -SERIALIZATION = SerializationMode.PICKLE - - def log(msg: str): if DEBUG: QgsMessageLog.logMessage(msg, 'spectrallibraries.py') @@ -390,13 +388,16 @@ def toType(t, arg, empty2None=True): return t(arg) -def encodeProfileValueDict(d: dict, mode: SerializationMode = SerializationMode.PICKLE) -> str: +def encodeProfileValueDict(d: dict, mode=None) -> QByteArray: """ Converts a SpectralProfile value dictionary into a compact JSON string, which can be extracted with `decodeProfileValueDict`. :param d: dict :return: str """ + if mode is not None: + warnings.warn('keyword "mode" is not not used anymore', DeprecationWarning) + if not isinstance(d, dict): return None d2 = {} @@ -405,30 +406,22 @@ def encodeProfileValueDict(d: dict, mode: SerializationMode = SerializationMode. # save keys with information only if v is not None: d2[k] = v - if mode == SerializationMode.JSON: - return json.dumps(d2, sort_keys=True, separators=(',', ':')) - elif mode == SerializationMode.PICKLE: - return QByteArray(pickle.dumps(d2)) - else: - raise NotImplementedError() + return QByteArray(pickle.dumps(d2)) -def decodeProfileValueDict(dump, mode: SerializationMode = SerializationMode.PICKLE): +def decodeProfileValueDict(dump, mode=None): """ Converts a json / pickle dump into a SpectralProfile value dictionary :param dump: str :return: dict """ + if mode is not None: + warnings.warn('keyword "mode" is not used anymore', DeprecationWarning) + d = EMPTY_PROFILE_VALUES.copy() if dump not in EMPTY_VALUES: - d2 = None - if mode == SerializationMode.JSON: - d2 = json.loads(dump) - elif mode == SerializationMode.PICKLE: - d2 = pickle.loads(dump) - else: - raise NotImplementedError() + d2 = pickle.loads(dump) d.update(d2) return d @@ -477,25 +470,35 @@ LUT_IDL2GDAL = {1: gdal.GDT_Byte, def ogrStandardFields() -> list: - """Returns the minimum set of field a Spectral Library has to contain""" + """Returns the minimum set of field a Spectral Library contains""" fields = [ ogr.FieldDefn(FIELD_FID, ogr.OFTInteger), ogr.FieldDefn(FIELD_NAME, ogr.OFTString), ogr.FieldDefn('source', ogr.OFTString), - ogr.FieldDefn(FIELD_VALUES, ogr.OFTString) \ - if SERIALIZATION == SerializationMode.JSON else \ - ogr.FieldDefn(FIELD_VALUES, ogr.OFTBinary), + ogr.FieldDefn(FIELD_VALUES, ogr.OFTBinary), ] return fields -def createStandardFields(): +def spectralValueFields(spectralLibrary: QgsVectorLayer) -> typing.List[QgsField]: + """ + Returns the fields that contains values of SpectralProfiles + :param spectralLibrary: + :return: + """ + fields = [f for f in spectralLibrary.fields() if + f.type() == QVariant.ByteArray and + f.editorWidgetSetup().type() == EDITOR_WIDGET_REGISTRY_KEY] + + return fields + + +def createStandardFields() -> QgsFields: fields = QgsFields() for f in ogrStandardFields(): assert isinstance(f, ogr.FieldDefn) name = f.GetName() ogrType = f.GetType() - if ogrType == ogr.OFTString: a, b = QVariant.String, 'varchar' elif ogrType in [ogr.OFTInteger, ogr.OFTInteger64]: @@ -681,33 +684,57 @@ class SpectralProfile(QgsFeature): return profile @staticmethod - def fromSpecLibFeature(feature: QgsFeature): + def fromQgsFeature(feature: QgsFeature, value_field: str = FIELD_VALUES): """ Converts a QgsFeature into a SpectralProfile :param feature: QgsFeature - :return: SpectralProfile + :param value_field: name of QgsField that stores the Spectral Profile BLOB + :return: """ assert isinstance(feature, QgsFeature) - sp = SpectralProfile(fields=feature.fields()) + if isinstance(value_field, QgsField): + value_field = value_field.name() + assert value_field in feature.fields().names(), f'field "{value_field}" does not exist' + sp = SpectralProfile(fields=feature.fields(), value_field=value_field) sp.setId(feature.id()) sp.setAttributes(feature.attributes()) sp.setGeometry(feature.geometry()) return sp - def __init__(self, parent=None, fields=None, values: dict = None): + @staticmethod + def fromSpecLibFeature(feature: QgsFeature): + """ + Converts a QgsFeature into a SpectralProfile + :param feature: QgsFeature + :return: SpectralProfile + """ + warnings.warn('Use SpectralProfile.fromQgsFeature instead', DeprecationWarning) + return SpectralProfile.fromQgsFeature(feature) + + def __init__(self, parent=None, + fields: QgsFields = None, + values: dict = None, + value_field: typing.Union[str, QgsField] = FIELD_VALUES): + """ + :param parent: + :param fields: + :param values: + :param value_field: name or index of field that contains the spectral values information. + Needs to be a BLOB field. + """ if fields is None: fields = createStandardFields() assert isinstance(fields, QgsFields) - # QgsFeature.__init__(self, fields) - # QObject.__init__(self) super(SpectralProfile, self).__init__(fields) - # QObject.__init__(self) - # fields = self.fields() assert isinstance(fields, QgsFields) self.mValueCache = None - # self.setStyle(DEFAULT_SPECTRUM_STYLE) + if isinstance(value_field, QgsField): + value_field = value_field.name() + + self.mValueField: str = value_field + if isinstance(values, dict): self.setValues(**values) @@ -770,7 +797,6 @@ class SpectralProfile(QgsFeature): def setName(self, name: str): if name != self.name(): self.setAttribute(FIELD_NAME, name) - # self.sigNameChanged.emit(name) def name(self) -> str: return self.metadata(FIELD_NAME) @@ -788,10 +814,17 @@ class SpectralProfile(QgsFeature): elif isinstance(pt, QgsPointXY): self.setGeometry(QgsGeometry.fromPointXY(pt)) + def key(self) -> typing.Tuple[int, any]: + """ + Returns a key tuple consisting of the profiles feature id and the columns that stores the profile data + :return: + """ + return (self.id(), self.mValueField) + def geoCoordinate(self): return self.geometry() - def updateMetadata(self, metaData): + def updateMetadata(self, metaData:dict): if isinstance(metaData, dict): for key, value in metaData.items(): self.setMetadata(key, value) @@ -853,21 +886,28 @@ class SpectralProfile(QgsFeature): """ return len(self.yValues()) + def isEmpty(self) -> bool: + """ + Returns True if there is not ByteArray stored in the BLOB value field + :return: + """ + return self.attribute(self.fields().indexFromName(self.mValueField)) in [None, QVariant()] + def values(self) -> dict: """ Returns a dictionary with 'x', 'y', 'xUnit' and 'yUnit' values. :return: {'x':list,'y':list,'xUnit':str,'yUnit':str, 'bbl':list} """ if self.mValueCache is None: - jsonStr = self.attribute(self.fields().indexFromName(FIELD_VALUES)) - d = decodeProfileValueDict(jsonStr) + byteArray = self.attribute(self.fields().indexFromName(self.mValueField)) + d = decodeProfileValueDict(byteArray) # save a reference to the decoded dictionary self.mValueCache = d return self.mValueCache - def setValues(self, x=None, y=None, xUnit=None, yUnit=None, bbl=None): + def setValues(self, x=None, y=None, xUnit: str = None, yUnit: str = None, bbl=None, **kwds): d = self.values().copy() @@ -911,7 +951,7 @@ class SpectralProfile(QgsFeature): if isinstance(yUnit, str): d['yUnit'] = yUnit - self.setAttribute(FIELD_VALUES, encodeProfileValueDict(d)) + self.setAttribute(self.mValueField, encodeProfileValueDict(d)) self.mValueCache = d def xValues(self) -> list: @@ -1286,7 +1326,7 @@ class SpectralProfileRenderer(object): customStyleNode = customStyleNodes.at(i) customStyle = PlotStyle.readXml(customStyleNode) if isinstance(customStyle, PlotStyle): - fids = customStyleNode.firstChildElement('fids').firstChild().nodeValue().split(',') + fids = customStyleNode.firstChildElement('keys').firstChild().nodeValue().split(',') fids = [int(f) for f in fids] renderer.setProfilePlotStyle(customStyle, fids) @@ -1326,7 +1366,7 @@ class SpectralProfileRenderer(object): fids = [k for k, s in self.mFID2Style.items() if s == style] nodeStyle = doc.createElement('custom_style') style.writeXml(nodeStyle, doc) - nodeFIDs = doc.createElement('fids') + nodeFIDs = doc.createElement('keys') nodeFIDs.appendChild(doc.createTextNode(','.join([str(i) for i in fids]))) nodeStyle.appendChild(nodeFIDs) nodeCustomStyles.appendChild(nodeStyle) @@ -2155,8 +2195,9 @@ class SpectralLibrary(QgsVectorLayer): path: str = None, baseName: str = DEFAULT_NAME, options: QgsVectorLayer.LayerOptions = None, + value_fields=[FIELD_VALUES], uri: str = None, # deprectated - name: str = None # deprectated + name: str = None, # deprectated ): if isinstance(uri, str): @@ -2172,6 +2213,9 @@ class SpectralLibrary(QgsVectorLayer): if isinstance(path, pathlib.Path): path = path.as_posix() + if not isinstance(options, QgsVectorLayer.LayerOptions): + options = QgsVectorLayer.LayerOptions(loadDefaultStyle=True, readExtentFromXml=True) + if path is None: # create a new, empty backend existing_vsi_files = vsiSpeclibs() @@ -2204,9 +2248,15 @@ class SpectralLibrary(QgsVectorLayer): assert isinstance(lyr, ogr.Layer) ldefn = lyr.GetLayerDefn() assert isinstance(ldefn, ogr.FeatureDefn) + fieldNames = [] for f in ogrStandardFields(): + fieldNames.append(f.GetName()) lyr.CreateField(f) + for f in value_fields: + if f not in fieldNames: + lyr.CreateField(ogr.FieldDefn(f, ogr.OFTBinary)) + try: dsSrc.FlushCache() except RuntimeError as rt: @@ -2216,25 +2266,47 @@ class SpectralLibrary(QgsVectorLayer): raise rt assert isinstance(path, str) - options = QgsVectorLayer.LayerOptions(loadDefaultStyle=False, readExtentFromXml=False) super(SpectralLibrary, self).__init__(path, baseName, 'ogr', options) # consistency check field_names = self.fields().names() assert FIELD_NAME in field_names - assert FIELD_VALUES in field_names f = self.fields().at(self.fields().lookupField(FIELD_NAME)) - assert f.type() == QVariant.String, 'Field {} not of type String / VARCHAR' - f = self.fields().at(self.fields().lookupField(FIELD_VALUES)) - assert f.type() == QVariant.ByteArray, 'Field {} not of type ByteArray / BLOB' + assert f.type() == QVariant.String, f'Field {f.name()} not of type String / VARCHAR' + + for n in value_fields: + assert n in field_names + f = self.fields().at(self.fields().lookupField(n)) + assert f.type() == QVariant.ByteArray, f'Field {n} not of type ByteArray / BLOB' # self.beforeCommitChanges.connect(self.onBeforeCommitChanges) + self.committedFeaturesAdded.connect(self.onCommittedFeaturesAdded) self.mProfileRenderer: SpectralProfileRenderer = SpectralProfileRenderer() self.mProfileRenderer.setInput(self) + + self.mSpectralValueFields: typing.List[QgsField] = [] + self.attributeAdded.connect(self.onAttributeAdded) + self.attributeDeleted.connect(self.onFieldsChanged) + + # set special default editors + for f in value_fields: + self.setEditorWidgetSetup(self.fields().lookupField(f), + QgsEditorWidgetSetup(EDITOR_WIDGET_REGISTRY_KEY, {})) + self.initTableConfig() self.initProfileRenderer() + def onAttributeAdded(self, idx:int): + + field: QgsField = self.fields().at(idx) + if field.type() == QVariant.ByteArray: + # let new ByteArray fields be SpectralProfile columns by default + self.setEditorWidgetSetup(idx, QgsEditorWidgetSetup(EDITOR_WIDGET_REGISTRY_KEY, {})) + + def onFieldsChanged(self): + self.mSpectralValueFields = spectralValueFields(self) + def onCommittedFeaturesAdded(self, id, features): if id != self.id(): @@ -2250,6 +2322,10 @@ class SpectralLibrary(QgsVectorLayer): updates[fidNew] = mFID2Style.pop(fidOld) mFID2Style.update(updates) + def setEditorWidgetSetup(self, index: int, setup: QgsEditorWidgetSetup): + super().setEditorWidgetSetup(index, setup) + self.onFieldsChanged() + def setProfileRenderer(self, profileRenderer: SpectralProfileRenderer): assert isinstance(profileRenderer, SpectralProfileRenderer) b = profileRenderer != self.mProfileRenderer @@ -2313,12 +2389,6 @@ class SpectralLibrary(QgsVectorLayer): self.setAttributeTableConfig(conf) - # set special default editors - # self.setEditorWidgetSetup(self.fields().lookupField(FIELD_STYLE), QgsEditorWidgetSetup(PlotSettingsEditorWidgetKey, {})) - - self.setEditorWidgetSetup(self.fields().lookupField(FIELD_VALUES), - QgsEditorWidgetSetup(EDITOR_WIDGET_REGISTRY_KEY, {})) - def mimeData(self, formats: list = None) -> QMimeData: """ Wraps this Speclib into a QMimeData object @@ -2350,36 +2420,23 @@ class SpectralLibrary(QgsVectorLayer): return mimeData - def optionalFields(self) -> list: - """ - Returns the list of optional fields that are not part of the standard field set. - :return: [list-of-QgsFields] - """ - standardFields = createStandardFields() - return [f for f in self.fields() if f not in standardFields] - def optionalFieldNames(self) -> list: """ Returns the names of additions fields / attributes :return: [list-of-str] """ - requiredFields = [f.name for f in ogrStandardFields()] - return [n for n in self.fields().names() if n not in requiredFields] + warnings.warn('Deprecated and desimplemented', DeprecationWarning) + # requiredFields = [f.name for f in ogrStandardFields()] + return [] - """ - def initConditionalStyles(self): - styles = self.conditionalStyles() - assert isinstance(styles, QgsConditionalLayerStyles) - - for fieldName in self.fieldNames(): - red = QgsConditionalStyle("@value is NULL") - red.setTextColor(QColor('red')) - styles.setFieldStyles(fieldName, [red]) - - red = QgsConditionalStyle('"__serialized__xvalues" is NULL OR "__serialized__yvalues is NULL" ') - red.setBackgroundColor(QColor('red')) - styles.setRowStyles([red]) - """ + def addSpectralProfileAttribute(self, name: str, comment: str = None) -> bool: + + field = QgsField(name, QVariant.ByteArray, 'Binary', comment=comment) + b = self.addAttribute(field) + if b: + self.setEditorWidgetSetup(self.fields().lookupField(field.name()), + QgsEditorWidgetSetup(EDITOR_WIDGET_REGISTRY_KEY, {})) + return b def addMissingFields(self, fields: QgsFields, copyEditorWidgetSetup: bool = True): """ @@ -2559,20 +2616,53 @@ class SpectralLibrary(QgsVectorLayer): for fid in fids: assert isinstance(fid, int) featureRequest.setFilterFids(fids) - # features = [f for f in self.features() if f.id() in fidsToRemove] + # features = [f for f in self.features() if f.id() in keys_to_remove] return self.getFeatures(featureRequest) - def profile(self, fid: int) -> SpectralProfile: - return SpectralProfile.fromSpecLibFeature(self.getFeature(fid)) - - def profiles(self, fids=None) -> typing.Generator[SpectralProfile, None, None]: - """ - Like features(fidsToRemove=None), but converts each returned QgsFeature into a SpectralProfile + def profile(self, fid: int, value_field=None) -> SpectralProfile: + if value_field is None: + value_field = self.spectralValueFields()[0] + return SpectralProfile.fromQgsFeature(self.getFeature(fid), value_field=value_field) + + def profiles(self, + fids=None, + value_fields=None, + profile_keys: typing.Tuple[int, str]=None) -> typing.Generator[SpectralProfile, None, None]: + """ + Like features(keys_to_remove=None), but converts each returned QgsFeature into a SpectralProfile. + If multiple value fields are set, profiles are returned ordered by (i) fid and (ii) value field. + :param value_fields: + :type value_fields: + :param profile_keys: + :type profile_keys: :param fids: optional, [int-list-of-feature-ids] to return :return: generator of [List-of-SpectralProfiles] """ - for f in self.features(fids=fids): - yield SpectralProfile.fromSpecLibFeature(f) + + if profile_keys is None: + if value_fields is None: + value_fields = [f.name() for f in self.spectralValueFields()] + elif not isinstance(value_fields, list): + value_fields = [value_fields] + + for f in self.features(fids=fids): + for field in value_fields: + yield SpectralProfile.fromQgsFeature(f, value_field=field) + else: + # sort by FID + LUT_FID2KEYS = dict() + for pkey in profile_keys: + fid, field = pkey + + fields = LUT_FID2KEYS.get(fid, []) + fields.append(field) + LUT_FID2KEYS[fid] = fields + + for f in self.features(fids=list(LUT_FID2KEYS.keys())): + assert isinstance(f, QgsFeature) + for field in LUT_FID2KEYS[f.id()]: + yield SpectralProfile.fromQgsFeature(f, value_field=field) + def groupBySpectralProperties(self, excludeEmptyProfiles: bool = True): """ @@ -2621,10 +2711,9 @@ class SpectralLibrary(QgsVectorLayer): msg = super(SpectralLibrary, self).exportNamedStyle(doc, context=context, categories=categories) if msg == '': qgsNode = doc.documentElement().toElement() - # speclibNode = doc.createElement(XMLNODE_PROFILE_RENDERER) + if isinstance(self.mProfileRenderer, SpectralProfileRenderer): self.mProfileRenderer.writeXml(qgsNode, doc) - # qgsNode.appendChild(speclibNode) return msg @@ -2726,6 +2815,9 @@ class SpectralLibrary(QgsVectorLayer): return [] + def spectralValueFields(self) -> typing.List[QgsField]: + return self.mSpectralValueFields + def yRange(self) -> typing.List[float]: """ Returns the maximum y range @@ -2837,17 +2929,15 @@ class SpectralLibrary(QgsVectorLayer): return max(cnt, 0) def __iter__(self): - r = QgsFeatureRequest() - for f in self.getFeatures(r): - yield SpectralProfile.fromSpecLibFeature(f) + return self.profiles() def __getitem__(self, slice) -> typing.Union[SpectralProfile, typing.List[SpectralProfile]]: fids = sorted(self.allFeatureIds())[slice] - + value_field = self.mSpectralValueFields[0].name() if isinstance(fids, list): return sorted(self.profiles(fids=fids), key=lambda p: p.id()) else: - return SpectralProfile.fromSpecLibFeature(self.getFeature(fids)) + return SpectralProfile.fromQgsFeature(self.getFeature(fids), value_field=value_field) def __delitem__(self, slice): profiles = self[slice] diff --git a/eotimeseriesviewer/externals/qps/speclib/function_help/format_py.json b/eotimeseriesviewer/externals/qps/speclib/function_help/format_py.json new file mode 100644 index 00000000..5a8489a4 --- /dev/null +++ b/eotimeseriesviewer/externals/qps/speclib/function_help/format_py.json @@ -0,0 +1,12 @@ +{ + "name": "format_py", + "type": "function", + "description": "Creates a string from inputs, using the <a href=\"https://docs.python.org/3/library/string.html#format-examples\">python format syntax</a>.", + "arguments": [ {"arg":"fmt","description":"python format string"}, + {"arg":"arg1","description":"1st argument to format string", "optional":"true"}, + {"arg":"arg2","description":"2nd argument to format string", "optional":"true"}, + {"arg":"argN","description":"N argument to format string", "optional":"true"} + ], + "examples": [ { "expression":"format_py('foobar{}',42)", "returns":"'foobar42'"}, + { "expression":"format_py('foobar{:04}',42)", "returns":"'foobar0042'"}] +} diff --git a/eotimeseriesviewer/externals/qps/speclib/function_help/spectraldata.json b/eotimeseriesviewer/externals/qps/speclib/function_help/spectraldata.json new file mode 100644 index 00000000..1f7599ca --- /dev/null +++ b/eotimeseriesviewer/externals/qps/speclib/function_help/spectraldata.json @@ -0,0 +1,8 @@ +{ + "name": "spectralData", + "type": "function", + "description": "Returns values of a spectral profile as map", + "arguments": [ {"arg":"field","description":"binary data column (BLOB)"} + ], + "examples": [ { "expression":"spectralData(field=\"values\")", "returns":"a map with spectral data from column \"values\""}] +} diff --git a/eotimeseriesviewer/externals/qps/speclib/function_help/spectralmath.json b/eotimeseriesviewer/externals/qps/speclib/function_help/spectralmath.json new file mode 100644 index 00000000..2eec5570 --- /dev/null +++ b/eotimeseriesviewer/externals/qps/speclib/function_help/spectralmath.json @@ -0,0 +1,8 @@ +{ + "name": "spectralMath", + "type": "function", + "description": "Create or modifies data of a SpectralProfile with python", + "arguments": [ {"arg":"expression","description":"a python expression to set the profile data"}, + {"arg":"field","description":"field to load existing spectral profile data from"}], + "examples": [ { "expression":"array_prepend(array(1,2,3),0)", "returns":"[ 0, 1, 2, 3 ]"}] +} diff --git a/eotimeseriesviewer/externals/qps/speclib/gui.py b/eotimeseriesviewer/externals/qps/speclib/gui.py index 47187307..c32f4e2d 100644 --- a/eotimeseriesviewer/externals/qps/speclib/gui.py +++ b/eotimeseriesviewer/externals/qps/speclib/gui.py @@ -23,6 +23,8 @@ along with this software. If not, see <http://www.gnu.org/licenses/>. *************************************************************************** """ +from typing import List, Tuple + import sip import textwrap from .core import * @@ -33,24 +35,26 @@ from ..externals import pyqtgraph as pg from ..externals.pyqtgraph.graphicsItems.ViewBox.ViewBoxMenu import ViewBoxMenu from ..externals.pyqtgraph.graphicsItems.PlotDataItem import PlotDataItem from ..layerproperties import AttributeTableWidget -from ..unitmodel import UnitModel, BAND_INDEX, XUnitModel, UnitConverterFunctionModel +from ..unitmodel import BAND_INDEX, XUnitModel, UnitConverterFunctionModel -from ..models import Option, OptionListModel from ..plotstyling.plotstyling import PlotStyleWidget, PlotStyle, PlotStyleDialog -from ..layerproperties import AddAttributeDialog + from qgis.core import \ - QgsFeature, QgsRenderContext, QgsNullSymbolRenderer, \ - QgsRasterLayer, QgsMapLayer, QgsVectorLayer, \ + QgsFeature, QgsRenderContext, QgsNullSymbolRenderer, QgsFieldFormatter, QgsApplication, \ + QgsRasterLayer, QgsMapLayer, QgsVectorLayer, QgsFieldFormatterRegistry, \ QgsSymbol, QgsMarkerSymbol, QgsLineSymbol, QgsFillSymbol, \ - QgsAttributeTableConfig, QgsField, QgsMapLayerProxyModel, QgsFileUtils + QgsAttributeTableConfig, QgsField, QgsMapLayerProxyModel, QgsFileUtils, \ + QgsExpression, QgsFieldProxyModel + from qgis.gui import \ QgsEditorWidgetWrapper, QgsAttributeTableView, \ QgsActionMenu, QgsEditorWidgetFactory, QgsStatusBar, \ QgsDualView, QgsGui, QgisInterface, QgsMapCanvas, QgsDockWidget, QgsEditorConfigWidget, \ - QgsAttributeTableFilterModel - + QgsAttributeTableFilterModel, QgsFieldExpressionWidget SPECTRAL_PROFILE_EDITOR_WIDGET_FACTORY: None +SPECTRAL_PROFILE_FIELD_FORMATTER: None +SPECTRAL_PROFILE_FIELD_REPRESENT_VALUE = 'Profile' class SpectralXAxis(pg.AxisItem): @@ -181,7 +185,8 @@ class SpectralProfileRendererWidget(QWidget): self.btnColorSchemeBright.setDefaultAction(self.actionActivateBrightTheme) self.btnColorSchemeDark.setDefaultAction(self.actionActivateDarkTheme) - self.actionActivateBrightTheme.triggered.connect(lambda: self.setRendererTheme(SpectralProfileRenderer.bright())) + self.actionActivateBrightTheme.triggered.connect( + lambda: self.setRendererTheme(SpectralProfileRenderer.bright())) self.actionActivateDarkTheme.triggered.connect(lambda: self.setRendererTheme(SpectralProfileRenderer.dark())) def setResetRenderer(self, profileRenderer: SpectralProfileRenderer): @@ -254,7 +259,7 @@ class SpectralProfileRendererWidget(QWidget): cs.infoColor = self.btnColorInfo.color() cs.selectionColor = self.btnColorSelection.color() cs.profileStyle = self.wDefaultProfileStyle.plotStyle() - #if isinstance(self.mLastRenderer, SpectralProfileRenderer): + # if isinstance(self.mLastRenderer, SpectralProfileRenderer): # cs.temporaryProfileStyle = self.mLastRenderer.temporaryProfileStyle.clone() # cs.mFID2Style.update(self.mLastRenderer.mFID2Style) cs.useRendererColors = self.optionUseColorsFromVectorRenderer.isChecked() @@ -277,12 +282,12 @@ class SpectralProfilePlotDataItem(PlotDataItem): self.curve.mouseClickEvent = self.onCurveMouseClickEvent self.scatter.sigClicked.connect(self.onScatterMouseClicked) - self.mValueConversionIsPossible : bool = True + self.mValueConversionIsPossible: bool = True self.mXValueConversionFunction = lambda v, *args: v self.mYValueConversionFunction = lambda v, *args: v self.mSortByXValues: bool = False - #self.mDefaultStyle = PlotStyle() + # self.mDefaultStyle = PlotStyle() self.mProfileSource = None @@ -471,9 +476,12 @@ class SpectralProfilePlotDataItem(PlotDataItem): pw.getPlotItem().addItem(self) return pw + def key(self) -> typing.Tuple[int, int]: + return self.mProfile.key() + def id(self) -> int: """ - Returns the profile id + Returns the profile fid :return: int """ return self.mProfile.id() @@ -537,7 +545,6 @@ class SpectralProfilePlotDataItem(PlotDataItem): class XAxisWidgetAction(QWidgetAction): - sigUnitChanged = pyqtSignal(str) def __init__(self, parent, **kwds): @@ -570,7 +577,8 @@ class XAxisWidgetAction(QWidgetAction): lambda: self.setUnit(unitComboBox.currentData(Qt.UserRole)) ) - self.sigUnitChanged.connect(lambda unit, cb=unitComboBox: cb.setCurrentIndex(self.mUnitModel.unitIndex(unit).row())) + self.sigUnitChanged.connect( + lambda unit, cb=unitComboBox: cb.setCurrentIndex(self.mUnitModel.unitIndex(unit).row())) return unitComboBox def createWidget(self, parent: QWidget) -> QWidget: @@ -590,7 +598,6 @@ class XAxisWidgetAction(QWidgetAction): class SpectralProfileRendererWidgetAction(QWidgetAction): - sigProfileRendererChanged = pyqtSignal(SpectralProfileRenderer) sigResetRendererChanged = pyqtSignal(SpectralProfileRenderer) @@ -613,7 +620,6 @@ class SpectralProfileRendererWidgetAction(QWidgetAction): return self.mProfileRenderer def createWidget(self, parent: QWidget) -> SpectralProfileRendererWidget: - w = SpectralProfileRendererWidget(parent) w.setProfileRenderer(self.profileRenderer()) w.sigProfileRendererChanged.connect(self.setProfileRenderer) @@ -623,7 +629,6 @@ class SpectralProfileRendererWidgetAction(QWidgetAction): class MaxNumberOfProfilesWidgetAction(QWidgetAction): - sigMaxNumberOfProfilesChanged = pyqtSignal(int) def __init__(self, parent, **kwds): @@ -658,8 +663,8 @@ class SpectralViewBoxMenu(ViewBoxMenu): """ The QMenu that is shown over the profile plot """ - def __init__(self, *args, **kwds): + def __init__(self, *args, **kwds): super().__init__(*args, **kwds) @@ -678,7 +683,8 @@ class SpectralViewBox(pg.ViewBox): self.mCurrentCursorPosition: typing.Tuple[int, int] = (0, 0) # define actions self.mActionMaxNumberOfProfiles: MaxNumberOfProfilesWidgetAction = MaxNumberOfProfilesWidgetAction(None) - self.mActionSpectralProfileRendering: SpectralProfileRendererWidgetAction = SpectralProfileRendererWidgetAction(None) + self.mActionSpectralProfileRendering: SpectralProfileRendererWidgetAction = SpectralProfileRendererWidgetAction( + None) self.mActionSpectralProfileRendering.setDefaultWidget(self.mActionSpectralProfileRendering.createWidget(None)) self.mOptionUseVectorSymbology: QAction = \ @@ -750,20 +756,22 @@ class SpectralViewBox(pg.ViewBox): def updateCurrentPosition(self, x, y): self.mCurrentCursorPosition = (x, y) + class SpectralLibraryPlotStats(object): def __init__(self): - self.features_speclib: int = 0 - self.features_speclib_selected: int = 0 - - self.filter_mode: QgsAttributeTableFilterModel.FilterMode = QgsAttributeTableFilterModel.ShowAll + self.features_total: int = 0 + self.features_selected: int = 0 self.features_filtered: int = 0 - self.features_filtered_selected: int = 0 - - self.features_plotted: int = 0 - self.features_plotted_max: int = 0 + self.filter_mode: QgsAttributeTableFilterModel.FilterMode = QgsAttributeTableFilterModel.ShowAll - self.features_with_value_error: int = 0 + self.profiles_plotted_max: int = 0 + self.profiles_total: int = 0 + self.profiles_empty: int = 0 + self.profiles_plotted: int = 0 + self.profiles_selected: int = 0 + self.profiles_filtered: int = 0 + self.profiles_error: int = 0 def __eq__(self, other) -> bool: if not isinstance(other, SpectralLibraryPlotStats): @@ -773,6 +781,7 @@ class SpectralLibraryPlotStats(object): return False return True + class SpectralLibraryPlotWidget(pg.PlotWidget): """ A widget to PlotWidget SpectralProfiles @@ -794,6 +803,9 @@ class SpectralLibraryPlotWidget(pg.PlotWidget): self.setMaxProfiles(64) self.mDualView = None + self.mNumberOfValueErrorsProfiles: int = 0 + self.mNumberOfEmptyProfiles: int = 0 + self.mMaxInfoLength: int = 30 # self.centralWidget.setParent(None) @@ -817,14 +829,11 @@ class SpectralLibraryPlotWidget(pg.PlotWidget): # describe functions to convert wavelength units from unit a to unit b self.mUnitConverter = UnitConverterFunctionModel() - self.mPlotDataItems: typing.List[int, SpectralProfilePlotDataItem] = dict() + self.mPlotDataItems: typing.List[typing.Tuple[int, str], SpectralProfilePlotDataItem] = dict() self.mPlotOverlayItems = [] self.setAntialiasing(True) self.setAcceptDrops(True) - self.mLastFIDs = [] - self.mNeedsPlotUpdate = False - self.mCrosshairLineV = pg.InfiniteLine(angle=90, movable=False) self.mCrosshairLineH = pg.InfiniteLine(angle=0, movable=False) @@ -849,7 +858,7 @@ class SpectralLibraryPlotWidget(pg.PlotWidget): pi.addItem(self.mCrosshairLineH, ignoreBounds=True) pi.addItem(self.mInfoScatterPoint) self.proxy2D = pg.SignalProxy(self.scene().sigMouseMoved, rateLimit=100, slot=self.onMouseMoved2D) - #self.proxy2D2 = pg.SignalProxy(self.scene().sigMouseClicked, rateLimit=100, slot=self.onMouseClicked) + # self.proxy2D2 = pg.SignalProxy(self.scene().sigMouseClicked, rateLimit=100, slot=self.onMouseClicked) # set default axis unit self.updateXUnit() @@ -857,7 +866,7 @@ class SpectralLibraryPlotWidget(pg.PlotWidget): self.actionXAxis().sigUnitChanged.connect(self.updateXUnit) self.mSPECIFIC_PROFILE_STYLES: typing.Dict[int, PlotStyle] = dict() - self.mTEMPORARY_HIGHLIGHTED: typing.Set[int] = set() + self.mTEMPORARY_HIGHLIGHTED: typing.Set[typing.Tuple[int, str]] = set() self.mDefaultProfileRenderer: SpectralProfileRenderer self.mDefaultProfileRenderer = SpectralProfileRenderer.default() @@ -873,6 +882,16 @@ class SpectralLibraryPlotWidget(pg.PlotWidget): self.setProfileRenderer(self.mDefaultProfileRenderer) self.setAcceptDrops(True) + def currentProfileKeys(self) -> typing.List[typing.Tuple[int, str]]: + return sorted(self.mTEMPORARY_HIGHLIGHTED) + + def currentProfileIDs(self) -> typing.List[int]: + return list(set([k[0] for k in self.currentProfileKeys()])) + + def currentProfiles(self) -> typing.List[SpectralProfile]: + keys = self.currentProfileKeys() + return list(self.speclib().profiles(profile_keys=keys)) + def onInfoScatterClicked(self, a, b): self.mInfoScatterPoint.setVisible(False) self.mInfoScatterPointHtml = "" @@ -948,7 +967,7 @@ class SpectralLibraryPlotWidget(pg.PlotWidget): self.mInfoLabelCursor.setHtml(positionInfoHtml) def onMouseClicked(self, event): - #print(event[0].accepted) + # print(event[0].accepted) s = "" def onMouseMoved2D(self, evt): @@ -1052,14 +1071,16 @@ class SpectralLibraryPlotWidget(pg.PlotWidget): """ return [i for i in self.getPlotItem().items if isinstance(i, SpectralProfilePlotDataItem)] - def removeSpectralProfilePDIs(self, fidsToRemove: typing.List[int], updateScene: bool = True): + def removeSpectralProfilePDIs(self, keys_to_remove: typing.List[typing.Tuple[int, str]], updateScene: bool = True): """ :param updateScene: - :param fidsToRemove: feature ids to remove - :type fidsToRemove: + :param keys_to_remove: feature ids to remove + :type keys_to_remove: :return: :rtype: """ + if len(keys_to_remove) == 0: + return def disconnect(sig, slot): while True: @@ -1071,7 +1092,7 @@ class SpectralLibraryPlotWidget(pg.PlotWidget): plotItem = self.getPlotItem() assert isinstance(plotItem, pg.PlotItem) - pdisToRemove = [pdi for pdi in self.spectralProfilePlotDataItems() if pdi.id() in fidsToRemove] + pdisToRemove = [pdi for pdi in self.spectralProfilePlotDataItems() if pdi.key() in keys_to_remove] for pdi in pdisToRemove: assert isinstance(pdi, SpectralProfilePlotDataItem) pdi.setClickable(False) @@ -1079,8 +1100,8 @@ class SpectralLibraryPlotWidget(pg.PlotWidget): plotItem.removeItem(pdi) # QtGui.QGraphicsScene.items(self, *args) assert pdi not in plotItem.dataItems - if pdi.id() in self.mPlotDataItems.keys(): - self.mPlotDataItems.pop(pdi.id(), None) + if pdi.key() in self.mPlotDataItems.keys(): + self.mPlotDataItems.pop(pdi.key(), None) if updateScene: self.scene().update() @@ -1197,21 +1218,29 @@ class SpectralLibraryPlotWidget(pg.PlotWidget): if layerID != self.speclib().id(): return - + speclib: SpectralLibrary = self.speclib() + fieldIndices = [speclib.fields().indexOf(f.name()) for f in speclib.spectralValueFields()] idxF = self.speclib().fields().indexOf(FIELD_VALUES) - fids = [] + fids = set() for fid, fieldMap in featureMap.items(): - if idxF in fieldMap.keys(): - fids.append(fid) + for idx in fieldIndices: + if idx in fieldMap.keys(): + fids.add(fid) if len(fids) == 0: return + fids = list(fids) + update = False for p in self.speclib().profiles(fids): assert isinstance(p, SpectralProfile) - pdi = self.spectralProfilePlotDataItem(p.id()) + pdi = self.mPlotDataItems.get(p.key(), None) if isinstance(pdi, SpectralProfilePlotDataItem): pdi.resetSpectralProfile(p) + else: + update = True + if update: + self.updateSpectralProfilePlotItems() @pyqtSlot() def onProfileRendererChanged(self): @@ -1315,7 +1344,6 @@ class SpectralLibraryPlotWidget(pg.PlotWidget): def updateXUnit(self): - unit = self.xUnit() label = self.xAxisUnitModel().unitData(unit, role=Qt.DisplayRole) @@ -1342,29 +1370,35 @@ class SpectralLibraryPlotWidget(pg.PlotWidget): def updateSpectralProfilePlotItems(self): pi = self.getPlotItem() assert isinstance(pi, SpectralLibraryPlotItem) + n_max = self.maxProfiles() - toBeVisualized: typing.List[int] = self.profileIDsToVisualize() - visualized: typing.List[int] = self.plottedProfileIDs() - toBeRemoved = [fid for fid in visualized if fid not in toBeVisualized] - toBeAdded = [fid for fid in toBeVisualized if fid not in visualized] - - if isinstance(self.speclib(), SpectralLibrary): - selectedNow = set(self.speclib().selectedFeatureIds()) - else: - selectedNow = set() + self.mNumberOfValueErrorsProfiles = 0 + self.mNumberOfEmptyProfiles = 0 - selectionChanged = list(selectedNow.symmetric_difference(self.mSelectedIds)) - self.mSelectedIds = selectedNow + keys_visualized: typing.List[typing.Tuple[int, str]] = self.plottedProfileKeys() + pdis_to_visualize: typing.List[SpectralProfilePlotDataItem] = [] + new_pdis: typing.List[SpectralProfilePlotDataItem] = [] + sort_x_values: bool = self.xUnit() in ['DOI'] + for pkey in self.profileKeysToVisualize(): + if len(pdis_to_visualize) >= n_max: + break - if len(toBeRemoved) > 0: - self.removeSpectralProfilePDIs(toBeRemoved) + fid, field_name = pkey - if len(toBeAdded) > 0: - sort_x_values = self.xUnit() in ['DOI'] - addedPDIs = [] - addedProfiles = self.speclib().profiles(toBeAdded) - for profile in addedProfiles: + pdi: SpectralProfilePlotDataItem = self.mPlotDataItems.get(pkey, None) + if isinstance(pdi, SpectralProfilePlotDataItem): + if pdi.valueConversionPossible(): + pdis_to_visualize.append(pdi) + else: + self.mNumberOfValueErrorsProfiles += 1 + else: + # create a new PDI + profile = self.speclib().profile(fid, value_field=field_name) assert isinstance(profile, SpectralProfile) + if profile.isEmpty(): + self.mNumberOfEmptyProfiles += 1 + continue + if not self.mXUnitInitialized: self.setXUnit(profile.xUnit()) self.mXUnitInitialized = True @@ -1377,16 +1411,35 @@ class SpectralLibraryPlotWidget(pg.PlotWidget): pdi.mSortByXValues = sort_x_values pdi.applyMapFunctions() pdi.sigProfileClicked.connect(self.onProfileClicked) + if pdi.valueConversionPossible(): + new_pdis.append(pdi) + pdis_to_visualize.append(pdi) + else: + self.mNumberOfValueErrorsProfiles += 1 + + keys_to_visualize = [pdi.key() for pdi in pdis_to_visualize] + keys_to_remove = [pkey for pkey in keys_visualized if pkey not in keys_to_visualize] + keys_new = [pdi.key() for pdi in new_pdis] + if len(keys_to_remove) > 0: + s = "" + self.removeSpectralProfilePDIs(keys_to_remove) + if len(new_pdis) > 0: + for pdi in new_pdis: + self.mPlotDataItems[pdi.key()] = pdi + pi.addItems(new_pdis) - self.mPlotDataItems[profile.id()] = pdi - addedPDIs.append(pdi) - pi.addItems(addedPDIs) + if isinstance(self.speclib(), SpectralLibrary): + selectedNow = set(self.speclib().selectedFeatureIds()) + else: + selectedNow = set() - update_styles = list(set(toBeAdded + selectionChanged)) - if len(update_styles) > 0: - self.updateProfileStyles(update_styles) + selectionChanged = list(selectedNow.symmetric_difference(self.mSelectedIds)) + self.mSelectedIds = selectedNow - if len(toBeAdded + toBeRemoved + selectionChanged) > 0: + key_to_update_style = [pkey for pkey in keys_to_visualize if pkey[0] in selectionChanged or pkey in keys_new] + self.updateProfileStyles(key_to_update_style) + + if len(keys_new) > 0 or len(keys_to_remove) > 0 or len(key_to_update_style) > 0: pi.update() def resetSpectralProfiles(self): @@ -1401,14 +1454,15 @@ class SpectralLibraryPlotWidget(pg.PlotWidget): :param fid: int | QgsFeature | SpectralProfile :return: SpectralProfilePlotDataItem """ + warnings.warn('Do not use', DeprecationWarning) if isinstance(fid, QgsFeature): fid = fid.id() return self.mPlotDataItems.get(fid) - def updateProfileStyles(self, fids: typing.List[int] = None): + def updateProfileStyles(self, keys: typing.List[typing.Tuple[int, str]] = None): """ - Updates the styles for a set of SpectralProfilePlotDataItems specified by its feature ids - :param fids: profile ids to update + Updates the styles for a set of SpectralProfilePlotDataItems specified by its feature keys + :param keys: profile ids to update """ if not isinstance(self.speclib(), SpectralLibrary): @@ -1419,10 +1473,10 @@ class SpectralLibraryPlotWidget(pg.PlotWidget): pdis = self.spectralProfilePlotDataItems() # update for requested FIDs only - if isinstance(fids, list): - pdis = [pdi for pdi in pdis if pdi.id() in fids] - - xUnit = self.xUnit() + if isinstance(keys, list): + if len(keys) == 0: + return + pdis = [pdi for pdi in pdis if pdi.key() in keys] # update line colors fids2 = [pdi.id() for pdi in pdis] @@ -1507,40 +1561,112 @@ class SpectralLibraryPlotWidget(pg.PlotWidget): Returns stats related to existing and visualized SpectralProfiles """ stats = SpectralLibraryPlotStats() - stats.features_plotted_max = self.maxProfiles() + stats.profiles_plotted_max = self.maxProfiles() + if isinstance(self.speclib(), SpectralLibrary) and not sip.isdeleted(self.speclib()): - stats.features_speclib = self.speclib().featureCount() - stats.features_speclib_selected = self.speclib().selectedFeatureCount() + stats.features_total = self.speclib().featureCount() + stats.features_selected = self.speclib().selectedFeatureCount() stats.filter_mode = self.dualView().filterMode() - if stats.filter_mode != QgsAttributeTableFilterModel.ShowAll: - stats.features_filtered = self.dualView().filteredFeatureCount() - selected_fids = self.speclib().selectedFeatureIds() - for f in self.dualView().filteredFeatures(): - if f in selected_fids: - stats.features_filtered_selected += 1 + filtered_fids = self.dualView().filteredFeatures() + selected_fids = self.speclib().selectedFeatureIds() + + stats.features_filtered = len(filtered_fids) + + stats.profiles_total = stats.features_total * len(self.speclib().spectralValueFields()) + stats.profiles_filtered = stats.features_filtered * len(self.speclib().spectralValueFields()) + stats.profiles_error = self.mNumberOfValueErrorsProfiles + stats.profiles_empty = self.mNumberOfEmptyProfiles + + for pdi in self.allSpectralProfilePlotDataItems(): + fid, value_field = pdi.key() + if pdi.isVisible(): + stats.profiles_plotted += 1 - for pdi in self.allSpectralProfilePlotDataItems(): - if pdi.isVisible(): - stats.features_plotted += 1 - elif not pdi.valueConversionPossible(): - stats.features_with_value_error += 1 + if fid in selected_fids: + stats.profiles_selected += 1 return stats + def plottedProfileKeys(self) -> typing.List[typing.Tuple[int, str]]: + return [pdi.key() for pdi in self.mPlotDataItems.values()] + def plottedProfileIDs(self) -> typing.List[int]: """ Returns the feature IDs of visualize SpectralProfiles from the connected SpectralLibrary. """ - return list(self.mPlotDataItems.keys()) + return [pdi.id() for pdi in self.mPlotDataItems.values()] - def profileIDsToVisualize(self) -> typing.List[int]: + def profileKeysToVisualize(self) -> typing.List[typing.Tuple[int, str]]: """ Returns the list of profile/feature ids to be visualized. - The maximum number is determined by self.maxProfiles() - Order of returned fids is equal to its importance. - 1st position = most important, should be plottet on top of all other profiles + Order of returned keys is equal to its importance. + 1st position = most important, should be plotted on top of all other profiles + """ + if not isinstance(self.speclib(), SpectralLibrary): + return [] + + fieldNames = [f.name() for f in self.speclib().spectralValueFields()] + + if len(fieldNames) == 0: + return [] + + selectedOnly = self.actionShowSelectedProfilesOnly().isChecked() + selectedIds = self.speclib().selectedFeatureIds() + + dualView = self.dualView() + if isinstance(dualView, QgsDualView) and dualView.filteredFeatureCount() > 0: + allIDs = dualView.filteredFeatures() + selectedIds = [fid for fid in allIDs if fid in selectedIds] + else: + allIDs = self.speclib().allFeatureIds() + + # Order: + # 1. visible in table + # 2. selected + # 3. others + + # overlaid features / current spectral + priority0 = self.currentProfileIDs() + priority1 = [] # visible features + priority2 = [] # selected features + priority3 = [] # any other : not visible / not selected + + if isinstance(dualView, QgsDualView): + tv = dualView.tableView() + assert isinstance(tv, QTableView) + if not selectedOnly: + rowHeight = tv.rowViewportPosition(1) - tv.rowViewportPosition(0) + if rowHeight > 0: + for y in range(0, tv.viewport().height(), rowHeight): + idx = dualView.tableView().indexAt(QPoint(0, y)) + if idx.isValid(): + fid = tv.model().data(idx, role=Qt.UserRole) + priority1.append(fid) + priority2 = self.dualView().masterModel().layer().selectedFeatureIds() + if not selectedOnly: + priority3 = dualView.filteredFeatures() + else: + priority2 = selectedIds + if not selectedOnly: + priority3 = allIDs + + featurePool = np.unique(priority0 + priority1 + priority2 + priority3).tolist() + toVisualize = sorted(featurePool, + key=lambda fid: (fid not in priority0, fid not in priority1, fid not in priority2, fid)) + + results = [] + for fid in toVisualize: + for n in fieldNames: + results.append((fid, n)) + return results + + def profileIDsToVisualizeOLD(self) -> typing.List[int]: + """ + Returns the list of profile/feature ids to be visualized. + Order of returned keys is equal to its importance. + 1st position = most important, should be plotted on top of all other profiles """ if not isinstance(self.speclib(), SpectralLibrary): return [] @@ -1568,10 +1694,8 @@ class SpectralLibraryPlotWidget(pg.PlotWidget): # 2. selected # 3. others - - # overlaid features / current spectral - priority0 = sorted(self.mTEMPORARY_HIGHLIGHTED) + priority0 = self.currentProfileIDs() priority1 = [] # visible features priority2 = [] # selected features priority3 = [] # any other : not visible / not selected @@ -1622,7 +1746,7 @@ class SpectralLibraryPlotWidget(pg.PlotWidget): mimeData = event.mimeData() if containsSpeclib(mimeData) and isinstance(self.speclib(), SpectralLibrary): speclib = SpectralLibrary.readFromMimeData(mimeData) - print(f'DROP SPECLIB {speclib}') + # print(f'DROP SPECLIB {speclib}') if isinstance(speclib, SpectralLibrary) and len(speclib) > 0: b = self.speclib().isEditable() @@ -1635,57 +1759,123 @@ class SpectralLibraryPlotWidget(pg.PlotWidget): super().dropEvent(event) -class SpectralProfileValueTableModel(QAbstractTableModel): +class SpectralProfileTableModel(QAbstractTableModel): """ A TableModel to show and edit spectral values of a SpectralProfile """ + sigXUnitChanged = pyqtSignal(str) + sigYUnitChanged = pyqtSignal(str) + def __init__(self, *args, **kwds): - super(SpectralProfileValueTableModel, self).__init__(*args, **kwds) + super(SpectralProfileTableModel, self).__init__(*args, **kwds) + + self.mColumnNames = {0: 'x', + 1: 'y'} + self.mColumnUnits = {0: None, + 1: None} + + self.mValuesX: typing.Dict[int, typing.Any] = {} + self.mValuesY: typing.Dict[int, typing.Any] = {} + self.mValuesBBL: typing.Dict[int, typing.Any] = {} + + self.mLastProfile: SpectralProfile = SpectralProfile() - self.mColumnDataTypes = [float, float] - self.mColumnDataUnits = ['-', '-'] - self.mValues = EMPTY_PROFILE_VALUES.copy() + self.mRows: int = 0 - def setProfileData(self, values): + def setBands(self, bands: int): + bands = int(bands) + + assert bands >= 0 + + if bands > self.bands(): + self.beginInsertRows(QModelIndex(), self.bands(), bands - 1) + self.mRows = bands + self.endInsertRows() + + elif bands < self.bands(): + self.beginRemoveRows(QModelIndex(), bands, self.bands() - 1) + self.mRows = bands + self.endRemoveRows() + + def bands(self) -> int: + return self.rowCount() + + def setProfile(self, profile: SpectralProfile): """ :param values: :return: """ - if isinstance(values, SpectralProfile): - values = values.values() - assert isinstance(values, dict) + assert isinstance(profile, SpectralProfile) - for k in EMPTY_PROFILE_VALUES.keys(): - assert k in values.keys() + self.beginResetModel() + self.mValuesX.clear() + self.mValuesY.clear() + self.mValuesBBL.clear() + self.mLastProfile = profile + self.mValuesX.update({i: v for i, v in enumerate(profile.xValues())}) + self.mValuesY.update({i: v for i, v in enumerate(profile.yValues())}) + self.mValuesBBL.update({i: v for i, v in enumerate(profile.bbl())}) - for i, k in enumerate(['y', 'x']): - if values[k] and len(values[k]) > 0: - self.setColumnDataType(i, type(values[k][0])) - else: - self.setColumnDataType(i, float) - self.setColumnValueUnit('y', values.get('yUnit', '')) - self.setColumnValueUnit('x', values.get('xUnit', '')) + self.setBands(len(self.mValuesY)) - self.beginResetModel() - self.mValues.update(values) self.endResetModel() + self.setXUnit(profile.xUnit()) + self.setYUnit(profile.yUnit()) + + def setXUnit(self, unit: str): + if self.xUnit() != unit: + self.mColumnUnits[0] = unit + idx0 = self.index(0, 0) + idx1 = self.index(self.rowCount(QModelIndex()) - 1, 0) + self.dataChanged.emit(idx0, idx1) + # self.headerDataChanged.emit(Qt.Horizontal, 0, self.columnCount(QModelIndex())-1) + self.sigXUnitChanged.emit(unit) + + def setYUnit(self, unit: str): + if self.yUnit() != unit: + self.mColumnUnits[1] = unit + # self.headerDataChanged.emit(Qt.Horizontal, 0, self.columnCount(QModelIndex())-1) + self.sigYUnitChanged.emit(unit) - def values(self) -> dict: + def xUnit(self) -> str: + return self.mColumnUnits[0] + + def yUnit(self) -> str: + return self.mColumnUnits[1] + + def profile(self) -> SpectralProfile: """ - Returns the value dictionary of a SpectralProfile - :return: dict + Return the data as new SpectralProfile + :return: + :rtype: """ - return self.mValues + p = SpectralProfile(fields=self.mLastProfile.fields()) + nb = self.bands() - def rowCount(self, QModelIndex_parent=None, *args, **kwargs): - if self.mValues['y'] is None: - return 0 + y = [self.mValuesY.get(b, None) for b in range(nb)] + if self.xUnit() == BAND_INDEX: + x = None else: - return len(self.mValues['y']) + x = [self.mValuesX.get(b, None) for b in range(nb)] + + bbl = [self.mValuesBBL.get(b, None) for b in range(nb)] + bbl = np.asarray(bbl, dtype=bool) + if np.any(bbl == False) == False: + bbl = None + p.setValues(x, y, xUnit=self.xUnit(), yUnit=self.yUnit(), bbl=bbl) + + return p + + def resetProfile(self): + self.setProfile(self.mLastProfile) - def columnCount(self, parent=QModelIndex()): - return 2 + def rowCount(self, parent: QModelIndex = None, *args, **kwargs) -> int: + + return self.mRows + + def columnCount(self, parent=QModelIndex()) -> int: + return len(self.mColumnNames) def data(self, index, role=Qt.DisplayRole): if role is None or not index.isValid(): @@ -1697,17 +1887,28 @@ class SpectralProfileValueTableModel(QAbstractTableModel): if role in [Qt.DisplayRole, Qt.EditRole]: value = None if c == 0: - value = self.mValues['y'][i] + if self.xUnit() != BAND_INDEX: + value = self.mValuesX.get(i, None) + if value: + return str(value) + else: + return None + else: + return i + 1 elif c == 1: - value = self.mValues['x'][i] - - # log('data: {} {}'.format(type(value), value)) - return value - - if role == Qt.UserRole: - return self.mValues + value = self.mValuesY.get(i, None) + if value: + return str(value) + else: + return None + elif role == Qt.CheckStateRole: + if c == 0: + if bool(self.mValuesBBL.get(i, True)): + return Qt.Checked + else: + return Qt.Unchecked return None def setData(self, index, value, role=None): @@ -1717,81 +1918,29 @@ class SpectralProfileValueTableModel(QAbstractTableModel): c = index.column() i = index.row() - if role == Qt.EditRole: - # cast to correct data type - dt = self.mColumnDataTypes[c] - value = dt(value) + modified = False + if role == Qt.CheckStateRole: + if c == 0: + self.mValuesBBL[i] = value == Qt.Checked + modified = True + if role == Qt.EditRole: if c == 0: - self.mValues['y'][i] = value - return True + try: + self.mValuesX[i] = float(value) + modified = True + except: + pass elif c == 1: - self.mValues['x'][i] = value - return True - return False - - def index2column(self, index) -> int: - """ - Returns a column index - :param index: QModelIndex, int or str from ['x','y'] - :return: int - """ - if isinstance(index, str): - index = ['y', 'x'].index(index.strip().lower()) - elif isinstance(index, QModelIndex): - index = index.column() - - assert isinstance(index, int) and index >= 0 - return index - - def setColumnValueUnit(self, index, valueUnit: str): - """ - Sets the unit of the value column - :param index: 'y','x', respective 0, 1 - :param valueUnit: str with unit, e.g. 'Reflectance' or 'um' - """ - index = self.index2column(index) - if valueUnit is None: - valueUnit = '-' - - assert isinstance(valueUnit, str) - - if self.mColumnDataUnits[index] != valueUnit: - self.mColumnDataUnits[index] = valueUnit - self.headerDataChanged.emit(Qt.Horizontal, index, index) - self.sigColumnValueUnitChanged.emit(index, valueUnit) - - sigColumnValueUnitChanged = pyqtSignal(int, str) - - def setColumnDataType(self, index, dataType: type): - """ - Sets the numeric dataType in which spectral values are returned - :param index: 'y','x', respective 0, 1 - :param dataType: int or float (default) - """ - index = self.index2column(index) - if isinstance(dataType, str): - i = ['Integer', 'Float'].index(dataType) - dataType = [int, float][i] - - assert dataType in [int, float] - - if self.mColumnDataTypes[index] != dataType: - self.mColumnDataTypes[index] = dataType - - if index == 0: - y = self.mValues.get('y') - if isinstance(y, list) and len(y) > 0: - self.mValues['y'] = [dataType(v) for v in self.mValues['y']] - elif index == 1: - x = self.mValues.get('x') - if isinstance(x, list) and len(x) > 0: - self.mValues['x'] = [dataType(v) for v in self.mValues['x']] - - self.dataChanged.emit(self.createIndex(0, index), self.createIndex(self.rowCount(), index)) - self.sigColumnDataTypeChanged.emit(index, dataType) + try: + self.mValuesY[i] = float(value) + modified = True + except: + pass - sigColumnDataTypeChanged = pyqtSignal(int, type) + if modified: + self.dataChanged.emit(index, index, [role]) + return modified def flags(self, index): if index.isValid(): @@ -1799,52 +1948,68 @@ class SpectralProfileValueTableModel(QAbstractTableModel): flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable if c == 0: - flags = flags | Qt.ItemIsEditable - elif c == 1 and self.mValues['xUnit']: + flags = flags | Qt.ItemIsUserCheckable + if self.xUnit() != BAND_INDEX: + flags = flags | Qt.ItemIsEditable + elif c == 1: flags = flags | Qt.ItemIsEditable return flags - # return item.qt_flags(index.column()) return None - def headerData(self, col, orientation, role): - if Qt is None: - return None + def headerData(self, col: int, orientation, role): + if orientation == Qt.Horizontal and role in [Qt.DisplayRole, Qt.ToolTipRole]: - name = ['Y', 'X'][col] - unit = self.mColumnDataUnits[col] - if unit in EMPTY_VALUES: - unit = '-' - return '{} [{}]'.format(name, unit) - elif orientation == Qt.Vertical and role == Qt.DisplayRole: - return col + return self.mColumnNames.get(col, f'{col + 1}') return None class SpectralProfileEditorWidget(QWidget): - sigProfileValuesChanged = pyqtSignal(dict) + sigProfileChanged = pyqtSignal() def __init__(self, *args, **kwds): super(SpectralProfileEditorWidget, self).__init__(*args, **kwds) loadUi(speclibUiPath('spectralprofileeditorwidget.ui'), self) - self.mDefault = None - self.mModel = SpectralProfileValueTableModel(parent=self) - self.mModel.dataChanged.connect(lambda: self.sigProfileValuesChanged.emit(self.profileValues())) - self.mModel.sigColumnValueUnitChanged.connect(self.onValueUnitChanged) - self.mModel.sigColumnDataTypeChanged.connect(self.onDataTypeChanged) + self.mDefault: SpectralProfile = None + self.mModel: SpectralProfileTableModel = SpectralProfileTableModel() + self.mModel.rowsInserted.connect(self.onBandsChanged) + self.mModel.rowsRemoved.connect(self.onBandsChanged) + self.mModel.dataChanged.connect(lambda *args: self.onProfileChanged()) + self.mXUnitModel: XUnitModel = XUnitModel() + self.cbXUnit.setModel(self.mXUnitModel) + self.cbXUnit.currentIndexChanged.connect( + lambda *args: self.mModel.setXUnit(self.cbXUnit.currentData(Qt.UserRole))) + self.mModel.sigXUnitChanged.connect(self.onXUnitChanged) + + self.tbYUnit.textChanged.connect(self.mModel.setYUnit) + self.mModel.sigYUnitChanged.connect(self.tbYUnit.setText) + self.mModel.sigYUnitChanged.connect(self.onProfileChanged) + self.mModel.sigXUnitChanged.connect(self.onProfileChanged) + # self.mModel.sigColumnValueUnitChanged.connect(self.onValueUnitChanged) + # self.mModel.sigColumnDataTypeChanged.connect(self.onDataTypeChanged) + self.tableView.setModel(self.mModel) + + self.actionReset.triggered.connect(self.resetProfile) + self.btnReset.setDefaultAction(self.actionReset) - self.cbYUnit.currentTextChanged.connect(lambda unit: self.mModel.setColumnValueUnit(0, unit)) - self.cbXUnit.currentTextChanged.connect(lambda unit: self.mModel.setColumnValueUnit(1, unit)) + self.sbBands.valueChanged.connect(self.mModel.setBands) + # self.onDataTypeChanged(0, float) + # self.onDataTypeChanged(1, float) - self.cbYUnitDataType.currentTextChanged.connect(lambda v: self.mModel.setColumnDataType(0, v)) - self.cbXUnitDataType.currentTextChanged.connect(lambda v: self.mModel.setColumnDataType(1, v)) + self.setProfile(SpectralProfile()) - self.actionReset.triggered.connect(self.resetProfileValues) - self.btnReset.setDefaultAction(self.actionReset) + def onProfileChanged(self): + if self.profile() != self.mDefault: + self.sigProfileChanged.emit() - self.onDataTypeChanged(0, float) - self.onDataTypeChanged(1, float) + def onXUnitChanged(self, unit: str): + unit = self.mXUnitModel.findUnit(unit) + if unit is None: + unit = BAND_INDEX + self.cbXUnit.setCurrentIndex(self.mXUnitModel.unitIndex(unit).row()) - self.setProfileValues(EMPTY_PROFILE_VALUES.copy()) + def onBandsChanged(self, *args): + self.sbBands.setValue(self.mModel.bands()) + self.onProfileChanged() def initConfig(self, conf: dict): """ @@ -1852,111 +2017,89 @@ class SpectralProfileEditorWidget(QWidget): :param conf: dict """ - if 'xUnitList' in conf.keys(): - self.cbXUnit.addItems(conf['xUnitList']) + pass - if 'yUnitList' in conf.keys(): - self.cbYUnit.addItems(conf['yUnitList']) - - def onValueUnitChanged(self, index: int, unit: str): - comboBox = [self.cbYUnit, self.cbXUnit][index] - setComboboxValue(comboBox, unit) - - def onDataTypeChanged(self, index: int, dataType: type): - - if dataType == int: - typeString = 'Integer' - elif dataType == float: - typeString = 'Float' - else: - raise NotImplementedError() - comboBox = [self.cbYUnitDataType, self.cbXUnitDataType][index] - - setComboboxValue(comboBox, typeString) - - def setProfileValues(self, values): + def setProfile(self, profile: SpectralProfile): """ Sets the profile values to be shown :param values: dict() or SpectralProfile :return: """ + assert isinstance(profile, SpectralProfile) + self.mDefault = profile - if isinstance(values, SpectralProfile): - values = values.values() - - assert isinstance(values, dict) - import copy - self.mDefault = copy.deepcopy(values) - self.mModel.setProfileData(values) + self.mModel.setProfile(profile) - def resetProfileValues(self): - self.setProfileValues(self.mDefault) + def resetProfile(self): + self.mModel.setProfile(self.mDefault) - def profileValues(self) -> dict: + def profile(self) -> SpectralProfile: """ - Returns the value dictionary of a SpectralProfile + Returns modified SpectralProfile :return: dict """ - return self.mModel.values() + + return self.mModel.profile() class SpectralProfileEditorWidgetWrapper(QgsEditorWidgetWrapper): def __init__(self, vl: QgsVectorLayer, fieldIdx: int, editor: QWidget, parent: QWidget): super(SpectralProfileEditorWidgetWrapper, self).__init__(vl, fieldIdx, editor, parent) - self.mEditorWidget = None - self.mLabel = None - self.mDefaultValue = None + self.mWidget: QWidget = None + + self.mLastValue = QVariant() def createWidget(self, parent: QWidget): # log('createWidget') - w = None + if not self.isInTable(parent): - w = SpectralProfileEditorWidget(parent=parent) + self.mWidget = SpectralProfileEditorWidget(parent=parent) else: - # w = PlotStyleButton(parent) - w = QWidget(parent) - w.setVisible(False) - return w + self.mWidget = QLabel(' Profile', parent=parent) + return self.mWidget def initWidget(self, editor: QWidget): # log(' initWidget') conf = self.config() if isinstance(editor, SpectralProfileEditorWidget): - self.mEditorWidget = editor - self.mEditorWidget.sigProfileValuesChanged.connect(self.onValueChanged) - self.mEditorWidget.initConfig(conf) - if isinstance(editor, QWidget): - self.mLabel = editor - self.mLabel.setVisible(False) - self.mLabel.setToolTip('Use Form View to edit values') + editor.sigProfileChanged.connect(self.onValueChanged) + editor.initConfig(conf) + + elif isinstance(editor, QLabel): + editor.setText(SPECTRAL_PROFILE_FIELD_REPRESENT_VALUE) + editor.setToolTip('Use Form View to edit values') def onValueChanged(self, *args): - self.valueChanged.emit(self.value()) + self.valuesChanged.emit(self.value()) s = "" def valid(self, *args, **kwargs) -> bool: - return isinstance(self.mEditorWidget, SpectralProfileEditorWidget) or isinstance(self.mLabel, QWidget) + return isinstance(self.mWidget, (SpectralProfileEditorWidget, QLabel)) def value(self, *args, **kwargs): - value = self.mDefaultValue - if isinstance(self.mEditorWidget, SpectralProfileEditorWidget): - v = self.mEditorWidget.profileValues() - value = encodeProfileValueDict(v) + value = self.mLastValue + w = self.widget() + if isinstance(w, SpectralProfileEditorWidget): + p = w.profile() + value = encodeProfileValueDict(p.values()) return value def setEnabled(self, enabled: bool): - - if self.mEditorWidget: - self.mEditorWidget.setEnabled(enabled) + w = self.widget() + if isinstance(w, SpectralProfileEditorWidget): + w.setEnabled(enabled) def setValue(self, value): - if isinstance(self.mEditorWidget, SpectralProfileEditorWidget): - self.mEditorWidget.setProfileValues(decodeProfileValueDict(value)) - self.mDefaultValue = value + self.mLastValue = value + p = SpectralProfile(values=decodeProfileValueDict(value)) + w = self.widget() + if isinstance(w, SpectralProfileEditorWidget): + w.setProfile(p) + # if isinstance(self.mLabel, QLabel): # self.mLabel.setText(value2str(value)) @@ -1968,60 +2111,73 @@ class SpectralProfileEditorConfigWidget(QgsEditorConfigWidget): super(SpectralProfileEditorConfigWidget, self).__init__(vl, fieldIdx, parent) loadUi(speclibUiPath('spectralprofileeditorconfigwidget.ui'), self) - self.mLastConfig = {} + self.mLastConfig: dict = {} + self.MYCACHE = dict() + self.mFieldExpressionName: QgsFieldExpressionWidget + self.mFieldExpressionSource: QgsFieldExpressionWidget - self.tbXUnits.textChanged.connect(lambda: self.changed.emit()) - self.tbYUnits.textChanged.connect(lambda: self.changed.emit()) + self.mFieldExpressionName.setLayer(vl) + self.mFieldExpressionSource.setLayer(vl) - self.tbResetX.setDefaultAction(self.actionResetX) - self.tbResetY.setDefaultAction(self.actionResetY) + self.mFieldExpressionName.setFilters(QgsFieldProxyModel.String) + self.mFieldExpressionSource.setFilters(QgsFieldProxyModel.String) - def unitTextBox(self, dim: str) -> QPlainTextEdit: - if dim == 'x': - return self.tbXUnits - elif dim == 'y': - return self.tbYUnits - else: - raise NotImplementedError() - - def units(self, dim: str) -> list: - textEdit = self.unitTextBox(dim) - assert isinstance(textEdit, QPlainTextEdit) - values = [] - for line in textEdit.toPlainText().splitlines(): - v = line.strip() - if len(v) > 0 and v not in values: - values.append(v) - return values - - def setUnits(self, dim: str, values: list): - textEdit = self.unitTextBox(dim) - assert isinstance(textEdit, QPlainTextEdit) - textEdit.setPlainText('\n'.join(values)) + self.mFieldExpressionName.fieldChanged[str, bool].connect(self.onFieldChanged) + self.mFieldExpressionSource.fieldChanged[str, bool].connect(self.onFieldChanged) + + def onFieldChanged(self, expr: str, valid: bool): + if valid: + self.changed.emit() + + def expressionName(self) -> QgsExpression: + exp = QgsExpression(self.mFieldExpressionName.expression()) + return exp + + def expressionSource(self) -> QgsExpression: + exp = QgsExpression(self.mFieldExpressionSource.expression()) + return exp def config(self, *args, **kwargs) -> dict: - config = {'xUnitList': self.units('x'), - 'yUnitList': self.units('y') - } + config = {'expressionName': self.mFieldExpressionName.expression(), + 'expressionSource': self.mFieldExpressionSource.expression(), + 'mycache': self.MYCACHE} + return config def setConfig(self, config: dict): - if 'xUnitList' in config.keys(): - self.setUnits('x', config['xUnitList']) - - if 'yUnitList' in config.keys(): - self.setUnits('y', config['yUnitList']) - self.mLastConfig = config + field: QgsField = self.layer().fields().at(self.field()) + defaultExprName = "format('Profile %1 {}',$id)".format(field.name()) + defaultExprSource = "" + # set some defaults + if True: + for field in self.layer().fields(): + assert isinstance(field, QgsField) + if field.name() == 'name': + defaultExprName = f'"{field.name()}"' + if field.name() == 'source': + defaultExprSource = f'"{field.name()}"' + + self.mFieldExpressionName.setExpression(config.get('expressionName', defaultExprName)) + self.mFieldExpressionSource.setExpression(config.get('expressionSource', defaultExprSource)) # print('setConfig') - def resetUnits(self, dim: str): - if dim == 'x' and 'xUnitList' in self.mLastConfig.keys(): - self.setUnit('x', self.mLastConfig['xUnitList']) +class SpectralProfileFieldFormatter(QgsFieldFormatter): + + def __init__(self, *args, **kwds): + super(SpectralProfileFieldFormatter, self).__init__(*args, **kwds) + + def id(self) -> str: + return EDITOR_WIDGET_REGISTRY_KEY - if dim == 'y' and 'yUnitList' in self.mLastConfig.keys(): - self.setUnit('y', self.mLastConfig['yUnitList']) + def representValue(self, layer: QgsVectorLayer, fieldIndex: int, config: dict, cache, value): + + if value not in [None, NULL]: + return SPECTRAL_PROFILE_FIELD_REPRESENT_VALUE + else: + return 'Empty' + s = "" class SpectralProfileEditorWidgetFactory(QgsEditorWidgetFactory): @@ -2044,17 +2200,17 @@ class SpectralProfileEditorWidgetFactory(QgsEditorWidgetFactory): w = SpectralProfileEditorConfigWidget(layer, fieldIdx, parent) key = self.configKey(layer, fieldIdx) w.setConfig(self.readConfig(key)) - w.changed.connect(lambda: self.writeConfig(key, w.config())) + w.changed.connect(lambda *args, ww=w, k=key: self.writeConfig(key, ww.config())) return w - def configKey(self, layer: QgsVectorLayer, fieldIdx: int): + def configKey(self, layer: QgsVectorLayer, fieldIdx: int) -> typing.Tuple[str, int]: """ Returns a tuple to be used as dictionary key to identify a layer field configuration. :param layer: QgsVectorLayer :param fieldIdx: int :return: (str, int) """ - return (layer.id(), fieldIdx) + return layer.id(), fieldIdx def create(self, layer: QgsVectorLayer, fieldIdx: int, editor: QWidget, parent: QWidget) -> SpectralProfileEditorWidgetWrapper: @@ -2066,7 +2222,9 @@ class SpectralProfileEditorWidgetFactory(QgsEditorWidgetFactory): :param parent: QWidget :return: SpectralProfileEditorWidgetWrapper """ + w = SpectralProfileEditorWidgetWrapper(layer, fieldIdx, editor, parent) + # self.editWrapper = w return w def writeConfig(self, key: tuple, config: dict): @@ -2083,16 +2241,16 @@ class SpectralProfileEditorWidgetFactory(QgsEditorWidgetFactory): :param key: tuple (str, int), as created with .configKey(layer, fieldIdx) :return: {} """ - if key in self.mConfigurations.keys(): - conf = self.mConfigurations[key] - else: - # return the very default configuration - conf = {'xUnitList': X_UNITS[:], - 'yUnitList': Y_UNITS[:] - } - # print('Read config') - # print((key, conf)) - return conf + return self.mConfigurations.get(key, {}) + + def supportsField(self, vl: QgsVectorLayer, fieldIdx: int) -> bool: + """ + :param vl: + :param fieldIdx: + :return: + """ + field: QgsField = vl.fields().at(fieldIdx) + return field.type() == QVariant.ByteArray def fieldScore(self, vl: QgsVectorLayer, fieldIdx: int) -> int: """ @@ -2108,21 +2266,24 @@ class SpectralProfileEditorWidgetFactory(QgsEditorWidgetFactory): # log(' fieldScore()') field = vl.fields().at(fieldIdx) assert isinstance(field, QgsField) - if field.type() == QVariant.String and field.name() == FIELD_VALUES: + if field.type() == QVariant.ByteArray: return 20 - elif field.type() == QVariant.String: - return 0 else: return 0 def registerSpectralProfileEditorWidget(): - reg = QgsGui.editorWidgetRegistry() + widgetRegistry = QgsGui.editorWidgetRegistry() + fieldFormaterRegistry = QgsApplication.instance().fieldFormatterRegistry() - if not EDITOR_WIDGET_REGISTRY_KEY in reg.factories().keys(): + if not EDITOR_WIDGET_REGISTRY_KEY in widgetRegistry.factories().keys(): global SPECTRAL_PROFILE_EDITOR_WIDGET_FACTORY + global SPECTRAL_PROFILE_FIELD_FORMATTER SPECTRAL_PROFILE_EDITOR_WIDGET_FACTORY = SpectralProfileEditorWidgetFactory(EDITOR_WIDGET_REGISTRY_KEY) - reg.registerWidget(EDITOR_WIDGET_REGISTRY_KEY, SPECTRAL_PROFILE_EDITOR_WIDGET_FACTORY) + SPECTRAL_PROFILE_FIELD_FORMATTER = SpectralProfileFieldFormatter() + widgetRegistry.registerWidget(EDITOR_WIDGET_REGISTRY_KEY, SPECTRAL_PROFILE_EDITOR_WIDGET_FACTORY) + fieldFormaterRegistry.addFieldFormatter(SPECTRAL_PROFILE_FIELD_FORMATTER) + s = "" class SpectralLibraryWidget(AttributeTableWidget): @@ -2211,7 +2372,7 @@ class SpectralLibraryWidget(AttributeTableWidget): self.actionImportVectorRasterSource = QAction('Import profiles from raster + vector source') self.actionImportVectorRasterSource.setToolTip('Import spectral profiles from a raster image ' - 'based on vector geometries (Points).') + 'based on vector geometries (Points).') self.actionImportVectorRasterSource.setIcon(QIcon(':/images/themes/default/mActionAddOgrLayer.svg')) self.actionImportVectorRasterSource.triggered.connect(self.onImportFromRasterSource) @@ -2252,7 +2413,7 @@ class SpectralLibraryWidget(AttributeTableWidget): self.insertToolBar(self.mToolbar, self.tbSpeclibAction) - self.actionShowProperties = QAction('Show Spectral Library Poperties') + self.actionShowProperties = QAction('Show Spectral Library Properties') self.actionShowProperties.setToolTip('Show Spectral Library Properties') self.actionShowProperties.setIcon(QIcon(':/images/themes/default/propertyicons/system.svg')) self.actionShowProperties.triggered.connect(self.showProperties) @@ -2371,7 +2532,6 @@ class SpectralLibraryWidget(AttributeTableWidget): for iface in separated: iface.addExportActions(self.speclib(), menu) - def plotWidget(self) -> SpectralLibraryPlotWidget: return self.mPlotWidget @@ -2411,7 +2571,7 @@ class SpectralLibraryWidget(AttributeTableWidget): Adds all current spectral profiles to the "persistent" SpectralLibrary """ - fids = self.currentProfileIds() + fids = self.plotWidget().currentProfileIDs() self.plotWidget().mTEMPORARY_HIGHLIGHTED.clear() self.plotWidget().updateProfileStyles(fids) @@ -2429,15 +2589,15 @@ class SpectralLibraryWidget(AttributeTableWidget): # stop plot updates plotWidget.mUpdateTimer.stop() restart_editing = not speclib.startEditing() - oldCurrentIds = self.currentProfileIds() - + oldCurrentKeys = self.plotWidget().currentProfileKeys() + oldCurrentIDs = self.plotWidget().currentProfileIDs() addAuto: bool = self.optionAddCurrentProfilesAutomatically.isChecked() if not addAuto: # delete previous current profiles from speclib - speclib.deleteFeatures(oldCurrentIds) - plotWidget.removeSpectralProfilePDIs(oldCurrentIds, updateScene=False) - # now there should'nt be any PDI or style ref related to an old ID + speclib.deleteFeatures(oldCurrentIDs) + plotWidget.removeSpectralProfilePDIs(oldCurrentKeys, updateScene=False) + # now there shouldn't be any PDI or style ref related to an old ID else: self.addCurrentSpectraToSpeclib() @@ -2447,7 +2607,7 @@ class SpectralLibraryWidget(AttributeTableWidget): p = currentProfiles[i] assert isinstance(p, QgsFeature) if not isinstance(p, SpectralProfile): - p = SpectralProfile.fromSpecLibFeature(p) + p = SpectralProfile.fromQgsFeature(p) currentProfiles[i] = p # add current profiles to speclib @@ -2459,7 +2619,12 @@ class SpectralLibraryWidget(AttributeTableWidget): speclib.startEditing() addedIDs = sorted(set(speclib.allFeatureIds()).difference(oldIDs)) + addedKeys = [] + value_fields = [f.name() for f in self.speclib().spectralValueFields()] + for id in addedIDs: + for n in value_fields: + addedKeys.append((id, n)) # set profile style PROFILE2FID = dict() for p, fid in zip(currentProfiles, addedIDs): @@ -2479,19 +2644,12 @@ class SpectralLibraryWidget(AttributeTableWidget): if not addAuto: # give current spectra the current spectral style - self.plotWidget().mTEMPORARY_HIGHLIGHTED.update(addedIDs) + self.plotWidget().mTEMPORARY_HIGHLIGHTED.update(addedKeys) plotWidget.mUpdateTimer.start() - def currentProfileIds(self) -> typing.List[int]: - return sorted(self.plotWidget().mTEMPORARY_HIGHLIGHTED) - def currentProfiles(self) -> typing.List[SpectralProfile]: - """ - Returns the SpectralProfiles which are not added to the SpectralLibrary but shown as over-plot items - :return: [list-of-SpectralProfiles] - """ - return list(self.speclib().profiles(self.currentProfileIds())) + return self.mPlotWidget.currentProfiles() def canvas(self) -> QgsMapCanvas: """ @@ -2560,16 +2718,7 @@ class SpectralLibraryWidget(AttributeTableWidget): """ Removes all SpectralProfiles and additional fields """ - feature_ids = self.speclib().allFeatureIds() - self.speclib().startEditing() - self.speclib().deleteFeatures(feature_ids) - self.speclib().commitChanges() - - for fieldName in self.speclib().optionalFieldNames(): - index = self.spectralLibrary().fields().indexFromName(fieldName) - self.spectralLibrary().startEditing() - self.spectralLibrary().deleteAttribute(index) - self.spectralLibrary().commitChanges() + warnings.warn('Deprectated and desimplemented', DeprecationWarning) class SpectralLibraryInfoLabel(QLabel): @@ -2603,37 +2752,33 @@ class SpectralLibraryInfoLabel(QLabel): # total + filtering if stats.filter_mode == QgsAttributeTableFilterModel.ShowFilteredList: - needed = stats.features_filtered - selected = stats.features_filtered_selected - msg += f'{stats.features_filtered}f/' - ttp += f'{stats.features_filtered} profiles filtered out of {stats.features_speclib}<br/>' + msg += f'{stats.profiles_filtered}f' + ttp += f'{stats.profiles_filtered} profiles filtered out of {stats.profiles_total}<br/>' else: # show all - needed = stats.features_speclib - selected = stats.features_speclib_selected - msg += f'{stats.features_speclib}</span>/' - ttp += f'{stats.features_speclib} profiles in total<br/>' + msg += f'{stats.profiles_total}' + ttp += f'{stats.profiles_total} profiles in total<br/>' # show selected - msg += f'{selected}/' - ttp += f'{selected} selected in plot/table<br/>' - - exceeds_limit = needed > stats.features_plotted - - if exceeds_limit: - msg += f'<span style="color:red">{stats.features_plotted}({needed})</span>' - ttp += f'<span style="color:red">' \ - f'{stats.features_plotted} of {needed} profiles plotted<br/>' \ - f'<br/>Increase plot limit to show more profiles at same time.' \ - f'(Might slow-down plot speed)</span>' + msg += f'/{stats.profiles_selected}' + ttp += f'{stats.profiles_selected} selected in plot<br/>' + + if stats.profiles_empty > 0: + msg += f'/<span style="color:red">{stats.profiles_empty}N</span>' + ttp += f'<span style="color:red">At least {stats.profiles_empty} profile fields empty (NULL)<br/>' + + if stats.profiles_error > 0: + msg += f'/<span style="color:red">{stats.profiles_error}E</span>' + ttp += f'<span style="color:red">At least {stats.profiles_error} profiles ' \ + f'can not be converted to X axis unit "{self.plotWidget().xUnit()}" (ERROR)</span><br/>' + + if stats.profiles_plotted >= stats.profiles_plotted_max and stats.profiles_total > stats.profiles_plotted_max: + msg += f'/<span style="color:red">{stats.profiles_plotted}</span>' + ttp += f'<span style="color:red">{stats.profiles_plotted} profiles plotted. Increase plot ' \ + f'limit ({stats.profiles_plotted_max}) to show more at same time.</span><br/>' else: - msg += f'{stats.features_plotted}' - ttp += f'{stats.features_plotted} profiles plotted<br/>' - - if stats.features_with_value_error > 0: - msg = f'/<span style="color:red">{stats.features_with_value_error}</span>' - ttp = f'<br/><span style="color:red">{stats.features_with_value_error} profiles ' \ - f'not convertible to {self.plotWidget().xUnit()}' + msg += f'/{stats.profiles_plotted}' + ttp += f'{stats.profiles_plotted} profiles plotted<br/>' msg += '</body></html>' ttp += '</p></body></html>' diff --git a/eotimeseriesviewer/externals/qps/speclib/io/vectorsources.py b/eotimeseriesviewer/externals/qps/speclib/io/vectorsources.py index 477d944e..7229cb65 100644 --- a/eotimeseriesviewer/externals/qps/speclib/io/vectorsources.py +++ b/eotimeseriesviewer/externals/qps/speclib/io/vectorsources.py @@ -68,15 +68,13 @@ class VectorSourceFieldValueConverter(QgsVectorFileWriter.FieldValueConverter): def convert(self, fieldIndex: int, value: any): if fieldIndex in self.mBLOB2TXT: dataDict = decodeProfileValueDict(value) - json = encodeProfileValueDict(dataDict, mode=SerializationMode.JSON) - return json + dataJSON = json.dumps(dataDict) + return dataJSON return value def fieldDefinition(self, field: QgsField) -> QgsField: if field.name() in self.mBLOB2TXT: - f = QgsField(FIELD_VALUES, QVariant.String, 'varchar', comment=field.comment()) - - return f + return QgsField(field.name(), QVariant.String, 'varchar', comment=field.comment()) return field diff --git a/eotimeseriesviewer/externals/qps/speclib/qgsfunctions.py b/eotimeseriesviewer/externals/qps/speclib/qgsfunctions.py index 7c62e5e9..32d171fb 100644 --- a/eotimeseriesviewer/externals/qps/speclib/qgsfunctions.py +++ b/eotimeseriesviewer/externals/qps/speclib/qgsfunctions.py @@ -23,36 +23,267 @@ along with this software. If not, see <http://www.gnu.org/licenses/>. *************************************************************************** """ -import pickle +import typing +import string +import pathlib +import json +import sys +import os +from qgis.PyQt.QtCore import QCoreApplication +from qgis.core import QgsExpression, QgsFeature, qgsfunction, QgsFeatureRequest, QgsExpressionFunction, \ + QgsMessageLog, Qgis, QgsExpressionContext +from qgis.PyQt.QtCore import QByteArray, QVariant, NULL +from .core import FIELD_VALUES, decodeProfileValueDict, SpectralProfile, encodeProfileValueDict -from qgis.core import * +QGS_FUNCTION_GROUP = "Spectral Libraries" -from .core import FIELD_VALUES, decodeProfileValueDict +QGIS_FUNCTION_INSTANCES = dict() -QGS_FUNCTION_GROUP = "Spectral Libraries" -@qgsfunction(0, QGS_FUNCTION_GROUP) -def spectralValues(values, feature, parent): - """ - Returns the spectral values dictionary - :param values: - :param feature: - :param parent: - :return: dict - """ - if isinstance(feature, QgsFeature): - i = feature.fieldNameIndex(FIELD_VALUES) - if i >= 0: - values = decodeProfileValueDict(feature.attribute(i)) - return values - return None +class HelpStringMaker(object): + + def __init__(self): + + helpDir = pathlib.Path(__file__).parent / 'function_help' + self.mHELP = dict() + + assert helpDir.is_dir() + + for e in os.scandir(helpDir): + if e.is_file() and e.name.endswith('.json'): + with open(e.path, 'r', encoding='utf-8') as f: + data = json.load(f) + if isinstance(data, dict) and 'name' in data.keys(): + self.mHELP[data['name']] = data + + def helpText(self, name: str, + parameters: typing.List[QgsExpressionFunction.Parameter] = []) -> str: + """ + re-implementation of QString QgsExpression::helpText( QString name ) + to generate similar help strings + :param name: + :param args: + :return: + """ + html = [f'<h3>{name}</h3>'] + LUT_PARAMETERS = dict() + for p in parameters: + LUT_PARAMETERS[p.name()] = p + + JSON = self.mHELP.get(name, None) + ARGUMENT_DESCRIPTIONS = {} + ARGUMENT_NAMES = [] + if isinstance(JSON, dict): + for D in JSON.get('arguments', []): + if isinstance(D, dict) and 'arg' in D: + ARGUMENT_NAMES.append(D['arg']) + ARGUMENT_DESCRIPTIONS[D['arg']] = D.get('description', '') + + if not isinstance(JSON, dict): + print(f'No help found for {name}', file=sys.stderr) + return '\n'.join(html) + + description = JSON.get('description', None) + if description: + html.append(f'<div class="description"><p>{description}</p></div>') + + arguments = JSON.get('arguments', None) + if arguments: + hasOptionalArgs: bool = False + html.append(f'<h4>Syntax</h4>') + syntax = f'<div class="syntax">\n<code>{name}(' + + if len(parameters) > 0: + delim = '' + syntaxParameters = set() + for P in parameters: + assert isinstance(P, QgsExpressionFunction.Parameter) + syntaxParameters.add(P.name()) + optional: bool = P.optional() + if optional: + hasOptionalArgs = True + syntax += '[' + syntax += delim + syntax += f'<span class="argument">{P.name()}' + defaultValue = P.defaultValue() + if isinstance(defaultValue, str): + defaultValue = f"'{defaultValue}'" + if defaultValue not in [None, QVariant()]: + syntax += f'={defaultValue}' + + syntax += '</span>' + if optional: + syntax += ']' + delim = ',' + # add other optional arguments from help file + for a in ARGUMENT_NAMES: + if a not in syntaxParameters: + pass + + syntax += ')</code>' + + if hasOptionalArgs: + syntax += '<br/><br/>[ ] marks optional components' + syntax += '</div>' + html.append(syntax) + + if len(parameters) > 0: + html.append(f'<h4>Arguments</h4>') + html.append(f'<div class="arguments"><table>') + + for P in parameters: + assert isinstance(P, QgsExpressionFunction.Parameter) + + description = ARGUMENT_DESCRIPTIONS.get(P.name(), '') + html.append(f'<tr><td class="argument">{P.name()}</td><td>{description}</td></tr>') + + html.append(f'</table></div>') + + return '\n'.join(html) + + +HM = HelpStringMaker() + +""" +@qgsfunction(args='auto', group='String') +def format_py(fmt: str, *args): + assert isinstance(fmt, str) + fmtArgs = args[0:-2] + feature, parent = args[-2:] + + return fmt.format(*fmtArgs) +""" +class Format_Py(QgsExpressionFunction): + + def __init__(self): + group = 'String' + name = 'format_py' + + args = [ + QgsExpressionFunction.Parameter('fmt', optional=False, defaultValue=FIELD_VALUES), + QgsExpressionFunction.Parameter('arg1', optional=True), + QgsExpressionFunction.Parameter('arg2', optional=True), + QgsExpressionFunction.Parameter('argN', optional=True), + ] + helptext = HM.helpText(name, args) + super(Format_Py, self).__init__(name, -1, group, helptext) + + def func(self, values, context: QgsExpressionContext, parent, node): + if len(values) == 0 or values[0] in (None, NULL): + return None + assert isinstance(values[0], str) + fmt: str = values[0] + fmtArgs = values[1:] + try: + return fmt.format(*fmtArgs) + except: + return None + + def usesGeometry(self, node) -> bool: + return False + + def referencedColumns(self, node) -> typing.List[str]: + return [QgsFeatureRequest.ALL_ATTRIBUTES] + + def handlesNull(self) -> bool: + return True + + +class SpectralData(QgsExpressionFunction): + def __init__(self): + group = QGS_FUNCTION_GROUP + name = 'spectralData' + + args = [ + QgsExpressionFunction.Parameter('field', optional=True, defaultValue=FIELD_VALUES) + ] + + helptext = HM.helpText(name, args) + super().__init__(name, args, group, helptext) + + def func(self, values, context: QgsExpressionContext, parent, node): + + value_field = values[0] + feature = None + if context: + feature = context.feature() + if not isinstance(feature, QgsFeature): + return None + try: + profile = SpectralProfile.fromQgsFeature(feature, value_field=value_field) + assert isinstance(profile, SpectralProfile) + return profile.values() + except Exception as ex: + parent.setEvalErrorString(str(ex)) + return None + + def usesGeometry(self, node) -> bool: + return False + + def referencedColumns(self, node) -> typing.List[str]: + return [QgsFeatureRequest.ALL_ATTRIBUTES] + + def handlesNull(self) -> bool: + return False + + +class SpectralMath(QgsExpressionFunction): + + def __init__(self): + group = QGS_FUNCTION_GROUP + name = 'spectralMath' + + args = [ + QgsExpressionFunction.Parameter('expression', optional=False, isSubExpression=True), + QgsExpressionFunction.Parameter('field', optional=True, defaultValue=FIELD_VALUES) + ] + helptext = HM.helpText(name, args) + super().__init__(name, args, group, helptext) + + def func(self, values, context: QgsExpressionContext, parent, node): + + expression, value_field = values + feature = None + if context: + feature = context.feature() + if not isinstance(feature, QgsFeature): + return None + try: + profile = SpectralProfile.fromQgsFeature(feature, value_field=value_field) + assert isinstance(profile, SpectralProfile) + values = profile.values() + exec(expression, values) + + newProfile = SpectralProfile(values=values) + return newProfile.attribute(profile.mValueField) + except Exception as ex: + parent.setEvalErrorString(str(ex)) + return None + + def usesGeometry(self, node) -> bool: + return True + + def referencedColumns(self, node) -> typing.List[str]: + return [QgsFeatureRequest.ALL_ATTRIBUTES] + + def handlesNull(self) -> bool: + return True def registerQgsExpressionFunctions(): """ Registers functions to support SpectraLibrary handling with QgsExpressions """ - #QgsExpression.registerFunction(plotStyleSymbolFillColor) - #QgsExpression.registerFunction(plotStyleSymbol) - #QgsExpression.registerFunction(plotStyleSymbolSize) - QgsExpression.registerFunction(spectralValues) + global QGIS_FUNCTION_INSTANCES + for func in [Format_Py(), SpectralMath(), SpectralData()]: + QGIS_FUNCTION_INSTANCES[func.name()] = func + if QgsExpression.isFunctionName(func.name()): + if not QgsExpression.unregisterFunction(func.name): + msgtitle = QCoreApplication.translate("UserExpressions", "User expressions") + msg = QCoreApplication.translate("UserExpressions", + "The user expression {0} already exists and could not be unregistered.").format( + func.name) + QgsMessageLog.logMessage(msg + "\n", msgtitle, Qgis.Warning) + return None + else: + QgsExpression.registerFunction(func) diff --git a/eotimeseriesviewer/externals/qps/speclib/spectralprofileeditorconfigwidget.ui b/eotimeseriesviewer/externals/qps/speclib/spectralprofileeditorconfigwidget.ui index 651c2f9a..3d572d20 100644 --- a/eotimeseriesviewer/externals/qps/speclib/spectralprofileeditorconfigwidget.ui +++ b/eotimeseriesviewer/externals/qps/speclib/spectralprofileeditorconfigwidget.ui @@ -6,74 +6,43 @@ <rect> <x>0</x> <y>0</y> - <width>408</width> - <height>245</height> + <width>326</width> + <height>396</height> </rect> </property> <property name="windowTitle"> <string>Form</string> </property> <layout class="QFormLayout" name="formLayout"> - <item row="1" column="1"> - <widget class="QPlainTextEdit" name="tbYUnits"/> - </item> - <item row="3" column="1"> - <widget class="QPlainTextEdit" name="tbXUnits"/> - </item> - <item row="0" column="1"> + <item row="2" column="0"> <widget class="QLabel" name="label"> <property name="text"> - <string>Units Y Axis (spectral value units)</string> + <string>Name</string> </property> </widget> </item> - <item row="2" column="1"> + <item row="3" column="0"> <widget class="QLabel" name="label_2"> <property name="text"> - <string>Units X Axis (band or wavelength units)</string> + <string>Source</string> </property> </widget> </item> - <item row="1" column="0"> - <widget class="QToolButton" name="tbResetY"> - <property name="text"> - <string>...</string> - </property> - </widget> + <item row="3" column="1"> + <widget class="QgsFieldExpressionWidget" name="mFieldExpressionSource"/> </item> - <item row="3" column="0"> - <widget class="QToolButton" name="tbResetX"> - <property name="text"> - <string>...</string> - </property> - </widget> + <item row="2" column="1"> + <widget class="QgsFieldExpressionWidget" name="mFieldExpressionName"/> </item> </layout> - <action name="actionResetY"> - <property name="icon"> - <iconset> - <normaloff>:/images/themes/default/mActionUndo.svg</normaloff>:/images/themes/default/mActionUndo.svg</iconset> - </property> - <property name="text"> - <string>Reset</string> - </property> - <property name="toolTip"> - <string>Reset to previous values</string> - </property> - </action> - <action name="actionResetX"> - <property name="icon"> - <iconset> - <normaloff>:/images/themes/default/mActionUndo.svg</normaloff>:/images/themes/default/mActionUndo.svg</iconset> - </property> - <property name="text"> - <string>Reset</string> - </property> - <property name="toolTip"> - <string>Reset to previous values</string> - </property> - </action> </widget> + <customwidgets> + <customwidget> + <class>QgsFieldExpressionWidget</class> + <extends>QWidget</extends> + <header>qgsfieldexpressionwidget.h</header> + </customwidget> + </customwidgets> <resources/> <connections/> </ui> diff --git a/eotimeseriesviewer/externals/qps/speclib/spectralprofileeditorwidget.ui b/eotimeseriesviewer/externals/qps/speclib/spectralprofileeditorwidget.ui index 5503b9b3..81ac003f 100644 --- a/eotimeseriesviewer/externals/qps/speclib/spectralprofileeditorwidget.ui +++ b/eotimeseriesviewer/externals/qps/speclib/spectralprofileeditorwidget.ui @@ -6,15 +6,30 @@ <rect> <x>0</x> <y>0</y> - <width>383</width> - <height>329</height> + <width>322</width> + <height>408</height> </rect> </property> <property name="windowTitle"> <string>Form</string> </property> - <layout class="QGridLayout" name="gridLayout_2" columnstretch="0,0,0"> - <item row="5" column="0" colspan="3"> + <layout class="QGridLayout" name="gridLayout"> + <property name="leftMargin"> + <number>2</number> + </property> + <property name="topMargin"> + <number>2</number> + </property> + <property name="rightMargin"> + <number>2</number> + </property> + <property name="bottomMargin"> + <number>2</number> + </property> + <property name="spacing"> + <number>2</number> + </property> + <item row="3" column="0"> <widget class="QTableView" name="tableView"> <property name="sizePolicy"> <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> @@ -28,96 +43,175 @@ <height>150</height> </size> </property> - </widget> - </item> - <item row="2" column="1"> - <widget class="QComboBox" name="cbYUnit"> - <property name="editable"> - <bool>false</bool> + <property name="styleSheet"> + <string notr="true">QTableView{ +margin:0; +padding:1px; +}</string> </property> + <attribute name="verticalHeaderMinimumSectionSize"> + <number>10</number> + </attribute> + <attribute name="verticalHeaderDefaultSectionSize"> + <number>10</number> + </attribute> </widget> </item> <item row="2" column="0"> - <widget class="QLabel" name="label_2"> - <property name="text"> - <string>Y (band value)</string> + <layout class="QFormLayout" name="formLayout"> + <property name="horizontalSpacing"> + <number>2</number> </property> - </widget> - </item> - <item row="0" column="0"> - <widget class="QToolButton" name="btnReset"> - <property name="text"> - <string>...</string> + <property name="verticalSpacing"> + <number>2</number> </property> - <property name="icon"> - <iconset> - <normaloff>:/qps/ui/icons/undo_orange.svg</normaloff>:/qps/ui/icons/undo_orange.svg</iconset> + <property name="topMargin"> + <number>0</number> </property> - </widget> - </item> - <item row="2" column="2"> - <widget class="QComboBox" name="cbYUnitDataType"> - <item> - <property name="text"> - <string>Integer</string> - </property> + <item row="0" column="0"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Bands</string> + </property> + </widget> </item> - <item> - <property name="text"> - <string>Float</string> - </property> + <item row="1" column="0"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Units</string> + </property> + </widget> </item> - </widget> - </item> - <item row="0" column="2"> - <widget class="QLabel" name="label_4"> - <property name="text"> - <string>Data Type</string> - </property> - </widget> - </item> - <item row="0" column="1"> - <widget class="QLabel" name="label_3"> - <property name="text"> - <string>Unit</string> - </property> - </widget> - </item> - <item row="3" column="0"> - <widget class="QLabel" name="label"> - <property name="text"> - <string>X (wavelength)</string> - </property> - </widget> - </item> - <item row="3" column="2"> - <widget class="QComboBox" name="cbXUnitDataType"> - <item> - <property name="text"> - <string>Integer</string> - </property> + <item row="0" column="1"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="spacing"> + <number>2</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <item> + <widget class="QgsSpinBox" name="sbBands"> + <property name="minimumSize"> + <size> + <width>100</width> + <height>0</height> + </size> + </property> + <property name="minimum"> + <number>0</number> + </property> + <property name="maximum"> + <number>10000</number> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>0</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QToolButton" name="btnReset"> + <property name="text"> + <string>...</string> + </property> + <property name="icon"> + <iconset resource="../qpsresources.qrc"> + <normaloff>:/qps/ui/icons/undo_orange.svg</normaloff>:/qps/ui/icons/undo_orange.svg</iconset> + </property> + </widget> + </item> + </layout> </item> - <item> - <property name="text"> - <string>Float</string> - </property> + <item row="1" column="1"> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <property name="topMargin"> + <number>0</number> + </property> + <item> + <widget class="QLabel" name="label_4"> + <property name="text"> + <string>X</string> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="cbXUnit"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="editable"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Y</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="tbYUnit"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>0</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> </item> - </widget> - </item> - <item row="3" column="1"> - <widget class="QComboBox" name="cbXUnit"> - <property name="editable"> - <bool>false</bool> - </property> - </widget> + </layout> </item> </layout> <action name="actionReset"> + <property name="icon"> + <iconset resource="../../../../QGIS/images/images.qrc"> + <normaloff>:/images/themes/default/mActionReload.svg</normaloff>:/images/themes/default/mActionReload.svg</iconset> + </property> <property name="text"> <string>Reset</string> </property> </action> </widget> - <resources/> + <customwidgets> + <customwidget> + <class>QgsSpinBox</class> + <extends>QSpinBox</extends> + <header>qgsspinbox.h</header> + </customwidget> + </customwidgets> + <resources> + <include location="../qpsresources.qrc"/> + <include location="../../../../QGIS/images/images.qrc"/> + </resources> <connections/> </ui> diff --git a/eotimeseriesviewer/externals/qps/subdatasets.py b/eotimeseriesviewer/externals/qps/subdatasets.py index 36e904b3..e73cd592 100644 --- a/eotimeseriesviewer/externals/qps/subdatasets.py +++ b/eotimeseriesviewer/externals/qps/subdatasets.py @@ -31,7 +31,7 @@ import sys import sip import pathlib import collections -from qgis.core import * +from qgis.core import QgsMapLayer, QgsRasterLayer, QgsTaskManager, QgsApplication, QgsTask from qgis.gui import * from qgis.PyQt.QtCore import * from qgis.PyQt.QtWidgets import * @@ -39,6 +39,7 @@ from osgeo import gdal from .utils import loadUi from . import DIR_UI_FILES + # read https://gdal.org/user/raster_data_model.html#subdatasets-domain class SubDatasetType(object): @@ -55,6 +56,7 @@ class SubDatasetType(object): return False return self.name == other.name + class DatasetInfo(object): @staticmethod @@ -122,16 +124,16 @@ class DatasetInfo(object): return False return self.mReferenceFile == other.mReferenceFile -class SubDatasetLoadingTask(QgsTask): +class SubDatasetLoadingTask(QgsTask): sigFoundSubDataSets = pyqtSignal(list) sigMessage = pyqtSignal(str, bool) def __init__(self, files: typing.List[str], description: str = "Collect subdata sets", - callback = None, - block_size : int = 10): + callback=None, + block_size: int = 10): super().__init__(description=description) self.mFiles = files @@ -159,7 +161,7 @@ class SubDatasetLoadingTask(QgsTask): result_block.clear() if self.isCanceled(): return False - self.setProgress(100 * (i+1) / n) + self.setProgress(100 * (i + 1) / n) if len(result_block) > 0: self.sigFoundSubDataSets.emit(result_block[:]) @@ -170,6 +172,7 @@ class SubDatasetLoadingTask(QgsTask): if self.mCallback is not None: self.mCallback(result, self) + class SubDatasetDescriptionModel(QAbstractTableModel): def __init__(self, *args, **kwds): @@ -264,6 +267,7 @@ class SubDatasetDescriptionModel(QAbstractTableModel): subs = [s for s in subs if s.checked == checked] return subs + class DatasetTableModel(QAbstractTableModel): def __init__(self, *args, **kwds): @@ -416,7 +420,7 @@ class SubDatasetSelectionDialog(QDialog): [descriptions.extend(i.subdataset_types()) for i in infos] self.subDatasetModel.addSubDatasetDescriptions(descriptions) - def startTask(self, qgsTask:QgsTask): + def startTask(self, qgsTask: QgsTask): self.setCursor(Qt.WaitCursor) self.fileWidget.setEnabled(False) self.fileWidget.lineEdit().setShowSpinner(True) @@ -440,7 +444,7 @@ class SubDatasetSelectionDialog(QDialog): if isinstance(task, SubDatasetLoadingTask) and not sip.isdeleted(task): self.onRemoveTask(id(task)) - def onTaskMessage(self, msg: str, is_error:bool): + def onTaskMessage(self, msg: str, is_error: bool): if is_error: print(msg, file=sys.stderr) self.setInfo(msg) @@ -475,4 +479,3 @@ class SubDatasetSelectionDialog(QDialog): :param filter: """ self.fileWidget.setFilter(filter) - diff --git a/eotimeseriesviewer/externals/qps/testing.py b/eotimeseriesviewer/externals/qps/testing.py index 6b645d4e..406c429c 100644 --- a/eotimeseriesviewer/externals/qps/testing.py +++ b/eotimeseriesviewer/externals/qps/testing.py @@ -326,19 +326,22 @@ class QgisMockup(QgisInterface): m = self.ui.menuBar().addAction('Add Vector') m = self.ui.menuBar().addAction('Add Raster') - def mapCanvas(self): + def mapCanvas(self) -> QgsMapCanvas: return self.mCanvas + def mapCanvases(self) -> typing.List[QgsMapCanvas]: + return [self.mCanvas] + def mapNavToolToolBar(self) -> QToolBar: return self.mMapNavToolBar - def messageBar(self, *args, **kwargs): + def messageBar(self, *args, **kwargs) -> QgsMessageBar: return self.mMessageBar - def rasterMenu(self): + def rasterMenu(self) -> QMenu: return self.mRasterMenu - def vectorMenu(self): + def vectorMenu(self) -> QMenu: return self.mVectorMenu def viewMenu(self) -> QMenu: diff --git a/eotimeseriesviewer/externals/qps/ui/cursorlocationinfodock.ui b/eotimeseriesviewer/externals/qps/ui/cursorlocationinfodock.ui index 40e74d00..6eb782d3 100644 --- a/eotimeseriesviewer/externals/qps/ui/cursorlocationinfodock.ui +++ b/eotimeseriesviewer/externals/qps/ui/cursorlocationinfodock.ui @@ -82,7 +82,7 @@ </layout> </item> <item> - <widget class="TreeView" name="treeView"/> + <widget class="CursorLocationInfoTreeView" name="mTreeView"/> </item> <item> <layout class="QHBoxLayout" name="horizontalLayout_4"> @@ -284,9 +284,9 @@ <container>1</container> </customwidget> <customwidget> - <class>TreeView</class> + <class>CursorLocationInfoTreeView</class> <extends>QTreeView</extends> - <header>qps.models</header> + <header>qps.cursorlocationvalue</header> </customwidget> </customwidgets> <resources> diff --git a/eotimeseriesviewer/externals/qps/ui/gdalmetadatamodelwidget.ui b/eotimeseriesviewer/externals/qps/ui/gdalmetadatamodelwidget.ui index dbc954b7..e3da6e9d 100644 --- a/eotimeseriesviewer/externals/qps/ui/gdalmetadatamodelwidget.ui +++ b/eotimeseriesviewer/externals/qps/ui/gdalmetadatamodelwidget.ui @@ -17,7 +17,7 @@ <iconset resource="../qpsresources.qrc"> <normaloff>:/qps/ui/icons/rasterband_select.svg</normaloff>:/qps/ui/icons/rasterband_select.svg</iconset> </property> - <layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,0,0"> + <layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,0,0,0"> <property name="spacing"> <number>2</number> </property> @@ -33,6 +33,16 @@ <property name="bottomMargin"> <number>2</number> </property> + <item> + <widget class="QgsMessageBar" name="mMessageBar"> + <property name="frameShape"> + <enum>QFrame::StyledPanel</enum> + </property> + <property name="frameShadow"> + <enum>QFrame::Raised</enum> + </property> + </widget> + </item> <item> <widget class="QgsCollapsibleGroupBox" name="gbBandNames"> <property name="sizePolicy"> @@ -231,7 +241,7 @@ <string>...</string> </property> <property name="icon"> - <iconset resource="../../../QGIS/images/images.qrc"> + <iconset> <normaloff>:/images/themes/default/symbologyAdd.svg</normaloff>:/images/themes/default/symbologyAdd.svg</iconset> </property> <property name="autoRaise"> @@ -245,7 +255,7 @@ <string>...</string> </property> <property name="icon"> - <iconset resource="../../../QGIS/images/images.qrc"> + <iconset> <normaloff>:/images/themes/default/symbologyRemove.svg</normaloff>:/images/themes/default/symbologyRemove.svg</iconset> </property> <property name="autoRaise"> @@ -325,7 +335,7 @@ </action> <action name="actionAddItem"> <property name="icon"> - <iconset resource="../../../QGIS/images/images.qrc"> + <iconset> <normaloff>:/images/themes/default/symbologyAdd.svg</normaloff>:/images/themes/default/symbologyAdd.svg</iconset> </property> <property name="text"> @@ -337,7 +347,7 @@ </action> <action name="actionRemoveItem"> <property name="icon"> - <iconset resource="../../../QGIS/images/images.qrc"> + <iconset> <normaloff>:/images/themes/default/symbologyRemove.svg</normaloff>:/images/themes/default/symbologyRemove.svg</iconset> </property> <property name="text"> @@ -349,7 +359,7 @@ </action> <action name="actionReset"> <property name="icon"> - <iconset resource="../../../QGIS/images/images.qrc"> + <iconset> <normaloff>:/images/themes/default/mIconClearText.svg</normaloff>:/images/themes/default/mIconClearText.svg</iconset> </property> <property name="text"> @@ -385,6 +395,12 @@ <extends>QTableView</extends> <header>qps.layerconfigwidgets.gdalmetadata</header> </customwidget> + <customwidget> + <class>QgsMessageBar</class> + <extends>QFrame</extends> + <header>qgis.gui</header> + <container>1</container> + </customwidget> </customwidgets> <resources> <include location="../qpsresources.qrc"/> diff --git a/eotimeseriesviewer/externals/qps/unitmodel.py b/eotimeseriesviewer/externals/qps/unitmodel.py index 6f7fa836..6f181ef9 100644 --- a/eotimeseriesviewer/externals/qps/unitmodel.py +++ b/eotimeseriesviewer/externals/qps/unitmodel.py @@ -191,3 +191,7 @@ class XUnitModel(UnitModel): self.addUnit('DecimalYear', description='Date [Decimal Year]') self.addUnit('DOY', description='Day of Year [DOY]') + def findUnit(self, unit): + if unit in [None, NULL]: + unit = BAND_INDEX + return super(XUnitModel, self).findUnit(unit) \ No newline at end of file diff --git a/eotimeseriesviewer/externals/qps/utils.py b/eotimeseriesviewer/externals/qps/utils.py index 57eda376..e0e99518 100644 --- a/eotimeseriesviewer/externals/qps/utils.py +++ b/eotimeseriesviewer/externals/qps/utils.py @@ -59,6 +59,11 @@ from qgis.PyQt.QtWidgets import * from osgeo import gdal, ogr, osr, gdal_array import numpy as np from qgis.PyQt.QtWidgets import QAction, QMenu, QToolButton, QDialogButtonBox, QLabel, QGridLayout, QMainWindow + +try: + from .. import qps +except: + import qps from . import DIR_UI_FILES # dictionary to store form classes and avoid multiple calls to read <myui>.i @@ -66,11 +71,6 @@ QGIS_RESOURCE_WARNINGS = set() REMOVE_setShortcutVisibleInContextMenu = hasattr(QAction, 'setShortcutVisibleInContextMenu') -try: - from .. import qps -except: - import qps - jp = os.path.join dn = os.path.dirname @@ -577,6 +577,8 @@ def createQgsField(name: str, exampleValue: typing.Any, comment: str = None) -> return QgsField(name, QVariant.String, 'varchar', comment=comment) elif isinstance(exampleValue, np.datetime64): return QgsField(name, QVariant.String, 'varchar', comment=comment) + elif isinstance(exampleValue, (bytes, QByteArray)): + return QgsField(name, QVariant.ByteArray, 'Binary', comment=comment) elif isinstance(exampleValue, list): assert len(exampleValue) > 0, 'need at least one value in provided list' v = exampleValue[0] @@ -682,7 +684,7 @@ def gdalDataset(dataset: typing.Union[str, QgsRasterLayer, QgsRasterDataProvider, gdal.Dataset], - eAccess:int = gdal.GA_ReadOnly) -> gdal.Dataset: + eAccess: int = gdal.GA_ReadOnly) -> gdal.Dataset: """ Returns a gdal.Dataset object instance :param dataset: @@ -899,6 +901,24 @@ def qgsRasterLayer(source) -> QgsRasterLayer: raise Exception('Unable to transform {} into QgsRasterLayer'.format(source)) +def qgsRasterLayers(sources) -> typing.Iterator[QgsRasterLayer]: + """ + Like qgsRasterLayer, but on multiple inputs and with extraction of sub-layers + :param sources: + :return: + """ + if not isinstance(sources, list): + sources = [sources] + assert isinstance(sources, list) + + for source in sources: + lyr: QgsRasterLayer = qgsRasterLayer(source) + if lyr.isValid(): + yield lyr + for lyr in qgsRasterLayers(lyr.subLayers()): + yield lyr + + def qgsMapLayer(value: typing.Any) -> QgsMapLayer: """ Tries to convert the input into a QgsMapLayer @@ -1549,19 +1569,14 @@ def parseWavelength(dataset) -> typing.Tuple[np.ndarray, str]: def checkWavelengthUnit(key: str, value: str) -> str: wlu = None value = value.strip() - if re.search(r'wavelength.units?', key, re.I): + if re.search(r'wavelength[ _]?units?', key, re.I): # metric length units - if re.search(r'^(Micrometers?|um|μm)$', values, re.I): - wlu = 'μm' # fix with python 3 UTF - elif re.search(r'^(Nanometers?|nm)$', values, re.I): - wlu = 'nm' - elif re.search(r'^(Millimeters?|mm)$', values, re.I): - wlu = 'nm' - elif re.search(r'^(Centimeters?|cm)$', values, re.I): - wlu = 'nm' - elif re.search(r'^(Meters?|m)$', values, re.I): - wlu = 'nm' - elif re.search(r'^Wavenumber$', values, re.I): + wlu = UnitLookup.baseUnit(value) + + if wlu is not None: + return wlu + + if re.search(r'^Wavenumber$', values, re.I): wlu = '-' elif re.search(r'^GHz$', values, re.I): wlu = 'GHz' diff --git a/eotimeseriesviewer/externals/qps/vectorlayertools.py b/eotimeseriesviewer/externals/qps/vectorlayertools.py index f21ed959..0ba69a0d 100644 --- a/eotimeseriesviewer/externals/qps/vectorlayertools.py +++ b/eotimeseriesviewer/externals/qps/vectorlayertools.py @@ -107,10 +107,6 @@ class VectorLayerTools(QgsVectorLayerTools): and isinstance(qgis.utils.iface, QgisInterface): qgis.utils.iface.pasteFromClipboard(layer) - def deleteSelected(self, layer: QgsVectorLayer): - if isinstance(layer, QgsVectorLayer) and layer.isEditable(): - layer.deleteSelectedFeatures() - def invertSelection(self, layer: QgsVectorLayer): if isinstance(layer, QgsVectorLayer): layer.invertSelection() @@ -125,7 +121,7 @@ class VectorLayerTools(QgsVectorLayerTools): def deleteSelection(self, layer: QgsVectorLayer): if isinstance(layer, QgsVectorLayer) and layer.isEditable(): - layer + layer.deleteSelectedFeatures() def toggleEditing(self, vlayer: QgsVectorLayer, allowCancel: bool = True) -> bool: """ -- GitLab