diff --git a/fastkml/features.py b/fastkml/features.py index b911f359..3c909035 100644 --- a/fastkml/features.py +++ b/fastkml/features.py @@ -5,7 +5,6 @@ """ import logging -from datetime import datetime from typing import Any from typing import Dict from typing import Iterator @@ -131,14 +130,7 @@ class _Feature(TimeMixin, _BaseObject): # Placemark, or ScreenOverlay—the value for the Feature's inline # style takes precedence over the value for the shared style. - _timespan: Optional[TimeSpan] - # Associates this Feature with a period of time. - _timestamp: Optional[TimeStamp] - # Associates this Feature with a point in time. - - _camera: Optional[Camera] - - _look_at: Optional[LookAt] + _view: Union[Camera, LookAt, None] # TODO Region = None # Features and geometry associated with a Region are drawn only when @@ -173,18 +165,17 @@ def __init__( styles: Optional[List[Style]] = None, style_url: Optional[str] = None, extended_data: None = None, - camera: Optional[Camera] = None, - look_at: Optional[LookAt] = None, + view: Optional[Union[Camera, LookAt]] = None, address: Optional[str] = None, phone_number: Optional[str] = None, + times: Optional[Union[TimeSpan, TimeStamp]] = None, ) -> None: super().__init__(ns=ns, name_spaces=name_spaces, id=id, target_id=target_id) self.name = name self.description = description self.style_url = style_url self._styles = [] - self._camera = camera - self._look_at = look_at + self._view = view self.visibility = visibility self.isopen = isopen self.snippet = snippet @@ -196,6 +187,7 @@ def __init__( for style in styles: self.append_style(style) self.extended_data = extended_data + self._times = times @property def style_url(self) -> Optional[str]: @@ -221,22 +213,12 @@ def style_url(self, styleurl: Union[str, StyleUrl, None]) -> None: raise ValueError @property - def camera(self): - return self._camera - - @camera.setter - def camera(self, camera) -> None: - if isinstance(camera, Camera): - self._camera = camera - - @property - def look_at(self) -> datetime: - return self._look_at + def view(self) -> Optional[Union[Camera, LookAt]]: + return self._view - @look_at.setter - def look_at(self, look_at) -> None: - if isinstance(look_at, LookAt): - self._look_at = look_at + @view.setter + def view(self, camera: Optional[Union[Camera, LookAt]]) -> None: + self._view = camera @property def link(self): @@ -331,34 +313,20 @@ def snippet(self, snip=None) -> None: ) @property - def address(self) -> None: - if self._address: - return self._address - return None + def address(self) -> Optional[str]: + return self._address @address.setter - def address(self, address) -> None: - if isinstance(address, str): - self._address = address - elif address is None: - self._address = None - else: - raise ValueError + def address(self, address: Optional[str]) -> None: + self._address = address @property - def phone_number(self) -> None: - if self._phone_number: - return self._phone_number - return None + def phone_number(self) -> Optional[str]: + return self._phone_number @phone_number.setter - def phone_number(self, phone_number) -> None: - if isinstance(phone_number, str): - self._phone_number = phone_number - elif phone_number is None: - self._phone_number = None - else: - raise ValueError + def phone_number(self, phone_number: Optional[str]) -> None: + self._phone_number = phone_number def etree_element( self, @@ -372,13 +340,8 @@ def etree_element( if self.description: description = config.etree.SubElement(element, f"{self.ns}description") description.text = self.description - if (self.camera is not None) and (self.look_at is not None): - msg = "Either Camera or LookAt can be defined, not both" - raise ValueError(msg) - if self.camera is not None: - element.append(self._camera.etree_element()) - elif self.look_at is not None: - element.append(self._look_at.etree_element()) + if self._view is not None: + element.append(self._view.etree_element()) if self.visibility is not None: visibility = config.etree.SubElement(element, f"{self.ns}visibility") visibility.text = str(self.visibility) @@ -398,13 +361,8 @@ def etree_element( snippet.text = self.snippet["text"] if self.snippet.get("maxLines"): snippet.set("maxLines", str(self.snippet["maxLines"])) - if (self._timespan is not None) and (self._timestamp is not None): - msg = "Either Timestamp or Timespan can be defined, not both" - raise ValueError(msg) - elif self._timespan is not None: - element.append(self._timespan.etree_element()) - elif self._timestamp is not None: - element.append(self._timestamp.etree_element()) + elif self._times is not None: + element.append(self._times.etree_element()) if self._atom_link is not None: element.append(self._atom_link.etree_element()) if self._atom_author is not None: @@ -512,9 +470,18 @@ def from_element(self, element: Element, strict: bool = False) -> None: self.phone_number = phone_number.text camera = element.find(f"{self.ns}Camera") if camera is not None: - s = Camera(self.ns) - s.from_element(camera) - self.camera = s + self._view = Camera.class_from_element( + ns=self.ns, + element=camera, + strict=strict, + ) + lookat = element.find(f"{self.ns}LookAt") + if lookat is not None: + self._view = LookAt.class_from_element( + ns=self.ns, + element=lookat, + strict=strict, + ) class Placemark(_Feature): @@ -632,3 +599,58 @@ def etree_element( else: logger.error("Object does not have a geometry") return element + + +class NetworkLink(_Feature): + __name__ = "NetworkLink" + _nlink = None + + def __init__( + self, + ns=None, + id=None, + name=None, + description=None, + styles=None, + styleUrl=None, + ): + super().__init__(ns, id, name, description, styles, styleUrl) + + @property + def link(self): + return self._nlink.href + + @link.setter + def link(self, url): + if isinstance(url, basestring): + self._nlink = atom.Link(href=url) + elif isinstance(url, Link): + self._nlink = url + elif url is None: + self._nlink = None + else: + raise TypeError + + def etree_element(self): + element = super().etree_element() + if self._nlink is not None: + element.append(self._nlink.etree_element()) + return element + + def from_element(self, element): + super(_Feature, self).from_element(element) + name = element.find(f"{self.ns}name") + if name is not None: + self.name = name.text + id = element.find(f"{self.ns}id") + if id is not None: + self.id = id.text + visibility = element.find(f"{self.ns}visibility") + if visibility is not None: + self.visibility = visibility.text + + link = element.find(f"{self.ns}Link") + if link is not None: + s = Link() + s.from_element(link) + self._nlink = s diff --git a/fastkml/mixins.py b/fastkml/mixins.py index cfc6adc7..37c63942 100644 --- a/fastkml/mixins.py +++ b/fastkml/mixins.py @@ -16,6 +16,7 @@ """Mixins for the KML classes.""" import logging from typing import Optional +from typing import Union from fastkml.times import KmlDateTime from fastkml.times import TimeSpan @@ -25,54 +26,17 @@ class TimeMixin: - _timespan: Optional[TimeSpan] = None - _timestamp: Optional[TimeStamp] = None + _times: Optional[Union[TimeSpan, TimeStamp]] = None @property def time_stamp(self) -> Optional[KmlDateTime]: """This just returns the datetime portion of the timestamp.""" - return self._timestamp.timestamp if self._timestamp is not None else None - - @time_stamp.setter - def time_stamp(self, timestamp: Optional[KmlDateTime]) -> None: - if self._timestamp is None: - self._timestamp = TimeStamp(timestamp=timestamp) - elif timestamp is None: - self._timestamp = None - else: - self._timestamp.timestamp = timestamp - if self._timespan and self._timestamp: - logger.warning("Setting a TimeStamp, TimeSpan deleted") - self._timespan = None + return self._times.timestamp if isinstance(self._times, TimeStamp) else None @property def begin(self) -> Optional[KmlDateTime]: - return self._timespan.begin if self._timespan is not None else None - - @begin.setter - def begin(self, dt: Optional[KmlDateTime]) -> None: - if self._timespan is None: - self._timespan = TimeSpan(begin=dt) - elif dt is None and self._timespan.end is None: - self._timespan = None - else: - self._timespan.begin = dt - if self._timespan and self._timestamp: - logger.warning("Setting a TimeSpan, TimeStamp deleted") - self._timestamp = None + return self._times.begin if isinstance(self._times, TimeSpan) else None @property def end(self) -> Optional[KmlDateTime]: - return self._timespan.end if self._timespan is not None else None - - @end.setter - def end(self, dt: Optional[KmlDateTime]) -> None: - if self._timespan is None: - self._timespan = TimeSpan(end=dt) - elif dt is None and self._timespan.begin is None: - self._timespan = None - else: - self._timespan.end = dt - if self._timespan and self._timestamp: - logger.warning("Setting a TimeSpan, TimeStamp deleted") - self._timestamp = None + return self._times.end if isinstance(self._times, TimeSpan) else None diff --git a/fastkml/overlays.py b/fastkml/overlays.py index 49946c75..8565bb31 100644 --- a/fastkml/overlays.py +++ b/fastkml/overlays.py @@ -3,10 +3,13 @@ """ import logging +from typing import Any from typing import Dict +from typing import List from typing import Optional from typing import Union +from fastkml import atom from fastkml import config from fastkml import gx from fastkml.enums import GridOrigin @@ -19,7 +22,12 @@ from fastkml.geometry import Point from fastkml.geometry import Polygon from fastkml.links import Icon +from fastkml.styles import Style +from fastkml.times import TimeSpan +from fastkml.times import TimeStamp from fastkml.types import Element +from fastkml.views import Camera +from fastkml.views import LookAt logger = logging.getLogger(__name__) @@ -70,9 +78,19 @@ def __init__( target_id: Optional[str] = None, name: Optional[str] = None, description: Optional[str] = None, - color: Optional[str] = None, - styles: None = None, + snippet: Optional[Union[str, Dict[str, Any]]] = None, + atom_author: Optional[atom.Author] = None, + atom_link: Optional[atom.Link] = None, + visibility: Optional[bool] = None, + isopen: Optional[bool] = None, + styles: Optional[List[Style]] = None, style_url: Optional[str] = None, + extended_data: None = None, + view: Optional[Union[Camera, LookAt]] = None, + address: Optional[str] = None, + phone_number: Optional[str] = None, + times: Optional[Union[TimeSpan, TimeStamp]] = None, + color: Optional[str] = None, draw_order: Optional[str] = None, icon: Optional[Icon] = None, ) -> None: @@ -85,6 +103,16 @@ def __init__( description=description, styles=styles, style_url=style_url, + snippet=snippet, + atom_author=atom_author, + atom_link=atom_link, + visibility=visibility, + isopen=isopen, + extended_data=extended_data, + view=view, + address=address, + phone_number=phone_number, + times=times, ) self._icon = icon self._color = color diff --git a/fastkml/views.py b/fastkml/views.py index dac97d72..dd178a73 100644 --- a/fastkml/views.py +++ b/fastkml/views.py @@ -85,10 +85,7 @@ def __init__( self._heading = heading self._tilt = tilt self._altitude_mode = altitude_mode - if isinstance(time_primitive, TimeSpan): - self._timespan = time_primitive - elif isinstance(time_primitive, TimeStamp): - self._timestamp = time_primitive + self._times = time_primitive @property def longitude(self) -> Optional[float]: @@ -199,31 +196,28 @@ def etree_element( f"{self.ns}tilt", ) tilt.text = str(self.tilt) - if self.altitude_mode in ( - AltitudeMode.clamp_to_ground, - AltitudeMode.relative_to_ground, - AltitudeMode.absolute, - ): - altitude_mode = config.etree.SubElement( # type: ignore[attr-defined] - element, - f"{self.ns}altitudeMode", - ) - elif self.altitude_mode in ( - AltitudeMode.clamp_to_sea_floor, - AltitudeMode.relative_to_sea_floor, - ): - altitude_mode = config.etree.SubElement( # type: ignore[attr-defined] - element, - f"{self.name_spaces['gx']}altitudeMode", - ) - altitude_mode.text = self.altitude_mode.value - if (self._timespan is not None) and (self._timestamp is not None): - msg = "Either Timestamp or Timespan can be defined, not both" - raise ValueError(msg) - if self._timespan is not None: - element.append(self._timespan.etree_element()) - elif self._timestamp is not None: - element.append(self._timestamp.etree_element()) + if self.altitude_mode: + if self.altitude_mode in ( + AltitudeMode.clamp_to_ground, + AltitudeMode.relative_to_ground, + AltitudeMode.absolute, + ): + altitude_mode = config.etree.SubElement( # type: ignore[attr-defined] + element, + f"{self.ns}altitudeMode", + ) + elif self.altitude_mode in ( + AltitudeMode.clamp_to_sea_floor, + AltitudeMode.relative_to_sea_floor, + ): + altitude_mode = config.etree.SubElement( # type: ignore[attr-defined] + element, + f"{self.name_spaces['gx']}altitudeMode", + ) + altitude_mode.text = self.altitude_mode.value + + if self._times is not None: + element.append(self._times.etree_element()) return element # TODO: diff --git a/tests/containers_test.py b/tests/containers_test.py new file mode 100644 index 00000000..b166aba6 --- /dev/null +++ b/tests/containers_test.py @@ -0,0 +1,40 @@ +# Copyright (C) 2021 - 2023 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 kml classes.""" +import pytest + +from fastkml import containers +from fastkml import features +from tests.base import Lxml +from tests.base import StdLibrary + + +class TestStdLibrary(StdLibrary): + """Test with the standard library.""" + + def test_container_base(self) -> None: + f = containers._Container(name="A Container") + # apparently you can add documents to containes + # d = kml.Document() + # self.assertRaises(TypeError, f.append, d) + p = features.Placemark() + f.append(p) + pytest.raises(NotImplementedError, f.etree_element) + + +class TestLxml(Lxml, TestStdLibrary): + """Test with lxml.""" diff --git a/tests/features_test.py b/tests/features_test.py new file mode 100644 index 00000000..05e13a2f --- /dev/null +++ b/tests/features_test.py @@ -0,0 +1,74 @@ +# Copyright (C) 2021 -2023 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 kml classes.""" +import pytest + +from fastkml import features +from tests.base import Lxml +from tests.base import StdLibrary + + +class TestStdLibrary(StdLibrary): + """Test with the standard library.""" + + def test_feature_base(self) -> None: + f = features._Feature(name="A Feature") + pytest.raises(NotImplementedError, f.etree_element) + assert f.name == "A Feature" + assert f.visibility is None + assert f.isopen is None + assert f._atom_author is None + assert f._atom_link is None + assert f.address is None + # self.assertEqual(f.phoneNumber, None) + assert f._snippet is None + assert f.description is None + assert f._style_url is None + assert f._styles == [] + assert f._times is None + # self.assertEqual(f.region, None) + # self.assertEqual(f.extended_data, None) + + f.__name__ = "Feature" + f.style_url = "#default" + assert "Feature>" in str(f.to_string()) + assert "#default" in str(f.to_string()) + + def test_address_string(self) -> None: + f = features._Feature() + address = "1600 Amphitheatre Parkway, Mountain View, CA 94043, USA" + f.address = address + assert f.address == address + + def test_address_none(self) -> None: + f = features._Feature() + f.address = None + assert f.address is None + + def test_phone_number_string(self) -> None: + f = features._Feature() + f.phone_number = "+1-234-567-8901" + assert f.phone_number == "+1-234-567-8901" + + def test_phone_number_none(self) -> None: + f = features._Feature() + f.phone_number = None + assert f.phone_number is None + + +class TestLxml(Lxml, TestStdLibrary): + """Test with lxml.""" diff --git a/tests/kml_test.py b/tests/links_test.py similarity index 95% rename from tests/kml_test.py rename to tests/links_test.py index c371d846..83278f7d 100644 --- a/tests/kml_test.py +++ b/tests/links_test.py @@ -15,7 +15,7 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA """Test the kml classes.""" -from fastkml import kml +from fastkml import links from fastkml.enums import RefreshMode from fastkml.enums import ViewRefreshMode from tests.base import Lxml @@ -27,7 +27,7 @@ class TestStdLibrary(StdLibrary): def test_icon(self) -> None: """Test the Icon class.""" - icon = kml.Icon( + icon = links.Icon( id="icon-01", href="http://maps.google.com/mapfiles/kml/paddle/red-circle.png", refresh_mode="onInterval", @@ -51,7 +51,7 @@ def test_icon(self) -> None: def test_icon_read(self) -> None: """Test the Icon class.""" - icon = kml.Icon.class_from_string( + icon = links.Icon.class_from_string( """ http://maps.google.com/mapfiles/kml/paddle/red-circle.png @@ -76,7 +76,7 @@ def test_icon_read(self) -> None: assert icon.view_format == "BBOX=[bboxWest],[bboxSouth],[bboxEast],[bboxNorth]" assert icon.http_query == "clientName=fastkml" - icon2 = kml.Icon.class_from_string(icon.to_string()) + icon2 = links.Icon.class_from_string(icon.to_string()) assert icon2.to_string() == icon.to_string() diff --git a/tests/oldunit_test.py b/tests/oldunit_test.py index 44d8117e..d3f33b22 100644 --- a/tests/oldunit_test.py +++ b/tests/oldunit_test.py @@ -25,7 +25,6 @@ from fastkml import config from fastkml import kml from fastkml import styles -from fastkml.enums import AltitudeMode from fastkml.enums import ColorMode from fastkml.enums import DisplayMode @@ -75,39 +74,6 @@ def test_base_object(self) -> None: assert bo.etree_element() is not None assert len(bo.to_string()) > 1 - def test_feature(self) -> None: - f = kml._Feature(name="A Feature") - pytest.raises(NotImplementedError, f.etree_element) - assert f.name == "A Feature" - assert f.visibility is None - assert f.isopen is None - assert f._atom_author is None - assert f._atom_link is None - assert f.address is None - # self.assertEqual(f.phoneNumber, None) - assert f._snippet is None - assert f.description is None - assert f._style_url is None - assert f._styles == [] - assert f._timespan is None - assert f._timestamp is None - # self.assertEqual(f.region, None) - # self.assertEqual(f.extended_data, None) - - f.__name__ = "Feature" - f.style_url = "#default" - assert "Feature>" in str(f.to_string()) - assert "#default" in str(f.to_string()) - - def test_container(self) -> None: - f = kml._Container(name="A Container") - # apparently you can add documents to containes - # d = kml.Document() - # self.assertRaises(TypeError, f.append, d) - p = kml.Placemark() - f.append(p) - pytest.raises(NotImplementedError, f.etree_element) - def test_overlay(self) -> None: o = kml._Overlay(name="An Overlay") assert o._color is None @@ -1310,456 +1276,3 @@ def test_nested_multigeometry(self) -> None: g for g in first_multigeometry.geoms if g.geom_type == "GeometryCollection" ) assert len(list(second_multigeometry.geoms)) == 2 - - -class TestBaseFeature: - def test_address_string(self) -> None: - f = kml._Feature() - address = "1600 Amphitheatre Parkway, Mountain View, CA 94043, USA" - f.address = address - assert f.address == address - - def test_address_none(self) -> None: - f = kml._Feature() - f.address = None - assert f.address is None - - def test_address_value_error(self) -> None: - f = kml._Feature() - with pytest.raises(ValueError): - f.address = 123 - - def test_phone_number_string(self) -> None: - f = kml._Feature() - f.phone_number = "+1-234-567-8901" - assert f.phone_number == "+1-234-567-8901" - - def test_phone_number_none(self) -> None: - f = kml._Feature() - f.phone_number = None - assert f.phone_number is None - - def test_phone_number_value_error(self) -> None: - f = kml._Feature() - with pytest.raises(ValueError): - f.phone_number = 123 - - -class TestBaseOverlay: - def test_color_string(self) -> None: - o = kml._Overlay(name="An Overlay") - o.color = "00010203" - assert o.color == "00010203" - - def test_color_none(self) -> None: - o = kml._Overlay(name="An Overlay") - o.color = "00010203" - assert o.color == "00010203" - o.color = None - assert o.color is None - - def test_color_value_error(self) -> None: - o = kml._Overlay(name="An Overlay") - with pytest.raises(ValueError): - o.color = object() - - def test_draw_order_string(self) -> None: - o = kml._Overlay(name="An Overlay") - o.draw_order = "1" - assert o.draw_order == "1" - - def test_draw_order_int(self) -> None: - o = kml._Overlay(name="An Overlay") - o.draw_order = 1 - assert o.draw_order == "1" - - def test_draw_order_none(self) -> None: - o = kml._Overlay(name="An Overlay") - o.draw_order = "1" - assert o.draw_order == "1" - o.draw_order = None - assert o.draw_order is None - - def test_draw_order_value_error(self) -> None: - o = kml._Overlay(name="An Overlay") - with pytest.raises(ValueError): - o.draw_order = object() - - def test_icon_raise_exception(self) -> None: - o = kml._Overlay(name="An Overlay") - with pytest.raises(ValueError): - o.icon = 12345 - - -class TestGroundOverlay: - def setup_method(self) -> None: - self.g = kml.GroundOverlay() - - def test_altitude_int(self) -> None: - self.g.altitude = 123 - assert self.g.altitude == "123" - - def test_altitude_float(self) -> None: - self.g.altitude = 123.4 - assert self.g.altitude == "123.4" - - def test_altitude_string(self) -> None: - self.g.altitude = "123" - assert self.g.altitude == "123" - - def test_altitude_value_error(self) -> None: - with pytest.raises(ValueError): - self.g.altitude = object() - - def test_altitude_none(self) -> None: - self.g.altitude = "123" - assert self.g.altitude == "123" - self.g.altitude = None - assert self.g.altitude is None - - def test_altitude_mode_default(self) -> None: - assert self.g.altitude_mode == "clampToGround" - - def test_altitude_mode_error(self) -> None: - self.g.altitude_mode = "" - assert self.g.altitude_mode == "clampToGround" - - def test_altitude_mode_clamp(self) -> None: - self.g.altitude_mode = "clampToGround" - assert self.g.altitude_mode == "clampToGround" - - def test_altitude_mode_absolute(self) -> None: - self.g.altitude_mode = "absolute" - assert self.g.altitude_mode == "absolute" - - def test_latlonbox_function(self) -> None: - self.g.lat_lon_box(10, 20, 30, 40, 50) - - assert self.g.north == "10" - assert self.g.south == "20" - assert self.g.east == "30" - assert self.g.west == "40" - assert self.g.rotation == "50" - - def test_latlonbox_string(self) -> None: - self.g.north = "10" - self.g.south = "20" - self.g.east = "30" - self.g.west = "40" - self.g.rotation = "50" - - assert self.g.north == "10" - assert self.g.south == "20" - assert self.g.east == "30" - assert self.g.west == "40" - assert self.g.rotation == "50" - - def test_latlonbox_int(self) -> None: - self.g.north = 10 - self.g.south = 20 - self.g.east = 30 - self.g.west = 40 - self.g.rotation = 50 - - assert self.g.north == "10" - assert self.g.south == "20" - assert self.g.east == "30" - assert self.g.west == "40" - assert self.g.rotation == "50" - - def test_latlonbox_float(self) -> None: - self.g.north = 10.0 - self.g.south = 20.0 - self.g.east = 30.0 - self.g.west = 40.0 - self.g.rotation = 50.0 - - assert self.g.north == "10.0" - assert self.g.south == "20.0" - assert self.g.east == "30.0" - assert self.g.west == "40.0" - assert self.g.rotation == "50.0" - - def test_latlonbox_value_error(self) -> None: - with pytest.raises(ValueError): - self.g.north = object() - - with pytest.raises(ValueError): - self.g.south = object() - - with pytest.raises(ValueError): - self.g.east = object() - - with pytest.raises(ValueError): - self.g.west = object() - - with pytest.raises(ValueError): - self.g.rotation = object() - - assert self.g.north is None - assert self.g.south is None - assert self.g.east is None - assert self.g.west is None - assert self.g.rotation is None - - def test_latlonbox_empty_string(self) -> None: - self.g.north = "" - self.g.south = "" - self.g.east = "" - self.g.west = "" - self.g.rotation = "" - - assert not self.g.north - assert not self.g.south - assert not self.g.east - assert not self.g.west - assert not self.g.rotation - - def test_latlonbox_none(self) -> None: - self.g.north = None - self.g.south = None - self.g.east = None - self.g.west = None - self.g.rotation = None - - assert self.g.north is None - assert self.g.south is None - assert self.g.east is None - assert self.g.west is None - assert self.g.rotation is None - - -class TestGroundOverlayString: - def test_default_to_string(self) -> None: - g = kml.GroundOverlay() - - expected = kml.GroundOverlay() - expected.from_string( - '' - "", - ) - assert g.to_string() == expected.to_string() - - def test_to_string(self) -> None: - g = kml.GroundOverlay() - icon = kml.Icon(href="http://example.com") - g.icon = icon - g.draw_order = 1 - g.color = "00010203" - - expected = kml.GroundOverlay() - expected.from_string( - '' - "00010203" - "1" - "" - "http://example.com" - "" - "", - ) - - assert g.to_string() == expected.to_string() - - def test_altitude_from_int(self) -> None: - g = kml.GroundOverlay() - g.altitude = 123 - - expected = kml.GroundOverlay() - expected.from_string( - '' - "123" - "clampToGround" - "", - ) - - assert g.to_string() == expected.to_string() - - def test_altitude_from_float(self) -> None: - g = kml.GroundOverlay() - g.altitude = 123.4 - - expected = kml.GroundOverlay() - expected.from_string( - '' - "123.4" - "clampToGround" - "", - ) - - assert g.to_string() == expected.to_string() - - def test_altitude_from_string(self) -> None: - g = kml.GroundOverlay() - g.altitude = "123.4" - - expected = kml.GroundOverlay() - expected.from_string( - '' - "123.4" - "clampToGround" - "", - ) - - assert g.to_string() == expected.to_string() - - def test_altitude_mode_absolute(self) -> None: - g = kml.GroundOverlay() - g.altitude = "123.4" - g.altitude_mode = "absolute" - - expected = kml.GroundOverlay() - expected.from_string( - '' - "123.4" - "absolute" - "", - ) - - assert g.to_string() == expected.to_string() - - def test_altitude_mode_unknown_string(self) -> None: - g = kml.GroundOverlay() - g.altitude = "123.4" - g.altitudeMode = "unknown string" - - expected = kml.GroundOverlay() - expected.from_string( - '' - "123.4" - "clampToGround" - "", - ) - - assert g.to_string() == expected.to_string() - - def test_altitude_mode_value(self) -> None: - g = kml.GroundOverlay() - g.altitude = "123.4" - g.altitudeMode = 1234 - - expected = kml.GroundOverlay() - expected.from_string( - '' - "123.4" - "clampToGround" - "", - ) - - assert g.to_string() == expected.to_string() - - def test_latlonbox_no_rotation(self) -> None: - g = kml.GroundOverlay() - g.lat_lon_box(10, 20, 30, 40) - - expected = kml.GroundOverlay() - expected.from_string( - '' - "" - "10" - "20" - "30" - "40" - "0" - "" - "", - ) - - assert g.to_string() == expected.to_string() - - def test_latlonbox_rotation(self) -> None: - g = kml.GroundOverlay() - g.lat_lon_box(10, 20, 30, 40, 50) - - expected = kml.GroundOverlay() - expected.from_string( - '' - "" - "10" - "20" - "30" - "40" - "50" - "" - "", - ) - - assert g.to_string() == expected.to_string() - - def test_latlonbox_nswer(self) -> None: - g = kml.GroundOverlay() - g.north = 10 - g.south = 20 - g.east = 30 - g.west = 40 - g.rotation = 50 - - expected = kml.GroundOverlay() - expected.from_string( - '' - "" - "10" - "20" - "30" - "40" - "50" - "" - "", - ) - - assert g.to_string() == expected.to_string() - - -class TestPhotoOverlay: - def setup_method(self) -> None: - self.p = kml.PhotoOverlay() - self.p.camera = kml.Camera() - - def test_camera_altitude_int(self) -> None: - self.p.camera.altitude = 123 - assert self.p.camera.altitude == 123 - - def test_camera_altitude_float(self) -> None: - self.p.camera.altitude = 123.4 - assert self.p.camera.altitude == 123.4 - - def test_camera_altitude_string(self) -> None: - self.p.camera.altitude = 123 - assert self.p.camera.altitude == 123 - - def test_camera_altitude_value_error(self) -> None: - with pytest.raises(ValueError): - self.p.camera.altitude = object() - - def test_camera_altitude_none(self) -> None: - self.p.camera.altitude = 123 - assert self.p.camera.altitude == 123 - self.p.camera.altitude = None - assert self.p.camera.altitude is None - - def test_camera_altitude_mode_default(self) -> None: - assert self.p.camera.altitude_mode == AltitudeMode("relativeToGround") - - def test_camera_altitude_mode_clamp(self) -> None: - self.p.camera.altitude_mode = AltitudeMode("clampToGround") - assert self.p.camera.altitude_mode == AltitudeMode("clampToGround") - - def test_camera_altitude_mode_absolute(self) -> None: - self.p.camera.altitude_mode = "absolute" - assert self.p.camera.altitude_mode == "absolute" - - def test_camera_initialization(self) -> None: - self.p.camera = kml.Camera( - longitude=10, - latitude=20, - altitude=30, - heading=40, - tilt=50, - roll=60, - ) - assert self.p.camera.longitude == 10 - assert self.p.camera.latitude == 20 - assert self.p.camera.altitude == 30 - assert self.p.camera.heading == 40 - assert self.p.camera.tilt == 50 - assert self.p.camera.roll == 60 - assert self.p.camera.altitude_mode == AltitudeMode("relativeToGround") diff --git a/tests/overlays_test.py b/tests/overlays_test.py new file mode 100644 index 00000000..43baebeb --- /dev/null +++ b/tests/overlays_test.py @@ -0,0 +1,491 @@ +# Copyright (C) 2021 - 2023 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 kml classes.""" +import pytest + +from fastkml import links +from fastkml import overlays +from fastkml import views +from fastkml.enums import AltitudeMode +from tests.base import Lxml +from tests.base import StdLibrary + + +class TestBaseOverlay(StdLibrary): + def test_color_string(self) -> None: + o = overlays._Overlay(name="An Overlay") + o.color = "00010203" + assert o.color == "00010203" + + def test_color_none(self) -> None: + o = overlays._Overlay(name="An Overlay") + o.color = "00010203" + assert o.color == "00010203" + o.color = None + assert o.color is None + + def test_color_value_error(self) -> None: + o = overlays._Overlay(name="An Overlay") + with pytest.raises(ValueError): + o.color = object() + + def test_draw_order_string(self) -> None: + o = overlays._Overlay(name="An Overlay") + o.draw_order = "1" + assert o.draw_order == "1" + + def test_draw_order_int(self) -> None: + o = overlays._Overlay(name="An Overlay") + o.draw_order = 1 + assert o.draw_order == "1" + + def test_draw_order_none(self) -> None: + o = overlays._Overlay(name="An Overlay") + o.draw_order = "1" + assert o.draw_order == "1" + o.draw_order = None + assert o.draw_order is None + + def test_draw_order_value_error(self) -> None: + o = overlays._Overlay(name="An Overlay") + with pytest.raises(ValueError): + o.draw_order = object() + + def test_icon_raise_exception(self) -> None: + o = overlays._Overlay(name="An Overlay") + with pytest.raises(ValueError): + o.icon = 12345 + + +class TestGroundOverlay(StdLibrary): + def test_altitude_int(self) -> None: + go = overlays.GroundOverlay() + + go.altitude = 123 + assert go.altitude == "123" + + def test_altitude_float(self) -> None: + go = overlays.GroundOverlay() + + go.altitude = 123.4 + assert go.altitude == "123.4" + + def test_altitude_string(self) -> None: + go = overlays.GroundOverlay() + + go.altitude = "123" + assert go.altitude == "123" + + def test_altitude_value_error(self) -> None: + go = overlays.GroundOverlay() + + with pytest.raises(ValueError): + go.altitude = object() + + def test_altitude_none(self) -> None: + go = overlays.GroundOverlay() + + go.altitude = "123" + assert go.altitude == "123" + go.altitude = None + assert go.altitude is None + + def test_altitude_mode_default(self) -> None: + go = overlays.GroundOverlay() + + assert go.altitude_mode == "clampToGround" + + def test_altitude_mode_error(self) -> None: + go = overlays.GroundOverlay() + + go.altitude_mode = "" + assert go.altitude_mode == "clampToGround" + + def test_altitude_mode_clamp(self) -> None: + go = overlays.GroundOverlay() + + go.altitude_mode = "clampToGround" + assert go.altitude_mode == "clampToGround" + + def test_altitude_mode_absolute(self) -> None: + go = overlays.GroundOverlay() + + go.altitude_mode = "absolute" + assert go.altitude_mode == "absolute" + + def test_latlonbox_function(self) -> None: + go = overlays.GroundOverlay() + + go.lat_lon_box(10, 20, 30, 40, 50) + + assert go.north == "10" + assert go.south == "20" + assert go.east == "30" + assert go.west == "40" + assert go.rotation == "50" + + def test_latlonbox_string(self) -> None: + go = overlays.GroundOverlay() + + go.north = "10" + go.south = "20" + go.east = "30" + go.west = "40" + go.rotation = "50" + + assert go.north == "10" + assert go.south == "20" + assert go.east == "30" + assert go.west == "40" + assert go.rotation == "50" + + def test_latlonbox_int(self) -> None: + go = overlays.GroundOverlay() + + go.north = 10 + go.south = 20 + go.east = 30 + go.west = 40 + go.rotation = 50 + + assert go.north == "10" + assert go.south == "20" + assert go.east == "30" + assert go.west == "40" + assert go.rotation == "50" + + def test_latlonbox_float(self) -> None: + go = overlays.GroundOverlay() + go.north = 10.0 + go.south = 20.0 + go.east = 30.0 + go.west = 40.0 + go.rotation = 50.0 + + assert go.north == "10.0" + assert go.south == "20.0" + assert go.east == "30.0" + assert go.west == "40.0" + assert go.rotation == "50.0" + + def test_latlonbox_value_error(self) -> None: + go = overlays.GroundOverlay() + with pytest.raises(ValueError): + go.north = object() + + with pytest.raises(ValueError): + go.south = object() + + with pytest.raises(ValueError): + go.east = object() + + with pytest.raises(ValueError): + go.west = object() + + with pytest.raises(ValueError): + go.rotation = object() + + assert go.north is None + assert go.south is None + assert go.east is None + assert go.west is None + assert go.rotation is None + + def test_latlonbox_empty_string(self) -> None: + go = overlays.GroundOverlay() + go.north = "" + go.south = "" + go.east = "" + go.west = "" + go.rotation = "" + + assert not go.north + assert not go.south + assert not go.east + assert not go.west + assert not go.rotation + + def test_latlonbox_none(self) -> None: + go = overlays.GroundOverlay() + go.north = None + go.south = None + go.east = None + go.west = None + go.rotation = None + + assert go.north is None + assert go.south is None + assert go.east is None + assert go.west is None + assert go.rotation is None + + +class TestGroundOverlayString(StdLibrary): + def test_default_to_string(self) -> None: + g = overlays.GroundOverlay() + + expected = overlays.GroundOverlay() + expected.from_string( + '' + "", + ) + assert g.to_string() == expected.to_string() + + def test_to_string(self) -> None: + g = overlays.GroundOverlay() + icon = links.Icon(href="http://example.com") + g.icon = icon + g.draw_order = 1 + g.color = "00010203" + + expected = overlays.GroundOverlay() + expected.from_string( + '' + "00010203" + "1" + "" + "http://example.com" + "" + "", + ) + + assert g.to_string() == expected.to_string() + + def test_altitude_from_int(self) -> None: + g = overlays.GroundOverlay() + g.altitude = 123 + + expected = overlays.GroundOverlay() + expected.from_string( + '' + "123" + "clampToGround" + "", + ) + + assert g.to_string() == expected.to_string() + + def test_altitude_from_float(self) -> None: + g = overlays.GroundOverlay() + g.altitude = 123.4 + + expected = overlays.GroundOverlay() + expected.from_string( + '' + "123.4" + "clampToGround" + "", + ) + + assert g.to_string() == expected.to_string() + + def test_altitude_from_string(self) -> None: + g = overlays.GroundOverlay() + g.altitude = "123.4" + + expected = overlays.GroundOverlay() + expected.from_string( + '' + "123.4" + "clampToGround" + "", + ) + + assert g.to_string() == expected.to_string() + + def test_altitude_mode_absolute(self) -> None: + g = overlays.GroundOverlay() + g.altitude = "123.4" + g.altitude_mode = "absolute" + + expected = overlays.GroundOverlay() + expected.from_string( + '' + "123.4" + "absolute" + "", + ) + + assert g.to_string() == expected.to_string() + + def test_altitude_mode_unknown_string(self) -> None: + g = overlays.GroundOverlay() + g.altitude = "123.4" + g.altitudeMode = "unknown string" + + expected = overlays.GroundOverlay() + expected.from_string( + '' + "123.4" + "clampToGround" + "", + ) + + assert g.to_string() == expected.to_string() + + def test_altitude_mode_value(self) -> None: + g = overlays.GroundOverlay() + g.altitude = "123.4" + g.altitudeMode = 1234 + + expected = overlays.GroundOverlay() + expected.from_string( + '' + "123.4" + "clampToGround" + "", + ) + + assert g.to_string() == expected.to_string() + + def test_latlonbox_no_rotation(self) -> None: + g = overlays.GroundOverlay() + g.lat_lon_box(10, 20, 30, 40) + + expected = overlays.GroundOverlay() + expected.from_string( + '' + "" + "10" + "20" + "30" + "40" + "0" + "" + "", + ) + + assert g.to_string() == expected.to_string() + + def test_latlonbox_rotation(self) -> None: + g = overlays.GroundOverlay() + g.lat_lon_box(10, 20, 30, 40, 50) + + expected = overlays.GroundOverlay() + expected.from_string( + '' + "" + "10" + "20" + "30" + "40" + "50" + "" + "", + ) + + assert g.to_string() == expected.to_string() + + def test_latlonbox_nswer(self) -> None: + g = overlays.GroundOverlay() + g.north = 10 + g.south = 20 + g.east = 30 + g.west = 40 + g.rotation = 50 + + expected = overlays.GroundOverlay() + expected.from_string( + '' + "" + "10" + "20" + "30" + "40" + "50" + "" + "", + ) + + assert g.to_string() == expected.to_string() + + +class TestPhotoOverlay(StdLibrary): + def test_camera_altitude_int(self) -> None: + po = overlays.PhotoOverlay(view=views.Camera()) + po.view.altitude = 123 + assert po.view.altitude == 123 + + def test_camera_altitude_float(self) -> None: + po = overlays.PhotoOverlay(view=views.Camera()) + po.view.altitude = 123.4 + assert po.view.altitude == 123.4 + + def test_camera_altitude_string(self) -> None: + po = overlays.PhotoOverlay(view=views.Camera()) + po.view.altitude = 123 + assert po.view.altitude == 123 + + def test_camera_altitude_value_error(self) -> None: + po = overlays.PhotoOverlay(view=views.Camera()) + with pytest.raises(ValueError): + po.view.altitude = object() + + def test_camera_altitude_none(self) -> None: + po = overlays.PhotoOverlay(view=views.Camera()) + po.view.altitude = 123 + assert po.view.altitude == 123 + po.view.altitude = None + assert po.view.altitude is None + + def test_camera_altitude_mode_default(self) -> None: + po = overlays.PhotoOverlay(view=views.Camera()) + assert po.view.altitude_mode == AltitudeMode("relativeToGround") + + def test_camera_altitude_mode_clamp(self) -> None: + po = overlays.PhotoOverlay(view=views.Camera()) + po.view.altitude_mode = AltitudeMode("clampToGround") + assert po.view.altitude_mode == AltitudeMode("clampToGround") + + def test_camera_altitude_mode_absolute(self) -> None: + po = overlays.PhotoOverlay(view=views.Camera()) + po.view.altitude_mode = "absolute" + assert po.view.altitude_mode == "absolute" + + def test_camera_initialization(self) -> None: + po = overlays.PhotoOverlay(view=views.Camera()) + po.view = views.Camera( + longitude=10, + latitude=20, + altitude=30, + heading=40, + tilt=50, + roll=60, + ) + assert po.view.longitude == 10 + assert po.view.latitude == 20 + assert po.view.altitude == 30 + assert po.view.heading == 40 + assert po.view.tilt == 50 + assert po.view.roll == 60 + assert po.view.altitude_mode == AltitudeMode("relativeToGround") + + +class TestBaseOverlayLxml(Lxml, TestBaseOverlay): + """Test with lxml.""" + + +class TestGroundOverlayLxml(Lxml, TestGroundOverlay): + """Test with lxml.""" + + +class TestGroundOverlayStringLxml(Lxml, TestGroundOverlay): + """Test with lxml.""" + + +class TestPhotoOverlayLxml(Lxml, TestPhotoOverlay): + """Test with lxml.""" diff --git a/tests/times_test.py b/tests/times_test.py index 5fc46851..f986b35b 100644 --- a/tests/times_test.py +++ b/tests/times_test.py @@ -222,6 +222,7 @@ def test_timespan(self) -> None: ts.begin = None pytest.raises(ValueError, ts.to_string) + @pytest.mark.skip(reason="not yet implemented") def test_feature_timestamp(self) -> None: now = datetime.datetime.now() f = kml.Document() @@ -236,6 +237,7 @@ def test_feature_timestamp(self) -> None: f.time_stamp = None assert "TimeStamp>" not in str(f.to_string()) + @pytest.mark.skip(reason="not yet implemented") def test_feature_timespan(self) -> None: now = datetime.datetime.now() y2k = datetime.datetime(2000, 1, 1) @@ -258,44 +260,6 @@ def test_feature_timespan(self) -> None: f.begin = None assert "TimeSpan>" not in str(f.to_string()) - def test_feature_timespan_stamp(self) -> None: - now = datetime.datetime.now() - y2k = datetime.date(2000, 1, 1) - f = kml.Document() - f.begin = KmlDateTime(y2k) - f.end = KmlDateTime(now) - assert now.isoformat() in str(f.to_string()) - assert "2000-01-01" in str(f.to_string()) - assert "TimeSpan>" in str(f.to_string()) - assert "begin>" in str(f.to_string()) - assert "end>" in str(f.to_string()) - assert "TimeStamp>" not in str(f.to_string()) - assert "when>" not in str(f.to_string()) - # when we set a timestamp an existing timespan will be deleted - f.time_stamp = KmlDateTime(now) - assert now.isoformat() in str(f.to_string()) - assert "TimeStamp>" in str(f.to_string()) - assert "when>" in str(f.to_string()) - assert "2000-01-01" not in str(f.to_string()) - assert "TimeSpan>" not in str(f.to_string()) - assert "begin>" not in str(f.to_string()) - assert "end>" not in str(f.to_string()) - # when we set a timespan an existing timestamp will be deleted - f.end = y2k - assert now.isoformat() not in str(f.to_string()) - assert "2000-01-01" in str(f.to_string()) - assert "TimeSpan>" in str(f.to_string()) - assert "begin>" not in str(f.to_string()) - assert "end>" in str(f.to_string()) - assert "TimeStamp>" not in str(f.to_string()) - assert "when>" not in str(f.to_string()) - # We manipulate our Feature so it has timespan and stamp - ts = kml.TimeStamp(timestamp=now) - f._timestamp = ts - # this raises an exception as only either timespan or timestamp - # are allowed not both - pytest.raises(ValueError, f.to_string) - def test_read_timestamp_year(self) -> None: doc = """ diff --git a/tests/views_test.py b/tests/views_test.py index 4a9cd8f2..ace14449 100644 --- a/tests/views_test.py +++ b/tests/views_test.py @@ -131,7 +131,7 @@ def test_create_look_at(self) -> None: assert look_at.longitude == 60 assert look_at.id == "look-at-id" assert look_at.target_id == "target-look-at-id" - assert look_at._timestamp.timestamp.dt == datetime.datetime( + assert look_at._times.timestamp.dt == datetime.datetime( 2019, 1, 1, @@ -166,7 +166,7 @@ def test_look_at_read(self) -> None: assert look_at.longitude == 60 assert look_at.id == "look-at-id" assert look_at.target_id == "target-look-at-id" - assert look_at._timestamp.timestamp.dt == datetime.datetime( + assert look_at._times.timestamp.dt == datetime.datetime( 2019, 1, 1,