From 876b2bd1bc0863f5aa34c63230b0ae3e96a7469b Mon Sep 17 00:00:00 2001 From: Amandus Butzer Date: Wed, 9 Mar 2022 14:55:27 +0100 Subject: [PATCH 1/3] Added provider storage for ORStoolsDialog the provider choice is now persisted in the config.yml file and automatically selected in the ORStoolsDialog window. The configmanager was extended and can now set and get the active provider --- CHANGELOG.md | 7 +- ORStools/gui/ORStoolsDialog.py | 12 ++- ORStools/utils/configmanager.py | 170 +++++++++++++++++++------------- 3 files changed, 117 insertions(+), 72 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d769ee0d..f5b22d0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,9 +38,14 @@ RELEASING: 12. Upload the package to https://plugins.qgis.org/plugins/ORStools/ (Manage > Add Version) 13. Create new release in GitHub with tag version and release title of `vX.X.X` --> +## [Unreleased] + +### Added +- active provider storage, persisted between QGIS sessions ([#168](https://github.com/GIScience/orstools-qgis-plugin/pull/166)) + ## [1.5.2] - 2022-01-20 -## Fixed +### Fixed - error for layers with z/m values ([#166](https://github.com/GIScience/orstools-qgis-plugin/pull/166)) ## [1.5.1] - 2022-01-11 diff --git a/ORStools/gui/ORStoolsDialog.py b/ORStools/gui/ORStoolsDialog.py index 96d7a492..0309c1bc 100644 --- a/ORStools/gui/ORStoolsDialog.py +++ b/ORStools/gui/ORStoolsDialog.py @@ -58,6 +58,7 @@ from . import resources_rc + def on_config_click(parent): """Pop up provider config window. Outside of classes because it's accessed by multiple dialogs. @@ -97,6 +98,12 @@ def on_about_click(parent): ) +def on_change_save_provider(index): + """Set the provider with the passed index as active provider if it isn't already""" + if index != configmanager.get_active_provider_index(): + configmanager.set_active_provider(index) + + class ORStoolsDialogMain: """Defines all mandatory QGIS things about dialog.""" @@ -219,6 +226,9 @@ def _init_gui_control(self): for provider in providers: self.dlg.provider_combo.addItem(provider['name'], provider) + self.dlg.provider_combo.setCurrentIndex(configmanager.get_active_provider_index()) + self.dlg.provider_combo.activated.connect(on_change_save_provider) + self.dlg.show() def run_gui_control(self): @@ -292,7 +302,7 @@ def run_gui_control(self): params['coordinates'] = directions.get_request_line_feature() profile = self.dlg.routing_travel_combo.currentText() # abort on empty avoid polygons layer - if 'options' in params and 'avoid_polygons' in params['options']\ + if 'options' in params and 'avoid_polygons' in params['options'] \ and params['options']['avoid_polygons'] == {}: QMessageBox.warning( self.dlg, diff --git a/ORStools/utils/configmanager.py b/ORStools/utils/configmanager.py index c84e87f6..837b759e 100644 --- a/ORStools/utils/configmanager.py +++ b/ORStools/utils/configmanager.py @@ -1,70 +1,100 @@ -# -*- coding: utf-8 -*- -""" -/*************************************************************************** - ORStools - A QGIS plugin - QGIS client to query openrouteservice - ------------------- - begin : 2017-02-01 - git sha : $Format:%H$ - copyright : (C) 2021 by HeiGIT gGmbH - email : support@openrouteservice.heigit.org - ***************************************************************************/ - - This plugin provides access to openrouteservice API functionalities - (https://openrouteservice.org), developed and - maintained by the openrouteservice team of HeiGIT gGmbH, Germany. By using - this plugin you agree to the ORS terms of service - (https://openrouteservice.org/terms-of-service/). - -/*************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ -""" -import os - -import yaml - -from ORStools import CONFIG_PATH - - -def read_config(): - """ - Reads config.yml from file and returns the parsed dict. - - :returns: Parsed settings dictionary. - :rtype: dict - """ - with open(CONFIG_PATH) as f: - doc = yaml.safe_load(f) - - return doc - - -def write_config(new_config): - """ - Dumps new config - - :param new_config: new provider settings after altering in dialog. - :type new_config: dict - """ - with open(CONFIG_PATH, 'w') as f: - yaml.safe_dump(new_config, f) - - -def write_env_var(key, value): - """ - Update quota env variables - - :param key: environment variable to update. - :type key: str - - :param value: value for env variable. - :type value: str - """ - os.environ[key] = value +# -*- coding: utf-8 -*- +""" +/*************************************************************************** + ORStools + A QGIS plugin + QGIS client to query openrouteservice + ------------------- + begin : 2017-02-01 + git sha : $Format:%H$ + copyright : (C) 2021 by HeiGIT gGmbH + email : support@openrouteservice.heigit.org + ***************************************************************************/ + + This plugin provides access to openrouteservice API functionalities + (https://openrouteservice.org), developed and + maintained by the openrouteservice team of HeiGIT gGmbH, Germany. By using + this plugin you agree to the ORS terms of service + (https://openrouteservice.org/terms-of-service/). + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +""" +import os + +import yaml + +from ORStools import CONFIG_PATH + + +def read_config(): + """ + Reads config.yml from file and returns the parsed dict. + + :returns: Parsed settings dictionary. + :rtype: dict + """ + with open(CONFIG_PATH) as f: + doc = yaml.safe_load(f) + + return doc + + +def write_config(new_config): + """ + Dumps new config + + :param new_config: new provider settings after altering in dialog. + :type new_config: dict + """ + with open(CONFIG_PATH, 'w') as f: + yaml.safe_dump(new_config, f) + + +def write_env_var(key, value): + """ + Update quota env variables + + :param key: environment variable to update. + :type key: str + + :param value: value for env variable. + :type value: str + """ + os.environ[key] = value + + +def set_active_provider(new_index: int): + """ + Sets the boolean 'active' flag for the provider to True and for all others to False + + :param new_index: index of the new active provider in the config.yml providers list + :type new_index: int + """ + config = read_config() + for i, provider in enumerate(config["providers"]): + provider["active"] = i == new_index + write_config(config) + + +def get_active_provider_index() -> int: + """ + Get the active provider index. + In case the active provider was removed the first provider is set to active. + + :return: active provider index + :rtype: int + """ + providers = read_config()["providers"] + active_list = [p['active'] if 'active' in p.keys() else False for p in providers] + if True in active_list: + return active_list.index(True) + else: + set_active_provider(0) + return 0 From ffbc8dc9e34a41a6f1b3276f3f65df12bf1b9506 Mon Sep 17 00:00:00 2001 From: Amandus Butzer Date: Wed, 9 Mar 2022 15:20:28 +0100 Subject: [PATCH 2/3] Move ors_client to base_processing_algorithm small refactor moving the ors_client and provider logic to the base processing algorithm class. processAlgorithm methods now use the complete signature --- ORStools/proc/base_processing_algorithm.py | 29 ++++++++++++------- ORStools/proc/directions_lines_proc.py | 8 ++--- ORStools/proc/directions_points_layer_proc.py | 8 ++--- .../proc/directions_points_layers_proc.py | 6 ++-- ORStools/proc/isochrones_layer_proc.py | 6 ++-- ORStools/proc/isochrones_point_proc.py | 6 ++-- ORStools/proc/matrix_proc.py | 6 ++-- 7 files changed, 39 insertions(+), 30 deletions(-) diff --git a/ORStools/proc/base_processing_algorithm.py b/ORStools/proc/base_processing_algorithm.py index 392563ea..10f03a5a 100644 --- a/ORStools/proc/base_processing_algorithm.py +++ b/ORStools/proc/base_processing_algorithm.py @@ -66,6 +66,7 @@ def __init__(self): self.IN_AVOID_POLYGONS = "INPUT_AVOID_POLYGONS" self.OUT = 'OUTPUT' self.PARAMETERS = None + self.ORS_CLIENT = None def createInstance(self) -> Any: """ @@ -131,11 +132,11 @@ def profile_parameter(self) -> QgsProcessingParameterEnum: Parameter definition for profile, used in all child classes """ return QgsProcessingParameterEnum( - self.IN_PROFILE, - "Travel mode", - PROFILES, - defaultValue=PROFILES[0] - ) + self.IN_PROFILE, + "Travel mode", + PROFILES, + defaultValue=PROFILES[0] + ) def output_parameter(self) -> QgsProcessingParameterFeatureSink: """ @@ -178,13 +179,17 @@ def option_parameters(self) -> [QgsProcessingParameterDefinition]: ] @staticmethod - def _get_ors_client_from_provider(provider: str, feedback: QgsProcessingFeedback) -> client.Client: + def _get_provider_from_id(provider_id: int) -> dict: + return configmanager.read_config()['providers'][provider_id] + + @staticmethod + def _get_ors_client_from_provider(provider: dict, feedback: QgsProcessingFeedback) -> client.Client: """ Connects client to provider and returns a client instance for requests to the ors API """ - providers = configmanager.read_config()['providers'] - ors_provider = providers[provider] - ors_client = client.Client(ors_provider) + ors_client = client.Client(provider) + if not ors_client: + feedback.reportError("Provider not found in provider configuration.", fatalError=True) ors_client.overQueryLimit.connect(lambda: feedback.reportError("OverQueryLimit: Retrying...")) return ors_client @@ -219,7 +224,7 @@ def initAlgorithm(self, configuration): for param in parameters: if param.name() in ADVANCED_PARAMETERS: if self.GROUP == "Matrix": - param.setFlags(param.flags()| QgsProcessingParameterDefinition.FlagHidden) + param.setFlags(param.flags() | QgsProcessingParameterDefinition.FlagHidden) else: # flags() is a wrapper around an enum of ints for type-safety. # Flags are added by or-ing values, much like the union operator would work @@ -228,3 +233,7 @@ def initAlgorithm(self, configuration): self.addParameter( param ) + + def processAlgorithm(self, parameters, context, feedback, **kwargs): + ors_provider = self._get_provider_from_id(parameters[self.IN_PROVIDER]) + self.ORS_CLIENT = self._get_ors_client_from_provider(ors_provider, feedback) diff --git a/ORStools/proc/directions_lines_proc.py b/ORStools/proc/directions_lines_proc.py index 2857a00a..964b0ae6 100644 --- a/ORStools/proc/directions_lines_proc.py +++ b/ORStools/proc/directions_lines_proc.py @@ -82,8 +82,8 @@ def __init__(self): ) ] - def processAlgorithm(self, parameters, context, feedback): - ors_client = self._get_ors_client_from_provider(parameters[self.IN_PROVIDER], feedback) + def processAlgorithm(self, parameters, context, feedback, **kwargs): + super().processAlgorithm(parameters, context, feedback, **kwargs) profile = dict(enumerate(PROFILES))[parameters[self.IN_PROFILE]] @@ -130,7 +130,7 @@ def processAlgorithm(self, parameters, context, feedback): try: if optimization_mode is not None: params = get_params_optimize(line, profile, optimization_mode) - response = ors_client.request('/optimization', {}, post_json=params) + response = self.ORS_CLIENT.request('/optimization', {}, post_json=params) sink.addFeature(directions_core.get_output_features_optimization( response, @@ -139,7 +139,7 @@ def processAlgorithm(self, parameters, context, feedback): )) else: params = directions_core.build_default_parameters(preference, point_list=line, options=options) - response = ors_client.request('/v2/directions/' + profile + '/geojson', {}, post_json=params) + response = self.ORS_CLIENT.request('/v2/directions/' + profile + '/geojson', {}, post_json=params) sink.addFeature(directions_core.get_output_feature_directions( response, diff --git a/ORStools/proc/directions_points_layer_proc.py b/ORStools/proc/directions_points_layer_proc.py index e4840fdc..9b182e56 100644 --- a/ORStools/proc/directions_points_layer_proc.py +++ b/ORStools/proc/directions_points_layer_proc.py @@ -91,8 +91,8 @@ def __init__(self): ) ] - def processAlgorithm(self, parameters, context, feedback): - ors_client = self._get_ors_client_from_provider(parameters[self.IN_PROVIDER], feedback) + def processAlgorithm(self, parameters, context, feedback, **kwargs): + super().processAlgorithm(parameters, context, feedback, **kwargs) profile = dict(enumerate(PROFILES))[parameters[self.IN_PROFILE]] @@ -160,7 +160,7 @@ def sort(f): return f.id() try: if optimization_mode is not None: params = get_params_optimize(points, profile, optimization_mode) - response = ors_client.request('/optimization', {}, post_json=params) + response = self.ORS_CLIENT.request('/optimization', {}, post_json=params) sink.addFeature(directions_core.get_output_features_optimization( response, @@ -169,7 +169,7 @@ def sort(f): return f.id() )) else: params = directions_core.build_default_parameters(preference, point_list=points, options=options) - response = ors_client.request('/v2/directions/' + profile + '/geojson', {}, post_json=params) + response = self.ORS_CLIENT.request('/v2/directions/' + profile + '/geojson', {}, post_json=params) sink.addFeature(directions_core.get_output_feature_directions( response, diff --git a/ORStools/proc/directions_points_layers_proc.py b/ORStools/proc/directions_points_layers_proc.py index 69fa9db0..a8ee5a50 100644 --- a/ORStools/proc/directions_points_layers_proc.py +++ b/ORStools/proc/directions_points_layers_proc.py @@ -111,8 +111,8 @@ def __init__(self): # TODO: preprocess parameters to options the range cleanup below: # https://www.qgis.org/pyqgis/master/core/Processing/QgsProcessingAlgorithm.html#qgis.core.QgsProcessingAlgorithm.preprocessParameters - def processAlgorithm(self, parameters, context, feedback): - ors_client = self._get_ors_client_from_provider(parameters[self.IN_PROVIDER], feedback) + def processAlgorithm(self, parameters, context, feedback, **kwargs): + super().processAlgorithm(parameters, context, feedback, **kwargs) profile = dict(enumerate(PROFILES))[parameters[self.IN_PROFILE]] @@ -186,7 +186,7 @@ def sort_end(f): return f.id() params = directions_core.build_default_parameters(preference, coordinates=coordinates, options=options) try: - response = ors_client.request('/v2/directions/' + profile + '/geojson', {}, post_json=params) + response = self.ORS_CLIENT.request('/v2/directions/' + profile + '/geojson', {}, post_json=params) except (exceptions.ApiError, exceptions.InvalidKey, exceptions.GenericServerError) as e: diff --git a/ORStools/proc/isochrones_layer_proc.py b/ORStools/proc/isochrones_layer_proc.py index be3bec03..21d9669b 100644 --- a/ORStools/proc/isochrones_layer_proc.py +++ b/ORStools/proc/isochrones_layer_proc.py @@ -94,8 +94,8 @@ def __init__(self): # TODO: preprocess parameters to options the range cleanup below: # https://www.qgis.org/pyqgis/master/core/Processing/QgsProcessingAlgorithm.html#qgis.core.QgsProcessingAlgorithm.prepareAlgorithm - def processAlgorithm(self, parameters, context, feedback): - ors_client = self._get_ors_client_from_provider(parameters[self.IN_PROVIDER], feedback) + def processAlgorithm(self, parameters, context, feedback, **kwargs): + super().processAlgorithm(parameters, context, feedback, **kwargs) profile = dict(enumerate(PROFILES))[parameters[self.IN_PROFILE]] dimension = dict(enumerate(DIMENSIONS))[parameters[self.IN_METRIC]] @@ -152,7 +152,7 @@ def processAlgorithm(self, parameters, context, feedback): # If feature causes error, report and continue with next try: # Populate features from response - response = ors_client.request('/v2/isochrones/' + profile, {}, post_json=params) + response = self.ORS_CLIENT.request('/v2/isochrones/' + profile, {}, post_json=params) for isochrone in self.isochrones.get_features(response, params['id']): sink.addFeature(isochrone) diff --git a/ORStools/proc/isochrones_point_proc.py b/ORStools/proc/isochrones_point_proc.py index eedebc9d..37d24c31 100644 --- a/ORStools/proc/isochrones_point_proc.py +++ b/ORStools/proc/isochrones_point_proc.py @@ -79,8 +79,8 @@ def __init__(self): # TODO: preprocess parameters to options the range cleanup below: # https://www.qgis.org/pyqgis/master/core/Processing/QgsProcessingAlgorithm.html#qgis.core.QgsProcessingAlgorithm.preprocessParameters - def processAlgorithm(self, parameters, context, feedback): - ors_client = self._get_ors_client_from_provider(parameters[self.IN_PROVIDER], feedback) + def processAlgorithm(self, parameters, context, feedback, **kwargs): + super().processAlgorithm(parameters, context, feedback, **kwargs) profile = dict(enumerate(PROFILES))[parameters[self.IN_PROFILE]] dimension = dict(enumerate(DIMENSIONS))[parameters[self.IN_METRIC]] @@ -113,7 +113,7 @@ def processAlgorithm(self, parameters, context, feedback): self.crs_out) try: - response = ors_client.request('/v2/isochrones/' + profile, {}, post_json=params) + response = self.ORS_CLIENT.request('/v2/isochrones/' + profile, {}, post_json=params) # Populate features from response for isochrone in self.isochrones.get_features(response, params['id']): diff --git a/ORStools/proc/matrix_proc.py b/ORStools/proc/matrix_proc.py index e95956d9..67c1609d 100644 --- a/ORStools/proc/matrix_proc.py +++ b/ORStools/proc/matrix_proc.py @@ -81,8 +81,8 @@ def __init__(self): ), ] - def processAlgorithm(self, parameters, context, feedback): - ors_client = self._get_ors_client_from_provider(parameters[self.IN_PROVIDER], feedback) + def processAlgorithm(self, parameters, context, feedback, **kwargs): + super().processAlgorithm(parameters, context, feedback, **kwargs) # Get profile value profile = dict(enumerate(PROFILES))[parameters[self.IN_PROFILE]] @@ -162,7 +162,7 @@ def processAlgorithm(self, parameters, context, feedback): # Make request and catch ApiError try: - response = ors_client.request('/v2/matrix/' + profile, {}, post_json=params) + response = self.ORS_CLIENT.request('/v2/matrix/' + profile, {}, post_json=params) except (exceptions.ApiError, exceptions.InvalidKey, From 81d65deb79d1392c4ba3385c5e803739163d2334 Mon Sep 17 00:00:00 2001 From: Amandus Butzer Date: Wed, 9 Mar 2022 15:54:22 +0100 Subject: [PATCH 3/3] Get and set active provider in processing algorithms to use the active provider in processing algorithms by default, it is read from the storage. However, the defaultValue for QgsProcessingParameterEnum is not working, which is why the options of the enum are reordered to show the active provider on top. As the index of the provider selected in the Enum does not refer to the same provider in the config anymore, it has to be resolved properly again, when using the provider to generate the ors_client. This can lead to problems (using the wrong provider) when the active provider is changed, after a processing algorithm window was opened, due to how the algorithm parameters are initialized. --- CHANGELOG.md | 1 + ORStools/proc/base_processing_algorithm.py | 27 ++++++++++++++++++---- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5b22d0a..6fe8ee2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ RELEASING: ### Added - active provider storage, persisted between QGIS sessions ([#168](https://github.com/GIScience/orstools-qgis-plugin/pull/166)) +- provider storage to processing scripts ## [1.5.2] - 2022-01-20 diff --git a/ORStools/proc/base_processing_algorithm.py b/ORStools/proc/base_processing_algorithm.py index 10f03a5a..629df250 100644 --- a/ORStools/proc/base_processing_algorithm.py +++ b/ORStools/proc/base_processing_algorithm.py @@ -42,6 +42,7 @@ from ORStools import RESOURCE_PREFIX, __help__ from ORStools.utils import configmanager +from ORStools.gui.ORStoolsDialog import on_change_save_provider from ..common import client, PROFILES, AVOID_BORDERS, AVOID_FEATURES, ADVANCED_PARAMETERS from ..utils.processing import read_help_file from ..gui.directions_gui import _get_avoid_polygons @@ -120,11 +121,15 @@ def provider_parameter(self) -> QgsProcessingParameterEnum: Parameter definition for provider, used in all child classes """ providers = [provider['name'] for provider in configmanager.read_config()['providers']] + active_provider = providers[configmanager.get_active_provider_index()] + # reorders enum options so the active provider is shown at the top which + # setting the defaultValue of the QgsProcessingParameterEnum here does not work + providers.sort(key=lambda x: x != active_provider) return QgsProcessingParameterEnum( self.IN_PROVIDER, "Provider", providers, - defaultValue=providers[0] + defaultValue=active_provider ) def profile_parameter(self) -> QgsProcessingParameterEnum: @@ -178,9 +183,23 @@ def option_parameters(self) -> [QgsProcessingParameterDefinition]: ) ] - @staticmethod - def _get_provider_from_id(provider_id: int) -> dict: - return configmanager.read_config()['providers'][provider_id] + def _get_provider_from_id(self, provider_id: int) -> dict: + """ + Resolve the index from the reordered QgsProcessingParameterEnum options to the actual provider + dict from the config storage. + + Also sets the selected provider to active. + + :param provider_id: the provider ID in the dropdown of the processing script + :return: + """ + provider_name = self.provider_parameter().options()[provider_id] + providers = configmanager.read_config()['providers'] + + ors_provider = next((p for p in providers if p['name'] == provider_name), None) + on_change_save_provider(providers.index(ors_provider)) + + return ors_provider @staticmethod def _get_ors_client_from_provider(provider: dict, feedback: QgsProcessingFeedback) -> client.Client: