diff --git a/CHANGELOG.md b/CHANGELOG.md index d769ee0d..6fe8ee2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,9 +38,15 @@ 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)) +- provider storage to processing scripts + ## [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/proc/base_processing_algorithm.py b/ORStools/proc/base_processing_algorithm.py index 392563ea..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 @@ -66,6 +67,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: """ @@ -119,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: @@ -131,11 +137,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: """ @@ -177,14 +183,32 @@ def option_parameters(self) -> [QgsProcessingParameterDefinition]: ) ] + 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: str, feedback: QgsProcessingFeedback) -> client.Client: + 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 +243,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 +252,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, 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