From e6f17f89592f0752d023f518ac2bba4b3434b168 Mon Sep 17 00:00:00 2001 From: Till Frankenbach Date: Fri, 30 Aug 2024 16:36:40 +0200 Subject: [PATCH 1/3] feat: use qgs settings instead of config yml (#249) Co-authored-by: Jakob Schnell --- CHANGELOG.md | 5 +++++ ORStools/ORStoolsPlugin.py | 22 ++++++++++++++++++++++ ORStools/config.yml | 8 -------- ORStools/utils/configmanager.py | 14 ++++++-------- tests/conftest.py | 22 ++++++++++++---------- tests/test_gui.py | 2 +- 6 files changed, 46 insertions(+), 27 deletions(-) delete mode 100755 ORStools/config.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index 07219bfc..67a66a58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,11 @@ RELEASING: 14. Create new release in GitHub with tag version and release title of `vX.X.X` --> +## Unreleased + +### 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/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/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" ) From 78064b1cbc07200a2837e237139bdd7af6c4567a Mon Sep 17 00:00:00 2001 From: Till Frankenbach Date: Thu, 5 Sep 2024 10:57:00 +0200 Subject: [PATCH 2/3] fix: correct badge links in readme (#282) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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) From 8687fe3ac2357d15e11f371c98d59808672a958e Mon Sep 17 00:00:00 2001 From: Till Frankenbach Date: Fri, 18 Oct 2024 16:13:53 +0200 Subject: [PATCH 3/3] feat: add processing algo for export endpoint (#269) Co-authored-by: Jakob Schnell --- CHANGELOG.md | 2 + ORStools/help/export_network_from_map.help | 9 + ORStools/help/export_network_from_map_de.help | 7 + ORStools/i18n/orstools_de.ts | 37 ++-- ORStools/i18n/translate.pro | 1 + ORStools/proc/base_processing_algorithm.py | 19 +- ORStools/proc/export_proc.py | 175 ++++++++++++++++++ ORStools/proc/provider.py | 2 + tests/test_proc.py | 23 +++ 9 files changed, 257 insertions(+), 18 deletions(-) create mode 100644 ORStools/help/export_network_from_map.help create mode 100644 ORStools/help/export_network_from_map_de.help create mode 100644 ORStools/proc/export_proc.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 67a66a58..c358d77b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,8 @@ RELEASING: --> ## 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)) 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 4325a73a..83af637f 100644 --- a/ORStools/i18n/orstools_de.ts +++ b/ORStools/i18n/orstools_de.ts @@ -4,12 +4,12 @@ @default - + About {} Über {} - + <b>ORS Tools</b> provides access to <a href="https://openrouteservice.org" style="color: {0}">openrouteservice</a> routing functionalities.<br><br><center><a href="https://heigit.org/de/willkommen"><img src=":/plugins/ORStools/img/logo_heigit_300.png"/></a><br><br></center>Author: HeiGIT gGmbH<br>Email: <a href="mailto:Openrouteservice <{1}>">{1}</a><br>Web: <a href="{2}">{2}</a><br>Repo: <a href="https://github.com/GIScience/orstools-qgis-plugin">github.com/GIScience/orstools-qgis-plugin</a><br>Version: {3} <b>ORS Tools</b> bietet Zugriff auf <a href="https://openrouteservice.org" style="color: {0}">openrouteservice</a> Berechnungen.<br><br><center><a href="https://heigit.org/de/willkommen"><img src=":/plugins/ORStools/img/logo_heigit_300.png"/></a><br><br></center>Author: HeiGIT gGmbH<br>Email: <a href="mailto:Openrouteservice <{1}>">{1}</a><br>Web: <a href="{2}">{2}</a><br>Repo: <a href="https://github.com/GIScience/orstools-qgis-plugin">github.com/GIScience/orstools-qgis-plugin</a><br>Version: {3} @@ -127,7 +127,7 @@ Wegpunktoptimierung (sonstige Konfiguration wird nicht berücksichtigt) - + Directions from 1 Point-Layer Routenberechnung aus einem Punkt-Layer @@ -229,6 +229,19 @@ Duplikate entfernen oder Wegpunktoptimierung abwählen. Csv Spalte (benötigt Csv Faktor und csv in Extra Info) + + ORSExportAlgo + + + Input Extent + Ausdehnung + + + + Export Network from Map + Netzwerk von Karte exportieren + + ORSIsochronesLayerAlgo @@ -331,12 +344,12 @@ Duplikate entfernen oder Wegpunktoptimierung abwählen. ORStoolsDialog - + Apply Anwenden - + Close Schließen @@ -443,7 +456,7 @@ p, li { white-space: pre-wrap; } <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'Ubuntu'; font-size:11pt; font-weight:400; font-style:normal;"> +</style></head><body style=" font-family:'Ubuntu'; font-size:11pt; font-weight:400; font-style:normal;"> <p style=" padding: 10px; -qt-block-indent:0; text-indent:0px ; background-color:#e7f2fa; color: #999999"><img stype="margin: 10px" src=":/plugins/ORStools/img/icon_about.png" width=16 height=16 /> Sämtliche Einstellungen werden überschrieben</p></body></html> @@ -696,27 +709,27 @@ p, li { white-space: pre-wrap; } ORStoolsDialogMain - + Help Hilfe - + Provider Settings Dienst-Einstellungen - + About Über - + Duplicates Duplikate - + There are duplicate points in the input layer. Traveling Salesman Optimization does not allow this. Either remove the duplicates or deselect Traveling Salesman. @@ -725,7 +738,7 @@ p, li { white-space: pre-wrap; } Duplikate entfernen oder Wegpunktoptimierung abwählen. - + The request has been aborted! 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/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)