From 16bd1d75a03bc1e4539de50fe4776d3aef796b2e Mon Sep 17 00:00:00 2001
From: josidd <joseph.siddons@noc.ac.uk>
Date: Thu, 10 Oct 2024 15:37:04 +0100
Subject: [PATCH] refactor(quadtree)!: Use bounding box definition for
 Rectangle class

---
 GeoSpatialTools/quadtree.py | 106 +++++++++++++++++-------------------
 test/test_quadtree.py       |  51 ++++++++++-------
 2 files changed, 81 insertions(+), 76 deletions(-)

diff --git a/GeoSpatialTools/quadtree.py b/GeoSpatialTools/quadtree.py
index 1bf7e7d..b7cee97 100644
--- a/GeoSpatialTools/quadtree.py
+++ b/GeoSpatialTools/quadtree.py
@@ -90,59 +90,55 @@ class Rectangle:
 
     Parameters
     ----------
-    lon : float
-        Horizontal centre of the rectangle
-    lat : float
-        Vertical centre of the rectangle
-    lon_range : float
-        Width of the rectangle
-    lat_range : float
-        Height of the rectangle
+    west : float
+        Western boundary of the Rectangle
+    east : float
+        Eastern boundary of the Rectangle
+    south : float
+        Southern boundary of the Rectangle
+    north : float
+        Northern boundary of the Rectangle
     """
 
-    lon: float
-    lat: float
-    lon_range: float
-    lat_range: float
+    west: float
+    east: float
+    south: float
+    north: float
 
     def __post_init__(self):
-        if self.lon > 180:
-            self.lon -= 360
-        if self.lat > 90 or self.lat < -90:
+        if self.east > 180 or self.east < -180:
+            self.east = ((self.east + 540) % 360) - 180
+        if self.west > 180 or self.west < -180:
+            self.west = ((self.west + 540) % 360) - 180
+        if self.north > 90 or self.south < -90:
             raise LatitudeError(
-                f"Central latitude value out of range {self.lat}, "
-                + "should be between -90, 90 degrees"
+                "Latitude bounds are out of bounds. "
+                + f"{self.north = }, {self.south = }"
             )
 
     @property
-    def west(self) -> float:
-        """Western boundary of the Rectangle"""
-        return (((self.lon - self.lon_range / 2) + 540) % 360) - 180
+    def lat_range(self) -> float:
+        """Latitude range of the Rectangle"""
+        return self.north - self.south
 
     @property
-    def east(self) -> float:
-        """Eastern boundary of the Rectangle"""
-        return (((self.lon + self.lon_range / 2) + 540) % 360) - 180
+    def lat(self) -> float:
+        """Centre latitude of the Rectangle"""
+        return self.south + self.lat_range / 2
 
     @property
-    def north(self) -> float:
-        """Northern boundary of the Rectangle"""
-        north = self.lat + self.lat_range / 2
-        if north > 90:
-            raise LatitudeError(
-                "Rectangle crosses north pole - Use two Rectangles"
-            )
-        return north
+    def lon_range(self) -> float:
+        """Longitude range of the Rectangle"""
+        if self.east < self.west:
+            return self.east - self.west + 360
+
+        return self.east - self.west
 
     @property
-    def south(self) -> float:
-        """Southern boundary of the Rectangle"""
-        south = self.lat - self.lat_range / 2
-        if south < -90:
-            raise LatitudeError(
-                "Rectangle crosses south pole - Use two Rectangles"
-            )
-        return south
+    def lon(self) -> float:
+        """Centre longitude of the Rectangle"""
+        lon = self.west + self.lon_range / 2
+        return ((lon + 540) % 360) - 180
 
     @property
     def edge_dist(self) -> float:
@@ -332,10 +328,10 @@ class QuadTree:
         """Divide the QuadTree"""
         self.northwest = QuadTree(
             Rectangle(
-                self.boundary.lon - self.boundary.lon_range / 4,
-                self.boundary.lat + self.boundary.lat_range / 4,
-                self.boundary.lon_range / 2,
-                self.boundary.lat_range / 2,
+                self.boundary.west,
+                self.boundary.lon,
+                self.boundary.lat,
+                self.boundary.north,
             ),
             capacity=self.capacity,
             depth=self.depth + 1,
@@ -343,10 +339,10 @@ class QuadTree:
         )
         self.northeast = QuadTree(
             Rectangle(
-                self.boundary.lon + self.boundary.lon_range / 4,
-                self.boundary.lat + self.boundary.lat_range / 4,
-                self.boundary.lon_range / 2,
-                self.boundary.lat_range / 2,
+                self.boundary.lon,
+                self.boundary.east,
+                self.boundary.lat,
+                self.boundary.north,
             ),
             capacity=self.capacity,
             depth=self.depth + 1,
@@ -354,10 +350,10 @@ class QuadTree:
         )
         self.southwest = QuadTree(
             Rectangle(
-                self.boundary.lon - self.boundary.lon_range / 4,
-                self.boundary.lat - self.boundary.lat_range / 4,
-                self.boundary.lon_range / 2,
-                self.boundary.lat_range / 2,
+                self.boundary.west,
+                self.boundary.lon,
+                self.boundary.south,
+                self.boundary.lat,
             ),
             capacity=self.capacity,
             depth=self.depth + 1,
@@ -365,10 +361,10 @@ class QuadTree:
         )
         self.southeast = QuadTree(
             Rectangle(
-                self.boundary.lon + self.boundary.lon_range / 4,
-                self.boundary.lat - self.boundary.lat_range / 4,
-                self.boundary.lon_range / 2,
-                self.boundary.lat_range / 2,
+                self.boundary.lon,
+                self.boundary.east,
+                self.boundary.south,
+                self.boundary.lat,
             ),
             capacity=self.capacity,
             depth=self.depth + 1,
diff --git a/test/test_quadtree.py b/test/test_quadtree.py
index e171b37..1accca4 100644
--- a/test/test_quadtree.py
+++ b/test/test_quadtree.py
@@ -7,7 +7,7 @@ from GeoSpatialTools.quadtree import QuadTree, Record, Rectangle, Ellipse
 
 class TestRect(unittest.TestCase):
     def test_contains(self):
-        rect = Rectangle(10, 5, 20, 10)
+        rect = Rectangle(0, 20, 0, 10)
         points: list[Record] = [
             Record(10, 5),
             Record(20, 10),
@@ -20,20 +20,20 @@ class TestRect(unittest.TestCase):
         assert res == expected
 
     def test_intersection(self):
-        rect = Rectangle(10, 5, 20, 10)
+        rect = Rectangle(0, 20, 0, 10)
         test_rects: list[Rectangle] = [
-            Rectangle(10, 5, 18, 8),
-            Rectangle(25, 5, 9, 12),
-            Rectangle(15, 8, 12, 7),
+            Rectangle(1, 19, 1, 9),
+            Rectangle(20.5, 29.5, -1, 11),
+            Rectangle(9, 21, 4.5, 11.5),
         ]
         expected = [True, False, True]
         res = list(map(rect.intersects, test_rects))
         assert res == expected
 
     def test_wrap(self):
-        rect = Rectangle(170, 45, 180, 20)
-        assert rect.east < 0
-        assert rect.west > 0
+        rect = Rectangle(80, -100, 35, 55)
+        assert rect.lon == 170
+        assert rect.lat == 45
         test_points: list[Record] = [
             Record(-140, 40),
             Record(0, 50),
@@ -43,20 +43,19 @@ class TestRect(unittest.TestCase):
         res = list(map(rect.contains, test_points))
         assert res == expected
 
-        test_rect = Rectangle(-100, 40, 80, 40)
-        assert test_rect.east < rect.west
+        test_rect = Rectangle(-140, -60, 20, 60)
         assert rect.intersects(test_rect)
 
 
 class TestQuadTree(unittest.TestCase):
     def test_divides(self):
-        boundary = Rectangle(10, 4, 20, 8)
+        boundary = Rectangle(0, 20, 0, 8)
         qtree = QuadTree(boundary)
         expected: list[Rectangle] = [
-            Rectangle(5, 6, 10, 4),
-            Rectangle(15, 6, 10, 4),
-            Rectangle(5, 2, 10, 4),
-            Rectangle(15, 2, 10, 4),
+            Rectangle(0, 10, 4, 8),
+            Rectangle(10, 20, 4, 8),
+            Rectangle(0, 10, 0, 4),
+            Rectangle(10, 20, 0, 4),
         ]
         qtree.divide()
         res = [
@@ -68,7 +67,7 @@ class TestQuadTree(unittest.TestCase):
         assert res == expected
 
     def test_insert(self):
-        boundary = Rectangle(10, 4, 20, 8)
+        boundary = Rectangle(0, 20, 0, 8)
         qtree = QuadTree(boundary, capacity=3)
         points: list[Record] = [
             Record(10, 5),
@@ -97,7 +96,7 @@ class TestQuadTree(unittest.TestCase):
         assert res == expected
 
     def test_query(self):
-        boundary = Rectangle(10, 4, 20, 8)
+        boundary = Rectangle(0, 20, 0, 8)
         qtree = QuadTree(boundary, capacity=3)
         points: list[Record] = [
             Record(10, 5),
@@ -106,7 +105,7 @@ class TestQuadTree(unittest.TestCase):
             Record(-2, -9.2),
             Record(12.8, 2.1),
         ]
-        test_rect = Rectangle(12.5, 2.5, 1, 1)
+        test_rect = Rectangle(12, 13, 2, 3)
         expected = [Record(12.8, 2.1)]
 
         for point in points:
@@ -118,10 +117,20 @@ class TestQuadTree(unittest.TestCase):
 
     def test_wrap_query(self):
         N = 100
-        qt_boundary = Rectangle(0, 0, 360, 180)
+        qt_boundary = Rectangle(-180, 180, -90, 90)
+        assert qt_boundary.lon == 0
+        assert qt_boundary.lon_range == 360
+        assert qt_boundary.lat == 0
+        assert qt_boundary.lat_range == 180
+
         qt = QuadTree(qt_boundary, capacity=3)
 
-        quert_rect = Rectangle(170, 45, 60, 10)
+        quert_rect = Rectangle(140, -160, 40, 50)
+        assert quert_rect.lon == 170
+        assert quert_rect.lon_range == 60
+        assert quert_rect.lat == 45
+        assert quert_rect.lat_range == 10
+
         points_want: list[Record] = [
             Record(175, 43),
             Record(-172, 49),
@@ -163,7 +172,7 @@ class TestQuadTree(unittest.TestCase):
         assert not ellipse.contains(Record(12.5, 3.01))
         assert not ellipse.contains(Record(12.5, 1.99))
 
-        boundary = Rectangle(10, 4, 20, 8)
+        boundary = Rectangle(0, 20, 0, 8)
         qtree = QuadTree(boundary, capacity=3)
         points: list[Record] = [
             Record(10, 5),
-- 
GitLab