diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index abe79d57..ff36e7a3 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -40,7 +40,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/run-all-tests.yml b/.github/workflows/run-all-tests.yml index 14972357..ead106f4 100644 --- a/.github/workflows/run-all-tests.yml +++ b/.github/workflows/run-all-tests.yml @@ -9,10 +9,10 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12-dev'] + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] steps: - - uses: actions/checkout@v3.2.0 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: @@ -29,10 +29,10 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] steps: - - uses: actions/checkout@v3.2.0 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: @@ -46,9 +46,11 @@ jobs: run: | pytest tests --cov=fastkml --cov=tests --cov-fail-under=88 --cov-report=xml - name: "Upload coverage to Codecov" + if: ${{ matrix.python-version==3.11 }} uses: codecov/codecov-action@v3 with: fail_ci_if_error: true + verbose: true static-tests: runs-on: ubuntu-latest @@ -57,7 +59,7 @@ jobs: python-version: ['3.9'] steps: - - uses: actions/checkout@v3.2.0 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: @@ -73,7 +75,8 @@ jobs: run: | flake8 fastkml examples docs black --check fastkml examples docs - yamllint .github/workflows/ + yamllint .github/ + yamllint .*.y*ml - name: Check complexity run: | radon cc --min B fastkml @@ -84,9 +87,9 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - pypy-version: ['pypy-3.7', 'pypy-3.8', 'pypy-3.9'] + pypy-version: ['pypy-3.7', 'pypy-3.8', 'pypy-3.9', 'pypy-3.10'] steps: - - uses: actions/checkout@v3.2.0 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.pypy-version }} uses: actions/setup-python@v4 with: @@ -104,7 +107,7 @@ jobs: name: Build and publish to PyPI and TestPyPI runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3.2.0 + - uses: actions/checkout@v4 - name: Set up Python 3.9 uses: actions/setup-python@v4 with: diff --git a/.gitignore b/.gitignore index 45d354fa..b0730ad2 100644 --- a/.gitignore +++ b/.gitignore @@ -23,10 +23,14 @@ setup.cfg # Installer logs pip-log.txt -# Unit test / coverage reports +# Unit/static test / coverage reports .coverage +coverage.xml .tox +.pytest_cache/ nosetests.xml +.ruff_cache/ +monkeytype.sqlite3 # Translations *.mo @@ -51,6 +55,8 @@ venv .mypy_cache/ .pyre/ .watchmanconfig +.pytype/ + # misc .dccache diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 76c2e53a..b6edf3c7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ --- repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: check-added-large-files - id: check-docstring-first @@ -20,8 +20,17 @@ repos: - id: pretty-format-json - id: requirements-txt-fixer - id: trailing-whitespace + + - repo: https://github.com/tox-dev/pyproject-fmt + rev: "1.2.0" + hooks: + - id: pyproject-fmt + - repo: https://github.com/abravalheri/validate-pyproject + rev: v0.15 + hooks: + - id: validate-pyproject - repo: https://github.com/ikamensh/flynt/ - rev: "0.77" + rev: "1.0.1" hooks: - id: flynt - repo: https://github.com/MarcoGorelli/absolufy-imports @@ -29,31 +38,51 @@ repos: hooks: - id: absolufy-imports - repo: https://github.com/hakancelikdev/unimport - rev: 33ead41ee30f1d323a9c2fcfd0114297efbbc4d5 + rev: 1.0.0 hooks: - id: unimport args: [--remove, --include-star-import, --ignore-init, --gitignore] - repo: https://github.com/asottile/pyupgrade - rev: v3.3.1 + rev: v3.15.0 hooks: - id: pyupgrade args: ["--py3-plus", "--py37-plus"] - repo: https://github.com/psf/black - rev: 22.12.0 + rev: 23.9.1 hooks: - id: black - repo: https://github.com/PyCQA/flake8 - rev: 6.0.0 + rev: 6.1.0 hooks: - id: flake8 - repo: https://github.com/pycqa/isort - rev: 5.11.4 + rev: 5.12.0 hooks: - id: isort - - repo: https://github.com/mgedmin/check-manifest - rev: "0.49" + - repo: https://github.com/charliermarsh/ruff-pre-commit + rev: 'v0.0.292' + hooks: + - id: ruff + - repo: https://github.com/PyCQA/flake8 + rev: 6.1.0 hooks: - - id: check-manifest + - id: flake8 + - repo: https://github.com/pre-commit/pygrep-hooks + rev: v1.10.0 # Use the ref you want to point at + hooks: + - id: python-use-type-annotations + - id: python-check-blanket-type-ignore + - id: python-check-mock-methods + - id: python-no-log-warn + - id: python-use-type-annotations + - id: rst-backticks + - id: rst-directive-colons + - id: rst-inline-touching-normal + - id: text-unicode-replacement-char + # - repo: https://github.com/mgedmin/check-manifest + # rev: "0.49" + # hooks: + # - id: check-manifest # - repo: https://github.com/Lucas-C/pre-commit-hooks-markup # rev: v1.0.1 # hooks: diff --git a/MANIFEST.in b/MANIFEST.in index c8ecd40b..8ae540fc 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,10 +1,10 @@ include *.rst recursive-include docs *.txt recursive-exclude *.pyc *.pyo -include docs/LICENSE.GPL -exclude fastkml/.* +include docs LICENSE.GPL include *.txt recursive-include docs *.py recursive-include docs *.rst recursive-include docs Makefile recursive-include tests *.py +recursive-include schema *.xsd diff --git a/README.rst b/README.rst index 09c3ea32..6df6e595 100644 --- a/README.rst +++ b/README.rst @@ -30,7 +30,7 @@ Fastkml is continually tested :target: http://codecov.io/github/cleder/fastkml?branch=main :alt: codecov.io -.. image:: https://img.shields.io/badge/code%20style-black-000000.svg +.. image:: https://img.shields.io/badge/code+style-black-000000.svg :target: https://github.com/psf/black :alt: Black diff --git a/docs/contributing.rst b/docs/contributing.rst index a2ee904e..319aaa39 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -15,7 +15,7 @@ especially in the following ways: Pull Requests ------------- -Start by submitting a pull request on GitHub against the `develop` branch of the +Start by submitting a pull request on GitHub against the ``develop`` branch of the repository. Your pull request should provide a good description of the change you are making, and/or the bug that you are fixing. @@ -64,6 +64,15 @@ available. .. _tox: https://pypi.python.org/pypi/tox +coverage +~~~~~~~~ + +You can also run the tests with coverage_ to see which lines are covered by the +tests. This is useful for writing new tests to cover any uncovered lines:: + + pytest tests --cov=fastkml --cov=tests --cov-report=xml + + pre-commit ~~~~~~~~~~~ diff --git a/docs/index.rst b/docs/index.rst index 10c07498..45b422b6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -63,7 +63,7 @@ requirements, namely: * You can parse any KML snippet, it does not need to be a complete KML document. * It is fully tested and actively maintained. -* Geometries are handled in the `__geo_interface__` standard. +* Geometries are handled in the ``__geo_interface__`` standard. * Minimal dependencies, pure Python. * If available, lxml_ will be used to increase its speed. diff --git a/examples/UsageExamples.py b/examples/UsageExamples.py index fa2221c2..552a2d7f 100644 --- a/examples/UsageExamples.py +++ b/examples/UsageExamples.py @@ -13,7 +13,6 @@ def print_child_features(element): if __name__ == "__main__": - fname = "KML_Samples.kml" k = kml.KML() diff --git a/fastkml/__init__.py b/fastkml/__init__.py index cbe4b4c2..4f603f57 100644 --- a/fastkml/__init__.py +++ b/fastkml/__init__.py @@ -34,7 +34,6 @@ from fastkml.data import ExtendedData from fastkml.data import Schema from fastkml.data import SchemaData -from fastkml.gx import GxGeometry from fastkml.kml import KML from fastkml.kml import Document from fastkml.kml import Folder @@ -66,7 +65,6 @@ "TimeStamp", "ExtendedData", "Data", - "GxGeometry", "Schema", "SchemaData", "StyleUrl", diff --git a/fastkml/atom.py b/fastkml/atom.py index 9ae52ce3..a07be3b1 100644 --- a/fastkml/atom.py +++ b/fastkml/atom.py @@ -38,6 +38,7 @@ from fastkml.base import _XMLObject from fastkml.config import ATOMNS as NS +from fastkml.enums import Verbosity from fastkml.helpers import o_from_attr from fastkml.helpers import o_from_subelement_text from fastkml.helpers import o_int_from_attr @@ -167,8 +168,12 @@ def __init__( def from_element(self, element: Element) -> None: super().from_element(element) - def etree_element(self) -> Element: - return super().etree_element() + def etree_element( + self, + precision: Optional[int] = None, + verbosity: Verbosity = Verbosity.normal, + ) -> Element: + return super().etree_element(precision=precision, verbosity=verbosity) class _Person(_XMLObject): @@ -227,8 +232,12 @@ def __init__( self.uri = uri self.email = email - def etree_element(self) -> Element: - return super().etree_element() + def etree_element( + self, + precision: Optional[int] = None, + verbosity: Verbosity = Verbosity.normal, + ) -> Element: + return super().etree_element(precision=precision, verbosity=verbosity) def from_element(self, element: Element) -> None: super().from_element(element) diff --git a/fastkml/base.py b/fastkml/base.py index 7b2defba..048fbf91 100644 --- a/fastkml/base.py +++ b/fastkml/base.py @@ -16,11 +16,14 @@ """Abstract base classes""" import logging +from typing import Any +from typing import Dict from typing import Optional from typing import Tuple from typing import cast from fastkml import config +from fastkml.enums import Verbosity from fastkml.helpers import o_from_attr from fastkml.helpers import o_to_attr from fastkml.types import Element @@ -32,14 +35,36 @@ class _XMLObject: """XML Baseclass.""" + _namespaces: Tuple[str, ...] = ("",) + _node_name: str = "" __name__ = "" kml_object_mapping: Tuple[KmlObjectMap, ...] = () def __init__(self, ns: Optional[str] = None) -> None: """Initialize the XML Object.""" - self.ns: str = config.KMLNS if ns is None else ns + self.ns: str = self._namespaces[0] if ns is None else ns - def etree_element(self) -> Element: + def __eq__(self, other: object) -> bool: + """Compare two XML Objects.""" + if not isinstance(other, self.__class__): + return False + return ( + other.ns == self.ns or other.ns in self._namespaces + if self.ns == "" + else True + ) + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(ns={self.ns})" + + def __str__(self) -> str: + return self.to_string() + + def etree_element( + self, + precision: Optional[int] = None, + verbosity: Verbosity = Verbosity.normal, + ) -> Element: """Return the KML Object as an Element.""" if self.__name__: element: Element = config.etree.Element( # type: ignore[attr-defined] @@ -54,25 +79,44 @@ def etree_element(self) -> Element: return element def from_element(self, element: Element) -> None: - """Load the KML Object from an Element.""" + """ + Load the KML Object from an Element. + + This implementation is deprecated and will be replaced by class_from_element + making it a classmethod. + """ if f"{self.ns}{self.__name__}" != element.tag: raise TypeError("Call of abstract base class, subclasses implement this!") for mapping in self.kml_object_mapping: mapping["from_kml"](self, element, **mapping) def from_string(self, xml_string: str) -> None: - """Load the KML Object from serialized xml.""" + """ + Load the KML Object from serialized xml. + + This implementation is deprecated and will be replaced by class_from_string + making it a classmethod. + """ self.from_element( cast(Element, config.etree.XML(xml_string)) # type: ignore[attr-defined] ) - def to_string(self, prettyprint: bool = True) -> str: + def to_string( + self, + *, + prettyprint: bool = True, + precision: Optional[int] = None, + verbosity: Verbosity = Verbosity.normal, + ) -> str: """Return the KML Object as serialized xml.""" try: return cast( str, config.etree.tostring( # type: ignore[attr-defined] - self.etree_element(), + self.etree_element( + precision=precision, + verbosity=verbosity, + ), encoding="UTF-8", pretty_print=prettyprint, ).decode("UTF-8"), @@ -85,6 +129,63 @@ def to_string(self, prettyprint: bool = True) -> str: ).decode("UTF-8"), ) + @classmethod + def _get_ns(cls, ns: Optional[str]) -> str: + return cls._namespaces[0] if ns is None else ns + + @classmethod + def _get_kwargs( + cls, + *, + ns: str, + element: Element, + strict: bool, + ) -> Dict[str, Any]: + """Returns a dictionary of kwargs for the class constructor.""" + kwargs: Dict[str, Any] = {} + return kwargs + + @classmethod + def class_from_element( + cls, + *, + ns: str, + element: Element, + strict: bool, + ) -> "_XMLObject": + """Creates an XML object from an etree element.""" + kwargs = cls._get_kwargs(ns=ns, element=element, strict=strict) + return cls( + ns=ns, + **kwargs, + ) + + @classmethod + def class_from_string( + cls, + string: str, + *, + ns: Optional[str] = None, + strict: bool = True, + ) -> "_XMLObject": + """Creates a geometry object from a string. + + Args: + string: String representation of the geometry object + + Returns: + Geometry object + """ + ns = cls._get_ns(ns) + return cls.class_from_element( + ns=ns, + strict=strict, + element=cast( + Element, + config.etree.fromstring(string), # type: ignore[attr-defined] + ), + ) + class _BaseObject(_XMLObject): """ @@ -98,6 +199,9 @@ class _BaseObject(_XMLObject): mechanism is to be used. """ + _namespace = config.KMLNS + _namespaces: Tuple[str, ...] = (config.KMLNS,) + id = None target_id = None kml_object_mapping: Tuple[KmlObjectMap, ...] = ( @@ -130,10 +234,50 @@ def __init__( self.id = id self.target_id = target_id - def etree_element(self) -> Element: + def __eq__(self, other: object) -> bool: + assert isinstance(other, self.__class__) + return ( + super().__eq__(other) + and self.id == other.id + and self.target_id == other.target_id + ) + + def __repr__(self) -> str: + return ( + f"{self.__class__.__name__}(ns={self.ns!r}, " + f"(id={self.id!r}, target_id={self.target_id!r})" + ) + + def etree_element( + self, + precision: Optional[int] = None, + verbosity: Verbosity = Verbosity.normal, + ) -> Element: """Return the KML Object as an Element.""" - return super().etree_element() + return super().etree_element(precision=precision, verbosity=verbosity) def from_element(self, element: Element) -> None: """Load the KML Object from an Element.""" super().from_element(element) + + @classmethod + def _get_id(cls, element: Element, strict: bool) -> str: + return element.get("id") or "" + + @classmethod + def _get_target_id(cls, element: Element, strict: bool) -> str: + return element.get("targetId") or "" + + @classmethod + def _get_kwargs( + cls, + *, + ns: str, + element: Element, + strict: bool, + ) -> Dict[str, Any]: + """Get the keyword arguments to build the object from an element.""" + return { + "id": cls._get_id(element=element, strict=strict), + "target_id": cls._get_target_id(element=element, strict=strict), + } diff --git a/fastkml/config.py b/fastkml/config.py index f7970c13..699ce5a3 100644 --- a/fastkml/config.py +++ b/fastkml/config.py @@ -51,13 +51,16 @@ def set_etree_implementation(implementation: ModuleType) -> None: ATOMNS = "{http://www.w3.org/2005/Atom}" # noqa: FS003 GXNS = "{http://www.google.com/kml/ext/2.2}" # noqa: FS003 -DEFAULT_NAME_SPACES = { - "kml": KMLNS[1:-1], - "atom": ATOMNS[1:-1], - "gx": GXNS[1:-1], +NAME_SPACES = { + "kml": KMLNS, + "atom": ATOMNS, + "gx": GXNS, } +DEFAULT_NAME_SPACES = {k: v[1:-1] for k, v in NAME_SPACES.items()} + + def register_namespaces(**namespaces: str) -> None: """Register namespaces for use in etree.ElementTree.parse().""" try: diff --git a/fastkml/data.py b/fastkml/data.py index 18ace775..72a5bb7a 100644 --- a/fastkml/data.py +++ b/fastkml/data.py @@ -14,6 +14,7 @@ # along with this library; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA """Add Custom Data""" +import logging from typing import Dict from typing import List from typing import Optional @@ -26,8 +27,11 @@ import fastkml.config as config from fastkml.base import _BaseObject from fastkml.base import _XMLObject +from fastkml.enums import Verbosity from fastkml.types import Element +logger = logging.getLogger(__name__) + class SimpleField(TypedDict): name: str @@ -55,11 +59,6 @@ class Schema(_BaseObject): __name__ = "Schema" - # The declaration of the custom fields, each of which must specify both the - # type and the name of this field. If either the type or the name is - # omitted, the field is ignored. - name = None - def __init__( self, ns: Optional[str] = None, @@ -150,11 +149,13 @@ def append(self, type: str, name: str, display_name: Optional[str] = None) -> No "bool", ] if type not in allowed_types: - raise TypeError( - f"{name} has the type {type} which is invalid. " + logger.warning( + "%s has the type %s which is invalid. " "The type must be one of " "'string', 'int', 'uint', 'short', " - "'ushort', 'float', 'double', 'bool'" + "'ushort', 'float', 'double', 'bool'", + name, + type, ) self._simple_fields.append( {"type": type, "name": name, "displayName": display_name or ""} @@ -171,8 +172,12 @@ def from_element(self, element: Element) -> None: sfdisplay_name = display_name.text if display_name is not None else None self.append(sftype, sfname, sfdisplay_name) - def etree_element(self) -> Element: - element = super().etree_element() + def etree_element( + self, + precision: Optional[int] = None, + verbosity: Verbosity = Verbosity.normal, + ) -> Element: + element = super().etree_element(precision=precision, verbosity=verbosity) if self.name: element.set("name", self.name) for simple_field in self.simple_fields: @@ -207,8 +212,20 @@ def __init__( self.value = value self.display_name = display_name - def etree_element(self) -> Element: - element = super().etree_element() + def __repr__(self) -> str: + return ( + f"{self.__class__.__name__}(" + f"ns='{self.ns}'," + f"name='{self.name}', value='{self.value}'" + f"display_name='{self.display_name}')" + ) + + def etree_element( + self, + precision: Optional[int] = None, + verbosity: Verbosity = Verbosity.normal, + ) -> Element: + element = super().etree_element(precision=precision, verbosity=verbosity) element.set("name", self.name or "") value = config.etree.SubElement( # type: ignore[attr-defined] element, f"{self.ns}value" @@ -250,8 +267,12 @@ def __init__( super().__init__(ns) self.elements = elements or [] - def etree_element(self) -> Element: - element = super().etree_element() + def etree_element( + self, + precision: Optional[int] = None, + verbosity: Verbosity = Verbosity.normal, + ) -> Element: + element = super().etree_element(precision=precision, verbosity=verbosity) for subelement in self.elements: element.append(subelement.etree_element()) return element @@ -352,8 +373,12 @@ def append_data(self, name: str, value: Union[int, str]) -> None: else: raise TypeError("name must be a nonempty string") - def etree_element(self) -> Element: - element = super().etree_element() + def etree_element( + self, + precision: Optional[int] = None, + verbosity: Verbosity = Verbosity.normal, + ) -> Element: + element = super().etree_element(precision=precision, verbosity=verbosity) element.set("schemaUrl", self.schema_url) for data in self.data: sd = config.etree.SubElement( # type: ignore[attr-defined] diff --git a/fastkml/enums.py b/fastkml/enums.py index b875796c..8c1ab589 100644 --- a/fastkml/enums.py +++ b/fastkml/enums.py @@ -17,6 +17,15 @@ from enum import unique +@unique +class Verbosity(Enum): + """Enum to represent the different verbosity levels.""" + + quiet = 0 + normal = 1 + verbose = 2 + + @unique class DateTimeResolution(Enum): """Enum to represent the different date time resolutions.""" @@ -25,3 +34,51 @@ class DateTimeResolution(Enum): date = "date" year_month = "gYearMonth" year = "gYear" + + +@unique +class AltitudeMode(Enum): + """ + Enum to represent the different altitude modes. + + Specifies how altitude components in the element are interpreted. + Possible values are + - clampToGround - (default) Indicates to ignore an altitude specification + (for example, in the tag). + - relativeToGround - Sets the altitude of the element relative to the actual + ground elevation of a particular location. + For example, if the ground elevation of a location is exactly at sea level + and the altitude for a point is set to 9 meters, + then the elevation for the icon of a point placemark elevation is 9 meters + with this mode. + However, if the same coordinate is set over a location where the ground + elevation is 10 meters above sea level, then the elevation of the coordinate + is 19 meters. + A typical use of this mode is for placing telephone poles or a ski lift. + - absolute - Sets the altitude of the coordinate relative to sea level, + regardless of the actual elevation of the terrain beneath the element. + For example, if you set the altitude of a coordinate to 10 meters with an + absolute altitude mode, the icon of a point placemark will appear to be at + ground level if the terrain beneath is also 10 meters above sea level. + If the terrain is 3 meters above sea level, the placemark will appear elevated + above the terrain by 7 meters. + A typical use of this mode is for aircraft placement. + - relativeToSeaFloor - Interprets the altitude as a value in meters above the + sea floor. + If the point is above land rather than sea, the altitude will be interpreted + as being above the ground. + - clampToSeaFloor - The altitude specification is ignored, and the point will be + positioned on the sea floor. + If the point is on land rather than at sea, the point will be positioned on + the ground. + + The Values relativeToSeaFloor and clampToSeaFloor are not part of the KML definition + but of the a KML extension in the Google extension namespace, + allowing altitudes relative to the sea floor. + """ + + clamp_to_ground = "clampToGround" + relative_to_ground = "relativeToGround" + absolute = "absolute" + clamp_to_sea_floor = "clampToSeaFloor" + relative_to_sea_floor = "relativeToSeaFloor" diff --git a/tests/geometry_test.py b/fastkml/exceptions.py similarity index 68% rename from tests/geometry_test.py rename to fastkml/exceptions.py index 25499538..600aaee3 100644 --- a/tests/geometry_test.py +++ b/fastkml/exceptions.py @@ -1,4 +1,4 @@ -# Copyright (C) 2021 Christian Ledermann +# Copyright (C) 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 @@ -13,15 +13,16 @@ # 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 +"""Exceptions for the fastkml package.""" -"""Test the geometry classes.""" -from tests.base import Lxml -from tests.base import StdLibrary +class FastKMLError(Exception): + """Base class for all fastkml exceptions.""" -class TestStdLibrary(StdLibrary): - """Test with the standard library.""" +class KMLParseError(FastKMLError): + """Raised when there is an error parsing KML.""" -class TestLxml(Lxml, TestStdLibrary): - """Test with lxml.""" + +class KMLWriteError(FastKMLError): + """Raised when there is an error writing KML.""" diff --git a/fastkml/geometry.py b/fastkml/geometry.py index 62b0c5ab..7a8aa5db 100644 --- a/fastkml/geometry.py +++ b/fastkml/geometry.py @@ -14,151 +14,114 @@ # along with this library; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +import contextlib import logging import re +from functools import partial from typing import Any +from typing import Dict from typing import List from typing import Optional from typing import Sequence from typing import Union from typing import cast -from pygeoif.factories import shape -from pygeoif.geometry import GeometryCollection -from pygeoif.geometry import LinearRing -from pygeoif.geometry import LineString -from pygeoif.geometry import MultiLineString -from pygeoif.geometry import MultiPoint -from pygeoif.geometry import MultiPolygon -from pygeoif.geometry import Point -from pygeoif.geometry import Polygon +import pygeoif.geometry as geo from pygeoif.types import PointType from fastkml import config from fastkml.base import _BaseObject +from fastkml.enums import AltitudeMode +from fastkml.enums import Verbosity +from fastkml.exceptions import KMLParseError +from fastkml.exceptions import KMLWriteError from fastkml.types import Element logger = logging.getLogger(__name__) -GeometryType = Union[Polygon, LineString, LinearRing, Point] -MultiGeometryType = Union[MultiPoint, MultiLineString, MultiPolygon, GeometryCollection] +GeometryType = Union[geo.Polygon, geo.LineString, geo.LinearRing, geo.Point] +MultiGeometryType = Union[ + geo.MultiPoint, geo.MultiLineString, geo.MultiPolygon, geo.GeometryCollection +] AnyGeometryType = Union[GeometryType, MultiGeometryType] -class Geometry(_BaseObject): - """ """ +class _Geometry(_BaseObject): + """Baseclass with common methods for all geometry objects. - geometry = None - extrude = False - tessellate = False - altitude_mode = None + Attributes: extrude: boolean --> Specifies whether to connect the feature to + the ground with a line. + tessellate: boolean --> Specifies whether to allow the LineString + to follow the terrain. + altitudeMode: --> Specifies how altitude components in the + element are interpreted. + + """ def __init__( self, + *, ns: Optional[str] = None, id: Optional[str] = None, - geometry: Optional[Any] = None, - extrude: bool = False, - tessellate: bool = False, - altitude_mode: Optional[str] = None, + target_id: Optional[str] = None, + extrude: Optional[bool] = False, + tessellate: Optional[bool] = False, + altitude_mode: Optional[AltitudeMode] = None, + geometry: Optional[AnyGeometryType] = None, ) -> None: """ - geometry: a geometry that implements the __geo_interface__ convention - - extrude: boolean --> Specifies whether to connect the feature to - the ground with a line. To extrude a Feature, the value for - 'altitudeMode' must be either relativeToGround, relativeToSeaFloor, - or absolute. The feature is extruded toward the center of the - Earth's sphere. - tessellate: boolean --> Specifies whether to allow the LineString - to follow the terrain. To enable tessellation, the altitude - mode must be clampToGround or clampToSeaFloor. Very large - LineStrings should enable tessellation so that they follow - the curvature of the earth (otherwise, they may go underground - and be hidden). - This field is not used by Polygon or Point. To allow a Polygon - to follow the terrain (that is, to enable tessellation) specify - an altitude mode of clampToGround or clampToSeaFloor. - altitudeMode: [clampToGround, relativeToGround, absolute] --> - Specifies how altitude components in the element - are interpreted. Possible values are - clampToGround - (default) Indicates to ignore an altitude - specification. - relativeToGround - Sets the altitude of the element relative - to the actual ground elevation of a particular location. - For example, if the ground elevation of a location is - exactly at sea level and the altitude for a point is - set to 9 meters, then the elevation for the icon of a - point placemark elevation is 9 meters with this mode. - However, if the same coordinate is set over a location - where the ground elevation is 10 meters above sea level, - then the elevation of the coordinate is 19 meters. - A typical use of this mode is for placing telephone - poles or a ski lift. - absolute - Sets the altitude of the coordinate relative to - sea level, regardless of the actual elevation of the - terrain beneath the element. For example, if you set - the altitude of a coordinate to 10 meters with an - absolute altitude mode, the icon of a point placemark - will appear to be at ground level if the terrain beneath - is also 10 meters above sea level. If the terrain is - 3 meters above sea level, the placemark will appear - elevated above the terrain by 7 meters. A typical use - of this mode is for aircraft placement. - - https://developers.google.com/kml/documentation/kmlreference#geometry + + Args: + ns: Namespace of the object + id: Id of the object + target_id: Target id of the object + extrude: Specifies whether to connect the feature to the ground with a line. + tessellate: Specifies whether to allow the LineString to follow the terrain. + altitude_mode: Specifies how altitude components in the + element are interpreted. """ - super().__init__(ns, id) - self.extrude = extrude - self.tessellate = tessellate - self.altitude_mode = altitude_mode - if geometry: - if isinstance( - geometry, - ( - Point, - LineString, - Polygon, - MultiPoint, - MultiLineString, - MultiPolygon, - LinearRing, - GeometryCollection, - ), - ): - self.geometry = geometry - else: - self.geometry = shape(geometry) + super().__init__(ns=ns, id=id, target_id=target_id) + self._extrude = extrude + self._tessellate = tessellate + self._altitude_mode = altitude_mode + self.geometry = geometry + + def __repr__(self) -> str: + return ( + f"{self.__class__.__name__}(" + f"ns={self.ns!r}, " + f"id={self.id!r}, " + f"target_id={self.target_id!r}, " + f"extrude={self.extrude!r}, " + f"tessellate={self.tessellate!r}, " + f"altitude_mode={self.altitude_mode} " + f"geometry={self.geometry!r}" + f")" + ) - # write kml + @property + def extrude(self) -> Optional[bool]: + return self._extrude - def _set_altitude_mode(self, element: Element) -> None: - if self.altitude_mode: - # XXX add 'relativeToSeaFloor', 'clampToSeaFloor', - assert self.altitude_mode in [ - "clampToGround", - "relativeToGround", - "absolute", - ] - if self.altitude_mode != "clampToGround": - am_element = config.etree.SubElement( # type: ignore[attr-defined] - element, f"{self.ns}altitudeMode" - ) - am_element.text = self.altitude_mode + @extrude.setter + def extrude(self, extrude: bool) -> None: + self._extrude = extrude - def _set_extrude(self, element: Element) -> None: - if self.extrude and self.altitude_mode in [ - "relativeToGround", - # 'relativeToSeaFloor', - "absolute", - ]: - et_element = cast( - Element, - config.etree.SubElement( # type: ignore[attr-defined] - element, f"{self.ns}extrude" - ), - ) - et_element.text = "1" + @property + def tessellate(self) -> Optional[bool]: + return self._tessellate + + @tessellate.setter + def tessellate(self, tessellate: bool) -> None: + self._tessellate = tessellate + + @property + def altitude_mode(self) -> Optional[AltitudeMode]: + return self._altitude_mode + + @altitude_mode.setter + def altitude_mode(self, altitude_mode: Optional[AltitudeMode]) -> None: + self._altitude_mode = altitude_mode def _etree_coordinates( self, @@ -169,305 +132,506 @@ def _etree_coordinates( config.etree.Element(f"{self.ns}coordinates"), # type: ignore[attr-defined] ) if len(coordinates[0]) == 2: - if config.FORCE3D: # and not clampToGround: - tuples = (f"{c[0]:f},{c[1]:f},0.000000" for c in coordinates) - else: - tuples = (f"{c[0]:f},{c[1]:f}" for c in coordinates) + tuples = (f"{c[0]:f},{c[1]:f}" for c in coordinates) elif len(coordinates[0]) == 3: tuples = ( f"{c[0]:f},{c[1]:f},{c[2]:f}" for c in coordinates # type: ignore[misc] ) else: - raise ValueError("Invalid dimensions") + raise KMLWriteError(f"Invalid dimensions in coordinates '{coordinates}'") element.text = " ".join(tuples) return element - def _etree_point(self, point: Point) -> Element: - element = self._extrude_and_altitude_mode("Point") - return self._extracted_from__etree_linearring_5(point, element) - - def _etree_linestring(self, linestring: LineString) -> Element: - element = self._extrude_and_altitude_mode("LineString") - if self.tessellate and self.altitude_mode in [ - "clampToGround", - "clampToSeaFloor", - ]: - ts_element = config.etree.SubElement( # type: ignore[attr-defined] - element, f"{self.ns}tessellate" + def _set_altitude_mode(self, element: Element) -> None: + if self.altitude_mode: + am_element = config.etree.SubElement( # type: ignore[attr-defined] + element, f"{self.ns}altitudeMode" ) - ts_element.text = "1" - return self._extracted_from__etree_linearring_5(linestring, element) + am_element.text = self.altitude_mode.value - def _etree_linearring(self, linearring: LinearRing) -> Element: - element = self._extrude_and_altitude_mode("LinearRing") - return self._extracted_from__etree_linearring_5(linearring, element) - - def _extracted_from__etree_linearring_5( - self, arg0: Union[LineString, LinearRing, Point], element: Element - ) -> Element: - coords = list(arg0.coords) - element.append(self._etree_coordinates(coords)) - return element + def _set_extrude(self, element: Element) -> None: + if self.extrude is not None: + et_element = cast( + Element, + config.etree.SubElement( # type: ignore[attr-defined] + element, f"{self.ns}extrude" + ), + ) + et_element.text = str(int(self.extrude)) - def _etree_polygon(self, polygon: Polygon) -> Element: - element = self._extrude_and_altitude_mode("Polygon") - outer_boundary = cast( - Element, - config.etree.SubElement( # type: ignore[attr-defined] - element, - f"{self.ns}outerBoundaryIs", - ), - ) - outer_boundary.append(self._etree_linearring(polygon.exterior)) - for ib in polygon.interiors: - inner_boundary = cast( + def _set_tessellate(self, element: Element) -> None: + if self.tessellate is not None: + t_element = cast( Element, config.etree.SubElement( # type: ignore[attr-defined] - element, - f"{self.ns}innerBoundaryIs", + element, f"{self.ns}tessellate" ), ) - inner_boundary.append(self._etree_linearring(ib)) + t_element.text = str(int(self.tessellate)) + + def etree_element( + self, + precision: Optional[int] = None, + verbosity: Verbosity = Verbosity.normal, + ) -> Element: + self.__name__ = self.__class__.__name__ + element = super().etree_element(precision=precision, verbosity=verbosity) + self._set_extrude(element) + self._set_altitude_mode(element) + self._set_tessellate(element) return element - def _extrude_and_altitude_mode(self, kml_geometry: str) -> Element: - result = cast( - Element, - config.etree.Element( # type: ignore[attr-defined] - f"{self.ns}{kml_geometry}" + @classmethod + def _get_coordinates( + cls, *, ns: str, element: Element, strict: bool + ) -> List[PointType]: + """ + Get coordinates from element. + + Coordinates can be any number of tuples separated by a space (potentially any + number of whitespace characters). + Values in tuples should be separated by commas with no spaces. + + https://developers.google.com/kml/documentation/kmlreference#coordinates + """ + coordinates = element.find(f"{ns}coordinates") + if coordinates is not None: + # Clean up badly formatted tuples by stripping + # space following commas. + try: + latlons = re.sub(r", +", ",", coordinates.text.strip()).split() + except AttributeError: + return [] + return [ + cast(PointType, tuple(float(c) for c in latlon.split(","))) + for latlon in latlons + ] + return [] + + @classmethod + def _get_extrude( + cls, + *, + ns: str, + element: Element, + strict: bool, + ) -> Optional[bool]: + extrude = element.find(f"{ns}extrude") + if extrude is None: + return None + with contextlib.suppress(ValueError, AttributeError): + return bool(int(extrude.text.strip())) + return None + + @classmethod + def _get_tessellate( + cls, + *, + ns: str, + element: Element, + strict: bool, + ) -> Optional[bool]: + tessellate = element.find(f"{ns}tessellate") + if tessellate is None: + return None + with contextlib.suppress(ValueError): + return bool(int(tessellate.text.strip())) + return None + + @classmethod + def _get_altitude_mode( + cls, + *, + ns: str, + element: Element, + strict: bool, + ) -> Optional[AltitudeMode]: + altitude_mode = element.find(f"{ns}altitudeMode") + if altitude_mode is None: + return None + with contextlib.suppress(ValueError): + return AltitudeMode(altitude_mode.text.strip()) + return None + + @classmethod + def _get_geometry_kwargs( + cls, + *, + ns: str, + element: Element, + strict: bool, + ) -> Dict[str, Any]: + return { + "extrude": cls._get_extrude(ns=ns, element=element, strict=strict), + "tessellate": cls._get_tessellate(ns=ns, element=element, strict=strict), + "altitude_mode": cls._get_altitude_mode( + ns=ns, element=element, strict=strict ), + } + + @classmethod + def _get_geometry( + cls, + *, + ns: str, + element: Element, + strict: bool, + ) -> Optional[AnyGeometryType]: + return None + + @classmethod + def _get_kwargs( + cls, + *, + ns: str, + element: Element, + strict: bool, + ) -> Dict[str, Any]: + kwargs = super()._get_kwargs(ns=ns, element=element, strict=strict) + kwargs.update(cls._get_geometry_kwargs(ns=ns, element=element, strict=strict)) + kwargs.update( + {"geometry": cls._get_geometry(ns=ns, element=element, strict=strict)} ) - self._set_extrude(result) - self._set_altitude_mode(result) - return result + return kwargs - def _etree_multipoint(self, points: MultiPoint) -> Element: - element = cast( - Element, - config.etree.Element( # type: ignore[attr-defined] - f"{self.ns}MultiGeometry" - ), + +class Point(_Geometry): + def __init__( + self, + *, + ns: Optional[str] = None, + id: Optional[str] = None, + target_id: Optional[str] = None, + extrude: Optional[bool] = False, + tessellate: Optional[bool] = False, + altitude_mode: Optional[AltitudeMode] = None, + geometry: geo.Point, + ) -> None: + super().__init__( + ns=ns, + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, ) - for point in points.geoms: - element.append(self._etree_point(point)) + + def etree_element( + self, + precision: Optional[int] = None, + verbosity: Verbosity = Verbosity.normal, + ) -> Element: + self.__name__ = self.__class__.__name__ + element = super().etree_element(precision=precision, verbosity=verbosity) + assert isinstance(self.geometry, geo.Point) + coords = self.geometry.coords + element.append(self._etree_coordinates(coords)) return element - def _etree_multilinestring(self, linestrings: MultiLineString) -> Element: - element = cast( - Element, - config.etree.Element( # type: ignore[attr-defined] - f"{self.ns}MultiGeometry" - ), + @classmethod + def _get_geometry( + cls, + *, + ns: str, + element: Element, + strict: bool, + ) -> geo.Point: + coords = cls._get_coordinates(ns=ns, element=element, strict=strict) + try: + return geo.Point.from_coordinates(coords) + except (IndexError, TypeError) as e: + error = config.etree.tostring( # type: ignore[attr-defined] + element, + encoding="UTF-8", + ).decode("UTF-8") + raise KMLParseError(f"Invalid coordinates in {error}") from e + + +class LineString(_Geometry): + def __init__( + self, + *, + ns: Optional[str] = None, + id: Optional[str] = None, + target_id: Optional[str] = None, + extrude: Optional[bool] = False, + tessellate: Optional[bool] = False, + altitude_mode: Optional[AltitudeMode] = None, + geometry: geo.LineString, + ) -> None: + super().__init__( + ns=ns, + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, ) - for linestring in linestrings.geoms: - element.append(self._etree_linestring(linestring)) + + def etree_element( + self, + precision: Optional[int] = None, + verbosity: Verbosity = Verbosity.normal, + ) -> Element: + self.__name__ = self.__class__.__name__ + element = super().etree_element(precision=precision, verbosity=verbosity) + assert isinstance(self.geometry, geo.LineString) + coords = self.geometry.coords + element.append(self._etree_coordinates(coords)) return element - def _etree_multipolygon(self, polygons: MultiPolygon) -> Element: - element = cast( - Element, - config.etree.Element( # type: ignore[attr-defined] - f"{self.ns}MultiGeometry" - ), + @classmethod + def _get_geometry( + cls, + *, + ns: str, + element: Element, + strict: bool, + ) -> geo.LineString: + coords = cls._get_coordinates(ns=ns, element=element, strict=strict) + try: + return geo.LineString.from_coordinates(coords) + except (IndexError, TypeError) as e: + error = config.etree.tostring( # type: ignore[attr-defined] + element, + encoding="UTF-8", + ).decode("UTF-8") + raise KMLParseError(f"Invalid coordinates in {error}") from e + + +class LinearRing(LineString): + def __init__( + self, + *, + ns: Optional[str] = None, + id: Optional[str] = None, + target_id: Optional[str] = None, + extrude: Optional[bool] = False, + tessellate: Optional[bool] = False, + altitude_mode: Optional[AltitudeMode] = None, + geometry: geo.LinearRing, + ) -> None: + super().__init__( + ns=ns, + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, ) - for polygon in polygons.geoms: - element.append(self._etree_polygon(polygon)) - return element - def _etree_collection(self, features: GeometryCollection) -> Element: - element = cast( + @classmethod + def _get_geometry( + cls, + *, + ns: str, + element: Element, + strict: bool, + ) -> geo.LinearRing: + coords = cls._get_coordinates(ns=ns, element=element, strict=strict) + try: + return cast(geo.LinearRing, geo.LinearRing.from_coordinates(coords)) + except (IndexError, TypeError) as e: + error = config.etree.tostring( # type: ignore[attr-defined] + element, + encoding="UTF-8", + ).decode("UTF-8") + raise KMLParseError(f"Invalid coordinates in {error}") from e + + +class Polygon(_Geometry): + def __init__( + self, + *, + ns: Optional[str] = None, + id: Optional[str] = None, + target_id: Optional[str] = None, + extrude: Optional[bool] = False, + tessellate: Optional[bool] = False, + altitude_mode: Optional[AltitudeMode] = None, + geometry: geo.Polygon, + ) -> None: + super().__init__( + ns=ns, + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + def etree_element( + self, + precision: Optional[int] = None, + verbosity: Verbosity = Verbosity.normal, + ) -> Element: + self.__name__ = self.__class__.__name__ + element = super().etree_element(precision=precision, verbosity=verbosity) + assert isinstance(self.geometry, geo.Polygon) + linear_ring = partial(LinearRing, ns=self.ns, extrude=None, tessellate=None) + outer_boundary = cast( Element, - config.etree.Element( # type: ignore[attr-defined] - f"{self.ns}MultiGeometry" + config.etree.SubElement( # type: ignore[attr-defined] + element, + f"{self.ns}outerBoundaryIs", ), ) - for feature in features.geoms: - if feature.geom_type == "Point": - element.append(self._etree_point(cast(Point, feature))) - elif feature.geom_type == "LinearRing": - element.append(self._etree_linearring(cast(LinearRing, feature))) - elif feature.geom_type == "LineString": - element.append(self._etree_linestring(cast(LineString, feature))) - elif feature.geom_type == "Polygon": - element.append(self._etree_polygon(cast(Polygon, feature))) - else: - raise ValueError("Illegal geometry type.") + outer_boundary.append( + linear_ring(geometry=self.geometry.exterior).etree_element( + precision=precision, verbosity=verbosity + ) + ) + for interior in self.geometry.interiors: + inner_boundary = cast( + Element, + config.etree.SubElement( # type: ignore[attr-defined] + element, + f"{self.ns}innerBoundaryIs", + ), + ) + inner_boundary.append( + linear_ring(geometry=interior).etree_element( + precision=precision, verbosity=verbosity + ) + ) return element - def etree_element(self) -> Element: - if isinstance(self.geometry, Point): - return self._etree_point(self.geometry) - elif isinstance(self.geometry, LinearRing): - return self._etree_linearring(self.geometry) - elif isinstance(self.geometry, LineString): - return self._etree_linestring(self.geometry) - elif isinstance(self.geometry, Polygon): - return self._etree_polygon(self.geometry) - elif isinstance(self.geometry, MultiPoint): - return self._etree_multipoint(self.geometry) - elif isinstance(self.geometry, MultiLineString): - return self._etree_multilinestring(self.geometry) - elif isinstance(self.geometry, MultiPolygon): - return self._etree_multipolygon(self.geometry) - elif isinstance(self.geometry, GeometryCollection): - return self._etree_collection(self.geometry) - else: - raise ValueError("Illegal geometry type.") + @classmethod + def _get_geometry(cls, *, ns: str, element: Element, strict: bool) -> geo.Polygon: + outer_boundary = element.find(f"{ns}outerBoundaryIs") + if outer_boundary is None: + error = config.etree.tostring( # type: ignore[attr-defined] + element, + encoding="UTF-8", + ).decode("UTF-8") + raise KMLParseError(f"Missing outerBoundaryIs in {error}") + outer_ring = outer_boundary.find(f"{ns}LinearRing") + if outer_ring is None: + error = config.etree.tostring( # type: ignore[attr-defined] + element, + encoding="UTF-8", + ).decode("UTF-8") + raise KMLParseError(f"Missing LinearRing in {error}") + exterior = LinearRing._get_geometry(ns=ns, element=outer_ring, strict=strict) + interiors = [] + for inner_boundary in element.findall(f"{ns}innerBoundaryIs"): + inner_ring = inner_boundary.find(f"{ns}LinearRing") + if inner_ring is None: + error = config.etree.tostring( # type: ignore[attr-defined] + element, + encoding="UTF-8", + ).decode("UTF-8") + raise KMLParseError(f"Missing LinearRing in {error}") + interiors.append( + LinearRing._get_geometry(ns=ns, element=inner_ring, strict=strict) + ) + return geo.Polygon.from_linear_rings(exterior, *interiors) - # read kml - def _get_geometry_spec(self, element: Element) -> None: - extrude = element.find(f"{self.ns}extrude") - if extrude is not None: - try: - et = bool(int(extrude.text.strip())) - except ValueError: - et = False - self.extrude = et - else: - self.extrude = False # type: ignore[unreachable] - tessellate = element.find(f"{self.ns}tessellate") - if tessellate is not None: - try: - te = bool(int(tessellate.text.strip())) - except ValueError: - te = False - self.tessellate = te - else: - self.tessellate = False # type: ignore[unreachable] - altitude_mode = element.find(f"{self.ns}altitudeMode") - if altitude_mode is not None: - am = altitude_mode.text.strip() - if am in [ - "clampToGround", - # 'relativeToSeaFloor', 'clampToSeaFloor', - "relativeToGround", - "absolute", - ]: - self.altitude_mode = am - else: - self.altitude_mode = None - else: - self.altitude_mode = None # type: ignore[unreachable] +def create_multigeometry( + geometries: Sequence[AnyGeometryType], +) -> Optional[MultiGeometryType]: + """Create a MultiGeometry from a sequence of geometries. - def _get_coordinates(self, element: Element) -> List[PointType]: - coordinates = element.find(f"{self.ns}coordinates") - if coordinates is not None: - # https://developers.google.com/kml/documentation/kmlreference#coordinates - # Coordinates can be any number of tuples separated by a - # space (potentially any number of whitespace characters). - # Values in tuples should be separated by commas with no - # spaces. Clean up badly formatted tuples by stripping - # space following commas. - latlons = re.sub(r", +", ",", coordinates.text.strip()).split() - return [ - cast(PointType, tuple(float(c) for c in latlon.split(","))) - for latlon in latlons - ] + Args: + geometries: Sequence of geometries. - def _get_linear_ring(self, element: Element) -> Optional[LinearRing]: - # LinearRing in polygon - lr = element.find(f"{self.ns}LinearRing") - if lr is not None: - coords = self._get_coordinates(lr) - return LinearRing(coords) - return None # type: ignore[unreachable] - - def _get_geometry(self, element: Element) -> Optional[GeometryType]: - # Point, LineString, - # Polygon, LinearRing - if element.tag == f"{self.ns}Point": - coords = self._get_coordinates(element) - self._get_geometry_spec(element) - return Point.from_coordinates(coords) - if element.tag == f"{self.ns}LineString": - coords = self._get_coordinates(element) - self._get_geometry_spec(element) - return LineString(coords) - if element.tag == f"{self.ns}Polygon": - self._get_geometry_spec(element) - outer_boundary = element.find(f"{self.ns}outerBoundaryIs") - ob = self._get_linear_ring(outer_boundary) - if not ob: - return None - inner_boundaries = element.findall(f"{self.ns}innerBoundaryIs") - ibs = [ - self._get_linear_ring(inner_boundary) - for inner_boundary in inner_boundaries - ] - return Polygon.from_linear_rings(ob, *[b for b in ibs if b]) - if element.tag == f"{self.ns}LinearRing": - coords = self._get_coordinates(element) - self._get_geometry_spec(element) - return LinearRing(coords) - return None + Returns: + MultiGeometry - def _get_multigeometry(self, element: Element) -> Optional[MultiGeometryType]: - # MultiGeometry - geoms: List[Union[AnyGeometryType, None]] = [] - if element.tag == f"{self.ns}MultiGeometry": - points = element.findall(f"{self.ns}Point") - for point in points: - self._get_geometry_spec(point) - geoms.append(Point.from_coordinates(self._get_coordinates(point))) - linestrings = element.findall(f"{self.ns}LineString") - for ls in linestrings: - self._get_geometry_spec(ls) - geoms.append(LineString(self._get_coordinates(ls))) - polygons = element.findall(f"{self.ns}Polygon") - for polygon in polygons: - self._get_geometry_spec(polygon) - outer_boundary = polygon.find(f"{self.ns}outerBoundaryIs") - ob = self._get_linear_ring(outer_boundary) - if not ob: - continue - inner_boundaries = polygon.findall(f"{self.ns}innerBoundaryIs") - inner_bs = [ - self._get_linear_ring(inner_boundary) - for inner_boundary in inner_boundaries - ] - ibs: List[LinearRing] = [ib for ib in inner_bs if ib] - geoms.append(Polygon.from_linear_rings(ob, *ibs)) - linearings = element.findall(f"{self.ns}LinearRing") - if linearings: - for lr in linearings: - self._get_geometry_spec(lr) - geoms.append(LinearRing(self._get_coordinates(lr))) - clean_geoms: List[AnyGeometryType] = [g for g in geoms if g] - if clean_geoms: - geom_types = {geom.geom_type for geom in clean_geoms} - if len(geom_types) > 1: - return GeometryCollection( - clean_geoms, # type: ignore[arg-type] - ) - if "Point" in geom_types: - return MultiPoint.from_points( - *clean_geoms, # type: ignore[arg-type] - ) - elif "LineString" in geom_types: - return MultiLineString.from_linestrings( - *clean_geoms, # type: ignore[arg-type] - ) - elif "Polygon" in geom_types: - return MultiPolygon.from_polygons( - *clean_geoms, # type: ignore[arg-type] - ) - elif "LinearRing" in geom_types: - return GeometryCollection( - clean_geoms, # type: ignore[arg-type] - ) + """ + geom_types = {geom.geom_type for geom in geometries} + if not geom_types: return None + if len(geom_types) == 1: + geom_type = geom_types.pop() + map_to_geometries = { + geo.Point.__name__: geo.MultiPoint.from_points, + geo.LineString.__name__: geo.MultiLineString.from_linestrings, + geo.Polygon.__name__: geo.MultiPolygon.from_polygons, + } + for geometry_name, constructor in map_to_geometries.items(): + if geom_type == geometry_name: + return constructor( # type: ignore[operator, no-any-return] + *geometries, + ) + + return geo.GeometryCollection(geometries) - def from_element(self, element: Element) -> None: - geom = self._get_geometry(element) - if geom is not None: - self.geometry = geom - else: - mgeom = self._get_multigeometry(element) - if mgeom is not None: - self.geometry = mgeom - else: - logger.warning("No geometries found") +class MultiGeometry(_Geometry): + map_to_kml = { + geo.Point: Point, + geo.LineString: LineString, + geo.Polygon: Polygon, + geo.LinearRing: LinearRing, + } + multi_geometries = ( + geo.MultiPoint, + geo.MultiLineString, + geo.MultiPolygon, + geo.GeometryCollection, + ) + + def __init__( + self, + *, + ns: Optional[str] = None, + id: Optional[str] = None, + target_id: Optional[str] = None, + extrude: Optional[bool] = False, + tessellate: Optional[bool] = False, + altitude_mode: Optional[AltitudeMode] = None, + geometry: MultiGeometryType, + ) -> None: + super().__init__( + ns=ns, + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + def etree_element( + self, + precision: Optional[int] = None, + verbosity: Verbosity = Verbosity.normal, + ) -> Element: + self.__name__ = self.__class__.__name__ + element = super().etree_element(precision=precision, verbosity=verbosity) + _map_to_kml = {mg: self.__class__ for mg in self.multi_geometries} + _map_to_kml.update(self.map_to_kml) + if self.geometry is None: + return element + assert isinstance(self.geometry, self.multi_geometries) + for geometry in self.geometry.geoms: + geometry_class = _map_to_kml[type(geometry)] + element.append( + geometry_class( + ns=self.ns, + extrude=None, + tessellate=None, + altitude_mode=None, + geometry=geometry, # type: ignore[arg-type] + ).etree_element(precision=precision, verbosity=verbosity) + ) + return element -__all__ = ["Geometry"] + @classmethod + def _get_geometry( + cls, *, ns: str, element: Element, strict: bool + ) -> Optional[MultiGeometryType]: + geometries = [] + allowed_geometries = (cls,) + tuple(cls.map_to_kml.values()) + for g in allowed_geometries: + for e in element.findall(f"{ns}{g.__name__}"): + geometry = g._get_geometry(ns=ns, element=e, strict=strict) + if geometry is not None: + geometries.append(geometry) + return create_multigeometry(geometries) diff --git a/fastkml/gx.py b/fastkml/gx.py index 86e42c6c..892850e9 100644 --- a/fastkml/gx.py +++ b/fastkml/gx.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012 - 2022 Christian Ledermann +# Copyright (C) 2012 - 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 @@ -75,79 +75,346 @@ The complete XML schema for elements in this extension namespace is located at http://developers.google.com/kml/schema/kml22gx.xsd. """ - +import contextlib +import datetime import logging +from dataclasses import dataclass +from itertools import zip_longest +from typing import Any +from typing import Dict +from typing import Iterator from typing import List from typing import Optional -from typing import Union +from typing import Sequence from typing import cast -from pygeoif.geometry import GeometryCollection -from pygeoif.geometry import LineString -from pygeoif.geometry import MultiLineString -from pygeoif.types import PointType +import dateutil.parser +import pygeoif.geometry as geo -from fastkml.config import GXNS as NS -from fastkml.geometry import Geometry +import fastkml.config as config +from fastkml.enums import AltitudeMode +from fastkml.enums import Verbosity +from fastkml.geometry import _Geometry from fastkml.types import Element logger = logging.getLogger(__name__) -class GxGeometry(Geometry): +@dataclass(frozen=True) +class Angle: + """ + The gx:angles element specifies the heading, tilt, and roll. + + The angles are specified in degrees, and the + default values are 0 (heading and tilt) and 0 (roll). The angles + are specified in the following order: heading, tilt, roll. + """ + + heading: float = 0.0 + tilt: float = 0.0 + roll: float = 0.0 + + +@dataclass(frozen=True) +class TrackItem: + """A track item describes an objects position and heading at a specific time.""" + + when: Optional[datetime.datetime] = None + coord: Optional[geo.Point] = None + angle: Optional[Angle] = None + + def etree_elements( + self, + *, + precision: Optional[int] = None, + verbosity: Verbosity = Verbosity.normal, + name_spaces: Optional[Dict[str, str]] = None, + ) -> Iterator[Element]: + name_spaces = name_spaces or {} + name_spaces = {**config.NAME_SPACES, **name_spaces} + element: Element = config.etree.Element( # type: ignore[attr-defined] + f"{name_spaces.get('kml', '')}when" + ) + if self.when: + element.text = self.when.isoformat() + yield element + element = config.etree.Element( # type: ignore[attr-defined] + f"{name_spaces.get('gx', '')}coord" + ) + if self.coord: + element.text = " ".join([str(c) for c in self.coord.coords[0]]) + yield element + element = config.etree.Element( # type: ignore[attr-defined] + f"{name_spaces.get('gx', '')}angles" + ) + if self.angle: + element.text = " ".join( + [str(self.angle.heading), str(self.angle.tilt), str(self.angle.roll)] + ) + yield element + + +def track_items_to_geometry(track_items: Sequence[TrackItem]) -> geo.LineString: + return geo.LineString.from_points( + *[item.coord for item in track_items if item.coord is not None] + ) + + +def linestring_to_track_items(linestring: geo.LineString) -> List[TrackItem]: + return [TrackItem(coord=point) for point in linestring.geoms] + + +class Track(_Geometry): + """ + A track describes how an object moves through the world over a given time period. + + This feature allows you to create one visible object in Google Earth + (either a Point icon or a Model) that encodes multiple positions for the same object + for multiple times. In Google Earth, the time slider allows the user to move the + view through time, which animates the position of the object. + + Tracks are a more efficient mechanism for associating time data with visible + Features, since you create only one Feature, which can be associated with multiple + time elements as the object moves through space. + """ + def __init__( self, - ns: None = None, - id: None = None, + *, + ns: Optional[str] = None, + id: Optional[str] = None, + target_id: Optional[str] = None, + extrude: Optional[bool] = False, + tessellate: Optional[bool] = False, + altitude_mode: Optional[AltitudeMode] = None, + geometry: Optional[geo.LineString] = None, + track_items: Optional[Sequence[TrackItem]] = None, ) -> None: - """ - gxgeometry: a read-only subclass of geometry supporting gx: features, - like gx:Track - """ - super().__init__(ns, id) - self.ns = NS if ns is None else ns - - def _get_geometry(self, element: Element) -> Optional[LineString]: - # Track - if element.tag == (f"{self.ns}Track"): - coords = self._get_coordinates(element) - self._get_geometry_spec(element) - return LineString( - coords, - ) - return None + if geometry and track_items: + raise ValueError("Cannot specify both geometry and track_items") + if geometry: + track_items = linestring_to_track_items(geometry) + elif track_items: + geometry = track_items_to_geometry(track_items) + self.track_items = track_items + super().__init__( + ns=ns, + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) - def _get_multigeometry( + def __repr__(self) -> str: + return ( + f"{self.__class__.__name__}(" + f"ns={self.ns!r}, " + f"id={self.id!r}, " + f"target_id={self.target_id!r}, " + f"extrude={self.extrude!r}, " + f"tessellate={self.tessellate!r}, " + f"altitude_mode={self.altitude_mode}, " + f"track_items={self.track_items!r}" + ")" + ) + + def etree_element( self, + precision: Optional[int] = None, + verbosity: Verbosity = Verbosity.normal, + name_spaces: Optional[Dict[str, str]] = None, + ) -> Element: + self.__name__ = self.__class__.__name__ + element = super().etree_element(precision=precision, verbosity=verbosity) + if self.track_items: + for track_item in self.track_items: + for track_item_element in track_item.etree_elements( + precision=precision, verbosity=verbosity, name_spaces=name_spaces + ): + element.append(track_item_element) + return element + + @classmethod + def track_items_kwargs_from_element( + cls, + *, + ns: str, element: Element, - ) -> Union[MultiLineString, GeometryCollection, None]: - # MultiTrack - geoms = [] - if element.tag == (f"{self.ns}MultiTrack"): - tracks = element.findall(f"{self.ns}Track") - for track in tracks: - self._get_geometry_spec(track) - geoms.append( - LineString( - self._get_coordinates(track), - ) + strict: bool, + ) -> List[TrackItem]: + time_stamps: List[Optional[datetime.datetime]] = [] + for time_stamp in element.findall(f"{config.KMLNS}when"): + if time_stamp is not None and time_stamp.text: + time_stamps.append(dateutil.parser.parse(time_stamp.text)) + else: + time_stamps.append(None) + coords: List[Optional[geo.Point]] = [] + for coord in element.findall(f"{config.GXNS}coord"): + if coord is not None and coord.text: + coords.append( + geo.Point(*[float(c) for c in coord.text.strip().split()]) ) + else: + coords.append(None) + angles: List[Optional[Angle]] = [] + for angle in element.findall(f"{config.GXNS}angles"): + if angle is not None and angle.text: + angles.append(Angle(*[float(a) for a in angle.text.strip().split()])) + else: + angles.append(None) + return [ + TrackItem(when=when, coord=coord, angle=angle) + for when, coord, angle in zip_longest(time_stamps, coords, angles) + ] - geom_types = {geom.geom_type for geom in geoms} - if len(geom_types) > 1: - return GeometryCollection(geoms) - if "LineString" in geom_types: - return MultiLineString.from_linestrings(*geoms) - return None + @classmethod + def _get_kwargs( + cls, + *, + ns: str, + element: Element, + strict: bool, + ) -> Dict[str, Any]: + kwargs = super()._get_kwargs(ns=ns, element=element, strict=strict) + kwargs["track_items"] = cls.track_items_kwargs_from_element( + ns=ns, element=element, strict=strict + ) + return kwargs + + +def multilinestring_to_tracks( + multilinestring: geo.MultiLineString, ns: Optional[str] +) -> List[Track]: + return [Track(ns=ns, geometry=linestring) for linestring in multilinestring.geoms] + + +def tracks_to_geometry(tracks: Sequence[Track]) -> geo.MultiLineString: + return geo.MultiLineString.from_linestrings( + *[cast(geo.LineString, track.geometry) for track in tracks if track.geometry] + ) + + +class MultiTrack(_Geometry): + def __init__( + self, + *, + ns: Optional[str] = None, + id: Optional[str] = None, + target_id: Optional[str] = None, + extrude: Optional[bool] = False, + tessellate: Optional[bool] = False, + altitude_mode: Optional[AltitudeMode] = None, + geometry: Optional[geo.MultiLineString] = None, + tracks: Optional[Sequence[Track]] = None, + interpolate: Optional[bool] = None, + ) -> None: + if geometry and tracks: + raise ValueError("Cannot specify both geometry and track_items") + if geometry: + tracks = multilinestring_to_tracks(geometry, ns=ns) + elif tracks: + geometry = tracks_to_geometry(tracks) + self.tracks = tracks + self.interpolate = interpolate + super().__init__( + ns=ns, + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) - def _get_coordinates(self, element: Element) -> List[PointType]: - coordinates = element.findall(f"{self.ns}coord") - if coordinates is not None: - return [ - cast(PointType, tuple(float(c) for c in coord.text.strip().split())) - for coord in coordinates - ] - return [] # type: ignore[unreachable] + def __repr__(self) -> str: + return ( + f"{self.__class__.__name__}(" + f"ns={self.ns!r}, " + f"id={self.id!r}, " + f"target_id={self.target_id!r}, " + f"extrude={self.extrude!r}, " + f"tessellate={self.tessellate!r}, " + f"altitude_mode={self.altitude_mode}, " + f"tracks={self.tracks!r}, " + f"interpolate={self.interpolate!r}" + ")" + ) + def etree_element( + self, + precision: Optional[int] = None, + verbosity: Verbosity = Verbosity.normal, + name_spaces: Optional[Dict[str, str]] = None, + ) -> Element: + self.__name__ = self.__class__.__name__ + element = super().etree_element(precision=precision, verbosity=verbosity) + if self.interpolate is not None: + i_element = cast( + Element, + config.etree.SubElement( # type: ignore[attr-defined] + element, f"{self.ns}interpolate" + ), + ) + i_element.text = str(int(self.interpolate)) + for track in self.tracks or []: + element.append( + track.etree_element( + precision=precision, verbosity=verbosity, name_spaces=name_spaces + ) + ) + return element -__all__ = ["GxGeometry"] + @classmethod + def _get_interpolate( + cls, + *, + ns: str, + element: Element, + strict: bool, + ) -> Optional[bool]: + interpolate = element.find(f"{ns}interpolate") + if interpolate is None: + return None + with contextlib.suppress(ValueError): + return bool(int(interpolate.text.strip())) + return None + + @classmethod + def _get_track_kwargs_from_element( + cls, + *, + ns: str, + element: Element, + strict: bool, + ) -> List[Track]: + return [ + cast( + Track, + Track.class_from_element( + ns=ns, + element=track, + strict=strict, + ), + ) + for track in element.findall(f"{ns}Track") + if track is not None + ] + + @classmethod + def _get_kwargs( + cls, + *, + ns: str, + element: Element, + strict: bool, + ) -> Dict[str, Any]: + kwargs = super()._get_kwargs(ns=ns, element=element, strict=strict) + kwargs["interpolate"] = cls._get_interpolate( + ns=ns, element=element, strict=strict + ) + kwargs["tracks"] = cls._get_track_kwargs_from_element( + ns=config.GXNS, element=element, strict=strict + ) + return kwargs diff --git a/fastkml/helpers.py b/fastkml/helpers.py index 24f6c2a4..781ea623 100644 --- a/fastkml/helpers.py +++ b/fastkml/helpers.py @@ -109,7 +109,7 @@ def o_from_subelement_text( ) else: setattr(obj, obj_attr, elem.text) - elif required: # type: ignore[unreachable] + elif required: logger.warning( "Required attribute '%s' for '%s' missing.", kml_attr, diff --git a/fastkml/kml.py b/fastkml/kml.py index 2b18d466..848f1614 100644 --- a/fastkml/kml.py +++ b/fastkml/kml.py @@ -37,7 +37,13 @@ from fastkml.base import _BaseObject from fastkml.data import ExtendedData from fastkml.data import Schema -from fastkml.geometry import Geometry +from fastkml.enums import Verbosity +from fastkml.geometry import AnyGeometryType +from fastkml.geometry import LinearRing +from fastkml.geometry import LineString +from fastkml.geometry import MultiGeometry +from fastkml.geometry import Point +from fastkml.geometry import Polygon from fastkml.mixins import TimeMixin from fastkml.styles import Style from fastkml.styles import StyleMap @@ -51,6 +57,16 @@ logger = logging.getLogger(__name__) +KmlGeometry = Union[ + Point, + LineString, + LinearRing, + Polygon, + MultiGeometry, + gx.MultiTrack, + gx.Track, +] + class _Feature(TimeMixin, _BaseObject): """ @@ -336,8 +352,12 @@ def phone_number(self, phone_number): else: raise ValueError - def etree_element(self) -> Element: - element = super().etree_element() + def etree_element( + self, + precision: Optional[int] = None, + verbosity: Verbosity = Verbosity.normal, + ) -> Element: + element = super().etree_element(precision=precision, verbosity=verbosity) if self.name: name = config.etree.SubElement(element, f"{self.ns}name") name.text = self.name @@ -448,10 +468,6 @@ def from_element(self, element: Element) -> None: x = ExtendedData(self.ns) x.from_element(extended_data) self.extended_data = x - # else: - # logger.warn( - # 'arbitrary or typed extended data is not yet supported' - # ) address = element.find(f"{self.ns}address") if address is not None: self.address = address.text @@ -689,8 +705,12 @@ def http_query(self, http_query): else: raise ValueError - def etree_element(self) -> Element: - element = super().etree_element() + def etree_element( + self, + precision: Optional[int] = None, + verbosity: Verbosity = Verbosity.normal, + ) -> Element: + element = super().etree_element(precision=precision, verbosity=verbosity) if self._href: href = config.etree.SubElement(element, f"{self.ns}href") @@ -817,14 +837,20 @@ def features(self) -> Iterator[_Feature]: "(Folder, Placemark, Document, Overlay)" ) - def etree_element(self) -> Element: - element = super().etree_element() + def etree_element( + self, + precision: Optional[int] = None, + verbosity: Verbosity = Verbosity.normal, + ) -> Element: + element = super().etree_element(precision=precision, verbosity=verbosity) for feature in self.features(): element.append(feature.etree_element()) return element def append(self, kmlobj: _Feature) -> None: """append a feature""" + if id(kmlobj) == id(self): + raise ValueError("Cannot append self") if isinstance(kmlobj, (Folder, Placemark, Document, _Overlay)): self._features.append(kmlobj) else: @@ -832,7 +858,6 @@ def append(self, kmlobj: _Feature) -> None: "Features must be instances of " "(Folder, Placemark, Document, Overlay)" ) - assert kmlobj != self class _Overlay(_Feature): @@ -924,8 +949,12 @@ def icon(self, value): else: raise ValueError - def etree_element(self) -> Element: - element = super().etree_element() + def etree_element( + self, + precision: Optional[int] = None, + verbosity: Verbosity = Verbosity.normal, + ) -> Element: + element = super().etree_element(precision=precision, verbosity=verbosity) if self._color: color = config.etree.SubElement(element, f"{self.ns}color") color.text = self._color @@ -1208,8 +1237,12 @@ def image_pyramid(self, tile_size, max_width, max_height, grid_origin): self.max_height = max_height self.grid_origin = grid_origin - def etree_element(self): - element = super().etree_element() + def etree_element( + self, + precision: Optional[int] = None, + verbosity: Verbosity = Verbosity.normal, + ): + element = super().etree_element(precision=precision, verbosity=verbosity) if self._rotation: rotation = config.etree.SubElement(element, f"{self.ns}rotation") rotation.text = self._rotation @@ -1464,8 +1497,12 @@ def lat_lon_box( else: raise ValueError - def etree_element(self) -> Element: - element = super().etree_element() + def etree_element( + self, + precision: Optional[int] = None, + verbosity: Verbosity = Verbosity.normal, + ) -> Element: + element = super().etree_element(precision=precision, verbosity=verbosity) if self._altitude: altitude = config.etree.SubElement(element, f"{self.ns}altitude") altitude.text = self._altitude @@ -1562,8 +1599,12 @@ def from_element(self, element: Element) -> None: s.from_element(schema) self.append_schema(s) - def etree_element(self) -> Element: - element = super().etree_element() + def etree_element( + self, + precision: Optional[int] = None, + verbosity: Verbosity = Verbosity.normal, + ) -> Element: + element = super().etree_element(precision=precision, verbosity=verbosity) if self._schemata is not None: for schema in self._schemata: element.append(schema.etree_element()) @@ -1614,67 +1655,104 @@ class Placemark(_Feature): __name__ = "Placemark" _geometry = None - @property - def geometry(self): - return self._geometry.geometry + def __init__( + self, + ns: Optional[str] = None, + id: Optional[str] = None, + target_id: Optional[str] = None, + name: Optional[str] = None, + description: Optional[str] = None, + styles: Optional[List[Style]] = None, + style_url: Optional[str] = None, + extended_data: None = None, + geometry: Optional[KmlGeometry] = None, + ) -> None: + super().__init__( + ns=ns, + id=id, + target_id=target_id, + name=name, + description=description, + styles=styles, + style_url=style_url, + extended_data=extended_data, + ) + self._geometry = geometry - @geometry.setter - def geometry(self, geometry): - if isinstance(geometry, Geometry): - self._geometry = geometry - else: - self._geometry = Geometry(ns=self.ns, geometry=geometry) + @property + def geometry(self) -> Optional[AnyGeometryType]: + if self._geometry is not None: + return self._geometry.geometry + return None - def from_element(self, element: Element) -> None: + def from_element(self, element: Element, strict=False) -> None: super().from_element(element) point = element.find(f"{self.ns}Point") if point is not None: - geom = Geometry(ns=self.ns) - geom.from_element(point) - self._geometry = geom + self._geometry = Point.class_from_element( + ns=self.ns, + element=point, + strict=strict, + ) return line = element.find(f"{self.ns}LineString") if line is not None: - geom = Geometry(ns=self.ns) - geom.from_element(line) - self._geometry = geom + self._geometry = LineString.class_from_element( + ns=self.ns, + element=line, + strict=strict, + ) return polygon = element.find(f"{self.ns}Polygon") if polygon is not None: - geom = Geometry(ns=self.ns) - geom.from_element(polygon) - self._geometry = geom + self._geometry = Polygon.class_from_element( + ns=self.ns, + element=polygon, + strict=strict, + ) return linearring = element.find(f"{self.ns}LinearRing") if linearring is not None: - geom = Geometry(ns=self.ns) - geom.from_element(linearring) - self._geometry = geom + self._geometry = LinearRing.class_from_element( + ns=self.ns, + element=linearring, + strict=strict, + ) return multigeometry = element.find(f"{self.ns}MultiGeometry") if multigeometry is not None: - geom = Geometry(ns=self.ns) - geom.from_element(multigeometry) - self._geometry = geom + self._geometry = MultiGeometry.class_from_element( + ns=self.ns, + element=multigeometry, + strict=strict, + ) return track = element.find(f"{self.ns}Track") if track is not None: - geom = gx.GxGeometry(ns=gx.NS) - geom.from_element(track) - self._geometry = geom + self._geometry = gx.Track.class_from_element( + ns=config.GXNS, + element=track, + strict=strict, + ) return multitrack = element.find(f"{self.ns}MultiTrack") - if line is not None: - geom = gx.GxGeometry(ns=gx.NS) - geom.from_element(multitrack) - self._geometry = geom + if multitrack is not None: + self._geometry = gx.MultiTrack.class_from_element( + ns=config.GXNS, + element=multitrack, + strict=strict, + ) return logger.warning("No geometries found") logger.debug("Problem with element: %", config.etree.tostring(element)) # raise ValueError('No geometries found') - def etree_element(self) -> Element: - element = super().etree_element() + def etree_element( + self, + precision: Optional[int] = None, + verbosity: Verbosity = Verbosity.normal, + ) -> Element: + element = super().etree_element(precision=precision, verbosity=verbosity) if self._geometry is not None: element.append(self._geometry.etree_element()) else: @@ -1737,7 +1815,11 @@ def from_string(self, xml_string: str) -> None: feature.from_element(photo_overlay) self.append(feature) - def etree_element(self) -> Element: + def etree_element( + self, + precision: Optional[int] = None, + verbosity: Verbosity = Verbosity.normal, + ) -> Element: # self.ns may be empty, which leads to unprefixed kml elements. # However, in this case the xlmns should still be mentioned on the kml # element, just without prefix. @@ -1772,7 +1854,6 @@ def features(self) -> Iterator[Union[Folder, Document, Placemark]]: """iterate over features""" for feature in self._features: if isinstance(feature, (Document, Folder, Placemark, _Overlay)): - yield feature else: raise TypeError( @@ -1782,7 +1863,8 @@ def features(self) -> Iterator[Union[Folder, Document, Placemark]]: def append(self, kmlobj: Union[Folder, Document, Placemark]) -> None: """append a feature""" - + if id(kmlobj) == id(self): + raise ValueError("Cannot append self") if isinstance(kmlobj, (Document, Folder, Placemark, _Overlay)): self._features.append(kmlobj) else: diff --git a/fastkml/mixins.py b/fastkml/mixins.py index 2ddc0d77..60b56a12 100644 --- a/fastkml/mixins.py +++ b/fastkml/mixins.py @@ -25,7 +25,6 @@ class TimeMixin: - _timespan: Optional[TimeSpan] = None _timestamp: Optional[TimeStamp] = None diff --git a/fastkml/styles.py b/fastkml/styles.py index 5df7a08c..48240c65 100644 --- a/fastkml/styles.py +++ b/fastkml/styles.py @@ -32,6 +32,7 @@ from fastkml import config from fastkml.base import _BaseObject +from fastkml.enums import Verbosity from fastkml.types import Element logger = logging.getLogger(__name__) @@ -57,8 +58,12 @@ def __init__( super().__init__(ns=ns, id=id, target_id=target_id) self.url = url - def etree_element(self) -> Element: - element = super().etree_element() + def etree_element( + self, + precision: Optional[int] = None, + verbosity: Verbosity = Verbosity.normal, + ) -> Element: + element = super().etree_element(precision=precision, verbosity=verbosity) if self.url: element.text = self.url else: @@ -113,8 +118,12 @@ def __init__( self.color = color self.color_mode = color_mode - def etree_element(self) -> Element: - element = super().etree_element() + def etree_element( + self, + precision: Optional[int] = None, + verbosity: Verbosity = Verbosity.normal, + ) -> Element: + element = super().etree_element(precision=precision, verbosity=verbosity) if self.color: color = config.etree.SubElement( # type: ignore[attr-defined] element, @@ -130,7 +139,6 @@ def etree_element(self) -> Element: return element def from_element(self, element: Element) -> None: - super().from_element(element) color_mode = element.find(f"{self.ns}colorMode") if color_mode is not None: @@ -181,8 +189,12 @@ def __init__( self.icon_href = icon_href self.hot_spot = hot_spot - def etree_element(self) -> Element: - element = super().etree_element() + def etree_element( + self, + precision: Optional[int] = None, + verbosity: Verbosity = Verbosity.normal, + ) -> Element: + element = super().etree_element(precision=precision, verbosity=verbosity) if self.scale is not None: scale = config.etree.SubElement( # type: ignore[attr-defined] element, @@ -264,8 +276,12 @@ def __init__( ) self.width = width - def etree_element(self) -> Element: - element = super().etree_element() + def etree_element( + self, + precision: Optional[int] = None, + verbosity: Verbosity = Verbosity.normal, + ) -> Element: + element = super().etree_element(precision=precision, verbosity=verbosity) if self.width is not None: width = config.etree.SubElement( # type: ignore[attr-defined] element, @@ -311,8 +327,12 @@ def __init__( self.fill = fill self.outline = outline - def etree_element(self) -> Element: - element = super().etree_element() + def etree_element( + self, + precision: Optional[int] = None, + verbosity: Verbosity = Verbosity.normal, + ) -> Element: + element = super().etree_element(precision=precision, verbosity=verbosity) if self.fill is not None: fill = config.etree.SubElement( # type: ignore[attr-defined] element, @@ -368,8 +388,12 @@ def __init__( ) self.scale = scale - def etree_element(self) -> Element: - element = super().etree_element() + def etree_element( + self, + precision: Optional[int] = None, + verbosity: Verbosity = Verbosity.normal, + ) -> Element: + element = super().etree_element(precision=precision, verbosity=verbosity) if self.scale is not None: scale = config.etree.SubElement( # type: ignore[attr-defined] element, @@ -458,7 +482,7 @@ def from_element(self, element: Element) -> None: if bg_color is not None: self.bg_color = bg_color.text else: - bg_color = element.find(f"{self.ns}color") # type: ignore[unreachable] + bg_color = element.find(f"{self.ns}color") if bg_color is not None: self.bg_color = bg_color.text text_color = element.find(f"{self.ns}textColor") @@ -471,8 +495,12 @@ def from_element(self, element: Element) -> None: if display_mode is not None: self.display_mode = display_mode.text - def etree_element(self) -> Element: - element = super().etree_element() + def etree_element( + self, + precision: Optional[int] = None, + verbosity: Verbosity = Verbosity.normal, + ) -> Element: + element = super().etree_element(precision=precision, verbosity=verbosity) if self.bg_color is not None: elem = config.etree.SubElement( # type: ignore[attr-defined] element, @@ -558,8 +586,12 @@ def from_element(self, element: Element) -> None: for style in [BalloonStyle, IconStyle, LabelStyle, LineStyle, PolyStyle]: self._get_style(element, style) - def etree_element(self) -> Element: - element = super().etree_element() + def etree_element( + self, + precision: Optional[int] = None, + verbosity: Verbosity = Verbosity.normal, + ) -> Element: + element = super().etree_element(precision=precision, verbosity=verbosity) for style in self.styles(): element.append(style.etree_element()) return element @@ -596,12 +628,14 @@ def from_element(self, element: Element) -> None: key = pair.find(f"{self.ns}key") style = pair.find(f"{self.ns}Style") style_url = pair.find(f"{self.ns}styleUrl") - if key.text == "highlight": + if key is None: + raise ValueError + elif key.text == "highlight": if style is not None: highlight = Style(self.ns) highlight.from_element(style) - elif style_url is not None: # type: ignore[unreachable] - highlight = StyleUrl(self.ns) + elif style_url is not None: + highlight = StyleUrl(self.ns) # type: ignore[assignment] highlight.from_element(style_url) else: raise ValueError @@ -610,8 +644,8 @@ def from_element(self, element: Element) -> None: if style is not None: normal = Style(self.ns) normal.from_element(style) - elif style_url is not None: # type: ignore[unreachable] - normal = StyleUrl(self.ns) + elif style_url is not None: + normal = StyleUrl(self.ns) # type: ignore[assignment] normal.from_element(style_url) else: raise ValueError @@ -619,8 +653,12 @@ def from_element(self, element: Element) -> None: else: raise ValueError - def etree_element(self) -> Element: - element = super().etree_element() + def etree_element( + self, + precision: Optional[int] = None, + verbosity: Verbosity = Verbosity.normal, + ) -> Element: + element = super().etree_element(precision=precision, verbosity=verbosity) if self.normal and isinstance(self.normal, (Style, StyleUrl)): pair = config.etree.SubElement( # type: ignore[attr-defined] element, diff --git a/fastkml/times.py b/fastkml/times.py index 574a547d..ccca30f1 100644 --- a/fastkml/times.py +++ b/fastkml/times.py @@ -29,6 +29,7 @@ import fastkml.config as config from fastkml.base import _BaseObject from fastkml.enums import DateTimeResolution +from fastkml.enums import Verbosity from fastkml.types import Element # regular expression to parse a gYearMonth string @@ -167,8 +168,12 @@ def __init__( super().__init__(ns=ns, id=id, target_id=target_id) self.timestamp = timestamp - def etree_element(self) -> Element: - element = super().etree_element() + def etree_element( + self, + precision: Optional[int] = None, + verbosity: Verbosity = Verbosity.normal, + ) -> Element: + element = super().etree_element(precision=precision, verbosity=verbosity) when = config.etree.SubElement( # type: ignore[attr-defined] element, f"{self.ns}when" ) @@ -208,8 +213,12 @@ def from_element(self, element: Element) -> None: if end is not None: self.end = KmlDateTime.parse(end.text) - def etree_element(self) -> Element: - element = super().etree_element() + def etree_element( + self, + precision: Optional[int] = None, + verbosity: Verbosity = Verbosity.normal, + ) -> Element: + element = super().etree_element(precision=precision, verbosity=verbosity) if self.begin is not None: text = str(self.begin) if text: diff --git a/fastkml/types.py b/fastkml/types.py index ec614eb0..f5cc7b6d 100644 --- a/fastkml/types.py +++ b/fastkml/types.py @@ -37,7 +37,7 @@ def set(self, tag: str, value: str) -> None: def get(self, tag: str) -> str: """Get the value of the tag.""" - def find(self, tag: str) -> "Element": + def find(self, tag: str) -> Optional["Element"]: """Find the first element with the given tag.""" def findall(self, tag: str) -> Iterable["Element"]: diff --git a/fastkml/views.py b/fastkml/views.py index 5b7d9e77..1fffea36 100644 --- a/fastkml/views.py +++ b/fastkml/views.py @@ -1,10 +1,12 @@ import logging from typing import Optional +from typing import SupportsFloat from typing import Union import fastkml.config as config import fastkml.gx as gx from fastkml.base import _BaseObject +from fastkml.enums import Verbosity from fastkml.mixins import TimeMixin from fastkml.times import TimeSpan from fastkml.times import TimeStamp @@ -89,7 +91,7 @@ def longitude(self) -> Optional[float]: return self._longitude @longitude.setter - def longitude(self, value) -> None: + def longitude(self, value: Optional[SupportsFloat]) -> None: if isinstance(value, (str, int, float)) and (-180 <= float(value) <= 180): self._longitude = float(value) elif value is None: @@ -102,7 +104,7 @@ def latitude(self) -> Optional[float]: return self._latitude @latitude.setter - def latitude(self, value) -> None: + def latitude(self, value: Optional[SupportsFloat]) -> None: if isinstance(value, (str, int, float)) and (-90 <= float(value) <= 90): self._latitude = float(value) elif value is None: @@ -115,7 +117,7 @@ def altitude(self) -> Optional[float]: return self._altitude @altitude.setter - def altitude(self, value) -> None: + def altitude(self, value: Optional[SupportsFloat]) -> None: if isinstance(value, (str, int, float)): self._altitude = float(value) elif value is None: @@ -128,7 +130,7 @@ def heading(self) -> Optional[float]: return self._heading @heading.setter - def heading(self, value) -> None: + def heading(self, value: Optional[SupportsFloat]) -> None: if isinstance(value, (str, int, float)) and (-180 <= float(value) <= 180): self._heading = float(value) elif value is None: @@ -141,7 +143,7 @@ def tilt(self) -> Optional[float]: return self._tilt @tilt.setter - def tilt(self, value) -> None: + def tilt(self, value: Optional[SupportsFloat]) -> None: if isinstance(value, (str, int, float)) and (0 <= float(value) <= 180): self._tilt = float(value) elif value is None: @@ -195,8 +197,12 @@ def from_element(self, element: Element): s.from_element(timestamp) self._timestamp = s - def etree_element(self) -> Element: - element = super().etree_element() + def etree_element( + self, + precision: Optional[int] = None, + verbosity: Verbosity = Verbosity.normal, + ) -> Element: + element = super().etree_element(precision=precision, verbosity=verbosity) if self.longitude: longitude = config.etree.SubElement(element, f"{self.ns}longitude") longitude.text = str(self.longitude) @@ -291,8 +297,12 @@ def from_element(self, element: Element) -> None: if roll is not None: self.roll = roll.text - def etree_element(self) -> Element: - element = super().etree_element() + def etree_element( + self, + precision: Optional[int] = None, + verbosity: Verbosity = Verbosity.normal, + ) -> Element: + element = super().etree_element(precision=precision, verbosity=verbosity) if self.roll: roll = config.etree.SubElement(element, f"{self.ns}roll") roll.text = str(self.roll) @@ -313,7 +323,6 @@ def roll(self, value) -> None: class LookAt(_AbstractView): - __name__ = "LookAt" _range: Optional[float] = None @@ -367,8 +376,12 @@ def from_element(self, element: Element) -> None: if range_var is not None: self.range = range_var.text - def etree_element(self) -> Element: - element = super().etree_element() + def etree_element( + self, + precision: Optional[int] = None, + verbosity: Verbosity = Verbosity.normal, + ) -> Element: + element = super().etree_element(precision=precision, verbosity=verbosity) if self.range: range_var = config.etree.SubElement(element, f"{self.ns}range") range_var.text = str(self._range) diff --git a/pyproject.toml b/pyproject.toml index 8ee0b917..a7216ab1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,46 +1,66 @@ +[tool.ruff.extend-per-file-ignores] +"setup.py" = [ + "E501", +] +"tests/oldunit_test.py" = [ + "E501", +] + [tool.isort] -line_length = 88 force_single_line = true +line_length = 88 [tool.flake8] max_line_length = 89 -[tool.check-manifest] -ignore = [".*", "mutmut_config.py", "test-requirements.txt", "tox.ini", "examples/*"] - -[tool.pyright] -include = ["fastkml"] -exclude = ["**/node_modules", - "**/__pycache__", - ".pytype", - ".pyre", -] - [tool.mypy] disallow_any_generics = true -disallow_untyped_calls = true -disallow_untyped_defs = true disallow_incomplete_defs = true +disallow_untyped_calls = true disallow_untyped_decorators = true +disallow_untyped_defs = true +enable_error_code = [ + "ignore-without-code", +] ignore_errors = false ignore_missing_imports = true implicit_reexport = false -strict_optional = true -strict_equality = true no_implicit_optional = true -warn_unused_ignores = true -warn_redundant_casts = true -warn_unused_configs = true -warn_unreachable = true +show_error_codes = true +strict_equality = true +strict_optional = true warn_no_return = true +warn_redundant_casts = true warn_return_any = true -show_error_codes = true - -# mypy per-module options: +warn_unreachable = true +warn_unused_configs = true +warn_unused_ignores = true [[tool.mypy.overrides]] +ignore_errors = true module = [ - "fastkml.kml", "fastkml.views", - "fastkml.tests.oldunit_test", "fastkml.tests.config_test" + "fastkml.kml", + "fastkml.tests.config_test", + "fastkml.tests.oldunit_test", + "fastkml.views", +] + +[tool.check-manifest] +ignore = [ + ".*", + "examples/*", + "mutmut_config.py", + "test-requirements.txt", + "tox.ini", +] + +[tool.pyright] +exclude = [ + "**/__pycache__", + "**/node_modules", + ".pyre", + ".pytype", +] +include = [ + "fastkml", ] -ignore_errors = true diff --git a/requirements.txt b/requirements.txt index ad28f0e3..c7b6a580 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ # Base package requirements -pygeoif>=1.0.0 +pygeoif>=1.1.0 python-dateutil +typing_extensions diff --git a/schema/ogckml22.xsd b/schema/ogckml22.xsd new file mode 100644 index 00000000..3496aef5 --- /dev/null +++ b/schema/ogckml22.xsd @@ -0,0 +1,1642 @@ + + + + + ogckml22.xsd 2008-01-23 + XML Schema Document for OGC KML version 2.2. Copyright (c) + 2008 Open Geospatial Consortium. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + not anyURI due to $[x] substitution in + PhotoOverlay + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Snippet deprecated in 2.2 + + + + + + + + + + + + + Metadata deprecated in 2.2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Metadata deprecated in 2.2 + + + + + + MetadataType deprecated in 2.2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + is the root element. + + ]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Url deprecated in 2.2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Url deprecated in 2.2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + color deprecated in 2.1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/setup.py b/setup.py index 5bfb0fe7..ca715166 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def run_tests(self) -> None: setup( name="fastkml", - version="1.0.alpha.4", + version="1.0.alpha.6", description="Fast KML processing in python", long_description=( open("README.rst").read() @@ -39,12 +39,13 @@ def run_tests(self) -> None: "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Intended Audience :: Developers", "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)", # "Development Status :: 5 - Production/Stable", "Development Status :: 3 - Alpha", "Operating System :: OS Independent", - ], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers + ], keywords="GIS KML Google Maps OpenLayers", author="Christian Ledermann", author_email="christian.ledermann@gmail.com", @@ -58,10 +59,10 @@ def run_tests(self) -> None: python_requires=">=3.7", install_requires=[ # -*- Extra requirements: -*- - "pygeoif>=1.0.0", + "pygeoif>=1.1.0", "python-dateutil", "setuptools", - "typing_extensions", + "typing-extensions", ], entry_points=""" # -*- Entry points: -*- diff --git a/tests/atom_test.py b/tests/atom_test.py index 223cdfee..09e98fdd 100644 --- a/tests/atom_test.py +++ b/tests/atom_test.py @@ -26,14 +26,14 @@ class TestStdLibrary(StdLibrary): def test_atom_link_ns(self) -> None: ns = "{http://www.opengis.net/kml/2.2}" # noqa: FS003 - l = atom.Link(ns=ns) - assert l.ns == ns - assert l.to_string().startswith( + link = atom.Link(ns=ns) + assert link.ns == ns + assert link.to_string().startswith( ' None: - l = atom.Link( + link = atom.Link( href="#here", rel="alternate", type="text/html", @@ -42,7 +42,7 @@ def test_atom_link(self) -> None: length=3456, ) - serialized = l.to_string() + serialized = link.to_string() assert ' None: assert 'length="3456"' in serialized def test_atom_link_read(self) -> None: - l = atom.Link() - l.from_string( + link = atom.Link() + link.from_string( '' ) - assert l.href == "#here" - assert l.rel == "alternate" - assert l.type == "text/html" - assert l.hreflang == "en" - assert l.title == "Title" - assert l.length == 3456 + assert link.href == "#here" + assert link.rel == "alternate" + assert link.type == "text/html" + assert link.hreflang == "en" + assert link.title == "Title" + assert link.length == 3456 def test_atom_link_read_no_href(self) -> None: - l = atom.Link() - l.from_string( + link = atom.Link() + link.from_string( '' ) - assert l.href is None + assert link.href is None def test_atom_person_ns(self) -> None: ns = "{http://www.opengis.net/kml/2.2}" # noqa: FS003 @@ -133,14 +133,14 @@ def test_author_roundtrip(self) -> None: assert a.to_string() == a2.to_string() def test_link_roundtrip(self) -> None: - l = atom.Link(href="http://localhost/", rel="alternate") - l.title = "Title" - l.type = "text/html" - l.hreflang = "en" - l.length = 4096 + link = atom.Link(href="http://localhost/", rel="alternate") + link.title = "Title" + link.type = "text/html" + link.hreflang = "en" + link.length = 4096 l2 = atom.Link() - l2.from_string(l.to_string()) - assert l.to_string() == l2.to_string() + l2.from_string(link.to_string()) + assert link.to_string() == l2.to_string() class TestLxml(Lxml, TestStdLibrary): diff --git a/tests/base_test.py b/tests/base_test.py index 5637dc8d..d6362198 100644 --- a/tests/base_test.py +++ b/tests/base_test.py @@ -1,4 +1,4 @@ -# Copyright (C) 2021 Christian Ledermann +# 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 @@ -33,17 +33,28 @@ def test_to_string(self) -> None: obj = base._BaseObject(id="id-0", target_id="target-id-0") obj.__name__ = "test" - assert ( - obj.to_string() - == '' + assert obj.to_string() == ( + '' ) + def test_to_str_empty_ns(self) -> None: + obj = base._BaseObject(ns="", id="id-0", target_id="target-id-0") + obj.__name__ = "test" + + assert obj.to_string().replace(" ", "").replace( + "\n", "" + ) == ''.replace(" ", "") + def test_from_string(self) -> None: be = base._BaseObject() be.__name__ = "test" be.from_string( - xml_string='' + xml_string=( + '' + ) ) assert be.id == "id-0" @@ -64,7 +75,10 @@ class Test(base._BaseObject): def test_base_from_element_raises(self) -> None: be = base._BaseObject() - element = cast(types.Element, config.etree.Element(config.KMLNS + "Base")) # type: ignore[attr-defined] + element = cast( + types.Element, + config.etree.Element(config.KMLNS + "Base"), # type: ignore[attr-defined] + ) with pytest.raises(TypeError): be.from_element(element=element) @@ -74,9 +88,23 @@ def test_base_from_string_raises(self) -> None: with pytest.raises(TypeError): be.from_string( - xml_string='' + '' ) + def test_base_class_from_string(self) -> None: + be = base._BaseObject.class_from_string('') + + assert be.id == "id-0" + assert be.target_id == "td-00" + assert be.ns == "{http://www.opengis.net/kml/2.2}" + + def test_base_class_from_empty_string(self) -> None: + be = base._BaseObject.class_from_string("") + + assert be.id == "" + assert be.target_id == "" + assert be.ns == "{http://www.opengis.net/kml/2.2}" + class TestLxml(Lxml, TestStdLibrary): """Test the base object with lxml.""" @@ -95,7 +123,10 @@ def test_from_string(self) -> None: be.__name__ = "test" be.from_string( - xml_string='\n' + xml_string=( + '\n' + ) ) assert be.id == "id-0" diff --git a/tests/data_test.py b/tests/data_test.py new file mode 100644 index 00000000..adad5f7b --- /dev/null +++ b/tests/data_test.py @@ -0,0 +1,203 @@ +# Copyright (C) 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 gx classes.""" +import pytest + +import fastkml as kml +from fastkml import data +from tests.base import Lxml +from tests.base import StdLibrary + + +class TestStdLibrary(StdLibrary): + """Test with the standard library.""" + + def test_schema(self) -> None: + ns = "{http://www.opengis.net/kml/2.2}" # noqa: FS003 + pytest.raises(ValueError, kml.Schema, ns) + s = kml.Schema(ns, "some_id") + assert not list(s.simple_fields) + s.append("int", "Integer", "An Integer") + assert list(s.simple_fields)[0]["type"] == "int" + assert list(s.simple_fields)[0]["name"] == "Integer" + assert list(s.simple_fields)[0]["displayName"] == "An Integer" + s.simple_fields = None + assert not list(s.simple_fields) + pytest.raises(TypeError, s.append, ("none", "Integer", "An Integer")) + # pytest.raises( + # TypeError, s.simple_fields, ("none", "Integer", "An Integer"), + # ) + # pytest.raises(TypeError, s.simple_fields, ("int", "Integer", "An Integer")) + fields = {"type": "int", "name": "Integer", "display_name": "An Integer"} + s.simple_fields = fields + assert list(s.simple_fields)[0]["type"] == "int" + assert list(s.simple_fields)[0]["name"] == "Integer" + assert list(s.simple_fields)[0]["displayName"] == "An Integer" + s.simple_fields = [["float", "Float"], fields] + assert list(s.simple_fields)[0]["type"] == "int" + assert list(s.simple_fields)[0]["name"] == "Integer" + assert list(s.simple_fields)[0]["displayName"] == "An Integer" + assert list(s.simple_fields)[1]["type"] == "float" + assert list(s.simple_fields)[1]["name"] == "Float" + assert list(s.simple_fields)[1]["displayName"] is None + + def test_schema_data(self) -> None: + ns = "{http://www.opengis.net/kml/2.2}" # noqa: FS003 + pytest.raises(ValueError, data.SchemaData, ns) + pytest.raises(ValueError, data.SchemaData, ns, "") + sd = data.SchemaData(ns, "#default") + sd.append_data("text", "Some Text") + assert len(sd.data) == 1 + sd.append_data(value=1, name="Integer") + assert len(sd.data) == 2 + assert sd.data[0] == {"value": "Some Text", "name": "text"} + assert sd.data[1] == {"value": 1, "name": "Integer"} + new_data = (("text", "Some new Text"), {"value": 2, "name": "Integer"}) + sd.data = new_data + assert len(sd.data) == 2 + assert sd.data[0] == {"value": "Some new Text", "name": "text"} + assert sd.data[1] == {"value": 2, "name": "Integer"} + + def test_untyped_extended_data(self) -> None: + ns = "{http://www.opengis.net/kml/2.2}" # noqa: FS003 + k = kml.KML(ns=ns) + + p = kml.Placemark(ns, "id", "name", "description") + p.extended_data = kml.ExtendedData( + ns=ns, + elements=[ + data.Data(ns=ns, name="info", value="so much to see"), + data.Data( + ns=ns, name="weather", display_name="Weather", value="blue skies" + ), + ], + ) + + assert len(p.extended_data.elements) == 2 + k.append(p) + + k2 = kml.KML() + k2.from_string(k.to_string(prettyprint=True)) + k.to_string() + + extended_data = list(k2.features())[0].extended_data + assert extended_data is not None + assert len(extended_data.elements), 2 + assert extended_data.elements[0].name == "info" + assert extended_data.elements[0].value == "so much to see" + assert extended_data.elements[0].display_name is None + assert extended_data.elements[1].name == "weather" + assert extended_data.elements[1].value == "blue skies" + assert extended_data.elements[1].display_name == "Weather" + + def test_untyped_extended_data_nested(self) -> None: + ns = "{http://www.opengis.net/kml/2.2}" # noqa: FS003 + k = kml.KML(ns=ns) + + d = kml.Document(ns, "docid", "doc name", "doc description") + d.extended_data = kml.ExtendedData( + ns=ns, elements=[data.Data(ns=ns, name="type", value="Document")] + ) + + f = kml.Folder(ns, "fid", "f name", "f description") + f.extended_data = kml.ExtendedData( + ns=ns, elements=[data.Data(ns=ns, name="type", value="Folder")] + ) + + k.append(d) + d.append(f) + + k2 = kml.KML() + k2.from_string(k.to_string()) + + document_data = list(k2.features())[0].extended_data + folder_data = list(list(k2.features())[0].features())[0].extended_data + + assert document_data.elements[0].name == "type" + assert document_data.elements[0].value == "Document" + + assert folder_data.elements[0].name == "type" + assert folder_data.elements[0].value == "Folder" + + def test_extended_data(self) -> None: + doc = """ + + Simple placemark + + + -122.0822035425683,37.42228990140251,0 + + + + This is hole + ]]> + 1 + + + The par for this hole is + ]]> + 4 + + + Mount Everest + 347.45 + 10000 + + + + """ + + k = kml.KML() + k.from_string(doc) + + extended_data = list(k.features())[0].extended_data + + assert extended_data.elements[0].name == "holeNumber" + assert extended_data.elements[0].value == "1" + assert "This is hole " in extended_data.elements[0].display_name + + assert extended_data.elements[1].name == "holePar" + assert extended_data.elements[1].value == "4" + assert ( + "The par for this hole is " in extended_data.elements[1].display_name + ) + sd = extended_data.elements[2] + assert sd.data[0]["name"] == "TrailHeadName" + assert sd.data[1]["value"] == "347.45" + + def test_schema_data_from_str(self) -> None: + doc = """ + Pi in the sky + 3.14159 + 10 + """ + + sd = data.SchemaData(ns="", schema_url="#default") + sd.from_string(doc) + assert sd.schema_url == "#TrailHeadTypeId" + assert sd.data[0] == {"name": "TrailHeadName", "value": "Pi in the sky"} + assert sd.data[1] == {"name": "TrailLength", "value": "3.14159"} + assert sd.data[2] == {"name": "ElevationGain", "value": "10"} + sd1 = data.SchemaData(ns="", schema_url="#default") + sd1.from_string(sd.to_string()) + assert sd1.schema_url == "#TrailHeadTypeId" + assert sd.to_string() == sd1.to_string() + + +class TestLxml(Lxml, TestStdLibrary): + """Test with lxml.""" diff --git a/tests/geometries/__init__.py b/tests/geometries/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/geometries/geometry_test.py b/tests/geometries/geometry_test.py new file mode 100644 index 00000000..19d4528f --- /dev/null +++ b/tests/geometries/geometry_test.py @@ -0,0 +1,396 @@ +# 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 geometry classes.""" + +from fastkml.geometry import AltitudeMode +from fastkml.geometry import LinearRing +from fastkml.geometry import LineString +from fastkml.geometry import MultiGeometry +from fastkml.geometry import Point +from fastkml.geometry import Polygon +from fastkml.geometry import _Geometry +from tests.base import Lxml +from tests.base import StdLibrary + + +class TestGetGeometry(StdLibrary): + def test_altitude_mode(self) -> None: + doc = """ + 0.000000,1.000000 + clampToGround + """ + + g = Point.class_from_string(doc) + + assert g.altitude_mode == AltitudeMode("clampToGround") + + def test_extrude(self) -> None: + doc = """ + 0.000000,1.000000 + 1 + """ + + g = Point.class_from_string(doc) + + assert g.extrude is True + + def test_tesselate(self) -> None: + doc = """ + 0.000000,1.000000 + 1 + """ + + g = Point.class_from_string(doc) + + assert g.tessellate is True + + def test_point(self) -> None: + doc = """ + 0.000000,1.000000 + """ + + g = Point.class_from_string(doc) + + assert g.geometry.__geo_interface__ == { + "type": "Point", + "bbox": (0.0, 1.0, 0.0, 1.0), + "coordinates": (0.0, 1.0), + } + + def test_linestring(self) -> None: + doc = """ + 0.000000,0.000000 1.000000,1.000000 + """ + + g = LineString.class_from_string(doc) + + assert g.geometry.__geo_interface__ == { + "type": "LineString", + "bbox": (0.0, 0.0, 1.0, 1.0), + "coordinates": ((0.0, 0.0), (1.0, 1.0)), + } + + def test_linearring(self) -> None: + doc = """ + 0.000000,0.000000 1.000000,0.000000 1.000000,1.000000 + 0.000000,0.000000 + + """ + + g = LinearRing.class_from_string(doc) + + assert g.geometry.__geo_interface__ == { + "type": "LinearRing", + "bbox": (0.0, 0.0, 1.0, 1.0), + "coordinates": ((0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)), + } + + def test_polygon(self) -> None: + doc = """ + + + 0.000000,0.000000 1.000000,0.000000 1.000000,1.000000 + 0.000000,0.000000 + + + + """ + + g = Polygon.class_from_string(doc) + + assert g.geometry.__geo_interface__ == { + "type": "Polygon", + "bbox": (0.0, 0.0, 1.0, 1.0), + "coordinates": (((0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)),), + } + + def test_polygon_with_inner_boundary(self) -> None: + doc = """ + + + -1.000000,-1.000000 2.000000,-1.000000 2.000000,2.000000 + -1.000000,-1.000000 + + + + + 0.000000,0.000000 1.000000,0.000000 1.000000,1.000000 + 0.000000,0.000000 + + + + """ + + g = Polygon.class_from_string(doc) + + assert g.geometry.__geo_interface__ == { + "type": "Polygon", + "bbox": (-1.0, -1.0, 2.0, 2.0), + "coordinates": ( + ((-1.0, -1.0), (2.0, -1.0), (2.0, 2.0), (-1.0, -1.0)), + ((0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)), + ), + } + + def test_multipoint(self) -> None: + doc = """ + + + 0.000000,1.000000 + + + 1.000000,1.000000 + + + """ + + g = MultiGeometry.class_from_string(doc) + + assert len(g.geometry) == 2 + + def test_multilinestring(self) -> None: + doc = """ + + + 0.000000,0.000000 1.000000,0.000000 + + + 0.000000,1.000000 1.000000,1.000000 + + + """ + + g = MultiGeometry.class_from_string(doc) + + assert len(g.geometry) == 2 + + def test_multipolygon(self) -> None: + doc = """ + + + + + -1.000000,-1.000000 2.000000,-1.000000 + 2.000000,2.000000 -1.000000,-1.000000 + + + + + 0.000000,0.000000 1.000000,0.000000 1.000000,1.000000 + 0.000000,0.000000 + + + + + + + 3.000000,0.000000 4.000000,0.000000 4.000000,1.000000 + 3.000000,0.000000 + + + + + """ + + g = MultiGeometry.class_from_string(doc) + + assert len(g.geometry) == 2 + + def test_geometrycollection(self) -> None: + doc = """ + + + + + 3,0 4,0 4,1 3,0 + + + + + 0.000000,1.000000 + + + 0.000000,0.000000 1.000000,1.000000 + + + 0.0,0.0 1.0,0.0 1.0,1.0 0.0,1.0 0.0,0.0 + + + """ + + g = MultiGeometry.class_from_string(doc) + + assert len(g.geometry) == 4 + + def test_geometrycollection_with_linearring(self) -> None: + doc = """ + + + 3.0,0.0 4.0,0.0 4.0,1.0 3.0,0.0 + + + 0.0,0.0 1.0,0.0 1.0,1.0 0.0,0.0 + + + """ + + g = MultiGeometry.class_from_string(doc) + + assert len(g.geometry) == 2 + assert g.geometry.geom_type == "GeometryCollection" + + +class TestGeometry(StdLibrary): + """Test the _Geometry class.""" + + def test_init(self) -> None: + """Test the init method.""" + g = _Geometry() + + assert g.ns == "{http://www.opengis.net/kml/2.2}" + assert g.target_id is None + assert g.id is None + assert g.extrude is False + assert g.altitude_mode is None + assert g.tessellate is False + + def test_init_with_args(self) -> None: + """Test the init method with arguments.""" + g = _Geometry( + ns="", + target_id="target_id", + id="id", + extrude=True, + altitude_mode=AltitudeMode.clamp_to_ground, + tessellate=True, + ) + + assert g.ns == "" + assert g.target_id == "target_id" + assert g.id == "id" + assert g.extrude is True + assert g.altitude_mode == AltitudeMode.clamp_to_ground + assert g.tessellate is True + + def test_to_string(self) -> None: + """Test the to_string method.""" + g = _Geometry() + + assert "http://www.opengis.net/kml/2.2" in g.to_string() + assert "targetId=" not in g.to_string() + assert "id=" not in g.to_string() + assert "extrude>00<" in g.to_string() + + def test_to_string_with_args(self) -> None: + """Test the to_string method.""" + g = _Geometry( + ns="{http://www.opengis.net/kml/2.3}", + target_id="target_id", + id="my-id", + extrude=True, + altitude_mode=AltitudeMode.relative_to_ground, + tessellate=True, + ) + + assert "http://www.opengis.net/kml/2.3" in g.to_string() + assert 'targetId="target_id"' in g.to_string() + assert 'id="my-id"' in g.to_string() + assert "extrude>1relativeToGround<" in g.to_string() + assert "tessellate>1<" in g.to_string() + # assert not g.to_string() + + def test_from_string(self) -> None: + """Test the from_string method.""" + g = _Geometry.class_from_string( + '<_Geometry id="my-id" targetId="target_id">' + "1" + "relativeToGround" + "1" + "", + ns="", + ) + + assert g.ns == "" + assert g.target_id == "target_id" + assert g.id == "my-id" + assert g.extrude is True + assert g.altitude_mode == AltitudeMode.relative_to_ground + assert g.tessellate is True + + def test_from_string_invalid_altitude_mode(self) -> None: + """Test the from_string method.""" + g = _Geometry.class_from_string( + '<_Geometry id="my-id" targetId="target_id">' + "relative" + "", + ns="", + ) + + assert g.altitude_mode is None + + def test_from_string_invalid_extrude(self) -> None: + """Test the from_string method.""" + g = _Geometry.class_from_string( + '<_Geometry id="my-id" targetId="target_id">' + "true" + "", + ns="", + ) + + assert g.extrude is None + + def test_from_minimal_string(self) -> None: + g = _Geometry.class_from_string( + "<_Geometry/>", + ns="", + ) + + assert g.ns == "" + assert g.target_id == "" + assert g.id == "" + assert g.extrude is None + assert g.altitude_mode is None + assert g.tessellate is None + + def test_from_string_omitting_ns(self) -> None: + """Test the from_string method.""" + g = _Geometry.class_from_string( + '' + "1" + "relativeToGround" + "1" + "", + ) + + assert g.ns == "{http://www.opengis.net/kml/2.2}" + assert g.target_id == "target_id" + assert g.id == "my-id" + assert g.extrude is True + assert g.altitude_mode == AltitudeMode.relative_to_ground + assert g.tessellate is True + + +class TestGetGeometryLxml(Lxml, TestGetGeometry): + """Test with lxml.""" + + +class TestGeometryLxml(Lxml, TestGeometry): + """Test with lxml.""" diff --git a/tests/geometries/linearring_test.py b/tests/geometries/linearring_test.py new file mode 100644 index 00000000..979a6b75 --- /dev/null +++ b/tests/geometries/linearring_test.py @@ -0,0 +1,66 @@ +# Copyright (C) 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 geometry classes.""" +from typing import cast + +import pygeoif.geometry as geo + +from fastkml.geometry import LinearRing +from tests.base import Lxml +from tests.base import StdLibrary + + +class TestLinearRing(StdLibrary): + def test_init(self) -> None: + """Test the init method.""" + lr = geo.LinearRing(((0, 0), (0, 1), (1, 1), (1, 0), (0, 0))) + + linear_ring = LinearRing(geometry=lr) + + assert linear_ring.geometry == lr + assert linear_ring.altitude_mode is None + assert linear_ring.extrude is False + + def test_to_string(self) -> None: + """Test the to_string method.""" + lr = geo.LinearRing(((0, 0), (0, 1), (1, 1), (1, 0), (0, 0))) + + linear_ring = LinearRing(geometry=lr) + + assert "LinearRing" in linear_ring.to_string() + assert ( + "coordinates>0.000000,0.000000 0.000000,1.000000 1.000000,1.000000 " + "1.000000,0.000000 0.000000,0.000000 None: + """Test the from_string method.""" + linear_ring = cast( + LinearRing, + LinearRing.class_from_string( + '' + "0.000000,0.000000 1.000000,0.000000 1.0,1.0 " + "0.000000,0.000000" + "" + ), + ) + + assert linear_ring.geometry == geo.LinearRing(((0, 0), (1, 0), (1, 1), (0, 0))) + + +class TestLinearRingLxml(Lxml, TestLinearRing): + """Test with lxml.""" diff --git a/tests/geometries/linestring_test.py b/tests/geometries/linestring_test.py new file mode 100644 index 00000000..e1351686 --- /dev/null +++ b/tests/geometries/linestring_test.py @@ -0,0 +1,71 @@ +# Copyright (C) 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 geometry classes.""" +from typing import cast + +import pygeoif.geometry as geo + +from fastkml.geometry import LineString +from tests.base import Lxml +from tests.base import StdLibrary + + +class TestLineString(StdLibrary): + def test_init(self) -> None: + """Test the init method.""" + ls = geo.LineString(((1, 2), (2, 0))) + + line_string = LineString(geometry=ls) + + assert line_string.geometry == ls + assert line_string.altitude_mode is None + assert line_string.extrude is False + + def test_to_string(self) -> None: + """Test the to_string method.""" + ls = geo.LineString(((1, 2), (2, 0))) + + line_string = LineString(geometry=ls) + + assert "LineString" in line_string.to_string() + assert ( + "coordinates>1.000000,2.000000 2.000000,0.000000 None: + """Test the from_string method.""" + linestring = cast( + LineString, + LineString.class_from_string( + '' + "1" + "1" + "" + "-122.364383,37.824664,0 -122.364152,37.824322,0" + "" + "" + ), + ) + + assert linestring.geometry == geo.LineString( + ((-122.364383, 37.824664, 0), (-122.364152, 37.824322, 0)) + ) + + +class TestLineStringLxml(Lxml, TestLineString): + """Test with lxml.""" diff --git a/tests/geometries/multigeometry_test.py b/tests/geometries/multigeometry_test.py new file mode 100644 index 00000000..00c26437 --- /dev/null +++ b/tests/geometries/multigeometry_test.py @@ -0,0 +1,374 @@ +# 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 geometry classes.""" +import pygeoif.geometry as geo + +from fastkml.geometry import MultiGeometry +from tests.base import Lxml +from tests.base import StdLibrary + + +class TestMultiPointStdLibrary(StdLibrary): + """Test with the standard library.""" + + def test_1_point(self): + """Test with one point.""" + p = geo.MultiPoint([(1, 2)]) + + mg = MultiGeometry(ns="", geometry=p) + + assert "coordinates>1.000000,2.000000" in mg.to_string() + assert "Point>" in mg.to_string() + + def test_2_points(self): + """Test with two points.""" + p = geo.MultiPoint([(1, 2), (3, 4)]) + + mg = MultiGeometry(ns="", geometry=p) + + assert "coordinates>1.000000,2.0000003.000000,4.000000" in mg.to_string() + assert "Point>" in mg.to_string() + + def test_2_points_read(self) -> None: + xml = ( + "00" + "1.000000,2.000000" + "3.000000,4.000000" + ) + + mg = MultiGeometry.class_from_string(xml, ns="") + + assert mg.geometry == geo.MultiPoint([(1, 2), (3, 4)]) + + +class TestMultiLineStringStdLibrary(StdLibrary): + def test_1_linestring(self): + """Test with one linestring.""" + p = geo.MultiLineString([[(1, 2), (3, 4)]]) + + mg = MultiGeometry(ns="", geometry=p) + + assert "coordinates>1.000000,2.000000 3.000000,4.000000" in mg.to_string() + assert "LineString>" in mg.to_string() + + def test_2_linestrings(self): + """Test with two linestrings.""" + p = geo.MultiLineString([[(1, 2), (3, 4)], [(5, 6), (7, 8)]]) + + mg = MultiGeometry(ns="", geometry=p) + + assert "coordinates>1.000000,2.000000 3.000000,4.0000005.000000,6.000000 7.000000,8.000000" in mg.to_string() + assert "LineString>" in mg.to_string() + + def test_2_linestrings_read(self) -> None: + xml = ( + "00" + "1.000000,2.000000 3.000000,4.000000" + "" + "5.000000,6.000000 7.000000,8.000000" + "" + ) + + mg = MultiGeometry.class_from_string(xml, ns="") + + assert mg.geometry == geo.MultiLineString([[(1, 2), (3, 4)], [(5, 6), (7, 8)]]) + + +class TestMultiPolygonStdLibrary(StdLibrary): + def test_1_polygon(self): + """Test with one polygon.""" + p = geo.MultiPolygon([[[[1, 2], [3, 4], [5, 6], [1, 2]]]]) + + mg = MultiGeometry(ns="", geometry=p) + + assert ( + "coordinates>1.000000,2.000000 3.000000,4.000000 5.000000,6.000000 " + "1.000000,2.000000" in mg.to_string() + assert "Polygon>" in mg.to_string() + assert "outerBoundaryIs>" in mg.to_string() + assert "innerBoundaryIs>" not in mg.to_string() + + def test_1_polygons_with_holes(self): + """Test with one polygon with holes.""" + p = geo.MultiPolygon( + [ + ( + ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)), + [((0.25, 0.25), (0.25, 0.5), (0.5, 0.5), (0.5, 0.25))], + ), + ], + ) + mg = MultiGeometry(ns="", geometry=p) + + assert ( + "coordinates>0.000000,0.000000 0.000000,1.000000 1.000000,1.000000 " + "1.000000,0.000000 0.000000,0.0000000.250000,0.250000 0.250000,0.500000 0.500000,0.500000 " + "0.500000,0.250000 0.250000,0.250000" in mg.to_string() + assert "Polygon>" in mg.to_string() + assert "outerBoundaryIs>" in mg.to_string() + assert "innerBoundaryIs>" in mg.to_string() + + def test_2_polygons(self): + """Test with two polygons.""" + p = geo.MultiPolygon( + [ + ( + ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)), + [((0.1, 0.1), (0.1, 0.2), (0.2, 0.2), (0.2, 0.1))], + ), + (((0.0, 0.0), (0.0, 2.0), (1.0, 1.0), (1.0, 0.0)),), + ], + ) + + mg = MultiGeometry(ns="", geometry=p) + + assert ( + "coordinates>0.000000,0.000000 0.000000,1.000000 1.000000,1.000000 " + "1.000000,0.000000 0.000000,0.0000000.100000,0.100000 0.100000,0.200000 0.200000,0.200000 " + "0.200000,0.100000 0.100000,0.1000000.000000,0.000000 0.000000,2.000000 1.000000,1.000000 " + "1.000000,0.000000 0.000000,0.000000" in mg.to_string() + assert "Polygon>" in mg.to_string() + assert "outerBoundaryIs>" in mg.to_string() + assert "innerBoundaryIs>" in mg.to_string() + + def test_2_polygons_read(self) -> None: + xml = ( + "00" + "" + "0.000000,0.000000 0.000000,1.000000 1.000000,1.000000 " + "1.000000,0.000000 0.000000,0.000000" + "" + "" + "0.100000,0.100000 0.100000,0.200000 0.200000,0.200000 " + "0.200000,0.100000 0.100000,0.100000" + "" + "" + "0.000000,0.000000 0.000000,2.000000 1.000000,1.000000 " + "1.000000,0.000000 0.000000,0.000000" + "" + ) + + mg = MultiGeometry.class_from_string(xml, ns="") + + assert mg.geometry == geo.MultiPolygon( + [ + ( + ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)), + [((0.1, 0.1), (0.1, 0.2), (0.2, 0.2), (0.2, 0.1))], + ), + (((0.0, 0.0), (0.0, 2.0), (1.0, 1.0), (1.0, 0.0)),), + ], + ) + + +class TestGeometryCollectionStdLibrary(StdLibrary): + """Test heterogeneous geometry collections.""" + + def test_1_point(self): + """Test with one point.""" + p = geo.GeometryCollection([geo.Point(1, 2)]) + + mg = MultiGeometry(ns="", geometry=p) + + assert "coordinates>1.000000,2.000000" in mg.to_string() + assert "Point>" in mg.to_string() + + def test_geometries(self): + p = geo.Point(1, 2) + ls = geo.LineString(((1, 2), (2, 0))) + lr = geo.LinearRing(((0, 0), (0, 1), (1, 1), (1, 0), (0, 0))) + poly = geo.Polygon( + [(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)], + [[(0.1, 0.1), (0.1, 0.9), (0.9, 0.9), (0.9, 0.1), (0.1, 0.1)]], + ) + gc = geo.GeometryCollection([p, ls, lr, poly]) + + mg = MultiGeometry(ns="", geometry=gc) + + assert "Point>" in mg.to_string() + assert "LineString>" in mg.to_string() + assert "LinearRing>" in mg.to_string() + assert "Polygon>" in mg.to_string() + assert "MultiGeometry>" in mg.to_string() + + def test_multi_geometries(self): + p = geo.Point(1, 2) + ls = geo.LineString(((1, 2), (2, 0))) + lr = geo.LinearRing(((0, 0), (0, 1), (1, 1), (1, 0), (0, 0))) + poly = geo.Polygon( + [(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)], + [[(0.1, 0.1), (0.1, 0.9), (0.9, 0.9), (0.9, 0.1), (0.1, 0.1)]], + ) + gc = geo.GeometryCollection([p, ls, lr, poly]) + mp = geo.MultiPolygon( + [ + ( + ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)), + [((0.1, 0.1), (0.1, 0.2), (0.2, 0.2), (0.2, 0.1))], + ), + (((0.0, 0.0), (0.0, 2.0), (1.0, 1.0), (1.0, 0.0)),), + ], + ) + ml = geo.MultiLineString([[(1, 2), (3, 4)], [(5, 6), (7, 8)]]) + mgc = geo.GeometryCollection([gc, mp, ml]) + mg = MultiGeometry(ns="", geometry=mgc) + + assert "Point>" in mg.to_string() + assert "LineString>" in mg.to_string() + assert "LinearRing>" in mg.to_string() + assert "Polygon>" in mg.to_string() + assert "MultiGeometry>" in mg.to_string() + + def test_multi_geometries_read(self) -> None: + xml = ( + "00" + "1.000000,2.000000" + "1.000000,2.000000 2.000000,0.000000" + "0.000000,0.000000 0.000000,1.000000 " + "1.000000,1.000000 1.000000,0.000000 0.000000,0.000000" + "" + "0.000000,0.000000 0.000000,1.000000 1.000000,1.000000 " + "1.000000,0.000000 0.000000,0.000000" + "" + "0.100000,0.100000 0.100000,0.900000 0.900000,0.900000 " + "0.900000,0.100000 0.100000,0.100000" + "" + "" + "0.000000,0.000000 0.000000,1.000000 1.000000,1.000000 " + "1.000000,0.000000 0.000000,0.000000" + "" + "0.100000,0.100000 0.100000,0.200000 0.200000,0.200000 " + "0.200000,0.100000 0.100000,0.100000" + "" + "" + "0.000000,0.000000 0.000000,2.000000 1.000000,1.000000 " + "1.000000,0.000000 0.000000,0.000000" + "" + "1.000000,2.000000 3.000000,4.000000" + "5.000000,6.000000 7.000000,8.000000" + "" + ) + + mg = MultiGeometry.class_from_string(xml, ns="") + + assert mg.geometry == geo.GeometryCollection( + ( + geo.GeometryCollection( + ( + geo.Point(1.0, 2.0), + geo.LineString(((1.0, 2.0), (2.0, 0.0))), + geo.Polygon( + ( + (0.0, 0.0), + (0.0, 1.0), + (1.0, 1.0), + (1.0, 0.0), + (0.0, 0.0), + ), + ( + ( + (0.1, 0.1), + (0.1, 0.9), + (0.9, 0.9), + (0.9, 0.1), + (0.1, 0.1), + ), + ), + ), + geo.LinearRing( + ((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)) + ), + ) + ), + geo.MultiPolygon( + ( + ( + ( + (0.0, 0.0), + (0.0, 1.0), + (1.0, 1.0), + (1.0, 0.0), + (0.0, 0.0), + ), + ( + ( + (0.1, 0.1), + (0.1, 0.2), + (0.2, 0.2), + (0.2, 0.1), + (0.1, 0.1), + ), + ), + ), + (((0.0, 0.0), (0.0, 2.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)),), + ) + ), + geo.MultiLineString( + (((1.0, 2.0), (3.0, 4.0)), ((5.0, 6.0), (7.0, 8.0))) + ), + ) + ) + + def test_empty_multi_geometries_read(self) -> None: + xml = ( + "00" + "" + ) + + mg = MultiGeometry.class_from_string(xml, ns="") + + assert mg.geometry is None + assert "MultiGeometry>" in mg.to_string() + assert "coordinates>" not in mg.to_string() + + +class TestMultiPointLxml(Lxml, TestMultiPointStdLibrary): + """Test with lxml.""" + + +class TestMultiLineStringLxml(Lxml, TestMultiLineStringStdLibrary): + """Test with lxml.""" + + +class TestMultiPolygonLxml(Lxml, TestMultiPolygonStdLibrary): + """Test with lxml.""" + + +class TestGeometryCollectionLxml(Lxml, TestGeometryCollectionStdLibrary): + """Test with lxml.""" diff --git a/tests/geometries/point_test.py b/tests/geometries/point_test.py new file mode 100644 index 00000000..2e60c87e --- /dev/null +++ b/tests/geometries/point_test.py @@ -0,0 +1,110 @@ +# 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 geometry classes.""" +from typing import cast + +import pygeoif.geometry as geo +import pytest + +from fastkml.exceptions import KMLParseError +from fastkml.exceptions import KMLWriteError +from fastkml.geometry import Point +from tests.base import Lxml +from tests.base import StdLibrary + + +class TestPoint(StdLibrary): + """Test the Point class.""" + + def test_init(self) -> None: + """Test the init method.""" + p = geo.Point(1, 2) + + point = Point(geometry=p) + + assert point.geometry == p + assert point.altitude_mode is None + assert point.extrude is False + + def test_to_string(self) -> None: + """Test the to_string method.""" + p = geo.Point(1, 2) + + point = Point(geometry=p) + + assert "Point" in point.to_string() + assert "coordinates>1.000000,2.000000 None: + """Test the to_string method.""" + point = Point(geometry=geo.Point(None, None)) # type: ignore[arg-type] + + with pytest.raises( + KMLWriteError, match=r"Invalid dimensions in coordinates '\(\(\),\)'" + ): + point.to_string() + + def test_from_string(self) -> None: + """Test the from_string method.""" + point = cast( + Point, + Point.class_from_string( + '' + "1.000000,2.000000" + "" + ), + ) + + assert point.geometry == geo.Point(1, 2) + assert point.altitude_mode is None + assert point.extrude is None + assert point.tessellate is None + + def test_empty_from_string(self) -> None: + """Test the from_string method.""" + with pytest.raises(KMLParseError, match=r"Invalid coordinates in "): + Point.class_from_string( + "", + ns="", + ) + + def test_from_string_empty_coordinates(self) -> None: + with pytest.raises( + KMLParseError, + match=r"Invalid coordinates in ", + ): + Point.class_from_string( + "", + ns="", + ) + + def test_from_string_invalid_coordinates(self) -> None: + with pytest.raises( + KMLParseError, + match=( + r"Invalid coordinates in " + "1" + ), + ): + Point.class_from_string( + "1", + ns="", + ) + + +class TestPointLxml(Lxml, TestPoint): + """Test with lxml.""" diff --git a/tests/geometries/polygon_test.py b/tests/geometries/polygon_test.py new file mode 100644 index 00000000..5274d233 --- /dev/null +++ b/tests/geometries/polygon_test.py @@ -0,0 +1,106 @@ +# Copyright (C) 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 geometry classes.""" +from typing import cast + +import pygeoif.geometry as geo + +from fastkml.geometry import Polygon +from tests.base import Lxml +from tests.base import StdLibrary + + +class TestStdLibrary(StdLibrary): + """Test with the standard library.""" + + def test_exterior_only(self): + """Test exterior only.""" + poly = geo.Polygon([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)]) + + polygon = Polygon(ns="", geometry=poly) + + assert "outerBoundaryIs>" in polygon.to_string() + assert "innerBoundaryIs>" not in polygon.to_string() + assert "LinearRing>" in polygon.to_string() + assert ( + "0.000000,0.000000 0.000000,1.000000 1.000000,1.000000 " + "1.000000,0.000000 0.000000,0.000000" + ) in polygon.to_string() + + def test_exterior_interior(self): + """Test exterior and interior.""" + poly = geo.Polygon( + [(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)], + [[(0.1, 0.1), (0.1, 0.9), (0.9, 0.9), (0.9, 0.1), (0.1, 0.1)]], + ) + + polygon = Polygon(ns="", geometry=poly) + + assert "outerBoundaryIs>" in polygon.to_string() + assert "innerBoundaryIs>" in polygon.to_string() + assert "LinearRing>" in polygon.to_string() + assert ( + "0.000000,0.000000 0.000000,1.000000 1.000000,1.000000 " + "1.000000,0.000000 0.000000,0.000000" + ) in polygon.to_string() + assert ( + "0.100000,0.100000 0.100000,0.900000 0.900000,0.900000 " + "0.900000,0.100000 0.100000,0.100000" + ) in polygon.to_string() + + def test_from_string_exterior_only(self): + """Test exterior only.""" + doc = """ + + + 0.000000,0.000000 1.000000,0.000000 1.000000,1.000000 + 0.000000,0.000000 + + + """ + + polygon2 = cast(Polygon, Polygon.class_from_string(doc)) + + assert polygon2.geometry == geo.Polygon([(0, 0), (1, 0), (1, 1), (0, 0)]) + + def test_from_string_exterior_interior(self): + doc = """ + + + -1.000000,-1.000000 2.000000,-1.000000 2.000000,2.000000 + -1.000000,-1.000000 + + + + + 0.000000,0.000000 1.000000,0.000000 1.000000,1.000000 + 0.000000,0.000000 + + + + """ + + polygon2 = cast(Polygon, Polygon.class_from_string(doc)) + + assert polygon2.geometry == geo.Polygon( + [(-1, -1), (2, -1), (2, 2), (-1, -1)], + [[(0, 0), (1, 0), (1, 1), (0, 0)]], + ) + + +class TestLxml(Lxml, TestStdLibrary): + """Test with lxml.""" diff --git a/tests/gx_test.py b/tests/gx_test.py index 66d43167..2269bb60 100644 --- a/tests/gx_test.py +++ b/tests/gx_test.py @@ -15,6 +15,16 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA """Test the gx classes.""" +import datetime + +import pygeoif.geometry as geo +from dateutil.tz import tzutc + +from fastkml.enums import AltitudeMode +from fastkml.gx import Angle +from fastkml.gx import MultiTrack +from fastkml.gx import Track +from fastkml.gx import TrackItem from tests.base import Lxml from tests.base import StdLibrary @@ -23,5 +33,402 @@ class TestStdLibrary(StdLibrary): """Test with the standard library.""" +class TestGetGxGeometry(StdLibrary): + def test_track(self) -> None: + doc = """ + 2020-01-01T00:00:00Z + 2020-01-01T00:10:00Z + 0.000000 0.000000 + 1.000000 1.000000 + """ + g = Track.class_from_string(doc, ns="") + + assert g.geometry.__geo_interface__ == { + "type": "LineString", + "bbox": (0.0, 0.0, 1.0, 1.0), + "coordinates": ((0.0, 0.0), (1.0, 1.0)), + } + + def test_multitrack(self) -> None: + doc = """ + + + 2020-01-01T00:00:00+00:00 + 2020-01-01T00:10:00+00:00 + 0.000000 0.000000 + 1.000000 0.000000 + + + 2020-01-01T00:10:00+00:00 + 2020-01-01T00:20:00+00:00 + 0.000000 1.000000 + 1.000000 1.000000 + + + """ + + mt = MultiTrack.class_from_string(doc, ns="") + + assert mt.geometry == geo.MultiLineString( + (((0.0, 0.0), (1.0, 0.0)), ((0.0, 1.0), (1.0, 1.0))) + ) + assert "when>" in mt.to_string() + assert ( + mt.to_string() + == MultiTrack( + ns="", + id="", + target_id="", + extrude=None, + tessellate=None, + altitude_mode=None, + tracks=[ + Track( + ns="{http://www.google.com/kml/ext/2.2}", + id="", + target_id="", + extrude=None, + tessellate=None, + altitude_mode=None, + track_items=[ + TrackItem( + when=datetime.datetime( + 2020, 1, 1, 0, 0, tzinfo=tzutc() + ), + coord=geo.Point(0.0, 0.0), + angle=None, + ), + TrackItem( + when=datetime.datetime( + 2020, 1, 1, 0, 10, tzinfo=tzutc() + ), + coord=geo.Point(1.0, 0.0), + angle=None, + ), + ], + ), + Track( + ns="{http://www.google.com/kml/ext/2.2}", + id="", + target_id="", + extrude=None, + tessellate=None, + altitude_mode=None, + track_items=[ + TrackItem( + when=datetime.datetime( + 2020, 1, 1, 0, 10, tzinfo=tzutc() + ), + coord=geo.Point(0.0, 1.0), + angle=None, + ), + TrackItem( + when=datetime.datetime( + 2020, 1, 1, 0, 20, tzinfo=tzutc() + ), + coord=geo.Point(1.0, 1.0), + angle=None, + ), + ], + ), + ], + interpolate=None, + ).to_string() + ) + + +class TestTrack(StdLibrary): + """Test gx.Track.""" + + def test_track_from_linestring(self) -> None: + ls = geo.LineString(((1, 2), (2, 0))) + + track = Track( + ns="", + id="track1", + target_id="track2", + altitude_mode=AltitudeMode.absolute, + extrude=True, + tessellate=True, + geometry=ls, + ) + + assert "1" in track.to_string() + assert "tessellate>1" in track.to_string() + assert "altitudeMode>absolute" in track.to_string() + assert "coord>" in track.to_string() + assert "angles" in track.to_string() + assert "when" in track.to_string() + assert "angles>" not in track.to_string() + assert "when>" not in track.to_string() + assert repr(track) == ( + "Track(ns='', id='track1', target_id='track2', extrude=True, " + "tessellate=True, altitude_mode=AltitudeMode.absolute, " + "track_items=[TrackItem(when=None, coord=Point(1, 2), angle=None), " + "TrackItem(when=None, coord=Point(2, 0), angle=None)])" + ) + + def test_track_from_track_items(self) -> None: + time1 = 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)] + + track = Track( + ns="", + track_items=track_items, + ) + + assert "when>" in track.to_string() + assert ">2023-01-01T00:00:00+00:00" in track.to_string() + assert ">1 2" in track.to_string() + assert ">0.0 0.0 0.0 None: + time1 = datetime.datetime(2023, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc) + angle = Angle() + track_items = [TrackItem(when=time1, coord=None, angle=angle)] + + track = Track( + ns="", + track_items=track_items, + ) + + assert "when>" in track.to_string() + assert ">2023-01-01T00:00:00+00:00" not in track.to_string() + assert "coord" in track.to_string() + assert "angles>" in track.to_string() + assert ">0.0 0.0 0.0 None: + doc = """ + + 2010-05-28T02:02:09Z + 2010-05-28T02:02:35Z + 2010-05-28T02:02:44Z + 2010-05-28T02:02:53Z + 2010-05-28T02:02:54Z + 2010-05-28T02:02:55Z + 2010-05-28T02:02:56Z + + 45.54676 66.2342 77.0 + + 1 2 3 + 1 2 3 + 1 2 3 + 1 2 3 + 1 2 3 + 1 2 3 + -122.207881 37.371915 156.000000 + -122.205712 37.373288 152.000000 + -122.204678 37.373939 147.000000 + -122.203572 37.374630 142.199997 + + -122.203451 37.374706 141.800003 + -122.203329 37.374780 141.199997 + -122.203207 37.374857 140.199997 + + """ + expected_track = Track( + ns="", + id="", + target_id="", + extrude=None, + tessellate=None, + altitude_mode=None, + track_items=[ + TrackItem( + when=datetime.datetime(2010, 5, 28, 2, 2, 9, tzinfo=tzutc()), + coord=geo.Point(-122.207881, 37.371915, 156.0), + angle=Angle(heading=45.54676, tilt=66.2342, roll=77.0), + ), + TrackItem( + when=datetime.datetime(2010, 5, 28, 2, 2, 35, tzinfo=tzutc()), + coord=geo.Point(-122.205712, 37.373288, 152.0), + angle=None, + ), + TrackItem( + when=datetime.datetime(2010, 5, 28, 2, 2, 44, tzinfo=tzutc()), + coord=geo.Point(-122.204678, 37.373939, 147.0), + angle=Angle(heading=1.0, tilt=2.0, roll=3.0), + ), + TrackItem( + when=datetime.datetime(2010, 5, 28, 2, 2, 53, tzinfo=tzutc()), + coord=geo.Point(-122.203572, 37.37463, 142.199997), + angle=Angle(heading=1.0, tilt=2.0, roll=3.0), + ), + TrackItem( + when=datetime.datetime(2010, 5, 28, 2, 2, 54, tzinfo=tzutc()), + coord=None, + angle=Angle(heading=1.0, tilt=2.0, roll=3.0), + ), + TrackItem( + when=datetime.datetime(2010, 5, 28, 2, 2, 55, tzinfo=tzutc()), + coord=geo.Point(-122.203451, 37.374706, 141.800003), + angle=Angle(heading=1.0, tilt=2.0, roll=3.0), + ), + TrackItem( + when=datetime.datetime(2010, 5, 28, 2, 2, 56, tzinfo=tzutc()), + coord=geo.Point(-122.203329, 37.37478, 141.199997), + angle=Angle(heading=1.0, tilt=2.0, roll=3.0), + ), + TrackItem( + when=None, + coord=geo.Point(-122.203207, 37.374857, 140.199997), + angle=Angle(heading=1.0, tilt=2.0, roll=3.0), + ), + ], + ) + + track = Track.class_from_string(doc, ns="") + + assert track.geometry == geo.LineString( + ( + (-122.207881, 37.371915, 156.0), + (-122.205712, 37.373288, 152.0), + (-122.204678, 37.373939, 147.0), + (-122.203572, 37.37463, 142.199997), + (-122.203451, 37.374706, 141.800003), + (-122.203329, 37.37478, 141.199997), + (-122.203207, 37.374857, 140.199997), + ) + ) + assert track.to_string() == expected_track.to_string() + + +class TestMultiTrack(StdLibrary): + def test_from_multilinestring(self) -> None: + lines = geo.MultiLineString( + ([(0, 0), (1, 1), (1, 2), (2, 2)], [[0.0, 0.0], [1.0, 2.0]]), + ) + + mt = MultiTrack(geometry=lines, ns="") + + assert repr(mt) == repr( + MultiTrack( + ns="", + id=None, + target_id=None, + extrude=False, + tessellate=False, + altitude_mode=None, + tracks=[ + Track( + ns="", + id=None, + target_id=None, + extrude=False, + tessellate=False, + altitude_mode=None, + track_items=[ + TrackItem(when=None, coord=geo.Point(0, 0), angle=None), + TrackItem(when=None, coord=geo.Point(1, 1), angle=None), + TrackItem(when=None, coord=geo.Point(1, 2), angle=None), + TrackItem(when=None, coord=geo.Point(2, 2), angle=None), + ], + ), + Track( + ns="", + id=None, + target_id=None, + extrude=False, + tessellate=False, + altitude_mode=None, + track_items=[ + TrackItem(when=None, coord=geo.Point(0.0, 0.0), angle=None), + TrackItem(when=None, coord=geo.Point(1.0, 2.0), angle=None), + ], + ), + ], + ) + ) + + def test_multitrack(self) -> None: + track = MultiTrack( + ns="", + interpolate=True, + tracks=[ + Track( + ns="", + track_items=[ + TrackItem(when=None, coord=geo.Point(0, 0), angle=None), + TrackItem(when=None, coord=geo.Point(1, 1), angle=None), + TrackItem(when=None, coord=geo.Point(1, 2), angle=None), + TrackItem(when=None, coord=geo.Point(2, 2), angle=None), + ], + ), + Track( + ns="", + track_items=[ + TrackItem( + when=datetime.datetime( + 2010, 5, 28, 2, 2, 55, tzinfo=tzutc() + ), + coord=geo.Point(-122.203451, 37.374706, 141.800003), + angle=Angle(heading=1.0, tilt=2.0, roll=3.0), + ), + TrackItem( + when=datetime.datetime( + 2010, 5, 28, 2, 2, 56, tzinfo=tzutc() + ), + coord=geo.Point(-122.203329, 37.37478, 141.199997), + angle=Angle(heading=1.0, tilt=2.0, roll=3.0), + ), + ], + ), + ], + ) + + assert track.geometry == geo.MultiLineString( + ( + ((0, 0), (1, 1), (1, 2), (2, 2)), + ( + (-122.203451, 37.374706, 141.800003), + (-122.203329, 37.37478, 141.199997), + ), + ) + ) + assert "MultiTrack>" in track.to_string() + assert "interpolate>1" in track.to_string() + assert "coord>" in track.to_string() + assert "angles>" in track.to_string() + assert "when>" in track.to_string() + + class TestLxml(Lxml, TestStdLibrary): """Test with lxml.""" + + +class TestLxmlGetGxGeometry(Lxml, TestGetGxGeometry): + """Test with lxml.""" + + +class TestLxmlTrack(Lxml, TestTrack): + """Test with lxml.""" + + +class TestLxmlMultiTrack(Lxml, TestMultiTrack): + """Test with lxml.""" diff --git a/tests/oldunit_test.py b/tests/oldunit_test.py index 76b7ecbf..ee4f2c9d 100644 --- a/tests/oldunit_test.py +++ b/tests/oldunit_test.py @@ -16,23 +16,15 @@ import xml.etree.ElementTree import pytest +from pygeoif.geometry import LinearRing +from pygeoif.geometry import MultiPoint +from pygeoif.geometry import Polygon from fastkml import atom from fastkml import base from fastkml import config -from fastkml import data from fastkml import kml from fastkml import styles -from fastkml.geometry import Geometry -from fastkml.geometry import GeometryCollection -from fastkml.geometry import LinearRing -from fastkml.geometry import LineString -from fastkml.geometry import MultiLineString -from fastkml.geometry import MultiPoint -from fastkml.geometry import MultiPolygon -from fastkml.geometry import Point -from fastkml.geometry import Polygon -from fastkml.gx import GxGeometry try: import lxml @@ -51,7 +43,7 @@ def setup_method(self) -> None: config.set_etree_implementation(xml.etree.ElementTree) config.set_default_namespaces() - def test_base_object(self): + def test_base_object(self) -> None: bo = base._BaseObject(id="id0") assert bo.id == "id0" assert bo.ns == config.KMLNS @@ -78,7 +70,7 @@ def test_base_object(self): assert not bo.etree_element(), None assert len(bo.to_string()) > 1 - def test_feature(self): + def test_feature(self) -> None: f = kml._Feature(name="A Feature") pytest.raises(NotImplementedError, f.etree_element) assert f.name == "A Feature" @@ -102,7 +94,7 @@ def test_feature(self): assert "Feature>" in str(f.to_string()) assert "#default" in str(f.to_string()) - def test_container(self): + def test_container(self) -> None: f = kml._Container(name="A Container") # apparently you can add documents to containes # d = kml.Document() @@ -111,7 +103,7 @@ def test_container(self): f.append(p) pytest.raises(NotImplementedError, f.etree_element) - def test_overlay(self): + def test_overlay(self) -> None: o = kml._Overlay(name="An Overlay") assert o._color is None assert o._draw_order is None @@ -127,7 +119,7 @@ def setup_method(self) -> None: config.set_etree_implementation(xml.etree.ElementTree) config.set_default_namespaces() - def test_kml(self): + def test_kml(self) -> None: """kml file without contents""" k = kml.KML() assert not list(k.features()) @@ -139,7 +131,7 @@ def test_kml(self): k2.from_string(k.to_string()) assert k.to_string() == k2.to_string() - def test_folder(self): + def test_folder(self) -> None: """KML file with folders""" ns = "{http://www.opengis.net/kml/2.2}" # noqa: FS003 k = kml.KML() @@ -156,13 +148,13 @@ def test_folder(self): k2.from_string(s) assert s == k2.to_string() - def test_placemark(self): + def test_placemark(self) -> None: ns = "{http://www.opengis.net/kml/2.2}" # noqa: FS003 k = kml.KML(ns=ns) p = kml.Placemark(ns, "id", "name", "description") - p.geometry = Point(0.0, 0.0, 0.0) + # XXX p.geometry = Point(0.0, 0.0, 0.0) p2 = kml.Placemark(ns, "id2", "name2", "description2") - p2.geometry = LineString([(0, 0, 0), (1, 1, 1)]) + # XXX p2.geometry = LineString([(0, 0, 0), (1, 1, 1)]) k.append(p) k.append(p2) assert len(list(k.features())) == 2 @@ -170,112 +162,7 @@ def test_placemark(self): k2.from_string(k.to_string(prettyprint=True)) assert k.to_string() == k2.to_string() - def test_schema(self): - ns = "{http://www.opengis.net/kml/2.2}" # noqa: FS003 - pytest.raises(ValueError, kml.Schema, ns) - s = kml.Schema(ns, "some_id") - assert not list(s.simple_fields) - s.append("int", "Integer", "An Integer") - assert list(s.simple_fields)[0]["type"] == "int" - assert list(s.simple_fields)[0]["name"] == "Integer" - assert list(s.simple_fields)[0]["displayName"] == "An Integer" - s.simple_fields = None - assert not list(s.simple_fields) - pytest.raises(TypeError, s.append, ("none", "Integer", "An Integer")) - # pytest.raises( - # TypeError, s.simple_fields, ("none", "Integer", "An Integer"), - # ) - # pytest.raises(TypeError, s.simple_fields, ("int", "Integer", "An Integer")) - fields = {"type": "int", "name": "Integer", "display_name": "An Integer"} - s.simple_fields = fields - assert list(s.simple_fields)[0]["type"] == "int" - assert list(s.simple_fields)[0]["name"] == "Integer" - assert list(s.simple_fields)[0]["displayName"] == "An Integer" - s.simple_fields = [["float", "Float"], fields] - assert list(s.simple_fields)[0]["type"] == "int" - assert list(s.simple_fields)[0]["name"] == "Integer" - assert list(s.simple_fields)[0]["displayName"] == "An Integer" - assert list(s.simple_fields)[1]["type"] == "float" - assert list(s.simple_fields)[1]["name"] == "Float" - assert list(s.simple_fields)[1]["displayName"] is None - - def test_schema_data(self): - ns = "{http://www.opengis.net/kml/2.2}" # noqa: FS003 - pytest.raises(ValueError, data.SchemaData, ns) - pytest.raises(ValueError, data.SchemaData, ns, "") - sd = data.SchemaData(ns, "#default") - sd.append_data("text", "Some Text") - assert len(sd.data) == 1 - sd.append_data(value=1, name="Integer") - assert len(sd.data) == 2 - assert sd.data[0] == {"value": "Some Text", "name": "text"} - assert sd.data[1] == {"value": 1, "name": "Integer"} - new_data = (("text", "Some new Text"), {"value": 2, "name": "Integer"}) - sd.data = new_data - assert len(sd.data) == 2 - assert sd.data[0] == {"value": "Some new Text", "name": "text"} - assert sd.data[1] == {"value": 2, "name": "Integer"} - - def test_untyped_extended_data(self): - ns = "{http://www.opengis.net/kml/2.2}" # noqa: FS003 - k = kml.KML(ns=ns) - - p = kml.Placemark(ns, "id", "name", "description") - p.geometry = Point(0.0, 0.0, 0.0) - p.extended_data = kml.ExtendedData( - elements=[ - data.Data(name="info", value="so much to see"), - data.Data(name="weather", display_name="Weather", value="blue skies"), - ] - ) - - assert len(p.extended_data.elements) == 2 - k.append(p) - - k2 = kml.KML() - k2.from_string(k.to_string(prettyprint=True)) - k.to_string() - - extended_data = list(k2.features())[0].extended_data - assert extended_data is not None - assert len(extended_data.elements), 2 - assert extended_data.elements[0].name == "info" - assert extended_data.elements[0].value == "so much to see" - assert extended_data.elements[0].display_name is None - assert extended_data.elements[1].name == "weather" - assert extended_data.elements[1].value == "blue skies" - assert extended_data.elements[1].display_name == "Weather" - - def test_untyped_extended_data_nested(self): - ns = "{http://www.opengis.net/kml/2.2}" # noqa: FS003 - k = kml.KML(ns=ns) - - d = kml.Document(ns, "docid", "doc name", "doc description") - d.extended_data = kml.ExtendedData( - elements=[data.Data(name="type", value="Document")] - ) - - f = kml.Folder(ns, "fid", "f name", "f description") - f.extended_data = kml.ExtendedData( - elements=[data.Data(name="type", value="Folder")] - ) - - k.append(d) - d.append(f) - - k2 = kml.KML() - k2.from_string(k.to_string()) - - document_data = list(k2.features())[0].extended_data - folder_data = list(list(k2.features())[0].features())[0].extended_data - - assert document_data.elements[0].name == "type" - assert document_data.elements[0].value == "Document" - - assert folder_data.elements[0].name == "type" - assert folder_data.elements[0].value == "Folder" - - def test_document(self): + def test_document(self) -> None: k = kml.KML() ns = "{http://www.opengis.net/kml/2.2}" # noqa: FS003 d = kml.Document(ns, "docid", "doc name", "doc description") @@ -287,7 +174,7 @@ def test_document(self): f2 = kml.Folder(ns, "id2", "name2", "description2") d.append(f2) p = kml.Placemark(ns, "id", "name", "description") - p.geometry = Polygon([(0, 0, 0), (1, 1, 0), (1, 0, 1)]) + # XXX p.geometry = Polygon([(0, 0, 0), (1, 1, 0), (1, 0, 1)]) p2 = kml.Placemark(ns, "id2", "name2", "description2") # p2 does not have a geometry! f2.append(p) @@ -298,7 +185,7 @@ def test_document(self): k2.from_string(k.to_string()) assert k.to_string() == k2.to_string() - def test_author(self): + def test_author(self) -> None: d = kml.Document() d.author = "Christian Ledermann" assert "Christian Ledermann" in str(d.to_string()) @@ -316,12 +203,11 @@ def test_author(self): assert d.to_string() == d2.to_string() d.author = None - def test_link(self): + def test_link(self) -> None: d = kml.Document() d.link = "http://localhost" assert "http://localhost" in str(d.to_string()) - l = atom.Link(href="#here") - d.link = l + d.link = atom.Link(href="#here") assert "#here" in str(d.to_string()) # pytest.raises(TypeError, d.link, object) d2 = kml.Document() @@ -329,14 +215,14 @@ def test_link(self): assert d.to_string() == d2.to_string() d.link = None - def test_address(self): + def test_address(self) -> None: address = "1600 Amphitheatre Parkway, Mountain View, CA 94043, USA" d = kml.Document() d.address = address assert address in str(d.to_string()) assert "address>" in str(d.to_string()) - def test_phone_number(self): + def test_phone_number(self) -> None: phone = "+1 234 567 8901" d = kml.Document() d.phone_number = phone @@ -345,7 +231,7 @@ def test_phone_number(self): class TestKmlFromString: - def test_document(self): + def test_document(self) -> None: doc = """ Document.kml @@ -380,7 +266,7 @@ def test_document(self): k2.from_string(k.to_string()) assert k.to_string() == k2.to_string() - def test_document_booleans(self): + def test_document_booleans(self) -> None: doc = """ Document.kml @@ -406,7 +292,7 @@ def test_document_booleans(self): assert list(k.features())[0].visibility == 0 assert list(k.features())[0].isopen == 0 - def test_folders(self): + def test_folders(self) -> None: doc = """ Folder.kml @@ -455,7 +341,7 @@ def test_folders(self): k2.from_string(k.to_string()) assert k.to_string() == k2.to_string() - def test_placemark(self): + def test_placemark(self) -> None: doc = """ Simple placemark @@ -475,55 +361,7 @@ def test_placemark(self): k2.from_string(k.to_string()) assert k.to_string() == k2.to_string() - def test_extended_data(self): - doc = """ - - Simple placemark - - - -122.0822035425683,37.42228990140251,0 - - - - This is hole - ]]> - 1 - - - The par for this hole is - ]]> - 4 - - - Mount Everest - 347.45 - 10000 - - - - """ - - k = kml.KML() - k.from_string(doc) - - extended_data = list(k.features())[0].extended_data - - assert extended_data.elements[0].name == "holeNumber" - assert extended_data.elements[0].value == "1" - assert "This is hole " in extended_data.elements[0].display_name - - assert extended_data.elements[1].name == "holePar" - assert extended_data.elements[1].value == "4" - assert ( - "The par for this hole is " in extended_data.elements[1].display_name - ) - sd = extended_data.elements[2] - assert sd.data[0]["name"] == "TrailHeadName" - assert sd.data[1]["value"] == "347.45" - - def test_polygon(self): + def test_polygon(self) -> None: doc = """ @@ -647,7 +485,7 @@ def test_polygon(self): k2.from_string(k.to_string()) assert k.to_string() == k2.to_string() - def test_multipoints(self): + def test_multipoints(self) -> None: doc = """ MultiPoint @@ -701,46 +539,10 @@ def test_multipoints(self): k2.from_string(k.to_string()) assert k.to_string() == k2.to_string() - def test_multilinestrings(self): - doc = """ - - Dnipro (Dnieper) - - 33.54,46.831,0 33.606,46.869,0 33.662,46.957,0 33.739,47.05,0 33.859,47.149,0 33.976,47.307,0 33.998,47.411,0 34.155,47.49,0 34.448,47.542,0 34.712,47.553,0 34.946,47.521,0 35.088,47.528,0 35.138,47.573,0 35.149,47.657,0 35.106,47.842,0 - 33.194,49.094,0 32.884,49.225,0 32.603,49.302,0 31.886,49.555,0 - 31.44,50,0 31.48,49.933,0 31.486,49.871,0 31.467,49.754,0 - 30.508,51.217,0 30.478,50.904,0 30.479,50.749,0 30.515,50.597,0 - - """ - - k = kml.KML() - k.from_string(doc) - assert len(list(k.features())) == 1 - assert isinstance(list(k.features())[0].geometry, MultiLineString) - assert len(list(list(k.features())[0].geometry.geoms)) == 4 - k2 = kml.KML() - k2.from_string(k.to_string()) - assert k.to_string() == k2.to_string() - - def test_multipolygon(self): - doc = """ - - Italy - 12.621,35.492,0 12.611,35.489,0 12.603,35.491,0 12.598,35.494,0 12.594,35.494,0 12.556,35.508,0 12.536,35.513,0 12.526,35.517,0 12.534,35.522,0 12.556,35.521,0 12.567,35.519,0 12.613,35.515,0 12.621,35.513,0 12.624,35.512,0 12.622,35.51,0 12.621,35.508,0 12.624,35.502,0 12.621,35.492,0 12.873,35.852,0 12.857,35.852,0 12.851,35.856,0 12.846,35.863,0 12.847,35.868,0 12.854,35.871,0 12.86,35.872,0 12.867,35.872,0 12.874,35.866,0 12.877,35.856,0 12.873,35.852,0 11.981,36.827,0 11.988,36.824,0 11.994,36.825,0 12,36.836,0 12.038,36.806,0 12.052,36.79,0 12.054,36.767,0 12.031,36.741,0 11.997,36.745,0 11.962,36.765,0 11.938,36.789,0 11.934,36.795,0 11.926,36.812,0 11.923,36.828,0 11.935,36.836,0 11.939,36.837,0 11.947,36.841,0 11.952,36.843,0 11.958,36.84,0 11.968,36.831,0 11.972,36.829,0 11.981,36.827,0 12.322,37.94,0 12.337,37.933,0 12.355,37.927,0 12.369,37.925,0 12.358,37.914,0 12.343,37.913,0 12.327,37.918,0 12.315,37.925,0 12.3,37.919,0 12.288,37.921,0 12.279,37.929,0 12.274,37.939,0 12.288,37.938,0 12.298,37.941,0 12.306,37.945,0 12.315,37.946,0 12.322,37.94,0 12.078,37.96,0 12.079,37.95,0 12.065,37.951,0 12.048,37.961,0 12.037,37.974,0 12.03,37.984,0 12.036,37.991,0 12.054,37.992,0 12.065,37.986,0 12.072,37.968,0 12.078,37.96,0 15.643,38.262,0 15.635,38.261,0 15.625,38.261,0 15.584,38.24,0 15.57,38.227,0 15.564,38.214,0 15.56,38.2,0 15.576,38.2,0 15.527,38.137,0 15.501,38.085,0 15.393,37.976,0 15.303,37.864,0 15.284,37.833,0 15.267,37.812,0 15.242,37.795,0 15.214,37.761,0 15.207,37.747,0 15.209,37.737,0 15.219,37.718,0 15.221,37.706,0 15.217,37.696,0 15.203,37.685,0 15.2,37.675,0 15.197,37.655,0 15.185,37.626,0 15.179,37.604,0 15.164,37.567,0 15.117,37.522,0 15.097,37.494,0 15.092,37.477,0 15.09,37.459,0 15.093,37.36,0 15.097,37.343,0 15.104,37.33,0 15.111,37.322,0 15.181,37.291,0 15.218,37.285,0 15.237,37.275,0 15.253,37.257,0 15.262,37.234,0 15.245,37.246,0 15.236,37.242,0 15.229,37.23,0 15.221,37.22,0 15.222,37.237,0 15.216,37.244,0 15.206,37.244,0 15.193,37.24,0 15.2,37.227,0 15.184,37.207,0 15.195,37.176,0 15.217,37.155,0 15.234,37.165,0 15.248,37.158,0 15.248,37.152,0 15.23,37.149,0 15.232,37.135,0 15.247,37.118,0 15.265,37.11,0 15.289,37.108,0 15.304,37.101,0 15.309,37.086,0 15.303,37.062,0 15.289,37.069,0 15.283,37.061,0 15.284,37.048,0 15.292,37.042,0 15.313,37.044,0 15.322,37.04,0 15.33,37.027,0 15.333,37.011,0 15.325,37.008,0 15.315,37.012,0 15.309,37.018,0 15.304,37.016,0 15.269,37,0 15.275,36.993,0 15.267,36.989,0 15.264,36.987,0 15.269,36.98,0 15.269,36.973,0 15.245,36.972,0 15.227,36.965,0 15.212,36.956,0 15.197,36.952,0 15.175,36.944,0 15.159,36.924,0 15.108,36.82,0 15.107,36.808,0 15.095,36.799,0 15.099,36.779,0 15.118,36.747,0 15.135,36.687,0 15.135,36.675,0 15.115,36.66,0 15.094,36.655,0 15.074,36.659,0 15.056,36.671,0 15.041,36.687,0 15.034,36.694,0 15.021,36.699,0 15.008,36.703,0 14.998,36.702,0 14.994,36.696,0 14.983,36.689,0 14.958,36.698,0 14.919,36.72,0 14.883,36.73,0 14.847,36.726,0 14.781,36.699,0 14.777,36.707,0 14.774,36.71,0 14.761,36.706,0 14.745,36.719,0 14.685,36.726,0 14.672,36.744,0 14.659,36.754,0 14.601,36.772,0 14.583,36.781,0 14.566,36.778,0 14.488,36.793,0 14.476,36.805,0 14.395,36.945,0 14.37,36.973,0 14.279,37.044,0 14.209,37.081,0 14.127,37.112,0 14.089,37.117,0 13.977,37.11,0 13.968,37.108,0 13.949,37.099,0 13.939,37.096,0 13.895,37.101,0 13.833,37.139,0 13.795,37.152,0 13.752,37.159,0 13.716,37.171,0 13.684,37.189,0 13.599,37.256,0 13.57,37.273,0 13.535,37.282,0 13.489,37.288,0 13.453,37.299,0 13.422,37.314,0 13.373,37.346,0 13.33,37.366,0 13.312,37.381,0 13.303,37.386,0 13.29,37.389,0 13.279,37.393,0 13.254,37.432,0 13.248,37.436,0 13.226,37.446,0 13.215,37.458,0 13.207,37.464,0 13.195,37.466,0 13.19,37.469,0 13.18,37.484,0 13.175,37.487,0 13.052,37.5,0 13.037,37.495,0 13.027,37.493,0 13.017,37.497,0 13.011,37.507,0 13.005,37.527,0 13.001,37.535,0 12.975,37.557,0 12.943,37.568,0 12.863,37.576,0 12.781,37.574,0 12.698,37.563,0 12.66,37.565,0 12.637,37.582,0 12.595,37.638,0 12.578,37.652,0 12.564,37.658,0 12.524,37.658,0 12.507,37.665,0 12.49,37.682,0 12.475,37.703,0 12.466,37.72,0 12.461,37.734,0 12.46,37.748,0 12.457,37.76,0 12.449,37.771,0 12.437,37.783,0 12.428,37.797,0 12.428,37.809,0 12.445,37.816,0 12.447,37.812,0 12.461,37.819,0 12.466,37.823,0 12.464,37.825,0 12.471,37.853,0 12.473,37.854,0 12.478,37.872,0 12.479,37.881,0 12.477,37.886,0 12.468,37.897,0 12.466,37.906,0 12.465,37.913,0 12.465,37.914,0 12.468,37.916,0 12.491,37.954,0 12.497,37.98,0 12.503,37.997,0 12.505,38.011,0 12.493,38.021,0 12.524,38.031,0 12.55,38.055,0 12.577,38.072,0 12.609,38.062,0 12.639,38.079,0 12.652,38.091,0 12.657,38.107,0 12.663,38.116,0 12.677,38.116,0 12.692,38.112,0 12.705,38.111,0 12.726,38.126,0 12.725,38.15,0 12.72,38.175,0 12.732,38.193,0 12.738,38.181,0 12.75,38.182,0 12.761,38.181,0 12.767,38.162,0 12.791,38.117,0 12.819,38.078,0 12.829,38.07,0 12.858,38.058,0 12.869,38.051,0 12.87,38.042,0 12.902,38.028,0 12.945,38.033,0 13.028,38.062,0 13.062,38.083,0 13.07,38.091,0 13.072,38.095,0 13.07,38.101,0 13.069,38.114,0 13.067,38.123,0 13.057,38.133,0 13.055,38.142,0 13.09,38.166,0 13.084,38.174,0 13.09,38.183,0 13.102,38.19,0 13.113,38.193,0 13.123,38.191,0 13.158,38.179,0 13.18,38.176,0 13.208,38.176,0 13.231,38.184,0 13.239,38.207,0 13.255,38.202,0 13.267,38.205,0 13.278,38.21,0 13.297,38.214,0 13.311,38.219,0 13.319,38.22,0 13.324,38.218,0 13.326,38.211,0 13.327,38.205,0 13.329,38.2,0 13.367,38.179,0 13.372,38.173,0 13.374,38.14,0 13.377,38.131,0 13.392,38.103,0 13.514,38.11,0 13.542,38.094,0 13.54,38.077,0 13.542,38.067,0 13.548,38.056,0 13.558,38.049,0 13.588,38.039,0 13.623,38.015,0 13.652,38.001,0 13.698,37.993,0 13.712,37.988,0 13.708,37.985,0 13.708,37.984,0 13.706,37.98,0 13.727,37.981,0 13.791,37.973,0 13.813,37.978,0 13.858,37.996,0 13.899,38.004,0 13.913,38.012,0 13.925,38.022,0 13.939,38.029,0 14.008,38.038,0 14.021,38.049,0 14.063,38.03,0 14.084,38.024,0 14.107,38.021,0 14.122,38.022,0 14.152,38.029,0 14.274,38.015,0 14.332,38.018,0 14.385,38.029,0 14.433,38.049,0 14.465,38.037,0 14.512,38.044,0 14.635,38.081,0 14.668,38.099,0 14.696,38.121,0 14.734,38.157,0 14.745,38.161,0 14.778,38.159,0 14.799,38.16,0 14.875,38.175,0 14.889,38.182,0 14.898,38.186,0 14.908,38.187,0 14.936,38.186,0 14.945,38.182,0 14.963,38.163,0 14.97,38.159,0 14.982,38.158,0 15.008,38.152,0 15.04,38.153,0 15.049,38.152,0 15.054,38.148,0 15.064,38.135,0 15.069,38.131,0 15.088,38.128,0 15.106,38.133,0 15.123,38.141,0 15.178,38.156,0 15.204,38.183,0 15.241,38.241,0 15.238,38.249,0 15.237,38.251,0 15.237,38.253,0 15.241,38.261,0 15.238,38.265,0 15.244,38.265,0 15.247,38.254,0 15.241,38.23,0 15.246,38.217,0 15.258,38.21,0 15.275,38.207,0 15.292,38.207,0 15.322,38.211,0 15.4,38.232,0 15.423,38.244,0 15.434,38.253,0 15.473,38.268,0 15.513,38.297,0 15.529,38.302,0 15.56,38.3,0 15.616,38.28,0 15.652,38.275,0 15.649,38.266,0 15.643,38.262,0 14.999,38.371,0 14.987,38.364,0 14.964,38.381,0 14.949,38.396,0 14.946,38.412,0 14.96,38.433,0 14.967,38.433,0 14.967,38.418,0 14.983,38.412,0 14.994,38.403,0 15.002,38.391,0 15.008,38.378,0 14.999,38.371,0 14.967,38.453,0 14.949,38.451,0 14.935,38.458,0 14.922,38.469,0 14.908,38.474,0 14.9,38.481,0 14.901,38.498,0 14.91,38.515,0 14.925,38.522,0 14.958,38.522,0 14.967,38.516,0 14.96,38.502,0 14.966,38.497,0 14.975,38.49,0 14.98,38.487,0 14.98,38.481,0 14.953,38.481,0 14.958,38.469,0 14.962,38.465,0 14.967,38.461,0 14.967,38.453,0 14.361,38.539,0 14.346,38.535,0 14.343,38.547,0 14.357,38.551,0 14.361,38.539,0 14.864,38.549,0 14.862,38.539,0 14.824,38.552,0 14.794,38.571,0 14.815,38.584,0 14.852,38.585,0 14.867,38.581,0 14.877,38.569,0 14.873,38.565,0 14.869,38.56,0 14.864,38.549,0 14.585,38.557,0 14.574,38.557,0 14.552,38.562,0 14.544,38.575,0 14.543,38.587,0 14.546,38.588,0 14.564,38.585,0 14.576,38.577,0 14.58,38.566,0 14.585,38.561,0 14.585,38.557,0 13.177,38.693,0 13.165,38.691,0 13.153,38.695,0 13.153,38.702,0 13.158,38.71,0 13.169,38.717,0 13.186,38.718,0 13.196,38.711,0 13.197,38.708,0 13.177,38.693,0 15.225,38.777,0 15.217,38.773,0 15.206,38.775,0 15.187,38.789,0 15.187,38.793,0 15.194,38.798,0 15.204,38.802,0 15.209,38.806,0 15.212,38.81,0 15.219,38.812,0 15.228,38.81,0 15.235,38.808,0 15.239,38.804,0 15.237,38.796,0 15.232,38.789,0 15.23,38.783,0 15.225,38.777,0 8.361,39.118,0 8.386,39.105,0 8.418,39.106,0 8.445,39.102,0 8.457,39.073,0 8.459,39.068,0 8.464,39.065,0 8.47,39.065,0 8.477,39.07,0 8.478,39.07,0 8.48,39.072,0 8.484,39.07,0 8.465,39.056,0 8.46,39.05,0 8.464,39.042,0 8.455,39.028,0 8.447,38.994,0 8.438,38.967,0 8.433,38.963,0 8.422,38.96,0 8.41,38.962,0 8.407,38.967,0 8.406,38.974,0 8.402,38.981,0 8.365,39.029,0 8.35,39.062,0 8.354,39.083,0 8.354,39.091,0 8.347,39.091,0 8.347,39.097,0 8.361,39.118,0 8.306,39.104,0 8.291,39.099,0 8.27,39.1,0 8.255,39.107,0 8.258,39.118,0 8.258,39.124,0 8.233,39.144,0 8.225,39.157,0 8.231,39.173,0 8.246,39.181,0 8.291,39.188,0 8.306,39.193,0 8.307,39.161,0 8.313,39.12,0 8.306,39.104,0 13.959,40.712,0 13.945,40.701,0 13.935,40.705,0 13.92,40.704,0 13.904,40.7,0 13.891,40.694,0 13.882,40.699,0 13.86,40.707,0 13.85,40.715,0 13.857,40.735,0 13.862,40.744,0 13.871,40.749,0 13.868,40.752,0 13.863,40.762,0 13.884,40.762,0 13.947,40.745,0 13.966,40.735,0 13.963,40.729,0 13.963,40.723,0 13.966,40.715,0 13.959,40.712,0 13.427,40.791,0 13.415,40.786,0 13.419,40.796,0 13.424,40.8,0 13.432,40.801,0 13.427,40.791,0 8.333,41.105,0 8.343,41.098,0 8.345,41.086,0 8.342,41.074,0 8.333,41.064,0 8.275,41.057,0 8.252,41.043,0 8.252,41.016,0 8.247,40.993,0 8.21,40.996,0 8.218,41.005,0 8.222,41.014,0 8.224,41.024,0 8.224,41.033,0 8.229,41.042,0 8.242,41.052,0 8.261,41.064,0 8.276,41.07,0 8.278,41.081,0 8.276,41.095,0 8.278,41.105,0 8.285,41.107,0 8.303,41.105,0 8.306,41.109,0 8.309,41.114,0 8.314,41.118,0 8.327,41.126,0 8.326,41.118,0 8.328,41.112,0 8.333,41.105,0 9.471,41.19,0 9.474,41.184,0 9.475,41.179,0 9.47,41.172,0 9.464,41.173,0 9.456,41.181,0 9.449,41.186,0 9.442,41.183,0 9.437,41.186,0 9.448,41.205,0 9.443,41.211,0 9.446,41.22,0 9.454,41.234,0 9.46,41.242,0 9.468,41.241,0 9.475,41.236,0 9.478,41.228,0 9.48,41.224,0 9.479,41.217,0 9.471,41.19,0 9.239,41.249,0 9.247,41.248,0 9.258,41.249,0 9.269,41.236,0 9.268,41.202,0 9.279,41.195,0 9.275,41.199,0 9.274,41.205,0 9.275,41.212,0 9.279,41.221,0 9.286,41.221,0 9.29,41.209,0 9.289,41.205,0 9.286,41.201,0 9.286,41.195,0 9.3,41.196,0 9.306,41.198,0 9.313,41.201,0 9.317,41.196,0 9.334,41.187,0 9.336,41.211,0 9.353,41.207,0 9.389,41.181,0 9.389,41.187,0 9.397,41.184,0 9.405,41.181,0 9.413,41.181,0 9.423,41.181,0 9.423,41.174,0 9.417,41.171,0 9.415,41.168,0 9.413,41.164,0 9.409,41.16,0 9.421,41.156,0 9.427,41.149,0 9.433,41.14,0 9.443,41.133,0 9.438,41.125,0 9.437,41.115,0 9.443,41.092,0 9.455,41.112,0 9.461,41.12,0 9.471,41.126,0 9.467,41.13,0 9.466,41.134,0 9.463,41.137,0 9.457,41.14,0 9.47,41.146,0 9.482,41.145,0 9.495,41.142,0 9.509,41.14,0 9.514,41.143,0 9.519,41.148,0 9.524,41.15,0 9.533,41.14,0 9.525,41.133,0 9.535,41.128,0 9.541,41.123,0 9.547,41.121,0 9.553,41.126,0 9.56,41.126,0 9.562,41.122,0 9.562,41.121,0 9.564,41.121,0 9.567,41.119,0 9.566,41.107,0 9.563,41.097,0 9.557,41.088,0 9.546,41.077,0 9.544,41.082,0 9.541,41.087,0 9.54,41.092,0 9.522,41.031,0 9.512,41.016,0 9.533,41.016,0 9.525,41.03,0 9.544,41.037,0 9.555,41.034,0 9.558,41.025,0 9.553,41.009,0 9.558,41.009,0 9.559,41.011,0 9.559,41.013,0 9.56,41.016,0 9.566,41.011,0 9.569,41.009,0 9.574,41.009,0 9.589,41.02,0 9.616,41.019,0 9.645,41.011,0 9.663,41.002,0 9.652,40.991,0 9.637,40.992,0 9.62,40.999,0 9.605,41.002,0 9.588,40.996,0 9.583,40.98,0 9.579,40.962,0 9.567,40.948,0 9.572,40.935,0 9.558,40.931,0 9.512,40.934,0 9.512,40.929,0 9.513,40.928,0 9.505,40.927,0 9.512,40.915,0 9.521,40.915,0 9.53,40.919,0 9.54,40.92,0 9.55,40.917,0 9.568,40.908,0 9.574,40.906,0 9.593,40.91,0 9.608,40.918,0 9.623,40.924,0 9.643,40.92,0 9.638,40.911,0 9.632,40.905,0 9.624,40.9,0 9.615,40.899,0 9.615,40.893,0 9.651,40.879,0 9.656,40.876,0 9.658,40.864,0 9.664,40.858,0 9.672,40.859,0 9.684,40.865,0 9.69,40.856,0 9.7,40.85,0 9.712,40.847,0 9.725,40.845,0 9.691,40.836,0 9.682,40.829,0 9.69,40.817,0 9.69,40.811,0 9.675,40.814,0 9.662,40.809,0 9.658,40.8,0 9.669,40.79,0 9.67,40.801,0 9.676,40.788,0 9.705,40.759,0 9.711,40.745,0 9.715,40.727,0 9.745,40.68,0 9.749,40.667,0 9.754,40.605,0 9.757,40.595,0 9.762,40.587,0 9.769,40.584,0 9.782,40.582,0 9.786,40.576,0 9.787,40.567,0 9.793,40.557,0 9.821,40.536,0 9.827,40.529,0 9.827,40.519,0 9.816,40.502,0 9.813,40.492,0 9.809,40.471,0 9.801,40.455,0 9.779,40.427,0 9.762,40.39,0 9.75,40.377,0 9.728,40.372,0 9.713,40.366,0 9.701,40.353,0 9.684,40.324,0 9.671,40.312,0 9.646,40.296,0 9.635,40.282,0 9.627,40.263,0 9.625,40.248,0 9.629,40.205,0 9.632,40.196,0 9.655,40.144,0 9.666,40.131,0 9.68,40.126,0 9.688,40.12,0 9.711,40.096,0 9.733,40.084,0 9.731,40.068,0 9.694,39.993,0 9.688,39.961,0 9.697,39.934,0 9.703,39.937,0 9.71,39.94,0 9.716,39.94,0 9.718,39.934,0 9.715,39.924,0 9.709,39.922,0 9.702,39.922,0 9.697,39.919,0 9.69,39.906,0 9.685,39.894,0 9.684,39.882,0 9.69,39.871,0 9.684,39.871,0 9.684,39.865,0 9.688,39.863,0 9.693,39.86,0 9.697,39.858,0 9.697,39.852,0 9.685,39.84,0 9.676,39.819,0 9.671,39.793,0 9.669,39.769,0 9.67,39.756,0 9.676,39.732,0 9.677,39.718,0 9.675,39.708,0 9.665,39.691,0 9.663,39.677,0 9.661,39.67,0 9.656,39.663,0 9.652,39.652,0 9.65,39.639,0 9.656,39.594,0 9.654,39.567,0 9.629,39.502,0 9.645,39.484,0 9.64,39.452,0 9.615,39.399,0 9.603,39.355,0 9.601,39.341,0 9.604,39.326,0 9.612,39.316,0 9.635,39.303,0 9.635,39.297,0 9.608,39.289,0 9.582,39.266,0 9.568,39.238,0 9.574,39.214,0 9.566,39.205,0 9.569,39.199,0 9.577,39.194,0 9.581,39.187,0 9.578,39.179,0 9.569,39.159,0 9.567,39.149,0 9.558,39.139,0 9.54,39.134,0 9.523,39.125,0 9.519,39.104,0 9.511,39.108,0 9.508,39.111,0 9.508,39.116,0 9.512,39.124,0 9.497,39.133,0 9.481,39.135,0 9.466,39.132,0 9.451,39.124,0 9.443,39.124,0 9.439,39.133,0 9.429,39.138,0 9.409,39.146,0 9.384,39.169,0 9.378,39.173,0 9.368,39.177,0 9.346,39.196,0 9.337,39.201,0 9.327,39.203,0 9.313,39.208,0 9.3,39.214,0 9.293,39.221,0 9.286,39.214,0 9.272,39.22,0 9.253,39.225,0 9.217,39.228,0 9.198,39.221,0 9.182,39.207,0 9.17,39.193,0 9.167,39.187,0 9.137,39.194,0 9.114,39.211,0 9.073,39.248,0 9.064,39.243,0 9.056,39.247,0 9.048,39.256,0 9.039,39.262,0 9.025,39.265,0 9.015,39.264,0 9.013,39.26,0 9.026,39.256,0 9.026,39.248,0 9.022,39.24,0 9.027,39.236,0 9.036,39.232,0 9.038,39.227,0 9.039,39.228,0 9.051,39.225,0 9.075,39.23,0 9.08,39.224,0 9.08,39.216,0 9.08,39.212,0 9.039,39.179,0 9.027,39.165,0 9.019,39.146,0 9.017,39.124,0 9.019,39.104,0 9.025,39.086,0 9.033,39.07,0 9.038,39.063,0 9.044,39.058,0 9.046,39.051,0 9.03,39.03,0 9.019,38.995,0 9.026,38.995,0 9.016,38.989,0 9.013,38.99,0 9.005,38.995,0 8.997,38.983,0 8.895,38.902,0 8.889,38.9,0 8.878,38.899,0 8.873,38.896,0 8.862,38.882,0 8.854,38.878,0 8.842,38.88,0 8.828,38.889,0 8.806,38.906,0 8.806,38.885,0 8.791,38.904,0 8.767,38.92,0 8.74,38.93,0 8.717,38.932,0 8.695,38.925,0 8.669,38.91,0 8.652,38.891,0 8.656,38.871,0 8.641,38.864,0 8.635,38.871,0 8.643,38.89,0 8.634,38.895,0 8.616,38.896,0 8.6,38.899,0 8.6,38.906,0 8.616,38.923,0 8.616,38.947,0 8.604,38.965,0 8.581,38.96,0 8.573,39.013,0 8.56,39.057,0 8.553,39.057,0 8.545,39.051,0 8.521,39.061,0 8.505,39.063,0 8.51,39.068,0 8.519,39.083,0 8.505,39.091,0 8.483,39.08,0 8.483,39.084,0 8.478,39.09,0 8.474,39.107,0 8.466,39.119,0 8.455,39.125,0 8.443,39.118,0 8.439,39.128,0 8.439,39.153,0 8.436,39.166,0 8.429,39.173,0 8.419,39.177,0 8.413,39.175,0 8.416,39.166,0 8.41,39.169,0 8.406,39.174,0 8.403,39.181,0 8.402,39.19,0 8.399,39.201,0 8.393,39.204,0 8.386,39.204,0 8.381,39.207,0 8.373,39.222,0 8.372,39.23,0 8.377,39.238,0 8.427,39.283,0 8.433,39.302,0 8.416,39.323,0 8.418,39.339,0 8.383,39.359,0 8.375,39.379,0 8.379,39.388,0 8.396,39.404,0 8.402,39.412,0 8.406,39.427,0 8.404,39.436,0 8.39,39.462,0 8.387,39.465,0 8.387,39.47,0 8.395,39.481,0 8.422,39.508,0 8.436,39.525,0 8.452,39.558,0 8.464,39.577,0 8.457,39.584,0 8.465,39.598,0 8.463,39.617,0 8.45,39.659,0 8.447,39.704,0 8.443,39.714,0 8.443,39.721,0 8.447,39.731,0 8.445,39.757,0 8.447,39.762,0 8.46,39.76,0 8.469,39.755,0 8.5,39.716,0 8.518,39.702,0 8.539,39.696,0 8.566,39.701,0 8.515,39.713,0 8.505,39.721,0 8.507,39.738,0 8.521,39.755,0 8.536,39.771,0 8.546,39.783,0 8.539,39.783,0 8.536,39.776,0 8.531,39.77,0 8.525,39.766,0 8.519,39.762,0 8.53,39.772,0 8.541,39.789,0 8.549,39.807,0 8.553,39.821,0 8.556,39.852,0 8.554,39.864,0 8.546,39.878,0 8.524,39.899,0 8.495,39.912,0 8.464,39.914,0 8.436,39.899,0 8.443,39.893,0 8.446,39.898,0 8.45,39.899,0 8.456,39.898,0 8.464,39.899,0 8.452,39.893,0 8.445,39.883,0 8.436,39.858,0 8.429,39.865,0 8.438,39.877,0 8.432,39.885,0 8.419,39.892,0 8.404,39.903,0 8.401,39.903,0 8.399,39.905,0 8.395,39.912,0 8.394,39.92,0 8.397,39.927,0 8.4,39.933,0 8.402,39.94,0 8.394,39.977,0 8.395,39.988,0 8.407,40.01,0 8.408,40.022,0 8.395,40.036,0 8.381,40.03,0 8.378,40.033,0 8.385,40.042,0 8.402,40.05,0 8.405,40.049,0 8.435,40.051,0 8.453,40.056,0 8.46,40.057,0 8.469,40.062,0 8.48,40.074,0 8.488,40.089,0 8.491,40.104,0 8.486,40.118,0 8.468,40.144,0 8.464,40.163,0 8.46,40.216,0 8.477,40.262,0 8.477,40.292,0 8.463,40.314,0 8.442,40.331,0 8.416,40.345,0 8.409,40.338,0 8.387,40.352,0 8.384,40.372,0 8.395,40.424,0 8.391,40.442,0 8.38,40.468,0 8.366,40.492,0 8.35,40.502,0 8.332,40.51,0 8.324,40.531,0 8.32,40.555,0 8.313,40.578,0 8.292,40.595,0 8.268,40.594,0 8.217,40.57,0 8.196,40.578,0 8.206,40.598,0 8.217,40.612,0 8.194,40.617,0 8.177,40.606,0 8.167,40.586,0 8.162,40.564,0 8.154,40.578,0 8.148,40.593,0 8.141,40.619,0 8.141,40.625,0 8.158,40.632,0 8.174,40.641,0 8.186,40.656,0 8.189,40.68,0 8.192,40.68,0 8.196,40.685,0 8.198,40.691,0 8.193,40.694,0 8.18,40.695,0 8.174,40.697,0 8.168,40.701,0 8.154,40.719,0 8.146,40.726,0 8.134,40.729,0 8.21,40.865,0 8.216,40.881,0 8.217,40.899,0 8.21,40.914,0 8.193,40.92,0 8.179,40.928,0 8.183,40.945,0 8.194,40.963,0 8.203,40.975,0 8.21,40.975,0 8.213,40.963,0 8.221,40.962,0 8.229,40.962,0 8.237,40.955,0 8.236,40.946,0 8.232,40.934,0 8.23,40.921,0 8.234,40.91,0 8.278,40.865,0 8.311,40.85,0 8.422,40.839,0 8.478,40.826,0 8.501,40.824,0 8.521,40.827,0 8.599,40.853,0 8.619,40.866,0 8.635,40.881,0 8.641,40.896,0 8.71,40.92,0 8.734,40.921,0 8.752,40.919,0 8.765,40.914,0 8.823,40.947,0 8.84,40.961,0 8.876,41.008,0 8.889,41.016,0 8.887,41.02,0 8.887,41.021,0 8.886,41.022,0 8.882,41.023,0 8.914,41.032,0 8.923,41.037,0 8.93,41.043,0 8.941,41.061,0 8.947,41.064,0 8.959,41.07,0 8.976,41.082,0 8.991,41.097,0 9.006,41.122,0 9.025,41.129,0 9.094,41.135,0 9.108,41.139,0 9.136,41.16,0 9.142,41.153,0 9.158,41.169,0 9.164,41.184,0 9.163,41.225,0 9.172,41.243,0 9.191,41.251,0 9.213,41.256,0 9.231,41.262,0 9.233,41.253,0 9.239,41.249,0 9.435,41.217,0 9.395,41.211,0 9.377,41.213,0 9.373,41.222,0 9.373,41.23,0 9.378,41.234,0 9.385,41.237,0 9.392,41.241,0 9.396,41.248,0 9.398,41.256,0 9.402,41.258,0 9.408,41.258,0 9.414,41.262,0 9.422,41.261,0 9.427,41.254,0 9.431,41.246,0 9.43,41.238,0 9.429,41.229,0 9.431,41.225,0 9.434,41.221,0 9.435,41.217,0 10.316,42.341,0 10.313,42.324,0 10.294,42.328,0 10.297,42.345,0 10.306,42.352,0 10.316,42.341,0 10.922,42.334,0 10.909,42.325,0 10.874,42.36,0 10.862,42.366,0 10.871,42.376,0 10.877,42.387,0 10.884,42.392,0 10.896,42.386,0 10.907,42.378,0 10.919,42.356,0 10.931,42.346,0 10.926,42.339,0 10.922,42.334,0 10.095,42.577,0 10.086,42.572,0 10.072,42.573,0 10.059,42.576,0 10.05,42.582,0 10.053,42.589,0 10.063,42.592,0 10.073,42.6,0 10.08,42.614,0 10.084,42.615,0 10.088,42.604,0 10.092,42.596,0 10.096,42.591,0 10.098,42.588,0 10.098,42.584,0 10.095,42.577,0 10.431,42.816,0 10.437,42.804,0 10.431,42.787,0 10.421,42.776,0 10.407,42.769,0 10.389,42.763,0 10.408,42.757,0 10.426,42.741,0 10.431,42.722,0 10.416,42.709,0 10.411,42.718,0 10.404,42.719,0 10.394,42.718,0 10.382,42.722,0 10.378,42.728,0 10.368,42.746,0 10.365,42.75,0 10.352,42.755,0 10.338,42.765,0 10.326,42.765,0 10.314,42.743,0 10.305,42.76,0 10.266,42.744,0 10.246,42.757,0 10.241,42.742,0 10.236,42.736,0 10.23,42.735,0 10.148,42.737,0 10.125,42.743,0 10.107,42.757,0 10.102,42.784,0 10.112,42.801,0 10.134,42.812,0 10.159,42.817,0 10.18,42.819,0 10.19,42.817,0 10.213,42.808,0 10.225,42.804,0 10.243,42.803,0 10.266,42.804,0 10.266,42.809,0 10.265,42.81,0 10.263,42.81,0 10.26,42.812,0 10.273,42.819,0 10.273,42.826,0 10.273,42.827,0 10.29,42.825,0 10.327,42.826,0 10.323,42.811,0 10.333,42.806,0 10.348,42.806,0 10.355,42.808,0 10.359,42.817,0 10.366,42.823,0 10.375,42.827,0 10.382,42.832,0 10.393,42.858,0 10.401,42.869,0 10.413,42.873,0 10.422,42.871,0 10.432,42.864,0 10.439,42.855,0 10.444,42.845,0 10.437,42.838,0 10.432,42.828,0 10.431,42.816,0 9.844,43.06,0 9.848,43.058,0 9.854,43.059,0 9.843,43.035,0 9.828,43.019,0 9.81,43.017,0 9.793,43.037,0 9.812,43.071,0 9.827,43.081,0 9.841,43.065,0 9.842,43.063,0 9.844,43.06,0 12.122,46.972,0 12.128,46.949,0 12.135,46.937,0 12.142,46.928,0 12.142,46.919,0 12.127,46.909,0 12.137,46.906,0 12.161,46.903,0 12.172,46.899,0 12.184,46.891,0 12.189,46.885,0 12.195,46.88,0 12.209,46.877,0 12.251,46.876,0 12.267,46.868,0 12.276,46.846,0 12.276,46.834,0 12.273,46.827,0 12.27,46.82,0 12.267,46.808,0 12.267,46.795,0 12.269,46.789,0 12.275,46.785,0 12.284,46.78,0 12.305,46.774,0 12.326,46.772,0 12.343,46.765,0 12.351,46.743,0 12.37,46.711,0 12.405,46.69,0 12.446,46.679,0 12.5,46.672,0 12.531,46.658,0 12.547,46.652,0 12.562,46.651,0 12.62,46.656,0 12.67,46.653,0 12.679,46.65,0 12.697,46.641,0 12.707,46.638,0 12.716,46.638,0 12.732,46.642,0 12.74,46.643,0 12.774,46.635,0 12.83,46.61,0 13.065,46.598,0 13.146,46.585,0 13.21,46.558,0 13.231,46.552,0 13.271,46.551,0 13.373,46.566,0 13.417,46.56,0 13.478,46.564,0 13.485,46.562,0 13.499,46.551,0 13.507,46.547,0 13.549,46.546,0 13.67,46.519,0 13.685,46.518,0 13.701,46.52,0 13.701,46.512,0 13.699,46.505,0 13.695,46.499,0 13.69,46.493,0 13.688,46.468,0 13.677,46.452,0 13.659,46.445,0 13.634,46.446,0 13.6,46.443,0 13.576,46.427,0 13.554,46.406,0 13.53,46.388,0 13.484,46.371,0 13.46,46.359,0 13.447,46.355,0 13.434,46.354,0 13.423,46.345,0 13.41,46.324,0 13.391,46.302,0 13.365,46.29,0 13.373,46.28,0 13.379,46.268,0 13.385,46.243,0 13.385,46.243,0 13.385,46.243,0 13.398,46.231,0 13.402,46.217,0 13.41,46.208,0 13.437,46.211,0 13.423,46.229,0 13.438,46.225,0 13.468,46.223,0 13.482,46.218,0 13.51,46.214,0 13.529,46.205,0 13.559,46.184,0 13.584,46.181,0 13.614,46.184,0 13.637,46.18,0 13.645,46.162,0 13.616,46.125,0 13.505,46.066,0 13.482,46.045,0 13.49,46.039,0 13.493,46.032,0 13.49,46.026,0 13.482,46.018,0 13.477,46.016,0 13.462,46.006,0 13.475,45.996,0 13.479,45.993,0 13.48,45.992,0 13.481,45.991,0 13.482,45.99,0 13.482,45.989,0 13.509,45.967,0 13.539,45.969,0 13.572,45.98,0 13.606,45.985,0 13.623,45.966,0 13.608,45.927,0 13.569,45.865,0 13.566,45.83,0 13.581,45.809,0 13.609,45.799,0 13.644,45.796,0 13.66,45.792,0 13.709,45.765,0 13.779,45.743,0 13.858,45.649,0 13.869,45.641,0 13.884,45.635,0 13.893,45.635,0 13.895,45.632,0 13.887,45.619,0 13.848,45.585,0 13.801,45.581,0 13.761,45.596,0 13.712,45.593,0 13.719,45.6,0 13.731,45.613,0 13.757,45.613,0 13.787,45.611,0 13.809,45.614,0 13.796,45.617,0 13.787,45.624,0 13.778,45.635,0 13.74,45.649,0 13.758,45.655,0 13.754,45.672,0 13.74,45.691,0 13.727,45.703,0 13.648,45.762,0 13.63,45.772,0 13.575,45.789,0 13.552,45.792,0 13.535,45.782,0 13.525,45.76,0 13.529,45.74,0 13.555,45.737,0 13.519,45.725,0 13.514,45.721,0 13.508,45.714,0 13.481,45.71,0 13.47,45.707,0 13.452,45.694,0 13.429,45.681,0 13.402,45.675,0 13.377,45.683,0 13.392,45.686,0 13.41,45.691,0 13.425,45.698,0 13.432,45.707,0 13.423,45.724,0 13.382,45.73,0 13.37,45.744,0 13.352,45.74,0 13.255,45.756,0 13.246,45.759,0 13.222,45.776,0 13.216,45.779,0 13.206,45.778,0 13.17,45.768,0 13.158,45.754,0 13.15,45.751,0 13.14,45.755,0 13.132,45.769,0 13.12,45.772,0 13.111,45.767,0 13.109,45.758,0 13.112,45.749,0 13.124,45.744,0 13.124,45.737,0 13.101,45.736,0 13.081,45.727,0 13.07,45.713,0 13.076,45.697,0 13.092,45.689,0 13.112,45.691,0 13.15,45.703,0 13.139,45.689,0 13.104,45.669,0 13.096,45.652,0 13.086,45.642,0 13.061,45.636,0 12.982,45.635,0 12.944,45.628,0 12.781,45.553,0 12.612,45.496,0 12.513,45.47,0 12.497,45.46,0 12.488,45.456,0 12.452,45.45,0 12.424,45.438,0 12.411,45.436,0 12.419,45.451,0 12.43,45.464,0 12.436,45.475,0 12.431,45.484,0 12.441,45.483,0 12.448,45.484,0 12.452,45.489,0 12.452,45.498,0 12.459,45.498,0 12.463,45.489,0 12.468,45.485,0 12.472,45.486,0 12.479,45.491,0 12.466,45.504,0 12.477,45.503,0 12.488,45.504,0 12.498,45.506,0 12.5,45.504,0 12.501,45.506,0 12.504,45.503,0 12.507,45.499,0 12.507,45.498,0 12.504,45.498,0 12.493,45.498,0 12.493,45.491,0 12.516,45.492,0 12.521,45.505,0 12.522,45.519,0 12.531,45.525,0 12.549,45.527,0 12.563,45.531,0 12.574,45.54,0 12.582,45.553,0 12.57,45.549,0 12.545,45.536,0 12.538,45.536,0 12.519,45.55,0 12.511,45.559,0 12.507,45.573,0 12.486,45.565,0 12.459,45.548,0 12.443,45.53,0 12.452,45.518,0 12.452,45.512,0 12.435,45.512,0 12.418,45.523,0 12.411,45.518,0 12.404,45.518,0 12.397,45.539,0 12.385,45.523,0 12.391,45.514,0 12.425,45.504,0 12.425,45.498,0 12.412,45.493,0 12.394,45.491,0 12.381,45.494,0 12.384,45.504,0 12.351,45.505,0 12.31,45.489,0 12.273,45.463,0 12.253,45.436,0 12.253,45.43,0 12.259,45.43,0 12.251,45.42,0 12.247,45.411,0 12.249,45.402,0 12.259,45.395,0 12.25,45.385,0 12.248,45.378,0 12.249,45.371,0 12.246,45.361,0 12.238,45.358,0 12.229,45.357,0 12.224,45.354,0 12.233,45.34,0 12.221,45.327,0 12.217,45.316,0 12.209,45.309,0 12.188,45.306,0 12.175,45.31,0 12.164,45.316,0 12.155,45.313,0 12.15,45.292,0 12.16,45.283,0 12.169,45.262,0 12.181,45.258,0 12.192,45.263,0 12.2,45.274,0 12.203,45.288,0 12.198,45.299,0 12.218,45.294,0 12.222,45.283,0 12.221,45.269,0 12.225,45.251,0 12.214,45.248,0 12.212,45.243,0 12.216,45.237,0 12.225,45.23,0 12.222,45.216,0 12.231,45.204,0 12.248,45.197,0 12.267,45.196,0 12.264,45.2,0 12.263,45.201,0 12.259,45.203,0 12.274,45.211,0 12.296,45.226,0 12.308,45.23,0 12.299,45.215,0 12.305,45.201,0 12.316,45.186,0 12.322,45.172,0 12.322,45.139,0 12.329,45.101,0 12.319,45.103,0 12.308,45.108,0 12.309,45.114,0 12.308,45.124,0 12.308,45.128,0 12.298,45.106,0 12.297,45.088,0 12.307,45.078,0 12.329,45.08,0 12.326,45.083,0 12.324,45.086,0 12.322,45.093,0 12.341,45.081,0 12.354,45.067,0 12.364,45.052,0 12.377,45.039,0 12.377,45.032,0 12.369,45.031,0 12.365,45.029,0 12.361,45.027,0 12.356,45.024,0 12.369,45.011,0 12.384,45.026,0 12.387,45.039,0 12.381,45.051,0 12.369,45.065,0 12.384,45.056,0 12.402,45.05,0 12.414,45.043,0 12.411,45.032,0 12.427,45.02,0 12.435,45.015,0 12.445,45.011,0 12.465,44.992,0 12.487,44.976,0 12.5,44.983,0 12.497,44.984,0 12.49,44.983,0 12.487,44.983,0 12.487,44.991,0 12.503,44.991,0 12.517,44.987,0 12.528,44.98,0 12.535,44.97,0 12.534,44.961,0 12.524,44.95,0 12.528,44.943,0 12.519,44.934,0 12.516,44.928,0 12.513,44.922,0 12.507,44.922,0 12.5,44.921,0 12.495,44.91,0 12.493,44.878,0 12.488,44.862,0 12.475,44.845,0 12.445,44.82,0 12.444,44.825,0 12.439,44.835,0 12.433,44.846,0 12.425,44.854,0 12.44,44.877,0 12.444,44.89,0 12.439,44.901,0 12.427,44.905,0 12.416,44.9,0 12.407,44.891,0 12.404,44.884,0 12.393,44.868,0 12.392,44.859,0 12.417,44.851,0 12.416,44.843,0 12.409,44.836,0 12.397,44.833,0 12.397,44.826,0 12.404,44.825,0 12.417,44.821,0 12.425,44.82,0 12.417,44.803,0 12.398,44.794,0 12.376,44.792,0 12.358,44.804,0 12.347,44.815,0 12.322,44.833,0 12.304,44.843,0 12.293,44.843,0 12.267,44.826,0 12.267,44.82,0 12.281,44.82,0 12.254,44.751,0 12.247,44.711,0 12.253,44.668,0 12.266,44.636,0 12.276,44.62,0 12.284,44.614,0 12.286,44.602,0 12.281,44.532,0 12.284,44.487,0 12.315,44.387,0 12.319,44.361,0 12.322,44.353,0 12.326,44.348,0 12.34,44.334,0 12.343,44.329,0 12.345,44.308,0 12.351,44.288,0 12.369,44.25,0 12.391,44.222,0 12.418,44.195,0 12.459,44.166,0 12.479,44.139,0 12.511,44.114,0 12.548,44.093,0 12.575,44.085,0 12.632,44.03,0 12.662,44.008,0 12.692,43.99,0 12.711,43.983,0 12.757,43.972,0 12.804,43.967,0 12.823,43.958,0 12.863,43.935,0 12.929,43.916,0 12.939,43.904,0 12.948,43.897,0 13.254,43.703,0 13.371,43.65,0 13.39,43.644,0 13.4,43.635,0 13.447,43.623,0 13.474,43.612,0 13.484,43.616,0 13.491,43.623,0 13.497,43.627,0 13.5,43.628,0 13.502,43.63,0 13.505,43.633,0 13.511,43.633,0 13.517,43.631,0 13.52,43.627,0 13.522,43.622,0 13.525,43.62,0 13.544,43.613,0 13.558,43.596,0 13.57,43.58,0 13.579,43.573,0 13.599,43.569,0 13.616,43.56,0 13.625,43.547,0 13.618,43.531,0 13.761,43.264,0 13.777,43.243,0 13.781,43.236,0 13.787,43.2,0 13.791,43.192,0 13.803,43.178,0 13.835,43.127,0 13.849,43.092,0 13.866,43.007,0 13.945,42.798,0 13.981,42.73,0 14.002,42.698,0 14.064,42.625,0 14.069,42.609,0 14.076,42.599,0 14.221,42.47,0 14.285,42.428,0 14.357,42.393,0 14.388,42.373,0 14.43,42.321,0 14.561,42.225,0 14.596,42.208,0 14.654,42.191,0 14.694,42.185,0 14.71,42.175,0 14.718,42.16,0 14.723,42.119,0 14.73,42.099,0 14.741,42.084,0 14.758,42.079,0 14.781,42.075,0 14.8,42.066,0 14.836,42.044,0 14.871,42.032,0 14.953,42.021,0 14.994,42.01,0 15.008,42.001,0 15.035,41.974,0 15.046,41.969,0 15.064,41.964,0 15.105,41.942,0 15.124,41.934,0 15.166,41.927,0 15.282,41.928,0 15.401,41.908,0 15.447,41.907,0 15.612,41.928,0 15.775,41.921,0 16.028,41.944,0 16.112,41.928,0 16.112,41.926,0 16.141,41.92,0 16.161,41.892,0 16.18,41.893,0 16.177,41.877,0 16.184,41.858,0 16.193,41.821,0 16.194,41.808,0 16.193,41.791,0 16.185,41.779,0 16.167,41.763,0 16.146,41.749,0 16.128,41.742,0 16.108,41.737,0 16.09,41.726,0 16.064,41.701,0 16.028,41.68,0 15.926,41.64,0 15.901,41.614,0 15.892,41.577,0 15.897,41.536,0 15.912,41.503,0 15.934,41.479,0 15.962,41.459,0 16.022,41.428,0 16.086,41.412,0 16.101,41.403,0 16.115,41.393,0 16.302,41.328,0 16.461,41.262,0 16.521,41.25,0 16.539,41.239,0 16.555,41.227,0 16.594,41.207,0 16.831,41.146,0 16.852,41.133,0 16.859,41.133,0 16.859,41.14,0 16.865,41.14,0 16.886,41.124,0 17.058,41.082,0 17.204,41.021,0 17.277,40.98,0 17.311,40.955,0 17.348,40.912,0 17.362,40.906,0 17.378,40.902,0 17.414,40.881,0 17.476,40.83,0 17.493,40.824,0 17.513,40.82,0 17.549,40.802,0 17.635,40.785,0 17.646,40.78,0 17.749,40.747,0 17.844,40.694,0 17.922,40.683,0 17.956,40.67,0 17.956,40.647,0 17.967,40.647,0 17.993,40.653,0 18.008,40.65,0 18.012,40.644,0 18.012,40.635,0 18.016,40.625,0 18.04,40.608,0 18.044,40.602,0 18.038,40.557,0 18.12,40.504,0 18.212,40.464,0 18.232,40.461,0 18.239,40.457,0 18.259,40.43,0 18.271,40.421,0 18.304,40.4,0 18.33,40.366,0 18.344,40.351,0 18.362,40.345,0 18.371,40.338,0 18.438,40.268,0 18.501,40.152,0 18.505,40.146,0 18.51,40.142,0 18.517,40.139,0 18.512,40.127,0 18.514,40.12,0 18.518,40.114,0 18.517,40.104,0 18.509,40.094,0 18.492,40.084,0 18.484,40.055,0 18.471,40.043,0 18.435,40.022,0 18.412,39.979,0 18.408,39.968,0 18.405,39.947,0 18.395,39.925,0 18.393,39.916,0 18.4,39.89,0 18.401,39.878,0 18.387,39.825,0 18.39,39.817,0 18.384,39.814,0 18.374,39.8,0 18.369,39.796,0 18.347,39.798,0 18.339,39.8,0 18.331,39.803,0 18.283,39.833,0 18.266,39.837,0 18.225,39.837,0 18.212,39.839,0 18.187,39.852,0 18.162,39.86,0 18.131,39.883,0 18.095,39.903,0 18.082,39.906,0 18.072,39.911,0 18.008,39.986,0 17.996,39.995,0 17.996,40.002,0 18.012,40.003,0 18.021,40.01,0 18.023,40.021,0 18.016,40.036,0 18.006,40.045,0 17.979,40.051,0 17.968,40.057,0 18.003,40.074,0 18.012,40.096,0 17.998,40.12,0 17.968,40.146,0 17.941,40.163,0 17.927,40.176,0 17.92,40.191,0 17.92,40.21,0 17.917,40.227,0 17.912,40.24,0 17.9,40.249,0 17.913,40.249,0 17.913,40.255,0 17.864,40.285,0 17.848,40.29,0 17.513,40.303,0 17.494,40.307,0 17.441,40.331,0 17.431,40.331,0 17.41,40.33,0 17.4,40.331,0 17.393,40.335,0 17.375,40.348,0 17.369,40.351,0 17.352,40.355,0 17.297,40.379,0 17.241,40.395,0 17.213,40.406,0 17.201,40.42,0 17.224,40.428,0 17.244,40.441,0 17.248,40.457,0 17.228,40.474,0 17.248,40.48,0 17.296,40.473,0 17.317,40.482,0 17.324,40.498,0 17.305,40.499,0 17.262,40.488,0 17.264,40.491,0 17.269,40.496,0 17.248,40.503,0 17.23,40.497,0 17.211,40.487,0 17.191,40.482,0 17.182,40.485,0 17.177,40.493,0 17.172,40.502,0 17.167,40.509,0 17.157,40.512,0 17.134,40.512,0 17.125,40.515,0 17.05,40.519,0 16.977,40.492,0 16.913,40.445,0 16.783,40.301,0 16.762,40.269,0 16.738,40.211,0 16.731,40.2,0 16.716,40.193,0 16.68,40.146,0 16.625,40.108,0 16.605,40.084,0 16.597,40.046,0 16.6,40.034,0 16.614,39.996,0 16.632,39.966,0 16.622,39.953,0 16.606,39.943,0 16.59,39.92,0 16.543,39.885,0 16.509,39.837,0 16.492,39.805,0 16.49,39.775,0 16.503,39.747,0 16.529,39.721,0 16.529,39.714,0 16.516,39.689,0 16.546,39.661,0 16.592,39.636,0 16.625,39.625,0 16.75,39.62,0 16.783,39.611,0 16.799,39.603,0 16.817,39.591,0 16.831,39.576,0 16.838,39.56,0 16.847,39.552,0 16.906,39.529,0 16.954,39.499,0 16.971,39.495,0 16.996,39.492,0 17.012,39.486,0 17.024,39.475,0 17.036,39.461,0 17.058,39.441,0 17.089,39.422,0 17.125,39.409,0 17.159,39.406,0 17.123,39.338,0 17.115,39.283,0 17.115,39.269,0 17.118,39.256,0 17.125,39.244,0 17.143,39.222,0 17.146,39.21,0 17.141,39.179,0 17.123,39.121,0 17.125,39.091,0 17.148,39.054,0 17.152,39.046,0 17.159,39.04,0 17.193,39.031,0 17.207,39.029,0 17.187,39.019,0 17.177,39.012,0 17.173,39.005,0 17.172,38.966,0 17.173,38.96,0 17.139,38.936,0 17.136,38.932,0 17.128,38.929,0 17.119,38.919,0 17.105,38.899,0 17.096,38.919,0 17.071,38.923,0 17.043,38.916,0 17.023,38.906,0 16.997,38.929,0 16.982,38.937,0 16.958,38.94,0 16.936,38.938,0 16.839,38.918,0 16.728,38.879,0 16.688,38.856,0 16.68,38.847,0 16.671,38.84,0 16.611,38.816,0 16.586,38.798,0 16.575,38.785,0 16.564,38.756,0 16.551,38.741,0 16.539,38.723,0 16.535,38.7,0 16.547,38.693,0 16.55,38.69,0 16.549,38.672,0 16.559,38.596,0 16.578,38.528,0 16.578,38.503,0 16.57,38.429,0 16.562,38.416,0 16.523,38.387,0 16.509,38.371,0 16.498,38.369,0 16.468,38.348,0 16.436,38.34,0 16.34,38.301,0 16.307,38.277,0 16.17,38.143,0 16.152,38.111,0 16.126,38.005,0 16.112,37.973,0 16.102,37.96,0 16.091,37.949,0 16.078,37.94,0 16.064,37.932,0 16.016,37.924,0 16.002,37.919,0 15.943,37.933,0 15.762,37.925,0 15.736,37.931,0 15.709,37.941,0 15.685,37.953,0 15.666,37.967,0 15.646,37.988,0 15.636,38.009,0 15.639,38.027,0 15.659,38.042,0 15.633,38.074,0 15.625,38.092,0 15.628,38.107,0 15.642,38.126,0 15.648,38.143,0 15.647,38.162,0 15.639,38.186,0 15.633,38.22,0 15.651,38.241,0 15.685,38.253,0 15.787,38.278,0 15.796,38.285,0 15.799,38.291,0 15.813,38.3,0 15.817,38.306,0 15.83,38.351,0 15.905,38.474,0 15.918,38.517,0 15.916,38.55,0 15.901,38.578,0 15.871,38.604,0 15.864,38.608,0 15.851,38.613,0 15.845,38.618,0 15.836,38.628,0 15.834,38.634,0 15.836,38.639,0 15.837,38.649,0 15.845,38.66,0 15.864,38.668,0 15.905,38.679,0 15.969,38.712,0 16.003,38.725,0 16.049,38.728,0 16.121,38.721,0 16.137,38.724,0 16.153,38.731,0 16.18,38.748,0 16.201,38.776,0 16.216,38.814,0 16.222,38.856,0 16.221,38.899,0 16.215,38.919,0 16.205,38.934,0 16.19,38.943,0 16.169,38.947,0 16.155,38.955,0 16.14,38.974,0 16.084,39.075,0 16.043,39.31,0 16.032,39.345,0 15.955,39.489,0 15.934,39.513,0 15.905,39.536,0 15.877,39.551,0 15.868,39.564,0 15.865,39.588,0 15.851,39.615,0 15.837,39.652,0 15.816,39.679,0 15.807,39.695,0 15.789,39.796,0 15.789,39.79,0 15.784,39.81,0 15.779,39.82,0 15.772,39.824,0 15.77,39.83,0 15.783,39.868,0 15.775,39.891,0 15.742,39.929,0 15.735,39.943,0 15.729,39.964,0 15.714,39.981,0 15.679,40.009,0 15.652,40.043,0 15.631,40.057,0 15.625,40.065,0 15.625,40.078,0 15.611,40.073,0 15.536,40.078,0 15.51,40.07,0 15.493,40.059,0 15.46,40.029,0 15.425,40.004,0 15.405,39.999,0 15.377,40.002,0 15.354,40.012,0 15.315,40.034,0 15.303,40.036,0 15.294,40.032,0 15.284,40.03,0 15.273,40.028,0 15.262,40.029,0 15.262,40.036,0 15.28,40.047,0 15.264,40.074,0 15.234,40.1,0 15.21,40.112,0 15.191,40.119,0 15.128,40.169,0 15.113,40.175,0 15.096,40.173,0 15.066,40.166,0 15.048,40.169,0 15.035,40.175,0 15.015,40.194,0 14.974,40.223,0 14.967,40.224,0 14.959,40.231,0 14.923,40.238,0 14.912,40.241,0 14.907,40.258,0 14.932,40.285,0 14.94,40.307,0 14.933,40.324,0 14.933,40.334,0 14.943,40.338,0 14.954,40.34,0 14.965,40.345,0 14.973,40.352,0 14.98,40.359,0 14.99,40.394,0 14.976,40.431,0 14.889,40.573,0 14.862,40.607,0 14.836,40.632,0 14.81,40.653,0 14.783,40.67,0 14.753,40.676,0 14.72,40.667,0 14.691,40.649,0 14.679,40.646,0 14.626,40.649,0 14.614,40.646,0 14.572,40.617,0 14.545,40.613,0 14.517,40.62,0 14.487,40.632,0 14.472,40.624,0 14.423,40.615,0 14.402,40.602,0 14.356,40.583,0 14.343,40.57,0 14.331,40.584,0 14.329,40.605,0 14.338,40.624,0 14.36,40.632,0 14.38,40.634,0 14.388,40.637,0 14.395,40.65,0 14.403,40.657,0 14.471,40.699,0 14.48,40.711,0 14.475,40.729,0 14.461,40.744,0 14.443,40.755,0 14.426,40.762,0 14.415,40.765,0 14.399,40.767,0 14.391,40.77,0 14.385,40.774,0 14.372,40.787,0 14.367,40.79,0 14.349,40.797,0 14.313,40.828,0 14.295,40.839,0 14.276,40.84,0 14.249,40.837,0 14.224,40.831,0 14.213,40.821,0 14.204,40.801,0 14.182,40.8,0 14.112,40.829,0 14.096,40.834,0 14.083,40.831,0 14.077,40.822,0 14.078,40.81,0 14.082,40.797,0 14.083,40.783,0 14.075,40.788,0 14.041,40.798,0 14.053,40.837,0 14.044,40.875,0 13.966,40.996,0 13.931,41.014,0 13.918,41.023,0 13.915,41.033,0 13.913,41.054,0 13.911,41.064,0 13.885,41.104,0 13.786,41.203,0 13.722,41.252,0 13.709,41.256,0 13.679,41.25,0 13.664,41.25,0 13.657,41.259,0 13.595,41.253,0 13.564,41.238,0 13.576,41.208,0 13.544,41.206,0 13.535,41.208,0 13.526,41.215,0 13.52,41.225,0 13.515,41.229,0 13.508,41.221,0 13.5,41.221,0 13.481,41.239,0 13.325,41.295,0 13.286,41.295,0 13.205,41.284,0 13.187,41.278,0 13.152,41.26,0 13.115,41.251,0 13.091,41.226,0 13.069,41.221,0 13.045,41.227,0 13.037,41.24,0 13.034,41.257,0 13.024,41.273,0 13.013,41.286,0 12.993,41.315,0 12.98,41.331,0 12.924,41.379,0 12.894,41.399,0 12.863,41.413,0 12.842,41.418,0 12.764,41.421,0 12.749,41.423,0 12.679,41.458,0 12.655,41.465,0 12.643,41.458,0 12.636,41.447,0 12.62,41.459,0 12.546,41.544,0 12.449,41.63,0 12.343,41.702,0 12.328,41.711,0 12.301,41.717,0 12.286,41.727,0 12.277,41.729,0 12.247,41.733,0 12.24,41.736,0 12.224,41.75,0 12.216,41.768,0 12.212,41.787,0 12.212,41.808,0 12.207,41.827,0 12.195,41.847,0 12.171,41.879,0 12.148,41.903,0 12.05,41.96,0 12.039,41.965,0 12.03,41.973,0 12.027,41.986,0 12.021,41.993,0 11.993,41.996,0 11.983,42,0 11.97,42.011,0 11.953,42.022,0 11.935,42.031,0 11.917,42.038,0 11.84,42.036,0 11.828,42.034,0 11.823,42.047,0 11.81,42.066,0 11.794,42.084,0 11.78,42.092,0 11.772,42.106,0 11.751,42.128,0 11.746,42.136,0 11.744,42.152,0 11.737,42.169,0 11.683,42.252,0 11.659,42.279,0 11.54,42.349,0 11.49,42.359,0 11.421,42.386,0 11.397,42.393,0 11.397,42.4,0 11.387,42.404,0 11.377,42.407,0 11.366,42.408,0 11.355,42.407,0 11.363,42.4,0 11.334,42.4,0 11.26,42.421,0 11.246,42.422,0 11.228,42.422,0 11.212,42.419,0 11.205,42.411,0 11.201,42.395,0 11.187,42.379,0 11.185,42.366,0 11.175,42.369,0 11.165,42.369,0 11.158,42.368,0 11.157,42.366,0 11.148,42.371,0 11.135,42.384,0 11.107,42.391,0 11.095,42.402,0 11.087,42.418,0 11.081,42.435,0 11.1,42.443,0 11.123,42.446,0 11.167,42.448,0 11.175,42.458,0 11.184,42.48,0 11.19,42.504,0 11.188,42.521,0 11.167,42.546,0 11.159,42.564,0 11.149,42.563,0 11.138,42.559,0 11.129,42.558,0 11.117,42.572,0 11.108,42.591,0 11.098,42.607,0 11.081,42.612,0 11.078,42.632,0 11.054,42.647,0 11.006,42.668,0 11.001,42.68,0 10.996,42.696,0 10.99,42.71,0 10.982,42.716,0 10.973,42.72,0 10.944,42.743,0 10.891,42.764,0 10.732,42.804,0 10.756,42.819,0 10.766,42.835,0 10.767,42.854,0 10.766,42.877,0 10.769,42.884,0 10.775,42.888,0 10.778,42.894,0 10.774,42.908,0 10.764,42.918,0 10.751,42.925,0 10.682,42.949,0 10.633,42.958,0 10.584,42.959,0 10.54,42.949,0 10.544,42.939,0 10.547,42.935,0 10.519,42.925,0 10.5,42.94,0 10.478,42.99,0 10.503,43.005,0 10.518,43.024,0 10.54,43.079,0 10.536,43.091,0 10.536,43.112,0 10.54,43.134,0 10.547,43.147,0 10.539,43.164,0 10.535,43.185,0 10.533,43.226,0 10.529,43.246,0 10.517,43.267,0 10.438,43.388,0 10.374,43.453,0 10.36,43.465,0 10.327,43.477,0 10.318,43.492,0 10.295,43.568,0 10.265,43.809,0 10.252,43.846,0 10.211,43.92,0 10.181,43.955,0 10.137,43.978,0 10.106,44.016,0 10.091,44.025,0 10.073,44.029,0 10.036,44.048,0 10.015,44.052,0 9.999,44.058,0 9.989,44.06,0 9.985,44.055,0 9.981,44.05,0 9.973,44.045,0 9.963,44.044,0 9.954,44.048,0 9.938,44.06,0 9.905,44.08,0 9.888,44.093,0 9.877,44.088,0 9.845,44.108,0 9.827,44.107,0 9.834,44.1,0 9.829,44.098,0 9.825,44.095,0 9.82,44.093,0 9.825,44.085,0 9.831,44.079,0 9.839,44.075,0 9.848,44.072,0 9.848,44.066,0 9.842,44.063,0 9.839,44.06,0 9.834,44.052,0 9.847,44.046,0 9.843,44.041,0 9.833,44.042,0 9.827,44.055,0 9.82,44.063,0 9.772,44.079,0 9.722,44.113,0 9.71,44.118,0 9.683,44.136,0 9.673,44.141,0 9.644,44.142,0 9.632,44.144,0 9.622,44.148,0 9.587,44.178,0 9.581,44.179,0 9.573,44.191,0 9.557,44.2,0 9.512,44.215,0 9.5,44.222,0 9.49,44.231,0 9.485,44.244,0 9.473,44.24,0 9.454,44.237,0 9.437,44.239,0 9.43,44.247,0 9.423,44.257,0 9.375,44.272,0 9.368,44.294,0 9.263,44.336,0 9.231,44.353,0 9.222,44.344,0 9.214,44.333,0 9.21,44.321,0 9.211,44.305,0 9.166,44.318,0 9.147,44.328,0 9.149,44.34,0 9.131,44.363,0 9.103,44.374,0 9.002,44.387,0 8.953,44.4,0 8.924,44.411,0 8.915,44.409,0 8.869,44.409,0 8.846,44.413,0 8.838,44.417,0 8.828,44.428,0 8.763,44.432,0 8.738,44.429,0 8.725,44.424,0 8.696,44.406,0 8.686,44.398,0 8.679,44.394,0 8.671,44.394,0 8.663,44.395,0 8.656,44.394,0 8.594,44.363,0 8.577,44.36,0 8.565,44.357,0 8.541,44.34,0 8.467,44.304,0 8.445,44.284,0 8.45,44.264,0 8.44,44.253,0 8.437,44.247,0 8.436,44.24,0 8.433,44.238,0 8.418,44.23,0 8.412,44.227,0 8.407,44.215,0 8.409,44.204,0 8.409,44.193,0 8.395,44.182,0 8.37,44.173,0 8.314,44.16,0 8.285,44.148,0 8.27,44.138,0 8.257,44.128,0 8.234,44.103,0 8.231,44.096,0 8.232,44.08,0 8.231,44.072,0 8.224,44.057,0 8.217,44.045,0 8.17,44.006,0 8.153,43.983,0 8.168,43.962,0 8.168,43.956,0 8.145,43.952,0 8.116,43.927,0 8.09,43.92,0 8.082,43.915,0 8.076,43.909,0 8.073,43.904,0 8.068,43.896,0 8.056,43.892,0 8.032,43.887,0 7.96,43.853,0 7.786,43.822,0 7.737,43.798,0 7.695,43.791,0 7.573,43.791,0 7.545,43.784,0 7.532,43.784,0 7.524,43.789,0 7.513,43.792,0 7.503,43.792,0 7.483,43.84,0 7.478,43.866,0 7.493,43.886,0 7.537,43.921,0 7.557,43.944,0 7.609,43.976,0 7.631,43.994,0 7.639,44.005,0 7.647,44.027,0 7.653,44.04,0 7.664,44.049,0 7.679,44.057,0 7.69,44.067,0 7.692,44.085,0 7.676,44.109,0 7.654,44.125,0 7.642,44.144,0 7.656,44.176,0 7.625,44.18,0 7.584,44.161,0 7.555,44.159,0 7.381,44.123,0 7.341,44.124,0 7.331,44.125,0 7.322,44.132,0 7.316,44.14,0 7.309,44.147,0 7.296,44.151,0 7.27,44.154,0 7.251,44.16,0 7.145,44.207,0 7.105,44.218,0 7.046,44.24,0 7.033,44.243,0 7.02,44.242,0 7.008,44.239,0 6.996,44.238,0 6.983,44.242,0 6.973,44.249,0 6.969,44.258,0 6.966,44.268,0 6.959,44.277,0 6.95,44.285,0 6.93,44.295,0 6.921,44.302,0 6.916,44.31,0 6.904,44.33,0 6.896,44.34,0 6.874,44.358,0 6.87,44.363,0 6.866,44.372,0 6.866,44.377,0 6.869,44.383,0 6.877,44.414,0 6.884,44.423,0 6.918,44.436,0 6.892,44.452,0 6.861,44.475,0 6.839,44.503,0 6.836,44.534,0 6.846,44.547,0 6.897,44.575,0 6.932,44.618,0 6.946,44.625,0 6.934,44.647,0 6.941,44.667,0 6.96,44.683,0 6.983,44.692,0 7.001,44.692,0 7.037,44.685,0 7.055,44.685,0 7.049,44.698,0 7.019,44.739,0 7.015,44.747,0 7.01,44.772,0 6.998,44.794,0 6.999,44.795,0 7.004,44.811,0 7.006,44.812,0 7.006,44.816,0 7.007,44.819,0 7.007,44.822,0 7.005,44.828,0 7.001,44.833,0 6.983,44.847,0 6.933,44.862,0 6.915,44.863,0 6.866,44.856,0 6.847,44.859,0 6.778,44.888,0 6.745,44.908,0 6.728,44.929,0 6.73,44.985,0 6.723,45.013,0 6.697,45.027,0 6.662,45.029,0 6.652,45.036,0 6.64,45.05,0 6.637,45.059,0 6.638,45.067,0 6.637,45.074,0 6.62,45.084,0 6.603,45.103,0 6.615,45.115,0 6.633,45.126,0 6.667,45.14,0 6.676,45.141,0 6.694,45.14,0 6.702,45.141,0 6.711,45.145,0 6.729,45.155,0 6.736,45.157,0 6.771,45.153,0 6.808,45.139,0 6.844,45.13,0 6.877,45.141,0 6.879,45.147,0 6.873,45.152,0 6.868,45.157,0 6.873,45.166,0 6.881,45.168,0 6.905,45.169,0 6.914,45.17,0 6.928,45.18,0 6.946,45.201,0 6.959,45.21,0 6.994,45.221,0 7.03,45.228,0 7.038,45.226,0 7.05,45.215,0 7.055,45.214,0 7.062,45.219,0 7.081,45.243,0 7.108,45.259,0 7.108,45.275,0 7.098,45.295,0 7.093,45.324,0 7.098,45.33,0 7.13,45.357,0 7.151,45.383,0 7.16,45.398,0 7.161,45.411,0 7.153,45.415,0 7.11,45.428,0 7.097,45.435,0 7.089,45.447,0 7.082,45.459,0 7.072,45.47,0 7.028,45.493,0 6.983,45.511,0 6.975,45.526,0 6.97,45.567,0 6.966,45.574,0 6.955,45.586,0 6.953,45.594,0 6.956,45.603,0 6.967,45.62,0 6.969,45.626,0 6.963,45.641,0 6.951,45.647,0 6.919,45.653,0 6.905,45.66,0 6.883,45.676,0 6.869,45.679,0 6.843,45.683,0 6.816,45.697,0 6.796,45.718,0 6.785,45.76,0 6.782,45.777,0 6.783,45.795,0 6.788,45.812,0 6.801,45.826,0 6.816,45.833,0 6.846,45.836,0 6.846,45.838,0 6.849,45.842,0 6.853,45.847,0 6.858,45.849,0 6.862,45.849,0 6.87,45.845,0 6.873,45.845,0 6.88,45.846,0 6.905,45.845,0 6.926,45.85,0 6.949,45.858,0 6.969,45.87,0 6.983,45.886,0 6.989,45.899,0 6.997,45.911,0 7.008,45.921,0 7.022,45.925,0 7.067,45.89,0 7.09,45.881,0 7.121,45.876,0 7.154,45.877,0 7.184,45.88,0 7.245,45.898,0 7.274,45.91,0 7.287,45.913,0 7.362,45.908,0 7.394,45.916,0 7.453,45.946,0 7.483,45.955,0 7.504,45.957,0 7.515,45.967,0 7.524,45.978,0 7.541,45.984,0 7.643,45.966,0 7.659,45.96,0 7.674,45.95,0 7.693,45.931,0 7.694,45.929,0 7.706,45.926,0 7.715,45.927,0 7.722,45.93,0 7.732,45.93,0 7.78,45.918,0 7.808,45.918,0 7.825,45.915,0 7.831,45.914,0 7.844,45.919,0 7.846,45.923,0 7.845,45.928,0 7.848,45.938,0 7.872,45.969,0 7.898,45.982,0 7.969,45.993,0 7.979,45.995,0 7.986,45.999,0 7.998,46.011,0 7.999,46.013,0 8.009,46.028,0 8.011,46.03,0 8.016,46.058,0 8.016,46.069,0 8.018,46.081,0 8.025,46.091,0 8.035,46.097,0 8.056,46.098,0 8.067,46.101,0 8.111,46.127,0 8.132,46.159,0 8.13,46.196,0 8.1,46.236,0 8.077,46.25,0 8.073,46.254,0 8.077,46.262,0 8.087,46.272,0 8.107,46.286,0 8.128,46.292,0 8.172,46.299,0 8.193,46.309,0 8.242,46.354,0 8.27,46.364,0 8.282,46.37,0 8.291,46.378,0 8.297,46.388,0 8.297,46.398,0 8.29,46.401,0 8.287,46.405,0 8.295,46.418,0 8.316,46.434,0 8.343,46.444,0 8.399,46.452,0 8.428,46.449,0 8.442,46.435,0 8.446,46.412,0 8.446,46.382,0 8.443,46.353,0 8.427,46.302,0 8.423,46.276,0 8.427,46.251,0 8.438,46.235,0 8.457,46.225,0 8.483,46.218,0 8.51,46.208,0 8.539,46.188,0 8.602,46.123,0 8.612,46.119,0 8.631,46.115,0 8.677,46.096,0 8.695,46.095,0 8.702,46.098,0 8.718,46.108,0 8.724,46.11,0 8.732,46.107,0 8.739,46.098,0 8.747,46.094,0 8.763,46.093,0 8.794,46.093,0 8.809,46.09,0 8.834,46.066,0 8.82,46.043,0 8.791,46.019,0 8.773,45.991,0 8.77,45.986,0 8.768,45.983,0 8.785,45.982,0 8.8,45.979,0 8.858,45.957,0 8.864,45.953,0 8.871,45.947,0 8.881,45.931,0 8.898,45.91,0 8.907,45.896,0 8.912,45.883,0 8.914,45.866,0 8.91,45.854,0 8.904,45.842,0 8.9,45.826,0 8.94,45.835,0 8.972,45.825,0 9.002,45.821,0 9.034,45.848,0 9.059,45.882,0 9.063,45.899,0 9.052,45.916,0 9.042,45.92,0 9.021,45.923,0 9.011,45.927,0 9.002,45.936,0 8.993,45.954,0 8.983,45.962,0 8.981,45.964,0 8.98,45.967,0 8.981,45.969,0 8.983,45.972,0 9.016,45.993,0 8.998,46.028,0 9.002,46.039,0 9.028,46.053,0 9.05,46.058,0 9.059,46.062,0 9.067,46.071,0 9.07,46.083,0 9.068,46.106,0 9.072,46.119,0 9.091,46.138,0 9.163,46.172,0 9.171,46.183,0 9.176,46.194,0 9.181,46.204,0 9.192,46.21,0 9.204,46.214,0 9.216,46.221,0 9.225,46.231,0 9.24,46.267,0 9.269,46.309,0 9.275,46.331,0 9.274,46.344,0 9.26,46.38,0 9.26,46.394,0 9.263,46.407,0 9.261,46.417,0 9.248,46.423,0 9.238,46.437,0 9.246,46.461,0 9.263,46.485,0 9.282,46.497,0 9.331,46.502,0 9.351,46.498,0 9.352,46.485,0 9.377,46.469,0 9.385,46.466,0 9.395,46.469,0 9.4,46.475,0 9.404,46.483,0 9.411,46.489,0 9.427,46.497,0 9.435,46.498,0 9.438,46.492,0 9.444,46.396,0 9.442,46.381,0 9.444,46.375,0 9.452,46.37,0 9.474,46.362,0 9.483,46.357,0 9.503,46.321,0 9.515,46.309,0 9.536,46.299,0 9.56,46.293,0 9.674,46.292,0 9.693,46.297,0 9.708,46.312,0 9.709,46.32,0 9.707,46.331,0 9.709,46.342,0 9.72,46.351,0 9.731,46.351,0 9.755,46.341,0 9.768,46.339,0 9.789,46.343,0 9.855,46.367,0 9.899,46.372,0 9.918,46.371,0 9.939,46.367,0 9.964,46.356,0 9.971,46.34,0 9.971,46.32,0 9.978,46.298,0 9.992,46.284,0 10.032,46.26,0 10.042,46.243,0 10.043,46.22,0 10.076,46.22,0 10.118,46.231,0 10.146,46.243,0 10.159,46.262,0 10.146,46.28,0 10.105,46.309,0 10.096,46.321,0 10.092,46.329,0 10.092,46.338,0 10.097,46.352,0 10.105,46.361,0 10.126,46.374,0 10.133,46.381,0 10.141,46.403,0 10.133,46.414,0 10.116,46.419,0 10.071,46.425,0 10.042,46.433,0 10.026,46.446,0 10.044,46.467,0 10.035,46.471,0 10.03,46.477,0 10.028,46.484,0 10.027,46.493,0 10.031,46.504,0 10.031,46.526,0 10.033,46.533,0 10.041,46.542,0 10.063,46.557,0 10.071,46.564,0 10.083,46.597,0 10.088,46.604,0 10.097,46.608,0 10.192,46.627,0 10.218,46.627,0 10.234,46.618,0 10.236,46.607,0 10.23,46.586,0 10.235,46.575,0 10.276,46.566,0 10.284,46.561,0 10.289,46.556,0 10.295,46.551,0 10.307,46.547,0 10.319,46.546,0 10.354,46.548,0 10.426,46.535,0 10.444,46.538,0 10.458,46.554,0 10.466,46.578,0 10.467,46.604,0 10.459,46.624,0 10.438,46.636,0 10.396,46.639,0 10.378,46.653,0 10.369,46.672,0 10.374,46.682,0 10.385,46.689,0 10.394,46.701,0 10.397,46.715,0 10.396,46.726,0 10.4,46.736,0 10.417,46.743,0 10.429,46.756,0 10.426,46.769,0 10.419,46.784,0 10.417,46.799,0 10.439,46.817,0 10.445,46.823,0 10.449,46.832,0 10.454,46.864,0 10.486,46.846,0 10.528,46.843,0 10.629,46.862,0 10.647,46.864,0 10.662,46.861,0 10.739,46.83,0 10.749,46.819,0 10.744,46.813,0 10.722,46.8,0 10.717,46.795,0 10.723,46.786,0 10.734,46.786,0 10.755,46.791,0 10.766,46.788,0 10.795,46.777,0 10.805,46.777,0 10.824,46.78,0 10.834,46.78,0 10.843,46.777,0 10.86,46.767,0 10.87,46.764,0 10.88,46.765,0 10.914,46.772,0 10.931,46.774,0 10.966,46.772,0 10.983,46.768,0 10.997,46.769,0 11.011,46.779,0 11.033,46.806,0 11.037,46.808,0 11.049,46.812,0 11.053,46.815,0 11.055,46.82,0 11.053,46.83,0 11.054,46.834,0 11.073,46.865,0 11.084,46.9,0 11.092,46.912,0 11.157,46.957,0 11.174,46.964,0 11.244,46.979,0 11.314,46.987,0 11.349,46.982,0 11.381,46.972,0 11.411,46.97,0 11.445,46.993,0 11.445,46.993,0 11.453,47.001,0 11.462,47.006,0 11.472,47.007,0 11.489,47.004,0 11.496,47.002,0 11.502,46.998,0 11.507,46.993,0 11.515,46.989,0 11.524,46.988,0 11.534,46.99,0 11.543,46.993,0 11.543,46.993,0 11.544,46.993,0 11.544,46.993,0 11.573,46.999,0 11.596,47,0 11.648,46.993,0 11.648,46.993,0 11.65,46.993,0 11.657,46.993,0 11.665,46.993,0 11.684,46.992,0 11.716,46.975,0 11.735,46.971,0 11.746,46.972,0 11.766,46.983,0 11.777,46.988,0 11.823,46.993,0 11.857,47.012,0 11.9,47.028,0 11.944,47.038,0 12.015,47.04,0 12.116,47.077,0 12.181,47.085,0 12.204,47.08,0 12.204,47.053,0 12.182,47.034,0 12.122,47.011,0 12.111,46.993,0 12.118,46.983,0 12.122,46.972,0 12.4,43.903,0 12.429,43.892,0 12.461,43.895,0 12.479,43.917,0 12.478,43.92,0 12.478,43.923,0 12.48,43.926,0 12.483,43.929,0 12.49,43.939,0 12.492,43.956,0 12.489,43.973,0 12.482,43.983,0 12.453,43.979,0 12.421,43.967,0 12.396,43.948,0 12.386,43.925,0 12.4,43.903,0 12.444,41.902,0 12.449,41.9,0 12.455,41.9,0 12.458,41.902,0 12.455,41.908,0 12.447,41.907,0 12.444,41.902,0 - """ - - k = kml.KML() - k.from_string(doc) - assert len(list(k.features())) == 1 - assert isinstance(list(k.features())[0].geometry, MultiPolygon) - k2 = kml.KML() - k2.from_string(k.to_string()) - assert k.to_string() == k2.to_string() - - def test_atom(self): + def test_atom(self) -> None: pass - def test_schema(self): + def test_schema(self) -> None: doc = """ Trail Head Name]]> @@ -789,25 +591,7 @@ def test_schema(self): assert "SimpleField" in k1.to_string() assert k1.to_string() == k.to_string() - def test_schema_data(self): - doc = """ - Pi in the sky - 3.14159 - 10 - """ - - sd = data.SchemaData(ns="", schema_url="#default") - sd.from_string(doc) - assert sd.schema_url == "#TrailHeadTypeId" - assert sd.data[0] == {"name": "TrailHeadName", "value": "Pi in the sky"} - assert sd.data[1] == {"name": "TrailLength", "value": "3.14159"} - assert sd.data[2] == {"name": "ElevationGain", "value": "10"} - sd1 = data.SchemaData(ns="", schema_url="#default") - sd1.from_string(sd.to_string()) - assert sd1.schema_url == "#TrailHeadTypeId" - assert sd.to_string() == sd1.to_string() - - def test_snippet(self): + def test_snippet(self) -> None: doc = """ Short Desc @@ -827,11 +611,11 @@ def test_snippet(self): assert "maxLines" not in k.to_string() assert "Diffrent Snippet" in k.to_string() - def test_from_wrong_string(self): + def test_from_wrong_string(self) -> None: doc = kml.KML() pytest.raises(TypeError, doc.from_string, "") - def test_from_string_with_unbound_prefix(self): + def test_from_string_with_unbound_prefix(self) -> None: doc = """ @@ -847,7 +631,7 @@ def test_from_string_with_unbound_prefix(self): k = kml.KML() pytest.raises(xml.etree.ElementTree.ParseError, k.from_string, doc) - def test_address(self): + def test_address(self) -> None: doc = kml.Document() doc.from_string( @@ -856,7 +640,7 @@ def test_address(self): pm-name pm-description 1 - 1600 Amphitheatre Parkway, Mountain View, CA 94043, USA + 1600 Amphitheatre Parkway,... """ ) @@ -865,7 +649,7 @@ def test_address(self): doc2.from_string(doc.to_string()) assert doc.to_string() == doc2.to_string() - def test_phone_number(self): + def test_phone_number(self) -> None: doc = kml.Document() doc.from_string( @@ -883,7 +667,7 @@ def test_phone_number(self): doc2.from_string(doc.to_string()) assert doc.to_string() == doc2.to_string() - def test_groundoverlay(self): + def test_groundoverlay(self) -> None: doc = kml.KML() doc.from_string( @@ -897,7 +681,7 @@ def test_groundoverlay(self): Overlay shows Mount Etna erupting on July 13th, 2001. - http://developers.google.com/kml/documentation/images/etna.jpg + http://developers.google.com/kml/etna.jpg 37.91904192681665 @@ -916,7 +700,7 @@ def test_groundoverlay(self): doc2.from_string(doc.to_string()) assert doc.to_string() == doc2.to_string() - def test_linarring_placemark(self): + def test_linarring_placemark(self) -> None: doc = kml.KML() doc.from_string( """ @@ -933,7 +717,7 @@ def test_linarring_placemark(self): class TestStyle: - def test_styleurl(self): + def test_styleurl(self) -> None: f = kml.Document() f.style_url = "#somestyle" assert f.style_url == "#somestyle" @@ -946,7 +730,7 @@ def test_styleurl(self): f2.from_string(f.to_string()) assert f.to_string() == f2.to_string() - def test_style(self): + def test_style(self) -> None: lstyle = styles.LineStyle(color="red", width=2.0) style = styles.Style(styles=[lstyle]) f = kml.Document(styles=[style]) @@ -954,15 +738,15 @@ def test_style(self): f2.from_string(f.to_string(prettyprint=True)) assert f.to_string() == f2.to_string() - def test_polystyle_fill(self): + def test_polystyle_fill(self) -> None: styles.PolyStyle() - def test_polystyle_outline(self): + def test_polystyle_outline(self) -> None: styles.PolyStyle() class TestStyleUsage: - def test_create_document_style(self): + def test_create_document_style(self) -> None: style = styles.Style(styles=[styles.PolyStyle(color="7f000000")]) doc = kml.Document(styles=[style]) @@ -990,7 +774,7 @@ def test_create_document_style(self): assert doc2.to_string() == doc3.to_string() assert doc.to_string() == doc3.to_string() - def test_create_placemark_style(self): + def test_create_placemark_style(self) -> None: style = styles.Style(styles=[styles.PolyStyle(color="7f000000")]) place = kml.Placemark(styles=[style]) @@ -1019,7 +803,7 @@ def test_create_placemark_style(self): class TestStyleFromString: - def test_styleurl(self): + def test_styleurl(self) -> None: doc = """ Document.kml @@ -1036,7 +820,7 @@ def test_styleurl(self): k2.from_string(k.to_string()) assert k.to_string() == k2.to_string() - def test_balloonstyle(self): + def test_balloonstyle(self) -> None: doc = """ Document.kml @@ -1078,7 +862,7 @@ def test_balloonstyle(self): k2.from_string(k.to_string()) assert k2.to_string() == k.to_string() - def test_balloonstyle_old_color(self): + def test_balloonstyle_old_color(self) -> None: doc = """ Document.kml @@ -1102,7 +886,7 @@ def test_balloonstyle_old_color(self): k2.from_string(k.to_string()) assert k2.to_string() == k.to_string() - def test_labelstyle(self): + def test_labelstyle(self) -> None: doc = """ Document.kml @@ -1127,7 +911,7 @@ def test_labelstyle(self): k2.from_string(k.to_string()) assert k.to_string() == k2.to_string() - def test_iconstyle(self): + def test_iconstyle(self) -> None: doc = """