Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Typecheck spatial module #605

Open
wants to merge 25 commits into
base: development
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
86345a5
Typecheck spatial module
mfisher87 Sep 10, 2024
bfa9562
Fix doctest I broke
mfisher87 Sep 18, 2024
153c307
add datetime import to query.py for typechecking
JessicaS11 Oct 24, 2024
e0ef967
make types in abc order
JessicaS11 Oct 24, 2024
635e31c
add shapely.Polygon accepted input to docstring
JessicaS11 Oct 29, 2024
130160b
change to ExhaustiveTypeGuardException error type
JessicaS11 Oct 29, 2024
830f471
change to ExhaustiveTypeGuardException error type
JessicaS11 Oct 29, 2024
0ca4485
Reorder `Spatial` private properties in alpha order
trey-stafford Oct 30, 2024
b035695
Remove typing allowing list of coordinates as strings
trey-stafford Oct 30, 2024
1c1c490
Support shapely.Polygon for `spatial.geodataframe` `spatial_extent`
trey-stafford Oct 30, 2024
cbc761c
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 30, 2024
52037e9
`spatial.geodataframe` supports a list of tuples for `spatial_extent`
trey-stafford Oct 30, 2024
7c64991
Test code branch indicated as a "dev goal" in comment
trey-stafford Oct 30, 2024
2adc38d
Fixup test to indicate that we want to read a bbox from file
trey-stafford Oct 30, 2024
9df3ec7
Add test for unlikely error case
trey-stafford Oct 30, 2024
6f771d8
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 30, 2024
3f923a6
Add unit test for unlikely error in `fmt_for_EGI`
trey-stafford Oct 30, 2024
27de548
Fixup typeerror message and add test covering check in geodataframe
trey-stafford Oct 30, 2024
191181e
Add test for code branch that performes a dateline-crossing adjustment
trey-stafford Oct 30, 2024
9aa54dc
import icepyx as ipx in test_spatial
trey-stafford Oct 31, 2024
207be53
Merge branch 'development' into typecheck-spatial
JessicaS11 Nov 4, 2024
c89df21
Refactor `geodataframe` to more clearly show where a list of floats i…
trey-stafford Nov 11, 2024
b632617
Convert `spatial_extent` to `list[float]` for most functions
trey-stafford Nov 11, 2024
7675584
Update unit tests: spatial extent must be floats - not ints
trey-stafford Nov 11, 2024
7bd668c
Remove OBE TODO
trey-stafford Nov 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 39 additions & 18 deletions icepyx/core/spatial.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from itertools import chain
import os
from typing import Literal, Optional, Union, cast
import warnings
Expand All @@ -8,6 +9,8 @@
from shapely.geometry import Polygon, box
from shapely.geometry.polygon import orient

import icepyx.core.exceptions

# DevGoal: need to update the spatial_extent docstring to describe coordinate order for input


Expand All @@ -16,7 +19,7 @@

def geodataframe(
extent_type: ExtentType,
spatial_extent: Union[str, list[float]],
spatial_extent: Union[str, list[float], list[tuple[float, float]], Polygon],
file: bool = False,
xdateline: Optional[bool] = None,
) -> gpd.GeoDataFrame:
Expand All @@ -29,14 +32,17 @@ def geodataframe(
One of 'bounding_box' or 'polygon', indicating what type of input the spatial extent is

spatial_extent :
JessicaS11 marked this conversation as resolved.
Show resolved Hide resolved
A list containing the spatial extent OR a string containing a filename.
If file is False, spatial_extent should be a
list of coordinates in decimal degrees of [lower-left-longitude,
lower-left-latitute, upper-right-longitude, upper-right-latitude] or
[longitude1, latitude1, longitude2, latitude2, ... longitude_n,latitude_n, longitude1,latitude1].
A list containing the spatial extent, a shapely.Polygon, a list of
tuples (i.e.,, `[(longitude1, latitude1), (longitude2, latitude2),
...]`)containing floats, OR a string containing a filename.
If file is False, spatial_extent should be a shapely.Polygon,
list of bounding box coordinates in decimal degrees of [lower-left-longitude,
lower-left-latitute, upper-right-longitude, upper-right-latitude] or polygon vertices as
[longitude1, latitude1, longitude2, latitude2, ...
longitude_n,latitude_n, longitude1,latitude1].

If file is True, spatial_extent is a string containing the full file path and filename to the
file containing the desired spatial extent.
If file is True, spatial_extent is a string containing the full file path and filename
to the file containing the desired spatial extent.

file :
Indication for whether the spatial_extent string is a filename or coordinate list
Expand Down Expand Up @@ -65,15 +71,31 @@ def geodataframe(
"""

# If extent_type is a polygon AND from a file, create a geopandas geodataframe from it
# DevGoal: Currently this branch isn't tested...
if file is True:
if extent_type == "polygon":
return gpd.read_file(spatial_extent)
else:
raise TypeError("When 'file' is True, 'extent_type' must be 'polygon'")

if isinstance(spatial_extent, str):
raise TypeError(f"Expected list of floats, received {spatial_extent=}")
raise TypeError(
f"Expected list of floats, list of tuples of floats, or Polygon, received {spatial_extent=}"
)

if isinstance(spatial_extent, Polygon):
# Convert `spatial_extent` into a list of floats like:
# `[longitude1, latitude1, longitude2, latitude2, ...]`
spatial_extent = [
float(coord) for point in spatial_extent.exterior.coords for coord in point
]

# We are dealing with a `list[tuple[float, float]]`
if isinstance(spatial_extent, list) and isinstance(spatial_extent[0], tuple):
# Convert the list of tuples into a flat list of floats
spatial_extent = cast(list[tuple[float, float]], spatial_extent)
spatial_extent = list(chain.from_iterable(spatial_extent))

spatial_extent = cast(list[float], spatial_extent)

if xdateline is not None:
xdateline = xdateline
Expand Down Expand Up @@ -312,7 +334,7 @@ def validate_polygon_pairs(

def validate_polygon_list(
spatial_extent: Union[
list[Union[float, str]],
list[float],
NDArray[np.floating],
],
) -> tuple[Literal["polygon"], list[float], None]:
Expand All @@ -327,7 +349,7 @@ def validate_polygon_list(
Parameters
----------
spatial_extent:
A list or np.ndarray of strings or numerics representing polygon coordinates,
A list or np.ndarray of numerics representing polygon coordinates,
provided as coordinate pairs in decimal degrees in the order:
[longitude1, latitude1, longitude2, latitude2, ...
... longitude_n,latitude_n, longitude1,latitude1]
Expand Down Expand Up @@ -411,14 +433,13 @@ def validate_polygon_file(

class Spatial:
_ext_type: ExtentType
_spatial_ext: list[float]
_geom_file: Optional[str]
_spatial_ext: list[float]

def __init__(
self,
spatial_extent: Union[
str, # Filepath
list[str], # Bounding box or polygon
list[float], # Bounding box or polygon
list[tuple[float, float]], # Polygon
NDArray, # Polygon
Expand All @@ -436,7 +457,7 @@ def __init__(
----------
spatial_extent : list or string
* list of coordinates
(stored in a list of strings, list of numerics, list of tuples, OR np.ndarray) as one of:
(stored in a list of numerics, list of tuples, OR np.ndarray) as one of:
* bounding box
* provided in the order: [lower-left-longitude, lower-left-latitude,
upper-right-longitude, upper-right-latitude].)
Expand Down Expand Up @@ -542,7 +563,7 @@ def __init__(
# HACK: Unfortunately, the typechecker can't narrow based on the
# above conditional expressions. Tell the typechecker, "trust us"!
cast(
Union[list[Union[str, float]], NDArray[np.floating]],
Union[list[float], NDArray[np.floating]],
spatial_extent,
)
)
Expand Down Expand Up @@ -730,7 +751,7 @@ def fmt_for_CMR(self) -> str:
cmr_extent = ",".join(map(str, extent))

else:
raise RuntimeError("Programmer error!")
raise icepyx.core.exceptions.ExhaustiveTypeGuardException

return cmr_extent

Expand Down Expand Up @@ -761,6 +782,6 @@ def fmt_for_EGI(self) -> str:
egi_extent = egi_extent.replace(" ", "") # remove spaces for API call

else:
raise RuntimeError("Programmer error!")
raise icepyx.core.exceptions.ExhaustiveTypeGuardException

return egi_extent
80 changes: 80 additions & 0 deletions icepyx/tests/unit/test_spatial.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import pytest
from shapely.geometry import Polygon

import icepyx
trey-stafford marked this conversation as resolved.
Show resolved Hide resolved
import icepyx.core.spatial as spat

# ######### "Bounding Box" input tests ################################################################################
Expand Down Expand Up @@ -406,6 +407,71 @@ def test_gdf_from_multi_bbox():
assert obs.geometry[0].equals(exp.geometry[0])


def test_gdf_from_polygon():
polygon = Polygon(list(zip([-55, -55, -48, -48, -55], [68, 71, 71, 68, 68])))
obs = spat.geodataframe("polygon", polygon)
exp = gpd.GeoDataFrame(geometry=[polygon])

# make sure there is only one geometry before comparing them
assert len(obs.geometry) == 1
assert len(exp.geometry) == 1
assert obs.geometry[0].equals(exp.geometry[0])


def test_gdf_from_list_tuples():
polygon_tuples = list(
zip([-55.0, -55.0, -48.0, -48.0, -55.0], [68.0, 71.0, 71.0, 68.0, 68.0])
)
obs = spat.geodataframe("polygon", polygon_tuples)
geom = [Polygon(polygon_tuples)]
exp = gpd.GeoDataFrame(geometry=geom)

# make sure there is only one geometry before comparing them
assert len(obs.geometry) == 1
assert len(exp.geometry) == 1
assert obs.geometry[0].equals(exp.geometry[0])


def test_gdf_raises_error_bounding_box_file():
with pytest.raises(TypeError):
spat.geodataframe("bounding_box", "/fake/file/somewhere/polygon.shp", file=True)


def test_gdf_raises_error_string_file_false():
with pytest.raises(TypeError):
spat.geodataframe(
"bounding_box", "/fake/file/somewhere/polygon.shp", file=False
)


def test_gdf_boundingbox_xdateline():
bbox = [-55.5, 66.2, -64.2, 72.5]

# construct a geodataframe with the geometry corrected for the xdateline.
bbox_with_fix_for_xdateline = [304.5, 66.2, 295.8, 72.5]
min_x, min_y, max_x, max_y = bbox_with_fix_for_xdateline
exp = gpd.GeoDataFrame(
geometry=[
Polygon(
[
(min_x, min_y),
(min_x, max_y),
(max_x, max_y),
(max_x, min_y),
(min_x, min_y),
]
)
]
)

obs = spat.geodataframe("bounding_box", bbox)

# make sure there is only one geometry before comparing them
assert len(obs.geometry) == 1
assert len(exp.geometry) == 1
assert obs.geometry[0].equals(exp.geometry[0])


# Potential tests to include once multipolygon and complex polygons are handled

# def test_gdf_from_strpoly_one_simple():
Expand Down Expand Up @@ -498,6 +564,20 @@ def test_bbox_fmt():
assert obs == exp


def test_fmt_for_cmr_fails_unknown_extent_type():
bbox = spat.Spatial([-55, 68, -48, 71])
bbox._ext_type = "Unknown_user_override"
with pytest.raises(icepyx.core.exceptions.ExhaustiveTypeGuardException):
trey-stafford marked this conversation as resolved.
Show resolved Hide resolved
bbox.fmt_for_CMR()


def test_fmt_for_egi_fails_unknown_extent_type():
bbox = spat.Spatial([-55, 68, -48, 71])
bbox._ext_type = "Unknown_user_override"
with pytest.raises(icepyx.core.exceptions.ExhaustiveTypeGuardException):
trey-stafford marked this conversation as resolved.
Show resolved Hide resolved
bbox.fmt_for_EGI()


@pytest.fixture
def poly():
coords = [
Expand Down