# -*- coding: utf-8 -*-
# /***************************************************************************
# Irmt
# A QGIS plugin
# OpenQuake Integrated Risk Modelling Toolkit
# -------------------
# begin : 2013-10-24
# copyright : (C) 2013-2017 by GEM Foundation
# email : devops@openquake.org
# ***************************************************************************/
#
# OpenQuake is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# OpenQuake is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with OpenQuake. If not, see <http://www.gnu.org/licenses/>.
import qgis # NOQA: it loads the environment
import os.path
import tempfile
import uuid
import fileinput
import re
import processing
from copy import deepcopy
from math import floor, ceil
from qgis.core import (
QgsVectorLayer,
QgsMapLayer,
QgsGraduatedSymbolRenderer,
QgsSymbol, QgsGradientColorRamp,
QgsRuleBasedRenderer,
QgsFillSymbol,
QgsProject,
QgsExpression,
Qgis,
QgsApplication,
QgsWkbTypes,
)
from qgis.PyQt.QtCore import (
QSettings,
QTranslator,
QCoreApplication,
qVersion,
QUrl,
Qt,
QUrlQuery,
)
from qgis.PyQt.QtWidgets import QAction, QFileDialog, QApplication, QMenu
from qgis.PyQt.QtGui import QIcon, QDesktopServices
from svir.dialogs.viewer_dock import ViewerDock
from svir.utilities.import_sv_data import get_loggedin_downloader
from svir.dialogs.download_layer_dialog import DownloadLayerDialog
from svir.dialogs.projects_manager_dialog import ProjectsManagerDialog
from svir.dialogs.select_sv_variables_dialog import SelectSvVariablesDialog
from svir.dialogs.settings_dialog import SettingsDialog
from svir.dialogs.transformation_dialog import TransformationDialog
from svir.dialogs.upload_settings_dialog import UploadSettingsDialog
from svir.dialogs.weight_data_dialog import WeightDataDialog
from svir.dialogs.recovery_modeling_dialog import RecoveryModelingDialog
from svir.dialogs.recovery_settings_dialog import RecoverySettingsDialog
from svir.dialogs.ipt_dialog import IptDialog
from svir.dialogs.taxtweb_dialog import TaxtwebDialog
from svir.dialogs.taxonomy_dialog import TaxonomyDialog
from svir.dialogs.drive_oq_engine_server_dialog import (
DriveOqEngineServerDialog)
from svir.thread_worker.abstract_worker import start_worker
from svir.thread_worker.download_platform_data_worker import (
DownloadPlatformDataWorker)
from svir.calculations.calculate_utils import calculate_composite_variable
from svir.calculations.process_layer import ProcessLayer
from svir.utilities.utils import (tr,
WaitCursorManager,
assign_default_weights,
clear_progress_message_bar,
SvNetworkError,
count_heading_commented_lines,
replace_fields,
toggle_select_features_widget,
read_layer_suppl_info_from_qgs,
write_layer_suppl_info_to_qgs,
log_msg,
save_layer_as_shapefile,
get_style,
get_checksum,
warn_missing_package,
)
from svir.utilities.shared import (DEBUG,
PROJECT_TEMPLATE,
THEME_TEMPLATE,
INDICATOR_TEMPLATE,
OPERATORS_DICT)
from svir.ui.tool_button_with_help_link import QToolButtonWithHelpLink
# DO NOT REMOVE THIS
# noinspection PyUnresolvedReferences
import svir.resources_rc # pylint: disable=unused-import # NOQA
from svir import IS_SCIPY_INSTALLED, IS_MATPLOTLIB_INSTALLED
[docs]class Irmt(object):
def __init__(self, iface):
if not IS_MATPLOTLIB_INSTALLED:
warn_missing_package('matplotlib')
return
# Save reference to the QGIS interface
self.iface = iface
self.canvas = self.iface.mapCanvas()
# initialize plugin directory
self.plugin_dir = os.path.dirname(__file__)
# initialize locale
locale = QSettings().value("locale/userLocale")[0:2]
locale_path = os.path.join(self.plugin_dir, 'i18n',
'irmt_{}.qm'.format(locale))
if os.path.exists(locale_path):
self.translator = QTranslator()
self.translator.load(locale_path)
if qVersion() > '4.3.3':
QCoreApplication.installTranslator(self.translator)
# our own toolbar
self.toolbar = None
# our own menu
self.menu = QMenu(self.iface.mainWindow())
self.menu.setTitle("OpenQuake IRMT")
# keep a list of the menu items, in order to easily unload them later
self.registered_actions = dict()
# avoid dialog to be deleted right after showing it
self.drive_oq_engine_server_dlg = None
self.ipt_dlg = None
self.taxtweb_dlg = None
self.taxonomy_dlg = None
# keep track of the supplemental information for each layer
# layer_id -> {}
# where each dict contains 'platform_layer_id',
# 'selected_project_definition_idx', 'project_definitions'
self.supplemental_information = {}
self.iface.currentLayerChanged.connect(self.current_layer_changed)
self.iface.newProjectCreated.connect(self.current_layer_changed)
self.iface.projectRead.connect(self.current_layer_changed)
QgsProject.instance().layersAdded.connect(self.layers_added)
QgsProject.instance().layersRemoved.connect(
self.layers_removed)
# get or create directory to store input files for the OQ-Engine
self.ipt_dir = self.get_ipt_dir()
[docs] def initGui(self):
if not IS_MATPLOTLIB_INSTALLED:
# the warning should have already been displayed by the __init__
return
# create our own toolbar
self.toolbar = self.iface.addToolBar('OpenQuake IRMT')
self.toolbar.setObjectName('IRMTToolBar')
menu_bar = self.iface.mainWindow().menuBar()
get_menu = self.get_menu(menu_bar, 'OpenQuake IRMT')
if get_menu is not None:
self.menu = get_menu
self.menu_action = menu_bar.insertMenu(
self.iface.firstRightStandardMenu().menuAction(), self.menu)
# Action to activate the modal dialog to import socioeconomic
# data from the platform
self.add_menu_item("import_sv_variables",
":/plugins/irmt/load.svg",
u"&Load socioeconomic indicators"
" from the OpenQuake Platform",
self.import_sv_variables,
enable=True,
add_to_layer_actions=False,
submenu='OQ Platform')
# Action to activate the modal dialog to import socioeconomic
# data from the platform
self.add_menu_item("import_layer",
":/plugins/irmt/load_layer.svg",
u"&Import project from the OpenQuake Platform",
self.download_layer,
enable=True,
add_to_layer_actions=False,
submenu='OQ Platform')
# Action to upload
self.add_menu_item("upload",
":/plugins/irmt/upload.svg",
u"&Upload project to the OpenQuake Platform",
self.upload,
enable=True,
add_to_layer_actions=True,
submenu='OQ Platform')
# # Action to drive ipt
# self.add_menu_item("ipt",
# ":/plugins/irmt/ipt.svg",
# u"OpenQuake Input Preparation Toolkit",
# self.ipt,
# enable=self.experimental_enabled(),
# submenu='OQ Engine',
# add_to_toolbar=True)
# # Action to drive taxtweb
# self.add_menu_item("taxtweb",
# ":/plugins/irmt/taxtweb.svg",
# u"OpenQuake TaxtWEB",
# self.taxtweb,
# enable=self.experimental_enabled(),
# submenu='OQ Engine',
# add_to_toolbar=True)
# Action to drive the oq-engine server
self.add_menu_item("drive_engine_server",
":/plugins/irmt/drive_oqengine.svg",
u"Drive the OpenQuake Engine",
self.on_drive_oq_engine_server_btn_clicked,
enable=True,
submenu='OQ Engine',
add_to_toolbar=True)
# Action to manage the projects
self.add_menu_item("project_definitions_manager",
":/plugins/irmt/copy.svg",
u"&Manage project definitions",
self.project_definitions_manager,
enable=False,
add_to_layer_actions=True,
submenu='Integrated risk')
# Action to activate the modal dialog to choose weighting of the
# data from the platform
self.add_menu_item("weight_data",
":/plugins/irmt/weights.svg",
u"&Weight data and calculate indices",
self.weight_data,
enable=False,
add_to_layer_actions=True,
add_to_toolbar=True,
submenu='Integrated risk')
# Action to run the recovery analysis
self.add_menu_item("recovery_modeling",
":/plugins/irmt/recovery.svg",
u"Run recovery modeling",
self.recovery_modeling,
enable=self.experimental_enabled(),
submenu='Recovery modeling')
# Action to set the recovery modeling parameters
self.add_menu_item("recovery_settings",
":/plugins/irmt/recovery_settings.svg",
u"Recovery modeling settings",
self.recovery_settings,
enable=self.experimental_enabled(),
submenu='Recovery modeling')
# Action to activate the modal dialog to select a layer and one of
# its attributes, in order to transform that attribute
self.add_menu_item("transform_attributes",
":/plugins/irmt/transform.svg",
u"&Transform attributes",
self.transform_attributes,
enable=False,
add_to_toolbar=True,
add_to_layer_actions=True,
submenu='Utilities')
# Action to open the Processing algorithm
# "Join attributes by location (summary)"
self.add_menu_item("aggregate",
":/plugins/irmt/aggregate.svg",
u"&Aggregate points by zone",
self.aggregate,
enable=True,
add_to_toolbar=False,
add_to_layer_actions=False,
submenu='Utilities')
self.menu.addSeparator()
# Action to activate the modal dialog to set up show_settings for the
# connection with the platform
self.add_menu_item("show_settings",
":/plugins/irmt/settings.svg",
u"&OpenQuake IRMT settings",
self.show_settings,
enable=True,
add_to_toolbar=True)
self._create_viewer_dock()
# Action to open the plugin's manual
self.add_menu_item("help",
":/plugins/irmt/manual.svg",
u"OpenQuake IRMT &manual",
self.show_manual,
enable=True,
add_to_toolbar=True)
self.update_actions_status()
@staticmethod
[docs] def experimental_enabled(self):
experimental_enabled = QSettings().value(
'/irmt/experimental_enabled', False, type=bool)
return experimental_enabled
[docs] def recovery_modeling(self):
if IS_SCIPY_INSTALLED:
dlg = RecoveryModelingDialog(self.iface)
dlg.exec_()
else:
warn_missing_package('scipy', self.iface.messageBar())
[docs] def recovery_settings(self):
dlg = RecoverySettingsDialog(self.iface)
dlg.exec_()
[docs] def aggregate(self):
processing.Processing.initialize()
alg_id = 'qgis:joinbylocationsummary'
alg = QgsApplication.processingRegistry().algorithmById(alg_id)
# make sure to use the actual lists of predicates and summaries as
# defined in the algorithm when it is instantiated
predicate_keys = [predicate[0] for predicate in alg.predicates]
PREDICATES = dict(zip(predicate_keys, range(len(predicate_keys))))
default_predicates = ['intersects']
summary_keys = [statistic[0] for statistic in alg.statistics]
SUMMARIES = dict(zip(summary_keys, range(len(summary_keys))))
default_summaries = ['sum', 'mean']
zonal_layer = None
points_layer = None
for layer in QgsProject.instance().mapLayers().values():
if layer.type() != QgsMapLayer.VectorLayer:
continue
if layer.geometryType() == QgsWkbTypes.PolygonGeometry:
zonal_layer = layer
continue
elif layer.geometryType() == QgsWkbTypes.PointGeometry:
points_layer = layer
continue
initial_params = {
'INPUT': zonal_layer,
'JOIN': points_layer,
'PREDICATE': [PREDICATES[predicate]
for predicate in default_predicates],
'SUMMARIES': [SUMMARIES[summary]
for summary in default_summaries],
}
processing.execAlgorithmDialog(alg_id, initial_params)
[docs] def ipt(self):
if self.ipt_dlg is None:
# we need self because ipt must be able to drive the oq-engine
self.ipt_dlg = IptDialog(self.ipt_dir, self)
self.ipt_dlg.show()
self.ipt_dlg.raise_()
[docs] def taxtweb(self):
if self.taxtweb_dlg is None:
self.instantiate_taxonomy_dlg()
self.taxtweb_dlg = TaxtwebDialog(self.taxonomy_dlg)
self.taxtweb_dlg.show()
self.taxtweb_dlg.raise_()
[docs] def instantiate_taxonomy_dlg(self):
if self.taxonomy_dlg is None:
self.taxonomy_dlg = TaxonomyDialog()
[docs] def taxonomy(self):
self.instantiate_taxonomy_dlg()
self.taxonomy_dlg.show()
self.taxonomy_dlg.raise_()
[docs] def on_drive_oq_engine_server_btn_clicked(self):
# we can't call drive_oq_engine_server directly, otherwise the signal
# triggered by the button would set show=False (it silently passes an
# additional parameter)
self.drive_oq_engine_server(show=True)
[docs] def drive_oq_engine_server(self, show=True, hostname=None):
if self.drive_oq_engine_server_dlg is None:
self.drive_oq_engine_server_dlg = DriveOqEngineServerDialog(
self.iface, self.viewer_dock, hostname=hostname)
else:
self.drive_oq_engine_server_dlg.attempt_login()
if show:
self.drive_oq_engine_server_dlg.show()
self.drive_oq_engine_server_dlg.raise_()
if self.drive_oq_engine_server_dlg.is_logged_in:
self.drive_oq_engine_server_dlg.start_polling()
else:
log_msg('Unable to connect to the OpenQuake Engine server. '
'Please check that the server (WebUI) is running and the '
'plugin connection settings are correct.', level='C',
message_bar=self.drive_oq_engine_server_dlg.message_bar)
[docs] def on_same_fs(self, checksum_file_path, local_checksum):
# initialize drive_oq_engine_server_dlg dialog without displaying it
self.drive_oq_engine_server(show=False)
return self.drive_oq_engine_server_dlg.on_same_fs(
checksum_file_path, local_checksum)
[docs] def reset_drive_oq_engine_server_dlg(self, hostname=None):
if self.drive_oq_engine_server_dlg is not None:
was_dlg_visible = self.drive_oq_engine_server_dlg.isVisible()
self.drive_oq_engine_server_dlg.reject()
self.drive_oq_engine_server_dlg = DriveOqEngineServerDialog(
self.iface, self.viewer_dock, hostname=hostname)
if was_dlg_visible:
self.drive_oq_engine_server_dlg.show()
[docs] def show_manual(self):
base_url = os.path.abspath(os.path.join(
__file__, os.pardir, 'help', 'build', 'html', 'index.html'))
if not os.path.exists(base_url):
msg = 'Help file not found: %s' % base_url
log_msg(msg, level='C', message_bar=self.iface.messageBar())
url = QUrl.fromLocalFile(base_url)
QDesktopServices.openUrl(url)
[docs] def layers_added(self):
self.update_actions_status()
[docs] def layers_removed(self, layer_ids):
for layer_id in layer_ids:
self.clear_layer_suppl_info(layer_id)
self.update_actions_status()
[docs] def clear_layer_suppl_info(self, layer_id):
self.supplemental_information.pop(layer_id, None)
QgsProject.instance().removeEntry('irmt', layer_id)
[docs] def current_layer_changed(self, layer=None):
self.update_actions_status()
output_type = ''
if layer:
output_type = layer.customProperty('output_type') or ''
self.viewer_dock.change_output_type(output_type)
self.viewer_dock.layer_changed()
[docs] def update_actions_status(self):
"""
Enable plugin's actions depending on the current status of the workflow
"""
reg = QgsProject.instance()
layer_count = len(list(reg.mapLayers()))
# Enable/disable "transform" action
self.registered_actions["transform_attributes"].setDisabled(
layer_count == 0)
if DEBUG:
log_msg('Selected: %s' % self.iface.activeLayer())
try:
# Activate actions which require a vector layer to be selected
if self.iface.activeLayer().type() != QgsMapLayer.VectorLayer:
raise AttributeError
self.supplemental_information[self.iface.activeLayer().id()]
self.registered_actions[
"project_definitions_manager"].setEnabled(True)
self.registered_actions["weight_data"].setEnabled(True)
self.registered_actions["transform_attributes"].setEnabled(True)
read_layer_suppl_info_from_qgs(
self.iface.activeLayer().id(), self.supplemental_information)
self.registered_actions["upload"].setEnabled(True)
except KeyError:
# self.supplemental_information[self.iface.activeLayer().id()]
# is not defined
self.registered_actions["upload"].setEnabled(False)
self.registered_actions[
"project_definitions_manager"].setEnabled(True)
self.registered_actions["weight_data"].setEnabled(True)
except AttributeError:
# self.iface.activeLayer().id() does not exist
# or self.iface.activeLayer() is not vector
self.registered_actions["transform_attributes"].setEnabled(False)
self.registered_actions["weight_data"].setEnabled(False)
self.registered_actions["upload"].setEnabled(False)
self.registered_actions[
"project_definitions_manager"].setEnabled(False)
[docs] def unload(self):
"""
Remove all plugin's actions and corresponding buttons and connects
"""
if not IS_MATPLOTLIB_INSTALLED:
return
# stop any running timers
if self.drive_oq_engine_server_dlg is not None:
self.drive_oq_engine_server_dlg.reject()
# Remove the plugin menu items and toolbar icons
for action_name in self.registered_actions:
action = self.registered_actions[action_name]
# Remove the actions in the layer legend
self.iface.removeCustomActionForLayerType(action)
self.iface.removeToolBarIcon(action)
clear_progress_message_bar(self.iface.messageBar())
self.menu.clear()
self.iface.mainWindow().menuBar().removeAction(self.menu_action)
# remove the dock
self.viewer_dock.remove_connects()
self.viewer_dock.deleteLater()
# remove connects
self.iface.currentLayerChanged.disconnect(self.current_layer_changed)
self.iface.newProjectCreated.disconnect(self.current_layer_changed)
self.iface.projectRead.disconnect(self.current_layer_changed)
QgsProject.instance().layersAdded.disconnect(
self.layers_added)
QgsProject.instance().layersRemoved.disconnect(
self.layers_removed)
[docs] def import_sv_variables(self):
"""
Open a modal dialog to select socioeconomic variables to
download from the openquake platform
"""
# login to platform, to be able to retrieve sv indices
sv_downloader = get_loggedin_downloader(self.iface.messageBar())
if sv_downloader is None:
self.show_settings()
return
try:
dlg = SelectSvVariablesDialog(sv_downloader)
if dlg.exec_():
dest_filename, _ = QFileDialog.getSaveFileName(
dlg,
'Download destination',
os.path.expanduser("~"),
'Shapefiles (*.shp)')
if dest_filename:
if dest_filename[-4:] != ".shp":
dest_filename += ".shp"
else:
return
# TODO: We should fix the workflow in case no geometries are
# downloaded. Currently we must download them, so the checkbox
# to let the user choose has been temporarily removed.
# load_geometries = dlg.load_geometries_chk.isChecked()
load_geometries = True
msg = ("Loading socioeconomic data from the OpenQuake "
"Platform...")
# Retrieve the indices selected by the user
indices_list = []
iso_codes_list = []
project_definition = deepcopy(PROJECT_TEMPLATE)
svi_themes = project_definition[
'children'][1]['children']
known_themes = []
with WaitCursorManager(msg, self.iface.messageBar()):
while dlg.indicator_multiselect.selected_widget.count(
) > 0:
item = \
dlg.indicator_multiselect.selected_widget.takeItem(
0)
ind_code = item.text().split(':')[0]
ind_info = dlg.indicators_info_dict[ind_code]
sv_theme = ind_info['theme']
sv_field = ind_code
sv_name = ind_info['name']
self._add_new_theme(svi_themes,
known_themes,
sv_theme,
sv_name,
sv_field)
indices_list.append(sv_field)
while dlg.country_multiselect.selected_widget.count() > 0:
item = \
dlg.country_multiselect.selected_widget.takeItem(0)
# get the iso from something like:
# country_name (iso_code)
iso_code = item.text().split('(')[1].split(')')[0]
iso_codes_list.append(iso_code)
# create string for DB query
indices_string = ",".join(indices_list)
iso_codes_string = ",".join(iso_codes_list)
assign_default_weights(svi_themes)
worker = DownloadPlatformDataWorker(
sv_downloader,
indices_string,
load_geometries,
iso_codes_string)
worker.successfully_finished.connect(
lambda result: self._data_download_successful(
result,
load_geometries,
dest_filename,
project_definition,
dlg.indicators_info_dict))
start_worker(worker, self.iface.messageBar(),
'Downloading data from platform')
except SvNetworkError as e:
log_msg('An error occurred. See the traceback for details.',
level='C', message_bar=self.iface.messageBar(),
exception=e)
def _data_download_successful(
self, result, load_geometries, dest_filename, project_definition,
indicators_info_dict):
"""
Called once the DonloadPlatformDataWorker has successfully downloaded
socioeconomic data as a csv file.
:param result: a tuple (fname, msg) where fname is the name of the csv
file exported by the OQ-Platform and msg is the message
returned
:param load_geometries: if True, also geometries were downloaded
:type load_geometries: bool
:param dest_filename: name of the file that will store the vector layer
containing the downloaded data
:param project_definition: the project definition that was
automatically built based on the DB structure
:param indicators_info_dict:
dict ind_code -> dict with additional info about it
"""
fname, msg = result
display_msg = tr("Socioeconomic data loaded in a new layer")
log_msg(display_msg, level='S', message_bar=self.iface.messageBar())
# don't remove the file, otherwise there will be concurrency
# problems
# fix an issue of QGIS 10, that is unable to read
# multipolygons if they contain spaces between two polygons.
# We remove those spaces from the csv file before importing it.
# TODO: Remove this as soon as QGIS solves that issue
for line in fileinput.input(fname, inplace=True):
line = re.sub('\)\),\s\(\(', ')),((', line.rstrip()) # NOQA
line = re.sub('\),\s\(', '),(', line.rstrip()) # NOQA
# thanks to inplace=True, 'print line' writes the line into the
# input file, overwriting the original line
print(line)
# count top lines in the csv starting with '#'
lines_to_skip_count = count_heading_commented_lines(fname)
url = QUrl.fromLocalFile(fname)
url_query = QUrlQuery()
url_query.addQueryItem('delimiter', ',')
url_query.addQueryItem('skipLines', str(lines_to_skip_count))
url_query.addQueryItem('trimFields', 'yes')
if load_geometries:
url_query.addQueryItem('crs', 'epsg:4326')
url_query.addQueryItem('wktField', 'geometry')
url.setQuery(url_query)
layer_uri = url.toString()
# create vector layer from the csv file exported by the
# platform (it is still not editable!)
vlayer_csv = QgsVectorLayer(layer_uri,
'socioeconomic_data_export',
'delimitedtext')
if not load_geometries:
if vlayer_csv.isValid():
QgsProject.instance().addMapLayer(vlayer_csv)
else:
raise RuntimeError('Layer invalid')
layer = vlayer_csv
else:
writer_error, error_msg = save_layer_as_shapefile(
vlayer_csv, dest_filename)
if writer_error:
raise RuntimeError(
'Could not save shapefile. %s: %s' % (writer_error,
error_msg))
layer = QgsVectorLayer(
dest_filename, 'Socioeconomic data', 'ogr')
if layer.isValid():
QgsProject.instance().addMapLayer(layer)
else:
raise RuntimeError('Layer invalid')
self.iface.setActiveLayer(layer)
suppl_info = {
'selected_project_definition_idx': 0,
'project_definitions': [project_definition]}
write_layer_suppl_info_to_qgs(layer.id(), suppl_info)
self.update_actions_status()
# assign ind_name as alias for each ind_code
for field_idx, field in enumerate(layer.fields()):
if field.name() in indicators_info_dict:
layer.setFieldAlias(
field_idx, indicators_info_dict[field.name()]['name'])
elif field.name() == 'ISO':
layer.setFieldAlias(
field_idx, 'Country ISO code')
elif field.name() == 'COUNTRY_NA':
layer.setFieldAlias(
field_idx, 'Country name')
[docs] def download_layer(self):
"""
Open dialog to select one of the integrated risk projects available on
the OQ-Platform and download it as a qgis project
"""
sv_downloader = get_loggedin_downloader(self.iface.messageBar())
if sv_downloader is None:
self.show_settings()
return
dlg = DownloadLayerDialog(self.iface, sv_downloader)
if dlg.exec_():
read_layer_suppl_info_from_qgs(
dlg.downloaded_layer_id, self.supplemental_information)
suppl_info = self.supplemental_information[dlg.downloaded_layer_id]
# in case of multiple project definitions, let the user select one
if len(suppl_info['project_definitions']) > 1:
self.project_definitions_manager()
self.update_actions_status()
@staticmethod
def _add_new_theme(svi_themes,
known_themes,
indicator_theme,
indicator_name,
indicator_field):
"""add a new theme to the project_definition"""
theme = deepcopy(THEME_TEMPLATE)
theme['name'] = indicator_theme
if theme['name'] not in known_themes:
known_themes.append(theme['name'])
svi_themes.append(theme)
theme_position = known_themes.index(theme['name'])
level = float('4.%d' % theme_position)
# add a new indicator to a theme
new_indicator = deepcopy(INDICATOR_TEMPLATE)
new_indicator['name'] = indicator_name
new_indicator['field'] = indicator_field
new_indicator['level'] = level
svi_themes[theme_position]['children'].append(new_indicator)
[docs] def project_definitions_manager(self):
"""
Open a dialog to manage one or multiple project definitions for the
selected layer.
"""
read_layer_suppl_info_from_qgs(
self.iface.activeLayer().id(), self.supplemental_information)
select_proj_def_dlg = ProjectsManagerDialog(self.iface)
if select_proj_def_dlg.exec_():
selected_project_definition = select_proj_def_dlg.selected_proj_def
added_attrs_ids, discarded_feats, project_definition = \
self._recalculate_indexes(selected_project_definition)
self.notify_added_attrs_and_discarded_feats(added_attrs_ids,
discarded_feats)
select_proj_def_dlg.suppl_info['project_definitions'][
select_proj_def_dlg.suppl_info[
'selected_project_definition_idx']] = project_definition
write_layer_suppl_info_to_qgs(
self.iface.activeLayer().id(), select_proj_def_dlg.suppl_info)
self.redraw_ir_layer(project_definition)
[docs] def notify_added_attrs_and_discarded_feats(self,
added_attrs_ids,
discarded_feats):
"""
Notify through the message bar that new attributes have been added
to the layer and specify if missing or invalid values were found.
"""
if added_attrs_ids:
all_field_names = [
field.name() for field in self.iface.activeLayer().fields()]
added_attrs_names = [all_field_names[attr_id]
for attr_id in added_attrs_ids]
msg = ('New attributes have been added to the layer: %s'
% ', '.join(added_attrs_names))
log_msg(msg, level='S', message_bar=self.iface.messageBar())
if discarded_feats:
discarded_feats_ids_missing = [
feat.feature_id for feat in discarded_feats
if feat.reason == 'Missing value']
if discarded_feats_ids_missing:
widget = toggle_select_features_widget(
tr('Warning'),
tr('Missing values were found in some features while '
'calculating composite variables'),
tr('Select features with incomplete data'),
self.iface.activeLayer(),
discarded_feats_ids_missing,
self.iface.activeLayer().selectedFeatureIds())
self.iface.messageBar().pushWidget(widget, Qgis.Warning)
discarded_feats_ids_invalid = [
feat.feature_id for feat in discarded_feats
if feat.reason == 'Invalid value']
if discarded_feats_ids_invalid:
widget = toggle_select_features_widget(
tr('Warning'),
tr('Invalid values were found in some features while '
'calculating composite variables using the chosen '
'operators (e.g. the geometric mean fails when '
'attempting to perform the root of a '
'negative value)'),
tr('Select features with invalid data'),
self.iface.activeLayer(),
discarded_feats_ids_invalid,
self.iface.activeLayer().selectedFeatureIds())
self.iface.messageBar().pushWidget(widget, Qgis.Warning)
[docs] def weight_data(self):
"""
Open a modal dialog to select weights in a d3.js visualization and to
run integrated risk calculations.
"""
active_layer_id = self.iface.activeLayer().id()
# get the project definition to work with, or create a default one
read_layer_suppl_info_from_qgs(
active_layer_id, self.supplemental_information)
try:
suppl_info = self.supplemental_information[active_layer_id]
orig_project_definition = suppl_info['project_definitions'][
suppl_info['selected_project_definition_idx']]
except KeyError:
orig_project_definition = deepcopy(PROJECT_TEMPLATE)
suppl_info = {'selected_project_definition_idx': 0,
'project_definitions': [orig_project_definition]
}
write_layer_suppl_info_to_qgs(active_layer_id, suppl_info)
edited_project_definition = deepcopy(orig_project_definition)
# Save the style so the following styling can be undone
fd, sld_file_name = tempfile.mkstemp(suffix=".sld")
os.close(fd)
(resp_text, sld_was_saved) = \
self.iface.activeLayer().saveSldStyle(sld_file_name)
if sld_was_saved:
if DEBUG:
log_msg('original sld saved in %s' % sld_file_name)
else:
err_msg = 'Unable to save the sld: %s' % resp_text
log_msg(err_msg, level='C', message_bar=self.iface.messageBar())
dlg = WeightDataDialog(self.iface, edited_project_definition)
dlg.show()
self.redraw_ir_layer(edited_project_definition)
dlg.json_cleaned.connect(lambda data: self._weights_changed(data, dlg))
if dlg.exec_():
# If the user just opens the dialog and presses OK, it probably
# means they want to just run the index calculation, so we
# recalculate the indexes. In case they have already made any
# change and consequent calculations, we don't need to redo the
# same calculation after OK is pressed.
if not dlg.any_changes_made:
(added_attrs_ids,
discarded_feats,
edited_project_definition) = self._recalculate_indexes(
dlg.project_definition)
dlg.added_attrs_ids.update(added_attrs_ids)
dlg.discarded_feats = discarded_feats
# But if changes were made to the tree while the on-the-fly
# calculation was disabled, then we need to recalculate indices
# using the modified project definition
elif dlg.modified_project_definition:
(added_attrs_ids,
discarded_feats,
edited_project_definition) = self._recalculate_indexes(
dlg.modified_project_definition)
dlg.added_attrs_ids.update(added_attrs_ids)
dlg.discarded_feats = discarded_feats
else:
edited_project_definition = deepcopy(dlg.project_definition)
self.notify_added_attrs_and_discarded_feats(
dlg.added_attrs_ids, dlg.discarded_feats)
self.update_actions_status()
else: # 'cancel' was pressed or the dialog was closed
if sld_was_saved: # was able to save the original style
self.iface.activeLayer().loadSldStyle(sld_file_name)
# if any of the indices were saved before starting to edit the
# tree, the corresponding fields might have been modified.
# Therefore we recalculate the indices using the old project
# definition
if dlg.any_changes_made:
edited_project_definition = deepcopy(orig_project_definition)
iri_node = edited_project_definition
ri_node = edited_project_definition['children'][0]
svi_node = edited_project_definition['children'][1]
if ('field' in iri_node
or 'field' in ri_node or 'field' in svi_node):
added_attrs_ids, _, edited_project_definition = \
self._recalculate_indexes(iri_node)
dlg.added_attrs_ids.update(added_attrs_ids)
# delete attributes added while the dialog was open
ProcessLayer(self.iface.activeLayer()).delete_attributes(
dlg.added_attrs_ids)
dlg.json_cleaned.disconnect()
# store the correct project definitions
selected_idx = suppl_info['selected_project_definition_idx']
suppl_info['project_definitions'][selected_idx] = deepcopy(
edited_project_definition)
write_layer_suppl_info_to_qgs(active_layer_id, suppl_info)
self.redraw_ir_layer(edited_project_definition)
def _weights_changed(self, data, dlg):
added_attrs_ids, discarded_feats, project_definition = \
self._recalculate_indexes(data)
dlg.added_attrs_ids.update(added_attrs_ids)
dlg.discarded_feats = discarded_feats
dlg.update_project_definition(project_definition)
self.redraw_ir_layer(project_definition)
def _recalculate_indexes(self, data):
project_definition = deepcopy(data)
if self.is_iri_computable(project_definition):
iri_node = deepcopy(project_definition)
msg = 'Calculating %s' % iri_node['name']
with WaitCursorManager(msg, self.iface.messageBar()):
(added_attrs_ids, discarded_feats,
iri_node, was_iri_computed) = calculate_composite_variable(
self.iface, self.iface.activeLayer(), iri_node)
project_definition = deepcopy(iri_node)
return added_attrs_ids, discarded_feats, project_definition
svi_added_attrs_ids = set()
svi_discarded_feats = set()
ri_added_attrs_ids = set()
ri_discarded_feats = set()
was_svi_computed = False
if self.is_svi_computable(project_definition):
svi_node = deepcopy(project_definition['children'][1])
msg = 'Calculating %s' % svi_node['name']
with WaitCursorManager(msg, self.iface.messageBar()):
(svi_added_attrs_ids, svi_discarded_feats,
svi_node, was_svi_computed) = calculate_composite_variable(
self.iface, self.iface.activeLayer(), svi_node)
project_definition['children'][1] = deepcopy(svi_node)
was_ri_computed = False
if self.is_ri_computable(project_definition):
ri_node = deepcopy(project_definition['children'][0])
msg = 'Calculating %s' % ri_node['name']
with WaitCursorManager(msg, self.iface.messageBar()):
(ri_added_attrs_ids, ri_discarded_feats,
ri_node, was_ri_computed) = calculate_composite_variable(
self.iface, self.iface.activeLayer(), ri_node)
project_definition['children'][0] = deepcopy(ri_node)
if not was_svi_computed and not was_ri_computed:
return set(), set(), data
added_attrs_ids = set()
discarded_feats = set()
if svi_added_attrs_ids:
added_attrs_ids.update(svi_added_attrs_ids)
if svi_discarded_feats:
discarded_feats.update(svi_discarded_feats)
if ri_added_attrs_ids:
added_attrs_ids.update(ri_added_attrs_ids)
if ri_discarded_feats:
discarded_feats.update(ri_discarded_feats)
return added_attrs_ids, discarded_feats, project_definition
[docs] def is_svi_computable(self, proj_def):
"""
Check if is it possible to compute the social vulnerability index,
depending on the current project definition structure.
"""
try:
svi_node = proj_def['children'][1]
except KeyError:
return False
# check that there is at least one theme
if 'children' not in svi_node:
return False
if len(svi_node['children']) == 0:
return False
# check that each theme contains at least one indicator
for theme_node in svi_node['children']:
if 'children' not in theme_node:
return False
if len(theme_node['children']) == 0:
return False
return True
[docs] def is_svi_renderable(self, proj_def):
"""
Check if is it possible to render the social vulnerability index,
depending on the current project definition structure.
"""
if not self.is_svi_computable(proj_def):
return False
# check that that the svi_node has a corresponding field
try:
svi_node = proj_def['children'][1]
except KeyError:
return False
if 'field' not in svi_node:
return False
return True
[docs] def is_ri_computable(self, proj_def):
"""
Check if is it possible to compute the risk index,
depending on the current project definition structure.
"""
try:
ri_node = proj_def['children'][0]
except KeyError:
return False
# check that there is at least one risk indicator
if 'children' not in ri_node:
return False
if len(ri_node['children']) == 0:
return False
return True
[docs] def is_ri_renderable(self, proj_def):
"""
Check if is it possible to render the risk index,
depending on the current project definition structure.
"""
if not self.is_ri_computable(proj_def):
return False
try:
ri_node = proj_def['children'][0]
except KeyError:
return False
# check that that the ri_node has a corresponding field
if 'field' not in ri_node:
return False
return True
[docs] def is_iri_computable(self, proj_def):
"""
Check if is it possible to compute the integrated risk index,
depending on the current project definition structure.
"""
iri_node = proj_def
# if the IRI node is a custom field, then we don't want to recompute it
# unless the description contains a valid formula
if iri_node['operator'] == OPERATORS_DICT['CUSTOM']:
customFormula = proj_def.get('customFormula', '')
expression = QgsExpression(customFormula)
if customFormula == '' or not expression.isValid():
return False
# check that all the sub-indices are well-defined
if not self.is_ri_computable(iri_node):
return False
if not self.is_svi_computable(iri_node):
return False
return True
[docs] def is_iri_renderable(self, proj_def):
"""
Check if is it possible to render the integrated risk index,
depending on the current project definition structure.
"""
iri_node = proj_def
# check that that the iri_node has a corresponding field
if 'field' not in iri_node:
return False
# if the IRI node is a custom field, then we assume it is renderable
if iri_node['operator'] == OPERATORS_DICT['CUSTOM']:
return True
if not self.is_iri_computable(proj_def):
return False
return True
[docs] def redraw_ir_layer(self, data):
"""
If the user has explicitly selected a field to use for styling, use
it, otherwise attempt to show the IRI, or the SVI, or the RI
"""
if 'style_by_field' in data:
target_field = data['style_by_field']
printing_str = target_field
# if an IRI has been already calculated, show it
elif self.is_iri_renderable(data):
iri_node = data
target_field = iri_node['field']
printing_str = 'IRI'
# else show the SVI if possible
elif self.is_svi_renderable(data):
svi_node = data['children'][1]
target_field = svi_node['field']
printing_str = 'SVI'
# otherwise attempt to show the RI
elif self.is_ri_renderable(data):
ri_node = data['children'][0]
target_field = ri_node['field']
printing_str = 'RI'
else: # if none of them can be rendered, then do nothing
return
# proceed only if the target field is actually a field of the layer
if self.iface.activeLayer().fields().indexOf(target_field) == -1:
return
if DEBUG:
import pprint
ppdata = pprint.pformat(data, indent=4)
log_msg('REDRAWING %s using: \n%s' % (printing_str, ppdata))
style = get_style(self.iface.activeLayer(), self.iface.messageBar())
if style['force_restyling']:
self._apply_style(style, target_field)
# NOTE QGIS3 probably not needed
# self.iface.layerTreeView().refreshLayerSymbology(
# self.iface.activeLayer().id())
self.iface.mapCanvas().refresh()
def _apply_style(self, style, target_field):
rule_renderer = QgsRuleBasedRenderer(
QgsSymbol.defaultSymbol(self.iface.activeLayer().geometryType()))
root_rule = rule_renderer.rootRule()
not_null_rule = root_rule.children()[0].clone()
# strip parentheses from stringified color HSL
col_str = str(style['color_from'].getHsl())[1:-1]
not_null_rule.setSymbol(QgsFillSymbol.createSimple(
{'color': col_str,
'color_border': '0,0,0,255'}))
not_null_rule.setFilterExpression(
'%s IS NOT NULL' % QgsExpression.quotedColumnRef(target_field))
not_null_rule.setLabel('%s:' % target_field)
root_rule.appendChild(not_null_rule)
null_rule = root_rule.children()[0].clone()
null_rule.setSymbol(QgsFillSymbol.createSimple(
{'style': 'no',
'color_border': '255,255,0,255',
'width_border': '0.5'}))
null_rule.setFilterExpression(
'%s IS NULL' % QgsExpression.quotedColumnRef(target_field))
null_rule.setLabel(tr('Invalid value'))
root_rule.appendChild(null_rule)
ramp = QgsGradientColorRamp(
style['color_from'], style['color_to'])
graduated_renderer = QgsGraduatedSymbolRenderer.createRenderer(
self.iface.activeLayer(),
QgsExpression.quotedColumnRef(target_field),
style['classes'],
style['mode'],
QgsSymbol.defaultSymbol(self.iface.activeLayer().geometryType()),
ramp)
if graduated_renderer.ranges():
first_range_index = 0
last_range_index = len(graduated_renderer.ranges()) - 1
# NOTE: Workaround to avoid rounding problem (QGIS renderer's bug)
# which creates problems styling the zone containing the lowest
# and highest values in some cases
lower_value = \
graduated_renderer.ranges()[first_range_index].lowerValue()
decreased_lower_value = floor(lower_value * 10000.0) / 10000.0
graduated_renderer.updateRangeLowerValue(
first_range_index, decreased_lower_value)
upper_value = \
graduated_renderer.ranges()[last_range_index].upperValue()
increased_upper_value = ceil(upper_value * 10000.0) / 10000.0
graduated_renderer.updateRangeUpperValue(
last_range_index, increased_upper_value)
elif DEBUG:
log_msg('All features are NULL')
# create value ranges
rule_renderer.refineRuleRanges(not_null_rule, graduated_renderer)
# remove default rule
root_rule.removeChildAt(0)
self.iface.activeLayer().setRenderer(rule_renderer)
[docs] def show_settings(self):
"""
Open a dialog to specify the connection settings used to interact
with the OpenQuake Platform
"""
SettingsDialog(self.iface, self).exec_()
[docs] def upload(self):
"""
Open a dialog to upload the current project to the OpenQuake Platform
"""
temp_dir = tempfile.gettempdir()
file_stem = '%s%sqgis_irmt_%s' % (temp_dir, os.path.sep, uuid.uuid4())
active_layer_id = self.iface.activeLayer().id()
read_layer_suppl_info_from_qgs(
active_layer_id, self.supplemental_information)
suppl_info = self.supplemental_information[active_layer_id]
# add layer's bounding box
extent = self.iface.activeLayer().extent()
bbox = {'minx': extent.xMinimum(),
'miny': extent.yMinimum(),
'maxx': extent.xMaximum(),
'maxy': extent.yMaximum()}
suppl_info['bounding_box'] = bbox
dlg = UploadSettingsDialog(self.iface, suppl_info, file_stem)
dlg.exec_()
[docs] def toggle_dock_visibility(self):
"""Show or hide the dock widget."""
if self.viewer_dock.isVisible():
self.viewer_dock.setVisible(False)
else:
self.viewer_dock.setVisible(True)
self.viewer_dock.raise_()
def _create_viewer_dock(self):
"""Create dockwidget and tabify it with the legend."""
# Action to drive the oq-engine server
action = self.add_menu_item("toggle_viewer_dock",
":/plugins/irmt/plot.svg",
u"Toggle viewer dock",
self.toggle_dock_visibility,
enable=True,
add_to_toolbar=True,
set_checkable=True,
set_checked=True)
self.viewer_dock = ViewerDock(self.iface, action)
self.viewer_dock.setObjectName('IRMT-Dock')
self.iface.addDockWidget(Qt.RightDockWidgetArea, self.viewer_dock)
legend_tab = self.iface.mainWindow().findChild(QApplication, 'Legend')
if legend_tab:
self.iface.mainWindow().tabifyDockWidget(
legend_tab, self.viewer_dock)
self.viewer_dock.raise_()
self.registered_actions['toggle_viewer_dock'].setChecked(
self.viewer_dock.isVisible())
self.viewer_dock.visibilityChanged[bool].connect(
self.on_viewer_dock_visibility_changed)
[docs] def on_viewer_dock_visibility_changed(self, visible):
self.registered_actions['toggle_viewer_dock'].setChecked(visible)
[docs] def get_ipt_dir(self):
home_dir = os.path.expanduser("~")
ipt_dir = os.path.join(home_dir, ".gem", "irmt", "ipt")
if not os.path.exists(ipt_dir):
os.makedirs(ipt_dir)
return ipt_dir
[docs] def get_ipt_checksum(self):
unique_filename = ".%s" % uuid.uuid4().hex
checksum_file_path = os.path.join(self.ipt_dir, unique_filename)
with open(checksum_file_path, "w", newline='') as f:
f.write(os.urandom(32))
return checksum_file_path, get_checksum(checksum_file_path)