Пример #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
Пример #2
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
Пример #3
0
    def generate_population_aggregation(self):

        # duplicate exposure data
        QgsVectorFileWriter.writeAsVectorFormat(
            self.exposure_layer.as_qgis_native(),
            self.population_aggregate_path, 'CP1250', None, 'ESRI Shapefile')
        population_aggregate = read_qgis_layer(self.population_aggregate_path,
                                               'Impacted Population')
        shutil.copy(self.population_path.replace('.shp', '.xml'),
                    self.population_aggregate_path.replace('.shp', '.xml'))

        # add affected population field
        population_aggregate.startEditing()
        field = QgsField(self.affect_field, QVariant.Int)
        field2 = QgsField(self.target_field, QVariant.Int)
        population_aggregate.dataProvider().addAttributes([field, field2])
        population_aggregate.commitChanges()

        idx = population_aggregate.fieldNameIndex(self.affect_field)
        idx2 = population_aggregate.fieldNameIndex(self.target_field)
        impact_layer = self.impact_layer.as_qgis_native()
        keyword_io = KeywordIO()
        name_field = keyword_io.read_keywords(population_aggregate,
                                              'area_name_field')
        attribute_field = keyword_io.read_keywords(population_aggregate,
                                                   'field')

        # calculate affected data
        district_dict = {}
        for f in impact_layer.getFeatures():
            if f[self.target_field] >= 1:
                if f[name_field] in district_dict:
                    district_dict[f[name_field]] += f[attribute_field]
                else:
                    district_dict[f[name_field]] = f[attribute_field]

        population_aggregate.startEditing()
        for f in population_aggregate.getFeatures():
            f[idx] = district_dict.get(f[name_field], 0)
            if f[idx] == 0:
                # mark as unaffected
                f[idx2] = 0
            else:
                # mark as affected
                f[idx2] = 1

            population_aggregate.updateFeature(f)
        population_aggregate.commitChanges()
        self.affected_aggregate = district_dict

        # calculate total affected people
        total_affected = 0
        for k, v in district_dict.iteritems():
            total_affected += v

        # calculate new minimum needs
        min_needs = self.impact_data.minimum_needs
        for k, v in min_needs.iteritems():
            for need in v:
                need['amount'] = need['value'] * total_affected
Пример #4
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
Пример #5
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
Пример #6
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
Пример #7
0
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)
Пример #8
0
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)
Пример #9
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')
Пример #10
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')
Пример #11
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
Пример #12
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
Пример #13
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
Пример #14
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
Пример #15
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'] = {}
Пример #16
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'] = {}
Пример #17
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'
Пример #18
0
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
Пример #19
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 = {}
Пример #20
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 = {}
Пример #21
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)
Пример #22
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)
Пример #23
0
class MetadataConverterDialog(QDialog, FORM_CLASS):

    """Dialog implementation class for the InaSAFE metadata converter tool."""

    resized = pyqtSignal()

    def __init__(self, parent=None, iface=None):
        """Constructor."""
        QDialog.__init__(self, parent)
        self.setupUi(self)
        icon = resources_path('img', 'icons', 'show-metadata-converter.svg')
        self.setWindowIcon(QIcon(icon))
        self.setWindowTitle(self.tr('InaSAFE Metadata Converter'))
        self.parent = parent
        self.iface = iface

        self.keyword_io = KeywordIO()
        self.layer = None

        # Setup header label
        self.header_label.setText(tr(
            'In this tool, you can convert a metadata 4.x of a layer to '
            'metadata 3.5. You will get a directory contains all the files of '
            'the layer and the new 3.5 metadata. If you want to convert '
            'hazard layer, you need to choose what exposure that you want to '
            'work with.')
        )

        # Setup input layer combo box
        # Filter our layers
        excepted_layers = []
        for i in range(self.input_layer_combo_box.count()):
            layer = self.input_layer_combo_box.layer(i)
            try:
                keywords = self.keyword_io.read_keywords(layer)
            except (KeywordNotFoundError, NoKeywordsFoundError):
                # Filter out if no keywords
                excepted_layers.append(layer)
                continue
            layer_purpose = keywords.get('layer_purpose')
            if not layer_purpose:
                # Filter out if no layer purpose
                excepted_layers.append(layer)
                continue
            if layer_purpose not in accepted_layer_purposes:
                # Filter out if not aggregation, hazard, or exposure layer
                excepted_layers.append(layer)
                continue
            keyword_version = keywords.get('keyword_version')
            if not keyword_version:
                # Filter out if no keyword version
                excepted_layers.append(layer)
                continue
            if not is_keyword_version_supported(keyword_version):
                # Filter out if the version is not supported (4.x)
                excepted_layers.append(layer)
                continue

        self.input_layer_combo_box.setExceptedLayerList(excepted_layers)

        # Select the active layer.
        if self.iface.activeLayer():
            found = self.input_layer_combo_box.findText(
                self.iface.activeLayer().name())
            if found > -1:
                self.input_layer_combo_box.setLayer(self.iface.activeLayer())

        # Set current layer as the active layer
        if self.input_layer_combo_box.currentLayer():
            self.set_layer(self.input_layer_combo_box.currentLayer())

        # Signals
        self.input_layer_combo_box.layerChanged.connect(self.set_layer)
        self.output_path_tool.clicked.connect(self.select_output_directory)

        # Set widget to show the main page (not help page)
        self.main_stacked_widget.setCurrentIndex(1)

        # Set up things for context help
        self.help_button = self.button_box.button(QDialogButtonBox.Help)
        # Allow toggling the help button
        self.help_button.setCheckable(True)
        self.help_button.toggled.connect(self.help_toggled)

        # Set up things for ok button
        self.ok_button = self.button_box.button(QDialogButtonBox.Ok)
        self.ok_button.clicked.connect(self.accept)

        # Set up things for cancel button
        self.cancel_button = self.button_box.button(QDialogButtonBox.Cancel)
        self.cancel_button.clicked.connect(self.reject)

        # The bug is fixed in QT 5.4
        if QT_VERSION < 0x050400:
            self.resized.connect(self.after_resize)

    def set_layer(self, layer=None):
        """Set layer and update UI accordingly.

        :param layer: A QgsVectorLayer.
        :type layer: QgsVectorLayer
        """
        if layer:
            self.layer = layer
        else:
            self.layer = self.input_layer_combo_box.currentLayer()
        if not self.layer:
            return

        try:
            keywords = self.keyword_io.read_keywords(layer)
            self.show_current_metadata()
        except NoKeywordsFoundError:
            # FIXME (elpaso)
            keywords = {}

        # TODO(IS): Show only possible exposure target
        if keywords.get('layer_purpose', False) == layer_purpose_hazard['key']:
            self.target_exposure_label.setEnabled(True)
            self.target_exposure_combo_box.setEnabled(True)
            self.target_exposure_combo_box.clear()
            for exposure in exposure_all:
                # Only show exposure that has active classification
                if active_classification(keywords, exposure['key']):
                    self.target_exposure_combo_box.addItem(
                        exposure['name'], exposure['key'])
        else:
            self.target_exposure_label.setEnabled(False)
            self.target_exposure_combo_box.setEnabled(False)
            self.target_exposure_combo_box.clear()
            self.target_exposure_combo_box.addItem(tr("Not Applicable"))

    def help_toggled(self, flag):
        """Show or hide the help tab in the stacked widget.

        .. versionadded: 3.2.1

        :param flag: Flag indicating whether help should be shown or hidden.
        :type flag: bool
        """
        if flag:
            self.help_button.setText(self.tr('Hide Help'))
            self.show_help()
        else:
            self.help_button.setText(self.tr('Show Help'))
            self.hide_help()

    def hide_help(self):
        """Hide the usage info from the user.

        .. versionadded: 3.2.1
        """
        self.main_stacked_widget.setCurrentIndex(1)

    def show_help(self):
        """Show usage info to the user."""
        # Read the header and footer html snippets
        self.main_stacked_widget.setCurrentIndex(0)
        header = html_header()
        footer = html_footer()
        message = metadata_converter_help()

        string = header
        string += message.to_html()
        string += footer

        self.help_web_view.setHtml(string)

    def convert_metadata(self):
        """Method invoked when OK button is clicked."""
        # Converter parameter
        converter_parameter = {}
        # Target exposure
        if self.target_exposure_combo_box.isEnabled():
            exposure_index = self.target_exposure_combo_box.currentIndex()
            exposure_key = self.target_exposure_combo_box.itemData(
                exposure_index, Qt.UserRole)
            converter_parameter['exposure'] = exposure_key

        # Metadata
        current_metadata = self.keyword_io.read_keywords(self.layer)
        old_metadata = convert_metadata(
            current_metadata, **converter_parameter)

        # Input
        input_layer_path = self.layer.source()
        input_directory_path = os.path.dirname(input_layer_path)
        input_file_name = os.path.basename(input_layer_path)
        input_base_name = os.path.splitext(input_file_name)[0]
        input_extension = os.path.splitext(input_file_name)[1]

        # Output
        output_path = self.output_path_line_edit.text()
        output_directory_path = os.path.dirname(output_path)
        output_file_name = os.path.basename(output_path)
        output_base_name = os.path.splitext(output_file_name)[0]

        # Copy all related files, if exists
        extensions = [
            # Vector layer
            '.shp', '.geojson', '.qml', '.shx', '.dbf', '.prj', 'qpj',
            # Raster layer
            '.tif', '.tiff', '.asc',
        ]
        for extension in extensions:
            source_path = os.path.join(
                input_directory_path, input_base_name + extension)
            if not os.path.exists(source_path):
                continue
            target_path = os.path.join(
                output_directory_path, output_base_name + extension)
            QFile.copy(source_path, target_path)

        # Replace the metadata with the old one
        output_file_path = os.path.join(
            output_directory_path, output_base_name + input_extension
        )
        write_iso19115_metadata(
            output_file_path, old_metadata, version_35=True)

    def accept(self):
        """Method invoked when OK button is clicked."""
        output_path = self.output_path_line_edit.text()
        if not output_path:
            display_warning_message_box(
                self,
                tr('Empty Output Path'),
                tr('Output path can not be empty'))
            return
        try:
            self.convert_metadata()
        except MetadataConversionError as e:
            display_warning_message_box(
                self,
                tr('Metadata Conversion Failed'),
                str(e))
            return

        if not os.path.exists(output_path):
            display_warning_message_box(
                self,
                tr('Metadata Conversion Failed'),
                tr('Result file is not found.'))
            return

        display_success_message_bar(
            tr('Metadata Conversion Success'),
            tr('You can find your copied layer with metadata version 3.5 in '
               '%s' % output_path),
            iface_object=self.iface
        )
        super(MetadataConverterDialog, self).accept()

    def select_output_directory(self):
        """Select output directory"""
        # Input layer
        input_layer_path = self.layer.source()
        input_file_name = os.path.basename(input_layer_path)
        input_extension = os.path.splitext(input_file_name)[1]

        # Get current path
        current_file_path = self.output_path_line_edit.text()
        if not current_file_path or not os.path.exists(current_file_path):
            current_file_path = input_layer_path

        # Filtering based on input layer
        extension_mapping = {
            '.shp': tr('Shapefile (*.shp);;'),
            '.geojson': tr('GeoJSON (*.geojson);;'),
            '.tif': tr('Raster TIF/TIFF (*.tif, *.tiff);;'),
            '.tiff': tr('Raster TIF/TIFF (*.tiff, *.tiff);;'),
            '.asc': tr('Raster ASCII File (*.asc);;'),
        }
        # Open File Dialog
        file_path, __ = QFileDialog.getSaveFileName(
            self,
            tr('Output File'),
            current_file_path,
            extension_mapping[input_extension]
        )
        if file_path:
            self.output_path_line_edit.setText(file_path)

    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)

    # Adapted from https://stackoverflow.com/a/43126946/1198772
    def resizeEvent(self, event):
        """Emit custom signal when the window is re-sized.

        :param event: The re-sized event.
        :type event: QResizeEvent
        """
        self.resized.emit()
        return super(MetadataConverterDialog, self).resizeEvent(event)

    def after_resize(self):
        """Method after resizing the window."""
        # https://stackoverflow.com/q/25644026/1198772
        # It's fixed in QT 5.4 https://bugreports.qt.io/browse/QTBUG-37673
        max_height = self.height() - 275  # Magic number, to make it pretty
        self.metadata_preview_web_view.setMaximumHeight(max_height)
Пример #24
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.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
        self.analysis.impact_function_id = self.parent.selected_function(
        )['id']
        self.analysis.impact_function_parameters = self.parent.if_params

        # 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:
Пример #25
0
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
        }
Пример #26
0
class ImpactReport(object):
    """A class for creating report using QgsComposition."""
    def __init__(self, iface, template, layer, extra_layers=[]):
        """Constructor for the Composition Report class.

        :param iface: Reference to the QGIS iface object.
        :type iface: QgsAppInterface

        :param template: The QGIS template path.
        :type template: str
        """
        LOGGER.debug('InaSAFE Impact Report class initialised')
        self._iface = iface
        self._template = None
        self.template = template
        self._layer = layer
        self._extra_layers = extra_layers
        self._extent = self._iface.mapCanvas().extent()
        self._page_dpi = 300.0
        self._black_inasafe_logo = black_inasafe_logo_path()
        self._white_inasafe_logo = white_inasafe_logo_path()
        # User can change this path in preferences
        self._organisation_logo = supporters_logo_path()
        self._supporters_logo = supporters_logo_path()
        self._north_arrow = default_north_arrow_path()
        self._disclaimer = disclaimer()

        # For QGIS < 2.4 compatibility
        # QgsMapSettings is added in 2.4
        if qgis_version() < 20400:
            map_settings = self._iface.mapCanvas().mapRenderer()
        else:
            map_settings = self._iface.mapCanvas().mapSettings()

        self._template_composition = TemplateComposition(
            template_path=self.template,
            map_settings=map_settings)
        self._keyword_io = KeywordIO()

    @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, basestring) and os.path.exists(template):
            self._template = template
        else:
            self._template = resources_path(
                'qgis-composer-templates', 'a4-portrait-blue.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 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 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, basestring) and os.path.exists(
                north_arrow_path):
            self._north_arrow = north_arrow_path
        else:
            self._north_arrow = default_north_arrow_path()

    @property
    def inasafe_logo(self):
        """Getter to safe logo path.

        .. versionchanged:: 3.2 - this property is now read only.
        """
        return self._black_inasafe_logo

    @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, basestring) and os.path.exists(logo):
            self._organisation_logo = logo
        else:
            self._organisation_logo = supporters_logo_path()

    @property
    def supporters_logo(self):
        """Getter to supporters logo path - this is a read only property.

        It always returns the InaSAFE supporters logo unlike the organisation
        logo which is customisable.

        .. versionadded:: 3.2
        """
        return self._supporters_logo

    @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, basestring):
            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,  # deprecated
            'disclaimer': self.disclaimer,
            # These added in 3.2
            'version-title': tr('Version'),
            'inasafe-version': version,
            'disclaimer-title': tr('Disclaimer'),
            'date-title': tr('Date'),
            'time-title': tr('Time'),
            'caution-title': tr('Note'),
            'caution-text': tr(
                'This assessment is a guide - we strongly recommend that you '
                'ground truth the results shown here before deploying '
                'resources and / or personnel.'),
            'version-text': tr(
                'Assessment carried out using InaSAFE release %s.' % version),
            'legend-title': tr('Legend'),
            'information-title': tr('Analysis information'),
            'supporters-title': tr('Report produced by')
        }

        # Load template
        self._template_composition.substitution = substitution_map
        try:
            self._template_composition.load_template()
        except TemplateLoadingError:
            raise

    @staticmethod
    def symbol_count(layer):
        """Get symbol count from whatever method we can get

        :param layer: QgsMapLayer
        :return: QgsMapLayer
        """
        try:
            return len(layer.legendSymbologyItems())
        except:
            pass

        try:
            return len(layer.rendererV2().legendSymbolItemsV2())
        except:
            pass

        return 1

    def draw_composition(self):
        """Draw all the components in the composition."""
        # This is deprecated - use inasafe-logo-<colour> rather
        safe_logo = self.composition.getComposerItemById(
            'safe-logo')
        # Next two options replace safe logo in 3.2
        black_inasafe_logo = self.composition.getComposerItemById(
            'black-inasafe-logo')
        white_inasafe_logo = self.composition.getComposerItemById(
            'white-inasafe-logo')
        north_arrow = self.composition.getComposerItemById(
            'north-arrow')
        organisation_logo = self.composition.getComposerItemById(
            'organisation-logo')
        supporters_logo = self.composition.getComposerItemById(
            'supporters-logo')

        if qgis_version() < 20600:
            if safe_logo is not None:
                # its deprecated so just use black_inasafe_logo
                safe_logo.setPictureFile(self.inasafe_logo)
            if black_inasafe_logo is not None:
                black_inasafe_logo.setPictureFile(self._black_inasafe_logo)
            if white_inasafe_logo is not None:
                white_inasafe_logo.setPictureFile(self._white_inasafe_logo)
            if north_arrow is not None:
                north_arrow.setPictureFile(self.north_arrow)
            if organisation_logo is not None:
                organisation_logo.setPictureFile(self.organisation_logo)
            if supporters_logo is not None:
                supporters_logo.setPictureFile(self.supporters_logo)
        else:
            if safe_logo is not None:
                # its deprecated so just use black_inasafe_logo
                safe_logo.setPicturePath(self.inasafe_logo)
            if black_inasafe_logo is not None:
                black_inasafe_logo.setPicturePath(self._black_inasafe_logo)
            if white_inasafe_logo is not None:
                white_inasafe_logo.setPicturePath(self._white_inasafe_logo)
            if north_arrow is not None:
                north_arrow.setPicturePath(self.north_arrow)
            if organisation_logo is not None:
                organisation_logo.setPicturePath(self.organisation_logo)
            if supporters_logo is not None:
                supporters_logo.setPicturePath(self.supporters_logo)

        # Set impact report table
        table = self.composition.getComposerItemById('impact-report')
        if table is not None:
            text = self._keyword_io.read_keywords(self.layer, 'impact_summary')
            if text is None:
                text = ''
            table.setText(text)
            table.setHtmlState(1)

        # Get the main map canvas on the composition and set its extents to
        # the event.
        composer_map = self.composition.getComposerItemById('impact-map')
        if composer_map is not None:
            # Recenter the composer map on the center of the extent
            # Note that since the composer map is square and the canvas may be
            # arbitrarily shaped, we center based on the longest edge
            canvas_extent = self.extent
            width = canvas_extent.width()
            height = canvas_extent.height()
            longest_width = width if width > height else height
            half_length = longest_width / 2
            center = canvas_extent.center()
            min_x = center.x() - half_length
            max_x = center.x() + half_length
            min_y = center.y() - half_length
            max_y = center.y() + half_length
            # noinspection PyCallingNonCallable
            square_extent = QgsRectangle(min_x, min_y, max_x, max_y)
            composer_map.setNewExtent(square_extent)

            # calculate intervals for grid
            split_count = 5
            x_interval = square_extent.width() / split_count
            composer_map.setGridIntervalX(x_interval)
            y_interval = square_extent.height() / split_count
            composer_map.setGridIntervalY(y_interval)

        legend = self.composition.getComposerItemById('impact-legend')
        if legend is not None:

            symbol_count = ImpactReport.symbol_count(self.layer)

            # add legend symbol count from extra_layers
            for l in self.extra_layers:
                symbol_count += ImpactReport.symbol_count(l)

            if symbol_count <= 5:
                legend.setColumnCount(1)
            else:
                legend.setColumnCount(symbol_count / 5 + 1)

            # Set back to blank to #2409
            legend.setTitle("")

            # Set Legend
            # Since QGIS 2.6, legend.model() is obsolete
            if qgis_version() < 20600:
                layer_set = [self.layer.id()]
                layer_set += [l.id() for l in self.extra_layers]
                legend.model().setLayerSet(layer_set)
                legend.synchronizeWithModel()
            else:
                root_group = legend.modelV2().rootGroup()
                root_group.addLayer(self.layer)
                for l in self.extra_layers:
                    root_group.addLayer(l)
                legend.synchronizeWithModel()

    def 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, unicode

        :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())

        try:
            impact_template = get_report_template(self.layer.source())
            summary_table = impact_template.generate_html_report()
        except:
            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 = m.Brand().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
        # TODO: Will break when we try to use larger print layouts TS
        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)
            # RMN: This line below breaks in InaSAFE Headless after one
            # successful call. This is because the function is not
            # thread safe. Can't do anything about this, so avoid calling this
            # function in multithreaded way.
            html_item.loadHtml()

        composition.exportAsPDF(output_path)
        return output_path
Пример #27
0
def buffer_points(point_layer, radii, hazard_zone_attribute, output_crs):
    """Buffer points for each point with defined radii.

    This function is used for making buffer of volcano point hazard.

    :param point_layer: A point layer to buffer.
    :type point_layer: QgsVectorLayer

    :param radii: Desired approximate radii in kilometers (must be
        monotonically ascending). Can be either one number or list of numbers
    :type radii: int, list

    :param hazard_zone_attribute: The name of the attributes representing
        hazard zone.
    :type hazard_zone_attribute: str

    :param output_crs: The output CRS.
    :type output_crs: QgsCoordinateReferenceSystem

    :return: Vector polygon layer representing circle in point layer CRS.
    :rtype: QgsVectorLayer
    """
    if not isinstance(radii, list):
        radii = [radii]

    if not is_point_layer(point_layer):
        message = ('Input hazard must be a vector point layer. I got %s '
                   'with layer type %s' %
                   (point_layer.name(), point_layer.type()))
        raise Exception(message)

    # Check that radii are monotonically increasing
    monotonically_increasing_flag = all(x < y
                                        for x, y in zip(radii, radii[1:]))
    if not monotonically_increasing_flag:
        raise RadiiException(RadiiException.suggestion)

    hazard_file_path = unique_filename(suffix='-polygon-volcano.shp')
    fields = point_layer.pendingFields()
    fields.append(QgsField(hazard_zone_attribute, QVariant.Double))
    writer = QgsVectorFileWriter(hazard_file_path, 'utf-8', fields,
                                 QGis.WKBPolygon, output_crs, 'ESRI Shapefile')
    input_crs = point_layer.crs()

    center = point_layer.extent().center()
    utm = None
    if output_crs.authid() == 'EPSG:4326':
        utm = QgsCoordinateReferenceSystem(
            get_utm_epsg(center.x(), center.y(), input_crs))
        transform = QgsCoordinateTransform(point_layer.crs(), utm)

    else:
        transform = QgsCoordinateTransform(point_layer.crs(), output_crs)

    for point in point_layer.getFeatures():
        geom = point.geometry()
        geom.transform(transform)

        inner_rings = None
        for radius in radii:
            attributes = point.attributes()
            # Generate circle polygon

            circle = geom.buffer(radius * 1000.0, 30)

            if inner_rings:
                circle.addRing(inner_rings)
            inner_rings = circle.asPolygon()[0]

            new_buffer = QgsFeature()

            if output_crs.authid() == 'EPSG:4326':
                circle.transform(QgsCoordinateTransform(utm, output_crs))

            new_buffer.setGeometry(circle)
            attributes.append(radius)
            new_buffer.setAttributes(attributes)

            writer.addFeature(new_buffer)

    del writer
    vector_layer = QgsVectorLayer(hazard_file_path, 'Polygons', 'ogr')

    keyword_io = KeywordIO()
    try:
        keywords = keyword_io.read_keywords(point_layer)
        keyword_io.write_keywords(vector_layer, keywords)
    except NoKeywordsFoundError:
        pass
    return vector_layer
Пример #28
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 = {'datatype': 'OSM'}

        # Raster Layer keywords
        hazard_path = test_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.2'
        }

        # Vector Layer keywords
        vector_path = test_data_path('exposure', 'buildings_osm_4326.shp')
        self.vector_layer, _ = load_layer(vector_path)
        self.expected_vector_keywords = {
            'keyword_version': '3.3',
            'structure_class_field': 'FLOODED',
            'title': 'buildings_osm_4326',
            'layer_geometry': 'polygon',
            'layer_purpose': 'exposure',
            'layer_mode': 'classified',
            'exposure': 'structure'
        }
        # Keyword less layer
        keywordless_path = test_data_path('other', 'keywordless_layer.shp')
        self.keywordless_layer, _ = load_layer(keywordless_path)
        # Keyword file
        self.keyword_path = test_data_path('exposure',
                                           'buildings_osm_4326.xml')

    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)
        self.assertEqual(hash_value, expected_hash, message)

    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
        """
        layer = clone_raster_layer(name='generic_continuous_flood',
                                   extension='.asc',
                                   include_keywords=True,
                                   source_directory=test_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.
         """
        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."""
        layer = clone_raster_layer(name='tsunami_wgs84',
                                   extension='.tif',
                                   include_keywords=True,
                                   source_directory=test_data_path('hazard'))
        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',
            'hazard': 'tsunami',
            'continuous_hazard_unit': 'metres',
            'layer_geometry': 'raster',
            'layer_purpose': 'hazard',
            'layer_mode': 'continuous',
            'keyword_version': inasafe_keyword_version
        }
        expected_keywords = {
            k: get_unicode(v)
            for k, v in expected_keywords.iteritems()
        }
        self.maxDiff = None
        self.assertDictEqual(keywords, expected_keywords)

    @unittest.skip('No longer used in the new metadata.')
    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=')

        self.assertEqual(sqlite_layer.source(), expected_source)

        keywords = self.keyword_io.read_keywords(sqlite_layer)
        expected_keywords = self.expected_sqlite_keywords

        self.assertDictEqual(keywords, expected_keywords)

        # 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='.shp')
        layer = clone_raster_layer(name='generic_continuous_flood',
                                   extension='.asc',
                                   include_keywords=True,
                                   source_directory=test_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.maxDiff = None
        self.assertDictEqual(copied_keywords, expected_keywords)

    def test_definition(self):
        """Test we can get definitions for keywords.

        .. versionadded:: 3.2

        """
        keyword = 'hazards'
        keyword_definition = definition(keyword)
        self.assertTrue('description' in keyword_definition)

    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
        """
        keywords = self.keyword_io.read_keywords_file(self.keyword_path)
        expected_keywords = self.expected_vector_keywords
        message = 'Got:\n%s\nExpected:\n%s\nSource:\n%s' % (
            keywords, expected_keywords, self.keyword_path)
        self.assertDictEqual(keywords, expected_keywords, message)
Пример #29
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.function_id = None
        self.keyword_io = KeywordIO()

        # Calling some init methods
        self.restore_state()

    def restore_state(self):
        """ Read last state of GUI from configuration file."""
        settings = QSettings()
        try:
            last_save_directory = settings.value(
                'inasafe/lastSourceDir', '.', type=str)
        except TypeError:
            last_save_directory = ''
        self.output_directory = last_save_directory

    def save_state(self):
        """ Store current state of GUI to configuration file """
        settings = QSettings()
        settings.setValue('inasafe/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 = self.dock.get_exposure_layer()
        self.hazard_layer = self.dock.get_hazard_layer()
        self.function_id = self.dock.get_function_id(
            self.dock.cboFunction.currentIndex())
        self.aggregation_layer = self.dock.get_aggregation_layer()

        is_valid = True
        warning_message = None
        if self.exposure_layer is None:
            warning_message = self.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 = self.tr(
                'Hazard layer is not found, can not save scenario. Please add '
                'hazard layer to do so.')
            is_valid = False

        if self.function_id == '' or self.function_id is None:
            warning_message = self.tr(
                'The impact function is empty, can not save scenario')
            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 = self.tr('InaSAFE Save Scenario Warning')
        is_valid, warning_message = self.validate_input()
        if not is_valid:
            # noinspection PyCallByClass,PyTypeChecker,PyArgumentList
            QtGui.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.user_extent_crs is not None:
            extent = extent_to_array(
                self.dock.extent.user_extent,
                self.dock.extent.user_extent_crs)
        else:
            extent = viewport_geo_array(self.iface.mapCanvas())
        extent_string = ', '.join(('%f' % x) for x in extent)

        exposure_path = self.exposure_layer.publicSource()
        hazard_path = self.hazard_layer.publicSource()
        title = self.keyword_io.read_keywords(self.hazard_layer, 'title')
        title = self.tr(title)
        default_filename = title.replace(
            ' ', '_').replace('(', '').replace(')', '')

        # Popup a dialog to request the filename if scenario_file_path = None
        dialog_title = self.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)

        # Get relative path for each layer
        relative_exposure_path = self.relative_path(
            scenario_file_path, exposure_path)
        relative_hazard_path = self.relative_path(
            scenario_file_path, hazard_path)

        #  Write to file
        parser = ConfigParser()
        parser.add_section(title)
        parser.set(title, 'exposure', relative_exposure_path)
        parser.set(title, 'hazard', relative_hazard_path)
        parser.set(title, 'function', self.function_id)
        parser.set(title, 'extent', extent_string)
        if self.dock.extent.user_extent_crs is None:
            parser.set(title, 'extent_crs',
                       'EPSG:4326')
        else:
            parser.set(
                title,
                'extent_crs',
                self.dock.extent.user_extent_crs.authid())
        if self.aggregation_layer is not None:
            aggregation_path = self.aggregation_layer.publicSource()
            relative_aggregation_path = self.relative_path(
                scenario_file_path, aggregation_path)
            parser.set(title, 'aggregation', relative_aggregation_path)

        try:
            parser.write(open(scenario_file_path, 'a'))
        except IOError:
            # noinspection PyTypeChecker,PyCallByClass,PyArgumentList
            QtGui.QMessageBox.warning(
                self,
                self.tr('InaSAFE'),
                self.tr('Failed to save scenario to ' + scenario_file_path))

        # 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, e:
            # LOGGER.info(e.message)
            relative_path = input_path
        return relative_path
Пример #30
0
class FieldMappingDialog(QDialog, FORM_CLASS):

    """Dialog implementation class for the InaSAFE field mapping tool."""

    def __init__(self, parent=None, iface=None, setting=None):
        """Constructor."""
        QDialog.__init__(self, parent)
        self.setupUi(self)

        self.setWindowTitle(self.tr('InaSAFE Field Mapping Tool'))
        icon = resources_path('img', 'icons', 'show-mapping-tool.svg')
        self.setWindowIcon(QIcon(icon))
        self.parent = parent
        self.iface = iface
        if setting is None:
            setting = QSettings()
        self.setting = setting

        self.keyword_io = KeywordIO()

        self.layer = None
        self.metadata = {}

        self.layer_input_layout = QHBoxLayout()
        self.layer_label = QLabel(tr('Layer'))
        self.layer_combo_box = QgsMapLayerComboBox()
        # Filter only for Polygon and Point
        self.layer_combo_box.setFilters(
            QgsMapLayerProxyModel.PolygonLayer
            | QgsMapLayerProxyModel.PointLayer)
        # Filter out a layer that don't have layer groups
        excepted_layers = []
        for i in range(self.layer_combo_box.count()):
            layer = self.layer_combo_box.layer(i)
            try:
                keywords = self.keyword_io.read_keywords(layer)
            except (KeywordNotFoundError, NoKeywordsFoundError):
                excepted_layers.append(layer)
                continue
            layer_purpose = keywords.get('layer_purpose')
            if not layer_purpose:
                excepted_layers.append(layer)
                continue
            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:
                excepted_layers.append(layer)
                continue
        self.layer_combo_box.setExceptedLayerList(excepted_layers)

        # Select the active layer.
        if self.iface.activeLayer():
            found = self.layer_combo_box.findText(
                self.iface.activeLayer().name())
            if found > -1:
                self.layer_combo_box.setLayer(self.iface.activeLayer())

        self.field_mapping_widget = None
        self.main_stacked_widget.setCurrentIndex(1)

        # Input
        self.layer_input_layout.addWidget(self.layer_label)
        self.layer_input_layout.addWidget(self.layer_combo_box)

        self.header_label = QLabel()
        self.header_label.setWordWrap(True)
        self.main_layout.addWidget(self.header_label)
        self.main_layout.addLayout(self.layer_input_layout)

        # Signal
        self.layer_combo_box.layerChanged.connect(self.set_layer)

        if self.layer_combo_box.currentLayer():
            self.set_layer(self.layer_combo_box.currentLayer())

        # Set up things for context help
        self.help_button = self.button_box.button(QDialogButtonBox.Help)
        # Allow toggling the help button
        self.help_button.setCheckable(True)
        self.help_button.toggled.connect(self.help_toggled)

        # Set up things for ok button
        self.ok_button = self.button_box.button(QDialogButtonBox.Ok)
        self.ok_button.clicked.connect(self.accept)

        # Set up things for cancel button
        self.cancel_button = self.button_box.button(QDialogButtonBox.Cancel)
        self.cancel_button.clicked.connect(self.reject)

    def set_layer(self, layer=None, keywords=None):
        """Set layer and update UI accordingly.

        :param layer: A QgsVectorLayer.
        :type layer: QgsVectorLayer

        :param keywords: Keywords for the layer.
        :type keywords: dict, None
        """
        if self.field_mapping_widget is not None:
            self.field_mapping_widget.setParent(None)
            self.field_mapping_widget.close()
            self.field_mapping_widget.deleteLater()
            self.main_layout.removeWidget(self.field_mapping_widget)
            self.field_mapping_widget = None

        if layer:
            self.layer = layer
        else:
            self.layer = self.layer_combo_box.currentLayer()
        if not self.layer:
            return

        if keywords is not None:
            self.metadata = keywords
        else:
            # Always read from metadata file.
            try:
                self.metadata = self.keyword_io.read_keywords(self.layer)
            except (
                    NoKeywordsFoundError,
                    KeywordNotFoundError,
                    MetadataReadError) as e:
                raise e
        if 'inasafe_default_values' not in self.metadata:
            self.metadata['inasafe_default_values'] = {}
        if 'inasafe_fields' not in self.metadata:
            self.metadata['inasafe_fields'] = {}
        self.field_mapping_widget = FieldMappingWidget(
            parent=self, iface=self.iface)
        self.field_mapping_widget.set_layer(self.layer, self.metadata)
        self.field_mapping_widget.show()
        self.main_layout.addWidget(self.field_mapping_widget)

        # Set header label
        group_names = [
            self.field_mapping_widget.tabText(i) for i in range(
                self.field_mapping_widget.count())]
        if len(group_names) == 0:
            header_text = tr(
                'There is no field group for this layer. Please select '
                'another layer.')
            self.header_label.setText(header_text)
            return
        elif len(group_names) == 1:
            pretty_group_name = group_names[0]
        elif len(group_names) == 2:
            pretty_group_name = group_names[0] + tr(' and ') + group_names[1]
        else:
            pretty_group_name = ', '.join(group_names[:-1])
            pretty_group_name += tr(', and {0}').format(group_names[-1])
        header_text = tr(
            'Please fill the information for every tab to determine the '
            'attribute for {0} group.').format(pretty_group_name)
        self.header_label.setText(header_text)

    @pyqtSlot(bool)  # prevents actions being handled twice
    def help_toggled(self, flag):
        """Show or hide the help tab in the stacked widget.

        .. versionadded: 3.2.1

        :param flag: Flag indicating whether help should be shown or hidden.
        :type flag: bool
        """
        if flag:
            self.help_button.setText(self.tr('Hide Help'))
            self.show_help()
        else:
            self.help_button.setText(self.tr('Show Help'))
            self.hide_help()

    def hide_help(self):
        """Hide the usage info from the user.

        .. versionadded: 3.2.1
        """
        self.main_stacked_widget.setCurrentIndex(1)

    def show_help(self):
        """Show usage info to the user."""
        # Read the header and footer html snippets
        self.main_stacked_widget.setCurrentIndex(0)
        header = html_header()
        footer = html_footer()

        string = header

        message = field_mapping_help()

        string += message.to_html()
        string += footer

        self.help_web_view.setHtml(string)

    def save_metadata(self):
        """Save metadata based on the field mapping state."""
        metadata = self.field_mapping_widget.get_field_mapping()
        for key, value in list(metadata['fields'].items()):
            # Delete the key if it's set to None
            if key in self.metadata['inasafe_default_values']:
                self.metadata['inasafe_default_values'].pop(key)
            if value is None or value == []:
                if key in self.metadata['inasafe_fields']:
                    self.metadata['inasafe_fields'].pop(key)
            else:
                self.metadata['inasafe_fields'][key] = value

        for key, value in list(metadata['values'].items()):
            # Delete the key if it's set to None
            if key in self.metadata['inasafe_fields']:
                self.metadata['inasafe_fields'].pop(key)
            if value is None:
                if key in self.metadata['inasafe_default_values']:
                    self.metadata['inasafe_default_values'].pop(key)
            else:
                self.metadata['inasafe_default_values'][key] = value

        # Save metadata
        try:
            self.keyword_io.write_keywords(
                layer=self.layer, keywords=self.metadata)
        except InaSAFEError as e:
            error_message = get_error_message(e)
            # noinspection PyCallByClass,PyTypeChecker,PyArgumentList
            QMessageBox.warning(
                self, self.tr('InaSAFE'),
                ((self.tr(
                    'An error was encountered when saving the following '
                    'keywords:\n %s') % error_message.to_html())))

        # Update setting fir recent value
        if self.metadata.get('inasafe_default_values'):
            for key, value in \
                    list(self.metadata['inasafe_default_values'].items()):
                set_inasafe_default_value_qsetting(
                    self.setting, key, RECENT, value)

    def accept(self):
        """Method invoked when OK button is clicked."""
        try:
            self.save_metadata()
        except InvalidValidationException as e:
            display_warning_message_box(
                self, tr('Invalid Field Mapping'), str(e))
            return
        super(FieldMappingDialog, self).accept()
Пример #31
0
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 = definitions.global_default_attribute[
            'name']
        self.do_not_use_string = definitions.do_not_use_attribute['name']
        self.global_default_data = definitions.global_default_attribute['id']
        self.do_not_use_data = definitions.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, unicode

        :param value: Value part of the key value pair (kvp).
        :type value: str, unicode
        """
        if key is None or key == '':
            return

        # make sure that both key and value is unicode
        key = get_unicode(key)
        value = get_unicode(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
        """
        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 = tokens[0].strip()
            value = 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 keywords[key] == 'Use default':
                    self.add_list_entry(key, self.global_default_data)
                    continue
            self.add_list_entry(key, 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', 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', 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 self.leTitle.text() != '':
            self.add_list_entry('title', self.leTitle.text())

        # make sure the source is listed too
        if self.leSource.text() != '':
            self.add_list_entry('source', 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 = tokens[0].strip()
            value = 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)
Пример #32
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
Пример #33
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
Пример #34
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_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())
Пример #35
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
        try:
            key = post_processor[0][1].keys()[0]
        except IndexError:
            return -1
        # 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_data(self, aoi_mode=True):
        """Parses the postprocessing output as dictionary.

        :param aoi_mode: adds a Total in aggregation areas
        row to the calculated table
        :type aoi_mode: bool

        :returns: The dictionary of postprocessing.
        :rtype: dict
        """
        result = {}

        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 = {'notes': []}
            name = get_postprocessor_human_name(processor).lower()
            translated_name = tr(name)

            if name == 'building type':
                table['caption'] = tr('Closed buildings')
            elif name == 'road type':
                table['caption'] = tr('Closed roads')
            elif name == 'people':
                table['caption'] = tr('Affected people')

            # Dirty hack to make "evacuated" come 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'] = tr(
                        'Detailed %s report '
                        '(for people needing evacuation)') % translated_name
                else:
                    table['caption'] = tr(
                        'Detailed %s report '
                        '(affected people)') % translated_name

            if processor in ['Gender', 'Age']:
                table['caption'] = tr('Detailed %s report '
                                      '(affected people)') % translated_name

            try:
                empty_table = not sorted_results[0][1]
            except IndexError:
                empty_table = True
            if empty_table:
                # The table is empty.
                # Due to an error or because every lines were removed.
                table['attributes'] = []
                table['fields'] = []
                table['notes'].append(
                    tr('The report "%s" is empty.') % translated_name)
                result['processor'] = table
                continue

            header = [str(self.attribute_title).capitalize()]
            for calculation_name in sorted_results[0][1]:
                header.append(self.tr(calculation_name))
            table['attributes'] = header

            # used to calculate the totals row as per issue #690
            postprocessor_totals = OrderedDict()

            null_index = 0  # counting how many null value in the data
            fields = []
            for zone_name, calc in sorted_results:
                if isinstance(zone_name, QPyNullVariant):
                    # I have made sure that the zone_name won't be Null in
                    # run method. But just in case there is something wrong.
                    zone_name = tr('Unnamed Area %s' % null_index)
                    null_index += 1
                if name == 'road type':
                    # We add the unit 'meter' as we are counting roads.
                    zone_name = tr('%(zone_name)s (m)' %
                                   {'zone_name': zone_name})
                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
                        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.append(value)
                fields.append(row)

            if not aoi_mode:
                # add the totals row
                row = [self.tr('Total in aggregation areas')]
                for _, total in postprocessor_totals.iteritems():
                    row.append(total)
                fields.append(row)

            table['fields'] = fields

            if has_no_data:
                table['notes'].append(
                    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')))

            table['notes'].append(
                self.tr(
                    'Columns and rows containing only 0 or "%s" values are '
                    'excluded from the tables.' %
                    self.aggregator.get_default_keyword('NO_DATA')))
            result[processor] = table

        return result

    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')
            name = get_postprocessor_human_name(processor).lower()
            translated_name = tr(name)

            if name == 'building type':
                table.caption = tr('Closed buildings')
            elif name == 'road type':
                table.caption = tr('Closed roads')
            elif name == 'people':
                table.caption = tr('Affected people')

            # Dirty hack to make "evacuated" come 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 = tr(
                        'Detailed %s report '
                        '(for people needing evacuation)') % translated_name
                else:
                    table.caption = tr('Detailed %s report '
                                       '(affected people)') % translated_name

            if processor in ['Gender', 'Age']:
                table.caption = tr('Detailed %s report '
                                   '(affected people)') % translated_name

            empty_table = not sorted_results[0][1]
            if empty_table:
                # Due to an error? The table is empty.
                message.add(table)
                message.add(
                    m.EmphasizedText(
                        tr('Could not compute the %s report.') %
                        translated_name))
                continue

            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()

            null_index = 0  # counting how many null value in the data
            for zone_name, calc in sorted_results:
                if isinstance(zone_name, QPyNullVariant):
                    # I have made sure that the zone_name won't be Null in
                    # run method. But just in case there is something wrong.
                    zone_name = tr('Unnamed Area %s' % null_index)
                    null_index += 1
                if name == 'road type':
                    # We add the unit 'meter' as we are counting roads.
                    # proper format for i186
                    zone_name = tr('%(zone_name)s (m)') % {
                        'zone_name': tr(zone_name)
                    }
                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
                        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'))))
            caption = m.EmphasizedText(
                self.tr(
                    'Columns and rows containing only 0 or "%s" values are '
                    'excluded from the tables.' %
                    self.aggregator.get_default_keyword('NO_DATA')))
            message.add(m.Paragraph(caption, style_class='caption'))

        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)
        except (TypeError, KeyError):
            # TypeError is for when function_parameters is none
            # KeyError is for when ['postprocessors'] is unavailable
            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')

        # 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]
            if isinstance(zone_name, QPyNullVariant):
                # proper format for i186
                zone_name = tr('Unnamed Area %(feature_id)s') % {
                    'feature_id': str(feature.id())
                }

            # create dictionary of attributes to pass to postprocessor
            general_params = {
                'target_field': self.aggregator.target_field,
                'function_params': self.function_parameters
            }

            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
                user_parameters = self.function_parameters['postprocessors'][
                    key]
                user_parameters = dict([(user_parameter.name,
                                         user_parameter.value)
                                        for user_parameter in user_parameters])
                try:
                    # user parameters override default parameters
                    parameters.update(user_parameters)
                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):
                            LOGGER.debug(
                                '--- only default age ratios used ---')
                            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':
                    if key == 'BuildingType':
                        key_attribute = self.keyword_io.read_keywords(
                            self.aggregator.exposure_layer,
                            'structure_class_field')
                    elif key == 'RoadType':
                        key_attribute = self.keyword_io.read_keywords(
                            self.aggregator.exposure_layer, 'road_class_field')
                    else:
                        try:
                            key_attribute = self.keyword_io.read_keywords(
                                self.aggregator.exposure_layer,
                                'key_attribute')
                        except KeywordNotFoundError:
                            # use 'type' as default
                            key_attribute = 'type'

                    parameters['key_attribute'] = key_attribute
                    LOGGER.debug('key_attribute: %s', key_attribute)

                    value_map = self.keyword_io.read_keywords(
                        self.aggregator.exposure_layer, 'value_mapping')
                    parameters['value_mapping'] = value_map

                try:
                    value.setup(parameters)
                    value.process()
                    results = value.results()
                    value.clear()
                    if key not in self.output:
                        self.output[key] = []
                    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
            # increment the index
            polygon_index += 1
        self.remove_empty_columns()
        self.remove_empty_lines()

    def remove_empty_columns(self):
        """Removes empty columns from output, to reduce table width.

        Columns containing exclusively 0 and "No data" are removed.

        .. note:: This is intended to be a temporary solution to the excessive
        amount of data in some of the postprocessors.
        """
        for (_postprocessor_type, output) in self.output.items():
            keys = output[0][1].keys()
            for key in keys:
                total_for_type = 0
                for _area, breakdown in output:
                    value = breakdown[key]['value']
                    try:
                        total_for_type += int(value.replace(',', ''))
                    except ValueError:
                        # no need to increment for a no data value
                        no_data_value = self.aggregator.get_default_keyword(
                            'NO_DATA')
                        if value != no_data_value:
                            total_for_type += 1
                if total_for_type == 0:
                    for _area, breakdown in output:
                        breakdown.pop(key)

    def remove_empty_lines(self):
        """Remove empty lines from output, to reduce table height.

        Lines containing exclusively 0 and "No data" are removed.
        """
        def keep_area(row, elements):
            """Internal function to know if we should keep the row.

            :param row: The row
            :param elements : Columns heading.

            :return: Boolean if we should keep the row.
            """
            for element in elements:
                value = row[element]['value']
                try:
                    if int(value.replace(',', '')) != 0:
                        # If there is one integer, different than 0, we keep it
                        return True
                except ValueError:
                    pass
            else:
                # Only no data or zero values has been found, we remove it.
                return False

        index_to_remove = {}
        for (postprocessor_type, output) in self.output.items():
            index_to_remove[postprocessor_type] = []
            keys = output[0][1].keys()
            for i, line in enumerate(output):
                if not keep_area(line[1], keys):
                    index_to_remove[postprocessor_type].append(i)

        for postprocessor in index_to_remove:
            # As we are removing many indexes in the list,
            # we need to reverse the order to not disturb the lower indexes.
            for i in sorted(index_to_remove[postprocessor], reverse=True):
                del self.output[postprocessor][i]

    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,
                                              multipart_polygon_key)):
                self._consolidate_multipart_stats()
        except KeywordNotFoundError:
            pass

        message.add(self._generate_tables(aoi_mode))
        return message

    def get_json_data(self, aoi_mode):
        """Returns the results of the post processing as a dict.

        :param aoi_mode: aoi mode of the aggregator.
        :type aoi_mode: bool

        :returns: dictionary representing the post processing result.
        :rtype: dict
        """
        return self._generate_data(aoi_mode)
Пример #36
0
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}
Пример #37
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',
            'structure_class_field': 'FLOODED',
            'value_mapping': {},
            'title': 'buildings_osm_4326',
            'layer_geometry': 'polygon',
            'layer_purpose': 'exposure',
            'layer_mode': 'classified',
            'exposure': 'structure',
        }
        # Keyword less layer
        keywordless_path = standard_data_path('other', 'keywordless_layer.shp')
        self.keywordless_layer, _ = load_layer(keywordless_path)
        # Keyword file
        self.keyword_path = standard_data_path('exposure',
                                               'buildings_osm_4326.xml')

    def 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'))
        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',
            'hazard': 'tsunami',
            'continuous_hazard_unit': 'metres',
            'layer_geometry': 'raster',
            'layer_purpose': 'hazard',
            'layer_mode': 'continuous',
            'keyword_version': inasafe_keyword_version
        }
        expected_keywords = {
            k: get_unicode(v)
            for k, v in expected_keywords.iteritems()
        }
        self.assertDictEqual(keywords, expected_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_definition(self):
        """Test we can get definitions for keywords.

        .. versionadded:: 3.2

        """
        keyword = 'hazards'
        keyword_definition = definition(keyword)
        self.assertTrue('description' in keyword_definition)

    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)
Пример #38
0
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)
Пример #39
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()
Пример #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)
        self.expected_raster_keywords = {
            'hazard_category': 'single_event',
            'title': 'Generic Continuous Flood',
            'hazard': 'flood',
            'continuous_hazard_unit': 'generic',
            'layer_geometry': 'raster',
            'layer_purpose': 'hazard',
            'layer_mode': 'continuous',
            'keyword_version': '3.5'
        }

        # Vector Layer keywords
        vector_path = standard_data_path('exposure', 'buildings_osm_4326.shp')
        self.vector_layer, _ = load_layer(vector_path)
        self.expected_vector_keywords = {
            'keyword_version': '3.5',
            'structure_class_field': 'FLOODED',
            'value_mapping': {},
            'title': 'buildings_osm_4326',
            'layer_geometry': 'polygon',
            'layer_purpose': 'exposure',
            'layer_mode': 'classified',
            'exposure': 'structure',
        }
        # Keyword less layer
        keywordless_path = standard_data_path('other', 'keywordless_layer.shp')
        self.keywordless_layer, _ = load_layer(keywordless_path)
        # Keyword file
        self.keyword_path = standard_data_path(
            'exposure', 'buildings_osm_4326.xml')

    def 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'))
        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',
            'hazard': 'tsunami',
            'continuous_hazard_unit': 'metres',
            'layer_geometry': 'raster',
            'layer_purpose': 'hazard',
            'layer_mode': 'continuous',
            'keyword_version': inasafe_keyword_version
        }
        expected_keywords = {
            k: get_unicode(v) for k, v in expected_keywords.iteritems()
        }
        self.assertDictEqual(keywords, expected_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_definition(self):
        """Test we can get definitions for keywords.

        .. versionadded:: 3.2

        """
        keyword = 'hazards'
        keyword_definition = definition(keyword)
        self.assertTrue('description' in keyword_definition)

    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)
Пример #41
0
    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)
class ImpactMergeDialog(QDialog, FORM_CLASS):
    """Tools for merging 2 impact layer based on different exposure."""

    def __init__(self, parent=None, iface=None):
        """Constructor for dialog.

        :param parent: Optional widget to use as parent
        :type parent: QWidget

        :param iface: An instance of QGisInterface
        :type iface: QGisInterface
        """
        QDialog.__init__(self, parent)
        self.parent = parent
        self.setupUi(self)
        self.setWindowTitle(self.tr('InaSAFE Impact Layer Merge Tool'))
        self.iface = iface
        self.keyword_io = KeywordIO()

        # Template Path for composer
        self.template_path = resources_path(
            'qgis-composer-templates', 'merged_report.qpt')

        # Safe Logo Path
        self.safe_logo_path = white_inasafe_logo_path()

        # Organisation Logo Path - defaults to supporters logo, will be
        # updated to user defined organisation logo path in read_settings in
        # user has specified a custom logo.
        self.organisation_logo_path = supporters_logo_path()

        # Disclaimer text
        self.disclaimer = disclaimer()

        # The output directory
        self.out_dir = None

        # Stored information from first impact layer
        self.first_impact = {
            'layer': None,
            'map_title': None,
            'hazard_title': None,
            'exposure_title': None,
            'postprocessing_report': None,
        }

        # Stored information from second impact layer
        self.second_impact = {
            'layer': None,
            'map_title': None,
            'hazard_title': None,
            'exposure_title': None,
            'postprocessing_report': None,
        }

        # Stored information from aggregation layer
        self.aggregation = {
            'layer': None,
            'aggregation_attribute': None
        }

        # Available aggregation layer
        self.available_aggregation = []

        # The summary report, contains report for each aggregation area
        self.summary_report = OrderedDict()

        # The html reports and its file path
        self.html_reports = OrderedDict()

        # A boolean flag whether to merge entire area or aggregated
        self.entire_area_mode = False

        # Get the global settings and override some variable if exist
        self.read_settings()

        # Get all current project layers for combo box
        self.get_project_layers()

        # 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)

        # Show usage info
        self.restore_state()

    def restore_state(self):
        """ Read last state of GUI from configuration file."""
        settings = QSettings()
        try:
            last_path = settings.value('directory', type=str)
        except TypeError:
            last_path = ''
        self.output_directory.setText(last_path)

    def save_state(self):
        """ Store current state of GUI to configuration file """
        settings = QSettings()
        settings.setValue('directory', self.output_directory.text())

    @pyqtSignature('')  # prevents actions being handled twice
    def on_directory_chooser_clicked(self):
        """Show a dialog to choose directory."""
        # noinspection PyCallByClass,PyTypeChecker
        self.output_directory.setText(
            QFileDialog.getExistingDirectory(
                self, self.tr('Select Output Directory')))

    @pyqtSignature('')  # prevents actions being handled twice
    def on_report_template_chooser_clicked(self):
        """Show a dialog to choose directory"""
        # noinspection PyCallByClass,PyTypeChecker
        report_template_path = QtGui.QFileDialog.getOpenFileName(
            self,
            self.tr('Select Report Template'),
            self.template_path,
            self.tr('QPT File (*.qpt)'))

        # noinspection PyCallByClass,PyTypeChecker
        self.report_template_le.setText(report_template_path)

    def accept(self):
        """Do merging two impact layers."""
        # Store the current state to configuration file
        self.save_state()

        # Prepare all the input from dialog, validate, and store it
        try:
            self.prepare_input()
        except (InvalidLayerError,
                EmptyDirectoryError,
                FileNotFoundError) as ex:
            # noinspection PyCallByClass,PyTypeChecker, PyArgumentList
            QMessageBox.information(
                self,
                self.tr("InaSAFE Merge Impact Tool Information"),
                str(ex))
            return
        except CanceledImportDialogError:
            return

        # Validate all the layers logically
        try:
            self.validate_all_layers()
        except (NoKeywordsFoundError,
                KeywordNotFoundError,
                InvalidLayerError) as ex:
            # noinspection PyCallByClass,PyTypeChecker, PyArgumentList
            QMessageBox.information(
                self,
                self.tr("InaSAFE Merge Impact Tools Information"),
                str(ex))
            return

        # The input is valid, do the merging
        # Set cursor to wait cursor
        QtGui.qApp.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor))
        # pylint: disable=W0703
        try:
            self.merge()
        except Exception as ex:
            # End wait cursor
            QtGui.qApp.restoreOverrideCursor()
            # noinspection PyCallByClass,PyTypeChecker,PyArgumentList
            QMessageBox.warning(
                self,
                self.tr("InaSAFE Merge Impact Tools Error"),
                str(ex))
            return
            # pylint: enable=W0703

        # Finish doing it. End wait cursor
        QtGui.qApp.restoreOverrideCursor()

        # Give user successful information!
        # noinspection PyCallByClass,PyTypeChecker, PyArgumentList
        QMessageBox.information(
            self,
            self.tr('InaSAFE Merge Impact Tool Information'),
            self.tr(
                'Report from merging two impact layers was generated '
                'successfully.'))

        # Open output directory on file explorer
        # noinspection PyArgumentList
        output_directory_url = QUrl.fromLocalFile(self.out_dir)
        # noinspection PyTypeChecker,PyCallByClass,PyArgumentList
        QDesktopServices.openUrl(output_directory_url)

    def read_settings(self):
        """Set some variables from global settings on inasafe options dialog.
        """
        settings = QtCore.QSettings()

        # Organisation logo
        organisation_logo_path = settings.value(
            'inasafe/organisation_logo_path', '', type=str)
        if organisation_logo_path != '':
            self.organisation_logo_path = organisation_logo_path

        # Disclaimer text
        customised_disclaimer = settings.value(
            'inasafe/reportDisclaimer', '', type=str)
        if customised_disclaimer != '':
            self.disclaimer = customised_disclaimer

    def get_project_layers(self):
        """Get impact layers and aggregation layer currently loaded in QGIS."""
        # noinspection PyArgumentList,PyUnresolvedReferences
        registry = QgsMapLayerRegistry.instance()

        # MapLayers returns a QMap<QString id, QgsMapLayer layer>
        layers = registry.mapLayers().values()

        if len(layers) == 0:
            return

        # Clear the combo box first
        self.first_layer.clear()
        self.second_layer.clear()
        self.aggregation_layer.clear()
        # empty list
        self.available_aggregation[:] = []

        for layer in layers:
            try:
                self.keyword_io.read_keywords(layer, 'impact_summary')
            except (NoKeywordsFoundError, KeywordNotFoundError):
                # Check if it has aggregation keyword
                try:
                    self.keyword_io.read_keywords(
                        layer, 'aggregation attribute')
                except (NoKeywordsFoundError, KeywordNotFoundError):
                    # Skip if there are no keywords at all
                    continue
                self.available_aggregation.append(layer)
                add_ordered_combo_item(
                    self.aggregation_layer,
                    layer.name(),
                    layer)
                continue
            except (UnsupportedProviderError, InvalidParameterError):
                # UnsupportedProviderError:
                #   Encounter unsupported provider layer, e.g Open Layer
                # InvalidParameterError:
                #   Encounter invalid layer source,
                #   see https://github.com/AIFDR/inasafe/issues/754
                continue

            add_ordered_combo_item(self.first_layer, layer.name(), layer)
            add_ordered_combo_item(self.second_layer, layer.name(), layer)

        # Add Entire Area Option to Aggregated Layer:
        self.aggregation_layer.insertItem(
            0,
            self.tr('Entire Area'),
            None
        )
        self.aggregation_layer.setCurrentIndex(0)

    def prepare_input(self):
        """Fetch all the input from dialog, validate, and store it.

        Consider this as a bridge between dialog interface and our logical
        stored data in this class

        :raises: InvalidLayerError, CanceledImportDialogError
        """
        # Validate The combobox impact layers (they should be different)
        first_layer_index = self.first_layer.currentIndex()
        second_layer_index = self.second_layer.currentIndex()

        if first_layer_index < 0:
            raise InvalidLayerError(self.tr('First layer is not valid.'))

        if second_layer_index < 0:
            raise InvalidLayerError(self.tr('Second layer is not valid.'))

        if first_layer_index == second_layer_index:
            raise InvalidLayerError(
                self.tr('First layer must be different to second layer''.'))

        # Get all chosen layers
        self.first_impact['layer'] = self.first_layer.itemData(
            self.first_layer.currentIndex(), QtCore.Qt.UserRole)
        self.second_impact['layer'] = self.second_layer.itemData(
            self.second_layer.currentIndex(), QtCore.Qt.UserRole)
        self.aggregation['layer'] = self.aggregation_layer.itemData(
            self.aggregation_layer.currentIndex(), QtCore.Qt.UserRole)

        # Validate the output directory
        self.require_directory()

        # Get output directory
        self.out_dir = self.output_directory.text()

        # Whether to use own report template:
        if self.report_template_checkbox.isChecked():
            own_template_path = self.report_template_le.text()
            if os.path.isfile(own_template_path):
                self.template_path = own_template_path
            else:
                raise FileNotFoundError(
                    self.tr('Template file does not exist.'))

        # Flag whether to merge entire area or based on aggregation unit
        # Rizky: Fix nasty bug where the dialog stuck in entire_area_mode
        # the mode should be rechecked based on selected aggregation layer
        self.entire_area_mode = True
        if (self.aggregation_layer.currentIndex() > 0 and
                not self.aggregation['layer'] is None):
            self.entire_area_mode = False

    def require_directory(self):
        """Ensure directory path entered in dialog exist.

        When the path does not exist, this function will ask the user if he
        wants to create it or not.

        :raises: CanceledImportDialogError - when user chooses 'No' in
            the question dialog for creating directory, or 'Yes' but the output
            directory path is empty
        """
        path = self.output_directory.text()

        if os.path.exists(path):
            return

        title = self.tr("Directory %s does not exist") % path
        question = self.tr(
            "Directory %s does not exist. Do you want to create it?"
        ) % path
        # noinspection PyCallByClass,PyTypeChecker
        answer = QMessageBox.question(
            self, title,
            question, QMessageBox.Yes | QMessageBox.No)

        if answer == QMessageBox.Yes:
            if len(path) != 0:
                os.makedirs(path)
            else:
                raise EmptyDirectoryError(
                    self.tr('Output directory cannot be empty.'))
        else:
            raise CanceledImportDialogError()

    def validate_all_layers(self):
        """Validate all layers based on the keywords.

        When we do the validation, we also fetch the information we need:

        1. 'map_title' from each impact layer
        2. 'exposure_title' from each impact layer
        3. 'postprocessing_report' from each impact layer
        4. 'aggregation_attribute' on aggregation layer, if user runs merging
           tools with aggregation layer chosen

        The things that we validate are:

        1. 'map_title' keyword must exist on each impact layer
        2. 'exposure_title' keyword must exist on each impact layer
        3. 'postprocessing_report' keyword must exist on each impact layer
        4. 'hazard_title' keyword must exist on each impact layer. Hazard title
           from first impact layer must be the same with second impact layer
           to indicate that both are generated from the same hazard layer.
        5. 'aggregation attribute' must exist when user wants to run merging
           tools with aggregation layer chosen.

        """
        required_attribute = ['map_title', 'exposure_title', 'hazard_title',
                              'postprocessing_report']
        # Fetch for first impact layer
        for attribute in required_attribute:
            try:
                # noinspection PyTypeChecker
                self.first_impact[attribute] = self.keyword_io.read_keywords(
                    self.first_impact['layer'], attribute)
            except NoKeywordsFoundError:
                raise NoKeywordsFoundError(
                    self.tr('No keywords found for first impact layer.'))
            except KeywordNotFoundError:
                raise KeywordNotFoundError(
                    self.tr(
                        'Keyword %s not found for first layer.' % attribute))

        # Fetch for second impact layer
        for attribute in required_attribute:
            try:
                # noinspection PyTypeChecker
                self.second_impact[attribute] = self.keyword_io.read_keywords(
                    self.second_impact['layer'], attribute)
            except NoKeywordsFoundError:
                raise NoKeywordsFoundError(
                    self.tr('No keywords found for second impact layer.'))
            except KeywordNotFoundError:
                raise KeywordNotFoundError(
                    self.tr(
                        'Keyword %s not found for second layer.' % attribute))

        # Validate that two impact layers are obtained from the same hazard.
        # Indicated by the same 'hazard_title' (to be fixed later by using
        # more reliable method)
        if (self.first_impact['hazard_title'] !=
                self.second_impact['hazard_title']):
            raise InvalidLayerError(
                self.tr('First impact layer and second impact layer do not '
                        'use the same hazard layer.'))

        # Fetch 'aggregation_attribute'
        # If the chosen aggregation layer not Entire Area, it should have
        # aggregation attribute keywords
        if not self.entire_area_mode:
            try:
                # noinspection PyTypeChecker
                self.aggregation['aggregation_attribute'] = \
                    self.keyword_io.read_keywords(
                        self.aggregation['layer'], 'aggregation attribute')
            except NoKeywordsFoundError:
                raise NoKeywordsFoundError(
                    self.tr('No keywords exist in aggregation layer.'))
            except KeywordNotFoundError:
                raise KeywordNotFoundError(
                    self.tr(
                        'Keyword aggregation attribute not found for '
                        'aggregation layer.'))

    def merge(self):
        """Merge the postprocessing_report from each impact."""
        # Ensure there is always only a single root element or minidom moans
        first_postprocessing_report = \
            self.first_impact['postprocessing_report']
        second_postprocessing_report = \
            self.second_impact['postprocessing_report']
        # noinspection PyTypeChecker
        first_report = '<body>' + first_postprocessing_report + '</body>'
        # noinspection PyTypeChecker
        second_report = '<body>' + second_postprocessing_report + '</body>'

        # Now create a dom document for each
        first_document = minidom.parseString(get_string(first_report))
        second_document = minidom.parseString(get_string(second_report))
        first_impact_tables = first_document.getElementsByTagName('table')
        second_impact_tables = second_document.getElementsByTagName('table')

        # Now create dictionary report from DOM
        first_report_dict = self.generate_report_dictionary_from_dom(
            first_impact_tables)
        second_report_dict = self.generate_report_dictionary_from_dom(
            second_impact_tables)

        # Rizky: Consistency checks with aggregation
        # Make sure the aggregation layer both presents in both layers

        # We shouldn't have problems with Entire Area mode. It just means
        # the impact layer and summary is merged into single report.
        # We can have 3 cases:
        # 1. If both of them were not aggregated, we can just merge the map
        #    only
        # 2. If both of them were aggregated, we can just merge the map, and
        #    merge postprocessor report using 'Total aggregation in areas' key
        # 3. If only one of them were aggregated, we can just merge the map,
        #    and uses postprocessor report from the one who has.
        if self.entire_area_mode:
            # We won't be bothered with the map, it will be merged anyway in
            # all 3 cases. We should bother with the postprocessor report.
            # If one of them has the report, it means it will contain more
            # than one report keys. We can just swap the first report if they
            # have one key, and the second have more than one
            if (len(first_report_dict.keys()) == 1 and
                    len(second_report_dict.keys()) > 1):
                swap_var = first_report_dict
                first_report_dict = second_report_dict
                second_report_dict = swap_var
        # This condition will covers aggregated mode
        # For this case, we should make sure both layers are aggregated with
        # the same aggregation layer of the chosen aggregation layer
        else:
            # check that both layers must have aggregated postprocessor.
            # aggregated postprocessor means the report_dict must have minimum
            # 2 keys
            if not (len(first_report_dict.keys()) > 1 and
                    len(second_report_dict.keys()) > 1):
                raise InvalidLayerError(self.tr(
                    'Please choose impact layers with aggregated '
                    'postprocessor if you want to use aggregation layer.'))

            # collect all report keys (will contain aggregation areas in the
            # report)
            report_keys = first_report_dict.keys()
            # Discard the last keys. It will always contains total area, not
            # aggregated area
            if len(report_keys) > 0:
                del report_keys[-1]

            sec_report = second_report_dict.keys()
            if len(sec_report) > 0:
                del sec_report[-1]

            for k in sec_report:
                if k not in report_keys:
                    report_keys.append(k)

            # collect all aggregation areas in aggregation layer
            layer = self.aggregation['layer']
            aggregation_attr = self.aggregation['aggregation_attribute']
            aggregation_attr_index = layer.fieldNameIndex(aggregation_attr)
            aggregation_keys = []
            for f in layer.getFeatures():
                area = f[aggregation_attr_index]
                if area not in aggregation_keys:
                    aggregation_keys.append(area)

            is_subset = True
            for k in report_keys:
                if k not in aggregation_keys:
                    is_subset = False

            if not is_subset:
                # This means report keys contains area keys that is not in
                # aggregation layer. Which means possibly it is using the
                # wrong aggregation layer.
                raise InvalidLayerError(
                    self.tr('First and Second layer does not use chosen '
                            'Aggregation layer'))

        # Generate report summary for all aggregation unit
        self.generate_report_summary(first_report_dict, second_report_dict)

        # Generate html reports file from merged dictionary
        self.generate_html_reports(first_report_dict, second_report_dict)

        # Generate PDF Reports using composer and/or atlas generation:
        self.generate_reports()

        # Delete html report files:
        for area in self.html_reports:
            report_path = self.html_reports[area]
            # Rizky : Fix possible bugs in Windows related to issue:
            # https://github.com/AIFDR/inasafe/issues/1862
            try:
                # avoid race condition using with statement
                with open(report_path, 'w') as report_file:
                    report_file.close()
                    os.remove(report_path)
            except OSError:
                pass

    @staticmethod
    def generate_report_dictionary_from_dom(html_dom):
        """Generate dictionary representing report from html dom.

        :param html_dom: Input representing document dom as report from each
            impact layer report.
        :type html_dom: str

        :return: Dictionary representing html_dom.
        :rtype: dict

        Dictionary Structure::

            { Aggregation_Area:
                {Exposure Type:{
                    Exposure Detail}
                }
            }

        Example::

           {"Jakarta Barat":
               {"Closed buildings":
                   {"Total inundated":150,
                    "Places of Worship": "No data"
                   }
               }
           }

        """
        merged_report_dict = OrderedDict()
        for table in html_dom:
            # noinspection PyUnresolvedReferences
            caption = table.getElementsByTagName('caption')[0].firstChild.data
            # noinspection PyUnresolvedReferences
            rows = table.getElementsByTagName('tr')
            header = rows[0]
            contains = rows[1:]
            for contain in contains:
                data = contain.getElementsByTagName('td')
                aggregation_area = data[0].firstChild.nodeValue
                exposure_dict = OrderedDict()
                if aggregation_area in merged_report_dict:
                    exposure_dict = merged_report_dict[aggregation_area]
                data_contain = data[1:]
                exposure_detail_dict = OrderedDict()
                for datum in data_contain:
                    index_datum = data.index(datum)
                    datum_header = \
                        header.getElementsByTagName('td')[index_datum]
                    datum_caption = datum_header.firstChild.nodeValue
                    exposure_detail_dict[datum_caption] = \
                        datum.firstChild.nodeValue
                exposure_dict[caption] = exposure_detail_dict
                merged_report_dict[aggregation_area] = exposure_dict
        return merged_report_dict

    def generate_report_summary(self, first_report_dict, second_report_dict):
        """Generate report summary for each aggregation area from merged
        report dictionary.

        For each exposure, search for the total only. Report dictionary looks
        like this:

        :param first_report_dict: Dictionary report from the first impact.
        :type first_report_dict: dict

        :param second_report_dict: Dictionary report from the second impact.
        :type second_report_dict: dict

        Dictionary structure::

            { aggregation_area:
                {exposure_type:{
                   exposure_detail}
                }
            }

        Example::

            {"Jakarta Barat":
                {"Closed buildings":
                    {"Total inundated":150,
                     "Places of Worship": "No data"
                    }
                }
            }

        """
        for aggregation_area in first_report_dict:
            html = ''
            html += '<table style="margin:0px auto">'

            # Summary total from first report
            html += '<tr><td><b>%s</b></td><td></td></tr>' % \
                    self.first_impact['exposure_title'].title()
            first_exposure_type_dict = first_report_dict[aggregation_area]
            first_exposure_type = first_exposure_type_dict.keys()[0]
            first_exposure_detail_dict = \
                first_exposure_type_dict[first_exposure_type]
            for datum in first_exposure_detail_dict:
                if self.tr('Total').lower() in datum.lower():
                    html += ('<tr>'
                             '<td>%s</td>'
                             '<td>%s</td>'
                             '</tr>') % \
                            (datum, first_exposure_detail_dict[datum])
                    break

            # Catch fallback for aggregation_area not exist in second_report
            if aggregation_area in second_report_dict:
                second_exposure_report_dict = second_report_dict[
                    aggregation_area]
                # Summary total from second report
                html += '<tr><td><b>%s</b></td><td></td></tr>' % \
                        self.second_impact['exposure_title'].title()
                second_exposure = second_exposure_report_dict.keys()[0]
                second_exposure_detail_dict = \
                    second_exposure_report_dict[second_exposure]
                for datum in second_exposure_detail_dict:
                    if self.tr('Total').lower() in datum.lower():
                        html += ('<tr>'
                                 '<td>%s</td>'
                                 '<td>%s</td>'
                                 '</tr>') % \
                                (datum, second_exposure_detail_dict[datum])
                        break

            html += '</table>'
            self.summary_report[aggregation_area.lower()] = html

    def generate_html_reports(self, first_report_dict, second_report_dict):
        """Generate html file for each aggregation units.

        It also saves the path of the each aggregation unit in
        self.html_reports.
        ::

            Ex. {"jakarta barat": "/home/jakarta barat.html",
                 "jakarta timur": "/home/jakarta timur.html"}

        :param first_report_dict: Dictionary report from first impact.
        :type first_report_dict: dict

        :param second_report_dict: Dictionary report from second impact.
        :type second_report_dict: dict
        """
        for aggregation_area in first_report_dict:
            html = html_header()
            html += ('<table width="100%" style="position:absolute;left:0px;"'
                     'class="table table-condensed table-striped">')
            html += '<caption><h4>%s</h4></caption>' % \
                    aggregation_area.title()

            html += '<tr>'

            # First impact on the left side
            html += '<td width="48%">'
            html += '<table width="100%">'
            html += '<thead><th>%s</th></thead>' % \
                    self.first_impact['exposure_title'].upper()
            first_exposure_report_dict = first_report_dict[aggregation_area]
            for first_exposure in first_exposure_report_dict:
                first_exposure_detail_dict = \
                    first_exposure_report_dict[first_exposure]
                html += '<tr><th><i>%s</i></th><th></th></tr>' % \
                        first_exposure.title()
                for datum in first_exposure_detail_dict:
                    html += ('<tr>'
                             '<td>%s</td>'
                             '<td>%s</td>'
                             '</tr>') % (datum,
                                         first_exposure_detail_dict[datum])
            html += '</table>'
            html += '</td>'

            # Second impact on the right side
            if aggregation_area in second_report_dict:
                # Add spaces between
                html += '<td width="4%">'
                html += '</td>'

                # Second impact report
                html += '<td width="48%">'
                html += '<table width="100%">'
                html += '<thead><th>%s</th></thead>' % \
                        self.second_impact['exposure_title'].upper()
                second_exposure_report_dict = \
                    second_report_dict[aggregation_area]
                for second_exposure in second_exposure_report_dict:
                    second_exposure_detail_dict = \
                        second_exposure_report_dict[second_exposure]
                    html += '<tr><th><i>%s</i></th><th></th></tr>' % \
                            second_exposure.title()
                    for datum in second_exposure_detail_dict:
                        html += ('<tr>'
                                 '<td>%s</td>'
                                 '<td>%s</td>'
                                 '</tr>') % \
                                (datum,
                                 second_exposure_detail_dict[datum])
                html += '</table>'
                html += '</td>'

            html += '</tr>'
            html += '</table>'
            html += html_footer()

            file_path = '%s.html' % aggregation_area
            path = os.path.join(
                temp_dir(self.__class__.__name__),
                file_path)
            html_to_file(html, path)
            self.html_reports[aggregation_area.lower()] = path

    def generate_reports(self):
        """Generate PDF reports for each aggregation unit using map composer.

        First the report template is loaded with the renderer from two
        impact layers. After it's loaded, if it is not aggregated then
        we just use composition to produce report. Since there are two
        impact maps here, we need to set a new extent for these impact maps
        by a simple calculation.

        If it is not aggregated then we use a powerful QGIS atlas generation
        on composition. Since we save each report table representing each
        aggregated area on self.html_report (which is a dictionary with the
        aggregation area name as a key and its path as a value), and we set
        the aggregation area name as current filename on atlas generation,
        we can match these two so that we have the right report table for
        each report.

        For those two cases, we use the same template. The report table is
        basically an HTML frame. Of course after the merging process is done,
        we delete each report table on self.html_reports physically on disk.
        """
        # Set the layer set
        layer_set = [self.first_impact['layer'].id(),
                     self.second_impact['layer'].id()]

        # If aggregated, append chosen aggregation layer
        if not self.entire_area_mode:
            layer_set.append(self.aggregation['layer'].id())

        # Instantiate Map Settings for Composition
        if qgis_version() < 20400:
            map_settings = QgsMapRenderer()
            map_settings.setLayerSet(layer_set)
        else:
            map_settings = QgsMapSettings()
            map_settings.setLayers(layer_set)

        # Create composition
        composition = self.load_template(map_settings)

        # Get Map
        composer_map = composition.getComposerItemById('impact-map')

        # Get HTML Report Frame
        html_report_item = \
            composition.getComposerItemById('merged-report-table')
        html_report_frame = composition.getComposerHtmlByItem(html_report_item)

        if self.entire_area_mode:
            # Get composer map size
            composer_map_width = composer_map.boundingRect().width()
            composer_map_height = composer_map.boundingRect().height()

            # Set the extent from two impact layers to fit into composer map
            composer_size_ratio = float(
                composer_map_height / composer_map_width)

            # The extent of two impact layers
            min_x = min(self.first_impact['layer'].extent().xMinimum(),
                        self.second_impact['layer'].extent().xMinimum())
            min_y = min(self.first_impact['layer'].extent().yMinimum(),
                        self.second_impact['layer'].extent().yMinimum())
            max_x = max(self.first_impact['layer'].extent().xMaximum(),
                        self.second_impact['layer'].extent().xMaximum())
            max_y = max(self.first_impact['layer'].extent().yMaximum(),
                        self.second_impact['layer'].extent().yMaximum())
            max_width = max_x - min_x
            max_height = max_y - min_y
            layers_size_ratio = float(max_height / max_width)
            center_x = min_x + float(max_width / 2.0)
            center_y = min_y + float(max_height / 2.0)

            # The extent should fit the composer map size
            new_width = max_width
            new_height = max_height

            # QgsComposerMap only overflows to height, so if it overflows,
            # the extent of the width should be widened
            if layers_size_ratio > composer_size_ratio:
                new_width = max_height / composer_size_ratio

            # Set new extent
            fit_min_x = center_x - (new_width / 2.0)
            fit_max_x = center_x + (new_width / 2.0)
            fit_min_y = center_y - (new_height / 2.0)
            fit_max_y = center_y + (new_height / 2.0)

            # Create the extent and set it to the map
            # noinspection PyCallingNonCallable
            map_extent = QgsRectangle(
                fit_min_x, fit_min_y, fit_max_x, fit_max_y)
            composer_map.setNewExtent(map_extent)

            # Add grid to composer map
            split_count = 5
            x_interval = new_width / split_count
            composer_map.setGridIntervalX(x_interval)
            y_interval = new_height / split_count
            composer_map.setGridIntervalY(y_interval)

            # Self.html_reports must have only 1 key value pair
            # Rizky: If layer components were aggregated, html_reports will
            # have several key value pairs. So we get the last value because
            # it is for the total/entire area
            area_title = list(self.html_reports.keys())[-1]

            # Set Report Summary
            summary_report = composition.getComposerItemById('summary-report')
            summary_report.setText(self.summary_report[area_title])

            # Set Aggregation Area Label
            area_label = composition.getComposerItemById('aggregation-area')
            area_label.setText(area_title.title())

            # Set merged-report-table
            html_report_path = self.html_reports[area_title]
            # noinspection PyArgumentList
            html_frame_url = QUrl.fromLocalFile(html_report_path)
            html_report_frame.setUrl(html_frame_url)

            # Export composition to PDF file
            file_name = '_'.join(area_title.split())
            file_path = '%s.pdf' % file_name
            path = os.path.join(self.out_dir, file_path)
            composition.exportAsPDF(path)
        else:
            # Create atlas composition:
            # noinspection PyCallingNonCallable
            atlas = QgsAtlasComposition(composition)

            # Set coverage layer
            # Map will be clipped by features from this layer:
            atlas.setCoverageLayer(self.aggregation['layer'])

            # Add grid to composer map from coverage layer
            split_count = 5
            map_width = self.aggregation['layer'].extent().width()
            map_height = self.aggregation['layer'].extent().height()
            x_interval = map_width / split_count
            composer_map.setGridIntervalX(x_interval)
            y_interval = map_height / split_count
            composer_map.setGridIntervalY(y_interval)

            # Set composer map that will be used for printing atlas
            atlas.setComposerMap(composer_map)

            # set output filename pattern
            atlas.setFilenamePattern(
                self.aggregation['aggregation_attribute'])

            # Start rendering
            atlas.beginRender()

            # Iterate all aggregation unit in aggregation layer
            for i in range(0, atlas.numFeatures()):
                atlas.prepareForFeature(i)

                current_filename = atlas.currentFilename()
                file_name = '_'.join(current_filename.split())
                file_path = '%s.pdf' % file_name
                path = os.path.join(self.out_dir, file_path)

                # Only print the area that has the report
                area_title = current_filename.lower()
                if area_title in self.summary_report:
                    # Set Report Summary
                    summary_report = composition.getComposerItemById(
                        'summary-report')
                    summary_report.setText(self.summary_report[area_title])

                    # Set Aggregation Area Label
                    area_label = composition.getComposerItemById(
                        'aggregation-area')
                    area_label.setText(area_title.title())

                    # Set merged-report-table
                    html_report_path = self.html_reports[area_title]
                    # noinspection PyArgumentList
                    html_frame_url = QUrl.fromLocalFile(html_report_path)
                    html_report_frame.setUrl(html_frame_url)

                    # Export composition to PDF file
                    composition.exportAsPDF(path)

            # End of rendering
            atlas.endRender()

    # noinspection PyArgumentList
    def load_template(self, map_settings):
        """Load composer template for merged report.

        Validate it as well. The template needs to have:
        1. QgsComposerMap with id 'impact-map' for merged impact map.
        2. QgsComposerPicture with id 'safe-logo' for InaSAFE logo.
        3. QgsComposerLabel with id 'summary-report' for a summary of two
        impacts.
        4. QgsComposerLabel with id 'aggregation-area' to indicate the area
        of aggregation.
        5. QgsComposerScaleBar with id 'map-scale' for impact map scale.
        6. QgsComposerLegend with id 'map-legend' for impact map legend.
        7. QgsComposerPicture with id 'organisation-logo' for organisation
        logo.
        8. QgsComposerLegend with id 'impact-legend' for map legend.
        9. QgsComposerHTML with id 'merged-report-table' for the merged report.

        :param map_settings: Map settings.
        :type map_settings: QgsMapSettings, QgsMapRenderer

        """
        # Create Composition
        template_composition = TemplateComposition(
            self.template_path, map_settings)

        # Validate the component in the template
        component_ids = ['impact-map', 'safe-logo', 'summary-report',
                         'aggregation-area', 'map-scale', 'map-legend',
                         'organisation-logo', 'merged-report-table']
        template_composition.component_ids = component_ids
        if len(template_composition.missing_elements) > 0:
            raise ReportCreationError(self.tr(
                'Components: %s could not be found' % ', '.join(
                    template_composition.missing_elements)))

        # Prepare map substitution and set to composition
        impact_title = '%s and %s' % (
            self.first_impact['map_title'],
            self.second_impact['map_title'])
        substitution_map = {
            'impact-title': impact_title,
            'hazard-title': self.first_impact['hazard_title'],
            'disclaimer': self.disclaimer
        }
        template_composition.substitution = substitution_map

        # Load Template
        try:
            template_composition.load_template()
        except TemplateLoadingError:
            raise

        # Draw Composition
        # Set InaSAFE logo
        composition = template_composition.composition
        safe_logo = composition.getComposerItemById('safe-logo')
        safe_logo.setPictureFile(self.safe_logo_path)

        # set organisation logo
        org_logo = composition.getComposerItemById('organisation-logo')
        org_logo.setPictureFile(self.organisation_logo_path)

        # Set Map Legend
        legend = composition.getComposerItemById('map-legend')

        if qgis_version() < 20400:
            layers = map_settings.layerSet()
        else:
            layers = map_settings.layers()

        if qgis_version() < 20600:
            legend.model().setLayerSet(layers)
            legend.synchronizeWithModel()
        else:
            root_group = legend.modelV2().rootGroup()

            layer_ids = map_settings.layers()
            for layer_id in layer_ids:
                # noinspection PyUnresolvedReferences
                layer = QgsMapLayerRegistry.instance().mapLayer(layer_id)
                root_group.addLayer(layer)
            legend.synchronizeWithModel()

        return composition

    @pyqtSlot()
    @pyqtSignature('bool')  # prevents actions being handled twice
    def help_toggled(self, flag):
        """Show or hide the help tab in the stacked widget.

        .. versionadded: 3.2.1

        :param flag: Flag indicating whether help should be shown or hidden.
        :type flag: bool
        """
        if flag:
            self.help_button.setText(self.tr('Hide Help'))
            self.show_help()
        else:
            self.help_button.setText(self.tr('Show Help'))
            self.hide_help()

    def hide_help(self):
        """Hide the usage info from the user.

        .. versionadded: 3.2.1
        """
        self.main_stacked_widget.setCurrentIndex(1)

    def show_help(self):
        """Show usage info to the user."""
        # Read the header and footer html snippets
        self.main_stacked_widget.setCurrentIndex(0)
        header = html_header()
        footer = html_footer()

        string = header

        message = impact_merge_help()

        string += message.to_html()
        string += footer

        self.help_web_view.setHtml(string)
Пример #43
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
Пример #44
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', 'tsunami_wgs84.tif')
        self.raster_layer, _ = load_layer(hazard_path)
        self.expected_raster_keywords = {
            'hazard_category': 'single_event',
            'title': 'Tsunami',
            'hazard': 'tsunami',
            'continuous_hazard_unit': 'metres',
            'layer_geometry': 'raster',
            'layer_purpose': 'hazard',
            'layer_mode': 'continuous',
            'keyword_version': inasafe_keyword_version
        }

        # Vector Layer keywords
        vector_path = test_data_path('exposure', 'buildings_osm_4326.shp')
        self.vector_layer, _ = load_layer(vector_path)
        self.expected_vector_keywords = {
            'keyword_version': inasafe_keyword_version,
            'structure_class_field': 'FLOODED',
            'title': 'buildings_osm_4326',
            'layer_geometry': 'polygon',
            'layer_purpose': 'exposure',
            'layer_mode': 'classified',
            'exposure': 'structure'
        }
        # 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)
        self.assertEqual(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)
        self.assertDictEqual(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)
        self.assertDictEqual(keywords, expected_keywords, 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 didn't 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.assertDictEqual(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)
        self.assertDictEqual(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='tsunami_wgs84',
            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',
            'hazard_category': 'single_event',
            'title': 'Tsunami',
            'hazard': 'tsunami',
            'continuous_hazard_unit': 'metres',
            'test': 'TEST',
            'layer_geometry': 'raster',
            'layer_purpose': 'hazard',
            'layer_mode': 'continuous',
            'keyword_version': inasafe_keyword_version
        }
        message = 'Got:\n%s\nExpected:\n%s' % (keywords, expected_keywords)
        self.assertDictEqual(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.assertDictEqual(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.assertDictEqual(copied_keywords, expected_keywords, message)

    def test_definition(self):
        """Test we can get definitions for keywords.

        .. versionadded:: 3.2

        """
        keyword = 'hazards'
        definition = self.keyword_io.definition(keyword)
        self.assertTrue('description' in definition)

    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_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())
Пример #45
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_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())
Пример #46
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:
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
        try:
            key = post_processor[0][1].keys()[0]
        except IndexError:
            return -1
        # 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')
            name = get_postprocessor_human_name(processor).lower()

            if name == 'building type':
                table.caption = self.tr('Closed buildings')
            elif name == 'road type':
                table.caption = self.tr('Closed roads')
            elif name == 'people':
                table.caption = self.tr('Affected people')

            # Dirty hack to make "evacuated" come 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())

            empty_table = not sorted_results[0][1]
            if empty_table:
                # Due to an error? The table is empty.
                message.add(table)
                message.add(m.EmphasizedText(self.tr(
                    'Could not compute the %s report.' %
                    tr(get_postprocessor_human_name(processor)).lower())))
                continue

            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()

            null_index = 0  # counting how many null value in the data
            for zone_name, calc in sorted_results:
                if isinstance(zone_name, QPyNullVariant):
                    # I have made sure that the zone_name won't be Null in
                    # run method. But just in case there is something wrong.
                    zone_name = 'Unnamed Area %s' % null_index
                    null_index += 1
                if name == 'road type':
                    # We add the unit 'meter' as we are counting roads.
                    zone_name = '%s (m)' % zone_name
                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'))))
            caption = m.EmphasizedText(self.tr(
                'Columns containing exclusively 0 and "%s" '
                'have not been shown in the table.' %
                self.aggregator.get_default_keyword('NO_DATA')))
            message.add(
                m.Paragraph(
                    caption,
                    style_class='caption')
                )

        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)
        except (TypeError, KeyError):
            # TypeError is for when function_parameters is none
            # KeyError is for when ['postprocessors'] is unavailable
            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]
            if isinstance(zone_name, QPyNullVariant):
                zone_name = 'Unnamed Area %s' % str(feature.id())

            # 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
                user_parameters = self.function_parameters[
                    'postprocessors'][key]
                user_parameters = dict(
                    [(user_parameter.name, user_parameter.value) for
                     user_parameter in user_parameters])
                try:
                    # user parameters override default parameters
                    parameters.update(user_parameters)
                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):
                            LOGGER.debug(
                                '--- only default age ratios used ---')
                            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()
                    if key not in self.output:
                        self.output[key] = []
                    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
            # increment the index
            polygon_index += 1
        self.remove_empty_columns()

    def remove_empty_columns(self):
        """Removes empty columns from output, to reduce table width.

        .. note:: This is intended to be a temporary solution to the excessive
        amount of data in some of the postprocessors.
        """
        for (_postprocessor_type, output) in self.output.items():
            keys = output[0][1].keys()
            for key in keys:
                total_for_type = 0
                for _area, breakdown in output:
                    value = breakdown[key]['value']
                    try:
                        total_for_type += int(value.replace(',', ''))
                    except ValueError:
                        # no need to increment for a no data value
                        no_data_value = self.aggregator.get_default_keyword(
                            'NO_DATA')
                        if value != no_data_value:
                            total_for_type += 1
                if total_for_type == 0:
                    for _area, breakdown in output:
                        breakdown.pop(key)

    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, multipart_polygon_key)):
                self._consolidate_multipart_stats()
        except KeywordNotFoundError:
            pass

        message.add(self._generate_tables(aoi_mode))
        return message
Пример #48
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
        try:
            key = post_processor[0][1].keys()[0]
        except IndexError:
            return -1
        # 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_data(self, aoi_mode=True):
        """Parses the postprocessing output as dictionary.

        :param aoi_mode: adds a Total in aggregation areas
        row to the calculated table
        :type aoi_mode: bool

        :returns: The dictionary of postprocessing.
        :rtype: dict
        """
        result = {}

        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 = {"notes": []}
            name = get_postprocessor_human_name(processor).lower()
            translated_name = tr(name)

            if name == "building type":
                table["caption"] = tr("Closed buildings")
            elif name == "road type":
                table["caption"] = tr("Closed roads")
            elif name == "people":
                table["caption"] = tr("Affected people")

            # Dirty hack to make "evacuated" come 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"] = tr("Detailed %s report " "(for people needing evacuation)") % translated_name
                else:
                    table["caption"] = tr("Detailed %s report " "(affected people)") % translated_name

            if processor in ["Gender", "Age"]:
                table["caption"] = tr("Detailed %s report " "(affected people)") % translated_name

            try:
                empty_table = not sorted_results[0][1]
            except IndexError:
                empty_table = True
            if empty_table:
                # The table is empty.
                # Due to an error or because every lines were removed.
                table["attributes"] = []
                table["fields"] = []
                table["notes"].append(tr('The report "%s" is empty.') % translated_name)
                result["processor"] = table
                continue

            header = [str(self.attribute_title).capitalize()]
            for calculation_name in sorted_results[0][1]:
                header.append(self.tr(calculation_name))
            table["attributes"] = header

            # used to calculate the totals row as per issue #690
            postprocessor_totals = OrderedDict()

            null_index = 0  # counting how many null value in the data
            fields = []
            for zone_name, calc in sorted_results:
                if isinstance(zone_name, QPyNullVariant):
                    # I have made sure that the zone_name won't be Null in
                    # run method. But just in case there is something wrong.
                    zone_name = tr("Unnamed Area %s" % null_index)
                    null_index += 1
                if name == "road type":
                    # We add the unit 'meter' as we are counting roads.
                    zone_name = tr("%(zone_name)s (m)" % {"zone_name": zone_name})
                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
                        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.append(value)
                fields.append(row)

            if not aoi_mode:
                # add the totals row
                row = [self.tr("Total in aggregation areas")]
                for _, total in postprocessor_totals.iteritems():
                    row.append(total)
                fields.append(row)

            table["fields"] = fields

            if has_no_data:
                table["notes"].append(
                    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"))
                )

            table["notes"].append(
                self.tr(
                    'Columns and rows containing only 0 or "%s" values are '
                    "excluded from the tables." % self.aggregator.get_default_keyword("NO_DATA")
                )
            )
            result[processor] = table

        return result

    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")
            name = get_postprocessor_human_name(processor).lower()
            translated_name = tr(name)

            if name == "building type":
                table.caption = tr("Closed buildings")
            elif name == "road type":
                table.caption = tr("Closed roads")
            elif name == "people":
                table.caption = tr("Affected people")

            # Dirty hack to make "evacuated" come 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 = tr("Detailed %s report " "(for people needing evacuation)") % translated_name
                else:
                    table.caption = tr("Detailed %s report " "(affected people)") % translated_name

            if processor in ["Gender", "Age"]:
                table.caption = tr("Detailed %s report " "(affected people)") % translated_name

            empty_table = not sorted_results[0][1]
            if empty_table:
                # Due to an error? The table is empty.
                message.add(table)
                message.add(m.EmphasizedText(tr("Could not compute the %s report.") % translated_name))
                continue

            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()

            null_index = 0  # counting how many null value in the data
            for zone_name, calc in sorted_results:
                if isinstance(zone_name, QPyNullVariant):
                    # I have made sure that the zone_name won't be Null in
                    # run method. But just in case there is something wrong.
                    zone_name = tr("Unnamed Area %s" % null_index)
                    null_index += 1
                if name == "road type":
                    # We add the unit 'meter' as we are counting roads.
                    # proper format for i186
                    zone_name = tr("%(zone_name)s (m)") % {"zone_name": tr(zone_name)}
                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
                        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"))
                    )
                )
            caption = m.EmphasizedText(
                self.tr(
                    'Columns and rows containing only 0 or "%s" values are '
                    "excluded from the tables." % self.aggregator.get_default_keyword("NO_DATA")
                )
            )
            message.add(m.Paragraph(caption, style_class="caption"))

        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)
        except (TypeError, KeyError):
            # TypeError is for when function_parameters is none
            # KeyError is for when ['postprocessors'] is unavailable
            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")

        # 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]
            if isinstance(zone_name, QPyNullVariant):
                # proper format for i186
                zone_name = tr("Unnamed Area %(feature_id)s") % {"feature_id": str(feature.id())}

            # create dictionary of attributes to pass to postprocessor
            general_params = {"target_field": self.aggregator.target_field, "function_params": self.function_parameters}

            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
                user_parameters = self.function_parameters["postprocessors"][key]
                user_parameters = dict(
                    [(user_parameter.name, user_parameter.value) for user_parameter in user_parameters]
                )
                try:
                    # user parameters override default parameters
                    parameters.update(user_parameters)
                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:
                            LOGGER.debug("--- only default age ratios used ---")
                            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":
                    if key == "BuildingType":
                        key_attribute = self.keyword_io.read_keywords(
                            self.aggregator.exposure_layer, "structure_class_field"
                        )
                    elif key == "RoadType":
                        key_attribute = self.keyword_io.read_keywords(
                            self.aggregator.exposure_layer, "road_class_field"
                        )
                    else:
                        try:
                            key_attribute = self.keyword_io.read_keywords(
                                self.aggregator.exposure_layer, "key_attribute"
                            )
                        except KeywordNotFoundError:
                            # use 'type' as default
                            key_attribute = "type"

                    parameters["key_attribute"] = key_attribute
                    LOGGER.debug("key_attribute: %s", key_attribute)

                    value_map = self.keyword_io.read_keywords(self.aggregator.exposure_layer, "value_mapping")
                    parameters["value_mapping"] = value_map

                try:
                    value.setup(parameters)
                    value.process()
                    results = value.results()
                    value.clear()
                    if key not in self.output:
                        self.output[key] = []
                    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
            # increment the index
            polygon_index += 1
        self.remove_empty_columns()
        self.remove_empty_lines()

    def remove_empty_columns(self):
        """Removes empty columns from output, to reduce table width.

        Columns containing exclusively 0 and "No data" are removed.

        .. note:: This is intended to be a temporary solution to the excessive
        amount of data in some of the postprocessors.
        """
        for (_postprocessor_type, output) in self.output.items():
            keys = output[0][1].keys()
            for key in keys:
                total_for_type = 0
                for _area, breakdown in output:
                    value = breakdown[key]["value"]
                    try:
                        total_for_type += int(value.replace(",", ""))
                    except ValueError:
                        # no need to increment for a no data value
                        no_data_value = self.aggregator.get_default_keyword("NO_DATA")
                        if value != no_data_value:
                            total_for_type += 1
                if total_for_type == 0:
                    for _area, breakdown in output:
                        breakdown.pop(key)

    def remove_empty_lines(self):
        """Remove empty lines from output, to reduce table height.

        Lines containing exclusively 0 and "No data" are removed.
        """

        def keep_area(row, elements):
            """Internal function to know if we should keep the row.

            :param row: The row
            :param elements : Columns heading.

            :return: Boolean if we should keep the row.
            """
            for element in elements:
                value = row[element]["value"]
                try:
                    if int(value.replace(",", "")) != 0:
                        # If there is one integer, different than 0, we keep it
                        return True
                except ValueError:
                    pass
            else:
                # Only no data or zero values has been found, we remove it.
                return False

        index_to_remove = {}
        for (postprocessor_type, output) in self.output.items():
            index_to_remove[postprocessor_type] = []
            keys = output[0][1].keys()
            for i, line in enumerate(output):
                if not keep_area(line[1], keys):
                    index_to_remove[postprocessor_type].append(i)

        for postprocessor in index_to_remove:
            # As we are removing many indexes in the list,
            # we need to reverse the order to not disturb the lower indexes.
            for i in sorted(index_to_remove[postprocessor], reverse=True):
                del self.output[postprocessor][i]

    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, multipart_polygon_key):
                self._consolidate_multipart_stats()
        except KeywordNotFoundError:
            pass

        message.add(self._generate_tables(aoi_mode))
        return message

    def get_json_data(self, aoi_mode):
        """Returns the results of the post processing as a dict.

        :param aoi_mode: aoi mode of the aggregator.
        :type aoi_mode: bool

        :returns: dictionary representing the post processing result.
        :rtype: dict
        """
        return self._generate_data(aoi_mode)
Пример #49
0
class ImpactMergeDialog(QDialog, FORM_CLASS):
    """Tools for merging 2 impact layer based on different exposure."""
    def __init__(self, parent=None, iface=None):
        """Constructor for dialog.

        :param parent: Optional widget to use as parent
        :type parent: QWidget

        :param iface: An instance of QGisInterface
        :type iface: QGisInterface
        """
        QDialog.__init__(self, parent)
        self.parent = parent
        self.setupUi(self)
        self.setWindowTitle(self.tr('InaSAFE Impact Layer Merge Tool'))
        self.iface = iface
        self.keyword_io = KeywordIO()

        # Template Path for composer
        self.template_path = resources_path('qgis-composer-templates',
                                            'merged_report.qpt')

        # Safe Logo Path
        self.safe_logo_path = white_inasafe_logo_path()

        # Organisation Logo Path - defaults to supporters logo, will be
        # updated to user defined organisation logo path in read_settings in
        # user has specified a custom logo.
        self.organisation_logo_path = supporters_logo_path()

        # Disclaimer text
        self.disclaimer = disclaimer()

        # The output directory
        self.out_dir = None

        # Stored information from first impact layer
        self.first_impact = {
            'layer': None,
            'map_title': None,
            'hazard_title': None,
            'exposure_title': None,
            'postprocessing_report': None,
        }

        # Stored information from second impact layer
        self.second_impact = {
            'layer': None,
            'map_title': None,
            'hazard_title': None,
            'exposure_title': None,
            'postprocessing_report': None,
        }

        # Stored information from aggregation layer
        self.aggregation = {'layer': None, 'aggregation_attribute': None}

        # Available aggregation layer
        self.available_aggregation = []

        # The summary report, contains report for each aggregation area
        self.summary_report = OrderedDict()

        # The html reports and its file path
        self.html_reports = OrderedDict()

        # A boolean flag whether to merge entire area or aggregated
        self.entire_area_mode = False

        # Get the global settings and override some variable if exist
        self.read_settings()

        # Get all current project layers for combo box
        self.get_project_layers()

        # 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)

        # Show usage info
        self.restore_state()

    def restore_state(self):
        """ Read last state of GUI from configuration file."""
        settings = QSettings()
        try:
            last_path = settings.value('directory', type=str)
        except TypeError:
            last_path = ''
        self.output_directory.setText(last_path)

    def save_state(self):
        """ Store current state of GUI to configuration file """
        settings = QSettings()
        settings.setValue('directory', self.output_directory.text())

    @pyqtSignature('')  # prevents actions being handled twice
    def on_directory_chooser_clicked(self):
        """Show a dialog to choose directory."""
        # noinspection PyCallByClass,PyTypeChecker
        self.output_directory.setText(
            QFileDialog.getExistingDirectory(
                self, self.tr('Select Output Directory')))

    @pyqtSignature('')  # prevents actions being handled twice
    def on_report_template_chooser_clicked(self):
        """Show a dialog to choose directory"""
        # noinspection PyCallByClass,PyTypeChecker
        report_template_path = QtGui.QFileDialog.getOpenFileName(
            self, self.tr('Select Report Template'), self.template_path,
            self.tr('QPT File (*.qpt)'))

        # noinspection PyCallByClass,PyTypeChecker
        self.report_template_le.setText(report_template_path)

    def accept(self):
        """Do merging two impact layers."""
        # Store the current state to configuration file
        self.save_state()

        # Prepare all the input from dialog, validate, and store it
        try:
            self.prepare_input()
        except (InvalidLayerError, EmptyDirectoryError,
                FileNotFoundError) as ex:
            # noinspection PyCallByClass,PyTypeChecker, PyArgumentList
            QMessageBox.information(
                self, self.tr("InaSAFE Merge Impact Tool Information"),
                str(ex))
            return
        except CanceledImportDialogError:
            return

        # Validate all the layers logically
        try:
            self.validate_all_layers()
        except (NoKeywordsFoundError, KeywordNotFoundError,
                InvalidLayerError) as ex:
            # noinspection PyCallByClass,PyTypeChecker, PyArgumentList
            QMessageBox.information(
                self, self.tr("InaSAFE Merge Impact Tools Information"),
                str(ex))
            return

        # The input is valid, do the merging
        # Set cursor to wait cursor
        QtGui.qApp.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor))
        # pylint: disable=W0703
        try:
            self.merge()
        except Exception as ex:
            # End wait cursor
            QtGui.qApp.restoreOverrideCursor()
            # noinspection PyCallByClass,PyTypeChecker,PyArgumentList
            QMessageBox.warning(self,
                                self.tr("InaSAFE Merge Impact Tools Error"),
                                str(ex))
            return
            # pylint: enable=W0703

        # Finish doing it. End wait cursor
        QtGui.qApp.restoreOverrideCursor()

        # Give user successful information!
        # noinspection PyCallByClass,PyTypeChecker, PyArgumentList
        QMessageBox.information(
            self, self.tr('InaSAFE Merge Impact Tool Information'),
            self.tr('Report from merging two impact layers was generated '
                    'successfully.'))

        # Open output directory on file explorer
        # noinspection PyArgumentList
        output_directory_url = QUrl.fromLocalFile(self.out_dir)
        # noinspection PyTypeChecker,PyCallByClass,PyArgumentList
        QDesktopServices.openUrl(output_directory_url)

    def read_settings(self):
        """Set some variables from global settings on inasafe options dialog.
        """
        settings = QtCore.QSettings()

        # Organisation logo
        organisation_logo_path = settings.value(
            'inasafe/organisation_logo_path', '', type=str)
        if organisation_logo_path != '':
            self.organisation_logo_path = organisation_logo_path

        # Disclaimer text
        customised_disclaimer = settings.value('inasafe/reportDisclaimer',
                                               '',
                                               type=str)
        if customised_disclaimer != '':
            self.disclaimer = customised_disclaimer

    def get_project_layers(self):
        """Get impact layers and aggregation layer currently loaded in QGIS."""
        # noinspection PyArgumentList,PyUnresolvedReferences
        registry = QgsMapLayerRegistry.instance()

        # MapLayers returns a QMap<QString id, QgsMapLayer layer>
        layers = registry.mapLayers().values()

        if len(layers) == 0:
            return

        # Clear the combo box first
        self.first_layer.clear()
        self.second_layer.clear()
        self.aggregation_layer.clear()
        # empty list
        self.available_aggregation[:] = []

        for layer in layers:
            try:
                self.keyword_io.read_keywords(layer, 'impact_summary')
            except (NoKeywordsFoundError, KeywordNotFoundError):
                # Check if it has aggregation keyword
                try:
                    self.keyword_io.read_keywords(layer,
                                                  'aggregation attribute')
                except (NoKeywordsFoundError, KeywordNotFoundError):
                    # Skip if there are no keywords at all
                    continue
                self.available_aggregation.append(layer)
                add_ordered_combo_item(self.aggregation_layer, layer.name(),
                                       layer)
                continue
            except (UnsupportedProviderError, InvalidParameterError):
                # UnsupportedProviderError:
                #   Encounter unsupported provider layer, e.g Open Layer
                # InvalidParameterError:
                #   Encounter invalid layer source,
                #   see https://github.com/AIFDR/inasafe/issues/754
                continue

            add_ordered_combo_item(self.first_layer, layer.name(), layer)
            add_ordered_combo_item(self.second_layer, layer.name(), layer)

        # Add Entire Area Option to Aggregated Layer:
        self.aggregation_layer.insertItem(0, self.tr('Entire Area'), None)
        self.aggregation_layer.setCurrentIndex(0)

    def prepare_input(self):
        """Fetch all the input from dialog, validate, and store it.

        Consider this as a bridge between dialog interface and our logical
        stored data in this class

        :raises: InvalidLayerError, CanceledImportDialogError
        """
        # Validate The combobox impact layers (they should be different)
        first_layer_index = self.first_layer.currentIndex()
        second_layer_index = self.second_layer.currentIndex()

        if first_layer_index < 0:
            raise InvalidLayerError(self.tr('First layer is not valid.'))

        if second_layer_index < 0:
            raise InvalidLayerError(self.tr('Second layer is not valid.'))

        if first_layer_index == second_layer_index:
            raise InvalidLayerError(
                self.tr('First layer must be different to second layer'
                        '.'))

        # Get all chosen layers
        self.first_impact['layer'] = self.first_layer.itemData(
            self.first_layer.currentIndex(), QtCore.Qt.UserRole)
        self.second_impact['layer'] = self.second_layer.itemData(
            self.second_layer.currentIndex(), QtCore.Qt.UserRole)
        self.aggregation['layer'] = self.aggregation_layer.itemData(
            self.aggregation_layer.currentIndex(), QtCore.Qt.UserRole)

        # Validate the output directory
        self.require_directory()

        # Get output directory
        self.out_dir = self.output_directory.text()

        # Whether to use own report template:
        if self.report_template_checkbox.isChecked():
            own_template_path = self.report_template_le.text()
            if os.path.isfile(own_template_path):
                self.template_path = own_template_path
            else:
                raise FileNotFoundError(
                    self.tr('Template file does not exist.'))

        # Flag whether to merge entire area or based on aggregation unit
        # Rizky: Fix nasty bug where the dialog stuck in entire_area_mode
        # the mode should be rechecked based on selected aggregation layer
        self.entire_area_mode = True
        if (self.aggregation_layer.currentIndex() > 0
                and not self.aggregation['layer'] is None):
            self.entire_area_mode = False

    def require_directory(self):
        """Ensure directory path entered in dialog exist.

        When the path does not exist, this function will ask the user if he
        wants to create it or not.

        :raises: CanceledImportDialogError - when user chooses 'No' in
            the question dialog for creating directory, or 'Yes' but the output
            directory path is empty
        """
        path = self.output_directory.text()

        if os.path.exists(path):
            return

        title = self.tr("Directory %s does not exist") % path
        question = self.tr(
            "Directory %s does not exist. Do you want to create it?") % path
        # noinspection PyCallByClass,PyTypeChecker
        answer = QMessageBox.question(self, title, question,
                                      QMessageBox.Yes | QMessageBox.No)

        if answer == QMessageBox.Yes:
            if len(path) != 0:
                os.makedirs(path)
            else:
                raise EmptyDirectoryError(
                    self.tr('Output directory cannot be empty.'))
        else:
            raise CanceledImportDialogError()

    def validate_all_layers(self):
        """Validate all layers based on the keywords.

        When we do the validation, we also fetch the information we need:

        1. 'map_title' from each impact layer
        2. 'exposure_title' from each impact layer
        3. 'postprocessing_report' from each impact layer
        4. 'aggregation_attribute' on aggregation layer, if user runs merging
           tools with aggregation layer chosen

        The things that we validate are:

        1. 'map_title' keyword must exist on each impact layer
        2. 'exposure_title' keyword must exist on each impact layer
        3. 'postprocessing_report' keyword must exist on each impact layer
        4. 'hazard_title' keyword must exist on each impact layer. Hazard title
           from first impact layer must be the same with second impact layer
           to indicate that both are generated from the same hazard layer.
        5. 'aggregation attribute' must exist when user wants to run merging
           tools with aggregation layer chosen.

        """
        required_attribute = [
            'map_title', 'exposure_title', 'hazard_title',
            'postprocessing_report'
        ]
        # Fetch for first impact layer
        for attribute in required_attribute:
            try:
                # noinspection PyTypeChecker
                self.first_impact[attribute] = self.keyword_io.read_keywords(
                    self.first_impact['layer'], attribute)
            except NoKeywordsFoundError:
                raise NoKeywordsFoundError(
                    self.tr('No keywords found for first impact layer.'))
            except KeywordNotFoundError:
                raise KeywordNotFoundError(
                    self.tr('Keyword %s not found for first layer.' %
                            attribute))

        # Fetch for second impact layer
        for attribute in required_attribute:
            try:
                # noinspection PyTypeChecker
                self.second_impact[attribute] = self.keyword_io.read_keywords(
                    self.second_impact['layer'], attribute)
            except NoKeywordsFoundError:
                raise NoKeywordsFoundError(
                    self.tr('No keywords found for second impact layer.'))
            except KeywordNotFoundError:
                raise KeywordNotFoundError(
                    self.tr('Keyword %s not found for second layer.' %
                            attribute))

        # Validate that two impact layers are obtained from the same hazard.
        # Indicated by the same 'hazard_title' (to be fixed later by using
        # more reliable method)
        if (self.first_impact['hazard_title'] !=
                self.second_impact['hazard_title']):
            raise InvalidLayerError(
                self.tr('First impact layer and second impact layer do not '
                        'use the same hazard layer.'))

        # Fetch 'aggregation_attribute'
        # If the chosen aggregation layer not Entire Area, it should have
        # aggregation attribute keywords
        if not self.entire_area_mode:
            try:
                # noinspection PyTypeChecker
                self.aggregation['aggregation_attribute'] = \
                    self.keyword_io.read_keywords(
                        self.aggregation['layer'], 'aggregation attribute')
            except NoKeywordsFoundError:
                raise NoKeywordsFoundError(
                    self.tr('No keywords exist in aggregation layer.'))
            except KeywordNotFoundError:
                raise KeywordNotFoundError(
                    self.tr('Keyword aggregation attribute not found for '
                            'aggregation layer.'))

    def merge(self):
        """Merge the postprocessing_report from each impact."""
        # Ensure there is always only a single root element or minidom moans
        first_postprocessing_report = \
            self.first_impact['postprocessing_report']
        second_postprocessing_report = \
            self.second_impact['postprocessing_report']
        # noinspection PyTypeChecker
        first_report = '<body>' + first_postprocessing_report + '</body>'
        # noinspection PyTypeChecker
        second_report = '<body>' + second_postprocessing_report + '</body>'

        # Now create a dom document for each
        first_document = minidom.parseString(get_string(first_report))
        second_document = minidom.parseString(get_string(second_report))
        first_impact_tables = first_document.getElementsByTagName('table')
        second_impact_tables = second_document.getElementsByTagName('table')

        # Now create dictionary report from DOM
        first_report_dict = self.generate_report_dictionary_from_dom(
            first_impact_tables)
        second_report_dict = self.generate_report_dictionary_from_dom(
            second_impact_tables)

        # Rizky: Consistency checks with aggregation
        # Make sure the aggregation layer both presents in both layers

        # We shouldn't have problems with Entire Area mode. It just means
        # the impact layer and summary is merged into single report.
        # We can have 3 cases:
        # 1. If both of them were not aggregated, we can just merge the map
        #    only
        # 2. If both of them were aggregated, we can just merge the map, and
        #    merge postprocessor report using 'Total aggregation in areas' key
        # 3. If only one of them were aggregated, we can just merge the map,
        #    and uses postprocessor report from the one who has.
        if self.entire_area_mode:
            # We won't be bothered with the map, it will be merged anyway in
            # all 3 cases. We should bother with the postprocessor report.
            # If one of them has the report, it means it will contain more
            # than one report keys. We can just swap the first report if they
            # have one key, and the second have more than one
            if (len(first_report_dict.keys()) == 1
                    and len(second_report_dict.keys()) > 1):
                swap_var = first_report_dict
                first_report_dict = second_report_dict
                second_report_dict = swap_var
        # This condition will covers aggregated mode
        # For this case, we should make sure both layers are aggregated with
        # the same aggregation layer of the chosen aggregation layer
        else:
            # check that both layers must have aggregated postprocessor.
            # aggregated postprocessor means the report_dict must have minimum
            # 2 keys
            if not (len(first_report_dict.keys()) > 1
                    and len(second_report_dict.keys()) > 1):
                raise InvalidLayerError(
                    self.tr(
                        'Please choose impact layers with aggregated '
                        'postprocessor if you want to use aggregation layer.'))

            # collect all report keys (will contain aggregation areas in the
            # report)
            report_keys = first_report_dict.keys()
            # Discard the last keys. It will always contains total area, not
            # aggregated area
            if len(report_keys) > 0:
                del report_keys[-1]

            sec_report = second_report_dict.keys()
            if len(sec_report) > 0:
                del sec_report[-1]

            for k in sec_report:
                if k not in report_keys:
                    report_keys.append(k)

            # collect all aggregation areas in aggregation layer
            layer = self.aggregation['layer']
            aggregation_attr = self.aggregation['aggregation_attribute']
            aggregation_attr_index = layer.fieldNameIndex(aggregation_attr)
            aggregation_keys = []
            for f in layer.getFeatures():
                area = f[aggregation_attr_index]
                if area not in aggregation_keys:
                    aggregation_keys.append(area)

            is_subset = True
            for k in report_keys:
                if k not in aggregation_keys:
                    is_subset = False

            if not is_subset:
                # This means report keys contains area keys that is not in
                # aggregation layer. Which means possibly it is using the
                # wrong aggregation layer.
                raise InvalidLayerError(
                    self.tr('First and Second layer does not use chosen '
                            'Aggregation layer'))

        # Generate report summary for all aggregation unit
        self.generate_report_summary(first_report_dict, second_report_dict)

        # Generate html reports file from merged dictionary
        self.generate_html_reports(first_report_dict, second_report_dict)

        # Generate PDF Reports using composer and/or atlas generation:
        self.generate_reports()

        # Delete html report files:
        for area in self.html_reports:
            report_path = self.html_reports[area]
            # Rizky : Fix possible bugs in Windows related to issue:
            # https://github.com/AIFDR/inasafe/issues/1862
            try:
                # avoid race condition using with statement
                with open(report_path, 'w') as report_file:
                    report_file.close()
                    os.remove(report_path)
            except OSError:
                pass

    @staticmethod
    def generate_report_dictionary_from_dom(html_dom):
        """Generate dictionary representing report from html dom.

        :param html_dom: Input representing document dom as report from each
            impact layer report.
        :type html_dom: str

        :return: Dictionary representing html_dom.
        :rtype: dict

        Dictionary Structure::

            { Aggregation_Area:
                {Exposure Type:{
                    Exposure Detail}
                }
            }

        Example::

           {"Jakarta Barat":
               {"Closed buildings":
                   {"Total inundated":150,
                    "Places of Worship": "No data"
                   }
               }
           }

        """
        merged_report_dict = OrderedDict()
        for table in html_dom:
            # noinspection PyUnresolvedReferences
            caption = table.getElementsByTagName('caption')[0].firstChild.data
            # noinspection PyUnresolvedReferences
            rows = table.getElementsByTagName('tr')
            header = rows[0]
            contains = rows[1:]
            for contain in contains:
                data = contain.getElementsByTagName('td')
                aggregation_area = data[0].firstChild.nodeValue
                exposure_dict = OrderedDict()
                if aggregation_area in merged_report_dict:
                    exposure_dict = merged_report_dict[aggregation_area]
                data_contain = data[1:]
                exposure_detail_dict = OrderedDict()
                for datum in data_contain:
                    index_datum = data.index(datum)
                    datum_header = \
                        header.getElementsByTagName('td')[index_datum]
                    datum_caption = datum_header.firstChild.nodeValue
                    exposure_detail_dict[datum_caption] = \
                        datum.firstChild.nodeValue
                exposure_dict[caption] = exposure_detail_dict
                merged_report_dict[aggregation_area] = exposure_dict
        return merged_report_dict

    def generate_report_summary(self, first_report_dict, second_report_dict):
        """Generate report summary for each aggregation area from merged
        report dictionary.

        For each exposure, search for the total only. Report dictionary looks
        like this:

        :param first_report_dict: Dictionary report from the first impact.
        :type first_report_dict: dict

        :param second_report_dict: Dictionary report from the second impact.
        :type second_report_dict: dict

        Dictionary structure::

            { aggregation_area:
                {exposure_type:{
                   exposure_detail}
                }
            }

        Example::

            {"Jakarta Barat":
                {"Closed buildings":
                    {"Total inundated":150,
                     "Places of Worship": "No data"
                    }
                }
            }

        """
        for aggregation_area in first_report_dict:
            html = ''
            html += '<table style="margin:0px auto">'

            # Summary total from first report
            html += '<tr><td><b>%s</b></td><td></td></tr>' % \
                    self.first_impact['exposure_title'].title()
            first_exposure_type_dict = first_report_dict[aggregation_area]
            first_exposure_type = first_exposure_type_dict.keys()[0]
            first_exposure_detail_dict = \
                first_exposure_type_dict[first_exposure_type]
            for datum in first_exposure_detail_dict:
                if self.tr('Total').lower() in datum.lower():
                    html += ('<tr>'
                             '<td>%s</td>'
                             '<td>%s</td>'
                             '</tr>') % \
                            (datum, first_exposure_detail_dict[datum])
                    break

            # Catch fallback for aggregation_area not exist in second_report
            if aggregation_area in second_report_dict:
                second_exposure_report_dict = second_report_dict[
                    aggregation_area]
                # Summary total from second report
                html += '<tr><td><b>%s</b></td><td></td></tr>' % \
                        self.second_impact['exposure_title'].title()
                second_exposure = second_exposure_report_dict.keys()[0]
                second_exposure_detail_dict = \
                    second_exposure_report_dict[second_exposure]
                for datum in second_exposure_detail_dict:
                    if self.tr('Total').lower() in datum.lower():
                        html += ('<tr>'
                                 '<td>%s</td>'
                                 '<td>%s</td>'
                                 '</tr>') % \
                                (datum, second_exposure_detail_dict[datum])
                        break

            html += '</table>'
            self.summary_report[aggregation_area.lower()] = html

    def generate_html_reports(self, first_report_dict, second_report_dict):
        """Generate html file for each aggregation units.

        It also saves the path of the each aggregation unit in
        self.html_reports.
        ::

            Ex. {"jakarta barat": "/home/jakarta barat.html",
                 "jakarta timur": "/home/jakarta timur.html"}

        :param first_report_dict: Dictionary report from first impact.
        :type first_report_dict: dict

        :param second_report_dict: Dictionary report from second impact.
        :type second_report_dict: dict
        """
        for aggregation_area in first_report_dict:
            html = html_header()
            html += ('<table width="100%" style="position:absolute;left:0px;"'
                     'class="table table-condensed table-striped">')
            html += '<caption><h4>%s</h4></caption>' % \
                    aggregation_area.title()

            html += '<tr>'

            # First impact on the left side
            html += '<td width="48%">'
            html += '<table width="100%">'
            html += '<thead><th>%s</th></thead>' % \
                    self.first_impact['exposure_title'].upper()
            first_exposure_report_dict = first_report_dict[aggregation_area]
            for first_exposure in first_exposure_report_dict:
                first_exposure_detail_dict = \
                    first_exposure_report_dict[first_exposure]
                html += '<tr><th><i>%s</i></th><th></th></tr>' % \
                        first_exposure.title()
                for datum in first_exposure_detail_dict:
                    html += ('<tr>'
                             '<td>%s</td>'
                             '<td>%s</td>'
                             '</tr>') % (datum,
                                         first_exposure_detail_dict[datum])
            html += '</table>'
            html += '</td>'

            # Second impact on the right side
            if aggregation_area in second_report_dict:
                # Add spaces between
                html += '<td width="4%">'
                html += '</td>'

                # Second impact report
                html += '<td width="48%">'
                html += '<table width="100%">'
                html += '<thead><th>%s</th></thead>' % \
                        self.second_impact['exposure_title'].upper()
                second_exposure_report_dict = \
                    second_report_dict[aggregation_area]
                for second_exposure in second_exposure_report_dict:
                    second_exposure_detail_dict = \
                        second_exposure_report_dict[second_exposure]
                    html += '<tr><th><i>%s</i></th><th></th></tr>' % \
                            second_exposure.title()
                    for datum in second_exposure_detail_dict:
                        html += ('<tr>'
                                 '<td>%s</td>'
                                 '<td>%s</td>'
                                 '</tr>') % \
                                (datum,
                                 second_exposure_detail_dict[datum])
                html += '</table>'
                html += '</td>'

            html += '</tr>'
            html += '</table>'
            html += html_footer()

            file_path = '%s.html' % aggregation_area
            path = os.path.join(temp_dir(self.__class__.__name__), file_path)
            html_to_file(html, path)
            self.html_reports[aggregation_area.lower()] = path

    def generate_reports(self):
        """Generate PDF reports for each aggregation unit using map composer.

        First the report template is loaded with the renderer from two
        impact layers. After it's loaded, if it is not aggregated then
        we just use composition to produce report. Since there are two
        impact maps here, we need to set a new extent for these impact maps
        by a simple calculation.

        If it is not aggregated then we use a powerful QGIS atlas generation
        on composition. Since we save each report table representing each
        aggregated area on self.html_report (which is a dictionary with the
        aggregation area name as a key and its path as a value), and we set
        the aggregation area name as current filename on atlas generation,
        we can match these two so that we have the right report table for
        each report.

        For those two cases, we use the same template. The report table is
        basically an HTML frame. Of course after the merging process is done,
        we delete each report table on self.html_reports physically on disk.
        """
        # Set the layer set
        layer_set = [
            self.first_impact['layer'].id(), self.second_impact['layer'].id()
        ]

        # If aggregated, append chosen aggregation layer
        if not self.entire_area_mode:
            layer_set.append(self.aggregation['layer'].id())

        # Instantiate Map Settings for Composition
        if qgis_version() < 20400:
            map_settings = QgsMapRenderer()
            map_settings.setLayerSet(layer_set)
        else:
            map_settings = QgsMapSettings()
            map_settings.setLayers(layer_set)

        # Create composition
        composition = self.load_template(map_settings)

        # Get Map
        composer_map = composition.getComposerItemById('impact-map')

        # Get HTML Report Frame
        html_report_item = \
            composition.getComposerItemById('merged-report-table')
        html_report_frame = composition.getComposerHtmlByItem(html_report_item)

        if self.entire_area_mode:
            # Get composer map size
            composer_map_width = composer_map.boundingRect().width()
            composer_map_height = composer_map.boundingRect().height()

            # Set the extent from two impact layers to fit into composer map
            composer_size_ratio = float(composer_map_height /
                                        composer_map_width)

            # The extent of two impact layers
            min_x = min(self.first_impact['layer'].extent().xMinimum(),
                        self.second_impact['layer'].extent().xMinimum())
            min_y = min(self.first_impact['layer'].extent().yMinimum(),
                        self.second_impact['layer'].extent().yMinimum())
            max_x = max(self.first_impact['layer'].extent().xMaximum(),
                        self.second_impact['layer'].extent().xMaximum())
            max_y = max(self.first_impact['layer'].extent().yMaximum(),
                        self.second_impact['layer'].extent().yMaximum())
            max_width = max_x - min_x
            max_height = max_y - min_y
            layers_size_ratio = float(max_height / max_width)
            center_x = min_x + float(max_width / 2.0)
            center_y = min_y + float(max_height / 2.0)

            # The extent should fit the composer map size
            new_width = max_width
            new_height = max_height

            # QgsComposerMap only overflows to height, so if it overflows,
            # the extent of the width should be widened
            if layers_size_ratio > composer_size_ratio:
                new_width = max_height / composer_size_ratio

            # Set new extent
            fit_min_x = center_x - (new_width / 2.0)
            fit_max_x = center_x + (new_width / 2.0)
            fit_min_y = center_y - (new_height / 2.0)
            fit_max_y = center_y + (new_height / 2.0)

            # Create the extent and set it to the map
            # noinspection PyCallingNonCallable
            map_extent = QgsRectangle(fit_min_x, fit_min_y, fit_max_x,
                                      fit_max_y)
            composer_map.setNewExtent(map_extent)

            # Add grid to composer map
            split_count = 5
            x_interval = new_width / split_count
            composer_map.setGridIntervalX(x_interval)
            y_interval = new_height / split_count
            composer_map.setGridIntervalY(y_interval)

            # Self.html_reports must have only 1 key value pair
            # Rizky: If layer components were aggregated, html_reports will
            # have several key value pairs. So we get the last value because
            # it is for the total/entire area
            area_title = list(self.html_reports.keys())[-1]

            # Set Report Summary
            summary_report = composition.getComposerItemById('summary-report')
            summary_report.setText(self.summary_report[area_title])

            # Set Aggregation Area Label
            area_label = composition.getComposerItemById('aggregation-area')
            area_label.setText(area_title.title())

            # Set merged-report-table
            html_report_path = self.html_reports[area_title]
            # noinspection PyArgumentList
            html_frame_url = QUrl.fromLocalFile(html_report_path)
            html_report_frame.setUrl(html_frame_url)

            # Export composition to PDF file
            file_name = '_'.join(area_title.split())
            file_path = '%s.pdf' % file_name
            path = os.path.join(self.out_dir, file_path)
            composition.exportAsPDF(path)
        else:
            # Create atlas composition:
            # noinspection PyCallingNonCallable
            atlas = QgsAtlasComposition(composition)

            # Set coverage layer
            # Map will be clipped by features from this layer:
            atlas.setCoverageLayer(self.aggregation['layer'])

            # Add grid to composer map from coverage layer
            split_count = 5
            map_width = self.aggregation['layer'].extent().width()
            map_height = self.aggregation['layer'].extent().height()
            x_interval = map_width / split_count
            composer_map.setGridIntervalX(x_interval)
            y_interval = map_height / split_count
            composer_map.setGridIntervalY(y_interval)

            # Set composer map that will be used for printing atlas
            atlas.setComposerMap(composer_map)

            # set output filename pattern
            atlas.setFilenamePattern(self.aggregation['aggregation_attribute'])

            # Start rendering
            atlas.beginRender()

            # Iterate all aggregation unit in aggregation layer
            for i in range(0, atlas.numFeatures()):
                atlas.prepareForFeature(i)

                current_filename = atlas.currentFilename()
                file_name = '_'.join(current_filename.split())
                file_path = '%s.pdf' % file_name
                path = os.path.join(self.out_dir, file_path)

                # Only print the area that has the report
                area_title = current_filename.lower()
                if area_title in self.summary_report:
                    # Set Report Summary
                    summary_report = composition.getComposerItemById(
                        'summary-report')
                    summary_report.setText(self.summary_report[area_title])

                    # Set Aggregation Area Label
                    area_label = composition.getComposerItemById(
                        'aggregation-area')
                    area_label.setText(area_title.title())

                    # Set merged-report-table
                    html_report_path = self.html_reports[area_title]
                    # noinspection PyArgumentList
                    html_frame_url = QUrl.fromLocalFile(html_report_path)
                    html_report_frame.setUrl(html_frame_url)

                    # Export composition to PDF file
                    composition.exportAsPDF(path)

            # End of rendering
            atlas.endRender()

    # noinspection PyArgumentList
    def load_template(self, map_settings):
        """Load composer template for merged report.

        Validate it as well. The template needs to have:
        1. QgsComposerMap with id 'impact-map' for merged impact map.
        2. QgsComposerPicture with id 'safe-logo' for InaSAFE logo.
        3. QgsComposerLabel with id 'summary-report' for a summary of two
        impacts.
        4. QgsComposerLabel with id 'aggregation-area' to indicate the area
        of aggregation.
        5. QgsComposerScaleBar with id 'map-scale' for impact map scale.
        6. QgsComposerLegend with id 'map-legend' for impact map legend.
        7. QgsComposerPicture with id 'organisation-logo' for organisation
        logo.
        8. QgsComposerLegend with id 'impact-legend' for map legend.
        9. QgsComposerHTML with id 'merged-report-table' for the merged report.

        :param map_settings: Map settings.
        :type map_settings: QgsMapSettings, QgsMapRenderer

        """
        # Create Composition
        template_composition = TemplateComposition(self.template_path,
                                                   map_settings)

        # Validate the component in the template
        component_ids = [
            'impact-map', 'safe-logo', 'summary-report', 'aggregation-area',
            'map-scale', 'map-legend', 'organisation-logo',
            'merged-report-table'
        ]
        template_composition.component_ids = component_ids
        if len(template_composition.missing_elements) > 0:
            raise ReportCreationError(
                self.tr('Components: %s could not be found' %
                        ', '.join(template_composition.missing_elements)))

        # Prepare map substitution and set to composition
        impact_title = '%s and %s' % (self.first_impact['map_title'],
                                      self.second_impact['map_title'])
        substitution_map = {
            'impact-title': impact_title,
            'hazard-title': self.first_impact['hazard_title'],
            'disclaimer': self.disclaimer
        }
        template_composition.substitution = substitution_map

        # Load Template
        try:
            template_composition.load_template()
        except TemplateLoadingError:
            raise

        # Draw Composition
        # Set InaSAFE logo
        composition = template_composition.composition
        safe_logo = composition.getComposerItemById('safe-logo')
        safe_logo.setPictureFile(self.safe_logo_path)

        # set organisation logo
        org_logo = composition.getComposerItemById('organisation-logo')
        org_logo.setPictureFile(self.organisation_logo_path)

        # Set Map Legend
        legend = composition.getComposerItemById('map-legend')

        if qgis_version() < 20400:
            layers = map_settings.layerSet()
        else:
            layers = map_settings.layers()

        if qgis_version() < 20600:
            legend.model().setLayerSet(layers)
            legend.synchronizeWithModel()
        else:
            root_group = legend.modelV2().rootGroup()

            layer_ids = map_settings.layers()
            for layer_id in layer_ids:
                # noinspection PyUnresolvedReferences
                layer = QgsMapLayerRegistry.instance().mapLayer(layer_id)
                root_group.addLayer(layer)
            legend.synchronizeWithModel()

        return composition

    @pyqtSlot()
    @pyqtSignature('bool')  # prevents actions being handled twice
    def help_toggled(self, flag):
        """Show or hide the help tab in the stacked widget.

        .. versionadded: 3.2.1

        :param flag: Flag indicating whether help should be shown or hidden.
        :type flag: bool
        """
        if flag:
            self.help_button.setText(self.tr('Hide Help'))
            self.show_help()
        else:
            self.help_button.setText(self.tr('Show Help'))
            self.hide_help()

    def hide_help(self):
        """Hide the usage info from the user.

        .. versionadded: 3.2.1
        """
        self.main_stacked_widget.setCurrentIndex(1)

    def show_help(self):
        """Show usage info to the user."""
        # Read the header and footer html snippets
        self.main_stacked_widget.setCurrentIndex(0)
        header = html_header()
        footer = html_footer()

        string = header

        message = impact_merge_help()

        string += message.to_html()
        string += footer

        self.help_web_view.setHtml(string)
Пример #50
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)
        except (TypeError, KeyError):
            # TypeError is for when function_parameters is none
            # KeyError is for when ['postprocessors'] is unavailable
            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
                user_parameters = self.function_parameters["postprocessors"][key]
                user_parameters = dict(
                    [(user_parameter.name, user_parameter.value) for user_parameter in user_parameters]
                )
                try:
                    # user parameters override default parameters
                    parameters.update(user_parameters)
                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:
                            LOGGER.debug("--- only default age ratios used ---")
                            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
Пример #51
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()

        # 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 = self.dock.get_exposure_layer()
        self.hazard_layer = self.dock.get_hazard_layer()
        self.aggregation_layer = self.dock.get_aggregation_layer()

        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
            QtGui.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.publicSource()
        hazard_path = self.hazard_layer.publicSource()
        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.publicSource()
            relative_aggregation_path = self.relative_path(
                scenario_file_path, aggregation_path)
            parser.set(title, 'aggregation', relative_aggregation_path)

        # noinspection PyBroadException
        try:
            parser.write(open(scenario_file_path, 'a'))
        except Exception as e:
            # noinspection PyTypeChecker,PyCallByClass,PyArgumentList
            QtGui.QMessageBox.warning(
                self, 'InaSAFE',
                tr('Failed to save scenario to {path}, exception '
                   '{exception}').format(path=scenario_file_path,
                                         exception=str(e)))

        # 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
Пример #52
0
class FieldMappingDialog(QDialog, FORM_CLASS):

    """Dialog implementation class for the InaSAFE field mapping tool."""

    def __init__(self, parent=None, iface=None, setting=None):
        """Constructor."""
        QDialog.__init__(self, parent)
        self.setupUi(self)

        self.setWindowTitle(self.tr('InaSAFE Field Mapping Tool'))
        icon = resources_path('img', 'icons', 'show-mapping-tool.svg')
        self.setWindowIcon(QIcon(icon))
        self.parent = parent
        self.iface = iface
        if setting is None:
            setting = QSettings()
        self.setting = setting

        self.keyword_io = KeywordIO()

        self.layer = None
        self.metadata = {}

        self.layer_input_layout = QHBoxLayout()
        self.layer_label = QLabel(tr('Layer'))
        self.layer_combo_box = QgsMapLayerComboBox()
        # Filter only for Polygon and Point
        self.layer_combo_box.setFilters(
            QgsMapLayerProxyModel.PolygonLayer |
            QgsMapLayerProxyModel.PointLayer)
        # Filter out a layer that don't have layer groups
        excepted_layers = []
        for i in range(self.layer_combo_box.count()):
            layer = self.layer_combo_box.layer(i)
            try:
                keywords = self.keyword_io.read_keywords(layer)
            except (KeywordNotFoundError, NoKeywordsFoundError):
                excepted_layers.append(layer)
                continue
            layer_purpose = keywords.get('layer_purpose')
            if not layer_purpose:
                excepted_layers.append(layer)
                continue
            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:
                excepted_layers.append(layer)
                continue
        self.layer_combo_box.setExceptedLayerList(excepted_layers)

        # Select the active layer.
        if self.iface.activeLayer():
            found = self.layer_combo_box.findText(
                self.iface.activeLayer().name())
            if found > -1:
                self.layer_combo_box.setLayer(self.iface.activeLayer())

        self.field_mapping_widget = None
        self.main_stacked_widget.setCurrentIndex(1)

        # Input
        self.layer_input_layout.addWidget(self.layer_label)
        self.layer_input_layout.addWidget(self.layer_combo_box)

        self.header_label = QLabel()
        self.header_label.setWordWrap(True)
        self.main_layout.addWidget(self.header_label)
        self.main_layout.addLayout(self.layer_input_layout)

        # Signal
        self.layer_combo_box.layerChanged.connect(self.set_layer)

        if self.layer_combo_box.currentLayer():
            self.set_layer(self.layer_combo_box.currentLayer())

        # Set up things for context help
        self.help_button = self.button_box.button(QDialogButtonBox.Help)
        # Allow toggling the help button
        self.help_button.setCheckable(True)
        self.help_button.toggled.connect(self.help_toggled)

        # Set up things for ok button
        self.ok_button = self.button_box.button(QDialogButtonBox.Ok)
        self.ok_button.clicked.connect(self.accept)

        # Set up things for cancel button
        self.cancel_button = self.button_box.button(QDialogButtonBox.Cancel)
        self.cancel_button.clicked.connect(self.reject)

    def set_layer(self, layer=None, keywords=None):
        """Set layer and update UI accordingly.

        :param layer: A QgsVectorLayer.
        :type layer: QgsVectorLayer

        :param keywords: Keywords for the layer.
        :type keywords: dict, None
        """
        if self.field_mapping_widget is not None:
            self.field_mapping_widget.setParent(None)
            self.field_mapping_widget.close()
            self.field_mapping_widget.deleteLater()
            self.main_layout.removeWidget(self.field_mapping_widget)

        if layer:
            self.layer = layer
        else:
            self.layer = self.layer_combo_box.currentLayer()
        if not self.layer:
            return

        if keywords is not None:
            self.metadata = keywords
        else:
            # Always read from metadata file.
            try:
                self.metadata = self.keyword_io.read_keywords(self.layer)
            except (
                    NoKeywordsFoundError,
                    KeywordNotFoundError,
                    MetadataReadError) as e:
                raise e
        if 'inasafe_default_values' not in self.metadata:
            self.metadata['inasafe_default_values'] = {}
        if 'inasafe_fields' not in self.metadata:
            self.metadata['inasafe_fields'] = {}
        self.field_mapping_widget = FieldMappingWidget(
            parent=self, iface=self.iface)
        self.field_mapping_widget.set_layer(self.layer, self.metadata)
        self.field_mapping_widget.show()
        self.main_layout.addWidget(self.field_mapping_widget)

        # Set header label
        group_names = [
            self.field_mapping_widget.tabText(i) for i in range(
                self.field_mapping_widget.count())]
        if len(group_names) == 0:
            header_text = tr(
                'There is no field group for this layer. Please select '
                'another layer.')
            self.header_label.setText(header_text)
            return
        elif len(group_names) == 1:
            pretty_group_name = group_names[0]
        elif len(group_names) == 2:
            pretty_group_name = group_names[0] + tr(' and ') + group_names[1]
        else:
            pretty_group_name = ', '.join(group_names[:-1])
            pretty_group_name += tr(', and {0}').format(group_names[-1])
        header_text = tr(
            'Please fill the information for every tab to determine the '
            'attribute for {0} group.').format(pretty_group_name)
        self.header_label.setText(header_text)

    @pyqtSlot()
    @pyqtSignature('bool')  # prevents actions being handled twice
    def help_toggled(self, flag):
        """Show or hide the help tab in the stacked widget.

        .. versionadded: 3.2.1

        :param flag: Flag indicating whether help should be shown or hidden.
        :type flag: bool
        """
        if flag:
            self.help_button.setText(self.tr('Hide Help'))
            self.show_help()
        else:
            self.help_button.setText(self.tr('Show Help'))
            self.hide_help()

    def hide_help(self):
        """Hide the usage info from the user.

        .. versionadded: 3.2.1
        """
        self.main_stacked_widget.setCurrentIndex(1)

    def show_help(self):
        """Show usage info to the user."""
        # Read the header and footer html snippets
        self.main_stacked_widget.setCurrentIndex(0)
        header = html_header()
        footer = html_footer()

        string = header

        message = field_mapping_help()

        string += message.to_html()
        string += footer

        self.help_web_view.setHtml(string)

    def save_metadata(self):
        """Save metadata based on the field mapping state."""
        metadata = self.field_mapping_widget.get_field_mapping()
        for key, value in metadata['fields'].items():
            # Delete the key if it's set to None
            if key in self.metadata['inasafe_default_values']:
                self.metadata['inasafe_default_values'].pop(key)
            if value is None or value == []:
                if key in self.metadata['inasafe_fields']:
                    self.metadata['inasafe_fields'].pop(key)
            else:
                self.metadata['inasafe_fields'][key] = value

        for key, value in metadata['values'].items():
            # Delete the key if it's set to None
            if key in self.metadata['inasafe_fields']:
                self.metadata['inasafe_fields'].pop(key)
            if value is None:
                if key in self.metadata['inasafe_default_values']:
                    self.metadata['inasafe_default_values'].pop(key)
            else:
                self.metadata['inasafe_default_values'][key] = value

        # Save metadata
        try:
            self.keyword_io.write_keywords(
                layer=self.layer, keywords=self.metadata)
        except InaSAFEError, e:
            error_message = get_error_message(e)
            # noinspection PyCallByClass,PyTypeChecker,PyArgumentList
            QMessageBox.warning(
                self, self.tr('InaSAFE'),
                ((self.tr(
                    'An error was encountered when saving the following '
                    'keywords:\n %s') % error_message.to_html())))

        # Update setting fir recent value
        if self.metadata.get('inasafe_default_values'):
            for key, value in \
                    self.metadata['inasafe_default_values'].items():
                set_inasafe_default_value_qsetting(
                    self.setting, key, RECENT, value)
Пример #53
0
class WizardDialog(QDialog, FORM_CLASS):
    """Dialog implementation class for the InaSAFE wizard."""

    resized = pyqtSignal()

    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

        self.resized.connect(self.after_resize)

    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>{layer_name}</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:
                    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:
                    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)
class MetadataConverterDialog(QDialog, FORM_CLASS):
    """Dialog implementation class for the InaSAFE metadata converter tool."""

    resized = pyqtSignal()

    def __init__(self, parent=None, iface=None):
        """Constructor."""
        QDialog.__init__(self, parent)
        self.setupUi(self)
        icon = resources_path('img', 'icons', 'show-metadata-converter.svg')
        self.setWindowIcon(QIcon(icon))
        self.setWindowTitle(self.tr('InaSAFE Metadata Converter'))
        self.parent = parent
        self.iface = iface

        self.keyword_io = KeywordIO()
        self.layer = None

        # Setup header label
        self.header_label.setText(
            tr('In this tool, you can convert a metadata 4.x of a layer to '
               'metadata 3.5. You will get a directory contains all the files of '
               'the layer and the new 3.5 metadata. If you want to convert '
               'hazard layer, you need to choose what exposure that you want to '
               'work with.'))

        # Setup input layer combo box
        # Filter our layers
        excepted_layers = []
        for i in range(self.input_layer_combo_box.count()):
            layer = self.input_layer_combo_box.layer(i)
            try:
                keywords = self.keyword_io.read_keywords(layer)
            except (KeywordNotFoundError, NoKeywordsFoundError):
                # Filter out if no keywords
                excepted_layers.append(layer)
                continue
            layer_purpose = keywords.get('layer_purpose')
            if not layer_purpose:
                # Filter out if no layer purpose
                excepted_layers.append(layer)
                continue
            if layer_purpose not in accepted_layer_purposes:
                # Filter out if not aggregation, hazard, or exposure layer
                excepted_layers.append(layer)
                continue
            keyword_version = keywords.get('keyword_version')
            if not keyword_version:
                # Filter out if no keyword version
                excepted_layers.append(layer)
                continue
            if not is_keyword_version_supported(keyword_version):
                # Filter out if the version is not supported (4.x)
                excepted_layers.append(layer)
                continue

        self.input_layer_combo_box.setExceptedLayerList(excepted_layers)

        # Select the active layer.
        if self.iface.activeLayer():
            found = self.input_layer_combo_box.findText(
                self.iface.activeLayer().name())
            if found > -1:
                self.input_layer_combo_box.setLayer(self.iface.activeLayer())

        # Set current layer as the active layer
        if self.input_layer_combo_box.currentLayer():
            self.set_layer(self.input_layer_combo_box.currentLayer())

        # Signals
        self.input_layer_combo_box.layerChanged.connect(self.set_layer)
        self.output_path_tool.clicked.connect(self.select_output_directory)

        # Set widget to show the main page (not help page)
        self.main_stacked_widget.setCurrentIndex(1)

        # Set up things for context help
        self.help_button = self.button_box.button(QDialogButtonBox.Help)
        # Allow toggling the help button
        self.help_button.setCheckable(True)
        self.help_button.toggled.connect(self.help_toggled)

        # Set up things for ok button
        self.ok_button = self.button_box.button(QDialogButtonBox.Ok)
        self.ok_button.clicked.connect(self.accept)

        # Set up things for cancel button
        self.cancel_button = self.button_box.button(QDialogButtonBox.Cancel)
        self.cancel_button.clicked.connect(self.reject)

        # The bug is fixed in QT 5.4
        if QT_VERSION < 0x050400:
            self.resized.connect(self.after_resize)

    def set_layer(self, layer=None):
        """Set layer and update UI accordingly.

        :param layer: A QgsVectorLayer.
        :type layer: QgsVectorLayer
        """
        if layer:
            self.layer = layer
        else:
            self.layer = self.input_layer_combo_box.currentLayer()
        if not self.layer:
            return

        try:
            keywords = self.keyword_io.read_keywords(layer)
            self.show_current_metadata()
        except NoKeywordsFoundError:
            # FIXME (elpaso)
            keywords = {}

        # TODO(IS): Show only possible exposure target
        if keywords.get('layer_purpose', False) == layer_purpose_hazard['key']:
            self.target_exposure_label.setEnabled(True)
            self.target_exposure_combo_box.setEnabled(True)
            self.target_exposure_combo_box.clear()
            for exposure in exposure_all:
                # Only show exposure that has active classification
                if active_classification(keywords, exposure['key']):
                    self.target_exposure_combo_box.addItem(
                        exposure['name'], exposure['key'])
        else:
            self.target_exposure_label.setEnabled(False)
            self.target_exposure_combo_box.setEnabled(False)
            self.target_exposure_combo_box.clear()
            self.target_exposure_combo_box.addItem(tr("Not Applicable"))

    def help_toggled(self, flag):
        """Show or hide the help tab in the stacked widget.

        .. versionadded: 3.2.1

        :param flag: Flag indicating whether help should be shown or hidden.
        :type flag: bool
        """
        if flag:
            self.help_button.setText(self.tr('Hide Help'))
            self.show_help()
        else:
            self.help_button.setText(self.tr('Show Help'))
            self.hide_help()

    def hide_help(self):
        """Hide the usage info from the user.

        .. versionadded: 3.2.1
        """
        self.main_stacked_widget.setCurrentIndex(1)

    def show_help(self):
        """Show usage info to the user."""
        # Read the header and footer html snippets
        self.main_stacked_widget.setCurrentIndex(0)
        header = html_header()
        footer = html_footer()
        message = metadata_converter_help()

        string = header
        string += message.to_html()
        string += footer

        self.help_web_view.setHtml(string)

    def convert_metadata(self):
        """Method invoked when OK button is clicked."""
        # Converter parameter
        converter_parameter = {}
        # Target exposure
        if self.target_exposure_combo_box.isEnabled():
            exposure_index = self.target_exposure_combo_box.currentIndex()
            exposure_key = self.target_exposure_combo_box.itemData(
                exposure_index, Qt.UserRole)
            converter_parameter['exposure'] = exposure_key

        # Metadata
        current_metadata = self.keyword_io.read_keywords(self.layer)
        old_metadata = convert_metadata(current_metadata,
                                        **converter_parameter)

        # Input
        input_layer_path = self.layer.source()
        input_directory_path = os.path.dirname(input_layer_path)
        input_file_name = os.path.basename(input_layer_path)
        input_base_name = os.path.splitext(input_file_name)[0]
        input_extension = os.path.splitext(input_file_name)[1]

        # Output
        output_path = self.output_path_line_edit.text()
        output_directory_path = os.path.dirname(output_path)
        output_file_name = os.path.basename(output_path)
        output_base_name = os.path.splitext(output_file_name)[0]

        # Copy all related files, if exists
        extensions = [
            # Vector layer
            '.shp',
            '.geojson',
            '.qml',
            '.shx',
            '.dbf',
            '.prj',
            'qpj',
            # Raster layer
            '.tif',
            '.tiff',
            '.asc',
        ]
        for extension in extensions:
            source_path = os.path.join(input_directory_path,
                                       input_base_name + extension)
            if not os.path.exists(source_path):
                continue
            target_path = os.path.join(output_directory_path,
                                       output_base_name + extension)
            QFile.copy(source_path, target_path)

        # Replace the metadata with the old one
        output_file_path = os.path.join(output_directory_path,
                                        output_base_name + input_extension)
        write_iso19115_metadata(output_file_path,
                                old_metadata,
                                version_35=True)

    def accept(self):
        """Method invoked when OK button is clicked."""
        output_path = self.output_path_line_edit.text()
        if not output_path:
            display_warning_message_box(self, tr('Empty Output Path'),
                                        tr('Output path can not be empty'))
            return
        try:
            self.convert_metadata()
        except MetadataConversionError as e:
            display_warning_message_box(self, tr('Metadata Conversion Failed'),
                                        str(e))
            return

        if not os.path.exists(output_path):
            display_warning_message_box(self, tr('Metadata Conversion Failed'),
                                        tr('Result file is not found.'))
            return

        display_success_message_bar(
            tr('Metadata Conversion Success'),
            tr('You can find your copied layer with metadata version 3.5 in '
               '%s' % output_path),
            iface_object=self.iface)
        super(MetadataConverterDialog, self).accept()

    def select_output_directory(self):
        """Select output directory"""
        # Input layer
        input_layer_path = self.layer.source()
        input_file_name = os.path.basename(input_layer_path)
        input_extension = os.path.splitext(input_file_name)[1]

        # Get current path
        current_file_path = self.output_path_line_edit.text()
        if not current_file_path or not os.path.exists(current_file_path):
            current_file_path = input_layer_path

        # Filtering based on input layer
        extension_mapping = {
            '.shp': tr('Shapefile (*.shp);;'),
            '.geojson': tr('GeoJSON (*.geojson);;'),
            '.tif': tr('Raster TIF/TIFF (*.tif, *.tiff);;'),
            '.tiff': tr('Raster TIF/TIFF (*.tiff, *.tiff);;'),
            '.asc': tr('Raster ASCII File (*.asc);;'),
        }
        # Open File Dialog
        file_path, __ = QFileDialog.getSaveFileName(
            self, tr('Output File'), current_file_path,
            extension_mapping[input_extension])
        if file_path:
            self.output_path_line_edit.setText(file_path)

    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)

    # Adapted from https://stackoverflow.com/a/43126946/1198772
    def resizeEvent(self, event):
        """Emit custom signal when the window is re-sized.

        :param event: The re-sized event.
        :type event: QResizeEvent
        """
        self.resized.emit()
        return super(MetadataConverterDialog, self).resizeEvent(event)

    def after_resize(self):
        """Method after resizing the window."""
        # https://stackoverflow.com/q/25644026/1198772
        # It's fixed in QT 5.4 https://bugreports.qt.io/browse/QTBUG-37673
        max_height = self.height() - 275  # Magic number, to make it pretty
        self.metadata_preview_web_view.setMaximumHeight(max_height)
Пример #55
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
Пример #56
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"))