diff --git a/ORStools/ORStoolsPlugin.py b/ORStools/ORStoolsPlugin.py index 2156bf8a..cdf61f0d 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: @@ -73,7 +74,10 @@ def __init__(self, iface: QgisInterface) -> None: except TypeError: pass - self.add_default_provider_to_settings() + try: + configmanager.read_config()["providers"] + except (TypeError, KeyError): + self.add_default_provider_to_settings() def initGui(self) -> None: """Create the menu entries and toolbar icons inside the QGIS GUI.""" @@ -90,7 +94,7 @@ def add_default_provider_to_settings(self): s = QgsSettings() settings = s.value("ORStools/config") - settings_keys = ["ENV_VARS", "base_url", "key", "name", "endpoints"] + settings_keys = ["ENV_VARS", "base_url", "key", "name", "endpoints", "profiles"] # Add any new settings here for backwards compatibility if settings: @@ -101,6 +105,8 @@ def add_default_provider_to_settings(self): # Add here, like the endpoints prov["endpoints"] = ENDPOINTS settings["providers"][i] = prov + prov["profiles"] = PROFILES + settings["providers"][i] = prov if changed: s.setValue("ORStools/config", settings) else: diff --git a/ORStools/gui/ORStoolsDialog.py b/ORStools/gui/ORStoolsDialog.py index 3d651098..f4462de8 100644 --- a/ORStools/gui/ORStoolsDialog.py +++ b/ORStools/gui/ORStoolsDialog.py @@ -77,7 +77,6 @@ __help__, ) from ORStools.common import ( - PROFILES, PREFERENCES, ) from ORStools.utils import maptools, configmanager, transform, gui, exceptions @@ -94,6 +93,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: @@ -324,7 +325,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 @@ -408,6 +410,13 @@ def __init__(self, iface: QgisInterface, parent=None) -> None: self.rubber_band = None + 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..f549ac8d 100644 --- a/ORStools/gui/ORStoolsDialogConfig.py +++ b/ORStools/gui/ORStoolsDialogConfig.py @@ -27,7 +27,12 @@ ***************************************************************************/ """ -from qgis.gui import QgsCollapsibleGroupBox +import json + +from PyQt5.QtCore import QUrl +from PyQt5.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 @@ -36,11 +41,18 @@ 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 +86,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 +132,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 +169,7 @@ def _build_ui(self) -> None: provider_entry["key"], provider_entry["timeout"], provider_entry["endpoints"], + provider_entry["profiles"], new=False, ) @@ -167,7 +189,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 +221,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 +332,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 +387,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 3f5751d4..5c0a706f 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] return QgsProcessingParameterEnum( self.IN_PROVIDER, self.tr("Provider", "ORSBaseProcessingAlgorithm"), @@ -132,11 +136,12 @@ def profile_parameter(self) -> QgsProcessingParameterEnum: """ Parameter definition for profile, used in all child classes """ + return QgsProcessingParameterEnum( self.IN_PROFILE, self.tr("Travel mode", "ORSBaseProcessingAlgorithm"), - PROFILES, - defaultValue=PROFILES[0], + self.profiles, + defaultValue=self.profiles[0], ) def output_parameter(self) -> QgsProcessingParameterFeatureSink: diff --git a/ORStools/proc/directions_lines_proc.py b/ORStools/proc/directions_lines_proc.py index 4d6436cb..f1cd68fe 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 @@ -130,7 +130,7 @@ def processAlgorithm( ) -> Dict[str, str]: ors_client = self._get_ors_client_from_provider(parameters[self.IN_PROVIDER], 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]] @@ -245,7 +245,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 41c8d5cf..2c49b48f 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 @@ -137,7 +137,7 @@ def processAlgorithm( ) -> Dict[str, str]: ors_client = self._get_ors_client_from_provider(parameters[self.IN_PROVIDER], 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]] @@ -286,7 +286,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 024e05ef..958bc706 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 @@ -151,7 +151,7 @@ def processAlgorithm( ) -> Dict[str, str]: ors_client = self._get_ors_client_from_provider(parameters[self.IN_PROVIDER], 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]] @@ -246,9 +246,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 279d9f3a..75d18603 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 @@ -80,7 +79,7 @@ def processAlgorithm( ors_client = self._get_ors_client_from_provider(parameters[self.IN_PROVIDER], 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) @@ -150,7 +149,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 0a4b60e0..85a88e11 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 @@ -125,7 +125,7 @@ def processAlgorithm( ) -> Dict[str, str]: ors_client = self._get_ors_client_from_provider(parameters[self.IN_PROVIDER], 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]] @@ -206,9 +206,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 b6b29c16..ffa36ec7 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 @@ -111,7 +111,7 @@ def processAlgorithm( ) -> Dict[str, str]: ors_client = self._get_ors_client_from_provider(parameters[self.IN_PROVIDER], 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]] @@ -164,7 +164,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 089347d3..c53cdac2 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 @@ -96,7 +95,7 @@ def processAlgorithm( ors_client = self._get_ors_client_from_provider(parameters[self.IN_PROVIDER], 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) @@ -182,41 +181,52 @@ def processAlgorithm( endpoint = self.get_endpoint_names_from_provider(parameters[self.IN_PROVIDER])["matrix"] response = ors_client.request(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 edd07be5..d2ab3a22 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 @@ -79,7 +78,7 @@ def processAlgorithm( ors_client = self._get_ors_client_from_provider(parameters[self.IN_PROVIDER], 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) @@ -131,7 +130,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 94fd2db1..1832c91e 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 @@ -82,7 +81,7 @@ def processAlgorithm( ors_client = self._get_ors_client_from_provider(parameters[self.IN_PROVIDER], 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) @@ -116,7 +115,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/test_gui.py b/tests/test_gui.py index d51a1728..74d08380 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -303,10 +303,13 @@ def test_ORStoolsDialogConfig_endpoints(self): layer = proc.test_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 @@ -354,7 +357,6 @@ def test_ORStoolsDialogConfig_url(self): layer = proc.test_directions_points_layer() - 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) diff --git a/tests/test_proc.py b/tests/test_proc.py index c13fc807..35e852f9 100644 --- a/tests/test_proc.py +++ b/tests/test_proc.py @@ -247,9 +247,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, @@ -264,9 +264,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