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.
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.