From d48c21abc2e7c59fa9b43c265bd852524cf23bae Mon Sep 17 00:00:00 2001 From: Amandus Butzer Date: Mon, 7 Jun 2021 14:57:25 +0200 Subject: [PATCH] Refactor processing algorithm classes Removes redundancies in processing algorithm classes by creating the ORSBaseProcessingAlgorithm class. Moved to base class: - basic attributes and functions - provider and output parameter - algorithm and ors_client initialization Other notable changes: - moved from class to instance attribute approach - clean algorithm class naming - build_default_parameters moved to directions_core - easier access to input values in processAlgorithm() - rename of help files for dynamic loading - moved from lookupField() to indexOf() closes https://github.com/GIScience/orstools-qgis-plugin/issues/133 --- ORStools/common/directions_core.py | 32 +++ ORStools/common/isochrones_core.py | 4 +- ...lp => directions_from_points_1_layer.help} | 0 ...p => directions_from_points_2_layers.help} | 0 ...p => directions_from_polylines_layer.help} | 0 ..._layer.help => isochrones_from_layer.help} | 0 ..._point.help => isochrones_from_point.help} | 0 ...hm_matrix.help => matrix_from_layers.help} | 0 ORStools/proc/__init__.py | 5 - ORStools/proc/base_processing_algorithm.py | 148 +++++++++++ ORStools/proc/directions_lines_proc.py | 232 +++--------------- ORStools/proc/directions_points_layer_proc.py | 231 +++-------------- .../proc/directions_points_layers_proc.py | 212 ++++------------ ORStools/proc/isochrones_layer_proc.py | 223 ++++++----------- ORStools/proc/isochrones_point_proc.py | 171 ++++--------- ORStools/proc/matrix_proc.py | 180 ++++---------- ORStools/proc/provider.py | 40 +-- ORStools/utils/processing.py | 79 ++++++ 18 files changed, 571 insertions(+), 986 deletions(-) rename ORStools/help/{algorithm_directions_point.help => directions_from_points_1_layer.help} (100%) rename ORStools/help/{algorithm_directions_points.help => directions_from_points_2_layers.help} (100%) rename ORStools/help/{algorithm_directions_line.help => directions_from_polylines_layer.help} (100%) rename ORStools/help/{algorithm_isochrone_layer.help => isochrones_from_layer.help} (100%) rename ORStools/help/{algorithm_isochrone_point.help => isochrones_from_point.help} (100%) rename ORStools/help/{algorithm_matrix.help => matrix_from_layers.help} (100%) create mode 100644 ORStools/proc/base_processing_algorithm.py create mode 100644 ORStools/utils/processing.py diff --git a/ORStools/common/directions_core.py b/ORStools/common/directions_core.py index e63b685c..1e4ee5f7 100644 --- a/ORStools/common/directions_core.py +++ b/ORStools/common/directions_core.py @@ -34,6 +34,7 @@ QgsFeature, QgsFields, QgsField) +from typing import List from PyQt5.QtCore import QVariant @@ -191,3 +192,34 @@ def get_output_features_optimization(response, profile, from_value=None): ]) return feat + + +def build_default_parameters(preference: str, point_list: List[QgsPointXY] = None, coordinates: list = None) -> dict: + """ + Build default parameters for directions endpoint. Either uses a list of QgsPointXY to create the coordinates + passed in point_list or an existing coordinate list within the coordinates parameter. + TODO no optimal solution, maybe let get_request_point_features() return QgsPointXY as well to only use point_list + + :param preference: routing preference, shortest/fastest/recommended + :type preference: str + + :param point_list: + :type point_list: list of QgsPointXY + + :param coordinates: + :type coordinates: list + + :returns: parameters for directions endpoint + :rtype: dict + """ + coords = coordinates if coordinates else [[round(point.x(), 6), round(point.y(), 6)] for point in point_list] + params = { + 'coordinates': coords, + 'preference': preference, + 'geometry': 'true', + 'instructions': 'false', + 'elevation': True, + 'id': None + } + + return params diff --git a/ORStools/common/isochrones_core.py b/ORStools/common/isochrones_core.py index 99cc73b8..dc66cb3d 100644 --- a/ORStools/common/isochrones_core.py +++ b/ORStools/common/isochrones_core.py @@ -155,11 +155,11 @@ def stylePoly(self, layer): """ if self.dimension == 'time': - legend_suffix = ' mins' + legend_suffix = ' min' else: legend_suffix = ' m' - field = layer.fields().lookupField(self.field_dimension_name) + field = layer.fields().indexOf(self.field_dimension_name) unique_values = sorted(layer.uniqueValues(field)) colors = {0: QColor('#2b83ba'), diff --git a/ORStools/help/algorithm_directions_point.help b/ORStools/help/directions_from_points_1_layer.help similarity index 100% rename from ORStools/help/algorithm_directions_point.help rename to ORStools/help/directions_from_points_1_layer.help diff --git a/ORStools/help/algorithm_directions_points.help b/ORStools/help/directions_from_points_2_layers.help similarity index 100% rename from ORStools/help/algorithm_directions_points.help rename to ORStools/help/directions_from_points_2_layers.help diff --git a/ORStools/help/algorithm_directions_line.help b/ORStools/help/directions_from_polylines_layer.help similarity index 100% rename from ORStools/help/algorithm_directions_line.help rename to ORStools/help/directions_from_polylines_layer.help diff --git a/ORStools/help/algorithm_isochrone_layer.help b/ORStools/help/isochrones_from_layer.help similarity index 100% rename from ORStools/help/algorithm_isochrone_layer.help rename to ORStools/help/isochrones_from_layer.help diff --git a/ORStools/help/algorithm_isochrone_point.help b/ORStools/help/isochrones_from_point.help similarity index 100% rename from ORStools/help/algorithm_isochrone_point.help rename to ORStools/help/isochrones_from_point.help diff --git a/ORStools/help/algorithm_matrix.help b/ORStools/help/matrix_from_layers.help similarity index 100% rename from ORStools/help/algorithm_matrix.help rename to ORStools/help/matrix_from_layers.help diff --git a/ORStools/proc/__init__.py b/ORStools/proc/__init__.py index 145b03a5..1edf9987 100644 --- a/ORStools/proc/__init__.py +++ b/ORStools/proc/__init__.py @@ -26,8 +26,3 @@ * * ***************************************************************************/ """ -import os.path - -from ORStools import BASE_DIR - -HELP_DIR = os.path.join(BASE_DIR, 'help') diff --git a/ORStools/proc/base_processing_algorithm.py b/ORStools/proc/base_processing_algorithm.py new file mode 100644 index 00000000..3ff34703 --- /dev/null +++ b/ORStools/proc/base_processing_algorithm.py @@ -0,0 +1,148 @@ +# -*- 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 (QgsProcessingAlgorithm, + QgsProcessingParameterEnum, + QgsProcessingParameterFeatureSink, + QgsProcessingFeedback + ) +from typing import Any + +from PyQt5.QtGui import QIcon + +from ORStools import RESOURCE_PREFIX, __help__ +from ORStools.utils import configmanager +from ..common import client +from ..utils.processing import read_help_file + + +# noinspection PyPep8Naming +class ORSBaseProcessingAlgorithm(QgsProcessingAlgorithm): + """Base algorithm class for ORS algorithms""" + + def __init__(self): + """ + Default attributes used in all child classes + """ + super().__init__() + self.ALGO_NAME = '' + self.GROUP = '' + self.IN_PROVIDER = "INPUT_PROVIDER" + self.OUT = 'OUTPUT' + self.PARAMETERS = None + + def createInstance(self) -> Any: + """ + Returns instance of any child class + """ + return self.__class__() + + def group(self) -> str: + """ + Returns group name (Directions, Isochrones, Matrix) defined in child class + """ + return self.GROUP + + def groupId(self) -> str: + return self.GROUP.lower() + + def name(self) -> str: + """ + Returns algorithm name defined in child class + """ + return self.ALGO_NAME + + def shortHelpString(self): + """ + Displays the sidebar help in the algorithm window + """ + return read_help_file(file_name=f'{self.ALGO_NAME}.help') + + @staticmethod + def helpUrl(): + """ + Will be connected to the Help button in the Algorithm window + """ + return __help__ + + def displayName(self) -> str: + """ + Algorithm name shown in QGIS toolbox + :return: + """ + return self.ALGO_NAME.capitalize().replace('_', ' ') + + def icon(self) -> QIcon: + """ + Icon used for algorithm in QGIS toolbox + """ + return QIcon(RESOURCE_PREFIX + f'icon_{self.groupId()}.png') + + def provider_parameter(self) -> QgsProcessingParameterEnum: + """ + Parameter definition for provider, used in all child classes + """ + providers = [provider['name'] for provider in configmanager.read_config()['providers']] + return QgsProcessingParameterEnum( + self.IN_PROVIDER, + "Provider", + providers, + defaultValue=providers[0] + ) + + def output_parameter(self) -> QgsProcessingParameterFeatureSink: + """ + Parameter definition for output, used in all child classes + """ + return QgsProcessingParameterFeatureSink( + name=self.OUT, + description=self.GROUP, + ) + + @staticmethod + def _get_ors_client_from_provider(provider: str, feedback: QgsProcessingFeedback) -> client.Client: + """ + Connects client to provider and returns a client instance for requests to the ors API + """ + providers = configmanager.read_config()['providers'] + ors_provider = providers[provider] + ors_client = client.Client(ors_provider) + ors_client.overQueryLimit.connect(lambda: feedback.reportError("OverQueryLimit: Retrying...")) + return ors_client + + # noinspection PyUnusedLocal + def initAlgorithm(self, configuration): + """ + Combines default and algorithm parameters and adds them in order to the + algorithm dialog window. + """ + parameters = [self.provider_parameter()] + self.PARAMETERS + [self.output_parameter()] + for param in parameters: + self.addParameter( + param + ) diff --git a/ORStools/proc/directions_lines_proc.py b/ORStools/proc/directions_lines_proc.py index 3b447031..fba24732 100644 --- a/ORStools/proc/directions_lines_proc.py +++ b/ORStools/proc/directions_lines_proc.py @@ -27,163 +27,73 @@ ***************************************************************************/ """ -import os.path from qgis.core import (QgsWkbTypes, QgsCoordinateReferenceSystem, QgsProcessing, - QgsProcessingAlgorithm, QgsProcessingParameterField, QgsProcessingParameterFeatureSource, QgsProcessingParameterEnum, - QgsProcessingParameterFeatureSink, QgsProcessingParameterBoolean, QgsPointXY, ) -from PyQt5.QtGui import QIcon +from ORStools.common import directions_core, PROFILES, PREFERENCES +from ORStools.utils import transform, exceptions, logger +from .base_processing_algorithm import ORSBaseProcessingAlgorithm +from ..utils.processing import get_params_optimize -from ORStools import RESOURCE_PREFIX, __help__ -from ORStools.common import client, directions_core, PROFILES, PREFERENCES -from ORStools.utils import configmanager, transform, exceptions, logger -from . import HELP_DIR - -class ORSdirectionsLinesAlgo(QgsProcessingAlgorithm): +# noinspection PyPep8Naming +class ORSDirectionsLinesAlgorithm(ORSBaseProcessingAlgorithm): """Algorithm class for Directions Lines.""" - - ALGO_NAME = 'directions_from_polylines_layer' - ALGO_NAME_LIST = ALGO_NAME.split('_') - - IN_PROVIDER = "INPUT_PROVIDER" - IN_LINES = "INPUT_LINE_LAYER" - IN_FIELD = "INPUT_LAYER_FIELD" - IN_PROFILE = "INPUT_PROFILE" - IN_PREFERENCE = "INPUT_PREFERENCE" - IN_OPTIMIZE = "INPUT_OPTIMIZE" - IN_MODE = "INPUT_MODE" - OUT = 'OUTPUT' - - # noinspection PyUnusedLocal - def initAlgorithm(self, configuration): - - providers = [provider['name'] for provider in configmanager.read_config()['providers']] - self.addParameter( - QgsProcessingParameterEnum( - self.IN_PROVIDER, - "Provider", - providers, - defaultValue=providers[0] - ) - ) - - self.addParameter( + def __init__(self): + super().__init__() + self.ALGO_NAME = 'directions_from_polylines_layer' + self.GROUP = "Directions" + self.IN_LINES = "INPUT_LINE_LAYER" + self.IN_FIELD = "INPUT_LAYER_FIELD" + self.IN_PROFILE = "INPUT_PROFILE" + self.IN_PREFERENCE = "INPUT_PREFERENCE" + self.IN_OPTIMIZE = "INPUT_OPTIMIZE" + self.IN_MODE = "INPUT_MODE" + self.PARAMETERS = [ QgsProcessingParameterFeatureSource( name=self.IN_LINES, description="Input Line layer", types=[QgsProcessing.TypeVectorLine], - ) - ) - - self.addParameter( + ), QgsProcessingParameterField( name=self.IN_FIELD, description="Layer ID Field", parentLayerParameterName=self.IN_LINES, - ) - ) - - self.addParameter( + ), QgsProcessingParameterEnum( self.IN_PROFILE, "Travel mode", PROFILES, defaultValue=PROFILES[0] - ) - ) - - self.addParameter( + ), QgsProcessingParameterEnum( self.IN_PREFERENCE, "Travel preference", PREFERENCES, defaultValue=PREFERENCES[0] - ) - ) - - self.addParameter( + ), QgsProcessingParameterBoolean( name=self.IN_OPTIMIZE, description="Optimize waypoint order (except first and last)", defaultValue=False ) - ) - - self.addParameter( - QgsProcessingParameterFeatureSink( - name=self.OUT, - description="Output Layer", - ) - ) - - def group(self): - return "Directions" - - def groupId(self): - return 'directions' - - def name(self): - return self.ALGO_NAME - - def shortHelpString(self): - """Displays the sidebar help in the algorithm window""" - - file = os.path.join( - HELP_DIR, - 'algorithm_directions_line.help' - ) - with open(file, encoding='utf-8') as helpf: - msg = helpf.read() - - return msg - - def helpUrl(self): - """will be connected to the Help button in the Algorithm window""" - return __help__ - - def displayName(self): - return " ".join(map(lambda x: x.capitalize(), self.ALGO_NAME_LIST)) - - def icon(self): - return QIcon(RESOURCE_PREFIX + 'icon_directions.png') - - def createInstance(self): - return ORSdirectionsLinesAlgo() + ] def processAlgorithm(self, parameters, context, feedback): - # Init ORS client + ors_client = self._get_ors_client_from_provider(parameters[self.IN_PROVIDER], feedback) - providers = configmanager.read_config()['providers'] - provider = providers[self.parameterAsEnum(parameters, self.IN_PROVIDER, context)] - clnt = client.Client(provider) - clnt.overQueryLimit.connect(lambda : feedback.reportError("OverQueryLimit: Retrying...")) + profile = dict(enumerate(PROFILES))[parameters[self.IN_PROFILE]] - profile = PROFILES[self.parameterAsEnum( - parameters, - self.IN_PROFILE, - context - )] + preference = dict(enumerate(PREFERENCES))[parameters[self.IN_PREFERENCE]] - preference = PREFERENCES[self.parameterAsEnum( - parameters, - self.IN_PREFERENCE, - context - )] - - optimize = self.parameterAsBool( - parameters, - self.IN_OPTIMIZE, - context - ) + optimize = parameters[self.IN_OPTIMIZE] # Get parameter values source = self.parameterAsSource( @@ -192,16 +102,13 @@ def processAlgorithm(self, parameters, context, feedback): context ) - source_field_name = self.parameterAsString( - parameters, - self.IN_FIELD, - context - ) + source_field_name = parameters[self.IN_FIELD] (sink, dest_id) = self.parameterAsSink(parameters, self.OUT, context, - directions_core.get_fields(from_type=source.fields().field(source_field_name).type(), - from_name=source_field_name, - line=True), + directions_core.get_fields( + from_type=source.fields().field(source_field_name).type(), + from_name=source_field_name, + line=True), source.wkbType(), QgsCoordinateReferenceSystem.fromEpsgId(4326)) count = source.featureCount() @@ -213,8 +120,8 @@ def processAlgorithm(self, parameters, context, feedback): try: if optimize: - params = self._get_params_optimize(line, profile) - response = clnt.request('/optimization', {}, post_json=params) + params = get_params_optimize(line, profile) + response = ors_client.request('/optimization', {}, post_json=params) sink.addFeature(directions_core.get_output_features_optimization( response, @@ -222,8 +129,8 @@ def processAlgorithm(self, parameters, context, feedback): from_value=field_value )) else: - params = self._get_params_directions(line, preference) - response = clnt.request('/v2/directions/' + profile + '/geojson', {}, post_json=params) + params = directions_core.build_default_parameters(preference, point_list=line) + response = ors_client.request('/v2/directions/' + profile + '/geojson', {}, post_json=params) sink.addFeature(directions_core.get_output_feature_directions( response, @@ -256,7 +163,7 @@ def _get_sorted_lines(layer, field_name): :type field_name: str """ # First get coordinate transformer - xformer = transform.transformToWGS(layer.sourceCrs()) + x_former = transform.transformToWGS(layer.sourceCrs()) for feat in sorted(layer.getFeatures(), key=lambda f: f.id()): line = None @@ -265,72 +172,9 @@ def _get_sorted_lines(layer, field_name): if layer.wkbType() == QgsWkbTypes.MultiLineString: # TODO: only takes the first polyline geometry from the multiline geometry currently # Loop over all polyline geometries - line = [xformer.transform(QgsPointXY(point)) for point in feat.geometry().asMultiPolyline()[0]] + line = [x_former.transform(QgsPointXY(point)) for point in feat.geometry().asMultiPolyline()[0]] elif layer.wkbType() == QgsWkbTypes.LineString: - line = [xformer.transform(QgsPointXY(point)) for point in feat.geometry().asPolyline()] + line = [x_former.transform(QgsPointXY(point)) for point in feat.geometry().asPolyline()] yield line, field_value - - @staticmethod - def _get_params_directions(line, preference): - """ - Build parameters for optimization endpoint - - :param line: individual polyline points - :type line: list of QgsPointXY - - :param preference: routing preference, shortest/fastest/recommended - :type preference: str - - :returns: parameters for optimization endpoint - :rtype: dict - """ - - params = { - 'coordinates': [[round(point.x(), 6), round(point.y(), 6)] for point in line], - 'preference': preference, - 'geometry': 'true', - 'format': 'geojson', - 'instructions': 'false', - 'elevation': True, - 'id': None - } - - return params - - @staticmethod - def _get_params_optimize(line, profile): - """ - Build parameters for optimization endpoint - - :param line: individual polyline points - :type line: list of QgsPointXY - - :param profile: transport profile to be used - :type profile: str - - :returns: parameters for optimization endpoint - :rtype: dict - """ - - start = line.pop(0) - end = line.pop(-1) - - params = { - 'jobs': list(), - 'vehicles': [{ - "id": 0, - "profile": profile, - "start": [start.x(), start.y()], - "end": [end.x(), end.y()] - }], - 'options': {'g': True} - } - for point in line: - params['jobs'].append({ - "location": [point.x(), point.y()], - "id": line.index(point) - }) - - return params diff --git a/ORStools/proc/directions_points_layer_proc.py b/ORStools/proc/directions_points_layer_proc.py index d8f18806..6d933cc2 100644 --- a/ORStools/proc/directions_points_layer_proc.py +++ b/ORStools/proc/directions_points_layer_proc.py @@ -27,163 +27,73 @@ ***************************************************************************/ """ -import os.path from qgis.core import (QgsWkbTypes, QgsCoordinateReferenceSystem, QgsProcessing, - QgsProcessingAlgorithm, QgsProcessingParameterField, QgsProcessingParameterFeatureSource, QgsProcessingParameterEnum, - QgsProcessingParameterFeatureSink, QgsProcessingParameterBoolean, QgsPointXY, ) -from PyQt5.QtGui import QIcon +from ORStools.common import directions_core, PROFILES, PREFERENCES +from ORStools.utils import transform, exceptions, logger +from .base_processing_algorithm import ORSBaseProcessingAlgorithm +from ..utils.processing import get_params_optimize -from ORStools import RESOURCE_PREFIX, __help__ -from ORStools.common import client, directions_core, PROFILES, PREFERENCES -from ORStools.utils import configmanager, transform, exceptions, logger -from . import HELP_DIR - -class ORSdirectionsPointsLayerAlgo(QgsProcessingAlgorithm): +# noinspection PyPep8Naming +class ORSDirectionsPointsLayerAlgo(ORSBaseProcessingAlgorithm): """Algorithm class for Directions Lines.""" - - ALGO_NAME = 'directions_from_points_1_layer' - ALGO_NAME_LIST = ALGO_NAME.split('_') - - IN_PROVIDER = "INPUT_PROVIDER" - IN_POINTS = "INPUT_POINT_LAYER" - IN_FIELD = "INPUT_LAYER_FIELD" - IN_PROFILE = "INPUT_PROFILE" - IN_PREFERENCE = "INPUT_PREFERENCE" - IN_OPTIMIZE = "INPUT_OPTIMIZE" - IN_MODE = "INPUT_MODE" - OUT = 'OUTPUT' - - # noinspection PyUnusedLocal - def initAlgorithm(self, configuration): - - providers = [provider['name'] for provider in configmanager.read_config()['providers']] - self.addParameter( - QgsProcessingParameterEnum( - self.IN_PROVIDER, - "Provider", - providers, - defaultValue=providers[0] - ) - ) - - self.addParameter( + def __init__(self): + super().__init__() + self.ALGO_NAME = 'directions_from_points_1_layer' + self.GROUP = "Directions" + self.IN_POINTS = "INPUT_POINT_LAYER" + self.IN_FIELD = "INPUT_LAYER_FIELD" + self.IN_PROFILE = "INPUT_PROFILE" + self.IN_PREFERENCE = "INPUT_PREFERENCE" + self.IN_OPTIMIZE = "INPUT_OPTIMIZE" + self.IN_MODE = "INPUT_MODE" + self.PARAMETERS = [ QgsProcessingParameterFeatureSource( name=self.IN_POINTS, description="Input (Multi)Point layer", types=[QgsProcessing.TypeVectorPoint], - ) - ) - - self.addParameter( + ), QgsProcessingParameterField( name=self.IN_FIELD, description="Layer ID Field", parentLayerParameterName=self.IN_POINTS, - ) - ) - - self.addParameter( + ), QgsProcessingParameterEnum( self.IN_PROFILE, "Travel mode", PROFILES, defaultValue=PROFILES[0] - ) - ) - - self.addParameter( + ), QgsProcessingParameterEnum( self.IN_PREFERENCE, "Travel preference", PREFERENCES, defaultValue=PREFERENCES[0] - ) - ) - - self.addParameter( + ), QgsProcessingParameterBoolean( name=self.IN_OPTIMIZE, description="Optimize waypoint order (except first and last)", defaultValue=False ) - ) - - self.addParameter( - QgsProcessingParameterFeatureSink( - name=self.OUT, - description="Output Layer", - ) - ) - - def group(self): - return "Directions" - - def groupId(self): - return 'directions' - - def name(self): - return self.ALGO_NAME - - def shortHelpString(self): - """Displays the sidebar help in the algorithm window""" - - file = os.path.join( - HELP_DIR, - 'algorithm_directions_line.help' - ) - with open(file, encoding='utf-8') as helpf: - msg = helpf.read() - - return msg - - def helpUrl(self): - """will be connected to the Help button in the Algorithm window""" - return __help__ - - def displayName(self): - return " ".join(map(lambda x: x.capitalize(), self.ALGO_NAME_LIST)) - - def icon(self): - return QIcon(RESOURCE_PREFIX + 'icon_directions.png') - - def createInstance(self): - return ORSdirectionsPointsLayerAlgo() + ] def processAlgorithm(self, parameters, context, feedback): - # Init ORS client + ors_client = self._get_ors_client_from_provider(parameters[self.IN_PROVIDER], feedback) - providers = configmanager.read_config()['providers'] - provider = providers[self.parameterAsEnum(parameters, self.IN_PROVIDER, context)] - clnt = client.Client(provider) - clnt.overQueryLimit.connect(lambda : feedback.reportError("OverQueryLimit: Retrying...")) + profile = dict(enumerate(PROFILES))[parameters[self.IN_PROFILE]] - profile = PROFILES[self.parameterAsEnum( - parameters, - self.IN_PROFILE, - context - )] + preference = dict(enumerate(PREFERENCES))[parameters[self.IN_PREFERENCE]] - preference = PREFERENCES[self.parameterAsEnum( - parameters, - self.IN_PREFERENCE, - context - )] - - optimize = self.parameterAsBool( - parameters, - self.IN_OPTIMIZE, - context - ) + optimize = parameters[self.IN_OPTIMIZE] # Get parameter values source = self.parameterAsSource( @@ -192,28 +102,25 @@ def processAlgorithm(self, parameters, context, feedback): context ) - source_field_name = self.parameterAsString( - parameters, - self.IN_FIELD, - context - ) + source_field_name = parameters[self.IN_FIELD] (sink, dest_id) = self.parameterAsSink(parameters, self.OUT, context, - directions_core.get_fields(from_type=source.fields().field(source_field_name).type(), - from_name=source_field_name, - line=True), + directions_core.get_fields( + from_type=source.fields().field(source_field_name).type(), + from_name=source_field_name, + line=True), QgsWkbTypes.LineString, QgsCoordinateReferenceSystem.fromEpsgId(4326)) count = source.featureCount() input_points = list() from_values = list() - xformer = transform.transformToWGS(source.sourceCrs()) + x_former = transform.transformToWGS(source.sourceCrs()) if source.wkbType() == QgsWkbTypes.Point: points = list() for feat in sorted(source.getFeatures(), key=lambda f: f.id()): - points.append(xformer.transform(QgsPointXY(feat.geometry().asPoint()))) + points.append(x_former.transform(QgsPointXY(feat.geometry().asPoint()))) input_points.append(points) from_values.append(None) elif source.wkbType() == QgsWkbTypes.MultiPoint: @@ -221,7 +128,7 @@ def processAlgorithm(self, parameters, context, feedback): for feat in sorted(source.getFeatures(), key=lambda f: f.id()): points = list() for point in feat.geometry().asMultiPoint(): - points.append(xformer.transform(QgsPointXY(point))) + points.append(x_former.transform(QgsPointXY(point))) input_points.append(points) from_values.append(feat[source_field_name]) @@ -232,8 +139,8 @@ def processAlgorithm(self, parameters, context, feedback): try: if optimize: - params = self._get_params_optimize(points, profile) - response = clnt.request('/optimization', {}, post_json=params) + params = get_params_optimize(points, profile) + response = ors_client.request('/optimization', {}, post_json=params) sink.addFeature(directions_core.get_output_features_optimization( response, @@ -241,8 +148,8 @@ def processAlgorithm(self, parameters, context, feedback): from_value=from_value )) else: - params = self._get_params_directions(points, preference) - response = clnt.request('/v2/directions/' + profile + '/geojson', {}, post_json=params) + params = directions_core.build_default_parameters(preference, point_list=points) + response = ors_client.request('/v2/directions/' + profile + '/geojson', {}, post_json=params) sink.addFeature(directions_core.get_output_feature_directions( response, @@ -261,65 +168,3 @@ def processAlgorithm(self, parameters, context, feedback): feedback.setProgress(int(100.0 / count * num)) return {self.OUT: dest_id} - - @staticmethod - def _get_params_directions(points, preference): - """ - Build parameters for optimization endpoint - - :param points: individual points - :type points: list of QgsPointXY - - :param preference: routing preference, shortest/fastest/recommended - :type preference: str - - :returns: parameters for optimization endpoint - :rtype: dict - """ - - params = { - 'coordinates': [[round(point.x(), 6), round(point.y(), 6)] for point in points], - 'preference': preference, - 'geometry': 'true', - 'instructions': 'false', - 'elevation': True, - 'id': None - } - - return params - - @staticmethod - def _get_params_optimize(points, profile): - """ - Build parameters for optimization endpoint - - :param points: individual points list - :type points: list of QgsPointXY - - :param profile: transport profile to be used - :type profile: str - - :returns: parameters for optimization endpoint - :rtype: dict - """ - - start = points.pop(0) - end = points.pop(-1) - - params = { - 'jobs': list(), - 'vehicles': [{ - "id": 0, - "profile": profile, - "start": [start.x(), start.y()], - "end": [end.x(), end.y()] - }], - 'options': {'g': True} - } - for point in points: - params['jobs'].append({ - "location": [point.x(), point.y()], - "id": points.index(point) - }) - - return params diff --git a/ORStools/proc/directions_points_layers_proc.py b/ORStools/proc/directions_points_layers_proc.py index 1a6b2580..29037898 100644 --- a/ORStools/proc/directions_points_layers_proc.py +++ b/ORStools/proc/directions_points_layers_proc.py @@ -27,184 +27,85 @@ ***************************************************************************/ """ -import os.path from qgis.core import (QgsWkbTypes, QgsCoordinateReferenceSystem, QgsProcessing, - QgsProcessingAlgorithm, QgsProcessingParameterField, QgsProcessingParameterFeatureSource, QgsProcessingParameterEnum, - QgsProcessingParameterFeatureSink, ) -from PyQt5.QtGui import QIcon - -from ORStools import RESOURCE_PREFIX, __help__ -from ORStools.common import client, directions_core, PROFILES, PREFERENCES -from ORStools.utils import configmanager, transform, exceptions, logger -from . import HELP_DIR - - -class ORSdirectionsPointsLayersAlgo(QgsProcessingAlgorithm): - # TODO: create base algorithm class common to all modules - - ALGO_NAME = 'directions_from_points_2_layers' - ALGO_NAME_LIST = ALGO_NAME.split('_') - MODE_SELECTION = ['Row-by-Row', 'All-by-All'] - - IN_PROVIDER = "INPUT_PROVIDER" - IN_START = "INPUT_START_LAYER" - IN_START_FIELD = "INPUT_START_FIELD" - IN_END = "INPUT_END_LAYER" - IN_END_FIELD = "INPUT_END_FIELD" - IN_PROFILE = "INPUT_PROFILE" - IN_PREFERENCE = "INPUT_PREFERENCE" - IN_MODE = "INPUT_MODE" - OUT = 'OUTPUT' - - # noinspection PyUnusedLocal - def initAlgorithm(self, configuration): - - providers = [provider['name'] for provider in configmanager.read_config()['providers']] - self.addParameter( - QgsProcessingParameterEnum( - self.IN_PROVIDER, - "Provider", - providers, - defaultValue=providers[0] - ) - ) - - self.addParameter( +from ORStools.common import directions_core, PROFILES, PREFERENCES +from ORStools.utils import transform, exceptions, logger +from .base_processing_algorithm import ORSBaseProcessingAlgorithm + + +# noinspection PyPep8Naming +class ORSDirectionsPointsLayersAlgo(ORSBaseProcessingAlgorithm): + + def __init__(self): + super().__init__() + self.ALGO_NAME = 'directions_from_points_2_layers' + self.GROUP = "Directions" + self.MODE_SELECTION: list = ['Row-by-Row', 'All-by-All'] + self.IN_START = "INPUT_START_LAYER" + self.IN_START_FIELD = "INPUT_START_FIELD" + self.IN_END = "INPUT_END_LAYER" + self.IN_END_FIELD = "INPUT_END_FIELD" + self.IN_PROFILE = "INPUT_PROFILE" + self.IN_PREFERENCE = "INPUT_PREFERENCE" + self.IN_MODE = "INPUT_MODE" + self.PARAMETERS = [ QgsProcessingParameterFeatureSource( name=self.IN_START, description="Input Start Point layer", types=[QgsProcessing.TypeVectorPoint], - ) - ) - - self.addParameter( + ), QgsProcessingParameterField( name=self.IN_START_FIELD, description="Start ID Field (can be used for joining)", parentLayerParameterName=self.IN_START, - ) - ) - - self.addParameter( + ), QgsProcessingParameterFeatureSource( name=self.IN_END, description="Input End Point layer", types=[QgsProcessing.TypeVectorPoint], - ) - ) - - self.addParameter( + ), QgsProcessingParameterField( name=self.IN_END_FIELD, description="End ID Field (can be used for joining)", parentLayerParameterName=self.IN_END, - ) - ) - - self.addParameter( + ), QgsProcessingParameterEnum( self.IN_PROFILE, "Travel mode", PROFILES, defaultValue=PROFILES[0] - ) - ) - - self.addParameter( + ), QgsProcessingParameterEnum( self.IN_PREFERENCE, "Travel preference", PREFERENCES, defaultValue=PREFERENCES[0] - ) - ) - - self.addParameter( + ), QgsProcessingParameterEnum( self.IN_MODE, "Layer mode", self.MODE_SELECTION, defaultValue=self.MODE_SELECTION[0] ) - ) - - self.addParameter( - QgsProcessingParameterFeatureSink( - name=self.OUT, - description="Directions", - ) - ) - - def group(self): - return "Directions" - - def groupId(self): - return 'directions' - - def name(self): - return self.ALGO_NAME - - def shortHelpString(self): - """Displays the sidebar help in the algorithm window""" - - file = os.path.join( - HELP_DIR, - 'algorithm_directions_points.help' - ) - with open(file, encoding='utf-8') as helpf: - msg = helpf.read() - - return msg - - def helpUrl(self): - """will be connected to the Help button in the Algorithm window""" - return __help__ - - def displayName(self): - return " ".join(map(lambda x: x.capitalize(), self.ALGO_NAME_LIST)) - - def icon(self): - return QIcon(RESOURCE_PREFIX + 'icon_directions.png') - - def createInstance(self): - return ORSdirectionsPointsLayersAlgo() + ] # TODO: preprocess parameters to options the range cleanup below: # https://www.qgis.org/pyqgis/master/core/Processing/QgsProcessingAlgorithm.html#qgis.core.QgsProcessingAlgorithm.preprocessParameters - def processAlgorithm(self, parameters, context, feedback): + ors_client = self._get_ors_client_from_provider(parameters[self.IN_PROVIDER], feedback) - # Init ORS client + profile = dict(enumerate(PROFILES))[parameters[self.IN_PROFILE]] - providers = configmanager.read_config()['providers'] - provider = providers[self.parameterAsEnum(parameters, self.IN_PROVIDER, context)] - clnt = client.Client(provider) - clnt.overQueryLimit.connect(lambda : feedback.reportError("OverQueryLimit: Retrying...")) + preference = dict(enumerate(PREFERENCES))[parameters[self.IN_PREFERENCE]] - profile = PROFILES[self.parameterAsEnum( - parameters, - self.IN_PROFILE, - context - )] - - preference = PREFERENCES[self.parameterAsEnum( - parameters, - self.IN_PREFERENCE, - context - )] - - mode = self.MODE_SELECTION[self.parameterAsEnum( - parameters, - self.IN_MODE, - context - )] + mode = dict(enumerate(self.MODE_SELECTION))[parameters[self.IN_MODE]] # Get parameter values source = self.parameterAsSource( @@ -212,35 +113,18 @@ def processAlgorithm(self, parameters, context, feedback): self.IN_START, context ) - source_field_name = self.parameterAsString( - parameters, - self.IN_START_FIELD, - context - ) + source_field_name = parameters[self.IN_START_FIELD] + destination = self.parameterAsSource( parameters, self.IN_END, context ) - destination_field_name = self.parameterAsString( - parameters, - self.IN_END_FIELD, - context - ) + destination_field_name = parameters[self.IN_END_FIELD] # Get fields from field name - source_field_id = source.fields().lookupField(source_field_name) - source_field = source.fields().field(source_field_id) - destination_field_id = destination.fields().lookupField(destination_field_name) - destination_field = destination.fields().field(destination_field_id) - - params = { - 'preference': preference, - 'geometry': 'true', - 'instructions': 'false', - 'elevation': True, - 'id': None - } + source_field = source.fields().field(source_field_name) + destination_field = destination.fields().field(destination_field_name) route_dict = self._get_route_dict( source, @@ -255,7 +139,8 @@ def processAlgorithm(self, parameters, context, feedback): route_count = source.featureCount() * destination.featureCount() (sink, dest_id) = self.parameterAsSink(parameters, self.OUT, context, - directions_core.get_fields(source_field.type(), destination_field.type()), + directions_core.get_fields(source_field.type(), + destination_field.type()), QgsWkbTypes.LineString, QgsCoordinateReferenceSystem.fromEpsgId(4326)) @@ -265,10 +150,10 @@ def processAlgorithm(self, parameters, context, feedback): if feedback.isCanceled(): break - params['coordinates'] = coordinates + params = directions_core.build_default_parameters(preference, coordinates=coordinates) try: - response = clnt.request('/v2/directions/' + profile + '/geojson', {}, post_json=params) + response = ors_client.request('/v2/directions/' + profile + '/geojson', {}, post_json=params) except (exceptions.ApiError, exceptions.InvalidKey, exceptions.GenericServerError) as e: @@ -290,7 +175,8 @@ def processAlgorithm(self, parameters, context, feedback): return {self.OUT: dest_id} - def _get_route_dict(self, source, source_field, destination, destination_field): + @staticmethod + def _get_route_dict(source, source_field, destination, destination_field): """ Compute route_dict from input layer. @@ -312,17 +198,17 @@ def _get_route_dict(self, source, source_field, destination, destination_field): route_dict = dict() source_feats = list(source.getFeatures()) - xformer_source = transform.transformToWGS(source.sourceCrs()) + x_former_source = transform.transformToWGS(source.sourceCrs()) route_dict['start'] = dict( - geometries=[xformer_source.transform(feat.geometry().asPoint()) for feat in source_feats], - values= [feat.attribute(source_field.name()) for feat in source_feats], + geometries=[x_former_source.transform(feat.geometry().asPoint()) for feat in source_feats], + values=[feat.attribute(source_field.name()) for feat in source_feats], ) destination_feats = list(destination.getFeatures()) - xformer_destination = transform.transformToWGS(destination.sourceCrs()) + x_former_destination = transform.transformToWGS(destination.sourceCrs()) route_dict['end'] = dict( - geometries=[xformer_destination.transform(feat.geometry().asPoint()) for feat in destination_feats], - values= [feat.attribute(destination_field.name()) for feat in destination_feats], + geometries=[x_former_destination.transform(feat.geometry().asPoint()) for feat in destination_feats], + values=[feat.attribute(destination_field.name()) for feat in destination_feats], ) return route_dict diff --git a/ORStools/proc/isochrones_layer_proc.py b/ORStools/proc/isochrones_layer_proc.py index 911342eb..94297bca 100644 --- a/ORStools/proc/isochrones_layer_proc.py +++ b/ORStools/proc/isochrones_layer_proc.py @@ -27,177 +27,89 @@ ***************************************************************************/ """ -import os.path -from copy import deepcopy from qgis.core import (QgsWkbTypes, QgsCoordinateReferenceSystem, - QgsField, QgsProcessing, QgsProcessingUtils, QgsProcessingException, - QgsProcessingAlgorithm, QgsProcessingParameterField, QgsProcessingParameterFeatureSource, QgsProcessingParameterString, - QgsProcessingParameterEnum, - QgsProcessingParameterFeatureSink + QgsProcessingParameterEnum ) -from PyQt5.QtGui import QIcon - -from ORStools import RESOURCE_PREFIX, __help__ -from ORStools.common import client, isochrones_core, PROFILES, DIMENSIONS -from ORStools.utils import transform, exceptions, configmanager, logger -from . import HELP_DIR - - -class ORSisochronesLayerAlgo(QgsProcessingAlgorithm): - # TODO: create base algorithm class common to all modules - - ALGO_NAME = 'isochrones_from_layer' - ALGO_NAME_LIST = ALGO_NAME.split('_') - - IN_PROVIDER = "INPUT_PROVIDER" - IN_POINTS = "INPUT_POINT_LAYER" - IN_FIELD = "INPUT_FIELD" - IN_PROFILE = "INPUT_PROFILE" - IN_METRIC = 'INPUT_METRIC' - IN_RANGES = 'INPUT_RANGES' - IN_KEY = 'INPUT_APIKEY' - IN_DIFFERENCE = 'INPUT_DIFFERENCE' - OUT = 'OUTPUT' - - # Save some important references - isochrones = isochrones_core.Isochrones() - dest_id = None - crs_out = QgsCoordinateReferenceSystem.fromEpsgId(4326) - # difference = None - - # noinspection PyUnusedLocal - def initAlgorithm(self, configuration): - - providers = [provider['name'] for provider in configmanager.read_config()['providers']] - self.addParameter( - QgsProcessingParameterEnum( - self.IN_PROVIDER, - "Provider", - providers, - defaultValue=providers[0] - ) - ) - - self.addParameter( +from ORStools.common import isochrones_core, PROFILES, DIMENSIONS +from ORStools.proc.base_processing_algorithm import ORSBaseProcessingAlgorithm +from ORStools.utils import transform, exceptions, logger + + +# noinspection PyPep8Naming +class ORSIsochronesLayerAlgo(ORSBaseProcessingAlgorithm): + def __init__(self): + super().__init__() + self.ALGO_NAME = 'isochrones_from_layer' + self.GROUP = 'Isochrones' + + self.IN_POINTS = "INPUT_POINT_LAYER" + self.IN_FIELD = "INPUT_FIELD" + self.IN_PROFILE = "INPUT_PROFILE" + self.IN_METRIC = 'INPUT_METRIC' + self.IN_RANGES = 'INPUT_RANGES' + self.IN_KEY = 'INPUT_APIKEY' + self.IN_DIFFERENCE = 'INPUT_DIFFERENCE' + self.PARAMETERS = [ QgsProcessingParameterFeatureSource( name=self.IN_POINTS, description="Input Point layer", types=[QgsProcessing.TypeVectorPoint] - ) - ) - - # self.addParameter( - # QgsProcessingParameterBoolean( - # name=self.IN_DIFFERENCE, - # description="Dissolve and calculate isochrone difference", - # ) - # ) - - self.addParameter( + ), + # QgsProcessingParameterBoolean( + # name=self.IN_DIFFERENCE, + # description="Dissolve and calculate isochrone difference", + # ) QgsProcessingParameterField( name=self.IN_FIELD, description="Input layer ID Field (mutually exclusive with Point option)", parentLayerParameterName=self.IN_POINTS, optional=True - ) - ) - - self.addParameter( + ), QgsProcessingParameterEnum( self.IN_PROFILE, "Travel mode", PROFILES, defaultValue=PROFILES[0] - ) - ) - - self.addParameter( + ), QgsProcessingParameterEnum( name=self.IN_METRIC, description="Dimension", options=DIMENSIONS, defaultValue=DIMENSIONS[0] - ) - ) - - self.addParameter( + ), QgsProcessingParameterString( name=self.IN_RANGES, - description="Comma-separated ranges [mins or m]", + description="Comma-separated ranges [min or m]", defaultValue="5, 10" ) - ) - - self.addParameter( - QgsProcessingParameterFeatureSink( - name=self.OUT, - description="Isochrones", - createByDefault=False - ) - ) - - def group(self): - return "Isochrones" - - def groupId(self): - return 'isochrones' - - def name(self): - return self.ALGO_NAME - - def shortHelpString(self): - """Displays the sidebar help in the algorithm window""" - - file = os.path.join( - HELP_DIR, - 'algorithm_isochrone_layer.help' - ) - with open(file, encoding='utf-8') as helpf: - msg = helpf.read() - - return msg - - def helpUrl(self): - """will be connected to the Help button in the Algorithm window""" - return __help__ - - def displayName(self): - return " ".join(map(lambda x: x.capitalize(), self.ALGO_NAME_LIST)) - - def icon(self): - return QIcon(RESOURCE_PREFIX + 'icon_isochrones.png') + ] - def createInstance(self): - return ORSisochronesLayerAlgo() + # Save some important references + # TODO bad style, refactor + isochrones = isochrones_core.Isochrones() + dest_id = None + crs_out = QgsCoordinateReferenceSystem.fromEpsgId(4326) + # difference = None # TODO: preprocess parameters to options the range cleanup below: # https://www.qgis.org/pyqgis/master/core/Processing/QgsProcessingAlgorithm.html#qgis.core.QgsProcessingAlgorithm.prepareAlgorithm - def processAlgorithm(self, parameters, context, feedback): - # Init ORS client - providers = configmanager.read_config()['providers'] - provider = providers[self.parameterAsEnum(parameters, self.IN_PROVIDER, context)] - clnt = client.Client(provider) - clnt.overQueryLimit.connect(lambda : feedback.reportError("OverQueryLimit: Retrying...")) + ors_client = self._get_ors_client_from_provider(parameters[self.IN_PROVIDER], feedback) - params = dict() - params['attributes'] = ['total_pop'] + profile = dict(enumerate(PROFILES))[parameters[self.IN_PROFILE]] + dimension = dict(enumerate(DIMENSIONS))[parameters[self.IN_METRIC]] - profile = PROFILES[self.parameterAsEnum(parameters, self.IN_PROFILE, context)] - params['range_type'] = dimension = DIMENSIONS[self.parameterAsEnum(parameters, self.IN_METRIC, context)] - - factor = 60 if params['range_type'] == 'time' else 1 - ranges_raw = self.parameterAsString(parameters, self.IN_RANGES, context) + factor = 60 if dimension == 'time' else 1 + ranges_raw = parameters[self.IN_RANGES] ranges_proc = [x * factor for x in map(int, ranges_raw.split(','))] - params['range'] = ranges_proc # self.difference = self.parameterAsBool(parameters, self.IN_DIFFERENCE, context) source = self.parameterAsSource(parameters, self.IN_POINTS, context) @@ -205,13 +117,14 @@ def processAlgorithm(self, parameters, context, feedback): # Make the actual requests requests = [] if source.wkbType() == 4: - raise QgsProcessingException("TypeError: Multipoint Layers are not accepted. Please convert to single geometry layer.") + raise QgsProcessingException( + "TypeError: Multipoint Layers are not accepted. Please convert to single geometry layer.") # Get ID field properties - id_field_name = self.parameterAsString(parameters, self.IN_FIELD, context) - id_field_id = source.fields().lookupField(id_field_name) + id_field_name = parameters[self.IN_FIELD] + id_field_id = source.fields().indexOf(id_field_name) - # LookupField will return -1 if the name cannot be found. + # indexOf will return -1 if the name cannot be found. # Try the first field in this case. if id_field_id == -1: id_field_id = 0 @@ -221,25 +134,27 @@ def processAlgorithm(self, parameters, context, feedback): id_field = source.fields().field(id_field_id) id_field_name = source.fields().field(id_field_id).name() self.isochrones.set_parameters(profile, dimension, factor, id_field.type(), id_field_name) - except KeyError: # Scratch layers don't neccessarily have fields + except KeyError: # Scratch layers don't necessarily have fields self.isochrones.set_parameters(profile, dimension, factor) - for properties in self.get_sorted_feature_parameters(source): + for locations, id_value in self.get_sorted_feature_parameters(source, id_field_name): # Stop the algorithm if cancel button has been clicked if feedback.isCanceled(): break - # Get transformed coordinates and feature - params['locations'], feat = properties - try: - params['id'] = feat[id_field_name] - except KeyError: - params['id'] = None - requests.append(deepcopy(params)) + requests.append({ + "locations": locations, + "range_type": dimension, + "range": ranges_proc, + "attributes": ['total_pop'], + "id": id_value, + }) (sink, self.dest_id) = self.parameterAsSink(parameters, self.OUT, context, self.isochrones.get_fields(), - QgsWkbTypes.Polygon, # Needs Multipolygon if difference parameter will ever be reactivated + QgsWkbTypes.Polygon, + # Needs Multipolygon if difference parameter will ever be + # reactivated self.crs_out) for num, params in enumerate(requests): @@ -249,7 +164,7 @@ def processAlgorithm(self, parameters, context, feedback): # If feature causes error, report and continue with next try: # Populate features from response - response = clnt.request('/v2/isochrones/' + profile, {}, post_json=params) + response = ors_client.request('/v2/isochrones/' + profile, {}, post_json=params) for isochrone in self.isochrones.get_features(response, params['id']): sink.addFeature(isochrone) @@ -269,24 +184,28 @@ def processAlgorithm(self, parameters, context, feedback): def postProcessAlgorithm(self, context, feedback): """Style polygon layer in post-processing step.""" # processed_layer = self.isochrones.calculate_difference(self.dest_id, context) - processed_layer= QgsProcessingUtils.mapLayerFromString(self.dest_id, context) + processed_layer = QgsProcessingUtils.mapLayerFromString(self.dest_id, context) self.isochrones.stylePoly(processed_layer) return {self.OUT: self.dest_id} - def get_sorted_feature_parameters(self, layer): + @staticmethod + def get_sorted_feature_parameters(layer: QgsProcessingParameterFeatureSource, id_field_name: str): """ Generator to yield geometry and id of features sorted by feature ID. Careful: feat.id() is not necessarily permanent :param layer: source input layer. - :type layer: QgsProcessingParameterFeatureSource + :param id_field_name: layer field containing id values """ # First get coordinate transformer - xformer = transform.transformToWGS(layer.sourceCrs()) + x_former = transform.transformToWGS(layer.sourceCrs()) for feat in sorted(layer.getFeatures(), key=lambda f: f.id()): - x_point = xformer.transform(feat.geometry().asPoint()) - - yield [[round(x_point.x(), 6), round(x_point.y(), 6)]], feat + x_point = x_former.transform(feat.geometry().asPoint()) + try: + id_value = feat[id_field_name] + except KeyError: + id_value = None + yield [[round(x_point.x(), 6), round(x_point.y(), 6)]], id_value diff --git a/ORStools/proc/isochrones_point_proc.py b/ORStools/proc/isochrones_point_proc.py index a2cbdfbc..5fe9bf69 100644 --- a/ORStools/proc/isochrones_point_proc.py +++ b/ORStools/proc/isochrones_point_proc.py @@ -27,176 +27,99 @@ ***************************************************************************/ """ -import os.path from qgis.core import (QgsWkbTypes, QgsCoordinateReferenceSystem, QgsProcessingUtils, - QgsProcessingAlgorithm, QgsProcessingParameterString, QgsProcessingParameterEnum, - QgsProcessingParameterFeatureSink, QgsProcessingParameterPoint, ) -from PyQt5.QtGui import QIcon - -from ORStools import RESOURCE_PREFIX, __help__ -from ORStools.common import client, isochrones_core, PROFILES, DIMENSIONS -from ORStools.utils import exceptions, configmanager, logger -from . import HELP_DIR - - -class ORSisochronesPointAlgo(QgsProcessingAlgorithm): - # TODO: create base algorithm class common to all modules - - ALGO_NAME = 'isochrones_from_point' - ALGO_NAME_LIST = ALGO_NAME.split('_') - - IN_PROVIDER = "INPUT_PROVIDER" - IN_POINT = "INPUT_POINT" - IN_PROFILE = "INPUT_PROFILE" - IN_METRIC = 'INPUT_METRIC' - IN_RANGES = 'INPUT_RANGES' - IN_KEY = 'INPUT_APIKEY' - IN_DIFFERENCE = 'INPUT_DIFFERENCE' - OUT = 'OUTPUT' - - # Save some important references - isochrones = isochrones_core.Isochrones() - dest_id = None - crs_out = QgsCoordinateReferenceSystem.fromEpsgId(4326) - # difference = None - - # noinspection PyUnusedLocal - def initAlgorithm(self, configuration): - - providers = [provider['name'] for provider in configmanager.read_config()['providers']] - self.addParameter( - QgsProcessingParameterEnum( - self.IN_PROVIDER, - "Provider", - providers, - defaultValue=providers[0] - ) - ) - - self.addParameter( +from ORStools.common import isochrones_core, PROFILES, DIMENSIONS +from ORStools.utils import exceptions, logger +from .base_processing_algorithm import ORSBaseProcessingAlgorithm + + +# noinspection PyPep8Naming +class ORSIsochronesPointAlgo(ORSBaseProcessingAlgorithm): + def __init__(self): + super().__init__() + self.ALGO_NAME = 'isochrones_from_point' + self.GROUP = "Isochrones" + self.IN_POINT = "INPUT_POINT" + self.IN_PROFILE = "INPUT_PROFILE" + self.IN_METRIC = 'INPUT_METRIC' + self.IN_RANGES = 'INPUT_RANGES' + self.IN_KEY = 'INPUT_APIKEY' + self.IN_DIFFERENCE = 'INPUT_DIFFERENCE' + self.PARAMETERS = [ QgsProcessingParameterPoint( name=self.IN_POINT, description="Input Point from map canvas (mutually exclusive with layer option)", optional=True - ) - ) - - self.addParameter( + ), QgsProcessingParameterEnum( self.IN_PROFILE, "Travel mode", PROFILES, defaultValue=PROFILES[0] - ) - ) - - self.addParameter( + ), QgsProcessingParameterEnum( name=self.IN_METRIC, description="Dimension", options=DIMENSIONS, defaultValue=DIMENSIONS[0] - ) - ) - - self.addParameter( + ), QgsProcessingParameterString( name=self.IN_RANGES, - description="Comma-separated ranges [mins or m]", + description="Comma-separated ranges [min or m]", defaultValue="5, 10" ) - ) - - self.addParameter( - QgsProcessingParameterFeatureSink( - name=self.OUT, - description="Isochrones", - createByDefault=False - ) - ) - - def group(self): - return "Isochrones" - - def groupId(self): - return 'isochrones' - - def name(self): - return self.ALGO_NAME - - def shortHelpString(self): - """Displays the sidebar help in the algorithm window""" - - file = os.path.join( - HELP_DIR, - 'algorithm_isochrone_point.help' - ) - with open(file, encoding='utf-8') as helpf: - msg = helpf.read() - - return msg - - def helpUrl(self): - """will be connected to the Help button in the Algorithm window""" - return __help__ - - def displayName(self): - return " ".join(map(lambda x: x.capitalize(), self.ALGO_NAME_LIST)) + ] - def icon(self): - return QIcon(RESOURCE_PREFIX + 'icon_isochrones.png') - - def createInstance(self): - return ORSisochronesPointAlgo() + # Save some important references + # TODO bad style, refactor + isochrones = isochrones_core.Isochrones() + dest_id = None + crs_out = QgsCoordinateReferenceSystem.fromEpsgId(4326) + # difference = None # TODO: preprocess parameters to options the range cleanup below: # https://www.qgis.org/pyqgis/master/core/Processing/QgsProcessingAlgorithm.html#qgis.core.QgsProcessingAlgorithm.preprocessParameters - def processAlgorithm(self, parameters, context, feedback): - # Init ORS client - providers = configmanager.read_config()['providers'] - provider = providers[self.parameterAsEnum(parameters, self.IN_PROVIDER, context)] - clnt = client.Client(provider) - clnt.overQueryLimit.connect(lambda : feedback.reportError("OverQueryLimit: Retrying...")) - - params = dict() - params['attributes'] = ['total_pop'] + ors_client = self._get_ors_client_from_provider(parameters[self.IN_PROVIDER], feedback) - profile = PROFILES[self.parameterAsEnum(parameters, self.IN_PROFILE, context)] - params['range_type'] = dimension = DIMENSIONS[self.parameterAsEnum(parameters, self.IN_METRIC, context)] + profile = dict(enumerate(PROFILES))[parameters[self.IN_PROFILE]] + dimension = dict(enumerate(DIMENSIONS))[parameters[self.IN_METRIC]] - factor = 60 if params['range_type'] == 'time' else 1 - ranges_raw = self.parameterAsString(parameters, self.IN_RANGES, context) + factor = 60 if dimension == 'time' else 1 + ranges_raw = parameters[self.IN_RANGES] ranges_proc = [x * factor for x in map(int, ranges_raw.split(','))] - params['range'] = ranges_proc point = self.parameterAsPoint(parameters, self.IN_POINT, context, self.crs_out) # Make the actual requests # If layer source is set - requests = [] self.isochrones.set_parameters(profile, dimension, factor) - params['locations'] = [[round(point.x(), 6), round(point.y(), 6)]] - params['id'] = None - requests.append(params) + params = { + "locations": [[round(point.x(), 6), round(point.y(), 6)]], + "range_type": dimension, + "range": ranges_proc, + "attributes": ['total_pop'], + "id": None + } (sink, self.dest_id) = self.parameterAsSink(parameters, self.OUT, context, self.isochrones.get_fields(), - QgsWkbTypes.Polygon, # Needs Multipolygon if difference parameter will ever be reactivated + QgsWkbTypes.Polygon, + # Needs Multipolygon if difference parameter will ever be + # reactivated self.crs_out) - # If feature causes error, report and continue with next try: - # Populate features from response - response = clnt.request('/v2/isochrones/' + profile, {}, post_json=params) + response = ors_client.request('/v2/isochrones/' + profile, {}, post_json=params) + # Populate features from response for isochrone in self.isochrones.get_features(response, params['id']): sink.addFeature(isochrone) @@ -212,7 +135,7 @@ def processAlgorithm(self, parameters, context, feedback): # noinspection PyUnusedLocal def postProcessAlgorithm(self, context, feedback): """Style polygon layer in post-processing step.""" - processed_layer= QgsProcessingUtils.mapLayerFromString(self.dest_id, context) + processed_layer = QgsProcessingUtils.mapLayerFromString(self.dest_id, context) self.isochrones.stylePoly(processed_layer) return {self.OUT: self.dest_id} diff --git a/ORStools/proc/matrix_proc.py b/ORStools/proc/matrix_proc.py index 9e0991bd..d490f8fe 100644 --- a/ORStools/proc/matrix_proc.py +++ b/ORStools/proc/matrix_proc.py @@ -27,154 +27,69 @@ ***************************************************************************/ """ -import os.path from qgis.core import (QgsWkbTypes, QgsFeature, QgsProcessing, QgsFields, QgsField, QgsProcessingException, - QgsProcessingAlgorithm, QgsProcessingParameterField, QgsProcessingParameterFeatureSource, QgsProcessingParameterEnum, - QgsProcessingParameterFeatureSink, ) from PyQt5.QtCore import QVariant -from PyQt5.QtGui import QIcon -from ORStools import RESOURCE_PREFIX, __help__ -from ORStools.common import client, PROFILES -from ORStools.utils import transform, exceptions, logger, configmanager -from . import HELP_DIR - - -class ORSmatrixAlgo(QgsProcessingAlgorithm): - # TODO: create base algorithm class common to all modules - - ALGO_NAME = 'matrix_from_layers' - ALGO_NAME_LIST = ALGO_NAME.split('_') - - IN_PROVIDER = "INPUT_PROVIDER" - IN_START = "INPUT_START_LAYER" - IN_START_FIELD = "INPUT_START_FIELD" - IN_END = "INPUT_END_LAYER" - IN_END_FIELD = "INPUT_END_FIELD" - IN_PROFILE = "INPUT_PROFILE" - OUT = 'OUTPUT' - - # noinspection PyUnusedLocal - def initAlgorithm(self, configuration): - - providers = [provider['name'] for provider in configmanager.read_config()['providers']] - self.addParameter( - QgsProcessingParameterEnum( - self.IN_PROVIDER, - "Provider", - providers, - defaultValue=providers[0] - ) - ) - - self.addParameter( +from ORStools.common import PROFILES +from ORStools.utils import transform, exceptions, logger +from .base_processing_algorithm import ORSBaseProcessingAlgorithm + + +# noinspection PyPep8Naming +class ORSMatrixAlgo(ORSBaseProcessingAlgorithm): + def __init__(self): + super().__init__() + self.ALGO_NAME = 'matrix_from_layers' + self.GROUP = "Matrix" + self.IN_START = "INPUT_START_LAYER" + self.IN_START_FIELD = "INPUT_START_FIELD" + self.IN_END = "INPUT_END_LAYER" + self.IN_END_FIELD = "INPUT_END_FIELD" + self.IN_PROFILE = "INPUT_PROFILE" + self.PARAMETERS = [ QgsProcessingParameterFeatureSource( name=self.IN_START, description="Input Start Point layer", types=[QgsProcessing.TypeVectorPoint], - ) - ) - - self.addParameter( + ), QgsProcessingParameterField( name=self.IN_START_FIELD, description="Start ID Field (can be used for joining)", parentLayerParameterName=self.IN_START, - ) - ) - - self.addParameter( + ), QgsProcessingParameterFeatureSource( name=self.IN_END, description="Input End Point layer", types=[QgsProcessing.TypeVectorPoint], - ) - ) - - self.addParameter( + ), QgsProcessingParameterField( name=self.IN_END_FIELD, description="End ID Field (can be used for joining)", parentLayerParameterName=self.IN_END, - ) - ) - - self.addParameter( + ), QgsProcessingParameterEnum( self.IN_PROFILE, "Travel mode", PROFILES, defaultValue=PROFILES[0] ) - ) - - self.addParameter( - QgsProcessingParameterFeatureSink( - name=self.OUT, - description="Matrix", - ) - ) - - def group(self): - return "Matrix" - - def groupId(self): - return 'matrix' - - def name(self): - return self.ALGO_NAME - - def shortHelpString(self): - """Displays the sidebar help in the algorithm window""" - - file = os.path.join( - HELP_DIR, - 'algorithm_matrix.help' - ) - with open(file, encoding='utf-8') as helpf: - msg = helpf.read() - - return msg - - def helpUrl(self): - """will be connected to the Help button in the Algorithm window""" - return __help__ - - def displayName(self): - return " ".join(map(lambda x: x.capitalize(), self.ALGO_NAME_LIST)) - - def icon(self): - return QIcon(RESOURCE_PREFIX + 'icon_matrix.png') - - def createInstance(self): - return ORSmatrixAlgo() + ] def processAlgorithm(self, parameters, context, feedback): - - # Init ORS client - providers = configmanager.read_config()['providers'] - provider = providers[self.parameterAsEnum(parameters, self.IN_PROVIDER, context)] - clnt = client.Client(provider) - clnt.overQueryLimit.connect(lambda: feedback.reportError("OverQueryLimit: Retrying")) - - params = dict() + ors_client = self._get_ors_client_from_provider(parameters[self.IN_PROVIDER], feedback) # Get profile value - profile = PROFILES[self.parameterAsEnum( - parameters, - self.IN_PROFILE, - context - )] + profile = dict(enumerate(PROFILES))[parameters[self.IN_PROFILE]] # Get parameter values source = self.parameterAsSource( @@ -182,32 +97,24 @@ def processAlgorithm(self, parameters, context, feedback): self.IN_START, context ) - source_field_name = self.parameterAsString( - parameters, - self.IN_START_FIELD, - context - ) + source_field_name = parameters[self.IN_START_FIELD] + destination = self.parameterAsSource( parameters, self.IN_END, context ) - destination_field_name = self.parameterAsString( - parameters, - self.IN_END_FIELD, - context - ) + destination_field_name = parameters[self.IN_END_FIELD] # Get fields from field name - source_field_id = source.fields().lookupField(source_field_name) - source_field = source.fields().field(source_field_id) + source_field = source.fields().field(source_field_name) - destination_field_id = destination.fields().lookupField(destination_field_name) - destination_field = destination.fields().field(destination_field_id) + destination_field = destination.fields().field(destination_field_name) # Abort when MultiPoint type if (source.wkbType() or destination.wkbType()) == 4: - raise QgsProcessingException("TypeError: Multipoint Layers are not accepted. Please convert to single geometry layer.") + raise QgsProcessingException( + "TypeError: Multipoint Layers are not accepted. Please convert to single geometry layer.") # Get source and destination features sources_features = list(source.getFeatures()) @@ -220,33 +127,34 @@ def processAlgorithm(self, parameters, context, feedback): source_equals_destination = parameters['INPUT_START_LAYER'] == parameters['INPUT_END_LAYER'] if source_equals_destination: features = sources_features - xformer = transform.transformToWGS(source.sourceCrs()) - features_points = [xformer.transform(feat.geometry().asPoint()) for feat in features] + x_former = transform.transformToWGS(source.sourceCrs()) + features_points = [x_former.transform(feat.geometry().asPoint()) for feat in features] else: - xformer = transform.transformToWGS(source.sourceCrs()) - sources_features_xformed = [xformer.transform(feat.geometry().asPoint()) for feat in sources_features] + x_former = transform.transformToWGS(source.sourceCrs()) + sources_features_x_formed = [x_former.transform(feat.geometry().asPoint()) for feat in sources_features] - xformer = transform.transformToWGS(destination.sourceCrs()) - destination_features_xformed = [xformer.transform(feat.geometry().asPoint()) for feat in destination_features] + x_former = transform.transformToWGS(destination.sourceCrs()) + destination_features_x_formed = [x_former.transform(feat.geometry().asPoint()) for feat in + destination_features] - features_points = sources_features_xformed + destination_features_xformed + features_points = sources_features_x_formed + destination_features_x_formed # Get IDs sources_ids = list(range(sources_amount)) if source_equals_destination else list(range(sources_amount)) - destination_ids = list(range(sources_amount)) if source_equals_destination else list(range(sources_amount, sources_amount + destinations_amount)) + destination_ids = list(range(sources_amount)) if source_equals_destination else list( + range(sources_amount, sources_amount + destinations_amount)) - # Populate parameters further - params.update({ + params = { 'locations': [[point.x(), point.y()] for point in features_points], 'sources': sources_ids, 'destinations': destination_ids, 'metrics': ["duration", "distance"], 'id': 'Matrix' - }) + } # Make request and catch ApiError try: - response = clnt.request('/v2/matrix/' + profile, {}, post_json=params) + response = ors_client.request('/v2/matrix/' + profile, {}, post_json=params) except (exceptions.ApiError, exceptions.InvalidKey, diff --git a/ORStools/proc/provider.py b/ORStools/proc/provider.py index 7dac5793..e72368be 100644 --- a/ORStools/proc/provider.py +++ b/ORStools/proc/provider.py @@ -32,12 +32,12 @@ from PyQt5.QtGui import QIcon from ORStools import RESOURCE_PREFIX, PLUGIN_NAME, __version__ -from .directions_lines_proc import ORSdirectionsLinesAlgo -from .directions_points_layer_proc import ORSdirectionsPointsLayerAlgo -from .directions_points_layers_proc import ORSdirectionsPointsLayersAlgo -from .isochrones_layer_proc import ORSisochronesLayerAlgo -from .isochrones_point_proc import ORSisochronesPointAlgo -from .matrix_proc import ORSmatrixAlgo +from .directions_lines_proc import ORSDirectionsLinesAlgorithm +from .directions_points_layer_proc import ORSDirectionsPointsLayerAlgo +from .directions_points_layers_proc import ORSDirectionsPointsLayersAlgo +from .isochrones_layer_proc import ORSIsochronesLayerAlgo +from .isochrones_point_proc import ORSIsochronesPointAlgo +from .matrix_proc import ORSMatrixAlgo class ORStoolsProvider(QgsProcessingProvider): @@ -53,22 +53,25 @@ def unload(self): """ pass + # noinspection PyPep8Naming def loadAlgorithms(self): """ Loads all algorithms belonging to this provider. """ # - self.addAlgorithm(ORSdirectionsPointsLayersAlgo()) - self.addAlgorithm(ORSdirectionsPointsLayerAlgo()) - self.addAlgorithm(ORSdirectionsLinesAlgo()) - self.addAlgorithm(ORSisochronesLayerAlgo()) - self.addAlgorithm(ORSisochronesPointAlgo()) - self.addAlgorithm(ORSmatrixAlgo()) - - def icon(self): + self.addAlgorithm(ORSDirectionsPointsLayersAlgo()) + self.addAlgorithm(ORSDirectionsPointsLayerAlgo()) + self.addAlgorithm(ORSDirectionsLinesAlgorithm()) + self.addAlgorithm(ORSIsochronesLayerAlgo()) + self.addAlgorithm(ORSIsochronesPointAlgo()) + self.addAlgorithm(ORSMatrixAlgo()) + + @staticmethod + def icon(): return QIcon(RESOURCE_PREFIX + 'icon_orstools.png') - def id(self): + @staticmethod + def id(): """ Returns the unique provider id, used for identifying the provider. This string should be a unique, short, character only string, eg "qgis" or @@ -76,7 +79,8 @@ def id(self): """ return PLUGIN_NAME.strip() - def name(self): + @staticmethod + def name(): """ Returns the provider name, which is used to describe the provider within the GUI. @@ -85,7 +89,9 @@ def name(self): """ return PLUGIN_NAME - def longName(self): + # noinspection PyPep8Naming + @staticmethod + def longName(): """ Returns the a longer version of the provider name, which can include extra details such as version numbers. E.g. "Lastools LIDAR tools diff --git a/ORStools/utils/processing.py b/ORStools/utils/processing.py new file mode 100644 index 00000000..cba4eb59 --- /dev/null +++ b/ORStools/utils/processing.py @@ -0,0 +1,79 @@ +# -*- 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. * + * * + ***************************************************************************/ +""" +import os +from qgis.core import QgsPointXY + +from typing import List + +from ORStools import BASE_DIR + + +def get_params_optimize(point_list: List[QgsPointXY], ors_profile: str) -> dict: + """ + Build parameters for optimization endpoint + + :param point_list: individual polyline points + :param ors_profile: ors transport profile to be used + """ + + start = point_list.pop(0) + end = point_list.pop(-1) + + params = { + 'jobs': list(), + 'vehicles': [{ + "id": 0, + "profile": ors_profile, + "start": [start.x(), start.y()], + "end": [end.x(), end.y()] + }], + 'options': {'g': True} + } + for point in point_list: + params['jobs'].append({ + "location": [point.x(), point.y()], + "id": point_list.index(point) + }) + + return params + + +def read_help_file(file_name: str): + """ + Returns the contents of a file from the help folder + :rtype: str + """ + file = os.path.join( + BASE_DIR, + 'help', + file_name + ) + with open(file, encoding='utf-8') as help_file: + msg = help_file.read() + return msg