Skip to content

Commit 4b92811

Browse files
authored
Merge branch 'main' into feat/load-vertices-from-layer
2 parents 77ad879 + 66dffa9 commit 4b92811

7 files changed

Lines changed: 165 additions & 88 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ RELEASING:
3939
13. Upload the package to https://plugins.qgis.org/plugins/ORStools/ (Manage > Add Version)
4040
14. Create new release in GitHub with tag version and release title of `vX.X.X`
4141
-->
42+
## Unreleased
43+
### Added
44+
- Add shortcut to apply route calculation with ctrl+return
4245

4346
## [2.1.0] - 2025-12-09
4447

ORStools/gui/ORStoolsDialog.py

Lines changed: 99 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@
3535
from qgis.PyQt.QtWidgets import QCheckBox
3636

3737
from ..utils.gui import LayerMessageBox
38-
from ..utils.router import route_as_layer
38+
from ..utils.router import route_as_layer, get_routing_parameters
39+
from ORStools.gui import directions_gui
3940

4041
try:
4142
import processing
@@ -59,15 +60,25 @@
5960
QgsAnnotation,
6061
QgsCoordinateTransform,
6162
QgsWkbTypes,
63+
QgsTask,
64+
QgsApplication,
6265
)
6366
from qgis.gui import (
6467
QgsMapCanvasAnnotationItem,
6568
QgsCollapsibleGroupBox,
6669
QgisInterface,
6770
)
6871
from qgis.PyQt.QtCore import QSizeF, QPointF, QCoreApplication
69-
from qgis.PyQt.QtGui import QTextDocument
70-
from qgis.PyQt.QtWidgets import QAction, QDialog, QApplication, QMenu, QMessageBox, QDialogButtonBox
72+
from qgis.PyQt.QtGui import QTextDocument, QKeySequence
73+
from qgis.PyQt.QtWidgets import (
74+
QAction,
75+
QDialog,
76+
QApplication,
77+
QMenu,
78+
QMessageBox,
79+
QDialogButtonBox,
80+
QShortcut,
81+
)
7182
from qgis.PyQt.QtGui import QColor
7283
from qgis.PyQt.QtWidgets import (
7384
QWidget,
@@ -256,55 +267,49 @@ def _init_gui_control(self) -> None:
256267

257268
self.dlg.show()
258269

259-
def run_gui_control(self) -> None:
260-
"""Slot function for OK button of main dialog."""
261-
if self.dlg.routing_fromline_list.count() == 0:
262-
return
270+
def handle_task_exception(self, exception: Exception) -> None:
271+
"""Handles exceptions thrown by routing task."""
272+
if isinstance(exception, exceptions.InvalidInput):
273+
QMessageBox.critical(
274+
self.dlg,
275+
self.tr("Wrong number of waypoints"),
276+
self.tr("""At least 3 or 4 waypoints are needed to perform routing optimization.
277+
Remember, the first and last location are not part of the optimization.
278+
"""),
279+
)
263280

264-
try:
265-
basepath = os.path.dirname(__file__)
266-
267-
layer_out = route_as_layer(self.dlg)
268-
269-
# style output layer
270-
qml_path = os.path.join(basepath, "linestyle.qml")
271-
layer_out.loadNamedStyle(qml_path, True)
272-
layer_out.triggerRepaint()
273-
274-
self.project.addMapLayer(layer_out)
275-
276-
# add ors svg path
277-
my_new_path = os.path.join(basepath, "img/svg")
278-
svg_paths = QgsSettings().value("svg/searchPathsForSVG") or []
279-
if my_new_path not in svg_paths:
280-
svg_paths.append(my_new_path)
281-
QgsSettings().setValue("svg/searchPathsForSVG", svg_paths)
282-
283-
# Associate annotations with map layer, so they get deleted when layer is deleted
284-
for annotation in self.dlg.annotations:
285-
# Has the potential to be pretty cool: instead of deleting, associate with mapLayer
286-
# , you can change order after optimization
287-
# Then in theory, when the layer is remove, the annotation is removed as well
288-
# Doesn't work though, the annotations are still there when project is re-opened
289-
# annotation.setMapLayer(layer_out)
290-
self.project.annotationManager().removeAnnotation(annotation)
281+
elif isinstance(exception, exceptions.EmptyLayerError):
282+
QMessageBox.warning(
283+
self.dlg,
284+
self.tr("Empty layer"),
285+
self.tr("""
286+
The specified avoid polygon(s) layer does not contain any features.
287+
Please add polygons to the layer or uncheck avoid polygons.
288+
"""),
289+
)
291290

292-
self.dlg.annotations = []
293-
self.dlg.rubber_band.reset()
291+
elif isinstance(exception, exceptions.InvalidKey):
292+
QMessageBox.critical(
293+
self.dlg,
294+
self.tr("Missing API key"),
295+
self.tr("""
296+
Did you forget to set an <b>API key</b> for openrouteservice?<br><br>
294297
295-
self.dlg._clear_listwidget()
296-
self.dlg.line_tool = maptools.LineTool(self.dlg)
298+
If you don't have an API key, please visit https://openrouteservice.org/sign-up to get one. <br><br>
299+
Then enter the API key for openrouteservice provider in Web ► ORS Tools ► Provider Settings or the
300+
settings symbol in the main ORS Tools GUI, next to the provider dropdown."""),
301+
)
297302

298-
except exceptions.ApiError as e:
303+
elif isinstance(exception, exceptions.ApiError):
299304
# Error thrown by ORStools/common/client.py, line 243, in _check_status
300305
try:
301-
parsed = json.loads(e.message)
306+
parsed = json.loads(exception.message)
302307
error_code = int(parsed["error"]["code"])
303308
except KeyError:
304-
error_code = e.status
309+
error_code = exception.status
305310

306311
if error_code == 2010:
307-
maptools.LineTool(self.dlg).radius_message_box(e)
312+
maptools.LineTool(self.dlg).radius_message_box(exception)
308313
return
309314
elif error_code == "404":
310315
self.iface.messageBar().pushMessage(
@@ -313,7 +318,56 @@ def run_gui_control(self) -> None:
313318
level=Qgis.MessageLevel.Warning,
314319
)
315320
else:
316-
raise e
321+
raise exception
322+
323+
def on_finished(self, exception, layer_out=None) -> None:
324+
"""
325+
Callback when task finishes.
326+
327+
:param exception: Exception if task failed, None otherwise
328+
:param result: The layer_out returned from the task function
329+
"""
330+
if exception is not None:
331+
self.handle_task_exception(exception)
332+
return
333+
334+
basepath = os.path.dirname(__file__)
335+
qml_path = os.path.join(basepath, "linestyle.qml")
336+
layer_out.loadNamedStyle(qml_path, True)
337+
layer_out.triggerRepaint()
338+
339+
self.project.addMapLayer(layer_out)
340+
341+
my_new_path = os.path.join(basepath, "img/svg")
342+
svg_paths = QgsSettings().value("svg/searchPathsForSVG") or []
343+
if my_new_path not in svg_paths:
344+
svg_paths.append(my_new_path)
345+
QgsSettings().setValue("svg/searchPathsForSVG", svg_paths)
346+
347+
def run_gui_control(self) -> None:
348+
"""Slot function for OK button of main dialog."""
349+
if self.dlg.routing_fromline_list.count() == 0:
350+
return
351+
352+
provider, profile, optimize = get_routing_parameters(self.dlg)
353+
directions = directions_gui.Directions(self.dlg)
354+
355+
self.task = QgsTask.fromFunction(
356+
"ORStools Routing Task",
357+
route_as_layer,
358+
provider=provider,
359+
profile=profile,
360+
optimize=optimize,
361+
directions=directions.get_directions(),
362+
on_finished=self.on_finished,
363+
)
364+
365+
self.dlg.setDisabled(True)
366+
367+
QgsApplication.taskManager().addTask(self.task)
368+
369+
self.task.taskCompleted.connect(lambda: self.dlg.setDisabled(False))
370+
self.task.taskTerminated.connect(lambda: self.dlg.setDisabled(False))
317371

318372
def tr(self, string: str) -> str:
319373
return QCoreApplication.translate(str(self.__class__.__name__), string)
@@ -396,6 +450,8 @@ def __init__(self, iface: QgisInterface, parent=None) -> None:
396450
self.pushButton_export.clicked.connect(
397451
lambda: processing.execAlgorithmDialog(f"{PLUGIN_NAME}:export_network_from_map")
398452
)
453+
shortcut = QShortcut(QKeySequence("Ctrl+Return"), self)
454+
shortcut.activated.connect(lambda: self.global_buttons.accepted.emit())
399455

400456
# Reset index of list items every time something is moved or deleted
401457
self.routing_fromline_list.model().rowsMoved.connect(self._reindex_list_items)

ORStools/gui/ORStoolsDialogUI.ui

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -972,6 +972,9 @@ p, li { white-space: pre-wrap; }
972972
</item>
973973
<item>
974974
<widget class="QDialogButtonBox" name="global_buttons">
975+
<property name="toolTip">
976+
<string>Ctrl+Enter to calculate route, Esc to Close.</string>
977+
</property>
975978
<property name="orientation">
976979
<enum>Qt::Horizontal</enum>
977980
</property>

ORStools/gui/directions_gui.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,26 @@ def __init__(self, dlg):
9393

9494
self.options = dict()
9595

96+
request_line_feature = self.get_request_line_feature()
97+
parameters = self.get_parameters()
98+
optimize_parameters = self._get_optimize_parameters()
99+
100+
self.directions = {
101+
"request_line_feature": request_line_feature,
102+
"parameters": parameters,
103+
"optimize_parameters": optimize_parameters,
104+
"options": self.options,
105+
}
106+
107+
def get_directions(self):
108+
"""
109+
Returns the directions dict for use in router.
110+
111+
:returns: directions dict
112+
:rtype: dict
113+
"""
114+
return self.directions
115+
96116
def get_request_line_feature(self):
97117
"""
98118
Extracts all coordinates for the list in GUI.

ORStools/utils/exceptions.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,18 @@ def __str__(self):
6060
return f"{self.status} ({self.message})"
6161

6262

63+
class InvalidInput(Exception):
64+
"""Signifies that the request failed because of invalid input data."""
65+
66+
pass
67+
68+
69+
class EmptyLayerError(Exception):
70+
"""Signifies that the request has been aborted due to an empty layer."""
71+
72+
pass
73+
74+
6375
class OverQueryLimit(Exception):
6476
"""Signifies that the request failed because the client exceeded its query rate limit."""
6577

ORStools/utils/maptools.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import json
3131
import math
3232

33+
from ORStools.gui import directions_gui
3334
from qgis.gui import QgsMapToolEmitPoint, QgsRubberBand
3435
from qgis.core import (
3536
QgsProject,
@@ -283,7 +284,11 @@ def create_rubber_band(self) -> None:
283284
self.dlg.rubber_band.setStrokeColor(color)
284285
self.dlg.rubber_band.setWidth(5)
285286
if self.dlg.toggle_preview.isChecked() and self.dlg.routing_fromline_list.count() > 1:
286-
route_layer = router.route_as_layer(self.dlg)
287+
provider, profile, optimize = router.get_routing_parameters(self.dlg)
288+
directions = directions_gui.Directions(self.dlg)
289+
route_layer = router.route_as_layer(
290+
None, provider, profile, optimize, directions.get_directions()
291+
)
287292
if route_layer:
288293
feature = next(route_layer.getFeatures())
289294
self.dlg.rubber_band.addGeometry(feature.geometry(), route_layer)

0 commit comments

Comments
 (0)