def testSubsetAttributes(self): req = QgsFeatureRequest() self.assertFalse(req.subsetOfAttributes()) self.assertFalse(req.flags() & QgsFeatureRequest.SubsetOfAttributes) req.setSubsetOfAttributes([1, 4]) self.assertEqual(req.subsetOfAttributes(), [1, 4]) self.assertTrue(req.flags() & QgsFeatureRequest.SubsetOfAttributes) req.setNoAttributes() self.assertEqual(req.subsetOfAttributes(), []) self.assertTrue(req.flags() & QgsFeatureRequest.SubsetOfAttributes) req.setSubsetOfAttributes([]) self.assertFalse(req.subsetOfAttributes()) self.assertTrue(req.flags() & QgsFeatureRequest.SubsetOfAttributes) req.setFlags(QgsFeatureRequest.Flags()) f = QgsFields() f.append(QgsField('a', QVariant.String)) f.append(QgsField('b', QVariant.String)) f.append(QgsField('c', QVariant.String)) req.setSubsetOfAttributes(['a', 'c'], f) self.assertEqual(req.subsetOfAttributes(), [0, 2]) self.assertTrue(req.flags() & QgsFeatureRequest.SubsetOfAttributes)
def select(self, mode): try: lay = self.iface.activeLayer() geo = self.geo_buffered req = QgsFeatureRequest() req.setFilterRect(geo.boundingBox()) req.setNoAttributes() if mode == 0: selFeat = [] else: selFeat = lay.selectedFeatureIds() for feat in lay.getFeatures(req): if geo.intersects(feat.geometry()): if mode < 2: selFeat.append(feat.id()) else: try: selFeat.remove(feat.id()) except: pass lay.selectByIds(selFeat) self.gtomain.runcmd(self.config.get('tools', [])) except Exception as e: self.info.err(e)
def get_plots_related_to_parcel(self, db, t_id, field_name=ID_FIELD, plot_layer=None, uebaunit_table=None): """ :param db: DB Connector object :param t_id: parcel t_id :param field_name: The field name to get from DB for the matching features, use None for the QGIS internal ID :param plot_layer: Plot QGIS layer, in case it exists already in the caller :param uebaunit_table: UEBaunit QGIS table, in case it exists already in the caller :return: list of plot ids related to the parcel """ required_layers = { PLOT_TABLE: { 'name': PLOT_TABLE, 'geometry': QgsWkbTypes.PolygonGeometry }, UEBAUNIT_TABLE: { 'name': UEBAUNIT_TABLE, 'geometry': None } } if plot_layer is not None: del required_layers[PLOT_TABLE] if uebaunit_table is not None: del required_layers[UEBAUNIT_TABLE] if required_layers: res_layers = self.qgis_utils.get_layers(db, required_layers, load=True) if PLOT_TABLE in required_layers: plot_layer = res_layers[PLOT_TABLE] if plot_layer is None: self.qgis_utils.message_emitted.emit( QCoreApplication.translate( "LADM_DATA", "Plot layer couldn't be found... {}").format( db.get_description()), Qgis.Warning) return if UEBAUNIT_TABLE in required_layers: uebaunit_table = res_layers[UEBAUNIT_TABLE] if uebaunit_table is None: self.qgis_utils.message_emitted.emit( QCoreApplication.translate( "LADM_DATA", "UEBAUnit table couldn't be found... {}").format( db.get_description()), Qgis.Warning) return features = uebaunit_table.getFeatures( "{}={} AND {} IS NOT NULL".format(UEBAUNIT_TABLE_PARCEL_FIELD, t_id, UEBAUNIT_TABLE_PLOT_FIELD)) plot_t_ids = list() for feature in features: plot_t_ids.append(feature[UEBAUNIT_TABLE_PLOT_FIELD]) if field_name == ID_FIELD: return plot_t_ids plot_ids = list() if plot_t_ids: request = QgsFeatureRequest( QgsExpression("{} IN ({})".format( ID_FIELD, ",".join([str(id) for id in plot_t_ids])))) field_found = False if field_name is None: # We are only interested in the QGIS internal id, no need to get other fields request.setNoAttributes() else: field_found = plot_layer.fields().indexOf(field_name) != -1 if field_found: request.setSubsetOfAttributes([field_name], plot_layer.fields()) request.setFlags(QgsFeatureRequest.NoGeometry) features = plot_layer.getFeatures(request) for feature in features: if field_name is None: plot_ids.append(feature.id()) else: if field_found: plot_ids.append(feature[field_name]) return plot_ids
def count_qgis_features(qgis_layer, qgis_feature_request=None, bbox_filter=None, attribute_filters=None, search_filter=None, extra_expression=None, extra_subset_string=None, **kwargs): """Returns a list of QgsFeatures from the QGIS vector layer, with optional filter options. The API can be used in two distinct ways (that are not mutually exclusive): 1. pass in a pre-configured QgsFeatureRequest instance 2. pass a series of filter attributes and let this method configure the QgsFeatureRequest. :param qgis_layer: the QGIS vector layer instance :type qgis_layer: QgsVectorLayer :param qgis_feature_request: the QGIS feature request :type qgis_feature_request: QgsFeatureRequest, optional :param bbox_filter: BBOX filter in layer's CRS, defaults to None :type bbox_filter: QgsRectangle, optional :param attribute_filters: dictionary of attribute filters combined with AND, defaults to None :type attribute_filters: dict, optional :param search_filter: string filter for all fields :type search_filter: str, optional :param: exclude_fields: list of fields to exclude from returned data, '__all__' for no attributes :param: extra_expression: extra expression for filtering features :type: extra_expression: str, optional :param: extra_subset_string: extra subset string (provider side WHERE condition) for filtering features :type: extra_subset_string: str, optional :return: list of features :rtype: QgsFeature list """ # Remove no_filters condition because featureCount() # is cached, so it could fail on multi-processes deploy # ------------------------------------------------------ # Fast track for no filters # no_filters = (attribute_filters is None # and (bbox_filter is None or bbox_filter.isEmpty()) and # attribute_filters is None and # extra_expression is None and # extra_subset_string is None) # # if qgis_feature_request is not None: # no_filters = (no_filters and # qgis_feature_request.filterRect().isEmpty() and # qgis_feature_request.filterType() == QgsFeatureRequest.FilterNone) # else: if not qgis_feature_request: qgis_feature_request = QgsFeatureRequest() # if no_filters: # # Try to patch a possible Oracle views QGIS featureCount bug. # qgs_fc = qgis_layer.featureCount() # if qgs_fc != -1: # return qgis_layer.featureCount() qgis_feature_request.setNoAttributes() if qgis_feature_request.limit() != -1: qgis_feature_request = QgsFeatureRequest(qgis_feature_request) qgis_feature_request.setLimit(-1) return len( __get_qgis_features( qgis_layer, qgis_feature_request, bbox_filter, attribute_filters, search_filter, False, #with_geometry, None, #page, None, #page_size, None, #ordering, None, #exclude_fields, extra_expression, extra_subset_string))
def __get_qgis_features(qgis_layer, qgis_feature_request=None, bbox_filter=None, attribute_filters=None, search_filter=None, with_geometry=True, page=None, page_size=None, ordering=None, exclude_fields=None, extra_expression=None, extra_subset_string=None): """Private implementation for count and get""" if qgis_feature_request is None: qgis_feature_request = QgsFeatureRequest() if exclude_fields is not None: if exclude_fields == '__all__': qgis_feature_request.setNoAttributes() else: qgis_feature_request.setSubsetOfAttributes([ name for name in qgis_layer.fields().names() if name not in exclude_fields ], qgis_layer.fields()) expression_parts = [] if extra_expression is not None: expression_parts.append(extra_expression) if not with_geometry: qgis_feature_request.setFlags(QgsFeatureRequest.NoGeometry) if bbox_filter is not None: assert isinstance(bbox_filter, QgsRectangle) qgis_feature_request.setFilterRect(bbox_filter) # Ordering if ordering is not None: ascending = True if ordering.startswith('-'): ordering = ordering[1:] ascending = False order_by = QgsFeatureRequest.OrderBy( [QgsFeatureRequest.OrderByClause('"%s"' % ordering, ascending)]) qgis_feature_request.setOrderBy(order_by) # Search if search_filter is not None: exp_template = '"{field_name}" ILIKE \'%' + search_filter.replace( '\'', '\\\'') + '%\'' exp_parts = [] for f in qgis_layer.fields(): exp_parts.append( exp_template.format(field_name=f.name().replace('"', '\\"'))) expression_parts.append(' OR '.join(exp_parts)) # Attribute filters if attribute_filters is not None: exp_parts = [] for field_name, field_value in attribute_filters.items(): exp_parts.append('"{field_name}" ILIKE \'%{field_value}%\''.format( field_name=field_name.replace('"', '\\"'), field_value=str(field_value).replace('\'', '\\\''))) expression_parts.append(' AND '.join(exp_parts)) offset = 0 feature_count = qgis_layer.featureCount() if page is not None and page_size is not None: page_size = int(page_size) page = int(page) offset = page_size * (page - 1) feature_count = page_size * page # Set to max, without taking filters into account qgis_feature_request.setLimit(feature_count) else: page_size = None # make sure it's none # Fetch features if expression_parts: qgis_feature_request.combineFilterExpression( '(' + ') AND ('.join(expression_parts) + ')') logger.debug( 'Fetching features from layer {layer_name} - filter expression: {filter} - BBOX: {bbox}' .format(layer_name=qgis_layer.name(), filter=qgis_feature_request.filterExpression(), bbox=qgis_feature_request.filterRect())) features = [] original_subset_string = qgis_layer.subsetString() if extra_subset_string is not None: subset_string = original_subset_string if subset_string: qgis_layer.setSubsetString( "({original_subset_string}) AND ({extra_subset_string})". format(original_subset_string=original_subset_string, extra_subset_string=extra_subset_string)) else: qgis_layer.setSubsetString(extra_subset_string) iterator = qgis_layer.getFeatures(qgis_feature_request) try: for _ in range(offset): next(iterator) if page_size is not None: for __ in range(page_size): features.append(next(iterator)) else: while True: features.append(next(iterator)) except StopIteration: pass if extra_subset_string is not None: qgis_layer.setSubsetString(original_subset_string) return features
def feature_validator(feature, layer): """Validate a QGIS feature by checking QGIS fields constraints The logic here is to: - if geometry is not None check if geometry type matches the layer type - loop through the fields and check for constraints: - NOT NULL, skip the next check if this fails - UNIQUE (only if not NULL) - EXPRESSION (QgsExpression configured in the form), always evaluated, even in case of NULLs Note: only hard constraints are checked! :param feature: QGIS feature :type feature: QgsFeature :param layer: QGIS layer :type layer: QgsVectorLayer :return: a dictionary of errors for each field + geometry :rtype: dict """ errors = dict() geometry = feature.geometry() data_provider = layer.dataProvider() def _has_default_value(field_index, field): return ( # Provider level data_provider.defaultValueClause(field_index) or data_provider.defaultValue(field_index) or field.defaultValueDefinition().isValid()) # Check geometry type if not geometry.isNull() and geometry.wkbType() != layer.wkbType(): if not (geometry.wkbType() == QgsWkbTypes.Point25D and layer.wkbType() == QgsWkbTypes.PointZ or geometry.wkbType() == QgsWkbTypes.Polygon25D and layer.wkbType() == QgsWkbTypes.PolygonZ or geometry.wkbType() == QgsWkbTypes.LineString25D and layer.wkbType() == QgsWkbTypes.LineStringZ or geometry.wkbType() == QgsWkbTypes.MultiPoint25D and layer.wkbType() == QgsWkbTypes.MultiPointZ or geometry.wkbType() == QgsWkbTypes.MultiPolygon25D and layer.wkbType() == QgsWkbTypes.MultiPolygonZ or geometry.wkbType() == QgsWkbTypes.MultiLineString25D and layer.wkbType() == QgsWkbTypes.MultiLineStringZ): errors['geometry'] = _( 'Feature geometry type %s does not match layer type: %s') % ( QgsWkbTypes.displayString(geometry.wkbType()), QgsWkbTypes.displayString(layer.wkbType())) def _set_error(field_name, error): if not field_name in errors: errors[field_name] = [] errors[field_name].append(error) # Check fields "hard" constraints for field_index in range(layer.fields().count()): field = layer.fields().field(field_index) # check if fields is a join field: if layer.fields().fieldOrigin(field_index) == QgsFields.OriginJoin: continue # Check not null first, if it fails skip other tests (unique and expression) value = feature.attribute(field.name()) # If there is a default value we assume it's not NULL (we cannot really know at this point # what will be the result of the default value clause evaluation, it might even be provider-side if (value is None or value == QVariant()) and not _has_default_value(field_index, field): not_null = (field.constraints().constraintOrigin( QgsFieldConstraints.ConstraintNotNull) != QgsFieldConstraints.ConstraintOriginNotSet and field.constraints().constraintStrength( QgsFieldConstraints.ConstraintNotNull) == QgsFieldConstraints.ConstraintStrengthHard) if not_null: _set_error(field.name(), _('Field value must be NOT NULL')) continue value = feature.attribute(field_index) # Skip if NULL, not sure if we want to continue in this case but it seems pointless # to check for unique or type compatibility on NULLs if value is not None and value != QVariant(): if not QVariant(value).convert(field.type()): _set_error( field.name(), _('Field value \'%s\' cannot be converted to %s') % (value, QVariant.typeToName(field.type()))) unique = (field.constraints().constraintOrigin( QgsFieldConstraints.ConstraintUnique) != QgsFieldConstraints.ConstraintOriginNotSet and field.constraints().constraintStrength( QgsFieldConstraints.ConstraintUnique) == QgsFieldConstraints.ConstraintStrengthHard) if unique: # Search for features, excluding self if it's an update request = QgsFeatureRequest() request.setNoAttributes() request.setFlags(QgsFeatureRequest.NoGeometry) request.setLimit(2) if field.isNumeric(): request.setFilterExpression( '"%s" = %s' % (field.name().replace('"', '\\"'), value)) elif field.type() == QVariant.String: request.setFilterExpression( '"%s" = \'%s\'' % (field.name().replace( '"', '\\"'), value.replace("'", "\\'"))) elif field.type() == QVariant.Date: request.setFilterExpression( 'to_date("%s") = \'%s\'' % (field.name().replace( '"', '\\"'), value.toString(Qt.ISODate))) elif field.type() == QVariant.DateTime: request.setFilterExpression( 'to_datetime("{field_name}") = \'{date_time_string}\' OR to_datetime("{field_name}") = \'{date_time_string}.000\'' .format(field_name=field.name().replace('"', '\\"'), date_time_string=value.toString(Qt.ISODate))) elif field.type( ) == QVariant.Bool: # This does not make any sense, but still request.setFilterExpression( '"%s" = %s' % (field.name().replace( '"', '\\"'), 'true' if value else 'false')) else: # All the other formats: let's convert to string and hope for the best request.setFilterExpression( '"%s" = \'%s\'' % (field.name().replace('"', '\\"'), value.toString())) # Exclude same feature by id found = [ f.id() for f in layer.getFeatures(request) if f.id() != feature.id() ] if len(found) > 0: _set_error(field.name(), _('Field value must be UNIQUE')) # Check for expressions, even in case of NULL because expressions may want to check for combined # conditions on multiple fields expression = (field.constraints().constraintOrigin( QgsFieldConstraints.ConstraintExpression) != QgsFieldConstraints.ConstraintOriginNotSet and field.constraints().constraintStrength( QgsFieldConstraints.ConstraintExpression) == QgsFieldConstraints.ConstraintStrengthHard) if expression: constraints = field.constraints() expression = constraints.constraintExpression() description = constraints.constraintDescription() value = feature.attribute(field_index) exp = QgsExpression(expression) context = QgsExpressionContextUtils.createFeatureBasedContext( feature, layer.fields()) context.appendScopes( QgsExpressionContextUtils.globalProjectLayerScopes(layer)) if not bool(exp.evaluate(context)): if not description: description = _('Expression check violation') _set_error(field.name(), _("%s Expression: %s") % (description, expression)) return errors
def isochrone_from_layer(input_qgis_layer_id, profile, params, project_id, qgis_layer_id, connection_id, new_layer_name, name, style, process_info=None): """Generate isochrones asynchronously from an input QGIS point layer. This function must be run as an asynchronous task. Expected params (dict): { "locations" null, // <-- will be populated in batches by the function "range_type":"time", "range":[480], "interval":60, "location_type":"start", "attributes":[ "area", "reachfactor", "total_pop" ] } * "range": Maximum range value of the analysis in seconds for time and metres for distance. Alternatively a comma separated list of specific single range values. * "locations": The locations to use for the route as an array of longitude/latitude pairs Returns: - in case of errors: { 'result': False, 'error': 'error message' } - in case of success { 'result': True, 'qgis_layer_id': qgis_layer_id } :param input_qgis_layer_id: QGIS layer ID of the points layer which contains the locations for the isochrones :type input_qgis_layer_id: str :param profile: ORS profile (such as `driving-car`) :type profile: str :param params: ORS params :type profile: str :param project_id: QDjango Project pk for the new or the existing layer :type project_id: int :param qgis_layer_id: optional, QGIS layer id :type qgis_layer_id: QGIS layer id :param connection_id: optional, connection id or the special value `__shapefile__`, `__spatialite__` or `__geopackage__` :type connection: str :param new_layer_name: optional, name of the new layer :type new_layer_name: str :param name: optional, name of the isochrone, default to current datetime :type name: str :param style: optional, dictionary with style properties: example {'color': [100, 50, 123], 'transparency': 0.5, 'stroke_width: 3 } :type style: dict :param process_info: optional Huey process information :type process_info: ProcessInfo :raises Exception: raise on error :rtype: dict """ project = get_object_or_404(Project, pk=project_id) # Loop through features from the layer input_layer = project.qgis_project.mapLayer(input_qgis_layer_id) # Check preconditions assert input_layer is not None and input_layer.geometryType( ) == QgsWkbTypes.PointGeometry # Store range rang = params['range'] req = QgsFeatureRequest() req.setNoAttributes() ct = QgsCoordinateTransform(input_layer.crs(), QgsCoordinateReferenceSystem(4326), project.qgis_project.transformContext()) # Collect point batches points = [] def _process_batch(points, qgis_layer_id): #import time; time.sleep(20) params['locations'] = points # Note: passing a range list is mutually exclusive # with "interval" result = isochrone(profile, params) if result.status_code != status.HTTP_200_OK: jcontent = json.loads(result.content.decode('utf-8')) try: error_message = jcontent['error']['message'] except (KeyError, TypeError): error_message = jcontent['error'] raise Exception( _('Error generating isochrone: %s.') % error_message) return add_geojson_features(result.content.decode('utf-8'), project, qgis_layer_id, connection_id, new_layer_name, name, style) feature_count = input_layer.featureCount() if process_info is not None: process_info.update_total(feature_count) for f in input_layer.getFeatures(req): g = f.geometry() if ct.isValid(): g.transform(ct) for point in g.constParts(): points.append([point.x(), point.y()]) if len(points) >= ORS_MAX_LOCATIONS: qgis_layer_id = _process_batch(points, qgis_layer_id) connection_id = None style = None points = [] if process_info is not None: process_info.update(n=1) if len(points) > 0: qgis_layer_id = _process_batch(points, qgis_layer_id) if qgis_layer_id is None: raise Exception( _('Unknown error adding results to the destination layer.')) return {'qgis_layer_id': qgis_layer_id}