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
示例#2
0
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]
示例#3
0
class Map():
    """A class for creating a map."""
    def __init__(self, iface):
        """Constructor for the Map class.

        :param iface: Reference to the QGIS iface object.
        :type iface: QgsAppInterface
        """
        LOGGER.debug('InaSAFE Map class initialised')
        self.iface = iface
        self.layer = iface.activeLayer()
        self.keywordIO = KeywordIO()
        self.printer = None
        self.composition = None
        self.legend = None
        self.pageWidth = 210  # width in mm
        self.pageHeight = 297  # height in mm
        self.pageDpi = 300.0
        self.pageMargin = 10  # margin in mm
        self.verticalSpacing = 1  # vertical spacing between elements
        self.showFramesFlag = False  # intended for debugging use only
        # make a square map where width = height = page width
        self.mapHeight = self.pageWidth - (self.pageMargin * 2)
        self.mapWidth = self.mapHeight
        self.disclaimer = self.tr('InaSAFE has been jointly developed by'
                                  ' BNPB, AusAid & the World Bank')

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

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

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

    def set_impact_layer(self, layer):
        """Set the layer that will be used for stats, legend and reporting.

        :param layer: Layer that will be used for stats, legend and reporting.
        :type layer: QgsMapLayer, QgsRasterLayer, QgsVectorLayer
        """
        self.layer = layer

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

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

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

        :returns: A three-tuple of:
            * str: myImagePath - absolute path to png of rendered map
            * QImage: myImage - in memory copy of rendered map
            * QRectF: myTargetArea - dimensions of rendered map
        :rtype: tuple
        """
        LOGGER.debug('InaSAFE Map renderComposition called')
        # NOTE: we ignore self.composition.printAsRaster() and always rasterise
        myWidth = int(self.pageDpi * self.pageWidth / 25.4)
        myHeight = int(self.pageDpi * self.pageHeight / 25.4)
        myImage = QtGui.QImage(QtCore.QSize(myWidth, myHeight),
                               QtGui.QImage.Format_ARGB32)
        myImage.setDotsPerMeterX(dpi_to_meters(self.pageDpi))
        myImage.setDotsPerMeterY(dpi_to_meters(self.pageDpi))

        # Only works in Qt4.8
        #myImage.fill(QtGui.qRgb(255, 255, 255))
        # Works in older Qt4 versions
        myImage.fill(55 + 255 * 256 + 255 * 256 * 256)
        myImagePainter = QtGui.QPainter(myImage)
        mySourceArea = QtCore.QRectF(0, 0, self.pageWidth,
                                     self.pageHeight)
        myTargetArea = QtCore.QRectF(0, 0, myWidth, myHeight)
        self.composition.render(myImagePainter, myTargetArea, mySourceArea)
        myImagePainter.end()
        myImagePath = unique_filename(prefix='mapRender_',
                                      suffix='.png',
                                      dir=temp_dir())
        myImage.save(myImagePath)
        return myImagePath, myImage, myTargetArea

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

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

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

        self.compose_map()
        self.printer = setup_printer(myMapPdfPath)
        _, myImage, myRectangle = self.render()
        myPainter = QtGui.QPainter(self.printer)
        myPainter.drawImage(myRectangle, myImage, myRectangle)
        myPainter.end()
        return myMapPdfPath

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

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

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

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

        :returns: The height of the label as rendered.
        :rtype: float
        """
        LOGGER.debug('InaSAFE Map drawTitle called')
        myFontSize = 14
        myFontWeight = QtGui.QFont.Bold
        myItalicsFlag = False
        myFont = QtGui.QFont('verdana',
                             myFontSize,
                             myFontWeight,
                             myItalicsFlag)
        myLabel = QgsComposerLabel(self.composition)
        myLabel.setFont(myFont)
        myHeading = self.tr('InaSAFE - Indonesia Scenario Assessment'
                            ' for Emergencies')
        myLabel.setText(myHeading)
        myLabel.adjustSizeToText()
        myLabelHeight = 10.0  # determined using qgis map composer
        myLabelWidth = 170.0   # item - position and size...option
        myLeftOffset = self.pageWidth - self.pageMargin - myLabelWidth
        myLabel.setItemPosition(myLeftOffset,
                                top_offset - 2,  # -2 to push it up a little
                                myLabelWidth,
                                myLabelHeight,
                                )
        myLabel.setFrameEnabled(self.showFramesFlag)
        self.composition.addItem(myLabel)
        return myLabelHeight

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

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

        :returns: The composer map.
        :rtype: QgsComposerMap
        """
        LOGGER.debug('InaSAFE Map drawMap called')
        myMapWidth = self.mapWidth
        myComposerMap = QgsComposerMap(
            self.composition,
            self.pageMargin,
            top_offset,
            myMapWidth,
            self.mapHeight)
        #myExtent = self.iface.mapCanvas().extent()
        # The dimensions of the map canvas and the print composer map may
        # differ. So we set the map composer extent using the canvas and
        # then defer to the map canvas's map extents thereafter
        # Update: disabled as it results in a rectangular rather than
        # square map
        #myComposerMap.setNewExtent(myExtent)
        myComposerExtent = myComposerMap.extent()
        # Recenter the composer map on the center of the canvas
        # Note that since the composer map is square and the canvas may be
        # arbitrarily shaped, we center based on the longest edge
        myCanvasExtent = self.iface.mapCanvas().extent()
        myWidth = myCanvasExtent.width()
        myHeight = myCanvasExtent.height()
        myLongestLength = myWidth
        if myWidth < myHeight:
            myLongestLength = myHeight
        myHalfLength = myLongestLength / 2
        myCenter = myCanvasExtent.center()
        myMinX = myCenter.x() - myHalfLength
        myMaxX = myCenter.x() + myHalfLength
        myMinY = myCenter.y() - myHalfLength
        myMaxY = myCenter.y() + myHalfLength
        mySquareExtent = QgsRectangle(myMinX, myMinY, myMaxX, myMaxY)
        myComposerMap.setNewExtent(mySquareExtent)

        myComposerMap.setGridEnabled(True)
        myNumberOfSplits = 5
        # .. todo:: Write logic to adjust precision so that adjacent tick marks
        #    always have different displayed values
        myPrecision = 2
        myXInterval = myComposerExtent.width() / myNumberOfSplits
        myComposerMap.setGridIntervalX(myXInterval)
        myYInterval = myComposerExtent.height() / myNumberOfSplits
        myComposerMap.setGridIntervalY(myYInterval)
        myComposerMap.setGridStyle(QgsComposerMap.Cross)
        myCrossLengthMM = 1
        myComposerMap.setCrossLength(myCrossLengthMM)
        myComposerMap.setZValue(0)  # To ensure it does not overlay logo
        myFontSize = 6
        myFontWeight = QtGui.QFont.Normal
        myItalicsFlag = False
        myFont = QtGui.QFont(
            'verdana',
            myFontSize,
            myFontWeight,
            myItalicsFlag)
        myComposerMap.setGridAnnotationFont(myFont)
        myComposerMap.setGridAnnotationPrecision(myPrecision)
        myComposerMap.setShowGridAnnotation(True)
        myComposerMap.setGridAnnotationDirection(
            QgsComposerMap.BoundaryDirection, QgsComposerMap.Top)
        self.composition.addItem(myComposerMap)
        self.draw_graticule_mask(top_offset)
        return myComposerMap

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

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

        :param top_offset: Vertical offset at which the logo should be drawn.
        :type top_offset: int
        """
        LOGGER.debug('InaSAFE Map drawGraticuleMask called')
        myLeftOffset = self.pageMargin + self.mapWidth
        myRect = QgsComposerShape(myLeftOffset + 0.5,
                                  top_offset,
                                  self.pageWidth - myLeftOffset,
                                  self.mapHeight + 1,
                                  self.composition)

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

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

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

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

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

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

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

        .. seealso:: :meth:`drawNativeScaleBar`

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

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

        #print "SBWMM:", myScaleBarWidthMM
        #print "SWMM:", myPrintSegmentWidthMM
        #print "SWM:", myGroundSegmentWidthM
        #print "SWKM:", myGroundSegmentWidthKM
        # start drawing in line segments
        myScaleBarHeight = 5  # mm
        myLineWidth = 0.3  # mm
        myInsetDistance = 7  # how much to inset the scalebar into the map by
        myScaleBarX = self.pageMargin + myInsetDistance
        myScaleBarY = (
            top_offset + self.mapHeight - myInsetDistance -
            myScaleBarHeight)  # mm

        # Draw an outer background box - shamelessly hardcoded buffer
        myRect = QgsComposerShape(myScaleBarX - 4,  # left edge
                                  myScaleBarY - 3,  # top edge
                                  myScaleBarWidthMM + 13,  # right edge
                                  myScaleBarHeight + 6,  # bottom edge
                                  self.composition)

        myRect.setShapeType(QgsComposerShape.Rectangle)
        myPen = QtGui.QPen()
        myPen.setColor(QtGui.QColor(255, 255, 255))
        myPen.setWidthF(myLineWidth)
        myRect.setPen(myPen)
        #myRect.setLineWidth(myLineWidth)
        myRect.setFrameEnabled(False)
        myBrush = QtGui.QBrush(QtGui.QColor(255, 255, 255))
        # workaround for missing setTransparentFill missing from python api
        myRect.setBrush(myBrush)
        self.composition.addItem(myRect)
        # Set up the tick label font
        myFontWeight = QtGui.QFont.Normal
        myFontSize = 6
        myItalicsFlag = False
        myFont = QtGui.QFont('verdana',
                             myFontSize,
                             myFontWeight,
                             myItalicsFlag)
        # Draw the bottom line
        myUpshift = 0.3  # shift the bottom line up for better rendering
        myRect = QgsComposerShape(myScaleBarX,
                                  myScaleBarY + myScaleBarHeight - myUpshift,
                                  myScaleBarWidthMM,
                                  0.1,
                                  self.composition)

        myRect.setShapeType(QgsComposerShape.Rectangle)
        myPen = QtGui.QPen()
        myPen.setColor(QtGui.QColor(255, 255, 255))
        myPen.setWidthF(myLineWidth)
        myRect.setPen(myPen)
        #myRect.setLineWidth(myLineWidth)
        myRect.setFrameEnabled(False)
        self.composition.addItem(myRect)

        # Now draw the scalebar ticks
        for myTickCountIterator in range(0, myTickCount + 1):
            myDistanceSuffix = ''
            if myTickCountIterator == myTickCount:
                myDistanceSuffix = ' ' + myUnits
            myRealWorldDistance = ('%.0f%s' %
                                   (myTickCountIterator *
                                    myGroundSegmentWidth,
                                    myDistanceSuffix))
            #print 'RW:', myRealWorldDistance
            myMMOffset = myScaleBarX + (myTickCountIterator *
                                        myPrintSegmentWidthMM)
            #print 'MM:', myMMOffset
            myTickHeight = myScaleBarHeight / 2
            # Lines are not exposed by the api yet so we
            # bodge drawing lines using rectangles with 1px height or width
            myTickWidth = 0.1  # width or rectangle to be drawn
            myUpTickLine = QgsComposerShape(
                myMMOffset,
                myScaleBarY + myScaleBarHeight - myTickHeight,
                myTickWidth,
                myTickHeight,
                self.composition)

            myUpTickLine.setShapeType(QgsComposerShape.Rectangle)
            myPen = QtGui.QPen()
            myPen.setWidthF(myLineWidth)
            myUpTickLine.setPen(myPen)
            #myUpTickLine.setLineWidth(myLineWidth)
            myUpTickLine.setFrameEnabled(False)
            self.composition.addItem(myUpTickLine)
            #
            # Add a tick label
            #
            myLabel = QgsComposerLabel(self.composition)
            myLabel.setFont(myFont)
            myLabel.setText(myRealWorldDistance)
            myLabel.adjustSizeToText()
            myLabel.setItemPosition(
                myMMOffset - 3,
                myScaleBarY - myTickHeight)
            myLabel.setFrameEnabled(self.showFramesFlag)
            self.composition.addItem(myLabel)

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

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

        :returns: The height of the label as rendered.
        :rtype: float
        """
        LOGGER.debug('InaSAFE Map drawImpactTitle called')
        myTitle = self.map_title()
        if myTitle is None:
            myTitle = ''
        myFontSize = 20
        myFontWeight = QtGui.QFont.Bold
        myItalicsFlag = False
        myFont = QtGui.QFont(
            'verdana', myFontSize, myFontWeight, myItalicsFlag)
        myLabel = QgsComposerLabel(self.composition)
        myLabel.setFont(myFont)
        myHeading = myTitle
        myLabel.setText(myHeading)
        myLabelWidth = self.pageWidth - (self.pageMargin * 2)
        myLabelHeight = 12
        myLabel.setItemPosition(
            self.pageMargin, top_offset, myLabelWidth, myLabelHeight)
        myLabel.setFrameEnabled(self.showFramesFlag)
        self.composition.addItem(myLabel)
        return myLabelHeight

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

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

        :param top_offset: Vertical offset at which the logo should be drawn.
        :type top_offset: int
        """
        LOGGER.debug('InaSAFE Map drawLegend called')
        mapLegendAttributes = self.map_legend_attributes()
        legendNotes = mapLegendAttributes.get('legend_notes', None)
        legendUnits = mapLegendAttributes.get('legend_units', None)
        legendTitle = mapLegendAttributes.get('legend_title', None)
        LOGGER.debug(mapLegendAttributes)
        myLegend = MapLegend(self.layer, self.pageDpi, legendTitle,
                             legendNotes, legendUnits)
        self.legend = myLegend.get_legend()
        myPicture1 = QgsComposerPicture(self.composition)
        myLegendFilePath = unique_filename(
            prefix='legend', suffix='.png', dir='work')
        self.legend.save(myLegendFilePath, 'PNG')
        myPicture1.setPictureFile(myLegendFilePath)
        myLegendHeight = points_to_mm(self.legend.height(), self.pageDpi)
        myLegendWidth = points_to_mm(self.legend.width(), self.pageDpi)
        myPicture1.setItemPosition(self.pageMargin,
                                   top_offset,
                                   myLegendWidth,
                                   myLegendHeight)
        myPicture1.setFrameEnabled(False)
        self.composition.addItem(myPicture1)
        os.remove(myLegendFilePath)

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

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

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

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

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

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

        :returns: Graphics scene item.
        :rtype: QGraphicsSceneItem
        """
        LOGGER.debug('InaSAFE Map drawImage called')
        myDesiredWidthMM = theWidthMM  # mm
        myDesiredWidthPX = mm_to_points(myDesiredWidthMM, self.pageDpi)
        myActualWidthPX = theImage.width()
        myScaleFactor = myDesiredWidthPX / myActualWidthPX

        LOGGER.debug('%s %s %s' % (
            myScaleFactor, myActualWidthPX, myDesiredWidthPX))
        myTransform = QtGui.QTransform()
        myTransform.scale(myScaleFactor, myScaleFactor)
        myTransform.rotate(0.5)
        # noinspection PyArgumentList
        myItem = self.composition.addPixmap(QtGui.QPixmap.fromImage(theImage))
        myItem.setTransform(myTransform)
        myItem.setOffset(theLeftOffset / myScaleFactor,
                         theTopOffset / myScaleFactor)
        return myItem

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

        :param top_offset: Vertical offset at which the logo should be drawn.
        :type top_offset: int
        """
        LOGGER.debug('InaSAFE Map drawDisclaimer called')
        #elapsed_time: 11.612545
        #user: timlinux
        #host_name: ultrabook
        #time_stamp: 2012-10-13_23:10:31
        #myUser = self.keywordIO.readKeywords(self.layer, 'user')
        #myHost = self.keywordIO.readKeywords(self.layer, 'host_name')
        myDateTime = self.keywordIO.read_keywords(self.layer, 'time_stamp')
        myTokens = myDateTime.split('_')
        myDate = myTokens[0]
        myTime = myTokens[1]
        #myElapsedTime = self.keywordIO.readKeywords(self.layer,
        #                                            'elapsed_time')
        #myElapsedTime = humaniseSeconds(myElapsedTime)
        myLongVersion = get_version()
        myTokens = myLongVersion.split('.')
        myVersion = '%s.%s.%s' % (myTokens[0], myTokens[1], myTokens[2])
        myLabelText = self.tr(
            'Date and time of assessment: %s %s\n'
            'Special note: This assessment is a guide - we strongly recommend '
            'that you ground truth the results shown here before deploying '
            'resources and / or personnel.\n'
            'Assessment carried out using InaSAFE release %s (QGIS '
            'plugin version).') % (myDate, myTime, myVersion)
        myFontSize = 6
        myFontWeight = QtGui.QFont.Normal
        myItalicsFlag = True
        myFont = QtGui.QFont('verdana',
                             myFontSize,
                             myFontWeight,
                             myItalicsFlag)
        myLabel = QgsComposerLabel(self.composition)
        myLabel.setFont(myFont)
        myLabel.setText(myLabelText)
        myLabel.adjustSizeToText()
        myLabelHeight = 50.0  # mm determined using qgis map composer
        myLabelWidth = (self.pageWidth / 2) - self.pageMargin
        myLeftOffset = self.pageWidth / 2  # put in right half of page
        myLabel.setItemPosition(myLeftOffset,
                                top_offset,
                                myLabelWidth,
                                myLabelHeight,
                                )
        myLabel.setFrameEnabled(self.showFramesFlag)
        self.composition.addItem(myLabel)

    def draw_disclaimer(self):
        """Add a disclaimer to the composition."""
        LOGGER.debug('InaSAFE Map drawDisclaimer called')
        myFontSize = 10
        myFontWeight = QtGui.QFont.Normal
        myItalicsFlag = True
        myFont = QtGui.QFont('verdana',
                             myFontSize,
                             myFontWeight,
                             myItalicsFlag)
        myLabel = QgsComposerLabel(self.composition)
        myLabel.setFont(myFont)
        myLabel.setText(self.disclaimer)
        myLabel.adjustSizeToText()
        myLabelHeight = 7.0  # mm determined using qgis map composer
        myLabelWidth = self.pageWidth   # item - position and size...option
        myLeftOffset = self.pageMargin
        myTopOffset = self.pageHeight - self.pageMargin
        myLabel.setItemPosition(myLeftOffset,
                                myTopOffset,
                                myLabelWidth,
                                myLabelHeight,
                                )
        myLabel.setFrameEnabled(self.showFramesFlag)
        self.composition.addItem(myLabel)

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

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

    def map_legend_attributes(self):
        """Get the map legend attribute from the layer keywords if possible.

        :returns: None on error, otherwise the attributes (notes and units).
        :rtype: None, str
        """
        LOGGER.debug('InaSAFE Map getMapLegendAtributes called')
        legendAttributes = ['legend_notes',
                            'legend_units',
                            'legend_title']
        dictLegendAttributes = {}
        for myLegendAttribute in legendAttributes:
            try:
                dictLegendAttributes[myLegendAttribute] = \
                    self.keywordIO.read_keywords(self.layer, myLegendAttribute)
            except KeywordNotFoundError:
                pass
            except Exception:
                pass
        return dictLegendAttributes

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

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

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

    def render_template(self, template_path, output_path):
        """Load a QgsComposer map from a template and render it.

        .. note:: THIS METHOD IS EXPERIMENTAL AND CURRENTLY NON FUNCTIONAL

        :param template_path:  Path to the template that should be loaded.
        :type template_path: str

        :param output_path: Path for the output pdf.
        :type output_path: str
        """
        self.setup_composition()

        myResolution = self.composition.printResolution()
        self.printer = setup_printer(
            output_path, resolution=myResolution)
        if self.composition:
            myFile = QtCore.QFile(template_path)
            myDocument = QtXml.QDomDocument()
            myDocument.setContent(myFile, False)  # .. todo:: fix magic param
            myNodeList = myDocument.elementsByTagName('Composer')
            if myNodeList.size() > 0:
                myElement = myNodeList.at(0).toElement()
                self.composition.readXML(myElement, myDocument)
        self.make_pdf(output_path)
示例#4
0
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]
示例#5
0
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]
示例#6
0
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
示例#7
0
文件: map.py 项目: vdeparday/inasafe
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()
示例#8
0
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]
示例#10
0
class Map():
    """A class for creating a map."""
    def __init__(self, iface):
        """Constructor for the Map class.

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

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

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

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

    def set_impact_layer(self, layer):
        """Set the layer that will be used for stats, legend and reporting.

        :param layer: Layer that will be used for stats, legend and reporting.
        :type layer: QgsMapLayer, QgsRasterLayer, QgsVectorLayer
        """
        self.layer = layer

    def set_logo(self, logo):
        """

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

    def set_template(self, template):
        """

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

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

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

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

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

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

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

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

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

        self.load_template()

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        .. seealso:: :meth:`drawNativeScaleBar`

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    def map_legend_attributes(self):
        """Get the map legend attribute from the layer keywords if possible.

        :returns: None on error, otherwise the attributes (notes and units).
        :rtype: None, str
        """
        LOGGER.debug('InaSAFE Map getMapLegendAtributes called')
        legend_attribute_list = [
            'legend_notes', 'legend_units', 'legend_title'
        ]
        legend_attribute_dict = {}
        for myLegendAttribute in legend_attribute_list:
            try:
                legend_attribute_dict[myLegendAttribute] = \
                    self.keyword_io.read_keywords(
                        self.layer, myLegendAttribute)
            except KeywordNotFoundError:
                pass
            except Exception:
                pass
        return legend_attribute_dict

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

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

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

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

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

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

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

        # get information for substitutions
        # date, time and plugin version
        date_time = self.keyword_io.read_keywords(self.layer, 'time_stamp')
        tokens = date_time.split('_')
        date = tokens[0]
        time = tokens[1]
        long_version = get_version()
        tokens = long_version.split('.')
        version = '%s.%s.%s' % (tokens[0], tokens[1], tokens[2])

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

        if not title:
            title = ''

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

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

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

        # Get the main map canvas on the composition and set
        # its extents to the event.
        map = self.composition.getComposerItemById('impact-map')
        if map is not None:
            # Recenter the composer map on the center of the canvas
            # Note that since the composer map is square and the canvas may be
            # arbitrarily shaped, we center based on the longest edge
            canvas_extent = self.iface.mapCanvas().extent()
            width = canvas_extent.width()
            height = canvas_extent.height()
            longest_width = width
            if width < height:
                longest_width = height
            half_length = longest_width / 2
            center = canvas_extent.center()
            min_x = center.x() - half_length
            max_x = center.x() + half_length
            min_y = center.y() - half_length
            max_y = center.y() + half_length
            square_extent = QgsRectangle(min_x, min_y, max_x, max_y)
            map.setNewExtent(square_extent)

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

        legend = self.composition.getComposerItemById('impact-legend')
        legend_attributes = self.map_legend_attributes()
        LOGGER.debug(legend_attributes)
        #legend_notes = mapLegendAttributes.get('legend_notes', None)
        #legend_units = mapLegendAttributes.get('legend_units', None)
        legend_title = legend_attributes.get('legend_title', None)
        if legend_title is None:
            legend_title = ""
        legend.setTitle(legend_title)
        legend.updateLegend()