def testCreateFieldEqualityExpression(self): e = QgsExpression() # test when value is null field = "myfield" value = QVariant() res = '"myfield" IS NULL' self.assertEqual(e.createFieldEqualityExpression(field, value), res) # test when value is null and field name has a quote field = "my'field" value = QVariant() res = '"my\'field" IS NULL' self.assertEqual(e.createFieldEqualityExpression(field, value), res) # test when field name has a quote and value is an int field = "my'field" value = 5 res = '"my\'field" = 5' self.assertEqual(e.createFieldEqualityExpression(field, value), res) # test when field name has a quote and value is a string field = "my'field" value = '5' res = '"my\'field" = \'5\'' self.assertEqual(e.createFieldEqualityExpression(field, value), res) # test when field name has a quote and value is a boolean field = "my'field" value = True res = '"my\'field" = TRUE' self.assertEqual(e.createFieldEqualityExpression(field, value), res)
def paint(self, painter, option, widget): # pylint: disable=missing-docstring, unused-argument, too-many-locals if self.image is not None: painter.drawImage(0, 0, self.image) return image_size = self.canvas.mapSettings().outputSize() self.image = QImage(image_size.width(), image_size.height(), QImage.Format_ARGB32) self.image.fill(0) image_painter = QPainter(self.image) render_context = QgsRenderContext.fromQPainter(image_painter) image_painter.setRenderHint(QPainter.Antialiasing, True) rect = self.canvas.mapSettings().visibleExtent() request = QgsFeatureRequest() request.setFilterRect(rect) request.setFilterExpression(QgsExpression.createFieldEqualityExpression('type', self.task)) for f in self.electorate_layer.getFeatures(request): # pole, dist = f.geometry().clipped(rect).poleOfInaccessibility(rect.width() / 30) pixel = self.toCanvasCoordinates(f.geometry().clipped(rect).centroid().asPoint()) estimated_pop = self.new_populations[f.id()] if f.id() in self.new_populations else \ self.original_populations[f.id()] variance = LinzElectoralDistrictRegistry.get_variation_from_quota_percent(self.quota, estimated_pop) text_string = ['{}'.format(f['name']), '{}'.format(int(estimated_pop)), '{}{}%'.format('+' if variance > 0 else '', variance)] QgsTextRenderer().drawText(QPointF(pixel.x(), pixel.y()), 0, QgsTextRenderer.AlignCenter, text_string, render_context, self.text_format) image_painter.end() painter.drawImage(0, 0, self.image)
def __copy_records_from_scenario( source_meshblock_electorate_layer: QgsVectorLayer, source_scenario_id, dest_meshblock_electorate_layer: QgsVectorLayer, new_scenario_id): """ Copies the records associated with a scenario from one table to another :param source_meshblock_electorate_layer: layer containing source meshblock->electorate mappings :param source_scenario_id: source scenario id :param dest_meshblock_electorate_layer: destination layer for copied meshblock->electorate mappings :param new_scenario_id: new scenario id for copied records """ request = QgsFeatureRequest() request.setFilterExpression( QgsExpression.createFieldEqualityExpression( 'scenario_id', source_scenario_id)) current_meshblocks = source_meshblock_electorate_layer.getFeatures( request) scenario_id_idx = source_meshblock_electorate_layer.fields( ).lookupField('scenario_id') fid_idx = source_meshblock_electorate_layer.fields().lookupField('fid') new_features = [] for f in current_meshblocks: f[scenario_id_idx] = new_scenario_id if fid_idx >= 0: f[fid_idx] = NULL new_features.append(f) dest_meshblock_electorate_layer.startEditing() dest_meshblock_electorate_layer.addFeatures(new_features) dest_meshblock_electorate_layer.commitChanges()
def processAlgorithm(self, parameters, context, feedback): if parameters[self.SPOKES] == parameters[self.HUBS]: raise QgsProcessingException( self.tr('Same layer given for both hubs and spokes')) hub_source = self.parameterAsSource(parameters, self.HUBS, context) spoke_source = self.parameterAsSource(parameters, self.SPOKES, context) field_hub = self.parameterAsString(parameters, self.HUB_FIELD, context) field_hub_index = hub_source.fields().lookupField(field_hub) field_spoke = self.parameterAsString(parameters, self.SPOKE_FIELD, context) field_spoke_index = hub_source.fields().lookupField(field_spoke) fields = vector.combineFields(hub_source.fields(), spoke_source.fields()) (sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context, fields, QgsWkbTypes.LineString, hub_source.sourceCrs()) hubs = hub_source.getFeatures() total = 100.0 / hub_source.featureCount() if hub_source.featureCount() else 0 matching_field_types = hub_source.fields().at(field_hub_index).type() == spoke_source.fields().at(field_spoke_index).type() for current, hub_point in enumerate(hubs): if feedback.isCanceled(): break if not hub_point.hasGeometry(): continue p = hub_point.geometry().boundingBox().center() hub_x = p.x() hub_y = p.y() hub_id = str(hub_point[field_hub]) hub_attributes = hub_point.attributes() request = QgsFeatureRequest().setDestinationCrs(hub_source.sourceCrs()) if matching_field_types: request.setFilterExpression(QgsExpression.createFieldEqualityExpression(field_spoke, hub_attributes[field_hub_index])) spokes = spoke_source.getFeatures() for spoke_point in spokes: if feedback.isCanceled(): break spoke_id = str(spoke_point[field_spoke]) if hub_id == spoke_id: p = spoke_point.geometry().boundingBox().center() spoke_x = p.x() spoke_y = p.y() f = QgsFeature() f.setAttributes(hub_attributes + spoke_point.attributes()) f.setGeometry(QgsGeometry.fromPolyline( [QgsPointXY(hub_x, hub_y), QgsPointXY(spoke_x, spoke_y)])) sink.addFeature(f, QgsFeatureSink.FastInsert) feedback.setProgress(int(current * total)) return {self.OUTPUT: dest_id}
def selectByAttribute(self, value): layer = getLayerFromId(self.LOCALITIES_LAYER) field_index = self.TAXON_FIELD_INDEX field_name = layer.fields()[int(field_index)].name() selected = [] filter = QgsExpression.createFieldEqualityExpression(field_name, str(value)) request = QgsFeatureRequest().setFilterExpression(filter) request.setSubsetOfAttributes([]) for feature in layer.getFeatures(request): selected.append(feature.id()) layer.selectByIds(selected)
def district_name_exists(self, district) -> bool: request = QgsFeatureRequest() request.setFilterExpression(QgsExpression.createFieldEqualityExpression(self.title_field, district)) request.setFlags(QgsFeatureRequest.NoGeometry) request.setSubsetOfAttributes([]) request.setLimit(1) try: next(self.source_layer.getFeatures(request)) except StopIteration: return False return True
def get_district_type(self, electorate_id) -> str: """ Returns the district type (GN/GS/M) for the specified district :param electorate_id: electorate id """ # lookup matching feature request = QgsFeatureRequest() request.setFilterExpression(QgsExpression.createFieldEqualityExpression(self.source_field, electorate_id)) request.setFlags(QgsFeatureRequest.NoGeometry) request.setSubsetOfAttributes([self.type_field_index]) request.setLimit(1) f = next(self.source_layer.getFeatures(request)) return f[self.type_field_index]
def get_estimated_population(self, electorate_id) -> int: """ Returns the estimated (offline) population for the district :param electorate_id: electorate code/id """ # lookup matching feature request = QgsFeatureRequest() request.setFilterExpression(QgsExpression.createFieldEqualityExpression(self.source_field, electorate_id)) request.setFlags(QgsFeatureRequest.NoGeometry) request.setSubsetOfAttributes([self.estimated_pop_field_index]) request.setLimit(1) f = next(self.source_layer.getFeatures(request)) return f[self.estimated_pop_field_index]
def modify_district_request(self, request): """ Allows subclasses to modify the request used to fetch available districts from the source layer, e.g. to add filtering or sorting to the request. :param request: base feature request to modify :return: modified feature request """ request.addOrderBy(self.source_field) if self.electorate_type: request.setFilterExpression( QgsExpression.createFieldEqualityExpression(self.type_field, self.electorate_type)) request.combineFilterExpression('"{}" is null or not "{}"'.format(self.deprecated_field, self.deprecated_field)) return request
def __init__( self, task_name: str, meshblock_layer: QgsVectorLayer, # pylint: disable=too-many-locals meshblock_number_field_name: str, scenario_registry: ScenarioRegistry, scenario, task: str): """ Constructor for ScenarioSwitchTask :param task_name: user-visible, translated name for task :param electorate_layer: electorate layer :param meshblock_layer: meshblock layer :param meshblock_number_field_name: name of meshblock number field :param scenario_registry: scenario registry :param scenario: target scenario id to switch to :param task: current task """ super().__init__(task_name) self.scenario = scenario self.mb_number_idx = scenario_registry.meshblock_electorate_layer.fields( ).lookupField('meshblock_number') electorate_field_name = '{}_id'.format(task.lower()) self.electorate_field_idx = scenario_registry.meshblock_electorate_layer.fields( ).lookupField(electorate_field_name) assert self.electorate_field_idx >= 0 self.scenario_id_field_idx = scenario_registry.meshblock_electorate_layer.fields( ).lookupField('scenario_id') assert self.scenario_id_field_idx >= 0 self.staged_electorate_field_idx = meshblock_layer.fields( ).lookupField('staged_electorate') assert self.staged_electorate_field_idx >= 0 self.meshblock_number_idx = meshblock_layer.fields().lookupField( meshblock_number_field_name) assert self.meshblock_number_idx >= 0 self.meshblock_layer = meshblock_layer request = QgsFeatureRequest() request.setFilterExpression( QgsExpression.createFieldEqualityExpression( 'scenario_id', scenario)) request.setSubsetOfAttributes( [self.mb_number_idx, self.electorate_field_idx]) self.meshblocks_for_scenario = scenario_registry.meshblock_electorate_layer.getFeatures( request) self.setDependentLayers([meshblock_layer])
def get_quota_for_district_type(self, district_type: str) -> int: """ Returns the quota for the specified district type :param district_type: district type, e.g. "GS" """ quota_field_index = self.quota_layer.fields().lookupField('quota') assert quota_field_index >= 0 request = QgsFeatureRequest() request.setFilterExpression(QgsExpression.createFieldEqualityExpression('type', district_type)) request.setFlags(QgsFeatureRequest.NoGeometry) request.setSubsetOfAttributes([quota_field_index]) request.setLimit(1) f = next(self.quota_layer.getFeatures(request)) return f[quota_field_index]
def electorate_meshblocks(self, electorate_id, electorate_type: str, scenario_id) -> QgsFeatureIterator: """ Returns meshblock features currently assigned to an electorate in a given scenario :param electorate_id: electorate id :param electorate_type: electorate type, e.g. 'GN','GS','M' :param scenario_id: scenario id """ request = QgsFeatureRequest() type_field = '{}_id'.format(electorate_type.lower()) type_field_index = self.meshblock_electorate_layer.fields( ).lookupField(type_field) assert type_field_index >= 0 request.setFilterExpression( QgsExpression.createFieldEqualityExpression( 'scenario_id', scenario_id)) request.combineFilterExpression( QgsExpression.createFieldEqualityExpression( type_field, electorate_id)) return self.meshblock_electorate_layer.getFeatures(request)
def get_scenario(self, scenario) -> QgsFeature: """ Returns the feature corresponding to the given scenario :param scenario: scenario id to get feature for """ # lookup matching feature request = QgsFeatureRequest() request.setFilterExpression( QgsExpression.createFieldEqualityExpression( self.id_field, scenario)) request.setFlags(QgsFeatureRequest.NoGeometry) request.setLimit(1) f = next(self.source_layer.getFeatures(request)) return f
def get_district_title(self, district): if self.title_field == self.source_field: return super().get_district_title(district) # lookup matching feature request = QgsFeatureRequest() request.setFilterExpression(QgsExpression.createFieldEqualityExpression(self.source_field, district)) request.setFlags(QgsFeatureRequest.NoGeometry) request.setSubsetOfAttributes([self.title_field_index]) request.setLimit(1) try: f = next(self.source_layer.getFeatures(request)) except StopIteration: return super().get_district_title(district) return f[self.title_field_index]
def scenario_exists(self, scenario_id) -> bool: """ Returns true if the given scenario exists :param scenario_id: ID for scenario """ request = QgsFeatureRequest() request.setFlags(QgsFeatureRequest.NoGeometry) request.setSubsetOfAttributes([]) request.setFilterExpression( QgsExpression.createFieldEqualityExpression( self.id_field, scenario_id)) request.setLimit(1) for f in self.source_layer.getFeatures(request): # pylint: disable=unused-variable # found a matching feature return True return False
def scenario_name_exists(self, new_scenario_name: str) -> bool: """ Returns true if the given scenario name already exists :param new_scenario_name: name of scenario to test """ request = QgsFeatureRequest() request.setFlags(QgsFeatureRequest.NoGeometry) request.setSubsetOfAttributes([]) request.setFilterExpression( QgsExpression.createFieldEqualityExpression( self.name_field, new_scenario_name)) request.setLimit(1) for f in self.source_layer.getFeatures(request): # pylint: disable=unused-variable # found a matching feature return True return False
def get_scenario_name(self, scenario) -> str: """ Returns a user-friendly name corresponding to the given scenario :param scenario: scenario id to get name for """ # lookup matching feature request = QgsFeatureRequest() request.setFilterExpression( QgsExpression.createFieldEqualityExpression( self.id_field, scenario)) request.setFlags(QgsFeatureRequest.NoGeometry) request.setSubsetOfAttributes([self.name_field_index]) request.setLimit(1) f = next(self.source_layer.getFeatures(request)) return f[self.name_field_index]
def get_stats_nz_calculations(self, electorate_id) -> dict: """ Returns a dictionary of Stats NZ calculations for the district :param electorate_id: electorate code/id """ # lookup matching feature request = QgsFeatureRequest() request.setFilterExpression(QgsExpression.createFieldEqualityExpression(self.source_field, electorate_id)) request.setFlags(QgsFeatureRequest.NoGeometry) request.setSubsetOfAttributes( [self.stats_nz_var_23_field_index, self.stats_nz_var_20_field_index, self.stats_nz_pop_field_index]) request.setLimit(1) f = next(self.source_layer.getFeatures(request)) return {'currentPopulation': f[self.stats_nz_pop_field_index], 'varianceYear1': f[self.stats_nz_var_20_field_index], 'varianceYear2': f[self.stats_nz_var_23_field_index]}
def flag_stats_nz_updating(self, electorate_id): """ Flags an electorate's Statistics NZ api calculations as being currently updated :param electorate_id: electorate to update """ # get feature ID request = QgsFeatureRequest() request.setFilterExpression(QgsExpression.createFieldEqualityExpression(self.source_field, electorate_id)) request.setFlags(QgsFeatureRequest.NoGeometry) request.setSubsetOfAttributes([]) request.setLimit(1) f = next(self.source_layer.getFeatures(request)) self.source_layer.dataProvider().changeAttributeValues({f.id(): {self.stats_nz_pop_field_index: -1, self.stats_nz_var_20_field_index: NULL, self.stats_nz_var_23_field_index: NULL}})
def toggle_electorate_deprecation(self, electorate): """ Toggles the deprecation flag for an electorate :param electorate: electorate id """ request = QgsFeatureRequest() request.setFlags(QgsFeatureRequest.NoGeometry) request.setSubsetOfAttributes([self.deprecated_field_index]) request.setFilterExpression(QgsExpression.createFieldEqualityExpression(self.source_field, electorate)) f = next(self.source_layer.getFeatures(request)) is_deprecated = f[self.deprecated_field_index] if is_deprecated == NULL: is_deprecated = False new_status = 0 if is_deprecated else 1 self.source_layer.dataProvider().changeAttributeValues({f.id(): {self.deprecated_field_index: new_status}})
def end_edit_group(self): if not self.pending_affected_districts: super().end_edit_group() return # step 1: get all electorate features corresponding to affected electorates electorate_features = {f[self.electorate_layer_field]: f for f in self.get_affected_districts([self.electorate_layer_field])} # and update the electorate boundaries based on these changes. # Ideally we'd redissolve the whole boundary from meshblocks, but that's too # slow. So instead we adjust piece-by-piece by adding or chomping away # the affected meshblocks only. new_geometries = {} new_attributes = {} for district in self.pending_affected_districts.keys(): # pylint: disable=consider-iterating-dictionary district_geometry = electorate_features[district].geometry() # add new bits district_geometry = self.grow_district_with_added_meshblocks(district, district_geometry) # minus lost bits district_geometry = self.shrink_district_by_removed_meshblocks(district, district_geometry) new_geometries[electorate_features[district].id()] = district_geometry calc = QgsAggregateCalculator(self.target_layer) calc.setFilter(QgsExpression.createFieldEqualityExpression(self.target_field, district)) estimated_pop, ok = calc.calculate(QgsAggregateCalculator.Sum, # pylint: disable=unused-variable self.offline_pop_field) new_attributes[electorate_features[district].id()] = {self.estimated_pop_idx: estimated_pop, self.stats_nz_pop_field_index: NULL, self.stats_nz_var_20_field_index: NULL, self.stats_nz_var_23_field_index: NULL, self.invalid_field_index: NULL, self.invalid_reason_field_index: NULL} self.electorate_changes_queue.push_changes(new_attributes, new_geometries) self.user_log_layer.dataProvider().addFeatures(self.pending_log_entries) self.electorate_changes_queue.blocked = True super().end_edit_group() self.electorate_changes_queue.blocked = False self.pending_affected_districts = {} self.pending_log_entries = [] self.redistrict_occured.emit()
def update_stats_nz_values(self, electorate_id, results: dict): """ Updates an electorate's stored Statistics NZ api calculations :param electorate_id: electorate to update :param results: results dictionary from stats API """ # get feature ID request = QgsFeatureRequest() request.setFilterExpression(QgsExpression.createFieldEqualityExpression(self.source_field, electorate_id)) request.setFlags(QgsFeatureRequest.NoGeometry) request.setSubsetOfAttributes([]) request.setLimit(1) f = next(self.source_layer.getFeatures(request)) self.source_layer.dataProvider().changeAttributeValues( {f.id(): {self.stats_nz_pop_field_index: results['currentPopulation'], self.stats_nz_var_20_field_index: results['varianceYear1'], self.stats_nz_var_23_field_index: results['varianceYear2']}})
def paint(self, painter, option, widget): # pylint: disable=missing-docstring, unused-argument, too-many-locals if self.image is not None: painter.drawImage(0, 0, self.image) return image_size = self.canvas.mapSettings().outputSize() self.image = QImage(image_size.width(), image_size.height(), QImage.Format_ARGB32) self.image.fill(0) image_painter = QPainter(self.image) render_context = QgsRenderContext.fromQPainter(image_painter) image_painter.setRenderHint(QPainter.Antialiasing, True) rect = self.canvas.mapSettings().visibleExtent() request = QgsFeatureRequest() request.setFilterRect(rect) request.setFilterExpression( QgsExpression.createFieldEqualityExpression('type', self.task)) for f in self.electorate_layer.getFeatures(request): # pole, dist = f.geometry().clipped(rect).poleOfInaccessibility(rect.width() / 30) pixel = self.toCanvasCoordinates( f.geometry().clipped(rect).centroid().asPoint()) calc = QgsAggregateCalculator(self.meshblock_layer) calc.setFilter('staged_electorate={}'.format(f['electorate_id'])) estimated_pop, ok = calc.calculate(QgsAggregateCalculator.Sum, 'offline_pop_{}'.format( self.task.lower())) # pylint: disable=unused-variable text_string = [ '{}'.format(f['name']), '{}'.format(int(estimated_pop)) ] QgsTextRenderer().drawText(QPointF(pixel.x(), pixel.y()), 0, QgsTextRenderer.AlignCenter, text_string, render_context, self.text_format) image_painter.end() painter.drawImage(0, 0, self.image)
def get_target_meshblock_ids_from_numbers( self, meshblock_numbers: List[int]) -> Dict[int, int]: """ Returns a dictionary of target meshblock feature IDs corresponding to the specified meshblock numbers :param meshblock_numbers: list of meshblock numbers to lookup """ assert self.scenario is not None request = QgsFeatureRequest() request.setSubsetOfAttributes([self.target_meshblock_number_idx]) request.setFilterExpression( QgsExpression.createFieldEqualityExpression( 'scenario_id', self.scenario)) meshblock_filter = 'meshblock_number IN ({})'.format(','.join( [str(mb) for mb in meshblock_numbers])) request.combineFilterExpression(meshblock_filter) # create dictionary of meshblock number to id mb_number_to_target_id = {} for f in self.meshblock_scenario_layer.getFeatures(request): mb_number_to_target_id[f[ self.target_meshblock_number_idx]] = f.id() return mb_number_to_target_id
def redraw(self, handler): """ Forces a redraw of the cached image """ self.image = None if not self.original_populations: # first run, get initial estimates request = QgsFeatureRequest() request.setFilterExpression(QgsExpression.createFieldEqualityExpression('type', self.task)) request.setFlags(QgsFeatureRequest.NoGeometry) for f in self.electorate_layer.getFeatures(request): estimated_pop = f.attribute(handler.stats_nz_pop_field_index) if estimated_pop is None or estimated_pop == NULL: # otherwise just use existing estimated pop as starting point estimated_pop = f.attribute(handler.estimated_pop_idx) self.original_populations[f.id()] = estimated_pop # step 1: get all electorate features corresponding to affected electorates electorate_features = {f[handler.electorate_layer_field]: f for f in handler.get_affected_districts( [handler.electorate_layer_field, handler.stats_nz_pop_field, 'estimated_pop'], needs_geometry=False)} self.new_populations = {} for district in handler.pending_affected_districts.keys(): # pylint: disable=consider-iterating-dictionary # use stats nz pop as initial estimate, if available estimated_pop = electorate_features[district].attribute(handler.stats_nz_pop_field_index) if estimated_pop is None or estimated_pop == NULL: # otherwise just use existing estimated pop as starting point estimated_pop = electorate_features[district].attribute(handler.estimated_pop_idx) # add new bits estimated_pop = handler.grow_population_with_added_meshblocks(district, estimated_pop) # minus lost bits estimated_pop = handler.shrink_population_by_removed_meshblocks(district, estimated_pop) self.new_populations[electorate_features[district].id()] = estimated_pop
def processAlgorithm(self, parameters, context, feedback): # Get variables from dialog source = self.parameterAsSource(parameters, self.INPUT, context) if source is None: raise QgsProcessingException( self.invalidSourceError(parameters, self.INPUT)) field_name = self.parameterAsString(parameters, self.FIELD, context) kneighbors = self.parameterAsInt(parameters, self.KNEIGHBORS, context) use_field = bool(field_name) field_index = -1 fields = QgsFields() fields.append(QgsField('id', QVariant.Int, '', 20)) current = 0 # Get properties of the field the grouping is based on if use_field: field_index = source.fields().lookupField(field_name) if field_index >= 0: fields.append( source.fields()[field_index] ) # Add a field with the name of the grouping field # Initialize writer (sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context, fields, QgsWkbTypes.Polygon, source.sourceCrs()) if sink is None: raise QgsProcessingException( self.invalidSinkError(parameters, self.OUTPUT)) success = False fid = 0 # Get unique values of grouping field unique_values = source.uniqueValues(field_index) total = 100.0 / float( source.featureCount() * len(unique_values)) for unique in unique_values: points = [] filter = QgsExpression.createFieldEqualityExpression( field_name, unique) request = QgsFeatureRequest().setFilterExpression(filter) request.setSubsetOfAttributes([]) # Get features with the grouping attribute equal to the current grouping value features = source.getFeatures(request) for in_feature in features: if feedback.isCanceled(): break # Add points or vertices of more complex geometry points.extend(extract_points(in_feature.geometry())) current += 1 feedback.setProgress(int(current * total)) # A minimum of 3 points is necessary to proceed if len(points) >= 3: out_feature = QgsFeature() the_hull = concave_hull(points, kneighbors) if the_hull: vertex = [ QgsPointXY(point[0], point[1]) for point in the_hull ] poly = QgsGeometry().fromPolygonXY([vertex]) out_feature.setGeometry(poly) # Give the polygon the same attribute as the point grouping attribute out_feature.setAttributes([fid, unique]) sink.addFeature(out_feature, QgsFeatureSink.FastInsert) success = True # at least one polygon created fid += 1 if not success: raise QgsProcessingException( 'No hulls could be created. Most likely there were not at least three unique points in any of the groups.' ) else: # Field parameter provided but can't read from it raise QgsProcessingException('Unable to find grouping field') else: # Not grouped by field # Initialize writer (sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context, fields, QgsWkbTypes.Polygon, source.sourceCrs()) if sink is None: raise QgsProcessingException( self.invalidSinkError(parameters, self.OUTPUT)) points = [] request = QgsFeatureRequest() request.setSubsetOfAttributes([]) features = source.getFeatures(request) # Get all features total = 100.0 / source.featureCount() if source.featureCount( ) else 0 for in_feature in features: if feedback.isCanceled(): break # Add points or vertices of more complex geometry points.extend(extract_points(in_feature.geometry())) current += 1 feedback.setProgress(int(current * total)) # A minimum of 3 points is necessary to proceed if len(points) >= 3: out_feature = QgsFeature() the_hull = concave_hull(points, kneighbors) if the_hull: vertex = [ QgsPointXY(point[0], point[1]) for point in the_hull ] poly = QgsGeometry().fromPolygonXY([vertex]) out_feature.setGeometry(poly) out_feature.setAttributes([0]) sink.addFeature(out_feature, QgsFeatureSink.FastInsert) else: # the_hull returns None only when there are less than three points after cleaning raise QgsProcessingException( 'At least three unique points are required to create a concave hull.' ) else: raise QgsProcessingException( 'At least three points are required to create a concave hull.' ) return {self.OUTPUT: dest_id}
def processAlgorithm(self, parameters, context, feedback): t_file = self.parameterAsVectorLayer( parameters, self.FILE_TABLE, context ) t_troncon = self.parameterAsVectorLayer( parameters, self.SEGMENTS_TABLE, context ) t_obs = self.parameterAsVectorLayer( parameters, self.OBSERVATIONS_TABLE, context ) t_regard = self.parameterAsVectorLayer( parameters, self.MANHOLES_TABLE, context ) g_regard = self.parameterAsVectorLayer( parameters, self.GEOM_MANHOLES, context ) g_troncon = self.parameterAsVectorLayer( parameters, self.GEOM_SEGMENT, context ) g_obs = self.parameterAsVectorLayer( parameters, self.GEOM_OBSERVATION, context ) v_regard = self.parameterAsVectorLayer( parameters, self.VIEW_MANHOLES_GEOLOCALIZED, context ) # define variables variables = context.project().customVariables() variables['itv_rerau_t_file'] = t_file.id() variables['itv_rerau_t_troncon'] = t_troncon.id() variables['itv_rerau_t_obs'] = t_obs.id() variables['itv_rerau_t_regard'] = t_regard.id() variables['itv_rerau_g_regard'] = g_regard.id() variables['itv_rerau_g_troncon'] = g_troncon.id() variables['itv_rerau_g_obs'] = g_obs.id() context.project().setCustomVariables(variables) # define relations relations = [ { 'id': 'fk_obs_id_file', 'name': tr('Link File - Observation'), 'referencingLayer': t_obs.id(), 'referencingField': 'id_file', 'referencedLayer': t_file.id(), 'referencedField': 'id' }, { 'id': 'fk_regard_id_file', 'name': tr('Link File - Manhole'), 'referencingLayer': t_regard.id(), 'referencingField': 'id_file', 'referencedLayer': t_file.id(), 'referencedField': 'id' }, { 'id': 'fk_troncon_id_file', 'name': tr('Link File - Pipe segment'), 'referencingLayer': t_troncon.id(), 'referencingField': 'id_file', 'referencedLayer': t_file.id(), 'referencedField': 'id' }, { 'id': 'fk_obs_id_troncon', 'name': tr('Link Pipe segment - Observation'), 'referencingLayer': t_obs.id(), 'referencingField': 'id_troncon', 'referencedLayer': t_troncon.id(), 'referencedField': 'id' }, { 'id': 'fk_regard_id_geom_regard', 'name': tr('Link Manhole inspection - Reference'), 'referencingLayer': t_regard.id(), 'referencingField': 'id_geom_regard', 'referencedLayer': g_regard.id(), 'referencedField': 'id' }, { 'id': 'fk_troncon_id_geom_trononc', 'name': tr('Link Pipe segment inspection - Reference'), 'referencingLayer': t_troncon.id(), 'referencingField': 'id_geom_troncon', 'referencedLayer': g_troncon.id(), 'referencedField': 'id' } ] relation_manager = context.project().relationManager() for rel_def in relations: feedback.pushInfo( 'Link: {}'.format(rel_def['name']) ) rel = QgsRelation() rel.setId(rel_def['id']) rel.setName(rel_def['name']) rel.setReferencingLayer(rel_def['referencingLayer']) rel.setReferencedLayer(rel_def['referencedLayer']) rel.addFieldPair( rel_def['referencingField'], rel_def['referencedField'] ) rel.setStrength(QgsRelation.Association) relation_manager.addRelation(rel) feedback.pushInfo( 'Count relations {}'.format( len(relation_manager.relations()) ) ) joins = [ { 'layer': t_obs, 'targetField': 'id_troncon', 'joinLayer': t_troncon, 'joinField': 'id', 'fieldNamesSubset': ['ack'] }, { 'layer': g_obs, 'targetField': 'id', 'joinLayer': t_obs, 'joinField': 'id', 'fieldNamesSubset': [] } ] for j_def in joins: layer = j_def['layer'] join = QgsVectorLayerJoinInfo() join.setJoinFieldName(j_def['joinField']) join.setJoinLayerId(j_def['joinLayer'].id()) join.setTargetFieldName(j_def['targetField']) if j_def['fieldNamesSubset']: join.setJoinFieldNamesSubset(j_def['fieldNamesSubset']) join.setUsingMemoryCache(False) join.setPrefix('') join.setEditable(False) join.setCascadedDelete(False) join.setJoinLayer(j_def['joinLayer']) layer.addJoin(join) layer.updateFields() # load styles styles = [ { 'layer': t_file, 'namedStyles': [ { 'file': 'itv_file_fields.qml', 'type': QgsMapLayer.Fields }, { 'file': 'itv_file_actions.qml', 'type': QgsMapLayer.Actions } ] }, { 'layer': t_troncon, 'namedStyles': [ { 'file': 'itv_troncon_fields.qml', 'type': QgsMapLayer.Fields }, { 'file': 'itv_troncon_table.qml', 'type': QgsMapLayer.AttributeTable } ] }, { 'layer': t_obs, 'namedStyles': [ { 'file': 'itv_obs_fields.qml', 'type': QgsMapLayer.Fields }, { 'file': 'itv_obs_table.qml', 'type': QgsMapLayer.AttributeTable } ] }, { 'layer': t_regard, 'namedStyles': [ { 'file': 'itv_regard_fields.qml', 'type': QgsMapLayer.Fields }, { 'file': 'itv_regard_forms.qml', 'type': QgsMapLayer.Forms }, { 'file': 'itv_regard_table.qml', 'type': QgsMapLayer.AttributeTable } ] }, { 'layer': g_regard, 'namedStyles': [ { 'file': 'itv_geom_regard_fields.qml', 'type': QgsMapLayer.Fields }, { 'file': 'itv_geom_regard_symbology.qml', 'type': QgsMapLayer.Symbology } ] }, { 'layer': g_troncon, 'namedStyles': [ { 'file': 'itv_geom_troncon_fields.qml', 'type': QgsMapLayer.Fields }, { 'file': 'itv_geom_troncon_symbology.qml', 'type': QgsMapLayer.Symbology }, { 'file': 'itv_geom_troncon_actions.qml', 'type': QgsMapLayer.Actions } ] }, { 'layer': g_obs, 'namedStyles': [ { 'file': 'itv_geom_obs_fields.qml', 'type': QgsMapLayer.Fields }, { 'file': 'itv_geom_obs_symbology.qml', 'type': QgsMapLayer.Symbology } ] }, { 'layer': v_regard, 'namedStyles': [ { 'file': 'itv_view_regard_fields.qml', 'type': QgsMapLayer.Fields }, { 'file': 'itv_view_regard_symbology.qml', 'type': QgsMapLayer.Symbology }, { 'file': 'itv_view_regard_labeling.qml', 'type': QgsMapLayer.Labeling } ] } ] for style in styles: layer = style['layer'] for n_style in style['namedStyles']: layer.loadNamedStyle( resources_path('styles', n_style['file']), categories=n_style['type'] ) # layer.saveStyleToDatabase('style', 'default style', True, '') layer.triggerRepaint() # Creation de la symbologie g_obs g_obs_rules = ( 'BAA', 'BAB', 'BAC', 'BAD', 'BAE', 'BAF', 'BAG', 'BAH', 'BAI', 'BAJ', 'BAK', 'BAL', 'BAM', 'BAN', 'BAO', 'BAP', 'BBA', 'BBB', 'BBC', 'BBD', 'BBE', 'BBF', 'BBG', 'BBH', 'BCA', 'BCB', 'BCC', 'BDA', 'BDB', 'BDC', 'BDD', 'BDE', 'BDF', 'BDG' ) g_obs_rule_descs = { 'BAA': 'Déformation', 'BAB': 'Fissure', 'BAC': 'Rupture/Effondrement', 'BAD': 'Elt maçonnerie', 'BAE': 'Mortier manquant', 'BAF': 'Dégradation de surface', 'BAG': 'Branchement pénétrant', 'BAH': 'Raccordement défectueux', 'BAI': 'Joint étanchéité apparent', 'BAJ': 'Déplacement d\'assemblage', 'BAK': 'Défaut de révêtement', 'BAL': 'Réparation défectueuse', 'BAM': 'Défaut soudure', 'BAN': 'Conduite poreuse', 'BAO': 'Sol visible', 'BAP': 'Trou visible', 'BBA': 'Racines', 'BBB': 'Dépots Adhérents', 'BBC': 'Dépôts', 'BBD': 'Entrée de terre', 'BBE': 'Autres obstacles', 'BBF': 'Infiltration', 'BBG': 'Exfiltration', 'BBH': 'Vermine', 'BCA': 'Raccordement', 'BCB': 'Réparation', 'BCC': 'Courbure de collecteur', 'BDA': 'Photographie générale', 'BDB': 'Remarque générale', 'BDC': 'Inspection abandonnée', 'BDD': 'Niveau d\'eau', 'BDE': 'Ecoulement dans une canlisation entrante', 'BDF': 'Atmosphère canalisation', 'BDG': 'Perte de visibilité' } g_obs_rootrule = QgsRuleBasedRenderer.Rule(None) rendering_pass_idx = len(g_obs_rules) for rule in g_obs_rules: # get svg path svg_path = resources_path('styles', 'img_obs', rule + '.svg') # create svg symbol layer svg_symbol_layer = QgsSvgMarkerSymbolLayer(svg_path) svg_symbol_layer.setRenderingPass(rendering_pass_idx) # create white square symbol layer for the backend simple_symbol_layer = QgsSimpleMarkerSymbolLayer( shape=QgsSimpleMarkerSymbolLayerBase.Circle, size=svg_symbol_layer.size(), color=QColor('white'), strokeColor=QColor('white') ) simple_symbol_layer.setRenderingPass(rendering_pass_idx) # create marker svg_marker = QgsMarkerSymbol() # set the backend symbol layer svg_marker.changeSymbolLayer(0, simple_symbol_layer) # add svg symbol layer svg_marker.appendSymbolLayer(svg_symbol_layer) # create rule svg_rule = QgsRuleBasedRenderer.Rule( svg_marker, 0, 10000, QgsExpression.createFieldEqualityExpression('a', rule), rule ) if rule in g_obs_rule_descs: svg_rule.setLabel(g_obs_rule_descs[rule]) svg_rule.setDescription('{}: {}'.format( rule, g_obs_rule_descs[rule] )) # add rule g_obs_rootrule.appendChild(svg_rule) rendering_pass_idx -= 1 g_obs_rootrule.appendChild( QgsRuleBasedRenderer.Rule( QgsMarkerSymbol.createSimple( { 'name': 'circle', 'color': '#0000b2', 'outline_color': '#0000b2', 'size': '1' } ), 0, 10000, 'ELSE', 'Autres' ) ) g_obs.setRenderer(QgsRuleBasedRenderer(g_obs_rootrule)) feedback.pushInfo('Project has been setup') return {}
def _remove_selected_from_relation(agg_id: int, src_primary_key: str, src_layer_name: str, rel_primary_key: str, rel_layer_name: str): # get project instance project = QgsProject.instance() # get layers needed src_layer = project.mapLayersByName(src_layer_name) # Control if layers exists if not len(src_layer): iface.messageBar().pushCritical( 'Véloroutes', 'La couche {} n\'a pas été trouvée'.format(src_layer_name)) return src_layer = src_layer[0] rel_layer = project.mapLayersByName(rel_layer_name) if not len(rel_layer): iface.messageBar().pushCritical( 'Véloroutes', 'La table {} n\'a pas été trouvée'.format(rel_layer_name)) return rel_layer = rel_layer[0] # count number of features selected from src_layer count = src_layer.selectedFeatureCount() if count < 1: iface.messageBar().pushCritical( 'Véloroutes', 'Vous devez sélectionner au moins un objet de la couche {}'.format( src_layer_name)) return # get list rel_layer couple_rel = [] for feat in rel_layer.getFeatures( QgsExpression.createFieldEqualityExpression( src_primary_key, agg_id)): couple_rel.append((feat[src_primary_key], feat[rel_primary_key])) # create list of couples between src_primary_key and rel_primary_key features = src_layer.getSelectedFeatures() couple_id = [] for feat in features: couple_id.append((agg_id, feat[rel_primary_key])) # test between two lists match_list = [] for item in couple_rel: if item in couple_id: match_list.append(item) # test if match_list is empty if len(match_list) < 1: iface.messageBar().pushInfo( 'Véloroutes', 'Pas d\'objet de la couche {} à supprimer'.format(rel_layer_name)) return rel_layer.startEditing() for feat in rel_layer.getFeatures( QgsExpression.createFieldEqualityExpression( src_primary_key, agg_id)): if (feat[src_primary_key], feat[rel_primary_key]) in match_list: rel_layer.deleteFeature(feat.id()) rel_layer.commitChanges() msg = "{} objet(s) ont été supprimées de la couche {}".format( len(match_list), rel_layer_name) iface.messageBar().pushInfo('Véloroutes', msg)
def processAlgorithm(self, parameters, context, feedback): source = self.parameterAsSource(parameters, self.INPUT, context) request = QgsFeatureRequest() request.setFlags(QgsFeatureRequest.NoGeometry) request.setSubsetOfAttributes(self.fields, source.fields()) request.addOrderBy("|| ' - ' || ".join(self.fields)) unique_couples = [] non_unique_couples = [] for src_feature in source.getFeatures(request): couple = (src_feature[self.fields[0]], src_feature[self.fields[1]]) if couple not in unique_couples: unique_couples.append(couple) else: non_unique_couples.append(couple) if not non_unique_couples: feedback.pushInfo( 'L\'ensemble des couples noms/faciès sont uniques') _, dest_id = self.parameterAsSink(parameters, self.OUTPUT, context, source.fields(), source.wkbType(), source.sourceCrs()) return { self.OUTPUT: dest_id, self.NUMBER_OF_UNIQUE: len(unique_couples), self.NUMBER_OF_NON_UNIQUE: 0 } feedback.pushInfo('Certains couples ne sont pas uniques :') for couple in non_unique_couples: feedback.pushInfo(' {} - {}'.format(couple[0], couple[1])) expressions = [] for couple in non_unique_couples: exp = ' AND '.join([ QgsExpression.createFieldEqualityExpression( self.fields[0], couple[0]), QgsExpression.createFieldEqualityExpression( self.fields[1], couple[1]) ]) expressions.append(exp) exp = '(' exp += ') OR ('.join(expressions) exp += ')' feedback.pushDebugInfo(exp) exp_context = self.createExpressionContext(parameters, context, source) request = QgsFeatureRequest() request.setFilterExpression(exp) request.setExpressionContext(exp_context) layer = source.materialize(request) params = { 'ALL_PARTS': True, 'INPUT': layer, 'OUTPUT': 'TEMPORARY_OUTPUT' } results = processing.run( "native:pointonsurface", params, context=context, feedback=feedback, is_child_algorithm=True, ) params = { 'INPUT': results['OUTPUT'], 'FIELD': self.fields, 'OUTPUT': parameters[self.OUTPUT] } results = processing.run( "native:collect", params, context=context, feedback=feedback, is_child_algorithm=True, ) output_layer = results['OUTPUT'] if context.willLoadLayerOnCompletion(output_layer): layer_details = context.layerToLoadOnCompletionDetails( output_layer) output_def = self.parameterDefinition(self.OUTPUT) layer_details.name = output_def.description() layer_details.setPostProcessor( SetLabelingPostProcessor.create(self.fields)) return { self.OUTPUT: output_layer, self.NUMBER_OF_UNIQUE: len(unique_couples), self.NUMBER_OF_NON_UNIQUE: len(non_unique_couples) }
def processAlgorithm(self, parameters, context, feedback): habitat = self.parameterAsVectorLayer(parameters, self.HABITAT_LAYER, context) impact = self.parameterAsVectorLayer(parameters, self.IMPACT_LAYER, context) self.output_layer = self.parameterAsVectorLayer( parameters, self.HABITAT_IMPACT_ETAT_ECOLOGIQUE_LAYER, context) multi_feedback = QgsProcessingMultiStepFeedback(4, feedback) multi_feedback.setCurrentStep(0) multi_feedback.pushInfo( "Calcul de l'intersection entre les couches habitat et {}".format(self.project_type.label)) params = { 'INPUT': habitat, 'OVERLAY': impact, 'INPUT_FIELDS': [], # All fields 'OVERLAY_FIELDS': ['id', 'scenario_id'], 'OVERLAY_FIELDS_PREFIX': '{}_'.format(self.project_type.label), 'OUTPUT': 'TEMPORARY_OUTPUT', } results = processing.run( "native:intersection", params, context=context, feedback=multi_feedback, is_child_algorithm=True, ) intersection = QgsProcessingUtils.mapLayerFromString(results['OUTPUT'], context, True) multi_feedback.pushInfo("Renommage des champs") multi_feedback.setCurrentStep(1) # Rename id to habitat_id # Rename pression_scenario_id to scenario_id (or compensation_id) index_habitat = intersection.fields().indexOf('id') index_impact_id = intersection.fields().indexOf('{}_scenario_id'.format(self.project_type.label)) with edit(intersection): intersection.renameAttribute(index_habitat, 'habitat_id') intersection.renameAttribute(index_impact_id, 'scenario_id') intersection.updateFields() multi_feedback.pushInfo( "Collect des géométries ayant les couples {} identiques".format(' ,'.join(self.fields_id))) multi_feedback.setCurrentStep(2) params = { 'INPUT': intersection, 'FIELD': list(self.fields_id), 'OUTPUT': 'TEMPORARY_OUTPUT', } results = processing.run( "native:collect", params, context=context, feedback=multi_feedback, is_child_algorithm=True, ) multi_feedback.pushInfo("Correction des géométries") multi_feedback.setCurrentStep(3) params = { 'INPUT': results['OUTPUT'], 'DISTANCE': 0, 'OUTPUT': 'TEMPORARY_OUTPUT', } results = processing.run( "native:buffer", params, context=context, feedback=multi_feedback, is_child_algorithm=True) layer = QgsProcessingUtils.mapLayerFromString(results['OUTPUT'], context, True) multi_feedback.pushInfo("Import des entités dans la couche du geopackage") multi_feedback.setCurrentStep(4) field_map = {} for i, field in enumerate(self.output_layer.fields()): field_map[field.name()] = i with edit(self.output_layer): for feature in layer.getFeatures(): habitat_id = feature['habitat_id'] impact_id = feature[self.impact_id] scenario_id = feature['scenario_id'] # TODO need to check for compensation this behavior if self.impact_field: # Test du type de pression associé filter_expression = QgsExpression.createFieldEqualityExpression('id', impact_id) filter_request = QgsFeatureRequest(QgsExpression(filter_expression)) filter_request.setLimit(1) for press in impact.getFeatures(filter_expression): pression_emprise = (press['type_pression'] == 6) break else: pression_emprise = False exists, existing_feature = self.feature_exists( self.output_layer, habitat_id, impact_id, scenario_id) if exists: self.output_layer.changeGeometry(existing_feature.id(), feature.geometry()) attribute_map = {} for field in layer.fields(): field_name = field.name() if field_name in self.fields_id: continue if field_name in self.output_layer.fields().names(): if pression_emprise and field_name in self.fields(): attribute_map[field_map[field_name]] = 0 else: attribute_map[field_map[field_name]] = feature[field_name] self.output_layer.changeAttributeValues(existing_feature.id(), attribute_map) else: # We create a new feature out_feature = QgsFeature(self.output_layer.fields()) out_feature.setAttribute('habitat_id', habitat_id) out_feature.setAttribute(self.impact_id, impact_id) out_feature.setAttribute('scenario_id', scenario_id) out_feature.setGeometry(feature.geometry()) for field in layer.fields(): field_name = field.name() if field_name in self.fields_id: continue if field_name in self.output_layer.fields().names(): if pression_emprise and field_name in self.fields(): out_feature.setAttribute(field_name, 0) else: out_feature.setAttribute(field_name, feature[field_name]) self.output_layer.addFeature(out_feature) return {}
def processAlgorithm(self, parameters, context, feedback): # Get variables from dialog source = self.parameterAsSource(parameters, self.INPUT, context) if source is None: raise QgsProcessingException(self.invalidSourceError(parameters, self.INPUT)) field_name = self.parameterAsString(parameters, self.FIELD, context) kneighbors = self.parameterAsInt(parameters, self.KNEIGHBORS, context) use_field = bool(field_name) field_index = -1 fields = QgsFields() fields.append(QgsField('id', QVariant.Int, '', 20)) current = 0 # Get properties of the field the grouping is based on if use_field: field_index = source.fields().lookupField(field_name) if field_index >= 0: fields.append(source.fields()[field_index]) # Add a field with the name of the grouping field # Initialize writer (sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context, fields, QgsWkbTypes.Polygon, source.sourceCrs()) if sink is None: raise QgsProcessingException(self.invalidSinkError(parameters, self.OUTPUT)) success = False fid = 0 # Get unique values of grouping field unique_values = source.uniqueValues(field_index) total = 100.0 / float(source.featureCount() * len(unique_values)) for unique in unique_values: points = [] filter = QgsExpression.createFieldEqualityExpression(field_name, unique) request = QgsFeatureRequest().setFilterExpression(filter) request.setSubsetOfAttributes([]) # Get features with the grouping attribute equal to the current grouping value features = source.getFeatures(request) for in_feature in features: if feedback.isCanceled(): break # Add points or vertices of more complex geometry points.extend(extract_points(in_feature.geometry())) current += 1 feedback.setProgress(int(current * total)) # A minimum of 3 points is necessary to proceed if len(points) >= 3: out_feature = QgsFeature() the_hull = concave_hull(points, kneighbors) if the_hull: vertex = [QgsPointXY(point[0], point[1]) for point in the_hull] poly = QgsGeometry().fromPolygonXY([vertex]) out_feature.setGeometry(poly) # Give the polygon the same attribute as the point grouping attribute out_feature.setAttributes([fid, unique]) sink.addFeature(out_feature, QgsFeatureSink.FastInsert) success = True # at least one polygon created fid += 1 if not success: raise QgsProcessingException('No hulls could be created. Most likely there were not at least three unique points in any of the groups.') else: # Field parameter provided but can't read from it raise QgsProcessingException('Unable to find grouping field') else: # Not grouped by field # Initialize writer (sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context, fields, QgsWkbTypes.Polygon, source.sourceCrs()) if sink is None: raise QgsProcessingException(self.invalidSinkError(parameters, self.OUTPUT)) points = [] request = QgsFeatureRequest() request.setSubsetOfAttributes([]) features = source.getFeatures(request) # Get all features total = 100.0 / source.featureCount() if source.featureCount() else 0 for in_feature in features: if feedback.isCanceled(): break # Add points or vertices of more complex geometry points.extend(extract_points(in_feature.geometry())) current += 1 feedback.setProgress(int(current * total)) # A minimum of 3 points is necessary to proceed if len(points) >= 3: out_feature = QgsFeature() the_hull = concave_hull(points, kneighbors) if the_hull: vertex = [QgsPointXY(point[0], point[1]) for point in the_hull] poly = QgsGeometry().fromPolygonXY([vertex]) out_feature.setGeometry(poly) out_feature.setAttributes([0]) sink.addFeature(out_feature, QgsFeatureSink.FastInsert) else: # the_hull returns None only when there are less than three points after cleaning raise QgsProcessingException('At least three unique points are required to create a concave hull.') else: raise QgsProcessingException('At least three points are required to create a concave hull.') return {self.OUTPUT: dest_id}
def processAlgorithm(self, parameters, context, feedback): path = self.parameterAsFile(parameters, self.INPUT, context) t_file = self.parameterAsVectorLayer( parameters, self.FILE_TABLE, context ) t_troncon = self.parameterAsVectorLayer( parameters, self.SEGMENT_TABLE, context ) t_obs = self.parameterAsVectorLayer( parameters, self.OBSERVATIONS_TABLE, context ) t_regard = self.parameterAsVectorLayer( parameters, self.MANHOLES_TABLE, context ) paths = path.split(';') if len(paths) != 1: raise QgsProcessingException( tr('* ERREUR: 1 fichier a la fois {}.').format(path) ) if not os.path.exists(path): raise QgsProcessingException( tr('* ERREUR: {} n\'existe pas.').format(path) ) # add date fields to itv file table # relation_aggregate( # 'itv_tronco_id_file_itv_file20_id', # 'max', # "abf" # ) if 'date_debut' not in t_file.dataProvider().fields().names(): with edit(t_file): res = t_file.dataProvider().addAttributes([ QgsField("date_debut", QVariant.String), QgsField("date_fin", QVariant.String) ]) if res: t_file.updateFields() feat_file = None with open(path, 'rb') as f: basename = os.path.basename(path) md = hashlib.md5() md.update(f.read()) hashcontent = md.hexdigest() feat_file = QgsVectorLayerUtils.createFeature(t_file) feat_file.setAttribute('basename', basename) feat_file.setAttribute('hashcontent', hashcontent) if not feat_file: raise QgsProcessingException( tr( '* ERREUR: le fichier {} n\'a pas été lu ' 'correctement.' ).format(path) ) exp_context = QgsExpressionContext() exp_context.appendScope( QgsExpressionContextUtils.globalScope() ) exp_context.appendScope( QgsExpressionContextUtils.projectScope(context.project()) ) exp_context.appendScope( QgsExpressionContextUtils.layerScope(t_file) ) exp_str = QgsExpression.createFieldEqualityExpression( 'basename', feat_file['basename'] ) + ' AND ' + QgsExpression.createFieldEqualityExpression( 'hashcontent', feat_file['hashcontent'] ) exp = QgsExpression(exp_str) exp.prepare(exp_context) if exp.hasEvalError(): raise QgsProcessingException( tr('* ERROR: Expression {} has eval error: {}').format( exp.expression(), exp.evalErrorString() ) ) request = QgsFeatureRequest(exp, exp_context) request.setLimit(1) for _t in t_file.getFeatures(request): raise QgsProcessingException( tr('* ERREUR: le fichier {} a deja ete lu').format( path ) ) exp_str = QgsExpression.createFieldEqualityExpression( 'hashcontent', feat_file['hashcontent'] ) exp = QgsExpression(exp_str) exp.prepare(exp_context) if exp.hasEvalError(): raise QgsProcessingException( tr('* ERROR: Expression {} has eval error: {}').format( (exp.expression(), exp.evalErrorString()) ) ) request = QgsFeatureRequest(exp, exp_context) request.setLimit(1) for _t in t_file.getFeatures(request): raise QgsProcessingException( tr( '* ERREUR: le fichier {} semble deja avoir ete lu' ).format(path) ) exp_context = QgsExpressionContext() exp_context.appendScope( QgsExpressionContextUtils.globalScope() ) exp_context.appendScope( QgsExpressionContextUtils.projectScope(context.project()) ) exp_context.appendScope( QgsExpressionContextUtils.layerScope(t_troncon) ) exp_str = 'maximum("id")' exp = QgsExpression(exp_str) exp.prepare(exp_context) if exp.hasEvalError(): raise QgsProcessingException( tr( '* ERROR: Expression {} has eval error: {}' ).format(exp.expression(), exp.evalErrorString()) ) last_t_id = exp.evaluate(exp_context) if not last_t_id: last_t_id = 0 exp_context = QgsExpressionContext() exp_context.appendScope( QgsExpressionContextUtils.globalScope() ) exp_context.appendScope( QgsExpressionContextUtils.projectScope(context.project()) ) exp_context.appendScope( QgsExpressionContextUtils.layerScope(t_regard) ) exp_str = 'maximum("id")' exp = QgsExpression(exp_str) exp.prepare(exp_context) if exp.hasEvalError(): raise QgsProcessingException( tr( '* ERROR: Expression {} has eval error: {}' ).format(exp.expression(), exp.evalErrorString()) ) last_r_id = exp.evaluate(exp_context) if not last_r_id: last_r_id = 0 # lecture des entetes ENCODING = 'ISO-8859-1' LANG = 'fr' DELIMITER = ',' DECIMAL = '.' QUOTECHAR = '"' VERSION = '' with open(path, 'rb') as f: for line in f: # Stop the algorithm if cancel button has been clicked if feedback.isCanceled(): return {self.SUCCESS: 0} try: line = line.decode() except UnicodeDecodeError: raise QgsProcessingException( 'Error while reading {}'.format(path) ) # remove break line line = line.replace('\n', '').replace('\r', '') if line.startswith('#'): if line.startswith('#A'): if line.startswith('#A1'): ENCODING = line[4:] if ENCODING.find(':') != -1: ENCODING = ENCODING[:ENCODING.find(':')] elif line.startswith('#A2'): LANG = line[4:] elif line.startswith('#A3'): DELIMITER = line[4:] elif line.startswith('#A4'): DECIMAL = line[4:] elif line.startswith('#A5'): QUOTECHAR = line[4:] else: break # Dialect CSV pour la lecture des tableaux de valeurs du # fichier d'ITV class itvDialect(csv.Dialect): strict = True skipinitialspace = True quoting = csv.QUOTE_MINIMAL delimiter = DELIMITER quotechar = QUOTECHAR lineterminator = '\r\n' # Liste des troncons, observations et regards troncons = [] regards = [] observations = [] # Lectures des donnees with open(path, 'rb') as f: # Identifiant de départ t_id = last_t_id r_id = last_r_id # Entête header = [] # Nom du tableau array = '' # Observations de troncons ? obs_for_troncon = False # initialisation du Dialect CSV pour ITV dia = itvDialect() # Lecture ligne à ligne du fichier for line in f: # Stop the algorithm if cancel button has been clicked if feedback.isCanceled(): return {self.SUCCESS: 0} # Decoding line en utilisant l'encoding du fichier line = line.decode(ENCODING) # remove break line line = line.replace('\n', '').replace('\r', '') # Ligne commençant par un # est une ligne d'entête if line.startswith('#'): # Entête de troncon ou regard if line.startswith('#B'): l_b = io.StringIO(line[5:]) for l_r in csv.reader(l_b, dia): header = l_r break array = line[1:4] continue # Entête d'observation elif line.startswith('#C'): if header[0].startswith('A'): obs_for_troncon = True else: obs_for_troncon = False l_b = io.StringIO(line[3:]) for l_r in csv.reader(l_b, dia): header = l_r break array = line[1:2] continue # Fin d'observation elif line.startswith('#Z'): header = [] array = '' obs_for_troncon = False continue # La ligne contient des donnees else: if not header: # an error in the file structure continue l_b = io.StringIO(line) for l_r in csv.reader(l_b, dia): data = l_r row = list( zip( [h.lower() for h in header], [t for t in data] ) ) # observation if array == 'C': if obs_for_troncon: observations.append( row + [('id_troncon', t_id)] ) # Premiere ligne de description d'un troncon ou regard elif array == 'B01': if header[0].startswith('A'): t_id += 1 troncons.append([('id', t_id)] + row) elif header[0].startswith('C'): r_id += 1 regards.append([('id', r_id)] + row) # Ligne complémentaire de description else: if header[0].startswith('A'): troncons[-1] += row elif header[0].startswith('C'): regards[-1] += row # Recuperation des references de noeuds et dates itv_dates = [] regard_node_refs = [] regard_ref_id = {} max_r_id = last_r_id for reg in regards: # Stop the algorithm if cancel button has been clicked if feedback.isCanceled(): return {self.SUCCESS: 0} d_rg = dict(reg) if d_rg['caa'] and d_rg['caa'] not in regard_node_refs: regard_node_refs.append(d_rg['caa']) regard_ref_id[d_rg['caa']] = d_rg['id'] if d_rg['id'] and d_rg['id'] > max_r_id: max_r_id = d_rg['id'] if 'cbf' in d_rg and d_rg['cbf'] not in itv_dates: itv_dates.append(d_rg['cbf']) node_refs = [] for tro in troncons: # Stop the algorithm if cancel button has been clicked if feedback.isCanceled(): return {self.SUCCESS: 0} d_tr = dict(tro) # The nodes ref are stored in AAB, AAD, AAF and AAT if 'aab' in d_tr and \ d_tr['aab'] and \ d_tr['aab'] not in regard_node_refs and \ d_tr['aab'] not in node_refs: node_refs.append(d_tr['aab']) if 'aad' in d_tr and \ d_tr['aad'] and \ d_tr['aad'] not in regard_node_refs and \ d_tr['aad'] not in node_refs: node_refs.append(d_tr['aad']) if 'aaf' in d_tr and \ d_tr['aaf'] and \ d_tr['aaf'] not in regard_node_refs and \ d_tr['aaf'] not in node_refs: node_refs.append(d_tr['aaf']) if 'aat' in d_tr and \ d_tr['aat'] and \ d_tr['aat'] not in regard_node_refs and \ d_tr['aat'] not in node_refs: node_refs.append(d_tr['aat']) if 'abf' in d_tr and d_tr['abf'] not in itv_dates: itv_dates.append(d_tr['abf']) # Ajout des regards manquant for n_ref in node_refs: # Stop the algorithm if cancel button has been clicked if feedback.isCanceled(): return {self.SUCCESS: 0} max_r_id += 1 regards.append([('id', max_r_id), ('caa', n_ref)]) regard_ref_id[n_ref] = max_r_id # Ajout des identifiants de regards aux tronçons regard_refs = regard_ref_id.keys() for tro in troncons: # Stop the algorithm if cancel button has been clicked if feedback.isCanceled(): return {self.SUCCESS: 0} d_tr = dict(tro) # If AAD is not defined then it is equal to AAB if 'aad' not in d: d['aad'] = d['aab'] if d_tr['aad'] and \ d_tr['aad'] in regard_refs: tro += [('id_regard1', regard_ref_id[d_tr['aad']])] if d_tr['aaf'] and \ d_tr['aaf'] in regard_refs: tro += [('id_regard2', regard_ref_id[d_tr['aaf']])] if 'aat' in d_tr.keys() and \ d_tr['aat'] and \ d_tr['aat'] in regard_refs: tro += [('id_regard3', regard_ref_id[d_tr['aat']])] # Verification des champs fields = provider_fields(t_troncon.fields()) for tro in troncons: # Stop the algorithm if cancel button has been clicked if feedback.isCanceled(): return {self.SUCCESS: 0} for key, val in tro: # Stop the algorithm if cancel button has been clicked if feedback.isCanceled(): return {self.SUCCESS: 0} if fields.indexOf(key) == -1: raise QgsProcessingException( tr( '* ERREUR dans le fichier : ' 'le champs de tronçon "{}" est inconnue' ).format(key) ) field = fields.field(key) if isinstance(val, str) and field.isNumeric(): if val: try: float(val.replace(DECIMAL, '.')) except BaseException: raise QgsProcessingException( tr( '* ERREUR dans le fichier : ' 'le champs de tronçon "{}" est ' 'numérique mais pas la valeur "{}"' ).format(key, val) ) fields = provider_fields(t_obs.fields()) for obs in observations: # Stop the algorithm if cancel button has been clicked if feedback.isCanceled(): return {self.SUCCESS: 0} for key, val in obs: # Stop the algorithm if cancel button has been clicked if feedback.isCanceled(): return {self.SUCCESS: 0} if fields.indexOf(key) == -1: raise QgsProcessingException( tr( '* ERREUR dans le fichier : ' 'le champs d\'observation "{}" est ' 'inconnue' ).format(key) ) field = fields.field(key) if isinstance(val, str) and field.isNumeric(): if val: try: float(val.replace(DECIMAL, '.')) except BaseException: raise QgsProcessingException( tr( '* ERREUR dans le fichier : ' 'le champs d\'observation "{}" est ' 'numérique mais pas la valeur "{}"' ).format(key, val) ) fields = provider_fields(t_regard.fields()) for reg in regards: # Stop the algorithm if cancel button has been clicked if feedback.isCanceled(): return {self.SUCCESS: 0} for key, val in reg: # Stop the algorithm if cancel button has been clicked if feedback.isCanceled(): return {self.SUCCESS: 0} if fields.indexOf(key) == -1: raise QgsProcessingException( tr( '* ERREUR dans le fichier : ' 'le champs de regard "{}" est inconnue' ).format(key) ) field = fields.field(key) if isinstance(val, str) and field.isNumeric(): if val: try: float(val.replace(DECIMAL, '.')) except BaseException: raise QgsProcessingException( tr( '* ERREUR dans le fichier : ' 'le champs de regard "{}" est ' 'numérique mais pas la valeur "{}"' ).format(key, val) ) # Finalisation objet fichier feat_file.setAttribute('encoding', ENCODING) feat_file.setAttribute('lang', LANG) if VERSION: feat_file.setAttribute('version', VERSION) if itv_dates: feat_file.setAttribute('date_debut', min(itv_dates)) feat_file.setAttribute('date_fin', max(itv_dates)) # Stop the algorithm if cancel button has been clicked if feedback.isCanceled(): return {self.SUCCESS: 0} # Ajout de l'objet fichier t_file.startEditing() (res, outFeats) = t_file.dataProvider().addFeatures([feat_file]) if not res or not outFeats: raise QgsProcessingException( tr( '* ERREUR: lors de l\'enregistrement du fichier {}' ).format(', '.join(t_file.dataProvider().errors())) ) if not t_file.commitChanges(): raise QgsProcessingException( tr('* ERROR: Commit {}.').format(t_file.commitErrors()) ) # Mise a jour de l'identifiant de l'objet fichier feat_file.setAttribute('id', outFeats[0]['id']) # Creation des objets troncons features = [] fields = provider_fields(t_troncon.fields()) for tro in troncons: feat_t = QgsVectorLayerUtils.createFeature(t_troncon) feat_t.setAttribute('id_file', feat_file['id']) for key, val in tro: field = fields.field(key) if isinstance(val, str) and field.isNumeric(): if val: feat_t.setAttribute( key, float(val.replace(DECIMAL, '.')) ) else: feat_t.setAttribute(key, val) features.append(feat_t) # Ajout des objets troncons if features: t_troncon.startEditing() (res, outFeats) = t_troncon.dataProvider().addFeatures(features) if not res or not outFeats: raise QgsProcessingException( tr( '* ERREUR: lors de l\'enregistrement ' 'des troncon {}' ).format( ', '.join(t_troncon.dataProvider().errors()) ) ) if not t_troncon.commitChanges(): raise QgsProcessingException( tr('* ERROR: Commit {}.').format( t_troncon.commitErrors() ) ) # Creation des objets observations features = [] fields = provider_fields(t_obs.fields()) for obs in observations: feat_o = QgsVectorLayerUtils.createFeature(t_obs) feat_o.setAttribute('id_file', feat_file['id']) for key, val in obs: field = fields.field(key) if isinstance(val, str) and field.isNumeric(): if val: feat_o.setAttribute( key, float(val.replace(DECIMAL, '.')) ) else: feat_o.setAttribute(key, val) features.append(feat_o) # Ajout des objets observations if features: t_obs.startEditing() (res, outFeats) = t_obs.dataProvider().addFeatures(features) if not res or not outFeats: raise QgsProcessingException( tr( '* ERREUR: lors de l\'enregistrement ' 'des observations {}' ).format( ', '.join(t_obs.dataProvider().errors()) ) ) if not t_obs.commitChanges(): raise QgsProcessingException( tr('* ERROR: Commit {}.').format( t_obs.commitErrors() ) ) # Creation des objets regards features = [] fields = provider_fields(t_regard.fields()) for reg in regards: feat_r = QgsVectorLayerUtils.createFeature(t_regard) feat_r.setAttribute('id_file', feat_file['id']) for key, val in reg: field = fields.field(key) if isinstance(val, str) and field.isNumeric(): if val: feat_r.setAttribute( key, float(val.replace(DECIMAL, '.')) ) else: feat_r.setAttribute(key, val) features.append(feat_r) # Ajout des objets regards if features: t_regard.startEditing() (res, outFeats) = t_regard.dataProvider().addFeatures( features ) if not res or not outFeats: raise QgsProcessingException( tr( '* ERREUR: lors de l\'enregistrement ' 'des regards {}' ).format( ', '.join(t_regard.dataProvider().errors()) ) ) if not t_regard.commitChanges(): raise QgsProcessingException( tr('* ERROR: Commit %s.').format( t_regard.commitErrors() ) ) # Returns empty dict if no outputs return {self.SUCCESS: 1}