fUP=GmAWye=19sHW%|Qs@043aAmdoni?XkND
zLdaZ8tuu-v%GL_nI)pNQRKYZ-Oa)4Tfr4YHg7BZCqTKEZ7J6Z2?I-nx@0x|e9t3q(s-5Oo}&lFJQ5iw{urcPEJM>@e-(
zt<=479?{LasQ32AVb9Xwg$Pk?8Ks8{LPnmK+Y
zKs33+GUMwlM4fk9{Ace+e4nLOCwhy=vTF5fh}&k_a30p@H*HP5W#^a+cynMkA@7`J
zZ{`Y7Xwcer2M90vT(^02`k7V!bste{jrG{yClmF}w4S`M2Axcpw)$@CzicXzz18}y
zfDRgWl&s~B67_7hwY=(tVqe%+RkWkPqqc{;P7?imnQh_#;+pYzI+ntHb}vJ4Cmga;)yX0EHel?UZjE>Do_;s-Jeq=d8HGOAdA55+t8u
z+Om13t$4-pd_LAn_;b7;Jwmjg)oFcwD
z3VYr8m6v`CN4fhRZ$FXeYqxk7jC&f~+b-UPeZsxF2?@P@?lTsFsPs+GO?$r}a;@}i
zdwePsc;B<ZO@m!w*8M7wp{6%w`XBk&Xu_rl>p#m*>j=u=;%vt^?RRyVA31Bh#`#m
zy<>L(c;0|_Y<~pO-sc@ZggDm;)0U2SkM6;FORe{_*f+S6bEciz?rVPlS6yK^+
zl63_rl27|a7%1TA^!-QNr9g;PFQ>e;z>1LmR3@ZQY6E-4i8Il>~$N)ilt$k@Y;)Y)moODJtqKDI~hKmrD>Bu${1{9?diH%
z^M|n)z({~E2y_{1j)vgnXd}ELo_pa>0sS3yt#kBR@jIhaN2tlX0O07W@8|X-
zH)~X6wcT|ywH=|kb7bR6i(^tx>(Pz?Up*p4V>kp;X6><1eg3CKf4}Z>)YUTu)XrQT
zm1Y{nKSum~h%8Jv5sCFLWd|flRKi>$!e(V@gD}J^Y&@Cd6@^tLHXLV%wV|LtA7B-A
zlgPIQl9DLNA$Fu;G!T)pd`6aXDzEVJ1|i9Zxp;)t9YQ9>uFGYTsvu?JqM!=A5)3Eg
zi18-*jLKsTXqjKv8W>j9tP+z`)g6h!e}DV^psfk;@$r&FvVCtCv{
zE-U6zLPp`hUDc^`LOR8XQc4;$0S22J8iLtOD!@f_KHrt)uGVeKsl$2)-MkR(8DLY<
zbfQ}i>P*K4F=TXX(9*Z66p9%=_Fij}4UaUAMgkFhi})+}#C3@yO^^nk;%Pshm99J1
z{x*O5T3Io;*RM|rO1;@pAcF3PbBSP5N^{YkrAdL$BzaEyk8YEjR4P
zs@reapLV}-!`?rsQOlQQw3objt=8SFCCUSZH+-)!g
Date: Fri, 17 May 2024 18:01:24 +0200
Subject: [PATCH 13/26] fix: use if-else instead of match-case
Structural Pattern Matching was only introduced in Python 3.10.
To maintain compatibility with older QGIS versions, result to if-else.
---
CHANGELOG.md | 5 ++
ORStools/utils/convert.py | 103 +++++++++++++++++++-------------------
2 files changed, 56 insertions(+), 52 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c6dc3d46..5bfdaace 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -40,6 +40,11 @@ RELEASING:
14. Create new release in GitHub with tag version and release title of `vX.X.X`
-->
+## Unreleased
+
+### Fixed
+- use if-else instead of structural pattern matching
+
## [1.8.0] - 2024-05-17
### Added
diff --git a/ORStools/utils/convert.py b/ORStools/utils/convert.py
index eef4d8ab..90b8dd30 100644
--- a/ORStools/utils/convert.py
+++ b/ORStools/utils/convert.py
@@ -139,55 +139,54 @@ def decode_extrainfo(extra_info: str, key: int) -> str | int:
">=16% incline",
]
- match extra_info:
- case "waytypes":
- try:
- return waytypes[key]
- except IndexError:
- return "Unknown"
- case "surface":
- try:
- return surfaces[key]
- except IndexError:
- return "Unknown"
- case "waycategory":
- binary = list(bin(key))[2:]
- padding = ["0"] * (len(waycategory) - len(binary))
- padded_binary = padding + binary
- category = ""
-
- for set_bit, value in zip(padded_binary, waycategory):
- if set_bit == "1":
- category += value
-
- if category == "":
- return "No category"
-
- return category
- case "roadaccessrestrictions":
- binary = list(bin(key))[2:]
- padding = ["0"] * (len(restrictions) - len(binary))
- padded_binary = padding + binary
- restriction = ""
-
- for set_bit, value in zip(padded_binary, restrictions):
- if set_bit == "1":
- restriction += value
- restriction += " "
-
- if restriction == "":
- return "None"
-
- return restriction
- case "steepness":
- # We get values from -5 to 5 here, but our decoded array is 11 values long.
- key += 5
- try:
- return steepness[key]
- except IndexError:
- return "No steepness available"
- case "traildifficulty":
- # TODO: we need to differentiate the profile here…
- return key
- case _:
- return key
+ if extra_info == "waytypes":
+ try:
+ return waytypes[key]
+ except IndexError:
+ return "Unknown"
+ elif extra_info == "surface":
+ try:
+ return surfaces[key]
+ except IndexError:
+ return "Unknown"
+ elif extra_info == "waycategory":
+ binary = list(bin(key))[2:]
+ padding = ["0"] * (len(waycategory) - len(binary))
+ padded_binary = padding + binary
+ category = ""
+
+ for set_bit, value in zip(padded_binary, waycategory):
+ if set_bit == "1":
+ category += value
+
+ if category == "":
+ return "No category"
+
+ return category
+ elif extra_info == "roadaccessrestrictions":
+ binary = list(bin(key))[2:]
+ padding = ["0"] * (len(restrictions) - len(binary))
+ padded_binary = padding + binary
+ restriction = ""
+
+ for set_bit, value in zip(padded_binary, restrictions):
+ if set_bit == "1":
+ restriction += value
+ restriction += " "
+
+ if restriction == "":
+ return "None"
+
+ return restriction
+ elif extra_info == "steepness":
+ # We get values from -5 to 5 here, but our decoded array is 11 values long.
+ key += 5
+ try:
+ return steepness[key]
+ except IndexError:
+ return "No steepness available"
+ elif extra_info == "traildifficulty":
+ # TODO: we need to differentiate the profile here…
+ return key
+ else:
+ return key
From f5631f48daaad56273aa5d368706353ed460851f Mon Sep 17 00:00:00 2001
From: koebi
Date: Fri, 17 May 2024 18:15:03 +0200
Subject: [PATCH 14/26] chore: release v1.8.1
Bugfix release to maintain compatibility with python<3.10
---
CHANGELOG.md | 5 +++--
ORStools/metadata.txt | 48 +++++++++++++++++++++++--------------------
2 files changed, 29 insertions(+), 24 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5bfdaace..56afe6f6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -40,7 +40,7 @@ RELEASING:
14. Create new release in GitHub with tag version and release title of `vX.X.X`
-->
-## Unreleased
+## [1.8.1] - 2024-05-17
### Fixed
- use if-else instead of structural pattern matching
@@ -246,7 +246,8 @@ RELEASING:
- first working version of ORS Tools, after replacing OSM Tools plugin
-[unreleased]: https://github.com/GIScience/orstools-qgis-plugin/compare/v1.8.0...HEAD
+[unreleased]: https://github.com/GIScience/orstools-qgis-plugin/compare/v1.8.1...HEAD
+[1.8.1]: https://github.com/GIScience/orstools-qgis-plugin/compare/v1.8.0...v1.8.1
[1.8.0]: https://github.com/GIScience/orstools-qgis-plugin/compare/v1.7.1...v1.8.0
[1.7.1]: https://github.com/GIScience/orstools-qgis-plugin/compare/v1.7.0...v1.7.1
[1.7.0]: https://github.com/GIScience/orstools-qgis-plugin/compare/v1.6.0...v1.7.0
diff --git a/ORStools/metadata.txt b/ORStools/metadata.txt
index dcc916fb..70da5da8 100644
--- a/ORStools/metadata.txt
+++ b/ORStools/metadata.txt
@@ -3,33 +3,37 @@ name=ORS Tools
qgisMinimumVersion=3.4.8
description=openrouteservice routing, isochrones and matrix calculations for QGIS
-version=1.8.0
+version=1.8.1
author=HeiGIT gGmbH
email=support@openrouteservice.heigit.org
about=ORS Tools provides access to most of the functions of openrouteservice.org, based on OpenStreetMap. The tool set includes routing, isochrones and matrix calculations, either interactive in the map canvas or from point files within the processing framework. Extensive attributes are set for output files, incl. duration, length and start/end locations.
-changelog=2024/05/17 v1.8.0
-### Added
-- Add support for decimal ranges with isochrones
-- Add hint for joining with `Layer ID Field`
-- Add option to export order of optimization route points
-- Add `extra_info` parameter to directions processing algorithms
-
-### Changed
-- Rename `Ok` button in configuration window to `Save` for clarification
-- Replace PyQt5 imports with qgis.PyQt imports to prepare for Qt6-QGis builds
-
-### Fixed
-- QGis crashes when selecting more than two vertices for deletion
-- Vertices on canvas not depicted fully with n having more than one digit in length
-- Replace qt QSettings with QgsSettings for centralized configuration management
-- Point Annotations stay after saving project and not deleting them manually
-- Issue with MultiPoint-layers and optimization
-- Improved type hints
-
-Complete changelog of all versions: https://github.com/GIScience/orstools-qgis-plugin/blob/main/CHANGELOG.md
-Wiki: https://github.com/GIScience/orstools-qgis-plugin/wiki
+changelog=2024/05/17 v1.8.1
+ Fixed
+ - remove structural pattern matching for compatibility with python<3.10
+
+ 2024/05/17 v1.8.0
+ Added
+ - Add support for decimal ranges with isochrones
+ - Add hint for joining with `Layer ID Field`
+ - Add option to export order of optimization route points
+ - Add `extra_info` parameter to directions processing algorithms
+
+ Changed
+ - Rename `Ok` button in configuration window to `Save` for clarification
+ - Replace PyQt5 imports with qgis.PyQt imports to prepare for Qt6-QGis builds
+
+ Fixed
+ - QGis crashes when selecting more than two vertices for deletion
+ - Vertices on canvas not depicted fully with n having more than one digit in length
+ - Replace qt QSettings with QgsSettings for centralized configuration management
+ - Point Annotations stay after saving project and not deleting them manually
+ - Issue with MultiPoint-layers and optimization
+ - Improved type hints
+
+ Complete changelog of all versions: https://github.com/GIScience/orstools-qgis-plugin/blob/main/CHANGELOG.md
+ Wiki: https://github.com/GIScience/orstools-qgis-plugin/wiki
tracker=https://github.com/GIScience/orstools-qgis-plugin/issues
repository=https://github.com/GIScience/orstools-qgis-plugin.git
From eece0f9ce78515c710af2a4dbd9a34962abbb66b Mon Sep 17 00:00:00 2001
From: Merydian
Date: Fri, 17 May 2024 19:37:16 +0200
Subject: [PATCH 15/26] Use python 3.9 compatible type hint
---
ORStools/utils/convert.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/ORStools/utils/convert.py b/ORStools/utils/convert.py
index 90b8dd30..7c06ea06 100644
--- a/ORStools/utils/convert.py
+++ b/ORStools/utils/convert.py
@@ -27,6 +27,8 @@
***************************************************************************/
"""
+from typing import Union
+
def decode_polyline(polyline: str, is3d: bool = False) -> list:
"""Decodes a Polyline string into a GeoJSON geometry.
@@ -89,7 +91,7 @@ def decode_polyline(polyline: str, is3d: bool = False) -> list:
return points
-def decode_extrainfo(extra_info: str, key: int) -> str | int:
+def decode_extrainfo(extra_info: str, key: int) -> Union[int, str]:
waytypes = [
"Unknown",
"state Road",
From 2f925e7fab6816d544d534be879cb04edd5fb247 Mon Sep 17 00:00:00 2001
From: koebi
Date: Mon, 20 May 2024 09:45:13 +0200
Subject: [PATCH 16/26] chore: release v1.8.2
---
CHANGELOG.md | 8 +++++++-
ORStools/metadata.txt | 8 ++++++--
2 files changed, 13 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 56afe6f6..4090769d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -40,6 +40,11 @@ RELEASING:
14. Create new release in GitHub with tag version and release title of `vX.X.X`
-->
+## [1.8.2] - 2024-05-20
+
+### Fixed
+- use python 3.9-compatible type hint
+
## [1.8.1] - 2024-05-17
### Fixed
@@ -246,7 +251,8 @@ RELEASING:
- first working version of ORS Tools, after replacing OSM Tools plugin
-[unreleased]: https://github.com/GIScience/orstools-qgis-plugin/compare/v1.8.1...HEAD
+[unreleased]: https://github.com/GIScience/orstools-qgis-plugin/compare/v1.8.2...HEAD
+[1.8.2]: https://github.com/GIScience/orstools-qgis-plugin/compare/v1.8.1...v1.8.2
[1.8.1]: https://github.com/GIScience/orstools-qgis-plugin/compare/v1.8.0...v1.8.1
[1.8.0]: https://github.com/GIScience/orstools-qgis-plugin/compare/v1.7.1...v1.8.0
[1.7.1]: https://github.com/GIScience/orstools-qgis-plugin/compare/v1.7.0...v1.7.1
diff --git a/ORStools/metadata.txt b/ORStools/metadata.txt
index 70da5da8..9a0e0647 100644
--- a/ORStools/metadata.txt
+++ b/ORStools/metadata.txt
@@ -3,13 +3,17 @@ name=ORS Tools
qgisMinimumVersion=3.4.8
description=openrouteservice routing, isochrones and matrix calculations for QGIS
-version=1.8.1
+version=1.8.2
author=HeiGIT gGmbH
email=support@openrouteservice.heigit.org
about=ORS Tools provides access to most of the functions of openrouteservice.org, based on OpenStreetMap. The tool set includes routing, isochrones and matrix calculations, either interactive in the map canvas or from point files within the processing framework. Extensive attributes are set for output files, incl. duration, length and start/end locations.
-changelog=2024/05/17 v1.8.1
+changelog=2024/05/20 v1.8.2
+ Fixed
+ - use python 3.9-compatible type hint
+
+ 2024/05/17 v1.8.1
Fixed
- remove structural pattern matching for compatibility with python<3.10
From 85d4f76b910e8dee4ec7293ebaf3cab7e452e005 Mon Sep 17 00:00:00 2001
From: Till Frankenbach <81414045+merydian@users.noreply.github.com>
Date: Wed, 29 May 2024 07:45:58 -0400
Subject: [PATCH 17/26] fix: make extra_info work with request from two
point/polyline layers
Co-authored-by: Jakob Schnell
---
CHANGELOG.md | 6 ++++++
ORStools/common/directions_core.py | 12 ++++++++++--
ORStools/proc/directions_lines_proc.py | 4 +++-
ORStools/proc/directions_points_layers_proc.py | 13 ++++++++++---
4 files changed, 29 insertions(+), 6 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4090769d..d6e58d90 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -40,6 +40,12 @@ RELEASING:
14. Create new release in GitHub with tag version and release title of `vX.X.X`
-->
+## Unreleased
+
+### Fixed
+- Add csv\_column parameter to request made by points\_layers\_proc([#260](https://github.com/GIScience/orstools-qgis-plugin/issues/260))
+- make extra\_info work with two points layers
+
## [1.8.2] - 2024-05-20
### Fixed
diff --git a/ORStools/common/directions_core.py b/ORStools/common/directions_core.py
index 0ec789c4..705abdea 100644
--- a/ORStools/common/directions_core.py
+++ b/ORStools/common/directions_core.py
@@ -81,6 +81,7 @@ def get_fields(
to_name: str = "TO_ID",
line: bool = False,
extra_info: list = [],
+ two_layers: bool = False,
) -> QgsFields:
"""
Builds output fields for directions response layer.
@@ -114,6 +115,8 @@ def get_fields(
fields.append(QgsField(from_name, from_type))
if not line:
fields.append(QgsField(to_name, to_type))
+ if two_layers:
+ fields.append(QgsField(from_name, from_type))
for info in extra_info:
field_type = QVariant.Int
if info in ["waytype", "surface", "waycategory", "roadaccessrestrictions", "steepness"]:
@@ -260,7 +263,9 @@ def build_default_parameters(
return params
-def get_extra_info_features_directions(response: dict, extra_info_order: list[str]):
+def get_extra_info_features_directions(
+ response: dict, extra_info_order: list[str], to_from_values: Optional[list] = None
+):
extra_info_order = [
key if key != "waytype" else "waytypes" for key in extra_info_order
] # inconsistency in API
@@ -268,7 +273,6 @@ def get_extra_info_features_directions(response: dict, extra_info_order: list[st
coordinates = response_mini["geometry"]["coordinates"]
feats = list()
extra_info = response_mini["properties"]["extras"]
- logger.log(str(extra_info))
extras_list = {i: [] for i in extra_info_order}
for key in extra_info_order:
try:
@@ -290,7 +294,11 @@ def get_extra_info_features_directions(response: dict, extra_info_order: list[st
extra = extras_list[j]
attr = extra[i]
attrs.append(attr)
+
+ if to_from_values: # for directions from two point layers
+ attrs = [to_from_values[0], to_from_values[1]] + attrs
feat.setAttributes(attrs)
+
feats.append(feat)
return feats
diff --git a/ORStools/proc/directions_lines_proc.py b/ORStools/proc/directions_lines_proc.py
index ccd37f9b..b0e52e6b 100644
--- a/ORStools/proc/directions_lines_proc.py
+++ b/ORStools/proc/directions_lines_proc.py
@@ -226,7 +226,9 @@ def processAlgorithm(
)
if extra_info:
- feats = directions_core.get_extra_info_features_directions(response)
+ feats = directions_core.get_extra_info_features_directions(
+ response, extra_info
+ )
for feat in feats:
sink.addFeature(feat)
else:
diff --git a/ORStools/proc/directions_points_layers_proc.py b/ORStools/proc/directions_points_layers_proc.py
index b342b9f7..8b09e755 100644
--- a/ORStools/proc/directions_points_layers_proc.py
+++ b/ORStools/proc/directions_points_layers_proc.py
@@ -157,8 +157,11 @@ def processAlgorithm(
options = self.parseOptions(parameters, context)
csv_factor = self.parameterAsDouble(parameters, self.CSV_FACTOR, context)
+ csv_column = self.parameterAsString(parameters, self.CSV_COLUMN, context)
if csv_factor > 0:
- options["profile_params"] = {"weightings": {"csv_factor": csv_factor}}
+ options["profile_params"] = {
+ "weightings": {"csv_factor": csv_factor, "csv_column": csv_column}
+ }
extra_info = self.parameterAsEnums(parameters, self.EXTRA_INFO, context)
extra_info = [EXTRA_INFOS[i] for i in extra_info]
@@ -209,7 +212,9 @@ def sort_end(f):
field_types.update({"from_type": source_field.type()})
if destination_field:
field_types.update({"to_type": destination_field.type()})
- sink_fields = directions_core.get_fields(**field_types, extra_info=extra_info)
+ sink_fields = directions_core.get_fields(
+ **field_types, extra_info=extra_info, two_layers=True
+ )
(sink, dest_id) = self.parameterAsSink(
parameters,
@@ -241,7 +246,9 @@ def sort_end(f):
continue
if extra_info:
- feats = directions_core.get_extra_info_features_directions(response)
+ feats = directions_core.get_extra_info_features_directions(
+ response, extra_info, values
+ )
for feat in feats:
sink.addFeature(feat)
else:
From 2b981c177fc67ba9f0ea7b05d53a13d1298610fc Mon Sep 17 00:00:00 2001
From: Jakob Schnell
Date: Wed, 29 May 2024 13:56:43 +0200
Subject: [PATCH 18/26] chore: release v1.8.3
---
CHANGELOG.md | 5 +++--
ORStools/metadata.txt | 8 ++++++--
2 files changed, 9 insertions(+), 4 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d6e58d90..d46e9da5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -40,7 +40,7 @@ RELEASING:
14. Create new release in GitHub with tag version and release title of `vX.X.X`
-->
-## Unreleased
+## [1.8.3] - 2024-05-29
### Fixed
- Add csv\_column parameter to request made by points\_layers\_proc([#260](https://github.com/GIScience/orstools-qgis-plugin/issues/260))
@@ -257,7 +257,8 @@ RELEASING:
- first working version of ORS Tools, after replacing OSM Tools plugin
-[unreleased]: https://github.com/GIScience/orstools-qgis-plugin/compare/v1.8.2...HEAD
+[unreleased]: https://github.com/GIScience/orstools-qgis-plugin/compare/v1.8.3...HEAD
+[1.8.3]: https://github.com/GIScience/orstools-qgis-plugin/compare/v1.8.2...v1.8.3
[1.8.2]: https://github.com/GIScience/orstools-qgis-plugin/compare/v1.8.1...v1.8.2
[1.8.1]: https://github.com/GIScience/orstools-qgis-plugin/compare/v1.8.0...v1.8.1
[1.8.0]: https://github.com/GIScience/orstools-qgis-plugin/compare/v1.7.1...v1.8.0
diff --git a/ORStools/metadata.txt b/ORStools/metadata.txt
index 9a0e0647..2aa2d214 100644
--- a/ORStools/metadata.txt
+++ b/ORStools/metadata.txt
@@ -3,13 +3,17 @@ name=ORS Tools
qgisMinimumVersion=3.4.8
description=openrouteservice routing, isochrones and matrix calculations for QGIS
-version=1.8.2
+version=1.8.3
author=HeiGIT gGmbH
email=support@openrouteservice.heigit.org
about=ORS Tools provides access to most of the functions of openrouteservice.org, based on OpenStreetMap. The tool set includes routing, isochrones and matrix calculations, either interactive in the map canvas or from point files within the processing framework. Extensive attributes are set for output files, incl. duration, length and start/end locations.
-changelog=2024/05/20 v1.8.2
+changelog=2024/05/29 v1.8.3
+ Fixed
+ - issues with extra_info in polylines/two point layer algorithms
+
+ 2024/05/20 v1.8.2
Fixed
- use python 3.9-compatible type hint
From e9731d2101c3489872d552b42ef7fd9f28a09334 Mon Sep 17 00:00:00 2001
From: Till Frankenbach
Date: Mon, 29 Jul 2024 14:48:47 +0200
Subject: [PATCH 19/26] fix: wrap locale reading in try/except statement
Co-Authored-By: Amandus <23240110+TheGreatRefrigerator@users.noreply.github.com>
---
ORStools/ORStoolsPlugin.py | 27 +++++++++++++++++----------
1 file changed, 17 insertions(+), 10 deletions(-)
diff --git a/ORStools/ORStoolsPlugin.py b/ORStools/ORStoolsPlugin.py
index 30d8a9e6..ed0d6e08 100644
--- a/ORStools/ORStoolsPlugin.py
+++ b/ORStools/ORStoolsPlugin.py
@@ -29,7 +29,7 @@
from qgis.gui import QgisInterface
from qgis.core import QgsApplication, QgsSettings
-from qgis.PyQt.QtCore import QTranslator, qVersion, QCoreApplication
+from qgis.PyQt.QtCore import QTranslator, qVersion, QCoreApplication, QLocale
import os.path
from .gui import ORStoolsDialog
@@ -56,15 +56,22 @@ def __init__(self, iface: QgisInterface) -> None:
self.plugin_dir = os.path.dirname(__file__)
# initialize locale
- locale = QgsSettings().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":
- QCoreApplication.installTranslator(self.translator)
+ try:
+ locale = QgsSettings().value("locale/userLocale")
+ if not locale:
+ locale = QLocale().name()
+ locale = locale[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":
+ QCoreApplication.installTranslator(self.translator)
+ except TypeError:
+ pass
def initGui(self) -> None:
"""Create the menu entries and toolbar icons inside the QGIS GUI."""
From 6a008c4069df6aa3e10724446214bf0ee8700e17 Mon Sep 17 00:00:00 2001
From: Amandus Butzer
Date: Mon, 29 Jul 2024 15:13:52 +0200
Subject: [PATCH 20/26] chore: update email address
---
ORStools/metadata.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ORStools/metadata.txt b/ORStools/metadata.txt
index 2aa2d214..73d42885 100644
--- a/ORStools/metadata.txt
+++ b/ORStools/metadata.txt
@@ -5,7 +5,7 @@ description=openrouteservice routing, isochrones and matrix calculations for QGI
version=1.8.3
author=HeiGIT gGmbH
-email=support@openrouteservice.heigit.org
+email=support@smartmobility.heigit.org
about=ORS Tools provides access to most of the functions of openrouteservice.org, based on OpenStreetMap. The tool set includes routing, isochrones and matrix calculations, either interactive in the map canvas or from point files within the processing framework. Extensive attributes are set for output files, incl. duration, length and start/end locations.
From 86a1d55c3402689c14321ce0215cd693d672a7ec Mon Sep 17 00:00:00 2001
From: Amandus Butzer
Date: Mon, 29 Jul 2024 15:14:02 +0200
Subject: [PATCH 21/26] chore: release v1.8.4
---
CHANGELOG.md | 8 +++++++-
ORStools/metadata.txt | 8 ++++++--
2 files changed, 13 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d46e9da5..93df682d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -40,6 +40,11 @@ RELEASING:
14. Create new release in GitHub with tag version and release title of `vX.X.X`
-->
+## [1.8.4] - 2024-07-29
+
+### Fixed
+- issue with missing locale value for non-default user([#271](https://github.com/GIScience/orstools-qgis-plugin/issues/271))
+
## [1.8.3] - 2024-05-29
### Fixed
@@ -257,7 +262,8 @@ RELEASING:
- first working version of ORS Tools, after replacing OSM Tools plugin
-[unreleased]: https://github.com/GIScience/orstools-qgis-plugin/compare/v1.8.3...HEAD
+[unreleased]: https://github.com/GIScience/orstools-qgis-plugin/compare/v1.8.4...HEAD
+[1.8.4]: https://github.com/GIScience/orstools-qgis-plugin/compare/v1.8.3...v1.8.4
[1.8.3]: https://github.com/GIScience/orstools-qgis-plugin/compare/v1.8.2...v1.8.3
[1.8.2]: https://github.com/GIScience/orstools-qgis-plugin/compare/v1.8.1...v1.8.2
[1.8.1]: https://github.com/GIScience/orstools-qgis-plugin/compare/v1.8.0...v1.8.1
diff --git a/ORStools/metadata.txt b/ORStools/metadata.txt
index 73d42885..cd6ec805 100644
--- a/ORStools/metadata.txt
+++ b/ORStools/metadata.txt
@@ -3,13 +3,17 @@ name=ORS Tools
qgisMinimumVersion=3.4.8
description=openrouteservice routing, isochrones and matrix calculations for QGIS
-version=1.8.3
+version=1.8.4
author=HeiGIT gGmbH
email=support@smartmobility.heigit.org
about=ORS Tools provides access to most of the functions of openrouteservice.org, based on OpenStreetMap. The tool set includes routing, isochrones and matrix calculations, either interactive in the map canvas or from point files within the processing framework. Extensive attributes are set for output files, incl. duration, length and start/end locations.
-changelog=2024/05/29 v1.8.3
+changelog=2024/07/29 v1.8.4
+ Fixed
+ - issue with missing locale value
+
+ 2024/05/29 v1.8.3
Fixed
- issues with extra_info in polylines/two point layer algorithms
From 2ceb0c2d0f484f00a0fa8f149a58f9d0893e50c4 Mon Sep 17 00:00:00 2001
From: Amandus Butzer
Date: Mon, 29 Jul 2024 15:44:57 +0200
Subject: [PATCH 22/26] chore: fix MIT licence to name copyright from original
author
- fix year of copyright application
refers to: https://github.com/GIScience/orstools-qgis-plugin/issues/228
---
LICENSE.md | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/LICENSE.md b/LICENSE.md
index 3da1a70c..031c95c7 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -1,6 +1,7 @@
MIT License
-Copyright (c) 2021 HeiGIT gGmbH
+Copyright (c) 2017 Nils Nolde
+Copyright (c) 2019 HeiGIT gGmbH
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
From 6c0cc6733a8f22e0f484d76213b6b91f28060e2a Mon Sep 17 00:00:00 2001
From: Amandus Butzer
Date: Tue, 30 Jul 2024 17:13:54 +0200
Subject: [PATCH 23/26] chore: ignore mac specific files
---
.gitignore | 1 +
1 file changed, 1 insertion(+)
diff --git a/.gitignore b/.gitignore
index e5668ccf..9aa64ac0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
+.DS_Store
tets/
docs/wiki/OSMtools.wiki/
.idea/
From d5ede0c25425fc9c81549ae97cd7310a03778e52 Mon Sep 17 00:00:00 2001
From: Amandus Butzer
Date: Tue, 30 Jul 2024 18:10:48 +0200
Subject: [PATCH 24/26] chore: add LICENSE file to plugin folder
---
ORStools/LICENSE | 22 ++++++++++++++++++++++
1 file changed, 22 insertions(+)
create mode 100644 ORStools/LICENSE
diff --git a/ORStools/LICENSE b/ORStools/LICENSE
new file mode 100644
index 00000000..031c95c7
--- /dev/null
+++ b/ORStools/LICENSE
@@ -0,0 +1,22 @@
+MIT License
+
+Copyright (c) 2017 Nils Nolde
+Copyright (c) 2019 HeiGIT gGmbH
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
From 1f0e1230d24ec35122be103d3e1efe2c618bc0d6 Mon Sep 17 00:00:00 2001
From: Till Frankenbach
Date: Fri, 23 Aug 2024 11:41:48 +0200
Subject: [PATCH 25/26] feat: introduce tests and CI for testing
Co-authored-by: Jakob Schnell
---
.github/workflows/test.yml | 34 +++
CHANGELOG.md | 4 +
ORStools/common/directions_core.py | 2 +-
ORStools/gui/ORStoolsDialog.py | 6 +-
README.md | 62 +++++
requirements.txt | 6 +
tests/__init__.py | 0
tests/conftest.py | 34 +++
tests/test_common.py | 380 +++++++++++++++++++++++++++++
tests/test_gui.py | 68 ++++++
tests/test_proc.py | 178 ++++++++++++++
tests/test_utils.py | 59 +++++
tests/utils/__init__.py | 0
tests/utils/qgis_interface.py | 237 ++++++++++++++++++
tests/utils/utilities.py | 101 ++++++++
15 files changed, 1169 insertions(+), 2 deletions(-)
create mode 100644 .github/workflows/test.yml
create mode 100644 tests/__init__.py
create mode 100644 tests/conftest.py
create mode 100644 tests/test_common.py
create mode 100644 tests/test_gui.py
create mode 100644 tests/test_proc.py
create mode 100644 tests/test_utils.py
create mode 100644 tests/utils/__init__.py
create mode 100644 tests/utils/qgis_interface.py
create mode 100644 tests/utils/utilities.py
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 00000000..08ccf6d4
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,34 @@
+
+name: Testing
+
+on:
+ pull_request:
+
+jobs:
+ test_3_16:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Run test 3.16
+ run: |
+ docker run -v ${GITHUB_WORKSPACE}:/src -w /src qgis/qgis:release-3_16 sh -c 'apt-get -y update && apt-get -y install xvfb && export ORS_API_KEY=${{ secrets.ORS_API_KEY }} && export DISPLAY=:0.0 && pip install -U pytest && xvfb-run pytest'
+ env:
+ DOCKER_IMAGE: ${{ steps.docker-build.outputs.FULL_IMAGE_NAME }}
+ test_3_22:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Run test 3.22
+ run: |
+ docker run -v ${GITHUB_WORKSPACE}:/src -w /src qgis/qgis:release-3_22 sh -c 'apt-get -y update && apt-get -y install xvfb && export DISPLAY=:0.0 && export ORS_API_KEY=${{ secrets.ORS_API_KEY }} && export DISPLAY=:0.0 && pip install -U pytest && xvfb-run pytest'
+ env:
+ DOCKER_IMAGE: ${{ steps.docker-build.outputs.FULL_IMAGE_NAME }}
+ test_latest:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Run test latest
+ run: |
+ docker run -v ${GITHUB_WORKSPACE}:/src -w /src qgis/qgis:latest sh -c 'apt-get -y update && apt-get -y install xvfb && export DISPLAY=:0.0 && export ORS_API_KEY=${{ secrets.ORS_API_KEY }} && apt install python3-pytest && xvfb-run pytest'
+ env:
+ DOCKER_IMAGE: ${{ steps.docker-build.outputs.FULL_IMAGE_NAME }}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 93df682d..07219bfc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -82,6 +82,10 @@ RELEASING:
- Improved type hints
+# Unreleased
+### Added
+- Unit- and e2e-testing
+
## [1.7.1] - 2024-01-15
### Added
diff --git a/ORStools/common/directions_core.py b/ORStools/common/directions_core.py
index 705abdea..06c530a9 100644
--- a/ORStools/common/directions_core.py
+++ b/ORStools/common/directions_core.py
@@ -264,7 +264,7 @@ def build_default_parameters(
def get_extra_info_features_directions(
- response: dict, extra_info_order: list[str], to_from_values: Optional[list] = None
+ response: dict, extra_info_order: List[str], to_from_values: Optional[list] = None
):
extra_info_order = [
key if key != "waytype" else "waytypes" for key in extra_info_order
diff --git a/ORStools/gui/ORStoolsDialog.py b/ORStools/gui/ORStoolsDialog.py
index 49f6a445..95cefdb5 100644
--- a/ORStools/gui/ORStoolsDialog.py
+++ b/ORStools/gui/ORStoolsDialog.py
@@ -31,7 +31,11 @@
import os
from typing import Optional
-import processing
+try:
+ import processing
+except ModuleNotFoundError:
+ pass
+
import webbrowser
from qgis._core import Qgis, QgsAnnotation
diff --git a/README.md b/README.md
index 90f21505..9a9d2bdb 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,8 @@
# ORS Tools QGIS plugin
+![Testing](https://github.com/Merydian/orstools-qgis-plugin/actions/workflows/test.yml/badge.svg)
+![Ruff](https://github.com/Merydian/orstools-qgis-plugin/actions/workflows/ruff.yml/badge.svg)
+
![ORS Tools](https://user-images.githubusercontent.com/23240110/122937401-3ee72400-d372-11eb-8e3b-6c435d1dd964.png)
Set of tools for QGIS to use the [openrouteservice](https://openrouteservice.org) (ORS) API.
@@ -120,6 +123,65 @@ where `` is one of:
- Windows: `C:\Users\USER\AppData\Roaming\QGIS\QGIS3\profiles\default\python\plugins\ORStools`
- Mac OS: `Library/Application Support/QGIS/QGIS3/profiles/default/python/plugins/ORStools`
+### CI
+#### Testing
+The repository tests on the QGis Versions *3.16*, *3.22* and the *latest* version.
+Until now, it's only possible to test one version at a time.
+
+#### Linux
+On linux machines you can run the tests with your local QGIS installation.
+
+1. Install QGIS and make sure it's available in your currently activated environment.
+
+You will need an ORS-API key. Either set it as an environment variable or do `export ORS_API_KEY=[Your API key here]` before you run the tests.
+
+To run the tests do:
+```shell
+cd orstools-qgis-plugin
+pytest
+```
+
+#### Windows
+Do all the following steps in a [*WSL*](https://learn.microsoft.com/en-us/windows/wsl/install). To run tests locally you can use a [conda installation](https://github.com/opengisch/qgis-conda-builder) of the QGis version you want to test.
+You will also have to install *xvfb* to run the tests on involving an interface.
+Lastly, we need [*Pytest*](https://docs.pytest.org/en/8.0.x/) to run tests in general.
+
+To do the above run use these commands:
+1. Install a version of anaconda, preferrably [*miniforge*](https://github.com/conda-forge/miniforge).
+
+2. Create and prepare the environment.
+
+```shell
+# create environment
+conda create --name qgis_test
+# activate environment
+conda activate qgis_test
+# install pip
+conda install pip
+```
+
+3. Install QGis using mamba.
+```shell
+conda install -c conda-forge qgis=[3.16, 3.22, latest] # choose one
+```
+
+4. Install *xvfb*
+```shell
+sudo apt-get update
+sudo apt install xvfb
+```
+
+5. Install *Pytest* using pip in testing environment.
+```shell
+pip install -U pytest
+```
+
+To run the tests you will need an ORS-API key:
+```shell
+cd orstools-qgis-plugin
+export ORS_API_KEY=[Your API key here] && xvfb-run pytest
+```
+
### Debugging
In the **PyCharm community edition** you will have to use logging and printing to inspect elements.
The First Aid QGIS plugin can probably also be used additionally.
diff --git a/requirements.txt b/requirements.txt
index af3ee576..ca4e9990 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1 +1,7 @@
+# developement
ruff
+pytest
+
+# testing
+pyyaml
+pytest
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 00000000..d39efd01
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,34 @@
+import os
+import yaml
+
+from ORStools.utils.configmanager import read_config
+
+with open("ORStools/config.yml", "r+") as file:
+ data = yaml.safe_load(file)
+
+
+def pytest_sessionstart(session):
+ """
+ Called after the Session object has been created and
+ before performing collection and entering the run test loop.
+ """
+ if data["providers"][0]["key"] == "":
+ data["providers"][0]["key"] = os.environ.get("ORS_API_KEY")
+ with open("ORStools/config.yml", "w") as file:
+ yaml.dump(data, file)
+ else:
+ raise ValueError("API key is not empty.")
+
+
+def pytest_sessionfinish(session, exitstatus):
+ """
+ Called after whole test run finished, right before
+ returning the exit status to the system.
+ """
+ with open("ORStools/config.yml", "w") as file:
+ if not data["providers"][0]["key"] == "":
+ data['providers'][0]['key'] = '' # fmt: skip
+ yaml.dump(data, file)
+
+ config = read_config()
+ assert config["providers"][0]["key"] == '' # fmt: skip
diff --git a/tests/test_common.py b/tests/test_common.py
new file mode 100644
index 00000000..2061f097
--- /dev/null
+++ b/tests/test_common.py
@@ -0,0 +1,380 @@
+from qgis.core import QgsPointXY
+from qgis.testing import unittest
+
+from ORStools.common import client, directions_core, isochrones_core
+import os
+
+
+class TestCommon(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls) -> None:
+ cls.api_key = os.environ.get("ORS_API_KEY")
+ if cls.api_key is None:
+ raise ValueError("ORS_API_KEY environment variable is not set")
+
+ def test_client_request_geometry(self):
+ test_response = {
+ "type": "FeatureCollection",
+ "metadata": {
+ "id": "1",
+ "attribution": "openrouteservice.org | OpenStreetMap contributors",
+ "service": "routing",
+ "timestamp": 1708505372024,
+ "query": {
+ "coordinates": [[8.684101, 50.131613], [8.68534, 50.131651]],
+ "profile": "driving-car",
+ "id": "1",
+ "preference": "fastest",
+ "format": "geojson",
+ "geometry": True,
+ "elevation": True,
+ },
+ "engine": {
+ "version": "7.1.1",
+ "build_date": "2024-01-29T14:41:12Z",
+ "graph_date": "2024-02-18T14:05:28Z",
+ },
+ "system_message": "Preference 'fastest' has been deprecated, using 'recommended'.",
+ },
+ "bbox": [8.684088, 50.131187, 131.0, 8.686212, 50.131663, 133.8],
+ "features": [
+ {
+ "bbox": [8.684088, 50.131187, 131.0, 8.686212, 50.131663, 133.8],
+ "type": "Feature",
+ "properties": {
+ "ascent": 2.8,
+ "descent": 0.0,
+ "transfers": 0,
+ "fare": 0,
+ "way_points": [0, 13],
+ "summary": {"distance": 247.2, "duration": 45.1},
+ },
+ "geometry": {
+ "coordinates": [
+ [8.684088, 50.131587, 131.0],
+ [8.684173, 50.13157, 131.0],
+ [8.684413, 50.131523, 131.0],
+ [8.684872, 50.131432, 131.0],
+ [8.685652, 50.131272, 132.1],
+ [8.685937, 50.131187, 132.7],
+ [8.686097, 50.131227, 132.9],
+ [8.686204, 50.131325, 133.1],
+ [8.686212, 50.13143, 133.3],
+ [8.686184, 50.13148, 133.4],
+ [8.68599, 50.131544, 133.6],
+ [8.685774, 50.131612, 133.7],
+ [8.685559, 50.131663, 133.7],
+ [8.68534, 50.13166, 133.8],
+ ],
+ "type": "LineString",
+ },
+ }
+ ],
+ }
+
+ provider = {
+ "ENV_VARS": {
+ "ORS_QUOTA": "X-Ratelimit-Limit",
+ "ORS_REMAINING": "X-Ratelimit-Remaining",
+ },
+ "base_url": "https://api.openrouteservice.org",
+ "key": self.api_key,
+ "name": "openrouteservice",
+ "timeout": 60,
+ }
+
+ params = {
+ "preference": "fastest",
+ "geometry": "true",
+ "instructions": "false",
+ "elevation": True,
+ "id": 1,
+ "coordinates": [[8.684101, 50.131613], [8.68534, 50.131651]],
+ }
+ agent = "QGIS_ORStools_testing"
+ profile = "driving-car"
+ clnt = client.Client(provider, agent)
+ response = clnt.request("/v2/directions/" + profile + "/geojson", {}, post_json=params)
+ self.assertAlmostEqual(
+ response["features"][0]["geometry"]["coordinates"][0][0],
+ test_response["features"][0]["geometry"]["coordinates"][0][0],
+ )
+
+ def test_output_feature_directions(self):
+ response = {
+ "type": "FeatureCollection",
+ "metadata": {
+ "id": "1",
+ "attribution": "openrouteservice.org | OpenStreetMap contributors",
+ "service": "routing",
+ "timestamp": 1708522371289,
+ "query": {
+ "coordinates": [
+ [-68.199488, -16.518187],
+ [-68.199201, -16.517873],
+ [-68.198438, -16.518486],
+ [-68.198067, -16.518183],
+ ],
+ "profile": "driving-car",
+ "id": "1",
+ "preference": "fastest",
+ "format": "geojson",
+ "geometry": True,
+ "elevation": True,
+ },
+ "engine": {
+ "version": "7.1.1",
+ "build_date": "2024-01-29T14:41:12Z",
+ "graph_date": "2024-02-18T14:05:28Z",
+ },
+ "system_message": "Preference 'fastest' has been deprecated, using 'recommended'.",
+ },
+ "bbox": [-68.199495, -16.518504, 4025.0, -68.198061, -16.51782, 4025.07],
+ "features": [
+ {
+ "bbox": [-68.199495, -16.518504, 4025.0, -68.198061, -16.51782, 4025.07],
+ "type": "Feature",
+ "properties": {
+ "ascent": 0.1,
+ "descent": 0.0,
+ "transfers": 0,
+ "fare": 0,
+ "way_points": [0, 2, 6, 9],
+ "summary": {"distance": 222.4, "duration": 53.0},
+ },
+ "geometry": {
+ "coordinates": [
+ [-68.199495, -16.518181, 4025.0],
+ [-68.199485, -16.51817, 4025.0],
+ [-68.199206, -16.517869, 4025.0],
+ [-68.199161, -16.51782, 4025.0],
+ [-68.198799, -16.518142, 4025.0],
+ [-68.198393, -16.518478, 4025.0],
+ [-68.198417, -16.518504, 4025.0],
+ [-68.198393, -16.518478, 4025.0],
+ [-68.198078, -16.518162, 4025.0],
+ [-68.198061, -16.518177, 4025.1],
+ ],
+ "type": "LineString",
+ },
+ }
+ ],
+ }
+ profile = "driving-car"
+ preference = "fastest"
+ feature = directions_core.get_output_feature_directions(response, profile, preference)
+ coordinates = [(vertex.x(), vertex.y()) for vertex in feature.geometry().vertices()]
+ test_coords = [
+ (-68.199495, -16.518181),
+ (-68.199485, -16.51817),
+ (-68.199206, -16.517869),
+ (-68.199161, -16.51782),
+ (-68.198799, -16.518142),
+ (-68.198393, -16.518478),
+ (-68.198417, -16.518504),
+ (-68.198393, -16.518478),
+ (-68.198078, -16.518162),
+ (-68.198061, -16.518177),
+ ]
+
+ self.assertAlmostEqual(coordinates, test_coords)
+
+ def test_output_features_optimization(self):
+ response = {
+ "code": 0,
+ "summary": {
+ "cost": 36,
+ "routes": 1,
+ "unassigned": 0,
+ "setup": 0,
+ "service": 0,
+ "duration": 36,
+ "waiting_time": 0,
+ "priority": 0,
+ "distance": 152,
+ "violations": [],
+ "computing_times": {"loading": 23, "solving": 0, "routing": 12},
+ },
+ "unassigned": [],
+ "routes": [
+ {
+ "vehicle": 0,
+ "cost": 36,
+ "setup": 0,
+ "service": 0,
+ "duration": 36,
+ "waiting_time": 0,
+ "priority": 0,
+ "distance": 152,
+ "steps": [
+ {
+ "type": "start",
+ "location": [-68.193407, -16.472978],
+ "setup": 0,
+ "service": 0,
+ "waiting_time": 0,
+ "arrival": 0,
+ "duration": 0,
+ "violations": [],
+ "distance": 0,
+ },
+ {
+ "type": "job",
+ "location": [-68.192889, -16.472475],
+ "id": 0,
+ "setup": 0,
+ "service": 0,
+ "waiting_time": 0,
+ "job": 0,
+ "arrival": 18,
+ "duration": 18,
+ "violations": [],
+ "distance": 76,
+ },
+ {
+ "type": "end",
+ "location": [-68.193407, -16.472978],
+ "setup": 0,
+ "service": 0,
+ "waiting_time": 0,
+ "arrival": 36,
+ "duration": 36,
+ "violations": [],
+ "distance": 152,
+ },
+ ],
+ "violations": [],
+ "geometry": "lkpcBd_f_LuBiAtBhA",
+ }
+ ],
+ }
+ profile = "driving-car"
+ preference = "fastest"
+ feature = directions_core.get_output_features_optimization(response, profile, preference)
+ coordinates = [(vertex.x(), vertex.y()) for vertex in feature.geometry().vertices()]
+
+ test_coords = [(-68.19331, -16.47303), (-68.19294, -16.47244), (-68.19331, -16.47303)]
+ self.assertAlmostEqual(coordinates, test_coords)
+
+ def test_build_default_parameters(self):
+ preference, point_list, coordinates, options = (
+ "fastest",
+ [
+ QgsPointXY(-68.1934067732971414, -16.47297756153070125),
+ QgsPointXY(-68.19288936751472363, -16.47247452813111934),
+ ],
+ None,
+ {},
+ )
+ params = directions_core.build_default_parameters(
+ preference, point_list, coordinates, options
+ )
+ test_params = {
+ "coordinates": [[-68.193407, -16.472978], [-68.192889, -16.472475]],
+ "preference": "fastest",
+ "geometry": "true",
+ "instructions": "false",
+ "elevation": True,
+ "id": None,
+ "options": {},
+ "extra_info": None,
+ }
+
+ self.assertDictEqual(params, test_params)
+
+ def test_isochrones(self):
+ response = {
+ "type": "FeatureCollection",
+ "metadata": {
+ "attribution": "openrouteservice.org | OpenStreetMap contributors",
+ "service": "isochrones",
+ "timestamp": 1710421093483,
+ "query": {
+ "profile": "driving-car",
+ "locations": [[-112.594673, 43.554193]],
+ "location_type": "start",
+ "range": [60.0],
+ "range_type": "time",
+ "options": {},
+ "attributes": ["total_pop"],
+ },
+ "engine": {
+ "version": "7.1.1",
+ "build_date": "2024-01-29T14:41:12Z",
+ "graph_date": "2024-03-10T15:19:08Z",
+ },
+ },
+ "bbox": [-112.637014, 43.548994, -112.550441, 43.554343],
+ "features": [
+ {
+ "type": "Feature",
+ "properties": {
+ "group_index": 0,
+ "value": 60.0,
+ "center": [-112.5946738217447, 43.55409137088865],
+ "total_pop": 0.0,
+ },
+ "geometry": {
+ "coordinates": [
+ [
+ [-112.637014, 43.549342],
+ [-112.63692, 43.548994],
+ [-112.631205, 43.550527],
+ [-112.625496, 43.552059],
+ [-112.623482, 43.552518],
+ [-112.617781, 43.553548],
+ [-112.615319, 43.553798],
+ [-112.612783, 43.553937],
+ [-112.61154, 43.553971],
+ [-112.609679, 43.553977],
+ [-112.607819, 43.553983],
+ [-112.603711, 43.553958],
+ [-112.599603, 43.553932],
+ [-112.598575, 43.553928],
+ [-112.594187, 43.553909],
+ [-112.593002, 43.553904],
+ [-112.588772, 43.553886],
+ [-112.587429, 43.553881],
+ [-112.578142, 43.553673],
+ [-112.568852, 43.553464],
+ [-112.559651, 43.553232],
+ [-112.55045, 43.553],
+ [-112.550441, 43.55336],
+ [-112.559642, 43.553592],
+ [-112.568844, 43.553824],
+ [-112.578134, 43.554032],
+ [-112.587427, 43.554241],
+ [-112.58877, 43.554246],
+ [-112.593, 43.554264],
+ [-112.594186, 43.554269],
+ [-112.598573, 43.554288],
+ [-112.599601, 43.554292],
+ [-112.603709, 43.554318],
+ [-112.607817, 43.554343],
+ [-112.60968, 43.554337],
+ [-112.611541, 43.554331],
+ [-112.612793, 43.554297],
+ [-112.614041, 43.554262],
+ [-112.615348, 43.554157],
+ [-112.616646, 43.554052],
+ [-112.617826, 43.553905],
+ [-112.618998, 43.553758],
+ [-112.620272, 43.553544],
+ [-112.621537, 43.553331],
+ [-112.623562, 43.552869],
+ [-112.625576, 43.55241],
+ [-112.631298, 43.550875],
+ [-112.637014, 43.549342],
+ ]
+ ],
+ "type": "Polygon",
+ },
+ }
+ ],
+ }
+ id_field_value = None
+ isochrones = isochrones_core.Isochrones()
+ isochrones.set_parameters("driving-car", "time", 60)
+
+ feats = isochrones.get_features(response, id_field_value)
+ self.assertAlmostEqual(next(feats).geometry().area(), 3.176372365487623e-05)
diff --git a/tests/test_gui.py b/tests/test_gui.py
new file mode 100644
index 00000000..17c97adf
--- /dev/null
+++ b/tests/test_gui.py
@@ -0,0 +1,68 @@
+from qgis.testing import unittest
+
+from qgis.PyQt.QtTest import QTest
+from qgis.PyQt.QtCore import Qt, QEvent, QPoint
+from qgis.PyQt.QtWidgets import QPushButton
+from qgis.gui import QgsMapCanvas, QgsMapMouseEvent
+from qgis.core import (
+ QgsCoordinateReferenceSystem,
+ QgsRectangle,
+)
+import pytest
+
+from tests.utils.utilities import get_qgis_app
+
+CANVAS: QgsMapCanvas
+QGISAPP, CANVAS, IFACE, PARENT = get_qgis_app()
+
+
+@pytest.mark.filterwarnings("ignore:.*imp module is deprecated.*")
+class TestGui(unittest.TestCase):
+ def test_ORStoolsDialog(self):
+ from ORStools.gui.ORStoolsDialog import ORStoolsDialog
+ from ORStools.utils import maptools
+
+ CRS = QgsCoordinateReferenceSystem.fromEpsgId(3857)
+ CANVAS.setExtent(QgsRectangle(258889, 7430342, 509995, 7661955))
+ CANVAS.setDestinationCrs(CRS)
+ CANVAS.setFrameStyle(0)
+ CANVAS.resize(600, 400)
+ self.assertEqual(CANVAS.width(), 600)
+ self.assertEqual(CANVAS.height(), 400)
+
+ dlg = ORStoolsDialog(IFACE)
+ dlg.open()
+ self.assertTrue(dlg.isVisible())
+
+ map_button: QPushButton = dlg.routing_fromline_map
+ # click 'routing_fromline_map'
+ QTest.mouseClick(map_button, Qt.LeftButton)
+ self.assertFalse(dlg.isVisible())
+ self.assertIsInstance(CANVAS.mapTool(), maptools.LineTool)
+
+ map_dclick = QgsMapMouseEvent(
+ CANVAS,
+ QEvent.MouseButtonDblClick,
+ QPoint(5, 5), # Relative to the canvas' dimensions
+ Qt.LeftButton,
+ Qt.LeftButton,
+ Qt.NoModifier,
+ )
+
+ map_click = QgsMapMouseEvent(
+ CANVAS,
+ QEvent.MouseButtonRelease,
+ QPoint(0, 0), # Relative to the canvas' dimensions
+ Qt.LeftButton,
+ Qt.LeftButton,
+ Qt.NoModifier,
+ )
+ # click on canvas at [0, 0]
+ dlg.line_tool.canvasReleaseEvent(map_click)
+ # doubleclick on canvas at [5, 5]
+ dlg.line_tool.canvasDoubleClickEvent(map_dclick)
+
+ self.assertTrue(dlg.isVisible())
+ self.assertAlmostEqual(
+ dlg.routing_fromline_list.item(0).text(), "Point 0: -0.187575, 56.516620"
+ )
diff --git a/tests/test_proc.py b/tests/test_proc.py
new file mode 100644
index 00000000..df4cd8d7
--- /dev/null
+++ b/tests/test_proc.py
@@ -0,0 +1,178 @@
+from qgis.core import (
+ QgsPointXY,
+ QgsProcessingFeedback,
+ QgsProcessingContext,
+ QgsProcessingUtils,
+ QgsVectorLayer,
+ QgsFeature,
+ QgsGeometry,
+)
+from qgis.testing import unittest
+
+from ORStools.proc.directions_lines_proc import ORSDirectionsLinesAlgo
+from ORStools.proc.directions_points_layer_proc import ORSDirectionsPointsLayerAlgo
+from ORStools.proc.directions_points_layers_proc import ORSDirectionsPointsLayersAlgo
+from ORStools.proc.isochrones_layer_proc import ORSIsochronesLayerAlgo
+from ORStools.proc.isochrones_point_proc import ORSIsochronesPointAlgo
+from ORStools.proc.matrix_proc import ORSMatrixAlgo
+
+
+class TestProc(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls) -> None:
+ uri = "point?crs=epsg:4326"
+ cls.point_layer_1 = QgsVectorLayer(uri, "Scratch point layer", "memory")
+ points_of_interest = [QgsPointXY(-118.2394, 34.0739), QgsPointXY(-118.3215, 34.1399)]
+ for point in points_of_interest:
+ feature = QgsFeature()
+ feature.setGeometry(QgsGeometry.fromPointXY(point))
+ cls.point_layer_1.dataProvider().addFeatures([feature])
+
+ cls.point_layer_2 = QgsVectorLayer(uri, "Scratch point layer", "memory")
+ points_of_interest = [QgsPointXY(-118.5, 34.2), QgsPointXY(-118.5, 34.3)]
+ for point in points_of_interest:
+ feature = QgsFeature()
+ feature.setGeometry(QgsGeometry.fromPointXY(point))
+ cls.point_layer_2.dataProvider().addFeatures([feature])
+
+ cls.line_layer = QgsVectorLayer(uri, "Scratch point layer", "memory")
+ vertices = [(-118.2394, 34.0739), (-118.3215, 34.1341), (-118.4961, 34.5)]
+ line_geometry = QgsGeometry.fromPolylineXY([QgsPointXY(x, y) for x, y in vertices])
+ feature = QgsFeature()
+ feature.setGeometry(line_geometry)
+ cls.line_layer.dataProvider().addFeatures([feature])
+
+ cls.feedback = QgsProcessingFeedback()
+ cls.context = QgsProcessingContext()
+
+ def test_directions_lines(self):
+ parameters = {
+ "INPUT_AVOID_BORDERS": None,
+ "INPUT_AVOID_COUNTRIES": "",
+ "INPUT_AVOID_FEATURES": [],
+ "INPUT_AVOID_POLYGONS": None,
+ "INPUT_LAYER_FIELD": None,
+ "INPUT_LINE_LAYER": self.line_layer,
+ "INPUT_OPTIMIZE": None,
+ "INPUT_PREFERENCE": 0,
+ "INPUT_PROFILE": 0,
+ "INPUT_PROVIDER": 0,
+ "INPUT_METRIC": 0,
+ "LOCATION_TYPE": 0,
+ "OUTPUT": "TEMPORARY_OUTPUT",
+ }
+
+ directions = ORSDirectionsLinesAlgo().create()
+ dest_id = directions.processAlgorithm(parameters, self.context, self.feedback)
+ processed_layer = QgsProcessingUtils.mapLayerFromString(dest_id["OUTPUT"], self.context)
+
+ self.assertEqual(type(processed_layer), QgsVectorLayer)
+
+ def test_directions_points_layer(self):
+ parameters = {
+ "INPUT_AVOID_BORDERS": None,
+ "INPUT_AVOID_COUNTRIES": "",
+ "INPUT_AVOID_FEATURES": [],
+ "INPUT_AVOID_POLYGONS": None,
+ "INPUT_LAYER_FIELD": None,
+ "INPUT_OPTIMIZE": None,
+ "INPUT_POINT_LAYER": self.point_layer_1,
+ "INPUT_PREFERENCE": 0,
+ "INPUT_PROFILE": 0,
+ "INPUT_PROVIDER": 0,
+ "INPUT_SORTBY": None,
+ "OUTPUT": "TEMPORARY_OUTPUT",
+ }
+
+ directions = ORSDirectionsPointsLayerAlgo().create()
+ dest_id = directions.processAlgorithm(parameters, self.context, self.feedback)
+ processed_layer = QgsProcessingUtils.mapLayerFromString(dest_id["OUTPUT"], self.context)
+
+ self.assertEqual(type(processed_layer), QgsVectorLayer)
+
+ def test_directions_points_layers(self):
+ parameters = {
+ "INPUT_AVOID_BORDERS": None,
+ "INPUT_AVOID_COUNTRIES": "",
+ "INPUT_AVOID_FEATURES": [],
+ "INPUT_AVOID_POLYGONS": None,
+ "INPUT_END_FIELD": None,
+ "INPUT_END_LAYER": self.point_layer_1,
+ "INPUT_MODE": 0,
+ "INPUT_PREFERENCE": 0,
+ "INPUT_PROFILE": 0,
+ "INPUT_PROVIDER": 0,
+ "INPUT_SORT_END_BY": None,
+ "INPUT_SORT_START_BY": None,
+ "INPUT_START_FIELD": None,
+ "INPUT_START_LAYER": self.point_layer_2,
+ "OUTPUT": "TEMPORARY_OUTPUT",
+ }
+
+ directions = ORSDirectionsPointsLayersAlgo().create()
+ dest_id = directions.processAlgorithm(parameters, self.context, self.feedback)
+ processed_layer = QgsProcessingUtils.mapLayerFromString(dest_id["OUTPUT"], self.context)
+
+ self.assertEqual(type(processed_layer), QgsVectorLayer)
+
+ def test_isochrones_layer(self):
+ parameters = {
+ "INPUT_AVOID_BORDERS": None,
+ "INPUT_AVOID_COUNTRIES": "",
+ "INPUT_AVOID_FEATURES": [],
+ "INPUT_AVOID_POLYGONS": None,
+ "INPUT_FIELD": None,
+ "INPUT_METRIC": 0,
+ "INPUT_POINT_LAYER": self.point_layer_1,
+ "INPUT_PROFILE": 0,
+ "INPUT_PROVIDER": 0,
+ "INPUT_RANGES": "5, 10",
+ "INPUT_SMOOTHING": None,
+ "LOCATION_TYPE": 0,
+ "OUTPUT": "TEMPORARY_OUTPUT",
+ }
+
+ iso = ORSIsochronesLayerAlgo().create()
+ dest_id = iso.processAlgorithm(parameters, self.context, self.feedback)
+ processed_layer = QgsProcessingUtils.mapLayerFromString(dest_id["OUTPUT"], self.context)
+
+ self.assertEqual(type(processed_layer), QgsVectorLayer)
+
+ def test_isochrones_point(self):
+ parameters = {
+ "INPUT_AVOID_BORDERS": None,
+ "INPUT_AVOID_COUNTRIES": "",
+ "INPUT_AVOID_FEATURES": [],
+ "INPUT_AVOID_POLYGONS": None,
+ "INPUT_METRIC": 0,
+ "INPUT_POINT": "-12476269.994314,3961968.635469 [EPSG:3857]",
+ "INPUT_PROFILE": 0,
+ "INPUT_PROVIDER": 0,
+ "INPUT_RANGES": "5, 10",
+ "INPUT_SMOOTHING": None,
+ "LOCATION_TYPE": 0,
+ "OUTPUT": "TEMPORARY_OUTPUT",
+ }
+
+ iso = ORSIsochronesPointAlgo().create()
+ dest_id = iso.processAlgorithm(parameters, self.context, self.feedback)
+ processed_layer = QgsProcessingUtils.mapLayerFromString(dest_id["OUTPUT"], self.context)
+
+ self.assertEqual(type(processed_layer), QgsVectorLayer)
+
+ def test_matrix(self):
+ parameters = {
+ "INPUT_END_FIELD": None,
+ "INPUT_END_LAYER": self.point_layer_1,
+ "INPUT_PROFILE": 0,
+ "INPUT_PROVIDER": 0,
+ "INPUT_START_FIELD": None,
+ "INPUT_START_LAYER": self.point_layer_2,
+ "OUTPUT": "TEMPORARY_OUTPUT",
+ }
+
+ matrix = ORSMatrixAlgo().create()
+ dest_id = matrix.processAlgorithm(parameters, self.context, self.feedback)
+ processed_layer = QgsProcessingUtils.mapLayerFromString(dest_id["OUTPUT"], self.context)
+
+ self.assertEqual(type(processed_layer), QgsVectorLayer)
diff --git a/tests/test_utils.py b/tests/test_utils.py
new file mode 100644
index 00000000..e7fccc87
--- /dev/null
+++ b/tests/test_utils.py
@@ -0,0 +1,59 @@
+from qgis.testing import unittest
+
+from qgis.core import QgsCoordinateReferenceSystem, QgsPointXY
+
+from ORStools.utils.transform import transformToWGS
+from ORStools.utils.convert import decode_polyline
+from ORStools.utils.processing import get_params_optimize
+
+
+class TestUtils(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls) -> None:
+ cls.WGS = QgsCoordinateReferenceSystem.fromEpsgId(4326)
+ cls.PSEUDO = QgsCoordinateReferenceSystem.fromEpsgId(3857)
+
+ def test_to_wgs_pseudo(self):
+ point = QgsPointXY(1493761.05913532, 6890799.81730105)
+ transformer = transformToWGS(self.PSEUDO)
+ self.assertEqual(
+ transformer.transform(point), QgsPointXY(13.41868390243822162, 52.49867709045137332)
+ )
+
+ def test_polyline_convert(self):
+ polyline = "psvcBxg}~KAGUoBMo@Ln@TnB@F"
+ decoded = decode_polyline(polyline)
+ self.assertEqual(
+ decoded,
+ [
+ [-68.14861, -16.50505],
+ [-68.14857, -16.50504],
+ [-68.14801, -16.50493],
+ [-68.14777, -16.50486],
+ [-68.14801, -16.50493],
+ [-68.14857, -16.50504],
+ [-68.14861, -16.50505],
+ ],
+ )
+
+ def test_get_params_optimize(self):
+ points = [
+ QgsPointXY(-68.14860459410432725, -16.5050554680791457),
+ QgsPointXY(-68.14776841920792094, -16.50487191749212812),
+ ]
+ profile = "driving-car"
+ mode = 0
+
+ params = {
+ "jobs": [{"location": [-68.147768, -16.504872], "id": 0}],
+ "vehicles": [
+ {
+ "id": 0,
+ "profile": "driving-car",
+ "start": [-68.148605, -16.505055],
+ "end": [-68.148605, -16.505055],
+ }
+ ],
+ "options": {"g": True},
+ }
+ self.assertEqual(get_params_optimize(points, profile, mode), params)
diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/utils/qgis_interface.py b/tests/utils/qgis_interface.py
new file mode 100644
index 00000000..6b157f73
--- /dev/null
+++ b/tests/utils/qgis_interface.py
@@ -0,0 +1,237 @@
+"""QGIS plugin implementation.
+
+.. note:: This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+.. note:: This source code was copied from the 'postgis viewer' application
+ with original authors:
+ Copyright (c) 2010 by Ivan Mincik, ivan.mincik@gista.sk
+ Copyright (c) 2011 German Carrillo, geotux_tuxman@linuxmail.org
+ Copyright (c) 2014 Tim Sutton, tim@linfiniti.com
+
+"""
+
+__author__ = "tim@linfiniti.com"
+__revision__ = "$Format:%H$"
+__date__ = "10/01/2011"
+__copyright__ = (
+ "Copyright (c) 2010 by Ivan Mincik, ivan.mincik@gista.sk and "
+ "Copyright (c) 2011 German Carrillo, geotux_tuxman@linuxmail.org"
+ "Copyright (c) 2014 Tim Sutton, tim@linfiniti.com"
+)
+
+import logging
+from typing import List
+from PyQt5.QtCore import QObject, pyqtSlot, pyqtSignal, QSize
+from qgis.PyQt.QtWidgets import QDockWidget
+from qgis.core import QgsProject, QgsMapLayer
+from qgis.gui import QgsMapCanvas, QgsMessageBar
+
+LOGGER = logging.getLogger("QGIS")
+
+
+# noinspection PyMethodMayBeStatic,PyPep8Naming
+# pylint: disable=too-many-public-methods
+class QgisInterface(QObject):
+ """Class to expose QGIS objects and functions to plugins.
+
+ This class is here for enabling us to run unit tests only,
+ so most methods are simply stubs.
+ """
+
+ currentLayerChanged = pyqtSignal(QgsMapLayer)
+
+ def __init__(self, canvas: QgsMapCanvas):
+ """Constructor
+ :param canvas:
+ """
+ QObject.__init__(self)
+ self.canvas = canvas
+ # Set up slots so we can mimic the behaviour of QGIS when layers
+ # are added.
+ LOGGER.debug("Initialising canvas...")
+ # noinspection PyArgumentList
+ QgsProject.instance().layersAdded.connect(self.addLayers)
+ # noinspection PyArgumentList
+ QgsProject.instance().layerWasAdded.connect(self.addLayer)
+ # noinspection PyArgumentList
+ QgsProject.instance().removeAll.connect(self.removeAllLayers)
+
+ # For processing module
+ self.destCrs = None
+
+ self.message_bar = QgsMessageBar()
+
+ def addLayers(self, layers: List[QgsMapLayer]):
+ """Handle layers being added to the registry so they show up in canvas.
+
+ :param layers: list list of map layers that were added
+
+ .. note:: The QgsInterface api does not include this method,
+ it is added here as a helper to facilitate testing.
+ """
+ # LOGGER.debug('addLayers called on qgis_interface')
+ # LOGGER.debug('Number of layers being added: %s' % len(layers))
+ # LOGGER.debug('Layer Count Before: %s' % len(self.canvas.layers()))
+ current_layers = self.canvas.layers()
+ final_layers = []
+ for layer in current_layers:
+ final_layers.append(layer)
+ for layer in layers:
+ final_layers.append(layer)
+
+ self.canvas.setLayers(final_layers)
+ # LOGGER.debug('Layer Count After: %s' % len(self.canvas.layers()))
+
+ def addLayer(self, layer: QgsMapLayer):
+ """Handle a layer being added to the registry so it shows up in canvas.
+
+ :param layer: list list of map layers that were added
+
+ .. note: The QgsInterface api does not include this method, it is added
+ here as a helper to facilitate testing.
+
+ .. note: The addLayer method was deprecated in QGIS 1.8 so you should
+ not need this method much.
+ """
+ pass # pylint: disable=unnecessary-pass
+
+ @pyqtSlot()
+ def removeAllLayers(self): # pylint: disable=no-self-use
+ """Remove layers from the canvas before they get deleted."""
+ self.canvas.setLayers([])
+
+ def newProject(self): # pylint: disable=no-self-use
+ """Create new project."""
+ # noinspection PyArgumentList
+ QgsProject.instance().clear()
+
+ # ---------------- API Mock for QgsInterface follows -------------------
+
+ def zoomFull(self):
+ """Zoom to the map full extent."""
+ pass # pylint: disable=unnecessary-pass
+
+ def zoomToPrevious(self):
+ """Zoom to previous view extent."""
+ pass # pylint: disable=unnecessary-pass
+
+ def zoomToNext(self):
+ """Zoom to next view extent."""
+ pass # pylint: disable=unnecessary-pass
+
+ def zoomToActiveLayer(self):
+ """Zoom to extent of active layer."""
+ pass # pylint: disable=unnecessary-pass
+
+ def addVectorLayer(self, path: str, base_name: str, provider_key: str):
+ """Add a vector layer.
+
+ :param path: Path to layer.
+ :type path: str
+
+ :param base_name: Base name for layer.
+ :type base_name: str
+
+ :param provider_key: Provider key e.g. 'ogr'
+ :type provider_key: str
+ """
+ pass # pylint: disable=unnecessary-pass
+
+ def addRasterLayer(self, path: str, base_name: str):
+ """Add a raster layer given a raster layer file name
+
+ :param path: Path to layer.
+ :type path: str
+
+ :param base_name: Base name for layer.
+ :type base_name: str
+ """
+ pass # pylint: disable=unnecessary-pass
+
+ def activeLayer(self) -> QgsMapLayer: # pylint: disable=no-self-use
+ """Get pointer to the active layer (layer selected in the legend)."""
+ # noinspection PyArgumentList
+ layers = QgsProject.instance().mapLayers()
+ for item in layers:
+ return layers[item]
+
+ def addToolBarIcon(self, action):
+ """Add an icon to the plugins toolbar.
+
+ :param action: Action to add to the toolbar.
+ :type action: QAction
+ """
+ pass # pylint: disable=unnecessary-pass
+
+ def removeToolBarIcon(self, action):
+ """Remove an action (icon) from the plugin toolbar.
+
+ :param action: Action to add to the toolbar.
+ :type action: QAction
+ """
+ pass # pylint: disable=unnecessary-pass
+
+ def addToolBar(self, name):
+ """Add toolbar with specified name.
+
+ :param name: Name for the toolbar.
+ :type name: str
+ """
+ pass # pylint: disable=unnecessary-pass
+
+ def mapCanvas(self) -> QgsMapCanvas:
+ """Return a pointer to the map canvas."""
+ return self.canvas
+
+ def mainWindow(self):
+ """Return a pointer to the main window.
+
+ In case of QGIS it returns an instance of QgisApp.
+ """
+ pass # pylint: disable=unnecessary-pass
+
+ def addDockWidget(self, area, dock_widget: QDockWidget):
+ """Add a dock widget to the main window.
+
+ :param area: Where in the ui the dock should be placed.
+ :type area:
+
+ :param dock_widget: A dock widget to add to the UI.
+ :type dock_widget: QDockWidget
+ """
+ pass # pylint: disable=unnecessary-pass
+
+ def removeDockWidget(self, dock_widget: QDockWidget):
+ """Remove a dock widget to the main window.
+
+ :param area: Where in the ui the dock should be placed.
+ :type area:
+
+ :param dock_widget: A dock widget to add to the UI.
+ :type dock_widget: QDockWidget
+ """
+ pass # pylint: disable=unnecessary-pass
+
+ def legendInterface(self):
+ """Get the legend."""
+ return self.canvas
+
+ def iconSize(self, dockedToolbar) -> int: # pylint: disable=no-self-use
+ """
+ Returns the toolbar icon size.
+ :param dockedToolbar: If True, the icon size
+ for toolbars contained within docks is returned.
+ """
+ if dockedToolbar:
+ return QSize(16, 16)
+
+ return QSize(24, 24)
+
+ def messageBar(self) -> QgsMessageBar:
+ """
+ Return the message bar of the main app
+ """
+ return self.message_bar
diff --git a/tests/utils/utilities.py b/tests/utils/utilities.py
new file mode 100644
index 00000000..54af22b5
--- /dev/null
+++ b/tests/utils/utilities.py
@@ -0,0 +1,101 @@
+"""Common functionality used by regression tests."""
+
+import sys
+import logging
+import os
+import atexit
+from qgis.core import QgsApplication
+from qgis.gui import QgsMapCanvas
+from qgis.PyQt.QtCore import QSize
+from qgis.PyQt.QtWidgets import QWidget
+from qgis.utils import iface
+from tests.utils.qgis_interface import QgisInterface
+
+LOGGER = logging.getLogger("QGIS")
+QGIS_APP = None # Static variable used to hold hand to running QGIS app
+CANVAS = None
+PARENT = None
+IFACE = None
+
+
+def get_qgis_app(cleanup=True):
+ """Start one QGIS application to test against.
+
+ :returns: Handle to QGIS app, canvas, iface and parent. If there are any
+ errors the tuple members will be returned as None.
+ :rtype: (QgsApplication, CANVAS, IFACE, PARENT)
+
+ If QGIS is already running the handle to that app will be returned.
+ """
+
+ global QGIS_APP, PARENT, IFACE, CANVAS # pylint: disable=W0603
+
+ if iface:
+ QGIS_APP = QgsApplication
+ CANVAS = iface.mapCanvas()
+ PARENT = iface.mainWindow()
+ IFACE = iface
+ return QGIS_APP, CANVAS, IFACE, PARENT
+
+ global QGISAPP # pylint: disable=global-variable-undefined
+
+ try:
+ QGISAPP # pylint: disable=used-before-assignment
+ except NameError:
+ myGuiFlag = False # All test will run qgis not in gui mode
+
+ # In python3 we need to convert to a bytes object (or should
+ # QgsApplication accept a QString instead of const char* ?)
+ try:
+ argvb = list(map(os.fsencode, sys.argv))
+ except AttributeError:
+ argvb = sys.argv
+
+ # Note: QGIS_PREFIX_PATH is evaluated in QgsApplication -
+ # no need to mess with it here.
+ QGISAPP = QgsApplication(argvb, myGuiFlag)
+
+ QGISAPP.initQgis()
+ s = QGISAPP.showSettings()
+ LOGGER.debug(s)
+
+ def debug_log_message(message, tag, level):
+ """
+ Prints a debug message to a log
+ :param message: message to print
+ :param tag: log tag
+ :param level: log message level (severity)
+ :return:
+ """
+ print(f"{tag}({level}): {message}")
+
+ QgsApplication.instance().messageLog().messageReceived.connect(debug_log_message)
+
+ if cleanup:
+
+ @atexit.register
+ def exitQgis(): # pylint: disable=unused-variable
+ """
+ Gracefully closes the QgsApplication instance
+ """
+ try:
+ QGISAPP.exitQgis() # noqa: F823
+ QGISAPP = None # noqa: F841
+ except NameError:
+ pass
+
+ if PARENT is None:
+ # noinspection PyPep8Naming
+ PARENT = QWidget()
+
+ if CANVAS is None:
+ # noinspection PyPep8Naming
+ CANVAS = QgsMapCanvas(PARENT)
+ CANVAS.resize(QSize(400, 400))
+
+ if IFACE is None:
+ # QgisInterface is a stub implementation of the QGIS plugin interface
+ # noinspection PyPep8Naming
+ IFACE = QgisInterface(CANVAS)
+
+ return QGISAPP, CANVAS, IFACE, PARENT
From 0131012f56769c91b20e1ee2876c7bb920b537a3 Mon Sep 17 00:00:00 2001
From: Till Frankenbach
Date: Tue, 27 Aug 2024 15:15:48 +0200
Subject: [PATCH 26/26] fix: alert on duplicate points traveling salesman
(#275)
Co-authored-by: Jakob Schnell
---
ORStools/gui/ORStoolsDialog.py | 52 +++++++++++-
ORStools/i18n/orstools_de.ts | 84 +++++++++----------
ORStools/proc/directions_points_layer_proc.py | 9 ++
ORStools/utils/exceptions.py | 8 ++
4 files changed, 109 insertions(+), 44 deletions(-)
diff --git a/ORStools/gui/ORStoolsDialog.py b/ORStools/gui/ORStoolsDialog.py
index 95cefdb5..4d6a4c70 100644
--- a/ORStools/gui/ORStoolsDialog.py
+++ b/ORStools/gui/ORStoolsDialog.py
@@ -54,7 +54,7 @@
from qgis.gui import QgsMapCanvasAnnotationItem
from qgis.PyQt.QtCore import QSizeF, QPointF, QCoreApplication
-from qgis.PyQt.QtGui import QIcon, QTextDocument
+from qgis.PyQt.QtGui import QIcon, QTextDocument, QColor
from qgis.PyQt.QtWidgets import (
QAction,
QDialog,
@@ -106,6 +106,8 @@ def on_help_click() -> None:
def on_about_click(parent: QWidget) -> None:
"""Slot for click event of About button/menu entry."""
+ # ruff will add trailing comma to last string line which breaks pylupdate5
+ # fmt: off
info = QCoreApplication.translate(
"@default",
'ORS Tools provides access to None:
'Web: {2}
'
'Repo: '
"github.com/GIScience/orstools-qgis-plugin
"
- "Version: {3}",
+ "Version: {3}"
).format(DEFAULT_COLOR, __email__, __web__, __version__)
+ # fmt: on
QMessageBox.information(
parent, QCoreApplication.translate("@default", "About {}").format(PLUGIN_NAME), info
@@ -324,6 +327,27 @@ def run_gui_control(self) -> None:
try:
params = directions.get_parameters()
if self.dlg.optimization_group.isChecked():
+ # check for duplicate points
+ points = [
+ self.dlg.routing_fromline_list.item(x).text()
+ for x in range(self.dlg.routing_fromline_list.count())
+ ]
+ if len(points) != len(set(points)):
+ QMessageBox.warning(
+ self.dlg,
+ self.tr("Duplicates"),
+ self.tr(
+ """
+ There are duplicate points in the input layer. Traveling Salesman Optimization does not allow this.
+ Either remove the duplicates or deselect Traveling Salesman.
+ """
+ ),
+ )
+ msg = self.tr("The request has been aborted!")
+ logger.log(msg, 0)
+ self.dlg.debug_text.setText(msg)
+ return
+
if len(params["jobs"]) <= 1: # Start/end locations don't count as job
QMessageBox.critical(
self.dlg,
@@ -494,6 +518,14 @@ def __init__(self, iface: QgisInterface, parent=None) -> None:
self.routing_fromline_list.model().rowsMoved.connect(self._reindex_list_items)
self.routing_fromline_list.model().rowsRemoved.connect(self._reindex_list_items)
+ # Connect signals to the color_duplicate_items function
+ self.routing_fromline_list.model().rowsRemoved.connect(
+ lambda: self.color_duplicate_items(self.routing_fromline_list)
+ )
+ self.routing_fromline_list.model().rowsInserted.connect(
+ lambda: self.color_duplicate_items(self.routing_fromline_list)
+ )
+
self.annotation_canvas = self._iface.mapCanvas()
def _save_vertices_to_layer(self) -> None:
@@ -637,3 +669,19 @@ def _on_linetool_map_doubleclick(self) -> None:
QApplication.restoreOverrideCursor()
self._iface.mapCanvas().setMapTool(self.last_maptool)
self.show()
+
+ def color_duplicate_items(self, list_widget):
+ item_dict = {}
+ for index in range(list_widget.count()):
+ item = list_widget.item(index)
+ text = item.text()
+ if text in item_dict:
+ item_dict[text].append(index)
+ else:
+ item_dict[text] = [index]
+
+ for indices in item_dict.values():
+ if len(indices) > 1:
+ for index in indices:
+ item = list_widget.item(index)
+ item.setBackground(QColor("lightsalmon"))
diff --git a/ORStools/i18n/orstools_de.ts b/ORStools/i18n/orstools_de.ts
index a3bd32b9..4325a73a 100644
--- a/ORStools/i18n/orstools_de.ts
+++ b/ORStools/i18n/orstools_de.ts
@@ -3,16 +3,16 @@
@default
-
-
-
- <b>ORS Tools</b> bietet Zugriff auf <a href="https://openrouteservice.org" style="color: {0}">openrouteservice</a> Berechnungen.<br><br><center><a href="https://heigit.org/de/willkommen"><img src=":/plugins/ORStools/img/logo_heigit_300.png"/></a><br><br></center>Author: HeiGIT gGmbH<br>Email: <a href="mailto:Openrouteservice <{1}>">{1}</a><br>Web: <a href="{2}">{2}</a><br>Repo: <a href="https://github.com/GIScience/orstools-qgis-plugin">github.com/GIScience/orstools-qgis-plugin</a><br>Version: {3}
-
Über {}
+
+
+
+ <b>ORS Tools</b> bietet Zugriff auf <a href="https://openrouteservice.org" style="color: {0}">openrouteservice</a> Berechnungen.<br><br><center><a href="https://heigit.org/de/willkommen"><img src=":/plugins/ORStools/img/logo_heigit_300.png"/></a><br><br></center>Author: HeiGIT gGmbH<br>Email: <a href="mailto:Openrouteservice <{1}>">{1}</a><br>Web: <a href="{2}">{2}</a><br>Repo: <a href="https://github.com/GIScience/orstools-qgis-plugin">github.com/GIScience/orstools-qgis-plugin</a><br>Version: {3}
+
ORSBaseProcessingAlgorithm
@@ -79,7 +79,7 @@
Wegpunktoptimierung (sonstige Konfiguration wird nicht berücksichtigt)
-
+
Routenberechnung aus einem Polyline-Layer
@@ -104,34 +104,6 @@
Csv Spalte (benötigt Csv Faktor und csv in Extra Info)
-
- ORSDirectionsLinesAlgorithm
-
-
-
- Eingabelayer (Linien)
-
-
-
-
- ID-Attribut
-
-
-
-
- Routenpräferenz
-
-
-
-
- Wegpunktoptimierung (sonstige Konfiguration wird nicht berücksichtigt)
-
-
-
-
- Routenberechnung aus einem Polyline-Layer
-
-
ORSDirectionsPointsLayerAlgo
@@ -155,7 +127,7 @@
Wegpunktoptimierung (sonstige Konfiguration wird nicht berücksichtigt)
-
+
Routenberechnung aus einem Punkt-Layer
@@ -165,7 +137,7 @@
ID-Attribut (zum Beispiel für joins)
-
+
Reihenfolge exportieren
@@ -175,15 +147,24 @@
Extra Info
-
+
Csv Faktor (benötigt Csv Spalte und csv in Extra Info)
-
+
Csv Spalte (benötigt Csv Faktor und csv in Extra Info)
+
+
+
+ Das Eingabelayer enthält duplizierte Punkte. Dies ist mit der Wegpunktoptimierung nicht erlaubt.
+Duplikate entfernen oder Wegpunktoptimierung abwählen.
+
ORSDirectionsPointsLayersAlgo
@@ -228,7 +209,7 @@
Zuordnungs-Verfahren
-
+
Routenberechnung aus zwei Punkt-Layern
@@ -350,12 +331,12 @@
ORStoolsDialog
-
+
Anwenden
-
+
Schließen
@@ -462,7 +443,7 @@ p, li { white-space: pre-wrap; }
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
<html><head><meta name="qrichtext" content="1" /><style type="text/css">
p, li { white-space: pre-wrap; }
-</style></head><body style=" font-family:'Ubuntu'; font-size:11pt; font-weight:400; font-style:normal;">
+</style></head><body style=" font-family:'Ubuntu'; font-size:11pt; font-weight:400; font-style:normal;">
<p style=" padding: 10px; -qt-block-indent:0; text-indent:0px ; background-color:#e7f2fa; color: #999999"><img stype="margin: 10px" src=":/plugins/ORStools/img/icon_about.png" width=16 height=16 /> Sämtliche Einstellungen werden überschrieben</p></body></html>
@@ -729,5 +710,24 @@ p, li { white-space: pre-wrap; }
Über
+
+
+
+ Duplikate
+
+
+
+
+ Das Eingabelayer enthält duplizierte Punkte. Dies ist mit der Wegpunktoptimierung nicht erlaubt.
+Duplikate entfernen oder Wegpunktoptimierung abwählen.
+
+
+
+
+ Die Anfrage wurde abgebrochen!
+
diff --git a/ORStools/proc/directions_points_layer_proc.py b/ORStools/proc/directions_points_layer_proc.py
index 68f0e598..f3544b08 100644
--- a/ORStools/proc/directions_points_layer_proc.py
+++ b/ORStools/proc/directions_points_layer_proc.py
@@ -218,6 +218,15 @@ def sort(f):
try:
if optimization_mode is not None:
+ # check for duplicate points
+ if len(points) != len(set(points)):
+ raise exceptions.DuplicateError(
+ self.tr("""
+ There are duplicate points in the input layer. Traveling Salesman Optimization does not allow this.
+ Either remove the duplicates or deselect Traveling Salesman.
+ """)
+ )
+
params = get_params_optimize(points, profile, optimization_mode)
response = ors_client.request("/optimization", {}, post_json=params)
diff --git a/ORStools/utils/exceptions.py b/ORStools/utils/exceptions.py
index ac8fe7d7..177a7b46 100644
--- a/ORStools/utils/exceptions.py
+++ b/ORStools/utils/exceptions.py
@@ -92,3 +92,11 @@ def __str__(self):
return self.status
else:
return f"{self.status} ({self.message})"
+
+
+class DuplicateError(Exception):
+ def __init__(self, message=None):
+ self.message = message
+
+ def __str__(self):
+ return self.message