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])
Example #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))
Example #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)
Example #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()
Example #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
Example #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}
Example #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
Example #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
            )
        )
Example #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
            )
        )
Example #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}
Example #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}
Example #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}
Example #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]
Example #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