def __snap(self): self.report_message.emit(self.layer_id, 'preparing ...') orig_layer = QgsMapLayerRegistry.instance().mapLayer(self.layer_id) # create a copy of the layer just for editing layer = QgsVectorLayer(orig_layer.source(), orig_layer.name(), orig_layer.providerType()) geom_type = layer.geometryType() # layer.wkbType() does not return reliable results wkb_type = layer.wkbType() if self.create_backup: self.report_message.emit(self.layer_id, 'creating backup ...') self.__create_backup_file(orig_layer) self.report_message.emit(self.layer_id, 'preparing ...') layer.startEditing() request = QgsFeatureRequest().setFilterRect(self.snap_extent) total_features = 0 for feature in layer.getFeatures(request): total_features += 1 QgsMessageLog.logMessage(self.plugin.tr('Features to be snapped in layer <{0}>: {1}'). format(orig_layer.name(), total_features), self.plugin.tr('Vertex Tools'), QgsMessageLog.INFO) if total_features == 0: self.report_message.emit(self.layer_id, 'no features') count = 0 for feature in layer.getFeatures(request): with QMutexLocker(self.mutex): if self.stopped: layer.rollBack() return if geom_type == QGis.Point: snapped_geom = self.__point_grid(feature, wkb_type) elif geom_type == QGis.Line: snapped_geom = self.__line_grid(feature, wkb_type) elif geom_type == QGis.Polygon: snapped_geom = self.__polygon_grid(feature, wkb_type) layer.changeGeometry(feature.id(), snapped_geom) count += 1 self.run_progressed.emit(self.layer_id, count, total_features) layer.commitChanges() self.completed = True
def xtest_SplitFeatureWithFailedCommit(self): """Create spatialite database""" layer = QgsVectorLayer("dbname=%s table=test_pg_mk (geometry)" % self.dbname, "test_pg_mk", "spatialite") self.assertTrue(layer.isValid()) self.assertTrue(layer.hasGeometryType()) layer.startEditing() self.asserEqual(layer.splitFeatures([QgsPoint(0.5, -0.5), QgsPoint(0.5, 1.5)], 0), 0) self.asserEqual(layer.splitFeatures([QgsPoint(-0.5, 0.5), QgsPoint(1.5, 0.5)], 0), 0) self.assertFalse(layer.commitChanges()) layer.rollBack() feat = next(layer.getFeatures()) ref = [[(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)]] res = feat.geometry().asPolygon() for ring1, ring2 in zip(ref, res): for p1, p2 in zip(ring1, ring2): for c1, c2 in zip(p1, p2): self.asserEqual(c1, c2)
def testVectorLayerUtilsUniqueWithProviderDefault(self): vl = QgsVectorLayer('%s table="qgis_test"."someData" sql=' % (self.dbconn), "someData", "postgres") default_clause = 'nextval(\'qgis_test."someData_pk_seq"\'::regclass)' vl.dataProvider().setProviderProperty(QgsDataProvider.EvaluateDefaultValues, False) self.assertEqual(vl.dataProvider().defaultValueClause(0), default_clause) self.assertTrue(QgsVectorLayerUtils.valueExists(vl, 0, 4)) vl.startEditing() f = QgsFeature(vl.fields()) f.setAttribute(0, default_clause) self.assertFalse(QgsVectorLayerUtils.valueExists(vl, 0, default_clause)) self.assertTrue(vl.addFeatures([f])) # the default value clause should exist... self.assertTrue(QgsVectorLayerUtils.valueExists(vl, 0, default_clause)) # but it should not prevent the attribute being validated self.assertTrue(QgsVectorLayerUtils.validateAttribute(vl, f, 0)) vl.rollBack()
def xtest_SplitFeatureWithFailedCommit(self): """Create spatialite database""" layer = QgsVectorLayer("dbname=%s table=test_pg_mk (geometry)" % self.dbname, "test_pg_mk", "spatialite") assert(layer.isValid()) assert(layer.hasGeometryType()) layer.startEditing() layer.splitFeatures([QgsPoint(0.5, -0.5), QgsPoint(0.5, 1.5)], 0) == 0 or die("error in split") layer.splitFeatures([QgsPoint(-0.5, 0.5), QgsPoint(1.5, 0.5)], 0) == 0 or die("error in split") if layer.commitChanges(): die("this commit should fail") layer.rollBack() feat = QgsFeature() it = layer.getFeatures() it.nextFeature(feat) ref = [[(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)]] res = feat.geometry().asPolygon() for ring1, ring2 in zip(ref, res): for p1, p2 in zip(ring1, ring2): for c1, c2 in zip(p1, p2): c1 == c2 or die("polygon has been altered by failed edition")
def test_FilterFids(self): # create point layer myShpFile = os.path.join(TEST_DATA_DIR, 'points.shp') pointLayer = QgsVectorLayer(myShpFile, 'Points', 'ogr') ids = [feat.id() for feat in pointLayer.getFeatures(QgsFeatureRequest().setFilterFids([7, 8, 12, 30]))] expectedIds = [7, 8, 12] self.assertEqual(set(ids), set(expectedIds)) pointLayer.startEditing() self.addFeatures(pointLayer) ids = [feat.id() for feat in pointLayer.getFeatures(QgsFeatureRequest().setFilterFids([-4, 7, 8, 12, 30]))] expectedIds = [-4, 7, 8, 12] self.assertEqual(set(ids), set(expectedIds)) pointLayer.rollBack() ids = [feat.id() for feat in pointLayer.getFeatures(QgsFeatureRequest().setFilterFids([-2, 7, 8, 12, 30]))] expectedIds = [7, 8, 12] self.assertEqual(set(ids), set(expectedIds))
def __restore_geometries(self): src_file_name = self.backup_path + QDir.separator() + self.layer_id + '.shp' if not QFile.exists(src_file_name): self.report_message.emit(self.layer_id, 'no backup file') self.completed = True return self.report_message.emit(self.layer_id, 'preparing ...') orig_layer = QgsMapLayerRegistry.instance().mapLayer(self.layer_id) # create a copy of the layer just for editing layer = QgsVectorLayer(orig_layer.source(), orig_layer.name(), orig_layer.providerType()) layer.startEditing() src_layer = QgsVectorLayer(src_file_name, layer.name(), 'ogr') total_features = src_layer.featureCount() QgsMessageLog.logMessage(self.plugin.tr('Features to be restored in layer <{0}>: {1}'). format(orig_layer.name(), total_features), self.plugin.tr('Vertex Tools'), QgsMessageLog.INFO) if total_features == 0: self.report_message.emit(self.layer_id, 'no features to restore') count = 0 for feature in src_layer.getFeatures(): with QMutexLocker(self.mutex): if self.stopped: layer.rollBack() return layer.changeGeometry(feature.id(), feature.geometry()) count += 1 self.run_progressed.emit(self.layer_id, count, total_features) layer.commitChanges() self.completed = True
def test_FilterExpression(self): # create point layer myShpFile = os.path.join(TEST_DATA_DIR, 'points.shp') pointLayer = QgsVectorLayer(myShpFile, 'Points', 'ogr') ids = [feat.id() for feat in pointLayer.getFeatures(QgsFeatureRequest().setFilterExpression('Staff > 3'))] expectedIds = [1, 5, 6, 7, 8] myMessage = '\nExpected: {0} features\nGot: {1} features'.format(repr(expectedIds), repr(ids)) assert ids == expectedIds, myMessage pointLayer.startEditing() self.addFeatures(pointLayer) ids = [feat.id() for feat in pointLayer.getFeatures(QgsFeatureRequest().setFilterExpression('Staff > 3'))] expectedIds = [-2, 1, 5, 6, 7, 8] myMessage = '\nExpected: {0} features\nGot: {1} features'.format(repr(expectedIds), repr(ids)) assert ids == expectedIds, myMessage pointLayer.rollBack() ids = [feat.id() for feat in pointLayer.getFeatures(QgsFeatureRequest().setFilterExpression('Staff > 3'))] expectedIds = [1, 5, 6, 7, 8] myMessage = '\nExpected: {0} features\nGot: {1} features'.format(repr(expectedIds), repr(ids)) assert ids == expectedIds, myMessage
def test_FilterFids(self): # create point layer myShpFile = os.path.join(TEST_DATA_DIR, "points.shp") pointLayer = QgsVectorLayer(myShpFile, "Points", "ogr") ids = [feat.id() for feat in pointLayer.getFeatures(QgsFeatureRequest().setFilterFids([7, 8, 12, 30]))] expectedIds = [7, 8, 12] myMessage = "\nExpected: {0} features\nGot: {1} features".format(repr(expectedIds), repr(ids)) assert ids == expectedIds, myMessage pointLayer.startEditing() self.addFeatures(pointLayer) ids = [feat.id() for feat in pointLayer.getFeatures(QgsFeatureRequest().setFilterFids([-4, 7, 8, 12, 30]))] expectedIds = [-4, 7, 8, 12] myMessage = "\nExpected: {0} features\nGot: {1} features".format(repr(expectedIds), repr(ids)) assert ids == expectedIds, myMessage pointLayer.rollBack() ids = [feat.id() for feat in pointLayer.getFeatures(QgsFeatureRequest().setFilterFids([-2, 7, 8, 12, 30]))] expectedIds = [7, 8, 12] myMessage = "\nExpected: {0} features\nGot: {1} features".format(repr(expectedIds), repr(ids)) assert ids == expectedIds, myMessage
def testUpdateFeature(self): p = QgsProject() vectorFileInfo = QFileInfo(unitTestDataPath() + "/france_parts.shp") vector_layer = QgsVectorLayer(vectorFileInfo.filePath(), vectorFileInfo.completeBaseName(), "ogr") self.assertTrue(vector_layer.isValid()) p.addMapLayer(vector_layer) l = QgsPrintLayout(p) atlas = l.atlas() atlas.setEnabled(True) atlas.setCoverageLayer(vector_layer) self.assertTrue(atlas.beginRender()) self.assertTrue(atlas.first()) self.assertEqual(atlas.currentFeatureNumber(), 0) self.assertEqual(l.reportContext().feature()[4], 'Basse-Normandie') self.assertEqual(l.reportContext().layer(), vector_layer) vector_layer.startEditing() self.assertTrue(vector_layer.changeAttributeValue(l.reportContext().feature().id(), 4, 'Nah, Canberra mate!')) self.assertEqual(l.reportContext().feature()[4], 'Basse-Normandie') l.atlas().refreshCurrentFeature() self.assertEqual(l.reportContext().feature()[4], 'Nah, Canberra mate!') vector_layer.rollBack()
class Worker(QtCore.QObject): '''The worker that does the heavy lifting. /* QGIS offers spatial indexes to make spatial search more * effective. QgsSpatialIndex will find the nearest index * (approximate) geometry (rectangle) for a supplied point. * QgsSpatialIndex will only give correct results when searching * for the nearest neighbour of a point in a point data set. * So something has to be done for non-point data sets * * Non-point join data set: * A two pass search is performed. First the index is used to * find the nearest index geometry (approximation - rectangle), * and then compute the distance to the actual indexed geometry. * A rectangle is constructed from this (maximum minimum) * distance, and this rectangle is used to find all features in * the join data set that may be the closest feature to the given * point. * For all the features is this candidate set, the actual * distance to the given point is calculated, and the nearest * feature is returned. * * Non-point input data set: * First the centroid of the non-point input geometry is * calculated. Then the index is used to find the nearest * neighbour to this point (using the approximate index * geometry). * The distance vector to this feature, combined with the * bounding rectangle of the input feature is used to create a * search rectangle to find the candidate join geometries. * For all the features is this candidate set, the actual * distance to the given feature is calculated, and the nearest * feature is returned. * * Joins involving multi-geometry datasets are not supported * by a spatial index. * */ ''' # Define the signals used to communicate back to the application progress = QtCore.pyqtSignal(float) # For reporting progress status = QtCore.pyqtSignal(str) # For reporting status error = QtCore.pyqtSignal(str) # For reporting errors # Signal for sending over the result: finished = QtCore.pyqtSignal(bool, object) def __init__(self, inputvectorlayer, joinvectorlayer, outputlayername, joinprefix, distancefieldname="distance", approximateinputgeom=False, usejoinlayerapproximation=False, usejoinlayerindex=True, selectedinputonly=True, selectedjoinonly=True, excludecontaining=True): """Initialise. Arguments: inputvectorlayer -- (QgsVectorLayer) The base vector layer for the join joinvectorlayer -- (QgsVectorLayer) the join layer outputlayername -- (string) the name of the output memory layer joinprefix -- (string) the prefix to use for the join layer attributes in the output layer distancefieldname -- name of the (new) field where neighbour distance is stored approximateinputgeom -- (boolean) should the input geometry be approximated? Is only be set for non-single-point layers usejoinlayerindexapproximation -- (boolean) should the index geometry approximations be used for the join? usejoinlayerindex -- (boolean) should an index for the join layer be used. selectedinputonly -- Only selected features from the input layer selectedjoinonly -- Only selected features from the join layer excludecontaining -- exclude the containing polygon for points """ QtCore.QObject.__init__(self) # Essential! # Set a variable to control the use of indexes and exact # geometries for non-point input geometries self.nonpointexactindex = usejoinlayerindex # Creating instance variables from the parameters self.inpvl = inputvectorlayer self.joinvl = joinvectorlayer self.outputlayername = outputlayername self.joinprefix = joinprefix self.approximateinputgeom = approximateinputgeom self.usejoinlayerapprox = usejoinlayerapproximation self.selectedinonly = selectedinputonly self.selectedjoonly = selectedjoinonly self.excludecontaining = excludecontaining # Check if the layers are the same (self join) self.selfjoin = False if self.inpvl is self.joinvl: # This is a self join self.selfjoin = True # The name of the attribute for the calculated distance self.distancename = distancefieldname # Creating instance variables for the progress bar ++ # Number of elements that have been processed - updated by # calculate_progress self.processed = 0 # Current percentage of progress - updated by # calculate_progress self.percentage = 0 # Flag set by kill(), checked in the loop self.abort = False # Number of features in the input layer - used by # calculate_progress (set when needed) self.feature_count = 1 # The number of elements that is needed to increment the # progressbar (set when needed) self.increment = 0 def run(self): try: # Check if the layers look OK if self.inpvl is None or self.joinvl is None: self.status.emit('Layer is missing!') self.finished.emit(False, None) return # Check if there are features in the layers incount = 0 if self.selectedinonly: incount = self.inpvl.selectedFeatureCount() else: incount = self.inpvl.featureCount() if incount == 0: self.status.emit('Input layer has no features!') self.finished.emit(False, None) return joincount = 0 if self.selectedjoonly: joincount = self.joinvl.selectedFeatureCount() else: joincount = self.joinvl.featureCount() if joincount == 0: self.status.emit('Join layer has no features!') self.finished.emit(False, None) return # Get the wkbtype of the layers self.inpWkbType = self.inpvl.wkbType() self.joinWkbType = self.joinvl.wkbType() # Check if the input layer does not have geometries if (self.inpvl.geometryType() == QgsWkbTypes.NullGeometry): self.status.emit('No geometries in the input layer!') self.finished.emit(False, None) return # Check if the join layer does not have geometries if (self.joinvl.geometryType() == QgsWkbTypes.NullGeometry): self.status.emit('No geometries in the join layer!') self.finished.emit(False, None) return # Set the geometry type and prepare the output layer inpWkbTypetext = QgsWkbTypes.displayString(int(self.inpWkbType)) # self.inputmulti = QgsWkbTypes.isMultiType(self.inpWkbType) # self.status.emit('wkbtype: ' + inpWkbTypetext) # geometryType = self.inpvl.geometryType() # geometrytypetext = 'Point' # if geometryType == QgsWkbTypes.PointGeometry: # geometrytypetext = 'Point' # elif geometryType == QgsWkbTypes.LineGeometry: # geometrytypetext = 'LineString' # elif geometryType == QgsWkbTypes.PolygonGeometry: # geometrytypetext = 'Polygon' # if self.inputmulti: # geometrytypetext = 'Multi' + geometrytypetext # geomttext = geometrytypetext geomttext = inpWkbTypetext # Set the coordinate reference system to the input # layer's CRS using authid (proj4 may be more robust) if self.inpvl.crs() is not None: geomttext = (geomttext + "?crs=" + str(self.inpvl.crs().authid())) # Retrieve the fields from the input layer outfields = self.inpvl.fields().toList() # Retrieve the fields from the join layer if self.joinvl.fields() is not None: jfields = self.joinvl.fields().toList() for joinfield in jfields: outfields.append(QgsField(self.joinprefix + str(joinfield.name()), joinfield.type())) else: self.status.emit('Unable to get any join layer fields') # Add the nearest neighbour distance field # Check if there is already a "distance" field # (should be avoided in the user interface) # Try a new name if there is a collission collission = True trynumber = 1 distnameorg = self.distancename while collission: # Iterate until there are no collissions collission = False for field in outfields: # This check should not be necessary - handled in the UI if field.name() == self.distancename: self.status.emit( 'Distance field already exists - renaming!') # self.abort = True # self.finished.emit(False, None) # break collission = True self.distancename = distnameorg + str(trynumber) trynumber = trynumber + 1 outfields.append(QgsField(self.distancename, QVariant.Double)) # Create a memory layer using a CRS description self.mem_joinl = QgsVectorLayer(geomttext, self.outputlayername, "memory") # Set the CRS to the inputlayer's CRS self.mem_joinl.setCrs(self.inpvl.crs()) self.mem_joinl.startEditing() # Add the fields for field in outfields: self.mem_joinl.dataProvider().addAttributes([field]) # For an index to be used, the input layer has to be a # point layer, or the input layer geometries have to be # approximated to centroids, or the user has to have # accepted that a join layer index is used (for # non-point input layers). # (Could be extended to multipoint) if (self.inpWkbType == QgsWkbTypes.Point or self.inpWkbType == QgsWkbTypes.Point25D or self.approximateinputgeom or self.nonpointexactindex): # Number of features in the join layer - used by # calculate_progress for the index creation if self.selectedjoonly: self.feature_count = self.joinvl.selectedFeatureCount() else: self.feature_count = self.joinvl.featureCount() # Create a spatial index to speed up joining self.status.emit('Creating join layer index...') # The number of elements that is needed to increment the # progressbar - set early in run() self.increment = self.feature_count // 1000 self.joinlind = QgsSpatialIndex() # Include geometries to enable exact distance calculations # self.joinlind = QgsSpatialIndex(flags=[QgsSpatialIndex.FlagStoreFeatureGeometries]) if self.selectedjoonly: for feat in self.joinvl.getSelectedFeatures(): # Allow user abort if self.abort is True: break self.joinlind.insertFeature(feat) self.calculate_progress() else: for feat in self.joinvl.getFeatures(): # Allow user abort if self.abort is True: break self.joinlind.insertFeature(feat) self.calculate_progress() self.status.emit('Join layer index created!') self.processed = 0 self.percentage = 0 # self.calculate_progress() # Is the join layer a multi-geometry layer? # self.joinmulti = QgsWkbTypes.isMultiType(self.joinWkbType) # Does the join layer contain multi geometries? # Try to check the first feature # This is not used for anything yet self.joinmulti = False if self.selectedjoonly: feats = self.joinvl.getSelectedFeatures() else: feats = self.joinvl.getFeatures() if feats is not None: testfeature = next(feats) feats.rewind() feats.close() if testfeature is not None: if testfeature.hasGeometry(): if testfeature.geometry().isMultipart(): self.joinmulti = True # Prepare for the join by fetching the layers into memory # Add the input features to a list self.inputf = [] if self.selectedinonly: for f in self.inpvl.getSelectedFeatures(): self.inputf.append(f) else: for f in self.inpvl.getFeatures(): self.inputf.append(f) # Add the join features to a list (used in the join) self.joinf = [] if self.selectedjoonly: for f in self.joinvl.getSelectedFeatures(): self.joinf.append(f) else: for f in self.joinvl.getFeatures(): self.joinf.append(f) # Initialise the global variable that will contain the # result of the nearest neighbour spatial join (list of # features) self.features = [] # Do the join! # Number of features in the input layer - used by # calculate_progress for the join operation if self.selectedinonly: self.feature_count = self.inpvl.selectedFeatureCount() else: self.feature_count = self.inpvl.featureCount() # The number of elements that is needed to increment the # progressbar - set early in run() self.increment = self.feature_count // 1000 # Using the original features from the input layer for feat in self.inputf: # Allow user abort if self.abort is True: break self.do_indexjoin(feat) self.calculate_progress() self.mem_joinl.dataProvider().addFeatures(self.features) self.status.emit('Join finished') except: import traceback self.error.emit(traceback.format_exc()) self.finished.emit(False, None) if self.mem_joinl is not None: self.mem_joinl.rollBack() else: self.mem_joinl.commitChanges() if self.abort: self.finished.emit(False, None) else: self.status.emit('Delivering the memory layer...') self.finished.emit(True, self.mem_joinl) def calculate_progress(self): '''Update progress and emit a signal with the percentage''' self.processed = self.processed + 1 # update the progress bar at certain increments if (self.increment == 0 or self.processed % self.increment == 0): # Calculate percentage as integer perc_new = (self.processed * 100) / self.feature_count if perc_new > self.percentage: self.percentage = perc_new self.progress.emit(self.percentage) def kill(self): '''Kill the thread by setting the abort flag''' self.abort = True def do_indexjoin(self, feat): '''Find the nearest neigbour of a feature. Using an index, if possible Parameter: feat -- The feature for which a neighbour is sought ''' infeature = feat # Get the feature ID infeatureid = infeature.id() # self.status.emit('**infeatureid: ' + str(infeatureid)) # Get the feature geometry inputgeom = infeature.geometry() # Check for missing input geometry if inputgeom.isEmpty(): # Prepare the result feature atMapA = infeature.attributes() atMapB = [] for thefield in self.joinvl.fields(): atMapB.extend([None]) attrs = [] attrs.extend(atMapA) attrs.extend(atMapB) attrs.append(0 - float("inf")) # Create the feature outFeat = QgsFeature() # Use the original input layer geometry!: outFeat.setGeometry(infeature.geometry()) # Use the modified input layer geometry (could be # centroid) # outFeat.setGeometry(inputgeom) # Add the attributes outFeat.setAttributes(attrs) # self.calculate_progress() self.features.append(outFeat) # self.mem_joinl.dataProvider().addFeatures([outFeat]) self.status.emit("Warning: Input feature with " "missing geometry: " + str(infeature.id())) return # Shall approximate input geometries be used? if self.approximateinputgeom: # Use the centroid as the input geometry inputgeom = infeature.geometry().centroid() # Check if the coordinate systems are equal, if not, # transform the input feature! if (self.inpvl.crs() != self.joinvl.crs()): try: # inputgeom.transform(QgsCoordinateTransform( # self.inpvl.crs(), self.joinvl.crs(), None)) # transcontext = QgsCoordinateTransformContext() # inputgeom.transform(QgsCoordinateTransform( # self.inpvl.crs(), self.joinvl.crs(), transcontext)) inputgeom.transform(QgsCoordinateTransform( self.inpvl.crs(), self.joinvl.crs(), QgsProject.instance())) except: import traceback self.error.emit(self.tr('CRS Transformation error!') + ' - ' + traceback.format_exc()) self.abort = True return # Find the closest feature! nnfeature = None minfound = False mindist = float("inf") # If the input layer's geometry type is point, or has been # approximated to point (centroid), then a join index will # be used. # if ((QgsWkbTypes.geometryType(self.inpWkbType) == QgsWkbTypes.PointGeometry and # not QgsWkbTypes.isMultiType(self.inpWkbType)) or self.approximateinputgeom): if (self.approximateinputgeom or self.inpWkbType == QgsWkbTypes.Point or self.inpWkbType == QgsWkbTypes.Point25D): # Are there points on the join side? # Then the index nearest neighbour function is sufficient # if ((QgsWkbTypes.geometryType(self.joinWkbType) == QgsWkbTypes.PointGeometry and # not QgsWkbTypes.isMultiType(self.joinWkbType)) or self.usejoinlayerapprox): if (self.usejoinlayerapprox or self.joinWkbType == QgsWkbTypes.Point or self.joinWkbType == QgsWkbTypes.Point25D): # Is it a self join? if self.selfjoin: # Have to consider the two nearest neighbours nearestids = self.joinlind.nearestNeighbor( inputgeom.asPoint(), 2) fch = 0 # Which of the two features to choose if (nearestids[0] == infeatureid and len(nearestids) > 1): # The first feature is the same as the input # feature, so choose the second one fch = 1 # Get the feature! if False: #if self.selectedjoonly: # This caused problems (wrong results) in QGIS 3.0.1 nnfeature = next( self.joinvl.getSelectedFeatures( QgsFeatureRequest(nearestids[fch]))) else: nnfeature = next(self.joinvl.getFeatures( QgsFeatureRequest(nearestids[fch]))) # Not a self join else: # Not a self join, so we search for only the # nearest neighbour (1) nearestids = self.joinlind.nearestNeighbor( inputgeom.asPoint(), 1) # Get the feature! if len(nearestids) > 0: nearestid = nearestids[0] nnfeature = next(self.joinvl.getFeatures( QgsFeatureRequest(nearestid))) #else: #if self.selectedjoonly: # nnfeature = next(self.joinvl.getSelectedFeatures( # QgsFeatureRequest(nearestid))) if nnfeature is not None: mindist = inputgeom.distance(nnfeature.geometry()) minfound = True # Not points on the join side # Handle common (non multi) non-point geometries elif (self.joinWkbType == QgsWkbTypes.Polygon or self.joinWkbType == QgsWkbTypes.Polygon25D or self.joinWkbType == QgsWkbTypes.LineString or self.joinWkbType == QgsWkbTypes.LineString25D): # Use the join layer index to speed up the join when # the join layer geometry type is polygon or line # and the input layer geometry type is point or a # point approximation nearestids = self.joinlind.nearestNeighbor( inputgeom.asPoint(), 1) # Possibe index out of range!!! ??? nearestindexid = nearestids[0] # Check for self join (possible if approx input) if self.selfjoin and nearestindexid == infeatureid: # Self join and same feature, so get the # first two neighbours nearestindexes = self.joinlind.nearestNeighbor( inputgeom.asPoint(), 2) # Possibe index out of range!!! ??? nearestindexid = nearestindexes[0] if (nearestindexid == infeatureid and len(nearestindexes) > 1): nearestindexid = nearestindexes[1] # If exclude containing, check for containment if self.excludecontaining: contained = False nearfeature = next(self.joinvl.getFeatures( QgsFeatureRequest(nearestindexid))) # Check for containment if nearfeature.geometry().contains(inputgeom): contained = True if inputgeom.contains(nearfeature.geometry()): contained = True numberofnn = 2 # Assumes that nearestNeighbor returns hits in the same # sequence for all numbers of nearest neighbour while contained: if self.abort is True: break nearestindexes = self.joinlind.nearestNeighbor( inputgeom.asPoint(), numberofnn) if len(nearestindexes) < numberofnn: nearestindexid = nearestindexes[numberofnn - 2] self.status.emit('No non-containing geometries!') break else: nearestindexid = nearestindexes[numberofnn - 1] # Seems to respect selection...? nearfeature = next(self.joinvl.getFeatures( QgsFeatureRequest(nearestindexid))) # Check for containment # Works! if nearfeature.geometry().contains( inputgeom): contained = True elif inputgeom.contains( nearfeature.geometry()): contained = True else: contained = False numberofnn = numberofnn + 1 # end while # Get the feature among the candidates from the index #if self.selectedjoonly: # # Does not get the correct feature! # nnfeature = next(self.joinvl.getSelectedFeatures( # QgsFeatureRequest(nearestindexid))) # This seems to work also in the presence of selections nnfeature = next(self.joinvl.getFeatures( QgsFeatureRequest(nearestindexid))) mindist = inputgeom.distance(nnfeature.geometry()) if mindist == 0: insidep = nnfeature.geometry().contains( inputgeom.asPoint()) # self.status.emit('0 distance! - ' + str(nearestindexid)) # self.status.emit('Inside: ' + str(insidep)) px = inputgeom.asPoint().x() py = inputgeom.asPoint().y() # Search the neighbourhood closefids = self.joinlind.intersects(QgsRectangle( px - mindist, py - mindist, px + mindist, py + mindist)) for closefid in closefids: if self.abort is True: break # Check for self join and same feature if self.selfjoin and closefid == infeatureid: continue # If exclude containing, check for containment if self.excludecontaining: # Seems to respect selection...? closefeature = next(self.joinvl.getFeatures( QgsFeatureRequest(closefid))) # Check for containment if closefeature.geometry().contains( inputgeom.asPoint()): continue if False: #if self.selectedjoonly: closef = next(self.joinvl.getSelectedFeatures( QgsFeatureRequest(closefid))) else: closef = next(self.joinvl.getFeatures( QgsFeatureRequest(closefid))) thisdistance = inputgeom.distance(closef.geometry()) if thisdistance < mindist: mindist = thisdistance nnfeature = closef if mindist == 0: # self.status.emit(' Mindist = 0!') break # Other geometry on the join side (multi and more) else: # Join with no index use # Go through all the features from the join layer! for inFeatJoin in self.joinf: if self.abort is True: break joingeom = inFeatJoin.geometry() thisdistance = inputgeom.distance(joingeom) if thisdistance < 0: self.status.emit("Warning: Join feature with " "missing geometry: " + str(inFeatJoin.id())) continue # If the distance is 0, check for equality of the # features (in case it is a self join) if (thisdistance == 0 and self.selfjoin and infeatureid == inFeatJoin.id()): continue if thisdistance < mindist: mindist = thisdistance nnfeature = inFeatJoin # For 0 distance, settle with the first feature if mindist == 0: break # non (simple) point input geometries (could be multipoint) else: if (self.nonpointexactindex): # Use the spatial index on the join layer (default). # First we do an approximate search # Get the input geometry centroid centroid = infeature.geometry().centroid() centroidgeom = centroid.asPoint() # Find the nearest neighbour (index geometries only) # Possibe index out of range!!! ??? nearestid = self.joinlind.nearestNeighbor(centroidgeom, 1)[0] # Check for self join if self.selfjoin and nearestid == infeatureid: # Self join and same feature, so get the two # first two neighbours nearestindexes = self.joinlind.nearestNeighbor( centroidgeom, 2) nearestid = nearestindexes[0] if nearestid == infeatureid and len(nearestindexes) > 1: nearestid = nearestindexes[1] # Get the feature! if False: #if self.selectedjoonly: nnfeature = next(self.joinvl.getSelectedFeatures( QgsFeatureRequest(nearestid))) else: nnfeature = next(self.joinvl.getFeatures( QgsFeatureRequest(nearestid))) mindist = inputgeom.distance(nnfeature.geometry()) # Calculate the search rectangle (inputgeom BBOX inpbbox = infeature.geometry().boundingBox() minx = inpbbox.xMinimum() - mindist maxx = inpbbox.xMaximum() + mindist miny = inpbbox.yMinimum() - mindist maxy = inpbbox.yMaximum() + mindist # minx = min(inpbbox.xMinimum(), centroidgeom.x() - mindist) # maxx = max(inpbbox.xMaximum(), centroidgeom.x() + mindist) # miny = min(inpbbox.yMinimum(), centroidgeom.y() - mindist) # maxy = max(inpbbox.yMaximum(), centroidgeom.y() + mindist) searchrectangle = QgsRectangle(minx, miny, maxx, maxy) # Fetch the candidate join geometries closefids = self.joinlind.intersects(searchrectangle) # Loop through the geometries and choose the closest # one for closefid in closefids: if self.abort is True: break # Check for self join and identical feature if self.selfjoin and closefid == infeatureid: continue if False: #if self.selectedjoonly: closef = next(self.joinvl.getSelectedFeatures( QgsFeatureRequest(closefid))) else: closef = next(self.joinvl.getFeatures( QgsFeatureRequest(closefid))) thisdistance = inputgeom.distance(closef.geometry()) if thisdistance < mindist: mindist = thisdistance nnfeature = closef if mindist == 0: break else: # Join with no index use # Check all the features of the join layer! mindist = float("inf") # should not be necessary for inFeatJoin in self.joinf: if self.abort is True: break joingeom = inFeatJoin.geometry() thisdistance = inputgeom.distance(joingeom) if thisdistance < 0: self.status.emit("Warning: Join feature with " "missing geometry: " + str(inFeatJoin.id())) continue # If the distance is 0, check for equality of the # features (in case it is a self join) if (thisdistance == 0 and self.selfjoin and infeatureid == inFeatJoin.id()): continue if thisdistance < mindist: mindist = thisdistance nnfeature = inFeatJoin # For 0 distance, settle with the first feature if mindist == 0: break if not self.abort: # self.status.emit('Near feature - ' + str(nnfeature.id())) # Collect the attribute atMapA = infeature.attributes() if nnfeature is not None: atMapB = nnfeature.attributes() else: atMapB = [] for thefield in self.joinvl.fields(): atMapB.extend([None]) attrs = [] attrs.extend(atMapA) attrs.extend(atMapB) attrs.append(mindist) # Create the feature outFeat = QgsFeature() # Use the original input layer geometry!: outFeat.setGeometry(infeature.geometry()) # Use the modified input layer geometry (could be # centroid) # outFeat.setGeometry(inputgeom) # Add the attributes outFeat.setAttributes(attrs) # self.calculate_progress() self.features.append(outFeat) # self.mem_joinl.dataProvider().addFeatures([outFeat]) # end of do_indexjoin def tr(self, message): """Get the translation for a string using Qt translation API. We implement this ourselves since we do not inherit QObject. :param message: String for translation. :type message: str, QString :returns: Translated version of message. :rtype: QString """ # noinspection PyTypeChecker,PyArgumentList,PyCallByClass return QCoreApplication.translate('NNJoinEngine', message)
def test_check_gaps_in_plots(self): gpkg_path = get_test_copy_path('db/static/gpkg/quality_validations.gpkg') uri = gpkg_path + '|layername={layername}'.format(layername='check_gaps_in_plots') test_plots_layer = QgsVectorLayer(uri, 'check_gaps_in_plots', 'ogr') print('\nINFO: Validating Gaps in Plots using roads and multiple geometries...') gaps = GeometryUtils.get_gaps_in_polygon_layer(test_plots_layer, True) geometries = [g.asWkt() for g in gaps] expected_list = [ 'Polygon ((1001839.42949045938439667 1013500.23419545334763825, 1001838.68766217899974436 1013479.83391774445772171, 1001839.42949045938439667 1013450.16078653128352016, 1001855.74971262644976377 1013449.78987239114940166, 1001858.3461116076214239 1013430.87325124291237444, 1001885.42284383939113468 1013430.87325124291237444, 1001901.72405463655013591 1013411.57209242216777056, 1001910.64500537037383765 1013418.26217047742102295, 1001917.32145989337004721 1013392.29818066605366766, 1001845.19794039404951036 1013415.08188382943626493, 1001851.47861975431442261 1013424.31817700632382184, 1001833.74493685469496995 1013433.92392191023100168, 1001829.49624199338722974 1013421.7320149167208001, 1001839.42949045938439667 1013500.23419545334763825))', 'Polygon ((1001935.86716690135654062 1013432.35690780356526375, 1001921.03060129494406283 1013446.08073098957538605, 1001920.28877301455941051 1013475.7538622027495876, 1001957.38018703076522797 1013429.01868054212536663, 1001935.86716690135654062 1013432.35690780356526375))', 'Polygon ((1001935.86716690135654062 1013432.35690780356526375, 1001921.03060129494406283 1013446.08073098957538605, 1001920.28877301455941051 1013475.7538622027495876, 1001957.38018703076522797 1013429.01868054212536663, 1001935.86716690135654062 1013432.35690780356526375))', 'Polygon ((1001920.28877301455941051 1013475.7538622027495876, 1001861.31342472892720252 1013477.9793470436707139, 1001862.05525300919543952 1013498.37962475256063044, 1001920.28877301455941051 1013475.7538622027495876))', 'Polygon ((1001895.43752562382724136 1013467.22283697873353958, 1001907.30677810893394053 1013464.25552385742776096, 1001907.67769224906805903 1013454.2408420731080696, 1001895.43752562382724136 1013454.2408420731080696, 1001895.43752562382724136 1013467.22283697873353958))', 'Polygon ((1001847.96051568305119872 1013470.1901501000393182, 1001867.98987925180699676 1013469.07740767952054739, 1001869.10262167232576758 1013455.72449863376095891, 1001847.58960154291708022 1013455.72449863376095891, 1001847.96051568305119872 1013470.1901501000393182))'] for expected in expected_list: self.assertIn(expected, geometries) self.assertEqual(len(geometries), 5) print('\nINFO: Validating Gaps in Plots using roads for one geometry...') test_plots_layer.startEditing() test_plots_layer.deleteFeature(2) gaps = GeometryUtils.get_gaps_in_polygon_layer(test_plots_layer, True) geometries = [g.asWkt() for g in gaps] self.assertIn( 'Polygon ((1001895.43752562382724136 1013467.22283697873353958, 1001907.30677810893394053 1013464.25552385742776096, 1001907.67769224906805903 1013454.2408420731080696, 1001895.43752562382724136 1013454.2408420731080696, 1001895.43752562382724136 1013467.22283697873353958))', geometries) self.assertIn( 'Polygon ((1001847.96051568305119872 1013470.1901501000393182, 1001867.98987925180699676 1013469.07740767952054739, 1001869.10262167232576758 1013455.72449863376095891, 1001847.58960154291708022 1013455.72449863376095891, 1001847.96051568305119872 1013470.1901501000393182))', geometries) self.assertEqual(len(geometries), 2) test_plots_layer.rollBack() print('\nINFO: Validating Gaps in Plots without using roads and multiple geometries...') gaps = GeometryUtils.get_gaps_in_polygon_layer(test_plots_layer, False) geometries = [g.asWkt() for g in gaps] self.assertIn( 'Polygon ((1001895.43752562382724136 1013467.22283697873353958, 1001907.30677810893394053 1013464.25552385742776096, 1001907.67769224906805903 1013454.2408420731080696, 1001895.43752562382724136 1013454.2408420731080696, 1001895.43752562382724136 1013467.22283697873353958))', geometries) self.assertIn( 'Polygon ((1001847.96051568305119872 1013470.1901501000393182, 1001867.98987925180699676 1013469.07740767952054739, 1001869.10262167232576758 1013455.72449863376095891, 1001847.58960154291708022 1013455.72449863376095891, 1001847.96051568305119872 1013470.1901501000393182))', geometries) self.assertEqual(len(geometries), 2) print('\nINFO: Validating Gaps in Plots without using roads for one geometry...') test_plots_layer.startEditing() test_plots_layer.deleteFeature(2) gaps = GeometryUtils.get_gaps_in_polygon_layer(test_plots_layer, False) geometries = [g.asWkt() for g in gaps] self.assertIn( 'Polygon ((1001895.43752562382724136 1013467.22283697873353958, 1001907.30677810893394053 1013464.25552385742776096, 1001907.67769224906805903 1013454.2408420731080696, 1001895.43752562382724136 1013454.2408420731080696, 1001895.43752562382724136 1013467.22283697873353958))', geometries) self.assertIn( 'Polygon ((1001847.96051568305119872 1013470.1901501000393182, 1001867.98987925180699676 1013469.07740767952054739, 1001869.10262167232576758 1013455.72449863376095891, 1001847.58960154291708022 1013455.72449863376095891, 1001847.96051568305119872 1013470.1901501000393182))', geometries) self.assertEqual(len(geometries), 2) test_plots_layer.rollBack() print('\nINFO: Validating Gaps in Plots using roads for only one geometry...') test_plots_layer.startEditing() test_plots_layer.deleteFeature(1) test_plots_layer.deleteFeature(2) test_plots_layer.deleteFeature(3) gaps = GeometryUtils.get_gaps_in_polygon_layer(test_plots_layer, True) geometries = [g.asWkt() for g in gaps] self.assertEqual([], geometries) self.assertEqual(len(geometries), 0) test_plots_layer.rollBack() print('\nINFO: Validating Gaps in Plots without using roads for only one geometry...') test_plots_layer.startEditing() test_plots_layer.deleteFeature(1) test_plots_layer.deleteFeature(2) test_plots_layer.deleteFeature(3) gaps = GeometryUtils.get_gaps_in_polygon_layer(test_plots_layer, False) geometries = [g.asWkt() for g in gaps] self.assertEqual([], geometries) self.assertEqual(len(geometries), 0) test_plots_layer.rollBack() print('\nINFO: Validating Gaps in Plots using roads for two geometries...') test_plots_layer.startEditing() test_plots_layer.deleteFeature(1) test_plots_layer.deleteFeature(3) gaps = GeometryUtils.get_gaps_in_polygon_layer(test_plots_layer, True) geometries = [g.asWkt() for g in gaps] self.assertIn( 'Polygon ((1001889.87381352134980261 1013447.93530169036239386, 1001885.42284383939113468 1013430.87325124291237444, 1001901.72405463655013591 1013411.57209242216777056, 1001845.19794039404951036 1013415.08188382943626493, 1001851.47861975431442261 1013424.31817700632382184, 1001833.74493685469496995 1013433.92392191023100168, 1001889.87381352134980261 1013447.93530169036239386))', geometries) self.assertEqual(len(geometries), 1) test_plots_layer.rollBack() print('\nINFO: Validating Gaps in Plots without using roads for two geometries...') test_plots_layer.startEditing() test_plots_layer.deleteFeature(1) test_plots_layer.deleteFeature(3 ) gaps = GeometryUtils.get_gaps_in_polygon_layer(test_plots_layer, False) geometries = [g.asWkt() for g in gaps] self.assertEqual([], geometries) self.assertEqual(len(geometries), 0) test_plots_layer.rollBack()
def test_invalidGeometryFilter(self): layer = QgsVectorLayer( "Polygon?field=x:string", "joinlayer", "memory") # add some features, one has invalid geometry pr = layer.dataProvider() f1 = QgsFeature(1) f1.setAttributes(["a"]) f1.setGeometry(QgsGeometry.fromWkt('Polygon((0 0, 1 0, 1 1, 0 1, 0 0))')) # valid f2 = QgsFeature(2) f2.setAttributes(["b"]) f2.setGeometry(QgsGeometry.fromWkt('Polygon((0 0, 1 0, 0 1, 1 1, 0 0))')) # invalid f3 = QgsFeature(3) f3.setAttributes(["c"]) f3.setGeometry(QgsGeometry.fromWkt('Polygon((0 0, 1 0, 1 1, 0 1, 0 0))')) # valid self.assertTrue(pr.addFeatures([f1, f2, f3])) res = [f['x'] for f in layer.getFeatures(QgsFeatureRequest().setInvalidGeometryCheck(QgsFeatureRequest.GeometryNoCheck))] self.assertEqual(res, ['a', 'b', 'c']) res = [f['x'] for f in layer.getFeatures(QgsFeatureRequest().setInvalidGeometryCheck(QgsFeatureRequest.GeometrySkipInvalid))] self.assertEqual(res, ['a', 'c']) res = [f['x'] for f in layer.getFeatures(QgsFeatureRequest().setInvalidGeometryCheck(QgsFeatureRequest.GeometryAbortOnInvalid))] self.assertEqual(res, ['a']) # with callback self.callback_feature_val = None def callback(feature): self.callback_feature_val = feature['x'] res = [f['x'] for f in layer.getFeatures(QgsFeatureRequest().setInvalidGeometryCheck( QgsFeatureRequest.GeometryAbortOnInvalid).setInvalidGeometryCallback(callback))] self.assertEqual(res, ['a']) self.assertEqual(self.callback_feature_val, 'b') # clear callback res = [f['x'] for f in layer.getFeatures(QgsFeatureRequest().setInvalidGeometryCheck( QgsFeatureRequest.GeometryAbortOnInvalid).setInvalidGeometryCallback(None))] self.assertEqual(res, ['a']) # check with filter fids res = [f['x'] for f in layer.getFeatures(QgsFeatureRequest().setFilterFid(f2.id()).setInvalidGeometryCheck(QgsFeatureRequest.GeometryNoCheck))] self.assertEqual(res, ['b']) res = [f['x'] for f in layer.getFeatures(QgsFeatureRequest().setFilterFid(f2.id()).setInvalidGeometryCheck(QgsFeatureRequest.GeometrySkipInvalid))] self.assertEqual(res, []) res = [f['x'] for f in layer.getFeatures(QgsFeatureRequest().setFilterFid(f2.id()).setInvalidGeometryCheck(QgsFeatureRequest.GeometryAbortOnInvalid))] self.assertEqual(res, []) f4 = QgsFeature(4) f4.setAttributes(["d"]) f4.setGeometry(QgsGeometry.fromWkt('Polygon((0 0, 1 0, 0 1, 1 1, 0 0))')) # invalid # check with added features layer.startEditing() self.assertTrue(layer.addFeatures([f4])) res = [f['x'] for f in layer.getFeatures(QgsFeatureRequest().setInvalidGeometryCheck(QgsFeatureRequest.GeometryNoCheck))] self.assertEqual(set(res), {'a', 'b', 'c', 'd'}) res = [f['x'] for f in layer.getFeatures(QgsFeatureRequest().setInvalidGeometryCheck(QgsFeatureRequest.GeometrySkipInvalid))] self.assertEqual(set(res), {'a', 'c'}) res = [f['x'] for f in layer.getFeatures(QgsFeatureRequest().setInvalidGeometryCheck(QgsFeatureRequest.GeometryAbortOnInvalid))] self.assertEqual(res, ['a']) # check with features with changed geometry layer.rollBack() layer.startEditing() layer.changeGeometry(2, QgsGeometry.fromWkt('Polygon((0 0, 1 0, 1 1, 0 1, 0 0))')) # valid layer.changeGeometry(3, QgsGeometry.fromWkt('Polygon((0 0, 1 0, 0 1, 1 1, 0 0))'))# invalid res = [f['x'] for f in layer.getFeatures(QgsFeatureRequest().setInvalidGeometryCheck(QgsFeatureRequest.GeometryNoCheck))] self.assertEqual(set(res), {'a', 'b', 'c'}) res = [f['x'] for f in layer.getFeatures(QgsFeatureRequest().setInvalidGeometryCheck(QgsFeatureRequest.GeometrySkipInvalid))] self.assertEqual(set(res), {'a', 'b'}) res = [f['x'] for f in layer.getFeatures(QgsFeatureRequest().setInvalidGeometryCheck(QgsFeatureRequest.GeometryAbortOnInvalid))] self.assertEqual(res, ['a', 'b']) layer.rollBack()
def testCreateFeature(self): """ test creating a feature respecting defaults and constraints """ layer = QgsVectorLayer("Point?field=fldtxt:string&field=fldint:integer&field=flddbl:double", "addfeat", "memory") # add a bunch of features f = QgsFeature() f.setAttributes(["test", 123, 1.0]) f1 = QgsFeature(2) f1.setAttributes(["test_1", 124, 1.1]) f2 = QgsFeature(3) f2.setAttributes(["test_2", 125, 2.4]) f3 = QgsFeature(4) f3.setAttributes(["test_3", 126, 1.7]) f4 = QgsFeature(5) f4.setAttributes(["superpig", 127, 0.8]) self.assertTrue(layer.dataProvider().addFeatures([f, f1, f2, f3, f4])) # no layer self.assertFalse(QgsVectorLayerUtils.createFeature(None).isValid()) # basic tests f = QgsVectorLayerUtils.createFeature(layer) self.assertTrue(f.isValid()) self.assertEqual(f.fields(), layer.fields()) self.assertFalse(f.hasGeometry()) self.assertEqual(f.attributes(), [NULL, NULL, NULL]) # set geometry g = QgsGeometry.fromPointXY(QgsPointXY(100, 200)) f = QgsVectorLayerUtils.createFeature(layer, g) self.assertTrue(f.hasGeometry()) self.assertEqual(f.geometry().asWkt(), g.asWkt()) # using attribute map f = QgsVectorLayerUtils.createFeature(layer, attributes={0: 'a', 2: 6.0}) self.assertEqual(f.attributes(), ['a', NULL, 6.0]) # layer with default value expression layer.setDefaultValueDefinition(2, QgsDefaultValue('3*4')) f = QgsVectorLayerUtils.createFeature(layer) self.assertEqual(f.attributes(), [NULL, NULL, 12]) # we do not expect the default value expression to take precedence over the attribute map f = QgsVectorLayerUtils.createFeature(layer, attributes={0: 'a', 2: 6.0}) self.assertEqual(f.attributes(), ['a', NULL, 6.0]) # layer with default value expression based on geometry layer.setDefaultValueDefinition(2, QgsDefaultValue('3*$x')) f = QgsVectorLayerUtils.createFeature(layer, g) #adjusted so that input value and output feature are the same self.assertEqual(f.attributes(), [NULL, NULL, 300.0]) layer.setDefaultValueDefinition(2, QgsDefaultValue(None)) # test with violated unique constraints layer.setFieldConstraint(1, QgsFieldConstraints.ConstraintUnique) f = QgsVectorLayerUtils.createFeature(layer, attributes={0: 'test_1', 1: 123}) # since field 1 has Unique Constraint, it ignores value 123 that already has been set and sets to 128 self.assertEqual(f.attributes(), ['test_1', 128, NULL]) layer.setFieldConstraint(0, QgsFieldConstraints.ConstraintUnique) # since field 0 and 1 already have values test_1 and 123, the output must be a new unique value f = QgsVectorLayerUtils.createFeature(layer, attributes={0: 'test_1', 1: 123}) self.assertEqual(f.attributes(), ['test_4', 128, NULL]) # test with violated unique constraints and default value expression providing unique value layer.setDefaultValueDefinition(1, QgsDefaultValue('130')) f = QgsVectorLayerUtils.createFeature(layer, attributes={0: 'test_1', 1: 123}) # since field 1 has Unique Constraint, it ignores value 123 that already has been set and adds the default value self.assertEqual(f.attributes(), ['test_4', 130, NULL]) # fallback: test with violated unique constraints and default value expression providing already existing value # add the feature with the default value: self.assertTrue(layer.dataProvider().addFeatures([f])) f = QgsVectorLayerUtils.createFeature(layer, attributes={0: 'test_1', 1: 123}) # since field 1 has Unique Constraint, it ignores value 123 that already has been set and adds the default value # and since the default value providing an already existing value (130) it generates a unique value (next int: 131) self.assertEqual(f.attributes(), ['test_5', 131, NULL]) layer.setDefaultValueDefinition(1, QgsDefaultValue(None)) # test with manually correct unique constraint f = QgsVectorLayerUtils.createFeature(layer, attributes={0: 'test_1', 1: 132}) self.assertEqual(f.attributes(), ['test_5', 132, NULL]) """ test creating a feature respecting unique values of postgres provider """ layer = QgsVectorLayer("Point?field=fldtxt:string&field=fldint:integer&field=flddbl:double", "addfeat", "memory") # init connection string dbconn = 'dbname=\'qgis_test\'' if 'QGIS_PGTEST_DB' in os.environ: dbconn = os.environ['QGIS_PGTEST_DB'] # create a vector layer pg_layer = QgsVectorLayer('{} table="qgis_test"."authors" sql='.format(dbconn), "authors", "postgres") self.assertTrue(pg_layer.isValid()) # check the default clause default_clause = 'nextval(\'qgis_test.authors_pk_seq\'::regclass)' self.assertEqual(pg_layer.dataProvider().defaultValueClause(0), default_clause) # though default_clause is after the first create not unique (until save), it should fill up all the features with it pg_layer.startEditing() f = QgsVectorLayerUtils.createFeature(pg_layer) self.assertEqual(f.attributes(), [default_clause, NULL]) self.assertTrue(pg_layer.addFeatures([f])) self.assertTrue(QgsVectorLayerUtils.valueExists(pg_layer, 0, default_clause)) f = QgsVectorLayerUtils.createFeature(pg_layer) self.assertEqual(f.attributes(), [default_clause, NULL]) self.assertTrue(pg_layer.addFeatures([f])) f = QgsVectorLayerUtils.createFeature(pg_layer) self.assertEqual(f.attributes(), [default_clause, NULL]) self.assertTrue(pg_layer.addFeatures([f])) # if a unique value is passed, use it f = QgsVectorLayerUtils.createFeature(pg_layer, attributes={0: 40, 1: NULL}) self.assertEqual(f.attributes(), [40, NULL]) # and if a default value is configured use it as well pg_layer.setDefaultValueDefinition(0, QgsDefaultValue('11*4')) f = QgsVectorLayerUtils.createFeature(pg_layer) self.assertEqual(f.attributes(), [44, NULL]) pg_layer.rollBack()
def testTypeValidation(self): """Test that incompatible types in attributes raise errors""" vl = QgsVectorLayer('Point?crs=epsg:4326&field=int:integer', 'test', 'memory') self.assertTrue(vl.isValid()) invalid = QgsFeature(vl.fields()) invalid.setAttribute('int', 'A string') invalid.setGeometry(QgsGeometry.fromWkt('point(9 45)')) self.assertTrue(vl.startEditing()) # Validation happens on commit self.assertTrue(vl.addFeatures([invalid])) self.assertFalse(vl.commitChanges()) self.assertTrue(vl.rollBack()) self.assertFalse(vl.hasFeatures()) # Add a valid feature valid = QgsFeature(vl.fields()) valid.setAttribute('int', 123) self.assertTrue(vl.startEditing()) self.assertTrue(vl.addFeatures([valid])) self.assertTrue(vl.commitChanges()) self.assertEqual(vl.featureCount(), 1) f = vl.getFeature(1) self.assertEqual(f.attribute('int'), 123) # Add both vl = QgsVectorLayer('Point?crs=epsg:4326&field=int:integer', 'test', 'memory') self.assertEqual(vl.featureCount(), 0) self.assertTrue(vl.startEditing()) self.assertTrue(vl.addFeatures([valid, invalid])) self.assertFalse(vl.commitChanges()) self.assertEqual(vl.featureCount(), 2) self.assertTrue(vl.rollBack()) self.assertEqual(vl.featureCount(), 0) # Add both swapped vl = QgsVectorLayer('Point?crs=epsg:4326&field=int:integer', 'test', 'memory') self.assertTrue(vl.startEditing()) self.assertTrue(vl.addFeatures([invalid, valid])) self.assertFalse(vl.commitChanges()) self.assertEqual(vl.featureCount(), 2) self.assertTrue(vl.rollBack()) self.assertEqual(vl.featureCount(), 0) # Change attribute value vl = QgsVectorLayer('Point?crs=epsg:4326&field=int:integer', 'test', 'memory') self.assertTrue(vl.startEditing()) self.assertTrue(vl.addFeatures([valid])) self.assertTrue(vl.commitChanges()) self.assertTrue(vl.startEditing()) self.assertTrue(vl.changeAttributeValue(1, 0, 'A string')) self.assertFalse(vl.commitChanges()) f = vl.getFeature(1) self.assertEqual(f.attribute('int'), 'A string') self.assertTrue(vl.rollBack()) f = vl.getFeature(1) self.assertEqual(f.attribute('int'), 123) # Change attribute values vl = QgsVectorLayer('Point?crs=epsg:4326&field=int:integer', 'test', 'memory') self.assertTrue(vl.startEditing()) self.assertTrue(vl.addFeatures([valid])) self.assertTrue(vl.commitChanges()) self.assertTrue(vl.startEditing()) self.assertTrue(vl.changeAttributeValues(1, {0: 'A string'})) self.assertFalse(vl.commitChanges()) f = vl.getFeature(1) self.assertEqual(f.attribute('int'), 'A string') self.assertTrue(vl.rollBack()) f = vl.getFeature(1) self.assertEqual(f.attribute('int'), 123) ############################################## # Test direct data provider calls # No rollback (old behavior) vl = QgsVectorLayer('Point?crs=epsg:4326&field=int:integer', 'test', 'memory') dp = vl.dataProvider() self.assertFalse(dp.addFeatures([valid, invalid])[0]) self.assertEqual([f.attributes() for f in dp.getFeatures()], [[123]]) f = vl.getFeature(1) self.assertEqual(f.attribute('int'), 123) # Roll back vl = QgsVectorLayer('Point?crs=epsg:4326&field=int:integer', 'test', 'memory') dp = vl.dataProvider() self.assertFalse( dp.addFeatures([valid, invalid], QgsFeatureSink.RollBackOnErrors)[0]) self.assertFalse(dp.hasFeatures()) # Expected behavior for changeAttributeValues is to always roll back self.assertTrue(dp.addFeatures([valid])[0]) self.assertFalse(dp.changeAttributeValues({1: {0: 'A string'}})) f = vl.getFeature(1) self.assertEqual(f.attribute('int'), 123)
def _test(autoTransaction): """Test buffer methods within and without transactions - create a feature - save - retrieve the feature - change geom and attrs - test changes are seen in the buffer """ def _check_feature(wkt): f = next(layer_a.getFeatures()) self.assertEqual(f.geometry().asWkt().upper(), wkt) f = list(buffer.addedFeatures().values())[0] self.assertEqual(f.geometry().asWkt().upper(), wkt) ml = QgsVectorLayer( 'Point?crs=epsg:4326&field=int:integer&field=int2:integer', 'test', 'memory') self.assertTrue(ml.isValid()) d = QTemporaryDir() options = QgsVectorFileWriter.SaveVectorOptions() options.driverName = 'GPKG' options.layerName = 'layer_a' err, msg, newFileName, newLayer = QgsVectorFileWriter.writeAsVectorFormatV3( ml, os.path.join(d.path(), 'transaction_test.gpkg'), QgsCoordinateTransformContext(), options) self.assertEqual(err, QgsVectorFileWriter.NoError) self.assertTrue(os.path.isfile(newFileName)) layer_a = QgsVectorLayer(newFileName + '|layername=layer_a') self.assertTrue(layer_a.isValid()) project = QgsProject() project.setAutoTransaction(autoTransaction) project.addMapLayers([layer_a]) ########################################### # Tests with a new feature self.assertTrue(layer_a.startEditing()) buffer = layer_a.editBuffer() f = QgsFeature(layer_a.fields()) f.setAttribute('int', 123) f.setGeometry(QgsGeometry.fromWkt('point(7 45)')) self.assertTrue(layer_a.addFeatures([f])) _check_feature('POINT (7 45)') # Need to fetch the feature because its ID is NULL (-9223372036854775808) f = next(layer_a.getFeatures()) self.assertEqual(len(buffer.addedFeatures()), 1) layer_a.undoStack().undo() self.assertEqual(len(buffer.addedFeatures()), 0) layer_a.undoStack().redo() self.assertEqual(len(buffer.addedFeatures()), 1) f = list(buffer.addedFeatures().values())[0] self.assertEqual(f.attribute('int'), 123) # Now change attribute self.assertEqual(buffer.changedAttributeValues(), {}) spy_attribute_changed = QSignalSpy(layer_a.attributeValueChanged) layer_a.changeAttributeValue(f.id(), 1, 321) self.assertEqual(len(spy_attribute_changed), 1) self.assertEqual(spy_attribute_changed[0], [f.id(), 1, 321]) self.assertEqual(len(buffer.addedFeatures()), 1) # This is surprising: because it was a new feature it has been changed directly self.assertEqual(buffer.changedAttributeValues(), {}) f = list(buffer.addedFeatures().values())[0] self.assertEqual(f.attribute('int'), 321) spy_attribute_changed = QSignalSpy(layer_a.attributeValueChanged) layer_a.undoStack().undo() self.assertEqual(len(spy_attribute_changed), 1) self.assertEqual(spy_attribute_changed[0], [f.id(), 1, 123]) self.assertEqual(buffer.changedAttributeValues(), {}) f = list(buffer.addedFeatures().values())[0] self.assertEqual(f.attribute('int'), 123) f = next(layer_a.getFeatures()) self.assertEqual(f.attribute('int'), 123) # Change multiple attributes spy_attribute_changed = QSignalSpy(layer_a.attributeValueChanged) layer_a.changeAttributeValues(f.id(), {1: 321, 2: 456}) self.assertEqual(len(spy_attribute_changed), 2) self.assertEqual(spy_attribute_changed[0], [f.id(), 1, 321]) self.assertEqual(spy_attribute_changed[1], [f.id(), 2, 456]) buffer = layer_a.editBuffer() # This is surprising: because it was a new feature it has been changed directly self.assertEqual(buffer.changedAttributeValues(), {}) spy_attribute_changed = QSignalSpy(layer_a.attributeValueChanged) layer_a.undoStack().undo() # This is because QgsVectorLayerUndoCommandChangeAttribute plural if not autoTransaction: layer_a.undoStack().undo() f = next(layer_a.getFeatures()) self.assertEqual(f.attribute('int'), 123) self.assertEqual(f.attribute('int2'), None) self.assertEqual(len(spy_attribute_changed), 2) self.assertEqual( spy_attribute_changed[1 if autoTransaction else 0], [f.id(), 2, None]) self.assertEqual( spy_attribute_changed[0 if autoTransaction else 1], [f.id(), 1, 123]) # Change geometry f = next(layer_a.getFeatures()) spy_geometry_changed = QSignalSpy(layer_a.geometryChanged) self.assertTrue( layer_a.changeGeometry(f.id(), QgsGeometry.fromWkt('point(9 43)'))) self.assertTrue(len(spy_geometry_changed), 1) self.assertEqual(spy_geometry_changed[0][0], f.id()) self.assertEqual(spy_geometry_changed[0][1].asWkt(), QgsGeometry.fromWkt('point(9 43)').asWkt()) _check_feature('POINT (9 43)') self.assertEqual(buffer.changedGeometries(), {}) layer_a.undoStack().undo() _check_feature('POINT (7 45)') self.assertEqual(buffer.changedGeometries(), {}) self.assertTrue( layer_a.changeGeometry(f.id(), QgsGeometry.fromWkt('point(9 43)'))) _check_feature('POINT (9 43)') self.assertTrue( layer_a.changeGeometry(f.id(), QgsGeometry.fromWkt('point(10 44)'))) _check_feature('POINT (10 44)') # This is another surprise: geometry edits get collapsed into a single # one because they have the same hardcoded id layer_a.undoStack().undo() _check_feature('POINT (7 45)') self.assertTrue(layer_a.commitChanges()) ########################################### # Tests with the existing feature # Get the feature f = next(layer_a.getFeatures()) self.assertTrue(f.isValid()) self.assertEqual(f.attribute('int'), 123) self.assertEqual(f.geometry().asWkt().upper(), 'POINT (7 45)') # Change single attribute self.assertTrue(layer_a.startEditing()) spy_attribute_changed = QSignalSpy(layer_a.attributeValueChanged) layer_a.changeAttributeValue(f.id(), 1, 321) self.assertEqual(len(spy_attribute_changed), 1) self.assertEqual(spy_attribute_changed[0], [f.id(), 1, 321]) buffer = layer_a.editBuffer() self.assertEqual(buffer.changedAttributeValues(), {1: {1: 321}}) f = next(layer_a.getFeatures()) self.assertEqual(f.attribute(1), 321) spy_attribute_changed = QSignalSpy(layer_a.attributeValueChanged) layer_a.undoStack().undo() f = next(layer_a.getFeatures()) self.assertEqual(f.attribute(1), 123) self.assertEqual(len(spy_attribute_changed), 1) self.assertEqual(spy_attribute_changed[0], [f.id(), 1, 123]) self.assertEqual(buffer.changedAttributeValues(), {}) # Change attributes spy_attribute_changed = QSignalSpy(layer_a.attributeValueChanged) layer_a.changeAttributeValues(f.id(), {1: 111, 2: 654}) self.assertEqual(len(spy_attribute_changed), 2) self.assertEqual(spy_attribute_changed[0], [1, 1, 111]) self.assertEqual(spy_attribute_changed[1], [1, 2, 654]) f = next(layer_a.getFeatures()) self.assertEqual(f.attributes(), [1, 111, 654]) self.assertEqual(buffer.changedAttributeValues(), {1: { 1: 111, 2: 654 }}) spy_attribute_changed = QSignalSpy(layer_a.attributeValueChanged) layer_a.undoStack().undo() # This is because QgsVectorLayerUndoCommandChangeAttribute plural if not autoTransaction: layer_a.undoStack().undo() self.assertEqual(len(spy_attribute_changed), 2) self.assertEqual( spy_attribute_changed[0 if autoTransaction else 1], [1, 1, 123]) self.assertEqual( spy_attribute_changed[1 if autoTransaction else 0], [1, 2, None]) f = next(layer_a.getFeatures()) self.assertEqual(f.attributes(), [1, 123, None]) self.assertEqual(buffer.changedAttributeValues(), {}) # Change geometry spy_geometry_changed = QSignalSpy(layer_a.geometryChanged) self.assertTrue( layer_a.changeGeometry(f.id(), QgsGeometry.fromWkt('point(9 43)'))) self.assertEqual(spy_geometry_changed[0][0], 1) self.assertEqual(spy_geometry_changed[0][1].asWkt(), QgsGeometry.fromWkt('point(9 43)').asWkt()) f = next(layer_a.getFeatures()) self.assertEqual(f.geometry().asWkt().upper(), 'POINT (9 43)') self.assertEqual(buffer.changedGeometries()[1].asWkt().upper(), 'POINT (9 43)') spy_geometry_changed = QSignalSpy(layer_a.geometryChanged) layer_a.undoStack().undo() self.assertEqual(spy_geometry_changed[0][0], 1) self.assertEqual(spy_geometry_changed[0][1].asWkt(), QgsGeometry.fromWkt('point(7 45)').asWkt()) self.assertEqual(buffer.changedGeometries(), {}) f = next(layer_a.getFeatures()) self.assertEqual(f.geometry().asWkt().upper(), 'POINT (7 45)') self.assertEqual(buffer.changedGeometries(), {}) # Delete an existing feature self.assertTrue(layer_a.deleteFeature(f.id())) with self.assertRaises(StopIteration): next(layer_a.getFeatures()) self.assertEqual(buffer.deletedFeatureIds(), [f.id()]) layer_a.undoStack().undo() self.assertTrue(layer_a.getFeature(f.id()).isValid()) self.assertEqual(buffer.deletedFeatureIds(), []) ########################################### # Test delete # Delete a new feature f = QgsFeature(layer_a.fields()) f.setAttribute('int', 555) f.setGeometry(QgsGeometry.fromWkt('point(8 46)')) self.assertTrue(layer_a.addFeatures([f])) f = [ f for f in layer_a.getFeatures() if f.attribute('int') == 555 ][0] self.assertTrue(f.id() in buffer.addedFeatures()) self.assertTrue(layer_a.deleteFeature(f.id())) self.assertFalse(f.id() in buffer.addedFeatures()) self.assertFalse(f.id() in buffer.deletedFeatureIds()) layer_a.undoStack().undo() self.assertTrue(f.id() in buffer.addedFeatures()) ########################################### # Add attribute field = QgsField('attr1', QVariant.String) self.assertTrue(layer_a.addAttribute(field)) self.assertNotEqual(layer_a.fields().lookupField(field.name()), -1) self.assertEqual(buffer.addedAttributes(), [field]) layer_a.undoStack().undo() self.assertEqual(layer_a.fields().lookupField(field.name()), -1) self.assertEqual(buffer.addedAttributes(), []) layer_a.undoStack().redo() self.assertNotEqual(layer_a.fields().lookupField(field.name()), -1) self.assertEqual(buffer.addedAttributes(), [field]) self.assertTrue(layer_a.commitChanges()) ########################################### # Remove attribute self.assertTrue(layer_a.startEditing()) buffer = layer_a.editBuffer() attr_idx = layer_a.fields().lookupField(field.name()) self.assertNotEqual(attr_idx, -1) self.assertTrue(layer_a.deleteAttribute(attr_idx)) self.assertEqual(buffer.deletedAttributeIds(), [attr_idx]) self.assertEqual(layer_a.fields().lookupField(field.name()), -1) layer_a.undoStack().undo() self.assertEqual(buffer.deletedAttributeIds(), []) self.assertEqual(layer_a.fields().lookupField(field.name()), attr_idx) # This is totally broken at least on OGR/GPKG: the rollback # does not restore the original fields if False: layer_a.undoStack().redo() self.assertEqual(buffer.deletedAttributeIds(), [attr_idx]) self.assertEqual(layer_a.fields().lookupField(field.name()), -1) # Rollback! self.assertTrue(layer_a.rollBack()) self.assertIn('attr1', layer_a.dataProvider().fields().names()) self.assertIn('attr1', layer_a.fields().names()) self.assertEqual(layer_a.fields().names(), layer_a.dataProvider().fields().names()) attr_idx = layer_a.fields().lookupField(field.name()) self.assertNotEqual(attr_idx, -1) self.assertTrue(layer_a.startEditing()) attr_idx = layer_a.fields().lookupField(field.name()) self.assertNotEqual(attr_idx, -1) ########################################### # Rename attribute attr_idx = layer_a.fields().lookupField(field.name()) self.assertEqual(layer_a.fields().lookupField('new_name'), -1) self.assertTrue(layer_a.renameAttribute(attr_idx, 'new_name')) self.assertEqual(layer_a.fields().lookupField('new_name'), attr_idx) layer_a.undoStack().undo() self.assertEqual(layer_a.fields().lookupField(field.name()), attr_idx) self.assertEqual(layer_a.fields().lookupField('new_name'), -1) layer_a.undoStack().redo() self.assertEqual(layer_a.fields().lookupField('new_name'), attr_idx) self.assertEqual(layer_a.fields().lookupField(field.name()), -1) ############################################# # Try hard to make this fail for transactions if autoTransaction: self.assertTrue(layer_a.commitChanges()) self.assertTrue(layer_a.startEditing()) f = next(layer_a.getFeatures()) # Do for i in range(10): spy_attribute_changed = QSignalSpy( layer_a.attributeValueChanged) layer_a.changeAttributeValue(f.id(), 2, i) self.assertEqual(len(spy_attribute_changed), 1) self.assertEqual(spy_attribute_changed[0], [f.id(), 2, i]) buffer = layer_a.editBuffer() self.assertEqual(buffer.changedAttributeValues(), {f.id(): { 2: i }}) f = next(layer_a.getFeatures()) self.assertEqual(f.attribute(2), i) # Undo/redo for i in range(9): # Undo spy_attribute_changed = QSignalSpy( layer_a.attributeValueChanged) layer_a.undoStack().undo() f = next(layer_a.getFeatures()) self.assertEqual(f.attribute(2), 8 - i) self.assertEqual(len(spy_attribute_changed), 1) self.assertEqual(spy_attribute_changed[0], [f.id(), 2, 8 - i]) buffer = layer_a.editBuffer() self.assertEqual(buffer.changedAttributeValues(), {f.id(): { 2: 8 - i }}) # Redo spy_attribute_changed = QSignalSpy( layer_a.attributeValueChanged) layer_a.undoStack().redo() f = next(layer_a.getFeatures()) self.assertEqual(f.attribute(2), 9 - i) self.assertEqual(len(spy_attribute_changed), 1) self.assertEqual(spy_attribute_changed[0], [f.id(), 2, 9 - i]) # Undo again spy_attribute_changed = QSignalSpy( layer_a.attributeValueChanged) layer_a.undoStack().undo() f = next(layer_a.getFeatures()) self.assertEqual(f.attribute(2), 8 - i) self.assertEqual(len(spy_attribute_changed), 1) self.assertEqual(spy_attribute_changed[0], [f.id(), 2, 8 - i]) buffer = layer_a.editBuffer() self.assertEqual(buffer.changedAttributeValues(), {f.id(): { 2: 8 - i }}) # Last check f = next(layer_a.getFeatures()) self.assertEqual(f.attribute(2), 8 - i) self.assertEqual(buffer.changedAttributeValues(), {f.id(): { 2: 0 }}) layer_a.undoStack().undo() buffer = layer_a.editBuffer() self.assertEqual(buffer.changedAttributeValues(), {}) f = next(layer_a.getFeatures()) self.assertEqual(f.attribute(2), None)
def testCreateFeature(self): """ test creating a feature respecting defaults and constraints """ layer = QgsVectorLayer( "Point?field=fldtxt:string&field=fldint:integer&field=flddbl:double", "addfeat", "memory") # add a bunch of features f = QgsFeature() f.setAttributes(["test", 123, 1.0]) f1 = QgsFeature(2) f1.setAttributes(["test_1", 124, 1.1]) f2 = QgsFeature(3) f2.setAttributes(["test_2", 125, 2.4]) f3 = QgsFeature(4) f3.setAttributes(["test_3", 126, 1.7]) f4 = QgsFeature(5) f4.setAttributes(["superpig", 127, 0.8]) self.assertTrue(layer.dataProvider().addFeatures([f, f1, f2, f3, f4])) # no layer self.assertFalse(QgsVectorLayerUtils.createFeature(None).isValid()) # basic tests f = QgsVectorLayerUtils.createFeature(layer) self.assertTrue(f.isValid()) self.assertEqual(f.fields(), layer.fields()) self.assertFalse(f.hasGeometry()) self.assertEqual(f.attributes(), [NULL, NULL, NULL]) # set geometry g = QgsGeometry.fromPointXY(QgsPointXY(100, 200)) f = QgsVectorLayerUtils.createFeature(layer, g) self.assertTrue(f.hasGeometry()) self.assertEqual(f.geometry().asWkt(), g.asWkt()) # using attribute map f = QgsVectorLayerUtils.createFeature(layer, attributes={ 0: 'a', 2: 6.0 }) self.assertEqual(f.attributes(), ['a', NULL, 6.0]) # layer with default value expression layer.setDefaultValueDefinition(2, QgsDefaultValue('3*4')) f = QgsVectorLayerUtils.createFeature(layer) self.assertEqual(f.attributes(), [NULL, NULL, 12]) # we do not expect the default value expression to take precedence over the attribute map f = QgsVectorLayerUtils.createFeature(layer, attributes={ 0: 'a', 2: 6.0 }) self.assertEqual(f.attributes(), ['a', NULL, 6.0]) # layer with default value expression based on geometry layer.setDefaultValueDefinition(2, QgsDefaultValue('3*$x')) f = QgsVectorLayerUtils.createFeature(layer, g) #adjusted so that input value and output feature are the same self.assertEqual(f.attributes(), [NULL, NULL, 300.0]) layer.setDefaultValueDefinition(2, QgsDefaultValue(None)) # test with violated unique constraints layer.setFieldConstraint(1, QgsFieldConstraints.ConstraintUnique) f = QgsVectorLayerUtils.createFeature(layer, attributes={ 0: 'test_1', 1: 123 }) # since field 1 has Unique Constraint, it ignores value 123 that already has been set and sets to 128 self.assertEqual(f.attributes(), ['test_1', 128, NULL]) layer.setFieldConstraint(0, QgsFieldConstraints.ConstraintUnique) # since field 0 and 1 already have values test_1 and 123, the output must be a new unique value f = QgsVectorLayerUtils.createFeature(layer, attributes={ 0: 'test_1', 1: 123 }) self.assertEqual(f.attributes(), ['test_4', 128, NULL]) # test with violated unique constraints and default value expression providing unique value layer.setDefaultValueDefinition(1, QgsDefaultValue('130')) f = QgsVectorLayerUtils.createFeature(layer, attributes={ 0: 'test_1', 1: 123 }) # since field 1 has Unique Constraint, it ignores value 123 that already has been set and adds the default value self.assertEqual(f.attributes(), ['test_4', 130, NULL]) # fallback: test with violated unique constraints and default value expression providing already existing value # add the feature with the default value: self.assertTrue(layer.dataProvider().addFeatures([f])) f = QgsVectorLayerUtils.createFeature(layer, attributes={ 0: 'test_1', 1: 123 }) # since field 1 has Unique Constraint, it ignores value 123 that already has been set and adds the default value # and since the default value providing an already existing value (130) it generates a unique value (next int: 131) self.assertEqual(f.attributes(), ['test_5', 131, NULL]) layer.setDefaultValueDefinition(1, QgsDefaultValue(None)) # test with manually correct unique constraint f = QgsVectorLayerUtils.createFeature(layer, attributes={ 0: 'test_1', 1: 132 }) self.assertEqual(f.attributes(), ['test_5', 132, NULL]) """ test creating a feature respecting unique values of postgres provider """ layer = QgsVectorLayer( "Point?field=fldtxt:string&field=fldint:integer&field=flddbl:double", "addfeat", "memory") # init connection string dbconn = 'dbname=\'qgis_test\'' if 'QGIS_PGTEST_DB' in os.environ: dbconn = os.environ['QGIS_PGTEST_DB'] # create a vector layer pg_layer = QgsVectorLayer( '{} table="qgis_test"."authors" sql='.format(dbconn), "authors", "postgres") self.assertTrue(pg_layer.isValid()) # check the default clause default_clause = 'nextval(\'qgis_test.authors_pk_seq\'::regclass)' self.assertEqual(pg_layer.dataProvider().defaultValueClause(0), default_clause) # though default_clause is after the first create not unique (until save), it should fill up all the features with it pg_layer.startEditing() f = QgsVectorLayerUtils.createFeature(pg_layer) self.assertEqual(f.attributes(), [default_clause, NULL]) self.assertTrue(pg_layer.addFeatures([f])) self.assertTrue( QgsVectorLayerUtils.valueExists(pg_layer, 0, default_clause)) f = QgsVectorLayerUtils.createFeature(pg_layer) self.assertEqual(f.attributes(), [default_clause, NULL]) self.assertTrue(pg_layer.addFeatures([f])) f = QgsVectorLayerUtils.createFeature(pg_layer) self.assertEqual(f.attributes(), [default_clause, NULL]) self.assertTrue(pg_layer.addFeatures([f])) # if a unique value is passed, use it f = QgsVectorLayerUtils.createFeature(pg_layer, attributes={ 0: 40, 1: NULL }) self.assertEqual(f.attributes(), [40, NULL]) # and if a default value is configured use it as well pg_layer.setDefaultValueDefinition(0, QgsDefaultValue('11*4')) f = QgsVectorLayerUtils.createFeature(pg_layer) self.assertEqual(f.attributes(), [44, NULL]) pg_layer.rollBack()
class Worker(QtCore.QObject): '''The worker that does the heavy lifting. /* QGIS offers spatial indexes to make spatial search more * effective. QgsSpatialIndex will find the nearest index * (approximate) geometry (rectangle) for a supplied point. * QgsSpatialIndex will give correct results when searching * for the nearest neighbour of a point in a point data set. * So something has to be done for non-point data sets * * Non-point join data set: * A two pass search is performed. First the index is used to * find the nearest index geometry (approximation - rectangle), * and then compute the actual distance to this geometry. * Then this rectangle is used to find all features in the join * data set that may be the closest feature to the given point. * For all the features is this candidate set, the actual * distance to the given point is calculated, and the nearest * feature is returned. * * Non-point input data set: * First the centroid of the non-point input geometry is * calculated. Then the index is used to find the nearest * neighbour to this point (using the approximate index * geometry). * The distance vector to this feature, combined with the * bounding rectangle of the input feature is used to create a * search rectangle to find the candidate join geometries. * For all the features is this candidate set, the actual * distance to the given feature is calculated, and the nearest * feature is returned. * * Joins involving multi-geometry data sets are not supported * by a spatial index. * */ ''' # Define the signals used to communicate back to the application progress = QtCore.pyqtSignal(float) # For reporting progress status = QtCore.pyqtSignal(str) # For reporting status error = QtCore.pyqtSignal(str) # For reporting errors #killed = QtCore.pyqtSignal() # Signal for sending over the result: finished = QtCore.pyqtSignal(bool, object) def __init__(self, inputvectorlayer, joinvectorlayer, outputlayername, approximateinputgeom, joinprefix, usejoinlayerapproximation, usejoinlayerindex): """Initialise. Arguments: inputvectorlayer -- (QgsVectorLayer) The base vector layer for the join joinvectorlayer -- (QgsVectorLayer) the join layer outputlayername -- (string) the name of the output memory layer approximateinputgeom -- (boolean) should the input geometry be approximated? Is only be set for non-single-point layers joinprefix -- (string) the prefix to use for the join layer attributes in the output layer usejoinlayerindexapproximation -- (boolean) should the index geometry approximations be used for the join? usejoinlayerindex -- (boolean) should an index for the join layer be used. Will only use the index geometry approximations for the join """ # Set a variable to control the use of indexes and exact # geometries for non-point input geometries #self.nonpointexactindex = True self.nonpointexactindex = usejoinlayerindex QtCore.QObject.__init__(self) # Essential! # Creating instance variables from the parameters self.inpvl = inputvectorlayer self.joinvl = joinvectorlayer self.outputlayername = outputlayername self.approximateinputgeom = approximateinputgeom self.joinprefix = joinprefix self.usejoinlayerapprox = usejoinlayerapproximation # Check if the layers are the same (self join) self.selfjoin = False if self.inpvl is self.joinvl: # This is a self join self.selfjoin = True # Creating instance variables for the progress bar ++ # Number of elements that have been processed - updated by # calculate_progress self.processed = 0 # Current percentage of progress - updated by # calculate_progress self.percentage = 0 # Flag set by kill(), checked in the loop self.abort = False # Number of features in the input layer - used by # calculate_progress self.feature_count = self.inpvl.featureCount() # The number of elements that is needed to increment the # progressbar - set early in run() self.increment = self.feature_count // 1000 def run(self): try: if self.inpvl is None or self.joinvl is None: self.status.emit('Layer is missing!') self.finished.emit(False, None) return #self.status.emit('Started!') # Check the geometry type and prepare the output layer geometryType = self.inpvl.geometryType() #self.status.emit('Input layer geometry type: ' + # str(geometryType)) geometrytypetext = 'Point' if geometryType == QGis.Point: geometrytypetext = 'Point' elif geometryType == QGis.Line: geometrytypetext = 'LineString' elif geometryType == QGis.Polygon: geometrytypetext = 'Polygon' # Does the input vector contain multi-geometries? # Try to check the first feature # This is not used for anything yet self.inputmulti = False feats = self.inpvl.getFeatures() if feats is not None: #self.status.emit('#Input features: ' + str(feats)) testfeature = feats.next() feats.rewind() feats.close() if testfeature is not None: #self.status.emit('Input feature geometry: ' + # str(testfeature.geometry())) if testfeature.geometry() is not None: if testfeature.geometry().isMultipart(): self.inputmulti = True geometrytypetext = 'Multi' + geometrytypetext else: pass else: self.status.emit('No geometry!') self.finished.emit(False, None) return else: self.status.emit('No input features!') self.finished.emit(False, None) return else: self.status.emit('getFeatures returns None for input layer!') self.finished.emit(False, None) return geomptext = geometrytypetext # Set the coordinate reference system to the input # layer's CRS if self.inpvl.crs() is not None: geomptext = (geomptext + "?crs=" + str(self.inpvl.crs().authid())) outfields = self.inpvl.pendingFields().toList() # if self.joinvl.pendingFields() is not None: jfields = self.joinvl.pendingFields().toList() for joinfield in jfields: outfields.append(QgsField(self.joinprefix + str(joinfield.name()), joinfield.type())) else: self.status.emit('Unable to get any join layer fields') #self.finished.emit(False, None) #return outfields.append(QgsField("distance", QVariant.Double)) # Create a memory layer self.mem_joinl = QgsVectorLayer(geomptext, self.outputlayername, "memory") self.mem_joinl.startEditing() for field in outfields: self.mem_joinl.dataProvider().addAttributes([field]) # For an index to be used, the input layer has to be a # point layer, the input layer geometries have to be # approximated to centroids, or the user has to have # accepted that a join layer index is used (for # non-point input layers). # (Could be extended to multipoint) if (self.inpvl.wkbType() == QGis.WKBPoint or self.inpvl.wkbType() == QGis.WKBPoint25D or self.approximateinputgeom or self.nonpointexactindex): # Create a spatial index to speed up joining self.status.emit('Creating join layer index...') self.joinlind = QgsSpatialIndex() for feat in self.joinvl.getFeatures(): # Allow user abort if self.abort is True: break self.joinlind.insertFeature(feat) self.status.emit('Join layer index created!') # Does the join layer contain multi geometries? # Try to check the first feature # This is not used for anything yet self.joinmulti = False feats = self.joinvl.getFeatures() if feats is not None: testfeature = feats.next() feats.rewind() feats.close() if testfeature is not None: if testfeature.geometry() is not None: if testfeature.geometry().isMultipart(): self.joinmulti = True else: self.status.emit('No join geometry!') self.finished.emit(False, None) return else: self.status.emit('No join features!') self.finished.emit(False, None) return #if feats.next().geometry().isMultipart(): # self.joinmulti = True #feats.rewind() #feats.close() # Prepare for the join by fetching the layers into memory # Add the input features to a list inputfeatures = self.inpvl.getFeatures() self.inputf = [] for f in inputfeatures: self.inputf.append(f) # Add the join features to a list joinfeatures = self.joinvl.getFeatures() self.joinf = [] for f in joinfeatures: self.joinf.append(f) self.features = [] # Do the join! # Using the original features from the input layer for feat in self.inputf: # Allow user abort if self.abort is True: break self.do_indexjoin(feat) self.calculate_progress() self.mem_joinl.dataProvider().addFeatures(self.features) self.status.emit('Join finished') except: import traceback self.error.emit(traceback.format_exc()) self.finished.emit(False, None) if self.mem_joinl is not None: self.mem_joinl.rollBack() else: self.mem_joinl.commitChanges() if self.abort: self.finished.emit(False, None) else: self.status.emit('Delivering the memory layer...') self.finished.emit(True, self.mem_joinl) def calculate_progress(self): '''Update progress and emit a signal with the percentage''' self.processed = self.processed + 1 # update the progress bar at certain increments if (self.increment == 0 or self.processed % self.increment == 0): perc_new = (self.processed * 100) / self.feature_count if perc_new > self.percentage: self.percentage = perc_new self.progress.emit(self.percentage) def kill(self): '''Kill the thread by setting the abort flag''' self.abort = True def do_indexjoin(self, feat): '''Find the nearest neigbour using an index, if possible Parameter: feat -- The feature for which a neighbour is sought ''' infeature = feat infeatureid = infeature.id() inputgeom = QgsGeometry(infeature.geometry()) # Shall approximate input geometries be used? if self.approximateinputgeom: # Use the centroid as the input geometry inputgeom = QgsGeometry(infeature.geometry()).centroid() # Check if the coordinate systems are equal, if not, # transform the input feature! if (self.inpvl.crs() != self.joinvl.crs()): try: inputgeom.transform(QgsCoordinateTransform( self.inpvl.crs(), self.joinvl.crs())) except: import traceback self.error.emit(self.tr('CRS Transformation error!') + ' - ' + traceback.format_exc()) self.abort = True return nnfeature = None mindist = float("inf") ## Find the closest feature! if (self.approximateinputgeom or self.inpvl.wkbType() == QGis.WKBPoint or self.inpvl.wkbType() == QGis.WKBPoint25D): # The input layer's geometry type is point, or shall be # approximated to point (centroid). # Then a join index will always be used. if (self.usejoinlayerapprox or self.joinvl.wkbType() == QGis.WKBPoint or self.joinvl.wkbType() == QGis.WKBPoint25D): # The join layer's geometry type is point, or the # user wants approximate join geometries to be used. # Then the join index nearest neighbour function can # be used without refinement. if self.selfjoin: # Self join! # Have to get the two nearest neighbours nearestids = self.joinlind.nearestNeighbor( inputgeom.asPoint(), 2) if nearestids[0] == infeatureid and len(nearestids) > 1: # The first feature is the same as the input # feature, so choose the second one nnfeature = self.joinvl.getFeatures( QgsFeatureRequest(nearestids[1])).next() else: # The first feature is not the same as the # input feature, so choose it nnfeature = self.joinvl.getFeatures( QgsFeatureRequest(nearestids[0])).next() ## Pick the second closest neighbour! ## (the first is supposed to be the point itself) ## Should we check for coinciding points? #nearestid = self.joinlind.nearestNeighbor( # inputgeom.asPoint(), 2)[1] #nnfeature = self.joinvl.getFeatures( # QgsFeatureRequest(nearestid)).next() else: # Not a self join, so we can search for only the # nearest neighbour (1) nearestid = self.joinlind.nearestNeighbor( inputgeom.asPoint(), 1)[0] nnfeature = self.joinvl.getFeatures( QgsFeatureRequest(nearestid)).next() mindist = inputgeom.distance(nnfeature.geometry()) elif (self.joinvl.wkbType() == QGis.WKBPolygon or self.joinvl.wkbType() == QGis.WKBPolygon25D or self.joinvl.wkbType() == QGis.WKBLineString or self.joinvl.wkbType() == QGis.WKBLineString25D): # Use the join layer index to speed up the join when # the join layer geometry type is polygon or line # and the input layer geometry type is point or an # approximation (point) nearestindexid = self.joinlind.nearestNeighbor( inputgeom.asPoint(), 1)[0] # Check for self join if self.selfjoin and nearestindexid == infeatureid: # Self join and same feature, so get the two # first two neighbours nearestindexes = self.joinlind.nearestNeighbor( inputgeom.asPoint(), 2) nearestindexid = nearestindexes[0] if (nearestindexid == infeatureid and len(nearestindexes) > 1): nearestindexid = nearestindexes[1] nnfeature = self.joinvl.getFeatures( QgsFeatureRequest(nearestindexid)).next() mindist = inputgeom.distance(nnfeature.geometry()) px = inputgeom.asPoint().x() py = inputgeom.asPoint().y() closefids = self.joinlind.intersects(QgsRectangle( px - mindist, py - mindist, px + mindist, py + mindist)) for closefid in closefids: if self.abort is True: break # Check for self join and same feature if self.selfjoin and closefid == infeatureid: continue closef = self.joinvl.getFeatures( QgsFeatureRequest(closefid)).next() thisdistance = inputgeom.distance(closef.geometry()) if thisdistance < mindist: mindist = thisdistance nnfeature = closef if mindist == 0: break else: # Join with no index use # Go through all the features from the join layer! for inFeatJoin in self.joinf: if self.abort is True: break joingeom = QgsGeometry(inFeatJoin.geometry()) thisdistance = inputgeom.distance(joingeom) # If the distance is 0, check for equality of the # features (in case it is a self join) if (thisdistance == 0 and self.selfjoin and infeatureid == inFeatJoin.id()): continue if thisdistance < mindist: mindist = thisdistance nnfeature = inFeatJoin # For 0 distance, settle with the first feature if mindist == 0: break else: # non-simple point input geometries (could be multipoint) if (self.nonpointexactindex): # Use the spatial index on the join layer (default). # First we do an approximate search # Get the input geometry centroid centroid = QgsGeometry(infeature.geometry()).centroid() centroidgeom = centroid.asPoint() # Find the nearest neighbour (index geometries only) nearestid = self.joinlind.nearestNeighbor(centroidgeom, 1)[0] # Check for self join if self.selfjoin and nearestid == infeatureid: # Self join and same feature, so get the two # first two neighbours nearestindexes = self.joinlind.nearestNeighbor( centroidgeom, 2) nearestid = nearestindexes[0] if nearestid == infeatureid and len(nearestindexes) > 1: nearestid = nearestindexes[1] nnfeature = self.joinvl.getFeatures( QgsFeatureRequest(nearestid)).next() mindist = inputgeom.distance(nnfeature.geometry()) # Calculate the search rectangle (inputgeom BBOX inpbbox = infeature.geometry().boundingBox() minx = inpbbox.xMinimum() - mindist maxx = inpbbox.xMaximum() + mindist miny = inpbbox.yMinimum() - mindist maxy = inpbbox.yMaximum() + mindist #minx = min(inpbbox.xMinimum(), centroidgeom.x() - mindist) #maxx = max(inpbbox.xMaximum(), centroidgeom.x() + mindist) #miny = min(inpbbox.yMinimum(), centroidgeom.y() - mindist) #maxy = max(inpbbox.yMaximum(), centroidgeom.y() + mindist) searchrectangle = QgsRectangle(minx, miny, maxx, maxy) # Fetch the candidate join geometries closefids = self.joinlind.intersects(searchrectangle) # Loop through the geometries and choose the closest # one for closefid in closefids: if self.abort is True: break # Check for self join and identical feature if self.selfjoin and closefid == infeatureid: continue closef = self.joinvl.getFeatures( QgsFeatureRequest(closefid)).next() thisdistance = inputgeom.distance(closef.geometry()) if thisdistance < mindist: mindist = thisdistance nnfeature = closef if mindist == 0: break else: # Join with no index use # Check all the features of the join layer! mindist = float("inf") # should not be necessary for inFeatJoin in self.joinf: if self.abort is True: break joingeom = QgsGeometry(inFeatJoin.geometry()) thisdistance = inputgeom.distance(joingeom) # If the distance is 0, check for equality of the # features (in case it is a self join) if (thisdistance == 0 and self.selfjoin and infeatureid == inFeatJoin.id()): continue if thisdistance < mindist: mindist = thisdistance nnfeature = inFeatJoin # For 0 distance, settle with the first feature if mindist == 0: break if not self.abort: atMapA = infeature.attributes() atMapB = nnfeature.attributes() attrs = [] attrs.extend(atMapA) attrs.extend(atMapB) attrs.append(mindist) outFeat = QgsFeature() # Use the original input layer geometry!: outFeat.setGeometry(QgsGeometry(infeature.geometry())) # Use the modified input layer geometry (could be # centroid) #outFeat.setGeometry(QgsGeometry(inputgeom)) outFeat.setAttributes(attrs) self.calculate_progress() self.features.append(outFeat) #self.mem_joinl.dataProvider().addFeatures([outFeat]) def tr(self, message): """Get the translation for a string using Qt translation API. We implement this ourselves since we do not inherit QObject. :param message: String for translation. :type message: str, QString :returns: Translated version of message. :rtype: QString """ # noinspection PyTypeChecker,PyArgumentList,PyCallByClass return QCoreApplication.translate('NNJoinEngine', message)
class Worker(QtCore.QObject): '''The worker that does the heavy lifting. /* QGIS offers spatial indexes to make spatial search more * effective. QgsSpatialIndex will find the nearest index * (approximate) geometry (rectangle) for a supplied point. * QgsSpatialIndex will only give correct results when searching * for the nearest neighbour of a point in a point data set. * So something has to be done for non-point data sets * * Non-point join data set: * A two pass search is performed. First the index is used to * find the nearest index geometry (approximation - rectangle), * and then compute the distance to the actual indexed geometry. * A rectangle is constructed from this (maximum minimum) * distance, and this rectangle is used to find all features in * the join data set that may be the closest feature to the given * point. * For all the features is this candidate set, the actual * distance to the given point is calculated, and the nearest * feature is returned. * * Non-point input data set: * First the centroid of the non-point input geometry is * calculated. Then the index is used to find the nearest * neighbour to this point (using the approximate index * geometry). * The distance vector to this feature, combined with the * bounding rectangle of the input feature is used to create a * search rectangle to find the candidate join geometries. * For all the features is this candidate set, the actual * distance to the given feature is calculated, and the nearest * feature is returned. * * Joins involving multi-geometry datasets are not supported * by a spatial index. * */ ''' # Define the signals used to communicate back to the application progress = QtCore.pyqtSignal(float) # For reporting progress status = QtCore.pyqtSignal(str) # For reporting status error = QtCore.pyqtSignal(str) # For reporting errors # Signal for sending over the result: finished = QtCore.pyqtSignal(bool, object) def __init__(self, inputvectorlayer, joinvectorlayer, outputlayername, joinprefix, distancefieldname="distance", approximateinputgeom=False, usejoinlayerapproximation=False, usejoinlayerindex=True, selectedinputonly=True, selectedjoinonly=True, excludecontaining=True): """Initialise. Arguments: inputvectorlayer -- (QgsVectorLayer) The base vector layer for the join joinvectorlayer -- (QgsVectorLayer) the join layer outputlayername -- (string) the name of the output memory layer joinprefix -- (string) the prefix to use for the join layer attributes in the output layer distancefieldname -- name of the (new) field where neighbour distance is stored approximateinputgeom -- (boolean) should the input geometry be approximated? Is only be set for non-single-point layers usejoinlayerindexapproximation -- (boolean) should the index geometry approximations be used for the join? usejoinlayerindex -- (boolean) should an index for the join layer be used. selectedinputonly -- Only selected features from the input layer selectedjoinonly -- Only selected features from the join layer excludecontaining -- exclude the containing polygon for points """ QtCore.QObject.__init__(self) # Essential! # Set a variable to control the use of indexes and exact # geometries for non-point input geometries self.nonpointexactindex = usejoinlayerindex # Creating instance variables from the parameters self.inpvl = inputvectorlayer self.joinvl = joinvectorlayer self.outputlayername = outputlayername self.joinprefix = joinprefix self.approximateinputgeom = approximateinputgeom self.usejoinlayerapprox = usejoinlayerapproximation self.selectedinonly = selectedinputonly self.selectedjoonly = selectedjoinonly self.excludecontaining = excludecontaining # Check if the layers are the same (self join) self.selfjoin = False if self.inpvl is self.joinvl: # This is a self join self.selfjoin = True # The name of the attribute for the calculated distance self.distancename = distancefieldname # Creating instance variables for the progress bar ++ # Number of elements that have been processed - updated by # calculate_progress self.processed = 0 # Current percentage of progress - updated by # calculate_progress self.percentage = 0 # Flag set by kill(), checked in the loop self.abort = False # Number of features in the input layer - used by # calculate_progress (set when needed) self.feature_count = 1 # The number of elements that is needed to increment the # progressbar (set when needed) self.increment = 0 def run(self): try: # Check if the layers look OK if self.inpvl is None or self.joinvl is None: self.status.emit('Layer is missing!') self.finished.emit(False, None) return # Check if there are features in the layers incount = 0 if self.selectedinonly: incount = self.inpvl.selectedFeatureCount() else: incount = self.inpvl.featureCount() if incount == 0: self.status.emit('Input layer has no features!') self.finished.emit(False, None) return joincount = 0 if self.selectedjoonly: joincount = self.joinvl.selectedFeatureCount() else: joincount = self.joinvl.featureCount() if joincount == 0: self.status.emit('Join layer has no features!') self.finished.emit(False, None) return # Get the wkbtype of the layers self.inpWkbType = self.inpvl.wkbType() self.joinWkbType = self.joinvl.wkbType() # Check if the input layer does not have geometries if (self.inpvl.geometryType() == QgsWkbTypes.NullGeometry): self.status.emit('No geometries in the input layer!') self.finished.emit(False, None) return # Check if the join layer does not have geometries if (self.joinvl.geometryType() == QgsWkbTypes.NullGeometry): self.status.emit('No geometries in the join layer!') self.finished.emit(False, None) return # Set the geometry type and prepare the output layer inpWkbTypetext = QgsWkbTypes.displayString(int(self.inpWkbType)) # self.inputmulti = QgsWkbTypes.isMultiType(self.inpWkbType) # self.status.emit('wkbtype: ' + inpWkbTypetext) # geometryType = self.inpvl.geometryType() # geometrytypetext = 'Point' # if geometryType == QgsWkbTypes.PointGeometry: # geometrytypetext = 'Point' # elif geometryType == QgsWkbTypes.LineGeometry: # geometrytypetext = 'LineString' # elif geometryType == QgsWkbTypes.PolygonGeometry: # geometrytypetext = 'Polygon' # if self.inputmulti: # geometrytypetext = 'Multi' + geometrytypetext # geomttext = geometrytypetext geomttext = inpWkbTypetext # Set the coordinate reference system to the input # layer's CRS using authid (proj4 may be more robust) if self.inpvl.crs() is not None: geomttext = (geomttext + "?crs=" + str(self.inpvl.crs().authid())) # Retrieve the fields from the input layer outfields = self.inpvl.fields().toList() # Retrieve the fields from the join layer if self.joinvl.fields() is not None: jfields = self.joinvl.fields().toList() for joinfield in jfields: outfields.append( QgsField(self.joinprefix + str(joinfield.name()), joinfield.type())) else: self.status.emit('Unable to get any join layer fields') # Add the nearest neighbour distance field # Check if there is already a "distance" field # (should be avoided in the user interface) # Try a new name if there is a collission collission = True trynumber = 1 distnameorg = self.distancename while collission: # Iterate until there are no collissions collission = False for field in outfields: # This check should not be necessary - handled in the UI if field.name() == self.distancename: self.status.emit( 'Distance field already exists - renaming!') # self.abort = True # self.finished.emit(False, None) # break collission = True self.distancename = distnameorg + str(trynumber) trynumber = trynumber + 1 outfields.append(QgsField(self.distancename, QVariant.Double)) # Create a memory layer using a CRS description self.mem_joinl = QgsVectorLayer(geomttext, self.outputlayername, "memory") # Set the CRS to the inputlayer's CRS self.mem_joinl.setCrs(self.inpvl.crs()) self.mem_joinl.startEditing() # Add the fields for field in outfields: self.mem_joinl.dataProvider().addAttributes([field]) # For an index to be used, the input layer has to be a # point layer, or the input layer geometries have to be # approximated to centroids, or the user has to have # accepted that a join layer index is used (for # non-point input layers). # (Could be extended to multipoint) if (self.inpWkbType == QgsWkbTypes.Point or self.inpWkbType == QgsWkbTypes.Point25D or self.approximateinputgeom or self.nonpointexactindex): # Number of features in the join layer - used by # calculate_progress for the index creation if self.selectedjoonly: self.feature_count = self.joinvl.selectedFeatureCount() else: self.feature_count = self.joinvl.featureCount() # Create a spatial index to speed up joining self.status.emit('Creating join layer index...') # The number of elements that is needed to increment the # progressbar - set early in run() self.increment = self.feature_count // 1000 self.joinlind = QgsSpatialIndex() # Include geometries to enable exact distance calculations # self.joinlind = QgsSpatialIndex(flags=[QgsSpatialIndex.FlagStoreFeatureGeometries]) if self.selectedjoonly: for feat in self.joinvl.getSelectedFeatures(): # Allow user abort if self.abort is True: break self.joinlind.insertFeature(feat) self.calculate_progress() else: for feat in self.joinvl.getFeatures(): # Allow user abort if self.abort is True: break self.joinlind.insertFeature(feat) self.calculate_progress() self.status.emit('Join layer index created!') self.processed = 0 self.percentage = 0 # self.calculate_progress() # Is the join layer a multi-geometry layer? # self.joinmulti = QgsWkbTypes.isMultiType(self.joinWkbType) # Does the join layer contain multi geometries? # Try to check the first feature # This is not used for anything yet self.joinmulti = False if self.selectedjoonly: feats = self.joinvl.getSelectedFeatures() else: feats = self.joinvl.getFeatures() if feats is not None: testfeature = next(feats) feats.rewind() feats.close() if testfeature is not None: if testfeature.hasGeometry(): if testfeature.geometry().isMultipart(): self.joinmulti = True # Prepare for the join by fetching the layers into memory # Add the input features to a list self.inputf = [] if self.selectedinonly: for f in self.inpvl.getSelectedFeatures(): self.inputf.append(f) else: for f in self.inpvl.getFeatures(): self.inputf.append(f) # Add the join features to a list (used in the join) self.joinf = [] if self.selectedjoonly: for f in self.joinvl.getSelectedFeatures(): self.joinf.append(f) else: for f in self.joinvl.getFeatures(): self.joinf.append(f) # Initialise the global variable that will contain the # result of the nearest neighbour spatial join (list of # features) self.features = [] # Do the join! # Number of features in the input layer - used by # calculate_progress for the join operation if self.selectedinonly: self.feature_count = self.inpvl.selectedFeatureCount() else: self.feature_count = self.inpvl.featureCount() # The number of elements that is needed to increment the # progressbar - set early in run() self.increment = self.feature_count // 1000 # Using the original features from the input layer for feat in self.inputf: # Allow user abort if self.abort is True: break self.do_indexjoin(feat) self.calculate_progress() self.mem_joinl.dataProvider().addFeatures(self.features) self.status.emit('Join finished') except: import traceback self.error.emit(traceback.format_exc()) self.finished.emit(False, None) if self.mem_joinl is not None: self.mem_joinl.rollBack() else: self.mem_joinl.commitChanges() if self.abort: self.finished.emit(False, None) else: self.status.emit('Delivering the memory layer...') self.finished.emit(True, self.mem_joinl) def calculate_progress(self): '''Update progress and emit a signal with the percentage''' self.processed = self.processed + 1 # update the progress bar at certain increments if (self.increment == 0 or self.processed % self.increment == 0): # Calculate percentage as integer perc_new = (self.processed * 100) / self.feature_count if perc_new > self.percentage: self.percentage = perc_new self.progress.emit(self.percentage) def kill(self): '''Kill the thread by setting the abort flag''' self.abort = True def do_indexjoin(self, feat): '''Find the nearest neigbour of a feature. Using an index, if possible Parameter: feat -- The feature for which a neighbour is sought ''' infeature = feat # Get the feature ID infeatureid = infeature.id() # self.status.emit('**infeatureid: ' + str(infeatureid)) # Get the feature geometry inputgeom = infeature.geometry() # Check for missing input geometry if inputgeom.isEmpty(): # Prepare the result feature atMapA = infeature.attributes() atMapB = [] for thefield in self.joinvl.fields(): atMapB.extend([None]) attrs = [] attrs.extend(atMapA) attrs.extend(atMapB) attrs.append(0 - float("inf")) # Create the feature outFeat = QgsFeature() # Use the original input layer geometry!: outFeat.setGeometry(infeature.geometry()) # Use the modified input layer geometry (could be # centroid) # outFeat.setGeometry(inputgeom) # Add the attributes outFeat.setAttributes(attrs) # self.calculate_progress() self.features.append(outFeat) # self.mem_joinl.dataProvider().addFeatures([outFeat]) self.status.emit("Warning: Input feature with " "missing geometry: " + str(infeature.id())) return # Shall approximate input geometries be used? if self.approximateinputgeom: # Use the centroid as the input geometry inputgeom = infeature.geometry().centroid() # Check if the coordinate systems are equal, if not, # transform the input feature! if (self.inpvl.crs() != self.joinvl.crs()): try: # inputgeom.transform(QgsCoordinateTransform( # self.inpvl.crs(), self.joinvl.crs(), None)) # transcontext = QgsCoordinateTransformContext() # inputgeom.transform(QgsCoordinateTransform( # self.inpvl.crs(), self.joinvl.crs(), transcontext)) inputgeom.transform( QgsCoordinateTransform(self.inpvl.crs(), self.joinvl.crs(), QgsProject.instance())) except: import traceback self.error.emit( self.tr('CRS Transformation error!') + ' - ' + traceback.format_exc()) self.abort = True return # Find the closest feature! nnfeature = None minfound = False mindist = float("inf") # If the input layer's geometry type is point, or has been # approximated to point (centroid), then a join index will # be used. # if ((QgsWkbTypes.geometryType(self.inpWkbType) == QgsWkbTypes.PointGeometry and # not QgsWkbTypes.isMultiType(self.inpWkbType)) or self.approximateinputgeom): if (self.approximateinputgeom or self.inpWkbType == QgsWkbTypes.Point or self.inpWkbType == QgsWkbTypes.Point25D): # Are there points on the join side? # Then the index nearest neighbour function is sufficient # if ((QgsWkbTypes.geometryType(self.joinWkbType) == QgsWkbTypes.PointGeometry and # not QgsWkbTypes.isMultiType(self.joinWkbType)) or self.usejoinlayerapprox): if (self.usejoinlayerapprox or self.joinWkbType == QgsWkbTypes.Point or self.joinWkbType == QgsWkbTypes.Point25D): # Is it a self join? if self.selfjoin: # Have to consider the two nearest neighbours nearestids = self.joinlind.nearestNeighbor( inputgeom.asPoint(), 2) fch = 0 # Which of the two features to choose if (nearestids[0] == infeatureid and len(nearestids) > 1): # The first feature is the same as the input # feature, so choose the second one fch = 1 # Get the feature! if False: #if self.selectedjoonly: # This caused problems (wrong results) in QGIS 3.0.1 nnfeature = next( self.joinvl.getSelectedFeatures( QgsFeatureRequest(nearestids[fch]))) else: nnfeature = next( self.joinvl.getFeatures( QgsFeatureRequest(nearestids[fch]))) # Not a self join else: # Not a self join, so we search for only the # nearest neighbour (1) nearestids = self.joinlind.nearestNeighbor( inputgeom.asPoint(), 1) # Get the feature! if len(nearestids) > 0: nearestid = nearestids[0] nnfeature = next( self.joinvl.getFeatures( QgsFeatureRequest(nearestid))) #else: #if self.selectedjoonly: # nnfeature = next(self.joinvl.getSelectedFeatures( # QgsFeatureRequest(nearestid))) if nnfeature is not None: mindist = inputgeom.distance(nnfeature.geometry()) minfound = True # Not points on the join side # Handle common (non multi) non-point geometries elif (self.joinWkbType == QgsWkbTypes.Polygon or self.joinWkbType == QgsWkbTypes.Polygon25D or self.joinWkbType == QgsWkbTypes.LineString or self.joinWkbType == QgsWkbTypes.LineString25D): # Use the join layer index to speed up the join when # the join layer geometry type is polygon or line # and the input layer geometry type is point or a # point approximation nearestids = self.joinlind.nearestNeighbor( inputgeom.asPoint(), 1) # Possibe index out of range!!! ??? nearestindexid = nearestids[0] # Check for self join (possible if approx input) if self.selfjoin and nearestindexid == infeatureid: # Self join and same feature, so get the # first two neighbours nearestindexes = self.joinlind.nearestNeighbor( inputgeom.asPoint(), 2) # Possibe index out of range!!! ??? nearestindexid = nearestindexes[0] if (nearestindexid == infeatureid and len(nearestindexes) > 1): nearestindexid = nearestindexes[1] # If exclude containing, check for containment if self.excludecontaining: contained = False nearfeature = next( self.joinvl.getFeatures( QgsFeatureRequest(nearestindexid))) # Check for containment if nearfeature.geometry().contains(inputgeom): contained = True if inputgeom.contains(nearfeature.geometry()): contained = True numberofnn = 2 # Assumes that nearestNeighbor returns hits in the same # sequence for all numbers of nearest neighbour while contained: if self.abort is True: break nearestindexes = self.joinlind.nearestNeighbor( inputgeom.asPoint(), numberofnn) if len(nearestindexes) < numberofnn: nearestindexid = nearestindexes[numberofnn - 2] self.status.emit('No non-containing geometries!') break else: nearestindexid = nearestindexes[numberofnn - 1] # Seems to respect selection...? nearfeature = next( self.joinvl.getFeatures( QgsFeatureRequest(nearestindexid))) # Check for containment # Works! if nearfeature.geometry().contains(inputgeom): contained = True elif inputgeom.contains(nearfeature.geometry()): contained = True else: contained = False numberofnn = numberofnn + 1 # end while # Get the feature among the candidates from the index #if self.selectedjoonly: # # Does not get the correct feature! # nnfeature = next(self.joinvl.getSelectedFeatures( # QgsFeatureRequest(nearestindexid))) # This seems to work also in the presence of selections nnfeature = next( self.joinvl.getFeatures(QgsFeatureRequest(nearestindexid))) mindist = inputgeom.distance(nnfeature.geometry()) if mindist == 0: insidep = nnfeature.geometry().contains( inputgeom.asPoint()) # self.status.emit('0 distance! - ' + str(nearestindexid)) # self.status.emit('Inside: ' + str(insidep)) px = inputgeom.asPoint().x() py = inputgeom.asPoint().y() # Search the neighbourhood closefids = self.joinlind.intersects( QgsRectangle(px - mindist, py - mindist, px + mindist, py + mindist)) for closefid in closefids: if self.abort is True: break # Check for self join and same feature if self.selfjoin and closefid == infeatureid: continue # If exclude containing, check for containment if self.excludecontaining: # Seems to respect selection...? closefeature = next( self.joinvl.getFeatures( QgsFeatureRequest(closefid))) # Check for containment if closefeature.geometry().contains( inputgeom.asPoint()): continue if False: #if self.selectedjoonly: closef = next( self.joinvl.getSelectedFeatures( QgsFeatureRequest(closefid))) else: closef = next( self.joinvl.getFeatures( QgsFeatureRequest(closefid))) thisdistance = inputgeom.distance(closef.geometry()) if thisdistance < mindist: mindist = thisdistance nnfeature = closef if mindist == 0: # self.status.emit(' Mindist = 0!') break # Other geometry on the join side (multi and more) else: # Join with no index use # Go through all the features from the join layer! for inFeatJoin in self.joinf: if self.abort is True: break joingeom = inFeatJoin.geometry() thisdistance = inputgeom.distance(joingeom) if thisdistance < 0: self.status.emit("Warning: Join feature with " "missing geometry: " + str(inFeatJoin.id())) continue # If the distance is 0, check for equality of the # features (in case it is a self join) if (thisdistance == 0 and self.selfjoin and infeatureid == inFeatJoin.id()): continue if thisdistance < mindist: mindist = thisdistance nnfeature = inFeatJoin # For 0 distance, settle with the first feature if mindist == 0: break # non (simple) point input geometries (could be multipoint) else: if (self.nonpointexactindex): # Use the spatial index on the join layer (default). # First we do an approximate search # Get the input geometry centroid centroid = infeature.geometry().centroid() centroidgeom = centroid.asPoint() # Find the nearest neighbour (index geometries only) # Possibe index out of range!!! ??? nearestid = self.joinlind.nearestNeighbor(centroidgeom, 1)[0] # Check for self join if self.selfjoin and nearestid == infeatureid: # Self join and same feature, so get the two # first two neighbours nearestindexes = self.joinlind.nearestNeighbor( centroidgeom, 2) nearestid = nearestindexes[0] if nearestid == infeatureid and len(nearestindexes) > 1: nearestid = nearestindexes[1] # Get the feature! if False: #if self.selectedjoonly: nnfeature = next( self.joinvl.getSelectedFeatures( QgsFeatureRequest(nearestid))) else: nnfeature = next( self.joinvl.getFeatures(QgsFeatureRequest(nearestid))) mindist = inputgeom.distance(nnfeature.geometry()) # Calculate the search rectangle (inputgeom BBOX inpbbox = infeature.geometry().boundingBox() minx = inpbbox.xMinimum() - mindist maxx = inpbbox.xMaximum() + mindist miny = inpbbox.yMinimum() - mindist maxy = inpbbox.yMaximum() + mindist # minx = min(inpbbox.xMinimum(), centroidgeom.x() - mindist) # maxx = max(inpbbox.xMaximum(), centroidgeom.x() + mindist) # miny = min(inpbbox.yMinimum(), centroidgeom.y() - mindist) # maxy = max(inpbbox.yMaximum(), centroidgeom.y() + mindist) searchrectangle = QgsRectangle(minx, miny, maxx, maxy) # Fetch the candidate join geometries closefids = self.joinlind.intersects(searchrectangle) # Loop through the geometries and choose the closest # one for closefid in closefids: if self.abort is True: break # Check for self join and identical feature if self.selfjoin and closefid == infeatureid: continue if False: #if self.selectedjoonly: closef = next( self.joinvl.getSelectedFeatures( QgsFeatureRequest(closefid))) else: closef = next( self.joinvl.getFeatures( QgsFeatureRequest(closefid))) thisdistance = inputgeom.distance(closef.geometry()) if thisdistance < mindist: mindist = thisdistance nnfeature = closef if mindist == 0: break else: # Join with no index use # Check all the features of the join layer! mindist = float("inf") # should not be necessary for inFeatJoin in self.joinf: if self.abort is True: break joingeom = inFeatJoin.geometry() thisdistance = inputgeom.distance(joingeom) if thisdistance < 0: self.status.emit("Warning: Join feature with " "missing geometry: " + str(inFeatJoin.id())) continue # If the distance is 0, check for equality of the # features (in case it is a self join) if (thisdistance == 0 and self.selfjoin and infeatureid == inFeatJoin.id()): continue if thisdistance < mindist: mindist = thisdistance nnfeature = inFeatJoin # For 0 distance, settle with the first feature if mindist == 0: break if not self.abort: # self.status.emit('Near feature - ' + str(nnfeature.id())) # Collect the attribute atMapA = infeature.attributes() if nnfeature is not None: atMapB = nnfeature.attributes() else: atMapB = [] for thefield in self.joinvl.fields(): atMapB.extend([None]) attrs = [] attrs.extend(atMapA) attrs.extend(atMapB) attrs.append(mindist) # Create the feature outFeat = QgsFeature() # Use the original input layer geometry!: outFeat.setGeometry(infeature.geometry()) # Use the modified input layer geometry (could be # centroid) # outFeat.setGeometry(inputgeom) # Add the attributes outFeat.setAttributes(attrs) # self.calculate_progress() self.features.append(outFeat) # self.mem_joinl.dataProvider().addFeatures([outFeat]) # end of do_indexjoin def tr(self, message): """Get the translation for a string using Qt translation API. We implement this ourselves since we do not inherit QObject. :param message: String for translation. :type message: str, QString :returns: Translated version of message. :rtype: QString """ # noinspection PyTypeChecker,PyArgumentList,PyCallByClass return QCoreApplication.translate('NNJoinEngine', message)
def testInteraction(self): # pylint: disable=too-many-statements """ Test tool interaction """ canvas = QgsMapCanvas() canvas.setDestinationCrs(QgsCoordinateReferenceSystem(4326)) canvas.setFrameStyle(0) canvas.resize(600, 400) self.assertEqual(canvas.width(), 600) self.assertEqual(canvas.height(), 400) layer = QgsVectorLayer("Polygon?crs=epsg:4326&field=fldtxt:string", "layer", "memory") f = QgsFeature() f.setAttributes(['a']) f.setGeometry(QgsGeometry.fromRect(QgsRectangle(5, 32, 15, 45))) f2 = QgsFeature() f2.setAttributes(['b']) f2.setGeometry(QgsGeometry.fromRect(QgsRectangle(15, 25, 18, 45))) success, (f, f2) = layer.dataProvider().addFeatures([f, f2]) self.assertTrue(success) canvas.setLayers([layer]) canvas.setExtent(QgsRectangle(10, 30, 20, 35)) canvas.show() handler = RedistrictHandler(layer, 'fldtxt') factory = DecoratorFactory() registry = DistrictRegistry(districts=['a', 'b']) tool = InteractiveRedistrictingTool(canvas, handler, district_registry=registry, decorator_factory=factory) # mouse over a feature's interior point = canvas.mapSettings().mapToPixel().transform(20, 33) event = QgsMapMouseEvent(canvas, QEvent.MouseMove, QPoint(point.x(), point.y())) tool.canvasMoveEvent(event) self.assertFalse(tool.is_active) self.assertFalse(tool.snap_indicator.match().isValid()) # mouse over a single feature's boundary (not valid district boundary) point = canvas.mapSettings().mapToPixel().transform(5, 33) event = QgsMapMouseEvent(canvas, QEvent.MouseMove, QPoint(point.x(), point.y())) tool.canvasMoveEvent(event) self.assertFalse(tool.is_active) self.assertFalse(tool.snap_indicator.match().isValid()) # mouse over a two feature's boundary (valid district boundary) point = canvas.mapSettings().mapToPixel().transform(15, 33) event = QgsMapMouseEvent(canvas, QEvent.MouseMove, QPoint(point.x(), point.y())) tool.canvasMoveEvent(event) self.assertFalse(tool.is_active) self.assertTrue(tool.snap_indicator.match().isValid()) # avoid segfault tool.snap_indicator.setMatch(QgsPointLocator.Match()) # clicks to ignore point = canvas.mapSettings().mapToPixel().transform(10, 33) event = QgsMapMouseEvent(canvas, QEvent.MouseButtonPress, QPoint(point.x(), point.y()), Qt.MidButton) tool.canvasPressEvent(event) self.assertFalse(tool.is_active) event = QgsMapMouseEvent(canvas, QEvent.MouseButtonPress, QPoint(point.x(), point.y()), Qt.RightButton) tool.canvasPressEvent(event) self.assertFalse(tool.is_active) # click over bad area point = canvas.mapSettings().mapToPixel().transform(10, 30) event = QgsMapMouseEvent(canvas, QEvent.MouseButtonPress, QPoint(point.x(), point.y()), Qt.LeftButton) tool.canvasPressEvent(event) self.assertFalse(tool.is_active) # click over feature area layer.startEditing() point = canvas.mapSettings().mapToPixel().transform(10, 33) event = QgsMapMouseEvent(canvas, QEvent.MouseButtonPress, QPoint(point.x(), point.y()), Qt.LeftButton) tool.canvasPressEvent(event) self.assertTrue(tool.is_active) self.assertEqual(tool.click_point.x(), 10) self.assertEqual(tool.click_point.y(), 33) self.assertEqual(tool.districts, {'a'}) self.assertFalse(tool.modified) # now move over current feature - should do nothing! point = canvas.mapSettings().mapToPixel().transform(10, 33) event = QgsMapMouseEvent(canvas, QEvent.MouseMove, QPoint(point.x(), point.y())) tool.canvasMoveEvent(event) self.assertTrue(tool.is_active) self.assertFalse(tool.modified) # move over other feature self.assertEqual(layer.getFeature(f2.id())[0], 'b') point = canvas.mapSettings().mapToPixel().transform(16, 33) event = QgsMapMouseEvent(canvas, QEvent.MouseMove, QPoint(point.x(), point.y())) tool.canvasMoveEvent(event) self.assertTrue(tool.is_active) self.assertEqual(tool.modified, {f2.id()}) self.assertEqual(tool.current_district, 'a') self.assertEqual(layer.getFeature(f2.id())[0], 'a') # move over nothing point = canvas.mapSettings().mapToPixel().transform(26, 33) event = QgsMapMouseEvent(canvas, QEvent.MouseMove, QPoint(point.x(), point.y())) tool.canvasMoveEvent(event) self.assertTrue(tool.is_active) self.assertEqual(tool.modified, {f2.id()}) # left click - commit changes event = QgsMapMouseEvent(canvas, QEvent.MouseButtonPress, QPoint(point.x(), point.y()), Qt.LeftButton) tool.canvasPressEvent(event) self.assertFalse(tool.is_active) layer.rollBack() layer.startEditing() # add a decorator tool.decorator_factory = TestDecoratorFactory() # now try with clicks over boundary point = canvas.mapSettings().mapToPixel().transform(15, 33) event = QgsMapMouseEvent(canvas, QEvent.MouseButtonPress, QPoint(point.x(), point.y()), Qt.LeftButton) tool.canvasPressEvent(event) self.assertTrue(tool.is_active) self.assertEqual(tool.click_point.x(), 15) self.assertEqual(tool.click_point.y(), 33) self.assertEqual(tool.districts, {'a', 'b'}) self.assertFalse(tool.modified) # move left self.assertEqual(layer.getFeature(f.id())[0], 'a') self.assertEqual(layer.getFeature(f2.id())[0], 'b') point = canvas.mapSettings().mapToPixel().transform(10, 33) event = QgsMapMouseEvent(canvas, QEvent.MouseMove, QPoint(point.x(), point.y())) tool.canvasMoveEvent(event) self.assertTrue(tool.is_active) self.assertEqual(tool.modified, {f.id()}) self.assertEqual(tool.current_district, 'b') self.assertEqual(layer.getFeature(f.id())[0], 'b') self.assertEqual(layer.getFeature(f2.id())[0], 'b') # move over nothing point = canvas.mapSettings().mapToPixel().transform(26, 33) event = QgsMapMouseEvent(canvas, QEvent.MouseMove, QPoint(point.x(), point.y())) tool.canvasMoveEvent(event) self.assertTrue(tool.is_active) self.assertEqual(tool.modified, {f.id()}) # right click - discard changes event = QgsMapMouseEvent(canvas, QEvent.MouseButtonPress, QPoint(point.x(), point.y()), Qt.RightButton) tool.canvasPressEvent(event) self.assertFalse(tool.is_active) self.assertEqual(layer.getFeature(f.id())[0], 'a') self.assertEqual(layer.getFeature(f2.id())[0], 'b') # try again, move right point = canvas.mapSettings().mapToPixel().transform(15, 33) event = QgsMapMouseEvent(canvas, QEvent.MouseButtonPress, QPoint(point.x(), point.y()), Qt.LeftButton) tool.canvasPressEvent(event) self.assertTrue(tool.is_active) self.assertEqual(tool.click_point.x(), 15) self.assertEqual(tool.click_point.y(), 33) self.assertEqual(tool.districts, {'a', 'b'}) self.assertFalse(tool.modified) # move right self.assertEqual(layer.getFeature(f.id())[0], 'a') self.assertEqual(layer.getFeature(f2.id())[0], 'b') point = canvas.mapSettings().mapToPixel().transform(17, 33) event = QgsMapMouseEvent(canvas, QEvent.MouseMove, QPoint(point.x(), point.y())) tool.canvasMoveEvent(event) self.assertTrue(tool.is_active) self.assertEqual(tool.modified, {f2.id()}) self.assertEqual(tool.current_district, 'a') self.assertEqual(layer.getFeature(f.id())[0], 'a') self.assertEqual(layer.getFeature(f2.id())[0], 'a') event = QgsMapMouseEvent(canvas, QEvent.MouseButtonPress, QPoint(point.x(), point.y()), Qt.RightButton) tool.canvasPressEvent(event) self.assertFalse(tool.is_active) self.assertEqual(layer.getFeature(f.id())[0], 'a') self.assertEqual(layer.getFeature(f2.id())[0], 'b') layer.rollBack()
def test_invalidGeometryFilter(self): layer = QgsVectorLayer("Polygon?field=x:string", "joinlayer", "memory") # add some features, one has invalid geometry pr = layer.dataProvider() f1 = QgsFeature(1) f1.setAttributes(["a"]) f1.setGeometry( QgsGeometry.fromWkt('Polygon((0 0, 1 0, 1 1, 0 1, 0 0))')) # valid f2 = QgsFeature(2) f2.setAttributes(["b"]) f2.setGeometry( QgsGeometry.fromWkt( 'Polygon((0 0, 1 0, 0 1, 1 1, 0 0))')) # invalid f3 = QgsFeature(3) f3.setAttributes(["c"]) f3.setGeometry( QgsGeometry.fromWkt('Polygon((0 0, 1 0, 1 1, 0 1, 0 0))')) # valid self.assertTrue(pr.addFeatures([f1, f2, f3])) res = [ f['x'] for f in layer.getFeatures(QgsFeatureRequest( ).setInvalidGeometryCheck(QgsFeatureRequest.GeometryNoCheck)) ] self.assertEqual(res, ['a', 'b', 'c']) res = [ f['x'] for f in layer.getFeatures(QgsFeatureRequest( ).setInvalidGeometryCheck(QgsFeatureRequest.GeometrySkipInvalid)) ] self.assertEqual(res, ['a', 'c']) res = [ f['x'] for f in layer.getFeatures( QgsFeatureRequest().setInvalidGeometryCheck( QgsFeatureRequest.GeometryAbortOnInvalid)) ] self.assertEqual(res, ['a']) # with callback self.callback_feature_val = None def callback(feature): self.callback_feature_val = feature['x'] res = [ f['x'] for f in layer.getFeatures(QgsFeatureRequest( ).setInvalidGeometryCheck(QgsFeatureRequest.GeometryAbortOnInvalid ).setInvalidGeometryCallback(callback)) ] self.assertEqual(res, ['a']) self.assertEqual(self.callback_feature_val, 'b') # clear callback res = [ f['x'] for f in layer.getFeatures(QgsFeatureRequest( ).setInvalidGeometryCheck(QgsFeatureRequest.GeometryAbortOnInvalid ).setInvalidGeometryCallback(None)) ] self.assertEqual(res, ['a']) # check with filter fids res = [ f['x'] for f in layer.getFeatures(QgsFeatureRequest().setFilterFid(f2.id( )).setInvalidGeometryCheck(QgsFeatureRequest.GeometryNoCheck)) ] self.assertEqual(res, ['b']) res = [ f['x'] for f in layer.getFeatures(QgsFeatureRequest().setFilterFid(f2.id( )).setInvalidGeometryCheck(QgsFeatureRequest.GeometrySkipInvalid)) ] self.assertEqual(res, []) res = [ f['x'] for f in layer.getFeatures(QgsFeatureRequest().setFilterFid( f2.id()).setInvalidGeometryCheck( QgsFeatureRequest.GeometryAbortOnInvalid)) ] self.assertEqual(res, []) f4 = QgsFeature(4) f4.setAttributes(["d"]) f4.setGeometry( QgsGeometry.fromWkt( 'Polygon((0 0, 1 0, 0 1, 1 1, 0 0))')) # invalid # check with added features layer.startEditing() self.assertTrue(layer.addFeatures([f4])) res = [ f['x'] for f in layer.getFeatures(QgsFeatureRequest( ).setInvalidGeometryCheck(QgsFeatureRequest.GeometryNoCheck)) ] self.assertEqual(set(res), {'a', 'b', 'c', 'd'}) res = [ f['x'] for f in layer.getFeatures(QgsFeatureRequest( ).setInvalidGeometryCheck(QgsFeatureRequest.GeometrySkipInvalid)) ] self.assertEqual(set(res), {'a', 'c'}) res = [ f['x'] for f in layer.getFeatures( QgsFeatureRequest().setInvalidGeometryCheck( QgsFeatureRequest.GeometryAbortOnInvalid)) ] self.assertEqual(res, ['a']) # check with features with changed geometry layer.rollBack() layer.startEditing() layer.changeGeometry( 2, QgsGeometry.fromWkt('Polygon((0 0, 1 0, 1 1, 0 1, 0 0))')) # valid layer.changeGeometry( 3, QgsGeometry.fromWkt( 'Polygon((0 0, 1 0, 0 1, 1 1, 0 0))')) # invalid res = [ f['x'] for f in layer.getFeatures(QgsFeatureRequest( ).setInvalidGeometryCheck(QgsFeatureRequest.GeometryNoCheck)) ] self.assertEqual(set(res), {'a', 'b', 'c'}) res = [ f['x'] for f in layer.getFeatures(QgsFeatureRequest( ).setInvalidGeometryCheck(QgsFeatureRequest.GeometrySkipInvalid)) ] self.assertEqual(set(res), {'a', 'b'}) res = [ f['x'] for f in layer.getFeatures( QgsFeatureRequest().setInvalidGeometryCheck( QgsFeatureRequest.GeometryAbortOnInvalid)) ] self.assertEqual(res, ['a', 'b']) layer.rollBack()
def test_add_feature_geometry(self): """ Test to add a feature with a geometry """ vl_pipes = QgsVectorLayer( self.dbconn + ' sslmode=disable key=\'pk\' table="qgis_test"."pipes" (geom) sql=', 'pipes', 'postgres') vl_leaks = QgsVectorLayer( self.dbconn + ' sslmode=disable key=\'pk\' table="qgis_test"."leaks" (geom) sql=', 'leaks', 'postgres') vl_leaks.startEditing() QgsProject.instance().addMapLayer(vl_pipes) QgsProject.instance().addMapLayer(vl_leaks) self.assertEqual(vl_pipes.featureCount(), 2) self.assertEqual(vl_leaks.featureCount(), 3) rel = QgsRelation() rel.setReferencingLayer(vl_leaks.id()) rel.setReferencedLayer(vl_pipes.id()) rel.addFieldPair('pipe', 'id') rel.setId('rel_pipe_leak') self.assertTrue(rel.isValid()) self.relMgr.addRelation(rel) # Mock vector layer tool to just set default value on created feature class DummyVlTools(QgsVectorLayerTools): def addFeature(self, layer, defaultValues, defaultGeometry): f = QgsFeature(layer.fields()) for idx, value in defaultValues.items(): f.setAttribute(idx, value) f.setGeometry(defaultGeometry) ok = layer.addFeature(f) return ok, f wrapper = QgsRelationWidgetWrapper(vl_leaks, rel) context = QgsAttributeEditorContext() vltool = DummyVlTools() context.setVectorLayerTools(vltool) context.setMapCanvas(self.mapCanvas) cadDockWidget = QgsAdvancedDigitizingDockWidget(self.mapCanvas) context.setCadDockWidget(cadDockWidget) wrapper.setContext(context) widget = wrapper.widget() widget.show() pipe = next(vl_pipes.getFeatures()) self.assertEqual(pipe.id(), 1) wrapper.setFeature(pipe) table_view = widget.findChild(QTableView) self.assertEqual(table_view.model().rowCount(), 1) btn = widget.findChild(QToolButton, 'mAddFeatureGeometryButton') self.assertTrue(btn.isVisible()) self.assertTrue(btn.isEnabled()) btn.click() self.assertTrue(self.mapCanvas.mapTool()) feature = QgsFeature(vl_leaks.fields()) feature.setGeometry(QgsGeometry.fromWkt('POINT(0 0.8)')) self.mapCanvas.mapTool().digitizingCompleted.emit(feature) self.assertEqual(table_view.model().rowCount(), 2) self.assertEqual(vl_leaks.featureCount(), 4) request = QgsFeatureRequest() request.addOrderBy("id", False) # get new created feature feat = next(vl_leaks.getFeatures('"id" is NULL')) self.assertTrue(feat.isValid()) self.assertTrue(feat.geometry().equals( QgsGeometry.fromWkt('POINT(0 0.8)'))) vl_leaks.rollBack()
def _test(autoTransaction): """Test buffer methods within and without transactions - create a feature - save - retrieve the feature - change geom and attrs - test changes are seen in the buffer """ def _check_feature(wkt): f = next(layer_a.getFeatures()) self.assertEqual(f.geometry().asWkt().upper(), wkt) f = list(buffer.addedFeatures().values())[0] self.assertEqual(f.geometry().asWkt().upper(), wkt) ml = QgsVectorLayer('Point?crs=epsg:4326&field=int:integer', 'test', 'memory') self.assertTrue(ml.isValid()) d = QTemporaryDir() options = QgsVectorFileWriter.SaveVectorOptions() options.driverName = 'GPKG' options.layerName = 'layer_a' err, _ = QgsVectorFileWriter.writeAsVectorFormatV2(ml, os.path.join(d.path(), 'transaction_test.gpkg'), QgsCoordinateTransformContext(), options) self.assertEqual(err, QgsVectorFileWriter.NoError) self.assertTrue(os.path.isfile(os.path.join(d.path(), 'transaction_test.gpkg'))) options.layerName = 'layer_b' options.actionOnExistingFile = QgsVectorFileWriter.CreateOrOverwriteLayer err, _ = QgsVectorFileWriter.writeAsVectorFormatV2(ml, os.path.join(d.path(), 'transaction_test.gpkg'), QgsCoordinateTransformContext(), options) layer_a = QgsVectorLayer(os.path.join(d.path(), 'transaction_test.gpkg|layername=layer_a')) self.assertTrue(layer_a.isValid()) project = QgsProject() project.setAutoTransaction(autoTransaction) project.addMapLayers([layer_a]) ########################################### # Tests with a new feature self.assertTrue(layer_a.startEditing()) buffer = layer_a.editBuffer() f = QgsFeature(layer_a.fields()) f.setAttribute('int', 123) f.setGeometry(QgsGeometry.fromWkt('point(7 45)')) self.assertTrue(layer_a.addFeatures([f])) _check_feature('POINT (7 45)') # Need to fetch the feature because its ID is NULL (-9223372036854775808) f = next(layer_a.getFeatures()) self.assertEqual(len(buffer.addedFeatures()), 1) layer_a.undoStack().undo() self.assertEqual(len(buffer.addedFeatures()), 0) layer_a.undoStack().redo() self.assertEqual(len(buffer.addedFeatures()), 1) f = list(buffer.addedFeatures().values())[0] self.assertEqual(f.attribute('int'), 123) # Now change attribute self.assertEqual(buffer.changedAttributeValues(), {}) layer_a.changeAttributeValue(f.id(), 1, 321) self.assertEqual(len(buffer.addedFeatures()), 1) # This is surprising: because it was a new feature it has been changed directly self.assertEqual(buffer.changedAttributeValues(), {}) f = list(buffer.addedFeatures().values())[0] self.assertEqual(f.attribute('int'), 321) layer_a.undoStack().undo() self.assertEqual(buffer.changedAttributeValues(), {}) f = list(buffer.addedFeatures().values())[0] self.assertEqual(f.attribute('int'), 123) f = next(layer_a.getFeatures()) self.assertEqual(f.attribute('int'), 123) # Change geometry f = next(layer_a.getFeatures()) self.assertTrue(layer_a.changeGeometry(f.id(), QgsGeometry.fromWkt('point(9 43)'))) _check_feature('POINT (9 43)') self.assertEqual(buffer.changedGeometries(), {}) layer_a.undoStack().undo() _check_feature('POINT (7 45)') self.assertEqual(buffer.changedGeometries(), {}) self.assertTrue(layer_a.changeGeometry(f.id(), QgsGeometry.fromWkt('point(9 43)'))) _check_feature('POINT (9 43)') self.assertTrue(layer_a.changeGeometry(f.id(), QgsGeometry.fromWkt('point(10 44)'))) _check_feature('POINT (10 44)') # This is anothr surprise: geometry edits get collapsed into a single # one because they have the same hardcoded id layer_a.undoStack().undo() _check_feature('POINT (7 45)') self.assertTrue(layer_a.commitChanges()) ########################################### # Tests with the existing feature # Get the feature f = next(layer_a.getFeatures()) self.assertTrue(f.isValid()) self.assertEqual(f.attribute('int'), 123) self.assertEqual(f.geometry().asWkt().upper(), 'POINT (7 45)') self.assertTrue(layer_a.startEditing()) layer_a.changeAttributeValue(f.id(), 1, 321) buffer = layer_a.editBuffer() self.assertEqual(buffer.changedAttributeValues(), {1: {1: 321}}) layer_a.undoStack().undo() self.assertEqual(buffer.changedAttributeValues(), {}) # Change geometry self.assertTrue(layer_a.changeGeometry(f.id(), QgsGeometry.fromWkt('point(9 43)'))) f = next(layer_a.getFeatures()) self.assertEqual(f.geometry().asWkt().upper(), 'POINT (9 43)') self.assertEqual(buffer.changedGeometries()[1].asWkt().upper(), 'POINT (9 43)') layer_a.undoStack().undo() self.assertEqual(buffer.changedGeometries(), {}) f = next(layer_a.getFeatures()) self.assertEqual(f.geometry().asWkt().upper(), 'POINT (7 45)') self.assertEqual(buffer.changedGeometries(), {}) # Delete an existing feature self.assertTrue(layer_a.deleteFeature(f.id())) with self.assertRaises(StopIteration): next(layer_a.getFeatures()) self.assertEqual(buffer.deletedFeatureIds(), [f.id()]) layer_a.undoStack().undo() self.assertTrue(layer_a.getFeature(f.id()).isValid()) self.assertEqual(buffer.deletedFeatureIds(), []) ########################################### # Test delete # Delete a new feature f = QgsFeature(layer_a.fields()) f.setAttribute('int', 555) f.setGeometry(QgsGeometry.fromWkt('point(8 46)')) self.assertTrue(layer_a.addFeatures([f])) f = [f for f in layer_a.getFeatures() if f.attribute('int') == 555][0] self.assertTrue(f.id() in buffer.addedFeatures()) self.assertTrue(layer_a.deleteFeature(f.id())) self.assertFalse(f.id() in buffer.addedFeatures()) self.assertFalse(f.id() in buffer.deletedFeatureIds()) layer_a.undoStack().undo() self.assertTrue(f.id() in buffer.addedFeatures()) ########################################### # Add attribute field = QgsField('attr1', QVariant.String) self.assertTrue(layer_a.addAttribute(field)) self.assertNotEqual(layer_a.fields().lookupField(field.name()), -1) self.assertEqual(buffer.addedAttributes(), [field]) layer_a.undoStack().undo() self.assertEqual(layer_a.fields().lookupField(field.name()), -1) self.assertEqual(buffer.addedAttributes(), []) layer_a.undoStack().redo() self.assertNotEqual(layer_a.fields().lookupField(field.name()), -1) self.assertEqual(buffer.addedAttributes(), [field]) self.assertTrue(layer_a.commitChanges()) ########################################### # Remove attribute self.assertTrue(layer_a.startEditing()) buffer = layer_a.editBuffer() attr_idx = layer_a.fields().lookupField(field.name()) self.assertNotEqual(attr_idx, -1) self.assertTrue(layer_a.deleteAttribute(attr_idx)) self.assertEqual(buffer.deletedAttributeIds(), [2]) self.assertEqual(layer_a.fields().lookupField(field.name()), -1) layer_a.undoStack().undo() self.assertEqual(buffer.deletedAttributeIds(), []) self.assertEqual(layer_a.fields().lookupField(field.name()), attr_idx) layer_a.undoStack().redo() self.assertEqual(buffer.deletedAttributeIds(), [2]) self.assertEqual(layer_a.fields().lookupField(field.name()), -1) self.assertTrue(layer_a.rollBack()) ########################################### # Rename attribute self.assertTrue(layer_a.startEditing()) attr_idx = layer_a.fields().lookupField(field.name()) self.assertNotEqual(attr_idx, -1) self.assertEqual(layer_a.fields().lookupField('new_name'), -1) self.assertTrue(layer_a.renameAttribute(attr_idx, 'new_name')) self.assertEqual(layer_a.fields().lookupField('new_name'), attr_idx) layer_a.undoStack().undo() self.assertEqual(layer_a.fields().lookupField(field.name()), attr_idx) self.assertEqual(layer_a.fields().lookupField('new_name'), -1) layer_a.undoStack().redo() self.assertEqual(layer_a.fields().lookupField('new_name'), attr_idx) self.assertEqual(layer_a.fields().lookupField(field.name()), -1)
class GoogleDriveLayer(QObject): """ Pretend we are a data provider """ invalidEdit = pyqtSignal() deferredEdit = pyqtSignal() dirty = False restyled = False doing_attr_update = False geom_types = ("Point", "LineString", "Polygon","Unknown","NoGeometry") def __init__(self, parent, authorization, layer_name, spreadsheet_id = None, loading_layer = None, importing_layer = None, crs_def = None, geom_type = None, test = None, precision=17): ''' Initialize the layer by reading the Google drive sheet, creating a memory layer, and adding records to it, optionally used fo layer export to google drive :param parent: :param authorization: google authorization object :param layer_name: the layer name :param spreadsheet_id: the spreadsheetId of the table to download and load as qgis layer; default to None :param loading_layer: the layer loading from project file; default to None :param importing_layer: the layer that is being imported; default to None :param test: used for testing ''' super(GoogleDriveLayer, self).__init__() # Save the path to the file soe we can update it in response to edits self.test = test self.parent = parent self.iface = parent.iface self.precision = None bar = progressBar(self, 'loading google drive layer') self.service_drive = service_drive(authorization) self.client_id = authorization.client_id self.authorization = authorization if spreadsheet_id: self.spreadsheet_id = spreadsheet_id self.service_sheet = service_spreadsheet(authorization, self.spreadsheet_id) self.precision = self.service_sheet.precision() elif importing_layer: self.precision = precision layer_as_list = self.qgis_layer_to_list(importing_layer) logger("1") self.service_sheet = service_spreadsheet(authorization, new_sheet_name=importing_layer.name(),new_sheet_data=True) self.spreadsheet_id = self.service_sheet.spreadsheetId logger("2") self.service_sheet.set_crs(importing_layer.crs().authid()) self.service_sheet.set_geom_type(self.geom_types[importing_layer.geometryType()]) self.service_sheet.set_style_qgis(self.layer_style_to_xml(importing_layer)) self.service_sheet.set_style_sld(self.SLD_to_xml(importing_layer)) self.service_sheet.set_style_mapbox(self.layer_style_to_json(importing_layer)) self.service_sheet.set_precision(precision) self.dirty = True self.saveFieldTypes(importing_layer.fields()) logger("5") self.service_sheet.upload_rows(layer_as_list) logger("6") self.service_sheet.lockedEntry.connect(self.rollbackRow) self.reader = self.service_sheet.get_sheet_values() self.header = self.reader[0] self.service_sheet.update_header() self.crs_def = self.service_sheet.crs() self.geom_type = self.service_sheet.geom_type() logger("LOADED GOOGLE SHEET LAYER: %s CRS_ID:%s GEOM_type:%s" % (self.service_sheet.name,self.crs_def, self.geom_type)) # Build up the URI needed to create memory layer if loading_layer: self.lyr = loading_layer attrIds = [i for i in range (0, self.lyr.fields().count())] self.lyr.dataProvider().deleteAttributes(attrIds) self.lyr.updateFields() else: self.uri = self.uri = "Multi%s?crs=%s&index=yes" % (self.geom_type, self.crs_def) #logger(self.uri) self.lyr = QgsVectorLayer(self.uri, layer_name, 'memory') if importing_layer: logger("3") self.update_summary_sheet() logger("4") self.saveMetadataState(importing_layer) ''' #check if public layer meta = self.service_drive.getFileMetadata(self.spreadsheet_id) permissions = meta["metadata"]["permissions"] self.is_public_layer = False for permission in permissions: if permission["id"] == "anyoneWithLink": self.is_public_layer = True logger("is public layer") ''' fields_types = self.service_sheet.get_line("ROWS", 1, sheet="settings") attributes = [] for i in range(2,len(self.header)): if self.header[i][:8] != 'DELETED_': type_pack = fields_types[i].split("|") attributes.append(QgsField(name=self.header[i],type=int(type_pack[0]), len=int(type_pack[1]), prec=int(type_pack[2]))) #self.uri += u'&field={}:{}'.format(fld.decode('utf8'), field_name_types[fld]) self.lyr.dataProvider().addAttributes(attributes) self.lyr.updateFields() self.xml_to_layer_style(self.lyr,self.service_sheet.style()) #disable memory layers save checking when closing project self.lyr.setCustomProperty("googleDriveId", self.spreadsheet_id) self.lyr.setCustomProperty("skipMemoryLayersCheck", 1) self.add_records() # Make connections self.makeConnections(self.lyr) # Add the layer the map if not loading_layer: QgsProject.instance().addMapLayer(self.lyr) self.lyr.setAbstract(self.service_sheet.abstract()) self.lyr.gdrive_control = self bar.stop("Layer %s succesfully loaded" % layer_name) def makeConnections(self,lyr): ''' The method handle default signal connections to the connected qgis memory layer :param lyr: qgis layer :return: ''' self.deferredEdit.connect(self.apply_locks) lyr.editingStarted.connect(self.editing_started) lyr.editingStopped.connect(self.editing_stopped) lyr.committedAttributesDeleted.connect(self.attributes_deleted) lyr.committedAttributesAdded .connect(self.attributes_added) lyr.committedFeaturesAdded.connect(self.features_added) lyr.committedGeometriesChanges.connect(self.geometry_changed) lyr.committedAttributeValuesChanges.connect(self.attributes_changed) lyr.destroyed.connect(self.unsubscribe) lyr.beforeCommitChanges.connect(self.inspect_changes) lyr.styleChanged.connect(self.style_changed) #add contextual menu self.sync_with_google_drive_action = QAction(QIcon(os.path.join(self.parent.plugin_dir,'sync.png')), "Sync with Google drive", self.iface ) self.iface.addCustomActionForLayerType(self.sync_with_google_drive_action, "", QgsMapLayer.VectorLayer, allLayers=False) self.iface.addCustomActionForLayer(self.sync_with_google_drive_action, lyr) self.sync_with_google_drive_action.triggered.connect(self.sync_with_google_drive) lyr.gDriveInterface = self def add_records(self): ''' Add records to the memory layer by reading the Google Sheet ''' self.lyr.startEditing() for i, row in enumerate(self.reader[1:]): flds = collections.OrderedDict(list(zip(self.header, row))) status = flds.pop('STATUS') if status != 'D': #non caricare i deleted wkt_geom = unpack(flds.pop('WKTGEOMETRY')) #fid = int(flds.pop('FEATUREID')) feature = QgsFeature() geometry = QgsGeometry.fromWkt(wkt_geom) feature.setGeometry(geometry) cleared_row = [] #[fid] for field, attribute in flds.items(): if field[:8] != 'DELETED_': #skip deleted fields if attribute == '()': cleared_row.append(qgis.core.NULL) else: cleared_row.append(attribute) else: logger( "DELETED " + field) feature.setAttributes(cleared_row) self.lyr.addFeature(feature) self.lyr.commitChanges() def saveMetadataState(self,lyr=None,metadata=None): logger ("metadata changed") if not metadata: metadata = self.get_layer_metadata(lyr) self.service_sheet.update_metadata(self.spreadsheet_id, metadata) def style_changed(self): ''' landing method for rendererChanged signal. It stores xml qgis style definition to the setting sheet ''' logger( "style changed") self.dirty = True self.restyled = True def renew_connection(self): ''' when connection stay alive too long we have to rebuild service ''' self.service_drive.renew_connection() def sync_with_google_drive(self): self.renew_connection() self.update_from_subscription() self.update_summary_sheet() def update_from_subscription(self): ''' The method updates qgis memory layer with changes made by other users and sincronize the local qgis layer with google sheet spreadsheet ''' self.renew_connection() bar = progressBar(self, 'updating local layer from remote') # fix_print_with_import if self.service_sheet.canEdit: updates = self.service_sheet.get_line('COLUMNS','A', sheet=self.client_id) if updates: self.service_sheet.erase_cells(self.client_id) else: new_changes_log_rows = self.service_sheet.get_line("COLUMNS",'A',sheet="changes_log") if len(new_changes_log_rows) > len(self.service_sheet.changes_log_rows): updates = new_changes_log_rows[-len(new_changes_log_rows)+len(self.service_sheet.changes_log_rows):] self.service_sheet.changes_log_rows = new_changes_log_rows else: updates = [] # fix_print_with_import for update in updates: decode_update = update.split("|") if decode_update[0] in ('new_feature', 'delete_feature', 'update_geometry', 'update_attributes'): sheet_feature = self.service_sheet.get_line('ROWS',decode_update[1]) if decode_update[0] == 'new_feature': feat = QgsFeature() geom = QgsGeometry().fromWkt(unpack(sheet_feature[0])) feat.setGeometry(geom) feat.setAttributes(sheet_feature[2:]) logger(( "updating from subscription, new_feature: " + str(self.lyr.dataProvider().addFeatures([feat])))) else: sheet_feature_id = decode_update[1] feat = next(self.lyr.getFeatures(QgsFeatureRequest(QgsExpression(' "FEATUREID" = %s' % sheet_feature_id)))) if decode_update[0] == 'delete_feature': # fix_print_with_import logger("updating from subscription, delete_feature: " + str(self.lyr.dataProvider().deleteFeatures([feat.id()]))) elif decode_update[0] == 'update_geometry': update_set = {feat.id(): QgsGeometry().fromWkt(unpack(sheet_feature[0]))} # fix_print_with_import logger("updating from subscription, update_geometry: " + str(self.lyr.dataProvider().changeGeometryValues(update_set))) elif decode_update[0] == 'update_attributes': new_attributes = sheet_feature_id[2:] attributes_map = {} for i in range(0, len(new_attributes)): attributes_map[i] = new_attributes[i] update_map = {feat.id(): attributes_map,} # fix_print_with_import logger("updating from subscription, update_attributes: " +(self.lyr.dataProvider().changeAttributeValues(update_map))) elif decode_update[0] == 'add_field': field_a1_notation = self.service_sheet.header_map[decode_update[1]] type_def = self.service_sheet.sheet_cell('settings!%s1' % field_a1_notation) type_def_decoded = type_def.split("|") new_field = QgsField(name=decode_update[1],type=int(type_def_decoded[0]), len=int(type_def_decoded[1]), prec=int(type_def_decoded[2])) # fix_print_with_import logger("updating from subscription, add_field: ", + (self.lyr.dataProvider().addAttributes([new_field]))) self.lyr.updateFields() elif decode_update[0] == 'delete_field': # fix_print_with_import logger("updating from subscription, delete_field: " + str(self.lyr.dataProvider().deleteAttributes([self.lyr.dataProvider().fields().fieldNameIndex(decode_update[1])]))) self.lyr.updateFields() self.lyr.triggerRepaint() bar.stop("local layer updated") def editing_started(self): ''' Connect to the edit buffer so we can capture geometry and attribute changes ''' # fix_print_with_import logger("editing") self.update_from_subscription() self.bar = None if self.service_sheet.canEdit: self.activeThreads = 0 self.editing = True self.lyr.geometryChanged.connect(self.buffer_geometry_changed) self.lyr.attributeValueChanged.connect(self.buffer_attributes_changed) self.lyr.beforeCommitChanges.connect(self.catch_deleted) self.lyr.beforeRollBack.connect(self.rollBack) self.invalidEdit.connect(self.rollBack) self.changes_log=[] self.locking_queue = [] self.timer = 0 else: #refuse editing if file is read only self.lyr.rollBack() def buffer_geometry_changed(self,fid,geom): ''' Landing method for geometryChanged signal. When a geometry is modified, the row related to the modified feature is marked as modified by local user. Further edits to the modified feature are denied to other concurrent users :param fid: :param geom: ''' if self.editing: self.lock_feature(fid) def buffer_attributes_changed(self,fid,attr_id,value): ''' Landing method for attributeValueChanged signal. When an attribute is modified, the row related to the modified feature is marked as modified by local user. Further edits to the modified feature are denied to other concurrent users :param fid: :param attr_id: :param value: ''' if self.editing: self.lock_feature(fid) def lock_feature(self, fid): """ The row in google sheet linked to feature that has been modified is locked Filling the the STATUS column with the client_id. Further edits to the modified feature are denied to other concurrent users """ if fid >= 0: # fid <0 means that the change relates to newly created features not yet present in the sheet self.locks_applied = None feature_locking = next(self.lyr.getFeatures(QgsFeatureRequest(fid))) locking_row_id = feature_locking[0] self.locking_queue.append(locking_row_id) _thread.start_new_thread(self.deferred_apply_locks, ()) def deferred_apply_locks(self): if self.timer > 0: self.timer = 0 return else: while self.timer < 100: self.timer += 1 sleep(0.01) #APPLY_LOCKS self.deferredEdit.emit() #self.apply_locks() def apply_locks(self): if self.locks_applied: return self.locks_applied = True status_range = [] for row_id in self.locking_queue: status_range.append(['STATUS', row_id]) status_control = self.service_sheet.multicell(status_range) if "valueRanges" in status_control: mods = [] for valueRange in status_control["valueRanges"]: if valueRange["values"][0][0] in ('()', None): mods.append([valueRange["range"],0,self.client_id]) row_id = valueRange["range"].split('B')[-1] if mods: self.service_sheet.set_multicell(mods, A1notation=True) self.locking_queue = [] self.timer = 0 def rollbackRow(self,row): logger(str(row)) rollback_row = self.service_sheet.get_line('ROWS',row) #print ("Original row", rollback_row) wkt_geom = unpack(rollback_row[0]) attrs = {} for attr in range(2,len(rollback_row)): attrs[attr-2] = rollback_row[attr] try: #identify feature to rollback feat = next(self.lyr.getFeatures(QgsFeatureRequest(QgsExpression(' "FEATUREID" = %s' % rollback_row[2])))) geom_update = {feat.id(): QgsGeometry.fromWkt(wkt_geom)} #rollback geometry self.lyr.dataProvider().changeGeometryValues(geom_update) #rollback attributes attrs_update = {feat.id():attrs} #print ("UPDATES", geom_update, attrs_update) self.lyr.dataProvider().changeFeatures(attrs_update, geom_update) except: logger("RESTORING DELETED FEATURE") #rollback deleted feat restore_feat = QgsFeature(self.lyr.fields()) restore_feat.setGeometry(QgsGeometry.fromWkt(wkt_geom)) for key,attr in attrs.items(): logger(str(key)+str(attr)) restore_feat.setAttribute(key, attr) self.lyr.dataProvider().addFeatures([restore_feat]) self.lyr.triggerRepaint() message = self.iface.messageBar().createMessage("GooGIS plugin:","Feature %d is locked by %s: pending edits not applied" % (rollback_row[2], rollback_row[1])) self.iface.messageBar().pushWidget(message, Qgis.Warning, 5) def rollBack(self): """ before rollback changes status field is cleared and the edits from concurrent user are allowed """ # fix_print_with_import logger("ROLLBACK") try: self.lyr.geometryChanged.disconnect(self.buffer_geometry_changed) except: pass try: self.lyr.attributeValueChanged.disconnect(self.buffer_attributes_changed) except: pass self.renew_connection() self.clean_status_row() try: self.lyr.beforeRollBack.disconnect(self.rollBack) except: pass #self.lyr.geometryChanged.disconnect(self.buffer_geometry_changed) #self.lyr.attributeValueChanged.disconnect(self.buffer_attributes_changed) self.editing = False def editing_stopped(self): """ Update the remote sheet if changes were committed """ # fix_print_with_import logger("EDITING_STOPPED") self.renew_connection() self.clean_status_row() if self.service_sheet.canEdit: self.service_sheet.advertise(self.changes_log) self.editing = False #if self.dirty: # self.update_summary_sheet() # self.dirty = None if self.bar: self.bar.stop("update to remote finished") def inspect_changes(self): ''' here we can inspect changes before commit them self.deleted_list = [] for deleted in self.lyr.editBuffer().deletedAttributeIds(): self.deleted_list.append(self.lyr.fields().at(deleted).name()) print self.deleted_list logger("attributes_added") for field in self.lyr.editBuffer().addedAttributes(): print "ADDED FIELD", field.name() self.service_sheet.add_column([field.name()], fill_with_null = True) ''' # fix_print_with_import logger("INSPECT_CHANGES") pass def attributes_added(self, layer, added): """ Landing method for attributeAdded. Fields (attribute) changed New colums are appended to the google drive spreadsheets creating remote colums syncronized with the local layer fields. Edits are advertized to other concurrent users for subsequent syncronization with remote table """ logger("attributes_added") for field in added: logger( "ADDED FIELD %s" % field.name()) self.service_sheet.add_column([field.name()], fill_with_null = True) self.service_sheet.add_column(["%d|%d|%d" % (field.type(), field.length(), field.precision())],child_sheet="settings", fill_with_null = None) self.changes_log.append('%s|%s' % ('add_field', field.name())) self.dirty = True def attributes_deleted(self, layer, deleted_ids): """ Landing method for attributeDeleted. Fields (attribute) are deleted New colums are marked as deleted in the google drive spreadsheets. Edits are advertized to other concurrent users for subsequent syncronization with remote table """ logger("attributes_deleted") for deleted in deleted_ids: deleted_name = self.service_sheet.mark_field_as_deleted(deleted) self.changes_log.append('%s|%s' % ('delete_field', deleted_name)) self.dirty = True def features_added(self, layer, features): """ Landing method for featureAdded. The new features are written adding rows to the google drive spreadsheets . Edits are advertized to other concurrent users for subsequent syncronization with remote table """ logger("features added") for count,feature in enumerate(features): new_fid = self.service_sheet.new_fid() self.lyr.dataProvider().changeAttributeValues({feature.id() : {0: new_fid}}) feature.setAttribute(0, new_fid+count) new_row_dict = {}.fromkeys(self.service_sheet.header,'()') new_row_dict['WKTGEOMETRY'] = pack(feature.geometry().asWkt(precision=self.precision)) new_row_dict['STATUS'] = '()' for i,item in enumerate(feature.attributes()): fieldName = self.lyr.fields().at(i).name() try: new_row_dict[fieldName] = item.toString(format = Qt.ISODate) except: if not item or item == qgis.core.NULL: new_row_dict[fieldName] = '()' else: new_row_dict[fieldName] = item new_row_dict['FEATUREID'] = '=ROW()' #assure correspondance between feature and sheet row result = self.service_sheet.add_row(new_row_dict) sheet_new_row = int(result['updates']['updatedRange'].split('!A')[1].split(':')[0]) self.changes_log.append('%s|%s' % ('new_feature', str(new_fid))) self.dirty = True def catch_deleted(self): """ Landing method for beforeCommitChanges signal. The method intercepts edits before they were written to the layer so from deleted features can be extracted the feature id of the google drive spreadsheet related rows. The affected rows are marked as deleted and hidden away from the layer syncronization """ self.bar = progressBar(self, 'updating local edits to remote') """ Features removed; but before commit """ deleted_ids = self.lyr.editBuffer().deletedFeatureIds() if deleted_ids: deleted_mods = [] for fid in deleted_ids: removed_feat = next(self.lyr.dataProvider().getFeatures(QgsFeatureRequest(fid))) removed_row = removed_feat[0] logger ("Deleting FEATUREID %s" % removed_row) deleted_mods.append(("STATUS",removed_row,'D')) self.changes_log.append('%s|%s' % ('delete_feature', str(removed_row))) if deleted_mods: self.service_sheet.set_protected_multicell(deleted_mods) self.dirty = True def geometry_changed(self, layer, geom_map): """ Landing method for geometryChange signal. Features geometries changed The edited geometry, not locked by other users, are written to the google drive spreadsheets modifying the related rows. the WKT geometry definition is zipped and then base64 encoded for a compact storage (sigle cells string contents can't be larger the 50000 bytes) Edits are advertized to other concurrent users for subsequent syncronization with remote table """ geometry_mod = [] for fid,geom in geom_map.items(): feature_changing = next(self.lyr.getFeatures(QgsFeatureRequest(fid))) row_id = feature_changing[0] wkt = geom.asWkt(precision=self.precision) geometry_mod.append(('WKTGEOMETRY',row_id, pack(wkt) )) logger ("Updated FEATUREID %s geometry" % row_id) self.changes_log.append('%s|%s' % ('update_geometry', str(row_id))) value_mods_result = self.service_sheet.set_protected_multicell(geometry_mod, lockBy=self.client_id) self.dirty = True def attributes_changed(self, layer, changes): """ Landing method for attributeChange. Attribute values changed Edited feature, not locked by other users, are written to the google drive spreadsheets modifying the related rows. Edits are advertized to other concurrent users for subsequent syncronization with remote table """ if not self.doing_attr_update: #print "changes",changes attribute_mods = [] for fid,attrib_change in changes.items(): feature_changing = next(self.lyr.getFeatures(QgsFeatureRequest(fid))) row_id = feature_changing[0] logger ( "Attribute changing FEATUREID: %s" % row_id) for attrib_idx, new_value in attrib_change.items(): fieldName = QgsProject.instance().mapLayer(layer).fields().field(attrib_idx).name() if fieldName == 'FEATUREID': logger("can't modify FEATUREID") continue try: cleaned_value = new_value.toString(format = Qt.ISODate) except: if not new_value or new_value == qgis.core.NULL: cleaned_value = '()' else: cleaned_value = new_value attribute_mods.append((fieldName,row_id, cleaned_value)) self.changes_log.append('%s|%s' % ('update_attributes', str(row_id))) if attribute_mods: attribute_mods_result = self.service_sheet.set_protected_multicell(attribute_mods, lockBy=self.client_id) print (attribute_mods) print (attribute_mods_result) self.dirty = True def clean_status_row(self): status_line = self.service_sheet.get_line("COLUMNS","B") clean_status_mods = [] for row_line, row_value in enumerate(status_line): if row_value == self.client_id: clean_status_mods.append(("STATUS",row_line+1,'()')) value_mods_result = self.service_sheet.set_multicell(clean_status_mods) return value_mods_result def unsubscribe(self): ''' When a read/write layer is removed from the legend the remote subscription sheet is removed and update summary sheet if dirty ''' self.renew_connection() self.service_sheet.unsubscribe() def qgis_layer_to_csv(self,qgis_layer): ''' method to transform the specified qgis layer in a csv object for uploading :param qgis_layer: :return: csv object ''' stream = io.BytesIO() writer = csv.writer(stream, delimiter=',', quotechar='"', lineterminator='\n') row = ["WKTGEOMETRY","FEATUREID","STATUS"] for feat in qgis_layer.getFeatures(): for field in feat.fields().toList(): row.append(field.name().encode("utf-8")) break writer.writerow(row) for feat in qgis_layer.getFeatures(): row = [pack(feat.geometry().asWkt(precision=self.precision)),feat.id(),"()"] for field in feat.fields().toList(): if feat[field.name()] == qgis.core.NULL: content = "()" else: if type(feat[field.name()]) == str: content = feat[field.name()].encode("utf-8") else: content = feat[field.name()] row.append(content) writer.writerow(row) stream.seek(0) #csv.reader(stream, delimiter=',', quotechar='"', lineterminator='\n') return stream def qgis_layer_to_list(self,qgis_layer): ''' method to transform the specified qgis layer in list of rows (field/value) dicts for uploading :param qgis_layer: :return: row list object ''' row = ["WKTGEOMETRY","STATUS","FEATUREID"] for feat in qgis_layer.getFeatures(): for field in feat.fields().toList(): row.append(field.name()) #row.append(str(field.name()).encode("utf-8"))# slugify(field.name()) break rows = [row] for feat in qgis_layer.getFeatures(): row = [pack(feat.geometry().asWkt(precision=self.precision)),"()","=ROW()"] # =ROW() perfect row/featureid correspondance if len(row[0]) > 50000: # ignore features with geometry > 50000 bytes zipped continue for field in feat.fields().toList(): if feat[field.name()] == qgis.core.NULL: content = "()" else: if type(feat[field.name()]) == str: content = feat[field.name()] #feat[field.name()].encode("utf-8") elif field.typeName() in ('Date', 'Time'): content = feat[field.name()].toString(format = Qt.ISODate) else: content = feat[field.name()] row.append(content) rows.append(row) #csv.reader(stream, delimiter=',', quotechar='"', lineterminator='\n') return rows def saveFieldTypes(self,fields): ''' writes the layer field types to the setting sheet :param fields: :return: ''' types_array = ["s1","s2","4|4|0"] #default featureId type to longint for field in fields.toList(): types_array.append("%d|%d|%d" % (field.type(), field.length(), field.precision())) # fix_print_with_import self.service_sheet.update_cells('settings!A1',types_array) def layer_style_to_xml(self,qgis_layer): ''' saves qgis style to the setting sheet :param qgis_layer: :return: ''' XMLDocument = QDomDocument("qgis_style") XMLStyleNode = XMLDocument.createElement("style") XMLDocument.appendChild(XMLStyleNode) error = None rw_context = QgsReadWriteContext() rw_context.setPathResolver( QgsProject.instance().pathResolver() ) qgis_layer.writeSymbology(XMLStyleNode, XMLDocument, error,rw_context) xmldoc = XMLDocument.toString(1) return xmldoc def SLD_to_xml(self,qgis_layer): ''' saves SLD style to the setting sheet. Not used, keeped here for further extensions. :param qgis_layer: :return: ''' XMLDocument = QDomDocument("sld_style") error = None qgis_layer.exportSldStyle(XMLDocument, error) xmldoc = XMLDocument.toString(1) return xmldoc def xml_to_layer_style(self,qgis_layer,xml): ''' retrieve qgis style from the setting sheet :param qgis_layer: :return: ''' XMLDocument = QDomDocument() error = None XMLDocument.setContent(xml) XMLStyleNode = XMLDocument.namedItem("style") rw_context = QgsReadWriteContext() rw_context.setPathResolver( QgsProject.instance().pathResolver() ) qgis_layer.readSymbology(XMLStyleNode, error, rw_context) def layer_style_to_json(self, qgis_layer): #old_mapbox_style = toMapboxgl([qgis_layer]) mapbox_style,icons,warnings = layerStyleAsMapbox(qgis_layer) if warnings: logger("Warning exporting to mapboxgl style: " + json.dumps(warnings)) else: logger("mapboxgl style ok") return mapbox_style def get_gdrive_id(self): ''' returns spreadsheet_id associated with layer :return: spreadsheet_id associated with layer ''' return self.spreadsheet_id def get_service_drive(self): ''' returns the google drive wrapper object associated with layer :return: google drive wrapper object ''' return self.service_drive def get_service_sheet(self): ''' returns the google spreadsheet wrapper object associated with layer :return: google spreadsheet wrapper object ''' return self.service_sheet def wgs84_extent(self,extent): llp = self.transformToWGS84(QgsPointXY(extent.xMinimum(),extent.yMinimum())) rtp = self.transformToWGS84(QgsPointXY(extent.xMaximum(),extent.yMaximum())) return QgsRectangle(llp,rtp) def transformToWGS84(self, pPoint): crsDest = QgsCoordinateReferenceSystem(4326) # WGS 84 xform = QgsCoordinateTransform(self.lyr.crs(), crsDest, QgsProject.instance()) return xform.transform(pPoint) # forward transformation: src -> dest 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 def update_summary_sheet(self,lyr=None, force=None): ''' Creates a summary sheet with thumbnail, layer metadata and online view link ''' #create a layer snapshot and upload it to google drive if not lyr: lyr = self.lyr mapbox_style = self.service_sheet.sheet_cell('settings!A5') if not mapbox_style: logger("migrating mapbox style") self.service_sheet.set_style_mapbox(self.layer_style_to_json(self.lyr)) if not force and not self.dirty and not self.restyled: return if self.restyled: self.service_sheet.set_style_qgis(self.layer_style_to_xml(self.lyr)) self.service_sheet.set_style_sld(self.SLD_to_xml(self.lyr)) self.service_sheet.set_style_mapbox(self.layer_style_to_json(self.lyr)) self.saveMetadataState() canvas = QgsMapCanvas() canvas.resize(QSize(600,600)) canvas.setCanvasColor(Qt.white) canvas.setExtent(lyr.extent()) canvas.setLayers([lyr]) canvas.refresh() canvas.update() settings = canvas.mapSettings() settings.setLayers([lyr]) job = QgsMapRendererParallelJob(settings) job.start() job.waitForFinished() image = job.renderedImage() transparent_image = QImage(image.width(), image.height(), QImage.Format_ARGB32) transparent_image.fill(Qt.transparent) p = QPainter(transparent_image) mask = image.createMaskFromColor(QColor(255, 255, 255).rgb(), Qt.MaskInColor) p.setClipRegion(QRegion(QBitmap(QPixmap.fromImage(mask)))) p.drawPixmap(0, 0, QPixmap.fromImage(image)) p.end() tmp_path = os.path.join(self.parent.plugin_dir,self.service_sheet.name+".png") transparent_image.save(tmp_path,"PNG") image_istances = self.service_drive.list_files(mimeTypeFilter='image/png',filename=self.service_sheet.name+".png") for imagename, image_props in image_istances.items(): self.service_drive.delete_file(image_props['id']) result = self.service_drive.upload_image(tmp_path) self.service_drive.add_permission(result['id'],'anyone','reader') webLink = result['webContentLink'] #'https://drive.google.com/uc?export=view&id='+result['id'] logger("webLink:" + webLink) canvas.setDestinationCrs(QgsCoordinateReferenceSystem(4326)) worldfile = QgsMapSettingsUtils.worldFileContent(settings) lonlat_min = self.transformToWGS84(QgsPointXY(canvas.extent().xMinimum(), canvas.extent().yMinimum())) lonlat_max = self.transformToWGS84(QgsPointXY(canvas.extent().xMaximum(), canvas.extent().yMaximum())) keymap_extent = [lonlat_min.x(),lonlat_min.y(),lonlat_max.x(),lonlat_max.y()] os.remove(tmp_path) #update layer metadata summary_id = self.service_sheet.add_sheet('summary', no_grid=True) appPropsUpdate = [ ["keymap",webLink], ["worldfile",pack(worldfile)], ["keymap_extent", json.dumps(keymap_extent)] ] res = self.service_sheet.update_appProperties(self.spreadsheet_id,appPropsUpdate) self.saveMetadataState(metadata=self.get_layer_metadata()) self.parent.public_db.setKey(self.spreadsheet_id, dict(self.get_layer_metadata()+appPropsUpdate),only_update=True) #merge cells to visualize snapshot and aaply image snapshot request_body = { 'requests': [{ 'mergeCells': { "range": { "sheetId": summary_id, "startRowIndex": 9, "endRowIndex": 32, "startColumnIndex": 0, "endColumnIndex": 9, }, "mergeType": 'MERGE_ALL' } }] } self.service_sheet.service.spreadsheets().batchUpdate(spreadsheetId=self.spreadsheet_id, body=request_body).execute() self.service_sheet.set_sheet_cell('summary!A10','=IMAGE("%s",3)' % webLink) permissions = self.service_drive.file_property(self.spreadsheet_id,'permissions') for permission in permissions: if permission['type'] == 'anyone': public = True break else: public = False if public: update_range = 'summary!A9:B9' update_body = { "range": update_range, "values": [['public link', "https://enricofer.github.io/gdrive_provider/weblink/converter.html?spreadsheet_id="+self.spreadsheet_id]] } self.service_sheet.service.spreadsheets().values().update(spreadsheetId=self.spreadsheet_id,range=update_range, body=update_body, valueInputOption='USER_ENTERED').execute() #hide worksheets except summary sheets = self.service_sheet.get_sheets() #self.service_sheet.toggle_sheet('summary', sheets['summary'], hidden=None) for sheet_name,sheet_id in sheets.items(): if not sheet_name == 'summary': self.service_sheet.toggle_sheet(sheet_name, sheet_id, hidden=True)