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