def make_pdf(): canvas = QgsMapCanvas() # Load our project QgsProject.instance().read(QFileInfo(project_path)) bridge = QgsLayerTreeMapCanvasBridge( QgsProject.instance().layerTreeRoot(), canvas) bridge.setCanvasLayers() if canvas.layerCount() < 1: print 'No layers loaded from this project, exiting.' return print canvas.mapSettings().extent().toString() template_file = file(template_path) template_content = template_file.read() template_file.close() document = QDomDocument() document.setContent(template_content) composition = QgsComposition(canvas.mapSettings()) # You can use this to replace any string like this [key] # in the template with a new value. e.g. to replace # [date] pass a map like this {'date': '1 Jan 2012'} substitution_map = { 'DATE_TIME_START': TIME_START, 'DATE_TIME_END': TIME_STOP} composition.loadFromTemplate(document, substitution_map) # You must set the id in the template map_item = composition.getComposerItemById('map') map_item.setMapCanvas(canvas) map_item.zoomToExtent(canvas.extent()) # You must set the id in the template legend_item = composition.getComposerItemById('legend') legend_item.updateLegend() composition.refreshItems() composition.exportAsPDF( '/home/web/reports/pdf/%s/%s.pdf' % (TIME_SLICE, LABEL)) QgsProject.instance().clear()
def make_pdf(): canvas = QgsMapCanvas() # Load our project QgsProject.instance().read(QFileInfo(project_path)) bridge = QgsLayerTreeMapCanvasBridge(QgsProject.instance().layerTreeRoot(), canvas) bridge.setCanvasLayers() template_file = file(template_path) template_content = template_file.read() template_file.close() document = QDomDocument() document.setContent(template_content) composition = QgsComposition(canvas.mapSettings()) # You can use this to replace any string like this [key] # in the template with a new value. e.g. to replace # [date] pass a map like this {'date': '1 Jan 2012'} substitution_map = {'DATE_TIME_START': 'foo', 'DATE_TIME_END': 'bar'} composition.loadFromTemplate(document, substitution_map) # You must set the id in the template map_item = composition.getComposerItemById('map') map_item.setMapCanvas(canvas) map_item.zoomToExtent(canvas.extent()) # You must set the id in the template legend_item = composition.getComposerItemById('legend') legend_item.updateLegend() composition.refreshItems() composition.exportAsPDF('report.pdf') QgsProject.instance().clear()
def export_pdf(self, title=''): ''' Export Composition (map view and checked layers) to PDF ''' title = self.active_scenario.name if self.active_scenario else '' dialog = ExportPDFDialog(title=title, parent=self) result = dialog.exec_() ok = result == QtGui.QDialog.Accepted if not ok: return title = dialog.title date = dialog.date filepath = browse_file(None, 'Export', PDF_FILTER, save=True, parent=self) if not filepath: return bridge = QgsLayerTreeMapCanvasBridge( QgsProject.instance().layerTreeRoot(), self.canvas) bridge.setCanvasLayers() template_file = file(REPORT_TEMPLATE_PATH) template_content = template_file.read() template_file.close() document = QDomDocument() document.setContent(template_content) composition = QgsComposition(self.canvas.mapSettings()) # You can use this to replace any string like this [key] # in the template with a new value. e.g. to replace # [date] pass a map like this {'date': '1 Jan 2012'} substitution_map = { 'TITLE': title, 'DATE_TIME': date} composition.loadFromTemplate(document, substitution_map) # You must set the id in the template map_item = composition.getComposerItemById('map') map_item.setMapCanvas(self.canvas) map_item.zoomToExtent(self.canvas.extent()) # You must set the id in the template legend_item = composition.getComposerItemById('legend') legend_item.updateLegend() composition.refreshItems() composition.exportAsPDF(filepath) if sys.platform.startswith('darwin'): subprocess.call(('open', filepath)) elif os.name == 'nt': os.startfile(filepath) elif os.name == 'posix': subprocess.call(('xdg-open', filepath))
def load_composition(self): """ Creates the composition object (which is needed for creating the file) from the template file. :returns: the composition object :rtype: QgsComposition """ template_path = get_plugin_path() + self.templates[ self.dockwidget.comboBox_template.currentText()] template_file = open(template_path, "r") content = template_file.read() template_file.close() # the method from QgsComposition for loading the template needs to be a QDomDocument document = QDomDocument() document.setContent(content) # composition = QgsComposition(iface.mapCanvas().mapSettings()) does not work #TODO: is deprecated but works ...fix in new version https://hub.qgis.org/issues/11077 composition = QgsComposition(iface.mapCanvas().mapRenderer()) if not composition.loadFromTemplate(document): iface.messageBar().pushMessage("Error while loading template!") return # set map map_item = composition.getComposerItemById("map") map_item.setMapCanvas(iface.mapCanvas()) map_item.zoomToExtent(iface.mapCanvas().extent()) # set legend try: legend_item = composition.getComposerItemById("legend") legend_item.updateLegend() except AttributeError: # in case first template was selected pass composition.refreshItems() return composition
def testComposerHtmlAccessor(self): """Test that we can retrieve the ComposerHtml instance given an item. """ myComposition = QgsComposition(self.iface.mapCanvas().mapSettings()) mySubstitutionMap = {'replace-me': 'Foo bar'} myFile = os.path.join(TEST_DATA_DIR, 'template.qpt') with open(myFile, 'rt') as myTemplateFile: myTemplateContent = myTemplateFile.read() myDocument = QDomDocument() myDocument.setContent(myTemplateContent) myComposition.loadFromTemplate(myDocument, mySubstitutionMap) myItem = myComposition.getComposerItemById('html-test') myComposerHtml = myComposition.getComposerHtmlByItem(myItem) myMessage = 'Could not retrieve the composer html given an item' self.assertIsNotNone(myComposerHtml, myMessage)
def testComposerHtmlAccessor(self): """Test that we can retrieve the ComposerHtml instance given an item. """ myComposition = QgsComposition(self.iface.mapCanvas().mapRenderer()) mySubstitutionMap = {'replace-me': 'Foo bar'} myFile = os.path.join(TEST_DATA_DIR, 'template.qpt') with open(myFile, 'rt') as myTemplateFile: myTemplateContent = myTemplateFile.read() myDocument = QDomDocument() myDocument.setContent(myTemplateContent) myComposition.loadFromTemplate(myDocument, mySubstitutionMap) myItem = myComposition.getComposerItemById('html-test') myComposerHtml = myComposition.getComposerHtmlByItem(myItem) myMessage = 'Could not retrieve the composer html given an item' self.assertIsNotNone(myComposerHtml, myMessage)
def testComposerHtmlAccessor(self): """Test that we can retrieve the ComposerHtml instance given an item. """ myComposition = QgsComposition(CANVAS.mapRenderer()) mySubstitutionMap = {"replace-me": "Foo bar"} myFile = os.path.join(TEST_DATA_DIR, "template.qpt") myTemplateFile = file(myFile, "rt") myTemplateContent = myTemplateFile.read() myTemplateFile.close() myDocument = QDomDocument() myDocument.setContent(myTemplateContent) myComposition.loadFromTemplate(myDocument, mySubstitutionMap) myItem = myComposition.getComposerItemById("html-test") myComposerHtml = myComposition.getComposerHtmlByItem(myItem) myMessage = "Could not retrieve the composer html given an item" assert myComposerHtml is not None, myMessage
def testComposerHtmlAccessor(self): """Test that we can retrieve the ComposerHtml instance given an item. """ myComposition = QgsComposition(CANVAS.mapRenderer()) mySubstitutionMap = {'replace-me': 'Foo bar'} myFile = os.path.join(TEST_DATA_DIR, 'template.qpt') myTemplateFile = file(myFile, 'rt') myTemplateContent = myTemplateFile.read() myTemplateFile.close() myDocument = QDomDocument() myDocument.setContent(myTemplateContent) myComposition.loadFromTemplate(myDocument, mySubstitutionMap) myItem = myComposition.getComposerItemById('html-test') myComposerHtml = myComposition.getComposerHtmlByItem(myItem) myMessage = 'Could not retrieve the composer html given an item' assert myComposerHtml is not None, myMessage
def generate_report(self): # Generate pdf report from impact/hazard LOGGER.info('Generating report') if not self.impact_exists: # Cannot generate report when no impact layer present LOGGER.info('Cannot Generate report when no impact present.') return project_instance = QgsProject.instance() project_instance.setFileName(self.project_path) project_instance.read() # get layer registry layer_registry = QgsMapLayerRegistry.instance() layer_registry.removeAllMapLayers() # Set up the map renderer that will be assigned to the composition map_renderer = CANVAS.mapRenderer() # Enable on the fly CRS transformations map_renderer.setProjectionsEnabled(True) default_crs = map_renderer.destinationCrs() crs = QgsCoordinateReferenceSystem('EPSG:4326') map_renderer.setDestinationCrs(crs) # add place name layer layer_registry.addMapLayer(self.cities_layer, False) # add airport layer layer_registry.addMapLayer(self.airport_layer, False) # add volcano layer layer_registry.addMapLayer(self.volcano_layer, False) # add impact layer hazard_layer = read_qgis_layer(self.hazard_path, self.tr('People Affected')) layer_registry.addMapLayer(hazard_layer, False) # add basemap layer layer_registry.addMapLayer(self.highlight_base_layer, False) # add basemap layer layer_registry.addMapLayer(self.overview_layer, False) CANVAS.setExtent(hazard_layer.extent()) CANVAS.refresh() template_path = self.ash_fixtures_dir('realtime-ash.qpt') with open(template_path) as f: template_content = f.read() document = QDomDocument() document.setContent(template_content) # Now set up the composition # map_settings = QgsMapSettings() # composition = QgsComposition(map_settings) composition = QgsComposition(map_renderer) subtitution_map = self.event_dict() LOGGER.debug(subtitution_map) # load composition object from template result = composition.loadFromTemplate(document, subtitution_map) if not result: LOGGER.exception('Error loading template %s with keywords\n %s', template_path, subtitution_map) raise MapComposerError # get main map canvas on the composition and set extent map_impact = composition.getComposerItemById('map-impact') if map_impact: map_impact.zoomToExtent(hazard_layer.extent()) map_impact.renderModeUpdateCachedImage() else: LOGGER.exception('Map canvas could not be found in template %s', template_path) raise MapComposerError # get overview map canvas on the composition and set extent map_overall = composition.getComposerItemById('map-overall') if map_overall: map_overall.setLayerSet([self.overview_layer.id()]) # this is indonesia extent indonesia_extent = QgsRectangle(94.0927980005593554, -15.6629591962689343, 142.0261493318861312, 10.7379406374101816) map_overall.zoomToExtent(indonesia_extent) map_overall.renderModeUpdateCachedImage() else: LOGGER.exception('Map canvas could not be found in template %s', template_path) raise MapComposerError # setup impact table self.render_population_table() self.render_nearby_table() self.render_landcover_table() impact_table = composition.getComposerItemById('table-impact') if impact_table is None: message = 'table-impact composer item could not be found' LOGGER.exception(message) raise MapComposerError(message) impacts_html = composition.getComposerHtmlByItem(impact_table) if impacts_html is None: message = 'Impacts QgsComposerHtml could not be found' LOGGER.exception(message) raise MapComposerError(message) impacts_html.setUrl(QUrl(self.population_html_path)) # setup nearby table nearby_table = composition.getComposerItemById('table-nearby') if nearby_table is None: message = 'table-nearby composer item could not be found' LOGGER.exception(message) raise MapComposerError(message) nearby_html = composition.getComposerHtmlByItem(nearby_table) if nearby_html is None: message = 'Nearby QgsComposerHtml could not be found' LOGGER.exception(message) raise MapComposerError(message) nearby_html.setUrl(QUrl(self.nearby_html_path)) # setup landcover table landcover_table = composition.getComposerItemById('table-landcover') if landcover_table is None: message = 'table-landcover composer item could not be found' LOGGER.exception(message) raise MapComposerError(message) landcover_html = composition.getComposerHtmlByItem(landcover_table) if landcover_html is None: message = 'Landcover QgsComposerHtml could not be found' LOGGER.exception(message) raise MapComposerError(message) landcover_html.setUrl(QUrl(self.landcover_html_path)) # setup logos logos_id = ['logo-bnpb', 'logo-geologi'] for logo_id in logos_id: logo_picture = composition.getComposerItemById(logo_id) if logo_picture is None: message = '%s composer item could not be found' % logo_id LOGGER.exception(message) raise MapComposerError(message) pic_path = os.path.basename(logo_picture.picturePath()) pic_path = os.path.join('logo', pic_path) logo_picture.setPicturePath(self.ash_fixtures_dir(pic_path)) # save a pdf composition.exportAsPDF(self.map_report_path) project_instance.write(QFileInfo(self.project_path)) layer_registry.removeAllMapLayers() map_renderer.setDestinationCrs(default_crs) map_renderer.setProjectionsEnabled(False) LOGGER.info('Report generation completed.')
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)
def generate_report(self): # Generate pdf report from impact/hazard LOGGER.info("Generating report") if not self.impact_exists: # Cannot generate report when no impact layer present LOGGER.info("Cannot Generate report when no impact present.") return project_instance = QgsProject.instance() project_instance.setFileName(self.project_path) project_instance.read() # get layer registry layer_registry = QgsMapLayerRegistry.instance() layer_registry.removeAllMapLayers() # Set up the map renderer that will be assigned to the composition map_renderer = CANVAS.mapRenderer() # Enable on the fly CRS transformations map_renderer.setProjectionsEnabled(True) default_crs = map_renderer.destinationCrs() crs = QgsCoordinateReferenceSystem("EPSG:4326") map_renderer.setDestinationCrs(crs) # add place name layer layer_registry.addMapLayer(self.cities_layer, False) # add airport layer layer_registry.addMapLayer(self.airport_layer, False) # add volcano layer layer_registry.addMapLayer(self.volcano_layer, False) # add impact layer hazard_layer = read_qgis_layer(self.hazard_path, self.tr("People Affected")) layer_registry.addMapLayer(hazard_layer, False) # add basemap layer layer_registry.addMapLayer(self.highlight_base_layer, False) # add basemap layer layer_registry.addMapLayer(self.overview_layer, False) CANVAS.setExtent(hazard_layer.extent()) CANVAS.refresh() template_path = self.ash_fixtures_dir("realtime-ash.qpt") with open(template_path) as f: template_content = f.read() document = QDomDocument() document.setContent(template_content) # Now set up the composition # map_settings = QgsMapSettings() # composition = QgsComposition(map_settings) composition = QgsComposition(map_renderer) subtitution_map = self.event_dict() LOGGER.debug(subtitution_map) # load composition object from template result = composition.loadFromTemplate(document, subtitution_map) if not result: LOGGER.exception("Error loading template %s with keywords\n %s", template_path, subtitution_map) raise MapComposerError # get main map canvas on the composition and set extent map_impact = composition.getComposerItemById("map-impact") if map_impact: map_impact.zoomToExtent(hazard_layer.extent()) map_impact.renderModeUpdateCachedImage() else: LOGGER.exception("Map canvas could not be found in template %s", template_path) raise MapComposerError # get overview map canvas on the composition and set extent map_overall = composition.getComposerItemById("map-overall") if map_overall: map_overall.setLayerSet([self.overview_layer.id()]) # this is indonesia extent indonesia_extent = QgsRectangle( 94.0927980005593554, -15.6629591962689343, 142.0261493318861312, 10.7379406374101816 ) map_overall.zoomToExtent(indonesia_extent) map_overall.renderModeUpdateCachedImage() else: LOGGER.exception("Map canvas could not be found in template %s", template_path) raise MapComposerError # setup impact table self.render_population_table() self.render_nearby_table() self.render_landcover_table() impact_table = composition.getComposerItemById("table-impact") if impact_table is None: message = "table-impact composer item could not be found" LOGGER.exception(message) raise MapComposerError(message) impacts_html = composition.getComposerHtmlByItem(impact_table) if impacts_html is None: message = "Impacts QgsComposerHtml could not be found" LOGGER.exception(message) raise MapComposerError(message) impacts_html.setUrl(QUrl(self.population_html_path)) # setup nearby table nearby_table = composition.getComposerItemById("table-nearby") if nearby_table is None: message = "table-nearby composer item could not be found" LOGGER.exception(message) raise MapComposerError(message) nearby_html = composition.getComposerHtmlByItem(nearby_table) if nearby_html is None: message = "Nearby QgsComposerHtml could not be found" LOGGER.exception(message) raise MapComposerError(message) nearby_html.setUrl(QUrl(self.nearby_html_path)) # setup landcover table landcover_table = composition.getComposerItemById("table-landcover") if landcover_table is None: message = "table-landcover composer item could not be found" LOGGER.exception(message) raise MapComposerError(message) landcover_html = composition.getComposerHtmlByItem(landcover_table) if landcover_html is None: message = "Landcover QgsComposerHtml could not be found" LOGGER.exception(message) raise MapComposerError(message) landcover_html.setUrl(QUrl(self.landcover_html_path)) # setup logos logos_id = ["logo-bnpb", "logo-geologi"] for logo_id in logos_id: logo_picture = composition.getComposerItemById(logo_id) if logo_picture is None: message = "%s composer item could not be found" % logo_id LOGGER.exception(message) raise MapComposerError(message) pic_path = os.path.basename(logo_picture.picturePath()) pic_path = os.path.join("logo", pic_path) logo_picture.setPicturePath(self.ash_fixtures_dir(pic_path)) # save a pdf composition.exportAsPDF(self.map_report_path) project_instance.write(QFileInfo(self.project_path)) layer_registry.removeAllMapLayers() map_renderer.setDestinationCrs(default_crs) map_renderer.setProjectionsEnabled(False) LOGGER.info("Report generation completed.")
def qgis_composer_renderer(impact_report, component): """Default Map Report Renderer using QGIS Composer. Render using qgis composer for a given impact_report data and component context. :param impact_report: ImpactReport contains data about the report that is going to be generated. :type impact_report: safe.report.impact_report.ImpactReport :param component: Contains the component metadata and context for rendering the output. :type component: safe.report.report_metadata.QgisComposerComponentsMetadata :return: Whatever type of output the component should be. .. versionadded:: 4.0 """ context = component.context """:type: safe.report.extractors.composer.QGISComposerContext""" qgis_composition_context = impact_report.qgis_composition_context # load composition object composition = QgsComposition(qgis_composition_context.map_settings) # load template main_template_folder = impact_report.metadata.template_folder # we do this condition in case custom template was found if component.template.startswith('../qgis-composer-templates/'): template_path = os.path.join(main_template_folder, component.template) else: template_path = component.template with open(template_path) as template_file: template_content = template_file.read() document = QtXml.QDomDocument() document.setContent(template_content) load_status = composition.loadFromTemplate( document, context.substitution_map) if not load_status: raise TemplateLoadingError( tr('Error loading template: %s') % template_path) # replace image path for img in context.image_elements: item_id = img.get('id') path = img.get('path') image = composition_item(composition, item_id, QgsComposerPicture) """:type: qgis.core.QgsComposerPicture""" if image and path: image.setPicturePath(path) # replace html frame for html_el in context.html_frame_elements: item_id = html_el.get('id') mode = html_el.get('mode') composer_item = composition.getComposerItemById(item_id) try: html_element = composition.getComposerHtmlByItem(composer_item) except: pass """:type: qgis.core.QgsComposerHtml""" if html_element: if mode == 'text': text = html_el.get('text') text = text if text else '' html_element.setContentMode(QgsComposerHtml.ManualHtml) html_element.setHtml(text) html_element.loadHtml() elif mode == 'url': url = html_el.get('url') html_element.setContentMode(QgsComposerHtml.Url) qurl = QUrl.fromLocalFile(url) html_element.setUrl(qurl) original_crs = impact_report.impact_function.crs destination_crs = qgis_composition_context.map_settings.destinationCrs() coord_transform = QgsCoordinateTransform(original_crs, destination_crs) # resize map extent for map_el in context.map_elements: item_id = map_el.get('id') split_count = map_el.get('grid_split_count') layers = [ layer for layer in map_el.get('layers') if isinstance( layer, QgsMapLayer) ] map_extent_option = map_el.get('extent') composer_map = composition_item(composition, item_id, QgsComposerMap) for index, layer in enumerate(layers): # we need to check whether the layer is registered or not registered_layer = ( QgsMapLayerRegistry.instance().mapLayer(layer.id())) if registered_layer: if not registered_layer == layer: layers[index] = registered_layer else: QgsMapLayerRegistry.instance().addMapLayer(layer) """:type: qgis.core.QgsComposerMap""" if composer_map: # Search for specified map extent in the template. min_x = composer_map.extent().xMinimum() if ( impact_report.use_template_extent) else None min_y = composer_map.extent().yMinimum() if ( impact_report.use_template_extent) else None max_x = composer_map.extent().xMaximum() if ( impact_report.use_template_extent) else None max_y = composer_map.extent().yMaximum() if ( impact_report.use_template_extent) else None composer_map.setKeepLayerSet(True) layer_set = [l.id() for l in layers if isinstance(l, QgsMapLayer)] composer_map.setLayerSet(layer_set) map_overview_extent = None if map_extent_option and isinstance( map_extent_option, QgsRectangle): # use provided map extent extent = coord_transform.transform(map_extent_option) for l in [layer for layer in layers if isinstance(layer, QgsMapLayer)]: layer_extent = coord_transform.transform(l.extent()) if l.name() == map_overview['id']: map_overview_extent = layer_extent else: # if map extent not provided, try to calculate extent # from list of given layers. Combine it so all layers were # shown properly extent = QgsRectangle() extent.setMinimal() for l in [layer for layer in layers if isinstance(layer, QgsMapLayer)]: # combine extent if different layer is provided. layer_extent = coord_transform.transform(l.extent()) extent.combineExtentWith(layer_extent) if l.name() == map_overview['id']: map_overview_extent = layer_extent width = extent.width() height = extent.height() longest_width = width if width > height else height half_length = longest_width / 2 margin = half_length / 5 center = extent.center() min_x = min_x or (center.x() - half_length - margin) max_x = max_x or (center.x() + half_length + margin) min_y = min_y or (center.y() - half_length - margin) max_y = max_y or (center.y() + half_length + margin) # noinspection PyCallingNonCallable square_extent = QgsRectangle(min_x, min_y, max_x, max_y) if component.key == 'population-infographic' and ( map_overview_extent): square_extent = map_overview_extent composer_map.zoomToExtent(square_extent) composer_map.renderModeUpdateCachedImage() actual_extent = composer_map.extent() # calculate intervals for grid x_interval = actual_extent.width() / split_count composer_map.grid().setIntervalX(x_interval) y_interval = actual_extent.height() / split_count composer_map.grid().setIntervalY(y_interval) # calculate legend element for leg_el in context.map_legends: item_id = leg_el.get('id') title = leg_el.get('title') layers = [ layer for layer in leg_el.get('layers') if isinstance( layer, QgsMapLayer) ] symbol_count = leg_el.get('symbol_count') column_count = leg_el.get('column_count') legend = composition_item(composition, item_id, QgsComposerLegend) """:type: qgis.core.QgsComposerLegend""" if legend: # set column count if column_count: legend.setColumnCount(column_count) elif symbol_count <= 7: legend.setColumnCount(1) else: legend.setColumnCount(symbol_count / 7 + 1) # set legend title if title is not None and not impact_report.legend_layers: legend.setTitle(title) # set legend root_group = legend.modelV2().rootGroup() for layer in layers: # we need to check whether the layer is registered or not registered_layer = ( QgsMapLayerRegistry.instance().mapLayer(layer.id())) if registered_layer: if not registered_layer == layer: layer = registered_layer else: QgsMapLayerRegistry.instance().addMapLayer(layer) # used for customizations tree_layer = root_group.addLayer(layer) if impact_report.legend_layers or ( not impact_report.multi_exposure_impact_function): QgsLegendRenderer.setNodeLegendStyle( tree_layer, QgsComposerLegendStyle.Hidden) legend.synchronizeWithModel() # process to output # in case output folder not specified if impact_report.output_folder is None: impact_report.output_folder = mkdtemp(dir=temp_dir()) output_format = component.output_format component_output_path = impact_report.component_absolute_output_path( component.key) component_output = None doc_format = QgisComposerComponentsMetadata.OutputFormat.DOC_OUTPUT template_format = QgisComposerComponentsMetadata.OutputFormat.QPT if isinstance(output_format, list): component_output = [] for i in range(len(output_format)): each_format = output_format[i] each_path = component_output_path[i] if each_format in doc_format: result_path = create_qgis_pdf_output( impact_report, each_path, composition, each_format, component) component_output.append(result_path) elif each_format == template_format: result_path = create_qgis_template_output( each_path, composition) component_output.append(result_path) elif isinstance(output_format, dict): component_output = {} for key, each_format in output_format.iteritems(): each_path = component_output_path[key] if each_format in doc_format: result_path = create_qgis_pdf_output( impact_report, each_path, composition, each_format, component) component_output[key] = result_path elif each_format == template_format: result_path = create_qgis_template_output( each_path, composition) component_output[key] = result_path elif (output_format in QgisComposerComponentsMetadata.OutputFormat.SUPPORTED_OUTPUT): component_output = None if output_format in doc_format: result_path = create_qgis_pdf_output( impact_report, component_output_path, composition, output_format, component) component_output = result_path elif output_format == template_format: result_path = create_qgis_template_output( component_output_path, composition) component_output = result_path component.output = component_output return component.output
def qgis_composer_renderer(impact_report, component): """Default Map Report Renderer using QGIS Composer. Render using qgis composer for a given impact_report data and component context :param impact_report: ImpactReport contains data about the report that is going to be generated :type impact_report: safe.report.impact_report.ImpactReport :param component: Contains the component metadata and context for rendering the output :type component: safe.report.report_metadata.QgisComposerComponentsMetadata :return: whatever type of output the component should be .. versionadded:: 4.0 """ context = component.context """:type: safe.report.extractors.composer.QGISComposerContext""" qgis_composition_context = impact_report.qgis_composition_context inasafe_context = impact_report.inasafe_context # load composition object composition = QgsComposition(qgis_composition_context.map_settings) # load template main_template_folder = impact_report.metadata.template_folder template_path = os.path.join(main_template_folder, component.template) with open(template_path) as template_file: template_content = template_file.read() document = QtXml.QDomDocument() document.setContent(template_content) load_status = composition.loadFromTemplate( document, context.substitution_map) if not load_status: raise TemplateLoadingError( tr('Error loading template: %s') % template_path) # replace image path for img in context.image_elements: item_id = img.get('id') path = img.get('path') image = composition.getComposerItemById(item_id) """:type: qgis.core.QgsComposerPicture""" if image is not None and path is not None: try: image.setPicturePath(path) except: pass # replace html frame for html_el in context.html_frame_elements: item_id = html_el.get('id') mode = html_el.get('mode') html_element = composition.getComposerItemById(item_id) """:type: qgis.core.QgsComposerHtml""" if html_element: if mode == 'text': text = html_el.get('text') text = text if text else '' html_element.setContentMode(QgsComposerHtml.ManualHtml) html_element.setHtml(text) html_element.loadHtml() elif mode == 'url': url = html_el.get('url') html_element.setContentMode(QgsComposerHtml.Url) qurl = QUrl.fromLocalFile(url) html_element.setUrl(qurl) # resize map extent for map_el in context.map_elements: item_id = map_el.get('id') split_count = map_el.get('grid_split_count') layers = map_el.get('layers') map_extent_option = map_el.get('extent') composer_map = composition.getComposerItemById(item_id) """:type: qgis.core.QgsComposerMap""" if isinstance(composer_map, QgsComposerMap): composer_map.setKeepLayerSet(True) layer_set = [l.id() for l in layers if isinstance(l, QgsMapLayer)] composer_map.setLayerSet(layer_set) if map_extent_option and isinstance( map_extent_option, QgsRectangle): # use provided map extent extent = map_extent_option else: # if map extent not provided, try to calculate extent # from list of given layers. Combine it so all layers were # shown properly extent = QgsRectangle() extent.setMinimal() for l in layers: # combine extent if different layer is provided. extent.combineExtentWith(l.extent()) width = extent.width() height = extent.height() longest_width = width if width > height else height half_length = longest_width / 2 margin = half_length / 5 center = extent.center() min_x = center.x() - half_length - margin max_x = center.x() + half_length + margin min_y = center.y() - half_length - margin max_y = center.y() + half_length + margin # noinspection PyCallingNonCallable square_extent = QgsRectangle(min_x, min_y, max_x, max_y) composer_map.zoomToExtent(square_extent) composer_map.renderModeUpdateCachedImage() actual_extent = composer_map.extent() # calculate intervals for grid x_interval = actual_extent.width() / split_count composer_map.grid().setIntervalX(x_interval) y_interval = actual_extent.height() / split_count composer_map.grid().setIntervalY(y_interval) # calculate legend element for leg_el in context.map_legends: item_id = leg_el.get('id') title = leg_el.get('title') layers = leg_el.get('layers') symbol_count = leg_el.get('symbol_count') column_count = leg_el.get('column_count') legend = composition.getComposerItemById(item_id) """:type: qgis.core.QgsComposerLegend""" if isinstance(legend, QgsComposerLegend): # set column count if column_count: legend.setColumnCount(column_count) elif symbol_count <= 5: legend.setColumnCount(1) else: legend.setColumnCount(symbol_count / 5 + 1) # set legend title if title is not None: legend.setTitle(title) # set legend root_group = legend.modelV2().rootGroup() for l in layers: # used for customizations tree_layer = root_group.addLayer(l) QgsLegendRenderer.setNodeLegendStyle( tree_layer, QgsComposerLegendStyle.Hidden) legend.synchronizeWithModel() # process to output # in case output folder not specified if impact_report.output_folder is None: impact_report.output_folder = mkdtemp(dir=temp_dir()) output_format = component.output_format component_output_path = impact_report.component_absolute_output_path( component.key) component_output = None doc_format = QgisComposerComponentsMetadata.OutputFormat.DOC_OUTPUT template_format = QgisComposerComponentsMetadata.OutputFormat.QPT if isinstance(output_format, list): component_output = [] for i in range(len(output_format)): each_format = output_format[i] each_path = component_output_path[i] if each_format in doc_format: result_path = create_qgis_pdf_output( each_path, composition, impact_report.qgis_composition_context, each_format, component) component_output.append(result_path) elif each_format == template_format: result_path = create_qgis_template_output( each_path, composition) component_output.append(result_path) elif isinstance(output_format, dict): component_output = {} for key, each_format in output_format.iteritems(): each_path = component_output_path[key] if each_format in doc_format: result_path = create_qgis_pdf_output( each_path, composition, impact_report.qgis_composition_context, each_format, component) component_output[key] = result_path elif each_format == template_format: result_path = create_qgis_template_output( each_path, composition) component_output[key] = result_path elif (output_format in QgisComposerComponentsMetadata.OutputFormat.SUPPORTED_OUTPUT): component_output = None if output_format in doc_format: result_path = create_qgis_pdf_output( component_output_path, composition, impact_report.qgis_composition_context, output_format, component) component_output = result_path elif output_format == template_format: result_path = create_qgis_template_output( component_output_path, composition) component_output = result_path component.output = component_output return component.output
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()
with open(template_file) as tf: template_content = tf.read() template_document = QDomDocument() template_document.setContent(template_content) map_settings = iface.mapCanvas().mapSettings() composition = QgsComposition(map_settings) composition.loadFromTemplate(template_document) # Set up atlas to produce one report per region vector_layer = dataobjects.getObjectFromUri(vector) atlas = composition.atlasComposition() atlas.setCoverageLayer(vector_layer) atlas.setEnabled(True) composition.setAtlasMode(QgsComposition.ExportAtlas) atlas_map = composition.getComposerItemById("atlas_map") atlas_map.setAtlasDriven(True) atlas_map.setLayerSet([vector_layer.id(), raster_layer.id()]) atlas_map.setKeepLayerSet(True) atlas_legend = composition.getComposerItemById("atlas_legend") raster_legend = atlas_legend.modelV2().rootGroup().addLayer(raster_layer) QgsLegendRenderer.setNodeLegendStyle(raster_legend, QgsComposerLegendStyle.Hidden) atlas_legend.updateLegend() # atlas_legend.refreshLayerLegend(raster_legend) stats_table = composition.getComposerItemById("stats_table").multiFrame() stats_table.setContentMode(QgsComposerHtml.ManualHtml) title_label = composition.getComposerItemById("title_label") title_text = title_label.text() description_label = composition.getComposerItemById("description_label") description_text = description_label.text()
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
def run(self, *args, **kwargs): """ :param templatePath: The file path to the user-defined template. :param entityFieldName: The name of the column for the specified entity which must exist in the data source view or table. :param entityFieldValue: The value for filtering the records in the data source view or table. :param outputMode: Whether the output composition should be an image or PDF. :param filePath: The output file where the composition will be written to. Applies to single mode output generation. :param dataFields: List containing the field names whose values will be used to name the files. This is used in multiple mode configuration. :param fileExtension: The output file format. Used in multiple mode configuration. :param dbmodel: In order to name the files using the custom column mapping, a callable sqlalchemy data model must be specified. """ #Unpack arguments templatePath = args[0] entityFieldName = args[1] entityFieldValue = args[2] outputMode = args[3] filePath = kwargs.get("filePath", None) dataFields = kwargs.get("dataFields", []) fileExtension = kwargs.get("fileExtension", "") dataModel = kwargs.get("dbmodel", None) templateFile = QFile(templatePath) if not templateFile.open(QIODevice.ReadOnly): return (False, QApplication.translate("DocumentGenerator", "Cannot read template file.")) templateDoc = QDomDocument() if templateDoc.setContent(templateFile): composerDS = ComposerDataSource.create(templateDoc) spatialFieldsConfig = SpatialFieldsConfiguration.create( templateDoc) composerDS.setSpatialFieldsConfig(spatialFieldsConfig) #Execute query dsTable, records = self._execQuery(composerDS.name(), entityFieldName, entityFieldValue) if records == None: return (False, QApplication.translate( "DocumentGenerator", "No matching records in the database")) """ Iterate through records where a single file output will be generated for each matching record. """ for rec in records: composition = QgsComposition(self._mapRenderer) composition.loadFromTemplate(templateDoc) #Set value of composer items based on the corresponding db values for composerId in composerDS.dataFieldMappings().reverse: #Use composer item id since the uuid is stripped off composerItem = composition.getComposerItemById(composerId) if composerItem != None: fieldName = composerDS.dataFieldName(composerId) fieldValue = getattr(rec, fieldName) self._composerItemValueHandler(composerItem, fieldValue) #Create memory layers for spatial features and add them to the map for mapId, spfmList in spatialFieldsConfig.spatialFieldsMapping( ).iteritems(): mapItem = composition.getComposerItemById(mapId) if mapItem != None: #Clear any previous memory layer self.clearTemporaryLayers() for spfm in spfmList: #Use the value of the label field to name the layer layerName = getattr(rec, spfm.labelField()) #Extract the geometry using geoalchemy spatial capabilities geomFunc = getattr( rec, spfm.spatialField()).ST_AsText() geomWKT = self._dbSession.scalar(geomFunc) #Create reference layer with feature refLayer = self._buildVectorLayer(layerName) #Add feature bbox = self._addFeatureToLayer(refLayer, geomWKT) bbox.scale(spfm.zoomLevel()) #Add layer to map QgsMapLayerRegistry.instance().addMapLayer( refLayer) self._iface.mapCanvas().setExtent(bbox) self._iface.mapCanvas().refresh() #mapItem.storeCurrentLayerSet() #mapItem.updateCachedImage() #Add layer to memory layer list self._memoryLayers.append(refLayer) mapItem.setNewExtent(self._mapRenderer.extent()) #Build output path and generate composition if filePath != None and len(dataFields) == 0: self._writeOutput(composition, outputMode, filePath) elif filePath == None and len(dataFields) > 0: docFileName = self._buildFileName(dataModel, entityFieldName, entityFieldValue, dataFields, fileExtension) if docFileName == "": return ( False, QApplication.translate( "DocumentGenerator", "File name could not be generated from the data fields." )) outputDir = self._composerOutputPath() if outputDir == None: return ( False, QApplication.translate( "DocumentGenerator", "System could not read the location of the output directory in the registry." )) qDir = QDir() if not qDir.exists(outputDir): return (False, QApplication.translate( "DocumentGenerator", "Output directory does not exist")) absDocPath = unicode(outputDir) + "/" + docFileName self._writeOutput(composition, outputMode, absDocPath) #Clear temporary layers self.clearTemporaryLayers() return (True, "Success") return (False, "Composition could not be generated")
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()
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
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()
def generate_report(self): # Generate pdf report from impact/hazard if not self.impact_exists: # Cannot generate report when no impact layer present return project_path = os.path.join( self.report_path, 'project-%s.qgs' % self.locale) project_instance = QgsProject.instance() project_instance.setFileName(project_path) project_instance.read() # Set up the map renderer that will be assigned to the composition map_renderer = CANVAS.mapRenderer() # Set the labelling engine for the canvas labelling_engine = QgsPalLabeling() map_renderer.setLabelingEngine(labelling_engine) # Enable on the fly CRS transformations map_renderer.setProjectionsEnabled(True) default_crs = map_renderer.destinationCrs() crs = QgsCoordinateReferenceSystem('EPSG:4326') map_renderer.setDestinationCrs(crs) # get layer registry layer_registry = QgsMapLayerRegistry.instance() layer_registry.removeAllMapLayers() # add impact layer population_affected_layer = read_qgis_layer( self.population_aggregate_path, self.tr('People Affected')) layer_registry.addMapLayer(population_affected_layer, True) # add boundary mask boundary_mask = read_qgis_layer( self.flood_fixtures_dir('boundary-mask.shp')) layer_registry.addMapLayer(boundary_mask, False) # add hazard layer hazard_layer = read_qgis_layer( self.hazard_path, self.tr('Flood Depth (cm)')) layer_registry.addMapLayer(hazard_layer, True) # add boundary layer boundary_layer = read_qgis_layer( self.flood_fixtures_dir('boundary-5.shp')) layer_registry.addMapLayer(boundary_layer, False) CANVAS.setExtent(boundary_layer.extent()) CANVAS.refresh() # add basemap layer # this code uses OpenlayersPlugin base_map = QgsRasterLayer( self.flood_fixtures_dir('jakarta.jpg')) layer_registry.addMapLayer(base_map, False) CANVAS.refresh() template_path = self.flood_fixtures_dir('realtime-flood.qpt') with open(template_path) as f: template_content = f.read() document = QDomDocument() document.setContent(template_content) # set destination CRS to Jakarta CRS # EPSG:32748 # This allows us to use the scalebar in meter unit scale crs = QgsCoordinateReferenceSystem('EPSG:32748') map_renderer.setDestinationCrs(crs) # Now set up the composition composition = QgsComposition(map_renderer) subtitution_map = self.event_dict() LOGGER.debug(subtitution_map) # load composition object from template result = composition.loadFromTemplate(document, subtitution_map) if not result: LOGGER.exception( 'Error loading template %s with keywords\n %s', template_path, subtitution_map) raise MapComposerError # get main map canvas on the composition and set extent map_canvas = composition.getComposerItemById('map-canvas') if map_canvas: map_canvas.setNewExtent(map_canvas.currentMapExtent()) map_canvas.renderModeUpdateCachedImage() else: LOGGER.exception('Map canvas could not be found in template %s', template_path) raise MapComposerError # get map legend on the composition map_legend = composition.getComposerItemById('map-legend') if map_legend: # show only legend for Flood Depth # ''.star # showed_legend = [layer_id for layer_id in map_renderer.layerSet() # if layer_id.startswith('Flood_Depth')] # LOGGER.info(showed_legend) # LOGGER.info(map_renderer.layerSet()) # LOGGER.info(hazard_layer.id()) # map_legend.model().setLayerSet(showed_legend) # map_legend.modelV2().clear() # print dir(map_legend.modelV2()) # map_legend.setLegendFilterByMapEnabled(True) map_legend.model().setLayerSet( [hazard_layer.id(), population_affected_layer.id()]) else: LOGGER.exception('Map legend could not be found in template %s', template_path) raise MapComposerError content_analysis = composition.getComposerItemById( 'content-analysis-result') if not content_analysis: message = 'Content analysis composer item could not be found' LOGGER.exception(message) raise MapComposerError(message) content_analysis_html = content_analysis.multiFrame() if content_analysis_html: # set url to generated html analysis_html_path = self.generate_analysis_result_html() # We're using manual HTML to avoid memory leak and segfault # happened when using Url Mode content_analysis_html.setContentMode(QgsComposerHtml.ManualHtml) with open(analysis_html_path) as f: content_analysis_html.setHtml(f.read()) content_analysis_html.loadHtml() else: message = 'Content analysis HTML not found in template' LOGGER.exception(message) raise MapComposerError(message) # save a pdf composition.exportAsPDF(self.map_report_path) project_instance.write(QFileInfo(project_path)) layer_registry.removeAllMapLayers() map_renderer.setDestinationCrs(default_crs) map_renderer.setProjectionsEnabled(False)
def run(self, *args, **kwargs): """ :param templatePath: The file path to the user-defined template. :param entityFieldName: The name of the column for the specified entity which must exist in the data source view or table. :param entityFieldValue: The value for filtering the records in the data source view or table. :param outputMode: Whether the output composition should be an image or PDF. :param filePath: The output file where the composition will be written to. Applies to single mode output generation. :param dataFields: List containing the field names whose values will be used to name the files. This is used in multiple mode configuration. :param fileExtension: The output file format. Used in multiple mode configuration. :param data_source: Name of the data source table or view whose row values will be used to name output files if the options has been specified by the user. """ templatePath = args[0] entityFieldName = args[1] entityFieldValue = args[2] outputMode = args[3] filePath = kwargs.get("filePath", None) dataFields = kwargs.get("dataFields", []) fileExtension = kwargs.get("fileExtension", "") data_source = kwargs.get("data_source", "") templateFile = QFile(templatePath) if not templateFile.open(QIODevice.ReadOnly): return False, QApplication.translate("DocumentGenerator", "Cannot read template file.") templateDoc = QDomDocument() if templateDoc.setContent(templateFile): composerDS = ComposerDataSource.create(templateDoc) spatialFieldsConfig = SpatialFieldsConfiguration.create( templateDoc) composerDS.setSpatialFieldsConfig(spatialFieldsConfig) #Check if data source exists and return if it doesn't if not self.data_source_exists(composerDS): msg = QApplication.translate( "DocumentGenerator", u"'{0}' data source does not exist in the database." u"\nPlease contact your database " u"administrator.".format(composerDS.name())) return False, msg #Set file name value formatter self._file_name_value_formatter = EntityValueFormatter( name=data_source) #Register field names to be used for file naming self._file_name_value_formatter.register_columns(dataFields) #TODO: Need to automatically register custom configuration collections #Photo config collection ph_config_collection = PhotoConfigurationCollection.create( templateDoc) #Table configuration collection table_config_collection = TableConfigurationCollection.create( templateDoc) #Create chart configuration collection object chart_config_collection = ChartConfigurationCollection.create( templateDoc) #Load the layers required by the table composer items self._table_mem_layers = load_table_layers(table_config_collection) #Execute query dsTable, records = self._exec_query(composerDS.name(), entityFieldName, entityFieldValue) if records is None or len(records) == 0: return False, QApplication.translate( "DocumentGenerator", "No matching records in the database") """ Iterate through records where a single file output will be generated for each matching record. """ for rec in records: composition = QgsComposition(self._map_renderer) composition.loadFromTemplate(templateDoc) ref_layer = None #Set value of composer items based on the corresponding db values for composerId in composerDS.dataFieldMappings().reverse: #Use composer item id since the uuid is stripped off composerItem = composition.getComposerItemById(composerId) if not composerItem is None: fieldName = composerDS.dataFieldName(composerId) fieldValue = getattr(rec, fieldName) self._composeritem_value_handler( composerItem, fieldValue) # Extract photo information self._extract_photo_info(composition, ph_config_collection, rec) # Set table item values based on configuration information self._set_table_data(composition, table_config_collection, rec) # Refresh non-custom map composer items self._refresh_composer_maps( composition, spatialFieldsConfig.spatialFieldsMapping().keys()) # Create memory layers for spatial features and add them to the map for mapId, spfmList in spatialFieldsConfig.spatialFieldsMapping( ).iteritems(): map_item = composition.getComposerItemById(mapId) if not map_item is None: # #Clear any previous map memory layer #self.clear_temporary_map_layers() for spfm in spfmList: #Use the value of the label field to name the layer lbl_field = spfm.labelField() spatial_field = spfm.spatialField() if not spatial_field: continue if lbl_field: if hasattr(rec, spfm.labelField()): layerName = getattr(rec, spfm.labelField()) else: layerName = self._random_feature_layer_name( spatial_field) else: layerName = self._random_feature_layer_name( spatial_field) #Extract the geometry using geoalchemy spatial capabilities geom_value = getattr(rec, spatial_field) if geom_value is None: continue geom_func = geom_value.ST_AsText() geomWKT = self._dbSession.scalar(geom_func) #Get geometry type geom_type, srid = geometryType( composerDS.name(), spatial_field) #Create reference layer with feature ref_layer = self._build_vector_layer( layerName, geom_type, srid) if ref_layer is None or not ref_layer.isValid(): continue #Add feature bbox = self._add_feature_to_layer( ref_layer, geomWKT) bbox.scale(spfm.zoomLevel()) #Workaround for zooming to single point extent if ref_layer.wkbType() == QGis.WKBPoint: canvas_extent = self._iface.mapCanvas( ).fullExtent() cnt_pnt = bbox.center() canvas_extent.scale(1.0 / 32, cnt_pnt) bbox = canvas_extent #Style layer based on the spatial field mapping symbol layer symbol_layer = spfm.symbolLayer() if not symbol_layer is None: ref_layer.rendererV2().symbols( )[0].changeSymbolLayer(0, spfm.symbolLayer()) ''' Add layer to map and ensure its always added at the top ''' self.map_registry.addMapLayer(ref_layer) self._iface.mapCanvas().setExtent(bbox) self._iface.mapCanvas().refresh() # Add layer to map memory layer list self._map_memory_layers.append(ref_layer.id()) self._hide_layer(ref_layer) ''' Use root layer tree to get the correct ordering of layers in the legend ''' self._refresh_map_item(map_item) #Extract chart information and generate chart self._generate_charts(composition, chart_config_collection, rec) #Build output path and generate composition if not filePath is None and len(dataFields) == 0: self._write_output(composition, outputMode, filePath) elif filePath is None and len(dataFields) > 0: docFileName = self._build_file_name( data_source, entityFieldName, entityFieldValue, dataFields, fileExtension) # Replace unsupported characters in Windows file naming docFileName = docFileName.replace('/', '_').replace \ ('\\', '_').replace(':', '_').strip('*?"<>|') if not docFileName: return ( False, QApplication.translate( "DocumentGenerator", "File name could not be generated from the data fields." )) outputDir = self._composer_output_path() if outputDir is None: return ( False, QApplication.translate( "DocumentGenerator", "System could not read the location of the output directory in the registry." )) qDir = QDir() if not qDir.exists(outputDir): return (False, QApplication.translate( "DocumentGenerator", "Output directory does not exist")) absDocPath = u"{0}/{1}".format(outputDir, docFileName) self._write_output(composition, outputMode, absDocPath) return True, "Success" return False, "Document composition could not be generated"
def run(self,*args,**kwargs): """ :param templatePath: The file path to the user-defined template. :param entityFieldName: The name of the column for the specified entity which must exist in the data source view or table. :param entityFieldValue: The value for filtering the records in the data source view or table. :param outputMode: Whether the output composition should be an image or PDF. :param filePath: The output file where the composition will be written to. Applies to single mode output generation. :param dataFields: List containing the field names whose values will be used to name the files. This is used in multiple mode configuration. :param fileExtension: The output file format. Used in multiple mode configuration. :param dbmodel: In order to name the files using the custom column mapping, a callable sqlalchemy data model must be specified. """ #Unpack arguments templatePath = args[0] entityFieldName = args[1] entityFieldValue = args[2] outputMode = args[3] filePath = kwargs.get("filePath",None) dataFields = kwargs.get("dataFields",[]) fileExtension = kwargs.get("fileExtension","") dataModel = kwargs.get("dbmodel",None) templateFile = QFile(templatePath) if not templateFile.open(QIODevice.ReadOnly): return (False,QApplication.translate("DocumentGenerator","Cannot read template file.")) templateDoc = QDomDocument() if templateDoc.setContent(templateFile): composerDS = ComposerDataSource.create(templateDoc) spatialFieldsConfig = SpatialFieldsConfiguration.create(templateDoc) composerDS.setSpatialFieldsConfig(spatialFieldsConfig) #Execute query dsTable,records = self._execQuery(composerDS.name(), entityFieldName, entityFieldValue) if records == None: return (False,QApplication.translate("DocumentGenerator","No matching records in the database")) """ Iterate through records where a single file output will be generated for each matching record. """ for rec in records: composition = QgsComposition(self._mapRenderer) composition.loadFromTemplate(templateDoc) #Set value of composer items based on the corresponding db values for composerId in composerDS.dataFieldMappings().reverse: #Use composer item id since the uuid is stripped off composerItem = composition.getComposerItemById(composerId) if composerItem != None: fieldName = composerDS.dataFieldName(composerId) fieldValue = getattr(rec,fieldName) self._composerItemValueHandler(composerItem, fieldValue) #Create memory layers for spatial features and add them to the map for mapId,spfmList in spatialFieldsConfig.spatialFieldsMapping().iteritems(): mapItem = composition.getComposerItemById(mapId) if mapItem!= None: #Clear any previous memory layer self.clearTemporaryLayers() for spfm in spfmList: #Use the value of the label field to name the layer layerName = getattr(rec,spfm.labelField()) #Extract the geometry using geoalchemy spatial capabilities geomFunc = getattr(rec,spfm.spatialField()).ST_AsText() geomWKT = self._dbSession.scalar(geomFunc) #Create reference layer with feature refLayer = self._buildVectorLayer(layerName) #Add feature bbox = self._addFeatureToLayer(refLayer, geomWKT) bbox.scale(spfm.zoomLevel()) #Add layer to map QgsMapLayerRegistry.instance().addMapLayer(refLayer) self._iface.mapCanvas().setExtent(bbox) self._iface.mapCanvas().refresh() #mapItem.storeCurrentLayerSet() #mapItem.updateCachedImage() #Add layer to memory layer list self._memoryLayers.append(refLayer) mapItem.setNewExtent(self._mapRenderer.extent()) #Build output path and generate composition if filePath != None and len(dataFields) == 0: self._writeOutput(composition,outputMode,filePath) elif filePath == None and len(dataFields) > 0: docFileName = self._buildFileName(dataModel,entityFieldName,entityFieldValue,dataFields,fileExtension) if docFileName == "": return (False,QApplication.translate("DocumentGenerator", "File name could not be generated from the data fields.")) outputDir = self._composerOutputPath() if outputDir == None: return (False,QApplication.translate("DocumentGenerator", "System could not read the location of the output directory in the registry.")) qDir = QDir() if not qDir.exists(outputDir): return (False,QApplication.translate("DocumentGenerator", "Output directory does not exist")) absDocPath = unicode(outputDir) + "/" + docFileName self._writeOutput(composition,outputMode,absDocPath) #Clear temporary layers self.clearTemporaryLayers() return (True,"Success") return (False,"Composition could not be generated")
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)
def exportToPdf(self, print_context, targetFile=None): self.iface.mapCanvas().setDestinationCrs( self.settings_instance.projection.crs()) myComposition = QgsComposition(self.iface.mapCanvas().mapSettings()) template = self.env.get_template(print_context['template']) custom_qpt = template.render(CONTEXT=print_context) myDocument = QDomDocument() myDocument.setContent(custom_qpt) myComposition.loadFromTemplate(myDocument) suggestedFile = os.path.join( self.settings_instance.projectFolderPath.text(), print_context['title'] + ".pdf") if not targetFile: targetFile = QFileDialog.getSaveFileName( None, "Export " + print_context['job'], suggestedFile, "*.pdf") interactive = True if not targetFile: return else: interactive = None outputDir = tempfile.gettempdir() with open(os.path.join(targetFile + '.qpt'), "wb") as qpt_file: qpt_file.write(custom_qpt) if print_context['type'] == 'report': myComposition.exportAsPDF(targetFile) elif print_context['type'] == 'map': print myComposition.getComposerMapById(0) print myComposition.getComposerItemById('5') for composer_map in myComposition.composerMapItems(): print composer_map, composer_map.id() fdtm_extent = None for layer in [ self.settings_instance.EPpLayer, self.settings_instance.EApLayer, self.settings_instance.WRLayer, self.settings_instance.WDSLayer ]: if layer.featureCount() > 0: if fdtm_extent: fdtm_extent.combineExtentWith(layer.extent()) else: fdtm_extent = layer.extent() fdtm_extent.scale(1.1) composer_map.zoomToExtent(fdtm_extent) composer_map.updateItem() myComposition.refreshItems() myComposition.exportAsPDF(targetFile) elif print_context['type'] == 'mapppp': print print_context myComposition = QgsComposition( self.iface.mapCanvas().mapSettings()) myComposition.setPlotStyle(QgsComposition.Print) myComposition.setPaperSize(297, 210) composer_map = QgsComposerMap(myComposition, 10, 10, 190, 190) fdtm_extent = self.settings_instance.EPpLayer.extent() for layer in [ self.settings_instance.EApLayer, self.settings_instance.WRLayer, self.settings_instance.WDSLayer ]: fdtm_extent.combineExtentWith(layer.extent()) composer_map.zoomToExtent(fdtm_extent) composer_map.updateItem() myComposition.addItem(composer_map) table = QgsComposerAttributeTable(myComposition) table.setItemPosition(205, 170) table.setVectorLayer(QgsMapLayerRegistry.instance().mapLayer( print_context['id'])) table.setMaximumNumberOfFeatures(20) table.setFilterFeatures(True) col1 = QgsComposerTableColumn() col1.setAttribute('types') col1.setHeading("types") col2 = QgsComposerTableColumn() col2.setAttribute('project units') col2.setHeading("project units") col3 = QgsComposerTableColumn() col3.setAttribute('Areas') col3.setHeading("Areas") col4 = QgsComposerTableColumn() col4.setAttribute('Lengths') col4.setHeading("Lengths") col5 = QgsComposerTableColumn() col5.setAttribute('Cost') col5.setHeading("Cost") table.setColumns([col1, col2, col3, col4, col5]) myComposition.addItem(table) myComposition.exportAsPDF(targetFile) elif print_context['type'] == 'atlas': myComposition.setAtlasMode(QgsComposition.ExportAtlas) for composer_map in myComposition.composerMapItems(): print composer_map, composer_map.id() atlas = myComposition.atlasComposition() atlas.setComposerMap(composer_map) #DEPRECATED atlas.setPredefinedScales(self.PREDEFINED_SCALES) composer_map.setAtlasDriven(True) composer_map.setAtlasScalingMode(QgsComposerMap.Predefined) atlas.beginRender() rendered_pdf = [] progress = progressBar(self, "exporting " + print_context['job'], atlas.numFeatures()) 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(outputDir, file_path) myComposition.exportAsPDF(path) rendered_pdf.append(path) progress.setStep(i) progress.stop(print_context['job'] + "exported to " + targetFile) atlas.endRender() merge_pdfs(rendered_pdf, targetFile) if interactive: open_file(targetFile) return targetFile
def generate_report(self): # Generate pdf report from impact/hazard if not self.impact_exists: # Cannot generate report when no impact layer present return project_path = os.path.join(self.report_path, 'project-%s.qgs' % self.locale) project_instance = QgsProject.instance() project_instance.setFileName(project_path) project_instance.read() # Set up the map renderer that will be assigned to the composition map_renderer = CANVAS.mapRenderer() # Set the labelling engine for the canvas labelling_engine = QgsPalLabeling() map_renderer.setLabelingEngine(labelling_engine) # Enable on the fly CRS transformations map_renderer.setProjectionsEnabled(True) default_crs = map_renderer.destinationCrs() crs = QgsCoordinateReferenceSystem('EPSG:4326') map_renderer.setDestinationCrs(crs) # get layer registry layer_registry = QgsMapLayerRegistry.instance() layer_registry.removeAllMapLayers() # add impact layer population_affected_layer = read_qgis_layer( self.population_aggregate_path, self.tr('People Affected')) layer_registry.addMapLayer(population_affected_layer, True) # add boundary mask boundary_mask = read_qgis_layer( self.flood_fixtures_dir('boundary-mask.shp')) layer_registry.addMapLayer(boundary_mask, False) # add hazard layer hazard_layer = read_qgis_layer(self.hazard_path, self.tr('Flood Depth (cm)')) layer_registry.addMapLayer(hazard_layer, True) # add boundary layer boundary_layer = read_qgis_layer( self.flood_fixtures_dir('boundary-5.shp')) layer_registry.addMapLayer(boundary_layer, False) CANVAS.setExtent(boundary_layer.extent()) CANVAS.refresh() # add basemap layer # this code uses OpenlayersPlugin base_map = QgsRasterLayer(self.flood_fixtures_dir('jakarta.jpg')) layer_registry.addMapLayer(base_map, False) CANVAS.refresh() template_path = self.flood_fixtures_dir('realtime-flood.qpt') with open(template_path) as f: template_content = f.read() document = QDomDocument() document.setContent(template_content) # set destination CRS to Jakarta CRS # EPSG:32748 # This allows us to use the scalebar in meter unit scale crs = QgsCoordinateReferenceSystem('EPSG:32748') map_renderer.setDestinationCrs(crs) # Now set up the composition composition = QgsComposition(map_renderer) subtitution_map = self.event_dict() LOGGER.debug(subtitution_map) # load composition object from template result = composition.loadFromTemplate(document, subtitution_map) if not result: LOGGER.exception('Error loading template %s with keywords\n %s', template_path, subtitution_map) raise MapComposerError # get main map canvas on the composition and set extent map_canvas = composition.getComposerItemById('map-canvas') if map_canvas: map_canvas.setNewExtent(map_canvas.currentMapExtent()) map_canvas.renderModeUpdateCachedImage() else: LOGGER.exception('Map canvas could not be found in template %s', template_path) raise MapComposerError # get map legend on the composition map_legend = composition.getComposerItemById('map-legend') if map_legend: # show only legend for Flood Depth # ''.star # showed_legend = [layer_id for layer_id in map_renderer.layerSet() # if layer_id.startswith('Flood_Depth')] # LOGGER.info(showed_legend) # LOGGER.info(map_renderer.layerSet()) # LOGGER.info(hazard_layer.id()) # map_legend.model().setLayerSet(showed_legend) # map_legend.modelV2().clear() # print dir(map_legend.modelV2()) # map_legend.setLegendFilterByMapEnabled(True) map_legend.model().setLayerSet( [hazard_layer.id(), population_affected_layer.id()]) else: LOGGER.exception('Map legend could not be found in template %s', template_path) raise MapComposerError content_analysis = composition.getComposerItemById( 'content-analysis-result') if not content_analysis: message = 'Content analysis composer item could not be found' LOGGER.exception(message) raise MapComposerError(message) content_analysis_html = content_analysis.multiFrame() if content_analysis_html: # set url to generated html analysis_html_path = self.generate_analysis_result_html() # We're using manual HTML to avoid memory leak and segfault # happened when using Url Mode content_analysis_html.setContentMode(QgsComposerHtml.ManualHtml) with open(analysis_html_path) as f: content_analysis_html.setHtml(f.read()) content_analysis_html.loadHtml() else: message = 'Content analysis HTML not found in template' LOGGER.exception(message) raise MapComposerError(message) # save a pdf composition.exportAsPDF(self.map_report_path) project_instance.write(QFileInfo(project_path)) layer_registry.removeAllMapLayers() map_renderer.setDestinationCrs(default_crs) map_renderer.setProjectionsEnabled(False)
def run(self, *args, **kwargs): """ :param templatePath: The file path to the user-defined template. :param entityFieldName: The name of the column for the specified entity which must exist in the data source view or table. :param entityFieldValue: The value for filtering the records in the data source view or table. :param outputMode: Whether the output composition should be an image or PDF. :param filePath: The output file where the composition will be written to. Applies to single mode output generation. :param dataFields: List containing the field names whose values will be used to name the files. This is used in multiple mode configuration. :param fileExtension: The output file format. Used in multiple mode configuration. :param data_source: Name of the data source table or view whose row values will be used to name output files if the options has been specified by the user. """ templatePath = args[0] entityFieldName = args[1] entityFieldValue = args[2] outputMode = args[3] filePath = kwargs.get("filePath", None) dataFields = kwargs.get("dataFields", []) fileExtension = kwargs.get("fileExtension", "") data_source = kwargs.get("data_source", "") templateFile = QFile(templatePath) if not templateFile.open(QIODevice.ReadOnly): return False, QApplication.translate("DocumentGenerator", "Cannot read template file.") templateDoc = QDomDocument() if templateDoc.setContent(templateFile): composerDS = ComposerDataSource.create(templateDoc) spatialFieldsConfig = SpatialFieldsConfiguration.create(templateDoc) composerDS.setSpatialFieldsConfig(spatialFieldsConfig) #Check if data source exists and return if it doesn't if not self.data_source_exists(composerDS): msg = QApplication.translate("DocumentGenerator", u"'{0}' data source does not exist in the database." u"\nPlease contact your database " u"administrator.".format(composerDS.name())) return False, msg #TODO: Need to automatically register custom configuration collections #Photo config collection ph_config_collection = PhotoConfigurationCollection.create(templateDoc) #Table configuration collection table_config_collection = TableConfigurationCollection.create(templateDoc) #Create chart configuration collection object chart_config_collection = ChartConfigurationCollection.create(templateDoc) #Load the layers required by the table composer items self._table_mem_layers = load_table_layers(table_config_collection) #Execute query dsTable,records = self._exec_query(composerDS.name(), entityFieldName, entityFieldValue) if records is None or len(records) == 0: return False, QApplication.translate("DocumentGenerator", "No matching records in the database") """ Iterate through records where a single file output will be generated for each matching record. """ for rec in records: composition = QgsComposition(self._map_renderer) composition.loadFromTemplate(templateDoc) #Set value of composer items based on the corresponding db values for composerId in composerDS.dataFieldMappings().reverse: #Use composer item id since the uuid is stripped off composerItem = composition.getComposerItemById(composerId) if not composerItem is None: fieldName = composerDS.dataFieldName(composerId) fieldValue = getattr(rec,fieldName) self._composeritem_value_handler(composerItem, fieldValue) #Extract photo information self._extract_photo_info(composition, ph_config_collection, rec) #Set table item values based on configuration information self._set_table_data(composition, table_config_collection, rec) #Refresh non-custom map composer items self._refresh_composer_maps(composition, spatialFieldsConfig.spatialFieldsMapping().keys()) #Create memory layers for spatial features and add them to the map for mapId,spfmList in spatialFieldsConfig.spatialFieldsMapping().iteritems(): map_item = composition.getComposerItemById(mapId) if not map_item is None: #Clear any previous map memory layer self.clear_temporary_map_layers() for spfm in spfmList: #Use the value of the label field to name the layer lbl_field = spfm.labelField() spatial_field = spfm.spatialField() if not spatial_field: continue if lbl_field: if hasattr(rec, spfm.labelField()): layerName = getattr(rec, spfm.labelField()) else: layerName = self._random_feature_layer_name(spatial_field) else: layerName = self._random_feature_layer_name(spatial_field) #Extract the geometry using geoalchemy spatial capabilities geom_value = getattr(rec, spatial_field) if geom_value is None: continue geom_func = geom_value.ST_AsText() geomWKT = self._dbSession.scalar(geom_func) #Get geometry type geom_type, srid = geometryType(composerDS.name(), spatial_field) #Create reference layer with feature ref_layer = self._build_vector_layer(layerName, geom_type, srid) if ref_layer is None or not ref_layer.isValid(): continue #Add feature bbox = self._add_feature_to_layer(ref_layer, geomWKT) bbox.scale(spfm.zoomLevel()) #Workaround for zooming to single point extent if ref_layer.wkbType() == QGis.WKBPoint: canvas_extent = self._iface.mapCanvas().fullExtent() cnt_pnt = bbox.center() canvas_extent.scale(1.0/32, cnt_pnt) bbox = canvas_extent #Style layer based on the spatial field mapping symbol layer symbol_layer = spfm.symbolLayer() if not symbol_layer is None: ref_layer.rendererV2().symbols()[0].changeSymbolLayer(0,spfm.symbolLayer()) ''' Add layer to map and ensure its always added at the top ''' QgsMapLayerRegistry.instance().addMapLayer(ref_layer, False) QgsProject.instance().layerTreeRoot().insertLayer(0, ref_layer) self._iface.mapCanvas().setExtent(bbox) self._iface.mapCanvas().refresh() #Add layer to map memory layer list self._map_memory_layers.append(ref_layer) ''' Use root layer tree to get the correct ordering of layers in the legend ''' self._refresh_map_item(map_item) #Extract chart information and generate chart self._generate_charts(composition, chart_config_collection, rec) #Build output path and generate composition if not filePath is None and len(dataFields) == 0: self._write_output(composition, outputMode, filePath) elif filePath is None and len(dataFields) > 0: docFileName = self._build_file_name(data_source, entityFieldName, entityFieldValue, dataFields, fileExtension) if not docFileName: return (False, QApplication.translate("DocumentGenerator", "File name could not be generated from the data fields.")) outputDir = self._composer_output_path() if outputDir is None: return (False, QApplication.translate("DocumentGenerator", "System could not read the location of the output directory in the registry.")) qDir = QDir() if not qDir.exists(outputDir): return (False, QApplication.translate("DocumentGenerator", "Output directory does not exist")) absDocPath = u"{0}/{1}".format(outputDir, docFileName) self._write_output(composition, outputMode, absDocPath) #Clear temporary layers self.clear_temporary_layers() return True, "Success" return False, "Document composition could not be generated"