Commit 2f72e7f8 authored by Joseph Siddons's avatar Joseph Siddons
Browse files

test(octtree): update tests for OctTree defined by bounding Rectangle

parent 7c9913f5
from dataclasses import dataclass from dataclasses import dataclass
import datetime import datetime
from .distance_metrics import haversine, destination from .distance_metrics import haversine, destination
from .utils import LatitudeError from .utils import LatitudeError, DateWarning
from math import degrees, sqrt from math import degrees, sqrt
from warnings import warn
class SpaceTimeRecord: class SpaceTimeRecord:
...@@ -101,75 +102,64 @@ class SpaceTimeRectangle: ...@@ -101,75 +102,64 @@ class SpaceTimeRectangle:
Parameters Parameters
---------- ----------
lon : float west : float
Horizontal centre of the rectangle (longitude). Western boundary of the Rectangle
lat : float east : float
Vertical centre of the rectangle (latitude). Eastern boundary of the Rectangle
datetime : datetime.datetime south : float
Datetime centre of the rectangle. Southern boundary of the Rectangle
w : float north : float
Width of the rectangle (longitude range). Northern boundary of the Rectangle
h : float start : datetime.datetime
Height of the rectangle (latitude range). Start datetime of the Rectangle
dt : datetime.timedelta end : datetime.datetime
time extent of the rectangle. End datetime of the Rectangle
""" """
lon: float west: float
lat: float east: float
date: datetime.datetime south: float
lon_range: float north: float
lat_range: float start: datetime.datetime
dt: datetime.timedelta end: datetime.datetime
def __post_init__(self): def __post_init__(self):
if self.lon > 180: if self.east > 180 or self.east < -180:
self.lon -= 360 self.east = ((self.east + 540) % 360) - 180
if self.lat > 90 or self.lat < -90: 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( raise LatitudeError(
f"Central latitude value out of range {self.lat}, " "Latitude bounds are out of bounds. "
+ "should be between -90, 90 degrees" + f"{self.north = }, {self.south = }"
) )
if self.end < self.start:
warn("End date is before start date. Swapping", DateWarning)
self.start, self.end = self.end, self.start
@property @property
def west(self) -> float: def lat_range(self) -> float:
"""Western boundary of the Rectangle""" """Latitude range of the Rectangle"""
return (((self.lon - self.lon_range / 2) + 540) % 360) - 180 return self.north - self.south
@property @property
def east(self) -> float: def lat(self) -> float:
"""Eastern boundary of the Rectangle""" """Centre latitude of the Rectangle"""
return (((self.lon + self.lon_range / 2) + 540) % 360) - 180 return self.south + self.lat_range / 2
@property @property
def north(self) -> float: def lon_range(self) -> float:
"""Northern boundary of the Rectangle""" """Longitude range of the Rectangle"""
north = self.lat + self.lat_range / 2 if self.east < self.west:
if north > 90: return self.east - self.west + 360
raise LatitudeError(
"Rectangle crosses north pole - Use two Rectangles"
)
return north
@property return self.east - self.west
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
@property
def start(self) -> datetime.datetime:
"""Start date of the Rectangle"""
return self.date - self.dt / 2
@property @property
def end(self) -> datetime.datetime: def lon(self) -> float:
"""End date of the Rectangle""" """Centre longitude of the Rectangle"""
return self.date + self.dt / 2 lon = self.west + self.lon_range / 2
return ((lon + 540) % 360) - 180
@property @property
def edge_dist(self) -> float: def edge_dist(self) -> float:
...@@ -185,6 +175,14 @@ class SpaceTimeRectangle: ...@@ -185,6 +175,14 @@ class SpaceTimeRectangle:
) )
return corner_dist return corner_dist
@property
def time_range(self) -> datetime.timedelta:
return self.end - self.start
@property
def centre_datetime(self) -> datetime.datetime:
return self.start + (self.end - self.start) / 2
def _test_east_west(self, lon: float) -> bool: def _test_east_west(self, lon: float) -> bool:
if self.lon_range >= 360: if self.lon_range >= 360:
# Rectangle encircles earth # Rectangle encircles earth
...@@ -203,6 +201,8 @@ class SpaceTimeRectangle: ...@@ -203,6 +201,8 @@ class SpaceTimeRectangle:
def contains(self, point: SpaceTimeRecord) -> bool: def contains(self, point: SpaceTimeRecord) -> bool:
"""Test if a point is contained within the SpaceTimeRectangle""" """Test if a point is contained within the SpaceTimeRectangle"""
if point.datetime > self.end or point.datetime < self.start: if point.datetime > self.end or point.datetime < self.start:
print("DATETIME FAILS")
print(point.datetime)
return False return False
return self._test_north_south(point.lat) and self._test_east_west( return self._test_north_south(point.lat) and self._test_east_west(
point.lon point.lon
...@@ -267,8 +267,8 @@ class SpaceTimeRectangle: ...@@ -267,8 +267,8 @@ class SpaceTimeRectangle:
bool : True if the point is <= dist + max(dist(centre, corners)) bool : True if the point is <= dist + max(dist(centre, corners))
""" """
if ( if (
point.datetime - t_dist > self.date + self.dt / 2 point.datetime - t_dist > self.end
or point.datetime + t_dist < self.date - self.dt / 2 or point.datetime + t_dist < self.start
): ):
return False return False
# QUESTION: Is this sufficient? Possibly it is overkill # QUESTION: Is this sufficient? Possibly it is overkill
...@@ -288,34 +288,40 @@ class SpaceTimeEllipse: ...@@ -288,34 +288,40 @@ class SpaceTimeEllipse:
Horizontal centre of the ellipse Horizontal centre of the ellipse
lat : float lat : float
Vertical centre of the ellipse Vertical centre of the ellipse
datetime : datetime.datetime
Datetime centre of the ellipse.
a : float a : float
Length of the semi-major axis Length of the semi-major axis
b : float b : float
Length of the semi-minor axis Length of the semi-minor axis
theta : float theta : float
Angle of the semi-major axis from horizontal anti-clockwise in radians Angle of the semi-major axis from horizontal anti-clockwise in radians
dt : datetime.timedelta start : datetime.datetime
(full) time extent of the ellipse. Start date of the Ellipse
end : datetime.datetime
Send date of the Ellipse
""" """
def __init__( def __init__(
self, self,
lon: float, lon: float,
lat: float, lat: float,
datetime: datetime.datetime,
a: float, a: float,
b: float, b: float,
theta: float, theta: float,
dt: datetime.timedelta, start: datetime.datetime,
end: datetime.datetime,
) -> None: ) -> None:
self.a = a self.a = a
self.b = b self.b = b
self.lon = lon self.lon = lon
if self.lon > 180:
self.lon = ((self.lon + 540) % 360) - 180
self.lat = lat self.lat = lat
self.datetime = datetime self.start = start
self.dt = dt self.end = end
if self.end < self.start:
warn("End date is before start date. Swapping")
self.start, self.end = self.end, self.start
# theta is anti-clockwise angle from horizontal in radians # theta is anti-clockwise angle from horizontal in radians
self.theta = theta self.theta = theta
# bearing is angle clockwise from north in degrees # bearing is angle clockwise from north in degrees
...@@ -337,8 +343,6 @@ class SpaceTimeEllipse: ...@@ -337,8 +343,6 @@ class SpaceTimeEllipse:
(self.bearing - 180) % 360, (self.bearing - 180) % 360,
self.c, self.c,
) )
self.start = self.datetime - self.dt / 2
self.end = self.datetime + self.dt / 2
def contains(self, point: SpaceTimeRecord) -> bool: def contains(self, point: SpaceTimeRecord) -> bool:
"""Test if a point is contained within the Ellipse""" """Test if a point is contained within the Ellipse"""
...@@ -439,12 +443,12 @@ class OctTree: ...@@ -439,12 +443,12 @@ class OctTree:
"""Divide the QuadTree""" """Divide the QuadTree"""
self.northwestfwd = OctTree( self.northwestfwd = OctTree(
SpaceTimeRectangle( SpaceTimeRectangle(
self.boundary.lon - self.boundary.lon_range / 4, self.boundary.west,
self.boundary.lat + self.boundary.lat_range / 4, self.boundary.lon,
self.boundary.date + self.boundary.dt / 4, self.boundary.lat,
self.boundary.lon_range / 2, self.boundary.north,
self.boundary.lat_range / 2, self.boundary.centre_datetime,
self.boundary.dt / 2, self.boundary.end,
), ),
capacity=self.capacity, capacity=self.capacity,
depth=self.depth + 1, depth=self.depth + 1,
...@@ -452,12 +456,12 @@ class OctTree: ...@@ -452,12 +456,12 @@ class OctTree:
) )
self.northeastfwd = OctTree( self.northeastfwd = OctTree(
SpaceTimeRectangle( SpaceTimeRectangle(
self.boundary.lon + self.boundary.lon_range / 4, self.boundary.lon,
self.boundary.lat + self.boundary.lat_range / 4, self.boundary.east,
self.boundary.date + self.boundary.dt / 4, self.boundary.lat,
self.boundary.lon_range / 2, self.boundary.north,
self.boundary.lat_range / 2, self.boundary.centre_datetime,
self.boundary.dt / 2, self.boundary.end,
), ),
capacity=self.capacity, capacity=self.capacity,
depth=self.depth + 1, depth=self.depth + 1,
...@@ -465,12 +469,12 @@ class OctTree: ...@@ -465,12 +469,12 @@ class OctTree:
) )
self.southwestfwd = OctTree( self.southwestfwd = OctTree(
SpaceTimeRectangle( SpaceTimeRectangle(
self.boundary.lon - self.boundary.lon_range / 4, self.boundary.west,
self.boundary.lat - self.boundary.lat_range / 4, self.boundary.lon,
self.boundary.date + self.boundary.dt / 4, self.boundary.south,
self.boundary.lon_range / 2, self.boundary.lat,
self.boundary.lat_range / 2, self.boundary.centre_datetime,
self.boundary.dt / 2, self.boundary.end,
), ),
capacity=self.capacity, capacity=self.capacity,
depth=self.depth + 1, depth=self.depth + 1,
...@@ -478,12 +482,12 @@ class OctTree: ...@@ -478,12 +482,12 @@ class OctTree:
) )
self.southeastfwd = OctTree( self.southeastfwd = OctTree(
SpaceTimeRectangle( SpaceTimeRectangle(
self.boundary.lon + self.boundary.lon_range / 4, self.boundary.lon,
self.boundary.lat - self.boundary.lat_range / 4, self.boundary.east,
self.boundary.date + self.boundary.dt / 4, self.boundary.south,
self.boundary.lon_range / 2, self.boundary.lat,
self.boundary.lat_range / 2, self.boundary.centre_datetime,
self.boundary.dt / 2, self.boundary.end,
), ),
capacity=self.capacity, capacity=self.capacity,
depth=self.depth + 1, depth=self.depth + 1,
...@@ -491,12 +495,12 @@ class OctTree: ...@@ -491,12 +495,12 @@ class OctTree:
) )
self.northwestback = OctTree( self.northwestback = OctTree(
SpaceTimeRectangle( SpaceTimeRectangle(
self.boundary.lon - self.boundary.lon_range / 4, self.boundary.west,
self.boundary.lat + self.boundary.lat_range / 4, self.boundary.lon,
self.boundary.date - self.boundary.dt / 4, self.boundary.lat,
self.boundary.lon_range / 2, self.boundary.north,
self.boundary.lat_range / 2, self.boundary.start,
self.boundary.dt / 2, self.boundary.centre_datetime,
), ),
capacity=self.capacity, capacity=self.capacity,
depth=self.depth + 1, depth=self.depth + 1,
...@@ -504,12 +508,12 @@ class OctTree: ...@@ -504,12 +508,12 @@ class OctTree:
) )
self.northeastback = OctTree( self.northeastback = OctTree(
SpaceTimeRectangle( SpaceTimeRectangle(
self.boundary.lon + self.boundary.lon_range / 4, self.boundary.lon,
self.boundary.lat + self.boundary.lat_range / 4, self.boundary.east,
self.boundary.date - self.boundary.dt / 4, self.boundary.lat,
self.boundary.lon_range / 2, self.boundary.north,
self.boundary.lat_range / 2, self.boundary.start,
self.boundary.dt / 2, self.boundary.centre_datetime,
), ),
capacity=self.capacity, capacity=self.capacity,
depth=self.depth + 1, depth=self.depth + 1,
...@@ -517,12 +521,12 @@ class OctTree: ...@@ -517,12 +521,12 @@ class OctTree:
) )
self.southwestback = OctTree( self.southwestback = OctTree(
SpaceTimeRectangle( SpaceTimeRectangle(
self.boundary.lon - self.boundary.lon_range / 4, self.boundary.west,
self.boundary.lat - self.boundary.lat_range / 4, self.boundary.lon,
self.boundary.date - self.boundary.dt / 4, self.boundary.south,
self.boundary.lon_range / 2, self.boundary.lat,
self.boundary.lat_range / 2, self.boundary.start,
self.boundary.dt / 2, self.boundary.centre_datetime,
), ),
capacity=self.capacity, capacity=self.capacity,
depth=self.depth + 1, depth=self.depth + 1,
...@@ -530,12 +534,12 @@ class OctTree: ...@@ -530,12 +534,12 @@ class OctTree:
) )
self.southeastback = OctTree( self.southeastback = OctTree(
SpaceTimeRectangle( SpaceTimeRectangle(
self.boundary.lon + self.boundary.lon_range / 4, self.boundary.lon,
self.boundary.lat - self.boundary.lat_range / 4, self.boundary.east,
self.boundary.date - self.boundary.dt / 4, self.boundary.south,
self.boundary.lon_range / 2, self.boundary.lat,
self.boundary.lat_range / 2, self.boundary.start,
self.boundary.dt / 2, self.boundary.centre_datetime,
), ),
capacity=self.capacity, capacity=self.capacity,
depth=self.depth + 1, depth=self.depth + 1,
...@@ -543,9 +547,6 @@ class OctTree: ...@@ -543,9 +547,6 @@ class OctTree:
) )
self.divided = True self.divided = True
def _datetime_is_numeric(self) -> bool:
return not isinstance(self.boundary.date, datetime)
def insert(self, point: SpaceTimeRecord) -> bool: def insert(self, point: SpaceTimeRecord) -> bool:
""" """
Insert a SpaceTimeRecord into the QuadTree. Insert a SpaceTimeRecord into the QuadTree.
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment