2

I'm learning PyQGIS (developer's cookbook) with QGIS version 3.14. I've encountered a problem I cannot fix when updating point feature attributes, including the geometry. The changes have no apparent effect on the attribute table or the canvas. I'm sure this is a beginner's issue, but I'm stuck and looking for insight.

I've tried multiple ways to update a feature, including several suggested on this site and settled with the transactional method on the layer:

def move_feature(self, feature, geometry, name_value):
    # Change geometry and name
    with edit(self.layer):
        self.layer.changeGeometry(feature.id(), geometry)
        self.layer.changeAttributeValue(feature.id(), name_idx, name_value)
        self.layer.updateFeature(feature)

The class this code belongs to creates a layer, a fixed point and a point which position is defined by random distance and azimuth relatively to the fixed point.

enter image description here

At each execution the code checks whether the layer already exists or must be created with the fixed point, and whether the moving point also exists and should only be moved (piece of code shown above), or doesn't exist and must be created from scratch.

My code works, except moving the feature, the update is not effective. I provide the full code here for reproduction:

from random import randint
from math import degrees, radians, pi

# A class which creates a layer, adds fixed points and manages moving points
class LayerModel():
    def __init__(self, locations, crs='EPSG:4326', ellipsoid='EPSG:7030'):
        # General data
        self.project = QgsProject.instance()
        self.layer_name = 'locations'
        
        # Attribute names
        self.name_key, self.class_key = 'name', 'class'
        self.attributes_names = (self.name_key, self.class_key)
        
        # Layer CRS
        self.crs = QgsCoordinateReferenceSystem(crs)
        if not self.crs.isValid(): raise(ValueError(f'Not able to create {crs} CRS'))
        
        # Calculator for ellipsoidal computations
        self.da_layer = QgsDistanceArea()
        self.da_layer.setSourceCrs(self.crs, self.project.transformContext())
        self.da_layer.setEllipsoid(ellipsoid)
        
        # Create layer and fixed features
        self.layer = self.initialize_layer(self.layer_name, locations)
        
    # Initialize layer
    def initialize_layer(self, layer_name, locations):
        # Check whether the layer already exists
        layers_found = self.project.mapLayersByName(layer_name)
        if layers_found is not None and len(layers_found) > 0:
            # Found, store the reference
            layer = layers_found[0]
        else:
            # Create layer and its fixed features
            layer = self.create_layer(layer_name, self.crs)
            self.create_features(layer, locations)
        
        return layer
    
    # Create a new point layer in memory
    def create_layer(self, layer_name, crs):
        layer = QgsVectorLayer('Point', layer_name, 'memory')
        layer.setCrs(crs)
        self.project.addMapLayer(layer)
        
        # Create attributes at the provider level
        provider = layer.dataProvider()
        provider.addAttributes([ QgsField(name, QVariant.String) for name in self.attributes_names ])
        
        # Migrate provider changes to layer
        layer.updateFields()
        
        return layer
        
    # Create features without checking they already exist
    def create_features(self, layer, locations):
        # Prepare features
        f_list = []
        class_value = 'fixed'
        for key, value in locations.items():
            f = QgsFeature()
            f.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(*value['coords'])))
            f.setAttributes([value['name'], class_value])
            f_list.append(f)
        # Add new features to layer
        with edit(layer):
            layer.addFeatures(f_list)
            
    # Return coordinates for a point at given distance and azimuth from another point
    def point_by_azimuth(self, origin, distance_m, azimuth_deg):
        pos = self.da_layer.computeSpheroidProject(origin, distance, radians(azimuth_deg))
        return pos
        
    # Convert the distance from CRS units to km
    def distance_km(self, distance):
        return self.da_layer.convertLengthMeasurement(distance, QgsUnitTypes.DistanceKilometers)
        
    # Create a point feature, or move it to new coordinates if it exists
    def create_or_move(self, xy, point_name='Random Location', class_value='random'):
        # Initial or new geometry
        geometry = QgsGeometry.fromPointXY(xy)
        
        # Try find a feature of this class
        request = QgsFeatureRequest(QgsExpression(f'\"{self.class_key}\" = \'{class_value}\''))
        found = list(self.layer.getFeatures(request))
        
        if len(found) > 0:
            # Move this feature
            self.move_feature(found[0], geometry, point_name)
        else:
            # Create a new feature
            self.create_feature(geometry, point_name, class_value)        
        
        # Update layer extent and canvas after feature addition / update
        self.layer.updateExtents()
        self.layer.triggerRepaint()
        
    # Create a new feature
    def create_feature(self, geometry, name_value, class_value):
        f_new = QgsFeature()
        f_new.setGeometry(geometry)
        f_new.setAttributes([name_value, class_value])
        
        # Add feature to layer
        with edit(self.layer):
            self.layer.addFeatures([f_new])

    # Move an existing feature
    def move_feature(self, feature, geometry, name_value):
        # Get name attribute index in the layer attribute table
        fields = self.layer.fields()
        name_idx = fields.indexFromName(self.name_key)
        
        # Change geometry and name
        with edit(self.layer):
            self.layer.changeGeometry(feature.id(), geometry)
            self.layer.changeAttributeValue(feature.id(), name_idx, name_value)
            self.layer.updateFeature(feature)
        
        p = geometry.asPoint()
        print(f'New position: {p.x():.2f}, {p.y():.2f}')


# Main
locations = { 'tenerife': { 'name': 'Tenerife', 'coords': (-16.5735, 28.0443) } }

# Create a layer with initial points
model = LayerModel(locations)

# Create or move a point at random distance and azimuth from Tenerife
center_key = 'tenerife'
origin_geom =  QgsPointXY(*locations[center_key]['coords'])
origin_name = locations[center_key]['name']

# Iniital or new position
distance = randint(1000, 5000) * 1e3
azimuth = randint(-180, 180)

# Give this point a name reflecting its position
distance_km = model.distance_km(distance)
point_name = f'{distance_km:.0f} km / {azimuth:.0f}° from {origin_name}'

random_xy = model.point_by_azimuth(origin_geom, distance, azimuth)
model.create_or_move(random_xy, point_name=point_name)

I thought the problem was linked to the type of layer (memory layer), but the problem persists after saving the layer as a shapefile and reloading it. In addition it's possible to edit the feature using the attribute table and to move the points with the vertex tool.

1 Answer 1

1

The line:

self.layer.updateFeature(feature)

is assumed to update the layer's feature identified by the id in feature object, with the geometry and attributes in feature object. But contrary to expectations, feature object hasn't been changed by the code prior to this line.The update is executed correctly, but with a feature which value is equal to the existing one, resulting in no apparent effect.

To actually update the feature object, this code:

def move_feature(self, feature, geometry, name_value):
    with edit(self.layer):
        self.layer.changeGeometry(feature.id(), geometry)
        self.layer.changeAttributeValue(feature.id(), name_idx, name_value)

must be replaced by:

def move_feature(self, feature, geometry, name_value):
    with edit(self.layer):
        feature.setGeometry(geometry)
        feature.setAttribute(self.label_key, name_value)

then self.layer.updateFeature(feature) works as expected. Also see "6.4.4 Modifying Vector Layers with an Editing Buffer" of PyQGIS Cookbook, though the examples mixing different methods are rather confusing without an overall explanation.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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