Пример #1
0
 def test_printImpactTable(self):
     """Test that we can render html from impact table keywords."""
     LOGGER.debug('InaSAFE HtmlRenderer testing printImpactTable')
     myFilename = 'test_floodimpact.tif'
     myLayer, _ = loadLayer(myFilename)
     myMessage = 'Layer is not valid: %s' % myFilename
     assert myLayer.isValid(), myMessage
     myPageDpi = 300
     myHtmlRenderer = HtmlRenderer(myPageDpi)
     myPath = unique_filename(prefix='impactTable',
                              suffix='.pdf',
                              dir=temp_dir('test'))
     myKeywordIO = KeywordIO()
     myKeywords = myKeywordIO.readKeywords(myLayer)
     myPath = myHtmlRenderer.printImpactTable(myKeywords,
                                              theFilename=myPath)
     myMessage = 'Rendered output does not exist: %s' % myPath
     assert os.path.exists(myPath), myMessage
     # pdf rendering is non deterministic so we can't do a hash check
     # test_renderComposition renders just the image instead of pdf
     # so we hash check there and here we just do a basic minimum file
     # size check.
     mySize = os.stat(myPath).st_size
     myExpectedSize = 20936  # as rendered on linux ub 12.04 64
     myMessage = ('Expected rendered table pdf to be at least %s, got %s'
                  % (myExpectedSize, mySize))
     assert mySize >= myExpectedSize, myMessage
Пример #2
0
    def __init__(self,
                 theLayer,
                 theDpi=300,
                 theLegendTitle=None,
                 theLegendNotes=None,
                 theLegendUnits=None):
        """Constructor for the Map Legend class.

        Args:
            * theLayer: QgsMapLayer object that the legend should be generated
                for.
            * theDpi: Optional DPI for generated legend image. Defaults to
                300 if not specified.
        Returns:
            None
        Raises:
            Any exceptions raised will be propagated.
        """
        LOGGER.debug('InaSAFE Map class initialised')
        self.legendImage = None
        self.layer = theLayer
        # how high each row of the legend should be
        self.legendIncrement = 42
        self.keywordIO = KeywordIO()
        self.legendFontSize = 8
        self.legendWidth = 900
        self.dpi = theDpi
        if theLegendTitle is None:
            self.legendTitle = self.tr('Legend')
        else:
            self.legendTitle = theLegendTitle
        self.legendNotes = theLegendNotes
        self.legendUnits = theLegendUnits
Пример #3
0
    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')
Пример #4
0
    def __init__(self, parent, iface, theDock=None):
        """Constructor for the dialog.

        Args:
           * parent - parent widget of this dialog
           * iface - a Quantum GIS QGisAppInterface instance.
           * theDock - Optional dock widget instance that we can notify of
             changes to the keywords.

        Returns:
           not applicable
        Raises:
           no exceptions explicitly raised
        """

        QtGui.QDialog.__init__(self, parent)
        self.setupUi(self)
        self.setWindowTitle(self.tr('InaSAFE %s Options' % get_version()))
        # Save reference to the QGIS interface and parent
        self.iface = iface
        self.parent = parent
        self.dock = theDock
        self.helpDialog = None
        self.keywordIO = KeywordIO()
        # Set up things for context help
        myButton = self.buttonBox.button(QtGui.QDialogButtonBox.Help)
        QtCore.QObject.connect(myButton, QtCore.SIGNAL('clicked()'),
                               self.showHelp)
        self.grpNotImplemented.hide()
        self.adjustSize()
        self.restoreState()
        # hack prevent showing use thread visible and set it false see #557
        self.cbxUseThread.setChecked(True)
        self.cbxUseThread.setVisible(False)
Пример #5
0
 def test_printImpactTable(self):
     """Test that we can render html from impact table keywords."""
     LOGGER.debug('InaSAFE HtmlRenderer testing printImpactTable')
     myFilename = 'test_floodimpact.tif'
     myLayer, _ = loadLayer(myFilename)
     myMessage = 'Layer is not valid: %s' % myFilename
     assert myLayer.isValid(), myMessage
     myPageDpi = 300
     myHtmlRenderer = HtmlRenderer(myPageDpi)
     myPath = unique_filename(prefix='impactTable',
                              suffix='.pdf',
                              dir=temp_dir('test'))
     myKeywordIO = KeywordIO()
     myKeywords = myKeywordIO.readKeywords(myLayer)
     myPath = myHtmlRenderer.printImpactTable(myKeywords,
                                              theFilename=myPath)
     myMessage = 'Rendered output does not exist: %s' % myPath
     assert os.path.exists(myPath), myMessage
     # pdf rendering is non deterministic so we can't do a hash check
     # test_renderComposition renders just the image instead of pdf
     # so we hash check there and here we just do a basic minimum file
     # size check.
     mySize = os.stat(myPath).st_size
     myExpectedSize = 20936  # as rendered on linux ub 12.04 64
     myMessage = ('Expected rendered table pdf to be at least %s, got %s'
                  % (myExpectedSize, mySize))
     assert mySize >= myExpectedSize, myMessage
Пример #6
0
 def setUp(self):
     self.keywordIO = KeywordIO()
     myUri = QgsDataSourceURI()
     myUri.setDatabase(os.path.join(TESTDATA, 'jk.sqlite'))
     myUri.setDataSource('', 'osm_buildings', 'Geometry')
     self.sqliteLayer = QgsVectorLayer(myUri.uri(), 'OSM Buildings',
                                       'spatialite')
     myHazardPath = os.path.join(HAZDATA, 'Shakemap_Padang_2009.asc')
     self.fileRasterLayer, myType = loadLayer(myHazardPath,
                                              theDirectory=None)
     del myType
     self.fileVectorLayer, myType = loadLayer('Padang_WGS84.shp')
     del myType
     self.expectedSqliteKeywords = {
         'category': 'exposure',
         'datatype': 'OSM',
         'subcategory': 'building'
     }
     self.expectedVectorKeywords = {
         'category': 'exposure',
         'datatype': 'itb',
         'subcategory': 'structure'
     }
     self.expectedRasterKeywords = {
         'category': 'hazard',
         'source': 'USGS',
         'subcategory': 'earthquake',
         'unit': 'MMI',
         'title': ('An earthquake in Padang '
                   'like in 2009')
     }
Пример #7
0
    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")
Пример #8
0
    def __init__(self, parent, iface, theDock=None):
        """Constructor for the dialog.

        Args:
           * parent - parent widget of this dialog
           * iface - a Quantum GIS QGisAppInterface instance.
           * theDock - Optional dock widget instance that we can notify of
             changes to the keywords.

        Returns:
           not applicable
        Raises:
           no exceptions explicitly raised
        """

        QtGui.QDialog.__init__(self, parent)
        self.setupUi(self)
        self.setWindowTitle(self.tr('InaSAFE %s Options' % get_version()))
        # Save reference to the QGIS interface and parent
        self.iface = iface
        self.parent = parent
        self.dock = theDock
        self.helpDialog = None
        self.keywordIO = KeywordIO()
        # Set up things for context help
        myButton = self.buttonBox.button(QtGui.QDialogButtonBox.Help)
        QtCore.QObject.connect(myButton, QtCore.SIGNAL('clicked()'),
                               self.showHelp)
        self.grpNotImplemented.hide()
        self.adjustSize()
        self.restoreState()
        # hack prevent showing use thread visible and set it false see #557
        self.cbxUseThread.setChecked(True)
        self.cbxUseThread.setVisible(False)
Пример #9
0
 def setUp(self):
     self.keywordIO = KeywordIO()
     myUri = QgsDataSourceURI()
     myUri.setDatabase(os.path.join(TESTDATA, 'jk.sqlite'))
     myUri.setDataSource('', 'osm_buildings', 'Geometry')
     self.sqliteLayer = QgsVectorLayer(myUri.uri(), 'OSM Buildings',
                                    'spatialite')
     myHazardPath = os.path.join(HAZDATA, 'Shakemap_Padang_2009.asc')
     self.fileRasterLayer, myType = loadLayer(myHazardPath,
                                              theDirectory=None)
     del myType
     self.fileVectorLayer, myType = loadLayer('Padang_WGS84.shp')
     del myType
     self.expectedSqliteKeywords = {'category': 'exposure',
                                    'datatype': 'OSM',
                                    'subcategory': 'building'}
     self.expectedVectorKeywords = {'category': 'exposure',
                                    'datatype': 'itb',
                                    'subcategory': 'structure'}
     self.expectedRasterKeywords = {'category': 'hazard',
                                    'source': 'USGS',
                                    'subcategory': 'earthquake',
                                    'unit': 'MMI',
                                    'title': ('An earthquake in Padang '
                                              'like in 2009')}
Пример #10
0
    def __init__(self, theLayer, theDpi=300, theLegendTitle=None,
                 theLegendNotes=None, theLegendUnits=None):
        """Constructor for the Map Legend class.

        Args:
            * theLayer: QgsMapLayer object that the legend should be generated
                for.
            * theDpi: Optional DPI for generated legend image. Defaults to
                300 if not specified.
        Returns:
            None
        Raises:
            Any exceptions raised will be propagated.
        """
        LOGGER.debug('InaSAFE Map class initialised')
        self.legendImage = None
        self.layer = theLayer
        # how high each row of the legend should be
        self.legendIncrement = 42
        self.keywordIO = KeywordIO()
        self.legendFontSize = 8
        self.legendWidth = 900
        self.dpi = theDpi
        if theLegendTitle is None:
            self.legendTitle = self.tr('Legend')
        else:
            self.legendTitle = theLegendTitle
        print 'AFUFUFU', self.legendTitle
        self.legendNotes = theLegendNotes
        self.legendUnits = theLegendUnits
Пример #11
0
class MapLegend():
    """A class for creating a map legend."""
    def __init__(self,
                 theLayer,
                 theDpi=300,
                 theLegendTitle=None,
                 theLegendNotes=None,
                 theLegendUnits=None):
        """Constructor for the Map Legend class.

        Args:
            * theLayer: QgsMapLayer object that the legend should be generated
                for.
            * theDpi: Optional DPI for generated legend image. Defaults to
                300 if not specified.
        Returns:
            None
        Raises:
            Any exceptions raised will be propagated.
        """
        LOGGER.debug('InaSAFE Map class initialised')
        self.legendImage = None
        self.layer = theLayer
        # how high each row of the legend should be
        self.legendIncrement = 42
        self.keywordIO = KeywordIO()
        self.legendFontSize = 8
        self.legendWidth = 900
        self.dpi = theDpi
        if theLegendTitle is None:
            self.legendTitle = self.tr('Legend')
        else:
            self.legendTitle = theLegendTitle
        self.legendNotes = theLegendNotes
        self.legendUnits = theLegendUnits

    def tr(self, theString):
        """We implement this ourself 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('MapLegend', theString)

    def getLegend(self):
        """Examine the classes of the impact layer associated with this print
        job.

        .. note: This is a wrapper for the rasterLegend and vectorLegend
           methods.

        Args:
            None
        Returns:
            None
        Raises:
            An InvalidLegendLayer will be raised if a legend cannot be
            created from the layer.
        """
        LOGGER.debug('InaSAFE Map Legend getLegend called')
        if self.layer is None:
            myMessage = self.tr('Unable to make a legend when map generator '
                                'has no layer set.')
            raise LegendLayerError(myMessage)
        try:
            self.keywordIO.readKeywords(self.layer, 'impact_summary')
        except KeywordNotFoundError, e:
            myMessage = self.tr('This layer does not appear to be an impact '
                                'layer. Try selecting an impact layer in the '
                                'QGIS layers list or creating a new impact '
                                'scenario before using the print tool.'
                                '\nMessage: %s' % str(e))
            raise Exception(myMessage)
        if self.layer.type() == QgsMapLayer.VectorLayer:
            return self.getVectorLegend()
        else:
            return self.getRasterLegend()
Пример #12
0
class KeywordIOTest(unittest.TestCase):
    """Tests for reading and writing of raster and vector data
    """

    def setUp(self):
        self.keywordIO = KeywordIO()
        myUri = QgsDataSourceURI()
        myUri.setDatabase(os.path.join(TESTDATA, 'jk.sqlite'))
        myUri.setDataSource('', 'osm_buildings', 'Geometry')
        self.sqliteLayer = QgsVectorLayer(myUri.uri(), 'OSM Buildings',
                                       'spatialite')
        myHazardPath = os.path.join(HAZDATA, 'Shakemap_Padang_2009.asc')
        self.fileRasterLayer, myType = loadLayer(myHazardPath,
                                                 theDirectory=None)
        del myType
        self.fileVectorLayer, myType = loadLayer('Padang_WGS84.shp')
        del myType
        self.expectedSqliteKeywords = {'category': 'exposure',
                                       'datatype': 'OSM',
                                       'subcategory': 'building'}
        self.expectedVectorKeywords = {'category': 'exposure',
                                       'datatype': 'itb',
                                       'subcategory': 'structure'}
        self.expectedRasterKeywords = {'category': 'hazard',
                                       'source': 'USGS',
                                       'subcategory': 'earthquake',
                                       'unit': 'MMI',
                                       'title': ('An earthquake in Padang '
                                                 'like in 2009')}

    def tearDown(self):
        pass

    def test_getHashForDatasource(self):
        """Test we can reliably get a hash for a uri"""
        myHash = self.keywordIO.getHashForDatasource(PG_URI)
        myExpectedHash = '7cc153e1b119ca54a91ddb98a56ea95e'
        myMessage = "Got: %s\nExpected: %s" % (myHash, myExpectedHash)
        assert myHash == myExpectedHash, myMessage

    def test_writeReadKeywordFromUri(self):
        """Test we can set and get keywords for a non local datasource"""
        myHandle, myFilename = tempfile.mkstemp('.db', 'keywords_',
                                            temp_dir())

        # Ensure the file is deleted before we try to write to it
        # fixes windows specific issue where you get a message like this
        # ERROR 1: c:\temp\inasafe\clip_jpxjnt.shp is not a directory.
        # This is because mkstemp creates the file handle and leaves
        # the file open.
        os.close(myHandle)
        os.remove(myFilename)
        myExpectedKeywords = {'category': 'exposure',
                              'datatype': 'itb',
                              'subcategory': 'building'}
        # SQL insert test
        # On first write schema is empty and there is no matching hash
        self.keywordIO.setKeywordDbPath(myFilename)
        self.keywordIO.writeKeywordsForUri(PG_URI, myExpectedKeywords)
        # SQL Update test
        # On second write schema is populated and we update matching hash
        myExpectedKeywords = {'category': 'exposure',
                              'datatype': 'OSM',  # <--note the change here!
                              'subcategory': 'building'}
        self.keywordIO.writeKeywordsForUri(PG_URI, myExpectedKeywords)
        # Test getting all keywords
        myKeywords = self.keywordIO.readKeywordFromUri(PG_URI)
        myMessage = 'Got: %s\n\nExpected %s\n\nDB: %s' % (
                    myKeywords, myExpectedKeywords, myFilename)
        assert myKeywords == myExpectedKeywords, myMessage
        # Test getting just a single keyword
        myKeyword = self.keywordIO.readKeywordFromUri(PG_URI, 'datatype')
        myExpectedKeyword = 'OSM'
        myMessage = 'Got: %s\n\nExpected %s\n\nDB: %s' % (
                    myKeyword, myExpectedKeyword, myFilename)
        assert myKeyword == myExpectedKeyword, myMessage
        # Test deleting keywords actually does delete
        self.keywordIO.deleteKeywordsForUri(PG_URI)
        try:
            myKeyword = self.keywordIO.readKeywordFromUri(PG_URI, 'datatype')
            #if the above didnt cause an exception then bad
            myMessage = 'Expected a HashNotFoundException to be raised'
            assert myMessage
        except HashNotFoundException:
            #we expect this outcome so good!
            pass

    def test_areKeywordsFileBased(self):
        """Can we correctly determine if keywords should be written to file or
        to database?"""
        assert not self.keywordIO.areKeywordsFileBased(self.sqliteLayer)
        assert self.keywordIO.areKeywordsFileBased(self.fileRasterLayer)
        assert self.keywordIO.areKeywordsFileBased(self.fileVectorLayer)

    def test_readRasterFileKeywords(self):
        """Can we read raster file keywords using generic readKeywords method
        """
        myKeywords = self.keywordIO.readKeywords(self.fileRasterLayer)
        myExpectedKeywords = self.expectedRasterKeywords
        mySource = self.fileRasterLayer.source()
        myMessage = 'Got:\n%s\nExpected:\n%s\nSource:\n%s' % (
                    myKeywords, myExpectedKeywords, mySource)
        assert myKeywords == myExpectedKeywords, myMessage

    def test_readVectorFileKeywords(self):
        """Can we read vector file keywords with the generic readKeywords
        method """
        myKeywords = self.keywordIO.readKeywords(self.fileVectorLayer)
        myExpectedKeywords = self.expectedVectorKeywords
        mySource = self.fileVectorLayer.source()
        myMessage = 'Got: %s\n\nExpected %s\n\nSource: %s' % (
                    myKeywords, myExpectedKeywords, mySource)
        assert myKeywords == myExpectedKeywords, myMessage

    def test_appendKeywords(self):
        """Can we append file keywords with the generic readKeywords method."""
        myLayer, _ = makePadangLayerClone()
        myNewKeywords = {'category': 'exposure', 'test': 'TEST'}
        self.keywordIO.appendKeywords(myLayer, myNewKeywords)
        myKeywords = self.keywordIO.readKeywords(myLayer)

        for myKey, myValue in myNewKeywords.iteritems():
            myMessage = ('Layer keywords misses appended key: %s\n'
                        'Layer keywords:\n%s\n'
                        'Appended keywords:\n%s\n' %
                        (myKey,
                         myKeywords,
                         myNewKeywords))
            assert myKey in myKeywords, myMessage
            myMessage = ('Layer keywords misses appended value: %s\n'
                         'Layer keywords:\n%s\n'
                         'Appended keywords:\n%s\n' %
                         (myValue,
                          myKeywords,
                          myNewKeywords))
            assert myKeywords[myKey] == myValue, myMessage

    def test_readDBKeywords(self):
        """Can we read sqlite keywords with the generic readKeywords method
        """
        myLocalPath = os.path.join(os.path.dirname(__file__),
                                   '..', 'jk.sqlite')
        myPath = os.path.join(TESTDATA, 'test_keywords.db')
        self.keywordIO.setKeywordDbPath(myPath)
        # We need to make a local copy of the dataset so
        # that we can use a local path that will hash properly on the
        # database to return us the correct / valid keywords record.
        shutil.copy2(os.path.join(TESTDATA, 'jk.sqlite'), myLocalPath)
        myUri = QgsDataSourceURI()
        # always use relative path!
        myUri.setDatabase('../jk.sqlite')
        myUri.setDataSource('', 'osm_buildings', 'Geometry')
        # create a local version that has the relative url
        mySqliteLayer = QgsVectorLayer(myUri.uri(), 'OSM Buildings',
                                       'spatialite')
        myExpectedSource = ('dbname=\'../jk.sqlite\' table="osm_buildings"'
             ' (Geometry) sql=')
        myMessage = 'Got source: %s\n\nExpected %s\n' % (
                    mySqliteLayer.source, myExpectedSource)
        assert mySqliteLayer.source() == myExpectedSource, myMessage
        myKeywords = self.keywordIO.readKeywords(mySqliteLayer)
        myExpectedKeywords = self.expectedSqliteKeywords
        assert myKeywords == myExpectedKeywords, myMessage
        mySource = self.sqliteLayer.source()
        os.remove(myLocalPath)
        myMessage = 'Got: %s\n\nExpected %s\n\nSource: %s' % (
                    myKeywords, myExpectedKeywords, mySource)
        assert myKeywords == myExpectedKeywords, myMessage
Пример #13
0
def _clipRasterLayer(theLayer, theExtent, theCellSize=None,
                     theExtraKeywords=None):
    """Clip a Hazard or Exposure raster layer to the extents provided. The
    layer must be a raster layer or an exception will be thrown.

    .. note:: The extent *must* be in EPSG:4326.

    The output layer will always be in WGS84/Geographic.

    Args:

        * theLayer - a valid QGIS raster layer in EPSG:4326
        * theExtent -  an array representing the exposure layer
           extents in the form [xmin, ymin, xmax, ymax]. It is assumed
           that the coordinates are in EPSG:4326 although currently
           no checks are made to enforce this.
        * theCellSize - cell size (in GeoCRS) which the layer should
            be resampled to. If not provided for a raster layer (i.e.
            theCellSize=None), the native raster cell size will be used.

    Returns:
        Path to the output clipped layer (placed in the
        system temp dir).

    Raises:
       Exception if input layer is a density layer in projected coordinates -
       see issue #123

    """
    if not theLayer or not theExtent:
        myMessage = tr('Layer or Extent passed to clip is None.')
        raise InvalidParameterException(myMessage)

    if theLayer.type() != QgsMapLayer.RasterLayer:
        myMessage = tr('Expected a raster layer but received a %s.' %
               str(theLayer.type()))
        raise InvalidParameterException(myMessage)

    myWorkingLayer = str(theLayer.source())

    # Check for existence of keywords file
    myKeywordsPath = myWorkingLayer[:-4] + '.keywords'
    myMessage = tr('Input file to be clipped "%s" does not have the '
           'expected keywords file %s' % (myWorkingLayer,
                                          myKeywordsPath))
    verify(os.path.isfile(myKeywordsPath), myMessage)

    # Raise exception if layer is projected and refers to density (issue #123)
    # FIXME (Ole): Need to deal with it - e.g. by automatically reprojecting
    # the layer at this point and setting the native resolution accordingly
    # in its keywords.
    myKeywords = readKeywordsFromFile(myKeywordsPath)
    if 'datatype' in myKeywords and myKeywords['datatype'] == 'density':
        if theLayer.srs().epsg() != 4326:

            # This layer is not WGS84 geographic
            myMessage = ('Layer %s represents density but has spatial '
                         'reference "%s". Density layers must be given in '
                         'WGS84 geographic coordinates, so please reproject '
                         'and try again. For more information, see issue '
                         'https://github.com/AIFDR/inasafe/issues/123'
                         % (myWorkingLayer, theLayer.srs().toProj4()))
            raise InvalidProjectionException(myMessage)

    # We need to provide gdalwarp with a dataset for the clip
    # because unline gdal_translate, it does not take projwin.
    myClipKml = extentToKml(theExtent)

    # Create a filename for the clipped, resampled and reprojected layer
    myHandle, myFilename = tempfile.mkstemp('.tif', 'clip_',
                                            temp_dir())
    os.close(myHandle)
    os.remove(myFilename)

    # If no cell size is specified, we need to run gdalwarp without
    # specifying the output pixel size to ensure the raster dims
    # remain consistent.
    if theCellSize is None:
        myCommand = ('gdalwarp -q -t_srs EPSG:4326 -r near '
                     '-cutline %s -crop_to_cutline -of GTiff '
                     '"%s" "%s"' % (myClipKml,
                                    myWorkingLayer,
                                    myFilename))
    else:
        myCommand = ('gdalwarp -q -t_srs EPSG:4326 -r near -tr %f %f '
                     '-cutline %s -crop_to_cutline -of GTiff '
                     '"%s" "%s"' % (theCellSize,
                                    theCellSize,
                                    myClipKml,
                                    myWorkingLayer,
                                    myFilename))
    myExecutablePrefix = ''
    if sys.platform == 'darwin':  # Mac OS X
        # .. todo:: FIXME - softcode gdal version in this path
        myExecutablePrefix = ('/Library/Frameworks/GDAL.framework/'
                              'Versions/1.9/Programs/')
    myCommand = myExecutablePrefix + myCommand

    LOGGER.debug(myCommand)
    myResult = QProcess().execute(myCommand)

    # For QProcess exit codes see
    # http://qt-project.org/doc/qt-4.8/qprocess.html#execute
    if myResult == -2:  # cannot be started
        myMessageDetail = tr('Process could not be started.')
        myMessage = tr('<p>Error while executing the following shell command:'
                     '</p><pre>%s</pre><p>Error message: %s'
                     % (myCommand, myMessageDetail))
        raise CallGDALError(myMessage)
    elif myResult == -1:  # process crashed
        myMessageDetail = tr('Process could not be started.')
        myMessage = tr('<p>Error while executing the following shell command:'
                       '</p><pre>%s</pre><p>Error message: %s'
                       % (myCommand, myMessageDetail))
        raise CallGDALError(myMessage)

    # .. todo:: Check the result of the shell call is ok
    myKeywordIO = KeywordIO()
    myKeywordIO.copyKeywords(theLayer, myFilename,
                             theExtraKeywords=theExtraKeywords)
    return myFilename  # Filename of created file
Пример #14
0
def _clipVectorLayer(theLayer, theExtent,
                     theExtraKeywords=None, explodeMultipart=True):
    """Clip a Hazard or Exposure layer to the
    extents of the current view frame. The layer must be a
    vector layer or an exception will be thrown.

    The output layer will always be in WGS84/Geographic.

    Args:

        * theLayer - a valid QGIS vector layer in EPSG:4326
        * theExtent -  an array representing the exposure layer
           extents in the form [xmin, ymin, xmax, ymax]. It is assumed
           that the coordinates are in EPSG:4326 although currently
           no checks are made to enforce this.
        * theExtraKeywords - any additional keywords over and above the
          original keywords that should be associated with the cliplayer.
        * explodeMultipart - a bool describing if to convert multipart
        features into singleparts

    Returns:
        Path to the output clipped layer (placed in the
        system temp dir).

    Raises:
       None

    """
    if not theLayer or not theExtent:
        myMessage = tr('Layer or Extent passed to clip is None.')
        raise InvalidParameterException(myMessage)

    if theLayer.type() != QgsMapLayer.VectorLayer:
        myMessage = tr('Expected a vector layer but received a %s.' %
                       str(theLayer.type()))
        raise InvalidParameterException(myMessage)

    #myHandle, myFilename = tempfile.mkstemp('.sqlite', 'clip_',
    #    temp_dir())
    myHandle, myFilename = tempfile.mkstemp('.shp', 'clip_',
                                            temp_dir())

    # Ensure the file is deleted before we try to write to it
    # fixes windows specific issue where you get a message like this
    # ERROR 1: c:\temp\inasafe\clip_jpxjnt.shp is not a directory.
    # This is because mkstemp creates the file handle and leaves
    # the file open.
    os.close(myHandle)
    os.remove(myFilename)

    # Get the clip extents in the layer's native CRS
    myGeoCrs = QgsCoordinateReferenceSystem()
    myGeoCrs.createFromId(4326, QgsCoordinateReferenceSystem.EpsgCrsId)
    myXForm = QgsCoordinateTransform(myGeoCrs, theLayer.crs())
    myRect = QgsRectangle(theExtent[0], theExtent[1],
        theExtent[2], theExtent[3])
    myProjectedExtent = myXForm.transformBoundingBox(myRect)

    # Get vector layer
    myProvider = theLayer.dataProvider()
    if myProvider is None:
        myMessage = tr('Could not obtain data provider from '
                       'layer "%s"' % theLayer.source())
        raise Exception(myMessage)

    # Get the layer field list, select by our extent then write to disk
    # .. todo:: FIXME - for different geometry types we should implement
    #    different clipping behaviour e.g. reject polygons that
    #    intersect the edge of the bbox. Tim
    myAttributes = myProvider.attributeIndexes()
    myFetchGeometryFlag = True
    myUseIntersectFlag = True
    myProvider.select(myAttributes,
        myProjectedExtent,
        myFetchGeometryFlag,
        myUseIntersectFlag)

    myFieldList = myProvider.fields()

    myWriter = QgsVectorFileWriter(myFilename,
        'UTF-8',
        myFieldList,
        theLayer.wkbType(),
        myGeoCrs,
        #'SQLite')  # FIXME (Ole): This works but is far too slow
        'ESRI Shapefile')
    if myWriter.hasError() != QgsVectorFileWriter.NoError:
        myMessage = tr('Error when creating shapefile: <br>Filename:'
                       '%s<br>Error: %s' %
                       (myFilename, myWriter.hasError()))
        raise Exception(myMessage)

    # Reverse the coordinate xform now so that we can convert
    # geometries from layer crs to geocrs.
    myXForm = QgsCoordinateTransform(theLayer.crs(), myGeoCrs)
    # Retrieve every feature with its geometry and attributes
    myFeature = QgsFeature()
    myCount = 0
    while myProvider.nextFeature(myFeature):
        myGeometry = myFeature.geometry()
        # Loop through the parts adding them to the output file
        # we write out single part features unless explodeMultipart is False
        if explodeMultipart:
            myGeometryList = explodeMultiPartGeometry(myGeometry)
        else:
            myGeometryList = [myGeometry]

        for myPart in myGeometryList:
            myPart.transform(myXForm)
            myFeature.setGeometry(myPart)
            myWriter.addFeature(myFeature)
        myCount += 1
    del myWriter  # Flush to disk

    if myCount < 1:
        myMessage = tr('No features fall within the clip extents. '
                       'Try panning / zooming to an area containing data '
                       'and then try to run your analysis again.')
        raise NoFeaturesInExtentException(myMessage)

    myKeywordIO = KeywordIO()
    myKeywordIO.copyKeywords(theLayer, myFilename,
        theExtraKeywords=theExtraKeywords)

    return myFilename  # Filename of created file
Пример #15
0
def _clipRasterLayer(theLayer,
                     theExtent,
                     theCellSize=None,
                     theExtraKeywords=None):
    """Clip a Hazard or Exposure raster layer to the extents provided. The
    layer must be a raster layer or an exception will be thrown.

    .. note:: The extent *must* be in EPSG:4326.

    The output layer will always be in WGS84/Geographic.

    Args:

        * theLayer - a valid QGIS raster layer in EPSG:4326
        * theExtent either: an array representing the exposure layer
           extents in the form [xmin, ymin, xmax, ymax]. It is assumed
           that the coordinates are in EPSG:4326 although currently
           no checks are made to enforce this.
                    or: A QgsGeometry of type polygon. **Polygon clipping is
           currently only supported for vector datasets.**
        * theCellSize - cell size (in GeoCRS) which the layer should
            be resampled to. If not provided for a raster layer (i.e.
            theCellSize=None), the native raster cell size will be used.

    Returns:
        Path to the output clipped layer (placed in the
        system temp dir).

    Raises:
       Exception if input layer is a density layer in projected coordinates -
       see issue #123

    """
    if not theLayer or not theExtent:
        myMessage = tr('Layer or Extent passed to clip is None.')
        raise InvalidParameterError(myMessage)

    if theLayer.type() != QgsMapLayer.RasterLayer:
        myMessage = tr('Expected a raster layer but received a %s.' %
                       str(theLayer.type()))
        raise InvalidParameterError(myMessage)

    myWorkingLayer = str(theLayer.source())

    # Check for existence of keywords file
    myKeywordsPath = myWorkingLayer[:-4] + '.keywords'
    myMessage = tr('Input file to be clipped "%s" does not have the '
                   'expected keywords file %s' %
                   (myWorkingLayer, myKeywordsPath))
    verify(os.path.isfile(myKeywordsPath), myMessage)

    # Raise exception if layer is projected and refers to density (issue #123)
    # FIXME (Ole): Need to deal with it - e.g. by automatically reprojecting
    # the layer at this point and setting the native resolution accordingly
    # in its keywords.
    myKeywords = readKeywordsFromFile(myKeywordsPath)
    if 'datatype' in myKeywords and myKeywords['datatype'] == 'density':
        if str(theLayer.crs().authid()) != 'EPSG:4326':

            # This layer is not WGS84 geographic
            myMessage = ('Layer %s represents density but has spatial '
                         'reference "%s". Density layers must be given in '
                         'WGS84 geographic coordinates, so please reproject '
                         'and try again. For more information, see issue '
                         'https://github.com/AIFDR/inasafe/issues/123' %
                         (myWorkingLayer, theLayer.crs().toProj4()))
            raise InvalidProjectionError(myMessage)

    # We need to provide gdalwarp with a dataset for the clip
    # because unline gdal_translate, it does not take projwin.
    myClipKml = extentToKml(theExtent)

    # Create a filename for the clipped, resampled and reprojected layer
    myHandle, myFilename = tempfile.mkstemp('.tif', 'clip_', temp_dir())
    os.close(myHandle)
    os.remove(myFilename)

    # If no cell size is specified, we need to run gdalwarp without
    # specifying the output pixel size to ensure the raster dims
    # remain consistent.
    myBinaryList = which('gdalwarp')
    LOGGER.debug('Path for gdalwarp: %s' % myBinaryList)
    if len(myBinaryList) < 1:
        raise CallGDALError(tr('gdalwarp could not be found on your computer'))
    # Use the first matching gdalwarp found
    myBinary = myBinaryList[0]
    if theCellSize is None:
        myCommand = ('%s -q -t_srs EPSG:4326 -r near '
                     '-cutline %s -crop_to_cutline -of GTiff '
                     '"%s" "%s"' %
                     (myBinary, myClipKml, myWorkingLayer, myFilename))
    else:
        myCommand = ('%s -q -t_srs EPSG:4326 -r near -tr %f %f '
                     '-cutline %s -crop_to_cutline -of GTiff '
                     '"%s" "%s"' % (myBinary, theCellSize, theCellSize,
                                    myClipKml, myWorkingLayer, myFilename))

    LOGGER.debug(myCommand)
    myResult = QProcess().execute(myCommand)

    # For QProcess exit codes see
    # http://qt-project.org/doc/qt-4.8/qprocess.html#execute
    if myResult == -2:  # cannot be started
        myMessageDetail = tr('Process could not be started.')
        myMessage = tr('<p>Error while executing the following shell command:'
                       '</p><pre>%s</pre><p>Error message: %s' %
                       (myCommand, myMessageDetail))
        raise CallGDALError(myMessage)
    elif myResult == -1:  # process crashed
        myMessageDetail = tr('Process could not be started.')
        myMessage = tr('<p>Error while executing the following shell command:'
                       '</p><pre>%s</pre><p>Error message: %s' %
                       (myCommand, myMessageDetail))
        raise CallGDALError(myMessage)

    # .. todo:: Check the result of the shell call is ok
    myKeywordIO = KeywordIO()
    myKeywordIO.copyKeywords(theLayer,
                             myFilename,
                             theExtraKeywords=theExtraKeywords)
    return myFilename  # Filename of created file
Пример #16
0
def _clipVectorLayer(theLayer,
                     theExtent,
                     theExtraKeywords=None,
                     explodeMultipart=True):
    """Clip a Hazard or Exposure layer to the
    extents of the current view frame. The layer must be a
    vector layer or an exception will be thrown.

    The output layer will always be in WGS84/Geographic.

    Args:

        * theLayer - a valid QGIS vector layer in EPSG:4326
        * theExtent -  an array representing the exposure layer
           extents in the form [xmin, ymin, xmax, ymax]. It is assumed
           that the coordinates are in EPSG:4326 although currently
           no checks are made to enforce this.
        * theExtraKeywords - any additional keywords over and above the
          original keywords that should be associated with the cliplayer.
        * explodeMultipart - a bool describing if to convert multipart
        features into singleparts

    Returns:
        Path to the output clipped layer (placed in the
        system temp dir).

    Raises:
       None

    """
    if not theLayer or not theExtent:
        myMessage = tr('Layer or Extent passed to clip is None.')
        raise InvalidParameterException(myMessage)

    if theLayer.type() != QgsMapLayer.VectorLayer:
        myMessage = tr('Expected a vector layer but received a %s.' %
                       str(theLayer.type()))
        raise InvalidParameterException(myMessage)

    #myHandle, myFilename = tempfile.mkstemp('.sqlite', 'clip_',
    #    temp_dir())
    myHandle, myFilename = tempfile.mkstemp('.shp', 'clip_', temp_dir())

    # Ensure the file is deleted before we try to write to it
    # fixes windows specific issue where you get a message like this
    # ERROR 1: c:\temp\inasafe\clip_jpxjnt.shp is not a directory.
    # This is because mkstemp creates the file handle and leaves
    # the file open.
    os.close(myHandle)
    os.remove(myFilename)

    # Get the clip extents in the layer's native CRS
    myGeoCrs = QgsCoordinateReferenceSystem()
    myGeoCrs.createFromId(4326, QgsCoordinateReferenceSystem.EpsgCrsId)
    myXForm = QgsCoordinateTransform(myGeoCrs, theLayer.crs())
    myRect = QgsRectangle(theExtent[0], theExtent[1], theExtent[2],
                          theExtent[3])
    myProjectedExtent = myXForm.transformBoundingBox(myRect)

    # Get vector layer
    myProvider = theLayer.dataProvider()
    if myProvider is None:
        myMessage = tr('Could not obtain data provider from '
                       'layer "%s"' % theLayer.source())
        raise Exception(myMessage)

    # Get the layer field list, select by our extent then write to disk
    # .. todo:: FIXME - for different geometry types we should implement
    #    different clipping behaviour e.g. reject polygons that
    #    intersect the edge of the bbox. Tim
    myAttributes = myProvider.attributeIndexes()
    myFetchGeometryFlag = True
    myUseIntersectFlag = True
    myProvider.select(myAttributes, myProjectedExtent, myFetchGeometryFlag,
                      myUseIntersectFlag)

    myFieldList = myProvider.fields()

    myWriter = QgsVectorFileWriter(
        myFilename,
        'UTF-8',
        myFieldList,
        theLayer.wkbType(),
        myGeoCrs,
        #'SQLite')  # FIXME (Ole): This works but is far too slow
        'ESRI Shapefile')
    if myWriter.hasError() != QgsVectorFileWriter.NoError:
        myMessage = tr('Error when creating shapefile: <br>Filename:'
                       '%s<br>Error: %s' % (myFilename, myWriter.hasError()))
        raise Exception(myMessage)

    # Reverse the coordinate xform now so that we can convert
    # geometries from layer crs to geocrs.
    myXForm = QgsCoordinateTransform(theLayer.crs(), myGeoCrs)
    # Retrieve every feature with its geometry and attributes
    myFeature = QgsFeature()
    myCount = 0
    while myProvider.nextFeature(myFeature):
        myGeometry = myFeature.geometry()
        # Loop through the parts adding them to the output file
        # we write out single part features unless explodeMultipart is False
        if explodeMultipart:
            myGeometryList = explodeMultiPartGeometry(myGeometry)
        else:
            myGeometryList = [myGeometry]

        for myPart in myGeometryList:
            myPart.transform(myXForm)
            myFeature.setGeometry(myPart)
            myWriter.addFeature(myFeature)
        myCount += 1
    del myWriter  # Flush to disk

    if myCount < 1:
        myMessage = tr('No features fall within the clip extents. '
                       'Try panning / zooming to an area containing data '
                       'and then try to run your analysis again.')
        raise NoFeaturesInExtentException(myMessage)

    myKeywordIO = KeywordIO()
    myKeywordIO.copyKeywords(theLayer,
                             myFilename,
                             theExtraKeywords=theExtraKeywords)

    return myFilename  # Filename of created file
Пример #17
0
class KeywordsDialog(QtGui.QDialog, Ui_KeywordsDialogBase):
    """Dialog implementation class for the Risk In A Box keywords editor."""

    def __init__(self, parent, iface, theDock=None):
        """Constructor for the dialog.
        .. note:: In QtDesigner the advanced editor's predefined keywords
           list should be shown in english always, so when adding entries to
           cboKeyword, be sure to choose :safe_qgis:`Properties<<` and untick
           the :safe_qgis:`translatable` property.

        Args:
           * parent - parent widget of this dialog
           * iface - a Quantum GIS QGisAppInterface instance.
           * theDock - Optional dock widget instance that we can notify of
             changes to the keywords.

        Returns:
           not applicable
        Raises:
           no exceptions explicitly raised
        """

        QtGui.QDialog.__init__(self, parent)
        self.setupUi(self)
        self.setWindowTitle(self.tr(
                            'InaSAFE %s Keywords Editor' % __version__))
        self.keywordIO = KeywordIO()
        # note the keys should remain untranslated as we need to write
        # english to the keywords file. The keys will be written as user data
        # in the combo entries.
        # .. seealso:: http://www.voidspace.org.uk/python/odict.html
        self.standardExposureList = OrderedDict([('population [density]',
                                      self.tr('population [density]')),
                                     ('population [count]',
                                      self.tr('population [count]')),
                                     ('building',
                                      self.tr('building')),
                                     ('building [osm]',
                                      self.tr('building [osm]')),
                                     ('building [sigab]',
                                      self.tr('building [sigab]')),
                                     ('roads',
                                      self.tr('roads'))])
        self.standardHazardList = OrderedDict([('earthquake [MMI]',
                                    self.tr('earthquake [MMI]')),
                                     ('tsunami [m]',
                                      self.tr('tsunami [m]')),
                                     ('tsunami [wet/dry]',
                                      self.tr('tsunami [wet/dry]')),
                                     ('tsunami [feet]',
                                      self.tr('tsunami [feet]')),
                                     ('flood [m]',
                                      self.tr('flood [m]')),
                                     ('flood [wet/dry]',
                                      self.tr('flood [wet/dry]')),
                                     ('flood [feet]', self.tr('flood [feet]')),
                                     ('tephra [kg2/m2',
                                      self.tr('tephra [kg2/m2]'))])
        # Save reference to the QGIS interface and parent
        self.iface = iface
        self.parent = parent
        self.dock = theDock

        QtCore.QObject.connect(self.lstKeywords,
                               QtCore.SIGNAL("itemClicked(QListWidgetItem *)"),
                               self.makeKeyValueEditable)

        # Set up help dialog showing logic.
        self.helpDialog = None
        myButton = self.buttonBox.button(QtGui.QDialogButtonBox.Help)
        QtCore.QObject.connect(myButton, QtCore.SIGNAL('clicked()'),
                               self.showHelp)

        # set some inital ui state:
        self.pbnAdvanced.setChecked(True)
        self.pbnAdvanced.toggle()
        self.radPredefined.setChecked(True)
        self.adjustSize()
        #myButton = self.buttonBox.button(QtGui.QDialogButtonBox.Ok)
        #myButton.setEnabled(False)
        self.layer = self.iface.activeLayer()
        if self.layer:
            self.loadStateFromKeywords()

    def showHelp(self):
        """Load the help text for the keywords safe_qgis"""
        if not self.helpDialog:
            self.helpDialog = Help(self.iface.mainWindow(), 'keywords')
        self.helpDialog.show()

    # prevents actions being handled twice
    @pyqtSignature('bool')
    def on_pbnAdvanced_toggled(self, theFlag):
        """Automatic slot executed when the advanced button is toggled.

        .. note:: some of the behaviour for hiding widgets is done using
           the signal/slot editor in designer, so if you are trying to figure
           out how the interactions work, look there too!

        Args:
           theFlag - boolean indicating the new checked state of the button
        Returns:
           None.
        Raises:
           no exceptions explicitly raised."""
        if theFlag:
            self.pbnAdvanced.setText(self.tr('Hide advanced editor'))
        else:
            self.pbnAdvanced.setText(self.tr('Show advanced editor'))
        self.adjustSize()

    # prevents actions being handled twice
    @pyqtSignature('bool')
    def on_radHazard_toggled(self, theFlag):
        """Automatic slot executed when the hazard radio is toggled.

        Args:
           theFlag - boolean indicating the new checked state of the button
        Returns:
           None.
        Raises:
           no exceptions explicitly raised."""
        if not theFlag:
            return
        self.setCategory('hazard')
        self.updateControlsFromList()

    # prevents actions being handled twice
    @pyqtSignature('bool')
    def on_radExposure_toggled(self, theFlag):
        """Automatic slot executed when the hazard radio is toggled on.

        Args:
           theFlag - boolean indicating the new checked state of the button
        Returns:
           None.
        Raises:
           no exceptions explicitly raised."""
        if not theFlag:
            return
        self.setCategory('exposure')
        self.updateControlsFromList()

    # prevents actions being handled twice
    @pyqtSignature('int')
    def on_cboSubcategory_currentIndexChanged(self, theIndex=None):
        """Automatic slot executed when the subcategory is changed.

        When the user changes the subcategory, we will extract the
        subcategory and dataype or unit (depending on if it is a hazard
        or exposure subcategory) from the [] after the name.

        Args:
           None
        Returns:
           None.
        Raises:
           no exceptions explicitly raised."""
        del theIndex
        myItem = self.cboSubcategory.itemData(
                            self.cboSubcategory.currentIndex()).toString()
        myText = str(myItem)
        if myText == self.tr('Not Set'):
            self.removeItemByKey('subcategory')
            return
        myTokens = myText.split(' ')
        if len(myTokens) < 1:
            self.removeItemByKey('subcategory')
            return
        mySubcategory = myTokens[0]
        self.addListEntry('subcategory', mySubcategory)

        # Some subcategories e.g. roads have no units or datatype
        if len(myTokens) == 1:
            return
        myCategory = self.getValueForKey('category')
        if 'hazard' == myCategory:
            myUnits = myTokens[1].replace('[', '').replace(']', '')
            self.addListEntry('unit', myUnits)
        if 'exposure' == myCategory:
            myDataType = myTokens[1].replace('[', '').replace(']', '')
            self.addListEntry('datatype', myDataType)

    # prevents actions being handled twice
    def setSubcategoryList(self, theEntries, theSelectedItem=None):
        """Helper to populate the subcategory list based on category context.

        Args:

           * theEntries - an OrderedDict of subcategories. The dict entries
             should be ('earthquake', self.tr('earthquake')). See
             http://www.voidspace.org.uk/python/odict.html for info on
             OrderedDict.
           * theSelectedItem - optional parameter indicating which item
             should be selected in the combo. If the selected item is not
             in theList, it will be appended to it.

        Returns:
           None.
        Raises:
           no exceptions explicitly raised.
        """
        # To aoid triggering on_cboSubcategory_currentIndexChanged
        # we block signals from the combo while updating it
        self.cboSubcategory.blockSignals(True)
        self.cboSubcategory.clear()
        if (theSelectedItem is not None and
            theSelectedItem not in theEntries.values() and
            theSelectedItem not in theEntries.keys()):
            # Add it to the OrderedList
            theEntries[theSelectedItem] = theSelectedItem
        myIndex = 0
        mySelectedIndex = 0
        for myKey, myValue in theEntries.iteritems():
            if (myValue == theSelectedItem or
               myKey == theSelectedItem):
                mySelectedIndex = myIndex
            myIndex += 1
            self.cboSubcategory.addItem(myValue, myKey)
        self.cboSubcategory.setCurrentIndex(mySelectedIndex)
        self.cboSubcategory.blockSignals(False)

    # prevents actions being handled twice
    @pyqtSignature('')
    def on_pbnAddToList1_clicked(self):
        """Automatic slot executed when the pbnAddToList1 button is pressed.

        Args:
           None
        Returns:
           None.
        Raises:
           no exceptions explicitly raised."""
        if (not self.lePredefinedValue.text().isEmpty() and not
            self.cboKeyword.currentText().isEmpty()):
            myCurrentKey = self.tr(self.cboKeyword.currentText())
            myCurrentValue = self.lePredefinedValue.text()
            self.addListEntry(myCurrentKey, myCurrentValue)
            self.lePredefinedValue.setText('')
            self.updateControlsFromList()

    # prevents actions being handled twice
    @pyqtSignature('')
    def on_pbnAddToList2_clicked(self):
        """Automatic slot executed when the pbnAddToList2 button is pressed.

        Args:
           None
        Returns:
           None.
        Raises:
           no exceptions explicitly raised."""

        myCurrentKey = self.leKey.text()
        myCurrentValue = self.leValue.text()
        if myCurrentKey == 'category' and myCurrentValue == 'hazard':
            self.radHazard.blockSignals(True)
            self.radHazard.setChecked(True)
            self.setSubcategoryList(self.standardHazardList)
            self.radHazard.blockSignals(False)
        elif myCurrentKey == 'category' and myCurrentValue == 'exposure':
            self.radExposure.blockSignals(True)
            self.radExposure.setChecked(True)
            self.setSubcategoryList(self.standardExposureList)
            self.radExposure.blockSignals(False)
        elif myCurrentKey == 'category':
            #.. todo:: notify the user their category is invalid
            pass
        self.addListEntry(myCurrentKey, myCurrentValue)
        self.leKey.setText('')
        self.leValue.setText('')
        self.updateControlsFromList()

    # prevents actions being handled twice
    @pyqtSignature('')
    def on_pbnRemove_clicked(self):
        """Automatic slot executed when the pbnRemove button is pressed.

        It will remove any selected items in the keywords list.

        Args:
           None
        Returns:
           None.
        Raises:
           no exceptions explicitly raised."""
        for myItem in self.lstKeywords.selectedItems():
            self.lstKeywords.takeItem(self.lstKeywords.row(myItem))
        self.leKey.setText('')
        self.leValue.setText('')
        self.updateControlsFromList()

    def addListEntry(self, theKey, theValue):
        """Add an item to the keywords list given its key/value.

        The key and value must both be valid, non empty strings
        or an InvalidKVPException will be raised.

        If an entry with the same key exists, it's value will be
        replaced with theValue.

        It will add the current key/value pair to the list if it is not
        already present. The kvp will also be stored in the data of the
        listwidgetitem as a simple string delimited with a bar ('|').

        Args:

           * theKey - string representing the key part of the key
             value pair (kvp)
           * theValue - string representing the value part of the key
             value pair (kvp)

        Returns:
           None.
        Raises:
           no exceptions explicitly raised."""
        if theKey is None or theKey == '':
            return
        if theValue is None or theValue == '':
            return

        myMessage = ''
        if ':' in theKey:
            theKey = theKey.replace(':', '.')
            myMessage = self.tr('Colons are not allowed, replaced with "."')
        if ':' in theValue:
            theValue = theValue.replace(':', '.')
            myMessage = self.tr('Colons are not allowed, replaced with "."')
        if myMessage == '':
            self.lblMessage.setText('')
            self.lblMessage.hide()
        else:
            self.lblMessage.setText(myMessage)
            self.lblMessage.show()
        myItem = QtGui.QListWidgetItem(theKey + ':' + theValue)
        # we are going to replace, so remove it if it exists already
        self.removeItemByKey(theKey)
        myData = theKey + '|' + theValue
        myItem.setData(QtCore.Qt.UserRole, myData)
        self.lstKeywords.insertItem(0, myItem)

    def setCategory(self, theCategory):
        """Set the category radio button based on theCategory.

        Args:
           theCategory - a string which must be either 'hazard' or 'exposure'.
        Returns:
           False if the radio button could not be updated
        Raises:
           no exceptions explicitly raised."""
        # convert from QString if needed
        myCategory = str(theCategory)
        if self.getValueForKey('category') == myCategory:
            #nothing to do, go home
            return True
        if myCategory not in ['hazard', 'exposure']:
            # .. todo:: report an error to the user
            return False
        # Special case when category changes, we start on a new slate!

        if myCategory == 'hazard':
            # only cause a toggle if we actually changed the category
            # This will only really be apparent if user manually enters
            # category as a keyword
            self.reset()
            self.radHazard.blockSignals(True)
            self.radHazard.setChecked(True)
            self.radHazard.blockSignals(False)
            self.removeItemByKey('subcategory')
            self.removeItemByKey('datatype')
            self.addListEntry('category', 'hazard')
            myList = self.standardHazardList
            self.setSubcategoryList(myList)

        else:
            self.reset()
            self.radExposure.blockSignals(True)
            self.radExposure.setChecked(True)
            self.radExposure.blockSignals(False)
            self.removeItemByKey('subcategory')
            self.removeItemByKey('unit')
            self.addListEntry('category', 'exposure')
            myList = self.standardExposureList
            self.setSubcategoryList(myList)

        return True

    def reset(self, thePrimaryKeywordsOnlyFlag=True):
        """Reset all controls to a blank state.

        Args:
            thePrimaryKeywordsOnlyFlag - if True (the default), only
            reset Subcategory, datatype and units.
        Returns:
            None
        Raises:
           no exceptions explicitly raised."""

        self.cboSubcategory.clear()
        self.removeItemByKey('subcategory')
        self.removeItemByKey('datatype')
        self.removeItemByKey('unit')
        if not thePrimaryKeywordsOnlyFlag:
            # Clear everything else too
            self.lstKeywords.clear()
            self.leKey.clear()
            self.leValue.clear()
            self.lePredefinedValue.clear()
            self.leTitle.clear()

    def removeItemByKey(self, theKey):
        """Remove an item from the kvp list given its key.

        Args:
            theKey - key of item to be removed.
        Returns:
            None
        Raises:
           no exceptions explicitly raised."""
        for myCounter in range(self.lstKeywords.count()):
            myExistingItem = self.lstKeywords.item(myCounter)
            myText = myExistingItem.text()
            myTokens = myText.split(':')
            if len(myTokens) < 2:
                break
            myKey = myTokens[0]
            if myKey == theKey:
                # remove it since the key is already present
                self.lstKeywords.takeItem(myCounter)
                break

    def removeItemByValue(self, theValue):
        """Remove an item from the kvp list given its key.

        Args:
            theValue - value of item to be removed.
        Returns:
            None
        Raises:
           no exceptions explicitly raised."""
        for myCounter in range(self.lstKeywords.count()):
            myExistingItem = self.lstKeywords.item(myCounter)
            myText = myExistingItem.text()
            myTokens = myText.split(':')
            myValue = myTokens[1]
            if myValue == theValue:
                # remove it since the key is already present
                self.lstKeywords.takeItem(myCounter)
                break

    def getValueForKey(self, theKey):
        """Check if our key list contains a specific key,
        and return its value if present.

        Args:
           theKey- String representing the key to search for
        Returns:
           Value of key if matched otherwise none
        Raises:
           no exceptions explicitly raised."""
        for myCounter in range(self.lstKeywords.count()):
            myExistingItem = self.lstKeywords.item(myCounter)
            myText = myExistingItem.text()
            myTokens = myText.split(':')
            myKey = str(myTokens[0]).strip()
            myValue = str(myTokens[1]).strip()
            if myKey == theKey:
                return myValue
        return None

    def loadStateFromKeywords(self):
        """Set the ui state to match the keywords of the
           currently active layer.

        Args:
           None
        Returns:
           None.
        Raises:
           no exceptions explicitly raised."""
        # In case the layer has no keywords or any problem occurs reading them,
        # start with a blank slate so that subcategory gets populated nicely &
        # we will assume exposure to start with.
        myKeywords = {'category': 'exposure'}

        try:
            # Now read the layer with sub layer if needed
            myKeywords = self.keywordIO.readKeywords(self.layer)
        except (InvalidParameterException, HashNotFoundException):
            pass

        myLayerName = self.layer.name()
        if 'title' not in myKeywords:
            self.leTitle.setText(myLayerName)
        self.lblLayerName.setText(myLayerName)
        #if we have a category key, unpack it first so radio button etc get set
        if 'category' in myKeywords:
            self.setCategory(myKeywords['category'])
            myKeywords.pop('category')

        for myKey in myKeywords.iterkeys():
            self.addListEntry(myKey, myKeywords[myKey])
        # now make the rest of the safe_qgis reflect the list entries
        self.updateControlsFromList()

    def updateControlsFromList(self):
        """Set the ui state to match the keywords of the
           currently active layer.

        Args:
           None
        Returns:
           None.
        Raises:
           no exceptions explicitly raised."""
        mySubcategory = self.getValueForKey('subcategory')
        myUnits = self.getValueForKey('unit')
        myType = self.getValueForKey('datatype')
        myTitle = self.getValueForKey('title')
        if myTitle is not None:
            self.leTitle.setText(myTitle)
        elif self.layer is not None:
            myLayerName = self.layer.name()
            self.lblLayerName.setText(myLayerName)
        else:
            self.lblLayerName.setText('')

        if self.radExposure.isChecked():
            if mySubcategory is not None and myType is not None:
                self.setSubcategoryList(self.standardExposureList,
                                     mySubcategory + ' [' + myType + ']')
            elif mySubcategory is not None:
                self.setSubcategoryList(self.standardExposureList,
                                        mySubcategory)
            else:
                self.setSubcategoryList(self.standardExposureList,
                                        self.tr('Not Set'))
        else:
            if mySubcategory is not None and myUnits is not None:
                self.setSubcategoryList(self.standardHazardList,
                                     mySubcategory + ' [' + myUnits + ']')
            elif mySubcategory is not None:
                self.setSubcategoryList(self.standardHazardList,
                                        mySubcategory)
            else:
                self.setSubcategoryList(self.standardHazardList,
                                        self.tr('Not Set'))

    # prevents actions being handled twice
    @pyqtSignature('QString')
    def on_leTitle_textEdited(self, theText):
        """Update the keywords list whenver the user changes the title.
        This slot is not called is the title is changed programmatically.

        Args:
           None
        Returns:
           dict - a dictionary of keyword reflecting the state of the dialog.
        Raises:
           no exceptions explicitly raised."""
        self.addListEntry('title', str(theText))

    def getKeywords(self):
        """Obtain the state of the dialog as a keywords dict

        Args:
           None
        Returns:
           dict - a dictionary of keyword reflecting the state of the dialog.
        Raises:
           no exceptions explicitly raised."""
                #make sure title is listed
        if str(self.leTitle.text()) != '':
            self.addListEntry('title', str(self.leTitle.text()))

        myKeywords = {}
        for myCounter in range(self.lstKeywords.count()):
            myExistingItem = self.lstKeywords.item(myCounter)
            myText = myExistingItem.text()
            myTokens = myText.split(':')
            myKey = str(myTokens[0]).strip()
            myValue = str(myTokens[1]).strip()
            myKeywords[myKey] = myValue
        return myKeywords

    def accept(self):
        """Automatic slot executed when the ok button is pressed.

        It will write out the keywords for the layer that is active.

        Args:
           None
        Returns:
           None.
        Raises:
           no exceptions explicitly raised."""
        self.applyPendingChanges()
        myKeywords = self.getKeywords()
        try:
            self.keywordIO.writeKeywords(theLayer=self.layer,
                                         theKeywords=myKeywords)
        except InaSAFEError, e:
            QtGui.QMessageBox.warning(self, self.tr('InaSAFE'),
            ((self.tr('An error was encountered when saving the keywords:\n'
                      '%s' % str(getExceptionWithStacktrace(e))))))
        if self.dock is not None:
            self.dock.getLayers()
        self.close()
Пример #18
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)
Пример #19
0
    def __init__(self, parent, iface, theDock=None):
        """Constructor for the dialog.
        .. note:: In QtDesigner the advanced editor's predefined keywords
           list should be shown in english always, so when adding entries to
           cboKeyword, be sure to choose :safe_qgis:`Properties<<` and untick
           the :safe_qgis:`translatable` property.

        Args:
           * parent - parent widget of this dialog
           * iface - a Quantum GIS QGisAppInterface instance.
           * theDock - Optional dock widget instance that we can notify of
             changes to the keywords.

        Returns:
           not applicable
        Raises:
           no exceptions explicitly raised
        """

        QtGui.QDialog.__init__(self, parent)
        self.setupUi(self)
        self.setWindowTitle(self.tr('InaSAFE %s Keywords Editor' %
                                    __version__))
        self.keywordIO = KeywordIO()
        # note the keys should remain untranslated as we need to write
        # english to the keywords file. The keys will be written as user data
        # in the combo entries.
        # .. seealso:: http://www.voidspace.org.uk/python/odict.html
        self.standardExposureList = OrderedDict([
            ('population [density]', self.tr('population [density]')),
            ('population [count]', self.tr('population [count]')),
            ('building', self.tr('building')),
            ('building [osm]', self.tr('building [osm]')),
            ('building [sigab]', self.tr('building [sigab]')),
            ('roads', self.tr('roads'))
        ])
        self.standardHazardList = OrderedDict([
            ('earthquake [MMI]', self.tr('earthquake [MMI]')),
            ('tsunami [m]', self.tr('tsunami [m]')),
            ('tsunami [wet/dry]', self.tr('tsunami [wet/dry]')),
            ('tsunami [feet]', self.tr('tsunami [feet]')),
            ('flood [m]', self.tr('flood [m]')),
            ('flood [wet/dry]', self.tr('flood [wet/dry]')),
            ('flood [feet]', self.tr('flood [feet]')),
            ('tephra [kg2/m2', self.tr('tephra [kg2/m2]'))
        ])
        # Save reference to the QGIS interface and parent
        self.iface = iface
        self.parent = parent
        self.dock = theDock
        # Set up things for context help
        myButton = self.buttonBox.button(QtGui.QDialogButtonBox.Help)
        QtCore.QObject.connect(myButton, QtCore.SIGNAL('clicked()'),
                               self.showHelp)
        self.helpDialog = None
        # set some inital ui state:
        self.pbnAdvanced.setChecked(True)
        self.pbnAdvanced.toggle()
        self.radPredefined.setChecked(True)
        self.adjustSize()
        #myButton = self.buttonBox.button(QtGui.QDialogButtonBox.Ok)
        #myButton.setEnabled(False)
        self.layer = self.iface.activeLayer()
        if self.layer:
            self.loadStateFromKeywords()
Пример #20
0
class KeywordIOTest(unittest.TestCase):
    """Tests for reading and writing of raster and vector data
    """
    def setUp(self):
        self.keywordIO = KeywordIO()
        myUri = QgsDataSourceURI()
        myUri.setDatabase(os.path.join(TESTDATA, 'jk.sqlite'))
        myUri.setDataSource('', 'osm_buildings', 'Geometry')
        self.sqliteLayer = QgsVectorLayer(myUri.uri(), 'OSM Buildings',
                                          'spatialite')
        myHazardPath = os.path.join(HAZDATA, 'Shakemap_Padang_2009.asc')
        self.fileRasterLayer, myType = loadLayer(myHazardPath,
                                                 theDirectory=None)
        del myType
        self.fileVectorLayer, myType = loadLayer('Padang_WGS84.shp')
        del myType
        self.expectedSqliteKeywords = {
            'category': 'exposure',
            'datatype': 'OSM',
            'subcategory': 'building'
        }
        self.expectedVectorKeywords = {
            'category': 'exposure',
            'datatype': 'itb',
            'subcategory': 'structure'
        }
        self.expectedRasterKeywords = {
            'category': 'hazard',
            'source': 'USGS',
            'subcategory': 'earthquake',
            'unit': 'MMI',
            'title': ('An earthquake in Padang '
                      'like in 2009')
        }

    def tearDown(self):
        pass

    def test_getHashForDatasource(self):
        """Test we can reliably get a hash for a uri"""
        myHash = self.keywordIO.getHashForDatasource(PG_URI)
        myExpectedHash = '7cc153e1b119ca54a91ddb98a56ea95e'
        myMessage = "Got: %s\nExpected: %s" % (myHash, myExpectedHash)
        assert myHash == myExpectedHash, myMessage

    def test_writeReadKeywordFromUri(self):
        """Test we can set and get keywords for a non local datasource"""
        myHandle, myFilename = tempfile.mkstemp('.db', 'keywords_', temp_dir())

        # Ensure the file is deleted before we try to write to it
        # fixes windows specific issue where you get a message like this
        # ERROR 1: c:\temp\inasafe\clip_jpxjnt.shp is not a directory.
        # This is because mkstemp creates the file handle and leaves
        # the file open.
        os.close(myHandle)
        os.remove(myFilename)
        myExpectedKeywords = {
            'category': 'exposure',
            'datatype': 'itb',
            'subcategory': 'building'
        }
        # SQL insert test
        # On first write schema is empty and there is no matching hash
        self.keywordIO.setKeywordDbPath(myFilename)
        self.keywordIO.writeKeywordsForUri(PG_URI, myExpectedKeywords)
        # SQL Update test
        # On second write schema is populated and we update matching hash
        myExpectedKeywords = {
            'category': 'exposure',
            'datatype': 'OSM',  # <--note the change here!
            'subcategory': 'building'
        }
        self.keywordIO.writeKeywordsForUri(PG_URI, myExpectedKeywords)
        # Test getting all keywords
        myKeywords = self.keywordIO.readKeywordFromUri(PG_URI)
        myMessage = 'Got: %s\n\nExpected %s\n\nDB: %s' % (
            myKeywords, myExpectedKeywords, myFilename)
        assert myKeywords == myExpectedKeywords, myMessage
        # Test getting just a single keyword
        myKeyword = self.keywordIO.readKeywordFromUri(PG_URI, 'datatype')
        myExpectedKeyword = 'OSM'
        myMessage = 'Got: %s\n\nExpected %s\n\nDB: %s' % (
            myKeyword, myExpectedKeyword, myFilename)
        assert myKeyword == myExpectedKeyword, myMessage
        # Test deleting keywords actually does delete
        self.keywordIO.deleteKeywordsForUri(PG_URI)
        try:
            myKeyword = self.keywordIO.readKeywordFromUri(PG_URI, 'datatype')
            #if the above didnt cause an exception then bad
            myMessage = 'Expected a HashNotFoundException to be raised'
            assert myMessage
        except HashNotFoundException:
            #we expect this outcome so good!
            pass

    def test_areKeywordsFileBased(self):
        """Can we correctly determine if keywords should be written to file or
        to database?"""
        assert not self.keywordIO.areKeywordsFileBased(self.sqliteLayer)
        assert self.keywordIO.areKeywordsFileBased(self.fileRasterLayer)
        assert self.keywordIO.areKeywordsFileBased(self.fileVectorLayer)

    def test_readRasterFileKeywords(self):
        """Can we read raster file keywords using generic readKeywords method
        """
        myKeywords = self.keywordIO.readKeywords(self.fileRasterLayer)
        myExpectedKeywords = self.expectedRasterKeywords
        mySource = self.fileRasterLayer.source()
        myMessage = 'Got:\n%s\nExpected:\n%s\nSource:\n%s' % (
            myKeywords, myExpectedKeywords, mySource)
        assert myKeywords == myExpectedKeywords, myMessage

    def test_readVectorFileKeywords(self):
        """Can we read vector file keywords with the generic readKeywords
        method """
        myKeywords = self.keywordIO.readKeywords(self.fileVectorLayer)
        myExpectedKeywords = self.expectedVectorKeywords
        mySource = self.fileVectorLayer.source()
        myMessage = 'Got: %s\n\nExpected %s\n\nSource: %s' % (
            myKeywords, myExpectedKeywords, mySource)
        assert myKeywords == myExpectedKeywords, myMessage

    def test_appendKeywords(self):
        """Can we append file keywords with the generic readKeywords method."""
        myLayer, _ = makePadangLayerClone()
        myNewKeywords = {'category': 'exposure', 'test': 'TEST'}
        self.keywordIO.appendKeywords(myLayer, myNewKeywords)
        myKeywords = self.keywordIO.readKeywords(myLayer)

        for myKey, myValue in myNewKeywords.iteritems():
            myMessage = ('Layer keywords misses appended key: %s\n'
                         'Layer keywords:\n%s\n'
                         'Appended keywords:\n%s\n' %
                         (myKey, myKeywords, myNewKeywords))
            assert myKey in myKeywords, myMessage
            myMessage = ('Layer keywords misses appended value: %s\n'
                         'Layer keywords:\n%s\n'
                         'Appended keywords:\n%s\n' %
                         (myValue, myKeywords, myNewKeywords))
            assert myKeywords[myKey] == myValue, myMessage

    def test_readDBKeywords(self):
        """Can we read sqlite keywords with the generic readKeywords method
        """
        myLocalPath = os.path.join(os.path.dirname(__file__), '..',
                                   'jk.sqlite')
        myPath = os.path.join(TESTDATA, 'test_keywords.db')
        self.keywordIO.setKeywordDbPath(myPath)
        # We need to make a local copy of the dataset so
        # that we can use a local path that will hash properly on the
        # database to return us the correct / valid keywords record.
        shutil.copy2(os.path.join(TESTDATA, 'jk.sqlite'), myLocalPath)
        myUri = QgsDataSourceURI()
        # always use relative path!
        myUri.setDatabase('../jk.sqlite')
        myUri.setDataSource('', 'osm_buildings', 'Geometry')
        # create a local version that has the relative url
        mySqliteLayer = QgsVectorLayer(myUri.uri(), 'OSM Buildings',
                                       'spatialite')
        myExpectedSource = ('dbname=\'../jk.sqlite\' table="osm_buildings"'
                            ' (Geometry) sql=')
        myMessage = 'Got source: %s\n\nExpected %s\n' % (mySqliteLayer.source,
                                                         myExpectedSource)
        assert mySqliteLayer.source() == myExpectedSource, myMessage
        myKeywords = self.keywordIO.readKeywords(mySqliteLayer)
        myExpectedKeywords = self.expectedSqliteKeywords
        assert myKeywords == myExpectedKeywords, myMessage
        mySource = self.sqliteLayer.source()
        os.remove(myLocalPath)
        myMessage = 'Got: %s\n\nExpected %s\n\nSource: %s' % (
            myKeywords, myExpectedKeywords, mySource)
        assert myKeywords == myExpectedKeywords, myMessage
Пример #21
0
class KeywordsDialog(QtGui.QDialog, Ui_KeywordsDialogBase):
    """Dialog implementation class for the Risk In A Box keywords editor."""

    def __init__(self, parent, iface, theDock=None, theLayer=None):
        """Constructor for the dialog.
        .. note:: In QtDesigner the advanced editor's predefined keywords
           list should be shown in english always, so when adding entries to
           cboKeyword, be sure to choose :safe_qgis:`Properties<<` and untick
           the :safe_qgis:`translatable` property.

        Args:
           * parent - parent widget of this dialog
           * iface - a Quantum GIS QGisAppInterface instance.
           * theDock - Optional dock widget instance that we can notify of
             changes to the keywords.

        Returns:
           not applicable
        Raises:
           no exceptions explicitly raised
        """

        QtGui.QDialog.__init__(self, parent)
        self.setupUi(self)
        self.setWindowTitle(self.tr(
            'InaSAFE %1 Keywords Editor').arg(get_version()))
        self.keywordIO = KeywordIO()
        # note the keys should remain untranslated as we need to write
        # english to the keywords file. The keys will be written as user data
        # in the combo entries.
        # .. seealso:: http://www.voidspace.org.uk/python/odict.html
        self.standardExposureList = OrderedDict([('population',
                                      self.tr('population')),
                                     ('structure', self.tr('structure')),
                                     ('road', self.tr('road')),
                                     ('Not Set', self.tr('Not Set'))])
        self.standardHazardList = OrderedDict([('earthquake [MMI]',
                                    self.tr('earthquake [MMI]')),
                                   ('tsunami [m]', self.tr('tsunami [m]')),
                                   ('tsunami [wet/dry]',
                                    self.tr('tsunami [wet/dry]')),
                                   ('tsunami [feet]',
                                    self.tr('tsunami [feet]')),
                                   ('flood [m]', self.tr('flood [m]')),
                                   ('flood [wet/dry]',
                                    self.tr('flood [wet/dry]')),
                                   ('flood [feet]', self.tr('flood [feet]')),
                                   ('tephra [kg2/m2]',
                                    self.tr('tephra [kg2/m2]')),
                                   ('volcano', self.tr('volcano')),
                                   ('Not Set', self.tr('Not Set'))])
        # Save reference to the QGIS interface and parent
        self.iface = iface
        self.parent = parent
        self.dock = theDock

        QtCore.QObject.connect(self.lstKeywords,
                               QtCore.SIGNAL("itemClicked(QListWidgetItem *)"),
                               self.makeKeyValueEditable)

        # Set up help dialog showing logic.
        self.helpDialog = None
        myButton = self.buttonBox.button(QtGui.QDialogButtonBox.Help)
        QtCore.QObject.connect(myButton, QtCore.SIGNAL('clicked()'),
                               self.showHelp)

        # set some inital ui state:
        self.defaults = getDefaults()
        self.pbnAdvanced.setChecked(True)
        self.pbnAdvanced.toggle()
        self.radPredefined.setChecked(True)
        self.dsbFemaleRatioDefault.blockSignals(True)
        self.dsbFemaleRatioDefault.setValue(self.defaults[
                                            'FEM_RATIO'])
        self.dsbFemaleRatioDefault.blockSignals(False)
        #myButton = self.buttonBox.button(QtGui.QDialogButtonBox.Ok)
        #myButton.setEnabled(False)
        if theLayer is None:
            self.layer = self.iface.activeLayer()
        else:
            self.layer = theLayer
        if self.layer:
            self.loadStateFromKeywords()

        #add a reload from keywords button
        myButton = self.buttonBox.addButton(self.tr('Reload'),
                                            QtGui.QDialogButtonBox.ActionRole)
        QtCore.QObject.connect(myButton, QtCore.SIGNAL('clicked()'),
                               self.loadStateFromKeywords)

    def setLayer(self, theLayer):
        self.layer = theLayer
        self.loadStateFromKeywords()

    def showHelp(self):
        """Load the help text for the keywords safe_qgis"""
        if self.helpDialog:
            del self.helpDialog
        self.helpDialog = Help(self.iface.mainWindow(), 'keywords')

    def togglePostprocessingWidgets(self):
        LOGGER.debug('togglePostprocessingWidgets')
        isPostprocessingOn = self.radPostprocessing.isChecked()
        self.cboSubcategory.setVisible(not isPostprocessingOn)
        self.lblSubcategory.setVisible(not isPostprocessingOn)
        self.showAggregationAttribute(isPostprocessingOn)
        self.showFemaleRatioAttribute(isPostprocessingOn)
        self.showFemaleRatioDefault(isPostprocessingOn)

    def showAggregationAttribute(self, theFlag):
        theBox = self.cboAggregationAttribute
        theBox.blockSignals(True)
        theBox.clear()
        theBox.blockSignals(False)
        if theFlag:
            currentKeyword = self.getValueForKey(
                self.defaults['AGGR_ATTR_KEY'])
            fields, attributePosition = getLayerAttributeNames(self.layer,
                               [QtCore.QVariant.Int, QtCore.QVariant.String],
                               currentKeyword)
            theBox.addItems(fields)
            if attributePosition is None:
                theBox.setCurrentIndex(0)
            else:
                theBox.setCurrentIndex(attributePosition)

        theBox.setVisible(theFlag)
        self.lblAggregationAttribute.setVisible(theFlag)

    def showFemaleRatioAttribute(self, theFlag):
        theBox = self.cboFemaleRatioAttribute
        theBox.blockSignals(True)
        theBox.clear()
        theBox.blockSignals(False)
        if theFlag:
            currentKeyword = self.getValueForKey(
                self.defaults['FEM_RATIO_ATTR_KEY'])
            fields, attributePosition = getLayerAttributeNames(self.layer,
                                               [QtCore.QVariant.Double],
                                               currentKeyword)
            fields.insert(0, self.tr('Use default'))
            fields.insert(1, self.tr('Don\'t use'))
            theBox.addItems(fields)
            if currentKeyword == self.tr('Use default'):
                theBox.setCurrentIndex(0)
            elif currentKeyword == self.tr('Don\'t use'):
                theBox.setCurrentIndex(1)
            elif attributePosition is None:
                # currentKeyword was not found in the attribute table.
                # Use default
                theBox.setCurrentIndex(0)
            else:
                # + 2 is because we add use defaults and don't use
                theBox.setCurrentIndex(attributePosition + 2)
        theBox.setVisible(theFlag)
        self.lblFemaleRatioAttribute.setVisible(theFlag)

    def showFemaleRatioDefault(self, theFlag):
        theBox = self.dsbFemaleRatioDefault
        if theFlag:
            currentValue = self.getValueForKey(
                self.defaults['FEM_RATIO_KEY'])
            if currentValue is None:
                val = self.defaults['FEM_RATIO']
            else:
                val = float(currentValue)
            theBox.setValue(val)

        theBox.setVisible(theFlag)
        self.lblFemaleRatioDefault.setVisible(theFlag)

    # prevents actions being handled twice
    @pyqtSignature('int')
    def on_cboAggregationAttribute_currentIndexChanged(self, theIndex=None):
        del theIndex
        self.addListEntry(self.defaults['AGGR_ATTR_KEY'],
                          self.cboAggregationAttribute.currentText())

    # prevents actions being handled twice
    @pyqtSignature('int')
    def on_cboFemaleRatioAttribute_currentIndexChanged(self, theIndex=None):
        del theIndex
        text = self.cboFemaleRatioAttribute.currentText()
        if text == self.tr('Use default'):
            self.dsbFemaleRatioDefault.setEnabled(True)
            currentDefault = self.getValueForKey(
                self.defaults['FEM_RATIO_KEY'])
            if currentDefault is None:
                self.addListEntry(self.defaults['FEM_RATIO_KEY'],
                                  self.dsbFemaleRatioDefault.value())
        else:
            self.dsbFemaleRatioDefault.setEnabled(False)
            self.removeItemByKey(self.defaults['FEM_RATIO_KEY'])
        self.addListEntry(self.defaults['FEM_RATIO_ATTR_KEY'], text)

    # prevents actions being handled twice
    @pyqtSignature('double')
    def on_dsbFemaleRatioDefault_valueChanged(self, theValue):
        del theValue
        theBox = self.dsbFemaleRatioDefault
        if theBox.isEnabled():
            self.addListEntry(self.defaults['FEM_RATIO_KEY'],
                              theBox.value())

    # prevents actions being handled twice
    @pyqtSignature('bool')
    def on_pbnAdvanced_toggled(self, theFlag):
        """Automatic slot executed when the advanced button is toggled.

        .. note:: some of the behaviour for hiding widgets is done using
           the signal/slot editor in designer, so if you are trying to figure
           out how the interactions work, look there too!

        Args:
           theFlag - boolean indicating the new checked state of the button
        Returns:
           None.
        Raises:
           no exceptions explicitly raised."""
        self.toggleAdvanced(theFlag)

    def toggleAdvanced(self, theFlag):
        if theFlag:
            self.pbnAdvanced.setText(self.tr('Hide advanced editor'))
        else:
            self.pbnAdvanced.setText(self.tr('Show advanced editor'))
        self.grpAdvanced.setVisible(theFlag)
        self.resizeDialog()

    # prevents actions being handled twice
    @pyqtSignature('bool')
    def on_radHazard_toggled(self, theFlag):
        """Automatic slot executed when the hazard radio is toggled.

        Args:
           theFlag - boolean indicating the new checked state of the button
        Returns:
           None.
        Raises:
           no exceptions explicitly raised."""
        if not theFlag:
            return
        self.setCategory('hazard')
        self.updateControlsFromList()

    # prevents actions being handled twice
    @pyqtSignature('bool')
    def on_radExposure_toggled(self, theFlag):
        """Automatic slot executed when the hazard radio is toggled on.

        Args:
           theFlag - boolean indicating the new checked state of the button
        Returns:
           None.
        Raises:
           no exceptions explicitly raised."""
        if not theFlag:
            return
        self.setCategory('exposure')
        self.updateControlsFromList()

    # prevents actions being handled twice
    @pyqtSignature('bool')
    def on_radPostprocessing_toggled(self, theFlag):
        """Automatic slot executed when the hazard radio is toggled on.

        Args:
           theFlag - boolean indicating the new checked state of the button
        Returns:
           None.
        Raises:
           no exceptions explicitly raised."""
        if not theFlag:
            self.removeItemByKey(self.defaults['AGGR_ATTR_KEY'])
            self.removeItemByKey(self.defaults['FEM_RATIO_ATTR_KEY'])
            self.removeItemByKey(self.defaults['FEM_RATIO_KEY'])
            return
        self.setCategory('postprocessing')
        self.updateControlsFromList()

    # prevents actions being handled twice
    @pyqtSignature('int')
    def on_cboSubcategory_currentIndexChanged(self, theIndex=None):
        """Automatic slot executed when the subcategory is changed.

        When the user changes the subcategory, we will extract the
        subcategory and dataype or unit (depending on if it is a hazard
        or exposure subcategory) from the [] after the name.

        Args:
           None
        Returns:
           None.
        Raises:
           no exceptions explicitly raised."""
        del theIndex
        myItem = self.cboSubcategory.itemData(
            self.cboSubcategory.currentIndex()).toString()
        myText = str(myItem)
        # I found that myText is 'Not Set' for every language
        if myText == self.tr('Not Set') or myText == 'Not Set':
            self.removeItemByKey('subcategory')
            return
        myTokens = myText.split(' ')
        if len(myTokens) < 1:
            self.removeItemByKey('subcategory')
            return
        mySubcategory = myTokens[0]
        self.addListEntry('subcategory', mySubcategory)

        # Some subcategories e.g. roads have no units or datatype
        if len(myTokens) == 1:
            return
        if myTokens[1].find('[') < 0:
            return
        myCategory = self.getValueForKey('category')
        if 'hazard' == myCategory:
            myUnits = myTokens[1].replace('[', '').replace(']', '')
            self.addListEntry('unit', myUnits)
        if 'exposure' == myCategory:
            myDataType = myTokens[1].replace('[', '').replace(']', '')
            self.addListEntry('datatype', myDataType)
        # prevents actions being handled twice

    def setSubcategoryList(self, theEntries, theSelectedItem=None):
        """Helper to populate the subcategory list based on category context.

        Args:

           * theEntries - an OrderedDict of subcategories. The dict entries
             should be ('earthquake', self.tr('earthquake')). See
             http://www.voidspace.org.uk/python/odict.html for info on
             OrderedDict.
           * theSelectedItem - optional parameter indicating which item
             should be selected in the combo. If the selected item is not
             in theList, it will be appended to it.

        Returns:
           None.
        Raises:
           no exceptions explicitly raised.
        """
        # To aoid triggering on_cboSubcategory_currentIndexChanged
        # we block signals from the combo while updating it
        self.cboSubcategory.blockSignals(True)
        self.cboSubcategory.clear()
        if (theSelectedItem is not None and
            theSelectedItem not in theEntries.values() and
            theSelectedItem not in theEntries.keys()):
            # Add it to the OrderedList
            theEntries[theSelectedItem] = theSelectedItem
        myIndex = 0
        mySelectedIndex = 0
        for myKey, myValue in theEntries.iteritems():
            if (myValue == theSelectedItem or
                myKey == theSelectedItem):
                mySelectedIndex = myIndex
            myIndex += 1
            self.cboSubcategory.addItem(myValue, myKey)
        self.cboSubcategory.setCurrentIndex(mySelectedIndex)
        self.cboSubcategory.blockSignals(False)

    # prevents actions being handled twice
    @pyqtSignature('')
    def on_pbnAddToList1_clicked(self):
        """Automatic slot executed when the pbnAddToList1 button is pressed.

        Args:
           None
        Returns:
           None.
        Raises:
           no exceptions explicitly raised."""
        if (not self.lePredefinedValue.text().isEmpty() and not
        self.cboKeyword.currentText().isEmpty()):
            myCurrentKey = self.tr(self.cboKeyword.currentText())
            myCurrentValue = self.lePredefinedValue.text()
            self.addListEntry(myCurrentKey, myCurrentValue)
            self.lePredefinedValue.setText('')
            self.updateControlsFromList()

    # prevents actions being handled twice
    @pyqtSignature('')
    def on_pbnAddToList2_clicked(self):
        """Automatic slot executed when the pbnAddToList2 button is pressed.

        Args:
           None
        Returns:
           None.
        Raises:
           no exceptions explicitly raised."""

        myCurrentKey = self.leKey.text()
        myCurrentValue = self.leValue.text()
        if myCurrentKey == 'category' and myCurrentValue == 'hazard':
            self.radHazard.blockSignals(True)
            self.radHazard.setChecked(True)
            self.setSubcategoryList(self.standardHazardList)
            self.radHazard.blockSignals(False)
        elif myCurrentKey == 'category' and myCurrentValue == 'exposure':
            self.radExposure.blockSignals(True)
            self.radExposure.setChecked(True)
            self.setSubcategoryList(self.standardExposureList)
            self.radExposure.blockSignals(False)
        elif myCurrentKey == 'category':
            #.. todo:: notify the user their category is invalid
            pass
        self.addListEntry(myCurrentKey, myCurrentValue)
        self.leKey.setText('')
        self.leValue.setText('')
        self.updateControlsFromList()

    # prevents actions being handled twice
    @pyqtSignature('')
    def on_pbnRemove_clicked(self):
        """Automatic slot executed when the pbnRemove button is pressed.

        It will remove any selected items in the keywords list.

        Args:
           None
        Returns:
           None.
        Raises:
           no exceptions explicitly raised."""
        for myItem in self.lstKeywords.selectedItems():
            self.lstKeywords.takeItem(self.lstKeywords.row(myItem))
        self.leKey.setText('')
        self.leValue.setText('')
        self.updateControlsFromList()

    def addListEntry(self, theKey, theValue):
        """Add an item to the keywords list given its key/value.

        The key and value must both be valid, non empty strings
        or an InvalidKVPException will be raised.

        If an entry with the same key exists, it's value will be
        replaced with theValue.

        It will add the current key/value pair to the list if it is not
        already present. The kvp will also be stored in the data of the
        listwidgetitem as a simple string delimited with a bar ('|').

        Args:

           * theKey - string representing the key part of the key
             value pair (kvp)
           * theValue - string representing the value part of the key
             value pair (kvp)

        Returns:
           None.
        Raises:
           no exceptions explicitly raised."""
        if theKey is None or theKey == '':
            return
        if theValue is None or theValue == '':
            return

        # make sure that both key and value is string
        theKey = str(theKey)
        theValue = str(theValue)
        myMessage = ''
        if ':' in theKey:
            theKey = theKey.replace(':', '.')
            myMessage = self.tr('Colons are not allowed, replaced with "."')
        if ':' in theValue:
            theValue = theValue.replace(':', '.')
            myMessage = self.tr('Colons are not allowed, replaced with "."')
        if myMessage == '':
            self.lblMessage.setText('')
            self.lblMessage.hide()
        else:
            self.lblMessage.setText(myMessage)
            self.lblMessage.show()
        myItem = QtGui.QListWidgetItem(theKey + ':' + theValue)
        # We are going to replace, so remove it if it exists already
        self.removeItemByKey(theKey)
        myData = theKey + '|' + theValue
        myItem.setData(QtCore.Qt.UserRole, myData)
        self.lstKeywords.insertItem(0, myItem)

    def setCategory(self, theCategory):
        """Set the category radio button based on theCategory.

        Args:
           theCategory - a string which must be either 'hazard' or 'exposure'
            or 'postprocessing'.
        Returns:
           False if the radio button could not be updated
        Raises:
           no exceptions explicitly raised."""
        # convert from QString if needed
        myCategory = str(theCategory)
        if self.getValueForKey('category') == myCategory:
            #nothing to do, go home
            return True
        if myCategory not in ['hazard', 'exposure', 'postprocessing']:
            # .. todo:: report an error to the user
            return False
            # Special case when category changes, we start on a new slate!

        if myCategory == 'hazard':
            # only cause a toggle if we actually changed the category
            # This will only really be apparent if user manually enters
            # category as a keyword
            self.reset()
            self.radHazard.blockSignals(True)
            self.radHazard.setChecked(True)
            self.radHazard.blockSignals(False)
            self.removeItemByKey('subcategory')
            self.removeItemByKey('datatype')
            self.addListEntry('category', 'hazard')
            myList = self.standardHazardList
            self.setSubcategoryList(myList)

        elif myCategory == 'exposure':
            self.reset()
            self.radExposure.blockSignals(True)
            self.radExposure.setChecked(True)
            self.radExposure.blockSignals(False)
            self.removeItemByKey('subcategory')
            self.removeItemByKey('unit')
            self.addListEntry('category', 'exposure')
            myList = self.standardExposureList
            self.setSubcategoryList(myList)

        else:
            self.reset()
            self.radPostprocessing.blockSignals(True)
            self.radPostprocessing.setChecked(True)
            self.radPostprocessing.blockSignals(False)
            self.removeItemByKey('subcategory')
            self.addListEntry('category', 'postprocessing')

        return True

    def reset(self, thePrimaryKeywordsOnlyFlag=True):
        """Reset all controls to a blank state.

        Args:
            thePrimaryKeywordsOnlyFlag - if True (the default), only
            reset Subcategory, datatype and units.
        Returns:
            None
        Raises:
           no exceptions explicitly raised."""

        self.cboSubcategory.clear()
        self.removeItemByKey('subcategory')
        self.removeItemByKey('datatype')
        self.removeItemByKey('unit')
        if not thePrimaryKeywordsOnlyFlag:
            # Clear everything else too
            self.lstKeywords.clear()
            self.leKey.clear()
            self.leValue.clear()
            self.lePredefinedValue.clear()
            self.leTitle.clear()

    def removeItemByKey(self, theKey):
        """Remove an item from the kvp list given its key.

        Args:
            theKey - key of item to be removed.
        Returns:
            None
        Raises:
           no exceptions explicitly raised."""
        for myCounter in range(self.lstKeywords.count()):
            myExistingItem = self.lstKeywords.item(myCounter)
            myText = myExistingItem.text()
            myTokens = myText.split(':')
            if len(myTokens) < 2:
                break
            myKey = myTokens[0]
            if myKey == theKey:
                # remove it since the key is already present
                self.lstKeywords.takeItem(myCounter)
                break

    def removeItemByValue(self, theValue):
        """Remove an item from the kvp list given its key.

        Args:
            theValue - value of item to be removed.
        Returns:
            None
        Raises:
           no exceptions explicitly raised."""
        for myCounter in range(self.lstKeywords.count()):
            myExistingItem = self.lstKeywords.item(myCounter)
            myText = myExistingItem.text()
            myTokens = myText.split(':')
            myValue = myTokens[1]
            if myValue == theValue:
                # remove it since the key is already present
                self.lstKeywords.takeItem(myCounter)
                break

    def getValueForKey(self, theKey):
        """Check if our key list contains a specific key,
        and return its value if present.

        Args:
           theKey- String representing the key to search for
        Returns:
           Value of key if matched otherwise none
        Raises:
           no exceptions explicitly raised."""
        for myCounter in range(self.lstKeywords.count()):
            myExistingItem = self.lstKeywords.item(myCounter)
            myText = myExistingItem.text()
            myTokens = myText.split(':')
            myKey = str(myTokens[0]).strip()
            myValue = str(myTokens[1]).strip()
            if myKey == theKey:
                return myValue
        return None

    def loadStateFromKeywords(self):
        """Set the ui state to match the keywords of the
           currently active layer.

        Args:
           None
        Returns:
           None.
        Raises:
           no exceptions explicitly raised."""
        # In case the layer has no keywords or any problem occurs reading them,
        # start with a blank slate so that subcategory gets populated nicely &
        # we will assume exposure to start with.
        myKeywords = {'category': 'exposure'}

        try:
            # Now read the layer with sub layer if needed
            myKeywords = self.keywordIO.readKeywords(self.layer)
        except (InvalidParameterException, HashNotFoundException):
            pass

        myLayerName = self.layer.name()
        if 'title' not in myKeywords:
            self.leTitle.setText(myLayerName)
        self.lblLayerName.setText(self.tr('Keywords for %s' % myLayerName))
        #if we have a category key, unpack it first so radio button etc get set
        if 'category' in myKeywords:
            self.setCategory(myKeywords['category'])
            myKeywords.pop('category')

        for myKey in myKeywords.iterkeys():
            self.addListEntry(myKey, str(myKeywords[myKey]))

        # now make the rest of the safe_qgis reflect the list entries
        self.updateControlsFromList()

    def updateControlsFromList(self):
        """Set the ui state to match the keywords of the
           currently active layer.

        Args:
           None
        Returns:
           None.
        Raises:
           no exceptions explicitly raised."""
        mySubcategory = self.getValueForKey('subcategory')
        myUnits = self.getValueForKey('unit')
        myType = self.getValueForKey('datatype')
        myTitle = self.getValueForKey('title')
        if myTitle is not None:
            self.leTitle.setText(myTitle)
        elif self.layer is not None:
            myLayerName = self.layer.name()
            self.lblLayerName.setText(self.tr('Keywords for %s' % myLayerName))
        else:
            self.lblLayerName.setText('')

        if not isLayerPolygonal(self.layer):
            self.radPostprocessing.setEnabled(False)

        #adapt gui if we are in postprocessing category
        self.togglePostprocessingWidgets()

        if self.radExposure.isChecked():
            if mySubcategory is not None and myType is not None:
                self.setSubcategoryList(self.standardExposureList,
                                        mySubcategory + ' [' + myType + ']')
            elif mySubcategory is not None:
                self.setSubcategoryList(self.standardExposureList,
                                        mySubcategory)
            else:
                self.setSubcategoryList(self.standardExposureList,
                                        self.tr('Not Set'))
        elif self.radHazard.isChecked():
            if mySubcategory is not None and myUnits is not None:
                self.setSubcategoryList(self.standardHazardList,
                                        mySubcategory + ' [' + myUnits + ']')
            elif mySubcategory is not None:
                self.setSubcategoryList(self.standardHazardList,
                                        mySubcategory)
            else:
                self.setSubcategoryList(self.standardHazardList,
                                        self.tr('Not Set'))

        self.resizeDialog()

    def resizeDialog(self):
        QtCore.QCoreApplication.processEvents()
        LOGGER.debug('adjust ing dialog size')
        self.adjustSize()

    # prevents actions being handled twice
    @pyqtSignature('QString')
    def on_leTitle_textEdited(self, theText):
        """Update the keywords list whenver the user changes the title.
        This slot is not called is the title is changed programmatically.

        Args:
           None
        Returns:
           dict - a dictionary of keyword reflecting the state of the dialog.
        Raises:
           no exceptions explicitly raised."""
        self.addListEntry('title', str(theText))

    def getKeywords(self):
        """Obtain the state of the dialog as a keywords dict

        Args:
           None
        Returns:
           dict - a dictionary of keyword reflecting the state of the dialog.
        Raises:
           no exceptions explicitly raised."""
        #make sure title is listed
        if str(self.leTitle.text()) != '':
            self.addListEntry('title', str(self.leTitle.text()))

        myKeywords = {}
        for myCounter in range(self.lstKeywords.count()):
            myExistingItem = self.lstKeywords.item(myCounter)
            myText = myExistingItem.text()
            myTokens = myText.split(':')
            myKey = str(myTokens[0]).strip()
            myValue = str(myTokens[1]).strip()
            myKeywords[myKey] = myValue
        return myKeywords

    def accept(self):
        """Automatic slot executed when the ok button is pressed.

        It will write out the keywords for the layer that is active.

        Args:
           None
        Returns:
           None.
        Raises:
           no exceptions explicitly raised."""
        self.applyPendingChanges()
        myKeywords = self.getKeywords()
        try:
            self.keywordIO.writeKeywords(theLayer=self.layer,
                                         theKeywords=myKeywords)
        except InaSAFEError, e:
            QtGui.QMessageBox.warning(self, self.tr('InaSAFE'),
              ((self.tr('An error was encountered when saving the keywords:\n'
                        '%s' % str(getExceptionWithStacktrace(e))))))
        if self.dock is not None:
            self.dock.getLayers()
        self.done(QtGui.QDialog.Accepted)
Пример #22
0
    except CalledProcessError, e:
        myMessage = tr('<p>Error while executing the following shell command:'
                     '</p><pre>%s</pre><p>Error message: %s'
                     % (myCommand, str(e)))
        # shameless hack - see https://github.com/AIFDR/inasafe/issues/141
        if sys.platform == 'darwin':  # Mac OS X
            if 'Errno 4' in str(e):
                # continue as the error seems to be non critical
                pass
            else:
                raise Exception(myMessage)
        else:
            raise Exception(myMessage)

    # .. todo:: Check the result of the shell call is ok
    myKeywordIO = KeywordIO()
    myKeywordIO.copyKeywords(theLayer, myFilename,
                             theExtraKeywords=theExtraKeywords)
    return myFilename  # Filename of created file


def extentToKml(theExtent):
    """A helper to get a little kml doc for an extent so that
    we can use it with gdal warp for clipping."""

    myBottomLeftCorner = '%f,%f' % (theExtent[0], theExtent[1])
    myTopLeftCorner = '%f,%f' % (theExtent[0], theExtent[3])
    myTopRightCorner = '%f,%f' % (theExtent[2], theExtent[3])
    myBottomRightCorner = '%f,%f' % (theExtent[2], theExtent[1])
    myKml = ("""<?xml version="1.0" encoding="utf-8" ?>
<kml xmlns="http://www.opengis.net/kml/2.2">
Пример #23
0
    def __init__(self, parent, iface, theDock=None, theLayer=None):
        """Constructor for the dialog.
        .. note:: In QtDesigner the advanced editor's predefined keywords
           list should be shown in english always, so when adding entries to
           cboKeyword, be sure to choose :safe_qgis:`Properties<<` and untick
           the :safe_qgis:`translatable` property.

        Args:
           * parent - parent widget of this dialog
           * iface - a Quantum GIS QGisAppInterface instance.
           * theDock - Optional dock widget instance that we can notify of
             changes to the keywords.

        Returns:
           not applicable
        Raises:
           no exceptions explicitly raised
        """

        QtGui.QDialog.__init__(self, parent)
        self.setupUi(self)
        self.setWindowTitle(self.tr(
            'InaSAFE %1 Keywords Editor').arg(get_version()))
        self.keywordIO = KeywordIO()
        # note the keys should remain untranslated as we need to write
        # english to the keywords file. The keys will be written as user data
        # in the combo entries.
        # .. seealso:: http://www.voidspace.org.uk/python/odict.html
        self.standardExposureList = OrderedDict([('population',
                                      self.tr('population')),
                                     ('structure', self.tr('structure')),
                                     ('road', self.tr('road')),
                                     ('Not Set', self.tr('Not Set'))])
        self.standardHazardList = OrderedDict([('earthquake [MMI]',
                                    self.tr('earthquake [MMI]')),
                                   ('tsunami [m]', self.tr('tsunami [m]')),
                                   ('tsunami [wet/dry]',
                                    self.tr('tsunami [wet/dry]')),
                                   ('tsunami [feet]',
                                    self.tr('tsunami [feet]')),
                                   ('flood [m]', self.tr('flood [m]')),
                                   ('flood [wet/dry]',
                                    self.tr('flood [wet/dry]')),
                                   ('flood [feet]', self.tr('flood [feet]')),
                                   ('tephra [kg2/m2]',
                                    self.tr('tephra [kg2/m2]')),
                                   ('volcano', self.tr('volcano')),
                                   ('Not Set', self.tr('Not Set'))])
        # Save reference to the QGIS interface and parent
        self.iface = iface
        self.parent = parent
        self.dock = theDock

        QtCore.QObject.connect(self.lstKeywords,
                               QtCore.SIGNAL("itemClicked(QListWidgetItem *)"),
                               self.makeKeyValueEditable)

        # Set up help dialog showing logic.
        self.helpDialog = None
        myButton = self.buttonBox.button(QtGui.QDialogButtonBox.Help)
        QtCore.QObject.connect(myButton, QtCore.SIGNAL('clicked()'),
                               self.showHelp)

        # set some inital ui state:
        self.defaults = getDefaults()
        self.pbnAdvanced.setChecked(True)
        self.pbnAdvanced.toggle()
        self.radPredefined.setChecked(True)
        self.dsbFemaleRatioDefault.blockSignals(True)
        self.dsbFemaleRatioDefault.setValue(self.defaults[
                                            'FEM_RATIO'])
        self.dsbFemaleRatioDefault.blockSignals(False)
        #myButton = self.buttonBox.button(QtGui.QDialogButtonBox.Ok)
        #myButton.setEnabled(False)
        if theLayer is None:
            self.layer = self.iface.activeLayer()
        else:
            self.layer = theLayer
        if self.layer:
            self.loadStateFromKeywords()

        #add a reload from keywords button
        myButton = self.buttonBox.addButton(self.tr('Reload'),
                                            QtGui.QDialogButtonBox.ActionRole)
        QtCore.QObject.connect(myButton, QtCore.SIGNAL('clicked()'),
                               self.loadStateFromKeywords)
Пример #24
0
class OptionsDialog(QtGui.QDialog, Ui_OptionsDialogBase):
    """Options dialog for the InaSAFE plugin."""

    def __init__(self, parent, iface, theDock=None):
        """Constructor for the dialog.

        Args:
           * parent - parent widget of this dialog
           * iface - a Quantum GIS QGisAppInterface instance.
           * theDock - Optional dock widget instance that we can notify of
             changes to the keywords.

        Returns:
           not applicable
        Raises:
           no exceptions explicitly raised
        """

        QtGui.QDialog.__init__(self, parent)
        self.setupUi(self)
        self.setWindowTitle(self.tr('InaSAFE %s Options' % get_version()))
        # Save reference to the QGIS interface and parent
        self.iface = iface
        self.parent = parent
        self.dock = theDock
        self.helpDialog = None
        self.keywordIO = KeywordIO()
        # Set up things for context help
        myButton = self.buttonBox.button(QtGui.QDialogButtonBox.Help)
        QtCore.QObject.connect(myButton, QtCore.SIGNAL('clicked()'),
                               self.showHelp)
        self.grpNotImplemented.hide()
        self.adjustSize()
        self.restoreState()
        # hack prevent showing use thread visible and set it false see #557
        self.cbxUseThread.setChecked(True)
        self.cbxUseThread.setVisible(False)

    def restoreState(self):
        """
        Args: Reinstate the options based on the user's stored session info
            None
        Returns:
            None
        Raises:
        """
        mySettings = QtCore.QSettings()
        # myFlag = mySettings.value(
        #     'inasafe/useThreadingFlag', False).toBool()
        # hack set use thread to false see #557
        myFlag = False
        self.cbxUseThread.setChecked(myFlag)

        myFlag = mySettings.value(
            'inasafe/visibleLayersOnlyFlag', True).toBool()
        self.cbxVisibleLayersOnly.setChecked(myFlag)

        myFlag = mySettings.value(
            'inasafe/setLayerNameFromTitleFlag', True).toBool()
        self.cbxSetLayerNameFromTitle.setChecked(myFlag)

        myFlag = mySettings.value(
            'inasafe/setZoomToImpactFlag', True).toBool()
        self.cbxZoomToImpact.setChecked(myFlag)
        # whether exposure layer should be hidden after model completes
        myFlag = mySettings.value(
            'inasafe/setHideExposureFlag', False).toBool()
        self.cbxHideExposure.setChecked(myFlag)

        myFlag = mySettings.value(
            'inasafe/clipToViewport', True).toBool()
        self.cbxClipToViewport.setChecked(myFlag)

        myFlag = mySettings.value(
            'inasafe/clipHard', False).toBool()
        self.cbxClipHard.setChecked(myFlag)

        myFlag = mySettings.value(
            'inasafe/useSentry', False).toBool()
        self.cbxUseSentry.setChecked(myFlag)

        myFlag = mySettings.value(
            'inasafe/showPostProcLayers', False).toBool()
        self.cbxShowPostprocessingLayers.setChecked(myFlag)

        myRatio = mySettings.value(
            'inasafe/defaultFemaleRatio',
            DEFAULTS['FEM_RATIO']).toDouble()
        self.dsbFemaleRatioDefault.setValue(myRatio[0])

        myPath = mySettings.value(
            'inasafe/keywordCachePath',
            self.keywordIO.defaultKeywordDbPath()).toString()
        self.leKeywordCachePath.setText(myPath)

    def saveState(self):
        """
        Args: Store the options into the user's stored session info
            None
        Returns:
            None
        Raises:
        """
        mySettings = QtCore.QSettings()
        mySettings.setValue('inasafe/useThreadingFlag',
                            False)
        mySettings.setValue('inasafe/visibleLayersOnlyFlag',
                            self.cbxVisibleLayersOnly.isChecked())
        mySettings.setValue('inasafe/setLayerNameFromTitleFlag',
                            self.cbxSetLayerNameFromTitle.isChecked())
        mySettings.setValue('inasafe/setZoomToImpactFlag',
                            self.cbxZoomToImpact.isChecked())
        mySettings.setValue('inasafe/setHideExposureFlag',
                            self.cbxHideExposure.isChecked())
        mySettings.setValue('inasafe/clipToViewport',
                            self.cbxClipToViewport.isChecked())
        mySettings.setValue('inasafe/clipHard',
                            self.cbxClipHard.isChecked())
        mySettings.setValue('inasafe/useSentry',
                            self.cbxUseSentry.isChecked())
        mySettings.setValue('inasafe/showPostProcLayers',
                            self.cbxShowPostprocessingLayers.isChecked())
        mySettings.setValue('inasafe/defaultFemaleRatio',
                            self.dsbFemaleRatioDefault.value())
        mySettings.setValue('inasafe/keywordCachePath',
                            self.leKeywordCachePath.text())

    def showHelp(self):
        """Load the help text for the options safe_qgis"""
        if not self.helpDialog:
            self.helpDialog = Help(self.iface.mainWindow(), 'options')

    def accept(self):
        """Method invoked when ok button is clicked
        Args:
            None
        Returns:
            None
        Raises:
        """
        self.saveState()
        self.dock.readSettings()
        self.close()

    @pyqtSignature('')  # prevents actions being handled twice
    def on_toolKeywordCachePath_clicked(self):
        """Autoconnect slot activated when the select cache file tool button is
        clicked,
        Args:
            None
        Returns:
            None
        Raises:
            None
        """
        myFilename = QtGui.QFileDialog.getSaveFileName(
            self,
            self.tr('Set keyword cache file'),
            self.keywordIO.defaultKeywordDbPath(),
            self.tr('Sqlite DB File (*.db)'))
        self.leKeywordCachePath.setText(myFilename)
Пример #25
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, myLabelWidth, myLabelHeight  # -2 to push it up a little
        )
        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)
Пример #26
0
class OptionsDialog(QtGui.QDialog, Ui_OptionsDialogBase):
    """Options dialog for the InaSAFE plugin."""
    def __init__(self, parent, iface, theDock=None):
        """Constructor for the dialog.

        Args:
           * parent - parent widget of this dialog
           * iface - a Quantum GIS QGisAppInterface instance.
           * theDock - Optional dock widget instance that we can notify of
             changes to the keywords.

        Returns:
           not applicable
        Raises:
           no exceptions explicitly raised
        """

        QtGui.QDialog.__init__(self, parent)
        self.setupUi(self)
        self.setWindowTitle(self.tr('InaSAFE %s Options' % get_version()))
        # Save reference to the QGIS interface and parent
        self.iface = iface
        self.parent = parent
        self.dock = theDock
        self.helpDialog = None
        self.keywordIO = KeywordIO()
        # Set up things for context help
        myButton = self.buttonBox.button(QtGui.QDialogButtonBox.Help)
        QtCore.QObject.connect(myButton, QtCore.SIGNAL('clicked()'),
                               self.showHelp)
        self.grpNotImplemented.hide()
        self.adjustSize()
        self.restoreState()
        # hack prevent showing use thread visible and set it false see #557
        self.cbxUseThread.setChecked(True)
        self.cbxUseThread.setVisible(False)

    def restoreState(self):
        """
        Args: Reinstate the options based on the user's stored session info
            None
        Returns:
            None
        Raises:
        """
        mySettings = QtCore.QSettings()
        # myFlag = mySettings.value(
        #     'inasafe/useThreadingFlag', False).toBool()
        # hack set use thread to false see #557
        myFlag = False
        self.cbxUseThread.setChecked(myFlag)

        myFlag = mySettings.value('inasafe/visibleLayersOnlyFlag',
                                  True).toBool()
        self.cbxVisibleLayersOnly.setChecked(myFlag)

        myFlag = mySettings.value('inasafe/setLayerNameFromTitleFlag',
                                  True).toBool()
        self.cbxSetLayerNameFromTitle.setChecked(myFlag)

        myFlag = mySettings.value('inasafe/setZoomToImpactFlag', True).toBool()
        self.cbxZoomToImpact.setChecked(myFlag)
        # whether exposure layer should be hidden after model completes
        myFlag = mySettings.value('inasafe/setHideExposureFlag',
                                  False).toBool()
        self.cbxHideExposure.setChecked(myFlag)

        myFlag = mySettings.value('inasafe/clipToViewport', True).toBool()
        self.cbxClipToViewport.setChecked(myFlag)

        myFlag = mySettings.value('inasafe/clipHard', False).toBool()
        self.cbxClipHard.setChecked(myFlag)

        myFlag = mySettings.value('inasafe/useSentry', False).toBool()
        self.cbxUseSentry.setChecked(myFlag)

        myFlag = mySettings.value('inasafe/showPostProcLayers', False).toBool()
        self.cbxShowPostprocessingLayers.setChecked(myFlag)

        myRatio = mySettings.value('inasafe/defaultFemaleRatio',
                                   DEFAULTS['FEM_RATIO']).toDouble()
        self.dsbFemaleRatioDefault.setValue(myRatio[0])

        myPath = mySettings.value(
            'inasafe/keywordCachePath',
            self.keywordIO.defaultKeywordDbPath()).toString()
        self.leKeywordCachePath.setText(myPath)

    def saveState(self):
        """
        Args: Store the options into the user's stored session info
            None
        Returns:
            None
        Raises:
        """
        mySettings = QtCore.QSettings()
        mySettings.setValue('inasafe/useThreadingFlag', False)
        mySettings.setValue('inasafe/visibleLayersOnlyFlag',
                            self.cbxVisibleLayersOnly.isChecked())
        mySettings.setValue('inasafe/setLayerNameFromTitleFlag',
                            self.cbxSetLayerNameFromTitle.isChecked())
        mySettings.setValue('inasafe/setZoomToImpactFlag',
                            self.cbxZoomToImpact.isChecked())
        mySettings.setValue('inasafe/setHideExposureFlag',
                            self.cbxHideExposure.isChecked())
        mySettings.setValue('inasafe/clipToViewport',
                            self.cbxClipToViewport.isChecked())
        mySettings.setValue('inasafe/clipHard', self.cbxClipHard.isChecked())
        mySettings.setValue('inasafe/useSentry', self.cbxUseSentry.isChecked())
        mySettings.setValue('inasafe/showPostProcLayers',
                            self.cbxShowPostprocessingLayers.isChecked())
        mySettings.setValue('inasafe/defaultFemaleRatio',
                            self.dsbFemaleRatioDefault.value())
        mySettings.setValue('inasafe/keywordCachePath',
                            self.leKeywordCachePath.text())

    def showHelp(self):
        """Load the help text for the options safe_qgis"""
        if not self.helpDialog:
            self.helpDialog = Help(self.iface.mainWindow(), 'options')

    def accept(self):
        """Method invoked when ok button is clicked
        Args:
            None
        Returns:
            None
        Raises:
        """
        self.saveState()
        self.dock.readSettings()
        self.close()

    @pyqtSignature('')  # prevents actions being handled twice
    def on_toolKeywordCachePath_clicked(self):
        """Autoconnect slot activated when the select cache file tool button is
        clicked,
        Args:
            None
        Returns:
            None
        Raises:
            None
        """
        myFilename = QtGui.QFileDialog.getSaveFileName(
            self, self.tr('Set keyword cache file'),
            self.keywordIO.defaultKeywordDbPath(),
            self.tr('Sqlite DB File (*.db)'))
        self.leKeywordCachePath.setText(myFilename)
Пример #27
0
def _clipVectorLayer(theLayer,
                     theExtent,
                     theExtraKeywords=None,
                     theExplodeFlag=True,
                     theHardClipFlag=False):
    """Clip a Hazard or Exposure layer to the
    extents of the current view frame. The layer must be a
    vector layer or an exception will be thrown.

    The output layer will always be in WGS84/Geographic.

    Args:

        * theLayer - a valid QGIS vector layer in EPSG:4326
        * theExtent either: an array representing the exposure layer
           extents in the form [xmin, ymin, xmax, ymax]. It is assumed
           that the coordinates are in EPSG:4326 although currently
           no checks are made to enforce this.
                    or: A QgsGeometry of type polygon. **Polygon clipping is
           currently only supported for vector datasets.**
        * theExtraKeywords - any additional keywords over and above the
          original keywords that should be associated with the cliplayer.
        * theExplodeFlag - a bool specifying whether multipart features
            should be 'exploded' into singleparts.
        * theHardClipFlag - a bool specifying whether line and polygon features
            that extend beyond the extents should be clipped such that they
            are reduced in size to the part of the geometry that intersects
            the extent only. Default is False.

    Returns:
        Path to the output clipped layer (placed in the system temp dir).

    Raises:
       None

    """
    if not theLayer or not theExtent:
        myMessage = tr('Layer or Extent passed to clip is None.')
        raise InvalidParameterError(myMessage)

    if theLayer.type() != QgsMapLayer.VectorLayer:
        myMessage = tr('Expected a vector layer but received a %s.' %
                       str(theLayer.type()))
        raise InvalidParameterError(myMessage)

    #myHandle, myFilename = tempfile.mkstemp('.sqlite', 'clip_',
    #    temp_dir())
    myHandle, myFilename = tempfile.mkstemp('.shp', 'clip_',
                                            temp_dir())

    # Ensure the file is deleted before we try to write to it
    # fixes windows specific issue where you get a message like this
    # ERROR 1: c:\temp\inasafe\clip_jpxjnt.shp is not a directory.
    # This is because mkstemp creates the file handle and leaves
    # the file open.
    os.close(myHandle)
    os.remove(myFilename)

    # Get the clip extents in the layer's native CRS
    myGeoCrs = QgsCoordinateReferenceSystem()
    myGeoCrs.createFromId(4326, QgsCoordinateReferenceSystem.EpsgCrsId)
    myXForm = QgsCoordinateTransform(myGeoCrs, theLayer.crs())
    myAllowedClipTypes = [QGis.WKBPolygon, QGis.WKBPolygon25D]
    if type(theExtent) is list:
        myRect = QgsRectangle(
            theExtent[0], theExtent[1],
            theExtent[2], theExtent[3])
        # noinspection PyCallByClass
        myClipPolygon = QgsGeometry.fromRect(myRect)
    elif (type(theExtent) is QgsGeometry and
          theExtent.wkbType in myAllowedClipTypes):
        myRect = theExtent.boundingBox().toRectF()
        myClipPolygon = theExtent
    else:
        raise InvalidClipGeometryError(
            tr(
                'Clip geometry must be an extent or a single part'
                'polygon based geometry.'))

    myProjectedExtent = myXForm.transformBoundingBox(myRect)

    # Get vector layer
    myProvider = theLayer.dataProvider()
    if myProvider is None:
        myMessage = tr('Could not obtain data provider from '
                       'layer "%s"' % theLayer.source())
        raise Exception(myMessage)

    # Get the layer field list, select by our extent then write to disk
    # .. todo:: FIXME - for different geometry types we should implement
    #    different clipping behaviour e.g. reject polygons that
    #    intersect the edge of the bbox. Tim
    myAttributes = myProvider.attributeIndexes()
    myFetchGeometryFlag = True
    myUseIntersectFlag = True
    myProvider.select(
        myAttributes,
        myProjectedExtent,
        myFetchGeometryFlag,
        myUseIntersectFlag)

    myFieldList = myProvider.fields()

    myWriter = QgsVectorFileWriter(
        myFilename,
        'UTF-8',
        myFieldList,
        theLayer.wkbType(),
        myGeoCrs,
        #'SQLite')  # FIXME (Ole): This works but is far too slow
        'ESRI Shapefile')
    if myWriter.hasError() != QgsVectorFileWriter.NoError:
        myMessage = tr('Error when creating shapefile: <br>Filename:'
                       '%s<br>Error: %s' %
                       (myFilename, myWriter.hasError()))
        raise Exception(myMessage)

    # Reverse the coordinate xform now so that we can convert
    # geometries from layer crs to geocrs.
    myXForm = QgsCoordinateTransform(theLayer.crs(), myGeoCrs)
    # Retrieve every feature with its geometry and attributes
    myFeature = QgsFeature()
    myCount = 0
    while myProvider.nextFeature(myFeature):
        myGeometry = myFeature.geometry()
        # Loop through the parts adding them to the output file
        # we write out single part features unless theExplodeFlag is False
        if theExplodeFlag:
            myGeometryList = explodeMultiPartGeometry(myGeometry)
        else:
            myGeometryList = [myGeometry]

        for myPart in myGeometryList:
            myPart.transform(myXForm)
            if theHardClipFlag:
                # Remove any dangling bits so only intersecting area is
                # kept.
                myPart = clipGeometry(myClipPolygon, myPart)
            if myPart is None:
                continue
            myFeature.setGeometry(myPart)
            myWriter.addFeature(myFeature)
        myCount += 1
    del myWriter  # Flush to disk

    if myCount < 1:
        myMessage = tr('No features fall within the clip extents. '
                       'Try panning / zooming to an area containing data '
                       'and then try to run your analysis again.'
                       'If hazard and exposure data doesn\'t overlap '
                       'at all, it is not possible to do an analysis.'
                       'Another possibility is that the layers do overlap '
                       'but because they may have different spatial '
                       'references, they appear to be disjoint. '
                       'If this is the case, try to turn on reproject '
                       'on-the-fly in QGIS.')
        raise NoFeaturesInExtentError(myMessage)

    myKeywordIO = KeywordIO()
    myKeywordIO.copyKeywords(
        theLayer, myFilename, theExtraKeywords=theExtraKeywords)

    return myFilename  # Filename of created file
Пример #28
0
class MapLegend():
    """A class for creating a map legend."""
    def __init__(self, theLayer, theDpi=300):
        """Constructor for the Map Legend class.

        Args:
            * theLayer: QgsMapLayer object that the legend should be generated
                for.
            * theDpi: Optional DPI for generated legend image. Defaults to
                300 if not specified.
        Returns:
            None
        Raises:
            Any exceptions raised will be propagated.
        """
        LOGGER.debug('InaSAFE Map class initialised')
        self.legendImage = None
        self.layer = theLayer
        # how high each row of the legend should be
        self.legendIncrement = 42
        self.keywordIO = KeywordIO()
        self.legendFontSize = 8
        self.legendWidth = 900
        self.dpi = theDpi

    def tr(self, theString):
        """We implement this ourself 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('MapLegend', theString)

    def getLegend(self):
        """Examine the classes of the impact layer associated with this print
        job.

        .. note: This is a wrapper for the rasterLegend and vectorLegend
           methods.

        Args:
            None
        Returns:
            None
        Raises:
            An InvalidLegendLayer will be raised if a legend cannot be
            created from the layer.
        """
        LOGGER.debug('InaSAFE Map Legend getLegend called')
        if self.layer is None:
            myMessage = self.tr('Unable to make a legend when map generator '
                                'has no layer set.')
            raise LegendLayerError(myMessage)
        try:
            self.keywordIO.readKeywords(self.layer, 'impact_summary')
        except KeywordNotFoundError, e:
            myMessage = self.tr('This layer does not appear to be an impact '
                                'layer. Try selecting an impact layer in the '
                                'QGIS layers list or creating a new impact '
                                'scenario before using the print tool.'
                                '\nMessage: %s' % str(e))
            raise Exception(myMessage)
        if self.layer.type() == QgsMapLayer.VectorLayer:
            return self.getVectorLegend()
        else:
            return self.getRasterLegend()
Пример #29
0
def _clipVectorLayer(theLayer,
                     theExtent,
                     theExtraKeywords=None,
                     theExplodeFlag=True,
                     theHardClipFlag=False):
    """Clip a Hazard or Exposure layer to the
    extents of the current view frame. The layer must be a
    vector layer or an exception will be thrown.

    The output layer will always be in WGS84/Geographic.

    Args:

        * theLayer - a valid QGIS vector layer in EPSG:4326
        * theExtent either: an array representing the exposure layer
           extents in the form [xmin, ymin, xmax, ymax]. It is assumed
           that the coordinates are in EPSG:4326 although currently
           no checks are made to enforce this.
                    or: A QgsGeometry of type polygon. **Polygon clipping is
           currently only supported for vector datasets.**
        * theExtraKeywords - any additional keywords over and above the
          original keywords that should be associated with the cliplayer.
        * theExplodeFlag - a bool specifying whether multipart features
            should be 'exploded' into singleparts.
        * theHardClipFlag - a bool specifying whether line and polygon features
            that extend beyond the extents should be clipped such that they
            are reduced in size to the part of the geometry that intersects
            the extent only. Default is False.

    Returns:
        Path to the output clipped layer (placed in the system temp dir).

    Raises:
       None

    """
    if not theLayer or not theExtent:
        myMessage = tr('Layer or Extent passed to clip is None.')
        raise InvalidParameterError(myMessage)

    if theLayer.type() != QgsMapLayer.VectorLayer:
        myMessage = tr('Expected a vector layer but received a %s.' %
                       str(theLayer.type()))
        raise InvalidParameterError(myMessage)

    #myHandle, myFilename = tempfile.mkstemp('.sqlite', 'clip_',
    #    temp_dir())
    myHandle, myFilename = tempfile.mkstemp('.shp', 'clip_', temp_dir())

    # Ensure the file is deleted before we try to write to it
    # fixes windows specific issue where you get a message like this
    # ERROR 1: c:\temp\inasafe\clip_jpxjnt.shp is not a directory.
    # This is because mkstemp creates the file handle and leaves
    # the file open.
    os.close(myHandle)
    os.remove(myFilename)

    # Get the clip extents in the layer's native CRS
    myGeoCrs = QgsCoordinateReferenceSystem()
    myGeoCrs.createFromId(4326, QgsCoordinateReferenceSystem.EpsgCrsId)
    myXForm = QgsCoordinateTransform(myGeoCrs, theLayer.crs())
    myAllowedClipTypes = [QGis.WKBPolygon, QGis.WKBPolygon25D]
    if type(theExtent) is list:
        myRect = QgsRectangle(theExtent[0], theExtent[1], theExtent[2],
                              theExtent[3])
        # noinspection PyCallByClass
        myClipPolygon = QgsGeometry.fromRect(myRect)
    elif (type(theExtent) is QgsGeometry
          and theExtent.wkbType in myAllowedClipTypes):
        myRect = theExtent.boundingBox().toRectF()
        myClipPolygon = theExtent
    else:
        raise InvalidClipGeometryError(
            tr('Clip geometry must be an extent or a single part'
               'polygon based geometry.'))

    myProjectedExtent = myXForm.transformBoundingBox(myRect)

    # Get vector layer
    myProvider = theLayer.dataProvider()
    if myProvider is None:
        myMessage = tr('Could not obtain data provider from '
                       'layer "%s"' % theLayer.source())
        raise Exception(myMessage)

    # Get the layer field list, select by our extent then write to disk
    # .. todo:: FIXME - for different geometry types we should implement
    #    different clipping behaviour e.g. reject polygons that
    #    intersect the edge of the bbox. Tim
    myAttributes = myProvider.attributeIndexes()
    myFetchGeometryFlag = True
    myUseIntersectFlag = True
    myProvider.select(myAttributes, myProjectedExtent, myFetchGeometryFlag,
                      myUseIntersectFlag)

    myFieldList = myProvider.fields()

    myWriter = QgsVectorFileWriter(
        myFilename,
        'UTF-8',
        myFieldList,
        theLayer.wkbType(),
        myGeoCrs,
        #'SQLite')  # FIXME (Ole): This works but is far too slow
        'ESRI Shapefile')
    if myWriter.hasError() != QgsVectorFileWriter.NoError:
        myMessage = tr('Error when creating shapefile: <br>Filename:'
                       '%s<br>Error: %s' % (myFilename, myWriter.hasError()))
        raise Exception(myMessage)

    # Reverse the coordinate xform now so that we can convert
    # geometries from layer crs to geocrs.
    myXForm = QgsCoordinateTransform(theLayer.crs(), myGeoCrs)
    # Retrieve every feature with its geometry and attributes
    myFeature = QgsFeature()
    myCount = 0
    while myProvider.nextFeature(myFeature):
        myGeometry = myFeature.geometry()
        # Loop through the parts adding them to the output file
        # we write out single part features unless theExplodeFlag is False
        if theExplodeFlag:
            myGeometryList = explodeMultiPartGeometry(myGeometry)
        else:
            myGeometryList = [myGeometry]

        for myPart in myGeometryList:
            myPart.transform(myXForm)
            if theHardClipFlag:
                # Remove any dangling bits so only intersecting area is
                # kept.
                myPart = clipGeometry(myClipPolygon, myPart)
            if myPart is None:
                continue
            myFeature.setGeometry(myPart)
            myWriter.addFeature(myFeature)
        myCount += 1
    del myWriter  # Flush to disk

    if myCount < 1:
        myMessage = tr('No features fall within the clip extents. '
                       'Try panning / zooming to an area containing data '
                       'and then try to run your analysis again.'
                       'If hazard and exposure data doesn\'t overlap '
                       'at all, it is not possible to do an analysis.'
                       'Another possibility is that the layers do overlap '
                       'but because they may have different spatial '
                       'references, they appear to be disjoint. '
                       'If this is the case, try to turn on reproject '
                       'on-the-fly in QGIS.')
        raise NoFeaturesInExtentError(myMessage)

    myKeywordIO = KeywordIO()
    myKeywordIO.copyKeywords(theLayer,
                             myFilename,
                             theExtraKeywords=theExtraKeywords)

    return myFilename  # Filename of created file
Пример #30
0
    def __init__(self, parent, iface, theDock=None):
        """Constructor for the dialog.
        .. note:: In QtDesigner the advanced editor's predefined keywords
           list should be shown in english always, so when adding entries to
           cboKeyword, be sure to choose :safe_qgis:`Properties<<` and untick
           the :safe_qgis:`translatable` property.

        Args:
           * parent - parent widget of this dialog
           * iface - a Quantum GIS QGisAppInterface instance.
           * theDock - Optional dock widget instance that we can notify of
             changes to the keywords.

        Returns:
           not applicable
        Raises:
           no exceptions explicitly raised
        """

        QtGui.QDialog.__init__(self, parent)
        self.setupUi(self)
        self.setWindowTitle(self.tr(
                            'InaSAFE %s Keywords Editor' % __version__))
        self.keywordIO = KeywordIO()
        # note the keys should remain untranslated as we need to write
        # english to the keywords file. The keys will be written as user data
        # in the combo entries.
        # .. seealso:: http://www.voidspace.org.uk/python/odict.html
        self.standardExposureList = OrderedDict([('population [density]',
                                      self.tr('population [density]')),
                                     ('population [count]',
                                      self.tr('population [count]')),
                                     ('building',
                                      self.tr('building')),
                                     ('building [osm]',
                                      self.tr('building [osm]')),
                                     ('building [sigab]',
                                      self.tr('building [sigab]')),
                                     ('roads',
                                      self.tr('roads'))])
        self.standardHazardList = OrderedDict([('earthquake [MMI]',
                                    self.tr('earthquake [MMI]')),
                                     ('tsunami [m]',
                                      self.tr('tsunami [m]')),
                                     ('tsunami [wet/dry]',
                                      self.tr('tsunami [wet/dry]')),
                                     ('tsunami [feet]',
                                      self.tr('tsunami [feet]')),
                                     ('flood [m]',
                                      self.tr('flood [m]')),
                                     ('flood [wet/dry]',
                                      self.tr('flood [wet/dry]')),
                                     ('flood [feet]', self.tr('flood [feet]')),
                                     ('tephra [kg2/m2',
                                      self.tr('tephra [kg2/m2]'))])
        # Save reference to the QGIS interface and parent
        self.iface = iface
        self.parent = parent
        self.dock = theDock

        QtCore.QObject.connect(self.lstKeywords,
                               QtCore.SIGNAL("itemClicked(QListWidgetItem *)"),
                               self.makeKeyValueEditable)

        # Set up help dialog showing logic.
        self.helpDialog = None
        myButton = self.buttonBox.button(QtGui.QDialogButtonBox.Help)
        QtCore.QObject.connect(myButton, QtCore.SIGNAL('clicked()'),
                               self.showHelp)

        # set some inital ui state:
        self.pbnAdvanced.setChecked(True)
        self.pbnAdvanced.toggle()
        self.radPredefined.setChecked(True)
        self.adjustSize()
        #myButton = self.buttonBox.button(QtGui.QDialogButtonBox.Ok)
        #myButton.setEnabled(False)
        self.layer = self.iface.activeLayer()
        if self.layer:
            self.loadStateFromKeywords()
Пример #31
0
    except CalledProcessError, e:
        myMessage = tr('<p>Error while executing the following shell command:'
                       '</p><pre>%s</pre><p>Error message: %s' %
                       (myCommand, str(e)))
        # shameless hack - see https://github.com/AIFDR/inasafe/issues/141
        if sys.platform == 'darwin':  # Mac OS X
            if 'Errno 4' in str(e):
                # continue as the error seems to be non critical
                pass
            else:
                raise Exception(myMessage)
        else:
            raise Exception(myMessage)

    # .. todo:: Check the result of the shell call is ok
    myKeywordIO = KeywordIO()
    myKeywordIO.copyKeywords(theLayer,
                             myFilename,
                             theExtraKeywords=theExtraKeywords)
    return myFilename  # Filename of created file


def extentToKml(theExtent):
    """A helper to get a little kml doc for an extent so that
    we can use it with gdal warp for clipping."""

    myBottomLeftCorner = '%f,%f' % (theExtent[0], theExtent[1])
    myTopLeftCorner = '%f,%f' % (theExtent[0], theExtent[3])
    myTopRightCorner = '%f,%f' % (theExtent[2], theExtent[3])
    myBottomRightCorner = '%f,%f' % (theExtent[2], theExtent[1])
    myKml = ("""<?xml version="1.0" encoding="utf-8" ?>