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")