diff --git a/CHANGELOG.md b/CHANGELOG.md
index 07219bfc..c358d77b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -40,6 +40,13 @@ RELEASING:
14. Create new release in GitHub with tag version and release title of `vX.X.X`
-->
+## Unreleased
+### Added
+- Processing algorithms for the Network Export endpoint ([#210](https://github.com/GIScience/orstools-qgis-plugin/issues/210))
+
+### Changed
+- Use QgsSettings instead of config.yml file to avoid deletion of providers on update ([#108](https://github.com/GIScience/orstools-qgis-plugin/issues/108))
+
## [1.8.4] - 2024-07-29
### Fixed
diff --git a/ORStools/ORStoolsPlugin.py b/ORStools/ORStoolsPlugin.py
index ed0d6e08..33fa4b5c 100644
--- a/ORStools/ORStoolsPlugin.py
+++ b/ORStools/ORStoolsPlugin.py
@@ -73,6 +73,8 @@ def __init__(self, iface: QgisInterface) -> None:
except TypeError:
pass
+ self.add_default_provider_to_settings()
+
def initGui(self) -> None:
"""Create the menu entries and toolbar icons inside the QGIS GUI."""
@@ -83,3 +85,23 @@ def unload(self) -> None:
"""remove menu entry and toolbar icons"""
QgsApplication.processingRegistry().removeProvider(self.provider)
self.dialog.unload()
+
+ def add_default_provider_to_settings(self):
+ s = QgsSettings()
+ settings = s.value("ORStools/config")
+ if not settings:
+ def_settings = {
+ "providers": [
+ {
+ "ENV_VARS": {
+ "ORS_QUOTA": "X-Ratelimit-Limit",
+ "ORS_REMAINING": "X-Ratelimit-Remaining",
+ },
+ "base_url": "https://api.openrouteservice.org",
+ "key": "",
+ "name": "openrouteservice",
+ "timeout": 60,
+ }
+ ]
+ }
+ s.setValue("ORStools/config", def_settings)
diff --git a/ORStools/config.yml b/ORStools/config.yml
deleted file mode 100755
index a08cd7d1..00000000
--- a/ORStools/config.yml
+++ /dev/null
@@ -1,8 +0,0 @@
-providers:
-- ENV_VARS:
- ORS_QUOTA: X-Ratelimit-Limit
- ORS_REMAINING: X-Ratelimit-Remaining
- base_url: https://api.openrouteservice.org
- key: ''
- name: openrouteservice
- timeout: 60
diff --git a/ORStools/help/export_network_from_map.help b/ORStools/help/export_network_from_map.help
new file mode 100644
index 00000000..62327252
--- /dev/null
+++ b/ORStools/help/export_network_from_map.help
@@ -0,0 +1,9 @@
+Export the base graph for different modes of transport.
+
+You need to have a valid API key ('Web' menu ► 'ORS Tools' ► 'Configuration') or sign up at https://openrouteservice.org/sign-up/.
+Current restriction limits for the openrouteservice API apply.
+
+Travel Mode: determines the profile used.
+
+Input Extent: Choose an extent, the content of which will be exported.
+
diff --git a/ORStools/help/export_network_from_map_de.help b/ORStools/help/export_network_from_map_de.help
new file mode 100644
index 00000000..467600a1
--- /dev/null
+++ b/ORStools/help/export_network_from_map_de.help
@@ -0,0 +1,7 @@
+Das Basisgraph für verschiedene Verkehrsmittel exportieren.
+
+Ein gültiger API-Schlüssel ist erforderlich ('Web'-Menü ► 'ORS Tools' ► 'Konfiguration') oder eine Anmeldung unter https://openrouteservice.org/sign-up/. Aktuelle Beschränkungslimits für die openrouteservice API gelten.
+
+Verkehrsmittel: bestimmt das genutzte Reise-Profil
+
+Input-Extent: Es ist ein Bereich auszuwählen, dessen Inhalt exportiert wird.
\ No newline at end of file
diff --git a/ORStools/i18n/orstools_de.ts b/ORStools/i18n/orstools_de.ts
index 85a64e50..40f7a2ec 100644
--- a/ORStools/i18n/orstools_de.ts
+++ b/ORStools/i18n/orstools_de.ts
@@ -229,6 +229,19 @@ Duplikate entfernen oder Wegpunktoptimierung abwählen.
Csv Spalte (benötigt Csv Faktor und csv in Extra Info)
+
+ ORSExportAlgo
+
+
+
+ Ausdehnung
+
+
+
+
+ Netzwerk von Karte exportieren
+
+
ORSIsochronesLayerAlgo
@@ -331,12 +344,12 @@ Duplikate entfernen oder Wegpunktoptimierung abwählen.
ORStoolsDialog
-
+
Anwenden
-
+
Schließen
@@ -721,12 +734,12 @@ p, li { white-space: pre-wrap; }
Über
-
+
Duplikate
-
+
-
+
Die Anfrage wurde abgebrochen!
diff --git a/ORStools/i18n/translate.pro b/ORStools/i18n/translate.pro
index 31f711c2..b54a7f1f 100644
--- a/ORStools/i18n/translate.pro
+++ b/ORStools/i18n/translate.pro
@@ -9,6 +9,7 @@ SOURCES = ../common/directions_core.py \
../proc/isochrones_layer_proc.py \
../proc/isochrones_point_proc.py \
../proc/matrix_proc.py \
+ ../proc/export_proc.py \
../gui/ORStoolsDialog.py \
../gui/ORStoolsDialogConfig.py
diff --git a/ORStools/proc/base_processing_algorithm.py b/ORStools/proc/base_processing_algorithm.py
index 6245b328..0cdbb106 100644
--- a/ORStools/proc/base_processing_algorithm.py
+++ b/ORStools/proc/base_processing_algorithm.py
@@ -227,12 +227,19 @@ def initAlgorithm(self, configuration: Dict) -> None:
Combines default and algorithm parameters and adds them in order to the
algorithm dialog window.
"""
- parameters = (
- [self.provider_parameter(), self.profile_parameter()]
- + self.PARAMETERS
- + self.option_parameters()
- + [self.output_parameter()]
- )
+ if self.ALGO_NAME not in ["export_network_from_map"]:
+ parameters = (
+ [self.provider_parameter(), self.profile_parameter()]
+ + self.PARAMETERS
+ + self.option_parameters()
+ + [self.output_parameter()]
+ )
+ else:
+ parameters = (
+ [self.provider_parameter(), self.profile_parameter()]
+ + self.PARAMETERS
+ + [self.output_parameter()]
+ )
for param in parameters:
if param.name() in ADVANCED_PARAMETERS:
if self.GROUP == "Matrix":
diff --git a/ORStools/proc/export_proc.py b/ORStools/proc/export_proc.py
new file mode 100644
index 00000000..9d190800
--- /dev/null
+++ b/ORStools/proc/export_proc.py
@@ -0,0 +1,175 @@
+# -*- coding: utf-8 -*-
+"""
+/***************************************************************************
+ ORStools
+ A QGIS plugin
+ QGIS client to query openrouteservice
+ -------------------
+ begin : 2017-02-01
+ git sha : $Format:%H$
+ copyright : (C) 2021 by HeiGIT gGmbH
+ email : support@openrouteservice.heigit.org
+ ***************************************************************************/
+
+ This plugin provides access to openrouteservice API functionalities
+ (https://openrouteservice.org), developed and
+ maintained by the openrouteservice team of HeiGIT gGmbH, Germany. By using
+ this plugin you agree to the ORS terms of service
+ (https://openrouteservice.org/terms-of-service/).
+
+/***************************************************************************
+ * *
+ * This program is free software; you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation; either version 2 of the License, or *
+ * (at your option) any later version. *
+ * *
+ ***************************************************************************/
+"""
+
+from typing import Dict
+
+from qgis.core import (
+ QgsWkbTypes,
+ QgsFeature,
+ QgsField,
+ QgsFields,
+ QgsCoordinateReferenceSystem,
+ QgsProcessingParameterExtent,
+ QgsProcessingParameterFeatureSink,
+ QgsProcessingContext,
+ QgsProcessingFeedback,
+ QgsPointXY,
+ QgsGeometry,
+)
+
+from qgis.PyQt.QtCore import QVariant
+
+
+from ORStools.common import PROFILES
+from ORStools.utils import exceptions, logger
+from .base_processing_algorithm import ORSBaseProcessingAlgorithm
+
+
+# noinspection PyPep8Naming
+class ORSExportAlgo(ORSBaseProcessingAlgorithm):
+ def __init__(self):
+ super().__init__()
+ self.ALGO_NAME: str = "export_network_from_map"
+ self.GROUP: str = "Export"
+ self.IN_EXPORT: str = "INPUT_EXPORT"
+ self.OUT_POINT = "OUTPUT_POINT"
+ self.PARAMETERS: list = [
+ QgsProcessingParameterExtent(
+ name=self.IN_EXPORT,
+ description=self.tr("Input Extent"),
+ ),
+ QgsProcessingParameterFeatureSink(
+ name=self.OUT_POINT,
+ description="Node Export",
+ ),
+ ]
+
+ def processAlgorithm(
+ self, parameters: dict, context: QgsProcessingContext, feedback: QgsProcessingFeedback
+ ) -> Dict[str, str]:
+ ors_client = self._get_ors_client_from_provider(parameters[self.IN_PROVIDER], feedback)
+
+ # Get profile value
+ profile = dict(enumerate(PROFILES))[parameters[self.IN_PROFILE]]
+
+ target_crs = QgsCoordinateReferenceSystem("EPSG:4326")
+ rect = self.parameterAsExtent(parameters, self.IN_EXPORT, context, crs=target_crs)
+
+ extent = [[rect.xMinimum(), rect.yMinimum()], [rect.xMaximum(), rect.yMaximum()]]
+
+ params = {
+ "bbox": extent,
+ "id": "export_request",
+ }
+
+ (sink_line, dest_id_line) = self.parameterAsSink(
+ parameters,
+ self.OUT,
+ context,
+ self.get_fields_line(),
+ QgsWkbTypes.Type.LineString,
+ QgsCoordinateReferenceSystem.fromEpsgId(4326),
+ )
+
+ (sink_point, dest_id_point) = self.parameterAsSink(
+ parameters,
+ self.OUT_POINT,
+ context,
+ self.get_fields_point(),
+ QgsWkbTypes.Type.Point,
+ QgsCoordinateReferenceSystem.fromEpsgId(4326),
+ )
+
+ # Make request and catch ApiError
+ try:
+ response = ors_client.request("/v2/export/" + profile, {}, post_json=params)
+ nodes_dict = {item["nodeId"]: item["location"] for item in response["nodes"]}
+ edges = response["edges"]
+ for edge in edges:
+ from_id = edge["fromId"]
+ to_id = edge["toId"]
+ weight = edge["weight"]
+
+ to_coords = nodes_dict[to_id]
+ from_coords = nodes_dict[from_id]
+
+ geometry = QgsGeometry.fromPolylineXY(
+ [
+ QgsPointXY(from_coords[0], from_coords[1]),
+ QgsPointXY(to_coords[0], to_coords[1]),
+ ]
+ )
+
+ feat = QgsFeature()
+ feat.setGeometry(geometry)
+ feat.setAttributes([from_id, to_id, weight])
+ sink_line.addFeature(feat)
+
+ unique_coordinates = {
+ tuple(item["location"]): item["nodeId"] for item in response["nodes"]
+ }
+ points = [(coords, node_id) for coords, node_id in unique_coordinates.items()]
+ for item in points:
+ point = QgsPointXY(item[0][0], item[0][1])
+ point_geometry = QgsGeometry.fromPointXY(point)
+
+ point_feat = QgsFeature()
+ point_feat.setGeometry(point_geometry)
+ point_feat.setAttributes([item[1]])
+ sink_point.addFeature(point_feat)
+
+ except (exceptions.ApiError, exceptions.InvalidKey, exceptions.GenericServerError) as e:
+ msg = f"{e.__class__.__name__}: {str(e)}"
+ feedback.reportError(msg)
+ logger.log(msg)
+
+ return {self.OUT: dest_id_line, self.OUT_POINT: dest_id_point}
+
+ @staticmethod
+ def get_fields_line():
+ fields = QgsFields()
+ fields.append(QgsField("FROM_ID", QVariant.Double))
+ fields.append(QgsField("TO_ID", QVariant.Double))
+ fields.append(QgsField("WEIGHT", QVariant.Double))
+
+ return fields
+
+ @staticmethod
+ def get_fields_point():
+ fields = QgsFields()
+ fields.append(QgsField("ID", QVariant.Int))
+
+ return fields
+
+ def displayName(self) -> str:
+ """
+ Algorithm name shown in QGIS toolbox
+ :return:
+ """
+ return self.tr("Export Network from Map")
diff --git a/ORStools/proc/provider.py b/ORStools/proc/provider.py
index 1492b140..e39abb91 100644
--- a/ORStools/proc/provider.py
+++ b/ORStools/proc/provider.py
@@ -35,6 +35,7 @@
from .directions_lines_proc import ORSDirectionsLinesAlgo
from .directions_points_layer_proc import ORSDirectionsPointsLayerAlgo
from .directions_points_layers_proc import ORSDirectionsPointsLayersAlgo
+from .export_proc import ORSExportAlgo
from .isochrones_layer_proc import ORSIsochronesLayerAlgo
from .isochrones_point_proc import ORSIsochronesPointAlgo
from .matrix_proc import ORSMatrixAlgo
@@ -63,6 +64,7 @@ def loadAlgorithms(self) -> None:
self.addAlgorithm(ORSIsochronesLayerAlgo())
self.addAlgorithm(ORSIsochronesPointAlgo())
self.addAlgorithm(ORSMatrixAlgo())
+ self.addAlgorithm(ORSExportAlgo())
@staticmethod
def icon() -> QIcon:
diff --git a/ORStools/utils/configmanager.py b/ORStools/utils/configmanager.py
index e8d1c534..f7fa62d2 100644
--- a/ORStools/utils/configmanager.py
+++ b/ORStools/utils/configmanager.py
@@ -29,9 +29,7 @@
import os
-import yaml
-
-from ORStools import CONFIG_PATH
+from qgis.core import QgsSettings
def read_config() -> dict:
@@ -41,10 +39,10 @@ def read_config() -> dict:
:returns: Parsed settings dictionary.
:rtype: dict
"""
- with open(CONFIG_PATH) as f:
- doc = yaml.safe_load(f)
+ s = QgsSettings()
+ config = s.value("ORStools/config")
- return doc
+ return config
def write_config(new_config: dict) -> None:
@@ -54,8 +52,8 @@ def write_config(new_config: dict) -> None:
:param new_config: new provider settings after altering in dialog.
:type new_config: dict
"""
- with open(CONFIG_PATH, "w") as f:
- yaml.safe_dump(new_config, f)
+ s = QgsSettings()
+ s.setValue("ORStools/config", new_config)
def write_env_var(key: str, value: str) -> None:
diff --git a/README.md b/README.md
index 9a9d2bdb..ad3f5675 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# ORS Tools QGIS plugin
-![Testing](https://github.com/Merydian/orstools-qgis-plugin/actions/workflows/test.yml/badge.svg)
-![Ruff](https://github.com/Merydian/orstools-qgis-plugin/actions/workflows/ruff.yml/badge.svg)
+![Testing](https://github.com/GIScience/orstools-qgis-plugin/actions/workflows/test.yml/badge.svg)
+![Ruff](https://github.com/GIScience/orstools-qgis-plugin/actions/workflows/ruff.yml/badge.svg)
![ORS Tools](https://user-images.githubusercontent.com/23240110/122937401-3ee72400-d372-11eb-8e3b-6c435d1dd964.png)
diff --git a/tests/conftest.py b/tests/conftest.py
index d39efd01..78dcd20f 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,11 +1,16 @@
import os
-import yaml
+from qgis.core import QgsSettings
+
+from ORStools.ORStoolsPlugin import ORStools
from ORStools.utils.configmanager import read_config
+from tests.utils.utilities import get_qgis_app
-with open("ORStools/config.yml", "r+") as file:
- data = yaml.safe_load(file)
+QGISAPP, CANVAS, IFACE, PARENT = get_qgis_app()
+ORStools(IFACE).add_default_provider_to_settings()
+s = QgsSettings()
+data = s.value("ORStools/config")
def pytest_sessionstart(session):
"""
@@ -14,8 +19,7 @@ def pytest_sessionstart(session):
"""
if data["providers"][0]["key"] == "":
data["providers"][0]["key"] = os.environ.get("ORS_API_KEY")
- with open("ORStools/config.yml", "w") as file:
- yaml.dump(data, file)
+ s.setValue("ORStools/config", data)
else:
raise ValueError("API key is not empty.")
@@ -25,10 +29,8 @@ def pytest_sessionfinish(session, exitstatus):
Called after whole test run finished, right before
returning the exit status to the system.
"""
- with open("ORStools/config.yml", "w") as file:
- if not data["providers"][0]["key"] == "":
- data['providers'][0]['key'] = '' # fmt: skip
- yaml.dump(data, file)
-
+ if not data["providers"][0]["key"] == "":
+ data['providers'][0]['key'] = '' # fmt: skip
+ s.setValue("ORStools/config", data)
config = read_config()
assert config["providers"][0]["key"] == '' # fmt: skip
diff --git a/tests/test_gui.py b/tests/test_gui.py
index 17c97adf..b9b0372b 100644
--- a/tests/test_gui.py
+++ b/tests/test_gui.py
@@ -63,6 +63,6 @@ def test_ORStoolsDialog(self):
dlg.line_tool.canvasDoubleClickEvent(map_dclick)
self.assertTrue(dlg.isVisible())
- self.assertAlmostEqual(
+ self.assertEqual(
dlg.routing_fromline_list.item(0).text(), "Point 0: -0.187575, 56.516620"
)
diff --git a/tests/test_proc.py b/tests/test_proc.py
index df4cd8d7..ebe9f3de 100644
--- a/tests/test_proc.py
+++ b/tests/test_proc.py
@@ -6,6 +6,7 @@
QgsVectorLayer,
QgsFeature,
QgsGeometry,
+ QgsRectangle,
)
from qgis.testing import unittest
@@ -15,6 +16,7 @@
from ORStools.proc.isochrones_layer_proc import ORSIsochronesLayerAlgo
from ORStools.proc.isochrones_point_proc import ORSIsochronesPointAlgo
from ORStools.proc.matrix_proc import ORSMatrixAlgo
+from ORStools.proc.export_proc import ORSExportAlgo
class TestProc(unittest.TestCase):
@@ -42,6 +44,10 @@ def setUpClass(cls) -> None:
feature.setGeometry(line_geometry)
cls.line_layer.dataProvider().addFeatures([feature])
+ lower_left = QgsPointXY(8.45, 48.85)
+ upper_right = QgsPointXY(8.46, 48.86)
+ cls.bbox = QgsRectangle(lower_left, upper_right)
+
cls.feedback = QgsProcessingFeedback()
cls.context = QgsProcessingContext()
@@ -176,3 +182,20 @@ def test_matrix(self):
processed_layer = QgsProcessingUtils.mapLayerFromString(dest_id["OUTPUT"], self.context)
self.assertEqual(type(processed_layer), QgsVectorLayer)
+
+ def test_export(self):
+ parameters = {
+ "INPUT_PROVIDER": 0,
+ "INPUT_PROFILE": 0,
+ "INPUT_EXPORT": self.bbox,
+ "OUTPUT_POINT": "TEMPORARY_OUTPUT",
+ "OUTPUT": "TEMPORARY_OUTPUT",
+ }
+
+ export = ORSExportAlgo().create()
+ dest_id = export.processAlgorithm(parameters, self.context, self.feedback)
+ processed_layer = QgsProcessingUtils.mapLayerFromString(dest_id["OUTPUT"], self.context)
+ processed_nodes = QgsProcessingUtils.mapLayerFromString(dest_id["OUTPUT_POINT"], self.context)
+
+ self.assertEqual(type(processed_layer), QgsVectorLayer)
+ self.assertEqual(type(processed_nodes), QgsVectorLayer)