From dea2b24bc9337a90b992af93fe61e4977ba3483a Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Wed, 1 May 2024 22:28:22 +0100 Subject: [PATCH] start implementing #316 coordinates for Point --- .gitignore | 1 + fastkml/geometry.py | 93 +++++++++++++++++++++------- tests/geometries/coordinates_test.py | 10 +-- 3 files changed, 78 insertions(+), 26 deletions(-) diff --git a/.gitignore b/.gitignore index b0730ad2..e581dd71 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ coverage.xml nosetests.xml .ruff_cache/ monkeytype.sqlite3 +.hypothesis/ # Translations *.mo diff --git a/fastkml/geometry.py b/fastkml/geometry.py index f5bd324b..8b164fde 100644 --- a/fastkml/geometry.py +++ b/fastkml/geometry.py @@ -53,6 +53,7 @@ __all__ = [ "AnyGeometryType", + "Coordinates", "GeometryType", "LineString", "LinearRing", @@ -75,6 +76,24 @@ AnyGeometryType = Union[GeometryType, MultiGeometryType] +def handle_invalid_geometry_error( + *, + error: Exception, + element: Element, + strict: bool, +) -> None: + error_in_xml = config.etree.tostring( # type: ignore[attr-defined] + element, + encoding="UTF-8", + ).decode( + "UTF-8", + ) + msg = f"Invalid coordinates in '{error_in_xml}' caused by '{error}'" + logger.error(msg) + if strict: + raise KMLParseError(msg) from error + + def coordinates_subelement( obj: _XMLObject, *, @@ -146,17 +165,32 @@ def subelement_coordinates_kwarg( class Coordinates(_XMLObject): + """ + Represents a set of coordinates in decimal degrees. + + Attributes + ---------- + coords (LineType): A list of tuples representing the coordinates. + Each coord consists of floating point values for + longitude, latitude, and altitude. + The altitude component is optional. + Coordinates are expressed in decimal degrees only. + + """ + + _default_ns = config.KMLNS + coords: LineType def __init__( self, *, ns: Optional[str] = None, name_spaces: Optional[Dict[str, str]] = None, - coords: Optional[LineType], + coords: Optional[LineType] = None, **kwargs: Any, ): super().__init__(ns=ns, name_spaces=name_spaces, **kwargs) - self.coords = coords if coords else [] + self.coords = coords or [] def __bool__(self) -> bool: return bool(self.coords) @@ -179,24 +213,6 @@ def get_tag_name(cls) -> str: ) -def handle_invalid_geometry_error( - *, - error: Exception, - element: Element, - strict: bool, -) -> None: - error_in_xml = config.etree.tostring( # type: ignore[attr-defined] - element, - encoding="UTF-8", - ).decode( - "UTF-8", - ) - msg = f"Invalid coordinates in '{error_in_xml}' caused by '{error}'" - logger.error(msg) - if strict: - raise KMLParseError(msg) from error - - class _Geometry(_BaseObject): """ Baseclass with common methods for all geometry objects. @@ -404,6 +420,9 @@ def _get_kwargs( class Point(_Geometry): + + kml_coordinates: Optional[Coordinates] + def __init__( self, *, @@ -414,9 +433,18 @@ def __init__( extrude: Optional[bool] = None, tessellate: Optional[bool] = None, altitude_mode: Optional[AltitudeMode] = None, - geometry: geo.Point, + geometry: Optional[geo.Point] = None, + kml_coordinates: Optional[Coordinates] = None, **kwargs: Any, ) -> None: + if geometry is not None and kml_coordinates is not None: + raise ValueError("geometry and kml_coordinates are mutually exclusive") + if kml_coordinates is None: + self.kml_coordinates = ( + Coordinates(coords=geometry.coords) # type: ignore[arg-type] + if geometry + else None + ) super().__init__( ns=ns, id=id, @@ -440,11 +468,20 @@ def __repr__(self) -> str: f"extrude={self.extrude!r}, " f"tessellate={self.tessellate!r}, " f"altitude_mode={self.altitude_mode}, " - f"geometry={self.geometry!r}, " + f"kml_coordinates={self.kml_coordinates!r}, " f"**kwargs={self._get_splat()!r}," ")" ) + def __bool__(self) -> bool: + return bool(self.kml_coordinates) + + @property + def __geometry(self) -> Optional[geo.Point]: + if not self.kml_coordinates: + return None + return geo.Point.from_coordinates(self.kml_coordinates.coords) + def etree_element( self, precision: Optional[int] = None, @@ -481,6 +518,18 @@ def _get_geometry( return None +# registry.register( +# Point, +# item=RegistryItem( +# classes=(Coordinates,), +# attr_name="kml_coordinates", +# node_name="coordinates", +# get_kwarg=subelement_coordinates_kwarg, +# set_element=coordinates_subelement, +# ), +# ) + + class LineString(_Geometry): def __init__( self, diff --git a/tests/geometries/coordinates_test.py b/tests/geometries/coordinates_test.py index b1f9c701..7358aca3 100644 --- a/tests/geometries/coordinates_test.py +++ b/tests/geometries/coordinates_test.py @@ -29,9 +29,11 @@ def test_coordinates(self) -> None: coordinates = Coordinates(coords=coords) - assert coordinates.to_string() == ( - "0.000000,0.000000 0.000000,1.000000 1.000000,1.000000 " - "1.000000,0.000000 0.000000,0.000000" + assert coordinates.to_string().strip() == ( + '' + "0.000000,0.000000 0.000000,1.000000 1.000000,1.000000 " + "1.000000,0.000000 0.000000,0.000000" + "" ) def test_coordinates_from_string(self) -> None: @@ -45,5 +47,5 @@ def test_coordinates_from_string(self) -> None: assert coordinates.coords == [(0, 0), (1, 0), (1, 1), (0, 0)] -class TestCoordinatesLxml(Lxml): +class TestCoordinatesLxml(Lxml, TestCoordinates): pass