Source code for safe.gui.tools.impact_merge_dialog

# coding=utf-8
"""
Impact Layer Merge Dialog.

Contact : [email protected]

.. note:: This program is free software; you can redistribute it and/or modify
     it under the terms of the GNU General Public License as published by
     the Free Software Foundation; either version 2 of the License, or
     (at your option) any later version.

"""

__author__ = '[email protected]'
__revision__ = '$Format:%H$'
__date__ = '06/05/2014'
__copyright__ = ('Copyright 2012, Australia Indonesia Facility for '
                 'Disaster Reduction')

import os
from collections import OrderedDict
from xml.dom import minidom

# noinspection PyUnresolvedReferences
# pylint: disable=F0401
# pylint: disable=unused-import
from qgis.core import (
    QgsMapLayerRegistry,
    QgsComposition,
    QgsRectangle,
    QgsAtlasComposition
)
# pylint: enable=F0401
# pylint: enable=unused-import
try:
    from qgis.core import QgsMapSettings
except ImportError:
    from qgis.core import QgsMapRenderer

# noinspection PyPackageRequirements
from PyQt4 import QtGui, QtCore
# noinspection PyPackageRequirements
from PyQt4.QtCore import QSettings, pyqtSignature, QUrl, pyqtSlot
# noinspection PyPackageRequirements
from PyQt4.QtGui import QDialog, QMessageBox, QFileDialog, QDesktopServices

from safe.common.exceptions import (
    InvalidLayerError,
    EmptyDirectoryError,
    FileNotFoundError,
    CanceledImportDialogError,
    NoKeywordsFoundError,
    KeywordNotFoundError,
    InvalidParameterError,
    ReportCreationError,
    UnsupportedProviderError,
    TemplateLoadingError)
from safe.common.utilities import temp_dir
from safe import messaging as m
from safe.messaging import styles
from safe.report.template_composition import TemplateComposition
from safe.utilities.resources import (
    html_header,
    html_footer,
    get_ui_class,
    resources_path)
from safe.utilities.gis import qgis_version
from safe.utilities.utilities import (
    html_to_file,
    add_ordered_combo_item)
from safe.gui.tools.help.impact_merge_help import impact_merge_help
from safe.utilities.keyword_io import KeywordIO
from safe.defaults import (
    disclaimer, white_inasafe_logo_path, supporters_logo_path)
from safe.utilities.unicode import get_string

INFO_STYLE = styles.INFO_STYLE
FORM_CLASS = get_ui_class('impact_merge_dialog_base.ui')


[docs]class ImpactMergeDialog(QDialog, FORM_CLASS): """Tools for merging 2 impact layer based on different exposure.""" def __init__(self, parent=None, iface=None): """Constructor for dialog. :param parent: Optional widget to use as parent :type parent: QWidget :param iface: An instance of QGisInterface :type iface: QGisInterface """ QDialog.__init__(self, parent) self.parent = parent self.setupUi(self) self.setWindowTitle(self.tr('InaSAFE Impact Layer Merge Tool')) self.iface = iface self.keyword_io = KeywordIO() # Template Path for composer self.template_path = resources_path( 'qgis-composer-templates', 'merged_report.qpt') # Safe Logo Path self.safe_logo_path = white_inasafe_logo_path() # Organisation Logo Path - defaults to supporters logo, will be # updated to user defined organisation logo path in read_settings in # user has specified a custom logo. self.organisation_logo_path = supporters_logo_path() # Disclaimer text self.disclaimer = disclaimer() # The output directory self.out_dir = None # Stored information from first impact layer self.first_impact = { 'layer': None, 'map_title': None, 'hazard_title': None, 'exposure_title': None, 'postprocessing_report': None, } # Stored information from second impact layer self.second_impact = { 'layer': None, 'map_title': None, 'hazard_title': None, 'exposure_title': None, 'postprocessing_report': None, } # Stored information from aggregation layer self.aggregation = { 'layer': None, 'aggregation_attribute': None } # Available aggregation layer self.available_aggregation = [] # The summary report, contains report for each aggregation area self.summary_report = OrderedDict() # The html reports and its file path self.html_reports = OrderedDict() # A boolean flag whether to merge entire area or aggregated self.entire_area_mode = False # Get the global settings and override some variable if exist self.read_settings() # Get all current project layers for combo box self.get_project_layers() # Set up things for context help self.help_button = self.button_box.button(QtGui.QDialogButtonBox.Help) # Allow toggling the help button self.help_button.setCheckable(True) self.help_button.toggled.connect(self.help_toggled) self.main_stacked_widget.setCurrentIndex(1) # Show usage info self.restore_state()
[docs] 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)
[docs] def save_state(self): """ Store current state of GUI to configuration file """ settings = QSettings() settings.setValue('directory', self.output_directory.text())
@pyqtSignature('') # prevents actions being handled twice
[docs] 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
[docs] 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)
[docs] def accept(self): """Do merging two impact layers.""" # Store the current state to configuration file self.save_state() # Prepare all the input from dialog, validate, and store it try: self.prepare_input() except (InvalidLayerError, EmptyDirectoryError, FileNotFoundError) as ex: # noinspection PyCallByClass,PyTypeChecker, PyArgumentList QMessageBox.information( self, self.tr("InaSAFE Merge Impact Tool Information"), str(ex)) return except CanceledImportDialogError: return # Validate all the layers logically try: self.validate_all_layers() except (NoKeywordsFoundError, KeywordNotFoundError, InvalidLayerError) as ex: # noinspection PyCallByClass,PyTypeChecker, PyArgumentList QMessageBox.information( self, self.tr("InaSAFE Merge Impact Tools Information"), str(ex)) return # The input is valid, do the merging # Set cursor to wait cursor QtGui.qApp.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor)) # pylint: disable=W0703 try: self.merge() except Exception as ex: # End wait cursor QtGui.qApp.restoreOverrideCursor() # noinspection PyCallByClass,PyTypeChecker,PyArgumentList QMessageBox.warning( self, self.tr("InaSAFE Merge Impact Tools Error"), str(ex)) return # pylint: enable=W0703 # Finish doing it. End wait cursor QtGui.qApp.restoreOverrideCursor() # Give user successful information! # noinspection PyCallByClass,PyTypeChecker, PyArgumentList QMessageBox.information( self, self.tr('InaSAFE Merge Impact Tool Information'), self.tr( 'Report from merging two impact layers was generated ' 'successfully.')) # Open output directory on file explorer # noinspection PyArgumentList output_directory_url = QUrl.fromLocalFile(self.out_dir) # noinspection PyTypeChecker,PyCallByClass,PyArgumentList QDesktopServices.openUrl(output_directory_url)
[docs] 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
[docs] def get_project_layers(self): """Get impact layers and aggregation layer currently loaded in QGIS.""" # noinspection PyArgumentList,PyUnresolvedReferences registry = QgsMapLayerRegistry.instance() # MapLayers returns a QMap<QString id, QgsMapLayer layer> layers = registry.mapLayers().values() if len(layers) == 0: return # Clear the combo box first self.first_layer.clear() self.second_layer.clear() self.aggregation_layer.clear() # empty list self.available_aggregation[:] = [] for layer in layers: try: self.keyword_io.read_keywords(layer, 'impact_summary') except (NoKeywordsFoundError, KeywordNotFoundError): # Check if it has aggregation keyword try: self.keyword_io.read_keywords( layer, 'aggregation attribute') except (NoKeywordsFoundError, KeywordNotFoundError): # Skip if there are no keywords at all continue self.available_aggregation.append(layer) add_ordered_combo_item( self.aggregation_layer, layer.name(), layer) continue except (UnsupportedProviderError, InvalidParameterError): # UnsupportedProviderError: # Encounter unsupported provider layer, e.g Open Layer # InvalidParameterError: # Encounter invalid layer source, # see https://github.com/AIFDR/inasafe/issues/754 continue add_ordered_combo_item(self.first_layer, layer.name(), layer) add_ordered_combo_item(self.second_layer, layer.name(), layer) # Add Entire Area Option to Aggregated Layer: self.aggregation_layer.insertItem( 0, self.tr('Entire Area'), None ) self.aggregation_layer.setCurrentIndex(0)
[docs] def prepare_input(self): """Fetch all the input from dialog, validate, and store it. Consider this as a bridge between dialog interface and our logical stored data in this class :raises: InvalidLayerError, CanceledImportDialogError """ # Validate The combobox impact layers (they should be different) first_layer_index = self.first_layer.currentIndex() second_layer_index = self.second_layer.currentIndex() if first_layer_index < 0: raise InvalidLayerError(self.tr('First layer is not valid.')) if second_layer_index < 0: raise InvalidLayerError(self.tr('Second layer is not valid.')) if first_layer_index == second_layer_index: raise InvalidLayerError( self.tr('First layer must be different to second layer''.')) # Get all chosen layers self.first_impact['layer'] = self.first_layer.itemData( self.first_layer.currentIndex(), QtCore.Qt.UserRole) self.second_impact['layer'] = self.second_layer.itemData( self.second_layer.currentIndex(), QtCore.Qt.UserRole) self.aggregation['layer'] = self.aggregation_layer.itemData( self.aggregation_layer.currentIndex(), QtCore.Qt.UserRole) # Validate the output directory self.require_directory() # Get output directory self.out_dir = self.output_directory.text() # Whether to use own report template: if self.report_template_checkbox.isChecked(): own_template_path = self.report_template_le.text() if os.path.isfile(own_template_path): self.template_path = own_template_path else: raise FileNotFoundError( self.tr('Template file does not exist.')) # Flag whether to merge entire area or based on aggregation unit # Rizky: Fix nasty bug where the dialog stuck in entire_area_mode # the mode should be rechecked based on selected aggregation layer self.entire_area_mode = True if (self.aggregation_layer.currentIndex() > 0 and not self.aggregation['layer'] is None): self.entire_area_mode = False
[docs] def require_directory(self): """Ensure directory path entered in dialog exist. When the path does not exist, this function will ask the user if he wants to create it or not. :raises: CanceledImportDialogError - when user chooses 'No' in the question dialog for creating directory, or 'Yes' but the output directory path is empty """ path = self.output_directory.text() if os.path.exists(path): return title = self.tr("Directory %s does not exist") % path question = self.tr( "Directory %s does not exist. Do you want to create it?" ) % path # noinspection PyCallByClass,PyTypeChecker answer = QMessageBox.question( self, title, question, QMessageBox.Yes | QMessageBox.No) if answer == QMessageBox.Yes: if len(path) != 0: os.makedirs(path) else: raise EmptyDirectoryError( self.tr('Output directory cannot be empty.')) else: raise CanceledImportDialogError()
[docs] 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.'))
[docs] def merge(self): """Merge the postprocessing_report from each impact.""" # Ensure there is always only a single root element or minidom moans first_postprocessing_report = \ self.first_impact['postprocessing_report'] second_postprocessing_report = \ self.second_impact['postprocessing_report'] # noinspection PyTypeChecker first_report = '<body>' + first_postprocessing_report + '</body>' # noinspection PyTypeChecker second_report = '<body>' + second_postprocessing_report + '</body>' # Now create a dom document for each first_document = minidom.parseString(get_string(first_report)) second_document = minidom.parseString(get_string(second_report)) first_impact_tables = first_document.getElementsByTagName('table') second_impact_tables = second_document.getElementsByTagName('table') # Now create dictionary report from DOM first_report_dict = self.generate_report_dictionary_from_dom( first_impact_tables) second_report_dict = self.generate_report_dictionary_from_dom( second_impact_tables) # Rizky: Consistency checks with aggregation # Make sure the aggregation layer both presents in both layers # We shouldn't have problems with Entire Area mode. It just means # the impact layer and summary is merged into single report. # We can have 3 cases: # 1. If both of them were not aggregated, we can just merge the map # only # 2. If both of them were aggregated, we can just merge the map, and # merge postprocessor report using 'Total aggregation in areas' key # 3. If only one of them were aggregated, we can just merge the map, # and uses postprocessor report from the one who has. if self.entire_area_mode: # We won't be bothered with the map, it will be merged anyway in # all 3 cases. We should bother with the postprocessor report. # If one of them has the report, it means it will contain more # than one report keys. We can just swap the first report if they # have one key, and the second have more than one if (len(first_report_dict.keys()) == 1 and len(second_report_dict.keys()) > 1): swap_var = first_report_dict first_report_dict = second_report_dict second_report_dict = swap_var # This condition will covers aggregated mode # For this case, we should make sure both layers are aggregated with # the same aggregation layer of the chosen aggregation layer else: # check that both layers must have aggregated postprocessor. # aggregated postprocessor means the report_dict must have minimum # 2 keys if not (len(first_report_dict.keys()) > 1 and len(second_report_dict.keys()) > 1): raise InvalidLayerError(self.tr( 'Please choose impact layers with aggregated ' 'postprocessor if you want to use aggregation layer.')) # collect all report keys (will contain aggregation areas in the # report) report_keys = first_report_dict.keys() # Discard the last keys. It will always contains total area, not # aggregated area if len(report_keys) > 0: del report_keys[-1] sec_report = second_report_dict.keys() if len(sec_report) > 0: del sec_report[-1] for k in sec_report: if k not in report_keys: report_keys.append(k) # collect all aggregation areas in aggregation layer layer = self.aggregation['layer'] aggregation_attr = self.aggregation['aggregation_attribute'] aggregation_attr_index = layer.fieldNameIndex(aggregation_attr) aggregation_keys = [] for f in layer.getFeatures(): area = f[aggregation_attr_index] if area not in aggregation_keys: aggregation_keys.append(area) is_subset = True for k in report_keys: if k not in aggregation_keys: is_subset = False if not is_subset: # This means report keys contains area keys that is not in # aggregation layer. Which means possibly it is using the # wrong aggregation layer. raise InvalidLayerError( self.tr('First and Second layer does not use chosen ' 'Aggregation layer')) # Generate report summary for all aggregation unit self.generate_report_summary(first_report_dict, second_report_dict) # Generate html reports file from merged dictionary self.generate_html_reports(first_report_dict, second_report_dict) # Generate PDF Reports using composer and/or atlas generation: self.generate_reports() # Delete html report files: for area in self.html_reports: report_path = self.html_reports[area] # Rizky : Fix possible bugs in Windows related to issue: # https://github.com/AIFDR/inasafe/issues/1862 try: # avoid race condition using with statement with open(report_path, 'w') as report_file: report_file.close() os.remove(report_path) except OSError: pass
@staticmethod
[docs] def generate_report_dictionary_from_dom(html_dom): """Generate dictionary representing report from html dom. :param html_dom: Input representing document dom as report from each impact layer report. :type html_dom: str :return: Dictionary representing html_dom. :rtype: dict Dictionary Structure:: { Aggregation_Area: {Exposure Type:{ Exposure Detail} } } Example:: {"Jakarta Barat": {"Closed buildings": {"Total inundated":150, "Places of Worship": "No data" } } } """ merged_report_dict = OrderedDict() for table in html_dom: # noinspection PyUnresolvedReferences caption = table.getElementsByTagName('caption')[0].firstChild.data # noinspection PyUnresolvedReferences rows = table.getElementsByTagName('tr') header = rows[0] contains = rows[1:] for contain in contains: data = contain.getElementsByTagName('td') aggregation_area = data[0].firstChild.nodeValue exposure_dict = OrderedDict() if aggregation_area in merged_report_dict: exposure_dict = merged_report_dict[aggregation_area] data_contain = data[1:] exposure_detail_dict = OrderedDict() for datum in data_contain: index_datum = data.index(datum) datum_header = \ header.getElementsByTagName('td')[index_datum] datum_caption = datum_header.firstChild.nodeValue exposure_detail_dict[datum_caption] = \ datum.firstChild.nodeValue exposure_dict[caption] = exposure_detail_dict merged_report_dict[aggregation_area] = exposure_dict return merged_report_dict
[docs] def generate_report_summary(self, first_report_dict, second_report_dict): """Generate report summary for each aggregation area from merged report dictionary. For each exposure, search for the total only. Report dictionary looks like this: :param first_report_dict: Dictionary report from the first impact. :type first_report_dict: dict :param second_report_dict: Dictionary report from the second impact. :type second_report_dict: dict Dictionary structure:: { aggregation_area: {exposure_type:{ exposure_detail} } } Example:: {"Jakarta Barat": {"Closed buildings": {"Total inundated":150, "Places of Worship": "No data" } } } """ for aggregation_area in first_report_dict: html = '' html += '<table style="margin:0px auto">' # Summary total from first report html += '<tr><td><b>%s</b></td><td></td></tr>' % \ self.first_impact['exposure_title'].title() first_exposure_type_dict = first_report_dict[aggregation_area] first_exposure_type = first_exposure_type_dict.keys()[0] first_exposure_detail_dict = \ first_exposure_type_dict[first_exposure_type] for datum in first_exposure_detail_dict: if self.tr('Total').lower() in datum.lower(): html += ('<tr>' '<td>%s</td>' '<td>%s</td>' '</tr>') % \ (datum, first_exposure_detail_dict[datum]) break # Catch fallback for aggregation_area not exist in second_report if aggregation_area in second_report_dict: second_exposure_report_dict = second_report_dict[ aggregation_area] # Summary total from second report html += '<tr><td><b>%s</b></td><td></td></tr>' % \ self.second_impact['exposure_title'].title() second_exposure = second_exposure_report_dict.keys()[0] second_exposure_detail_dict = \ second_exposure_report_dict[second_exposure] for datum in second_exposure_detail_dict: if self.tr('Total').lower() in datum.lower(): html += ('<tr>' '<td>%s</td>' '<td>%s</td>' '</tr>') % \ (datum, second_exposure_detail_dict[datum]) break html += '</table>' self.summary_report[aggregation_area.lower()] = html
[docs] def generate_html_reports(self, first_report_dict, second_report_dict): """Generate html file for each aggregation units. It also saves the path of the each aggregation unit in self.html_reports. :: Ex. {"jakarta barat": "/home/jakarta barat.html", "jakarta timur": "/home/jakarta timur.html"} :param first_report_dict: Dictionary report from first impact. :type first_report_dict: dict :param second_report_dict: Dictionary report from second impact. :type second_report_dict: dict """ for aggregation_area in first_report_dict: html = html_header() html += ('<table width="100%" style="position:absolute;left:0px;"' 'class="table table-condensed table-striped">') html += '<caption><h4>%s</h4></caption>' % \ aggregation_area.title() html += '<tr>' # First impact on the left side html += '<td width="48%">' html += '<table width="100%">' html += '<thead><th>%s</th></thead>' % \ self.first_impact['exposure_title'].upper() first_exposure_report_dict = first_report_dict[aggregation_area] for first_exposure in first_exposure_report_dict: first_exposure_detail_dict = \ first_exposure_report_dict[first_exposure] html += '<tr><th><i>%s</i></th><th></th></tr>' % \ first_exposure.title() for datum in first_exposure_detail_dict: html += ('<tr>' '<td>%s</td>' '<td>%s</td>' '</tr>') % (datum, first_exposure_detail_dict[datum]) html += '</table>' html += '</td>' # Second impact on the right side if aggregation_area in second_report_dict: # Add spaces between html += '<td width="4%">' html += '</td>' # Second impact report html += '<td width="48%">' html += '<table width="100%">' html += '<thead><th>%s</th></thead>' % \ self.second_impact['exposure_title'].upper() second_exposure_report_dict = \ second_report_dict[aggregation_area] for second_exposure in second_exposure_report_dict: second_exposure_detail_dict = \ second_exposure_report_dict[second_exposure] html += '<tr><th><i>%s</i></th><th></th></tr>' % \ second_exposure.title() for datum in second_exposure_detail_dict: html += ('<tr>' '<td>%s</td>' '<td>%s</td>' '</tr>') % \ (datum, second_exposure_detail_dict[datum]) html += '</table>' html += '</td>' html += '</tr>' html += '</table>' html += html_footer() file_path = '%s.html' % aggregation_area path = os.path.join( temp_dir(self.__class__.__name__), file_path) html_to_file(html, path) self.html_reports[aggregation_area.lower()] = path
[docs] def generate_reports(self): """Generate PDF reports for each aggregation unit using map composer. First the report template is loaded with the renderer from two impact layers. After it's loaded, if it is not aggregated then we just use composition to produce report. Since there are two impact maps here, we need to set a new extent for these impact maps by a simple calculation. If it is not aggregated then we use a powerful QGIS atlas generation on composition. Since we save each report table representing each aggregated area on self.html_report (which is a dictionary with the aggregation area name as a key and its path as a value), and we set the aggregation area name as current filename on atlas generation, we can match these two so that we have the right report table for each report. For those two cases, we use the same template. The report table is basically an HTML frame. Of course after the merging process is done, we delete each report table on self.html_reports physically on disk. """ # Set the layer set layer_set = [self.first_impact['layer'].id(), self.second_impact['layer'].id()] # If aggregated, append chosen aggregation layer if not self.entire_area_mode: layer_set.append(self.aggregation['layer'].id()) # Instantiate Map Settings for Composition if qgis_version() < 20400: map_settings = QgsMapRenderer() map_settings.setLayerSet(layer_set) else: map_settings = QgsMapSettings() map_settings.setLayers(layer_set) # Create composition composition = self.load_template(map_settings) # Get Map composer_map = composition.getComposerItemById('impact-map') # Get HTML Report Frame html_report_item = \ composition.getComposerItemById('merged-report-table') html_report_frame = composition.getComposerHtmlByItem(html_report_item) if self.entire_area_mode: # Get composer map size composer_map_width = composer_map.boundingRect().width() composer_map_height = composer_map.boundingRect().height() # Set the extent from two impact layers to fit into composer map composer_size_ratio = float( composer_map_height / composer_map_width) # The extent of two impact layers min_x = min(self.first_impact['layer'].extent().xMinimum(), self.second_impact['layer'].extent().xMinimum()) min_y = min(self.first_impact['layer'].extent().yMinimum(), self.second_impact['layer'].extent().yMinimum()) max_x = max(self.first_impact['layer'].extent().xMaximum(), self.second_impact['layer'].extent().xMaximum()) max_y = max(self.first_impact['layer'].extent().yMaximum(), self.second_impact['layer'].extent().yMaximum()) max_width = max_x - min_x max_height = max_y - min_y layers_size_ratio = float(max_height / max_width) center_x = min_x + float(max_width / 2.0) center_y = min_y + float(max_height / 2.0) # The extent should fit the composer map size new_width = max_width new_height = max_height # QgsComposerMap only overflows to height, so if it overflows, # the extent of the width should be widened if layers_size_ratio > composer_size_ratio: new_width = max_height / composer_size_ratio # Set new extent fit_min_x = center_x - (new_width / 2.0) fit_max_x = center_x + (new_width / 2.0) fit_min_y = center_y - (new_height / 2.0) fit_max_y = center_y + (new_height / 2.0) # Create the extent and set it to the map # noinspection PyCallingNonCallable map_extent = QgsRectangle( fit_min_x, fit_min_y, fit_max_x, fit_max_y) composer_map.setNewExtent(map_extent) # Add grid to composer map split_count = 5 x_interval = new_width / split_count composer_map.setGridIntervalX(x_interval) y_interval = new_height / split_count composer_map.setGridIntervalY(y_interval) # Self.html_reports must have only 1 key value pair # Rizky: If layer components were aggregated, html_reports will # have several key value pairs. So we get the last value because # it is for the total/entire area area_title = list(self.html_reports.keys())[-1] # Set Report Summary summary_report = composition.getComposerItemById('summary-report') summary_report.setText(self.summary_report[area_title]) # Set Aggregation Area Label area_label = composition.getComposerItemById('aggregation-area') area_label.setText(area_title.title()) # Set merged-report-table html_report_path = self.html_reports[area_title] # noinspection PyArgumentList html_frame_url = QUrl.fromLocalFile(html_report_path) html_report_frame.setUrl(html_frame_url) # Export composition to PDF file file_name = '_'.join(area_title.split()) file_path = '%s.pdf' % file_name path = os.path.join(self.out_dir, file_path) composition.exportAsPDF(path) else: # Create atlas composition: # noinspection PyCallingNonCallable atlas = QgsAtlasComposition(composition) # Set coverage layer # Map will be clipped by features from this layer: atlas.setCoverageLayer(self.aggregation['layer']) # Add grid to composer map from coverage layer split_count = 5 map_width = self.aggregation['layer'].extent().width() map_height = self.aggregation['layer'].extent().height() x_interval = map_width / split_count composer_map.setGridIntervalX(x_interval) y_interval = map_height / split_count composer_map.setGridIntervalY(y_interval) # Set composer map that will be used for printing atlas atlas.setComposerMap(composer_map) # set output filename pattern atlas.setFilenamePattern( self.aggregation['aggregation_attribute']) # Start rendering atlas.beginRender() # Iterate all aggregation unit in aggregation layer for i in range(0, atlas.numFeatures()): atlas.prepareForFeature(i) current_filename = atlas.currentFilename() file_name = '_'.join(current_filename.split()) file_path = '%s.pdf' % file_name path = os.path.join(self.out_dir, file_path) # Only print the area that has the report area_title = current_filename.lower() if area_title in self.summary_report: # Set Report Summary summary_report = composition.getComposerItemById( 'summary-report') summary_report.setText(self.summary_report[area_title]) # Set Aggregation Area Label area_label = composition.getComposerItemById( 'aggregation-area') area_label.setText(area_title.title()) # Set merged-report-table html_report_path = self.html_reports[area_title] # noinspection PyArgumentList html_frame_url = QUrl.fromLocalFile(html_report_path) html_report_frame.setUrl(html_frame_url) # Export composition to PDF file composition.exportAsPDF(path) # End of rendering atlas.endRender() # noinspection PyArgumentList
[docs] def load_template(self, map_settings): """Load composer template for merged report. Validate it as well. The template needs to have: 1. QgsComposerMap with id 'impact-map' for merged impact map. 2. QgsComposerPicture with id 'safe-logo' for InaSAFE logo. 3. QgsComposerLabel with id 'summary-report' for a summary of two impacts. 4. QgsComposerLabel with id 'aggregation-area' to indicate the area of aggregation. 5. QgsComposerScaleBar with id 'map-scale' for impact map scale. 6. QgsComposerLegend with id 'map-legend' for impact map legend. 7. QgsComposerPicture with id 'organisation-logo' for organisation logo. 8. QgsComposerLegend with id 'impact-legend' for map legend. 9. QgsComposerHTML with id 'merged-report-table' for the merged report. :param map_settings: Map settings. :type map_settings: QgsMapSettings, QgsMapRenderer """ # Create Composition template_composition = TemplateComposition( self.template_path, map_settings) # Validate the component in the template component_ids = ['impact-map', 'safe-logo', 'summary-report', 'aggregation-area', 'map-scale', 'map-legend', 'organisation-logo', 'merged-report-table'] template_composition.component_ids = component_ids if len(template_composition.missing_elements) > 0: raise ReportCreationError(self.tr( 'Components: %s could not be found' % ', '.join( template_composition.missing_elements))) # Prepare map substitution and set to composition impact_title = '%s and %s' % ( self.first_impact['map_title'], self.second_impact['map_title']) substitution_map = { 'impact-title': impact_title, 'hazard-title': self.first_impact['hazard_title'], 'disclaimer': self.disclaimer } template_composition.substitution = substitution_map # Load Template try: template_composition.load_template() except TemplateLoadingError: raise # Draw Composition # Set InaSAFE logo composition = template_composition.composition safe_logo = composition.getComposerItemById('safe-logo') safe_logo.setPictureFile(self.safe_logo_path) # set organisation logo org_logo = composition.getComposerItemById('organisation-logo') org_logo.setPictureFile(self.organisation_logo_path) # Set Map Legend legend = composition.getComposerItemById('map-legend') if qgis_version() < 20400: layers = map_settings.layerSet() else: layers = map_settings.layers() if qgis_version() < 20600: legend.model().setLayerSet(layers) legend.synchronizeWithModel() else: root_group = legend.modelV2().rootGroup() layer_ids = map_settings.layers() for layer_id in layer_ids: # noinspection PyUnresolvedReferences layer = QgsMapLayerRegistry.instance().mapLayer(layer_id) root_group.addLayer(layer) legend.synchronizeWithModel() return composition
@pyqtSlot() @pyqtSignature('bool') # prevents actions being handled twice
[docs] def help_toggled(self, flag): """Show or hide the help tab in the stacked widget. .. versionadded: 3.2.1 :param flag: Flag indicating whether help should be shown or hidden. :type flag: bool """ if flag: self.help_button.setText(self.tr('Hide Help')) self.show_help() else: self.help_button.setText(self.tr('Show Help')) self.hide_help()
[docs] def hide_help(self): """Hide the usage info from the user. .. versionadded: 3.2.1 """ self.main_stacked_widget.setCurrentIndex(1)
[docs] def show_help(self): """Show usage info to the user.""" # Read the header and footer html snippets self.main_stacked_widget.setCurrentIndex(0) header = html_header() footer = html_footer() string = header message = impact_merge_help() string += message.to_html() string += footer self.help_web_view.setHtml(string)