Пример #1
0
    def show_keywords_editor(self):
        """Show the keywords editor."""
        # import here only so that it is AFTER i18n set up
        from safe_qgis.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
            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

        dialog = KeywordsDialog(
            self.iface.mainWindow(),
            self.iface,
            self.dock_widget)
        dialog.exec_()  # modal
Пример #2
0
    def show_keywords_editor(self):
        """Show the keywords editor."""
        # import here only so that it is AFTER i18n set up
        from safe_qgis.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
            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

        dialog = KeywordsDialog(self.iface.mainWindow(), self.iface,
                                self.dock_widget)
        dialog.exec_()  # modal
Пример #3
0
    def show_keywords_editor(self):
        """Show the keywords editor."""
        # import here only so that it is AFTER i18n set up
        from safe_qgis.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
Пример #4
0
 def test_printImpactTable(self):
     """Test that we can render html from impact table keywords."""
     LOGGER.debug('InaSAFE HtmlRenderer testing printImpactTable')
     myFilename = 'test_floodimpact.tif'
     myLayer, _ = load_layer(myFilename)
     myMessage = 'Layer is not valid: %s' % myFilename
     assert myLayer.isValid(), myMessage
     myPageDpi = 300
     myHtmlRenderer = HtmlRenderer(myPageDpi)
     myPath = unique_filename(prefix='impactTable',
                              suffix='.pdf',
                              dir=temp_dir('test'))
     myKeywordIO = KeywordIO()
     myKeywords = myKeywordIO.read_keywords(myLayer)
     myPath = myHtmlRenderer.print_impact_table(myKeywords,
                                              filename=myPath)
     myMessage = 'Rendered output does not exist: %s' % myPath
     assert os.path.exists(myPath), myMessage
     # pdf rendering is non deterministic so we can't do a hash check
     # test_renderComposition renders just the image instead of pdf
     # so we hash check there and here we just do a basic minimum file
     # size check.
     mySize = os.stat(myPath).st_size
     myExpectedSizes = [20936,  # as rendered on linux ub 12.04 64
                        21523,  # as rendered on linux ub 12.10 64
                        20605,  # as rendered on linux ub 13.04 64
                        21527,  # as rendered on Jenkins post 22 June 2013
                        377191,  # as rendered on OSX
                        252699L,  # as rendered on Windows 7 64 bit
                        251782L,  # as rendered on Windows 8 64 bit amd
                        21491,  # as rendered on Slackware64 14.0
                        ]
     print 'Output pdf to %s' % myPath
     self.assertIn(mySize, myExpectedSizes)
Пример #5
0
    def show_keywords_editor(self):
        """Show the keywords editor."""
        # import here only so that it is AFTER i18n set up
        from safe_qgis.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
            QMessageBox.warning(
                None,
                self.tr('Invalid Layer'),
                e.message)
            return
Пример #6
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
Пример #7
0
 def test_print_impact_table(self):
     """Test that we can render html from impact table keywords."""
     LOGGER.debug('InaSAFE HtmlRenderer testing printImpactTable')
     file_name = 'test_floodimpact.tif'
     layer, _ = load_layer(file_name)
     message = 'Layer is not valid: %s' % file_name
     self.assertTrue(layer.isValid(), message)
     page_dpi = 300
     html_renderer = HtmlRenderer(page_dpi)
     path = unique_filename(
         prefix='impact_table',
         suffix='.pdf',
         dir=temp_dir('test'))
     keyword_io = KeywordIO()
     keywords = keyword_io.read_keywords(layer)
     path = html_renderer.print_impact_table(keywords, filename=path)
     message = 'Rendered output does not exist: %s' % path
     self.assertTrue(os.path.exists(path), message)
     # pdf rendering is non deterministic so we can't do a hash check
     # test_renderComposition renders just the image instead of pdf
     # so we hash check there and here we just do a basic minimum file
     # size check.
     size = os.stat(path).st_size
     expected_sizes = [
         20936,  # as rendered on linux ub 12.04 64
         21523,  # as rendered on linux ub 12.10 64
         20605,  # as rendered on linux ub 13.04 64
         13965,  # as rendered on linux ub 13.10 64
         14220,  # as rendered on linux ub 13.04 64 MB
         11085,  # as rendered on linux ub 14.04 64 AG
         17306,  # as rendered on linux ub 14.04_64 TS
         17127,  # as rendered on linux ub 14.04_64 MB
         17295,  # as rendered on linux ub 14.04_64 IS
         18665,  # as rendered on Jenkins per 19 June 2014
         377191,  # as rendered on OSX
         17556,  # as rendered on Windows 7_32
         16163L,  # as rendered on Windows 7 64 bit Ultimate i3
         251782L,  # as rendered on Windows 8 64 bit amd
         21491,  # as rendered on Slackware64 14.0
         18667,  # as rendered on Linux Mint 14_64
     ]
     print 'Output pdf to %s' % path
     self.assertIn(size, expected_sizes)
Пример #8
0
 def test_print_impact_table(self):
     """Test that we can render html from impact table keywords."""
     LOGGER.debug('InaSAFE HtmlRenderer testing printImpactTable')
     file_name = 'test_floodimpact.tif'
     layer, _ = load_layer(file_name)
     message = 'Layer is not valid: %s' % file_name
     self.assertTrue(layer.isValid(), message)
     page_dpi = 300
     html_renderer = HtmlRenderer(page_dpi)
     path = unique_filename(prefix='impact_table',
                            suffix='.pdf',
                            dir=temp_dir('test'))
     keyword_io = KeywordIO()
     keywords = keyword_io.read_keywords(layer)
     path = html_renderer.print_impact_table(keywords, filename=path)
     message = 'Rendered output does not exist: %s' % path
     self.assertTrue(os.path.exists(path), message)
     # pdf rendering is non deterministic so we can't do a hash check
     # test_renderComposition renders just the image instead of pdf
     # so we hash check there and here we just do a basic minimum file
     # size check.
     size = os.stat(path).st_size
     expected_sizes = [
         20936,  # as rendered on linux ub 12.04 64
         21523,  # as rendered on linux ub 12.10 64
         20605,  # as rendered on linux ub 13.04 64
         13965,  # as rendered on linux ub 13.10 64
         14220,  # as rendered on linux ub 13.04 64 MB
         11085,  # as rendered on linux ub 14.04 64 AG
         17306,  # as rendered on linux ub 14.04_64 TS
         17127,  # as rendered on linux ub 14.04_64 MB
         17295,  # as rendered on linux ub 14.04_64 IS
         18665,  # as rendered on Jenkins per 19 June 2014
         377191,  # as rendered on OSX
         17556,  # as rendered on Windows 7_32
         16163L,  # as rendered on Windows 7 64 bit Ultimate i3
         251782L,  # as rendered on Windows 8 64 bit amd
         21491,  # as rendered on Slackware64 14.0
         18667,  # as rendered on Linux Mint 14_64
     ]
     print 'Output pdf to %s' % path
     self.assertIn(size, expected_sizes)
Пример #9
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.user_extent is not None \
                and self.dock.user_extent_crs is not None:
            extent = extent_to_array(self.dock.user_extent,
                                     self.dock.user_extent_crs)
        else:
            extent = viewport_geo_array(self.iface.mapCanvas())
        extent_string = ', '.join(('%f' % x) for x in extent)

        exposure_path = str(self.exposure_layer.publicSource())
        hazard_path = str(self.hazard_layer.publicSource())
        title = self.keyword_io.read_keywords(self.hazard_layer, 'title')
        title = safeTr(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 = str(
                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)
        parser.set(title, 'extent_crs', self.dock.user_extent_crs.authid())
        if self.aggregation_layer is not None:
            aggregation_path = str(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
Пример #10
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
        extent = viewport_geo_array(self.iface.mapCanvas())
        extent_string = ', '.join(('%f' % x) for x in extent)

        exposure_path = str(self.exposure_layer.publicSource())
        hazard_path = str(self.hazard_layer.publicSource())
        title = self.keyword_io.read_keywords(self.hazard_layer, 'title')
        title = safeTr(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 = str(
                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.aggregation_layer is not None:
            aggregation_path = str(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
Пример #11
0
class PostprocessorManager(QtCore.QObject):
    """A manager for post processing of impact function results.
    """

    def __init__(
            self,
            theAggregator):
        """Director for aggregation based operations.
        Args:
          theAggregationLayer: QgsMapLayer representing clipped
              aggregation. This will be converted to a memory layer inside
              this class. see self.aggregator.layer
        Returns:
           not applicable
        Raises:
           no exceptions explicitly raised
        """

        super(PostprocessorManager, self).__init__()

        # Aggregation / post processing related items
        self.postProcessingOutput = {}
        self.keywordIO = KeywordIO()
        self.errorMessage = None

        self.aggregator = theAggregator

    def _sumFieldName(self):
        return self.aggregator.prefix + 'sum'

    def _sortNoData(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

        Args:
            list - data

        Returns:
            returns -1 if the value is NO_DATA else the value
        """
        #black magic to get the value of each postprocessor field
        #get the first postprocessor just to discover the data structure
        myFirsPostprocessor = self.postProcessingOutput.itervalues().next()
        #get the key position of the value field
        myValueKey = myFirsPostprocessor[0][1].keyAt(0)

        #get the value
        # data[1] is the orderedDict
        # data[1][myFirstKey] is the 1st indicator in the orderedDict
        if data[1][myValueKey]['value'] == self.aggregator.defaults['NO_DATA']:
            myPosition = -1
        else:
            myPosition = data[1][myValueKey]['value']
            #FIXME MB this is to dehumanize the strings and have ints
            myPosition = myPosition.replace(',', '')
            myPosition = int(float(myPosition))

        return myPosition

    def _generateTables(self):
        """Parses the postprocessing output as one table per postprocessor.

        Args:
            None

        Returns:
            str - a string containing the html
        """
        myMessage = m.Message()

        for proc, resList in self.postProcessingOutput.iteritems():
            # resList 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'}})
            #    ]))
            #]
            try:
                #sorting using the first indicator of a postprocessor
                sortedResList = sorted(
                    resList,
                    key=self._sortNoData,
                    reverse=True)

            except KeyError:
                LOGGER.debug('Skipping sorting as the postprocessor did not '
                             'have a "Total" field')

            #init table
            hasNoDataValues = False
            myTable = m.Table(
                style_class='table table-condensed table-striped')
            myTable.caption = self.tr('Detailed %1 report').arg(safeTr(
                get_postprocessor_human_name(proc)).lower())

            myHeaderRow = m.Row()
            myHeaderRow.add(str(self.attributeTitle).capitalize())
            for calculationName in sortedResList[0][1]:
                myHeaderRow.add(self.tr(calculationName))
            myTable.add(myHeaderRow)

            for zoneName, calc in sortedResList:
                myRow = m.Row(zoneName)

                for _, calculationData in calc.iteritems():
                    myValue = calculationData['value']
                    if myValue == self.aggregator.defaults['NO_DATA']:
                        hasNoDataValues = True
                        myValue += ' *'
                    myRow.add(myValue)
                myTable.add(myRow)

            #add table to message
            myMessage.add(myTable)
            if hasNoDataValues:
                myMessage.add(m.EmphasizedText(self.tr(
                    '* "%1" values mean that there where some problems while '
                    'calculating them. This did not affect the other '
                    'values.').arg(self.aggregator.defaults['NO_DATA'])))

        try:
            if (self.keywordIO.read_keywords(
                    self.aggregator.layer, 'HAD_MULTIPART_POLY')):
                myMessage.add(m.EmphasizedText(self.tr(
                    'The aggregation layer had multipart polygons, these have '
                    'been exploded and are now marked with a #. This has no '
                    'influence on the calculation, just keep in mind that the '
                    'attributes shown may represent the original multipart '
                    'polygon and not the individual exploded polygon parts.')))
        except Exception:  # pylint: disable=W0703
            pass

        return myMessage

    def run(self):
        """Run any post processors requested by the impact function.

        Args:
            None

        Returns:
            None

        Raises:
            None
        """
        try:
            myRequestedPostProcessors = self.functionParams['postprocessors']
            myPostProcessors = get_postprocessors(myRequestedPostProcessors)
        except (TypeError, KeyError):
            # TypeError is for when functionParams is none
            # KeyError is for when ['postprocessors'] is unavailable
            myPostProcessors = {}
        LOGGER.debug('Running this postprocessors: ' + str(myPostProcessors))

        myFeatureNameAttribute = self.aggregator.attributes[
            self.aggregator.defaults['AGGR_ATTR_KEY']]
        if myFeatureNameAttribute is None:
            self.attributeTitle = self.tr('Aggregation unit')
        else:
            self.attributeTitle = myFeatureNameAttribute

        myNameFieldIndex = self.aggregator.layer.fieldNameIndex(
            self.attributeTitle)
        mySumFieldIndex = self.aggregator.layer.fieldNameIndex(
            self._sumFieldName())

        myFemaleRatioIsVariable = False
        myFemRatioFieldIndex = None
        myFemaleRatio = None

        if 'Gender' in myPostProcessors:
            #look if we need to look for a variable female ratio in a layer
            try:
                myFemRatioField = self.aggregator.attributes[
                    self.aggregator.defaults['FEM_RATIO_ATTR_KEY']]
                myFemRatioFieldIndex = self.aggregator.layer.fieldNameIndex(
                    myFemRatioField)
                myFemaleRatioIsVariable = True

            except KeyError:
                try:
                    myFemaleRatio = self.keywordIO.read_keywords(
                        self.aggregator.layer,
                        self.aggregator.defaults['FEM_RATIO_KEY'])
                except KeywordNotFoundError:
                    myFemaleRatio = self.aggregator.defaults['FEM_RATIO']

        #iterate zone features
        myProvider = self.aggregator.layer.dataProvider()
        myAttributes = myProvider.attributeIndexes()
        # start data retreival: fetch no geometry and all attributes for each
        # feature
        myProvider.select(myAttributes, QgsRectangle(), False)
        myFeature = QgsFeature()
        myPolygonIndex = 0
        while myProvider.nextFeature(myFeature):
            #get all attributes of a feature
            myAttributeMap = myFeature.attributeMap()

            #if a feature has no field called
            if myNameFieldIndex == -1:
                myZoneName = str(myFeature.id())
            else:
                myZoneName = myAttributeMap[myNameFieldIndex].toString()

            #create dictionary of attributes to pass to postprocessor
            myGeneralParams = {'target_field': self.aggregator.targetField}

            if self.aggregator.statisticsType == 'class_count':
                myGeneralParams['impact_classes'] = (
                    self.aggregator.statisticsClasses)
            elif self.aggregator.statisticsType == 'sum':
                myImpactTotal, _ = myAttributeMap[mySumFieldIndex].toDouble()
                myGeneralParams['impact_total'] = myImpactTotal

            try:
                myGeneralParams['impact_attrs'] = (
                    self.aggregator.impactLayerAttributes[myPolygonIndex])
            except IndexError:
                #rasters and attributeless vectors have no attributes
                myGeneralParams['impact_attrs'] = None

            for myKey, myValue in myPostProcessors.iteritems():
                myParameters = myGeneralParams
                try:
                    #look if params are available for this postprocessor
                    myParameters.update(
                        self.functionParams['postprocessors'][myKey]['params'])
                except KeyError:
                    pass

                if myKey == 'Gender':
                    if myFemaleRatioIsVariable:
                        myFemaleRatio, mySuccessFlag = myAttributeMap[
                            myFemRatioFieldIndex].toDouble()
                        if not mySuccessFlag:
                            myFemaleRatio = self.aggregator.defaults[
                                'FEM_RATIO']
                        LOGGER.debug(mySuccessFlag)
                    myParameters['female_ratio'] = myFemaleRatio

                myValue.setup(myParameters)
                myValue.process()
                myResults = myValue.results()
                myValue.clear()
#                LOGGER.debug(myResults)
                try:
                    self.postProcessingOutput[myKey].append(
                        (myZoneName, myResults))
                except KeyError:
                    self.postProcessingOutput[myKey] = []
                    self.postProcessingOutput[myKey].append(
                        (myZoneName, myResults))
            #increment the index
            myPolygonIndex += 1

    def getOutput(self):
        """Returns the results of the post processing as a table.

        Args:
            theSingleTableFlag - bool indicating if result should be rendered
                as a single table. Default False.

        Returns: str - a string containing the html in the requested format.
        """

        # LOGGER.debug(self.postProcessingOutput)
        if self.errorMessage is not None:
            myMessage = m.Message(
                m.Heading(self.tr('Postprocessing report skipped')),
                m.Paragraph(self.tr(
                    'Due to a problem while processing the results,'
                    ' the detailed postprocessing report is unavailable:'
                    ' %1').arg(self.errorMessage)))
            return myMessage

        return self._generateTables()
Пример #12
0
class KeywordsDialog(QtGui.QDialog, Ui_KeywordsDialogBase):
    """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()))
        self.keywordIO = 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.standardExposureList = OrderedDict(
            [
                ("population", self.tr("population")),
                ("structure", self.tr("structure")),
                ("Not Set", self.tr("Not Set")),
            ]
        )
        self.standardHazardList = 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")),
                ("Not Set", self.tr("Not Set")),
            ]
        )
        # Save reference to the QGIS interface and parent
        self.iface = iface
        self.parent = parent
        self.dock = dock

        self.lstKeywords.itemClicked.connect(self.edit_key_value_pair)

        # Set up help dialog showing logic.
        self.helpDialog = None
        helpButton = self.buttonBox.button(QtGui.QDialogButtonBox.Help)
        helpButton.clicked.connect(self.show_help)

        # set some inital ui state:
        self.defaults = breakdown_defaults()
        self.pbnAdvanced.setChecked(True)
        self.pbnAdvanced.toggle()
        self.radPredefined.setChecked(True)
        self.dsbFemaleRatioDefault.blockSignals(True)
        self.dsbFemaleRatioDefault.setValue(self.defaults["FEM_RATIO"])
        self.dsbFemaleRatioDefault.blockSignals(False)
        # myButton = self.buttonBox.button(QtGui.QDialogButtonBox.Ok)
        # myButton.setEnabled(False)
        if layer is None:
            self.layer = self.iface.activeLayer()
        else:
            self.layer = layer
        if self.layer:
            self.load_state_from_keywords()

        # add a reload from keywords button
        reloadButton = self.buttonBox.addButton(self.tr("Reload"), QtGui.QDialogButtonBox.ActionRole)
        reloadButton.clicked.connect(self.load_state_from_keywords)

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

    def show_help(self):
        """Load the help text for the keywords dialog."""
        show_context_help(context="keywords")

    def toggle_postprocessing_widgets(self):
        """Hide or show the post processing widgets depending on context.
        """
        LOGGER.debug("togglePostprocessingWidgets")
        isPostprocessingOn = self.radPostprocessing.isChecked()
        self.cboSubcategory.setVisible(not isPostprocessingOn)
        self.lblSubcategory.setVisible(not isPostprocessingOn)
        self.show_aggregation_attribute(isPostprocessingOn)
        self.show_female_ratio_attribute(isPostprocessingOn)
        self.show_female_ratio_default(isPostprocessingOn)

    def show_aggregation_attribute(self, visible_flag):
        """Hide or show the aggregation attribute in the keyword editor dialog.

        :param visible_flag: Flag indicating if the aggregation attribute
            should be hidden or shown.
        :type visible_flag: bool
        """
        theBox = self.cboAggregationAttribute
        theBox.blockSignals(True)
        theBox.clear()
        theBox.blockSignals(False)
        if visible_flag:
            currentKeyword = self.get_value_for_key(self.defaults["AGGR_ATTR_KEY"])
            fields, attributePosition = layer_attribute_names(
                self.layer, [QtCore.QVariant.Int, QtCore.QVariant.String], currentKeyword
            )
            theBox.addItems(fields)
            if attributePosition is None:
                theBox.setCurrentIndex(0)
            else:
                theBox.setCurrentIndex(attributePosition)

        theBox.setVisible(visible_flag)
        self.lblAggregationAttribute.setVisible(visible_flag)

    def show_female_ratio_attribute(self, visible_flag):
        """Hide or show the female ratio attribute in the dialog.

        :param visible_flag: Flag indicating if the female ratio attribute
            should be hidden or shown.
        :type visible_flag: bool
        """
        theBox = self.cboFemaleRatioAttribute
        theBox.blockSignals(True)
        theBox.clear()
        theBox.blockSignals(False)
        if visible_flag:
            currentKeyword = self.get_value_for_key(self.defaults["FEM_RATIO_ATTR_KEY"])
            fields, attributePosition = layer_attribute_names(self.layer, [QtCore.QVariant.Double], currentKeyword)
            fields.insert(0, self.tr("Use default"))
            fields.insert(1, self.tr("Don't use"))
            theBox.addItems(fields)
            if currentKeyword == self.tr("Use default"):
                theBox.setCurrentIndex(0)
            elif currentKeyword == self.tr("Don't use"):
                theBox.setCurrentIndex(1)
            elif attributePosition is None:
                # currentKeyword was not found in the attribute table.
                # Use default
                theBox.setCurrentIndex(0)
            else:
                # + 2 is because we add use defaults and don't use
                theBox.setCurrentIndex(attributePosition + 2)
        theBox.setVisible(visible_flag)
        self.lblFemaleRatioAttribute.setVisible(visible_flag)

    def show_female_ratio_default(self, visible_flag):
        """Hide or show the female ratio default attribute in the dialog.

        :param visible_flag: Flag indicating if the female ratio
            default attribute should be hidden or shown.
        :type visible_flag: bool
        """
        theBox = self.dsbFemaleRatioDefault
        if visible_flag:
            currentValue = self.get_value_for_key(self.defaults["FEM_RATIO_KEY"])
            if currentValue is None:
                val = self.defaults["FEM_RATIO"]
            else:
                val = float(currentValue)
            theBox.setValue(val)

        theBox.setVisible(visible_flag)
        self.lblFemaleRatioDefault.setVisible(visible_flag)

    # prevents actions being handled twice
    @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
        self.add_list_entry(self.defaults["AGGR_ATTR_KEY"], self.cboAggregationAttribute.currentText())

    # prevents actions being handled twice
    @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
        text = self.cboFemaleRatioAttribute.currentText()
        if text == self.tr("Use default"):
            self.dsbFemaleRatioDefault.setEnabled(True)
            currentDefault = self.get_value_for_key(self.defaults["FEM_RATIO_KEY"])
            if currentDefault is None:
                self.add_list_entry(self.defaults["FEM_RATIO_KEY"], self.dsbFemaleRatioDefault.value())
        else:
            self.dsbFemaleRatioDefault.setEnabled(False)
            self.remove_item_by_key(self.defaults["FEM_RATIO_KEY"])
        self.add_list_entry(self.defaults["FEM_RATIO_ATTR_KEY"], text)

    # prevents actions being handled twice
    @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
        box = self.dsbFemaleRatioDefault
        if box.isEnabled():
            self.add_list_entry(self.defaults["FEM_RATIO_KEY"], box.value())

    # prevents actions being handled twice
    @pyqtSignature("bool")
    def on_pbnAdvanced_toggled(self, flag):
        """Automatic slot executed when the advanced button is toggled.

        .. note:: some of the behaviour for hiding widgets is done using
           the signal/slot editor in designer, so if you are trying to figure
           out how the interactions work, look there too!

        :param flag: Flag indicating the new checked state of the button.
        :type flag: bool
        """
        self.toggle_advanced(flag)

    def toggle_advanced(self, flag):
        """Hide or show advanced editor.

        :param flag: Desired state for advanced editor visibility.
        :type flag: bool
        """
        if flag:
            self.pbnAdvanced.setText(self.tr("Hide advanced editor"))
        else:
            self.pbnAdvanced.setText(self.tr("Show advanced editor"))
        self.grpAdvanced.setVisible(flag)
        self.resize_dialog()

    # prevents actions being handled twice
    @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
    @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
    @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 not flag:
            self.remove_item_by_key(self.defaults["AGGR_ATTR_KEY"])
            self.remove_item_by_key(self.defaults["FEM_RATIO_ATTR_KEY"])
            self.remove_item_by_key(self.defaults["FEM_RATIO_KEY"])
            return
        self.set_category("postprocessing")
        self.update_controls_from_list()

    # prevents actions being handled twice
    @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.
        """
        del index
        myItem = self.cboSubcategory.itemData(self.cboSubcategory.currentIndex())
        myText = str(myItem)
        # I found that myText is 'Not Set' for every language
        if myText == self.tr("Not Set") or myText == "Not Set":
            self.remove_item_by_key("subcategory")
            return
        myTokens = myText.split(" ")
        if len(myTokens) < 1:
            self.remove_item_by_key("subcategory")
            return
        mySubcategory = myTokens[0]
        self.add_list_entry("subcategory", mySubcategory)

        # Some subcategories e.g. roads have no units or datatype
        if len(myTokens) == 1:
            return
        if myTokens[1].find("[") < 0:
            return
        myCategory = self.get_value_for_key("category")
        if "hazard" == myCategory:
            myUnits = myTokens[1].replace("[", "").replace("]", "")
            self.add_list_entry("unit", myUnits)
        if "exposure" == myCategory:
            myDataType = myTokens[1].replace("[", "").replace("]", "")
            self.add_list_entry("datatype", myDataType)
            # 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()
        theSelectedItemNone = selected_item is not None
        theSelectedItemInValues = selected_item not in entries.values()
        theSelectedItemInKeys = selected_item not in entries.keys()
        if theSelectedItemNone and theSelectedItemInValues and theSelectedItemInKeys:
            # Add it to the OrderedList
            entries[selected_item] = selected_item
        myIndex = 0
        mySelectedIndex = 0
        for myKey, myValue in entries.iteritems():
            if myValue == selected_item or myKey == selected_item:
                mySelectedIndex = myIndex
            myIndex += 1
            self.cboSubcategory.addItem(myValue, myKey)
        self.cboSubcategory.setCurrentIndex(mySelectedIndex)
        self.cboSubcategory.blockSignals(False)

    # prevents actions being handled twice
    @pyqtSignature("")
    def on_pbnAddToList1_clicked(self):
        """Automatic slot executed when the pbnAddToList1 button is pressed.
        """
        if self.lePredefinedValue.text() != "" and self.cboKeyword.currentText() != "":
            myCurrentKey = self.tr(self.cboKeyword.currentText())
            myCurrentValue = self.lePredefinedValue.text()
            self.add_list_entry(myCurrentKey, myCurrentValue)
            self.lePredefinedValue.setText("")
            self.update_controls_from_list()

    # prevents actions being handled twice
    @pyqtSignature("")
    def on_pbnAddToList2_clicked(self):
        """Automatic slot executed when the pbnAddToList2 button is pressed.
        """

        myCurrentKey = self.leKey.text()
        myCurrentValue = self.leValue.text()
        if myCurrentKey == "category" and myCurrentValue == "hazard":
            self.radHazard.blockSignals(True)
            self.radHazard.setChecked(True)
            self.set_subcategory_list(self.standardHazardList)
            self.radHazard.blockSignals(False)
        elif myCurrentKey == "category" and myCurrentValue == "exposure":
            self.radExposure.blockSignals(True)
            self.radExposure.setChecked(True)
            self.set_subcategory_list(self.standardExposureList)
            self.radExposure.blockSignals(False)
        elif myCurrentKey == "category":
            # .. todo:: notify the user their category is invalid
            pass
        self.add_list_entry(myCurrentKey, myCurrentValue)
        self.leKey.setText("")
        self.leValue.setText("")
        self.update_controls_from_list()

    # prevents actions being handled twice
    @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 myItem in self.lstKeywords.selectedItems():
            self.lstKeywords.takeItem(self.lstKeywords.row(myItem))
        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)
        myMessage = ""
        if ":" in key:
            key = key.replace(":", ".")
            myMessage = self.tr('Colons are not allowed, replaced with "."')
        if ":" in value:
            value = value.replace(":", ".")
            myMessage = self.tr('Colons are not allowed, replaced with "."')
        if myMessage == "":
            self.lblMessage.setText("")
            self.lblMessage.hide()
        else:
            self.lblMessage.setText(myMessage)
            self.lblMessage.show()
        myItem = QtGui.QListWidgetItem(key + ":" + value)
        # We are going to replace, so remove it if it exists already
        self.remove_item_by_key(key)
        myData = key + "|" + value
        myItem.setData(QtCore.Qt.UserRole, myData)
        self.lstKeywords.insertItem(0, myItem)

    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
        myCategory = str(category)
        if self.get_value_for_key("category") == myCategory:
            # nothing to do, go home
            return True
        if myCategory 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 myCategory == "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")
            myList = self.standardHazardList
            self.set_subcategory_list(myList)

        elif myCategory == "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")
            myList = self.standardExposureList
            self.set_subcategory_list(myList)

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

    def remove_item_by_key(self, key):
        """Remove an item from the kvp list given its key.

        :param key: Key of item to be removed.
        :type key: str
        """
        for myCounter in range(self.lstKeywords.count()):
            myExistingItem = self.lstKeywords.item(myCounter)
            myText = myExistingItem.text()
            myTokens = myText.split(":")
            if len(myTokens) < 2:
                break
            myKey = myTokens[0]
            if myKey == key:
                # remove it since the key is already present
                self.lstKeywords.takeItem(myCounter)
                break

    def remove_item_by_value(self, value):
        """Remove an item from the kvp list given its key.

        :param value: Value of item to be removed.
        :type value: str
        """
        for myCounter in range(self.lstKeywords.count()):
            myExistingItem = self.lstKeywords.item(myCounter)
            myText = myExistingItem.text()
            myTokens = myText.split(":")
            myValue = myTokens[1]
            if myValue == value:
                # remove it since the key is already present
                self.lstKeywords.takeItem(myCounter)
                break

    def get_value_for_key(self, key):
        """If key list contains a specific key, return its value.

        :param key: The key to search for
        :type key: str

        :returns: Value of key if matched otherwise none.
        :rtype: str
        """
        for myCounter in range(self.lstKeywords.count()):
            myExistingItem = self.lstKeywords.item(myCounter)
            myText = myExistingItem.text()
            myTokens = myText.split(":")
            myKey = str(myTokens[0]).strip()
            myValue = str(myTokens[1]).strip()
            if myKey == key:
                return myValue
        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 slate so that subcategory gets populated nicely &
        we will assume exposure to start with.
        """
        myKeywords = {"category": "exposure"}

        try:
            # Now read the layer with sub layer if needed
            myKeywords = self.keywordIO.read_keywords(self.layer)
        except (InvalidParameterError, HashNotFoundError, NoKeywordsFoundError):
            pass

        myLayerName = self.layer.name()
        if "title" not in myKeywords:
            self.leTitle.setText(myLayerName)
        self.lblLayerName.setText(self.tr("Keywords for %s" % myLayerName))
        # if we have a category key, unpack it first
        # so radio button etc get set
        if "category" in myKeywords:
            self.set_category(myKeywords["category"])
            myKeywords.pop("category")

        for myKey in myKeywords.iterkeys():
            self.add_list_entry(myKey, str(myKeywords[myKey]))

        # 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."""
        mySubcategory = self.get_value_for_key("subcategory")
        myUnits = self.get_value_for_key("unit")
        myType = self.get_value_for_key("datatype")
        myTitle = self.get_value_for_key("title")
        if myTitle is not None:
            self.leTitle.setText(myTitle)
        elif self.layer is not None:
            myLayerName = self.layer.name()
            self.lblLayerName.setText(self.tr("Keywords for %s" % myLayerName))
        else:
            self.lblLayerName.setText("")

        if not is_polygon_layer(self.layer):
            self.radPostprocessing.setEnabled(False)

        # adapt gui if we are in postprocessing category
        self.toggle_postprocessing_widgets()

        if self.radExposure.isChecked():
            if mySubcategory is not None and myType is not None:
                self.set_subcategory_list(self.standardExposureList, mySubcategory + " [" + myType + "]")
            elif mySubcategory is not None:
                self.set_subcategory_list(self.standardExposureList, mySubcategory)
            else:
                self.set_subcategory_list(self.standardExposureList, self.tr("Not Set"))
        elif self.radHazard.isChecked():
            if mySubcategory is not None and myUnits is not None:
                self.set_subcategory_list(self.standardHazardList, mySubcategory + " [" + myUnits + "]")
            elif mySubcategory is not None:
                self.set_subcategory_list(self.standardHazardList, mySubcategory)
            else:
                self.set_subcategory_list(self.standardHazardList, 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
    @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))

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

        myKeywords = {}
        for myCounter in range(self.lstKeywords.count()):
            myExistingItem = self.lstKeywords.item(myCounter)
            myText = myExistingItem.text()
            myTokens = myText.split(":")
            myKey = str(myTokens[0]).strip()
            myValue = str(myTokens[1]).strip()
            myKeywords[myKey] = myValue
        return myKeywords

    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()
        myKeywords = self.get_keywords()
        try:
            self.keywordIO.write_keywords(layer=self.layer, keywords=myKeywords)
        except InaSAFEError, e:
            myErrorMessage = get_error_message(e)
            # noinspection PyCallByClass,PyTypeChecker,PyArgumentList
            QtGui.QMessageBox.warning(
                self,
                self.tr("InaSAFE"),
                ((self.tr("An error was encountered when saving the keywords:\n" "%s" % myErrorMessage.to_html()))),
            )
        if self.dock is not None:
            self.dock.get_layers()
        self.done(QtGui.QDialog.Accepted)
Пример #13
0
class MapLegend():
    """A class for creating a map legend."""
    def __init__(self, theLayer, theDpi=300, theLegendTitle=None,
                 theLegendNotes=None, theLegendUnits=None):
        """Constructor for the Map Legend class.

        Args:
            * theLayer: QgsMapLayer object that the legend should be generated
                for.
            * theDpi: Optional DPI for generated legend image. Defaults to
                300 if not specified.
        Returns:
            None
        Raises:
            Any exceptions raised will be propagated.
        """
        LOGGER.debug('InaSAFE Map class initialised')
        self.legendImage = None
        self.layer = theLayer
        # how high each row of the legend should be
        self.legendIncrement = 42
        self.keywordIO = KeywordIO()
        self.legendFontSize = 8
        self.legendWidth = 900
        self.dpi = theDpi
        if theLegendTitle is None:
            self.legendTitle = self.tr('Legend')
        else:
            self.legendTitle = theLegendTitle
        self.legendNotes = theLegendNotes
        self.legendUnits = theLegendUnits

    def tr(self, theString):
        """We implement this ourself since we do not inherit QObject.

        Args:
           theString - string for translation.
        Returns:
           Translated version of theString.
        Raises:
           no exceptions explicitly raised.
        """
        # noinspection PyCallByClass,PyTypeChecker
        return QtCore.QCoreApplication.translate('MapLegend', theString)

    def getLegend(self):
        """Examine the classes of the impact layer associated with this print
        job.

        .. note: This is a wrapper for the rasterLegend and vectorLegend
           methods.

        Args:
            None
        Returns:
            None
        Raises:
            An InvalidLegendLayer will be raised if a legend cannot be
            created from the layer.
        """
        LOGGER.debug('InaSAFE Map Legend getLegend called')
        if self.layer is None:
            myMessage = self.tr('Unable to make a legend when map generator '
                                'has no layer set.')
            raise LegendLayerError(myMessage)
        try:
            self.keywordIO.read_keywords(self.layer, 'impact_summary')
        except KeywordNotFoundError, e:
            myMessage = self.tr('This layer does not appear to be an impact '
                                'layer. Try selecting an impact layer in the '
                                'QGIS layers list or creating a new impact '
                                'scenario before using the print tool.'
                                '\nMessage: %s' % str(e))
            raise Exception(myMessage)
        if self.layer.type() == QgsMapLayer.VectorLayer:
            return self.getVectorLegend()
        else:
            return self.getRasterLegend()
Пример #14
0
class Aggregator(QtCore.QObject):
    """The aggregator class facilitates aggregation of impact function results.
    """

    def __init__(
            self,
            iface,
            theAggregationLayer):
        """Director for aggregation based operations.
        Args:
          theAggregationLayer: QgsMapLayer representing clipped
              aggregation. This will be converted to a memory layer inside
              this class. see self.layer
        Returns:
           not applicable
        Raises:
           no exceptions explicitly raised
        """

        QtCore.QObject.__init__(self)

        self.hazardLayer = None
        self.exposureLayer = None
        self.safeLayer = None

        self.prefix = 'aggr_'
        self.attributes = {}
        self.attributeTitle = None

        self.iface = iface
        self.keywordIO = KeywordIO()
        self.defaults = defaults()
        self.errorMessage = None
        self.targetField = None
        self.impactLayerAttributes = []
        self.aoiMode = True

        # If this flag is not True, no aggregation or postprocessing will run
        # this is set as True by validateKeywords()
        self.isValid = False
        self.showIntermediateLayers = False

        # This is used to hold an *in memory copy* of the aggregation layer
        # or None if the clip extents should be used.
        if theAggregationLayer is None:
            self.aoiMode = True
            # Will be completed in _prepareLayer just before deintersect call
            self.layer = self._createPolygonLayer()
        else:
            self.aoiMode = False
            self.layer = theAggregationLayer

    def validateKeywords(self):
        """Check if the postprocessing layer has all needed attribute keywords.

        This is only applicable in the case where were are not using the AOI
        (in other words self.aoiMode is False). When self.aoiMode is True
        then we always use just the defaults and dont allow the user to
        create custom aggregation field mappings.

        This method is called on instance creation and should always be
        called if you change any state of the aggregator class.

        On completion of this method the self.isValid flag is set. If this
        flag is not True, then no aggregation or postprocessing work will be
        carried out (these methods will raise an InvalidAggregatorError).

        Args:
            None

        Returns:
            None

        Raises:
            Errors are propogated
        """

        # Otherwise get the attributes for the aggregation layer.
        # noinspection PyBroadException
        try:
            myKeywords = self.keywordIO.read_keywords(self.layer)
        #discussed with Tim,in this case its ok to be generic
        except Exception:  # pylint: disable=W0703
            myKeywords = {}

        if self.aoiMode:
            myKeywords[self.defaults['FEM_RATIO_ATTR_KEY']] = self.tr(
                'Use default')
            self.keywordIO.update_keywords(self.layer, myKeywords)
            self.isValid = True
            return
        else:
            myMessage = m.Message(
                m.Heading(
                    self.tr('Select attribute'), **PROGRESS_UPDATE_STYLE),
                m.Paragraph(self.tr(
                    'Please select which attribute you want to use as ID for '
                    'the aggregated results')))
            self._sendMessage(myMessage)

            #myKeywords are already complete
            category = myKeywords['category']
            aggregationAttribute = self.defaults['AGGR_ATTR_KEY']
            femaleRatio = self.defaults['FEM_RATIO_ATTR_KEY']
            femaleRatioKey = self.defaults['FEM_RATIO_KEY']
            if ('category' in myKeywords and
                category == 'postprocessing' and
                aggregationAttribute in myKeywords and
                femaleRatio in myKeywords and
                (femaleRatio != self.tr('Use default') or
                 femaleRatioKey in myKeywords)):
                self.isValid = True
            #some keywords are needed
            else:
                #set the default values by writing to the myKeywords
                myKeywords['category'] = 'postprocessing'

                myAttributes, _ = layer_attribute_names(
                    self.layer,
                    [QtCore.QVariant.Int, QtCore.QVariant.String])
                if self.defaults['AGGR_ATTR_KEY'] not in myKeywords:
                    myKeywords[self.defaults['AGGR_ATTR_KEY']] = \
                        myAttributes[0]

                if self.defaults['FEM_RATIO_ATTR_KEY'] not in myKeywords:
                    myKeywords[self.defaults['FEM_RATIO_ATTR_KEY']] = self.tr(
                        'Use default')

                if self.defaults['FEM_RATIO_KEY'] not in myKeywords:
                    myKeywords[self.defaults['FEM_RATIO_KEY']] = \
                        self.defaults['FEM_RATIO']

                self.keywordIO.update_keywords(self.layer, myKeywords)
                self.isValid = False

    def deintersect(self, theHazardLayer, theExposureLayer):
        """Ensure there are no intersecting features with self.layer.

        This should only happen after initial checks have been made.

        Buildings are not split up by this method.

        """

        if not self.isValid:
            raise InvalidAggregatorError

        # These should have already been clipped to analysis extents
        self.hazardLayer = theHazardLayer
        self.exposureLayer = theExposureLayer
        self._prepareLayer()

        if not self.aoiMode:
            # This is a safe version of the aggregation layer
            self.safeLayer = safe_read_layer(str(self.layer.source()))

            if is_polygon_layer(self.hazardLayer):
                self.hazardLayer = self._preparePolygonLayer(self.hazardLayer)

            if is_polygon_layer(self.exposureLayer):
                # Find out the subcategory for this layer
                mySubcategory = self.keywordIO.read_keywords(
                    self.exposureLayer, 'subcategory')
                # We dont want to chop up buildings!
                if mySubcategory != 'structure':
                    self.exposureLayer = self._preparePolygonLayer(
                        self.exposureLayer)

    def aggregate(self, theSafeImpactLayer):
        """Do any requested aggregation post processing.

        Performs Aggregation postprocessing step by

            * creating a copy of the dataset clipped by the impactlayer
              bounding box
            * stripping all attributes beside the aggregation attribute
            * delegating to the appropriate aggregator for raster and vectors

        :raises: ReadLayerError
        """

        if not self.isValid:
            raise InvalidAggregatorError

        myMessage = m.Message(
            m.Heading(self.tr('Aggregating results'), **PROGRESS_UPDATE_STYLE),
            m.Paragraph(self.tr(
                'This may take a little while - we are aggregating the impact'
                ' by %1').arg(self.layer.name())))
        self._sendMessage(myMessage)

        myQGISImpactLayer = safe_to_qgis_layer(theSafeImpactLayer)
        if not myQGISImpactLayer.isValid():
            myMessage = self.tr('Error when reading %1').arg(myQGISImpactLayer)
            # noinspection PyExceptionInherit
            raise ReadLayerError(myMessage)
        myLayerName = str(self.tr('%1 aggregated to %2').arg(
            myQGISImpactLayer.name()).arg(self.layer.name()))

        #delete unwanted fields
        myProvider = self.layer.dataProvider()
        myFields = myProvider.fields()

        #mark important attributes as needed
        self._setPersistantAttributes()
        myUnneededAttributes = []

        for i in myFields:
            if (myFields[i].name() not in
                    self.attributes.values()):
                myUnneededAttributes.append(i)
        LOGGER.debug('Removing this attributes: ' + str(myUnneededAttributes))
        # noinspection PyBroadException
        try:
            self.layer.startEditing()
            myProvider.deleteAttributes(myUnneededAttributes)
            self.layer.commitChanges()
        # FIXME (Ole): Disable pylint check for the moment
        # Need to work out what exceptions we will catch here, though.
        except:  # pylint: disable=W0702
            myMessage = self.tr('Could not remove the unneeded fields')
            LOGGER.debug(myMessage)

        del myUnneededAttributes, myProvider, myFields
        self.keywordIO.update_keywords(
            self.layer, {'title': myLayerName})

        self.statisticsType, self.statisticsClasses = (
            self.keywordIO.get_statistics(myQGISImpactLayer))

        #call the correct aggregator
        if myQGISImpactLayer.type() == QgsMapLayer.VectorLayer:
            self._aggregateVectorImpact(myQGISImpactLayer, theSafeImpactLayer)
        elif myQGISImpactLayer.type() == QgsMapLayer.RasterLayer:
            self._aggregateRasterImpact(myQGISImpactLayer)
        else:
            myMessage = self.tr('%1 is %2 but it should be either vector or '
                                'raster').\
                arg(myQGISImpactLayer.name()).arg(myQGISImpactLayer.type())
            # noinspection PyExceptionInherit
            raise ReadLayerError(myMessage)

        # show a styled aggregation layer
        if self.showIntermediateLayers:
            if self.statisticsType == 'sum':
                #style layer if we are summing
                myProvider = self.layer.dataProvider()
                myAttr = self._sumFieldName()
                myAttrIndex = myProvider.fieldNameIndex(myAttr)
                myProvider.select([myAttrIndex], QgsRectangle(), False)
                myFeature = QgsFeature()
                myHighestVal = 0

                while myProvider.nextFeature(myFeature):
                    myAttrMap = myFeature.attributeMap()
                    myVal, ok = myAttrMap[myAttrIndex].toInt()
                    if ok and myVal > myHighestVal:
                        myHighestVal = myVal

                myClasses = []
                myColors = ['#fecc5c', '#fd8d3c', '#f31a1c']
                myStep = int(myHighestVal / len(myColors))
                myCounter = 0
                for myColor in myColors:
                    myMin = myCounter
                    myCounter += myStep
                    myMax = myCounter

                    myClasses.append(
                        {'min': myMin,
                         'max': myMax,
                         'colour': myColor,
                         'transparency': 30,
                         'label': '%s - %s' % (myMin, myMax)})
                    myCounter += 1

                myStyle = {'target_field': myAttr,
                           'style_classes': myClasses}
                set_vector_graduated_style(self.layer, myStyle)
            else:
                #make style of layer pretty much invisible
                myProps = {'style': 'no',
                           'color_border': '0,0,0,127',
                           'width_border': '0.0'
                           }
                # noinspection PyCallByClass,PyTypeChecker,PyArgumentList
                mySymbol = QgsFillSymbolV2.createSimple(myProps)
                myRenderer = QgsSingleSymbolRendererV2(mySymbol)
                self.layer.setRendererV2(myRenderer)
                self.layer.saveDefaultStyle()

    def _aggregateVectorImpact(self, theQGISImpactLayer, theSafeImpactLayer):
        """Performs Aggregation postprocessing step on vector impact layers.

        Args:
            myQGISImpactLayer a valid QgsRasterLayer

        Returns:
            None
        """
        #TODO (MB) implement line aggregation

        myAggrFieldMap = {}
        myAggrFieldIndex = None

        try:
            self.targetField = self.keywordIO.read_keywords(theQGISImpactLayer,
                                                           'target_field')
        except KeywordNotFoundError:
            myMessage = m.Paragraph(
                self.tr(
                    'No "target_field" keyword found in the impact layer %1 '
                    'keywords. The impact function should define this.').arg(
                        theQGISImpactLayer.name()))
            LOGGER.debug('Skipping postprocessing due to: %s' % myMessage)
            self.errorMessage = myMessage
            return
        myImpactProvider = theQGISImpactLayer.dataProvider()
        myTargetFieldIndex = theQGISImpactLayer.fieldNameIndex(
            self.targetField)
        #if a feature has no field called
        if myTargetFieldIndex == -1:
            myMessage = m.Paragraph(
                self.tr('No attribute "%1" was found in the attribute table '
                        'for layer "%2". The impact function must define this'
                        ' attribute for postprocessing to work.').arg(
                            self.targetField, theQGISImpactLayer.name()))
            LOGGER.debug('Skipping postprocessing due to: %s' % myMessage)
            self.errorMessage = myMessage
            return

        # start data retreival: fetch no geometry and
        # 1 attr for each feature
        myImpactProvider.select([myTargetFieldIndex], QgsRectangle(), False)
        myTotal = 0

        myAggregationProvider = self.layer.dataProvider()
        self.layer.startEditing()

        if self.statisticsType == 'class_count':
            #add the class count fields to the layer
            myFields = [QgsField('%s_%s' % (f, self.targetField),
                                 QtCore.QVariant.String) for f in
                        self.statisticsClasses]
            myAggregationProvider.addAttributes(myFields)
            self.layer.commitChanges()

            myTmpAggrFieldMap = myAggregationProvider.fieldNameMap()
            for k, v in myTmpAggrFieldMap.iteritems():
                myAggrFieldMap[str(k)] = v

        elif self.statisticsType == 'sum':
            #add the total field to the layer
            myAggrField = self._sumFieldName()
            myAggregationProvider.addAttributes([QgsField(
                myAggrField, QtCore.QVariant.Int)])

            self.layer.commitChanges()
            myAggrFieldIndex = self.layer.fieldNameIndex(
                myAggrField)

        self.layer.startEditing()

        myImpactGeoms = theSafeImpactLayer.get_geometry()
        myImpactValues = theSafeImpactLayer.get_data()

        if not self.aoiMode:
            myAggregtionUnits = self.safeLayer.get_geometry()

            if (theSafeImpactLayer.is_point_data or
                    theSafeImpactLayer.is_polygon_data):
                LOGGER.debug('Doing point in polygon aggregation')

                myRemainingValues = myImpactValues

                if theSafeImpactLayer.is_polygon_data:
                    # Using centroids to do polygon in polygon aggregation
                    # this is always ok because
                    # deintersect() took care of splitting
                    # polygons that spawn across multiple postprocessing
                    # polygons. After deintersect()
                    # each impact polygon will never be contained by more than
                    # one aggregation polygon

                    # Calculate points for each polygon
                    myCentroids = []
                    for myPolygon in myImpactGeoms:
                        if hasattr(myPolygon, 'outer_ring'):
                            outer_ring = myPolygon.outer_ring
                        else:
                            # Assume it is an array
                            outer_ring = myPolygon
                        c = calculate_polygon_centroid(outer_ring)
                        myCentroids.append(c)
                    myRemainingPoints = myCentroids

                else:
                    #this are already points data
                    myRemainingPoints = myImpactGeoms

                #iterate over the aggregation units
                for myPolygonIndex, myPolygon in enumerate(myAggregtionUnits):
                    if hasattr(myPolygon, 'outer_ring'):
                        outer_ring = myPolygon.outer_ring
                        inner_rings = myPolygon.inner_rings
                    else:
                        # Assume it is an array
                        outer_ring = myPolygon
                        inner_rings = None

                    inside, outside = points_in_and_outside_polygon(
                        myRemainingPoints,
                        outer_ring,
                        holes=inner_rings,
                        closed=True,
                        check_input=True)

                    #self.impactLayerAttributes is a list of list of dict
                    #[
                    #   [{...},{...},{...}],
                    #   [{...},{...},{...}]
                    #]
                    self.impactLayerAttributes.append([])
                    if self.statisticsType == 'class_count':
                        myResults = OrderedDict()
                        for myClass in self.statisticsClasses:
                            myResults[myClass] = 0

                        for i in inside:
                            myKey = myRemainingValues[i][self.targetField]
                            try:
                                myResults[myKey] += 1
                            except KeyError:
                                myError = ('StatisticsClasses %s does not '
                                           'include the %s class which was '
                                           'found in the data. This is a '
                                           'problem in the %s '
                                           'statistics_classes definition' %
                                           (self.statisticsClasses,
                                            myKey,
                                            self.getFunctionID()))
                                raise KeyError(myError)

                            self.impactLayerAttributes[myPolygonIndex].append(
                                myRemainingValues[i])
                        myAttrs = {}
                        for k, v in myResults.iteritems():
                            myKey = '%s_%s' % (k, self.targetField)
                            #FIXME (MB) remove next line when we get rid of
                            #shape files as internal format
                            myKey = myKey[:10]
                            myAggrFieldIndex = myAggrFieldMap[myKey]
                            myAttrs[myAggrFieldIndex] = QtCore.QVariant(v)

                    elif self.statisticsType == 'sum':
                        #by default summ attributes
                        myTotal = 0
                        for i in inside:
                            try:
                                myTotal += myRemainingValues[i][
                                    self.targetField]
                            except TypeError:
                                pass

                            #add all attributes to the impactLayerAttributes
                            self.impactLayerAttributes[myPolygonIndex].append(
                                myRemainingValues[i])
                        myAttrs = {myAggrFieldIndex: QtCore.QVariant(myTotal)}

                    # Add features inside this polygon
                    myFID = myPolygonIndex
                    myAggregationProvider.changeAttributeValues(
                        {myFID: myAttrs})

                    # make outside points the input to the next iteration
                    # this could maybe be done quicklier using directly numpy
                    # arrays like this:
                    # myRemainingPoints = myRemainingPoints[outside]
                    # myRemainingValues =
                    # [myRemainingValues[i] for i in outside]
                    myTmpPoints = []
                    myTmpValues = []
                    for i in outside:
                        myTmpPoints.append(myRemainingPoints[i])
                        myTmpValues.append(myRemainingValues[i])
                    myRemainingPoints = myTmpPoints
                    myRemainingValues = myTmpValues

                    # LOGGER.debug('Before: ' + str(len(myRemainingValues)))
                    # LOGGER.debug('After: ' + str(len(myRemainingValues)))
                    # LOGGER.debug('Inside: ' + str(len(inside)))
                    # LOGGER.debug('Outside: ' + str(len(outside)))

            elif theSafeImpactLayer.is_line_data:
                LOGGER.debug('Doing line in polygon aggregation')

            else:
                myMessage = m.Paragraph(
                    self.tr(
                        'Aggregation on vector impact layers other than points'
                        ' or polygons not implemented yet not implemented yet.'
                        ' Called on %1').arg(theQGISImpactLayer.name()))
                LOGGER.debug('Skipping postprocessing due to: %s' % myMessage)
                self.errorMessage = myMessage
                self.layer.commitChanges()
                return
        else:
            if self.statisticsType == 'class_count':
                #loop over all features in impact layer
                myResults = OrderedDict()
                for myClass in self.statisticsClasses:
                    myResults[myClass] = 0

                self.impactLayerAttributes.append([])
                for myImpactValueList in myImpactValues:
                    myKey = myImpactValueList[self.targetField]
                    try:
                        myResults[myKey] += 1
                    except KeyError:
                        myError = ('StatisticsClasses %s does not '
                                   'include the %s class which was '
                                   'found in the data. This is a '
                                   'problem in the %s '
                                   'statistics_classes definition' %
                                   (self.statisticsClasses,
                                    myKey,
                                    self.getFunctionID()))
                        raise KeyError(myError)

                    self.impactLayerAttributes[0].append(myImpactValueList)

                myAttrs = {}
                for k, v in myResults.iteritems():
                    myKey = '%s_%s' % (k, self.targetField)
                    #FIXME (MB) remove next line when we get rid of
                    #shape files as internal format
                    myKey = myKey[:10]
                    myAggrFieldIndex = myAggrFieldMap[myKey]
                    myAttrs[myAggrFieldIndex] = QtCore.QVariant(v)

            elif self.statisticsType == 'sum':
                #loop over all features in impact layer
                self.impactLayerAttributes.append([])
                for myImpactValueList in myImpactValues:
                    if myImpactValueList[self.targetField] == 'None':
                        myImpactValueList[self.targetField] = None
                    try:
                        myTotal += myImpactValueList[self.targetField]
                    except TypeError:
                        pass
                    self.impactLayerAttributes[0].append(myImpactValueList)
                myAttrs = {myAggrFieldIndex: QtCore.QVariant(myTotal)}

            #apply to all area feature
            myFID = 0
            myAggregationProvider.changeAttributeValues({myFID: myAttrs})

        self.layer.commitChanges()
        return

    def _aggregateRasterImpact(self, theQGISImpactLayer):
        """
        Performs Aggregation postprocessing step on raster impact layers by
        calling QgsZonalStatistics
        Args:
            QgsMapLayer: theQGISImpactLayer a valid QgsVectorLayer

        Returns: None
        """
        myZonalStatistics = QgsZonalStatistics(
            self.layer,
            theQGISImpactLayer.dataProvider().dataSourceUri(),
            self.prefix)
        myProgressDialog = QtGui.QProgressDialog(
            self.tr('Calculating zonal statistics'),
            self.tr('Abort...'),
            0,
            0)
        startTime = time.clock()
        myZonalStatistics.calculateStatistics(myProgressDialog)
        if myProgressDialog.wasCanceled():
            QtGui.QMessageBox.error(
                self, self.tr('ZonalStats: Error'),
                self.tr('You aborted aggregation, '
                        'so there are no data for analysis. Exiting...'))
        cppDuration = time.clock() - startTime
        print 'CPP duration: %ss' % (cppDuration)

        startTime = time.clock()
        # new way
        # myZonalStatistics = {
        # 0L: {'count': 50539,
        #      'sum': 12015061.876953125,
        #      'mean': 237.73841739949594},
        # 1L: {
        #   'count': 19492,
        #   'sum': 2945658.1220703125,
        #   'mean': 151.12138939412642},
        # 2L: {
        #   'count': 57372,
        #   'sum': 1643522.3984985352, 'mean': 28.6467684323108},
        # 3L: {
        #   'count': 0.00013265823369700314,
        #   'sum': 0.24983273179242008,
        #   'mean': 1883.2810058593748},
        # 4L: {
        #   'count': 1.8158245316933218e-05,
        #   'sum': 0.034197078505115275,
        #   'mean': 1883.281005859375},
        # 5L: {
        #   'count': 73941,
        #   'sum': 10945062.435424805,
        #   'mean': 148.024268476553},
        # 6L: {
        #   'count': 54998,
        #   'sum': 11330910.488220215,
        #   'mean': 206.02404611477172}}

        myZonalStatistics = calculateZonalStats(theQGISImpactLayer, self.layer)
        pyDuration = time.clock() - startTime
        print 'CPP duration: %ss' % (pyDuration)

        try:
            ratio = pyDuration / cppDuration
        except ZeroDivisionError:
            ratio = 1

        print 'py to CPP: %s%%' % (ratio * 100)
        # FIXME (MB) remove this once fully implemented
        oldPrefix = self.prefix

        self.prefix = 'newAggr'
        myProvider = self.layer.dataProvider()
        self.layer.startEditing()

        # add fields for stats to aggregation layer
        # { 1: {'sum': 10, 'count': 20, 'min': 1, 'max': 4, 'mean': 2},
        #             QgsField(self._minFieldName(), QtCore.QVariant.Double),
        #             QgsField(self._maxFieldName(), QtCore.QVariant.Double)]
        myFields = [QgsField(self._countFieldName(), QtCore.QVariant.Double),
                    QgsField(self._sumFieldName(), QtCore.QVariant.Double),
                    QgsField(self._meanFieldName(), QtCore.QVariant.Double)
                    ]
        myProvider.addAttributes(myFields)
        self.layer.commitChanges()

        sumIndex = myProvider.fieldNameIndex(self._sumFieldName())
        countIndex = myProvider.fieldNameIndex(self._countFieldName())
        meanIndex = myProvider.fieldNameIndex(self._meanFieldName())
        # minIndex = myProvider.fieldNameIndex(self._minFieldName())
        # maxIndex = myProvider.fieldNameIndex(self._maxFieldName())

        self.layer.startEditing()
        allPolygonAttrs = myProvider.attributeIndexes()
        myProvider.select(allPolygonAttrs)
        myFeature = QgsFeature()

        while myProvider.nextFeature(myFeature):
            myFid = myFeature.id()
            myStats = myZonalStatistics[myFid]
            #          minIndex: QtCore.QVariant(myStats['min']),
            #          maxIndex: QtCore.QVariant(myStats['max'])}
            attrs = {sumIndex: QtCore.QVariant(myStats['sum']),
                     countIndex: QtCore.QVariant(myStats['count']),
                     meanIndex: QtCore.QVariant(myStats['mean'])
                     }
            myProvider.changeAttributeValues({myFid: attrs})
        self.layer.commitChanges()

        # FIXME (MB) remove this once fully implemented
        self.prefix = oldPrefix
        return

    def _prepareLayer(self):
        """Prepare the aggregation layer to match analysis extents."""
        myMessage = m.Message(
            m.Heading(
                self.tr('Preparing aggregation layer'),
                **PROGRESS_UPDATE_STYLE),
            m.Paragraph(self.tr(
                'We are clipping the aggregation layer to match the '
                'intersection of the hazard and exposure layer extents.')))
        self._sendMessage(myMessage)

        # This is used to hold an *in memory copy* of the aggregation layer
        # or a in memory layer with the clip extents as a feature.
        if self.aoiMode:
            self.layer = self._extentsToLayer()
            # Area Of Interest (AOI) mode flag
        else:
            # we use only the exposure extent, because both exposure and hazard
            # have the same extent at this point.
            myGeoExtent = extent_to_geo_array(
                self.exposureLayer.extent(),
                self.exposureLayer.crs())

            myAggrAttribute = self.keywordIO.read_keywords(
                self.layer, self.defaults['AGGR_ATTR_KEY'])

            myClippedLayer = clip_layer(
                layer=self.layer,
                extent=myGeoExtent,
                explode_flag=True,
                explode_attribute=myAggrAttribute)

            myName = '%s %s' % (self.layer.name(), self.tr('aggregation'))
            self.layer = myClippedLayer
            self.layer.setLayerName(myName)
            if self.showIntermediateLayers:
                self.keywordIO.update_keywords(self.layer, {'title': myName})
                QgsMapLayerRegistry.instance().addMapLayer(self.layer)

    def _countFieldName(self):
        return (self.prefix + 'count')[:10]

    def _meanFieldName(self):
        return (self.prefix + 'mean')[:10]

    def _minFieldName(self):
        return (self.prefix + 'min')[:10]

    def _maxFieldName(self):
        return (self.prefix + 'max')[:10]

    def _sumFieldName(self):
        return (self.prefix + 'sum')[:10]

    # noinspection PyDictCreation
    def _setPersistantAttributes(self):
        """Mark any attributes that should remain in the self.layer table."""
        self.attributes = {}
        self.attributes[self.defaults[
            'AGGR_ATTR_KEY']] = (
                self.keywordIO.read_keywords(
                    self.layer,
                    self.defaults['AGGR_ATTR_KEY']))

        myFemaleRatioKey = self.defaults['FEM_RATIO_ATTR_KEY']
        myFemRatioAttr = self.keywordIO.read_keywords(
            self.layer,
            myFemaleRatioKey)
        if ((myFemRatioAttr != self.tr('Don\'t use')) and
                (myFemRatioAttr != self.tr('Use default'))):
            self.attributes[myFemaleRatioKey] = \
                myFemRatioAttr

    def _preparePolygonLayer(self, theQgisLayer):
        """Create a new layer with no intersecting features to self.layer.

        A helper function to align the polygons to the postprocLayer
        polygons. If one input polygon is in two or more postprocLayer polygons
        then it is divided so that each part is within only one of the
        postprocLayer polygons. this allows to aggregate in postrocessing using
        centroid in polygon.

        The function assumes EPSG:4326 but no checks are enforced

        Args:
            theQgisLayer of the file to be processed
        Returns:
            QgisLayer of the processed file

        Raises:
            Any exceptions raised by the InaSAFE library will be propagated.
        """
#        import time
#        startTime = time.clock()

        myMessage = m.Message(
            m.Heading(self.tr('Preclipping input data...')),
            m.Paragraph(self.tr(
                'Modifying %1 to avoid intersections with the aggregation '
                'layer'
            ).arg(theQgisLayer.name())))
        self._sendMessage(myMessage)

        theLayerFilename = str(theQgisLayer.source())
        myPostprocPolygons = self.safeLayer.get_geometry()
        myPolygonsLayer = safe_read_layer(theLayerFilename)
        myRemainingPolygons = numpy.array(myPolygonsLayer.get_geometry())
#        myRemainingAttributes = numpy.array(myPolygonsLayer.get_data())
        myRemainingIndexes = numpy.array(range(len(myRemainingPolygons)))

        #used for unit tests only
        self.preprocessedFeatureCount = 0

        # FIXME (MB) the intersecting array is used only for debugging and
        # could be safely removed
        myIntersectingPolygons = []
        myInsidePolygons = []

        # FIXME (MB) maybe do raw geos without qgis
        #select all postproc polygons with no attributes
        aggregationProvider = self.layer.dataProvider()
        aggregationProvider.select([])

        # copy polygons to a memory layer
        myQgisMemoryLayer = create_memory_layer(theQgisLayer)

        polygonsProvider = myQgisMemoryLayer.dataProvider()
        allPolygonAttrs = polygonsProvider.attributeIndexes()
        polygonsProvider.select(allPolygonAttrs)
        myQgisPostprocPoly = QgsFeature()
        myQgisFeat = QgsFeature()
        myInsideFeat = QgsFeature()
        fields = polygonsProvider.fields()
        myTempdir = temp_dir(sub_dir='preprocess')
        myOutFilename = unique_filename(suffix='.shp',
                                        dir=myTempdir)

        self.keywordIO.copy_keywords(theQgisLayer, myOutFilename)
        mySHPWriter = QgsVectorFileWriter(myOutFilename,
                                          'UTF-8',
                                          fields,
                                          polygonsProvider.geometryType(),
                                          polygonsProvider.crs())
        if mySHPWriter.hasError():
            raise InvalidParameterError(mySHPWriter.errorMessage())
        # end FIXME

        for (myPostprocPolygonIndex,
             myPostprocPolygon) in enumerate(myPostprocPolygons):
            LOGGER.debug('PostprocPolygon %s' % myPostprocPolygonIndex)
            myPolygonsCount = len(myRemainingPolygons)
            aggregationProvider.featureAtId(
                myPostprocPolygonIndex, myQgisPostprocPoly, True, [])
            myQgisPostprocGeom = QgsGeometry(myQgisPostprocPoly.geometry())

            # myPostprocPolygon bounding box values
            A = numpy.array(myPostprocPolygon)
            minx = miny = sys.maxint
            maxx = maxy = -minx
            myPostprocPolygonMinx = min(minx, min(A[:, 0]))
            myPostprocPolygonMaxx = max(maxx, max(A[:, 0]))
            myPostprocPolygonMiny = min(miny, min(A[:, 1]))
            myPostprocPolygonMaxy = max(maxy, max(A[:, 1]))

            # create an array full of False to store if a BB vertex is inside
            # or outside the myPostprocPolygon
            myAreVerticesInside = numpy.zeros(myPolygonsCount * 4,
                                              dtype=numpy.bool)

            # Create Nx2 vector of vertices of bounding boxes
            myBBVertices = []
            # Compute bounding box for each geometry type
            for myPoly in myRemainingPolygons:
                minx = miny = sys.maxint
                maxx = maxy = -minx
                # Do outer ring only as the BB is outside anyway
                A = numpy.array(myPoly)
                minx = min(minx, numpy.min(A[:, 0]))
                maxx = max(maxx, numpy.max(A[:, 0]))
                miny = min(miny, numpy.min(A[:, 1]))
                maxy = max(maxy, numpy.max(A[:, 1]))
                myBBVertices.extend([(minx, miny),
                                    (minx, maxy),
                                    (maxx, maxy),
                                    (maxx, miny)])

            # see if BB vertices are in myPostprocPolygon
            myBBVertices = numpy.array(myBBVertices)
            inside, _ = points_in_and_outside_polygon(myBBVertices,
                                                      myPostprocPolygon)
            # make True if the vertice was in myPostprocPolygon
            myAreVerticesInside[inside] = True

            # myNextIterPolygons has the 0:count indexes
            # myOutsidePolygons has the mapped to original indexes
            # and is overwritten at every iteration because we care only of
            # the outside polygons remaining after the last iteration
            myNextIterPolygons = []
            myOutsidePolygons = []

            for i in range(myPolygonsCount):
                k = i * 4
                myMappedIndex = myRemainingIndexes[i]
                # memory layers counting starts at 1 instead of 0 as in our
                # indexes
                myFeatId = myMappedIndex + 1
                doIntersection = False
                # summ the isInside bool for each of the boundingbox vertices
                # of each poygon. for example True + True + False + True is 3
                myPolygonLocation = numpy.sum(myAreVerticesInside[k:k + 4])

                if myPolygonLocation == 4:
                    # all vertices are inside -> polygon is inside
                    #ignore this polygon from further analysis
                    myInsidePolygons.append(myMappedIndex)
                    polygonsProvider.featureAtId(myFeatId,
                                                 myQgisFeat,
                                                 True,
                                                 allPolygonAttrs)
                    mySHPWriter.addFeature(myQgisFeat)
                    self.preprocessedFeatureCount += 1
#                    LOGGER.debug('Polygon %s is fully inside' %myMappedIndex)
#                    tmpWriter.addFeature(myQgisFeat)

                elif myPolygonLocation == 0:
                    # all vertices are outside
                    # check if the polygon BB is completely outside of the
                    # myPostprocPolygon BB.
                    myPolyMinx = numpy.min(myBBVertices[k:k + 4, 0])
                    myPolyMaxx = numpy.max(myBBVertices[k:k + 4, 0])
                    myPolyMiny = numpy.min(myBBVertices[k:k + 4, 1])
                    myPolyMaxy = numpy.max(myBBVertices[k:k + 4, 1])

                    # check if myPoly is all E,W,N,S of myPostprocPolygon
                    if ((myPolyMinx > myPostprocPolygonMaxx) or
                            (myPolyMaxx < myPostprocPolygonMinx) or
                            (myPolyMiny > myPostprocPolygonMaxy) or
                            (myPolyMaxy < myPostprocPolygonMiny)):
                        #polygon is surely outside
                        myOutsidePolygons.append(myMappedIndex)
                        # we need this polygon in the next iteration
                        myNextIterPolygons.append(i)
                    else:
                        # polygon might be outside or intersecting. consider
                        # it intersecting so it goes into further analysis
                        doIntersection = True
                else:
                    # some vertices are outside some inside -> polygon is
                    # intersecting
                    doIntersection = True

                #intersect using qgis
                if doIntersection:
#                    LOGGER.debug('Intersecting polygon %s' % myMappedIndex)
                    myIntersectingPolygons.append(myMappedIndex)

                    ok = polygonsProvider.featureAtId(myFeatId,
                                                      myQgisFeat,
                                                      True,
                                                      allPolygonAttrs)
                    if not ok:
                        LOGGER.debug('Couldn\'t fetch feature: %s' % myFeatId)
                        LOGGER.debug([str(error) for error in
                                      polygonsProvider.errors()])

                    myQgisPolyGeom = QgsGeometry(myQgisFeat.geometry())
                    myAtMap = myQgisFeat.attributeMap()
#                    for (k, attr) in myAtMap.iteritems():
#                        LOGGER.debug( "%d: %s" % (k, attr.toString()))

                    # make intersection of the myQgisFeat and the postprocPoly
                    # write the inside part to a shp file and the outside part
                    # back to the original QGIS layer
                    try:
                        myIntersec = myQgisPostprocGeom.intersection(
                            myQgisPolyGeom)
#                        if myIntersec is not None:
                        myIntersecGeom = QgsGeometry(myIntersec)

                        #from ftools
                        myUnknownGeomType = 0
                        if myIntersecGeom.wkbType() == myUnknownGeomType:
                            int_com = myQgisPostprocGeom.combine(
                                myQgisPolyGeom)
                            int_sym = myQgisPostprocGeom.symDifference(
                                myQgisPolyGeom)
                            myIntersecGeom = QgsGeometry(
                                int_com.difference(int_sym))
#                        LOGGER.debug('wkbType type of intersection: %s' %
# myIntersecGeom.wkbType())
                        polygonTypesList = [QGis.WKBPolygon,
                                            QGis.WKBMultiPolygon]
                        if myIntersecGeom.wkbType() in polygonTypesList:
                            myInsideFeat.setGeometry(myIntersecGeom)
                            myInsideFeat.setAttributeMap(myAtMap)
                            mySHPWriter.addFeature(myInsideFeat)
                            self.preprocessedFeatureCount += 1
                        else:
                            pass
#                            LOGGER.debug('Intersection not a polygon so '
#                                         'the two polygons either touch '
#                                         'only or do not intersect. Not '
#                                         'adding this to the inside list')
                        #Part of the polygon that is outside the postprocpoly
                        myOutside = myQgisPolyGeom.difference(myIntersecGeom)
#                        if myOutside is not None:
                        myOutsideGeom = QgsGeometry(myOutside)

                        if myOutsideGeom.wkbType() in polygonTypesList:
                            # modifiy the original geometry to the part
                            # outside of the postproc polygon
                            polygonsProvider.changeGeometryValues(
                                {myFeatId: myOutsideGeom})
                            # we need this polygon in the next iteration
                            myOutsidePolygons.append(myMappedIndex)
                            myNextIterPolygons.append(i)

                    except TypeError:
                        LOGGER.debug('ERROR with FID %s', myMappedIndex)

#            LOGGER.debug('Inside %s' % myInsidePolygons)
#            LOGGER.debug('Outside %s' % myOutsidePolygons)
#            LOGGER.debug('Intersec %s' % myIntersectingPolygons)
            if len(myNextIterPolygons) > 0:
                #some polygons are still completely outside of the postprocPoly
                #so go on and reiterate using only these
                nextIterPolygonsIndex = numpy.array(myNextIterPolygons)

                myRemainingPolygons = myRemainingPolygons[
                    nextIterPolygonsIndex]
#                myRemainingAttributes = myRemainingAttributes[
#                                        nextIterPolygonsIndex]
                myRemainingIndexes = myRemainingIndexes[nextIterPolygonsIndex]
                LOGGER.debug('Remaining: %s' % len(myRemainingPolygons))
            else:
                print 'no more polygons to be checked'
                break
#            del tmpWriter

        # here the full polygon set is represented by:
        # myInsidePolygons + myIntersectingPolygons + myNextIterPolygons
        # the a polygon intersecting multiple postproc polygons appears
        # multiple times in the array
        # noinspection PyUnboundLocalVariable
        LOGGER.debug('Results:\nInside: %s\nIntersect: %s\nOutside: %s' % (
            myInsidePolygons, myIntersectingPolygons, myOutsidePolygons))

        #add in- and outside polygons

        for i in myOutsidePolygons:
            myFeatId = i + 1
            polygonsProvider.featureAtId(myFeatId, myQgisFeat, True,
                                         allPolygonAttrs)
            mySHPWriter.addFeature(myQgisFeat)
            self.preprocessedFeatureCount += 1

        del mySHPWriter
#        LOGGER.debug('Created: %s' % self.preprocessedFeatureCount)

        myName = '%s %s' % (theQgisLayer.name(), self.tr('preprocessed'))
        myOutLayer = QgsVectorLayer(myOutFilename, myName, 'ogr')
        if not myOutLayer.isValid():
            #TODO (MB) use a better exception
            raise Exception('Invalid qgis Layer')

        if self.showIntermediateLayers:
            self.keywordIO.update_keywords(myOutLayer, {'title': myName})
            QgsMapLayerRegistry.instance().addMapLayer(myOutLayer)

        return myOutLayer

    def _createPolygonLayer(self, crs=None, fields=None):
        """Creates an empty shape file layer"""

        if crs is None:
            crs = QgsCoordinateReferenceSystem()
            crs.createFromEpsg(4326)

        if fields is None:
            fields = {}

        myTempdir = temp_dir(sub_dir='preprocess')
        myOutFilename = unique_filename(suffix='.shp',
                                        dir=myTempdir)
        mySHPWriter = QgsVectorFileWriter(myOutFilename,
                                          'UTF-8',
                                          fields,
                                          QGis.WKBPolygon,
                                          crs)
        #flush the writer to write to file
        del mySHPWriter
        myName = self.tr('Entire area')
        myLayer = QgsVectorLayer(myOutFilename, myName, 'ogr')
        LOGGER.debug('created' + myLayer.name())
        return myLayer

    def _extentsToLayer(self):
        """Memory layer for aggregation by using canvas extents as feature.

        We do this because the user elected to use no aggregation layer so we
        make a 'dummy' one which covers the whole study area extent.

        This layer is needed when postprocessing because we always want a
        vector layer to store aggregation information in.

        Returns:
            QgsMapLayer - a memory layer representing the extents of the clip.
        """

        # Note: this code duplicates from Dock.viewportGeoArray - make DRY. TS

        myRect = self.iface.mapCanvas().extent()
        myCrs = QgsCoordinateReferenceSystem()
        myCrs.createFromEpsg(4326)
        myGeoExtent = extent_to_geo_array(myRect, myCrs)

        if not self.layer.isValid():
            myMessage = self.tr(
                'An exception occurred when creating the entire area layer.')
            raise (Exception(myMessage))

        myProvider = self.layer.dataProvider()

        myAttrName = self.tr('Area')
        myProvider.addAttributes(
            [QgsField(myAttrName, QtCore.QVariant.String)])

        self.layer.startEditing()
        # add a feature the size of the impact layer bounding box
        myFeature = QgsFeature()
        # noinspection PyCallByClass,PyTypeChecker,PyArgumentList
        myFeature.setGeometry(QgsGeometry.fromRect(
            QgsRectangle(
                QgsPoint(myGeoExtent[0], myGeoExtent[1]),
                QgsPoint(myGeoExtent[2], myGeoExtent[3]))))
        myFeature.setAttributeMap({0: QtCore.QVariant(
            self.tr('Entire area'))})
        myProvider.addFeatures([myFeature])
        self.layer.commitChanges()

        try:
            self.keywordIO.update_keywords(
                self.layer,
                {self.defaults['AGGR_ATTR_KEY']: myAttrName})
        except InvalidParameterError:
            self.keywordIO.write_keywords(
                self.layer,
                {self.defaults['AGGR_ATTR_KEY']: myAttrName})
        except KeywordDbError, e:
            raise e
        return self.layer
Пример #15
0
class KeywordIOTest(unittest.TestCase):
    """Tests for reading and writing of raster and vector data
    """

    def setUp(self):
        self.keyword_io = KeywordIO()
        uri = QgsDataSourceURI()
        uri.setDatabase(os.path.join(TESTDATA, 'jk.sqlite'))
        uri.setDataSource('', 'osm_buildings', 'Geometry')
        self.sqlite_layer = QgsVectorLayer(
            uri.uri(), 'OSM Buildings', 'spatialite')
        hazard_path = os.path.join(HAZDATA, 'Shakemap_Padang_2009.asc')
        self.raster_layer, layer_type = load_layer(
            hazard_path, directory=None)
        del layer_type
        self.vector_layer, layer_type = load_layer('Padang_WGS84.shp')
        del layer_type
        self.expected_sqlite_keywords = {
            'category': 'exposure',
            'datatype': 'OSM',
            'subcategory': 'building'}
        self.expected_vector_keywords = {
            'category': 'exposure',
            'datatype': 'itb',
            'subcategory': 'structure',
            'title': 'Padang WGS84'}
        self.expected_raster_keywords = {
            'category': 'hazard',
            'source': 'USGS',
            'subcategory': 'earthquake',
            'unit': 'MMI',
            'title': ('An earthquake in Padang '
                      'like in 2009')}

    def tearDown(self):
        pass

    def test_get_hash_for_datasource(self):
        """Test we can reliably get a hash for a uri"""
        hash_value = self.keyword_io.hash_for_datasource(PG_URI)
        expected_hash = '7cc153e1b119ca54a91ddb98a56ea95e'
        message = "Got: %s\nExpected: %s" % (hash_value, expected_hash)
        assert hash_value == expected_hash, message

    def test_write_read_keyword_from_uri(self):
        """Test we can set and get keywords for a non local datasource"""
        handle, filename = tempfile.mkstemp(
            '.db', 'keywords_', temp_dir())

        # Ensure the file is deleted before we try to write to it
        # fixes windows specific issue where you get a message like this
        # ERROR 1: c:\temp\inasafe\clip_jpxjnt.shp is not a directory.
        # This is because mkstemp creates the file handle and leaves
        # the file open.

        os.close(handle)
        os.remove(filename)
        expected_keywords = {
            'category': 'exposure',
            'datatype': 'itb',
            'subcategory': 'building'}
        # SQL insert test
        # On first write schema is empty and there is no matching hash
        self.keyword_io.set_keyword_db_path(filename)
        self.keyword_io.write_keywords_for_uri(PG_URI, expected_keywords)
        # SQL Update test
        # On second write schema is populated and we update matching hash
        expected_keywords = {
            'category': 'exposure',
            'datatype': 'OSM',  # <--note the change here!
            'subcategory': 'building'}
        self.keyword_io.write_keywords_for_uri(PG_URI, expected_keywords)
        # Test getting all keywords
        keywords = self.keyword_io.read_keyword_from_uri(PG_URI)
        message = 'Got: %s\n\nExpected %s\n\nDB: %s' % (
            keywords, expected_keywords, filename)
        assert keywords == expected_keywords, message
        # Test getting just a single keyword
        keyword = self.keyword_io.read_keyword_from_uri(PG_URI, 'datatype')
        expected_keyword = 'OSM'
        message = 'Got: %s\n\nExpected %s\n\nDB: %s' % (
            keyword, expected_keyword, filename)
        assert keyword == expected_keyword, message
        # Test deleting keywords actually does delete
        self.keyword_io.delete_keywords_for_uri(PG_URI)
        try:
            _ = self.keyword_io.read_keyword_from_uri(PG_URI, 'datatype')
            # if the above didnt cause an exception then bad
            message = 'Expected a HashNotFoundError to be raised'
            assert message
        except HashNotFoundError:
            # we expect this outcome so good!
            pass

    def test_are_keywords_file_based(self):
        """Can we correctly determine if keywords should be written to file or
        to database?"""
        assert not self.keyword_io.are_keywords_file_based(self.sqlite_layer)
        assert self.keyword_io.are_keywords_file_based(self.raster_layer)
        assert self.keyword_io.are_keywords_file_based(self.vector_layer)

    def test_read_raster_file_keywords(self):
        """Can we read raster file keywords using generic readKeywords method
        """
        keywords = self.keyword_io.read_keywords(self.raster_layer)
        expected_keywords = self.expected_raster_keywords
        source = self.raster_layer.source()
        message = 'Got:\n%s\nExpected:\n%s\nSource:\n%s' % (
            keywords, expected_keywords, source)
        self.assertEquals(keywords, expected_keywords, message)

    def test_read_vector_file_keywords(self):
        """Test read vector file keywords with the generic readKeywords method.
         """
        keywords = self.keyword_io.read_keywords(self.vector_layer)
        expected_keywords = self.expected_vector_keywords
        source = self.vector_layer.source()
        message = 'Got: %s\n\nExpected %s\n\nSource: %s' % (
            keywords, expected_keywords, source)
        assert keywords == expected_keywords, message

    def test_append_keywords(self):
        """Can we append file keywords with the generic readKeywords method."""
        layer, _ = clone_padang_layer()
        new_keywords = {'category': 'exposure', 'test': 'TEST'}
        self.keyword_io.update_keywords(layer, new_keywords)
        keywords = self.keyword_io.read_keywords(layer)

        for key, value in new_keywords.iteritems():
            message = (
                'Layer keywords misses appended key: %s\n'
                'Layer keywords:\n%s\n'
                'Appended keywords:\n%s\n' %
                (key,
                 keywords,
                 new_keywords))
            assert key in keywords, message
            message = (
                'Layer keywords misses appended value: %s\n'
                'Layer keywords:\n%s\n'
                'Appended keywords:\n%s\n' %
                (value,
                 keywords,
                 new_keywords))
            assert keywords[key] == value, message

    def test_read_db_keywords(self):
        """Can we read sqlite kw with the generic readKeywords method
        """
        db_path = os.path.join(TESTDATA, 'test_keywords.db')
        self.read_db_keywords(db_path)

    def test_read_legacy_db_keywords(self):
        """Can we read legacy sqlite kw with the generic readKeywords method
        """
        db_path = os.path.join(TESTDATA, 'test_keywords_legacy.db')
        self.read_db_keywords(db_path)

    def read_db_keywords(self, db_path):
        """Can we read sqlite keywords with the generic readKeywords method
        """
        # noinspection PyUnresolvedReferences
        local_path = os.path.join(
            os.path.dirname(__file__), '../../..///', 'jk.sqlite')
        self.keyword_io.set_keyword_db_path(db_path)
        # We need to make a local copy of the dataset so
        # that we can use a local path that will hash properly on the
        # database to return us the correct / valid keywords record.
        shutil.copy2(os.path.join(TESTDATA, 'jk.sqlite'), local_path)
        uri = QgsDataSourceURI()
        # always use relative path!
        uri.setDatabase('../jk.sqlite')
        uri.setDataSource('', 'osm_buildings', 'Geometry')
        # create a local version that has the relative url
        sqlite_layer = QgsVectorLayer(uri.uri(), 'OSM Buildings', 'spatialite')
        expected_source = (
            'dbname=\'../jk.sqlite\' table="osm_buildings" (Geometry) sql=')
        message = 'Got source: %s\n\nExpected %s\n' % (
            sqlite_layer.source(), expected_source)
        assert 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())
        assert keywords == expected_keywords, message
        source = self.sqlite_layer.source()
        # delete sqlite_layer so that we can delete the file
        del sqlite_layer
        os.remove(local_path)
        message = 'Got: %s\n\nExpected %s\n\nSource: %s' % (
            keywords, expected_keywords, source)
        assert keywords == expected_keywords, message

    def test_copy_keywords(self):
        """Test we can copy the keywords."""
        out_path = unique_filename(
            prefix='test_copy_keywords', suffix='.keywords')
        self.keyword_io.copy_keywords(self.raster_layer, out_path)
        copied_keywords = read_file_keywords(out_path)
        expected_keywords = self.expected_raster_keywords
        message = 'Got:\n%s\nExpected:\n%s\nSource:\n%s' % (
            copied_keywords, expected_keywords, out_path)
        self.assertEquals(copied_keywords, expected_keywords, message)
Пример #16
0
    def test_keywords_creation_wizard(self):
        """Test how the widgets work."""
        expected_category_count = 3
        expected_categories = ['exposure', 'hazard', 'aggregation']
        chosen_category = 'hazard'

        expected_subcategory_count = 4
        expected_subcategories = ['volcano', 'earthquake', 'flood', 'tsunami']
        chosen_subcategory = "tsunami"

        expected_unit_count = 3
        expected_units = ['wetdry', 'metres_depth', 'feet_depth']
        expected_chosen_unit = 'feet_depth'

        expected_field_count = 5
        expected_fields = ['OBJECTID', 'GRIDCODE', 'Shape_Leng', 'Shape_Area',
                           'Category']
        expected_chosen_field = 'GRIDCODE'

        expected_keywords = {
            'category': 'hazard',
            'subcategory': 'tsunami',
            'unit': 'feet_depth',
            'field': 'GRIDCODE',
            'source': 'some source',
            'title': 'some title'
        }

        layer = clone_shp_layer(name='tsunami_polygon')

        # check the environment first
        message = 'Test layer is not readable. Check environment variables.'
        self.assertIsNotNone(layer.dataProvider(), message)

        # Initialize dialog
        # noinspection PyTypeChecker
        dialog = WizardDialog(layer=layer)

        # step 1 of 7 - select category
        count = dialog.lstCategories.count()
        message = ('Invalid category count! There should be %d while there '
                   'were: %d') % (expected_category_count, count)
        self.assertEqual(count, expected_category_count, message)

        # Get all the categories given by wizards and save the 'hazard' index
        categories = []
        hazard_index = -1
        for i in range(expected_category_count):
            category_name = eval(
                dialog.lstCategories.item(i).data(Qt.UserRole))['id']
            categories.append(category_name)
            if category_name == chosen_category:
                hazard_index = i
        # Check if categories is the same with expected_categories
        message = 'Invalid categories! It should be "%s" while it was %s' % (
            expected_categories, categories)
        self.assertEqual(set(categories), set(expected_categories), message)
        # Check if the Next button state is on the right state
        message = ('Invalid Next button state in step 1! Enabled while '
                   'there\'s nothing selected yet')
        self.assertTrue(
            not dialog.pbnNext.isEnabled(), message)
        # Select hazard one
        dialog.lstCategories.setCurrentRow(hazard_index)
        message = ('Invalid Next button state in step 1! Still disabled after '
                   'an item selected')
        self.assertTrue(
            dialog.pbnNext.isEnabled(), message)
        # Click Next
        dialog.pbnNext.click()

        # step 2 of 7 - select subcategory
        # Check the number of sub categories
        count = dialog.lstSubcategories.count()
        message = ('Invalid subcategory count! There should be %d and there '
                   'were: %d') % (expected_subcategory_count, count)
        self.assertEqual(count, expected_subcategory_count, message)

        # Get all the subcategories given and save the 'tsunami' index
        subcategories = []
        tsunami_index = -1
        for i in range(expected_subcategory_count):
            subcategory_name = eval(
                dialog.lstSubcategories.item(i).data(Qt.UserRole))['id']
            subcategories.append(subcategory_name)
            if subcategory_name == chosen_subcategory:
                tsunami_index = i
        # Check if subcategories is the same with expected_subcategories
        message = ('Invalid sub categories! It should be "%s" while it was '
                   '%s') % (expected_subcategories, subcategories)
        self.assertEqual(
            set(subcategories), set(expected_subcategories), message)
        # The Next button should be on disabled state first
        self.assertTrue(
            not dialog.pbnNext.isEnabled(), 'Invalid Next button'
            ' state in step 2! Enabled while there\'s nothing selected yet')
        # Set to tsunami subcategories
        dialog.lstSubcategories.setCurrentRow(tsunami_index)
        message = ('Invalid Next button state in step 2! Still disabled after '
                   'an item selected')
        self.assertTrue(dialog.pbnNext.isEnabled(), message)
        # Click next button
        dialog.pbnNext.click()

        # step 3 of 7 - select tsunami units
        # Check if the number of unit for tsunami is 3
        count = dialog.lstUnits.count()
        message = ('Invalid unit count! There should be %d while there were: '
                   '%d') % (expected_unit_count, count)
        self.assertEqual(count, expected_unit_count, message)
        # Get all the units given and save the 'feet_depth' index
        units = []
        feet_unit_index = -1
        for i in range(expected_unit_count):
            unit_name = eval(
                dialog.lstUnits.item(i).data(Qt.UserRole))['id']
            units.append(unit_name)
            if unit_name == expected_chosen_unit:
                feet_unit_index = i
        # Check if units is the same with expected_units
        message = ('Invalid units! It should be "%s" while it was '
                   '%s') % (expected_units, units)
        self.assertEqual(
            set(expected_units), set(units), message)
        # The button should be on disabled state first
        message = ('Invalid Next button state in step 3! Enabled while '
                   'there\'s nothing selected yet')
        self.assertTrue(
            not dialog.pbnNext.isEnabled(), message)
        dialog.lstUnits.setCurrentRow(feet_unit_index)
        message = ('Invalid Next button state in step 3! Enabled while '
                   'there\'s nothing selected yet')
        self.assertTrue(
            dialog.pbnNext.isEnabled(), message)

        dialog.pbnNext.click()

        # step 4 of 7 - select data field for tsunami feet
        count = dialog.lstFields.count()
        message = ('Invalid field count! There should be %d while there were: '
                   '%d') % (expected_field_count, count)
        self.assertEqual(count, expected_field_count, message)
        # Get all the fields given and save the 'GRIDCODE' index
        fields = []
        gridcode_index = -1
        for i in range(expected_field_count):
            field_name = dialog.lstFields.item(i).text()
            fields.append(field_name)
            if field_name == expected_chosen_field:
                gridcode_index = i
        # Check if fields is the same with expected_fields
        message = ('Invalid fields! It should be "%s" while it was '
                   '%s') % (expected_fields, fields)
        self.assertEqual(
            set(expected_fields), set(fields), message)
        # The button should be on disabled first
        message = ('Invalid Next button state in step 4! Enabled while '
                   'there\'s nothing selected yet')
        self.assertTrue(not dialog.pbnNext.isEnabled(), message)
        dialog.lstFields.setCurrentRow(gridcode_index)
        message = ('Invalid Next button state in step 4! Still disabled after '
                   'an item selected')
        self.assertTrue(dialog.pbnNext.isEnabled(), message)
        # Click next
        dialog.pbnNext.click()

        # step 6 of 7 - enter source
        message = ('Invalid Next button state in step 6! Disabled while '
                   'source is optional')
        self.assertTrue(dialog.pbnNext.isEnabled(), message)
        dialog.leSource.setText('some source')
        dialog.pbnNext.click()

        # step 7 of 7 - enter title
        dialog.leTitle.setText('some title')
        message = ('Invalid Next button state in step 7! Still disabled '
                   'after a text entered')
        self.assertTrue(dialog.pbnNext.isEnabled(), message)
        dialog.pbnNext.click()

        # test the resulting keywords
        keyword_io = KeywordIO()
        # noinspection PyTypeChecker
        keywords = keyword_io.read_keywords(layer)

        message = 'Invalid metadata!\n Was: %s\n Should be: %s' % (
            unicode(keywords), unicode(expected_keywords))

        self.assertEqual(keywords, expected_keywords, message)
Пример #17
0
class KeywordIOTest(unittest.TestCase):
    """Tests for reading and writing of raster and vector data
    """

    def setUp(self):
        self.keywordIO = KeywordIO()
        myUri = QgsDataSourceURI()
        myUri.setDatabase(os.path.join(TESTDATA, 'jk.sqlite'))
        myUri.setDataSource('', 'osm_buildings', 'Geometry')
        self.sqliteLayer = QgsVectorLayer(myUri.uri(), 'OSM Buildings',
                                          'spatialite')
        myHazardPath = os.path.join(HAZDATA, 'Shakemap_Padang_2009.asc')
        self.fileRasterLayer, myType = load_layer(
            myHazardPath, directory=None)
        del myType
        self.fileVectorLayer, myType = load_layer('Padang_WGS84.shp')
        del myType
        self.expectedSqliteKeywords = {
            'category': 'exposure',
            'datatype': 'OSM',
            'subcategory': 'building'}
        self.expectedVectorKeywords = {
            'category': 'exposure',
            'datatype': 'itb',
            'subcategory': 'structure',
            'title': 'Padang WGS84'}
        self.expectedRasterKeywords = {
            'category': 'hazard',
            'source': 'USGS',
            'subcategory': 'earthquake',
            'unit': 'MMI',
            'title': ('An earthquake in Padang '
            'like in 2009')}

    def tearDown(self):
        pass

    def test_getHashForDatasource(self):
        """Test we can reliably get a hash for a uri"""
        myHash = self.keywordIO.hash_for_datasource(PG_URI)
        myExpectedHash = '7cc153e1b119ca54a91ddb98a56ea95e'
        myMessage = "Got: %s\nExpected: %s" % (myHash, myExpectedHash)
        assert myHash == myExpectedHash, myMessage

    def test_writeReadKeywordFromUri(self):
        """Test we can set and get keywords for a non local datasource"""
        myHandle, myFilename = 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(myHandle)
        os.remove(myFilename)
        myExpectedKeywords = {'category': 'exposure',
                              'datatype': 'itb',
                              'subcategory': 'building'}
        # SQL insert test
        # On first write schema is empty and there is no matching hash
        self.keywordIO.set_keyword_db_path(myFilename)
        self.keywordIO.write_keywords_for_uri(PG_URI, myExpectedKeywords)
        # SQL Update test
        # On second write schema is populated and we update matching hash
        myExpectedKeywords = {'category': 'exposure',
                              'datatype': 'OSM',  # <--note the change here!
                              'subcategory': 'building'}
        self.keywordIO.write_keywords_for_uri(PG_URI, myExpectedKeywords)
        # Test getting all keywords
        myKeywords = self.keywordIO.readKeywordFromUri(PG_URI)
        myMessage = 'Got: %s\n\nExpected %s\n\nDB: %s' % (
                    myKeywords, myExpectedKeywords, myFilename)
        assert myKeywords == myExpectedKeywords, myMessage
        # Test getting just a single keyword
        myKeyword = self.keywordIO.readKeywordFromUri(PG_URI, 'datatype')
        myExpectedKeyword = 'OSM'
        myMessage = 'Got: %s\n\nExpected %s\n\nDB: %s' % (
                    myKeyword, myExpectedKeyword, myFilename)
        assert myKeyword == myExpectedKeyword, myMessage
        # Test deleting keywords actually does delete
        self.keywordIO.delete_keywords_for_uri(PG_URI)
        try:
            myKeyword = self.keywordIO.readKeywordFromUri(PG_URI, 'datatype')
            #if the above didnt cause an exception then bad
            myMessage = 'Expected a HashNotFoundError to be raised'
            assert myMessage
        except HashNotFoundError:
            #we expect this outcome so good!
            pass

    def test_areKeywordsFileBased(self):
        """Can we correctly determine if keywords should be written to file or
        to database?"""
        assert not self.keywordIO.are_keywords_file_based(self.sqliteLayer)
        assert self.keywordIO.are_keywords_file_based(self.fileRasterLayer)
        assert self.keywordIO.are_keywords_file_based(self.fileVectorLayer)

    def test_readRasterFileKeywords(self):
        """Can we read raster file keywords using generic readKeywords method
        """
        myKeywords = self.keywordIO.read_keywords(self.fileRasterLayer)
        myExpectedKeywords = self.expectedRasterKeywords
        mySource = self.fileRasterLayer.source()
        myMessage = 'Got:\n%s\nExpected:\n%s\nSource:\n%s' % (
                    myKeywords, myExpectedKeywords, mySource)
        assert myKeywords == myExpectedKeywords, myMessage

    def test_readVectorFileKeywords(self):
        """Test read vector file keywords with the generic readKeywords method.
         """
        myKeywords = self.keywordIO.read_keywords(self.fileVectorLayer)
        myExpectedKeywords = self.expectedVectorKeywords
        mySource = self.fileVectorLayer.source()
        myMessage = 'Got: %s\n\nExpected %s\n\nSource: %s' % (
                    myKeywords, myExpectedKeywords, mySource)
        assert myKeywords == myExpectedKeywords, myMessage

    def test_appendKeywords(self):
        """Can we append file keywords with the generic readKeywords method."""
        myLayer, _ = makePadangLayerClone()
        myNewKeywords = {'category': 'exposure', 'test': 'TEST'}
        self.keywordIO.update_keywords(myLayer, myNewKeywords)
        myKeywords = self.keywordIO.read_keywords(myLayer)

        for myKey, myValue in myNewKeywords.iteritems():
            myMessage = (
                'Layer keywords misses appended key: %s\n'
                'Layer keywords:\n%s\n'
                'Appended keywords:\n%s\n' %
                (myKey,
                myKeywords,
                myNewKeywords))
            assert myKey in myKeywords, myMessage
            myMessage = (
                'Layer keywords misses appended value: %s\n'
                'Layer keywords:\n%s\n'
                'Appended keywords:\n%s\n' %
                (myValue,
                myKeywords,
                myNewKeywords))
            assert myKeywords[myKey] == myValue, myMessage

    def test_readDBKeywords(self):
        """Can we read sqlite keywords with the generic readKeywords method
        """
        myLocalPath = os.path.join(os.path.dirname(__file__),
                                   '../../..///', 'jk.sqlite')
        myPath = os.path.join(TESTDATA, 'test_keywords.db')
        self.keywordIO.set_keyword_db_path(myPath)
        # We need to make a local copy of the dataset so
        # that we can use a local path that will hash properly on the
        # database to return us the correct / valid keywords record.
        shutil.copy2(os.path.join(TESTDATA, 'jk.sqlite'), myLocalPath)
        myUri = QgsDataSourceURI()
        # always use relative path!
        myUri.setDatabase('../jk.sqlite')
        myUri.setDataSource('', 'osm_buildings', 'Geometry')
        # create a local version that has the relative url
        mySqliteLayer = QgsVectorLayer(myUri.uri(), 'OSM Buildings',
                                       'spatialite')
        myExpectedSource = ('dbname=\'../jk.sqlite\' table="osm_buildings"'
                            ' (Geometry) sql=')
        myMessage = 'Got source: %s\n\nExpected %s\n' % (
                    mySqliteLayer.source, myExpectedSource)
        assert mySqliteLayer.source() == myExpectedSource, myMessage
        myKeywords = self.keywordIO.read_keywords(mySqliteLayer)
        myExpectedKeywords = self.expectedSqliteKeywords
        assert myKeywords == myExpectedKeywords, myMessage
        mySource = self.sqliteLayer.source()
        # delete mySqliteLayer so that we can delete the file
        del mySqliteLayer
        os.remove(myLocalPath)
        myMessage = 'Got: %s\n\nExpected %s\n\nSource: %s' % (
                    myKeywords, myExpectedKeywords, mySource)
        assert myKeywords == myExpectedKeywords, myMessage
Пример #18
0
class Map():
    """A class for creating a map."""
    def __init__(self, iface):
        """Constructor for the Map class.

        :param iface: Reference to the QGIS iface object.
        :type iface: QgsAppInterface
        """
        LOGGER.debug('InaSAFE Map class initialised')
        self.iface = iface
        self.layer = iface.activeLayer()
        self.keyword_io = KeywordIO()
        self.printer = None
        self.composition = None
        self.extent = iface.mapCanvas().extent()
        self.logo = ':/plugins/inasafe/bnpb_logo.png'
        self.template = ':/plugins/inasafe/inasafe.qpt'
        self.page_width = 0  # width in mm
        self.page_height = 0  # height in mm
        self.page_dpi = 300.0
        self.show_frames = False  # intended for debugging use only

    @staticmethod
    def tr(string):
        """We implement this since we do not inherit QObject.

        :param string: String for translation.
        :type string: QString, str

        :returns: Translated version of theString.
        :rtype: QString
        """
        # noinspection PyCallByClass,PyTypeChecker,PyArgumentList
        return QtCore.QCoreApplication.translate('Map', string)

    def set_impact_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

    def set_logo(self, logo):
        """Set image that will be used as logo in reports.

        :param logo: Path to image file
        :type logo: str
        """
        self.logo = logo

    def set_template(self, template):
        """Set template that will be used for report generation.

        :param template: Path to composer template
        :type template: str
        """
        self.template = template

    def set_extent(self, extent):
        """Set extent or the report map

        :param extent: Extent of the report map
        :type extent: QgsRectangle

        """
        self.extent = extent

    def setup_composition(self):
        """Set up the composition ready for drawing elements onto it."""
        LOGGER.debug('InaSAFE Map setupComposition called')
        canvas = self.iface.mapCanvas()
        renderer = canvas.mapRenderer()
        self.composition = QgsComposition(renderer)
        self.composition.setPlotStyle(QgsComposition.Print)  # or preview
        self.composition.setPrintResolution(self.page_dpi)
        self.composition.setPrintAsRaster(True)

    def render(self):
        """Render the map composition to an image and save that to disk.

        :returns: A three-tuple of:
            * str: image_path - absolute path to png of rendered map
            * QImage: image - in memory copy of rendered map
            * QRectF: target_area - dimensions of rendered map
        :rtype: tuple
        """
        LOGGER.debug('InaSAFE Map renderComposition called')
        # NOTE: we ignore self.composition.printAsRaster() and always rasterize
        width = int(self.page_dpi * self.page_width / 25.4)
        height = int(self.page_dpi * self.page_height / 25.4)
        image = QtGui.QImage(
            QtCore.QSize(width, height),
            QtGui.QImage.Format_ARGB32)
        image.setDotsPerMeterX(dpi_to_meters(self.page_dpi))
        image.setDotsPerMeterY(dpi_to_meters(self.page_dpi))

        # Only works in Qt4.8
        #image.fill(QtGui.qRgb(255, 255, 255))
        # Works in older Qt4 versions
        image.fill(55 + 255 * 256 + 255 * 256 * 256)
        image_painter = QtGui.QPainter(image)
        source_area = QtCore.QRectF(
            0, 0, self.page_width,
            self.page_height)
        target_area = QtCore.QRectF(0, 0, width, height)
        self.composition.render(image_painter, target_area, source_area)
        image_painter.end()
        image_path = unique_filename(
            prefix='mapRender_',
            suffix='.png',
            dir=temp_dir())
        image.save(image_path)
        return image_path, image, target_area

    def make_pdf(self, filename):
        """Generate the printout for our final map.

        :param filename: Path on the file system to which the pdf should be
            saved. If None, a generated file name will be used.
        :type filename: str

        :returns: File name of the output file (equivalent to filename if
                provided).
        :rtype: str
        """
        LOGGER.debug('InaSAFE Map printToPdf called')
        if filename is None:
            map_pdf_path = unique_filename(
                prefix='report', suffix='.pdf', dir=temp_dir())
        else:
            # We need to cast to python string in case we receive a QString
            map_pdf_path = str(filename)

        self.load_template()

        resolution = self.composition.printResolution()
        self.printer = setup_printer(map_pdf_path, resolution=resolution)
        _, image, rectangle = self.render()
        painter = QtGui.QPainter(self.printer)
        painter.drawImage(rectangle, image, rectangle)
        painter.end()
        return map_pdf_path

    def map_title(self):
        """Get the map title from the layer keywords if possible.

        :returns: None on error, otherwise the title.
        :rtype: None, str
        """
        LOGGER.debug('InaSAFE Map getMapTitle called')
        try:
            title = self.keyword_io.read_keywords(self.layer, 'map_title')
            return title
        except KeywordNotFoundError:
            return None
        except Exception:
            return None

    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 getMapLegendAtributes called')
        legend_attribute_list = [
            'legend_notes',
            'legend_units',
            'legend_title']
        legend_attribute_dict = {}
        for myLegendAttribute in legend_attribute_list:
            try:
                legend_attribute_dict[myLegendAttribute] = \
                    self.keyword_io.read_keywords(
                        self.layer, myLegendAttribute)
            except KeywordNotFoundError:
                pass
            except Exception:
                pass
        return legend_attribute_dict

    def load_template(self):
        """Load a QgsComposer map from a template.
        """
        self.setup_composition()

        template_file = QtCore.QFile(self.template)
        template_file.open(QtCore.QIODevice.ReadOnly | QtCore.QIODevice.Text)
        template_content = template_file.readAll()
        template_file.close()

        document = QtXml.QDomDocument()
        document.setContent(template_content)

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

        title = self.map_title()
        if not title:
            title = ''

        substitution_map = {
            'impact-title': title,
            'date': date,
            'time': time,
            'safe-version': version
        }
        LOGGER.debug(substitution_map)
        load_ok = self.composition.loadFromTemplate(document,
                                                    substitution_map)
        if not load_ok:
            raise ReportCreationError(
                self.tr('Error loading template %s') %
                self.template)

        self.page_width = self.composition.paperWidth()
        self.page_height = self.composition.paperHeight()

        # set logo
        image = self.composition.getComposerItemById('safe-logo')
        if image is not None:
            image.setPictureFile(self.logo)
        else:
            raise ReportCreationError(self.tr(
                'Image "safe-logo" could not be found'))

        # 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
            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)
        else:
            raise ReportCreationError(self.tr(
                'Map "impact-map" could not be found'))

        legend = self.composition.getComposerItemById('impact-legend')
        legend_attributes = self.map_legend_attributes()
        LOGGER.debug(legend_attributes)
        #legend_notes = mapLegendAttributes.get('legend_notes', None)
        #legend_units = mapLegendAttributes.get('legend_units', None)
        legend_title = legend_attributes.get('legend_title', None)
        if legend_title is None:
            legend_title = ""
        legend.setTitle(legend_title)
        legend.updateLegend()
Пример #19
0
class Map():
    """A class for creating a map."""
    def __init__(self, iface):
        """Constructor for the Map class.

        :param iface: Reference to the QGIS iface object.
        :type iface: QgsAppInterface
        """
        LOGGER.debug('InaSAFE Map class initialised')
        self.iface = iface
        self.layer = iface.activeLayer()
        self.keyword_io = KeywordIO()
        self.printer = None
        self.composition = None
        self.legend = None
        self.logo = ':/plugins/inasafe/bnpb_logo.png'
        self.template = ':/plugins/inasafe/inasafe.qpt'
        #self.page_width = 210  # width in mm
        #self.page_height = 297  # height in mm
        self.page_width = 0  # width in mm
        self.page_height = 0  # height in mm
        self.page_dpi = 300.0
        #self.page_margin = 10  # margin in mm
        self.show_frames = False  # intended for debugging use only
        self.page_margin = None
        #vertical spacing between elements
        self.vertical_spacing = None
        self.map_height = None
        self.mapWidth = None
        # make a square map where width = height = page width
        #self.map_height = self.page_width - (self.page_margin * 2)
        #self.mapWidth = self.map_height
        #self.disclaimer = self.tr('InaSAFE has been jointly developed by'
        #                          ' BNPB, AusAid & the World Bank')

    @staticmethod
    def tr(string):
        """We implement this since we do not inherit QObject.

        :param string: String for translation.
        :type string: QString, str

        :returns: Translated version of theString.
        :rtype: QString
        """
        # noinspection PyCallByClass,PyTypeChecker,PyArgumentList
        return QtCore.QCoreApplication.translate('Map', string)

    def set_impact_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

    def set_logo(self, logo):
        """

        :param logo: Path to image that will be used as logo in report
        :type logo: str
        """
        self.logo = logo

    def set_template(self, template):
        """

        :param template: Path to composer template that will be used for report
        :type template: str
        """
        self.template = template

    def setup_composition(self):
        """Set up the composition ready for drawing elements onto it."""
        LOGGER.debug('InaSAFE Map setupComposition called')
        canvas = self.iface.mapCanvas()
        renderer = canvas.mapRenderer()
        self.composition = QgsComposition(renderer)
        self.composition.setPlotStyle(QgsComposition.Print)  # or preview
        #self.composition.setPaperSize(self.page_width, self.page_height)
        self.composition.setPrintResolution(self.page_dpi)
        self.composition.setPrintAsRaster(True)

    def compose_map(self):
        """Place all elements on the map ready for printing."""
        self.setup_composition()
        # Keep track of our vertical positioning as we work our way down
        # the page placing elements on it.
        top_offset = self.page_margin
        self.draw_logo(top_offset)
        label_height = self.draw_title(top_offset)
        # Update the map offset for the next row of content
        top_offset += label_height + self.vertical_spacing
        composer_map = self.draw_map(top_offset)
        self.draw_scalebar(composer_map, top_offset)
        # Update the top offset for the next horizontal row of items
        top_offset += self.map_height + self.vertical_spacing - 1
        impact_title_height = self.draw_impact_title(top_offset)
        # Update the top offset for the next horizontal row of items
        if impact_title_height:
            top_offset += impact_title_height + self.vertical_spacing + 2
        self.draw_legend(top_offset)
        self.draw_host_and_time(top_offset)
        self.draw_disclaimer()

    def render(self):
        """Render the map composition to an image and save that to disk.

        :returns: A three-tuple of:
            * str: image_path - absolute path to png of rendered map
            * QImage: image - in memory copy of rendered map
            * QRectF: target_area - dimensions of rendered map
        :rtype: tuple
        """
        LOGGER.debug('InaSAFE Map renderComposition called')
        # NOTE: we ignore self.composition.printAsRaster() and always rasterise
        width = int(self.page_dpi * self.page_width / 25.4)
        height = int(self.page_dpi * self.page_height / 25.4)
        image = QtGui.QImage(QtCore.QSize(width, height),
                             QtGui.QImage.Format_ARGB32)
        image.setDotsPerMeterX(dpi_to_meters(self.page_dpi))
        image.setDotsPerMeterY(dpi_to_meters(self.page_dpi))

        # Only works in Qt4.8
        #image.fill(QtGui.qRgb(255, 255, 255))
        # Works in older Qt4 versions
        image.fill(55 + 255 * 256 + 255 * 256 * 256)
        image_painter = QtGui.QPainter(image)
        source_area = QtCore.QRectF(0, 0, self.page_width, self.page_height)
        target_area = QtCore.QRectF(0, 0, width, height)
        self.composition.render(image_painter, target_area, source_area)
        image_painter.end()
        image_path = unique_filename(prefix='mapRender_',
                                     suffix='.png',
                                     dir=temp_dir())
        image.save(image_path)
        return image_path, image, target_area

    def make_pdf(self, filename):
        """Generate the printout for our final map.

        :param filename: Path on the file system to which the pdf should be
            saved. If None, a generated file name will be used.
        :type filename: str

        :returns: File name of the output file (equivalent to filename if
                provided).
        :rtype: str
        """
        LOGGER.debug('InaSAFE Map printToPdf called')
        if filename is None:
            map_pdf_path = unique_filename(prefix='report',
                                           suffix='.pdf',
                                           dir=temp_dir())
        else:
            # We need to cast to python string in case we receive a QString
            map_pdf_path = str(filename)

        self.load_template()

        resolution = self.composition.printResolution()
        self.printer = setup_printer(map_pdf_path, resolution=resolution)
        _, image, rectangle = self.render()
        painter = QtGui.QPainter(self.printer)
        painter.drawImage(rectangle, image, rectangle)
        painter.end()
        return map_pdf_path

    def draw_logo(self, top_offset):
        """Add a picture containing the logo to the map top left corner

        :param top_offset: Vertical offset at which the logo should be drawn.
        :type top_offset: int
        """
        logo = QgsComposerPicture(self.composition)
        logo.setPictureFile(':/plugins/inasafe/bnpb_logo.png')
        logo.setItemPosition(self.page_margin, top_offset, 10, 10)
        logo.setFrameEnabled(self.show_frames)
        logo.setZValue(1)  # To ensure it overlays graticule markers
        self.composition.addItem(logo)

    def draw_title(self, top_offset):
        """Add a title to the composition.

        :param top_offset: Vertical offset at which the logo should be drawn.
        :type top_offset: int

        :returns: The height of the label as rendered.
        :rtype: float
        """
        LOGGER.debug('InaSAFE Map drawTitle called')
        font_size = 14
        font_weight = QtGui.QFont.Bold
        italics_flag = False
        font = QtGui.QFont('verdana', font_size, font_weight, italics_flag)
        label = QgsComposerLabel(self.composition)
        label.setFont(font)
        heading = self.tr(
            'InaSAFE - Indonesia Scenario Assessment for Emergencies')
        label.setText(heading)
        label.adjustSizeToText()
        label_height = 10.0  # determined using qgis map composer
        label_width = 170.0  # item - position and size...option
        left_offset = self.page_width - self.page_margin - label_width
        label.setItemPosition(
            left_offset,
            top_offset - 2,  # -2 to push it up a little
            label_width,
            label_height)
        label.setFrameEnabled(self.show_frames)
        self.composition.addItem(label)
        return label_height

    def draw_map(self, top_offset):
        """Add a map to the composition and return the composer map instance.

        :param top_offset: Vertical offset at which the logo should be drawn.
        :type top_offset: int

        :returns: The composer map.
        :rtype: QgsComposerMap
        """
        LOGGER.debug('InaSAFE Map drawMap called')
        map_width = self.mapWidth
        composer_map = QgsComposerMap(self.composition, self.page_margin,
                                      top_offset, map_width, self.map_height)
        #myExtent = self.iface.mapCanvas().extent()
        # The dimensions of the map canvas and the print composer map may
        # differ. So we set the map composer extent using the canvas and
        # then defer to the map canvas's map extents thereafter
        # Update: disabled as it results in a rectangular rather than
        # square map
        #composer_map.setNewExtent(myExtent)
        composer_extent = composer_map.extent()
        # Recenter the composer map on the center of the canvas
        # 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.iface.mapCanvas().extent()
        width = canvas_extent.width()
        height = canvas_extent.height()
        longest_length = width
        if width < height:
            longest_length = height
        half_length = longest_length / 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
        square_extent = QgsRectangle(min_x, min_y, max_x, max_y)
        composer_map.setNewExtent(square_extent)

        composer_map.setGridEnabled(True)
        split_count = 5
        # .. todo:: Write logic to adjust precision so that adjacent tick marks
        #    always have different displayed values
        precision = 2
        x_interval = composer_extent.width() / split_count
        composer_map.setGridIntervalX(x_interval)
        y_interval = composer_extent.height() / split_count
        composer_map.setGridIntervalY(y_interval)
        composer_map.setGridStyle(QgsComposerMap.Cross)
        cross_length_mm = 1
        composer_map.setCrossLength(cross_length_mm)
        composer_map.setZValue(0)  # To ensure it does not overlay logo
        font_size = 6
        font_weight = QtGui.QFont.Normal
        italics_flag = False
        font = QtGui.QFont('verdana', font_size, font_weight, italics_flag)
        composer_map.setGridAnnotationFont(font)
        composer_map.setGridAnnotationPrecision(precision)
        composer_map.setShowGridAnnotation(True)
        composer_map.setGridAnnotationDirection(
            QgsComposerMap.BoundaryDirection, QgsComposerMap.Top)
        self.composition.addItem(composer_map)
        self.draw_graticule_mask(top_offset)
        return composer_map

    def draw_graticule_mask(self, top_offset):
        """A helper function to mask out graticule labels.

         It will hide labels on the right side by over painting a white
         rectangle with white border on them. **kludge**

        :param top_offset: Vertical offset at which the logo should be drawn.
        :type top_offset: int
        """
        LOGGER.debug('InaSAFE Map drawGraticuleMask called')
        left_offset = self.page_margin + self.mapWidth
        rect = QgsComposerShape(left_offset + 0.5, top_offset,
                                self.page_width - left_offset,
                                self.map_height + 1, self.composition)

        rect.setShapeType(QgsComposerShape.Rectangle)
        pen = QtGui.QPen()
        pen.setColor(QtGui.QColor(0, 0, 0))
        pen.setWidthF(0.1)
        rect.setPen(pen)
        rect.setBackgroundColor(QtGui.QColor(255, 255, 255))
        rect.setTransparency(100)
        #rect.setLineWidth(0.1)
        #rect.setFrameEnabled(False)
        #rect.setOutlineColor(QtGui.QColor(255, 255, 255))
        #rect.setFillColor(QtGui.QColor(255, 255, 255))
        #rect.setOpacity(100)
        # These two lines seem superfluous but are needed
        brush = QtGui.QBrush(QtGui.QColor(255, 255, 255))
        rect.setBrush(brush)
        self.composition.addItem(rect)

    def draw_native_scalebar(self, composer_map, top_offset):
        """Draw a scale bar using QGIS' native drawing.

        In the case of geographic maps, scale will be in degrees, not km.

        :param composer_map: Composer map on which to draw the scalebar.
        :type composer_map: QgsComposerMap

        :param top_offset: Vertical offset at which the logo should be drawn.
        :type top_offset: int
        """
        LOGGER.debug('InaSAFE Map drawNativeScaleBar called')
        scale_bar = QgsComposerScaleBar(self.composition)
        scale_bar.setStyle('Numeric')  # optionally modify the style
        scale_bar.setComposerMap(composer_map)
        scale_bar.applyDefaultSize()
        scale_bar_height = scale_bar.boundingRect().height()
        scale_bar_width = scale_bar.boundingRect().width()
        # -1 to avoid overlapping the map border
        scale_bar.setItemPosition(
            self.page_margin + 1,
            top_offset + self.map_height - (scale_bar_height * 2),
            scale_bar_width, scale_bar_height)
        scale_bar.setFrameEnabled(self.show_frames)
        # Disabled for now
        #self.composition.addItem(scale_bar)

    def draw_scalebar(self, composer_map, top_offset):
        """Add a numeric scale to the bottom left of the map.

        We draw the scale bar manually because QGIS does not yet support
        rendering a scale bar for a geographic map in km.

        .. seealso:: :meth:`drawNativeScaleBar`

        :param composer_map: Composer map on which to draw the scalebar.
        :type composer_map: QgsComposerMap

        :param top_offset: Vertical offset at which the logo should be drawn.
        :type top_offset: int
        """
        LOGGER.debug('InaSAFE Map drawScaleBar called')
        canvas = self.iface.mapCanvas()
        renderer = canvas.mapRenderer()
        #
        # Add a linear map scale
        #
        distance_area = QgsDistanceArea()
        distance_area.setSourceCrs(renderer.destinationCrs().srsid())
        distance_area.setEllipsoidalMode(True)
        # Determine how wide our map is in km/m
        # Starting point at BL corner
        composer_extent = composer_map.extent()
        start_point = QgsPoint(composer_extent.xMinimum(),
                               composer_extent.yMinimum())
        # Ending point at BR corner
        end_point = QgsPoint(composer_extent.xMaximum(),
                             composer_extent.yMinimum())
        ground_distance = distance_area.measureLine(start_point, end_point)
        # Get the equivalent map distance per page mm
        map_width = self.mapWidth
        # How far is 1mm on map on the ground in meters?
        mm_to_ground = ground_distance / map_width
        #print 'MM:', myMMDistance
        # How long we want the scale bar to be in relation to the map
        scalebar_to_map_ratio = 0.5
        # How many divisions the scale bar should have
        tick_count = 5
        scale_bar_width_mm = map_width * scalebar_to_map_ratio
        print_segment_width_mm = scale_bar_width_mm / tick_count
        # Segment width in real world (m)
        # We apply some logic here so that segments are displayed in meters
        # if each segment is less that 1000m otherwise km. Also the segment
        # lengths are rounded down to human looking numbers e.g. 1km not 1.1km
        units = ''
        ground_segment_width = print_segment_width_mm * mm_to_ground
        if ground_segment_width < 1000:
            units = 'm'
            ground_segment_width = round(ground_segment_width)
            # adjust the segment width now to account for rounding
            print_segment_width_mm = ground_segment_width / mm_to_ground
        else:
            units = 'km'
            # Segment with in real world (km)
            ground_segment_width = round(ground_segment_width / 1000)
            print_segment_width_mm = ((ground_segment_width * 1000) /
                                      mm_to_ground)
        # Now adjust the scalebar width to account for rounding
        scale_bar_width_mm = tick_count * print_segment_width_mm

        #print "SBWMM:", scale_bar_width_mm
        #print "SWMM:", print_segment_width_mm
        #print "SWM:", myGroundSegmentWidthM
        #print "SWKM:", myGroundSegmentWidthKM
        # start drawing in line segments
        scalebar_height = 5  # mm
        line_width = 0.3  # mm
        inset_distance = 7  # how much to inset the scalebar into the map by
        scalebar_x = self.page_margin + inset_distance
        scalebar_y = (top_offset + self.map_height - inset_distance -
                      scalebar_height)  # mm

        # Draw an outer background box - shamelessly hardcoded buffer
        rectangle = QgsComposerShape(
            scalebar_x - 4,  # left edge
            scalebar_y - 3,  # top edge
            scale_bar_width_mm + 13,  # right edge
            scalebar_height + 6,  # bottom edge
            self.composition)

        rectangle.setShapeType(QgsComposerShape.Rectangle)
        pen = QtGui.QPen()
        pen.setColor(QtGui.QColor(255, 255, 255))
        pen.setWidthF(line_width)
        rectangle.setPen(pen)
        #rectangle.setLineWidth(line_width)
        rectangle.setFrameEnabled(False)
        brush = QtGui.QBrush(QtGui.QColor(255, 255, 255))
        # workaround for missing setTransparentFill missing from python api
        rectangle.setBrush(brush)
        self.composition.addItem(rectangle)
        # Set up the tick label font
        font_weight = QtGui.QFont.Normal
        font_size = 6
        italics_flag = False
        font = QtGui.QFont('verdana', font_size, font_weight, italics_flag)
        # Draw the bottom line
        up_shift = 0.3  # shift the bottom line up for better rendering
        rectangle = QgsComposerShape(scalebar_x,
                                     scalebar_y + scalebar_height - up_shift,
                                     scale_bar_width_mm, 0.1, self.composition)

        rectangle.setShapeType(QgsComposerShape.Rectangle)
        pen = QtGui.QPen()
        pen.setColor(QtGui.QColor(255, 255, 255))
        pen.setWidthF(line_width)
        rectangle.setPen(pen)
        #rectangle.setLineWidth(line_width)
        rectangle.setFrameEnabled(False)
        self.composition.addItem(rectangle)

        # Now draw the scalebar ticks
        for tick_counter in range(0, tick_count + 1):
            distance_suffix = ''
            if tick_counter == tick_count:
                distance_suffix = ' ' + units
            real_world_distance = (
                '%.0f%s' %
                (tick_counter * ground_segment_width, distance_suffix))
            #print 'RW:', myRealWorldDistance
            mm_offset = scalebar_x + (tick_counter * print_segment_width_mm)
            #print 'MM:', mm_offset
            tick_height = scalebar_height / 2
            # Lines are not exposed by the api yet so we
            # bodge drawing lines using rectangles with 1px height or width
            tick_width = 0.1  # width or rectangle to be drawn
            uptick_line = QgsComposerShape(
                mm_offset, scalebar_y + scalebar_height - tick_height,
                tick_width, tick_height, self.composition)

            uptick_line.setShapeType(QgsComposerShape.Rectangle)
            pen = QtGui.QPen()
            pen.setWidthF(line_width)
            uptick_line.setPen(pen)
            #uptick_line.setLineWidth(line_width)
            uptick_line.setFrameEnabled(False)
            self.composition.addItem(uptick_line)
            #
            # Add a tick label
            #
            label = QgsComposerLabel(self.composition)
            label.setFont(font)
            label.setText(real_world_distance)
            label.adjustSizeToText()
            label.setItemPosition(mm_offset - 3, scalebar_y - tick_height)
            label.setFrameEnabled(self.show_frames)
            self.composition.addItem(label)

    def draw_impact_title(self, top_offset):
        """Draw the map subtitle - obtained from the impact layer keywords.

        :param top_offset: Vertical offset at which the logo should be drawn.
        :type top_offset: int

        :returns: The height of the label as rendered.
        :rtype: float
        """
        LOGGER.debug('InaSAFE Map drawImpactTitle called')
        title = self.map_title()
        if title is None:
            title = ''
        font_size = 20
        font_weight = QtGui.QFont.Bold
        italics_flag = False
        font = QtGui.QFont('verdana', font_size, font_weight, italics_flag)
        label = QgsComposerLabel(self.composition)
        label.setFont(font)
        heading = title
        label.setText(heading)
        label_width = self.page_width - (self.page_margin * 2)
        label_height = 12
        label.setItemPosition(self.page_margin, top_offset, label_width,
                              label_height)
        label.setFrameEnabled(self.show_frames)
        self.composition.addItem(label)
        return label_height

    def draw_legend(self, top_offset):
        """Add a legend to the map using our custom legend renderer.

        .. note:: getLegend generates a pixmap in 150dpi so if you set
           the map to a higher dpi it will appear undersized.

        :param top_offset: Vertical offset at which the logo should be drawn.
        :type top_offset: int
        """
        LOGGER.debug('InaSAFE Map drawLegend called')
        legend_attributes = self.map_legend_attributes()
        legend_notes = legend_attributes.get('legend_notes', None)
        legend_units = legend_attributes.get('legend_units', None)
        legend_title = legend_attributes.get('legend_title', None)
        LOGGER.debug(legend_attributes)
        legend = MapLegend(self.layer, self.page_dpi, legend_title,
                           legend_notes, legend_units)
        self.legend = legend.get_legend()
        picture1 = QgsComposerPicture(self.composition)
        legend_file_path = unique_filename(prefix='legend',
                                           suffix='.png',
                                           dir='work')
        self.legend.save(legend_file_path, 'PNG')
        picture1.setPictureFile(legend_file_path)
        legend_height = points_to_mm(self.legend.height(), self.page_dpi)
        legend_width = points_to_mm(self.legend.width(), self.page_dpi)
        picture1.setItemPosition(self.page_margin, top_offset, legend_width,
                                 legend_height)
        picture1.setFrameEnabled(False)
        self.composition.addItem(picture1)
        os.remove(legend_file_path)

    def draw_image(self, image, width_mm, left_offset, top_offset):
        """Helper to draw an image directly onto the QGraphicsScene.
        This is an alternative to using QgsComposerPicture which in
        some cases leaves artifacts under windows.

        The Pixmap will have a transform applied to it so that
        it is rendered with the same resolution as the composition.

        :param image: Image that will be rendered to the layout.
        :type image: QImage

        :param width_mm: Desired width in mm of output on page.
        :type width_mm: int

        :param left_offset: Offset from left of page.
        :type left_offset: int

        :param top_offset: Offset from top of page.
        :type top_offset: int

        :returns: Graphics scene item.
        :rtype: QGraphicsSceneItem
        """
        LOGGER.debug('InaSAFE Map drawImage called')
        desired_width_mm = width_mm  # mm
        desired_width_px = mm_to_points(desired_width_mm, self.page_dpi)
        actual_width_px = image.width()
        scale_factor = desired_width_px / actual_width_px

        LOGGER.debug('%s %s %s' %
                     (scale_factor, actual_width_px, desired_width_px))
        transform = QtGui.QTransform()
        transform.scale(scale_factor, scale_factor)
        transform.rotate(0.5)
        # noinspection PyArgumentList
        item = self.composition.addPixmap(QtGui.QPixmap.fromImage(image))
        item.setTransform(transform)
        item.setOffset(left_offset / scale_factor, top_offset / scale_factor)
        return item

    def draw_host_and_time(self, top_offset):
        """Add a note with hostname and time to the composition.

        :param top_offset: Vertical offset at which the logo should be drawn.
        :type top_offset: int
        """
        LOGGER.debug('InaSAFE Map drawDisclaimer called')
        #elapsed_time: 11.612545
        #user: timlinux
        #host_name: ultrabook
        #time_stamp: 2012-10-13_23:10:31
        #myUser = self.keyword_io.readKeywords(self.layer, 'user')
        #myHost = self.keyword_io.readKeywords(self.layer, 'host_name')
        date_time = self.keyword_io.read_keywords(self.layer, 'time_stamp')
        tokens = date_time.split('_')
        date = tokens[0]
        time = tokens[1]
        #myElapsedTime = self.keyword_io.readKeywords(self.layer,
        #                                            'elapsed_time')
        #myElapsedTime = humaniseSeconds(myElapsedTime)
        long_version = get_version()
        tokens = long_version.split('.')
        version = '%s.%s.%s' % (tokens[0], tokens[1], tokens[2])
        label_text = self.tr(
            'Date and time of assessment: %s %s\n'
            'Special note: This assessment is a guide - we strongly recommend '
            'that you ground truth the results shown here before deploying '
            'resources and / or personnel.\n'
            'Assessment carried out using InaSAFE release %s (QGIS '
            'plugin version).') % (date, time, version)
        font_size = 6
        font_weight = QtGui.QFont.Normal
        italics_flag = True
        font = QtGui.QFont('verdana', font_size, font_weight, italics_flag)
        label = QgsComposerLabel(self.composition)
        label.setFont(font)
        label.setText(label_text)
        label.adjustSizeToText()
        label_height = 50.0  # mm determined using qgis map composer
        label_width = (self.page_width / 2) - self.page_margin
        left_offset = self.page_width / 2  # put in right half of page
        label.setItemPosition(
            left_offset,
            top_offset,
            label_width,
            label_height,
        )
        label.setFrameEnabled(self.show_frames)
        self.composition.addItem(label)

    def draw_disclaimer(self):
        """Add a disclaimer to the composition."""
        LOGGER.debug('InaSAFE Map drawDisclaimer called')
        font_size = 10
        font_weight = QtGui.QFont.Normal
        italics_flag = True
        font = QtGui.QFont('verdana', font_size, font_weight, italics_flag)
        label = QgsComposerLabel(self.composition)
        label.setFont(font)
        label.setText(self.disclaimer)
        label.adjustSizeToText()
        label_height = 7.0  # mm determined using qgis map composer
        label_width = self.page_width  # item - position and size...option
        left_offset = self.page_margin
        top_offset = self.page_height - self.page_margin
        label.setItemPosition(
            left_offset,
            top_offset,
            label_width,
            label_height,
        )
        label.setFrameEnabled(self.show_frames)
        self.composition.addItem(label)

    def map_title(self):
        """Get the map title from the layer keywords if possible.

        :returns: None on error, otherwise the title.
        :rtype: None, str
        """
        LOGGER.debug('InaSAFE Map getMapTitle called')
        try:
            title = self.keyword_io.read_keywords(self.layer, 'map_title')
            return title
        except KeywordNotFoundError:
            return None
        except Exception:
            return None

    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 getMapLegendAtributes called')
        legend_attribute_list = [
            'legend_notes', 'legend_units', 'legend_title'
        ]
        legend_attribute_dict = {}
        for myLegendAttribute in legend_attribute_list:
            try:
                legend_attribute_dict[myLegendAttribute] = \
                    self.keyword_io.read_keywords(
                        self.layer, myLegendAttribute)
            except KeywordNotFoundError:
                pass
            except Exception:
                pass
        return legend_attribute_dict

    def show_composer(self):
        """Show the composition in a composer view so the user can tweak it.
        """
        view = QgsComposerView(self.iface.mainWindow())
        view.show()

    def write_template(self, template_path):
        """Write current composition as a template that can be re-used in QGIS.

        :param template_path: Path to which template should be written.
        :type template_path: str
        """
        document = QtXml.QDomDocument()
        element = document.createElement('Composer')
        document.appendChild(element)
        self.composition.writeXML(element, document)
        xml = document.toByteArray()
        template_file = file(template_path, 'wb')
        template_file.write(xml)
        template_file.close()

    def load_template(self):
        """Load a QgsComposer map from a template and render it.

        .. note:: THIS METHOD IS EXPERIMENTAL
        """
        self.setup_composition()

        template_file = QtCore.QFile(self.template)
        template_file.open(QtCore.QIODevice.ReadOnly | QtCore.QIODevice.Text)
        template_content = template_file.readAll()
        template_file.close()

        document = QtXml.QDomDocument()
        document.setContent(template_content)

        # get information for substitutions
        # date, time and plugin version
        date_time = self.keyword_io.read_keywords(self.layer, 'time_stamp')
        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])

        # map title
        LOGGER.debug('InaSAFE Map getMapTitle called')
        try:
            title = self.keyword_io.read_keywords(self.layer, 'map_title')
        except KeywordNotFoundError:
            title = None
        except Exception:
            title = None

        if not title:
            title = ''

        substitution_map = {
            'impact-title': title,
            'date': date,
            'time': time,
            'safe-version': version
        }
        LOGGER.debug(substitution_map)
        load_ok = self.composition.loadFromTemplate(document, substitution_map)
        if not load_ok:
            raise ReportCreationError(
                self.tr('Error loading template %s') % self.template)

        self.page_width = self.composition.paperWidth()
        self.page_height = self.composition.paperHeight()

        # set logo
        image = self.composition.getComposerItemById('safe-logo')
        image.setPictureFile(self.logo)

        # Get the main map canvas on the composition and set
        # its extents to the event.
        map = self.composition.getComposerItemById('impact-map')
        if map is not None:
            # Recenter the composer map on the center of the canvas
            # 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.iface.mapCanvas().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
            square_extent = QgsRectangle(min_x, min_y, max_x, max_y)
            map.setNewExtent(square_extent)

            # calculate intervals for grid
            split_count = 5
            x_interval = square_extent.width() / split_count
            map.setGridIntervalX(x_interval)
            y_interval = square_extent.height() / split_count
            map.setGridIntervalY(y_interval)
        else:
            raise ReportCreationError(
                self.tr('Map "impact-map" could not be found'))

        legend = self.composition.getComposerItemById('impact-legend')
        legend_attributes = self.map_legend_attributes()
        LOGGER.debug(legend_attributes)
        #legend_notes = mapLegendAttributes.get('legend_notes', None)
        #legend_units = mapLegendAttributes.get('legend_units', None)
        legend_title = legend_attributes.get('legend_title', None)
        if legend_title is None:
            legend_title = ""
        legend.setTitle(legend_title)
        legend.updateLegend()
Пример #20
0
class KeywordIOTest(unittest.TestCase):
    """Tests for reading and writing of raster and vector data
    """

    def setUp(self):
        self.keyword_io = KeywordIO()
        uri = QgsDataSourceURI()
        uri.setDatabase(os.path.join(TESTDATA, 'jk.sqlite'))
        uri.setDataSource('', 'osm_buildings', 'Geometry')
        self.sqlite_layer = QgsVectorLayer(
            uri.uri(), 'OSM Buildings', 'spatialite')
        hazard_path = os.path.join(HAZDATA, 'Shakemap_Padang_2009.asc')
        self.raster_layer, layer_type = load_layer(
            hazard_path, directory=None)
        del layer_type
        self.vector_layer, layer_type = load_layer('Padang_WGS84.shp')
        del layer_type
        self.expected_sqlite_keywords = {
            'category': 'exposure',
            'datatype': 'OSM',
            'subcategory': 'building'}
        self.expected_vector_keywords = {
            'category': 'exposure',
            'datatype': 'itb',
            'subcategory': 'structure',
            'title': 'Padang WGS84'}
        self.expected_raster_keywords = {
            'category': 'hazard',
            'source': 'USGS',
            'subcategory': 'earthquake',
            'unit': 'MMI',
            'title': ('An earthquake in Padang '
            'like in 2009')}

    def tearDown(self):
        pass

    def test_get_hash_for_datasource(self):
        """Test we can reliably get a hash for a uri"""
        hash_value = self.keyword_io.hash_for_datasource(PG_URI)
        expected_hash = '7cc153e1b119ca54a91ddb98a56ea95e'
        message = "Got: %s\nExpected: %s" % (hash_value, expected_hash)
        assert hash_value == expected_hash, message

    def test_write_read_keyword_from_uri(self):
        """Test we can set and get keywords for a non local datasource"""
        handle, filename = tempfile.mkstemp(
            '.db', 'keywords_', temp_dir())

        # Ensure the file is deleted before we try to write to it
        # fixes windows specific issue where you get a message like this
        # ERROR 1: c:\temp\inasafe\clip_jpxjnt.shp is not a directory.
        # This is because mkstemp creates the file handle and leaves
        # the file open.
        os.close(handle)
        os.remove(filename)
        expected_keywords = {
            'category': 'exposure',
            'datatype': 'itb',
            'subcategory': 'building'}
        # SQL insert test
        # On first write schema is empty and there is no matching hash
        self.keyword_io.set_keyword_db_path(filename)
        self.keyword_io.write_keywords_for_uri(PG_URI, expected_keywords)
        # SQL Update test
        # On second write schema is populated and we update matching hash
        expected_keywords = {
            'category': 'exposure',
            'datatype': 'OSM',  # <--note the change here!
            'subcategory': 'building'}
        self.keyword_io.write_keywords_for_uri(PG_URI, expected_keywords)
        # Test getting all keywords
        keywords = self.keyword_io.read_keyword_from_uri(PG_URI)
        message = 'Got: %s\n\nExpected %s\n\nDB: %s' % (
            keywords, expected_keywords, filename)
        assert keywords == expected_keywords, message
        # Test getting just a single keyword
        keyword = self.keyword_io.read_keyword_from_uri(PG_URI, 'datatype')
        expected_keyword = 'OSM'
        message = 'Got: %s\n\nExpected %s\n\nDB: %s' % (
            keyword, expected_keyword, filename)
        assert keyword == expected_keyword, message
        # Test deleting keywords actually does delete
        self.keyword_io.delete_keywords_for_uri(PG_URI)
        try:
            _ = self.keyword_io.read_keyword_from_uri(PG_URI, 'datatype')
            #if the above didnt cause an exception then bad
            message = 'Expected a HashNotFoundError to be raised'
            assert message
        except HashNotFoundError:
            #we expect this outcome so good!
            pass

    def test_are_keywords_file_based(self):
        """Can we correctly determine if keywords should be written to file or
        to database?"""
        assert not self.keyword_io.are_keywords_file_based(self.sqlite_layer)
        assert self.keyword_io.are_keywords_file_based(self.raster_layer)
        assert self.keyword_io.are_keywords_file_based(self.vector_layer)

    def test_read_raster_file_keywords(self):
        """Can we read raster file keywords using generic readKeywords method
        """
        keywords = self.keyword_io.read_keywords(self.raster_layer)
        expected_keywords = self.expected_raster_keywords
        source = self.raster_layer.source()
        message = 'Got:\n%s\nExpected:\n%s\nSource:\n%s' % (
            keywords, expected_keywords, source)
        self.assertEquals(keywords, expected_keywords, message)

    def test_read_vector_file_keywords(self):
        """Test read vector file keywords with the generic readKeywords method.
         """
        keywords = self.keyword_io.read_keywords(self.vector_layer)
        expected_keywords = self.expected_vector_keywords
        source = self.vector_layer.source()
        message = 'Got: %s\n\nExpected %s\n\nSource: %s' % (
            keywords, expected_keywords, source)
        assert keywords == expected_keywords, message

    def test_append_keywords(self):
        """Can we append file keywords with the generic readKeywords method."""
        layer, _ = clone_padang_layer()
        new_keywords = {'category': 'exposure', 'test': 'TEST'}
        self.keyword_io.update_keywords(layer, new_keywords)
        keywords = self.keyword_io.read_keywords(layer)

        for key, value in new_keywords.iteritems():
            message = (
                'Layer keywords misses appended key: %s\n'
                'Layer keywords:\n%s\n'
                'Appended keywords:\n%s\n' %
                (key,
                keywords,
                new_keywords))
            assert key in keywords, message
            message = (
                'Layer keywords misses appended value: %s\n'
                'Layer keywords:\n%s\n'
                'Appended keywords:\n%s\n' %
                (value,
                keywords,
                new_keywords))
            assert keywords[key] == value, message

    def test_read_db_keywords(self):
        """Can we read sqlite keywords with the generic readKeywords method
        """
        # noinspection PyUnresolvedReferences
        local_path = os.path.join(
            os.path.dirname(__file__), '../../..///', 'jk.sqlite')
        path = os.path.join(TESTDATA, 'test_keywords.db')
        self.keyword_io.set_keyword_db_path(path)
        # We need to make a local copy of the dataset so
        # that we can use a local path that will hash properly on the
        # database to return us the correct / valid keywords record.
        shutil.copy2(os.path.join(TESTDATA, 'jk.sqlite'), local_path)
        uri = QgsDataSourceURI()
        # always use relative path!
        uri.setDatabase('../jk.sqlite')
        uri.setDataSource('', 'osm_buildings', 'Geometry')
        # create a local version that has the relative url
        sqlite_layer = QgsVectorLayer(uri.uri(), 'OSM Buildings', 'spatialite')
        expected_source = (
            'dbname=\'../jk.sqlite\' table="osm_buildings" (Geometry) sql=')
        message = 'Got source: %s\n\nExpected %s\n' % (
            sqlite_layer.source, expected_source)
        assert sqlite_layer.source() == expected_source, message
        keywords = self.keyword_io.read_keywords(sqlite_layer)
        expected_keywords = self.expected_sqlite_keywords
        assert keywords == expected_keywords, message
        source = self.sqlite_layer.source()
        # delete sqlite_layer so that we can delete the file
        del sqlite_layer
        os.remove(local_path)
        message = 'Got: %s\n\nExpected %s\n\nSource: %s' % (
            keywords, expected_keywords, source)
        assert keywords == expected_keywords, message

    def test_copy_keywords(self):
        """Test we can copy the keywords."""
        out_path = unique_filename(
            prefix='test_copy_keywords', suffix='.keywords')
        self.keyword_io.copy_keywords(self.raster_layer, out_path)
        copied_keywords = read_file_keywords(out_path)
        expected_keywords = self.expected_raster_keywords
        message = 'Got:\n%s\nExpected:\n%s\nSource:\n%s' % (
            copied_keywords, expected_keywords, out_path)
        self.assertEquals(copied_keywords, expected_keywords, message)
Пример #21
0
class ImpactMergeDialog(QDialog, Ui_ImpactMergeDialogBase):
    """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 = ':/plugins/inasafe/merged_report.qpt'

        # Safe Logo Path
        self.safe_logo_path = ':/plugins/inasafe/inasafe-logo-url.png'

        # Organisation Logo Path
        self.organisation_logo_path = ':/plugins/inasafe/supporters.png'

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

        # The summary report, contains report for each aggregation area
        self.summary_report = {}

        # The html reports and its file path
        self.html_reports = {}

        # 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 context help
        help_button = self.button_box.button(QtGui.QDialogButtonBox.Help)
        help_button.clicked.connect(self.show_help)

        # Show usage info
        self.show_info()
        self.restore_state()

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

        string = header

        heading = m.Heading(self.tr('Impact Layer Merge Tool'), **INFO_STYLE)
        body = self.tr(
            'This tool will merge the outputs from two impact maps for the '
            'same area. The maps must be created using the same aggregation '
            'areas and same hazard. To use:')
        tips = m.BulletedList()
        tips.add(
            self.tr(
                'Run an impact assessment for an area using aggregation. e.g.'
                'Flood Impact on Buildings aggregated by municipal boundaries.'
            ))
        tips.add(
            self.
            tr('Run a second impact assessment for the same area using the same '
               'aggregation. e.g. Flood Impact on People aggregated by '
               'municipal boundaries.'))
        tips.add(
            self.
            tr('Open this tool and select each impact layer from the pick lists '
               'provided below.'))
        tips.add(
            self.tr(
                'Select the aggregation layer that was used to generate the '
                'first and second impact layer.'))
        tips.add(self.tr('Select an output directory.'))
        tips.add(
            self.
            tr('Check "Use customized report template" checkbox and select the '
               'report template file if you want to use your own template. Note '
               'that all the map composer components that are needed must be '
               'fulfilled.'))
        tips.add(
            self.tr('Click OK to generate the per aggregation area combined '
                    'summaries.'))
        message = m.Message()
        message.add(heading)
        message.add(body)
        message.add(tips)
        string += message.to_html()
        string += footer

        self.web_view.setHtml(string)

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

    @staticmethod
    def show_help():
        """Load the help text for the dialog."""
        show_context_help('impact_layer_merge_tool')

    @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
        output_directory_url = QUrl.fromLocalFile(self.out_dir)
        # noinspection PyTypeChecker,PyCallByClass
        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
        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()

        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
                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 Layer
        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
        if self.aggregation['layer'] is None:
            self.entire_area_mode = True

    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 = str(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(first_report)
        second_document = minidom.parseString(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)

        # 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]
            if os.path.exists(report_path):
                os.remove(report_path)

    @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":
               {"Detailed Building Type Report":
                   {"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":
                {"Detailed Building Type Report":
                    {"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(), 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.
        """
        # Setup Map Renderer and set all the layer
        renderer = QgsMapRenderer()
        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())

        # Set Layer set to renderer
        renderer.setLayerSet(layer_set)

        # Create composition
        composition = self.load_template(renderer)

        # 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
            map_extent = QgsRectangle(fit_min_x, fit_min_y, fit_max_x,
                                      fit_max_y)
            composer_map.setNewExtent(map_extent)

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

            # Self.html_reports must have only 1 key value pair
            area_title = list(self.html_reports.keys())[0]

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

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

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

            # Export composition to PDF file
            file_name = '_'.join(area_title.split())
            file_path = '%s.pdf' % file_name
            path = os.path.join(self.out_dir, file_path)
            composition.exportAsPDF(path)
        else:
            # Create atlas composition:
            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, renderer):
        """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 renderer: Map renderer
        :type renderer: QgsMapRenderer

        """
        # Create Composition
        composition = QgsComposition(renderer)

        template_file = QtCore.QFile(self.template_path)
        template_file.open(QtCore.QIODevice.ReadOnly | QtCore.QIODevice.Text)
        template_content = template_file.readAll()
        template_file.close()

        # Create a dom document containing template content
        document = QtXml.QDomDocument()
        document.setContent(template_content)

        # Prepare Map Substitution
        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
        }

        # Load template
        load_status = composition.loadFromTemplate(document, substitution_map)
        if not load_status:
            raise ReportCreationError(
                self.tr('Error loading template %s') % self.template_path)

        # Validate all needed composer components
        component_ids = [
            'impact-map', 'safe-logo', 'summary-report', 'aggregation-area',
            'map-scale', 'map-legend', 'organisation-logo',
            'merged-report-table'
        ]
        for component_id in component_ids:
            component = composition.getComposerItemById(component_id)
            if component is None:
                raise ReportCreationError(
                    self.tr('Component %s could not be found' % component_id))

        # Set InaSAFE logo
        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')
        legend.updateLegend()

        return composition
Пример #22
0
class KeywordsDialog(QtGui.QDialog, Ui_KeywordsDialogBase):
    """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

        if layer is None:
            self.layer = 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')),
             ('Not Set', self.tr('Not Set'))])

        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)

        # set some initial ui state:
        self.defaults = breakdown_defaults()
        self.pbnAdvanced.setChecked(False)
        self.radPredefined.setChecked(True)
        self.dsbFemaleRatioDefault.blockSignals(True)
        self.dsbFemaleRatioDefault.setValue(self.defaults['FEM_RATIO'])
        self.dsbFemaleRatioDefault.blockSignals(False)

        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.grpAdvanced.setVisible(False)
        self.resize_dialog()

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

    def toggle_postprocessing_widgets(self):
        """Hide or show the post processing widgets depending on context."""
        LOGGER.debug('togglePostprocessingWidgets')
        postprocessing_flag = self.radPostprocessing.isChecked()
        self.cboSubcategory.setVisible(not postprocessing_flag)
        self.lblSubcategory.setVisible(not postprocessing_flag)
        self.show_aggregation_attribute(postprocessing_flag)
        self.show_female_ratio_attribute(postprocessing_flag)
        self.show_female_ratio_default(postprocessing_flag)

    def show_aggregation_attribute(self, visible_flag):
        """Hide or show the aggregation attribute in the keyword editor dialog.

        :param visible_flag: Flag indicating if the aggregation attribute
            should be hidden or shown.
        :type visible_flag: bool
        """
        box = self.cboAggregationAttribute
        box.blockSignals(True)
        box.clear()
        box.blockSignals(False)
        if visible_flag:
            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)

        box.setVisible(visible_flag)
        self.lblAggregationAttribute.setVisible(visible_flag)

    def show_female_ratio_attribute(self, visible_flag):
        """Hide or show the female ratio attribute in the dialog.

        :param visible_flag: Flag indicating if the female ratio attribute
            should be hidden or shown.
        :type visible_flag: bool
        """
        box = self.cboFemaleRatioAttribute
        box.blockSignals(True)
        box.clear()
        box.blockSignals(False)
        if visible_flag:
            current_keyword = self.get_value_for_key(
                self.defaults['FEM_RATIO_ATTR_KEY'])
            fields, attribute_position = layer_attribute_names(
                self.layer,
                [QtCore.QVariant.Double],
                current_keyword)
            fields.insert(0, self.tr('Use default'))
            fields.insert(1, self.tr('Don\'t use'))
            box.addItems(fields)
            if current_keyword == self.tr('Use default'):
                box.setCurrentIndex(0)
            elif current_keyword == self.tr('Don\'t use'):
                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)
        box.setVisible(visible_flag)
        self.lblFemaleRatioAttribute.setVisible(visible_flag)

    def show_female_ratio_default(self, visible_flag):
        """Hide or show the female ratio default attribute in the dialog.

        :param visible_flag: Flag indicating if the female ratio
            default attribute should be hidden or shown.
        :type visible_flag: bool
        """
        box = self.dsbFemaleRatioDefault
        if visible_flag:
            current_value = self.get_value_for_key(
                self.defaults['FEM_RATIO_KEY'])
            if current_value is None:
                val = self.defaults['FEM_RATIO']
            else:
                val = float(current_value)
            box.setValue(val)

        box.setVisible(visible_flag)
        self.lblFemaleRatioDefault.setVisible(visible_flag)

    # 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
        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
        text = self.cboFemaleRatioAttribute.currentText()
        if text == self.tr('Use default'):
            self.dsbFemaleRatioDefault.setEnabled(True)
            current_default = self.get_value_for_key(
                self.defaults['FEM_RATIO_KEY'])
            if current_default is None:
                self.add_list_entry(
                    self.defaults['FEM_RATIO_KEY'],
                    self.dsbFemaleRatioDefault.value())
        else:
            self.dsbFemaleRatioDefault.setEnabled(False)
            self.remove_item_by_key(self.defaults['FEM_RATIO_KEY'])
        self.add_list_entry(self.defaults['FEM_RATIO_ATTR_KEY'], text)

    # 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
        box = self.dsbFemaleRatioDefault
        if box.isEnabled():
            self.add_list_entry(
                self.defaults['FEM_RATIO_KEY'],
                box.value())

    # prevents actions being handled twice
    # noinspection PyPep8Naming
    @pyqtSignature('bool')
    def on_pbnAdvanced_toggled(self, flag):
        """Automatic slot executed when the advanced button is toggled.

        .. note:: some of the behaviour for hiding widgets is done using
           the signal/slot editor in designer, so if you are trying to figure
           out how the interactions work, look there too!

        :param flag: Flag indicating the new checked state of the button.
        :type flag: bool
        """
        self.toggle_advanced(flag)

    def toggle_advanced(self, flag):
        """Hide or show advanced editor.

        :param flag: Desired state for advanced editor visibility.
        :type flag: bool
        """
        if flag:
            self.pbnAdvanced.setText(self.tr('Hide advanced editor'))
        else:
            self.pbnAdvanced.setText(self.tr('Show advanced editor'))
        self.grpAdvanced.setVisible(flag)
        self.resize_dialog()

    # 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 not flag:
            self.remove_item_by_key(self.defaults['AGGR_ATTR_KEY'])
            self.remove_item_by_key(self.defaults['FEM_RATIO_ATTR_KEY'])
            self.remove_item_by_key(self.defaults['FEM_RATIO_KEY'])
            return
        self.set_category('postprocessing')
        self.update_controls_from_list()

    # 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 myCounter in range(self.lstKeywords.count()):
            existing_item = self.lstKeywords.item(myCounter)
            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.lstKeywords.takeItem(myCounter)
                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.lstKeywords.takeItem(counter)
                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 slate 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')

        for key in keywords.iterkeys():
            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)

        # 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 myCounter in range(self.lstKeywords.count()):
            existing_item = self.lstKeywords.item(myCounter)
            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()
        try:
            self.keyword_io.write_keywords(
                layer=self.layer, keywords=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 keywords:\n'
                    '%s' % error_message.to_html()))))
        if self.dock is not None:
            self.dock.get_layers()
        self.done(QtGui.QDialog.Accepted)
Пример #23
0
class KeywordsDialog(QtGui.QDialog, Ui_KeywordsDialogBase):
    """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

        if layer is None:
            self.layer = 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')), ('Not Set', self.tr('Not Set'))
        ])

        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)

        # set some initial ui state:
        self.defaults = breakdown_defaults()
        self.pbnAdvanced.setChecked(False)
        self.radPredefined.setChecked(True)
        self.dsbFemaleRatioDefault.blockSignals(True)
        self.dsbFemaleRatioDefault.setValue(self.defaults['FEM_RATIO'])
        self.dsbFemaleRatioDefault.blockSignals(False)

        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.grpAdvanced.setVisible(False)
        self.resize_dialog()

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

    def toggle_postprocessing_widgets(self):
        """Hide or show the post processing widgets depending on context."""
        LOGGER.debug('togglePostprocessingWidgets')
        postprocessing_flag = self.radPostprocessing.isChecked()
        self.cboSubcategory.setVisible(not postprocessing_flag)
        self.lblSubcategory.setVisible(not postprocessing_flag)
        self.show_aggregation_attribute(postprocessing_flag)
        self.show_female_ratio_attribute(postprocessing_flag)
        self.show_female_ratio_default(postprocessing_flag)

    def show_aggregation_attribute(self, visible_flag):
        """Hide or show the aggregation attribute in the keyword editor dialog.

        :param visible_flag: Flag indicating if the aggregation attribute
            should be hidden or shown.
        :type visible_flag: bool
        """
        box = self.cboAggregationAttribute
        box.blockSignals(True)
        box.clear()
        box.blockSignals(False)
        if visible_flag:
            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)

        box.setVisible(visible_flag)
        self.lblAggregationAttribute.setVisible(visible_flag)

    def show_female_ratio_attribute(self, visible_flag):
        """Hide or show the female ratio attribute in the dialog.

        :param visible_flag: Flag indicating if the female ratio attribute
            should be hidden or shown.
        :type visible_flag: bool
        """
        box = self.cboFemaleRatioAttribute
        box.blockSignals(True)
        box.clear()
        box.blockSignals(False)
        if visible_flag:
            current_keyword = self.get_value_for_key(
                self.defaults['FEM_RATIO_ATTR_KEY'])
            fields, attribute_position = layer_attribute_names(
                self.layer, [QtCore.QVariant.Double], current_keyword)
            fields.insert(0, self.tr('Use default'))
            fields.insert(1, self.tr('Don\'t use'))
            box.addItems(fields)
            if current_keyword == self.tr('Use default'):
                box.setCurrentIndex(0)
            elif current_keyword == self.tr('Don\'t use'):
                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)
        box.setVisible(visible_flag)
        self.lblFemaleRatioAttribute.setVisible(visible_flag)

    def show_female_ratio_default(self, visible_flag):
        """Hide or show the female ratio default attribute in the dialog.

        :param visible_flag: Flag indicating if the female ratio
            default attribute should be hidden or shown.
        :type visible_flag: bool
        """
        box = self.dsbFemaleRatioDefault
        if visible_flag:
            current_value = self.get_value_for_key(
                self.defaults['FEM_RATIO_KEY'])
            if current_value is None:
                val = self.defaults['FEM_RATIO']
            else:
                val = float(current_value)
            box.setValue(val)

        box.setVisible(visible_flag)
        self.lblFemaleRatioDefault.setVisible(visible_flag)

    # 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
        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
        text = self.cboFemaleRatioAttribute.currentText()
        if text == self.tr('Use default'):
            self.dsbFemaleRatioDefault.setEnabled(True)
            current_default = self.get_value_for_key(
                self.defaults['FEM_RATIO_KEY'])
            if current_default is None:
                self.add_list_entry(self.defaults['FEM_RATIO_KEY'],
                                    self.dsbFemaleRatioDefault.value())
        else:
            self.dsbFemaleRatioDefault.setEnabled(False)
            self.remove_item_by_key(self.defaults['FEM_RATIO_KEY'])
        self.add_list_entry(self.defaults['FEM_RATIO_ATTR_KEY'], text)

    # 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
        box = self.dsbFemaleRatioDefault
        if box.isEnabled():
            self.add_list_entry(self.defaults['FEM_RATIO_KEY'], box.value())

    # prevents actions being handled twice
    # noinspection PyPep8Naming
    @pyqtSignature('bool')
    def on_pbnAdvanced_toggled(self, flag):
        """Automatic slot executed when the advanced button is toggled.

        .. note:: some of the behaviour for hiding widgets is done using
           the signal/slot editor in designer, so if you are trying to figure
           out how the interactions work, look there too!

        :param flag: Flag indicating the new checked state of the button.
        :type flag: bool
        """
        self.toggle_advanced(flag)

    def toggle_advanced(self, flag):
        """Hide or show advanced editor.

        :param flag: Desired state for advanced editor visibility.
        :type flag: bool
        """
        if flag:
            self.pbnAdvanced.setText(self.tr('Hide advanced editor'))
        else:
            self.pbnAdvanced.setText(self.tr('Show advanced editor'))
        self.grpAdvanced.setVisible(flag)
        self.resize_dialog()

    # 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 not flag:
            self.remove_item_by_key(self.defaults['AGGR_ATTR_KEY'])
            self.remove_item_by_key(self.defaults['FEM_RATIO_ATTR_KEY'])
            self.remove_item_by_key(self.defaults['FEM_RATIO_KEY'])
            return
        self.set_category('postprocessing')
        self.update_controls_from_list()

    # 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 myCounter in range(self.lstKeywords.count()):
            existing_item = self.lstKeywords.item(myCounter)
            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.lstKeywords.takeItem(myCounter)
                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.lstKeywords.takeItem(counter)
                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 slate 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')

        for key in keywords.iterkeys():
            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)

        # 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 myCounter in range(self.lstKeywords.count()):
            existing_item = self.lstKeywords.item(myCounter)
            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()
        try:
            self.keyword_io.write_keywords(layer=self.layer, keywords=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 keywords:\n'
                '%s' % error_message.to_html()))))
        if self.dock is not None:
            self.dock.get_layers()
        self.done(QtGui.QDialog.Accepted)
Пример #24
0
class PostprocessorManager(QtCore.QObject):
    """A manager for post processing of impact function results.
    """

    def __init__(
            self,
            theAggregator):
        """Director for aggregation based operations.
        Args:
          theAggregationLayer: QgsMapLayer representing clipped
              aggregation. This will be converted to a memory layer inside
              this class. see self.aggregator.layer
        Returns:
           not applicable
        Raises:
           no exceptions explicitly raised
        """

        super(PostprocessorManager, self).__init__()

        # Aggregation / post processing related items
        self.postProcessingOutput = {}
        self.keywordIO = KeywordIO()
        self.errorMessage = None

        self.aggregator = theAggregator

    def _sumFieldName(self):
        return self.aggregator.prefix + 'sum'

    def _sortNoData(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

        Args:
            list - data

        Returns:
            returns -1 if the value is NO_DATA else the value
        """

        myPostprocessor = self.postProcessingOutput[
            self._currentOutputPostprocessor]
        #get the key position of the value field
        myValueKey = myPostprocessor[0][1].keyAt(0)
        #get the value
        # data[1] is the orderedDict
        # data[1][myFirstKey] is the 1st indicator in the orderedDict
        if data[1][myValueKey]['value'] == self.aggregator.defaults['NO_DATA']:
            myPosition = -1
        else:
            myPosition = data[1][myValueKey]['value']
            myPosition = unhumanize_number(myPosition)

        return myPosition

    def _generateTables(self):
        """Parses the postprocessing output as one table per postprocessor.

        Args:
            None

        Returns:
            str - a string containing the html
        """
        myMessage = m.Message()

        for proc, results_list in self.postProcessingOutput.iteritems():

            self._currentOutputPostprocessor = proc
            # 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
            sortedResList = sorted(
                results_list,
                key=self._sortNoData,
                reverse=True)

            #init table
            hasNoDataValues = False
            myTable = m.Table(
                style_class='table table-condensed table-striped')
            myTable.caption = self.tr('Detailed %s report') % (safeTr(
                get_postprocessor_human_name(proc)).lower())

            myHeaderRow = m.Row()
            myHeaderRow.add(str(self.attributeTitle).capitalize())
            for calculationName in sortedResList[0][1]:
                myHeaderRow.add(self.tr(calculationName))
            myTable.add(myHeaderRow)

            for zoneName, calc in sortedResList:
                myRow = m.Row(zoneName)

                for _, calculationData in calc.iteritems():
                    myValue = calculationData['value']
                    if myValue == self.aggregator.defaults['NO_DATA']:
                        hasNoDataValues = True
                        myValue += ' *'
                    myRow.add(myValue)
                myTable.add(myRow)

            # add table to message
            myMessage.add(myTable)
            if hasNoDataValues:
                myMessage.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.defaults['NO_DATA'])))

        return myMessage

    def _consolidate_multipart_stats(self):
        """Sums the values of multipart polygons together to display only one.

        Args:
            None

        Returns:
            None

        Raises:
            None
        """
        LOGGER.debug('Consolidating multipart postprocessing results')

        # copy needed because of
        # self.postProcessingOutput[proc].pop(corrected_index)
        postProcessingOutput = self.postProcessingOutput

        # iterate postprocessors
        for proc, results_list in postProcessingOutput.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():
                    LOGGER.debug('%s postprocessor found multipart polygon '
                                 'with name %s' % (proc, polygon_name))
                    for result_name, result in results.iteritems():
                        first_part_index = checked_polygon_names[polygon_name]
                        first_part = self.postProcessingOutput[proc][
                            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',
                        # see http://irclogs.geoapt.com/inasafe/
                        # %23inasafe.2013-08-09.log (at 22.29)

                        no_data = self.aggregator.defaults['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 and result_value != no_data:
                                first_part_result['value'] = 0
                            # the other is No data
                            elif value != no_data and result_value == no_data:
                                result['value'] = 0
                            #if we got 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.postProcessingOutput[proc] = results_list

    def run(self):
        """Run any post processors requested by the impact function.

        Args:
            None

        Returns:
            None

        Raises:
            None
        """
        try:
            myRequestedPostProcessors = self.functionParams['postprocessors']
            myPostProcessors = get_postprocessors(myRequestedPostProcessors)
        except (TypeError, KeyError):
            # TypeError is for when functionParams is none
            # KeyError is for when ['postprocessors'] is unavailable
            myPostProcessors = {}
        LOGGER.debug('Running this postprocessors: ' + str(myPostProcessors))

        myFeatureNameAttribute = self.aggregator.attributes[
            self.aggregator.defaults['AGGR_ATTR_KEY']]
        if myFeatureNameAttribute is None:
            self.attributeTitle = self.tr('Aggregation unit')
        else:
            self.attributeTitle = myFeatureNameAttribute

        myNameFieldIndex = self.aggregator.layer.fieldNameIndex(
            self.attributeTitle)
        mySumFieldIndex = self.aggregator.layer.fieldNameIndex(
            self._sumFieldName())

        myFemaleRatioIsVariable = False
        myFemRatioFieldIndex = None
        myFemaleRatio = None

        if 'Gender' in myPostProcessors:
            # look if we need to look for a variable female ratio in a layer
            try:
                myFemRatioField = self.aggregator.attributes[
                    self.aggregator.defaults['FEM_RATIO_ATTR_KEY']]
                myFemRatioFieldIndex = self.aggregator.layer.fieldNameIndex(
                    myFemRatioField)

                # something went wrong finding the female ratio field,
                # use defaults from below except block
                if myFemRatioFieldIndex == -1:
                    raise KeyError

                myFemaleRatioIsVariable = True

            except KeyError:
                try:
                    myFemaleRatio = self.keywordIO.read_keywords(
                        self.aggregator.layer,
                        self.aggregator.defaults['FEM_RATIO_KEY'])
                except KeywordNotFoundError:
                    myFemaleRatio = self.aggregator.defaults['FEM_RATIO']

        # iterate zone features
        myRequest = QgsFeatureRequest()
        myRequest.setFlags(QgsFeatureRequest.NoGeometry)
        myProvider = self.aggregator.layer.dataProvider()
        # start data retrieval: fetch no geometry and all attributes for each
        # feature
        myPolygonIndex = 0
        for myFeature in myProvider.getFeatures(myRequest):
            # if a feature has no field called
            if myNameFieldIndex == -1:
                myZoneName = str(myFeature.id())
            else:
                myZoneName = myFeature[myNameFieldIndex]

            # create dictionary of attributes to pass to postprocessor
            myGeneralParams = {'target_field': self.aggregator.targetField,
                               'function_params': self.functionParams}

            if self.aggregator.statisticsType == 'class_count':
                myGeneralParams['impact_classes'] = (
                    self.aggregator.statisticsClasses)
            elif self.aggregator.statisticsType == 'sum':
                myImpactTotal = myFeature[mySumFieldIndex]
                myGeneralParams['impact_total'] = myImpactTotal

            try:
                myGeneralParams['impact_attrs'] = (
                    self.aggregator.impactLayerAttributes[myPolygonIndex])
            except IndexError:
                # rasters and attributeless vectors have no attributes
                myGeneralParams['impact_attrs'] = None

            for myKey, myValue in myPostProcessors.iteritems():
                myParameters = myGeneralParams
                try:
                    # look if params are available for this postprocessor
                    myParameters.update(
                        self.functionParams['postprocessors'][myKey]['params'])
                except KeyError:
                    pass

                if myKey == 'Gender':
                    if myFemaleRatioIsVariable:
                        myFemaleRatio = myFeature[myFemRatioFieldIndex]
                        if myFemaleRatio is None:
                            myFemaleRatio = self.aggregator.defaults[
                                'FEM_RATIO']
                        LOGGER.debug(myFemaleRatio)
                    myParameters['female_ratio'] = myFemaleRatio

                myValue.setup(myParameters)
                myValue.process()
                myResults = myValue.results()
                myValue.clear()
#                LOGGER.debug(myResults)
                try:
                    self.postProcessingOutput[myKey].append(
                        (myZoneName, myResults))
                except KeyError:
                    self.postProcessingOutput[myKey] = []
                    self.postProcessingOutput[myKey].append(
                        (myZoneName, myResults))
            # increment the index
            myPolygonIndex += 1

    def getOutput(self):
        """Returns the results of the post processing as a table.

        Args:
            theSingleTableFlag - bool indicating if result should be rendered
                as a single table. Default False.

        Returns: str - a string containing the html in the requested format.
        """
        if self.errorMessage is not None:
            myMessage = m.Message(
                m.Heading(self.tr('Postprocessing report skipped')),
                m.Paragraph(self.tr(
                    'Due to a problem while processing the results,'
                    ' the detailed postprocessing report is unavailable:'
                    ' %s') % self.errorMessage))
            return myMessage
        else:
            try:
                if (self.keywordIO.read_keywords(
                        self.aggregator.layer, 'had multipart polygon')):
                    self._consolidate_multipart_stats()
            except KeywordNotFoundError:
                pass

            return self._generateTables()
Пример #25
0
class MapLegend():
    """A class for creating a map legend."""
    def __init__(self,
                 layer,
                 dpi=300,
                 legend_title=None,
                 legend_notes=None,
                 legend_units=None):
        """Constructor for the Map Legend class.

        :param layer: Layer that the legend should be generated for.
        :type layer: QgsMapLayer, QgsVectorLayer

        :param dpi: DPI for the generated legend image. Defaults to 300 if
            not specified.
        :type dpi: int

        :param legend_title: Title for the legend.
        :type legend_title: str

        :param legend_notes: Notes to display under the title.
        :type legend_notes: str

        :param legend_units: Units for the legend.
        :type legend_units: str
        """
        LOGGER.debug('InaSAFE Map class initialised')
        self.legend_image = None
        self.layer = layer
        # how high each row of the legend should be
        self.legend_increment = 42
        self.keyword_io = KeywordIO()
        self.legend_font_size = 8
        self.legend_width = 900
        self.dpi = dpi
        if legend_title is None:
            self.legend_title = self.tr('Legend')
        else:
            self.legend_title = legend_title
        self.legend_notes = legend_notes
        self.legend_units = legend_units

    # noinspection PyMethodMayBeStatic
    def tr(self, string):
        """We implement this ourself since we do not inherit QObject.

        :param string: String for translation.
        :type string: QString, str

        :returns: Translated version of string.
        :rtype: QString
        """
        # noinspection PyCallByClass,PyTypeChecker
        return QtCore.QCoreApplication.translate('MapLegend', string)

    def get_legend(self):
        """Create a legend for the classes in the layer.

        .. note: This is a wrapper for raster_legend and vector_legend.

        :raises: InvalidLegendLayer will be raised if a legend cannot be
            created from the layer.
        """
        LOGGER.debug('InaSAFE Map Legend getLegend called')
        if self.layer is None:
            message = self.tr(
                'Unable to make a legend when map generator has no layer set.')
            raise LegendLayerError(message)
        try:
            self.keyword_io.read_keywords(self.layer, 'impact_summary')
        except KeywordNotFoundError, e:
            message = self.tr(
                'This layer does not appear to be an impact layer. Try '
                'selecting an impact layer in the QGIS layers list or '
                'creating a new impact scenario before using the print tool.'
                '\nMessage: %s' % str(e))

            raise Exception(message)
        if self.layer.type() == QgsMapLayer.VectorLayer:
            return self.vector_legend()
        else:
            return self.raster_legend()
Пример #26
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

    def _sum_field_name(self):
        return self.aggregator.prefix + 'sum'

    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].keyAt(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.defaults['NO_DATA']:
            position = -1
        else:
            position = data[1][key]['value']
            position = unhumanize_number(position)

        return position

    def _generate_tables(self):
        """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.

        :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') % (safeTr(
                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']
                    if value == self.aggregator.defaults['NO_DATA']:
                        has_no_data = True
                        value += ' *'
                        try:
                            postprocessor_totals[indicator] += 0
                        except KeyError:
                            postprocessor_totals[indicator] = 0
                    else:
                        try:
                            postprocessor_totals[indicator] += int(value)
                        except KeyError:
                            postprocessor_totals[indicator] = int(value)
                    row.add(value)
                table.add(row)

            # add the totals row
            row = m.Row(self.tr('Total in aggregation areas'))
            for _, total in postprocessor_totals.iteritems():
                row.add(str(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.defaults['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.defaults['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.functionParams['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 = {}
        LOGGER.debug('Running this postprocessors: ' + str(postprocessors))

        feature_names_attribute = self.aggregator.attributes[
            self.aggregator.defaults['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

        if 'Gender' in postprocessors:
            # look if we need to look for a variable female ratio in a layer
            try:
                female_ration_field = self.aggregator.attributes[
                    self.aggregator.defaults['FEM_RATIO_ATTR_KEY']]
                female_ratio_field_index = self.aggregator.layer.fieldNameIndex(
                    female_ration_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.defaults['FEM_RATIO_KEY'])
                except KeywordNotFoundError:
                    female_ratio = self.aggregator.defaults['FEM_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]

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

            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.functionParams['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[
                                'FEM_RATIO']
                        LOGGER.debug(female_ratio)
                    parameters['female_ratio'] = female_ratio

                value.setup(parameters)
                value.process()
                results = value.results()
                value.clear()
#                LOGGER.debug(results)
                try:
                    self.output[key].append(
                        (zone_name, results))
                except KeyError:
                    self.output[key] = []
                    self.output[key].append(
                        (zone_name, results))
            # increment the index
            polygon_index += 1

    def get_output(self):
        """Returns the results of the post processing as a table.

        :returns: str - a string containing the html in the requested format.
        """
        if self.error_message is not None:
            message = m.Message(
                m.Heading(self.tr('Postprocessing report skipped')),
                m.Paragraph(self.tr(
                    'Due to a problem while processing the results,'
                    ' the detailed postprocessing report is unavailable:'
                    ' %s') % self.error_message))
            return message
        else:
            try:
                if (self.keyword_io.read_keywords(
                        self.aggregator.layer, 'had multipart polygon')):
                    self._consolidate_multipart_stats()
            except KeywordNotFoundError:
                pass

            return self._generate_tables()
Пример #27
0
class AggregatorTest(unittest.TestCase):
    """Test the InaSAFE GUI"""

    def setUp(self):
        """Fixture run before all tests"""

        self.maxDiff = None  # show full diff for assert errors

        os.environ['LANG'] = 'en'
        DOCK.showOnlyVisibleLayersFlag = True
        load_standard_layers()
        DOCK.cboHazard.setCurrentIndex(0)
        DOCK.cboExposure.setCurrentIndex(0)
        DOCK.cboFunction.setCurrentIndex(0)
        DOCK.runInThreadFlag = False
        DOCK.showOnlyVisibleLayersFlag = False
        DOCK.setLayerNameFromTitleFlag = False
        DOCK.zoomToImpactFlag = False
        DOCK.hideExposureFlag = False
        DOCK.showIntermediateLayers = False
        set_jakarta_extent()

        self.keywordIO = KeywordIO()
        self.defaults = breakdown_defaults()

    def test_cboAggregationLoadedProject(self):
        """Aggregation combo changes properly according loaded layers"""
        myLayerList = [DOCK.tr('Entire area'),
                       DOCK.tr('kabupaten jakarta singlepart')]
        currentLayers = [DOCK.cboAggregation.itemText(i) for i in range(
            DOCK.cboAggregation.count())]

        myMessage = ('The aggregation combobox should have:\n %s \nFound: %s'
                     % (myLayerList, currentLayers))
        self.assertEquals(currentLayers, myLayerList, myMessage)

    def test_checkAggregationAttributeInKW(self):
        """Aggregation attribute is chosen correctly when present in keywords.
        """
        myAttrKey = breakdown_defaults('AGGR_ATTR_KEY')

        # with KAB_NAME aggregation attribute defined in .keyword using
        # kabupaten_jakarta_singlepart.shp
        myResult, myMessage = setup_scenario(
            DOCK,
            hazard='A flood in Jakarta like in 2007',
            exposure='People',
            function='Need evacuation',
            function_id='Flood Evacuation Function',
            aggregation_layer='kabupaten jakarta singlepart',
            aggregation_enabled_flag=True)
        assert myResult, myMessage
        # Press RUN
        DOCK.accept()
        DOCK.runtimeKeywordsDialog.accept()
        myAttribute = DOCK.aggregator.attributes[myAttrKey]
        myMessage = ('The aggregation should be KAB_NAME. Found: %s' %
                     myAttribute)
        self.assertEqual(myAttribute, 'KAB_NAME', myMessage)

    def test_checkAggregationAttribute1Attr(self):
        """Aggregation attribute is chosen correctly when there is only
        one attr available."""
        myFileList = ['kabupaten_jakarta_singlepart_1_good_attr.shp']
        #add additional layers
        load_layers(myFileList, clear_flag=False, data_directory=TESTDATA)
        myAttrKey = breakdown_defaults('AGGR_ATTR_KEY')

        # with 1 good aggregation attribute using
        # kabupaten_jakarta_singlepart_1_good_attr.shp
        myResult, myMessage = setup_scenario(
            DOCK,
            hazard='A flood in Jakarta like in 2007',
            exposure='People',
            function='Need evacuation',
            function_id='Flood Evacuation Function',
            aggregation_layer='kabupaten jakarta singlepart 1 good attr')
        assert myResult, myMessage
        # Press RUN
        # noinspection PyCallByClass,PyTypeChecker
        DOCK.accept()
        DOCK.runtimeKeywordsDialog.accept()
        print myAttrKey
        print DOCK.aggregator.attributes
        myAttribute = DOCK.aggregator.attributes[myAttrKey]
        myMessage = ('The aggregation should be KAB_NAME. Found: %s' %
                     myAttribute)
        self.assertEqual(myAttribute, 'KAB_NAME', myMessage)

    def test_checkAggregationAttributeNoAttr(self):
        """Aggregation attribute chosen correctly when no attr available."""

        myFileList = ['kabupaten_jakarta_singlepart_0_good_attr.shp']
        #add additional layers
        load_layers(myFileList, clear_flag=False, data_directory=TESTDATA)
        myAttrKey = breakdown_defaults('AGGR_ATTR_KEY')
        # with no good aggregation attribute using
        # kabupaten_jakarta_singlepart_0_good_attr.shp
        myResult, myMessage = setup_scenario(
            DOCK,
            hazard='A flood in Jakarta like in 2007',
            exposure='People',
            function='Need evacuation',
            function_id='Flood Evacuation Function',
            aggregation_layer='kabupaten jakarta singlepart 0 good attr')
        assert myResult, myMessage
        # Press RUN
        DOCK.accept()
        DOCK.runtimeKeywordsDialog.accept()
        myAttribute = DOCK.aggregator.attributes[myAttrKey]
        myMessage = ('The aggregation should be None. Found: %s' %
                     myAttribute)
        assert myAttribute is None, myMessage

    def test_checkAggregationAttributeNoneAttr(self):
        """Aggregation attribute is chosen correctly when None in keywords."""

        myFileList = ['kabupaten_jakarta_singlepart_with_None_keyword.shp']
        #add additional layers
        load_layers(myFileList, clear_flag=False, data_directory=TESTDATA)
        myAttrKey = breakdown_defaults('AGGR_ATTR_KEY')
        # with None aggregation attribute defined in .keyword using
        # kabupaten_jakarta_singlepart_with_None_keyword.shp
        myResult, myMessage = setup_scenario(
            DOCK,
            hazard='A flood in Jakarta like in 2007',
            exposure='People',
            function='Need evacuation',
            function_id='Flood Evacuation Function',
            aggregation_layer='kabupaten jakarta singlepart with None keyword')
        assert myResult, myMessage
        # Press RUN
        DOCK.accept()
        DOCK.runtimeKeywordsDialog.accept()
        myAttribute = DOCK.aggregator.attributes[myAttrKey]
        myMessage = ('The aggregation should be None. Found: %s' % myAttribute)
        assert myAttribute is None, myMessage

    def test_preprocessing(self):
        """Preprocessing results are correct.

        TODO - this needs to be fixed post dock refactor.

        """

        # See qgis project in test data: vector_preprocessing_test.qgs
        #add additional layers
        myFileList = ['jakarta_crosskabupaten_polygons.shp']
        load_layers(myFileList, clear_flag=False, data_directory=TESTDATA)
        myFileList = ['kabupaten_jakarta.shp']
        load_layers(myFileList, clear_flag=False, data_directory=BOUNDDATA)

        myResult, myMessage = setup_scenario(
            DOCK,
            hazard='jakarta_crosskabupaten_polygons',
            exposure='People',
            function='Need evacuation',
            function_id='Flood Evacuation Function Vector Hazard',
            aggregation_layer='kabupaten jakarta',
            aggregation_enabled_flag=True)
        assert myResult, myMessage

        # Enable on-the-fly reprojection
        set_canvas_crs(GEOCRS, True)
        set_jakarta_extent()
        # Press RUN
        DOCK.accept()
        DOCK.runtimeKeywordsDialog.accept()

        myExpectedFeatureCount = 20
        myMessage = ('The preprocessing should have generated %s features, '
                     'found %s' % (myExpectedFeatureCount,
                                   DOCK.aggregator.preprocessedFeatureCount))
        self.assertEqual(myExpectedFeatureCount,
                         DOCK.aggregator.preprocessedFeatureCount,
                         myMessage)

    def _aggregate(self,
                   myImpactLayer,
                   myExpectedResults,
                   useNativeZonalStats=False):
        """Helper to calculate aggregation.

        Expected results is split into two lists - one list contains numeric
        attributes, the other strings. This is done so that we can use numpy
        .testing.assert_allclose which doesn't work on strings
        """

        myExpectedStringResults = []
        myExpectedNumericResults = []

        for item in myExpectedResults:
            myItemNumResults = []
            myItemStrResults = []
            for field in item:
                try:
                    value = float(field)
                    myItemNumResults.append(value)
                except ValueError:
                    myItemStrResults.append(str(field))

            myExpectedNumericResults.append(myItemNumResults)
            myExpectedStringResults.append(myItemStrResults)

        myAggregationLayer = QgsVectorLayer(
            os.path.join(BOUNDDATA, 'kabupaten_jakarta.shp'),
            'test aggregation',
            'ogr')
        # create a copy of aggregation layer
        myGeoExtent = extent_to_geo_array(
            myAggregationLayer.extent(),
            myAggregationLayer.crs())

        myAggrAttribute = self.keywordIO.read_keywords(
            myAggregationLayer, self.defaults['AGGR_ATTR_KEY'])
        # noinspection PyArgumentEqualDefault
        myAggregationLayer = clip_layer(
            layer=myAggregationLayer,
            extent=myGeoExtent,
            explode_flag=True,
            explode_attribute=myAggrAttribute)

        myAggregator = Aggregator(None, myAggregationLayer)
        # setting up
        myAggregator.isValid = True
        myAggregator.layer = myAggregationLayer
        myAggregator.safeLayer = safe_read_layer(
            str(myAggregator.layer.source()))
        myAggregator.aoiMode = False
        myAggregator.useNativeZonalStats = useNativeZonalStats
        myAggregator.aggregate(myImpactLayer)

        myProvider = myAggregator.layer.dataProvider()
        myNumericResults = []
        myStringResults = []

        for myFeature in myProvider.getFeatures():
            myFeatureNumResults = []
            myFeatureStrResults = []
            myAttrs = myFeature.attributes()
            for attr in myAttrs:
                if isinstance(attr, (int, float)):
                    myFeatureNumResults.append(attr)
                else:
                    myFeatureStrResults.append(attr)

            myNumericResults.append(myFeatureNumResults)
            myStringResults.append(myFeatureStrResults)

        # check string attributes
        self.assertEqual(myExpectedStringResults, myStringResults)
        # check numeric attributes with a 0.01% tolerance compared to the
        # native QGIS stats
        numpy.testing.assert_allclose(myExpectedNumericResults,
                                      myNumericResults,
                                      rtol=0.01)

    def test_aggregate_raster_impact_python(self):
        """Check aggregation on raster impact using python zonal stats"""
        self._aggregate_raster_impact()

    def test_aggregate_raster_impact_native(self):
        """Check aggregation on raster impact using native qgis zonal stats.

        TODO: this failes on Tims machine but not on MB or Jenkins.

        """
        self._aggregate_raster_impact(useNativeZonalStats=True)

    def _aggregate_raster_impact(self, useNativeZonalStats=False):
        """Check aggregation on raster impact.

        Created from loadStandardLayers.qgs with:
        - a flood in Jakarta like in 2007
        - Penduduk Jakarta
        - need evacuation
        - kabupaten_jakarta_singlepart.shp

        """
        myImpactLayer = Raster(
            data=os.path.join(TESTDATA, 'aggregation_test_impact_raster.tif'),
            name='test raster impact')

        myExpectedResults = [
            ['JAKARTA BARAT',
             '50540',
             '12015061.8769531',
             '237.733713433976'],
            ['JAKARTA PUSAT',
             '19492',
             '2943702.11401367',
             '151.021040119725'],
            ['JAKARTA SELATAN',
             '57367',
             '1645498.26947021',
             '28.6837078716024'],
            ['JAKARTA UTARA',
             '55004',
             '11332095.7334595',
             '206.023120745027'],
            ['JAKARTA TIMUR',
             '73949',
             '10943934.3182373',
             '147.992999475819']]

        self._aggregate(myImpactLayer, myExpectedResults, useNativeZonalStats)

    def test_aggregate_vector_impact(self):
        """Test aggregation results on a vector layer.
        created from loadStandardLayers.qgs with:
        - a flood in Jakarta like in 2007
        - Essential buildings
        - be flodded
        - kabupaten_jakarta_singlepart.shp
        """
        myImpactLayer = Vector(
            data=os.path.join(TESTDATA, 'aggregation_test_impact_vector.shp'),
            name='test vector impact')

        myExpectedResults = [
            ['JAKARTA BARAT', '87'],
            ['JAKARTA PUSAT', '117'],
            ['JAKARTA SELATAN', '22'],
            ['JAKARTA UTARA', '286'],
            ['JAKARTA TIMUR', '198']
        ]
        self._aggregate(myImpactLayer, myExpectedResults)

        myImpactLayer = Vector(
            data=TESTDATA + '/aggregation_test_impact_vector_small.shp',
            name='test vector impact')

        myExpectedResults = [
            ['JAKARTA BARAT', '2'],
            ['JAKARTA PUSAT', '0'],
            ['JAKARTA SELATAN', '0'],
            ['JAKARTA UTARA', '1'],
            ['JAKARTA TIMUR', '0']
        ]

        self._aggregate(myImpactLayer, myExpectedResults)
Пример #28
0
class AggregatorTest(unittest.TestCase):
    """Test the InaSAFE GUI"""

    #noinspection PyPep8Naming
    def setUp(self):
        """Fixture run before all tests"""

        self.maxDiff = None  # show full diff for assert errors

        os.environ['LANG'] = 'en'
        DOCK.show_only_visible_layers_flag = True
        load_standard_layers()
        DOCK.cboHazard.setCurrentIndex(0)
        DOCK.cboExposure.setCurrentIndex(0)
        DOCK.cboFunction.setCurrentIndex(0)
        DOCK.run_in_thread_flag = False
        DOCK.show_only_visible_layers_flag = False
        DOCK.set_layer_from_title_flag = False
        DOCK.zoom_to_impact_flag = False
        DOCK.hide_exposure_flag = False
        DOCK.show_intermediate_layers = False
        set_jakarta_extent()

        self.keywordIO = KeywordIO()
        self.defaults = breakdown_defaults()

    def test_combo_aggregation_loaded_project(self):
        """Aggregation combo changes properly according loaded layers"""
        layer_list = [
            DOCK.tr('Entire area'),
            DOCK.tr('kabupaten jakarta singlepart')
        ]
        current_layers = [
            DOCK.cboAggregation.itemText(i)
            for i in range(DOCK.cboAggregation.count())
        ]

        message = ('The aggregation combobox should have:\n %s \nFound: %s' %
                   (layer_list, current_layers))
        self.assertEquals(current_layers, layer_list, message)

    def test_aggregation_attribute_in_keywords(self):
        """Aggregation attribute is chosen correctly when present in keywords.
        """
        attribute_key = breakdown_defaults('AGGR_ATTR_KEY')

        # with KAB_NAME aggregation attribute defined in .keyword using
        # kabupaten_jakarta_singlepart.shp
        result, message = setup_scenario(
            DOCK,
            hazard='A flood in Jakarta like in 2007',
            exposure='People',
            function='Need evacuation',
            function_id='Flood Evacuation Function',
            aggregation_layer='kabupaten jakarta singlepart',
            aggregation_enabled_flag=True)
        assert result, message
        # Press RUN
        DOCK.accept()
        DOCK.runtime_keywords_dialog.accept()
        attribute = DOCK.aggregator.attributes[attribute_key]
        message = ('The aggregation should be KAB_NAME. Found: %s' % attribute)
        self.assertEqual(attribute, 'KAB_NAME', message)

    def test_check_aggregation_single_attribute(self):
        """Aggregation attribute is chosen correctly when there is only
        one attr available."""
        file_list = ['kabupaten_jakarta_singlepart_1_good_attr.shp']
        #add additional layers
        load_layers(file_list, clear_flag=False)
        attribute_key = breakdown_defaults('AGGR_ATTR_KEY')

        # with 1 good aggregation attribute using
        # kabupaten_jakarta_singlepart_1_good_attr.shp
        result, message = setup_scenario(
            DOCK,
            hazard='A flood in Jakarta like in 2007',
            exposure='People',
            function='Need evacuation',
            function_id='Flood Evacuation Function',
            aggregation_layer='kabupaten jakarta singlepart 1 good attr')
        assert result, message
        # Press RUN
        # noinspection PyCallByClass,PyTypeChecker
        DOCK.accept()
        DOCK.runtime_keywords_dialog.accept()
        print attribute_key
        print DOCK.aggregator.attributes
        attribute = DOCK.aggregator.attributes[attribute_key]
        message = ('The aggregation should be KAB_NAME. Found: %s' % attribute)
        self.assertEqual(attribute, 'KAB_NAME', message)

    #noinspection PyMethodMayBeStatic
    def test_check_aggregation_no_attributes(self):
        """Aggregation attribute chosen correctly when no attr available."""

        file_list = ['kabupaten_jakarta_singlepart_0_good_attr.shp']
        #add additional layers
        load_layers(file_list, clear_flag=False)
        attribute_key = breakdown_defaults('AGGR_ATTR_KEY')
        # with no good aggregation attribute using
        # kabupaten_jakarta_singlepart_0_good_attr.shp
        result, message = setup_scenario(
            DOCK,
            hazard='A flood in Jakarta like in 2007',
            exposure='People',
            function='Need evacuation',
            function_id='Flood Evacuation Function',
            aggregation_layer='kabupaten jakarta singlepart 0 good attr')
        assert result, message
        # Press RUN
        DOCK.accept()
        DOCK.runtime_keywords_dialog.accept()
        attribute = DOCK.aggregator.attributes[attribute_key]
        message = ('The aggregation should be None. Found: %s' % attribute)
        assert attribute is None, message

    #noinspection PyMethodMayBeStatic
    def test_check_aggregation_none_in_keywords(self):
        """Aggregation attribute is chosen correctly when None in keywords."""

        file_list = ['kabupaten_jakarta_singlepart_with_None_keyword.shp']
        #add additional layers
        load_layers(file_list, clear_flag=False)
        attribute_key = breakdown_defaults('AGGR_ATTR_KEY')
        # with None aggregation attribute defined in .keyword using
        # kabupaten_jakarta_singlepart_with_None_keyword.shp
        result, message = setup_scenario(
            DOCK,
            hazard='A flood in Jakarta like in 2007',
            exposure='People',
            function='Need evacuation',
            function_id='Flood Evacuation Function',
            aggregation_layer='kabupaten jakarta singlepart with None keyword')
        assert result, message
        # Press RUN
        DOCK.accept()
        DOCK.runtime_keywords_dialog.accept()
        attribute = DOCK.aggregator.attributes[attribute_key]
        message = ('The aggregation should be None. Found: %s' % attribute)
        assert attribute is None, message

    def test_preprocessing(self):
        """Preprocessing results are correct.

        TODO - this needs to be fixed post dock refactor.

        """

        # See qgis project in test data: vector_preprocessing_test.qgs
        #add additional layers
        file_list = ['jakarta_crosskabupaten_polygons.shp']
        load_layers(file_list, clear_flag=False)
        file_list = ['kabupaten_jakarta.shp']
        load_layers(file_list, clear_flag=False, data_directory=BOUNDDATA)

        result, message = setup_scenario(
            DOCK,
            hazard='jakarta_crosskabupaten_polygons',
            exposure='People',
            function='Need evacuation',
            function_id='Flood Evacuation Function Vector Hazard',
            aggregation_layer='kabupaten jakarta',
            aggregation_enabled_flag=True)
        assert result, message

        # Enable on-the-fly reprojection
        set_canvas_crs(GEOCRS, True)
        set_jakarta_extent()
        # Press RUN
        DOCK.accept()
        DOCK.runtime_keywords_dialog.accept()

        expected_feature_count = 20
        message = ('The preprocessing should have generated %s features, '
                   'found %s' % (expected_feature_count,
                                 DOCK.aggregator.preprocessed_feature_count))
        self.assertEqual(expected_feature_count,
                         DOCK.aggregator.preprocessed_feature_count, message)

    def _aggregate(self,
                   impact_layer,
                   expected_results,
                   use_native_zonal_stats=False):
        """Helper to calculate aggregation.

        Expected results is split into two lists - one list contains numeric
        attributes, the other strings. This is done so that we can use numpy
        .testing.assert_allclose which doesn't work on strings
        """

        expected_string_results = []
        expected_numeric_results = []

        for item in expected_results:
            string_results = []
            numeric_results = []
            for field in item:
                try:
                    value = float(field)
                    numeric_results.append(value)
                except ValueError:
                    string_results.append(str(field))

            expected_numeric_results.append(numeric_results)
            expected_string_results.append(string_results)

        aggregation_layer = QgsVectorLayer(
            os.path.join(BOUNDDATA, 'kabupaten_jakarta.shp'),
            'test aggregation', 'ogr')
        # create a copy of aggregation layer
        geo_extent = extent_to_geo_array(aggregation_layer.extent(),
                                         aggregation_layer.crs())

        aggregation_attribute = self.keywordIO.read_keywords(
            aggregation_layer, self.defaults['AGGR_ATTR_KEY'])
        # noinspection PyArgumentEqualDefault
        aggregation_layer = clip_layer(layer=aggregation_layer,
                                       extent=geo_extent,
                                       explode_flag=True,
                                       explode_attribute=aggregation_attribute)

        aggregator = Aggregator(None, aggregation_layer)
        # setting up
        aggregator.is_valid = True
        aggregator.layer = aggregation_layer
        aggregator.safe_layer = safe_read_layer(str(aggregator.layer.source()))
        aggregator.aoi_mode = False
        aggregator.use_native_zonal_stats = use_native_zonal_stats
        aggregator.aggregate(impact_layer)

        provider = aggregator.layer.dataProvider()
        string_results = []
        numeric_results = []

        for feature in provider.getFeatures():
            feature_string_results = []
            feature_numeric_results = []
            attributes = feature.attributes()
            for attr in attributes:
                if isinstance(attr, (int, float)):
                    feature_numeric_results.append(attr)
                else:
                    feature_string_results.append(attr)

            numeric_results.append(feature_numeric_results)
            string_results.append(feature_string_results)

        # check string attributes
        self.assertEqual(expected_string_results, string_results)
        # check numeric attributes with a 0.01% tolerance compared to the
        # native QGIS stats
        numpy.testing.assert_allclose(expected_numeric_results,
                                      numeric_results,
                                      rtol=0.01)

    def test_aggregate_raster_impact_python(self):
        """Check aggregation on raster impact using python zonal stats"""
        self._aggregate_raster_impact()

    def test_aggregate_raster_impact_native(self):
        """Check aggregation on raster impact using native qgis zonal stats.

        TODO: this fails on Tim's machine but not on MB or Jenkins.

        """
        self._aggregate_raster_impact(use_native_zonal_stats=True)

    def _aggregate_raster_impact(self, use_native_zonal_stats=False):
        """Check aggregation on raster impact.

        :param use_native_zonal_stats:

        Created from loadStandardLayers.qgs with:
        - a flood in Jakarta like in 2007
        - Penduduk Jakarta
        - need evacuation
        - kabupaten_jakarta_singlepart.shp

        """
        impact_layer = Raster(data=os.path.join(
            TESTDATA, 'aggregation_test_impact_raster.tif'),
                              name='test raster impact')

        expected_results = [
            ['JAKARTA BARAT', '50540', '12015061.8769531', '237.733713433976'],
            ['JAKARTA PUSAT', '19492', '2943702.11401367', '151.021040119725'],
            [
                'JAKARTA SELATAN', '57367', '1645498.26947021',
                '28.6837078716024'
            ],
            ['JAKARTA UTARA', '55004', '11332095.7334595', '206.023120745027'],
            ['JAKARTA TIMUR', '73949', '10943934.3182373', '147.992999475819']
        ]

        self._aggregate(impact_layer, expected_results, use_native_zonal_stats)

    def test_aggregate_vector_impact(self):
        """Test aggregation results on a vector layer.
        created from loadStandardLayers.qgs with:
        - a flood in Jakarta like in 2007
        - Essential buildings
        - be flooded
        - kabupaten_jakarta_singlepart.shp
        """
        impact_layer = Vector(data=os.path.join(
            TESTDATA, 'aggregation_test_impact_vector.shp'),
                              name='test vector impact')

        expected_results = [['JAKARTA BARAT', '87'], ['JAKARTA PUSAT', '117'],
                            ['JAKARTA SELATAN', '22'],
                            ['JAKARTA UTARA', '286'], ['JAKARTA TIMUR', '198']]
        self._aggregate(impact_layer, expected_results)

        impact_layer = Vector(data=TESTDATA +
                              '/aggregation_test_impact_vector_small.shp',
                              name='test vector impact')

        expected_results = [['JAKARTA BARAT', '2'], ['JAKARTA PUSAT', '0'],
                            ['JAKARTA SELATAN', '0'], ['JAKARTA UTARA', '1'],
                            ['JAKARTA TIMUR', '0']]

        self._aggregate(impact_layer, expected_results)
Пример #29
0
class Map():
    """A class for creating a map."""
    def __init__(self, iface):
        """Constructor for the Map class.

        :param iface: Reference to the QGIS iface object.
        :type iface: QgsAppInterface
        """
        LOGGER.debug('InaSAFE Map class initialised')
        self.iface = iface
        self.layer = iface.activeLayer()
        self.keyword_io = KeywordIO()
        self.printer = None
        self.composition = None
        self.extent = iface.mapCanvas().extent()
        self.safe_logo = ':/plugins/inasafe/inasafe-logo-url.svg'
        self.north_arrow = ':/plugins/inasafe/simple_north_arrow.png'
        self.org_logo = ':/plugins/inasafe/supporters.png'
        self.template = ':/plugins/inasafe/inasafe-portrait-a4.qpt'
        self.disclaimer = disclaimer()
        self.page_width = 0  # width in mm
        self.page_height = 0  # height in mm
        self.page_dpi = 300.0
        self.show_frames = False  # intended for debugging use only

    @staticmethod
    def tr(string):
        """We implement this since we do not inherit QObject.

        :param string: String for translation.
        :type string: QString, str

        :returns: Translated version of theString.
        :rtype: QString
        """
        # noinspection PyCallByClass,PyTypeChecker,PyArgumentList
        return QtCore.QCoreApplication.translate('Map', string)

    def set_impact_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

    def set_north_arrow_image(self, logo_path):
        """Set image that will be used as organisation logo in reports.

        :param logo_path: Path to image file
        :type logo_path: str
        """
        self.north_arrow = logo_path

    def set_organisation_logo(self, logo):
        """Set image that will be used as organisation logo in reports.

        :param logo: Path to image file
        :type logo: str
        """
        self.org_logo = logo

    def set_disclaimer(self, text):
        """Set text that will be used as disclaimer in reports.

        :param text: Disclaimer text
        :type text: str
        """
        self.disclaimer = text

    def set_template(self, template):
        """Set template that will be used for report generation.

        :param template: Path to composer template
        :type template: str
        """
        self.template = template

    def set_extent(self, extent):
        """Set extent or the report map

        :param extent: Extent of the report map
        :type extent: QgsRectangle

        """
        self.extent = extent

    def setup_composition(self):
        """Set up the composition ready for drawing elements onto it."""
        LOGGER.debug('InaSAFE Map setupComposition called')
        canvas = self.iface.mapCanvas()
        renderer = canvas.mapRenderer()
        self.composition = QgsComposition(renderer)
        self.composition.setPlotStyle(QgsComposition.Preview)  # or preview
        self.composition.setPrintResolution(self.page_dpi)
        self.composition.setPrintAsRaster(True)

    def make_pdf(self, filename):
        """Generate the printout for our final map.

        :param filename: Path on the file system to which the pdf should be
            saved. If None, a generated file name will be used.
        :type filename: str

        :returns: File name of the output file (equivalent to filename if
                provided).
        :rtype: str
        """
        LOGGER.debug('InaSAFE Map printToPdf called')
        if filename is None:
            map_pdf_path = unique_filename(
                prefix='report', suffix='.pdf', dir=temp_dir())
        else:
            # We need to cast to python string in case we receive a QString
            map_pdf_path = str(filename)

        self.load_template()
        self.composition.exportAsPDF(map_pdf_path)
        return map_pdf_path

    def map_title(self):
        """Get the map title from the layer keywords if possible.

        :returns: None on error, otherwise the title.
        :rtype: None, str
        """
        LOGGER.debug('InaSAFE Map getMapTitle called')
        try:
            title = self.keyword_io.read_keywords(self.layer, 'map_title')
            return title
        except KeywordNotFoundError:
            return None
        except Exception:
            return None

    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 myLegendAttribute in legend_attribute_list:
            # noinspection PyBroadException
            try:
                legend_attribute_dict[myLegendAttribute] = \
                    self.keyword_io.read_keywords(
                        self.layer, myLegendAttribute)
            except KeywordNotFoundError:
                pass
            except Exception:
                pass
        return legend_attribute_dict

    def load_template(self):
        """Load a QgsComposer map from a template.
        """
        self.setup_composition()

        template_file = QtCore.QFile(self.template)
        template_file.open(QtCore.QIODevice.ReadOnly | QtCore.QIODevice.Text)
        template_content = template_file.readAll()
        template_file.close()

        document = QtXml.QDomDocument()
        document.setContent(template_content)

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

        title = self.map_title()
        if not title:
            title = ''

        substitution_map = {
            'impact-title': title,
            'date': date,
            'time': time,
            'safe-version': version,
            'disclaimer': self.disclaimer
        }
        LOGGER.debug(substitution_map)
        load_ok = self.composition.loadFromTemplate(
            document, substitution_map)
        if not load_ok:
            raise ReportCreationError(
                self.tr('Error loading template %s') %
                self.template)

        self.page_width = self.composition.paperWidth()
        self.page_height = self.composition.paperHeight()

        # set InaSAFE logo
        image = self.composition.getComposerItemById('safe-logo')
        if image is not None:
            image.setPictureFile(self.safe_logo)
        else:
            raise ReportCreationError(self.tr(
                'Image "safe-logo" could not be found'))

        # set north arrow
        image = self.composition.getComposerItemById('north-arrow')
        if image is not None:
            image.setPictureFile(self.north_arrow)
        else:
            raise ReportCreationError(self.tr(
                'Image "north arrow" could not be found'))

        # set organisation logo
        image = self.composition.getComposerItemById('organisation-logo')
        if image is not None:
            image.setPictureFile(self.org_logo)
        else:
            raise ReportCreationError(self.tr(
                'Image "organisation-logo" could not be found'))

        # 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)
        else:
            LOGGER.debug('"impact-report" element not found.')

        # 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
            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)
        else:
            raise ReportCreationError(self.tr(
                'Map "impact-map" could not be found'))

        legend = self.composition.getComposerItemById('impact-legend')
        legend_attributes = self.map_legend_attributes()
        LOGGER.debug(legend_attributes)
        #legend_notes = mapLegendAttributes.get('legend_notes', None)
        #legend_units = mapLegendAttributes.get('legend_units', None)
        legend_title = legend_attributes.get('legend_title', None)

        symbol_count = 1
        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)
        legend.updateLegend()

        # remove from legend all layers, except impact one
        model = legend.model()
        if model.rowCount() > 0 and model.columnCount() > 0:
            impact_item = model.findItems(self.layer.name())[0]
            row = impact_item.index().row()
            model.removeRows(row + 1, model.rowCount() - row)
            if row > 0:
                model.removeRows(0, row)
Пример #30
0
class PostprocessorManager(QtCore.QObject):
    """A manager for post processing of impact function results.
    """
    def __init__(self, theAggregator):
        """Director for aggregation based operations.
        Args:
          theAggregationLayer: QgsMapLayer representing clipped
              aggregation. This will be converted to a memory layer inside
              this class. see self.aggregator.layer
        Returns:
           not applicable
        Raises:
           no exceptions explicitly raised
        """

        super(PostprocessorManager, self).__init__()

        # Aggregation / post processing related items
        self.postProcessingOutput = {}
        self.keywordIO = KeywordIO()
        self.errorMessage = None

        self.aggregator = theAggregator

    def _sumFieldName(self):
        return self.aggregator.prefix + 'sum'

    def _sortNoData(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

        Args:
            list - data

        Returns:
            returns -1 if the value is NO_DATA else the value
        """
        #black magic to get the value of each postprocessor field
        #get the first postprocessor just to discover the data structure
        myFirsPostprocessor = self.postProcessingOutput.itervalues().next()
        #get the key position of the value field
        myValueKey = myFirsPostprocessor[0][1].keyAt(0)

        #get the value
        # data[1] is the orderedDict
        # data[1][myFirstKey] is the 1st indicator in the orderedDict
        if data[1][myValueKey]['value'] == self.aggregator.defaults['NO_DATA']:
            myPosition = -1
        else:
            myPosition = data[1][myValueKey]['value']
            #FIXME MB this is to dehumanize the strings and have ints
            myPosition = myPosition.replace(',', '')
            myPosition = int(float(myPosition))

        return myPosition

    def _generateTables(self):
        """Parses the postprocessing output as one table per postprocessor.

        Args:
            None

        Returns:
            str - a string containing the html
        """
        myMessage = m.Message()

        for proc, resList in self.postProcessingOutput.iteritems():
            # resList 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'}})
            #    ]))
            #]
            try:
                #sorting using the first indicator of a postprocessor
                sortedResList = sorted(resList,
                                       key=self._sortNoData,
                                       reverse=True)

            except KeyError:
                LOGGER.debug('Skipping sorting as the postprocessor did not '
                             'have a "Total" field')

            #init table
            hasNoDataValues = False
            myTable = m.Table(
                style_class='table table-condensed table-striped')
            myTable.caption = self.tr('Detailed %1 report').arg(
                safeTr(get_postprocessor_human_name(proc)).lower())

            myHeaderRow = m.Row()
            myHeaderRow.add(str(self.attributeTitle).capitalize())
            for calculationName in sortedResList[0][1]:
                myHeaderRow.add(self.tr(calculationName))
            myTable.add(myHeaderRow)

            for zoneName, calc in sortedResList:
                myRow = m.Row(zoneName)

                for _, calculationData in calc.iteritems():
                    myValue = calculationData['value']
                    if myValue == self.aggregator.defaults['NO_DATA']:
                        hasNoDataValues = True
                        myValue += ' *'
                    myRow.add(myValue)
                myTable.add(myRow)

            #add table to message
            myMessage.add(myTable)
            if hasNoDataValues:
                myMessage.add(
                    m.EmphasizedText(
                        self.
                        tr('* "%1" values mean that there where some problems while '
                           'calculating them. This did not affect the other '
                           'values.').arg(
                               self.aggregator.defaults['NO_DATA'])))

        try:
            if (self.keywordIO.read_keywords(self.aggregator.layer,
                                             'HAD_MULTIPART_POLY')):
                myMessage.add(
                    m.EmphasizedText(
                        self.
                        tr('The aggregation layer had multipart polygons, these have '
                           'been exploded and are now marked with a #. This has no '
                           'influence on the calculation, just keep in mind that the '
                           'attributes shown may represent the original multipart '
                           'polygon and not the individual exploded polygon parts.'
                           )))
        except Exception:  # pylint: disable=W0703
            pass

        return myMessage

    def run(self):
        """Run any post processors requested by the impact function.

        Args:
            None

        Returns:
            None

        Raises:
            None
        """
        try:
            myRequestedPostProcessors = self.functionParams['postprocessors']
            myPostProcessors = get_postprocessors(myRequestedPostProcessors)
        except (TypeError, KeyError):
            # TypeError is for when functionParams is none
            # KeyError is for when ['postprocessors'] is unavailable
            myPostProcessors = {}
        LOGGER.debug('Running this postprocessors: ' + str(myPostProcessors))

        myFeatureNameAttribute = self.aggregator.attributes[
            self.aggregator.defaults['AGGR_ATTR_KEY']]
        if myFeatureNameAttribute is None:
            self.attributeTitle = self.tr('Aggregation unit')
        else:
            self.attributeTitle = myFeatureNameAttribute

        myNameFieldIndex = self.aggregator.layer.fieldNameIndex(
            self.attributeTitle)
        mySumFieldIndex = self.aggregator.layer.fieldNameIndex(
            self._sumFieldName())

        myFemaleRatioIsVariable = False
        myFemRatioFieldIndex = None
        myFemaleRatio = None

        if 'Gender' in myPostProcessors:
            #look if we need to look for a variable female ratio in a layer
            try:
                myFemRatioField = self.aggregator.attributes[
                    self.aggregator.defaults['FEM_RATIO_ATTR_KEY']]
                myFemRatioFieldIndex = self.aggregator.layer.fieldNameIndex(
                    myFemRatioField)
                myFemaleRatioIsVariable = True

            except KeyError:
                try:
                    myFemaleRatio = self.keywordIO.read_keywords(
                        self.aggregator.layer,
                        self.aggregator.defaults['FEM_RATIO_KEY'])
                except KeywordNotFoundError:
                    myFemaleRatio = self.aggregator.defaults['FEM_RATIO']

        #iterate zone features
        myProvider = self.aggregator.layer.dataProvider()
        myAttributes = myProvider.attributeIndexes()
        # start data retreival: fetch no geometry and all attributes for each
        # feature
        myProvider.select(myAttributes, QgsRectangle(), False)
        myFeature = QgsFeature()
        myPolygonIndex = 0
        while myProvider.nextFeature(myFeature):
            #get all attributes of a feature
            myAttributeMap = myFeature.attributeMap()

            #if a feature has no field called
            if myNameFieldIndex == -1:
                myZoneName = str(myFeature.id())
            else:
                myZoneName = myAttributeMap[myNameFieldIndex].toString()

            #create dictionary of attributes to pass to postprocessor
            myGeneralParams = {'target_field': self.aggregator.targetField}

            if self.aggregator.statisticsType == 'class_count':
                myGeneralParams['impact_classes'] = (
                    self.aggregator.statisticsClasses)
            elif self.aggregator.statisticsType == 'sum':
                myImpactTotal, _ = myAttributeMap[mySumFieldIndex].toDouble()
                myGeneralParams['impact_total'] = myImpactTotal

            try:
                myGeneralParams['impact_attrs'] = (
                    self.aggregator.impactLayerAttributes[myPolygonIndex])
            except IndexError:
                #rasters and attributeless vectors have no attributes
                myGeneralParams['impact_attrs'] = None

            for myKey, myValue in myPostProcessors.iteritems():
                myParameters = myGeneralParams
                try:
                    #look if params are available for this postprocessor
                    myParameters.update(
                        self.functionParams['postprocessors'][myKey]['params'])
                except KeyError:
                    pass

                if myKey == 'Gender':
                    if myFemaleRatioIsVariable:
                        myFemaleRatio, mySuccessFlag = myAttributeMap[
                            myFemRatioFieldIndex].toDouble()
                        if not mySuccessFlag:
                            myFemaleRatio = self.aggregator.defaults[
                                'FEM_RATIO']
                        LOGGER.debug(mySuccessFlag)
                    myParameters['female_ratio'] = myFemaleRatio

                myValue.setup(myParameters)
                myValue.process()
                myResults = myValue.results()
                myValue.clear()
                #                LOGGER.debug(myResults)
                try:
                    self.postProcessingOutput[myKey].append(
                        (myZoneName, myResults))
                except KeyError:
                    self.postProcessingOutput[myKey] = []
                    self.postProcessingOutput[myKey].append(
                        (myZoneName, myResults))
            #increment the index
            myPolygonIndex += 1

    def getOutput(self):
        """Returns the results of the post processing as a table.

        Args:
            theSingleTableFlag - bool indicating if result should be rendered
                as a single table. Default False.

        Returns: str - a string containing the html in the requested format.
        """

        # LOGGER.debug(self.postProcessingOutput)
        if self.errorMessage is not None:
            myMessage = m.Message(
                m.Heading(self.tr('Postprocessing report skipped')),
                m.Paragraph(
                    self.tr(
                        'Due to a problem while processing the results,'
                        ' the detailed postprocessing report is unavailable:'
                        ' %1').arg(self.errorMessage)))
            return myMessage

        return self._generateTables()
Пример #31
0
    def test_keywords_creation_wizard(self):
        """Test how the widgets work."""
        expected_category_count = 3
        expected_categories = ['exposure', 'hazard', 'aggregation']
        chosen_category = 'hazard'

        expected_subcategory_count = 4
        expected_subcategories = ['volcano', 'earthquake', 'flood', 'tsunami']
        chosen_subcategory = "tsunami"

        expected_unit_count = 3
        expected_units = ['wetdry', 'metres_depth', 'feet_depth']
        expected_chosen_unit = 'feet_depth'

        expected_field_count = 5
        expected_fields = [
            'OBJECTID', 'GRIDCODE', 'Shape_Leng', 'Shape_Area', 'Category'
        ]
        expected_chosen_field = 'GRIDCODE'

        expected_keywords = {
            'category': 'hazard',
            'subcategory': 'tsunami',
            'unit': 'feet_depth',
            'field': 'GRIDCODE',
            'source': 'some source',
            'title': 'some title'
        }

        layer = clone_shp_layer(name='tsunami_polygon')

        # check the environment first
        message = 'Test layer is not readable. Check environment variables.'
        self.assertIsNotNone(layer.dataProvider(), message)

        # Initialize dialog
        # noinspection PyTypeChecker
        dialog = WizardDialog(layer=layer)

        # step 1 of 7 - select category
        count = dialog.lstCategories.count()
        message = ('Invalid category count! There should be %d while there '
                   'were: %d') % (expected_category_count, count)
        self.assertEqual(count, expected_category_count, message)

        # Get all the categories given by wizards and save the 'hazard' index
        categories = []
        hazard_index = -1
        for i in range(expected_category_count):
            category_name = eval(
                dialog.lstCategories.item(i).data(Qt.UserRole))['id']
            categories.append(category_name)
            if category_name == chosen_category:
                hazard_index = i
        # Check if categories is the same with expected_categories
        message = 'Invalid categories! It should be "%s" while it was %s' % (
            expected_categories, categories)
        self.assertEqual(set(categories), set(expected_categories), message)
        # Check if the Next button state is on the right state
        message = ('Invalid Next button state in step 1! Enabled while '
                   'there\'s nothing selected yet')
        self.assertTrue(not dialog.pbnNext.isEnabled(), message)
        # Select hazard one
        dialog.lstCategories.setCurrentRow(hazard_index)
        message = ('Invalid Next button state in step 1! Still disabled after '
                   'an item selected')
        self.assertTrue(dialog.pbnNext.isEnabled(), message)
        # Click Next
        dialog.pbnNext.click()

        # step 2 of 7 - select subcategory
        # Check the number of sub categories
        count = dialog.lstSubcategories.count()
        message = ('Invalid subcategory count! There should be %d and there '
                   'were: %d') % (expected_subcategory_count, count)
        self.assertEqual(count, expected_subcategory_count, message)

        # Get all the subcategories given and save the 'tsunami' index
        subcategories = []
        tsunami_index = -1
        for i in range(expected_subcategory_count):
            subcategory_name = eval(
                dialog.lstSubcategories.item(i).data(Qt.UserRole))['id']
            subcategories.append(subcategory_name)
            if subcategory_name == chosen_subcategory:
                tsunami_index = i
        # Check if subcategories is the same with expected_subcategories
        message = ('Invalid sub categories! It should be "%s" while it was '
                   '%s') % (expected_subcategories, subcategories)
        self.assertEqual(set(subcategories), set(expected_subcategories),
                         message)
        # The Next button should be on disabled state first
        self.assertTrue(
            not dialog.pbnNext.isEnabled(), 'Invalid Next button'
            ' state in step 2! Enabled while there\'s nothing selected yet')
        # Set to tsunami subcategories
        dialog.lstSubcategories.setCurrentRow(tsunami_index)
        message = ('Invalid Next button state in step 2! Still disabled after '
                   'an item selected')
        self.assertTrue(dialog.pbnNext.isEnabled(), message)
        # Click next button
        dialog.pbnNext.click()

        # step 3 of 7 - select tsunami units
        # Check if the number of unit for tsunami is 3
        count = dialog.lstUnits.count()
        message = ('Invalid unit count! There should be %d while there were: '
                   '%d') % (expected_unit_count, count)
        self.assertEqual(count, expected_unit_count, message)
        # Get all the units given and save the 'feet_depth' index
        units = []
        feet_unit_index = -1
        for i in range(expected_unit_count):
            unit_name = eval(dialog.lstUnits.item(i).data(Qt.UserRole))['id']
            units.append(unit_name)
            if unit_name == expected_chosen_unit:
                feet_unit_index = i
        # Check if units is the same with expected_units
        message = ('Invalid units! It should be "%s" while it was '
                   '%s') % (expected_units, units)
        self.assertEqual(set(expected_units), set(units), message)
        # The button should be on disabled state first
        message = ('Invalid Next button state in step 3! Enabled while '
                   'there\'s nothing selected yet')
        self.assertTrue(not dialog.pbnNext.isEnabled(), message)
        dialog.lstUnits.setCurrentRow(feet_unit_index)
        message = ('Invalid Next button state in step 3! Enabled while '
                   'there\'s nothing selected yet')
        self.assertTrue(dialog.pbnNext.isEnabled(), message)

        dialog.pbnNext.click()

        # step 4 of 7 - select data field for tsunami feet
        count = dialog.lstFields.count()
        message = ('Invalid field count! There should be %d while there were: '
                   '%d') % (expected_field_count, count)
        self.assertEqual(count, expected_field_count, message)
        # Get all the fields given and save the 'GRIDCODE' index
        fields = []
        gridcode_index = -1
        for i in range(expected_field_count):
            field_name = dialog.lstFields.item(i).text()
            fields.append(field_name)
            if field_name == expected_chosen_field:
                gridcode_index = i
        # Check if fields is the same with expected_fields
        message = ('Invalid fields! It should be "%s" while it was '
                   '%s') % (expected_fields, fields)
        self.assertEqual(set(expected_fields), set(fields), message)
        # The button should be on disabled first
        message = ('Invalid Next button state in step 4! Enabled while '
                   'there\'s nothing selected yet')
        self.assertTrue(not dialog.pbnNext.isEnabled(), message)
        dialog.lstFields.setCurrentRow(gridcode_index)
        message = ('Invalid Next button state in step 4! Still disabled after '
                   'an item selected')
        self.assertTrue(dialog.pbnNext.isEnabled(), message)
        # Click next
        dialog.pbnNext.click()

        # step 6 of 7 - enter source
        message = ('Invalid Next button state in step 6! Disabled while '
                   'source is optional')
        self.assertTrue(dialog.pbnNext.isEnabled(), message)
        dialog.leSource.setText('some source')
        dialog.pbnNext.click()

        # step 7 of 7 - enter title
        dialog.leTitle.setText('some title')
        message = ('Invalid Next button state in step 7! Still disabled '
                   'after a text entered')
        self.assertTrue(dialog.pbnNext.isEnabled(), message)
        dialog.pbnNext.click()

        # test the resulting keywords
        keyword_io = KeywordIO()
        # noinspection PyTypeChecker
        keywords = keyword_io.read_keywords(layer)

        message = 'Invalid metadata!\n Was: %s\n Should be: %s' % (
            unicode(keywords), unicode(expected_keywords))

        self.assertEqual(keywords, expected_keywords, message)
Пример #32
0
class Map():
    """A class for creating a map."""
    def __init__(self, iface):
        """Constructor for the Map class.

        :param iface: Reference to the QGIS iface object.
        :type iface: QgsAppInterface
        """
        LOGGER.debug('InaSAFE Map class initialised')
        self.iface = iface
        self.layer = iface.activeLayer()
        self.keyword_io = KeywordIO()
        self.printer = None
        self.composition = None
        self.legend = None
        self.logo = ':/plugins/inasafe/bnpb_logo.png'
        self.template = ':/plugins/inasafe/inasafe.qpt'
        #self.page_width = 210  # width in mm
        #self.page_height = 297  # height in mm
        self.page_width = 0  # width in mm
        self.page_height = 0  # height in mm
        self.page_dpi = 300.0
        #self.page_margin = 10  # margin in mm
        self.show_frames = False  # intended for debugging use only
        self.page_margin = None
        #vertical spacing between elements
        self.vertical_spacing = None
        self.map_height = None
        self.mapWidth = None
        # make a square map where width = height = page width
        #self.map_height = self.page_width - (self.page_margin * 2)
        #self.mapWidth = self.map_height
        #self.disclaimer = self.tr('InaSAFE has been jointly developed by'
        #                          ' BNPB, AusAid & the World Bank')

    @staticmethod
    def tr(string):
        """We implement this since we do not inherit QObject.

        :param string: String for translation.
        :type string: QString, str

        :returns: Translated version of theString.
        :rtype: QString
        """
        # noinspection PyCallByClass,PyTypeChecker,PyArgumentList
        return QtCore.QCoreApplication.translate('Map', string)

    def set_impact_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

    def set_logo(self, logo):
        """

        :param logo: Path to image that will be used as logo in report
        :type logo: str
        """
        self.logo = logo

    def set_template(self, template):
        """

        :param template: Path to composer template that will be used for report
        :type template: str
        """
        self.template = template

    def setup_composition(self):
        """Set up the composition ready for drawing elements onto it."""
        LOGGER.debug('InaSAFE Map setupComposition called')
        canvas = self.iface.mapCanvas()
        renderer = canvas.mapRenderer()
        self.composition = QgsComposition(renderer)
        self.composition.setPlotStyle(QgsComposition.Print)  # or preview
        #self.composition.setPaperSize(self.page_width, self.page_height)
        self.composition.setPrintResolution(self.page_dpi)
        self.composition.setPrintAsRaster(True)

    def compose_map(self):
        """Place all elements on the map ready for printing."""
        self.setup_composition()
        # Keep track of our vertical positioning as we work our way down
        # the page placing elements on it.
        top_offset = self.page_margin
        self.draw_logo(top_offset)
        label_height = self.draw_title(top_offset)
        # Update the map offset for the next row of content
        top_offset += label_height + self.vertical_spacing
        composer_map = self.draw_map(top_offset)
        self.draw_scalebar(composer_map, top_offset)
        # Update the top offset for the next horizontal row of items
        top_offset += self.map_height + self.vertical_spacing - 1
        impact_title_height = self.draw_impact_title(top_offset)
        # Update the top offset for the next horizontal row of items
        if impact_title_height:
            top_offset += impact_title_height + self.vertical_spacing + 2
        self.draw_legend(top_offset)
        self.draw_host_and_time(top_offset)
        self.draw_disclaimer()

    def render(self):
        """Render the map composition to an image and save that to disk.

        :returns: A three-tuple of:
            * str: image_path - absolute path to png of rendered map
            * QImage: image - in memory copy of rendered map
            * QRectF: target_area - dimensions of rendered map
        :rtype: tuple
        """
        LOGGER.debug('InaSAFE Map renderComposition called')
        # NOTE: we ignore self.composition.printAsRaster() and always rasterise
        width = int(self.page_dpi * self.page_width / 25.4)
        height = int(self.page_dpi * self.page_height / 25.4)
        image = QtGui.QImage(
            QtCore.QSize(width, height),
            QtGui.QImage.Format_ARGB32)
        image.setDotsPerMeterX(dpi_to_meters(self.page_dpi))
        image.setDotsPerMeterY(dpi_to_meters(self.page_dpi))

        # Only works in Qt4.8
        #image.fill(QtGui.qRgb(255, 255, 255))
        # Works in older Qt4 versions
        image.fill(55 + 255 * 256 + 255 * 256 * 256)
        image_painter = QtGui.QPainter(image)
        source_area = QtCore.QRectF(
            0, 0, self.page_width,
            self.page_height)
        target_area = QtCore.QRectF(0, 0, width, height)
        self.composition.render(image_painter, target_area, source_area)
        image_painter.end()
        image_path = unique_filename(
            prefix='mapRender_',
            suffix='.png',
            dir=temp_dir())
        image.save(image_path)
        return image_path, image, target_area

    def make_pdf(self, filename):
        """Generate the printout for our final map.

        :param filename: Path on the file system to which the pdf should be
            saved. If None, a generated file name will be used.
        :type filename: str

        :returns: File name of the output file (equivalent to filename if
                provided).
        :rtype: str
        """
        LOGGER.debug('InaSAFE Map printToPdf called')
        if filename is None:
            map_pdf_path = unique_filename(
                prefix='report', suffix='.pdf', dir=temp_dir())
        else:
            # We need to cast to python string in case we receive a QString
            map_pdf_path = str(filename)

        self.load_template()

        resolution = self.composition.printResolution()
        self.printer = setup_printer(map_pdf_path, resolution=resolution)
        _, image, rectangle = self.render()
        painter = QtGui.QPainter(self.printer)
        painter.drawImage(rectangle, image, rectangle)
        painter.end()
        return map_pdf_path

    def draw_logo(self, top_offset):
        """Add a picture containing the logo to the map top left corner

        :param top_offset: Vertical offset at which the logo should be drawn.
        :type top_offset: int
        """
        logo = QgsComposerPicture(self.composition)
        logo.setPictureFile(':/plugins/inasafe/bnpb_logo.png')
        logo.setItemPosition(self.page_margin, top_offset, 10, 10)
        logo.setFrameEnabled(self.show_frames)
        logo.setZValue(1)  # To ensure it overlays graticule markers
        self.composition.addItem(logo)

    def draw_title(self, top_offset):
        """Add a title to the composition.

        :param top_offset: Vertical offset at which the logo should be drawn.
        :type top_offset: int

        :returns: The height of the label as rendered.
        :rtype: float
        """
        LOGGER.debug('InaSAFE Map drawTitle called')
        font_size = 14
        font_weight = QtGui.QFont.Bold
        italics_flag = False
        font = QtGui.QFont(
            'verdana',
            font_size,
            font_weight,
            italics_flag)
        label = QgsComposerLabel(self.composition)
        label.setFont(font)
        heading = self.tr(
            'InaSAFE - Indonesia Scenario Assessment for Emergencies')
        label.setText(heading)
        label.adjustSizeToText()
        label_height = 10.0  # determined using qgis map composer
        label_width = 170.0   # item - position and size...option
        left_offset = self.page_width - self.page_margin - label_width
        label.setItemPosition(
            left_offset,
            top_offset - 2,  # -2 to push it up a little
            label_width,
            label_height)
        label.setFrameEnabled(self.show_frames)
        self.composition.addItem(label)
        return label_height

    def draw_map(self, top_offset):
        """Add a map to the composition and return the composer map instance.

        :param top_offset: Vertical offset at which the logo should be drawn.
        :type top_offset: int

        :returns: The composer map.
        :rtype: QgsComposerMap
        """
        LOGGER.debug('InaSAFE Map drawMap called')
        map_width = self.mapWidth
        composer_map = QgsComposerMap(
            self.composition,
            self.page_margin,
            top_offset,
            map_width,
            self.map_height)
        #myExtent = self.iface.mapCanvas().extent()
        # The dimensions of the map canvas and the print composer map may
        # differ. So we set the map composer extent using the canvas and
        # then defer to the map canvas's map extents thereafter
        # Update: disabled as it results in a rectangular rather than
        # square map
        #composer_map.setNewExtent(myExtent)
        composer_extent = composer_map.extent()
        # Recenter the composer map on the center of the canvas
        # 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.iface.mapCanvas().extent()
        width = canvas_extent.width()
        height = canvas_extent.height()
        longest_length = width
        if width < height:
            longest_length = height
        half_length = longest_length / 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
        square_extent = QgsRectangle(min_x, min_y, max_x, max_y)
        composer_map.setNewExtent(square_extent)

        composer_map.setGridEnabled(True)
        split_count = 5
        # .. todo:: Write logic to adjust precision so that adjacent tick marks
        #    always have different displayed values
        precision = 2
        x_interval = composer_extent.width() / split_count
        composer_map.setGridIntervalX(x_interval)
        y_interval = composer_extent.height() / split_count
        composer_map.setGridIntervalY(y_interval)
        composer_map.setGridStyle(QgsComposerMap.Cross)
        cross_length_mm = 1
        composer_map.setCrossLength(cross_length_mm)
        composer_map.setZValue(0)  # To ensure it does not overlay logo
        font_size = 6
        font_weight = QtGui.QFont.Normal
        italics_flag = False
        font = QtGui.QFont(
            'verdana',
            font_size,
            font_weight,
            italics_flag)
        composer_map.setGridAnnotationFont(font)
        composer_map.setGridAnnotationPrecision(precision)
        composer_map.setShowGridAnnotation(True)
        composer_map.setGridAnnotationDirection(
            QgsComposerMap.BoundaryDirection, QgsComposerMap.Top)
        self.composition.addItem(composer_map)
        self.draw_graticule_mask(top_offset)
        return composer_map

    def draw_graticule_mask(self, top_offset):
        """A helper function to mask out graticule labels.

         It will hide labels on the right side by over painting a white
         rectangle with white border on them. **kludge**

        :param top_offset: Vertical offset at which the logo should be drawn.
        :type top_offset: int
        """
        LOGGER.debug('InaSAFE Map drawGraticuleMask called')
        left_offset = self.page_margin + self.mapWidth
        rect = QgsComposerShape(
            left_offset + 0.5,
            top_offset,
            self.page_width - left_offset,
            self.map_height + 1,
            self.composition)

        rect.setShapeType(QgsComposerShape.Rectangle)
        pen = QtGui.QPen()
        pen.setColor(QtGui.QColor(0, 0, 0))
        pen.setWidthF(0.1)
        rect.setPen(pen)
        rect.setBackgroundColor(QtGui.QColor(255, 255, 255))
        rect.setTransparency(100)
        #rect.setLineWidth(0.1)
        #rect.setFrameEnabled(False)
        #rect.setOutlineColor(QtGui.QColor(255, 255, 255))
        #rect.setFillColor(QtGui.QColor(255, 255, 255))
        #rect.setOpacity(100)
        # These two lines seem superfluous but are needed
        brush = QtGui.QBrush(QtGui.QColor(255, 255, 255))
        rect.setBrush(brush)
        self.composition.addItem(rect)

    def draw_native_scalebar(self, composer_map, top_offset):
        """Draw a scale bar using QGIS' native drawing.

        In the case of geographic maps, scale will be in degrees, not km.

        :param composer_map: Composer map on which to draw the scalebar.
        :type composer_map: QgsComposerMap

        :param top_offset: Vertical offset at which the logo should be drawn.
        :type top_offset: int
        """
        LOGGER.debug('InaSAFE Map drawNativeScaleBar called')
        scale_bar = QgsComposerScaleBar(self.composition)
        scale_bar.setStyle('Numeric')  # optionally modify the style
        scale_bar.setComposerMap(composer_map)
        scale_bar.applyDefaultSize()
        scale_bar_height = scale_bar.boundingRect().height()
        scale_bar_width = scale_bar.boundingRect().width()
        # -1 to avoid overlapping the map border
        scale_bar.setItemPosition(
            self.page_margin + 1,
            top_offset + self.map_height - (scale_bar_height * 2),
            scale_bar_width,
            scale_bar_height)
        scale_bar.setFrameEnabled(self.show_frames)
        # Disabled for now
        #self.composition.addItem(scale_bar)

    def draw_scalebar(self, composer_map, top_offset):
        """Add a numeric scale to the bottom left of the map.

        We draw the scale bar manually because QGIS does not yet support
        rendering a scale bar for a geographic map in km.

        .. seealso:: :meth:`drawNativeScaleBar`

        :param composer_map: Composer map on which to draw the scalebar.
        :type composer_map: QgsComposerMap

        :param top_offset: Vertical offset at which the logo should be drawn.
        :type top_offset: int
        """
        LOGGER.debug('InaSAFE Map drawScaleBar called')
        canvas = self.iface.mapCanvas()
        renderer = canvas.mapRenderer()
        #
        # Add a linear map scale
        #
        distance_area = QgsDistanceArea()
        distance_area.setSourceCrs(renderer.destinationCrs().srsid())
        distance_area.setEllipsoidalMode(True)
        # Determine how wide our map is in km/m
        # Starting point at BL corner
        composer_extent = composer_map.extent()
        start_point = QgsPoint(
            composer_extent.xMinimum(),
            composer_extent.yMinimum())
        # Ending point at BR corner
        end_point = QgsPoint(
            composer_extent.xMaximum(),
            composer_extent.yMinimum())
        ground_distance = distance_area.measureLine(start_point, end_point)
        # Get the equivalent map distance per page mm
        map_width = self.mapWidth
        # How far is 1mm on map on the ground in meters?
        mm_to_ground = ground_distance / map_width
        #print 'MM:', myMMDistance
        # How long we want the scale bar to be in relation to the map
        scalebar_to_map_ratio = 0.5
        # How many divisions the scale bar should have
        tick_count = 5
        scale_bar_width_mm = map_width * scalebar_to_map_ratio
        print_segment_width_mm = scale_bar_width_mm / tick_count
        # Segment width in real world (m)
        # We apply some logic here so that segments are displayed in meters
        # if each segment is less that 1000m otherwise km. Also the segment
        # lengths are rounded down to human looking numbers e.g. 1km not 1.1km
        units = ''
        ground_segment_width = print_segment_width_mm * mm_to_ground
        if ground_segment_width < 1000:
            units = 'm'
            ground_segment_width = round(ground_segment_width)
            # adjust the segment width now to account for rounding
            print_segment_width_mm = ground_segment_width / mm_to_ground
        else:
            units = 'km'
            # Segment with in real world (km)
            ground_segment_width = round(ground_segment_width / 1000)
            print_segment_width_mm = (
                (ground_segment_width * 1000) / mm_to_ground)
        # Now adjust the scalebar width to account for rounding
        scale_bar_width_mm = tick_count * print_segment_width_mm

        #print "SBWMM:", scale_bar_width_mm
        #print "SWMM:", print_segment_width_mm
        #print "SWM:", myGroundSegmentWidthM
        #print "SWKM:", myGroundSegmentWidthKM
        # start drawing in line segments
        scalebar_height = 5  # mm
        line_width = 0.3  # mm
        inset_distance = 7  # how much to inset the scalebar into the map by
        scalebar_x = self.page_margin + inset_distance
        scalebar_y = (
            top_offset + self.map_height - inset_distance -
            scalebar_height)  # mm

        # Draw an outer background box - shamelessly hardcoded buffer
        rectangle = QgsComposerShape(
            scalebar_x - 4,  # left edge
            scalebar_y - 3,  # top edge
            scale_bar_width_mm + 13,  # right edge
            scalebar_height + 6,  # bottom edge
            self.composition)

        rectangle.setShapeType(QgsComposerShape.Rectangle)
        pen = QtGui.QPen()
        pen.setColor(QtGui.QColor(255, 255, 255))
        pen.setWidthF(line_width)
        rectangle.setPen(pen)
        #rectangle.setLineWidth(line_width)
        rectangle.setFrameEnabled(False)
        brush = QtGui.QBrush(QtGui.QColor(255, 255, 255))
        # workaround for missing setTransparentFill missing from python api
        rectangle.setBrush(brush)
        self.composition.addItem(rectangle)
        # Set up the tick label font
        font_weight = QtGui.QFont.Normal
        font_size = 6
        italics_flag = False
        font = QtGui.QFont(
            'verdana',
            font_size,
            font_weight,
            italics_flag)
        # Draw the bottom line
        up_shift = 0.3  # shift the bottom line up for better rendering
        rectangle = QgsComposerShape(
            scalebar_x,
            scalebar_y + scalebar_height - up_shift,
            scale_bar_width_mm,
            0.1,
            self.composition)

        rectangle.setShapeType(QgsComposerShape.Rectangle)
        pen = QtGui.QPen()
        pen.setColor(QtGui.QColor(255, 255, 255))
        pen.setWidthF(line_width)
        rectangle.setPen(pen)
        #rectangle.setLineWidth(line_width)
        rectangle.setFrameEnabled(False)
        self.composition.addItem(rectangle)

        # Now draw the scalebar ticks
        for tick_counter in range(0, tick_count + 1):
            distance_suffix = ''
            if tick_counter == tick_count:
                distance_suffix = ' ' + units
            real_world_distance = (
                '%.0f%s' %
                (tick_counter *
                ground_segment_width,
                distance_suffix))
            #print 'RW:', myRealWorldDistance
            mm_offset = scalebar_x + (
                tick_counter * print_segment_width_mm)
            #print 'MM:', mm_offset
            tick_height = scalebar_height / 2
            # Lines are not exposed by the api yet so we
            # bodge drawing lines using rectangles with 1px height or width
            tick_width = 0.1  # width or rectangle to be drawn
            uptick_line = QgsComposerShape(
                mm_offset,
                scalebar_y + scalebar_height - tick_height,
                tick_width,
                tick_height,
                self.composition)

            uptick_line.setShapeType(QgsComposerShape.Rectangle)
            pen = QtGui.QPen()
            pen.setWidthF(line_width)
            uptick_line.setPen(pen)
            #uptick_line.setLineWidth(line_width)
            uptick_line.setFrameEnabled(False)
            self.composition.addItem(uptick_line)
            #
            # Add a tick label
            #
            label = QgsComposerLabel(self.composition)
            label.setFont(font)
            label.setText(real_world_distance)
            label.adjustSizeToText()
            label.setItemPosition(
                mm_offset - 3,
                scalebar_y - tick_height)
            label.setFrameEnabled(self.show_frames)
            self.composition.addItem(label)

    def draw_impact_title(self, top_offset):
        """Draw the map subtitle - obtained from the impact layer keywords.

        :param top_offset: Vertical offset at which the logo should be drawn.
        :type top_offset: int

        :returns: The height of the label as rendered.
        :rtype: float
        """
        LOGGER.debug('InaSAFE Map drawImpactTitle called')
        title = self.map_title()
        if title is None:
            title = ''
        font_size = 20
        font_weight = QtGui.QFont.Bold
        italics_flag = False
        font = QtGui.QFont(
            'verdana', font_size, font_weight, italics_flag)
        label = QgsComposerLabel(self.composition)
        label.setFont(font)
        heading = title
        label.setText(heading)
        label_width = self.page_width - (self.page_margin * 2)
        label_height = 12
        label.setItemPosition(
            self.page_margin, top_offset, label_width, label_height)
        label.setFrameEnabled(self.show_frames)
        self.composition.addItem(label)
        return label_height

    def draw_legend(self, top_offset):
        """Add a legend to the map using our custom legend renderer.

        .. note:: getLegend generates a pixmap in 150dpi so if you set
           the map to a higher dpi it will appear undersized.

        :param top_offset: Vertical offset at which the logo should be drawn.
        :type top_offset: int
        """
        LOGGER.debug('InaSAFE Map drawLegend called')
        legend_attributes = self.map_legend_attributes()
        legend_notes = legend_attributes.get('legend_notes', None)
        legend_units = legend_attributes.get('legend_units', None)
        legend_title = legend_attributes.get('legend_title', None)
        LOGGER.debug(legend_attributes)
        legend = MapLegend(
            self.layer,
            self.page_dpi,
            legend_title,
            legend_notes,
            legend_units)
        self.legend = legend.get_legend()
        picture1 = QgsComposerPicture(self.composition)
        legend_file_path = unique_filename(
            prefix='legend', suffix='.png', dir='work')
        self.legend.save(legend_file_path, 'PNG')
        picture1.setPictureFile(legend_file_path)
        legend_height = points_to_mm(self.legend.height(), self.page_dpi)
        legend_width = points_to_mm(self.legend.width(), self.page_dpi)
        picture1.setItemPosition(
            self.page_margin,
            top_offset,
            legend_width,
            legend_height)
        picture1.setFrameEnabled(False)
        self.composition.addItem(picture1)
        os.remove(legend_file_path)

    def draw_image(self, image, width_mm, left_offset, top_offset):
        """Helper to draw an image directly onto the QGraphicsScene.
        This is an alternative to using QgsComposerPicture which in
        some cases leaves artifacts under windows.

        The Pixmap will have a transform applied to it so that
        it is rendered with the same resolution as the composition.

        :param image: Image that will be rendered to the layout.
        :type image: QImage

        :param width_mm: Desired width in mm of output on page.
        :type width_mm: int

        :param left_offset: Offset from left of page.
        :type left_offset: int

        :param top_offset: Offset from top of page.
        :type top_offset: int

        :returns: Graphics scene item.
        :rtype: QGraphicsSceneItem
        """
        LOGGER.debug('InaSAFE Map drawImage called')
        desired_width_mm = width_mm  # mm
        desired_width_px = mm_to_points(desired_width_mm, self.page_dpi)
        actual_width_px = image.width()
        scale_factor = desired_width_px / actual_width_px

        LOGGER.debug('%s %s %s' % (
            scale_factor, actual_width_px, desired_width_px))
        transform = QtGui.QTransform()
        transform.scale(scale_factor, scale_factor)
        transform.rotate(0.5)
        # noinspection PyArgumentList
        item = self.composition.addPixmap(QtGui.QPixmap.fromImage(image))
        item.setTransform(transform)
        item.setOffset(
            left_offset / scale_factor, top_offset / scale_factor)
        return item

    def draw_host_and_time(self, top_offset):
        """Add a note with hostname and time to the composition.

        :param top_offset: Vertical offset at which the logo should be drawn.
        :type top_offset: int
        """
        LOGGER.debug('InaSAFE Map drawDisclaimer called')
        #elapsed_time: 11.612545
        #user: timlinux
        #host_name: ultrabook
        #time_stamp: 2012-10-13_23:10:31
        #myUser = self.keyword_io.readKeywords(self.layer, 'user')
        #myHost = self.keyword_io.readKeywords(self.layer, 'host_name')
        date_time = self.keyword_io.read_keywords(self.layer, 'time_stamp')
        tokens = date_time.split('_')
        date = tokens[0]
        time = tokens[1]
        #myElapsedTime = self.keyword_io.readKeywords(self.layer,
        #                                            'elapsed_time')
        #myElapsedTime = humaniseSeconds(myElapsedTime)
        long_version = get_version()
        tokens = long_version.split('.')
        version = '%s.%s.%s' % (tokens[0], tokens[1], tokens[2])
        label_text = self.tr(
            'Date and time of assessment: %s %s\n'
            'Special note: This assessment is a guide - we strongly recommend '
            'that you ground truth the results shown here before deploying '
            'resources and / or personnel.\n'
            'Assessment carried out using InaSAFE release %s (QGIS '
            'plugin version).') % (date, time, version)
        font_size = 6
        font_weight = QtGui.QFont.Normal
        italics_flag = True
        font = QtGui.QFont(
            'verdana',
            font_size,
            font_weight,
            italics_flag)
        label = QgsComposerLabel(self.composition)
        label.setFont(font)
        label.setText(label_text)
        label.adjustSizeToText()
        label_height = 50.0  # mm determined using qgis map composer
        label_width = (self.page_width / 2) - self.page_margin
        left_offset = self.page_width / 2  # put in right half of page
        label.setItemPosition(
            left_offset,
            top_offset,
            label_width,
            label_height,)
        label.setFrameEnabled(self.show_frames)
        self.composition.addItem(label)

    def draw_disclaimer(self):
        """Add a disclaimer to the composition."""
        LOGGER.debug('InaSAFE Map drawDisclaimer called')
        font_size = 10
        font_weight = QtGui.QFont.Normal
        italics_flag = True
        font = QtGui.QFont(
            'verdana',
            font_size,
            font_weight,
            italics_flag)
        label = QgsComposerLabel(self.composition)
        label.setFont(font)
        label.setText(self.disclaimer)
        label.adjustSizeToText()
        label_height = 7.0  # mm determined using qgis map composer
        label_width = self.page_width   # item - position and size...option
        left_offset = self.page_margin
        top_offset = self.page_height - self.page_margin
        label.setItemPosition(
            left_offset,
            top_offset,
            label_width,
            label_height,)
        label.setFrameEnabled(self.show_frames)
        self.composition.addItem(label)

    def map_title(self):
        """Get the map title from the layer keywords if possible.

        :returns: None on error, otherwise the title.
        :rtype: None, str
        """
        LOGGER.debug('InaSAFE Map getMapTitle called')
        try:
            title = self.keyword_io.read_keywords(self.layer, 'map_title')
            return title
        except KeywordNotFoundError:
            return None
        except Exception:
            return None

    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 getMapLegendAtributes called')
        legend_attribute_list = [
            'legend_notes',
            'legend_units',
            'legend_title']
        legend_attribute_dict = {}
        for myLegendAttribute in legend_attribute_list:
            try:
                legend_attribute_dict[myLegendAttribute] = \
                    self.keyword_io.read_keywords(
                        self.layer, myLegendAttribute)
            except KeywordNotFoundError:
                pass
            except Exception:
                pass
        return legend_attribute_dict

    def show_composer(self):
        """Show the composition in a composer view so the user can tweak it.
        """
        view = QgsComposerView(self.iface.mainWindow())
        view.show()

    def write_template(self, template_path):
        """Write current composition as a template that can be re-used in QGIS.

        :param template_path: Path to which template should be written.
        :type template_path: str
        """
        document = QtXml.QDomDocument()
        element = document.createElement('Composer')
        document.appendChild(element)
        self.composition.writeXML(element, document)
        xml = document.toByteArray()
        template_file = file(template_path, 'wb')
        template_file.write(xml)
        template_file.close()

    def load_template(self):
        """Load a QgsComposer map from a template and render it.

        .. note:: THIS METHOD IS EXPERIMENTAL
        """
        self.setup_composition()

        template_file = QtCore.QFile(self.template)
        template_file.open(QtCore.QIODevice.ReadOnly | QtCore.QIODevice.Text)
        template_content = template_file.readAll()
        template_file.close()

        document = QtXml.QDomDocument()
        document.setContent(template_content)

        # get information for substitutions
        # date, time and plugin version
        date_time = self.keyword_io.read_keywords(self.layer, 'time_stamp')
        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])

        # map title
        LOGGER.debug('InaSAFE Map getMapTitle called')
        try:
            title = self.keyword_io.read_keywords(self.layer, 'map_title')
        except KeywordNotFoundError:
            title = None
        except Exception:
            title = None

        if not title:
            title = ''

        substitution_map = {
            'impact-title': title,
            'date': date,
            'time': time,
            'safe-version': version
        }
        LOGGER.debug(substitution_map)
        load_ok = self.composition.loadFromTemplate(document,
                                                    substitution_map)
        if not load_ok:
            raise ReportCreationError(
                self.tr('Error loading template %s') %
                self.template)

        self.page_width = self.composition.paperWidth()
        self.page_height = self.composition.paperHeight()

        # set logo
        image = self.composition.getComposerItemById('safe-logo')
        image.setPictureFile(self.logo)

        # Get the main map canvas on the composition and set
        # its extents to the event.
        map = self.composition.getComposerItemById('impact-map')
        if map is not None:
            # Recenter the composer map on the center of the canvas
            # 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.iface.mapCanvas().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
            square_extent = QgsRectangle(min_x, min_y, max_x, max_y)
            map.setNewExtent(square_extent)

            # calculate intervals for grid
            split_count = 5
            x_interval = square_extent.width() / split_count
            map.setGridIntervalX(x_interval)
            y_interval = square_extent.height() / split_count
            map.setGridIntervalY(y_interval)
        else:
            raise ReportCreationError(self.tr(
                'Map "impact-map" could not be found'))

        legend = self.composition.getComposerItemById('impact-legend')
        legend_attributes = self.map_legend_attributes()
        LOGGER.debug(legend_attributes)
        #legend_notes = mapLegendAttributes.get('legend_notes', None)
        #legend_units = mapLegendAttributes.get('legend_units', None)
        legend_title = legend_attributes.get('legend_title', None)
        if legend_title is None:
            legend_title = ""
        legend.setTitle(legend_title)
        legend.updateLegend()
Пример #33
0
class KeywordIOTest(unittest.TestCase):
    """Tests for reading and writing of raster and vector data
    """

    def setUp(self):
        self.keywordIO = KeywordIO()
        myUri = QgsDataSourceURI()
        myUri.setDatabase(os.path.join(TESTDATA, 'jk.sqlite'))
        myUri.setDataSource('', 'osm_buildings', 'Geometry')
        self.sqliteLayer = QgsVectorLayer(myUri.uri(), 'OSM Buildings',
                                          'spatialite')
        myHazardPath = os.path.join(HAZDATA, 'Shakemap_Padang_2009.asc')
        self.fileRasterLayer, myType = load_layer(
            myHazardPath, directory=None)
        del myType
        self.fileVectorLayer, myType = load_layer('Padang_WGS84.shp')
        del myType
        self.expectedSqliteKeywords = {
            'category': 'exposure',
            'datatype': 'OSM',
            'subcategory': 'building'}
        self.expectedVectorKeywords = {
            'category': 'exposure',
            'datatype': 'itb',
            'subcategory': 'structure',
            'title': 'Padang WGS84'}
        self.expectedRasterKeywords = {
            'category': 'hazard',
            'source': 'USGS',
            'subcategory': 'earthquake',
            'unit': 'MMI',
            'title': ('An earthquake in Padang '
            'like in 2009')}

    def tearDown(self):
        pass

    def test_getHashForDatasource(self):
        """Test we can reliably get a hash for a uri"""
        myHash = self.keywordIO.hash_for_datasource(PG_URI)
        myExpectedHash = '7cc153e1b119ca54a91ddb98a56ea95e'
        myMessage = "Got: %s\nExpected: %s" % (myHash, myExpectedHash)
        assert myHash == myExpectedHash, myMessage

    def test_writeReadKeywordFromUri(self):
        """Test we can set and get keywords for a non local datasource"""
        myHandle, myFilename = 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(myHandle)
        os.remove(myFilename)
        myExpectedKeywords = {'category': 'exposure',
                              'datatype': 'itb',
                              'subcategory': 'building'}
        # SQL insert test
        # On first write schema is empty and there is no matching hash
        self.keywordIO.set_keyword_db_path(myFilename)
        self.keywordIO.write_keywords_for_uri(PG_URI, myExpectedKeywords)
        # SQL Update test
        # On second write schema is populated and we update matching hash
        myExpectedKeywords = {'category': 'exposure',
                              'datatype': 'OSM',  # <--note the change here!
                              'subcategory': 'building'}
        self.keywordIO.write_keywords_for_uri(PG_URI, myExpectedKeywords)
        # Test getting all keywords
        myKeywords = self.keywordIO.read_keyword_from_uri(PG_URI)
        myMessage = 'Got: %s\n\nExpected %s\n\nDB: %s' % (
                    myKeywords, myExpectedKeywords, myFilename)
        assert myKeywords == myExpectedKeywords, myMessage
        # Test getting just a single keyword
        myKeyword = self.keywordIO.read_keyword_from_uri(PG_URI, 'datatype')
        myExpectedKeyword = 'OSM'
        myMessage = 'Got: %s\n\nExpected %s\n\nDB: %s' % (
                    myKeyword, myExpectedKeyword, myFilename)
        assert myKeyword == myExpectedKeyword, myMessage
        # Test deleting keywords actually does delete
        self.keywordIO.delete_keywords_for_uri(PG_URI)
        try:
            myKeyword = self.keywordIO.read_keyword_from_uri(PG_URI, 'datatype')
            #if the above didnt cause an exception then bad
            myMessage = 'Expected a HashNotFoundError to be raised'
            assert myMessage
        except HashNotFoundError:
            #we expect this outcome so good!
            pass

    def test_areKeywordsFileBased(self):
        """Can we correctly determine if keywords should be written to file or
        to database?"""
        assert not self.keywordIO.are_keywords_file_based(self.sqliteLayer)
        assert self.keywordIO.are_keywords_file_based(self.fileRasterLayer)
        assert self.keywordIO.are_keywords_file_based(self.fileVectorLayer)

    def test_readRasterFileKeywords(self):
        """Can we read raster file keywords using generic readKeywords method
        """
        myKeywords = self.keywordIO.read_keywords(self.fileRasterLayer)
        myExpectedKeywords = self.expectedRasterKeywords
        mySource = self.fileRasterLayer.source()
        myMessage = 'Got:\n%s\nExpected:\n%s\nSource:\n%s' % (
                    myKeywords, myExpectedKeywords, mySource)
        assert myKeywords == myExpectedKeywords, myMessage

    def test_readVectorFileKeywords(self):
        """Test read vector file keywords with the generic readKeywords method.
         """
        myKeywords = self.keywordIO.read_keywords(self.fileVectorLayer)
        myExpectedKeywords = self.expectedVectorKeywords
        mySource = self.fileVectorLayer.source()
        myMessage = 'Got: %s\n\nExpected %s\n\nSource: %s' % (
                    myKeywords, myExpectedKeywords, mySource)
        assert myKeywords == myExpectedKeywords, myMessage

    def test_appendKeywords(self):
        """Can we append file keywords with the generic readKeywords method."""
        myLayer, _ = makePadangLayerClone()
        myNewKeywords = {'category': 'exposure', 'test': 'TEST'}
        self.keywordIO.update_keywords(myLayer, myNewKeywords)
        myKeywords = self.keywordIO.read_keywords(myLayer)

        for myKey, myValue in myNewKeywords.iteritems():
            myMessage = (
                'Layer keywords misses appended key: %s\n'
                'Layer keywords:\n%s\n'
                'Appended keywords:\n%s\n' %
                (myKey,
                myKeywords,
                myNewKeywords))
            assert myKey in myKeywords, myMessage
            myMessage = (
                'Layer keywords misses appended value: %s\n'
                'Layer keywords:\n%s\n'
                'Appended keywords:\n%s\n' %
                (myValue,
                myKeywords,
                myNewKeywords))
            assert myKeywords[myKey] == myValue, myMessage

    def test_readDBKeywords(self):
        """Can we read sqlite keywords with the generic readKeywords method
        """
        myLocalPath = os.path.join(os.path.dirname(__file__),
                                   '../../..///', 'jk.sqlite')
        myPath = os.path.join(TESTDATA, 'test_keywords.db')
        self.keywordIO.set_keyword_db_path(myPath)
        # We need to make a local copy of the dataset so
        # that we can use a local path that will hash properly on the
        # database to return us the correct / valid keywords record.
        shutil.copy2(os.path.join(TESTDATA, 'jk.sqlite'), myLocalPath)
        myUri = QgsDataSourceURI()
        # always use relative path!
        myUri.setDatabase('../jk.sqlite')
        myUri.setDataSource('', 'osm_buildings', 'Geometry')
        # create a local version that has the relative url
        mySqliteLayer = QgsVectorLayer(myUri.uri(), 'OSM Buildings',
                                       'spatialite')
        myExpectedSource = ('dbname=\'../jk.sqlite\' table="osm_buildings"'
                            ' (Geometry) sql=')
        myMessage = 'Got source: %s\n\nExpected %s\n' % (
                    mySqliteLayer.source, myExpectedSource)
        assert mySqliteLayer.source() == myExpectedSource, myMessage
        myKeywords = self.keywordIO.read_keywords(mySqliteLayer)
        myExpectedKeywords = self.expectedSqliteKeywords
        assert myKeywords == myExpectedKeywords, myMessage
        mySource = self.sqliteLayer.source()
        # delete mySqliteLayer so that we can delete the file
        del mySqliteLayer
        os.remove(myLocalPath)
        myMessage = 'Got: %s\n\nExpected %s\n\nSource: %s' % (
                    myKeywords, myExpectedKeywords, mySource)
        assert myKeywords == myExpectedKeywords, myMessage
Пример #34
0
class Map():
    """A class for creating a map."""
    def __init__(self, iface):
        """Constructor for the Map class.

        :param iface: Reference to the QGIS iface object.
        :type iface: QgsAppInterface
        """
        LOGGER.debug('InaSAFE Map class initialised')
        self.iface = iface
        self.layer = iface.activeLayer()
        self.keyword_io = KeywordIO()
        self.printer = None
        self.composition = None
        self.extent = iface.mapCanvas().extent()
        self.safe_logo = ':/plugins/inasafe/inasafe-logo-url.svg'
        self.north_arrow = ':/plugins/inasafe/simple_north_arrow.png'
        self.org_logo = ':/plugins/inasafe/supporters.png'
        self.template = ':/plugins/inasafe/inasafe-portrait-a4.qpt'
        self.disclaimer = disclaimer()
        self.page_width = 0  # width in mm
        self.page_height = 0  # height in mm
        self.page_dpi = 300.0
        self.show_frames = False  # intended for debugging use only

    @staticmethod
    def tr(string):
        """We implement this since we do not inherit QObject.

        :param string: String for translation.
        :type string: QString, str

        :returns: Translated version of theString.
        :rtype: QString
        """
        # noinspection PyCallByClass,PyTypeChecker,PyArgumentList
        return QtCore.QCoreApplication.translate('Map', string)

    def set_impact_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

    def set_north_arrow_image(self, logo_path):
        """Set image that will be used as organisation logo in reports.

        :param logo_path: Path to image file
        :type logo_path: str
        """
        self.north_arrow = logo_path

    def set_organisation_logo(self, logo):
        """Set image that will be used as organisation logo in reports.

        :param logo: Path to image file
        :type logo: str
        """
        self.org_logo = logo

    def set_disclaimer(self, text):
        """Set text that will be used as disclaimer in reports.

        :param text: Disclaimer text
        :type text: str
        """
        self.disclaimer = text

    def set_template(self, template):
        """Set template that will be used for report generation.

        :param template: Path to composer template
        :type template: str
        """
        self.template = template

    def set_extent(self, extent):
        """Set extent or the report map

        :param extent: Extent of the report map
        :type extent: QgsRectangle

        """
        self.extent = extent

    def setup_composition(self):
        """Set up the composition ready for drawing elements onto it."""
        LOGGER.debug('InaSAFE Map setupComposition called')
        canvas = self.iface.mapCanvas()
        renderer = canvas.mapRenderer()
        self.composition = QgsComposition(renderer)
        self.composition.setPlotStyle(QgsComposition.Preview)  # or preview
        self.composition.setPrintResolution(self.page_dpi)
        self.composition.setPrintAsRaster(True)

    def make_pdf(self, filename):
        """Generate the printout for our final map.

        :param filename: Path on the file system to which the pdf should be
            saved. If None, a generated file name will be used.
        :type filename: str

        :returns: File name of the output file (equivalent to filename if
                provided).
        :rtype: str
        """
        LOGGER.debug('InaSAFE Map printToPdf called')
        if filename is None:
            map_pdf_path = unique_filename(prefix='report',
                                           suffix='.pdf',
                                           dir=temp_dir())
        else:
            # We need to cast to python string in case we receive a QString
            map_pdf_path = str(filename)

        self.load_template()
        self.composition.exportAsPDF(map_pdf_path)
        return map_pdf_path

    def map_title(self):
        """Get the map title from the layer keywords if possible.

        :returns: None on error, otherwise the title.
        :rtype: None, str
        """
        LOGGER.debug('InaSAFE Map getMapTitle called')
        try:
            title = self.keyword_io.read_keywords(self.layer, 'map_title')
            return title
        except KeywordNotFoundError:
            return None
        except Exception:
            return None

    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 myLegendAttribute in legend_attribute_list:
            # noinspection PyBroadException
            try:
                legend_attribute_dict[myLegendAttribute] = \
                    self.keyword_io.read_keywords(
                        self.layer, myLegendAttribute)
            except KeywordNotFoundError:
                pass
            except Exception:
                pass
        return legend_attribute_dict

    def load_template(self):
        """Load a QgsComposer map from a template.
        """
        self.setup_composition()

        template_file = QtCore.QFile(self.template)
        template_file.open(QtCore.QIODevice.ReadOnly | QtCore.QIODevice.Text)
        template_content = template_file.readAll()
        template_file.close()

        document = QtXml.QDomDocument()
        document.setContent(template_content)

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

        title = self.map_title()
        if not title:
            title = ''

        substitution_map = {
            'impact-title': title,
            'date': date,
            'time': time,
            'safe-version': version,
            'disclaimer': self.disclaimer
        }
        LOGGER.debug(substitution_map)
        load_ok = self.composition.loadFromTemplate(document, substitution_map)
        if not load_ok:
            raise ReportCreationError(
                self.tr('Error loading template %s') % self.template)

        self.page_width = self.composition.paperWidth()
        self.page_height = self.composition.paperHeight()

        # set InaSAFE logo
        image = self.composition.getComposerItemById('safe-logo')
        if image is not None:
            image.setPictureFile(self.safe_logo)
        else:
            raise ReportCreationError(
                self.tr('Image "safe-logo" could not be found'))

        # set north arrow
        image = self.composition.getComposerItemById('north-arrow')
        if image is not None:
            image.setPictureFile(self.north_arrow)
        else:
            raise ReportCreationError(
                self.tr('Image "north arrow" could not be found'))

        # set organisation logo
        image = self.composition.getComposerItemById('organisation-logo')
        if image is not None:
            image.setPictureFile(self.org_logo)
        else:
            raise ReportCreationError(
                self.tr('Image "organisation-logo" could not be found'))

        # 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)
        else:
            LOGGER.debug('"impact-report" element not found.')

        # 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
            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)
        else:
            raise ReportCreationError(
                self.tr('Map "impact-map" could not be found'))

        legend = self.composition.getComposerItemById('impact-legend')
        legend_attributes = self.map_legend_attributes()
        LOGGER.debug(legend_attributes)
        #legend_notes = mapLegendAttributes.get('legend_notes', None)
        #legend_units = mapLegendAttributes.get('legend_units', None)
        legend_title = legend_attributes.get('legend_title', None)

        symbol_count = 1
        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)
        legend.updateLegend()

        # remove from legend all layers, except impact one
        model = legend.model()
        if model.rowCount() > 0 and model.columnCount() > 0:
            impact_item = model.findItems(self.layer.name())[0]
            row = impact_item.index().row()
            model.removeRows(row + 1, model.rowCount() - row)
            if row > 0:
                model.removeRows(0, row)
Пример #35
0
class AggregatorTest(unittest.TestCase):
    """Test the InaSAFE GUI"""

    def setUp(self):
        """Fixture run before all tests"""

        self.maxDiff = None  # show full diff for assert errors

        os.environ['LANG'] = 'en'
        DOCK.showOnlyVisibleLayersFlag = True
        load_standard_layers()
        DOCK.cboHazard.setCurrentIndex(0)
        DOCK.cboExposure.setCurrentIndex(0)
        DOCK.cboFunction.setCurrentIndex(0)
        DOCK.runInThreadFlag = False
        DOCK.showOnlyVisibleLayersFlag = False
        DOCK.setLayerNameFromTitleFlag = False
        DOCK.zoomToImpactFlag = False
        DOCK.hideExposureFlag = False
        DOCK.showIntermediateLayers = False
        set_jakarta_extent()

        self.keywordIO = KeywordIO()
        self.defaults = defaults()

    def test_cboAggregationLoadedProject(self):
        """Aggregation combo changes properly according loaded layers"""
        myLayerList = [DOCK.tr('Entire area'),
                       DOCK.tr('kabupaten jakarta singlepart')]
        currentLayers = [DOCK.cboAggregation.itemText(i) for i in range(
            DOCK.cboAggregation.count())]

        myMessage = ('The aggregation combobox should have:\n %s \nFound: %s'
                     % (myLayerList, currentLayers))
        self.assertEquals(currentLayers, myLayerList, myMessage)

    def test_checkAggregationAttributeInKW(self):
        """Aggregation attribute is chosen correctly when present
            in kezwords."""
        myRunButton = DOCK.pbnRunStop
        myAttrKey = defaults('AGGR_ATTR_KEY')

        # with KAB_NAME aggregation attribute defined in .keyword using
        # kabupaten_jakarta_singlepart.shp
        myResult, myMessage = setup_scenario(
            DOCK,
            hazard='A flood in Jakarta like in 2007',
            exposure='People',
            function='Need evacuation',
            function_id='Flood Evacuation Function',
            aggregation_layer='kabupaten jakarta singlepart',
            aggregation_enabled_flag=True)
        assert myResult, myMessage
        # Press RUN
        # noinspection PyCallByClass,PyTypeChecker
        QTest.mouseClick(myRunButton, QtCore.Qt.LeftButton)
        DOCK.runtimeKeywordsDialog.accept()
        myAttribute = DOCK.aggregator.attributes[myAttrKey]
        myMessage = ('The aggregation should be KAB_NAME. Found: %s' %
                     myAttribute)
        self.assertEqual(myAttribute, 'KAB_NAME', myMessage)

    def test_checkAggregationAttribute1Attr(self):
        """Aggregation attribute is chosen correctly when there is only
        one attr available."""
        myRunButton = DOCK.pbnRunStop
        myFileList = ['kabupaten_jakarta_singlepart_1_good_attr.shp']
        #add additional layers
        load_layers(myFileList, clear_flag=False, data_directory=TESTDATA)
        myAttrKey = defaults('AGGR_ATTR_KEY')

        # with 1 good aggregation attribute using
        # kabupaten_jakarta_singlepart_1_good_attr.shp
        myResult, myMessage = setup_scenario(
            DOCK,
            hazard='A flood in Jakarta like in 2007',
            exposure='People',
            function='Need evacuation',
            function_id='Flood Evacuation Function',
            aggregation_layer='kabupaten jakarta singlepart 1 good attr')
        assert myResult, myMessage
        # Press RUN
        # noinspection PyCallByClass,PyTypeChecker
        QTest.mouseClick(myRunButton, QtCore.Qt.LeftButton)
        DOCK.runtimeKeywordsDialog.accept()
        print myAttrKey
        print DOCK.aggregator.attributes
        myAttribute = DOCK.aggregator.attributes[myAttrKey]
        myMessage = ('The aggregation should be KAB_NAME. Found: %s' %
                     myAttribute)
        self.assertEqual(myAttribute, 'KAB_NAME', myMessage)

    def test_checkAggregationAttributeNoAttr(self):
        """Aggregation attribute is chosen correctly when there is no
        attr available."""

        myRunButton = DOCK.pbnRunStop
        myFileList = ['kabupaten_jakarta_singlepart_0_good_attr.shp']
        #add additional layers
        load_layers(myFileList, clear_flag=False, data_directory=TESTDATA)
        myAttrKey = defaults('AGGR_ATTR_KEY')
        # with no good aggregation attribute using
        # kabupaten_jakarta_singlepart_0_good_attr.shp
        myResult, myMessage = setup_scenario(
            DOCK,
            hazard='A flood in Jakarta like in 2007',
            exposure='People',
            function='Need evacuation',
            function_id='Flood Evacuation Function',
            aggregation_layer='kabupaten jakarta singlepart 0 good attr')
        assert myResult, myMessage
        # Press RUN
        # noinspection PyCallByClass,PyTypeChecker
        QTest.mouseClick(myRunButton, QtCore.Qt.LeftButton)
        DOCK.runtimeKeywordsDialog.accept()
        myAttribute = DOCK.aggregator.attributes[myAttrKey]
        myMessage = ('The aggregation should be None. Found: %s' %
                     myAttribute)
        assert myAttribute is None, myMessage

    def test_checkAggregationAttributeNoneAttr(self):
        """Aggregation attribute is chosen correctly when there None in the
            kezwords"""

        myRunButton = DOCK.pbnRunStop
        myFileList = ['kabupaten_jakarta_singlepart_with_None_keyword.shp']
        #add additional layers
        load_layers(myFileList, clear_flag=False, data_directory=TESTDATA)
        myAttrKey = defaults('AGGR_ATTR_KEY')
        # with None aggregation attribute defined in .keyword using
        # kabupaten_jakarta_singlepart_with_None_keyword.shp
        myResult, myMessage = setup_scenario(
            DOCK,
            hazard='A flood in Jakarta like in 2007',
            exposure='People',
            function='Need evacuation',
            function_id='Flood Evacuation Function',
            aggregation_layer='kabupaten jakarta singlepart with None '
                                'keyword')
        assert myResult, myMessage
        # Press RUN
        # noinspection PyCallByClass,PyTypeChecker
        QTest.mouseClick(myRunButton, QtCore.Qt.LeftButton)
        DOCK.runtimeKeywordsDialog.accept()
        myAttribute = DOCK.aggregator.attributes[myAttrKey]
        myMessage = ('The aggregation should be None. Found: %s' % myAttribute)
        assert myAttribute is None, myMessage

    def test_preprocessing(self):
        """Preprocessing results are correct.

        TODO - this needs to be fixed post dock refactor.

        """

        # See qgis project in test data: vector_preprocessing_test.qgs
        #add additional layers
        myFileList = ['jakarta_crosskabupaten_polygons.shp']
        load_layers(myFileList, clear_flag=False, data_directory=TESTDATA)
        myFileList = ['kabupaten_jakarta.shp']
        load_layers(myFileList, clear_flag=False, data_directory=BOUNDDATA)

        myRunButton = DOCK.pbnRunStop

        myResult, myMessage = setup_scenario(
            DOCK,
            hazard='jakarta_crosskabupaten_polygons',
            exposure='People',
            function='Need evacuation',
            function_id='Flood Evacuation Function Vector Hazard',
            aggregation_layer='kabupaten jakarta',
            aggregation_enabled_flag=True)
        assert myResult, myMessage

        # Enable on-the-fly reprojection
        set_canvas_crs(GEOCRS, True)
        set_jakarta_extent()
        # Press RUN
        # noinspection PyTypeChecker,PyCallByClass
        QTest.mouseClick(myRunButton, QtCore.Qt.LeftButton)
        DOCK.runtimeKeywordsDialog.accept()

        myExpectedFeatureCount = 20
        myMessage = ('The preprocessing should have generated %s features, '
                     'found %s' % (myExpectedFeatureCount,
                                   DOCK.aggregator.preprocessedFeatureCount))
        self.assertEqual(myExpectedFeatureCount,
                         DOCK.aggregator.preprocessedFeatureCount,
                         myMessage)

    def _aggregate(self, myImpactLayer, myExpectedResults):
        myAggregationLayer = QgsVectorLayer(
            os.path.join(BOUNDDATA, 'kabupaten_jakarta.shp'),
            'test aggregation',
            'ogr')
        # create a copy of aggregation layer
        myGeoExtent = extent_to_geo_array(
            myAggregationLayer.extent(),
            myAggregationLayer.crs())

        myAggrAttribute = self.keywordIO.read_keywords(
            myAggregationLayer, self.defaults['AGGR_ATTR_KEY'])
        # noinspection PyArgumentEqualDefault
        myAggregationLayer = clip_layer(
            layer=myAggregationLayer,
            extent=myGeoExtent,
            explode_flag=True,
            explode_attribute=myAggrAttribute)

        myAggregator = Aggregator(None, myAggregationLayer)
        # setting up
        myAggregator.isValid = True
        myAggregator.layer = myAggregationLayer
        myAggregator.safeLayer = safe_read_layer(
            str(myAggregator.layer.source()))
        myAggregator.aoiMode = False
        myAggregator.aggregate(myImpactLayer)

        myProvider = myAggregator.layer.dataProvider()
        myProvider.select(myProvider.attributeIndexes())
        myFeature = QgsFeature()
        myResults = []

        while myProvider.nextFeature(myFeature):
            myFeatureResults = {}
            myAtMap = myFeature.attributeMap()
            for (k, attr) in myAtMap.iteritems():
                myFeatureResults[k] = str(attr.toString())
            myResults.append(myFeatureResults)

        self.assertEqual(myExpectedResults, myResults)

    def test_aggregate_raster_impact(self):
        """Check aggregation on raster impact.

        Created from loadStandardLayers.qgs with:
        - a flood in Jakarta like in 2007
        - Penduduk Jakarta
        - need evacuation
        - kabupaten_jakarta_singlepart.shp

        """
        myImpactLayer = Raster(
            data=os.path.join(TESTDATA, 'aggregation_test_impact_raster.tif'),
            name='test raster impact')

        myExpectedResults = [
            {0: 'JAKARTA BARAT',
             1: '50540',
             2: '12015061.8769531',
             3: '237.733713433976',
             4: '50539',
             5: '12015061.8769531',
             6: '237.738417399496'},
            {0: 'JAKARTA PUSAT',
             1: '19492',
             2: '2943702.11401367',
             3: '151.021040119725',
             4: '19492',
             5: '2945658.12207031',
             6: '151.121389394126'},
            {0: 'JAKARTA SELATAN',
             1: '57367',
             2: '1645498.26947021',
             3: '28.6837078716024',
             4: '57372',
             5: '1643522.39849854',
             6: '28.6467684323108'},
            {0: 'JAKARTA UTARA',
             1: '55004',
             2: '11332095.7334595',
             3: '206.023120745027',
             4: '54998',
             5: '11330910.4882202',
             6: '206.024046114772'},
            {0: 'JAKARTA TIMUR',
             1: '73949',
             2: '10943934.3182373',
             3: '147.992999475819',
             4: '73944',
             5: '10945062.4354248',
             6: '148.018262947971'}]

        self._aggregate(myImpactLayer, myExpectedResults)

    def test_aggregate_vector_impact(self):
        """Test aggregation results on a vector layer.
        created from loadStandardLayers.qgs with:
        - a flood in Jakarta like in 2007
        - Essential buildings
        - be flodded
        - kabupaten_jakarta_singlepart.shp
        """
        myImpactLayer = Vector(
            data=os.path.join(TESTDATA, 'aggregation_test_impact_vector.shp'),
            name='test vector impact')

        myExpectedResults = [
            {0: 'JAKARTA BARAT', 1: '87'},
            {0: 'JAKARTA PUSAT', 1: '117'},
            {0: 'JAKARTA SELATAN', 1: '22'},
            {0: 'JAKARTA UTARA', 1: '286'},
            {0: 'JAKARTA TIMUR', 1: '198'}
        ]
        # self._aggregate(myImpactLayer, myExpectedResults)

        myImpactLayer = Vector(
            data=TESTDATA + '/aggregation_test_impact_vector_small.shp',
            name='test vector impact')

        myExpectedResults = [
            {0: 'JAKARTA BARAT', 1: '87'},
            {0: 'JAKARTA PUSAT', 1: '117'},
            {0: 'JAKARTA SELATAN', 1: '22'},
            {0: 'JAKARTA UTARA', 1: '286'},
            {0: 'JAKARTA TIMUR', 1: '198'}
        ]

        # TODO (MB) enable this
        self._aggregate(myImpactLayer, myExpectedResults)
Пример #36
0
class Map():
    """A class for creating a map."""
    def __init__(self, iface):
        """Constructor for the Map class.

        :param iface: Reference to the QGIS iface object.
        :type iface: QgsAppInterface
        """
        LOGGER.debug('InaSAFE Map class initialised')
        self.iface = iface
        self.layer = iface.activeLayer()
        self.keywordIO = KeywordIO()
        self.printer = None
        self.composition = None
        self.legend = None
        self.pageWidth = 210  # width in mm
        self.pageHeight = 297  # height in mm
        self.pageDpi = 300.0
        self.pageMargin = 10  # margin in mm
        self.verticalSpacing = 1  # vertical spacing between elements
        self.showFramesFlag = False  # intended for debugging use only
        # make a square map where width = height = page width
        self.mapHeight = self.pageWidth - (self.pageMargin * 2)
        self.mapWidth = self.mapHeight
        self.disclaimer = self.tr('InaSAFE has been jointly developed by'
                                  ' BNPB, AusAid & the World Bank')

    def tr(self, string):
        """We implement this since we do not inherit QObject.

        :param string: String for translation.
        :type string: QString, str

        :returns: Translated version of theString.
        :rtype: QString
        """
        # noinspection PyCallByClass,PyTypeChecker,PyArgumentList
        return QtCore.QCoreApplication.translate('Map', string)

    def set_impact_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

    def setup_composition(self):
        """Set up the composition ready for drawing elements onto it."""
        LOGGER.debug('InaSAFE Map setupComposition called')
        myCanvas = self.iface.mapCanvas()
        myRenderer = myCanvas.mapRenderer()
        self.composition = QgsComposition(myRenderer)
        self.composition.setPlotStyle(QgsComposition.Print)  # or preview
        self.composition.setPaperSize(self.pageWidth, self.pageHeight)
        self.composition.setPrintResolution(self.pageDpi)
        self.composition.setPrintAsRaster(True)

    def compose_map(self):
        """Place all elements on the map ready for printing."""
        self.setup_composition()
        # Keep track of our vertical positioning as we work our way down
        # the page placing elements on it.
        myTopOffset = self.pageMargin
        self.draw_logo(myTopOffset)
        myLabelHeight = self.draw_title(myTopOffset)
        # Update the map offset for the next row of content
        myTopOffset += myLabelHeight + self.verticalSpacing
        myComposerMap = self.draw_map(myTopOffset)
        self.draw_scalebar(myComposerMap, myTopOffset)
        # Update the top offset for the next horizontal row of items
        myTopOffset += self.mapHeight + self.verticalSpacing - 1
        myImpactTitleHeight = self.draw_impact_title(myTopOffset)
        # Update the top offset for the next horizontal row of items
        if myImpactTitleHeight:
            myTopOffset += myImpactTitleHeight + self.verticalSpacing + 2
        self.draw_legend(myTopOffset)
        self.draw_host_and_time(myTopOffset)
        self.draw_disclaimer()

    def render(self):
        """Render the map composition to an image and save that to disk.

        :returns: A three-tuple of:
            * str: myImagePath - absolute path to png of rendered map
            * QImage: myImage - in memory copy of rendered map
            * QRectF: myTargetArea - dimensions of rendered map
        :rtype: tuple
        """
        LOGGER.debug('InaSAFE Map renderComposition called')
        # NOTE: we ignore self.composition.printAsRaster() and always rasterise
        myWidth = int(self.pageDpi * self.pageWidth / 25.4)
        myHeight = int(self.pageDpi * self.pageHeight / 25.4)
        myImage = QtGui.QImage(QtCore.QSize(myWidth, myHeight),
                               QtGui.QImage.Format_ARGB32)
        myImage.setDotsPerMeterX(dpi_to_meters(self.pageDpi))
        myImage.setDotsPerMeterY(dpi_to_meters(self.pageDpi))

        # Only works in Qt4.8
        #myImage.fill(QtGui.qRgb(255, 255, 255))
        # Works in older Qt4 versions
        myImage.fill(55 + 255 * 256 + 255 * 256 * 256)
        myImagePainter = QtGui.QPainter(myImage)
        mySourceArea = QtCore.QRectF(0, 0, self.pageWidth,
                                     self.pageHeight)
        myTargetArea = QtCore.QRectF(0, 0, myWidth, myHeight)
        self.composition.render(myImagePainter, myTargetArea, mySourceArea)
        myImagePainter.end()
        myImagePath = unique_filename(prefix='mapRender_',
                                      suffix='.png',
                                      dir=temp_dir())
        myImage.save(myImagePath)
        return myImagePath, myImage, myTargetArea

    def make_pdf(self, filename):
        """Generate the printout for our final map.

        :param filename: Path on the file system to which the pdf should be
            saved. If None, a generated file name will be used.
        :type filename: str

        :returns: File name of the output file (equivalent to filename if
                provided).
        :rtype: str
        """
        LOGGER.debug('InaSAFE Map printToPdf called')
        if filename is None:
            myMapPdfPath = unique_filename(
                prefix='report', suffix='.pdf', dir=temp_dir('work'))
        else:
            # We need to cast to python string in case we receive a QString
            myMapPdfPath = str(filename)

        self.compose_map()
        self.printer = setup_printer(myMapPdfPath)
        _, myImage, myRectangle = self.render()
        myPainter = QtGui.QPainter(self.printer)
        myPainter.drawImage(myRectangle, myImage, myRectangle)
        myPainter.end()
        return myMapPdfPath

    def draw_logo(self, top_offset):
        """Add a picture containing the logo to the map top left corner

        :param top_offset: Vertical offset at which the logo should be drawn.
        :type top_offset: int
        """
        myLogo = QgsComposerPicture(self.composition)
        myLogo.setPictureFile(':/plugins/inasafe/bnpb_logo.png')
        myLogo.setItemPosition(self.pageMargin, top_offset, 10, 10)
        myLogo.setFrameEnabled(self.showFramesFlag)
        myLogo.setZValue(1)  # To ensure it overlays graticule markers
        self.composition.addItem(myLogo)

    def draw_title(self, top_offset):
        """Add a title to the composition.

        :param top_offset: Vertical offset at which the logo should be drawn.
        :type top_offset: int

        :returns: The height of the label as rendered.
        :rtype: float
        """
        LOGGER.debug('InaSAFE Map drawTitle called')
        myFontSize = 14
        myFontWeight = QtGui.QFont.Bold
        myItalicsFlag = False
        myFont = QtGui.QFont('verdana',
                             myFontSize,
                             myFontWeight,
                             myItalicsFlag)
        myLabel = QgsComposerLabel(self.composition)
        myLabel.setFont(myFont)
        myHeading = self.tr('InaSAFE - Indonesia Scenario Assessment'
                            ' for Emergencies')
        myLabel.setText(myHeading)
        myLabel.adjustSizeToText()
        myLabelHeight = 10.0  # determined using qgis map composer
        myLabelWidth = 170.0   # item - position and size...option
        myLeftOffset = self.pageWidth - self.pageMargin - myLabelWidth
        myLabel.setItemPosition(myLeftOffset,
                                top_offset - 2,  # -2 to push it up a little
                                myLabelWidth,
                                myLabelHeight,
                                )
        myLabel.setFrameEnabled(self.showFramesFlag)
        self.composition.addItem(myLabel)
        return myLabelHeight

    def draw_map(self, top_offset):
        """Add a map to the composition and return the composer map instance.

        :param top_offset: Vertical offset at which the logo should be drawn.
        :type top_offset: int

        :returns: The composer map.
        :rtype: QgsComposerMap
        """
        LOGGER.debug('InaSAFE Map drawMap called')
        myMapWidth = self.mapWidth
        myComposerMap = QgsComposerMap(
            self.composition,
            self.pageMargin,
            top_offset,
            myMapWidth,
            self.mapHeight)
        #myExtent = self.iface.mapCanvas().extent()
        # The dimensions of the map canvas and the print composer map may
        # differ. So we set the map composer extent using the canvas and
        # then defer to the map canvas's map extents thereafter
        # Update: disabled as it results in a rectangular rather than
        # square map
        #myComposerMap.setNewExtent(myExtent)
        myComposerExtent = myComposerMap.extent()
        # Recenter the composer map on the center of the canvas
        # Note that since the composer map is square and the canvas may be
        # arbitrarily shaped, we center based on the longest edge
        myCanvasExtent = self.iface.mapCanvas().extent()
        myWidth = myCanvasExtent.width()
        myHeight = myCanvasExtent.height()
        myLongestLength = myWidth
        if myWidth < myHeight:
            myLongestLength = myHeight
        myHalfLength = myLongestLength / 2
        myCenter = myCanvasExtent.center()
        myMinX = myCenter.x() - myHalfLength
        myMaxX = myCenter.x() + myHalfLength
        myMinY = myCenter.y() - myHalfLength
        myMaxY = myCenter.y() + myHalfLength
        mySquareExtent = QgsRectangle(myMinX, myMinY, myMaxX, myMaxY)
        myComposerMap.setNewExtent(mySquareExtent)

        myComposerMap.setGridEnabled(True)
        myNumberOfSplits = 5
        # .. todo:: Write logic to adjust precision so that adjacent tick marks
        #    always have different displayed values
        myPrecision = 2
        myXInterval = myComposerExtent.width() / myNumberOfSplits
        myComposerMap.setGridIntervalX(myXInterval)
        myYInterval = myComposerExtent.height() / myNumberOfSplits
        myComposerMap.setGridIntervalY(myYInterval)
        myComposerMap.setGridStyle(QgsComposerMap.Cross)
        myCrossLengthMM = 1
        myComposerMap.setCrossLength(myCrossLengthMM)
        myComposerMap.setZValue(0)  # To ensure it does not overlay logo
        myFontSize = 6
        myFontWeight = QtGui.QFont.Normal
        myItalicsFlag = False
        myFont = QtGui.QFont(
            'verdana',
            myFontSize,
            myFontWeight,
            myItalicsFlag)
        myComposerMap.setGridAnnotationFont(myFont)
        myComposerMap.setGridAnnotationPrecision(myPrecision)
        myComposerMap.setShowGridAnnotation(True)
        myComposerMap.setGridAnnotationDirection(
            QgsComposerMap.BoundaryDirection, QgsComposerMap.Top)
        self.composition.addItem(myComposerMap)
        self.draw_graticule_mask(top_offset)
        return myComposerMap

    def draw_graticule_mask(self, top_offset):
        """A helper function to mask out graticule labels.

         It will hide labels on the right side by over painting a white
         rectangle with white border on them. **kludge**

        :param top_offset: Vertical offset at which the logo should be drawn.
        :type top_offset: int
        """
        LOGGER.debug('InaSAFE Map drawGraticuleMask called')
        myLeftOffset = self.pageMargin + self.mapWidth
        myRect = QgsComposerShape(myLeftOffset + 0.5,
                                  top_offset,
                                  self.pageWidth - myLeftOffset,
                                  self.mapHeight + 1,
                                  self.composition)

        myRect.setShapeType(QgsComposerShape.Rectangle)
        myPen = QtGui.QPen()
        myPen.setColor(QtGui.QColor(0, 0, 0))
        myPen.setWidthF(0.1)
        myRect.setPen(myPen)
        myRect.setBackgroundColor(QtGui.QColor(255, 255, 255))
        myRect.setTransparency(100)
        #myRect.setLineWidth(0.1)
        #myRect.setFrameEnabled(False)
        #myRect.setOutlineColor(QtGui.QColor(255, 255, 255))
        #myRect.setFillColor(QtGui.QColor(255, 255, 255))
        #myRect.setOpacity(100)
        # These two lines seem superfluous but are needed
        myBrush = QtGui.QBrush(QtGui.QColor(255, 255, 255))
        myRect.setBrush(myBrush)
        self.composition.addItem(myRect)

    def draw_native_scalebar(self, composer_map, top_offset):
        """Draw a scale bar using QGIS' native drawing.

        In the case of geographic maps, scale will be in degrees, not km.

        :param composer_map: Composer map on which to draw the scalebar.
        :type composer_map: QgsComposerMap

        :param top_offset: Vertical offset at which the logo should be drawn.
        :type top_offset: int
        """
        LOGGER.debug('InaSAFE Map drawNativeScaleBar called')
        myScaleBar = QgsComposerScaleBar(self.composition)
        myScaleBar.setStyle('Numeric')  # optionally modify the style
        myScaleBar.setComposerMap(composer_map)
        myScaleBar.applyDefaultSize()
        myScaleBarHeight = myScaleBar.boundingRect().height()
        myScaleBarWidth = myScaleBar.boundingRect().width()
        # -1 to avoid overlapping the map border
        myScaleBar.setItemPosition(
            self.pageMargin + 1,
            top_offset + self.mapHeight - (myScaleBarHeight * 2),
            myScaleBarWidth,
            myScaleBarHeight)
        myScaleBar.setFrameEnabled(self.showFramesFlag)
        # Disabled for now
        #self.composition.addItem(myScaleBar)

    def draw_scalebar(self, composer_map, top_offset):
        """Add a numeric scale to the bottom left of the map.

        We draw the scale bar manually because QGIS does not yet support
        rendering a scale bar for a geographic map in km.

        .. seealso:: :meth:`drawNativeScaleBar`

        :param composer_map: Composer map on which to draw the scalebar.
        :type composer_map: QgsComposerMap

        :param top_offset: Vertical offset at which the logo should be drawn.
        :type top_offset: int
        """
        LOGGER.debug('InaSAFE Map drawScaleBar called')
        myCanvas = self.iface.mapCanvas()
        myRenderer = myCanvas.mapRenderer()
        #
        # Add a linear map scale
        #
        myDistanceArea = QgsDistanceArea()
        myDistanceArea.setSourceCrs(myRenderer.destinationCrs().srsid())
        myDistanceArea.setEllipsoidalMode(True)
        # Determine how wide our map is in km/m
        # Starting point at BL corner
        myComposerExtent = composer_map.extent()
        myStartPoint = QgsPoint(myComposerExtent.xMinimum(),
                                myComposerExtent.yMinimum())
        # Ending point at BR corner
        myEndPoint = QgsPoint(myComposerExtent.xMaximum(),
                              myComposerExtent.yMinimum())
        myGroundDistance = myDistanceArea.measureLine(myStartPoint, myEndPoint)
        # Get the equivalent map distance per page mm
        myMapWidth = self.mapWidth
        # How far is 1mm on map on the ground in meters?
        myMMToGroundDistance = myGroundDistance / myMapWidth
        #print 'MM:', myMMDistance
        # How long we want the scale bar to be in relation to the map
        myScaleBarToMapRatio = 0.5
        # How many divisions the scale bar should have
        myTickCount = 5
        myScaleBarWidthMM = myMapWidth * myScaleBarToMapRatio
        myPrintSegmentWidthMM = myScaleBarWidthMM / myTickCount
        # Segment width in real world (m)
        # We apply some logic here so that segments are displayed in meters
        # if each segment is less that 1000m otherwise km. Also the segment
        # lengths are rounded down to human looking numbers e.g. 1km not 1.1km
        myUnits = ''
        myGroundSegmentWidth = myPrintSegmentWidthMM * myMMToGroundDistance
        if myGroundSegmentWidth < 1000:
            myUnits = 'm'
            myGroundSegmentWidth = round(myGroundSegmentWidth)
            # adjust the segment width now to account for rounding
            myPrintSegmentWidthMM = myGroundSegmentWidth / myMMToGroundDistance
        else:
            myUnits = 'km'
            # Segment with in real world (km)
            myGroundSegmentWidth = round(myGroundSegmentWidth / 1000)
            myPrintSegmentWidthMM = ((myGroundSegmentWidth * 1000) /
                                     myMMToGroundDistance)
        # Now adjust the scalebar width to account for rounding
        myScaleBarWidthMM = myTickCount * myPrintSegmentWidthMM

        #print "SBWMM:", myScaleBarWidthMM
        #print "SWMM:", myPrintSegmentWidthMM
        #print "SWM:", myGroundSegmentWidthM
        #print "SWKM:", myGroundSegmentWidthKM
        # start drawing in line segments
        myScaleBarHeight = 5  # mm
        myLineWidth = 0.3  # mm
        myInsetDistance = 7  # how much to inset the scalebar into the map by
        myScaleBarX = self.pageMargin + myInsetDistance
        myScaleBarY = (
            top_offset + self.mapHeight - myInsetDistance -
            myScaleBarHeight)  # mm

        # Draw an outer background box - shamelessly hardcoded buffer
        myRect = QgsComposerShape(myScaleBarX - 4,  # left edge
                                  myScaleBarY - 3,  # top edge
                                  myScaleBarWidthMM + 13,  # right edge
                                  myScaleBarHeight + 6,  # bottom edge
                                  self.composition)

        myRect.setShapeType(QgsComposerShape.Rectangle)
        myPen = QtGui.QPen()
        myPen.setColor(QtGui.QColor(255, 255, 255))
        myPen.setWidthF(myLineWidth)
        myRect.setPen(myPen)
        #myRect.setLineWidth(myLineWidth)
        myRect.setFrameEnabled(False)
        myBrush = QtGui.QBrush(QtGui.QColor(255, 255, 255))
        # workaround for missing setTransparentFill missing from python api
        myRect.setBrush(myBrush)
        self.composition.addItem(myRect)
        # Set up the tick label font
        myFontWeight = QtGui.QFont.Normal
        myFontSize = 6
        myItalicsFlag = False
        myFont = QtGui.QFont('verdana',
                             myFontSize,
                             myFontWeight,
                             myItalicsFlag)
        # Draw the bottom line
        myUpshift = 0.3  # shift the bottom line up for better rendering
        myRect = QgsComposerShape(myScaleBarX,
                                  myScaleBarY + myScaleBarHeight - myUpshift,
                                  myScaleBarWidthMM,
                                  0.1,
                                  self.composition)

        myRect.setShapeType(QgsComposerShape.Rectangle)
        myPen = QtGui.QPen()
        myPen.setColor(QtGui.QColor(255, 255, 255))
        myPen.setWidthF(myLineWidth)
        myRect.setPen(myPen)
        #myRect.setLineWidth(myLineWidth)
        myRect.setFrameEnabled(False)
        self.composition.addItem(myRect)

        # Now draw the scalebar ticks
        for myTickCountIterator in range(0, myTickCount + 1):
            myDistanceSuffix = ''
            if myTickCountIterator == myTickCount:
                myDistanceSuffix = ' ' + myUnits
            myRealWorldDistance = ('%.0f%s' %
                                   (myTickCountIterator *
                                    myGroundSegmentWidth,
                                    myDistanceSuffix))
            #print 'RW:', myRealWorldDistance
            myMMOffset = myScaleBarX + (myTickCountIterator *
                                        myPrintSegmentWidthMM)
            #print 'MM:', myMMOffset
            myTickHeight = myScaleBarHeight / 2
            # Lines are not exposed by the api yet so we
            # bodge drawing lines using rectangles with 1px height or width
            myTickWidth = 0.1  # width or rectangle to be drawn
            myUpTickLine = QgsComposerShape(
                myMMOffset,
                myScaleBarY + myScaleBarHeight - myTickHeight,
                myTickWidth,
                myTickHeight,
                self.composition)

            myUpTickLine.setShapeType(QgsComposerShape.Rectangle)
            myPen = QtGui.QPen()
            myPen.setWidthF(myLineWidth)
            myUpTickLine.setPen(myPen)
            #myUpTickLine.setLineWidth(myLineWidth)
            myUpTickLine.setFrameEnabled(False)
            self.composition.addItem(myUpTickLine)
            #
            # Add a tick label
            #
            myLabel = QgsComposerLabel(self.composition)
            myLabel.setFont(myFont)
            myLabel.setText(myRealWorldDistance)
            myLabel.adjustSizeToText()
            myLabel.setItemPosition(
                myMMOffset - 3,
                myScaleBarY - myTickHeight)
            myLabel.setFrameEnabled(self.showFramesFlag)
            self.composition.addItem(myLabel)

    def draw_impact_title(self, top_offset):
        """Draw the map subtitle - obtained from the impact layer keywords.

        :param top_offset: Vertical offset at which the logo should be drawn.
        :type top_offset: int

        :returns: The height of the label as rendered.
        :rtype: float
        """
        LOGGER.debug('InaSAFE Map drawImpactTitle called')
        myTitle = self.map_title()
        if myTitle is None:
            myTitle = ''
        myFontSize = 20
        myFontWeight = QtGui.QFont.Bold
        myItalicsFlag = False
        myFont = QtGui.QFont(
            'verdana', myFontSize, myFontWeight, myItalicsFlag)
        myLabel = QgsComposerLabel(self.composition)
        myLabel.setFont(myFont)
        myHeading = myTitle
        myLabel.setText(myHeading)
        myLabelWidth = self.pageWidth - (self.pageMargin * 2)
        myLabelHeight = 12
        myLabel.setItemPosition(
            self.pageMargin, top_offset, myLabelWidth, myLabelHeight)
        myLabel.setFrameEnabled(self.showFramesFlag)
        self.composition.addItem(myLabel)
        return myLabelHeight

    def draw_legend(self, top_offset):
        """Add a legend to the map using our custom legend renderer.

        .. note:: getLegend generates a pixmap in 150dpi so if you set
           the map to a higher dpi it will appear undersized.

        :param top_offset: Vertical offset at which the logo should be drawn.
        :type top_offset: int
        """
        LOGGER.debug('InaSAFE Map drawLegend called')
        mapLegendAttributes = self.map_legend_attributes()
        legendNotes = mapLegendAttributes.get('legend_notes', None)
        legendUnits = mapLegendAttributes.get('legend_units', None)
        legendTitle = mapLegendAttributes.get('legend_title', None)
        LOGGER.debug(mapLegendAttributes)
        myLegend = MapLegend(self.layer, self.pageDpi, legendTitle,
                             legendNotes, legendUnits)
        self.legend = myLegend.get_legend()
        myPicture1 = QgsComposerPicture(self.composition)
        myLegendFilePath = unique_filename(
            prefix='legend', suffix='.png', dir='work')
        self.legend.save(myLegendFilePath, 'PNG')
        myPicture1.setPictureFile(myLegendFilePath)
        myLegendHeight = points_to_mm(self.legend.height(), self.pageDpi)
        myLegendWidth = points_to_mm(self.legend.width(), self.pageDpi)
        myPicture1.setItemPosition(self.pageMargin,
                                   top_offset,
                                   myLegendWidth,
                                   myLegendHeight)
        myPicture1.setFrameEnabled(False)
        self.composition.addItem(myPicture1)
        os.remove(myLegendFilePath)

    def draw_image(self, theImage, theWidthMM, theLeftOffset, theTopOffset):
        """Helper to draw an image directly onto the QGraphicsScene.
        This is an alternative to using QgsComposerPicture which in
        some cases leaves artifacts under windows.

        The Pixmap will have a transform applied to it so that
        it is rendered with the same resolution as the composition.

        :param theImage: Image that will be rendered to the layout.
        :type theImage: QImage

        :param theWidthMM: Desired width in mm of output on page.
        :type theWidthMM: int

        :param theLeftOffset: Offset from left of page.
        :type theLeftOffset: int

        :param theTopOffset: Offset from top of page.
        :type theTopOffset: int

        :returns: Graphics scene item.
        :rtype: QGraphicsSceneItem
        """
        LOGGER.debug('InaSAFE Map drawImage called')
        myDesiredWidthMM = theWidthMM  # mm
        myDesiredWidthPX = mm_to_points(myDesiredWidthMM, self.pageDpi)
        myActualWidthPX = theImage.width()
        myScaleFactor = myDesiredWidthPX / myActualWidthPX

        LOGGER.debug('%s %s %s' % (
            myScaleFactor, myActualWidthPX, myDesiredWidthPX))
        myTransform = QtGui.QTransform()
        myTransform.scale(myScaleFactor, myScaleFactor)
        myTransform.rotate(0.5)
        # noinspection PyArgumentList
        myItem = self.composition.addPixmap(QtGui.QPixmap.fromImage(theImage))
        myItem.setTransform(myTransform)
        myItem.setOffset(theLeftOffset / myScaleFactor,
                         theTopOffset / myScaleFactor)
        return myItem

    def draw_host_and_time(self, top_offset):
        """Add a note with hostname and time to the composition.

        :param top_offset: Vertical offset at which the logo should be drawn.
        :type top_offset: int
        """
        LOGGER.debug('InaSAFE Map drawDisclaimer called')
        #elapsed_time: 11.612545
        #user: timlinux
        #host_name: ultrabook
        #time_stamp: 2012-10-13_23:10:31
        #myUser = self.keywordIO.readKeywords(self.layer, 'user')
        #myHost = self.keywordIO.readKeywords(self.layer, 'host_name')
        myDateTime = self.keywordIO.read_keywords(self.layer, 'time_stamp')
        myTokens = myDateTime.split('_')
        myDate = myTokens[0]
        myTime = myTokens[1]
        #myElapsedTime = self.keywordIO.readKeywords(self.layer,
        #                                            'elapsed_time')
        #myElapsedTime = humaniseSeconds(myElapsedTime)
        myLongVersion = get_version()
        myTokens = myLongVersion.split('.')
        myVersion = '%s.%s.%s' % (myTokens[0], myTokens[1], myTokens[2])
        myLabelText = self.tr(
            'Date and time of assessment: %s %s\n'
            'Special note: This assessment is a guide - we strongly recommend '
            'that you ground truth the results shown here before deploying '
            'resources and / or personnel.\n'
            'Assessment carried out using InaSAFE release %s (QGIS '
            'plugin version).') % (myDate, myTime, myVersion)
        myFontSize = 6
        myFontWeight = QtGui.QFont.Normal
        myItalicsFlag = True
        myFont = QtGui.QFont('verdana',
                             myFontSize,
                             myFontWeight,
                             myItalicsFlag)
        myLabel = QgsComposerLabel(self.composition)
        myLabel.setFont(myFont)
        myLabel.setText(myLabelText)
        myLabel.adjustSizeToText()
        myLabelHeight = 50.0  # mm determined using qgis map composer
        myLabelWidth = (self.pageWidth / 2) - self.pageMargin
        myLeftOffset = self.pageWidth / 2  # put in right half of page
        myLabel.setItemPosition(myLeftOffset,
                                top_offset,
                                myLabelWidth,
                                myLabelHeight,
                                )
        myLabel.setFrameEnabled(self.showFramesFlag)
        self.composition.addItem(myLabel)

    def draw_disclaimer(self):
        """Add a disclaimer to the composition."""
        LOGGER.debug('InaSAFE Map drawDisclaimer called')
        myFontSize = 10
        myFontWeight = QtGui.QFont.Normal
        myItalicsFlag = True
        myFont = QtGui.QFont('verdana',
                             myFontSize,
                             myFontWeight,
                             myItalicsFlag)
        myLabel = QgsComposerLabel(self.composition)
        myLabel.setFont(myFont)
        myLabel.setText(self.disclaimer)
        myLabel.adjustSizeToText()
        myLabelHeight = 7.0  # mm determined using qgis map composer
        myLabelWidth = self.pageWidth   # item - position and size...option
        myLeftOffset = self.pageMargin
        myTopOffset = self.pageHeight - self.pageMargin
        myLabel.setItemPosition(myLeftOffset,
                                myTopOffset,
                                myLabelWidth,
                                myLabelHeight,
                                )
        myLabel.setFrameEnabled(self.showFramesFlag)
        self.composition.addItem(myLabel)

    def map_title(self):
        """Get the map title from the layer keywords if possible.

        :returns: None on error, otherwise the title.
        :rtype: None, str
        """
        LOGGER.debug('InaSAFE Map getMapTitle called')
        try:
            myTitle = self.keywordIO.read_keywords(self.layer, 'map_title')
            return myTitle
        except KeywordNotFoundError:
            return None
        except Exception:
            return None

    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 getMapLegendAtributes called')
        legendAttributes = ['legend_notes',
                            'legend_units',
                            'legend_title']
        dictLegendAttributes = {}
        for myLegendAttribute in legendAttributes:
            try:
                dictLegendAttributes[myLegendAttribute] = \
                    self.keywordIO.read_keywords(self.layer, myLegendAttribute)
            except KeywordNotFoundError:
                pass
            except Exception:
                pass
        return dictLegendAttributes

    def showComposer(self):
        """Show the composition in a composer view so the user can tweak it.
        """
        myView = QgsComposerView(self.iface.mainWindow())
        myView.show()

    def write_template(self, template_path):
        """Write current composition as a template that can be re-used in QGIS.

        :param template_path: Path to which template should be written.
        :type template_path: str
        """
        myDocument = QtXml.QDomDocument()
        myElement = myDocument.createElement('Composer')
        myDocument.appendChild(myElement)
        self.composition.writeXML(myElement, myDocument)
        myXml = myDocument.toByteArray()
        myFile = file(template_path, 'wb')
        myFile.write(myXml)
        myFile.close()

    def render_template(self, template_path, output_path):
        """Load a QgsComposer map from a template and render it.

        .. note:: THIS METHOD IS EXPERIMENTAL AND CURRENTLY NON FUNCTIONAL

        :param template_path:  Path to the template that should be loaded.
        :type template_path: str

        :param output_path: Path for the output pdf.
        :type output_path: str
        """
        self.setup_composition()

        myResolution = self.composition.printResolution()
        self.printer = setup_printer(
            output_path, resolution=myResolution)
        if self.composition:
            myFile = QtCore.QFile(template_path)
            myDocument = QtXml.QDomDocument()
            myDocument.setContent(myFile, False)  # .. todo:: fix magic param
            myNodeList = myDocument.elementsByTagName('Composer')
            if myNodeList.size() > 0:
                myElement = myNodeList.at(0).toElement()
                self.composition.readXML(myElement, myDocument)
        self.make_pdf(output_path)
Пример #37
0
class MapLegend():
    """A class for creating a map legend."""
    def __init__(
            self,
            layer,
            dpi=300,
            legend_title=None,
            legend_notes=None,
            legend_units=None):
        """Constructor for the Map Legend class.

        :param layer: Layer that the legend should be generated for.
        :type layer: QgsMapLayer, QgsVectorLayer

        :param dpi: DPI for the generated legend image. Defaults to 300 if
            not specified.
        :type dpi: int

        :param legend_title: Title for the legend.
        :type legend_title: str

        :param legend_notes: Notes to display under the title.
        :type legend_notes: str

        :param legend_units: Units for the legend.
        :type legend_units: str
        """
        LOGGER.debug('InaSAFE Map class initialised')
        self.legendImage = None
        self.layer = layer
        # how high each row of the legend should be
        self.legendIncrement = 42
        self.keywordIO = KeywordIO()
        self.legendFontSize = 8
        self.legendWidth = 900
        self.dpi = dpi
        if legend_title is None:
            self.legendTitle = self.tr('Legend')
        else:
            self.legendTitle = legend_title
        self.legendNotes = legend_notes
        self.legendUnits = legend_units

    def tr(self, string):
        """We implement this ourself since we do not inherit QObject.

        :param string: String for translation.
        :type string: QString, str

        :returns: Translated version of string.
        :rtype: QString
        """
        # noinspection PyCallByClass,PyTypeChecker
        return QtCore.QCoreApplication.translate('MapLegend', string)

    def get_legend(self):
        """Create a legend for the classes in the layer.

        .. note: This is a wrapper for raster_legend and vector_legend.

        :raises: InvalidLegendLayer will be raised if a legend cannot be
            created from the layer.
        """
        LOGGER.debug('InaSAFE Map Legend getLegend called')
        if self.layer is None:
            message = self.tr('Unable to make a legend when map generator '
                                'has no layer set.')
            raise LegendLayerError(message)
        try:
            self.keywordIO.read_keywords(self.layer, 'impact_summary')
        except KeywordNotFoundError, e:
            message = self.tr('This layer does not appear to be an impact '
                                'layer. Try selecting an impact layer in the '
                                'QGIS layers list or creating a new impact '
                                'scenario before using the print tool.'
                                '\nMessage: %s' % str(e))
            raise Exception(message)
        if self.layer.type() == QgsMapLayer.VectorLayer:
            return self.vector_legend()
        else:
            return self.raster_legend()
Пример #38
0
class ImpactMergeDialog(QDialog, Ui_ImpactMergeDialogBase):
    """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 = ':/plugins/inasafe/merged_report.qpt'

        # Safe Logo Path
        self.safe_logo_path = ':/plugins/inasafe/inasafe-logo-url.png'

        # Organisation Logo Path
        self.organisation_logo_path = ':/plugins/inasafe/supporters.png'

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

        # The summary report, contains report for each aggregation area
        self.summary_report = {}

        # The html reports and its file path
        self.html_reports = {}

        # 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 context help
        help_button = self.button_box.button(QtGui.QDialogButtonBox.Help)
        help_button.clicked.connect(self.show_help)

        # Show usage info
        self.show_info()
        self.restore_state()

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

        string = header

        heading = m.Heading(self.tr('Impact Layer Merge Tool'), **INFO_STYLE)
        body = self.tr(
            'This tool will merge the outputs from two impact maps for the '
            'same area. The maps must be created using the same aggregation '
            'areas and same hazard. To use:'
        )
        tips = m.BulletedList()
        tips.add(self.tr(
            'Run an impact assessment for an area using aggregation. e.g.'
            'Flood Impact on Buildings aggregated by municipal boundaries.'))
        tips.add(self.tr(
            'Run a second impact assessment for the same area using the same '
            'aggregation. e.g. Flood Impact on People aggregated by '
            'municipal boundaries.'))
        tips.add(self.tr(
            'Open this tool and select each impact layer from the pick lists '
            'provided below.'))
        tips.add(self.tr(
            'Select the aggregation layer that was used to generate the '
            'first and second impact layer.'))
        tips.add(self.tr(
            'Select an output directory.'))
        tips.add(self.tr(
            'Check "Use customized report template" checkbox and select the '
            'report template file if you want to use your own template. Note '
            'that all the map composer components that are needed must be '
            'fulfilled.'))
        tips.add(self.tr(
            'Click OK to generate the per aggregation area combined '
            'summaries.'))
        message = m.Message()
        message.add(heading)
        message.add(body)
        message.add(tips)
        string += message.to_html()
        string += footer

        self.web_view.setHtml(string)

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

    @staticmethod
    def show_help():
        """Load the help text for the dialog."""
        show_context_help('impact_layer_merge_tool')

    @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
        output_directory_url = QUrl.fromLocalFile(self.out_dir)
        #noinspection PyTypeChecker,PyCallByClass
        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
        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()

        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
                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 Layer
        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
        if self.aggregation['layer'] is None:
            self.entire_area_mode = True

    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 = str(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(first_report)
        second_document = minidom.parseString(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)

        # 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]
            if os.path.exists(report_path):
                os.remove(report_path)

    @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":
               {"Detailed Building Type Report":
                   {"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":
                {"Detailed Building Type Report":
                    {"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(), 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.
        """
        # Setup Map Renderer and set all the layer
        renderer = QgsMapRenderer()
        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())

        # Set Layer set to renderer
        renderer.setLayerSet(layer_set)

        # Create composition
        composition = self.load_template(renderer)

        # 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
            map_extent = QgsRectangle(
                fit_min_x, fit_min_y, fit_max_x, fit_max_y)
            composer_map.setNewExtent(map_extent)

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

            # Self.html_reports must have only 1 key value pair
            area_title = list(self.html_reports.keys())[0]

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

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

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

            # Export composition to PDF file
            file_name = '_'.join(area_title.split())
            file_path = '%s.pdf' % file_name
            path = os.path.join(self.out_dir, file_path)
            composition.exportAsPDF(path)
        else:
            # Create atlas composition:
            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, renderer):
        """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 renderer: Map renderer
        :type renderer: QgsMapRenderer

        """
        # Create Composition
        composition = QgsComposition(renderer)

        template_file = QtCore.QFile(self.template_path)
        template_file.open(QtCore.QIODevice.ReadOnly | QtCore.QIODevice.Text)
        template_content = template_file.readAll()
        template_file.close()

        # Create a dom document containing template content
        document = QtXml.QDomDocument()
        document.setContent(template_content)

        # Prepare Map Substitution
        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
        }

        # Load template
        load_status = composition.loadFromTemplate(document, substitution_map)
        if not load_status:
            raise ReportCreationError(
                self.tr('Error loading template %s') %
                self.template_path)

        # Validate all needed composer components
        component_ids = ['impact-map', 'safe-logo', 'summary-report',
                         'aggregation-area', 'map-scale', 'map-legend',
                         'organisation-logo', 'merged-report-table']
        for component_id in component_ids:
            component = composition.getComposerItemById(component_id)
            if component is None:
                raise ReportCreationError(self.tr(
                    'Component %s could not be found' % component_id))

        # Set InaSAFE logo
        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')
        legend.updateLegend()

        return composition
Пример #39
0
class Map():
    """A class for creating a map."""
    def __init__(self, iface):
        """Constructor for the Map class.

        :param iface: Reference to the QGIS iface object.
        :type iface: QgsAppInterface
        """
        LOGGER.debug('InaSAFE Map class initialised')
        self.iface = iface
        self.layer = iface.activeLayer()
        self.keywordIO = KeywordIO()
        self.printer = None
        self.composition = None
        self.legend = None
        self.pageWidth = 210  # width in mm
        self.pageHeight = 297  # height in mm
        self.pageDpi = 300.0
        self.pageMargin = 10  # margin in mm
        self.verticalSpacing = 1  # vertical spacing between elements
        self.showFramesFlag = False  # intended for debugging use only
        # make a square map where width = height = page width
        self.mapHeight = self.pageWidth - (self.pageMargin * 2)
        self.mapWidth = self.mapHeight
        self.disclaimer = self.tr('InaSAFE has been jointly developed by'
                                  ' BNPB, AusAid & the World Bank')

    def tr(self, string):
        """We implement this since we do not inherit QObject.

        :param string: String for translation.
        :type string: QString, str

        :returns: Translated version of theString.
        :rtype: QString
        """
        # noinspection PyCallByClass,PyTypeChecker,PyArgumentList
        return QtCore.QCoreApplication.translate('Map', string)

    def set_impact_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

    def setup_composition(self):
        """Set up the composition ready for drawing elements onto it."""
        LOGGER.debug('InaSAFE Map setupComposition called')
        myCanvas = self.iface.mapCanvas()
        myRenderer = myCanvas.mapRenderer()
        self.composition = QgsComposition(myRenderer)
        self.composition.setPlotStyle(QgsComposition.Print)  # or preview
        self.composition.setPaperSize(self.pageWidth, self.pageHeight)
        self.composition.setPrintResolution(self.pageDpi)
        self.composition.setPrintAsRaster(True)

    def compose_map(self):
        """Place all elements on the map ready for printing."""
        self.setup_composition()
        # Keep track of our vertical positioning as we work our way down
        # the page placing elements on it.
        myTopOffset = self.pageMargin
        self.draw_logo(myTopOffset)
        myLabelHeight = self.draw_title(myTopOffset)
        # Update the map offset for the next row of content
        myTopOffset += myLabelHeight + self.verticalSpacing
        myComposerMap = self.draw_map(myTopOffset)
        self.draw_scalebar(myComposerMap, myTopOffset)
        # Update the top offset for the next horizontal row of items
        myTopOffset += self.mapHeight + self.verticalSpacing - 1
        myImpactTitleHeight = self.draw_impact_title(myTopOffset)
        # Update the top offset for the next horizontal row of items
        if myImpactTitleHeight:
            myTopOffset += myImpactTitleHeight + self.verticalSpacing + 2
        self.draw_legend(myTopOffset)
        self.draw_host_and_time(myTopOffset)
        self.draw_disclaimer()

    def render(self):
        """Render the map composition to an image and save that to disk.

        :returns: A three-tuple of:
            * str: myImagePath - absolute path to png of rendered map
            * QImage: myImage - in memory copy of rendered map
            * QRectF: myTargetArea - dimensions of rendered map
        :rtype: tuple
        """
        LOGGER.debug('InaSAFE Map renderComposition called')
        # NOTE: we ignore self.composition.printAsRaster() and always rasterise
        myWidth = (int)(self.pageDpi * self.pageWidth / 25.4)
        myHeight = (int)(self.pageDpi * self.pageHeight / 25.4)
        myImage = QtGui.QImage(QtCore.QSize(myWidth, myHeight),
                               QtGui.QImage.Format_ARGB32)
        myImage.setDotsPerMeterX(dpi_to_meters(self.pageDpi))
        myImage.setDotsPerMeterY(dpi_to_meters(self.pageDpi))

        # Only works in Qt4.8
        #myImage.fill(QtGui.qRgb(255, 255, 255))
        # Works in older Qt4 versions
        myImage.fill(55 + 255 * 256 + 255 * 256 * 256)
        myImagePainter = QtGui.QPainter(myImage)
        mySourceArea = QtCore.QRectF(0, 0, self.pageWidth, self.pageHeight)
        myTargetArea = QtCore.QRectF(0, 0, myWidth, myHeight)
        self.composition.render(myImagePainter, myTargetArea, mySourceArea)
        myImagePainter.end()
        myImagePath = unique_filename(prefix='mapRender_',
                                      suffix='.png',
                                      dir=temp_dir())
        myImage.save(myImagePath)
        return myImagePath, myImage, myTargetArea

    def make_pdf(self, filename):
        """Generate the printout for our final map.

        :param filename: Path on the file system to which the pdf should be
            saved. If None, a generated file name will be used.
        :type filename: str

        :returns: File name of the output file (equivalent to filename if
                provided).
        :rtype: str
        """
        LOGGER.debug('InaSAFE Map printToPdf called')
        if filename is None:
            myMapPdfPath = unique_filename(prefix='report',
                                           suffix='.pdf',
                                           dir=temp_dir('work'))
        else:
            # We need to cast to python string in case we receive a QString
            myMapPdfPath = str(filename)

        self.compose_map()
        self.printer = setup_printer(myMapPdfPath)
        _, myImage, myRectangle = self.render()
        myPainter = QtGui.QPainter(self.printer)
        myPainter.drawImage(myRectangle, myImage, myRectangle)
        myPainter.end()
        return myMapPdfPath

    def draw_logo(self, top_offset):
        """Add a picture containing the logo to the map top left corner

        :param top_offset: Vertical offset at which the logo should be drawn.
        :type top_offset: int
        """
        myLogo = QgsComposerPicture(self.composition)
        myLogo.setPictureFile(':/plugins/inasafe/bnpb_logo.png')
        myLogo.setItemPosition(self.pageMargin, top_offset, 10, 10)
        if qgis_version() >= 10800:  # 1.8 or newer
            myLogo.setFrameEnabled(self.showFramesFlag)
        else:
            myLogo.setFrame(self.showFramesFlag)
        myLogo.setZValue(1)  # To ensure it overlays graticule markers
        self.composition.addItem(myLogo)

    def draw_title(self, top_offset):
        """Add a title to the composition.

        :param top_offset: Vertical offset at which the logo should be drawn.
        :type top_offset: int

        :returns: The height of the label as rendered.
        :rtype: float
        """
        LOGGER.debug('InaSAFE Map drawTitle called')
        myFontSize = 14
        myFontWeight = QtGui.QFont.Bold
        myItalicsFlag = False
        myFont = QtGui.QFont('verdana', myFontSize, myFontWeight,
                             myItalicsFlag)
        myLabel = QgsComposerLabel(self.composition)
        myLabel.setFont(myFont)
        myHeading = self.tr('InaSAFE - Indonesia Scenario Assessment'
                            ' for Emergencies')
        myLabel.setText(myHeading)
        myLabel.adjustSizeToText()
        myLabelHeight = 10.0  # determined using qgis map composer
        myLabelWidth = 170.0  # item - position and size...option
        myLeftOffset = self.pageWidth - self.pageMargin - myLabelWidth
        myLabel.setItemPosition(
            myLeftOffset,
            top_offset - 2,  # -2 to push it up a little
            myLabelWidth,
            myLabelHeight,
        )
        myLabel.setFrame(self.showFramesFlag)
        self.composition.addItem(myLabel)
        return myLabelHeight

    def draw_map(self, top_offset):
        """Add a map to the composition and return the composer map instance.

        :param top_offset: Vertical offset at which the logo should be drawn.
        :type top_offset: int

        :returns: The composer map.
        :rtype: QgsComposerMap
        """
        LOGGER.debug('InaSAFE Map drawMap called')
        myMapWidth = self.mapWidth
        myComposerMap = QgsComposerMap(self.composition, self.pageMargin,
                                       top_offset, myMapWidth, self.mapHeight)
        #myExtent = self.iface.mapCanvas().extent()
        # The dimensions of the map canvas and the print compser map may
        # differ. So we set the map composer extent using the canvas and
        # then defer to the map canvas's map extents thereafter
        # Update: disabled as it results in a rectangular rather than
        # square map
        #myComposerMap.setNewExtent(myExtent)
        myComposerExtent = myComposerMap.extent()
        # Recenter the composer map on the center of the canvas
        # Note that since the composer map is square and the canvas may be
        # arbitrarily shaped, we center based on the longest edge
        myCanvasExtent = self.iface.mapCanvas().extent()
        myWidth = myCanvasExtent.width()
        myHeight = myCanvasExtent.height()
        myLongestLength = myWidth
        if myWidth < myHeight:
            myLongestLength = myHeight
        myHalfLength = myLongestLength / 2
        myCenter = myCanvasExtent.center()
        myMinX = myCenter.x() - myHalfLength
        myMaxX = myCenter.x() + myHalfLength
        myMinY = myCenter.y() - myHalfLength
        myMaxY = myCenter.y() + myHalfLength
        mySquareExtent = QgsRectangle(myMinX, myMinY, myMaxX, myMaxY)
        myComposerMap.setNewExtent(mySquareExtent)

        myComposerMap.setGridEnabled(True)
        myNumberOfSplits = 5
        # .. todo:: Write logic to adjust preciosn so that adjacent tick marks
        #    always have different displayed values
        myPrecision = 2
        myXInterval = myComposerExtent.width() / myNumberOfSplits
        myComposerMap.setGridIntervalX(myXInterval)
        myYInterval = myComposerExtent.height() / myNumberOfSplits
        myComposerMap.setGridIntervalY(myYInterval)
        myComposerMap.setGridStyle(QgsComposerMap.Cross)
        myCrossLengthMM = 1
        myComposerMap.setCrossLength(myCrossLengthMM)
        myComposerMap.setZValue(0)  # To ensure it does not overlay logo
        myFontSize = 6
        myFontWeight = QtGui.QFont.Normal
        myItalicsFlag = False
        myFont = QtGui.QFont('verdana', myFontSize, myFontWeight,
                             myItalicsFlag)
        myComposerMap.setGridAnnotationFont(myFont)
        myComposerMap.setGridAnnotationPrecision(myPrecision)
        myComposerMap.setShowGridAnnotation(True)
        myComposerMap.setGridAnnotationDirection(
            QgsComposerMap.BoundaryDirection)
        self.composition.addItem(myComposerMap)
        self.draw_graticule_mask(top_offset)
        return myComposerMap

    def draw_graticule_mask(self, top_offset):
        """A helper function to mask out graticule labels.

         It will hide labels on the right side by over painting a white
         rectangle with white border on them. **kludge**

        :param top_offset: Vertical offset at which the logo should be drawn.
        :type top_offset: int
        """
        LOGGER.debug('InaSAFE Map drawGraticuleMask called')
        myLeftOffset = self.pageMargin + self.mapWidth
        myRect = QgsComposerShape(myLeftOffset + 0.5, top_offset,
                                  self.pageWidth - myLeftOffset,
                                  self.mapHeight + 1, self.composition)

        myRect.setShapeType(QgsComposerShape.Rectangle)
        myRect.setLineWidth(0.1)
        myRect.setFrame(False)
        myRect.setOutlineColor(QtGui.QColor(255, 255, 255))
        myRect.setFillColor(QtGui.QColor(255, 255, 255))
        myRect.setOpacity(100)
        # These two lines seem superfluous but are needed
        myBrush = QtGui.QBrush(QtGui.QColor(255, 255, 255))
        myRect.setBrush(myBrush)
        self.composition.addItem(myRect)

    def draw_native_scalebar(self, composer_map, top_offset):
        """Draw a scale bar using QGIS' native drawing.

        In the case of geographic maps, scale will be in degrees, not km.

        :param composer_map: Composer map on which to draw the scalebar.
        :type composer_map: QgsComposerMap

        :param top_offset: Vertical offset at which the logo should be drawn.
        :type top_offset: int
        """
        LOGGER.debug('InaSAFE Map drawNativeScaleBar called')
        myScaleBar = QgsComposerScaleBar(self.composition)
        myScaleBar.setStyle('Numeric')  # optionally modify the style
        myScaleBar.setComposerMap(composer_map)
        myScaleBar.applyDefaultSize()
        myScaleBarHeight = myScaleBar.boundingRect().height()
        myScaleBarWidth = myScaleBar.boundingRect().width()
        # -1 to avoid overlapping the map border
        myScaleBar.setItemPosition(
            self.pageMargin + 1,
            top_offset + self.mapHeight - (myScaleBarHeight * 2),
            myScaleBarWidth, myScaleBarHeight)
        myScaleBar.setFrame(self.showFramesFlag)
        # Disabled for now
        #self.composition.addItem(myScaleBar)

    def draw_scalebar(self, composer_map, top_offset):
        """Add a numeric scale to the bottom left of the map.

        We draw the scale bar manually because QGIS does not yet support
        rendering a scale bar for a geographic map in km.

        .. seealso:: :meth:`drawNativeScaleBar`

        :param composer_map: Composer map on which to draw the scalebar.
        :type composer_map: QgsComposerMap

        :param top_offset: Vertical offset at which the logo should be drawn.
        :type top_offset: int
        """
        LOGGER.debug('InaSAFE Map drawScaleBar called')
        myCanvas = self.iface.mapCanvas()
        myRenderer = myCanvas.mapRenderer()
        #
        # Add a linear map scale
        #
        myDistanceArea = QgsDistanceArea()
        myDistanceArea.setSourceCrs(myRenderer.destinationCrs().srsid())
        myDistanceArea.setProjectionsEnabled(True)
        # Determine how wide our map is in km/m
        # Starting point at BL corner
        myComposerExtent = composer_map.extent()
        myStartPoint = QgsPoint(myComposerExtent.xMinimum(),
                                myComposerExtent.yMinimum())
        # Ending point at BR corner
        myEndPoint = QgsPoint(myComposerExtent.xMaximum(),
                              myComposerExtent.yMinimum())
        myGroundDistance = myDistanceArea.measureLine(myStartPoint, myEndPoint)
        # Get the equivalent map distance per page mm
        myMapWidth = self.mapWidth
        # How far is 1mm on map on the ground in meters?
        myMMToGroundDistance = myGroundDistance / myMapWidth
        #print 'MM:', myMMDistance
        # How long we want the scale bar to be in relation to the map
        myScaleBarToMapRatio = 0.5
        # How many divisions the scale bar should have
        myTickCount = 5
        myScaleBarWidthMM = myMapWidth * myScaleBarToMapRatio
        myPrintSegmentWidthMM = myScaleBarWidthMM / myTickCount
        # Segment width in real world (m)
        # We apply some logic here so that segments are displayed in meters
        # if each segment is less that 1000m otherwise km. Also the segment
        # lengths are rounded down to human looking numbers e.g. 1km not 1.1km
        myUnits = ''
        myGroundSegmentWidth = myPrintSegmentWidthMM * myMMToGroundDistance
        if myGroundSegmentWidth < 1000:
            myUnits = 'm'
            myGroundSegmentWidth = round(myGroundSegmentWidth)
            # adjust the segment width now to account for rounding
            myPrintSegmentWidthMM = myGroundSegmentWidth / myMMToGroundDistance
        else:
            myUnits = 'km'
            # Segment with in real world (km)
            myGroundSegmentWidth = round(myGroundSegmentWidth / 1000)
            myPrintSegmentWidthMM = ((myGroundSegmentWidth * 1000) /
                                     myMMToGroundDistance)
        # Now adjust the scalebar width to account for rounding
        myScaleBarWidthMM = myTickCount * myPrintSegmentWidthMM

        #print "SBWMM:", myScaleBarWidthMM
        #print "SWMM:", myPrintSegmentWidthMM
        #print "SWM:", myGroundSegmentWidthM
        #print "SWKM:", myGroundSegmentWidthKM
        # start drawing in line segments
        myScaleBarHeight = 5  # mm
        myLineWidth = 0.3  # mm
        myInsetDistance = 7  # how much to inset the scalebar into the map by
        myScaleBarX = self.pageMargin + myInsetDistance
        myScaleBarY = (top_offset + self.mapHeight - myInsetDistance -
                       myScaleBarHeight)  # mm

        # Draw an outer background box - shamelessly hardcoded buffer
        myRect = QgsComposerShape(
            myScaleBarX - 4,  # left edge
            myScaleBarY - 3,  # top edge
            myScaleBarWidthMM + 13,  # right edge
            myScaleBarHeight + 6,  # bottom edge
            self.composition)

        myRect.setShapeType(QgsComposerShape.Rectangle)
        myRect.setLineWidth(myLineWidth)
        myRect.setFrame(False)
        myBrush = QtGui.QBrush(QtGui.QColor(255, 255, 255))
        # workaround for missing setTransparentFill missing from python api
        myRect.setBrush(myBrush)
        self.composition.addItem(myRect)
        # Set up the tick label font
        myFontWeight = QtGui.QFont.Normal
        myFontSize = 6
        myItalicsFlag = False
        myFont = QtGui.QFont('verdana', myFontSize, myFontWeight,
                             myItalicsFlag)
        # Draw the bottom line
        myUpshift = 0.3  # shift the bottom line up for better rendering
        myRect = QgsComposerShape(myScaleBarX,
                                  myScaleBarY + myScaleBarHeight - myUpshift,
                                  myScaleBarWidthMM, 0.1, self.composition)

        myRect.setShapeType(QgsComposerShape.Rectangle)
        myRect.setLineWidth(myLineWidth)
        myRect.setFrame(False)
        self.composition.addItem(myRect)

        # Now draw the scalebar ticks
        for myTickCountIterator in range(0, myTickCount + 1):
            myDistanceSuffix = ''
            if myTickCountIterator == myTickCount:
                myDistanceSuffix = ' ' + myUnits
            myRealWorldDistance = (
                '%.0f%s' %
                (myTickCountIterator * myGroundSegmentWidth, myDistanceSuffix))
            #print 'RW:', myRealWorldDistance
            myMMOffset = myScaleBarX + (myTickCountIterator *
                                        myPrintSegmentWidthMM)
            #print 'MM:', myMMOffset
            myTickHeight = myScaleBarHeight / 2
            # Lines are not exposed by the api yet so we
            # bodge drawing lines using rectangles with 1px height or width
            myTickWidth = 0.1  # width or rectangle to be drawn
            myUpTickLine = QgsComposerShape(
                myMMOffset, myScaleBarY + myScaleBarHeight - myTickHeight,
                myTickWidth, myTickHeight, self.composition)

            myUpTickLine.setShapeType(QgsComposerShape.Rectangle)
            myUpTickLine.setLineWidth(myLineWidth)
            myUpTickLine.setFrame(False)
            self.composition.addItem(myUpTickLine)
            #
            # Add a tick label
            #
            myLabel = QgsComposerLabel(self.composition)
            myLabel.setFont(myFont)
            myLabel.setText(myRealWorldDistance)
            myLabel.adjustSizeToText()
            myLabel.setItemPosition(myMMOffset - 3, myScaleBarY - myTickHeight)
            myLabel.setFrame(self.showFramesFlag)
            self.composition.addItem(myLabel)

    def draw_impact_title(self, top_offset):
        """Draw the map subtitle - obtained from the impact layer keywords.

        :param top_offset: Vertical offset at which the logo should be drawn.
        :type top_offset: int

        :returns: The height of the label as rendered.
        :rtype: float
        """
        LOGGER.debug('InaSAFE Map drawImpactTitle called')
        myTitle = self.map_title()
        if myTitle is None:
            myTitle = ''
        myFontSize = 20
        myFontWeight = QtGui.QFont.Bold
        myItalicsFlag = False
        myFont = QtGui.QFont('verdana', myFontSize, myFontWeight,
                             myItalicsFlag)
        myLabel = QgsComposerLabel(self.composition)
        myLabel.setFont(myFont)
        myHeading = myTitle
        myLabel.setText(myHeading)
        myLabelWidth = self.pageWidth - (self.pageMargin * 2)
        myLabelHeight = 12
        myLabel.setItemPosition(self.pageMargin, top_offset, myLabelWidth,
                                myLabelHeight)
        myLabel.setFrame(self.showFramesFlag)
        self.composition.addItem(myLabel)
        return myLabelHeight

    def draw_legend(self, top_offset):
        """Add a legend to the map using our custom legend renderer.

        .. note:: getLegend generates a pixmap in 150dpi so if you set
           the map to a higher dpi it will appear undersized.

        :param top_offset: Vertical offset at which the logo should be drawn.
        :type top_offset: int
        """
        LOGGER.debug('InaSAFE Map drawLegend called')
        mapLegendAttributes = self.map_legend_attributes()
        legendNotes = mapLegendAttributes.get('legend_notes', None)
        legendUnits = mapLegendAttributes.get('legend_units', None)
        legendTitle = mapLegendAttributes.get('legend_title', None)
        LOGGER.debug(mapLegendAttributes)
        myLegend = MapLegend(self.layer, self.pageDpi, legendTitle,
                             legendNotes, legendUnits)
        self.legend = myLegend.get_legend()
        myPicture1 = QgsComposerPicture(self.composition)
        myLegendFilePath = unique_filename(prefix='legend',
                                           suffix='.png',
                                           dir='work')
        self.legend.save(myLegendFilePath, 'PNG')
        myPicture1.setPictureFile(myLegendFilePath)
        myLegendHeight = points_to_mm(self.legend.height(), self.pageDpi)
        myLegendWidth = points_to_mm(self.legend.width(), self.pageDpi)
        myPicture1.setItemPosition(self.pageMargin, top_offset, myLegendWidth,
                                   myLegendHeight)
        myPicture1.setFrame(False)
        self.composition.addItem(myPicture1)
        os.remove(myLegendFilePath)

    def draw_image(self, theImage, theWidthMM, theLeftOffset, theTopOffset):
        """Helper to draw an image directly onto the QGraphicsScene.
        This is an alternative to using QgsComposerPicture which in
        some cases leaves artifacts under windows.

        The Pixmap will have a transform applied to it so that
        it is rendered with the same resolution as the composition.

        :param theImage: Image that will be rendered to the layout.
        :type theImage: QImage

        :param theWidthMM: Desired width in mm of output on page.
        :type theWidthMM: int

        :param theLeftOffset: Offset from left of page.
        :type theLeftOffset: int

        :param theTopOffset: Offset from top of page.
        :type theTopOffset: int

        :returns: Graphics scene item.
        :rtype: QGraphicsSceneItem
        """
        LOGGER.debug('InaSAFE Map drawImage called')
        myDesiredWidthMM = theWidthMM  # mm
        myDesiredWidthPX = mm_to_points(myDesiredWidthMM, self.pageDpi)
        myActualWidthPX = theImage.width()
        myScaleFactor = myDesiredWidthPX / myActualWidthPX

        LOGGER.debug('%s %s %s' %
                     (myScaleFactor, myActualWidthPX, myDesiredWidthPX))
        myTransform = QtGui.QTransform()
        myTransform.scale(myScaleFactor, myScaleFactor)
        myTransform.rotate(0.5)
        # noinspection PyArgumentList
        myItem = self.composition.addPixmap(QtGui.QPixmap.fromImage(theImage))
        myItem.setTransform(myTransform)
        myItem.setOffset(theLeftOffset / myScaleFactor,
                         theTopOffset / myScaleFactor)
        return myItem

    def draw_host_and_time(self, top_offset):
        """Add a note with hostname and time to the composition.

        :param top_offset: Vertical offset at which the logo should be drawn.
        :type top_offset: int
        """
        LOGGER.debug('InaSAFE Map drawDisclaimer called')
        #elapsed_time: 11.612545
        #user: timlinux
        #host_name: ultrabook
        #time_stamp: 2012-10-13_23:10:31
        #myUser = self.keywordIO.readKeywords(self.layer, 'user')
        #myHost = self.keywordIO.readKeywords(self.layer, 'host_name')
        myDateTime = self.keywordIO.read_keywords(self.layer, 'time_stamp')
        myTokens = myDateTime.split('_')
        myDate = myTokens[0]
        myTime = myTokens[1]
        #myElapsedTime = self.keywordIO.readKeywords(self.layer,
        #                                            'elapsed_time')
        #myElapsedTime = humaniseSeconds(myElapsedTime)
        myLongVersion = get_version()
        myTokens = myLongVersion.split('.')
        myVersion = '%s.%s.%s' % (myTokens[0], myTokens[1], myTokens[2])
        myLabelText = self.tr(
            'Date and time of assessment: %1 %2\n'
            'Special note: This assessment is a guide - we strongly recommend '
            'that you ground truth the results shown here before deploying '
            'resources and / or personnel.\n'
            'Assessment carried out using InaSAFE release %3 (QGIS '
            'plugin version).').arg(myDate).arg(myTime).arg(myVersion)
        myFontSize = 6
        myFontWeight = QtGui.QFont.Normal
        myItalicsFlag = True
        myFont = QtGui.QFont('verdana', myFontSize, myFontWeight,
                             myItalicsFlag)
        myLabel = QgsComposerLabel(self.composition)
        myLabel.setFont(myFont)
        myLabel.setText(myLabelText)
        myLabel.adjustSizeToText()
        myLabelHeight = 50.0  # mm determined using qgis map composer
        myLabelWidth = (self.pageWidth / 2) - self.pageMargin
        myLeftOffset = self.pageWidth / 2  # put in right half of page
        myLabel.setItemPosition(
            myLeftOffset,
            top_offset,
            myLabelWidth,
            myLabelHeight,
        )
        myLabel.setFrame(self.showFramesFlag)
        self.composition.addItem(myLabel)

    def draw_disclaimer(self):
        """Add a disclaimer to the composition."""
        LOGGER.debug('InaSAFE Map drawDisclaimer called')
        myFontSize = 10
        myFontWeight = QtGui.QFont.Normal
        myItalicsFlag = True
        myFont = QtGui.QFont('verdana', myFontSize, myFontWeight,
                             myItalicsFlag)
        myLabel = QgsComposerLabel(self.composition)
        myLabel.setFont(myFont)
        myLabel.setText(self.disclaimer)
        myLabel.adjustSizeToText()
        myLabelHeight = 7.0  # mm determined using qgis map composer
        myLabelWidth = self.pageWidth  # item - position and size...option
        myLeftOffset = self.pageMargin
        myTopOffset = self.pageHeight - self.pageMargin
        myLabel.setItemPosition(
            myLeftOffset,
            myTopOffset,
            myLabelWidth,
            myLabelHeight,
        )
        myLabel.setFrame(self.showFramesFlag)
        self.composition.addItem(myLabel)

    def map_title(self):
        """Get the map title from the layer keywords if possible.

        :returns: None on error, otherwise the title.
        :rtype: None, str
        """
        LOGGER.debug('InaSAFE Map getMapTitle called')
        try:
            myTitle = self.keywordIO.read_keywords(self.layer, 'map_title')
            return myTitle
        except KeywordNotFoundError:
            return None
        except Exception:
            return None

    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 getMapLegendAtributes called')
        legendAttributes = ['legend_notes', 'legend_units', 'legend_title']
        dictLegendAttributes = {}
        for myLegendAttribute in legendAttributes:
            try:
                dictLegendAttributes[myLegendAttribute] = \
                    self.keywordIO.read_keywords(self.layer, myLegendAttribute)
            except KeywordNotFoundError:
                pass
            except Exception:
                pass
        return dictLegendAttributes

    def showComposer(self):
        """Show the composition in a composer view so the user can tweak it.
        """
        myView = QgsComposerView(self.iface.mainWindow())
        myView.show()

    def write_template(self, template_path):
        """Write current composition as a template that can be re-used in QGIS.

        :param template_path: Path to which template should be written.
        :type template_path: str
        """
        myDocument = QtXml.QDomDocument()
        myElement = myDocument.createElement('Composer')
        myDocument.appendChild(myElement)
        self.composition.writeXML(myElement, myDocument)
        myXml = myDocument.toByteArray()
        myFile = file(template_path, 'wb')
        myFile.write(myXml)
        myFile.close()

    def render_template(self, template_path, output_path):
        """Load a QgsComposer map from a template and render it.

        .. note:: THIS METHOD IS EXPERIMENTAL AND CURRENTLY NON FUNCTIONAL

        :param template_path:  Path to the template that should be loaded.
        :type template_path: str

        :param output_path: Path for the output pdf.
        :type output_path: str
        """
        self.setup_composition()

        myResolution = self.composition.printResolution()
        self.printer = setup_printer(output_path, resolution=myResolution)
        if self.composition:
            myFile = QtCore.QFile(template_path)
            myDocument = QtXml.QDomDocument()
            myDocument.setContent(myFile, False)  # .. todo:: fix magic param
            myNodeList = myDocument.elementsByTagName('Composer')
            if myNodeList.size() > 0:
                myElement = myNodeList.at(0).toElement()
                self.composition.readXML(myElement, myDocument)
        self.make_pdf(output_path)