class TestQgsComposerMap(TestCase): def __init__(self, methodName): """Run once on class initialisation.""" unittest.TestCase.__init__(self, methodName) myPath = os.path.join(TEST_DATA_DIR, 'landsat.tif') rasterFileInfo = QFileInfo(myPath) mRasterLayer = QgsRasterLayer(rasterFileInfo.filePath(), rasterFileInfo.completeBaseName()) rasterRenderer = QgsMultiBandColorRenderer( mRasterLayer.dataProvider(), 2, 3, 4) mRasterLayer.setRenderer(rasterRenderer) #pipe = mRasterLayer.pipe() #assert pipe.set(rasterRenderer), 'Cannot set pipe renderer' QgsMapLayerRegistry.instance().addMapLayers([mRasterLayer]) # create composition with composer map self.mMapRenderer = QgsMapRenderer() layerStringList = QStringList() layerStringList.append(mRasterLayer.id()) self.mMapRenderer.setLayerSet(layerStringList) self.mMapRenderer.setProjectionsEnabled(False) self.mComposition = QgsComposition(self.mMapRenderer) self.mComposition.setPaperSize(297, 210) self.mComposerMap = QgsComposerMap(self.mComposition, 20, 20, 200, 100) self.mComposerMap.setFrameEnabled(True) self.mComposition.addComposerMap(self.mComposerMap) def testGrid(self): """Test that we can create a grid for a map.""" myRectangle = QgsRectangle(781662.375, 3339523.125, 793062.375, 3345223.125) self.mComposerMap.setNewExtent(myRectangle) self.mComposerMap.setGridEnabled(True) self.mComposerMap.setGridIntervalX(2000) self.mComposerMap.setGridIntervalY(2000) self.mComposerMap.setShowGridAnnotation(True) self.mComposerMap.setGridPenWidth(0.5) self.mComposerMap.setGridAnnotationPrecision(0) self.mComposerMap.setGridAnnotationPosition(QgsComposerMap.Disabled, QgsComposerMap.Left) self.mComposerMap.setGridAnnotationPosition( QgsComposerMap.OutsideMapFrame, QgsComposerMap.Right) self.mComposerMap.setGridAnnotationPosition(QgsComposerMap.Disabled, QgsComposerMap.Top) self.mComposerMap.setGridAnnotationPosition( QgsComposerMap.OutsideMapFrame, QgsComposerMap.Bottom) self.mComposerMap.setGridAnnotationDirection(QgsComposerMap.Horizontal, QgsComposerMap.Right) self.mComposerMap.setGridAnnotationDirection(QgsComposerMap.Horizontal, QgsComposerMap.Bottom) checker = QgsCompositionChecker() myPath = os.path.join(TEST_DATA_DIR, 'control_images', 'expected_composermap', 'composermap_landsat_grid.png') myTestResult, myMessage = checker.testComposition('Composer map grid', self.mComposition, myPath) self.mComposerMap.setGridEnabled(False) self.mComposerMap.setShowGridAnnotation(False) assert myTestResult == True, myMessage def testOverviewMap(self): overviewMap = QgsComposerMap(self.mComposition, 20, 130, 70, 70) overviewMap.setFrameEnabled(True) self.mComposition.addComposerMap(overviewMap) # zoom in myRectangle = QgsRectangle(785462.375, 3341423.125, 789262.375, 3343323.125) self.mComposerMap.setNewExtent(myRectangle) myRectangle2 = QgsRectangle(781662.375, 3339523.125, 793062.375, 3350923.125) overviewMap.setNewExtent(myRectangle2) overviewMap.setOverviewFrameMap(self.mComposerMap.id()) checker = QgsCompositionChecker() myPngPath = os.path.join(TEST_DATA_DIR, 'control_images', 'expected_composermap', 'composermap_landsat_overview.png') myTestResult, myMessage = checker.testComposition( 'Composer map overview', self.mComposition, myPngPath) self.mComposition.removeComposerItem(overviewMap) assert myTestResult == True, myMessage def testOverviewMapBlend(self): overviewMap = QgsComposerMap(self.mComposition, 20, 130, 70, 70) overviewMap.setFrameEnabled(True) self.mComposition.addComposerMap(overviewMap) # zoom in myRectangle = QgsRectangle(785462.375, 3341423.125, 789262.375, 3343323.125) self.mComposerMap.setNewExtent(myRectangle) myRectangle2 = QgsRectangle(781662.375, 3339523.125, 793062.375, 3350923.125) overviewMap.setNewExtent(myRectangle2) overviewMap.setOverviewFrameMap(self.mComposerMap.id()) overviewMap.setOverviewBlendMode(QPainter.CompositionMode_Multiply) checker = QgsCompositionChecker() myPngPath = os.path.join(TEST_DATA_DIR, 'control_images', 'expected_composermap', 'composermap_landsat_overview_blend.png') myTestResult, myMessage = checker.testComposition( 'Composer map overview blending', self.mComposition, myPngPath) self.mComposition.removeComposerItem(overviewMap) assert myTestResult == True, myMessage def testOverviewMapInvert(self): overviewMap = QgsComposerMap(self.mComposition, 20, 130, 70, 70) overviewMap.setFrameEnabled(True) self.mComposition.addComposerMap(overviewMap) # zoom in myRectangle = QgsRectangle(785462.375, 3341423.125, 789262.375, 3343323.125) self.mComposerMap.setNewExtent(myRectangle) myRectangle2 = QgsRectangle(781662.375, 3339523.125, 793062.375, 3350923.125) overviewMap.setNewExtent(myRectangle2) overviewMap.setOverviewFrameMap(self.mComposerMap.id()) overviewMap.setOverviewInverted(True) checker = QgsCompositionChecker() myPngPath = os.path.join(TEST_DATA_DIR, 'control_images', 'expected_composermap', 'composermap_landsat_overview_invert.png') myTestResult, myMessage = checker.testComposition( 'Composer map overview inverted', self.mComposition, myPngPath) self.mComposition.removeComposerItem(overviewMap) assert myTestResult == True, myMessage # Fails because addItemsFromXML has been commented out in sip @expectedFailure def testuniqueId(self): doc = QDomDocument() documentElement = doc.createElement('ComposerItemClipboard') self.mComposition.writeXML(documentElement, doc) self.mComposition.addItemsFromXML(documentElement, doc, 0, False) #test if both composer maps have different ids newMap = QgsComposerMap() mapList = self.mComposition.composerMapItems() for mapIt in mapList: if mapIt != self.mComposerMap: newMap = mapIt break oldId = self.mComposerMap.id() newId = newMap.id() self.mComposition.removeComposerItem(newMap) myMessage = 'old: %s new: %s' % (oldId, newId) assert oldId != newId, myMessage def testZebraStyle(self): self.mComposerMap.setGridFrameStyle(QgsComposerMap.Zebra) myRectangle = QgsRectangle(785462.375, 3341423.125, 789262.375, 3343323.125) self.mComposerMap.setNewExtent( myRectangle ) self.mComposerMap.setGridEnabled(True) self.mComposerMap.setGridIntervalX(2000) self.mComposerMap.setGridIntervalY(2000) checker = QgsCompositionChecker() myPngPath = os.path.join(TEST_DATA_DIR, 'control_images', 'expected_composermap', 'composermap_zebra_style.png') testResult, myMessage = checker.testComposition('Composer map zebra', self.mComposition, myPngPath) assert testResult == True, myMessage
class TestQgsComposerMap(TestCase): def __init__(self, methodName): """Run once on class initialisation.""" unittest.TestCase.__init__(self, methodName) myPath = os.path.join(TEST_DATA_DIR, 'landsat.tif') rasterFileInfo = QFileInfo(myPath) mRasterLayer = QgsRasterLayer(rasterFileInfo.filePath(), rasterFileInfo.completeBaseName()) rasterRenderer = QgsMultiBandColorRenderer(mRasterLayer.dataProvider(), 2, 3, 4) mRasterLayer.setRenderer(rasterRenderer) #pipe = mRasterLayer.pipe() #assert pipe.set(rasterRenderer), 'Cannot set pipe renderer' QgsMapLayerRegistry.instance().addMapLayers([mRasterLayer]) # create composition with composer map self.mMapRenderer = QgsMapRenderer() layerStringList = [] layerStringList.append(mRasterLayer.id()) self.mMapRenderer.setLayerSet(layerStringList) self.mMapRenderer.setProjectionsEnabled(False) self.mComposition = QgsComposition(self.mMapRenderer) self.mComposition.setPaperSize(297, 210) self.mComposerMap = QgsComposerMap(self.mComposition, 20, 20, 200, 100) self.mComposerMap.setFrameEnabled(True) self.mComposition.addComposerMap(self.mComposerMap) def testGrid(self): """Test that we can create a grid for a map.""" myRectangle = QgsRectangle(781662.375, 3339523.125, 793062.375, 3345223.125) self.mComposerMap.setNewExtent(myRectangle) self.mComposerMap.setGridEnabled(True) self.mComposerMap.setGridIntervalX(2000) self.mComposerMap.setGridIntervalY(2000) self.mComposerMap.setShowGridAnnotation(True) self.mComposerMap.setGridPenWidth(0.5) self.mComposerMap.setGridPenColor(QColor(0, 255, 0)) self.mComposerMap.setGridAnnotationPrecision(0) self.mComposerMap.setGridAnnotationPosition(QgsComposerMap.Disabled, QgsComposerMap.Left) self.mComposerMap.setGridAnnotationPosition( QgsComposerMap.OutsideMapFrame, QgsComposerMap.Right) self.mComposerMap.setGridAnnotationPosition(QgsComposerMap.Disabled, QgsComposerMap.Top) self.mComposerMap.setGridAnnotationPosition( QgsComposerMap.OutsideMapFrame, QgsComposerMap.Bottom) self.mComposerMap.setGridAnnotationDirection(QgsComposerMap.Horizontal, QgsComposerMap.Right) self.mComposerMap.setGridAnnotationDirection(QgsComposerMap.Horizontal, QgsComposerMap.Bottom) self.mComposerMap.setAnnotationFontColor(QColor(255, 0, 0, 150)) self.mComposerMap.setGridBlendMode(QPainter.CompositionMode_Overlay) checker = QgsCompositionChecker('composermap_grid', self.mComposition) myTestResult, myMessage = checker.testComposition() self.mComposerMap.setGridEnabled(False) self.mComposerMap.setShowGridAnnotation(False) assert myTestResult == True, myMessage def testOverviewMap(self): overviewMap = QgsComposerMap(self.mComposition, 20, 130, 70, 70) overviewMap.setFrameEnabled(True) self.mComposition.addComposerMap(overviewMap) # zoom in myRectangle = QgsRectangle(785462.375, 3341423.125, 789262.375, 3343323.125) self.mComposerMap.setNewExtent(myRectangle) myRectangle2 = QgsRectangle(781662.375, 3339523.125, 793062.375, 3350923.125) overviewMap.setNewExtent(myRectangle2) overviewMap.setOverviewFrameMap(self.mComposerMap.id()) checker = QgsCompositionChecker('composermap_overview', self.mComposition) myTestResult, myMessage = checker.testComposition() self.mComposition.removeComposerItem(overviewMap) assert myTestResult == True, myMessage def testOverviewMapBlend(self): overviewMap = QgsComposerMap(self.mComposition, 20, 130, 70, 70) overviewMap.setFrameEnabled(True) self.mComposition.addComposerMap(overviewMap) # zoom in myRectangle = QgsRectangle(785462.375, 3341423.125, 789262.375, 3343323.125) self.mComposerMap.setNewExtent(myRectangle) myRectangle2 = QgsRectangle(781662.375, 3339523.125, 793062.375, 3350923.125) overviewMap.setNewExtent(myRectangle2) overviewMap.setOverviewFrameMap(self.mComposerMap.id()) overviewMap.setOverviewBlendMode(QPainter.CompositionMode_Multiply) checker = QgsCompositionChecker('composermap_overview_blending', self.mComposition) myTestResult, myMessage = checker.testComposition() self.mComposition.removeComposerItem(overviewMap) assert myTestResult == True, myMessage def testOverviewMapInvert(self): overviewMap = QgsComposerMap(self.mComposition, 20, 130, 70, 70) overviewMap.setFrameEnabled(True) self.mComposition.addComposerMap(overviewMap) # zoom in myRectangle = QgsRectangle(785462.375, 3341423.125, 789262.375, 3343323.125) self.mComposerMap.setNewExtent(myRectangle) myRectangle2 = QgsRectangle(781662.375, 3339523.125, 793062.375, 3350923.125) overviewMap.setNewExtent(myRectangle2) overviewMap.setOverviewFrameMap(self.mComposerMap.id()) overviewMap.setOverviewInverted(True) checker = QgsCompositionChecker('composermap_overview_invert', self.mComposition) myTestResult, myMessage = checker.testComposition() self.mComposition.removeComposerItem(overviewMap) assert myTestResult == True, myMessage def testOverviewMapCenter(self): overviewMap = QgsComposerMap(self.mComposition, 20, 130, 70, 70) overviewMap.setFrameEnabled(True) self.mComposition.addComposerMap(overviewMap) # zoom in myRectangle = QgsRectangle(785462.375 + 5000, 3341423.125, 789262.375 + 5000, 3343323.125) self.mComposerMap.setNewExtent(myRectangle) myRectangle2 = QgsRectangle(781662.375, 3339523.125, 793062.375, 3350923.125) overviewMap.setNewExtent(myRectangle2) overviewMap.setOverviewFrameMap(self.mComposerMap.id()) overviewMap.setOverviewInverted(False) overviewMap.setOverviewCentered(True) checker = QgsCompositionChecker('composermap_overview_center', self.mComposition) myTestResult, myMessage = checker.testComposition() self.mComposition.removeComposerItem(overviewMap) assert myTestResult == True, myMessage # Fails because addItemsFromXML has been commented out in sip @expectedFailure def testuniqueId(self): doc = QDomDocument() documentElement = doc.createElement('ComposerItemClipboard') self.mComposition.writeXML(documentElement, doc) self.mComposition.addItemsFromXML(documentElement, doc, 0, False) #test if both composer maps have different ids newMap = QgsComposerMap() mapList = self.mComposition.composerMapItems() for mapIt in mapList: if mapIt != self.mComposerMap: newMap = mapIt break oldId = self.mComposerMap.id() newId = newMap.id() self.mComposition.removeComposerItem(newMap) myMessage = 'old: %s new: %s' % (oldId, newId) assert oldId != newId, myMessage def testZebraStyle(self): self.mComposerMap.setGridFrameStyle(QgsComposerMap.Zebra) myRectangle = QgsRectangle(785462.375, 3341423.125, 789262.375, 3343323.125) self.mComposerMap.setNewExtent(myRectangle) self.mComposerMap.setGridEnabled(True) self.mComposerMap.setGridIntervalX(2000) self.mComposerMap.setGridIntervalY(2000) self.mComposerMap.setGridFrameWidth(10) self.mComposerMap.setGridFramePenSize(1) self.mComposerMap.setGridPenWidth(0.5) self.mComposerMap.setGridFramePenColor(QColor(255, 100, 0, 200)) self.mComposerMap.setGridFrameFillColor1(QColor(50, 90, 50, 100)) self.mComposerMap.setGridFrameFillColor2(QColor(200, 220, 100, 60)) checker = QgsCompositionChecker('composermap_zebrastyle', self.mComposition) myTestResult, myMessage = checker.testComposition() assert myTestResult == True, myMessage def testWorldFileGeneration(self): myRectangle = QgsRectangle(781662.375, 3339523.125, 793062.375, 3345223.125) self.mComposerMap.setNewExtent(myRectangle) self.mComposerMap.setMapRotation(30.0) self.mComposition.setGenerateWorldFile(True) self.mComposition.setWorldFileMap(self.mComposerMap) p = self.mComposition.computeWorldFileParameters() pexpected = (4.180480199790922, 2.4133064516129026, 779443.7612381146, 2.4136013686911886, -4.179969388427311, 3342408.5663611) ptolerance = (0.001, 0.001, 1, 0.001, 0.001, 1e+03) for i in range(0, 6): assert abs(p[i] - pexpected[i]) < ptolerance[i]
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)
class TestQgsComposerMap(unittest.TestCase): def __init__(self, methodName): """Run once on class initialisation.""" unittest.TestCase.__init__(self, methodName) myPath = os.path.join(TEST_DATA_DIR, 'rgb256x256.png') rasterFileInfo = QFileInfo(myPath) mRasterLayer = QgsRasterLayer(rasterFileInfo.filePath(), rasterFileInfo.completeBaseName()) rasterRenderer = QgsMultiBandColorRenderer(mRasterLayer.dataProvider(), 1, 2, 3) mRasterLayer.setRenderer(rasterRenderer) #pipe = mRasterLayer.pipe() #assert pipe.set(rasterRenderer), 'Cannot set pipe renderer' QgsMapLayerRegistry.instance().addMapLayers([mRasterLayer]) # create composition with composer map self.mMapRenderer = QgsMapRenderer() layerStringList = [] layerStringList.append(mRasterLayer.id()) self.mMapRenderer.setLayerSet(layerStringList) self.mMapRenderer.setProjectionsEnabled(False) self.mComposition = QgsComposition(self.mMapRenderer) self.mComposition.setPaperSize(297, 210) self.mComposerMap = QgsComposerMap(self.mComposition, 20, 20, 200, 100) self.mComposerMap.setFrameEnabled(True) self.mComposition.addComposerMap(self.mComposerMap) def testOverviewMap(self): overviewMap = QgsComposerMap(self.mComposition, 20, 130, 70, 70) overviewMap.setFrameEnabled(True) self.mComposition.addComposerMap(overviewMap) # zoom in myRectangle = QgsRectangle(96, -152, 160, -120) self.mComposerMap.setNewExtent(myRectangle) myRectangle2 = QgsRectangle(0, -256, 256, 0) overviewMap.setNewExtent(myRectangle2) overviewMap.setOverviewFrameMap(self.mComposerMap.id()) checker = QgsCompositionChecker('composermap_overview', self.mComposition) checker.setControlPathPrefix("composer_mapoverview") myTestResult, myMessage = checker.testComposition() self.mComposition.removeComposerItem(overviewMap) assert myTestResult, myMessage def testOverviewMapBlend(self): overviewMap = QgsComposerMap(self.mComposition, 20, 130, 70, 70) overviewMap.setFrameEnabled(True) self.mComposition.addComposerMap(overviewMap) # zoom in myRectangle = QgsRectangle(96, -152, 160, -120) self.mComposerMap.setNewExtent(myRectangle) myRectangle2 = QgsRectangle(0, -256, 256, 0) overviewMap.setNewExtent(myRectangle2) overviewMap.setOverviewFrameMap(self.mComposerMap.id()) overviewMap.setOverviewBlendMode(QPainter.CompositionMode_Multiply) checker = QgsCompositionChecker('composermap_overview_blending', self.mComposition) checker.setControlPathPrefix("composer_mapoverview") myTestResult, myMessage = checker.testComposition() self.mComposition.removeComposerItem(overviewMap) assert myTestResult, myMessage def testOverviewMapInvert(self): overviewMap = QgsComposerMap(self.mComposition, 20, 130, 70, 70) overviewMap.setFrameEnabled(True) self.mComposition.addComposerMap(overviewMap) # zoom in myRectangle = QgsRectangle(96, -152, 160, -120) self.mComposerMap.setNewExtent(myRectangle) myRectangle2 = QgsRectangle(0, -256, 256, 0) overviewMap.setNewExtent(myRectangle2) overviewMap.setOverviewFrameMap(self.mComposerMap.id()) overviewMap.setOverviewInverted(True) checker = QgsCompositionChecker('composermap_overview_invert', self.mComposition) checker.setControlPathPrefix("composer_mapoverview") myTestResult, myMessage = checker.testComposition() self.mComposition.removeComposerItem(overviewMap) assert myTestResult, myMessage def testOverviewMapCenter(self): overviewMap = QgsComposerMap(self.mComposition, 20, 130, 70, 70) overviewMap.setFrameEnabled(True) self.mComposition.addComposerMap(overviewMap) # zoom in myRectangle = QgsRectangle(192, -288, 320, -224) self.mComposerMap.setNewExtent(myRectangle) myRectangle2 = QgsRectangle(0, -256, 256, 0) overviewMap.setNewExtent(myRectangle2) overviewMap.setOverviewFrameMap(self.mComposerMap.id()) overviewMap.setOverviewInverted(False) overviewMap.setOverviewCentered(True) checker = QgsCompositionChecker('composermap_overview_center', self.mComposition) checker.setControlPathPrefix("composer_mapoverview") myTestResult, myMessage = checker.testComposition() self.mComposition.removeComposerItem(overviewMap) assert myTestResult, myMessage # Fails because addItemsFromXML has been commented out in sip @unittest.expectedFailure def testuniqueId(self): doc = QDomDocument() documentElement = doc.createElement('ComposerItemClipboard') self.mComposition.writeXML(documentElement, doc) self.mComposition.addItemsFromXML(documentElement, doc, 0, False) # test if both composer maps have different ids newMap = QgsComposerMap() mapList = self.mComposition.composerMapItems() for mapIt in mapList: if mapIt != self.mComposerMap: newMap = mapIt break oldId = self.mComposerMap.id() newId = newMap.id() self.mComposition.removeComposerItem(newMap) myMessage = 'old: %s new: %s' % (oldId, newId) assert oldId != newId, myMessage def testWorldFileGeneration(self): myRectangle = QgsRectangle(781662.375, 3339523.125, 793062.375, 3345223.125) self.mComposerMap.setNewExtent(myRectangle) self.mComposerMap.setMapRotation(30.0) self.mComposition.setGenerateWorldFile(True) self.mComposition.setWorldFileMap(self.mComposerMap) p = self.mComposition.computeWorldFileParameters() pexpected = (4.180480199790922, 2.4133064516129026, 779443.7612381146, 2.4136013686911886, -4.179969388427311, 3342408.5663611) ptolerance = (0.001, 0.001, 1, 0.001, 0.001, 1e+03) for i in range(0, 6): assert abs(p[i] - pexpected[i]) < ptolerance[i]
class TestQgsComposerMap(unittest.TestCase): def __init__(self, methodName): """Run once on class initialization.""" unittest.TestCase.__init__(self, methodName) myPath = os.path.join(TEST_DATA_DIR, 'rgb256x256.png') rasterFileInfo = QFileInfo(myPath) mRasterLayer = QgsRasterLayer(rasterFileInfo.filePath(), rasterFileInfo.completeBaseName()) rasterRenderer = QgsMultiBandColorRenderer( mRasterLayer.dataProvider(), 1, 2, 3) mRasterLayer.setRenderer(rasterRenderer) #pipe = mRasterLayer.pipe() #assert pipe.set(rasterRenderer), 'Cannot set pipe renderer' QgsMapLayerRegistry.instance().addMapLayers([mRasterLayer]) # create composition with composer map self.mMapRenderer = QgsMapRenderer() layerStringList = [] layerStringList.append(mRasterLayer.id()) self.mMapRenderer.setLayerSet(layerStringList) self.mMapRenderer.setProjectionsEnabled(False) self.mComposition = QgsComposition(self.mMapRenderer) self.mComposition.setPaperSize(297, 210) self.mComposerMap = QgsComposerMap(self.mComposition, 20, 20, 200, 100) self.mComposerMap.setFrameEnabled(True) self.mComposition.addComposerMap(self.mComposerMap) def testOverviewMap(self): overviewMap = QgsComposerMap(self.mComposition, 20, 130, 70, 70) overviewMap.setFrameEnabled(True) self.mComposition.addComposerMap(overviewMap) # zoom in myRectangle = QgsRectangle(96, -152, 160, -120) self.mComposerMap.setNewExtent(myRectangle) myRectangle2 = QgsRectangle(0, -256, 256, 0) overviewMap.setNewExtent(myRectangle2) overviewMap.setOverviewFrameMap(self.mComposerMap.id()) checker = QgsCompositionChecker('composermap_overview', self.mComposition) checker.setControlPathPrefix("composer_mapoverview") myTestResult, myMessage = checker.testComposition() self.mComposition.removeComposerItem(overviewMap) assert myTestResult, myMessage def testOverviewMapBlend(self): overviewMap = QgsComposerMap(self.mComposition, 20, 130, 70, 70) overviewMap.setFrameEnabled(True) self.mComposition.addComposerMap(overviewMap) # zoom in myRectangle = QgsRectangle(96, -152, 160, -120) self.mComposerMap.setNewExtent(myRectangle) myRectangle2 = QgsRectangle(0, -256, 256, 0) overviewMap.setNewExtent(myRectangle2) overviewMap.setOverviewFrameMap(self.mComposerMap.id()) overviewMap.setOverviewBlendMode(QPainter.CompositionMode_Multiply) checker = QgsCompositionChecker('composermap_overview_blending', self.mComposition) checker.setControlPathPrefix("composer_mapoverview") myTestResult, myMessage = checker.testComposition() self.mComposition.removeComposerItem(overviewMap) assert myTestResult, myMessage def testOverviewMapInvert(self): overviewMap = QgsComposerMap(self.mComposition, 20, 130, 70, 70) overviewMap.setFrameEnabled(True) self.mComposition.addComposerMap(overviewMap) # zoom in myRectangle = QgsRectangle(96, -152, 160, -120) self.mComposerMap.setNewExtent(myRectangle) myRectangle2 = QgsRectangle(0, -256, 256, 0) overviewMap.setNewExtent(myRectangle2) overviewMap.setOverviewFrameMap(self.mComposerMap.id()) overviewMap.setOverviewInverted(True) checker = QgsCompositionChecker('composermap_overview_invert', self.mComposition) checker.setControlPathPrefix("composer_mapoverview") myTestResult, myMessage = checker.testComposition() self.mComposition.removeComposerItem(overviewMap) assert myTestResult, myMessage def testOverviewMapCenter(self): overviewMap = QgsComposerMap(self.mComposition, 20, 130, 70, 70) overviewMap.setFrameEnabled(True) self.mComposition.addComposerMap(overviewMap) # zoom in myRectangle = QgsRectangle(192, -288, 320, -224) self.mComposerMap.setNewExtent(myRectangle) myRectangle2 = QgsRectangle(0, -256, 256, 0) overviewMap.setNewExtent(myRectangle2) overviewMap.setOverviewFrameMap(self.mComposerMap.id()) overviewMap.setOverviewInverted(False) overviewMap.setOverviewCentered(True) checker = QgsCompositionChecker('composermap_overview_center', self.mComposition) checker.setControlPathPrefix("composer_mapoverview") myTestResult, myMessage = checker.testComposition() self.mComposition.removeComposerItem(overviewMap) assert myTestResult, myMessage # Fails because addItemsFromXML has been commented out in sip @unittest.expectedFailure def testuniqueId(self): doc = QDomDocument() documentElement = doc.createElement('ComposerItemClipboard') self.mComposition.writeXML(documentElement, doc) self.mComposition.addItemsFromXML(documentElement, doc, 0, False) # test if both composer maps have different ids newMap = QgsComposerMap() mapList = self.mComposition.composerMapItems() for mapIt in mapList: if mapIt != self.mComposerMap: newMap = mapIt break oldId = self.mComposerMap.id() newId = newMap.id() self.mComposition.removeComposerItem(newMap) myMessage = 'old: %s new: %s' % (oldId, newId) assert oldId != newId, myMessage def testWorldFileGeneration(self): myRectangle = QgsRectangle(781662.375, 3339523.125, 793062.375, 3345223.125) self.mComposerMap.setNewExtent(myRectangle) self.mComposerMap.setMapRotation(30.0) self.mComposition.setGenerateWorldFile(True) self.mComposition.setWorldFileMap(self.mComposerMap) p = self.mComposition.computeWorldFileParameters() pexpected = (4.180480199790922, 2.4133064516129026, 779443.7612381146, 2.4136013686911886, -4.179969388427311, 3342408.5663611) ptolerance = (0.001, 0.001, 1, 0.001, 0.001, 1e+03) for i in range(0, 6): assert abs(p[i] - pexpected[i]) < ptolerance[i]
class TestQgsComposerMap(TestCase): def __init__(self, methodName): """Run once on class initialisation.""" unittest.TestCase.__init__(self, methodName) myPath = os.path.join(TEST_DATA_DIR, 'landsat.tif') rasterFileInfo = QFileInfo(myPath) mRasterLayer = QgsRasterLayer(rasterFileInfo.filePath(), rasterFileInfo.completeBaseName()) rasterRenderer = QgsMultiBandColorRenderer( mRasterLayer.dataProvider(), 2, 3, 4) mRasterLayer.setRenderer(rasterRenderer) #pipe = mRasterLayer.pipe() #assert pipe.set(rasterRenderer), 'Cannot set pipe renderer' QgsMapLayerRegistry.instance().addMapLayers([mRasterLayer]) # create composition with composer map self.mMapRenderer = QgsMapRenderer() layerStringList = QStringList() layerStringList.append(mRasterLayer.id()) self.mMapRenderer.setLayerSet(layerStringList) self.mMapRenderer.setProjectionsEnabled(False) self.mComposition = QgsComposition(self.mMapRenderer) self.mComposition.setPaperSize(297, 210) self.mComposerMap = QgsComposerMap(self.mComposition, 20, 20, 200, 100) self.mComposerMap.setFrameEnabled(True) self.mComposition.addComposerMap(self.mComposerMap) def testGrid(self): """Test that we can create a grid for a map.""" myRectangle = QgsRectangle(781662.375, 3339523.125, 793062.375, 3345223.125) self.mComposerMap.setNewExtent(myRectangle) self.mComposerMap.setGridEnabled(True) self.mComposerMap.setGridIntervalX(2000) self.mComposerMap.setGridIntervalY(2000) self.mComposerMap.setShowGridAnnotation(True) self.mComposerMap.setGridPenWidth(0.5) self.mComposerMap.setGridPenColor(QColor(0,255,0)) self.mComposerMap.setGridAnnotationPrecision(0) self.mComposerMap.setGridAnnotationPosition(QgsComposerMap.Disabled, QgsComposerMap.Left) self.mComposerMap.setGridAnnotationPosition( QgsComposerMap.OutsideMapFrame, QgsComposerMap.Right) self.mComposerMap.setGridAnnotationPosition(QgsComposerMap.Disabled, QgsComposerMap.Top) self.mComposerMap.setGridAnnotationPosition( QgsComposerMap.OutsideMapFrame, QgsComposerMap.Bottom) self.mComposerMap.setGridAnnotationDirection(QgsComposerMap.Horizontal, QgsComposerMap.Right) self.mComposerMap.setGridAnnotationDirection(QgsComposerMap.Horizontal, QgsComposerMap.Bottom) self.mComposerMap.setAnnotationFontColor(QColor(255,0,0,150)) self.mComposerMap.setGridBlendMode(QPainter.CompositionMode_Overlay) checker = QgsCompositionChecker() myPath = os.path.join(TEST_DATA_DIR, 'control_images', 'expected_composermap', 'composermap_landsat_grid.png') myTestResult, myMessage = checker.testComposition('Composer map grid', self.mComposition, myPath) self.mComposerMap.setGridEnabled(False) self.mComposerMap.setShowGridAnnotation(False) assert myTestResult == True, myMessage def testOverviewMap(self): overviewMap = QgsComposerMap(self.mComposition, 20, 130, 70, 70) overviewMap.setFrameEnabled(True) self.mComposition.addComposerMap(overviewMap) # zoom in myRectangle = QgsRectangle(785462.375, 3341423.125, 789262.375, 3343323.125) self.mComposerMap.setNewExtent(myRectangle) myRectangle2 = QgsRectangle(781662.375, 3339523.125, 793062.375, 3350923.125) overviewMap.setNewExtent(myRectangle2) overviewMap.setOverviewFrameMap(self.mComposerMap.id()) checker = QgsCompositionChecker() myPngPath = os.path.join(TEST_DATA_DIR, 'control_images', 'expected_composermap', 'composermap_landsat_overview.png') myTestResult, myMessage = checker.testComposition( 'Composer map overview', self.mComposition, myPngPath) self.mComposition.removeComposerItem(overviewMap) assert myTestResult == True, myMessage def testOverviewMapBlend(self): overviewMap = QgsComposerMap(self.mComposition, 20, 130, 70, 70) overviewMap.setFrameEnabled(True) self.mComposition.addComposerMap(overviewMap) # zoom in myRectangle = QgsRectangle(785462.375, 3341423.125, 789262.375, 3343323.125) self.mComposerMap.setNewExtent(myRectangle) myRectangle2 = QgsRectangle(781662.375, 3339523.125, 793062.375, 3350923.125) overviewMap.setNewExtent(myRectangle2) overviewMap.setOverviewFrameMap(self.mComposerMap.id()) overviewMap.setOverviewBlendMode(QPainter.CompositionMode_Multiply) checker = QgsCompositionChecker() myPngPath = os.path.join(TEST_DATA_DIR, 'control_images', 'expected_composermap', 'composermap_landsat_overview_blend.png') myTestResult, myMessage = checker.testComposition( 'Composer map overview blending', self.mComposition, myPngPath) self.mComposition.removeComposerItem(overviewMap) assert myTestResult == True, myMessage def testOverviewMapInvert(self): overviewMap = QgsComposerMap(self.mComposition, 20, 130, 70, 70) overviewMap.setFrameEnabled(True) self.mComposition.addComposerMap(overviewMap) # zoom in myRectangle = QgsRectangle(785462.375, 3341423.125, 789262.375, 3343323.125) self.mComposerMap.setNewExtent(myRectangle) myRectangle2 = QgsRectangle(781662.375, 3339523.125, 793062.375, 3350923.125) overviewMap.setNewExtent(myRectangle2) overviewMap.setOverviewFrameMap(self.mComposerMap.id()) overviewMap.setOverviewInverted(True) checker = QgsCompositionChecker() myPngPath = os.path.join(TEST_DATA_DIR, 'control_images', 'expected_composermap', 'composermap_landsat_overview_invert.png') myTestResult, myMessage = checker.testComposition( 'Composer map overview inverted', self.mComposition, myPngPath) self.mComposition.removeComposerItem(overviewMap) assert myTestResult == True, myMessage # Fails because addItemsFromXML has been commented out in sip @expectedFailure def testuniqueId(self): doc = QDomDocument() documentElement = doc.createElement('ComposerItemClipboard') self.mComposition.writeXML(documentElement, doc) self.mComposition.addItemsFromXML(documentElement, doc, 0, False) #test if both composer maps have different ids newMap = QgsComposerMap() mapList = self.mComposition.composerMapItems() for mapIt in mapList: if mapIt != self.mComposerMap: newMap = mapIt break oldId = self.mComposerMap.id() newId = newMap.id() self.mComposition.removeComposerItem(newMap) myMessage = 'old: %s new: %s' % (oldId, newId) assert oldId != newId, myMessage def testZebraStyle(self): self.mComposerMap.setGridFrameStyle(QgsComposerMap.Zebra) myRectangle = QgsRectangle(785462.375, 3341423.125, 789262.375, 3343323.125) self.mComposerMap.setNewExtent( myRectangle ) self.mComposerMap.setGridEnabled(True) self.mComposerMap.setGridIntervalX(2000) self.mComposerMap.setGridIntervalY(2000) checker = QgsCompositionChecker() myPngPath = os.path.join(TEST_DATA_DIR, 'control_images', 'expected_composermap', 'composermap_zebra_style.png') testResult, myMessage = checker.testComposition('Composer map zebra', self.mComposition, myPngPath) assert testResult == True, myMessage
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, 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)
class TestQgsComposerMap(TestCase): def __init__(self, methodName): """Run once on class initialisation.""" unittest.TestCase.__init__(self, methodName) myPath = os.path.join(TEST_DATA_DIR, 'landsat.tif') rasterFileInfo = QFileInfo(myPath) mRasterLayer = QgsRasterLayer(rasterFileInfo.filePath(), rasterFileInfo.completeBaseName()) rasterRenderer = QgsMultiBandColorRenderer( mRasterLayer.dataProvider(), 2, 3, 4) mRasterLayer.setRenderer(rasterRenderer) #pipe = mRasterLayer.pipe() #assert pipe.set(rasterRenderer), 'Cannot set pipe renderer' QgsMapLayerRegistry.instance().addMapLayers([mRasterLayer]) # create composition with composer map self.mMapRenderer = QgsMapRenderer() layerStringList = [] layerStringList.append(mRasterLayer.id()) self.mMapRenderer.setLayerSet(layerStringList) self.mMapRenderer.setProjectionsEnabled(False) self.mComposition = QgsComposition(self.mMapRenderer) self.mComposition.setPaperSize(297, 210) self.mComposerMap = QgsComposerMap(self.mComposition, 20, 20, 200, 100) self.mComposerMap.setFrameEnabled(True) self.mComposition.addComposerMap(self.mComposerMap) def testGrid(self): """Test that we can create a grid for a map.""" myRectangle = QgsRectangle(781662.375, 3339523.125, 793062.375, 3345223.125) self.mComposerMap.setNewExtent(myRectangle) self.mComposerMap.setGridEnabled(True) self.mComposerMap.setGridIntervalX(2000) self.mComposerMap.setGridIntervalY(2000) self.mComposerMap.setShowGridAnnotation(True) self.mComposerMap.setGridPenWidth(0.5) self.mComposerMap.setGridPenColor(QColor(0,255,0)) self.mComposerMap.setGridAnnotationPrecision(0) self.mComposerMap.setGridAnnotationPosition(QgsComposerMap.Disabled, QgsComposerMap.Left) self.mComposerMap.setGridAnnotationPosition( QgsComposerMap.OutsideMapFrame, QgsComposerMap.Right) self.mComposerMap.setGridAnnotationPosition(QgsComposerMap.Disabled, QgsComposerMap.Top) self.mComposerMap.setGridAnnotationPosition( QgsComposerMap.OutsideMapFrame, QgsComposerMap.Bottom) self.mComposerMap.setGridAnnotationDirection(QgsComposerMap.Horizontal, QgsComposerMap.Right) self.mComposerMap.setGridAnnotationDirection(QgsComposerMap.Horizontal, QgsComposerMap.Bottom) self.mComposerMap.setAnnotationFontColor(QColor(255,0,0,150)) self.mComposerMap.setGridBlendMode(QPainter.CompositionMode_Overlay) checker = QgsCompositionChecker('composermap_grid', self.mComposition) myTestResult, myMessage = checker.testComposition() self.mComposerMap.setGridEnabled(False) self.mComposerMap.setShowGridAnnotation(False) assert myTestResult == True, myMessage def testOverviewMap(self): overviewMap = QgsComposerMap(self.mComposition, 20, 130, 70, 70) overviewMap.setFrameEnabled(True) self.mComposition.addComposerMap(overviewMap) # zoom in myRectangle = QgsRectangle(785462.375, 3341423.125, 789262.375, 3343323.125) self.mComposerMap.setNewExtent(myRectangle) myRectangle2 = QgsRectangle(781662.375, 3339523.125, 793062.375, 3350923.125) overviewMap.setNewExtent(myRectangle2) overviewMap.setOverviewFrameMap(self.mComposerMap.id()) checker = QgsCompositionChecker('composermap_overview', self.mComposition) myTestResult, myMessage = checker.testComposition() self.mComposition.removeComposerItem(overviewMap) assert myTestResult == True, myMessage def testOverviewMapBlend(self): overviewMap = QgsComposerMap(self.mComposition, 20, 130, 70, 70) overviewMap.setFrameEnabled(True) self.mComposition.addComposerMap(overviewMap) # zoom in myRectangle = QgsRectangle(785462.375, 3341423.125, 789262.375, 3343323.125) self.mComposerMap.setNewExtent(myRectangle) myRectangle2 = QgsRectangle(781662.375, 3339523.125, 793062.375, 3350923.125) overviewMap.setNewExtent(myRectangle2) overviewMap.setOverviewFrameMap(self.mComposerMap.id()) overviewMap.setOverviewBlendMode(QPainter.CompositionMode_Multiply) checker = QgsCompositionChecker('composermap_overview_blending', self.mComposition) myTestResult, myMessage = checker.testComposition() self.mComposition.removeComposerItem(overviewMap) assert myTestResult == True, myMessage def testOverviewMapInvert(self): overviewMap = QgsComposerMap(self.mComposition, 20, 130, 70, 70) overviewMap.setFrameEnabled(True) self.mComposition.addComposerMap(overviewMap) # zoom in myRectangle = QgsRectangle(785462.375, 3341423.125, 789262.375, 3343323.125) self.mComposerMap.setNewExtent(myRectangle) myRectangle2 = QgsRectangle(781662.375, 3339523.125, 793062.375, 3350923.125) overviewMap.setNewExtent(myRectangle2) overviewMap.setOverviewFrameMap(self.mComposerMap.id()) overviewMap.setOverviewInverted(True) checker = QgsCompositionChecker('composermap_overview_invert', self.mComposition) myTestResult, myMessage = checker.testComposition() self.mComposition.removeComposerItem(overviewMap) assert myTestResult == True, myMessage def testOverviewMapCenter(self): overviewMap = QgsComposerMap(self.mComposition, 20, 130, 70, 70) overviewMap.setFrameEnabled(True) self.mComposition.addComposerMap(overviewMap) # zoom in myRectangle = QgsRectangle(785462.375+5000, 3341423.125, 789262.375+5000, 3343323.125) self.mComposerMap.setNewExtent(myRectangle) myRectangle2 = QgsRectangle(781662.375, 3339523.125, 793062.375, 3350923.125) overviewMap.setNewExtent(myRectangle2) overviewMap.setOverviewFrameMap(self.mComposerMap.id()) overviewMap.setOverviewInverted(False) overviewMap.setOverviewCentered(True) checker = QgsCompositionChecker('composermap_overview_center', self.mComposition) myTestResult, myMessage = checker.testComposition() self.mComposition.removeComposerItem(overviewMap) assert myTestResult == True, myMessage # Fails because addItemsFromXML has been commented out in sip @expectedFailure def testuniqueId(self): doc = QDomDocument() documentElement = doc.createElement('ComposerItemClipboard') self.mComposition.writeXML(documentElement, doc) self.mComposition.addItemsFromXML(documentElement, doc, 0, False) #test if both composer maps have different ids newMap = QgsComposerMap() mapList = self.mComposition.composerMapItems() for mapIt in mapList: if mapIt != self.mComposerMap: newMap = mapIt break oldId = self.mComposerMap.id() newId = newMap.id() self.mComposition.removeComposerItem(newMap) myMessage = 'old: %s new: %s' % (oldId, newId) assert oldId != newId, myMessage def testZebraStyle(self): self.mComposerMap.setGridFrameStyle(QgsComposerMap.Zebra) myRectangle = QgsRectangle(785462.375, 3341423.125, 789262.375, 3343323.125) self.mComposerMap.setNewExtent( myRectangle ) self.mComposerMap.setGridEnabled(True) self.mComposerMap.setGridIntervalX(2000) self.mComposerMap.setGridIntervalY(2000) self.mComposerMap.setGridFrameWidth( 10 ) self.mComposerMap.setGridFramePenSize( 1 ) self.mComposerMap.setGridPenWidth( 0.5 ) self.mComposerMap.setGridFramePenColor( QColor( 255, 100, 0, 200 ) ) self.mComposerMap.setGridFrameFillColor1( QColor( 50, 90, 50, 100 ) ) self.mComposerMap.setGridFrameFillColor2( QColor( 200, 220, 100, 60 ) ) checker = QgsCompositionChecker('composermap_zebrastyle', self.mComposition) myTestResult, myMessage = checker.testComposition() assert myTestResult == True, myMessage def testWorldFileGeneration( self ): myRectangle = QgsRectangle(781662.375, 3339523.125, 793062.375, 3345223.125) self.mComposerMap.setNewExtent( myRectangle ) self.mComposerMap.setMapRotation( 30.0 ) self.mComposition.setGenerateWorldFile( True ) self.mComposition.setWorldFileMap( self.mComposerMap ) p = self.mComposition.computeWorldFileParameters() pexpected = (4.180480199790922, 2.4133064516129026, 779443.7612381146, 2.4136013686911886, -4.179969388427311, 3342408.5663611) ptolerance = (0.001, 0.001, 1, 0.001, 0.001, 1e+03) for i in range(0,6): assert abs(p[i]-pexpected[i]) < ptolerance[i]
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()