From 56f607b55fc37b4e7b99311071d89c10e9eb0d73 Mon Sep 17 00:00:00 2001 From: Sam Deans Date: Tue, 15 Apr 2025 19:41:12 +0100 Subject: [PATCH] feat: allow optional altitude --- pydantic_geojson/_base.py | 23 ++++++++++++-- tests/test_coordinates.py | 44 ++++++++++++++++++++++++++ tests/test_feature_on_lat_lon_limit.py | 2 +- tests/test_line_string.py | 2 +- 4 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 tests/test_coordinates.py diff --git a/pydantic_geojson/_base.py b/pydantic_geojson/_base.py index 8c7f957..32f58a2 100644 --- a/pydantic_geojson/_base.py +++ b/pydantic_geojson/_base.py @@ -34,6 +34,13 @@ ), ] +AltField = Annotated[ + Union[float, int], + Field( + title="Coordinate altitude", + ), +] + PointFieldType = Annotated[Literal[POINT], Field(POINT, title="Point")] # type: ignore MultiPointFieldType = Annotated[ @@ -104,10 +111,22 @@ class Coordinates(NamedTuple): lon: LonField lat: LatField + alt: Optional[AltField] = None def __eq__(self, other): - # Note that +180 and -180 are not considered equal here - return math.isclose(self.lon, other.lon) and math.isclose(self.lat, other.lat) + # Note that +180 and -180 are not considered equal latitude here + lon_equal = math.isclose(self.lon, other.lon) + lat_equal = math.isclose(self.lat, other.lat) + alt_equal = ( + self.alt is None + and other.alt is None + or ( + self.alt is not None + and other.alt is not None + and math.isclose(self.alt, other.alt) + ) + ) + return lon_equal and lat_equal and alt_equal def check_linear_ring(linear_ring: List[Coordinates]) -> List[Coordinates]: diff --git a/tests/test_coordinates.py b/tests/test_coordinates.py new file mode 100644 index 0000000..961a395 --- /dev/null +++ b/tests/test_coordinates.py @@ -0,0 +1,44 @@ +import pytest +from pydantic_geojson._base import Coordinates + + +@pytest.mark.parametrize( + "coord_one,coord_two,is_equal", + [ + ( + Coordinates(0, 0, None), + Coordinates(0, 0, None), + True, + ), # None and None are equivalent + ( + Coordinates(0, 0, 0), + Coordinates(0, 0, 0), + True, + ), # Zero and Zero are equivalent + ( + Coordinates(0, 0, None), + Coordinates(0, 0, 0), + False, + ), # Altiture not specified for coord_one + (Coordinates(1, 0, None), Coordinates(0, 0, None), False), # Latitude not equal + ( + Coordinates(0, 1, None), + Coordinates(0, 0, None), + False, + ), # Longitude not equal + ( + Coordinates(0, 0, 1), + Coordinates(0, 0, 0), + False, + ), # Altitude specified but not equal + ( + Coordinates(180, 0, None), + Coordinates(-180, 0, None), + False, + ), # Plus and minus 180 latitude are not considered equal + ], +) +def test_coordinate_equality(coord_one, coord_two, is_equal): + assert ( + coord_one == coord_two + ) == is_equal, f"Result of {coord_one} == {coord_two} should be {is_equal}" diff --git a/tests/test_feature_on_lat_lon_limit.py b/tests/test_feature_on_lat_lon_limit.py index 7a50500..d8daa39 100644 --- a/tests/test_feature_on_lat_lon_limit.py +++ b/tests/test_feature_on_lat_lon_limit.py @@ -33,7 +33,7 @@ def test_loads_model_linestring(self): coordinates = ls_model.coordinates for lsi_key, ls_item in enumerate(coordinates): - lon, lat = ls_item + lon, lat, _ = ls_item assert data_linestring["coordinates"][lsi_key] == [lon, lat] assert ls_model.type == data_linestring["type"] diff --git a/tests/test_line_string.py b/tests/test_line_string.py index 3e0257c..af7823d 100644 --- a/tests/test_line_string.py +++ b/tests/test_line_string.py @@ -33,7 +33,7 @@ def test_loads_model(self, valid_linestring_data): coordinates = ls_model.coordinates for lsi_key, ls_item in enumerate(coordinates): - lon, lat = ls_item + lon, lat, _ = ls_item assert valid_linestring_data["coordinates"][lsi_key] == [lon, lat] assert ls_model.type == valid_linestring_data["type"]