diff --git a/ORStools/ORStoolsPlugin.py b/ORStools/ORStoolsPlugin.py index 2156bf8a..299ec0bd 100644 --- a/ORStools/ORStoolsPlugin.py +++ b/ORStools/ORStoolsPlugin.py @@ -33,7 +33,8 @@ import os.path from .gui import ORStoolsDialog -from .proc import provider, ENDPOINTS, DEFAULT_SETTINGS +from .proc import provider, ENDPOINTS, DEFAULT_SETTINGS, PROFILES +from .utils import configmanager class ORStools: @@ -51,6 +52,7 @@ def __init__(self, iface: QgisInterface) -> None: """ self.dialog = ORStoolsDialog.ORStoolsDialogMain(iface) self.provider = provider.ORStoolsProvider() + self.settings_keys = ["ENV_VARS", "base_url", "key", "name", "endpoints", "profiles"] # initialize plugin directory self.plugin_dir = os.path.dirname(__file__) @@ -73,7 +75,7 @@ def __init__(self, iface: QgisInterface) -> None: except TypeError: pass - self.add_default_provider_to_settings() + self.update_settings() def initGui(self) -> None: """Create the menu entries and toolbar icons inside the QGIS GUI.""" @@ -86,22 +88,21 @@ def unload(self) -> None: QgsApplication.processingRegistry().removeProvider(self.provider) self.dialog.unload() - def add_default_provider_to_settings(self): - s = QgsSettings() - settings = s.value("ORStools/config") + def update_settings(self): + settings = configmanager.read_config() - settings_keys = ["ENV_VARS", "base_url", "key", "name", "endpoints"] + if settings is not None and settings != {}: + endpoints = settings.get("endpoints", ENDPOINTS) + profiles = settings.get("profiles", PROFILES) - # Add any new settings here for backwards compatibility - if settings: changed = False for i, prov in enumerate(settings["providers"]): - if any([i not in prov for i in settings_keys]): + if any([key not in prov for key in self.settings_keys]): changed = True - # Add here, like the endpoints - prov["endpoints"] = ENDPOINTS + prov["endpoints"] = endpoints + prov["profiles"] = profiles settings["providers"][i] = prov if changed: - s.setValue("ORStools/config", settings) + configmanager.write_config(settings) else: - s.setValue("ORStools/config", DEFAULT_SETTINGS) + configmanager.write_config(DEFAULT_SETTINGS) diff --git a/ORStools/gui/ORStoolsDialog.py b/ORStools/gui/ORStoolsDialog.py index 276b60a1..c33de6fc 100644 --- a/ORStools/gui/ORStoolsDialog.py +++ b/ORStools/gui/ORStoolsDialog.py @@ -100,7 +100,6 @@ __help__, ) from ORStools.common import ( - PROFILES, PREFERENCES, ) from ORStools.utils import maptools, configmanager, transform, gui, exceptions @@ -118,6 +117,8 @@ def on_config_click(parent): """ config_dlg = ORStoolsDialogConfigMain(parent=parent) config_dlg.exec() + if type(parent) is ORStoolsDialog: + parent.refresh_profiles() def on_help_click() -> None: @@ -408,7 +409,8 @@ def __init__(self, iface: QgisInterface, parent=None) -> None: os.environ["ORS_REMAINING"] = "None" # Populate combo boxes - self.routing_travel_combo.addItems(PROFILES) + self.provider_combo.currentIndexChanged.connect(self.refresh_profiles) + self.refresh_profiles() self.routing_preference_combo.addItems(PREFERENCES) # Change OK and Cancel button names @@ -592,6 +594,13 @@ def add_geocoded_item(self, coordinates, lineEdit) -> None: self._reindex_list_items() self.geocoded = True + def refresh_profiles(self) -> None: + """Refreshes the profiles in the routing travel combo box when a provider is selected.""" + self.routing_travel_combo.clear() + index = self.provider_combo.currentIndex() + provider = configmanager.read_config()["providers"][index] + self.routing_travel_combo.addItems(provider["profiles"]) + def _save_vertices_to_layer(self) -> None: """Saves the vertices list to a temp layer""" items = [ diff --git a/ORStools/gui/ORStoolsDialogConfig.py b/ORStools/gui/ORStoolsDialogConfig.py index 8dfe38dd..940d6392 100644 --- a/ORStools/gui/ORStoolsDialogConfig.py +++ b/ORStools/gui/ORStoolsDialogConfig.py @@ -27,20 +27,31 @@ ***************************************************************************/ """ -from qgis.gui import QgsCollapsibleGroupBox +import json + +from qgis.PyQt.QtNetwork import QNetworkRequest +from qgis.core import QgsBlockingNetworkRequest +from qgis.gui import QgsCollapsibleGroupBox, QgsNewNameDialog from qgis.PyQt import QtWidgets, uic -from qgis.PyQt.QtCore import QMetaObject +from qgis.PyQt.QtCore import QMetaObject, QUrl from qgis.PyQt.QtWidgets import ( QDialog, QInputDialog, QLineEdit, QDialogButtonBox, + QMessageBox, + QWidget, + QListWidget, + QVBoxLayout, + QHBoxLayout, + QPushButton, ) from qgis.PyQt.QtGui import QIntValidator from ORStools.utils import configmanager, gui from ..proc import ENDPOINTS, DEFAULT_SETTINGS +from ..common import PROFILES CONFIG_WIDGET, _ = uic.loadUiType(gui.GuiUtils.get_ui_file_path("ORStoolsDialogConfigUI.ui")) @@ -74,7 +85,10 @@ def accept(self) -> None: collapsible_boxes = self.providers.findChildren(QgsCollapsibleGroupBox) collapsible_boxes = [ - i for i in collapsible_boxes if "_provider_endpoints" not in i.objectName() + i + for i in collapsible_boxes + if "_provider_endpoints" not in i.objectName() + and "_provider_profiles" not in i.objectName() ] for idx, box in enumerate(collapsible_boxes): current_provider = self.temp_config["providers"][idx] @@ -117,6 +131,12 @@ def accept(self) -> None: QtWidgets.QLineEdit, box.title() + "_snapping_endpoint" ).text(), } + profile_box = box.findChild(QgsCollapsibleGroupBox, f"{box.title()}_provider_profiles") + + list_widget = profile_box.findChild(QListWidget) + current_provider["profiles"] = [ + list_widget.item(i).text() for i in range(list_widget.count()) + ] configmanager.write_config(self.temp_config) self.close() @@ -148,6 +168,7 @@ def _build_ui(self) -> None: provider_entry["key"], provider_entry["timeout"], provider_entry["endpoints"], + provider_entry["profiles"], new=False, ) @@ -167,7 +188,9 @@ def _add_provider(self) -> None: self, self.tr("New ORS provider"), self.tr("Enter a name for the provider") ) if ok: - self._add_box(provider_name, "http://localhost:8082/ors", "", 60, ENDPOINTS, new=True) + self._add_box( + provider_name, "http://localhost:8082/ors", "", 60, ENDPOINTS, PROFILES, new=True + ) def _remove_provider(self) -> None: """Remove list of providers from list.""" @@ -197,15 +220,57 @@ def _collapse_boxes(self) -> None: for box in collapsible_boxes: box.setCollapsed(True) + def _reset_all_providers(self) -> None: + """Reset all providers.""" + + msg_box = QMessageBox() + msg_box.setIcon(QMessageBox.Warning) + msg_box.setWindowTitle("Confirm Reset") + msg_box.setText( + "Are you sure you want to delete all providers? This action cannot be undone." + ) + msg_box.setStandardButtons(QMessageBox.Yes | QMessageBox.No) + + result = msg_box.exec() + if result == QMessageBox.Yes: + for box_remove in self.providers.findChildren(QWidget): + if box_remove.objectName() in ["_provider_endpoints", "_provider_profiles"]: + continue + self.verticalLayout.removeWidget(box_remove) + box_remove.setParent(None) + box_remove.deleteLater() + + configmanager.write_config(DEFAULT_SETTINGS) + + self.temp_config = configmanager.read_config() + self._build_ui() + + else: + pass + def _add_box( - self, name: str, url: str, key: str, timeout: int, endpoints: dict, new: bool = False + self, + name: str, + url: str, + key: str, + timeout: int, + endpoints: dict, + profiles: dict, + new: bool = False, ) -> None: """ Adds a provider box to the QWidget layout and self.temp_config. """ if new: self.temp_config["providers"].append( - dict(name=name, base_url=url, key=key, timeout=timeout, endpoints=endpoints) + dict( + name=name, + base_url=url, + key=key, + timeout=timeout, + endpoints=endpoints, + profiles=profiles, + ) ) provider = QgsCollapsibleGroupBox(self.providers) @@ -266,10 +331,52 @@ def _add_box( endpoint_lineedit.setObjectName(f"{name}_{endpoint_name}_endpoint") endpoint_layout.addWidget(endpoint_lineedit, row, 1, 1, 3) - row += 1 - # Add reset buttons at the bottom + reset_endpoints_button = QtWidgets.QPushButton(self.tr("Reset Endpoints"), provider) + reset_endpoints_button.setObjectName(name + "_reset_endpoints_button") + reset_endpoints_button.clicked.connect(self._reset_endpoints) + endpoint_layout.addWidget(reset_endpoints_button) + + # Profile Section + profile_box = QgsCollapsibleGroupBox(provider) + profile_box.setObjectName(name + "_provider_profiles") + profile_box.setTitle(self.tr("Profiles")) + profile_layout = QHBoxLayout(profile_box) + + list_widget_profiles = QListWidget(profile_box) + list_widget_profiles.addItems(profiles) + profile_layout.addWidget(list_widget_profiles) + + button_layout = QVBoxLayout() + add_profile_button = QPushButton(self.tr("+"), profile_box) + remove_profile_button = QPushButton(self.tr("-"), profile_box) + load_profiles_button = QPushButton(self.tr("Load profiles"), profile_box) + restore_defaults_button = QPushButton(self.tr("Restore defaults"), profile_box) + + add_profile_button.clicked.connect( + lambda: self.add_profile_button_clicked(add_profile_button) + ) + remove_profile_button.clicked.connect( + lambda: self.remove_profile_button_clicked(remove_profile_button) + ) + load_profiles_button.clicked.connect( + lambda: self.load_profiles_button_clicked(load_profiles_button) + ) + restore_defaults_button.clicked.connect( + lambda: self.restore_defaults_button_clicked(restore_defaults_button) + ) + + button_layout.addWidget(add_profile_button) + button_layout.addWidget(remove_profile_button) + button_layout.addWidget(load_profiles_button) + button_layout.addWidget(restore_defaults_button) + + profile_layout.addLayout(button_layout) + + gridLayout_3.addWidget(profile_box, 7, 0, 1, 4) + + # 6. Reset buttons section button_layout = QtWidgets.QHBoxLayout() reset_url_button = QtWidgets.QPushButton(self.tr("Reset URL"), provider) @@ -279,15 +386,63 @@ def _add_box( ) button_layout.addWidget(reset_url_button) - reset_endpoints_button = QtWidgets.QPushButton(self.tr("Reset Endpoints"), provider) - reset_endpoints_button.setObjectName(name + "_reset_endpoints_button") - reset_endpoints_button.clicked.connect(self._reset_endpoints) - button_layout.addWidget(reset_endpoints_button) - - gridLayout_3.addLayout(button_layout, 7, 0, 1, 4) + gridLayout_3.addLayout(button_layout, 8, 0, 1, 4) # (8, 0–3) self.verticalLayout.addWidget(provider) + def add_profile_button_clicked(self, button: QPushButton) -> None: + dlg = QgsNewNameDialog("Enter profile name", "New Profile") + list_widget = button.parent().findChild(QListWidget) + if dlg.exec_(): + profile_name = dlg.name() + if profile_name: + list_widget.addItem(profile_name) + + def remove_profile_button_clicked(self, button: QPushButton) -> None: + list_widget = button.parent().findChild(QListWidget) + selected = list_widget.selectedItems() + if selected: + for item in selected: + list_widget.takeItem(list_widget.row(item)) + else: + list_widget.takeItem(0) + + def load_profiles_button_clicked(self, button: QPushButton) -> None: + list_widget = button.parent().findChild(QListWidget) + grand_parent = button.parent().parent() + base_url = None + for child in grand_parent.findChildren(QLineEdit): + if "_base_url_text" in child.objectName(): + base_url = child.text() + + url = f"{base_url}/v2/status" + + if "api.openrouteservice.org" in url: + QMessageBox.warning( + self, + "Load profiles not possible", + "Load profiles not possible, please use 'Restore Defaults' for the openrouteservice live API", + ) + return + + request = QgsBlockingNetworkRequest() + print(url) + error_code = request.get(QNetworkRequest(QUrl(url))) + + if error_code == QgsBlockingNetworkRequest.ErrorCode.NoError: + reply = request.reply() + content = json.loads(reply.content().data().decode("utf-8")) + list_widget.addItems([i for i in content["profiles"].keys()]) + else: + QMessageBox.warning( + self, "Unable to load profiles", "There was an error loading the profiles." + ) + + def restore_defaults_button_clicked(self, button: QPushButton) -> None: + list_widget = button.parent().findChild(QListWidget) + list_widget.clear() + list_widget.addItems(PROFILES) + def _reset_endpoints(self) -> None: """Resets the endpoints to their original values.""" for line_edit_remove in self.providers.findChildren(QLineEdit): diff --git a/ORStools/proc/__init__.py b/ORStools/proc/__init__.py index 87e79c3f..728aa49c 100644 --- a/ORStools/proc/__init__.py +++ b/ORStools/proc/__init__.py @@ -27,6 +27,8 @@ ***************************************************************************/ """ +from ORStools.common import PROFILES + ENDPOINTS = { "directions": "directions", "isochrones": "isochrones", @@ -48,6 +50,7 @@ "name": "openrouteservice", "timeout": 60, "endpoints": ENDPOINTS, + "profiles": PROFILES, } ] } diff --git a/ORStools/proc/base_processing_algorithm.py b/ORStools/proc/base_processing_algorithm.py index 5a6ece34..4d81f937 100644 --- a/ORStools/proc/base_processing_algorithm.py +++ b/ORStools/proc/base_processing_algorithm.py @@ -48,7 +48,7 @@ from ORStools import RESOURCE_PREFIX, __help__ from ORStools.utils import configmanager -from ..common import client, PROFILES, AVOID_BORDERS, AVOID_FEATURES, ADVANCED_PARAMETERS +from ..common import client, AVOID_BORDERS, AVOID_FEATURES, ADVANCED_PARAMETERS from ..utils.processing import read_help_file from ..gui.directions_gui import _get_avoid_polygons @@ -74,6 +74,10 @@ def __init__(self) -> None: self.OUT_NAME = "ORSTOOLS_OUTPUT" self.PARAMETERS = None + self.providers = configmanager.read_config()["providers"] + profiles_list = [provider["profiles"] for provider in self.providers] + self.profiles = list(set(element for sublist in profiles_list for element in sublist)) + def createInstance(self) -> Any: """ Returns instance of any child class @@ -120,7 +124,7 @@ def provider_parameter(self) -> QgsProcessingParameterEnum: """ Parameter definition for provider, used in all child classes """ - providers = [provider["name"] for provider in configmanager.read_config()["providers"]] + providers = [provider["name"] for provider in self.providers] parameter = QgsProcessingParameterEnum( self.IN_PROVIDER, self.tr("Provider", "ORSBaseProcessingAlgorithm"), @@ -136,11 +140,12 @@ def profile_parameter(self) -> QgsProcessingParameterEnum: """ Parameter definition for profile, used in all child classes """ + parameter = QgsProcessingParameterEnum( self.IN_PROFILE, self.tr("Travel mode", "ORSBaseProcessingAlgorithm"), - PROFILES, - defaultValue=PROFILES[0], + self.profiles, + defaultValue=self.profiles[0], ) self.setToolTip(parameter, self.tr("Select a mode of travel.")) diff --git a/ORStools/proc/directions_lines_proc.py b/ORStools/proc/directions_lines_proc.py index 8b697c22..43ab59b3 100644 --- a/ORStools/proc/directions_lines_proc.py +++ b/ORStools/proc/directions_lines_proc.py @@ -50,7 +50,7 @@ ) from qgis.PyQt.QtGui import QIcon -from ORStools.common import directions_core, PROFILES, PREFERENCES, OPTIMIZATION_MODES, EXTRA_INFOS +from ORStools.common import directions_core, PREFERENCES, OPTIMIZATION_MODES, EXTRA_INFOS from ORStools.utils import transform, exceptions, logger from .base_processing_algorithm import ORSBaseProcessingAlgorithm from ..utils.processing import get_params_optimize @@ -148,7 +148,7 @@ def processAlgorithm( ) -> Dict[str, str]: ors_client = self.get_client(parameters, context, feedback) - profile = dict(enumerate(PROFILES))[parameters[self.IN_PROFILE]] + profile = dict(enumerate(self.profiles))[parameters[self.IN_PROFILE]] preference = dict(enumerate(PREFERENCES))[parameters[self.IN_PREFERENCE]] @@ -263,7 +263,16 @@ def processAlgorithm( ) ) except (exceptions.ApiError, exceptions.InvalidKey, exceptions.GenericServerError) as e: - msg = f"Feature ID {num} caused a {e.__class__.__name__}:\n{str(e)}" + if ( + isinstance(e, exceptions.ApiError) + and "Parameter 'profile' has incorrect value" in e.message + ): + provider = self.providers[parameters[self.IN_PROVIDER]]["name"] + msg = self.tr( + f'The selected profile "{profile}" is not available in the chosen provider "{provider}"' + ) + else: + msg = f"Feature ID {num} caused a {e.__class__.__name__}:\n{str(e)}" feedback.reportError(msg) logger.log(msg) continue diff --git a/ORStools/proc/directions_points_layer_proc.py b/ORStools/proc/directions_points_layer_proc.py index 62a1fc2c..832e1545 100644 --- a/ORStools/proc/directions_points_layer_proc.py +++ b/ORStools/proc/directions_points_layer_proc.py @@ -49,7 +49,7 @@ QgsProcessingFeedback, ) -from ORStools.common import directions_core, PROFILES, PREFERENCES, OPTIMIZATION_MODES, EXTRA_INFOS +from ORStools.common import directions_core, PREFERENCES, OPTIMIZATION_MODES, EXTRA_INFOS from ORStools.utils import transform, exceptions, logger from .base_processing_algorithm import ORSBaseProcessingAlgorithm from ..utils.gui import GuiUtils @@ -161,7 +161,7 @@ def processAlgorithm( ) -> Dict[str, str]: ors_client = self.get_client(parameters, context, feedback) - profile = dict(enumerate(PROFILES))[parameters[self.IN_PROFILE]] + profile = dict(enumerate(self.profiles))[parameters[self.IN_PROFILE]] preference = dict(enumerate(PREFERENCES))[parameters[self.IN_PREFERENCE]] @@ -310,7 +310,16 @@ def sort(f): ) ) except (exceptions.ApiError, exceptions.InvalidKey, exceptions.GenericServerError) as e: - msg = f"Feature ID {from_value} caused a {e.__class__.__name__}:\n{str(e)}" + if ( + isinstance(e, exceptions.ApiError) + and "Parameter 'profile' has incorrect value" in e.message + ): + provider = self.providers[parameters[self.IN_PROVIDER]]["name"] + msg = self.tr( + f'The selected profile "{profile}" is not available in the chosen provider "{provider}"' + ) + else: + msg = f"Feature ID {from_value} caused a {e.__class__.__name__}:\n{str(e)}" feedback.reportError(msg) logger.log(msg) continue diff --git a/ORStools/proc/directions_points_layers_proc.py b/ORStools/proc/directions_points_layers_proc.py index 34678b3e..e5c90001 100644 --- a/ORStools/proc/directions_points_layers_proc.py +++ b/ORStools/proc/directions_points_layers_proc.py @@ -46,7 +46,7 @@ QgsProcessingFeedback, ) -from ORStools.common import directions_core, PROFILES, PREFERENCES, EXTRA_INFOS +from ORStools.common import directions_core, PREFERENCES, EXTRA_INFOS from ORStools.utils import transform, exceptions, logger from .base_processing_algorithm import ORSBaseProcessingAlgorithm @@ -189,7 +189,7 @@ def processAlgorithm( ) -> Dict[str, str]: ors_client = self.get_client(parameters, context, feedback) - profile = dict(enumerate(PROFILES))[parameters[self.IN_PROFILE]] + profile = dict(enumerate(self.profiles))[parameters[self.IN_PROFILE]] preference = dict(enumerate(PREFERENCES))[parameters[self.IN_PREFERENCE]] @@ -284,9 +284,21 @@ def sort_end(f): f"/v2/{endpoint}/{profile}/geojson", {}, post_json=params ) except (exceptions.ApiError, exceptions.InvalidKey, exceptions.GenericServerError) as e: - msg = f"Route from {values[0]} to {values[1]} caused a {e.__class__.__name__}:\n{str(e)}" - feedback.reportError(msg) - logger.log(msg) + if ( + isinstance(e, exceptions.ApiError) + and "Parameter 'profile' has incorrect value" in e.message + ): + provider = self.providers[parameters[self.IN_PROVIDER]]["name"] + msg = self.tr( + f'The selected profile "{profile}" is not available in the chosen provider "{provider}"' + ) + feedback.reportError(msg) + logger.log(msg) + break + else: + msg = f"Route from {values[0]} to {values[1]} caused a {e.__class__.__name__}:\n{str(e)}" + feedback.reportError(msg) + logger.log(msg) continue if extra_info: diff --git a/ORStools/proc/export_proc.py b/ORStools/proc/export_proc.py index 17536c71..158383ff 100644 --- a/ORStools/proc/export_proc.py +++ b/ORStools/proc/export_proc.py @@ -49,7 +49,6 @@ from ..utils.wrapper import create_qgs_field -from ORStools.common import PROFILES from ORStools.utils import exceptions, logger from .base_processing_algorithm import ORSBaseProcessingAlgorithm @@ -83,7 +82,7 @@ def processAlgorithm( ors_client = self.get_client(parameters, context, feedback) # Get profile value - profile = dict(enumerate(PROFILES))[parameters[self.IN_PROFILE]] + profile = dict(enumerate(self.profiles))[parameters[self.IN_PROFILE]] target_crs = QgsCoordinateReferenceSystem("EPSG:4326") rect = self.parameterAsExtent(parameters, self.IN_EXPORT, context, crs=target_crs) @@ -155,7 +154,16 @@ def processAlgorithm( sink_point.addFeature(point_feat) except (exceptions.ApiError, exceptions.InvalidKey, exceptions.GenericServerError) as e: - msg = f"{e.__class__.__name__}: {str(e)}" + if ( + isinstance(e, exceptions.ApiError) + and "Parameter 'profile' has incorrect value" in e.message + ): + provider = self.providers[parameters[self.IN_PROVIDER]]["name"] + msg = self.tr( + f'The selected profile "{profile}" is not available in the chosen provider "{provider}"' + ) + else: + msg = f"{e.__class__.__name__}: {str(e)}" feedback.reportError(msg) logger.log(msg) diff --git a/ORStools/proc/isochrones_layer_proc.py b/ORStools/proc/isochrones_layer_proc.py index 875ce9ec..59767d1f 100644 --- a/ORStools/proc/isochrones_layer_proc.py +++ b/ORStools/proc/isochrones_layer_proc.py @@ -45,7 +45,7 @@ QgsProcessingFeedback, ) -from ORStools.common import isochrones_core, PROFILES, DIMENSIONS, LOCATION_TYPES +from ORStools.common import isochrones_core, DIMENSIONS, LOCATION_TYPES from ORStools.proc.base_processing_algorithm import ORSBaseProcessingAlgorithm from ORStools.utils import transform, exceptions, logger from ORStools.utils.gui import GuiUtils @@ -151,7 +151,7 @@ def processAlgorithm( ) -> Dict[str, str]: ors_client = self.get_client(parameters, context, feedback) - profile = dict(enumerate(PROFILES))[parameters[self.IN_PROFILE]] + profile = dict(enumerate(self.profiles))[parameters[self.IN_PROFILE]] dimension = dict(enumerate(DIMENSIONS))[parameters[self.IN_METRIC]] location_type = dict(enumerate(LOCATION_TYPES))[parameters[self.LOCATION_TYPE]] @@ -234,9 +234,21 @@ def processAlgorithm( sink.addFeature(isochrone) except (exceptions.ApiError, exceptions.InvalidKey, exceptions.GenericServerError) as e: - msg = f"Feature ID {params['id']} caused a {e.__class__.__name__}:\n{str(e)}" - feedback.reportError(msg) - logger.log(msg, 2) + if ( + isinstance(e, exceptions.ApiError) + and "Parameter 'profile' has incorrect value" in e.message + ): + provider = self.providers[parameters[self.IN_PROVIDER]]["name"] + msg = self.tr( + f'The selected profile "{profile}" is not available in the chosen provider "{provider}"' + ) + feedback.reportError(msg) + logger.log(msg, 2) + break + else: + msg = f"Feature ID {params['id']} caused a {e.__class__.__name__}:\n{str(e)}" + feedback.reportError(msg) + logger.log(msg, 2) continue feedback.setProgress(int(100.0 / source.featureCount() * num)) diff --git a/ORStools/proc/isochrones_point_proc.py b/ORStools/proc/isochrones_point_proc.py index f9feb333..7cdb86c2 100644 --- a/ORStools/proc/isochrones_point_proc.py +++ b/ORStools/proc/isochrones_point_proc.py @@ -42,7 +42,7 @@ QgsProcessingFeedback, ) -from ORStools.common import isochrones_core, PROFILES, DIMENSIONS, LOCATION_TYPES +from ORStools.common import isochrones_core, DIMENSIONS, LOCATION_TYPES from ORStools.utils import exceptions, logger from .base_processing_algorithm import ORSBaseProcessingAlgorithm from ..utils.gui import GuiUtils @@ -129,7 +129,7 @@ def processAlgorithm( ) -> Dict[str, str]: ors_client = self.get_client(parameters, context, feedback) - profile = dict(enumerate(PROFILES))[parameters[self.IN_PROFILE]] + profile = dict(enumerate(self.profiles))[parameters[self.IN_PROFILE]] dimension = dict(enumerate(DIMENSIONS))[parameters[self.IN_METRIC]] location_type = dict(enumerate(LOCATION_TYPES))[parameters[self.LOCATION_TYPE]] @@ -184,7 +184,16 @@ def processAlgorithm( sink.addFeature(isochrone) except (exceptions.ApiError, exceptions.InvalidKey, exceptions.GenericServerError) as e: - msg = f"Feature ID {params['id']} caused a {e.__class__.__name__}:\n{str(e)}" + if ( + isinstance(e, exceptions.ApiError) + and "Parameter 'profile' has incorrect value" in e.message + ): + provider = self.providers[parameters[self.IN_PROVIDER]]["name"] + msg = self.tr( + f'The selected profile "{profile}" is not available in the chosen provider "{provider}"' + ) + else: + msg = f"Feature ID {params['id']} caused a {e.__class__.__name__}:\n{str(e)}" feedback.reportError(msg) logger.log(msg, 2) diff --git a/ORStools/proc/matrix_proc.py b/ORStools/proc/matrix_proc.py index cc9da16b..7502156a 100644 --- a/ORStools/proc/matrix_proc.py +++ b/ORStools/proc/matrix_proc.py @@ -46,7 +46,6 @@ from qgis.PyQt.QtCore import QMetaType -from ORStools.common import PROFILES from ORStools.utils import transform, exceptions, logger from .base_processing_algorithm import ORSBaseProcessingAlgorithm from ..utils.gui import GuiUtils @@ -113,7 +112,7 @@ def processAlgorithm( ors_client = self.get_client(parameters, context, feedback) # Get profile value - profile = dict(enumerate(PROFILES))[parameters[self.IN_PROFILE]] + profile = dict(enumerate(self.profiles))[parameters[self.IN_PROFILE]] # TODO: enable once core matrix is available # options = self.parseOptions(parameters, context) @@ -201,41 +200,52 @@ def processAlgorithm( f"/v2/{endpoint}/{profile}", {}, post_json=params ) - except (exceptions.ApiError, exceptions.InvalidKey, exceptions.GenericServerError) as e: - msg = f"{e.__class__.__name__}: {str(e)}" - feedback.reportError(msg) - logger.log(msg) + (sink, dest_id) = self.parameterAsSink( + parameters, self.OUT, context, sink_fields, QgsWkbTypes.Type.NoGeometry + ) - (sink, dest_id) = self.parameterAsSink( - parameters, self.OUT, context, sink_fields, QgsWkbTypes.Type.NoGeometry - ) + sources_attributes = [ + feat.attribute(source_field_name) if source_field_name else feat.id() + for feat in sources_features + ] + destinations_attributes = [ + feat.attribute(destination_field_name) if destination_field_name else feat.id() + for feat in destination_features + ] - sources_attributes = [ - feat.attribute(source_field_name) if source_field_name else feat.id() - for feat in sources_features - ] - destinations_attributes = [ - feat.attribute(destination_field_name) if destination_field_name else feat.id() - for feat in destination_features - ] + for s, source in enumerate(sources_attributes): + for d, destination in enumerate(destinations_attributes): + duration = response["durations"][s][d] + distance = response["distances"][s][d] + feat = QgsFeature() + feat.setAttributes( + [ + source, + destination, + duration / 3600 if duration is not None else None, + distance / 1000 if distance is not None else None, + ] + ) - for s, source in enumerate(sources_attributes): - for d, destination in enumerate(destinations_attributes): - duration = response["durations"][s][d] - distance = response["distances"][s][d] - feat = QgsFeature() - feat.setAttributes( - [ - source, - destination, - duration / 3600 if duration is not None else None, - distance / 1000 if distance is not None else None, - ] - ) + sink.addFeature(feat) - sink.addFeature(feat) + return {self.OUT: dest_id} + + except (exceptions.ApiError, exceptions.InvalidKey, exceptions.GenericServerError) as e: + if ( + isinstance(e, exceptions.ApiError) + and "Parameter 'profile' has incorrect value" in e.message + ): + provider = self.providers[parameters[self.IN_PROVIDER]]["name"] + msg = self.tr( + f'The selected profile "{profile}" is not available in the chosen provider "{provider}"' + ) + else: + msg = f"Feature ID {params['id']} caused a {e.__class__.__name__}:\n{str(e)}" + feedback.reportError(msg) + logger.log(msg) - return {self.OUT: dest_id} + return {self.OUT: ""} # TODO working source_type and destination_type differ in both name and type from get_fields in directions_core. # Change to be consistent diff --git a/ORStools/proc/snap_layer_proc.py b/ORStools/proc/snap_layer_proc.py index ade26e22..7d44b180 100644 --- a/ORStools/proc/snap_layer_proc.py +++ b/ORStools/proc/snap_layer_proc.py @@ -42,7 +42,6 @@ QgsCoordinateReferenceSystem, ) -from ORStools.common import PROFILES from ORStools.utils.gui import GuiUtils from ORStools.utils.processing import get_snapped_point_features from ORStools.proc.base_processing_algorithm import ORSBaseProcessingAlgorithm @@ -86,7 +85,7 @@ def processAlgorithm( ors_client = self.get_client(parameters, context, feedback) # Get profile value - profile = dict(enumerate(PROFILES))[parameters[self.IN_PROFILE]] + profile = dict(enumerate(self.profiles))[parameters[self.IN_PROFILE]] # Get parameter values source = self.parameterAsSource(parameters, self.IN_POINTS, context) @@ -138,7 +137,16 @@ def processAlgorithm( sink.addFeature(feat) except (exceptions.ApiError, exceptions.InvalidKey, exceptions.GenericServerError) as e: - msg = f"{e.__class__.__name__}: {str(e)}" + if ( + isinstance(e, exceptions.ApiError) + and "Parameter 'profile' has incorrect value" in e.message + ): + provider = self.providers[parameters[self.IN_PROVIDER]]["name"] + msg = self.tr( + f'The selected profile "{profile}" is not available in the chosen provider "{provider}"' + ) + else: + msg = f"{e.__class__.__name__}: {str(e)}" feedback.reportError(msg) logger.log(msg) diff --git a/ORStools/proc/snap_point_proc.py b/ORStools/proc/snap_point_proc.py index ec6bb157..a6445781 100644 --- a/ORStools/proc/snap_point_proc.py +++ b/ORStools/proc/snap_point_proc.py @@ -41,7 +41,6 @@ QgsCoordinateReferenceSystem, ) -from ORStools.common import PROFILES from ORStools.utils.gui import GuiUtils from ORStools.utils.processing import get_snapped_point_features from ORStools.proc.base_processing_algorithm import ORSBaseProcessingAlgorithm @@ -90,7 +89,7 @@ def processAlgorithm( ors_client = self.get_client(parameters, context, feedback) # Get profile value - profile = dict(enumerate(PROFILES))[parameters[self.IN_PROFILE]] + profile = dict(enumerate(self.profiles))[parameters[self.IN_PROFILE]] # Get parameter values point = self.parameterAsPoint(parameters, self.IN_POINT, context, self.crs_out) @@ -124,7 +123,16 @@ def processAlgorithm( sink.addFeature(feat) except (exceptions.ApiError, exceptions.InvalidKey, exceptions.GenericServerError) as e: - msg = f"{e.__class__.__name__}: {str(e)}" + if ( + isinstance(e, exceptions.ApiError) + and "Parameter 'profile' has incorrect value" in e.message + ): + provider = self.providers[parameters[self.IN_PROVIDER]]["name"] + msg = self.tr( + f'The selected profile "{profile}" is not available in the chosen provider "{provider}"' + ) + else: + msg = f"Feature ID {params['id']} caused a {e.__class__.__name__}:\n{str(e)}" feedback.reportError(msg) logger.log(msg) diff --git a/tests/conftest.py b/tests/conftest.py index 4c8ead41..1259b371 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,9 +1,9 @@ import os -from qgis.core import QgsSettings from ORStools.ORStoolsPlugin import ORStools from tests.utils.utilities import get_qgis_app +from ORStools.utils import configmanager def pytest_sessionstart(session): @@ -13,13 +13,13 @@ def pytest_sessionstart(session): """ QGISAPP, CANVAS, IFACE, PARENT = get_qgis_app() - ORStools(IFACE).add_default_provider_to_settings() - s = QgsSettings() - data = s.value("ORStools/config") + ORStools(IFACE) + + data = configmanager.read_config() if not os.environ.get("ORS_API_KEY"): raise ValueError( "No API key found in environment variables. Please set ORS_API_KEY environment variable to run tests." ) data["providers"][0]["key"] = os.environ.get("ORS_API_KEY") - s.setValue("ORStools/config", data) + configmanager.write_config(data) diff --git a/tests/test_gui.py b/tests/test_gui.py index 023e5a3a..75032196 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -320,10 +320,13 @@ def test_ORStoolsDialogConfig_endpoints(self): layer = proc.get_directions_points_layer() - self.assertEqual( - "POINT(8.67251100000000008 49.39887900000000087)", - next(layer.getFeatures()).geometry().asPolyline()[0].asWkt(), - ) + # self.assertEqual( + # "POINT(8.67251100000000008 49.39887900000000087)", + # next(layer.getFeatures()).geometry().asPolyline()[0].asWkt(), + # ) + pt = next(layer.getFeatures()).geometry().asPolyline()[0] + self.assertAlmostEqual(pt.x(), 8.67251100000000008, 3) + self.assertAlmostEqual(pt.y(), 49.39887900000000087, 3) def test_ORStoolsDialogConfig_url(self): from ORStools.gui.ORStoolsDialogConfig import ORStoolsDialogConfigMain @@ -413,10 +416,9 @@ def test_custom_endpoints(self): layer = proc.get_directions_points_layer() self.assertAlmostEqual(layer.featureCount(), 93, delta=3) - self.assertEqual( - "POINT(8.67251100000000008 49.39887900000000087)", - next(layer.getFeatures()).geometry().asPolyline()[0].asWkt(), - ) + pt = next(layer.getFeatures()).geometry().asPolyline()[0] + self.assertAlmostEqual(pt.x(), 8.67251100000000008, 3) + self.assertAlmostEqual(pt.y(), 49.39887900000000087, 3) def test_reload_geocode_completer_ors(self): """Test geocoding completer functionality with mocked API response.""" diff --git a/tests/test_proc.py b/tests/test_proc.py index 1fdea206..1ad3b610 100644 --- a/tests/test_proc.py +++ b/tests/test_proc.py @@ -317,9 +317,9 @@ def test_snapping(self): dest_id = snap_point.processAlgorithm(parameters, self.context, self.feedback) processed_layer = QgsProcessingUtils.mapLayerFromString(dest_id["OUTPUT"], self.context) new_feat = next(processed_layer.getFeatures()) - self.assertEqual( - new_feat.geometry().asWkt(), "Point (-106.61225600000000213 34.98548300000000211)" - ) + pt = new_feat.geometry().asPoint() + self.assertAlmostEqual(pt.x(), -106.61225600000000213, places=3) + self.assertAlmostEqual(pt.y(), 34.98548300000000211, places=3) parameters = { "INPUT_PROFILE": 0, @@ -334,9 +334,10 @@ def test_snapping(self): processed_layer = QgsProcessingUtils.mapLayerFromString(dest_id["OUTPUT"], self.context) new_feat = next(processed_layer.getFeatures()) - self.assertEqual( - new_feat.geometry().asWkt(), "Point (8.46554599999999979 49.48699799999999982)" - ) + pt = new_feat.geometry().asPoint() + self.assertAlmostEqual(pt.x(), 8.46554599999999979, places=2) + self.assertAlmostEqual(pt.y(), 49.48699799999999982, places=2) + self.assertEqual(len([i for i in processed_layer.getFeatures()]), 2) # test with "SNAPPED_NAME" being present in layer fields