def test_run(self): """TestVolcanoPolygonBuildingFunction: Test running the IF.""" volcano_path = standard_data_path('hazard', 'volcano_krb.shp') building_path = standard_data_path('exposure', 'buildings.shp') hazard_layer = read_layer(volcano_path) exposure_layer = read_layer(building_path) impact_function = VolcanoPolygonBuildingFunction.instance() impact_function.hazard = SafeLayer(hazard_layer) impact_function.exposure = SafeLayer(exposure_layer) impact_function.run() impact_layer = impact_function.impact # Check the question expected_question = ( 'In the event of volcano krb how many buildings might be ' 'affected?') self.assertEqual(expected_question, impact_function.question) # The buildings should all be categorised into 5000 zone zone_sum = impact_layer.get_data( attribute=impact_function.target_field) krb3_zone_count = zone_sum.count('High Hazard Zone') krb2_zone_count = zone_sum.count('Medium Hazard Zone') # The result (counted by hand) expected_krb3_count = 11 expected_krb2_count = 161 message = 'Expecting %s for KRB III zone, but it returns %s' % ( krb3_zone_count, expected_krb3_count) self.assertEqual(krb3_zone_count, expected_krb3_count, message) message = 'Expecting %s for KRB II zone, but it returns %s' % ( krb2_zone_count, expected_krb2_count) self.assertEqual(krb2_zone_count, expected_krb2_count, message)
def calculate_impact(self): if_manager = ImpactFunctionManager() function_id = self.function_id impact_function = if_manager.get_instance(function_id) impact_function.hazard = SafeLayer(self.hazard_layer) impact_function.exposure = SafeLayer(self.exposure_layer) try: self.impact_layer = safe_calculate_impact(impact_function) self.set_style() except ZeroImpactException as e: # in case zero impact, just return LOGGER.info('No impact detected') LOGGER.info(e.message) return # copy results of impact to report_path directory base_name, _ = os.path.splitext(self.impact_layer.filename) dir_name = os.path.dirname(self.impact_layer.filename) for (root, dirs, files) in os.walk(dir_name): for f in files: source_filename = os.path.join(root, f) if source_filename.find(base_name) >= 0: extensions = source_filename.replace(base_name, '') new_path = os.path.join(self.report_path, 'impact' + extensions) shutil.copy(source_filename, new_path) self.impact_layer = read_layer(self.impact_path)
def test_run(self): impact_function = FloodRasterBuildingFunction.instance() hazard_path = standard_data_path('hazard', 'continuous_flood_20_20.asc') exposure_path = standard_data_path('exposure', 'buildings.shp') hazard_layer = read_layer(hazard_path) exposure_layer = read_layer(exposure_path) impact_function.hazard = SafeLayer(hazard_layer) impact_function.exposure = SafeLayer(exposure_layer) impact_function.run() impact_layer = impact_function.impact # Extract calculated result impact_data = impact_layer.get_data() self.assertEqual(len(impact_data), 181) # 1 = inundated, 2 = wet, 3 = dry expected_result = {1: 64, 2: 117, 3: 0} result = {1: 0, 2: 0, 3: 0} for feature in impact_data: inundated_status = feature[impact_function.target_field] result[inundated_status] += 1 message = 'Expecting %s, but it returns %s' % (expected_result, result) self.assertEqual(expected_result, result, message)
def test_run(self): function = AshRasterPopulationFunction.instance() hazard_path = standard_data_path('hazard', 'ash_raster_wgs84.tif') exposure_path = standard_data_path( 'exposure', 'pop_binary_raster_20_20.asc') # We need clipping for both layers to be in the same dimension clipped_hazard, clipped_exposure = clip_layers( hazard_path, exposure_path) hazard_layer = read_layer(clipped_hazard.source()) exposure_layer = read_layer(clipped_exposure.source()) # Let's set the extent to the hazard extent function.hazard = SafeLayer(hazard_layer) function.exposure = SafeLayer(exposure_layer) function.run() impact = function.impact expected = [ [u'Population in very low hazard zone', 0], [u'Population in medium hazard zone', 1374], [u'Population in high hazard zone', 20], [u'Population in very high hazard zone', 0], [u'Population in low hazard zone', 8443], [u'Total affected population', 9837], [u'Unaffected population', 0], [u'Total population', 9837], [u'Population needing evacuation <sup>1</sup>', 9837] ] self.assertListEqual( expected, impact.impact_data['impact summary']['fields'])
def test_run(self): function = FloodEvacuationRasterHazardFunction.instance() hazard_path = test_data_path('hazard', 'continuous_flood_20_20.asc') exposure_path = test_data_path('exposure', 'pop_binary_raster_20_20.asc') hazard_layer = read_layer(hazard_path) exposure_layer = read_layer(exposure_path) function.parameters['thresholds'].value = [0.5, 0.7, 1.0] function.hazard = SafeLayer(hazard_layer) function.exposure = SafeLayer(exposure_layer) function.run() impact = function.impact # Count of flooded objects is calculated "by the hands" # print "keywords", keywords keywords = impact.get_keywords() evacuated = float(keywords['evacuated']) total_needs_full = keywords['total_needs'] total_needs_weekly = OrderedDict([[x['table name'], x['amount']] for x in total_needs_full['weekly']]) total_needs_single = OrderedDict([[x['table name'], x['amount']] for x in total_needs_full['single']]) expected_evacuated = 100 self.assertEqual(evacuated, expected_evacuated) self.assertEqual(total_needs_weekly['Rice [kg]'], 280) self.assertEqual(total_needs_weekly['Family Kits'], 20) self.assertEqual(total_needs_weekly['Drinking Water [l]'], 1750) self.assertEqual(total_needs_weekly['Clean Water [l]'], 6700) self.assertEqual(total_needs_single['Toilets'], 5)
def test_run(self): function = ContinuousHazardPopulationFunction.instance() hazard_path = test_data_path('hazard', 'continuous_flood_20_20.asc') exposure_path = test_data_path('exposure', 'pop_binary_raster_20_20.asc') hazard_layer = read_layer(hazard_path) exposure_layer = read_layer(exposure_path) function.hazard = SafeLayer(hazard_layer) function.exposure = SafeLayer(exposure_layer) function.run() impact = function.impact # print "keywords", keywords keywords = impact.get_keywords() total_needs_full = keywords['total_needs'] total_needs_weekly = OrderedDict([[x['table name'], x['amount']] for x in total_needs_full['weekly']]) total_needs_single = OrderedDict([[x['table name'], x['amount']] for x in total_needs_full['single']]) self.assertEqual(total_needs_weekly['Rice [kg]'], 336) self.assertEqual(total_needs_weekly['Drinking Water [l]'], 2100) self.assertEqual(total_needs_weekly['Clean Water [l]'], 8040) self.assertEqual(total_needs_weekly['Family Kits'], 24) self.assertEqual(total_needs_single['Toilets'], 6)
def test_run(self): """TestVolcanoPointBuildingFunction: Test running the IF.""" volcano_path = standard_data_path('hazard', 'volcano_point.shp') building_path = standard_data_path('exposure', 'buildings.shp') hazard_layer = read_layer(volcano_path) exposure_layer = read_layer(building_path) impact_function = VolcanoPointBuildingFunction.instance() impact_function.hazard = SafeLayer(hazard_layer) impact_function.exposure = SafeLayer(exposure_layer) impact_function._prepare() impact_function.run() impact_layer = impact_function.impact # Check the question expected_question = ( 'In the event of volcano point how many buildings might be ' 'affected?') self.assertEqual(expected_question, impact_function.question) # The buildings should all be categorised into 3000 zone zone_sum = sum( impact_layer.get_data(attribute=impact_function.target_field)) expected_sum = 3 * 181 message = 'Expecting %s, but it returns %s' % (expected_sum, zone_sum) self.assertEqual(zone_sum, expected_sum, message)
def test_run(self): function = ClassifiedRasterHazardBuildingFunction.instance() hazard_path = test_data_path('hazard', 'classified_flood_20_20.asc') exposure_path = test_data_path('exposure', 'buildings.shp') hazard_layer = read_layer(hazard_path) exposure_layer = read_layer(exposure_path) function.hazard = SafeLayer(hazard_layer) function.exposure = SafeLayer(exposure_layer) function.run() impact_layer = function.impact impact_data = impact_layer.get_data() # Count expected_impact = {1.0: 67, 2.0: 49, 3.0: 64} result_impact = {1.0: 0, 2.0: 0, 3.0: 0} for impact_feature in impact_data: level = impact_feature['level'] if not math.isnan(level): result_impact[level] += 1 message = 'Expecting %s, but it returns %s' % (expected_impact, result_impact) self.assertEqual(expected_impact, result_impact, message)
def test_run(self): """TestClassifiedPolygonPopulationFunction: Test running the IF.""" generic_polygon_path = test_data_path( 'hazard', 'classified_generic_polygon.shp') population_path = test_data_path('exposure', 'pop_binary_raster_20_20.asc') generic_polygon_layer = read_layer(generic_polygon_path) population_layer = read_layer(population_path) impact_function = ClassifiedPolygonHazardPopulationFunction.instance() impact_function.hazard = SafeLayer(generic_polygon_layer) impact_function.exposure = SafeLayer(population_layer) impact_function.run() impact_layer = impact_function.impact # Check the question expected_question = ('In each of the hazard zones how many people ' 'might be impacted.') message = 'The question should be %s, but it returns %s' % ( expected_question, impact_function.question) self.assertEqual(expected_question, impact_function.question, message) # Count by hand expected_affected_population = 181 result = numpy.nansum(impact_layer.get_data()) self.assertEqual(expected_affected_population, result, message)
def test_run(self): function = ClassifiedRasterHazardBuildingFunction.instance() hazard_path = standard_data_path( 'hazard', 'classified_hazard.tif') exposure_path = standard_data_path('exposure', 'small_building.shp') hazard_layer = read_layer(hazard_path) exposure_layer = read_layer(exposure_path) function.hazard = SafeLayer(hazard_layer) function.exposure = SafeLayer(exposure_layer) function.run_analysis() impact_layer = function.impact impact_data = impact_layer.get_data() # Count expected_impact = { 'Not affected': 5, 'Low hazard zone': 2, 'Medium hazard zone': 9, 'High hazard zone': 5 } result_impact = {} for impact_feature in impact_data: hazard_class = impact_feature[function.target_field] if hazard_class in result_impact: result_impact[hazard_class] += 1 else: result_impact[hazard_class] = 1 self.assertDictEqual(expected_impact, result_impact)
def test_run(self): function = FloodPolygonBuildingFunction.instance() hazard_path = test_data_path('hazard', 'flood_multipart_polygons.shp') # exposure_path = test_data_path('exposure', 'buildings.shp') # noinspection PyCallingNonCallable hazard_layer = QgsVectorLayer(hazard_path, 'Flood', 'ogr') # noinspection PyCallingNonCallable # exposure_layer = QgsVectorLayer(exposure_path, 'Buildings', 'ogr') exposure_layer = clone_shp_layer( name='buildings', include_keywords=True, source_directory=test_data_path('exposure')) # Let's set the extent to the hazard extent extent = hazard_layer.extent() rect_extent = [ extent.xMinimum(), extent.yMaximum(), extent.xMaximum(), extent.yMinimum() ] function.hazard = SafeLayer(hazard_layer) function.exposure = SafeLayer(exposure_layer) function.requested_extent = rect_extent function.run() impact = function.impact # Count of flooded objects is calculated "by the hands" # total flooded = 27, total buildings = 129 count = sum(impact.get_data(attribute=function.target_field)) self.assertEquals(count, 33) count = len(impact.get_data()) self.assertEquals(count, 176)
def test_run(self): function = FloodRasterRoadsFunction.instance() hazard_path = standard_data_path('hazard', 'continuous_flood_20_20.asc') exposure_path = standard_data_path('exposure', 'roads.shp') # noinspection PyCallingNonCallable hazard_layer = QgsRasterLayer(hazard_path, 'Flood') # noinspection PyCallingNonCallable exposure_layer = QgsVectorLayer(exposure_path, 'Roads', 'ogr') # Let's set the extent to the hazard extent extent = hazard_layer.extent() rect_extent = [ extent.xMinimum(), extent.yMaximum(), extent.xMaximum(), extent.yMinimum() ] function.hazard = SafeLayer(hazard_layer) function.exposure = SafeLayer(exposure_layer) function.requested_extent = rect_extent function.run() impact = function.impact keywords = impact.get_keywords() self.assertEquals(function.target_field, keywords['target_field']) expected_inundated_feature = 182 count = sum(impact.get_data(attribute=function.target_field)) self.assertEquals(count, expected_inundated_feature)
def test_run(self): function = FloodPolygonRoadsFunction.instance() hazard_path = test_data_path('hazard', 'flood_multipart_polygons.shp') exposure_path = test_data_path('exposure', 'roads.shp') # noinspection PyCallingNonCallable hazard_layer = QgsVectorLayer(hazard_path, 'Flood', 'ogr') # noinspection PyCallingNonCallable exposure_layer = QgsVectorLayer(exposure_path, 'Roads', 'ogr') # Let's set the extent to the hazard extent extent = hazard_layer.extent() rect_extent = [ extent.xMinimum(), extent.yMaximum(), extent.xMaximum(), extent.yMinimum() ] function.hazard = SafeLayer(hazard_layer) function.exposure = SafeLayer(exposure_layer) function.requested_extent = rect_extent function.run() impact = function.impact # Count of flooded objects is calculated "by the hands" # the count = 69 expected_feature_total = 69 count = sum(impact.get_data(attribute=function.target_field)) message = 'Expecting %s, but it returns %s' % (expected_feature_total, count) self.assertEquals(count, expected_feature_total, message)
def test_run(self): """TestVolcanoPointPopulationFunction: Test running the IF.""" merapi_point_path = test_data_path('hazard', 'volcano_point.shp') population_path = test_data_path('exposure', 'pop_binary_raster_20_20.asc') merapi_point_layer = read_layer(merapi_point_path) population_layer = read_layer(population_path) impact_function = VolcanoPointPopulationFunction.instance() # Run merapi point impact_function.hazard = SafeLayer(merapi_point_layer) impact_function.exposure = SafeLayer(population_layer) impact_function.run() impact_layer = impact_function.impact # Check the question expected_question = ('In the event of a volcano point how many ' 'people might be impacted') message = 'The question should be %s, but it returns %s' % ( expected_question, impact_function.question) self.assertEqual(expected_question, impact_function.question, message) # Count by hand expected_affected_population = 200 result = numpy.nansum(impact_layer.get_data()) message = 'Expecting %s, but it returns %s' % ( expected_affected_population, result) self.assertEqual(expected_affected_population, result, message)
def test_run(self): """TestVolcanoPolygonPopulationFunction: Test running the IF.""" merapi_krb_path = test_data_path('hazard', 'volcano_krb.shp') population_path = test_data_path( 'exposure', 'pop_binary_raster_20_20.asc') merapi_krb_layer = read_layer(merapi_krb_path) population_layer = read_layer(population_path) impact_function = VolcanoPolygonPopulationFunction.instance() # 2. Run merapi krb impact_function.hazard = SafeLayer(merapi_krb_layer) impact_function.exposure = SafeLayer(population_layer) impact_function.run() impact_layer = impact_function.impact # Check the question expected_question = ('In the event of volcano krb how many population ' 'might need evacuation') message = 'The question should be %s, but it returns %s' % ( expected_question, impact_function.question) self.assertEqual(expected_question, impact_function.question, message) # Count by hand expected_affected_population = 181 result = numpy.nansum(impact_layer.get_data()) self.assertEqual(expected_affected_population, result, message)
def test_run_point_exposure(self): """Run the IF for point exposure. See https://github.com/AIFDR/inasafe/issues/2156. """ generic_polygon_path = test_data_path( 'hazard', 'classified_generic_polygon.shp') building_path = test_data_path('exposure', 'building-points.shp') hazard_layer = QgsVectorLayer(generic_polygon_path, 'Hazard', 'ogr') exposure_layer = QgsVectorLayer(building_path, 'Buildings', 'ogr') # Let's set the extent to the hazard extent extent = hazard_layer.extent() rect_extent = [ extent.xMinimum(), extent.yMaximum(), extent.xMaximum(), extent.yMinimum() ] impact_function = ClassifiedPolygonHazardBuildingFunction.instance() impact_function.hazard = SafeLayer(hazard_layer) impact_function.exposure = SafeLayer(exposure_layer) impact_function.requested_extent = rect_extent impact_function.run() impact_layer = impact_function.impact # Check the question expected_question = ('In each of the hazard zones how many buildings ' 'might be affected.') message = 'The question should be %s, but it returns %s' % ( expected_question, impact_function.question) self.assertEqual(expected_question, impact_function.question, message) zone_sum = impact_layer.get_data( attribute=impact_function.target_field) high_zone_count = zone_sum.count('High Hazard Zone') medium_zone_count = zone_sum.count('Medium Hazard Zone') low_zone_count = zone_sum.count('Low Hazard Zone') # The result expected_high_count = 12 expected_medium_count = 172 expected_low_count = 3 message = 'Expecting %s for High Hazard Zone, but it returns %s' % ( high_zone_count, expected_high_count) self.assertEqual(high_zone_count, expected_high_count, message) message = 'Expecting %s for Medium Hazard Zone, but it returns %s' % ( expected_medium_count, medium_zone_count) self.assertEqual(medium_zone_count, expected_medium_count, message) message = 'Expecting %s for Low Hazard Zone, but it returns %s' % ( expected_low_count, low_zone_count) self.assertEqual(expected_low_count, low_zone_count, message)
def test_calculate_impact(self): """Test calculating impact.""" eq_path = test_data_path('hazard', 'earthquake.tif') building_path = test_data_path('exposure', 'buildings.shp') eq_layer = read_layer(eq_path) building_layer = read_layer(building_path) impact_function = EarthquakeBuildingFunction.instance() impact_function.hazard = SafeLayer(eq_layer) impact_function.exposure = SafeLayer(building_layer) impact_layer = calculate_impact(impact_function) self.assertIsNotNone(impact_layer)
def test_run_failed(self): """Test run IF with missing keywords.""" merapi_krb_path = standard_data_path('hazard', 'volcano_krb.shp') population_path = standard_data_path('exposure', 'pop_binary_raster_20_20.asc') merapi_krb_layer = read_layer(merapi_krb_path) population_layer = read_layer(population_path) impact_function = VolcanoPolygonPopulationFunction.instance() # 2. Run merapi krb layer = SafeLayer(merapi_krb_layer) layer.keywords = {} impact_function.hazard = layer impact_function.exposure = SafeLayer(population_layer) self.assertRaises(KeywordNotFoundError, impact_function.run)
def test_run_failed(self): """Test run IF with missing keywords.""" merapi_krb_path = test_data_path('hazard', 'volcano_krb.shp') population_path = test_data_path( 'exposure', 'pop_binary_raster_20_20.asc') merapi_krb_layer = read_layer(merapi_krb_path) population_layer = read_layer(population_path) impact_function = VolcanoPolygonPopulationFunction.instance() # 2. Run merapi krb layer = SafeLayer(merapi_krb_layer) layer.keywords = {} impact_function.hazard = layer impact_function.exposure = SafeLayer(population_layer) self.assertRaises(KeywordNotFoundError, impact_function.run)
def test_zero_intersection(self): hazard_path = test_data_path('hazard', 'continuous_flood_20_20.asc') exposure_path = test_data_path('exposure', 'roads.shp') # noinspection PyCallingNonCallable hazard_layer = QgsRasterLayer(hazard_path, 'Flood') # noinspection PyCallingNonCallable exposure_layer = QgsVectorLayer(exposure_path, 'Roads', 'ogr') # Let's set the extent to the hazard extent function = FloodRasterRoadsFunction.instance() rect_extent = [106.831991, -6.170044, 106.834868, -6.167793] function.hazard = SafeLayer(hazard_layer) function.exposure = SafeLayer(exposure_layer) function.requested_extent = rect_extent with self.assertRaises(ZeroImpactException): function.run()
def direct_execution(): arg = AnalysisArguments.read_arguments() register_impact_functions() registry = ImpactFunctionManager().registry function = registry.get_instance(arg.impact_function_name) function.hazard = SafeLayer(read_layer(arg.hazard_filename)) function.exposure = SafeLayer(read_layer(arg.exposure_filename)) impact = calculate_impact(function) qgis_impact = safe_to_qgis_layer(impact) generate_styles(impact, qgis_impact) copy_impact_layer(impact, arg.impact_filename)
def test_safe_layer_attributes(self): """Test creating safe layer.""" self.maxDiff = None building_path = standard_data_path('exposure', 'buildings.shp') building_layer = read_layer(building_path) exposure = SafeLayer(building_layer) # Expect InvalidLayerError self.assertRaises(InvalidLayerError, SafeLayer, None) # Expect KeywordNotFoundError self.assertRaises(KeywordNotFoundError, exposure.keyword, 'dummy') self.assertEquals(exposure.name, 'Buildings') expected_keywords = { 'license': u'Open Data Commons Open Database License (ODbL)', 'keyword_version': u'3.5', 'value_mapping': { u'government': [u'Government'], u'residential': [u'Residential'], u'commercial': [u'Commercial'], u'health': [u'Clinic/Doctor'], u'education': [u'School'], u'place of worship': [u'Place of Worship - Islam'] }, 'structure_class_field': u'TYPE', 'title': u'Buildings', 'source': u'OpenStreetMap - www.openstreetmap.org', 'layer_geometry': u'polygon', 'layer_purpose': u'exposure', 'layer_mode': u'classified', 'exposure': u'structure' } self.assertEquals(exposure.keywords, expected_keywords) self.assertFalse(exposure.is_qgsvectorlayer()) self.assertTrue(isinstance(exposure.qgis_layer(), QgsMapLayer)) self.assertTrue(isinstance(exposure.qgis_layer(), QgsVectorLayer)) self.assertFalse(isinstance(exposure.qgis_layer(), QgsRasterLayer)) self.assertEquals(exposure.crs().authid(), 'EPSG:4326') self.assertEquals( exposure.extent().asWktCoordinates(), u'106.80645110100005013 -6.18730753857923688, ' u'106.82525023478235937 -6.17267712704860294') self.assertEquals(exposure.layer_type(), 0) self.assertEquals(exposure.geometry_type(), 2)
def aggregation(self, layer): """Setter for aggregation layer property. :param layer: Aggregation layer to be used for the analysis. :type layer: SafeLayer """ if isinstance(layer, SafeLayer): self._aggregation = layer elif isinstance(layer, QgsMapLayer): self._aggregation = SafeLayer(layer) else: self._aggregation = None
def test_run(self): function = ClassifiedRasterHazardPopulationFunction.instance() hazard_path = test_data_path('hazard', 'classified_flood_20_20.asc') exposure_path = test_data_path('exposure', 'pop_binary_raster_20_20.asc') hazard_layer = read_layer(hazard_path) exposure_layer = read_layer(exposure_path) function.hazard = SafeLayer(hazard_layer) function.exposure = SafeLayer(exposure_layer) function.run() impact_layer = function.impact impact_data = impact_layer.get_data() # Total people affected = 200 expected = 200 result = sum(sum(impact_data)) message = 'Expecting %s, but it returns %s' % (expected, result) self.assertEqual(expected, result, message)
def test_run(self): """TestEarthquakeBuildingFunction: Test running the IF.""" eq_path = test_data_path('hazard', 'earthquake.tif') building_path = test_data_path('exposure', 'buildings.shp') eq_layer = read_layer(eq_path) building_layer = read_layer(building_path) impact_function = EarthquakeBuildingFunction.instance() impact_function.hazard = SafeLayer(eq_layer) impact_function.exposure = SafeLayer(building_layer) impact_function.run() impact_layer = impact_function.impact # Check the question expected_question = ( 'In the event of earthquake how many buildings might be affected') message = 'The question should be %s, but it returns %s' % ( expected_question, impact_function.question) self.assertEqual(expected_question, impact_function.question, message) # Count by hand, # 1 = low, 2 = medium, 3 = high impact = { 1: 0, 2: 181, 3: 0 } result = { 1: 0, 2: 0, 3: 0 } impact_features = impact_layer.get_data() for i in range(len(impact_features)): impact_feature = impact_features[i] level = impact_feature.get(impact_function.target_field) result[level] += 1 message = 'Expecting %s, but it returns %s' % (impact, result) self.assertEqual(impact, result, message)
def test_run(self): function = FloodEvacuationVectorHazardFunction.instance() hazard_path = test_data_path('hazard', 'flood_multipart_polygons.shp') exposure_path = test_data_path('exposure', 'pop_binary_raster_20_20.asc') hazard_layer = read_layer(hazard_path) exposure_layer = read_layer(exposure_path) function.hazard = SafeLayer(hazard_layer) function.exposure = SafeLayer(exposure_layer) function.run() impact = function.impact keywords = impact.get_keywords() # print "keywords", keywords affected_population = numpy.nansum(impact.get_data()) total_population = keywords['total_population'] self.assertEqual(affected_population, 20) self.assertEqual(total_population, 200)
def test_run(self): function = TsunamiEvacuationFunction.instance() hazard_path = test_data_path('hazard', 'tsunami_wgs84.tif') exposure_path = test_data_path('exposure', 'pop_binary_raster_20_20.asc') # We need clipping for both layers to be in the same dimension clipped_hazard, clipped_exposure = clip_layers(hazard_path, exposure_path) hazard_layer = read_layer(clipped_hazard.source()) exposure_layer = read_layer(clipped_exposure.source()) # Let's set the extent to the hazard extent function.parameters['thresholds'].value = [0.7, 0.8, 0.9] function.hazard = SafeLayer(hazard_layer) function.exposure = SafeLayer(exposure_layer) function.run() impact = function.impact # Count of flooded objects is calculated "by the hands" # print "keywords", keywords keywords = impact.get_keywords() evacuated = float(keywords['evacuated']) total_needs_full = keywords['total_needs'] total_needs_weekly = OrderedDict([[x['table name'], x['amount']] for x in total_needs_full['weekly']]) total_needs_single = OrderedDict([[x['table name'], x['amount']] for x in total_needs_full['single']]) # #FIXME: This doesn't make sense due to clipping above. Update # clip_layers expected_evacuated = 1198 self.assertEqual(evacuated, expected_evacuated) self.assertEqual(total_needs_weekly['Rice [kg]'], 3355) self.assertEqual(total_needs_weekly['Family Kits'], 240) self.assertEqual(total_needs_weekly['Drinking Water [l]'], 20965) self.assertEqual(total_needs_weekly['Clean Water [l]'], 80266) self.assertEqual(total_needs_single['Toilets'], 60)
def test_parameter(self): """Test for checking parameter is carried out""" eq_path = standard_data_path('hazard', 'earthquake.tif') population_path = standard_data_path('exposure', 'pop_binary_raster_20_20.asc') # For EQ on Pops we need to clip the hazard and exposure first to the # same dimension clipped_hazard, clipped_exposure = clip_layers(eq_path, population_path) # noinspection PyUnresolvedReferences eq_layer = read_layer(str(clipped_hazard.source())) # noinspection PyUnresolvedReferences population_layer = read_layer(str(clipped_exposure.source())) impact_function = ITBBayesianFatalityFunction.instance() impact_function.hazard = SafeLayer(eq_layer) impact_function.exposure = SafeLayer(population_layer) expected = { 'postprocessors': { 'Age': { 'Age': { 'Adult ratio': 0.659, 'Elderly ratio': 0.078, 'Youth ratio': 0.263 } }, 'Gender': { 'Gender': True }, 'MinimumNeeds': { 'MinimumNeeds': True } } } self.assertDictEqual(expected, impact_function.parameters_value())
def test_run_aggregation(self): """Test running the IF with aggregation with same attribute name #2750. """ function = FloodPolygonRoadsFunction.instance() hazard_path = standard_data_path('hazard', 'flood_multipart_polygons.shp') exposure_path = standard_data_path('exposure', 'roads.shp') aggregation_path = standard_data_path('boundaries', 'grid_jakarta.shp') # noinspection PyCallingNonCallable hazard_layer = QgsVectorLayer(hazard_path, 'Flood', 'ogr') # noinspection PyCallingNonCallable exposure_layer = QgsVectorLayer(exposure_path, 'Roads', 'ogr') # noinspection PyCallingNonCallable aggregation_layer = QgsVectorLayer(aggregation_path, 'Grids', 'ogr') # Let's set the extent to the hazard extent extent = aggregation_layer.extent() rect_extent = [ extent.xMinimum(), extent.yMaximum(), extent.xMaximum(), extent.yMinimum() ] function.hazard = SafeLayer(hazard_layer) function.exposure = SafeLayer(exposure_layer) function.aggregation = aggregation_layer function.requested_extent = rect_extent try: function._validate() function._prepare() function._impact = function._calculate_impact() function._run_aggregator() except KeyError as e: self.fail('KeyError because of %s' % e) except Exception as e: self.fail('Exception because of %s' % e)
def exposure(self, layer): """Setter for exposure layer property. :param layer: exposure layer to be used for the analysis. :type layer: SafeLayer """ if isinstance(layer, SafeLayer): self._exposure = layer else: if self.function_type() == 'old-style': self._exposure = SafeLayer(convert_to_safe_layer(layer)) elif self.function_type() == 'qgis2.0': # convert for new style impact function self._exposure = SafeLayer(layer) else: message = tr('Error: Impact Function has unknown style.') raise Exception(message) # Update the target field to a non-conflicting one if self.exposure.is_qgsvectorlayer(): self._target_field = get_non_conflicting_attribute_name( self.target_field, self.exposure.layer.dataProvider().fieldNameMap().keys())
def hazard(self, layer): """Setter for hazard layer property. :param layer: Hazard layer to be used for the analysis. :type layer: SafeLayer, Layer, QgsMapLayer """ if isinstance(layer, SafeLayer): self._hazard = layer else: if self.function_type() == 'old-style': self._hazard = SafeLayer(convert_to_safe_layer(layer)) elif self.function_type() == 'qgis2.0': # convert for new style impact function self._hazard = SafeLayer(layer) else: message = tr('Error: Impact Function has unknown style.') raise Exception(message) # Update the target field to a non-conflicting one if self._hazard.is_qgsvectorlayer(): self._target_field = get_non_conflicting_attribute_name( self.target_field, self._hazard.layer.dataProvider().fieldNameMap().keys() )
def test_safe_layer_attributes(self): """Test creating safe layer.""" self.maxDiff = None building_path = standard_data_path('exposure', 'buildings.shp') building_layer = read_layer(building_path) exposure = SafeLayer(building_layer) # Expect InvalidLayerError self.assertRaises(InvalidLayerError, SafeLayer, None) # Expect KeywordNotFoundError self.assertRaises(KeywordNotFoundError, exposure.keyword, 'dummy') self.assertEquals(exposure.name, 'Buildings') expected_keywords = { 'license': u'Open Data Commons Open Database License (ODbL)', 'keyword_version': u'3.5', 'value_mapping': {u'government': [u'Government'], u'residential': [u'Residential'], u'commercial': [u'Commercial'], u'health': [u'Clinic/Doctor'], u'education': [u'School'], u'place of worship': [ u'Place of Worship - Islam']}, 'structure_class_field': u'TYPE', 'title': u'Buildings', 'source': u'OpenStreetMap - www.openstreetmap.org', 'layer_geometry': u'polygon', 'layer_purpose': u'exposure', 'layer_mode': u'classified', 'exposure': u'structure'} self.assertEquals(exposure.keywords, expected_keywords) self.assertFalse(exposure.is_qgsvectorlayer()) self.assertTrue(isinstance(exposure.qgis_layer(), QgsMapLayer)) self.assertTrue(isinstance(exposure.qgis_layer(), QgsVectorLayer)) self.assertFalse(isinstance(exposure.qgis_layer(), QgsRasterLayer)) self.assertEquals(exposure.crs().authid(), 'EPSG:4326') self.assertEquals( exposure.extent().asWktCoordinates(), u'106.80645110100005013 -6.18730753857923688, ' u'106.82525023478235937 -6.17267712704860294') self.assertEquals(exposure.layer_type(), 0) self.assertEquals(exposure.geometry_type(), 2)
class ImpactFunction(object): """Abstract base class for all impact functions.""" # Class properties _metadata = ImpactFunctionMetadata def __init__(self): """Base class constructor. All derived classes should normally call this constructor e.g.:: def __init__(self): super(FloodImpactFunction, self).__init__() """ # User who runs this self._user = getpass.getuser().replace(' ', '_') # The host that runs this self._host_name = gethostname() # Requested extent to use self._requested_extent = None # Requested extent's CRS self._requested_extent_crs = QgsCoordinateReferenceSystem('EPSG:4326') # The current viewport extent of the map canvas self._viewport_extent = None # Actual extent to use - Read Only # For 'old-style' IF we do some manipulation to the requested extent self._actual_extent = None # Actual extent's CRS - Read Only self._actual_extent_crs = QgsCoordinateReferenceSystem('EPSG:4326') # set this to a gui call back / web callback etc as needed. self._callback = self.console_progress_callback # Set the default parameters self._parameters = self._metadata.parameters() # Layer representing hazard e.g. flood self._hazard = None # Layer representing people / infrastructure that are exposed self._exposure = None # Layer used for aggregating results by area / district self._aggregation = None # The best extents to use for the assessment self._clip_parameters = None # Clip features that extend beyond the extents. self._clip_hard = False # Show intermediate layers. self._show_intermediate_layers = False # Force memory. self._force_memory = False # Layer produced by the impact function self._impact = None # The question of the impact function self._question = None # Post analysis Result dictionary (suitable to conversion to json etc.) self._tabulated_impact = None # Style information for the impact layer - at some point we should # formalise this into a more natural model # ABC's will normally set this property. self._impact_style = None # The target field for vector impact layer self._target_field = 'safe_ag' # The string to mark not affected value in the vector impact layer self._not_affected_value = 'Not Affected' # Store provenances self._provenances = Provenance() # Start time self._start_time = None self.provenance.append_step('Initialize Impact Function', 'Impact function is being initialized') @classmethod def metadata(cls): """Get the metadata class of this impact function.""" return cls._metadata @classmethod def function_type(cls): """Property for the type of impact function ('old-style' or 'qgis2.0'). QGIS2 impact functions are using the QGIS api and have more dependencies. Legacy IF's use only numpy, gdal etc. and can be used in contexts where no QGIS is present. """ return cls.metadata().as_dict().get('function_type', None) @property def user(self): """Property for the user who runs this. :returns: User who runs this :rtype: basestring """ return self._user @property def host_name(self): """Property for the host name that runs this. :returns: The host name. :rtype: basestring """ return self._host_name @property def requested_extent(self): """Property for the extent of impact function analysis. :returns: A list in the form [xmin, ymin, xmax, ymax]. :rtype: list """ return self._requested_extent @requested_extent.setter def requested_extent(self, extent): """Setter for extent property. :param extent: Analysis boundaries expressed as [xmin, ymin, xmax, ymax]. The extent CRS should match the extent_crs property of this IF instance. :type extent: list """ # add more robust checks here if len(extent) != 4: raise InvalidExtentError('%s is not a valid extent.' % extent) self._requested_extent = extent @property def requested_extent_crs(self): """Property for the extent CRS of impact function analysis. :return crs: The coordinate reference system for the analysis boundary. :rtype: QgsCoordinateReferenceSystem """ return self._requested_extent_crs @requested_extent_crs.setter def requested_extent_crs(self, crs): """Setter for extent_crs property. .. note:: We break our rule here on not allowing acronyms for parameter names. :param crs: The coordinate reference system for the analysis boundary. :type crs: QgsCoordinateReferenceSystem """ self._requested_extent_crs = crs @property def actual_extent(self): """Property for the actual extent for analysis. :returns: A list in the form [xmin, ymin, xmax, ymax]. :rtype: list """ return self._actual_extent @property def actual_extent_crs(self): """Property for the actual extent crs for analysis. :returns: The CRS for the actual extent. :rtype: QgsCoordinateReferenceSystem """ return self._actual_extent_crs @property def viewport_extent(self): """Property for the viewport extent of the map canvas. :returns: Viewport extent in the form [xmin, ymin, xmax, ymax] where all coordinates provided are in Geographic / EPSG:4326. :rtype: list """ return self._viewport_extent @viewport_extent.setter def viewport_extent(self, viewport_extent): """Setter for the viewport extent of the map canvas. :param viewport_extent: Viewport extent in the form [xmin, ymin, xmax, ymax] in EPSG:4326. :type viewport_extent: list """ self._viewport_extent = viewport_extent @property def callback(self): """Property for the callback used to relay processing progress. :returns: A callback function. The callback function will have the following parameter requirements. progress_callback(current, maximum, message=None) :rtype: function .. seealso:: console_progress_callback """ return self._callback @callback.setter def callback(self, callback): """Setter for callback property. :param callback: A callback function reference that provides the following signature: progress_callback(current, maximum, message=None) :type callback: function """ self._callback = callback @classmethod def instance(cls): """Make an instance of the impact function.""" return cls() @property def hazard(self): """Property for the hazard layer to be used for the analysis. :returns: A map layer. :rtype: SafeLayer """ return self._hazard @hazard.setter def hazard(self, layer): """Setter for hazard layer property. :param layer: Hazard layer to be used for the analysis. :type layer: SafeLayer, Layer, QgsMapLayer """ if isinstance(layer, SafeLayer): self._hazard = layer else: if self.function_type() == 'old-style': self._hazard = SafeLayer(convert_to_safe_layer(layer)) elif self.function_type() == 'qgis2.0': # convert for new style impact function self._hazard = SafeLayer(layer) else: message = tr('Error: Impact Function has unknown style.') raise Exception(message) # Update the target field to a non-conflicting one if self._hazard.is_qgsvectorlayer(): self._target_field = get_non_conflicting_attribute_name( self.target_field, self._hazard.layer.dataProvider().fieldNameMap().keys()) @property def exposure(self): """Property for the exposure layer to be used for the analysis. :returns: A map layer. :rtype: SafeLayer """ return self._exposure @exposure.setter def exposure(self, layer): """Setter for exposure layer property. :param layer: exposure layer to be used for the analysis. :type layer: SafeLayer """ if isinstance(layer, SafeLayer): self._exposure = layer else: if self.function_type() == 'old-style': self._exposure = SafeLayer(convert_to_safe_layer(layer)) elif self.function_type() == 'qgis2.0': # convert for new style impact function self._exposure = SafeLayer(layer) else: message = tr('Error: Impact Function has unknown style.') raise Exception(message) # Update the target field to a non-conflicting one if self.exposure.is_qgsvectorlayer(): self._target_field = get_non_conflicting_attribute_name( self.target_field, self.exposure.layer.dataProvider().fieldNameMap().keys()) @property def aggregation(self): """Property for the aggregation layer to be used for the analysis. :returns: A map layer. :rtype: SafeLayer """ return self._aggregation @aggregation.setter def aggregation(self, layer): """Setter for aggregation layer property. :param layer: Aggregation layer to be used for the analysis. :type layer: SafeLayer """ if isinstance(layer, SafeLayer): self._aggregation = layer elif isinstance(layer, QgsMapLayer): self._aggregation = SafeLayer(layer) else: self._aggregation = None @property def parameters(self): """Get the parameter.""" return self._parameters @parameters.setter def parameters(self, parameters): """Set the parameter. :param parameters: IF parameters. :type parameters: dict """ self._parameters = parameters @property def clip_hard(self): """Property if we need to clip features which are beyond the extents. :return: The value. :rtype: bool """ return self._clip_hard @clip_hard.setter def clip_hard(self, clip_hard): """Setter if we need to clip features which are beyond the extents. :param clip_hard: The value. :type clip_hard: bool """ if isinstance(clip_hard, bool): self._clip_hard = clip_hard else: raise Exception('clip_hard is not a boolean.') @property def show_intermediate_layers(self): """Property if we show intermediate layers. :return: The value. :rtype: bool """ return self._show_intermediate_layers @show_intermediate_layers.setter def show_intermediate_layers(self, show_intermediate_layers): """Setter if we show intermediate layers. :param show_intermediate_layers: The value. :type show_intermediate_layers: bool """ if isinstance(show_intermediate_layers, bool): self._show_intermediate_layers = show_intermediate_layers else: raise Exception('show_intermediate_layers is not a boolean.') @property def force_memory(self): """Property if we force memory. :return: The value. :rtype: bool """ return self._force_memory @force_memory.setter def force_memory(self, flag): """Setter if we force memory. :param flag: The value. :type flag: bool """ if isinstance(flag, bool): self._force_memory = flag else: raise Exception('force_memory is not a boolean.') @property def impact(self): """Property for the impact layer generated by the analysis. .. note:: It is not guaranteed that all impact functions produce a spatial layer. :returns: A map layer. :rtype: QgsMapLayer, QgsVectorLayer, QgsRasterLayer """ return self._impact @property def requires_clipping(self): """Check to clip or not to clip layers. If function type is a 'qgis2.0' impact function, then return False -- clipping is unnecessary, else return True. :returns: To clip or not to clip. :rtype: bool """ if self.function_type() == 'old-style': return True elif self.function_type() == 'qgis2.0': return False else: message = tr('Error: Impact Function has unknown style.') raise Exception(message) @property def target_field(self): """Property for the target_field of the impact layer. :returns: The target field in the impact layer in case it's a vector. :rtype: basestring """ return self._target_field @property def tabulated_impact(self): """Property for the result (excluding GIS layer) of the analysis. This property is read only. :returns: A dictionary containing the analysis results. The format of the dictionary may vary between impact function but the following sections are expected: * title: A brief title for the results * headings: column headings for the results * totals: totals for all rows in the tabulation area * tabulation: detailed line items for the tabulation The returned dictionary is probably best described with a simple example:: Example to follow here.... :rtype: dict """ return self._tabulated_impact @property def style(self): """Property for the style for the impact layer. This property is read only. :returns: A dictionary containing the analysis style. Generally this should be an adjunct to the qml style applied to the impact layer so that other types of style (e.g. SLD) can be generated for the impact layer. :rtype: dict """ return self._impact_style @property def question(self): """Formulate the question for this impact function. This method produces a natural language question for this impact function derived from the following three inputs: * descriptive name of the hazard layer e.g. 'a flood like in January 2004' * descriptive name of the exposure layer e.g. 'people' * question statement in the impact function metadata e.g. 'will be affected'. These inputs will be concatenated into a string e.g.: "In the event of a flood like in January 2004, how many people will be affected." """ if self._question is None: function_title = self.metadata().as_dict()['title'] return (tr('In the event of %(hazard)s how many ' '%(exposure)s might %(impact)s') % { 'hazard': self.hazard.name.lower(), 'exposure': self.exposure.name.lower(), 'impact': function_title.lower() }) else: return self._question @question.setter def question(self, question): """Setter of the question. :param question: The question for the impact function. :type question: basestring """ if isinstance(question, basestring): self._question = question else: raise Exception('The question should be a basestring instance.') @staticmethod def console_progress_callback(current, maximum, message=None): """Simple console based callback implementation for tests. :param current: Current progress. :type current: int :param maximum: Maximum range (point at which task is complete. :type maximum: int :param message: Optional message to display in the progress bar :type message: str, QString """ # noinspection PyChainedComparisons if maximum > 1000 and current % 1000 != 0 and current != maximum: return if message is not None: print message print 'Task progress: %i of %i' % (current, maximum) def validate(self): """Validate things needed before running the analysis.""" # Set start time. self._start_time = datetime.now() # Validate that input layers are valid self.provenance.append_step( 'Validating Step', 'Impact function is validating the inputs.') if (self.hazard is None) or (self.exposure is None): message = tr( 'Ensure that hazard and exposure layers are all set before ' 'trying to run the impact function.') raise FunctionParametersError(message) # Validate extent, with the QGIS IF, we need requested_extent set if self.function_type() == 'qgis2.0' and self.requested_extent is None: message = tr( 'Impact Function with QGIS function type is used, but no ' 'extent is provided.') raise InvalidExtentError(message) def prepare(self): """Prepare this impact function for running the analysis. This method should normally be called in your concrete class's run method before it attempts to do any real processing. This method will do any needed house keeping such as: * checking that the exposure and hazard layers sufficiently overlap (post 3.1) * clipping or subselecting features from both layers such that only features / coverage within the actual analysis extent will be analysed (post 3.1) * raising errors if any untenable condition exists e.g. extent has no valid CRS. (post 3.1) We suggest to overload this method in your concrete class implementation so that it includes any impact function specific checks too. ..note: For 3.1, we will still do those preprocessing in analysis class. We will just need to check if the function_type is 'qgis2.0', it needs to have the extent set. # """ # Fixme : When Analysis.py will not exist anymore, we will uncomment. # self.emit_pre_run_message() self.provenance.append_step( 'Preparation Step', 'Impact function is being prepared to run the analysis.') def generate_impact_keywords(self, extra_keywords=None): """Obtain keywords for the impact layer. :param extra_keywords: Additional keywords from the analysis. :type extra_keywords: dict :returns: Impact layer's keywords. :rtype: dict """ keywords = { 'layer_purpose': 'impact', 'keyword_version': inasafe_keyword_version, 'if_provenance': self.provenance } if extra_keywords: keywords.update(extra_keywords) return keywords @property def provenance(self): """Get the provenances""" return self._provenances def set_if_provenance(self): """Set IF provenance step for the IF.""" data = { 'start_time': self._start_time, 'finish_time': datetime.now(), 'hazard_layer': self.hazard.keywords['title'], 'exposure_layer': self.exposure.keywords['title'], 'impact_function_id': self.metadata().as_dict()['id'], 'impact_function_version': '1.0', # TODO: Add IF version. 'host_name': self.host_name, 'user': self.user, 'qgis_version': QGis.QGIS_VERSION, 'gdal_version': gdal.__version__, 'qt_version': QT_VERSION_STR, 'pyqt_version': PYQT_VERSION_STR, 'os': platform.version(), 'inasafe_version': get_version(), # Temporary. # TODO: Update it later. 'exposure_pixel_size': '', 'hazard_pixel_size': '', 'impact_pixel_size': '', 'analysis_extent': '', 'parameter': '' } self.provenance.append_if_provenance_step( 'IF Provenance', 'Impact function\'s provenance.', timestamp=None, data=data) def emit_pre_run_message(self): """Inform the user about parameters before starting the processing.""" title = tr('Processing started') details = tr( 'Please wait - processing may take a while depending on your ' 'hardware configuration and the analysis extents and data.') # trap for issue 706 try: exposure_name = self.exposure.name hazard_name = self.hazard.name # aggregation layer could be set to AOI so no check for that except AttributeError: title = tr('No valid layers') details = tr( 'Please ensure your hazard and exposure layers are set ' 'in the question area and then press run again.') message = m.Message(LOGO_ELEMENT, m.Heading(title, **WARNING_STYLE), m.Paragraph(details)) raise NoValidLayerError(message) text = m.Text( tr('This analysis will calculate the impact of'), m.EmphasizedText(hazard_name), tr('on'), m.EmphasizedText(exposure_name), ) if self.aggregation is not None: try: aggregation_name = self.aggregation.name # noinspection PyTypeChecker text.add( m.Text(tr('and bullet list the results'), m.ImportantText(tr('aggregated by')), m.EmphasizedText(aggregation_name))) except AttributeError: pass text.add('.') message = m.Message(LOGO_ELEMENT, m.Heading(title, **PROGRESS_UPDATE_STYLE), m.Paragraph(details), m.Paragraph(text)) try: # add which postprocessors will run when appropriated # noinspection PyTypeChecker post_processors_names = self.parameters['postprocessors'] post_processors = get_postprocessors(post_processors_names) message.add( m.Paragraph(tr('The following postprocessors will be used:'))) bullet_list = m.BulletedList() for name, post_processor in post_processors.iteritems(): bullet_list.add('%s: %s' % (get_postprocessor_human_name(name), post_processor.description())) message.add(bullet_list) except (TypeError, KeyError): # TypeError is for when function_parameters is none # KeyError is for when ['postprocessors'] is unavailable pass send_static_message(self, message) @property def clip_parameters(self): """Calculate the best extents to use for the assessment. :returns: A dictionary consisting of: * extra_exposure_keywords: dict - any additional keywords that should be written to the exposure layer. For example if rescaling is required for a raster, the original resolution can be added to the keywords file. * adjusted_geo_extent: list - [xmin, ymin, xmax, ymax] - the best extent that can be used given the input datasets and the current viewport extents. * cell_size: float - the cell size that is the best of the hazard and exposure rasters. :rtype: dict, QgsRectangle, float, QgsMapLayer, QgsRectangle, QgsMapLayer :raises: InsufficientOverlapError """ if self._clip_parameters is None: # Get the Hazard extents as an array in EPSG:4326 # noinspection PyTypeChecker hazard_geoextent = extent_to_array(self.hazard.extent(), self.hazard.crs()) # Get the Exposure extents as an array in EPSG:4326 # noinspection PyTypeChecker exposure_geoextent = extent_to_array(self.exposure.extent(), self.exposure.crs()) # Set the analysis extents based on user's desired behaviour settings = QSettings() mode_name = settings.value('inasafe/analysis_extents_mode', 'HazardExposureView') # Default to using canvas extents if no case below matches analysis_geoextent = self.viewport_extent if mode_name == 'HazardExposureView': analysis_geoextent = self.viewport_extent elif mode_name == 'HazardExposure': analysis_geoextent = None elif mode_name == 'HazardExposureBookmark' or \ mode_name == 'HazardExposureBoundingBox': if self.requested_extent is not None \ and self.requested_extent_crs is not None: # User has defined preferred extent, so use that analysis_geoextent = array_to_geo_array( self.requested_extent, self.requested_extent_crs) # Now work out the optimal extent between the two layers and # the current view extent. The optimal extent is the intersection # between the two layers and the viewport. try: # Extent is returned as an array [xmin,ymin,xmax,ymax] # We will convert it to a QgsRectangle afterwards. # If the user has defined a preferred analysis extent it will # always be used, otherwise the data will be clipped to # the viewport unless the user has deselected clip to viewport # in options. geo_extent = get_optimal_extent(hazard_geoextent, exposure_geoextent, analysis_geoextent) except InsufficientOverlapError, e: # noinspection PyTypeChecker message = generate_insufficient_overlap_message( e, exposure_geoextent, self.exposure.qgis_layer(), hazard_geoextent, self.hazard.qgis_layer(), analysis_geoextent) raise InsufficientOverlapError(message) # TODO: move this to its own function # Next work out the ideal spatial resolution for rasters # in the analysis. If layers are not native WGS84, we estimate # this based on the geographic extents # rather than the layers native extents so that we can pass # the ideal WGS84 cell size and extents to the layer prep routines # and do all preprocessing in a single operation. # All this is done in the function getWGS84resolution adjusted_geo_extent = geo_extent cell_size = None extra_exposure_keywords = {} if self.hazard.layer_type() == QgsMapLayer.RasterLayer: # Hazard layer is raster hazard_geo_cell_size, _ = get_wgs84_resolution( self.hazard.qgis_layer()) if self.exposure.layer_type() == QgsMapLayer.RasterLayer: # In case of two raster layers establish common resolution exposure_geo_cell_size, _ = get_wgs84_resolution( self.exposure.qgis_layer()) # See issue #1008 - the flag below is used to indicate # if the user wishes to prevent resampling of exposure data keywords = self.exposure.keywords allow_resampling_flag = True if 'allow_resampling' in keywords: resampling_lower = keywords['allow_resampling'].lower() allow_resampling_flag = resampling_lower == 'true' if hazard_geo_cell_size < exposure_geo_cell_size and \ allow_resampling_flag: cell_size = hazard_geo_cell_size # Adjust the geo extent to coincide with hazard grids # so gdalwarp can do clipping properly adjusted_geo_extent = adjust_clip_extent( geo_extent, get_wgs84_resolution(self.hazard.qgis_layer()), hazard_geoextent) else: cell_size = exposure_geo_cell_size # Adjust extent to coincide with exposure grids # so gdalwarp can do clipping properly adjusted_geo_extent = adjust_clip_extent( geo_extent, get_wgs84_resolution(self.exposure.qgis_layer()), exposure_geoextent) # Record native resolution to allow rescaling of exposure if not numpy.allclose(cell_size, exposure_geo_cell_size): extra_exposure_keywords['resolution'] = \ exposure_geo_cell_size else: if self.exposure.layer_type() != QgsMapLayer.VectorLayer: raise RuntimeError # In here we do not set cell_size so that in # _clip_raster_layer we can perform gdalwarp without # specifying cell size as we still want to have the # original pixel size. # Adjust the geo extent to be at the edge of the pixel in # so gdalwarp can do clipping properly adjusted_geo_extent = adjust_clip_extent( geo_extent, get_wgs84_resolution(self.hazard.qgis_layer()), hazard_geoextent) # If exposure is vector data grow hazard raster layer to # ensure there are enough pixels for points at the edge of # the view port to be interpolated correctly. This requires # resolution to be available adjusted_geo_extent = get_buffered_extent( adjusted_geo_extent, get_wgs84_resolution(self.hazard.qgis_layer())) else: # Hazard layer is vector # In case hazard data is a point data set, we will need to set # the geo_extent to the extent of exposure and the analysis # extent. We check the extent first if the point extent # intersects with geo_extent. if self.hazard.geometry_type() == QGis.Point: user_extent_enabled = (self.requested_extent is not None and self.requested_extent_crs is not None) if user_extent_enabled: # Get intersection between exposure and analysis extent geo_extent = bbox_intersection(exposure_geoextent, analysis_geoextent) # Check if the point is within geo_extent if bbox_intersection(geo_extent, exposure_geoextent) is None: raise InsufficientOverlapError else: geo_extent = exposure_geoextent adjusted_geo_extent = geo_extent if self.exposure.layer_type() == QgsMapLayer.RasterLayer: # Adjust the geo extent to be at the edge of the pixel in # so gdalwarp can do clipping properly adjusted_geo_extent = adjust_clip_extent( geo_extent, get_wgs84_resolution(self.exposure.qgis_layer()), exposure_geoextent) self._clip_parameters = { 'extra_exposure_keywords': extra_exposure_keywords, 'adjusted_geo_extent': adjusted_geo_extent, 'cell_size': cell_size } return self._clip_parameters
class ImpactFunction(object): """Abstract base class for all impact functions.""" # Class properties _metadata = ImpactFunctionMetadata def __init__(self): """Base class constructor. All derived classes should normally call this constructor e.g.:: def __init__(self): super(FloodImpactFunction, self).__init__() """ # User who runs this self._user = getpass.getuser().replace(' ', '_') # The host that runs this self._host_name = gethostname() # Requested extent to use self._requested_extent = None # Requested extent's CRS self._requested_extent_crs = QgsCoordinateReferenceSystem('EPSG:4326') # The current viewport extent of the map canvas self._viewport_extent = None # Actual extent to use - Read Only # For 'old-style' IF we do some manipulation to the requested extent self._actual_extent = None # Actual extent's CRS - Read Only self._actual_extent_crs = QgsCoordinateReferenceSystem('EPSG:4326') # set this to a gui call back / web callback etc as needed. self._callback = self.console_progress_callback # Set the default parameters self._parameters = self._metadata.parameters() # Layer representing hazard e.g. flood self._hazard = None # Layer representing people / infrastructure that are exposed self._exposure = None # Layer used for aggregating results by area / district self._aggregation = None # The best extents to use for the assessment self._clip_parameters = None # Clip features that extend beyond the extents. self._clip_hard = False # Show intermediate layers. self._show_intermediate_layers = False # Force memory. self._force_memory = False # Layer produced by the impact function self._impact = None # The question of the impact function self._question = None # Post analysis Result dictionary (suitable to conversion to json etc.) self._tabulated_impact = None # Style information for the impact layer - at some point we should # formalise this into a more natural model # ABC's will normally set this property. self._impact_style = None # The target field for vector impact layer self._target_field = 'safe_ag' # The string to mark not affected value in the vector impact layer self._not_affected_value = 'Not Affected' # Store provenances self._provenances = Provenance() # Start time self._start_time = None self.provenance.append_step( 'Initialize Impact Function', 'Impact function is being initialized') @classmethod def metadata(cls): """Get the metadata class of this impact function.""" return cls._metadata @classmethod def function_type(cls): """Property for the type of impact function ('old-style' or 'qgis2.0'). QGIS2 impact functions are using the QGIS api and have more dependencies. Legacy IF's use only numpy, gdal etc. and can be used in contexts where no QGIS is present. """ return cls.metadata().as_dict().get('function_type', None) @property def user(self): """Property for the user who runs this. :returns: User who runs this :rtype: basestring """ return self._user @property def host_name(self): """Property for the host name that runs this. :returns: The host name. :rtype: basestring """ return self._host_name @property def requested_extent(self): """Property for the extent of impact function analysis. :returns: A list in the form [xmin, ymin, xmax, ymax]. :rtype: list """ return self._requested_extent @requested_extent.setter def requested_extent(self, extent): """Setter for extent property. :param extent: Analysis boundaries expressed as [xmin, ymin, xmax, ymax]. The extent CRS should match the extent_crs property of this IF instance. :type extent: list """ # add more robust checks here if len(extent) != 4: raise InvalidExtentError('%s is not a valid extent.' % extent) self._requested_extent = extent @property def requested_extent_crs(self): """Property for the extent CRS of impact function analysis. :return crs: The coordinate reference system for the analysis boundary. :rtype: QgsCoordinateReferenceSystem """ return self._requested_extent_crs @requested_extent_crs.setter def requested_extent_crs(self, crs): """Setter for extent_crs property. .. note:: We break our rule here on not allowing acronyms for parameter names. :param crs: The coordinate reference system for the analysis boundary. :type crs: QgsCoordinateReferenceSystem """ self._requested_extent_crs = crs @property def actual_extent(self): """Property for the actual extent for analysis. :returns: A list in the form [xmin, ymin, xmax, ymax]. :rtype: list """ return self._actual_extent @property def actual_extent_crs(self): """Property for the actual extent crs for analysis. :returns: The CRS for the actual extent. :rtype: QgsCoordinateReferenceSystem """ return self._actual_extent_crs @property def viewport_extent(self): """Property for the viewport extent of the map canvas. :returns: Viewport extent in the form [xmin, ymin, xmax, ymax] where all coordinates provided are in Geographic / EPSG:4326. :rtype: list """ return self._viewport_extent @viewport_extent.setter def viewport_extent(self, viewport_extent): """Setter for the viewport extent of the map canvas. :param viewport_extent: Viewport extent in the form [xmin, ymin, xmax, ymax] in EPSG:4326. :type viewport_extent: list """ self._viewport_extent = viewport_extent @property def callback(self): """Property for the callback used to relay processing progress. :returns: A callback function. The callback function will have the following parameter requirements. progress_callback(current, maximum, message=None) :rtype: function .. seealso:: console_progress_callback """ return self._callback @callback.setter def callback(self, callback): """Setter for callback property. :param callback: A callback function reference that provides the following signature: progress_callback(current, maximum, message=None) :type callback: function """ self._callback = callback @classmethod def instance(cls): """Make an instance of the impact function.""" return cls() @property def hazard(self): """Property for the hazard layer to be used for the analysis. :returns: A map layer. :rtype: SafeLayer """ return self._hazard @hazard.setter def hazard(self, layer): """Setter for hazard layer property. :param layer: Hazard layer to be used for the analysis. :type layer: SafeLayer, Layer, QgsMapLayer """ if isinstance(layer, SafeLayer): self._hazard = layer else: if self.function_type() == 'old-style': self._hazard = SafeLayer(convert_to_safe_layer(layer)) elif self.function_type() == 'qgis2.0': # convert for new style impact function self._hazard = SafeLayer(layer) else: message = tr('Error: Impact Function has unknown style.') raise Exception(message) # Update the target field to a non-conflicting one if self._hazard.is_qgsvectorlayer(): self._target_field = get_non_conflicting_attribute_name( self.target_field, self._hazard.layer.dataProvider().fieldNameMap().keys() ) @property def exposure(self): """Property for the exposure layer to be used for the analysis. :returns: A map layer. :rtype: SafeLayer """ return self._exposure @exposure.setter def exposure(self, layer): """Setter for exposure layer property. :param layer: exposure layer to be used for the analysis. :type layer: SafeLayer """ if isinstance(layer, SafeLayer): self._exposure = layer else: if self.function_type() == 'old-style': self._exposure = SafeLayer(convert_to_safe_layer(layer)) elif self.function_type() == 'qgis2.0': # convert for new style impact function self._exposure = SafeLayer(layer) else: message = tr('Error: Impact Function has unknown style.') raise Exception(message) # Update the target field to a non-conflicting one if self.exposure.is_qgsvectorlayer(): self._target_field = get_non_conflicting_attribute_name( self.target_field, self.exposure.layer.dataProvider().fieldNameMap().keys() ) @property def aggregation(self): """Property for the aggregation layer to be used for the analysis. :returns: A map layer. :rtype: SafeLayer """ return self._aggregation @aggregation.setter def aggregation(self, layer): """Setter for aggregation layer property. :param layer: Aggregation layer to be used for the analysis. :type layer: SafeLayer """ if isinstance(layer, SafeLayer): self._aggregation = layer elif isinstance(layer, QgsMapLayer): self._aggregation = SafeLayer(layer) else: self._aggregation = None @property def parameters(self): """Get the parameter.""" return self._parameters @parameters.setter def parameters(self, parameters): """Set the parameter. :param parameters: IF parameters. :type parameters: dict """ self._parameters = parameters @property def clip_hard(self): """Property if we need to clip features which are beyond the extents. :return: The value. :rtype: bool """ return self._clip_hard @clip_hard.setter def clip_hard(self, clip_hard): """Setter if we need to clip features which are beyond the extents. :param clip_hard: The value. :type clip_hard: bool """ if isinstance(clip_hard, bool): self._clip_hard = clip_hard else: raise Exception('clip_hard is not a boolean.') @property def show_intermediate_layers(self): """Property if we show intermediate layers. :return: The value. :rtype: bool """ return self._show_intermediate_layers @show_intermediate_layers.setter def show_intermediate_layers(self, show_intermediate_layers): """Setter if we show intermediate layers. :param show_intermediate_layers: The value. :type show_intermediate_layers: bool """ if isinstance(show_intermediate_layers, bool): self._show_intermediate_layers = show_intermediate_layers else: raise Exception('show_intermediate_layers is not a boolean.') @property def force_memory(self): """Property if we force memory. :return: The value. :rtype: bool """ return self._force_memory @force_memory.setter def force_memory(self, flag): """Setter if we force memory. :param flag: The value. :type flag: bool """ if isinstance(flag, bool): self._force_memory = flag else: raise Exception('force_memory is not a boolean.') @property def impact(self): """Property for the impact layer generated by the analysis. .. note:: It is not guaranteed that all impact functions produce a spatial layer. :returns: A map layer. :rtype: QgsMapLayer, QgsVectorLayer, QgsRasterLayer """ return self._impact @property def requires_clipping(self): """Check to clip or not to clip layers. If function type is a 'qgis2.0' impact function, then return False -- clipping is unnecessary, else return True. :returns: To clip or not to clip. :rtype: bool """ if self.function_type() == 'old-style': return True elif self.function_type() == 'qgis2.0': return False else: message = tr('Error: Impact Function has unknown style.') raise Exception(message) @property def target_field(self): """Property for the target_field of the impact layer. :returns: The target field in the impact layer in case it's a vector. :rtype: basestring """ return self._target_field @property def tabulated_impact(self): """Property for the result (excluding GIS layer) of the analysis. This property is read only. :returns: A dictionary containing the analysis results. The format of the dictionary may vary between impact function but the following sections are expected: * title: A brief title for the results * headings: column headings for the results * totals: totals for all rows in the tabulation area * tabulation: detailed line items for the tabulation The returned dictionary is probably best described with a simple example:: Example to follow here.... :rtype: dict """ return self._tabulated_impact @property def style(self): """Property for the style for the impact layer. This property is read only. :returns: A dictionary containing the analysis style. Generally this should be an adjunct to the qml style applied to the impact layer so that other types of style (e.g. SLD) can be generated for the impact layer. :rtype: dict """ return self._impact_style @property def question(self): """Formulate the question for this impact function. This method produces a natural language question for this impact function derived from the following three inputs: * descriptive name of the hazard layer e.g. 'a flood like in January 2004' * descriptive name of the exposure layer e.g. 'people' * question statement in the impact function metadata e.g. 'will be affected'. These inputs will be concatenated into a string e.g.: "In the event of a flood like in January 2004, how many people will be affected." """ if self._question is None: function_title = self.metadata().as_dict()['title'] return (tr('In the event of %(hazard)s how many ' '%(exposure)s might %(impact)s') % {'hazard': self.hazard.name.lower(), 'exposure': self.exposure.name.lower(), 'impact': function_title.lower()}) else: return self._question @question.setter def question(self, question): """Setter of the question. :param question: The question for the impact function. :type question: basestring """ if isinstance(question, basestring): self._question = question else: raise Exception('The question should be a basestring instance.') @staticmethod def console_progress_callback(current, maximum, message=None): """Simple console based callback implementation for tests. :param current: Current progress. :type current: int :param maximum: Maximum range (point at which task is complete. :type maximum: int :param message: Optional message to display in the progress bar :type message: str, QString """ # noinspection PyChainedComparisons if maximum > 1000 and current % 1000 != 0 and current != maximum: return if message is not None: print message print 'Task progress: %i of %i' % (current, maximum) def validate(self): """Validate things needed before running the analysis.""" # Set start time. self._start_time = datetime.now() # Validate that input layers are valid self.provenance.append_step( 'Validating Step', 'Impact function is validating the inputs.') if (self.hazard is None) or (self.exposure is None): message = tr( 'Ensure that hazard and exposure layers are all set before ' 'trying to run the impact function.') raise FunctionParametersError(message) # Validate extent, with the QGIS IF, we need requested_extent set if self.function_type() == 'qgis2.0' and self.requested_extent is None: message = tr( 'Impact Function with QGIS function type is used, but no ' 'extent is provided.') raise InvalidExtentError(message) def prepare(self): """Prepare this impact function for running the analysis. This method should normally be called in your concrete class's run method before it attempts to do any real processing. This method will do any needed house keeping such as: * checking that the exposure and hazard layers sufficiently overlap (post 3.1) * clipping or subselecting features from both layers such that only features / coverage within the actual analysis extent will be analysed (post 3.1) * raising errors if any untenable condition exists e.g. extent has no valid CRS. (post 3.1) We suggest to overload this method in your concrete class implementation so that it includes any impact function specific checks too. ..note: For 3.1, we will still do those preprocessing in analysis class. We will just need to check if the function_type is 'qgis2.0', it needs to have the extent set. # """ # Fixme : When Analysis.py will not exist anymore, we will uncomment. # self.emit_pre_run_message() self.provenance.append_step( 'Preparation Step', 'Impact function is being prepared to run the analysis.') def generate_impact_keywords(self, extra_keywords=None): """Obtain keywords for the impact layer. :param extra_keywords: Additional keywords from the analysis. :type extra_keywords: dict :returns: Impact layer's keywords. :rtype: dict """ keywords = { 'layer_purpose': 'impact', 'keyword_version': inasafe_keyword_version, 'if_provenance': self.provenance } if extra_keywords: keywords.update(extra_keywords) return keywords @property def provenance(self): """Get the provenances""" return self._provenances def set_if_provenance(self): """Set IF provenance step for the IF.""" data = { 'start_time': self._start_time , 'finish_time': datetime.now(), 'hazard_layer': self.hazard.keywords['title'], 'exposure_layer': self.exposure.keywords['title'], 'impact_function_id': self.metadata().as_dict()['id'], 'impact_function_version': '1.0', # TODO: Add IF version. 'host_name': self.host_name, 'user': self.user, 'qgis_version': QGis.QGIS_VERSION, 'gdal_version': gdal.__version__, 'qt_version': QT_VERSION_STR, 'pyqt_version': PYQT_VERSION_STR, 'os': platform.version(), 'inasafe_version': get_version(), # Temporary. # TODO: Update it later. 'exposure_pixel_size': '', 'hazard_pixel_size': '', 'impact_pixel_size': '', 'analysis_extent': '', 'parameter': '' } self.provenance.append_if_provenance_step( 'IF Provenance', 'Impact function\'s provenance.', timestamp=None, data=data ) def emit_pre_run_message(self): """Inform the user about parameters before starting the processing.""" title = tr('Processing started') details = tr( 'Please wait - processing may take a while depending on your ' 'hardware configuration and the analysis extents and data.') # trap for issue 706 try: exposure_name = self.exposure.name hazard_name = self.hazard.name # aggregation layer could be set to AOI so no check for that except AttributeError: title = tr('No valid layers') details = tr( 'Please ensure your hazard and exposure layers are set ' 'in the question area and then press run again.') message = m.Message( LOGO_ELEMENT, m.Heading(title, **WARNING_STYLE), m.Paragraph(details)) raise NoValidLayerError(message) text = m.Text( tr('This analysis will calculate the impact of'), m.EmphasizedText(hazard_name), tr('on'), m.EmphasizedText(exposure_name), ) if self.aggregation is not None: try: aggregation_name = self.aggregation.name # noinspection PyTypeChecker text.add(m.Text( tr('and bullet list the results'), m.ImportantText(tr('aggregated by')), m.EmphasizedText(aggregation_name))) except AttributeError: pass text.add('.') message = m.Message( LOGO_ELEMENT, m.Heading(title, **PROGRESS_UPDATE_STYLE), m.Paragraph(details), m.Paragraph(text)) try: # add which postprocessors will run when appropriated # noinspection PyTypeChecker post_processors_names = self.parameters['postprocessors'] post_processors = get_postprocessors(post_processors_names) message.add(m.Paragraph(tr( 'The following postprocessors will be used:'))) bullet_list = m.BulletedList() for name, post_processor in post_processors.iteritems(): bullet_list.add('%s: %s' % ( get_postprocessor_human_name(name), post_processor.description())) message.add(bullet_list) except (TypeError, KeyError): # TypeError is for when function_parameters is none # KeyError is for when ['postprocessors'] is unavailable pass send_static_message(self, message) @property def clip_parameters(self): """Calculate the best extents to use for the assessment. :returns: A dictionary consisting of: * extra_exposure_keywords: dict - any additional keywords that should be written to the exposure layer. For example if rescaling is required for a raster, the original resolution can be added to the keywords file. * adjusted_geo_extent: list - [xmin, ymin, xmax, ymax] - the best extent that can be used given the input datasets and the current viewport extents. * cell_size: float - the cell size that is the best of the hazard and exposure rasters. :rtype: dict, QgsRectangle, float, QgsMapLayer, QgsRectangle, QgsMapLayer :raises: InsufficientOverlapError """ if self._clip_parameters is None: # Get the Hazard extents as an array in EPSG:4326 # noinspection PyTypeChecker hazard_geoextent = extent_to_array( self.hazard.extent(), self.hazard.crs()) # Get the Exposure extents as an array in EPSG:4326 # noinspection PyTypeChecker exposure_geoextent = extent_to_array( self.exposure.extent(), self.exposure.crs()) # Set the analysis extents based on user's desired behaviour settings = QSettings() mode_name = settings.value( 'inasafe/analysis_extents_mode', 'HazardExposureView') # Default to using canvas extents if no case below matches analysis_geoextent = self.viewport_extent if mode_name == 'HazardExposureView': analysis_geoextent = self.viewport_extent elif mode_name == 'HazardExposure': analysis_geoextent = None elif mode_name == 'HazardExposureBookmark' or \ mode_name == 'HazardExposureBoundingBox': if self.requested_extent is not None \ and self.requested_extent_crs is not None: # User has defined preferred extent, so use that analysis_geoextent = array_to_geo_array( self.requested_extent, self.requested_extent_crs) # Now work out the optimal extent between the two layers and # the current view extent. The optimal extent is the intersection # between the two layers and the viewport. try: # Extent is returned as an array [xmin,ymin,xmax,ymax] # We will convert it to a QgsRectangle afterwards. # If the user has defined a preferred analysis extent it will # always be used, otherwise the data will be clipped to # the viewport unless the user has deselected clip to viewport # in options. geo_extent = get_optimal_extent( hazard_geoextent, exposure_geoextent, analysis_geoextent) except InsufficientOverlapError, e: # noinspection PyTypeChecker message = generate_insufficient_overlap_message( e, exposure_geoextent, self.exposure.qgis_layer(), hazard_geoextent, self.hazard.qgis_layer(), analysis_geoextent) raise InsufficientOverlapError(message) # TODO: move this to its own function # Next work out the ideal spatial resolution for rasters # in the analysis. If layers are not native WGS84, we estimate # this based on the geographic extents # rather than the layers native extents so that we can pass # the ideal WGS84 cell size and extents to the layer prep routines # and do all preprocessing in a single operation. # All this is done in the function getWGS84resolution adjusted_geo_extent = geo_extent cell_size = None extra_exposure_keywords = {} if self.hazard.layer_type() == QgsMapLayer.RasterLayer: # Hazard layer is raster hazard_geo_cell_size, _ = get_wgs84_resolution( self.hazard.qgis_layer()) if self.exposure.layer_type() == QgsMapLayer.RasterLayer: # In case of two raster layers establish common resolution exposure_geo_cell_size, _ = get_wgs84_resolution( self.exposure.qgis_layer()) # See issue #1008 - the flag below is used to indicate # if the user wishes to prevent resampling of exposure data keywords = self.exposure.keywords allow_resampling_flag = True if 'allow_resampling' in keywords: resampling_lower = keywords['allow_resampling'].lower() allow_resampling_flag = resampling_lower == 'true' if hazard_geo_cell_size < exposure_geo_cell_size and \ allow_resampling_flag: cell_size = hazard_geo_cell_size # Adjust the geo extent to coincide with hazard grids # so gdalwarp can do clipping properly adjusted_geo_extent = adjust_clip_extent( geo_extent, get_wgs84_resolution(self.hazard.qgis_layer()), hazard_geoextent) else: cell_size = exposure_geo_cell_size # Adjust extent to coincide with exposure grids # so gdalwarp can do clipping properly adjusted_geo_extent = adjust_clip_extent( geo_extent, get_wgs84_resolution(self.exposure.qgis_layer()), exposure_geoextent) # Record native resolution to allow rescaling of exposure if not numpy.allclose(cell_size, exposure_geo_cell_size): extra_exposure_keywords['resolution'] = \ exposure_geo_cell_size else: if self.exposure.layer_type() != QgsMapLayer.VectorLayer: raise RuntimeError # In here we do not set cell_size so that in # _clip_raster_layer we can perform gdalwarp without # specifying cell size as we still want to have the # original pixel size. # Adjust the geo extent to be at the edge of the pixel in # so gdalwarp can do clipping properly adjusted_geo_extent = adjust_clip_extent( geo_extent, get_wgs84_resolution(self.hazard.qgis_layer()), hazard_geoextent) # If exposure is vector data grow hazard raster layer to # ensure there are enough pixels for points at the edge of # the view port to be interpolated correctly. This requires # resolution to be available adjusted_geo_extent = get_buffered_extent( adjusted_geo_extent, get_wgs84_resolution(self.hazard.qgis_layer())) else: # Hazard layer is vector # In case hazard data is a point data set, we will need to set # the geo_extent to the extent of exposure and the analysis # extent. We check the extent first if the point extent # intersects with geo_extent. if self.hazard.geometry_type() == QGis.Point: user_extent_enabled = ( self.requested_extent is not None and self.requested_extent_crs is not None) if user_extent_enabled: # Get intersection between exposure and analysis extent geo_extent = bbox_intersection( exposure_geoextent, analysis_geoextent) # Check if the point is within geo_extent if bbox_intersection( geo_extent, exposure_geoextent) is None: raise InsufficientOverlapError else: geo_extent = exposure_geoextent adjusted_geo_extent = geo_extent if self.exposure.layer_type() == QgsMapLayer.RasterLayer: # Adjust the geo extent to be at the edge of the pixel in # so gdalwarp can do clipping properly adjusted_geo_extent = adjust_clip_extent( geo_extent, get_wgs84_resolution(self.exposure.qgis_layer()), exposure_geoextent) self._clip_parameters = { 'extra_exposure_keywords': extra_exposure_keywords, 'adjusted_geo_extent': adjusted_geo_extent, 'cell_size': cell_size } return self._clip_parameters
class ImpactFunction(object): """Abstract base class for all impact functions.""" # Class properties _metadata = ImpactFunctionMetadata def __init__(self): """Base class constructor. All derived classes should normally call this constructor e.g.:: def __init__(self): super(FloodImpactFunction, self).__init__() """ # User who runs this self._user = getpass.getuser().replace(' ', '_') # The host that runs this self._host_name = gethostname() # Requested extent to use self._requested_extent = None # Requested extent's CRS as EPSG number self._requested_extent_crs = 4326 # Actual extent to use - Read Only # For 'old-style' IF we do some manipulation to the requested extent self._actual_extent = None # Actual extent's CRS as EPSG number - Read Only self._actual_extent_crs = 4326 # set this to a gui call back / web callback etc as needed. self._callback = self.console_progress_callback # Set the default parameters self._parameters = self._metadata.parameters() # Layer representing hazard e.g. flood self._hazard = None # Layer representing people / infrastructure that are exposed self._exposure = None # Layer used for aggregating results by area / district self._aggregation = None # Layer produced by the impact function self._impact = None # The question of the impact function self._question = None # Post analysis Result dictionary (suitable to conversion to json etc.) self._tabulated_impact = None # Style information for the impact layer - at some point we should # formalise this into a more natural model # ABC's will normally set this property. self._impact_style = None # The target field for vector impact layer self._target_field = 'safe_ag' # The string to mark not affected value in the vector impact layer self._not_affected_value = 'Not Affected' @classmethod def metadata(cls): """Get the metadata class of this impact function.""" return cls._metadata @classmethod def function_type(cls): """Property for the type of impact function ('old-style' or 'qgis2.0'). QGIS2 impact functions are using the QGIS api and have more dependencies. Legacy IF's use only numpy, gdal etc. and can be used in contexts where no QGIS is present. """ return cls.metadata().as_dict().get('function_type', None) @classmethod def function_category(cls): """Property for function category based on hazard categories. Function category could be 'single_event' or/and 'multiple_event'. Single event data type means that the data is captured by a single observation, while 'multiple_event' has been aggregated for some observations. :returns: The hazard categories that this function supports. :rtype: list """ return cls.metadata().as_dict().get('layer_requirements').get( 'hazard').get('hazard_categories') @property def user(self): """Property for the user who runs this. :returns: User who runs this :rtype: basestring """ return self._user @property def host_name(self): """Property for the host name that runs this. :returns: The host name. :rtype: basestring """ return self._host_name @property def requested_extent(self): """Property for the extent of impact function analysis. :returns: A list in the form [xmin, ymin, xmax, ymax]. :rtype: list """ return self._requested_extent @requested_extent.setter def requested_extent(self, extent): """Setter for extent property. :param extent: Analysis boundaries expressed as [xmin, ymin, xmax, ymax]. The extent CRS should match the extent_crs property of this IF instance. :type extent: list """ # add more robust checks here if len(extent) != 4: raise InvalidExtentError('%s is not a valid extent.' % extent) self._requested_extent = extent @property def requested_extent_crs(self): """Property for the extent CRS of impact function analysis. :returns: A number representing the EPSG code for the CRS. e.g. 4326 :rtype: int """ return self._requested_extent_crs @requested_extent_crs.setter def requested_extent_crs(self, crs): """Setter for extent_crs property. .. note:: We break our rule here on not allowing acronyms for parameter names. :param crs: Analysis boundary EPSG CRS expressed as an integer. :type crs: int """ self._requested_extent_crs = crs @property def actual_extent(self): """Property for the actual extent for analysis. :returns: A list in the form [xmin, ymin, xmax, ymax]. :rtype: list """ return self._actual_extent @property def actual_extent_crs(self): """Property for the actual extent crs for analysis. :returns: A number representing the EPSG code for the CRS. e.g. 4326 :rtype: int """ return self._actual_extent_crs @property def callback(self): """Property for the callback used to relay processing progress. :returns: A callback function. The callback function will have the following parameter requirements. progress_callback(current, maximum, message=None) :rtype: function .. seealso:: console_progress_callback """ return self._callback @callback.setter def callback(self, callback): """Setter for callback property. :param callback: A callback function reference that provides the following signature: progress_callback(current, maximum, message=None) :type callback: function """ self._callback = callback @classmethod def instance(cls): """Make an instance of the impact function.""" return cls() @property def hazard(self): """Property for the hazard layer to be used for the analysis. :returns: A map layer. :rtype: SafeLayer """ return self._hazard @hazard.setter def hazard(self, layer): """Setter for hazard layer property. :param layer: Hazard layer to be used for the analysis. :type layer: SafeLayer, Layer, QgsMapLayer """ if isinstance(layer, SafeLayer): self._hazard = layer else: if self.function_type() == 'old-style': self._hazard = SafeLayer(convert_to_safe_layer(layer)) elif self.function_type() == 'qgis2.0': # convert for new style impact function self._hazard = SafeLayer(layer) else: message = tr('Error: Impact Function has unknown style.') raise Exception(message) # Update the target field to a non-conflicting one if self._hazard.is_qgsvectorlayer(): self._target_field = get_non_conflicting_attribute_name( self.target_field, self._hazard.layer.dataProvider().fieldNameMap().keys() ) @property def exposure(self): """Property for the exposure layer to be used for the analysis. :returns: A map layer. :rtype: SafeLayer """ return self._exposure @exposure.setter def exposure(self, layer): """Setter for exposure layer property. :param layer: exposure layer to be used for the analysis. :type layer: SafeLayer """ if isinstance(layer, SafeLayer): self._exposure = layer else: if self.function_type() == 'old-style': self._exposure = SafeLayer(convert_to_safe_layer(layer)) elif self.function_type() == 'qgis2.0': # convert for new style impact function self._exposure = SafeLayer(layer) else: message = tr('Error: Impact Function has unknown style.') raise Exception(message) # Update the target field to a non-conflicting one if self.exposure.is_qgsvectorlayer(): self._target_field = get_non_conflicting_attribute_name( self.target_field, self.exposure.layer.dataProvider().fieldNameMap().keys() ) @property def aggregation(self): """Property for the aggregation layer to be used for the analysis. :returns: A map layer. :rtype: SafeLayer """ return self._aggregation @aggregation.setter def aggregation(self, layer): """Setter for aggregation layer property. :param layer: Aggregation layer to be used for the analysis. :type layer: SafeLayer """ # add more robust checks here self._aggregation = layer @property def parameters(self): """Get the parameter.""" return self._parameters @parameters.setter def parameters(self, parameters): """Set the parameter. :param parameters: IF parameters. :type parameters: dict """ self._parameters = parameters @property def impact(self): """Property for the impact layer generated by the analysis. .. note:: It is not guaranteed that all impact functions produce a spatial layer. :returns: A map layer. :rtype: QgsMapLayer, QgsVectorLayer, QgsRasterLayer """ return self._impact @property def requires_clipping(self): """Check to clip or not to clip layers. If function type is a 'qgis2.0' impact function, then return False -- clipping is unnecessary, else return True. :returns: To clip or not to clip. :rtype: bool """ if self.function_type() == 'old-style': return True elif self.function_type() == 'qgis2.0': return False else: message = tr('Error: Impact Function has unknown style.') raise Exception(message) @property def target_field(self): """Property for the target_field of the impact layer. :returns: The target field in the impact layer in case it's a vector. :rtype: basestring """ return self._target_field @property def tabulated_impact(self): """Property for the result (excluding GIS layer) of the analysis. This property is read only. :returns: A dictionary containing the analysis results. The format of the dictionary may vary between impact function but the following sections are expected: * title: A brief title for the results * headings: column headings for the results * totals: totals for all rows in the tabulation area * tabulation: detailed line items for the tabulation The returned dictionary is probably best described with a simple example:: Example to follow here.... :rtype: dict """ return self._tabulated_impact @property def style(self): """Property for the style for the impact layer. This property is read only. :returns: A dictionary containing the analysis style. Generally this should be an adjunct to the qml style applied to the impact layer so that other types of style (e.g. SLD) can be generated for the impact layer. :rtype: dict """ return self._impact_style @property def question(self): """Formulate the question for this impact function. This method produces a natural language question for this impact function derived from the following three inputs: * descriptive name of the hazard layer e.g. 'a flood like in January 2004' * descriptive name of the exposure layer e.g. 'people' * question statement in the impact function metadata e.g. 'will be affected'. These inputs will be concatenated into a string e.g.: "In the event of a flood like in January 2004, how many people will be affected." """ if self._question is None: function_title = self.metadata().as_dict()['title'] return (tr('In the event of %(hazard)s how many ' '%(exposure)s might %(impact)s') % {'hazard': self.hazard.name.lower(), 'exposure': self.exposure.name.lower(), 'impact': function_title.lower()}) else: return self._question @question.setter def question(self, question): """Setter of the question. :param question: The question for the impact function. :type question: basestring """ if isinstance(question, basestring): self._question = question else: raise Exception('The question should be a basestring instance.') @staticmethod def console_progress_callback(current, maximum, message=None): """Simple console based callback implementation for tests. :param current: Current progress. :type current: int :param maximum: Maximum range (point at which task is complete. :type maximum: int :param message: Optional message to display in the progress bar :type message: str, QString """ # noinspection PyChainedComparisons if maximum > 1000 and current % 1000 != 0 and current != maximum: return if message is not None: print message print 'Task progress: %i of %i' % (current, maximum) def validate(self): """Validate things needed before running the analysis.""" # Validate that input layers are valid if (self.hazard is None) or (self.exposure is None): message = tr( 'Ensure that hazard and exposure layers are all set before ' 'trying to run the impact function.') raise FunctionParametersError(message) # Validate extent, with the QGIS IF, we need requested_extent set if self.function_type() == 'qgis2.0' and self.requested_extent is None: message = tr( 'Impact Function with QGIS function type is used, but no ' 'extent is provided.') raise InvalidExtentError(message) def prepare(self): """Prepare this impact function for running the analysis. This method should normally be called in your concrete class's run method before it attempts to do any real processing. This method will do any needed house keeping such as: * checking that the exposure and hazard layers sufficiently overlap (post 3.1) * clipping or subselecting features from both layers such that only features / coverage within the actual analysis extent will be analysed (post 3.1) * raising errors if any untenable condition exists e.g. extent has no valid CRS. (post 3.1) We suggest to overload this method in your concrete class implementation so that it includes any impact function specific checks too. ..note: For 3.1, we will still do those preprocessing in analysis class. We will just need to check if the function_type is 'qgis2.0', it needs to have the extent set. # """ pass