def smart_clip(layer_to_clip, mask_layer, callback=None): """Smart clip a vector layer with another. Issue https://github.com/inasafe/inasafe/issues/3186 :param layer_to_clip: The vector layer to clip. :type layer_to_clip: QgsVectorLayer :param mask_layer: The vector layer to use for clipping. :type mask_layer: QgsVectorLayer :param callback: A function to all to indicate progress. The function should accept params 'current' (int), 'maximum' (int) and 'step' (str). Defaults to None. :type callback: function :return: The clip vector layer. :rtype: QgsVectorLayer .. versionadded:: 4.0 """ output_layer_name = smart_clip_steps['output_layer_name'] processing_step = smart_clip_steps['step_name'] writer = create_memory_layer( output_layer_name, layer_to_clip.geometryType(), layer_to_clip.crs(), layer_to_clip.fields() ) writer.startEditing() # first build up a list of clip geometries request = QgsFeatureRequest().setSubsetOfAttributes([]) iterator = mask_layer.getFeatures(request) feature = next(iterator) geometries = QgsGeometry(feature.geometry()) # use prepared geometries for faster intersection tests # noinspection PyArgumentList engine = QgsGeometry.createGeometryEngine(geometries.geometry()) engine.prepareGeometry() extent = mask_layer.extent() for feature in layer_to_clip.getFeatures(QgsFeatureRequest(extent)): if engine.intersects(feature.geometry().geometry()): out_feat = QgsFeature() out_feat.setGeometry(feature.geometry()) out_feat.setAttributes(feature.attributes()) writer.addFeature(out_feat) writer.commitChanges() writer.keywords = layer_to_clip.keywords.copy() writer.keywords['title'] = output_layer_name check_layer(writer) return writer
def union(union_a, union_b): """Union of two vector layers. Issue https://github.com/inasafe/inasafe/issues/3186 :param union_a: The vector layer for the union. :type union_a: QgsVectorLayer :param union_b: The vector layer for the union. :type union_b: QgsVectorLayer :return: The clip vector layer. :rtype: QgsVectorLayer .. versionadded:: 4.0 """ output_layer_name = union_steps['output_layer_name'] output_layer_name = output_layer_name % ( union_a.keywords['layer_purpose'], union_b.keywords['layer_purpose'] ) keywords_union_1 = union_a.keywords keywords_union_2 = union_b.keywords inasafe_fields_union_1 = keywords_union_1['inasafe_fields'] inasafe_fields_union_2 = keywords_union_2['inasafe_fields'] inasafe_fields = inasafe_fields_union_1 inasafe_fields.update(inasafe_fields_union_2) parameters = {'INPUT': union_a, 'OVERLAY': union_b, 'OUTPUT': 'memory:'} # TODO implement callback through QgsProcessingFeedback object initialize_processing() feedback = create_processing_feedback() context = create_processing_context(feedback=feedback) result = processing.run('native:union', parameters, context=context) if result is None: raise ProcessingInstallationError union_layer = result['OUTPUT'] union_layer.setName(output_layer_name) # use to avoid modifying original source union_layer.keywords = dict(union_a.keywords) union_layer.keywords['inasafe_fields'] = inasafe_fields union_layer.keywords['title'] = output_layer_name union_layer.keywords['layer_purpose'] = 'aggregate_hazard' union_layer.keywords['hazard_keywords'] = keywords_union_1.copy() union_layer.keywords['aggregation_keywords'] = keywords_union_2.copy() fill_hazard_class(union_layer) check_layer(union_layer) return union_layer
def reproject(layer, output_crs, callback=None): """Reproject a vector layer to a specific CRS. Issue https://github.com/inasafe/inasafe/issues/3183 :param layer: The layer to reproject. :type layer: QgsVectorLayer :param output_crs: The destination CRS. :type output_crs: QgsCoordinateReferenceSystem :param callback: A function to all to indicate progress. The function should accept params 'current' (int), 'maximum' (int) and 'step' (str). Defaults to None. :type callback: function :return: Reprojected memory layer. :rtype: QgsVectorLayer .. versionadded:: 4.0 """ output_layer_name = reproject_steps['output_layer_name'] output_layer_name = output_layer_name % layer.keywords['layer_purpose'] processing_step = reproject_steps['step_name'] input_crs = layer.crs() input_fields = layer.fields() feature_count = layer.featureCount() reprojected = create_memory_layer(output_layer_name, layer.geometryType(), output_crs, input_fields) reprojected.startEditing() crs_transform = QgsCoordinateTransform(input_crs, output_crs, QgsProject.instance()) out_feature = QgsFeature() for i, feature in enumerate(layer.getFeatures()): geom = feature.geometry() geom.transform(crs_transform) out_feature.setGeometry(geom) out_feature.setAttributes(feature.attributes()) reprojected.addFeature(out_feature) if callback: callback(current=i, maximum=feature_count, step=processing_step) reprojected.commitChanges() # We transfer keywords to the output. # We don't need to update keywords as the CRS is dynamic. reprojected.keywords = layer.keywords reprojected.keywords['title'] = output_layer_name check_layer(reprojected) return reprojected
def reproject(layer, output_crs, callback=None): """Reproject a vector layer to a specific CRS. Issue https://github.com/inasafe/inasafe/issues/3183 :param layer: The layer to reproject. :type layer: QgsVectorLayer :param output_crs: The destination CRS. :type output_crs: QgsCoordinateReferenceSystem :param callback: A function to all to indicate progress. The function should accept params 'current' (int), 'maximum' (int) and 'step' (str). Defaults to None. :type callback: function :return: Reprojected memory layer. :rtype: QgsVectorLayer .. versionadded:: 4.0 """ output_layer_name = reproject_steps['output_layer_name'] output_layer_name = output_layer_name % layer.keywords['layer_purpose'] processing_step = reproject_steps['step_name'] input_crs = layer.crs() input_fields = layer.fields() feature_count = layer.featureCount() reprojected = create_memory_layer( output_layer_name, layer.geometryType(), output_crs, input_fields) reprojected.startEditing() crs_transform = QgsCoordinateTransform(input_crs, output_crs) out_feature = QgsFeature() for i, feature in enumerate(layer.getFeatures()): geom = feature.geometry() geom.transform(crs_transform) out_feature.setGeometry(geom) out_feature.setAttributes(feature.attributes()) reprojected.addFeature(out_feature) if callback: callback(current=i, maximum=feature_count, step=processing_step) reprojected.commitChanges() # We transfer keywords to the output. # We don't need to update keywords as the CRS is dynamic. reprojected.keywords = layer.keywords reprojected.keywords['title'] = output_layer_name check_layer(reprojected) return reprojected
def intersection(source, mask): """Intersect two layers. Issue https://github.com/inasafe/inasafe/issues/3186 :param source: The vector layer to clip. :type source: QgsVectorLayer :param mask: The vector layer to use for clipping. :type mask: QgsVectorLayer :return: The clip vector layer. :rtype: QgsVectorLayer .. versionadded:: 4.0 """ output_layer_name = intersection_steps['output_layer_name'] output_layer_name = output_layer_name % ( source.keywords['layer_purpose']) parameters = {'INPUT': source, 'OVERLAY': mask, 'OUTPUT': 'memory:'} # TODO implement callback through QgsProcessingFeedback object initialize_processing() feedback = create_processing_feedback() context = create_processing_context(feedback=feedback) result = processing.run('native:intersection', parameters, context=context) if result is None: raise ProcessingInstallationError intersect = result['OUTPUT'] intersect.setName(output_layer_name) intersect.keywords = dict(source.keywords) intersect.keywords['title'] = output_layer_name intersect.keywords['layer_purpose'] = \ layer_purpose_exposure_summary['key'] intersect.keywords['inasafe_fields'] = \ dict(source.keywords['inasafe_fields']) intersect.keywords['inasafe_fields'].update( mask.keywords['inasafe_fields']) intersect.keywords['hazard_keywords'] = \ dict(mask.keywords['hazard_keywords']) intersect.keywords['exposure_keywords'] = dict(source.keywords) intersect.keywords['aggregation_keywords'] = dict( mask.keywords['aggregation_keywords']) check_layer(intersect) return intersect
def intersection(source, mask): """Intersect two layers. Issue https://github.com/inasafe/inasafe/issues/3186 :param source: The vector layer to clip. :type source: QgsVectorLayer :param mask: The vector layer to use for clipping. :type mask: QgsVectorLayer :return: The clip vector layer. :rtype: QgsVectorLayer .. versionadded:: 4.0 """ output_layer_name = intersection_steps['output_layer_name'] output_layer_name = output_layer_name % (source.keywords['layer_purpose']) parameters = {'INPUT': source, 'OVERLAY': mask, 'OUTPUT': 'memory:'} # TODO implement callback through QgsProcessingFeedback object initialize_processing() feedback = create_processing_feedback() context = create_processing_context(feedback=feedback) result = processing.run('native:intersection', parameters, context=context) if result is None: raise ProcessingInstallationError intersect = result['OUTPUT'] intersect.setName(output_layer_name) intersect.keywords = dict(source.keywords) intersect.keywords['title'] = output_layer_name intersect.keywords['layer_purpose'] = \ layer_purpose_exposure_summary['key'] intersect.keywords['inasafe_fields'] = \ dict(source.keywords['inasafe_fields']) intersect.keywords['inasafe_fields'].update( mask.keywords['inasafe_fields']) intersect.keywords['hazard_keywords'] = \ dict(mask.keywords['hazard_keywords']) intersect.keywords['exposure_keywords'] = dict(source.keywords) intersect.keywords['aggregation_keywords'] = dict( mask.keywords['aggregation_keywords']) check_layer(intersect) return intersect
def clean_layer(layer): """Clean a vector layer. :param layer: The vector layer. :type layer: QgsVectorLayer :return: The buffered vector layer. :rtype: QgsVectorLayer """ output_layer_name = clean_geometry_steps['output_layer_name'] output_layer_name = output_layer_name % layer.keywords['layer_purpose'] # start editing layer.startEditing() count = 0 # iterate through all features request = QgsFeatureRequest().setSubsetOfAttributes([]) for feature in layer.getFeatures(request): geom = feature.geometry() was_valid, geometry_cleaned = geometry_checker(geom) if was_valid: # Do nothing if it was valid pass elif not was_valid and geometry_cleaned: # Update the geometry if it was not valid, and clean now layer.changeGeometry(feature.id(), geometry_cleaned, True) else: # Delete if it was not valid and not able to be cleaned count += 1 layer.deleteFeature(feature.id()) if count: LOGGER.critical( '%s features have been removed from %s because of invalid ' 'geometries.' % (count, layer.name())) else: LOGGER.info('No feature has been removed from the layer: %s' % layer.name()) # save changes layer.commitChanges() layer.keywords['title'] = output_layer_name check_layer(layer) return layer
def clean_layer(layer): """Clean a vector layer. :param layer: The vector layer. :type layer: QgsVectorLayer :return: The buffered vector layer. :rtype: QgsVectorLayer """ output_layer_name = clean_geometry_steps['output_layer_name'] output_layer_name = output_layer_name % layer.keywords['layer_purpose'] # start editing layer.startEditing() count = 0 # iterate through all features request = QgsFeatureRequest().setSubsetOfAttributes([]) for feature in layer.getFeatures(request): geom = feature.geometry() was_valid, geometry_cleaned = geometry_checker(geom) if was_valid: # Do nothing if it was valid pass elif not was_valid and geometry_cleaned: # Update the geometry if it was not valid, and clean now layer.changeGeometry(feature.id(), geometry_cleaned, True) else: # Delete if it was not valid and not able to be cleaned count += 1 layer.deleteFeature(feature.id()) if count: LOGGER.critical( '%s features have been removed from %s because of invalid ' 'geometries.' % (count, layer.name())) else: LOGGER.info( 'No feature has been removed from the layer: %s' % layer.name()) # save changes layer.commitChanges() layer.keywords['title'] = output_layer_name check_layer(layer) return layer
def clean_layer(layer): """Clean a vector layer. :param layer: The vector layer. :type layer: qgis.core.QgsVectorLayer :return: The buffered vector layer. :rtype: qgis.core.QgsVectorLayer """ output_layer_name = clean_geometry_steps['output_layer_name'] output_layer_name = output_layer_name % layer.keywords['layer_purpose'] count = layer.featureCount() parameters = {'INPUT': layer, 'OUTPUT': 'memory:'} initialize_processing() feedback = create_processing_feedback() context = create_processing_context(feedback=feedback) result = processing.run('qgis:fixgeometries', parameters, context=context) if result is None: raise ProcessingInstallationError cleaned = result['OUTPUT'] cleaned.setName(output_layer_name) removed_count = count - cleaned.featureCount() if removed_count: LOGGER.critical( '{removed_count} features have been removed from {layer_name} ' 'because of invalid geometries.'.format( removed_count=removed_count, layer_name=layer.name())) else: LOGGER.info('No feature has been removed from the layer: ' '{layer_name}'.format(layer_name=layer.name())) cleaned.keywords = layer.keywords.copy() cleaned.keywords['title'] = output_layer_name check_layer(cleaned) return cleaned
def clean_layer(layer, callback=None): """Clean a vector layer. :param layer: The vector layer. :type layer: QgsVectorLayer :param callback: A function to all to indicate progress. The function editing should accept params 'current' (int), 'maximum' (int) and 'step' (str). Defaults to None. :type callback: function :return: The buffered vector layer. :rtype: QgsVectorLayer """ output_layer_name = clean_geometry_steps['output_layer_name'] processing_step = clean_geometry_steps['step_name'] # NOQA output_layer_name = output_layer_name % layer.keywords['layer_purpose'] # start editing layer.startEditing() count = 0 # iterate through all features for feature in layer.getFeatures(): geom = feature.geometry() geometry_cleaned = geometry_checker(geom) if geometry_cleaned: feature.setGeometry(geometry_cleaned) else: count += 1 layer.deleteFeature(feature.id()) LOGGER.critical( '%s features have been removed from %s because of invalid geometries.' % (count, layer.name())) # save changes layer.commitChanges() layer.keywords['title'] = output_layer_name check_layer(layer) return layer
def clip(layer_to_clip, mask_layer): """Clip a vector layer with another. Issue https://github.com/inasafe/inasafe/issues/3186 :param layer_to_clip: The vector layer to clip. :type layer_to_clip: QgsVectorLayer :param mask_layer: The vector layer to use for clipping. :type mask_layer: QgsVectorLayer :return: The clip vector layer. :rtype: QgsVectorLayer .. versionadded:: 4.0 """ output_layer_name = clip_steps['output_layer_name'] output_layer_name = output_layer_name % ( layer_to_clip.keywords['layer_purpose']) parameters = { 'INPUT': layer_to_clip, 'OVERLAY': mask_layer, 'OUTPUT': 'memory:' } # TODO implement callback through QgsProcessingFeedback object initialize_processing() feedback = create_processing_feedback() context = create_processing_context(feedback=feedback) result = processing.run('native:clip', parameters, context=context) if result is None: raise ProcessingInstallationError clipped = result['OUTPUT'] clipped.setName(output_layer_name) clipped.keywords = layer_to_clip.keywords.copy() clipped.keywords['title'] = output_layer_name check_layer(clipped) return clipped
def clean_layer(layer, callback=None): """Clean a vector layer. :param layer: The vector layer. :type layer: QgsVectorLayer :param callback: A function to all to indicate progress. The function editing should accept params 'current' (int), 'maximum' (int) and 'step' (str). Defaults to None. :type callback: function :return: The buffered vector layer. :rtype: QgsVectorLayer """ output_layer_name = clean_geometry_steps['output_layer_name'] processing_step = clean_geometry_steps['step_name'] output_layer_name = output_layer_name % layer.keywords['layer_purpose'] # start editing layer.startEditing() # iterate through all features for feature in layer.getFeatures(): geom = feature.geometry() geometry_cleaned = geometry_checker(geom) if geometry_cleaned: feature.setGeometry(geometry_cleaned) else: layer.deleteFeature(feature.id()) # save changes layer.commitChanges() layer.keywords['title'] = output_layer_name check_layer(layer) return layer
def from_counts_to_ratios(layer, callback=None): """Transform counts to ratios. Formula: ratio = subset count / total count :param layer: The vector layer. :type layer: QgsVectorLayer :param callback: A function to all to indicate progress. The function should accept params 'current' (int), 'maximum' (int) and 'step' (str). Defaults to None. :type callback: function :return: The layer with new ratios. :rtype: QgsVectorLayer .. versionadded:: 4.0 """ output_layer_name = recompute_counts_steps['output_layer_name'] processing_step = recompute_counts_steps['step_name'] exposure = definition(layer.keywords['exposure']) inasafe_fields = layer.keywords['inasafe_fields'] layer.keywords['title'] = output_layer_name if not population_count_field['key'] in inasafe_fields: # There is not a population count field. Let's skip this layer. LOGGER.info( 'Population count field {population_count_field} is not detected ' 'in the exposure. We will not compute a ratio from this field ' 'because the formula needs Population count field. Formula: ' 'ratio = subset count / total count.'.format( population_count_field=population_count_field['key'])) return layer layer.startEditing() mapping = {} non_compulsory_fields = get_non_compulsory_fields( layer_purpose_exposure['key'], exposure['key']) for count_field in non_compulsory_fields: exists = count_field['key'] in inasafe_fields if count_field['key'] in count_ratio_mapping.keys() and exists: ratio_field = definition(count_ratio_mapping[count_field['key']]) field = create_field_from_definition(ratio_field) layer.addAttribute(field) name = ratio_field['field_name'] layer.keywords['inasafe_fields'][ratio_field['key']] = name mapping[count_field['field_name']] = layer.fieldNameIndex(name) LOGGER.info( 'Count field {count_field} detected in the exposure, we are ' 'going to create a equivalent field {ratio_field} in the ' 'exposure layer.'.format(count_field=count_field['key'], ratio_field=ratio_field['key'])) else: LOGGER.info( 'Count field {count_field} not detected in the exposure. We ' 'will not compute a ratio from this field.'.format( count_field=count_field['key'])) if len(mapping) == 0: # There is not a subset count field. Let's skip this layer. layer.commitChanges() return layer for feature in layer.getFeatures(): total_count = feature[inasafe_fields[population_count_field['key']]] for count_field, index in mapping.iteritems(): count = feature[count_field] try: new_value = count / total_count except TypeError: new_value = '' layer.changeAttributeValue(feature.id(), index, new_value) layer.commitChanges() check_layer(layer) return layer
def intersection(source, mask, callback=None): """Intersect two layers. Issue https://github.com/inasafe/inasafe/issues/3186 Note : This algorithm is copied from : https://github.com/qgis/QGIS/blob/master/python/plugins/processing/algs/ qgis/Intersection.py :param source: The vector layer to clip. :type source: QgsVectorLayer :param mask: The vector layer to use for clipping. :type mask: QgsVectorLayer :param callback: A function to all to indicate progress. The function should accept params 'current' (int), 'maximum' (int) and 'step' (str). Defaults to None. :type callback: function :return: The clip vector layer. :rtype: QgsVectorLayer .. versionadded:: 4.0 """ output_layer_name = intersection_steps['output_layer_name'] output_layer_name = output_layer_name % ( source.keywords['layer_purpose']) processing_step = intersection_steps['step_name'] fields = source.fields() fields.extend(mask.fields()) writer = create_memory_layer( output_layer_name, source.geometryType(), source.crs(), fields ) writer.startEditing() # Begin copy/paste from Processing plugin. # Please follow their code as their code is optimized. # The code below is not following our coding standards because we want to # be able to track any diffs from QGIS easily. out_feature = QgsFeature() index = create_spatial_index(mask) # Todo callback # total = 100.0 / len(selectionA) for current, in_feature in enumerate(source.getFeatures()): # progress.setPercentage(int(current * total)) geom = in_feature.geometry() attributes = in_feature.attributes() intersects = index.intersects(geom.boundingBox()) for i in intersects: request = QgsFeatureRequest().setFilterFid(i) feature_mask = next(mask.getFeatures(request)) tmp_geom = feature_mask.geometry() if geom.intersects(tmp_geom): mask_attributes = feature_mask.attributes() int_geom = QgsGeometry(geom.intersection(tmp_geom)) if int_geom.wkbType() == QgsWKBTypes.Unknown\ or QgsWKBTypes.flatType( int_geom.geometry().wkbType()) ==\ QgsWKBTypes.GeometryCollection: int_com = geom.combine(tmp_geom) int_geom = QgsGeometry() if int_com: int_sym = geom.symDifference(tmp_geom) int_geom = QgsGeometry(int_com.difference(int_sym)) if int_geom.isGeosEmpty() or not int_geom.isGeosValid(): # LOGGER.debug( # tr('GEOS geoprocessing error: One or more input ' # 'features have invalid geometry.')) pass try: geom_types = wkb_type_groups[ wkb_type_groups[int_geom.wkbType()]] if int_geom.wkbType() in geom_types: if int_geom.type() == source.geometryType(): # We got some features which have not the same # kind of geometry. We want to skip them. out_feature.setGeometry(int_geom) attrs = [] attrs.extend(attributes) attrs.extend(mask_attributes) out_feature.setAttributes(attrs) writer.addFeature(out_feature) except: LOGGER.debug( tr('Feature geometry error: One or more output ' 'features ignored due to invalid geometry.')) continue # End copy/paste from Processing plugin. writer.commitChanges() writer.keywords = dict(source.keywords) writer.keywords['title'] = output_layer_name writer.keywords['layer_purpose'] = layer_purpose_exposure_summary['key'] writer.keywords['inasafe_fields'] = dict(source.keywords['inasafe_fields']) writer.keywords['inasafe_fields'].update(mask.keywords['inasafe_fields']) writer.keywords['hazard_keywords'] = dict(mask.keywords['hazard_keywords']) writer.keywords['exposure_keywords'] = dict(source.keywords) writer.keywords['aggregation_keywords'] = dict( mask.keywords['aggregation_keywords']) check_layer(writer) return writer
def analysis_summary(aggregate_hazard, analysis, callback=None): """Compute the summary from the aggregate hazard to analysis. Source layer : | haz_id | haz_class | aggr_id | aggr_name | total_feature | Target layer : | analysis_id | Output layer : | analysis_id | count_hazard_class | affected_count | total | :param aggregate_hazard: The layer to aggregate vector layer. :type aggregate_hazard: QgsVectorLayer :param analysis: The target vector layer where to write statistics. :type analysis: QgsVectorLayer :param callback: A function to all to indicate progress. The function should accept params 'current' (int), 'maximum' (int) and 'step' (str). Defaults to None. :type callback: function :return: The new target layer with summary. :rtype: QgsVectorLayer .. versionadded:: 4.0 """ output_layer_name = summary_3_analysis_steps['output_layer_name'] processing_step = summary_3_analysis_steps['step_name'] source_fields = aggregate_hazard.keywords['inasafe_fields'] target_fields = analysis.keywords['inasafe_fields'] target_compulsory_fields = [ analysis_id_field, analysis_name_field, ] check_inputs(target_compulsory_fields, target_fields) source_compulsory_fields = [ aggregation_id_field, aggregation_name_field, hazard_id_field, hazard_class_field, total_field ] check_inputs(source_compulsory_fields, source_fields) absolute_values = create_absolute_values_structure(aggregate_hazard, ['all']) hazard_class = source_fields[hazard_class_field['key']] hazard_class_index = aggregate_hazard.fieldNameIndex(hazard_class) unique_hazard = aggregate_hazard.uniqueValues(hazard_class_index) hazard_keywords = aggregate_hazard.keywords['hazard_keywords'] classification = hazard_keywords['classification'] total = source_fields[total_field['key']] flat_table = FlatTable('hazard_class') # First loop over the aggregate_hazard layer request = QgsFeatureRequest() request.setSubsetOfAttributes([hazard_class, total], aggregate_hazard.fields()) request.setFlags(QgsFeatureRequest.NoGeometry) for area in aggregate_hazard.getFeatures(): hazard_value = area[hazard_class_index] value = area[total] if not value or isinstance(value, QPyNullVariant) or isnan(value): # For isnan, see ticket #3812 value = 0 if not hazard_value or isinstance(hazard_value, QPyNullVariant): hazard_value = 'NULL' flat_table.add_value(value, hazard_class=hazard_value) # We summarize every absolute values. for field, field_definition in absolute_values.iteritems(): value = area[field] if not value or isinstance(value, QPyNullVariant): value = 0 field_definition[0].add_value(value, all='all') analysis.startEditing() shift = analysis.fields().count() counts = [ total_affected_field, total_not_affected_field, total_not_exposed_field, total_field ] add_fields(analysis, absolute_values, counts, unique_hazard, hazard_count_field) affected_sum = 0 not_affected_sum = 0 not_exposed_sum = 0 for area in analysis.getFeatures(request): total = 0 for i, val in enumerate(unique_hazard): if not val or isinstance(val, QPyNullVariant): val = 'NULL' sum = flat_table.get_value(hazard_class=val) total += sum analysis.changeAttributeValue(area.id(), shift + i, sum) affected = post_processor_affected_function( classification=classification, hazard_class=val) if affected == not_exposed_class['key']: not_exposed_sum += sum elif affected: affected_sum += sum else: not_affected_sum += sum # Affected field analysis.changeAttributeValue(area.id(), shift + len(unique_hazard), affected_sum) # Not affected field analysis.changeAttributeValue(area.id(), shift + len(unique_hazard) + 1, not_affected_sum) # Not exposed field analysis.changeAttributeValue(area.id(), shift + len(unique_hazard) + 2, not_exposed_sum) # Total field analysis.changeAttributeValue(area.id(), shift + len(unique_hazard) + 3, total) # Any absolute postprocessors for i, field in enumerate(absolute_values.itervalues()): value = field[0].get_value(all='all') analysis.changeAttributeValue(area.id(), shift + len(unique_hazard) + 4 + i, value) # Sanity check ± 1 to the result. Disabled for now as it seems ± 1 is not # enough. ET 13/02/17 # total_computed = ( # affected_sum + not_affected_sum + not_exposed_sum) # if not -1 < (total_computed - total) < 1: # raise ComputationError analysis.commitChanges() analysis.keywords['title'] = layer_purpose_analysis_impacted['name'] if qgis_version() >= 21600: analysis.setName(analysis.keywords['title']) else: analysis.setLayerName(analysis.keywords['title']) analysis.keywords['layer_purpose'] = layer_purpose_analysis_impacted['key'] check_layer(analysis) return analysis
def align_rasters(hazard_layer, exposure_layer, extent): """Align hazard and exposure raster layers. Align hazard and exposure raster layers so they fit perfectly and so they can be used for raster algebra. The method uses QGIS raster alignment tool to do the work (which in turn uses GDAL). Alignment of layers means that the layers have the same CRS, cell size, grid origin and size. That involves clipping and resampling of rasters. From the two layers, the layer with finer resolution (smaller cell size) will be used as the reference for the alignment (i.e. parameters will be set to its CRS, cell size and grid offset). - Reproject to the same CRS. - Resample to the same cell size and offset in the grid. - Clip to a region of interest. :param hazard_layer: Hazard layer to be aligned. :type hazard_layer: QgsRasterLayer :param exposure_layer: Exposure layer to be aligned. :type exposure_layer: QgsRasterLayer :param extent: Extent in exposure CRS to which raster should be clipped. :type extent: QgsRectangle :return: Clipped hazard and exposure layers. :rtype: QgsRasterLayer, QgsRasterLayer """ output_layer_name = align_steps['output_layer_name'] processing_step = align_steps['step_name'] hazard_output = unique_filename(suffix='.tif') exposure_output = unique_filename(suffix='.tif') # Setup the two raster layers for alignment align = QgsAlignRaster() inputs = [ QgsAlignRaster.Item(hazard_layer.source(), hazard_output), QgsAlignRaster.Item(exposure_layer.source(), exposure_output) ] if exposure_layer.keywords.get('exposure_unit') == 'count': inputs[1].rescaleValues = True align.setRasters(inputs) # Find out which layer has finer grid and use it as the reference. # This will setup destination CRS, cell size and grid origin if exposure_layer.keywords.get('allow_resampling', True): index = align.suggestedReferenceLayer() else: index = 1 # have to use exposure layer as the reference if index < 0: raise AlignRastersError(tr('Unable to select reference layer')) if not align.setParametersFromRaster( inputs[index].inputFilename, exposure_layer.crs().toWkt()): raise AlignRastersError(align.errorMessage()) # Setup clip extent align.setClipExtent(extent) # Everything configured - do the alignment now! # For each raster, it will create output file and write resampled values if not align.run(): raise AlignRastersError(align.errorMessage()) # Load resulting layers aligned_hazard_layer = QgsRasterLayer( hazard_output, output_layer_name % 'hazard') aligned_exposure_layer = QgsRasterLayer( exposure_output, output_layer_name % 'exposure') aligned_hazard_layer.keywords = dict(hazard_layer.keywords) aligned_hazard_layer.keywords['title'] = output_layer_name % 'hazard' aligned_exposure_layer.keywords = dict(exposure_layer.keywords) aligned_exposure_layer.keywords['title'] = output_layer_name % 'exposure' # avoid any possible further rescaling of exposure data by correctly # setting original resolution to be the same as current resolution aligned_exposure_layer.keywords['resolution'] = ( align.cellSize().width(), align.cellSize().height()) check_layer(exposure_layer) check_layer(hazard_layer) return aligned_hazard_layer, aligned_exposure_layer
def add_default_values(layer, callback=None): """Add or fill default values to the layer, see #3325. 1. It doesn't have inasafe_field and it doesn't have inasafe_default_value --> Do nothing. 2. It has inasafe_field and it does not have inasafe_default_value --> Do nothing. 3. It does not have inasafe_field but it has inasafe_default_value --> Create new field, and fill with the default value for all features 4. It has inasafe_field and it has inasafe_default_value --> Replace the null value with the default one. :param layer: The vector layer. :type layer: QgsVectorLayer :param callback: A function to all to indicate progress. The function should accept params 'current' (int), 'maximum' (int) and 'step' (str). Defaults to None. :type callback: function :return: The vector layer with the default values. :rtype: QgsVectorLayer .. versionadded:: 4.0 """ output_layer_name = assign_default_values_steps['output_layer_name'] output_layer_name = output_layer_name % layer.keywords['layer_purpose'] processing_step = assign_default_values_steps['step_name'] fields = layer.keywords.get('inasafe_fields') if not isinstance(fields, dict): msg = 'inasafe_fields is missing in keywords from %s' % layer.name() raise InvalidKeywordsForProcessingAlgorithm(msg) defaults = layer.keywords.get('inasafe_default_values') if not defaults: # Case 1 and 2. LOGGER.info( 'inasafe_default_value is not present, we can not fill default ' 'ratios for this layer.') return layer for default in defaults.keys(): field = fields.get(default) target_field = definition(default) layer.startEditing() if not field: # Case 3 LOGGER.info( '{field} is not present but the layer has {value} as a ' 'default for {field}. We create the new field ' '{new_field} with this value.'.format( field=target_field['key'], value=defaults[default], new_field=target_field['field_name'])) new_field = create_field_from_definition(target_field) layer.addAttribute(new_field) new_index = layer.fieldNameIndex(new_field.name()) for feature in layer.getFeatures(): layer.changeAttributeValue( feature.id(), new_index, defaults[default]) layer.keywords['inasafe_fields'][target_field['key']] = ( target_field['field_name']) else: # Case 4 LOGGER.info( '{field} is present and the layer has {value} as a ' 'default for {field}, we MUST do nothing.'.format( field=target_field['key'], value=defaults[default])) index = layer.fieldNameIndex(field) for feature in layer.getFeatures(): if isinstance(feature.attributes()[index], QPyNullVariant): layer.changeAttributeValue( feature.id(), index, defaults[default]) continue if feature.attributes()[index] == '': layer.changeAttributeValue( feature.id(), index, defaults[default]) continue layer.commitChanges() layer.keywords['title'] = output_layer_name check_layer(layer) return layer
def aggregation_summary(aggregate_hazard, aggregation, callback=None): """Compute the summary from the aggregate hazard to the analysis layer. Source layer : | haz_id | haz_class | aggr_id | aggr_name | total_feature | Target layer : | aggr_id | aggr_name | Output layer : | aggr_id | aggr_name | count of affected features per exposure type :param aggregate_hazard: The layer to aggregate vector layer. :type aggregate_hazard: QgsVectorLayer :param aggregation: The aggregation vector layer where to write statistics. :type aggregation: QgsVectorLayer :param callback: A function to all to indicate progress. The function should accept params 'current' (int), 'maximum' (int) and 'step' (str). Defaults to None. :type callback: function :return: The new aggregation layer with summary. :rtype: QgsVectorLayer .. versionadded:: 4.0 """ output_layer_name = summary_2_aggregation_steps['output_layer_name'] processing_step = summary_2_aggregation_steps['step_name'] source_fields = aggregate_hazard.keywords['inasafe_fields'] target_fields = aggregation.keywords['inasafe_fields'] target_compulsory_fields = [ aggregation_id_field, aggregation_name_field, ] check_inputs(target_compulsory_fields, target_fields) # Missing exposure_count_field source_compulsory_fields = [ aggregation_id_field, aggregation_name_field, hazard_id_field, hazard_class_field, affected_field, ] check_inputs(source_compulsory_fields, source_fields) pattern = exposure_count_field['key'] pattern = pattern.replace('%s', '') unique_exposure = read_dynamic_inasafe_field( source_fields, exposure_count_field) absolute_values = create_absolute_values_structure( aggregate_hazard, ['aggregation_id']) flat_table = FlatTable('aggregation_id', 'exposure_class') aggregation_index = source_fields[aggregation_id_field['key']] # We want to loop over affected features only. request = QgsFeatureRequest() request.setFlags(QgsFeatureRequest.NoGeometry) expression = '\"%s\" = \'%s\'' % ( affected_field['field_name'], tr('True')) request.setFilterExpression(expression) for area in aggregate_hazard.getFeatures(request): for key, name_field in source_fields.iteritems(): if key.endswith(pattern): aggregation_id = area[aggregation_index] exposure_class = key.replace(pattern, '') value = area[name_field] flat_table.add_value( value, aggregation_id=aggregation_id, exposure_class=exposure_class ) # We summarize every absolute values. for field, field_definition in absolute_values.iteritems(): value = area[field] if not value or isinstance(value, QPyNullVariant): value = 0 field_definition[0].add_value( value, aggregation_id=area[aggregation_index], ) shift = aggregation.fields().count() aggregation.startEditing() add_fields( aggregation, absolute_values, [total_affected_field], unique_exposure, affected_exposure_count_field) aggregation_index = target_fields[aggregation_id_field['key']] request = QgsFeatureRequest() request.setFlags(QgsFeatureRequest.NoGeometry) for area in aggregation.getFeatures(request): aggregation_value = area[aggregation_index] total = 0 for i, val in enumerate(unique_exposure): sum = flat_table.get_value( aggregation_id=aggregation_value, exposure_class=val ) total += sum aggregation.changeAttributeValue(area.id(), shift + i, sum) aggregation.changeAttributeValue( area.id(), shift + len(unique_exposure), total) for i, field in enumerate(absolute_values.itervalues()): value = field[0].get_value( aggregation_id=aggregation_value, ) target_index = shift + len(unique_exposure) + 1 + i aggregation.changeAttributeValue( area.id(), target_index, value) aggregation.commitChanges() aggregation.keywords['title'] = layer_purpose_aggregation_summary['name'] if qgis_version() >= 21800: aggregation.setName(aggregation.keywords['title']) else: aggregation.setLayerName(aggregation.keywords['title']) aggregation.keywords['layer_purpose'] = ( layer_purpose_aggregation_summary['key']) check_layer(aggregation) return aggregation
def analysis_summary(aggregate_hazard, analysis, callback=None): """Compute the summary from the aggregate hazard to analysis. Source layer : | haz_id | haz_class | aggr_id | aggr_name | total_feature | Target layer : | analysis_id | Output layer : | analysis_id | count_hazard_class | affected_count | total | :param aggregate_hazard: The layer to aggregate vector layer. :type aggregate_hazard: QgsVectorLayer :param analysis: The target vector layer where to write statistics. :type analysis: QgsVectorLayer :param callback: A function to all to indicate progress. The function should accept params 'current' (int), 'maximum' (int) and 'step' (str). Defaults to None. :type callback: function :return: The new target layer with summary. :rtype: QgsVectorLayer .. versionadded:: 4.0 """ output_layer_name = summary_3_analysis_steps['output_layer_name'] processing_step = summary_3_analysis_steps['step_name'] source_fields = aggregate_hazard.keywords['inasafe_fields'] target_fields = analysis.keywords['inasafe_fields'] target_compulsory_fields = [ analysis_id_field, analysis_name_field, ] check_inputs(target_compulsory_fields, target_fields) source_compulsory_fields = [ aggregation_id_field, aggregation_name_field, hazard_id_field, hazard_class_field, total_field ] check_inputs(source_compulsory_fields, source_fields) absolute_values = create_absolute_values_structure( aggregate_hazard, ['all']) hazard_class = source_fields[hazard_class_field['key']] hazard_class_index = aggregate_hazard.fieldNameIndex(hazard_class) unique_hazard = aggregate_hazard.uniqueValues(hazard_class_index) hazard_keywords = aggregate_hazard.keywords['hazard_keywords'] classification = hazard_keywords['classification'] total = source_fields[total_field['key']] flat_table = FlatTable('hazard_class') # First loop over the aggregate_hazard layer request = QgsFeatureRequest() request.setSubsetOfAttributes( [hazard_class, total], aggregate_hazard.fields()) request.setFlags(QgsFeatureRequest.NoGeometry) for area in aggregate_hazard.getFeatures(): hazard_value = area[hazard_class_index] value = area[total] if not value or isinstance(value, QPyNullVariant) or isnan(value): # For isnan, see ticket #3812 value = 0 if not hazard_value or isinstance(hazard_value, QPyNullVariant): hazard_value = 'NULL' flat_table.add_value( value, hazard_class=hazard_value ) # We summarize every absolute values. for field, field_definition in absolute_values.iteritems(): value = area[field] if not value or isinstance(value, QPyNullVariant): value = 0 field_definition[0].add_value( value, all='all' ) analysis.startEditing() shift = analysis.fields().count() counts = [ total_affected_field, total_not_affected_field, total_not_exposed_field, total_field] add_fields( analysis, absolute_values, counts, unique_hazard, hazard_count_field) affected_sum = 0 not_affected_sum = 0 not_exposed_sum = 0 for area in analysis.getFeatures(request): total = 0 for i, val in enumerate(unique_hazard): if not val or isinstance(val, QPyNullVariant): val = 'NULL' sum = flat_table.get_value(hazard_class=val) total += sum analysis.changeAttributeValue(area.id(), shift + i, sum) affected = post_processor_affected_function( classification=classification, hazard_class=val) if affected == not_exposed_class['key']: not_exposed_sum += sum elif affected: affected_sum += sum else: not_affected_sum += sum # Affected field analysis.changeAttributeValue( area.id(), shift + len(unique_hazard), affected_sum) # Not affected field analysis.changeAttributeValue( area.id(), shift + len(unique_hazard) + 1, not_affected_sum) # Not exposed field analysis.changeAttributeValue( area.id(), shift + len(unique_hazard) + 2, not_exposed_sum) # Total field analysis.changeAttributeValue( area.id(), shift + len(unique_hazard) + 3, total) # Any absolute postprocessors for i, field in enumerate(absolute_values.itervalues()): value = field[0].get_value( all='all' ) analysis.changeAttributeValue( area.id(), shift + len(unique_hazard) + 4 + i, value) # Sanity check ± 1 to the result. Disabled for now as it seems ± 1 is not # enough. ET 13/02/17 # total_computed = ( # affected_sum + not_affected_sum + not_exposed_sum) # if not -1 < (total_computed - total) < 1: # raise ComputationError analysis.commitChanges() analysis.keywords['title'] = layer_purpose_analysis_impacted['name'] if qgis_version() >= 21600: analysis.setName(analysis.keywords['title']) else: analysis.setLayerName(analysis.keywords['title']) analysis.keywords['layer_purpose'] = layer_purpose_analysis_impacted['key'] check_layer(analysis) return analysis
def from_counts_to_ratios(layer, callback=None): """Transform counts to ratios. Formula: ratio = subset count / total count :param layer: The vector layer. :type layer: QgsVectorLayer :param callback: A function to all to indicate progress. The function should accept params 'current' (int), 'maximum' (int) and 'step' (str). Defaults to None. :type callback: function :return: The layer with new ratios. :rtype: QgsVectorLayer .. versionadded:: 4.0 """ output_layer_name = recompute_counts_steps['output_layer_name'] processing_step = recompute_counts_steps['step_name'] exposure = definition(layer.keywords['exposure']) inasafe_fields = layer.keywords['inasafe_fields'] layer.keywords['title'] = output_layer_name if not population_count_field['key'] in inasafe_fields: # There is not a population count field. Let's skip this layer. LOGGER.info( 'Population count field {population_count_field} is not detected ' 'in the exposure. We will not compute a ratio from this field ' 'because the formula needs Population count field. Formula: ' 'ratio = subset count / total count.'.format( population_count_field=population_count_field['key'])) return layer layer.startEditing() mapping = {} non_compulsory_fields = get_non_compulsory_fields( layer_purpose_exposure['key'], exposure['key']) for count_field in non_compulsory_fields: exists = count_field['key'] in inasafe_fields if count_field['key'] in count_ratio_mapping.keys() and exists: ratio_field = definition(count_ratio_mapping[count_field['key']]) field = create_field_from_definition(ratio_field) layer.addAttribute(field) name = ratio_field['field_name'] layer.keywords['inasafe_fields'][ratio_field['key']] = name mapping[count_field['field_name']] = layer.fieldNameIndex(name) LOGGER.info( 'Count field {count_field} detected in the exposure, we are ' 'going to create a equivalent field {ratio_field} in the ' 'exposure layer.'.format( count_field=count_field['key'], ratio_field=ratio_field['key'])) else: LOGGER.info( 'Count field {count_field} not detected in the exposure. We ' 'will not compute a ratio from this field.'.format( count_field=count_field['key'])) if len(mapping) == 0: # There is not a subset count field. Let's skip this layer. layer.commitChanges() return layer for feature in layer.getFeatures(): total_count = feature[inasafe_fields[population_count_field['key']]] for count_field, index in mapping.iteritems(): count = feature[count_field] try: new_value = count / total_count except TypeError: new_value = '' layer.changeAttributeValue(feature.id(), index, new_value) layer.commitChanges() check_layer(layer) return layer
def prepare_vector_layer(layer, callback=None): """This function will prepare the layer to be used in InaSAFE : * Make a local copy of the layer. * Make sure that we have an InaSAFE ID column. * Rename fields according to our definitions. * Remove fields which are not used. :param layer: The layer to prepare. :type layer: QgsVectorLayer :param callback: A function to all to indicate progress. The function should accept params 'current' (int), 'maximum' (int) and 'step' (str). Defaults to None. :type callback: function :return: Cleaned memory layer. :rtype: QgsVectorLayer .. versionadded:: 4.0 """ output_layer_name = prepare_vector_steps['output_layer_name'] output_layer_name = output_layer_name % layer.keywords['layer_purpose'] processing_step = prepare_vector_steps['step_name'] if not layer.keywords.get('inasafe_fields'): msg = 'inasafe_fields is missing in keywords from %s' % layer.name() raise InvalidKeywordsForProcessingAlgorithm(msg) feature_count = layer.featureCount() cleaned = create_memory_layer( output_layer_name, layer.geometryType(), layer.crs(), layer.fields()) # We transfer keywords to the output. cleaned.keywords = layer.keywords copy_layer(layer, cleaned) _remove_features(cleaned) # After removing rows, let's check if there is still a feature. request = QgsFeatureRequest().setFlags(QgsFeatureRequest.NoGeometry) iterator = cleaned.getFeatures(request) try: next(iterator) except StopIteration: LOGGER.warning( tr('No feature has been found in the {purpose}' .format(purpose=layer.keywords['layer_purpose']))) raise NoFeaturesInExtentError _add_id_column(cleaned) clean_inasafe_fields(cleaned) if _size_is_needed(cleaned): LOGGER.info( 'We noticed some counts in your exposure layer. Before to update ' 'geometries, we compute the original size for each feature.') run_single_post_processor(cleaned, post_processor_size) if cleaned.keywords['layer_purpose'] == 'exposure': fields = cleaned.keywords['inasafe_fields'] if exposure_type_field['key'] not in fields: _add_default_exposure_class(cleaned) # Check value mapping _check_value_mapping(cleaned) cleaned.keywords['title'] = output_layer_name check_layer(cleaned) return cleaned
def zonal_stats(raster, vector): """Reclassify a continuous raster layer. Issue https://github.com/inasafe/inasafe/issues/3190 The algorithm will take care about projections. We don't want to reproject the raster layer. So if CRS are different, we reproject the vector layer and then we do a lookup from the reprojected layer to the original vector layer. :param raster: The raster layer. :type raster: QgsRasterLayer :param vector: The vector layer. :type vector: QgsVectorLayer :return: The output of the zonal stats. :rtype: QgsVectorLayer .. versionadded:: 4.0 """ output_layer_name = zonal_stats_steps['output_layer_name'] exposure = raster.keywords['exposure'] if raster.crs().authid() != vector.crs().authid(): layer = reproject(vector, raster.crs()) # We prepare the copy output_layer = create_memory_layer( output_layer_name, vector.geometryType(), vector.crs(), vector.fields() ) copy_layer(vector, output_layer) else: layer = create_memory_layer( output_layer_name, vector.geometryType(), vector.crs(), vector.fields() ) copy_layer(vector, layer) input_band = layer.keywords.get('active_band', 1) analysis = QgsZonalStatistics( layer, raster, 'exposure_', input_band, QgsZonalStatistics.Sum) result = analysis.calculateStatistics(None) LOGGER.debug(tr('Zonal stats on %s : %s' % (raster.source(), result))) output_field = exposure_count_field['field_name'] % exposure if raster.crs().authid() != vector.crs().authid(): output_layer.startEditing() field = create_field_from_definition( exposure_count_field, exposure) output_layer.addAttribute(field) new_index = output_layer.fields().lookupField(field.name()) old_index = layer.fields().lookupField('exposure_sum') for feature_input, feature_output in zip( layer.getFeatures(), output_layer.getFeatures()): output_layer.changeAttributeValue( feature_input.id(), new_index, feature_input[old_index]) output_layer.commitChanges() layer = output_layer else: fields_to_rename = { 'exposure_sum': output_field } if qgis_version() >= 21600: rename_fields(layer, fields_to_rename) else: copy_fields(layer, fields_to_rename) remove_fields(layer, list(fields_to_rename.keys())) layer.commitChanges() # The zonal stats is producing some None values. We need to fill these # with 0. See issue : #3778 # We should start a new editing session as previous fields need to be # committed first. layer.startEditing() request = QgsFeatureRequest() expression = '\"%s\" is None' % output_field request.setFilterExpression(expression) request.setFlags(QgsFeatureRequest.NoGeometry) index = layer.fields().lookupField(output_field) for feature in layer.getFeatures(): if feature[output_field] is None: layer.changeAttributeValue(feature.id(), index, 0) layer.commitChanges() layer.keywords = raster.keywords.copy() layer.keywords['inasafe_fields'] = vector.keywords['inasafe_fields'].copy() layer.keywords['inasafe_default_values'] = ( raster.keywords['inasafe_default_values'].copy()) key = exposure_count_field['key'] % raster.keywords['exposure'] # Special case here, one field is the exposure count and the total. layer.keywords['inasafe_fields'][key] = output_field layer.keywords['inasafe_fields'][total_field['key']] = output_field layer.keywords['exposure_keywords'] = raster.keywords.copy() layer.keywords['hazard_keywords'] = vector.keywords[ 'hazard_keywords'].copy() layer.keywords['aggregation_keywords'] = ( vector.keywords['aggregation_keywords']) layer.keywords['layer_purpose'] = ( layer_purpose_aggregate_hazard_impacted['key']) layer.keywords['title'] = output_layer_name check_layer(layer) return layer
def aggregate_hazard_summary(impact, aggregate_hazard, callback=None): """Compute the summary from the source layer to the aggregate_hazard layer. Source layer : |exp_id|exp_class|haz_id|haz_class|aggr_id|aggr_name|affected|extra*| Target layer : | aggr_id | aggr_name | haz_id | haz_class | extra* | Output layer : |aggr_id| aggr_name|haz_id|haz_class|affected|extra*|count ber exposure*| :param impact: The layer to aggregate vector layer. :type impact: QgsVectorLayer :param aggregate_hazard: The aggregate_hazard vector layer where to write statistics. :type aggregate_hazard: QgsVectorLayer :param callback: A function to all to indicate progress. The function should accept params 'current' (int), 'maximum' (int) and 'step' (str). Defaults to None. :type callback: function :return: The new aggregate_hazard layer with summary. :rtype: QgsVectorLayer .. versionadded:: 4.0 """ output_layer_name = summary_1_aggregate_hazard_steps['output_layer_name'] processing_step = summary_1_aggregate_hazard_steps['step_name'] source_fields = impact.keywords['inasafe_fields'] target_fields = aggregate_hazard.keywords['inasafe_fields'] target_compulsory_fields = [ aggregation_id_field, aggregation_name_field, hazard_id_field, hazard_class_field ] check_inputs(target_compulsory_fields, target_fields) source_compulsory_fields = [ exposure_id_field, exposure_class_field, aggregation_id_field, aggregation_name_field, hazard_id_field, hazard_class_field ] check_inputs(source_compulsory_fields, source_fields) aggregation_id = target_fields[aggregation_id_field['key']] hazard_id = target_fields[hazard_id_field['key']] hazard_class = target_fields[hazard_class_field['key']] exposure_class = source_fields[exposure_class_field['key']] exposure_class_index = impact.fieldNameIndex(exposure_class) unique_exposure = impact.uniqueValues(exposure_class_index) fields = ['aggregation_id', 'hazard_id'] absolute_values = create_absolute_values_structure(impact, fields) # We need to know what kind of exposure we are going to count. # the size, or the number of features or population. field_index = report_on_field(impact) aggregate_hazard.startEditing() shift = aggregate_hazard.fields().count() add_fields( aggregate_hazard, absolute_values, [affected_field, total_field], unique_exposure, exposure_count_field ) flat_table = FlatTable('aggregation_id', 'hazard_id', 'exposure_class') request = QgsFeatureRequest() request.setFlags(QgsFeatureRequest.NoGeometry) LOGGER.debug('Computing the aggregate hazard summary.') for feature in impact.getFeatures(request): # Field_index can be equal to 0. if field_index is not None: value = feature[field_index] else: value = 1 aggregation_value = feature[aggregation_id] hazard_value = feature[hazard_id] if not hazard_value or isinstance(hazard_value, QPyNullVariant): hazard_value = not_exposed_class['key'] exposure_value = feature[exposure_class] if not exposure_value or isinstance(exposure_value, QPyNullVariant): exposure_value = 'NULL' flat_table.add_value( value, aggregation_id=aggregation_value, hazard_id=hazard_value, exposure_class=exposure_value ) # We summarize every absolute values. for field, field_definition in absolute_values.iteritems(): value = feature[field] if not value or isinstance(value, QPyNullVariant): value = 0 field_definition[0].add_value( value, aggregation_id=aggregation_value, hazard_id=hazard_value ) hazard_keywords = aggregate_hazard.keywords['hazard_keywords'] classification = hazard_keywords['classification'] for area in aggregate_hazard.getFeatures(request): aggregation_value = area[aggregation_id] feature_hazard_id = area[hazard_id] if not feature_hazard_id or isinstance( feature_hazard_id, QPyNullVariant): feature_hazard_id = not_exposed_class['key'] feature_hazard_value = area[hazard_class] total = 0 for i, val in enumerate(unique_exposure): sum = flat_table.get_value( aggregation_id=aggregation_value, hazard_id=feature_hazard_id, exposure_class=val ) total += sum aggregate_hazard.changeAttributeValue(area.id(), shift + i, sum) affected = post_processor_affected_function( classification=classification, hazard_class=feature_hazard_value) affected = tr(unicode(affected)) aggregate_hazard.changeAttributeValue( area.id(), shift + len(unique_exposure), affected) aggregate_hazard.changeAttributeValue( area.id(), shift + len(unique_exposure) + 1, total) for i, field in enumerate(absolute_values.itervalues()): value = field[0].get_value( aggregation_id=aggregation_value, hazard_id=feature_hazard_id ) aggregate_hazard.changeAttributeValue( area.id(), shift + len(unique_exposure) + 2 + i, value) aggregate_hazard.commitChanges() aggregate_hazard.keywords['title'] = ( layer_purpose_aggregate_hazard_impacted['name']) if qgis_version() >= 21800: aggregate_hazard.setName(aggregate_hazard.keywords['title']) else: aggregate_hazard.setLayerName(aggregate_hazard.keywords['title']) aggregate_hazard.keywords['layer_purpose'] = ( layer_purpose_aggregate_hazard_impacted['key']) check_layer(aggregate_hazard) return aggregate_hazard
def reclassify(layer, exposure_key=None, overwrite_input=False, callback=None): """Reclassify a continuous raster layer. Issue https://github.com/inasafe/inasafe/issues/3182 This function is a wrapper for the code from https://github.com/chiatt/gdal_reclassify For instance if you want to reclassify like this table : Original Value | Class - ∞ < val <= 0 | 1 0 < val <= 0.5 | 2 0.5 < val <= 5 | 3 5 < val < + ∞ | 6 You need a dictionary : ranges = OrderedDict() ranges[1] = [None, 0] ranges[2] = [0.0, 0.5] ranges[3] = [0.5, 5] ranges[6] = [5, None] :param layer: The raster layer. :type layer: QgsRasterLayer :param overwrite_input: Option for the output layer. True will overwrite the input layer. False will create a temporary layer. :type overwrite_input: bool :param exposure_key: The exposure key. :type exposure_key: str :param callback: A function to all to indicate progress. The function should accept params 'current' (int), 'maximum' (int) and 'step' (str). Defaults to None. :type callback: function :return: The classified raster layer. :rtype: QgsRasterLayer .. versionadded:: 4.0 """ output_layer_name = reclassify_raster_steps['output_layer_name'] processing_step = reclassify_raster_steps['step_name'] output_layer_name = output_layer_name % layer.keywords['layer_purpose'] if exposure_key: classification_key = active_classification( layer.keywords, exposure_key) thresholds = active_thresholds_value_maps(layer.keywords, exposure_key) layer.keywords['thresholds'] = thresholds layer.keywords['classification'] = classification_key else: classification_key = layer.keywords.get('classification') thresholds = layer.keywords.get('thresholds') if not thresholds: raise InvalidKeywordsForProcessingAlgorithm( 'thresholds are missing from the layer %s' % layer.keywords['layer_purpose']) if not classification_key: raise InvalidKeywordsForProcessingAlgorithm( 'classification is missing from the layer %s' % layer.keywords['layer_purpose']) ranges = {} value_map = {} hazard_classes = definition(classification_key)['classes'] for hazard_class in hazard_classes: ranges[hazard_class['value']] = thresholds[hazard_class['key']] value_map[hazard_class['key']] = [hazard_class['value']] if overwrite_input: output_raster = layer.source() else: output_raster = unique_filename(suffix='.tiff', dir=temp_dir()) driver = gdal.GetDriverByName('GTiff') raster_file = gdal.Open(layer.source()) band = raster_file.GetRasterBand(1) no_data = band.GetNoDataValue() source = band.ReadAsArray() destination = source.copy() for value, interval in ranges.iteritems(): v_min = interval[0] v_max = interval[1] if v_min is None: destination[np.where(source <= v_max)] = value if v_max is None: destination[np.where(source > v_min)] = value if v_min < v_max: destination[np.where((v_min < source) & (source <= v_max))] = value # Tag no data cells destination[np.where(source == no_data)] = no_data_value # Create the new file. output_file = driver.Create( output_raster, raster_file.RasterXSize, raster_file.RasterYSize, 1) output_file.GetRasterBand(1).WriteArray(destination) output_file.GetRasterBand(1).SetNoDataValue(no_data_value) # CRS output_file.SetProjection(raster_file.GetProjection()) output_file.SetGeoTransform(raster_file.GetGeoTransform()) output_file.FlushCache() del output_file if not isfile(output_raster): raise FileNotFoundError reclassified = QgsRasterLayer(output_raster, output_layer_name) # We transfer keywords to the output. reclassified.keywords = layer.keywords.copy() reclassified.keywords['layer_mode'] = 'classified' value_map = {} hazard_classes = definition(classification_key)['classes'] for hazard_class in reversed(hazard_classes): value_map[hazard_class['key']] = [hazard_class['value']] reclassified.keywords['value_map'] = value_map reclassified.keywords['title'] = output_layer_name check_layer(reclassified) return reclassified
def polygonize(layer, callback=None): """Polygonize a raster layer into a vector layer using GDAL. Issue https://github.com/inasafe/inasafe/issues/3183 :param layer: The layer to reproject. :type layer: QgsRasterLayer :param callback: A function to all to indicate progress. The function should accept params 'current' (int) and 'maximum' (int). Defaults to None. :type callback: function :return: Reprojected memory layer. :rtype: QgsRasterLayer .. versionadded:: 4.0 """ output_layer_name = polygonize_steps['output_layer_name'] processing_step = polygonize_steps['step_name'] output_layer_name = output_layer_name % layer.keywords['layer_purpose'] gdal_layer_name = polygonize_steps['gdal_layer_name'] if layer.keywords.get('layer_purpose') == 'exposure': output_field = exposure_type_field else: output_field = hazard_value_field input_raster = gdal.Open(layer.source(), gdal.GA_ReadOnly) srs = osr.SpatialReference() srs.ImportFromWkt(input_raster.GetProjectionRef()) temporary_dir = temp_dir(sub_dir='pre-process') out_shapefile = unique_filename( suffix='-%s.shp' % output_layer_name, dir=temporary_dir) driver = ogr.GetDriverByName("ESRI Shapefile") destination = driver.CreateDataSource(out_shapefile) output_layer = destination.CreateLayer(gdal_layer_name, srs) # We have no other way to use a shapefile. We need only the first 10 chars. field_name = output_field['field_name'][0:10] fd = ogr.FieldDefn(field_name, ogr.OFTInteger) output_layer.CreateField(fd) input_band = input_raster.GetRasterBand(1) # Fixme : add our own callback to Polygonize gdal.Polygonize(input_band, None, output_layer, 0, [], callback=None) destination.Destroy() vector_layer = QgsVectorLayer(out_shapefile, output_layer_name, 'ogr') # Let's remove polygons which were no data request = QgsFeatureRequest() expression = '"%s" = %s' % (field_name, no_data_value) request.setFilterExpression(expression) vector_layer.startEditing() for feature in vector_layer.getFeatures(request): vector_layer.deleteFeature(feature.id()) vector_layer.commitChanges() # We transfer keywords to the output. vector_layer.keywords = layer.keywords.copy() vector_layer.keywords[ layer_geometry['key']] = layer_geometry_polygon['key'] vector_layer.keywords['title'] = output_layer_name # We just polygonized the raster layer. inasafe_fields do not exist. vector_layer.keywords['inasafe_fields'] = { output_field['key']: field_name } check_layer(vector_layer) return vector_layer
def multi_buffering(layer, radii, callback=None): """Buffer a vector layer using many buffers (for volcanoes or rivers). This processing algorithm will keep the original attribute table and will add a new one for the hazard class name according to safe.definitions.fields.hazard_value_field. radii = OrderedDict() radii[500] = 'high' radii[1000] = 'medium' radii[2000] = 'low' Issue https://github.com/inasafe/inasafe/issues/3185 :param layer: The layer to polygonize. :type layer: QgsVectorLayer :param radii: A dictionary of radius. :type radii: OrderedDict :param callback: A function to all to indicate progress. The function should accept params 'current' (int), 'maximum' (int) and 'step' (str). Defaults to None. :type callback: function :return: The buffered vector layer. :rtype: QgsVectorLayer """ # Layer output output_layer_name = buffer_steps['output_layer_name'] processing_step = buffer_steps['step_name'] input_crs = layer.crs() feature_count = layer.featureCount() fields = layer.fields() # Set the new hazard class field. new_field = create_field_from_definition(hazard_class_field) fields.append(new_field) # Set the new buffer distances field. new_field = create_field_from_definition(buffer_distance_field) fields.append(new_field) buffered = create_memory_layer( output_layer_name, QGis.Polygon, input_crs, fields) data_provider = buffered.dataProvider() # Reproject features if needed into UTM if the layer is in 4326. if layer.crs().authid() == 'EPSG:4326': center = layer.extent().center() utm = QgsCoordinateReferenceSystem( get_utm_epsg(center.x(), center.y(), input_crs)) transform = QgsCoordinateTransform(layer.crs(), utm) reverse_transform = QgsCoordinateTransform(utm, layer.crs()) else: transform = None reverse_transform = None for i, feature in enumerate(layer.getFeatures()): geom = QgsGeometry(feature.geometry()) if transform: geom.transform(transform) inner_ring = None for radius in radii: attributes = feature.attributes() # We add the hazard value name to the attribute table. attributes.append(radii[radius]) # We add the value of buffer distance to the attribute table. attributes.append(radius) circle = geom.buffer(radius, 30) if inner_ring: circle.addRing(inner_ring) inner_ring = circle.asPolygon()[0] new_feature = QgsFeature() if reverse_transform: circle.transform(reverse_transform) new_feature.setGeometry(circle) new_feature.setAttributes(attributes) data_provider.addFeatures([new_feature]) if callback: callback(current=i, maximum=feature_count, step=processing_step) # We transfer keywords to the output. buffered.keywords = layer.keywords buffered.keywords['layer_geometry'] = 'polygon' buffered.keywords['layer_purpose'] = layer_purpose_hazard['key'] buffered.keywords['inasafe_fields'][hazard_class_field['key']] = ( hazard_class_field['field_name']) check_layer(buffered) return buffered
def analysis_summary(aggregate_hazard, analysis): """Compute the summary from the aggregate hazard to analysis. Source layer : | haz_id | haz_class | aggr_id | aggr_name | total_feature | Target layer : | analysis_name | Output layer : | analysis_name | count_hazard_class | affected_count | total | :param aggregate_hazard: The layer to aggregate vector layer. :type aggregate_hazard: QgsVectorLayer :param analysis: The target vector layer where to write statistics. :type analysis: QgsVectorLayer :return: The new target layer with summary. :rtype: QgsVectorLayer .. versionadded:: 4.0 """ source_fields = aggregate_hazard.keywords['inasafe_fields'] target_fields = analysis.keywords['inasafe_fields'] target_compulsory_fields = [ analysis_name_field, ] check_inputs(target_compulsory_fields, target_fields) source_compulsory_fields = [ aggregation_id_field, aggregation_name_field, hazard_id_field, hazard_class_field, total_field ] check_inputs(source_compulsory_fields, source_fields) absolute_values = create_absolute_values_structure( aggregate_hazard, ['all']) hazard_class = source_fields[hazard_class_field['key']] hazard_class_index = aggregate_hazard.fields().lookupField(hazard_class) unique_hazard = list(aggregate_hazard.uniqueValues(hazard_class_index)) hazard_keywords = aggregate_hazard.keywords['hazard_keywords'] hazard = hazard_keywords['hazard'] classification = hazard_keywords['classification'] exposure_keywords = aggregate_hazard.keywords['exposure_keywords'] exposure = exposure_keywords['exposure'] total = source_fields[total_field['key']] flat_table = FlatTable('hazard_class') # First loop over the aggregate_hazard layer request = QgsFeatureRequest() request.setSubsetOfAttributes( [hazard_class, total], aggregate_hazard.fields()) request.setFlags(QgsFeatureRequest.NoGeometry) for area in aggregate_hazard.getFeatures(): hazard_value = area[hazard_class_index] value = area[total] if (value == '' or value is None or isnan(value) or (hasattr(value, 'isNull') and value.isNull())): # For isnan, see ticket #3812 value = 0 if (hazard_value == '' or hazard_value is None or (hasattr(hazard_value, 'isNull') and hazard_value.isNull())): hazard_value = 'NULL' flat_table.add_value( value, hazard_class=hazard_value ) # We summarize every absolute values. for field, field_definition in list(absolute_values.items()): value = area[field] if (value == '' or value is None or (hasattr(value, 'isNull') and value.isNull())): value = 0 field_definition[0].add_value( value, all='all' ) analysis.startEditing() shift = analysis.fields().count() counts = [ total_affected_field, total_not_affected_field, total_exposed_field, total_not_exposed_field, total_field] dynamic_structure = [ [hazard_count_field, unique_hazard], ] add_fields( analysis, absolute_values, counts, dynamic_structure) affected_sum = 0 not_affected_sum = 0 not_exposed_sum = 0 # Summarization summary_values = {} for key, summary_rule in list(summary_rules.items()): input_field = summary_rule['input_field'] case_field = summary_rule['case_field'] if aggregate_hazard.fields().lookupField(input_field['field_name']) \ == -1: continue if aggregate_hazard.fields().lookupField(case_field['field_name']) \ == -1: continue summary_value = 0 for area in aggregate_hazard.getFeatures(): case_value = area[case_field['field_name']] if case_value in summary_rule['case_values']: summary_value += area[input_field['field_name']] summary_values[key] = summary_value for area in analysis.getFeatures(request): total = 0 for i, val in enumerate(unique_hazard): if (val == '' or val is None or (hasattr(val, 'isNull') and val.isNull())): val = 'NULL' sum = flat_table.get_value(hazard_class=val) total += sum analysis.changeAttributeValue(area.id(), shift + i, sum) affected = post_processor_affected_function( exposure=exposure, hazard=hazard, classification=classification, hazard_class=val) if affected == not_exposed_class['key']: not_exposed_sum += sum elif affected: affected_sum += sum else: not_affected_sum += sum # Total Affected field analysis.changeAttributeValue( area.id(), shift + len(unique_hazard), affected_sum) # Total Not affected field analysis.changeAttributeValue( area.id(), shift + len(unique_hazard) + 1, not_affected_sum) # Total Exposed field analysis.changeAttributeValue( area.id(), shift + len(unique_hazard) + 2, total - not_exposed_sum) # Total Not exposed field analysis.changeAttributeValue( area.id(), shift + len(unique_hazard) + 3, not_exposed_sum) # Total field analysis.changeAttributeValue( area.id(), shift + len(unique_hazard) + 4, total) # Any absolute postprocessors for i, field in enumerate(absolute_values.values()): value = field[0].get_value( all='all' ) analysis.changeAttributeValue( area.id(), shift + len(unique_hazard) + 5 + i, value) # Summarizer of custom attributes for key, summary_value in list(summary_values.items()): summary_field = summary_rules[key]['summary_field'] field = create_field_from_definition(summary_field) analysis.addAttribute(field) field_index = analysis.fields().lookupField(field.name()) # noinspection PyTypeChecker analysis.keywords['inasafe_fields'][summary_field['key']] = ( summary_field['field_name']) analysis.changeAttributeValue( area.id(), field_index, summary_value) # Sanity check ± 1 to the result. Disabled for now as it seems ± 1 is not # enough. ET 13/02/17 # total_computed = ( # affected_sum + not_affected_sum + not_exposed_sum) # if not -1 < (total_computed - total) < 1: # raise ComputationError analysis.commitChanges() analysis.keywords['title'] = layer_purpose_analysis_impacted['name'] if qgis_version() >= 21600: analysis.setName(analysis.keywords['title']) else: analysis.setLayerName(analysis.keywords['title']) analysis.keywords['layer_purpose'] = layer_purpose_analysis_impacted['key'] check_layer(analysis) return analysis
def clip_by_extent(layer, extent, callback=None): """Clip a raster using a bounding box using processing. Issue https://github.com/inasafe/inasafe/issues/3183 :param layer: The layer to reproject. :type layer: QgsRasterLayer :param extent: The extent. :type extent: QgsRectangle :param callback: A function to all to indicate progress. The function should accept params 'current' (int) and 'maximum' (int). Defaults to None. :type callback: function :return: Reprojected memory layer. :rtype: QgsRasterLayer .. versionadded:: 4.0 """ parameters = dict() # noinspection PyBroadException try: output_layer_name = quick_clip_steps['output_layer_name'] processing_step = quick_clip_steps['step_name'] output_layer_name = output_layer_name % layer.keywords['layer_purpose'] output_raster = unique_filename(dir=temp_dir()) # We make one pixel size buffer on the extent to cover every pixels. # See https://github.com/inasafe/inasafe/issues/3655 pixel_size_x = layer.rasterUnitsPerPixelX() pixel_size_y = layer.rasterUnitsPerPixelY() buffer_size = max(pixel_size_x, pixel_size_y) extent = extent.buffer(buffer_size) if is_raster_y_inverted(layer): # The raster is Y inverted. We need to switch Y min and Y max. bbox = [ str(extent.xMinimum()), str(extent.xMaximum()), str(extent.yMaximum()), str(extent.yMinimum()) ] else: # The raster is normal. bbox = [ str(extent.xMinimum()), str(extent.xMaximum()), str(extent.yMinimum()), str(extent.yMaximum()) ] # These values are all from the processing algorithm. # https://github.com/qgis/QGIS/blob/master/python/plugins/processing/ # algs/gdal/ClipByExtent.py # Please read the file to know these parameters. parameters['INPUT'] = layer.source() parameters['NO_DATA'] = '' parameters['PROJWIN'] = ','.join(bbox) parameters['RTYPE'] = 5 parameters['COMPRESS'] = 4 parameters['JPEGCOMPRESSION'] = 75 parameters['ZLEVEL'] = 6 parameters['PREDICTOR'] = 1 parameters['TILED'] = False parameters['BIGTIFF'] = 0 parameters['TFW'] = False parameters['EXTRA'] = '' parameters['OUTPUT'] = output_raster result = processing.runalg("gdalogr:cliprasterbyextent", parameters) if result is None: raise ProcessingInstallationError clipped = QgsRasterLayer(result['OUTPUT'], output_layer_name) # We transfer keywords to the output. clipped.keywords = layer.keywords.copy() clipped.keywords['title'] = output_layer_name check_layer(clipped) except Exception as e: # This step clip_raster_by_extent was nice to speedup the analysis. # As we got an exception because the layer is invalid, we are not going # to stop the analysis. We will return the original raster layer. # It will take more processing time until we clip the vector layer. # Check https://github.com/inasafe/inasafe/issues/4026 why we got some # exceptions with this step. LOGGER.exception(parameters) LOGGER.exception( 'Error from QGIS clip raster by extent. Please check the QGIS ' 'logs too !') LOGGER.info( 'Even if we got an exception, we are continuing the analysis. The ' 'layer is not clip.') LOGGER.exception(str(e)) LOGGER.exception(get_error_message(e).to_text()) clipped = layer return clipped
def zonal_stats(raster, vector, callback=None): """Reclassify a continuous raster layer. Issue https://github.com/inasafe/inasafe/issues/3190 :param raster: The raster layer. :type raster: QgsRasterLayer :param vector: The vector layer. :type vector: QgsVectorLayer :param callback: A function to all to indicate progress. The function should accept params 'current' (int), 'maximum' (int) and 'step' (str). Defaults to None. :type callback: function :return: The output of the zonal stats. :rtype: QgsVectorLayer .. versionadded:: 4.0 """ output_layer_name = zonal_stats_steps['output_layer_name'] processing_step = zonal_stats_steps['step_name'] layer = create_memory_layer( output_layer_name, vector.geometryType(), vector.crs(), vector.fields() ) copy_layer(vector, layer) analysis = QgsZonalStatistics( layer, raster.source(), 'exposure_', 1, QgsZonalStatistics.Sum) result = analysis.calculateStatistics(None) LOGGER.debug(tr('Zonal stats on %s : %s' % (raster.source(), result))) layer.startEditing() exposure = raster.keywords['exposure'] output_field = exposure_count_field['field_name'] % exposure fields_to_rename = { 'exposure_sum': output_field } copy_fields(layer, fields_to_rename) remove_fields(layer, fields_to_rename.keys()) layer.commitChanges() # The zonal stats is producing some None values. We need to fill these # with 0. See issue : #3778 # We should start a new editing session as previous fields need to be # commited first. layer.startEditing() request = QgsFeatureRequest() expression = '\"%s\" is None' % output_field request.setFilterExpression(expression) request.setFlags(QgsFeatureRequest.NoGeometry) index = layer.fieldNameIndex(output_field) for feature in layer.getFeatures(): if feature[output_field] is None: layer.changeAttributeValue(feature.id(), index, 0) layer.commitChanges() layer.keywords = raster.keywords.copy() layer.keywords['inasafe_fields'] = vector.keywords['inasafe_fields'].copy() layer.keywords['inasafe_default_values'] = ( raster.keywords['inasafe_default_values'].copy()) key = exposure_count_field['key'] % raster.keywords['exposure'] # Special case here, one field is the exposure count and the total. layer.keywords['inasafe_fields'][key] = output_field layer.keywords['inasafe_fields'][total_field['key']] = output_field layer.keywords['exposure_keywords'] = raster.keywords.copy() layer.keywords['hazard_keywords'] = vector.keywords[ 'hazard_keywords'].copy() layer.keywords['aggregation_keywords'] = ( vector.keywords['aggregation_keywords']) layer.keywords['layer_purpose'] = ( layer_purpose_aggregate_hazard_impacted['key']) layer.keywords['title'] = output_layer_name check_layer(layer) return layer
def reclassify(layer, exposure_key=None, callback=None): """Reclassify a continuous vector layer. This function will modify the input. For instance if you want to reclassify like this table : Original Value | Class - ∞ < val <= 0 | 1 0 < val <= 0.5 | 2 0.5 < val <= 5 | 3 5 < val < + ∞ | 6 You need a dictionary : ranges = OrderedDict() ranges[1] = [None, 0] ranges[2] = [0.0, 0.5] ranges[3] = [0.5, 5] ranges[6] = [5, None] :param layer: The raster layer. :type layer: QgsRasterLayer :param exposure_key: The exposure key. :type exposure_key: str :param callback: A function to all to indicate progress. The function should accept params 'current' (int), 'maximum' (int) and 'step' (str). Defaults to None. :type callback: function :return: The classified vector layer. :rtype: QgsVectorLayer .. versionadded:: 4.0 """ output_layer_name = reclassify_vector_steps['output_layer_name'] output_layer_name = output_layer_name % layer.keywords['title'] processing_step = reclassify_vector_steps['step_name'] # This layer should have this keyword, or it's a mistake from the dev. inasafe_fields = layer.keywords['inasafe_fields'] continuous_column = inasafe_fields[hazard_value_field['key']] if exposure_key: classification_key = active_classification(layer.keywords, exposure_key) thresholds = active_thresholds_value_maps(layer.keywords, exposure_key) layer.keywords['thresholds'] = thresholds layer.keywords['classification'] = classification_key else: classification_key = layer.keywords.get('classification') thresholds = layer.keywords.get('thresholds') if not thresholds: raise InvalidKeywordsForProcessingAlgorithm( 'thresholds are missing from the layer %s' % layer.keywords['layer_purpose']) continuous_index = layer.fieldNameIndex(continuous_column) classified_field = QgsField() classified_field.setType(hazard_class_field['type']) classified_field.setName(hazard_class_field['field_name']) classified_field.setLength(hazard_class_field['length']) classified_field.setPrecision(hazard_class_field['precision']) layer.startEditing() layer.addAttribute(classified_field) classified_field_index = layer.fieldNameIndex(classified_field.name()) for feature in layer.getFeatures(): attributes = feature.attributes() source_value = attributes[continuous_index] classified_value = _classified_value(source_value, thresholds) if not classified_value: layer.deleteFeature(feature.id()) else: layer.changeAttributeValue(feature.id(), classified_field_index, classified_value) layer.commitChanges() layer.updateFields() # We transfer keywords to the output. inasafe_fields[hazard_class_field['key']] = ( hazard_class_field['field_name']) value_map = {} hazard_classes = definition(classification_key)['classes'] for hazard_class in reversed(hazard_classes): value_map[hazard_class['key']] = [hazard_class['value']] layer.keywords['value_map'] = value_map layer.keywords['title'] = output_layer_name check_layer(layer) return layer
def assign_highest_value(exposure, hazard, callback=None): """Assign the highest hazard value to an indivisible feature. For indivisible polygon exposure layers such as buildings, we need to assigned the greatest hazard that each polygon touches and use that as the effective hazard class. Issue https://github.com/inasafe/inasafe/issues/3192 We follow the concept here that any part of the exposure dataset that touches the hazard is affected, and the greatest hazard is the effective hazard. :param exposure: The building vector layer. :type exposure: QgsVectorLayer :param hazard: The vector layer to use for hazard. :type hazard: QgsVectorLayer :param callback: A function to all to indicate progress. The function should accept params 'current' (int), 'maximum' (int) and 'step' (str). Defaults to None. :type callback: function :return: The new impact layer. :rtype: QgsVectorLayer .. versionadded:: 4.0 """ output_layer_name = assign_highest_value_steps['output_layer_name'] processing_step = assign_highest_value_steps['step_name'] hazard_inasafe_fields = hazard.keywords['inasafe_fields'] if not hazard.keywords.get('classification'): raise InvalidKeywordsForProcessingAlgorithm if not hazard_inasafe_fields.get(hazard_class_field['key']): raise InvalidKeywordsForProcessingAlgorithm indices = [] exposure.startEditing() for field in hazard.fields(): exposure.addAttribute(field) indices.append(exposure.fieldNameIndex(field.name())) exposure.commitChanges() provider = exposure.dataProvider() spatial_index = create_spatial_index(exposure) # cache features from exposure layer for faster retrieval exposure_features = {} for f in exposure.getFeatures(): exposure_features[f.id()] = f # Todo callback # total = 100.0 / len(selectionA) hazard_field = hazard_inasafe_fields[hazard_class_field['key']] layer_classification = None for classification in hazard_classification['types']: if classification['key'] == hazard.keywords['classification']: layer_classification = classification break # Get a ordered list of classes like ['high', 'medium', 'low'] levels = [key['key'] for key in layer_classification['classes']] levels.append(not_exposed_class['key']) # Let's loop over the hazard layer, from high to low hazard zone. for hazard_value in levels: expression = '"%s" = \'%s\'' % (hazard_field, hazard_value) hazard_request = QgsFeatureRequest().setFilterExpression(expression) update_map = {} for area in hazard.getFeatures(hazard_request): geometry = area.geometry() intersects = spatial_index.intersects(geometry.boundingBox()) # use prepared geometry: makes multiple intersection tests faster geometry_prepared = QgsGeometry.createGeometryEngine( geometry.geometry()) geometry_prepared.prepareGeometry() # We need to loop over each intersections exposure / hazard. for i in intersects: building = exposure_features[i] building_geometry = building.geometry() if geometry_prepared.intersects(building_geometry.geometry()): update_map[building.id()] = {} for index, value in zip(indices, area.attributes()): update_map[building.id()][index] = value # We don't want this building again, let's remove it from # the index. spatial_index.deleteFeature(building) provider.changeAttributeValues(update_map) exposure.updateExtents() exposure.updateFields() exposure.keywords['inasafe_fields'].update( hazard.keywords['inasafe_fields']) exposure.keywords['layer_purpose'] = layer_purpose_exposure_summary['key'] exposure.keywords['exposure_keywords'] = exposure.keywords.copy() exposure.keywords['aggregation_keywords'] = ( hazard.keywords['aggregation_keywords'].copy()) exposure.keywords['hazard_keywords'] = ( hazard.keywords['hazard_keywords'].copy()) exposure.keywords['title'] = output_layer_name check_layer(exposure) return exposure
def reclassify(layer, exposure_key=None): """Reclassify a continuous vector layer. This function will modify the input. For instance if you want to reclassify like this table : Original Value | Class - ∞ < val <= 0 | 1 0 < val <= 0.5 | 2 0.5 < val <= 5 | 3 5 < val < + ∞ | 6 You need a dictionary : ranges = OrderedDict() ranges[1] = [None, 0] ranges[2] = [0.0, 0.5] ranges[3] = [0.5, 5] ranges[6] = [5, None] :param layer: The raster layer. :type layer: QgsRasterLayer :param exposure_key: The exposure key. :type exposure_key: str :return: The classified vector layer. :rtype: QgsVectorLayer .. versionadded:: 4.0 """ output_layer_name = reclassify_vector_steps['output_layer_name'] output_layer_name = output_layer_name % layer.keywords['title'] # This layer should have this keyword, or it's a mistake from the dev. inasafe_fields = layer.keywords['inasafe_fields'] continuous_column = inasafe_fields[hazard_value_field['key']] if exposure_key: classification_key = active_classification( layer.keywords, exposure_key) thresholds = active_thresholds_value_maps(layer.keywords, exposure_key) layer.keywords['thresholds'] = thresholds layer.keywords['classification'] = classification_key else: classification_key = layer.keywords.get('classification') thresholds = layer.keywords.get('thresholds') if not thresholds: raise InvalidKeywordsForProcessingAlgorithm( 'thresholds are missing from the layer %s' % layer.keywords['layer_purpose']) continuous_index = layer.fields().lookupField(continuous_column) classified_field = QgsField() classified_field.setType(hazard_class_field['type']) classified_field.setName(hazard_class_field['field_name']) classified_field.setLength(hazard_class_field['length']) classified_field.setPrecision(hazard_class_field['precision']) layer.startEditing() layer.addAttribute(classified_field) classified_field_index = layer.fields(). \ lookupField(classified_field.name()) for feature in layer.getFeatures(): attributes = feature.attributes() source_value = attributes[continuous_index] classified_value = reclassify_value(source_value, thresholds) if (classified_value is None or (hasattr(classified_value, 'isNull') and classified_value.isNull())): layer.deleteFeature(feature.id()) else: layer.changeAttributeValue( feature.id(), classified_field_index, classified_value) layer.commitChanges() layer.updateFields() # We transfer keywords to the output. inasafe_fields[hazard_class_field['key']] = ( hazard_class_field['field_name']) value_map = {} hazard_classes = definition(classification_key)['classes'] for hazard_class in reversed(hazard_classes): value_map[hazard_class['key']] = [hazard_class['value']] layer.keywords['value_map'] = value_map layer.keywords['title'] = output_layer_name check_layer(layer) return layer
def aggregation_summary(aggregate_hazard, aggregation, callback=None): """Compute the summary from the aggregate hazard to the analysis layer. Source layer : | haz_id | haz_class | aggr_id | aggr_name | total_feature | Target layer : | aggr_id | aggr_name | Output layer : | aggr_id | aggr_name | count of affected features per exposure type :param aggregate_hazard: The layer to aggregate vector layer. :type aggregate_hazard: QgsVectorLayer :param aggregation: The aggregation vector layer where to write statistics. :type aggregation: QgsVectorLayer :param callback: A function to all to indicate progress. The function should accept params 'current' (int), 'maximum' (int) and 'step' (str). Defaults to None. :type callback: function :return: The new aggregation layer with summary. :rtype: QgsVectorLayer .. versionadded:: 4.0 """ output_layer_name = summary_2_aggregation_steps['output_layer_name'] processing_step = summary_2_aggregation_steps['step_name'] source_fields = aggregate_hazard.keywords['inasafe_fields'] target_fields = aggregation.keywords['inasafe_fields'] target_compulsory_fields = [ aggregation_id_field, aggregation_name_field, ] check_inputs(target_compulsory_fields, target_fields) # Missing exposure_count_field source_compulsory_fields = [ aggregation_id_field, aggregation_name_field, hazard_id_field, hazard_class_field, affected_field, ] check_inputs(source_compulsory_fields, source_fields) pattern = exposure_count_field['key'] pattern = pattern.replace('%s', '') unique_exposure = read_dynamic_inasafe_field(source_fields, exposure_count_field) absolute_values = create_absolute_values_structure(aggregate_hazard, ['aggregation_id']) flat_table = FlatTable('aggregation_id', 'exposure_class') aggregation_index = source_fields[aggregation_id_field['key']] # We want to loop over affected features only. request = QgsFeatureRequest() request.setFlags(QgsFeatureRequest.NoGeometry) expression = '\"%s\" = \'%s\'' % (affected_field['field_name'], tr('True')) request.setFilterExpression(expression) for area in aggregate_hazard.getFeatures(request): for key, name_field in source_fields.iteritems(): if key.endswith(pattern): aggregation_id = area[aggregation_index] exposure_class = key.replace(pattern, '') value = area[name_field] flat_table.add_value(value, aggregation_id=aggregation_id, exposure_class=exposure_class) # We summarize every absolute values. for field, field_definition in absolute_values.iteritems(): value = area[field] if not value or isinstance(value, QPyNullVariant): value = 0 field_definition[0].add_value( value, aggregation_id=area[aggregation_index], ) shift = aggregation.fields().count() aggregation.startEditing() add_fields(aggregation, absolute_values, [total_affected_field], unique_exposure, affected_exposure_count_field) aggregation_index = target_fields[aggregation_id_field['key']] request = QgsFeatureRequest() request.setFlags(QgsFeatureRequest.NoGeometry) for area in aggregation.getFeatures(request): aggregation_value = area[aggregation_index] total = 0 for i, val in enumerate(unique_exposure): sum = flat_table.get_value(aggregation_id=aggregation_value, exposure_class=val) total += sum aggregation.changeAttributeValue(area.id(), shift + i, sum) aggregation.changeAttributeValue(area.id(), shift + len(unique_exposure), total) for i, field in enumerate(absolute_values.itervalues()): value = field[0].get_value(aggregation_id=aggregation_value, ) target_index = shift + len(unique_exposure) + 1 + i aggregation.changeAttributeValue(area.id(), target_index, value) aggregation.commitChanges() aggregation.keywords['title'] = layer_purpose_aggregation_summary['name'] if qgis_version() >= 21800: aggregation.setName(aggregation.keywords['title']) else: aggregation.setLayerName(aggregation.keywords['title']) aggregation.keywords['layer_purpose'] = ( layer_purpose_aggregation_summary['key']) check_layer(aggregation) return aggregation
def from_counts_to_ratios(layer): """Transform counts to ratios. Formula: ratio = subset count / total count :param layer: The vector layer. :type layer: QgsVectorLayer :return: The layer with new ratios. :rtype: QgsVectorLayer .. versionadded:: 4.0 """ output_layer_name = recompute_counts_steps['output_layer_name'] exposure = definition(layer.keywords['exposure']) inasafe_fields = layer.keywords['inasafe_fields'] layer.keywords['title'] = output_layer_name if not population_count_field['key'] in inasafe_fields: # There is not a population count field. Let's skip this layer. LOGGER.info( 'Population count field {population_count_field} is not detected ' 'in the exposure. We will not compute a ratio from this field ' 'because the formula needs Population count field. Formula: ' 'ratio = subset count / total count.'.format( population_count_field=population_count_field['key'])) return layer layer.startEditing() mapping = {} non_compulsory_fields = get_non_compulsory_fields( layer_purpose_exposure['key'], exposure['key']) for count_field in non_compulsory_fields: exists = count_field['key'] in inasafe_fields if count_field['key'] in list(count_ratio_mapping.keys()) and exists: ratio_field = definition(count_ratio_mapping[count_field['key']]) field = create_field_from_definition(ratio_field) layer.addAttribute(field) name = ratio_field['field_name'] layer.keywords['inasafe_fields'][ratio_field['key']] = name mapping[count_field['field_name'] ] = layer.fields().lookupField(name) LOGGER.info( 'Count field {count_field} detected in the exposure, we are ' 'going to create a equivalent field {ratio_field} in the ' 'exposure layer.'.format( count_field=count_field['key'], ratio_field=ratio_field['key'])) else: LOGGER.info( 'Count field {count_field} not detected in the exposure. We ' 'will not compute a ratio from this field.'.format( count_field=count_field['key'])) if len(mapping) == 0: # There is not a subset count field. Let's skip this layer. layer.commitChanges() return layer for feature in layer.getFeatures(): total_count = feature[inasafe_fields[population_count_field['key']]] for count_field, index in list(mapping.items()): count = feature[count_field] try: # For #4669, fix always get 0 new_value = count / float(total_count) except TypeError: new_value = '' except ZeroDivisionError: new_value = 0 layer.changeAttributeValue(feature.id(), index, new_value) layer.commitChanges() check_layer(layer) return layer
def polygonize(layer): """Polygonize a raster layer into a vector layer using GDAL. Issue https://github.com/inasafe/inasafe/issues/3183 :param layer: The layer to reproject. :type layer: QgsRasterLayer :return: Reprojected memory layer. :rtype: QgsRasterLayer .. versionadded:: 4.0 """ output_layer_name = polygonize_steps['output_layer_name'] output_layer_name = output_layer_name % layer.keywords['layer_purpose'] gdal_layer_name = polygonize_steps['gdal_layer_name'] if layer.keywords.get('layer_purpose') == 'exposure': output_field = exposure_type_field else: output_field = hazard_value_field input_raster = gdal.Open(layer.source(), gdal.GA_ReadOnly) srs = osr.SpatialReference() srs.ImportFromWkt(input_raster.GetProjectionRef()) temporary_dir = temp_dir(sub_dir='pre-process') out_shapefile = unique_filename(suffix='-%s.shp' % output_layer_name, dir=temporary_dir) driver = ogr.GetDriverByName("ESRI Shapefile") destination = driver.CreateDataSource(out_shapefile) output_layer = destination.CreateLayer(gdal_layer_name, srs) # We have no other way to use a shapefile. We need only the first 10 chars. field_name = output_field['field_name'][0:10] fd = ogr.FieldDefn(field_name, ogr.OFTInteger) output_layer.CreateField(fd) active_band = layer.keywords.get('active_band', 1) input_band = input_raster.GetRasterBand(active_band) # Fixme : add our own callback to Polygonize gdal.Polygonize(input_band, None, output_layer, 0, [], callback=None) destination.Destroy() vector_layer = QgsVectorLayer(out_shapefile, output_layer_name, 'ogr') # Let's remove polygons which were no data request = QgsFeatureRequest() expression = '"%s" = %s' % (field_name, no_data_value) request.setFilterExpression(expression) vector_layer.startEditing() for feature in vector_layer.getFeatures(request): vector_layer.deleteFeature(feature.id()) vector_layer.commitChanges() # We transfer keywords to the output. vector_layer.keywords = layer.keywords.copy() vector_layer.keywords[ layer_geometry['key']] = layer_geometry_polygon['key'] vector_layer.keywords['title'] = output_layer_name # We just polygonized the raster layer. inasafe_fields do not exist. vector_layer.keywords['inasafe_fields'] = {output_field['key']: field_name} check_layer(vector_layer) return vector_layer
def assign_highest_value(exposure, hazard): """Assign the highest hazard value to an indivisible feature. For indivisible polygon exposure layers such as buildings, we need to assigned the greatest hazard that each polygon touches and use that as the effective hazard class. Issue https://github.com/inasafe/inasafe/issues/3192 We follow the concept here that any part of the exposure dataset that touches the hazard is affected, and the greatest hazard is the effective hazard. :param exposure: The building vector layer. :type exposure: QgsVectorLayer :param hazard: The vector layer to use for hazard. :type hazard: QgsVectorLayer :return: The new impact layer. :rtype: QgsVectorLayer .. versionadded:: 4.0 """ output_layer_name = assign_highest_value_steps['output_layer_name'] hazard_inasafe_fields = hazard.keywords['inasafe_fields'] haz_id_field = hazard_inasafe_fields[hazard_id_field['key']] try: aggr_id_field = hazard_inasafe_fields[aggregation_id_field['key']] except AttributeError: aggr_id_field = None if not hazard.keywords.get('classification'): raise InvalidKeywordsForProcessingAlgorithm if not hazard_inasafe_fields.get(hazard_class_field['key']): raise InvalidKeywordsForProcessingAlgorithm indices = [] exposure.startEditing() for field in hazard.fields(): exposure.addAttribute(field) indices.append(exposure.fields().lookupField(field.name())) exposure.commitChanges() provider = exposure.dataProvider() spatial_index = create_spatial_index(exposure) # cache features from exposure layer for faster retrieval exposure_features = {} for f in exposure.getFeatures(): exposure_features[f.id()] = f # Todo callback # total = 100.0 / len(selectionA) hazard_field = hazard_inasafe_fields[hazard_class_field['key']] layer_classification = None for classification in hazard_classification['types']: if classification['key'] == hazard.keywords['classification']: layer_classification = classification break # Get a ordered list of classes like ['high', 'medium', 'low'] levels = [key['key'] for key in layer_classification['classes']] levels.append(not_exposed_class['key']) def _hazard_sort_key(feature): """Custom feature sort function. The function were intended to sort hazard features, in order for maintaining consistencies between subsequent runs. With controlled order, we will have the same output if one hazard spans over multiple aggregation boundaries. """ if aggr_id_field: return (feature[haz_id_field] or feature.id(), feature[aggr_id_field]) return feature[haz_id_field] or feature.id() # Let's loop over the hazard layer, from high to low hazard zone. for hazard_value in levels: expression = '"%s" = \'%s\'' % (hazard_field, hazard_value) order_by_hazard_field = QgsFeatureRequest.OrderByClause(haz_id_field) order_by_aggregation_field = QgsFeatureRequest.OrderByClause( aggr_id_field) order_by = QgsFeatureRequest.OrderBy( [order_by_hazard_field, order_by_aggregation_field]) hazard_request = QgsFeatureRequest()\ .setOrderBy(order_by).setFilterExpression(expression) update_map = {} areas = sorted(hazard.getFeatures(hazard_request), key=_hazard_sort_key) for area in areas: geometry = area.geometry().constGet() intersects = spatial_index.intersects(geometry.boundingBox()) # to force consistencies between subsequent runs, sort the index. # ideally each exposure feature needs to have prioritization # value/score to determine which hazard/aggregation it belongs to, # in case one feature were intersected with one or more high # hazard geometry. sorting the ids works by ignoring this # tendencies but still maintains consistencies for subsequent run. intersects.sort() # use prepared geometry: makes multiple intersection tests faster geometry_prepared = QgsGeometry.createGeometryEngine(geometry) geometry_prepared.prepareGeometry() # We need to loop over each intersections exposure / hazard. for i in intersects: building = exposure_features[i] building_geometry = building.geometry() if geometry_prepared.intersects(building_geometry.constGet()): update_map[building.id()] = {} for index, value in zip(indices, area.attributes()): update_map[building.id()][index] = value # We don't want this building again, let's remove it from # the index. spatial_index.deleteFeature(building) provider.changeAttributeValues(update_map) exposure.updateExtents() exposure.updateFields() exposure.keywords['inasafe_fields'].update( hazard.keywords['inasafe_fields']) exposure.keywords['layer_purpose'] = layer_purpose_exposure_summary['key'] exposure.keywords['exposure_keywords'] = exposure.keywords.copy() exposure.keywords['aggregation_keywords'] = ( hazard.keywords['aggregation_keywords'].copy()) exposure.keywords['hazard_keywords'] = ( hazard.keywords['hazard_keywords'].copy()) exposure.keywords['title'] = output_layer_name check_layer(exposure) return exposure
def rasterize_vector_layer(layer, width, height, extent): """Rasterize a vector layer to the grid given by extent and width/height. :param layer: The vector layer. :type layer: QgsVectorLayer :param width: The width of the output. :type width: int :param height: The height of the output. :type height: int :param extent: The extent to use. :type extent: QgsRectangle :return: The new raster layer. :rtype: QgsRasterLayer """ name = rasterize_steps['gdal_layer_name'] output_filename = unique_filename(prefix=name, suffix='.tif') extent_str = '%f,%f,%f,%f' % (extent.xMinimum(), extent.xMaximum(), extent.yMinimum(), extent.yMaximum()) keywords = dict(layer.keywords) # The layer is in memory, we need to save it to a file for Processing. data_store = Folder(mkdtemp()) data_store.default_vector_format = 'geojson' result = data_store.add_layer(layer, 'vector_layer') layer = data_store.layer(result[1]) assert layer.isValid() field = layer.keywords['inasafe_fields'][aggregation_id_field['key']] # ET 21/02/17. I got some issues using rasterize algorithm from Processing. # I keep it in case of we need it later. Let's use gdal command line. use_gdal_command_line = True if use_gdal_command_line: startupinfo = None if sys.platform == 'win32': # On windows, we don't want to display the bash shell. # https://github.com/inasafe/inasafe/issues/3980 startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW commands = [which('gdal_rasterize')[0]] commands += ['-a', field] commands += ['-ts', str(width), str(height)] commands += ['-ot', 'Int16'] commands += ['-a_nodata', "'-1'"] commands += [layer.source(), output_filename] LOGGER.info(' '.join(commands)) result = subprocess.check_call(commands, startupinfo=startupinfo) LOGGER.info('Result : %s' % result) else: parameters = dict() parameters['INPUT'] = layer parameters['FIELD'] = field parameters['DIMENSIONS'] = 0 # output size is given in pixels parameters['WIDTH'] = width parameters['HEIGHT'] = height parameters['RASTER_EXT'] = extent_str parameters['TFW'] = False # force generation of ESRI TFW parameters['RTYPE'] = 1 # raster type: Int16 parameters['NO_DATA'] = '-1' # nodata value parameters['COMPRESS'] = 4 # GeoTIFF compression: DEFLATE parameters['JPEGCOMPRESSION'] = 75 # JPEG compression level: 75 parameters['ZLEVEL'] = 6 # DEFLATE compression level parameters['PREDICTOR'] = 1 # predictor for JPEG/DEFLATE parameters['TILED'] = False # Tiled GeoTIFF? parameters['BIGTIFF'] = 0 # whether to make big TIFF parameters['EXTRA'] = '' # additional creation parameters parameters['OUTPUT'] = output_filename result = runalg('gdalogr:rasterize', parameters) if result is None: # Let's try be removing a new parameter added between 2.14 and 2.16 del parameters['RASTER_EXT'] result = runalg('gdalogr:rasterize', parameters) assert result is not None layer_aligned = QgsRasterLayer(output_filename, name, 'gdal') assert layer_aligned.isValid() layer_aligned.keywords = keywords layer_aligned.keywords['title'] = (rasterize_steps['output_layer_name'] % 'aggregation') layer_aligned.keywords['layer_purpose'] = ( layer_purpose_aggregation_summary['key']) del layer_aligned.keywords['inasafe_fields'] check_layer(layer_aligned) return layer_aligned
def clip_by_extent(layer, extent): """Clip a raster using a bounding box using processing. Issue https://github.com/inasafe/inasafe/issues/3183 :param layer: The layer to clip. :type layer: QgsRasterLayer :param extent: The extent. :type extent: QgsRectangle :return: Clipped layer. :rtype: QgsRasterLayer .. versionadded:: 4.0 """ parameters = dict() # noinspection PyBroadException try: output_layer_name = quick_clip_steps['output_layer_name'] output_layer_name = output_layer_name % layer.keywords['layer_purpose'] output_raster = unique_filename(suffix='.tif', dir=temp_dir()) # We make one pixel size buffer on the extent to cover every pixels. # See https://github.com/inasafe/inasafe/issues/3655 pixel_size_x = layer.rasterUnitsPerPixelX() pixel_size_y = layer.rasterUnitsPerPixelY() buffer_size = max(pixel_size_x, pixel_size_y) extent = extent.buffered(buffer_size) if is_raster_y_inverted(layer): # The raster is Y inverted. We need to switch Y min and Y max. bbox = [ str(extent.xMinimum()), str(extent.xMaximum()), str(extent.yMaximum()), str(extent.yMinimum()) ] else: # The raster is normal. bbox = [ str(extent.xMinimum()), str(extent.xMaximum()), str(extent.yMinimum()), str(extent.yMaximum()) ] # These values are all from the processing algorithm. # https://github.com/qgis/QGIS/blob/master/python/plugins/processing/ # algs/gdal/ClipByExtent.py # Please read the file to know these parameters. parameters['INPUT'] = layer.source() parameters['NO_DATA'] = '' parameters['PROJWIN'] = ','.join(bbox) parameters['DATA_TYPE'] = 5 parameters['COMPRESS'] = 4 parameters['JPEGCOMPRESSION'] = 75 parameters['ZLEVEL'] = 6 parameters['PREDICTOR'] = 1 parameters['TILED'] = False parameters['BIGTIFF'] = 0 parameters['TFW'] = False parameters['EXTRA'] = '' parameters['OUTPUT'] = output_raster initialize_processing() feedback = create_processing_feedback() context = create_processing_context(feedback=feedback) result = processing.run( "gdal:cliprasterbyextent", parameters, context=context) if result is None: raise ProcessingInstallationError clipped = QgsRasterLayer(result['OUTPUT'], output_layer_name) # We transfer keywords to the output. clipped.keywords = layer.keywords.copy() clipped.keywords['title'] = output_layer_name check_layer(clipped) except Exception as e: # This step clip_raster_by_extent was nice to speedup the analysis. # As we got an exception because the layer is invalid, we are not going # to stop the analysis. We will return the original raster layer. # It will take more processing time until we clip the vector layer. # Check https://github.com/inasafe/inasafe/issues/4026 why we got some # exceptions with this step. LOGGER.exception(parameters) LOGGER.exception( 'Error from QGIS clip raster by extent. Please check the QGIS ' 'logs too !') LOGGER.info( 'Even if we got an exception, we are continuing the analysis. The ' 'layer was not clipped.') LOGGER.exception(str(e)) LOGGER.exception(get_error_message(e).to_text()) clipped = layer return clipped
def prepare_vector_layer(layer, callback=None): """This function will prepare the layer to be used in InaSAFE : * Make a local copy of the layer. * Make sure that we have an InaSAFE ID column. * Rename fields according to our definitions. * Remove fields which are not used. :param layer: The layer to prepare. :type layer: QgsVectorLayer :param callback: A function to all to indicate progress. The function should accept params 'current' (int), 'maximum' (int) and 'step' (str). Defaults to None. :type callback: function :return: Cleaned memory layer. :rtype: QgsVectorLayer .. versionadded:: 4.0 """ output_layer_name = prepare_vector_steps['output_layer_name'] output_layer_name = output_layer_name % layer.keywords['layer_purpose'] processing_step = prepare_vector_steps['step_name'] if not layer.keywords.get('inasafe_fields'): msg = 'inasafe_fields is missing in keywords from %s' % layer.name() raise InvalidKeywordsForProcessingAlgorithm(msg) feature_count = layer.featureCount() cleaned = create_memory_layer( output_layer_name, layer.geometryType(), layer.crs(), layer.fields()) # We transfer keywords to the output. cleaned.keywords = layer.keywords copy_layer(layer, cleaned) _remove_features(cleaned) # After removing rows, let's check if there is still a feature. request = QgsFeatureRequest().setFlags(QgsFeatureRequest.NoGeometry) iterator = cleaned.getFeatures(request) try: next(iterator) except StopIteration: LOGGER.warning( tr('No feature has been found in the {purpose}' .format(purpose=layer.keywords['layer_purpose']))) raise NoFeaturesInExtentError _add_id_column(cleaned) rename_remove_inasafe_fields(cleaned) if _size_is_needed(cleaned): LOGGER.info( 'We noticed some counts in your exposure layer. Before to update ' 'geometries, we compute the original size for each feature.') run_single_post_processor(cleaned, post_processor_size) if cleaned.keywords['layer_purpose'] == 'exposure': fields = cleaned.keywords['inasafe_fields'] if exposure_type_field['key'] not in fields: _add_default_exposure_class(cleaned) # Check value mapping _check_value_mapping(cleaned) cleaned.keywords['title'] = output_layer_name check_layer(cleaned) return cleaned
def reclassify(layer, exposure_key=None, overwrite_input=False, callback=None): """Reclassify a continuous raster layer. Issue https://github.com/inasafe/inasafe/issues/3182 This function is a wrapper for the code from https://github.com/chiatt/gdal_reclassify For instance if you want to reclassify like this table : Original Value | Class - ∞ < val <= 0 | 1 0 < val <= 0.5 | 2 0.5 < val <= 5 | 3 5 < val < + ∞ | 6 You need a dictionary : ranges = OrderedDict() ranges[1] = [None, 0] ranges[2] = [0.0, 0.5] ranges[3] = [0.5, 5] ranges[6] = [5, None] :param layer: The raster layer. :type layer: QgsRasterLayer :param overwrite_input: Option for the output layer. True will overwrite the input layer. False will create a temporary layer. :type overwrite_input: bool :param exposure_key: The exposure key. :type exposure_key: str :param callback: A function to all to indicate progress. The function should accept params 'current' (int), 'maximum' (int) and 'step' (str). Defaults to None. :type callback: function :return: The classified raster layer. :rtype: QgsRasterLayer .. versionadded:: 4.0 """ output_layer_name = reclassify_raster_steps['output_layer_name'] processing_step = reclassify_raster_steps['step_name'] output_layer_name = output_layer_name % layer.keywords['layer_purpose'] if exposure_key: classification_key = active_classification(layer.keywords, exposure_key) thresholds = active_thresholds_value_maps(layer.keywords, exposure_key) layer.keywords['thresholds'] = thresholds layer.keywords['classification'] = classification_key else: classification_key = layer.keywords.get('classification') thresholds = layer.keywords.get('thresholds') if not thresholds: raise InvalidKeywordsForProcessingAlgorithm( 'thresholds are missing from the layer %s' % layer.keywords['layer_purpose']) if not classification_key: raise InvalidKeywordsForProcessingAlgorithm( 'classification is missing from the layer %s' % layer.keywords['layer_purpose']) ranges = {} value_map = {} hazard_classes = definition(classification_key)['classes'] for hazard_class in hazard_classes: ranges[hazard_class['value']] = thresholds[hazard_class['key']] value_map[hazard_class['key']] = [hazard_class['value']] if overwrite_input: output_raster = layer.source() else: output_raster = unique_filename(suffix='.tiff', dir=temp_dir()) driver = gdal.GetDriverByName('GTiff') raster_file = gdal.Open(layer.source()) band = raster_file.GetRasterBand(1) no_data = band.GetNoDataValue() source = band.ReadAsArray() destination = source.copy() for value, interval in ranges.iteritems(): v_min = interval[0] v_max = interval[1] if v_min is None: destination[np.where(source <= v_max)] = value if v_max is None: destination[np.where(source > v_min)] = value if v_min < v_max: destination[np.where((v_min < source) & (source <= v_max))] = value # Tag no data cells destination[np.where(source == no_data)] = no_data_value # Create the new file. output_file = driver.Create(output_raster, raster_file.RasterXSize, raster_file.RasterYSize, 1) output_file.GetRasterBand(1).WriteArray(destination) output_file.GetRasterBand(1).SetNoDataValue(no_data_value) # CRS output_file.SetProjection(raster_file.GetProjection()) output_file.SetGeoTransform(raster_file.GetGeoTransform()) output_file.FlushCache() del output_file if not isfile(output_raster): raise FileNotFoundError reclassified = QgsRasterLayer(output_raster, output_layer_name) # We transfer keywords to the output. reclassified.keywords = layer.keywords.copy() reclassified.keywords['layer_mode'] = 'classified' value_map = {} hazard_classes = definition(classification_key)['classes'] for hazard_class in reversed(hazard_classes): value_map[hazard_class['key']] = [hazard_class['value']] reclassified.keywords['value_map'] = value_map reclassified.keywords['title'] = output_layer_name check_layer(reclassified) return reclassified
def analysis_eartquake_summary(aggregation, analysis, callback=None): """Compute the summary from the aggregation to the analysis. Source layer : | aggr_id | aggr_name | total_feature | total_displaced | total_fatalities Target layer : | analysis_id | Output layer : | analysis_id | total_feature | total_displaced | total_fatalities | :param aggregation: The layer to aggregate vector layer. :type aggregation: QgsVectorLayer :param analysis: The target vector layer where to write statistics. :type analysis: QgsVectorLayer :param callback: A function to all to indicate progress. The function should accept params 'current' (int), 'maximum' (int) and 'step' (str). Defaults to None. :type callback: function :return: The new target layer with summary. :rtype: QgsVectorLayer .. versionadded:: 4.0 """ output_layer_name = summary_3_analysis_steps['output_layer_name'] processing_step = summary_3_analysis_steps['step_name'] source_fields = aggregation.keywords['inasafe_fields'] target_fields = analysis.keywords['inasafe_fields'] target_compulsory_fields = [ analysis_id_field, analysis_name_field, ] check_inputs(target_compulsory_fields, target_fields) source_compulsory_fields = [ aggregation_id_field, aggregation_name_field, population_count_field, fatalities_field, displaced_field, ] check_inputs(source_compulsory_fields, source_fields) analysis.startEditing() count_hazard_level = {} index_hazard_level = {} classification = definition( aggregation.keywords['hazard_keywords']['classification']) hazard_classes = classification['classes'] for hazard_class in hazard_classes: key = hazard_class['key'] new_field = create_field_from_definition(hazard_count_field, key) analysis.addAttribute(new_field) new_index = analysis.fieldNameIndex(new_field.name()) count_hazard_level[key] = 0 index_hazard_level[key] = new_index target_fields[hazard_count_field['key'] % key] = ( hazard_count_field['field_name'] % key) summaries = {} # Summary is a dictionary with a tuple a key and the value to write in # the output layer as a value. # tuple (index in the aggregation layer, index in the analysis layer) : val for layer_field in source_fields: for definition_field in count_fields: if layer_field == definition_field['key']: field_name = source_fields[layer_field] index = aggregation.fieldNameIndex(field_name) new_field = create_field_from_definition(definition_field) analysis.addAttribute(new_field) new_index = analysis.fieldNameIndex(new_field.name()) summaries[(index, new_index)] = 0 target_fields[definition_field['key']] = ( definition_field['field_name']) request = QgsFeatureRequest() request.setFlags(QgsFeatureRequest.NoGeometry) for area in aggregation.getFeatures(): for index in summaries.keys(): value = area[index[0]] if not value or isinstance(value, QPyNullVariant): value = 0 summaries[index] += value for mmi_level in range(2, 11): field_name = ( population_exposed_per_mmi_field['field_name'] % mmi_level) index = aggregation.fieldNameIndex(field_name) if index > 0: value = area[index] if not value or isinstance(value, QPyNullVariant): value = 0 hazard_class = from_mmi_to_hazard_class( mmi_level, classification['key']) if hazard_class: count_hazard_level[hazard_class] += value for row in analysis.getFeatures(): # We should have only one row in the analysis layer. for field in summaries.keys(): analysis.changeAttributeValue( row.id(), field[1], summaries[field]) for hazard_level, index in index_hazard_level.iteritems(): analysis.changeAttributeValue( row.id(), index, count_hazard_level[hazard_level]) analysis.commitChanges() analysis.keywords['title'] = output_layer_name analysis.keywords['layer_purpose'] = layer_purpose_analysis_impacted['key'] analysis.keywords['hazard_keywords'] = dict( aggregation.keywords['hazard_keywords']) analysis.keywords['exposure_keywords'] = dict( aggregation.keywords['exposure_keywords']) check_layer(analysis) return analysis
def recompute_counts(layer, callback=None): """Recompute counts according to the size field and the new size. This function will also take care of updating the size field. The size post processor won't run after this function again. :param layer: The vector layer. :type layer: QgsVectorLayer :param callback: A function to all to indicate progress. The function should accept params 'current' (int), 'maximum' (int) and 'step' (str). Defaults to None. :type callback: function :return: The layer with updated counts. :rtype: QgsVectorLayer .. versionadded:: 4.0 """ output_layer_name = recompute_counts_steps['output_layer_name'] processing_step = recompute_counts_steps['step_name'] fields = layer.keywords['inasafe_fields'] if size_field['key'] not in fields: # noinspection PyTypeChecker msg = '%s not found in %s' % (size_field['key'], layer.keywords['title']) raise InvalidKeywordsForProcessingAlgorithm(msg) indexes = [] absolute_field_keys = [f['key'] for f in count_fields] for field, field_name in fields.iteritems(): if field in absolute_field_keys and field != size_field['key']: indexes.append(layer.fieldNameIndex(field_name)) LOGGER.info( 'We detected the count {field_name}, we will recompute the ' 'count according to the new size.'.format( field_name=field_name)) if not len(indexes): msg = 'Absolute field not found in the layer %s' % ( layer.keywords['title']) raise InvalidKeywordsForProcessingAlgorithm(msg) size_field_name = fields[size_field['key']] size_field_index = layer.fieldNameIndex(size_field_name) layer.startEditing() exposure_key = layer.keywords['exposure_keywords']['exposure'] size_calculator = SizeCalculator(layer.crs(), layer.geometryType(), exposure_key) for feature in layer.getFeatures(): old_size = feature[size_field_name] new_size = size(size_calculator=size_calculator, geometry=feature.geometry()) layer.changeAttributeValue(feature.id(), size_field_index, new_size) # Cross multiplication for each field for index in indexes: old_count = feature[index] try: new_value = new_size * old_count / old_size except TypeError: new_value = '' layer.changeAttributeValue(feature.id(), index, new_value) layer.commitChanges() layer.keywords['title'] = output_layer_name check_layer(layer) return layer
def union(union_a, union_b, callback=None): """Union of two vector layers. Issue https://github.com/inasafe/inasafe/issues/3186 Note : This algorithm is copied from : https://github.com/qgis/QGIS/blob/master/python/plugins/processing/algs/ qgis/Union.py :param union_a: The vector layer for the union. :type union_a: QgsVectorLayer :param union_b: The vector layer for the union. :type union_b: QgsVectorLayer :param callback: A function to all to indicate progress. The function should accept params 'current' (int), 'maximum' (int) and 'step' (str). Defaults to None. :type callback: function :return: The clip vector layer. :rtype: QgsVectorLayer .. versionadded:: 4.0 """ output_layer_name = union_steps['output_layer_name'] processing_step = union_steps['step_name'] output_layer_name = output_layer_name % ( union_a.keywords['layer_purpose'], union_b.keywords['layer_purpose'] ) fields = union_a.fields() fields.extend(union_b.fields()) writer = create_memory_layer( output_layer_name, union_a.geometryType(), union_a.crs(), fields ) keywords_union_1 = union_a.keywords keywords_union_2 = union_b.keywords inasafe_fields_union_1 = keywords_union_1['inasafe_fields'] inasafe_fields_union_2 = keywords_union_2['inasafe_fields'] inasafe_fields = inasafe_fields_union_1 inasafe_fields.update(inasafe_fields_union_2) # use to avoid modifying original source writer.keywords = dict(union_a.keywords) writer.keywords['inasafe_fields'] = inasafe_fields writer.keywords['title'] = output_layer_name writer.keywords['layer_purpose'] = 'aggregate_hazard' writer.keywords['hazard_keywords'] = keywords_union_1.copy() writer.keywords['aggregation_keywords'] = keywords_union_2.copy() skip_field = inasafe_fields_union_2[aggregation_id_field['key']] not_null_field_index = writer.fieldNameIndex(skip_field) writer.startEditing() # Begin copy/paste from Processing plugin. # Please follow their code as their code is optimized. # The code below is not following our coding standards because we want to # be able to track any diffs from QGIS easily. index_a = create_spatial_index(union_b) index_b = create_spatial_index(union_a) count = 0 n_element = 0 # Todo fix callback # nFeat = len(union_a.getFeatures()) for in_feat_a in union_a.getFeatures(): # progress.setPercentage(nElement / float(nFeat) * 50) n_element += 1 list_intersecting_b = [] geom = geometry_checker(in_feat_a.geometry()) at_map_a = in_feat_a.attributes() intersects = index_a.intersects(geom.boundingBox()) if len(intersects) < 1: try: _write_feature(at_map_a, geom, writer, not_null_field_index) except: # This really shouldn't happen, as we haven't # edited the input geom at all LOGGER.debug( tr('Feature geometry error: One or more output features ' 'ignored due to invalid geometry.')) else: request = QgsFeatureRequest().setFilterFids(intersects) engine = QgsGeometry.createGeometryEngine(geom.geometry()) engine.prepareGeometry() for in_feat_b in union_b.getFeatures(request): count += 1 at_map_b = in_feat_b.attributes() tmp_geom = geometry_checker(in_feat_b.geometry()) if engine.intersects(tmp_geom.geometry()): int_geom = geometry_checker(geom.intersection(tmp_geom)) list_intersecting_b.append(QgsGeometry(tmp_geom)) if not int_geom: # There was a problem creating the intersection # LOGGER.debug( # tr('GEOS geoprocessing error: One or more input ' # 'features have invalid geometry.')) pass int_geom = QgsGeometry() else: int_geom = QgsGeometry(int_geom) if int_geom.wkbType() == QgsWKBTypes.Unknown\ or QgsWKBTypes.flatType( int_geom.geometry().wkbType()) == \ QgsWKBTypes.GeometryCollection: # Intersection produced different geometry types temp_list = int_geom.asGeometryCollection() for i in temp_list: if i.type() == geom.type(): int_geom = QgsGeometry(geometry_checker(i)) try: _write_feature( at_map_a + at_map_b, int_geom, writer, not_null_field_index, ) except: LOGGER.debug( tr('Feature geometry error: One or ' 'more output features ignored due ' 'to invalid geometry.')) else: # Geometry list: prevents writing error # in geometries of different types # produced by the intersection # fix #3549 if int_geom.wkbType() in wkb_type_groups[ wkb_type_groups[int_geom.wkbType()]]: try: _write_feature( at_map_a + at_map_b, int_geom, writer, not_null_field_index) except: LOGGER.debug( tr('Feature geometry error: One or more ' 'output features ignored due to ' 'invalid geometry.')) # the remaining bit of inFeatA's geometry # if there is nothing left, this will just silently fail and we # are good diff_geom = QgsGeometry(geom) if len(list_intersecting_b) != 0: int_b = QgsGeometry.unaryUnion(list_intersecting_b) diff_geom = geometry_checker(diff_geom.difference(int_b)) if diff_geom is None or \ diff_geom.isGeosEmpty() or not diff_geom.isGeosValid(): # LOGGER.debug( # tr('GEOS geoprocessing error: One or more input ' # 'features have invalid geometry.')) pass if diff_geom is not None and ( diff_geom.wkbType() == 0 or QgsWKBTypes.flatType( diff_geom.geometry().wkbType()) == QgsWKBTypes.GeometryCollection): temp_list = diff_geom.asGeometryCollection() for i in temp_list: if i.type() == geom.type(): diff_geom = QgsGeometry(geometry_checker(i)) try: _write_feature( at_map_a, diff_geom, writer, not_null_field_index) except: LOGGER.debug( tr('Feature geometry error: One or more output features ' 'ignored due to invalid geometry.')) length = len(union_a.fields()) at_map_a = [None] * length # nFeat = len(union_b.getFeatures()) for in_feat_a in union_b.getFeatures(): # progress.setPercentage(nElement / float(nFeat) * 100) add = False geom = geometry_checker(in_feat_a.geometry()) atMap = [None] * length atMap.extend(in_feat_a.attributes()) intersects = index_b.intersects(geom.boundingBox()) lstIntersectingA = [] for id in intersects: request = QgsFeatureRequest().setFilterFid(id) inFeatB = union_a.getFeatures(request).next() atMapB = inFeatB.attributes() tmpGeom = QgsGeometry(geometry_checker(inFeatB.geometry())) if geom.intersects(tmpGeom): lstIntersectingA.append(tmpGeom) if len(lstIntersectingA) == 0: res_geom = geom else: intA = QgsGeometry.unaryUnion(lstIntersectingA) res_geom = geom.difference(intA) if res_geom is None: # LOGGER.debug( # tr('GEOS geoprocessing error: One or more input features ' # 'have null geometry.')) pass continue # maybe it is better to fail like @gustry # does below .... if res_geom.isGeosEmpty() or not res_geom.isGeosValid(): # LOGGER.debug( # tr('GEOS geoprocessing error: One or more input features ' # 'have invalid geometry.')) pass try: _write_feature(atMap, res_geom, writer, not_null_field_index) except: # LOGGER.debug( # tr('Feature geometry error: One or more output features ' # 'ignored due to invalid geometry.')) pass n_element += 1 # End of copy/paste from processing writer.commitChanges() fill_hazard_class(writer) check_layer(writer) return writer
def union(union_a, union_b, callback=None): """Union of two vector layers. Issue https://github.com/inasafe/inasafe/issues/3186 Note : This algorithm is copied from : https://github.com/qgis/QGIS/blob/master/python/plugins/processing/algs/ qgis/Union.py :param union_a: The vector layer for the union. :type union_a: QgsVectorLayer :param union_b: The vector layer for the union. :type union_b: QgsVectorLayer :param callback: A function to all to indicate progress. The function should accept params 'current' (int), 'maximum' (int) and 'step' (str). Defaults to None. :type callback: function :return: The clip vector layer. :rtype: QgsVectorLayer .. versionadded:: 4.0 """ output_layer_name = union_steps['output_layer_name'] processing_step = union_steps['step_name'] # NOQA output_layer_name = output_layer_name % ( union_a.keywords['layer_purpose'], union_b.keywords['layer_purpose'] ) fields = union_a.fields() fields.extend(union_b.fields()) writer = create_memory_layer( output_layer_name, union_a.geometryType(), union_a.crs(), fields ) keywords_union_1 = union_a.keywords keywords_union_2 = union_b.keywords inasafe_fields_union_1 = keywords_union_1['inasafe_fields'] inasafe_fields_union_2 = keywords_union_2['inasafe_fields'] inasafe_fields = inasafe_fields_union_1 inasafe_fields.update(inasafe_fields_union_2) # use to avoid modifying original source writer.keywords = dict(union_a.keywords) writer.keywords['inasafe_fields'] = inasafe_fields writer.keywords['title'] = output_layer_name writer.keywords['layer_purpose'] = 'aggregate_hazard' writer.keywords['hazard_keywords'] = keywords_union_1.copy() writer.keywords['aggregation_keywords'] = keywords_union_2.copy() skip_field = inasafe_fields_union_2[aggregation_id_field['key']] not_null_field_index = writer.fieldNameIndex(skip_field) writer.startEditing() # Begin copy/paste from Processing plugin. # Please follow their code as their code is optimized. # The code below is not following our coding standards because we want to # be able to track any diffs from QGIS easily. index_a = create_spatial_index(union_b) index_b = create_spatial_index(union_a) count = 0 n_element = 0 # Todo fix callback # nFeat = len(union_a.getFeatures()) for in_feat_a in union_a.getFeatures(): # progress.setPercentage(nElement / float(nFeat) * 50) n_element += 1 list_intersecting_b = [] geom = geometry_checker(in_feat_a.geometry()) at_map_a = in_feat_a.attributes() intersects = index_a.intersects(geom.boundingBox()) if len(intersects) < 1: try: _write_feature(at_map_a, geom, writer, not_null_field_index) except: # This really shouldn't happen, as we haven't # edited the input geom at all LOGGER.debug( tr('Feature geometry error: One or more output features ' 'ignored due to invalid geometry.')) else: request = QgsFeatureRequest().setFilterFids(intersects) engine = QgsGeometry.createGeometryEngine(geom.geometry()) engine.prepareGeometry() for in_feat_b in union_b.getFeatures(request): count += 1 at_map_b = in_feat_b.attributes() tmp_geom = geometry_checker(in_feat_b.geometry()) if engine.intersects(tmp_geom.geometry()): int_geom = geometry_checker(geom.intersection(tmp_geom)) list_intersecting_b.append(QgsGeometry(tmp_geom)) if not int_geom: # There was a problem creating the intersection # LOGGER.debug( # tr('GEOS geoprocessing error: One or more input ' # 'features have invalid geometry.')) pass int_geom = QgsGeometry() else: int_geom = QgsGeometry(int_geom) if int_geom.wkbType() == QgsWKBTypes.Unknown\ or QgsWKBTypes.flatType( int_geom.geometry().wkbType()) == \ QgsWKBTypes.GeometryCollection: # Intersection produced different geometry types temp_list = int_geom.asGeometryCollection() for i in temp_list: if i.type() == geom.type(): int_geom = QgsGeometry(geometry_checker(i)) try: _write_feature( at_map_a + at_map_b, int_geom, writer, not_null_field_index, ) except: LOGGER.debug( tr('Feature geometry error: One or ' 'more output features ignored due ' 'to invalid geometry.')) else: # Geometry list: prevents writing error # in geometries of different types # produced by the intersection # fix #3549 if int_geom.wkbType() in wkb_type_groups[ wkb_type_groups[int_geom.wkbType()]]: try: _write_feature( at_map_a + at_map_b, int_geom, writer, not_null_field_index) except: LOGGER.debug( tr('Feature geometry error: One or more ' 'output features ignored due to ' 'invalid geometry.')) # the remaining bit of inFeatA's geometry # if there is nothing left, this will just silently fail and we # are good diff_geom = QgsGeometry(geom) if len(list_intersecting_b) != 0: int_b = QgsGeometry.unaryUnion(list_intersecting_b) diff_geom = geometry_checker(diff_geom.difference(int_b)) if diff_geom is None or \ diff_geom.isGeosEmpty() or not diff_geom.isGeosValid(): # LOGGER.debug( # tr('GEOS geoprocessing error: One or more input ' # 'features have invalid geometry.')) pass if diff_geom is not None and ( diff_geom.wkbType() == 0 or QgsWKBTypes.flatType( diff_geom.geometry().wkbType()) == QgsWKBTypes.GeometryCollection): temp_list = diff_geom.asGeometryCollection() for i in temp_list: if i.type() == geom.type(): diff_geom = QgsGeometry(geometry_checker(i)) try: _write_feature( at_map_a, diff_geom, writer, not_null_field_index) except: LOGGER.debug( tr('Feature geometry error: One or more output features ' 'ignored due to invalid geometry.')) length = len(union_a.fields()) # nFeat = len(union_b.getFeatures()) for in_feat_a in union_b.getFeatures(): # progress.setPercentage(nElement / float(nFeat) * 100) geom = geometry_checker(in_feat_a.geometry()) atMap = [None] * length atMap.extend(in_feat_a.attributes()) intersects = index_b.intersects(geom.boundingBox()) lstIntersectingA = [] for id in intersects: request = QgsFeatureRequest().setFilterFid(id) inFeatB = union_a.getFeatures(request).next() tmpGeom = QgsGeometry(geometry_checker(inFeatB.geometry())) if geom.intersects(tmpGeom): lstIntersectingA.append(tmpGeom) if len(lstIntersectingA) == 0: res_geom = geom else: intA = QgsGeometry.unaryUnion(lstIntersectingA) res_geom = geom.difference(intA) if res_geom is None: # LOGGER.debug( # tr('GEOS geoprocessing error: One or more input features ' # 'have null geometry.')) pass continue # maybe it is better to fail like @gustry # does below .... if res_geom.isGeosEmpty() or not res_geom.isGeosValid(): # LOGGER.debug( # tr('GEOS geoprocessing error: One or more input features ' # 'have invalid geometry.')) pass try: _write_feature(atMap, res_geom, writer, not_null_field_index) except: # LOGGER.debug( # tr('Feature geometry error: One or more output features ' # 'ignored due to invalid geometry.')) pass n_element += 1 # End of copy/paste from processing writer.commitChanges() fill_hazard_class(writer) check_layer(writer) return writer
def clip(layer_to_clip, mask_layer, callback=None): """Clip a vector layer with another. Issue https://github.com/inasafe/inasafe/issues/3186 Note : This algorithm is copied from : https://github.com/qgis/QGIS/blob/master/python/plugins/processing/algs/ qgis/Clip.py :param layer_to_clip: The vector layer to clip. :type layer_to_clip: QgsVectorLayer :param mask_layer: The vector layer to use for clipping. :type mask_layer: QgsVectorLayer :param callback: A function to all to indicate progress. The function should accept params 'current' (int), 'maximum' (int) and 'step' (str). Defaults to None. :type callback: function :return: The clip vector layer. :rtype: QgsVectorLayer .. versionadded:: 4.0 """ output_layer_name = clip_steps['output_layer_name'] output_layer_name = output_layer_name % ( layer_to_clip.keywords['layer_purpose']) processing_step = clip_steps['step_name'] writer = create_memory_layer(output_layer_name, layer_to_clip.geometryType(), layer_to_clip.crs(), layer_to_clip.fields()) writer.startEditing() # Begin copy/paste from Processing plugin. # Please follow their code as their code is optimized. # The code below is not following our coding standards because we want to # be able to track any diffs from QGIS easily. # first build up a list of clip geometries clip_geometries = [] request = QgsFeatureRequest().setSubsetOfAttributes([]) for mask_feature in mask_layer.getFeatures(request): clip_geometries.append(QgsGeometry(mask_feature.geometry())) # are we clipping against a single feature? if so, # we can show finer progress reports if len(clip_geometries) > 1: # noinspection PyTypeChecker,PyCallByClass,PyArgumentList combined_clip_geom = QgsGeometry.unaryUnion(clip_geometries) single_clip_feature = False else: combined_clip_geom = clip_geometries[0] single_clip_feature = True # use prepared geometries for faster intersection tests # noinspection PyArgumentList engine = QgsGeometry.createGeometryEngine(combined_clip_geom.geometry()) engine.prepareGeometry() tested_feature_ids = set() for i, clip_geom in enumerate(clip_geometries): request = QgsFeatureRequest().setFilterRect(clip_geom.boundingBox()) input_features = [f for f in layer_to_clip.getFeatures(request)] if not input_features: continue if single_clip_feature: total = 100.0 / len(input_features) else: total = 0 for current, in_feat in enumerate(input_features): if not in_feat.geometry(): continue if in_feat.id() in tested_feature_ids: # don't retest a feature we have already checked continue tested_feature_ids.add(in_feat.id()) if not engine.intersects(in_feat.geometry().geometry()): continue if not engine.contains(in_feat.geometry().geometry()): cur_geom = in_feat.geometry() new_geom = combined_clip_geom.intersection(cur_geom) if new_geom.wkbType() == QgsWKBTypes.Unknown \ or QgsWKBTypes.flatType( new_geom.geometry().wkbType()) == \ QgsWKBTypes.GeometryCollection: int_com = in_feat.geometry().combine(new_geom) int_sym = in_feat.geometry().symDifference(new_geom) if not int_com or not int_sym: # LOGGER.debug( # tr('GEOS geoprocessing error: One or more input ' # 'features have invalid geometry.')) pass else: new_geom = int_com.difference(int_sym) if new_geom.isGeosEmpty()\ or not new_geom.isGeosValid(): # LOGGER.debug( # tr('GEOS geoprocessing error: One or more ' # 'input features have invalid geometry.')) pass else: # clip geometry totally contains feature geometry, # so no need to perform intersection new_geom = in_feat.geometry() try: out_feat = QgsFeature() out_feat.setGeometry(new_geom) out_feat.setAttributes(in_feat.attributes()) writer.addFeature(out_feat) except: LOGGER.debug( tr('Feature geometry error: One or more output features ' 'ignored due to invalid geometry.')) continue # TODO implement callback if single_clip_feature: # progress.setPercentage(int(current * total)) pass if not single_clip_feature: # coarse progress report for multiple clip geometries # progress.setPercentage(100.0 * i / len(clip_geoms)) pass # End copy/paste from Processing plugin. writer.commitChanges() writer.keywords = layer_to_clip.keywords.copy() writer.keywords['title'] = output_layer_name check_layer(writer) return writer
def clip(layer_to_clip, mask_layer, callback=None): """Clip a vector layer with another. Issue https://github.com/inasafe/inasafe/issues/3186 Note : This algorithm is copied from : https://github.com/qgis/QGIS/blob/master/python/plugins/processing/algs/ qgis/Clip.py :param layer_to_clip: The vector layer to clip. :type layer_to_clip: QgsVectorLayer :param mask_layer: The vector layer to use for clipping. :type mask_layer: QgsVectorLayer :param callback: A function to all to indicate progress. The function should accept params 'current' (int), 'maximum' (int) and 'step' (str). Defaults to None. :type callback: function :return: The clip vector layer. :rtype: QgsVectorLayer .. versionadded:: 4.0 """ output_layer_name = clip_steps['output_layer_name'] output_layer_name = output_layer_name % ( layer_to_clip.keywords['layer_purpose']) processing_step = clip_steps['step_name'] writer = create_memory_layer( output_layer_name, layer_to_clip.geometryType(), layer_to_clip.crs(), layer_to_clip.fields() ) writer.startEditing() # Begin copy/paste from Processing plugin. # Please follow their code as their code is optimized. # The code below is not following our coding standards because we want to # be able to track any diffs from QGIS easily. # first build up a list of clip geometries clip_geometries = [] request = QgsFeatureRequest().setSubsetOfAttributes([]) for mask_feature in mask_layer.getFeatures(request): clip_geometries.append(QgsGeometry(mask_feature.geometry())) # are we clipping against a single feature? if so, # we can show finer progress reports if len(clip_geometries) > 1: # noinspection PyTypeChecker,PyCallByClass,PyArgumentList combined_clip_geom = QgsGeometry.unaryUnion(clip_geometries) single_clip_feature = False else: combined_clip_geom = clip_geometries[0] single_clip_feature = True # use prepared geometries for faster intersection tests # noinspection PyArgumentList engine = QgsGeometry.createGeometryEngine(combined_clip_geom.geometry()) engine.prepareGeometry() tested_feature_ids = set() for i, clip_geom in enumerate(clip_geometries): request = QgsFeatureRequest().setFilterRect(clip_geom.boundingBox()) input_features = [f for f in layer_to_clip.getFeatures(request)] if not input_features: continue if single_clip_feature: total = 100.0 / len(input_features) else: total = 0 for current, in_feat in enumerate(input_features): if not in_feat.geometry(): continue if in_feat.id() in tested_feature_ids: # don't retest a feature we have already checked continue tested_feature_ids.add(in_feat.id()) if not engine.intersects(in_feat.geometry().geometry()): continue if not engine.contains(in_feat.geometry().geometry()): cur_geom = in_feat.geometry() new_geom = combined_clip_geom.intersection(cur_geom) if new_geom.wkbType() == QgsWKBTypes.Unknown \ or QgsWKBTypes.flatType( new_geom.geometry().wkbType()) == \ QgsWKBTypes.GeometryCollection: int_com = in_feat.geometry().combine(new_geom) int_sym = in_feat.geometry().symDifference(new_geom) if not int_com or not int_sym: # LOGGER.debug( # tr('GEOS geoprocessing error: One or more input ' # 'features have invalid geometry.')) pass else: new_geom = int_com.difference(int_sym) if new_geom.isGeosEmpty()\ or not new_geom.isGeosValid(): # LOGGER.debug( # tr('GEOS geoprocessing error: One or more ' # 'input features have invalid geometry.')) pass else: # clip geometry totally contains feature geometry, # so no need to perform intersection new_geom = in_feat.geometry() try: out_feat = QgsFeature() out_feat.setGeometry(new_geom) out_feat.setAttributes(in_feat.attributes()) if new_geom.type() == layer_to_clip.geometryType(): writer.addFeature(out_feat) except: LOGGER.debug( tr('Feature geometry error: One or more output features ' 'ignored due to invalid geometry.')) continue # TODO implement callback if single_clip_feature: # progress.setPercentage(int(current * total)) pass if not single_clip_feature: # coarse progress report for multiple clip geometries # progress.setPercentage(100.0 * i / len(clip_geoms)) pass # End copy/paste from Processing plugin. writer.commitChanges() writer.keywords = layer_to_clip.keywords.copy() writer.keywords['title'] = output_layer_name check_layer(writer) return writer
def update_value_map(layer, exposure_key=None, callback=None): """Assign inasafe values according to definitions for a vector layer. :param layer: The vector layer. :type layer: QgsVectorLayer :param exposure_key: The exposure key. :type exposure_key: str :param callback: A function to all to indicate progress. The function should accept params 'current' (int), 'maximum' (int) and 'step' (str). Defaults to None. :type callback: function :return: The classified vector layer. :rtype: QgsVectorLayer .. versionadded:: 4.0 """ output_layer_name = assign_inasafe_values_steps['output_layer_name'] processing_step = assign_inasafe_values_steps['step_name'] output_layer_name = output_layer_name % layer.keywords['layer_purpose'] keywords = layer.keywords inasafe_fields = keywords['inasafe_fields'] classification = None if keywords['layer_purpose'] == layer_purpose_hazard['key']: if not inasafe_fields.get(hazard_value_field['key']): raise InvalidKeywordsForProcessingAlgorithm old_field = hazard_value_field new_field = hazard_class_field classification = active_classification(layer.keywords, exposure_key) elif keywords['layer_purpose'] == layer_purpose_exposure['key']: if not inasafe_fields.get(exposure_type_field['key']): raise InvalidKeywordsForProcessingAlgorithm old_field = exposure_type_field new_field = exposure_class_field else: raise InvalidKeywordsForProcessingAlgorithm # It's a hazard layer if exposure_key: if not active_thresholds_value_maps(keywords, exposure_key): raise InvalidKeywordsForProcessingAlgorithm value_map = active_thresholds_value_maps(keywords, exposure_key) # It's exposure layer else: if not keywords.get('value_map'): raise InvalidKeywordsForProcessingAlgorithm value_map = keywords.get('value_map') unclassified_column = inasafe_fields[old_field['key']] unclassified_index = layer.fieldNameIndex(unclassified_column) reversed_value_map = {} for inasafe_class, values in value_map.iteritems(): for val in values: reversed_value_map[val] = inasafe_class classified_field = QgsField() classified_field.setType(new_field['type']) classified_field.setName(new_field['field_name']) classified_field.setLength(new_field['length']) classified_field.setPrecision(new_field['precision']) layer.startEditing() layer.addAttribute(classified_field) classified_field_index = layer.fieldNameIndex(classified_field.name()) for feature in layer.getFeatures(): attributes = feature.attributes() source_value = attributes[unclassified_index] classified_value = reversed_value_map.get(source_value) if not classified_value: classified_value = '' layer.changeAttributeValue( feature.id(), classified_field_index, classified_value) layer.commitChanges() remove_fields(layer, [unclassified_column]) # We transfer keywords to the output. # We add new class field inasafe_fields[new_field['key']] = new_field['field_name'] # and we remove hazard value field inasafe_fields.pop(old_field['key']) layer.keywords = keywords layer.keywords['inasafe_fields'] = inasafe_fields if exposure_key: value_map_key = 'value_maps' else: value_map_key = 'value_map' if value_map_key in layer.keywords.keys(): layer.keywords.pop(value_map_key) layer.keywords['title'] = output_layer_name if classification: layer.keywords['classification'] = classification check_layer(layer) return layer
def exposure_summary_table(aggregate_hazard, exposure_summary=None, callback=None): """Compute the summary from the aggregate hazard to analysis. Source layer : | haz_id | haz_class | aggr_id | aggr_name | exposure_count | Output layer : | exp_type | count_hazard_class | total | :param aggregate_hazard: The layer to aggregate vector layer. :type aggregate_hazard: QgsVectorLayer :param exposure_summary: The layer impact layer. :type exposure_summary: QgsVectorLayer :param callback: A function to all to indicate progress. The function should accept params 'current' (int), 'maximum' (int) and 'step' (str). Defaults to None. :type callback: function :return: The new tabular table, without geometry. :rtype: QgsVectorLayer .. versionadded:: 4.0 """ output_layer_name = summary_4_exposure_summary_table_steps[ 'output_layer_name'] source_fields = aggregate_hazard.keywords['inasafe_fields'] source_compulsory_fields = [ aggregation_id_field, aggregation_name_field, hazard_id_field, hazard_class_field, affected_field, total_field ] check_inputs(source_compulsory_fields, source_fields) absolute_values = create_absolute_values_structure(aggregate_hazard, ['all']) hazard_class = source_fields[hazard_class_field['key']] hazard_class_index = aggregate_hazard.fields().lookupField(hazard_class) unique_hazard = aggregate_hazard.uniqueValues(hazard_class_index) unique_exposure = read_dynamic_inasafe_field(source_fields, exposure_count_field) flat_table = FlatTable('hazard_class', 'exposure_class') request = QgsFeatureRequest() request.setFlags(QgsFeatureRequest.NoGeometry) for area in aggregate_hazard.getFeatures(): hazard_value = area[hazard_class_index] for exposure in unique_exposure: key_name = exposure_count_field['key'] % exposure field_name = source_fields[key_name] exposure_count = area[field_name] if not exposure_count: exposure_count = 0 flat_table.add_value(exposure_count, hazard_class=hazard_value, exposure_class=exposure) # We summarize every absolute values. for field, field_definition in list(absolute_values.items()): value = area[field] if not value: value = 0 field_definition[0].add_value(value, all='all') tabular = create_memory_layer(output_layer_name, QgsWkbTypes.NullGeometry) tabular.startEditing() field = create_field_from_definition(exposure_type_field) tabular.addAttribute(field) tabular.keywords['inasafe_fields'][exposure_type_field['key']] = ( exposure_type_field['field_name']) hazard_keywords = aggregate_hazard.keywords['hazard_keywords'] hazard = hazard_keywords['hazard'] classification = hazard_keywords['classification'] exposure_keywords = aggregate_hazard.keywords['exposure_keywords'] exposure = exposure_keywords['exposure'] hazard_affected = {} for hazard_class in unique_hazard: if (hazard_class == '' or hazard_class is None or (hasattr(hazard_class, 'isNull') and hazard_class.isNull())): hazard_class = 'NULL' field = create_field_from_definition(hazard_count_field, hazard_class) tabular.addAttribute(field) key = hazard_count_field['key'] % hazard_class value = hazard_count_field['field_name'] % hazard_class tabular.keywords['inasafe_fields'][key] = value hazard_affected[hazard_class] = post_processor_affected_function( exposure=exposure, hazard=hazard, classification=classification, hazard_class=hazard_class) field = create_field_from_definition(total_affected_field) tabular.addAttribute(field) tabular.keywords['inasafe_fields'][total_affected_field['key']] = ( total_affected_field['field_name']) # essentially have the same value as NULL_hazard_count # but with this, make sure that it exists in layer so it can be used for # reporting, and can be referenced to fields.py to take the label. field = create_field_from_definition(total_not_affected_field) tabular.addAttribute(field) tabular.keywords['inasafe_fields'][total_not_affected_field['key']] = ( total_not_affected_field['field_name']) field = create_field_from_definition(total_not_exposed_field) tabular.addAttribute(field) tabular.keywords['inasafe_fields'][total_not_exposed_field['key']] = ( total_not_exposed_field['field_name']) field = create_field_from_definition(total_field) tabular.addAttribute(field) tabular.keywords['inasafe_fields'][total_field['key']] = ( total_field['field_name']) summarization_dicts = {} if exposure_summary: summarization_dicts = summarize_result(exposure_summary) sorted_keys = sorted(summarization_dicts.keys()) for key in sorted_keys: summary_field = summary_rules[key]['summary_field'] field = create_field_from_definition(summary_field) tabular.addAttribute(field) tabular.keywords['inasafe_fields'][summary_field['key']] = ( summary_field['field_name']) # Only add absolute value if there is no summarization / no exposure # classification if not summarization_dicts: # For each absolute values for absolute_field in list(absolute_values.keys()): field_definition = definition(absolute_values[absolute_field][1]) field = create_field_from_definition(field_definition) tabular.addAttribute(field) key = field_definition['key'] value = field_definition['field_name'] tabular.keywords['inasafe_fields'][key] = value for exposure_type in unique_exposure: feature = QgsFeature() attributes = [exposure_type] total_affected = 0 total_not_affected = 0 total_not_exposed = 0 total = 0 for hazard_class in unique_hazard: if hazard_class == '' or hazard_class is None: hazard_class = 'NULL' value = flat_table.get_value(hazard_class=hazard_class, exposure_class=exposure_type) attributes.append(value) if hazard_affected[hazard_class] == not_exposed_class['key']: total_not_exposed += value elif hazard_affected[hazard_class]: total_affected += value else: total_not_affected += value total += value attributes.append(total_affected) attributes.append(total_not_affected) attributes.append(total_not_exposed) attributes.append(total) if summarization_dicts: for key in sorted_keys: attributes.append(summarization_dicts[key].get( exposure_type, 0)) else: for i, field in enumerate(absolute_values.values()): value = field[0].get_value(all='all') attributes.append(value) feature.setAttributes(attributes) tabular.addFeature(feature) # Sanity check ± 1 to the result. Disabled for now as it seems ± 1 is # not enough. ET 13/02/17 # total_computed = ( # total_affected + total_not_affected + total_not_exposed) # if not -1 < (total_computed - total) < 1: # raise ComputationError tabular.commitChanges() tabular.keywords['title'] = layer_purpose_exposure_summary_table['name'] if qgis_version() >= 21800: tabular.setName(tabular.keywords['title']) else: tabular.setLayerName(tabular.keywords['title']) tabular.keywords['layer_purpose'] = layer_purpose_exposure_summary_table[ 'key'] check_layer(tabular, has_geometry=False) return tabular
def exposure_summary_table( aggregate_hazard, exposure_summary=None, callback=None): """Compute the summary from the aggregate hazard to analysis. Source layer : | haz_id | haz_class | aggr_id | aggr_name | exposure_count | Output layer : | exp_type | count_hazard_class | total | :param aggregate_hazard: The layer to aggregate vector layer. :type aggregate_hazard: QgsVectorLayer :param exposure_summary: The layer impact layer. :type exposure_summary: QgsVectorLayer :param callback: A function to all to indicate progress. The function should accept params 'current' (int), 'maximum' (int) and 'step' (str). Defaults to None. :type callback: function :return: The new tabular table, without geometry. :rtype: QgsVectorLayer .. versionadded:: 4.0 """ output_layer_name = summary_4_exposure_summary_table_steps[ 'output_layer_name'] source_fields = aggregate_hazard.keywords['inasafe_fields'] source_compulsory_fields = [ aggregation_id_field, aggregation_name_field, hazard_id_field, hazard_class_field, affected_field, total_field ] check_inputs(source_compulsory_fields, source_fields) absolute_values = create_absolute_values_structure( aggregate_hazard, ['all']) hazard_class = source_fields[hazard_class_field['key']] hazard_class_index = aggregate_hazard.fields().lookupField(hazard_class) unique_hazard = aggregate_hazard.uniqueValues(hazard_class_index) unique_exposure = read_dynamic_inasafe_field( source_fields, exposure_count_field) flat_table = FlatTable('hazard_class', 'exposure_class') request = QgsFeatureRequest() request.setFlags(QgsFeatureRequest.NoGeometry) for area in aggregate_hazard.getFeatures(): hazard_value = area[hazard_class_index] for exposure in unique_exposure: key_name = exposure_count_field['key'] % exposure field_name = source_fields[key_name] exposure_count = area[field_name] if not exposure_count: exposure_count = 0 flat_table.add_value( exposure_count, hazard_class=hazard_value, exposure_class=exposure ) # We summarize every absolute values. for field, field_definition in list(absolute_values.items()): value = area[field] if not value: value = 0 field_definition[0].add_value( value, all='all' ) tabular = create_memory_layer(output_layer_name, QgsWkbTypes.NullGeometry) tabular.startEditing() field = create_field_from_definition(exposure_type_field) tabular.addAttribute(field) tabular.keywords['inasafe_fields'][exposure_type_field['key']] = ( exposure_type_field['field_name']) hazard_keywords = aggregate_hazard.keywords['hazard_keywords'] hazard = hazard_keywords['hazard'] classification = hazard_keywords['classification'] exposure_keywords = aggregate_hazard.keywords['exposure_keywords'] exposure = exposure_keywords['exposure'] hazard_affected = {} for hazard_class in unique_hazard: if (hazard_class == '' or hazard_class is None or (hasattr(hazard_class, 'isNull') and hazard_class.isNull())): hazard_class = 'NULL' field = create_field_from_definition(hazard_count_field, hazard_class) tabular.addAttribute(field) key = hazard_count_field['key'] % hazard_class value = hazard_count_field['field_name'] % hazard_class tabular.keywords['inasafe_fields'][key] = value hazard_affected[hazard_class] = post_processor_affected_function( exposure=exposure, hazard=hazard, classification=classification, hazard_class=hazard_class ) field = create_field_from_definition(total_affected_field) tabular.addAttribute(field) tabular.keywords['inasafe_fields'][total_affected_field['key']] = ( total_affected_field['field_name']) # essentially have the same value as NULL_hazard_count # but with this, make sure that it exists in layer so it can be used for # reporting, and can be referenced to fields.py to take the label. field = create_field_from_definition(total_not_affected_field) tabular.addAttribute(field) tabular.keywords['inasafe_fields'][total_not_affected_field['key']] = ( total_not_affected_field['field_name']) field = create_field_from_definition(total_not_exposed_field) tabular.addAttribute(field) tabular.keywords['inasafe_fields'][total_not_exposed_field['key']] = ( total_not_exposed_field['field_name']) field = create_field_from_definition(total_field) tabular.addAttribute(field) tabular.keywords['inasafe_fields'][total_field['key']] = ( total_field['field_name']) summarization_dicts = {} if exposure_summary: summarization_dicts = summarize_result(exposure_summary) sorted_keys = sorted(summarization_dicts.keys()) for key in sorted_keys: summary_field = summary_rules[key]['summary_field'] field = create_field_from_definition(summary_field) tabular.addAttribute(field) tabular.keywords['inasafe_fields'][ summary_field['key']] = ( summary_field['field_name']) # Only add absolute value if there is no summarization / no exposure # classification if not summarization_dicts: # For each absolute values for absolute_field in list(absolute_values.keys()): field_definition = definition(absolute_values[absolute_field][1]) field = create_field_from_definition(field_definition) tabular.addAttribute(field) key = field_definition['key'] value = field_definition['field_name'] tabular.keywords['inasafe_fields'][key] = value for exposure_type in unique_exposure: feature = QgsFeature() attributes = [exposure_type] total_affected = 0 total_not_affected = 0 total_not_exposed = 0 total = 0 for hazard_class in unique_hazard: if hazard_class == '' or hazard_class is None: hazard_class = 'NULL' value = flat_table.get_value( hazard_class=hazard_class, exposure_class=exposure_type ) attributes.append(value) if hazard_affected[hazard_class] == not_exposed_class['key']: total_not_exposed += value elif hazard_affected[hazard_class]: total_affected += value else: total_not_affected += value total += value attributes.append(total_affected) attributes.append(total_not_affected) attributes.append(total_not_exposed) attributes.append(total) if summarization_dicts: for key in sorted_keys: attributes.append(summarization_dicts[key].get( exposure_type, 0)) else: for i, field in enumerate(absolute_values.values()): value = field[0].get_value( all='all' ) attributes.append(value) feature.setAttributes(attributes) tabular.addFeature(feature) # Sanity check ± 1 to the result. Disabled for now as it seems ± 1 is # not enough. ET 13/02/17 # total_computed = ( # total_affected + total_not_affected + total_not_exposed) # if not -1 < (total_computed - total) < 1: # raise ComputationError tabular.commitChanges() tabular.keywords['title'] = layer_purpose_exposure_summary_table['name'] if qgis_version() >= 21800: tabular.setName(tabular.keywords['title']) else: tabular.setLayerName(tabular.keywords['title']) tabular.keywords['layer_purpose'] = layer_purpose_exposure_summary_table[ 'key'] check_layer(tabular, has_geometry=False) return tabular
def recompute_counts(layer, callback=None): """Recompute counts according to the size field and the new size. This function will also take care of updating the size field. The size post processor won't run after this function again. :param layer: The vector layer. :type layer: QgsVectorLayer :param callback: A function to all to indicate progress. The function should accept params 'current' (int), 'maximum' (int) and 'step' (str). Defaults to None. :type callback: function :return: The layer with updated counts. :rtype: QgsVectorLayer .. versionadded:: 4.0 """ output_layer_name = recompute_counts_steps['output_layer_name'] processing_step = recompute_counts_steps['step_name'] fields = layer.keywords['inasafe_fields'] if size_field['key'] not in fields: # noinspection PyTypeChecker msg = '%s not found in %s' % ( size_field['key'], layer.keywords['title']) raise InvalidKeywordsForProcessingAlgorithm(msg) indexes = [] absolute_field_keys = [f['key'] for f in count_fields] for field, field_name in fields.iteritems(): if field in absolute_field_keys and field != size_field['key']: indexes.append(layer.fieldNameIndex(field_name)) LOGGER.info( 'We detected the count {field_name}, we will recompute the ' 'count according to the new size.'.format( field_name=field_name)) if not len(indexes): msg = 'Absolute field not found in the layer %s' % ( layer.keywords['title']) raise InvalidKeywordsForProcessingAlgorithm(msg) size_field_name = fields[size_field['key']] size_field_index = layer.fieldNameIndex(size_field_name) layer.startEditing() exposure_key = layer.keywords['exposure_keywords']['exposure'] size_calculator = SizeCalculator( layer.crs(), layer.geometryType(), exposure_key) for feature in layer.getFeatures(): old_size = feature[size_field_name] new_size = size( size_calculator=size_calculator, geometry=feature.geometry()) layer.changeAttributeValue(feature.id(), size_field_index, new_size) # Cross multiplication for each field for index in indexes: old_count = feature[index] try: new_value = new_size * old_count / old_size except TypeError: new_value = '' layer.changeAttributeValue(feature.id(), index, new_value) layer.commitChanges() layer.keywords['title'] = output_layer_name check_layer(layer) return layer
def update_value_map(layer, exposure_key=None): """Assign inasafe values according to definitions for a vector layer. :param layer: The vector layer. :type layer: QgsVectorLayer :param exposure_key: The exposure key. :type exposure_key: str :return: The classified vector layer. :rtype: QgsVectorLayer .. versionadded:: 4.0 """ output_layer_name = assign_inasafe_values_steps['output_layer_name'] output_layer_name = output_layer_name % layer.keywords['layer_purpose'] keywords = layer.keywords inasafe_fields = keywords['inasafe_fields'] classification = None if keywords['layer_purpose'] == layer_purpose_hazard['key']: if not inasafe_fields.get(hazard_value_field['key']): raise InvalidKeywordsForProcessingAlgorithm old_field = hazard_value_field new_field = hazard_class_field classification = active_classification(layer.keywords, exposure_key) elif keywords['layer_purpose'] == layer_purpose_exposure['key']: if not inasafe_fields.get(exposure_type_field['key']): raise InvalidKeywordsForProcessingAlgorithm old_field = exposure_type_field new_field = exposure_class_field else: raise InvalidKeywordsForProcessingAlgorithm # It's a hazard layer if exposure_key: if not active_thresholds_value_maps(keywords, exposure_key): raise InvalidKeywordsForProcessingAlgorithm value_map = active_thresholds_value_maps(keywords, exposure_key) # It's exposure layer else: if not keywords.get('value_map'): raise InvalidKeywordsForProcessingAlgorithm value_map = keywords.get('value_map') unclassified_column = inasafe_fields[old_field['key']] unclassified_index = layer.fields().lookupField(unclassified_column) reversed_value_map = {} for inasafe_class, values in list(value_map.items()): for val in values: reversed_value_map[val] = inasafe_class classified_field = QgsField() classified_field.setType(new_field['type']) classified_field.setName(new_field['field_name']) classified_field.setLength(new_field['length']) classified_field.setPrecision(new_field['precision']) layer.startEditing() layer.addAttribute(classified_field) classified_field_index = layer.fields(). \ lookupField(classified_field.name()) for feature in layer.getFeatures(): attributes = feature.attributes() source_value = attributes[unclassified_index] classified_value = reversed_value_map.get(source_value) if not classified_value: classified_value = '' layer.changeAttributeValue(feature.id(), classified_field_index, classified_value) layer.commitChanges() remove_fields(layer, [unclassified_column]) # We transfer keywords to the output. # We add new class field inasafe_fields[new_field['key']] = new_field['field_name'] # and we remove hazard value field inasafe_fields.pop(old_field['key']) layer.keywords = keywords layer.keywords['inasafe_fields'] = inasafe_fields if exposure_key: value_map_key = 'value_maps' else: value_map_key = 'value_map' if value_map_key in list(layer.keywords.keys()): layer.keywords.pop(value_map_key) layer.keywords['title'] = output_layer_name if classification: layer.keywords['classification'] = classification check_layer(layer) return layer
def add_default_values(layer, callback=None): """Add or fill default values to the layer, see #3325. 1. It doesn't have inasafe_field and it doesn't have inasafe_default_value --> Do nothing. 2. It has inasafe_field and it does not have inasafe_default_value --> Do nothing. 3. It does not have inasafe_field but it has inasafe_default_value --> Create new field, and fill with the default value for all features 4. It has inasafe_field and it has inasafe_default_value --> Replace the null value with the default one. :param layer: The vector layer. :type layer: QgsVectorLayer :param callback: A function to all to indicate progress. The function should accept params 'current' (int), 'maximum' (int) and 'step' (str). Defaults to None. :type callback: function :return: The vector layer with the default values. :rtype: QgsVectorLayer .. versionadded:: 4.0 """ output_layer_name = assign_default_values_steps['output_layer_name'] output_layer_name = output_layer_name % layer.keywords['layer_purpose'] processing_step = assign_default_values_steps['step_name'] fields = layer.keywords.get('inasafe_fields') if not isinstance(fields, dict): msg = 'inasafe_fields is missing in keywords from %s' % layer.name() raise InvalidKeywordsForProcessingAlgorithm(msg) defaults = layer.keywords.get('inasafe_default_values') if not defaults: # Case 1 and 2. LOGGER.info( 'inasafe_default_value is not present, we can not fill default ' 'ratios for this layer.') return layer for default in defaults.keys(): field = fields.get(default) target_field = definition(default) layer.startEditing() if not field: # Case 3 LOGGER.info( '{field} is not present but the layer has {value} as a ' 'default for {field}. We create the new field ' '{new_field} with this value.'.format( field=target_field['key'], value=defaults[default], new_field=target_field['field_name'])) new_field = create_field_from_definition(target_field) layer.addAttribute(new_field) new_index = layer.fieldNameIndex(new_field.name()) for feature in layer.getFeatures(): layer.changeAttributeValue(feature.id(), new_index, defaults[default]) layer.keywords['inasafe_fields'][target_field['key']] = ( target_field['field_name']) else: # Case 4 LOGGER.info('{field} is present and the layer has {value} as a ' 'default for {field}, we MUST do nothing.'.format( field=target_field['key'], value=defaults[default])) index = layer.fieldNameIndex(field) for feature in layer.getFeatures(): if isinstance(feature.attributes()[index], QPyNullVariant): layer.changeAttributeValue(feature.id(), index, defaults[default]) continue if feature.attributes()[index] == '': layer.changeAttributeValue(feature.id(), index, defaults[default]) continue layer.commitChanges() layer.keywords['title'] = output_layer_name check_layer(layer) return layer