def layerRenderByComposer(self, layer, outName): self.hideOtherLayers(layer) mapRenderer = self.iface.mapCanvas().mapRenderer() mapRenderer.setLayerSet([layer.id()]) c = QgsComposition(mapRenderer) c.setPlotStyle(QgsComposition.Print) c.setPaperSize(297, 210) c.setPrintResolution(300) x, y = 0, 0 w, h = c.paperWidth(), c.paperHeight() composerMap = QgsComposerMap(c, x, y, w, h) composerMap.setMapCanvas(self.iface.mapCanvas()) c.addItem(composerMap) legend = QgsComposerLegend(c) root = QgsProject.instance().layerTreeRoot().clone() root = self.buildLegendLayerTree(root, [layer]) legend.modelV2().setRootGroup(root) legend.setItemPosition(250, 20) legend.setResizeToContents(True) c.addItem(legend) label = QgsComposerLabel(c) label.setMarginX(125) label.setMarginY(10) label.setText(layer.name()) label.setFont(QFont('PMinliu', 20, QFont.Bold)) label.adjustSizeToText() c.addItem(label) item = QgsComposerScaleBar(c) item.setStyle('Double Box') # optionally modify the style item.setComposerMap(composerMap) item.applyDefaultSize() item.setItemPosition(10, 190) c.addItem(item) dpmm = 300 / 25.4 width = int(dpmm * c.paperWidth()) width = int(width / 2) * 2 height = int(dpmm * c.paperHeight()) height = int(height / 2) * 2 Dots = int(dpmm * 1000 / 2) * 2 # create output image and initialize it image = QImage(QSize(width, height), QImage.Format_ARGB32) image.setDotsPerMeterX(Dots) image.setDotsPerMeterY(Dots) image.fill(0) # render the composition imagePainter = QPainter(image) c.renderPage(imagePainter, 0) imagePainter.end() outName = os.path.join(self.workingFolder, outName + '.png') image.save(outName, "png") return outName
def export_view(self): ''' Export current view to PDF ''' # Load template from file s = QSettings() f = s.value("cadastre/composerTemplateFile", '', type=str) if not os.path.exists(f): f = '%s/composers/paysage_a4.qpt' % os.path.dirname(__file__) s.setValue("cadastre/composerTemplateFile", f) QApplication.setOverrideCursor(Qt.WaitCursor) tf = file(f, 'rt') tc= tf.read() tf.close() d= QDomDocument() d.setContent(tc) # Create composition ms = self.iface.mapCanvas().mapSettings() c = QgsComposition(ms) c.loadFromTemplate(d) c.setPlotStyle(QgsComposition.Print) #c.setPrintResolution(300) # Set map canvas = self.iface.mapCanvas() cm = c.getComposerMapById(0) extent = canvas.extent() scale = canvas.scale() cm.setMapCanvas(canvas) if extent: cm.zoomToExtent(extent) if scale: cm.setNewScale(scale) # Export tempDir = s.value("cadastre/tempDir", '%s' % tempfile.gettempdir(), type=str) self.targetDir = tempfile.mkdtemp('', 'cad_export_', tempDir) temp = int(time()*100) temppath = os.path.join(tempDir, 'export_cadastre_%s.pdf' % temp) c.exportAsPDF(temppath) QApplication.restoreOverrideCursor() if os.path.exists(temppath): cadastre_common.openFile(temppath)
def printToPdf(self, params): self.check_required_params(params) with change_directory(self.project_root): crs = QgsCoordinateReferenceSystem() crs.createFromSrid(params.get('srs')) mapRenderer = QgsMapRenderer() mapUnits = crs.mapUnits() mapRenderer.setMapUnits(mapUnits) mapExtent = QgsRectangle(*params.get('bbox')) mapRenderer.setExtent(mapExtent) le = QgsPalLabeling() mapRenderer.setLabelingEngine(le) layers = params.get('layers') self.setTransparencies(layers, params.get('transparencies')) mapRenderer.setLayerSet(layers) composer = ( self.getLayoutbyName(params['layout']) .firstChildElement('Composition') ) comp = QgsComposition(mapRenderer) comp.setPlotStyle(QgsComposition.Print) comp.readXML(composer, self.doc) # read composition elements comp.addItemsFromXML(composer, self.doc) comp.setPrintResolution(90) # set bbox for the first Map in the layout comp_map = comp.getComposerMapById(0) comp_map.setNewExtent(QgsRectangle(*params.get('bbox'))) # comp_map.setNewScale(10000) # comp_map.setLayerSet(layers) # comp_map.setKeepLayerSet(True) # save the file comp.exportAsPDF(params['tmpFile'] + '.pdf')
def createComposition(self): ''' Create a composition ''' # Create composition c = QgsComposition(self.mInstance) # Set main properties c.setPaperSize(self.pageWidth, self.pageHeight) c.setPrintResolution(self.printResolution) c.setSnapGridOffsetX(3.5) c.setSnapGridOffsetY(0) c.setSnapGridResolution(2.5) # Set page number self.getPageNumberNeeded() c.setNumPages(self.numPages) # Set plot style if map added if self.etype == 'parcelle': c.setPlotStyle(QgsComposition.Print) self.currentComposition = c
class TestComposerBase(TestQgsPalLabeling): layer = None """:type: QgsVectorLayer""" @classmethod def setUpClass(cls): if not cls._BaseSetup: TestQgsPalLabeling.setUpClass() # the blue background (set via layer style) to match renderchecker's TestQgsPalLabeling.loadFeatureLayer('background', True) cls._TestKind = 0 # OutputKind.(Img|Svg|Pdf) @classmethod def tearDownClass(cls): """Run after all tests""" TestQgsPalLabeling.tearDownClass() cls.removeMapLayer(cls.layer) cls.layer = None def setUp(self): """Run before each test.""" super(TestComposerBase, self).setUp() self._TestImage = '' # ensure per test map settings stay encapsulated self._TestMapSettings = self.cloneMapSettings(self._MapSettings) self._Mismatch = 0 self._ColorTol = 0 self._Mismatches.clear() self._ColorTols.clear() def _set_up_composition(self, width, height, dpi): # set up composition and add map self._c = QgsComposition(self._TestMapSettings, QgsProject.instance()) """:type: QgsComposition""" # self._c.setUseAdvancedEffects(False) self._c.setPrintResolution(dpi) # 600 x 400 px = 211.67 x 141.11 mm @ 72 dpi paperw = width * 25.4 / dpi paperh = height * 25.4 / dpi self._c.setPaperSize(paperw, paperh) # NOTE: do not use QgsComposerMap(self._c, 0, 0, paperw, paperh) since # it only takes integers as parameters and the composition will grow # larger based upon union of item scene rectangles and a slight buffer # see end of QgsComposition::compositionBounds() # add map as small graphics item first, then set its scene QRectF later self._cmap = QgsComposerMap(self._c, 10, 10, 10, 10) """:type: QgsComposerMap""" self._cmap.setPreviewMode(QgsComposerMap.Render) self._cmap.setFrameEnabled(False) self._c.addComposerMap(self._cmap) # now expand map to fill page and set its extent self._cmap.setSceneRect(QRectF(0, 0, paperw, paperw)) self._cmap.setNewExtent(self.aoiExtent()) # self._cmap.updateCachedImage() self._c.setPlotStyle(QgsComposition.Print) # noinspection PyUnusedLocal def _get_composer_image(self, width, height, dpi): image = QImage(QSize(width, height), self._TestMapSettings.outputImageFormat()) image.fill(QColor(152, 219, 249).rgb()) image.setDotsPerMeterX(dpi / 25.4 * 1000) image.setDotsPerMeterY(dpi / 25.4 * 1000) p = QPainter(image) p.setRenderHint( QPainter.Antialiasing, self._TestMapSettings.testFlag(QgsMapSettings.Antialiasing) ) self._c.renderPage(p, 0) p.end() # image = self._c.printPageAsRaster(0) # """:type: QImage""" if image.isNull(): return False, '' filepath = getTempfilePath('png') res = image.save(filepath, 'png') if not res: os.unlink(filepath) filepath = '' return res, filepath def _get_composer_svg_image(self, width, height, dpi): # from qgscomposer.cpp, QgsComposer::on_mActionExportAsSVG_triggered, # near end of function svgpath = getTempfilePath('svg') temp_size = os.path.getsize(svgpath) svg_g = QSvgGenerator() # noinspection PyArgumentList svg_g.setTitle(QgsProject.instance().title()) svg_g.setFileName(svgpath) svg_g.setSize(QSize(width, height)) svg_g.setViewBox(QRect(0, 0, width, height)) svg_g.setResolution(dpi) sp = QPainter(svg_g) self._c.renderPage(sp, 0) sp.end() if temp_size == os.path.getsize(svgpath): return False, '' image = QImage(width, height, self._TestMapSettings.outputImageFormat()) image.fill(QColor(152, 219, 249).rgb()) image.setDotsPerMeterX(dpi / 25.4 * 1000) image.setDotsPerMeterY(dpi / 25.4 * 1000) svgr = QSvgRenderer(svgpath) p = QPainter(image) p.setRenderHint( QPainter.Antialiasing, self._TestMapSettings.testFlag(QgsMapSettings.Antialiasing) ) p.setRenderHint(QPainter.TextAntialiasing) svgr.render(p) p.end() filepath = getTempfilePath('png') res = image.save(filepath, 'png') if not res: os.unlink(filepath) filepath = '' # TODO: remove .svg file as well? return res, filepath def _get_composer_pdf_image(self, width, height, dpi): pdfpath = getTempfilePath('pdf') temp_size = os.path.getsize(pdfpath) p = QPrinter() p.setOutputFormat(QPrinter.PdfFormat) p.setOutputFileName(pdfpath) p.setPaperSize(QSizeF(self._c.paperWidth(), self._c.paperHeight()), QPrinter.Millimeter) p.setFullPage(True) p.setColorMode(QPrinter.Color) p.setResolution(self._c.printResolution()) pdf_p = QPainter(p) # page_mm = p.pageRect(QPrinter.Millimeter) # page_px = p.pageRect(QPrinter.DevicePixel) # self._c.render(pdf_p, page_px, page_mm) self._c.renderPage(pdf_p, 0) pdf_p.end() if temp_size == os.path.getsize(pdfpath): return False, '' filepath = getTempfilePath('png') # Poppler (pdftocairo or pdftoppm): # PDFUTIL -png -singlefile -r 72 -x 0 -y 0 -W 420 -H 280 in.pdf pngbase # muPDF (mudraw): # PDFUTIL -c rgb[a] -r 72 -w 420 -h 280 -o out.png in.pdf if PDFUTIL.strip().endswith('pdftocairo'): filebase = os.path.join( os.path.dirname(filepath), os.path.splitext(os.path.basename(filepath))[0] ) call = [ PDFUTIL, '-png', '-singlefile', '-r', str(dpi), '-x', '0', '-y', '0', '-W', str(width), '-H', str(height), pdfpath, filebase ] elif PDFUTIL.strip().endswith('mudraw'): call = [ PDFUTIL, '-c', 'rgba', '-r', str(dpi), '-w', str(width), '-h', str(height), # '-b', '8', '-o', filepath, pdfpath ] else: return False, '' qDebug("_get_composer_pdf_image call: {0}".format(' '.join(call))) res = False try: subprocess.check_call(call) res = True except subprocess.CalledProcessError as e: qDebug("_get_composer_pdf_image failed!\n" "cmd: {0}\n" "returncode: {1}\n" "message: {2}".format(e.cmd, e.returncode, e.message)) if not res: os.unlink(filepath) filepath = '' return res, filepath def get_composer_output(self, kind): ms = self._TestMapSettings osize = ms.outputSize() width, height, dpi = osize.width(), osize.height(), ms.outputDpi() self._set_up_composition(width, height, dpi) if kind == OutputKind.Svg: return self._get_composer_svg_image(width, height, dpi) elif kind == OutputKind.Pdf: return self._get_composer_pdf_image(width, height, dpi) else: # OutputKind.Img return self._get_composer_image(width, height, dpi) # noinspection PyUnusedLocal def checkTest(self, **kwargs): self.lyr.writeToLayer(self.layer) ms = self._MapSettings # class settings settings_type = 'Class' if self._TestMapSettings is not None: ms = self._TestMapSettings # per test settings settings_type = 'Test' if 'PAL_VERBOSE' in os.environ: qDebug('MapSettings type: {0}'.format(settings_type)) qDebug(mapSettingsString(ms)) res_m, self._TestImage = self.get_composer_output(self._TestKind) self.assertTrue(res_m, 'Failed to retrieve/save output from composer') self.saveControlImage(self._TestImage) mismatch = 0 if 'PAL_NO_MISMATCH' not in os.environ: # some mismatch expected mismatch = self._Mismatch if self._Mismatch else 20 if self._TestGroup in self._Mismatches: mismatch = self._Mismatches[self._TestGroup] colortol = 0 if 'PAL_NO_COLORTOL' not in os.environ: colortol = self._ColorTol if self._ColorTol else 0 if self._TestGroup in self._ColorTols: colortol = self._ColorTols[self._TestGroup] self.assertTrue(*self.renderCheck(mismatch=mismatch, colortol=colortol, imgpath=self._TestImage))
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 print_impact_table(self, output_path): """Pint summary from impact layer to PDF. ..note:: The order of the report: 1. Summary table 2. Aggregation table 3. Attribution table :param output_path: Output path. :type output_path: str :return: Path to generated pdf file. :rtype: str :raises: None """ keywords = self._keyword_io.read_keywords(self.layer) if output_path is None: output_path = unique_filename(suffix='.pdf', dir=temp_dir()) try: impact_template = get_report_template(self.layer.source()) summary_table = impact_template.generate_html_report() except: summary_table = keywords.get('impact_summary', None) full_table = keywords.get('impact_table', None) aggregation_table = keywords.get('postprocessing_report', None) attribution_table = impact_attribution(keywords) # (AG) We will not use impact_table as most of the IF use that as: # impact_table = impact_summary + some information intended to be # shown on screen (see FloodOsmBuilding) # Unless the impact_summary is None, we will use impact_table as the # alternative html = m.Brand().to_html() html += m.Heading(tr('Analysis Results'), **INFO_STYLE).to_html() if summary_table is None: html += full_table else: html += summary_table if aggregation_table is not None: html += aggregation_table if attribution_table is not None: html += attribution_table.to_html() html = html_header() + html + html_footer() # Print HTML using composition # For QGIS < 2.4 compatibility # QgsMapSettings is added in 2.4 if qgis_version() < 20400: map_settings = QgsMapRenderer() else: map_settings = QgsMapSettings() # A4 Portrait # TODO: Will break when we try to use larger print layouts TS paper_width = 210 paper_height = 297 # noinspection PyCallingNonCallable composition = QgsComposition(map_settings) # noinspection PyUnresolvedReferences composition.setPlotStyle(QgsComposition.Print) composition.setPaperSize(paper_width, paper_height) composition.setPrintResolution(300) # Add HTML Frame # noinspection PyCallingNonCallable html_item = QgsComposerHtml(composition, False) margin_left = 10 margin_top = 10 # noinspection PyCallingNonCallable html_frame = QgsComposerFrame( composition, html_item, margin_left, margin_top, paper_width - 2 * margin_left, paper_height - 2 * margin_top) html_item.addFrame(html_frame) # Set HTML # From QGIS 2.6, we can set composer HTML with manual HTML if qgis_version() < 20600: html_path = unique_filename( prefix='report', suffix='.html', dir=temp_dir()) html_to_file(html, file_path=html_path) html_url = QUrl.fromLocalFile(html_path) html_item.setUrl(html_url) else: # noinspection PyUnresolvedReferences html_item.setContentMode(QgsComposerHtml.ManualHtml) # noinspection PyUnresolvedReferences html_item.setResizeMode(QgsComposerHtml.RepeatUntilFinished) html_item.setHtml(html) # RMN: This line below breaks in InaSAFE Headless after one # successful call. This is because the function is not # thread safe. Can't do anything about this, so avoid calling this # function in multithreaded way. html_item.loadHtml() composition.exportAsPDF(output_path) return output_path
def export_all_features(self): pdf_painter = None """Export map to pdf atlas style (one page per feature)""" if VRP_DEBUG is True: QgsMessageLog.logMessage(u'exporting map', DLG_CAPTION) try: result = self.__delete_pdf() if not result is None: return result ids = [] exp = QgsExpression(self.feature_filter) if exp.hasParserError(): raise Exception(exp.parserErrorString()) exp.prepare(self.coverage_layer.pendingFields()) for feature in self.coverage_layer.getFeatures(): value = exp.evaluate(feature) if exp.hasEvalError(): raise ValueError(exp.evalErrorString()) if bool(value): if VRP_DEBUG is True: QgsMessageLog.logMessage(u'export map, feature id:{0}'.format(feature.id()), DLG_CAPTION) ids.append(feature.id()) self.coverage_layer.select(ids) bbox = self.coverage_layer.boundingBoxOfSelected() self.canvas.zoomToSelected(self.coverage_layer) if VRP_DEBUG is True: QgsMessageLog.logMessage(u'bbox:{0}'.format(bbox.toString()), DLG_CAPTION) #self.map_renderer.setExtent(bbox) #self.map_renderer.updateScale() #read plotlayout composition = QgsComposition(self.map_renderer) self.composition = composition composition.setPlotStyle(QgsComposition.Print) error, xml_doc = self.__read_template() if not error is None: return error if composition.loadFromTemplate(xml_doc) is False: return u'Konnte Template nicht laden!\n{0}'.format(self.template_qpt) #read textinfo layout self.comp_textinfo = QgsComposition(self.map_renderer) self.comp_textinfo.setPlotStyle(QgsComposition.Print) error, xml_doc = self.__read_template(True) if not error is None: return error if self.comp_textinfo.loadFromTemplate(xml_doc) is False: return u'Konnte Template nicht laden!\n{0}'.format(self.settings.textinfo_layout()) new_ext = bbox if QGis.QGIS_VERSION_INT > 20200: compmaps = self.__get_items(QgsComposerMap) if len(compmaps) < 1: return u'Kein Kartenfenster im Layout vorhanden!' compmap = compmaps[0] else: if len(composition.composerMapItems()) < 1: return u'Kein Kartenfenster im Layout vorhanden!' compmap = composition.composerMapItems()[0] self.composermap = compmap #self.composermap.setPreviewMode(QgsComposerMap.Render) #self.composermap.setPreviewMode(QgsComposerMap.Rectangle) #taken from QgsComposerMap::setNewAtlasFeatureExtent (not yet available in QGIS 2.0) #http://www.qgis.org/api/qgscomposermap_8cpp_source.html#l00610 old_ratio = compmap.rect().width() / compmap.rect().height() new_ratio = new_ext.width() / new_ext.height() if old_ratio < new_ratio: new_height = new_ext.width() / old_ratio delta_height = new_height - new_ext.height() new_ext.setYMinimum( bbox.yMinimum() - delta_height / 2) new_ext.setYMaximum(bbox.yMaximum() + delta_height / 2) else: new_width = old_ratio * new_ext.height() delta_width = new_width - new_ext.width() new_ext.setXMinimum(bbox.xMinimum() - delta_width / 2) new_ext.setXMaximum(bbox.xMaximum() + delta_width / 2) if VRP_DEBUG is True: QgsMessageLog.logMessage(u'bbox old:{0}'.format(compmap.extent().toString()), DLG_CAPTION) compmap.setNewExtent(new_ext) if VRP_DEBUG is True: QgsMessageLog.logMessage(u'bbox new:{0}'.format(compmap.extent().toString()), DLG_CAPTION) #round up to next 1000 compmap.setNewScale(math.ceil((compmap.scale()/1000.0)) * 1000.0) if VRP_DEBUG is True: QgsMessageLog.logMessage(u'bbox new (after scale):{0}'.format(compmap.extent().toString()), DLG_CAPTION) #add ORTHO after new extent -> performance if not self.ortho is None: self.ortho_lyr = self.__add_raster_layer(self.ortho, self.lyrname_ortho) self.__reorder_layers() self.comp_leg = self.__get_items(QgsComposerLegend) self.comp_lbl = self.__get_items(QgsComposerLabel) self.__update_composer_items(self.settings.dkm_gemeinde(self.gem_name)['lyrnamegstk']) if VRP_DEBUG is True: QgsMessageLog.logMessage(u'paperWidth:{0} paperHeight:{1}'.format(composition.paperWidth(), composition.paperHeight()), DLG_CAPTION) printer = QPrinter() printer.setOutputFormat(QPrinter.PdfFormat) printer.setOutputFileName(self.pdf_map) printer.setPaperSize(QSizeF(composition.paperWidth(), composition.paperHeight()), QPrinter.Millimeter) printer.setFullPage(True) printer.setColorMode(QPrinter.Color) printer.setResolution(composition.printResolution()) pdf_painter = QPainter(printer) paper_rect_pixel = printer.pageRect(QPrinter.DevicePixel) paper_rect_mm = printer.pageRect(QPrinter.Millimeter) QgsPaintEngineHack.fixEngineFlags(printer.paintEngine()) #DKM only if len(self.themen) < 1: composition.render(pdf_painter, paper_rect_pixel, paper_rect_mm) else: self.statistics = OrderedDict() try: pass #lyr = QgsVectorLayer('/home/bergw/VoGIS-Raumplanung-Daten/Geodaten/Raumplanung/Flaechenwidmung/Dornbirn/Flaechenwidmungsplan/fwp_flaeche.shp', 'flaeiw', 'ogr') #lyr.loadNamedStyle('/home/bergw/VoGIS-Raumplanung-Daten/Geodaten/Raumplanung/Flaechenwidmung/Vorarlberg/Flaechenwidmungsplan/fwp_flaeche.qml') #QgsMapLayerRegistry.instance().addMapLayer(lyr) except: QgsMessageLog.logMessage('new lyr:{0}'.format(sys.exc_info()[0]), DLG_CAPTION) #QgsMapLayerRegistry.instance().addMapLayer(lyr) cntr = 0 for thema, sub_themen in self.themen.iteritems(): if VRP_DEBUG is True: QgsMessageLog.logMessage('drucke Thema:{0}'.format(thema.name), DLG_CAPTION) if sub_themen is None: layers = self.__add_layers(thema) self.__calculate_statistics(thema, thema, layers) #no qml -> not visible -> means no map if self.__at_least_one_visible(layers) is True: if cntr > 0: printer.newPage() self.__reorder_layers() self.__update_composer_items(thema.name, layers=layers) composition.renderPage(pdf_painter, 0) QgsMapLayerRegistry.instance().removeMapLayers([lyr.id() for lyr in layers]) cntr += 1 else: QgsMapLayerRegistry.instance().removeMapLayers([lyr.id() for lyr in layers]) if not sub_themen is None: for sub_thema in sub_themen: if VRP_DEBUG is True: QgsMessageLog.logMessage(u'drucke SubThema:{0}'.format(sub_thema.name), DLG_CAPTION) layers = self.__add_layers(sub_thema) self.__calculate_statistics(thema, sub_thema, layers) #no qml -> not visible -> means no map if self.__at_least_one_visible(layers) is True: if cntr > 0: printer.newPage() self.__reorder_layers() self.__update_composer_items(thema.name, subthema=sub_thema.name, layers=layers) composition.renderPage(pdf_painter, 0) QgsMapLayerRegistry.instance().removeMapLayers([lyr.id() for lyr in layers]) cntr += 1 else: QgsMapLayerRegistry.instance().removeMapLayers([lyr.id() for lyr in layers]) #output statistics if len(self.statistics) > 0: printer.setPaperSize(QSizeF(210, 297), QPrinter.Millimeter) tabelle = self.__get_item_byid(self.comp_textinfo, 'TABELLE') if tabelle is None: self.iface.messageBar().pushMessage(u'Layout (Textinfo): Kein Textelement mit ID "TABELLE" vorhanden.', QgsMessageBar.CRITICAL) else: try: str_flaechen = '' idx = 0 for gnr, stats in self.statistics.iteritems(): comma = ', ' if idx > 0 else '' str_flaechen += u'{0}{1} ({2:.2f}m²)'.format(comma, gnr, stats[0].flaeche) idx += 1 lbls = self.__get_items(QgsComposerLabel, self.comp_textinfo) self.__update_composer_items('', labels=lbls, gnrflaeche=str_flaechen) html = tabelle.text() html += u'<table>' #gnrcnt = 0 for gnr, stats in self.statistics.iteritems(): #if gnrcnt > 0: # html += u'<tr class="abstand"><td> </td><td> </td><td> </td></tr>' html += u'<tr><th class="gnr"></th><th class="gnr">{0}</th><th class="gnr"></th></tr>'.format(gnr) #html += u'<tr class="abstand"><td> </td><td> </td><td> </td></tr>' curr_thema = '' for stat in stats: if stat.thema != curr_thema: html += u'<tr><th class="thema"></th><th class="thema">{0}</th><th class="thema"></th></tr>'.format(stat.thema) curr_thema = stat.thema for thema, subthema in stat.subthemen.iteritems(): for quelle in subthema: html += u'<tr><td class="col1">{0}</td>'.format(quelle.name) attr_val = '' attr_area = '' for text, area in quelle.txt_area.iteritems(): attr_val += u'{0}<br />'.format(text) attr_area += u'{0:.2f}m² <br />'.format(area) html += u'<td class="col2">{0}</td><td class="col3">{1}</td></tr>'.format(attr_val, attr_area) #gnrcnt += 1 html += u'</table>' tabelle.setText(html) printer.newPage() self.comp_textinfo.renderPage(pdf_painter, 0) except: msg = 'Statistikausgabe:\n\n{0}'.format(traceback.format_exc()) QgsMessageLog.logMessage(msg, DLG_CAPTION) self.iface.messageBar().pushMessage(msg, QgsMessageBar.CRITICAL) except: msg = 'export pdf (catch all):\n\n{0}'.format(traceback.format_exc()) QgsMessageLog.logMessage(msg, DLG_CAPTION) self.iface.messageBar().pushMessage(msg.replace(u'\n', u''), QgsMessageBar.CRITICAL) return msg finally: #end pdf if not pdf_painter is None: pdf_painter.end() return None
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 = default_north_arrow_path() self.org_logo = default_organisation_logo_path() 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 # List of all component id that should be exist on the template self.component_ids = [] # Prompt user if template does not contain some elements self.template_warning_verbose = False @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, north_arrow_path): """Set image that will be used as organisation logo in reports. :param north_arrow_path: Path to image file :type north_arrow_path: str """ self.north_arrow = north_arrow_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_component_ids(self, component_ids): """Set component_ids that should be present on the standard template. :param component_ids: List of component id on the standard template :type component_ids: list """ self.component_ids = component_ids def set_template_warning_verbose(self, verbose): """Set the choice to prompt user the error or not if template does not have some elements. :param verbose: the choice to keep it verbose or not :type verbose: bool """ self.template_warning_verbose = verbose 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 :raises: TemplateElementMissingError - when template elements are missing :returns: File name of the output file (equivalent to filename if provided). :rtype: str """ LOGGER.debug('InaSAFE Map printToPdf called') try: self.load_template() except TemplateElementMissingError, msg: raise TemplateElementMissingError(msg) 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.composition.exportAsPDF(map_pdf_path) return map_pdf_path
class TestComposerBase(TestQgsPalLabeling): layer = None """:type: QgsVectorLayer""" @classmethod def setUpClass(cls): if not cls._BaseSetup: TestQgsPalLabeling.setUpClass() # the blue background (set via layer style) to match renderchecker's TestQgsPalLabeling.loadFeatureLayer('background', True) cls._TestKind = 0 # OutputKind.(Img|Svg|Pdf) @classmethod def tearDownClass(cls): """Run after all tests""" TestQgsPalLabeling.tearDownClass() cls.removeMapLayer(cls.layer) cls.layer = None def setUp(self): """Run before each test.""" super(TestComposerBase, self).setUp() self._TestImage = '' # ensure per test map settings stay encapsulated self._TestMapSettings = self.cloneMapSettings(self._MapSettings) self._Mismatch = 0 self._ColorTol = 0 self._Mismatches.clear() self._ColorTols.clear() def _set_up_composition(self, width, height, dpi): # set up composition and add map self._c = QgsComposition(QgsProject.instance()) """:type: QgsComposition""" # self._c.setUseAdvancedEffects(False) self._c.setPrintResolution(dpi) # 600 x 400 px = 211.67 x 141.11 mm @ 72 dpi paperw = width * 25.4 / dpi paperh = height * 25.4 / dpi self._c.setPaperSize(paperw, paperh) # NOTE: do not use QgsComposerMap(self._c, 0, 0, paperw, paperh) since # it only takes integers as parameters and the composition will grow # larger based upon union of item scene rectangles and a slight buffer # see end of QgsComposition::compositionBounds() # add map as small graphics item first, then set its scene QRectF later self._cmap = QgsComposerMap(self._c, 10, 10, 10, 10) """:type: QgsComposerMap""" self._cmap.setPreviewMode(QgsComposerMap.Render) self._cmap.setFrameEnabled(False) self._cmap.setLayers(self._TestMapSettings.layers()) self._c.addComposerMap(self._cmap) # now expand map to fill page and set its extent self._cmap.setSceneRect(QRectF(0, 0, paperw, paperw)) self._cmap.setNewExtent(self.aoiExtent()) # self._cmap.updateCachedImage() self._c.setPlotStyle(QgsComposition.Print) # noinspection PyUnusedLocal def _get_composer_image(self, width, height, dpi): image = QImage(QSize(width, height), self._TestMapSettings.outputImageFormat()) image.fill(QColor(152, 219, 249).rgb()) image.setDotsPerMeterX(dpi / 25.4 * 1000) image.setDotsPerMeterY(dpi / 25.4 * 1000) p = QPainter(image) p.setRenderHint( QPainter.Antialiasing, self._TestMapSettings.testFlag(QgsMapSettings.Antialiasing)) self._c.renderPage(p, 0) p.end() # image = self._c.printPageAsRaster(0) # """:type: QImage""" if image.isNull(): return False, '' filepath = getTempfilePath('png') res = image.save(filepath, 'png') if not res: os.unlink(filepath) filepath = '' return res, filepath def _get_composer_svg_image(self, width, height, dpi): # from qgscomposer.cpp, QgsComposer::on_mActionExportAsSVG_triggered, # near end of function svgpath = getTempfilePath('svg') temp_size = os.path.getsize(svgpath) svg_g = QSvgGenerator() # noinspection PyArgumentList svg_g.setTitle(QgsProject.instance().title()) svg_g.setFileName(svgpath) svg_g.setSize(QSize(width, height)) svg_g.setViewBox(QRect(0, 0, width, height)) svg_g.setResolution(dpi) sp = QPainter(svg_g) self._c.renderPage(sp, 0) sp.end() if temp_size == os.path.getsize(svgpath): return False, '' image = QImage(width, height, self._TestMapSettings.outputImageFormat()) image.fill(QColor(152, 219, 249).rgb()) image.setDotsPerMeterX(dpi / 25.4 * 1000) image.setDotsPerMeterY(dpi / 25.4 * 1000) svgr = QSvgRenderer(svgpath) p = QPainter(image) p.setRenderHint( QPainter.Antialiasing, self._TestMapSettings.testFlag(QgsMapSettings.Antialiasing)) p.setRenderHint(QPainter.TextAntialiasing) svgr.render(p) p.end() filepath = getTempfilePath('png') res = image.save(filepath, 'png') if not res: os.unlink(filepath) filepath = '' # TODO: remove .svg file as well? return res, filepath def _get_composer_pdf_image(self, width, height, dpi): pdfpath = getTempfilePath('pdf') temp_size = os.path.getsize(pdfpath) p = QPrinter() p.setOutputFormat(QPrinter.PdfFormat) p.setOutputFileName(pdfpath) p.setPaperSize(QSizeF(self._c.paperWidth(), self._c.paperHeight()), QPrinter.Millimeter) p.setFullPage(True) p.setColorMode(QPrinter.Color) p.setResolution(self._c.printResolution()) pdf_p = QPainter(p) # page_mm = p.pageRect(QPrinter.Millimeter) # page_px = p.pageRect(QPrinter.DevicePixel) # self._c.render(pdf_p, page_px, page_mm) self._c.renderPage(pdf_p, 0) pdf_p.end() if temp_size == os.path.getsize(pdfpath): return False, '' filepath = getTempfilePath('png') # Poppler (pdftocairo or pdftoppm): # PDFUTIL -png -singlefile -r 72 -x 0 -y 0 -W 420 -H 280 in.pdf pngbase # muPDF (mudraw): # PDFUTIL -c rgb[a] -r 72 -w 420 -h 280 -o out.png in.pdf if PDFUTIL.strip().endswith('pdftocairo'): filebase = os.path.join( os.path.dirname(filepath), os.path.splitext(os.path.basename(filepath))[0]) call = [ PDFUTIL, '-png', '-singlefile', '-r', str(dpi), '-x', '0', '-y', '0', '-W', str(width), '-H', str(height), pdfpath, filebase ] elif PDFUTIL.strip().endswith('mudraw'): call = [ PDFUTIL, '-c', 'rgba', '-r', str(dpi), '-w', str(width), '-h', str(height), # '-b', '8', '-o', filepath, pdfpath ] else: return False, '' qDebug("_get_composer_pdf_image call: {0}".format(' '.join(call))) res = False try: subprocess.check_call(call) res = True except subprocess.CalledProcessError as e: qDebug("_get_composer_pdf_image failed!\n" "cmd: {0}\n" "returncode: {1}\n" "message: {2}".format(e.cmd, e.returncode, e.message)) if not res: os.unlink(filepath) filepath = '' return res, filepath def get_composer_output(self, kind): ms = self._TestMapSettings osize = ms.outputSize() width, height, dpi = osize.width(), osize.height(), ms.outputDpi() self._set_up_composition(width, height, dpi) if kind == OutputKind.Svg: return self._get_composer_svg_image(width, height, dpi) elif kind == OutputKind.Pdf: return self._get_composer_pdf_image(width, height, dpi) else: # OutputKind.Img return self._get_composer_image(width, height, dpi) # noinspection PyUnusedLocal def checkTest(self, **kwargs): self.lyr.writeToLayer(self.layer) ms = self._MapSettings # class settings settings_type = 'Class' if self._TestMapSettings is not None: ms = self._TestMapSettings # per test settings settings_type = 'Test' if 'PAL_VERBOSE' in os.environ: qDebug('MapSettings type: {0}'.format(settings_type)) qDebug(mapSettingsString(ms)) res_m, self._TestImage = self.get_composer_output(self._TestKind) self.assertTrue(res_m, 'Failed to retrieve/save output from composer') self.saveControlImage(self._TestImage) mismatch = 0 if 'PAL_NO_MISMATCH' not in os.environ: # some mismatch expected mismatch = self._Mismatch if self._Mismatch else 20 if self._TestGroup in self._Mismatches: mismatch = self._Mismatches[self._TestGroup] colortol = 0 if 'PAL_NO_COLORTOL' not in os.environ: colortol = self._ColorTol if self._ColorTol else 0 if self._TestGroup in self._ColorTols: colortol = self._ColorTols[self._TestGroup] self.assertTrue(*self.renderCheck( mismatch=mismatch, colortol=colortol, imgpath=self._TestImage))
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 composer(self, setData, flag=True): paperWidth = setData["Width"] paperHeight = setData["Height"] mapRenderer = define._canvas.mapRenderer() # comp = None comp = QgsComposition(mapRenderer) comp.setPlotStyle(QgsComposition.Print) comp.setPaperSize(paperWidth, paperHeight) # rectangleAll = QgsComposerShape(comp) # rectangleAll.setItemPosition(3, 3, paperWidth - 3, paperHeight - 3) # rectangleAll.setShapeType(1) # comp.addItem(rectangleAll) self.txt1 = QgsComposerLabel(comp) self.txt1.setItemPosition(20, 20) self.txt1.setFrameEnabled(False) font = QFont("Arial", 10) self.txt1.setFont(font) self.txt1.setText(setData["TextData"][0]) self.txt1.adjustSizeToText() comp.addItem(self.txt1) self.txt2 = QgsComposerLabel(comp) self.txt2.setItemPosition(int(paperWidth / 2) + 10, 20) self.txt2.setFrameEnabled(False) font = QFont("Arial", 10) self.txt2.setFont(font) self.txt2.setText(setData["TextData"][1]) self.txt2.adjustSizeToText() comp.addItem(self.txt2) self.txt3 = QgsComposerLabel(comp) self.txt3.setItemPosition(paperWidth - 40, 20) self.txt3.setFrameEnabled(False) font = QFont("Arial", 10) self.txt3.setFont(font) self.txt3.setText(setData["TextData"][2]) self.txt3.adjustSizeToText() comp.addItem(self.txt3) w = int((paperWidth - 40) / 4) self.txt4 = QgsComposerLabel(comp) self.txt4.setItemPosition(20, 35) self.txt4.setFrameEnabled(False) font = QFont("Arial", 10) self.txt4.setFont(font) self.txt4.setText(setData["TextData"][3]) self.txt4.adjustSizeToText() comp.addItem(self.txt4) self.txt5 = QgsComposerLabel(comp) self.txt5.setItemPosition(20 + w, 35) self.txt5.setFrameEnabled(False) font = QFont("Arial", 10) self.txt5.setFont(font) self.txt5.setText(setData["TextData"][4]) self.txt5.adjustSizeToText() comp.addItem(self.txt5) self.txt6 = QgsComposerLabel(comp) self.txt6.setItemPosition(20 + w * 2, 35) self.txt6.setFrameEnabled(False) font = QFont("Arial", 10) self.txt6.setFont(font) self.txt6.setText(setData["TextData"][5]) self.txt6.adjustSizeToText() comp.addItem(self.txt6) self.txt7 = QgsComposerLabel(comp) self.txt7.setItemPosition(20 + w * 3, 35) self.txt7.setFrameEnabled(False) font = QFont("Arial", 10) self.txt7.setFont(font) self.txt7.setText(setData["TextData"][6]) self.txt7.adjustSizeToText() comp.addItem(self.txt7) # if flag: x, y = 20, 45 composerMap = QgsComposerMap(comp, x, y, paperWidth - 40, 150) # composerMap.setPreviewMode(QgsComposerMap.Render) # composerMap.setGridEnabled(False) #rect = composerMap.currentMapExtent () renderer = composerMap.mapRenderer() newExtent = renderer.extent() #Make sure the width/height ratio is the same as in current composer map extent. #This is to keep the map item frame and the page layout fixed currentMapExtent = composerMap.currentMapExtent() currentWidthHeightRatio = currentMapExtent.width( ) / currentMapExtent.height() newWidthHeightRatio = newExtent.width() / newExtent.height() if currentWidthHeightRatio < newWidthHeightRatio: #enlarge height of new extent, ensuring the map center stays the same newHeight = newExtent.width() / currentWidthHeightRatio deltaHeight = newHeight - newExtent.height() newExtent.setYMinimum(newExtent.yMinimum() - deltaHeight / 2) newExtent.setYMaximum(newExtent.yMaximum() + deltaHeight / 2) else: #enlarge width of new extent, ensuring the map center stays the same newWidth = currentWidthHeightRatio * newExtent.height() deltaWidth = newWidth - newExtent.width() newExtent.setXMinimum(newExtent.xMinimum() - deltaWidth / 2) newExtent.setXMaximum(newExtent.xMaximum() + deltaWidth / 2) composerMap.beginCommand("Map extent changed") composerMap.setNewExtent(newExtent) composerMap.endCommand() # composerMap.setNewScale(3500) composerMap.setFrameEnabled(True) composerMap.setFrameOutlineWidth(1.0) composerMap.setPreviewMode(QgsComposerMap.Render) composerMap.updateCachedImage() composerMap.setGridEnabled(True) composerMap.setGridPenColor(QColor(255, 255, 255, 0)) composerMap.setGridPenWidth(0.0) #composerMap.setGridStyle(QgsComposerMap.FrameAnnotationsOnly) composerMap.setGridIntervalX(1.0) composerMap.setGridIntervalY(1.0) # mySymbol1 = composerMap.gridLineSymbol () # mySymbol1.setAlpha(0) #composerMap.setGridLineSymbol(mySymbol1) composerMap.setShowGridAnnotation(True) composerMap.setGridAnnotationFormat(1) composerMap.setGridAnnotationPrecision(0) composerMap.setGridAnnotationDirection(1, 0) composerMap.setGridAnnotationDirection(1, 1) comp.addItem(composerMap) w = int((paperWidth - 40) / 3) self.txt8 = QgsComposerLabel(comp) self.txt8.setItemPosition(20, 225) self.txt8.setFrameEnabled(False) font = QFont("Arial", 10) self.txt8.setFont(font) self.txt8.setText(setData["TextData"][7]) self.txt8.adjustSizeToText() comp.addItem(self.txt8) self.txt9 = QgsComposerLabel(comp) self.txt9.setItemPosition(20 + w, 225) self.txt9.setFrameEnabled(False) font = QFont("Arial", 10) self.txt9.setFont(font) self.txt9.setText(setData["TextData"][8]) self.txt9.adjustSizeToText() comp.addItem(self.txt9) self.txt10 = QgsComposerLabel(comp) self.txt10.setItemPosition(20 + w * 2, 225) self.txt10.setFrameEnabled(False) font = QFont("Arial", 10) self.txt10.setFont(font) self.txt10.setText(setData["TextData"][9]) self.txt10.adjustSizeToText() comp.addItem(self.txt10) # profileRect = QgsComposerPicture(comp) # profileRect.setItemPosition(20, 200, paperWidth - 40, 50) # comp.addItem(profileRect) self.txt11 = QgsComposerLabel(comp) self.txt11.setItemPosition(20, paperHeight - 30) self.txt11.setFrameEnabled(False) font = QFont("Arial", 10) self.txt11.setFont(font) self.txt11.setText(setData["TextData"][10]) self.txt11.adjustSizeToText() comp.addItem(self.txt11) self.txt12 = QgsComposerLabel(comp) self.txt12.setItemPosition(20 + w, paperHeight - 30) self.txt12.setFrameEnabled(False) font = QFont("Arial", 10) self.txt12.setFont(font) self.txt12.setText(setData["TextData"][11]) self.txt12.adjustSizeToText() comp.addItem(self.txt12) self.txt13 = QgsComposerLabel(comp) self.txt13.setItemPosition(20 + w * 2, paperHeight - 30) self.txt13.setFrameEnabled(False) font = QFont("Arial", 10) self.txt13.setFont(font) self.txt13.setText(setData["TextData"][12]) self.txt13.adjustSizeToText() comp.addItem(self.txt13) # gpw = QGraphicsProxyWidget() self.tblView = QTableView() tableHeight = None tableWidth = None if self.tableChangedValue == None: self.tblView.setFixedWidth(paperWidth - 40) tableHeight = 50 tableWidth = paperWidth - 40 self.tblView.setFixedHeight(tableHeight) else: self.tblView.setFixedWidth(self.tableChangedValue["Width"]) self.tblView.setFixedHeight(self.tableChangedValue["Height"]) tableHeight = self.tableChangedValue["Height"] tableWidth = self.tableChangedValue["Width"] self.tblView.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.tblView.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) hHeder = self.tblView.horizontalHeader() hHeder.setVisible(False) vHeder = self.tblView.verticalHeader() vHeder.setVisible(False) # if flag: self.stdModel = QStandardItemModel() self.data.__setitem__("TableWidth", tableWidth) self.data.__setitem__("TableHeight", tableHeight) self.setTableView(self.tblView, self.stdModel, self.data) self.calcALT() self.calcTimeRate() self.gpw = QGraphicsProxyWidget() self.gpw.setWidget(self.tblView) # gpw.setWidget(tblView) if self.tableChangedValue == None: self.gpw.setPos(20, 210) font = QFont() font.setPixelSize(2) self.gpw.setFont(font) else: self.gpw.setPos(self.tableChangedValue["X"], self.tableChangedValue["Y"]) font = QFont() font.setPixelSize(self.tableChangedValue["FontSize"]) self.gpw.setFont(font) # tblView.setFrameRect(QRect(20, 210, paperWidth - 40, 50)) comp.addItem(self.gpw) self.connect(self.stdModel, SIGNAL("itemChanged(QStandardItem *)"), self.stdModel_itemChanged) return comp
class Map(): """A class for creating a map.""" def __init__(self, theIface): """Constructor for the Map class. Args: theIface - reference to the QGIS iface object Returns: None Raises: Any exceptions raised by the InaSAFE library will be propagated. """ LOGGER.debug('InaSAFE Map class initialised') self.iface = theIface self.layer = theIface.activeLayer() self.keywordIO = KeywordIO() self.printer = None self.composition = None self.legend = None self.pageWidth = 210 # width in mm self.pageHeight = 297 # height in mm self.pageDpi = 300.0 self.pageMargin = 10 # margin in mm self.verticalSpacing = 1 # vertical spacing between elements self.showFramesFlag = False # intended for debugging use only # make a square map where width = height = page width self.mapHeight = self.pageWidth - (self.pageMargin * 2) self.mapWidth = self.mapHeight self.disclaimer = self.tr('InaSAFE has been jointly developed by' ' BNPB, AusAid & the World Bank') def tr(self, theString): """We implement this since we do not inherit QObject. Args: theString - string for translation. Returns: Translated version of theString. Raises: no exceptions explicitly raised. """ return QtCore.QCoreApplication.translate('Map', theString) def setImpactLayer(self, theLayer): """Mutator for the impact layer that will be used for stats, legend and reporting. Args: theLayer - a valid QgsMapLayer Returns: None Raises: Any exceptions raised by the InaSAFE library will be propagated. """ self.layer = theLayer def setupComposition(self): """Set up the composition ready for drawing elements onto it. Args: None Returns: None Raises: None """ LOGGER.debug('InaSAFE Map setupComposition called') myCanvas = self.iface.mapCanvas() myRenderer = myCanvas.mapRenderer() self.composition = QgsComposition(myRenderer) self.composition.setPlotStyle(QgsComposition.Print) # or preview self.composition.setPaperSize(self.pageWidth, self.pageHeight) self.composition.setPrintResolution(self.pageDpi) self.composition.setPrintAsRaster(True) def composeMap(self): """Place all elements on the map ready for printing. Args: None Returns: None Raises: Any exceptions raised will be propagated. """ self.setupComposition() # Keep track of our vertical positioning as we work our way down # the page placing elements on it. myTopOffset = self.pageMargin self.drawLogo(myTopOffset) myLabelHeight = self.drawTitle(myTopOffset) # Update the map offset for the next row of content myTopOffset += myLabelHeight + self.verticalSpacing myComposerMap = self.drawMap(myTopOffset) self.drawScaleBar(myComposerMap, myTopOffset) # Update the top offset for the next horizontal row of items myTopOffset += self.mapHeight + self.verticalSpacing - 1 myImpactTitleHeight = self.drawImpactTitle(myTopOffset) # Update the top offset for the next horizontal row of items if myImpactTitleHeight: myTopOffset += myImpactTitleHeight + self.verticalSpacing + 2 self.drawLegend(myTopOffset) self.drawHostAndTime(myTopOffset) self.drawDisclaimer() def renderComposition(self): """Render the map composition to an image and save that to disk. Args: None Returns: tuple: * str: myImagePath - absolute path to png of rendered map * QImage: myImage - in memory copy of rendered map * QRectF: myTargetArea - dimensions of rendered map str: Absolute file system path to the rendered image. Raises: None """ LOGGER.debug('InaSAFE Map renderComposition called') # NOTE: we ignore self.composition.printAsRaster() and always rasterise myWidth = (int)(self.pageDpi * self.pageWidth / 25.4) myHeight = (int)(self.pageDpi * self.pageHeight / 25.4) myImage = QtGui.QImage(QtCore.QSize(myWidth, myHeight), QtGui.QImage.Format_ARGB32) myImage.setDotsPerMeterX(dpiToMeters(self.pageDpi)) myImage.setDotsPerMeterY(dpiToMeters(self.pageDpi)) # Only works in Qt4.8 #myImage.fill(QtGui.qRgb(255, 255, 255)) # Works in older Qt4 versions myImage.fill(55 + 255 * 256 + 255 * 256 * 256) myImagePainter = QtGui.QPainter(myImage) mySourceArea = QtCore.QRectF(0, 0, self.pageWidth, self.pageHeight) myTargetArea = QtCore.QRectF(0, 0, myWidth, myHeight) self.composition.render(myImagePainter, myTargetArea, mySourceArea) myImagePainter.end() myImagePath = unique_filename(prefix='mapRender_', suffix='.png', dir=temp_dir()) myImage.save(myImagePath) return myImagePath, myImage, myTargetArea def printToPdf(self, theFilename): """Generate the printout for our final map. Args: theFilename: str - optional path on the file system to which the pdf should be saved. If None, a generated file name will be used. Returns: str: file name of the output file (equivalent to theFilename if provided). Raises: None """ LOGGER.debug('InaSAFE Map printToPdf called') if theFilename is None: myMapPdfPath = unique_filename(prefix='report', suffix='.pdf', dir=temp_dir('work')) else: # We need to cast to python string in case we receive a QString myMapPdfPath = str(theFilename) self.composeMap() self.printer = setupPrinter(myMapPdfPath) _, myImage, myRectangle = self.renderComposition() myPainter = QtGui.QPainter(self.printer) myPainter.drawImage(myRectangle, myImage, myRectangle) myPainter.end() return myMapPdfPath def drawLogo(self, theTopOffset): """Add a picture containing the logo to the map top left corner Args: theTopOffset - vertical offset at which the logo shoudl be drawn Returns: None Raises: None """ myLogo = QgsComposerPicture(self.composition) myLogo.setPictureFile(':/plugins/inasafe/bnpb_logo.png') myLogo.setItemPosition(self.pageMargin, theTopOffset, 10, 10) if qgisVersion() >= 10800: # 1.8 or newer myLogo.setFrameEnabled(self.showFramesFlag) else: myLogo.setFrame(self.showFramesFlag) myLogo.setZValue(1) # To ensure it overlays graticule markers self.composition.addItem(myLogo) def drawTitle(self, theTopOffset): """Add a title to the composition. Args: theTopOffset - vertical offset at which the map should be drawn Returns: float - the height of the label as rendered Raises: None """ LOGGER.debug('InaSAFE Map drawTitle called') myFontSize = 14 myFontWeight = QtGui.QFont.Bold myItalicsFlag = False myFont = QtGui.QFont('verdana', myFontSize, myFontWeight, myItalicsFlag) myLabel = QgsComposerLabel(self.composition) myLabel.setFont(myFont) myHeading = self.tr('InaSAFE - Indonesia Scenario Assessment' ' for Emergencies') myLabel.setText(myHeading) myLabel.adjustSizeToText() myLabelHeight = 10.0 # determined using qgis map composer myLabelWidth = 170.0 # item - position and size...option myLeftOffset = self.pageWidth - self.pageMargin - myLabelWidth myLabel.setItemPosition( myLeftOffset, theTopOffset - 2, # -2 to push it up a little myLabelWidth, myLabelHeight, ) myLabel.setFrame(self.showFramesFlag) self.composition.addItem(myLabel) return myLabelHeight def drawMap(self, theTopOffset): """Add a map to the composition and return the compsermap instance. Args: theTopOffset - vertical offset at which the map should be drawn Returns: A QgsComposerMap instance is returned Raises: None """ LOGGER.debug('InaSAFE Map drawMap called') myMapWidth = self.mapWidth myComposerMap = QgsComposerMap(self.composition, self.pageMargin, theTopOffset, myMapWidth, self.mapHeight) #myExtent = self.iface.mapCanvas().extent() # The dimensions of the map canvas and the print compser map may # differ. So we set the map composer extent using the canvas and # then defer to the map canvas's map extents thereafter # Update: disabled as it results in a rectangular rather than # square map #myComposerMap.setNewExtent(myExtent) myComposerExtent = myComposerMap.extent() # Recenter the composer map on the center of the canvas # Note that since the composer map is square and the canvas may be # arbitrarily shaped, we center based on the longest edge myCanvasExtent = self.iface.mapCanvas().extent() myWidth = myCanvasExtent.width() myHeight = myCanvasExtent.height() myLongestLength = myWidth if myWidth < myHeight: myLongestLength = myHeight myHalfLength = myLongestLength / 2 myCenter = myCanvasExtent.center() myMinX = myCenter.x() - myHalfLength myMaxX = myCenter.x() + myHalfLength myMinY = myCenter.y() - myHalfLength myMaxY = myCenter.y() + myHalfLength mySquareExtent = QgsRectangle(myMinX, myMinY, myMaxX, myMaxY) myComposerMap.setNewExtent(mySquareExtent) myComposerMap.setGridEnabled(True) myNumberOfSplits = 5 # .. todo:: Write logic to adjust preciosn so that adjacent tick marks # always have different displayed values myPrecision = 2 myXInterval = myComposerExtent.width() / myNumberOfSplits myComposerMap.setGridIntervalX(myXInterval) myYInterval = myComposerExtent.height() / myNumberOfSplits myComposerMap.setGridIntervalY(myYInterval) myComposerMap.setGridStyle(QgsComposerMap.Cross) myCrossLengthMM = 1 myComposerMap.setCrossLength(myCrossLengthMM) myComposerMap.setZValue(0) # To ensure it does not overlay logo myFontSize = 6 myFontWeight = QtGui.QFont.Normal myItalicsFlag = False myFont = QtGui.QFont('verdana', myFontSize, myFontWeight, myItalicsFlag) myComposerMap.setGridAnnotationFont(myFont) myComposerMap.setGridAnnotationPrecision(myPrecision) myComposerMap.setShowGridAnnotation(True) myComposerMap.setGridAnnotationDirection( QgsComposerMap.BoundaryDirection) self.composition.addItem(myComposerMap) self.drawGraticuleMask(theTopOffset) return myComposerMap def drawGraticuleMask(self, theTopOffset): """A helper function to mask out graticule labels on the right side by over painting a white rectangle with white border on them. Args: theTopOffset - vertical offset at which the map should be drawn Returns: None Raises: None """ LOGGER.debug('InaSAFE Map drawGraticuleMask called') myLeftOffset = self.pageMargin + self.mapWidth myRect = QgsComposerShape(myLeftOffset + 0.5, theTopOffset, self.pageWidth - myLeftOffset, self.mapHeight + 1, self.composition) myRect.setShapeType(QgsComposerShape.Rectangle) myRect.setLineWidth(0.1) myRect.setFrame(False) myRect.setOutlineColor(QtGui.QColor(255, 255, 255)) myRect.setFillColor(QtGui.QColor(255, 255, 255)) myRect.setOpacity(100) # These two lines seem superfluous but are needed myBrush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) myRect.setBrush(myBrush) self.composition.addItem(myRect) def drawNativeScaleBar(self, theComposerMap, theTopOffset): """Draw a scale bar using QGIS' native drawing - in the case of geographic maps, scale will be in degrees, not km. Args: None Returns: None Raises: Any exceptions raised by the InaSAFE library will be propagated. """ LOGGER.debug('InaSAFE Map drawNativeScaleBar called') myScaleBar = QgsComposerScaleBar(self.composition) myScaleBar.setStyle('Numeric') # optionally modify the style myScaleBar.setComposerMap(theComposerMap) myScaleBar.applyDefaultSize() myScaleBarHeight = myScaleBar.boundingRect().height() myScaleBarWidth = myScaleBar.boundingRect().width() # -1 to avoid overlapping the map border myScaleBar.setItemPosition( self.pageMargin + 1, theTopOffset + self.mapHeight - (myScaleBarHeight * 2), myScaleBarWidth, myScaleBarHeight) myScaleBar.setFrame(self.showFramesFlag) # Disabled for now #self.composition.addItem(myScaleBar) def drawScaleBar(self, theComposerMap, theTopOffset): """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 scalebar for a geographic map in km. .. seealso:: :meth:`drawNativeScaleBar` Args: * theComposerMap - QgsComposerMap instance used as the basis scale calculations. * theTopOffset - vertical offset at which the map should be drawn Returns: None Raises: Any exceptions raised by the InaSAFE library will be propagated. """ LOGGER.debug('InaSAFE Map drawScaleBar called') myCanvas = self.iface.mapCanvas() myRenderer = myCanvas.mapRenderer() # # Add a linear map scale # myDistanceArea = QgsDistanceArea() myDistanceArea.setSourceCrs(myRenderer.destinationCrs().srsid()) myDistanceArea.setProjectionsEnabled(True) # Determine how wide our map is in km/m # Starting point at BL corner myComposerExtent = theComposerMap.extent() myStartPoint = QgsPoint(myComposerExtent.xMinimum(), myComposerExtent.yMinimum()) # Ending point at BR corner myEndPoint = QgsPoint(myComposerExtent.xMaximum(), myComposerExtent.yMinimum()) myGroundDistance = myDistanceArea.measureLine(myStartPoint, myEndPoint) # Get the equivalent map distance per page mm myMapWidth = self.mapWidth # How far is 1mm on map on the ground in meters? myMMToGroundDistance = myGroundDistance / myMapWidth #print 'MM:', myMMDistance # How long we want the scale bar to be in relation to the map myScaleBarToMapRatio = 0.5 # How many divisions the scale bar should have myTickCount = 5 myScaleBarWidthMM = myMapWidth * myScaleBarToMapRatio myPrintSegmentWidthMM = myScaleBarWidthMM / myTickCount # Segment width in real world (m) # We apply some logic here so that segments are displayed in meters # if each segment is less that 1000m otherwise km. Also the segment # lengths are rounded down to human looking numbers e.g. 1km not 1.1km myUnits = '' myGroundSegmentWidth = myPrintSegmentWidthMM * myMMToGroundDistance if myGroundSegmentWidth < 1000: myUnits = 'm' myGroundSegmentWidth = round(myGroundSegmentWidth) # adjust the segment width now to account for rounding myPrintSegmentWidthMM = myGroundSegmentWidth / myMMToGroundDistance else: myUnits = 'km' # Segment with in real world (km) myGroundSegmentWidth = round(myGroundSegmentWidth / 1000) myPrintSegmentWidthMM = ((myGroundSegmentWidth * 1000) / myMMToGroundDistance) # Now adjust the scalebar width to account for rounding myScaleBarWidthMM = myTickCount * myPrintSegmentWidthMM #print "SBWMM:", myScaleBarWidthMM #print "SWMM:", myPrintSegmentWidthMM #print "SWM:", myGroundSegmentWidthM #print "SWKM:", myGroundSegmentWidthKM # start drawing in line segments myScaleBarHeight = 5 # mm myLineWidth = 0.3 # mm myInsetDistance = 7 # how much to inset the scalebar into the map by myScaleBarX = self.pageMargin + myInsetDistance myScaleBarY = (theTopOffset + self.mapHeight - myInsetDistance - myScaleBarHeight) # mm # Draw an outer background box - shamelessly hardcoded buffer myRect = QgsComposerShape( myScaleBarX - 4, # left edge myScaleBarY - 3, # top edge myScaleBarWidthMM + 13, # right edge myScaleBarHeight + 6, # bottom edge self.composition) myRect.setShapeType(QgsComposerShape.Rectangle) myRect.setLineWidth(myLineWidth) myRect.setFrame(False) myBrush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) # workaround for missing setTransparentFill missing from python api myRect.setBrush(myBrush) self.composition.addItem(myRect) # Set up the tick label font myFontWeight = QtGui.QFont.Normal myFontSize = 6 myItalicsFlag = False myFont = QtGui.QFont('verdana', myFontSize, myFontWeight, myItalicsFlag) # Draw the bottom line myUpshift = 0.3 # shift the bottom line up for better rendering myRect = QgsComposerShape(myScaleBarX, myScaleBarY + myScaleBarHeight - myUpshift, myScaleBarWidthMM, 0.1, self.composition) myRect.setShapeType(QgsComposerShape.Rectangle) myRect.setLineWidth(myLineWidth) myRect.setFrame(False) self.composition.addItem(myRect) # Now draw the scalebar ticks for myTickCountIterator in range(0, myTickCount + 1): myDistanceSuffix = '' if myTickCountIterator == myTickCount: myDistanceSuffix = ' ' + myUnits myRealWorldDistance = ( '%.0f%s' % (myTickCountIterator * myGroundSegmentWidth, myDistanceSuffix)) #print 'RW:', myRealWorldDistance myMMOffset = myScaleBarX + (myTickCountIterator * myPrintSegmentWidthMM) #print 'MM:', myMMOffset myTickHeight = myScaleBarHeight / 2 # Lines are not exposed by the api yet so we # bodge drawing lines using rectangles with 1px height or width myTickWidth = 0.1 # width or rectangle to be drawn myUpTickLine = QgsComposerShape( myMMOffset, myScaleBarY + myScaleBarHeight - myTickHeight, myTickWidth, myTickHeight, self.composition) myUpTickLine.setShapeType(QgsComposerShape.Rectangle) myUpTickLine.setLineWidth(myLineWidth) myUpTickLine.setFrame(False) self.composition.addItem(myUpTickLine) # # Add a tick label # myLabel = QgsComposerLabel(self.composition) myLabel.setFont(myFont) myLabel.setText(myRealWorldDistance) myLabel.adjustSizeToText() myLabel.setItemPosition(myMMOffset - 3, myScaleBarY - myTickHeight) myLabel.setFrame(self.showFramesFlag) self.composition.addItem(myLabel) def drawImpactTitle(self, theTopOffset): """Draw the map subtitle - obtained from the impact layer keywords. Args: theTopOffset - vertical offset at which to begin drawing Returns: float - the height of the label as rendered Raises: None """ LOGGER.debug('InaSAFE Map drawImpactTitle called') myTitle = self.getMapTitle() if myTitle is None: myTitle = '' myFontSize = 20 myFontWeight = QtGui.QFont.Bold myItalicsFlag = False myFont = QtGui.QFont('verdana', myFontSize, myFontWeight, myItalicsFlag) myLabel = QgsComposerLabel(self.composition) myLabel.setFont(myFont) myHeading = myTitle myLabel.setText(myHeading) myLabelWidth = self.pageWidth - (self.pageMargin * 2) myLabelHeight = 12 myLabel.setItemPosition(self.pageMargin, theTopOffset, myLabelWidth, myLabelHeight) myLabel.setFrame(self.showFramesFlag) self.composition.addItem(myLabel) return myLabelHeight def drawLegend(self, theTopOffset): """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. Args: theTopOffset - vertical offset at which to begin drawing Returns: None Raises: None """ LOGGER.debug('InaSAFE Map drawLegend called') mapLegendAttributes = self.getMapLegendAtributes() legendNotes = mapLegendAttributes.get('legend_notes', None) legendUnits = mapLegendAttributes.get('legend_units', None) legendTitle = mapLegendAttributes.get('legend_title', None) LOGGER.debug(mapLegendAttributes) myLegend = MapLegend(self.layer, self.pageDpi, legendTitle, legendNotes, legendUnits) self.legend = myLegend.getLegend() myPicture1 = QgsComposerPicture(self.composition) myLegendFilePath = unique_filename(prefix='legend', suffix='.png', dir='work') self.legend.save(myLegendFilePath, 'PNG') myPicture1.setPictureFile(myLegendFilePath) myLegendHeight = pointsToMM(self.legend.height(), self.pageDpi) myLegendWidth = pointsToMM(self.legend.width(), self.pageDpi) myPicture1.setItemPosition(self.pageMargin, theTopOffset, myLegendWidth, myLegendHeight) myPicture1.setFrame(False) self.composition.addItem(myPicture1) os.remove(myLegendFilePath) def drawImage(self, theImage, theWidthMM, theLeftOffset, theTopOffset): """Helper to draw an image directly onto the QGraphicsScene. This is an alternative to using QgsComposerPicture which in some cases leaves artifacts under windows. The Pixmap will have a transform applied to it so that it is rendered with the same resolution as the composition. Args: * theImage: QImage that will be rendered to the layout. * theWidthMM: int - desired width in mm of output on page. * theLeftOffset: int - offset from left of page. * theTopOffset: int - offset from top of page. Returns: QGraphicsSceneItem is returned Raises: None """ LOGGER.debug('InaSAFE Map drawImage called') myDesiredWidthMM = theWidthMM # mm myDesiredWidthPX = mmToPoints(myDesiredWidthMM, self.pageDpi) myActualWidthPX = theImage.width() myScaleFactor = myDesiredWidthPX / myActualWidthPX LOGGER.debug('%s %s %s' % (myScaleFactor, myActualWidthPX, myDesiredWidthPX)) myTransform = QtGui.QTransform() myTransform.scale(myScaleFactor, myScaleFactor) myTransform.rotate(0.5) myItem = self.composition.addPixmap(QtGui.QPixmap.fromImage(theImage)) myItem.setTransform(myTransform) myItem.setOffset(theLeftOffset / myScaleFactor, theTopOffset / myScaleFactor) return myItem def drawHostAndTime(self, theTopOffset): """Add a disclaimer to the composition. Args: theTopOffset - vertical offset at which to begin drawing Returns: None Raises: None """ LOGGER.debug('InaSAFE Map drawDisclaimer called') #elapsed_time: 11.612545 #user: timlinux #host_name: ultrabook #time_stamp: 2012-10-13_23:10:31 #myUser = self.keywordIO.readKeywords(self.layer, 'user') #myHost = self.keywordIO.readKeywords(self.layer, 'host_name') myDateTime = self.keywordIO.readKeywords(self.layer, 'time_stamp') myTokens = myDateTime.split('_') myDate = myTokens[0] myTime = myTokens[1] #myElapsedTime = self.keywordIO.readKeywords(self.layer, # 'elapsed_time') #myElapsedTime = humaniseSeconds(myElapsedTime) myLongVersion = get_version() myTokens = myLongVersion.split('.') myVersion = '%s.%s.%s' % (myTokens[0], myTokens[1], myTokens[2]) myLabelText = self.tr( 'Date and time of assessment: %1 %2\n' 'Special note: This assessment is a guide - we strongly recommend ' 'that you ground truth the results shown here before deploying ' 'resources and / or personnel.\n' 'Assessment carried out using InaSAFE release %3 (QGIS ' 'plugin version).').arg(myDate).arg(myTime).arg(myVersion) myFontSize = 6 myFontWeight = QtGui.QFont.Normal myItalicsFlag = True myFont = QtGui.QFont('verdana', myFontSize, myFontWeight, myItalicsFlag) myLabel = QgsComposerLabel(self.composition) myLabel.setFont(myFont) myLabel.setText(myLabelText) myLabel.adjustSizeToText() myLabelHeight = 50.0 # mm determined using qgis map composer myLabelWidth = (self.pageWidth / 2) - self.pageMargin myLeftOffset = self.pageWidth / 2 # put in right half of page myLabel.setItemPosition( myLeftOffset, theTopOffset, myLabelWidth, myLabelHeight, ) myLabel.setFrame(self.showFramesFlag) self.composition.addItem(myLabel) def drawDisclaimer(self): """Add a disclaimer to the composition. Args: None Returns: None Raises: None """ LOGGER.debug('InaSAFE Map drawDisclaimer called') myFontSize = 10 myFontWeight = QtGui.QFont.Normal myItalicsFlag = True myFont = QtGui.QFont('verdana', myFontSize, myFontWeight, myItalicsFlag) myLabel = QgsComposerLabel(self.composition) myLabel.setFont(myFont) myLabel.setText(self.disclaimer) myLabel.adjustSizeToText() myLabelHeight = 7.0 # mm determined using qgis map composer myLabelWidth = self.pageWidth # item - position and size...option myLeftOffset = self.pageMargin myTopOffset = self.pageHeight - self.pageMargin myLabel.setItemPosition( myLeftOffset, myTopOffset, myLabelWidth, myLabelHeight, ) myLabel.setFrame(self.showFramesFlag) self.composition.addItem(myLabel) def getMapTitle(self): """Get the map title from the layer keywords if possible. Args: None Returns: None on error, otherwise the title Raises: Any exceptions raised by the InaSAFE library will be propagated. """ LOGGER.debug('InaSAFE Map getMapTitle called') try: myTitle = self.keywordIO.readKeywords(self.layer, 'map_title') return myTitle except KeywordNotFoundError: return None except Exception: return None def getMapLegendAtributes(self): """Get the map legend attribute from the layer keywords if possible. Args: None Returns: None on error, otherwise the attributes (notes and units) Raises: Any exceptions raised by the InaSAFE library will be propagated. """ LOGGER.debug('InaSAFE Map getMapLegendAtributes called') legendAttributes = ['legend_notes', 'legend_units', 'legend_title'] dictLegendAttributes = {} for myLegendAttribute in legendAttributes: try: dictLegendAttributes[myLegendAttribute] = \ self.keywordIO.readKeywords(self.layer, myLegendAttribute) except KeywordNotFoundError: pass except Exception: pass return dictLegendAttributes def showComposer(self): """Show the composition in a composer view so the user can tweak it if they want to. Args: None Returns: None Raises: None """ myView = QgsComposerView(self.iface.mainWindow()) myView.show() def writeTemplate(self, theTemplateFilePath): """Write the current composition as a template that can be re-used in QGIS.""" myDocument = QtXml.QDomDocument() myElement = myDocument.createElement('Composer') myDocument.appendChild(myElement) self.composition.writeXML(myElement, myDocument) myXml = myDocument.toByteArray() myFile = file(theTemplateFilePath, 'wb') myFile.write(myXml) myFile.close() def renderTemplate(self, theTemplateFilePath, theOutputFilePath): """Load a QgsComposer map from a template and render it .. note:: THIS METHOD IS EXPERIMENTAL AND CURRENTLY NON FUNCTIONAL Args: theTemplateFilePath - path to the template that should be loaded. theOutputFilePath - path for the output pdf Returns: None Raises: None """ self.setupComposition() myResolution = self.composition.printResolution() self.printer = setupPrinter(theOutputFilePath, theResolution=myResolution) if self.composition: myFile = QtCore.QFile(theTemplateFilePath) myDocument = QtXml.QDomDocument() myDocument.setContent(myFile, False) # .. todo:: fix magic param myNodeList = myDocument.elementsByTagName('Composer') if myNodeList.size() > 0: myElement = myNodeList.at(0).toElement() self.composition.readXML(myElement, myDocument) self.printToPdf(theOutputFilePath)
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
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()
class Map(): """A class for creating a map.""" def __init__(self, iface): """Constructor for the Map class. :param iface: Reference to the QGIS iface object. :type iface: QgsAppInterface """ LOGGER.debug('InaSAFE Map class initialised') self.iface = iface self.layer = iface.activeLayer() self.keywordIO = KeywordIO() self.printer = None self.composition = None self.legend = None self.pageWidth = 210 # width in mm self.pageHeight = 297 # height in mm self.pageDpi = 300.0 self.pageMargin = 10 # margin in mm self.verticalSpacing = 1 # vertical spacing between elements self.showFramesFlag = False # intended for debugging use only # make a square map where width = height = page width self.mapHeight = self.pageWidth - (self.pageMargin * 2) self.mapWidth = self.mapHeight self.disclaimer = self.tr('InaSAFE has been jointly developed by' ' BNPB, AusAid & the World Bank') def tr(self, string): """We implement this since we do not inherit QObject. :param string: String for translation. :type string: QString, str :returns: Translated version of theString. :rtype: QString """ # noinspection PyCallByClass,PyTypeChecker,PyArgumentList return QtCore.QCoreApplication.translate('Map', string) def set_impact_layer(self, layer): """Set the layer that will be used for stats, legend and reporting. :param layer: Layer that will be used for stats, legend and reporting. :type layer: QgsMapLayer, QgsRasterLayer, QgsVectorLayer """ self.layer = layer def setup_composition(self): """Set up the composition ready for drawing elements onto it.""" LOGGER.debug('InaSAFE Map setupComposition called') myCanvas = self.iface.mapCanvas() myRenderer = myCanvas.mapRenderer() self.composition = QgsComposition(myRenderer) self.composition.setPlotStyle(QgsComposition.Print) # or preview self.composition.setPaperSize(self.pageWidth, self.pageHeight) self.composition.setPrintResolution(self.pageDpi) self.composition.setPrintAsRaster(True) def compose_map(self): """Place all elements on the map ready for printing.""" self.setup_composition() # Keep track of our vertical positioning as we work our way down # the page placing elements on it. myTopOffset = self.pageMargin self.draw_logo(myTopOffset) myLabelHeight = self.draw_title(myTopOffset) # Update the map offset for the next row of content myTopOffset += myLabelHeight + self.verticalSpacing myComposerMap = self.draw_map(myTopOffset) self.draw_scalebar(myComposerMap, myTopOffset) # Update the top offset for the next horizontal row of items myTopOffset += self.mapHeight + self.verticalSpacing - 1 myImpactTitleHeight = self.draw_impact_title(myTopOffset) # Update the top offset for the next horizontal row of items if myImpactTitleHeight: myTopOffset += myImpactTitleHeight + self.verticalSpacing + 2 self.draw_legend(myTopOffset) self.draw_host_and_time(myTopOffset) self.draw_disclaimer() def render(self): """Render the map composition to an image and save that to disk. :returns: A three-tuple of: * str: myImagePath - absolute path to png of rendered map * QImage: myImage - in memory copy of rendered map * QRectF: myTargetArea - dimensions of rendered map :rtype: tuple """ LOGGER.debug('InaSAFE Map renderComposition called') # NOTE: we ignore self.composition.printAsRaster() and always rasterise myWidth = int(self.pageDpi * self.pageWidth / 25.4) myHeight = int(self.pageDpi * self.pageHeight / 25.4) myImage = QtGui.QImage(QtCore.QSize(myWidth, myHeight), QtGui.QImage.Format_ARGB32) myImage.setDotsPerMeterX(dpi_to_meters(self.pageDpi)) myImage.setDotsPerMeterY(dpi_to_meters(self.pageDpi)) # Only works in Qt4.8 #myImage.fill(QtGui.qRgb(255, 255, 255)) # Works in older Qt4 versions myImage.fill(55 + 255 * 256 + 255 * 256 * 256) myImagePainter = QtGui.QPainter(myImage) mySourceArea = QtCore.QRectF(0, 0, self.pageWidth, self.pageHeight) myTargetArea = QtCore.QRectF(0, 0, myWidth, myHeight) self.composition.render(myImagePainter, myTargetArea, mySourceArea) myImagePainter.end() myImagePath = unique_filename(prefix='mapRender_', suffix='.png', dir=temp_dir()) myImage.save(myImagePath) return myImagePath, myImage, myTargetArea def make_pdf(self, filename): """Generate the printout for our final map. :param filename: Path on the file system to which the pdf should be saved. If None, a generated file name will be used. :type filename: str :returns: File name of the output file (equivalent to filename if provided). :rtype: str """ LOGGER.debug('InaSAFE Map printToPdf called') if filename is None: myMapPdfPath = unique_filename( prefix='report', suffix='.pdf', dir=temp_dir('work')) else: # We need to cast to python string in case we receive a QString myMapPdfPath = str(filename) self.compose_map() self.printer = setup_printer(myMapPdfPath) _, myImage, myRectangle = self.render() myPainter = QtGui.QPainter(self.printer) myPainter.drawImage(myRectangle, myImage, myRectangle) myPainter.end() return myMapPdfPath def draw_logo(self, top_offset): """Add a picture containing the logo to the map top left corner :param top_offset: Vertical offset at which the logo should be drawn. :type top_offset: int """ myLogo = QgsComposerPicture(self.composition) myLogo.setPictureFile(':/plugins/inasafe/bnpb_logo.png') myLogo.setItemPosition(self.pageMargin, top_offset, 10, 10) myLogo.setFrameEnabled(self.showFramesFlag) myLogo.setZValue(1) # To ensure it overlays graticule markers self.composition.addItem(myLogo) def draw_title(self, top_offset): """Add a title to the composition. :param top_offset: Vertical offset at which the logo should be drawn. :type top_offset: int :returns: The height of the label as rendered. :rtype: float """ LOGGER.debug('InaSAFE Map drawTitle called') myFontSize = 14 myFontWeight = QtGui.QFont.Bold myItalicsFlag = False myFont = QtGui.QFont('verdana', myFontSize, myFontWeight, myItalicsFlag) myLabel = QgsComposerLabel(self.composition) myLabel.setFont(myFont) myHeading = self.tr('InaSAFE - Indonesia Scenario Assessment' ' for Emergencies') myLabel.setText(myHeading) myLabel.adjustSizeToText() myLabelHeight = 10.0 # determined using qgis map composer myLabelWidth = 170.0 # item - position and size...option myLeftOffset = self.pageWidth - self.pageMargin - myLabelWidth myLabel.setItemPosition(myLeftOffset, top_offset - 2, # -2 to push it up a little myLabelWidth, myLabelHeight, ) myLabel.setFrameEnabled(self.showFramesFlag) self.composition.addItem(myLabel) return myLabelHeight def draw_map(self, top_offset): """Add a map to the composition and return the composer map instance. :param top_offset: Vertical offset at which the logo should be drawn. :type top_offset: int :returns: The composer map. :rtype: QgsComposerMap """ LOGGER.debug('InaSAFE Map drawMap called') myMapWidth = self.mapWidth myComposerMap = QgsComposerMap( self.composition, self.pageMargin, top_offset, myMapWidth, self.mapHeight) #myExtent = self.iface.mapCanvas().extent() # The dimensions of the map canvas and the print composer map may # differ. So we set the map composer extent using the canvas and # then defer to the map canvas's map extents thereafter # Update: disabled as it results in a rectangular rather than # square map #myComposerMap.setNewExtent(myExtent) myComposerExtent = myComposerMap.extent() # Recenter the composer map on the center of the canvas # Note that since the composer map is square and the canvas may be # arbitrarily shaped, we center based on the longest edge myCanvasExtent = self.iface.mapCanvas().extent() myWidth = myCanvasExtent.width() myHeight = myCanvasExtent.height() myLongestLength = myWidth if myWidth < myHeight: myLongestLength = myHeight myHalfLength = myLongestLength / 2 myCenter = myCanvasExtent.center() myMinX = myCenter.x() - myHalfLength myMaxX = myCenter.x() + myHalfLength myMinY = myCenter.y() - myHalfLength myMaxY = myCenter.y() + myHalfLength mySquareExtent = QgsRectangle(myMinX, myMinY, myMaxX, myMaxY) myComposerMap.setNewExtent(mySquareExtent) myComposerMap.setGridEnabled(True) myNumberOfSplits = 5 # .. todo:: Write logic to adjust precision so that adjacent tick marks # always have different displayed values myPrecision = 2 myXInterval = myComposerExtent.width() / myNumberOfSplits myComposerMap.setGridIntervalX(myXInterval) myYInterval = myComposerExtent.height() / myNumberOfSplits myComposerMap.setGridIntervalY(myYInterval) myComposerMap.setGridStyle(QgsComposerMap.Cross) myCrossLengthMM = 1 myComposerMap.setCrossLength(myCrossLengthMM) myComposerMap.setZValue(0) # To ensure it does not overlay logo myFontSize = 6 myFontWeight = QtGui.QFont.Normal myItalicsFlag = False myFont = QtGui.QFont( 'verdana', myFontSize, myFontWeight, myItalicsFlag) myComposerMap.setGridAnnotationFont(myFont) myComposerMap.setGridAnnotationPrecision(myPrecision) myComposerMap.setShowGridAnnotation(True) myComposerMap.setGridAnnotationDirection( QgsComposerMap.BoundaryDirection, QgsComposerMap.Top) self.composition.addItem(myComposerMap) self.draw_graticule_mask(top_offset) return myComposerMap def draw_graticule_mask(self, top_offset): """A helper function to mask out graticule labels. It will hide labels on the right side by over painting a white rectangle with white border on them. **kludge** :param top_offset: Vertical offset at which the logo should be drawn. :type top_offset: int """ LOGGER.debug('InaSAFE Map drawGraticuleMask called') myLeftOffset = self.pageMargin + self.mapWidth myRect = QgsComposerShape(myLeftOffset + 0.5, top_offset, self.pageWidth - myLeftOffset, self.mapHeight + 1, self.composition) myRect.setShapeType(QgsComposerShape.Rectangle) myPen = QtGui.QPen() myPen.setColor(QtGui.QColor(0, 0, 0)) myPen.setWidthF(0.1) myRect.setPen(myPen) myRect.setBackgroundColor(QtGui.QColor(255, 255, 255)) myRect.setTransparency(100) #myRect.setLineWidth(0.1) #myRect.setFrameEnabled(False) #myRect.setOutlineColor(QtGui.QColor(255, 255, 255)) #myRect.setFillColor(QtGui.QColor(255, 255, 255)) #myRect.setOpacity(100) # These two lines seem superfluous but are needed myBrush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) myRect.setBrush(myBrush) self.composition.addItem(myRect) def draw_native_scalebar(self, composer_map, top_offset): """Draw a scale bar using QGIS' native drawing. In the case of geographic maps, scale will be in degrees, not km. :param composer_map: Composer map on which to draw the scalebar. :type composer_map: QgsComposerMap :param top_offset: Vertical offset at which the logo should be drawn. :type top_offset: int """ LOGGER.debug('InaSAFE Map drawNativeScaleBar called') myScaleBar = QgsComposerScaleBar(self.composition) myScaleBar.setStyle('Numeric') # optionally modify the style myScaleBar.setComposerMap(composer_map) myScaleBar.applyDefaultSize() myScaleBarHeight = myScaleBar.boundingRect().height() myScaleBarWidth = myScaleBar.boundingRect().width() # -1 to avoid overlapping the map border myScaleBar.setItemPosition( self.pageMargin + 1, top_offset + self.mapHeight - (myScaleBarHeight * 2), myScaleBarWidth, myScaleBarHeight) myScaleBar.setFrameEnabled(self.showFramesFlag) # Disabled for now #self.composition.addItem(myScaleBar) def draw_scalebar(self, composer_map, top_offset): """Add a numeric scale to the bottom left of the map. We draw the scale bar manually because QGIS does not yet support rendering a scale bar for a geographic map in km. .. seealso:: :meth:`drawNativeScaleBar` :param composer_map: Composer map on which to draw the scalebar. :type composer_map: QgsComposerMap :param top_offset: Vertical offset at which the logo should be drawn. :type top_offset: int """ LOGGER.debug('InaSAFE Map drawScaleBar called') myCanvas = self.iface.mapCanvas() myRenderer = myCanvas.mapRenderer() # # Add a linear map scale # myDistanceArea = QgsDistanceArea() myDistanceArea.setSourceCrs(myRenderer.destinationCrs().srsid()) myDistanceArea.setEllipsoidalMode(True) # Determine how wide our map is in km/m # Starting point at BL corner myComposerExtent = composer_map.extent() myStartPoint = QgsPoint(myComposerExtent.xMinimum(), myComposerExtent.yMinimum()) # Ending point at BR corner myEndPoint = QgsPoint(myComposerExtent.xMaximum(), myComposerExtent.yMinimum()) myGroundDistance = myDistanceArea.measureLine(myStartPoint, myEndPoint) # Get the equivalent map distance per page mm myMapWidth = self.mapWidth # How far is 1mm on map on the ground in meters? myMMToGroundDistance = myGroundDistance / myMapWidth #print 'MM:', myMMDistance # How long we want the scale bar to be in relation to the map myScaleBarToMapRatio = 0.5 # How many divisions the scale bar should have myTickCount = 5 myScaleBarWidthMM = myMapWidth * myScaleBarToMapRatio myPrintSegmentWidthMM = myScaleBarWidthMM / myTickCount # Segment width in real world (m) # We apply some logic here so that segments are displayed in meters # if each segment is less that 1000m otherwise km. Also the segment # lengths are rounded down to human looking numbers e.g. 1km not 1.1km myUnits = '' myGroundSegmentWidth = myPrintSegmentWidthMM * myMMToGroundDistance if myGroundSegmentWidth < 1000: myUnits = 'm' myGroundSegmentWidth = round(myGroundSegmentWidth) # adjust the segment width now to account for rounding myPrintSegmentWidthMM = myGroundSegmentWidth / myMMToGroundDistance else: myUnits = 'km' # Segment with in real world (km) myGroundSegmentWidth = round(myGroundSegmentWidth / 1000) myPrintSegmentWidthMM = ((myGroundSegmentWidth * 1000) / myMMToGroundDistance) # Now adjust the scalebar width to account for rounding myScaleBarWidthMM = myTickCount * myPrintSegmentWidthMM #print "SBWMM:", myScaleBarWidthMM #print "SWMM:", myPrintSegmentWidthMM #print "SWM:", myGroundSegmentWidthM #print "SWKM:", myGroundSegmentWidthKM # start drawing in line segments myScaleBarHeight = 5 # mm myLineWidth = 0.3 # mm myInsetDistance = 7 # how much to inset the scalebar into the map by myScaleBarX = self.pageMargin + myInsetDistance myScaleBarY = ( top_offset + self.mapHeight - myInsetDistance - myScaleBarHeight) # mm # Draw an outer background box - shamelessly hardcoded buffer myRect = QgsComposerShape(myScaleBarX - 4, # left edge myScaleBarY - 3, # top edge myScaleBarWidthMM + 13, # right edge myScaleBarHeight + 6, # bottom edge self.composition) myRect.setShapeType(QgsComposerShape.Rectangle) myPen = QtGui.QPen() myPen.setColor(QtGui.QColor(255, 255, 255)) myPen.setWidthF(myLineWidth) myRect.setPen(myPen) #myRect.setLineWidth(myLineWidth) myRect.setFrameEnabled(False) myBrush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) # workaround for missing setTransparentFill missing from python api myRect.setBrush(myBrush) self.composition.addItem(myRect) # Set up the tick label font myFontWeight = QtGui.QFont.Normal myFontSize = 6 myItalicsFlag = False myFont = QtGui.QFont('verdana', myFontSize, myFontWeight, myItalicsFlag) # Draw the bottom line myUpshift = 0.3 # shift the bottom line up for better rendering myRect = QgsComposerShape(myScaleBarX, myScaleBarY + myScaleBarHeight - myUpshift, myScaleBarWidthMM, 0.1, self.composition) myRect.setShapeType(QgsComposerShape.Rectangle) myPen = QtGui.QPen() myPen.setColor(QtGui.QColor(255, 255, 255)) myPen.setWidthF(myLineWidth) myRect.setPen(myPen) #myRect.setLineWidth(myLineWidth) myRect.setFrameEnabled(False) self.composition.addItem(myRect) # Now draw the scalebar ticks for myTickCountIterator in range(0, myTickCount + 1): myDistanceSuffix = '' if myTickCountIterator == myTickCount: myDistanceSuffix = ' ' + myUnits myRealWorldDistance = ('%.0f%s' % (myTickCountIterator * myGroundSegmentWidth, myDistanceSuffix)) #print 'RW:', myRealWorldDistance myMMOffset = myScaleBarX + (myTickCountIterator * myPrintSegmentWidthMM) #print 'MM:', myMMOffset myTickHeight = myScaleBarHeight / 2 # Lines are not exposed by the api yet so we # bodge drawing lines using rectangles with 1px height or width myTickWidth = 0.1 # width or rectangle to be drawn myUpTickLine = QgsComposerShape( myMMOffset, myScaleBarY + myScaleBarHeight - myTickHeight, myTickWidth, myTickHeight, self.composition) myUpTickLine.setShapeType(QgsComposerShape.Rectangle) myPen = QtGui.QPen() myPen.setWidthF(myLineWidth) myUpTickLine.setPen(myPen) #myUpTickLine.setLineWidth(myLineWidth) myUpTickLine.setFrameEnabled(False) self.composition.addItem(myUpTickLine) # # Add a tick label # myLabel = QgsComposerLabel(self.composition) myLabel.setFont(myFont) myLabel.setText(myRealWorldDistance) myLabel.adjustSizeToText() myLabel.setItemPosition( myMMOffset - 3, myScaleBarY - myTickHeight) myLabel.setFrameEnabled(self.showFramesFlag) self.composition.addItem(myLabel) def draw_impact_title(self, top_offset): """Draw the map subtitle - obtained from the impact layer keywords. :param top_offset: Vertical offset at which the logo should be drawn. :type top_offset: int :returns: The height of the label as rendered. :rtype: float """ LOGGER.debug('InaSAFE Map drawImpactTitle called') myTitle = self.map_title() if myTitle is None: myTitle = '' myFontSize = 20 myFontWeight = QtGui.QFont.Bold myItalicsFlag = False myFont = QtGui.QFont( 'verdana', myFontSize, myFontWeight, myItalicsFlag) myLabel = QgsComposerLabel(self.composition) myLabel.setFont(myFont) myHeading = myTitle myLabel.setText(myHeading) myLabelWidth = self.pageWidth - (self.pageMargin * 2) myLabelHeight = 12 myLabel.setItemPosition( self.pageMargin, top_offset, myLabelWidth, myLabelHeight) myLabel.setFrameEnabled(self.showFramesFlag) self.composition.addItem(myLabel) return myLabelHeight def draw_legend(self, top_offset): """Add a legend to the map using our custom legend renderer. .. note:: getLegend generates a pixmap in 150dpi so if you set the map to a higher dpi it will appear undersized. :param top_offset: Vertical offset at which the logo should be drawn. :type top_offset: int """ LOGGER.debug('InaSAFE Map drawLegend called') mapLegendAttributes = self.map_legend_attributes() legendNotes = mapLegendAttributes.get('legend_notes', None) legendUnits = mapLegendAttributes.get('legend_units', None) legendTitle = mapLegendAttributes.get('legend_title', None) LOGGER.debug(mapLegendAttributes) myLegend = MapLegend(self.layer, self.pageDpi, legendTitle, legendNotes, legendUnits) self.legend = myLegend.get_legend() myPicture1 = QgsComposerPicture(self.composition) myLegendFilePath = unique_filename( prefix='legend', suffix='.png', dir='work') self.legend.save(myLegendFilePath, 'PNG') myPicture1.setPictureFile(myLegendFilePath) myLegendHeight = points_to_mm(self.legend.height(), self.pageDpi) myLegendWidth = points_to_mm(self.legend.width(), self.pageDpi) myPicture1.setItemPosition(self.pageMargin, top_offset, myLegendWidth, myLegendHeight) myPicture1.setFrameEnabled(False) self.composition.addItem(myPicture1) os.remove(myLegendFilePath) def draw_image(self, theImage, theWidthMM, theLeftOffset, theTopOffset): """Helper to draw an image directly onto the QGraphicsScene. This is an alternative to using QgsComposerPicture which in some cases leaves artifacts under windows. The Pixmap will have a transform applied to it so that it is rendered with the same resolution as the composition. :param theImage: Image that will be rendered to the layout. :type theImage: QImage :param theWidthMM: Desired width in mm of output on page. :type theWidthMM: int :param theLeftOffset: Offset from left of page. :type theLeftOffset: int :param theTopOffset: Offset from top of page. :type theTopOffset: int :returns: Graphics scene item. :rtype: QGraphicsSceneItem """ LOGGER.debug('InaSAFE Map drawImage called') myDesiredWidthMM = theWidthMM # mm myDesiredWidthPX = mm_to_points(myDesiredWidthMM, self.pageDpi) myActualWidthPX = theImage.width() myScaleFactor = myDesiredWidthPX / myActualWidthPX LOGGER.debug('%s %s %s' % ( myScaleFactor, myActualWidthPX, myDesiredWidthPX)) myTransform = QtGui.QTransform() myTransform.scale(myScaleFactor, myScaleFactor) myTransform.rotate(0.5) # noinspection PyArgumentList myItem = self.composition.addPixmap(QtGui.QPixmap.fromImage(theImage)) myItem.setTransform(myTransform) myItem.setOffset(theLeftOffset / myScaleFactor, theTopOffset / myScaleFactor) return myItem def draw_host_and_time(self, top_offset): """Add a note with hostname and time to the composition. :param top_offset: Vertical offset at which the logo should be drawn. :type top_offset: int """ LOGGER.debug('InaSAFE Map drawDisclaimer called') #elapsed_time: 11.612545 #user: timlinux #host_name: ultrabook #time_stamp: 2012-10-13_23:10:31 #myUser = self.keywordIO.readKeywords(self.layer, 'user') #myHost = self.keywordIO.readKeywords(self.layer, 'host_name') myDateTime = self.keywordIO.read_keywords(self.layer, 'time_stamp') myTokens = myDateTime.split('_') myDate = myTokens[0] myTime = myTokens[1] #myElapsedTime = self.keywordIO.readKeywords(self.layer, # 'elapsed_time') #myElapsedTime = humaniseSeconds(myElapsedTime) myLongVersion = get_version() myTokens = myLongVersion.split('.') myVersion = '%s.%s.%s' % (myTokens[0], myTokens[1], myTokens[2]) myLabelText = self.tr( 'Date and time of assessment: %s %s\n' 'Special note: This assessment is a guide - we strongly recommend ' 'that you ground truth the results shown here before deploying ' 'resources and / or personnel.\n' 'Assessment carried out using InaSAFE release %s (QGIS ' 'plugin version).') % (myDate, myTime, myVersion) myFontSize = 6 myFontWeight = QtGui.QFont.Normal myItalicsFlag = True myFont = QtGui.QFont('verdana', myFontSize, myFontWeight, myItalicsFlag) myLabel = QgsComposerLabel(self.composition) myLabel.setFont(myFont) myLabel.setText(myLabelText) myLabel.adjustSizeToText() myLabelHeight = 50.0 # mm determined using qgis map composer myLabelWidth = (self.pageWidth / 2) - self.pageMargin myLeftOffset = self.pageWidth / 2 # put in right half of page myLabel.setItemPosition(myLeftOffset, top_offset, myLabelWidth, myLabelHeight, ) myLabel.setFrameEnabled(self.showFramesFlag) self.composition.addItem(myLabel) def draw_disclaimer(self): """Add a disclaimer to the composition.""" LOGGER.debug('InaSAFE Map drawDisclaimer called') myFontSize = 10 myFontWeight = QtGui.QFont.Normal myItalicsFlag = True myFont = QtGui.QFont('verdana', myFontSize, myFontWeight, myItalicsFlag) myLabel = QgsComposerLabel(self.composition) myLabel.setFont(myFont) myLabel.setText(self.disclaimer) myLabel.adjustSizeToText() myLabelHeight = 7.0 # mm determined using qgis map composer myLabelWidth = self.pageWidth # item - position and size...option myLeftOffset = self.pageMargin myTopOffset = self.pageHeight - self.pageMargin myLabel.setItemPosition(myLeftOffset, myTopOffset, myLabelWidth, myLabelHeight, ) myLabel.setFrameEnabled(self.showFramesFlag) self.composition.addItem(myLabel) def map_title(self): """Get the map title from the layer keywords if possible. :returns: None on error, otherwise the title. :rtype: None, str """ LOGGER.debug('InaSAFE Map getMapTitle called') try: myTitle = self.keywordIO.read_keywords(self.layer, 'map_title') return myTitle except KeywordNotFoundError: return None except Exception: return None def map_legend_attributes(self): """Get the map legend attribute from the layer keywords if possible. :returns: None on error, otherwise the attributes (notes and units). :rtype: None, str """ LOGGER.debug('InaSAFE Map getMapLegendAtributes called') legendAttributes = ['legend_notes', 'legend_units', 'legend_title'] dictLegendAttributes = {} for myLegendAttribute in legendAttributes: try: dictLegendAttributes[myLegendAttribute] = \ self.keywordIO.read_keywords(self.layer, myLegendAttribute) except KeywordNotFoundError: pass except Exception: pass return dictLegendAttributes def showComposer(self): """Show the composition in a composer view so the user can tweak it. """ myView = QgsComposerView(self.iface.mainWindow()) myView.show() def write_template(self, template_path): """Write current composition as a template that can be re-used in QGIS. :param template_path: Path to which template should be written. :type template_path: str """ myDocument = QtXml.QDomDocument() myElement = myDocument.createElement('Composer') myDocument.appendChild(myElement) self.composition.writeXML(myElement, myDocument) myXml = myDocument.toByteArray() myFile = file(template_path, 'wb') myFile.write(myXml) myFile.close() def render_template(self, template_path, output_path): """Load a QgsComposer map from a template and render it. .. note:: THIS METHOD IS EXPERIMENTAL AND CURRENTLY NON FUNCTIONAL :param template_path: Path to the template that should be loaded. :type template_path: str :param output_path: Path for the output pdf. :type output_path: str """ self.setup_composition() myResolution = self.composition.printResolution() self.printer = setup_printer( output_path, resolution=myResolution) if self.composition: myFile = QtCore.QFile(template_path) myDocument = QtXml.QDomDocument() myDocument.setContent(myFile, False) # .. todo:: fix magic param myNodeList = myDocument.elementsByTagName('Composer') if myNodeList.size() > 0: myElement = myNodeList.at(0).toElement() self.composition.readXML(myElement, myDocument) self.make_pdf(output_path)
def print_impact_table(self, output_path): """Pint summary from impact layer to PDF. ..note:: The order of the report: 1. Summary table 2. Aggregation table 3. Attribution table :param output_path: Output path. :type output_path: str :return: Path to generated pdf file. :rtype: str :raises: None """ keywords = self._keyword_io.read_keywords(self.layer) if output_path is None: output_path = unique_filename(suffix='.pdf', dir=temp_dir()) summary_table = keywords.get('impact_summary', None) full_table = keywords.get('impact_table', None) aggregation_table = keywords.get('postprocessing_report', None) attribution_table = impact_attribution(keywords) # (AG) We will not use impact_table as most of the IF use that as: # impact_table = impact_summary + some information intended to be # shown on screen (see FloodOsmBuilding) # Unless the impact_summary is None, we will use impact_table as the # alternative html = LOGO_ELEMENT.to_html() html += m.Heading(tr('Analysis Results'), **INFO_STYLE).to_html() if summary_table is None: html += full_table else: html += summary_table if aggregation_table is not None: html += aggregation_table if attribution_table is not None: html += attribution_table.to_html() html = html_header() + html + html_footer() # Print HTML using composition # For QGIS < 2.4 compatibility # QgsMapSettings is added in 2.4 if qgis_version() < 20400: map_settings = QgsMapRenderer() else: map_settings = QgsMapSettings() # A4 Portrait paper_width = 210 paper_height = 297 # noinspection PyCallingNonCallable composition = QgsComposition(map_settings) # noinspection PyUnresolvedReferences composition.setPlotStyle(QgsComposition.Print) composition.setPaperSize(paper_width, paper_height) composition.setPrintResolution(300) # Add HTML Frame # noinspection PyCallingNonCallable html_item = QgsComposerHtml(composition, False) margin_left = 10 margin_top = 10 # noinspection PyCallingNonCallable html_frame = QgsComposerFrame( composition, html_item, margin_left, margin_top, paper_width - 2 * margin_left, paper_height - 2 * margin_top) html_item.addFrame(html_frame) # Set HTML # From QGIS 2.6, we can set composer HTML with manual HTML if qgis_version() < 20600: html_path = unique_filename( prefix='report', suffix='.html', dir=temp_dir()) html_to_file(html, file_path=html_path) html_url = QUrl.fromLocalFile(html_path) html_item.setUrl(html_url) else: # noinspection PyUnresolvedReferences html_item.setContentMode(QgsComposerHtml.ManualHtml) # noinspection PyUnresolvedReferences html_item.setResizeMode(QgsComposerHtml.RepeatUntilFinished) html_item.setHtml(html) html_item.loadHtml() composition.exportAsPDF(output_path) return output_path
class 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()
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)
class VRPPrintComposer: """Atlas Generation""" def __init__( self, iface, gemname, coveragelayer, json_settings, gnr_nbrs, featurefilter, orthoimage, themen, templateqpt, pdfmap ): self.iface = iface self.legiface = self.iface.legendInterface() self.toc = self.iface.mainWindow().findChild(QTreeWidget, 'theMapLegend') self.canvas = self.iface.mapCanvas() self.map_renderer = self.canvas.mapRenderer() self.gem_name = gemname self.coverage_layer = coveragelayer self.gnrs = gnr_nbrs self.settings = json_settings self.feature_filter = featurefilter self.ortho = orthoimage self.ortho_lyr = None self.themen = themen self.composition = None self.composermap = None self.comp_textinfo = None self.template_qpt = templateqpt self.pdf_map = pdfmap dkmgem = self.settings.dkm_gemeinde(gemname) self.lyrname_ortho = self.settings.luftbild_lyrname() self.lyrname_dkm_gst = dkmgem['lyrnamegstk'] self.lyrname_dkm_gnr = dkmgem['lyrnamegnr'] self.comp_leg = [] self.comp_lbl = [] self.statistics = OrderedDict() def export_all_features_TEST(self): lyr = QgsVectorLayer('/home/bergw/VoGIS-Raumplanung-Daten/Geodaten/Raumplanung/Flaechenwidmung/Dornbirn/Flaechenwidmungsplan/fwp_flaeche.shp', 'flaeiw', 'ogr') lyr.loadNamedStyle('/home/bergw/VoGIS-Raumplanung-Daten/Geodaten/Raumplanung/Flaechenwidmung/Vorarlberg/Flaechenwidmungsplan/fwp_flaeche.qml') QgsMapLayerRegistry.instance().addMapLayer(lyr) def export_all_features(self): pdf_painter = None """Export map to pdf atlas style (one page per feature)""" if VRP_DEBUG is True: QgsMessageLog.logMessage(u'exporting map', DLG_CAPTION) try: result = self.__delete_pdf() if not result is None: return result ids = [] exp = QgsExpression(self.feature_filter) if exp.hasParserError(): raise Exception(exp.parserErrorString()) exp.prepare(self.coverage_layer.pendingFields()) for feature in self.coverage_layer.getFeatures(): value = exp.evaluate(feature) if exp.hasEvalError(): raise ValueError(exp.evalErrorString()) if bool(value): if VRP_DEBUG is True: QgsMessageLog.logMessage(u'export map, feature id:{0}'.format(feature.id()), DLG_CAPTION) ids.append(feature.id()) self.coverage_layer.select(ids) bbox = self.coverage_layer.boundingBoxOfSelected() self.canvas.zoomToSelected(self.coverage_layer) if VRP_DEBUG is True: QgsMessageLog.logMessage(u'bbox:{0}'.format(bbox.toString()), DLG_CAPTION) #self.map_renderer.setExtent(bbox) #self.map_renderer.updateScale() #read plotlayout composition = QgsComposition(self.map_renderer) self.composition = composition composition.setPlotStyle(QgsComposition.Print) error, xml_doc = self.__read_template() if not error is None: return error if composition.loadFromTemplate(xml_doc) is False: return u'Konnte Template nicht laden!\n{0}'.format(self.template_qpt) #read textinfo layout self.comp_textinfo = QgsComposition(self.map_renderer) self.comp_textinfo.setPlotStyle(QgsComposition.Print) error, xml_doc = self.__read_template(True) if not error is None: return error if self.comp_textinfo.loadFromTemplate(xml_doc) is False: return u'Konnte Template nicht laden!\n{0}'.format(self.settings.textinfo_layout()) new_ext = bbox if QGis.QGIS_VERSION_INT > 20200: compmaps = self.__get_items(QgsComposerMap) if len(compmaps) < 1: return u'Kein Kartenfenster im Layout vorhanden!' compmap = compmaps[0] else: if len(composition.composerMapItems()) < 1: return u'Kein Kartenfenster im Layout vorhanden!' compmap = composition.composerMapItems()[0] self.composermap = compmap #self.composermap.setPreviewMode(QgsComposerMap.Render) #self.composermap.setPreviewMode(QgsComposerMap.Rectangle) #taken from QgsComposerMap::setNewAtlasFeatureExtent (not yet available in QGIS 2.0) #http://www.qgis.org/api/qgscomposermap_8cpp_source.html#l00610 old_ratio = compmap.rect().width() / compmap.rect().height() new_ratio = new_ext.width() / new_ext.height() if old_ratio < new_ratio: new_height = new_ext.width() / old_ratio delta_height = new_height - new_ext.height() new_ext.setYMinimum( bbox.yMinimum() - delta_height / 2) new_ext.setYMaximum(bbox.yMaximum() + delta_height / 2) else: new_width = old_ratio * new_ext.height() delta_width = new_width - new_ext.width() new_ext.setXMinimum(bbox.xMinimum() - delta_width / 2) new_ext.setXMaximum(bbox.xMaximum() + delta_width / 2) if VRP_DEBUG is True: QgsMessageLog.logMessage(u'bbox old:{0}'.format(compmap.extent().toString()), DLG_CAPTION) compmap.setNewExtent(new_ext) if VRP_DEBUG is True: QgsMessageLog.logMessage(u'bbox new:{0}'.format(compmap.extent().toString()), DLG_CAPTION) #round up to next 1000 compmap.setNewScale(math.ceil((compmap.scale()/1000.0)) * 1000.0) if VRP_DEBUG is True: QgsMessageLog.logMessage(u'bbox new (after scale):{0}'.format(compmap.extent().toString()), DLG_CAPTION) #add ORTHO after new extent -> performance if not self.ortho is None: self.ortho_lyr = self.__add_raster_layer(self.ortho, self.lyrname_ortho) self.__reorder_layers() self.comp_leg = self.__get_items(QgsComposerLegend) self.comp_lbl = self.__get_items(QgsComposerLabel) self.__update_composer_items(self.settings.dkm_gemeinde(self.gem_name)['lyrnamegstk']) if VRP_DEBUG is True: QgsMessageLog.logMessage(u'paperWidth:{0} paperHeight:{1}'.format(composition.paperWidth(), composition.paperHeight()), DLG_CAPTION) printer = QPrinter() printer.setOutputFormat(QPrinter.PdfFormat) printer.setOutputFileName(self.pdf_map) printer.setPaperSize(QSizeF(composition.paperWidth(), composition.paperHeight()), QPrinter.Millimeter) printer.setFullPage(True) printer.setColorMode(QPrinter.Color) printer.setResolution(composition.printResolution()) pdf_painter = QPainter(printer) paper_rect_pixel = printer.pageRect(QPrinter.DevicePixel) paper_rect_mm = printer.pageRect(QPrinter.Millimeter) QgsPaintEngineHack.fixEngineFlags(printer.paintEngine()) #DKM only if len(self.themen) < 1: composition.render(pdf_painter, paper_rect_pixel, paper_rect_mm) else: self.statistics = OrderedDict() try: pass #lyr = QgsVectorLayer('/home/bergw/VoGIS-Raumplanung-Daten/Geodaten/Raumplanung/Flaechenwidmung/Dornbirn/Flaechenwidmungsplan/fwp_flaeche.shp', 'flaeiw', 'ogr') #lyr.loadNamedStyle('/home/bergw/VoGIS-Raumplanung-Daten/Geodaten/Raumplanung/Flaechenwidmung/Vorarlberg/Flaechenwidmungsplan/fwp_flaeche.qml') #QgsMapLayerRegistry.instance().addMapLayer(lyr) except: QgsMessageLog.logMessage('new lyr:{0}'.format(sys.exc_info()[0]), DLG_CAPTION) #QgsMapLayerRegistry.instance().addMapLayer(lyr) cntr = 0 for thema, sub_themen in self.themen.iteritems(): if VRP_DEBUG is True: QgsMessageLog.logMessage('drucke Thema:{0}'.format(thema.name), DLG_CAPTION) if sub_themen is None: layers = self.__add_layers(thema) self.__calculate_statistics(thema, thema, layers) #no qml -> not visible -> means no map if self.__at_least_one_visible(layers) is True: if cntr > 0: printer.newPage() self.__reorder_layers() self.__update_composer_items(thema.name, layers=layers) composition.renderPage(pdf_painter, 0) QgsMapLayerRegistry.instance().removeMapLayers([lyr.id() for lyr in layers]) cntr += 1 else: QgsMapLayerRegistry.instance().removeMapLayers([lyr.id() for lyr in layers]) if not sub_themen is None: for sub_thema in sub_themen: if VRP_DEBUG is True: QgsMessageLog.logMessage(u'drucke SubThema:{0}'.format(sub_thema.name), DLG_CAPTION) layers = self.__add_layers(sub_thema) self.__calculate_statistics(thema, sub_thema, layers) #no qml -> not visible -> means no map if self.__at_least_one_visible(layers) is True: if cntr > 0: printer.newPage() self.__reorder_layers() self.__update_composer_items(thema.name, subthema=sub_thema.name, layers=layers) composition.renderPage(pdf_painter, 0) QgsMapLayerRegistry.instance().removeMapLayers([lyr.id() for lyr in layers]) cntr += 1 else: QgsMapLayerRegistry.instance().removeMapLayers([lyr.id() for lyr in layers]) #output statistics if len(self.statistics) > 0: printer.setPaperSize(QSizeF(210, 297), QPrinter.Millimeter) tabelle = self.__get_item_byid(self.comp_textinfo, 'TABELLE') if tabelle is None: self.iface.messageBar().pushMessage(u'Layout (Textinfo): Kein Textelement mit ID "TABELLE" vorhanden.', QgsMessageBar.CRITICAL) else: try: str_flaechen = '' idx = 0 for gnr, stats in self.statistics.iteritems(): comma = ', ' if idx > 0 else '' str_flaechen += u'{0}{1} ({2:.2f}m²)'.format(comma, gnr, stats[0].flaeche) idx += 1 lbls = self.__get_items(QgsComposerLabel, self.comp_textinfo) self.__update_composer_items('', labels=lbls, gnrflaeche=str_flaechen) html = tabelle.text() html += u'<table>' #gnrcnt = 0 for gnr, stats in self.statistics.iteritems(): #if gnrcnt > 0: # html += u'<tr class="abstand"><td> </td><td> </td><td> </td></tr>' html += u'<tr><th class="gnr"></th><th class="gnr">{0}</th><th class="gnr"></th></tr>'.format(gnr) #html += u'<tr class="abstand"><td> </td><td> </td><td> </td></tr>' curr_thema = '' for stat in stats: if stat.thema != curr_thema: html += u'<tr><th class="thema"></th><th class="thema">{0}</th><th class="thema"></th></tr>'.format(stat.thema) curr_thema = stat.thema for thema, subthema in stat.subthemen.iteritems(): for quelle in subthema: html += u'<tr><td class="col1">{0}</td>'.format(quelle.name) attr_val = '' attr_area = '' for text, area in quelle.txt_area.iteritems(): attr_val += u'{0}<br />'.format(text) attr_area += u'{0:.2f}m² <br />'.format(area) html += u'<td class="col2">{0}</td><td class="col3">{1}</td></tr>'.format(attr_val, attr_area) #gnrcnt += 1 html += u'</table>' tabelle.setText(html) printer.newPage() self.comp_textinfo.renderPage(pdf_painter, 0) except: msg = 'Statistikausgabe:\n\n{0}'.format(traceback.format_exc()) QgsMessageLog.logMessage(msg, DLG_CAPTION) self.iface.messageBar().pushMessage(msg, QgsMessageBar.CRITICAL) except: msg = 'export pdf (catch all):\n\n{0}'.format(traceback.format_exc()) QgsMessageLog.logMessage(msg, DLG_CAPTION) self.iface.messageBar().pushMessage(msg.replace(u'\n', u''), QgsMessageBar.CRITICAL) return msg finally: #end pdf if not pdf_painter is None: pdf_painter.end() return None def __at_least_one_visible(self, layers): """ check, if at least one layer is visible layers are not visible, if they don't have a qml if there is no visible layer, there must not be an extra plot page """ one_visible = False for lyr in layers: if self.legiface.isLayerVisible(lyr): one_visible = True return one_visible def __calculate_statistics(self, thema, subthema, layers): #features = processing.features(self.coverage_layer) features = self.coverage_layer.selectedFeatures() for gstk in features: try: gnr = gstk[self.settings.fld_gnr()] flaeche = gstk.geometry().area() gstk_stats = VRPStatistik(gnr, thema.name, flaeche, self.gem_name) #pyqtRemoveInputHook() #pdb.set_trace() #go thru all data sources of subthema for quelle in subthema.quellen: if VRP_DEBUG is True: QgsMessageLog.logMessage(u'quelle:{0}'.format(quelle.name), DLG_CAPTION) #only use those with statistik == True if quelle.statistik is False: continue lyr_curr_quelle = None for lyr in layers: if quelle.name == lyr.name(): lyr_curr_quelle = lyr if lyr_curr_quelle is None: continue text_flaeche = self.__get_text_flaeche(quelle, gstk, lyr_curr_quelle, quelle.attribut) sub_stat = VRPStatistikSubThema(quelle.name, text_flaeche) gstk_stats.add_subthema(thema.name, sub_stat) if gnr in self.statistics: self.statistics[gnr].append(gstk_stats) else: self.statistics[gnr] = [gstk_stats] except: msg = '__calculate_statistics:\n\n{0}'.format(traceback.format_exc()) QgsMessageLog.logMessage(msg, DLG_CAPTION, QgsMessageLog.CRITICAL) msg = msg.replace('\n', '') self.iface.messageBar().pushMessage(msg, QgsMessageBar.CRITICAL) return def __get_text_flaeche(self, quelle, gstk, layer, fld_name): text = {} #performance! filter by bb of gstk first feat_req = QgsFeatureRequest() feat_req.setFilterRect(gstk.geometry().boundingBox()) for feat in layer.getFeatures(feat_req): if feat.geometry().intersects(gstk.geometry()): #no fld_name defined: means yes/no only if fld_name is None: attr_val = u'Ja' else: attr_val = feat[fld_name] #convert everything to string #JSON only allows for string keys -> settingsfile if isinstance( attr_val, (int, long)): attr_val = unicode(attr_val) elif isinstance(attr_val, float): attr_val = u'{0:.0f}'.format(attr_val) #replace attribute values with mapping text from settings file if not quelle.text is None: if attr_val in quelle.text: attr_val = quelle.text[attr_val] flaeche = feat.geometry().intersection(gstk.geometry()).area() if fld_name in text: text[attr_val] += flaeche else: text[attr_val] = flaeche if len(text) < 1 and fld_name is None: text[u'Nein'] = 0 elif len(text) < 1 and not fld_name is None: text[u'Nein'] = 0 return text def __get_thema_by_layername(self, lyrname): for thema in self.themen: if thema.name == lyrname: return thema for subthema in thema.subthemen: if subthema.name == lyrname: return subthema for quelle in subthema.quellen: if quelle.name == lyrname: return subthema return None def __update_composer_items(self, oberthema, subthema=None, labels=None, gnrflaeche=None, layers=None): if labels is None: labels = self.comp_lbl for leg in self.comp_leg: #if not layers is None: #leg_model = leg.model() # QgsMessageLog.logMessage(u'layerSet:{0}'.format(leg.composerMap().layerSet()), DLG_CAPTION) # for lyr in layers: # QgsMessageLog.logMessage(u'LYR(ID):{0}'.format(lyr.id()), DLG_CAPTION) # leg_model.removeLayer(lyr.id()) #QgsMessageLog.logMessage(u'maprenderer layerSet:{0}'.format(self.map_renderer.layerSet()), DLG_CAPTION) #QgsMessageLog.logMessage(u'legmodel:{0}'.format(dir(leg_model)), DLG_CAPTION) #leg_model.setLayerSet([lyr.id() for lyr in layers]) #for lyr in layers: # leg_model.addLayer(lyr) leg.updateLegend() if not layers is None: lyr_names = [lyr.name() for lyr in layers] leg_model = leg.model() row_count = leg_model.rowCount() - 1 for idx in xrange(row_count, -1, -1): leg_row = leg_model.item(idx) if not leg_row.text() in lyr_names: leg_model.removeRow(idx) leg.adjustBoxSize() for lbl in labels: txt = lbl[1].replace('[Gemeindename]', self.gem_name) txt = txt.replace('[Oberthema]', oberthema) if not subthema is None: txt = txt.replace('[Subthema]', subthema) txt = txt.replace('[GNR]', ', '.join(self.gnrs)) if not gnrflaeche is None: txt = txt.replace('[GNRFLAECHE]', gnrflaeche) txt = txt.replace('[TODAY]', strftime("%d.%m.%Y")) txt = txt.replace('[DATE]', self.settings.dkm_stand()) lbl[0].setText(txt) #self.composermap.updateItem() #self.composermap.updateCachedImage() #self.composermap.mapRenderer().updateFullExtent () def __get_items(self, typ, composition=None): if composition is None: composition = self.composition items = [] for item in composition.items(): if isinstance(item, typ): #if label keep original text for placeholders if isinstance(item, QgsComposerLabel): items.append([item, item.text()]) else: items.append(item) return items def __get_item_byid(self, composition, item_id): return composition.getComposerItemById(item_id) def __reorder_layers(self): #move ortho to bottom if not self.ortho_lyr is None: for idx in range(0, self.toc.topLevelItemCount()): if self.toc.topLevelItem(idx).text(0) == self.lyrname_ortho: if VRP_DEBUG is True: QgsMessageLog.logMessage(u'idx {0}:{1}'.format(self.lyrname_ortho, idx), DLG_CAPTION) item = self.toc.takeTopLevelItem(idx) if VRP_DEBUG is True: QgsMessageLog.logMessage(u'topLevelItemCount:{0}'.format(self.toc.topLevelItemCount()), DLG_CAPTION) self.toc.insertTopLevelItem(self.toc.topLevelItemCount(), item) #legiface.refreshLayerSymbology(self.ortho_lyr) #self.canvas.setDirty(True) #self.canvas.refresh() #!!!!HACK TO REFRESH DRAWING ORDER #With Python there is no possibility to change oder of layer #in legend (=TOC) #http://gis.stackexchange.com/a/42007 #Currently, using Python, there is limited functionality #for manipulating the QgsLegend. There is the QgsLegendInterface #but this does not have all the goodies that are present #in the QgsLegend, QgsLegendLayer, the inherited QgsLegendItem, #or any of the other classes associated with QgsLegend. self.legiface.setLayerVisible(self.ortho_lyr, False) self.legiface.setLayerVisible(self.ortho_lyr, True) if VRP_DEBUG is True: tmp = [lyr.name() for lyr in self.canvas.layers()] QgsMessageLog.logMessage(u'layers:{0}'.format(tmp), DLG_CAPTION) break #o = self.toc.findItems(self.lyrname_ortho, Qt.MatchExactly)[0] #QgsMessageLog.logMessage('toc:{0}'.format(dir(o)), DLG_CAPTION) #toc.sortItems(0, Qt.AscendingOrder) #move dkm to top lyr_dkm_gst = self.__get_layer(self.lyrname_dkm_gst) lyr_dkm_gnr = self.__get_layer(self.lyrname_dkm_gnr) if lyr_dkm_gst is None or lyr_dkm_gnr is None: return #gst to top for idx in range(0, self.toc.topLevelItemCount()): if self.toc.topLevelItem(idx).text(0) == self.lyrname_dkm_gst: item = self.toc.takeTopLevelItem(idx) self.toc.insertTopLevelItem(0, item) self.legiface.setLayerVisible(lyr_dkm_gst, False) self.legiface.setLayerVisible(lyr_dkm_gst, True) break #move gnr to top for idx in range(0, self.toc.topLevelItemCount()): if self.toc.topLevelItem(idx).text(0) == self.lyrname_dkm_gnr: item = self.toc.takeTopLevelItem(idx) self.toc.insertTopLevelItem(0, item) self.legiface.setLayerVisible(lyr_dkm_gnr, False) self.legiface.setLayerVisible(lyr_dkm_gnr, True) break def __get_layer(self, lyrname): for lyr in self.legiface.layers(): if lyr.name() == lyrname: return lyr return None def __add_raster_layer(self, rasterfile, legend_name=None): try: if VRP_DEBUG is True: QgsMessageLog.logMessage('export pdf (__add_raster_layer): {0}'.format(rasterfile), DLG_CAPTION) if legend_name is None: fileinfo = QFileInfo(rasterfile) basename = fileinfo.baseName() else: basename = legend_name lyr = QgsRasterLayer(rasterfile, basename) if not lyr.isValid(): QgsMessageLog.logMessage( u'Raster [{0}] konnte nicht geladen werden!'.format(rasterfile), DLG_CAPTION) return None QgsMapLayerRegistry.instance().addMapLayer(lyr) return lyr except: msg = 'export pdf (__add_raster_layer): {0}'.format(traceback.format_exc()) QgsMessageLog.logMessage(msg, DLG_CAPTION) return None def __add_layers(self, thema): try: layers = [] for quelle in thema.quellen: pfad = quelle.pfad.replace('{gem_name}', self.gem_name) qml = None if not quelle.qml is None: qml = quelle.qml.replace('{gem_name}', self.gem_name) if VRP_DEBUG is True: QgsMessageLog.logMessage('adding lyr:\n{0}\n{1}'.format(pfad, qml), DLG_CAPTION) if pfad.lower().endswith('.shp') is True: lyr = QgsVectorLayer(pfad, quelle.name, 'ogr') if not quelle.filter is None: if VRP_DEBUG is True: QgsMessageLog.logMessage('{0}'.format(quelle.filter), DLG_CAPTION) #exp = QgsExpression(quelle.filter) #if exp.hasParserError(): # QgsMessageLog.logMessage( u'Filter ungültig!\nQuelle:[{0}]\nFilter:{1}'.format(quelle.name, quelle.filter), DLG_CAPTION) #else: # exp.prepare(lyr.pendingFields()) lyr.setSubsetString(quelle.filter) else: fileinfo = QFileInfo(pfad) basename = fileinfo.baseName() lyr = QgsRasterLayer(pfad, basename) if not lyr.isValid(): QgsMessageLog.logMessage( u'Raster [{0}] konnte nicht geladen werden:\n{1}'.format(thema.name, pfad), DLG_CAPTION) continue if not qml is None: lyr.loadNamedStyle(qml) QgsMapLayerRegistry.instance().addMapLayer(lyr) #turn off layer, if no qml present #for layer that should not be displayed but should be #used for statistics if qml is None: self.legiface.setLayerVisible(lyr, False) layers.append(lyr) return layers except: msg = 'export pdf (__add_layers): {0}'.format(sys.exc_info()[0]) QgsMessageLog.logMessage(msg, DLG_CAPTION) return None def __delete_pdf(self): if os.path.isfile(self.pdf_map): try: os.remove(self.pdf_map) except: if VRP_DEBUG is True: QgsMessageLog.logMessage(u'delete error: {0}'.format(self.pdf_map), DLG_CAPTION) return u'Konnte Ausgabedatei nicht ĺöschen!\n{0}'.format(self.pdf_map) return None def __read_template(self, textinfo=False): if textinfo: filename = self.settings.textinfo_layout() else: filename = self.template_qpt if VRP_DEBUG is True: QgsMessageLog.logMessage(u'reading template: {0}'.format(filename), DLG_CAPTION) xml_file = QFile(filename) #if xml_file.exists() is False: # return u'\nTemplate ist nicht vorhanden!\n\n{0}'.format(self.template_qpt), None if xml_file.open(QIODevice.ReadOnly) is False: return u'\nKonnte Template nicht öffnen!\n\n{0}\n\n{1}: {2}'.format(filename, xml_file.error(), xml_file.errorString()), None xml_doc = QDomDocument('mydoc') xml_doc.setContent(xml_file) return None, xml_doc