From 0e142990308c34360f2775b73ac74a3faf5ab6a3 Mon Sep 17 00:00:00 2001 From: "Yann E. MORIN" Date: Mon, 15 Jul 2024 13:20:28 +0200 Subject: [PATCH 1/5] python/its-quadkeys: fix type hint for QuadZone PEP484 [0] mandates that, for arbitrary argument lists (e.g. *args and **kwargs), the type for the individual elements whould be hinted, and while *args is a list (and **wargs a dictionary), it's the items in the list (the values of the jeys in the dict) that have to be type hinted, e.g. (from the PEP itself): def foo(*args: str, **kwargs: int): ... specifies that foo takes any number of positional arguments (*args) that are all str, and any number of keyword arguments that are all int. Thus, the current type hint for the QuadZone is wrong. What we really wanted is to be able to pass an arbitrary number of any of those types; - a string representing a QuadKey - a QuadKey object - a list of strings each representing a QuadKey, or of QuadKey objects So, all the following calls are expected to be valid and equivalent: * QuadZone() * QuadZone("0", "1", "2") * QuadZone(["0", "1", "2"]) * QuadZone(["0", "1"], "2") * QuadZone(["0", "1"], ["2"]) * QuadZone(QuadKey("0"), QuadKey("1"), QuadKey("2")) * QuadZone([QuadKey("0"), QuadKey("1"), QuadKey("2")]) * QuadZone([QuadKey("0"), QuadKey("1)"], QuadKey("2")) * QuadZone([QuadKey("0"), QuadKey("1)"], [QuadKey("2")]) * QuadZone(QuadKey("0"), ["1", "2"]) * QuadZone(QuadKey("0"), "1", [QuadKey("2")]) * and so on... [0] https://peps.python.org/pep-0484/#arbitrary-argument-lists-and-default-argument-values Reported-by: GARDES Frederic INNOV/IT-S Signed-off-by: Yann E. MORIN --- python/its-quadkeys/its_quadkeys/quadkeys.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/its-quadkeys/its_quadkeys/quadkeys.py b/python/its-quadkeys/its_quadkeys/quadkeys.py index 20c408e0..899d31a6 100644 --- a/python/its-quadkeys/its_quadkeys/quadkeys.py +++ b/python/its-quadkeys/its_quadkeys/quadkeys.py @@ -293,7 +293,7 @@ def __south_east_of_s(q: str): class QuadZone: - def __init__(self, *args: list[QuadKey | str | list[QuadKey | str]]): + def __init__(self, *args: QuadKey | str | list[QuadKey | str]): """Create a new QuadZone from an iterable of QuadKeys""" self.quadkeys = set() for arg in args: From 1c21e5bad2e3c50bf2d2f3489b3da67980b4beb8 Mon Sep 17 00:00:00 2001 From: "Yann E. MORIN" Date: Mon, 8 Jul 2024 13:16:40 +0200 Subject: [PATCH 2/5] python/its-quadkeys: a quadkey can't be a zero-length string Signed-off-by: Yann E. MORIN --- python/its-quadkeys/its_quadkeys/quadkeys.py | 2 ++ python/its-quadkeys/quadkeys-test | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/python/its-quadkeys/its_quadkeys/quadkeys.py b/python/its-quadkeys/its_quadkeys/quadkeys.py index 899d31a6..28edfbe1 100644 --- a/python/its-quadkeys/its_quadkeys/quadkeys.py +++ b/python/its-quadkeys/its_quadkeys/quadkeys.py @@ -49,6 +49,8 @@ def __init__( f"cannot create a QuadKey from a {type(quadkey)}={quadkey!s}" ) qk = quadkey.replace(separator, "") + if not qk: + raise ValueError("QuadKey can't be zero-length") err = "".join(set([q for q in qk if q not in "0123"])) if err: raise ValueError(f"QuadKey can oly contain '0123', not any of '{err}'") diff --git a/python/its-quadkeys/quadkeys-test b/python/its-quadkeys/quadkeys-test index 561e0c43..8c6a1ef2 100755 --- a/python/its-quadkeys/quadkeys-test +++ b/python/its-quadkeys/quadkeys-test @@ -53,6 +53,15 @@ def test_quadkey(): # fmt: on ] + print("QuadKey empty: ", end="", flush=True) + try: + _ = its_quadkeys.QuadKey('') + except ValueError: + pass # We expected that + else: + raise FailedError("QuadKey cannot be empty") from None + print("OK") + print(f"QuadKey in: ", end="", flush=True) if qk_in not in qk: raise FailedError( From 5f1004523d23a50523eb448328540ee486288138 Mon Sep 17 00:00:00 2001 From: "Yann E. MORIN" Date: Mon, 8 Jul 2024 13:18:00 +0200 Subject: [PATCH 3/5] python/its-quadkeys: return the actually shallower quadkey To make a shallower quadkey, the new depth was computed so that the new quadkey would be at least of depth 1, i.e. always with at least one digit. However, the new quadkey was not built from that depth, but from the depth of the original quadkey. That means that, when a quadkey of depth 1 was made shallower, an empty quadkey was returned, which is invalid. Fix that by using the proper depth. Fixes: #130. Reported-by: GARDES Frederic INNOV/IT-S Signed-off-by: Yann E. MORIN --- python/its-quadkeys/its_quadkeys/quadkeys.py | 2 +- python/its-quadkeys/quadkeys-test | 22 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/python/its-quadkeys/its_quadkeys/quadkeys.py b/python/its-quadkeys/its_quadkeys/quadkeys.py index 28edfbe1..631447fd 100644 --- a/python/its-quadkeys/its_quadkeys/quadkeys.py +++ b/python/its-quadkeys/its_quadkeys/quadkeys.py @@ -89,7 +89,7 @@ def make_shallower(self, depth: int): new_depth = max(1, len(self.quadkey) + depth) else: new_depth = min(len(self.quadkey), depth) - return QuadKey(self.quadkey[:depth]) + return QuadKey(self.quadkey[:new_depth]) def split(self, *, depth: int = None, extra_depth: int = None): """Split this QuadKey into an extra_depth-deeper QuadZone""" diff --git a/python/its-quadkeys/quadkeys-test b/python/its-quadkeys/quadkeys-test index 8c6a1ef2..bb764383 100755 --- a/python/its-quadkeys/quadkeys-test +++ b/python/its-quadkeys/quadkeys-test @@ -52,6 +52,19 @@ def test_quadkey(): "1203021330", "1203021331", "1203021332", "1203021333", # fmt: on ] + expected_shallowers = [ + # fmt: off + (0, "12030213"), + (-1, "1203021"), + (-2, "120302"), + (-5, "120"), + (-100, "1"), + (1, "1"), + (2, "12"), + (5, "12030"), + (100, "12030213"), + # fmt: on + ] print("QuadKey empty: ", end="", flush=True) try: @@ -85,6 +98,15 @@ def test_quadkey(): check_zone(qk_split_z, expected_split_z_2) print(f"OK") + print(f"QuadKey shallower: ", end="", flush=True) + for depth, expected in expected_shallowers: + qk_shallow = qk.make_shallower(depth) + if qk_shallow != expected: + raise FailedError( + f"with QuadKey '{qk}' for depth {depth}, expecting '{expected}', got '{qk_shallow}'" + ) + print(f"OK") + print(f"QuadKey neighbours: ", end="", flush=True) nghbs = qk.neighbours() if nghbs._asdict() != expected_nghbs: From dd61a7bef98a445336ac610bf55550d84924532f Mon Sep 17 00:00:00 2001 From: "Yann E. MORIN" Date: Mon, 8 Jul 2024 13:25:20 +0200 Subject: [PATCH 4/5] python/its-quadkeys: new method to get the root of a quadkey The root of a quadkey Q is the quadkey q which depth is exactly one less that the depth of Q, unless Q's depth is already one, in which case it has no root. This will help callers to detect whether their quadkeys was the top-most one easily. Signed-off-by: Yann E. MORIN --- python/its-quadkeys/its_quadkeys/quadkeys.py | 5 +++++ python/its-quadkeys/quadkeys-test | 16 ++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/python/its-quadkeys/its_quadkeys/quadkeys.py b/python/its-quadkeys/its_quadkeys/quadkeys.py index 631447fd..adc99468 100644 --- a/python/its-quadkeys/its_quadkeys/quadkeys.py +++ b/python/its-quadkeys/its_quadkeys/quadkeys.py @@ -91,6 +91,11 @@ def make_shallower(self, depth: int): new_depth = min(len(self.quadkey), depth) return QuadKey(self.quadkey[:new_depth]) + def root(self): + """Returns the QuadKey immediately shallower, or None if this QuadKey + is already the shallowest.""" + return None if len(self.quadkey) == 1 else self.make_shallower(-1) + def split(self, *, depth: int = None, extra_depth: int = None): """Split this QuadKey into an extra_depth-deeper QuadZone""" if (depth is None and extra_depth is None) or (depth and extra_depth): diff --git a/python/its-quadkeys/quadkeys-test b/python/its-quadkeys/quadkeys-test index bb764383..c3b218be 100755 --- a/python/its-quadkeys/quadkeys-test +++ b/python/its-quadkeys/quadkeys-test @@ -65,6 +65,13 @@ def test_quadkey(): (100, "12030213"), # fmt: on ] + expected_roots = [ + # fmt: off + ("123123", "12312"), + ("12", "1"), + ("1", None), + # fmt: on + ] print("QuadKey empty: ", end="", flush=True) try: @@ -107,6 +114,15 @@ def test_quadkey(): ) print(f"OK") + print(f"QuadKey root: ", end="", flush=True) + for qk_s, expected in expected_roots: + qk_r = its_quadkeys.QuadKey(qk_s).root() + if qk_r != expected: + raise FailedError( + f"for QuadKey '{qk_s}', expecting root '{expected}', got '{qk_r}'" + ) + print("OK") + print(f"QuadKey neighbours: ", end="", flush=True) nghbs = qk.neighbours() if nghbs._asdict() != expected_nghbs: From 7bd655a8fbf213db0b1b71d4d6a32a12ee7923e9 Mon Sep 17 00:00:00 2001 From: "Yann E. MORIN" Date: Mon, 8 Jul 2024 13:27:57 +0200 Subject: [PATCH 5/5] python/its-quadkeys: don't merge the shallowest quadkeys When we consider quadkeys for merging, we need to avoid merging the top-most quadkeys, i.e. the whole-Earth set [0, 1, 2, 3]. Fixes: #130 Reported-by: GARDES Frederic INNOV/IT-S Signed-off-by: Yann E. MORIN --- python/its-quadkeys/its_quadkeys/quadkeys.py | 8 +++++--- python/its-quadkeys/quadkeys-test | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/python/its-quadkeys/its_quadkeys/quadkeys.py b/python/its-quadkeys/its_quadkeys/quadkeys.py index adc99468..35662307 100644 --- a/python/its-quadkeys/its_quadkeys/quadkeys.py +++ b/python/its-quadkeys/its_quadkeys/quadkeys.py @@ -390,11 +390,13 @@ def optimise(self): continue # Are this QuadKey and the following three making a super - # QuadKey? I.e. do we have root0, root1, root2, and root3? + # QuadKey? I.e. do we have 'root0', 'root1', 'root2', and + # 'root3', with a non-empty 'root' (issue #130) qk_depth = quadkey.depth() - root = quadkey.make_shallower(-1) + root = quadkey.root() if ( - to_merge[0] == root + "1" + root + and to_merge[0] == root + "1" and to_merge[1] == root + "2" and to_merge[2] == root + "3" ): diff --git a/python/its-quadkeys/quadkeys-test b/python/its-quadkeys/quadkeys-test index c3b218be..a05f95ee 100755 --- a/python/its-quadkeys/quadkeys-test +++ b/python/its-quadkeys/quadkeys-test @@ -404,6 +404,20 @@ def test_quadzone(): check_zone(z2, expected_xor) print("OK") + earth = its_quadkeys.QuadZone("0", "1", "2", "3") + print("QuadZone Whole-Earth optimise: ", end="", flush=True) + earth2 = its_quadkeys.QuadZone("0", "1", "2", "3") + earth2.optimise() + check_zone(earth2, earth) + print("OK") + + print("QuadZone Whole-Earth 2 optimise: ", end="", flush=True) + earth2_lst = ["0", "1", "2", "30", "31", "32", "330", "331", "332", "333"] + earth2 = its_quadkeys.QuadZone(earth2_lst) + earth2.optimise() + check_zone(earth2, earth) + print("OK") + def check_zone(some, expected): some_l = list(some)