def download(feature_type, output_base_path, extent, progress_dialog=None): """Download shapefiles from Kartoza server. .. versionadded:: 3.2 :param feature_type: What kind of features should be downloaded. Currently 'buildings', 'building-points' or 'roads' are supported. :type feature_type: str :param output_base_path: The base path of the shape file. :type output_base_path: str :param extent: A list in the form [xmin, ymin, xmax, ymax] where all coordinates provided are in Geographic / EPSG:4326. :type extent: list :param progress_dialog: A progress dialog. :type progress_dialog: QProgressDialog :raises: ImportDialogError, CanceledImportDialogError """ # preparing necessary data min_longitude = extent[0] min_latitude = extent[1] max_longitude = extent[2] max_latitude = extent[3] box = ( '{min_longitude},{min_latitude},{max_longitude},' '{max_latitude}').format( min_longitude=min_longitude, min_latitude=min_latitude, max_longitude=max_longitude, max_latitude=max_latitude ) url = ( '{url_osm_prefix}' '{feature_type}' '{url_osm_suffix}?' 'bbox={box}&' 'qgis_version={qgis}&' 'lang={lang}&' 'inasafe_version={inasafe_version}'.format( url_osm_prefix=URL_OSM_PREFIX, feature_type=feature_type, url_osm_suffix=URL_OSM_SUFFIX, box=box, qgis=qgis_version(), lang=locale(), inasafe_version=get_version())) path = tempfile.mktemp('.shp.zip') # download and extract it fetch_zip(url, path, feature_type, progress_dialog) extract_zip(path, output_base_path) if progress_dialog: progress_dialog.done(QDialog.Accepted)
def test_print_default_template(self): """Test printing report to pdf using default template works.""" impact_layer_path = test_data_path( 'impact', 'population_affected_entire_area.shp') layer, _ = load_layer(impact_layer_path) # noinspection PyUnresolvedReferences QgsMapLayerRegistry.instance().addMapLayer(layer) # noinspection PyCallingNonCallable rect = QgsRectangle(106.8194, -6.2108, 106.8201, -6.1964) CANVAS.setExtent(rect) CANVAS.refresh() template = resources_path( 'qgis-composer-templates', 'inasafe-portrait-a4.qpt') report = ImpactReport(IFACE, template, layer) out_path = unique_filename( prefix='map_default_template_test', suffix='.pdf', dir=temp_dir('test')) report.print_map_to_pdf(out_path) # Check the file exists message = 'Rendered output does not exist: %s' % out_path self.assertTrue(os.path.exists(out_path), message) # Check the file is not corrupt message = 'The output file %s is corrupt' % out_path out_size = os.stat(out_path).st_size self.assertTrue(out_size > 0, message) # Check the components in composition are default components if qgis_version() < 20500: safe_logo = report.composition.getComposerItemById( 'safe-logo').pictureFile() north_arrow = report.composition.getComposerItemById( 'north-arrow').pictureFile() org_logo = report.composition.getComposerItemById( 'organisation-logo').pictureFile() else: safe_logo = report.composition.getComposerItemById( 'safe-logo').picturePath() north_arrow = report.composition.getComposerItemById( 'north-arrow').picturePath() org_logo = report.composition.getComposerItemById( 'organisation-logo').picturePath() expected_safe_logo = resources_path( 'img', 'logos', 'inasafe-logo-url.svg') expected_north_arrow = resources_path( 'img', 'north_arrows', 'simple_north_arrow.png') expected_org_logo = resources_path('img', 'logos', 'supporters.png') message = 'The safe logo path is not the default one' self.assertEqual(expected_safe_logo, safe_logo, message) message = 'The north arrow path is not the default one' self.assertEqual(expected_north_arrow, north_arrow, message) message = 'The organisation logo path is not the default one' self.assertEqual(expected_org_logo, org_logo, message)
def _run_tests(test_suite, package_name, with_coverage=False): """Core function to test a test suite.""" count = test_suite.countTestCases() print('########') print('%s tests has been discovered in %s' % (count, package_name)) print('QGIS : %s' % qgis_version()) print('Python GDAL : %s' % gdal.VersionInfo('VERSION_NUM')) print('QT : %s' % Qt.QT_VERSION_STR) print('Run slow tests : %s' % (not os.environ.get('ON_TRAVIS', False))) print('########') if with_coverage: cov = coverage.Coverage( source=['safe/'], omit=['*/test/*', 'safe/definitions/*'], ) cov.start() unittest.TextTestRunner(verbosity=3, stream=sys.stdout).run(test_suite) if with_coverage: cov.stop() cov.save() report = tempfile.NamedTemporaryFile(delete=False) cov.report(file=report) # Produce HTML reports in the `htmlcov` folder and open index.html # cov.html_report() report.close() with open(report.name, 'r') as fin: print(fin.read())
def __init__(self, iface, template, layer): """Constructor for the Composition Report class. :param iface: Reference to the QGIS iface object. :type iface: QgsAppInterface :param template: The QGIS template path. :type template: str """ LOGGER.debug('InaSAFE Impact Report class initialised') self._iface = iface self._template = template self._layer = layer self._extent = self._iface.mapCanvas().extent() self._page_dpi = 300.0 self._safe_logo = resources_path( 'img', 'logos', 'inasafe-logo-url.svg') self._organisation_logo = default_organisation_logo_path() self._north_arrow = default_north_arrow_path() self._disclaimer = disclaimer() # For QGIS < 2.4 compatibility # QgsMapSettings is added in 2.4 if qgis_version() < 20400: map_settings = self._iface.mapCanvas().mapRenderer() else: map_settings = self._iface.mapCanvas().mapSettings() self._template_composition = TemplateComposition( template_path=self.template, map_settings=map_settings) self._keyword_io = KeywordIO()
def request(self, request_url): """Mock handler for an http request. :param request_url: Url being requested. """ url = str(request_url.url().toString()) reply = MockQNetworkReply() if url == 'http://hot-export.geofabrik.de/newjob': reply.content = read_all('test-importdlg-newjob.html') elif url == 'http://hot-export.geofabrik.de/wizard_area': reply.content = read_all('test-importdlg-wizardarea.html') elif url == 'http://hot-export.geofabrik.de/tagupload': reply.content = read_all('test-importdlg-job.html') reply._url = 'http://hot-export.geofabrik.de/jobs/1990' elif url == 'http://hot-export.geofabrik.de/jobs/1990': reply.content = read_all('test-importdlg-job.html') elif url == ('http://osm.inasafe.org/buildings-shp?' 'bbox=20.389938354492188,-34.10782492987083' ',20.712661743164062,' '-34.008273470938335&qgis_version=%s' '&inasafe_version=%s' '&lang=en' % (qgis_version(), get_version())): reply.content = read_all("test-importdlg-extractzip.zip") return reply
def accept(self): """Upload the layer to Geonode.""" enable_busy_cursor() self.button_box.setEnabled(False) layer = layer_from_combo(self.layers) login = self.login.text() if self.save_login.isChecked(): set_setting(GEONODE_USER, login) else: set_setting(GEONODE_USER, '') password = self.password.text() if self.save_password.isChecked(): set_setting(GEONODE_PASSWORD, password) else: set_setting(GEONODE_PASSWORD, '') url = self.url.text() if self.save_url.isChecked(): set_setting(GEONODE_URL, url) else: set_setting(GEONODE_URL, '') geonode_session = login_user(url, login, password) try: result = upload(url, geonode_session, layer.source()) except GeoNodeLayerUploadError as e: result = { 'success': False, 'message': str(e) } finally: self.button_box.setEnabled(True) disable_busy_cursor() if result['success']: self.done(QDialog.Accepted) layer_url = urljoin(url, result['url']) # Link is not working in QGIS 2. # It's gonna work in QGIS 3. if qgis_version() >= 29900: external_link = '<a href=\"{url}\">{url}</a>'.format( url=layer_url) else: external_link = layer_url display_success_message_bar( tr('Uploading done'), tr('Successfully uploaded to {external_link}').format( external_link=external_link), ) else: display_warning_message_box( self, tr('Error while uploading the layer.'), str(result) )
def test_issue71(self): """Test issue #71 in github - cbo changes should update ok button.""" # See https://github.com/AIFDR/inasafe/issues/71 # Push OK with the left mouse button print 'Using QGIS: %s' % qgis_version() self.tearDown() button = DOCK.pbnRunStop # First part of scenario should have enabled run file_list = [ join(HAZDATA, 'Flood_Current_Depth_Jakarta_geographic.asc'), join(TESTDATA, 'Population_Jakarta_geographic.asc')] hazard_layer_count, exposure_layer_count = load_layers(file_list) message = ( 'Incorrect number of Hazard layers: expected 1 got %s' % hazard_layer_count) self.assertTrue(hazard_layer_count == 1, message) message = ( 'Incorrect number of Exposure layers: expected 1 got %s' % exposure_layer_count) self.assertTrue(exposure_layer_count == 1, message) message = 'Run button was not enabled' self.assertTrue(button.isEnabled(), message) # Second part of scenario - run disabled when adding invalid layer # and select it - run should be disabled path = os.path.join(TESTDATA, 'issue71.tif') file_list = [path] # This layer has incorrect keywords clear_flag = False _, _ = load_layers(file_list, clear_flag) # set exposure to : Population Count (5kmx5km) # by moving one down DOCK.cboExposure.setCurrentIndex(DOCK.cboExposure.currentIndex() + 1) actual_dict = get_ui_state(DOCK) expected_dict = { 'Run Button Enabled': False, 'Impact Function Id': '', 'Impact Function Title': '', 'Hazard': 'A flood in Jakarta like in 2007', 'Exposure': 'Population Count (5kmx5km)'} message = (( 'Run button was not disabled when exposure set to \n%s' '\nUI State: \n%s\nExpected State:\n%s\n%s') % ( DOCK.cboExposure.currentText(), actual_dict, expected_dict, combos_to_string(DOCK))) self.assertTrue(expected_dict == actual_dict, message) # Now select again a valid layer and the run button # should be enabled DOCK.cboExposure.setCurrentIndex(DOCK.cboExposure.currentIndex() - 1) message = ( 'Run button was not enabled when exposure set to \n%s' % DOCK.cboExposure.currentText()) self.assertTrue(button.isEnabled(), message)
def _run_tests(test_suite, package_name): """Core function to test a test suite.""" count = test_suite.countTestCases() print '########' print '%s tests has been discovered in %s' % (count, package_name) print 'QGIS : %s' % qgis_version() print 'Run slow tests : %s' % (not os.environ.get('ON_TRAVIS', False)) print '########' unittest.TextTestRunner(verbosity=3, stream=sys.stdout).run(test_suite)
def test_save_scenario(self): """Test saving Current scenario.""" result, message = setup_scenario( DOCK, hazard='Classified Flood', exposure='Population', function='Be affected by each hazard class', function_id='ClassifiedRasterHazardPopulationFunction') self.assertTrue(result, message) # Enable on-the-fly reprojection set_canvas_crs(GEOCRS, True) set_jakarta_extent(dock=DOCK) # create unique file scenario_file = unique_filename( prefix='scenarioTest', suffix='.txt', dir=temp_dir('test')) self.save_scenario_dialog.save_scenario( scenario_file_path=scenario_file) with open(scenario_file) as f: data = f.readlines() title = data[0][:-1] exposure = data[1][:-1] hazard = data[2][:-1] function = data[3][:-1] extent = data[4][:-1] self.assertTrue( os.path.exists(scenario_file), 'File %s does not exist' % scenario_file) self.assertTrue( title == '[Classified Flood]', 'Title is not the same') self.assertTrue( exposure.startswith('exposure =') and exposure.endswith( 'pop_binary_raster_20_20.asc'), 'Exposure is not the same') self.assertTrue( hazard.startswith('hazard =') and hazard.endswith( 'classified_flood_20_20.asc'), 'Hazard is not the same') self.assertTrue( function == ( 'function = ClassifiedRasterHazardPopulationFunction'), 'Impact function is not same') # TODO: figure out why this changed between releases if qgis_version() < 20400: # For QGIS 2.0 expected_extent = ( 'extent = 106.313333, -6.380000, 107.346667, -6.070000') self.assertEqual(expected_extent, extent) else: # for QGIS 2.4 expected_extent = ( 'extent = 106.287500, -6.380000, 107.372500, -6.070000') self.assertEqual(expected_extent, expected_extent)
def download(feature_type, output_base_path, extent, progress_dialog=None): """Download shapefiles from Kartoza server. .. versionadded:: 3.2 :param feature_type: What kind of features should be downloaded. Currently 'buildings', 'building-points' or 'roads' are supported. :type feature_type: str :param output_base_path: The base path of the shape file. :type output_base_path: str :param extent: A list in the form [xmin, ymin, xmax, ymax] where all coordinates provided are in Geographic / EPSG:4326. :type extent: list :param progress_dialog: A progress dialog. :type progress_dialog: QProgressDialog :raises: ImportDialogError, CanceledImportDialogError """ # preparing necessary data min_longitude = extent[0] min_latitude = extent[1] max_longitude = extent[2] max_latitude = extent[3] box = ( '{min_longitude},{min_latitude},{max_longitude},' '{max_latitude}').format( min_longitude=min_longitude, min_latitude=min_latitude, max_longitude=max_longitude, max_latitude=max_latitude ) url = URL_OSM_PREFIX + feature_type + URL_OSM_SUFFIX url = '{url}?bbox={box}&qgis_version={qgis}'.format( url=url, box=box, qgis=qgis_version()) url += '&inasafe_version=%s' % get_version() if 'LANG' in os.environ: # Get the language only : eg 'en_US' -> 'en' env_lang = os.environ['LANG'].split('_')[0] url += '&lang=%s' % env_lang path = tempfile.mktemp('.shp.zip') # download and extract it fetch_zip(url, path, feature_type, progress_dialog) extract_zip(path, output_base_path) if progress_dialog: progress_dialog.done(QDialog.Accepted)
def create_profile_layer(profiling): """Create a tabular layer with the profiling. :param profiling: A dict containing benchmarking data. :type profiling: safe.messaging.message.Message :return: A tabular layer. :rtype: QgsVectorLayer """ fields = [ create_field_from_definition(profiling_function_field), create_field_from_definition(profiling_time_field) ] if setting(key='memory_profile', expected_type=bool): fields.append(create_field_from_definition(profiling_memory_field)) tabular = create_memory_layer( 'profiling', QgsWkbTypes.NullGeometry, fields=fields) # Generate profiling keywords tabular.keywords['layer_purpose'] = layer_purpose_profiling['key'] tabular.keywords['title'] = layer_purpose_profiling['name'] if qgis_version() >= 21800: tabular.setName(tabular.keywords['title']) else: tabular.setLayerName(tabular.keywords['title']) tabular.keywords['inasafe_fields'] = { profiling_function_field['key']: profiling_function_field['field_name'], profiling_time_field['key']: profiling_time_field['field_name'], } if setting(key='memory_profile', expected_type=bool): tabular.keywords['inasafe_fields'][ profiling_memory_field['key']] = profiling_memory_field[ 'field_name'] tabular.keywords[inasafe_keyword_version_key] = ( inasafe_keyword_version) table = profiling.to_text().splitlines()[3:] tabular.startEditing() for line in table: feature = QgsFeature() items = line.split(', ') time = items[1].replace('-', '') if setting(key='memory_profile', expected_type=bool): memory = items[2].replace('-', '') feature.setAttributes([items[0], time, memory]) else: feature.setAttributes([items[0], time]) tabular.addFeature(feature) tabular.commitChanges() return tabular
def add_impact_layers_to_canvas(impact_function, group=None, iface=None): """Helper method to add impact layer to QGIS from impact function. :param impact_function: The impact function used. :type impact_function: ImpactFunction :param group: An existing group as a parent, optional. :type group: QgsLayerTreeGroup :param iface: QGIS QgisAppInterface instance. :type iface: QgisAppInterface """ layers = impact_function.outputs name = impact_function.name if group: group_analysis = group else: # noinspection PyArgumentList root = QgsProject.instance().layerTreeRoot() group_analysis = root.insertGroup(0, name) group_analysis.setItemVisibilityChecked(True) group_analysis.setExpanded(group is None) for layer in layers: # noinspection PyArgumentList QgsProject.instance().addMapLayer(layer, False) layer_node = group_analysis.addLayer(layer) # set layer title if any try: title = layer.keywords['title'] if qgis_version() >= 21800: layer.setName(title) else: layer.setLayerName(title) except KeyError: pass visible_layers = [impact_function.impact.id()] print_atlas = setting('print_atlas_report', False, bool) if print_atlas: visible_layers.append(impact_function.aggregation_summary.id()) # Let's enable only the more detailed layer. See #2925 if layer.id() in visible_layers: layer_node.setItemVisibilityChecked(True) else: layer_node.setItemVisibilityChecked(False) # we need to set analysis_impacted as an active layer because we need # to get all qgis variables that we need from this layer for # infographic. if iface: iface.setActiveLayer(impact_function.analysis_impacted)
def test_clip_geometry(self): """Test that we can clip a geometry using another geometry.""" geometry = QgsGeometry.fromPolyline([ QgsPoint(10, 10), QgsPoint(20, 20), QgsPoint(30, 30), QgsPoint(40, 40)]) clip_polygon = QgsGeometry.fromPolygon([ [QgsPoint(20, 20), QgsPoint(20, 30), QgsPoint(30, 30), QgsPoint(30, 20), QgsPoint(20, 20)]]) result = clip_geometry(clip_polygon, geometry) expected_wkt = 'LINESTRING(20.0 20.0, 30.0 30.0)' # There should only be one feature that intersects this clip # poly so this assertion should work. assert compare_wkt(expected_wkt, str(result.exportToWkt())) # Now poly on poly clip test clip_polygon = QgsGeometry.fromWkt( 'POLYGON((106.8218 -6.1842,106.8232 -6.1842,' '106.82304963 -6.18317148,106.82179736 -6.18314774,' '106.8218 -6.1842))') geometry = QgsGeometry.fromWkt( 'POLYGON((106.8216869 -6.1852067,106.8213233 -6.1843051,' '106.8212891 -6.1835559,106.8222566 -6.1835184,' '106.8227557 -6.1835076,106.8228588 -6.1851204,' '106.8216869 -6.1852067))') result = clip_geometry(clip_polygon, geometry) expected_wkt = ( 'POLYGON((106.82179833 -6.18353616,106.8222566 -6.1835184,' '106.8227557 -6.1835076,106.82279996 -6.1842,' '106.8218 -6.1842,106.82179833 -6.18353616))') # There should only be one feature that intersects this clip # poly so this assertion should work. assert compare_wkt(expected_wkt, str(result.exportToWkt())) # Now point on poly test clip geometry = QgsGeometry.fromWkt('POINT(106.82241 -6.18369)') result = clip_geometry(clip_polygon, geometry) if qgis_version() > 10800: expected_wkt = 'POINT(106.82241 -6.18369)' else: expected_wkt = 'POINT(106.822410 -6.183690)' # There should only be one feature that intersects this clip # poly so this assertion should work. assert compare_wkt(expected_wkt, str(result.exportToWkt()))
def make_keywordless_layer(): """Helper function that returns a single predefined keywordless layer.""" path = 'keywordless_layer.tif' base_path = test_data_path('hazard') full_path = os.path.abspath(os.path.join(base_path, path)) title = 'Keywordless Layer' # noinspection PyCallingNonCallable layer = QgsRasterLayer(full_path, title) if qgis_version() >= 10800: # 1.8 or newer # noinspection PyArgumentList,PyUnresolvedReferences QgsMapLayerRegistry.instance().addMapLayers([layer]) else: # noinspection PyArgumentList,PyUnresolvedReferences QgsMapLayerRegistry.instance().addMapLayers([layer]) return layer
def add_layer_to_canvas(layer, name): """Helper method to add layer to QGIS. :param layer: The layer. :type layer: QgsMapLayer :param name: Layer name. :type name: str """ if qgis_version() >= 21800: layer.setName(name) else: layer.setLayerName(name) QgsProject.instance().addMapLayer(layer, False)
def load_shapefile(self, feature_type, base_path): """Load downloaded shape file to QGIS Main Window. :param feature_type: What kind of features should be downloaded. Currently 'buildings', 'building-points' or 'roads' are supported. :type feature_type: str :param base_path: The base path of the shape file (without extension). :type base_path: str :raises: FileMissingError - when buildings.shp not exist """ path = '%s.shp' % base_path if not os.path.exists(path): message = self.tr( '%s does not exist. The server does not have any data for ' 'this extent.' % path) raise FileMissingError(message) layer = self.iface.addVectorLayer(path, feature_type, 'ogr') # Check if it's a building layer and if it's QGIS 2.14 about the 2.5D if qgis_version() >= 21400 and feature_type == 'buildings': layer_scope = QgsExpressionContextUtils.layerScope(layer) if not layer_scope.variable('qgis_25d_height'): QgsExpressionContextUtils.setLayerVariable( layer, 'qgis_25d_height', 0.0002) if not layer_scope.variable('qgis_25d_angle'): QgsExpressionContextUtils.setLayerVariable( layer, 'qgis_25d_angle', 70) canvas_srid = self.canvas.mapSettings().destinationCrs().srsid() on_the_fly_projection = self.canvas.hasCrsTransformEnabled() if canvas_srid != 4326 and not on_the_fly_projection: if QGis.QGIS_VERSION_INT >= 20400: self.canvas.setCrsTransformEnabled(True) else: display_warning_message_bar( self.iface, self.tr('Enable \'on the fly\''), self.tr( 'Your current projection is different than EPSG:4326. ' 'You should enable \'on the fly\' to display ' 'correctly your layers') )
def test_custom_logo(self): """Test that setting user-defined logo works.""" LOGGER.info('Testing custom_logo') impact_layer_path = test_data_path( 'impact', 'population_affected_entire_area.shp') layer, _ = load_layer(impact_layer_path) # noinspection PyUnresolvedReferences,PyArgumentList QgsMapLayerRegistry.instance().addMapLayer(layer) # noinspection PyCallingNonCallable rect = QgsRectangle(106.8194, -6.2108, 106.8201, -6.1964) CANVAS.setExtent(rect) CANVAS.refresh() template = resources_path( 'qgis-composer-templates', 'a4-portrait-blue.qpt') report = ImpactReport(IFACE, template, layer) # Set custom logo custom_logo_path = resources_path('img', 'logos', 'supporters.png') report.organisation_logo = custom_logo_path out_path = unique_filename( prefix='map_custom_logo_test', suffix='.pdf', dir=temp_dir('test')) report.print_map_to_pdf(out_path) # Check the file exists message = 'Rendered output does not exist: %s' % out_path self.assertTrue(os.path.exists(out_path), message) # Check the file is not corrupt message = 'The output file %s is corrupt' % out_path out_size = os.stat(out_path).st_size self.assertTrue(out_size > 0, message) # Check the organisation logo in composition sets correctly to # logo-flower if qgis_version() < 20500: custom_img_path = report.composition.getComposerItemById( 'organisation-logo').pictureFile() else: custom_img_path = report.composition.getComposerItemById( 'organisation-logo').picturePath() message = 'The custom logo path is not set correctly' self.assertEqual(custom_logo_path, custom_img_path, message)
def add_impact_layers_to_canvas(impact_function, iface): """Helper method to add impact layer to QGIS from impact function. :param impact_function: The impact function used. :type impact_function: ImpactFunction :param iface: QGIS QGisAppInterface instance. :type iface: QGisAppInterface """ layers = impact_function.outputs name = impact_function.name # noinspection PyArgumentList root = QgsProject.instance().layerTreeRoot() group_analysis = root.insertGroup(0, name) group_analysis.setVisible(Qt.Checked) for layer in layers: # noinspection PyArgumentList QgsMapLayerRegistry.instance().addMapLayer(layer, False) layer_node = group_analysis.addLayer(layer) # set layer title if any try: title = layer.keywords['title'] if qgis_version() >= 21800: layer.setName(title) else: layer.setLayerName(title) except KeyError: pass # Let's enable only the more detailed layer. See #2925 if layer.id() == impact_function.impact.id(): layer_node.setVisible(Qt.Checked) iface.setActiveLayer(layer) elif is_raster_layer(layer): layer_node.setVisible(Qt.Checked) else: layer_node.setVisible(Qt.Unchecked)
def __init__(self, iface, template, layer, extra_layers=[]): """Constructor for the Composition Report class. :param iface: Reference to the QGIS iface object. :type iface: QgsAppInterface :param template: The QGIS template path. :type template: str """ LOGGER.debug('InaSAFE Impact Report class initialised') self._iface = iface self._template = None self.template = template self._layer = layer self._extra_layers = extra_layers self._extent = self._iface.mapCanvas().extent() self._page_dpi = 300.0 self._black_inasafe_logo = black_inasafe_logo_path() self._white_inasafe_logo = white_inasafe_logo_path() # User can change this path in preferences self._organisation_logo = supporters_logo_path() self._supporters_logo = supporters_logo_path() self._north_arrow = default_north_arrow_path() self._disclaimer = disclaimer() # For QGIS < 2.4 compatibility # QgsMapSettings is added in 2.4 if qgis_version() < 20400: map_settings = self._iface.mapCanvas().mapRenderer() else: map_settings = self._iface.mapCanvas().mapSettings() self._template_composition = TemplateComposition( template_path=self.template, map_settings=map_settings) self._keyword_io = KeywordIO()
def test_get_qgis_version(self): """Test we can get the version of QGIS""" version = qgis_version() message = 'Got version %s of QGIS, but at least 107000 is needed' assert version > 10700, message
def add_layers_to_canvas_with_custom_orders(order, impact_function, iface=None): """Helper to add layers to the map canvas following a specific order. From top to bottom in the legend: [ ('FromCanvas', layer name, full layer URI, QML), ('FromAnalysis', layer purpose, layer group, None), ... ] The full layer URI is coming from our helper. :param order: Special structure the list of layers to add. :type order: list :param impact_function: The multi exposure impact function used. :type impact_function: MultiExposureImpactFunction :param iface: QGIS QgisAppInterface instance. :type iface: QgisAppInterface """ root = QgsProject.instance().layerTreeRoot() # Make all layers hidden. for child in root.children(): child.setItemVisibilityChecked(False) group_analysis = root.insertGroup(0, impact_function.name) group_analysis.setItemVisibilityChecked(True) group_analysis.setCustomProperty(MULTI_EXPOSURE_ANALYSIS_FLAG, True) # Insert layers in the good order in the group. for layer_definition in order: if layer_definition[0] == FROM_CANVAS['key']: style = QDomDocument() style.setContent(layer_definition[3]) layer = load_layer(layer_definition[2], layer_definition[1])[0] layer.importNamedStyle(style) QgsProject.instance().addMapLayer(layer, False) layer_node = group_analysis.addLayer(layer) layer_node.setItemVisibilityChecked(True) else: if layer_definition[2] == impact_function.name: for layer in impact_function.outputs: if layer.keywords['layer_purpose'] == layer_definition[1]: QgsProject.instance().addMapLayer(layer, False) layer_node = group_analysis.addLayer(layer) layer_node.setItemVisibilityChecked(True) try: title = layer.keywords['title'] if qgis_version() >= 21800: layer.setName(title) else: layer.setLayerName(title) except KeyError: pass break else: for sub_impact_function in impact_function.impact_functions: # Iterate over each sub impact function used in the # multi exposure analysis. if sub_impact_function.name == layer_definition[2]: for layer in sub_impact_function.outputs: purpose = layer_definition[1] if layer.keywords['layer_purpose'] == purpose: QgsProject.instance().addMapLayer(layer, False) layer_node = group_analysis.addLayer(layer) layer_node.setItemVisibilityChecked(True) try: title = layer.keywords['title'] if qgis_version() >= 21800: layer.setName(title) else: layer.setLayerName(title) except KeyError: pass break if iface: iface.setActiveLayer(impact_function.analysis_impacted)
def analysis_summary(aggregate_hazard, analysis, callback=None): """Compute the summary from the aggregate hazard to analysis. Source layer : | haz_id | haz_class | aggr_id | aggr_name | total_feature | Target layer : | analysis_id | Output layer : | analysis_id | count_hazard_class | affected_count | total | :param aggregate_hazard: The layer to aggregate vector layer. :type aggregate_hazard: QgsVectorLayer :param analysis: The target vector layer where to write statistics. :type analysis: QgsVectorLayer :param callback: A function to all to indicate progress. The function should accept params 'current' (int), 'maximum' (int) and 'step' (str). Defaults to None. :type callback: function :return: The new target layer with summary. :rtype: QgsVectorLayer .. versionadded:: 4.0 """ output_layer_name = summary_3_analysis_steps['output_layer_name'] processing_step = summary_3_analysis_steps['step_name'] source_fields = aggregate_hazard.keywords['inasafe_fields'] target_fields = analysis.keywords['inasafe_fields'] target_compulsory_fields = [ analysis_id_field, analysis_name_field, ] check_inputs(target_compulsory_fields, target_fields) source_compulsory_fields = [ aggregation_id_field, aggregation_name_field, hazard_id_field, hazard_class_field, total_field ] check_inputs(source_compulsory_fields, source_fields) absolute_values = create_absolute_values_structure( aggregate_hazard, ['all']) hazard_class = source_fields[hazard_class_field['key']] hazard_class_index = aggregate_hazard.fieldNameIndex(hazard_class) unique_hazard = aggregate_hazard.uniqueValues(hazard_class_index) hazard_keywords = aggregate_hazard.keywords['hazard_keywords'] classification = hazard_keywords['classification'] total = source_fields[total_field['key']] flat_table = FlatTable('hazard_class') # First loop over the aggregate_hazard layer request = QgsFeatureRequest() request.setSubsetOfAttributes( [hazard_class, total], aggregate_hazard.fields()) request.setFlags(QgsFeatureRequest.NoGeometry) for area in aggregate_hazard.getFeatures(): hazard_value = area[hazard_class_index] value = area[total] if not value or isinstance(value, QPyNullVariant) or isnan(value): # For isnan, see ticket #3812 value = 0 if not hazard_value or isinstance(hazard_value, QPyNullVariant): hazard_value = 'NULL' flat_table.add_value( value, hazard_class=hazard_value ) # We summarize every absolute values. for field, field_definition in absolute_values.iteritems(): value = area[field] if not value or isinstance(value, QPyNullVariant): value = 0 field_definition[0].add_value( value, all='all' ) analysis.startEditing() shift = analysis.fields().count() counts = [ total_affected_field, total_not_affected_field, total_not_exposed_field, total_field] add_fields( analysis, absolute_values, counts, unique_hazard, hazard_count_field) affected_sum = 0 not_affected_sum = 0 not_exposed_sum = 0 for area in analysis.getFeatures(request): total = 0 for i, val in enumerate(unique_hazard): if not val or isinstance(val, QPyNullVariant): val = 'NULL' sum = flat_table.get_value(hazard_class=val) total += sum analysis.changeAttributeValue(area.id(), shift + i, sum) affected = post_processor_affected_function( classification=classification, hazard_class=val) if affected == not_exposed_class['key']: not_exposed_sum += sum elif affected: affected_sum += sum else: not_affected_sum += sum # Affected field analysis.changeAttributeValue( area.id(), shift + len(unique_hazard), affected_sum) # Not affected field analysis.changeAttributeValue( area.id(), shift + len(unique_hazard) + 1, not_affected_sum) # Not exposed field analysis.changeAttributeValue( area.id(), shift + len(unique_hazard) + 2, not_exposed_sum) # Total field analysis.changeAttributeValue( area.id(), shift + len(unique_hazard) + 3, total) # Any absolute postprocessors for i, field in enumerate(absolute_values.itervalues()): value = field[0].get_value( all='all' ) analysis.changeAttributeValue( area.id(), shift + len(unique_hazard) + 4 + i, value) # Sanity check ± 1 to the result. Disabled for now as it seems ± 1 is not # enough. ET 13/02/17 # total_computed = ( # affected_sum + not_affected_sum + not_exposed_sum) # if not -1 < (total_computed - total) < 1: # raise ComputationError analysis.commitChanges() analysis.keywords['title'] = layer_purpose_analysis_impacted['name'] if qgis_version() >= 21600: analysis.setName(analysis.keywords['title']) else: analysis.setLayerName(analysis.keywords['title']) analysis.keywords['layer_purpose'] = layer_purpose_analysis_impacted['key'] check_layer(analysis) return analysis
def _create_exposure_combos(self): """Create one combobox for each exposure and insert them in the UI.""" # Map registry may be invalid if QGIS is shutting down project = QgsProject.instance() canvas_layers = self.iface.mapCanvas().layers() # MapLayers returns a QMap<QString id, QgsMapLayer layer> layers = list(project.mapLayers().values()) # Sort by name for tests layers.sort(key=lambda x: x.name()) show_only_visible_layers = setting('visibleLayersOnlyFlag', expected_type=bool) # For issue #618 if len(layers) == 0: # self.message_viewer.setHtml(getting_started_message()) return for one_exposure in exposure_all: label = QLabel(one_exposure['name']) combo = QComboBox() combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) combo.addItem(tr('Do not use'), None) self.form_layout.addRow(label, combo) self.combos_exposures[one_exposure['key']] = combo for layer in layers: if (show_only_visible_layers and (layer not in canvas_layers)): continue try: layer_purpose = self.keyword_io.read_keywords( layer, 'layer_purpose') keyword_version = str( self.keyword_io.read_keywords(layer, inasafe_keyword_version_key)) if not is_keyword_version_supported(keyword_version): continue except BaseException: # pylint: disable=W0702 # continue ignoring this layer continue # See if there is a title for this layer, if not, # fallback to the layer's filename # noinspection PyBroadException try: title = self.keyword_io.read_keywords(layer, 'title') except (NoKeywordsFoundError, KeywordNotFoundError, MetadataReadError): # Skip if there are no keywords at all, or missing keyword continue except BaseException: # pylint: disable=W0702 pass else: # Lookup internationalised title if available title = self.tr(title) # Register title with layer set_layer_from_title = setting('set_layer_from_title_flag', True, bool) if title and set_layer_from_title: if qgis_version() >= 21800: layer.setName(title) else: # QGIS 2.14 layer.setLayerName(title) source = layer.id() icon = layer_icon(layer) if layer_purpose == layer_purpose_hazard['key']: add_ordered_combo_item(self.cbx_hazard, title, source, icon=icon) elif layer_purpose == layer_purpose_aggregation['key']: if self.use_selected_only: count_selected = layer.selectedFeatureCount() if count_selected > 0: add_ordered_combo_item(self.cbx_aggregation, title, source, count_selected, icon=icon) else: add_ordered_combo_item(self.cbx_aggregation, title, source, None, icon) else: add_ordered_combo_item(self.cbx_aggregation, title, source, None, icon) elif layer_purpose == layer_purpose_exposure['key']: # fetching the exposure try: exposure_type = self.keyword_io.read_keywords( layer, layer_purpose_exposure['key']) except BaseException: # pylint: disable=W0702 # continue ignoring this layer continue for key, combo in list(self.combos_exposures.items()): if key == exposure_type: add_ordered_combo_item(combo, title, source, icon=icon) self.cbx_aggregation.addItem(entire_area_item_aggregation, None) for combo in list(self.combos_exposures.values()): combo.currentIndexChanged.connect(self.validate_impact_function)
def test_print_default_template(self): """Test printing report to pdf using default template works.""" impact_layer_path = standard_data_path( 'impact', 'population_affected_entire_area.shp') layer, _ = load_layer(impact_layer_path) # noinspection PyUnresolvedReferences,PyArgumentList QgsMapLayerRegistry.instance().addMapLayer(layer) # noinspection PyCallingNonCallable rect = QgsRectangle(106.8194, -6.2108, 106.8201, -6.1964) CANVAS.setExtent(rect) CANVAS.refresh() template = resources_path( 'qgis-composer-templates', 'a4-portrait-blue.qpt') report = ImpactReport(IFACE, template, layer) out_path = unique_filename( prefix='map_default_template_test', suffix='.pdf', dir=temp_dir('test')) report.print_map_to_pdf(out_path) # Check the file exists message = 'Rendered output does not exist: %s' % out_path self.assertTrue(os.path.exists(out_path), message) # Check the file is not corrupt message = 'The output file %s is corrupt' % out_path out_size = os.stat(out_path).st_size self.assertTrue(out_size > 0, message) # Check the components in composition are default components if qgis_version() < 20500: safe_logo = report.composition.getComposerItemById( 'inasafe-logo').pictureFile() north_arrow = report.composition.getComposerItemById( 'north-arrow').pictureFile() org_logo = report.composition.getComposerItemById( 'organisation-logo').pictureFile() else: safe_logo = report.composition.getComposerItemById( 'white-inasafe-logo').picturePath() north_arrow = report.composition.getComposerItemById( 'north-arrow').picturePath() org_logo = report.composition.getComposerItemById( 'organisation-logo').picturePath() expected_safe_logo = resources_path( 'img', 'logos', 'inasafe-logo-url-white.svg') expected_north_arrow = resources_path( 'img', 'north_arrows', 'simple_north_arrow.png') expected_org_logo = resources_path('img', 'logos', 'supporters.png') message = ( 'The safe logo path is not the default one: %s isn\'t %s' % (expected_safe_logo, safe_logo)) self.assertEqual(expected_safe_logo, safe_logo, message) message = 'The north arrow path is not the default one' self.assertEqual(expected_north_arrow, north_arrow, message) message = 'The organisation logo path is not the default one' self.assertEqual(expected_org_logo, org_logo, message)
def print_impact_table(self, output_path): """Pint summary from impact layer to PDF. ..note:: The order of the report: 1. Summary table 2. Aggregation table 3. Attribution table :param output_path: Output path. :type output_path: str :return: Path to generated pdf file. :rtype: str :raises: None """ keywords = self._keyword_io.read_keywords(self.layer) if output_path is None: output_path = unique_filename(suffix='.pdf', dir=temp_dir()) summary_table = keywords.get('impact_summary', None) full_table = keywords.get('impact_table', None) aggregation_table = keywords.get('postprocessing_report', None) attribution_table = impact_attribution(keywords) # (AG) We will not use impact_table as most of the IF use that as: # impact_table = impact_summary + some information intended to be # shown on screen (see FloodOsmBuilding) # Unless the impact_summary is None, we will use impact_table as the # alternative html = LOGO_ELEMENT.to_html() html += m.Heading(tr('Analysis Results'), **INFO_STYLE).to_html() if summary_table is None: html += full_table else: html += summary_table if aggregation_table is not None: html += aggregation_table if attribution_table is not None: html += attribution_table.to_html() html = html_header() + html + html_footer() # Print HTML using composition # For QGIS < 2.4 compatibility # QgsMapSettings is added in 2.4 if qgis_version() < 20400: map_settings = QgsMapRenderer() else: map_settings = QgsMapSettings() # A4 Portrait paper_width = 210 paper_height = 297 # noinspection PyCallingNonCallable composition = QgsComposition(map_settings) # noinspection PyUnresolvedReferences composition.setPlotStyle(QgsComposition.Print) composition.setPaperSize(paper_width, paper_height) composition.setPrintResolution(300) # Add HTML Frame # noinspection PyCallingNonCallable html_item = QgsComposerHtml(composition, False) margin_left = 10 margin_top = 10 # noinspection PyCallingNonCallable html_frame = QgsComposerFrame( composition, html_item, margin_left, margin_top, paper_width - 2 * margin_left, paper_height - 2 * margin_top) html_item.addFrame(html_frame) # Set HTML # From QGIS 2.6, we can set composer HTML with manual HTML if qgis_version() < 20600: html_path = unique_filename( prefix='report', suffix='.html', dir=temp_dir()) html_to_file(html, file_path=html_path) html_url = QUrl.fromLocalFile(html_path) html_item.setUrl(html_url) else: # noinspection PyUnresolvedReferences html_item.setContentMode(QgsComposerHtml.ManualHtml) # noinspection PyUnresolvedReferences html_item.setResizeMode(QgsComposerHtml.RepeatUntilFinished) html_item.setHtml(html) html_item.loadHtml() composition.exportAsPDF(output_path) return output_path
def get_layer_description_from_browser(self, category): """Obtain the description of the browser layer selected by user. :param category: The category of the layer to get the description. :type category: string :returns: Tuple of boolean and string. Boolean is true if layer is validated as compatible for current role (impact function and category) and false otherwise. String contains a description of the selected layer or an error message. :rtype: tuple """ if category == 'hazard': browser = self.tvBrowserHazard elif category == 'exposure': browser = self.tvBrowserExposure elif category == 'aggregation': browser = self.tvBrowserAggregation else: raise InaSAFEError index = browser.selectionModel().currentIndex() if not index: return False, '' # Map the proxy model index to the source model index index = browser.model().mapToSource(index) item = browser.model().sourceModel().dataItem(index) if not item: return False, '' item_class_name = item.metaObject().className() # if not itemClassName.endswith('LayerItem'): if not item.type() == QgsDataItem.Layer: if item_class_name == 'QgsPGRootItem' and not item.children(): return False, create_postGIS_connection_first else: return False, '' if item_class_name not in [ 'QgsOgrLayerItem', 'QgsGdalLayerItem', 'QgsPGLayerItem', 'QgsLayerItem', ]: return False, '' path = item.path() if item_class_name in ['QgsOgrLayerItem', 'QgsGdalLayerItem', 'QgsLayerItem'] and not os.path.exists(path): return False, '' # try to create the layer if item_class_name == 'QgsOgrLayerItem': layer = QgsVectorLayer(path, '', 'ogr') elif item_class_name == 'QgsPGLayerItem': uri = self.postgis_path_to_uri(path) if uri: layer = QgsVectorLayer(uri.uri(), uri.table(), 'postgres') else: layer = None else: layer = QgsRasterLayer(path, '', 'gdal') if not layer or not layer.isValid(): return False, self.tr('Not a valid layer.') try: keywords = self.keyword_io.read_keywords(layer) if ('layer_purpose' not in keywords and 'impact_summary' not in keywords): keywords = None except (HashNotFoundError, OperationalError, NoKeywordsFoundError, KeywordNotFoundError, InvalidParameterError, UnsupportedProviderError, MissingMetadata): keywords = None # set the layer name for further use in the step_fc_summary if keywords: if qgis_version() >= 21800: layer.setName(keywords.get('title')) else: layer.setLayerName(keywords.get('title')) if not self.parent.is_layer_compatible(layer, category, keywords): label_text = '%s<br/>%s' % ( self.tr( 'This layer\'s keywords or type are not suitable:'), self.unsuitable_layer_description_html( layer, category, keywords)) return False, label_text # set the current layer (e.g. for the keyword creation sub-thread # or for adding the layer to mapCanvas) self.parent.layer = layer if category == 'hazard': self.parent.hazard_layer = layer elif category == 'exposure': self.parent.exposure_layer = layer else: self.parent.aggregation_layer = layer # Check if the layer is keywordless if keywords and 'keyword_version' in keywords: kw_ver = str(keywords['keyword_version']) self.parent.is_selected_layer_keywordless = ( not is_keyword_version_supported(kw_ver)) else: self.parent.is_selected_layer_keywordless = True desc = layer_description_html(layer, keywords) return True, desc
def multi_exposure_analysis_summary(analysis, intermediate_analysis): """Merge intermediate analysis into one analysis summary. List of analysis layers like: | analysis_id | count_hazard_class | affected_count | total | Target layer : | analysis_id | Output layer : | analysis_id | count_hazard_class | affected_count | total | :param analysis: The target vector layer where to write statistics. :type analysis: QgsVectorLayer :param intermediate_analysis: List of analysis layer for a single exposure. :type intermediate_analysis: list :return: The new target layer with summary. :rtype: QgsVectorLayer .. versionadded:: 4.3 """ analysis.startEditing() request = QgsFeatureRequest() request.setFlags(QgsFeatureRequest.NoGeometry) target_id = next(analysis.getFeatures(request)).id() for analysis_result in intermediate_analysis: exposure = analysis_result.keywords['exposure_keywords']['exposure'] iterator = analysis_result.getFeatures(request) feature = next(iterator) source_fields = analysis_result.keywords['inasafe_fields'] # Dynamic fields hazards = read_dynamic_inasafe_field(source_fields, hazard_count_field) for hazard_zone in hazards: field = create_field_from_definition(exposure_hazard_count_field, exposure, hazard_zone) analysis.addAttribute(field) index = analysis.fieldNameIndex(field.name()) value = feature[analysis_result.fieldNameIndex( hazard_count_field['field_name'] % hazard_zone)] analysis.changeAttributeValue(target_id, index, value) # keywords key = exposure_hazard_count_field['key'] % (exposure, hazard_zone) value = exposure_hazard_count_field['field_name'] % (exposure, hazard_zone) analysis.keywords['inasafe_fields'][key] = value # total affected source_index = analysis_result.fieldNameIndex( total_affected_field['field_name']) field = create_field_from_definition(exposure_total_affected_field, exposure) analysis.addAttribute(field) index = analysis.fieldNameIndex(field.name()) analysis.changeAttributeValue(target_id, index, feature[source_index]) # keywords key = exposure_total_affected_field['key'] % exposure value = exposure_total_affected_field['field_name'] % exposure analysis.keywords['inasafe_fields'][key] = value # total not affected source_index = analysis_result.fieldNameIndex( total_not_affected_field['field_name']) field = create_field_from_definition(exposure_total_not_affected_field, exposure) analysis.addAttribute(field) index = analysis.fieldNameIndex(field.name()) analysis.changeAttributeValue(target_id, index, feature[source_index]) # keywords key = exposure_total_not_affected_field['key'] % exposure value = exposure_total_not_affected_field['field_name'] % exposure analysis.keywords['inasafe_fields'][key] = value # total exposed source_index = analysis_result.fieldNameIndex( total_exposed_field['field_name']) field = create_field_from_definition(exposure_total_exposed_field, exposure) analysis.addAttribute(field) index = analysis.fieldNameIndex(field.name()) analysis.changeAttributeValue(target_id, index, feature[source_index]) # keywords key = exposure_total_exposed_field['key'] % exposure value = exposure_total_exposed_field['field_name'] % exposure analysis.keywords['inasafe_fields'][key] = value # total not exposed source_index = analysis_result.fieldNameIndex( total_not_exposed_field['field_name']) field = create_field_from_definition(exposure_total_not_exposed_field, exposure) analysis.addAttribute(field) index = analysis.fieldNameIndex(field.name()) analysis.changeAttributeValue(target_id, index, feature[source_index]) # keywords key = exposure_total_not_exposed_field['key'] % exposure value = exposure_total_not_exposed_field['field_name'] % exposure analysis.keywords['inasafe_fields'][key] = value # total source_index = analysis_result.fieldNameIndex( total_field['field_name']) field = create_field_from_definition(exposure_total_field, exposure) analysis.addAttribute(field) index = analysis.fieldNameIndex(field.name()) analysis.changeAttributeValue(target_id, index, feature[source_index]) # keywords key = exposure_total_field['key'] % exposure value = exposure_total_field['field_name'] % exposure analysis.keywords['inasafe_fields'][key] = value analysis.commitChanges() analysis.keywords['title'] = ( layer_purpose_analysis_impacted['multi_exposure_name']) analysis.keywords['layer_purpose'] = layer_purpose_analysis_impacted['key'] # Set up the extra keywords so everyone knows it's a # multi exposure analysis result. extra_keywords = { extra_keyword_analysis_type['key']: MULTI_EXPOSURE_ANALYSIS_FLAG } analysis.keywords['extra_keywords'] = extra_keywords if qgis_version() >= 21600: analysis.setName(analysis.keywords['title']) else: analysis.setLayerName(analysis.keywords['title']) return analysis
def load_template(self, map_settings): """Load composer template for merged report. Validate it as well. The template needs to have: 1. QgsComposerMap with id 'impact-map' for merged impact map. 2. QgsComposerPicture with id 'safe-logo' for InaSAFE logo. 3. QgsComposerLabel with id 'summary-report' for a summary of two impacts. 4. QgsComposerLabel with id 'aggregation-area' to indicate the area of aggregation. 5. QgsComposerScaleBar with id 'map-scale' for impact map scale. 6. QgsComposerLegend with id 'map-legend' for impact map legend. 7. QgsComposerPicture with id 'organisation-logo' for organisation logo. 8. QgsComposerLegend with id 'impact-legend' for map legend. 9. QgsComposerHTML with id 'merged-report-table' for the merged report. :param map_settings: Map settings. :type map_settings: QgsMapSettings, QgsMapRenderer """ # Create Composition template_composition = TemplateComposition(self.template_path, map_settings) # Validate the component in the template component_ids = [ 'impact-map', 'safe-logo', 'summary-report', 'aggregation-area', 'map-scale', 'map-legend', 'organisation-logo', 'merged-report-table' ] template_composition.component_ids = component_ids if len(template_composition.missing_elements) > 0: raise ReportCreationError( self.tr('Components: %s could not be found' % ', '.join(template_composition.missing_elements))) # Prepare map substitution and set to composition impact_title = '%s and %s' % (self.first_impact['map_title'], self.second_impact['map_title']) substitution_map = { 'impact-title': impact_title, 'hazard-title': self.first_impact['hazard_title'], 'disclaimer': self.disclaimer } template_composition.substitution = substitution_map # Load Template try: template_composition.load_template() except TemplateLoadingError: raise # Draw Composition # Set InaSAFE logo composition = template_composition.composition safe_logo = composition.getComposerItemById('safe-logo') safe_logo.setPictureFile(self.safe_logo_path) # set organisation logo org_logo = composition.getComposerItemById('organisation-logo') org_logo.setPictureFile(self.organisation_logo_path) # Set Map Legend legend = composition.getComposerItemById('map-legend') if qgis_version() < 20400: layers = map_settings.layerSet() else: layers = map_settings.layers() if qgis_version() < 20600: legend.model().setLayerSet(layers) legend.synchronizeWithModel() else: root_group = legend.modelV2().rootGroup() layer_ids = map_settings.layers() for layer_id in layer_ids: # noinspection PyUnresolvedReferences layer = QgsMapLayerRegistry.instance().mapLayer(layer_id) root_group.addLayer(layer) legend.synchronizeWithModel() return composition
def download(feature_type, output_base_path, extent, progress_dialog=None, server_url=None): """Download shapefiles from Kartoza server. .. versionadded:: 3.2 :param feature_type: What kind of features should be downloaded. Currently 'buildings', 'building-points' or 'roads' are supported. :type feature_type: str :param output_base_path: The base path of the shape file. :type output_base_path: str :param extent: A list in the form [xmin, ymin, xmax, ymax] where all coordinates provided are in Geographic / EPSG:4326. :type extent: list :param progress_dialog: A progress dialog. :type progress_dialog: QProgressDialog :param server_url: The server URL to use. :type: basestring :raises: ImportDialogError, CanceledImportDialogError """ if not server_url: server_url = PRODUCTION_SERVER # preparing necessary data min_longitude = extent[0] min_latitude = extent[1] max_longitude = extent[2] max_latitude = extent[3] box = ('{min_longitude},{min_latitude},{max_longitude},' '{max_latitude}').format(min_longitude=min_longitude, min_latitude=min_latitude, max_longitude=max_longitude, max_latitude=max_latitude) url = ('{url_osm_prefix}' '{feature_type}' '{url_osm_suffix}?' 'bbox={box}&' 'qgis_version={qgis}&' 'lang={lang}&' 'inasafe_version={inasafe_version}'.format( url_osm_prefix=server_url, feature_type=feature_type, url_osm_suffix=URL_OSM_SUFFIX, box=box, qgis=qgis_version(), lang=locale(), inasafe_version=get_version())) path = tempfile.mktemp('.shp.zip') # download and extract it fetch_zip(url, path, feature_type, progress_dialog) extract_zip(path, output_base_path) if progress_dialog: progress_dialog.done(QDialog.Accepted)
def exposure_summary_table(aggregate_hazard, exposure_summary=None, callback=None): """Compute the summary from the aggregate hazard to analysis. Source layer : | haz_id | haz_class | aggr_id | aggr_name | exposure_count | Output layer : | exp_type | count_hazard_class | total | :param aggregate_hazard: The layer to aggregate vector layer. :type aggregate_hazard: QgsVectorLayer :param exposure_summary: The layer impact layer. :type exposure_summary: QgsVectorLayer :param callback: A function to all to indicate progress. The function should accept params 'current' (int), 'maximum' (int) and 'step' (str). Defaults to None. :type callback: function :return: The new tabular table, without geometry. :rtype: QgsVectorLayer .. versionadded:: 4.0 """ output_layer_name = summary_4_exposure_summary_table_steps[ 'output_layer_name'] source_fields = aggregate_hazard.keywords['inasafe_fields'] source_compulsory_fields = [ aggregation_id_field, aggregation_name_field, hazard_id_field, hazard_class_field, affected_field, total_field ] check_inputs(source_compulsory_fields, source_fields) absolute_values = create_absolute_values_structure(aggregate_hazard, ['all']) hazard_class = source_fields[hazard_class_field['key']] hazard_class_index = aggregate_hazard.fields().lookupField(hazard_class) unique_hazard = aggregate_hazard.uniqueValues(hazard_class_index) unique_exposure = read_dynamic_inasafe_field(source_fields, exposure_count_field) flat_table = FlatTable('hazard_class', 'exposure_class') request = QgsFeatureRequest() request.setFlags(QgsFeatureRequest.NoGeometry) for area in aggregate_hazard.getFeatures(): hazard_value = area[hazard_class_index] for exposure in unique_exposure: key_name = exposure_count_field['key'] % exposure field_name = source_fields[key_name] exposure_count = area[field_name] if not exposure_count: exposure_count = 0 flat_table.add_value(exposure_count, hazard_class=hazard_value, exposure_class=exposure) # We summarize every absolute values. for field, field_definition in list(absolute_values.items()): value = area[field] if not value: value = 0 field_definition[0].add_value(value, all='all') tabular = create_memory_layer(output_layer_name, QgsWkbTypes.NullGeometry) tabular.startEditing() field = create_field_from_definition(exposure_type_field) tabular.addAttribute(field) tabular.keywords['inasafe_fields'][exposure_type_field['key']] = ( exposure_type_field['field_name']) hazard_keywords = aggregate_hazard.keywords['hazard_keywords'] hazard = hazard_keywords['hazard'] classification = hazard_keywords['classification'] exposure_keywords = aggregate_hazard.keywords['exposure_keywords'] exposure = exposure_keywords['exposure'] hazard_affected = {} for hazard_class in unique_hazard: if (hazard_class == '' or hazard_class is None or (hasattr(hazard_class, 'isNull') and hazard_class.isNull())): hazard_class = 'NULL' field = create_field_from_definition(hazard_count_field, hazard_class) tabular.addAttribute(field) key = hazard_count_field['key'] % hazard_class value = hazard_count_field['field_name'] % hazard_class tabular.keywords['inasafe_fields'][key] = value hazard_affected[hazard_class] = post_processor_affected_function( exposure=exposure, hazard=hazard, classification=classification, hazard_class=hazard_class) field = create_field_from_definition(total_affected_field) tabular.addAttribute(field) tabular.keywords['inasafe_fields'][total_affected_field['key']] = ( total_affected_field['field_name']) # essentially have the same value as NULL_hazard_count # but with this, make sure that it exists in layer so it can be used for # reporting, and can be referenced to fields.py to take the label. field = create_field_from_definition(total_not_affected_field) tabular.addAttribute(field) tabular.keywords['inasafe_fields'][total_not_affected_field['key']] = ( total_not_affected_field['field_name']) field = create_field_from_definition(total_not_exposed_field) tabular.addAttribute(field) tabular.keywords['inasafe_fields'][total_not_exposed_field['key']] = ( total_not_exposed_field['field_name']) field = create_field_from_definition(total_field) tabular.addAttribute(field) tabular.keywords['inasafe_fields'][total_field['key']] = ( total_field['field_name']) summarization_dicts = {} if exposure_summary: summarization_dicts = summarize_result(exposure_summary) sorted_keys = sorted(summarization_dicts.keys()) for key in sorted_keys: summary_field = summary_rules[key]['summary_field'] field = create_field_from_definition(summary_field) tabular.addAttribute(field) tabular.keywords['inasafe_fields'][summary_field['key']] = ( summary_field['field_name']) # Only add absolute value if there is no summarization / no exposure # classification if not summarization_dicts: # For each absolute values for absolute_field in list(absolute_values.keys()): field_definition = definition(absolute_values[absolute_field][1]) field = create_field_from_definition(field_definition) tabular.addAttribute(field) key = field_definition['key'] value = field_definition['field_name'] tabular.keywords['inasafe_fields'][key] = value for exposure_type in unique_exposure: feature = QgsFeature() attributes = [exposure_type] total_affected = 0 total_not_affected = 0 total_not_exposed = 0 total = 0 for hazard_class in unique_hazard: if hazard_class == '' or hazard_class is None: hazard_class = 'NULL' value = flat_table.get_value(hazard_class=hazard_class, exposure_class=exposure_type) attributes.append(value) if hazard_affected[hazard_class] == not_exposed_class['key']: total_not_exposed += value elif hazard_affected[hazard_class]: total_affected += value else: total_not_affected += value total += value attributes.append(total_affected) attributes.append(total_not_affected) attributes.append(total_not_exposed) attributes.append(total) if summarization_dicts: for key in sorted_keys: attributes.append(summarization_dicts[key].get( exposure_type, 0)) else: for i, field in enumerate(absolute_values.values()): value = field[0].get_value(all='all') attributes.append(value) feature.setAttributes(attributes) tabular.addFeature(feature) # Sanity check ± 1 to the result. Disabled for now as it seems ± 1 is # not enough. ET 13/02/17 # total_computed = ( # total_affected + total_not_affected + total_not_exposed) # if not -1 < (total_computed - total) < 1: # raise ComputationError tabular.commitChanges() tabular.keywords['title'] = layer_purpose_exposure_summary_table['name'] if qgis_version() >= 21800: tabular.setName(tabular.keywords['title']) else: tabular.setLayerName(tabular.keywords['title']) tabular.keywords['layer_purpose'] = layer_purpose_exposure_summary_table[ 'key'] check_layer(tabular, has_geometry=False) return tabular
def analysis_summary(aggregate_hazard, analysis, callback=None): """Compute the summary from the aggregate hazard to analysis. Source layer : | haz_id | haz_class | aggr_id | aggr_name | total_feature | Target layer : | analysis_id | Output layer : | analysis_id | count_hazard_class | affected_count | total | :param aggregate_hazard: The layer to aggregate vector layer. :type aggregate_hazard: QgsVectorLayer :param analysis: The target vector layer where to write statistics. :type analysis: QgsVectorLayer :param callback: A function to all to indicate progress. The function should accept params 'current' (int), 'maximum' (int) and 'step' (str). Defaults to None. :type callback: function :return: The new target layer with summary. :rtype: QgsVectorLayer .. versionadded:: 4.0 """ output_layer_name = summary_3_analysis_steps['output_layer_name'] processing_step = summary_3_analysis_steps['step_name'] source_fields = aggregate_hazard.keywords['inasafe_fields'] target_fields = analysis.keywords['inasafe_fields'] target_compulsory_fields = [ analysis_id_field, analysis_name_field, ] check_inputs(target_compulsory_fields, target_fields) source_compulsory_fields = [ aggregation_id_field, aggregation_name_field, hazard_id_field, hazard_class_field, total_field ] check_inputs(source_compulsory_fields, source_fields) absolute_values = create_absolute_values_structure(aggregate_hazard, ['all']) hazard_class = source_fields[hazard_class_field['key']] hazard_class_index = aggregate_hazard.fieldNameIndex(hazard_class) unique_hazard = aggregate_hazard.uniqueValues(hazard_class_index) hazard_keywords = aggregate_hazard.keywords['hazard_keywords'] classification = hazard_keywords['classification'] total = source_fields[total_field['key']] flat_table = FlatTable('hazard_class') # First loop over the aggregate_hazard layer request = QgsFeatureRequest() request.setSubsetOfAttributes([hazard_class, total], aggregate_hazard.fields()) request.setFlags(QgsFeatureRequest.NoGeometry) for area in aggregate_hazard.getFeatures(): hazard_value = area[hazard_class_index] value = area[total] if not value or isinstance(value, QPyNullVariant) or isnan(value): # For isnan, see ticket #3812 value = 0 if not hazard_value or isinstance(hazard_value, QPyNullVariant): hazard_value = 'NULL' flat_table.add_value(value, hazard_class=hazard_value) # We summarize every absolute values. for field, field_definition in absolute_values.iteritems(): value = area[field] if not value or isinstance(value, QPyNullVariant): value = 0 field_definition[0].add_value(value, all='all') analysis.startEditing() shift = analysis.fields().count() counts = [ total_affected_field, total_not_affected_field, total_not_exposed_field, total_field ] add_fields(analysis, absolute_values, counts, unique_hazard, hazard_count_field) affected_sum = 0 not_affected_sum = 0 not_exposed_sum = 0 for area in analysis.getFeatures(request): total = 0 for i, val in enumerate(unique_hazard): if not val or isinstance(val, QPyNullVariant): val = 'NULL' sum = flat_table.get_value(hazard_class=val) total += sum analysis.changeAttributeValue(area.id(), shift + i, sum) affected = post_processor_affected_function( classification=classification, hazard_class=val) if affected == not_exposed_class['key']: not_exposed_sum += sum elif affected: affected_sum += sum else: not_affected_sum += sum # Affected field analysis.changeAttributeValue(area.id(), shift + len(unique_hazard), affected_sum) # Not affected field analysis.changeAttributeValue(area.id(), shift + len(unique_hazard) + 1, not_affected_sum) # Not exposed field analysis.changeAttributeValue(area.id(), shift + len(unique_hazard) + 2, not_exposed_sum) # Total field analysis.changeAttributeValue(area.id(), shift + len(unique_hazard) + 3, total) # Any absolute postprocessors for i, field in enumerate(absolute_values.itervalues()): value = field[0].get_value(all='all') analysis.changeAttributeValue(area.id(), shift + len(unique_hazard) + 4 + i, value) # Sanity check ± 1 to the result. Disabled for now as it seems ± 1 is not # enough. ET 13/02/17 # total_computed = ( # affected_sum + not_affected_sum + not_exposed_sum) # if not -1 < (total_computed - total) < 1: # raise ComputationError analysis.commitChanges() analysis.keywords['title'] = layer_purpose_analysis_impacted['name'] if qgis_version() >= 21600: analysis.setName(analysis.keywords['title']) else: analysis.setLayerName(analysis.keywords['title']) analysis.keywords['layer_purpose'] = layer_purpose_analysis_impacted['key'] check_layer(analysis) return analysis
def test_get_qgis_version(self): """Test we can get the version of QGIS.""" version = qgis_version() message = 'Got version %s of QGIS, but at least 214000 is needed' self.assertTrue(version > 21400, message)
def load_layer(full_layer_uri_string, name=None, provider=None): """Helper to load and return a single QGIS layer based on our layer URI. :param provider: The provider name to use if known to open the layer. Default to None, we will try to guess it, but it's much better if you can provide it. :type provider: :param name: The name of the layer. If not provided, it will be computed based on the URI. :type name: basestring :param full_layer_uri_string: Layer URI, with provider type. :type full_layer_uri_string: str :returns: tuple containing layer and its layer_purpose. :rtype: (QgsMapLayer, str) """ if provider: # Cool ! layer_path = full_layer_uri_string else: # Let's check if the driver is included in the path layer_path, provider = decode_full_layer_uri(full_layer_uri_string) if not provider: # Let's try to check if it's file based and look for a extension if '|' in layer_path: clean_uri = layer_path.split('|')[0] else: clean_uri = layer_path is_file_based = os.path.exists(clean_uri) if is_file_based: # Extract basename and absolute path file_name = os.path.split(layer_path)[ -1] # If path was absolute extension = os.path.splitext(file_name)[1] if extension in OGR_EXTENSIONS: provider = 'ogr' elif extension in GDAL_EXTENSIONS: provider = 'gdal' else: provider = None if not provider: layer = load_layer_without_provider(layer_path) else: layer = load_layer_with_provider(layer_path, provider) if not layer or not layer.isValid(): message = 'Layer "%s" is not valid' % layer_path LOGGER.debug(message) raise InvalidLayerError(message) # Define the name if not name: source = layer.source() if '|' in source: clean_uri = source.split('|')[0] else: clean_uri = source is_file_based = os.path.exists(clean_uri) if is_file_based: # Extract basename and absolute path file_name = os.path.split(layer_path)[-1] # If path was absolute name = os.path.splitext(file_name)[0] else: # Might be a DB, take the DB name source = QgsDataSourceUri(source) name = source.table() if not name: name = 'default' if qgis_version() >= 21800: layer.setName(name) else: layer.setLayerName(name) # update the layer keywords monkey_patch_keywords(layer) layer_purpose = layer.keywords.get('layer_purpose') return layer, layer_purpose
def test_multi_exposure(self): """Test we can run a multi exposure analysis.""" hazard_layer = load_test_vector_layer('gisv4', 'hazard', 'classified_vector.geojson') building_layer = load_test_vector_layer('gisv4', 'exposure', 'building-points.geojson') population_layer = load_test_vector_layer('gisv4', 'exposure', 'population.geojson') roads_layer = load_test_vector_layer('gisv4', 'exposure', 'roads.geojson') aggregation_layer = load_test_vector_layer('gisv4', 'aggregation', 'small_grid.geojson') impact_function = MultiExposureImpactFunction() impact_function.hazard = hazard_layer impact_function.exposures = [ building_layer, population_layer, roads_layer ] impact_function.aggregation = aggregation_layer code, message = impact_function.prepare() self.assertEqual(code, PREPARE_SUCCESS, message) code, message, exposure = impact_function.run() self.assertEqual(code, ANALYSIS_SUCCESS, message) # Test provenance hazard = definition(hazard_layer.keywords['hazard']) # exposure = definition(exposure_layer.keywords['exposure']) hazard_category = definition(hazard_layer.keywords['hazard_category']) expected_provenance = { provenance_gdal_version['provenance_key']: gdal.__version__, provenance_host_name['provenance_key']: gethostname(), provenance_user['provenance_key']: getpass.getuser(), provenance_os['provenance_key']: readable_os_version(), provenance_pyqt_version['provenance_key']: PYQT_VERSION_STR, provenance_qgis_version['provenance_key']: qgis_version(), provenance_qt_version['provenance_key']: QT_VERSION_STR, provenance_inasafe_version['provenance_key']: get_version(), provenance_aggregation_layer['provenance_key']: aggregation_layer.source(), provenance_aggregation_layer_id['provenance_key']: aggregation_layer.id(), # provenance_exposure_layer['provenance_key']: # exposure_layer.source(), # provenance_exposure_layer_id['provenance_key']: # exposure_layer.id(), provenance_hazard_layer['provenance_key']: hazard_layer.source(), provenance_hazard_layer_id['provenance_key']: hazard_layer.id(), provenance_aggregation_keywords['provenance_key']: deepcopy(aggregation_layer.keywords), # provenance_exposure_keywords['provenance_key']: # deepcopy(exposure_layer.keywords), provenance_hazard_keywords['provenance_key']: deepcopy(hazard_layer.keywords), } self.maxDiff = None expected_provenance.update({ provenance_analysis_extent['provenance_key']: impact_function.analysis_extent.exportToWkt(), provenance_impact_function_name['provenance_key']: impact_function.name, # provenance_requested_extent['provenance_key']: impact_function. # requested_extent, provenance_data_store_uri['provenance_key']: impact_function.datastore.uri_path, provenance_start_datetime['provenance_key']: impact_function.start_datetime, provenance_end_datetime['provenance_key']: impact_function.end_datetime, provenance_duration['provenance_key']: impact_function.duration }) self.assertDictContainsSubset(expected_provenance, impact_function.provenance) output_layer_provenance_keys = [ provenance_layer_aggregation_summary['provenance_key'], provenance_layer_analysis_impacted['provenance_key'], ] for key in output_layer_provenance_keys: self.assertIn(key, impact_function.provenance.keys()) # Test serialization/deserialization output_metadata = impact_function.aggregation_summary.keywords new_impact_function = MultiExposureImpactFunction. \ load_from_output_metadata(output_metadata) self.assertEqualImpactFunction(impact_function, new_impact_function) # Check the analysis layer id equal with the actual layer old_analysis_layer_id = impact_function.provenance[ provenance_layer_analysis_impacted['provenance_key']] new_analysis_layer_id = new_impact_function.provenance[ provenance_layer_analysis_impacted['provenance_key']] self.assertEqual(old_analysis_layer_id, new_analysis_layer_id)
def multi_exposure_aggregation_summary(aggregation, intermediate_layers): """Merge intermediate aggregations into one aggregation summary. Source layer : | aggr_id | aggr_name | count of affected features per exposure type Target layer : | aggregation_id | aggregation_name | Output layer : | aggr_id | aggr_name | count of affected per exposure type for each :param aggregation: The target vector layer where to write statistics. :type aggregation: QgsVectorLayer :param intermediate_layers: List of aggregation layer for a single exposure :type intermediate_layers: list :return: The new target layer with summary. :rtype: QgsVectorLayer .. versionadded:: 4.3 """ target_index_field_name = ( aggregation.keywords['inasafe_fields'][aggregation_id_field['key']]) aggregation.startEditing() request = QgsFeatureRequest() request.setFlags(QgsFeatureRequest.NoGeometry) for layer in intermediate_layers: source_fields = layer.keywords['inasafe_fields'] exposure = layer.keywords['exposure_keywords']['exposure'] unique_exposure = read_dynamic_inasafe_field( source_fields, affected_exposure_count_field, [total_affected_field]) field_map = {} for exposure_class in unique_exposure: field = create_field_from_definition( exposure_affected_exposure_type_count_field, name=exposure, sub_name=exposure_class) aggregation.addAttribute(field) source_field_index = layer.fieldNameIndex( affected_exposure_count_field['field_name'] % exposure_class) target_field_index = aggregation.fieldNameIndex(field.name()) field_map[source_field_index] = target_field_index # Total affected field field = create_field_from_definition(exposure_total_not_affected_field, exposure) aggregation.addAttribute(field) source_field_index = layer.fieldNameIndex( total_affected_field['field_name']) target_field_index = aggregation.fieldNameIndex(field.name()) field_map[source_field_index] = target_field_index # Get Aggregation ID from original feature index = (layer.fieldNameIndex( source_fields[aggregation_id_field['key']])) for source_feature in layer.getFeatures(request): target_expression = QgsFeatureRequest() target_expression.setFlags(QgsFeatureRequest.NoGeometry) expression = '\"{field_name}\" = {id_value}'.format( field_name=target_index_field_name, id_value=source_feature[index]) target_expression.setFilterExpression(expression) iterator = aggregation.getFeatures(target_expression) target_feature = next(iterator) # It must return only 1 feature. for source_field, target_field in field_map.iteritems(): aggregation.changeAttributeValue(target_feature.id(), target_field, source_feature[source_field]) try: next(iterator) except StopIteration: # Everything is fine, it's normal. pass else: # This should never happen ! IDs are duplicated in the # aggregation layer. raise Exception( 'Aggregation IDs are duplicated in the aggregation layer. ' 'We can\'t make any joins.') aggregation.commitChanges() aggregation.keywords['title'] = ( layer_purpose_aggregation_summary['multi_exposure_name']) aggregation.keywords['layer_purpose'] = ( layer_purpose_aggregation_summary['key']) # Set up the extra keywords so everyone knows it's a # multi exposure analysis result. extra_keywords = { extra_keyword_analysis_type['key']: MULTI_EXPOSURE_ANALYSIS_FLAG } aggregation.keywords['extra_keywords'] = extra_keywords if qgis_version() >= 21600: aggregation.setName(aggregation.keywords['title']) else: aggregation.setLayerName(aggregation.keywords['title']) return aggregation
def zonal_stats(raster, vector): """Reclassify a continuous raster layer. Issue https://github.com/inasafe/inasafe/issues/3190 The algorithm will take care about projections. We don't want to reproject the raster layer. So if CRS are different, we reproject the vector layer and then we do a lookup from the reprojected layer to the original vector layer. :param raster: The raster layer. :type raster: QgsRasterLayer :param vector: The vector layer. :type vector: QgsVectorLayer :return: The output of the zonal stats. :rtype: QgsVectorLayer .. versionadded:: 4.0 """ output_layer_name = zonal_stats_steps['output_layer_name'] exposure = raster.keywords['exposure'] if raster.crs().authid() != vector.crs().authid(): layer = reproject(vector, raster.crs()) # We prepare the copy output_layer = create_memory_layer(output_layer_name, vector.geometryType(), vector.crs(), vector.fields()) copy_layer(vector, output_layer) else: layer = create_memory_layer(output_layer_name, vector.geometryType(), vector.crs(), vector.fields()) copy_layer(vector, layer) input_band = layer.keywords.get('active_band', 1) analysis = QgsZonalStatistics(layer, raster, 'exposure_', input_band, QgsZonalStatistics.Sum) result = analysis.calculateStatistics(None) LOGGER.debug(tr('Zonal stats on %s : %s' % (raster.source(), result))) output_field = exposure_count_field['field_name'] % exposure if raster.crs().authid() != vector.crs().authid(): output_layer.startEditing() field = create_field_from_definition(exposure_count_field, exposure) output_layer.addAttribute(field) new_index = output_layer.fields().lookupField(field.name()) old_index = layer.fields().lookupField('exposure_sum') for feature_input, feature_output in zip(layer.getFeatures(), output_layer.getFeatures()): output_layer.changeAttributeValue(feature_input.id(), new_index, feature_input[old_index]) output_layer.commitChanges() layer = output_layer else: fields_to_rename = {'exposure_sum': output_field} if qgis_version() >= 21600: rename_fields(layer, fields_to_rename) else: copy_fields(layer, fields_to_rename) remove_fields(layer, list(fields_to_rename.keys())) layer.commitChanges() # The zonal stats is producing some None values. We need to fill these # with 0. See issue : #3778 # We should start a new editing session as previous fields need to be # committed first. layer.startEditing() request = QgsFeatureRequest() expression = '\"%s\" is None' % output_field request.setFilterExpression(expression) request.setFlags(QgsFeatureRequest.NoGeometry) index = layer.fields().lookupField(output_field) for feature in layer.getFeatures(): if feature[output_field] is None: layer.changeAttributeValue(feature.id(), index, 0) layer.commitChanges() layer.keywords = raster.keywords.copy() layer.keywords['inasafe_fields'] = vector.keywords['inasafe_fields'].copy() layer.keywords['inasafe_default_values'] = ( raster.keywords['inasafe_default_values'].copy()) key = exposure_count_field['key'] % raster.keywords['exposure'] # Special case here, one field is the exposure count and the total. layer.keywords['inasafe_fields'][key] = output_field layer.keywords['inasafe_fields'][total_field['key']] = output_field layer.keywords['exposure_keywords'] = raster.keywords.copy() layer.keywords['hazard_keywords'] = vector.keywords[ 'hazard_keywords'].copy() layer.keywords['aggregation_keywords'] = ( vector.keywords['aggregation_keywords']) layer.keywords['layer_purpose'] = ( layer_purpose_aggregate_hazard_impacted['key']) layer.keywords['title'] = output_layer_name check_layer(layer) return layer
def generate_reports(self): """Generate PDF reports for each aggregation unit using map composer. First the report template is loaded with the renderer from two impact layers. After it's loaded, if it is not aggregated then we just use composition to produce report. Since there are two impact maps here, we need to set a new extent for these impact maps by a simple calculation. If it is not aggregated then we use a powerful QGIS atlas generation on composition. Since we save each report table representing each aggregated area on self.html_report (which is a dictionary with the aggregation area name as a key and its path as a value), and we set the aggregation area name as current filename on atlas generation, we can match these two so that we have the right report table for each report. For those two cases, we use the same template. The report table is basically an HTML frame. Of course after the merging process is done, we delete each report table on self.html_reports physically on disk. """ # Set the layer set layer_set = [ self.first_impact['layer'].id(), self.second_impact['layer'].id() ] # If aggregated, append chosen aggregation layer if not self.entire_area_mode: layer_set.append(self.aggregation['layer'].id()) # Instantiate Map Settings for Composition if qgis_version() < 20400: map_settings = QgsMapRenderer() map_settings.setLayerSet(layer_set) else: map_settings = QgsMapSettings() map_settings.setLayers(layer_set) # Create composition composition = self.load_template(map_settings) # Get Map composer_map = composition.getComposerItemById('impact-map') # Get HTML Report Frame html_report_item = \ composition.getComposerItemById('merged-report-table') html_report_frame = composition.getComposerHtmlByItem(html_report_item) if self.entire_area_mode: # Get composer map size composer_map_width = composer_map.boundingRect().width() composer_map_height = composer_map.boundingRect().height() # Set the extent from two impact layers to fit into composer map composer_size_ratio = float(composer_map_height / composer_map_width) # The extent of two impact layers min_x = min(self.first_impact['layer'].extent().xMinimum(), self.second_impact['layer'].extent().xMinimum()) min_y = min(self.first_impact['layer'].extent().yMinimum(), self.second_impact['layer'].extent().yMinimum()) max_x = max(self.first_impact['layer'].extent().xMaximum(), self.second_impact['layer'].extent().xMaximum()) max_y = max(self.first_impact['layer'].extent().yMaximum(), self.second_impact['layer'].extent().yMaximum()) max_width = max_x - min_x max_height = max_y - min_y layers_size_ratio = float(max_height / max_width) center_x = min_x + float(max_width / 2.0) center_y = min_y + float(max_height / 2.0) # The extent should fit the composer map size new_width = max_width new_height = max_height # QgsComposerMap only overflows to height, so if it overflows, # the extent of the width should be widened if layers_size_ratio > composer_size_ratio: new_width = max_height / composer_size_ratio # Set new extent fit_min_x = center_x - (new_width / 2.0) fit_max_x = center_x + (new_width / 2.0) fit_min_y = center_y - (new_height / 2.0) fit_max_y = center_y + (new_height / 2.0) # Create the extent and set it to the map # noinspection PyCallingNonCallable map_extent = QgsRectangle(fit_min_x, fit_min_y, fit_max_x, fit_max_y) composer_map.setNewExtent(map_extent) # Add grid to composer map split_count = 5 x_interval = new_width / split_count composer_map.setGridIntervalX(x_interval) y_interval = new_height / split_count composer_map.setGridIntervalY(y_interval) # Self.html_reports must have only 1 key value pair area_title = list(self.html_reports.keys())[0] # Set Report Summary summary_report = composition.getComposerItemById('summary-report') summary_report.setText(self.summary_report[area_title]) # Set Aggregation Area Label area_label = composition.getComposerItemById('aggregation-area') area_label.setText(area_title.title()) # Set merged-report-table html_report_path = self.html_reports[area_title] # noinspection PyArgumentList html_frame_url = QUrl.fromLocalFile(html_report_path) html_report_frame.setUrl(html_frame_url) # Export composition to PDF file file_name = '_'.join(area_title.split()) file_path = '%s.pdf' % file_name path = os.path.join(self.out_dir, file_path) composition.exportAsPDF(path) else: # Create atlas composition: # noinspection PyCallingNonCallable atlas = QgsAtlasComposition(composition) # Set coverage layer # Map will be clipped by features from this layer: atlas.setCoverageLayer(self.aggregation['layer']) # Add grid to composer map from coverage layer split_count = 5 map_width = self.aggregation['layer'].extent().width() map_height = self.aggregation['layer'].extent().height() x_interval = map_width / split_count composer_map.setGridIntervalX(x_interval) y_interval = map_height / split_count composer_map.setGridIntervalY(y_interval) # Set composer map that will be used for printing atlas atlas.setComposerMap(composer_map) # set output filename pattern atlas.setFilenamePattern(self.aggregation['aggregation_attribute']) # Start rendering atlas.beginRender() # Iterate all aggregation unit in aggregation layer for i in range(0, atlas.numFeatures()): atlas.prepareForFeature(i) current_filename = atlas.currentFilename() file_name = '_'.join(current_filename.split()) file_path = '%s.pdf' % file_name path = os.path.join(self.out_dir, file_path) # Only print the area that has the report area_title = current_filename.lower() if area_title in self.summary_report: # Set Report Summary summary_report = composition.getComposerItemById( 'summary-report') summary_report.setText(self.summary_report[area_title]) # Set Aggregation Area Label area_label = composition.getComposerItemById( 'aggregation-area') area_label.setText(area_title.title()) # Set merged-report-table html_report_path = self.html_reports[area_title] # noinspection PyArgumentList html_frame_url = QUrl.fromLocalFile(html_report_path) html_report_frame.setUrl(html_frame_url) # Export composition to PDF file composition.exportAsPDF(path) # End of rendering atlas.endRender()
def test_issue71(self): """Test issue #71 in github - cbo changes should update ok button.""" # See https://github.com/AIFDR/inasafe/issues/71 # Push OK with the left mouse button print 'Using QGIS: %s' % qgis_version() settings = QtCore.QSettings() settings.setValue( 'inasafe/analysis_extents_mode', 'HazardExposure') self.tearDown() button = self.dock.pbnRunStop # First part of scenario should have enabled run file_list = [ test_data_path('hazard', 'continuous_flood_20_20.asc'), test_data_path('exposure', 'pop_binary_raster_20_20.asc')] hazard_layer_count, exposure_layer_count = load_layers(file_list) message = ( 'Incorrect number of Hazard layers: expected 1 got %s' % hazard_layer_count) self.assertTrue(hazard_layer_count == 1, message) message = ( 'Incorrect number of Exposure layers: expected 1 got %s' % exposure_layer_count) self.assertTrue(exposure_layer_count == 1, message) message = 'Run button was not enabled' self.assertTrue(button.isEnabled(), message) # Second part of scenario - run disabled when adding invalid layer # and select it - run should be disabled path = os.path.join(TESTDATA, 'issue71.tif') file_list = [path] # This layer has incorrect keywords clear_flag = False _, _ = load_layers(file_list, clear_flag) # set exposure to : Population Count (5kmx5km) # by moving one down self.dock.cboExposure.setCurrentIndex( self.dock.cboExposure.currentIndex() + 1) actual_dict = get_ui_state(self.dock) expected_dict = { 'Run Button Enabled': False, 'Impact Function Id': '', 'Impact Function Title': '', 'Hazard': 'Continuous Flood', 'Exposure': 'Population Count (5kmx5km)'} message = (( 'Run button was not disabled when exposure set to \n%s' '\nUI State: \n%s\nExpected State:\n%s\n%s') % ( self.dock.cboExposure.currentText(), actual_dict, expected_dict, combos_to_string(self.dock))) self.assertTrue(expected_dict == actual_dict, message) # Now select again a valid layer and the run button # should be enabled self.dock.cboExposure.setCurrentIndex( self.dock.cboExposure.currentIndex() - 1) message = ( 'Run button was not enabled when exposure set to \n%s' % self.dock.cboExposure.currentText()) self.assertTrue(button.isEnabled(), message)
def generate_reports(self): """Generate PDF reports for each aggregation unit using map composer. First the report template is loaded with the renderer from two impact layers. After it's loaded, if it is not aggregated then we just use composition to produce report. Since there are two impact maps here, we need to set a new extent for these impact maps by a simple calculation. If it is not aggregated then we use a powerful QGIS atlas generation on composition. Since we save each report table representing each aggregated area on self.html_report (which is a dictionary with the aggregation area name as a key and its path as a value), and we set the aggregation area name as current filename on atlas generation, we can match these two so that we have the right report table for each report. For those two cases, we use the same template. The report table is basically an HTML frame. Of course after the merging process is done, we delete each report table on self.html_reports physically on disk. """ # Set the layer set layer_set = [self.first_impact['layer'].id(), self.second_impact['layer'].id()] # If aggregated, append chosen aggregation layer if not self.entire_area_mode: layer_set.append(self.aggregation['layer'].id()) # Instantiate Map Settings for Composition if qgis_version() < 20400: map_settings = QgsMapRenderer() map_settings.setLayerSet(layer_set) else: map_settings = QgsMapSettings() map_settings.setLayers(layer_set) # Create composition composition = self.load_template(map_settings) # Get Map composer_map = composition.getComposerItemById('impact-map') # Get HTML Report Frame html_report_item = \ composition.getComposerItemById('merged-report-table') html_report_frame = composition.getComposerHtmlByItem(html_report_item) if self.entire_area_mode: # Get composer map size composer_map_width = composer_map.boundingRect().width() composer_map_height = composer_map.boundingRect().height() # Set the extent from two impact layers to fit into composer map composer_size_ratio = float( composer_map_height / composer_map_width) # The extent of two impact layers min_x = min(self.first_impact['layer'].extent().xMinimum(), self.second_impact['layer'].extent().xMinimum()) min_y = min(self.first_impact['layer'].extent().yMinimum(), self.second_impact['layer'].extent().yMinimum()) max_x = max(self.first_impact['layer'].extent().xMaximum(), self.second_impact['layer'].extent().xMaximum()) max_y = max(self.first_impact['layer'].extent().yMaximum(), self.second_impact['layer'].extent().yMaximum()) max_width = max_x - min_x max_height = max_y - min_y layers_size_ratio = float(max_height / max_width) center_x = min_x + float(max_width / 2.0) center_y = min_y + float(max_height / 2.0) # The extent should fit the composer map size new_width = max_width new_height = max_height # QgsComposerMap only overflows to height, so if it overflows, # the extent of the width should be widened if layers_size_ratio > composer_size_ratio: new_width = max_height / composer_size_ratio # Set new extent fit_min_x = center_x - (new_width / 2.0) fit_max_x = center_x + (new_width / 2.0) fit_min_y = center_y - (new_height / 2.0) fit_max_y = center_y + (new_height / 2.0) # Create the extent and set it to the map # noinspection PyCallingNonCallable map_extent = QgsRectangle( fit_min_x, fit_min_y, fit_max_x, fit_max_y) composer_map.setNewExtent(map_extent) # Add grid to composer map split_count = 5 x_interval = new_width / split_count composer_map.setGridIntervalX(x_interval) y_interval = new_height / split_count composer_map.setGridIntervalY(y_interval) # Self.html_reports must have only 1 key value pair # Rizky: If layer components were aggregated, html_reports will # have several key value pairs. So we get the last value because # it is for the total/entire area area_title = list(self.html_reports.keys())[-1] # Set Report Summary summary_report = composition.getComposerItemById('summary-report') summary_report.setText(self.summary_report[area_title]) # Set Aggregation Area Label area_label = composition.getComposerItemById('aggregation-area') area_label.setText(area_title.title()) # Set merged-report-table html_report_path = self.html_reports[area_title] # noinspection PyArgumentList html_frame_url = QUrl.fromLocalFile(html_report_path) html_report_frame.setUrl(html_frame_url) # Export composition to PDF file file_name = '_'.join(area_title.split()) file_path = '%s.pdf' % file_name path = os.path.join(self.out_dir, file_path) composition.exportAsPDF(path) else: # Create atlas composition: # noinspection PyCallingNonCallable atlas = QgsAtlasComposition(composition) # Set coverage layer # Map will be clipped by features from this layer: atlas.setCoverageLayer(self.aggregation['layer']) # Add grid to composer map from coverage layer split_count = 5 map_width = self.aggregation['layer'].extent().width() map_height = self.aggregation['layer'].extent().height() x_interval = map_width / split_count composer_map.setGridIntervalX(x_interval) y_interval = map_height / split_count composer_map.setGridIntervalY(y_interval) # Set composer map that will be used for printing atlas atlas.setComposerMap(composer_map) # set output filename pattern atlas.setFilenamePattern( self.aggregation['aggregation_attribute']) # Start rendering atlas.beginRender() # Iterate all aggregation unit in aggregation layer for i in range(0, atlas.numFeatures()): atlas.prepareForFeature(i) current_filename = atlas.currentFilename() file_name = '_'.join(current_filename.split()) file_path = '%s.pdf' % file_name path = os.path.join(self.out_dir, file_path) # Only print the area that has the report area_title = current_filename.lower() if area_title in self.summary_report: # Set Report Summary summary_report = composition.getComposerItemById( 'summary-report') summary_report.setText(self.summary_report[area_title]) # Set Aggregation Area Label area_label = composition.getComposerItemById( 'aggregation-area') area_label.setText(area_title.title()) # Set merged-report-table html_report_path = self.html_reports[area_title] # noinspection PyArgumentList html_frame_url = QUrl.fromLocalFile(html_report_path) html_report_frame.setUrl(html_frame_url) # Export composition to PDF file composition.exportAsPDF(path) # End of rendering atlas.endRender()
def draw_composition(self): """Draw all the components in the composition.""" safe_logo = self.composition.getComposerItemById('safe-logo') north_arrow = self.composition.getComposerItemById('north-arrow') organisation_logo = self.composition.getComposerItemById( 'organisation-logo') if qgis_version() < 20600: if safe_logo is not None: safe_logo.setPictureFile(self.safe_logo) if north_arrow is not None: north_arrow.setPictureFile(self.north_arrow) if organisation_logo is not None: organisation_logo.setPictureFile(self.organisation_logo) else: if safe_logo is not None: safe_logo.setPicturePath(self.safe_logo) if north_arrow is not None: north_arrow.setPicturePath(self.north_arrow) if organisation_logo is not None: organisation_logo.setPicturePath(self.organisation_logo) # Set impact report table table = self.composition.getComposerItemById('impact-report') if table is not None: text = self._keyword_io.read_keywords(self.layer, 'impact_summary') if text is None: text = '' table.setText(text) table.setHtmlState(1) # Get the main map canvas on the composition and set its extents to # the event. composer_map = self.composition.getComposerItemById('impact-map') if composer_map is not None: # Recenter the composer map on the center of the extent # Note that since the composer map is square and the canvas may be # arbitrarily shaped, we center based on the longest edge canvas_extent = self.extent width = canvas_extent.width() height = canvas_extent.height() longest_width = width if width < height: longest_width = height half_length = longest_width / 2 center = canvas_extent.center() min_x = center.x() - half_length max_x = center.x() + half_length min_y = center.y() - half_length max_y = center.y() + half_length # noinspection PyCallingNonCallable square_extent = QgsRectangle(min_x, min_y, max_x, max_y) composer_map.setNewExtent(square_extent) # calculate intervals for grid split_count = 5 x_interval = square_extent.width() / split_count composer_map.setGridIntervalX(x_interval) y_interval = square_extent.height() / split_count composer_map.setGridIntervalY(y_interval) legend = self.composition.getComposerItemById('impact-legend') if legend is not None: legend_attributes = self.map_legend_attributes legend_title = legend_attributes.get('legend_title', None) symbol_count = 1 # noinspection PyUnresolvedReferences if self.layer.type() == QgsMapLayer.VectorLayer: renderer = self.layer.rendererV2() if renderer.type() in ['', '']: symbol_count = len(self.layer.legendSymbologyItems()) else: renderer = self.layer.renderer() if renderer.type() in ['']: symbol_count = len(self.layer.legendSymbologyItems()) if symbol_count <= 5: legend.setColumnCount(1) else: legend.setColumnCount(symbol_count / 5 + 1) if legend_title is None: legend_title = "" legend.setTitle(legend_title) # Set Legend # Since QGIS 2.6, legend.model() is obsolete if qgis_version() < 20600: legend.model().setLayerSet([self.layer.id()]) legend.synchronizeWithModel() else: root_group = legend.modelV2().rootGroup() root_group.addLayer(self.layer) legend.synchronizeWithModel()
def aggregation_summary(aggregate_hazard, aggregation, callback=None): """Compute the summary from the aggregate hazard to the analysis layer. Source layer : | haz_id | haz_class | aggr_id | aggr_name | total_feature | Target layer : | aggr_id | aggr_name | Output layer : | aggr_id | aggr_name | count of affected features per exposure type :param aggregate_hazard: The layer to aggregate vector layer. :type aggregate_hazard: QgsVectorLayer :param aggregation: The aggregation vector layer where to write statistics. :type aggregation: QgsVectorLayer :param callback: A function to all to indicate progress. The function should accept params 'current' (int), 'maximum' (int) and 'step' (str). Defaults to None. :type callback: function :return: The new aggregation layer with summary. :rtype: QgsVectorLayer .. versionadded:: 4.0 """ output_layer_name = summary_2_aggregation_steps['output_layer_name'] processing_step = summary_2_aggregation_steps['step_name'] source_fields = aggregate_hazard.keywords['inasafe_fields'] target_fields = aggregation.keywords['inasafe_fields'] target_compulsory_fields = [ aggregation_id_field, aggregation_name_field, ] check_inputs(target_compulsory_fields, target_fields) # Missing exposure_count_field source_compulsory_fields = [ aggregation_id_field, aggregation_name_field, hazard_id_field, hazard_class_field, affected_field, ] check_inputs(source_compulsory_fields, source_fields) pattern = exposure_count_field['key'] pattern = pattern.replace('%s', '') unique_exposure = read_dynamic_inasafe_field(source_fields, exposure_count_field) absolute_values = create_absolute_values_structure(aggregate_hazard, ['aggregation_id']) flat_table = FlatTable('aggregation_id', 'exposure_class') aggregation_index = source_fields[aggregation_id_field['key']] # We want to loop over affected features only. request = QgsFeatureRequest() request.setFlags(QgsFeatureRequest.NoGeometry) expression = '\"%s\" = \'%s\'' % (affected_field['field_name'], tr('True')) request.setFilterExpression(expression) for area in aggregate_hazard.getFeatures(request): for key, name_field in source_fields.iteritems(): if key.endswith(pattern): aggregation_id = area[aggregation_index] exposure_class = key.replace(pattern, '') value = area[name_field] flat_table.add_value(value, aggregation_id=aggregation_id, exposure_class=exposure_class) # We summarize every absolute values. for field, field_definition in absolute_values.iteritems(): value = area[field] if not value or isinstance(value, QPyNullVariant): value = 0 field_definition[0].add_value( value, aggregation_id=area[aggregation_index], ) shift = aggregation.fields().count() aggregation.startEditing() add_fields(aggregation, absolute_values, [total_affected_field], unique_exposure, affected_exposure_count_field) aggregation_index = target_fields[aggregation_id_field['key']] request = QgsFeatureRequest() request.setFlags(QgsFeatureRequest.NoGeometry) for area in aggregation.getFeatures(request): aggregation_value = area[aggregation_index] total = 0 for i, val in enumerate(unique_exposure): sum = flat_table.get_value(aggregation_id=aggregation_value, exposure_class=val) total += sum aggregation.changeAttributeValue(area.id(), shift + i, sum) aggregation.changeAttributeValue(area.id(), shift + len(unique_exposure), total) for i, field in enumerate(absolute_values.itervalues()): value = field[0].get_value(aggregation_id=aggregation_value, ) target_index = shift + len(unique_exposure) + 1 + i aggregation.changeAttributeValue(area.id(), target_index, value) aggregation.commitChanges() aggregation.keywords['title'] = layer_purpose_aggregation_summary['name'] if qgis_version() >= 21800: aggregation.setName(aggregation.keywords['title']) else: aggregation.setLayerName(aggregation.keywords['title']) aggregation.keywords['layer_purpose'] = ( layer_purpose_aggregation_summary['key']) check_layer(aggregation) return aggregation
def load_template(self, map_settings): """Load composer template for merged report. Validate it as well. The template needs to have: 1. QgsComposerMap with id 'impact-map' for merged impact map. 2. QgsComposerPicture with id 'safe-logo' for InaSAFE logo. 3. QgsComposerLabel with id 'summary-report' for a summary of two impacts. 4. QgsComposerLabel with id 'aggregation-area' to indicate the area of aggregation. 5. QgsComposerScaleBar with id 'map-scale' for impact map scale. 6. QgsComposerLegend with id 'map-legend' for impact map legend. 7. QgsComposerPicture with id 'organisation-logo' for organisation logo. 8. QgsComposerLegend with id 'impact-legend' for map legend. 9. QgsComposerHTML with id 'merged-report-table' for the merged report. :param map_settings: Map settings. :type map_settings: QgsMapSettings, QgsMapRenderer """ # Create Composition template_composition = TemplateComposition( self.template_path, map_settings) # Validate the component in the template component_ids = ['impact-map', 'safe-logo', 'summary-report', 'aggregation-area', 'map-scale', 'map-legend', 'organisation-logo', 'merged-report-table'] template_composition.component_ids = component_ids if len(template_composition.missing_elements) > 0: raise ReportCreationError(self.tr( 'Components: %s could not be found' % ', '.join( template_composition.missing_elements))) # Prepare map substitution and set to composition impact_title = '%s and %s' % ( self.first_impact['map_title'], self.second_impact['map_title']) substitution_map = { 'impact-title': impact_title, 'hazard-title': self.first_impact['hazard_title'], 'disclaimer': self.disclaimer } template_composition.substitution = substitution_map # Load Template try: template_composition.load_template() except TemplateLoadingError: raise # Draw Composition # Set InaSAFE logo composition = template_composition.composition safe_logo = composition.getComposerItemById('safe-logo') safe_logo.setPictureFile(self.safe_logo_path) # set organisation logo org_logo = composition.getComposerItemById('organisation-logo') org_logo.setPictureFile(self.organisation_logo_path) # Set Map Legend legend = composition.getComposerItemById('map-legend') if qgis_version() < 20400: layers = map_settings.layerSet() else: layers = map_settings.layers() if qgis_version() < 20600: legend.model().setLayerSet(layers) legend.synchronizeWithModel() else: root_group = legend.modelV2().rootGroup() layer_ids = map_settings.layers() for layer_id in layer_ids: # noinspection PyUnresolvedReferences layer = QgsMapLayerRegistry.instance().mapLayer(layer_id) root_group.addLayer(layer) legend.synchronizeWithModel() return composition
def draw_composition(self): """Draw all the components in the composition.""" # This is deprecated - use inasafe-logo-<colour> rather safe_logo = self.composition.getComposerItemById( 'safe-logo') # Next two options replace safe logo in 3.2 black_inasafe_logo = self.composition.getComposerItemById( 'black-inasafe-logo') white_inasafe_logo = self.composition.getComposerItemById( 'white-inasafe-logo') north_arrow = self.composition.getComposerItemById( 'north-arrow') organisation_logo = self.composition.getComposerItemById( 'organisation-logo') supporters_logo = self.composition.getComposerItemById( 'supporters-logo') if qgis_version() < 20600: if safe_logo is not None: # its deprecated so just use black_inasafe_logo safe_logo.setPictureFile(self.inasafe_logo) if black_inasafe_logo is not None: black_inasafe_logo.setPictureFile(self._black_inasafe_logo) if white_inasafe_logo is not None: white_inasafe_logo.setPictureFile(self._white_inasafe_logo) if north_arrow is not None: north_arrow.setPictureFile(self.north_arrow) if organisation_logo is not None: organisation_logo.setPictureFile(self.organisation_logo) if supporters_logo is not None: supporters_logo.setPictureFile(self.supporters_logo) else: if safe_logo is not None: # its deprecated so just use black_inasafe_logo safe_logo.setPicturePath(self.inasafe_logo) if black_inasafe_logo is not None: black_inasafe_logo.setPicturePath(self._black_inasafe_logo) if white_inasafe_logo is not None: white_inasafe_logo.setPicturePath(self._white_inasafe_logo) if north_arrow is not None: north_arrow.setPicturePath(self.north_arrow) if organisation_logo is not None: organisation_logo.setPicturePath(self.organisation_logo) if supporters_logo is not None: supporters_logo.setPicturePath(self.supporters_logo) # Set impact report table table = self.composition.getComposerItemById('impact-report') if table is not None: text = self._keyword_io.read_keywords(self.layer, 'impact_summary') if text is None: text = '' table.setText(text) table.setHtmlState(1) # Get the main map canvas on the composition and set its extents to # the event. composer_map = self.composition.getComposerItemById('impact-map') if composer_map is not None: # Recenter the composer map on the center of the extent # Note that since the composer map is square and the canvas may be # arbitrarily shaped, we center based on the longest edge canvas_extent = self.extent width = canvas_extent.width() height = canvas_extent.height() longest_width = width if width > height else height half_length = longest_width / 2 center = canvas_extent.center() min_x = center.x() - half_length max_x = center.x() + half_length min_y = center.y() - half_length max_y = center.y() + half_length # noinspection PyCallingNonCallable square_extent = QgsRectangle(min_x, min_y, max_x, max_y) composer_map.setNewExtent(square_extent) # calculate intervals for grid split_count = 5 x_interval = square_extent.width() / split_count composer_map.setGridIntervalX(x_interval) y_interval = square_extent.height() / split_count composer_map.setGridIntervalY(y_interval) legend = self.composition.getComposerItemById('impact-legend') if legend is not None: symbol_count = ImpactReport.symbol_count(self.layer) # add legend symbol count from extra_layers for l in self.extra_layers: symbol_count += ImpactReport.symbol_count(l) if symbol_count <= 5: legend.setColumnCount(1) else: legend.setColumnCount(symbol_count / 5 + 1) # Set back to blank to #2409 legend.setTitle("") # Set Legend # Since QGIS 2.6, legend.model() is obsolete if qgis_version() < 20600: layer_set = [self.layer.id()] layer_set += [l.id() for l in self.extra_layers] legend.model().setLayerSet(layer_set) legend.synchronizeWithModel() else: root_group = legend.modelV2().rootGroup() root_group.addLayer(self.layer) for l in self.extra_layers: root_group.addLayer(l) legend.synchronizeWithModel()
def aggregate_hazard_summary(impact, aggregate_hazard): """Compute the summary from the source layer to the aggregate_hazard layer. Source layer : |exp_id|exp_class|haz_id|haz_class|aggr_id|aggr_name|affected|extra*| Target layer : | aggr_id | aggr_name | haz_id | haz_class | extra* | Output layer : |aggr_id| aggr_name|haz_id|haz_class|affected|extra*|count ber exposure*| :param impact: The layer to aggregate vector layer. :type impact: QgsVectorLayer :param aggregate_hazard: The aggregate_hazard vector layer where to write statistics. :type aggregate_hazard: QgsVectorLayer :return: The new aggregate_hazard layer with summary. :rtype: QgsVectorLayer .. versionadded:: 4.0 """ source_fields = impact.keywords['inasafe_fields'] target_fields = aggregate_hazard.keywords['inasafe_fields'] target_compulsory_fields = [ aggregation_id_field, aggregation_name_field, hazard_id_field, hazard_class_field ] check_inputs(target_compulsory_fields, target_fields) source_compulsory_fields = [ exposure_id_field, exposure_class_field, aggregation_id_field, aggregation_name_field, hazard_id_field, hazard_class_field ] check_inputs(source_compulsory_fields, source_fields) aggregation_id = target_fields[aggregation_id_field['key']] hazard_id = target_fields[hazard_id_field['key']] hazard_class = target_fields[hazard_class_field['key']] exposure_class = source_fields[exposure_class_field['key']] exposure_class_index = impact.fields().lookupField(exposure_class) unique_exposure = list(impact.uniqueValues(exposure_class_index)) fields = ['aggregation_id', 'hazard_id'] absolute_values = create_absolute_values_structure(impact, fields) # We need to know what kind of exposure we are going to count. # the size, or the number of features or population. field_index = report_on_field(impact) aggregate_hazard.startEditing() shift = aggregate_hazard.fields().count() dynamic_structure = [ [exposure_count_field, unique_exposure], ] add_fields( aggregate_hazard, absolute_values, [affected_field, total_field], dynamic_structure, ) flat_table = FlatTable('aggregation_id', 'hazard_id', 'exposure_class') request = QgsFeatureRequest() request.setFlags(QgsFeatureRequest.NoGeometry) LOGGER.debug('Computing the aggregate hazard summary.') for feature in impact.getFeatures(request): # Field_index can be equal to 0. if field_index is not None: value = feature[field_index] else: value = 1 aggregation_value = feature[aggregation_id] hazard_value = feature[hazard_id] if (hazard_value is None or hazard_value == '' or (hasattr(hazard_value, 'isNull') and hazard_value.isNull())): hazard_value = not_exposed_class['key'] exposure_value = feature[exposure_class] if (exposure_value is None or exposure_value == '' or (hasattr(exposure_value, 'isNull') and exposure_value.isNull())): exposure_value = 'NULL' flat_table.add_value(value, aggregation_id=aggregation_value, hazard_id=hazard_value, exposure_class=exposure_value) # We summarize every absolute values. for field, field_definition in list(absolute_values.items()): value = feature[field] if (value == '' or value is None or (hasattr(value, 'isNull') and value.isNull())): value = 0 field_definition[0].add_value(value, aggregation_id=aggregation_value, hazard_id=hazard_value) hazard_keywords = aggregate_hazard.keywords['hazard_keywords'] hazard = hazard_keywords['hazard'] classification = hazard_keywords['classification'] exposure_keywords = impact.keywords['exposure_keywords'] exposure = exposure_keywords['exposure'] for area in aggregate_hazard.getFeatures(request): aggregation_value = area[aggregation_id] feature_hazard_id = area[hazard_id] if (feature_hazard_id == '' or feature_hazard_id is None or (hasattr(feature_hazard_id, 'isNull') and feature_hazard_id.isNull())): feature_hazard_id = not_exposed_class['key'] feature_hazard_value = area[hazard_class] total = 0 for i, val in enumerate(unique_exposure): sum = flat_table.get_value(aggregation_id=aggregation_value, hazard_id=feature_hazard_id, exposure_class=val) total += sum aggregate_hazard.changeAttributeValue(area.id(), shift + i, sum) affected = post_processor_affected_function( exposure=exposure, hazard=hazard, classification=classification, hazard_class=feature_hazard_value) affected = tr(str(affected)) aggregate_hazard.changeAttributeValue(area.id(), shift + len(unique_exposure), affected) aggregate_hazard.changeAttributeValue(area.id(), shift + len(unique_exposure) + 1, total) for i, field in enumerate(absolute_values.values()): value = field[0].get_value(aggregation_id=aggregation_value, hazard_id=feature_hazard_id) aggregate_hazard.changeAttributeValue( area.id(), shift + len(unique_exposure) + 2 + i, value) aggregate_hazard.commitChanges() aggregate_hazard.keywords['title'] = ( layer_purpose_aggregate_hazard_impacted['name']) if qgis_version() >= 21800: aggregate_hazard.setName(aggregate_hazard.keywords['title']) else: aggregate_hazard.setLayerName(aggregate_hazard.keywords['title']) aggregate_hazard.keywords['layer_purpose'] = ( layer_purpose_aggregate_hazard_impacted['key']) aggregate_hazard.keywords['exposure_keywords'] = impact.keywords.copy() check_layer(aggregate_hazard) return aggregate_hazard
LOGGER.debug("Parse argument") shell_arguments = docopt(usage) LOGGER.debug("Parse done") except DocoptExit as exc: print exc.message try: arguments = CommandLineArguments(shell_arguments) LOGGER.debug(shell_arguments) if arguments.show_list is True: # setup functions register_impact_functions() show_impact_function_names(get_impact_function_list(arguments)) elif arguments.version is True: print "QGIS VERSION: " + str(qgis_version()).replace("0", ".") # user is only interested in doing a download elif arguments.download is True and arguments.exposure is None and arguments.hazard is None: print "downloading ..." download_exposure(arguments) elif (arguments.hazard is not None) and (arguments.output_file is not None): # first do download if necessary if arguments.exposure is None and arguments.download is True: download_exposure(arguments) if arguments.exposure is not None: run_impact_function(arguments) else: print "Download unsuccessful" elif arguments.report_template is not None and arguments.output_file is not None:
def get_layer_description_from_browser(self, category): """Obtain the description of the browser layer selected by user. :param category: The category of the layer to get the description. :type category: string :returns: Tuple of boolean and string. Boolean is true if layer is validated as compatible for current role (impact function and category) and false otherwise. String contains a description of the selected layer or an error message. :rtype: tuple """ if category == 'hazard': browser = self.tvBrowserHazard elif category == 'exposure': browser = self.tvBrowserExposure elif category == 'aggregation': browser = self.tvBrowserAggregation else: raise InaSAFEError index = browser.selectionModel().currentIndex() if not index: return False, '' # Map the proxy model index to the source model index index = browser.model().mapToSource(index) item = browser.model().sourceModel().dataItem(index) if not item: return False, '' item_class_name = item.metaObject().className() # if not itemClassName.endswith('LayerItem'): if not item.type() == QgsDataItem.Layer: if item_class_name == 'QgsPGRootItem' and not item.children(): return False, create_postGIS_connection_first else: return False, '' if item_class_name not in [ 'QgsOgrLayerItem', 'QgsGdalLayerItem', 'QgsPGLayerItem', 'QgsLayerItem', ]: return False, '' path = item.path() if item_class_name in ['QgsOgrLayerItem', 'QgsGdalLayerItem', 'QgsLayerItem'] and not os.path.exists(path): return False, '' # try to create the layer if item_class_name == 'QgsOgrLayerItem': layer = QgsVectorLayer(path, '', 'ogr') elif item_class_name == 'QgsPGLayerItem': uri = self.postgis_path_to_uri(path) if uri: layer = QgsVectorLayer(uri.uri(), uri.table(), 'postgres') else: layer = None else: layer = QgsRasterLayer(path, '', 'gdal') if not layer or not layer.isValid(): return False, self.tr('Not a valid layer.') try: keywords = self.keyword_io.read_keywords(layer) if 'layer_purpose' not in keywords: keywords = None except (HashNotFoundError, OperationalError, NoKeywordsFoundError, KeywordNotFoundError, InvalidParameterError, UnsupportedProviderError, MissingMetadata): keywords = None # set the layer name for further use in the step_fc_summary if keywords: if qgis_version() >= 21800: layer.setName(keywords.get('title')) else: layer.setLayerName(keywords.get('title')) if not self.parent.is_layer_compatible(layer, category, keywords): label_text = '%s<br/>%s' % ( self.tr( 'This layer\'s keywords or type are not suitable:'), self.unsuitable_layer_description_html( layer, category, keywords)) return False, label_text # set the current layer (e.g. for the keyword creation sub-thread # or for adding the layer to mapCanvas) self.parent.layer = layer if category == 'hazard': self.parent.hazard_layer = layer elif category == 'exposure': self.parent.exposure_layer = layer else: self.parent.aggregation_layer = layer # Check if the layer is keywordless if keywords and 'keyword_version' in keywords: kw_ver = str(keywords['keyword_version']) self.parent.is_selected_layer_keywordless = ( not is_keyword_version_supported(kw_ver)) else: self.parent.is_selected_layer_keywordless = True desc = layer_description_html(layer, keywords) return True, desc
def accept(self): """Launch the multi exposure analysis.""" if not isinstance(self._multi_exposure_if, MultiExposureImpactFunction): # This should not happen as the "accept" button must be disabled if # the impact function is not ready. return ANALYSIS_FAILED_BAD_CODE, None self.tab_widget.setCurrentIndex(2) self.set_enabled_buttons(False) enable_busy_cursor() try: code, message, exposure = self._multi_exposure_if.run() message = basestring_to_message(message) if code == ANALYSIS_FAILED_BAD_INPUT: LOGGER.warning( tr('The impact function could not run because of the inputs.' )) send_error_message(self, message) LOGGER.warning(message.to_text()) disable_busy_cursor() self.set_enabled_buttons(True) return code, message elif code == ANALYSIS_FAILED_BAD_CODE: LOGGER.warning( tr('The impact function could not run because of a bug.')) LOGGER.exception(message.to_text()) send_error_message(self, message) disable_busy_cursor() self.set_enabled_buttons(True) return code, message if setting('generate_report', True, bool): LOGGER.info( 'Reports are going to be generated for the multiexposure.') # Report for the multi exposure report = [standard_multi_exposure_impact_report_metadata_html] error_code, message = ( self._multi_exposure_if.generate_report(report)) message = basestring_to_message(message) if error_code == ImpactReport.REPORT_GENERATION_FAILED: LOGGER.warning('The impact report could not be generated.') send_error_message(self, message) LOGGER.exception(message.to_text()) disable_busy_cursor() self.set_enabled_buttons(True) return error_code, message else: LOGGER.warning( 'Reports are not generated because of your settings.') display_warning_message_bar( tr('Reports'), tr('Reports are not going to be generated because of your ' 'InaSAFE settings.'), duration=10, iface_object=self.iface) # We always create the multi exposure group because we need # reports to be generated. root = QgsProject.instance().layerTreeRoot() if len(self.ordered_expected_layers()) == 0: group_analysis = root.insertGroup(0, self._multi_exposure_if.name) group_analysis.setItemVisibilityChecked(True) group_analysis.setCustomProperty(MULTI_EXPOSURE_ANALYSIS_FLAG, True) for layer in self._multi_exposure_if.outputs: QgsProject.instance().addMapLayer(layer, False) layer_node = group_analysis.addLayer(layer) layer_node.setItemVisibilityChecked(False) # set layer title if any try: title = layer.keywords['title'] if qgis_version() >= 21800: layer.setName(title) else: layer.setLayerName(title) except KeyError: pass for analysis in self._multi_exposure_if.impact_functions: detailed_group = group_analysis.insertGroup( 0, analysis.name) detailed_group.setItemVisibilityChecked(True) add_impact_layers_to_canvas(analysis, group=detailed_group) if self.iface: self.iface.setActiveLayer( self._multi_exposure_if.analysis_impacted) else: add_layers_to_canvas_with_custom_orders( self.ordered_expected_layers(), self._multi_exposure_if, self.iface) if setting('generate_report', True, bool): LOGGER.info( 'Reports are going to be generated for each single ' 'exposure.') # Report for the single exposure with hazard for analysis in self._multi_exposure_if.impact_functions: # we only want to generate non pdf/qpt report html_components = [standard_impact_report_metadata_html] error_code, message = ( analysis.generate_report(html_components)) message = basestring_to_message(message) if error_code == (ImpactReport.REPORT_GENERATION_FAILED): LOGGER.info( 'The impact report could not be generated.') send_error_message(self, message) LOGGER.info(message.to_text()) disable_busy_cursor() self.set_enabled_buttons(True) return error_code, message else: LOGGER.info( 'Reports are not generated because of your settings.') display_warning_message_bar( tr('Reports'), tr('Reports are not going to be generated because of your ' 'InaSAFE settings.'), duration=10, iface_object=self.iface) # If zoom to impact is enabled if setting('setZoomToImpactFlag', expected_type=bool): self.iface.zoomToActiveLayer() # If hide exposure layers if setting('setHideExposureFlag', expected_type=bool): treeroot = QgsProject.instance().layerTreeRoot() for combo in list(self.combos_exposures.values()): layer = layer_from_combo(combo) if layer is not None: treelayer = treeroot.findLayer(layer.id()) if treelayer: treelayer.setItemVisibilityChecked(False) # Set last analysis extent self._extent.set_last_analysis_extent( self._multi_exposure_if.analysis_extent, self._multi_exposure_if.crs) self.done(QDialog.Accepted) except Exception as e: error_message = get_error_message(e) send_error_message(self, error_message) LOGGER.exception(e) LOGGER.debug(error_message.to_text()) finally: disable_busy_cursor() self.set_enabled_buttons(True)
def aggregate_hazard_summary(impact, aggregate_hazard, callback=None): """Compute the summary from the source layer to the aggregate_hazard layer. Source layer : |exp_id|exp_class|haz_id|haz_class|aggr_id|aggr_name|affected|extra*| Target layer : | aggr_id | aggr_name | haz_id | haz_class | extra* | Output layer : |aggr_id| aggr_name|haz_id|haz_class|affected|extra*|count ber exposure*| :param impact: The layer to aggregate vector layer. :type impact: QgsVectorLayer :param aggregate_hazard: The aggregate_hazard vector layer where to write statistics. :type aggregate_hazard: QgsVectorLayer :param callback: A function to all to indicate progress. The function should accept params 'current' (int), 'maximum' (int) and 'step' (str). Defaults to None. :type callback: function :return: The new aggregate_hazard layer with summary. :rtype: QgsVectorLayer .. versionadded:: 4.0 """ output_layer_name = summary_1_aggregate_hazard_steps['output_layer_name'] processing_step = summary_1_aggregate_hazard_steps['step_name'] source_fields = impact.keywords['inasafe_fields'] target_fields = aggregate_hazard.keywords['inasafe_fields'] target_compulsory_fields = [ aggregation_id_field, aggregation_name_field, hazard_id_field, hazard_class_field ] check_inputs(target_compulsory_fields, target_fields) source_compulsory_fields = [ exposure_id_field, exposure_class_field, aggregation_id_field, aggregation_name_field, hazard_id_field, hazard_class_field ] check_inputs(source_compulsory_fields, source_fields) aggregation_id = target_fields[aggregation_id_field['key']] hazard_id = target_fields[hazard_id_field['key']] hazard_class = target_fields[hazard_class_field['key']] exposure_class = source_fields[exposure_class_field['key']] exposure_class_index = impact.fieldNameIndex(exposure_class) unique_exposure = impact.uniqueValues(exposure_class_index) fields = ['aggregation_id', 'hazard_id'] absolute_values = create_absolute_values_structure(impact, fields) # We need to know what kind of exposure we are going to count. # the size, or the number of features or population. field_index = report_on_field(impact) aggregate_hazard.startEditing() shift = aggregate_hazard.fields().count() add_fields(aggregate_hazard, absolute_values, [affected_field, total_field], unique_exposure, exposure_count_field) flat_table = FlatTable('aggregation_id', 'hazard_id', 'exposure_class') request = QgsFeatureRequest() request.setFlags(QgsFeatureRequest.NoGeometry) LOGGER.debug('Computing the aggregate hazard summary.') for feature in impact.getFeatures(request): # Field_index can be equal to 0. if field_index is not None: value = feature[field_index] else: value = 1 aggregation_value = feature[aggregation_id] hazard_value = feature[hazard_id] if not hazard_value or isinstance(hazard_value, QPyNullVariant): hazard_value = not_exposed_class['key'] exposure_value = feature[exposure_class] if not exposure_value or isinstance(exposure_value, QPyNullVariant): exposure_value = 'NULL' flat_table.add_value(value, aggregation_id=aggregation_value, hazard_id=hazard_value, exposure_class=exposure_value) # We summarize every absolute values. for field, field_definition in absolute_values.iteritems(): value = feature[field] if not value or isinstance(value, QPyNullVariant): value = 0 field_definition[0].add_value(value, aggregation_id=aggregation_value, hazard_id=hazard_value) hazard_keywords = aggregate_hazard.keywords['hazard_keywords'] classification = hazard_keywords['classification'] for area in aggregate_hazard.getFeatures(request): aggregation_value = area[aggregation_id] feature_hazard_id = area[hazard_id] if not feature_hazard_id or isinstance(feature_hazard_id, QPyNullVariant): feature_hazard_id = not_exposed_class['key'] feature_hazard_value = area[hazard_class] total = 0 for i, val in enumerate(unique_exposure): sum = flat_table.get_value(aggregation_id=aggregation_value, hazard_id=feature_hazard_id, exposure_class=val) total += sum aggregate_hazard.changeAttributeValue(area.id(), shift + i, sum) affected = post_processor_affected_function( classification=classification, hazard_class=feature_hazard_value) affected = tr(unicode(affected)) aggregate_hazard.changeAttributeValue(area.id(), shift + len(unique_exposure), affected) aggregate_hazard.changeAttributeValue(area.id(), shift + len(unique_exposure) + 1, total) for i, field in enumerate(absolute_values.itervalues()): value = field[0].get_value(aggregation_id=aggregation_value, hazard_id=feature_hazard_id) aggregate_hazard.changeAttributeValue( area.id(), shift + len(unique_exposure) + 2 + i, value) aggregate_hazard.commitChanges() aggregate_hazard.keywords['title'] = ( layer_purpose_aggregate_hazard_impacted['name']) if qgis_version() >= 21800: aggregate_hazard.setName(aggregate_hazard.keywords['title']) else: aggregate_hazard.setLayerName(aggregate_hazard.keywords['title']) aggregate_hazard.keywords['layer_purpose'] = ( layer_purpose_aggregate_hazard_impacted['key']) check_layer(aggregate_hazard) return aggregate_hazard
def add_layers_to_canvas_with_custom_orders( order, impact_function, iface=None): """Helper to add layers to the map canvas following a specific order. From top to bottom in the legend: [ ('FromCanvas', layer name, full layer URI, QML), ('FromAnalysis', layer purpose, layer group, None), ... ] The full layer URI is coming from our helper. :param order: Special structure the list of layers to add. :type order: list :param impact_function: The multi exposure impact function used. :type impact_function: MultiExposureImpactFunction :param iface: QGIS QgisAppInterface instance. :type iface: QgisAppInterface """ root = QgsProject.instance().layerTreeRoot() # Make all layers hidden. for child in root.children(): child.setItemVisibilityChecked(False) group_analysis = root.insertGroup(0, impact_function.name) group_analysis.setItemVisibilityChecked(True) group_analysis.setCustomProperty(MULTI_EXPOSURE_ANALYSIS_FLAG, True) # Insert layers in the good order in the group. for layer_definition in order: if layer_definition[0] == FROM_CANVAS['key']: style = QDomDocument() style.setContent(layer_definition[3]) layer = load_layer(layer_definition[2], layer_definition[1])[0] layer.importNamedStyle(style) QgsProject.instance().addMapLayer(layer, False) layer_node = group_analysis.addLayer(layer) layer_node.setItemVisibilityChecked(True) else: if layer_definition[2] == impact_function.name: for layer in impact_function.outputs: if layer.keywords['layer_purpose'] == layer_definition[1]: QgsProject.instance().addMapLayer( layer, False) layer_node = group_analysis.addLayer(layer) layer_node.setItemVisibilityChecked(True) try: title = layer.keywords['title'] if qgis_version() >= 21800: layer.setName(title) else: layer.setLayerName(title) except KeyError: pass break else: for sub_impact_function in impact_function.impact_functions: # Iterate over each sub impact function used in the # multi exposure analysis. if sub_impact_function.name == layer_definition[2]: for layer in sub_impact_function.outputs: purpose = layer_definition[1] if layer.keywords['layer_purpose'] == purpose: QgsProject.instance().addMapLayer( layer, False) layer_node = group_analysis.addLayer( layer) layer_node.setItemVisibilityChecked(True) try: title = layer.keywords['title'] if qgis_version() >= 21800: layer.setName(title) else: layer.setLayerName(title) except KeyError: pass break if iface: iface.setActiveLayer(impact_function.analysis_impacted)
def aggregation_summary(aggregate_hazard, aggregation, callback=None): """Compute the summary from the aggregate hazard to the analysis layer. Source layer : | haz_id | haz_class | aggr_id | aggr_name | total_feature | Target layer : | aggr_id | aggr_name | Output layer : | aggr_id | aggr_name | count of affected features per exposure type :param aggregate_hazard: The layer to aggregate vector layer. :type aggregate_hazard: QgsVectorLayer :param aggregation: The aggregation vector layer where to write statistics. :type aggregation: QgsVectorLayer :param callback: A function to all to indicate progress. The function should accept params 'current' (int), 'maximum' (int) and 'step' (str). Defaults to None. :type callback: function :return: The new aggregation layer with summary. :rtype: QgsVectorLayer .. versionadded:: 4.0 """ output_layer_name = summary_2_aggregation_steps['output_layer_name'] processing_step = summary_2_aggregation_steps['step_name'] source_fields = aggregate_hazard.keywords['inasafe_fields'] target_fields = aggregation.keywords['inasafe_fields'] target_compulsory_fields = [ aggregation_id_field, aggregation_name_field, ] check_inputs(target_compulsory_fields, target_fields) # Missing exposure_count_field source_compulsory_fields = [ aggregation_id_field, aggregation_name_field, hazard_id_field, hazard_class_field, affected_field, ] check_inputs(source_compulsory_fields, source_fields) pattern = exposure_count_field['key'] pattern = pattern.replace('%s', '') unique_exposure = read_dynamic_inasafe_field( source_fields, exposure_count_field) absolute_values = create_absolute_values_structure( aggregate_hazard, ['aggregation_id']) flat_table = FlatTable('aggregation_id', 'exposure_class') aggregation_index = source_fields[aggregation_id_field['key']] # We want to loop over affected features only. request = QgsFeatureRequest() request.setFlags(QgsFeatureRequest.NoGeometry) expression = '\"%s\" = \'%s\'' % ( affected_field['field_name'], tr('True')) request.setFilterExpression(expression) for area in aggregate_hazard.getFeatures(request): for key, name_field in source_fields.iteritems(): if key.endswith(pattern): aggregation_id = area[aggregation_index] exposure_class = key.replace(pattern, '') value = area[name_field] flat_table.add_value( value, aggregation_id=aggregation_id, exposure_class=exposure_class ) # We summarize every absolute values. for field, field_definition in absolute_values.iteritems(): value = area[field] if not value or isinstance(value, QPyNullVariant): value = 0 field_definition[0].add_value( value, aggregation_id=area[aggregation_index], ) shift = aggregation.fields().count() aggregation.startEditing() add_fields( aggregation, absolute_values, [total_affected_field], unique_exposure, affected_exposure_count_field) aggregation_index = target_fields[aggregation_id_field['key']] request = QgsFeatureRequest() request.setFlags(QgsFeatureRequest.NoGeometry) for area in aggregation.getFeatures(request): aggregation_value = area[aggregation_index] total = 0 for i, val in enumerate(unique_exposure): sum = flat_table.get_value( aggregation_id=aggregation_value, exposure_class=val ) total += sum aggregation.changeAttributeValue(area.id(), shift + i, sum) aggregation.changeAttributeValue( area.id(), shift + len(unique_exposure), total) for i, field in enumerate(absolute_values.itervalues()): value = field[0].get_value( aggregation_id=aggregation_value, ) target_index = shift + len(unique_exposure) + 1 + i aggregation.changeAttributeValue( area.id(), target_index, value) aggregation.commitChanges() aggregation.keywords['title'] = layer_purpose_aggregation_summary['name'] if qgis_version() >= 21800: aggregation.setName(aggregation.keywords['title']) else: aggregation.setLayerName(aggregation.keywords['title']) aggregation.keywords['layer_purpose'] = ( layer_purpose_aggregation_summary['key']) check_layer(aggregation) return aggregation
class TestEarthquakeReport(unittest.TestCase): """Test Earthquake Report. .. versionadded:: 4.0 """ maxDiff = None @classmethod def fixtures_dir(cls, path): """Helper to return fixture path.""" directory_name = os.path.dirname(__file__) return os.path.join(directory_name, 'fixtures', path) def assert_compare_file_control(self, control_path, actual_path): """Helper to compare file.""" current_directory = resources_path() context = {'current_directory': current_directory} with open(control_path) as control_file: template_string = control_file.read() template = Template(template_string) control_string = template.render(context).strip() with io.open(actual_path, encoding='utf-8') as actual_file: actual_string = actual_file.read().strip() self.assertEqual(control_string, actual_string) @unittest.skipIf( qgis_version() < 31000, 'Skip this in QGIS 3.4 due to stall issue from core qgis thread.') def test_earthquake_population_without_aggregation(self): """Testing Earthquake in Population without aggregation. .. versionadded:: 4.0 """ output_folder = self.fixtures_dir('../output/earthquake_population') # Classified vector with building-points shutil.rmtree(output_folder, ignore_errors=True) hazard_layer = load_test_raster_layer('hazard', 'earthquake.tif') exposure_layer = load_test_raster_layer('exposure', 'pop_binary_raster_20_20.asc') impact_function = ImpactFunction() impact_function.exposure = exposure_layer impact_function.hazard = hazard_layer impact_function.crs = QgsCoordinateReferenceSystem(4326) impact_function.prepare() return_code, message = impact_function.run() self.assertEqual(return_code, ANALYSIS_SUCCESS, message) report_metadata = ReportMetadata( metadata_dict=standard_impact_report_metadata_html) impact_report = ImpactReport(IFACE, report_metadata, impact_function=impact_function) impact_report.output_folder = output_folder return_code, message = impact_report.process_components() self.assertEqual(return_code, ImpactReport.REPORT_GENERATION_SUCCESS, message) """Checking generated context.""" empty_component_output_message = 'Empty component output' # Check Analysis Summary analysis_summary = impact_report.metadata.component_by_key( general_report_component['key']) """:type: safe.report.report_metadata.Jinja2ComponentsMetadata""" expected_context = { 'component_key': 'general-report', 'table_header': ('Estimated Number of people affected per MMI intensity'), 'header': 'General Report', 'summary': [{ 'header_label': 'Hazard Zone', 'rows': [{ 'numbers': ['0'], 'name': 'X', 'key': 'X' }, { 'numbers': ['0'], 'name': 'IX', 'key': 'IX' }, { 'numbers': ['200'], 'name': 'VIII', 'key': 'VIII' }, { 'numbers': ['0'], 'name': 'VII', 'key': 'VII' }, { 'numbers': ['0'], 'name': 'VI', 'key': 'VI' }, { 'numbers': ['0'], 'name': 'V', 'key': 'V' }, { 'numbers': ['0'], 'name': 'IV', 'key': 'IV' }, { 'numbers': ['0'], 'name': 'III', 'key': 'III' }, { 'numbers': ['0'], 'name': 'II', 'key': 'II' }, { 'numbers': ['0'], 'name': 'I', 'key': 'I' }, { 'as_header': True, 'key': 'total_exposed_field', 'name': 'Total Exposed', 'numbers': ['200'] }], 'value_labels': ['Count'] }, { 'header_label': 'Population', 'rows': [{ 'numbers': ['200'], 'name': 'Affected', 'key': 'total_affected_field', }, { 'key': 'total_not_affected_field', 'name': 'Not Affected', 'numbers': ['0'] }, { 'key': 'total_not_exposed_field', 'name': 'Not Exposed', 'numbers': ['0'] }, { 'numbers': ['200'], 'name': 'Displaced', 'key': 'displaced_field' }, { 'numbers': ['0 - 100'], 'name': 'Fatalities', 'key': 'fatalities_field' }], 'value_labels': ['Count'] }], 'notes': [ 'Exposed People: People who are present in hazard zones and ' 'are thereby subject to potential losses. In InaSAFE, people ' 'who are exposed are those people who are within the extent ' 'of the hazard.', 'Affected People: People who are affected by a hazardous ' 'event. People can be affected directly or indirectly. ' 'Affected people may experience short-term or long-term ' 'consequences to their lives, livelihoods or health and in ' 'the economic, physical, social, cultural and environmental ' 'assets. In InaSAFE, people who are killed during the event ' 'are also considered affected.', 'Displaced People: Displaced people are people who, for ' 'different reasons and circumstances because of risk or ' 'disaster, have to leave their place of residence. ' 'In InaSAFE, demographic and minimum needs reports are based ' 'on displaced / evacuated people.' ] } actual_context = analysis_summary.context self.assertDictEqual(expected_context, actual_context) self.assertTrue(analysis_summary.output, empty_component_output_message) report_metadata = ReportMetadata(metadata_dict=infographic_report) infographic_impact_report = ImpactReport( IFACE, report_metadata, impact_function=impact_function) infographic_impact_report.output_folder = output_folder return_code, message = infographic_impact_report.process_components() self.assertEqual(return_code, ImpactReport.REPORT_GENERATION_SUCCESS, message) # check population pie chart if we have 100% donut slice population_chart_svg = ( infographic_impact_report.metadata.component_by_key( population_chart_svg_component['key'])) expected_slices = [{ 'value': 200, 'show_label': True, 'center': (224.0, 128.0), 'stroke_opacity': 1, 'path': 'M128.000000,0.000000a128.000000,128.000000 0 0 1 ' '0.000000,256.000000l-0.000000,-64.000000a64.000000,' '64.000000 0 0 0 0.000000,-128.000000Z', 'percentage': 100, 'label': 'VIII', 'stroke': '#ff7000', 'label_position': (256, 0), 'fill': '#ff7000' }, { 'value': 100, 'show_label': False, 'center': (32.0, 128.0), 'stroke_opacity': 1, 'path': 'M128.000000,256.000000a128.000000,128.000000 0 0 1 ' '-0.000000,-256.000000l0.000000,64.000000a64.000000,' '64.000000 0 0 0 0.000000,128.000000Z', 'percentage': 50.0, 'label': '', 'stroke': '#ff7000', 'label_position': (256, 0), 'fill': '#ff7000' }, { 'value': 0, 'show_label': False, 'center': (128.0, 224.0), 'stroke_opacity': 1, 'path': 'M128.000000,256.000000a128.000000,128.000000 0 0 1 ' '0.000000,0.000000l-0.000000,-64.000000a64.000000,' '64.000000 0 0 0 0.000000,0.000000Z', 'percentage': 0.0, 'label': 'Total Not Affected', 'stroke': '#fff', 'label_position': (256, 0), 'fill': '#1a9641' }] actual_context = population_chart_svg.context['context'] actual_slices = actual_context.slices self.assertEqual(expected_slices, actual_slices) self.assertTrue(population_chart_svg.output, empty_component_output_message) shutil.rmtree(output_folder, ignore_errors=True)
print "inasafe" print "" try: # Parse arguments, use usage.txt as syntax definition. LOGGER.debug('Parse argument') shell_arguments = docopt(usage) LOGGER.debug('Parse done') except DocoptExit as exc: print exc.message try: arguments = CommandLineArguments(shell_arguments) LOGGER.debug(shell_arguments) if arguments.version is True: print "QGIS VERSION: " + str(qgis_version()).replace('0', '.') # user is only interested in doing a download elif arguments.download is True and\ arguments.exposure is None and\ arguments.hazard is None: print "downloading ..." download_exposure(arguments) elif (arguments.hazard is not None) and\ (arguments.output_file is not None): # first do download if necessary if arguments.exposure is None and arguments.download is True: download_exposure(arguments) if arguments.exposure is not None: run_impact_function(arguments)
def analysis_summary(aggregate_hazard, analysis): """Compute the summary from the aggregate hazard to analysis. Source layer : | haz_id | haz_class | aggr_id | aggr_name | total_feature | Target layer : | analysis_name | Output layer : | analysis_name | count_hazard_class | affected_count | total | :param aggregate_hazard: The layer to aggregate vector layer. :type aggregate_hazard: QgsVectorLayer :param analysis: The target vector layer where to write statistics. :type analysis: QgsVectorLayer :return: The new target layer with summary. :rtype: QgsVectorLayer .. versionadded:: 4.0 """ source_fields = aggregate_hazard.keywords['inasafe_fields'] target_fields = analysis.keywords['inasafe_fields'] target_compulsory_fields = [ analysis_name_field, ] check_inputs(target_compulsory_fields, target_fields) source_compulsory_fields = [ aggregation_id_field, aggregation_name_field, hazard_id_field, hazard_class_field, total_field ] check_inputs(source_compulsory_fields, source_fields) absolute_values = create_absolute_values_structure(aggregate_hazard, ['all']) hazard_class = source_fields[hazard_class_field['key']] hazard_class_index = aggregate_hazard.fields().lookupField(hazard_class) unique_hazard = list(aggregate_hazard.uniqueValues(hazard_class_index)) hazard_keywords = aggregate_hazard.keywords['hazard_keywords'] hazard = hazard_keywords['hazard'] classification = hazard_keywords['classification'] exposure_keywords = aggregate_hazard.keywords['exposure_keywords'] exposure = exposure_keywords['exposure'] total = source_fields[total_field['key']] flat_table = FlatTable('hazard_class') # First loop over the aggregate_hazard layer request = QgsFeatureRequest() request.setSubsetOfAttributes([hazard_class, total], aggregate_hazard.fields()) request.setFlags(QgsFeatureRequest.NoGeometry) for area in aggregate_hazard.getFeatures(): hazard_value = area[hazard_class_index] value = area[total] if (value == '' or value is None or isnan(value) or (hasattr(value, 'isNull') and value.isNull())): # For isnan, see ticket #3812 value = 0 if (hazard_value == '' or hazard_value is None or (hasattr(hazard_value, 'isNull') and hazard_value.isNull())): hazard_value = 'NULL' flat_table.add_value(value, hazard_class=hazard_value) # We summarize every absolute values. for field, field_definition in list(absolute_values.items()): value = area[field] if (value == '' or value is None or (hasattr(value, 'isNull') and value.isNull())): value = 0 field_definition[0].add_value(value, all='all') analysis.startEditing() shift = analysis.fields().count() counts = [ total_affected_field, total_not_affected_field, total_exposed_field, total_not_exposed_field, total_field ] dynamic_structure = [ [hazard_count_field, unique_hazard], ] add_fields(analysis, absolute_values, counts, dynamic_structure) affected_sum = 0 not_affected_sum = 0 not_exposed_sum = 0 # Summarization summary_values = {} for key, summary_rule in list(summary_rules.items()): input_field = summary_rule['input_field'] case_field = summary_rule['case_field'] if aggregate_hazard.fields().lookupField(input_field['field_name']) \ == -1: continue if aggregate_hazard.fields().lookupField(case_field['field_name']) \ == -1: continue summary_value = 0 for area in aggregate_hazard.getFeatures(): case_value = area[case_field['field_name']] if case_value in summary_rule['case_values']: summary_value += area[input_field['field_name']] summary_values[key] = summary_value for area in analysis.getFeatures(request): total = 0 for i, val in enumerate(unique_hazard): if (val == '' or val is None or (hasattr(val, 'isNull') and val.isNull())): val = 'NULL' sum = flat_table.get_value(hazard_class=val) total += sum analysis.changeAttributeValue(area.id(), shift + i, sum) affected = post_processor_affected_function( exposure=exposure, hazard=hazard, classification=classification, hazard_class=val) if affected == not_exposed_class['key']: not_exposed_sum += sum elif affected: affected_sum += sum else: not_affected_sum += sum # Total Affected field analysis.changeAttributeValue(area.id(), shift + len(unique_hazard), affected_sum) # Total Not affected field analysis.changeAttributeValue(area.id(), shift + len(unique_hazard) + 1, not_affected_sum) # Total Exposed field analysis.changeAttributeValue(area.id(), shift + len(unique_hazard) + 2, total - not_exposed_sum) # Total Not exposed field analysis.changeAttributeValue(area.id(), shift + len(unique_hazard) + 3, not_exposed_sum) # Total field analysis.changeAttributeValue(area.id(), shift + len(unique_hazard) + 4, total) # Any absolute postprocessors for i, field in enumerate(absolute_values.values()): value = field[0].get_value(all='all') analysis.changeAttributeValue(area.id(), shift + len(unique_hazard) + 5 + i, value) # Summarizer of custom attributes for key, summary_value in list(summary_values.items()): summary_field = summary_rules[key]['summary_field'] field = create_field_from_definition(summary_field) analysis.addAttribute(field) field_index = analysis.fields().lookupField(field.name()) # noinspection PyTypeChecker analysis.keywords['inasafe_fields'][summary_field['key']] = ( summary_field['field_name']) analysis.changeAttributeValue(area.id(), field_index, summary_value) # Sanity check ± 1 to the result. Disabled for now as it seems ± 1 is not # enough. ET 13/02/17 # total_computed = ( # affected_sum + not_affected_sum + not_exposed_sum) # if not -1 < (total_computed - total) < 1: # raise ComputationError analysis.commitChanges() analysis.keywords['title'] = layer_purpose_analysis_impacted['name'] if qgis_version() >= 21600: analysis.setName(analysis.keywords['title']) else: analysis.setLayerName(analysis.keywords['title']) analysis.keywords['layer_purpose'] = layer_purpose_analysis_impacted['key'] check_layer(analysis) return analysis
def download(self): """Downloading the file. :returns: True if success, otherwise returns a tuple with format like this (QNetworkReply.NetworkError, error_message) :raises: IOError - when cannot create output_path """ # Prepare output path self.output_file = QFile(self.output_path) if not self.output_file.open(QFile.WriteOnly): raise IOError(self.output_file.errorString()) # Prepare downloaded buffer self.downloaded_file_buffer = QByteArray() # Request the url request = QNetworkRequest(self.url) self.reply = self.manager.get(request) self.reply.readyRead.connect(self.get_buffer) self.reply.finished.connect(self.write_data) self.manager.requestTimedOut.connect(self.request_timeout) if self.progress_dialog: # progress bar def progress_event(received, total): """Update progress. :param received: Data received so far. :type received: int :param total: Total expected data. :type total: int """ # noinspection PyArgumentList QCoreApplication.processEvents() self.progress_dialog.adjustSize() human_received = humanize_file_size(received) human_total = humanize_file_size(total) label_text = tr( "%s : %s of %s" % (self.prefix_text, human_received, human_total)) self.progress_dialog.setLabelText(label_text) self.progress_dialog.setMaximum(total) self.progress_dialog.setValue(received) # cancel def cancel_action(): """Cancel download.""" self.manager.deleteReply(self.reply) self.reply.downloadProgress.connect(progress_event) self.progress_dialog.canceled.connect(cancel_action) # Wait until finished # On Windows 32bit AND QGIS 2.2, self.reply.isFinished() always # returns False even after finished slot is called. So, that's why we # are adding self.finished_flag (see #864) while not self.reply.isFinished() and not self.finished_flag: # noinspection PyArgumentList QCoreApplication.processEvents() result = self.reply.error() if qgis_version() >= 21100: self.manager.deleteReply(self.reply) if result == QNetworkReply.NoError: return True, None elif result == QNetworkReply.UnknownNetworkError: return False, tr( 'The network is unreachable. Please check your internet ' 'connection.') elif result == QNetworkReply.ProtocolUnknownError or \ result == QNetworkReply.HostNotFoundError: LOGGER.exception('Host not found : %s' % self.url.encodedHost()) return False, tr( 'Sorry, the server is unreachable. Please try again later.') elif result == QNetworkReply.ContentNotFoundError: LOGGER.exception('Path not found : %s' % self.url.path()) return False, tr('Sorry, the layer was not found on the server.') else: return result, self.reply.errorString()