diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml new file mode 100644 index 00000000..12961605 --- /dev/null +++ b/.github/workflows/ruff.yml @@ -0,0 +1,15 @@ +name: Ruff +on: [ push, pull_request ] +jobs: + ruff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: chartboost/ruff-action@v1 + with: + src: ORStools + args: format --check + - uses: chartboost/ruff-action@v1 + with: + src: ORStools + args: check diff --git a/CHANGELOG.md b/CHANGELOG.md index 10ec17f4..1f25e242 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,8 @@ RELEASING: ### Added - remove blue lines every time the red X button is clicked ([#120](https://github.com/GIScience/orstools-qgis-plugin/issues/120)) +- Additional parameter for the "smoothing factor" to isochrones processing algorithms ([#172](https://github.com/GIScience/orstools-qgis-plugin/issues/172)) +- Mention omission of configuration options when using traveling salesman ## [1.6.0] - 2023-07-25 @@ -50,7 +52,6 @@ RELEASING: - translation mechanism ([#183](https://github.com/GIScience/orstools-qgis-plugin/pull/183)) - german translation ([#183](https://github.com/GIScience/orstools-qgis-plugin/pull/183)) - ## [1.5.3] - 2023-03-30 ### Fixed diff --git a/ORStools/ORStoolsPlugin.py b/ORStools/ORStoolsPlugin.py index e95558de..4b653b41 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,14 @@ def __init__(self, iface): self.plugin_dir = os.path.dirname(__file__) # initialize locale - locale = QSettings().value('locale/userLocale')[0:2] - locale_path = os.path.join( - self.plugin_dir, - 'i18n', - 'orstools_{}.qm'.format(locale)) + locale = QSettings().value("locale/userLocale")[0:2] + locale_path = os.path.join(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..5dbffe60 100644 --- a/ORStools/common/__init__.py +++ b/ORStools/common/__init__.py @@ -28,25 +28,31 @@ """ 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", + "INPUT_SMOOTHING", +] diff --git a/ORStools/common/client.py b/ORStools/common/client.py index 8a0b6afb..35b62c99 100644 --- a/ORStools/common/client.py +++ b/ORStools/common/client.py @@ -57,33 +57,29 @@ 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.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 +120,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 +137,20 @@ 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,7 +159,6 @@ def request(self, self._check_status() except exceptions.OverQueryLimit as e: - # Let the instances know something happened # noinspection PyUnresolvedReferences self.overQueryLimit.emit() @@ -176,7 +167,9 @@ def request(self, 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 +177,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 +197,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 @@ -245,7 +234,7 @@ def _generate_auth_url(self, path, params): :rtype: string """ - if type(params) is dict: + if isinstance(params, dict): params = sorted(dict(**params).items()) # Only auto-add API key when using ORS. If own instance, API key must diff --git a/ORStools/common/directions_core.py b/ORStools/common/directions_core.py index 038319a6..e7496ac3 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,18 @@ 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 +74,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 +116,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 +143,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 +182,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 +226,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..39c90150 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,9 @@ 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 +87,7 @@ 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): """ @@ -119,22 +122,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 +166,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,8 +194,9 @@ 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: diff --git a/ORStools/common/networkaccessmanager.py b/ORStools/common/networkaccessmanager.py index 8ae126ae..c2dd8132 100644 --- a/ORStools/common/networkaccessmanager.py +++ b/ORStools/common/networkaccessmanager.py @@ -21,42 +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.request, urllib.error, urllib.parse +import urllib.parse from qgis.PyQt.QtCore import QUrl, QEventLoop + 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: @@ -89,6 +92,7 @@ def __delitem__(self, key): class Response(Map): pass + class NetworkAccessManager(object): """ This class mimics httplib2 by using QgsNetworkAccessManager for all @@ -142,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 @@ -150,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: @@ -177,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() @@ -192,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()): @@ -204,8 +217,8 @@ 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 ... @@ -215,21 +228,21 @@ def request(self, url, method="GET", body=None, headers=None, blocking=True): 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 @@ -268,7 +281,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 @@ -286,14 +299,18 @@ def replyFinished(self): 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() @@ -305,7 +322,7 @@ 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 @@ -351,14 +368,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 a3ae4623..3ca36468 100644 --- a/ORStools/gui/ORStoolsDialog.py +++ b/ORStools/gui/ORStoolsDialog.py @@ -31,31 +31,32 @@ 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 from .ORStoolsDialogUI import Ui_ORStoolsDialogBase -from . import resources_rc def on_config_click(parent): @@ -76,24 +77,25 @@ 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 + parent, QCoreApplication.translate("@default", "About {}").format(PLUGIN_NAME), info ) @@ -131,33 +133,24 @@ 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() - ), + QAction(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() - ) - + QAction(create_icon("icon_help.png"), self.tr("Help"), self.iface.mainWindow()), ] # Create menu @@ -177,12 +170,19 @@ def create_icon(f): self.actions[2].triggered.connect(lambda: on_about_click(parent=self.iface.mainWindow())) self.actions[3].triggered.connect(on_help_click) + # Add keyboard shortcut + self.iface.registerMainWindowAction(self.actions[0], "Ctrl+R") + def unload(self): """Called when QGIS closes or plugin is deactivated in Plugin Manager""" self.iface.webMenu().removeAction(self.menu.menuAction()) self.iface.removeWebToolBarIcon(self.actions[0]) QApplication.restoreOverrideCursor() + + # Remove action for keyboard shortcut + self.iface.unregisterMainWindowAction(self.actions[0]) + del self.dlg # @staticmethod @@ -207,17 +207,19 @@ 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) # 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() @@ -239,7 +241,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(): @@ -250,12 +252,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", @@ -264,54 +268,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) @@ -328,10 +336,7 @@ 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 @@ -367,7 +372,6 @@ def __init__(self, iface, parent=None): self._iface = iface self.project = QgsProject.instance() # invoke a QgsProject instance - self.map_crs = self._iface.mapCanvas().mapSettings().destinationCrs() # Set things around the custom map tool self.line_tool = None @@ -383,8 +387,8 @@ 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 @@ -399,25 +403,32 @@ 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.""" @@ -439,6 +450,7 @@ def _on_clear_listwidget_click(self): self.line_tool.canvas.scene().removeItem(self.line_tool.rubberBand) def _linetool_annotate_point(self, point, idx): + map_crs = self._iface.mapCanvas().mapSettings().destinationCrs() annotation = QgsTextAnnotation() c = QTextDocument() @@ -450,7 +462,7 @@ def _linetool_annotate_point(self, point, idx): annotation.setFrameSizeMm(QSizeF(7, 5)) annotation.setFrameOffsetFromReferencePointMm(QPointF(1.3, 1.3)) annotation.setMapPosition(point) - annotation.setMapPositionCrs(self.map_crs) + annotation.setMapPositionCrs(map_crs) return QgsMapCanvasAnnotationItem(annotation, self._iface.mapCanvas()).annotation() @@ -474,13 +486,16 @@ 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): """Adds an item to QgsListWidget and annotates the point in the map canvas""" + map_crs = self._iface.mapCanvas().mapSettings().destinationCrs() - transformer = transform.transformToWGS(self.map_crs) + 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}") diff --git a/ORStools/gui/ORStoolsDialogConfig.py b/ORStools/gui/ORStoolsDialogConfig.py index 46c8b023..03fc2dc3 100644 --- a/ORStools/gui/ORStoolsDialogConfig.py +++ b/ORStools/gui/ORStoolsDialogConfig.py @@ -64,14 +64,18 @@ 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() + 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 +91,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 +100,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 +121,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 +147,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 +155,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 +172,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") diff --git a/ORStools/gui/ORStoolsDialogUI.py b/ORStools/gui/ORStoolsDialogUI.py index ac5c49ae..2f4f2429 100644 --- a/ORStools/gui/ORStoolsDialogUI.py +++ b/ORStools/gui/ORStoolsDialogUI.py @@ -471,7 +471,7 @@ def retranslateUi(self, ORStoolsDialogBase): self.routing_fromline_clear.setToolTip(_translate("ORStoolsDialogBase", "

If waypoints are selected in the list, only these will be deleted. Else all waypoints will be deleted.

")) self.routing_fromline_list.setToolTip(_translate("ORStoolsDialogBase", "Select waypoints from the map!")) self.advances_group.setTitle(_translate("ORStoolsDialogBase", "Advanced Configuration")) - self.optimization_group.setToolTip(_translate("ORStoolsDialogBase", "

Enabling Traveling Salesman will erase all other advanced configuration and assume the preference to be fastest.

")) + self.optimization_group.setToolTip(_translate("ORStoolsDialogBase", "

Enabling Traveling Salesman will omit all other advanced configuration and assume the preference to be fastest.

")) self.optimization_group.setTitle(_translate("ORStoolsDialogBase", "Traveling Salesman")) self.label_4.setText(_translate("ORStoolsDialogBase", "\n" "