def setUp(self): self.keyword_io = KeywordIO() uri = QgsDataSourceURI() uri.setDatabase(os.path.join(TESTDATA, 'jk.sqlite')) uri.setDataSource('', 'osm_buildings', 'Geometry') self.sqlite_layer = QgsVectorLayer( uri.uri(), 'OSM Buildings', 'spatialite') hazard_path = os.path.join(HAZDATA, 'Shakemap_Padang_2009.asc') self.raster_layer, layer_type = load_layer( hazard_path, directory=None) del layer_type self.vector_layer, layer_type = load_layer('Padang_WGS84.shp') del layer_type self.expected_sqlite_keywords = { 'category': 'exposure', 'datatype': 'OSM', 'subcategory': 'building'} self.expected_vector_keywords = { 'category': 'exposure', 'datatype': 'itb', 'subcategory': 'structure', 'title': 'Padang WGS84'} self.expected_raster_keywords = { 'category': 'hazard', 'source': 'USGS', 'subcategory': 'earthquake', 'unit': 'MMI', 'title': ('An earthquake in Padang ' 'like in 2009')}
def __init__(self, iface, dock=None, parent=None): """Constructor for the dialog. :param iface: A Quantum GIS QGisAppInterface instance. :type iface: QGisAppInterface :param parent: Parent widget of this dialog :type parent: QWidget :param dock: Optional dock widget instance that we can notify of changes to the keywords. :type dock: Dock """ 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 = dock 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.show_help) self.grpNotImplemented.hide() self.adjustSize() self.restore_state() # hack prevent showing use thread visible and set it false see #557 self.cbxUseThread.setChecked(True) self.cbxUseThread.setVisible(False)
def show_keywords_editor(self): """Show the keywords editor.""" # import here only so that it is AFTER i18n set up from safe_qgis.tools.keywords_dialog import KeywordsDialog # Next block is a fix for #776 if self.iface.activeLayer() is None: return try: keyword_io = KeywordIO() keyword_io.read_keywords(self.iface.activeLayer()) except UnsupportedProviderError: # noinspection PyUnresolvedReferences,PyCallByClass # noinspection PyTypeChecker,PyArgumentList QMessageBox.warning( None, self.tr('Unsupported layer type'), self.tr('The layer you have selected cannot be used for ' 'analysis because its data type is unsupported.')) return # End of fix for #776 # Fix for #793 except NoKeywordsFoundError: # we will create them from scratch in the dialog pass # End of fix for #793 # Fix for filtered-layer except InvalidParameterError, e: # noinspection PyTypeChecker,PyTypeChecker,PyArgumentList QMessageBox.warning(None, self.tr('Invalid Layer'), e.message) return
def __init__(self, iface): """Constructor for the Map class. :param iface: Reference to the QGIS iface object. :type iface: QgsAppInterface """ LOGGER.debug('InaSAFE Map class initialised') self.iface = iface self.layer = iface.activeLayer() self.keyword_io = KeywordIO() self.printer = None self.composition = None self.legend = None self.logo = ':/plugins/inasafe/bnpb_logo.png' self.template = ':/plugins/inasafe/inasafe.qpt' #self.page_width = 210 # width in mm #self.page_height = 297 # height in mm self.page_width = 0 # width in mm self.page_height = 0 # height in mm self.page_dpi = 300.0 #self.page_margin = 10 # margin in mm self.show_frames = False # intended for debugging use only self.page_margin = None #vertical spacing between elements self.vertical_spacing = None self.map_height = None self.mapWidth = None
def show_keywords_editor(self): """Show the keywords editor.""" # import here only so that it is AFTER i18n set up from safe_qgis.tools.keywords_dialog import KeywordsDialog # Next block is a fix for #776 if self.iface.activeLayer() is None: return try: keyword_io = KeywordIO() keyword_io.read_keywords(self.iface.activeLayer()) except UnsupportedProviderError: # noinspection PyUnresolvedReferences,PyCallByClass QMessageBox.warning( None, self.tr('Unsupported layer type'), self.tr('The layer you have selected cannot be used for ' 'analysis because its data type is unsupported.')) return # End of fix for #776 # Fix for #793 except NoKeywordsFoundError: # we will create them from scratch in the dialog pass # End of fix for #793 dialog = KeywordsDialog(self.iface.mainWindow(), self.iface, self.dock_widget) dialog.exec_() # modal
def show_keywords_editor(self): """Show the keywords editor.""" # import here only so that it is AFTER i18n set up from safe_qgis.tools.keywords_dialog import KeywordsDialog # Next block is a fix for #776 if self.iface.activeLayer() is None: return try: keyword_io = KeywordIO() keyword_io.read_keywords(self.iface.activeLayer()) except UnsupportedProviderError: # noinspection PyUnresolvedReferences,PyCallByClass QMessageBox.warning( None, self.tr('Unsupported layer type'), self.tr( 'The layer you have selected cannot be used for ' 'analysis because its data type is unsupported.')) return # End of fix for #776 # Fix for #793 except NoKeywordsFoundError: # we will create them from scratch in the dialog pass # End of fix for #793 dialog = KeywordsDialog( self.iface.mainWindow(), self.iface, self.dock_widget) dialog.exec_() # modal
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, _ = load_layer(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.read_keywords(myLayer) myPath = myHtmlRenderer.print_impact_table(myKeywords, filename=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 myExpectedSizes = [20936, # as rendered on linux ub 12.04 64 21523, # as rendered on linux ub 12.10 64 20605, # as rendered on linux ub 13.04 64 21527, # as rendered on Jenkins post 22 June 2013 377191, # as rendered on OSX 252699L, # as rendered on Windows 7 64 bit 251782L, # as rendered on Windows 8 64 bit amd 21491, # as rendered on Slackware64 14.0 ] print 'Output pdf to %s' % myPath self.assertIn(mySize, myExpectedSizes)
def __init__(self, iface, dock): """Constructor for the class.""" QDialog.__init__(self) # Class Member self.iface = iface self.dock = dock self.output_directory = None self.exposure_layer = None self.hazard_layer = None self.aggregation_layer = None self.function_id = None self.keyword_io = KeywordIO() # Calling some init methods self.restore_state()
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 setUp(self): """Fixture run before all tests""" self.maxDiff = None # show full diff for assert errors os.environ['LANG'] = 'en' DOCK.show_only_visible_layers_flag = True load_standard_layers() DOCK.cboHazard.setCurrentIndex(0) DOCK.cboExposure.setCurrentIndex(0) DOCK.cboFunction.setCurrentIndex(0) DOCK.run_in_thread_flag = False DOCK.show_only_visible_layers_flag = False DOCK.set_layer_from_title_flag = False DOCK.zoom_to_impact_flag = False DOCK.hide_exposure_flag = False DOCK.show_intermediate_layers = False set_jakarta_extent() self._keywordIO = KeywordIO() self._defaults = breakdown_defaults() # Set extent as Jakarta extent geo_crs = QgsCoordinateReferenceSystem() geo_crs.createFromSrid(4326) self.extent = extent_to_geo_array(CANVAS.extent(), geo_crs)
def __init__(self, iface, dock=None, parent=None): """Constructor for the dialog. :param iface: A Quantum GIS QGisAppInterface instance. :type iface: QGisAppInterface :param parent: Parent widget of this dialog :type parent: QWidget :param dock: Optional dock widget instance that we can notify of changes to the keywords. :type dock: Dock """ 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 = dock self.keywordIO = KeywordIO() # Set up things for context help myButton = self.buttonBox.button(QtGui.QDialogButtonBox.Help) myButton.clicked.connect(self.show_help) self.grpNotImplemented.hide() self.adjustSize() self.restore_state() # hack prevent showing use thread visible and set it false see #557 self.cbxUseThread.setChecked(True) self.cbxUseThread.setVisible(False)
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 __init__(self, aggregator): """Director for aggregation based operations. :param aggregator: Aggregator that will be used in conjunction with postprocessors. :type aggregator: Aggregator """ super(PostprocessorManager, self).__init__() # Aggregation / post processing related items self.output = {} self.keyword_io = KeywordIO() self.error_message = None self.aggregator = aggregator self.current_output_postprocessor = None self.attribute_title = None
def __init__(self, layer, name=None): """Create the wrapper :param layer: Qgis layer :type layer: QgsMapLayer :param name: A layer's name :type name: Basestring or None """ self.data = layer self.keyword_io = KeywordIO() self.keywords = self.keyword_io.read_keywords(layer) if name is None: try: self.name = self.get_keywords(key='title') except KeywordNotFoundError: pass
def test_print_impact_table(self): """Test that we can render html from impact table keywords.""" LOGGER.debug('InaSAFE HtmlRenderer testing printImpactTable') file_name = 'test_floodimpact.tif' layer, _ = load_layer(file_name) message = 'Layer is not valid: %s' % file_name self.assertTrue(layer.isValid(), message) page_dpi = 300 html_renderer = HtmlRenderer(page_dpi) path = unique_filename( prefix='impact_table', suffix='.pdf', dir=temp_dir('test')) keyword_io = KeywordIO() keywords = keyword_io.read_keywords(layer) path = html_renderer.print_impact_table(keywords, filename=path) message = 'Rendered output does not exist: %s' % path self.assertTrue(os.path.exists(path), message) # 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. size = os.stat(path).st_size expected_sizes = [ 20936, # as rendered on linux ub 12.04 64 21523, # as rendered on linux ub 12.10 64 20605, # as rendered on linux ub 13.04 64 13965, # as rendered on linux ub 13.10 64 14220, # as rendered on linux ub 13.04 64 MB 11085, # as rendered on linux ub 14.04 64 AG 17306, # as rendered on linux ub 14.04_64 TS 17127, # as rendered on linux ub 14.04_64 MB 17295, # as rendered on linux ub 14.04_64 IS 18665, # as rendered on Jenkins per 19 June 2014 377191, # as rendered on OSX 17556, # as rendered on Windows 7_32 16163L, # as rendered on Windows 7 64 bit Ultimate i3 251782L, # as rendered on Windows 8 64 bit amd 21491, # as rendered on Slackware64 14.0 18667, # as rendered on Linux Mint 14_64 ] print 'Output pdf to %s' % path self.assertIn(size, expected_sizes)
def __init__(self, iface, dock=None, parent=None): """Constructor for the dialog. :param iface: A Quantum GIS QGisAppInterface instance. :type iface: QGisAppInterface :param parent: Parent widget of this dialog :type parent: QWidget :param dock: Optional dock widget instance that we can notify of changes to the keywords. :type dock: Dock """ 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 = dock self.keyword_io = KeywordIO() self.defaults = get_defaults() # Set up things for context help button = self.buttonBox.button(QtGui.QDialogButtonBox.Help) button.clicked.connect(self.show_help) self.grpNotImplemented.hide() self.adjustSize() self.restore_state() # hack prevent showing use thread visible and set it false see #557 self.cbxUseThread.setChecked(True) self.cbxUseThread.setVisible(False) # Set up listener for various UI self.custom_org_logo_checkbox.toggled.connect( self.set_organisation_logo) self.custom_north_arrow_checkbox.toggled.connect(self.set_north_arrow) self.custom_templates_dir_checkbox.toggled.connect( self.set_templates_dir) self.custom_org_disclaimer_checkbox.toggled.connect( self.set_org_disclaimer)
def test_print_impact_table(self): """Test that we can render html from impact table keywords.""" LOGGER.debug('InaSAFE HtmlRenderer testing printImpactTable') file_name = 'test_floodimpact.tif' layer, _ = load_layer(file_name) message = 'Layer is not valid: %s' % file_name self.assertTrue(layer.isValid(), message) page_dpi = 300 html_renderer = HtmlRenderer(page_dpi) path = unique_filename(prefix='impact_table', suffix='.pdf', dir=temp_dir('test')) keyword_io = KeywordIO() keywords = keyword_io.read_keywords(layer) path = html_renderer.print_impact_table(keywords, filename=path) message = 'Rendered output does not exist: %s' % path self.assertTrue(os.path.exists(path), message) # 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. size = os.stat(path).st_size expected_sizes = [ 20936, # as rendered on linux ub 12.04 64 21523, # as rendered on linux ub 12.10 64 20605, # as rendered on linux ub 13.04 64 13965, # as rendered on linux ub 13.10 64 14220, # as rendered on linux ub 13.04 64 MB 11085, # as rendered on linux ub 14.04 64 AG 17306, # as rendered on linux ub 14.04_64 TS 17127, # as rendered on linux ub 14.04_64 MB 17295, # as rendered on linux ub 14.04_64 IS 18665, # as rendered on Jenkins per 19 June 2014 377191, # as rendered on OSX 17556, # as rendered on Windows 7_32 16163L, # as rendered on Windows 7 64 bit Ultimate i3 251782L, # as rendered on Windows 8 64 bit amd 21491, # as rendered on Slackware64 14.0 18667, # as rendered on Linux Mint 14_64 ] print 'Output pdf to %s' % path self.assertIn(size, expected_sizes)
def __init__(self, theAggregator): """Director for aggregation based operations. Args: theAggregationLayer: QgsMapLayer representing clipped aggregation. This will be converted to a memory layer inside this class. see self.aggregator.layer Returns: not applicable Raises: no exceptions explicitly raised """ super(PostprocessorManager, self).__init__() # Aggregation / post processing related items self.postProcessingOutput = {} self.keywordIO = KeywordIO() self.errorMessage = None self.aggregator = theAggregator
def setUp(self): """Fixture run before all tests""" self.maxDiff = None # show full diff for assert errors os.environ['LANG'] = 'en' DOCK.showOnlyVisibleLayersFlag = True load_standard_layers() DOCK.cboHazard.setCurrentIndex(0) DOCK.cboExposure.setCurrentIndex(0) DOCK.cboFunction.setCurrentIndex(0) DOCK.runInThreadFlag = False DOCK.showOnlyVisibleLayersFlag = False DOCK.setLayerNameFromTitleFlag = False DOCK.zoomToImpactFlag = False DOCK.hideExposureFlag = False DOCK.showIntermediateLayers = False set_jakarta_extent() self.keywordIO = KeywordIO() self.defaults = defaults()
def setUp(self): """Fixture run before all tests""" self.maxDiff = None # show full diff for assert errors os.environ['LANG'] = 'en' DOCK.show_only_visible_layers_flag = True load_standard_layers() DOCK.cboHazard.setCurrentIndex(0) DOCK.cboExposure.setCurrentIndex(0) DOCK.cboFunction.setCurrentIndex(0) DOCK.run_in_thread_flag = False DOCK.show_only_visible_layers_flag = False DOCK.set_layer_from_title_flag = False DOCK.zoom_to_impact_flag = False DOCK.hide_exposure_flag = False DOCK.show_intermediate_layers = False set_jakarta_extent() self.keywordIO = KeywordIO() self.defaults = breakdown_defaults()
def __init__(self, layer, dpi=300, legend_title=None, legend_notes=None, legend_units=None): """Constructor for the Map Legend class. :param layer: Layer that the legend should be generated for. :type layer: QgsMapLayer, QgsVectorLayer :param dpi: DPI for the generated legend image. Defaults to 300 if not specified. :type dpi: int :param legend_title: Title for the legend. :type legend_title: str :param legend_notes: Notes to display under the title. :type legend_notes: str :param legend_units: Units for the legend. :type legend_units: str """ LOGGER.debug('InaSAFE Map class initialised') self.legend_image = None self.layer = layer # how high each row of the legend should be self.legend_increment = 42 self.keyword_io = KeywordIO() self.legend_font_size = 8 self.legend_width = 900 self.dpi = dpi if legend_title is None: self.legend_title = self.tr('Legend') else: self.legend_title = legend_title self.legend_notes = legend_notes self.legend_units = legend_units
def __init__(self, iface): """Constructor for the Map class. :param iface: Reference to the QGIS iface object. :type iface: QgsAppInterface """ LOGGER.debug('InaSAFE Map class initialised') self.iface = iface self.layer = iface.activeLayer() self.keyword_io = KeywordIO() self.printer = None self.composition = None self.extent = iface.mapCanvas().extent() self.safe_logo = ':/plugins/inasafe/inasafe-logo-url.svg' self.north_arrow = ':/plugins/inasafe/simple_north_arrow.png' self.org_logo = ':/plugins/inasafe/supporters.png' self.template = ':/plugins/inasafe/inasafe-portrait-a4.qpt' self.disclaimer = disclaimer() self.page_width = 0 # width in mm self.page_height = 0 # height in mm self.page_dpi = 300.0 self.show_frames = False # intended for debugging use only
def show_keywords_editor(self): """Show the keywords editor.""" # import here only so that it is AFTER i18n set up from safe_qgis.tools.keywords_dialog import KeywordsDialog # Next block is a fix for #776 if self.iface.activeLayer() is None: return try: keyword_io = KeywordIO() keyword_io.read_keywords(self.iface.activeLayer()) except UnsupportedProviderError: # noinspection PyUnresolvedReferences,PyCallByClass # noinspection PyTypeChecker,PyArgumentList QMessageBox.warning( None, self.tr('Unsupported layer type'), self.tr( 'The layer you have selected cannot be used for ' 'analysis because its data type is unsupported.')) return # End of fix for #776 # Fix for #793 except NoKeywordsFoundError: # we will create them from scratch in the dialog pass # End of fix for #793 # Fix for filtered-layer except InvalidParameterError, e: # noinspection PyTypeChecker,PyTypeChecker QMessageBox.warning( None, self.tr('Invalid Layer'), e.message) return
def __init__(self, iface): """Constructor for the Map class. :param iface: Reference to the QGIS iface object. :type iface: QgsAppInterface """ LOGGER.debug('InaSAFE Map class initialised') self.iface = iface self.layer = iface.activeLayer() self.keywordIO = KeywordIO() self.printer = None self.composition = None self.legend = None self.pageWidth = 210 # width in mm self.pageHeight = 297 # height in mm self.pageDpi = 300.0 self.pageMargin = 10 # margin in mm self.verticalSpacing = 1 # vertical spacing between elements self.showFramesFlag = False # intended for debugging use only # make a square map where width = height = page width self.mapHeight = self.pageWidth - (self.pageMargin * 2) self.mapWidth = self.mapHeight self.disclaimer = self.tr('InaSAFE has been jointly developed by' ' BNPB, AusAid & the World Bank')
def __init__( self, iface, theAggregationLayer): """Director for aggregation based operations. Args: theAggregationLayer: QgsMapLayer representing clipped aggregation. This will be converted to a memory layer inside this class. see self.layer Returns: not applicable Raises: no exceptions explicitly raised """ QtCore.QObject.__init__(self) self.hazardLayer = None self.exposureLayer = None self.safeLayer = None self.prefix = 'aggr_' self.attributes = {} self.attributeTitle = None self.iface = iface self.keywordIO = KeywordIO() self.defaults = defaults() self.errorMessage = None self.targetField = None self.impactLayerAttributes = [] self.aoiMode = True # If this flag is not True, no aggregation or postprocessing will run # this is set as True by validateKeywords() self.isValid = False self.showIntermediateLayers = False # This is used to hold an *in memory copy* of the aggregation layer # or None if the clip extents should be used. if theAggregationLayer is None: self.aoiMode = True # Will be completed in _prepareLayer just before deintersect call self.layer = self._createPolygonLayer() else: self.aoiMode = False self.layer = theAggregationLayer
class QgisWrapper(): """Wrapper class to add keywords functionality to Qgis layers """ def __init__(self, layer, name=None): """Create the wrapper :param layer: Qgis layer :type layer: QgsMapLayer :param name: A layer's name :type name: Basestring or None """ self.data = layer self.keyword_io = KeywordIO() self.keywords = self.keyword_io.read_keywords(layer) if name is None: try: self.name = self.get_keywords(key='title') except KeywordNotFoundError: pass def get_name(self): return self.name def set_name(self, name): self.name = name def get_keywords(self, key=None): """Return a copy of the keywords dictionary Args: * key (optional): If specified value will be returned for key only """ if key is None: return self.keywords.copy() else: if key in self.keywords: return self.keywords[key] else: msg = ('Keyword %s does not exist in %s: Options are ' '%s' % (key, self.get_name(), self.keywords.keys())) raise KeywordNotFoundError(msg) def get_layer(self): return self.data
def __init__(self, iface): """Constructor for the Map class. :param iface: Reference to the QGIS iface object. :type iface: QgsAppInterface """ LOGGER.debug('InaSAFE Map class initialised') self.iface = iface self.layer = iface.activeLayer() self.keyword_io = KeywordIO() self.printer = None self.composition = None self.extent = iface.mapCanvas().extent() self.logo = ':/plugins/inasafe/bnpb_logo.png' self.template = ':/plugins/inasafe/inasafe.qpt' self.page_width = 0 # width in mm self.page_height = 0 # height in mm self.page_dpi = 300.0 self.show_frames = False # intended for debugging use only
def setUp(self): """Fixture run before all tests""" self.maxDiff = None # show full diff for assert errors os.environ['LANG'] = 'en' DOCK.show_only_visible_layers_flag = True load_standard_layers() DOCK.cboHazard.setCurrentIndex(0) DOCK.cboExposure.setCurrentIndex(0) DOCK.cboFunction.setCurrentIndex(0) DOCK.run_in_thread_flag = False DOCK.show_only_visible_layers_flag = False DOCK.set_layer_from_title_flag = False DOCK.zoom_to_impact_flag = False DOCK.hide_exposure_flag = False DOCK.show_intermediate_layers = False set_jakarta_extent() self._keywordIO = KeywordIO() self._defaults = breakdown_defaults()
def setUp(self): """Fixture run before all tests""" self.maxDiff = None # show full diff for assert errors os.environ['LANG'] = 'en' DOCK.showOnlyVisibleLayersFlag = True load_standard_layers() DOCK.cboHazard.setCurrentIndex(0) DOCK.cboExposure.setCurrentIndex(0) DOCK.cboFunction.setCurrentIndex(0) DOCK.runInThreadFlag = False DOCK.showOnlyVisibleLayersFlag = False DOCK.setLayerNameFromTitleFlag = False DOCK.zoomToImpactFlag = False DOCK.hideExposureFlag = False DOCK.showIntermediateLayers = False set_jakarta_extent() self.keywordIO = KeywordIO() self.defaults = breakdown_defaults()
def __init__( self, layer, dpi=300, legend_title=None, legend_notes=None, legend_units=None): """Constructor for the Map Legend class. :param layer: Layer that the legend should be generated for. :type layer: QgsMapLayer, QgsVectorLayer :param dpi: DPI for the generated legend image. Defaults to 300 if not specified. :type dpi: int :param legend_title: Title for the legend. :type legend_title: str :param legend_notes: Notes to display under the title. :type legend_notes: str :param legend_units: Units for the legend. :type legend_units: str """ LOGGER.debug('InaSAFE Map class initialised') self.legendImage = None self.layer = layer # how high each row of the legend should be self.legendIncrement = 42 self.keywordIO = KeywordIO() self.legendFontSize = 8 self.legendWidth = 900 self.dpi = dpi if legend_title is None: self.legendTitle = self.tr('Legend') else: self.legendTitle = legend_title self.legendNotes = legend_notes self.legendUnits = legend_units
def __init__( self, theAggregator): """Director for aggregation based operations. Args: theAggregationLayer: QgsMapLayer representing clipped aggregation. This will be converted to a memory layer inside this class. see self.aggregator.layer Returns: not applicable Raises: no exceptions explicitly raised """ super(PostprocessorManager, self).__init__() # Aggregation / post processing related items self.postProcessingOutput = {} self.keywordIO = KeywordIO() self.errorMessage = None self.aggregator = theAggregator
def __init__(self, iface): """Constructor for the Map class. :param iface: Reference to the QGIS iface object. :type iface: QgsAppInterface """ LOGGER.debug('InaSAFE Map class initialised') self.iface = iface self.layer = iface.activeLayer() self.keyword_io = KeywordIO() self.printer = None self.composition = None self.extent = iface.mapCanvas().extent() self.safe_logo = ':/plugins/inasafe/inasafe-logo-url.svg' self.north_arrow = default_north_arrow_path() self.org_logo = default_organisation_logo_path() self.template = ':/plugins/inasafe/inasafe-portrait-a4.qpt' self.disclaimer = disclaimer() self.page_width = 0 # width in mm self.page_height = 0 # height in mm self.page_dpi = 300.0 self.show_frames = False # intended for debugging use only
def _clip_vector_layer( layer, extent, extra_keywords=None, explode_flag=True, hard_clip_flag=False, explode_attribute=None): """Clip a Hazard or Exposure layer to the extents provided. The layer must be a vector layer or an exception will be thrown. The output layer will always be in WGS84/Geographic. :param layer: A valid QGIS vector or raster layer :type layer: :param extent: 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.** :type extent: list(float, float, float, float) :param extra_keywords: Optional keywords dictionary to be added to output layer. :type extra_keywords: dict :param explode_flag: A bool specifying whether multipart features should be 'exploded' into singleparts. **This parameter is ignored for raster layer clipping.** :type explode_flag: bool :param hard_clip_flag: 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. **This parameter is ignored for raster layer clipping.** :type hard_clip_flag: bool :param explode_attribute: A str specifying to which attribute #1, #2 and so on will be added in case of explode_flag being true. The attribute is modified only if there are at least 2 parts. :type explode_attribute: str :returns: Clipped layer (placed in the system temp dir). The output layer will be reprojected to EPSG:4326 if needed. :rtype: QgsVectorLayer """ if not layer or not extent: myMessage = tr('Layer or Extent passed to clip is None.') raise InvalidParameterError(myMessage) if layer.type() != QgsMapLayer.VectorLayer: myMessage = tr('Expected a vector layer but received a %s.' % str(layer.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.createFromSrid(4326) myXForm = QgsCoordinateTransform(myGeoCrs, layer.crs()) myAllowedClipTypes = [QGis.WKBPolygon, QGis.WKBPolygon25D] if type(extent) is list: myRect = QgsRectangle( extent[0], extent[1], extent[2], extent[3]) # noinspection PyCallByClass myClipPolygon = QgsGeometry.fromRect(myRect) elif (type(extent) is QgsGeometry and extent.wkbType in myAllowedClipTypes): myRect = extent.boundingBox().toRectF() myClipPolygon = extent 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 = layer.dataProvider() if myProvider is None: myMessage = tr('Could not obtain data provider from ' 'layer "%s"' % layer.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 myRequest = QgsFeatureRequest() if not myProjectedExtent.isEmpty(): myRequest.setFilterRect(myProjectedExtent) myRequest.setFlags(QgsFeatureRequest.ExactIntersect) myFieldList = myProvider.fields() myWriter = QgsVectorFileWriter( myFilename, 'UTF-8', myFieldList, layer.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(layer.crs(), myGeoCrs) # Retrieve every feature with its geometry and attributes myCount = 0 myHasMultipart = False for myFeature in myProvider.getFeatures(myRequest): myGeometry = myFeature.geometry() # Loop through the parts adding them to the output file # we write out single part features unless explode_flag is False if explode_flag: myGeometryList = explode_multipart_geometry(myGeometry) else: myGeometryList = [myGeometry] for myPartIndex, myPart in enumerate(myGeometryList): myPart.transform(myXForm) if hard_clip_flag: # Remove any dangling bits so only intersecting area is # kept. myPart = clip_geometry(myClipPolygon, myPart) if myPart is None: continue myFeature.setGeometry(myPart) # There are multiple parts and we want to show it in the # explode_attribute if myPartIndex > 0 and explode_attribute is not None: myHasMultipart = True 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 disjointed. If this is the ' 'case, try to turn on reproject on-the-fly in QGIS.') raise NoFeaturesInExtentError(myMessage) myKeywordIO = KeywordIO() if extra_keywords is None: extra_keywords = {} extra_keywords['had multipart polygon'] = myHasMultipart myKeywordIO.copy_keywords( layer, myFilename, extra_keywords=extra_keywords) myBaseName = '%s clipped' % layer.name() myLayer = QgsVectorLayer(myFilename, myBaseName, 'ogr') return myLayer
class PostprocessorManager(QtCore.QObject): """A manager for post processing of impact function results. """ def __init__(self, aggregator): """Director for aggregation based operations. :param aggregator: Aggregator that will be used in conjunction with postprocessors. :type aggregator: Aggregator """ super(PostprocessorManager, self).__init__() # Aggregation / post processing related items self.output = {} self.keyword_io = KeywordIO() self.error_message = None self.aggregator = aggregator self.current_output_postprocessor = None self.attribute_title = None def _sum_field_name(self): return self.aggregator.prefix + 'sum' def _sort_no_data(self, data): """Check if the value field of the postprocessor is NO_DATA. This is used for sorting, it returns -1 if the value is NO_DATA, so that no data items can be put at the end of a list :param data: Value to be checked. :type data: list :returns: -1 if the value is NO_DATA else the value :rtype: int, float """ post_processor = self.output[self.current_output_postprocessor] #get the key position of the value field key = post_processor[0][1].keyAt(0) #get the value # data[1] is the orderedDict # data[1][myFirstKey] is the 1st indicator in the orderedDict if data[1][key]['value'] == self.aggregator.defaults['NO_DATA']: position = -1 else: position = data[1][key]['value'] position = unhumanize_number(position) return position def _generate_tables(self): """Parses the postprocessing output as one table per postprocessor. TODO: This should rather return json and then have a helper method to make html from the JSON. :returns: The html. :rtype: str """ message = m.Message() for processor, results_list in self.output.iteritems(): self.current_output_postprocessor = processor # results_list is for example: # [ # (PyQt4.QtCore.QString(u'Entire area'), OrderedDict([ # (u'Total', {'value': 977536, 'metadata': {}}), # (u'Female population', {'value': 508319, 'metadata': {}}), # (u'Weekly hygiene packs', {'value': 403453, 'metadata': { # 'description': 'Females hygiene packs for weekly use'}}) # ])) #] #sorting using the first indicator of a postprocessor sorted_results = sorted( results_list, key=self._sort_no_data, reverse=True) #init table has_no_data = False table = m.Table( style_class='table table-condensed table-striped') table.caption = self.tr('Detailed %s report') % (safeTr( get_postprocessor_human_name(processor)).lower()) header = m.Row() header.add(str(self.attribute_title).capitalize()) for calculation_name in sorted_results[0][1]: header.add(self.tr(calculation_name)) table.add(header) # used to calculate the totals row as per issue #690 postprocessor_totals = OrderedDict() for zone_name, calc in sorted_results: row = m.Row(zone_name) for indicator, calculation_data in calc.iteritems(): value = calculation_data['value'] if value == self.aggregator.defaults['NO_DATA']: has_no_data = True value += ' *' try: postprocessor_totals[indicator] += 0 except KeyError: postprocessor_totals[indicator] = 0 else: try: postprocessor_totals[indicator] += int(value) except KeyError: postprocessor_totals[indicator] = int(value) row.add(value) table.add(row) # add the totals row row = m.Row(self.tr('Total in aggregation areas')) for _, total in postprocessor_totals.iteritems(): row.add(str(total)) table.add(row) # add table to message message.add(table) if has_no_data: message.add(m.EmphasizedText(self.tr( '* "%s" values mean that there where some problems while ' 'calculating them. This did not affect the other ' 'values.') % (self.aggregator.defaults['NO_DATA']))) return message def _consolidate_multipart_stats(self): """Sums the values of multipart polygons together to display only one. """ LOGGER.debug('Consolidating multipart postprocessing results') # copy needed because of # self.output[postprocessor].pop(corrected_index) output = self.output # iterate postprocessors for postprocessor, results_list in output.iteritems(): #see self._generateTables to see details about results_list checked_polygon_names = {} parts_to_delete = [] polygon_index = 0 # iterate polygons for polygon_name, results in results_list: if polygon_name in checked_polygon_names.keys(): for result_name, result in results.iteritems(): first_part_index = checked_polygon_names[polygon_name] first_part = self.output[postprocessor][ first_part_index] first_part_results = first_part[1] first_part_result = first_part_results[result_name] # FIXME one of the parts was 'No data', # is it matematically correct to do no_data = 0? # see http://irclogs.geoapt.com/inasafe/ # %23inasafe.2013-08-09.log (at 22.29) no_data = self.aggregator.defaults['NO_DATA'] # both are No data value = first_part_result['value'] result_value = result['value'] if value == no_data and result_value == no_data: new_result = no_data else: # one is No data if value == no_data: value = 0 # the other is No data elif result_value == no_data: result_value = 0 # here none is No data new_result = ( unhumanize_number(value) + unhumanize_number(result_value)) first_part_result['value'] = format_int(new_result) parts_to_delete.append(polygon_index) else: # add polygon to checked list checked_polygon_names[polygon_name] = polygon_index polygon_index += 1 # http://stackoverflow.com/questions/497426/ # deleting-multiple-elements-from-a-list results_list = [res for j, res in enumerate(results_list) if j not in parts_to_delete] self.output[postprocessor] = results_list def run(self): """Run any post processors requested by the impact function. """ try: requested_postprocessors = self.functionParams['postprocessors'] postprocessors = get_postprocessors(requested_postprocessors) except (TypeError, KeyError): # TypeError is for when function_parameters is none # KeyError is for when ['postprocessors'] is unavailable postprocessors = {} LOGGER.debug('Running this postprocessors: ' + str(postprocessors)) feature_names_attribute = self.aggregator.attributes[ self.aggregator.defaults['AGGR_ATTR_KEY']] if feature_names_attribute is None: self.attribute_title = self.tr('Aggregation unit') else: self.attribute_title = feature_names_attribute name_filed_index = self.aggregator.layer.fieldNameIndex( self.attribute_title) sum_field_index = self.aggregator.layer.fieldNameIndex( self._sum_field_name()) user_defined_female_ratio = False female_ratio_field_index = None female_ratio = None if 'Gender' in postprocessors: # look if we need to look for a variable female ratio in a layer try: female_ration_field = self.aggregator.attributes[ self.aggregator.defaults['FEM_RATIO_ATTR_KEY']] female_ratio_field_index = self.aggregator.layer.fieldNameIndex( female_ration_field) # something went wrong finding the female ratio field, # use defaults from below except block if female_ratio_field_index == -1: raise KeyError user_defined_female_ratio = True except KeyError: try: female_ratio = self.keyword_io.read_keywords( self.aggregator.layer, self.aggregator.defaults['FEM_RATIO_KEY']) except KeywordNotFoundError: female_ratio = self.aggregator.defaults['FEM_RATIO'] # iterate zone features request = QgsFeatureRequest() request.setFlags(QgsFeatureRequest.NoGeometry) provider = self.aggregator.layer.dataProvider() # start data retrieval: fetch no geometry and all attributes for each # feature polygon_index = 0 for feature in provider.getFeatures(request): # if a feature has no field called if name_filed_index == -1: zone_name = str(feature.id()) else: zone_name = feature[name_filed_index] # create dictionary of attributes to pass to postprocessor general_params = { 'target_field': self.aggregator.target_field, 'function_params': self.functionParams} if self.aggregator.statistics_type == 'class_count': general_params['impact_classes'] = ( self.aggregator.statistics_classes) elif self.aggregator.statistics_type == 'sum': impact_total = feature[sum_field_index] general_params['impact_total'] = impact_total try: general_params['impact_attrs'] = ( self.aggregator.impact_layer_attributes[polygon_index]) except IndexError: # rasters and attributeless vectors have no attributes general_params['impact_attrs'] = None for key, value in postprocessors.iteritems(): parameters = general_params try: # look if params are available for this postprocessor parameters.update( self.functionParams['postprocessors'][key]['params']) except KeyError: pass if key == 'Gender': if user_defined_female_ratio: female_ratio = feature[female_ratio_field_index] if female_ratio is None: female_ratio = self.aggregator.defaults[ 'FEM_RATIO'] LOGGER.debug(female_ratio) parameters['female_ratio'] = female_ratio value.setup(parameters) value.process() results = value.results() value.clear() # LOGGER.debug(results) try: self.output[key].append( (zone_name, results)) except KeyError: self.output[key] = [] self.output[key].append( (zone_name, results)) # increment the index polygon_index += 1 def get_output(self): """Returns the results of the post processing as a table. :returns: str - a string containing the html in the requested format. """ if self.error_message is not None: message = m.Message( m.Heading(self.tr('Postprocessing report skipped')), m.Paragraph(self.tr( 'Due to a problem while processing the results,' ' the detailed postprocessing report is unavailable:' ' %s') % self.error_message)) return message else: try: if (self.keyword_io.read_keywords( self.aggregator.layer, 'had multipart polygon')): self._consolidate_multipart_stats() except KeywordNotFoundError: pass return self._generate_tables()
class AggregatorTest(unittest.TestCase): """Test the InaSAFE GUI""" def setUp(self): """Fixture run before all tests""" self.maxDiff = None # show full diff for assert errors os.environ['LANG'] = 'en' DOCK.showOnlyVisibleLayersFlag = True load_standard_layers() DOCK.cboHazard.setCurrentIndex(0) DOCK.cboExposure.setCurrentIndex(0) DOCK.cboFunction.setCurrentIndex(0) DOCK.runInThreadFlag = False DOCK.showOnlyVisibleLayersFlag = False DOCK.setLayerNameFromTitleFlag = False DOCK.zoomToImpactFlag = False DOCK.hideExposureFlag = False DOCK.showIntermediateLayers = False set_jakarta_extent() self.keywordIO = KeywordIO() self.defaults = breakdown_defaults() def test_cboAggregationLoadedProject(self): """Aggregation combo changes properly according loaded layers""" myLayerList = [DOCK.tr('Entire area'), DOCK.tr('kabupaten jakarta singlepart')] currentLayers = [DOCK.cboAggregation.itemText(i) for i in range( DOCK.cboAggregation.count())] myMessage = ('The aggregation combobox should have:\n %s \nFound: %s' % (myLayerList, currentLayers)) self.assertEquals(currentLayers, myLayerList, myMessage) def test_checkAggregationAttributeInKW(self): """Aggregation attribute is chosen correctly when present in keywords. """ myAttrKey = breakdown_defaults('AGGR_ATTR_KEY') # with KAB_NAME aggregation attribute defined in .keyword using # kabupaten_jakarta_singlepart.shp myResult, myMessage = setup_scenario( DOCK, hazard='A flood in Jakarta like in 2007', exposure='People', function='Need evacuation', function_id='Flood Evacuation Function', aggregation_layer='kabupaten jakarta singlepart', aggregation_enabled_flag=True) assert myResult, myMessage # Press RUN DOCK.accept() DOCK.runtimeKeywordsDialog.accept() myAttribute = DOCK.aggregator.attributes[myAttrKey] myMessage = ('The aggregation should be KAB_NAME. Found: %s' % myAttribute) self.assertEqual(myAttribute, 'KAB_NAME', myMessage) def test_checkAggregationAttribute1Attr(self): """Aggregation attribute is chosen correctly when there is only one attr available.""" myFileList = ['kabupaten_jakarta_singlepart_1_good_attr.shp'] #add additional layers load_layers(myFileList, clear_flag=False, data_directory=TESTDATA) myAttrKey = breakdown_defaults('AGGR_ATTR_KEY') # with 1 good aggregation attribute using # kabupaten_jakarta_singlepart_1_good_attr.shp myResult, myMessage = setup_scenario( DOCK, hazard='A flood in Jakarta like in 2007', exposure='People', function='Need evacuation', function_id='Flood Evacuation Function', aggregation_layer='kabupaten jakarta singlepart 1 good attr') assert myResult, myMessage # Press RUN # noinspection PyCallByClass,PyTypeChecker DOCK.accept() DOCK.runtimeKeywordsDialog.accept() print myAttrKey print DOCK.aggregator.attributes myAttribute = DOCK.aggregator.attributes[myAttrKey] myMessage = ('The aggregation should be KAB_NAME. Found: %s' % myAttribute) self.assertEqual(myAttribute, 'KAB_NAME', myMessage) def test_checkAggregationAttributeNoAttr(self): """Aggregation attribute chosen correctly when no attr available.""" myFileList = ['kabupaten_jakarta_singlepart_0_good_attr.shp'] #add additional layers load_layers(myFileList, clear_flag=False, data_directory=TESTDATA) myAttrKey = breakdown_defaults('AGGR_ATTR_KEY') # with no good aggregation attribute using # kabupaten_jakarta_singlepart_0_good_attr.shp myResult, myMessage = setup_scenario( DOCK, hazard='A flood in Jakarta like in 2007', exposure='People', function='Need evacuation', function_id='Flood Evacuation Function', aggregation_layer='kabupaten jakarta singlepart 0 good attr') assert myResult, myMessage # Press RUN DOCK.accept() DOCK.runtimeKeywordsDialog.accept() myAttribute = DOCK.aggregator.attributes[myAttrKey] myMessage = ('The aggregation should be None. Found: %s' % myAttribute) assert myAttribute is None, myMessage def test_checkAggregationAttributeNoneAttr(self): """Aggregation attribute is chosen correctly when None in keywords.""" myFileList = ['kabupaten_jakarta_singlepart_with_None_keyword.shp'] #add additional layers load_layers(myFileList, clear_flag=False, data_directory=TESTDATA) myAttrKey = breakdown_defaults('AGGR_ATTR_KEY') # with None aggregation attribute defined in .keyword using # kabupaten_jakarta_singlepart_with_None_keyword.shp myResult, myMessage = setup_scenario( DOCK, hazard='A flood in Jakarta like in 2007', exposure='People', function='Need evacuation', function_id='Flood Evacuation Function', aggregation_layer='kabupaten jakarta singlepart with None keyword') assert myResult, myMessage # Press RUN DOCK.accept() DOCK.runtimeKeywordsDialog.accept() myAttribute = DOCK.aggregator.attributes[myAttrKey] myMessage = ('The aggregation should be None. Found: %s' % myAttribute) assert myAttribute is None, myMessage def test_preprocessing(self): """Preprocessing results are correct. TODO - this needs to be fixed post dock refactor. """ # See qgis project in test data: vector_preprocessing_test.qgs #add additional layers myFileList = ['jakarta_crosskabupaten_polygons.shp'] load_layers(myFileList, clear_flag=False, data_directory=TESTDATA) myFileList = ['kabupaten_jakarta.shp'] load_layers(myFileList, clear_flag=False, data_directory=BOUNDDATA) myResult, myMessage = setup_scenario( DOCK, hazard='jakarta_crosskabupaten_polygons', exposure='People', function='Need evacuation', function_id='Flood Evacuation Function Vector Hazard', aggregation_layer='kabupaten jakarta', aggregation_enabled_flag=True) assert myResult, myMessage # Enable on-the-fly reprojection set_canvas_crs(GEOCRS, True) set_jakarta_extent() # Press RUN DOCK.accept() DOCK.runtimeKeywordsDialog.accept() myExpectedFeatureCount = 20 myMessage = ('The preprocessing should have generated %s features, ' 'found %s' % (myExpectedFeatureCount, DOCK.aggregator.preprocessedFeatureCount)) self.assertEqual(myExpectedFeatureCount, DOCK.aggregator.preprocessedFeatureCount, myMessage) def _aggregate(self, myImpactLayer, myExpectedResults, useNativeZonalStats=False): """Helper to calculate aggregation. Expected results is split into two lists - one list contains numeric attributes, the other strings. This is done so that we can use numpy .testing.assert_allclose which doesn't work on strings """ myExpectedStringResults = [] myExpectedNumericResults = [] for item in myExpectedResults: myItemNumResults = [] myItemStrResults = [] for field in item: try: value = float(field) myItemNumResults.append(value) except ValueError: myItemStrResults.append(str(field)) myExpectedNumericResults.append(myItemNumResults) myExpectedStringResults.append(myItemStrResults) myAggregationLayer = QgsVectorLayer( os.path.join(BOUNDDATA, 'kabupaten_jakarta.shp'), 'test aggregation', 'ogr') # create a copy of aggregation layer myGeoExtent = extent_to_geo_array( myAggregationLayer.extent(), myAggregationLayer.crs()) myAggrAttribute = self.keywordIO.read_keywords( myAggregationLayer, self.defaults['AGGR_ATTR_KEY']) # noinspection PyArgumentEqualDefault myAggregationLayer = clip_layer( layer=myAggregationLayer, extent=myGeoExtent, explode_flag=True, explode_attribute=myAggrAttribute) myAggregator = Aggregator(None, myAggregationLayer) # setting up myAggregator.isValid = True myAggregator.layer = myAggregationLayer myAggregator.safeLayer = safe_read_layer( str(myAggregator.layer.source())) myAggregator.aoiMode = False myAggregator.useNativeZonalStats = useNativeZonalStats myAggregator.aggregate(myImpactLayer) myProvider = myAggregator.layer.dataProvider() myNumericResults = [] myStringResults = [] for myFeature in myProvider.getFeatures(): myFeatureNumResults = [] myFeatureStrResults = [] myAttrs = myFeature.attributes() for attr in myAttrs: if isinstance(attr, (int, float)): myFeatureNumResults.append(attr) else: myFeatureStrResults.append(attr) myNumericResults.append(myFeatureNumResults) myStringResults.append(myFeatureStrResults) # check string attributes self.assertEqual(myExpectedStringResults, myStringResults) # check numeric attributes with a 0.01% tolerance compared to the # native QGIS stats numpy.testing.assert_allclose(myExpectedNumericResults, myNumericResults, rtol=0.01) def test_aggregate_raster_impact_python(self): """Check aggregation on raster impact using python zonal stats""" self._aggregate_raster_impact() def test_aggregate_raster_impact_native(self): """Check aggregation on raster impact using native qgis zonal stats. TODO: this failes on Tims machine but not on MB or Jenkins. """ self._aggregate_raster_impact(useNativeZonalStats=True) def _aggregate_raster_impact(self, useNativeZonalStats=False): """Check aggregation on raster impact. Created from loadStandardLayers.qgs with: - a flood in Jakarta like in 2007 - Penduduk Jakarta - need evacuation - kabupaten_jakarta_singlepart.shp """ myImpactLayer = Raster( data=os.path.join(TESTDATA, 'aggregation_test_impact_raster.tif'), name='test raster impact') myExpectedResults = [ ['JAKARTA BARAT', '50540', '12015061.8769531', '237.733713433976'], ['JAKARTA PUSAT', '19492', '2943702.11401367', '151.021040119725'], ['JAKARTA SELATAN', '57367', '1645498.26947021', '28.6837078716024'], ['JAKARTA UTARA', '55004', '11332095.7334595', '206.023120745027'], ['JAKARTA TIMUR', '73949', '10943934.3182373', '147.992999475819']] self._aggregate(myImpactLayer, myExpectedResults, useNativeZonalStats) def test_aggregate_vector_impact(self): """Test aggregation results on a vector layer. created from loadStandardLayers.qgs with: - a flood in Jakarta like in 2007 - Essential buildings - be flodded - kabupaten_jakarta_singlepart.shp """ myImpactLayer = Vector( data=os.path.join(TESTDATA, 'aggregation_test_impact_vector.shp'), name='test vector impact') myExpectedResults = [ ['JAKARTA BARAT', '87'], ['JAKARTA PUSAT', '117'], ['JAKARTA SELATAN', '22'], ['JAKARTA UTARA', '286'], ['JAKARTA TIMUR', '198'] ] self._aggregate(myImpactLayer, myExpectedResults) myImpactLayer = Vector( data=TESTDATA + '/aggregation_test_impact_vector_small.shp', name='test vector impact') myExpectedResults = [ ['JAKARTA BARAT', '2'], ['JAKARTA PUSAT', '0'], ['JAKARTA SELATAN', '0'], ['JAKARTA UTARA', '1'], ['JAKARTA TIMUR', '0'] ] self._aggregate(myImpactLayer, myExpectedResults)
class OptionsDialog(QtGui.QDialog, Ui_OptionsDialogBase): """Options dialog for the InaSAFE plugin.""" def __init__(self, iface, dock=None, parent=None): """Constructor for the dialog. :param iface: A Quantum GIS QGisAppInterface instance. :type iface: QGisAppInterface :param parent: Parent widget of this dialog :type parent: QWidget :param dock: Optional dock widget instance that we can notify of changes to the keywords. :type dock: Dock """ 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 = dock self.keyword_io = KeywordIO() # Set up things for context help button = self.buttonBox.button(QtGui.QDialogButtonBox.Help) button.clicked.connect(self.show_help) self.grpNotImplemented.hide() self.adjustSize() self.restore_state() # hack prevent showing use thread visible and set it false see #557 self.cbxUseThread.setChecked(True) self.cbxUseThread.setVisible(False) def restore_state(self): """Reinstate the options based on the user's stored session info. """ settings = QtCore.QSettings() # flag = settings.value( # 'inasafe/useThreadingFlag', False) # hack set use thread to false see #557 flag = False self.cbxUseThread.setChecked(flag) flag = bool(settings.value( 'inasafe/visibleLayersOnlyFlag', True, type=bool)) self.cbxVisibleLayersOnly.setChecked(flag) flag = bool(settings.value( 'inasafe/set_layer_from_title_flag', True, type=bool)) self.cbxSetLayerNameFromTitle.setChecked(flag) flag = bool(settings.value( 'inasafe/setZoomToImpactFlag', True, type=bool)) self.cbxZoomToImpact.setChecked(flag) # whether exposure layer should be hidden after model completes flag = bool(settings.value( 'inasafe/setHideExposureFlag', False, type=bool)) self.cbxHideExposure.setChecked(flag) flag = bool(settings.value( 'inasafe/clip_to_viewport', True, type=bool)) self.cbxClipToViewport.setChecked(flag) flag = bool(settings.value( 'inasafe/clip_hard', False, type=bool)) self.cbxClipHard.setChecked(flag) flag = bool(settings.value( 'inasafe/useSentry', False, type=bool)) self.cbxUseSentry.setChecked(flag) flag = bool(settings.value( 'inasafe/show_intermediate_layers', False, type=bool)) self.cbxShowPostprocessingLayers.setChecked(flag) ratio = float(settings.value( 'inasafe/defaultFemaleRatio', DEFAULTS['FEM_RATIO'], type=float)) self.dsbFemaleRatioDefault.setValue(ratio) path = settings.value( 'inasafe/keywordCachePath', self.keyword_io.default_keyword_db_path(), type=str) self.leKeywordCachePath.setText(path) path = settings.value('inasafe/northArrowPath', '', type=str) self.leNorthArrowPath.setText(path) path = settings.value( 'inasafe/organisationLogoPath', ':/plugins/inasafe/bnpb_logo_64.png', type=str) self.leOrganisationLogoPath.setText(path) flag = bool(settings.value( 'inasafe/showOrganisationLogoInDockFlag', True, type=bool)) self.organisation_on_dock_checkbox.setChecked(flag) path = settings.value('inasafe/reportTemplatePath', '', type=str) self.leReportTemplatePath.setText(path) disclaimer = settings.value('inasafe/reportDisclaimer', '', type=str) self.txtDisclaimer.setPlainText(disclaimer) flag = bool( settings.value('inasafe/developer_mode', False, type=bool)) self.cbxDevMode.setChecked(flag) flag = bool( settings.value('inasafe/use_native_zonal_stats', False, type=bool)) self.cbxNativeZonalStats.setChecked(flag) def save_state(self): """Store the options into the user's stored session info. """ settings = QtCore.QSettings() settings.setValue( 'inasafe/useThreadingFlag', False) settings.setValue( 'inasafe/visibleLayersOnlyFlag', self.cbxVisibleLayersOnly.isChecked()) settings.setValue( 'inasafe/set_layer_from_title_flag', self.cbxSetLayerNameFromTitle.isChecked()) settings.setValue( 'inasafe/setZoomToImpactFlag', self.cbxZoomToImpact.isChecked()) settings.setValue( 'inasafe/setHideExposureFlag', self.cbxHideExposure.isChecked()) settings.setValue( 'inasafe/clip_to_viewport', self.cbxClipToViewport.isChecked()) settings.setValue( 'inasafe/clip_hard', self.cbxClipHard.isChecked()) settings.setValue( 'inasafe/useSentry', self.cbxUseSentry.isChecked()) settings.setValue( 'inasafe/show_intermediate_layers', self.cbxShowPostprocessingLayers.isChecked()) settings.setValue( 'inasafe/defaultFemaleRatio', self.dsbFemaleRatioDefault.value()) settings.setValue( 'inasafe/keywordCachePath', self.leKeywordCachePath.text()) settings.setValue( 'inasafe/northArrowPath', self.leNorthArrowPath.text()) settings.setValue( 'inasafe/organisationLogoPath', self.leOrganisationLogoPath.text()) settings.setValue( 'inasafe/showOrganisationLogoInDockFlag', self.organisation_on_dock_checkbox.isChecked()) settings.setValue( 'inasafe/reportTemplatePath', self.leReportTemplatePath.text()) settings.setValue( 'inasafe/reportDisclaimer', self.txtDisclaimer.toPlainText()) settings.setValue( 'inasafe/developer_mode', self.cbxDevMode.isChecked()) settings.setValue( 'inasafe/use_native_zonal_stats', self.cbxNativeZonalStats.isChecked()) @staticmethod def show_help(): """Show context help for the options dialog.""" show_context_help('options') def accept(self): """Method invoked when OK button is clicked.""" self.save_state() self.dock.read_settings() self.close() @pyqtSignature('') # prevents actions being handled twice def on_toolKeywordCachePath_clicked(self): """Auto-connect slot activated when cache file tool button is clicked. """ # noinspection PyCallByClass,PyTypeChecker file_name = QtGui.QFileDialog.getSaveFileName( self, self.tr('Set keyword cache file'), self.keyword_io.default_keyword_db_path(), self.tr('Sqlite DB File (*.db)')) self.leKeywordCachePath.setText(file_name) @pyqtSignature('') # prevents actions being handled twice def on_toolNorthArrowPath_clicked(self): """Auto-connect slot activated when north arrow tool button is clicked. """ # noinspection PyCallByClass,PyTypeChecker file_name = QtGui.QFileDialog.getOpenFileName( self, self.tr('Set north arrow image file'), '', self.tr('Portable Network Graphics files (*.png *.PNG)')) self.leNorthArrowPath.setText(file_name) @pyqtSignature('') # prevents actions being handled twice def on_toolOrganisationLogoPath_clicked(self): """Auto-connect slot activated when logo file tool button is clicked. """ # noinspection PyCallByClass,PyTypeChecker file_name = QtGui.QFileDialog.getOpenFileName( self, self.tr('Set organisation logo file'), '', self.tr('Portable Network Graphics files (*.png *.PNG)')) self.leOrganisationLogoPath.setText(file_name) @pyqtSignature('') # prevents actions being handled twice def on_toolReportTemplatePath_clicked(self): """Auto-connect slot activated when report file tool button is clicked. """ # noinspection PyCallByClass,PyTypeChecker dir_name = QtGui.QFileDialog.getExistingDirectory( self, self.tr('Templates directory'), '', QtGui.QFileDialog.ShowDirsOnly) self.leReportTemplatePath.setText(dir_name)
class AggregatorTest(unittest.TestCase): """Test the InaSAFE GUI""" def setUp(self): """Fixture run before all tests""" self.maxDiff = None # show full diff for assert errors os.environ['LANG'] = 'en' DOCK.showOnlyVisibleLayersFlag = True load_standard_layers() DOCK.cboHazard.setCurrentIndex(0) DOCK.cboExposure.setCurrentIndex(0) DOCK.cboFunction.setCurrentIndex(0) DOCK.runInThreadFlag = False DOCK.showOnlyVisibleLayersFlag = False DOCK.setLayerNameFromTitleFlag = False DOCK.zoomToImpactFlag = False DOCK.hideExposureFlag = False DOCK.showIntermediateLayers = False set_jakarta_extent() self.keywordIO = KeywordIO() self.defaults = defaults() def test_cboAggregationLoadedProject(self): """Aggregation combo changes properly according loaded layers""" myLayerList = [DOCK.tr('Entire area'), DOCK.tr('kabupaten jakarta singlepart')] currentLayers = [DOCK.cboAggregation.itemText(i) for i in range( DOCK.cboAggregation.count())] myMessage = ('The aggregation combobox should have:\n %s \nFound: %s' % (myLayerList, currentLayers)) self.assertEquals(currentLayers, myLayerList, myMessage) def test_checkAggregationAttributeInKW(self): """Aggregation attribute is chosen correctly when present in kezwords.""" myRunButton = DOCK.pbnRunStop myAttrKey = defaults('AGGR_ATTR_KEY') # with KAB_NAME aggregation attribute defined in .keyword using # kabupaten_jakarta_singlepart.shp myResult, myMessage = setup_scenario( DOCK, hazard='A flood in Jakarta like in 2007', exposure='People', function='Need evacuation', function_id='Flood Evacuation Function', aggregation_layer='kabupaten jakarta singlepart', aggregation_enabled_flag=True) assert myResult, myMessage # Press RUN # noinspection PyCallByClass,PyTypeChecker QTest.mouseClick(myRunButton, QtCore.Qt.LeftButton) DOCK.runtimeKeywordsDialog.accept() myAttribute = DOCK.aggregator.attributes[myAttrKey] myMessage = ('The aggregation should be KAB_NAME. Found: %s' % myAttribute) self.assertEqual(myAttribute, 'KAB_NAME', myMessage) def test_checkAggregationAttribute1Attr(self): """Aggregation attribute is chosen correctly when there is only one attr available.""" myRunButton = DOCK.pbnRunStop myFileList = ['kabupaten_jakarta_singlepart_1_good_attr.shp'] #add additional layers load_layers(myFileList, clear_flag=False, data_directory=TESTDATA) myAttrKey = defaults('AGGR_ATTR_KEY') # with 1 good aggregation attribute using # kabupaten_jakarta_singlepart_1_good_attr.shp myResult, myMessage = setup_scenario( DOCK, hazard='A flood in Jakarta like in 2007', exposure='People', function='Need evacuation', function_id='Flood Evacuation Function', aggregation_layer='kabupaten jakarta singlepart 1 good attr') assert myResult, myMessage # Press RUN # noinspection PyCallByClass,PyTypeChecker QTest.mouseClick(myRunButton, QtCore.Qt.LeftButton) DOCK.runtimeKeywordsDialog.accept() print myAttrKey print DOCK.aggregator.attributes myAttribute = DOCK.aggregator.attributes[myAttrKey] myMessage = ('The aggregation should be KAB_NAME. Found: %s' % myAttribute) self.assertEqual(myAttribute, 'KAB_NAME', myMessage) def test_checkAggregationAttributeNoAttr(self): """Aggregation attribute is chosen correctly when there is no attr available.""" myRunButton = DOCK.pbnRunStop myFileList = ['kabupaten_jakarta_singlepart_0_good_attr.shp'] #add additional layers load_layers(myFileList, clear_flag=False, data_directory=TESTDATA) myAttrKey = defaults('AGGR_ATTR_KEY') # with no good aggregation attribute using # kabupaten_jakarta_singlepart_0_good_attr.shp myResult, myMessage = setup_scenario( DOCK, hazard='A flood in Jakarta like in 2007', exposure='People', function='Need evacuation', function_id='Flood Evacuation Function', aggregation_layer='kabupaten jakarta singlepart 0 good attr') assert myResult, myMessage # Press RUN # noinspection PyCallByClass,PyTypeChecker QTest.mouseClick(myRunButton, QtCore.Qt.LeftButton) DOCK.runtimeKeywordsDialog.accept() myAttribute = DOCK.aggregator.attributes[myAttrKey] myMessage = ('The aggregation should be None. Found: %s' % myAttribute) assert myAttribute is None, myMessage def test_checkAggregationAttributeNoneAttr(self): """Aggregation attribute is chosen correctly when there None in the kezwords""" myRunButton = DOCK.pbnRunStop myFileList = ['kabupaten_jakarta_singlepart_with_None_keyword.shp'] #add additional layers load_layers(myFileList, clear_flag=False, data_directory=TESTDATA) myAttrKey = defaults('AGGR_ATTR_KEY') # with None aggregation attribute defined in .keyword using # kabupaten_jakarta_singlepart_with_None_keyword.shp myResult, myMessage = setup_scenario( DOCK, hazard='A flood in Jakarta like in 2007', exposure='People', function='Need evacuation', function_id='Flood Evacuation Function', aggregation_layer='kabupaten jakarta singlepart with None ' 'keyword') assert myResult, myMessage # Press RUN # noinspection PyCallByClass,PyTypeChecker QTest.mouseClick(myRunButton, QtCore.Qt.LeftButton) DOCK.runtimeKeywordsDialog.accept() myAttribute = DOCK.aggregator.attributes[myAttrKey] myMessage = ('The aggregation should be None. Found: %s' % myAttribute) assert myAttribute is None, myMessage def test_preprocessing(self): """Preprocessing results are correct. TODO - this needs to be fixed post dock refactor. """ # See qgis project in test data: vector_preprocessing_test.qgs #add additional layers myFileList = ['jakarta_crosskabupaten_polygons.shp'] load_layers(myFileList, clear_flag=False, data_directory=TESTDATA) myFileList = ['kabupaten_jakarta.shp'] load_layers(myFileList, clear_flag=False, data_directory=BOUNDDATA) myRunButton = DOCK.pbnRunStop myResult, myMessage = setup_scenario( DOCK, hazard='jakarta_crosskabupaten_polygons', exposure='People', function='Need evacuation', function_id='Flood Evacuation Function Vector Hazard', aggregation_layer='kabupaten jakarta', aggregation_enabled_flag=True) assert myResult, myMessage # Enable on-the-fly reprojection set_canvas_crs(GEOCRS, True) set_jakarta_extent() # Press RUN # noinspection PyTypeChecker,PyCallByClass QTest.mouseClick(myRunButton, QtCore.Qt.LeftButton) DOCK.runtimeKeywordsDialog.accept() myExpectedFeatureCount = 20 myMessage = ('The preprocessing should have generated %s features, ' 'found %s' % (myExpectedFeatureCount, DOCK.aggregator.preprocessedFeatureCount)) self.assertEqual(myExpectedFeatureCount, DOCK.aggregator.preprocessedFeatureCount, myMessage) def _aggregate(self, myImpactLayer, myExpectedResults): myAggregationLayer = QgsVectorLayer( os.path.join(BOUNDDATA, 'kabupaten_jakarta.shp'), 'test aggregation', 'ogr') # create a copy of aggregation layer myGeoExtent = extent_to_geo_array( myAggregationLayer.extent(), myAggregationLayer.crs()) myAggrAttribute = self.keywordIO.read_keywords( myAggregationLayer, self.defaults['AGGR_ATTR_KEY']) # noinspection PyArgumentEqualDefault myAggregationLayer = clip_layer( layer=myAggregationLayer, extent=myGeoExtent, explode_flag=True, explode_attribute=myAggrAttribute) myAggregator = Aggregator(None, myAggregationLayer) # setting up myAggregator.isValid = True myAggregator.layer = myAggregationLayer myAggregator.safeLayer = safe_read_layer( str(myAggregator.layer.source())) myAggregator.aoiMode = False myAggregator.aggregate(myImpactLayer) myProvider = myAggregator.layer.dataProvider() myProvider.select(myProvider.attributeIndexes()) myFeature = QgsFeature() myResults = [] while myProvider.nextFeature(myFeature): myFeatureResults = {} myAtMap = myFeature.attributeMap() for (k, attr) in myAtMap.iteritems(): myFeatureResults[k] = str(attr.toString()) myResults.append(myFeatureResults) self.assertEqual(myExpectedResults, myResults) def test_aggregate_raster_impact(self): """Check aggregation on raster impact. Created from loadStandardLayers.qgs with: - a flood in Jakarta like in 2007 - Penduduk Jakarta - need evacuation - kabupaten_jakarta_singlepart.shp """ myImpactLayer = Raster( data=os.path.join(TESTDATA, 'aggregation_test_impact_raster.tif'), name='test raster impact') myExpectedResults = [ {0: 'JAKARTA BARAT', 1: '50540', 2: '12015061.8769531', 3: '237.733713433976', 4: '50539', 5: '12015061.8769531', 6: '237.738417399496'}, {0: 'JAKARTA PUSAT', 1: '19492', 2: '2943702.11401367', 3: '151.021040119725', 4: '19492', 5: '2945658.12207031', 6: '151.121389394126'}, {0: 'JAKARTA SELATAN', 1: '57367', 2: '1645498.26947021', 3: '28.6837078716024', 4: '57372', 5: '1643522.39849854', 6: '28.6467684323108'}, {0: 'JAKARTA UTARA', 1: '55004', 2: '11332095.7334595', 3: '206.023120745027', 4: '54998', 5: '11330910.4882202', 6: '206.024046114772'}, {0: 'JAKARTA TIMUR', 1: '73949', 2: '10943934.3182373', 3: '147.992999475819', 4: '73944', 5: '10945062.4354248', 6: '148.018262947971'}] self._aggregate(myImpactLayer, myExpectedResults) def test_aggregate_vector_impact(self): """Test aggregation results on a vector layer. created from loadStandardLayers.qgs with: - a flood in Jakarta like in 2007 - Essential buildings - be flodded - kabupaten_jakarta_singlepart.shp """ myImpactLayer = Vector( data=os.path.join(TESTDATA, 'aggregation_test_impact_vector.shp'), name='test vector impact') myExpectedResults = [ {0: 'JAKARTA BARAT', 1: '87'}, {0: 'JAKARTA PUSAT', 1: '117'}, {0: 'JAKARTA SELATAN', 1: '22'}, {0: 'JAKARTA UTARA', 1: '286'}, {0: 'JAKARTA TIMUR', 1: '198'} ] # self._aggregate(myImpactLayer, myExpectedResults) myImpactLayer = Vector( data=TESTDATA + '/aggregation_test_impact_vector_small.shp', name='test vector impact') myExpectedResults = [ {0: 'JAKARTA BARAT', 1: '87'}, {0: 'JAKARTA PUSAT', 1: '117'}, {0: 'JAKARTA SELATAN', 1: '22'}, {0: 'JAKARTA UTARA', 1: '286'}, {0: 'JAKARTA TIMUR', 1: '198'} ] # TODO (MB) enable this self._aggregate(myImpactLayer, myExpectedResults)
class AggregatorTest(unittest.TestCase): """Test the InaSAFE GUI""" #noinspection PyPep8Naming def setUp(self): """Fixture run before all tests""" self.maxDiff = None # show full diff for assert errors os.environ['LANG'] = 'en' DOCK.show_only_visible_layers_flag = True load_standard_layers() DOCK.cboHazard.setCurrentIndex(0) DOCK.cboExposure.setCurrentIndex(0) DOCK.cboFunction.setCurrentIndex(0) DOCK.run_in_thread_flag = False DOCK.show_only_visible_layers_flag = False DOCK.set_layer_from_title_flag = False DOCK.zoom_to_impact_flag = False DOCK.hide_exposure_flag = False DOCK.show_intermediate_layers = False set_jakarta_extent() self.keywordIO = KeywordIO() self.defaults = breakdown_defaults() def test_combo_aggregation_loaded_project(self): """Aggregation combo changes properly according loaded layers""" layer_list = [ DOCK.tr('Entire area'), DOCK.tr('kabupaten jakarta singlepart') ] current_layers = [ DOCK.cboAggregation.itemText(i) for i in range(DOCK.cboAggregation.count()) ] message = ('The aggregation combobox should have:\n %s \nFound: %s' % (layer_list, current_layers)) self.assertEquals(current_layers, layer_list, message) def test_aggregation_attribute_in_keywords(self): """Aggregation attribute is chosen correctly when present in keywords. """ attribute_key = breakdown_defaults('AGGR_ATTR_KEY') # with KAB_NAME aggregation attribute defined in .keyword using # kabupaten_jakarta_singlepart.shp result, message = setup_scenario( DOCK, hazard='A flood in Jakarta like in 2007', exposure='People', function='Need evacuation', function_id='Flood Evacuation Function', aggregation_layer='kabupaten jakarta singlepart', aggregation_enabled_flag=True) assert result, message # Press RUN DOCK.accept() DOCK.runtime_keywords_dialog.accept() attribute = DOCK.aggregator.attributes[attribute_key] message = ('The aggregation should be KAB_NAME. Found: %s' % attribute) self.assertEqual(attribute, 'KAB_NAME', message) def test_check_aggregation_single_attribute(self): """Aggregation attribute is chosen correctly when there is only one attr available.""" file_list = ['kabupaten_jakarta_singlepart_1_good_attr.shp'] #add additional layers load_layers(file_list, clear_flag=False) attribute_key = breakdown_defaults('AGGR_ATTR_KEY') # with 1 good aggregation attribute using # kabupaten_jakarta_singlepart_1_good_attr.shp result, message = setup_scenario( DOCK, hazard='A flood in Jakarta like in 2007', exposure='People', function='Need evacuation', function_id='Flood Evacuation Function', aggregation_layer='kabupaten jakarta singlepart 1 good attr') assert result, message # Press RUN # noinspection PyCallByClass,PyTypeChecker DOCK.accept() DOCK.runtime_keywords_dialog.accept() print attribute_key print DOCK.aggregator.attributes attribute = DOCK.aggregator.attributes[attribute_key] message = ('The aggregation should be KAB_NAME. Found: %s' % attribute) self.assertEqual(attribute, 'KAB_NAME', message) #noinspection PyMethodMayBeStatic def test_check_aggregation_no_attributes(self): """Aggregation attribute chosen correctly when no attr available.""" file_list = ['kabupaten_jakarta_singlepart_0_good_attr.shp'] #add additional layers load_layers(file_list, clear_flag=False) attribute_key = breakdown_defaults('AGGR_ATTR_KEY') # with no good aggregation attribute using # kabupaten_jakarta_singlepart_0_good_attr.shp result, message = setup_scenario( DOCK, hazard='A flood in Jakarta like in 2007', exposure='People', function='Need evacuation', function_id='Flood Evacuation Function', aggregation_layer='kabupaten jakarta singlepart 0 good attr') assert result, message # Press RUN DOCK.accept() DOCK.runtime_keywords_dialog.accept() attribute = DOCK.aggregator.attributes[attribute_key] message = ('The aggregation should be None. Found: %s' % attribute) assert attribute is None, message #noinspection PyMethodMayBeStatic def test_check_aggregation_none_in_keywords(self): """Aggregation attribute is chosen correctly when None in keywords.""" file_list = ['kabupaten_jakarta_singlepart_with_None_keyword.shp'] #add additional layers load_layers(file_list, clear_flag=False) attribute_key = breakdown_defaults('AGGR_ATTR_KEY') # with None aggregation attribute defined in .keyword using # kabupaten_jakarta_singlepart_with_None_keyword.shp result, message = setup_scenario( DOCK, hazard='A flood in Jakarta like in 2007', exposure='People', function='Need evacuation', function_id='Flood Evacuation Function', aggregation_layer='kabupaten jakarta singlepart with None keyword') assert result, message # Press RUN DOCK.accept() DOCK.runtime_keywords_dialog.accept() attribute = DOCK.aggregator.attributes[attribute_key] message = ('The aggregation should be None. Found: %s' % attribute) assert attribute is None, message def test_preprocessing(self): """Preprocessing results are correct. TODO - this needs to be fixed post dock refactor. """ # See qgis project in test data: vector_preprocessing_test.qgs #add additional layers file_list = ['jakarta_crosskabupaten_polygons.shp'] load_layers(file_list, clear_flag=False) file_list = ['kabupaten_jakarta.shp'] load_layers(file_list, clear_flag=False, data_directory=BOUNDDATA) result, message = setup_scenario( DOCK, hazard='jakarta_crosskabupaten_polygons', exposure='People', function='Need evacuation', function_id='Flood Evacuation Function Vector Hazard', aggregation_layer='kabupaten jakarta', aggregation_enabled_flag=True) assert result, message # Enable on-the-fly reprojection set_canvas_crs(GEOCRS, True) set_jakarta_extent() # Press RUN DOCK.accept() DOCK.runtime_keywords_dialog.accept() expected_feature_count = 20 message = ('The preprocessing should have generated %s features, ' 'found %s' % (expected_feature_count, DOCK.aggregator.preprocessed_feature_count)) self.assertEqual(expected_feature_count, DOCK.aggregator.preprocessed_feature_count, message) def _aggregate(self, impact_layer, expected_results, use_native_zonal_stats=False): """Helper to calculate aggregation. Expected results is split into two lists - one list contains numeric attributes, the other strings. This is done so that we can use numpy .testing.assert_allclose which doesn't work on strings """ expected_string_results = [] expected_numeric_results = [] for item in expected_results: string_results = [] numeric_results = [] for field in item: try: value = float(field) numeric_results.append(value) except ValueError: string_results.append(str(field)) expected_numeric_results.append(numeric_results) expected_string_results.append(string_results) aggregation_layer = QgsVectorLayer( os.path.join(BOUNDDATA, 'kabupaten_jakarta.shp'), 'test aggregation', 'ogr') # create a copy of aggregation layer geo_extent = extent_to_geo_array(aggregation_layer.extent(), aggregation_layer.crs()) aggregation_attribute = self.keywordIO.read_keywords( aggregation_layer, self.defaults['AGGR_ATTR_KEY']) # noinspection PyArgumentEqualDefault aggregation_layer = clip_layer(layer=aggregation_layer, extent=geo_extent, explode_flag=True, explode_attribute=aggregation_attribute) aggregator = Aggregator(None, aggregation_layer) # setting up aggregator.is_valid = True aggregator.layer = aggregation_layer aggregator.safe_layer = safe_read_layer(str(aggregator.layer.source())) aggregator.aoi_mode = False aggregator.use_native_zonal_stats = use_native_zonal_stats aggregator.aggregate(impact_layer) provider = aggregator.layer.dataProvider() string_results = [] numeric_results = [] for feature in provider.getFeatures(): feature_string_results = [] feature_numeric_results = [] attributes = feature.attributes() for attr in attributes: if isinstance(attr, (int, float)): feature_numeric_results.append(attr) else: feature_string_results.append(attr) numeric_results.append(feature_numeric_results) string_results.append(feature_string_results) # check string attributes self.assertEqual(expected_string_results, string_results) # check numeric attributes with a 0.01% tolerance compared to the # native QGIS stats numpy.testing.assert_allclose(expected_numeric_results, numeric_results, rtol=0.01) def test_aggregate_raster_impact_python(self): """Check aggregation on raster impact using python zonal stats""" self._aggregate_raster_impact() def test_aggregate_raster_impact_native(self): """Check aggregation on raster impact using native qgis zonal stats. TODO: this fails on Tim's machine but not on MB or Jenkins. """ self._aggregate_raster_impact(use_native_zonal_stats=True) def _aggregate_raster_impact(self, use_native_zonal_stats=False): """Check aggregation on raster impact. :param use_native_zonal_stats: Created from loadStandardLayers.qgs with: - a flood in Jakarta like in 2007 - Penduduk Jakarta - need evacuation - kabupaten_jakarta_singlepart.shp """ impact_layer = Raster(data=os.path.join( TESTDATA, 'aggregation_test_impact_raster.tif'), name='test raster impact') expected_results = [ ['JAKARTA BARAT', '50540', '12015061.8769531', '237.733713433976'], ['JAKARTA PUSAT', '19492', '2943702.11401367', '151.021040119725'], [ 'JAKARTA SELATAN', '57367', '1645498.26947021', '28.6837078716024' ], ['JAKARTA UTARA', '55004', '11332095.7334595', '206.023120745027'], ['JAKARTA TIMUR', '73949', '10943934.3182373', '147.992999475819'] ] self._aggregate(impact_layer, expected_results, use_native_zonal_stats) def test_aggregate_vector_impact(self): """Test aggregation results on a vector layer. created from loadStandardLayers.qgs with: - a flood in Jakarta like in 2007 - Essential buildings - be flooded - kabupaten_jakarta_singlepart.shp """ impact_layer = Vector(data=os.path.join( TESTDATA, 'aggregation_test_impact_vector.shp'), name='test vector impact') expected_results = [['JAKARTA BARAT', '87'], ['JAKARTA PUSAT', '117'], ['JAKARTA SELATAN', '22'], ['JAKARTA UTARA', '286'], ['JAKARTA TIMUR', '198']] self._aggregate(impact_layer, expected_results) impact_layer = Vector(data=TESTDATA + '/aggregation_test_impact_vector_small.shp', name='test vector impact') expected_results = [['JAKARTA BARAT', '2'], ['JAKARTA PUSAT', '0'], ['JAKARTA SELATAN', '0'], ['JAKARTA UTARA', '1'], ['JAKARTA TIMUR', '0']] self._aggregate(impact_layer, expected_results)
class OptionsDialog(QtGui.QDialog, Ui_OptionsDialogBase): """Options dialog for the InaSAFE plugin.""" def __init__(self, iface, dock=None, parent=None): """Constructor for the dialog. :param iface: A Quantum GIS QGisAppInterface instance. :type iface: QGisAppInterface :param parent: Parent widget of this dialog :type parent: QWidget :param dock: Optional dock widget instance that we can notify of changes to the keywords. :type dock: Dock """ 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 = dock self.keywordIO = KeywordIO() # Set up things for context help myButton = self.buttonBox.button(QtGui.QDialogButtonBox.Help) myButton.clicked.connect(self.show_help) self.grpNotImplemented.hide() self.adjustSize() self.restore_state() # hack prevent showing use thread visible and set it false see #557 self.cbxUseThread.setChecked(True) self.cbxUseThread.setVisible(False) def restore_state(self): """Reinstate the options based on the user's stored session info. """ mySettings = QtCore.QSettings() # myFlag = mySettings.value( # 'inasafe/useThreadingFlag', False) # hack set use thread to false see #557 myFlag = False self.cbxUseThread.setChecked(myFlag) myFlag = bool(mySettings.value( 'inasafe/visibleLayersOnlyFlag', True)) self.cbxVisibleLayersOnly.setChecked(myFlag) myFlag = bool(mySettings.value( 'inasafe/setLayerNameFromTitleFlag', True)) self.cbxSetLayerNameFromTitle.setChecked(myFlag) myFlag = bool(mySettings.value( 'inasafe/setZoomToImpactFlag', True)) self.cbxZoomToImpact.setChecked(myFlag) # whether exposure layer should be hidden after model completes myFlag = bool(mySettings.value( 'inasafe/setHideExposureFlag', False)) self.cbxHideExposure.setChecked(myFlag) myFlag = bool(mySettings.value( 'inasafe/clipToViewport', True)) self.cbxClipToViewport.setChecked(myFlag) myFlag = bool(mySettings.value( 'inasafe/clipHard', False)) self.cbxClipHard.setChecked(myFlag) myFlag = bool(mySettings.value( 'inasafe/useSentry', False)) self.cbxUseSentry.setChecked(myFlag) myFlag = bool(mySettings.value( 'inasafe/showIntermediateLayers', False)) self.cbxShowPostprocessingLayers.setChecked(myFlag) myRatio = float(mySettings.value( 'inasafe/defaultFemaleRatio', DEFAULTS['FEM_RATIO'])) self.dsbFemaleRatioDefault.setValue(myRatio) myPath = mySettings.value( 'inasafe/keywordCachePath', self.keywordIO.default_keyword_db_path()) self.leKeywordCachePath.setText(myPath) myFlag = bool(mySettings.value( 'inasafe/devMode', False)) self.cbxDevMode.setChecked(myFlag) myFlag = bool(mySettings.value( 'inasafe/useNativeZonalStats', False)) self.cbxNativeZonalStats.setChecked(myFlag) def save_state(self): """Store the options into the user's stored session info. """ 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/showIntermediateLayers', self.cbxShowPostprocessingLayers.isChecked()) mySettings.setValue('inasafe/defaultFemaleRatio', self.dsbFemaleRatioDefault.value()) mySettings.setValue('inasafe/keywordCachePath', self.leKeywordCachePath.text()) mySettings.setValue('inasafe/devMode', self.cbxDevMode.isChecked()) mySettings.setValue('inasafe/useNativeZonalStats', self.cbxNativeZonalStats.isChecked()) def show_help(self): """Show context help for the options dialog.""" show_context_help('options') def accept(self): """Method invoked when OK button is clicked.""" self.save_state() self.dock.read_settings() self.close() @pyqtSignature('') # prevents actions being handled twice def on_toolKeywordCachePath_clicked(self): """Auto-connect slot activated when cache file tool button is clicked. """ # noinspection PyCallByClass,PyTypeChecker myFilename = QtGui.QFileDialog.getSaveFileName( self, self.tr('Set keyword cache file'), self.keywordIO.default_keyword_db_path(), self.tr('Sqlite DB File (*.db)')) self.leKeywordCachePath.setText(myFilename)
class ImpactMergeDialog(QDialog, Ui_ImpactMergeDialogBase): """Tools for merging 2 impact layer based on different exposure.""" def __init__(self, parent=None, iface=None): """Constructor for dialog. :param parent: Optional widget to use as parent :type parent: QWidget :param iface: An instance of QGisInterface :type iface: QGisInterface """ QDialog.__init__(self, parent) self.parent = parent self.setupUi(self) self.setWindowTitle(self.tr('InaSAFE Impact Layer Merge Tool')) self.iface = iface self.keyword_io = KeywordIO() # Template Path for composer self.template_path = ':/plugins/inasafe/merged_report.qpt' # Safe Logo Path self.safe_logo_path = ':/plugins/inasafe/inasafe-logo-url.png' # Organisation Logo Path self.organisation_logo_path = ':/plugins/inasafe/supporters.png' # Disclaimer text self.disclaimer = disclaimer() # The output directory self.out_dir = None # Stored information from first impact layer self.first_impact = { 'layer': None, 'map_title': None, 'hazard_title': None, 'exposure_title': None, 'postprocessing_report': None, } # Stored information from second impact layer self.second_impact = { 'layer': None, 'map_title': None, 'hazard_title': None, 'exposure_title': None, 'postprocessing_report': None, } # Stored information from aggregation layer self.aggregation = { 'layer': None, 'aggregation_attribute': None } # The summary report, contains report for each aggregation area self.summary_report = {} # The html reports and its file path self.html_reports = {} # A boolean flag whether to merge entire area or aggregated self.entire_area_mode = False # Get the global settings and override some variable if exist self.read_settings() # Get all current project layers for combo box self.get_project_layers() # Set up context help help_button = self.button_box.button(QtGui.QDialogButtonBox.Help) help_button.clicked.connect(self.show_help) # Show usage info self.show_info() self.restore_state() def show_info(self): """Show usage info to the user.""" # Read the header and footer html snippets header = html_header() footer = html_footer() string = header heading = m.Heading(self.tr('Impact Layer Merge Tool'), **INFO_STYLE) body = self.tr( 'This tool will merge the outputs from two impact maps for the ' 'same area. The maps must be created using the same aggregation ' 'areas and same hazard. To use:' ) tips = m.BulletedList() tips.add(self.tr( 'Run an impact assessment for an area using aggregation. e.g.' 'Flood Impact on Buildings aggregated by municipal boundaries.')) tips.add(self.tr( 'Run a second impact assessment for the same area using the same ' 'aggregation. e.g. Flood Impact on People aggregated by ' 'municipal boundaries.')) tips.add(self.tr( 'Open this tool and select each impact layer from the pick lists ' 'provided below.')) tips.add(self.tr( 'Select the aggregation layer that was used to generate the ' 'first and second impact layer.')) tips.add(self.tr( 'Select an output directory.')) tips.add(self.tr( 'Check "Use customized report template" checkbox and select the ' 'report template file if you want to use your own template. Note ' 'that all the map composer components that are needed must be ' 'fulfilled.')) tips.add(self.tr( 'Click OK to generate the per aggregation area combined ' 'summaries.')) message = m.Message() message.add(heading) message.add(body) message.add(tips) string += message.to_html() string += footer self.web_view.setHtml(string) def restore_state(self): """ Read last state of GUI from configuration file.""" settings = QSettings() try: last_path = settings.value('directory', type=str) except TypeError: last_path = '' self.output_directory.setText(last_path) def save_state(self): """ Store current state of GUI to configuration file """ settings = QSettings() settings.setValue('directory', self.output_directory.text()) @staticmethod def show_help(): """Load the help text for the dialog.""" show_context_help('impact_layer_merge_tool') @pyqtSignature('') # prevents actions being handled twice def on_directory_chooser_clicked(self): """Show a dialog to choose directory.""" # noinspection PyCallByClass,PyTypeChecker self.output_directory.setText(QFileDialog.getExistingDirectory( self, self.tr("Select Output Directory"))) @pyqtSignature('') # prevents actions being handled twice def on_report_template_chooser_clicked(self): """Show a dialog to choose directory""" # noinspection PyCallByClass,PyTypeChecker report_template_path = QtGui.QFileDialog.getOpenFileName( self, self.tr('Select Report Template'), self.template_path, self.tr('QPT File (*.qpt)')) # noinspection PyCallByClass,PyTypeChecker self.report_template_le.setText(report_template_path) def accept(self): """Do merging two impact layers.""" # Store the current state to configuration file self.save_state() # Prepare all the input from dialog, validate, and store it try: self.prepare_input() except (InvalidLayerError, EmptyDirectoryError, FileNotFoundError) as ex: # noinspection PyCallByClass,PyTypeChecker, PyArgumentList QMessageBox.information( self, self.tr("InaSAFE Merge Impact Tool Information"), str(ex)) return except CanceledImportDialogError: return # Validate all the layers logically try: self.validate_all_layers() except (NoKeywordsFoundError, KeywordNotFoundError, InvalidLayerError) as ex: # noinspection PyCallByClass,PyTypeChecker, PyArgumentList QMessageBox.information( self, self.tr("InaSAFE Merge Impact Tools Information"), str(ex)) return # The input is valid, do the merging # Set cursor to wait cursor QtGui.qApp.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor)) #pylint: disable=W0703 try: self.merge() except Exception as ex: # End wait cursor QtGui.qApp.restoreOverrideCursor() # noinspection PyCallByClass,PyTypeChecker,PyArgumentList QMessageBox.warning( self, self.tr("InaSAFE Merge Impact Tools Error"), str(ex)) return #pylint: enable=W0703 # Finish doing it. End wait cursor QtGui.qApp.restoreOverrideCursor() # Give user successful information! # noinspection PyCallByClass,PyTypeChecker, PyArgumentList QMessageBox.information( self, self.tr('InaSAFE Merge Impact Tool Information'), self.tr( 'Report from merging two impact layers was generated ' 'successfully.')) # Open output directory on file explorer output_directory_url = QUrl.fromLocalFile(self.out_dir) #noinspection PyTypeChecker,PyCallByClass QDesktopServices.openUrl(output_directory_url) def read_settings(self): """Set some variables from global settings on inasafe options dialog. """ settings = QtCore.QSettings() # Organisation logo organisation_logo_path = settings.value( 'inasafe/organisation_logo_path', '', type=str) if organisation_logo_path != '': self.organisation_logo_path = organisation_logo_path # Disclaimer text customised_disclaimer = settings.value( 'inasafe/reportDisclaimer', '', type=str) if customised_disclaimer != '': self.disclaimer = customised_disclaimer def get_project_layers(self): """Get impact layers and aggregation layer currently loaded in QGIS.""" #noinspection PyArgumentList registry = QgsMapLayerRegistry.instance() # MapLayers returns a QMap<QString id, QgsMapLayer layer> layers = registry.mapLayers().values() if len(layers) == 0: return # Clear the combo box first self.first_layer.clear() self.second_layer.clear() self.aggregation_layer.clear() for layer in layers: try: self.keyword_io.read_keywords(layer, 'impact_summary') except (NoKeywordsFoundError, KeywordNotFoundError): # Check if it has aggregation keyword try: self.keyword_io.read_keywords( layer, 'aggregation attribute') except (NoKeywordsFoundError, KeywordNotFoundError): # Skip if there are no keywords at all continue add_ordered_combo_item( self.aggregation_layer, layer.name(), layer) continue except (UnsupportedProviderError, InvalidParameterError): # UnsupportedProviderError: # Encounter unsupported provider layer, e.g Open Layer # InvalidParameterError: # Encounter invalid layer source, # see https://github.com/AIFDR/inasafe/issues/754 continue add_ordered_combo_item(self.first_layer, layer.name(), layer) add_ordered_combo_item(self.second_layer, layer.name(), layer) # Add Entire Area Option to Aggregated Layer: self.aggregation_layer.insertItem( 0, self.tr('Entire Area'), None ) self.aggregation_layer.setCurrentIndex(0) def prepare_input(self): """Fetch all the input from dialog, validate, and store it. Consider this as a bridge between dialog interface and our logical stored data in this class :raises: InvalidLayerError, CanceledImportDialogError """ # Validate The combobox impact layers (they should be different) first_layer_index = self.first_layer.currentIndex() second_layer_index = self.second_layer.currentIndex() if first_layer_index < 0: raise InvalidLayerError(self.tr('First layer is not valid.')) if second_layer_index < 0: raise InvalidLayerError(self.tr('Second layer is not valid.')) if first_layer_index == second_layer_index: raise InvalidLayerError( self.tr('First layer must be different to second layer''.')) # Get All Chosen Layer self.first_impact['layer'] = self.first_layer.itemData( self.first_layer.currentIndex(), QtCore.Qt.UserRole) self.second_impact['layer'] = self.second_layer.itemData( self.second_layer.currentIndex(), QtCore.Qt.UserRole) self.aggregation['layer'] = self.aggregation_layer.itemData( self.aggregation_layer.currentIndex(), QtCore.Qt.UserRole) # Validate the output directory self.require_directory() # Get output directory self.out_dir = self.output_directory.text() # Whether to use own report template: if self.report_template_checkbox.isChecked(): own_template_path = self.report_template_le.text() if os.path.isfile(own_template_path): self.template_path = own_template_path else: raise FileNotFoundError( self.tr('Template file does not exist.')) # Flag whether to merge entire area or based on aggregation unit if self.aggregation['layer'] is None: self.entire_area_mode = True def require_directory(self): """Ensure directory path entered in dialog exist. When the path does not exist, this function will ask the user if he wants to create it or not. :raises: CanceledImportDialogError - when user chooses 'No' in the question dialog for creating directory, or 'Yes' but the output directory path is empty """ path = str(self.output_directory.text()) if os.path.exists(path): return title = self.tr("Directory %s does not exist") % path question = self.tr( "Directory %s does not exist. Do you want to create it?" ) % path # noinspection PyCallByClass,PyTypeChecker answer = QMessageBox.question( self, title, question, QMessageBox.Yes | QMessageBox.No) if answer == QMessageBox.Yes: if len(path) != 0: os.makedirs(path) else: raise EmptyDirectoryError( self.tr('Output directory cannot be empty.')) else: raise CanceledImportDialogError() def validate_all_layers(self): """Validate all layers based on the keywords. When we do the validation, we also fetch the information we need: 1. 'map_title' from each impact layer 2. 'exposure_title' from each impact layer 3. 'postprocessing_report' from each impact layer 4. 'aggregation_attribute' on aggregation layer, if user runs merging tools with aggregation layer chosen The things that we validate are: 1. 'map_title' keyword must exist on each impact layer 2. 'exposure_title' keyword must exist on each impact layer 3. 'postprocessing_report' keyword must exist on each impact layer 4. 'hazard_title' keyword must exist on each impact layer. Hazard title from first impact layer must be the same with second impact layer to indicate that both are generated from the same hazard layer. 5. 'aggregation attribute' must exist when user wants to run merging tools with aggregation layer chosen. """ required_attribute = ['map_title', 'exposure_title', 'hazard_title', 'postprocessing_report'] # Fetch for first impact layer for attribute in required_attribute: try: #noinspection PyTypeChecker self.first_impact[attribute] = self.keyword_io.read_keywords( self.first_impact['layer'], attribute) except NoKeywordsFoundError: raise NoKeywordsFoundError( self.tr('No keywords found for first impact layer.')) except KeywordNotFoundError: raise KeywordNotFoundError( self.tr( 'Keyword %s not found for first layer.' % attribute)) # Fetch for second impact layer for attribute in required_attribute: try: #noinspection PyTypeChecker self.second_impact[attribute] = self.keyword_io.read_keywords( self.second_impact['layer'], attribute) except NoKeywordsFoundError: raise NoKeywordsFoundError( self.tr('No keywords found for second impact layer.')) except KeywordNotFoundError: raise KeywordNotFoundError( self.tr( 'Keyword %s not found for second layer.' % attribute)) # Validate that two impact layers are obtained from the same hazard. # Indicated by the same 'hazard_title' (to be fixed later by using # more reliable method) if (self.first_impact['hazard_title'] != self.second_impact['hazard_title']): raise InvalidLayerError( self.tr('First impact layer and second impact layer do not ' 'use the same hazard layer.')) # Fetch 'aggregation_attribute' # If the chosen aggregation layer not Entire Area, it should have # aggregation attribute keywords if not self.entire_area_mode: try: #noinspection PyTypeChecker self.aggregation['aggregation_attribute'] = \ self.keyword_io.read_keywords( self.aggregation['layer'], 'aggregation attribute') except NoKeywordsFoundError: raise NoKeywordsFoundError( self.tr('No keywords exist in aggregation layer.')) except KeywordNotFoundError: raise KeywordNotFoundError( self.tr( 'Keyword aggregation attribute not found for ' 'aggregation layer.')) def merge(self): """Merge the postprocessing_report from each impact.""" # Ensure there is always only a single root element or minidom moans first_postprocessing_report = \ self.first_impact['postprocessing_report'] second_postprocessing_report = \ self.second_impact['postprocessing_report'] #noinspection PyTypeChecker first_report = '<body>' + first_postprocessing_report + '</body>' #noinspection PyTypeChecker second_report = '<body>' + second_postprocessing_report + '</body>' # Now create a dom document for each first_document = minidom.parseString(first_report) second_document = minidom.parseString(second_report) first_impact_tables = first_document.getElementsByTagName('table') second_impact_tables = second_document.getElementsByTagName('table') # Now create dictionary report from DOM first_report_dict = self.generate_report_dictionary_from_dom( first_impact_tables) second_report_dict = self.generate_report_dictionary_from_dom( second_impact_tables) # Generate report summary for all aggregation unit self.generate_report_summary(first_report_dict, second_report_dict) # Generate html reports file from merged dictionary self.generate_html_reports(first_report_dict, second_report_dict) # Generate PDF Reports using composer and/or atlas generation: self.generate_reports() # Delete html report files: for area in self.html_reports: report_path = self.html_reports[area] if os.path.exists(report_path): os.remove(report_path) @staticmethod def generate_report_dictionary_from_dom(html_dom): """Generate dictionary representing report from html dom. :param html_dom: Input representing document dom as report from each impact layer report. :type html_dom: str :return: Dictionary representing html_dom. :rtype: dict Dictionary Structure:: { Aggregation_Area: {Exposure Type:{ Exposure Detail} } } Example:: {"Jakarta Barat": {"Detailed Building Type Report": {"Total inundated":150, "Places of Worship": "No data" } } } """ merged_report_dict = OrderedDict() for table in html_dom: #noinspection PyUnresolvedReferences caption = table.getElementsByTagName('caption')[0].firstChild.data #noinspection PyUnresolvedReferences rows = table.getElementsByTagName('tr') header = rows[0] contains = rows[1:] for contain in contains: data = contain.getElementsByTagName('td') aggregation_area = data[0].firstChild.nodeValue exposure_dict = OrderedDict() if aggregation_area in merged_report_dict: exposure_dict = merged_report_dict[aggregation_area] data_contain = data[1:] exposure_detail_dict = OrderedDict() for datum in data_contain: index_datum = data.index(datum) datum_header = \ header.getElementsByTagName('td')[index_datum] datum_caption = datum_header.firstChild.nodeValue exposure_detail_dict[datum_caption] = \ datum.firstChild.nodeValue exposure_dict[caption] = exposure_detail_dict merged_report_dict[aggregation_area] = exposure_dict return merged_report_dict def generate_report_summary(self, first_report_dict, second_report_dict): """Generate report summary for each aggregation area from merged report dictionary. For each exposure, search for the total only. Report dictionary looks like this: :param first_report_dict: Dictionary report from the first impact. :type first_report_dict: dict :param second_report_dict: Dictionary report from the second impact. :type second_report_dict: dict Dictionary structure:: { aggregation_area: {exposure_type:{ exposure_detail} } } Example:: {"Jakarta Barat": {"Detailed Building Type Report": {"Total inundated":150, "Places of Worship": "No data" } } } """ for aggregation_area in first_report_dict: html = '' html += '<table style="margin:0px auto">' # Summary total from first report html += '<tr><td><b>%s</b></td><td></td></tr>' % \ self.first_impact['exposure_title'].title() first_exposure_type_dict = first_report_dict[aggregation_area] first_exposure_type = first_exposure_type_dict.keys()[0] first_exposure_detail_dict = \ first_exposure_type_dict[first_exposure_type] for datum in first_exposure_detail_dict: if self.tr('Total').lower() in datum.lower(): html += ('<tr>' '<td>%s</td>' '<td>%s</td>' '</tr>') % \ (datum, first_exposure_detail_dict[datum]) break # Catch fallback for aggregation_area not exist in second_report if aggregation_area in second_report_dict: second_exposure_report_dict = second_report_dict[ aggregation_area] # Summary total from second report html += '<tr><td><b>%s</b></td><td></td></tr>' % \ self.second_impact['exposure_title'].title() second_exposure = second_exposure_report_dict.keys()[0] second_exposure_detail_dict = \ second_exposure_report_dict[second_exposure] for datum in second_exposure_detail_dict: if self.tr('Total').lower() in datum.lower(): html += ('<tr>' '<td>%s</td>' '<td>%s</td>' '</tr>') % \ (datum, second_exposure_detail_dict[datum]) break html += '</table>' self.summary_report[aggregation_area.lower()] = html def generate_html_reports(self, first_report_dict, second_report_dict): """Generate html file for each aggregation units. It also saves the path of the each aggregation unit in self.html_reports. :: Ex. {"jakarta barat": "/home/jakarta barat.html", "jakarta timur": "/home/jakarta timur.html"} :param first_report_dict: Dictionary report from first impact. :type first_report_dict: dict :param second_report_dict: Dictionary report from second impact. :type second_report_dict: dict """ for aggregation_area in first_report_dict: html = html_header() html += ('<table width="100%" style="position:absolute;left:0px;"' 'class="table table-condensed table-striped">') html += '<caption><h4>%s</h4></caption>' % \ aggregation_area.title() html += '<tr>' # First impact on the left side html += '<td width="48%">' html += '<table width="100%">' html += '<thead><th>%s</th></thead>' % \ self.first_impact['exposure_title'].upper() first_exposure_report_dict = first_report_dict[aggregation_area] for first_exposure in first_exposure_report_dict: first_exposure_detail_dict = \ first_exposure_report_dict[first_exposure] html += '<tr><th><i>%s</i></th><th></th></tr>' % \ first_exposure.title() for datum in first_exposure_detail_dict: html += ('<tr>' '<td>%s</td>' '<td>%s</td>' '</tr>') % (datum, first_exposure_detail_dict[datum]) html += '</table>' html += '</td>' # Second impact on the right side if aggregation_area in second_report_dict: # Add spaces between html += '<td width="4%">' html += '</td>' # Second impact report html += '<td width="48%">' html += '<table width="100%">' html += '<thead><th>%s</th></thead>' % \ self.second_impact['exposure_title'].upper() second_exposure_report_dict = \ second_report_dict[aggregation_area] for second_exposure in second_exposure_report_dict: second_exposure_detail_dict = \ second_exposure_report_dict[second_exposure] html += '<tr><th><i>%s</i></th><th></th></tr>' % \ second_exposure.title() for datum in second_exposure_detail_dict: html += ('<tr>' '<td>%s</td>' '<td>%s</td>' '</tr>') % \ (datum, second_exposure_detail_dict[datum]) html += '</table>' html += '</td>' html += '</tr>' html += '</table>' html += html_footer() file_path = '%s.html' % aggregation_area path = os.path.join(temp_dir(), file_path) html_to_file(html, path) self.html_reports[aggregation_area.lower()] = path def generate_reports(self): """Generate PDF reports for each aggregation unit using map composer. First the report template is loaded with the renderer from two impact layers. After it's loaded, if it is not aggregated then we just use composition to produce report. Since there are two impact maps here, we need to set a new extent for these impact maps by a simple calculation. If it is not aggregated then we use a powerful QGIS atlas generation on composition. Since we save each report table representing each aggregated area on self.html_report (which is a dictionary with the aggregation area name as a key and its path as a value), and we set the aggregation area name as current filename on atlas generation, we can match these two so that we have the right report table for each report. For those two cases, we use the same template. The report table is basically an HTML frame. Of course after the merging process is done, we delete each report table on self.html_reports physically on disk. """ # Setup Map Renderer and set all the layer renderer = QgsMapRenderer() layer_set = [self.first_impact['layer'].id(), self.second_impact['layer'].id()] # If aggregated, append chosen aggregation layer if not self.entire_area_mode: layer_set.append(self.aggregation['layer'].id()) # Set Layer set to renderer renderer.setLayerSet(layer_set) # Create composition composition = self.load_template(renderer) # Get Map composer_map = composition.getComposerItemById('impact-map') # Get HTML Report Frame html_report_item = \ composition.getComposerItemById('merged-report-table') html_report_frame = composition.getComposerHtmlByItem(html_report_item) if self.entire_area_mode: # Get composer map size composer_map_width = composer_map.boundingRect().width() composer_map_height = composer_map.boundingRect().height() # Set the extent from two impact layers to fit into composer map composer_size_ratio = float( composer_map_height / composer_map_width) # The extent of two impact layers min_x = min(self.first_impact['layer'].extent().xMinimum(), self.second_impact['layer'].extent().xMinimum()) min_y = min(self.first_impact['layer'].extent().yMinimum(), self.second_impact['layer'].extent().yMinimum()) max_x = max(self.first_impact['layer'].extent().xMaximum(), self.second_impact['layer'].extent().xMaximum()) max_y = max(self.first_impact['layer'].extent().yMaximum(), self.second_impact['layer'].extent().yMaximum()) max_width = max_x - min_x max_height = max_y - min_y layers_size_ratio = float(max_height / max_width) center_x = min_x + float(max_width / 2.0) center_y = min_y + float(max_height / 2.0) # The extent should fit the composer map size new_width = max_width new_height = max_height # QgsComposerMap only overflows to height, so if it overflows, # the extent of the width should be widened if layers_size_ratio > composer_size_ratio: new_width = max_height / composer_size_ratio # Set new extent fit_min_x = center_x - (new_width / 2.0) fit_max_x = center_x + (new_width / 2.0) fit_min_y = center_y - (new_height / 2.0) fit_max_y = center_y + (new_height / 2.0) # Create the extent and set it to the map map_extent = QgsRectangle( fit_min_x, fit_min_y, fit_max_x, fit_max_y) composer_map.setNewExtent(map_extent) # Add grid to composer map split_count = 5 x_interval = new_width / split_count composer_map.setGridIntervalX(x_interval) y_interval = new_height / split_count composer_map.setGridIntervalY(y_interval) # Self.html_reports must have only 1 key value pair area_title = list(self.html_reports.keys())[0] # Set Report Summary summary_report = composition.getComposerItemById('summary-report') summary_report.setText(self.summary_report[area_title]) # Set Aggregation Area Label area_label = composition.getComposerItemById('aggregation-area') area_label.setText(area_title.title()) # Set merged-report-table html_report_path = self.html_reports[area_title] #noinspection PyArgumentList html_frame_url = QUrl.fromLocalFile(html_report_path) html_report_frame.setUrl(html_frame_url) # Export composition to PDF file file_name = '_'.join(area_title.split()) file_path = '%s.pdf' % file_name path = os.path.join(self.out_dir, file_path) composition.exportAsPDF(path) else: # Create atlas composition: atlas = QgsAtlasComposition(composition) # Set coverage layer # Map will be clipped by features from this layer: atlas.setCoverageLayer(self.aggregation['layer']) # Add grid to composer map from coverage layer split_count = 5 map_width = self.aggregation['layer'].extent().width() map_height = self.aggregation['layer'].extent().height() x_interval = map_width / split_count composer_map.setGridIntervalX(x_interval) y_interval = map_height / split_count composer_map.setGridIntervalY(y_interval) # Set composer map that will be used for printing atlas atlas.setComposerMap(composer_map) # set output filename pattern atlas.setFilenamePattern( self.aggregation['aggregation_attribute']) # Start rendering atlas.beginRender() # Iterate all aggregation unit in aggregation layer for i in range(0, atlas.numFeatures()): atlas.prepareForFeature(i) current_filename = atlas.currentFilename() file_name = '_'.join(current_filename.split()) file_path = '%s.pdf' % file_name path = os.path.join(self.out_dir, file_path) # Only print the area that has the report area_title = current_filename.lower() if area_title in self.summary_report: # Set Report Summary summary_report = composition.getComposerItemById( 'summary-report') summary_report.setText(self.summary_report[area_title]) # Set Aggregation Area Label area_label = composition.getComposerItemById( 'aggregation-area') area_label.setText(area_title.title()) # Set merged-report-table html_report_path = self.html_reports[area_title] #noinspection PyArgumentList html_frame_url = QUrl.fromLocalFile(html_report_path) html_report_frame.setUrl(html_frame_url) # Export composition to PDF file composition.exportAsPDF(path) # End of rendering atlas.endRender() #noinspection PyArgumentList def load_template(self, renderer): """Load composer template for merged report. Validate it as well. The template needs to have: 1. QgsComposerMap with id 'impact-map' for merged impact map. 2. QgsComposerPicture with id 'safe-logo' for InaSAFE logo. 3. QgsComposerLabel with id 'summary-report' for a summary of two impacts. 4. QgsComposerLabel with id 'aggregation-area' to indicate the area of aggregation. 5. QgsComposerScaleBar with id 'map-scale' for impact map scale. 6. QgsComposerLegend with id 'map-legend' for impact map legend. 7. QgsComposerPicture with id 'organisation-logo' for organisation logo. 8. QgsComposerLegend with id 'impact-legend' for map legend. 9. QgsComposerHTML with id 'merged-report-table' for the merged report. :param renderer: Map renderer :type renderer: QgsMapRenderer """ # Create Composition composition = QgsComposition(renderer) template_file = QtCore.QFile(self.template_path) template_file.open(QtCore.QIODevice.ReadOnly | QtCore.QIODevice.Text) template_content = template_file.readAll() template_file.close() # Create a dom document containing template content document = QtXml.QDomDocument() document.setContent(template_content) # Prepare Map Substitution impact_title = '%s and %s' % ( self.first_impact['map_title'], self.second_impact['map_title']) substitution_map = { 'impact-title': impact_title, 'hazard-title': self.first_impact['hazard_title'], 'disclaimer': self.disclaimer } # Load template load_status = composition.loadFromTemplate(document, substitution_map) if not load_status: raise ReportCreationError( self.tr('Error loading template %s') % self.template_path) # Validate all needed composer components component_ids = ['impact-map', 'safe-logo', 'summary-report', 'aggregation-area', 'map-scale', 'map-legend', 'organisation-logo', 'merged-report-table'] for component_id in component_ids: component = composition.getComposerItemById(component_id) if component is None: raise ReportCreationError(self.tr( 'Component %s could not be found' % component_id)) # Set InaSAFE logo safe_logo = composition.getComposerItemById('safe-logo') safe_logo.setPictureFile(self.safe_logo_path) # set organisation logo org_logo = composition.getComposerItemById('organisation-logo') org_logo.setPictureFile(self.organisation_logo_path) # Set Map Legend legend = composition.getComposerItemById('map-legend') legend.updateLegend() return composition
def _clip_raster_layer( layer, extent, cell_size=None, extra_keywords=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. :param layer: A valid QGIS raster layer in EPSG:4326 :type layer: QgsRasterLayer :param extent: 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 currently only supported for vector datasets.** :type extent: list(float), QgsGeometry :param cell_size: 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. :type cell_size: float :returns: Output clipped layer (placed in the system temp dir). :rtype: QgsRasterLayer :raises: InvalidProjectionError - if input layer is a density layer in projected coordinates. See issue #123. """ if not layer or not extent: message = tr('Layer or Extent passed to clip is None.') raise InvalidParameterError(message) if layer.type() != QgsMapLayer.RasterLayer: message = tr( 'Expected a raster layer but received a %s.' % str(layer.type())) raise InvalidParameterError(message) working_layer = str(layer.source()) # Check for existence of keywords file base, _ = os.path.splitext(working_layer) keywords_path = base + '.keywords' message = tr( 'Input file to be clipped "%s" does not have the ' 'expected keywords file %s' % ( working_layer, keywords_path )) verify(os.path.isfile(keywords_path), message) # 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. keywords = read_file_keywords(keywords_path) if 'datatype' in keywords and keywords['datatype'] == 'density': if str(layer.crs().authid()) != 'EPSG:4326': # This layer is not WGS84 geographic message = ( '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' % ( working_layer, layer.crs().toProj4() )) raise InvalidProjectionError(message) # We need to provide gdalwarp with a dataset for the clip # because unline gdal_translate, it does not take projwin. clip_kml = extent_to_kml(extent) # Create a filename for the clipped, resampled and reprojected layer handle, filename = tempfile.mkstemp('.tif', 'clip_', temp_dir()) os.close(handle) os.remove(filename) # If no cell size is specified, we need to run gdalwarp without # specifying the output pixel size to ensure the raster dims # remain consistent. binary_list = which('gdalwarp') LOGGER.debug('Path for gdalwarp: %s' % binary_list) if len(binary_list) < 1: raise CallGDALError( tr('gdalwarp could not be found on your computer')) # Use the first matching gdalwarp found binary = binary_list[0] if cell_size is None: command = ( '"%s" -q -t_srs EPSG:4326 -r near -cutline %s -crop_to_cutline ' '-of GTiff "%s" "%s"' % ( binary, clip_kml, working_layer, filename)) else: command = ( '"%s" -q -t_srs EPSG:4326 -r near -tr %f %f -cutline %s ' '-crop_to_cutline -of GTiff "%s" "%s"' % ( binary, cell_size, cell_size, clip_kml, working_layer, filename)) LOGGER.debug(command) result = QProcess().execute(command) # For QProcess exit codes see # http://qt-project.org/doc/qt-4.8/qprocess.html#execute if result == -2: # cannot be started message_detail = tr('Process could not be started.') message = tr( '<p>Error while executing the following shell command:' '</p><pre>%s</pre><p>Error message: %s' % (command, message_detail)) raise CallGDALError(message) elif result == -1: # process crashed message_detail = tr('Process crashed.') message = tr('<p>Error while executing the following shell command:</p>' '<pre>%s</pre><p>Error message: %s' % (command, message_detail)) raise CallGDALError(message) # .. todo:: Check the result of the shell call is ok keyword_io = KeywordIO() keyword_io.copy_keywords(layer, filename, extra_keywords=extra_keywords) base_name = '%s clipped' % layer.name() layer = QgsRasterLayer(filename, base_name) return layer
def __init__(self, parent, iface, dock=None, layer=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. :param parent: Parent widget of this dialog. :type parent: QWidget :param iface: Quantum GIS QGisAppInterface instance. :type iface: QGisAppInterface :param dock: Dock widget instance that we can notify of changes to the keywords. Optional. :type dock: Dock """ QtGui.QDialog.__init__(self, parent) self.setupUi(self) self.setWindowTitle( self.tr('InaSAFE %s Keywords Editor' % get_version())) # Save reference to the QGIS interface and parent self.iface = iface self.parent = parent self.dock = dock if layer is None: self.layer = iface.activeLayer() else: self.layer = layer self.keyword_io = 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.standard_exposure_list = OrderedDict([ ('population', self.tr('population')), ('structure', self.tr('structure')), ('road', self.tr('road')), ('Not Set', self.tr('Not Set')) ]) self.standard_hazard_list = 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')) ]) self.lstKeywords.itemClicked.connect(self.edit_key_value_pair) # Set up help dialog showing logic. help_button = self.buttonBox.button(QtGui.QDialogButtonBox.Help) help_button.clicked.connect(self.show_help) # set some initial ui state: self.defaults = breakdown_defaults() self.pbnAdvanced.setChecked(False) self.radPredefined.setChecked(True) self.dsbFemaleRatioDefault.blockSignals(True) self.dsbFemaleRatioDefault.setValue(self.defaults['FEM_RATIO']) self.dsbFemaleRatioDefault.blockSignals(False) if self.layer: self.load_state_from_keywords() # add a reload from keywords button reload_button = self.buttonBox.addButton( self.tr('Reload'), QtGui.QDialogButtonBox.ActionRole) reload_button.clicked.connect(self.load_state_from_keywords) self.grpAdvanced.setVisible(False) self.resize_dialog()
class KeywordsDialog(QtGui.QDialog, Ui_KeywordsDialogBase): """Dialog implementation class for the InaSAFE keywords editor.""" def __init__(self, parent, iface, dock=None, layer=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. :param parent: Parent widget of this dialog. :type parent: QWidget :param iface: Quantum GIS QGisAppInterface instance. :type iface: QGisAppInterface :param dock: Dock widget instance that we can notify of changes to the keywords. Optional. :type dock: Dock """ QtGui.QDialog.__init__(self, parent) self.setupUi(self) self.setWindowTitle( self.tr('InaSAFE %s Keywords Editor' % get_version())) # Save reference to the QGIS interface and parent self.iface = iface self.parent = parent self.dock = dock if layer is None: self.layer = iface.activeLayer() else: self.layer = layer self.keyword_io = 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.standard_exposure_list = OrderedDict([ ('population', self.tr('population')), ('structure', self.tr('structure')), ('road', self.tr('road')), ('Not Set', self.tr('Not Set')) ]) self.standard_hazard_list = 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')) ]) self.lstKeywords.itemClicked.connect(self.edit_key_value_pair) # Set up help dialog showing logic. help_button = self.buttonBox.button(QtGui.QDialogButtonBox.Help) help_button.clicked.connect(self.show_help) # set some initial ui state: self.defaults = breakdown_defaults() self.pbnAdvanced.setChecked(False) self.radPredefined.setChecked(True) self.dsbFemaleRatioDefault.blockSignals(True) self.dsbFemaleRatioDefault.setValue(self.defaults['FEM_RATIO']) self.dsbFemaleRatioDefault.blockSignals(False) if self.layer: self.load_state_from_keywords() # add a reload from keywords button reload_button = self.buttonBox.addButton( self.tr('Reload'), QtGui.QDialogButtonBox.ActionRole) reload_button.clicked.connect(self.load_state_from_keywords) self.grpAdvanced.setVisible(False) self.resize_dialog() def set_layer(self, layer): """Set the layer associated with the keyword editor. :param layer: Layer whose keywords should be edited. :type layer: QgsMapLayer """ self.layer = layer self.load_state_from_keywords() #noinspection PyMethodMayBeStatic def show_help(self): """Load the help text for the keywords dialog.""" show_context_help(context='keywords') def toggle_postprocessing_widgets(self): """Hide or show the post processing widgets depending on context.""" LOGGER.debug('togglePostprocessingWidgets') postprocessing_flag = self.radPostprocessing.isChecked() self.cboSubcategory.setVisible(not postprocessing_flag) self.lblSubcategory.setVisible(not postprocessing_flag) self.show_aggregation_attribute(postprocessing_flag) self.show_female_ratio_attribute(postprocessing_flag) self.show_female_ratio_default(postprocessing_flag) def show_aggregation_attribute(self, visible_flag): """Hide or show the aggregation attribute in the keyword editor dialog. :param visible_flag: Flag indicating if the aggregation attribute should be hidden or shown. :type visible_flag: bool """ box = self.cboAggregationAttribute box.blockSignals(True) box.clear() box.blockSignals(False) if visible_flag: current_keyword = self.get_value_for_key( self.defaults['AGGR_ATTR_KEY']) fields, attribute_position = layer_attribute_names( self.layer, [QtCore.QVariant.Int, QtCore.QVariant.String], current_keyword) box.addItems(fields) if attribute_position is None: box.setCurrentIndex(0) else: box.setCurrentIndex(attribute_position) box.setVisible(visible_flag) self.lblAggregationAttribute.setVisible(visible_flag) def show_female_ratio_attribute(self, visible_flag): """Hide or show the female ratio attribute in the dialog. :param visible_flag: Flag indicating if the female ratio attribute should be hidden or shown. :type visible_flag: bool """ box = self.cboFemaleRatioAttribute box.blockSignals(True) box.clear() box.blockSignals(False) if visible_flag: current_keyword = self.get_value_for_key( self.defaults['FEM_RATIO_ATTR_KEY']) fields, attribute_position = layer_attribute_names( self.layer, [QtCore.QVariant.Double], current_keyword) fields.insert(0, self.tr('Use default')) fields.insert(1, self.tr('Don\'t use')) box.addItems(fields) if current_keyword == self.tr('Use default'): box.setCurrentIndex(0) elif current_keyword == self.tr('Don\'t use'): box.setCurrentIndex(1) elif attribute_position is None: # current_keyword was not found in the attribute table. # Use default box.setCurrentIndex(0) else: # + 2 is because we add use defaults and don't use box.setCurrentIndex(attribute_position + 2) box.setVisible(visible_flag) self.lblFemaleRatioAttribute.setVisible(visible_flag) def show_female_ratio_default(self, visible_flag): """Hide or show the female ratio default attribute in the dialog. :param visible_flag: Flag indicating if the female ratio default attribute should be hidden or shown. :type visible_flag: bool """ box = self.dsbFemaleRatioDefault if visible_flag: current_value = self.get_value_for_key( self.defaults['FEM_RATIO_KEY']) if current_value is None: val = self.defaults['FEM_RATIO'] else: val = float(current_value) box.setValue(val) box.setVisible(visible_flag) self.lblFemaleRatioDefault.setVisible(visible_flag) # prevents actions being handled twice # noinspection PyPep8Naming @pyqtSignature('int') def on_cboAggregationAttribute_currentIndexChanged(self, index=None): """Handler for aggregation attribute combo change. :param index: Not used but required for slot. """ del index self.add_list_entry(self.defaults['AGGR_ATTR_KEY'], self.cboAggregationAttribute.currentText()) # prevents actions being handled twice # noinspection PyPep8Naming @pyqtSignature('int') def on_cboFemaleRatioAttribute_currentIndexChanged(self, index=None): """Handler for female ratio attribute change. :param index: Not used but required for slot. """ del index text = self.cboFemaleRatioAttribute.currentText() if text == self.tr('Use default'): self.dsbFemaleRatioDefault.setEnabled(True) current_default = self.get_value_for_key( self.defaults['FEM_RATIO_KEY']) if current_default is None: self.add_list_entry(self.defaults['FEM_RATIO_KEY'], self.dsbFemaleRatioDefault.value()) else: self.dsbFemaleRatioDefault.setEnabled(False) self.remove_item_by_key(self.defaults['FEM_RATIO_KEY']) self.add_list_entry(self.defaults['FEM_RATIO_ATTR_KEY'], text) # prevents actions being handled twice # noinspection PyPep8Naming @pyqtSignature('double') def on_dsbFemaleRatioDefault_valueChanged(self, value): """Handler for female ration default value changing. :param value: Not used but required for slot. """ del value box = self.dsbFemaleRatioDefault if box.isEnabled(): self.add_list_entry(self.defaults['FEM_RATIO_KEY'], box.value()) # prevents actions being handled twice # noinspection PyPep8Naming @pyqtSignature('bool') def on_pbnAdvanced_toggled(self, flag): """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! :param flag: Flag indicating the new checked state of the button. :type flag: bool """ self.toggle_advanced(flag) def toggle_advanced(self, flag): """Hide or show advanced editor. :param flag: Desired state for advanced editor visibility. :type flag: bool """ if flag: self.pbnAdvanced.setText(self.tr('Hide advanced editor')) else: self.pbnAdvanced.setText(self.tr('Show advanced editor')) self.grpAdvanced.setVisible(flag) self.resize_dialog() # prevents actions being handled twice # noinspection PyPep8Naming @pyqtSignature('bool') def on_radHazard_toggled(self, flag): """Automatic slot executed when the hazard radio is toggled. :param flag: Flag indicating the new checked state of the button. :type flag: bool """ if not flag: return self.set_category('hazard') self.update_controls_from_list() # prevents actions being handled twice # noinspection PyPep8Naming @pyqtSignature('bool') def on_radExposure_toggled(self, theFlag): """Automatic slot executed when the hazard radio is toggled on. :param theFlag: Flag indicating the new checked state of the button. :type theFlag: bool """ if not theFlag: return self.set_category('exposure') self.update_controls_from_list() # prevents actions being handled twice # noinspection PyPep8Naming @pyqtSignature('bool') def on_radPostprocessing_toggled(self, flag): """Automatic slot executed when the hazard radio is toggled on. :param flag: Flag indicating the new checked state of the button. :type flag: bool """ if not flag: self.remove_item_by_key(self.defaults['AGGR_ATTR_KEY']) self.remove_item_by_key(self.defaults['FEM_RATIO_ATTR_KEY']) self.remove_item_by_key(self.defaults['FEM_RATIO_KEY']) return self.set_category('postprocessing') self.update_controls_from_list() # prevents actions being handled twice # noinspection PyPep8Naming @pyqtSignature('int') def on_cboSubcategory_currentIndexChanged(self, index=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. :param index: Not used but required for Qt slot. """ if index == -1: self.remove_item_by_key('subcategory') return text = self.cboSubcategory.itemData(self.cboSubcategory.currentIndex()) # I found that myText is 'Not Set' for every language if text == self.tr('Not Set') or text == 'Not Set': self.remove_item_by_key('subcategory') return tokens = text.split(' ') if len(tokens) < 1: self.remove_item_by_key('subcategory') return subcategory = tokens[0] self.add_list_entry('subcategory', subcategory) # Some subcategories e.g. roads have no units or datatype if len(tokens) == 1: return if tokens[1].find('[') < 0: return category = self.get_value_for_key('category') if 'hazard' == category: units = tokens[1].replace('[', '').replace(']', '') self.add_list_entry('unit', units) if 'exposure' == category: data_type = tokens[1].replace('[', '').replace(']', '') self.add_list_entry('datatype', data_type) # prevents actions being handled twice def set_subcategory_list(self, entries, selected_item=None): """Helper to populate the subcategory list based on category context. :param entries: An OrderedDict of subcategories. The dict entries should be in the form ('earthquake', self.tr('earthquake')). See http://www.voidspace.org.uk/python/odict.html for info on OrderedDict. :type entries: OrderedDict :param selected_item: Which item should be selected in the combo. If the selected item is not in entries, it will be appended to it. This is optional. :type selected_item: str """ # To avoid triggering on_cboSubcategory_currentIndexChanged # we block signals from the combo while updating it self.cboSubcategory.blockSignals(True) self.cboSubcategory.clear() item_selected_flag = selected_item is not None selected_item_values = selected_item not in entries.values() selected_item_keys = selected_item not in entries.keys() if (item_selected_flag and selected_item_values and selected_item_keys): # Add it to the OrderedList entries[selected_item] = selected_item index = 0 selected_index = 0 for key, value in entries.iteritems(): if value == selected_item or key == selected_item: selected_index = index index += 1 self.cboSubcategory.addItem(value, key) self.cboSubcategory.setCurrentIndex(selected_index) self.cboSubcategory.blockSignals(False) # prevents actions being handled twice # noinspection PyPep8Naming @pyqtSignature('') def on_pbnAddToList1_clicked(self): """Automatic slot executed when the pbnAddToList1 button is pressed. """ if (self.lePredefinedValue.text() != "" and self.cboKeyword.currentText() != ""): current_key = self.tr(self.cboKeyword.currentText()) current_value = self.lePredefinedValue.text() self.add_list_entry(current_key, current_value) self.lePredefinedValue.setText('') self.update_controls_from_list() # prevents actions being handled twice # noinspection PyPep8Naming @pyqtSignature('') def on_pbnAddToList2_clicked(self): """Automatic slot executed when the pbnAddToList2 button is pressed. """ current_key = self.leKey.text() current_value = self.leValue.text() if current_key == 'category' and current_value == 'hazard': self.radHazard.blockSignals(True) self.radHazard.setChecked(True) self.set_subcategory_list(self.standard_hazard_list) self.radHazard.blockSignals(False) elif current_key == 'category' and current_value == 'exposure': self.radExposure.blockSignals(True) self.radExposure.setChecked(True) self.set_subcategory_list(self.standard_exposure_list) self.radExposure.blockSignals(False) elif current_key == 'category': #.. todo:: notify the user their category is invalid pass self.add_list_entry(current_key, current_value) self.leKey.setText('') self.leValue.setText('') self.update_controls_from_list() # prevents actions being handled twice # noinspection PyPep8Naming @pyqtSignature('') def on_pbnRemove_clicked(self): """Automatic slot executed when the pbnRemove button is pressed. Any selected items in the keywords list will be removed. """ for item in self.lstKeywords.selectedItems(): self.lstKeywords.takeItem(self.lstKeywords.row(item)) self.leKey.setText('') self.leValue.setText('') self.update_controls_from_list() def add_list_entry(self, key, value): """Add an item to the keywords list given its key/value. The key and value must both be valid, non empty strings or an InvalidKVPError will be raised. If an entry with the same key exists, it's value will be replaced with value. 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 ('|'). :param key: The key part of the key value pair (kvp). :type key: str :param value: Value part of the key value pair (kvp). :type value: str """ if key is None or key == '': return if value is None or value == '': return # make sure that both key and value is string key = str(key) value = str(value) message = '' if ':' in key: key = key.replace(':', '.') message = self.tr('Colons are not allowed, replaced with "."') if ':' in value: value = value.replace(':', '.') message = self.tr('Colons are not allowed, replaced with "."') if message == '': self.lblMessage.setText('') self.lblMessage.hide() else: self.lblMessage.setText(message) self.lblMessage.show() item = QtGui.QListWidgetItem(key + ':' + value) # We are going to replace, so remove it if it exists already self.remove_item_by_key(key) data = key + '|' + value item.setData(QtCore.Qt.UserRole, data) self.lstKeywords.insertItem(0, item) def set_category(self, category): """Set the category radio button based on category. :param category: Either 'hazard', 'exposure' or 'postprocessing'. :type category: str :returns: False if radio button could not be updated, otherwise True. :rtype: bool """ # convert from QString if needed category = str(category) if self.get_value_for_key('category') == category: #nothing to do, go home return True if category 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 category == '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.remove_item_by_key('subcategory') self.remove_item_by_key('datatype') self.add_list_entry('category', 'hazard') hazard_list = self.standard_hazard_list self.set_subcategory_list(hazard_list) elif category == 'exposure': self.reset() self.radExposure.blockSignals(True) self.radExposure.setChecked(True) self.radExposure.blockSignals(False) self.remove_item_by_key('subcategory') self.remove_item_by_key('unit') self.add_list_entry('category', 'exposure') exposure_list = self.standard_exposure_list self.set_subcategory_list(exposure_list) else: self.reset() self.radPostprocessing.blockSignals(True) self.radPostprocessing.setChecked(True) self.radPostprocessing.blockSignals(False) self.remove_item_by_key('subcategory') self.add_list_entry('category', 'postprocessing') return True def reset(self, primary_keywords_only=True): """Reset all controls to a blank state. :param primary_keywords_only: If True (the default), only reset Subcategory, datatype and units. :type primary_keywords_only: bool """ self.cboSubcategory.clear() self.remove_item_by_key('subcategory') self.remove_item_by_key('datatype') self.remove_item_by_key('unit') self.remove_item_by_key('source') if not primary_keywords_only: # Clear everything else too self.lstKeywords.clear() self.leKey.clear() self.leValue.clear() self.lePredefinedValue.clear() self.leTitle.clear() self.leSource.clear() def remove_item_by_key(self, removal_key): """Remove an item from the kvp list given its key. :param removal_key: Key of item to be removed. :type removal_key: str """ for myCounter in range(self.lstKeywords.count()): existing_item = self.lstKeywords.item(myCounter) text = existing_item.text() tokens = text.split(':') if len(tokens) < 2: break key = tokens[0] if removal_key == key: # remove it since the removal_key is already present self.lstKeywords.takeItem(myCounter) break def remove_item_by_value(self, removal_value): """Remove an item from the kvp list given its key. :param removal_value: Value of item to be removed. :type removal_value: str """ for counter in range(self.lstKeywords.count()): existing_item = self.lstKeywords.item(counter) text = existing_item.text() tokens = text.split(':') value = tokens[1] if removal_value == value: # remove it since the key is already present self.lstKeywords.takeItem(counter) break def get_value_for_key(self, lookup_key): """If key list contains a specific key, return its value. :param lookup_key: The key to search for :type lookup_key: str :returns: Value of key if matched otherwise none. :rtype: str """ for counter in range(self.lstKeywords.count()): existing_item = self.lstKeywords.item(counter) text = existing_item.text() tokens = text.split(':') key = str(tokens[0]).strip() value = str(tokens[1]).strip() if lookup_key == key: return value return None def load_state_from_keywords(self): """Set the ui state to match the keywords of the active layer. 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. Also if only title is set we use similar logic (title is added by default in dock and other defaults need to be explicitly added when opening this dialog). See #751 """ keywords = {'category': 'exposure'} try: # Now read the layer with sub layer if needed keywords = self.keyword_io.read_keywords(self.layer) except (InvalidParameterError, HashNotFoundError, NoKeywordsFoundError): pass layer_name = self.layer.name() if 'title' not in keywords: self.leTitle.setText(layer_name) self.lblLayerName.setText(self.tr('Keywords for %s' % layer_name)) if 'source' in keywords: self.leSource.setText(keywords['source']) else: self.leSource.setText('') # if we have a category key, unpack it first # so radio button etc get set if 'category' in keywords: self.set_category(keywords['category']) keywords.pop('category') else: # assume exposure to match ui. See issue #751 self.add_list_entry('category', 'exposure') for key in keywords.iterkeys(): self.add_list_entry(key, str(keywords[key])) # now make the rest of the safe_qgis reflect the list entries self.update_controls_from_list() def update_controls_from_list(self): """Set the ui state to match the keywords of the active layer.""" subcategory = self.get_value_for_key('subcategory') units = self.get_value_for_key('unit') data_type = self.get_value_for_key('datatype') title = self.get_value_for_key('title') if title is not None: self.leTitle.setText(title) elif self.layer is not None: layer_name = self.layer.name() self.lblLayerName.setText(self.tr('Keywords for %s' % layer_name)) else: self.lblLayerName.setText('') if not is_polygon_layer(self.layer): self.radPostprocessing.setEnabled(False) # adapt gui if we are in postprocessing category self.toggle_postprocessing_widgets() if self.radExposure.isChecked(): if subcategory is not None and data_type is not None: self.set_subcategory_list(self.standard_exposure_list, subcategory + ' [' + data_type + ']') elif subcategory is not None: self.set_subcategory_list(self.standard_exposure_list, subcategory) else: self.set_subcategory_list(self.standard_exposure_list, self.tr('Not Set')) elif self.radHazard.isChecked(): if subcategory is not None and units is not None: self.set_subcategory_list(self.standard_hazard_list, subcategory + ' [' + units + ']') elif subcategory is not None: self.set_subcategory_list(self.standard_hazard_list, subcategory) else: self.set_subcategory_list(self.standard_hazard_list, self.tr('Not Set')) self.resize_dialog() def resize_dialog(self): """Resize the dialog to fit its contents.""" # noinspection PyArgumentList QtCore.QCoreApplication.processEvents() LOGGER.debug('adjust ing dialog size') self.adjustSize() # prevents actions being handled twice # noinspection PyPep8Naming @pyqtSignature('QString') def on_leTitle_textEdited(self, title): """Update the keywords list whenever the user changes the title. This slot is not called if the title is changed programmatically. :param title: New title keyword for the layer. :type title: str """ self.add_list_entry('title', str(title)) # prevents actions being handled twice # noinspection PyPep8Naming @pyqtSignature('QString') def on_leSource_textEdited(self, source): """Update the keywords list whenever the user changes the source. This slot is not called if the source is changed programmatically. :param source: New source keyword for the layer. :type source: str """ if source is None or source == '': self.remove_item_by_key('source') else: self.add_list_entry('source', str(source)) def get_keywords(self): """Obtain the state of the dialog as a keywords dict. :returns: Keywords reflecting the state of the dialog. :rtype: dict """ #make sure title is listed if str(self.leTitle.text()) != '': self.add_list_entry('title', str(self.leTitle.text())) # make sure the source is listed too if str(self.leSource.text()) != '': self.add_list_entry('source', str(self.leSource.text())) keywords = {} for myCounter in range(self.lstKeywords.count()): existing_item = self.lstKeywords.item(myCounter) text = existing_item.text() tokens = text.split(':') key = str(tokens[0]).strip() value = str(tokens[1]).strip() keywords[key] = value return keywords def accept(self): """Automatic slot executed when the ok button is pressed. It will write out the keywords for the layer that is active. """ self.apply_changes() keywords = self.get_keywords() try: self.keyword_io.write_keywords(layer=self.layer, keywords=keywords) except InaSAFEError, e: error_message = get_error_message(e) # noinspection PyCallByClass,PyTypeChecker,PyArgumentList QtGui.QMessageBox.warning(self, self.tr('InaSAFE'), ((self.tr( 'An error was encountered when saving the keywords:\n' '%s' % error_message.to_html())))) if self.dock is not None: self.dock.get_layers() self.done(QtGui.QDialog.Accepted)
class PostprocessorManager(QtCore.QObject): """A manager for post processing of impact function results. """ def __init__(self, theAggregator): """Director for aggregation based operations. Args: theAggregationLayer: QgsMapLayer representing clipped aggregation. This will be converted to a memory layer inside this class. see self.aggregator.layer Returns: not applicable Raises: no exceptions explicitly raised """ super(PostprocessorManager, self).__init__() # Aggregation / post processing related items self.postProcessingOutput = {} self.keywordIO = KeywordIO() self.errorMessage = None self.aggregator = theAggregator def _sumFieldName(self): return self.aggregator.prefix + 'sum' def _sortNoData(self, data): """Check if the value field of the postprocessor is NO_DATA. this is used for sorting, it returns -1 if the value is NO_DATA, so that no data items can be put at the end of a list Args: list - data Returns: returns -1 if the value is NO_DATA else the value """ #black magic to get the value of each postprocessor field #get the first postprocessor just to discover the data structure myFirsPostprocessor = self.postProcessingOutput.itervalues().next() #get the key position of the value field myValueKey = myFirsPostprocessor[0][1].keyAt(0) #get the value # data[1] is the orderedDict # data[1][myFirstKey] is the 1st indicator in the orderedDict if data[1][myValueKey]['value'] == self.aggregator.defaults['NO_DATA']: myPosition = -1 else: myPosition = data[1][myValueKey]['value'] #FIXME MB this is to dehumanize the strings and have ints myPosition = myPosition.replace(',', '') myPosition = int(float(myPosition)) return myPosition def _generateTables(self): """Parses the postprocessing output as one table per postprocessor. Args: None Returns: str - a string containing the html """ myMessage = m.Message() for proc, resList in self.postProcessingOutput.iteritems(): # resList is for example: # [ # (PyQt4.QtCore.QString(u'Entire area'), OrderedDict([ # (u'Total', {'value': 977536, 'metadata': {}}), # (u'Female population', {'value': 508319, 'metadata': {}}), # (u'Weekly hygiene packs', {'value': 403453, 'metadata': { # 'description': 'Females hygiene packs for weekly use'}}) # ])) #] try: #sorting using the first indicator of a postprocessor sortedResList = sorted(resList, key=self._sortNoData, reverse=True) except KeyError: LOGGER.debug('Skipping sorting as the postprocessor did not ' 'have a "Total" field') #init table hasNoDataValues = False myTable = m.Table( style_class='table table-condensed table-striped') myTable.caption = self.tr('Detailed %1 report').arg( safeTr(get_postprocessor_human_name(proc)).lower()) myHeaderRow = m.Row() myHeaderRow.add(str(self.attributeTitle).capitalize()) for calculationName in sortedResList[0][1]: myHeaderRow.add(self.tr(calculationName)) myTable.add(myHeaderRow) for zoneName, calc in sortedResList: myRow = m.Row(zoneName) for _, calculationData in calc.iteritems(): myValue = calculationData['value'] if myValue == self.aggregator.defaults['NO_DATA']: hasNoDataValues = True myValue += ' *' myRow.add(myValue) myTable.add(myRow) #add table to message myMessage.add(myTable) if hasNoDataValues: myMessage.add( m.EmphasizedText( self. tr('* "%1" values mean that there where some problems while ' 'calculating them. This did not affect the other ' 'values.').arg( self.aggregator.defaults['NO_DATA']))) try: if (self.keywordIO.read_keywords(self.aggregator.layer, 'HAD_MULTIPART_POLY')): myMessage.add( m.EmphasizedText( self. tr('The aggregation layer had multipart polygons, these have ' 'been exploded and are now marked with a #. This has no ' 'influence on the calculation, just keep in mind that the ' 'attributes shown may represent the original multipart ' 'polygon and not the individual exploded polygon parts.' ))) except Exception: # pylint: disable=W0703 pass return myMessage def run(self): """Run any post processors requested by the impact function. Args: None Returns: None Raises: None """ try: myRequestedPostProcessors = self.functionParams['postprocessors'] myPostProcessors = get_postprocessors(myRequestedPostProcessors) except (TypeError, KeyError): # TypeError is for when functionParams is none # KeyError is for when ['postprocessors'] is unavailable myPostProcessors = {} LOGGER.debug('Running this postprocessors: ' + str(myPostProcessors)) myFeatureNameAttribute = self.aggregator.attributes[ self.aggregator.defaults['AGGR_ATTR_KEY']] if myFeatureNameAttribute is None: self.attributeTitle = self.tr('Aggregation unit') else: self.attributeTitle = myFeatureNameAttribute myNameFieldIndex = self.aggregator.layer.fieldNameIndex( self.attributeTitle) mySumFieldIndex = self.aggregator.layer.fieldNameIndex( self._sumFieldName()) myFemaleRatioIsVariable = False myFemRatioFieldIndex = None myFemaleRatio = None if 'Gender' in myPostProcessors: #look if we need to look for a variable female ratio in a layer try: myFemRatioField = self.aggregator.attributes[ self.aggregator.defaults['FEM_RATIO_ATTR_KEY']] myFemRatioFieldIndex = self.aggregator.layer.fieldNameIndex( myFemRatioField) myFemaleRatioIsVariable = True except KeyError: try: myFemaleRatio = self.keywordIO.read_keywords( self.aggregator.layer, self.aggregator.defaults['FEM_RATIO_KEY']) except KeywordNotFoundError: myFemaleRatio = self.aggregator.defaults['FEM_RATIO'] #iterate zone features myProvider = self.aggregator.layer.dataProvider() myAttributes = myProvider.attributeIndexes() # start data retreival: fetch no geometry and all attributes for each # feature myProvider.select(myAttributes, QgsRectangle(), False) myFeature = QgsFeature() myPolygonIndex = 0 while myProvider.nextFeature(myFeature): #get all attributes of a feature myAttributeMap = myFeature.attributeMap() #if a feature has no field called if myNameFieldIndex == -1: myZoneName = str(myFeature.id()) else: myZoneName = myAttributeMap[myNameFieldIndex].toString() #create dictionary of attributes to pass to postprocessor myGeneralParams = {'target_field': self.aggregator.targetField} if self.aggregator.statisticsType == 'class_count': myGeneralParams['impact_classes'] = ( self.aggregator.statisticsClasses) elif self.aggregator.statisticsType == 'sum': myImpactTotal, _ = myAttributeMap[mySumFieldIndex].toDouble() myGeneralParams['impact_total'] = myImpactTotal try: myGeneralParams['impact_attrs'] = ( self.aggregator.impactLayerAttributes[myPolygonIndex]) except IndexError: #rasters and attributeless vectors have no attributes myGeneralParams['impact_attrs'] = None for myKey, myValue in myPostProcessors.iteritems(): myParameters = myGeneralParams try: #look if params are available for this postprocessor myParameters.update( self.functionParams['postprocessors'][myKey]['params']) except KeyError: pass if myKey == 'Gender': if myFemaleRatioIsVariable: myFemaleRatio, mySuccessFlag = myAttributeMap[ myFemRatioFieldIndex].toDouble() if not mySuccessFlag: myFemaleRatio = self.aggregator.defaults[ 'FEM_RATIO'] LOGGER.debug(mySuccessFlag) myParameters['female_ratio'] = myFemaleRatio myValue.setup(myParameters) myValue.process() myResults = myValue.results() myValue.clear() # LOGGER.debug(myResults) try: self.postProcessingOutput[myKey].append( (myZoneName, myResults)) except KeyError: self.postProcessingOutput[myKey] = [] self.postProcessingOutput[myKey].append( (myZoneName, myResults)) #increment the index myPolygonIndex += 1 def getOutput(self): """Returns the results of the post processing as a table. Args: theSingleTableFlag - bool indicating if result should be rendered as a single table. Default False. Returns: str - a string containing the html in the requested format. """ # LOGGER.debug(self.postProcessingOutput) if self.errorMessage is not None: myMessage = m.Message( m.Heading(self.tr('Postprocessing report skipped')), m.Paragraph( self.tr( 'Due to a problem while processing the results,' ' the detailed postprocessing report is unavailable:' ' %1').arg(self.errorMessage))) return myMessage return self._generateTables()
class Map(): """A class for creating a map.""" def __init__(self, iface): """Constructor for the Map class. :param iface: Reference to the QGIS iface object. :type iface: QgsAppInterface """ LOGGER.debug('InaSAFE Map class initialised') self.iface = iface self.layer = iface.activeLayer() self.keywordIO = KeywordIO() self.printer = None self.composition = None self.legend = None self.pageWidth = 210 # width in mm self.pageHeight = 297 # height in mm self.pageDpi = 300.0 self.pageMargin = 10 # margin in mm self.verticalSpacing = 1 # vertical spacing between elements self.showFramesFlag = False # intended for debugging use only # make a square map where width = height = page width self.mapHeight = self.pageWidth - (self.pageMargin * 2) self.mapWidth = self.mapHeight self.disclaimer = self.tr('InaSAFE has been jointly developed by' ' BNPB, AusAid & the World Bank') def tr(self, string): """We implement this since we do not inherit QObject. :param string: String for translation. :type string: QString, str :returns: Translated version of theString. :rtype: QString """ # noinspection PyCallByClass,PyTypeChecker,PyArgumentList return QtCore.QCoreApplication.translate('Map', string) def set_impact_layer(self, layer): """Set the layer that will be used for stats, legend and reporting. :param layer: Layer that will be used for stats, legend and reporting. :type layer: QgsMapLayer, QgsRasterLayer, QgsVectorLayer """ self.layer = layer def setup_composition(self): """Set up the composition ready for drawing elements onto it.""" LOGGER.debug('InaSAFE Map setupComposition called') myCanvas = self.iface.mapCanvas() myRenderer = myCanvas.mapRenderer() self.composition = QgsComposition(myRenderer) self.composition.setPlotStyle(QgsComposition.Print) # or preview self.composition.setPaperSize(self.pageWidth, self.pageHeight) self.composition.setPrintResolution(self.pageDpi) self.composition.setPrintAsRaster(True) def compose_map(self): """Place all elements on the map ready for printing.""" self.setup_composition() # Keep track of our vertical positioning as we work our way down # the page placing elements on it. myTopOffset = self.pageMargin self.draw_logo(myTopOffset) myLabelHeight = self.draw_title(myTopOffset) # Update the map offset for the next row of content myTopOffset += myLabelHeight + self.verticalSpacing myComposerMap = self.draw_map(myTopOffset) self.draw_scalebar(myComposerMap, myTopOffset) # Update the top offset for the next horizontal row of items myTopOffset += self.mapHeight + self.verticalSpacing - 1 myImpactTitleHeight = self.draw_impact_title(myTopOffset) # Update the top offset for the next horizontal row of items if myImpactTitleHeight: myTopOffset += myImpactTitleHeight + self.verticalSpacing + 2 self.draw_legend(myTopOffset) self.draw_host_and_time(myTopOffset) self.draw_disclaimer() def render(self): """Render the map composition to an image and save that to disk. :returns: A three-tuple of: * str: myImagePath - absolute path to png of rendered map * QImage: myImage - in memory copy of rendered map * QRectF: myTargetArea - dimensions of rendered map :rtype: tuple """ LOGGER.debug('InaSAFE Map renderComposition called') # NOTE: we ignore self.composition.printAsRaster() and always rasterise myWidth = int(self.pageDpi * self.pageWidth / 25.4) myHeight = int(self.pageDpi * self.pageHeight / 25.4) myImage = QtGui.QImage(QtCore.QSize(myWidth, myHeight), QtGui.QImage.Format_ARGB32) myImage.setDotsPerMeterX(dpi_to_meters(self.pageDpi)) myImage.setDotsPerMeterY(dpi_to_meters(self.pageDpi)) # Only works in Qt4.8 #myImage.fill(QtGui.qRgb(255, 255, 255)) # Works in older Qt4 versions myImage.fill(55 + 255 * 256 + 255 * 256 * 256) myImagePainter = QtGui.QPainter(myImage) mySourceArea = QtCore.QRectF(0, 0, self.pageWidth, self.pageHeight) myTargetArea = QtCore.QRectF(0, 0, myWidth, myHeight) self.composition.render(myImagePainter, myTargetArea, mySourceArea) myImagePainter.end() myImagePath = unique_filename(prefix='mapRender_', suffix='.png', dir=temp_dir()) myImage.save(myImagePath) return myImagePath, myImage, myTargetArea def make_pdf(self, filename): """Generate the printout for our final map. :param filename: Path on the file system to which the pdf should be saved. If None, a generated file name will be used. :type filename: str :returns: File name of the output file (equivalent to filename if provided). :rtype: str """ LOGGER.debug('InaSAFE Map printToPdf called') if filename is None: myMapPdfPath = unique_filename( prefix='report', suffix='.pdf', dir=temp_dir('work')) else: # We need to cast to python string in case we receive a QString myMapPdfPath = str(filename) self.compose_map() self.printer = setup_printer(myMapPdfPath) _, myImage, myRectangle = self.render() myPainter = QtGui.QPainter(self.printer) myPainter.drawImage(myRectangle, myImage, myRectangle) myPainter.end() return myMapPdfPath def draw_logo(self, top_offset): """Add a picture containing the logo to the map top left corner :param top_offset: Vertical offset at which the logo should be drawn. :type top_offset: int """ myLogo = QgsComposerPicture(self.composition) myLogo.setPictureFile(':/plugins/inasafe/bnpb_logo.png') myLogo.setItemPosition(self.pageMargin, top_offset, 10, 10) myLogo.setFrameEnabled(self.showFramesFlag) myLogo.setZValue(1) # To ensure it overlays graticule markers self.composition.addItem(myLogo) def draw_title(self, top_offset): """Add a title to the composition. :param top_offset: Vertical offset at which the logo should be drawn. :type top_offset: int :returns: The height of the label as rendered. :rtype: float """ LOGGER.debug('InaSAFE Map drawTitle called') myFontSize = 14 myFontWeight = QtGui.QFont.Bold myItalicsFlag = False myFont = QtGui.QFont('verdana', myFontSize, myFontWeight, myItalicsFlag) myLabel = QgsComposerLabel(self.composition) myLabel.setFont(myFont) myHeading = self.tr('InaSAFE - Indonesia Scenario Assessment' ' for Emergencies') myLabel.setText(myHeading) myLabel.adjustSizeToText() myLabelHeight = 10.0 # determined using qgis map composer myLabelWidth = 170.0 # item - position and size...option myLeftOffset = self.pageWidth - self.pageMargin - myLabelWidth myLabel.setItemPosition(myLeftOffset, top_offset - 2, # -2 to push it up a little myLabelWidth, myLabelHeight, ) myLabel.setFrameEnabled(self.showFramesFlag) self.composition.addItem(myLabel) return myLabelHeight def draw_map(self, top_offset): """Add a map to the composition and return the composer map instance. :param top_offset: Vertical offset at which the logo should be drawn. :type top_offset: int :returns: The composer map. :rtype: QgsComposerMap """ LOGGER.debug('InaSAFE Map drawMap called') myMapWidth = self.mapWidth myComposerMap = QgsComposerMap( self.composition, self.pageMargin, top_offset, myMapWidth, self.mapHeight) #myExtent = self.iface.mapCanvas().extent() # The dimensions of the map canvas and the print composer map may # differ. So we set the map composer extent using the canvas and # then defer to the map canvas's map extents thereafter # Update: disabled as it results in a rectangular rather than # square map #myComposerMap.setNewExtent(myExtent) myComposerExtent = myComposerMap.extent() # Recenter the composer map on the center of the canvas # Note that since the composer map is square and the canvas may be # arbitrarily shaped, we center based on the longest edge myCanvasExtent = self.iface.mapCanvas().extent() myWidth = myCanvasExtent.width() myHeight = myCanvasExtent.height() myLongestLength = myWidth if myWidth < myHeight: myLongestLength = myHeight myHalfLength = myLongestLength / 2 myCenter = myCanvasExtent.center() myMinX = myCenter.x() - myHalfLength myMaxX = myCenter.x() + myHalfLength myMinY = myCenter.y() - myHalfLength myMaxY = myCenter.y() + myHalfLength mySquareExtent = QgsRectangle(myMinX, myMinY, myMaxX, myMaxY) myComposerMap.setNewExtent(mySquareExtent) myComposerMap.setGridEnabled(True) myNumberOfSplits = 5 # .. todo:: Write logic to adjust precision so that adjacent tick marks # always have different displayed values myPrecision = 2 myXInterval = myComposerExtent.width() / myNumberOfSplits myComposerMap.setGridIntervalX(myXInterval) myYInterval = myComposerExtent.height() / myNumberOfSplits myComposerMap.setGridIntervalY(myYInterval) myComposerMap.setGridStyle(QgsComposerMap.Cross) myCrossLengthMM = 1 myComposerMap.setCrossLength(myCrossLengthMM) myComposerMap.setZValue(0) # To ensure it does not overlay logo myFontSize = 6 myFontWeight = QtGui.QFont.Normal myItalicsFlag = False myFont = QtGui.QFont( 'verdana', myFontSize, myFontWeight, myItalicsFlag) myComposerMap.setGridAnnotationFont(myFont) myComposerMap.setGridAnnotationPrecision(myPrecision) myComposerMap.setShowGridAnnotation(True) myComposerMap.setGridAnnotationDirection( QgsComposerMap.BoundaryDirection, QgsComposerMap.Top) self.composition.addItem(myComposerMap) self.draw_graticule_mask(top_offset) return myComposerMap def draw_graticule_mask(self, top_offset): """A helper function to mask out graticule labels. It will hide labels on the right side by over painting a white rectangle with white border on them. **kludge** :param top_offset: Vertical offset at which the logo should be drawn. :type top_offset: int """ LOGGER.debug('InaSAFE Map drawGraticuleMask called') myLeftOffset = self.pageMargin + self.mapWidth myRect = QgsComposerShape(myLeftOffset + 0.5, top_offset, self.pageWidth - myLeftOffset, self.mapHeight + 1, self.composition) myRect.setShapeType(QgsComposerShape.Rectangle) myPen = QtGui.QPen() myPen.setColor(QtGui.QColor(0, 0, 0)) myPen.setWidthF(0.1) myRect.setPen(myPen) myRect.setBackgroundColor(QtGui.QColor(255, 255, 255)) myRect.setTransparency(100) #myRect.setLineWidth(0.1) #myRect.setFrameEnabled(False) #myRect.setOutlineColor(QtGui.QColor(255, 255, 255)) #myRect.setFillColor(QtGui.QColor(255, 255, 255)) #myRect.setOpacity(100) # These two lines seem superfluous but are needed myBrush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) myRect.setBrush(myBrush) self.composition.addItem(myRect) def draw_native_scalebar(self, composer_map, top_offset): """Draw a scale bar using QGIS' native drawing. In the case of geographic maps, scale will be in degrees, not km. :param composer_map: Composer map on which to draw the scalebar. :type composer_map: QgsComposerMap :param top_offset: Vertical offset at which the logo should be drawn. :type top_offset: int """ LOGGER.debug('InaSAFE Map drawNativeScaleBar called') myScaleBar = QgsComposerScaleBar(self.composition) myScaleBar.setStyle('Numeric') # optionally modify the style myScaleBar.setComposerMap(composer_map) myScaleBar.applyDefaultSize() myScaleBarHeight = myScaleBar.boundingRect().height() myScaleBarWidth = myScaleBar.boundingRect().width() # -1 to avoid overlapping the map border myScaleBar.setItemPosition( self.pageMargin + 1, top_offset + self.mapHeight - (myScaleBarHeight * 2), myScaleBarWidth, myScaleBarHeight) myScaleBar.setFrameEnabled(self.showFramesFlag) # Disabled for now #self.composition.addItem(myScaleBar) def draw_scalebar(self, composer_map, top_offset): """Add a numeric scale to the bottom left of the map. We draw the scale bar manually because QGIS does not yet support rendering a scale bar for a geographic map in km. .. seealso:: :meth:`drawNativeScaleBar` :param composer_map: Composer map on which to draw the scalebar. :type composer_map: QgsComposerMap :param top_offset: Vertical offset at which the logo should be drawn. :type top_offset: int """ LOGGER.debug('InaSAFE Map drawScaleBar called') myCanvas = self.iface.mapCanvas() myRenderer = myCanvas.mapRenderer() # # Add a linear map scale # myDistanceArea = QgsDistanceArea() myDistanceArea.setSourceCrs(myRenderer.destinationCrs().srsid()) myDistanceArea.setEllipsoidalMode(True) # Determine how wide our map is in km/m # Starting point at BL corner myComposerExtent = composer_map.extent() myStartPoint = QgsPoint(myComposerExtent.xMinimum(), myComposerExtent.yMinimum()) # Ending point at BR corner myEndPoint = QgsPoint(myComposerExtent.xMaximum(), myComposerExtent.yMinimum()) myGroundDistance = myDistanceArea.measureLine(myStartPoint, myEndPoint) # Get the equivalent map distance per page mm myMapWidth = self.mapWidth # How far is 1mm on map on the ground in meters? myMMToGroundDistance = myGroundDistance / myMapWidth #print 'MM:', myMMDistance # How long we want the scale bar to be in relation to the map myScaleBarToMapRatio = 0.5 # How many divisions the scale bar should have myTickCount = 5 myScaleBarWidthMM = myMapWidth * myScaleBarToMapRatio myPrintSegmentWidthMM = myScaleBarWidthMM / myTickCount # Segment width in real world (m) # We apply some logic here so that segments are displayed in meters # if each segment is less that 1000m otherwise km. Also the segment # lengths are rounded down to human looking numbers e.g. 1km not 1.1km myUnits = '' myGroundSegmentWidth = myPrintSegmentWidthMM * myMMToGroundDistance if myGroundSegmentWidth < 1000: myUnits = 'm' myGroundSegmentWidth = round(myGroundSegmentWidth) # adjust the segment width now to account for rounding myPrintSegmentWidthMM = myGroundSegmentWidth / myMMToGroundDistance else: myUnits = 'km' # Segment with in real world (km) myGroundSegmentWidth = round(myGroundSegmentWidth / 1000) myPrintSegmentWidthMM = ((myGroundSegmentWidth * 1000) / myMMToGroundDistance) # Now adjust the scalebar width to account for rounding myScaleBarWidthMM = myTickCount * myPrintSegmentWidthMM #print "SBWMM:", myScaleBarWidthMM #print "SWMM:", myPrintSegmentWidthMM #print "SWM:", myGroundSegmentWidthM #print "SWKM:", myGroundSegmentWidthKM # start drawing in line segments myScaleBarHeight = 5 # mm myLineWidth = 0.3 # mm myInsetDistance = 7 # how much to inset the scalebar into the map by myScaleBarX = self.pageMargin + myInsetDistance myScaleBarY = ( top_offset + self.mapHeight - myInsetDistance - myScaleBarHeight) # mm # Draw an outer background box - shamelessly hardcoded buffer myRect = QgsComposerShape(myScaleBarX - 4, # left edge myScaleBarY - 3, # top edge myScaleBarWidthMM + 13, # right edge myScaleBarHeight + 6, # bottom edge self.composition) myRect.setShapeType(QgsComposerShape.Rectangle) myPen = QtGui.QPen() myPen.setColor(QtGui.QColor(255, 255, 255)) myPen.setWidthF(myLineWidth) myRect.setPen(myPen) #myRect.setLineWidth(myLineWidth) myRect.setFrameEnabled(False) myBrush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) # workaround for missing setTransparentFill missing from python api myRect.setBrush(myBrush) self.composition.addItem(myRect) # Set up the tick label font myFontWeight = QtGui.QFont.Normal myFontSize = 6 myItalicsFlag = False myFont = QtGui.QFont('verdana', myFontSize, myFontWeight, myItalicsFlag) # Draw the bottom line myUpshift = 0.3 # shift the bottom line up for better rendering myRect = QgsComposerShape(myScaleBarX, myScaleBarY + myScaleBarHeight - myUpshift, myScaleBarWidthMM, 0.1, self.composition) myRect.setShapeType(QgsComposerShape.Rectangle) myPen = QtGui.QPen() myPen.setColor(QtGui.QColor(255, 255, 255)) myPen.setWidthF(myLineWidth) myRect.setPen(myPen) #myRect.setLineWidth(myLineWidth) myRect.setFrameEnabled(False) self.composition.addItem(myRect) # Now draw the scalebar ticks for myTickCountIterator in range(0, myTickCount + 1): myDistanceSuffix = '' if myTickCountIterator == myTickCount: myDistanceSuffix = ' ' + myUnits myRealWorldDistance = ('%.0f%s' % (myTickCountIterator * myGroundSegmentWidth, myDistanceSuffix)) #print 'RW:', myRealWorldDistance myMMOffset = myScaleBarX + (myTickCountIterator * myPrintSegmentWidthMM) #print 'MM:', myMMOffset myTickHeight = myScaleBarHeight / 2 # Lines are not exposed by the api yet so we # bodge drawing lines using rectangles with 1px height or width myTickWidth = 0.1 # width or rectangle to be drawn myUpTickLine = QgsComposerShape( myMMOffset, myScaleBarY + myScaleBarHeight - myTickHeight, myTickWidth, myTickHeight, self.composition) myUpTickLine.setShapeType(QgsComposerShape.Rectangle) myPen = QtGui.QPen() myPen.setWidthF(myLineWidth) myUpTickLine.setPen(myPen) #myUpTickLine.setLineWidth(myLineWidth) myUpTickLine.setFrameEnabled(False) self.composition.addItem(myUpTickLine) # # Add a tick label # myLabel = QgsComposerLabel(self.composition) myLabel.setFont(myFont) myLabel.setText(myRealWorldDistance) myLabel.adjustSizeToText() myLabel.setItemPosition( myMMOffset - 3, myScaleBarY - myTickHeight) myLabel.setFrameEnabled(self.showFramesFlag) self.composition.addItem(myLabel) def draw_impact_title(self, top_offset): """Draw the map subtitle - obtained from the impact layer keywords. :param top_offset: Vertical offset at which the logo should be drawn. :type top_offset: int :returns: The height of the label as rendered. :rtype: float """ LOGGER.debug('InaSAFE Map drawImpactTitle called') myTitle = self.map_title() if myTitle is None: myTitle = '' myFontSize = 20 myFontWeight = QtGui.QFont.Bold myItalicsFlag = False myFont = QtGui.QFont( 'verdana', myFontSize, myFontWeight, myItalicsFlag) myLabel = QgsComposerLabel(self.composition) myLabel.setFont(myFont) myHeading = myTitle myLabel.setText(myHeading) myLabelWidth = self.pageWidth - (self.pageMargin * 2) myLabelHeight = 12 myLabel.setItemPosition( self.pageMargin, top_offset, myLabelWidth, myLabelHeight) myLabel.setFrameEnabled(self.showFramesFlag) self.composition.addItem(myLabel) return myLabelHeight def draw_legend(self, top_offset): """Add a legend to the map using our custom legend renderer. .. note:: getLegend generates a pixmap in 150dpi so if you set the map to a higher dpi it will appear undersized. :param top_offset: Vertical offset at which the logo should be drawn. :type top_offset: int """ LOGGER.debug('InaSAFE Map drawLegend called') mapLegendAttributes = self.map_legend_attributes() legendNotes = mapLegendAttributes.get('legend_notes', None) legendUnits = mapLegendAttributes.get('legend_units', None) legendTitle = mapLegendAttributes.get('legend_title', None) LOGGER.debug(mapLegendAttributes) myLegend = MapLegend(self.layer, self.pageDpi, legendTitle, legendNotes, legendUnits) self.legend = myLegend.get_legend() myPicture1 = QgsComposerPicture(self.composition) myLegendFilePath = unique_filename( prefix='legend', suffix='.png', dir='work') self.legend.save(myLegendFilePath, 'PNG') myPicture1.setPictureFile(myLegendFilePath) myLegendHeight = points_to_mm(self.legend.height(), self.pageDpi) myLegendWidth = points_to_mm(self.legend.width(), self.pageDpi) myPicture1.setItemPosition(self.pageMargin, top_offset, myLegendWidth, myLegendHeight) myPicture1.setFrameEnabled(False) self.composition.addItem(myPicture1) os.remove(myLegendFilePath) def draw_image(self, theImage, theWidthMM, theLeftOffset, theTopOffset): """Helper to draw an image directly onto the QGraphicsScene. This is an alternative to using QgsComposerPicture which in some cases leaves artifacts under windows. The Pixmap will have a transform applied to it so that it is rendered with the same resolution as the composition. :param theImage: Image that will be rendered to the layout. :type theImage: QImage :param theWidthMM: Desired width in mm of output on page. :type theWidthMM: int :param theLeftOffset: Offset from left of page. :type theLeftOffset: int :param theTopOffset: Offset from top of page. :type theTopOffset: int :returns: Graphics scene item. :rtype: QGraphicsSceneItem """ LOGGER.debug('InaSAFE Map drawImage called') myDesiredWidthMM = theWidthMM # mm myDesiredWidthPX = mm_to_points(myDesiredWidthMM, self.pageDpi) myActualWidthPX = theImage.width() myScaleFactor = myDesiredWidthPX / myActualWidthPX LOGGER.debug('%s %s %s' % ( myScaleFactor, myActualWidthPX, myDesiredWidthPX)) myTransform = QtGui.QTransform() myTransform.scale(myScaleFactor, myScaleFactor) myTransform.rotate(0.5) # noinspection PyArgumentList myItem = self.composition.addPixmap(QtGui.QPixmap.fromImage(theImage)) myItem.setTransform(myTransform) myItem.setOffset(theLeftOffset / myScaleFactor, theTopOffset / myScaleFactor) return myItem def draw_host_and_time(self, top_offset): """Add a note with hostname and time to the composition. :param top_offset: Vertical offset at which the logo should be drawn. :type top_offset: int """ LOGGER.debug('InaSAFE Map drawDisclaimer called') #elapsed_time: 11.612545 #user: timlinux #host_name: ultrabook #time_stamp: 2012-10-13_23:10:31 #myUser = self.keywordIO.readKeywords(self.layer, 'user') #myHost = self.keywordIO.readKeywords(self.layer, 'host_name') myDateTime = self.keywordIO.read_keywords(self.layer, 'time_stamp') myTokens = myDateTime.split('_') myDate = myTokens[0] myTime = myTokens[1] #myElapsedTime = self.keywordIO.readKeywords(self.layer, # 'elapsed_time') #myElapsedTime = humaniseSeconds(myElapsedTime) myLongVersion = get_version() myTokens = myLongVersion.split('.') myVersion = '%s.%s.%s' % (myTokens[0], myTokens[1], myTokens[2]) myLabelText = self.tr( 'Date and time of assessment: %s %s\n' 'Special note: This assessment is a guide - we strongly recommend ' 'that you ground truth the results shown here before deploying ' 'resources and / or personnel.\n' 'Assessment carried out using InaSAFE release %s (QGIS ' 'plugin version).') % (myDate, myTime, myVersion) myFontSize = 6 myFontWeight = QtGui.QFont.Normal myItalicsFlag = True myFont = QtGui.QFont('verdana', myFontSize, myFontWeight, myItalicsFlag) myLabel = QgsComposerLabel(self.composition) myLabel.setFont(myFont) myLabel.setText(myLabelText) myLabel.adjustSizeToText() myLabelHeight = 50.0 # mm determined using qgis map composer myLabelWidth = (self.pageWidth / 2) - self.pageMargin myLeftOffset = self.pageWidth / 2 # put in right half of page myLabel.setItemPosition(myLeftOffset, top_offset, myLabelWidth, myLabelHeight, ) myLabel.setFrameEnabled(self.showFramesFlag) self.composition.addItem(myLabel) def draw_disclaimer(self): """Add a disclaimer to the composition.""" LOGGER.debug('InaSAFE Map drawDisclaimer called') myFontSize = 10 myFontWeight = QtGui.QFont.Normal myItalicsFlag = True myFont = QtGui.QFont('verdana', myFontSize, myFontWeight, myItalicsFlag) myLabel = QgsComposerLabel(self.composition) myLabel.setFont(myFont) myLabel.setText(self.disclaimer) myLabel.adjustSizeToText() myLabelHeight = 7.0 # mm determined using qgis map composer myLabelWidth = self.pageWidth # item - position and size...option myLeftOffset = self.pageMargin myTopOffset = self.pageHeight - self.pageMargin myLabel.setItemPosition(myLeftOffset, myTopOffset, myLabelWidth, myLabelHeight, ) myLabel.setFrameEnabled(self.showFramesFlag) self.composition.addItem(myLabel) def map_title(self): """Get the map title from the layer keywords if possible. :returns: None on error, otherwise the title. :rtype: None, str """ LOGGER.debug('InaSAFE Map getMapTitle called') try: myTitle = self.keywordIO.read_keywords(self.layer, 'map_title') return myTitle except KeywordNotFoundError: return None except Exception: return None def map_legend_attributes(self): """Get the map legend attribute from the layer keywords if possible. :returns: None on error, otherwise the attributes (notes and units). :rtype: None, str """ LOGGER.debug('InaSAFE Map getMapLegendAtributes called') legendAttributes = ['legend_notes', 'legend_units', 'legend_title'] dictLegendAttributes = {} for myLegendAttribute in legendAttributes: try: dictLegendAttributes[myLegendAttribute] = \ self.keywordIO.read_keywords(self.layer, myLegendAttribute) except KeywordNotFoundError: pass except Exception: pass return dictLegendAttributes def showComposer(self): """Show the composition in a composer view so the user can tweak it. """ myView = QgsComposerView(self.iface.mainWindow()) myView.show() def write_template(self, template_path): """Write current composition as a template that can be re-used in QGIS. :param template_path: Path to which template should be written. :type template_path: str """ myDocument = QtXml.QDomDocument() myElement = myDocument.createElement('Composer') myDocument.appendChild(myElement) self.composition.writeXML(myElement, myDocument) myXml = myDocument.toByteArray() myFile = file(template_path, 'wb') myFile.write(myXml) myFile.close() def render_template(self, template_path, output_path): """Load a QgsComposer map from a template and render it. .. note:: THIS METHOD IS EXPERIMENTAL AND CURRENTLY NON FUNCTIONAL :param template_path: Path to the template that should be loaded. :type template_path: str :param output_path: Path for the output pdf. :type output_path: str """ self.setup_composition() myResolution = self.composition.printResolution() self.printer = setup_printer( output_path, resolution=myResolution) if self.composition: myFile = QtCore.QFile(template_path) myDocument = QtXml.QDomDocument() myDocument.setContent(myFile, False) # .. todo:: fix magic param myNodeList = myDocument.elementsByTagName('Composer') if myNodeList.size() > 0: myElement = myNodeList.at(0).toElement() self.composition.readXML(myElement, myDocument) self.make_pdf(output_path)
def __init__(self, parent, iface, dock=None, layer=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. :param parent: Parent widget of this dialog. :type parent: QWidget :param iface: Quantum GIS QGisAppInterface instance. :type iface: QGisAppInterface :param dock: Dock widget instance that we can notify of changes to the keywords. Optional. :type dock: Dock """ QtGui.QDialog.__init__(self, parent) self.setupUi(self) self.setWindowTitle( self.tr('InaSAFE %s Keywords Editor' % get_version())) # Save reference to the QGIS interface and parent self.iface = iface self.parent = parent self.dock = dock self.defaults = None # string constants self.global_default_string = metadata.global_default_attribute['name'] self.do_not_use_string = metadata.do_not_use_attribute['name'] self.global_default_data = metadata.global_default_attribute['id'] self.do_not_use_data = metadata.do_not_use_attribute['id'] if layer is None: self.layer = self.iface.activeLayer() else: self.layer = layer self.keyword_io = 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.standard_exposure_list = OrderedDict([ ('population', self.tr('population')), ('structure', self.tr('structure')), ('road', self.tr('road')), ('Not Set', self.tr('Not Set')) ]) self.standard_hazard_list = 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')), ('generic [categorised]', self.tr('generic [categorised]')), ('Not Set', self.tr('Not Set')) ]) # noinspection PyUnresolvedReferences self.lstKeywords.itemClicked.connect(self.edit_key_value_pair) # Set up help dialog showing logic. help_button = self.buttonBox.button(QtGui.QDialogButtonBox.Help) help_button.clicked.connect(self.show_help) if self.layer is not None and is_polygon_layer(self.layer): # set some initial ui state: self.defaults = get_defaults() self.radPredefined.setChecked(True) self.dsbFemaleRatioDefault.blockSignals(True) self.dsbFemaleRatioDefault.setValue(self.defaults['FEMALE_RATIO']) self.dsbFemaleRatioDefault.blockSignals(False) self.dsbYouthRatioDefault.blockSignals(True) self.dsbYouthRatioDefault.setValue(self.defaults['YOUTH_RATIO']) self.dsbYouthRatioDefault.blockSignals(False) self.dsbAdultRatioDefault.blockSignals(True) self.dsbAdultRatioDefault.setValue(self.defaults['ADULT_RATIO']) self.dsbAdultRatioDefault.blockSignals(False) self.dsbElderlyRatioDefault.blockSignals(True) self.dsbElderlyRatioDefault.setValue( self.defaults['ELDERLY_RATIO']) self.dsbElderlyRatioDefault.blockSignals(False) else: self.radPostprocessing.hide() self.tab_widget.removeTab(1) if self.layer: self.load_state_from_keywords() # add a reload from keywords button reload_button = self.buttonBox.addButton( self.tr('Reload'), QtGui.QDialogButtonBox.ActionRole) reload_button.clicked.connect(self.load_state_from_keywords) self.resize_dialog() self.tab_widget.setCurrentIndex(0) # TODO No we should not have test related stuff in prod code. TS self.test = False
class Map(): """A class for creating a map.""" def __init__(self, iface): """Constructor for the Map class. :param iface: Reference to the QGIS iface object. :type iface: QgsAppInterface """ LOGGER.debug('InaSAFE Map class initialised') self.iface = iface self.layer = iface.activeLayer() self.keyword_io = KeywordIO() self.printer = None self.composition = None self.extent = iface.mapCanvas().extent() self.safe_logo = ':/plugins/inasafe/inasafe-logo-url.svg' self.north_arrow = ':/plugins/inasafe/simple_north_arrow.png' self.org_logo = ':/plugins/inasafe/supporters.png' self.template = ':/plugins/inasafe/inasafe-portrait-a4.qpt' self.disclaimer = disclaimer() self.page_width = 0 # width in mm self.page_height = 0 # height in mm self.page_dpi = 300.0 self.show_frames = False # intended for debugging use only @staticmethod def tr(string): """We implement this since we do not inherit QObject. :param string: String for translation. :type string: QString, str :returns: Translated version of theString. :rtype: QString """ # noinspection PyCallByClass,PyTypeChecker,PyArgumentList return QtCore.QCoreApplication.translate('Map', string) def set_impact_layer(self, layer): """Set the layer that will be used for stats, legend and reporting. :param layer: Layer that will be used for stats, legend and reporting. :type layer: QgsMapLayer, QgsRasterLayer, QgsVectorLayer """ self.layer = layer def set_north_arrow_image(self, logo_path): """Set image that will be used as organisation logo in reports. :param logo_path: Path to image file :type logo_path: str """ self.north_arrow = logo_path def set_organisation_logo(self, logo): """Set image that will be used as organisation logo in reports. :param logo: Path to image file :type logo: str """ self.org_logo = logo def set_disclaimer(self, text): """Set text that will be used as disclaimer in reports. :param text: Disclaimer text :type text: str """ self.disclaimer = text def set_template(self, template): """Set template that will be used for report generation. :param template: Path to composer template :type template: str """ self.template = template def set_extent(self, extent): """Set extent or the report map :param extent: Extent of the report map :type extent: QgsRectangle """ self.extent = extent def setup_composition(self): """Set up the composition ready for drawing elements onto it.""" LOGGER.debug('InaSAFE Map setupComposition called') canvas = self.iface.mapCanvas() renderer = canvas.mapRenderer() self.composition = QgsComposition(renderer) self.composition.setPlotStyle(QgsComposition.Preview) # or preview self.composition.setPrintResolution(self.page_dpi) self.composition.setPrintAsRaster(True) def make_pdf(self, filename): """Generate the printout for our final map. :param filename: Path on the file system to which the pdf should be saved. If None, a generated file name will be used. :type filename: str :returns: File name of the output file (equivalent to filename if provided). :rtype: str """ LOGGER.debug('InaSAFE Map printToPdf called') if filename is None: map_pdf_path = unique_filename( prefix='report', suffix='.pdf', dir=temp_dir()) else: # We need to cast to python string in case we receive a QString map_pdf_path = str(filename) self.load_template() self.composition.exportAsPDF(map_pdf_path) return map_pdf_path def map_title(self): """Get the map title from the layer keywords if possible. :returns: None on error, otherwise the title. :rtype: None, str """ LOGGER.debug('InaSAFE Map getMapTitle called') try: title = self.keyword_io.read_keywords(self.layer, 'map_title') return title except KeywordNotFoundError: return None except Exception: return None def map_legend_attributes(self): """Get the map legend attribute from the layer keywords if possible. :returns: None on error, otherwise the attributes (notes and units). :rtype: None, str """ LOGGER.debug('InaSAFE Map getMapLegendAttributes called') legend_attribute_list = [ 'legend_notes', 'legend_units', 'legend_title'] legend_attribute_dict = {} for myLegendAttribute in legend_attribute_list: # noinspection PyBroadException try: legend_attribute_dict[myLegendAttribute] = \ self.keyword_io.read_keywords( self.layer, myLegendAttribute) except KeywordNotFoundError: pass except Exception: pass return legend_attribute_dict def load_template(self): """Load a QgsComposer map from a template. """ self.setup_composition() template_file = QtCore.QFile(self.template) template_file.open(QtCore.QIODevice.ReadOnly | QtCore.QIODevice.Text) template_content = template_file.readAll() template_file.close() document = QtXml.QDomDocument() document.setContent(template_content) # get information for substitutions # date, time and plugin version date_time = self.keyword_io.read_keywords(self.layer, 'time_stamp') if date_time is None: date = '' time = '' else: tokens = date_time.split('_') date = tokens[0] time = tokens[1] long_version = get_version() tokens = long_version.split('.') version = '%s.%s.%s' % (tokens[0], tokens[1], tokens[2]) title = self.map_title() if not title: title = '' substitution_map = { 'impact-title': title, 'date': date, 'time': time, 'safe-version': version, 'disclaimer': self.disclaimer } LOGGER.debug(substitution_map) load_ok = self.composition.loadFromTemplate( document, substitution_map) if not load_ok: raise ReportCreationError( self.tr('Error loading template %s') % self.template) self.page_width = self.composition.paperWidth() self.page_height = self.composition.paperHeight() # set InaSAFE logo image = self.composition.getComposerItemById('safe-logo') if image is not None: image.setPictureFile(self.safe_logo) else: raise ReportCreationError(self.tr( 'Image "safe-logo" could not be found')) # set north arrow image = self.composition.getComposerItemById('north-arrow') if image is not None: image.setPictureFile(self.north_arrow) else: raise ReportCreationError(self.tr( 'Image "north arrow" could not be found')) # set organisation logo image = self.composition.getComposerItemById('organisation-logo') if image is not None: image.setPictureFile(self.org_logo) else: raise ReportCreationError(self.tr( 'Image "organisation-logo" could not be found')) # set impact report table table = self.composition.getComposerItemById('impact-report') if table is not None: text = self.keyword_io.read_keywords(self.layer, 'impact_summary') if text is None: text = '' table.setText(text) table.setHtmlState(1) else: LOGGER.debug('"impact-report" element not found.') # Get the main map canvas on the composition and set # its extents to the event. composer_map = self.composition.getComposerItemById('impact-map') if composer_map is not None: # Recenter the composer map on the center of the extent # Note that since the composer map is square and the canvas may be # arbitrarily shaped, we center based on the longest edge canvas_extent = self.extent width = canvas_extent.width() height = canvas_extent.height() longest_width = width if width < height: longest_width = height half_length = longest_width / 2 center = canvas_extent.center() min_x = center.x() - half_length max_x = center.x() + half_length min_y = center.y() - half_length max_y = center.y() + half_length square_extent = QgsRectangle(min_x, min_y, max_x, max_y) composer_map.setNewExtent(square_extent) # calculate intervals for grid split_count = 5 x_interval = square_extent.width() / split_count composer_map.setGridIntervalX(x_interval) y_interval = square_extent.height() / split_count composer_map.setGridIntervalY(y_interval) else: raise ReportCreationError(self.tr( 'Map "impact-map" could not be found')) legend = self.composition.getComposerItemById('impact-legend') legend_attributes = self.map_legend_attributes() LOGGER.debug(legend_attributes) #legend_notes = mapLegendAttributes.get('legend_notes', None) #legend_units = mapLegendAttributes.get('legend_units', None) legend_title = legend_attributes.get('legend_title', None) symbol_count = 1 if self.layer.type() == QgsMapLayer.VectorLayer: renderer = self.layer.rendererV2() if renderer.type() in ['', '']: symbol_count = len(self.layer.legendSymbologyItems()) else: renderer = self.layer.renderer() if renderer.type() in ['']: symbol_count = len(self.layer.legendSymbologyItems()) if symbol_count <= 5: legend.setColumnCount(1) else: legend.setColumnCount(symbol_count / 5 + 1) if legend_title is None: legend_title = "" legend.setTitle(legend_title) legend.updateLegend() # remove from legend all layers, except impact one model = legend.model() if model.rowCount() > 0 and model.columnCount() > 0: impact_item = model.findItems(self.layer.name())[0] row = impact_item.index().row() model.removeRows(row + 1, model.rowCount() - row) if row > 0: model.removeRows(0, row)
class MapLegend(): """A class for creating a map legend.""" def __init__(self, layer, dpi=300, legend_title=None, legend_notes=None, legend_units=None): """Constructor for the Map Legend class. :param layer: Layer that the legend should be generated for. :type layer: QgsMapLayer, QgsVectorLayer :param dpi: DPI for the generated legend image. Defaults to 300 if not specified. :type dpi: int :param legend_title: Title for the legend. :type legend_title: str :param legend_notes: Notes to display under the title. :type legend_notes: str :param legend_units: Units for the legend. :type legend_units: str """ LOGGER.debug('InaSAFE Map class initialised') self.legend_image = None self.layer = layer # how high each row of the legend should be self.legend_increment = 42 self.keyword_io = KeywordIO() self.legend_font_size = 8 self.legend_width = 900 self.dpi = dpi if legend_title is None: self.legend_title = self.tr('Legend') else: self.legend_title = legend_title self.legend_notes = legend_notes self.legend_units = legend_units # noinspection PyMethodMayBeStatic def tr(self, string): """We implement this ourself since we do not inherit QObject. :param string: String for translation. :type string: QString, str :returns: Translated version of string. :rtype: QString """ # noinspection PyCallByClass,PyTypeChecker return QtCore.QCoreApplication.translate('MapLegend', string) def get_legend(self): """Create a legend for the classes in the layer. .. note: This is a wrapper for raster_legend and vector_legend. :raises: 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: message = self.tr( 'Unable to make a legend when map generator has no layer set.') raise LegendLayerError(message) try: self.keyword_io.read_keywords(self.layer, 'impact_summary') except KeywordNotFoundError, e: message = 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(message) if self.layer.type() == QgsMapLayer.VectorLayer: return self.vector_legend() else: return self.raster_legend()
def __init__(self, parent=None, iface=None): """Constructor for dialog. :param parent: Optional widget to use as parent :type parent: QWidget :param iface: An instance of QGisInterface :type iface: QGisInterface """ QDialog.__init__(self, parent) self.parent = parent self.setupUi(self) self.setWindowTitle(self.tr('InaSAFE Impact Layer Merge Tool')) self.iface = iface self.keyword_io = KeywordIO() # Template Path for composer self.template_path = ':/plugins/inasafe/merged_report.qpt' # Safe Logo Path self.safe_logo_path = ':/plugins/inasafe/inasafe-logo-url.png' # Organisation Logo Path self.organisation_logo_path = ':/plugins/inasafe/supporters.png' # Disclaimer text self.disclaimer = disclaimer() # The output directory self.out_dir = None # Stored information from first impact layer self.first_impact = { 'layer': None, 'map_title': None, 'hazard_title': None, 'exposure_title': None, 'postprocessing_report': None, } # Stored information from second impact layer self.second_impact = { 'layer': None, 'map_title': None, 'hazard_title': None, 'exposure_title': None, 'postprocessing_report': None, } # Stored information from aggregation layer self.aggregation = { 'layer': None, 'aggregation_attribute': None } # The summary report, contains report for each aggregation area self.summary_report = {} # The html reports and its file path self.html_reports = {} # A boolean flag whether to merge entire area or aggregated self.entire_area_mode = False # Get the global settings and override some variable if exist self.read_settings() # Get all current project layers for combo box self.get_project_layers() # Set up context help help_button = self.button_box.button(QtGui.QDialogButtonBox.Help) help_button.clicked.connect(self.show_help) # Show usage info self.show_info() self.restore_state()
def __init__(self, parent=None, iface=None): """Constructor for dialog. :param parent: Optional widget to use as parent :type parent: QWidget :param iface: An instance of QGisInterface :type iface: QGisInterface """ QDialog.__init__(self, parent) self.parent = parent self.setupUi(self) self.setWindowTitle(self.tr('InaSAFE Impact Layer Merge Tool')) self.iface = iface self.keyword_io = KeywordIO() # Template Path for composer self.template_path = ':/plugins/inasafe/merged_report.qpt' # Safe Logo Path self.safe_logo_path = ':/plugins/inasafe/inasafe-logo-url.png' # Organisation Logo Path self.organisation_logo_path = ':/plugins/inasafe/supporters.png' # Disclaimer text self.disclaimer = disclaimer() # The output directory self.out_dir = None # Stored information from first impact layer self.first_impact = { 'layer': None, 'map_title': None, 'hazard_title': None, 'exposure_title': None, 'postprocessing_report': None, } # Stored information from second impact layer self.second_impact = { 'layer': None, 'map_title': None, 'hazard_title': None, 'exposure_title': None, 'postprocessing_report': None, } # Stored information from aggregation layer self.aggregation = {'layer': None, 'aggregation_attribute': None} # The summary report, contains report for each aggregation area self.summary_report = {} # The html reports and its file path self.html_reports = {} # A boolean flag whether to merge entire area or aggregated self.entire_area_mode = False # Get the global settings and override some variable if exist self.read_settings() # Get all current project layers for combo box self.get_project_layers() # Set up context help help_button = self.button_box.button(QtGui.QDialogButtonBox.Help) help_button.clicked.connect(self.show_help) # Show usage info self.show_info() self.restore_state()
class Map(): """A class for creating a map.""" def __init__(self, iface): """Constructor for the Map class. :param iface: Reference to the QGIS iface object. :type iface: QgsAppInterface """ LOGGER.debug('InaSAFE Map class initialised') self.iface = iface self.layer = iface.activeLayer() self.keyword_io = KeywordIO() self.printer = None self.composition = None self.extent = iface.mapCanvas().extent() self.safe_logo = ':/plugins/inasafe/inasafe-logo-url.svg' self.north_arrow = ':/plugins/inasafe/simple_north_arrow.png' self.org_logo = ':/plugins/inasafe/supporters.png' self.template = ':/plugins/inasafe/inasafe-portrait-a4.qpt' self.disclaimer = disclaimer() self.page_width = 0 # width in mm self.page_height = 0 # height in mm self.page_dpi = 300.0 self.show_frames = False # intended for debugging use only @staticmethod def tr(string): """We implement this since we do not inherit QObject. :param string: String for translation. :type string: QString, str :returns: Translated version of theString. :rtype: QString """ # noinspection PyCallByClass,PyTypeChecker,PyArgumentList return QtCore.QCoreApplication.translate('Map', string) def set_impact_layer(self, layer): """Set the layer that will be used for stats, legend and reporting. :param layer: Layer that will be used for stats, legend and reporting. :type layer: QgsMapLayer, QgsRasterLayer, QgsVectorLayer """ self.layer = layer def set_north_arrow_image(self, logo_path): """Set image that will be used as organisation logo in reports. :param logo_path: Path to image file :type logo_path: str """ self.north_arrow = logo_path def set_organisation_logo(self, logo): """Set image that will be used as organisation logo in reports. :param logo: Path to image file :type logo: str """ self.org_logo = logo def set_disclaimer(self, text): """Set text that will be used as disclaimer in reports. :param text: Disclaimer text :type text: str """ self.disclaimer = text def set_template(self, template): """Set template that will be used for report generation. :param template: Path to composer template :type template: str """ self.template = template def set_extent(self, extent): """Set extent or the report map :param extent: Extent of the report map :type extent: QgsRectangle """ self.extent = extent def setup_composition(self): """Set up the composition ready for drawing elements onto it.""" LOGGER.debug('InaSAFE Map setupComposition called') canvas = self.iface.mapCanvas() renderer = canvas.mapRenderer() self.composition = QgsComposition(renderer) self.composition.setPlotStyle(QgsComposition.Preview) # or preview self.composition.setPrintResolution(self.page_dpi) self.composition.setPrintAsRaster(True) def make_pdf(self, filename): """Generate the printout for our final map. :param filename: Path on the file system to which the pdf should be saved. If None, a generated file name will be used. :type filename: str :returns: File name of the output file (equivalent to filename if provided). :rtype: str """ LOGGER.debug('InaSAFE Map printToPdf called') if filename is None: map_pdf_path = unique_filename(prefix='report', suffix='.pdf', dir=temp_dir()) else: # We need to cast to python string in case we receive a QString map_pdf_path = str(filename) self.load_template() self.composition.exportAsPDF(map_pdf_path) return map_pdf_path def map_title(self): """Get the map title from the layer keywords if possible. :returns: None on error, otherwise the title. :rtype: None, str """ LOGGER.debug('InaSAFE Map getMapTitle called') try: title = self.keyword_io.read_keywords(self.layer, 'map_title') return title except KeywordNotFoundError: return None except Exception: return None def map_legend_attributes(self): """Get the map legend attribute from the layer keywords if possible. :returns: None on error, otherwise the attributes (notes and units). :rtype: None, str """ LOGGER.debug('InaSAFE Map getMapLegendAttributes called') legend_attribute_list = [ 'legend_notes', 'legend_units', 'legend_title' ] legend_attribute_dict = {} for myLegendAttribute in legend_attribute_list: # noinspection PyBroadException try: legend_attribute_dict[myLegendAttribute] = \ self.keyword_io.read_keywords( self.layer, myLegendAttribute) except KeywordNotFoundError: pass except Exception: pass return legend_attribute_dict def load_template(self): """Load a QgsComposer map from a template. """ self.setup_composition() template_file = QtCore.QFile(self.template) template_file.open(QtCore.QIODevice.ReadOnly | QtCore.QIODevice.Text) template_content = template_file.readAll() template_file.close() document = QtXml.QDomDocument() document.setContent(template_content) # get information for substitutions # date, time and plugin version date_time = self.keyword_io.read_keywords(self.layer, 'time_stamp') if date_time is None: date = '' time = '' else: tokens = date_time.split('_') date = tokens[0] time = tokens[1] long_version = get_version() tokens = long_version.split('.') version = '%s.%s.%s' % (tokens[0], tokens[1], tokens[2]) title = self.map_title() if not title: title = '' substitution_map = { 'impact-title': title, 'date': date, 'time': time, 'safe-version': version, 'disclaimer': self.disclaimer } LOGGER.debug(substitution_map) load_ok = self.composition.loadFromTemplate(document, substitution_map) if not load_ok: raise ReportCreationError( self.tr('Error loading template %s') % self.template) self.page_width = self.composition.paperWidth() self.page_height = self.composition.paperHeight() # set InaSAFE logo image = self.composition.getComposerItemById('safe-logo') if image is not None: image.setPictureFile(self.safe_logo) else: raise ReportCreationError( self.tr('Image "safe-logo" could not be found')) # set north arrow image = self.composition.getComposerItemById('north-arrow') if image is not None: image.setPictureFile(self.north_arrow) else: raise ReportCreationError( self.tr('Image "north arrow" could not be found')) # set organisation logo image = self.composition.getComposerItemById('organisation-logo') if image is not None: image.setPictureFile(self.org_logo) else: raise ReportCreationError( self.tr('Image "organisation-logo" could not be found')) # set impact report table table = self.composition.getComposerItemById('impact-report') if table is not None: text = self.keyword_io.read_keywords(self.layer, 'impact_summary') if text is None: text = '' table.setText(text) table.setHtmlState(1) else: LOGGER.debug('"impact-report" element not found.') # Get the main map canvas on the composition and set # its extents to the event. composer_map = self.composition.getComposerItemById('impact-map') if composer_map is not None: # Recenter the composer map on the center of the extent # Note that since the composer map is square and the canvas may be # arbitrarily shaped, we center based on the longest edge canvas_extent = self.extent width = canvas_extent.width() height = canvas_extent.height() longest_width = width if width < height: longest_width = height half_length = longest_width / 2 center = canvas_extent.center() min_x = center.x() - half_length max_x = center.x() + half_length min_y = center.y() - half_length max_y = center.y() + half_length square_extent = QgsRectangle(min_x, min_y, max_x, max_y) composer_map.setNewExtent(square_extent) # calculate intervals for grid split_count = 5 x_interval = square_extent.width() / split_count composer_map.setGridIntervalX(x_interval) y_interval = square_extent.height() / split_count composer_map.setGridIntervalY(y_interval) else: raise ReportCreationError( self.tr('Map "impact-map" could not be found')) legend = self.composition.getComposerItemById('impact-legend') legend_attributes = self.map_legend_attributes() LOGGER.debug(legend_attributes) #legend_notes = mapLegendAttributes.get('legend_notes', None) #legend_units = mapLegendAttributes.get('legend_units', None) legend_title = legend_attributes.get('legend_title', None) symbol_count = 1 if self.layer.type() == QgsMapLayer.VectorLayer: renderer = self.layer.rendererV2() if renderer.type() in ['', '']: symbol_count = len(self.layer.legendSymbologyItems()) else: renderer = self.layer.renderer() if renderer.type() in ['']: symbol_count = len(self.layer.legendSymbologyItems()) if symbol_count <= 5: legend.setColumnCount(1) else: legend.setColumnCount(symbol_count / 5 + 1) if legend_title is None: legend_title = "" legend.setTitle(legend_title) legend.updateLegend() # remove from legend all layers, except impact one model = legend.model() if model.rowCount() > 0 and model.columnCount() > 0: impact_item = model.findItems(self.layer.name())[0] row = impact_item.index().row() model.removeRows(row + 1, model.rowCount() - row) if row > 0: model.removeRows(0, row)
class OptionsDialog(QtGui.QDialog, Ui_OptionsDialogBase): """Options dialog for the InaSAFE plugin.""" def __init__(self, iface, dock=None, parent=None): """Constructor for the dialog. :param iface: A Quantum GIS QGisAppInterface instance. :type iface: QGisAppInterface :param parent: Parent widget of this dialog :type parent: QWidget :param dock: Optional dock widget instance that we can notify of changes to the keywords. :type dock: Dock """ 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 = dock 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.show_help) self.grpNotImplemented.hide() self.adjustSize() self.restore_state() # hack prevent showing use thread visible and set it false see #557 self.cbxUseThread.setChecked(True) self.cbxUseThread.setVisible(False) def restore_state(self): """Reinstate the options based on the user's stored session info. """ 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/showIntermediateLayers', 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.default_keyword_db_path()).toString() self.leKeywordCachePath.setText(myPath) myFlag = mySettings.value('inasafe/devMode', False).toBool() self.cbxDevMode.setChecked(myFlag) def save_state(self): """Store the options into the user's stored session info. """ 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/showIntermediateLayers', self.cbxShowPostprocessingLayers.isChecked()) mySettings.setValue('inasafe/defaultFemaleRatio', self.dsbFemaleRatioDefault.value()) mySettings.setValue('inasafe/keywordCachePath', self.leKeywordCachePath.text()) mySettings.setValue('inasafe/devMode', self.cbxDevMode.isChecked()) def show_help(self): """Show context help for the options dialog.""" show_context_help('options') def accept(self): """Method invoked when OK button is clicked.""" self.save_state() self.dock.read_settings() self.close() @pyqtSignature('') # prevents actions being handled twice def on_toolKeywordCachePath_clicked(self): """Auto-connect slot activated when cache file tool button is clicked. """ # noinspection PyCallByClass,PyTypeChecker myFilename = QtGui.QFileDialog.getSaveFileName( self, self.tr('Set keyword cache file'), self.keywordIO.default_keyword_db_path(), self.tr('Sqlite DB File (*.db)')) self.leKeywordCachePath.setText(myFilename)