Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 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
c421409
Merge branch 'main' into allow-custom-profiles
koebi Jan 27, 2026
e316c72
refactor: update import statements for qt6
merydian Jan 27, 2026
33cbc96
fix: update providers on update of plugin
merydian Jan 28, 2026
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
27 changes: 14 additions & 13 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 All @@ -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__)
Expand All @@ -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."""
Expand All @@ -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)
13 changes: 11 additions & 2 deletions ORStools/gui/ORStoolsDialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,6 @@
__help__,
)
from ORStools.common import (
PROFILES,
PREFERENCES,
)
from ORStools.utils import maptools, configmanager, transform, gui, exceptions
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 = [
Expand Down
183 changes: 169 additions & 14 deletions ORStools/gui/ORStoolsDialogConfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"))

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

Expand All @@ -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."""
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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):
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
Loading