Commit 77fb3001 authored by Joseph Siddons's avatar Joseph Siddons
Browse files

Merge branch 'rect_lon_wrap' into 'main'

Fix Rectangle query for QuadTree and OctTree classes

See merge request !8
parents 85e6d8af 6b1e0751
......@@ -49,12 +49,6 @@ class KDTree:
points.sort(key=lambda p: getattr(p, self.variable))
split_index = n_points // 2
self.partition_value = getattr(points[split_index - 1], self.variable)
# while (
# split_index < n_points
# and getattr(points[split_index], self.variable)
# == self.partition_value
# ):
# split_index += 1
self.split = True
......@@ -119,9 +113,9 @@ class KDTree:
def query(self, point) -> tuple[list[Record], float]:
"""Find the nearest Record within the KDTree to a query Record"""
if point.lon < 0:
point2 = Record(point.lon + 360, point.lat)
point2 = Record(point.lon + 360, point.lat, fix_lon=False)
else:
point2 = Record(point.lon - 360, point.lat)
point2 = Record(point.lon - 360, point.lat, fix_lon=False)
r1, d1 = self._query(point)
r2, d2 = self._query(point2)
......
......@@ -11,10 +11,14 @@ Numeric = TypeVar("Numeric", int, float, datetime, date)
class SortedWarning(Warning):
"""Warning class for Sortedness"""
pass
class SortedError(Exception):
"""Error class for Sortedness"""
pass
......
from datetime import datetime, timedelta
from .distance_metrics import haversine
from dataclasses import dataclass
import datetime
from .distance_metrics import haversine, destination
from .utils import LatitudeError
from math import degrees, sqrt
......@@ -25,12 +26,14 @@ class SpaceTimeRecord:
Horizontal coordinate (longitude).
lat : float
Vertical coordinate (latitude).
datetime : datetime
datetime : datetime.datetime
Datetime of the record. Can also be a numeric value such as pentad.
Comparisons between Records with datetime and Records with numeric
datetime will fail.
uid : str | None
Unique Identifier.
fix_lon : bool
Force longitude to -180, 180
**data
Additional data passed to the SpaceTimeRecord for use by other functions
or classes.
......@@ -40,11 +43,19 @@ class SpaceTimeRecord:
self,
lon: float,
lat: float,
datetime: datetime,
datetime: datetime.datetime,
uid: str | None = None,
fix_lon: bool = True,
**data,
) -> None:
self.lon = lon
if fix_lon:
# Move lon to -180, 180
self.lon = ((self.lon + 540) % 360) - 180
if lat < -90 or lat > 90:
raise LatitudeError(
"Expected latitude value to be between -90 and 90 degrees"
)
self.lat = lat
self.datetime = datetime
self.uid = uid
......@@ -53,12 +64,15 @@ class SpaceTimeRecord:
return None
def __str__(self) -> str:
return f"Record(x = {self.lon}, y = {self.lat}, datetime = {self.datetime}, uid = {self.uid})"
return f"SpaceTimeRecord(x = {self.lon}, y = {self.lat}, datetime = {self.datetime}, uid = {self.uid})"
def __eq__(self, other: object) -> bool:
if not isinstance(other, SpaceTimeRecord):
return False
if self.uid and other.uid:
return self.uid == other.uid
return (
isinstance(other, SpaceTimeRecord)
and self.lon == other.lon
self.lon == other.lon
and self.lat == other.lat
and self.datetime == other.datetime
and (not (self.uid or other.uid) or self.uid == other.uid)
......@@ -69,6 +83,7 @@ class SpaceTimeRecords(list[SpaceTimeRecord]):
"""List of SpaceTimeRecords"""
@dataclass
class SpaceTimeRectangle:
"""
A simple Space Time SpaceTimeRectangle class.
......@@ -90,73 +105,134 @@ class SpaceTimeRectangle:
Horizontal centre of the rectangle (longitude).
lat : float
Vertical centre of the rectangle (latitude).
datetime : datetime
datetime : datetime.datetime
Datetime centre of the rectangle.
w : float
Width of the rectangle (longitude range).
h : float
Height of the rectangle (latitude range).
dt : timedelta
dt : datetime.timedelta
time extent of the rectangle.
"""
def __init__(
self,
lon: float,
lat: float,
datetime: datetime,
lon_range: float,
lat_range: float,
dt: timedelta,
) -> None:
self.lon = lon
self.lat = lat
self.lon_range = lon_range
self.lat_range = lat_range
self.datetime = datetime
self.dt = dt
lon: float
lat: float
date: datetime.datetime
lon_range: float
lat_range: float
dt: datetime.timedelta
def __post_init__(self):
if self.lon > 180:
self.lon -= 360
if self.lat > 90 or self.lat < -90:
raise LatitudeError(
f"Central latitude value out of range {self.lat}, "
+ "should be between -90, 90 degrees"
)
def __str__(self) -> str:
return f"SpaceTimeRectangle(x = {self.lon}, y = {self.lat}, w = {self.lon_range}, h = {self.lat_range}, t = {self.datetime}, dt = {self.dt})"
@property
def west(self) -> float:
"""Western boundary of the Rectangle"""
return (((self.lon - self.lon_range / 2) + 540) % 360) - 180
@property
def east(self) -> float:
"""Eastern boundary of the Rectangle"""
return (((self.lon + self.lon_range / 2) + 540) % 360) - 180
@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
@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 __eq__(self, other: object) -> bool:
return (
isinstance(other, SpaceTimeRectangle)
and self.lon == other.lon
and self.lat == other.lat
and self.lon_range == other.lon_range
and self.lat_range == other.lat_range
and self.datetime == other.datetime
and self.dt == other.dt
@property
def start(self) -> datetime.datetime:
"""Start date of the Rectangle"""
return self.date - self.dt / 2
@property
def end(self) -> datetime.datetime:
"""End date of the Rectangle"""
return self.date + self.dt / 2
@property
def edge_dist(self) -> float:
"""Approximate maximum distance from the centre to an edge"""
corner_dist = max(
haversine(self.lon, self.lat, self.east, self.north),
haversine(self.lon, self.lat, self.east, self.south),
)
if self.north * self.south < 0:
corner_dist = max(
corner_dist,
haversine(self.lon, self.lat, self.east, 0),
)
return corner_dist
def _test_east_west(self, lon: float) -> bool:
if self.lon_range >= 360:
# Rectangle encircles earth
return True
if self.east > self.lon and self.west < self.lon:
return lon <= self.east and lon >= self.west
if self.east < self.lon:
return not (lon > self.east and lon < self.west)
if self.west > self.lon:
return not (lon < self.east and lon > self.west)
return False
def _test_north_south(self, lat: float) -> bool:
return lat <= self.north and lat >= self.south
def contains(self, point: SpaceTimeRecord) -> bool:
"""Test if a point is contained within the SpaceTimeRectangle"""
return (
point.lon <= self.lon + self.lon_range / 2
and point.lon >= self.lon - self.lon_range / 2
and point.lat <= self.lat + self.lat_range / 2
and point.lat >= self.lat - self.lat_range / 2
and point.datetime <= self.datetime + self.dt / 2
and point.datetime >= self.datetime - self.dt / 2
if point.datetime > self.end or point.datetime < self.start:
return False
return self._test_north_south(point.lat) and self._test_east_west(
point.lon
)
def intersects(self, other: object) -> bool:
"""Test if another Rectangle object intersects this Rectangle"""
return isinstance(other, SpaceTimeRectangle) and not (
self.lon - self.lon_range / 2 > other.lon + other.lon_range / 2
or self.lon + self.lon_range / 2 < other.lon - other.lon_range / 2
or self.lat - self.lat_range / 2 > other.lat + other.lat_range / 2
or self.lat + self.lat_range / 2 < other.lat - other.lat_range / 2
or self.datetime - self.dt / 2 > other.datetime + other.dt / 2
or self.datetime + self.dt / 2 < other.datetime - other.dt / 2
if not isinstance(other, SpaceTimeRectangle):
raise TypeError(
f"other must be a Rectangle class, got {type(other)}"
)
if other.end < self.start or other.start > self.end:
# Not in the same time range
return False
if other.south > self.north:
# Other is fully north of self
return False
if other.north < self.south:
# Other is fully south of self
return False
# Handle east / west edges
return self._test_east_west(other.west) or self._test_east_west(
other.east
)
def nearby(
self,
point: SpaceTimeRecord,
dist: float,
t_dist: timedelta,
t_dist: datetime.timedelta,
) -> bool:
"""
Check if point is nearby the Rectangle
......@@ -178,42 +254,21 @@ class SpaceTimeRectangle:
----------
point : SpaceTimeRecord
dist : float,
t_dist : timedelta
t_dist : datetime.timedelta
Returns
-------
bool : True if the point is <= dist + max(dist(centre, corners))
"""
if (
point.datetime - t_dist > self.datetime + self.dt / 2
or point.datetime + t_dist < self.datetime - self.dt / 2
point.datetime - t_dist > self.date + self.dt / 2
or point.datetime + t_dist < self.date - self.dt / 2
):
return False
# QUESTION: Is this sufficient? Possibly it is overkill
corner_dist = max(
haversine(
self.lon,
self.lat,
self.lon + self.lon_range / 2,
self.lat + self.lat_range / 2,
),
haversine(
self.lon,
self.lat,
self.lon + self.lon_range / 2,
self.lat - self.lat_range / 2,
),
)
if (self.lat + self.lat_range / 2) * (
self.lat - self.lat_range / 2
) < 0:
corner_dist = max(
corner_dist,
haversine(self.lon, self.lat, self.lon + self.lon_range / 2, 0),
)
return (
haversine(self.lon, self.lat, point.lon, point.lat)
<= dist + corner_dist
<= dist + self.edge_dist
)
......@@ -227,7 +282,7 @@ class SpaceTimeEllipse:
Horizontal centre of the ellipse
lat : float
Vertical centre of the ellipse
datetime : datetime
datetime : datetime.datetime
Datetime centre of the ellipse.
a : float
Length of the semi-major axis
......@@ -235,7 +290,7 @@ class SpaceTimeEllipse:
Length of the semi-minor axis
theta : float
Angle of the semi-major axis from horizontal anti-clockwise in radians
dt : timedelta
dt : datetime.timedelta
(full) time extent of the ellipse.
"""
......@@ -243,11 +298,11 @@ class SpaceTimeEllipse:
self,
lon: float,
lat: float,
datetime: datetime,
datetime: datetime.datetime,
a: float,
b: float,
theta: float,
dt: timedelta,
dt: datetime.timedelta,
) -> None:
self.a = a
self.b = b
......@@ -276,53 +331,28 @@ class SpaceTimeEllipse:
(self.bearing - 180) % 360,
self.c,
)
self.start = self.datetime - self.dt / 2
self.end = self.datetime + self.dt / 2
def contains(self, point: SpaceTimeRecord) -> bool:
"""Test if a point is contained within the Ellipse"""
if point.datetime > self.end or point.datetime < self.start:
return False
return (
(
haversine(self.p1_lon, self.p1_lat, point.lon, point.lat)
+ haversine(self.p2_lon, self.p2_lat, point.lon, point.lat)
)
<= 2 * self.a
and point.datetime <= self.datetime + self.dt / 2
and point.datetime >= self.datetime - self.dt / 2
)
haversine(self.p1_lon, self.p1_lat, point.lon, point.lat)
+ haversine(self.p2_lon, self.p2_lat, point.lon, point.lat)
) <= 2 * self.a
def nearby_rect(self, rect: SpaceTimeRectangle) -> bool:
"""Test if a rectangle is near to the Ellipse"""
if (
rect.datetime - rect.dt / 2 > self.datetime + self.dt / 2
or rect.datetime + rect.dt / 2 < self.datetime - self.dt / 2
):
if rect.start > self.end or rect.end < self.start:
return False
# TODO: Check corners, and 0 lat
corner_dist = max(
haversine(
rect.lon,
rect.lat,
rect.lon + rect.lon_range / 2,
rect.lat + rect.lat_range / 2,
),
haversine(
rect.lon,
rect.lat,
rect.lon + rect.lon_range / 2,
rect.lat - rect.lat_range / 2,
),
)
if (rect.lat + rect.lat_range / 2) * (
rect.lat - rect.lat_range / 2
) < 0:
corner_dist = max(
corner_dist,
haversine(rect.lon, rect.lat, rect.lon + rect.lon_range / 2, 0),
)
return (
haversine(self.p1_lon, self.p1_lat, rect.lon, rect.lat)
<= corner_dist + self.a
<= rect.edge_dist + self.a
and haversine(self.p2_lon, self.p2_lat, rect.lon, rect.lat)
<= corner_dist + self.a
<= rect.edge_dist + self.a
)
......@@ -405,7 +435,7 @@ class OctTree:
SpaceTimeRectangle(
self.boundary.lon - self.boundary.lon_range / 4,
self.boundary.lat + self.boundary.lat_range / 4,
self.boundary.datetime + self.boundary.dt / 4,
self.boundary.date + self.boundary.dt / 4,
self.boundary.lon_range / 2,
self.boundary.lat_range / 2,
self.boundary.dt / 2,
......@@ -418,7 +448,7 @@ class OctTree:
SpaceTimeRectangle(
self.boundary.lon + self.boundary.lon_range / 4,
self.boundary.lat + self.boundary.lat_range / 4,
self.boundary.datetime + self.boundary.dt / 4,
self.boundary.date + self.boundary.dt / 4,
self.boundary.lon_range / 2,
self.boundary.lat_range / 2,
self.boundary.dt / 2,
......@@ -431,7 +461,7 @@ class OctTree:
SpaceTimeRectangle(
self.boundary.lon - self.boundary.lon_range / 4,
self.boundary.lat - self.boundary.lat_range / 4,
self.boundary.datetime + self.boundary.dt / 4,
self.boundary.date + self.boundary.dt / 4,
self.boundary.lon_range / 2,
self.boundary.lat_range / 2,
self.boundary.dt / 2,
......@@ -444,7 +474,7 @@ class OctTree:
SpaceTimeRectangle(
self.boundary.lon + self.boundary.lon_range / 4,
self.boundary.lat - self.boundary.lat_range / 4,
self.boundary.datetime + self.boundary.dt / 4,
self.boundary.date + self.boundary.dt / 4,
self.boundary.lon_range / 2,
self.boundary.lat_range / 2,
self.boundary.dt / 2,
......@@ -457,7 +487,7 @@ class OctTree:
SpaceTimeRectangle(
self.boundary.lon - self.boundary.lon_range / 4,
self.boundary.lat + self.boundary.lat_range / 4,
self.boundary.datetime - self.boundary.dt / 4,
self.boundary.date - self.boundary.dt / 4,
self.boundary.lon_range / 2,
self.boundary.lat_range / 2,
self.boundary.dt / 2,
......@@ -470,7 +500,7 @@ class OctTree:
SpaceTimeRectangle(
self.boundary.lon + self.boundary.lon_range / 4,
self.boundary.lat + self.boundary.lat_range / 4,
self.boundary.datetime - self.boundary.dt / 4,
self.boundary.date - self.boundary.dt / 4,
self.boundary.lon_range / 2,
self.boundary.lat_range / 2,
self.boundary.dt / 2,
......@@ -483,7 +513,7 @@ class OctTree:
SpaceTimeRectangle(
self.boundary.lon - self.boundary.lon_range / 4,
self.boundary.lat - self.boundary.lat_range / 4,
self.boundary.datetime - self.boundary.dt / 4,
self.boundary.date - self.boundary.dt / 4,
self.boundary.lon_range / 2,
self.boundary.lat_range / 2,
self.boundary.dt / 2,
......@@ -496,7 +526,7 @@ class OctTree:
SpaceTimeRectangle(
self.boundary.lon + self.boundary.lon_range / 4,
self.boundary.lat - self.boundary.lat_range / 4,
self.boundary.datetime - self.boundary.dt / 4,
self.boundary.date - self.boundary.dt / 4,
self.boundary.lon_range / 2,
self.boundary.lat_range / 2,
self.boundary.dt / 2,
......@@ -508,7 +538,7 @@ class OctTree:
self.divided = True
def _datetime_is_numeric(self) -> bool:
return not isinstance(self.boundary.datetime, datetime)
return not isinstance(self.boundary.date, datetime)
def insert(self, point: SpaceTimeRecord) -> bool:
"""
......@@ -604,7 +634,7 @@ class OctTree:
self,
point: SpaceTimeRecord,
dist: float,
t_dist: timedelta,
t_dist: datetime.timedelta,
points: SpaceTimeRecords | None = None,
) -> SpaceTimeRecords:
"""
......@@ -623,7 +653,7 @@ class OctTree:
The distance for comparison. Note that Haversine distance is used
as the distance metric as the query SpaceTimeRecord and OctTree are
assumed to lie on the surface of Earth.
t_dist : timedelta
t_dist : datetime.timedelta
Max time gap between SpaceTimeRecords within the OctTree and the
query SpaceTimeRecord. Can be numeric if the OctTree boundaries,
SpaceTimeRecords, and query SpaceTimeRecord have numeric datetime
......
......@@ -3,8 +3,10 @@ Constuctors for QuadTree classes that can decrease the number of comparisons
for detecting nearby records for example
"""
from dataclasses import dataclass
from datetime import datetime
from .distance_metrics import haversine, destination
from .utils import LatitudeError
from math import degrees, sqrt
......@@ -12,6 +14,12 @@ class Record:
"""
ICOADS Record class
This is a simple instance of an ICOARDS record, it requires position data.
It can optionally include datetime, a UID, and extra data passed as
keyword arguments.
Equality is checked only on the required fields + UID if it is specified.
Parameters
----------
lon : float
......@@ -22,6 +30,11 @@ class Record:
Datetime of the record
uid : str | None
Unique Identifier
fix_lon : bool
Force longitude to -180, 180
**data
Additional data passed to the Record for use by other functions or
classes.
"""
def __init__(
......@@ -30,9 +43,17 @@ class Record:
lat: float,
datetime: datetime | None = None,
uid: str | None = None,
fix_lon: bool = True,
**data,
) -> None:
self.lon = lon
if fix_lon:
# Move lon to -180, 180
self.lon = ((self.lon + 540) % 360) - 180
if lat < -90 or lat > 90:
raise LatitudeError(
"Expected latitude value to be between -90 and 90 degrees"
)
self.lat = lat
self.datetime = datetime
self.uid = uid
......@@ -44,9 +65,12 @@ class Record:
return f"Record(lon = {self.lon}, lat = {self.lat}, datetime = {self.datetime}, uid = {self.uid})"
def __eq__(self, other: object) -> bool:
if not isinstance(other, Record):
return False
if self.uid and other.uid:
return self.uid == other.uid
return (
isinstance(other, Record)
and self.lon == other.lon
self.lon == other.lon
and self.lat == other.lat
and self.datetime == other.datetime
and (not (self.uid or other.uid) or self.uid == other.uid)
......@@ -59,6 +83,7 @@ class Record:
return haversine(self.lon, self.lat, other.lon, other.lat)
@dataclass
class Rectangle:
"""
A simple Rectangle class
......@@ -75,46 +100,100 @@ class Rectangle:
Height of the rectangle
"""
def __init__(
self,
lon: float,
lat: float,
lon_range: float,
lat_range: float,
) -> None:
self.lon = lon
self.lat = lat
self.lon_range = lon_range
self.lat_range = lat_range
lon: float
lat: float
lon_range: float
lat_range: float
def __post_init__(self):
if self.lon > 180:
self.lon -= 360
if self.lat > 90 or self.lat < -90:
raise LatitudeError(
f"Central latitude value out of range {self.lat}, "
+ "should be between -90, 90 degrees"
)
def __str__(self) -> str:
return f"Rectangle(x = {self.lon}, y = {self.lat}, w = {self.lon_range}, h = {self.lat_range})"
@property
def west(self) -> float:
"""Western boundary of the Rectangle"""
return (((self.lon - self.lon_range / 2) + 540) % 360) - 180
@property
def east(self) -> float:
"""Eastern boundary of the Rectangle"""
return (((self.lon + self.lon_range / 2) + 540) % 360) - 180
@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
@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 __eq__(self, other: object) -> bool:
return (
isinstance(other, Rectangle)
and self.lon == other.lon
and self.lat == other.lat
and self.lon_range == other.lon_range
and self.lat_range == other.lat_range
@property
def edge_dist(self) -> float:
"""Approximate maximum distance from the centre to an edge"""
corner_dist = max(
haversine(self.lon, self.lat, self.east, self.north),
haversine(self.lon, self.lat, self.east, self.south),
)
if self.north * self.south < 0:
corner_dist = max(
corner_dist,
haversine(self.lon, self.lat, self.east, 0),
)
return corner_dist
def _test_east_west(self, lon: float) -> bool:
if self.lon_range >= 360:
# Rectangle encircles earth
return True
if self.east > self.lon and self.west < self.lon:
return lon <= self.east and lon >= self.west
if self.east < self.lon:
return not (lon > self.east and lon < self.west)
if self.west > self.lon:
return not (lon < self.east and lon > self.west)
return False
def _test_north_south(self, lat: float) -> bool:
return lat <= self.north and lat >= self.south
def contains(self, point: Record) -> bool:
"""Test if a point is contained within the Rectangle"""
return (
point.lon <= self.lon + self.lon_range / 2
and point.lon >= self.lon - self.lon_range / 2
and point.lat <= self.lat + self.lat_range / 2
and point.lat >= self.lat - self.lat_range / 2
return self._test_north_south(point.lat) and self._test_east_west(
point.lon
)
def intersects(self, other: object) -> bool:
"""Test if another Rectangle object intersects this Rectangle"""
return isinstance(other, Rectangle) and not (
self.lon - self.lon_range / 2 > other.lon + other.lon_range / 2
or self.lon + self.lon_range / 2 < other.lon - other.lon_range / 2
or self.lat - self.lat_range / 2 > other.lat + other.lat_range / 2
or self.lat + self.lat_range / 2 < other.lat - other.lat_range / 2
if not isinstance(other, Rectangle):
raise TypeError(
f"other must be a Rectangle class, got {type(other)}"
)
if other.south > self.north:
# Other is fully north of self
return False
if other.north < self.south:
# Other is fully south of self
return False
# Handle east / west edges
return self._test_east_west(other.west) or self._test_east_west(
other.east
)
def nearby(
......@@ -124,30 +203,9 @@ class Rectangle:
) -> bool:
"""Check if point is nearby the Rectangle"""
# QUESTION: Is this sufficient? Possibly it is overkill
corner_dist = max(
haversine(
self.lon,
self.lat,
self.lon + self.lon_range / 2,
self.lat + self.lat_range / 2,
),
haversine(
self.lon,
self.lat,
self.lon + self.lon_range / 2,
self.lat - self.lat_range / 2,
),
)
if (self.lat + self.lat_range / 2) * (
self.lat - self.lat_range / 2
) < 0:
corner_dist = max(
corner_dist,
haversine(self.lon, self.lat, self.lon + self.lon_range / 2, 0),
)
return (
haversine(self.lon, self.lat, point.lon, point.lat)
<= dist + corner_dist
<= dist + self.edge_dist
)
......@@ -212,33 +270,11 @@ class Ellipse:
def nearby_rect(self, rect: Rectangle) -> bool:
"""Test if a rectangle is near to the Ellipse"""
# TODO: Check corners, and 0 lat
corner_dist = max(
haversine(
rect.lon,
rect.lat,
rect.lon + rect.lon_range / 2,
rect.lat + rect.lat_range / 2,
),
haversine(
rect.lon,
rect.lat,
rect.lon + rect.lon_range / 2,
rect.lat - rect.lat_range / 2,
),
)
if (rect.lat + rect.lat_range / 2) * (
rect.lat - rect.lat_range / 2
) < 0:
corner_dist = max(
corner_dist,
haversine(rect.lon, rect.lat, rect.lon + rect.lon_range / 2, 0),
)
return (
haversine(self.p1_lon, self.p1_lat, rect.lon, rect.lat)
<= corner_dist + self.a
<= rect.edge_dist + self.a
and haversine(self.p2_lon, self.p2_lat, rect.lon, rect.lat)
<= corner_dist + self.a
<= rect.edge_dist + self.a
)
......
class LatitudeError(ValueError):
"""Error for invalid Latitude Value"""
pass
......@@ -48,7 +48,7 @@
},
{
"cell_type": "code",
"execution_count": 11,
"execution_count": 4,
"id": "c60b30de-f864-477a-a09a-5f1caa4d9b9a",
"metadata": {},
"outputs": [
......@@ -63,11 +63,11 @@
"│ --- ┆ --- │\n",
"│ i64 ┆ i64 │\n",
"╞══════╪═════╡\n",
"│ 16 -75 │\n",
"│ 144 -77 │\n",
"│ -173 ┆ -83 │\n",
"│ 142-81 │\n",
"│ -50 ┆ -38 │\n",
"│ 12721 │\n",
"│ -14836 │\n",
"│ -46 ┆ -15 │\n",
"│ 104 ┆ 89 │\n",
"│ -57 ┆ -31 │\n",
"└──────┴─────┘\n"
]
}
......@@ -90,7 +90,7 @@
},
{
"cell_type": "code",
"execution_count": 12,
"execution_count": 5,
"id": "875f2a67-49fe-476f-add1-b1d76c6cd8f9",
"metadata": {},
"outputs": [],
......@@ -100,7 +100,7 @@
},
{
"cell_type": "code",
"execution_count": 13,
"execution_count": 6,
"id": "1e883e5a-5086-4c29-aff2-d308874eae16",
"metadata": {},
"outputs": [
......@@ -108,8 +108,8 @@
"name": "stdout",
"output_type": "stream",
"text": [
"CPU times: user 82 ms, sys: 4.14 ms, total: 86.1 ms\n",
"Wall time: 84.3 ms\n"
"CPU times: user 151 ms, sys: 360 ms, total: 511 ms\n",
"Wall time: 57.3 ms\n"
]
}
],
......@@ -120,7 +120,7 @@
},
{
"cell_type": "code",
"execution_count": 14,
"execution_count": 7,
"id": "69022ad1-5ec8-4a09-836c-273ef452451f",
"metadata": {},
"outputs": [
......@@ -128,7 +128,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
"188 μs ± 3.45 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)\n"
"203 μs ± 4.56 μs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)\n"
]
}
],
......@@ -140,7 +140,7 @@
},
{
"cell_type": "code",
"execution_count": 15,
"execution_count": 8,
"id": "28031966-c7d0-4201-a467-37590118e851",
"metadata": {},
"outputs": [
......@@ -148,7 +148,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
"8.72 ms ± 74.8 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n"
"8.87 ms ± 188 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n"
]
}
],
......@@ -160,7 +160,7 @@
},
{
"cell_type": "code",
"execution_count": 16,
"execution_count": 9,
"id": "0d10b2ba-57b2-475c-9d01-135363423990",
"metadata": {},
"outputs": [
......@@ -168,8 +168,8 @@
"name": "stdout",
"output_type": "stream",
"text": [
"CPU times: user 17.3 s, sys: 31.6 ms, total: 17.3 s\n",
"Wall time: 17.3 s\n"
"CPU times: user 17.4 s, sys: 147 ms, total: 17.6 s\n",
"Wall time: 17.6 s\n"
]
}
],
......@@ -188,7 +188,7 @@
},
{
"cell_type": "code",
"execution_count": 17,
"execution_count": 10,
"id": "a6aa6926-7fd5-4fff-bd20-7bc0305b948d",
"metadata": {},
"outputs": [
......@@ -214,7 +214,7 @@
"└──────────┴──────────┴─────────┴────────┴────────┴─────────┴────────┴────────┘"
]
},
"execution_count": 17,
"execution_count": 10,
"metadata": {},
"output_type": "execute_result"
}
......@@ -259,7 +259,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.6"
"version": "3.12.7"
}
},
"nbformat": 4,
......
......@@ -133,8 +133,8 @@
"name": "stdout",
"output_type": "stream",
"text": [
"CPU times: user 186 ms, sys: 191 ms, total: 377 ms\n",
"Wall time: 118 ms\n"
"CPU times: user 213 ms, sys: 5.74 ms, total: 219 ms\n",
"Wall time: 218 ms\n"
]
}
],
......@@ -157,105 +157,105 @@
"output_type": "stream",
"text": [
"OctTree:\n",
"- boundary: SpaceTimeRectangle(x = 0, y = 0, w = 360, h = 180, t = 1900-01-16 00:00:00, dt = 32 days, 0:00:00)\n",
"- boundary: SpaceTimeRectangle(lon=0, lat=0, date=datetime.datetime(1900, 1, 16, 0, 0), lon_range=360, lat_range=180, dt=datetime.timedelta(days=32))\n",
"- capacity: 10\n",
"- depth: 0\n",
"- max_depth: 25\n",
"- contents:\n",
"- number of elements: 10\n",
" * Record(x = 43, y = -68, datetime = 1900-01-08 13:00:00, uid = OBiqSYcn)\n",
" * Record(x = 97, y = -47, datetime = 1900-01-02 14:00:00, uid = w589k3Oe)\n",
" * Record(x = -68, y = 44, datetime = 1900-01-30 11:00:00, uid = XAaA7McU)\n",
" * Record(x = -170, y = 77, datetime = 1900-01-19 09:00:00, uid = x6eLi65N)\n",
" * Record(x = -2, y = 7, datetime = 1900-01-12 09:00:00, uid = CjB2Pglt)\n",
" * Record(x = -175, y = 65, datetime = 1900-01-15 01:00:00, uid = bTB9DkDI)\n",
" * Record(x = 8, y = 83, datetime = 1900-01-04 10:00:00, uid = aYCKIBl9)\n",
" * Record(x = 20, y = 60, datetime = 1900-01-24 16:00:00, uid = 8GsD19WF)\n",
" * Record(x = 161, y = 40, datetime = 1900-01-24 20:00:00, uid = FIfAABuC)\n",
" * Record(x = -69, y = -9, datetime = 1900-01-11 11:00:00, uid = uTcS5D4e)\n",
" * SpaceTimeRecord(x = -12, y = 89, datetime = 1900-01-03 18:00:00, uid = 70Fv8aPq)\n",
" * SpaceTimeRecord(x = -157, y = 21, datetime = 1900-01-29 16:00:00, uid = j2xUkgob)\n",
" * SpaceTimeRecord(x = -179, y = -25, datetime = 1900-01-30 19:00:00, uid = Nmw6lJSS)\n",
" * SpaceTimeRecord(x = -23, y = 89, datetime = 1900-01-14 15:00:00, uid = ILOWgH6u)\n",
" * SpaceTimeRecord(x = 145, y = 41, datetime = 1900-01-26 03:00:00, uid = Cii4cemG)\n",
" * SpaceTimeRecord(x = -139, y = -10, datetime = 1900-01-06 21:00:00, uid = 3QFhOlsW)\n",
" * SpaceTimeRecord(x = -146, y = -51, datetime = 1900-01-18 04:00:00, uid = IHZEXm2l)\n",
" * SpaceTimeRecord(x = -96, y = 86, datetime = 1900-01-14 22:00:00, uid = DtwMgmpH)\n",
" * SpaceTimeRecord(x = 113, y = 11, datetime = 1900-01-19 00:00:00, uid = OcFalcj8)\n",
" * SpaceTimeRecord(x = -141, y = -26, datetime = 1900-01-08 11:00:00, uid = qoJquq8j)\n",
"- with children:\n",
" OctTree:\n",
" - boundary: SpaceTimeRectangle(x = -90.0, y = 45.0, w = 180.0, h = 90.0, t = 1900-01-08 00:00:00, dt = 16 days, 0:00:00)\n",
" - boundary: SpaceTimeRectangle(lon=-90.0, lat=45.0, date=datetime.datetime(1900, 1, 8, 0, 0), lon_range=180.0, lat_range=90.0, dt=datetime.timedelta(days=16))\n",
" - capacity: 10\n",
" - depth: 1\n",
" - max_depth: 25\n",
" - contents:\n",
" - number of elements: 10\n",
" * Record(x = -156, y = 57, datetime = 1900-01-08 10:00:00, uid = aFheRU2n)\n",
" * Record(x = -100, y = 61, datetime = 1900-01-15 09:00:00, uid = Sa1iavle)\n",
" * Record(x = -168, y = 88, datetime = 1900-01-03 07:00:00, uid = IlYKGW0N)\n",
" * Record(x = -80, y = 50, datetime = 1900-01-05 09:00:00, uid = Rg3GHM4d)\n",
" * Record(x = -92, y = 39, datetime = 1900-01-15 06:00:00, uid = u804YMFB)\n",
" * Record(x = -119, y = 60, datetime = 1900-01-12 22:00:00, uid = vdEPjkib)\n",
" * Record(x = -160, y = 79, datetime = 1900-01-06 08:00:00, uid = QmrPEL6h)\n",
" * Record(x = -95, y = 21, datetime = 1900-01-09 04:00:00, uid = hfjTKSCH)\n",
" * Record(x = -93, y = 61, datetime = 1900-01-09 20:00:00, uid = SzIrja9S)\n",
" * Record(x = -149, y = 34, datetime = 1900-01-05 05:00:00, uid = b02MxQjV)\n",
" * SpaceTimeRecord(x = -89, y = 62, datetime = 1900-01-02 15:00:00, uid = zo7Thw1L)\n",
" * SpaceTimeRecord(x = -34, y = 62, datetime = 1900-01-13 19:00:00, uid = faiiSA9Y)\n",
" * SpaceTimeRecord(x = -61, y = 54, datetime = 1900-01-07 17:00:00, uid = HwzmXILd)\n",
" * SpaceTimeRecord(x = -154, y = 21, datetime = 1900-01-08 21:00:00, uid = lA7DANvC)\n",
" * SpaceTimeRecord(x = -59, y = 87, datetime = 1900-01-02 23:00:00, uid = GSzuXTLF)\n",
" * SpaceTimeRecord(x = -109, y = 27, datetime = 1900-01-01 21:00:00, uid = qy7npkH7)\n",
" * SpaceTimeRecord(x = -86, y = 46, datetime = 1900-01-10 19:00:00, uid = RJBRR7Rl)\n",
" * SpaceTimeRecord(x = -114, y = 46, datetime = 1900-01-03 05:00:00, uid = Eop56HgI)\n",
" * SpaceTimeRecord(x = -148, y = 49, datetime = 1900-01-08 13:00:00, uid = 3bavQs9B)\n",
" * SpaceTimeRecord(x = -109, y = 61, datetime = 1900-01-06 06:00:00, uid = zaTZk8xi)\n",
" - with children:\n",
" OctTree:\n",
" - boundary: SpaceTimeRectangle(x = -135.0, y = 67.5, w = 90.0, h = 45.0, t = 1900-01-04 00:00:00, dt = 8 days, 0:00:00)\n",
" - boundary: SpaceTimeRectangle(lon=-135.0, lat=67.5, date=datetime.datetime(1900, 1, 4, 0, 0), lon_range=90.0, lat_range=45.0, dt=datetime.timedelta(days=8))\n",
" - capacity: 10\n",
" - depth: 2\n",
" - max_depth: 25\n",
" - contents:\n",
" - number of elements: 10\n",
" * Record(x = -134, y = 79, datetime = 1900-01-05 14:00:00, uid = 7Q0FKGMk)\n",
" * Record(x = -90, y = 53, datetime = 1900-01-05 03:00:00, uid = LLx7iz2v)\n",
" * Record(x = -176, y = 50, datetime = 1900-01-06 20:00:00, uid = x6K5DlTl)\n",
" * Record(x = -141, y = 52, datetime = 1900-01-02 15:00:00, uid = xTpGPaEy)\n",
" * Record(x = -116, y = 68, datetime = 1900-01-05 16:00:00, uid = eECSkpdU)\n",
" * Record(x = -138, y = 63, datetime = 1900-01-05 02:00:00, uid = Ftf9uhH3)\n",
" * Record(x = -173, y = 71, datetime = 1900-01-03 03:00:00, uid = mu3vwHM5)\n",
" * Record(x = -148, y = 49, datetime = 1900-01-05 15:00:00, uid = 8DFDI3CJ)\n",
" * Record(x = -157, y = 63, datetime = 1900-01-06 19:00:00, uid = mVqLntgh)\n",
" * Record(x = -154, y = 45, datetime = 1900-01-07 11:00:00, uid = 1UoA1NNC)\n",
" * SpaceTimeRecord(x = -93, y = 49, datetime = 1900-01-07 05:00:00, uid = Vq4kU1vN)\n",
" * SpaceTimeRecord(x = -151, y = 84, datetime = 1900-01-03 03:00:00, uid = 1pz9w5ZR)\n",
" * SpaceTimeRecord(x = -154, y = 74, datetime = 1900-01-03 17:00:00, uid = m1jkOrRF)\n",
" * SpaceTimeRecord(x = -121, y = 81, datetime = 1900-01-02 13:00:00, uid = IcrUGFud)\n",
" * SpaceTimeRecord(x = -169, y = 52, datetime = 1900-01-03 12:00:00, uid = Rmytp4VV)\n",
" * SpaceTimeRecord(x = -115, y = 70, datetime = 1900-01-07 17:00:00, uid = XYiNarG0)\n",
" * SpaceTimeRecord(x = -104, y = 51, datetime = 1900-01-02 06:00:00, uid = je7IfMVs)\n",
" * SpaceTimeRecord(x = -161, y = 58, datetime = 1900-01-01 19:00:00, uid = szpJUEjI)\n",
" * SpaceTimeRecord(x = -133, y = 46, datetime = 1900-01-03 07:00:00, uid = vD2DhqWZ)\n",
" * SpaceTimeRecord(x = -125, y = 67, datetime = 1900-01-07 12:00:00, uid = rOJwKqKs)\n",
" - with children:\n",
" OctTree:\n",
" - boundary: SpaceTimeRectangle(x = -157.5, y = 78.75, w = 45.0, h = 22.5, t = 1900-01-02 00:00:00, dt = 4 days, 0:00:00)\n",
" - boundary: SpaceTimeRectangle(lon=-157.5, lat=78.75, date=datetime.datetime(1900, 1, 2, 0, 0), lon_range=45.0, lat_range=22.5, dt=datetime.timedelta(days=4))\n",
" - capacity: 10\n",
" - depth: 3\n",
" - max_depth: 25\n",
" - contents:\n",
" - number of elements: 10\n",
" * Record(x = -147, y = 83, datetime = 1900-01-01 18:00:00, uid = WaO5R7fy)\n",
" * Record(x = -136, y = 72, datetime = 1900-01-02 03:00:00, uid = OWaMqULr)\n",
" * Record(x = -176, y = 79, datetime = 1900-01-02 06:00:00, uid = NTjvqz2c)\n",
" * Record(x = -152, y = 72, datetime = 1900-01-03 18:00:00, uid = 7rtQIGtn)\n",
" * Record(x = -162, y = 78, datetime = 1900-01-02 04:00:00, uid = Wi9RsOIX)\n",
" * Record(x = -136, y = 79, datetime = 1900-01-01 11:00:00, uid = hSltzeuH)\n",
" * Record(x = -176, y = 89, datetime = 1900-01-02 09:00:00, uid = cOLgAely)\n",
" * Record(x = -141, y = 75, datetime = 1900-01-03 23:00:00, uid = gH755dC3)\n",
" * Record(x = -158, y = 72, datetime = 1900-01-02 23:00:00, uid = NUmMfw9K)\n",
" * Record(x = -168, y = 72, datetime = 1900-01-02 01:00:00, uid = ZFcsxYG4)\n",
" * SpaceTimeRecord(x = -141, y = 82, datetime = 1900-01-01 13:00:00, uid = B8slJoTY)\n",
" * SpaceTimeRecord(x = -147, y = 70, datetime = 1900-01-02 00:00:00, uid = bcnBHvsx)\n",
" * SpaceTimeRecord(x = -170, y = 86, datetime = 1900-01-02 17:00:00, uid = jEUfXIsD)\n",
" * SpaceTimeRecord(x = -180, y = 78, datetime = 1900-01-02 23:00:00, uid = TYebBlyX)\n",
" * SpaceTimeRecord(x = -135, y = 68, datetime = 1900-01-01 11:00:00, uid = UtzHGKY0)\n",
" * SpaceTimeRecord(x = -136, y = 85, datetime = 1900-01-01 02:00:00, uid = HxwAfFf7)\n",
" * SpaceTimeRecord(x = -169, y = 71, datetime = 1900-01-03 12:00:00, uid = kGOcDjS4)\n",
" * SpaceTimeRecord(x = -164, y = 79, datetime = 1900-01-03 11:00:00, uid = IFXekuK1)\n",
" * SpaceTimeRecord(x = -138, y = 76, datetime = 1900-01-01 09:00:00, uid = Qs0TDXtf)\n",
" * SpaceTimeRecord(x = -154, y = 71, datetime = 1900-01-01 16:00:00, uid = E6eJ2eiF)\n",
" - with children:\n",
" OctTree:\n",
" - boundary: SpaceTimeRectangle(x = -168.75, y = 84.375, w = 22.5, h = 11.25, t = 1900-01-01 00:00:00, dt = 2 days, 0:00:00)\n",
" - boundary: SpaceTimeRectangle(lon=-168.75, lat=84.375, date=datetime.datetime(1900, 1, 1, 0, 0), lon_range=22.5, lat_range=11.25, dt=datetime.timedelta(days=2))\n",
" - capacity: 10\n",
" - depth: 4\n",
" - max_depth: 25\n",
" - contents:\n",
" - number of elements: 6\n",
" * Record(x = -158, y = 86, datetime = 1900-01-01 15:00:00, uid = DOD5jT2l)\n",
" * Record(x = -165, y = 88, datetime = 1900-01-01 13:00:00, uid = kdGlzz41)\n",
" * Record(x = -173, y = 82, datetime = 1900-01-01 04:00:00, uid = aWBwIP4U)\n",
" * Record(x = -180, y = 89, datetime = 1900-01-01 22:00:00, uid = HOxbaCm8)\n",
" * Record(x = -165, y = 81, datetime = 1900-01-01 16:00:00, uid = JtRn9y9e)\n",
" * Record(x = -164, y = 84, datetime = 1900-01-01 03:00:00, uid = vELpx1ij)\n",
" OctTree:\n",
" - boundary: SpaceTimeRectangle(x = -146.25, y = 84.375, w = 22.5, h = 11.25, t = 1900-01-01 00:00:00, dt = 2 days, 0:00:00)\n",
" - boundary: SpaceTimeRectangle(lon=-146.25, lat=84.375, date=datetime.datetime(1900, 1, 1, 0, 0), lon_range=22.5, lat_range=11.25, dt=datetime.timedelta(days=2))\n",
" - capacity: 10\n",
" - depth: 4\n",
" - max_depth: 25\n",
" - contents:\n",
" - number of elements: 1\n",
" * Record(x = -157, y = 84, datetime = 1900-01-01 17:00:00, uid = 6DlgVOXg)\n",
" * SpaceTimeRecord(x = -135, y = 89, datetime = 1900-01-01 04:00:00, uid = MUrEpv1f)\n",
" OctTree:\n",
" - boundary: SpaceTimeRectangle(x = -168.75, y = 73.125, w = 22.5, h = 11.25, t = 1900-01-01 00:00:00, dt = 2 days, 0:00:00)\n",
" - boundary: SpaceTimeRectangle(lon=-168.75, lat=73.125, date=datetime.datetime(1900, 1, 1, 0, 0), lon_range=22.5, lat_range=11.25, dt=datetime.timedelta(days=2))\n",
" - capacity: 10\n",
" - depth: 4\n",
" - max_depth: 25\n",
" - contents:\n",
" - number of elements: 2\n"
" OctTree:\n",
" - boundary: SpaceTimeRectangle(lon=-146.25, lat=73.125, date=datetime.datetime(1900, 1, 1, 0, 0), lon_range=22.5, lat_range=11.25, dt=datetime.timedelta(days=2))\n",
" - capacity: 10\n",
" - depth: 4\n",
" - max_depth: 25\n",
" OctTree:\n",
" - boundary: SpaceTimeRectangle(lon=-168.75, lat=84.375, date=datetime.datetime(1900, 1, 3, 0, 0), lon_range=22.5, lat_range=11.25, dt=datetime.timedelta(days=2))\n",
" - capacity: 10\n",
" - depth: 4\n",
" - max_depth: 25\n"
]
}
],
......@@ -301,7 +301,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
"206 μs ± 3.36 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)\n"
"224 μs ± 6.15 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)\n"
]
}
],
......@@ -601,7 +601,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
"5.33 ms ± 20.1 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n"
"5.4 ms ± 19.5 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n"
]
}
],
......@@ -631,7 +631,7 @@
"name": "stdout",
"output_type": "stream",
"text": [
"CPU times: user 2.52 s, sys: 237 ms, total: 2.75 s\n",
"CPU times: user 2.5 s, sys: 228 ms, total: 2.73 s\n",
"Wall time: 2.65 s\n"
]
}
......@@ -699,7 +699,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.6"
"version": "3.12.7"
}
},
"nbformat": 4,
......
import unittest
import random
from numpy import min, argmin
from GeoSpatialTools import haversine, KDTree, Record
......@@ -91,12 +93,39 @@ class TestKDTree(unittest.TestCase):
def test_wrap(self):
# TEST: Accounts for wrap at -180, 180
kt = KDTree(self.records)
kt.insert(Record(-160, -64, uid="G"))
bad_rec = Record(-160, -64, uid="G")
kt.insert(bad_rec)
query_rec = Record(-178, -79, uid="E")
r, _ = kt.query(query_rec)
assert len(r) == 1
assert r[0].uid == "C"
def test_near_pole_query(self):
test_records = [
Record(-180, 89.5, uid="1"),
Record(-90, 89.9, uid="2"),
Record(0, 89.5, uid="3"),
]
N_others = 50
test_records.extend(
[
Record(
random.choice(range(-180, 180)),
random.choice(range(80, 90)),
)
for _ in range(N_others)
]
)
kt = KDTree(test_records, max_depth=3)
query_rec = Record(90, 89.8, uid="4")
r, d = kt.query(query_rec)
assert len(r) == 1
print(r[0])
print(d)
assert r[0].uid == "2"
if __name__ == "__main__":
unittest.main()
import random
import unittest
from datetime import datetime, timedelta
......@@ -27,6 +28,50 @@ class TestRect(unittest.TestCase):
res = list(map(rect.contains, points))
assert res == expected
def test_intersection(self):
d = datetime(2009, 1, 1, 0, 0)
dt = timedelta(days=14)
rect = Rectangle(10, 5, d, 20, 10, dt)
test_rects: list[Rectangle] = [
Rectangle(10, 5, d + timedelta(days=2), 18, 8, dt),
Rectangle(25, 5, d, 9, 12, timedelta(hours=7)),
Rectangle(
15, 8, d - timedelta(hours=18), 12, 7, timedelta(hours=4)
),
Rectangle(15, 8, d + timedelta(days=25), 12, 7, dt),
]
expected = [True, False, True, False]
res = list(map(rect.intersects, test_rects))
assert res == expected
def test_wrap(self):
d = datetime(2009, 1, 1, 0, 0)
dt = timedelta(days=14)
rect = Rectangle(170, 45, d, 180, 20, dt)
assert rect.east < 0
assert rect.west > 0
test_points: list[Record] = [
Record(-140, 40, d),
Record(0, 50, d),
Record(100, 45, d - timedelta(hours=2)),
Record(100, 45, d + timedelta(days=12)),
]
expected = [True, False, True, False]
res = list(map(rect.contains, test_points))
assert res == expected
test_rect = Rectangle(
-100, 40, d + timedelta(days=3), 80, 40, timedelta(days=2)
)
assert test_rect.east < rect.west
assert rect.intersects(test_rect)
# TEST: spatially match, time fail
test_rect = Rectangle(
-100, 40, d + timedelta(days=13), 80, 40, timedelta(days=2)
)
assert not rect.intersects(test_rect)
class TestOctTree(unittest.TestCase):
def test_divides(self):
......@@ -130,6 +175,36 @@ class TestOctTree(unittest.TestCase):
assert res == expected
def test_wrap_query(self):
N = 100
d = datetime(2023, 3, 24, 12, 0)
dt = timedelta(days=10)
boundary = Rectangle(0, 0, d, 360, 180, dt)
ot = OctTree(boundary, capacity=3)
quert_rect = Rectangle(
170, 45, d + timedelta(days=4), 60, 10, timedelta(days=8)
)
points_want: list[Record] = [
Record(175, 43, d + timedelta(days=2)),
Record(-172, 49, d + timedelta(days=4)),
]
points: list[Record] = [
Record(
random.choice(range(-150, 130)),
random.choice(range(-90, 91)),
d + timedelta(hours=random.choice(range(-120, 120))),
)
for _ in range(N)
]
points.extend(points_want)
for p in points:
ot.insert(p)
res = ot.query(quert_rect)
assert len(res) == len(points_want)
assert all([p in res for p in points_want])
def test_ellipse_query(self):
d1 = haversine(0, 2.5, 1, 2.5)
d2 = haversine(0, 2.5, 0, 3.0)
......
from math import pi
import random
import unittest
from GeoSpatialTools import haversine
from GeoSpatialTools.quadtree import QuadTree, Record, Rectangle, Ellipse
......@@ -29,6 +30,23 @@ class TestRect(unittest.TestCase):
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
test_points: list[Record] = [
Record(-140, 40),
Record(0, 50),
Record(100, 45),
]
expected = [True, False, True]
res = list(map(rect.contains, test_points))
assert res == expected
test_rect = Rectangle(-100, 40, 80, 40)
assert test_rect.east < rect.west
assert rect.intersects(test_rect)
class TestQuadTree(unittest.TestCase):
def test_divides(self):
......@@ -98,6 +116,31 @@ class TestQuadTree(unittest.TestCase):
assert res == expected
def test_wrap_query(self):
N = 100
qt_boundary = Rectangle(0, 0, 360, 180)
qt = QuadTree(qt_boundary, capacity=3)
quert_rect = Rectangle(170, 45, 60, 10)
points_want: list[Record] = [
Record(175, 43),
Record(-172, 49),
]
points: list[Record] = [
Record(
random.choice(range(-150, 130)),
random.choice(range(-90, 91)),
)
for _ in range(N)
]
points.extend(points_want)
for p in points:
qt.insert(p)
res = qt.query(quert_rect)
assert len(res) == len(points_want)
assert all([p in res for p in points_want])
def test_ellipse_query(self):
d1 = haversine(0, 2.5, 1, 2.5)
d2 = haversine(0, 2.5, 0, 3.0)
......
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