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 @@ +