def show_keywords_editor(self): """Show the keywords editor.""" # import here only so that it is AFTER i18n set up from safe.gui.tools.keywords_dialog import KeywordsDialog # Next block is a fix for #776 if self.iface.activeLayer() is None: return try: keyword_io = KeywordIO() keyword_io.read_keywords(self.iface.activeLayer()) except UnsupportedProviderError: # noinspection PyUnresolvedReferences,PyCallByClass # noinspection PyTypeChecker,PyArgumentList QMessageBox.warning( None, self.tr("Unsupported layer type"), self.tr( "The layer you have selected cannot be used for " "analysis because its data type is unsupported." ), ) return # End of fix for #776 # Fix for #793 except NoKeywordsFoundError: # we will create them from scratch in the dialog pass # End of fix for #793 # Fix for filtered-layer except InvalidParameterError, e: # noinspection PyTypeChecker,PyTypeChecker,PyArgumentList QMessageBox.warning(None, self.tr("Invalid Layer"), e.message) return
def resolve_dict_keywords(keywords): """Replace dictionary content with html. :param keywords: The keywords. :type keywords: dict :return: New keywords with updated content. :rtype: dict """ for keyword in ['value_map', 'inasafe_fields', 'inasafe_default_values']: value = keywords.get(keyword) if value: value = value.get('content') value = KeywordIO._dict_to_row(value).to_html() keywords[keyword]['content'] = value value_maps = keywords.get('value_maps') thresholds = keywords.get('thresholds') if value_maps: value_maps = value_maps.get('content') value_maps = KeywordIO._value_maps_row(value_maps).to_html() keywords['value_maps']['content'] = value_maps if thresholds: thresholds = thresholds.get('content') thresholds = KeywordIO._threshold_to_row(thresholds).to_html() keywords['thresholds']['content'] = thresholds return keywords
def save_hazard_layer(self, hazard_path): # download or copy hazard path/url # It is a single tif file if not hazard_path and not os.path.exists(self.hazard_path): raise IOError("Hazard file not specified") if hazard_path: temp_hazard = download_file(hazard_path) shutil.copy(temp_hazard, self.hazard_path) # copy qml and metadata shutil.copy(self.ash_fixtures_dir("hazard.qml"), self.working_dir_path("hazard.qml")) keyword_io = KeywordIO() keywords = { "hazard_category": u"single_event", "keyword_version": u"3.5", "title": u"Ash Fall", "hazard": u"volcanic_ash", "continuous_hazard_unit": u"centimetres", "layer_geometry": u"raster", "layer_purpose": u"hazard", "layer_mode": u"continuous", } hazard_layer = read_qgis_layer(self.hazard_path, "Ash Fall") keyword_io.write_keywords(hazard_layer, keywords)
def test_issue1191(self): """Test setting a layer's title in the kw directly from qgis api""" settings = QtCore.QSettings() settings.setValue( 'inasafe/analysis_extents_mode', 'HazardExposure') self.dock.set_layer_from_title_flag = True set_canvas_crs(GEOCRS, True) set_yogya_extent(self.dock) result, message = setup_scenario( self.dock, hazard='Earthquake', exposure='Buildings', function='Be affected', function_id='EarthquakeBuildingFunction') self.assertTrue(result, message) layer = self.dock.get_hazard_layer() keyword_io = KeywordIO() original_title = 'Earthquake' title = keyword_io.read_keywords(layer, 'title') self.assertEqual(title, original_title) # change layer name as if done in the legend expected_title = 'TEST' layer.setLayerName(expected_title) title = keyword_io.read_keywords(layer, 'title') self.assertEqual(title, expected_title) # reset KW file to original state layer.setLayerName(original_title) title = keyword_io.read_keywords(layer, 'title') self.assertEqual(title, original_title) self.dock.set_layer_from_title_flag = False
def test_issue1191(self): """Test setting a layer's title in the kw directly from qgis api""" DOCK.set_layer_from_title_flag = True set_canvas_crs(GEOCRS, True) set_yogya_extent(DOCK) result, message = setup_scenario( DOCK, hazard='An earthquake in Yogyakarta like in 2006', exposure='OSM Building Polygons', function='Be affected', function_id='Earthquake Building Impact Function') self.assertTrue(result, message) layer = DOCK.get_hazard_layer() keyword_io = KeywordIO() original_title = 'An earthquake in Yogyakarta like in 2006' title = keyword_io.read_keywords(layer, 'title') self.assertEqual(title, original_title) # change layer name as if done in the legend expected_title = 'TEST' layer.setLayerName(expected_title) title = keyword_io.read_keywords(layer, 'title') self.assertEqual(title, expected_title) # reset KW file to original state layer.setLayerName(original_title) title = keyword_io.read_keywords(layer, 'title') self.assertEqual(title, original_title) DOCK.set_layer_from_title_flag = False
def show_current_metadata(self): """Show metadata of the current selected layer.""" LOGGER.debug('Showing layer: ' + self.layer.name()) keywords = KeywordIO(self.layer) content_html = keywords.to_message().to_html() full_html = html_header() + content_html + html_footer() self.metadata_preview_web_view.setHtml(full_html)
def test_layer_to_message(self): """Test to show augmented keywords if KeywordsIO ctor passed a layer. .. versionadded:: 3.3 """ keywords = KeywordIO(self.vector_layer) message = keywords.to_message().to_text() self.assertIn('*Reference system*, ', message)
def create_keyword_file(self, algorithm): """Create keyword file for the raster file created. Basically copy a template from keyword file in converter data and add extra keyword (usually a title) :param algorithm: Which re-sampling algorithm to use. valid options are 'nearest' (for nearest neighbour), 'invdist' (for inverse distance), 'average' (for moving average). Defaults to 'nearest' if not specified. Note that passing re-sampling alg parameters is currently not supported. If None is passed it will be replaced with 'nearest'. :type algorithm: str """ keyword_io = KeywordIO() classes = {} for item in earthquake_mmi_scale['classes']: classes[item['key']] = [ item['numeric_default_min'], item['numeric_default_max']] keywords = { 'hazard': hazard_earthquake['key'], 'hazard_category': hazard_category_single_event['key'], 'keyword_version': inasafe_keyword_version, 'layer_geometry': layer_geometry_raster['key'], 'layer_mode': layer_mode_continuous['key'], 'layer_purpose': layer_purpose_hazard['key'], 'continuous_hazard_unit': unit_mmi['key'], 'classification': earthquake_mmi_scale['key'], 'thresholds': classes } if self.algorithm_name: layer_path = os.path.join( self.output_dir, '%s-%s.tif' % ( self.output_basename, algorithm)) else: layer_path = os.path.join( self.output_dir, '%s.tif' % self.output_basename) # append title and source to the keywords file if len(self.title.strip()) == 0: keyword_title = self.output_basename else: keyword_title = self.title keywords['title'] = keyword_title hazard_layer = QgsRasterLayer(layer_path, keyword_title) if not hazard_layer.isValid(): raise InvalidLayerError() keyword_io.write_keywords(hazard_layer, keywords)
def read_keywords_iso_metadata(metadata_url, keyword=None): """Read xml metadata of a layer""" filename = download_file(metadata_url) # add xml extension new_filename = filename + '.xml' shutil.move(filename, new_filename) keyword_io = KeywordIO() keywords = keyword_io.read_keywords_file(new_filename) if keyword: return keywords.get(keyword, None) return keywords
def analysis_execution(): from safe.test.utilities import get_qgis_app # get_qgis_app must be called before importing Analysis QGIS_APP, CANVAS, IFACE, PARENT = get_qgis_app() from safe.utilities.analysis import Analysis from safe.utilities.keyword_io import KeywordIO analysis = Analysis() arg = AnalysisArguments.read_arguments() register_impact_functions() registry = ImpactFunctionManager().registry function = registry.get_instance(arg.impact_function_name) hazard_layer = safe_to_qgis_layer(read_layer(arg.hazard_filename)) exposure_layer = safe_to_qgis_layer(read_layer(arg.exposure_filename)) if arg.aggregation_filename: aggregation_layer = safe_to_qgis_layer(read_layer( arg.aggregation_filename)) keywords_io = KeywordIO() try: analysis.map_canvas = IFACE.mapCanvas() analysis.hazard_layer = hazard_layer analysis.hazard_keyword = keywords_io.read_keywords(hazard_layer) analysis.exposure_layer = exposure_layer analysis.exposure_keyword = keywords_io.read_keywords(exposure_layer) if aggregation_layer: analysis.aggregation_layer = aggregation_layer analysis.aggregation_keyword = keywords_io.read_keywords( aggregation_layer) analysis.impact_function = function analysis.setup_analysis() print 'Setup analysis done' analysis.run_analysis() print 'Analysis done' except Exception as e: print e.message impact = analysis.impact_layer qgis_impact = safe_to_qgis_layer(impact) generate_styles(impact, qgis_impact) copy_impact_layer(impact, arg.impact_filename)
def layer_changed(self, layer): """Enable or disable keywords editor icon when active layer changes. :param layer: The layer that is now active. :type layer: QgsMapLayer """ if not layer: enable_keyword_wizard = False elif not hasattr(layer, 'providerType'): enable_keyword_wizard = False elif layer.providerType() == 'wms': enable_keyword_wizard = False elif is_raster_layer(layer) and layer.bandCount() > 1: enable_keyword_wizard = False else: enable_keyword_wizard = True try: if layer: if is_raster_layer(layer): enable_field_mapping_tool = False else: keywords = KeywordIO().read_keywords(layer) layer_purpose = keywords.get('layer_purpose') if not layer_purpose: enable_field_mapping_tool = False if layer_purpose == layer_purpose_exposure['key']: layer_subcategory = keywords.get('exposure') elif layer_purpose == layer_purpose_hazard['key']: layer_subcategory = keywords.get('hazard') else: layer_subcategory = None field_groups = get_field_groups( layer_purpose, layer_subcategory) if len(field_groups) == 0: # No field group, disable field mapping tool. enable_field_mapping_tool = False else: enable_field_mapping_tool = True else: enable_field_mapping_tool = False except (KeywordNotFoundError, NoKeywordsFoundError, MetadataReadError): # No keywords, disable field mapping tool. enable_field_mapping_tool = False self.action_keywords_wizard.setEnabled(enable_keyword_wizard) self.action_field_mapping.setEnabled(enable_field_mapping_tool)
def __init__(self, parent): """Constructor for the class. :param parent: Parent widget i.e. the wizard dialog. :type parent: QWidget """ QtCore.QObject.__init__(self) self.parent = parent # Do not delete this self.iface = parent.iface self.keyword_io = KeywordIO() self.impact_function_manager = ImpactFunctionManager() self.extent = Extent(self.iface) self.analysis = None # Values for settings these get set in read_settings. self.run_in_thread_flag = None self.zoom_to_impact_flag = None self.hide_exposure_flag = None self.clip_hard = None self.show_intermediate_layers = None self.show_rubber_bands = False self.last_analysis_rubberband = None # This is a rubber band to show what the AOI of the # next analysis will be. Also added in 2.1.0 self.next_analysis_rubberband = None self.read_settings()
def monkey_patch_keywords(layer): """In InaSAFE V4, we do monkey patching for keywords. :param layer: The layer to monkey patch keywords. :type layer: QgsMapLayer """ keyword_io = KeywordIO() try: layer.keywords = keyword_io.read_keywords(layer) except (NoKeywordsFoundError, MetadataReadError): layer.keywords = {} if not layer.keywords.get('inasafe_fields'): layer.keywords['inasafe_fields'] = {} if not layer.keywords.get('layer_purpose'): layer.keywords['layer_purpose'] = 'undefined'
def monkey_patch_keywords(layer): """In InaSAFE V4, we do monkey patching for keywords. :param layer: The layer to monkey patch keywords. :type layer: QgsMapLayer """ keyword_io = KeywordIO() try: layer.keywords = keyword_io.read_keywords(layer) except (NoKeywordsFoundError, MetadataReadError): layer.keywords = {} try: layer.keywords['inasafe_fields'] except KeyError: layer.keywords['inasafe_fields'] = {}
def hazard_extra_keyword(keyword, feature, parent): """Given a keyword, it will return the value of the keyword from the hazard layer's extra keywords. For instance: * hazard_extra_keyword( 'depth' ) -> will return the value of 'depth' in current hazard layer's extra keywords. """ _ = feature, parent # NOQA hazard_layer_path = QgsExpressionContextUtils. \ projectScope(QgsProject.instance()).variable( 'hazard_layer') hazard_layer = load_layer(hazard_layer_path)[0] keywords = KeywordIO.read_keywords(hazard_layer) extra_keywords = keywords.get('extra_keywords') if extra_keywords: value = extra_keywords.get(keyword) if value: value_definition = definition(value) if value_definition: return value_definition['name'] return value else: return tr('Keyword %s is not found' % keyword) return tr('No extra keywords found')
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 __init__( self, iface, template_metadata, impact_function=None, hazard=None, exposure=None, impact=None, analysis=None, exposure_summary_table=None, aggregation_summary=None, extra_layers=None, minimum_needs_profile=None): """Constructor for the Composition Report class. :param iface: Reference to the QGIS iface object. :type iface: QgsAppInterface :param template_metadata: InaSAFE template metadata. :type template_metadata: ReportMetadata :param impact_function: Impact function instance for the report :type impact_function: safe.impact_function.impact_function.ImpactFunction .. versionadded:: 4.0 """ LOGGER.debug('InaSAFE Impact Report class initialised') self._iface = iface self._metadata = template_metadata self._output_folder = None self._impact_function = impact_function self._hazard = hazard or self._impact_function.hazard self._exposure = ( exposure or self._impact_function.exposure) self._impact = ( impact or self._impact_function.impact) self._analysis = (analysis or self._impact_function.analysis_impacted) self._exposure_summary_table = ( exposure_summary_table or self._impact_function.exposure_summary_table) self._aggregation_summary = ( aggregation_summary or self._impact_function.aggregation_summary) if extra_layers is None: extra_layers = [] self._extra_layers = extra_layers self._minimum_needs = minimum_needs_profile self._extent = self._iface.mapCanvas().extent() self._inasafe_context = InaSAFEReportContext() # QgsMapSettings is added in 2.4 map_settings = self._iface.mapCanvas().mapSettings() self._qgis_composition_context = QGISCompositionContext( None, map_settings, ImpactReport.DEFAULT_PAGE_DPI) self._keyword_io = KeywordIO()
def layer(self, layer): """Setter for layer property. :param layer: The actual layer. :type layer: QgsMapLayer, Layer """ if isinstance(layer, QgsMapLayer) or isinstance(layer, Layer): self._layer = layer else: message = tr("SafeLayer only accept QgsMapLayer or " "safe.storage.layer.Layer.") raise InvalidLayerError(message) if isinstance(layer, Layer): self.keywords = layer.keywords elif isinstance(layer, QgsMapLayer): keyword_io = KeywordIO() self.keywords = keyword_io.read_keywords(layer) else: self.keywords = {}
def get_impact_function_list(arguments): """Returns all available impact function ids. .. versionadded:: 3.2 :returns: List of impact functions. :rtype: list """ LOGGER.debug("get IF list") manager = ImpactFunctionManager() if arguments.hazard and arguments.exposure: hazard = get_hazard(arguments) exposure = get_exposure(arguments) keyword_io = KeywordIO() hazard_keyword = keyword_io.read_keywords(hazard) exposure_keyword = keyword_io.read_keywords(exposure) ifs = manager.filter_by_keywords(hazard_keyword, exposure_keyword) else: ifs = manager.filter() LOGGER.debug(ifs) return ifs
def exposure_summary_layer(): """Helper method for retrieving exposure summary layer. If the analysis is multi-exposure, then it will return the exposure summary layer from place exposure analysis. """ project_context_scope = QgsExpressionContextUtils.projectScope( QgsProject.instance()) project = QgsProject.instance() key = provenance_layer_analysis_impacted_id['provenance_key'] analysis_summary_layer = project.mapLayer( project_context_scope.variable(key)) if not analysis_summary_layer: key = provenance_layer_analysis_impacted['provenance_key'] if project_context_scope.hasVariable(key): analysis_summary_layer = load_layer( project_context_scope.variable(key))[0] if not analysis_summary_layer: return None keywords = KeywordIO.read_keywords(analysis_summary_layer) extra_keywords = keywords.get(property_extra_keywords['key'], {}) is_multi_exposure = extra_keywords.get(extra_keyword_analysis_type['key']) key = provenance_layer_exposure_summary_id['provenance_key'] if is_multi_exposure: key = ('{provenance}__{exposure}').format( provenance=provenance_multi_exposure_summary_layers_id[ 'provenance_key'], exposure=exposure_place['key']) if not project_context_scope.hasVariable(key): return None exposure_summary_layer = project.mapLayer( project_context_scope.variable(key)) if not exposure_summary_layer: key = provenance_layer_exposure_summary['provenance_key'] if is_multi_exposure: key = ('{provenance}__{exposure}').format( provenance=provenance_multi_exposure_summary_layers[ 'provenance_key'], exposure=exposure_place['key']) if project_context_scope.hasVariable(key): exposure_summary_layer = load_layer( project_context_scope.variable(key))[0] else: return None return exposure_summary_layer
def __init__(self, iface, dock): """Constructor for the class.""" QDialog.__init__(self) # Class Member self.iface = iface self.dock = dock self.output_directory = None self.exposure_layer = None self.hazard_layer = None self.aggregation_layer = None self.keyword_io = KeywordIO() # Calling some init methods self.restore_state()
def __init__(self, parent=None, iface=None): """Constructor.""" super(FieldMappingWidget, self).__init__(parent) # Attributes self.parent = parent self.iface = iface self.layer = None self.metadata = {} self.tabs = [] # Store all tabs self.keyword_io = KeywordIO()
def read_keywords_iso_metadata(metadata_url, keyword=None): """Read xml metadata of a layer :param keyword: Can be string or tuple containing keywords to search for :type keyword: str, (str, ) :return: the keywords, or a dictionary with key-value pair """ filename = download_file(metadata_url) # add xml extension new_filename = filename + '.xml' shutil.move(filename, new_filename) keyword_io = KeywordIO() keywords = keyword_io.read_keywords_file(new_filename) if keyword: if isinstance(keyword, tuple) or isinstance(keyword, list): ret_val = {} for key in keyword: ret_val[key] = keywords.get(key, None) return ret_val else: return keywords.get(keyword, None) return keywords
def test_clip_vector_with_unicode(self): """Test clipping vector layer with unicode attribute in feature. This issue is described at Github #2262 and #2233 TODO: FIXME: This is a hacky fix. See above ticket for further explanation. To fix this, we should be able to specify UTF-8 encoding for QgsVectorFileWriter """ # this layer contains unicode values in the layer_path = standard_data_path( 'boundaries', 'district_osm_jakarta.shp') vector_layer = QgsVectorLayer(layer_path, 'District Jakarta', 'ogr') keyword_io = KeywordIO() aggregation_keyword = get_defaults()['AGGR_ATTR_KEY'] aggregation_attribute = keyword_io.read_keywords( vector_layer, keyword=aggregation_keyword) source_extent = vector_layer.extent() extent = [source_extent.xMinimum(), source_extent.yMinimum(), source_extent.xMaximum(), source_extent.yMaximum()] clipped_layer = clip_layer( layer=vector_layer, extent=extent, explode_flag=True, explode_attribute=aggregation_attribute) # cross check vector layer attribute in clipped layer vector_values = [] for f in vector_layer.getFeatures(): vector_values.append(f.attributes()) clipped_values = [] for f in clipped_layer.getFeatures(): clipped_values.append(f.attributes()) for val in clipped_values: self.assertIn(val, vector_values)
def setUp(self): self.keyword_io = KeywordIO() # SQLite Layer uri = QgsDataSourceURI() sqlite_building_path = standard_data_path( 'exposure', 'exposure.sqlite') uri.setDatabase(sqlite_building_path) uri.setDataSource('', 'buildings_osm_4326', 'Geometry') self.sqlite_layer = QgsVectorLayer( uri.uri(), 'OSM Buildings', 'spatialite') self.expected_sqlite_keywords = { 'datatype': 'OSM' } # Raster Layer keywords hazard_path = standard_data_path('hazard', 'tsunami_wgs84.tif') self.raster_layer, _ = load_layer(hazard_path) self.expected_raster_keywords = { 'hazard_category': 'single_event', 'title': 'Generic Continuous Flood', 'hazard': 'flood', 'continuous_hazard_unit': 'generic', 'layer_geometry': 'raster', 'layer_purpose': 'hazard', 'layer_mode': 'continuous', 'keyword_version': '3.5' } # Vector Layer keywords vector_path = standard_data_path('exposure', 'buildings_osm_4326.shp') self.vector_layer, _ = load_layer(vector_path) self.expected_vector_keywords = { 'keyword_version': '3.5', 'structure_class_field': 'FLOODED', 'value_mapping': {}, 'title': 'buildings_osm_4326', 'layer_geometry': 'polygon', 'layer_purpose': 'exposure', 'layer_mode': 'classified', 'exposure': 'structure', } # Keyword less layer keywordless_path = standard_data_path('other', 'keywordless_layer.shp') self.keywordless_layer, _ = load_layer(keywordless_path) # Keyword file self.keyword_path = standard_data_path( 'exposure', 'buildings_osm_4326.xml')
def __init__(self, iface, dock=None, parent=None): """Constructor for the dialog. :param iface: A Quantum GIS QGisAppInterface instance. :type iface: QGisAppInterface :param parent: Parent widget of this dialog :type parent: QWidget :param dock: Optional dock widget instance that we can notify of changes to the keywords. :type dock: Dock """ QtGui.QDialog.__init__(self, parent) self.setupUi(self) self.setWindowTitle(self.tr('InaSAFE %s Options' % get_version())) # Save reference to the QGIS interface and parent self.iface = iface self.parent = parent self.dock = dock self.keyword_io = KeywordIO() self.defaults = get_defaults() # Set up things for context help self.help_button = self.button_box.button(QtGui.QDialogButtonBox.Help) # Allow toggling the help button self.help_button.setCheckable(True) self.help_button.toggled.connect(self.help_toggled) self.main_stacked_widget.setCurrentIndex(1) self.grpNotImplemented.hide() self.adjustSize() self.restore_state() # hack prevent showing use thread visible and set it false see #557 self.cbxUseThread.setChecked(True) self.cbxUseThread.setVisible(False) # Set up listener for various UI self.custom_org_logo_checkbox.toggled.connect( self.set_organisation_logo) self.custom_north_arrow_checkbox.toggled.connect(self.set_north_arrow) self.custom_UseUserDirectory_checkbox.toggled.connect( self.set_user_dir) self.custom_templates_dir_checkbox.toggled.connect( self.set_templates_dir) self.custom_org_disclaimer_checkbox.toggled.connect( self.set_org_disclaimer)
class QgisWrapper(): """Wrapper class to add keywords functionality to Qgis layers """ def __init__(self, layer, name=None): """Create the wrapper :param layer: Qgis layer :type layer: QgsMapLayer :param name: A layer's name :type name: Basestring or None """ self.data = layer self.keyword_io = KeywordIO() self.keywords = self.keyword_io.read_keywords(layer) if name is None: try: self.name = self.get_keywords(key='title') except KeywordNotFoundError: pass def get_name(self): return self.name def set_name(self, name): self.name = name def get_keywords(self, key=None): """Return a copy of the keywords dictionary Args: * key (optional): If specified value will be returned for key only """ if key is None: return self.keywords.copy() else: if key in self.keywords: return self.keywords[key] else: msg = ('Keyword %s does not exist in %s: Options are ' '%s' % (key, self.get_name(), self.keywords.keys())) raise KeywordNotFoundError(msg) def get_layer(self): return self.data
def __init__(self, layer, name=None): """Create the wrapper :param layer: Qgis layer :type layer: QgsMapLayer :param name: A layer's name :type name: Basestring or None """ self.data = layer self.keyword_io = KeywordIO() self.keywords = self.keyword_io.read_keywords(layer) if name is None: try: self.name = self.get_keywords(key='title') except KeywordNotFoundError: pass
def __init__(self, aggregator): """Director for aggregation based operations. :param aggregator: Aggregator that will be used in conjunction with postprocessors. :type aggregator: Aggregator """ super(PostprocessorManager, self).__init__() # Aggregation / post processing related items self.output = {} self.keyword_io = KeywordIO() self.error_message = None self.aggregator = aggregator self.current_output_postprocessor = None self.attribute_title = None self.function_parameters = None
def setUp(self): self.keyword_io = KeywordIO() # SQLite Layer uri = QgsDataSourceURI() sqlite_building_path = test_data_path('exposure', 'exposure.sqlite') uri.setDatabase(sqlite_building_path) uri.setDataSource('', 'buildings_osm_4326', 'Geometry') self.sqlite_layer = QgsVectorLayer( uri.uri(), 'OSM Buildings', 'spatialite') self.expected_sqlite_keywords = { 'category': 'exposure', 'datatype': 'OSM', 'subcategory': 'building'} # Raster Layer keywords hazard_path = test_data_path('hazard', 'tsunami_wgs84.tif') self.raster_layer, _ = load_layer(hazard_path) self.expected_raster_keywords = { 'category': 'hazard', 'subcategory': 'tsunami', 'data_type': 'continuous', 'unit': 'metres_depth', 'title': 'Tsunami'} # Vector Layer keywords vector_path = test_data_path('exposure', 'buildings_osm_4326.shp') self.vector_layer, _ = load_layer(vector_path) self.expected_vector_keywords = { 'category': 'exposure', 'datatype': 'osm', 'subcategory': 'structure', 'title': 'buildings_osm_4326', 'purpose': 'dki'} # Keyword less layer keywordless_path = test_data_path('other', 'keywordless_layer.shp') self.keywordless_layer, _ = load_layer(keywordless_path)
class PostprocessorManager(QtCore.QObject): """A manager for post processing of impact function results. """ def __init__(self, aggregator): """Director for aggregation based operations. :param aggregator: Aggregator that will be used in conjunction with postprocessors. :type aggregator: Aggregator """ super(PostprocessorManager, self).__init__() # Aggregation / post processing related items self.output = {} self.keyword_io = KeywordIO() self.error_message = None self.aggregator = aggregator self.current_output_postprocessor = None self.attribute_title = None self.function_parameters = None def _sum_field_name(self): return self.aggregator.sum_field_name() def _sort_no_data(self, data): """Check if the value field of the postprocessor is NO_DATA. This is used for sorting, it returns -1 if the value is NO_DATA, so that no data items can be put at the end of a list :param data: Value to be checked. :type data: list :returns: -1 if the value is NO_DATA else the value :rtype: int, float """ post_processor = self.output[self.current_output_postprocessor] # get the key position of the value field key = post_processor[0][1].keys()[0] # get the value # data[1] is the orderedDict # data[1][myFirstKey] is the 1st indicator in the orderedDict if (data[1][key]['value'] == self.aggregator.get_default_keyword( 'NO_DATA')): position = -1 else: position = data[1][key]['value'] position = unhumanize_number(position) return position def _generate_tables(self, aoi_mode=True): """Parses the postprocessing output as one table per postprocessor. TODO: This should rather return json and then have a helper method to make html from the JSON. :param aoi_mode: adds a Total in aggregation areas row to the calculated table :type aoi_mode: bool :returns: The html. :rtype: str """ message = m.Message() for processor, results_list in self.output.iteritems(): self.current_output_postprocessor = processor # results_list is for example: # [ # (PyQt4.QtCore.QString(u'Entire area'), OrderedDict([ # (u'Total', {'value': 977536, 'metadata': {}}), # (u'Female population', {'value': 508319, 'metadata': {}}), # (u'Weekly hygiene packs', {'value': 403453, 'metadata': { # 'description': 'Females hygiene packs for weekly use'}}) # ])) # ] # sorting using the first indicator of a postprocessor sorted_results = sorted(results_list, key=self._sort_no_data, reverse=True) # init table has_no_data = False table = m.Table(style_class='table table-condensed table-striped') table.caption = self.tr('Detailed %s report') % (tr( get_postprocessor_human_name(processor)).lower()) # Dirty hack to make "evacuated" comes out in the report. # Currently only MinimumNeeds that calculate from evacuation # percentage. if processor == 'MinimumNeeds': if 'evacuation_percentage' in self.function_parameters.keys(): table.caption = self.tr( 'Detailed %s report (for people needing ' 'evacuation)') % (tr( get_postprocessor_human_name(processor)).lower()) else: table.caption = self.tr( 'Detailed %s report (affected people)') % (tr( get_postprocessor_human_name(processor)).lower()) if processor in ['Gender', 'Age']: table.caption = self.tr( 'Detailed %s report (affected people)') % (tr( get_postprocessor_human_name(processor)).lower()) header = m.Row() header.add(str(self.attribute_title).capitalize()) for calculation_name in sorted_results[0][1]: header.add(self.tr(calculation_name)) table.add(header) # used to calculate the totals row as per issue #690 postprocessor_totals = OrderedDict() for zone_name, calc in sorted_results: row = m.Row(zone_name) for indicator, calculation_data in calc.iteritems(): value = calculation_data['value'] value = str(unhumanize_number(value)) if value == self.aggregator.get_default_keyword('NO_DATA'): has_no_data = True value += ' *' try: postprocessor_totals[indicator] += 0 except KeyError: postprocessor_totals[indicator] = 0 else: value = int(value) try: postprocessor_totals[indicator] += value except KeyError: postprocessor_totals[indicator] = value row.add(format_int(value)) table.add(row) if not aoi_mode: # add the totals row row = m.Row(self.tr('Total in aggregation areas')) for _, total in postprocessor_totals.iteritems(): row.add(format_int(total)) table.add(row) # add table to message message.add(table) if has_no_data: message.add( m.EmphasizedText( self. tr('* "%s" values mean that there where some problems while ' 'calculating them. This did not affect the other ' 'values.') % (self.aggregator.get_default_keyword('NO_DATA')))) return message def _consolidate_multipart_stats(self): """Sums the values of multipart polygons together to display only one. """ LOGGER.debug('Consolidating multipart postprocessing results') # copy needed because of # self.output[postprocessor].pop(corrected_index) output = self.output # iterate postprocessors for postprocessor, results_list in output.iteritems(): # see self._generateTables to see details about results_list checked_polygon_names = {} parts_to_delete = [] polygon_index = 0 # iterate polygons for polygon_name, results in results_list: if polygon_name in checked_polygon_names.keys(): for result_name, result in results.iteritems(): first_part_index = checked_polygon_names[polygon_name] first_part = self.output[postprocessor][ first_part_index] first_part_results = first_part[1] first_part_result = first_part_results[result_name] # FIXME one of the parts was 'No data', # is it matematically correct to do no_data = 0? # see http://irclogs.geoapt.com/inasafe/ # %23inasafe.2013-08-09.log (at 22.29) no_data = \ self.aggregator.get_default_keyword('NO_DATA') # both are No data value = first_part_result['value'] result_value = result['value'] if value == no_data and result_value == no_data: new_result = no_data else: # one is No data if value == no_data: value = 0 # the other is No data elif result_value == no_data: result_value = 0 # here none is No data new_result = (unhumanize_number(value) + unhumanize_number(result_value)) first_part_result['value'] = format_int(new_result) parts_to_delete.append(polygon_index) else: # add polygon to checked list checked_polygon_names[polygon_name] = polygon_index polygon_index += 1 # http://stackoverflow.com/questions/497426/ # deleting-multiple-elements-from-a-list results_list = [ res for j, res in enumerate(results_list) if j not in parts_to_delete ] self.output[postprocessor] = results_list def run(self): """Run any post processors requested by the impact function. """ try: requested_postprocessors = self.function_parameters[ 'postprocessors'] postprocessors = get_postprocessors(requested_postprocessors, self.aggregator.aoi_mode) except (TypeError, KeyError): # TypeError is for when function_parameters is none # KeyError is for when ['postprocessors'] is unavailable postprocessors = {} LOGGER.debug('Running this postprocessors: ' + str(postprocessors)) feature_names_attribute = self.aggregator.attributes[ self.aggregator.get_default_keyword('AGGR_ATTR_KEY')] if feature_names_attribute is None: self.attribute_title = self.tr('Aggregation unit') else: self.attribute_title = feature_names_attribute name_filed_index = self.aggregator.layer.fieldNameIndex( self.attribute_title) sum_field_index = self.aggregator.layer.fieldNameIndex( self._sum_field_name()) user_defined_female_ratio = False female_ratio_field_index = None female_ratio = None user_defined_age_ratios = False youth_ratio_field_index = None youth_ratio = None adult_ratio_field_index = None adult_ratio = None elderly_ratio_field_index = None elderly_ratio = None if 'Gender' in postprocessors: # look if we need to look for a variable female ratio in a layer try: female_ratio_field = self.aggregator.attributes[ self.aggregator.get_default_keyword( 'FEMALE_RATIO_ATTR_KEY')] female_ratio_field_index = \ self.aggregator.layer.fieldNameIndex(female_ratio_field) # something went wrong finding the female ratio field, # use defaults from below except block if female_ratio_field_index == -1: raise KeyError user_defined_female_ratio = True except KeyError: try: female_ratio = self.keyword_io.read_keywords( self.aggregator.layer, self.aggregator.get_default_keyword( 'FEMALE_RATIO_KEY')) except KeywordNotFoundError: female_ratio = \ self.aggregator.get_default_keyword('FEMALE_RATIO') if 'Age' in postprocessors: # look if we need to look for a variable age ratio in a layer try: youth_ratio_field = self.aggregator.attributes[ self.aggregator.get_default_keyword( 'YOUTH_RATIO_ATTR_KEY')] youth_ratio_field_index = \ self.aggregator.layer.fieldNameIndex(youth_ratio_field) adult_ratio_field = self.aggregator.attributes[ self.aggregator.get_default_keyword( 'ADULT_RATIO_ATTR_KEY')] adult_ratio_field_index = \ self.aggregator.layer.fieldNameIndex(adult_ratio_field) elderly_ratio_field = self.aggregator.attributes[ self.aggregator.get_default_keyword( 'ELDERLY_RATIO_ATTR_KEY')] elderly_ratio_field_index = \ self.aggregator.layer.fieldNameIndex(elderly_ratio_field) # something went wrong finding the youth ratio field, # use defaults from below except block if (youth_ratio_field_index == -1 or adult_ratio_field_index == -1 or elderly_ratio_field_index == -1): raise KeyError user_defined_age_ratios = True except KeyError: try: youth_ratio = self.keyword_io.read_keywords( self.aggregator.layer, self.aggregator.get_default_keyword('YOUTH_RATIO_KEY')) adult_ratio = self.keyword_io.read_keywords( self.aggregator.layer, self.aggregator.get_default_keyword('ADULT_RATIO_KEY')) elderly_ratio = self.keyword_io.read_keywords( self.aggregator.layer, self.aggregator.get_default_keyword( 'ELDERLY_RATIO_KEY')) except KeywordNotFoundError: youth_ratio = \ self.aggregator.get_default_keyword('YOUTH_RATIO') adult_ratio = \ self.aggregator.get_default_keyword('ADULT_RATIO') elderly_ratio = \ self.aggregator.get_default_keyword('ELDERLY_RATIO') if 'BuildingType' or 'RoadType' in postprocessors: try: key_attribute = self.keyword_io.read_keywords( self.aggregator.exposure_layer, 'key_attribute') except KeywordNotFoundError: # use 'type' as default key_attribute = 'type' # iterate zone features request = QgsFeatureRequest() request.setFlags(QgsFeatureRequest.NoGeometry) provider = self.aggregator.layer.dataProvider() # start data retrieval: fetch no geometry and all attributes for each # feature polygon_index = 0 for feature in provider.getFeatures(request): # if a feature has no field called if name_filed_index == -1: zone_name = str(feature.id()) else: zone_name = feature[name_filed_index] # create dictionary of attributes to pass to postprocessor general_params = { 'target_field': self.aggregator.target_field, 'function_params': self.function_parameters } if self.aggregator.statistics_type == 'class_count': general_params['impact_classes'] = ( self.aggregator.statistics_classes) elif self.aggregator.statistics_type == 'sum': impact_total = feature[sum_field_index] general_params['impact_total'] = impact_total try: general_params['impact_attrs'] = ( self.aggregator.impact_layer_attributes[polygon_index]) except IndexError: # rasters and attributeless vectors have no attributes general_params['impact_attrs'] = None for key, value in postprocessors.iteritems(): parameters = general_params try: # look if params are available for this postprocessor parameters.update( self.function_parameters['postprocessors'][key] ['params']) except KeyError: pass if key == 'Gender': if user_defined_female_ratio: female_ratio = feature[female_ratio_field_index] if female_ratio is None: female_ratio = self.aggregator.defaults[ 'FEMALE_RATIO'] LOGGER.warning('Data Driven Female ratio ' 'incomplete, using defaults for' ' aggregation unit' ' %s' % feature.id) parameters['female_ratio'] = female_ratio if key == 'Age': if user_defined_age_ratios: youth_ratio = feature[youth_ratio_field_index] adult_ratio = feature[adult_ratio_field_index] elderly_ratio = feature[elderly_ratio_field_index] if (youth_ratio is None or adult_ratio is None or elderly_ratio is None): youth_ratio = self.aggregator.defaults[ 'YOUTH_RATIO'] adult_ratio = self.aggregator.defaults[ 'ADULT_RATIO'] elderly_ratio = self.aggregator.defaults[ 'ELDERLY_RATIO'] LOGGER.warning('Data Driven Age ratios ' 'incomplete, using defaults for' ' aggregation unit' ' %s' % feature.id) parameters['youth_ratio'] = youth_ratio parameters['adult_ratio'] = adult_ratio parameters['elderly_ratio'] = elderly_ratio if key == 'BuildingType' or key == 'RoadType': # TODO: Fix this might be referenced before assignment parameters['key_attribute'] = key_attribute try: value.setup(parameters) value.process() results = value.results() value.clear() # LOGGER.debug(results) # this can raise a KeyError self.output[key].append((zone_name, results)) except PostProcessorError as e: message = m.Message( m.Heading(self.tr('%s postprocessor problem' % key), **styles.DETAILS_STYLE), m.Paragraph(self.tr(str(e)))) self.error_message = message except KeyError: self.output[key] = [] # TODO: Fix this might be referenced before assignment self.output[key].append((zone_name, results)) # increment the index polygon_index += 1 def get_output(self, aoi_mode): """Returns the results of the post processing as a table. :param aoi_mode: aoi mode of the aggregator. :type aoi_mode: bool :returns: str - a string containing the html in the requested format. """ message = m.Message() if self.error_message is not None: message.add( m.Heading(self.tr('Postprocessing report partially skipped'), **styles.WARNING_STYLE)) message.add( m.Paragraph( self. tr('Due to a problem while processing the results, part of ' 'the detailed postprocessing report is unavailable:'))) message.add(self.error_message) try: if (self.keyword_io.read_keywords(self.aggregator.layer, 'had multipart polygon')): self._consolidate_multipart_stats() except KeywordNotFoundError: pass message.add(self._generate_tables(aoi_mode)) return message
class AnalysisHandler(QObject): """Analysis handler for the dock and the wizard.""" analysisDone = pyqtSignal(bool) # noinspection PyUnresolvedReferences def __init__(self, parent): """Constructor for the class. :param parent: Parent widget i.e. the wizard dialog. :type parent: QWidget """ QtCore.QObject.__init__(self) self.parent = parent # Do not delete this self.iface = parent.iface self.keyword_io = KeywordIO() self.impact_function_manager = ImpactFunctionManager() self.extent = Extent(self.iface) self.analysis = None # Values for settings these get set in read_settings. self.run_in_thread_flag = None self.zoom_to_impact_flag = None self.hide_exposure_flag = None self.clip_hard = None self.show_intermediate_layers = None self.show_rubber_bands = False self.last_analysis_rubberband = None # This is a rubber band to show what the AOI of the # next analysis will be. Also added in 2.1.0 self.next_analysis_rubberband = None self.read_settings() def enable_signal_receiver(self): """Setup dispatcher for all available signal from Analysis. .. note:: Adapted from the dock """ dispatcher.connect(self.show_busy, signal=BUSY_SIGNAL) dispatcher.connect(self.hide_busy, signal=NOT_BUSY_SIGNAL) dispatcher.connect(self.completed, signal=ANALYSIS_DONE_SIGNAL) # noinspection PyArgumentEqualDefault dispatcher.connect(self.show_dynamic_message, signal=DYNAMIC_MESSAGE_SIGNAL) # noinspection PyArgumentEqualDefault dispatcher.connect(self.parent.wvResults.static_message_event, signal=STATIC_MESSAGE_SIGNAL, sender=dispatcher.Any) # noinspection PyArgumentEqualDefault dispatcher.connect(self.parent.wvResults.error_message_event, signal=ERROR_MESSAGE_SIGNAL, sender=dispatcher.Any) def disable_signal_receiver(self): """Remove dispatcher for all available signal from Analysis. .. note:: Adapted from the dock """ dispatcher.disconnect(self.show_busy, signal=BUSY_SIGNAL) dispatcher.disconnect(self.hide_busy, signal=NOT_BUSY_SIGNAL) dispatcher.disconnect(self.completed, signal=ANALYSIS_DONE_SIGNAL) dispatcher.disconnect(self.show_dynamic_message, signal=DYNAMIC_MESSAGE_SIGNAL) def show_static_message(self, message): """Send a static message to the message viewer. Static messages cause any previous content in the MessageViewer to be replaced with new content. .. note:: Copied from the dock :param message: An instance of our rich message class. :type message: Message """ dispatcher.send(signal=STATIC_MESSAGE_SIGNAL, sender=self, message=message) def show_dynamic_message(self, sender, message): """Send a dynamic message to the message viewer. Dynamic messages are appended to any existing content in the MessageViewer. .. note:: Modified from the dock :param sender: The object that sent the message. :type sender: Object, None :param message: An instance of our rich message class. :type message: Message """ # TODO Hardcoded step - may overflow, if number of messages increase self.parent.pbProgress.setValue(self.parent.pbProgress.value() + 15) self.parent.wvResults.dynamic_message_event(sender, message) def show_error_message(self, error_message): """Send an error message to the message viewer. Error messages cause any previous content in the MessageViewer to be replaced with new content. .. note:: Copied from the dock :param error_message: An instance of our rich error message class. :type error_message: ErrorMessage """ dispatcher.send(signal=ERROR_MESSAGE_SIGNAL, sender=self, message=error_message) self.hide_busy() def read_settings(self): """Restore settings from QSettings. Do this on init and after changing options in the options dialog. """ settings = QSettings() flag = bool(settings.value('inasafe/showRubberBands', False, type=bool)) self.extent.show_rubber_bands = flag try: extent = settings.value('inasafe/analysis_extent', '', type=str) crs = settings.value('inasafe/analysis_extent_crs', '', type=str) except TypeError: # Any bogus stuff in settings and we just clear them extent = '' crs = '' if extent != '' and crs != '': extent = extent_string_to_array(extent) try: self.extent.user_extent = QgsRectangle(*extent) self.extent.user_extent_crs = QgsCoordinateReferenceSystem(crs) self.extent.show_user_analysis_extent() except TypeError: self.extent.user_extent = None self.extent.user_extent_crs = None flag = settings.value('inasafe/useThreadingFlag', False, type=bool) self.run_in_thread_flag = flag flag = settings.value('inasafe/setZoomToImpactFlag', True, type=bool) self.zoom_to_impact_flag = flag # whether exposure layer should be hidden after model completes flag = settings.value('inasafe/setHideExposureFlag', False, type=bool) self.hide_exposure_flag = flag # whether to 'hard clip' layers (e.g. cut buildings in half if they # lie partially in the AOI self.clip_hard = settings.value('inasafe/clip_hard', False, type=bool) # whether to show or not postprocessing generated layers self.show_intermediate_layers = settings.value( 'inasafe/show_intermediate_layers', False, type=bool) # whether to show or not dev only options self.developer_mode = settings.value('inasafe/developer_mode', False, type=bool) # whether to show or not a custom Logo self.organisation_logo_path = settings.value( 'inasafe/organisation_logo_path', default_organisation_logo_path(), type=str) flag = bool( settings.value('inasafe/showOrganisationLogoInDockFlag', True, type=bool)) def show_busy(self): """Lock buttons and enable the busy cursor.""" self.parent.pbnNext.setEnabled(False) self.parent.pbnBack.setEnabled(False) self.parent.pbnCancel.setEnabled(False) QtGui.qApp.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor)) self.parent.repaint() QtGui.qApp.processEvents() def hide_busy(self): """Unlock buttons A helper function to indicate processing is done.""" self.parent.pbnNext.setEnabled(True) self.parent.pbnBack.setEnabled(True) self.parent.pbnCancel.setEnabled(True) self.parent.repaint() QtGui.qApp.restoreOverrideCursor() def analysis_error(self, exception, message): """A helper to spawn an error and halt processing. An exception will be logged, busy status removed and a message displayed. .. note:: Copied from the dock :param message: an ErrorMessage to display :type message: ErrorMessage, Message :param exception: An exception that was raised :type exception: Exception """ self.hide_busy() LOGGER.exception(message) message = get_error_message(exception, context=message) self.show_error_message(message) self.analysisDone.emit(False) def setup_and_run_analysis(self): """Setup and execute the analysis""" self.enable_signal_receiver() self.show_busy() self.init_analysis() try: self.analysis.setup_analysis() except InsufficientOverlapError as e: raise e self.extent.show_last_analysis_extent(self.analysis.clip_parameters[1]) # Start the analysis self.analysis.run_analysis() self.disable_signal_receiver() def init_analysis(self): """Setup analysis to make it ready to work. .. note:: Copied or adapted from the dock """ self.analysis = Analysis() # Layers self.analysis.hazard_layer = self.parent.hazard_layer self.analysis.exposure_layer = self.parent.exposure_layer self.analysis.aggregation_layer = self.parent.aggregation_layer # TODO test if the implement aggregation layer works! # noinspection PyTypeChecker self.analysis.hazard_keyword = self.keyword_io.read_keywords( self.parent.hazard_layer) self.analysis.exposure_keyword = self.keyword_io.read_keywords( self.parent.exposure_layer) # Need to check since aggregation layer is not mandatory if self.analysis.aggregation_layer: self.analysis.aggregation_keyword = self.keyword_io.read_keywords( self.parent.aggregation_layer) # Impact Function impact_function = self.impact_function_manager.get( self.parent.selected_function()['id']) impact_function.parameters = self.parent.if_params self.analysis.impact_function = impact_function # Variables self.analysis.clip_hard = self.clip_hard self.analysis.show_intermediate_layers = self.show_intermediate_layers self.analysis.run_in_thread_flag = self.run_in_thread_flag self.analysis.map_canvas = self.iface.mapCanvas() # Extent if self.parent.rbExtentUser.isChecked(): self.analysis.user_extent = self.extent.user_extent else: self.analysis.user_extent = None self.analysis.user_extent_crs = self.extent.user_extent_crs self.analysis.clip_to_viewport = self.parent.rbExtentScreen.isChecked() def completed(self): """Slot activated when the process is done. .. note:: Adapted from the dock """ # Try to run completion code try: from datetime import datetime LOGGER.debug(datetime.now()) LOGGER.debug('get engine impact layer') LOGGER.debug(self.analysis is None) engine_impact_layer = self.analysis.get_impact_layer() # Load impact layer into QGIS qgis_impact_layer = read_impact_layer(engine_impact_layer) report = self.show_results(qgis_impact_layer, engine_impact_layer) except Exception, e: # pylint: disable=W0703 # FIXME (Ole): This branch is not covered by the tests self.analysis_error(e, self.tr('Error loading impact layer.')) else:
class KeywordIOTest(unittest.TestCase): """Tests for reading and writing of raster and vector data """ def setUp(self): self.keyword_io = KeywordIO() # SQLite Layer uri = QgsDataSourceURI() sqlite_building_path = test_data_path('exposure', 'exposure.sqlite') uri.setDatabase(sqlite_building_path) uri.setDataSource('', 'buildings_osm_4326', 'Geometry') self.sqlite_layer = QgsVectorLayer(uri.uri(), 'OSM Buildings', 'spatialite') self.expected_sqlite_keywords = { 'category': 'exposure', 'datatype': 'OSM', 'subcategory': 'building' } # Raster Layer keywords hazard_path = test_data_path('hazard', 'padang_tsunami_mw8.tif') self.raster_layer, _ = load_layer(hazard_path) self.expected_raster_keywords = { 'category': 'hazard', 'subcategory': 'tsunami', 'unit': 'm', 'title': 'A tsunami in Padang (Mw 8.8)' } # Vector Layer keywords vector_path = test_data_path('exposure', 'buildings_osm_4326.shp') self.vector_layer, _ = load_layer(vector_path) self.expected_vector_keywords = { 'category': 'exposure', 'datatype': 'osm', 'subcategory': 'structure', 'title': 'buildings_osm_4326', 'purpose': 'dki' } # Keyword less layer keywordless_path = test_data_path('other', 'keywordless_layer.shp') self.keywordless_layer, _ = load_layer(keywordless_path) def tearDown(self): pass def test_get_hash_for_datasource(self): """Test we can reliably get a hash for a uri""" hash_value = self.keyword_io.hash_for_datasource(PG_URI) expected_hash = '7cc153e1b119ca54a91ddb98a56ea95e' message = "Got: %s\nExpected: %s" % (hash_value, expected_hash) assert hash_value == expected_hash, message def test_write_read_keyword_from_uri(self): """Test we can set and get keywords for a non local datasource""" handle, filename = tempfile.mkstemp('.db', 'keywords_', temp_dir()) # Ensure the file is deleted before we try to write to it # fixes windows specific issue where you get a message like this # ERROR 1: c:\temp\inasafe\clip_jpxjnt.shp is not a directory. # This is because mkstemp creates the file handle and leaves # the file open. os.close(handle) os.remove(filename) expected_keywords = { 'category': 'exposure', 'datatype': 'itb', 'subcategory': 'building' } # SQL insert test # On first write schema is empty and there is no matching hash self.keyword_io.set_keyword_db_path(filename) self.keyword_io.write_keywords_for_uri(PG_URI, expected_keywords) # SQL Update test # On second write schema is populated and we update matching hash expected_keywords = { 'category': 'exposure', 'datatype': 'OSM', # <--note the change here! 'subcategory': 'building' } self.keyword_io.write_keywords_for_uri(PG_URI, expected_keywords) # Test getting all keywords keywords = self.keyword_io.read_keyword_from_uri(PG_URI) message = 'Got: %s\n\nExpected %s\n\nDB: %s' % ( keywords, expected_keywords, filename) assert keywords == expected_keywords, message # Test getting just a single keyword keyword = self.keyword_io.read_keyword_from_uri(PG_URI, 'datatype') expected_keyword = 'OSM' message = 'Got: %s\n\nExpected %s\n\nDB: %s' % ( keyword, expected_keyword, filename) assert keyword == expected_keyword, message # Test deleting keywords actually does delete self.keyword_io.delete_keywords_for_uri(PG_URI) try: _ = self.keyword_io.read_keyword_from_uri(PG_URI, 'datatype') # if the above didnt cause an exception then bad message = 'Expected a HashNotFoundError to be raised' assert message except HashNotFoundError: # we expect this outcome so good! pass def test_are_keywords_file_based(self): """Can we correctly determine if keywords should be written to file or to database?""" assert not self.keyword_io.are_keywords_file_based(self.sqlite_layer) assert self.keyword_io.are_keywords_file_based(self.raster_layer) assert self.keyword_io.are_keywords_file_based(self.vector_layer) def test_read_raster_file_keywords(self): """Can we read raster file keywords using generic readKeywords method """ keywords = self.keyword_io.read_keywords(self.raster_layer) expected_keywords = self.expected_raster_keywords source = self.raster_layer.source() message = 'Got:\n%s\nExpected:\n%s\nSource:\n%s' % ( keywords, expected_keywords, source) self.assertEquals(keywords, expected_keywords, message) def test_read_vector_file_keywords(self): """Test read vector file keywords with the generic readKeywords method. """ keywords = self.keyword_io.read_keywords(self.vector_layer) expected_keywords = self.expected_vector_keywords source = self.vector_layer.source() message = 'Got: %s\n\nExpected %s\n\nSource: %s' % ( keywords, expected_keywords, source) assert keywords == expected_keywords, message def test_read_keywordless_layer(self): """Test read 'keyword' file from keywordless layer. """ self.assertRaises( NoKeywordsFoundError, self.keyword_io.read_keywords, self.keywordless_layer, ) def test_update_keywords(self): """Test append file keywords with update_keywords method.""" layer = clone_raster_layer(name='padang_tsunami_mw8', extension='.tif', include_keywords=True, source_directory=test_data_path('hazard')) new_keywords = {'category': 'exposure', 'test': 'TEST'} self.keyword_io.update_keywords(layer, new_keywords) keywords = self.keyword_io.read_keywords(layer) expected_keywords = { 'category': 'exposure', 'test': 'TEST', 'subcategory': 'tsunami', 'unit': 'm', 'title': 'A tsunami in Padang (Mw 8.8)' } message = 'Keywords: %s. Expected: %s' % (keywords, expected_keywords) self.assertEqual(keywords, expected_keywords, message) def test_read_db_keywords(self): """Can we read sqlite kw with the generic read_keywords method """ db_path = test_data_path('other', 'test_keywords.db') self.read_db_keywords(db_path) def read_db_keywords(self, db_path): """Can we read sqlite keywords with the generic readKeywords method """ self.keyword_io.set_keyword_db_path(db_path) # We need to use relative path so that the hash from URI will match local_path = os.path.join(os.path.dirname(__file__), 'exposure.sqlite') sqlite_building_path = test_data_path('exposure', 'exposure.sqlite') shutil.copy2(sqlite_building_path, local_path) uri = QgsDataSourceURI() uri.setDatabase('exposure.sqlite') uri.setDataSource('', 'buildings_osm_4326', 'Geometry') sqlite_layer = QgsVectorLayer(uri.uri(), 'OSM Buildings', 'spatialite') expected_source = ( 'dbname=\'exposure.sqlite\' table="buildings_osm_4326" (' 'Geometry) sql=') message = 'Got source: %s\n\nExpected %s\n' % (sqlite_layer.source(), expected_source) self.assertEqual(sqlite_layer.source(), expected_source, message) keywords = self.keyword_io.read_keywords(sqlite_layer) expected_keywords = self.expected_sqlite_keywords message = 'Got: %s\n\nExpected %s\n\nSource: %s' % ( keywords, expected_keywords, self.sqlite_layer.source()) self.assertEqual(keywords, expected_keywords, message) # Delete SQL Layer so that we can delete the file del sqlite_layer os.remove(local_path) def test_copy_keywords(self): """Test we can copy the keywords.""" out_path = unique_filename(prefix='test_copy_keywords', suffix='.keywords') self.keyword_io.copy_keywords(self.raster_layer, out_path) copied_keywords = read_file_keywords(out_path) expected_keywords = self.expected_raster_keywords message = 'Got:\n%s\nExpected:\n%s\nSource:\n%s' % ( copied_keywords, expected_keywords, out_path) self.assertEquals(copied_keywords, expected_keywords, message)
def accept(self): """Handler for when OK is clicked.""" input_path = self.input_path.text() input_title = self.line_edit_title.text() input_source = self.line_edit_source.text() output_path = self.output_path.text() if not output_path.endswith('.tif'): # noinspection PyArgumentList,PyCallByClass,PyTypeChecker QMessageBox.warning(self, self.tr('InaSAFE'), (self.tr('Output file name must be tif file'))) if not os.path.exists(input_path): # noinspection PyArgumentList,PyCallByClass,PyTypeChecker QMessageBox.warning(self, self.tr('InaSAFE'), (self.tr('Input file does not exist'))) return if self.nearest_mode.isChecked(): algorithm = 'nearest' else: algorithm = 'invdist' QtGui.qApp.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor)) file_name = convert_mmi_data(input_path, input_title, input_source, output_path, algorithm=algorithm, algorithm_filename_flag=True) # reclassify raster file_info = QFileInfo(file_name) base_name = file_info.baseName() self.output_layer = QgsRasterLayer(file_name, base_name) self.output_layer.keywords = KeywordIO.read_keywords(self.output_layer) self.output_layer.keywords['classification'] = ( earthquake_mmi_scale['key']) keywords = self.output_layer.keywords if self.output_layer.isValid(): self.output_layer = reclassify(self.output_layer, overwrite_input=True) KeywordIO.write_keywords(self.output_layer, keywords) else: LOGGER.debug("Failed to load") QtGui.qApp.restoreOverrideCursor() if self.load_result.isChecked(): # noinspection PyTypeChecker mmi_ramp_roman(self.output_layer) self.output_layer.saveDefaultStyle() if not self.output_layer.isValid(): LOGGER.debug("Failed to load") else: # noinspection PyArgumentList QgsMapLayerRegistry.instance().addMapLayer(self.output_layer) iface.zoomToActiveLayer() if (self.keyword_wizard_checkbox.isChecked() and self.keyword_wizard_checkbox.isEnabled()): self.launch_keyword_wizard() self.done(self.Accepted)
def save_hazard_data(self): hazard_geojson = PetaJakartaAPI.get_aggregate_report( self.duration, self.level) if not hazard_geojson: raise PetaJakartaAPIError("Can't access PetaJakarta REST API") with open(self.hazard_path, 'w+') as f: f.write(hazard_geojson) # Save the layer as shp file_info = QFileInfo(self.hazard_path) hazard_layer = QgsVectorLayer(self.hazard_path, file_info.baseName(), 'ogr', False) target_name = 'flood_data.shp' self.hazard_path = os.path.join(self.report_path, target_name) QgsVectorFileWriter.writeAsVectorFormat(hazard_layer, self.hazard_path, 'CP1250', None, 'ESRI Shapefile') file_info = QFileInfo(self.hazard_path) hazard_layer = QgsVectorLayer(self.hazard_path, file_info.baseName(), 'ogr') hazard_layer.startEditing() field = QgsField('flooded', QVariant.Int) hazard_layer.dataProvider().addAttributes([field]) hazard_layer.commitChanges() idx = hazard_layer.fieldNameIndex('flooded') expression = QgsExpression('count > 0') expression.prepare(hazard_layer.pendingFields()) hazard_layer.startEditing() for feature in hazard_layer.getFeatures(): feature[idx] = expression.evaluate(feature) hazard_layer.updateFeature(feature) hazard_layer.commitChanges() # writing keywords keyword_io = KeywordIO() keywords = { 'field': 'flooded', 'hazard': 'flood', 'hazard_category': 'single_event', 'keyword_version': '3.3', 'layer_geometry': 'polygon', 'layer_mode': 'classified', 'layer_purpose': 'hazard', 'title': 'Flood', 'value_map': '{"wet": [1], "dry": [0]}', 'vector_hazard_classification': 'flood_vector_hazard_classes' } keyword_io.write_keywords(hazard_layer, keywords) # archiving hazard layer with ZipFile(self.hazard_zip_path, 'w') as zf: for root, dirs, files in os.walk(self.report_path): for f in files: _, ext = os.path.splitext(f) if 'flood_data' in f: filename = os.path.join(root, f) zf.write(filename, arcname=f)
def get_analysis_dir(exposure_key=None): """Retrieve an output directory of an analysis/ImpactFunction from a multi exposure analysis/ImpactFunction based on exposure type. :param exposure_key: An exposure keyword. :type exposure_key: str :return: A directory contains analysis outputs. :rtype: str """ keyword_io = KeywordIO() layer_tree_root = QgsProject.instance().layerTreeRoot() all_groups = [ child for child in layer_tree_root.children() if (isinstance(child, QgsLayerTreeGroup)) ] multi_exposure_group = None for group in all_groups: if group.customProperty(MULTI_EXPOSURE_ANALYSIS_FLAG): multi_exposure_group = group break if multi_exposure_group: multi_exposure_tree_layers = [ child for child in multi_exposure_group.children() if (isinstance(child, QgsLayerTreeLayer)) ] exposure_groups = [ child for child in multi_exposure_group.children() if (isinstance(child, QgsLayerTreeGroup)) ] def get_report_ready_layer(tree_layers): """Get a layer which has a report inn its directory. :param tree_layers: A list of tree layer nodes (QgsLayerTreeLayer) :type tree_layers: list :return: A vector layer :rtype: QgsMapLayer """ for tree_layer in tree_layers: layer = tree_layer.layer() keywords = keyword_io.read_keywords(layer) extra_keywords_found = keywords.get('extra_keywords') provenance = keywords.get('provenance_data') if provenance: exposure_keywords = provenance.get('exposure_keywords', {}) exposure_key_found = exposure_keywords.get('exposure') if exposure_key_found and (exposure_key == exposure_key_found): return layer if not exposure_key and extra_keywords_found and ( extra_keywords_found[ extra_keyword_analysis_type['key']] == (MULTI_EXPOSURE_ANALYSIS_FLAG)): return layer return None layer = get_report_ready_layer(multi_exposure_tree_layers) if not layer: for exposure_group in exposure_groups: tree_layers = [ child for child in exposure_group.children() if (isinstance(child, QgsLayerTreeLayer)) ] layer = get_report_ready_layer(tree_layers) if layer: break if layer: return dirname(layer.source()) return None
def __init__( self, iface, template_metadata, impact_function=None, hazard=None, exposure=None, impact=None, analysis=None, exposure_summary_table=None, aggregation_summary=None, extra_layers=None, ordered_layers=None, legend_layers=None, minimum_needs_profile=None, multi_exposure_impact_function=None, use_template_extent=False): """Constructor for the Composition Report class. :param iface: Reference to the QGIS iface object. :type iface: QgsAppInterface :param template_metadata: InaSAFE template metadata. :type template_metadata: ReportMetadata :param impact_function: Impact function instance for the report :type impact_function: safe.impact_function.impact_function.ImpactFunction .. versionadded:: 4.0 """ LOGGER.debug('InaSAFE Impact Report class initialised') self._iface = iface self._metadata = template_metadata self._output_folder = None self._impact_function = impact_function or ( multi_exposure_impact_function) self._hazard = hazard or self._impact_function.hazard self._analysis = (analysis or self._impact_function.analysis_impacted) if impact_function: self._exposure = ( exposure or self._impact_function.exposure) self._impact = ( impact or self._impact_function.impact) self._exposure_summary_table = ( exposure_summary_table or self._impact_function.exposure_summary_table) self._aggregation_summary = ( aggregation_summary or self._impact_function.aggregation_summary) if extra_layers is None: extra_layers = [] self._extra_layers = extra_layers self._ordered_layers = ordered_layers self._legend_layers = legend_layers self._minimum_needs = minimum_needs_profile self._multi_exposure_impact_function = multi_exposure_impact_function self._use_template_extent = use_template_extent self._inasafe_context = InaSAFEReportContext() # QgsMapSettings is added in 2.4 if self._iface: map_settings = self._iface.mapCanvas().mapSettings() else: map_settings = QgsMapSettings() self._qgis_composition_context = QgsLayoutContext( None, map_settings, ImpactReport.DEFAULT_PAGE_DPI) self._keyword_io = KeywordIO()
class WizardDialog(QDialog, FORM_CLASS): """Dialog implementation class for the InaSAFE wizard.""" def __init__(self, parent=None, iface=None, dock=None): """Constructor for the dialog. .. note:: In QtDesigner the advanced editor's predefined keywords list should be shown in english always, so when adding entries to cboKeyword, be sure to choose :safe_qgis:`Properties<<` and untick the :safe_qgis:`translatable` property. :param parent: Parent widget of this dialog. :type parent: QWidget :param iface: QGIS QGisAppInterface instance. :type iface: QGisAppInterface :param dock: Dock widget instance that we can notify of changes to the keywords. Optional. :type dock: Dock """ QDialog.__init__(self, parent) self.setupUi(self) self.setWindowTitle('InaSAFE') # Constants self.keyword_creation_wizard_name = 'InaSAFE Keywords Creation Wizard' self.ifcw_name = 'InaSAFE Impact Function Centric Wizard' # Note the keys should remain untranslated as we need to write # english to the keywords file. # Save reference to the QGIS interface and parent self.iface = iface self.parent = parent self.dock = dock self.suppress_warning_dialog = False self.lblStep.clear() # Set icons self.lblMainIcon.setPixmap( QPixmap(resources_path('img', 'icons', 'icon-white.svg'))) self.keyword_io = KeywordIO() self.impact_function_manager = ImpactFunctionManager() self.is_selected_layer_keywordless = False self.parent_step = None self.pbnBack.setEnabled(False) self.pbnNext.setEnabled(False) self.pbnCancel.released.connect(self.reject) # Initialize attributes self.existing_keywords = None self.layer = None self.hazard_layer = None self.exposure_layer = None self.aggregation_layer = None self.analysis_handler = None self.step_kw_purpose = StepKwPurpose(self) self.step_kw_subcategory = StepKwSubcategory(self) self.step_kw_hazard_category = StepKwHazardCategory(self) self.step_kw_layermode = StepKwLayerMode(self) self.step_kw_unit = StepKwUnit(self) self.step_kw_classification = StepKwClassification(self) self.step_kw_field = StepKwField(self) self.step_kw_resample = StepKwResample(self) self.step_kw_classify = StepKwClassify(self) self.step_kw_name_field = StepKwNameField(self) self.step_kw_population_field = StepKwPopulationField(self) self.step_kw_extrakeywords = StepKwExtraKeywords(self) self.step_kw_aggregation = StepKwAggregation(self) self.step_kw_source = StepKwSource(self) self.step_kw_title = StepKwTitle(self) self.step_kw_summary = StepKwSummary(self) self.step_fc_functions1 = StepFcFunctions1(self) self.step_fc_functions2 = StepFcFunctions2(self) self.step_fc_function = StepFcFunction(self) self.step_fc_hazlayer_origin = StepFcHazLayerOrigin(self) self.step_fc_hazlayer_from_canvas = StepFcHazLayerFromCanvas(self) self.step_fc_hazlayer_from_browser = StepFcHazLayerFromBrowser(self) self.step_fc_explayer_origin = StepFcExpLayerOrigin(self) self.step_fc_explayer_from_canvas = StepFcExpLayerFromCanvas(self) self.step_fc_explayer_from_browser = StepFcExpLayerFromBrowser(self) self.step_fc_disjoint_layers = StepFcDisjointLayers(self) self.step_fc_agglayer_origin = StepFcAggLayerOrigin(self) self.step_fc_agglayer_from_canvas = StepFcAggLayerFromCanvas(self) self.step_fc_agglayer_from_browser = StepFcAggLayerFromBrowser(self) self.step_fc_agglayer_disjoint = StepFcAggLayerDisjoint(self) self.step_fc_extent = StepFcExtent(self) self.step_fc_extent_disjoint = StepFcExtentDisjoint(self) self.step_fc_params = StepFcParams(self) self.step_fc_summary = StepFcSummary(self) self.step_fc_analysis = StepFcAnalysis(self) self.stackedWidget.addWidget(self.step_kw_purpose) self.stackedWidget.addWidget(self.step_kw_subcategory) self.stackedWidget.addWidget(self.step_kw_hazard_category) self.stackedWidget.addWidget(self.step_kw_layermode) self.stackedWidget.addWidget(self.step_kw_unit) self.stackedWidget.addWidget(self.step_kw_classification) self.stackedWidget.addWidget(self.step_kw_field) self.stackedWidget.addWidget(self.step_kw_resample) self.stackedWidget.addWidget(self.step_kw_classify) self.stackedWidget.addWidget(self.step_kw_name_field) self.stackedWidget.addWidget(self.step_kw_population_field) self.stackedWidget.addWidget(self.step_kw_extrakeywords) self.stackedWidget.addWidget(self.step_kw_aggregation) self.stackedWidget.addWidget(self.step_kw_source) self.stackedWidget.addWidget(self.step_kw_title) self.stackedWidget.addWidget(self.step_kw_summary) self.stackedWidget.addWidget(self.step_fc_functions1) self.stackedWidget.addWidget(self.step_fc_functions2) self.stackedWidget.addWidget(self.step_fc_function) self.stackedWidget.addWidget(self.step_fc_hazlayer_origin) self.stackedWidget.addWidget(self.step_fc_hazlayer_from_canvas) self.stackedWidget.addWidget(self.step_fc_hazlayer_from_browser) self.stackedWidget.addWidget(self.step_fc_explayer_origin) self.stackedWidget.addWidget(self.step_fc_explayer_from_canvas) self.stackedWidget.addWidget(self.step_fc_explayer_from_browser) self.stackedWidget.addWidget(self.step_fc_disjoint_layers) self.stackedWidget.addWidget(self.step_fc_agglayer_origin) self.stackedWidget.addWidget(self.step_fc_agglayer_from_canvas) self.stackedWidget.addWidget(self.step_fc_agglayer_from_browser) self.stackedWidget.addWidget(self.step_fc_agglayer_disjoint) self.stackedWidget.addWidget(self.step_fc_extent) self.stackedWidget.addWidget(self.step_fc_extent_disjoint) self.stackedWidget.addWidget(self.step_fc_params) self.stackedWidget.addWidget(self.step_fc_summary) self.stackedWidget.addWidget(self.step_fc_analysis) def set_mode_label_to_keywords_creation(self): """Set the mode label to the Keywords Creation/Update mode """ self.setWindowTitle(self.keyword_creation_wizard_name) if self.get_existing_keyword('layer_purpose'): mode_name = ( self.tr('Keywords update wizard for layer <b>%s</b>') % self.layer.name()) else: mode_name = ( self.tr('Keywords creation wizard for layer <b>%s</b>') % self.layer.name()) self.lblSubtitle.setText(mode_name) def set_mode_label_to_ifcw(self): """Set the mode label to the IFCW """ self.setWindowTitle(self.ifcw_name) self.lblSubtitle.setText( self.tr('Use this wizard to run a guided impact assessment')) def set_keywords_creation_mode(self, layer=None): """Set the Wizard to the Keywords Creation mode :param layer: Layer to set the keywords for :type layer: QgsMapLayer """ self.layer = layer or self.iface.mapCanvas().currentLayer() try: self.existing_keywords = self.keyword_io.read_keywords(self.layer) # if 'layer_purpose' not in self.existing_keywords: # self.existing_keywords = None except (HashNotFoundError, OperationalError, NoKeywordsFoundError, KeywordNotFoundError, InvalidParameterError, UnsupportedProviderError, MetadataReadError): self.existing_keywords = None self.set_mode_label_to_keywords_creation() step = self.step_kw_purpose step.set_widgets() self.go_to_step(step) def set_function_centric_mode(self): """Set the Wizard to the Function Centric mode""" self.set_mode_label_to_ifcw() step = self.step_fc_functions1 step.set_widgets() self.go_to_step(step) def field_keyword_for_the_layer(self): """Return the proper keyword for field for the current layer. Expected values are: 'field', 'structure_class_field', road_class_field :returns: the field keyword :rtype: string """ if self.step_kw_purpose.selected_purpose() == \ layer_purpose_aggregation: # purpose: aggregation return 'aggregation attribute' elif self.step_kw_purpose.selected_purpose() == layer_purpose_hazard: # purpose: hazard if (self.step_kw_layermode.selected_layermode() == layer_mode_classified and is_point_layer(self.layer)): # No field for classified point hazards return '' else: # purpose: exposure layer_mode_key = self.step_kw_layermode.selected_layermode()['key'] layer_geometry_key = self.get_layer_geometry_id() exposure_key = self.step_kw_subcategory.\ selected_subcategory()['key'] exposure_class_fields = self.impact_function_manager.\ exposure_class_fields( layer_mode_key, layer_geometry_key, exposure_key) if exposure_class_fields and len(exposure_class_fields) == 1: return exposure_class_fields[0]['key'] # Fallback to default return 'field' def get_parent_mode_constraints(self): """Return the category and subcategory keys to be set in the subordinate mode. :returns: (the category definition, the hazard/exposure definition) :rtype: (dict, dict) """ h, e, _hc, _ec = self.selected_impact_function_constraints() if self.parent_step in [ self.step_fc_hazlayer_from_canvas, self.step_fc_hazlayer_from_browser ]: category = layer_purpose_hazard subcategory = h elif self.parent_step in [ self.step_fc_explayer_from_canvas, self.step_fc_explayer_from_browser ]: category = layer_purpose_exposure subcategory = e elif self.parent_step: category = layer_purpose_aggregation subcategory = None else: category = None subcategory = None return category, subcategory def selected_impact_function_constraints(self): """Obtain impact function constraints selected by user. :returns: Tuple of metadata of hazard, exposure, hazard layer constraints and exposure layer constraints :rtype: tuple """ selection = self.step_fc_functions1.tblFunctions1.selectedItems() if len(selection) != 1: return None, None, None, None h = selection[0].data(RoleHazard) e = selection[0].data(RoleExposure) selection = self.step_fc_functions2.tblFunctions2.selectedItems() if len(selection) != 1: return h, e, None, None hc = selection[0].data(RoleHazardConstraint) ec = selection[0].data(RoleExposureConstraint) return h, e, hc, ec def is_layer_compatible(self, layer, layer_purpose=None, keywords=None): """Validate if a given layer is compatible for selected IF as a given layer_purpose :param layer: The layer to be validated :type layer: QgsVectorLayer | QgsRasterLayer :param layer_purpose: The layer_purpose the layer is validated for :type layer_purpose: None, string :param keywords: The layer keywords :type keywords: None, dict :returns: True if layer is appropriate for the selected role :rtype: boolean """ # If not explicitly stated, find the desired purpose # from the parent step if not layer_purpose: layer_purpose = self.get_parent_mode_constraints()[0]['key'] # If not explicitly stated, read the layer's keywords if not keywords: 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): keywords = None # Get allowed subcategory and layer_geometry from IF constraints h, e, hc, ec = self.selected_impact_function_constraints() if layer_purpose == 'hazard': subcategory = h['key'] layer_geometry = hc['key'] elif layer_purpose == 'exposure': subcategory = e['key'] layer_geometry = ec['key'] else: # For aggregation layers, use a simplified test and return if (keywords and 'layer_purpose' in keywords and keywords['layer_purpose'] == layer_purpose): return True if not keywords and is_polygon_layer(layer): return True return False # Compare layer properties with explicitly set constraints # Reject if layer geometry doesn't match if layer_geometry != self.get_layer_geometry_id(layer): return False # If no keywords, there's nothing more we can check. # The same if the keywords version doesn't match if not keywords or 'keyword_version' not in keywords: return True keyword_version = str(keywords['keyword_version']) if not is_keyword_version_supported(keyword_version): return True # Compare layer keywords with explicitly set constraints # Reject if layer purpose missing or doesn't match if ('layer_purpose' not in keywords or keywords['layer_purpose'] != layer_purpose): return False # Reject if layer subcategory doesn't match if (layer_purpose in keywords and keywords[layer_purpose] != subcategory): return False # Compare layer keywords with the chosen function's constraints imfunc = self.step_fc_function.selected_function() lay_req = imfunc['layer_requirements'][layer_purpose] # Reject if layer mode doesn't match if ('layer_mode' in keywords and lay_req['layer_mode']['key'] != keywords['layer_mode']): return False # Reject if classification doesn't match classification_key = '%s_%s_classification' % ( 'raster' if is_raster_layer(layer) else 'vector', layer_purpose) classification_keys = classification_key + 's' if (lay_req['layer_mode'] == layer_mode_classified and classification_key in keywords and classification_keys in lay_req): allowed_classifications = [ c['key'] for c in lay_req[classification_keys] ] if keywords[classification_key] not in allowed_classifications: return False # Reject if unit doesn't match unit_key = ('continuous_hazard_unit' if layer_purpose == layer_purpose_hazard['key'] else 'exposure_unit') unit_keys = unit_key + 's' if (lay_req['layer_mode'] == layer_mode_continuous and unit_key in keywords and unit_keys in lay_req): allowed_units = [c['key'] for c in lay_req[unit_keys]] if keywords[unit_key] not in allowed_units: return False # Finally return True return True def get_compatible_canvas_layers(self, category): """Collect layers from map canvas, compatible for the given category and selected impact function .. note:: Returns layers with keywords and layermode matching the category and compatible with the selected impact function. Also returns layers without keywords with layermode compatible with the selected impact function. :param category: The category to filter for. :type category: string :returns: Metadata of found layers. :rtype: list of dicts """ # Collect compatible layers layers = [] for layer in self.iface.mapCanvas().layers(): 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): keywords = None if self.is_layer_compatible(layer, category, keywords): layers += [{ 'id': layer.id(), 'name': layer.name(), 'keywords': keywords }] # Move layers without keywords to the end l1 = [l for l in layers if l['keywords']] l2 = [l for l in layers if not l['keywords']] layers = l1 + l2 return layers def get_layer_geometry_id(self, layer=None): """Obtain layer mode of a given layer. If no layer specified, the current layer is used :param layer : layer to examine :type layer: QgsMapLayer or None :returns: The layer mode. :rtype: str """ if not layer: layer = self.layer if is_raster_layer(layer): return 'raster' elif is_point_layer(layer): return 'point' elif is_polygon_layer(layer): return 'polygon' else: return 'line' def get_existing_keyword(self, keyword): """Obtain an existing keyword's value. :param keyword: A keyword from keywords. :type keyword: str :returns: The value of the keyword. :rtype: str, QUrl """ if self.existing_keywords is None: return None if keyword is not None: return self.existing_keywords.get(keyword, None) else: return None def get_layer_description_from_canvas(self, layer, purpose): """Obtain the description of a canvas layer selected by user. :param layer: The QGIS layer. :type layer: QgsMapLayer :param category: The category of the layer to get the description. :type category: string :returns: description of the selected layer. :rtype: string """ if not layer: return "" try: keywords = self.keyword_io.read_keywords(layer) if 'layer_purpose' not in keywords: keywords = None except (HashNotFoundError, OperationalError, NoKeywordsFoundError, KeywordNotFoundError, InvalidParameterError, UnsupportedProviderError): keywords = None # set the current layer (e.g. for the keyword creation sub-thread) self.layer = layer if purpose == 'hazard': self.hazard_layer = layer elif purpose == 'exposure': self.exposure_layer = layer else: self.aggregation_layer = layer # Check if the layer is keywordless if keywords and 'keyword_version' in keywords: kw_ver = str(keywords['keyword_version']) self.is_selected_layer_keywordless = ( not is_keyword_version_supported(kw_ver)) else: self.is_selected_layer_keywordless = True desc = layer_description_html(layer, keywords) return desc # =========================== # NAVIGATION # =========================== def go_to_step(self, step): """Set the stacked widget to the given step, set up the buttons, and run all operations that should start immediately after entering the new step. :param step: The step widget to be moved to. :type step: QWidget """ self.stackedWidget.setCurrentWidget(step) # Disable the Next button unless new data already entered self.pbnNext.setEnabled(step.is_ready_to_next_step()) # Enable the Back button unless it's not the first step self.pbnBack.setEnabled( step not in [self.step_kw_purpose, self.step_fc_functions1] or self.parent_step is not None) # Set Next button label if (step in [self.step_kw_summary, self.step_fc_analysis] and self.parent_step is None): self.pbnNext.setText(self.tr('Finish')) elif step == self.step_fc_summary: self.pbnNext.setText(self.tr('Run')) else: self.pbnNext.setText(self.tr('Next')) # Run analysis after switching to the new step if step == self.step_fc_analysis: self.step_fc_analysis.setup_and_run_analysis() # Set lblSelectCategory label if entering the kw mode # from the ifcw mode if step == self.step_kw_purpose and self.parent_step: if self.parent_step in [ self.step_fc_hazlayer_from_canvas, self.step_fc_hazlayer_from_browser ]: text_label = category_question_hazard elif self.parent_step in [ self.step_fc_explayer_from_canvas, self.step_fc_explayer_from_browser ]: text_label = category_question_exposure else: text_label = category_question_aggregation self.step_kw_purpose.lblSelectCategory.setText(text_label) # prevents actions being handled twice # noinspection PyPep8Naming @pyqtSignature('') def on_pbnNext_released(self): """Handle the Next button release. .. note:: This is an automatic Qt slot executed when the Next button is released. """ current_step = self.get_current_step() # Save keywords if it's the end of the keyword creation mode if current_step == self.step_kw_summary: self.save_current_keywords() if current_step == self.step_kw_aggregation: good_age_ratio, sum_age_ratios = self.step_kw_aggregation.\ age_ratios_are_valid() if not good_age_ratio: message = self.tr( 'The sum of age ratio default is %s and it is more ' 'than 1. Please adjust the age ratio default so that they ' 'will not more than 1.' % sum_age_ratios) if not self.suppress_warning_dialog: # noinspection PyCallByClass,PyTypeChecker,PyArgumentList QtGui.QMessageBox.warning(self, self.tr('InaSAFE'), message) return # After any step involving Browser, add selected layer to map canvas if current_step in [ self.step_fc_hazlayer_from_browser, self.step_fc_explayer_from_browser, self.step_fc_agglayer_from_browser ]: if not QgsMapLayerRegistry.instance().mapLayersByName( self.layer.name()): QgsMapLayerRegistry.instance().addMapLayers([self.layer]) # Make the layer visible. Might be hidden by default. See #2925 legend = self.iface.legendInterface() legend.setLayerVisible(self.layer, True) # After the extent selection, save the extent and disconnect signals if current_step == self.step_fc_extent: self.step_fc_extent.write_extent() # Determine the new step to be switched new_step = current_step.get_next_step() if (new_step == self.step_kw_extrakeywords and not self. step_kw_extrakeywords.additional_keywords_for_the_layer()): # Skip the extra_keywords tab if no extra keywords available: new_step = self.step_kw_source if new_step is not None: # Prepare the next tab new_step.set_widgets() else: # Wizard complete self.accept() return self.go_to_step(new_step) # prevents actions being handled twice # noinspection PyPep8Naming @pyqtSignature('') def on_pbnBack_released(self): """Handle the Back button release. .. note:: This is an automatic Qt slot executed when the Back button is released. """ current_step = self.get_current_step() new_step = current_step.get_previous_step() # set focus to table widgets, as the inactive selection style is gray if new_step == self.step_fc_functions1: self.step_fc_functions1.tblFunctions1.setFocus() if new_step == self.step_fc_functions2: self.step_fc_functions2.tblFunctions2.setFocus() # Re-connect disconnected signals when coming back to the Extent step if new_step == self.step_fc_extent: self.step_fc_extent.set_widgets() # Set Next button label self.pbnNext.setText(self.tr('Next')) self.pbnNext.setEnabled(True) self.go_to_step(new_step) def get_current_step(self): """Return current step of the wizard. :returns: Current step of the wizard. :rtype: WizardStep instance """ return self.stackedWidget.currentWidget() def get_keywords(self): """Obtain the state of the dialog as a keywords dict. :returns: Keywords reflecting the state of the dialog. :rtype: dict """ keywords = {} keywords['layer_geometry'] = self.get_layer_geometry_id() if self.step_kw_purpose.selected_purpose(): keywords['layer_purpose'] = self.step_kw_purpose.\ selected_purpose()['key'] if keywords['layer_purpose'] == 'aggregation': keywords.update( self.step_kw_aggregation.get_aggregation_attributes()) if self.step_kw_subcategory.selected_subcategory(): key = self.step_kw_purpose.selected_purpose()['key'] keywords[key] = self.step_kw_subcategory.\ selected_subcategory()['key'] if self.step_kw_hazard_category.selected_hazard_category(): keywords['hazard_category'] \ = self.step_kw_hazard_category.\ selected_hazard_category()['key'] if self.step_kw_layermode.selected_layermode(): keywords['layer_mode'] = self.step_kw_layermode.\ selected_layermode()['key'] if self.step_kw_unit.selected_unit(): if self.step_kw_purpose.selected_purpose() == layer_purpose_hazard: key = continuous_hazard_unit['key'] else: key = exposure_unit['key'] keywords[key] = self.step_kw_unit.selected_unit()['key'] if self.step_kw_resample.selected_allowresampling() is not None: keywords['allow_resampling'] = ( self.step_kw_resample.selected_allowresampling() and 'true' or 'false') if self.step_kw_field.lstFields.currentItem(): field_keyword = self.field_keyword_for_the_layer() keywords[field_keyword] = self.step_kw_field.\ lstFields.currentItem().text() if self.step_kw_classification.selected_classification(): geom = 'raster' if is_raster_layer(self.layer) else 'vector' key = '%s_%s_classification' % ( geom, self.step_kw_purpose.selected_purpose()['key']) keywords[key] = self.step_kw_classification.\ selected_classification()['key'] value_map = self.step_kw_classify.selected_mapping() if value_map: if self.step_kw_classification.selected_classification(): # hazard mapping keyword = 'value_map' else: # exposure mapping keyword = 'value_mapping' keywords[keyword] = json.dumps(value_map) name_field = self.step_kw_name_field.selected_field() if name_field: keywords['name_field'] = name_field population_field = self.step_kw_population_field.selected_field() if population_field: keywords['population_field'] = population_field extra_keywords = self.step_kw_extrakeywords.selected_extra_keywords() for key in extra_keywords: keywords[key] = extra_keywords[key] if self.step_kw_source.leSource.text(): keywords['source'] = get_unicode( self.step_kw_source.leSource.text()) if self.step_kw_source.leSource_url.text(): keywords['url'] = get_unicode( self.step_kw_source.leSource_url.text()) if self.step_kw_source.leSource_scale.text(): keywords['scale'] = get_unicode( self.step_kw_source.leSource_scale.text()) if self.step_kw_source.ckbSource_date.isChecked(): keywords['date'] = self.step_kw_source.dtSource_date.dateTime() if self.step_kw_source.leSource_license.text(): keywords['license'] = get_unicode( self.step_kw_source.leSource_license.text()) if self.step_kw_title.leTitle.text(): keywords['title'] = get_unicode(self.step_kw_title.leTitle.text()) return keywords def save_current_keywords(self): """Save keywords to the layer. It will write out the keywords for the current layer. This method is based on the KeywordsDialog class. """ current_keywords = self.get_keywords() try: self.keyword_io.write_keywords(layer=self.layer, keywords=current_keywords) except InaSAFEError, e: error_message = get_error_message(e) # noinspection PyCallByClass,PyTypeChecker,PyArgumentList QtGui.QMessageBox.warning( self, self.tr('InaSAFE'), ((self.tr('An error was encountered when saving the following ' 'keywords:\n %s') % error_message.to_html()))) if self.dock is not None: # noinspection PyUnresolvedReferences self.dock.get_layers()
class SaveScenarioDialog(QDialog): """Tools for saving an active scenario on the dock.""" def __init__(self, iface, dock): """Constructor for the class.""" QDialog.__init__(self) # Class Member self.iface = iface self.dock = dock self.output_directory = None self.exposure_layer = None self.hazard_layer = None self.aggregation_layer = None self.keyword_io = KeywordIO() icon = resources_path('img', 'icons', 'save-as-scenario.svg') self.setWindowIcon(QtGui.QIcon(icon)) # Calling some init methods self.restore_state() def restore_state(self): """Read last state of GUI from configuration file.""" self.output_directory = setting('lastSourceDir', '.', str) def save_state(self): """Store current state of GUI to configuration file.""" set_setting('lastSourceDir', self.output_directory) def validate_input(self): """Validate the input before saving a scenario. Those validations are: 1. self.exposure_layer must be not None 2. self.hazard_layer must be not None 3. self.function_id is not an empty string or None """ self.exposure_layer = layer_from_combo(self.dock.exposure_layer_combo) self.hazard_layer = layer_from_combo(self.dock.hazard_layer_combo) self.aggregation_layer = layer_from_combo( self.dock.aggregation_layer_combo) is_valid = True warning_message = None if self.exposure_layer is None: warning_message = tr( 'Exposure layer is not found, can not save scenario. Please ' 'add exposure layer to do so.') is_valid = False if self.hazard_layer is None: warning_message = tr( 'Hazard layer is not found, can not save scenario. Please add ' 'hazard layer to do so.') is_valid = False return is_valid, warning_message def save_scenario(self, scenario_file_path=None): """Save current scenario to a text file. You can use the saved scenario with the batch runner. :param scenario_file_path: A path to the scenario file. :type scenario_file_path: str """ # Validate Input warning_title = tr('InaSAFE Save Scenario Warning') is_valid, warning_message = self.validate_input() if not is_valid: # noinspection PyCallByClass,PyTypeChecker,PyArgumentList QMessageBox.warning(self, warning_title, warning_message) return # Make extent to look like: # 109.829170982, -8.13333290561, 111.005344795, -7.49226294379 # Added in 2.2 to support user defined analysis extents if self.dock.extent.user_extent is not None \ and self.dock.extent.crs is not None: # In V4.0, user_extent is QgsGeometry. user_extent = self.dock.extent.user_extent.boundingBox() extent = extent_to_array(user_extent, self.dock.extent.crs) else: extent = viewport_geo_array(self.iface.mapCanvas()) extent_string = ', '.join(('%f' % x) for x in extent) exposure_path = self.exposure_layer.source() hazard_path = self.hazard_layer.source() title = self.keyword_io.read_keywords(self.hazard_layer, 'title') title = tr(title) default_filename = title.replace( ' ', '_').replace('(', '').replace(')', '') # Popup a dialog to request the filename if scenario_file_path = None dialog_title = tr('Save Scenario') if scenario_file_path is None: # noinspection PyCallByClass,PyTypeChecker scenario_file_path, __ = QFileDialog.getSaveFileName( self, dialog_title, os.path.join(self.output_directory, default_filename + '.txt'), "Text files (*.txt)") if scenario_file_path is None or scenario_file_path == '': return self.output_directory = os.path.dirname(scenario_file_path) # Write to file parser = ConfigParser() parser.add_section(title) # Relative path is not recognized by the batch runner, so we use # absolute path. parser.set(title, 'exposure', exposure_path) parser.set(title, 'hazard', hazard_path) parser.set(title, 'extent', extent_string) if self.dock.extent.crs is None: parser.set(title, 'extent_crs', 'EPSG:4326') else: parser.set( title, 'extent_crs', self.dock.extent.crs.authid()) if self.aggregation_layer is not None: aggregation_path = self.aggregation_layer.source() relative_aggregation_path = self.relative_path( scenario_file_path, aggregation_path) parser.set(title, 'aggregation', relative_aggregation_path) # noinspection PyBroadException try: of = open(scenario_file_path, 'a') parser.write(of) of.close() except Exception as e: # noinspection PyTypeChecker,PyCallByClass,PyArgumentList QMessageBox.warning( self, 'InaSAFE', tr( 'Failed to save scenario to {path}, exception ' '{exception}').format( path=scenario_file_path, exception=str(e))) finally: of.close() # Save State self.save_state() @staticmethod def relative_path(reference_path, input_path): """Get the relative path to input_path from reference_path. :param reference_path: The reference path. :type reference_path: str :param input_path: The input path. :type input_path: str """ start_path = os.path.dirname(reference_path) try: relative_path = os.path.relpath(input_path, start_path) except ValueError: # LOGGER.info(e.message) relative_path = input_path return relative_path
class KeywordIOTest(unittest.TestCase): """Tests for reading and writing of raster and vector data.""" def setUp(self): self.keyword_io = KeywordIO() # SQLite Layer uri = QgsDataSourceUri() sqlite_building_path = standard_data_path( 'exposure', 'exposure.sqlite') uri.setDatabase(sqlite_building_path) uri.setDataSource('', 'buildings_osm_4326', 'Geometry') self.sqlite_layer = QgsVectorLayer( uri.uri(), 'OSM Buildings', 'spatialite') self.expected_sqlite_keywords = { 'datatype': 'OSM' } # Raster Layer keywords hazard_path = standard_data_path('hazard', 'tsunami_wgs84.tif') self.raster_layer, _ = load_layer(hazard_path, provider='gdal') self.expected_raster_keywords = { 'hazard_category': 'single_event', 'title': 'Generic Continuous Flood', 'hazard': 'flood', 'continuous_hazard_unit': 'generic', 'layer_geometry': 'raster', 'layer_purpose': 'hazard', 'layer_mode': 'continuous', 'keyword_version': '3.5' } # Vector Layer keywords vector_path = standard_data_path('exposure', 'buildings_osm_4326.shp') self.vector_layer, _ = load_layer(vector_path, provider='ogr') self.expected_vector_keywords = { 'keyword_version': '3.5', 'value_map': {}, 'title': 'buildings_osm_4326', 'layer_geometry': 'polygon', 'layer_purpose': 'exposure', 'layer_mode': 'classified', 'exposure': 'structure', } # Keyword less layer keywordless_path = standard_data_path('other', 'keywordless_layer.shp') self.keywordless_layer, _ = load_layer( keywordless_path, provider='ogr') # Keyword file self.keyword_path = standard_data_path( 'exposure', 'buildings_osm_4326.xml') def test_read_raster_file_keywords(self): """Can we read raster file keywords using generic readKeywords method """ layer = clone_raster_layer( name='generic_continuous_flood', extension='.asc', include_keywords=True, source_directory=standard_data_path('hazard')) keywords = self.keyword_io.read_keywords(layer) expected_keywords = self.expected_raster_keywords self.assertDictEqual(keywords, expected_keywords) def test_read_vector_file_keywords(self): """Test read vector file keywords with the generic readKeywords method. """ self.maxDiff = None keywords = self.keyword_io.read_keywords(self.vector_layer) expected_keywords = self.expected_vector_keywords self.assertDictEqual(keywords, expected_keywords) def test_read_keywordless_layer(self): """Test read 'keyword' file from keywordless layer. """ self.assertRaises( NoKeywordsFoundError, self.keyword_io.read_keywords, self.keywordless_layer, ) def test_to_message(self): """Test we can convert keywords to a message object. .. versionadded:: 3.2 """ keywords = self.keyword_io.read_keywords(self.vector_layer) message = self.keyword_io.to_message(keywords).to_text() self.assertIn('*Exposure*, Structures------', message) def test_layer_to_message(self): """Test to show augmented keywords if KeywordsIO ctor passed a layer. .. versionadded:: 3.3 """ keywords = KeywordIO(self.vector_layer) message = keywords.to_message().to_text() self.assertIn('*Reference system*, ', message) def test_dict_to_row(self): """Test the dict to row helper works. .. versionadded:: 3.2 """ keyword_value = ( "{'high': ['Kawasan Rawan Bencana III'], " "'medium': ['Kawasan Rawan Bencana II'], " "'low': ['Kawasan Rawan Bencana I']}") table = self.keyword_io._dict_to_row(keyword_value) self.assertIn( '\n---\n*High*, Kawasan Rawan Bencana III------', table.to_text()) # should also work passing a dict keyword_value = { 'high': ['Kawasan Rawan Bencana III'], 'medium': ['Kawasan Rawan Bencana II'], 'low': ['Kawasan Rawan Bencana I']} table = self.keyword_io._dict_to_row(keyword_value) self.assertIn( '\n---\n*High*, Kawasan Rawan Bencana III------', table.to_text())
class MultiExposureDialog(QDialog, FORM_CLASS): """Dialog for multi exposure tool.""" def __init__(self, parent=None, iface=iface_object): """Constructor for the multi exposure dialog. :param parent: Parent widget of this dialog. :type parent: QWidget :param iface: An instance of QGisInterface :type iface: QGisInterface """ QDialog.__init__(self, parent) self.use_selected_only = setting('useSelectedFeaturesOnly', expected_type=bool) self.parent = parent self.iface = iface self.setupUi(self) icon = resources_path('img', 'icons', 'show-multi-exposure.svg') self.setWindowIcon(QIcon(icon)) self.tab_widget.setCurrentIndex(0) self.combos_exposures = {} self.keyword_io = KeywordIO() self._create_exposure_combos() self._multi_exposure_if = None self._extent = Extent(iface) self._extent.show_rubber_bands = setting('showRubberBands', False, bool) enable_messaging(self.message_viewer, self) self.btn_back.clicked.connect(self.back_clicked) self.btn_next.clicked.connect(self.next_clicked) self.btn_cancel.clicked.connect(self.reject) self.btn_run.clicked.connect(self.accept) self.validate_impact_function() self.tab_widget.currentChanged.connect(self._tab_changed) self.tree.itemSelectionChanged.connect(self._tree_selection_changed) self.list_layers_in_map_report.itemSelectionChanged.connect( self._list_selection_changed) self.add_layer.clicked.connect(self._add_layer_clicked) self.remove_layer.clicked.connect(self._remove_layer_clicked) self.move_up.clicked.connect(self.move_layer_up) self.move_down.clicked.connect(self.move_layer_down) self.cbx_hazard.currentIndexChanged.connect( self.validate_impact_function) self.cbx_aggregation.currentIndexChanged.connect( self.validate_impact_function) # Keep track of the current panel self._current_index = 0 self.tab_widget.setCurrentIndex(self._current_index) def _tab_changed(self): """Triggered when the current tab is changed.""" current = self.tab_widget.currentWidget() if current == self.analysisTab: self.btn_back.setEnabled(False) self.btn_next.setEnabled(True) elif current == self.reportingTab: if self._current_index == 0: # Only if the user is coming from the first tab self._populate_reporting_tab() self.reporting_options_layout.setEnabled( self._multi_exposure_if is not None) self.btn_back.setEnabled(True) self.btn_next.setEnabled(True) else: self.btn_back.setEnabled(True) self.btn_next.setEnabled(False) self._current_index = current def back_clicked(self): """Back button clicked.""" self.tab_widget.setCurrentIndex(self.tab_widget.currentIndex() - 1) def next_clicked(self): """Next button clicked.""" self.tab_widget.setCurrentIndex(self.tab_widget.currentIndex() + 1) def ordered_expected_layers(self): """Get an ordered list of layers according to users input. 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. :return: An ordered list of layers following a structure. :rtype: list """ registry = QgsMapLayerRegistry.instance() layers = [] count = self.list_layers_in_map_report.count() for i in range(count): layer = self.list_layers_in_map_report.item(i) origin = layer.data(LAYER_ORIGIN_ROLE) if origin == FROM_ANALYSIS['key']: key = layer.data(LAYER_PURPOSE_KEY_OR_ID_ROLE) parent = layer.data(LAYER_PARENT_ANALYSIS_ROLE) layers.append((FROM_ANALYSIS['key'], key, parent, None)) else: layer_id = layer.data(LAYER_PURPOSE_KEY_OR_ID_ROLE) layer = registry.mapLayer(layer_id) style_document = QDomDocument() error = '' layer.exportNamedStyle(style_document, error) layers.append( (FROM_CANVAS['key'], layer.name(), full_layer_uri(layer), style_document.toString())) return layers def _add_layer_clicked(self): """Add layer clicked.""" layer = self.tree.selectedItems()[0] origin = layer.data(0, LAYER_ORIGIN_ROLE) if origin == FROM_ANALYSIS['key']: parent = layer.data(0, LAYER_PARENT_ANALYSIS_ROLE) key = layer.data(0, LAYER_PURPOSE_KEY_OR_ID_ROLE) item = QListWidgetItem('%s - %s' % (layer.text(0), parent)) item.setData(LAYER_PARENT_ANALYSIS_ROLE, parent) item.setData(LAYER_PURPOSE_KEY_OR_ID_ROLE, key) else: item = QListWidgetItem(layer.text(0)) layer_id = layer.data(0, LAYER_PURPOSE_KEY_OR_ID_ROLE) item.setData(LAYER_PURPOSE_KEY_OR_ID_ROLE, layer_id) item.setData(LAYER_ORIGIN_ROLE, origin) self.list_layers_in_map_report.addItem(item) self.tree.invisibleRootItem().removeChild(layer) self.tree.clearSelection() def _remove_layer_clicked(self): """Remove layer clicked.""" layer = self.list_layers_in_map_report.selectedItems()[0] origin = layer.data(LAYER_ORIGIN_ROLE) if origin == FROM_ANALYSIS['key']: key = layer.data(LAYER_PURPOSE_KEY_OR_ID_ROLE) parent = layer.data(LAYER_PARENT_ANALYSIS_ROLE) parent_item = self.tree.findItems( parent, Qt.MatchContains | Qt.MatchRecursive, 0)[0] item = QTreeWidgetItem(parent_item, [definition(key)['name']]) item.setData(0, LAYER_PARENT_ANALYSIS_ROLE, parent) else: parent_item = self.tree.findItems( FROM_CANVAS['name'], Qt.MatchContains | Qt.MatchRecursive, 0)[0] item = QTreeWidgetItem(parent_item, [layer.text()]) layer_id = layer.data(LAYER_PURPOSE_KEY_OR_ID_ROLE) item.setData(0, LAYER_PURPOSE_KEY_OR_ID_ROLE, layer_id) item.setData(0, LAYER_ORIGIN_ROLE, origin) index = self.list_layers_in_map_report.indexFromItem(layer) self.list_layers_in_map_report.takeItem(index.row()) self.list_layers_in_map_report.clearSelection() def move_layer_up(self): """Move the layer up.""" layer = self.list_layers_in_map_report.selectedItems()[0] index = self.list_layers_in_map_report.indexFromItem(layer).row() item = self.list_layers_in_map_report.takeItem(index) self.list_layers_in_map_report.insertItem(index - 1, item) self.list_layers_in_map_report.item(index - 1).setSelected(True) def move_layer_down(self): """Move the layer down.""" layer = self.list_layers_in_map_report.selectedItems()[0] index = self.list_layers_in_map_report.indexFromItem(layer).row() item = self.list_layers_in_map_report.takeItem(index) self.list_layers_in_map_report.insertItem(index + 1, item) self.list_layers_in_map_report.item(index + 1).setSelected(True) def _list_selection_changed(self): """Selection has changed in the list.""" items = self.list_layers_in_map_report.selectedItems() self.remove_layer.setEnabled(len(items) >= 1) if len(items) == 1 and self.list_layers_in_map_report.count() >= 2: index = self.list_layers_in_map_report.indexFromItem(items[0]) index = index.row() if index == 0: self.move_up.setEnabled(False) self.move_down.setEnabled(True) elif index == self.list_layers_in_map_report.count() - 1: self.move_up.setEnabled(True) self.move_down.setEnabled(False) else: self.move_up.setEnabled(True) self.move_down.setEnabled(True) else: self.move_up.setEnabled(False) self.move_down.setEnabled(False) def _tree_selection_changed(self): """Selection has changed in the tree.""" self.add_layer.setEnabled(len(self.tree.selectedItems()) >= 1) def _populate_reporting_tab(self): """Populate trees about layers.""" self.tree.clear() self.add_layer.setEnabled(False) self.remove_layer.setEnabled(False) self.move_up.setEnabled(False) self.move_down.setEnabled(False) self.tree.setColumnCount(1) self.tree.setRootIsDecorated(False) self.tree.setHeaderHidden(True) analysis_branch = QTreeWidgetItem(self.tree.invisibleRootItem(), [FROM_ANALYSIS['name']]) analysis_branch.setFont(0, bold_font) analysis_branch.setExpanded(True) analysis_branch.setFlags(Qt.ItemIsEnabled) if self._multi_exposure_if: expected = self._multi_exposure_if.output_layers_expected() for group, layers in expected.iteritems(): group_branch = QTreeWidgetItem(analysis_branch, [group]) group_branch.setFont(0, bold_font) group_branch.setExpanded(True) group_branch.setFlags(Qt.ItemIsEnabled) for layer in layers: layer = definition(layer) if layer.get('allowed_geometries', None): item = QTreeWidgetItem(group_branch, [layer.get('name')]) item.setData(0, LAYER_ORIGIN_ROLE, FROM_ANALYSIS['key']) item.setData(0, LAYER_PARENT_ANALYSIS_ROLE, group) item.setData(0, LAYER_PURPOSE_KEY_OR_ID_ROLE, layer['key']) item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable) canvas_branch = QTreeWidgetItem(self.tree.invisibleRootItem(), [FROM_CANVAS['name']]) canvas_branch.setFont(0, bold_font) canvas_branch.setExpanded(True) canvas_branch.setFlags(Qt.ItemIsEnabled) # List layers from the canvas loaded_layers = QgsMapLayerRegistry.instance().mapLayers().values() canvas_layers = self.iface.mapCanvas().layers() flag = setting('visibleLayersOnlyFlag', expected_type=bool) for loaded_layer in loaded_layers: if flag and loaded_layer not in canvas_layers: continue title = loaded_layer.name() item = QTreeWidgetItem(canvas_branch, [title]) item.setData(0, LAYER_ORIGIN_ROLE, FROM_CANVAS['key']) item.setData(0, LAYER_PURPOSE_KEY_OR_ID_ROLE, loaded_layer.id()) item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable) self.tree.resizeColumnToContents(0) 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 registry = QgsMapLayerRegistry.instance() canvas_layers = self.iface.mapCanvas().layers() # MapLayers returns a QMap<QString id, QgsMapLayer layer> layers = registry.mapLayers().values() 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: # 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: # 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: # pylint: disable=W0702 # continue ignoring this layer continue for key, combo in self.combos_exposures.iteritems(): 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 self.combos_exposures.itervalues(): combo.currentIndexChanged.connect(self.validate_impact_function) def progress_callback(self, current_value, maximum_value, message=None): """GUI based callback implementation for showing progress. :param current_value: Current progress. :type current_value: int :param maximum_value: Maximum range (point at which task is complete. :type maximum_value: int :param message: Optional message dictionary to containing content we can display to the user. See safe.definitions.analysis_steps for an example of the expected format :type message: dict """ report = m.Message() report.add(LOGO_ELEMENT) report.add(m.Heading(self.tr('Analysis status'), **INFO_STYLE)) if message is not None: report.add(m.ImportantText(message['name'])) report.add(m.Paragraph(message['description'])) report.add(self._multi_exposure_if.current_impact_function. performance_log_message()) send_static_message(self, report) self.progress_bar.setMaximum(maximum_value) self.progress_bar.setValue(current_value) QApplication.processEvents() def validate_impact_function(self): """Check validity of the current impact function.""" # Always set it to False self.btn_run.setEnabled(False) for combo in self.combos_exposures.itervalues(): if combo.count() == 1: combo.setEnabled(False) hazard = layer_from_combo(self.cbx_hazard) aggregation = layer_from_combo(self.cbx_aggregation) exposures = [] for combo in self.combos_exposures.itervalues(): exposures.append(layer_from_combo(combo)) exposures = [layer for layer in exposures if layer] multi_exposure_if = MultiExposureImpactFunction() multi_exposure_if.hazard = hazard multi_exposure_if.exposures = exposures multi_exposure_if.debug = False multi_exposure_if.callback = self.progress_callback if aggregation: multi_exposure_if.use_selected_features_only = ( self.use_selected_only) multi_exposure_if.aggregation = aggregation else: multi_exposure_if.crs = ( self.iface.mapCanvas().mapSettings().destinationCrs()) if len(self.ordered_expected_layers()) != 0: self._multi_exposure_if.output_layers_ordered = ( self.ordered_expected_layers()) status, message = multi_exposure_if.prepare() if status == PREPARE_SUCCESS: self._multi_exposure_if = multi_exposure_if self.btn_run.setEnabled(True) send_static_message(self, ready_message()) self.list_layers_in_map_report.clear() return else: disable_busy_cursor() send_error_message(self, message) self._multi_exposure_if = None 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.info( tr('The impact function could not run because of the inputs.' )) send_error_message(self, message) LOGGER.info(message.to_text()) disable_busy_cursor() self.set_enabled_buttons(True) return code, message elif code == ANALYSIS_FAILED_BAD_CODE: LOGGER.exception( 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.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) # 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.setVisible(Qt.Checked) group_analysis.setCustomProperty(MULTI_EXPOSURE_ANALYSIS_FLAG, True) for layer in self._multi_exposure_if.outputs: QgsMapLayerRegistry.instance().addMapLayer(layer, False) layer_node = group_analysis.addLayer(layer) layer_node.setVisible(Qt.Unchecked) # 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.setVisible(Qt.Checked) 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, ) # 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): legend = self.iface.legendInterface() for combo in self.combos_exposures.itervalues(): layer = layer_from_combo(combo) legend.setLayerVisible(layer, 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 reject(self): """Redefinition of the reject method.""" self._populate_reporting_tab() super(MultiExposureDialog, self).reject() def set_enabled_buttons(self, enabled): self.btn_cancel.setEnabled(enabled) self.btn_back.setEnabled(enabled) self.btn_next.setEnabled(enabled) self.btn_run.setEnabled(enabled)
class WizardDialog(QDialog, FORM_CLASS): """Dialog implementation class for the InaSAFE wizard.""" def __init__(self, parent=None, iface=None, dock=None): """Constructor for the dialog. .. note:: In QtDesigner the advanced editor's predefined keywords list should be shown in english always, so when adding entries to cboKeyword, be sure to choose :safe_qgis:`Properties<<` and untick the :safe_qgis:`translatable` property. :param parent: Parent widget of this dialog. :type parent: QWidget :param iface: QGIS QGisAppInterface instance. :type iface: QGisAppInterface :param dock: Dock widget instance that we can notify of changes to the keywords. Optional. :type dock: Dock """ QDialog.__init__(self, parent) self.setupUi(self) self.setWindowTitle('InaSAFE') # Constants self.keyword_creation_wizard_name = tr( 'InaSAFE Keywords Creation Wizard') self.ifcw_name = tr('InaSAFE Impact Function Centric Wizard') # Note the keys should remain untranslated as we need to write # english to the keywords file. # Save reference to the QGIS interface and parent self.iface = iface self.parent = parent self.dock = dock self.suppress_warning_dialog = False self.lblStep.clear() # Set icons self.lblMainIcon.setPixmap( QPixmap(resources_path('img', 'icons', 'icon-white.svg'))) self.keyword_io = KeywordIO() self.is_selected_layer_keywordless = False self.parent_step = None self.pbnBack.setEnabled(False) self.pbnNext.setEnabled(False) self.pbnCancel.released.connect(self.reject) # Initialize attributes self.existing_keywords = None self.layer = None self.hazard_layer = None self.exposure_layer = None self.aggregation_layer = None self.step_kw_purpose = StepKwPurpose(self) self.step_kw_subcategory = StepKwSubcategory(self) self.step_kw_hazard_category = StepKwHazardCategory(self) self.step_kw_band_selector = StepKwBandSelector(self) self.step_kw_layermode = StepKwLayerMode(self) self.step_kw_unit = StepKwUnit(self) self.step_kw_classification = StepKwClassification(self) self.step_kw_field = StepKwField(self) self.step_kw_multi_classifications = StepKwMultiClassifications(self) self.step_kw_classify = StepKwClassify(self) self.step_kw_threshold = StepKwThreshold(self) self.step_kw_fields_mapping = StepKwFieldsMapping(self) self.step_kw_inasafe_fields = StepKwInaSAFEFields(self) self.step_kw_default_inasafe_fields = StepKwDefaultInaSAFEFields(self) self.step_kw_inasafe_raster_default_values = \ StepKwInaSAFERasterDefaultValues(self) self.step_kw_source = StepKwSource(self) self.step_kw_title = StepKwTitle(self) self.step_kw_summary = StepKwSummary(self) self.step_fc_functions1 = StepFcFunctions1(self) self.step_fc_functions2 = StepFcFunctions2(self) self.step_fc_hazlayer_origin = StepFcHazLayerOrigin(self) self.step_fc_hazlayer_from_canvas = StepFcHazLayerFromCanvas(self) self.step_fc_hazlayer_from_browser = StepFcHazLayerFromBrowser(self) self.step_fc_explayer_origin = StepFcExpLayerOrigin(self) self.step_fc_explayer_from_canvas = StepFcExpLayerFromCanvas(self) self.step_fc_explayer_from_browser = StepFcExpLayerFromBrowser(self) self.step_fc_disjoint_layers = StepFcDisjointLayers(self) self.step_fc_agglayer_origin = StepFcAggLayerOrigin(self) self.step_fc_agglayer_from_canvas = StepFcAggLayerFromCanvas(self) self.step_fc_agglayer_from_browser = StepFcAggLayerFromBrowser(self) self.step_fc_agglayer_disjoint = StepFcAggLayerDisjoint(self) self.step_fc_extent = StepFcExtent(self) self.step_fc_extent_disjoint = StepFcExtentDisjoint(self) self.step_fc_summary = StepFcSummary(self) self.step_fc_analysis = StepFcAnalysis(self) self.wizard_help = WizardHelp(self) self.stackedWidget.addWidget(self.step_kw_purpose) self.stackedWidget.addWidget(self.step_kw_subcategory) self.stackedWidget.addWidget(self.step_kw_hazard_category) self.stackedWidget.addWidget(self.step_kw_band_selector) self.stackedWidget.addWidget(self.step_kw_layermode) self.stackedWidget.addWidget(self.step_kw_unit) self.stackedWidget.addWidget(self.step_kw_classification) self.stackedWidget.addWidget(self.step_kw_field) self.stackedWidget.addWidget(self.step_kw_multi_classifications) self.stackedWidget.addWidget(self.step_kw_classify) self.stackedWidget.addWidget(self.step_kw_threshold) self.stackedWidget.addWidget(self.step_kw_fields_mapping) self.stackedWidget.addWidget(self.step_kw_inasafe_fields) self.stackedWidget.addWidget(self.step_kw_default_inasafe_fields) self.stackedWidget.addWidget( self.step_kw_inasafe_raster_default_values) self.stackedWidget.addWidget(self.step_kw_source) self.stackedWidget.addWidget(self.step_kw_title) self.stackedWidget.addWidget(self.step_kw_summary) self.stackedWidget.addWidget(self.step_fc_functions1) self.stackedWidget.addWidget(self.step_fc_functions2) self.stackedWidget.addWidget(self.step_fc_hazlayer_origin) self.stackedWidget.addWidget(self.step_fc_hazlayer_from_canvas) self.stackedWidget.addWidget(self.step_fc_hazlayer_from_browser) self.stackedWidget.addWidget(self.step_fc_explayer_origin) self.stackedWidget.addWidget(self.step_fc_explayer_from_canvas) self.stackedWidget.addWidget(self.step_fc_explayer_from_browser) self.stackedWidget.addWidget(self.step_fc_disjoint_layers) self.stackedWidget.addWidget(self.step_fc_agglayer_origin) self.stackedWidget.addWidget(self.step_fc_agglayer_from_canvas) self.stackedWidget.addWidget(self.step_fc_agglayer_from_browser) self.stackedWidget.addWidget(self.step_fc_agglayer_disjoint) self.stackedWidget.addWidget(self.step_fc_extent) self.stackedWidget.addWidget(self.step_fc_extent_disjoint) self.stackedWidget.addWidget(self.step_fc_summary) self.stackedWidget.addWidget(self.step_fc_analysis) self.stackedWidget.addWidget(self.wizard_help) # QSetting self.setting = QSettings() # Wizard Steps self.impact_function_steps = [] self.keyword_steps = [] self.on_help = False def set_mode_label_to_keywords_creation(self): """Set the mode label to the Keywords Creation/Update mode.""" self.setWindowTitle(self.keyword_creation_wizard_name) if self.get_existing_keyword('layer_purpose'): mode_name = tr( 'Keywords update wizard for layer <b>{layer_name}</b>').format( layer_name=self.layer.name()) else: mode_name = tr('Keywords creation wizard for layer <b>%s</b>' ).format(layer_name=self.layer.name()) self.lblSubtitle.setText(mode_name) def set_mode_label_to_ifcw(self): """Set the mode label to the IFCW.""" self.setWindowTitle(self.ifcw_name) self.lblSubtitle.setText( tr('Use this wizard to run a guided impact assessment')) def set_keywords_creation_mode(self, layer=None, keywords=None): """Set the Wizard to the Keywords Creation mode. :param layer: Layer to set the keywords for :type layer: QgsMapLayer :param keywords: Keywords for the layer. :type keywords: dict, None """ self.layer = layer or self.iface.mapCanvas().currentLayer() if keywords is not None: self.existing_keywords = keywords else: # Always read from metadata file. try: self.existing_keywords = self.keyword_io.read_keywords( self.layer) except (HashNotFoundError, OperationalError, NoKeywordsFoundError, KeywordNotFoundError, InvalidParameterError, UnsupportedProviderError, MetadataReadError): self.existing_keywords = None self.set_mode_label_to_keywords_creation() step = self.step_kw_purpose step.set_widgets() self.go_to_step(step) def set_function_centric_mode(self): """Set the Wizard to the Function Centric mode.""" self.set_mode_label_to_ifcw() step = self.step_fc_functions1 step.set_widgets() self.go_to_step(step) def field_keyword_for_the_layer(self): """Return the proper keyword for field for the current layer. :returns: the field keyword :rtype: str """ layer_purpose_key = self.step_kw_purpose.selected_purpose()['key'] if layer_purpose_key == layer_purpose_aggregation['key']: return get_compulsory_fields(layer_purpose_key)['key'] elif layer_purpose_key in [ layer_purpose_exposure['key'], layer_purpose_hazard['key'] ]: layer_subcategory_key = \ self.step_kw_subcategory.selected_subcategory()['key'] return get_compulsory_fields(layer_purpose_key, layer_subcategory_key)['key'] else: raise InvalidParameterError def get_parent_mode_constraints(self): """Return the category and subcategory keys to be set in the subordinate mode. :returns: (the category definition, the hazard/exposure definition) :rtype: (dict, dict) """ h, e, _hc, _ec = self.selected_impact_function_constraints() if self.parent_step in [ self.step_fc_hazlayer_from_canvas, self.step_fc_hazlayer_from_browser ]: category = layer_purpose_hazard subcategory = h elif self.parent_step in [ self.step_fc_explayer_from_canvas, self.step_fc_explayer_from_browser ]: category = layer_purpose_exposure subcategory = e elif self.parent_step: category = layer_purpose_aggregation subcategory = None else: category = None subcategory = None return category, subcategory def selected_impact_function_constraints(self): """Obtain impact function constraints selected by user. :returns: Tuple of metadata of hazard, exposure, hazard layer geometry and exposure layer geometry :rtype: tuple """ hazard = self.step_fc_functions1.selected_value( layer_purpose_hazard['key']) exposure = self.step_fc_functions1.selected_value( layer_purpose_exposure['key']) hazard_geometry = self.step_fc_functions2.selected_value( layer_purpose_hazard['key']) exposure_geometry = self.step_fc_functions2.selected_value( layer_purpose_exposure['key']) return hazard, exposure, hazard_geometry, exposure_geometry def is_layer_compatible(self, layer, layer_purpose=None, keywords=None): """Validate if a given layer is compatible for selected IF as a given layer_purpose :param layer: The layer to be validated :type layer: QgsVectorLayer | QgsRasterLayer :param layer_purpose: The layer_purpose the layer is validated for :type layer_purpose: None, string :param keywords: The layer keywords :type keywords: None, dict :returns: True if layer is appropriate for the selected role :rtype: boolean """ # If not explicitly stated, find the desired purpose # from the parent step if not layer_purpose: layer_purpose = self.get_parent_mode_constraints()[0]['key'] # If not explicitly stated, read the layer's keywords if not keywords: 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): keywords = None # Get allowed subcategory and layer_geometry from IF constraints h, e, hc, ec = self.selected_impact_function_constraints() if layer_purpose == 'hazard': subcategory = h['key'] layer_geometry = hc['key'] elif layer_purpose == 'exposure': subcategory = e['key'] layer_geometry = ec['key'] else: # For aggregation layers, use a simplified test and return if (keywords and 'layer_purpose' in keywords and keywords['layer_purpose'] == layer_purpose): return True if not keywords and is_polygon_layer(layer): return True return False # Compare layer properties with explicitly set constraints # Reject if layer geometry doesn't match if layer_geometry != self.get_layer_geometry_key(layer): return False # If no keywords, there's nothing more we can check. # The same if the keywords version doesn't match if not keywords or 'keyword_version' not in keywords: return True keyword_version = str(keywords['keyword_version']) if not is_keyword_version_supported(keyword_version): return True # Compare layer keywords with explicitly set constraints # Reject if layer purpose missing or doesn't match if ('layer_purpose' not in keywords or keywords['layer_purpose'] != layer_purpose): return False # Reject if layer subcategory doesn't match if (layer_purpose in keywords and keywords[layer_purpose] != subcategory): return False return True def get_compatible_canvas_layers(self, category): """Collect layers from map canvas, compatible for the given category and selected impact function .. note:: Returns layers with keywords and layermode matching the category and compatible with the selected impact function. Also returns layers without keywords with layermode compatible with the selected impact function. :param category: The category to filter for. :type category: string :returns: Metadata of found layers. :rtype: list of dicts """ # Collect compatible layers layers = [] for layer in self.iface.mapCanvas().layers(): 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): keywords = None if self.is_layer_compatible(layer, category, keywords): layers += [{ 'id': layer.id(), 'name': layer.name(), 'keywords': keywords }] # Move layers without keywords to the end l1 = [l for l in layers if l['keywords']] l2 = [l for l in layers if not l['keywords']] layers = l1 + l2 return layers def get_layer_geometry_key(self, layer=None): """Obtain layer mode of a given layer. If no layer specified, the current layer is used :param layer : layer to examine :type layer: QgsMapLayer or None :returns: The layer mode. :rtype: str """ if not layer: layer = self.layer if is_raster_layer(layer): return layer_geometry_raster['key'] elif is_point_layer(layer): return layer_geometry_point['key'] elif is_polygon_layer(layer): return layer_geometry_polygon['key'] else: return layer_geometry_line['key'] def get_existing_keyword(self, keyword): """Obtain an existing keyword's value. :param keyword: A keyword from keywords. :type keyword: str :returns: The value of the keyword. :rtype: str, QUrl """ if self.existing_keywords is None: return {} if keyword is not None: return self.existing_keywords.get(keyword, {}) else: return {} def get_layer_description_from_canvas(self, layer, purpose): """Obtain the description of a canvas layer selected by user. :param layer: The QGIS layer. :type layer: QgsMapLayer :param purpose: The layer purpose of the layer to get the description. :type purpose: string :returns: description of the selected layer. :rtype: string """ if not layer: return "" try: keywords = self.keyword_io.read_keywords(layer) if 'layer_purpose' not in keywords: keywords = None except (HashNotFoundError, OperationalError, NoKeywordsFoundError, KeywordNotFoundError, InvalidParameterError, UnsupportedProviderError): keywords = None # set the current layer (e.g. for the keyword creation sub-thread) self.layer = layer if purpose == layer_purpose_hazard['key']: self.hazard_layer = layer elif purpose == layer_purpose_exposure['key']: self.exposure_layer = layer else: self.aggregation_layer = layer # Check if the layer is keywordless if keywords and 'keyword_version' in keywords: kw_ver = str(keywords['keyword_version']) self.is_selected_layer_keywordless = ( not is_keyword_version_supported(kw_ver)) else: self.is_selected_layer_keywordless = True description = layer_description_html(layer, keywords) return description # =========================== # NAVIGATION # =========================== def go_to_step(self, step): """Set the stacked widget to the given step, set up the buttons, and run all operations that should start immediately after entering the new step. :param step: The step widget to be moved to. :type step: WizardStep """ self.stackedWidget.setCurrentWidget(step) # Disable the Next button unless new data already entered self.pbnNext.setEnabled(step.is_ready_to_next_step()) # Enable the Back button unless it's not the first step self.pbnBack.setEnabled( step not in [self.step_kw_purpose, self.step_fc_functions1] or self.parent_step is not None) # Set Next button label if (step in [self.step_kw_summary, self.step_fc_analysis] and self.parent_step is None): self.pbnNext.setText(tr('Finish')) elif step == self.step_fc_summary: self.pbnNext.setText(tr('Run')) else: self.pbnNext.setText(tr('Next')) # Run analysis after switching to the new step if step == self.step_fc_analysis: self.step_fc_analysis.setup_and_run_analysis() # Set lblSelectCategory label if entering the kw mode # from the ifcw mode if step == self.step_kw_purpose and self.parent_step: if self.parent_step in [ self.step_fc_hazlayer_from_canvas, self.step_fc_hazlayer_from_browser ]: text_label = category_question_hazard elif self.parent_step in [ self.step_fc_explayer_from_canvas, self.step_fc_explayer_from_browser ]: text_label = category_question_exposure else: text_label = category_question_aggregation self.step_kw_purpose.lblSelectCategory.setText(text_label) # prevents actions being handled twice # noinspection PyPep8Naming @pyqtSignature('') def on_pbnNext_released(self): """Handle the Next button release. .. note:: This is an automatic Qt slot executed when the Next button is released. """ current_step = self.get_current_step() if current_step == self.step_kw_fields_mapping: try: self.step_kw_fields_mapping.get_field_mapping() except InvalidValidationException as e: display_warning_message_box(self, tr('Invalid Field Mapping'), get_string(e.message)) return if current_step.step_type == STEP_FC: self.impact_function_steps.append(current_step) elif current_step.step_type == STEP_KW: self.keyword_steps.append(current_step) else: LOGGER.debug(current_step.step_type) raise InvalidWizardStep # Save keywords if it's the end of the keyword creation mode if current_step == self.step_kw_summary: self.save_current_keywords() # After any step involving Browser, add selected layer to map canvas if current_step in [ self.step_fc_hazlayer_from_browser, self.step_fc_explayer_from_browser, self.step_fc_agglayer_from_browser ]: if not QgsMapLayerRegistry.instance().mapLayersByName( self.layer.name()): QgsMapLayerRegistry.instance().addMapLayers([self.layer]) # Make the layer visible. Might be hidden by default. See #2925 legend = self.iface.legendInterface() legend.setLayerVisible(self.layer, True) # After the extent selection, save the extent and disconnect signals if current_step == self.step_fc_extent: self.step_fc_extent.write_extent() # Determine the new step to be switched new_step = current_step.get_next_step() if new_step is not None: # Prepare the next tab new_step.set_widgets() else: # Wizard complete self.accept() return self.go_to_step(new_step) # prevents actions being handled twice # noinspection PyPep8Naming @pyqtSignature('') def on_pbnBack_released(self): """Handle the Back button release. .. note:: This is an automatic Qt slot executed when the Back button is released. """ current_step = self.get_current_step() if current_step.step_type == STEP_FC: new_step = self.impact_function_steps.pop() elif current_step.step_type == STEP_KW: try: new_step = self.keyword_steps.pop() except IndexError: new_step = self.impact_function_steps.pop() else: raise InvalidWizardStep # set focus to table widgets, as the inactive selection style is gray if new_step == self.step_fc_functions1: self.step_fc_functions1.tblFunctions1.setFocus() if new_step == self.step_fc_functions2: self.step_fc_functions2.tblFunctions2.setFocus() # Re-connect disconnected signals when coming back to the Extent step if new_step == self.step_fc_extent: self.step_fc_extent.set_widgets() # Set Next button label self.pbnNext.setText(tr('Next')) self.pbnNext.setEnabled(True) self.go_to_step(new_step) # prevents actions being handled twice # noinspection PyPep8Naming @pyqtSignature('') def on_pbnHelp_released(self): if self.on_help: self.pbnHelp.setText(tr('Show help')) self.wizard_help.restore_button_state() self.stackedWidget.setCurrentWidget(self.wizard_help.wizard_step) else: self.pbnHelp.setText(tr('Hide help')) self.wizard_help.show_help(self.get_current_step()) self.stackedWidget.setCurrentWidget(self.wizard_help) self.on_help = not self.on_help def get_current_step(self): """Return current step of the wizard. :returns: Current step of the wizard. :rtype: WizardStep instance """ return self.stackedWidget.currentWidget() def get_keywords(self): """Obtain the state of the dialog as a keywords dict. :returns: Keywords reflecting the state of the dialog. :rtype: dict """ keywords = {} inasafe_fields = {} keywords['layer_geometry'] = self.get_layer_geometry_key() if self.step_kw_purpose.selected_purpose(): keywords['layer_purpose'] = self.step_kw_purpose.\ selected_purpose()['key'] if self.step_kw_subcategory.selected_subcategory(): key = self.step_kw_purpose.selected_purpose()['key'] keywords[key] = self.step_kw_subcategory.\ selected_subcategory()['key'] if self.get_layer_geometry_key() == layer_geometry_raster['key']: if self.step_kw_band_selector.selected_band(): keywords['active_band'] = self.step_kw_band_selector.\ selected_band() if keywords['layer_purpose'] == layer_purpose_hazard['key']: if self.step_kw_hazard_category.selected_hazard_category(): keywords['hazard_category'] \ = self.step_kw_hazard_category.\ selected_hazard_category()['key'] if self.step_kw_layermode.selected_layermode(): keywords['layer_mode'] = self.step_kw_layermode.\ selected_layermode()['key'] if self.step_kw_unit.selected_unit(): if self.step_kw_purpose.selected_purpose() == layer_purpose_hazard: key = continuous_hazard_unit['key'] else: key = exposure_unit['key'] keywords[key] = self.step_kw_unit.selected_unit()['key'] if self.step_kw_field.selected_fields(): field_key = self.field_keyword_for_the_layer() inasafe_fields[field_key] = self.step_kw_field.selected_fields() if self.step_kw_classification.selected_classification(): keywords['classification'] = self.step_kw_classification.\ selected_classification()['key'] if keywords['layer_purpose'] == layer_purpose_hazard['key']: multi_classifications = self.step_kw_multi_classifications.\ get_current_state() value_maps = multi_classifications.get('value_maps') if value_maps is not None: keywords['value_maps'] = value_maps thresholds = multi_classifications.get('thresholds') if thresholds is not None: keywords['thresholds'] = thresholds else: if self.step_kw_layermode.selected_layermode(): layer_mode = self.step_kw_layermode.selected_layermode() if layer_mode == layer_mode_continuous: thresholds = self.step_kw_threshold.get_threshold() if thresholds: keywords['thresholds'] = thresholds elif layer_mode == layer_mode_classified: value_map = self.step_kw_classify.selected_mapping() if value_map: keywords['value_map'] = value_map if self.step_kw_source.leSource.text(): keywords['source'] = get_unicode( self.step_kw_source.leSource.text()) if self.step_kw_source.leSource_url.text(): keywords['url'] = get_unicode( self.step_kw_source.leSource_url.text()) if self.step_kw_source.leSource_scale.text(): keywords['scale'] = get_unicode( self.step_kw_source.leSource_scale.text()) if self.step_kw_source.ckbSource_date.isChecked(): keywords['date'] = self.step_kw_source.dtSource_date.dateTime() if self.step_kw_source.leSource_license.text(): keywords['license'] = get_unicode( self.step_kw_source.leSource_license.text()) if self.step_kw_title.leTitle.text(): keywords['title'] = get_unicode(self.step_kw_title.leTitle.text()) inasafe_fields.update(self.step_kw_inasafe_fields.get_inasafe_fields()) inasafe_fields.update( self.step_kw_default_inasafe_fields.get_inasafe_fields()) inasafe_fields.update( self.step_kw_fields_mapping.get_field_mapping()['fields']) if inasafe_fields: keywords['inasafe_fields'] = inasafe_fields inasafe_default_values = {} if keywords['layer_geometry'] == layer_geometry_raster['key']: pass # Notes(IS): Skipped assigning raster inasafe default value for # now. # inasafe_default_values = self.\ # step_kw_inasafe_raster_default_values.\ # get_inasafe_default_values() else: inasafe_default_values.update(self.step_kw_default_inasafe_fields. get_inasafe_default_values()) inasafe_default_values.update( self.step_kw_fields_mapping.get_field_mapping()['values']) if inasafe_default_values: keywords['inasafe_default_values'] = inasafe_default_values return keywords def save_current_keywords(self): """Save keywords to the layer. It will write out the keywords for the current layer. This method is based on the KeywordsDialog class. """ current_keywords = self.get_keywords() try: self.keyword_io.write_keywords(layer=self.layer, keywords=current_keywords) except InaSAFEError, e: error_message = get_error_message(e) # noinspection PyCallByClass,PyTypeChecker,PyArgumentList QtGui.QMessageBox.warning( self, tr('InaSAFE'), tr('An error was encountered when saving the following ' 'keywords:\n {error_message}').format( error_message=error_message.to_html())) if self.dock is not None: # noinspection PyUnresolvedReferences self.dock.get_layers() # Save default value to QSetting if current_keywords.get('inasafe_default_values'): for key, value in ( current_keywords['inasafe_default_values'].items()): set_inasafe_default_value_qsetting(self.setting, RECENT, key, value)
def __init__(self, parent=None, iface=None, dock=None): """Constructor for the dialog. .. note:: In QtDesigner the advanced editor's predefined keywords list should be shown in english always, so when adding entries to cboKeyword, be sure to choose :safe_qgis:`Properties<<` and untick the :safe_qgis:`translatable` property. :param parent: Parent widget of this dialog. :type parent: QWidget :param iface: QGIS QGisAppInterface instance. :type iface: QGisAppInterface :param dock: Dock widget instance that we can notify of changes to the keywords. Optional. :type dock: Dock """ QDialog.__init__(self, parent) self.setupUi(self) self.setWindowTitle('InaSAFE') # Constants self.keyword_creation_wizard_name = tr( 'InaSAFE Keywords Creation Wizard') self.ifcw_name = tr('InaSAFE Impact Function Centric Wizard') # Note the keys should remain untranslated as we need to write # english to the keywords file. # Save reference to the QGIS interface and parent self.iface = iface self.parent = parent self.dock = dock self.suppress_warning_dialog = False self.lblStep.clear() # Set icons self.lblMainIcon.setPixmap( QPixmap(resources_path('img', 'icons', 'icon-white.svg'))) self.keyword_io = KeywordIO() self.is_selected_layer_keywordless = False self.parent_step = None self.pbnBack.setEnabled(False) self.pbnNext.setEnabled(False) self.pbnCancel.released.connect(self.reject) # Initialize attributes self.existing_keywords = None self.layer = None self.hazard_layer = None self.exposure_layer = None self.aggregation_layer = None self.step_kw_purpose = StepKwPurpose(self) self.step_kw_subcategory = StepKwSubcategory(self) self.step_kw_hazard_category = StepKwHazardCategory(self) self.step_kw_band_selector = StepKwBandSelector(self) self.step_kw_layermode = StepKwLayerMode(self) self.step_kw_unit = StepKwUnit(self) self.step_kw_classification = StepKwClassification(self) self.step_kw_field = StepKwField(self) self.step_kw_multi_classifications = StepKwMultiClassifications(self) self.step_kw_classify = StepKwClassify(self) self.step_kw_threshold = StepKwThreshold(self) self.step_kw_fields_mapping = StepKwFieldsMapping(self) self.step_kw_inasafe_fields = StepKwInaSAFEFields(self) self.step_kw_default_inasafe_fields = StepKwDefaultInaSAFEFields(self) self.step_kw_inasafe_raster_default_values = \ StepKwInaSAFERasterDefaultValues(self) self.step_kw_source = StepKwSource(self) self.step_kw_title = StepKwTitle(self) self.step_kw_summary = StepKwSummary(self) self.step_fc_functions1 = StepFcFunctions1(self) self.step_fc_functions2 = StepFcFunctions2(self) self.step_fc_hazlayer_origin = StepFcHazLayerOrigin(self) self.step_fc_hazlayer_from_canvas = StepFcHazLayerFromCanvas(self) self.step_fc_hazlayer_from_browser = StepFcHazLayerFromBrowser(self) self.step_fc_explayer_origin = StepFcExpLayerOrigin(self) self.step_fc_explayer_from_canvas = StepFcExpLayerFromCanvas(self) self.step_fc_explayer_from_browser = StepFcExpLayerFromBrowser(self) self.step_fc_disjoint_layers = StepFcDisjointLayers(self) self.step_fc_agglayer_origin = StepFcAggLayerOrigin(self) self.step_fc_agglayer_from_canvas = StepFcAggLayerFromCanvas(self) self.step_fc_agglayer_from_browser = StepFcAggLayerFromBrowser(self) self.step_fc_agglayer_disjoint = StepFcAggLayerDisjoint(self) self.step_fc_extent = StepFcExtent(self) self.step_fc_extent_disjoint = StepFcExtentDisjoint(self) self.step_fc_summary = StepFcSummary(self) self.step_fc_analysis = StepFcAnalysis(self) self.wizard_help = WizardHelp(self) self.stackedWidget.addWidget(self.step_kw_purpose) self.stackedWidget.addWidget(self.step_kw_subcategory) self.stackedWidget.addWidget(self.step_kw_hazard_category) self.stackedWidget.addWidget(self.step_kw_band_selector) self.stackedWidget.addWidget(self.step_kw_layermode) self.stackedWidget.addWidget(self.step_kw_unit) self.stackedWidget.addWidget(self.step_kw_classification) self.stackedWidget.addWidget(self.step_kw_field) self.stackedWidget.addWidget(self.step_kw_multi_classifications) self.stackedWidget.addWidget(self.step_kw_classify) self.stackedWidget.addWidget(self.step_kw_threshold) self.stackedWidget.addWidget(self.step_kw_fields_mapping) self.stackedWidget.addWidget(self.step_kw_inasafe_fields) self.stackedWidget.addWidget(self.step_kw_default_inasafe_fields) self.stackedWidget.addWidget( self.step_kw_inasafe_raster_default_values) self.stackedWidget.addWidget(self.step_kw_source) self.stackedWidget.addWidget(self.step_kw_title) self.stackedWidget.addWidget(self.step_kw_summary) self.stackedWidget.addWidget(self.step_fc_functions1) self.stackedWidget.addWidget(self.step_fc_functions2) self.stackedWidget.addWidget(self.step_fc_hazlayer_origin) self.stackedWidget.addWidget(self.step_fc_hazlayer_from_canvas) self.stackedWidget.addWidget(self.step_fc_hazlayer_from_browser) self.stackedWidget.addWidget(self.step_fc_explayer_origin) self.stackedWidget.addWidget(self.step_fc_explayer_from_canvas) self.stackedWidget.addWidget(self.step_fc_explayer_from_browser) self.stackedWidget.addWidget(self.step_fc_disjoint_layers) self.stackedWidget.addWidget(self.step_fc_agglayer_origin) self.stackedWidget.addWidget(self.step_fc_agglayer_from_canvas) self.stackedWidget.addWidget(self.step_fc_agglayer_from_browser) self.stackedWidget.addWidget(self.step_fc_agglayer_disjoint) self.stackedWidget.addWidget(self.step_fc_extent) self.stackedWidget.addWidget(self.step_fc_extent_disjoint) self.stackedWidget.addWidget(self.step_fc_summary) self.stackedWidget.addWidget(self.step_fc_analysis) self.stackedWidget.addWidget(self.wizard_help) # QSetting self.setting = QSettings() # Wizard Steps self.impact_function_steps = [] self.keyword_steps = [] self.on_help = False
def render_nearby_table(self): hazard_mapping = { 0: 'Very Low', 1: 'Low', 2: 'Moderate', 3: 'High', 4: 'Very High' } # load PLACES keyword_io = KeywordIO() try: cities_impact = read_qgis_layer( self.working_dir_path('cities_impact.shp'), 'Cities') hazard = keyword_io.read_keywords(cities_impact, 'target_field') hazard_field_index = cities_impact.fieldNameIndex(hazard) name_field = keyword_io.read_keywords(self.cities_layer, 'name_field') name_field_index = cities_impact.fieldNameIndex(name_field) try: population_field = keyword_io.read_keywords( self.cities_layer, 'population_field') population_field_index = cities_impact.fieldNameIndex( population_field) except KeywordNotFoundError: population_field = None population_field_index = None table_places = [] for f in cities_impact.getFeatures(): haz_class = f.attributes()[hazard_field_index] city_name = f.attributes()[name_field_index] if population_field_index >= 0: city_pop = f.attributes()[population_field_index] else: city_pop = 1 # format: # [ # 'hazard class', # 'city's population', # 'city's name', # 'the type' # ] haz = hazard_mapping[haz_class] item = { 'class': haz_class, 'hazard': haz, 'css': haz.lower().replace(' ', '-'), 'population': format_int(population_rounding(city_pop / 1000)), 'name': city_name.title(), 'type': 'places' } table_places.append(item) # sort table by hazard zone, then population table_places = sorted(table_places, key=lambda x: (-x['class'], -x['population'])) except Exception as e: LOGGER.exception(e) table_places = [] # load AIRPORTS try: airport_impact = read_qgis_layer( self.working_dir_path('airport_impact.shp'), 'Airport') hazard = keyword_io.read_keywords(airport_impact, 'target_field') hazard_field_index = airport_impact.fieldNameIndex(hazard) name_field = keyword_io.read_keywords(self.airport_layer, 'name_field') name_field_index = airport_impact.fieldNameIndex(name_field) # airport doesnt have population, so enter 0 for population table_airports = [] for f in airport_impact.getFeatures(): haz_class = f.attributes()[hazard_field_index] airport_name = f.attributes()[name_field_index] haz = hazard_mapping[haz_class] item = { 'class': haz_class, 'hazard': haz, 'css': haz.lower().replace(' ', '-'), 'population': 0, 'name': airport_name.title(), 'type': 'airport' } table_airports.append(item) # Sort by hazard class table_airports = sorted(table_airports, key=lambda x: -x['class']) except Exception as e: LOGGER.exception(e) table_airports = [] # decide which to show # maximum 2 airport max_airports = 2 airport_count = min(max_airports, len(table_airports)) # maximum total 7 entries to show max_rows = 6 places_count = min(len(table_places), max_rows - airport_count) # get top airport table_airports = table_airports[:airport_count] # get top places table_places = table_places[:places_count] item_list = table_places + table_airports # sort entry by hazard level item_list = sorted(item_list, key=lambda x: (-x['class'], -x['population'])) nearby_template = self.ash_fixtures_dir('nearby-table.template.html') with open(nearby_template) as f: template = Template(f.read()) # generate table here html_string = template.render(item_list=item_list) with open(self.nearby_html_path, 'w') as f: f.write(html_string) # copy airport logo shutil.copy(self.ash_fixtures_dir('logo/airport.jpg'), self.working_dir_path('airport.jpg'))
class ImpactReport(object): """A class for creating and generating report. .. versionadded:: 4.0 """ # constant for default PAGE_DPI settings DEFAULT_PAGE_DPI = 300 REPORT_GENERATION_SUCCESS = 0 REPORT_GENERATION_FAILED = 1 class LayerException(Exception): """Class for Layer Exception. Raised if layer being used is not valid. """ pass def __init__( self, iface, template_metadata, impact_function=None, hazard=None, exposure=None, impact=None, analysis=None, exposure_summary_table=None, aggregation_summary=None, extra_layers=None, minimum_needs_profile=None): """Constructor for the Composition Report class. :param iface: Reference to the QGIS iface object. :type iface: QgsAppInterface :param template_metadata: InaSAFE template metadata. :type template_metadata: ReportMetadata :param impact_function: Impact function instance for the report :type impact_function: safe.impact_function.impact_function.ImpactFunction .. versionadded:: 4.0 """ LOGGER.debug('InaSAFE Impact Report class initialised') self._iface = iface self._metadata = template_metadata self._output_folder = None self._impact_function = impact_function self._hazard = hazard or self._impact_function.hazard self._exposure = ( exposure or self._impact_function.exposure) self._impact = ( impact or self._impact_function.impact) self._analysis = (analysis or self._impact_function.analysis_impacted) self._exposure_summary_table = ( exposure_summary_table or self._impact_function.exposure_summary_table) self._aggregation_summary = ( aggregation_summary or self._impact_function.aggregation_summary) if extra_layers is None: extra_layers = [] self._extra_layers = extra_layers self._minimum_needs = minimum_needs_profile self._extent = self._iface.mapCanvas().extent() self._inasafe_context = InaSAFEReportContext() # QgsMapSettings is added in 2.4 map_settings = self._iface.mapCanvas().mapSettings() self._qgis_composition_context = QGISCompositionContext( None, map_settings, ImpactReport.DEFAULT_PAGE_DPI) self._keyword_io = KeywordIO() @property def inasafe_context(self): """Reference to default InaSAFE Context. :rtype: InaSAFEReportContext """ return self._inasafe_context @property def qgis_composition_context(self): """Reference to default QGIS Composition Context. :rtype: QGISCompositionContext """ return self._qgis_composition_context @property def metadata(self): """Getter to the template. :return: ReportMetadata :rtype: safe.report.report_metadata.ReportMetadata """ return self._metadata @property def output_folder(self): """Output folder path for the rendering. :rtype: str """ return self._output_folder @output_folder.setter def output_folder(self, value): """Output folder path for the rendering. :param value: output folder path :type value: str """ self._output_folder = value if not os.path.exists(self._output_folder): os.makedirs(self._output_folder) @staticmethod def absolute_output_path( output_folder, components, component_key): """Return absolute output path of component. :param output_folder: The base output folder :type output_folder: str :param components: The list of components to look up :type components: list[ReportMetadata] :param component_key: The component key :type component_key: str :return: absolute output path :rtype: str .. versionadded:: 4.0 """ comp_keys = [c.key for c in components] if component_key in comp_keys: idx = comp_keys.index(component_key) output_path = components[idx].output_path if isinstance(output_path, str): return os.path.abspath( os.path.join(output_folder, output_path)) elif isinstance(output_path, list): output_list = [] for path in output_path: output_list.append(os.path.abspath( os.path.join(output_folder, path))) return output_list elif isinstance(output_path, dict): output_dict = {} for key, path in output_path.iteritems(): output_dict[key] = os.path.abspath( os.path.join(output_folder, path)) return output_dict return None def component_absolute_output_path(self, component_key): """Return absolute output path of component. :param component_key: The component key :type component_key: str :return: absolute output path :rtype: str .. versionadded:: 4.0 """ return ImpactReport.absolute_output_path( self.output_folder, self.metadata.components, component_key) @property def impact_function(self): """Getter for impact function instance to use. :rtype: safe.impact_function.impact_function.ImpactFunction """ return self._impact_function def _check_layer_count(self, layer): """Check for the validity of the layer. :param layer: QGIS layer :type layer: qgis.core.QgsVectorLayer :return: """ if layer: if not layer.isValid(): raise ImpactReport.LayerException('Layer is not valid') if isinstance(layer, QgsRasterLayer): # can't check feature count of raster layer return feature_count = len([f for f in layer.getFeatures()]) if feature_count == 0: raise ImpactReport.LayerException( 'Layer contains no features') @property def hazard(self): """Getter to hazard layer. :rtype: qgis.core.QgsVectorLayer """ self._check_layer_count(self._hazard) return self._hazard @hazard.setter def hazard(self, layer): """Hazard layer. :param layer: hazard layer :type layer: qgis.core.QgsVectorLayer """ self._hazard = layer @property def exposure(self): """Getter to exposure layer. :rtype: qgis.core.QgsVectorLayer """ self._check_layer_count(self._exposure) return self._exposure @exposure.setter def exposure(self, layer): """Exposure layer. :param layer: exposure layer :type layer: qgis.core.QgsVectorLayer """ self._impact = layer @property def impact(self): """Getter to layer that will be used for stats, legend, reporting. :rtype: qgis.core.QgsVectorLayer """ self._check_layer_count(self._impact) return self._impact @impact.setter def impact(self, layer): """Set the layer that will be used for stats, legend and reporting. :param layer: Layer that will be used for stats, legend and reporting. :type layer: qgis.core.QgsVectorLayer """ self._impact = layer @property def analysis(self): """Analysis layer. :rtype: qgis.core.QgsVectorLayer """ self._check_layer_count(self._analysis) return self._analysis @analysis.setter def analysis(self, layer): """Analysis layer. :param layer: Analysis layer :type layer: qgis.core.QgsVectorLayer """ self._analysis = layer @property def exposure_summary_table(self): """Exposure summary table. :rtype: qgis.core.QgsVectorLayer """ # self._check_layer_count(self._exposure_summary_table) return self._exposure_summary_table @exposure_summary_table.setter def exposure_summary_table(self, value): """Exposure summary table. :param value: Exposure Summary Table :type value: qgis.core.QgsVectorLayer :return: """ self._exposure_summary_table = value @property def aggregation_summary(self): """Aggregation summary. :rtype: qgis.core.QgsVectorLayer """ self._check_layer_count(self._aggregation_summary) return self._aggregation_summary @aggregation_summary.setter def aggregation_summary(self, value): """Aggregation summary. :param value: Aggregation Summary :type value: qgis.core.QgsVectorLayer """ self._aggregation_summary = value @property def extra_layers(self): """Getter to extra layers. extra layers will be rendered alongside impact layer """ return self._extra_layers @extra_layers.setter def extra_layers(self, extra_layers): """Set extra layers. extra layers will be rendered alongside impact layer :param extra_layers: List of QgsMapLayer :type extra_layers: list(QgsMapLayer) """ self._extra_layers = extra_layers @property def minimum_needs(self): """Minimum needs. :return: minimum needs used in impact report :rtype: safe.gui.tools.minimum_needs.needs_profile.NeedsProfile """ return self._minimum_needs @minimum_needs.setter def minimum_needs(self, value): """Minimum needs. :param value: minimum needs used in impact report :type value: safe.gui.tools.minimum_needs.needs_profile.NeedsProfile """ self._minimum_needs = value @property def map_title(self): """Get the map title from the layer keywords if possible. :returns: None on error, otherwise the title. :rtype: None, str """ # noinspection PyBroadException try: title = self._keyword_io.read_keywords( self.impact, 'map_title') return title except KeywordNotFoundError: return None except Exception: # pylint: disable=broad-except return None @property def map_legend_attributes(self): """Get the map legend attribute from the layer keywords if possible. :returns: None on error, otherwise the attributes (notes and units). :rtype: None, str """ LOGGER.debug('InaSAFE Map getMapLegendAttributes called') legend_attribute_list = [ 'legend_notes', 'legend_units', 'legend_title'] legend_attribute_dict = {} for legend_attribute in legend_attribute_list: # noinspection PyBroadException try: legend_attribute_dict[legend_attribute] = \ self._keyword_io.read_keywords( self.impact, legend_attribute) except KeywordNotFoundError: pass except Exception: # pylint: disable=broad-except pass return legend_attribute_dict def process_components(self): """Process context for each component and a given template. :returns: Tuple of error code and message :type: tuple .. versionadded:: 4.0 """ message = m.Message() warning_heading = m.Heading( tr('Report Generation issue'), **WARNING_STYLE) message.add(warning_heading) failed_extract_context = m.Heading(tr( 'Failed to extract context'), **WARNING_STYLE) failed_render_context = m.Heading(tr( 'Failed to render context'), **WARNING_STYLE) failed_find_extractor = m.Heading(tr( 'Failed to load extractor method'), **WARNING_STYLE) failed_find_renderer = m.Heading(tr( 'Failed to load renderer method'), **WARNING_STYLE) generation_error_code = self.REPORT_GENERATION_SUCCESS for component in self.metadata.components: # load extractors try: if not component.context: if callable(component.extractor): _extractor_method = component.extractor else: _package_name = ( '%(report-key)s.extractors.%(component-key)s') _package_name %= { 'report-key': self.metadata.key, 'component-key': component.key } # replace dash with underscores _package_name = _package_name.replace('-', '_') _extractor_path = os.path.join( self.metadata.template_folder, component.extractor ) _module = imp.load_source( _package_name, _extractor_path) _extractor_method = getattr(_module, 'extractor') else: LOGGER.info('Predefined context. Extractor not needed.') except Exception as e: # pylint: disable=broad-except generation_error_code = self.REPORT_GENERATION_FAILED LOGGER.info(e) if self.impact_function.debug_mode: raise else: message.add(failed_find_extractor) message.add(component.info) message.add(get_error_message(e)) continue # method signature: # - this ImpactReport # - this component try: if not component.context: context = _extractor_method(self, component) component.context = context else: LOGGER.info('Using predefined context.') except Exception as e: # pylint: disable=broad-except generation_error_code = self.REPORT_GENERATION_FAILED LOGGER.info(e) if self.impact_function.debug_mode: raise else: message.add(failed_extract_context) message.add(get_error_message(e)) continue try: # load processor if callable(component.processor): _renderer = component.processor else: _package_name = '%(report-key)s.renderer.%(component-key)s' _package_name %= { 'report-key': self.metadata.key, 'component-key': component.key } # replace dash with underscores _package_name = _package_name.replace('-', '_') _renderer_path = os.path.join( self.metadata.template_folder, component.processor ) _module = imp.load_source(_package_name, _renderer_path) _renderer = getattr(_module, 'renderer') except Exception as e: # pylint: disable=broad-except generation_error_code = self.REPORT_GENERATION_FAILED LOGGER.info(e) if self.impact_function.debug_mode: raise else: message.add(failed_find_renderer) message.add(component.info) message.add(get_error_message(e)) continue # method signature: # - this ImpactReport # - this component if component.context: try: output = _renderer(self, component) output_path = self.component_absolute_output_path( component.key) if isinstance(output_path, dict): try: dirname = os.path.dirname(output_path.get('doc')) except: dirname = os.path.dirname(output_path.get('map')) else: dirname = os.path.dirname(output_path) if component.resources: for resource in component.resources: target_resource = os.path.basename(resource) target_dir = os.path.join( dirname, 'resources', target_resource) # copy here shutil.copytree(resource, target_dir) component.output = output except Exception as e: # pylint: disable=broad-except generation_error_code = self.REPORT_GENERATION_FAILED LOGGER.info(e) if self.impact_function.debug_mode: raise else: message.add(failed_render_context) message.add(get_error_message(e)) continue return generation_error_code, message
def create_keyword_file(self, algorithm): """Create keyword file for the raster file created. Basically copy a template from keyword file in converter data and add extra keyword (usually a title) :param algorithm: Which re-sampling algorithm to use. valid options are 'nearest' (for nearest neighbour), 'invdist' (for inverse distance), 'average' (for moving average). Defaults to 'nearest' if not specified. Note that passing re-sampling alg parameters is currently not supported. If None is passed it will be replaced with 'nearest'. :type algorithm: str """ keyword_io = KeywordIO() # Set thresholds for each exposure mmi_default_classes = default_classification_thresholds( earthquake_mmi_scale ) mmi_default_threshold = { earthquake_mmi_scale['key']: { 'active': True, 'classes': mmi_default_classes } } generic_default_classes = default_classification_thresholds( generic_hazard_classes ) generic_default_threshold = { generic_hazard_classes['key']: { 'active': True, 'classes': generic_default_classes } } threshold_keyword = {} for exposure in exposure_all: # Not all exposure is supported by earthquake_mmi_scale if exposure in earthquake_mmi_scale['exposures']: threshold_keyword[exposure['key']] = mmi_default_threshold else: threshold_keyword[ exposure['key']] = generic_default_threshold extra_keywords = { extra_keyword_earthquake_latitude['key']: self.latitude, extra_keyword_earthquake_longitude['key']: self.longitude, extra_keyword_earthquake_magnitude['key']: self.magnitude, extra_keyword_earthquake_depth['key']: self.depth, extra_keyword_earthquake_description['key']: self.description, extra_keyword_earthquake_location['key']: self.location, extra_keyword_earthquake_event_time['key']: self.time.strftime( '%Y-%m-%dT%H:%M:%S'), extra_keyword_time_zone['key']: self.time_zone, extra_keyword_earthquake_x_minimum['key']: self.x_minimum, extra_keyword_earthquake_x_maximum['key']: self.x_maximum, extra_keyword_earthquake_y_minimum['key']: self.y_minimum, extra_keyword_earthquake_y_maximum['key']: self.y_maximum, extra_keyword_earthquake_event_id['key']: self.event_id } for key, value in list(self.extra_keywords.items()): extra_keywords[key] = value # Delete empty element. empty_keys = [] for key, value in list(extra_keywords.items()): if value is None: empty_keys.append(key) for empty_key in empty_keys: extra_keywords.pop(empty_key) keywords = { 'hazard': hazard_earthquake['key'], 'hazard_category': hazard_category_single_event['key'], 'keyword_version': inasafe_keyword_version, 'layer_geometry': layer_geometry_raster['key'], 'layer_mode': layer_mode_continuous['key'], 'layer_purpose': layer_purpose_hazard['key'], 'continuous_hazard_unit': unit_mmi['key'], 'classification': earthquake_mmi_scale['key'], 'thresholds': threshold_keyword, 'extra_keywords': extra_keywords, 'active_band': 1 } if self.algorithm_name: layer_path = os.path.join( self.output_dir, '%s-%s.tif' % ( self.output_basename, algorithm)) else: layer_path = os.path.join( self.output_dir, '%s.tif' % self.output_basename) # append title and source to the keywords file if len(self.title.strip()) == 0: keyword_title = self.output_basename else: keyword_title = self.title keywords['title'] = keyword_title hazard_layer = QgsRasterLayer(layer_path, keyword_title) if not hazard_layer.isValid(): raise InvalidLayerError() keyword_io.write_keywords(hazard_layer, keywords)
def __init__(self, parent, iface, dock=None, layer=None): """Constructor for the dialog. .. note:: In QtDesigner the advanced editor's predefined keywords list should be shown in english always, so when adding entries to cboKeyword, be sure to choose :safe_qgis:`Properties<<` and untick the :safe_qgis:`translatable` property. :param parent: Parent widget of this dialog. :type parent: QWidget :param iface: Quantum GIS QGisAppInterface instance. :type iface: QGisAppInterface :param dock: Dock widget instance that we can notify of changes to the keywords. Optional. :type dock: Dock """ QtGui.QDialog.__init__(self, parent) self.setupUi(self) self.setWindowTitle( self.tr('InaSAFE %s Keywords Editor' % get_version())) # Save reference to the QGIS interface and parent self.iface = iface self.parent = parent self.dock = dock self.defaults = None # string constants self.global_default_string = metadata.global_default_attribute['name'] self.do_not_use_string = metadata.do_not_use_attribute['name'] self.global_default_data = metadata.global_default_attribute['id'] self.do_not_use_data = metadata.do_not_use_attribute['id'] if layer is None: self.layer = self.iface.activeLayer() else: self.layer = layer self.keyword_io = KeywordIO() # note the keys should remain untranslated as we need to write # english to the keywords file. The keys will be written as user data # in the combo entries. # .. seealso:: http://www.voidspace.org.uk/python/odict.html self.standard_exposure_list = OrderedDict([ ('population', self.tr('population')), ('structure', self.tr('structure')), ('road', self.tr('road')), ('Not Set', self.tr('Not Set')) ]) self.standard_hazard_list = OrderedDict([ ('earthquake [MMI]', self.tr('earthquake [MMI]')), ('tsunami [m]', self.tr('tsunami [m]')), ('tsunami [wet/dry]', self.tr('tsunami [wet/dry]')), ('tsunami [feet]', self.tr('tsunami [feet]')), ('flood [m]', self.tr('flood [m]')), ('flood [wet/dry]', self.tr('flood [wet/dry]')), ('flood [feet]', self.tr('flood [feet]')), ('tephra [kg2/m2]', self.tr('tephra [kg2/m2]')), ('volcano', self.tr('volcano')), ('generic [categorised]', self.tr('generic [categorised]')), ('Not Set', self.tr('Not Set')) ]) # noinspection PyUnresolvedReferences self.lstKeywords.itemClicked.connect(self.edit_key_value_pair) # Set up help dialog showing logic. help_button = self.buttonBox.button(QtGui.QDialogButtonBox.Help) help_button.clicked.connect(self.show_help) if self.layer is not None and is_polygon_layer(self.layer): # set some initial ui state: self.defaults = get_defaults() self.radPredefined.setChecked(True) self.dsbFemaleRatioDefault.blockSignals(True) self.dsbFemaleRatioDefault.setValue(self.defaults['FEMALE_RATIO']) self.dsbFemaleRatioDefault.blockSignals(False) self.dsbYouthRatioDefault.blockSignals(True) self.dsbYouthRatioDefault.setValue(self.defaults['YOUTH_RATIO']) self.dsbYouthRatioDefault.blockSignals(False) self.dsbAdultRatioDefault.blockSignals(True) self.dsbAdultRatioDefault.setValue(self.defaults['ADULT_RATIO']) self.dsbAdultRatioDefault.blockSignals(False) self.dsbElderlyRatioDefault.blockSignals(True) self.dsbElderlyRatioDefault.setValue( self.defaults['ELDERLY_RATIO']) self.dsbElderlyRatioDefault.blockSignals(False) else: self.radPostprocessing.hide() self.tab_widget.removeTab(1) if self.layer: self.load_state_from_keywords() # add a reload from keywords button reload_button = self.buttonBox.addButton( self.tr('Reload'), QtGui.QDialogButtonBox.ActionRole) reload_button.clicked.connect(self.load_state_from_keywords) self.resize_dialog() self.tab_widget.setCurrentIndex(0) # TODO No we should not have test related stuff in prod code. TS self.test = False
class KeywordsDialog(QtGui.QDialog, FORM_CLASS): """Dialog implementation class for the InaSAFE keywords editor.""" def __init__(self, parent, iface, dock=None, layer=None): """Constructor for the dialog. .. note:: In QtDesigner the advanced editor's predefined keywords list should be shown in english always, so when adding entries to cboKeyword, be sure to choose :safe_qgis:`Properties<<` and untick the :safe_qgis:`translatable` property. :param parent: Parent widget of this dialog. :type parent: QWidget :param iface: Quantum GIS QGisAppInterface instance. :type iface: QGisAppInterface :param dock: Dock widget instance that we can notify of changes to the keywords. Optional. :type dock: Dock """ QtGui.QDialog.__init__(self, parent) self.setupUi(self) self.setWindowTitle( self.tr('InaSAFE %s Keywords Editor' % get_version())) # Save reference to the QGIS interface and parent self.iface = iface self.parent = parent self.dock = dock self.defaults = None # string constants self.global_default_string = metadata.global_default_attribute['name'] self.do_not_use_string = metadata.do_not_use_attribute['name'] self.global_default_data = metadata.global_default_attribute['id'] self.do_not_use_data = metadata.do_not_use_attribute['id'] if layer is None: self.layer = self.iface.activeLayer() else: self.layer = layer self.keyword_io = KeywordIO() # note the keys should remain untranslated as we need to write # english to the keywords file. The keys will be written as user data # in the combo entries. # .. seealso:: http://www.voidspace.org.uk/python/odict.html self.standard_exposure_list = OrderedDict([ ('population', self.tr('population')), ('structure', self.tr('structure')), ('road', self.tr('road')), ('Not Set', self.tr('Not Set')) ]) self.standard_hazard_list = OrderedDict([ ('earthquake [MMI]', self.tr('earthquake [MMI]')), ('tsunami [m]', self.tr('tsunami [m]')), ('tsunami [wet/dry]', self.tr('tsunami [wet/dry]')), ('tsunami [feet]', self.tr('tsunami [feet]')), ('flood [m]', self.tr('flood [m]')), ('flood [wet/dry]', self.tr('flood [wet/dry]')), ('flood [feet]', self.tr('flood [feet]')), ('tephra [kg2/m2]', self.tr('tephra [kg2/m2]')), ('volcano', self.tr('volcano')), ('generic [categorised]', self.tr('generic [categorised]')), ('Not Set', self.tr('Not Set')) ]) # noinspection PyUnresolvedReferences self.lstKeywords.itemClicked.connect(self.edit_key_value_pair) # Set up help dialog showing logic. help_button = self.buttonBox.button(QtGui.QDialogButtonBox.Help) help_button.clicked.connect(self.show_help) if self.layer is not None and is_polygon_layer(self.layer): # set some initial ui state: self.defaults = get_defaults() self.radPredefined.setChecked(True) self.dsbFemaleRatioDefault.blockSignals(True) self.dsbFemaleRatioDefault.setValue(self.defaults['FEMALE_RATIO']) self.dsbFemaleRatioDefault.blockSignals(False) self.dsbYouthRatioDefault.blockSignals(True) self.dsbYouthRatioDefault.setValue(self.defaults['YOUTH_RATIO']) self.dsbYouthRatioDefault.blockSignals(False) self.dsbAdultRatioDefault.blockSignals(True) self.dsbAdultRatioDefault.setValue(self.defaults['ADULT_RATIO']) self.dsbAdultRatioDefault.blockSignals(False) self.dsbElderlyRatioDefault.blockSignals(True) self.dsbElderlyRatioDefault.setValue( self.defaults['ELDERLY_RATIO']) self.dsbElderlyRatioDefault.blockSignals(False) else: self.radPostprocessing.hide() self.tab_widget.removeTab(1) if self.layer: self.load_state_from_keywords() # add a reload from keywords button reload_button = self.buttonBox.addButton( self.tr('Reload'), QtGui.QDialogButtonBox.ActionRole) reload_button.clicked.connect(self.load_state_from_keywords) self.resize_dialog() self.tab_widget.setCurrentIndex(0) # TODO No we should not have test related stuff in prod code. TS self.test = False def set_layer(self, layer): """Set the layer associated with the keyword editor. :param layer: Layer whose keywords should be edited. :type layer: QgsMapLayer """ self.layer = layer self.load_state_from_keywords() # noinspection PyMethodMayBeStatic def show_help(self): """Load the help text for the keywords dialog.""" show_context_help(context='keywords_editor') def toggle_postprocessing_widgets(self): """Hide or show the post processing widgets depending on context.""" LOGGER.debug('togglePostprocessingWidgets') # TODO Too much baggage here. Can't we just disable/enable the tab? TS postprocessing_flag = self.radPostprocessing.isChecked() self.cboSubcategory.setVisible(not postprocessing_flag) self.lblSubcategory.setVisible(not postprocessing_flag) self.show_aggregation_attribute() self.show_female_ratio_attribute() self.show_female_ratio_default() self.show_youth_ratio_attribute() self.show_youth_ratio_default() self.show_adult_ratio_attribute() self.show_adult_ratio_default() self.show_elderly_ratio_attribute() self.show_elderly_ratio_default() # Also enable/disable the aggregation tab self.aggregation_tab.setEnabled(postprocessing_flag) def show_aggregation_attribute(self): """Hide or show the aggregation attribute in the keyword editor dialog. """ box = self.cboAggregationAttribute box.blockSignals(True) box.clear() box.blockSignals(False) current_keyword = self.get_value_for_key( self.defaults['AGGR_ATTR_KEY']) fields, attribute_position = layer_attribute_names( self.layer, [QtCore.QVariant.Int, QtCore.QVariant.String], current_keyword) box.addItems(fields) if attribute_position is None: box.setCurrentIndex(0) else: box.setCurrentIndex(attribute_position) def show_female_ratio_attribute(self): """Hide or show the female ratio attribute in the dialog. """ box = self.cboFemaleRatioAttribute box.blockSignals(True) box.clear() box.blockSignals(False) current_keyword = self.get_value_for_key( self.defaults['FEMALE_RATIO_ATTR_KEY']) fields, attribute_position = layer_attribute_names( self.layer, [QtCore.QVariant.Double], current_keyword) box.addItem(self.global_default_string, self.global_default_data) box.addItem(self.do_not_use_string, self.do_not_use_data) for field in fields: box.addItem(field, field) if current_keyword == self.global_default_data: box.setCurrentIndex(0) elif current_keyword == self.do_not_use_data: box.setCurrentIndex(1) elif attribute_position is None: # current_keyword was not found in the attribute table. # Use default box.setCurrentIndex(0) else: # + 2 is because we add use defaults and don't use box.setCurrentIndex(attribute_position + 2) def show_female_ratio_default(self): """Hide or show the female ratio default attribute in the dialog. """ box = self.dsbFemaleRatioDefault current_value = self.get_value_for_key( self.defaults['FEMALE_RATIO_KEY']) if current_value is None: val = self.defaults['FEMALE_RATIO'] else: val = float(current_value) box.setValue(val) def show_youth_ratio_attribute(self): """Hide or show the youth ratio attribute in the dialog. """ box = self.cboYouthRatioAttribute box.blockSignals(True) box.clear() box.blockSignals(False) current_keyword = self.get_value_for_key( self.defaults['YOUTH_RATIO_ATTR_KEY']) fields, attribute_position = layer_attribute_names( self.layer, [QtCore.QVariant.Double], current_keyword) box.addItem(self.global_default_string, self.global_default_data) box.addItem(self.do_not_use_string, self.do_not_use_data) for field in fields: box.addItem(field, field) if current_keyword == self.global_default_data: box.setCurrentIndex(0) elif current_keyword == self.do_not_use_data: box.setCurrentIndex(1) elif attribute_position is None: # current_keyword was not found in the attribute table. # Use default box.setCurrentIndex(0) else: # + 2 is because we add use defaults and don't use box.setCurrentIndex(attribute_position + 2) def show_youth_ratio_default(self): """Hide or show the youth ratio default attribute in the dialog. """ box = self.dsbYouthRatioDefault current_value = self.get_value_for_key( self.defaults['YOUTH_RATIO_KEY']) if current_value is None: val = self.defaults['YOUTH_RATIO'] else: val = float(current_value) box.setValue(val) def show_adult_ratio_attribute(self): """Hide or show the adult ratio attribute in the dialog. """ box = self.cboAdultRatioAttribute box.blockSignals(True) box.clear() box.blockSignals(False) current_keyword = self.get_value_for_key( self.defaults['ADULT_RATIO_ATTR_KEY']) fields, attribute_position = layer_attribute_names( self.layer, [QtCore.QVariant.Double], current_keyword) box.addItem(self.global_default_string, self.global_default_data) box.addItem(self.do_not_use_string, self.do_not_use_data) for field in fields: box.addItem(field, field) if current_keyword == self.global_default_data: box.setCurrentIndex(0) elif current_keyword == self.do_not_use_data: box.setCurrentIndex(1) elif attribute_position is None: # current_keyword was not found in the attribute table. # Use default box.setCurrentIndex(0) else: # + 2 is because we add use defaults and don't use box.setCurrentIndex(attribute_position + 2) def show_adult_ratio_default(self): """Hide or show the adult ratio default attribute in the dialog. """ box = self.dsbAdultRatioDefault current_value = self.get_value_for_key( self.defaults['ADULT_RATIO_KEY']) if current_value is None: val = self.defaults['ADULT_RATIO'] else: val = float(current_value) box.setValue(val) def show_elderly_ratio_attribute(self): """Show the elderly ratio attribute in the dialog. """ box = self.cboElderlyRatioAttribute box.blockSignals(True) box.clear() box.blockSignals(False) current_keyword = self.get_value_for_key( self.defaults['ELDERLY_RATIO_ATTR_KEY']) fields, attribute_position = layer_attribute_names( self.layer, [QtCore.QVariant.Double], current_keyword) box.addItem(self.global_default_string, self.global_default_data) box.addItem(self.do_not_use_string, self.do_not_use_data) for field in fields: box.addItem(field, field) if current_keyword == self.global_default_data: box.setCurrentIndex(0) elif current_keyword == self.do_not_use_data: box.setCurrentIndex(1) elif attribute_position is None: # current_keyword was not found in the attribute table. # Use default box.setCurrentIndex(0) else: # + 2 is because we add use defaults and don't use box.setCurrentIndex(attribute_position + 2) def show_elderly_ratio_default(self): """Show the elderly ratio default attribute in the dialog. """ box = self.dsbElderlyRatioDefault current_value = self.get_value_for_key( self.defaults['ELDERLY_RATIO_KEY']) if current_value is None: val = self.defaults['ELDERLY_RATIO'] else: val = float(current_value) box.setValue(val) # prevents actions being handled twice # noinspection PyPep8Naming @pyqtSignature('int') def on_cboAggregationAttribute_currentIndexChanged(self, index=None): """Handler for aggregation attribute combo change. :param index: Not used but required for slot. """ del index if not self.radPostprocessing.isChecked(): return self.add_list_entry(self.defaults['AGGR_ATTR_KEY'], self.cboAggregationAttribute.currentText()) # prevents actions being handled twice # noinspection PyPep8Naming @pyqtSignature('int') def on_cboFemaleRatioAttribute_currentIndexChanged(self, index=None): """Handler for female ratio attribute change. :param index: Not used but required for slot. """ del index if not self.radPostprocessing.isChecked(): return current_index = self.cboFemaleRatioAttribute.currentIndex() data = self.cboFemaleRatioAttribute.itemData(current_index) if data == self.global_default_data: self.dsbFemaleRatioDefault.setEnabled(True) current_default = self.get_value_for_key( self.defaults['FEMALE_RATIO_KEY']) if current_default is None: self.add_list_entry(self.defaults['FEMALE_RATIO_KEY'], self.dsbFemaleRatioDefault.value()) else: self.dsbFemaleRatioDefault.setEnabled(False) self.remove_item_by_key(self.defaults['FEMALE_RATIO_KEY']) self.add_list_entry(self.defaults['FEMALE_RATIO_ATTR_KEY'], data) # noinspection PyPep8Naming def on_cboYouthRatioAttribute_currentIndexChanged(self, index=None): """Handler for youth ratio attribute change. :param index: Not used but required for slot. """ del index if not self.radPostprocessing.isChecked(): return current_index = self.cboYouthRatioAttribute.currentIndex() data = self.cboYouthRatioAttribute.itemData(current_index) if data == self.global_default_data: self.dsbYouthRatioDefault.setEnabled(True) current_default = self.get_value_for_key( self.defaults['YOUTH_RATIO_KEY']) if current_default is None: self.add_list_entry(self.defaults['YOUTH_RATIO_KEY'], self.dsbYouthRatioDefault.value()) else: self.dsbYouthRatioDefault.setEnabled(False) self.remove_item_by_key(self.defaults['YOUTH_RATIO_KEY']) self.add_list_entry(self.defaults['YOUTH_RATIO_ATTR_KEY'], data) # noinspection PyPep8Naming def on_cboAdultRatioAttribute_currentIndexChanged(self, index=None): """Handler for adult ratio attribute change. :param index: Not used but required for slot. """ del index if not self.radPostprocessing.isChecked(): return current_index = self.cboAdultRatioAttribute.currentIndex() data = self.cboAdultRatioAttribute.itemData(current_index) if data == self.global_default_data: self.dsbAdultRatioDefault.setEnabled(True) current_default = self.get_value_for_key( self.defaults['ADULT_RATIO_KEY']) if current_default is None: self.add_list_entry(self.defaults['ADULT_RATIO_KEY'], self.dsbAdultRatioDefault.value()) else: self.dsbAdultRatioDefault.setEnabled(False) self.remove_item_by_key(self.defaults['ADULT_RATIO_KEY']) self.add_list_entry(self.defaults['ADULT_RATIO_ATTR_KEY'], data) # noinspection PyPep8Naming def on_cboElderlyRatioAttribute_currentIndexChanged(self, index=None): """Handler for elderly ratio attribute change. :param index: Not used but required for slot. """ del index if not self.radPostprocessing.isChecked(): return current_index = self.cboElderlyRatioAttribute.currentIndex() data = self.cboElderlyRatioAttribute.itemData(current_index) if data == self.global_default_data: self.dsbElderlyRatioDefault.setEnabled(True) current_default = self.get_value_for_key( self.defaults['ELDERLY_RATIO_KEY']) if current_default is None: self.add_list_entry(self.defaults['ELDERLY_RATIO_KEY'], self.dsbElderlyRatioDefault.value()) else: self.dsbElderlyRatioDefault.setEnabled(False) self.remove_item_by_key(self.defaults['ELDERLY_RATIO_KEY']) self.add_list_entry(self.defaults['ELDERLY_RATIO_ATTR_KEY'], data) # prevents actions being handled twice # noinspection PyPep8Naming @pyqtSignature('double') def on_dsbFemaleRatioDefault_valueChanged(self, value): """Handler for female ration default value changing. :param value: Not used but required for slot. """ del value if not self.radPostprocessing.isChecked(): return box = self.dsbFemaleRatioDefault if box.isEnabled(): self.add_list_entry(self.defaults['FEMALE_RATIO_KEY'], box.value()) # noinspection PyPep8Naming def on_dsbYouthRatioDefault_valueChanged(self, value): """Handler for youth ration default value changing. :param value: Not used but required for slot. """ del value if not self.radPostprocessing.isChecked(): return box = self.dsbYouthRatioDefault if box.isEnabled(): self.add_list_entry(self.defaults['YOUTH_RATIO_KEY'], box.value()) # noinspection PyPep8Naming def on_dsbAdultRatioDefault_valueChanged(self, value): """Handler for adult ration default value changing. :param value: Not used but required for slot. """ del value if not self.radPostprocessing.isChecked(): return box = self.dsbAdultRatioDefault if box.isEnabled(): self.add_list_entry(self.defaults['ADULT_RATIO_KEY'], box.value()) # noinspection PyPep8Naming def on_dsbElderlyRatioDefault_valueChanged(self, value): """Handler for elderly ration default value changing. :param value: Not used but required for slot. """ del value if not self.radPostprocessing.isChecked(): return box = self.dsbElderlyRatioDefault if box.isEnabled(): self.add_list_entry(self.defaults['ELDERLY_RATIO_KEY'], box.value()) # prevents actions being handled twice # noinspection PyPep8Naming @pyqtSignature('bool') def on_radHazard_toggled(self, flag): """Automatic slot executed when the hazard radio is toggled. :param flag: Flag indicating the new checked state of the button. :type flag: bool """ if not flag: return self.set_category('hazard') self.update_controls_from_list() # prevents actions being handled twice # noinspection PyPep8Naming @pyqtSignature('bool') def on_radExposure_toggled(self, theFlag): """Automatic slot executed when the hazard radio is toggled on. :param theFlag: Flag indicating the new checked state of the button. :type theFlag: bool """ if not theFlag: return self.set_category('exposure') self.update_controls_from_list() # prevents actions being handled twice # noinspection PyPep8Naming @pyqtSignature('bool') def on_radPostprocessing_toggled(self, flag): """Automatic slot executed when the hazard radio is toggled on. :param flag: Flag indicating the new checked state of the button. :type flag: bool """ if flag: self.set_category('postprocessing') self.update_controls_from_list() return if self.defaults is not None: self.remove_item_by_key(self.defaults['AGGR_ATTR_KEY']) self.remove_item_by_key(self.defaults['FEMALE_RATIO_ATTR_KEY']) self.remove_item_by_key(self.defaults['FEMALE_RATIO_KEY']) self.remove_item_by_key(self.defaults['YOUTH_RATIO_ATTR_KEY']) self.remove_item_by_key(self.defaults['YOUTH_RATIO_KEY']) self.remove_item_by_key(self.defaults['ADULT_RATIO_ATTR_KEY']) self.remove_item_by_key(self.defaults['ADULT_RATIO_KEY']) self.remove_item_by_key(self.defaults['ELDERLY_RATIO_ATTR_KEY']) self.remove_item_by_key(self.defaults['ELDERLY_RATIO_KEY']) # prevents actions being handled twice # noinspection PyPep8Naming @pyqtSignature('int') def on_cboSubcategory_currentIndexChanged(self, index=None): """Automatic slot executed when the subcategory is changed. When the user changes the subcategory, we will extract the subcategory and dataype or unit (depending on if it is a hazard or exposure subcategory) from the [] after the name. :param index: Not used but required for Qt slot. """ if index == -1: self.remove_item_by_key('subcategory') return text = self.cboSubcategory.itemData(self.cboSubcategory.currentIndex()) # I found that myText is 'Not Set' for every language if text == self.tr('Not Set') or text == 'Not Set': self.remove_item_by_key('subcategory') return tokens = text.split(' ') if len(tokens) < 1: self.remove_item_by_key('subcategory') return subcategory = tokens[0] self.add_list_entry('subcategory', subcategory) # Some subcategories e.g. roads have no units or datatype if len(tokens) == 1: return if tokens[1].find('[') < 0: return category = self.get_value_for_key('category') if 'hazard' == category: units = tokens[1].replace('[', '').replace(']', '') self.add_list_entry('unit', units) if 'exposure' == category: data_type = tokens[1].replace('[', '').replace(']', '') self.add_list_entry('datatype', data_type) # prevents actions being handled twice def set_subcategory_list(self, entries, selected_item=None): """Helper to populate the subcategory list based on category context. :param entries: An OrderedDict of subcategories. The dict entries should be in the form ('earthquake', self.tr('earthquake')). See http://www.voidspace.org.uk/python/odict.html for info on OrderedDict. :type entries: OrderedDict :param selected_item: Which item should be selected in the combo. If the selected item is not in entries, it will be appended to it. This is optional. :type selected_item: str """ # To avoid triggering on_cboSubcategory_currentIndexChanged # we block signals from the combo while updating it self.cboSubcategory.blockSignals(True) self.cboSubcategory.clear() item_selected_flag = selected_item is not None selected_item_values = selected_item not in entries.values() selected_item_keys = selected_item not in entries.keys() if (item_selected_flag and selected_item_values and selected_item_keys): # Add it to the OrderedList entries[selected_item] = selected_item index = 0 selected_index = 0 for key, value in entries.iteritems(): if value == selected_item or key == selected_item: selected_index = index index += 1 self.cboSubcategory.addItem(value, key) self.cboSubcategory.setCurrentIndex(selected_index) self.cboSubcategory.blockSignals(False) # prevents actions being handled twice # noinspection PyPep8Naming @pyqtSignature('') def on_pbnAddToList1_clicked(self): """Automatic slot executed when the pbnAddToList1 button is pressed. """ if (self.lePredefinedValue.text() != "" and self.cboKeyword.currentText() != ""): current_key = self.tr(self.cboKeyword.currentText()) current_value = self.lePredefinedValue.text() self.add_list_entry(current_key, current_value) self.lePredefinedValue.setText('') self.update_controls_from_list() # prevents actions being handled twice # noinspection PyPep8Naming @pyqtSignature('') def on_pbnAddToList2_clicked(self): """Automatic slot executed when the pbnAddToList2 button is pressed. """ current_key = self.leKey.text() current_value = self.leValue.text() if current_key == 'category' and current_value == 'hazard': self.radHazard.blockSignals(True) self.radHazard.setChecked(True) self.set_subcategory_list(self.standard_hazard_list) self.radHazard.blockSignals(False) elif current_key == 'category' and current_value == 'exposure': self.radExposure.blockSignals(True) self.radExposure.setChecked(True) self.set_subcategory_list(self.standard_exposure_list) self.radExposure.blockSignals(False) elif current_key == 'category': # .. todo:: notify the user their category is invalid pass self.add_list_entry(current_key, current_value) self.leKey.setText('') self.leValue.setText('') self.update_controls_from_list() # prevents actions being handled twice # noinspection PyPep8Naming @pyqtSignature('') def on_pbnRemove_clicked(self): """Automatic slot executed when the pbnRemove button is pressed. Any selected items in the keywords list will be removed. """ for item in self.lstKeywords.selectedItems(): self.lstKeywords.takeItem(self.lstKeywords.row(item)) self.leKey.setText('') self.leValue.setText('') self.update_controls_from_list() def add_list_entry(self, key, value): """Add an item to the keywords list given its key/value. The key and value must both be valid, non empty strings or an InvalidKVPError will be raised. If an entry with the same key exists, it's value will be replaced with value. It will add the current key/value pair to the list if it is not already present. The kvp will also be stored in the data of the listwidgetitem as a simple string delimited with a bar ('|'). :param key: The key part of the key value pair (kvp). :type key: str :param value: Value part of the key value pair (kvp). :type value: str """ if key is None or key == '': return if value is None or value == '': return # make sure that both key and value is string key = str(key) value = str(value) message = '' if ':' in key: key = key.replace(':', '.') message = self.tr('Colons are not allowed, replaced with "."') if ':' in value: value = value.replace(':', '.') message = self.tr('Colons are not allowed, replaced with "."') if message == '': self.lblMessage.setText('') self.lblMessage.hide() else: self.lblMessage.setText(message) self.lblMessage.show() item = QtGui.QListWidgetItem(key + ':' + value) # We are going to replace, so remove it if it exists already self.remove_item_by_key(key) data = key + '|' + value item.setData(QtCore.Qt.UserRole, data) self.lstKeywords.insertItem(0, item) def set_category(self, category): """Set the category radio button based on category. :param category: Either 'hazard', 'exposure' or 'postprocessing'. :type category: str :returns: False if radio button could not be updated, otherwise True. :rtype: bool """ # convert from QString if needed category = str(category) if self.get_value_for_key('category') == category: # nothing to do, go home return True if category not in ['hazard', 'exposure', 'postprocessing']: # .. todo:: report an error to the user return False # Special case when category changes, we start on a new slate! if category == 'hazard': # only cause a toggle if we actually changed the category # This will only really be apparent if user manually enters # category as a keyword self.reset() self.radHazard.blockSignals(True) self.radHazard.setChecked(True) self.radHazard.blockSignals(False) self.remove_item_by_key('subcategory') self.remove_item_by_key('datatype') self.add_list_entry('category', 'hazard') hazard_list = self.standard_hazard_list self.set_subcategory_list(hazard_list) elif category == 'exposure': self.reset() self.radExposure.blockSignals(True) self.radExposure.setChecked(True) self.radExposure.blockSignals(False) self.remove_item_by_key('subcategory') self.remove_item_by_key('unit') self.add_list_entry('category', 'exposure') exposure_list = self.standard_exposure_list self.set_subcategory_list(exposure_list) else: self.reset() self.radPostprocessing.blockSignals(True) self.radPostprocessing.setChecked(True) self.radPostprocessing.blockSignals(False) self.remove_item_by_key('subcategory') self.add_list_entry('category', 'postprocessing') return True def reset(self, primary_keywords_only=True): """Reset all controls to a blank state. :param primary_keywords_only: If True (the default), only reset Subcategory, datatype and units. :type primary_keywords_only: bool """ self.cboSubcategory.clear() self.remove_item_by_key('subcategory') self.remove_item_by_key('datatype') self.remove_item_by_key('unit') self.remove_item_by_key('source') if not primary_keywords_only: # Clear everything else too self.lstKeywords.clear() self.leKey.clear() self.leValue.clear() self.lePredefinedValue.clear() self.leTitle.clear() self.leSource.clear() def remove_item_by_key(self, removal_key): """Remove an item from the kvp list given its key. :param removal_key: Key of item to be removed. :type removal_key: str """ for counter in range(self.lstKeywords.count()): existing_item = self.lstKeywords.item(counter) text = existing_item.text() tokens = text.split(':') if len(tokens) < 2: break key = tokens[0] if removal_key == key: # remove it since the removal_key is already present self.blockSignals(True) self.lstKeywords.takeItem(counter) self.blockSignals(False) break def remove_item_by_value(self, removal_value): """Remove an item from the kvp list given its key. :param removal_value: Value of item to be removed. :type removal_value: str """ for counter in range(self.lstKeywords.count()): existing_item = self.lstKeywords.item(counter) text = existing_item.text() tokens = text.split(':') value = tokens[1] if removal_value == value: # remove it since the key is already present self.blockSignals(True) self.lstKeywords.takeItem(counter) self.blockSignals(False) break def get_value_for_key(self, lookup_key): """If key list contains a specific key, return its value. :param lookup_key: The key to search for :type lookup_key: str :returns: Value of key if matched otherwise none. :rtype: str """ for counter in range(self.lstKeywords.count()): existing_item = self.lstKeywords.item(counter) text = existing_item.text() tokens = text.split(':') key = str(tokens[0]).strip() value = str(tokens[1]).strip() if lookup_key == key: return value return None def load_state_from_keywords(self): """Set the ui state to match the keywords of the active layer. In case the layer has no keywords or any problem occurs reading them, start with a blank state so that subcategory gets populated nicely & we will assume exposure to start with. Also if only title is set we use similar logic (title is added by default in dock and other defaults need to be explicitly added when opening this dialog). See #751 """ keywords = {'category': 'exposure'} try: # Now read the layer with sub layer if needed keywords = self.keyword_io.read_keywords(self.layer) except (InvalidParameterError, HashNotFoundError, NoKeywordsFoundError): pass layer_name = self.layer.name() if 'title' not in keywords: self.leTitle.setText(layer_name) self.lblLayerName.setText(self.tr('Keywords for %s' % layer_name)) if 'source' in keywords: self.leSource.setText(keywords['source']) else: self.leSource.setText('') # if we have a category key, unpack it first # so radio button etc get set if 'category' in keywords: self.set_category(keywords['category']) keywords.pop('category') else: # assume exposure to match ui. See issue #751 self.add_list_entry('category', 'exposure') aggregation_attributes = [ DEFAULTS['FEMALE_RATIO_ATTR_KEY'], DEFAULTS['YOUTH_RATIO_ATTR_KEY'], DEFAULTS['ADULT_RATIO_ATTR_KEY'], DEFAULTS['ELDERLY_RATIO_ATTR_KEY'], ] for key in keywords.iterkeys(): if key in aggregation_attributes: if str(keywords[key]) == 'Use default': self.add_list_entry(key, self.global_default_data) continue self.add_list_entry(key, str(keywords[key])) # now make the rest of the safe_qgis reflect the list entries self.update_controls_from_list() def update_controls_from_list(self): """Set the ui state to match the keywords of the active layer.""" subcategory = self.get_value_for_key('subcategory') units = self.get_value_for_key('unit') data_type = self.get_value_for_key('datatype') title = self.get_value_for_key('title') if title is not None: self.leTitle.setText(title) elif self.layer is not None: layer_name = self.layer.name() self.lblLayerName.setText(self.tr('Keywords for %s' % layer_name)) else: self.lblLayerName.setText('') if not is_polygon_layer(self.layer): self.radPostprocessing.setEnabled(False) else: # adapt gui if we are in postprocessing category self.toggle_postprocessing_widgets() if self.radExposure.isChecked(): if subcategory is not None and data_type is not None: self.set_subcategory_list(self.standard_exposure_list, subcategory + ' [' + data_type + ']') elif subcategory is not None: self.set_subcategory_list(self.standard_exposure_list, subcategory) else: self.set_subcategory_list(self.standard_exposure_list, self.tr('Not Set')) elif self.radHazard.isChecked(): if subcategory is not None and units is not None: self.set_subcategory_list(self.standard_hazard_list, subcategory + ' [' + units + ']') elif subcategory is not None: self.set_subcategory_list(self.standard_hazard_list, subcategory) else: self.set_subcategory_list(self.standard_hazard_list, self.tr('Not Set')) self.resize_dialog() def resize_dialog(self): """Resize the dialog to fit its contents.""" # noinspection PyArgumentList QtCore.QCoreApplication.processEvents() LOGGER.debug('adjust ing dialog size') self.adjustSize() # prevents actions being handled twice # noinspection PyPep8Naming @pyqtSignature('QString') def on_leTitle_textEdited(self, title): """Update the keywords list whenever the user changes the title. This slot is not called if the title is changed programmatically. :param title: New title keyword for the layer. :type title: str """ self.add_list_entry('title', str(title)) # prevents actions being handled twice # noinspection PyPep8Naming @pyqtSignature('QString') def on_leSource_textEdited(self, source): """Update the keywords list whenever the user changes the source. This slot is not called if the source is changed programmatically. :param source: New source keyword for the layer. :type source: str """ if source is None or source == '': self.remove_item_by_key('source') else: self.add_list_entry('source', str(source)) def get_keywords(self): """Obtain the state of the dialog as a keywords dict. :returns: Keywords reflecting the state of the dialog. :rtype: dict """ # make sure title is listed if str(self.leTitle.text()) != '': self.add_list_entry('title', str(self.leTitle.text())) # make sure the source is listed too if str(self.leSource.text()) != '': self.add_list_entry('source', str(self.leSource.text())) keywords = {} for counter in range(self.lstKeywords.count()): existing_item = self.lstKeywords.item(counter) text = existing_item.text() tokens = text.split(':') key = str(tokens[0]).strip() value = str(tokens[1]).strip() keywords[key] = value return keywords def accept(self): """Automatic slot executed when the ok button is pressed. It will write out the keywords for the layer that is active. """ self.apply_changes() keywords = self.get_keywords() # If it's postprocessing layer, we need to check if age ratio is valid if self.radPostprocessing.isChecked(): valid_age_ratio, sum_age_ratios = self.age_ratios_are_valid( keywords) if not valid_age_ratio: message = self.tr( 'The sum of age ratios is %s which exceeds 1. Please ' 'adjust the age ration defaults so that their cumulative ' 'value is not greater than 1.' % sum_age_ratios) if not self.test: # noinspection PyCallByClass,PyTypeChecker,PyArgumentList QtGui.QMessageBox.warning(self, self.tr('InaSAFE'), message) return try: self.keyword_io.write_keywords(layer=self.layer, keywords=keywords) except InaSAFEError, e: error_message = get_error_message(e) message = self.tr( 'An error was encountered when saving the keywords:\n' '%s' % error_message.to_html()) # noinspection PyCallByClass,PyTypeChecker,PyArgumentList QtGui.QMessageBox.warning(self, self.tr('InaSAFE'), message) if self.dock is not None: self.dock.get_layers() self.done(QtGui.QDialog.Accepted)
def _clip_raster_layer( layer, extent, cell_size=None, extra_keywords=None): """Clip a Hazard or Exposure raster layer to the extents provided. The layer must be a raster layer or an exception will be thrown. .. note:: The extent *must* be in EPSG:4326. The output layer will always be in WGS84/Geographic. :param layer: A valid QGIS raster layer in EPSG:4326 :type layer: QgsRasterLayer :param extent: An array representing the exposure layer extents in the form [xmin, ymin, xmax, ymax]. It is assumed that the coordinates are in EPSG:4326 although currently no checks are made to enforce this. or: A QgsGeometry of type polygon. **Polygon clipping currently only supported for vector datasets.** :type extent: list(float), QgsGeometry :param cell_size: Cell size (in GeoCRS) which the layer should be resampled to. If not provided for a raster layer (i.e. theCellSize=None), the native raster cell size will be used. :type cell_size: float :returns: Output clipped layer (placed in the system temp dir). :rtype: QgsRasterLayer :raises: InvalidProjectionError - if input layer is a density layer in projected coordinates. See issue #123. """ if not layer or not extent: message = tr('Layer or Extent passed to clip is None.') raise InvalidParameterError(message) if layer.type() != QgsMapLayer.RasterLayer: message = tr( 'Expected a raster layer but received a %s.' % str(layer.type())) raise InvalidParameterError(message) working_layer = str(layer.source()) # Check for existence of keywords file base, _ = os.path.splitext(working_layer) keywords_path = base + '.keywords' message = tr( 'Input file to be clipped "%s" does not have the ' 'expected keywords file %s' % ( working_layer, keywords_path )) verify(os.path.isfile(keywords_path), message) # Raise exception if layer is projected and refers to density (issue #123) # FIXME (Ole): Need to deal with it - e.g. by automatically reprojecting # the layer at this point and setting the native resolution accordingly # in its keywords. keywords = read_file_keywords(keywords_path) if 'datatype' in keywords and keywords['datatype'] == 'count': if str(layer.crs().authid()) != 'EPSG:4326': # This layer is not WGS84 geographic message = ( 'Layer %s represents count but has spatial reference "%s". ' 'Count layers must be given in WGS84 geographic coordinates, ' 'so please reproject and try again. For more information, see ' 'issue https://github.com/AIFDR/inasafe/issues/123' % ( working_layer, layer.crs().toProj4() )) raise InvalidProjectionError(message) # We need to provide gdalwarp with a dataset for the clip # because unlike gdal_translate, it does not take projwin. clip_kml = extent_to_kml(extent) # Create a filename for the clipped, resampled and reprojected layer handle, filename = tempfile.mkstemp('.tif', 'clip_', temp_dir()) os.close(handle) os.remove(filename) # If no cell size is specified, we need to run gdalwarp without # specifying the output pixel size to ensure the raster dims # remain consistent. binary_list = which('gdalwarp') LOGGER.debug('Path for gdalwarp: %s' % binary_list) if len(binary_list) < 1: raise CallGDALError( tr('gdalwarp could not be found on your computer')) # Use the first matching gdalwarp found binary = binary_list[0] if cell_size is None: command = ( '"%s" -q -t_srs EPSG:4326 -r near -cutline %s -crop_to_cutline ' '-ot Float64 -of GTiff "%s" "%s"' % ( binary, clip_kml, working_layer, filename)) else: command = ( '"%s" -q -t_srs EPSG:4326 -r near -tr %s %s -cutline %s ' '-crop_to_cutline -ot Float64 -of GTiff "%s" "%s"' % ( binary, repr(cell_size), repr(cell_size), clip_kml, working_layer, filename)) LOGGER.debug(command) result = QProcess().execute(command) # For QProcess exit codes see # http://qt-project.org/doc/qt-4.8/qprocess.html#execute if result == -2: # cannot be started message_detail = tr('Process could not be started.') message = tr( '<p>Error while executing the following shell command:' '</p><pre>%s</pre><p>Error message: %s' % (command, message_detail)) raise CallGDALError(message) elif result == -1: # process crashed message_detail = tr('Process crashed.') message = tr( '<p>Error while executing the following shell command:</p>' '<pre>%s</pre><p>Error message: %s' % (command, message_detail)) raise CallGDALError(message) # .. todo:: Check the result of the shell call is ok keyword_io = KeywordIO() keyword_io.copy_keywords(layer, filename, extra_keywords=extra_keywords) base_name = '%s clipped' % layer.name() layer = QgsRasterLayer(filename, base_name) return layer
def test_regression_2553_no_resample(self): """Test for regression 2553 (no resampling). see : https://github.com/inasafe/inasafe/issues/2553 We want to verify that population with resampling should produce a result within a reasonable range of the same analysis but doing population with no resampling. """ hazard_path = standard_data_path( 'hazard', 'continuous_flood_unaligned_big_size.tif') exposure_path = standard_data_path( 'exposure', 'people_allow_resampling_false.tif') hazard_layer, hazard_layer_purpose = load_layer(hazard_path) # Check if there is a regression about keywords being updated from # another layer - see #2605 keywords = KeywordIO(hazard_layer) self.assertIn('flood unaligned', keywords.to_message().to_text()) exposure_layer, exposure_layer_purpose = load_layer(exposure_path) keywords = KeywordIO(exposure_layer) self.assertIn('*Allow resampling*, false------', keywords.to_message().to_text()) QgsMapLayerRegistry.instance().addMapLayers( [hazard_layer, exposure_layer]) # Count the total value of all exposure pixels # this is arse about face but width is actually giving height height = exposure_layer.width() # this is arse about face but height is actually giving width width = exposure_layer.height() provider = exposure_layer.dataProvider() # Bands count from 1! block = provider.block(1, provider.extent(), height, width) # Enable on-the-fly reprojection set_canvas_crs(GEOCRS, True) # This is the nicer way but wierdly it gets nan for every cell total_population = 0.0 cell_count = 0 row = 0 # Iterate down each column to match the layout produced by r.stats while row < width: column = 0 while column < height: cell_count += 1 value = block.value(row, column) if value > 0: total_population += value column += 1 row += 1 # print "Total value of all cells is: %d" % total_population # print "Number of cells counted: %d" % cell_count # 131 computed using r.sum self.assertAlmostEqual(total_population, 131.0177006121) result, message = setup_scenario( self.dock, hazard='flood unaligned', exposure='People never resample', function='Need evacuation', function_id='FloodEvacuationRasterHazardFunction') self.assertTrue(result, message) # Press RUN self.dock.accept() safe_layer = self.dock.impact_function.impact keywords = safe_layer.get_keywords() evacuated = float(keywords['evacuated']) self.assertLess(evacuated, total_population) expected_evacuated = 131.0 self.assertEqual(evacuated, expected_evacuated)
def _clip_vector_layer( layer, extent, extra_keywords=None, explode_flag=True, hard_clip_flag=False, explode_attribute=None): """Clip a Hazard or Exposure layer to the extents provided. The layer must be a vector layer or an exception will be thrown. The output layer will always be in WGS84/Geographic. :param layer: A valid QGIS vector or raster layer :type layer: :param extent: Either an array representing the exposure layer extents in the form [xmin, ymin, xmax, ymax]. It is assumed that the coordinates are in EPSG:4326 although currently no checks are made to enforce this. or: A QgsGeometry of type polygon. **Polygon clipping is currently only supported for vector datasets.** :type extent: list(float, float, float, float) :param extra_keywords: Optional keywords dictionary to be added to output layer. :type extra_keywords: dict :param explode_flag: A bool specifying whether multipart features should be 'exploded' into singleparts. **This parameter is ignored for raster layer clipping.** :type explode_flag: bool :param hard_clip_flag: A bool specifying whether line and polygon features that extend beyond the extents should be clipped such that they are reduced in size to the part of the geometry that intersects the extent only. Default is False. **This parameter is ignored for raster layer clipping.** :type hard_clip_flag: bool :param explode_attribute: A str specifying to which attribute #1, #2 and so on will be added in case of explode_flag being true. The attribute is modified only if there are at least 2 parts. :type explode_attribute: str :returns: Clipped layer (placed in the system temp dir). The output layer will be reprojected to EPSG:4326 if needed. :rtype: QgsVectorLayer """ if not layer or not extent: message = tr('Layer or Extent passed to clip is None.') raise InvalidParameterError(message) if layer.type() != QgsMapLayer.VectorLayer: message = tr( 'Expected a vector layer but received a %s.' % str(layer.type())) raise InvalidParameterError(message) # handle, file_name = tempfile.mkstemp('.sqlite', 'clip_', # temp_dir()) handle, file_name = tempfile.mkstemp( '.shp', 'clip_', temp_dir()) # Ensure the file is deleted before we try to write to it # fixes windows specific issue where you get a message like this # ERROR 1: c:\temp\inasafe\clip_jpxjnt.shp is not a directory. # This is because mkstemp creates the file handle and leaves # the file open. os.close(handle) os.remove(file_name) # Get the clip extents in the layer's native CRS geo_crs = QgsCoordinateReferenceSystem() geo_crs.createFromSrid(4326) transform = QgsCoordinateTransform(geo_crs, layer.crs()) allowed_clip_values = [QGis.WKBPolygon, QGis.WKBPolygon25D] if type(extent) is list: rectangle = QgsRectangle( extent[0], extent[1], extent[2], extent[3]) # noinspection PyCallByClass # noinspection PyTypeChecker polygon = QgsGeometry.fromRect(rectangle) elif (type(extent) is QgsGeometry and extent.wkbType in allowed_clip_values): rectangle = extent.boundingBox().toRectF() polygon = extent else: raise InvalidClipGeometryError( tr( 'Clip geometry must be an extent or a single part' 'polygon based geometry.')) projected_extent = transform.transformBoundingBox(rectangle) # Get vector layer provider = layer.dataProvider() if provider is None: message = tr( 'Could not obtain data provider from ' 'layer "%s"' % layer.source()) raise Exception(message) # Get the layer field list, select by our extent then write to disk # .. todo:: FIXME - for different geometry types we should implement # different clipping behaviour e.g. reject polygons that # intersect the edge of the bbox. Tim request = QgsFeatureRequest() if not projected_extent.isEmpty(): request.setFilterRect(projected_extent) request.setFlags(QgsFeatureRequest.ExactIntersect) field_list = provider.fields() writer = QgsVectorFileWriter( file_name, 'UTF-8', field_list, layer.wkbType(), geo_crs, # 'SQLite') # FIXME (Ole): This works but is far too slow 'ESRI Shapefile') if writer.hasError() != QgsVectorFileWriter.NoError: message = tr( 'Error when creating shapefile: <br>Filename:' '%s<br>Error: %s' % (file_name, writer.hasError())) raise Exception(message) # Reverse the coordinate xform now so that we can convert # geometries from layer crs to geocrs. transform = QgsCoordinateTransform(layer.crs(), geo_crs) # Retrieve every feature with its geometry and attributes count = 0 has_multipart = False for feature in provider.getFeatures(request): geometry = feature.geometry() # Loop through the parts adding them to the output file # we write out single part features unless explode_flag is False if explode_flag: geometry_list = explode_multipart_geometry(geometry) else: geometry_list = [geometry] for part_index, part in enumerate(geometry_list): part.transform(transform) if hard_clip_flag: # Remove any dangling bits so only intersecting area is # kept. part = clip_geometry(polygon, part) if part is None: continue feature.setGeometry(part) # There are multiple parts and we want to show it in the # explode_attribute if part_index > 0 and explode_attribute is not None: has_multipart = True writer.addFeature(feature) count += 1 del writer # Flush to disk if count < 1: message = tr( 'No features fall within the clip extents. Try panning / zooming ' 'to an area containing data and then try to run your analysis ' 'again. If hazard and exposure data doesn\'t overlap at all, it ' 'is not possible to do an analysis. Another possibility is that ' 'the layers do overlap but because they may have different ' 'spatial references, they appear to be disjointed. If this is the ' 'case, try to turn on reproject on-the-fly in QGIS.') raise NoFeaturesInExtentError(message) keyword_io = KeywordIO() if extra_keywords is None: extra_keywords = {} extra_keywords['had multipart polygon'] = has_multipart keyword_io.copy_keywords( layer, file_name, extra_keywords=extra_keywords) base_name = '%s clipped' % layer.name() layer = QgsVectorLayer(file_name, base_name, 'ogr') return layer
class ImpactReport(object): """A class for creating report using QgsComposition.""" 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() @property def template(self): """Getter to the template""" return self._template @template.setter def template(self, template): """Set template that will be used for report generation. :param template: Path to composer template :type template: str """ if isinstance(template, str) and os.path.exists(template): self._template = template else: self._template = resources_path( 'qgis-composer-templates', 'inasafe-portrait-a4.qpt') # Also recreate template composition self._template_composition = TemplateComposition( template_path=self.template, map_settings=self._iface.mapCanvas().mapSettings()) @property def layer(self): """Getter to layer that will be used for stats, legend, reporting.""" return self._layer @layer.setter def layer(self, layer): """Set the layer that will be used for stats, legend and reporting. :param layer: Layer that will be used for stats, legend and reporting. :type layer: QgsMapLayer, QgsRasterLayer, QgsVectorLayer """ self._layer = layer @property def composition(self): """Getter to QgsComposition instance.""" return self._template_composition.composition @property def extent(self): """Getter to extent for map component in composition.""" return self._extent @extent.setter def extent(self, extent): """Set the extent that will be used for map component in composition. :param extent: The extent. :type extent: QgsRectangle """ if isinstance(extent, QgsRectangle): self._extent = extent else: self._extent = self._iface.mapCanvas().extent() @property def page_dpi(self): """Getter to page resolution in dots per inch.""" return self._page_dpi @page_dpi.setter def page_dpi(self, page_dpi): """Set the page resolution in dpi. :param page_dpi: The page resolution in dots per inch. :type page_dpi: int """ self._page_dpi = page_dpi @property def north_arrow(self): """Getter to north arrow path.""" return self._north_arrow @north_arrow.setter def north_arrow(self, north_arrow_path): """Set image that will be used as north arrow in reports. :param north_arrow_path: Path to the north arrow image. :type north_arrow_path: str """ if isinstance(north_arrow_path, str) and os.path.exists( north_arrow_path): self._north_arrow = north_arrow_path else: self._north_arrow = default_north_arrow_path() @property def safe_logo(self): """Getter to safe logo path.""" return self._safe_logo @safe_logo.setter def safe_logo(self, logo): """Set image that will be used as safe logo in reports. :param logo: Path to the safe logo image. :type logo: str """ if isinstance(logo, str) and os.path.exists(logo): self._safe_logo = logo else: self._safe_logo = default_organisation_logo_path() @property def organisation_logo(self): """Getter to organisation logo path.""" return self._organisation_logo @organisation_logo.setter def organisation_logo(self, logo): """Set image that will be used as organisation logo in reports. :param logo: Path to the organisation logo image. :type logo: str """ if isinstance(logo, str) and os.path.exists(logo): self._organisation_logo = logo else: self._organisation_logo = default_organisation_logo_path() @property def disclaimer(self): """Getter to disclaimer.""" return self._disclaimer @disclaimer.setter def disclaimer(self, text): """Set text that will be used as disclaimer in reports. :param text: Disclaimer text :type text: str """ if not isinstance(text, str): self._disclaimer = disclaimer() else: self._disclaimer = text @property def component_ids(self): """Getter to the component ids""" return self._template_composition.component_ids @component_ids.setter def component_ids(self, component_ids): """Set the component ids. :param component_ids: The component IDs that are needed in the composition. :type component_ids: list """ if not isinstance(component_ids, list): self._template_composition.component_ids = [] else: self._template_composition.component_ids = component_ids @property def missing_elements(self): """Getter to the missing elements.""" return self._template_composition.missing_elements @property def map_title(self): """Get the map title from the layer keywords if possible. :returns: None on error, otherwise the title. :rtype: None, str """ # noinspection PyBroadException try: title = self._keyword_io.read_keywords(self.layer, 'map_title') return title except KeywordNotFoundError: return None except Exception: # pylint: disable=broad-except return None @property def map_legend_attributes(self): """Get the map legend attribute from the layer keywords if possible. :returns: None on error, otherwise the attributes (notes and units). :rtype: None, str """ LOGGER.debug('InaSAFE Map getMapLegendAttributes called') legend_attribute_list = [ 'legend_notes', 'legend_units', 'legend_title'] legend_attribute_dict = {} for legend_attribute in legend_attribute_list: # noinspection PyBroadException try: legend_attribute_dict[legend_attribute] = \ self._keyword_io.read_keywords( self.layer, legend_attribute) except KeywordNotFoundError: pass except Exception: # pylint: disable=broad-except pass return legend_attribute_dict def setup_composition(self): """Set up the composition ready.""" # noinspection PyUnresolvedReferences self._template_composition.composition.setPlotStyle( QgsComposition.Preview) self._template_composition.composition.setPrintResolution( self.page_dpi) self._template_composition.composition.setPrintAsRaster(True) def load_template(self): """Load the template to composition.""" # Get information for substitutions # date, time and plugin version date_time = self._keyword_io.read_keywords(self.layer, 'time_stamp') if date_time is None: date = '' time = '' else: tokens = date_time.split('_') date = tokens[0] time = tokens[1] long_version = get_version() tokens = long_version.split('.') version = '%s.%s.%s' % (tokens[0], tokens[1], tokens[2]) # Get title of the layer title = self.map_title if not title: title = '' # Prepare the substitution map substitution_map = { 'impact-title': title, 'date': date, 'time': time, 'safe-version': version, 'disclaimer': self.disclaimer } # Load template self._template_composition.substitution = substitution_map try: self._template_composition.load_template() except TemplateLoadingError: raise 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 print_to_pdf(self, output_path): """A wrapper to print both the map and the impact table to PDF. :param output_path: Path on the file system to which the pdf should be saved. If None, a generated file name will be used. Note that the table will be prefixed with '_table'. :type output_path: str :returns: The map path and the table path to the pdfs generated. :rtype: tuple """ # Print the map to pdf try: map_path = self.print_map_to_pdf(output_path) except TemplateLoadingError: raise # Print the table to pdf table_path = os.path.splitext(output_path)[0] + '_table.pdf' table_path = self.print_impact_table(table_path) return map_path, table_path def print_map_to_pdf(self, output_path): """Generate the printout for our final map as pdf. :param output_path: Path on the file system to which the pdf should be saved. If None, a generated file name will be used. :type output_path: str :returns: File name of the output file (equivalent to filename if provided). :rtype: str """ LOGGER.debug('InaSAFE Map print_to_pdf called') self.setup_composition() try: self.load_template() except TemplateLoadingError: raise self.draw_composition() if output_path is None: output_path = unique_filename( prefix='report', suffix='.pdf', dir=temp_dir()) self.composition.exportAsPDF(output_path) return output_path 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
class FieldMappingWidget(QTabWidget, object): """Field Mapping Widget.""" def __init__(self, parent=None, iface=None): """Constructor.""" super(FieldMappingWidget, self).__init__(parent) # Attributes self.parent = parent self.iface = iface self.layer = None self.metadata = {} self.tabs = [] # Store all tabs self.keyword_io = KeywordIO() def set_layer(self, layer, keywords=None): """Set layer and update UI accordingly. :param layer: A vector layer that has been already patched with metadata. :type layer: QgsVectorLayer :param keywords: Custom keyword for the layer. :type keywords: dict, None """ self.layer = layer if keywords is not None: self.metadata = keywords else: self.metadata = self.keyword_io.read_keywords(self.layer) self.populate_tabs() def populate_tabs(self): """Populating tabs based on layer metadata.""" self.delete_tabs() layer_purpose = self.metadata.get('layer_purpose') if not layer_purpose: message = tr( 'Key layer_purpose is not found in the layer {layer_name}' ).format(layer_name=self.layer.name()) raise KeywordNotFoundError(message) if layer_purpose == layer_purpose_exposure['key']: layer_subcategory = self.metadata.get('exposure') elif layer_purpose == layer_purpose_hazard['key']: layer_subcategory = self.metadata.get('hazard') else: layer_subcategory = None field_groups = get_field_groups(layer_purpose, layer_subcategory) for field_group in field_groups: tab = FieldMappingTab(field_group, self, self.iface) tab.set_layer(self.layer, self.metadata) self.addTab(tab, field_group['name']) self.tabs.append(tab) def delete_tabs(self): """Methods to delete tabs.""" self.clear() self.tabs = [] def get_field_mapping(self): """Obtain metadata from current state of the widget. :returns: Dictionary of values by type in this format: {'fields': {}, 'values': {}}. :rtype: dict """ fields = {} values = {} for tab in self.tabs: parameter_values = tab.get_parameter_value() fields.update(parameter_values['fields']) values.update(parameter_values['values']) return {'fields': fields, 'values': values}
class KeywordIOTest(unittest.TestCase): """Tests for reading and writing of raster and vector data """ def setUp(self): self.keyword_io = KeywordIO() # SQLite Layer uri = QgsDataSourceURI() sqlite_building_path = standard_data_path( 'exposure', 'exposure.sqlite') uri.setDatabase(sqlite_building_path) uri.setDataSource('', 'buildings_osm_4326', 'Geometry') self.sqlite_layer = QgsVectorLayer( uri.uri(), 'OSM Buildings', 'spatialite') self.expected_sqlite_keywords = { 'datatype': 'OSM' } # Raster Layer keywords hazard_path = standard_data_path('hazard', 'tsunami_wgs84.tif') self.raster_layer, _ = load_layer(hazard_path) self.expected_raster_keywords = { 'hazard_category': 'single_event', 'title': 'Generic Continuous Flood', 'hazard': 'flood', 'continuous_hazard_unit': 'generic', 'layer_geometry': 'raster', 'layer_purpose': 'hazard', 'layer_mode': 'continuous', 'keyword_version': '3.5' } # Vector Layer keywords vector_path = standard_data_path('exposure', 'buildings_osm_4326.shp') self.vector_layer, _ = load_layer(vector_path) self.expected_vector_keywords = { 'keyword_version': '3.5', 'value_map': {}, 'title': 'buildings_osm_4326', 'layer_geometry': 'polygon', 'layer_purpose': 'exposure', 'layer_mode': 'classified', 'exposure': 'structure', } # Keyword less layer keywordless_path = standard_data_path('other', 'keywordless_layer.shp') self.keywordless_layer, _ = load_layer(keywordless_path) # Keyword file self.keyword_path = standard_data_path( 'exposure', 'buildings_osm_4326.xml') def test_read_raster_file_keywords(self): """Can we read raster file keywords using generic readKeywords method """ layer = clone_raster_layer( name='generic_continuous_flood', extension='.asc', include_keywords=True, source_directory=standard_data_path('hazard')) keywords = self.keyword_io.read_keywords(layer) expected_keywords = self.expected_raster_keywords self.assertDictEqual(keywords, expected_keywords) def test_read_vector_file_keywords(self): """Test read vector file keywords with the generic readKeywords method. """ self.maxDiff = None keywords = self.keyword_io.read_keywords(self.vector_layer) expected_keywords = self.expected_vector_keywords self.assertDictEqual(keywords, expected_keywords) def test_read_keywordless_layer(self): """Test read 'keyword' file from keywordless layer. """ self.assertRaises( NoKeywordsFoundError, self.keyword_io.read_keywords, self.keywordless_layer, ) def test_update_keywords(self): """Test append file keywords with update_keywords method.""" self.maxDiff = None layer = clone_raster_layer( name='tsunami_wgs84', extension='.tif', include_keywords=True, source_directory=standard_data_path('hazard')) layer.keywords = { 'hazard_category': u'single_event', 'title': u'tsunami_wgs84', 'keyword_version': u'3.5', 'hazard': u'tsunami', 'continuous_hazard_unit': u'metres', 'inasafe_fields': {}, 'layer_geometry': u'raster', 'layer_purpose': u'hazard', 'layer_mode': u'continuous', } new_keywords = { 'hazard_category': 'multiple_event' } self.keyword_io.update_keywords(layer, new_keywords) keywords = self.keyword_io.read_keywords(layer) expected_keywords = { 'hazard_category': 'multiple_event', 'title': 'tsunami_wgs84', 'hazard': 'tsunami', 'continuous_hazard_unit': 'metres', 'layer_geometry': 'raster', 'layer_purpose': 'hazard', 'layer_mode': 'continuous', 'thresholds': { 'road': { 'tsunami_hazard_classes': { 'active': True, 'classes': { 'dry': [0.0, 0.1], 'high': [3.0, 8.0], 'medium': [1.0, 3.0], 'low': [0.1, 1.0], 'very high': [8.0, 16.68] } } }, 'structure': { 'tsunami_hazard_classes': { 'active': True, 'classes': { 'dry': [0.0, 0.1], 'high': [3.0, 8.0], 'medium': [1.0, 3.0], 'low': [0.1, 1.0], 'very high': [8.0, 16.68] } } }, 'place': { 'tsunami_hazard_classes': { 'active': True, 'classes': { 'dry': [0.0, 0.1], 'high': [3.0, 8.0], 'medium': [1.0, 3.0], 'low': [0.1, 1.0], 'very high': [8.0, 16.68] } } }, 'land_cover': { 'tsunami_hazard_classes': { 'active': True, 'classes': { 'dry': [0.0, 0.1], 'high': [3.0, 8.0], 'medium': [1.0, 3.0], 'low': [0.1, 1.0], 'very high': [8.0, 16.68] } } }, 'population': { 'tsunami_hazard_classes': { 'active': True, 'classes': { 'dry': [0.0, 0.1], 'high': [3.0, 8.0], 'medium': [1.0, 3.0], 'low': [0.1, 1.0], 'very high': [8.0, 16.68] } } } }, 'keyword_version': inasafe_keyword_version } expected_thresholds = expected_keywords.pop('thresholds') expected_keywords = { k: get_unicode(v) for k, v in expected_keywords.iteritems() } thresholds_keywords = keywords.pop('thresholds') self.assertDictEqual(expected_keywords, keywords) self.assertDictEqual(expected_thresholds, thresholds_keywords) def test_copy_keywords(self): """Test we can copy the keywords.""" self.maxDiff = None out_path = unique_filename( prefix='test_copy_keywords', suffix='.shp') layer = clone_raster_layer( name='generic_continuous_flood', extension='.asc', include_keywords=True, source_directory=standard_data_path('hazard')) self.keyword_io.copy_keywords(layer, out_path) # copied_keywords = read_file_keywords(out_path.split('.')[0] + 'xml') copied_keywords = read_iso19115_metadata(out_path) expected_keywords = self.expected_raster_keywords expected_keywords['keyword_version'] = inasafe_keyword_version self.assertDictEqual(copied_keywords, expected_keywords) def test_to_message(self): """Test we can convert keywords to a message object. .. versionadded:: 3.2 """ keywords = self.keyword_io.read_keywords(self.vector_layer) message = self.keyword_io.to_message(keywords).to_text() self.assertIn('*Exposure*, structure------', message) def test_layer_to_message(self): """Test to show augmented keywords if KeywordsIO ctor passed a layer. .. versionadded:: 3.3 """ keywords = KeywordIO(self.vector_layer) message = keywords.to_message().to_text() self.assertIn('*Reference system*, ', message) def test_dict_to_row(self): """Test the dict to row helper works. .. versionadded:: 3.2 """ keyword_value = ( "{'high': ['Kawasan Rawan Bencana III'], " "'medium': ['Kawasan Rawan Bencana II'], " "'low': ['Kawasan Rawan Bencana I']}") table = self.keyword_io._dict_to_row(keyword_value) self.assertIn( u'\n---\n*High*, Kawasan Rawan Bencana III------', table.to_text()) # should also work passing a dict keyword_value = { 'high': ['Kawasan Rawan Bencana III'], 'medium': ['Kawasan Rawan Bencana II'], 'low': ['Kawasan Rawan Bencana I']} table = self.keyword_io._dict_to_row(keyword_value) self.assertIn( u'\n---\n*High*, Kawasan Rawan Bencana III------', table.to_text()) def test_keyword_io(self): """Test read keywords directly from keywords file .. versionadded:: 3.2 """ self.maxDiff = None keywords = self.keyword_io.read_keywords_file(self.keyword_path) expected_keywords = self.expected_vector_keywords self.assertDictEqual(keywords, expected_keywords)