Skip to content
Snippets Groups Projects
classificationscheme.py 58.2 KiB
Newer Older
Benjamin Jakimow's avatar
Benjamin Jakimow committed
                if layer.dataProvider().name() == 'gdal':
                    scheme = ClassificationScheme.fromRasterImage(layer.source())


        if isinstance(layer, QgsVectorLayer):
            scheme = ClassificationScheme.fromFeatureRenderer(layer.renderer())

        return scheme

    @staticmethod
    def fromRasterBand(band: gdal.Band):
        """
        Reads the ClassificationScheme of a gdal.Band
        :param band: gdal.Band
        :return: ClassificationScheme, None if classes are undefined.
        """
        assert isinstance(band, gdal.Band)
        cat = band.GetCategoryNames()
        ct = band.GetColorTable()
        if cat is None or len(cat) == 0:
            return None
        scheme = ClassificationScheme()
        classes = []
        for i, catName in enumerate(cat):
            cli = ClassInfo(name=catName, label=i)
            if ct is not None:
                cli.setColor(QColor(*ct.GetColorEntry(i)))
            classes.append(cli)
        scheme.insertClasses(classes)
        return scheme

    @staticmethod
    def fromRasterImage(path, bandIndex=None):
        """
        Reads a ClassificationScheme from a gdal.Dataset
        :param path: str with path to gdal.Dataset or gdal.Dataset instances
        :param bandIndex: int with band index
        :return: ClassificationScheme
        """
        ds = gdalDataset(path)
        assert ds is not None

        if bandIndex is None:
            for b in range(ds.RasterCount):
                band = ds.GetRasterBand(b + 1)
                cat = band.GetCategoryNames()

                if cat != None:
                    bandIndex = b
                    break
                s = ""
            if bandIndex is None:
                return None

        assert bandIndex >= 0 and bandIndex < ds.RasterCount
        band = ds.GetRasterBand(bandIndex + 1)
        return ClassificationScheme.fromRasterBand(band)

    @staticmethod
    def fromCsv(pathCSV:str, mode:str=None):
        """
        Read the ClassificationScheme from a CSV table
        :param path: str, path of CSV file
        :return: ClassificationScheme
        """
        text = None
        with open(pathCSV) as f:
            text = f.read()
        if not isinstance(text, str):
            raise Exception('Unable to read {}'.format(pathCSV))

        lines = text.splitlines()
        lines = [l.strip() for l in lines]
        lines = [l for l in lines if len(l) > 0]
        if len(lines) <= 1:
            raise Exception('CSV does not contain enough values')

        match = re.search(r'ClassificationScheme\("(.*)"\)', text)
        if match:
            name = re.search(r'ClassificationScheme\("(.*)"\)', text).group(1)
        else:
            name = 'Classification'

        b = False
        columnNames = None
        delimiter = ';'
        for i, line in enumerate(lines):
            match = re.search(r'^[ ]*(?P<label>label)[ ]*[;\t,][ ]*(?P<name>name)[ ]*([;\t,][ ]*(?P<color>color))?',
                              line, re.IGNORECASE)
            if match:
                delimiter = re.search(r'[;\t,]', line).group()
                b = True
                break

        if not match:
            raise Exception('Missing column header "label;name:color"')

        cName = match.group('name')
        cColor = match.group('color')
        fieldnames = [match.group('label'), match.group('name'), match.group('color')]

        cs = ClassificationScheme()
        cs.setName(name)
        # read CSV data
        reader = csv.DictReader(lines[i:], delimiter=delimiter)

        iName = None
        iColor = None
        for i, name in enumerate(reader.fieldnames):
            if iName is None and re.search(r'name', name, re.I):
                iName = i
            if iColor is None and re.search(r'color', name, re.I):
                iColor = i
        rows = [row for row in reader]

        nc = len(rows)
        if nc == 0:
            return None

        cs = ClassificationScheme.create(nc)
        for i, row in enumerate(rows):
            c = cs[i]
            assert isinstance(c, ClassInfo)
            if iName is not None:
                c.setName(row[fieldnames[iName]])
            if iColor is not None:
                colorValue = row[fieldnames[iColor]].strip()

                match = re.search(r'^(?P<R>\d+),(?P<G>\d+),(?P<B>\d+)(,(?P<A>\d+))?$', colorValue)
                if match:
                    R = int(match.group('R'))
                    G = int(match.group('G'))
                    B = int(match.group('B'))
                    A = match.group('B')
                    if A:
                        A = int(A)
                    c.setColor(QColor(R,G,B,A))
                else:
                    c.setColor(QColor(colorValue))

        return cs

    def saveToQml(self, path):
        """
        Saves the class infos into a QML file
        :param path: str, path of QML file
        """
        raise NotImplementedError()

    @staticmethod
    def fromQml(path:str):
        """
        Reads a ClassificationScheme from a QML file.
        :param path: str, path to QML file
        :return: ClassificationScheme
        """
        raise NotImplementedError()


Benjamin Jakimow's avatar
Benjamin Jakimow committed
class ClassificationSchemeComboBoxModel(QAbstractListModel):
Benjamin Jakimow's avatar
Benjamin Jakimow committed
    def __init__(self):
        super(ClassificationSchemeComboBoxModel, self).__init__()

        self.mClassScheme = None
        self.mAllowEmptyField = False
Benjamin Jakimow's avatar
Benjamin Jakimow committed
    def setAllowEmptyField(self, b:bool):
        assert isinstance(b, bool)
        changed = self.mAllowEmptyField != b
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        if changed:
            if b:
                self.beginInsertRows(QModelIndex(), 0, 0)
                self.mAllowEmptyField = b
                self.endInsertRows()
            else:
                self.beginRemoveRows(QModelIndex(), 0, 0)
                self.mAllowEmptyField = b
                self.endRemoveRows()
Benjamin Jakimow's avatar
Benjamin Jakimow committed
    def allowEmptyField(self)->bool:
        return self.mAllowEmptyField
Benjamin Jakimow's avatar
Benjamin Jakimow committed
    def setClassificationScheme(self, classScheme:ClassificationScheme):
        assert isinstance(classScheme, ClassificationScheme)
        self.beginResetModel()
        self.mClassScheme = classScheme
        self.endResetModel()
Benjamin Jakimow's avatar
Benjamin Jakimow committed
    def classificationScheme(self)->ClassificationScheme:
        return self.mClassScheme

    def rowCount(self, parent)->int:
        if not isinstance(self.mClassScheme, ClassificationScheme):
            return 0

        n = len(self.mClassScheme)
        if self.allowEmptyField():
            n += 1
        return n

    def columnCount(self, parent: QModelIndex):
        return 1

    def idx2csIdx(self, index:QModelIndex):

        if not isinstance(self.mClassScheme, ClassificationScheme):
            return QModelIndex()

        if self.allowEmptyField():
            return self.mClassScheme.createIndex(index.row() - 1, index.column())
        else:
            return self.mClassScheme.createIndex(index.row(), index.column())
Benjamin Jakimow's avatar
Benjamin Jakimow committed
    def data(self, index: QModelIndex, role: int = Qt.DisplayRole):

        if not index.isValid():
            return None

        if self.allowEmptyField() and index.row() == 0:
            if role == Qt.DisplayRole:
                return ''

        idxCs = self.idx2csIdx(index)
        if not idxCs.isValid():
            return None
        else:

            classInfo = self.mClassScheme.data(idxCs, role=Qt.UserRole)
            assert isinstance(classInfo, ClassInfo)
            if role == Qt.UserRole:
                return classInfo
            assert isinstance(classInfo, ClassInfo)
            nCols = self.mClassScheme.columnCount(idxCs)
            if role in [Qt.DisplayRole, Qt.ToolTipRole, Qt.WhatsThisRole]:
                infos = []
                for col in range(nCols):
                    idx = self.mClassScheme.createIndex(idxCs.row(), col)
                    infos.append(str(self.mClassScheme.data(idx, role=role)))
                if role == Qt.DisplayRole:
                    return ' '.join(infos[0:2])
                if role == Qt.ToolTipRole:
                    return '\n'.join(infos)
                if role == Qt.WhatsThisRole:
                    return '\n'.join(infos)

            elif role == Qt.DecorationRole:
                return classInfo.icon()


        return None

    def flags(self, index: QModelIndex):
        if not index.isValid():
            return None
        return Qt.ItemIsEnabled | Qt.ItemIsSelectable


class ClassificationSourceComboBox(QgsMapLayerComboBox):
Benjamin Jakimow's avatar
Benjamin Jakimow committed
    def __init__(self, parent):
        super(ClassificationSourceComboBox, self).__init__(parent)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
    def addClassificationSource(self, source):
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        if hasClassification(source):
            pass

    def currentClassificationScheme(self)->ClassificationScheme:
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        raise NotImplementedError()
Benjamin Jakimow's avatar
Benjamin Jakimow committed
    def currentClassificationSource(self)->str:

        raise NotImplementedError()
Benjamin Jakimow's avatar
Benjamin Jakimow committed

class ClassificationSchemeComboBox(QComboBox):

    def __init__(self, parent=None, classification:ClassificationScheme=None):
        super(ClassificationSchemeComboBox, self).__init__(parent)
        if not isinstance(classification, ClassificationScheme):
            classification = ClassificationScheme()
        self.view().setMinimumWidth(200)
        model = ClassificationSchemeComboBoxModel()
        model.setClassificationScheme(classification)
        self.setModel(model)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
    def classIndexFromValue(self, value)->int:
        """
        Returns the index
        :param value:
        :return:
        """
        cs = self.classificationScheme()
        i = cs.classIndexFromValue(value)

        if isinstance(self.mModel, ClassificationSchemeComboBoxModel) and self.mModel.allowEmptyField():
            i += 1
        return i
Benjamin Jakimow's avatar
Benjamin Jakimow committed
    def setModel(self, model: ClassificationSchemeComboBoxModel):
        """
        Sets the combobox model. Must be of type `ClassificationSchemeComboBoxModel`.
        :param model: ClassificationSchemeComboBoxModel
        """
        assert isinstance(model, ClassificationSchemeComboBoxModel)
        super(ClassificationSchemeComboBox, self).setModel(model)
        self.mModel = model

    def classificationScheme(self)->ClassificationScheme:
        """
        Returns the ClassificationScheme
        :return: ClassificationScheme
        """
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        return self.mModel.classificationScheme()
Benjamin Jakimow's avatar
Benjamin Jakimow committed
    def setClassificationScheme(self, classificationScheme:ClassificationScheme):
        """
        Specifies the ClassificationScheme which is represented by this mode.
        :param classificationScheme: ClassificationScheme
        """
        self.mModel.setClassificationScheme(classificationScheme)

    def currentClassInfo(self)->ClassInfo:
        """
        Returns the currently selected ClassInfo
        :return: ClassInfo
        """
        i = self.currentIndex()
        classInfo = None
        if i >= 0 and i < self.count():
            classInfo = self.itemData(i, role=Qt.UserRole)
        return classInfo

class ClassificationSchemeWidget(QWidget, loadClassificationUI('classificationscheme.ui')):

    sigValuesChanged = pyqtSignal()

    def __init__(self, parent=None, classificationScheme=None):
        super(ClassificationSchemeWidget, self).__init__(parent)
        self.setupUi(self)

        self.mScheme = ClassificationScheme()
        if classificationScheme is not None:
            self.setClassificationScheme(classificationScheme)




        assert isinstance(self.tableClassificationScheme, QTableView)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        #self.tableClassificationScheme.horizontalHeader().setResizeMode(QHeaderView.ResizeToContents)
        self.tableClassificationScheme.setModel(self.mScheme)
        self.tableClassificationScheme.doubleClicked.connect(self.onTableDoubleClick)
        self.tableClassificationScheme.resizeColumnsToContents()
        self.selectionModel = QItemSelectionModel(self.mScheme)
        self.selectionModel.selectionChanged.connect(self.onSelectionChanged)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        self.onSelectionChanged()  # enable/disable widgets depending on a selection
        self.tableClassificationScheme.setSelectionModel(self.selectionModel)

        self.initActions()

    def onCopyClasses(self):

        classes = self.selectedClasses()
        if len(classes) == 0:
            return
        cs = ClassificationScheme()
        cs.insertClasses(classes)
        cb = QApplication.clipboard()
        assert isinstance(cb, QClipboard)
        cb.setMimeData(cs.mimeData(None))

    def onPasteClasses(self):
        cb = QApplication.clipboard()
        assert isinstance(cb, QClipboard)
        mimeData = QApplication.clipboard().mimeData()

        cs = ClassificationScheme.fromMimeData(mimeData)
        if isinstance(cs, ClassificationScheme):
            self.mScheme.insertClasses(cs[:])

    def onSaveClasses(self):

        classes = self.selectedClasses()
        if len(classes) == 0:
            return

        cs = ClassificationScheme()
        cs.insertClasses(classes)

        filter = "CSV (*.csv *.txt);;JSON (*.json)"
        path, filter = QFileDialog.getSaveFileName(self, "Save classes to file",
                                                   "/home", filter)
        if isinstance(path, str) and len(path) > 0:

            if path.endswith('.json'):

                pass

            elif path.endswith('.csv'):

                cs.saveToCsv(path)

            if filter == 'csv':
                pass

            s  =""



    def onLoadClasses(self, mode:str):
        """
        Opens a dialog to add ClassInfos from other sources, like raster images, text files and QgsMapLayers.
        :param mode: 'raster', 'layer', 'textfile'
        """
        if mode == 'raster':
            filter = QgsProviderRegistry.instance().fileRasterFilters()
            path, filter = QFileDialog.getOpenFileName(self,
                                                   "Read classes from raster image",
                                                   "/home", filter)
            if isinstance(path, str) and os.path.isfile(path):
                cs = ClassificationScheme.fromRasterImage(path)
                if isinstance(cs, ClassificationScheme):
                    self.mScheme.insertClasses(cs[:])


        if mode == 'layer':
Benjamin Jakimow's avatar
Benjamin Jakimow committed
            possibleLayers = findMapLayersWithClassInfo()
            if len(possibleLayers) == 0:
                QMessageBox.information(self, 'Load classes from layer', 'No layers with categorical render styles available.')
            else:
                choices = ['{} ({})'.format(l.name(), l.source()) for l  in possibleLayers]

                dialog = QInputDialog(parent=self)
                dialog.setWindowTitle('Load classes from layer')
                dialog.setTextValue('Select map layer')
                dialog.setComboBoxItems(choices)
                dialog.setOption(QInputDialog.UseListViewForComboBoxItems)
                if dialog.exec_() == QDialog.Accepted:
                    selection = dialog.textValue()
                    i = choices.index(selection)
                    layer = possibleLayers[i]
                    if isinstance(layer, QgsVectorLayer):
                        cs = ClassificationScheme.fromFeatureRenderer(layer.renderer())
                    elif isinstance(layer, QgsRasterLayer):
                        cs = ClassificationScheme.fromRasterRenderer(layer.renderer())
                    if isinstance(cs, ClassificationScheme):
                        self.mScheme.insertClasses(cs[:])
            pass

        if mode == 'textfile':

            filter = "CSV (*.csv *.txt);;JSON (*.json);;QML (*.qml)"
            path, filter = QFileDialog.getOpenFileName(self,
                                                   "Read classes from text file",
                                                    "/home", filter)
            if isinstance(path, str) and os.path.isfile(path):
                cs = ClassificationScheme.fromFile()
                if isinstance(cs, ClassificationScheme):
                    self.mScheme.insertClasses(cs[:])


    def initActions(self):

        m = QMenu('Load classes')
        m.setToolTip('Load classes ...')
        a = m.addAction('Load from raster')
        a.triggered.connect(lambda : self.onLoadClasses('raster'))
        a = m.addAction('Load from map layer')
        a.triggered.connect(lambda : self.onLoadClasses('layer'))
        a = m.addAction('Load from other textfile')
        a.triggered.connect(lambda : self.onLoadClasses('textfile'))


        self.btnLoadClasses.setMenu(m)

        self.actionRemoveClasses.triggered.connect(self.removeSelectedClasses)
        self.actionAddClasses.triggered.connect(lambda : self.createClasses(1))

        self.actionSaveClasses.setIcon(QIcon(r'://images/themes/default/mActionFileSaveAs.svg'))
        self.actionSaveClasses.triggered.connect(self.onSaveClasses)

        QApplication.clipboard().dataChanged.connect(self.onClipboard)
        self.actionPasteClasses.setIcon(QIcon(r'://images/themes/default/mActionEditPaste.svg'))
        self.actionPasteClasses.triggered.connect(self.onPasteClasses)

        self.actionCopyClasses.setIcon(QIcon(r'://images/themes/default/mActionEditCopy.svg'))
        self.actionCopyClasses.triggered.connect(self.onCopyClasses)

        self.btnSaveClasses.setDefaultAction(self.actionSaveClasses)
        self.btnRemoveClasses.setDefaultAction(self.actionRemoveClasses)
        self.btnAddClasses.setDefaultAction(self.actionAddClasses)
        self.btnCopyClasses.setDefaultAction(self.actionCopyClasses)
        self.btnPasteClasses.setDefaultAction(self.actionPasteClasses)

        self.onClipboard()

    def onClipboard(self, *args):
        mimeData = QApplication.clipboard().mimeData()
        b = isinstance(mimeData, QMimeData) and MIMEDATA_KEY_TEXT in mimeData.formats()
        self.actionPasteClasses.setEnabled(b)


    def onTableDoubleClick(self, idx):
        model = self.tableClassificationScheme.model()
        assert isinstance(model, ClassificationScheme)
        classInfo = model.index2ClassInfo(idx)
        if idx.column() == model.columnNames().index(model.mColColor):
            c = QColorDialog.getColor(classInfo.mColor, self.tableClassificationScheme, \
                                      'Set color for "{}"'.format(classInfo.name()))
            model.setData(idx, c, role=Qt.EditRole)

    def onSelectionChanged(self, *args):
        b = self.selectionModel is not None and len(self.selectionModel.selectedRows()) > 0
        self.actionRemoveClasses.setEnabled(b)
        self.actionCopyClasses.setEnabled(b)
        self.actionSaveClasses.setEnabled(b)

    def createClasses(self, n):
        self.mScheme.createClasses(n)




    def selectedClasses(self)->list:
        """
        Returns the list of selected ClassInfos
        :return: [list-of-ClassInfo]
        """
        indices = reversed(self.selectionModel.selectedRows())
        return [self.mScheme.index2ClassInfo(idx) for idx in indices]

    def removeSelectedClasses(self):
        classes = self.selectedClasses()
        if len(classes) > 0:
            self.mScheme.removeClasses(classes)

    def loadClasses(self, *args):
Benjamin Jakimow's avatar
Benjamin Jakimow committed

        defDir = None
        path, _ = QFileDialog.getOpenFileName(self, 'Select Raster File', directory=defDir)
        if os.path.exists(path):
Benjamin Jakimow's avatar
Benjamin Jakimow committed

            try:
                scheme = ClassificationScheme.fromRasterImage(path)
                if scheme is not None:
                    self.appendClassificationScheme(scheme)
            except Exception as ex:
                QMessageBox.critical(self, "Unable to load class info", str(ex))


    def appendClassificationScheme(self, classificationScheme):
        assert isinstance(classificationScheme, ClassificationScheme)
        self.mScheme.insertClasses([c for c in classificationScheme])

    def setClassificationScheme(self, classificationScheme):
        assert isinstance(classificationScheme, ClassificationScheme)
        self.mScheme.clear()
        self.appendClassificationScheme(classificationScheme)

    def classificationScheme(self):
        return self.mScheme


class ClassificationSchemeDialog(QgsDialog):
    @staticmethod
    def getClassificationScheme(*args, **kwds):
        """
        Opens a dialog to edit a ClassificationScheme
        :param args:
        :param kwds:
        :return: None | ClassificationScheme
        """
        d = ClassificationSchemeDialog(*args, **kwds)
        d.exec_()

        if d.result() == QDialog.Accepted:
            return d.classificationScheme()
        else:
            return None

    def __init__(self, parent=None, classificationScheme=None, title='Specify Classification Scheme'):
        super(ClassificationSchemeDialog, self).__init__(parent=parent, \
                                                         buttons=QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
        self.w = ClassificationSchemeWidget(parent=self, classificationScheme=classificationScheme)
        self.setWindowTitle(title)
        self.btOk = QPushButton('Ok')
        self.btCancel = QPushButton('Cancel')
        buttonBar = QHBoxLayout()
        # buttonBar.addWidget(self.btCancel)
        # buttonBar.addWidget(self.btOk)
        l = self.layout()
        l.addWidget(self.w)
        l.addLayout(buttonBar)
        # self.setLayout(l)

        if isinstance(classificationScheme, ClassificationScheme):
            self.setClassificationScheme(classificationScheme)
        s = ""

    def classificationScheme(self):
        return self.w.classificationScheme()

    def setClassificationScheme(self, classificationScheme):
        assert isinstance(classificationScheme, ClassificationScheme)
        self.w.setClassificationScheme(classificationScheme)


class ClassificationSchemeEditorWidgetWrapper(QgsEditorWidgetWrapper):

    def __init__(self, vl:QgsVectorLayer, fieldIdx:int, editor:QWidget, parent:QWidget):
        super(ClassificationSchemeEditorWidgetWrapper, self).__init__(vl, fieldIdx, editor, parent)

        self.mComboBox = None
        self.mDefaultValue = None

    def createWidget(self, parent: QWidget):
        #log('createWidget')
        w = ClassificationSchemeComboBox(parent)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        w.model().setAllowEmptyField(True)
        w.setVisible(True)
        return w

    def initWidget(self, editor:QWidget):
        #log(' initWidget')
        conf = self.config()

        if isinstance(editor, ClassificationSchemeComboBox):
            self.mComboBox = editor
            self.mComboBox.setClassificationScheme(classSchemeFromConfig(conf))
            self.mComboBox.currentIndexChanged.connect(self.onValueChanged)

        else:
            s = ""

    def onValueChanged(self, *args):
        self.valueChanged.emit(self.value())
        s = ""

    def valid(self, *args, **kwargs)->bool:
        return isinstance(self.mComboBox, ClassificationSchemeComboBox)

    def value(self, *args, **kwargs):

Benjamin Jakimow's avatar
Benjamin Jakimow committed
        value = None
        if isinstance(self.mComboBox, ClassificationSchemeComboBox):
            classInfo = self.mComboBox.currentClassInfo()
            if isinstance(classInfo, ClassInfo):
Benjamin Jakimow's avatar
Benjamin Jakimow committed

                typeCode = self.field().type()
                if typeCode == QVariant.String:
Benjamin Jakimow's avatar
Benjamin Jakimow committed
                    value =  classInfo.name()
                elif typeCode in [QVariant.Int, QVariant.Double]:
                    value = classInfo.label()
                else:
                    s = ""
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        return value


    def setEnabled(self, enabled:bool):

        if isinstance(self.mComboBox, ClassificationSchemeComboBox):
            self.mComboBox.setEnabled(enabled)


    def setValue(self, value):

        if isinstance(self.mComboBox, ClassificationSchemeComboBox):
Benjamin Jakimow's avatar
Benjamin Jakimow committed
            i = self.mComboBox.classIndexFromValue(value)
            self.mComboBox.setCurrentIndex(i)


class ClassificationSchemeEditorConfigWidget(QgsEditorConfigWidget):

    def __init__(self, vl:QgsVectorLayer, fieldIdx:int, parent:QWidget):

        super(ClassificationSchemeEditorConfigWidget, self).__init__(vl, fieldIdx, parent)
        #self.setupUi(self)
        self.mSchemeWidget = ClassificationSchemeWidget(parent=self)
        self.mSchemeWidget.sigValuesChanged.connect(self.changed)
        self.setLayout(QVBoxLayout())
        self.layout().addWidget(self.mSchemeWidget)
        self.mLastConfig = {}


    def config(self, *args, **kwargs)->dict:
        return classSchemeToConfig(self.mSchemeWidget.classificationScheme())

    def setConfig(self, config:dict):
        self.mLastConfig = config
        cs = classSchemeFromConfig(config)
        self.mSchemeWidget.setClassificationScheme(cs)

    def resetClassificationScheme(self):
        self.setConfig(self.mLastConfig)

def classSchemeToConfig(classScheme:ClassificationScheme)->dict:
    config = {'classes': classScheme.json()}
    return config

def classSchemeFromConfig(conf:dict)->ClassificationScheme:
    cs = None
    if 'classes' in conf.keys():
        cs = ClassificationScheme.fromJson(conf['classes'])
    if not isinstance(cs, ClassificationScheme):
        return ClassificationScheme()
    else:
        return cs


class ClassificationSchemeWidgetFactory(QgsEditorWidgetFactory):

    def __init__(self, name:str):

        super(ClassificationSchemeWidgetFactory, self).__init__(name)

        self.mConfigurations = {}

    def configWidget(self, layer:QgsVectorLayer, fieldIdx:int, parent=QWidget)->ClassificationSchemeEditorConfigWidget:
        """
        Returns a SpectralProfileEditorConfigWidget
        :param layer: QgsVectorLayer
        :param fieldIdx: int
        :param parent: QWidget
        :return: SpectralProfileEditorConfigWidget
        """

        w = ClassificationSchemeEditorConfigWidget(layer, fieldIdx, parent)
Benjamin Jakimow's avatar
Benjamin Jakimow committed

Benjamin Jakimow's avatar
Benjamin Jakimow committed

        initialConfig = layer.editorWidgetSetup(fieldIdx).config()
        self.writeConfig(key, initialConfig)
        w.setConfig(self.readConfig(key))
        w.changed.connect(lambda : self.writeConfig(key, w.config()))
        return w

    def configKey(self, layer:QgsVectorLayer, fieldIdx: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)

    def create(self, layer:QgsVectorLayer, fieldIdx:int, editor:QWidget, parent:QWidget)->ClassificationSchemeEditorWidgetWrapper:
        """
        Create a ClassificationSchemeEditorWidgetWrapper
        :param layer: QgsVectorLayer
        :param fieldIdx: int
        :param editor: QWidget
        :param parent: QWidget
        :return: ClassificationSchemeEditorWidgetWrapper
        """
        w = ClassificationSchemeEditorWidgetWrapper(layer, fieldIdx, editor, parent)
        return w

    def writeConfig(self, key:tuple, config:dict):
        """
        :param key: tuple (str, int), as created with .configKey(layer, fieldIdx)
        :param config: dict with config values
        """
        self.mConfigurations[key] = config

    def readConfig(self, key:tuple):
        """
        :param key: tuple (str, int), as created with .configKey(layer, fieldIdx)
        :return: {}
        """
        if key in self.mConfigurations.keys():
            conf = self.mConfigurations[key]
        else:
Benjamin Jakimow's avatar
Benjamin Jakimow committed
            # return the very default "empty" configuration
            conf = {}
        return conf

    def fieldScore(self, vl:QgsVectorLayer, fieldIdx:int)->int:
        """
        This method allows disabling this editor widget type for a certain field.
        0: not supported: none String fields
        5: maybe support String fields with length <= 400
        20: specialized support: String fields with length > 400

        :param vl: QgsVectorLayer
        :param fieldIdx: int
        :return: int
        """
        #log(' fieldScore()')
        if fieldIdx < 0:
            return 0
        field = vl.fields().at(fieldIdx)
        assert isinstance(field, QgsField)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        if re.search('(int|float|double|text|string)', field.typeName(), re.I):
            if re.search('class', field.name(), re.I):
                return 10
            else:
                return 5
Benjamin Jakimow's avatar
Benjamin Jakimow committed
            return 0 # no support

    def supportsField(self, vl:QgsVectorLayer, idx:int):
        field = vl.fields().at(idx)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        if isinstance(field, QgsField) and re.search('(int|float|double|text|string)', field.typeName(), re.I):
Benjamin Jakimow's avatar
Benjamin Jakimow committed
EDITOR_WIDGET_REGISTRY_KEY = 'Raster Classification'
def registerClassificationSchemeEditorWidget():
    reg = QgsGui.editorWidgetRegistry()
    if not EDITOR_WIDGET_REGISTRY_KEY in reg.factories().keys():
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        global CLASS_SCHEME_EDITOR_WIDGET_FACTORY
        factory = ClassificationSchemeWidgetFactory(EDITOR_WIDGET_REGISTRY_KEY)
        reg.registerWidget(EDITOR_WIDGET_REGISTRY_KEY, factory)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        CLASS_SCHEME_EDITOR_WIDGET_FACTORY = factory