From b7bdb786192c773312a89fd4e93359acc777053e Mon Sep 17 00:00:00 2001 From: Christian Ledermann Date: Wed, 1 May 2024 21:22:46 +0100 Subject: [PATCH] add coordinates class --- fastkml/data.py | 2 +- fastkml/geometry.py | 120 +++++++++++++++++++++++++-- fastkml/gx.py | 4 +- fastkml/links.py | 4 +- fastkml/overlays.py | 6 +- fastkml/styles.py | 18 ++-- fastkml/times.py | 2 +- fastkml/views.py | 8 +- tests/geometries/coordinates_test.py | 49 +++++++++++ 9 files changed, 185 insertions(+), 28 deletions(-) create mode 100644 tests/geometries/coordinates_test.py diff --git a/fastkml/data.py b/fastkml/data.py index 04840ff8..576d0804 100644 --- a/fastkml/data.py +++ b/fastkml/data.py @@ -112,7 +112,7 @@ def __repr__(self) -> str: f"id={self.id!r}, " f"target_id={self.target_id!r}, " f"name={self.name!r}, " - f"type={self.type!r}, " + f"type={self.type}, " f"display_name={self.display_name!r}, " f"**kwargs={self._get_splat()!r}," ")" diff --git a/fastkml/geometry.py b/fastkml/geometry.py index 8158fcb6..f5bd324b 100644 --- a/fastkml/geometry.py +++ b/fastkml/geometry.py @@ -22,6 +22,7 @@ from typing import List from typing import Optional from typing import Sequence +from typing import Tuple from typing import Union from typing import cast @@ -30,11 +31,13 @@ from pygeoif.factories import shape from pygeoif.types import GeoCollectionType from pygeoif.types import GeoType +from pygeoif.types import LineType from pygeoif.types import Point2D from pygeoif.types import Point3D from fastkml import config from fastkml.base import _BaseObject +from fastkml.base import _XMLObject from fastkml.enums import AltitudeMode from fastkml.enums import Verbosity from fastkml.exceptions import KMLParseError @@ -44,6 +47,7 @@ from fastkml.helpers import subelement_bool_kwarg from fastkml.helpers import subelement_enum_kwarg from fastkml.registry import RegistryItem +from fastkml.registry import known_types from fastkml.registry import registry from fastkml.types import Element @@ -71,6 +75,110 @@ AnyGeometryType = Union[GeometryType, MultiGeometryType] +def coordinates_subelement( + obj: _XMLObject, + *, + element: Element, + attr_name: str, + node_name: str, + precision: Optional[int], + verbosity: Optional[Verbosity], +) -> None: + """ + Set the value of an attribute from a subelement with a text node. + + Args: + ---- + obj (_XMLObject): The object from which to retrieve the attribute value. + element (Element): The parent element to add the subelement to. + attr_name (str): The name of the attribute to retrieve the value from. + node_name (str): The name of the subelement to create. + precision (Optional[int]): The precision of the attribute value. + verbosity (Optional[Verbosity]): The verbosity level. + + Returns: + ------- + None + + """ + if getattr(obj, attr_name, None): + p = precision if precision is not None else 6 + coordinates = getattr(obj, attr_name) + if len(coordinates[0]) == 2: + tuples = (f"{c[0]:.{p}f},{c[1]:.{p}f}" for c in coordinates) + elif len(coordinates[0]) == 3: + tuples = (f"{c[0]:.{p}f},{c[1]:.{p}f},{c[2]:.{p}f}" for c in coordinates) + else: + msg = f"Invalid dimensions in coordinates '{coordinates}'" + raise KMLWriteError(msg) + element.text = " ".join(tuples) + + +def subelement_coordinates_kwarg( + *, + element: Element, + ns: str, + name_spaces: Dict[str, str], + node_name: str, + kwarg: str, + classes: Tuple[known_types, ...], + strict: bool, +) -> Dict[str, LineType]: + # Clean up badly formatted tuples by stripping + # space following commas. + try: + latlons = re.sub(r", +", ",", element.text.strip()).split() + except AttributeError: + return {} + try: + return { + kwarg: [ # type: ignore[dict-item] + tuple(float(c) for c in latlon.split(",")) for latlon in latlons + ], + } + except ValueError as error: + handle_invalid_geometry_error( + error=error, + element=element, + strict=strict, + ) + return {} + + +class Coordinates(_XMLObject): + + def __init__( + self, + *, + ns: Optional[str] = None, + name_spaces: Optional[Dict[str, str]] = None, + coords: Optional[LineType], + **kwargs: Any, + ): + super().__init__(ns=ns, name_spaces=name_spaces, **kwargs) + self.coords = coords if coords else [] + + def __bool__(self) -> bool: + return bool(self.coords) + + @classmethod + def get_tag_name(cls) -> str: + """Return the tag name.""" + return cls.__name__.lower() + + +registry.register( + Coordinates, + item=RegistryItem( + classes=(LineType,), # type: ignore[arg-type] + attr_name="coords", + node_name="coordinates", + get_kwarg=subelement_coordinates_kwarg, + set_element=coordinates_subelement, + ), +) + + def handle_invalid_geometry_error( *, error: Exception, @@ -155,7 +263,7 @@ def __repr__(self) -> str: f"target_id={self.target_id!r}, " f"extrude={self.extrude!r}, " f"tessellate={self.tessellate!r}, " - f"altitude_mode={self.altitude_mode!r}, " + f"altitude_mode={self.altitude_mode}, " f"geometry={self.geometry!r}, " f"**kwargs={self._get_splat()!r}," ")" @@ -331,7 +439,7 @@ def __repr__(self) -> str: f"target_id={self.target_id!r}, " f"extrude={self.extrude!r}, " f"tessellate={self.tessellate!r}, " - f"altitude_mode={self.altitude_mode!r}, " + f"altitude_mode={self.altitude_mode}, " f"geometry={self.geometry!r}, " f"**kwargs={self._get_splat()!r}," ")" @@ -409,7 +517,7 @@ def __repr__(self) -> str: f"target_id={self.target_id!r}, " f"extrude={self.extrude!r}, " f"tessellate={self.tessellate!r}, " - f"altitude_mode={self.altitude_mode!r}, " + f"altitude_mode={self.altitude_mode}, " f"geometry={self.geometry!r}, " f"**kwargs={self._get_splat()!r}," ")" @@ -482,7 +590,7 @@ def __repr__(self) -> str: f"target_id={self.target_id!r}, " f"extrude={self.extrude!r}, " f"tessellate={self.tessellate!r}, " - f"altitude_mode={self.altitude_mode!r}, " + f"altitude_mode={self.altitude_mode}, " f"geometry={self.geometry!r}, " f"**kwargs={self._get_splat()!r}," ")" @@ -544,7 +652,7 @@ def __repr__(self) -> str: f"target_id={self.target_id!r}, " f"extrude={self.extrude!r}, " f"tessellate={self.tessellate!r}, " - f"altitude_mode={self.altitude_mode!r}, " + f"altitude_mode={self.altitude_mode}, " f"geometry={self.geometry!r}, " f"**kwargs={self._get_splat()!r}," ")" @@ -724,7 +832,7 @@ def __repr__(self) -> str: f"target_id={self.target_id!r}, " f"extrude={self.extrude!r}, " f"tessellate={self.tessellate!r}, " - f"altitude_mode={self.altitude_mode!r}, " + f"altitude_mode={self.altitude_mode}, " f"geometry={self.geometry!r}, " f"**kwargs={self._get_splat()!r}," ")" diff --git a/fastkml/gx.py b/fastkml/gx.py index 62fe03ea..991193eb 100644 --- a/fastkml/gx.py +++ b/fastkml/gx.py @@ -239,7 +239,7 @@ def __repr__(self) -> str: f"target_id={self.target_id!r}, " f"extrude={self.extrude!r}, " f"tessellate={self.tessellate!r}, " - f"altitude_mode={self.altitude_mode!r}, " + f"altitude_mode={self.altitude_mode}, " f"geometry={self.geometry!r}, " f"track_items={self.track_items!r}, " f"**kwargs={self._get_splat()!r}," @@ -402,7 +402,7 @@ def __repr__(self) -> str: f"target_id={self.target_id!r}, " f"extrude={self.extrude!r}, " f"tessellate={self.tessellate!r}, " - f"altitude_mode={self.altitude_mode!r}, " + f"altitude_mode={self.altitude_mode}, " f"geometry={self.geometry!r}, " f"tracks={self.tracks!r}, " f"interpolate={self.interpolate!r}, " diff --git a/fastkml/links.py b/fastkml/links.py index 189dd36a..215084a8 100644 --- a/fastkml/links.py +++ b/fastkml/links.py @@ -95,9 +95,9 @@ def __repr__(self) -> str: f"id={self.id!r}, " f"target_id={self.target_id!r}, " f"href={self.href!r}, " - f"refresh_mode={self.refresh_mode!r}, " + f"refresh_mode={self.refresh_mode}, " f"refresh_interval={self.refresh_interval!r}, " - f"view_refresh_mode={self.view_refresh_mode!r}, " + f"view_refresh_mode={self.view_refresh_mode}, " f"view_refresh_time={self.view_refresh_time!r}, " f"view_bound_scale={self.view_bound_scale!r}, " f"view_format={self.view_format!r}, " diff --git a/fastkml/overlays.py b/fastkml/overlays.py index c59e6c49..e34bc980 100644 --- a/fastkml/overlays.py +++ b/fastkml/overlays.py @@ -408,7 +408,7 @@ def __repr__(self) -> str: f"tile_size={self.tile_size!r}, " f"max_width={self.max_width!r}, " f"max_height={self.max_height!r}, " - f"grid_origin={self.grid_origin!r}, " + f"grid_origin={self.grid_origin}, " f"**kwargs={self._get_splat()!r}," ")" ) @@ -612,7 +612,7 @@ def __repr__(self) -> str: f"view_volume={self.view_volume!r}, " f"image_pyramid={self.image_pyramid!r}, " f"point={self.point!r}, " - f"shape={self.shape!r}, " + f"shape={self.shape}, " f"**kwargs={self._get_splat()!r}," ")" ) @@ -917,7 +917,7 @@ def __repr__(self) -> str: f"draw_order={self.draw_order!r}, " f"icon={self.icon!r}, " f"altitude={self.altitude!r}, " - f"altitude_mode={self.altitude_mode!r}, " + f"altitude_mode={self.altitude_mode}, " f"lat_lon_box={self.lat_lon_box!r}, " f"**kwargs={self._get_splat()!r}," ")" diff --git a/fastkml/styles.py b/fastkml/styles.py index 518cbcc4..3d54079c 100644 --- a/fastkml/styles.py +++ b/fastkml/styles.py @@ -188,7 +188,7 @@ def __repr__(self) -> str: f"id={self.id!r}, " f"target_id={self.target_id!r}, " f"color={self.color!r}, " - f"color_mode={self.color_mode!r}, " + f"color_mode={self.color_mode}, " f"**kwargs={self._get_splat()!r}," ")" ) @@ -264,8 +264,8 @@ def __repr__(self) -> str: f"name_spaces={self.name_spaces!r}, " f"x={self.x!r}, " f"y={self.y!r}, " - f"xunits={self.xunits!r}, " - f"yunits={self.yunits!r}, " + f"xunits={self.xunits}, " + f"yunits={self.yunits}, " f"**kwargs={self._get_splat()!r}," ")" ) @@ -380,7 +380,7 @@ def __repr__(self) -> str: f"id={self.id!r}, " f"target_id={self.target_id!r}, " f"color={self.color!r}, " - f"color_mode={self.color_mode!r}, " + f"color_mode={self.color_mode}, " f"scale={self.scale!r}, " f"heading={self.heading!r}, " f"icon={self.icon!r}, " @@ -478,7 +478,7 @@ def __repr__(self) -> str: f"id={self.id!r}, " f"target_id={self.target_id!r}, " f"color={self.color!r}, " - f"color_mode={self.color_mode!r}, " + f"color_mode={self.color_mode}, " f"width={self.width!r}, " f"**kwargs={self._get_splat()!r}," ")" @@ -550,7 +550,7 @@ def __repr__(self) -> str: f"id={self.id!r}, " f"target_id={self.target_id!r}, " f"color={self.color!r}, " - f"color_mode={self.color_mode!r}, " + f"color_mode={self.color_mode}, " f"fill={self.fill!r}, " f"outline={self.outline!r}, " f"**kwargs={self._get_splat()!r}," @@ -626,7 +626,7 @@ def __repr__(self) -> str: f"id={self.id!r}, " f"target_id={self.target_id!r}, " f"color={self.color!r}, " - f"color_mode={self.color_mode!r}, " + f"color_mode={self.color_mode}, " f"scale={self.scale!r}, " f"**kwargs={self._get_splat()!r}," ")" @@ -742,7 +742,7 @@ def __repr__(self) -> str: f"bg_color={self.bg_color!r}, " f"text_color={self.text_color!r}, " f"text={self.text!r}, " - f"display_mode={self.display_mode!r}, " + f"display_mode={self.display_mode}, " f"**kwargs={self._get_splat()!r}," ")" ) @@ -917,7 +917,7 @@ def __repr__(self) -> str: f"name_spaces={self.name_spaces!r}, " f"id={self.id!r}, " f"target_id={self.target_id!r}, " - f"key={self.key!r}, " + f"key={self.key}, " f"style={self.style!r}, " f"**kwargs={self._get_splat()!r}," ")" diff --git a/fastkml/times.py b/fastkml/times.py index f825db5c..9fed8496 100644 --- a/fastkml/times.py +++ b/fastkml/times.py @@ -93,7 +93,7 @@ def __repr__(self) -> str: return ( f"{self.__class__.__module__}.{self.__class__.__name__}(" f"dt={self.dt!r}, " - f"resolution={self.resolution!r}, " + f"resolution={self.resolution}, " ")" ) diff --git a/fastkml/views.py b/fastkml/views.py index fc0d5cd2..6022d270 100644 --- a/fastkml/views.py +++ b/fastkml/views.py @@ -128,7 +128,7 @@ def __repr__(self) -> str: f"altitude={self.altitude!r}, " f"heading={self.heading!r}, " f"tilt={self.tilt!r}, " - f"altitude_mode={self.altitude_mode!r}, " + f"altitude_mode={self.altitude_mode}, " f"time_primitive={self.times!r}, " f"**kwargs={self._get_splat()!r}," ")" @@ -279,7 +279,7 @@ def __repr__(self) -> str: f"heading={self.heading!r}, " f"tilt={self.tilt!r}, " f"roll={self.roll!r}, " - f"altitude_mode={self.altitude_mode!r}, " + f"altitude_mode={self.altitude_mode}, " f"time_primitive={self.times!r}, " f"**kwargs={self._get_splat()!r}," ")" @@ -349,7 +349,7 @@ def __repr__(self) -> str: f"heading={self.heading!r}, " f"tilt={self.tilt!r}, " f"range={self.range!r}, " - f"altitude_mode={self.altitude_mode!r}, " + f"altitude_mode={self.altitude_mode}, " f"time_primitive={self.times!r}, " f"**kwargs={self._get_splat()!r}," ")" @@ -419,7 +419,7 @@ def __repr__(self) -> str: f"west={self.west!r}, " f"min_altitude={self.min_altitude!r}, " f"max_altitude={self.max_altitude!r}, " - f"altitude_mode={self.altitude_mode!r}, " + f"altitude_mode={self.altitude_mode}, " f"**kwargs={self._get_splat()!r}," ")" ) diff --git a/tests/geometries/coordinates_test.py b/tests/geometries/coordinates_test.py new file mode 100644 index 00000000..b1f9c701 --- /dev/null +++ b/tests/geometries/coordinates_test.py @@ -0,0 +1,49 @@ +# Copyright (C) 2023 - 2024 Christian Ledermann +# +# This library is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +"""Test the coordinates class.""" + + +from fastkml.geometry import Coordinates +from tests.base import Lxml +from tests.base import StdLibrary + + +class TestCoordinates(StdLibrary): + def test_coordinates(self) -> None: + """Test the init method.""" + coords = ((0, 0), (0, 1), (1, 1), (1, 0), (0, 0)) + + 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" + ) + + def test_coordinates_from_string(self) -> None: + """Test the from_string method.""" + coordinates = Coordinates.class_from_string( + '' + "0.000000,0.000000 1.000000,0.000000 1.0,1.0 0.000000,0.000000" + "", + ) + + assert coordinates.coords == [(0, 0), (1, 0), (1, 1), (0, 0)] + + +class TestCoordinatesLxml(Lxml): + pass