def _on_prov_refresh_click(self):
        """Populates provider dropdown with fresh list from config.yml"""

        providers = configmanager.read_config()['providers']
        self.provider_combo.clear()
        for provider in providers:
            self.provider_combo.addItem(provider['name'], provider)
    def _init_gui_control(self):
        """Slot for main plugin button. Initializes the GUI and shows it."""

        # Only populate GUI if it's the first start of the plugin within the QGIS session
        # If not checked, GUI would be rebuilt every time!
        if self.first_start:
            self.first_start = False
            self.dlg = ORStoolsDialog(
                self.iface,
                self.iface.mainWindow())  # setting parent enables modal view
            self.dlg.routing_advanced_button.clicked.connect(
                self._on_advanced_click)
            # Make sure plugin window stays open when OK is clicked by reconnecting the accepted() signal
            self.dlg.global_buttons.accepted.disconnect(self.dlg.accept)
            self.dlg.global_buttons.accepted.connect(self.run_gui_control)

        # Populate provider box on window startup, since can be changed from multiple menus/buttons
        providers = configmanager.read_config()['providers']
        self.dlg.provider_combo.clear()
        for provider in providers:
            self.dlg.provider_combo.addItem(provider['name'], provider)

        # Populate Advanced dialog; makes sure that dialog is re-populated every time plugin starts,
        # but stays alive during one session
        self.advanced = ORStoolsDialogAdvancedMain(parent=self.dlg)
        self.dlg.show()
    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']

        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)
        ranges_proc = [x * factor for x in map(int, ranges_raw.split(','))]
        params['range'] = ranges_proc

        interval_raw = self.parameterAsString(parameters, self.IN_INTERVAL, context)
        if interval_raw:
            params['interval'] = interval_raw
        smoothing_raw = self.parameterAsString(parameters, self.IN_SMOOTH, context)
        if smoothing_raw:
            params['smoothing'] = smoothing_raw

        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)

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

            for isochrone in self.isochrones.get_features(response, params['id']):
                sink.addFeature(isochrone)

        except (exceptions.ApiError,
                exceptions.InvalidKey,
                exceptions.GenericServerError) as e:
            msg = "Feature ID {} caused a {}:\n{}".format(
                params['id'],
                e.__class__.__name__,
                str(e))
            feedback.reportError(msg)
            logger.log(msg, 2)

        return {self.OUT: self.dest_id}
    def initAlgorithm(self,
                      configuration,
                      p_str=None,
                      Any=None,
                      *args,
                      **kwargs):

        providers = [
            provider['name']
            for provider in configmanager.read_config()['providers']
        ]
        self.addParameter(
            QgsProcessingParameterEnum(self.IN_PROVIDER,
                                       "Provider",
                                       providers,
                                       defaultValue=providers[0]))

        self.addParameter(
            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 initAlgorithm(self,
                      configuration,
                      p_str=None,
                      Any=None,
                      *args,
                      **kwargs):

        providers = [
            provider['name']
            for provider in configmanager.read_config()['providers']
        ]
        self.addParameter(
            QgsProcessingParameterEnum(self.IN_PROVIDER,
                                       "Provider",
                                       providers,
                                       defaultValue=providers[0]))

        self.addParameter(
            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 _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
 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])
示例#8
0
    def initAlgorithm(self,
                      configuration,
                      p_str=None,
                      Any=None,
                      *args,
                      **kwargs):

        providers = [
            provider['name']
            for provider in configmanager.read_config()['providers']
        ]
        self.addParameter(
            QgsProcessingParameterEnum(self.IN_PROVIDER,
                                       "Provider",
                                       providers,
                                       defaultValue=providers[0]))

        self.addParameter(
            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]",
                defaultValue="5, 10"))

        self.addParameter(
            QgsProcessingParameterFeatureSink(name=self.OUT,
                                              description="Isochrones",
                                              createByDefault=False))
示例#9
0
    def __init__(self, parent=None):
        """
        :param parent: Parent window for modality.
        :type parent: QDialog
        """
        QDialog.__init__(self, parent)

        self.setupUi(self)

        # Temp storage for config dict
        self.temp_config = configmanager.read_config()

        self._build_ui()
        self._collapse_boxes()

        self.provider_add.clicked.connect(self._add_provider)
        self.provider_remove.clicked.connect(self._remove_provider)
示例#10
0
    def _init_gui_control(self):
        """Slot for main plugin button. Initializes the GUI and shows it."""

        # Only populate GUI if it's the first start of the plugin within the QGIS session
        # If not checked, GUI would be rebuilt every time!
        if self.first_start:
            self.first_start = False
            self.dlg = ORStoolsDialog(
                self.iface,
                self.iface.mainWindow())  # setting parent enables modal view
            # Make sure plugin window stays open when OK is clicked by reconnecting the accepted() signal
            self.dlg.global_buttons.accepted.disconnect(self.dlg.accept)
            self.dlg.global_buttons.accepted.connect(self.run_gui_control)
            self.dlg.avoidpolygon_dropdown.setFilters(
                QgsMapLayerProxyModel.PolygonLayer)

        # Populate provider box on window startup, since can be changed from multiple menus/buttons
        providers = configmanager.read_config()['providers']
        self.dlg.provider_combo.clear()
        for provider in providers:
            self.dlg.provider_combo.addItem(provider['name'], provider)

        self.dlg.show()
示例#11
0
class ORSdirectionsLinesAlgo(QgsProcessingAlgorithm):
    """Algorithm class for Directions Lines."""

    ALGO_NAME = 'directions_lines'
    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_MODE = "INPUT_MODE"
    OUT = 'OUTPUT'

    providers = configmanager.read_config()['providers']

    def initAlgorithm(self,
                      configuration,
                      p_str=None,
                      Any=None,
                      *args,
                      **kwargs):

        providers = [provider['name'] for provider in self.providers]
        self.addParameter(
            QgsProcessingParameterEnum(self.IN_PROVIDER, "Provider",
                                       providers))

        self.addParameter(
            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))

        self.addParameter(
            QgsProcessingParameterEnum(self.IN_PREFERENCE, "Travel preference",
                                       PREFERENCES))

        self.addParameter(
            QgsProcessingParameterFeatureSink(
                name=self.OUT,
                description="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) 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 'Generate ' + " ".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

        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 = PROFILES[self.parameterAsEnum(parameters, self.IN_PROFILE,
                                                context)]

        preference = PREFERENCES[self.parameterAsEnum(parameters,
                                                      self.IN_PREFERENCE,
                                                      context)]

        # Get parameter values
        source = self.parameterAsSource(parameters, self.IN_LINES, context)

        source_field_idx = self.parameterAsEnum(parameters, self.IN_FIELD,
                                                context)

        source_field_name = self.parameterAsString(parameters, self.IN_FIELD,
                                                   context)

        params = {
            'profile': profile,
            'preference': preference,
            'geometry': 'true',
            'format': 'geojson',
            'geometry_format': 'geojson',
            'instructions': 'false',
            'id': None
        }

        (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), source.wkbType(),
            QgsCoordinateReferenceSystem(4326))
        count = source.featureCount()

        for num, (line, field_value) in enumerate(
                self.get_sorted_lines(source, source_field_name)):
            # Stop the algorithm if cancel button has been clicked
            if feedback.isCanceled():
                break

            params['coordinates'] = convert.build_coords(
                [[point.x(), point.y()] for point in line])

            try:
                response = clnt.request(
                    provider['endpoints'][self.ALGO_NAME_LIST[0]], params)
            except (exceptions.ApiError, exceptions.InvalidKey,
                    exceptions.GenericServerError) as e:
                msg = "Feature ID {} caused a {}:\n{}".format(
                    line[source_field_name], e.__class__.__name__, str(e))
                feedback.reportError(msg)
                logger.log(msg)
                continue

            sink.addFeature(
                directions_core.get_output_feature(response,
                                                   profile,
                                                   preference,
                                                   from_value=field_value,
                                                   line=True))

            feedback.setProgress(int(100.0 / count * num))

        return {self.OUT: dest_id}

    def get_sorted_lines(self, layer, field_name):
        """
        Generator to yield geometry and ID value sorted by feature ID. Careful: feat.id() is not necessarily
        permanent

        :param layer: source input layer
        :type layer: QgsProcessingParameterFeatureSource

        :param field_name: name of ID field
        :type field_name: str
        """
        # First get coordinate transformer
        xformer = transform.transformToWGS(layer.sourceCrs())

        for feat in sorted(layer.getFeatures(), key=lambda f: f.id()):
            line = None
            field_value = feat[field_name]
            # for
            if layer.wkbType() == QgsWkbTypes.MultiLineString:
                line = [
                    xformer.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()
                ]

            yield line, field_value
示例#12
0
    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..."))

        profile = PROFILES[self.parameterAsEnum(parameters, self.IN_PROFILE,
                                                context)]

        preference = PREFERENCES[self.parameterAsEnum(parameters,
                                                      self.IN_PREFERENCE,
                                                      context)]

        # Get parameter values
        source = self.parameterAsSource(parameters, self.IN_LINES, context)

        source_field_idx = self.parameterAsEnum(parameters, self.IN_FIELD,
                                                context)

        source_field_name = self.parameterAsString(parameters, self.IN_FIELD,
                                                   context)

        params = {
            'profile': profile,
            'preference': preference,
            'geometry': 'true',
            'format': 'geojson',
            'geometry_format': 'geojson',
            'instructions': 'false',
            'id': None
        }

        (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), source.wkbType(),
            QgsCoordinateReferenceSystem(4326))
        count = source.featureCount()

        for num, (line, field_value) in enumerate(
                self.get_sorted_lines(source, source_field_name)):
            # Stop the algorithm if cancel button has been clicked
            if feedback.isCanceled():
                break

            params['coordinates'] = convert.build_coords(
                [[point.x(), point.y()] for point in line])

            try:
                response = clnt.request(
                    provider['endpoints'][self.ALGO_NAME_LIST[0]], params)
            except (exceptions.ApiError, exceptions.InvalidKey,
                    exceptions.GenericServerError) as e:
                msg = "Feature ID {} caused a {}:\n{}".format(
                    line[source_field_name], e.__class__.__name__, str(e))
                feedback.reportError(msg)
                logger.log(msg)
                continue

            sink.addFeature(
                directions_core.get_output_feature(response,
                                                   profile,
                                                   preference,
                                                   from_value=field_value,
                                                   line=True))

            feedback.setProgress(int(100.0 / count * num))

        return {self.OUT: dest_id}
示例#13
0
    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..."))

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

        # Get parameter values
        source = self.parameterAsSource(parameters, self.IN_START, context)
        source_field_name = self.parameterAsString(parameters,
                                                   self.IN_START_FIELD,
                                                   context)
        destination = self.parameterAsSource(parameters, self.IN_END, context)
        destination_field_name = self.parameterAsString(
            parameters, self.IN_END_FIELD, context)

        # 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
        }

        route_dict = self._get_route_dict(source, source_field, destination,
                                          destination_field)

        if mode == 'Row-by-Row':
            route_count = min(
                [source.featureCount(),
                 destination.featureCount()])
        else:
            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()),
            QgsWkbTypes.LineString, QgsCoordinateReferenceSystem(4326))

        counter = 0
        for coordinates, values in directions_core.get_request_point_features(
                route_dict, mode):
            # Stop the algorithm if cancel button has been clicked
            if feedback.isCanceled():
                break

            params['coordinates'] = coordinates

            try:
                response = clnt.request('/v2/directions/' + profile +
                                        '/geojson', {},
                                        post_json=params)
            except (exceptions.ApiError, exceptions.InvalidKey,
                    exceptions.GenericServerError) as e:
                msg = "Route from {} to {} caused a {}:\n{}".format(
                    values[0], values[1], e.__class__.__name__, str(e))
                feedback.reportError(msg)
                logger.log(msg)
                continue

            sink.addFeature(
                directions_core.get_output_feature_directions(
                    response,
                    profile,
                    preference,
                    from_value=values[0],
                    to_value=values[1]))

            counter += 1
            feedback.setProgress(int(100.0 / route_count * counter))

        return {self.OUT: dest_id}
class ORSisochronesAlgo(QgsProcessingAlgorithm):
    # TODO: create base algorithm class common to all modules

    ALGO_NAME = 'isochrones'

    IN_PROVIDER = "INPUT_PROVIDER"
    IN_POINT = "INPUT_POINT"
    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(4326)
    providers = configmanager.read_config()['providers']

    # difference = None

    def initAlgorithm(self,
                      configuration,
                      p_str=None,
                      Any=None,
                      *args,
                      **kwargs):
        providers = [provider['name'] for provider in self.providers]
        self.addParameter(
            QgsProcessingParameterEnum(self.IN_PROVIDER, "Provider",
                                       providers))

        self.addParameter(
            QgsProcessingParameterPoint(
                name=self.IN_POINT,
                description=
                "Input Point from map canvas (mutually exclusive with layer option)",
                optional=True,
            ))

        self.addParameter(
            QgsProcessingParameterFeatureSource(
                name=self.IN_POINTS,
                description=
                "Input Point layer (mutually exclusive with Point option)",
                types=[QgsProcessing.TypeVectorPoint],
                defaultValue=None,
                optional=True))

        # self.addParameter(
        #     QgsProcessingParameterBoolean(
        #         name=self.IN_DIFFERENCE,
        #         description="Dissolve and calulate isochrone difference",
        #     )
        # )

        self.addParameter(
            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))

        self.addParameter(
            QgsProcessingParameterEnum(
                name=self.IN_METRIC,
                description="Dimension",
                options=DIMENSIONS,
            ))

        self.addParameter(
            QgsProcessingParameterString(
                name=self.IN_RANGES,
                description="Comma-separated ranges [mins or m]",
            ))

        self.addParameter(
            QgsProcessingParameterFeatureSink(name=self.OUT,
                                              description="Isochrones",
                                              createByDefault=False))

    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.help')
        with open(file) 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 'Generate ' + self.ALGO_NAME.capitalize()

    def icon(self):
        return QIcon(RESOURCE_PREFIX + 'icon_isochrones.png')

    def createInstance(self):
        return ORSisochronesAlgo()

    # TODO: preprocess parameters to avoid the range clenaup 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'

        params['profile'] = 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)
        ranges_proc = [x * factor for x in map(int, ranges_raw.split(','))]
        params['range'] = convert.comma_list(ranges_proc)

        # self.difference = self.parameterAsBool(parameters, self.IN_DIFFERENCE, context)
        point = self.parameterAsPoint(parameters, self.IN_POINT, context,
                                      self.crs_out)
        source = self.parameterAsSource(parameters, self.IN_POINTS, context)

        # Make the actual requests
        # If layer source is set
        requests = []
        if source:
            if source.wkbType() == 4:
                raise QgsProcessingException(
                    "TypeError: Multipoint Layers are not accepted. Please convert to single geometry layer."
                )

            # Get ID field properties
            # TODO: id_field should have a default (#90)
            id_field_name = self.parameterAsString(parameters, self.IN_FIELD,
                                                   context)
            id_field_id = source.fields().lookupField(id_field_name)
            if id_field_name == '':
                id_field_id = 0
                id_field_name = source.fields().field(id_field_id).name()
            id_field = source.fields().field(id_field_id)

            # Populate iso_layer instance with parameters
            self.isochrones.set_parameters(profile, dimension, factor,
                                           id_field.type(), id_field_name)

            for properties in self.get_sorted_feature_parameters(source):
                # Stop the algorithm if cancel button has been clicked
                if feedback.isCanceled():
                    break

                # Get transformed coordinates and feature
                params['locations'], feat = properties
                params['id'] = feat[id_field_name]
                requests.append(deepcopy(params))
        # elif point source is set
        else:
            self.isochrones.set_parameters(profile, dimension, factor)
            params['locations'] = convert.build_coords([point.x(), point.y()])
            params['id'] = None
            requests.append(params)

        (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
            self.crs_out)

        for num, params in enumerate(requests):
            # If feature causes error, report and continue with next
            try:
                # Populate features from response
                response = clnt.request(provider['endpoints'][self.ALGO_NAME],
                                        params)

                for isochrone in self.isochrones.get_features(
                        response, params['id']):
                    sink.addFeature(isochrone)

            except (exceptions.ApiError, exceptions.InvalidKey,
                    exceptions.GenericServerError) as e:
                msg = "Feature ID {} caused a {}:\n{}".format(
                    params['id'], e.__class__.__name__, str(e))
                feedback.reportError(msg)
                logger.log(msg, 2)
                continue
            if source:
                feedback.setProgress(int(100.0 / source.featureCount() * num))

        return {self.OUT: self.dest_id}

    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)
        self.isochrones.stylePoly(processed_layer)

        return {self.OUT: self.dest_id}

    def get_sorted_feature_parameters(self, layer):
        """
        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
        """
        # First get coordinate transformer
        xformer = transform.transformToWGS(layer.sourceCrs())

        for feat in sorted(layer.getFeatures(), key=lambda f: f.id()):
            x_point = xformer.transform(feat.geometry().asPoint())

            yield (convert.build_coords([x_point.x(), x_point.y()]), feat)
    def run_gui_control(self):
        """Slot function for OK button of main dialog."""

        layer_out = QgsVectorLayer("LineString?crs=EPSG:4326", "Route_ORS",
                                   "memory")
        layer_out.dataProvider().addAttributes(directions_core.get_fields())
        layer_out.updateFields()

        provider_id = self.dlg.provider_combo.currentIndex()
        provider = configmanager.read_config()['providers'][provider_id]

        # Check if API key was set when using ORS
        if provider['key'] is None:
            QMessageBox.critical(
                self.dlg, "Missing API key", """
                Did you forget to set an <b>API key</b> for openrouteservice?<br><br>
                
                If you don't have an API key, please visit https://openrouteservice.org/sign-up to get one. <br><br>
                Then enter the API key for openrouteservice provider in Web ► ORS Tools ► Provider Settings or the settings symbol in the main ORS Tools GUI, next to the provider dropdown.
                """)
            return

        clnt = client.Client(provider)
        clnt_msg = ''

        directions = directions_gui.Directions(self.dlg, self.advanced)
        params = directions.get_basic_paramters()
        from_id = None
        to_id = None
        try:
            if self.dlg.routing_tab.currentIndex() == 0:
                x_start = self.dlg.routing_frompoint_start_x.value()
                y_start = self.dlg.routing_frompoint_start_y.value()
                x_end = self.dlg.routing_frompoint_end_x.value()
                y_end = self.dlg.routing_frompoint_end_y.value()

                params['coordinates'] = convert.build_coords(
                    [[x_start, y_start], [x_end, y_end]])
                from_id = convert.comma_list([x_start, y_start])
                to_id = convert.comma_list([x_end, y_end])

            elif self.dlg.routing_tab.currentIndex() == 1:
                params['coordinates'] = convert.build_coords(
                    directions.get_request_line_feature())

            response = clnt.request(provider['endpoints']['directions'],
                                    params)
            layer_out.dataProvider().addFeature(
                directions_core.get_output_feature(response, params['profile'],
                                                   params['preference'],
                                                   directions.avoid, from_id,
                                                   to_id))

            layer_out.updateExtents()
            self.project.addMapLayer(layer_out)

            # Update quota; handled in client module after successful request
            if provider.get('ENV_VARS'):
                self.dlg.quota_text.setText(
                    self.get_quota(provider) + ' calls')
        except exceptions.Timeout:
            msg = "The connection has timed out!"
            logger.log(msg, 2)
            self.dlg.debug_text.setText(msg)
            return

        except (exceptions.ApiError, exceptions.InvalidKey,
                exceptions.GenericServerError) as e:
            msg = (e.__class__.__name__, str(e))

            logger.log("{}: {}".format(*msg), 2)
            clnt_msg += "<b>{}</b>: ({})<br>".format(*msg)
            return

        except Exception as e:
            msg = [e.__class__.__name__, str(e)]
            logger.log("{}: {}".format(*msg), 2)
            clnt_msg += "<b>{}</b>: {}<br>".format(*msg)
            raise

        finally:
            # Set URL in debug window
            clnt_msg += '<a href="{0}">{0}</a><br>'.format(clnt.url)
            self.dlg.debug_text.setHtml(clnt_msg)
            return
示例#16
0
    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()

        # Get profile value
        profile = PROFILES[self.parameterAsEnum(
            parameters,
            self.IN_PROFILE,
            context
        )]

        # Get parameter values
        source = self.parameterAsSource(
            parameters,
            self.IN_START,
            context
        )
        source_field_name = self.parameterAsString(
            parameters,
            self.IN_START_FIELD,
            context
        )
        destination = self.parameterAsSource(
            parameters,
            self.IN_END,
            context
        )
        destination_field_name = self.parameterAsString(
            parameters,
            self.IN_END_FIELD,
            context
        )

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

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

        # Get source and destination features
        sources_features = list(source.getFeatures())
        destination_features = list(destination.getFeatures())
        # Get feature amounts/counts
        sources_amount = source.featureCount()
        destinations_amount = destination.featureCount()

        # Allow for 50 features in source if source == destination
        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]
        else:
            xformer = transform.transformToWGS(source.sourceCrs())
            sources_features_xformed = [xformer.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]

            features_points = sources_features_xformed + destination_features_xformed

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

        # Populate parameters further
        params.update({
            '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)

        except (exceptions.ApiError,
                exceptions.InvalidKey,
                exceptions.GenericServerError) as e:
            msg = "{}: {}".format(
                e.__class__.__name__,
                str(e))
            feedback.reportError(msg)
            logger.log(msg)

        (sink, dest_id) = self.parameterAsSink(
            parameters,
            self.OUT,
            context,
            self.get_fields(
                source_field.type(),
                destination_field.type()
            ),
            QgsWkbTypes.NoGeometry
        )

        sources_attributes = [feat.attribute(source_field_name) for feat in sources_features]
        destinations_attributes = [feat.attribute(destination_field_name) for feat in destination_features]

        for s, source in enumerate(sources_attributes):
            for d, destination in enumerate(destinations_attributes):
                duration = response['durations'][s][d]
                distance = response['distances'][s][d]
                feat = QgsFeature()
                feat.setAttributes([
                    source,
                    destination,
                    duration / 3600 if duration is not None else None,
                    distance / 1000 if distance is not None else None
                ])

                sink.addFeature(feat)

        return {self.OUT: dest_id}
    def initAlgorithm(self, configuration, p_str=None, Any=None, *args, **kwargs):

        providers = [provider['name'] for provider in configmanager.read_config()['providers']]
        self.addParameter(
            QgsProcessingParameterEnum(
                self.IN_PROVIDER,
                "Provider",
                providers,
                defaultValue=providers[0]
            )
        )

        self.addParameter(
            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]",
                defaultValue="5, 10"
            )
        )

        self.addParameter(
            QgsProcessingParameterString(
                name=self.IN_INTERVAL,
                description="Interval range in seconds or meters",
                optional=True
            )
        )

        self.addParameter(
            QgsProcessingParameterString(
                name=self.IN_SMOOTH,
                description="Applies a level of generalisation to the isochrone polygons generated as a smoothing_factor between 0 and 100.0",
                optional=True
            )
        )

        self.addParameter(
            QgsProcessingParameterFeatureSink(
                name=self.OUT,
                description="Isochrones",
                createByDefault=False
            )
        )
示例#18
0
    def initAlgorithm(self, configuration, p_str=None, Any=None, *args, **kwargs):

        providers = [provider['name'] for provider in configmanager.read_config()['providers']]
        self.addParameter(
            QgsProcessingParameterEnum(
                self.IN_PROVIDER,
                "Provider",
                providers,
                defaultValue=providers[0]
            )
        )

        self.addParameter(
            QgsProcessingParameterFeatureSource(
                name=self.IN_POINTS,
                description="Input Point layer",
                types=[QgsProcessing.TypeVectorPoint]
            )
        )

        # self.addParameter(
        #     QgsProcessingParameterBoolean(
        #         name=self.IN_DIFFERENCE,
        #         description="Dissolve and calulate isochrone difference",
        #     )
        # )

        self.addParameter(
            QgsProcessingParameterField(
                name=self.IN_FIELD,
                description="Input layer ID Field (mutually exclusive with Point option)",
                parentLayerParameterName=self.IN_POINTS
            )
        )

        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]",
                defaultValue="5, 10"
            )
        )

        self.addParameter(
            QgsProcessingParameterFeatureSink(
                name=self.OUT,
                description="Isochrones",
                createByDefault=False
            )
        )
示例#19
0
    def run_gui_control(self):
        """Slot function for OK button of main dialog."""

        layer_out = QgsVectorLayer("LineString?crs=EPSG:4326", "Route_ORS",
                                   "memory")
        layer_out.dataProvider().addAttributes(directions_core.get_fields())
        layer_out.updateFields()

        # Associate annotations with map layer, so they get deleted when layer is deleted
        for annotation in self.dlg.annotations:
            # Has the potential to be pretty cool: instead of deleting, associate with mapLayer, you can change order after optimization
            # Then in theory, when the layer is remove, the annotation is removed as well
            # Doesng't work though, the annotations are still there when project is re-opened
            # annotation.setMapLayer(layer_out)
            self.project.annotationManager().removeAnnotation(annotation)
        self.dlg.annotations = []

        provider_id = self.dlg.provider_combo.currentIndex()
        provider = configmanager.read_config()['providers'][provider_id]

        # if there are no coordinates, throw an error message
        if not self.dlg.routing_fromline_list.count():
            QMessageBox.critical(
                self.dlg, "Missing API key", """
                Did you forget to set routing waypoints?<br><br>
                
                Use the 'Add Waypoint' button to add up to 50 waypoints.
                """)
            return

        # if no API key is present, when ORS is selected, throw an error message
        if not provider['key'] and provider['base_url'].startswith(
                'https://api.openrouteservice.org'):
            QMessageBox.critical(
                self.dlg, "Missing API key", """
                Did you forget to set an <b>API key</b> for openrouteservice?<br><br>
                
                If you don't have an API key, please visit https://openrouteservice.org/sign-up to get one. <br><br>
                Then enter the API key for openrouteservice provider in Web ► ORS Tools ► Provider Settings or the settings symbol in the main ORS Tools GUI, next to the provider dropdown.
                """)
            return

        clnt = client.Client(provider)
        clnt_msg = ''

        directions = directions_gui.Directions(self.dlg)
        params = directions.get_parameters()
        try:
            if self.dlg.optimization_group.isChecked():
                if len(params['jobs']
                       ) <= 1:  # Start/end locations don't count as job
                    QMessageBox.critical(
                        self.dlg, "Wrong number of waypoints",
                        """At least 3 or 4 waypoints are needed to perform routing optimization. 

Remember, the first and last location are not part of the optimization.
                        """)
                    return
                response = clnt.request('/optimization', {}, post_json=params)
                feat = directions_core.get_output_features_optimization(
                    response, params['vehicles'][0]['profile'])
            else:
                params['coordinates'] = directions.get_request_line_feature()
                profile = self.dlg.routing_travel_combo.currentText()
                response = clnt.request('/v2/directions/' + profile +
                                        '/geojson', {},
                                        post_json=params)
                feat = directions_core.get_output_feature_directions(
                    response, profile, params['preference'],
                    directions.options)

            layer_out.dataProvider().addFeature(feat)

            layer_out.updateExtents()
            self.project.addMapLayer(layer_out)

            # Update quota; handled in client module after successful request
            # if provider.get('ENV_VARS'):
            #     self.dlg.quota_text.setText(self.get_quota(provider) + ' calls')
        except exceptions.Timeout:
            msg = "The connection has timed out!"
            logger.log(msg, 2)
            self.dlg.debug_text.setText(msg)
            return

        except (exceptions.ApiError, exceptions.InvalidKey,
                exceptions.GenericServerError) as e:
            msg = (e.__class__.__name__, str(e))

            logger.log("{}: {}".format(*msg), 2)
            clnt_msg += "<b>{}</b>: ({})<br>".format(*msg)
            raise

        except Exception as e:
            msg = [e.__class__.__name__, str(e)]
            logger.log("{}: {}".format(*msg), 2)
            clnt_msg += "<b>{}</b>: {}<br>".format(*msg)
            raise

        finally:
            # Set URL in debug window
            clnt_msg += '<a href="{0}">{0}</a><br>Parameters:<br>{1}'.format(
                clnt.url, json.dumps(params, indent=2))
            self.dlg.debug_text.setHtml(clnt_msg)
    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..."))

        profile = PROFILES[self.parameterAsEnum(parameters, self.IN_PROFILE,
                                                context)]

        preference = PREFERENCES[self.parameterAsEnum(parameters,
                                                      self.IN_PREFERENCE,
                                                      context)]

        optimize = self.parameterAsBool(parameters, self.IN_OPTIMIZE, context)

        # Get parameter values
        source = self.parameterAsSource(parameters, self.IN_POINTS, context)

        source_field_idx = self.parameterAsEnum(parameters, self.IN_FIELD,
                                                context)

        source_field_name = self.parameterAsString(parameters, self.IN_FIELD,
                                                   context)

        (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), QgsWkbTypes.LineString,
            QgsCoordinateReferenceSystem(4326))
        count = source.featureCount()

        input_points = list()
        from_values = list()
        xformer = 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())))
            input_points.append(points)
            from_values.append('')
        elif source.wkbType() == QgsWkbTypes.MultiPoint:
            # loop through multipoint features
            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)))
                input_points.append(points)
                from_values.append(feat[source_field_name])

        for num, (points,
                  from_value) in enumerate(zip(input_points, from_values)):
            # Stop the algorithm if cancel button has been clicked
            if feedback.isCanceled():
                break

            try:
                if optimize:
                    params = self._get_params_optimize(points, profile)
                    response = clnt.request('/optimization', {},
                                            post_json=params)

                    sink.addFeature(
                        directions_core.get_output_features_optimization(
                            response, profile, from_value=from_value))
                else:
                    params = self._get_params_directions(
                        points, profile, preference)
                    response = clnt.request('/v2/directions/' + profile +
                                            '/geojson', {},
                                            post_json=params)

                    sink.addFeature(
                        directions_core.get_output_feature_directions(
                            response,
                            profile,
                            preference,
                            from_value=from_value))
            except (exceptions.ApiError, exceptions.InvalidKey,
                    exceptions.GenericServerError) as e:
                msg = "Feature ID {} caused a {}:\n{}".format(
                    from_value, e.__class__.__name__, str(e))
                feedback.reportError(msg)
                logger.log(msg)
                continue

            feedback.setProgress(int(100.0 / count * num))

        return {self.OUT: dest_id}
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(4326)
    providers = configmanager.read_config()['providers']

    # difference = None

    def initAlgorithm(self,
                      configuration,
                      p_str=None,
                      Any=None,
                      *args,
                      **kwargs):
        providers = [provider['name'] for provider in self.providers]
        self.addParameter(
            QgsProcessingParameterEnum(self.IN_PROVIDER,
                                       "Provider",
                                       providers,
                                       defaultValue=providers[0]))

        self.addParameter(
            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]",
                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) 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()

    # TODO: preprocess parameters to options the range clenaup 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']

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

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

            for isochrone in self.isochrones.get_features(
                    response, params['id']):
                sink.addFeature(isochrone)

        except (exceptions.ApiError, exceptions.InvalidKey,
                exceptions.GenericServerError) as e:
            msg = "Feature ID {} caused a {}:\n{}".format(
                params['id'], e.__class__.__name__, str(e))
            feedback.reportError(msg)
            logger.log(msg, 2)

        return {self.OUT: self.dest_id}

    def postProcessAlgorithm(self, context, feedback):
        """Style polygon layer in post-processing step."""
        processed_layer = QgsProcessingUtils.mapLayerFromString(
            self.dest_id, context)
        self.isochrones.stylePoly(processed_layer)

        return {self.OUT: self.dest_id}
示例#22
0
    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..."))

        profile = PROFILES[self.parameterAsEnum(parameters, self.IN_PROFILE,
                                                context)]

        preference = PREFERENCES[self.parameterAsEnum(parameters,
                                                      self.IN_PREFERENCE,
                                                      context)]

        optimize = self.parameterAsBool(parameters, self.IN_OPTIMIZE, context)

        # Get parameter values
        source = self.parameterAsSource(parameters, self.IN_LINES, context)

        source_field_idx = self.parameterAsEnum(parameters, self.IN_FIELD,
                                                context)

        source_field_name = self.parameterAsString(parameters, self.IN_FIELD,
                                                   context)

        (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), source.wkbType(),
            QgsCoordinateReferenceSystem(4326))
        count = source.featureCount()

        for num, (line, field_value) in enumerate(
                self._get_sorted_lines(source, source_field_name)):
            # Stop the algorithm if cancel button has been clicked
            if feedback.isCanceled():
                break

            try:
                if optimize:
                    params = self._get_params_optimize(line, profile)
                    response = clnt.request('/optimization', {},
                                            post_json=params)

                    sink.addFeature(
                        directions_core.get_output_features_optimization(
                            response, profile, from_value=field_value))
                else:
                    params = self._get_params_directions(
                        line, profile, preference)
                    response = clnt.request(
                        '/v2/directions/' + profile + '/geojson', params)

                    sink.addFeature(
                        directions_core.get_output_feature_directions(
                            response,
                            profile,
                            preference,
                            from_value=field_value))
            except (exceptions.ApiError, exceptions.InvalidKey,
                    exceptions.GenericServerError) as e:
                msg = "Feature ID {} caused a {}:\n{}".format(
                    line[source_field_name], e.__class__.__name__, str(e))
                feedback.reportError(msg)
                logger.log(msg)
                continue

            feedback.setProgress(int(100.0 / count * num))

        return {self.OUT: dest_id}
示例#23
0
class ORSdirectionsLinesAlgo(QgsProcessingAlgorithm):
    """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'

    providers = configmanager.read_config()['providers']

    def initAlgorithm(self,
                      configuration,
                      p_str=None,
                      Any=None,
                      *args,
                      **kwargs):

        providers = [provider['name'] for provider in self.providers]
        self.addParameter(
            QgsProcessingParameterEnum(self.IN_PROVIDER,
                                       "Provider",
                                       providers,
                                       defaultValue=providers[0]))

        self.addParameter(
            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) 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

        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 = PROFILES[self.parameterAsEnum(parameters, self.IN_PROFILE,
                                                context)]

        preference = PREFERENCES[self.parameterAsEnum(parameters,
                                                      self.IN_PREFERENCE,
                                                      context)]

        optimize = self.parameterAsBool(parameters, self.IN_OPTIMIZE, context)

        # Get parameter values
        source = self.parameterAsSource(parameters, self.IN_LINES, context)

        source_field_idx = self.parameterAsEnum(parameters, self.IN_FIELD,
                                                context)

        source_field_name = self.parameterAsString(parameters, self.IN_FIELD,
                                                   context)

        (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), source.wkbType(),
            QgsCoordinateReferenceSystem(4326))
        count = source.featureCount()

        for num, (line, field_value) in enumerate(
                self._get_sorted_lines(source, source_field_name)):
            # Stop the algorithm if cancel button has been clicked
            if feedback.isCanceled():
                break

            try:
                if optimize:
                    params = self._get_params_optimize(line, profile)
                    response = clnt.request('/optimization', {},
                                            post_json=params)

                    sink.addFeature(
                        directions_core.get_output_features_optimization(
                            response, profile, from_value=field_value))
                else:
                    params = self._get_params_directions(
                        line, profile, preference)
                    response = clnt.request(
                        '/v2/directions/' + profile + '/geojson', params)

                    sink.addFeature(
                        directions_core.get_output_feature_directions(
                            response,
                            profile,
                            preference,
                            from_value=field_value))
            except (exceptions.ApiError, exceptions.InvalidKey,
                    exceptions.GenericServerError) as e:
                msg = "Feature ID {} caused a {}:\n{}".format(
                    line[source_field_name], e.__class__.__name__, str(e))
                feedback.reportError(msg)
                logger.log(msg)
                continue

            feedback.setProgress(int(100.0 / count * num))

        return {self.OUT: dest_id}

    @staticmethod
    def _get_sorted_lines(layer, field_name):
        """
        Generator to yield geometry and ID value sorted by feature ID. Careful: feat.id() is not necessarily
        permanent

        :param layer: source input layer
        :type layer: QgsProcessingParameterFeatureSource

        :param field_name: name of ID field
        :type field_name: str
        """
        # First get coordinate transformer
        xformer = transform.transformToWGS(layer.sourceCrs())

        for feat in sorted(layer.getFeatures(), key=lambda f: f.id()):
            line = None
            field_value = feat[field_name]
            # for
            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]
                ]

            elif layer.wkbType() == QgsWkbTypes.LineString:
                line = [
                    xformer.transform(QgsPointXY(point))
                    for point in feat.geometry().asPolyline()
                ]

            yield line, field_value

    @staticmethod
    def _get_params_directions(line, profile, preference):
        """
        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

        :param preference: routing preference, shortest/fastest
        :type preference: str

        :returns: parameters for optimization endpoint
        :rtype: dict
        """

        params = {
            'coordinates':
            convert.build_coords([[point.x(), point.y()] for point in line]),
            'profile':
            profile,
            'preference':
            preference,
            'geometry':
            'true',
            'format':
            'geojson',
            'geometry_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
    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'

        params['profile'] = 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)
        ranges_proc = [x * factor for x in map(int, ranges_raw.split(','))]
        params['range'] = convert.comma_list(ranges_proc)

        # self.difference = self.parameterAsBool(parameters, self.IN_DIFFERENCE, context)
        point = self.parameterAsPoint(parameters, self.IN_POINT, context,
                                      self.crs_out)
        source = self.parameterAsSource(parameters, self.IN_POINTS, context)

        # Make the actual requests
        # If layer source is set
        requests = []
        if source:
            if source.wkbType() == 4:
                raise QgsProcessingException(
                    "TypeError: Multipoint Layers are not accepted. Please convert to single geometry layer."
                )

            # Get ID field properties
            # TODO: id_field should have a default (#90)
            id_field_name = self.parameterAsString(parameters, self.IN_FIELD,
                                                   context)
            id_field_id = source.fields().lookupField(id_field_name)
            if id_field_name == '':
                id_field_id = 0
                id_field_name = source.fields().field(id_field_id).name()
            id_field = source.fields().field(id_field_id)

            # Populate iso_layer instance with parameters
            self.isochrones.set_parameters(profile, dimension, factor,
                                           id_field.type(), id_field_name)

            for properties in self.get_sorted_feature_parameters(source):
                # Stop the algorithm if cancel button has been clicked
                if feedback.isCanceled():
                    break

                # Get transformed coordinates and feature
                params['locations'], feat = properties
                params['id'] = feat[id_field_name]
                requests.append(deepcopy(params))
        # elif point source is set
        else:
            self.isochrones.set_parameters(profile, dimension, factor)
            params['locations'] = convert.build_coords([point.x(), point.y()])
            params['id'] = None
            requests.append(params)

        (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
            self.crs_out)

        for num, params in enumerate(requests):
            # If feature causes error, report and continue with next
            try:
                # Populate features from response
                response = clnt.request(provider['endpoints'][self.ALGO_NAME],
                                        params)

                for isochrone in self.isochrones.get_features(
                        response, params['id']):
                    sink.addFeature(isochrone)

            except (exceptions.ApiError, exceptions.InvalidKey,
                    exceptions.GenericServerError) as e:
                msg = "Feature ID {} caused a {}:\n{}".format(
                    params['id'], e.__class__.__name__, str(e))
                feedback.reportError(msg)
                logger.log(msg, 2)
                continue
            if source:
                feedback.setProgress(int(100.0 / source.featureCount() * num))

        return {self.OUT: self.dest_id}
示例#25
0
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'

    providers = configmanager.read_config()['providers']

    def initAlgorithm(self, configuration, p_str=None, Any=None, *args, **kwargs):

        providers = [provider['name'] for provider in self.providers]
        self.addParameter(
            QgsProcessingParameterEnum(
                self.IN_PROVIDER,
                "Provider",
                providers,
                defaultValue=providers[0]
            )
        )

        self.addParameter(
            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) 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()

        # Get profile value
        profile = PROFILES[self.parameterAsEnum(
            parameters,
            self.IN_PROFILE,
            context
        )]

        # Get parameter values
        source = self.parameterAsSource(
            parameters,
            self.IN_START,
            context
        )
        source_field_name = self.parameterAsString(
            parameters,
            self.IN_START_FIELD,
            context
        )
        destination = self.parameterAsSource(
            parameters,
            self.IN_END,
            context
        )
        destination_field_name = self.parameterAsString(
            parameters,
            self.IN_END_FIELD,
            context
        )

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

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

        # Get source and destination features
        sources_features = list(source.getFeatures())
        destination_features = list(destination.getFeatures())
        # Get feature amounts/counts
        sources_amount = source.featureCount()
        destinations_amount = destination.featureCount()

        # Allow for 50 features in source if source == destination
        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]
        else:
            xformer = transform.transformToWGS(source.sourceCrs())
            sources_features_xformed = [xformer.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]

            features_points = sources_features_xformed + destination_features_xformed

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

        # Populate parameters further
        params.update({
            '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)

        except (exceptions.ApiError,
                exceptions.InvalidKey,
                exceptions.GenericServerError) as e:
            msg = "{}: {}".format(
                e.__class__.__name__,
                str(e))
            feedback.reportError(msg)
            logger.log(msg)

        (sink, dest_id) = self.parameterAsSink(
            parameters,
            self.OUT,
            context,
            self.get_fields(
                source_field.type(),
                destination_field.type()
            ),
            QgsWkbTypes.NoGeometry
        )

        sources_attributes = [feat.attribute(source_field_name) for feat in sources_features]
        destinations_attributes = [feat.attribute(destination_field_name) for feat in destination_features]

        for s, source in enumerate(sources_attributes):
            for d, destination in enumerate(destinations_attributes):
                duration = response['durations'][s][d]
                distance = response['distances'][s][d]
                feat = QgsFeature()
                feat.setAttributes([
                    source,
                    destination,
                    duration / 3600 if duration is not None else None,
                    distance / 1000 if distance is not None else None
                ])

                sink.addFeature(feat)

        return {self.OUT: dest_id}

    @staticmethod
    def get_fields(source_type, destination_type):

        fields = QgsFields()
        fields.append(QgsField("FROM_ID", source_type))
        fields.append(QgsField("TO_ID", destination_type))
        fields.append(QgsField("DURATION_H", QVariant.Double))
        fields.append(QgsField("DIST_KM", QVariant.Double))

        return fields

    @staticmethod
    def chunks(l, n):
        """Yield successive n-sized chunks from l."""
        for i in range(0, len(l), n):
            yield l[i:i + n]
示例#26
0
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'

    providers = configmanager.read_config()['providers']

    def initAlgorithm(self,
                      configuration,
                      p_str=None,
                      Any=None,
                      *args,
                      **kwargs):

        providers = [provider['name'] for provider in self.providers]
        self.addParameter(
            QgsProcessingParameterEnum(self.IN_PROVIDER,
                                       "Provider",
                                       providers,
                                       defaultValue=providers[0]))

        self.addParameter(
            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) 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 clenaup 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..."))

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

        # Get parameter values
        source = self.parameterAsSource(parameters, self.IN_START, context)
        source_field_name = self.parameterAsString(parameters,
                                                   self.IN_START_FIELD,
                                                   context)
        destination = self.parameterAsSource(parameters, self.IN_END, context)
        destination_field_name = self.parameterAsString(
            parameters, self.IN_END_FIELD, context)

        # 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
        }

        route_dict = self._get_route_dict(source, source_field, destination,
                                          destination_field)

        if mode == 'Row-by-Row':
            route_count = min(
                [source.featureCount(),
                 destination.featureCount()])
        else:
            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()),
            QgsWkbTypes.LineString, QgsCoordinateReferenceSystem(4326))

        counter = 0
        for coordinates, values in directions_core.get_request_point_features(
                route_dict, mode):
            # Stop the algorithm if cancel button has been clicked
            if feedback.isCanceled():
                break

            params['coordinates'] = coordinates

            try:
                response = clnt.request('/v2/directions/' + profile +
                                        '/geojson', {},
                                        post_json=params)
            except (exceptions.ApiError, exceptions.InvalidKey,
                    exceptions.GenericServerError) as e:
                msg = "Route from {} to {} caused a {}:\n{}".format(
                    values[0], values[1], e.__class__.__name__, str(e))
                feedback.reportError(msg)
                logger.log(msg)
                continue

            sink.addFeature(
                directions_core.get_output_feature_directions(
                    response,
                    profile,
                    preference,
                    from_value=values[0],
                    to_value=values[1]))

            counter += 1
            feedback.setProgress(int(100.0 / route_count * counter))

        return {self.OUT: dest_id}

    def _get_route_dict(self, source, source_field, destination,
                        destination_field):
        """
        Compute route_dict from input layer.

        :param source: Input from layer
        :type source: QgsProcessingParameterFeatureSource

        :param source_field: ID field from layer.
        :type source_field: QgsField

        :param destination: Input to layer.
        :type destination: QgsProcessingParameterFeatureSource

        :param destination_field: ID field to layer.
        :type destination_field: QgsField

        :returns: route_dict with coordinates and ID values
        :rtype: dict
        """
        route_dict = dict()

        source_feats = list(source.getFeatures())
        xformer_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
            ],
        )

        destination_feats = list(destination.getFeatures())
        xformer_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
            ],
        )

        return route_dict