5

I found a problem with exporting the vector layer with: "Replace all selected raw field values by displayed values" in the PyQGIS script.

The source layer contains a value map for attribute "value_map":

enter image description here

PyQGIS script:

# -*- coding: utf-8 -*-
from qgis.PyQt.QtCore import QCoreApplication
from qgis.core import (QgsProcessing,
                       QgsFeatureSink,
                       QgsVectorFileWriter,
                       QgsProcessingException,
                       QgsProcessingAlgorithm,
                       QgsVectorFileWriter,
                       QgsProcessingParameterFeatureSource,
                       QgsProcessingParameterFeatureSink)
from qgis import processing
from qgis.utils import iface


class ExampleProcessingAlgorithm(QgsProcessingAlgorithm):
    INPUT = 'INPUT'

    def tr(self, string):
        return QCoreApplication.translate('Processing', string)

    def createInstance(self):
        return ExampleProcessingAlgorithm()

    def name(self):
        return 'myscript'

    def displayName(self):
        return self.tr('My Script')

    def group(self):
        return self.tr('Example scripts')

    def groupId(self):
        return 'examplescripts'

    def shortHelpString(self):
        return self.tr("Example algorithm short description")

    def initAlgorithm(self, config=None):
        # We add the input vector features source. It can have any kind of
        # geometry.
        self.addParameter(
            QgsProcessingParameterFeatureSource(
                self.INPUT,
                self.tr('Input layer'),
                [QgsProcessing.TypeVectorAnyGeometry]
            )
        )

    def processAlgorithm(self, parameters, context, feedback):
        layer = iface.activeLayer()
        feedback.pushInfo(str(layer))
        QgsVectorFileWriter.writeAsVectorFormat( 
            layer=layer,
            fileName='C://Users//001-PC//Desktop//output_layer.json', 
            fileEncoding="utf-8", 
            driverName="GEOJSON",
            fieldValueConverter=QgsVectorFileWriter.FieldValueConverter()
        )
        return {}

How can I export layer values with a values map? My script does not work with:

fieldValueConverter = QgsVectorFileWriter.FieldValueConverter()

Exported JSON (script result):

{
"type": "FeatureCollection",
"name": "output_layer",
"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:EPSG::5514" } },
"features": [
{ "type": "Feature", "properties": { "fid": 1, "text": "aaa", "value_map": 1 }, "geometry": { "type": "Point", "coordinates": [ -553020.462518186424859, -1170227.009075149893761 ] } },
{ "type": "Feature", "properties": { "fid": 2, "text": "sadasd", "value_map": 2 }, "geometry": { "type": "Point", "coordinates": [ -553025.836183290113695, -1170234.612239605281502 ] } },
{ "type": "Feature", "properties": { "fid": 3, "text": "sdasdsad", "value_map": 3 }, "geometry": { "type": "Point", "coordinates": [ -553018.004352234653197, -1170230.038907602196559 ] } }
]
}

Source layer value map setting:

enter image description here

2 Answers 2

8

The official doc for QgsVectorFileWriter.FieldValueConverter tells you that it's an "Interface to convert raw field values to their user-friendly value"

The interface has 3 methods, clone, convert and fieldDefinition.

For convert,

Default implementation will return provided value unmodified.

For fieldDefinition,

Default implementation will return provided field unmodified.

So, the solution is to create your own class base on QgsVectorFileWriter.FieldValueConverter to get the expected behaviour. In Python tests in QGIS repo, there is an example TestFieldValueConverter implemented https://github.com/qgis/QGIS/blob/master/tests/src/python/test_qgsvectorfilewriter.py#L68 and called from https://github.com/qgis/QGIS/blob/master/tests/src/python/test_qgsvectorfilewriter.py#L533-L569

I've made an example that take valueMap info and report them in the export correctly. I've tested below that should more or less do the job as I did not try it extensively. I just manage valueMap here but you will need more code to manage other types of fields when converting from fields values to display values. In current code, I did not try in particular to manage NULL values. I've also used writeAsVectorFormatV2 as writeAsVectorFormat different ways to export are deprecated although always working at the moment.

class CustomFieldValueConverter(QgsVectorFileWriter.FieldValueConverter):

    def __init__(self, layer):
        QgsVectorFileWriter.FieldValueConverter.__init__(self)
        self.layer = layer

    def fieldDefinition(self, field):
        idx = self.layer.fields().indexFromName(field.name())
        editorWidget = self.layer.editorWidgetSetup(idx)
        if editorWidget.type() == 'ValueMap':
            return QgsField(field.name(), QVariant.String)
        else:
            return self.layer.fields()[idx]

    def convert(self, idx, value):
        editorWidget = self.layer.editorWidgetSetup(idx)
        # print(editorWidget.config(), editorWidget.type(), editorWidget.isNull())
        if editorWidget.type() == 'ValueMap':
            valueMap = editorWidget.config()['map']
            dictValueMapWithKeyValueSwapped = {v: k for d in valueMap for k, v in d.items()}
            return dictValueMapWithKeyValueSwapped.get(str(value))
        else:
            return value

layer = iface.activeLayer()

myoptions = QgsVectorFileWriter.SaveVectorOptions()
myconverter = CustomFieldValueConverter(layer)
myoptions.fieldValueConverter = myconverter
myoptions.driverName = 'GeoJSON'
context = QgsProject.instance().transformContext()
mypath = '/tmp/output.geojson'
write_result, error_message = QgsVectorFileWriter.writeAsVectorFormatV2(layer, mypath, context, myoptions)

print(write_result, error_message)

Edit:

A more generic version that should work for all fields. Do not hesitate to make comment about it (only tested with a ValueMap sample on my side)

class CustomFieldValueConverter(QgsVectorFileWriter.FieldValueConverter):

    def __init__(self, layer):
        QgsVectorFileWriter.FieldValueConverter.__init__(self)
        self.__formattersAllowList = ["KeyValue", "List", "ValueRelation", "ValueMap"]
        self.layer = layer
        self.__mFormatters = {}
        self.__mConfig = {}
        self.__mCaches= {}
        
        for i, field in enumerate(layer.fields()):
            setup = field.editorWidgetSetup()
            fieldFormatter = QgsApplication.fieldFormatterRegistry().fieldFormatter(setup.type())
            if fieldFormatter.id() in self.__formattersAllowList:
                self.__mFormatters[i] = fieldFormatter
                keysValue = setup.config()['map']
                dict_target = {}
                for j in keysValue:
                    dict_target.update(j)
                self.__mConfig[i] = {"map": dict_target}

    def fieldDefinition(self, field):
        if not self.layer:
            return field
        idx = self.layer.fields().indexFromName(field.name())
        editorWidget = self.layer.editorWidgetSetup(idx)
        if idx in self.__mFormatters:
            return QgsField(field.name(), QVariant.String)
        else:
            return field

    def convert(self, idx, value):
        formatter = self.__mFormatters.get(idx)
        if formatter is None:
            return value
        if self.__mCaches.get(idx) is not None:
            cache = self.__mCaches.get(idx)
        else:
            cache = formatter.createCache(self.layer, idx, self.__mConfig.get(idx))
            self.__mCaches[idx] = cache
        return formatter.representValue(self.layer, idx, self.__mConfig.get(idx), cache, value);

    def clone(self):
        return FieldValueConverter(self)

layer = iface.activeLayer()

myoptions = QgsVectorFileWriter.SaveVectorOptions()
myconverter = CustomFieldValueConverter(layer)
myoptions.fieldValueConverter = myconverter
myoptions.driverName = 'GeoJSON'
context = QgsProject.instance().transformContext()
mypath = '/tmp/output.geojson'
write_result, error_message = QgsVectorFileWriter.writeAsVectorFormatV2(layer, mypath, context, myoptions)

print(write_result, error_message)
2
  • Thank you for your answer, your soulution works well.
    – Wenceslauw
    Commented Mar 15, 2021 at 6:58
  • Hi @ThomasG77, I am trying to make your solution work with a Value Relation type, but I get errors considering the 'map' keyword. I searched a bunch, but can't figure out how to make this work. Any help would be greatly appreciated :)
    – Horizen
    Commented Dec 8, 2021 at 22:16
0

I used @ThomasG77 solution and modified my script. Script is working well now:

 # -*- coding: utf-8 -*-
from qgis.PyQt.QtCore import *
from qgis.core import (QgsProcessing,
                       QgsField,
                       QgsFeatureSink,
                       QgsVectorFileWriter,
                       QgsProcessingException,
                       QgsProcessingAlgorithm,
                       QgsVectorFileWriter,
                       QgsProcessingParameterFeatureSource,
                       QgsProcessingParameterFeatureSink)
from qgis import processing
from qgis.utils import iface


class CustomFieldValueConverter(QgsVectorFileWriter.FieldValueConverter):
    def __init__(self, layer):
        QgsVectorFileWriter.FieldValueConverter.__init__(self)
        self.layer = layer

    def fieldDefinition(self, field):
        idx = self.layer.fields().indexFromName(field.name())
        editorWidget = self.layer.editorWidgetSetup(idx)
        if editorWidget.type() == 'ValueMap':
            return QgsField(field.name(), QVariant.String)
        else:
            return self.layer.fields()[idx]

    def convert(self, idx, value):
        editorWidget = self.layer.editorWidgetSetup(idx)
        # print(editorWidget.config(), editorWidget.type(), editorWidget.isNull())
        if editorWidget.type() == 'ValueMap':
            valueMap = editorWidget.config()['map']
            dictValueMapWithKeyValueSwapped = {v: k for d in valueMap for k, v in d.items()}
            return dictValueMapWithKeyValueSwapped.get(str(value))
        else:
            return value

class ExampleProcessingAlgorithm(QgsProcessingAlgorithm):
    INPUT = 'INPUT'
    OUTPUT = 'OUTPUT'

    def tr(self, string):
        return QCoreApplication.translate('Processing', string)

    def createInstance(self):
        return ExampleProcessingAlgorithm()

    def name(self):
        return 'myscript'

    def displayName(self):
        return self.tr('My Script')

    def group(self):
        return self.tr('Example scripts')

    def groupId(self):
        return 'examplescripts'

    def shortHelpString(self):
        return self.tr("Example algorithm short description")

    def initAlgorithm(self, config=None):
        # We add the input vector features source. It can have any kind of
        # geometry.
        self.addParameter(
            QgsProcessingParameterFeatureSource(
                self.INPUT,
                self.tr('Input layer'),
                [QgsProcessing.TypeVectorAnyGeometry]
            )
        )
        
    def processAlgorithm(self, parameters, context, feedback):
        layer = iface.activeLayer()
        myconverter = CustomFieldValueConverter(layer)
        QgsVectorFileWriter.writeAsVectorFormat( 
            layer=layer,
            fileName='C://Users//001-PC//Desktop//output_layer.json', 
            fileEncoding="utf-8", 
            driverName="GEOJSON",
            fieldValueConverter=myconverter
        )
        return {}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.