Skip to content

Commit

Permalink
Refactor boundary classes and improve KML representation
Browse files Browse the repository at this point in the history
- Renamed OuterBoundaryIs to BoundaryIs to generalize the class for both inner and outer boundaries.
- Introduced InnerBoundaryIs as a subclass of BoundaryIs.
- Updated the get_tag_name method for BoundaryIs.
- Simplified the find function in utils.py.
- Added tests for InnerBoundaryIs representation and track handling.
  • Loading branch information
cleder committed Nov 3, 2024
1 parent 1ef276c commit ffb41e9
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 104 deletions.
115 changes: 15 additions & 100 deletions fastkml/geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -817,9 +817,9 @@ def geometry(self) -> Optional[geo.LinearRing]:
return None


class OuterBoundaryIs(_XMLObject):
class BoundaryIs(_XMLObject):
"""
Represents the outer boundary of a polygon in KML.
Represents the inner or outer boundary of a polygon in KML.
Attributes
----------
Expand Down Expand Up @@ -905,19 +905,6 @@ def __repr__(self) -> str:
")"
)

@classmethod
def get_tag_name(cls) -> str:
"""
Get the tag name for the OuterBoundaryIs object.
Returns
-------
str
The tag name.
"""
return "outerBoundaryIs"

@property
def geometry(self) -> Optional[geo.LinearRing]:
"""
Expand All @@ -932,106 +919,34 @@ def geometry(self) -> Optional[geo.LinearRing]:
return self.kml_geometry.geometry if self.kml_geometry else None


registry.register(
OuterBoundaryIs,
item=RegistryItem(
ns_ids=("kml", ""),
classes=(LinearRing,),
attr_name="kml_geometry",
node_name="LinearRing",
get_kwarg=xml_subelement_kwarg,
set_element=xml_subelement,
),
)
class OuterBoundaryIs(BoundaryIs):
"""Represents the outer boundary of a polygon in KML."""


class InnerBoundaryIs(_XMLObject):
"""Represents the inner boundary of a polygon in KML."""

_default_nsid = config.KML
kml_geometry: Optional[LinearRing]

def __init__(
self,
*,
ns: Optional[str] = None,
name_spaces: Optional[Dict[str, str]] = None,
geometry: Optional[geo.LinearRing] = None,
kml_geometry: Optional[LinearRing] = None,
**kwargs: Any,
) -> None:
@classmethod
def get_tag_name(cls) -> str:
"""
Initialize a Geometry object.
Parameters
----------
ns : Optional[str], optional
The namespace for the KML element, by default None.
name_spaces : Optional[Dict[str, str]], optional
The namespace dictionary for the KML element, by default None.
geometry : Optional[geo.LinearRing], optional
The geometry to be converted to a KML geometry, by default None.
kml_geometry : Optional[LinearRing], optional
The KML geometry, by default None.
**kwargs : Any
Additional keyword arguments.
Raises
------
GeometryError
If both `geometry` and `kml_geometry` are provided.
Get the tag name for the OuterBoundaryIs object.
Notes
-----
- If `geometry` is provided, it will be converted to KML geometries and
stored in `kml_geometry`.
- If `geometry` and `kml_geometry` are both provided, a GeometryError will be
raised.
Returns
-------
str
The tag name.
"""
if geometry is not None and kml_geometry is not None:
raise GeometryError(MsgMutualExclusive)
if kml_geometry is None:
kml_geometry = LinearRing(ns=ns, name_spaces=name_spaces, geometry=geometry)
self.kml_geometry = kml_geometry
super().__init__(
ns=ns,
name_spaces=name_spaces,
**kwargs,
)
return "outerBoundaryIs"

def __bool__(self) -> bool:
"""Return True if any of the inner boundary geometries exist."""
return bool(self.kml_geometry)

def __repr__(self) -> str:
"""Create a string (c)representation for InnerBoundaryIs."""
return (
f"{self.__class__.__module__}.{self.__class__.__name__}("
f"ns={self.ns!r}, "
f"name_spaces={self.name_spaces!r}, "
f"kml_geometry={self.kml_geometry!r}, "
f"**{self._get_splat()},"
")"
)
class InnerBoundaryIs(BoundaryIs):
"""Represents the inner boundary of a polygon in KML."""

@classmethod
def get_tag_name(cls) -> str:
"""Return the tag name of the element."""
return "innerBoundaryIs"

@property
def geometry(self) -> Optional[geo.LinearRing]:
"""
Return the list of LinearRing objects representing the inner boundary.
If no inner boundary geometries exist, returns None.
"""
return self.kml_geometry.geometry if self.kml_geometry else None


registry.register(
InnerBoundaryIs,
BoundaryIs,
item=RegistryItem(
ns_ids=("kml",),
classes=(LinearRing,),
Expand Down
5 changes: 1 addition & 4 deletions fastkml/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,4 @@ def find(
The first instance of the given type in the given object or None if not found.
"""
try:
return next(find_all(obj, of_type=of_type, **kwargs))
except StopIteration:
return None
return next(find_all(obj, of_type=of_type, **kwargs), None)
17 changes: 17 additions & 0 deletions tests/geometries/boundaries_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import pygeoif.geometry as geo
import pytest

import fastkml
from fastkml.exceptions import GeometryError
from fastkml.geometry import Coordinates
from fastkml.geometry import InnerBoundaryIs
Expand Down Expand Up @@ -122,6 +123,22 @@ def test_read_inner_boundary_multiple_linestrings(self) -> None:
((1, 4), (2, 0), (0, 0), (1, 4)),
)

def test_inner_boundary_repr_roundtrip(self) -> None:
"""Test that repr(obj) can be eval'd back to obj."""
coords = ((1, 2), (2, 0), (0, 0), (1, 2))
inner_boundary = InnerBoundaryIs(
kml_geometry=LinearRing(kml_coordinates=Coordinates(coords=coords)),
)

assert inner_boundary == eval( # noqa: S307
repr(inner_boundary),
{},
{
"fastkml": fastkml,
"LinearRing": geo.LinearRing,
},
)


class TestBoundariesLxml(Lxml, TestBoundaries):
pass
24 changes: 24 additions & 0 deletions tests/geometries/point_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,30 @@ def test_from_string_uppercase_altitude_mode_strict(self) -> None:
"</Point>",
)

def test_from_string_invalid_altitude_mode_strict(self) -> None:
with pytest.raises(
KMLParseError,
match=r"^Error parsing '<",
):
assert Point.from_string(
'<Point xmlns="http://www.opengis.net/kml/2.2">'
"<altitudeMode>INVALID</altitudeMode>"
"<coordinates>1.000000,2.000000</coordinates>"
"</Point>",
)

def test_from_string_invalid_altitude_mode_relaxed(self) -> None:
point = Point.from_string(
'<Point xmlns="http://www.opengis.net/kml/2.2">'
"<altitudeMode>invalid</altitudeMode>"
"<coordinates>1.000000,2.000000</coordinates>"
"</Point>",
strict=False,
)

assert point.geometry == geo.Point(1, 2)
assert not point.altitude_mode

def test_from_string_3d(self) -> None:
"""Test the from_string method for a 3 dimensional point."""
point = Point.from_string(
Expand Down
57 changes: 57 additions & 0 deletions tests/gx_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import datetime

import pygeoif.geometry as geo
import pytest
from dateutil.tz import tzoffset
from dateutil.tz import tzutc

Expand Down Expand Up @@ -175,6 +176,48 @@ def test_track_from_track_items(self) -> None:
assert "angles>" in track.to_string()
assert ">0.0 0.0 0.0</" in track.to_string()

def test_track_from_whens_and_coords(self) -> None:
whens = [
KmlDateTime(
datetime.datetime(2023, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc),
),
]
coords = [(1, 2)]

track = Track(
whens=whens,
coords=coords,
)

assert "when>" in track.to_string()
assert ">2023-01-01T00:00:00+00:00</" in track.to_string()
assert "coord>" in track.to_string()
assert ">1 2</" in track.to_string()
assert track.coords == ((1, 2),)

def test_track_from_whens_and_coords_and_track_items(self) -> None:
whens = [
KmlDateTime(
datetime.datetime(2023, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc),
),
]
coords = [(1, 2)]
time1 = KmlDateTime(
datetime.datetime(2023, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc),
)
angle = Angle()
track_items = [TrackItem(when=time1, coord=geo.Point(1, 2), angle=angle)]

with pytest.raises(
ValueError,
match="^Cannot specify both geometry and track_items$",
):
Track(
whens=whens,
coords=coords,
track_items=track_items,
)

def test_track_precision(self) -> None:
track = Track(
id="x",
Expand Down Expand Up @@ -335,6 +378,20 @@ def test_track_from_str_invalid_when(self) -> None:

assert track.track_items == []

def test_track_from_str_invalid_coord(self) -> None:
doc = """
<gx:Track xmlns:gx="http://www.google.com/kml/ext/2.2"
xmlns:kml="http://www.opengis.net/kml/2.2">
<kml:when>2010-02-14T02:02:09Z</kml:when>
<gx:angles>45.54676 66.2342 77.0</gx:angles>
<gx:coord>XYZ 37.371915 156.000000</gx:coord>
</gx:Track>
"""

track = Track.from_string(doc, strict=False)

assert track.track_items == []


class TestMultiTrack(StdLibrary):
"""Test gx.MultiTrack."""
Expand Down

0 comments on commit ffb41e9

Please sign in to comment.