diff --git a/ORStools/ORStoolsDialogUI.py b/ORStools/ORStoolsDialogUI.py new file mode 100644 index 00000000..78b5f970 --- /dev/null +++ b/ORStools/ORStoolsDialogUI.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'ORStoolsDialogUI.ui' +# +# Created by: PyQt5 UI code generator 5.15.9 +# +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. + + diff --git a/ORStools/__init__.py b/ORStools/__init__.py index 5e82c330..b48c5b24 100644 --- a/ORStools/__init__.py +++ b/ORStools/__init__.py @@ -48,6 +48,7 @@ def classFactory(iface): # pylint: disable=invalid-name # Define plugin wide constants PLUGIN_NAME = "ORS Tools" DEFAULT_COLOR = "#a8b1f5" +ROUTE_COLOR = "#c62828" BASE_DIR = os.path.dirname(os.path.abspath(__file__)) RESOURCE_PREFIX = ":plugins/ORStools/img/" diff --git a/ORStools/config.yml b/ORStools/config.yml index a08cd7d1..10473043 100755 --- a/ORStools/config.yml +++ b/ORStools/config.yml @@ -3,6 +3,6 @@ providers: ORS_QUOTA: X-Ratelimit-Limit ORS_REMAINING: X-Ratelimit-Remaining base_url: https://api.openrouteservice.org - key: '' + key: 5b3ce3597851110001cf62480254341035db4503883921d377dd5a64 name: openrouteservice timeout: 60 diff --git a/ORStools/gui/ORStoolsDialog.py b/ORStools/gui/ORStoolsDialog.py index 49f6a445..6a6e6c84 100644 --- a/ORStools/gui/ORStoolsDialog.py +++ b/ORStools/gui/ORStoolsDialog.py @@ -34,8 +34,8 @@ import processing import webbrowser -from qgis._core import Qgis, QgsAnnotation -from qgis._gui import QgisInterface +from qgis._core import Qgis, QgsAnnotation, QgsCoordinateTransform, QgsWkbTypes +from qgis._gui import QgisInterface, QgsRubberBand from qgis.core import ( QgsProject, QgsVectorLayer, @@ -49,8 +49,8 @@ ) from qgis.gui import QgsMapCanvasAnnotationItem -from qgis.PyQt.QtCore import QSizeF, QPointF, QCoreApplication -from qgis.PyQt.QtGui import QIcon, QTextDocument +from qgis.PyQt.QtCore import QSizeF, QPointF, QCoreApplication, Qt +from qgis.PyQt.QtGui import QIcon, QTextDocument,QColor from qgis.PyQt.QtWidgets import ( QAction, QDialog, @@ -60,6 +60,7 @@ QDialogButtonBox, QWidget, ) +from shapely import Point from ORStools import ( RESOURCE_PREFIX, @@ -68,7 +69,7 @@ __version__, __email__, __web__, - __help__, + __help__, ROUTE_COLOR, ) from ORStools.common import ( client, @@ -77,11 +78,12 @@ PREFERENCES, ) from ORStools.gui import directions_gui -from ORStools.utils import exceptions, maptools, logger, configmanager, transform +from ORStools.utils import exceptions, maptools, logger, configmanager, transform, router from .ORStoolsDialogConfig import ORStoolsDialogConfigMain from .ORStoolsDialogUI import Ui_ORStoolsDialogBase from . import resources_rc # noqa: F401 +from ..utils.exceptions import ApiError def on_config_click(parent): @@ -440,6 +442,7 @@ def __init__(self, iface: QgisInterface, parent=None) -> None: self.line_tool = None self.last_maptool = self._iface.mapCanvas().mapTool() self.annotations = [] + self.rubber_band = None # Set up env variables for remaining quota os.environ["ORS_QUOTA"] = "None" @@ -492,6 +495,11 @@ def __init__(self, iface: QgisInterface, parent=None) -> None: self.annotation_canvas = self._iface.mapCanvas() + self.moving = None + self.moved_idxs = 0 + self.error_idxs = 0 + self.click_dist = 10 + def _save_vertices_to_layer(self) -> None: """Saves the vertices list to a temp layer""" items = [ @@ -542,10 +550,6 @@ def _on_clear_listwidget_click(self) -> None: self.routing_fromline_list.clear() self._clear_annotations() - # Remove blue lines (rubber band) - if self.line_tool: - self.line_tool.canvas.scene().removeItem(self.line_tool.rubberBand) - def _linetool_annotate_point( self, point: QgsPointXY, idx: int, crs: Optional[QgsCoordinateReferenceSystem] = None ) -> QgsAnnotation: @@ -577,21 +581,188 @@ def _clear_annotations(self) -> None: def _on_linetool_init(self) -> None: """Hides GUI dialog, inits line maptool and add items to line list box.""" - # Remove blue lines (rubber band) - if self.line_tool: - self.line_tool.canvas.scene().removeItem(self.line_tool.rubberBand) - self.hide() - self.routing_fromline_list.clear() - # Remove all annotations which were added (if any) self._clear_annotations() - + self.routing_fromline_list.clear() 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.pointPressed.connect(lambda point: self._on_movetool_map_press(point)) + self.line_tool.pointReleased.connect( + lambda point, idx: self._on_movetool_map_release(point, idx) ) - self.line_tool.doubleClicked.connect(self._on_linetool_map_doubleclick) + self.line_tool.doubleClicked.connect(self._on_line_tool_map_doubleclick) + self.line_tool.mouseMoved.connect(lambda pos: self.change_cursor_on_hover(pos)) + + def change_cursor_on_hover(self, pos): + idx = self.check_annotation_hover(pos) + if idx: + QApplication.setOverrideCursor(Qt.OpenHandCursor) + else: + if not self.moving: + QApplication.restoreOverrideCursor() + + def check_annotation_hover(self, pos): + click = Point(pos.x(), pos.y()) + dists = {} + for i, anno in enumerate(self.annotations): + x, y = anno.mapPosition() + mapcanvas = self._iface.mapCanvas() + point = mapcanvas.getCoordinateTransform().transform(x, y) # die ist es + p = Point(point.x(), point.y()) + dist = click.distance(p) + if dist > 0: + dists[dist] = anno + if dists and min(dists) < self.click_dist: + idx = dists[min(dists)] + return idx + + def _on_movetool_map_press(self, pos): + idx = self.check_annotation_hover(pos) + if idx: + self.line_tool.mouseMoved.disconnect() + QApplication.setOverrideCursor(Qt.ClosedHandCursor) + if self.rubber_band: + self.rubber_band.reset() + self.move_i = self.annotations.index(idx) + self.project.annotationManager().removeAnnotation(self.annotations.pop(self.move_i)) + self.moving = True + + def _on_movetool_map_release(self, point, idx): + if self.moving: + try: + self.moving = False + QApplication.restoreOverrideCursor() + crs = self._iface.mapCanvas().mapSettings().destinationCrs() + + annotation = self._linetool_annotate_point(point, self.move_i, crs=crs) + self.annotations.insert(self.move_i, annotation) + self.project.annotationManager().addAnnotation(annotation) + + transformer = transform.transformToWGS(crs) + point_wgs = transformer.transform(point) + + items = [ + self.routing_fromline_list.item(x).text() + for x in range(self.routing_fromline_list.count()) + ] + backup = items.copy() + items[ + self.move_i + ] = f"Point {self.move_i}: {point_wgs.x():.6f}, {point_wgs.y():.6f}" + self.moved_idxs += 1 + + self.routing_fromline_list.clear() + for i, x in enumerate(items): + coords = x.split(":")[1] + item = f"Point {i}:{coords}" + self.routing_fromline_list.addItem(item) + self.create_rubber_band() + self.line_tool.mouseMoved.connect(lambda pos: self.change_cursor_on_hover(pos)) + + except ApiError as e: + json_start_index = e.message.find("{") + json_end_index = e.message.rfind("}") + 1 + json_str = e.message[json_start_index:json_end_index] + error_dict = json.loads(json_str) + error_code = error_dict["error"]["code"] + if error_code == 2010: + self.error_idxs += 1 + self.moving = False + self.routing_fromline_list.clear() + for i, x in enumerate(backup): + coords = x.split(":")[1] + item = f"Point {i}:{coords}" + self.routing_fromline_list.addItem(item) + self._reindex_list_items() + QMessageBox.warning( + self, + "Please use a different point", + """Could not find routable point within a radius of 350.0 meters of specified coordinate. Use a different point closer to a road.""", + ) + self.line_tool.mouseMoved.connect(lambda pos: self.change_cursor_on_hover(pos)) + self.moved_idxs -= 1 + else: + raise e + + else: + try: + idx -= self.moved_idxs + idx -= self.error_idxs + self.create_vertex(point, idx) + + if self.routing_fromline_list.count() > 1: + self.create_rubber_band() + self.moving = False + except ApiError as e: + json_start_index = e.message.find("{") + json_end_index = e.message.rfind("}") + 1 + json_str = e.message[json_start_index:json_end_index] + error_dict = json.loads(json_str) + error_code = error_dict["error"]["code"] + if error_code == 2010: + self.error_idxs += 1 + num = len(self.routing_fromline_list) - 1 + + if num < 2: + self.routing_fromline_list.clear() + for annotation in self.annotations: + self.project.annotationManager().removeAnnotation(annotation) + self.annotations = [] + else: + self.routing_fromline_list.takeItem(num) + self.create_rubber_band() + QMessageBox.warning( + self, + "Please use a different point", + """Could not find routable point within a radius of 350.0 meters of specified coordinate. Use a different point closer to a road.""", + ) + else: + raise e + + def create_rubber_band(self): + if self.rubber_band: + self.rubber_band.reset() + self.rubber_band = QgsRubberBand(self._iface.mapCanvas(), QgsWkbTypes.LineGeometry) + color = QColor(ROUTE_COLOR) + color.setAlpha(100) + self.rubber_band.setStrokeColor(color) + self.rubber_band.setWidth(5) + if self.toggle_preview.isChecked(): + route_layer = router.route_as_layer(self) + if route_layer: + feature = next(route_layer.getFeatures()) + self.rubber_band.addGeometry(feature.geometry(), route_layer) + self.rubber_band.show() + else: + dest_crs = self._iface.mapCanvas().mapSettings().destinationCrs() + original_crs = QgsCoordinateReferenceSystem("EPSG:4326") + transform = QgsCoordinateTransform(original_crs, dest_crs, QgsProject.instance()) + items = [ + self.routing_fromline_list.item(x).text() + for x in range(self.routing_fromline_list.count()) + ] + split = [x.split(":")[1] for x in items] + coords = [tuple(map(float, coord.split(", "))) for coord in split] + points_xy = [QgsPointXY(x, y) for x, y in coords] + reprojected_point = [transform.transform(point) for point in points_xy] + for point in reprojected_point: + if point == reprojected_point[-1]: + self.rubber_band.addPoint(point, True) + self.rubber_band.addPoint(point, False) + self.rubber_band.show() + + def create_vertex(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(map_crs) + point_wgs = transformer.transform(point) + self.routing_fromline_list.addItem(f"Point {idx}: {point_wgs.x():.6f}, {point_wgs.y():.6f}") + + crs = self._iface.mapCanvas().mapSettings().destinationCrs() + annotation = self._linetool_annotate_point(point, idx, crs) + self.annotations.append(annotation) + self.project.annotationManager().addAnnotation(annotation) def _on_linetool_map_click(self, point: QgsPointXY, idx: int) -> None: """Adds an item to QgsListWidget and annotates the point in the map canvas""" @@ -621,15 +792,21 @@ def _reindex_list_items(self) -> None: self.routing_fromline_list.addItem(item) annotation = self._linetool_annotate_point(point, idx, crs) + self.annotations.append(annotation) self.project.annotationManager().addAnnotation(annotation) + self.create_rubber_band() - def _on_linetool_map_doubleclick(self) -> None: + def _on_line_tool_map_doubleclick(self): """ - Populate line list widget with coordinates, end line drawing and show dialog again. + Populate line list widget with coordinates, end point moving and show dialog again. """ - - self.line_tool.pointDrawn.disconnect() + self.moved_idxs = 0 + self.error_idxs = 0 + self._reindex_list_items() + self.line_tool.mouseMoved.disconnect() + self.line_tool.pointPressed.disconnect() self.line_tool.doubleClicked.disconnect() + self.line_tool.pointReleased.disconnect() QApplication.restoreOverrideCursor() self._iface.mapCanvas().setMapTool(self.last_maptool) self.show() diff --git a/ORStools/gui/ORStoolsDialogUI.py b/ORStools/gui/ORStoolsDialogUI.py index cd01815a..2f36f87f 100644 --- a/ORStools/gui/ORStoolsDialogUI.py +++ b/ORStools/gui/ORStoolsDialogUI.py @@ -2,7 +2,7 @@ # Form implementation generated from reading ui file 'ORStoolsDialogUI.ui' # -# Created by: PyQt5 UI code generator 5.15.10 +# Created by: PyQt5 UI code generator 5.15.9 # # WARNING: Any manual changes made to this file will be lost when pyuic5 is # run again. Do not edit this file unless you know what you are doing. @@ -209,6 +209,9 @@ def setupUi(self, ORStoolsDialogBase): self.save_vertices.setIcon(icon4) self.save_vertices.setObjectName("save_vertices") self.gridLayout.addWidget(self.save_vertices, 2, 0, 1, 1) + self.toggle_preview = QtWidgets.QCheckBox(self.widget) + self.toggle_preview.setObjectName("toggle_preview") + self.gridLayout.addWidget(self.toggle_preview, 3, 0, 1, 1) self.verticalLayout_7.addWidget(self.widget) self.advances_group = QgsCollapsibleGroupBox(self.qwidget) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) @@ -219,8 +222,8 @@ def setupUi(self, ORStoolsDialogBase): self.advances_group.setMaximumSize(QtCore.QSize(16777215, 20)) self.advances_group.setCheckable(False) self.advances_group.setChecked(False) - self.advances_group.setCollapsed(True) - self.advances_group.setSaveCollapsedState(False) + self.advances_group.setProperty("collapsed", True) + self.advances_group.setProperty("saveCollapsedState", False) self.advances_group.setObjectName("advances_group") self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.advances_group) self.verticalLayout_3.setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint) @@ -235,8 +238,8 @@ def setupUi(self, ORStoolsDialogBase): self.optimization_group.setMaximumSize(QtCore.QSize(16777215, 20)) self.optimization_group.setCheckable(True) self.optimization_group.setChecked(False) - self.optimization_group.setCollapsed(True) - self.optimization_group.setSaveCollapsedState(False) + self.optimization_group.setProperty("collapsed", True) + self.optimization_group.setProperty("saveCollapsedState", False) self.optimization_group.setObjectName("optimization_group") self.gridLayout_2 = QtWidgets.QGridLayout(self.optimization_group) self.gridLayout_2.setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint) @@ -280,8 +283,8 @@ def setupUi(self, ORStoolsDialogBase): self.routing_avoid_tags_group.setSizePolicy(sizePolicy) self.routing_avoid_tags_group.setCheckable(True) self.routing_avoid_tags_group.setChecked(False) - self.routing_avoid_tags_group.setCollapsed(True) - self.routing_avoid_tags_group.setSaveCollapsedState(False) + self.routing_avoid_tags_group.setProperty("collapsed", True) + self.routing_avoid_tags_group.setProperty("saveCollapsedState", False) self.routing_avoid_tags_group.setObjectName("routing_avoid_tags_group") self.gridLayout_4 = QtWidgets.QGridLayout(self.routing_avoid_tags_group) self.gridLayout_4.setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint) @@ -311,8 +314,8 @@ def setupUi(self, ORStoolsDialogBase): self.routing_avoid_countries_group.setMaximumSize(QtCore.QSize(16777215, 20)) self.routing_avoid_countries_group.setCheckable(True) self.routing_avoid_countries_group.setChecked(False) - self.routing_avoid_countries_group.setCollapsed(True) - self.routing_avoid_countries_group.setSaveCollapsedState(False) + self.routing_avoid_countries_group.setProperty("collapsed", True) + self.routing_avoid_countries_group.setProperty("saveCollapsedState", False) self.routing_avoid_countries_group.setObjectName("routing_avoid_countries_group") self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.routing_avoid_countries_group) self.verticalLayout_4.setObjectName("verticalLayout_4") @@ -330,13 +333,13 @@ def setupUi(self, ORStoolsDialogBase): self.avoidpolygon_group.setMaximumSize(QtCore.QSize(16777215, 20)) self.avoidpolygon_group.setCheckable(True) self.avoidpolygon_group.setChecked(False) - self.avoidpolygon_group.setCollapsed(True) - self.avoidpolygon_group.setSaveCollapsedState(False) + self.avoidpolygon_group.setProperty("collapsed", True) + self.avoidpolygon_group.setProperty("saveCollapsedState", False) self.avoidpolygon_group.setObjectName("avoidpolygon_group") self.verticalLayout_6 = QtWidgets.QVBoxLayout(self.avoidpolygon_group) self.verticalLayout_6.setObjectName("verticalLayout_6") self.avoidpolygon_dropdown = QgsMapLayerComboBox(self.avoidpolygon_group) - self.avoidpolygon_dropdown.setShowCrs(False) + self.avoidpolygon_dropdown.setProperty("showCrs", False) self.avoidpolygon_dropdown.setObjectName("avoidpolygon_dropdown") self.verticalLayout_6.addWidget(self.avoidpolygon_dropdown) self.verticalLayout_3.addWidget(self.avoidpolygon_group) @@ -407,8 +410,8 @@ def setupUi(self, ORStoolsDialogBase): self.ors_log_group.setMinimumSize(QtCore.QSize(0, 0)) self.ors_log_group.setMaximumSize(QtCore.QSize(16777215, 20)) self.ors_log_group.setFlat(True) - self.ors_log_group.setCollapsed(True) - self.ors_log_group.setSaveCollapsedState(False) + self.ors_log_group.setProperty("collapsed", True) + self.ors_log_group.setProperty("saveCollapsedState", False) self.ors_log_group.setObjectName("ors_log_group") self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.ors_log_group) self.verticalLayout_2.setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint) @@ -488,6 +491,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.save_vertices.setToolTip(_translate("ORStoolsDialogBase", "

Save points in list to layer. Use the processing algorithms (batch jobs) to work with points from layers.

")) + self.toggle_preview.setText(_translate("ORStoolsDialogBase", "LivePreview")) self.advances_group.setTitle(_translate("ORStoolsDialogBase", "Advanced Configuration")) 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")) diff --git a/ORStools/gui/ORStoolsDialogUI.ui b/ORStools/gui/ORStoolsDialogUI.ui index 68aa6722..472a6c38 100644 --- a/ORStools/gui/ORStoolsDialogUI.ui +++ b/ORStools/gui/ORStoolsDialogUI.ui @@ -361,6 +361,13 @@ + + + + LivePreview + + + @@ -387,10 +394,10 @@ false - + true - + false @@ -429,10 +436,10 @@ false - + true - + false @@ -551,10 +558,10 @@ p, li { white-space: pre-wrap; } false - + true - + false @@ -625,10 +632,10 @@ p, li { white-space: pre-wrap; } false - + true - + false @@ -637,7 +644,7 @@ p, li { white-space: pre-wrap; } <html><head/><body><p>Avoid countries based on ISO 3166 Alpha-2 or Alpha-3 codes.</p></body></html> - + @@ -671,10 +678,10 @@ p, li { white-space: pre-wrap; } false - + true - + false @@ -683,7 +690,7 @@ p, li { white-space: pre-wrap; } <html><head/><body><p>Avoid areas by specifying a (Multi-)Polygon layer. </p><p>Does <span style=" font-weight:600;">not work</span> for memory (scratch) Polygon layers!</p><p><span style=" font-weight:600;">Note</span>, only the first feature of the layer will be respected.</p></body></html> - + false @@ -793,7 +800,7 @@ p, li { white-space: pre-wrap; } Qt::Vertical - + 20 40 @@ -831,10 +838,10 @@ p, li { white-space: pre-wrap; } true - + true - + false diff --git a/ORStools/utils/maptools.py b/ORStools/utils/maptools.py index 9088dddd..7244a383 100644 --- a/ORStools/utils/maptools.py +++ b/ORStools/utils/maptools.py @@ -27,13 +27,9 @@ ***************************************************************************/ """ -from qgis.core import QgsWkbTypes -from qgis.gui import QgsMapToolEmitPoint, QgsRubberBand +from qgis.gui import QgsMapToolEmitPoint -from qgis.PyQt.QtCore import pyqtSignal -from qgis.PyQt.QtGui import QColor - -from ORStools import DEFAULT_COLOR +from PyQt5.QtCore import pyqtSignal class LineTool(QgsMapToolEmitPoint): @@ -47,12 +43,6 @@ def __init__(self, canvas): self.canvas = canvas QgsMapToolEmitPoint.__init__(self, self.canvas) - self.rubberBand = QgsRubberBand( - mapCanvas=self.canvas, geometryType=QgsWkbTypes.GeometryType.LineGeometry - ) - self.rubberBand.setStrokeColor(QColor(DEFAULT_COLOR)) - self.rubberBand.setWidth(3) - self.crsSrc = self.canvas.mapSettings().destinationCrs() self.previous_point = None self.points = [] @@ -62,9 +52,9 @@ def reset(self): """reset rubber band and captured points.""" self.points = [] - self.rubberBand.reset(geometryType=QgsWkbTypes.GeometryType.LineGeometry) + # self.rubberBand.reset(geometryType=QgsWkbTypes.LineGeometry) - pointDrawn = pyqtSignal(["QgsPointXY", "int"]) + pointReleased = pyqtSignal(["QgsPointXY", "int"]) def canvasReleaseEvent(self, e): """Add marker to canvas and shows line.""" @@ -72,27 +62,28 @@ def canvasReleaseEvent(self, e): self.points.append(new_point) # noinspection PyUnresolvedReferences - self.pointDrawn.emit(new_point, self.points.index(new_point)) - self.showLine() - - def showLine(self): - """Builds rubber band from all points and adds it to the map canvas.""" - self.rubberBand.reset(geometryType=QgsWkbTypes.GeometryType.LineGeometry) - for point in self.points: - if point == self.points[-1]: - self.rubberBand.addPoint(point, True) - self.rubberBand.addPoint(point, False) - self.rubberBand.show() - - doubleClicked = pyqtSignal() + self.pointReleased.emit(new_point, self.points.index(new_point)) # noinspection PyUnusedLocal def canvasDoubleClickEvent(self, e): """Ends line drawing and deletes rubber band and markers from map canvas.""" # noinspection PyUnresolvedReferences self.doubleClicked.emit() - self.canvas.scene().removeItem(self.rubberBand) + # self.canvas.scene().removeItem(self.rubberBand) + + doubleClicked = pyqtSignal() def deactivate(self): super(LineTool, self).deactivate() self.deactivated.emit() + + pointPressed = pyqtSignal(["QPoint"]) + + def canvasPressEvent(self, e): + # Make tooltip look like marker + self.pointPressed.emit(e.pos()) + + mouseMoved = pyqtSignal(["QPoint"]) + + def canvasMoveEvent(self, e): + self.mouseMoved.emit(e.pos()) \ No newline at end of file diff --git a/ORStools/utils/router.py b/ORStools/utils/router.py new file mode 100644 index 00000000..e7195ad0 --- /dev/null +++ b/ORStools/utils/router.py @@ -0,0 +1,118 @@ +import json + + +from qgis.core import ( + QgsVectorLayer, +) + +from PyQt5.QtWidgets import QMessageBox + +from ORStools.common import ( + client, + directions_core, +) +from ORStools.gui import directions_gui +from ORStools.utils import exceptions, logger, configmanager + + +def route_as_layer(dlg): + layer_out = QgsVectorLayer("LineString?crs=EPSG:4326", "Route_ORS", "memory") + layer_out.dataProvider().addAttributes(directions_core.get_fields()) + layer_out.updateFields() + + provider_id = dlg.provider_combo.currentIndex() + provider = configmanager.read_config()["providers"][provider_id] + + # if no API key is present, when ORS is selected, throw an error message + if not provider["key"] and provider["base_url"].startswith("https://api.openrouteservice.org"): + QMessageBox.critical( + dlg, + "Missing API key", + """ + Did you forget to set an API key for openrouteservice?

+ + 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.""", + ) + return + + agent = "QGIS_ORStoolsDialog" + clnt = client.Client(provider, agent) + clnt_msg = "" + + directions = directions_gui.Directions(dlg) + params = None + try: + params = directions.get_parameters() + if dlg.optimization_group.isChecked(): + if len(params["jobs"]) <= 1: # Start/end locations don't count as job + QMessageBox.critical( + dlg, + "Wrong number of waypoints", + """At least 3 or 4 waypoints are needed to perform routing optimization. + +Remember, the first and last location are not part of the optimization. + """, + ) + return + response = clnt.request("/optimization", {}, post_json=params) + feat = directions_core.get_output_features_optimization( + response, params["vehicles"][0]["profile"] + ) + else: + params["coordinates"] = directions.get_request_line_feature() + profile = 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"] == {} + ): + QMessageBox.warning( + 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) + dlg.debug_text.setText(msg) + return + response = clnt.request("/v2/directions/" + profile + "/geojson", {}, post_json=params) + feat = directions_core.get_output_feature_directions( + response, profile, params["preference"], directions.options + ) + + layer_out.dataProvider().addFeature(feat) + + layer_out.updateExtents() + + return layer_out + + # Update quota; handled in client module after successful request + # if provider.get('ENV_VARS'): + # self.dlg.quota_text.setText(self.get_quota(provider) + ' calls') + except exceptions.Timeout: + msg = "The connection has timed out!" + logger.log(msg, 2) + dlg.debug_text.setText(msg) + return + + 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 + + except Exception as e: + logger.log(f"{e.__class__.__name__}: {str(e)}", 2) + clnt_msg += f"{e.__class__.__name__}: {str(e)}
" + raise + + finally: + # Set URL in debug window + if params: + clnt_msg += f'{clnt.url}
Parameters:
{json.dumps(params, indent=2)}' + dlg.debug_text.setHtml(clnt_msg) \ No newline at end of file diff --git a/ORStoolsDialogUI.py b/ORStoolsDialogUI.py new file mode 100644 index 00000000..78b5f970 --- /dev/null +++ b/ORStoolsDialogUI.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'ORStoolsDialogUI.ui' +# +# Created by: PyQt5 UI code generator 5.15.9 +# +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. + +