diff --git a/CHANGELOG.md b/CHANGELOG.md index d46e9da5..4af44db2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,10 @@ RELEASING: 14. Create new release in GitHub with tag version and release title of `vX.X.X` --> +## Unreleased +### Added +- Support for Snap endpoint ([#262](https://github.com/GIScience/orstools-qgis-plugin/issues/262)) + ## [1.8.3] - 2024-05-29 ### Fixed diff --git a/ORStools/common/snap_core.py b/ORStools/common/snap_core.py new file mode 100644 index 00000000..83d026a0 --- /dev/null +++ b/ORStools/common/snap_core.py @@ -0,0 +1,49 @@ +# -*- 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 qgis.core import QgsFeature, QgsPointXY, QgsGeometry + + +def get_snapped_point_features(response: dict) -> list: + locations = response["locations"] + feats = [] + for location in locations: + feat = QgsFeature() + if location: + coords = location["location"] + if "name" in location.keys(): + name = location["name"] + snapped_distance = location["snapped_distance"] + attr = [name, snapped_distance] if "name" in location.keys() else ["", snapped_distance] + feat.setAttributes(attr) + feat.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(coords[0], coords[1]))) + + feats.append(feat) + + return feats diff --git a/ORStools/help/snap_from_point.help b/ORStools/help/snap_from_point.help new file mode 100644 index 00000000..1e7b2160 --- /dev/null +++ b/ORStools/help/snap_from_point.help @@ -0,0 +1,11 @@ +The snapping endpoint can be used to snap points to the edges of the street network for a specific means of transportation. + +You need to have a valid API key ('Web' menu > 'Configuration') or sign up at https://openrouteservice.org/sign-up/. + +Travel Mode: determines the profile used. + +Input layer: select an input point from the canvas. + +Radius: Radius in which to search. + +Current restriction limits for the openrouteservice API apply. diff --git a/ORStools/help/snap_from_point_de.help b/ORStools/help/snap_from_point_de.help new file mode 100644 index 00000000..93a18d63 --- /dev/null +++ b/ORStools/help/snap_from_point_de.help @@ -0,0 +1,11 @@ +Der Snap-Algorithmus kann verwendet werden, um Punkte an den Rändern des Straßennetzes für ein bestimmtes Verkehrsmittel zu finden. + +Ein gültiger API-Key ('Web'-Menü > 'Dienst-Einstellungen') oder Registrierung unter https://openrouteservice.org/sign-up/ wird benötigt. + +Verkehrsmittel: bestimmt das genutzte Reise-Profil + +Eingabelayer: Eingabepunkt + +Radius: Radius in welchem gesucht wird. + +Es gelten die Restriktionen der openrouteservice-API. diff --git a/ORStools/help/snap_from_point_layer.help b/ORStools/help/snap_from_point_layer.help new file mode 100644 index 00000000..2d3af935 --- /dev/null +++ b/ORStools/help/snap_from_point_layer.help @@ -0,0 +1,11 @@ +The snapping endpoint can be used to snap points to the edges of the street network for a specific means of transportation. + +You need to have a valid API key ('Web' menu > 'Configuration') or sign up at https://openrouteservice.org/sign-up/. + +Travel Mode: determines the profile used. + +Input layer: only Point layers are allowed. + +Radius: Radius in which to search. + +Current restriction limits for the openrouteservice API apply. diff --git a/ORStools/help/snap_from_point_layer_de.help b/ORStools/help/snap_from_point_layer_de.help new file mode 100644 index 00000000..daa2a96a --- /dev/null +++ b/ORStools/help/snap_from_point_layer_de.help @@ -0,0 +1,11 @@ +Der Snap-Algorithmus kann verwendet werden, um Punkte an den Rändern des Straßennetzes für ein bestimmtes Verkehrsmittel zu finden. + +Ein gültiger API-Key ('Web'-Menü > 'Dienst-Einstellungen') oder Registrierung unter https://openrouteservice.org/sign-up/ wird benötigt. + +Verkehrsmittel: bestimmt das genutzte Reise-Profil + +Eingabelayer: nur Punkt-Layer zugelassen. + +Radius: Radius in welchem gesucht wird. + +Es gelten die Restriktionen der openrouteservice-API. diff --git a/ORStools/proc/base_processing_algorithm.py b/ORStools/proc/base_processing_algorithm.py index 6245b328..5fdb925b 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 ["snap_from_point_layer", "snap_from_point"]: + 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/provider.py b/ORStools/proc/provider.py index 1492b140..df972757 100644 --- a/ORStools/proc/provider.py +++ b/ORStools/proc/provider.py @@ -38,6 +38,8 @@ from .isochrones_layer_proc import ORSIsochronesLayerAlgo from .isochrones_point_proc import ORSIsochronesPointAlgo from .matrix_proc import ORSMatrixAlgo +from .snap_layer_proc import ORSSnapLayerAlgo +from .snap_point_proc import ORSSnapPointAlgo class ORStoolsProvider(QgsProcessingProvider): @@ -63,6 +65,8 @@ def loadAlgorithms(self) -> None: self.addAlgorithm(ORSIsochronesLayerAlgo()) self.addAlgorithm(ORSIsochronesPointAlgo()) self.addAlgorithm(ORSMatrixAlgo()) + self.addAlgorithm(ORSSnapLayerAlgo()) + self.addAlgorithm(ORSSnapPointAlgo()) @staticmethod def icon() -> QIcon: diff --git a/ORStools/proc/snap_layer_proc.py b/ORStools/proc/snap_layer_proc.py new file mode 100644 index 00000000..06ca8eb9 --- /dev/null +++ b/ORStools/proc/snap_layer_proc.py @@ -0,0 +1,130 @@ +# -*- 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.PyQt.QtCore import QVariant +from qgis.core import ( + QgsProcessingParameterFeatureSource, + QgsProcessing, + QgsProcessingParameterNumber, + QgsProcessingContext, + QgsProcessingFeedback, + QgsWkbTypes, + QgsFields, + QgsCoordinateReferenceSystem, + QgsField, +) + +from ORStools.common import PROFILES +from ORStools.common.snap_core import get_snapped_point_features +from ORStools.proc.base_processing_algorithm import ORSBaseProcessingAlgorithm +from ORStools.utils import exceptions, logger, transform + + +# noinspection PyPep8Naming +class ORSSnapLayerAlgo(ORSBaseProcessingAlgorithm): + def __init__(self) -> None: + super().__init__() + self.ALGO_NAME: str = "snap_from_point_layer" + self.GROUP: str = "Snap" + self.IN_POINTS: str = "IN_POINTS" + self.RADIUS: str = "RADIUS" + self.PARAMETERS: list = [ + QgsProcessingParameterFeatureSource( + name=self.IN_POINTS, + description=self.tr("Input Point Layer"), + types=[QgsProcessing.SourceType.TypeVectorPoint], + ), + QgsProcessingParameterNumber( + name=self.RADIUS, + description=self.tr("Search Radius [m]"), + defaultValue=300, + ), + ] + + 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]] + + # Get parameter values + source = self.parameterAsSource(parameters, self.IN_POINTS, context) + radius = self.parameterAsDouble(parameters, self.RADIUS, context) + + sources_features = list(source.getFeatures()) + + x_former = transform.transformToWGS(source.sourceCrs()) + sources_features_x_formed = [ + x_former.transform(feat.geometry().asPoint()) for feat in sources_features + ] + + params = { + "locations": [[point.x(), point.y()] for point in sources_features_x_formed], + "radius": radius, + "id": None, + } + + sink_fields = QgsFields() + sink_fields.append(QgsField("NAME", QVariant.String)) + sink_fields.append(QgsField("SNAPPED_DISTANCE", QVariant.Double)) + + (sink, dest_id) = self.parameterAsSink( + parameters, + self.OUT, + context, + sink_fields, + QgsWkbTypes.Type.Point, + QgsCoordinateReferenceSystem.fromEpsgId(4326), + ) + + # Make request and catch ApiError + try: + response = ors_client.request("/v2/snap/" + profile, {}, post_json=params) + point_features = get_snapped_point_features(response) + + for feat in point_features: + sink.addFeature(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} + + def displayName(self) -> str: + """ + Algorithm name shown in QGIS toolbox + :return: + """ + return self.tr("Snap from Point Layer") diff --git a/ORStools/proc/snap_point_proc.py b/ORStools/proc/snap_point_proc.py new file mode 100644 index 00000000..942f1258 --- /dev/null +++ b/ORStools/proc/snap_point_proc.py @@ -0,0 +1,127 @@ +# -*- 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.PyQt.QtCore import QVariant +from qgis.core import ( + QgsProcessingParameterPoint, + QgsProcessingParameterNumber, + QgsProcessingContext, + QgsProcessingFeedback, + QgsWkbTypes, + QgsFields, + QgsCoordinateReferenceSystem, + QgsField, +) + +from ORStools.common import PROFILES +from ORStools.common.snap_core import get_snapped_point_features +from ORStools.proc.base_processing_algorithm import ORSBaseProcessingAlgorithm +from ORStools.utils import exceptions, logger + + +# noinspection PyPep8Naming +class ORSSnapPointAlgo(ORSBaseProcessingAlgorithm): + def __init__(self) -> None: + super().__init__() + self.ALGO_NAME: str = "snap_from_point" + self.GROUP: str = "Snap" + self.IN_POINT: str = "IN_POINT" + self.RADIUS: str = "RADIUS" + self.PARAMETERS: list = [ + QgsProcessingParameterPoint( + name=self.IN_POINT, + description=self.tr( + "Input Point from map canvas (mutually exclusive with layer option)" + ), + optional=True, + ), + QgsProcessingParameterNumber( + name=self.RADIUS, + description=self.tr("Search Radius [m]"), + defaultValue=300, + ), + ] + + crs_out = QgsCoordinateReferenceSystem.fromEpsgId(4326) + + 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]] + + # Get parameter values + point = self.parameterAsPoint(parameters, self.IN_POINT, context, self.crs_out) + radius = self.parameterAsDouble(parameters, self.RADIUS, context) + + params = { + "locations": [[round(point.x(), 6), round(point.y(), 6)]], + "radius": radius, + "id": None, + } + + sink_fields = QgsFields() + sink_fields.append(QgsField("NAME", QVariant.String)) + sink_fields.append(QgsField("SNAPPED_DISTANCE", QVariant.Double)) + + (sink, dest_id) = self.parameterAsSink( + parameters, + self.OUT, + context, + sink_fields, + QgsWkbTypes.Type.Point, + QgsCoordinateReferenceSystem.fromEpsgId(4326), + ) + + # Make request and catch ApiError + try: + response = ors_client.request("/v2/snap/" + profile, {}, post_json=params) + logger.log(str(response)) + point_features = get_snapped_point_features(response) + + for feat in point_features: + sink.addFeature(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} + + def displayName(self) -> str: + """ + Algorithm name shown in QGIS toolbox + :return: + """ + return self.tr("Snap from Point")