Beispiel #1
0
    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
Beispiel #3
0
    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)
Beispiel #4
0
    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
Beispiel #5
0
    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)
Beispiel #7
0
    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)
Beispiel #8
0
    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)
Beispiel #11
0
    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()
Beispiel #13
0
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'
Beispiel #14
0
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'] = {}
Beispiel #15
0
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')
Beispiel #16
0
    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()
Beispiel #17
0
    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()
Beispiel #18
0
    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 = {}
Beispiel #19
0
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
Beispiel #20
0
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
Beispiel #21
0
    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()
Beispiel #23
0
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
Beispiel #24
0
    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)
Beispiel #25
0
    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')
Beispiel #26
0
    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)
Beispiel #31
0
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
Beispiel #32
0
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:
Beispiel #33
0
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)
Beispiel #35
0
    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)
Beispiel #36
0
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
Beispiel #37
0
    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()
Beispiel #38
0
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()
Beispiel #39
0
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
Beispiel #40
0
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())
Beispiel #41
0
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)
Beispiel #42
0
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)
Beispiel #43
0
    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
Beispiel #44
0
    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'))
Beispiel #45
0
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
Beispiel #46
0
    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)
Beispiel #49
0
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
Beispiel #50
0
    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)
Beispiel #51
0
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
Beispiel #52
0
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}
Beispiel #54
0
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)