Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
6c628a4
feat: add default profiles
merydian Mar 6, 2025
2766415
feat: add box for profiles to settings and connect it
merydian Mar 6, 2025
77dc892
feat: ensure backward compatibility
merydian Mar 6, 2025
f994984
feat: make reset work with profiles
merydian Mar 6, 2025
f06b607
feat: ensure newly created config will be set up right
merydian Mar 6, 2025
359b008
refactor: rework ui to use QListWidget
merydian Mar 6, 2025
2911716
feat: add functionality to use add profile button
merydian Mar 6, 2025
e68c24e
feat: add functionality to use remove profile button
merydian Mar 6, 2025
afa4783
refactor: rename profile list widget
merydian Mar 6, 2025
1407f17
fix: modify the right listwidget
merydian Mar 6, 2025
f297e32
feat: add load button to get profiles from status endpoint
merydian Mar 6, 2025
9fb6613
feat: save state and add reset to defaults button
merydian Mar 6, 2025
b6dcf9e
feat: import profiles from existing source
merydian Mar 6, 2025
4b9e584
feat: add profiles key to settings key
merydian Mar 6, 2025
fa49b91
feat: set custom profiles in base processing alg
merydian Mar 6, 2025
77e1657
style: run ruff
merydian Mar 6, 2025
e7ecf54
refactor: make profiles and providers member of base class
merydian Mar 6, 2025
42e4483
fix: buttons wrongly positioned
merydian May 19, 2025
66f8a24
refactor: remove unused duplicate method
merydian May 19, 2025
bb84d28
feat: show warning when trying to query status endpoint of live API
merydian May 19, 2025
41cb3fb
feat: implement profile refresh functionality for routing travel comb…
merydian Jun 24, 2025
f4bdd51
fix: actually save settings
merydian Jun 24, 2025
be9cd11
style: run ruff
merydian Jun 24, 2025
1e0e3ff
fix: update profile retrieval to use instance profiles
merydian Jun 25, 2025
722b964
fix: improve error handling for invalid profile parameters
merydian Jun 25, 2025
8999359
fix: enhance error handling for provider configuration
merydian Jun 25, 2025
876d69f
test: update assertions to use assertAlmostEqual for precision
merydian Jun 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions ORStools/ORStoolsPlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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."""
Expand All @@ -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:
Expand All @@ -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:
Expand Down
13 changes: 11 additions & 2 deletions ORStools/gui/ORStoolsDialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@
__help__,
)
from ORStools.common import (
PROFILES,
PREFERENCES,
)
from ORStools.utils import maptools, configmanager, transform, gui, exceptions
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 = [
Expand Down
182 changes: 169 additions & 13 deletions ORStools/gui/ORStoolsDialogConfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"))

Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -148,6 +169,7 @@ def _build_ui(self) -> None:
provider_entry["key"],
provider_entry["timeout"],
provider_entry["endpoints"],
provider_entry["profiles"],
new=False,
)

Expand All @@ -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."""
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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):
Expand Down
3 changes: 3 additions & 0 deletions ORStools/proc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
***************************************************************************/
"""

from ORStools.common import PROFILES

ENDPOINTS = {
"directions": "directions",
"isochrones": "isochrones",
Expand All @@ -48,6 +50,7 @@
"name": "openrouteservice",
"timeout": 60,
"endpoints": ENDPOINTS,
"profiles": PROFILES,
}
]
}
Loading