diff --git a/ORStools/ORStoolsPlugin.py b/ORStools/ORStoolsPlugin.py index e95558de..b22866cd 100644 --- a/ORStools/ORStoolsPlugin.py +++ b/ORStools/ORStoolsPlugin.py @@ -37,6 +37,7 @@ class ORStools: """QGIS Plugin Implementation.""" + # noinspection PyTypeChecker,PyArgumentList,PyCallByClass def __init__(self, iface): @@ -54,17 +55,16 @@ def __init__(self, iface): self.plugin_dir = os.path.dirname(__file__) # initialize locale - locale = QSettings().value('locale/userLocale')[0:2] + locale = QSettings().value("locale/userLocale")[0:2] locale_path = os.path.join( - self.plugin_dir, - 'i18n', - 'orstools_{}.qm'.format(locale)) + self.plugin_dir, "i18n", "orstools_{}.qm".format(locale) + ) if os.path.exists(locale_path): self.translator = QTranslator() self.translator.load(locale_path) - if qVersion() > '4.3.3': + if qVersion() > "4.3.3": QCoreApplication.installTranslator(self.translator) def initGui(self): diff --git a/ORStools/__init__.py b/ORStools/__init__.py index 155acf1c..5e82c330 100644 --- a/ORStools/__init__.py +++ b/ORStools/__init__.py @@ -41,28 +41,28 @@ def classFactory(iface): # pylint: disable=invalid-name """ from .ORStoolsPlugin import ORStools + return ORStools(iface) # Define plugin wide constants -PLUGIN_NAME = 'ORS Tools' -DEFAULT_COLOR = '#a8b1f5' +PLUGIN_NAME = "ORS Tools" +DEFAULT_COLOR = "#a8b1f5" BASE_DIR = os.path.dirname(os.path.abspath(__file__)) RESOURCE_PREFIX = ":plugins/ORStools/img/" -CONFIG_PATH = os.path.join(BASE_DIR, 'config.yml') -ENV_VARS = {'ORS_REMAINING': 'X-Ratelimit-Remaining', - 'ORS_QUOTA': 'X-Ratelimit-Limit'} +CONFIG_PATH = os.path.join(BASE_DIR, "config.yml") +ENV_VARS = {"ORS_REMAINING": "X-Ratelimit-Remaining", "ORS_QUOTA": "X-Ratelimit-Limit"} # Read metadata.txt METADATA = configparser.ConfigParser() -METADATA.read(os.path.join(BASE_DIR, 'metadata.txt'), encoding='utf-8') +METADATA.read(os.path.join(BASE_DIR, "metadata.txt"), encoding="utf-8") today = datetime.today() -__version__ = METADATA['general']['version'] -__author__ = METADATA['general']['author'] -__email__ = METADATA['general']['email'] -__web__ = METADATA['general']['homepage'] -__help__ = METADATA['general']['help'] -__date__ = today.strftime('%Y-%m-%d') -__copyright__ = f'(C) {today.year} by {__author__}' +__version__ = METADATA["general"]["version"] +__author__ = METADATA["general"]["author"] +__email__ = METADATA["general"]["email"] +__web__ = METADATA["general"]["homepage"] +__help__ = METADATA["general"]["help"] +__date__ = today.strftime("%Y-%m-%d") +__copyright__ = f"(C) {today.year} by {__author__}" diff --git a/ORStools/common/__init__.py b/ORStools/common/__init__.py index b714c41f..dcf05192 100644 --- a/ORStools/common/__init__.py +++ b/ORStools/common/__init__.py @@ -28,25 +28,35 @@ """ PROFILES = [ - 'driving-car', - 'driving-hgv', - 'cycling-regular', - 'cycling-road', - 'cycling-mountain', - 'cycling-electric', - 'foot-walking', - 'foot-hiking', - 'wheelchair' - ] - -DIMENSIONS = ['time', 'distance'] - -PREFERENCES = ['fastest', 'shortest', 'recommended'] - -OPTIMIZATION_MODES = ['Round Trip', 'Fix Start Point', 'Fix End Point', 'Fix Start and End Point'] - -AVOID_FEATURES = ['highways', 'tollways', 'ferries', 'fords', 'steps'] - -AVOID_BORDERS = ['all', 'controlled', 'none'] - -ADVANCED_PARAMETERS = ["INPUT_AVOID_FEATURES", "INPUT_AVOID_BORDERS", "INPUT_AVOID_COUNTRIES", "INPUT_AVOID_POLYGONS"] + "driving-car", + "driving-hgv", + "cycling-regular", + "cycling-road", + "cycling-mountain", + "cycling-electric", + "foot-walking", + "foot-hiking", + "wheelchair", +] + +DIMENSIONS = ["time", "distance"] + +PREFERENCES = ["fastest", "shortest", "recommended"] + +OPTIMIZATION_MODES = [ + "Round Trip", + "Fix Start Point", + "Fix End Point", + "Fix Start and End Point", +] + +AVOID_FEATURES = ["highways", "tollways", "ferries", "fords", "steps"] + +AVOID_BORDERS = ["all", "controlled", "none"] + +ADVANCED_PARAMETERS = [ + "INPUT_AVOID_FEATURES", + "INPUT_AVOID_BORDERS", + "INPUT_AVOID_COUNTRIES", + "INPUT_AVOID_POLYGONS", +] diff --git a/ORStools/common/client.py b/ORStools/common/client.py index 042f3afe..1534f4ec 100644 --- a/ORStools/common/client.py +++ b/ORStools/common/client.py @@ -57,33 +57,33 @@ def __init__(self, provider=None): """ QObject.__init__(self) - self.key = provider['key'] - self.base_url = provider['base_url'] - self.ENV_VARS = provider.get('ENV_VARS') + self.key = provider["key"] + self.base_url = provider["base_url"] + self.ENV_VARS = provider.get("ENV_VARS") # self.session = requests.Session() - retry_timeout = provider.get('timeout') + retry_timeout = provider.get("timeout") - self.nam = networkaccessmanager.NetworkAccessManager(debug=False, timeout=retry_timeout) + self.nam = networkaccessmanager.NetworkAccessManager( + debug=False, timeout=retry_timeout + ) self.retry_timeout = timedelta(seconds=retry_timeout) self.headers = { - "User-Agent": _USER_AGENT, - 'Content-type': 'application/json', - 'Authorization': provider['key'] - } + "User-Agent": _USER_AGENT, + "Content-type": "application/json", + "Authorization": provider["key"], + } # Save some references to retrieve in client instances self.url = None self.warnings = None overQueryLimit = pyqtSignal() - def request(self, - url, - params, - first_request_time=None, - retry_counter=0, - post_json=None): + + def request( + self, url, params, first_request_time=None, retry_counter=0, post_json=None + ): """Performs HTTP GET/POST with credentials, returning the body as JSON. @@ -124,14 +124,15 @@ def request(self, # 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) + delay_seconds = 1.5 ** (retry_counter - 1) # Jitter this value by 50% and pause. time.sleep(delay_seconds * (random.random() + 0.5)) - authed_url = self._generate_auth_url(url, - params, - ) + authed_url = self._generate_auth_url( + url, + params, + ) self.url = self.base_url + authed_url # Default to the client-level self.requests_kwargs, with method-level @@ -140,25 +141,24 @@ def request(self, # Determine GET/POST # requests_method = self.session.get - requests_method = 'GET' + requests_method = "GET" body = None if post_json is not None: # requests_method = self.session.post # final_requests_kwargs["json"] = post_json body = post_json - requests_method = 'POST' + requests_method = "POST" - logger.log( - f"url: {self.url}\nParameters: {json.dumps(body, indent=2)}", - 0 - ) + logger.log(f"url: {self.url}\nParameters: {json.dumps(body, indent=2)}", 0) try: - response, content = self.nam.request(self.url, - method=requests_method, - body=body, - headers=self.headers, - blocking=True) + response, content = self.nam.request( + self.url, + method=requests_method, + body=body, + headers=self.headers, + blocking=True, + ) except networkaccessmanager.RequestsExceptionTimeout: raise exceptions.Timeout @@ -167,16 +167,20 @@ def request(self, self._check_status() except exceptions.OverQueryLimit as e: - # Let the instances know something happened # noinspection PyUnresolvedReferences self.overQueryLimit.emit() logger.log(f"{e.__class__.__name__}: {str(e)}", 1) - return self.request(url, params, first_request_time, retry_counter + 1, post_json) + return self.request( + url, params, first_request_time, retry_counter + 1, post_json + ) except exceptions.ApiError as e: - logger.log(f"Feature ID {post_json['id']} caused a {e.__class__.__name__}: {str(e)}", 2) + logger.log( + f"Feature ID {post_json['id']} caused a {e.__class__.__name__}: {str(e)}", + 2, + ) raise raise @@ -184,9 +188,11 @@ def request(self, # Write env variables if successful if self.ENV_VARS: for env_var in self.ENV_VARS: - configmanager.write_env_var(env_var, response.headers.get(self.ENV_VARS[env_var], 'None')) + configmanager.write_env_var( + env_var, response.headers.get(self.ENV_VARS[env_var], "None") + ) - return json.loads(content.decode('utf-8')) + return json.loads(content.decode("utf-8")) def _check_status(self): """ @@ -202,34 +208,28 @@ def _check_status(self): """ status_code = self.nam.http_call_result.status_code - message = self.nam.http_call_result.text if self.nam.http_call_result.text != '' else self.nam.http_call_result.reason + message = ( + self.nam.http_call_result.text + if self.nam.http_call_result.text != "" + else self.nam.http_call_result.reason + ) if not status_code: - raise Exception(f"{message}. Are your provider settings correct and the provider ready?") + raise Exception( + f"{message}. Are your provider settings correct and the provider ready?" + ) elif status_code == 403: - raise exceptions.InvalidKey( - str(status_code), - message - ) + raise exceptions.InvalidKey(str(status_code), message) elif status_code == 429: - raise exceptions.OverQueryLimit( - str(status_code), - message - ) + raise exceptions.OverQueryLimit(str(status_code), message) # Internal error message for Bad Request elif 400 <= status_code < 500: - raise exceptions.ApiError( - str(status_code), - message - ) + raise exceptions.ApiError(str(status_code), message) # Other HTTP errors have different formatting elif status_code != 200: - raise exceptions.GenericServerError( - str(status_code), - message - ) + raise exceptions.GenericServerError(str(status_code), message) def _generate_auth_url(self, path, params): """Returns the path and query string portion of the request URL, first diff --git a/ORStools/common/directions_core.py b/ORStools/common/directions_core.py index 038319a6..875a2437 100644 --- a/ORStools/common/directions_core.py +++ b/ORStools/common/directions_core.py @@ -28,12 +28,7 @@ """ from itertools import product -from qgis.core import (QgsPoint, - QgsPointXY, - QgsGeometry, - QgsFeature, - QgsFields, - QgsField) +from qgis.core import QgsPoint, QgsPointXY, QgsGeometry, QgsFeature, QgsFields, QgsField from typing import List from PyQt5.QtCore import QVariant @@ -55,18 +50,22 @@ def get_request_point_features(route_dict, row_by_row): :rtype: tuple """ - locations_list = list(product(route_dict['start']['geometries'], - route_dict['end']['geometries'])) - values_list = list(product(route_dict['start']['values'], - route_dict['end']['values'])) + locations_list = list( + product(route_dict["start"]["geometries"], route_dict["end"]["geometries"]) + ) + values_list = list( + product(route_dict["start"]["values"], route_dict["end"]["values"]) + ) # If row-by-row in two-layer mode, then only zip the locations - if row_by_row == 'Row-by-Row': - locations_list = list(zip(route_dict['start']['geometries'], - route_dict['end']['geometries'])) + if row_by_row == "Row-by-Row": + locations_list = list( + zip(route_dict["start"]["geometries"], route_dict["end"]["geometries"]) + ) - values_list = list(zip(route_dict['start']['values'], - route_dict['end']['values'])) + values_list = list( + zip(route_dict["start"]["values"], route_dict["end"]["values"]) + ) for properties in zip(locations_list, values_list): # Skip if first and last location are the same @@ -79,7 +78,13 @@ def get_request_point_features(route_dict, row_by_row): yield coordinates, values -def get_fields(from_type=QVariant.String, to_type=QVariant.String, from_name="FROM_ID", to_name="TO_ID", line=False): +def get_fields( + from_type=QVariant.String, + to_type=QVariant.String, + from_name="FROM_ID", + to_name="TO_ID", + line=False, +): """ Builds output fields for directions response layer. @@ -115,7 +120,9 @@ def get_fields(from_type=QVariant.String, to_type=QVariant.String, from_name="FR return fields -def get_output_feature_directions(response, profile, preference, options=None, from_value=None, to_value=None): +def get_output_feature_directions( + response, profile, preference, options=None, from_value=None, to_value=None +): """ Build output feature based on response attributes for directions endpoint. @@ -140,21 +147,24 @@ def get_output_feature_directions(response, profile, preference, options=None, f :returns: Output feature with attributes and geometry set. :rtype: QgsFeature """ - response_mini = response['features'][0] + response_mini = response["features"][0] feat = QgsFeature() - coordinates = response_mini['geometry']['coordinates'] - distance = response_mini['properties']['summary']['distance'] - duration = response_mini['properties']['summary']['duration'] + coordinates = response_mini["geometry"]["coordinates"] + distance = response_mini["properties"]["summary"]["distance"] + duration = response_mini["properties"]["summary"]["duration"] qgis_coords = [QgsPoint(x, y, z) for x, y, z in coordinates] feat.setGeometry(QgsGeometry.fromPolyline(qgis_coords)) - feat.setAttributes([f"{distance / 1000:.3f}", - f"{duration / 3600:.3f}", - profile, - preference, - str(options), - from_value, - to_value - ]) + feat.setAttributes( + [ + f"{distance / 1000:.3f}", + f"{duration / 3600:.3f}", + profile, + preference, + str(options), + from_value, + to_value, + ] + ) return feat @@ -176,25 +186,33 @@ def get_output_features_optimization(response, profile, from_value=None): :rtype: QgsFeature """ - response_mini = response['routes'][0] + response_mini = response["routes"][0] feat = QgsFeature() - polyline = response_mini['geometry'] - distance = response_mini['distance'] - duration = response_mini['cost'] + polyline = response_mini["geometry"] + distance = response_mini["distance"] + duration = response_mini["cost"] qgis_coords = [QgsPointXY(x, y) for x, y in convert.decode_polyline(polyline)] feat.setGeometry(QgsGeometry.fromPolylineXY(qgis_coords)) - feat.setAttributes([f"{distance / 1000:.3f}", - f"{duration / 3600:.3f}", - profile, - 'fastest', - 'optimized', - from_value - ]) + feat.setAttributes( + [ + f"{distance / 1000:.3f}", + f"{duration / 3600:.3f}", + profile, + "fastest", + "optimized", + from_value, + ] + ) return feat -def build_default_parameters(preference: str, point_list: List[QgsPointXY] = None, coordinates: list = None, options: dict = None) -> dict: +def build_default_parameters( + preference: str, + point_list: List[QgsPointXY] = None, + coordinates: list = None, + options: dict = None, +) -> dict: """ Build default parameters for directions endpoint. Either uses a list of QgsPointXY to create the coordinates passed in point_list or an existing coordinate list within the coordinates parameter. @@ -212,15 +230,19 @@ def build_default_parameters(preference: str, point_list: List[QgsPointXY] = Non :returns: parameters for directions endpoint :rtype: dict """ - coords = coordinates if coordinates else [[round(point.x(), 6), round(point.y(), 6)] for point in point_list] + coords = ( + coordinates + if coordinates + else [[round(point.x(), 6), round(point.y(), 6)] for point in point_list] + ) params = { - 'coordinates': coords, - 'preference': preference, - 'geometry': 'true', - 'instructions': 'false', - 'elevation': True, - 'id': None, - "options": options + "coordinates": coords, + "preference": preference, + "geometry": "true", + "instructions": "false", + "elevation": True, + "id": None, + "options": options, } return params diff --git a/ORStools/common/isochrones_core.py b/ORStools/common/isochrones_core.py index f60a82f8..fe7b0a92 100644 --- a/ORStools/common/isochrones_core.py +++ b/ORStools/common/isochrones_core.py @@ -27,15 +27,17 @@ ***************************************************************************/ """ -from qgis.core import (QgsPointXY, - QgsFeature, - QgsField, - QgsFields, - QgsGeometry, - QgsSymbol, - QgsSimpleFillSymbolLayer, - QgsRendererCategory, - QgsCategorizedSymbolRenderer) +from qgis.core import ( + QgsPointXY, + QgsFeature, + QgsField, + QgsFields, + QgsGeometry, + QgsSymbol, + QgsSimpleFillSymbolLayer, + QgsRendererCategory, + QgsCategorizedSymbolRenderer, +) from PyQt5.QtCore import QVariant from PyQt5.QtGui import QColor @@ -48,7 +50,6 @@ class Isochrones: """convenience class to build isochrones""" def __init__(self): - # Will all be set in self.set_parameters(), bcs Processing Algo has to initialize this class before it # knows about its own parameters self.profile = None @@ -58,7 +59,14 @@ def __init__(self): self.factor = None self.field_dimension_name = None - def set_parameters(self, profile, dimension, factor, id_field_type=QVariant.String, id_field_name='ID'): + def set_parameters( + self, + profile, + dimension, + factor, + id_field_type=QVariant.String, + id_field_name="ID", + ): """ Sets all parameters defined in __init__, because processing algorithm calls this class when it doesn't know its parameters yet. @@ -84,7 +92,9 @@ def set_parameters(self, profile, dimension, factor, id_field_type=QVariant.Stri self.id_field_name = id_field_name self.factor = factor - self.field_dimension_name = "AA_MINS" if self.dimension == 'time' else "AA_METERS" + self.field_dimension_name = ( + "AA_MINS" if self.dimension == "time" else "AA_METERS" + ) def get_fields(self): """ @@ -97,7 +107,9 @@ def get_fields(self): fields.append(QgsField(self.id_field_name, self.id_field_type)) # ID field fields.append(QgsField("CENTER_LON", QVariant.String)) fields.append(QgsField("CENTER_LAT", QVariant.String)) - fields.append(QgsField(self.field_dimension_name, QVariant.Int)) # Dimension field + fields.append( + QgsField(self.field_dimension_name, QVariant.Int) + ) # Dimension field fields.append(QgsField("AA_MODE", QVariant.String)) fields.append(QgsField("TOTAL_POP", QVariant.String)) @@ -119,22 +131,26 @@ def get_features(self, response, id_field_value): # Sort features based on the isochrone value, so that longest isochrone # is added first. This will plot the isochrones on top of each other. - for isochrone in sorted(response['features'], key=lambda x: x['properties']['value'], reverse=True): + for isochrone in sorted( + response["features"], key=lambda x: x["properties"]["value"], reverse=True + ): feat = QgsFeature() - coordinates = isochrone['geometry']['coordinates'] - iso_value = isochrone['properties']['value'] - center = isochrone['properties']['center'] - total_pop = isochrone['properties'].get('total_pop') + coordinates = isochrone["geometry"]["coordinates"] + iso_value = isochrone["properties"]["value"] + center = isochrone["properties"]["center"] + total_pop = isochrone["properties"].get("total_pop") qgis_coords = [QgsPointXY(x, y) for x, y in coordinates[0]] feat.setGeometry(QgsGeometry.fromPolygonXY([qgis_coords])) - feat.setAttributes([ - id_field_value, - center[0], - center[1], - int(iso_value / self.factor), - self.profile, - total_pop - ]) + feat.setAttributes( + [ + id_field_value, + center[0], + center[1], + int(iso_value / self.factor), + self.profile, + total_pop, + ] + ) yield feat @@ -159,24 +175,26 @@ def stylePoly(self, layer): :type layer: QgsMapLayer """ - if self.dimension == 'time': - legend_suffix = ' min' + if self.dimension == "time": + legend_suffix = " min" else: - legend_suffix = ' m' + legend_suffix = " m" field = layer.fields().indexOf(self.field_dimension_name) unique_values = sorted(layer.uniqueValues(field)) - colors = {0: QColor('#2b83ba'), - 1: QColor('#64abb0'), - 2: QColor('#9dd3a7'), - 3: QColor('#c7e9ad'), - 4: QColor('#edf8b9'), - 5: QColor('#ffedaa'), - 6: QColor('#fec980'), - 7: QColor('#f99e59'), - 8: QColor('#e85b3a'), - 9: QColor('#d7191c')} + colors = { + 0: QColor("#2b83ba"), + 1: QColor("#64abb0"), + 2: QColor("#9dd3a7"), + 3: QColor("#c7e9ad"), + 4: QColor("#edf8b9"), + 5: QColor("#ffedaa"), + 6: QColor("#fec980"), + 7: QColor("#f99e59"), + 8: QColor("#e85b3a"), + 9: QColor("#d7191c"), + } categories = [] @@ -185,15 +203,18 @@ def stylePoly(self, layer): symbol = QgsSymbol.defaultSymbol(layer.geometryType()) # configure a symbol layer - symbol_layer = QgsSimpleFillSymbolLayer(color=colors[cid], - strokeColor=QColor('#000000')) + symbol_layer = QgsSimpleFillSymbolLayer( + color=colors[cid], strokeColor=QColor("#000000") + ) # replace default symbol layer with the configured one if symbol_layer is not None: symbol.changeSymbolLayer(0, symbol_layer) # create renderer object - category = QgsRendererCategory(unique_value, symbol, str(unique_value) + legend_suffix) + category = QgsRendererCategory( + unique_value, symbol, str(unique_value) + legend_suffix + ) # entry for the list of category items categories.append(category) diff --git a/ORStools/common/networkaccessmanager.py b/ORStools/common/networkaccessmanager.py index cb9197da..971a0d32 100644 --- a/ORStools/common/networkaccessmanager.py +++ b/ORStools/common/networkaccessmanager.py @@ -21,45 +21,45 @@ from builtins import object import json -__author__ = 'Alessandro Pasotti' -__date__ = 'August 2016' +__author__ = "Alessandro Pasotti" +__date__ = "August 2016" import re import io import urllib.parse -from qgis.PyQt.QtCore import (QUrl, - QEventLoop) +from qgis.PyQt.QtCore import QUrl, QEventLoop -from qgis.PyQt.QtNetwork import (QNetworkRequest, - QNetworkReply) +from qgis.PyQt.QtNetwork import QNetworkRequest, QNetworkReply -from qgis.core import ( - QgsApplication, - QgsNetworkAccessManager, - QgsMessageLog -) +from qgis.core import QgsApplication, QgsNetworkAccessManager, QgsMessageLog # FIXME: ignored DEFAULT_MAX_REDIRECTS = 4 + class RequestsException(Exception): pass + class RequestsExceptionTimeout(RequestsException): pass + class RequestsExceptionConnectionError(RequestsException): pass + class RequestsExceptionUserAbort(RequestsException): pass + class Map(dict): """ Example: m = Map({'first_name': 'Eduardo'}, last_name='Pool', age=24, sports=['Soccer']) """ + def __init__(self, *args, **kwargs): super(Map, self).__init__(*args, **kwargs) for arg in args: @@ -92,6 +92,7 @@ def __delitem__(self, key): class Response(Map): pass + class NetworkAccessManager(object): """ This class mimics httplib2 by using QgsNetworkAccessManager for all @@ -145,7 +146,14 @@ class NetworkAccessManager(object): 'exception' - the exception returned during execution """ - def __init__(self, authid=None, disable_ssl_certificate_validation=False, exception_class=None, debug=True, timeout=60): + def __init__( + self, + authid=None, + disable_ssl_certificate_validation=False, + exception_class=None, + debug=True, + timeout=60, + ): self.disable_ssl_certificate_validation = disable_ssl_certificate_validation self.authid = authid self.reply = None @@ -153,17 +161,19 @@ def __init__(self, authid=None, disable_ssl_certificate_validation=False, except self.exception_class = exception_class self.on_abort = False self.blocking_mode = False - self.http_call_result = Response({ - 'status': 0, - 'status_code': 0, - 'status_message': '', - 'content' : '', - 'ok': False, - 'headers': {}, - 'reason': '', - 'exception': None, - }) - self.timeout=timeout + self.http_call_result = Response( + { + "status": 0, + "status_code": 0, + "status_message": "", + "content": "", + "ok": False, + "headers": {}, + "reason": "", + "exception": None, + } + ) + self.timeout = timeout def msg_log(self, msg): if self.debug: @@ -180,7 +190,7 @@ def request(self, url, method="GET", body=None, headers=None, blocking=True): Make a network request by calling QgsNetworkAccessManager. redirections argument is ignored and is here only for httplib2 compatibility. """ - self.msg_log(f'http_call request: {url}') + self.msg_log(f"http_call request: {url}") self.blocking_mode = blocking req = QNetworkRequest() @@ -195,7 +205,7 @@ def request(self, url, method="GET", body=None, headers=None, blocking=True): # encoding processing". # See: https://bugs.webkit.org/show_bug.cgi?id=63696#c1 try: - del headers['Accept-Encoding'] + del headers["Accept-Encoding"] except KeyError: pass for k, v in list(headers.items()): @@ -207,32 +217,34 @@ def request(self, url, method="GET", body=None, headers=None, blocking=True): self.auth_manager().updateNetworkRequest(req, self.authid) if self.reply is not None and self.reply.isRunning(): self.reply.close() - if method.lower() == 'delete': - func = getattr(QgsNetworkAccessManager.instance(), 'deleteResource') + if method.lower() == "delete": + func = getattr(QgsNetworkAccessManager.instance(), "deleteResource") else: func = getattr(QgsNetworkAccessManager.instance(), method.lower()) # Calling the server ... # Let's log the whole call for debugging purposes: - self.msg_log("Sending %s request to %s" % (method.upper(), req.url().toString())) + self.msg_log( + "Sending %s request to %s" % (method.upper(), req.url().toString()) + ) self.on_abort = False headers = {str(h): str(req.rawHeader(h)) for h in req.rawHeaderList()} for k, v in list(headers.items()): self.msg_log("%s: %s" % (k, v)) - if method.lower() in ['post', 'put']: + if method.lower() in ["post", "put"]: if isinstance(body, io.IOBase): body = body.read() if isinstance(body, str): body = body.encode() if isinstance(body, dict): - body = str(json.dumps(body)).encode(encoding='utf-8') + body = str(json.dumps(body)).encode(encoding="utf-8") self.reply = func(req, body) else: self.reply = func(req) if self.authid: self.msg_log(f"Update reply w/ authid: {self.authid}") self.auth_manager().updateNetworkReply(self.reply, self.authid) - - QgsNetworkAccessManager.instance().setTimeout(self.timeout*1000) + + QgsNetworkAccessManager.instance().setTimeout(self.timeout * 1000) # necessary to trap local timeout managed by QgsNetworkAccessManager # calling QgsNetworkAccessManager::abortRequest @@ -271,7 +283,7 @@ def request(self, url, method="GET", body=None, headers=None, blocking=True): def downloadProgress(self, bytesReceived, bytesTotal): """Keep track of the download progress""" - #self.msg_log("downloadProgress %s of %s ..." % (bytesReceived, bytesTotal)) + # self.msg_log("downloadProgress %s of %s ..." % (bytesReceived, bytesTotal)) pass # noinspection PyUnusedLocal @@ -284,19 +296,25 @@ def requestTimedOut(self, reply): def replyFinished(self): err = self.reply.error() httpStatus = self.reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) - httpStatusMessage = self.reply.attribute(QNetworkRequest.HttpReasonPhraseAttribute) + httpStatusMessage = self.reply.attribute( + QNetworkRequest.HttpReasonPhraseAttribute + ) self.http_call_result.status_code = httpStatus self.http_call_result.status = httpStatus self.http_call_result.status_message = httpStatusMessage for k, v in self.reply.rawHeaderPairs(): - self.http_call_result.headers[str(k.data(), encoding='utf-8')] = str(v.data(), encoding='utf-8') - self.http_call_result.headers[str(k.data(), encoding='utf-8').lower()] = str(v.data(), encoding='utf-8') + self.http_call_result.headers[str(k.data(), encoding="utf-8")] = str( + v.data(), encoding="utf-8" + ) + self.http_call_result.headers[ + str(k.data(), encoding="utf-8").lower() + ] = str(v.data(), encoding="utf-8") if err != QNetworkReply.NoError: # handle error # check if errorString is empty, if so, then set err string as # reply dump - if re.match('(.)*server replied: $', self.reply.errorString()): + if re.match("(.)*server replied: $", self.reply.errorString()): errString = self.reply.errorString() + self.http_call_result.content else: errString = self.reply.errorString() @@ -308,7 +326,9 @@ def replyFinished(self): msg = f"Network error: {errString}" self.http_call_result.reason = msg - self.http_call_result.text = str(self.reply.readAll().data(), encoding='utf-8') + self.http_call_result.text = str( + self.reply.readAll().data(), encoding="utf-8" + ) self.http_call_result.ok = False self.msg_log(msg) # set return exception @@ -334,7 +354,9 @@ def replyFinished(self): else: # Handle redirections - redirectionUrl = self.reply.attribute(QNetworkRequest.RedirectionTargetAttribute) + redirectionUrl = self.reply.attribute( + QNetworkRequest.RedirectionTargetAttribute + ) if redirectionUrl is not None and redirectionUrl != self.reply.url(): if redirectionUrl.isRelative(): redirectionUrl = self.reply.url().resolved(redirectionUrl) @@ -354,14 +376,18 @@ def replyFinished(self): ba = self.reply.readAll() self.http_call_result.content = bytes(ba) - self.http_call_result.text = str(ba.data(), encoding='utf-8') + self.http_call_result.text = str(ba.data(), encoding="utf-8") self.http_call_result.ok = True # Let's log the whole response for debugging purposes: - self.msg_log("Got response %s %s from %s" % \ - (self.http_call_result.status_code, - self.http_call_result.status_message, - self.reply.url().toString())) + self.msg_log( + "Got response %s %s from %s" + % ( + self.http_call_result.status_code, + self.http_call_result.status_message, + self.reply.url().toString(), + ) + ) for k, v in list(self.http_call_result.headers.items()): self.msg_log("%s: %s" % (k, v)) if len(self.http_call_result.content) < 1024: diff --git a/ORStools/gui/ORStoolsDialog.py b/ORStools/gui/ORStoolsDialog.py index 2ed30e14..0c5e5501 100644 --- a/ORStools/gui/ORStoolsDialog.py +++ b/ORStools/gui/ORStoolsDialog.py @@ -31,26 +31,40 @@ import os import processing import webbrowser -from qgis.core import (QgsProject, - QgsVectorLayer, - QgsTextAnnotation, - QgsMapLayerProxyModel) +from qgis.core import ( + QgsProject, + QgsVectorLayer, + QgsTextAnnotation, + QgsMapLayerProxyModel, +) from qgis.gui import QgsMapCanvasAnnotationItem from PyQt5.QtCore import QSizeF, QPointF, QCoreApplication from PyQt5.QtGui import QIcon, QTextDocument -from PyQt5.QtWidgets import (QAction, - QDialog, - QApplication, - QMenu, - QMessageBox, - QDialogButtonBox) - -from ORStools import RESOURCE_PREFIX, PLUGIN_NAME, DEFAULT_COLOR, __version__, __email__, __web__, __help__ -from ORStools.common import (client, - directions_core, - PROFILES, - PREFERENCES, ) +from PyQt5.QtWidgets import ( + QAction, + QDialog, + QApplication, + QMenu, + QMessageBox, + QDialogButtonBox, +) + +from ORStools import ( + RESOURCE_PREFIX, + PLUGIN_NAME, + DEFAULT_COLOR, + __version__, + __email__, + __web__, + __help__, +) +from ORStools.common import ( + client, + directions_core, + PROFILES, + PREFERENCES, +) from ORStools.gui import directions_gui from ORStools.utils import exceptions, maptools, logger, configmanager, transform from .ORStoolsDialogConfig import ORStoolsDialogConfigMain @@ -75,24 +89,27 @@ def on_help_click(): def on_about_click(parent): """Slot for click event of About button/menu entry.""" - info = QCoreApplication.translate('@default', 'ORS Tools provides access to openrouteservice routing functionalities.' \ - '

' \ - '
' \ - '' \ - '

' \ - '
' \ - 'Author: HeiGIT gGmbH
' \ - 'Email: {1}
' \ - 'Web: {2}
' \ - 'Repo: ' \ - 'github.com/GIScience/orstools-qgis-plugin
' \ - 'Version: {3}').format(DEFAULT_COLOR, __email__, __web__, __version__) + info = QCoreApplication.translate( + "@default", + 'ORS Tools provides access to openrouteservice routing functionalities.' + "

" + "
" + '' + "

" + "
" + "Author: HeiGIT gGmbH
" + 'Email: {1}
' + 'Web: {2}
' + 'Repo: ' + "github.com/GIScience/orstools-qgis-plugin
" + "Version: {3}", + ).format(DEFAULT_COLOR, __email__, __web__, __version__) QMessageBox.information( parent, - QCoreApplication.translate('@default', 'About {}').format(PLUGIN_NAME), - info + QCoreApplication.translate("@default", "About {}").format(PLUGIN_NAME), + info, ) @@ -130,33 +147,28 @@ def create_icon(f): """ return QIcon(RESOURCE_PREFIX + f) - icon_plugin = create_icon('icon_orstools.png') + icon_plugin = create_icon("icon_orstools.png") self.actions = [ QAction( icon_plugin, PLUGIN_NAME, # tr text - self.iface.mainWindow() # parent + self.iface.mainWindow(), # parent ), # Config dialog QAction( - create_icon('icon_settings.png'), - self.tr('Provider Settings'), - self.iface.mainWindow() + create_icon("icon_settings.png"), + self.tr("Provider Settings"), + self.iface.mainWindow(), ), # About dialog QAction( - create_icon('icon_about.png'), - self.tr('About'), - self.iface.mainWindow() + create_icon("icon_about.png"), self.tr("About"), self.iface.mainWindow() ), # Help page QAction( - create_icon('icon_help.png'), - self.tr('Help'), - self.iface.mainWindow() - ) - + create_icon("icon_help.png"), self.tr("Help"), self.iface.mainWindow() + ), ] # Create menu @@ -172,8 +184,12 @@ def create_icon(f): # Connect slots to events self.actions[0].triggered.connect(self._init_gui_control) - self.actions[1].triggered.connect(lambda: on_config_click(parent=self.iface.mainWindow())) - self.actions[2].triggered.connect(lambda: on_about_click(parent=self.iface.mainWindow())) + self.actions[1].triggered.connect( + lambda: on_config_click(parent=self.iface.mainWindow()) + ) + self.actions[2].triggered.connect( + lambda: on_about_click(parent=self.iface.mainWindow()) + ) self.actions[3].triggered.connect(on_help_click) # Add keyboard shortcut @@ -188,7 +204,7 @@ def unload(self): # Remove action for keyboard shortcut self.iface.unregisterMainWindowAction(self.actions[0]) - + del self.dlg # @staticmethod @@ -213,17 +229,21 @@ def _init_gui_control(self): # 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 = 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) + 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'] + providers = configmanager.read_config()["providers"] self.dlg.provider_combo.clear() for provider in providers: - self.dlg.provider_combo.addItem(provider['name'], provider) + self.dlg.provider_combo.addItem(provider["name"], provider) self.dlg.show() @@ -245,7 +265,7 @@ def run_gui_control(self): self.dlg.annotations = [] provider_id = self.dlg.provider_combo.currentIndex() - provider = configmanager.read_config()['providers'][provider_id] + provider = configmanager.read_config()["providers"][provider_id] # if there are no coordinates, throw an error message if not self.dlg.routing_fromline_list.count(): @@ -256,12 +276,14 @@ def run_gui_control(self): Did you forget to set routing waypoints?

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'): + if not provider["key"] and provider["base_url"].startswith( + "https://api.openrouteservice.org" + ): QMessageBox.critical( self.dlg, "Missing API key", @@ -270,54 +292,58 @@ def run_gui_control(self): If you don't have an API key, please visit https://openrouteservice.org/sign-up to get one.

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.""" + settings symbol in the main ORS Tools GUI, next to the provider dropdown.""", ) return clnt = client.Client(provider) - clnt_msg = '' + clnt_msg = "" directions = directions_gui.Directions(self.dlg) params = None try: params = directions.get_parameters() if self.dlg.optimization_group.isChecked(): - if len(params['jobs']) <= 1: # Start/end locations don't count as job + 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']) + 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() + params["coordinates"] = directions.get_request_line_feature() profile = self.dlg.routing_travel_combo.currentText() # abort on empty avoid polygons layer - if 'options' in params and 'avoid_polygons' in params['options']\ - and params['options']['avoid_polygons'] == {}: + if ( + "options" in params + and "avoid_polygons" in params["options"] + and params["options"]["avoid_polygons"] == {} + ): QMessageBox.warning( self.dlg, "Empty layer", """ The specified avoid polygon(s) layer does not contain any features. Please add polygons to the layer or uncheck avoid polygons. - """ + """, ) msg = "The request has been aborted!" logger.log(msg, 0) self.dlg.debug_text.setText(msg) return - response = clnt.request('/v2/directions/' + profile + '/geojson', {}, post_json=params) + response = clnt.request( + "/v2/directions/" + profile + "/geojson", {}, post_json=params + ) feat = directions_core.get_output_feature_directions( - response, - profile, - params['preference'], - directions.options + response, profile, params["preference"], directions.options ) layer_out.dataProvider().addFeature(feat) @@ -334,10 +360,11 @@ def run_gui_control(self): self.dlg.debug_text.setText(msg) return - except (exceptions.ApiError, - exceptions.InvalidKey, - exceptions.GenericServerError) as e: - + except ( + exceptions.ApiError, + exceptions.InvalidKey, + exceptions.GenericServerError, + ) as e: logger.log(f"{e.__class__.__name__}: {str(e)}", 2) clnt_msg += f"{e.__class__.__name__}: ({str(e)})
" raise @@ -388,15 +415,17 @@ def __init__(self, iface, parent=None): self.routing_preference_combo.addItems(PREFERENCES) # Change OK and Cancel button names - self.global_buttons.button(QDialogButtonBox.Ok).setText(self.tr('Apply')) - self.global_buttons.button(QDialogButtonBox.Cancel).setText(self.tr('Close')) + self.global_buttons.button(QDialogButtonBox.Ok).setText(self.tr("Apply")) + self.global_buttons.button(QDialogButtonBox.Cancel).setText(self.tr("Close")) # Set up signals/slots # Config/Help dialogs self.provider_config.clicked.connect(lambda: on_config_click(self)) self.help_button.clicked.connect(on_help_click) - self.about_button.clicked.connect(lambda: on_about_click(parent=self._iface.mainWindow())) + self.about_button.clicked.connect( + lambda: on_about_click(parent=self._iface.mainWindow()) + ) self.provider_refresh.clicked.connect(self._on_prov_refresh_click) # Routing tab @@ -404,25 +433,42 @@ def __init__(self, iface, parent=None): self.routing_fromline_clear.clicked.connect(self._on_clear_listwidget_click) # Batch - self.batch_routing_points.clicked.connect(lambda: processing.execAlgorithmDialog( - f'{PLUGIN_NAME}:directions_from_points_2_layers')) - self.batch_routing_point.clicked.connect(lambda: processing.execAlgorithmDialog( - f'{PLUGIN_NAME}:directions_from_points_1_layer')) - self.batch_routing_line.clicked.connect(lambda: processing.execAlgorithmDialog( - f'{PLUGIN_NAME}:directions_from_polylines_layer')) - self.batch_iso_point.clicked.connect(lambda: processing.execAlgorithmDialog( - f'{PLUGIN_NAME}:isochrones_from_point')) - self.batch_iso_layer.clicked.connect(lambda: processing.execAlgorithmDialog( - f'{PLUGIN_NAME}:isochrones_from_layer')) - self.batch_matrix.clicked.connect(lambda: processing.execAlgorithmDialog(f'{PLUGIN_NAME}:matrix_from_layers')) + self.batch_routing_points.clicked.connect( + lambda: processing.execAlgorithmDialog( + f"{PLUGIN_NAME}:directions_from_points_2_layers" + ) + ) + self.batch_routing_point.clicked.connect( + lambda: processing.execAlgorithmDialog( + f"{PLUGIN_NAME}:directions_from_points_1_layer" + ) + ) + self.batch_routing_line.clicked.connect( + lambda: processing.execAlgorithmDialog( + f"{PLUGIN_NAME}:directions_from_polylines_layer" + ) + ) + self.batch_iso_point.clicked.connect( + lambda: processing.execAlgorithmDialog( + f"{PLUGIN_NAME}:isochrones_from_point" + ) + ) + self.batch_iso_layer.clicked.connect( + lambda: processing.execAlgorithmDialog( + f"{PLUGIN_NAME}:isochrones_from_layer" + ) + ) + self.batch_matrix.clicked.connect( + lambda: processing.execAlgorithmDialog(f"{PLUGIN_NAME}:matrix_from_layers") + ) def _on_prov_refresh_click(self): """Populates provider dropdown with fresh list from config.yml""" - providers = configmanager.read_config()['providers'] + providers = configmanager.read_config()["providers"] self.provider_combo.clear() for provider in providers: - self.provider_combo.addItem(provider['name'], provider) + self.provider_combo.addItem(provider["name"], provider) def _on_clear_listwidget_click(self): """Clears the contents of the QgsListWidget and the annotations.""" @@ -433,7 +479,9 @@ def _on_clear_listwidget_click(self): row = self.routing_fromline_list.row(item) self.routing_fromline_list.takeItem(row) if self.annotations: - self.project.annotationManager().removeAnnotation(self.annotations.pop(row)) + self.project.annotationManager().removeAnnotation( + self.annotations.pop(row) + ) else: # else clear all items and annotations self.routing_fromline_list.clear() @@ -454,7 +502,9 @@ def _linetool_annotate_point(self, point, idx): annotation.setMapPosition(point) annotation.setMapPositionCrs(map_crs) - return QgsMapCanvasAnnotationItem(annotation, self._iface.mapCanvas()).annotation() + return QgsMapCanvasAnnotationItem( + annotation, self._iface.mapCanvas() + ).annotation() def _clear_annotations(self): """Clears annotations""" @@ -472,7 +522,9 @@ def _on_linetool_init(self): self.line_tool = maptools.LineTool(self._iface.mapCanvas()) self._iface.mapCanvas().setMapTool(self.line_tool) - self.line_tool.pointDrawn.connect(lambda point, idx: self._on_linetool_map_click(point, idx)) + self.line_tool.pointDrawn.connect( + lambda point, idx: self._on_linetool_map_click(point, idx) + ) self.line_tool.doubleClicked.connect(self._on_linetool_map_doubleclick) def _on_linetool_map_click(self, point, idx): @@ -481,7 +533,9 @@ def _on_linetool_map_click(self, point, idx): transformer = transform.transformToWGS(map_crs) point_wgs = transformer.transform(point) - self.routing_fromline_list.addItem(f"Point {idx}: {point_wgs.x():.6f}, {point_wgs.y():.6f}") + self.routing_fromline_list.addItem( + f"Point {idx}: {point_wgs.x():.6f}, {point_wgs.y():.6f}" + ) annotation = self._linetool_annotate_point(point, idx) self.annotations.append(annotation) diff --git a/ORStools/gui/ORStoolsDialogConfig.py b/ORStools/gui/ORStoolsDialogConfig.py index 46c8b023..c1b49c59 100644 --- a/ORStools/gui/ORStoolsDialogConfig.py +++ b/ORStools/gui/ORStoolsDialogConfig.py @@ -64,14 +64,20 @@ def accept(self): collapsible_boxes = self.providers.findChildren(QgsCollapsibleGroupBox) for idx, box in enumerate(collapsible_boxes): - current_provider = self.temp_config['providers'][idx] - current_provider['key'] = box.findChild(QtWidgets.QLineEdit, box.title() + "_key_text").text() - current_provider['base_url'] = box.findChild(QtWidgets.QLineEdit, box.title() + "_base_url_text").text() - timeout_input = box.findChild(QtWidgets.QLineEdit, box.title() + "_timeout_text") + current_provider = self.temp_config["providers"][idx] + current_provider["key"] = box.findChild( + QtWidgets.QLineEdit, box.title() + "_key_text" + ).text() + current_provider["base_url"] = box.findChild( + QtWidgets.QLineEdit, box.title() + "_base_url_text" + ).text() + timeout_input = box.findChild( + QtWidgets.QLineEdit, box.title() + "_timeout_text" + ) # https://doc.qt.io/qt-5/qvalidator.html#State-enum if timeout_input.validator().State() != 2: self._adjust_timeout_input(timeout_input) - current_provider['timeout'] = int(timeout_input.text()) + current_provider["timeout"] = int(timeout_input.text()) configmanager.write_config(self.temp_config) self.close() @@ -87,7 +93,7 @@ def _adjust_timeout_input(input_line_edit: QLineEdit): val = input_line_edit.validator() text = input_line_edit.text() if not text: - input_line_edit.setText('60') + input_line_edit.setText("60") elif int(text) < val.bottom(): input_line_edit.setText(str(val.bottom())) elif int(text) > val.top(): @@ -96,12 +102,14 @@ def _adjust_timeout_input(input_line_edit: QLineEdit): def _build_ui(self): """Builds the UI on dialog startup.""" - for provider_entry in self.temp_config['providers']: - self._add_box(provider_entry['name'], - provider_entry['base_url'], - provider_entry['key'], - provider_entry['timeout'], - new=False) + for provider_entry in self.temp_config["providers"]: + self._add_box( + provider_entry["name"], + provider_entry["base_url"], + provider_entry["key"], + provider_entry["timeout"], + new=False, + ) self.gridLayout.addWidget(self.providers, 0, 0, 1, 3) @@ -115,19 +123,25 @@ def _add_provider(self): self._collapse_boxes() # Show quick user input dialog - provider_name, ok = QInputDialog.getText(self, self.tr("New ORS provider"), self.tr("Enter a name for the provider")) + provider_name, ok = QInputDialog.getText( + self, self.tr("New ORS provider"), self.tr("Enter a name for the provider") + ) if ok: - self._add_box(provider_name, 'http://localhost:8082/ors', '', 60, new=True) + self._add_box(provider_name, "http://localhost:8082/ors", "", 60, new=True) def _remove_provider(self): """Remove list of providers from list.""" - providers = [provider['name'] for provider in self.temp_config['providers']] + providers = [provider["name"] for provider in self.temp_config["providers"]] - provider, ok = QInputDialog.getItem(self, - self.tr("Remove ORS provider"), - self.tr("Choose provider to remove"), - providers, 0, False) + provider, ok = QInputDialog.getItem( + self, + self.tr("Remove ORS provider"), + self.tr("Choose provider to remove"), + providers, + 0, + False, + ) if ok: box_remove = self.providers.findChild(QgsCollapsibleGroupBox, provider) self.gridLayout.removeWidget(box_remove) @@ -135,7 +149,7 @@ def _remove_provider(self): # delete from in-memory self.temp_config provider_id = providers.index(provider) - del self.temp_config['providers'][provider_id] + del self.temp_config["providers"][provider_id] def _collapse_boxes(self): """Collapse all QgsCollapsibleGroupBoxes.""" @@ -143,12 +157,7 @@ def _collapse_boxes(self): for box in collapsible_boxes: box.setCollapsed(True) - def _add_box(self, - name, - url, - key, - timeout, - new=False): + def _add_box(self, name, url, key, timeout, new=False): """ Adds a provider box to the QWidget layout and self.temp_config. @@ -165,23 +174,18 @@ def _add_box(self, :type new: boolean """ if new: - self.temp_config['providers'].append( - dict( - name=name, - base_url=url, - key=key, - timeout=timeout - ) + self.temp_config["providers"].append( + dict(name=name, base_url=url, key=key, timeout=timeout) ) provider = QgsCollapsibleGroupBox(self.providers) provider.setObjectName(name) provider.setTitle(name) gridLayout_3 = QtWidgets.QGridLayout(provider) - gridLayout_3.setObjectName(name + '_grid') + gridLayout_3.setObjectName(name + "_grid") key_label = QtWidgets.QLabel(provider) - key_label.setObjectName(name + '_key_label') - key_label.setText(self.tr('API Key')) + key_label.setObjectName(name + "_key_label") + key_label.setText(self.tr("API Key")) gridLayout_3.addWidget(key_label, 0, 0, 1, 1) key_text = QtWidgets.QLineEdit(provider) key_text.setObjectName(name + "_key_text") @@ -207,4 +211,6 @@ def _add_box(self, gridLayout_3.addWidget(timeout_text, 5, 0, 1, 4) self.verticalLayout.addWidget(provider) - provider.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) + provider.setSizePolicy( + QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed + ) diff --git a/ORStools/gui/directions_gui.py b/ORStools/gui/directions_gui.py index 28525995..d3bb7c97 100644 --- a/ORStools/gui/directions_gui.py +++ b/ORStools/gui/directions_gui.py @@ -106,7 +106,7 @@ def get_request_line_feature(self): item = layers_list.item(idx).text() param, coords = item.split(":") - coordinates.append([float(coord) for coord in coords.split(', ')]) + coordinates.append([float(coord) for coord in coords.split(", ")]) return [[round(x, 6), round(y, 6)] for x, y in coordinates] @@ -125,35 +125,35 @@ def get_parameters(self): route_pref = self.dlg.routing_preference_combo.currentText() params = { - 'preference': route_pref, - 'geometry': 'true', - 'instructions': 'false', - 'elevation': True, - 'id': 1, + "preference": route_pref, + "geometry": "true", + "instructions": "false", + "elevation": True, + "id": 1, } # Get Advanced parameters if self.dlg.routing_avoid_tags_group.isChecked(): avoid_boxes = self.dlg.routing_avoid_tags_group.findChildren(QCheckBox) if any(box.isChecked() for box in avoid_boxes): - self.options['avoid_features'] = _get_avoid_options(avoid_boxes) + self.options["avoid_features"] = _get_avoid_options(avoid_boxes) if self.dlg.routing_avoid_countries_group.isChecked(): countries_text = self.dlg.countries_text.value() if countries_text: - countries = countries_text.split(',') + countries = countries_text.split(",") if all(map(lambda x: x.isdigit(), countries)): countries = [int(x) for x in countries] - self.options['avoid_countries'] = countries + self.options["avoid_countries"] = countries if self.dlg.avoidpolygon_group.isChecked(): layer = self.dlg.avoidpolygon_dropdown.currentLayer() if layer: polygons = _get_avoid_polygons(layer) - self.options['avoid_polygons'] = polygons + self.options["avoid_polygons"] = polygons if self.options: - params['options'] = self.options + params["options"] = self.options return params @@ -162,34 +162,30 @@ def _get_optimize_parameters(self): coordinates = self.get_request_line_feature() params = { - 'jobs': list(), - 'vehicles': [{ - "id": 0, - "profile": self.dlg.routing_travel_combo.currentText() - }], - 'options': {'g': True} + "jobs": list(), + "vehicles": [ + {"id": 0, "profile": self.dlg.routing_travel_combo.currentText()} + ], + "options": {"g": True}, } if self.dlg.fix_end.isChecked(): end = coordinates.pop(-1) - params['vehicles'][0]['end'] = end + params["vehicles"][0]["end"] = end elif self.dlg.fix_start.isChecked(): start = coordinates.pop(0) - params['vehicles'][0]['start'] = start + params["vehicles"][0]["start"] = start elif self.dlg.fix_both.isChecked(): start = coordinates.pop(0) end = coordinates.pop(-1) - params['vehicles'][0]['start'] = start - params['vehicles'][0]['end'] = end + params["vehicles"][0]["start"] = start + params["vehicles"][0]["end"] = end elif self.dlg.round_trip.isChecked(): start = coordinates.pop(0) - params['vehicles'][0]['start'] = start - params['vehicles'][0]['end'] = start + params["vehicles"][0]["start"] = start + params["vehicles"][0]["end"] = start for coord in coordinates: - params['jobs'].append({ - "location": coord, - "id": coordinates.index(coord) - }) + params["jobs"].append({"location": coord, "id": coordinates.index(coord)}) return params diff --git a/ORStools/gui/resources_rc.py b/ORStools/gui/resources_rc.py index 810d3b19..c638c273 100644 --- a/ORStools/gui/resources_rc.py +++ b/ORStools/gui/resources_rc.py @@ -6526,18 +6526,25 @@ \x00\x00\x01\x69\x54\xec\x86\x12\ " -qt_version = QtCore.qVersion().split('.') -if qt_version < ['5', '8', '0']: +qt_version = QtCore.qVersion().split(".") +if qt_version < ["5", "8", "0"]: rcc_version = 1 qt_resource_struct = qt_resource_struct_v1 else: rcc_version = 2 qt_resource_struct = qt_resource_struct_v2 + def qInitResources(): - QtCore.qRegisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) + QtCore.qRegisterResourceData( + rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data + ) + def qCleanupResources(): - QtCore.qUnregisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) + QtCore.qUnregisterResourceData( + rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data + ) + qInitResources() diff --git a/ORStools/proc/base_processing_algorithm.py b/ORStools/proc/base_processing_algorithm.py index 4dd21120..e2c128ad 100644 --- a/ORStools/proc/base_processing_algorithm.py +++ b/ORStools/proc/base_processing_algorithm.py @@ -27,23 +27,30 @@ ***************************************************************************/ """ from PyQt5.QtCore import QCoreApplication, QSettings -from qgis.core import (QgsProcessing, - QgsProcessingAlgorithm, - QgsProcessingContext, - QgsProcessingParameterDefinition, - QgsProcessingParameterEnum, - QgsProcessingParameterString, - QgsProcessingParameterFeatureSink, - QgsProcessingParameterFeatureSource, - QgsProcessingFeedback - ) +from qgis.core import ( + QgsProcessing, + QgsProcessingAlgorithm, + QgsProcessingContext, + QgsProcessingParameterDefinition, + QgsProcessingParameterEnum, + QgsProcessingParameterString, + QgsProcessingParameterFeatureSink, + QgsProcessingParameterFeatureSource, + QgsProcessingFeedback, +) from typing import Any from PyQt5.QtGui import QIcon from ORStools import RESOURCE_PREFIX, __help__ from ORStools.utils import configmanager -from ..common import client, PROFILES, AVOID_BORDERS, AVOID_FEATURES, ADVANCED_PARAMETERS +from ..common import ( + client, + PROFILES, + AVOID_BORDERS, + AVOID_FEATURES, + ADVANCED_PARAMETERS, +) from ..utils.processing import read_help_file from ..gui.directions_gui import _get_avoid_polygons @@ -57,15 +64,15 @@ def __init__(self): Default attributes used in all child classes """ super().__init__() - self.ALGO_NAME = '' - self.GROUP = '' + self.ALGO_NAME = "" + self.GROUP = "" self.IN_PROVIDER = "INPUT_PROVIDER" self.IN_PROFILE = "INPUT_PROFILE" self.IN_AVOID_FEATS = "INPUT_AVOID_FEATURES" self.IN_AVOID_BORDERS = "INPUT_AVOID_BORDERS" self.IN_AVOID_COUNTRIES = "INPUT_AVOID_COUNTRIES" self.IN_AVOID_POLYGONS = "INPUT_AVOID_POLYGONS" - self.OUT = 'OUTPUT' + self.OUT = "OUTPUT" self.PARAMETERS = None def createInstance(self) -> Any: @@ -93,7 +100,7 @@ def shortHelpString(self): """ Displays the sidebar help in the algorithm window """ - locale = QSettings().value('locale/userLocale')[0:2] + locale = QSettings().value("locale/userLocale")[0:2] return read_help_file(algorithm=self.ALGO_NAME, locale=locale) @@ -108,18 +115,20 @@ def icon(self) -> QIcon: """ Icon used for algorithm in QGIS toolbox """ - return QIcon(RESOURCE_PREFIX + f'icon_{self.groupId()}.png') + return QIcon(RESOURCE_PREFIX + f"icon_{self.groupId()}.png") def provider_parameter(self) -> QgsProcessingParameterEnum: """ Parameter definition for provider, used in all child classes """ - providers = [provider['name'] for provider in configmanager.read_config()['providers']] + providers = [ + provider["name"] for provider in configmanager.read_config()["providers"] + ] return QgsProcessingParameterEnum( self.IN_PROVIDER, - self.tr("Provider", 'ORSBaseProcessingAlgorithm'), + self.tr("Provider", "ORSBaseProcessingAlgorithm"), providers, - defaultValue=providers[0] + defaultValue=providers[0], ) def profile_parameter(self) -> QgsProcessingParameterEnum: @@ -127,11 +136,11 @@ def profile_parameter(self) -> QgsProcessingParameterEnum: Parameter definition for profile, used in all child classes """ return QgsProcessingParameterEnum( - self.IN_PROFILE, - self.tr("Travel mode", 'ORSBaseProcessingAlgorithm'), - PROFILES, - defaultValue=PROFILES[0] - ) + self.IN_PROFILE, + self.tr("Travel mode", "ORSBaseProcessingAlgorithm"), + PROFILES, + defaultValue=PROFILES[0], + ) def output_parameter(self) -> QgsProcessingParameterFeatureSink: """ @@ -146,42 +155,49 @@ def option_parameters(self) -> [QgsProcessingParameterDefinition]: return [ QgsProcessingParameterEnum( self.IN_AVOID_FEATS, - self.tr("Features to avoid", 'ORSBaseProcessingAlgorithm'), + self.tr("Features to avoid", "ORSBaseProcessingAlgorithm"), AVOID_FEATURES, defaultValue=None, optional=True, - allowMultiple=True + allowMultiple=True, ), QgsProcessingParameterEnum( self.IN_AVOID_BORDERS, - self.tr("Types of borders to avoid", 'ORSBaseProcessingAlgorithm'), + self.tr("Types of borders to avoid", "ORSBaseProcessingAlgorithm"), AVOID_BORDERS, defaultValue=None, - optional=True + optional=True, ), QgsProcessingParameterString( self.IN_AVOID_COUNTRIES, - self.tr("Comma-separated list of ids of countries to avoid", 'ORSBaseProcessingAlgorithm'), + self.tr( + "Comma-separated list of ids of countries to avoid", + "ORSBaseProcessingAlgorithm", + ), defaultValue=None, - optional=True + optional=True, ), QgsProcessingParameterFeatureSource( self.IN_AVOID_POLYGONS, - self.tr("Polygons to avoid", 'ORSBaseProcessingAlgorithm'), + self.tr("Polygons to avoid", "ORSBaseProcessingAlgorithm"), types=[QgsProcessing.TypeVectorPolygon], - optional=True - ) + optional=True, + ), ] @staticmethod - def _get_ors_client_from_provider(provider: str, feedback: QgsProcessingFeedback) -> client.Client: + 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'] + providers = configmanager.read_config()["providers"] ors_provider = providers[provider] ors_client = client.Client(ors_provider) - ors_client.overQueryLimit.connect(lambda: feedback.reportError("OverQueryLimit: Retrying...")) + ors_client.overQueryLimit.connect( + lambda: feedback.reportError("OverQueryLimit: Retrying...") + ) return ors_client def parseOptions(self, parameters: dict, context: QgsProcessingContext) -> dict: @@ -189,19 +205,23 @@ def parseOptions(self, parameters: dict, context: QgsProcessingContext) -> dict: features_raw = parameters[self.IN_AVOID_FEATS] if features_raw: - options['avoid_features'] = [dict(enumerate(AVOID_FEATURES))[feat] for feat in features_raw] + options["avoid_features"] = [ + dict(enumerate(AVOID_FEATURES))[feat] for feat in features_raw + ] borders_raw = parameters[self.IN_AVOID_BORDERS] if borders_raw: - options['avoid_borders'] = dict(enumerate(AVOID_BORDERS))[borders_raw] + options["avoid_borders"] = dict(enumerate(AVOID_BORDERS))[borders_raw] countries_raw = parameters[self.IN_AVOID_COUNTRIES] if countries_raw: - options['avoid_countries'] = list(map(int, countries_raw.split(','))) + options["avoid_countries"] = list(map(int, countries_raw.split(","))) - polygons_layer = self.parameterAsLayer(parameters, self.IN_AVOID_POLYGONS, context) + polygons_layer = self.parameterAsLayer( + parameters, self.IN_AVOID_POLYGONS, context + ) if polygons_layer: - options['avoid_polygons'] = _get_avoid_polygons(polygons_layer) + options["avoid_polygons"] = _get_avoid_polygons(polygons_layer) return options @@ -211,19 +231,26 @@ def initAlgorithm(self, configuration): Combines default and algorithm parameters and adds them in order to the algorithm dialog window. """ - parameters = [self.provider_parameter(), self.profile_parameter()] + self.PARAMETERS + self.option_parameters() + [self.output_parameter()] + parameters = ( + [self.provider_parameter(), self.profile_parameter()] + + self.PARAMETERS + + self.option_parameters() + + [self.output_parameter()] + ) for param in parameters: if param.name() in ADVANCED_PARAMETERS: if self.GROUP == "Matrix": - param.setFlags(param.flags()| QgsProcessingParameterDefinition.FlagHidden) + param.setFlags( + param.flags() | QgsProcessingParameterDefinition.FlagHidden + ) else: # flags() is a wrapper around an enum of ints for type-safety. # Flags are added by or-ing values, much like the union operator would work - param.setFlags(param.flags() | QgsProcessingParameterDefinition.FlagAdvanced) + param.setFlags( + param.flags() | QgsProcessingParameterDefinition.FlagAdvanced + ) - self.addParameter( - param - ) + self.addParameter(param) def tr(self, string, context=None): context = context or self.__class__.__name__ diff --git a/ORStools/proc/directions_lines_proc.py b/ORStools/proc/directions_lines_proc.py index c39dfff2..07c16643 100644 --- a/ORStools/proc/directions_lines_proc.py +++ b/ORStools/proc/directions_lines_proc.py @@ -27,14 +27,15 @@ ***************************************************************************/ """ -from qgis.core import (QgsWkbTypes, - QgsCoordinateReferenceSystem, - QgsProcessing, - QgsProcessingParameterField, - QgsProcessingParameterFeatureSource, - QgsProcessingParameterEnum, - QgsPointXY, - ) +from qgis.core import ( + QgsWkbTypes, + QgsCoordinateReferenceSystem, + QgsProcessing, + QgsProcessingParameterField, + QgsProcessingParameterFeatureSource, + QgsProcessingParameterEnum, + QgsPointXY, +) from ORStools.common import directions_core, PROFILES, PREFERENCES, OPTIMIZATION_MODES from ORStools.utils import transform, exceptions, logger @@ -45,9 +46,10 @@ # noinspection PyPep8Naming class ORSDirectionsLinesAlgorithm(ORSBaseProcessingAlgorithm): """Algorithm class for Directions Lines.""" + def __init__(self): super().__init__() - self.ALGO_NAME = 'directions_from_polylines_layer' + self.ALGO_NAME = "directions_from_polylines_layer" self.GROUP = "Directions" self.IN_LINES = "INPUT_LINE_LAYER" self.IN_FIELD = "INPUT_LAYER_FIELD" @@ -71,7 +73,7 @@ def __init__(self): self.IN_PREFERENCE, self.tr("Travel preference"), PREFERENCES, - defaultValue=PREFERENCES[0] + defaultValue=PREFERENCES[0], ), QgsProcessingParameterEnum( self.IN_OPTIMIZE, @@ -79,11 +81,13 @@ def __init__(self): OPTIMIZATION_MODES, defaultValue=None, optional=True, - ) + ), ] def processAlgorithm(self, parameters, context, feedback): - ors_client = self._get_ors_client_from_provider(parameters[self.IN_PROVIDER], feedback) + ors_client = self._get_ors_client_from_provider( + parameters[self.IN_PROVIDER], feedback + ) profile = dict(enumerate(PROFILES))[parameters[self.IN_PROFILE]] @@ -94,11 +98,7 @@ def processAlgorithm(self, parameters, context, feedback): options = self.parseOptions(parameters, context) # Get parameter values - source = self.parameterAsSource( - parameters, - self.IN_LINES, - context - ) + source = self.parameterAsSource(parameters, self.IN_LINES, context) # parameters[self.IN_FIELD] returns a PyQt5.QtCore.QVariant with "NULL" as content # in case of absence of self.IN_FIELD. @@ -111,18 +111,25 @@ def processAlgorithm(self, parameters, context, feedback): get_fields_options = dict() if source_field_name: get_fields_options.update( - from_type=source.fields().field(source_field_name).type(), - from_name=source_field_name - ) + from_type=source.fields().field(source_field_name).type(), + from_name=source_field_name, + ) sink_fields = directions_core.get_fields(**get_fields_options, line=True) - (sink, dest_id) = self.parameterAsSink(parameters, self.OUT, context, sink_fields, - source.wkbType(), - QgsCoordinateReferenceSystem.fromEpsgId(4326)) + (sink, dest_id) = self.parameterAsSink( + parameters, + self.OUT, + context, + sink_fields, + source.wkbType(), + QgsCoordinateReferenceSystem.fromEpsgId(4326), + ) count = source.featureCount() - for num, (line, field_value) in enumerate(self._get_sorted_lines(source, source_field_name)): + 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 @@ -130,26 +137,31 @@ def processAlgorithm(self, parameters, context, feedback): try: if optimization_mode is not None: params = get_params_optimize(line, profile, optimization_mode) - response = ors_client.request('/optimization', {}, post_json=params) + response = ors_client.request("/optimization", {}, post_json=params) - sink.addFeature(directions_core.get_output_features_optimization( - response, - profile, - from_value=field_value - )) + sink.addFeature( + directions_core.get_output_features_optimization( + response, profile, from_value=field_value + ) + ) else: - params = directions_core.build_default_parameters(preference, point_list=line, options=options) - response = ors_client.request('/v2/directions/' + profile + '/geojson', {}, post_json=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: + params = directions_core.build_default_parameters( + preference, point_list=line, options=options + ) + response = ors_client.request( + "/v2/directions/" + profile + "/geojson", {}, post_json=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 = f"Feature ID {num} caused a {e.__class__.__name__}:\n{str(e)}" feedback.reportError(msg) logger.log(msg) @@ -181,10 +193,16 @@ def _get_sorted_lines(layer, field_name): if QgsWkbTypes.flatType(layer.wkbType()) == QgsWkbTypes.MultiLineString: # TODO: only takes the first polyline geometry from the multiline geometry currently # Loop over all polyline geometries - line = [x_former.transform(QgsPointXY(point)) for point in feat.geometry().asMultiPolyline()[0]] + line = [ + x_former.transform(QgsPointXY(point)) + for point in feat.geometry().asMultiPolyline()[0] + ] elif QgsWkbTypes.flatType(layer.wkbType()) == QgsWkbTypes.LineString: - line = [x_former.transform(QgsPointXY(point)) for point in feat.geometry().asPolyline()] + line = [ + x_former.transform(QgsPointXY(point)) + for point in feat.geometry().asPolyline() + ] yield line, field_value diff --git a/ORStools/proc/directions_points_layer_proc.py b/ORStools/proc/directions_points_layer_proc.py index 2c7fddb0..134aa483 100644 --- a/ORStools/proc/directions_points_layer_proc.py +++ b/ORStools/proc/directions_points_layer_proc.py @@ -27,14 +27,15 @@ ***************************************************************************/ """ -from qgis.core import (QgsWkbTypes, - QgsCoordinateReferenceSystem, - QgsProcessing, - QgsProcessingParameterField, - QgsProcessingParameterFeatureSource, - QgsProcessingParameterEnum, - QgsPointXY, - ) +from qgis.core import ( + QgsWkbTypes, + QgsCoordinateReferenceSystem, + QgsProcessing, + QgsProcessingParameterField, + QgsProcessingParameterFeatureSource, + QgsProcessingParameterEnum, + QgsPointXY, +) from ORStools.common import directions_core, PROFILES, PREFERENCES, OPTIMIZATION_MODES from ORStools.utils import transform, exceptions, logger @@ -48,7 +49,7 @@ class ORSDirectionsPointsLayerAlgo(ORSBaseProcessingAlgorithm): def __init__(self): super().__init__() - self.ALGO_NAME = 'directions_from_points_1_layer' + self.ALGO_NAME = "directions_from_points_1_layer" self.GROUP = "Directions" self.IN_POINTS = "INPUT_POINT_LAYER" self.IN_FIELD = "INPUT_LAYER_FIELD" @@ -67,20 +68,20 @@ def __init__(self): description=self.tr("Layer ID Field"), parentLayerParameterName=self.IN_POINTS, defaultValue=None, - optional=True + optional=True, ), QgsProcessingParameterField( name=self.IN_SORTBY, description=self.tr("Sort Points by"), parentLayerParameterName=self.IN_POINTS, defaultValue=None, - optional=True + optional=True, ), QgsProcessingParameterEnum( self.IN_PREFERENCE, self.tr("Travel preference"), PREFERENCES, - defaultValue=PREFERENCES[0] + defaultValue=PREFERENCES[0], ), QgsProcessingParameterEnum( self.IN_OPTIMIZE, @@ -88,11 +89,13 @@ def __init__(self): OPTIMIZATION_MODES, defaultValue=None, optional=True, - ) + ), ] def processAlgorithm(self, parameters, context, feedback): - ors_client = self._get_ors_client_from_provider(parameters[self.IN_PROVIDER], feedback) + ors_client = self._get_ors_client_from_provider( + parameters[self.IN_PROVIDER], feedback + ) profile = dict(enumerate(PROFILES))[parameters[self.IN_PROFILE]] @@ -103,33 +106,37 @@ def processAlgorithm(self, parameters, context, feedback): options = self.parseOptions(parameters, context) # Get parameter values - source = self.parameterAsSource( - parameters, - self.IN_POINTS, - context - ) + source = self.parameterAsSource(parameters, self.IN_POINTS, context) source_field_name = parameters[self.IN_FIELD] get_fields_options = dict() if source_field_name: get_fields_options.update( from_type=source.fields().field(source_field_name).type(), - from_name=source_field_name + from_name=source_field_name, ) sink_fields = directions_core.get_fields(**get_fields_options, line=True) - (sink, dest_id) = self.parameterAsSink(parameters, self.OUT, context, - sink_fields, - QgsWkbTypes.LineString, - QgsCoordinateReferenceSystem.fromEpsgId(4326)) + (sink, dest_id) = self.parameterAsSink( + parameters, + self.OUT, + context, + sink_fields, + QgsWkbTypes.LineString, + QgsCoordinateReferenceSystem.fromEpsgId(4326), + ) sort_by = parameters[self.IN_SORTBY] if sort_by: - def sort(f): return f.attribute(sort_by) + + def sort(f): + return f.attribute(sort_by) else: - def sort(f): return f.id() + + def sort(f): + return f.id() count = source.featureCount() @@ -150,7 +157,9 @@ def sort(f): return f.id() for point in feat.geometry().asMultiPoint(): points.append(x_former.transform(QgsPointXY(point))) input_points.append(points) - from_values.append(feat[source_field_name] if source_field_name else None) + from_values.append( + feat[source_field_name] if source_field_name else None + ) for num, (points, from_value) in enumerate(zip(input_points, from_values)): # Stop the algorithm if cancel button has been clicked @@ -160,26 +169,31 @@ def sort(f): return f.id() try: if optimization_mode is not None: params = get_params_optimize(points, profile, optimization_mode) - response = ors_client.request('/optimization', {}, post_json=params) + response = ors_client.request("/optimization", {}, post_json=params) - sink.addFeature(directions_core.get_output_features_optimization( - response, - profile, - from_value=from_value - )) + sink.addFeature( + directions_core.get_output_features_optimization( + response, profile, from_value=from_value + ) + ) else: - params = directions_core.build_default_parameters(preference, point_list=points, options=options) - response = ors_client.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: + params = directions_core.build_default_parameters( + preference, point_list=points, options=options + ) + response = ors_client.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 = f"Feature ID {from_value} caused a {e.__class__.__name__}:\n{str(e)}" feedback.reportError(msg) logger.log(msg) diff --git a/ORStools/proc/directions_points_layers_proc.py b/ORStools/proc/directions_points_layers_proc.py index 08a293b1..cae81049 100644 --- a/ORStools/proc/directions_points_layers_proc.py +++ b/ORStools/proc/directions_points_layers_proc.py @@ -27,13 +27,14 @@ ***************************************************************************/ """ -from qgis.core import (QgsWkbTypes, - QgsCoordinateReferenceSystem, - QgsProcessing, - QgsProcessingParameterField, - QgsProcessingParameterFeatureSource, - QgsProcessingParameterEnum, - ) +from qgis.core import ( + QgsWkbTypes, + QgsCoordinateReferenceSystem, + QgsProcessing, + QgsProcessingParameterField, + QgsProcessingParameterFeatureSource, + QgsProcessingParameterEnum, +) from ORStools.common import directions_core, PROFILES, PREFERENCES from ORStools.utils import transform, exceptions, logger @@ -42,12 +43,11 @@ # noinspection PyPep8Naming class ORSDirectionsPointsLayersAlgo(ORSBaseProcessingAlgorithm): - def __init__(self): super().__init__() - self.ALGO_NAME = 'directions_from_points_2_layers' + self.ALGO_NAME = "directions_from_points_2_layers" self.GROUP = "Directions" - self.MODE_SELECTION: list = ['Row-by-Row', 'All-by-All'] + self.MODE_SELECTION: list = ["Row-by-Row", "All-by-All"] self.IN_START = "INPUT_START_LAYER" self.IN_START_FIELD = "INPUT_START_FIELD" self.IN_SORT_START_BY = "INPUT_SORT_START_BY" @@ -74,7 +74,7 @@ def __init__(self): description=self.tr("Sort Start Points by"), parentLayerParameterName=self.IN_START, defaultValue=None, - optional=True + optional=True, ), QgsProcessingParameterFeatureSource( name=self.IN_END, @@ -93,26 +93,28 @@ def __init__(self): description=self.tr("Sort End Points by"), parentLayerParameterName=self.IN_END, defaultValue=None, - optional=True + optional=True, ), QgsProcessingParameterEnum( self.IN_PREFERENCE, self.tr("Travel preference"), PREFERENCES, - defaultValue=PREFERENCES[0] + defaultValue=PREFERENCES[0], ), QgsProcessingParameterEnum( self.IN_MODE, self.tr("Layer mode"), self.MODE_SELECTION, - defaultValue=self.MODE_SELECTION[0] - ) + defaultValue=self.MODE_SELECTION[0], + ), ] # TODO: preprocess parameters to options the range cleanup below: # https://www.qgis.org/pyqgis/master/core/Processing/QgsProcessingAlgorithm.html#qgis.core.QgsProcessingAlgorithm.preprocessParameters def processAlgorithm(self, parameters, context, feedback): - ors_client = self._get_ors_client_from_provider(parameters[self.IN_PROVIDER], feedback) + ors_client = self._get_ors_client_from_provider( + parameters[self.IN_PROVIDER], feedback + ) profile = dict(enumerate(PROFILES))[parameters[self.IN_PROFILE]] @@ -123,44 +125,45 @@ def processAlgorithm(self, parameters, context, feedback): options = self.parseOptions(parameters, context) # Get parameter values - source = self.parameterAsSource( - parameters, - self.IN_START, - context - ) + source = self.parameterAsSource(parameters, self.IN_START, context) source_field_name = parameters[self.IN_START_FIELD] - source_field = source.fields().field(source_field_name) if source_field_name else None + source_field = ( + source.fields().field(source_field_name) if source_field_name else None + ) sort_start_by = parameters[self.IN_SORT_START_BY] if sort_start_by: - def sort_start(f): return f.attribute(sort_start_by) + + def sort_start(f): + return f.attribute(sort_start_by) else: - def sort_start(f): return f.id() - destination = self.parameterAsSource( - parameters, - self.IN_END, - context - ) + def sort_start(f): + return f.id() + + destination = self.parameterAsSource(parameters, self.IN_END, context) destination_field_name = parameters[self.IN_END_FIELD] - destination_field = destination.fields().field(destination_field_name) if destination_field_name else None + destination_field = ( + destination.fields().field(destination_field_name) + if destination_field_name + else None + ) sort_end_by = parameters[self.IN_SORT_END_BY] if sort_end_by: - def sort_end(f): return f.attribute(sort_end_by) + + def sort_end(f): + return f.attribute(sort_end_by) else: - def sort_end(f): return f.id() + + def sort_end(f): + return f.id() route_dict = self._get_route_dict( - source, - source_field, - sort_start, - destination, - destination_field, - sort_end + source, source_field, sort_start, destination, destination_field, sort_end ) - if mode == 'Row-by-Row': + if mode == "Row-by-Row": route_count = min([source.featureCount(), destination.featureCount()]) else: route_count = source.featureCount() * destination.featureCount() @@ -173,35 +176,50 @@ def sort_end(f): return f.id() field_types.update({"to_type": destination_field.type()}) sink_fields = directions_core.get_fields(**field_types) - (sink, dest_id) = self.parameterAsSink(parameters, self.OUT, context, sink_fields, - QgsWkbTypes.LineString, - QgsCoordinateReferenceSystem.fromEpsgId(4326)) + (sink, dest_id) = self.parameterAsSink( + parameters, + self.OUT, + context, + sink_fields, + QgsWkbTypes.LineString, + QgsCoordinateReferenceSystem.fromEpsgId(4326), + ) counter = 0 - for coordinates, values in directions_core.get_request_point_features(route_dict, mode): + 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 = directions_core.build_default_parameters(preference, coordinates=coordinates, options=options) + params = directions_core.build_default_parameters( + preference, coordinates=coordinates, options=options + ) try: - response = ors_client.request('/v2/directions/' + profile + '/geojson', {}, post_json=params) - except (exceptions.ApiError, - exceptions.InvalidKey, - exceptions.GenericServerError) as e: + response = ors_client.request( + "/v2/directions/" + profile + "/geojson", {}, post_json=params + ) + except ( + exceptions.ApiError, + exceptions.InvalidKey, + exceptions.GenericServerError, + ) as e: msg = f"Route from {values[0]} to {values[1]} caused a {e.__class__.__name__}:\n{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] - )) + 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)) @@ -209,7 +227,9 @@ def sort_end(f): return f.id() return {self.OUT: dest_id} @staticmethod - def _get_route_dict(source, source_field, sort_start, destination, destination_field, sort_end): + def _get_route_dict( + source, source_field, sort_start, destination, destination_field, sort_end + ): """ Compute route_dict from input layer. @@ -231,18 +251,30 @@ def _get_route_dict(source, source_field, sort_start, destination, destination_f route_dict = dict() source_feats = sorted(list(source.getFeatures()), key=sort_start) x_former_source = transform.transformToWGS(source.sourceCrs()) - route_dict['start'] = dict( - geometries=[x_former_source.transform(feat.geometry().asPoint()) for feat in source_feats], - values=[feat.attribute(source_field.name()) if source_field else feat.id() for feat in source_feats], + route_dict["start"] = dict( + geometries=[ + x_former_source.transform(feat.geometry().asPoint()) + for feat in source_feats + ], + values=[ + feat.attribute(source_field.name()) if source_field else feat.id() + for feat in source_feats + ], ) destination_feats = sorted(list(destination.getFeatures()), key=sort_end) x_former_destination = transform.transformToWGS(destination.sourceCrs()) - route_dict['end'] = dict( - geometries=[x_former_destination.transform(feat.geometry().asPoint()) for feat in destination_feats], - values=[feat.attribute(destination_field.name()) if destination_field else feat.id() for feat in - destination_feats - ], + route_dict["end"] = dict( + geometries=[ + x_former_destination.transform(feat.geometry().asPoint()) + for feat in destination_feats + ], + values=[ + feat.attribute(destination_field.name()) + if destination_field + else feat.id() + for feat in destination_feats + ], ) return route_dict diff --git a/ORStools/proc/isochrones_layer_proc.py b/ORStools/proc/isochrones_layer_proc.py index 8fdd7367..da0b3849 100644 --- a/ORStools/proc/isochrones_layer_proc.py +++ b/ORStools/proc/isochrones_layer_proc.py @@ -27,16 +27,17 @@ ***************************************************************************/ """ -from qgis.core import (QgsWkbTypes, - QgsCoordinateReferenceSystem, - QgsProcessing, - QgsProcessingUtils, - QgsProcessingException, - QgsProcessingParameterField, - QgsProcessingParameterFeatureSource, - QgsProcessingParameterString, - QgsProcessingParameterEnum - ) +from qgis.core import ( + QgsWkbTypes, + QgsCoordinateReferenceSystem, + QgsProcessing, + QgsProcessingUtils, + QgsProcessingException, + QgsProcessingParameterField, + QgsProcessingParameterFeatureSource, + QgsProcessingParameterString, + QgsProcessingParameterEnum, +) from ORStools.common import isochrones_core, PROFILES, DIMENSIONS from ORStools.proc.base_processing_algorithm import ORSBaseProcessingAlgorithm @@ -47,20 +48,20 @@ class ORSIsochronesLayerAlgo(ORSBaseProcessingAlgorithm): def __init__(self): super().__init__() - self.ALGO_NAME = 'isochrones_from_layer' - self.GROUP = 'Isochrones' + self.ALGO_NAME = "isochrones_from_layer" + self.GROUP = "Isochrones" self.IN_POINTS = "INPUT_POINT_LAYER" self.IN_FIELD = "INPUT_FIELD" - self.IN_METRIC = 'INPUT_METRIC' - self.IN_RANGES = 'INPUT_RANGES' - self.IN_KEY = 'INPUT_APIKEY' - self.IN_DIFFERENCE = 'INPUT_DIFFERENCE' + self.IN_METRIC = "INPUT_METRIC" + self.IN_RANGES = "INPUT_RANGES" + self.IN_KEY = "INPUT_APIKEY" + self.IN_DIFFERENCE = "INPUT_DIFFERENCE" self.PARAMETERS = [ QgsProcessingParameterFeatureSource( name=self.IN_POINTS, description=self.tr("Input Point layer"), - types=[QgsProcessing.TypeVectorPoint] + types=[QgsProcessing.TypeVectorPoint], ), # QgsProcessingParameterBoolean( # name=self.IN_DIFFERENCE, @@ -68,21 +69,23 @@ def __init__(self): # ) QgsProcessingParameterField( name=self.IN_FIELD, - description=self.tr("Input layer ID Field (mutually exclusive with Point option)"), + description=self.tr( + "Input layer ID Field (mutually exclusive with Point option)" + ), parentLayerParameterName=self.IN_POINTS, - optional=True + optional=True, ), QgsProcessingParameterEnum( name=self.IN_METRIC, description=self.tr("Dimension"), options=DIMENSIONS, - defaultValue=DIMENSIONS[0] + defaultValue=DIMENSIONS[0], ), QgsProcessingParameterString( name=self.IN_RANGES, description=self.tr("Comma-separated ranges [min or m]"), - defaultValue="5, 10" - ) + defaultValue="5, 10", + ), ] # Save some important references @@ -95,14 +98,16 @@ def __init__(self): # TODO: preprocess parameters to options the range cleanup below: # https://www.qgis.org/pyqgis/master/core/Processing/QgsProcessingAlgorithm.html#qgis.core.QgsProcessingAlgorithm.prepareAlgorithm def processAlgorithm(self, parameters, context, feedback): - ors_client = self._get_ors_client_from_provider(parameters[self.IN_PROVIDER], feedback) + ors_client = self._get_ors_client_from_provider( + parameters[self.IN_PROVIDER], feedback + ) profile = dict(enumerate(PROFILES))[parameters[self.IN_PROFILE]] dimension = dict(enumerate(DIMENSIONS))[parameters[self.IN_METRIC]] - factor = 60 if dimension == 'time' else 1 + factor = 60 if dimension == "time" else 1 ranges_raw = parameters[self.IN_RANGES] - ranges_proc = [x * factor for x in map(int, ranges_raw.split(','))] + ranges_proc = [x * factor for x in map(int, ranges_raw.split(","))] # self.difference = self.parameterAsBool(parameters, self.IN_DIFFERENCE, context) source = self.parameterAsSource(parameters, self.IN_POINTS, context) @@ -113,7 +118,8 @@ def processAlgorithm(self, parameters, context, feedback): requests = [] if QgsWkbTypes.flatType(source.wkbType()) == QgsWkbTypes.MultiPoint: raise QgsProcessingException( - "TypeError: Multipoint Layers are not accepted. Please convert to single geometry layer.") + "TypeError: Multipoint Layers are not accepted. Please convert to single geometry layer." + ) # Get ID field properties id_field_name = parameters[self.IN_FIELD] @@ -124,26 +130,34 @@ def processAlgorithm(self, parameters, context, feedback): self.isochrones.set_parameters(profile, dimension, factor, *parameter_options) - for locations, id_value in self.get_sorted_feature_parameters(source, id_field_name): + for locations, id_value in self.get_sorted_feature_parameters( + source, id_field_name + ): # Stop the algorithm if cancel button has been clicked if feedback.isCanceled(): break - requests.append({ - "locations": locations, - "range_type": dimension, - "range": ranges_proc, - "attributes": ['total_pop'], - "id": id_value, - "options": options - }) - - (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) + requests.append( + { + "locations": locations, + "range_type": dimension, + "range": ranges_proc, + "attributes": ["total_pop"], + "id": id_value, + "options": options, + } + ) + + (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 feedback.isCanceled(): @@ -152,14 +166,18 @@ def processAlgorithm(self, parameters, context, feedback): # If feature causes error, report and continue with next try: # Populate features from response - response = ors_client.request('/v2/isochrones/' + profile, {}, post_json=params) + response = ors_client.request( + "/v2/isochrones/" + profile, {}, post_json=params + ) - for isochrone in self.isochrones.get_features(response, params['id']): + for isochrone in self.isochrones.get_features(response, params["id"]): sink.addFeature(isochrone) - except (exceptions.ApiError, - exceptions.InvalidKey, - exceptions.GenericServerError) as e: + except ( + exceptions.ApiError, + exceptions.InvalidKey, + exceptions.GenericServerError, + ) as e: msg = f"Feature ID {params['id']} caused a {e.__class__.__name__}:\n{str(e)}" feedback.reportError(msg) logger.log(msg, 2) @@ -178,7 +196,9 @@ def postProcessAlgorithm(self, context, feedback): return {self.OUT: self.dest_id} @staticmethod - def get_sorted_feature_parameters(layer: QgsProcessingParameterFeatureSource, id_field_name: str): + def get_sorted_feature_parameters( + layer: QgsProcessingParameterFeatureSource, id_field_name: str + ): """ Generator to yield geometry and id of features sorted by feature ID. Careful: feat.id() is not necessarily permanent diff --git a/ORStools/proc/isochrones_point_proc.py b/ORStools/proc/isochrones_point_proc.py index f1594ece..a6b1d123 100644 --- a/ORStools/proc/isochrones_point_proc.py +++ b/ORStools/proc/isochrones_point_proc.py @@ -27,13 +27,14 @@ ***************************************************************************/ """ -from qgis.core import (QgsWkbTypes, - QgsCoordinateReferenceSystem, - QgsProcessingUtils, - QgsProcessingParameterString, - QgsProcessingParameterEnum, - QgsProcessingParameterPoint, - ) +from qgis.core import ( + QgsWkbTypes, + QgsCoordinateReferenceSystem, + QgsProcessingUtils, + QgsProcessingParameterString, + QgsProcessingParameterEnum, + QgsProcessingParameterPoint, +) from ORStools.common import isochrones_core, PROFILES, DIMENSIONS from ORStools.utils import exceptions, logger @@ -44,30 +45,32 @@ class ORSIsochronesPointAlgo(ORSBaseProcessingAlgorithm): def __init__(self): super().__init__() - self.ALGO_NAME = 'isochrones_from_point' + self.ALGO_NAME = "isochrones_from_point" self.GROUP = "Isochrones" self.IN_POINT = "INPUT_POINT" - self.IN_METRIC = 'INPUT_METRIC' - self.IN_RANGES = 'INPUT_RANGES' - self.IN_KEY = 'INPUT_APIKEY' - self.IN_DIFFERENCE = 'INPUT_DIFFERENCE' + self.IN_METRIC = "INPUT_METRIC" + self.IN_RANGES = "INPUT_RANGES" + self.IN_KEY = "INPUT_APIKEY" + self.IN_DIFFERENCE = "INPUT_DIFFERENCE" self.PARAMETERS = [ QgsProcessingParameterPoint( name=self.IN_POINT, - description=self.tr("Input Point from map canvas (mutually exclusive with layer option)"), - optional=True + description=self.tr( + "Input Point from map canvas (mutually exclusive with layer option)" + ), + optional=True, ), QgsProcessingParameterEnum( name=self.IN_METRIC, description=self.tr("Dimension"), options=DIMENSIONS, - defaultValue=DIMENSIONS[0] + defaultValue=DIMENSIONS[0], ), QgsProcessingParameterString( name=self.IN_RANGES, description=self.tr("Comma-separated ranges [min or m]"), - defaultValue="5, 10" - ) + defaultValue="5, 10", + ), ] # Save some important references @@ -80,14 +83,16 @@ def __init__(self): # TODO: preprocess parameters to options the range cleanup below: # https://www.qgis.org/pyqgis/master/core/Processing/QgsProcessingAlgorithm.html#qgis.core.QgsProcessingAlgorithm.preprocessParameters def processAlgorithm(self, parameters, context, feedback): - ors_client = self._get_ors_client_from_provider(parameters[self.IN_PROVIDER], feedback) + ors_client = self._get_ors_client_from_provider( + parameters[self.IN_PROVIDER], feedback + ) profile = dict(enumerate(PROFILES))[parameters[self.IN_PROFILE]] dimension = dict(enumerate(DIMENSIONS))[parameters[self.IN_METRIC]] - factor = 60 if dimension == 'time' else 1 + factor = 60 if dimension == "time" else 1 ranges_raw = parameters[self.IN_RANGES] - ranges_proc = [x * factor for x in map(int, ranges_raw.split(','))] + ranges_proc = [x * factor for x in map(int, ranges_raw.split(","))] options = self.parseOptions(parameters, context) @@ -100,29 +105,39 @@ def processAlgorithm(self, parameters, context, feedback): "locations": [[round(point.x(), 6), round(point.y(), 6)]], "range_type": dimension, "range": ranges_proc, - "attributes": ['total_pop'], + "attributes": ["total_pop"], "id": None, - "options": options + "options": options, } - (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) + (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, + ) try: - response = ors_client.request('/v2/isochrones/' + profile, {}, post_json=params) + response = ors_client.request( + "/v2/isochrones/" + profile, {}, post_json=params + ) # Populate features from response - for isochrone in self.isochrones.get_features(response, params['id']): + for isochrone in self.isochrones.get_features(response, params["id"]): sink.addFeature(isochrone) - except (exceptions.ApiError, - exceptions.InvalidKey, - exceptions.GenericServerError) as e: - msg = f"Feature ID {params['id']} caused a {e.__class__.__name__}:\n{str(e)}" + except ( + exceptions.ApiError, + exceptions.InvalidKey, + exceptions.GenericServerError, + ) as e: + msg = ( + f"Feature ID {params['id']} caused a {e.__class__.__name__}:\n{str(e)}" + ) feedback.reportError(msg) logger.log(msg, 2) diff --git a/ORStools/proc/matrix_proc.py b/ORStools/proc/matrix_proc.py index ee7c34a3..2cdedb54 100644 --- a/ORStools/proc/matrix_proc.py +++ b/ORStools/proc/matrix_proc.py @@ -27,15 +27,16 @@ ***************************************************************************/ """ -from qgis.core import (QgsWkbTypes, - QgsFeature, - QgsProcessing, - QgsFields, - QgsField, - QgsProcessingException, - QgsProcessingParameterField, - QgsProcessingParameterFeatureSource, - ) +from qgis.core import ( + QgsWkbTypes, + QgsFeature, + QgsProcessing, + QgsFields, + QgsField, + QgsProcessingException, + QgsProcessingParameterField, + QgsProcessingParameterFeatureSource, +) from PyQt5.QtCore import QVariant @@ -48,7 +49,7 @@ class ORSMatrixAlgo(ORSBaseProcessingAlgorithm): def __init__(self): super().__init__() - self.ALGO_NAME = 'matrix_from_layers' + self.ALGO_NAME = "matrix_from_layers" self.GROUP = "Matrix" self.IN_START = "INPUT_START_LAYER" self.IN_START_FIELD = "INPUT_START_FIELD" @@ -82,7 +83,9 @@ def __init__(self): ] def processAlgorithm(self, parameters, context, feedback): - ors_client = self._get_ors_client_from_provider(parameters[self.IN_PROVIDER], feedback) + ors_client = self._get_ors_client_from_provider( + parameters[self.IN_PROVIDER], feedback + ) # Get profile value profile = dict(enumerate(PROFILES))[parameters[self.IN_PROFILE]] @@ -91,28 +94,29 @@ def processAlgorithm(self, parameters, context, feedback): # options = self.parseOptions(parameters, context) # Get parameter values - source = self.parameterAsSource( - parameters, - self.IN_START, - context - ) + source = self.parameterAsSource(parameters, self.IN_START, context) source_field_name = parameters[self.IN_START_FIELD] - source_field = source.fields().field(source_field_name) if source_field_name else None - - destination = self.parameterAsSource( - parameters, - self.IN_END, - context + source_field = ( + source.fields().field(source_field_name) if source_field_name else None ) + + destination = self.parameterAsSource(parameters, self.IN_END, context) destination_field_name = parameters[self.IN_END_FIELD] - destination_field = destination.fields().field(destination_field_name) if destination_field_name else None + destination_field = ( + destination.fields().field(destination_field_name) + if destination_field_name + else None + ) # Abort when MultiPoint type - if (QgsWkbTypes.flatType(source.wkbType()) or QgsWkbTypes.flatType(destination.wkbType()))\ - == QgsWkbTypes.MultiPoint: + if ( + QgsWkbTypes.flatType(source.wkbType()) + or QgsWkbTypes.flatType(destination.wkbType()) + ) == QgsWkbTypes.MultiPoint: raise QgsProcessingException( - "TypeError: Multipoint Layers are not accepted. Please convert to single geometry layer.") + "TypeError: Multipoint Layers are not accepted. Please convert to single geometry layer." + ) # Get source and destination features sources_features = list(source.getFeatures()) @@ -122,32 +126,48 @@ def processAlgorithm(self, parameters, context, feedback): destinations_amount = destination.featureCount() # Allow for 50 features in source if source == destination - source_equals_destination = parameters['INPUT_START_LAYER'] == parameters['INPUT_END_LAYER'] + source_equals_destination = ( + parameters["INPUT_START_LAYER"] == parameters["INPUT_END_LAYER"] + ) if source_equals_destination: features = sources_features x_former = transform.transformToWGS(source.sourceCrs()) - features_points = [x_former.transform(feat.geometry().asPoint()) for feat in features] + features_points = [ + x_former.transform(feat.geometry().asPoint()) for feat in features + ] else: x_former = transform.transformToWGS(source.sourceCrs()) - sources_features_x_formed = [x_former.transform(feat.geometry().asPoint()) for feat in sources_features] + sources_features_x_formed = [ + x_former.transform(feat.geometry().asPoint()) + for feat in sources_features + ] x_former = transform.transformToWGS(destination.sourceCrs()) - destination_features_x_formed = [x_former.transform(feat.geometry().asPoint()) for feat in - destination_features] + destination_features_x_formed = [ + x_former.transform(feat.geometry().asPoint()) + for feat in destination_features + ] features_points = sources_features_x_formed + destination_features_x_formed # Get IDs - sources_ids = list(range(sources_amount)) if source_equals_destination else list(range(sources_amount)) - destination_ids = list(range(sources_amount)) if source_equals_destination else list( - range(sources_amount, sources_amount + destinations_amount)) + 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)) + ) params = { - 'locations': [[point.x(), point.y()] for point in features_points], - 'sources': sources_ids, - 'destinations': destination_ids, - 'metrics': ["duration", "distance"], - 'id': 'Matrix' + "locations": [[point.x(), point.y()] for point in features_points], + "sources": sources_ids, + "destinations": destination_ids, + "metrics": ["duration", "distance"], + "id": "Matrix", # 'options': options } @@ -162,39 +182,45 @@ def processAlgorithm(self, parameters, context, feedback): # Make request and catch ApiError try: - response = ors_client.request('/v2/matrix/' + profile, {}, post_json=params) + response = ors_client.request("/v2/matrix/" + profile, {}, post_json=params) - except (exceptions.ApiError, - exceptions.InvalidKey, - exceptions.GenericServerError) as e: + except ( + exceptions.ApiError, + exceptions.InvalidKey, + exceptions.GenericServerError, + ) as e: msg = f"{e.__class__.__name__}: {str(e)}" feedback.reportError(msg) logger.log(msg) (sink, dest_id) = self.parameterAsSink( - parameters, - self.OUT, - context, - sink_fields, - QgsWkbTypes.NoGeometry + parameters, self.OUT, context, sink_fields, QgsWkbTypes.NoGeometry ) - sources_attributes = [feat.attribute(source_field_name) if source_field_name else feat.id() for feat in - sources_features] - destinations_attributes = [feat.attribute(destination_field_name) if destination_field_name else feat.id() for - feat in destination_features] + sources_attributes = [ + feat.attribute(source_field_name) if source_field_name else feat.id() + for feat in sources_features + ] + destinations_attributes = [ + feat.attribute(destination_field_name) + if destination_field_name + else feat.id() + 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] + 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 - ]) + 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) @@ -204,7 +230,6 @@ def processAlgorithm(self, parameters, context, feedback): # Change to be consistent @staticmethod def get_fields(source_type=QVariant.Int, destination_type=QVariant.Int): - fields = QgsFields() fields.append(QgsField("FROM_ID", source_type)) fields.append(QgsField("TO_ID", destination_type)) diff --git a/ORStools/proc/provider.py b/ORStools/proc/provider.py index e72368be..be8e6335 100644 --- a/ORStools/proc/provider.py +++ b/ORStools/proc/provider.py @@ -41,11 +41,9 @@ class ORStoolsProvider(QgsProcessingProvider): - def __init__(self): QgsProcessingProvider.__init__(self) - def unload(self): """ Unloads the provider. Any tear-down steps required by the provider @@ -68,7 +66,7 @@ def loadAlgorithms(self): @staticmethod def icon(): - return QIcon(RESOURCE_PREFIX + 'icon_orstools.png') + return QIcon(RESOURCE_PREFIX + "icon_orstools.png") @staticmethod def id(): @@ -98,4 +96,4 @@ def longName(): (version 2.2.1)". This string should be localised. The default implementation returns the same string as name(). """ - return PLUGIN_NAME + ' plugin v' + __version__ + return PLUGIN_NAME + " plugin v" + __version__ diff --git a/ORStools/utils/configmanager.py b/ORStools/utils/configmanager.py index c84e87f6..26e4d614 100644 --- a/ORStools/utils/configmanager.py +++ b/ORStools/utils/configmanager.py @@ -53,7 +53,7 @@ def write_config(new_config): :param new_config: new provider settings after altering in dialog. :type new_config: dict """ - with open(CONFIG_PATH, 'w') as f: + with open(CONFIG_PATH, "w") as f: yaml.safe_dump(new_config, f) diff --git a/ORStools/utils/convert.py b/ORStools/utils/convert.py index addd763f..3e267a2a 100644 --- a/ORStools/utils/convert.py +++ b/ORStools/utils/convert.py @@ -51,7 +51,7 @@ def decode_polyline(polyline, is3d=False): index += 1 result += b << shift shift += 5 - if b < 0x1f: + if b < 0x1F: break lat += (~result >> 1) if (result & 1) != 0 else (result >> 1) @@ -62,7 +62,7 @@ def decode_polyline(polyline, is3d=False): index += 1 result += b << shift shift += 5 - if b < 0x1f: + if b < 0x1F: break lng += ~(result >> 1) if (result & 1) != 0 else (result >> 1) @@ -74,14 +74,16 @@ def decode_polyline(polyline, is3d=False): index += 1 result += b << shift shift += 5 - if b < 0x1f: + if b < 0x1F: break if (result & 1) != 0: z += ~(result >> 1) else: - z += (result >> 1) + z += result >> 1 - points.append([round(lng * 1e-5, 6), round(lat * 1e-5, 6), round(z * 1e-2, 1)]) + points.append( + [round(lng * 1e-5, 6), round(lat * 1e-5, 6), round(z * 1e-2, 1)] + ) else: points.append([round(lng * 1e-5, 6), round(lat * 1e-5, 6)]) diff --git a/ORStools/utils/exceptions.py b/ORStools/utils/exceptions.py index 5828688b..ac8fe7d7 100644 --- a/ORStools/utils/exceptions.py +++ b/ORStools/utils/exceptions.py @@ -34,6 +34,7 @@ class ApiError(Exception): """Represents an exception returned by the remote API.""" + def __init__(self, status, message=None): self.status = status self.message = message @@ -47,6 +48,7 @@ def __str__(self): class InvalidKey(Exception): """only called for 403""" + def __init__(self, status, message): self.status = status self.message = message @@ -74,6 +76,7 @@ def __str__(self): class Timeout(Exception): """The request timed out.""" + pass diff --git a/ORStools/utils/logger.py b/ORStools/utils/logger.py index 6fdaeca7..cd6bf301 100644 --- a/ORStools/utils/logger.py +++ b/ORStools/utils/logger.py @@ -31,6 +31,7 @@ from ORStools import PLUGIN_NAME + def log(message, level_in=0): """ Writes to QGIS inbuilt logger accessible through panel. diff --git a/ORStools/utils/maptools.py b/ORStools/utils/maptools.py index ce9f94ef..ae347d0b 100644 --- a/ORStools/utils/maptools.py +++ b/ORStools/utils/maptools.py @@ -27,9 +27,8 @@ ***************************************************************************/ """ -from qgis.core import (QgsWkbTypes) -from qgis.gui import (QgsMapToolEmitPoint, - QgsRubberBand) +from qgis.core import QgsWkbTypes +from qgis.gui import QgsMapToolEmitPoint, QgsRubberBand from PyQt5.QtCore import pyqtSignal from PyQt5.QtGui import QColor @@ -48,7 +47,9 @@ def __init__(self, canvas): self.canvas = canvas QgsMapToolEmitPoint.__init__(self, self.canvas) - self.rubberBand = QgsRubberBand(mapCanvas=self.canvas, geometryType=QgsWkbTypes.LineGeometry) + self.rubberBand = QgsRubberBand( + mapCanvas=self.canvas, geometryType=QgsWkbTypes.LineGeometry + ) self.rubberBand.setStrokeColor(QColor(DEFAULT_COLOR)) self.rubberBand.setWidth(3) diff --git a/ORStools/utils/processing.py b/ORStools/utils/processing.py index 93bf09ca..4b619097 100644 --- a/ORStools/utils/processing.py +++ b/ORStools/utils/processing.py @@ -35,7 +35,9 @@ from ORStools.common import OPTIMIZATION_MODES -def get_params_optimize(point_list: List[QgsPointXY], ors_profile: str, mode: int) -> dict: +def get_params_optimize( + point_list: List[QgsPointXY], ors_profile: str, mode: int +) -> dict: """ Build parameters for optimization endpoint @@ -55,10 +57,7 @@ def get_params_optimize(point_list: List[QgsPointXY], ors_profile: str, mode: in elif mode == OPTIMIZATION_MODES.index("Round Trip"): start = end = point_list.pop(0) - vehicle = { - "id": 0, - "profile": ors_profile - } + vehicle = {"id": 0, "profile": ors_profile} if start: vehicle.update({"start": [round(start.x(), 6), round(start.y(), 6)]}) @@ -66,12 +65,15 @@ def get_params_optimize(point_list: List[QgsPointXY], ors_profile: str, mode: in vehicle.update({"end": [round(end.x(), 6), round(end.y(), 6)]}) params = { - 'jobs': [{ - "location": [round(point.x(), 6), round(point.y(), 6)], - "id": point_list.index(point) - } for point in point_list], - 'vehicles': [vehicle], - 'options': {'g': True} + "jobs": [ + { + "location": [round(point.x(), 6), round(point.y(), 6)], + "id": point_list.index(point), + } + for point in point_list + ], + "vehicles": [vehicle], + "options": {"g": True}, } return params @@ -82,19 +84,15 @@ def read_help_file(algorithm: str, locale: str = ""): Returns the contents of a file from the help folder :rtype: str """ - extension = '_' + locale if locale else '' + extension = "_" + locale if locale else "" - i18n_file = os.path.join( - BASE_DIR, - 'help', - f"{algorithm}{extension}.help" - ) + i18n_file = os.path.join(BASE_DIR, "help", f"{algorithm}{extension}.help") - file = i18n_file if os.path.isfile(i18n_file) else os.path.join( - BASE_DIR, - 'help', - f"{algorithm}.help" + file = ( + i18n_file + if os.path.isfile(i18n_file) + else os.path.join(BASE_DIR, "help", f"{algorithm}.help") ) - with open(file, encoding='utf-8') as help_file: + with open(file, encoding="utf-8") as help_file: msg = help_file.read() return msg diff --git a/ORStools/utils/transform.py b/ORStools/utils/transform.py index c5d08ce3..cd603e02 100644 --- a/ORStools/utils/transform.py +++ b/ORStools/utils/transform.py @@ -27,10 +27,7 @@ ***************************************************************************/ """ -from qgis.core import (QgsCoordinateReferenceSystem, - QgsCoordinateTransform, - QgsProject - ) +from qgis.core import QgsCoordinateReferenceSystem, QgsCoordinateTransform, QgsProject def transformToWGS(old_crs):