Пример #1
0
    def handle_response(self, response, feat_id):
        """
        Casts JSON response to dict

        :raises valhalla.utils.exceptions.OverQueryLimitError: when rate limit is exhausted, HTTP 429
        :raises valhalla.utils.exceptions.ApiError: when the backend API throws an error, HTTP 400
        :raises valhalla.utils.exceptions.InvalidKey: when API key is invalid (or quota is exceeded), HTTP 403
        :raises valhalla.utils.exceptions.GenericServerError: all other HTTP errors

        :returns: response body
        :rtype: dict
        """

        self.status_code = response.attribute(
            QNetworkRequest.HttpStatusCodeAttribute)
        if response.error():
            # First try non-HTTP error codes
            error_code = response.error()
            error_msg = response.errorString()
            if error_code in (QNetworkReply.ConnectionRefusedError,
                              QNetworkReply.HostNotFoundError):
                raise exceptions.GenericServerError(
                    1, f"Host {self.base_url} not valid.")
            elif error_code == QNetworkReply.TimeoutError:
                raise exceptions.Timeout("Request timed out.")

            if self.status_code == 401:
                raise exceptions.InvalidKey(str(self.status_code), error_msg)
            elif self.status_code == 429:
                logger.log("{}: {}".format(exceptions.OverQueryLimit.__name__,
                                           "Query limit exceeded"))
                raise exceptions.OverQueryLimit(str(429), error_msg)
            # Internal error message for Bad Request
            elif self.status_code and 400 <= self.status_code < 500:
                logger.log("Feature ID {} caused a {}: {}".format(
                    feat_id, exceptions.ApiError.__name__, error_msg, 2))
                raise exceptions.ApiError(str(self.status_code), error_msg)
            else:
                raise exceptions.GenericServerError(str(self.status_code),
                                                    error_msg)
Пример #2
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"))

        mode = self.MODE_TYPES[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)
        avoid_layer = self.parameterAsSource(parameters, self.IN_AVOID,
                                             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)

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

        # 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 feature amounts/counts
        sources_amount = source.featureCount()
        destinations_amount = destination.featureCount()
        if (sources_amount or destinations_amount) > 10000:
            raise QgsProcessingException(
                "ProcessingError: Too large input, please decimate.")

        sources_features = list(source.getFeatures())
        destinations_features = list(destination.getFeatures())

        # Get source and destination features
        xformer_source = transform.transformToWGS(source.sourceCrs())
        sources_points = [
            xformer_source.transform(feat.geometry().asPoint())
            for feat in sources_features
        ]
        xformer_destination = transform.transformToWGS(destination.sourceCrs())
        destination_points = [
            xformer_destination.transform(feat.geometry().asPoint())
            for feat in destinations_features
        ]

        # Build params
        params = dict(costing=self.PROFILE)

        # Sets all advanced parameters as attributes of self.costing_options
        self.costing_options.set_costing_options(self, parameters, context)

        costing_params = get_costing_options(self.costing_options,
                                             self.PROFILE, mode)
        if costing_params:
            params['costing_options'] = costing_params

        if avoid_layer:
            params['avoid_locations'] = get_avoid_locations(avoid_layer)

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

        source_attr_iter = self._chunks(sources_attributes, 50)
        for sources in self._chunks(sources_points, 50):
            params["sources"] = get_locations(sources)
            source_attributes = next(source_attr_iter)

            destination_attr_iter = self._chunks(destinations_attributes, 50)
            for destinations in self._chunks(destination_points, 50):
                params["targets"] = get_locations(destinations)
                params["id"] = "matrix"
                destination_attributes = next(destination_attr_iter)

                # Make request and catch ApiError
                try:
                    response = clnt.request('/sources_to_targets',
                                            post_json=params)
                except (exceptions.ApiError) as e:
                    msg = "{}: {}".format(e.__class__.__name__, str(e))
                    feedback.reportError(msg)
                    logger.log(msg)
                    continue
                except (exceptions.InvalidKey,
                        exceptions.GenericServerError) as e:
                    msg = "{}:\n{}".format(e.__class__.__name__, str(e))
                    logger.log(msg)
                    raise

                feats = matrix_core.get_output_features_matrix(
                    response, self.PROFILE, costing_params, source_attributes,
                    destination_attributes)

                for feat in feats:
                    sink.addFeature(feat)

        return {self.OUT: dest_id}
    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..."))

        mode = self.MODE_TYPES[self.parameterAsEnum(parameters, self.IN_MODE, context)]

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

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

        avoid_layer = self.parameterAsLayer(
            parameters,
            self.IN_AVOID,
            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))
        input_points = list()
        from_values = list()
        xformer_source = 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_source.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_source.transform(QgsPointXY(point)))
                input_points.append(points)
                from_values.append(feat[source_field_name])

        count = source.featureCount()

        params = dict()
        if avoid_layer:
            params['avoid_locations'] = get_avoid_locations(avoid_layer)

        # Sets all advanced parameters as attributes of self.costing_options
        self.costing_options.set_costing_options(self, parameters, context)

        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

            params.update(get_directions_params(points, self.PROFILE, self.costing_options, mode))
            params['id'] = from_value

            try:
                response = clnt.request('/route', post_json=params)
            except (exceptions.ApiError) as e:
                msg = "Feature ID {} caused a {}:\n{}".format(
                    from_value,
                    e.__class__.__name__,
                    str(e))
                feedback.reportError(msg)
                logger.log(msg)
                continue

            except (exceptions.InvalidKey, exceptions.GenericServerError) as e:
                msg = "{}:\n{}".format(
                    e.__class__.__name__,
                    str(e))
                logger.log(msg)
                raise

            options = {}
            if params.get('costing_options'):
                options = params['costing_options']

            sink.addFeature(directions_core.get_output_feature_directions(
                response,
                self.PROFILE,
                options.get(self.PROFILE),
                from_value=from_value
            ))

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

        return {self.OUT: dest_id}
    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..."))

        mode = self.MODE_TYPES[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
        )

        matrix_mode = self.MODE_SELECTION[self.parameterAsEnum(
            parameters,
            self.IN_MATRIX_MODE,
            context
        )]

        avoid_layer = self.parameterAsLayer(
            parameters,
            self.IN_AVOID,
            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)

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

        if matrix_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

        params = dict()
        if avoid_layer:
            params['avoid_locations'] = get_avoid_locations(avoid_layer)

        # Sets all advanced parameters as attributes of self.costing_options
        self.costing_options.set_costing_options(self, parameters, context)

        for points, values in directions_core.get_request_point_features(route_dict, matrix_mode):
            # Stop the algorithm if cancel button has been clicked
            if feedback.isCanceled():
                break

            params.update(get_directions_params(points, self.PROFILE, self.costing_options, mode))
            params['id'] = f"{values[0]} & {values[1]}"

            try:
                response = clnt.request('/route', post_json=params)
            except (exceptions.ApiError) 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

            except (exceptions.InvalidKey, exceptions.GenericServerError) as e:
                msg = "{}:\n{}".format(
                    e.__class__.__name__,
                    str(e))
                logger.log(msg)
                raise

            options = {}
            if params.get('costing_options'):
                options = params['costing_options']

            sink.addFeature(directions_core.get_output_feature_directions(
                response,
                self.PROFILE,
                options.get(self.PROFILE),
                from_value=values[0],
                to_value=values[1]
            ))

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

        return {self.OUT: dest_id}
    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()

        geometry_param = self.GEOMETRY_TYPES[self.parameterAsEnum(
            parameters, self.IN_GEOMETRY, context)]
        params[
            self.IN_GEOMETRY] = True if geometry_param == 'Polygon' else False

        mode = self.MODE_TYPES[self.parameterAsEnum(parameters, self.IN_MODE,
                                                    context)]

        source = self.parameterAsSource(parameters, self.IN_POINTS, context)
        if source.wkbType() == 4:
            raise QgsProcessingException(
                "TypeError: Multipoint Layers are not accepted. Please convert to single geometry layer."
            )

        # Get ID field properties
        id_field_name = self.parameterAsString(parameters, self.IN_FIELD,
                                               context)
        id_field_id = source.fields().lookupField(id_field_name)
        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(self.PROFILE, geometry_param,
                                       id_field.type(), id_field_name)

        layer_time = QgsVectorLayer(f'{geometry_param}?crs=EPSG:4326',
                                    f'Isochrones {self.PROFILE.capitalize()}',
                                    'memory')
        self.isos_time_id = layer_time.id()
        layer_time_pr = layer_time.dataProvider()
        layer_time_pr.addAttributes(self.isochrones.get_fields())
        layer_time.updateFields()

        layer_dist = QgsVectorLayer(
            f'{geometry_param}?crs=EPSG:4326',
            f'Isodistances {self.PROFILE.capitalize()}', 'memory')
        self.isos_dist_id = layer_dist.id()
        layer_dist_pr = layer_dist.dataProvider()
        layer_dist_pr.addAttributes(self.isochrones.get_fields())
        layer_dist.updateFields()

        layer_snapped_points = QgsVectorLayer(
            f'MultiPoint?crs=EPSG:4326',
            f'Snapped Points {self.PROFILE.capitalize()}', 'memory')
        self.points_snapped_id = layer_snapped_points.id()
        layer_snapped_points_pr = layer_snapped_points.dataProvider()
        layer_snapped_points_pr.addAttributes(
            self.isochrones.get_point_fields())
        layer_snapped_points.updateFields()

        layer_input_points = QgsVectorLayer(
            f'Point?crs=EPSG:4326',
            f'Input Points {self.PROFILE.capitalize()}', 'memory')
        self.points_input_id = layer_input_points.id()
        layer_input_points_pr = layer_input_points.dataProvider()
        layer_input_points_pr.addAttributes(self.isochrones.get_point_fields())
        layer_input_points.updateFields()

        denoise = self.parameterAsDouble(parameters, self.IN_DENOISE, context)
        if denoise:
            params[self.IN_DENOISE] = denoise

        generalize = self.parameterAsDouble(parameters, self.IN_GENERALIZE,
                                            context)
        if generalize:
            params[self.IN_GENERALIZE] = generalize

        avoid_layer = self.parameterAsLayer(parameters, self.IN_AVOID, context)
        if avoid_layer:
            params['avoid_locations'] = get_avoid_locations(avoid_layer)

        show_locations = self.parameterAsBool(parameters,
                                              self.IN_SHOW_LOCATIONS, context)

        # Sets all advanced parameters as attributes of self.costing_options
        self.costing_options.set_costing_options(self, parameters, context)

        intervals_time = self.parameterAsString(parameters,
                                                self.IN_INTERVALS_TIME,
                                                context)
        intervals_distance = self.parameterAsString(parameters,
                                                    self.IN_INTERVALS_DISTANCE,
                                                    context)

        feat_count = source.featureCount(
        ) if not intervals_time else source.featureCount() * 2

        self.intervals = {
            "time": [{
                "time": int(x)
            } for x in intervals_time.split(',')] if intervals_time else [],
            "distance": [{
                "distance": int(x)
            } for x in intervals_distance.split(',')]
            if intervals_distance else []
        }

        counter = 0

        for metric, interv in self.intervals.items():
            if feedback.isCanceled():
                break
            if not interv:
                continue
            # Make the actual requests
            requests = []
            for properties in self.get_sorted_feature_parameters(source):
                if feedback.isCanceled():
                    break
                r_params = deepcopy(params)
                r_params['contours'] = interv
                # Get transformed coordinates and feature
                locations, feat = properties
                r_params.update(
                    get_directions_params(locations, self.PROFILE,
                                          self.costing_options, mode))
                r_params['id'] = feat[id_field_name]
                requests.append(r_params)

            for params in requests:
                counter += 1
                if feedback.isCanceled():
                    break
                # If feature causes error, report and continue with next
                try:
                    # Populate features from response
                    response = clnt.request('/isochrone', post_json=params)
                except (exceptions.ApiError) as e:
                    msg = "Feature ID {} caused a {}:\n{}".format(
                        params['id'], e.__class__.__name__, str(e))
                    feedback.reportError(msg)
                    logger.log(msg, 2)
                    continue
                except (exceptions.InvalidKey,
                        exceptions.GenericServerError) as e:
                    msg = "{}:\n{}".format(e.__class__.__name__, str(e))
                    feedback.reportError(msg)
                    logger.log(msg)
                    raise

                options = {}
                if params.get('costing_options'):
                    options = params['costing_options']

                self.isochrones.set_response(response)
                for isochrone in self.isochrones.get_features(
                        params['id'], options.get(self.PROFILE)):
                    if metric == 'time':
                        layer_time_pr.addFeature(isochrone)
                    elif metric == 'distance':
                        layer_dist_pr.addFeature(isochrone)

                if show_locations:
                    for point_feat in self.isochrones.get_multipoint_features(
                            params['id']):
                        layer_snapped_points_pr.addFeature(point_feat)
                    for point_feat in self.isochrones.get_point_features(
                            params['id']):
                        layer_input_points_pr.addFeature(point_feat)

                feedback.setProgress(int((counter / feat_count) * 100))

        temp = []
        if layer_time.hasFeatures():
            layer_time.updateExtents()
            context.temporaryLayerStore().addMapLayer(layer_time)
            temp.append(
                ("Isochrones " + self.PROFILE.capitalize(), self.OUT_TIME,
                 layer_time.id()))
        if layer_dist.hasFeatures():
            layer_dist.updateExtents()
            context.temporaryLayerStore().addMapLayer(layer_dist)
            temp.append(
                ("Isochrones " + self.PROFILE.capitalize(), self.OUT_DISTANCE,
                 layer_dist.id()))
        if show_locations:
            layer_snapped_points.updateExtents()
            context.temporaryLayerStore().addMapLayer(layer_snapped_points)
            temp.append(("Snapped Points " + self.PROFILE.capitalize(),
                         self.POINTS_SNAPPED, layer_snapped_points.id()))

            layer_input_points.updateExtents()
            context.temporaryLayerStore().addMapLayer(layer_input_points)
            temp.append(("Input Points " + self.PROFILE.capitalize(),
                         self.POINTS_INPUT, layer_input_points.id()))

        results = dict()
        for l_name, e_id, l_id in temp:
            results[e_id] = l_id
            context.addLayerToLoadOnCompletion(
                l_id,
                QgsProcessingContext.LayerDetails(l_name, context.project(),
                                                  l_name))

        return results
    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..."))

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

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

        avoid_layer = self.parameterAsLayer(parameters, self.IN_AVOID, context)
        mode = self.MODE_TYPES[self.parameterAsEnum(parameters, self.IN_MODE,
                                                    context)]

        params = dict()
        # Sets all advanced parameters as attributes of self.costing_options
        self.costing_options.set_costing_options(self, parameters, context)
        if avoid_layer:
            params['avoid_locations'] = get_avoid_locations(avoid_layer)

        (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.update(
                get_directions_params(line, self.PROFILE, self.costing_options,
                                      mode))
            params['id'] = field_value

            try:
                response = clnt.request('/route', post_json=params)
            except (exceptions.ApiError) as e:
                msg = "Feature ID {} caused a {}:\n{}".format(
                    field_value, e.__class__.__name__, str(e))
                feedback.reportError(msg)
                logger.log(msg)
                continue

            except (exceptions.InvalidKey, exceptions.GenericServerError) as e:
                msg = "{}:\n{}".format(e.__class__.__name__, str(e))
                logger.log(msg)
                raise

            options = {}
            if params.get('costing_options'):
                options = params['costing_options']

            sink.addFeature(
                directions_core.get_output_feature_directions(
                    response,
                    self.PROFILE,
                    options.get(self.PROFILE),
                    from_value=field_value))

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

        return {self.OUT: dest_id}
Пример #7
0
    def request(self,
                url,
                first_request_time=None,
                retry_counter=0,
                post_json=None):
        """Performs HTTP GET/POST with credentials, returning the body as
        JSON.

        :param url: URL extension for request. Should begin with a slash.
        :type url: string

        :param first_request_time: The time of the first request (None if no
            retries have occurred).
        :type first_request_time: datetime.datetime

        :param post_json: Parameters for POST endpoints
        :type post_json: dict

        :raises valhalla.utils.exceptions.ApiError: when the API returns an error.

        :returns: openrouteservice response body
        :rtype: dict
        """

        if not first_request_time:
            first_request_time = datetime.now()

        elapsed = datetime.now() - first_request_time
        if elapsed > self.retry_timeout:
            raise exceptions.Timeout()

        if retry_counter > 0:
            # 0.5 * (1.5 ^ i) is an increased sleep time of 1.5x per iteration,
            # starting at 0.5s when retry_counter=1. The first retry will occur
            # at 1, so subtract that first.
            delay_seconds = 1.5**(retry_counter - 1)

            # Jitter this value by 50% and pause.
            time.sleep(delay_seconds * (random.random() + 0.5))

        # Define the request
        params = {'access_token': self.key}
        authed_url = self._generate_auth_url(
            url,
            params,
        )
        url_object = QUrl(self.base_url + authed_url)
        self.url = url_object.url()
        body = QJsonDocument.fromJson(json.dumps(post_json).encode())
        request = QNetworkRequest(url_object)
        request.setHeader(QNetworkRequest.ContentTypeHeader,
                          'application/json')

        logger.log(
            "url: {}\nParameters: {}".format(
                self.url,
                # final_requests_kwargs
                json.dumps(post_json, indent=2)),
            0)

        start = time.time()
        response: QgsNetworkReplyContent = self.nam.blockingPost(
            request, body.toJson())
        self.response_time = time.time() - start

        try:
            self.handle_response(response, post_json['id'])
        except exceptions.OverQueryLimit:
            # Let the instances know smth happened
            self.overQueryLimit.emit()
            return self.request(url, first_request_time, retry_counter + 1,
                                post_json)

        response_content = json.loads(bytes(response.content()))

        # Mapbox treats 400 errors with a 200 status code
        if 'error' in response_content:
            raise exceptions.ApiError(str(response_content['status_code']),
                                      response_content['error'])

        return response_content
Пример #8
0
    def run_gui_control(self):
        """Slot function for OK button of main dialog."""

        self.dlg: ValhallaDialog

        # 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, "No waypoints", """
                Did you forget to set routing waypoints?<br><br>
                
                Use the 'Add Waypoint' button to add up to 20 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 Mapbox?<br><br>
                
                If you don't have an API key, please visit https://account.mapbox.com/auth/signup/?route-to="/" to get one. <br><br>
                Then enter the API key in Web ► Valhalla ► Provider Settings or the settings symbol in the main Valhalla GUI, next to the provider dropdown.
                """)
            return

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

        method = self.dlg.routing_method.currentText()
        profile = self.dlg.routing_travel_combo.currentText()
        params = {}
        # Add extra params
        extra_params_text = self.dlg.dlg_params.extra_params_text.toPlainText()
        extra_params = {}
        if extra_params_text:
            extra_params = json.loads(extra_params_text)
        try:
            if method == 'route':
                layer_out = QgsVectorLayer("LineString?crs=EPSG:4326",
                                           f"Route {profile.capitalize()}",
                                           "memory")
                layer_out.dataProvider().addAttributes(
                    directions_core.get_fields())
                layer_out.updateFields()

                directions = directions_gui.Directions(self.dlg)
                params = directions.get_parameters()
                params.update(extra_params)
                response = clnt.request('/route', {}, post_json=params)
                feat = directions_core.get_output_feature_directions(
                    response, profile, directions.costing_options,
                    "{}, {}".format(params['locations'][0]['lon'],
                                    params['locations'][0]['lat']),
                    "{}, {}".format(params['locations'][-1]['lon'],
                                    params['locations'][-1]['lat']))
                layer_out.dataProvider().addFeature(feat)
                layer_out.updateExtents()
                self.project.addMapLayer(layer_out)

            elif method == 'isochrone':
                geometry_type = self.dlg.polygons.currentText()
                isochrones = isochrones_core.Isochrones()
                isochrones.set_parameters(profile, geometry_type)
                locations = get_locations(self.dlg.routing_fromline_list)

                aggregate = self.dlg.iso_aggregate.isChecked()
                locations = [locations] if aggregate else locations

                no_points = self.dlg.iso_no_points.isChecked()

                metrics = []
                if self.dlg.contours_distance.text():
                    metrics.append('distance')
                if self.dlg.contours.text():
                    metrics.append('time')

                for metric in metrics:
                    isochrones_ui = isochrones_gui.Isochrones(self.dlg)
                    params = isochrones_ui.get_parameters(
                        metric)  # change once isodistances are there too
                    params.update(extra_params)

                    name = 'Isodistance' if metric == 'distance' else 'Isochrone'
                    layer_out = QgsVectorLayer(
                        f"{geometry_type}?crs=EPSG:4326",
                        f"{name} {params['costing']}", "memory")
                    layer_out.dataProvider().addAttributes(
                        isochrones.get_fields())
                    layer_out.updateFields()

                    for i, location in enumerate(locations):
                        params['locations'] = location if aggregate else [
                            location
                        ]
                        isochrones.set_response(
                            clnt.request('/isochrone', {}, post_json=params))
                        for feat in isochrones.get_features(
                                str(i), isochrones_ui.costing_options):
                            layer_out.dataProvider().addFeature(feat)

                    layer_out.updateExtents()
                    isochrones.stylePoly(layer_out, metric)
                    self.project.addMapLayer(layer_out)

                if not no_points:
                    multipoint_layer = QgsVectorLayer(
                        "MultiPoint?crs=EPSG:4326",
                        f"Snapped Points {params['costing']}", "memory")
                    point_layer = QgsVectorLayer(
                        "Point?crs=EPSG:4326",
                        f"Input Points {params['costing']}", "memory")

                    multipoint_layer.dataProvider().addAttributes(
                        isochrones.get_point_fields())
                    multipoint_layer.updateFields()
                    point_layer.dataProvider().addAttributes(
                        isochrones.get_point_fields())
                    point_layer.updateFields()

                    for feat in isochrones.get_multipoint_features('0'):
                        multipoint_layer.dataProvider().addFeature(feat)
                    for feat in isochrones.get_point_features('0'):
                        point_layer.dataProvider().addFeature(feat)
                    multipoint_layer.updateExtents()
                    point_layer.updateExtents()
                    self.project.addMapLayer(multipoint_layer)
                    self.project.addMapLayer(point_layer)

            elif method == 'sources_to_targets':
                layer_out = QgsVectorLayer("None", 'Matrix_Valhalla', "memory")
                layer_out.dataProvider().addAttributes(
                    matrix_core.get_fields())
                layer_out.updateFields()

                matrix = matrix_gui.Matrix(self.dlg)
                params = matrix.get_parameters()
                params.update(extra_params)
                response = clnt.request('/sources_to_targets',
                                        post_json=params)
                feats = matrix_core.get_output_features_matrix(
                    response, profile, matrix.costing_options)
                for feat in feats:
                    layer_out.dataProvider().addFeature(feat)

                self.project.addMapLayer(layer_out)

            elif method == 'locate':
                locate_dlg = ValhallaDialogLocateMain()
                locate_dlg.setWindowTitle('Locate Response')

                locate = locate_gui.Locate(self.dlg)
                params = locate.get_parameters()
                params.update(extra_params)
                response = clnt.request('/locate', post_json=params)

                locate_dlg.responseArrived.emit(json.dumps(response, indent=4))

                locate_dlg.exec_()

            elif method == 'extract-osm':
                if not which('osmium'):
                    QMessageBox.critical(
                        self.dlg, "ModuleNotFoundError",
                        """<a href="https://osmcode.org/osmium-tool/">osmium</a> wasn\'t found in your PATH. <br/><br/>Please install before trying again."""
                    )
                    return
                if not self.dlg.pbf_file.filePath():
                    QMessageBox.critical(
                        self.dlg, "FileNotFoundError",
                        """Seems like you forgot to set a PBF file path in the configuration for the Identity tool."""
                    )
                    return

                identify = identify_gui.Identify(self.dlg)
                params = identify.get_locate_parameters()
                response = clnt.request('/locate', post_json=params)
                way_dict = identify.get_tags(response)

                for way_id in way_dict:
                    way = way_dict[way_id]

                    layer_out = QgsVectorLayer("LineString?crs=EPSG:4326",
                                               "Way " + str(way_id), "memory")
                    layer_out.dataProvider().addAttributes(
                        identify.get_fields(way["tags"]))
                    layer_out.updateFields()

                    feat = identify.get_output_feature(way)
                    layer_out.dataProvider().addFeature(feat)
                    layer_out.updateExtents()

                    self.project.addMapLayer(layer_out)
            elif method == 'centroid [experimental]':
                layer_routes = QgsVectorLayer("LineString?crs=EPSG:4326",
                                              f"Centroid Routes {profile}",
                                              "memory")
                layer_gravity = QgsVectorLayer("Point?crs=EPSG:4326",
                                               f"Centroid Point {profile}",
                                               "memory")
                layer_routes.dataProvider().addAttributes(
                    gravity_core.get_fields())
                layer_gravity.dataProvider().addAttributes(
                    gravity_core.get_fields())
                layer_routes.updateFields()
                layer_gravity.updateFields()

                directions = directions_gui.Directions(self.dlg)
                params = directions.get_parameters()
                params.update(extra_params)
                response = clnt.request('/centroid', {}, post_json=params)
                line_feats, point_feat = gravity_core.get_output_feature_gravity(
                    response, profile, directions.costing_options)
                layer_routes.dataProvider().addFeatures(line_feats)
                layer_gravity.dataProvider().addFeature(point_feat)

                layer_routes.updateExtents()
                layer_gravity.updateExtents()

                self.project.addMapLayer(layer_routes)
                self.project.addMapLayer(layer_gravity)

        except exceptions.Timeout as e:
            msg = "The connection has timed out!"
            logger.log(msg, 2)
            self.dlg.debug_text.setText(msg)
            self._display_error_popup(e)
            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)
            self._display_error_popup(e)
            return

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

        finally:
            # Set URL in debug window
            clnt_msg += '<a href="{0}">{0}</a><br>Parameters:<br>{1}<br><b>timing</b>: {2:.3f} secs'.format(
                clnt.url, json.dumps(params, indent=2), clnt.response_time)
            self.dlg.debug_text.setHtml(clnt_msg)