Ejemplo n.º 1
0
    def get_layer_metadata(self,lyr=None):
        '''
        builds a metadata dict of the current layer to be stored in summary sheet
        '''

        if not lyr:
            lyr = self.lyr

        #fields = collections.OrderedDict()
        fields = ""
        for field in lyr.fields().toList():
            fields += field.name()+'_'+QVariant.typeToName(field.type())+'|'+str(field.length())+'|'+str(field.precision())+' '
        #metadata = collections.OrderedDict()
        metadata = [
            ['layer_name', lyr.name(),],
            ['gdrive_id', self.service_sheet.spreadsheetId,],
            ['geometry_type', self.geom_types[lyr.geometryType()],],
            ['features', "'%s" % str(lyr.featureCount()),],
            ['extent', self.wgs84_extent(lyr.extent()).asWktCoordinates(),],
            #['fields', fields,],
            ['abstract', lyr.abstract(),],
            ['srid', lyr.crs().authid(),],
            ['proj4_def', "'%s" % lyr.crs().toProj4(),],
        ]

        return metadata
Ejemplo n.º 2
0
    def get_metadata(self, instance, qgs_maplayer):
        """Build metadata for layer by QgsMapLayer instance
        :param instance: qdjango Layer model instance
        :param qgs_maplayer: QgsMapLayer instance
        :return: dict
        """

        metadata = {}
        metadata['title'] = instance.title

        # try to build by qgs_layer
        metadata['attributes'] = []
        if instance.database_columns:

            for f in qgs_maplayer.fields():
                attribute = {}
                attribute['name'] = f.name()
                # attribute['editType'] = f.editType()
                attribute['typeName'] = f.typeName()
                attribute['comment'] = f.comment()
                attribute['length'] = f.length()
                attribute['precision'] = f.precision()
                attribute['type'] = QVariant.typeToName(f.type())
                attribute['alias'] = f.alias()

                metadata['attributes'].append(attribute)

        # FIXME: ask to elapso where to find CRS getprojectsettings layer list.
        metadata['crs'] = []

        metadata['dataurl'] = {}
        if qgs_maplayer.dataUrl() != '':
            metadata['dataurl']['onlineresource'] = qgs_maplayer.dataUrl()

        metadata['metadataurl'] = {}

        if qgs_maplayer.metadataUrl() != '':
            metadata['metadataurl'][
                'onlineresource'] = qgs_maplayer.metadataUrl()
            metadata['metadataurl']['type'] = qgs_maplayer.metadataUrlType()
            metadata['metadataurl']['format'] = qgs_maplayer.metadataUrlFormat(
            )

        metadata['attribution'] = {}

        if qgs_maplayer.attribution() != '':
            metadata['attribution']['title'] = qgs_maplayer.attribution()

        if qgs_maplayer.attributionUrl() != '':
            metadata['attribution'][
                'onlineresource'] = qgs_maplayer.attributionUrl()

        # add abstract
        if qgs_maplayer.abstract() != '':
            metadata['abstract'] = qgs_maplayer.abstract()

        return metadata
Ejemplo n.º 3
0
    def _set_add_edit_user_field_choices(self):
        """
        Set choices for add_user_field select and edit_user_field select
        """

        touse = []
        fields = self.layer.qgis_layer.fields()

        for f in fields:
            type = QVariant.typeToName(f.type()).upper()
            if type == 'QSTRING':  # and f.length() > 200:
                touse.append(f.name())

        self.fields['edit_user_field'].choices = \
            self.fields['add_user_field'].choices = [(None, '--------')] + [(f, f) for f in touse]
Ejemplo n.º 4
0
    def setValue(self, value):
        if type(value) != dict:
            return
        destinationFields = QgsFields()
        expressions = {}
        for field_def in value:
            f = QgsField(
                field_def.get('name'), field_def.get('type', QVariant.Invalid),
                field_def.get(
                    QVariant.typeToName(field_def.get('type',
                                                      QVariant.Invalid))),
                field_def.get('length', 0), field_def.get('precision', 0))
            try:
                expressions[f.name()] = field_def['expressions']
            except AttributeError:
                pass
            destinationFields.append(f)

        if len(destinationFields):
            self.fieldsView.setDestinationFields(destinationFields,
                                                 expressions)
Ejemplo n.º 5
0
def generate_model_doc():  # NOQA C901
    global TEMPLATE

    markdown_all = TEMPLATE

    files = os.listdir(resources_path('data_models'))
    mermaid_md = '```mermaid\n'
    mermaid_md += 'classDiagram\n'

    mermaid_field_md = ''

    for csv_file in files:
        if csv_file.endswith('csvt'):
            continue
        table_name = csv_file.replace('.csv', '')

        if table_name == 'metadata':
            continue

        md = ''

        # Ajout de la geom
        if MAPPING[table_name][0] is not None:
            field_md = TEMPLATE_FIELDS.format(
                id='',
                name='*geom*',
                type=MAPPING[table_name][0],
                alias='',
            )
            md += field_md
            mermaid_field_md += '{} : geom {}\n'.format(
                table_name,
                MAPPING[table_name][0],
            )

        mermaid_md += table_name + '\n'
        pretty_name = table_name.replace('_', ' ')
        pretty_name = pretty_name.title()

        csv_path = resources_path('data_models', '{}.csv'.format(table_name))
        csv = QgsVectorLayer(csv_path, table_name, 'ogr')

        for i, field in enumerate(csv.getFeatures()):

            display_name = mermaid_display_name = field['name']

            if display_name == 'id':
                display_name = '**' + display_name + '**'
                mermaid_display_name += ' PK'

            # if display_name.endswith('_id'):
            #     display_name = '[{title} FK](#{anchor})'.format(
            #         title=display_name,
            #         anchor=slug(find_relation(field['name'], table_name))
            #     )
            #     mermaid_display_name += ' FK'

            field_md = TEMPLATE_FIELDS.format(
                id=field['idx'],
                name=display_name,
                type=QVariant.typeToName(int(field['type'])),
                alias=field['alias'],
            )
            md += field_md

            if i < 10:
                mermaid_field_md += '{} : {}\n'.format(
                    table_name,
                    mermaid_display_name,
                )
            elif i == 10:
                mermaid_field_md += '{} : ...\n'.format(table_name)

        markdown = TEMPLATE_TABLE.format(name=pretty_name, fields=md)
        markdown_all += markdown

    for relation in relations:
        mermaid_md += '{} <|-- {}\n'.format(
            relation['referencedLayer'],
            relation['referencingLayer'],
        )

    mermaid_md += mermaid_field_md
    mermaid_md += '```'
    markdown_all = markdown_all.format(relationships=mermaid_md)

    output_file = join(PATH, 'README.md')
    output_file = '/home/etienne/dev/python/dsvi/docs/model.md'
    text_file = open(output_file, "w+")
    text_file.write(markdown_all)
    text_file.close()
Ejemplo n.º 6
0
def feature_validator(feature, layer):
    """Validate a QGIS feature by checking QGIS fields constraints

    The logic here is to:
    - if geometry is not None check if geometry type matches the layer type
    - loop through the fields and check for constraints:
        - NOT NULL, skip the next check if this fails
        - UNIQUE (only if not NULL)
        - EXPRESSION (QgsExpression configured in the form), always evaluated,
          even in case of NULLs

    Note: only hard constraints are checked!

    :param feature: QGIS feature
    :type feature: QgsFeature
    :param layer: QGIS layer
    :type layer: QgsVectorLayer
    :return: a dictionary of errors for each field + geometry
    :rtype: dict
    """

    errors = dict()
    geometry = feature.geometry()

    data_provider = layer.dataProvider()

    def _has_default_value(field_index, field):
        return (
            # Provider level
            data_provider.defaultValueClause(field_index)
            or data_provider.defaultValue(field_index)
            or field.defaultValueDefinition().isValid())

    # Check geometry type
    if not geometry.isNull() and geometry.wkbType() != layer.wkbType():
        if not (geometry.wkbType() == QgsWkbTypes.Point25D
                and layer.wkbType() == QgsWkbTypes.PointZ
                or geometry.wkbType() == QgsWkbTypes.Polygon25D
                and layer.wkbType() == QgsWkbTypes.PolygonZ
                or geometry.wkbType() == QgsWkbTypes.LineString25D
                and layer.wkbType() == QgsWkbTypes.LineStringZ
                or geometry.wkbType() == QgsWkbTypes.MultiPoint25D
                and layer.wkbType() == QgsWkbTypes.MultiPointZ
                or geometry.wkbType() == QgsWkbTypes.MultiPolygon25D
                and layer.wkbType() == QgsWkbTypes.MultiPolygonZ
                or geometry.wkbType() == QgsWkbTypes.MultiLineString25D
                and layer.wkbType() == QgsWkbTypes.MultiLineStringZ):

            errors['geometry'] = _(
                'Feature geometry type %s does not match layer type: %s') % (
                    QgsWkbTypes.displayString(geometry.wkbType()),
                    QgsWkbTypes.displayString(layer.wkbType()))

    def _set_error(field_name, error):
        if not field_name in errors:
            errors[field_name] = []
        errors[field_name].append(error)

    # Check fields "hard" constraints
    for field_index in range(layer.fields().count()):

        field = layer.fields().field(field_index)

        # check if fields is a join field:
        if layer.fields().fieldOrigin(field_index) == QgsFields.OriginJoin:
            continue

        # Check not null first, if it fails skip other tests (unique and expression)
        value = feature.attribute(field.name())
        # If there is a default value we assume it's not NULL (we cannot really know at this point
        # what will be the result of the default value clause evaluation, it might even be provider-side
        if (value is None or value
                == QVariant()) and not _has_default_value(field_index, field):
            not_null = (field.constraints().constraintOrigin(
                QgsFieldConstraints.ConstraintNotNull) !=
                        QgsFieldConstraints.ConstraintOriginNotSet
                        and field.constraints().constraintStrength(
                            QgsFieldConstraints.ConstraintNotNull)
                        == QgsFieldConstraints.ConstraintStrengthHard)
            if not_null:
                _set_error(field.name(), _('Field value must be NOT NULL'))
                continue

        value = feature.attribute(field_index)

        # Skip if NULL, not sure if we want to continue in this case but it seems pointless
        # to check for unique or type compatibility on NULLs
        if value is not None and value != QVariant():

            if not QVariant(value).convert(field.type()):
                _set_error(
                    field.name(),
                    _('Field value \'%s\' cannot be converted to %s') %
                    (value, QVariant.typeToName(field.type())))

            unique = (field.constraints().constraintOrigin(
                QgsFieldConstraints.ConstraintUnique) !=
                      QgsFieldConstraints.ConstraintOriginNotSet
                      and field.constraints().constraintStrength(
                          QgsFieldConstraints.ConstraintUnique)
                      == QgsFieldConstraints.ConstraintStrengthHard)

            if unique:
                # Search for features, excluding self if it's an update
                request = QgsFeatureRequest()
                request.setNoAttributes()
                request.setFlags(QgsFeatureRequest.NoGeometry)
                request.setLimit(2)
                if field.isNumeric():
                    request.setFilterExpression(
                        '"%s" = %s' %
                        (field.name().replace('"', '\\"'), value))
                elif field.type() == QVariant.String:
                    request.setFilterExpression(
                        '"%s" = \'%s\'' % (field.name().replace(
                            '"', '\\"'), value.replace("'", "\\'")))
                elif field.type() == QVariant.Date:
                    request.setFilterExpression(
                        'to_date("%s") = \'%s\'' % (field.name().replace(
                            '"', '\\"'), value.toString(Qt.ISODate)))
                elif field.type() == QVariant.DateTime:
                    request.setFilterExpression(
                        'to_datetime("{field_name}") = \'{date_time_string}\' OR to_datetime("{field_name}") = \'{date_time_string}.000\''
                        .format(field_name=field.name().replace('"', '\\"'),
                                date_time_string=value.toString(Qt.ISODate)))
                elif field.type(
                ) == QVariant.Bool:  # This does not make any sense, but still
                    request.setFilterExpression(
                        '"%s" = %s' % (field.name().replace(
                            '"', '\\"'), 'true' if value else 'false'))
                else:  # All the other formats: let's convert to string and hope for the best
                    request.setFilterExpression(
                        '"%s" = \'%s\'' %
                        (field.name().replace('"', '\\"'), value.toString()))

                # Exclude same feature by id
                found = [
                    f.id() for f in layer.getFeatures(request)
                    if f.id() != feature.id()
                ]
                if len(found) > 0:
                    _set_error(field.name(), _('Field value must be UNIQUE'))

        # Check for expressions, even in case of NULL because expressions may want to check for combined
        # conditions on multiple fields
        expression = (field.constraints().constraintOrigin(
            QgsFieldConstraints.ConstraintExpression) !=
                      QgsFieldConstraints.ConstraintOriginNotSet
                      and field.constraints().constraintStrength(
                          QgsFieldConstraints.ConstraintExpression)
                      == QgsFieldConstraints.ConstraintStrengthHard)
        if expression:
            constraints = field.constraints()
            expression = constraints.constraintExpression()
            description = constraints.constraintDescription()
            value = feature.attribute(field_index)
            exp = QgsExpression(expression)
            context = QgsExpressionContextUtils.createFeatureBasedContext(
                feature, layer.fields())
            context.appendScopes(
                QgsExpressionContextUtils.globalProjectLayerScopes(layer))
            if not bool(exp.evaluate(context)):
                if not description:
                    description = _('Expression check violation')
                _set_error(field.name(),
                           _("%s Expression: %s") % (description, expression))

    return errors
Ejemplo n.º 7
0
def mapLayerAttributesFromQgisLayer(qgis_layer, **kwargs):
    """
    map QGIS layer's simple and direct field to Attributes for client editing system
    only concrete field not virtual field and many2many
    """

    # Set fields to exclude
    fieldsToExclude = kwargs['exclude'] if 'exclude' in kwargs else []

    toRes = OrderedDict()
    fields = qgis_layer.fields()

    data_provider = qgis_layer.dataProvider()

    field_index = 0

    pk_attributes = qgis_layer.primaryKeyAttributes()

    # Determine if we are using an old and bugged version of QGIS
    IS_QGIS_3_10 = Qgis.QGIS_VERSION.startswith('3.10')

    # FIXME: find better way for layer join 1:1 managment
    for field in fields:
        if field.name() not in fieldsToExclude and field.name(
        ) in kwargs['fields']:
            #internal_typename = field.typeName().split('(')[0]
            internal_typename = QVariant.typeToName(field.type()).upper()
            if internal_typename in FIELD_TYPES_MAPPING:

                # Get constraints and default clause to define if the field is editable
                # or set editable property by kwargs.
                # Only consider "strong" constraints
                constraints = qgis_layer.fieldConstraints(field_index)
                not_null = bool(constraints & QgsFieldConstraints.ConstraintNotNull) and \
                    field.constraints().constraintStrength(
                        QgsFieldConstraints.ConstraintNotNull) == QgsFieldConstraints.ConstraintStrengthHard
                unique = bool(constraints & QgsFieldConstraints.ConstraintUnique) and \
                    field.constraints().constraintStrength(
                        QgsFieldConstraints.ConstraintUnique) == QgsFieldConstraints.ConstraintStrengthHard
                has_expression = bool(constraints & QgsFieldConstraints.ConstraintExpression) and \
                    field.constraints().constraintStrength(
                        QgsFieldConstraints.ConstraintExpression) == QgsFieldConstraints.ConstraintStrengthHard
                default_clause = data_provider.defaultValueClause(field_index)

                # default value for editing from qgis_layer
                if 'default' not in kwargs:
                    default_value = qgis_layer.defaultValue(field_index) if qgis_layer.defaultValue(field_index) \
                        else None
                else:
                    default_value = kwargs['default']

                if isinstance(default_value, QDate) or isinstance(
                        default_value, QDateTime):
                    try:
                        default_value = default_value.toString(
                            kwargs['fields'][field.name()]['input']['options']
                            ['formats'][0]['displayformat'])
                    except Exception as e:
                        default_value = ''

                expression = ''
                if has_expression:
                    expression = field.constraints().constraintExpression()

                # Check for defaultValueDefinition with expression
                # 2021/10/04 snippet by Alessandro Pasotti (elpaso)
                has_default_value_expression = False
                if field.defaultValueDefinition().expression() != '':
                    exp = QgsExpression(
                        field.defaultValueDefinition().expression())
                    has_default_value_expression = exp.rootNode().nodeType(
                    ) != QgsExpressionNode.ntLiteral

                if not_null and unique and default_clause or has_default_value_expression:
                    editable = False
                else:
                    editable = kwargs['fields'][field.name()]['editable']

                # remove editable from kwargs:
                del (kwargs['fields'][field.name()]['editable'])

                comment = field.comment() if field.comment() else field.name()
                fieldType = FIELD_TYPES_MAPPING[internal_typename]

                if IS_QGIS_3_10:
                    is_pk = unique and default_clause and not_null
                else:
                    is_pk = (field_index in pk_attributes)

                toRes[field.name()] = editingFormField(
                    field.name(),
                    required=not_null,
                    fieldLabel=comment,
                    type=fieldType,
                    inputType=FORM_FIELDS_MAPPING[fieldType],
                    editable=editable,
                    default_clause=default_clause,
                    unique=unique,
                    expression=expression,
                    pk=is_pk,
                    default=default_value)

                # add upload url to image type if module is set
                if 'editing' in settings.G3WADMIN_LOCAL_MORE_APPS:
                    if fieldType == FIELD_TYPE_IMAGE:
                        toRes[field.name()].update(
                            {'uploadurl': reverse('editing-upload')})

                # update with fields configs data
                if 'fields' in kwargs and field.name() in kwargs['fields']:
                    deepupdate(toRes[field.name()],
                               kwargs['fields'][field.name()])
                    if fieldType == FIELD_TYPE_BOOLEAN:
                        toRes[field.name()]['input']['options']['values'] = [{
                            'checked':
                            True,
                            'value':
                            True
                        }, {
                            'checked':
                            False,
                            'value':
                            False
                        }]

        field_index += 1

    return toRes
Ejemplo n.º 8
0
def mapLayerAttributesFromQgisLayer(qgis_layer, **kwargs):
    """
    map QGIS layer's simple and direct field to Attributes for client editing system
    only concrete field not virtual field and many2many
    """

    fieldsToExclude = kwargs[
        'fieldsToExclude'] if 'fieldsToExclude' in kwargs else []

    toRes = OrderedDict()
    fields = qgis_layer.fields()

    data_provider = qgis_layer.dataProvider()

    # exclude if set:
    if 'exclude' in kwargs:
        _fieldsMapped = []
        for field in fields:
            if field.name not in kwargs['exclude']:
                _fieldsMapped.append(field)
        fields = _fieldsMapped

    field_index = 0

    pk_attributes = qgis_layer.primaryKeyAttributes()

    # Determine if we are using an old and bugged version of QGIS
    IS_QGIS_3_10 = Qgis.QGIS_VERSION.startswith('3.10')

    # FIXME: find better way for layer join 1:1 managment
    for field in fields:
        if field.name() not in fieldsToExclude and field.name(
        ) in kwargs['fields']:
            #internal_typename = field.typeName().split('(')[0]
            internal_typename = QVariant.typeToName(field.type()).upper()
            if internal_typename in FIELD_TYPES_MAPPING:

                # Get constraints and default clause to define if the field is editable
                # or set editable property by kwargs.
                # Only consider "strong" constraints
                constraints = qgis_layer.fieldConstraints(field_index)
                not_null = bool(constraints & QgsFieldConstraints.ConstraintNotNull) and \
                    field.constraints().constraintStrength(
                        QgsFieldConstraints.ConstraintNotNull) == QgsFieldConstraints.ConstraintStrengthHard
                unique = bool(constraints & QgsFieldConstraints.ConstraintUnique) and \
                    field.constraints().constraintStrength(
                        QgsFieldConstraints.ConstraintUnique) == QgsFieldConstraints.ConstraintStrengthHard
                has_expression = bool(constraints & QgsFieldConstraints.ConstraintExpression) and \
                    field.constraints().constraintStrength(
                        QgsFieldConstraints.ConstraintExpression) == QgsFieldConstraints.ConstraintStrengthHard
                default_clause = data_provider.defaultValueClause(field_index)

                expression = ''
                if has_expression:
                    expression = field.constraints().constraintExpression()

                if not_null and unique and default_clause:
                    editable = False
                else:
                    editable = kwargs['fields'][field.name()]['editable']

                # remove editable from kwargs:
                del (kwargs['fields'][field.name()]['editable'])

                comment = field.comment() if field.comment() else field.name()
                fieldType = FIELD_TYPES_MAPPING[internal_typename]

                if IS_QGIS_3_10:
                    is_pk = unique and default_clause and not_null
                else:
                    is_pk = (field_index in pk_attributes)

                toRes[field.name()] = editingFormField(
                    field.name(),
                    required=not_null,
                    fieldLabel=comment,
                    type=fieldType,
                    inputType=FORM_FIELDS_MAPPING[fieldType],
                    editable=editable,
                    default_clause=default_clause,
                    unique=unique,
                    expression=expression,
                    pk=is_pk)

                # add upload url to image type if module is set
                if 'editing' in settings.G3WADMIN_LOCAL_MORE_APPS:
                    if fieldType == FIELD_TYPE_IMAGE:
                        toRes[field.name()].update(
                            {'uploadurl': reverse('editing-upload')})
                    if fieldType == FIELD_TYPE_BOOLEAN:
                        toRes[field.name()]['input']['options'].update({
                            'values': [{
                                'key': _('Yes'),
                                'value': True
                            }, {
                                'key': 'No',
                                'value': False
                            }]
                        })

                # update with fields configs data
                if 'fields' in kwargs and field.name() in kwargs['fields']:
                    deepupdate(toRes[field.name()],
                               kwargs['fields'][field.name()])

        field_index += 1

    return toRes
Ejemplo n.º 9
0
def add_geojson_features(geojson,
                         project,
                         qgis_layer_id=None,
                         connection_id=None,
                         new_layer_name=None,
                         name=None,
                         style=None):
    """Add geojson features to a destination layer, the layer can be specified
    by passing a QgsVectorLayer instance or by specifying a connection and
    a new layer name plus the QDjango project for the new layer.

    The connection may assume the special values `__shapefile__`, `__spatialite__` or `__geopackage__`
    for the creation of new OGR files of the corresponding type.

    The creation of the new layer may raise an exception is a layer with the same name as
    new_layer_name already exists. For already existing layers the `qgis_layer_id` can be used,
    provided that the layer belongs to the current `project`.

    Returns the qgis_layer_id

    :param geojson: new features in GeoJSON format
    :type geojson: str
    :param project: QDjango Project instance for the new or the existing layer
    :type project: Project instance
    :param layer_id: optional, QGIS layer id
    :type layer_id: QGIS layer id
    :param connection: optional, connection id or the special value `__shapefile__`, `__spatialite__` or `__geopackage__`
    :type connection: str
    :param new_layer_name: optional, name of the new layer
    :type new_layer_name: str
    :param name: optional, name of the isochrone, default to current datetime
    :type name: str
    :param style: optional, dictionary with style properties: example {'color': [100, 50, 123], 'transparency': 0.5, 'stroke_width: 3 }
    :type style: dict
    :raises Exception: raise on error
    :rtype: str
    """

    # Additional fields that are not returned by the service as feature attributes
    json_data = json.loads(geojson)

    if name is None:
        name = "Isochrone %s" % QDateTime.currentDateTime().toString(
            Qt.ISODateWithMs)

    metadata = {
        'range_type': json_data['metadata']['query']['range_type'],
        'name': name,
        # 'timestamp': json_data['metadata']['timestamp'],  // Not supported
    }

    for f in json_data['features']:
        f['properties'].update(metadata)

    geojson = json.dumps(json_data)

    fields = QgsJsonUtils.stringToFields(geojson)

    # Patch timestamp type to DateTime // Not supported
    # fields.remove(fields.lookupField('timestamp'))
    #fields.append(QgsField('timestamp', QVariant.DateTime))

    # Create the new layer
    if connection_id is not None:

        def _write_to_ogr(destination_path, new_layer_name, driverName=None):
            """Writes features to new or existing OGR layer"""

            tmp_dir = QTemporaryDir()
            tmp_path = os.path.join(tmp_dir.path(), 'isochrone.json')
            with open(tmp_path, 'w+') as f:
                f.write(geojson)

            tmp_layer = QgsVectorLayer(tmp_path, 'tmp_isochrone', 'ogr')

            if not tmp_layer.isValid():
                raise Exception(
                    _('Cannot create temporary layer for isochrone result.'))

            # Note: shp attribute names are max 10 chars long
            save_options = QgsVectorFileWriter.SaveVectorOptions()
            if driverName is not None:
                save_options.driverName = driverName
            save_options.layerName = new_layer_name
            save_options.fileEncoding = 'utf-8'

            # This is nonsense to me: if the file does not exist the actionOnExistingFile
            # should be ignored instead of raising an error, probable QGIS bug
            if os.path.exists(destination_path):
                # Check if the layer already exists
                layer_exists = QgsVectorFileWriter.targetLayerExists(
                    destination_path, new_layer_name)

                if layer_exists:
                    raise Exception(
                        _('Cannot save isochrone result to destination layer: layer already exists (use "qgis_layer_id" instead)!'
                          ))

                save_options.actionOnExistingFile = QgsVectorFileWriter.CreateOrOverwriteLayer

            error_code, error_message = QgsVectorFileWriter.writeAsVectorFormatV2(
                tmp_layer, destination_path,
                project.qgis_project.transformContext(), save_options)

            if error_code != QgsVectorFileWriter.NoError:
                raise Exception(
                    _('Cannot save isochrone result to destination layer: ') +
                    error_message)

            layer_uri = destination_path

            if driverName != 'ESRI Shapefile':
                layer_uri += '|layername=' + new_layer_name

            provider = 'ogr'
            return layer_uri, provider

        destination_path = None

        if connection_id == '__shapefile__':  # new shapefile
            driverName = 'ESRI Shapefile'
            extension = 'shp'
        elif connection_id == '__spatialite__':  # new sqlite
            driverName = 'SpatiaLite'
            extension = 'sqlite'
        elif connection_id == '__geopackage__':  # new gpkg
            driverName = 'GPKG'
            extension = 'gpkg'
        else:  # Add new table to an existing DB connection

            try:
                connection = get_db_connections(
                    project.qgis_project)[connection_id]
            except:
                raise Exception(_('Wrong connection id.'))

            if connection['provider'] == 'ogr':
                destination_path = connection_id
                driverName = 'GPKG' if destination_path.lower().endswith(
                    '.gpkg') else 'SpatiaLite'
            else:
                driverName = None

        # Create a new file/layer
        if driverName is not None:
            new_layer_name = os.path.basename(new_layer_name)

            if destination_path is None:  # new files!
                destination_path = os.path.join(
                    settings.DATASOURCE_PATH,
                    "{}.{}".format(new_layer_name, extension))
                i = 0
                while os.path.exists(destination_path):
                    i += 1
                    destination_path = os.path.join(
                        settings.DATASOURCE_PATH,
                        "{}_{}.{}".format(new_layer_name, i, extension))

            layer_uri, provider = _write_to_ogr(destination_path,
                                                new_layer_name, driverName)

        # Create a new DB table
        else:
            assert connection['provider'] != 'ogr'
            md = QgsProviderRegistry.instance().providerMetadata(
                connection['provider'])
            if not md:
                raise Exception(
                    _('Error creating destination layer connection.'))
            conn = md.createConnection(connection_id, {})
            try:
                conn.createVectorTable(connection['schema'], new_layer_name,
                                       fields, QgsWkbTypes.Polygon,
                                       QgsCoordinateReferenceSystem(4326),
                                       False, {'geometryColumn': 'geom'})
            except QgsProviderConnectionException as ex:
                raise Exception(
                    _('Error creating destination layer: ') + str(ex))

            uri = QgsDataSourceUri(conn.uri())
            uri.setTable(new_layer_name)
            uri.setSchema(connection['schema'])
            uri.setSrid('4326')
            uri.setGeometryColumn('geom')
            provider = connection['provider']
            layer_uri = uri.uri()

        # Now reload the new layer and add it to the project
        qgis_layer = QgsVectorLayer(layer_uri, new_layer_name, provider)
        if not qgis_layer.isValid():
            raise Exception(
                _('Error creating destination layer: layer is not valid!'))

        qgis_layer_id = qgis_layer.id()

        with QgisProjectFileLocker(project) as project:
            apply_style(qgis_layer, style, True, name)
            project.qgis_project.addMapLayers([qgis_layer])
            root = project.qgis_project.layerTreeRoot()
            if qgis_layer_id not in root.findLayerIds():
                # Append layer at the end
                root.insertLayer(-1, qgis_layer)
            if not project.update_qgis_project():
                raise Exception(
                    _('Error saving the destination layer: could not write project!'
                      ))

        # Retrieve the layer again because saving the project deleted it
        qgis_layer = project.qgis_project.mapLayer(qgis_layer_id)

        # Create Layer object
        instance, created = Layer.objects.get_or_create(
            qgs_layer_id=qgis_layer_id,
            project=project,
            defaults={
                'origname':
                new_layer_name,
                'name':
                new_layer_name,
                'title':
                new_layer_name,
                'is_visible':
                True,
                'layer_type':
                provider,
                'srid':
                4326,
                'datasource':
                layer_uri,
                'geometrytype':
                'Polygon',
                # TODO: make this a property of the Layer object
                'database_columns':
                str([{
                    'name': f.name(),
                    'type': QVariant.typeToName(f.type()).upper(),
                    'label': f.displayName()
                } for f in qgis_layer.fields()]),
            })

        if not created:
            raise Exception(
                _('Error adding destination Layer to the project: layer already exists.'
                  ))

        # for OGR (already filled with features) returns the id of the new layer
        if driverName is not None:
            return qgis_layer_id

    # Append to an existing layer
    qgis_layer = project.qgis_project.mapLayer(qgis_layer_id)
    if qgis_layer is None:
        raise Exception(
            _('Error opening destination layer %s: layer not found in QGIS project!'
              % qgis_layer_id))

    features = QgsJsonUtils.stringToFeatureList(geojson, fields)

    compatible_features = []
    for f in features:
        compatible_features.extend(
            QgsVectorLayerUtils.makeFeatureCompatible(f, qgis_layer))

    if qgis_layer.crs().isValid(
    ) and qgis_layer.crs() != QgsCoordinateReferenceSystem(4326):
        ct = QgsCoordinateTransform(QgsCoordinateReferenceSystem(4326),
                                    qgis_layer.crs(), project.qgis_project)
        for f in compatible_features:
            geom = f.geometry()
            if geom.transform(ct) != QgsGeometry.Success:
                raise Exception(
                    _('Error transforming geometry from 4326 to destination layer CRS.'
                      ))
            f.setGeometry(geom)

    if len(compatible_features) == 0:
        raise Exception(_('No compatible features returned from isochrone.'))

    if not qgis_layer.startEditing():
        raise Exception(_('Destination layer is not editable.'))

    # Add features to the layer
    if not qgis_layer.addFeatures(compatible_features):
        raise Exception(_('Error adding features to the destination layer.'))

    if not qgis_layer.commitChanges():
        raise Exception(
            _('Error committing features to the destination layer.'))

    if style is not None:
        with QgisProjectFileLocker(project) as project:
            apply_style(qgis_layer, style, False, name)
            project.update_qgis_project()

    return qgis_layer_id