diff --git a/.github/workflows/run-all-tests.yml b/.github/workflows/run-all-tests.yml index 5b2d46cb..fa04e128 100644 --- a/.github/workflows/run-all-tests.yml +++ b/.github/workflows/run-all-tests.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13-dev', '3.14-dev'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14-dev'] steps: - uses: actions/checkout@v4 @@ -29,7 +29,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13-dev'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] steps: - uses: actions/checkout@v4 @@ -43,7 +43,7 @@ jobs: pip install -e ".[tests, lxml]" - name: Test with pytest run: | - pytest tests --cov=fastkml --cov=tests --cov-fail-under=88 --cov-report=xml + pytest tests --cov=fastkml --cov=tests --cov-fail-under=95 --cov-report=xml - name: "Upload coverage to Codecov" if: ${{ matrix.python-version==3.11 }} uses: codecov/codecov-action@v4 @@ -67,10 +67,18 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip wheel - pip install -e ".[tests, lxml]" + pip install -e ".[tests, lxml, docs]" - name: test the pythoncode in the documentation run: | python -m doctest docs/*.rst -v + - name: Run the pythoncode in the examples + run: | + python examples/read_kml.py + python examples/shp2kml.py + python examples/shp2kml_timed.py + python examples/simple_example.py + python examples/transform_cascading_style.py + static-tests: runs-on: ubuntu-latest @@ -122,39 +130,69 @@ jobs: run: | pytest tests - publish: - if: "github.event_name == 'push' && github.repository == 'cleder/fastkml'" - needs: [cpython, static-tests, pypy, cpython-lxml, doctest-lxml] - name: Build and publish to PyPI and TestPyPI + build-package: + name: Build & inspect our package. runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 + - uses: hynek/build-and-inspect-python-package@v2 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Check with pyroma + run: | + python -m pip install pyroma + pyroma . + - name: Check tag name + if: >- + github.event_name == 'push' && + startsWith(github.ref, 'refs/tags') + run: | + python -m pip install vercheck + vercheck $GITHUB_REF_NAME fastkml/about.py + + test-publish: + if: >- + github.event_name == 'push' && + github.repository == 'cleder/fastkml' && + startsWith(github.ref, 'refs/tags') + needs: [cpython, static-tests, pypy, cpython-lxml, doctest-lxml, build-package] + name: Test install on TestPyPI + runs-on: ubuntu-latest + environment: test-release + permissions: + id-token: write + steps: + - name: Download packages built by build-package + uses: actions/download-artifact@v4 with: - python-version: 3.12 - - name: Install pypa/build - run: >- - python -m - pip install - build - --user - - name: Build a binary wheel and a source tarball - run: >- - python -m - build - --sdist - --wheel - --outdir dist/ - - name: Publish distribution 📦 to Test PyPI for tags - if: startsWith(github.ref, 'refs/tags') + name: Packages + path: dist + + - name: Upload package to Test PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: - password: ${{ secrets.TEST_PYPI_API_TOKEN }} - repository_url: https://test.pypi.org/legacy/ + repository-url: https://test.pypi.org/legacy/ + + + publish: + if: >- + github.event_name == 'push' && + github.repository == 'cleder/fastkml' && + github.ref == 'refs/heads/main' + needs: [cpython, static-tests, pypy, cpython-lxml, doctest-lxml, build-package] + name: Publish to PyPI on push to main + runs-on: ubuntu-latest + environment: release + permissions: + id-token: write + steps: + - name: Download packages built by build-package + uses: actions/download-artifact@v4 + with: + name: Packages + path: dist + - name: Publish distribution 📦 to PyPI for push to main - if: github.event_name == 'push' && github.ref == 'refs/heads/main' uses: pypa/gh-action-pypi-publish@release/v1 - with: - password: ${{ secrets.PYPI_API_TOKEN }} ... diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 53344162..b2fcf01b 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.6.0 + rev: v5.0.0 hooks: - id: check-added-large-files - id: check-docstring-first @@ -15,7 +15,11 @@ repos: - id: end-of-file-fixer - id: mixed-line-ending - id: name-tests-test - exclude: ^tests/base.py + exclude: (?x)^( + tests/base.py| + tests/hypothesis/common.py| + tests/hypothesis/strategies.py + )$ - id: no-commit-to-branch - id: pretty-format-json - id: requirements-txt-fixer @@ -25,24 +29,15 @@ repos: hooks: - id: pyprojectsort - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.20.2 + rev: v0.22 hooks: - id: validate-pyproject - - repo: https://github.com/ikamensh/flynt/ - rev: "1.0.1" - hooks: - - id: flynt - repo: https://github.com/MarcoGorelli/absolufy-imports rev: v0.3.1 hooks: - id: absolufy-imports - - repo: https://github.com/asottile/pyupgrade - rev: v3.17.0 - hooks: - - id: pyupgrade - args: ["--py3-plus", "--py37-plus"] - repo: https://github.com/psf/black - rev: 24.8.0 + rev: 24.10.0 hooks: - id: black - repo: https://github.com/pycqa/isort @@ -50,7 +45,7 @@ repos: hooks: - id: isort - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.6.8' + rev: 'v0.7.2' hooks: - id: ruff - repo: https://github.com/PyCQA/flake8 @@ -58,7 +53,7 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/pygrep-hooks - rev: v1.10.0 # Use the ref you want to point at + rev: v1.10.0 hooks: - id: python-use-type-annotations - id: python-check-blanket-type-ignore @@ -73,21 +68,21 @@ repos: rev: "v6.2.4" hooks: - id: rstcheck + - repo: https://github.com/sphinx-contrib/sphinx-lint + rev: "v1.0.0" + hooks: + - id: sphinx-lint - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.11.2 + rev: v1.13.0 hooks: - id: mypy - additional_dependencies: [pygeoif>=1.4, arrow, pytest] + additional_dependencies: [pygeoif>=1.4, arrow, pytest, hypothesis] - repo: https://github.com/adamchainz/blacken-docs - rev: "1.18.0" + rev: "1.19.1" hooks: - id: blacken-docs - repo: https://github.com/crate-ci/typos - rev: v1.25.0 + rev: v1.27.0 hooks: - id: typos - # - repo: https://github.com/Lucas-C/pre-commit-hooks-markup - # rev: v1.0.1 - # hooks: - # - id: rst-linter ... diff --git a/README.rst b/README.rst index 6db81743..d96255f5 100644 --- a/README.rst +++ b/README.rst @@ -1,16 +1,22 @@ Introduction ============ +.. inclusion-marker-do-not-remove + KML is an XML geospatial data format and an OGC_ standard that deserves a canonical python implementation. Fastkml is a library to read, write and manipulate KML files. It aims to keep it simple and fast (using lxml_ if available). Fast refers to the time you spend to write and read KML files as well as the time you spend to get acquainted to the library or to create KML objects. It aims to provide all of -the functionality that KML clients such as `OpenLayers -`_, `Google Maps `_, and -`Google Earth `_ provides. +the functionality that KML clients such as `Marble `_, +`Cesium JS `_, `OpenLayers `_, +`Google Maps `_, and +`Google Earth `_ support. +For more details about the KML Specification, check out the `KML Reference +`_ on the Google +developers site. Geometries are handled as pygeoif_ objects. @@ -87,14 +93,6 @@ You can find all of the documentation for FastKML at `fastkml.readthedocs.org please submit a pull request on `GitHub `_ with the improvement. -Have a look at Aryan Guptas -`The Definite Guide to FastKML. `_ - -Alternatives -============ - -`Keytree `_ provides a less comprehensive, but more flexible -approach. Install ======== @@ -117,10 +115,6 @@ Optional Limitations =========== -*Tessellate*, *Extrude* and *Altitude Mode* are assigned to a Geometry or -Geometry collection (MultiGeometry). You cannot assign different values of -*Tessellate*, *Extrude* or *Altitude Mode* on parts of a MultiGeometry. - Currently, the only major feature missing for the full Google Earth experience is the `gx extension `_. diff --git a/docs/Document-clean.kml b/docs/Document-clean.kml new file mode 120000 index 00000000..e39077a5 --- /dev/null +++ b/docs/Document-clean.kml @@ -0,0 +1 @@ +../tests/ogc_conformance/data/kml/Document-clean.kml \ No newline at end of file diff --git a/docs/alternatives.rst b/docs/alternatives.rst new file mode 100644 index 00000000..0609aff0 --- /dev/null +++ b/docs/alternatives.rst @@ -0,0 +1,19 @@ +Alternative KML libraries +========================= +There are several other libraries that can read and/or write KML files. +Here are a few of them: + +- Keytree_ provides a less comprehensive, but more flexible approach. It provides functions for reading and writing KML using the ElementTree API. +- PyKML_ is a more feature-rich library, but it is not actively maintained. +- geojson2kml_ converts GeoJSON to KML files. +- kml2geojson_ is a Python library to convert KML files to GeoJSON files. +- Simplekml_ is a python package which enables you to generate KML with as little effort as possible. +- kmlb_ A Straightforward Google Earth KML Builder. + + +.. _kml2geojson: https://github.com/mrcagney/kml2geojson +.. _PyKML: https://pythonhosted.org/pykml/ +.. _Keytree: https://github.com/Toblerity/keytree +.. _geojson2kml: https://github.com/aguinane/geojson-to-kml +.. _Simplekml: https://github.com/eisoldt/simplekml +.. _kmlb: https://github.com/HFM3/kmlb diff --git a/docs/co2-per-capita-2020.jpg b/docs/co2-per-capita-2020.jpg new file mode 100644 index 00000000..087ea548 Binary files /dev/null and b/docs/co2-per-capita-2020.jpg differ diff --git a/docs/conf.py b/docs/conf.py index a55a3d99..63233c5c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -32,7 +32,11 @@ "sphinx.ext.autodoc", "sphinx.ext.doctest", "sphinx.ext.napoleon", + "sphinx.ext.autosummary", + "sphinx.ext.coverage", + "sphinx.ext.viewcode", ] +autosummary_generate = True # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] @@ -48,14 +52,14 @@ # General information about the project. project = "FastKML" -copyright = "2014, Christian Ledermann & Ian Lee" +copyright = "2014 -2024, Christian Ledermann & Ian Lee" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = about.__version__ +version = ".".join(about.__version__.split(".")[:2]) # The full version, including alpha/beta/rc tags. release = about.__version__ diff --git a/docs/Configuration.rst b/docs/configuration.rst similarity index 100% rename from docs/Configuration.rst rename to docs/configuration.rst diff --git a/docs/create_kml_files.rst b/docs/create_kml_files.rst new file mode 100644 index 00000000..5fff34db --- /dev/null +++ b/docs/create_kml_files.rst @@ -0,0 +1,233 @@ +Creating KML files +================== + +Read a shapefile and build a 3D KML visualization. +-------------------------------------------------- + +This example shows how to read a shapefile and build a 3D KML visualization from it. + +You will need to install `pyshp `_ (``pip install pyshp``). + +For this example we will use the +`Data on CO2 and Greenhouse Gas Emissions `_ by +Our World in Data, and the Small scale data (1:110m) shapefile from +`Natural Earth `_. + +First we import the necessary modules: + +.. code-block:: python + + import csv + import pathlib + import random + + import shapefile + from pygeoif.factories import force_3d + from pygeoif.factories import shape + + import fastkml + import fastkml.containers + import fastkml.features + import fastkml.styles + from fastkml.enums import AltitudeMode + from fastkml.geometry import create_kml_geometry + + +Read the shapefile: + +.. code-block:: python + + shp = shapefile.Reader("ne_110m_admin_0_countries.shp") + +Read the CSV file and store the CO2 data for 2020: + +.. code-block:: python + + co2_csv = pathlib.Path("owid-co2-data.csv") + co2_data = {} + with co2_csv.open() as csvfile: + reader = csv.DictReader(csvfile) + for row in reader: + if row["year"] == "2020": + co2_data[row["iso_code"]] = ( + float(row["co2_per_capita"]) if row["co2_per_capita"] else 0 + ) + + +We prepare the styles and placemarks for the KML file, using random colors for each +country and the CO2 emissions as the height of the geometry. The shapefile offers +a handy ``__geo_interface__`` attribute that we can use to iterate over the features, +just like we would with a ``GeoJSON`` object, and extract the necessary information: + +.. code-block:: python + + placemarks = [] + for feature in shp.__geo_interface__["features"]: + iso3_code = feature["properties"]["ADM0_A3"] + geometry = shape(feature["geometry"]) + co2_emission = co2_data.get(iso3_code, 0) + geometry = force_3d(geometry, co2_emission * 100_000) + kml_geometry = create_kml_geometry( + geometry, + extrude=True, + altitude_mode=AltitudeMode.relative_to_ground, + ) + color = random.randint(0, 0xFFFFFF) + style = fastkml.styles.Style( + id=iso3_code, + styles=[ + fastkml.styles.LineStyle(color=f"33{color:06X}", width=2), + fastkml.styles.PolyStyle( + color=f"88{color:06X}", + fill=True, + outline=True, + ), + ], + ) + placemark = fastkml.features.Placemark( + name=feature["properties"]["NAME"], + description=feature["properties"]["FORMAL_EN"], + kml_geometry=kml_geometry, + styles=[style], + ) + placemarks.append(placemark) + + +Finally, we create the KML object and write it to a file: + +.. code-block:: python + + document = fastkml.containers.Document(features=placemarks, styles=styles) + kml = fastkml.KML(features=[document]) + + outfile = pathlib.Path("co2_per_capita_2020.kml") + with outfile.open("w") as f: + f.write(kml.to_string(prettyprint=True, precision=6)) + +The resulting KML file can be opened in Google Earth or any other KML viewer. + +.. image:: co2-per-capita-2020.jpg + :alt: CO2 emissions per capita in 2020 + :align: center + :width: 800px + :target: https://ion.cesium.com/stories/viewer/?id=a3cf93bb-bbb8-488b-8643-09c037ec12b8 + + +Build an animated over time KML visualization +---------------------------------------------- + +This example shows how to build an animated KML visualization over time. +We will use the same data as in the previous example, but this time we will +create a KML file that shows the CO2 emissions accumulating from 1995 to 2022. + +First we import the necessary modules: + +.. code-block:: python + + import csv + import pathlib + import random + import datetime + import shapefile + from pygeoif.factories import force_3d + from pygeoif.factories import shape + + import fastkml + import fastkml.containers + import fastkml.features + import fastkml.styles + import fastkml.times + from fastkml.enums import AltitudeMode, DateTimeResolution + from fastkml.geometry import create_kml_geometry + +Read the shapefile, the CSV file and store the CO2 data for each year: + +.. code-block:: python + + shp = shapefile.Reader("ne_110m_admin_0_countries.shp") + co2_csv = pathlib.Path("owid-co2-data.csv") + co2_pa = {str(i): {} for i in range(1995, 2023)} + with co2_csv.open() as csvfile: + reader = csv.DictReader(csvfile) + for row in reader: + if row["year"] >= "1995": + co2_pa[row["year"]][row["iso_code"]] = ( + float(row["co2_per_capita"]) if row["co2_per_capita"] else 0 + ) + + + +This time we will create a folder for each country, and a placemark for each year, +with the CO2 emissions per capita as the height of the geometry. +We will also create a style for each country, which we store at the document level to +prevent creating duplicate styles. +Each placemark will have a time-span that covers the whole year: + +.. code-block:: python + + styles = [] + folders = [] + for feature in shp.__geo_interface__["features"]: + iso3_code = feature["properties"]["ADM0_A3"] + geometry = shape(feature["geometry"]) + color = random.randint(0, 0xFFFFFF) + styles.append( + fastkml.styles.Style( + id=iso3_code, + styles=[ + fastkml.styles.LineStyle(color=f"33{color:06X}", width=2), + fastkml.styles.PolyStyle( + color=f"88{color:06X}", + fill=True, + outline=True, + ), + ], + ), + ) + style_url = fastkml.styles.StyleUrl(url=f"#{iso3_code}") + folder = fastkml.containers.Folder(name=feature["properties"]["NAME"]) + co2_growth = 0 + for year in range(1995, 2023): + co2_year = co2_pa[str(year)].get(iso3_code, 0) + co2_growth += co2_year + + kml_geometry = create_kml_geometry( + force_3d(geometry, co2_growth * 5_000), + extrude=True, + altitude_mode=AltitudeMode.relative_to_ground, + ) + timespan = fastkml.times.TimeSpan( + begin=fastkml.times.KmlDateTime( + datetime.date(year, 1, 1), resolution=DateTimeResolution.year_month + ), + end=fastkml.times.KmlDateTime( + datetime.date(year, 12, 31), resolution=DateTimeResolution.year_month + ), + ) + placemark = fastkml.features.Placemark( + name=f"{feature['properties']['NAME']} - {year}", + description=feature["properties"]["FORMAL_EN"], + kml_geometry=kml_geometry, + style_url=style_url, + times=timespan, + ) + folder.features.append(placemark) + folders.append(folder) + +Finally, we create the KML object and write it to a file: + +.. code-block:: python + + document = fastkml.containers.Document(features=folders, styles=styles) + kml = fastkml.KML(features=[document]) + + outfile = pathlib.Path("co2_growth_1995_2022.kml") + with outfile.open("w") as f: + f.write(kml.to_string(prettyprint=True, precision=3)) + + +You can open the resulting KML file in Google Earth Desktop and use the time slider to +see the CO2 emissions per capita grow over time, Google Earth Web does not support +time animations, but +`Cesium Ion `_ +can display the time animation. diff --git a/docs/fastkml.rst b/docs/fastkml.rst index 7cc89984..67c325ed 100644 --- a/docs/fastkml.rst +++ b/docs/fastkml.rst @@ -1,17 +1,14 @@ -fastkml package +=============== +Reference Guide =============== -Module contents ---------------- +.. toctree:: + :maxdepth: 4 + -.. automodule:: fastkml - :members: - :undoc-members: - :no-index: +.. automodule:: fastkml -Submodules ----------- fastkml.about module -------------------- @@ -45,6 +42,14 @@ fastkml.config module :undoc-members: :show-inheritance: +fastkml.containers module +------------------------- + +.. automodule:: fastkml.containers + :members: + :undoc-members: + :show-inheritance: + fastkml.data module ------------------- @@ -69,6 +74,14 @@ fastkml.exceptions module :undoc-members: :show-inheritance: +fastkml.features module +----------------------- + +.. automodule:: fastkml.features + :members: + :undoc-members: + :show-inheritance: + fastkml.geometry module ----------------------- @@ -100,7 +113,22 @@ fastkml.kml module :members: :undoc-members: :show-inheritance: - :no-index: StyleUrl + +fastkml.kml\_base module +------------------------ + +.. automodule:: fastkml.kml_base + :members: + :undoc-members: + :show-inheritance: + +fastkml.links module +-------------------- + +.. automodule:: fastkml.links + :members: + :undoc-members: + :show-inheritance: fastkml.mixins module --------------------- @@ -110,6 +138,22 @@ fastkml.mixins module :undoc-members: :show-inheritance: +fastkml.overlays module +----------------------- + +.. automodule:: fastkml.overlays + :members: + :undoc-members: + :show-inheritance: + +fastkml.registry module +----------------------- + +.. automodule:: fastkml.registry + :members: + :undoc-members: + :show-inheritance: + fastkml.styles module --------------------- @@ -134,6 +178,14 @@ fastkml.types module :undoc-members: :show-inheritance: +fastkml.utils module +-------------------- + +.. automodule:: fastkml.utils + :members: + :undoc-members: + :show-inheritance: + fastkml.views module -------------------- diff --git a/docs/index.rst b/docs/index.rst index f9c92016..acdaf036 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,17 +1,8 @@ Welcome to FastKML's documentation! =================================== -``fastkml`` is a library to read, write and manipulate KML files. It aims to keep -it simple and fast (using lxml_ if available). "Fast" refers to the time you -spend to write and read KML files, as well as the time you spend to get -acquainted with the library or to create KML objects. It provides a subset of KML -and is aimed at documents that can be read from multiple clients such as -openlayers and google maps rather than to give you all functionality that KML -on google earth provides. - -For more details about the KML Specification, check out the `KML Reference -`_ on the Google -developers site. +.. include:: ../README.rst + :start-after: inclusion-marker-do-not-remove Rationale --------- @@ -26,18 +17,16 @@ requirements, namely: * It is fully tested and actively maintained. * Geometries are handled in the ``__geo_interface__`` standard. * Minimal dependencies, pure Python. -* If available, lxml_ will be used to increase its speed. +* If available, ``lxml`` will be used to increase its speed. .. toctree:: :maxdepth: 2 quickstart - installing - usage_guide - reference_guide - modules + create_kml_files + working_with_kml + configuration + fastkml contributing - -.. _lxml: https://pypi.python.org/pypi/lxml -.. _tox: https://pypi.python.org/pypi/tox -.. _kml_reference: https://developers.google.com/kml/documentation/kmlreference + alternatives + HISTORY diff --git a/docs/installing.rst b/docs/installing.rst deleted file mode 100644 index f0738c8d..00000000 --- a/docs/installing.rst +++ /dev/null @@ -1,24 +0,0 @@ -Installation -============ - -fastkml works with CPython and Pypy version 3.7+ and is -continually tested for these version. -Jython and IronPython are not tested but *should* work. - -.. image:: https://api.travis-ci.org/cleder/fastkml.png - :target: https://travis-ci.org/cleder/fastkml - -fastkml works on Unix/Linux, OS X, and Windows. - -Install it with ``pip install fastkml``. - -If you use fastkml extensively or need to process big KML files, consider -installing lxml_ as it speeds up processing. - -You can install all requirements for working with fastkml by using pip_ from -the base of the source tree:: - - pip install -r requirements.txt - -.. _lxml: https://pypi.python.org/pypi/lxml -.. _pip: https://pypi.python.org/pypi/pip diff --git a/docs/modules.rst b/docs/modules.rst index 8af3d880..d3cafeb8 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -1,7 +1 @@ -fastkml -======= - -.. toctree:: - :maxdepth: 4 - fastkml diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 2a906d11..34b2355e 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -1,11 +1,208 @@ Quickstart ========== +This quickstart guide will show you how to create a simple KML file from scratch and how +to read a KML file from a string. The example demonstrates organizing geographic data using +nested folders and placemarks - common structures used to group and display locations in +Google Earth and other KML viewers. -:: +Build a KML from Scratch +------------------------ - $ pip install fastkml +Example how to build a simple KML file from the Python interpreter. + +First we import the necessary modules: + +.. code-block:: pycon - # Start working with the library - $ python >>> from fastkml import kml + >>> from pygeoif.geometry import Polygon + +Create a KML object and set the namespace: + +.. code-block:: pycon + >>> k = kml.KML() + >>> ns = "{http://www.opengis.net/kml/2.2}" + +Create a KML Document and add it to the KML root object: + +.. code-block:: pycon + + >>> d = kml.Document(ns=ns, id="docid", name="doc name", description="doc description") + >>> k.append(d) + +Create a KML Folder and add it to the Document: + +.. code-block:: pycon + + >>> f = kml.Folder(ns=ns, id="fid", name="f name", description="f description") + >>> d.append(f) + +Create a KML Folder and nest it in the first Folder: + +.. code-block:: pycon + + >>> nf = kml.Folder( + ... ns=ns, id="nested-fid", name="nested f name", description="nested f description" + ... ) + >>> f.append(nf) + +Create a second KML Folder within the Document: + +.. code-block:: pycon + + >>> f2 = kml.Folder(ns=ns, id="id2", name="name2", description="description2") + >>> d.append(f2) + +Create a KML Placemark with a simple polygon geometry and add it to the second Folder: + +.. code-block:: pycon + + >>> polygon = Polygon([(0, 0, 0), (1, 1, 0), (1, 0, 1)]) + >>> p = kml.Placemark( + ... ns=ns, id="id", name="name", description="description", geometry=polygon + ... ) + >>> f2.append(p) + +Finally, print out the KML object as a string: + +.. code-block:: pycon + + >>> print(k.to_string(prettyprint=True, precision=6)) + + + doc name + doc description + + f name + f description + + nested f name + nested f description + + + + name2 + description2 + + name + description + + + + 0.000000,0.000000,0.000000 1.000000,1.000000,0.000000 1.000000,0.000000,1.000000 0.000000,0.000000,0.000000 + + + + + + + + + + + +Read a KML File/String +---------------------- + +You can create a KML object by reading a KML file from a string + +.. code-block:: pycon + + >>> doc = """ + ... + ... Document.kml + ... 1 + ... + ... + ... Document Feature 1 + ... #exampleStyleDocument + ... + ... -122.371,37.816,0 + ... + ... + ... + ... Document Feature 2 + ... #exampleStyleDocument + ... + ... -122.370,37.817,0 + ... + ... + ... + ... """ + +Read in the KML string + +.. code-block:: pycon + + >>> k = kml.KML.from_string(doc) + +Next we perform some simple sanity checks, such as checking the number of features. + +.. code-block:: pycon + + # This corresponds to the single ``Document`` + >>> len(k.features) + 1 + +Check the number of Placemarks in the Document: + +.. code-block:: pycon + + # (The two Placemarks of the Document) + >>> k.features[0].features # doctest: +ELLIPSIS + [fastkml.features.Placemark... + >>> len(k.features[0].features) + 2 + +Check the Placemarks in the Document: + +.. code-block:: pycon + + # Check specifics of the first Placemark in the Document + >>> k.features[0].features[0] # doctest: +ELLIPSIS + fastkml.features.Placemark(... + >>> k.features[0].features[0].description + >>> k.features[0].features[0].name + 'Document Feature 1' + + # Check specifics of the second Placemark in the Document + >>> k.features[0].features[1].name + 'Document Feature 2' + >>> k.features[0].features[1].name = "ANOTHER NAME" + +Finally, print out the KML object as a string: + +.. code-block:: pycon + + >>> print(k.to_string(prettyprint=True, precision=6)) + + + Document.kml + 1 + + + Document Feature 1 + #exampleStyleDocument + + -122.371000,37.816000,0.000000 + + + + ANOTHER NAME + #exampleStyleDocument + + -122.370000,37.817000,0.000000 + + + + + diff --git a/docs/reference_guide.rst b/docs/reference_guide.rst deleted file mode 100644 index d5915e59..00000000 --- a/docs/reference_guide.rst +++ /dev/null @@ -1,45 +0,0 @@ -=============== -Reference Guide -=============== - -Atom -==== - -.. automodule:: fastkml.atom - :members: - -Base -==== - -.. automodule:: fastkml.base - :members: - -Config -====== - -.. automodule:: fastkml.config - :members: - -Geometry -======== - -.. automodule:: fastkml.geometry - :members: - -GX -== - -.. automodule:: fastkml.gx - :members: - -KML -=== - -.. automodule:: fastkml.kml - :members: - -Styles -====== - -.. automodule:: fastkml.styles - :members: diff --git a/docs/usage_guide.rst b/docs/usage_guide.rst deleted file mode 100644 index 1775894f..00000000 --- a/docs/usage_guide.rst +++ /dev/null @@ -1,174 +0,0 @@ -Usage Guide -=========== - -You can find more examples in the included ``tests/`` directory. - -Build a KML from Scratch ------------------------- - -Example how to build a simple KML file from the Python interpreter. - -.. code-block:: pycon - - # Import the library - >>> from fastkml import kml - >>> from pygeoif.geometry import Polygon - - # Create the root KML object - >>> k = kml.KML() - >>> ns = "{http://www.opengis.net/kml/2.2}" - - # Create a KML Document and add it to the KML root object - >>> d = kml.Document(ns=ns, id="docid", name="doc name", description="doc description") - >>> k.append(d) - - # Create a KML Folder and add it to the Document - >>> f = kml.Folder(ns=ns, id="fid", name="f name", description="f description") - >>> d.append(f) - - # Create a KML Folder and nest it in the first Folder - >>> nf = kml.Folder( - ... ns=ns, id="nested-fid", name="nested f name", description="nested f description" - ... ) - >>> f.append(nf) - - # Create a second KML Folder within the Document - >>> f2 = kml.Folder(ns=ns, id="id2", name="name2", description="description2") - >>> d.append(f2) - - # Create a Placemark with a simple polygon geometry and add it to the - # second folder of the Document - >>> polygon = Polygon([(0, 0, 0), (1, 1, 0), (1, 0, 1)]) - >>> p = kml.Placemark( - ... ns=ns, id="id", name="name", description="description", geometry=polygon - ... ) - >>> f2.append(p) - - # Print out the KML Object as a string - >>> print(k.to_string(prettyprint=True)) - - - doc name - doc description - - f name - f description - - nested f name - nested f description - - - - name2 - description2 - - name - description - - - - 0.000000,0.000000,0.000000 1.000000,1.000000,0.000000 1.000000,0.000000,1.000000 0.000000,0.000000,0.000000 - - - - - - - - - - - -Read a KML File/String ----------------------- - -You can create a KML object by reading a KML file as a string - -.. code-block:: pycon - - # Start by importing the kml module - >>> from fastkml import kml - - # Setup the string which contains the KML file we want to read - >>> doc = """ - ... - ... Document.kml - ... 1 - ... - ... - ... Document Feature 1 - ... #exampleStyleDocument - ... - ... -122.371,37.816,0 - ... - ... - ... - ... Document Feature 2 - ... #exampleStyleDocument - ... - ... -122.370,37.817,0 - ... - ... - ... - ... """ - - # Read in the KML string - >>> k = kml.KML.class_from_string(doc) - - # Next we perform some simple sanity checks - - # Check that the number of features is correct - # This corresponds to the single ``Document`` - >>> len(k.features) - 1 - - # (The two Placemarks of the Document) - >>> k.features[0].features # doctest: +ELLIPSIS - [fastkml.features.Placemark... - >>> len(k.features[0].features) - 2 - - # Check specifics of the first Placemark in the Document - >>> k.features[0].features[0] # doctest: +ELLIPSIS - fastkml.features.Placemark(... - >>> k.features[0].features[0].description - >>> k.features[0].features[0].name - 'Document Feature 1' - - # Check specifics of the second Placemark in the Document - >>> k.features[0].features[1].name - 'Document Feature 2' - >>> k.features[0].features[1].name = "ANOTHER NAME" - - # Verify that we can print back out the KML object as a string - >>> print(k.to_string(prettyprint=True)) - - - Document.kml - 1 - - - Document Feature 1 - #exampleStyleDocument - - -122.371000,37.816000,0.000000 - - - - ANOTHER NAME - #exampleStyleDocument - - -122.370000,37.817000,0.000000 - - - - - diff --git a/docs/working_with_kml.rst b/docs/working_with_kml.rst new file mode 100644 index 00000000..c227bb15 --- /dev/null +++ b/docs/working_with_kml.rst @@ -0,0 +1,238 @@ +Working with KML Files +====================== + +Import the necessary modules: + +.. code-block:: pycon + + >>> from fastkml.utils import find, find_all + >>> from fastkml import KML + >>> from fastkml import Placemark, Point, StyleUrl, Style + >>> from typing import Dict, Any, Optional, Iterable + + +Open a KML file: + +.. code-block:: pycon + + >>> k = KML.parse("docs/Document-clean.kml") + +Extract all placemarks and print their geometries. +The function ``find_all`` recursively searches a KML document for elements of a specific +type and returns an iterator of all matching elements found in the document tree. + +.. code-block:: pycon + + >>> placemarks = list(find_all(k, of_type=Placemark)) + >>> for p in placemarks: + ... print(p.geometry) # doctest: +ELLIPSIS + ... + POINT Z (-123.93563168 49.16716103 5.0) + POLYGON Z ((-123.940449937288 49.16927524669021 17.0, ... + >>> pts = list(find_all(k, of_type=Point)) + >>> for point in pts: + ... print(point.geometry) + ... + POINT Z (-123.93563168 49.16716103 5.0) + POINT Z (-123.1097 49.2774 0.0) + POINT Z (-123.028369 49.26107900000001 0.0) + POINT Z (-123.3215766 49.2760338 0.0) + POINT Z (-123.2643704 49.3301853 0.0) + POINT Z (-123.2477084 49.2890857 0.0) + + + +``find_all`` can also search for arbitrary elements by their attributes, by passing the +attribute name and value as keyword arguments. +``find`` is a shortcut for ``find_all`` that returns the first element found, which is +useful when we know there is only one element that matches the search criteria. + +.. code-block:: pycon + + >>> pm = find(k, name="Vancouver Film Studios") + >>> pm.name + 'Vancouver Film Studios' + >>> pm.get_tag_name() + 'Placemark' + >>> find(k, href="http://www.vancouverfilmstudios.com/") # doctest: +ELLIPSIS + fastkml.atom.Link(ns=... + +We can also use ``find`` to get the parent of the element we found: + +.. code-block:: pycon + + >>> a_link = find(k, href="http://www.vancouverfilmstudios.com/") + >>> find(k, atom_link=a_link) # doctest: +ELLIPSIS + fastkml.features.Placemark(ns=... + +For more targeted searches, we can combine multiple search criteria: + +.. code-block:: pycon + + >>> style_url = StyleUrl(url="#khStyle712") + >>> pm = find(k, of_type=Placemark, name="HBC Bastion", style_url=style_url) + >>> pm.geometry + Point(-123.93563168, 49.16716103, 5.0) + >>> pm.style_url.url + '#khStyle712' + + +Extending FastKML +----------------- + +FastKML is designed to be easily extended. For example, we can add a new object to KML +by subclassing ``fastkml.base.__XMLObject`` or ``fastkml.kml_base._BaseObject`` and +defining the new element's tag name and attributes. +The ```` is an undocumented element that is created in +Google Earth Web that is unsupported by Google Earth Pro, we want to transform it into +a supported element. + +.. code-block:: pycon + + >>> from fastkml.kml_base import _BaseObject + >>> from fastkml import config + >>> class CascadingStyle(_BaseObject): + ... _default_nsid = config.GX + ... def __init__( + ... self, + ... ns: Optional[str] = None, + ... name_spaces: Optional[Dict[str, str]] = None, + ... id: Optional[str] = None, + ... target_id: Optional[str] = None, + ... style: Optional[Style] = None, + ... **kwargs: Any, + ... ) -> None: + ... self.style = style + ... super().__init__(ns, name_spaces, id, target_id, **kwargs) + ... + +We need to register the attributes of the KML object to be able to parse it: + +.. code-block:: pycon + + >>> from fastkml.registry import RegistryItem, registry + >>> from fastkml.helpers import xml_subelement, xml_subelement_kwarg + >>> registry.register( + ... CascadingStyle, + ... RegistryItem( + ... ns_ids=("kml",), + ... attr_name="style", + ... node_name="Style", + ... classes=(Style,), + ... get_kwarg=xml_subelement_kwarg, + ... set_element=xml_subelement, + ... ), + ... ) + +And register the new element with the KML Document object: + +.. code-block:: pycon + + >>> from fastkml import Document + >>> from fastkml.helpers import xml_subelement_list, xml_subelement_list_kwarg + >>> registry.register( + ... Document, + ... RegistryItem( + ... ns_ids=("gx",), + ... attr_name="gx_cascading_style", + ... node_name="CascadingStyle", + ... classes=(CascadingStyle,), + ... get_kwarg=xml_subelement_list_kwarg, + ... set_element=xml_subelement_list, + ... ), + ... ) + +The CascadingStyle object is now part of the KML document and can be accessed like any +other element. +Now we can create a new KML object and confirm that the new element is parsed correctly: + +.. code-block:: pycon + + >>> cs_kml = KML.parse("examples/gx_cascading_style.kml") + >>> cs = find(cs_kml, of_type=CascadingStyle) + >>> cs.style # doctest: +ELLIPSIS + fastkml.styles.Style(... + + +To be able to open the KML file in Google Earth Pro, we need to transform the +CascadingStyle element into a supported Style element. + +.. code-block:: pycon + + >>> document = find(cs_kml, of_type=Document) + >>> for cascading_style in document.gx_cascading_style: + ... kml_style = cascading_style.style + ... kml_style.id = cascading_style.id + ... document.styles.append(kml_style) + ... + >>> document.gx_cascading_style = [] + >>> print(document.to_string(prettyprint=True)) + + Test2 + + + normal + #__managed_style_14CDD4276C14827EFCCB + + + highlight + #__managed_style_25EBAAC82614827EFCCB + + + + + hide + + + 1.2 + + https://earth.google.com/earth/rpc/cc/icon?color=1976d2&id=2000&scale=4 + + + + + 24.0 + + + 80000000 + + + + + hide + + + + https://earth.google.com/earth/rpc/cc/icon?color=1976d2&id=2000&scale=4 + + + + + 16.0 + + + 80000000 + + + + Ort1 + + 10.06256752902339 + 53.57036326842834 + 13.96486261382906 + 0.0 + 0.0 + absolute + 632.584179697442 + + #__managed_style_0D301BCC0014827EFCCB + + + + 10.05998904317019,53.57172202479447,10.32521244530025 10.06072970043745,53.57050957507556,13.60797686155092 10.06170365480513,53.57072597737833,13.60026817081542 10.06094034058923,53.57192922042453,10.47620396741323 10.05998904317019,53.57172202479447,10.32521244530025 + + + + + + diff --git a/examples/CreateKml.py b/examples/CreateKml.py deleted file mode 100644 index 6906825b..00000000 --- a/examples/CreateKml.py +++ /dev/null @@ -1,78 +0,0 @@ -# Import the library -from pygeoif.geometry import Polygon - -from fastkml import kml - -# Create the root KML object -k = kml.KML() -ns = "{http://www.opengis.net/kml/2.2}" - -# Create a KML Document and add it to the KML root object -d = kml.Document(ns, "docid", "doc name", "doc description") -k.append(d) - -# Create a KML Folder and add it to the Document -f = kml.Folder(ns, "fid", "f name", "f description") -d.append(f) - -# Create a KML Folder and nest it in the first Folder -nf = kml.Folder(ns, "nested-fid", "nested f name", "nested f description") -f.append(nf) - -# Create a second KML Folder within the Document -f2 = kml.Folder(ns, "id2", "name2", "description2") -d.append(f2) - -# Create a Placemark with a simple polygon geometry and add it to the -# second folder of the Document -p = kml.Placemark(ns, "id", "name", "description") -p.geometry = Polygon([(0, 0, 0), (1, 1, 0), (1, 0, 1)]) -f2.append(p) - -# Print out the KML Object as a string -print(k.to_string(prettyprint=True)) - -expected = """ - - doc name - doc description - 1 - 0 - - f name - f description - 1 - 0 - - nested f name - nested f description - 1 - 0 - - - - name2 - description2 - 1 - 0 - - name - description - 1 - 0 - - - - - 0.000000,0.000000,0.000000 - 1.000000,1.000000,0.000000 - 1.000000,0.000000,1.000000 - 0.000000,0.000000,0.000000 - - - - - - - -""" diff --git a/examples/README.md b/examples/README.md index f29c96c8..1ed57fa9 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,3 +1,13 @@ -Simply run `UsageExamples.py` to see some ways to use `fastkml` library (which should already be [installed](https://github.com/heltonbiker/fastkml#install)). +# Usage Examples for FastKML -File `KML_Samples.kml` was found in [Google Developers](https://developers.google.com/kml/documentation/KML_Samples.kml) site. +The `KML_Samples.kml` file was found in [Google Developers](https://developers.google.com/kml/documentation/KML_Samples.kml) site. + + +The data for the `shp2kml` examples was obtained from +[Data on CO2 and Greenhouse Gas Emissions by Our World in Data](https://github.com/owid/co2-data) +and the shapefile (Small scale data, 1:110m) from +[Natural Earth](https://www.naturalearthdata.com/). +The data provided in this directory is only a small subset of that data just enough +to create the KML. + +![CO2 Emissions Per Capita](co2_per_capita.jpg) diff --git a/examples/__init__.py b/examples/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/co2_per_capita.jpg b/examples/co2_per_capita.jpg new file mode 100644 index 00000000..429f3ad9 Binary files /dev/null and b/examples/co2_per_capita.jpg differ diff --git a/examples/gx_cascading_style.kml b/examples/gx_cascading_style.kml new file mode 100644 index 00000000..77f32841 --- /dev/null +++ b/examples/gx_cascading_style.kml @@ -0,0 +1,93 @@ + + + + Test2 + + + + + + + + + normal + #__managed_style_14CDD4276C14827EFCCB + + + highlight + #__managed_style_25EBAAC82614827EFCCB + + + + Ort1 + + 10.06256752902339 + 53.57036326842834 + 13.96486261382906 + 0 + 0 + 35 + 632.584179697442 + absolute + + #__managed_style_0D301BCC0014827EFCCB + + + + + 10.05998904317019,53.57172202479447,10.32521244530025 + 10.06072970043745,53.57050957507556,13.60797686155092 + 10.06170365480513,53.57072597737833,13.60026817081542 + 10.06094034058923,53.57192922042453,10.47620396741323 + 10.05998904317019,53.57172202479447,10.32521244530025 + + + + + + + diff --git a/examples/ne_110m_admin_0_countries.README.html b/examples/ne_110m_admin_0_countries.README.html new file mode 100644 index 00000000..3ddf0d42 --- /dev/null +++ b/examples/ne_110m_admin_0_countries.README.html @@ -0,0 +1,547 @@ + + + + + + +Natural Earth » Blog Archive » Admin 0 – Countries - Free vector and raster map data at 1:10m, 1:50m, and 1:110m scales + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+ + +
+

Admin 0 – Countries

+ +
+
+
countries_thumb
+
There are 258 countries in the world. Greenland as separate from Denmark. Most users will want this file instead of sovereign states, though some users will want map units instead when needing to distinguish overseas regions of France.
+
+

Natural Earth shows de facto boundaries by default according to who controls the territory, versus de jure.

+ + +

+
+
+
+

countries_banner

+

About

+

Countries distinguish between metropolitan (homeland) and independent and semi-independent portions of sovereign states. If you want to see the dependent overseas regions broken out (like in ISO codes, see France for example), use map units instead.

+

Each country is coded with a world region that roughly follows the United Nations setup.

+

Includes some thematic data from the United Nations, U.S. Central Intelligence Agency, and elsewhere.

+

Disclaimer

+

Natural Earth Vector draws boundaries of countries according to defacto status. We show who actually controls the situation on the ground. Please feel free to mashup our disputed areas (link) theme to match your particular political outlook.

+

Known Problems

+

None.

+

Version History

+ + +

The master changelog is available on Github »

+
+ + + + +
+ +
+ + + + + +
+ + + + + + +
+
    +
  1. +
    + + + + +

    […] earlier. It’s the result of a conversion of a polygon shapefile of country boundaries (from Natural Earth, a fantastic, public domain, physical/cultural spatial data source) to a raster data […]

    + + +
    +
  2. +
  3. +
    + + + + +

    […] Le mappe sono scaricate da https://www.naturalearthdata.com […]

    + + +
    +
  4. +
  5. +
    + + + + +

    […] Le mappe sono scaricate da https://www.naturalearthdata.com […]

    + + +
    +
  6. +
+
+
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + diff --git a/examples/ne_110m_admin_0_countries.cpg b/examples/ne_110m_admin_0_countries.cpg new file mode 100644 index 00000000..7edc66b0 --- /dev/null +++ b/examples/ne_110m_admin_0_countries.cpg @@ -0,0 +1 @@ +UTF-8 diff --git a/examples/ne_110m_admin_0_countries.dbf b/examples/ne_110m_admin_0_countries.dbf new file mode 100755 index 00000000..e0acd066 Binary files /dev/null and b/examples/ne_110m_admin_0_countries.dbf differ diff --git a/examples/ne_110m_admin_0_countries.prj b/examples/ne_110m_admin_0_countries.prj new file mode 100755 index 00000000..40dd8c6c --- /dev/null +++ b/examples/ne_110m_admin_0_countries.prj @@ -0,0 +1 @@ +GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.017453292519943295]] diff --git a/examples/ne_110m_admin_0_countries.shp b/examples/ne_110m_admin_0_countries.shp new file mode 100755 index 00000000..9318e45c Binary files /dev/null and b/examples/ne_110m_admin_0_countries.shp differ diff --git a/examples/ne_110m_admin_0_countries.shx b/examples/ne_110m_admin_0_countries.shx new file mode 100755 index 00000000..c3728e0d Binary files /dev/null and b/examples/ne_110m_admin_0_countries.shx differ diff --git a/examples/owid-co2-data.csv b/examples/owid-co2-data.csv new file mode 100644 index 00000000..f6f5e1a3 --- /dev/null +++ b/examples/owid-co2-data.csv @@ -0,0 +1,6133 @@ +year,iso_code,co2_per_capita +1995,AFG,0.086 +1995,,4.096 +1995,ALB,0.636 +1995,DZA,3.398 +1995,AND,6.752 +1995,AGO,0.907 +1995,AIA,6.091 +1995,ATA,0 +1995,ATG,3.963 +1995,ARG,3.584 +1995,ARM,1.012 +1995,ABW,9.175 +1995,AUS,16.945 +1995,AUT,8.059 +1995,AZE,4.222 +1995,BHS,6.14 +1995,BHR,24.184 +1995,BGD,0.179 +1995,BRB,4.891 +1995,BLR,5.822 +1995,BEL,12.475 +1995,BLZ,1.854 +1995,BEN,0.158 +1995,BMU,8.931 +1995,BTN,0.472 +1995,BOL,1.15 +1995,BES,4.706 +1995,BIH,0.907 +1995,BWA,1.972 +1995,BRA,1.643 +1995,VGB,4.97 +1995,BRN,19.476 +1995,BGR,6.831 +1995,BFA,0.06 +1995,BDI,0.04 +1995,KHM,0.142 +1995,CMR,0.407 +1995,CAN,16.792 +1995,CPV,0.481 +1995,CALF,0.064 +1995,TCD,0.062 +1995,CHL,2.883 +1995,CHN,2.76 +1995,CXR,0 +1995,COL,1.652 +1995,COM,0.159 +1995,COG,1.176 +1995,COK,2.682 +1995,CRI,1.344 +1995,CIV,0.435 +1995,HRV,3.498 +1995,CUB,2.09 +1995,CUW,30.84 +1995,CYP,6.797 +1995,CZE,12.764 +1995,COD,0.072 +1995,DNK,11.777 +1995,DJI,0.495 +1995,DMA,1.164 +1995,DOM,1.92 +1995,TLS,0.0 +1995,ECU,2.032 +1995,EGY,1.543 +1995,SLV,0.87 +1995,GNQ,2.188 +1995,ERI,0.381 +1995,EST,12.432 +1995,SWZ,1.114 +1995,ETH,0.044 +1995,FRO,13.041 +1995,FJI,0.93 +1995,FIN,11.378 +1995,FRA,6.73 +1995,PYF,2.042 +1995,GAB,4.653 +1995,GMB,0.174 +1995,GEO,0.47 +1995,DEU,11.586 +1995,GHA,0.265 +1995,GRC,8.106 +1995,GRL,9.575 +1995,GRD,1.443 +1995,GTM,0.683 +1995,GIN,0.156 +1995,GNB,0.164 +1995,GUY,2.086 +1995,HTI,0.115 +1995,HND,0.66 +1995,HKG,5.008 +1995,HUN,5.965 +1995,ISL,9.225 +1995,IND,0.789 +1995,IDN,1.122 +1995,IRN,4.429 +1995,IRQ,3.804 +1995,IRL,10.016 +1995,ISR,9.102 +1995,ITA,7.901 +1995,JAM,3.656 +1995,JPN,9.878 +1995,JOR,2.963 +1995,KAZ,10.24 +1995,KEN,0.281 +1995,KIR,0.36 +1995,KWT,33.909 +1995,KGZ,0.981 +1995,LAO,0.136 +1995,LVA,3.554 +1995,LBN,3.095 +1995,ALSO,0.883 +1995,LBR,0.194 +1995,LBY,11.535 +1995,LIE,6.606 +1995,LTU,4.018 +1995,LUX,22.461 +1995,MAC,3.104 +1995,MDG,0.086 +1995,MWI,0.09 +1995,MYS,5.504 +1995,MDV,0.965 +1995,MLI,0.06 +1995,MLT,6.422 +1995,MHL,1.734 +1995,MRT,0.426 +1995,MUS,1.576 +1995,MEX,3.681 +1995,FSM,1.627 +1995,MDA,2.557 +1995,MCO,0 +1995,MNG,3.363 +1995,MNE,2.1 +1995,MSR,3.942 +1995,MAR,1.081 +1995,MOZ,0.073 +1995,MMR,0.161 +1995,NAME,1.009 +1995,NRU,10.28 +1995,NPL,0.104 +1995,NLD,11.213 +1995,NCL,10.33 +1995,NZL,7.622 +1995,NIC,0.583 +1995,NER,0.057 +1995,NGA,0.78 +1995,NIU,3.102 +1995,PRK,3.399 +1995,MKD,3.995 +1995,NOR,8.822 +1995,OMN,8.623 +1995,PAK,0.628 +1995,PLW,11.481 +1995,PSE,0.36 +1995,PAN,1.345 +1995,PNG,0.446 +1995,PRY,0.855 +1995,PER,1.028 +1995,PHL,0.866 +1995,POL,9.443 +1995,PRT,5.419 +1995,QAT,64.326 +1995,ROU,5.579 +1995,RUS,10.903 +1995,RWA,0.08 +1995,SHN,1.726 +1995,KNA,3.012 +1995,LCA,1.804 +1995,SPM,10.812 +1995,VCT,1.123 +1995,WSM,0.649 +1995,SMR,0 +1995,STP,0.358 +1995,SAU,14.195 +1995,SEN,0.397 +1995,SRB,4.903 +1995,SYC,2.647 +1995,SLE,0.052 +1995,SGP,11.441 +1995,SXM,16.265 +1995,SVK,8.251 +1995,SVN,7.711 +1995,SLB,0.508 +1995,SOME,0.081 +1995,ZAF,8.223 +1995,KOR,8.452 +1995,SSD,0.067 +1995,ESP,6.682 +1995,LKA,0.32 +1995,SDN,0.17 +1995,SURE,4.742 +1995,SWE,6.794 +1995,CHE,6.167 +1995,SYR,3.22 +1995,TWN,7.871 +1995,TJK,0.413 +1995,TZA,0.08 +1995,THA,2.586 +1995,TGO,0.274 +1995,TON,0.953 +1995,TO,11.265 +1995,TUN,1.751 +1995,TUR,3.058 +1995,TKM,8.179 +1995,TCA,4.603 +1995,TUV,0.381 +1995,UGA,0.045 +1995,UKR,7.639 +1995,ARE,29.771 +1995,GBR,9.773 +1995,USA,20.422 +1995,URY,1.42 +1995,UZB,4.551 +1995,VUT,0.387 +1995,VAT,0 +1995,VEN,5.642 +1995,VNM,0.408 +1995,WLF,1.29 +1995,YEM,0.687 +1995,ZMB,0.247 +1995,ZWE,1.367 +1996,AFG,0.08 +1996,,4.163 +1996,ALB,0.617 +1996,DZA,3.451 +1996,AND,7.081 +1996,AGO,1.06 +1996,AIA,6.301 +1996,ATA,0 +1996,ATG,4.146 +1996,ARG,3.705 +1996,ARM,0.758 +1996,ABW,9.133 +1996,AUS,17.128 +1996,AUT,8.468 +1996,AZE,3.93 +1996,BHS,5.948 +1996,BHR,25.315 +1996,BGD,0.179 +1996,BRB,5.095 +1996,BLR,5.915 +1996,BEL,12.79 +1996,BLZ,1.467 +1996,BEN,0.191 +1996,BMU,8.815 +1996,BTN,0.555 +1996,BOL,1.254 +1996,BES,4.355 +1996,BIH,1.085 +1996,BWA,1.741 +1996,BRA,1.742 +1996,VGB,5.452 +1996,BRN,19.198 +1996,BGR,6.958 +1996,BFA,0.066 +1996,BDI,0.041 +1996,KHM,0.146 +1996,CMR,0.44 +1996,CAN,17.172 +1996,CPV,0.513 +1996,CALF,0.063 +1996,TCD,0.062 +1996,CHL,3.3 +1996,CHN,2.857 +1996,CXR,0 +1996,COL,1.636 +1996,COM,0.163 +1996,COG,1.433 +1996,COK,2.878 +1996,CRI,1.282 +1996,CIV,0.508 +1996,HRV,3.645 +1996,CUB,2.128 +1996,CUW,29.169 +1996,CYP,7.058 +1996,CZE,13.114 +1996,COD,0.079 +1996,DNK,14.25 +1996,DJI,0.477 +1996,DMA,1.059 +1996,DOM,2.057 +1996,TLS,0.0 +1996,ECU,2.149 +1996,EGY,1.494 +1996,SLV,0.761 +1996,GNQ,2.327 +1996,ERI,0.397 +1996,EST,13.273 +1996,SWZ,0.809 +1996,ETH,0.047 +1996,FRO,13.606 +1996,FJI,0.995 +1996,FIN,12.495 +1996,FRA,6.992 +1996,PYF,1.994 +1996,GAB,5.115 +1996,GMB,0.172 +1996,GEO,0.851 +1996,DEU,11.804 +1996,GHA,0.272 +1996,GRC,8.248 +1996,GRL,10.663 +1996,GRD,1.468 +1996,GTM,0.618 +1996,GIN,0.161 +1996,GNB,0.18 +1996,GUY,2.193 +1996,HTI,0.131 +1996,HND,0.648 +1996,HKG,4.564 +1996,HUN,6.127 +1996,ISL,9.377 +1996,IND,0.838 +1996,IDN,1.257 +1996,IRN,4.463 +1996,IRQ,3.453 +1996,IRL,10.389 +1996,ISR,9.254 +1996,ITA,7.797 +1996,JAM,3.825 +1996,JPN,9.955 +1996,JOR,3.0 +1996,KAZ,9.57 +1996,KEN,0.336 +1996,KIR,0.354 +1996,KWT,30.31 +1996,KGZ,1.211 +1996,LAO,0.149 +1996,LVA,3.635 +1996,LBN,3.161 +1996,ALSO,0.888 +1996,LBR,0.194 +1996,LBY,11.193 +1996,LIE,6.567 +1996,LTU,4.229 +1996,LUX,22.283 +1996,MAC,3.48 +1996,MDG,0.087 +1996,MWI,0.089 +1996,MYS,5.347 +1996,MDV,1.097 +1996,MLI,0.064 +1996,MLT,6.577 +1996,MHL,1.706 +1996,MRT,0.437 +1996,MUS,1.662 +1996,MEX,3.732 +1996,FSM,1.653 +1996,MDA,2.638 +1996,MCO,0 +1996,MNG,3.367 +1996,MNE,2.453 +1996,MSR,4.736 +1996,MAR,1.094 +1996,MOZ,0.068 +1996,MMR,0.166 +1996,NAME,1.044 +1996,NRU,9.893 +1996,NPL,0.103 +1996,NLD,11.732 +1996,NCL,10.7 +1996,NZL,7.86 +1996,NIC,0.592 +1996,NER,0.063 +1996,NGA,0.882 +1996,NIU,3.155 +1996,PRK,2.931 +1996,MKD,4.722 +1996,NOR,9.477 +1996,OMN,8.194 +1996,PAK,0.682 +1996,PLW,11.141 +1996,PSE,0.379 +1996,PAN,1.604 +1996,PNG,0.458 +1996,PRY,0.789 +1996,PER,0.992 +1996,PHL,0.864 +1996,POL,9.825 +1996,PRT,5.131 +1996,QAT,65.039 +1996,ROU,5.761 +1996,RUS,10.705 +1996,RWA,0.069 +1996,SHN,1.741 +1996,KNA,3.059 +1996,LCA,1.904 +1996,SPM,10.817 +1996,VCT,1.154 +1996,WSM,0.705 +1996,SMR,0 +1996,STP,0.352 +1996,SAU,13.566 +1996,SEN,0.414 +1996,SRB,5.733 +1996,SYC,3.103 +1996,SLE,0.053 +1996,SGP,13.147 +1996,SXM,15.305 +1996,SVK,8.216 +1996,SVN,8.037 +1996,SLB,0.512 +1996,SOME,0.077 +1996,ZAF,8.144 +1996,KOR,9.22 +1996,SSD,0.068 +1996,ESP,6.338 +1996,LKA,0.38 +1996,SDN,0.171 +1996,SURE,4.692 +1996,SWE,7.214 +1996,CHE,6.238 +1996,SYR,3.204 +1996,TWN,8.18 +1996,TJK,0.468 +1996,TZA,0.081 +1996,THA,2.883 +1996,TGO,0.285 +1996,TON,0.768 +1996,TO,14.053 +1996,TUN,1.762 +1996,TUR,3.311 +1996,TKM,7.36 +1996,TCA,4.398 +1996,TUV,0.38 +1996,UGA,0.048 +1996,UKR,6.95 +1996,ARE,29.312 +1996,GBR,10.103 +1996,USA,20.867 +1996,URY,1.675 +1996,UZB,4.61 +1996,VUT,0.482 +1996,VAT,0 +1996,VEN,5.239 +1996,VNM,0.473 +1996,WLF,1.536 +1996,YEM,0.68 +1996,ZMB,0.206 +1996,ZWE,1.334 +1997,AFG,0.073 +1997,,4.13 +1997,ALB,0.474 +1997,DZA,2.998 +1997,AND,7.192 +1997,AGO,1.068 +1997,AIA,6.136 +1997,ATA,0 +1997,ATG,4.372 +1997,ARG,3.812 +1997,ARM,0.994 +1997,ABW,9.264 +1997,AUS,17.4 +1997,AUT,8.445 +1997,AZE,3.73 +1997,BHS,5.186 +1997,BHR,26.49 +1997,BGD,0.189 +1997,BRB,5.852 +1997,BLR,5.921 +1997,BEL,12.21 +1997,BLZ,1.789 +1997,BEN,0.192 +1997,BMU,8.697 +1997,BTN,0.713 +1997,BOL,1.44 +1997,BES,4.508 +1997,BIH,2.067 +1997,BWA,1.729 +1997,BRA,1.821 +1997,VGB,5.513 +1997,BRN,19.434 +1997,BGR,6.709 +1997,BFA,0.074 +1997,BDI,0.042 +1997,KHM,0.136 +1997,CMR,0.386 +1997,CAN,17.488 +1997,CPV,0.527 +1997,CALF,0.063 +1997,TCD,0.061 +1997,CHL,3.777 +1997,CHN,2.84 +1997,CXR,0 +1997,COL,1.75 +1997,COM,0.167 +1997,COG,1.68 +1997,COK,2.914 +1997,CRI,1.315 +1997,CIV,0.47 +1997,HRV,3.939 +1997,CUB,2.256 +1997,CUW,30.898 +1997,CYP,7.018 +1997,CZE,12.739 +1997,COD,0.069 +1997,DNK,12.4 +1997,DJI,0.499 +1997,DMA,1.167 +1997,DOM,2.118 +1997,TLS,0.0 +1997,ECU,1.621 +1997,EGY,1.667 +1997,SLV,0.927 +1997,GNQ,3.261 +1997,ERI,0.353 +1997,EST,13.143 +1997,SWZ,1.154 +1997,ETH,0.049 +1997,FRO,13.251 +1997,FJI,0.95 +1997,FIN,12.198 +1997,FRA,6.838 +1997,PYF,2.054 +1997,GAB,4.685 +1997,GMB,0.17 +1997,GEO,0.954 +1997,DEU,11.438 +1997,GHA,0.302 +1997,GRC,8.625 +1997,GRL,11.015 +1997,GRD,1.597 +1997,GTM,0.688 +1997,GIN,0.164 +1997,GNB,0.173 +1997,GUY,2.392 +1997,HTI,0.17 +1997,HND,0.653 +1997,HKG,4.735 +1997,HUN,5.999 +1997,ISL,9.653 +1997,IND,0.856 +1997,IDN,1.368 +1997,IRN,4.279 +1997,IRQ,3.411 +1997,IRL,10.66 +1997,ISR,9.553 +1997,ITA,7.895 +1997,JAM,3.941 +1997,JPN,9.873 +1997,JOR,2.973 +1997,KAZ,9.255 +1997,KEN,0.289 +1997,KIR,0.348 +1997,KWT,32.144 +1997,KGZ,1.174 +1997,LAO,0.154 +1997,LVA,3.474 +1997,LBN,3.676 +1997,ALSO,0.897 +1997,LBR,0.187 +1997,LBY,10.562 +1997,LIE,6.868 +1997,LTU,4.104 +1997,LUX,20.468 +1997,MAC,3.613 +1997,MDG,0.104 +1997,MWI,0.087 +1997,MYS,5.63 +1997,MDV,1.226 +1997,MLI,0.07 +1997,MLT,6.574 +1997,MHL,1.679 +1997,MRT,0.434 +1997,MUS,1.686 +1997,MEX,3.892 +1997,FSM,1.747 +1997,MDA,1.662 +1997,MCO,0 +1997,MNG,3.197 +1997,MNE,2.635 +1997,MSR,4.65 +1997,MAR,1.094 +1997,MOZ,0.071 +1997,MMR,0.169 +1997,NAME,1.045 +1997,NRU,9.878 +1997,NPL,0.114 +1997,NLD,11.215 +1997,NCL,8.72 +1997,NZL,8.284 +1997,NIC,0.636 +1997,NER,0.061 +1997,NGA,0.842 +1997,NIU,3.211 +1997,PRK,2.79 +1997,MKD,4.214 +1997,NOR,9.449 +1997,OMN,8.04 +1997,PAK,0.664 +1997,PLW,10.798 +1997,PSE,0.304 +1997,PAN,1.637 +1997,PNG,0.527 +1997,PRY,0.857 +1997,PER,1.094 +1997,PHL,0.964 +1997,POL,9.564 +1997,PRT,5.392 +1997,QAT,76.612 +1997,ROU,5.301 +1997,RUS,10.019 +1997,RWA,0.063 +1997,SHN,1.758 +1997,KNA,3.271 +1997,LCA,1.909 +1997,SPM,7.425 +1997,VCT,1.154 +1997,WSM,0.698 +1997,SMR,0 +1997,STP,0.346 +1997,SAU,11.118 +1997,SEN,0.352 +1997,SRB,6.16 +1997,SYC,3.59 +1997,SLE,0.047 +1997,SGP,15.087 +1997,SXM,16.13 +1997,SVK,8.218 +1997,SVN,8.296 +1997,SLB,0.517 +1997,SOME,0.069 +1997,ZAF,8.507 +1997,KOR,9.681 +1997,SSD,0.08 +1997,ESP,6.616 +1997,LKA,0.408 +1997,SDN,0.205 +1997,SURE,4.647 +1997,SWE,6.629 +1997,CHE,6.072 +1997,SYR,3.261 +1997,TWN,8.755 +1997,TJK,0.351 +1997,TZA,0.089 +1997,THA,2.971 +1997,TGO,0.191 +1997,TON,0.983 +1997,TO,14.346 +1997,TUN,1.801 +1997,TUR,3.461 +1997,TKM,7.149 +1997,TCA,5.095 +1997,TUV,0.38 +1997,UGA,0.049 +1997,UKR,6.787 +1997,ARE,26.554 +1997,GBR,9.66 +1997,USA,20.882 +1997,URY,1.699 +1997,UZB,4.566 +1997,VUT,0.492 +1997,VAT,0 +1997,VEN,5.629 +1997,VNM,0.598 +1997,WLF,1.525 +1997,YEM,0.711 +1997,ZMB,0.258 +1997,ZWE,1.224 +1998,AFG,0.069 +1998,,4.064 +1998,ALB,0.543 +1998,DZA,3.56 +1998,AND,7.53 +1998,AGO,1.075 +1998,AIA,6.318 +1998,ATA,0 +1998,ATG,4.442 +1998,ARG,3.836 +1998,ARM,1.055 +1998,ABW,9.555 +1998,AUS,17.963 +1998,AUT,8.39 +1998,AZE,3.953 +1998,BHS,6.308 +1998,BHR,27.311 +1998,BGD,0.187 +1998,BRB,5.963 +1998,BLR,5.8 +1998,BEL,12.781 +1998,BLZ,1.645 +1998,BEN,0.169 +1998,BMU,8.582 +1998,BTN,0.686 +1998,BOL,1.358 +1998,BES,0.246 +1998,BIH,2.564 +1998,BWA,2.018 +1998,BRA,1.855 +1998,VGB,5.371 +1998,BRN,17.477 +1998,BGR,6.45 +1998,BFA,0.077 +1998,BDI,0.041 +1998,KHM,0.168 +1998,CMR,0.375 +1998,CAN,17.591 +1998,CPV,0.549 +1998,CALF,0.062 +1998,TCD,0.06 +1998,CHL,3.818 +1998,CHN,2.699 +1998,CXR,0 +1998,COL,1.734 +1998,COM,0.178 +1998,COG,1.317 +1998,COK,2.787 +1998,CRI,1.37 +1998,CIV,0.426 +1998,HRV,4.08 +1998,CUB,2.202 +1998,CUW,1.728 +1998,CYP,7.208 +1998,CZE,12.238 +1998,COD,0.06 +1998,DNK,11.557 +1998,DJI,0.554 +1998,DMA,1.116 +1998,DOM,2.148 +1998,TLS,0.071 +1998,ECU,1.893 +1998,EGY,1.827 +1998,SLV,0.974 +1998,GNQ,3.656 +1998,ERI,0.252 +1998,EST,12.005 +1998,SWZ,1.156 +1998,ETH,0.05 +1998,FRO,13.876 +1998,FJI,0.913 +1998,FIN,11.517 +1998,FRA,7.127 +1998,PYF,2.006 +1998,GAB,5.334 +1998,GMB,0.178 +1998,GEO,1.093 +1998,DEU,11.327 +1998,GHA,0.298 +1998,GRC,9.015 +1998,GRL,10.637 +1998,GRD,1.656 +1998,GTM,0.772 +1998,GIN,0.171 +1998,GNB,0.145 +1998,GUY,2.416 +1998,HTI,0.153 +1998,HND,0.763 +1998,HKG,5.946 +1998,HUN,5.975 +1998,ISL,9.62 +1998,IND,0.857 +1998,IDN,1.176 +1998,IRN,4.783 +1998,IRQ,3.507 +1998,IRL,11.066 +1998,ISR,9.543 +1998,ITA,8.108 +1998,JAM,3.762 +1998,JPN,9.535 +1998,JOR,2.942 +1998,KAZ,9.324 +1998,KEN,0.342 +1998,KIR,0.342 +1998,KWT,29.025 +1998,KGZ,1.227 +1998,LAO,0.156 +1998,LVA,3.374 +1998,LBN,3.897 +1998,ALSO,0.908 +1998,LBR,0.179 +1998,LBY,10.37 +1998,LIE,7.117 +1998,LTU,4.364 +1998,LUX,18.092 +1998,MAC,3.718 +1998,MDG,0.106 +1998,MWI,0.084 +1998,MYS,5.033 +1998,MDV,1.097 +1998,MLI,0.088 +1998,MLT,6.433 +1998,MHL,1.722 +1998,MRT,0.419 +1998,MUS,1.815 +1998,MEX,4.024 +1998,FSM,1.809 +1998,MDA,1.48 +1998,MCO,0 +1998,MNG,3.169 +1998,MNE,2.773 +1998,MSR,4.555 +1998,MAR,1.111 +1998,MOZ,0.066 +1998,MMR,0.181 +1998,NAME,1.047 +1998,NRU,9.512 +1998,NPL,0.094 +1998,NLD,11.209 +1998,NCL,8.507 +1998,NZL,7.837 +1998,NIC,0.682 +1998,NER,0.064 +1998,NGA,0.741 +1998,NIU,3.286 +1998,PRK,2.552 +1998,MKD,4.793 +1998,NOR,9.436 +1998,OMN,8.091 +1998,PAK,0.665 +1998,PLW,10.486 +1998,PSE,0.495 +1998,PAN,2.045 +1998,PNG,0.687 +1998,PRY,0.89 +1998,PER,1.087 +1998,PHL,0.916 +1998,POL,8.835 +1998,PRT,5.813 +1998,QAT,63.49 +1998,ROU,4.732 +1998,RUS,9.922 +1998,RWA,0.061 +1998,SHN,1.778 +1998,KNA,3.394 +1998,LCA,2.079 +1998,SPM,8.635 +1998,VCT,1.412 +1998,WSM,0.792 +1998,SMR,0 +1998,STP,0.341 +1998,SAU,10.412 +1998,SEN,0.36 +1998,SRB,6.497 +1998,SYC,3.726 +1998,SLE,0.049 +1998,SGP,12.263 +1998,SXM,0.897 +1998,SVK,8.16 +1998,SVN,8.067 +1998,SLB,0.53 +1998,SOME,0.063 +1998,ZAF,8.228 +1998,KOR,8.174 +1998,SSD,0.068 +1998,ESP,6.803 +1998,LKA,0.417 +1998,SDN,0.174 +1998,SURE,4.558 +1998,SWE,6.66 +1998,CHE,6.276 +1998,SYR,3.429 +1998,TWN,9.135 +1998,TJK,0.406 +1998,TZA,0.076 +1998,THA,2.587 +1998,TGO,0.273 +1998,TON,0.868 +1998,TO,15.05 +1998,TUN,1.838 +1998,TUR,3.408 +1998,TKM,7.547 +1998,TCA,5.093 +1998,TUV,1.138 +1998,UGA,0.054 +1998,UKR,6.61 +1998,ARE,27.423 +1998,GBR,9.729 +1998,USA,20.785 +1998,URY,1.728 +1998,UZB,4.987 +1998,VUT,0.46 +1998,VAT,0 +1998,VEN,7.248 +1998,VNM,0.621 +1998,WLF,1.515 +1998,YEM,0.729 +1998,ZMB,0.243 +1998,ZWE,1.224 +1999,AFG,0.057 +1999,,4.093 +1999,ALB,0.931 +1999,DZA,3.014 +1999,AND,7.811 +1999,AGO,1.088 +1999,AIA,6.138 +1999,ATA,0 +1999,ATG,4.665 +1999,ARG,4.029 +1999,ARM,0.948 +1999,ABW,9.321 +1999,AUS,18.295 +1999,AUT,8.218 +1999,AZE,3.558 +1999,BHS,6.349 +1999,BHR,26.461 +1999,BGD,0.198 +1999,BRB,6.259 +1999,BLR,5.623 +1999,BEL,12.195 +1999,BLZ,1.496 +1999,BEN,0.215 +1999,BMU,8.409 +1999,BTN,0.676 +1999,BOL,1.296 +1999,BES,1.823 +1999,BIH,2.488 +1999,BWA,1.867 +1999,BRA,1.886 +1999,VGB,5.792 +1999,BRN,18.278 +1999,BGR,5.675 +1999,BFA,0.079 +1999,BDI,0.041 +1999,KHM,0.159 +1999,CMR,0.355 +1999,CAN,17.911 +1999,CPV,0.644 +1999,CALF,0.063 +1999,TCD,0.06 +1999,CHL,4.046 +1999,CHN,2.834 +1999,CXR,0 +1999,COL,1.438 +1999,COM,0.181 +1999,COG,1.298 +1999,COK,3.105 +1999,CRI,1.394 +1999,CIV,0.373 +1999,HRV,4.348 +1999,CUB,2.21 +1999,CUW,13.128 +1999,CYP,7.362 +1999,CZE,11.384 +1999,COD,0.047 +1999,DNK,11.025 +1999,DJI,0.535 +1999,DMA,1.173 +1999,DOM,2.119 +1999,TLS,0.177 +1999,ECU,1.771 +1999,EGY,1.834 +1999,SLV,0.943 +1999,GNQ,3.834 +1999,ERI,0.261 +1999,EST,11.356 +1999,SWZ,1.216 +1999,ETH,0.048 +1999,FRO,13.933 +1999,FJI,0.919 +1999,FIN,11.396 +1999,FRA,7.067 +1999,PYF,2.182 +1999,GAB,4.847 +1999,GMB,0.197 +1999,GEO,0.981 +1999,DEU,10.979 +1999,GHA,0.293 +1999,GRC,8.908 +1999,GRL,10.599 +1999,GRD,1.818 +1999,GTM,0.768 +1999,GIN,0.175 +1999,GNB,0.161 +1999,GUY,2.417 +1999,HTI,0.155 +1999,HND,0.726 +1999,HKG,6.406 +1999,HUN,6.042 +1999,ISL,10.234 +1999,IND,0.913 +1999,IDN,1.384 +1999,IRN,5.769 +1999,IRQ,3.444 +1999,IRL,11.409 +1999,ISR,9.135 +1999,ITA,8.19 +1999,JAM,3.836 +1999,JPN,9.806 +1999,JOR,2.882 +1999,KAZ,7.817 +1999,KEN,0.336 +1999,KIR,0.336 +1999,KWT,29.284 +1999,KGZ,0.952 +1999,LAO,0.158 +1999,LVA,3.181 +1999,LBN,3.866 +1999,ALSO,0.915 +1999,LBR,0.135 +1999,LBY,9.937 +1999,LIE,6.942 +1999,LTU,3.701 +1999,LUX,18.907 +1999,MAC,3.565 +1999,MDG,0.112 +1999,MWI,0.081 +1999,MYS,4.649 +1999,MDV,1.515 +1999,MLI,0.089 +1999,MLT,6.497 +1999,MHL,1.633 +1999,MRT,0.418 +1999,MUS,2.012 +1999,MEX,4.01 +1999,FSM,1.708 +1999,MDA,1.088 +1999,MCO,0 +1999,MNG,3.083 +1999,MNE,1.912 +1999,MSR,6.328 +1999,MAR,1.135 +1999,MOZ,0.067 +1999,MMR,0.198 +1999,NAME,0.935 +1999,NRU,8.803 +1999,NPL,0.132 +1999,NLD,10.805 +1999,NCL,9.467 +1999,NZL,8.208 +1999,NIC,0.712 +1999,NER,0.061 +1999,NGA,0.71 +1999,NIU,3.382 +1999,PRK,2.764 +1999,MKD,4.34 +1999,NOR,9.545 +1999,OMN,9.507 +1999,PAK,0.664 +1999,PLW,10.198 +1999,PSE,0.449 +1999,PAN,1.91 +1999,PNG,0.48 +1999,PRY,0.871 +1999,PER,1.126 +1999,PHL,0.894 +1999,POL,8.585 +1999,PRT,6.531 +1999,QAT,59.102 +1999,ROU,4.039 +1999,RUS,10.141 +1999,RWA,0.063 +1999,SHN,1.798 +1999,KNA,3.594 +1999,LCA,2.13 +1999,SPM,8.698 +1999,VCT,1.446 +1999,WSM,0.744 +1999,SMR,0 +1999,STP,0.336 +1999,SAU,11.018 +1999,SEN,0.38 +1999,SRB,4.496 +1999,SYC,3.857 +1999,SLE,0.039 +1999,SGP,12.404 +1999,SXM,6.774 +1999,SVK,8.008 +1999,SVN,7.76 +1999,SLB,0.516 +1999,SOME,0.058 +1999,ZAF,8.086 +1999,KOR,8.824 +1999,SSD,0.072 +1999,ESP,7.352 +1999,LKA,0.456 +1999,SDN,0.185 +1999,SURE,4.512 +1999,SWE,6.326 +1999,CHE,6.222 +1999,SYR,3.468 +1999,TWN,9.421 +1999,TJK,0.407 +1999,TZA,0.073 +1999,THA,2.678 +1999,TGO,0.38 +1999,TON,1.078 +1999,TO,17.026 +1999,TUN,1.904 +1999,TUR,3.291 +1999,TKM,8.842 +1999,TCA,5.494 +1999,TUV,1.138 +1999,UGA,0.054 +1999,UKR,6.054 +1999,ARE,24.96 +1999,GBR,9.579 +1999,USA,20.789 +1999,URY,2.038 +1999,UZB,5.023 +1999,VUT,0.469 +1999,VAT,0 +1999,VEN,5.969 +1999,VNM,0.621 +1999,WLF,1.755 +1999,YEM,0.811 +1999,ZMB,0.185 +1999,ZWE,1.342 +2000,AFG,0.054 +2000,,4.147 +2000,ALB,0.951 +2000,DZA,2.775 +2000,AND,7.925 +2000,AGO,0.976 +2000,AIA,6.95 +2000,ATA,0 +2000,ATG,4.734 +2000,ARG,3.851 +2000,ARM,1.102 +2000,ABW,26.683 +2000,AUS,18.404 +2000,AUT,8.261 +2000,AZE,3.634 +2000,BHS,6.234 +2000,BHR,27.035 +2000,BGD,0.205 +2000,BRB,6.608 +2000,BLR,5.353 +2000,BEL,12.346 +2000,BLZ,1.646 +2000,BEN,0.21 +2000,BMU,8.415 +2000,BTN,0.676 +2000,BOL,0.982 +2000,BES,4.783 +2000,BIH,3.279 +2000,BWA,2.187 +2000,BRA,1.934 +2000,VGB,5.826 +2000,BRN,15.957 +2000,BGR,5.608 +2000,BFA,0.087 +2000,BDI,0.043 +2000,KHM,0.163 +2000,CMR,0.37 +2000,CAN,18.482 +2000,CPV,0.648 +2000,CALF,0.062 +2000,TCD,0.059 +2000,CHL,3.811 +2000,CHN,2.887 +2000,CXR,0 +2000,COL,1.443 +2000,COM,0.191 +2000,COG,1.367 +2000,COK,3.222 +2000,CRI,1.356 +2000,CIV,0.395 +2000,HRV,4.322 +2000,CUB,2.321 +2000,CUW,35.283 +2000,CYP,7.492 +2000,CZE,12.432 +2000,COD,0.035 +2000,DNK,10.169 +2000,DJI,0.494 +2000,DMA,1.501 +2000,DOM,2.224 +2000,TLS,0.25 +2000,ECU,1.657 +2000,EGY,2.015 +2000,SLV,0.952 +2000,GNQ,3.941 +2000,ERI,0.253 +2000,EST,11.083 +2000,SWZ,1.173 +2000,ETH,0.052 +2000,FRO,15.078 +2000,FJI,0.979 +2000,FIN,11.014 +2000,FRA,6.929 +2000,PYF,2.438 +2000,GAB,4.814 +2000,GMB,0.191 +2000,GEO,1.055 +2000,DEU,11.023 +2000,GHA,0.27 +2000,GRC,9.329 +2000,GRL,11.881 +2000,GRD,1.773 +2000,GTM,0.832 +2000,GIN,0.179 +2000,GNB,0.119 +2000,GUY,2.302 +2000,HTI,0.195 +2000,HND,0.756 +2000,HKG,5.984 +2000,HUN,5.735 +2000,ISL,10.421 +2000,IND,0.923 +2000,IDN,1.314 +2000,IRN,5.558 +2000,IRQ,3.381 +2000,IRL,12.006 +2000,ISR,9.73 +2000,ITA,8.26 +2000,JAM,3.948 +2000,JPN,9.966 +2000,JOR,3.006 +2000,KAZ,9.41 +2000,KEN,0.337 +2000,KIR,0.412 +2000,KWT,28.368 +2000,KGZ,0.927 +2000,LAO,0.177 +2000,LVA,2.96 +2000,LBN,3.58 +2000,ALSO,0.926 +2000,LBR,0.138 +2000,LBY,10.328 +2000,LIE,6.561 +2000,LTU,3.29 +2000,LUX,19.979 +2000,MAC,3.775 +2000,MDG,0.12 +2000,MWI,0.076 +2000,MYS,5.355 +2000,MDV,1.595 +2000,MLI,0.095 +2000,MLT,6.183 +2000,MHL,1.824 +2000,MRT,0.413 +2000,MUS,2.212 +2000,MEX,4.002 +2000,FSM,1.607 +2000,MDA,0.84 +2000,MCO,0 +2000,MNG,3.032 +2000,MNE,2.401 +2000,MSR,4.965 +2000,MAR,1.163 +2000,MOZ,0.074 +2000,MMR,0.224 +2000,NAME,0.882 +2000,NRU,8.103 +2000,NPL,0.124 +2000,NLD,10.819 +2000,NCL,10.022 +2000,NZL,8.364 +2000,NIC,0.726 +2000,NER,0.059 +2000,NGA,0.788 +2000,NIU,3.496 +2000,PRK,2.96 +2000,MKD,4.155 +2000,NOR,9.376 +2000,OMN,10.191 +2000,PAK,0.673 +2000,PLW,10.577 +2000,PSE,0.529 +2000,PAN,1.908 +2000,PNG,0.533 +2000,PRY,0.704 +2000,PER,1.076 +2000,PHL,0.928 +2000,POL,8.245 +2000,PRT,6.37 +2000,QAT,62.411 +2000,ROU,4.258 +2000,RUS,10.073 +2000,RWA,0.064 +2000,SHN,1.817 +2000,KNA,3.785 +2000,LCA,2.182 +2000,SPM,8.724 +2000,VCT,1.288 +2000,WSM,0.777 +2000,SMR,0 +2000,STP,0.331 +2000,SAU,14.031 +2000,SEN,0.402 +2000,SRB,5.665 +2000,SYC,3.935 +2000,SLE,0.059 +2000,SGP,11.939 +2000,SXM,18.108 +2000,SVK,7.651 +2000,SVN,7.586 +2000,SLB,0.52 +2000,SOME,0.055 +2000,ZAF,8.081 +2000,KOR,9.404 +2000,SSD,0.075 +2000,ESP,7.611 +2000,LKA,0.54 +2000,SDN,0.201 +2000,SURE,4.577 +2000,SWE,6.192 +2000,CHE,6.073 +2000,SYR,3.322 +2000,TWN,10.223 +2000,TJK,0.356 +2000,TZA,0.075 +2000,THA,2.654 +2000,TGO,0.266 +2000,TON,0.928 +2000,TO,18.29 +2000,TUN,1.977 +2000,TUR,3.586 +2000,TKM,8.615 +2000,TCA,5.856 +2000,TUV,0.758 +2000,UGA,0.057 +2000,UKR,5.844 +2000,ARE,34.138 +2000,GBR,9.669 +2000,USA,21.282 +2000,URY,1.6 +2000,UZB,4.954 +2000,VUT,0.458 +2000,VAT,0 +2000,VEN,5.847 +2000,VNM,0.683 +2000,WLF,1.739 +2000,YEM,0.844 +2000,ZMB,0.18 +2000,ZWE,1.168 +2001,AFG,0.054 +2001,,4.121 +2001,ALB,1.021 +2001,DZA,2.783 +2001,AND,7.723 +2001,AGO,0.939 +2001,AIA,7.414 +2001,ATA,0 +2001,ATG,4.855 +2001,ARG,3.586 +2001,ARM,1.128 +2001,ABW,26.54 +2001,AUS,18.588 +2001,AUT,8.728 +2001,AZE,3.431 +2001,BHS,5.902 +2001,BHR,21.054 +2001,BGD,0.236 +2001,BRB,6.225 +2001,BLR,5.294 +2001,BEL,12.228 +2001,BLZ,1.802 +2001,BEN,0.234 +2001,BMU,8.54 +2001,BTN,0.637 +2001,BOL,1.0 +2001,BES,4.775 +2001,BIH,3.162 +2001,BWA,2.184 +2001,BRA,1.942 +2001,VGB,6.023 +2001,BRN,15.23 +2001,BGR,6.089 +2001,BFA,0.081 +2001,BDI,0.032 +2001,KHM,0.182 +2001,CMR,0.359 +2001,CAN,18.035 +2001,CPV,0.747 +2001,CALF,0.064 +2001,TCD,0.059 +2001,CHL,3.409 +2001,CHN,2.93 +2001,CXR,0 +2001,COL,1.427 +2001,COM,0.194 +2001,COG,1.284 +2001,COK,3.109 +2001,CRI,1.398 +2001,CIV,0.444 +2001,HRV,4.611 +2001,CUB,2.264 +2001,CUW,36.13 +2001,CYP,7.233 +2001,CZE,12.428 +2001,COD,0.033 +2001,DNK,10.427 +2001,DJI,0.479 +2001,DMA,1.612 +2001,DOM,2.208 +2001,TLS,0.133 +2001,ECU,1.822 +2001,EGY,1.752 +2001,SLV,0.978 +2001,GNQ,4.302 +2001,ERI,0.254 +2001,EST,11.442 +2001,SWZ,1.052 +2001,ETH,0.062 +2001,FRO,16.471 +2001,FJI,1.239 +2001,FIN,12.05 +2001,FRA,6.965 +2001,PYF,2.844 +2001,GAB,4.959 +2001,GMB,0.203 +2001,GEO,0.899 +2001,DEU,11.228 +2001,GHA,0.296 +2001,GRC,9.516 +2001,GRL,10.964 +2001,GRD,1.799 +2001,GTM,0.852 +2001,GIN,0.185 +2001,GNB,0.119 +2001,GUY,2.295 +2001,HTI,0.173 +2001,HND,0.828 +2001,HKG,5.558 +2001,HUN,5.906 +2001,ISL,10.045 +2001,IND,0.918 +2001,IDN,1.46 +2001,IRN,5.792 +2001,IRQ,3.789 +2001,IRL,12.433 +2001,ISR,10.103 +2001,ITA,8.256 +2001,JAM,4.028 +2001,JPN,9.831 +2001,JOR,3.023 +2001,KAZ,9.081 +2001,KEN,0.292 +2001,KIR,0.405 +2001,KWT,29.649 +2001,KGZ,0.774 +2001,LAO,0.193 +2001,LVA,3.178 +2001,LBN,3.738 +2001,ALSO,0.938 +2001,LBR,0.14 +2001,LBY,10.067 +2001,LIE,6.427 +2001,LTU,3.537 +2001,LUX,20.862 +2001,MAC,3.838 +2001,MDG,0.104 +2001,MWI,0.067 +2001,MYS,5.56 +2001,MDV,1.607 +2001,MLI,0.1 +2001,MLT,6.805 +2001,MHL,1.885 +2001,MRT,0.425 +2001,MUS,2.335 +2001,MEX,4.102 +2001,FSM,1.473 +2001,MDA,0.901 +2001,MCO,0 +2001,MNG,3.156 +2001,MNE,2.634 +2001,MSR,5.426 +2001,MAR,1.272 +2001,MOZ,0.085 +2001,MMR,0.192 +2001,NAME,1.084 +2001,NRU,7.762 +2001,NPL,0.13 +2001,NLD,11.084 +2001,NCL,8.304 +2001,NZL,8.839 +2001,NIC,0.755 +2001,NER,0.054 +2001,NGA,0.794 +2001,NIU,3.596 +2001,PRK,3.048 +2001,MKD,4.036 +2001,NOR,9.636 +2001,OMN,9.435 +2001,PAK,0.662 +2001,PLW,10.889 +2001,PSE,0.417 +2001,PAN,2.271 +2001,PNG,0.585 +2001,PRY,0.709 +2001,PER,0.941 +2001,PHL,0.883 +2001,POL,8.109 +2001,PRT,6.282 +2001,QAT,67.494 +2001,ROU,4.541 +2001,RUS,10.364 +2001,RWA,0.064 +2001,SHN,1.841 +2001,KNA,3.822 +2001,LCA,2.236 +2001,SPM,8.747 +2001,VCT,1.58 +2001,WSM,0.829 +2001,SMR,0 +2001,STP,0.351 +2001,SAU,13.706 +2001,SEN,0.428 +2001,SRB,6.227 +2001,SYC,4.238 +2001,SLE,0.076 +2001,SGP,11.938 +2001,SXM,18.493 +2001,SVK,8.04 +2001,SVN,8.223 +2001,SLB,0.532 +2001,SOME,0.055 +2001,ZAF,7.868 +2001,KOR,9.681 +2001,SSD,0.083 +2001,ESP,7.611 +2001,LKA,0.546 +2001,SDN,0.223 +2001,SURE,4.848 +2001,SWE,6.276 +2001,CHE,6.24 +2001,SYR,3.046 +2001,TWN,10.289 +2001,TJK,0.357 +2001,TZA,0.086 +2001,THA,2.706 +2001,TGO,0.225 +2001,TON,0.852 +2001,TO,19.989 +2001,TUN,2.044 +2001,TUR,3.282 +2001,TKM,7.343 +2001,TCA,5.795 +2001,TUV,1.14 +2001,UGA,0.057 +2001,UKR,6.277 +2001,ARE,29.361 +2001,GBR,9.781 +2001,USA,20.695 +2001,URY,1.524 +2001,UZB,4.999 +2001,VUT,0.465 +2001,VAT,0 +2001,VEN,5.246 +2001,VNM,0.764 +2001,WLF,1.725 +2001,YEM,0.892 +2001,ZMB,0.185 +2001,ZWE,1.05 +2002,AFG,0.064 +2002,,4.158 +2002,ALB,1.2 +2002,DZA,2.868 +2002,AND,7.497 +2002,AGO,0.918 +2002,AIA,7.275 +2002,ATA,0 +2002,ATG,5.267 +2002,ARG,3.285 +2002,ARM,0.991 +2002,ABW,26.543 +2002,AUS,18.615 +2002,AUT,8.909 +2002,AZE,3.413 +2002,BHS,6.099 +2002,BHR,21.205 +2002,BGD,0.238 +2002,BRB,6.221 +2002,BLR,5.312 +2002,BEL,12.251 +2002,BLZ,1.689 +2002,BEN,0.284 +2002,BMU,8.96 +2002,BTN,0.673 +2002,BOL,1.169 +2002,BES,4.442 +2002,BIH,3.376 +2002,BWA,2.215 +2002,BRA,1.927 +2002,VGB,6.706 +2002,BRN,13.44 +2002,BGR,5.781 +2002,BFA,0.079 +2002,BDI,0.032 +2002,KHM,0.176 +2002,CMR,0.336 +2002,CAN,18.018 +2002,CPV,0.805 +2002,CALF,0.062 +2002,TCD,0.06 +2002,CHL,3.494 +2002,CHN,3.203 +2002,CXR,0 +2002,COL,1.382 +2002,COM,0.19 +2002,COG,0.817 +2002,COK,2.678 +2002,CRI,1.515 +2002,CIV,0.421 +2002,HRV,4.882 +2002,CUB,2.285 +2002,CUW,34.647 +2002,CYP,7.299 +2002,CZE,12.109 +2002,COD,0.034 +2002,DNK,10.329 +2002,DJI,0.506 +2002,DMA,1.502 +2002,DOM,2.382 +2002,TLS,0.288 +2002,ECU,1.899 +2002,EGY,1.736 +2002,SLV,1.01 +2002,GNQ,3.662 +2002,ERI,0.236 +2002,EST,11.144 +2002,SWZ,1.029 +2002,ETH,0.062 +2002,FRO,15.489 +2002,FJI,1.035 +2002,FIN,12.506 +2002,FRA,6.845 +2002,PYF,2.8 +2002,GAB,4.659 +2002,GMB,0.195 +2002,GEO,0.824 +2002,DEU,11.036 +2002,GHA,0.312 +2002,GRC,9.462 +2002,GRL,10.251 +2002,GRD,1.896 +2002,GTM,0.872 +2002,GIN,0.189 +2002,GNB,0.12 +2002,GUY,2.255 +2002,HTI,0.206 +2002,HND,0.847 +2002,HKG,5.765 +2002,HUN,5.822 +2002,ISL,10.398 +2002,IND,0.93 +2002,IDN,1.401 +2002,IRN,5.843 +2002,IRQ,3.677 +2002,IRL,11.833 +2002,ISR,9.329 +2002,ITA,8.37 +2002,JAM,3.867 +2002,JPN,10.045 +2002,JOR,3.113 +2002,KAZ,10.27 +2002,KEN,0.241 +2002,KIR,0.397 +2002,KWT,29.299 +2002,KGZ,0.978 +2002,LAO,0.208 +2002,LVA,3.233 +2002,LBN,3.653 +2002,ALSO,0.954 +2002,LBR,0.141 +2002,LBY,9.717 +2002,LIE,6.525 +2002,LTU,3.603 +2002,LUX,22.382 +2002,MAC,3.373 +2002,MDG,0.072 +2002,MWI,0.073 +2002,MYS,5.469 +2002,MDV,2.031 +2002,MLI,0.101 +2002,MLT,6.788 +2002,MHL,2.016 +2002,MRT,0.443 +2002,MUS,2.337 +2002,MEX,4.068 +2002,FSM,1.341 +2002,MDA,0.977 +2002,MCO,0 +2002,MNG,3.286 +2002,MNE,2.791 +2002,MSR,10.294 +2002,MAR,1.273 +2002,MOZ,0.082 +2002,MMR,0.201 +2002,NAME,0.947 +2002,NRU,7.416 +2002,NPL,0.102 +2002,NLD,10.99 +2002,NCL,10.383 +2002,NZL,8.748 +2002,NIC,0.759 +2002,NER,0.055 +2002,NGA,0.694 +2002,NIU,3.682 +2002,PRK,2.904 +2002,MKD,3.794 +2002,NOR,9.376 +2002,OMN,11.167 +2002,PAK,0.692 +2002,PLW,10.692 +2002,PSE,0.349 +2002,PAN,1.865 +2002,PNG,0.622 +2002,PRY,0.73 +2002,PER,0.931 +2002,PHL,0.864 +2002,POL,7.921 +2002,PRT,6.669 +2002,QAT,63.19 +2002,ROU,4.586 +2002,RUS,10.35 +2002,RWA,0.062 +2002,SHN,1.871 +2002,KNA,4.274 +2002,LCA,2.219 +2002,SPM,9.359 +2002,VCT,1.647 +2002,WSM,0.864 +2002,SMR,0 +2002,STP,0.391 +2002,SAU,14.674 +2002,SEN,0.436 +2002,SRB,6.608 +2002,SYC,4.222 +2002,SLE,0.098 +2002,SGP,11.275 +2002,SXM,17.727 +2002,SVK,7.81 +2002,SVN,8.35 +2002,SLB,0.545 +2002,SOME,0.059 +2002,ZAF,7.48 +2002,KOR,10.06 +2002,SSD,0.101 +2002,ESP,8.003 +2002,LKA,0.573 +2002,SDN,0.276 +2002,SURE,3.147 +2002,SWE,6.351 +2002,CHE,5.973 +2002,SYR,2.351 +2002,TWN,10.562 +2002,TJK,0.287 +2002,TZA,0.096 +2002,THA,2.875 +2002,TGO,0.25 +2002,TON,0.988 +2002,TO,21.324 +2002,TUN,2.049 +2002,TUR,3.352 +2002,TKM,6.412 +2002,TCA,7.285 +2002,TUV,1.141 +2002,UGA,0.057 +2002,UKR,6.168 +2002,ARE,23.296 +2002,GBR,9.439 +2002,USA,20.622 +2002,URY,1.378 +2002,UZB,5.183 +2002,VUT,0.417 +2002,VAT,0 +2002,VEN,6.48 +2002,VNM,0.862 +2002,WLF,1.708 +2002,YEM,0.843 +2002,ZMB,0.187 +2002,ZWE,0.993 +2003,AFG,0.069 +2003,,4.324 +2003,ALB,1.391 +2003,DZA,2.95 +2003,AND,7.236 +2003,AGO,0.965 +2003,AIA,7.477 +2003,ATA,0 +2003,ATG,5.489 +2003,ARG,3.501 +2003,ARM,1.12 +2003,ABW,27.624 +2003,AUS,18.754 +2003,AUT,9.532 +2003,AZE,3.603 +2003,BHS,6.116 +2003,BHR,21.547 +2003,BGD,0.245 +2003,BRB,6.353 +2003,BLR,5.473 +2003,BEL,12.329 +2003,BLZ,1.638 +2003,BEN,0.313 +2003,BMU,8.971 +2003,BTN,0.593 +2003,BOL,1.211 +2003,BES,4.263 +2003,BIH,3.432 +2003,BWA,2.094 +2003,BRA,1.887 +2003,VGB,6.661 +2003,BRN,15.811 +2003,BGR,6.375 +2003,BFA,0.082 +2003,BDI,0.023 +2003,KHM,0.186 +2003,CMR,0.349 +2003,CAN,18.388 +2003,CPV,0.862 +2003,CALF,0.056 +2003,TCD,0.096 +2003,CHL,3.474 +2003,CHN,3.756 +2003,CXR,0 +2003,COL,1.397 +2003,COM,0.238 +2003,COG,0.951 +2003,COK,3.154 +2003,CRI,1.572 +2003,CIV,0.293 +2003,HRV,5.203 +2003,CUB,2.316 +2003,CUW,34.252 +2003,CYP,7.557 +2003,CZE,12.448 +2003,COD,0.039 +2003,DNK,11.238 +2003,DJI,0.523 +2003,DMA,1.713 +2003,DOM,2.393 +2003,TLS,0.268 +2003,ECU,2.035 +2003,EGY,1.974 +2003,SLV,1.068 +2003,GNQ,3.968 +2003,ERI,0.268 +2003,EST,12.595 +2003,SWZ,0.948 +2003,ETH,0.067 +2003,FRO,15.455 +2003,FJI,1.208 +2003,FIN,13.937 +2003,FRA,6.906 +2003,PYF,2.965 +2003,GAB,4.894 +2003,GMB,0.189 +2003,GEO,0.929 +2003,DEU,11.062 +2003,GHA,0.313 +2003,GRC,9.817 +2003,GRL,11.442 +2003,GRD,1.988 +2003,GTM,0.831 +2003,GIN,0.194 +2003,GNB,0.148 +2003,GUY,2.442 +2003,HTI,0.193 +2003,HND,0.928 +2003,HKG,6.274 +2003,HUN,6.114 +2003,ISL,10.303 +2003,IND,0.948 +2003,IDN,1.521 +2003,IRN,5.99 +2003,IRQ,3.741 +2003,IRL,11.54 +2003,ISR,9.638 +2003,ITA,8.637 +2003,JAM,4.024 +2003,JPN,10.096 +2003,JOR,3.147 +2003,KAZ,11.407 +2003,KEN,0.198 +2003,KIR,0.427 +2003,KWT,29.663 +2003,KGZ,1.059 +2003,LAO,0.215 +2003,LVA,3.368 +2003,LBN,4.113 +2003,ALSO,0.969 +2003,LBR,0.151 +2003,LBY,10.009 +2003,LIE,6.741 +2003,LTU,3.649 +2003,LUX,23.158 +2003,MAC,3.311 +2003,MDG,0.095 +2003,MWI,0.076 +2003,MYS,6.2 +2003,MDV,1.701 +2003,MLI,0.101 +2003,MLT,7.276 +2003,MHL,1.949 +2003,MRT,0.447 +2003,MUS,2.463 +2003,MEX,4.258 +2003,FSM,1.442 +2003,MDA,1.076 +2003,MCO,0 +2003,MNG,3.152 +2003,MNE,2.988 +2003,MSR,7.876 +2003,MAR,1.233 +2003,MOZ,0.097 +2003,MMR,0.213 +2003,NAME,0.983 +2003,NRU,6.363 +2003,NPL,0.109 +2003,NLD,11.155 +2003,NCL,11.613 +2003,NZL,9.015 +2003,NIC,0.814 +2003,NER,0.058 +2003,NGA,0.752 +2003,NIU,1.877 +2003,PRK,2.949 +2003,MKD,4.118 +2003,NOR,9.616 +2003,OMN,14.129 +2003,PAK,0.704 +2003,PLW,10.677 +2003,PSE,0.378 +2003,PAN,1.914 +2003,PNG,0.666 +2003,PRY,0.753 +2003,PER,0.893 +2003,PHL,0.847 +2003,POL,8.258 +2003,PRT,6.154 +2003,QAT,62.141 +2003,ROU,4.833 +2003,RUS,10.604 +2003,RWA,0.059 +2003,SHN,1.905 +2003,KNA,4.259 +2003,LCA,2.314 +2003,SPM,10.001 +2003,VCT,1.749 +2003,WSM,0.84 +2003,SMR,0 +2003,STP,0.429 +2003,SAU,14.427 +2003,SEN,0.472 +2003,SRB,7.077 +2003,SYC,4.075 +2003,SLE,0.1 +2003,SGP,11.697 +2003,SXM,17.567 +2003,SVK,7.871 +2003,SVN,8.179 +2003,SLB,0.556 +2003,SOME,0.057 +2003,ZAF,8.407 +2003,KOR,10.206 +2003,SSD,0.108 +2003,ESP,7.964 +2003,LKA,0.567 +2003,SDN,0.301 +2003,SURE,3.052 +2003,SWE,6.393 +2003,CHE,6.09 +2003,SYR,3.122 +2003,TWN,10.987 +2003,TJK,0.31 +2003,TZA,0.099 +2003,THA,2.95 +2003,TGO,0.333 +2003,TON,1.123 +2003,TO,23.931 +2003,TUN,2.074 +2003,TUR,3.541 +2003,TKM,8.524 +2003,TCA,7.239 +2003,TUV,1.134 +2003,UGA,0.057 +2003,UKR,6.454 +2003,ARE,27.913 +2003,GBR,9.583 +2003,USA,20.646 +2003,URY,1.368 +2003,UZB,5.03 +2003,VUT,0.407 +2003,VAT,0 +2003,VEN,5.983 +2003,VNM,0.948 +2003,WLF,1.703 +2003,YEM,0.915 +2003,ZMB,0.191 +2003,ZWE,0.879 +2004,AFG,0.053 +2004,,4.42 +2004,ALB,1.364 +2004,DZA,2.821 +2004,AND,7.285 +2004,AGO,0.906 +2004,AIA,8.58 +2004,ATA,0 +2004,ATG,5.708 +2004,ARG,4.053 +2004,ARM,1.204 +2004,ABW,27.963 +2004,AUS,19.216 +2004,AUT,9.509 +2004,AZE,3.77 +2004,BHS,5.97 +2004,BHR,21.457 +2004,BGD,0.259 +2004,BRB,6.406 +2004,BLR,5.826 +2004,BEL,12.315 +2004,BLZ,1.441 +2004,BEN,0.329 +2004,BMU,9.283 +2004,BTN,0.474 +2004,BOL,1.197 +2004,BES,4.288 +2004,BIH,3.732 +2004,BWA,2.095 +2004,BRA,1.957 +2004,VGB,6.767 +2004,BRN,15.648 +2004,BGR,6.297 +2004,BFA,0.082 +2004,BDI,0.028 +2004,KHM,0.188 +2004,CMR,0.339 +2004,CAN,18.165 +2004,CPV,0.873 +2004,CALF,0.053 +2004,TCD,0.094 +2004,CHL,3.698 +2004,CHN,4.023 +2004,CXR,0 +2004,COL,1.312 +2004,COM,0.252 +2004,COG,1.002 +2004,COK,3.631 +2004,CRI,1.609 +2004,CIV,0.398 +2004,HRV,5.15 +2004,CUB,2.247 +2004,CUW,35.449 +2004,CYP,7.646 +2004,CZE,12.503 +2004,COD,0.037 +2004,DNK,10.174 +2004,DJI,0.497 +2004,DMA,2.083 +2004,DOM,1.917 +2004,TLS,0.525 +2004,ECU,2.162 +2004,EGY,1.968 +2004,SLV,1.036 +2004,GNQ,7.427 +2004,ERI,0.278 +2004,EST,12.718 +2004,SWZ,0.939 +2004,ETH,0.069 +2004,FRO,15.569 +2004,FJI,1.547 +2004,FIN,13.186 +2004,FRA,6.882 +2004,PYF,2.88 +2004,GAB,4.507 +2004,GMB,0.198 +2004,GEO,1.071 +2004,DEU,10.898 +2004,GHA,0.294 +2004,GRC,9.85 +2004,GRL,11.241 +2004,GRD,1.873 +2004,GTM,0.87 +2004,GIN,0.197 +2004,GNB,0.15 +2004,GUY,2.534 +2004,HTI,0.184 +2004,HND,0.974 +2004,HKG,6.011 +2004,HUN,5.969 +2004,ISL,10.637 +2004,IND,0.99 +2004,IDN,1.518 +2004,IRN,6.338 +2004,IRQ,4.049 +2004,IRL,11.45 +2004,ISR,8.896 +2004,ITA,8.67 +2004,JAM,3.969 +2004,JPN,10.047 +2004,JOR,3.375 +2004,KAZ,12.006 +2004,KEN,0.218 +2004,KIR,0.419 +2004,KWT,30.476 +2004,KGZ,1.131 +2004,LAO,0.222 +2004,LVA,3.416 +2004,LBN,3.762 +2004,ALSO,0.998 +2004,LBR,0.175 +2004,LBY,9.996 +2004,LIE,6.683 +2004,LTU,3.873 +2004,LUX,25.823 +2004,MAC,3.606 +2004,MDG,0.099 +2004,MWI,0.073 +2004,MYS,6.698 +2004,MDV,2.207 +2004,MLI,0.11 +2004,MLT,6.969 +2004,MHL,2.153 +2004,MRT,0.47 +2004,MUS,2.47 +2004,MEX,4.221 +2004,FSM,1.282 +2004,MDA,1.14 +2004,MCO,0 +2004,MNG,3.35 +2004,MNE,3.227 +2004,MSR,10.173 +2004,MAR,1.406 +2004,MOZ,0.095 +2004,MMR,0.265 +2004,NAME,1.016 +2004,NRU,6.368 +2004,NPL,0.099 +2004,NLD,11.224 +2004,NCL,10.538 +2004,NZL,8.78 +2004,NIC,0.813 +2004,NER,0.059 +2004,NGA,0.693 +2004,NIU,1.914 +2004,PRK,2.995 +2004,MKD,3.963 +2004,NOR,9.633 +2004,OMN,11.756 +2004,PAK,0.76 +2004,PLW,10.848 +2004,PSE,0.634 +2004,PAN,1.761 +2004,PNG,0.746 +2004,PRY,0.75 +2004,PER,1.016 +2004,PHL,0.861 +2004,POL,8.389 +2004,PRT,6.409 +2004,QAT,60.827 +2004,ROU,4.851 +2004,RUS,10.696 +2004,RWA,0.059 +2004,SHN,1.941 +2004,KNA,4.56 +2004,LCA,2.454 +2004,SPM,9.477 +2004,VCT,1.952 +2004,WSM,0.877 +2004,SMR,0 +2004,STP,0.465 +2004,SAU,16.948 +2004,SEN,0.49 +2004,SRB,7.651 +2004,SYC,4.367 +2004,SLE,0.094 +2004,SGP,10.973 +2004,SXM,18.248 +2004,SVK,7.961 +2004,SVN,8.366 +2004,SLB,0.582 +2004,SOME,0.055 +2004,ZAF,9.253 +2004,KOR,10.296 +2004,SSD,0.13 +2004,ESP,8.217 +2004,LKA,0.623 +2004,SDN,0.376 +2004,SURE,3.047 +2004,SWE,6.277 +2004,CHE,6.128 +2004,SYR,2.836 +2004,TWN,11.359 +2004,TJK,0.376 +2004,TZA,0.11 +2004,THA,3.175 +2004,TGO,0.313 +2004,TON,1.046 +2004,TO,24.032 +2004,TUN,2.147 +2004,TUR,3.611 +2004,TKM,10.24 +2004,TCA,7.041 +2004,TUV,1.12 +2004,UGA,0.059 +2004,UKR,6.572 +2004,ARE,27.972 +2004,GBR,9.558 +2004,USA,20.795 +2004,URY,1.679 +2004,UZB,4.921 +2004,VUT,0.276 +2004,VAT,0 +2004,VEN,5.444 +2004,VNM,1.081 +2004,WLF,1.724 +2004,YEM,0.964 +2004,ZMB,0.188 +2004,ZWE,0.775 +2005,AFG,0.077 +2005,,4.512 +2005,ALB,1.405 +2005,DZA,3.368 +2005,AND,7.205 +2005,AGO,0.791 +2005,AIA,9.048 +2005,ATA,0 +2005,ATG,6.009 +2005,ARG,4.126 +2005,ARM,1.436 +2005,ABW,28.771 +2005,AUS,19.146 +2005,AUT,9.614 +2005,AZE,3.95 +2005,BHS,5.478 +2005,BHR,21.957 +2005,BGD,0.267 +2005,BRB,6.54 +2005,BLR,5.97 +2005,BEL,11.945 +2005,BLZ,1.503 +2005,BEN,0.306 +2005,BMU,9.25 +2005,BTN,0.597 +2005,BOL,1.313 +2005,BES,4.08 +2005,BIH,3.919 +2005,BWA,2.162 +2005,BRA,1.951 +2005,VGB,7.01 +2005,BRN,14.803 +2005,BGR,6.474 +2005,BFA,0.081 +2005,BDI,0.021 +2005,KHM,0.209 +2005,CMR,0.314 +2005,CAN,17.841 +2005,CPV,0.907 +2005,CALF,0.051 +2005,TCD,0.093 +2005,CHL,3.779 +2005,CHN,4.508 +2005,CXR,0 +2005,COL,1.425 +2005,COM,0.241 +2005,COG,1.185 +2005,COK,4.107 +2005,CRI,1.558 +2005,CIV,0.399 +2005,HRV,5.269 +2005,CUB,2.331 +2005,CUW,34.614 +2005,CYP,7.673 +2005,CZE,12.227 +2005,COD,0.04 +2005,DNK,9.48 +2005,DJI,0.498 +2005,DMA,2.187 +2005,DOM,1.958 +2005,TLS,0.331 +2005,ECU,2.221 +2005,EGY,2.127 +2005,SLV,1.043 +2005,GNQ,7.179 +2005,ERI,0.272 +2005,EST,12.621 +2005,SWZ,0.947 +2005,ETH,0.064 +2005,FRO,14.94 +2005,FJI,1.237 +2005,FIN,10.874 +2005,FRA,6.877 +2005,PYF,3.055 +2005,GAB,4.267 +2005,GMB,0.194 +2005,GEO,1.266 +2005,DEU,10.657 +2005,GHA,0.272 +2005,GRC,10.248 +2005,GRL,11.309 +2005,GRD,1.96 +2005,GTM,0.922 +2005,GIN,0.2 +2005,GNB,0.154 +2005,GUY,2.132 +2005,HTI,0.189 +2005,HND,0.904 +2005,HKG,6.304 +2005,HUN,5.996 +2005,ISL,10.027 +2005,IND,1.027 +2005,IDN,1.519 +2005,IRN,6.583 +2005,IRQ,3.973 +2005,IRL,11.685 +2005,ISR,8.419 +2005,ITA,8.631 +2005,JAM,3.891 +2005,JPN,10.095 +2005,JOR,3.599 +2005,KAZ,12.777 +2005,KEN,0.239 +2005,KIR,0.485 +2005,KWT,33.294 +2005,KGZ,1.063 +2005,LAO,0.228 +2005,LVA,3.498 +2005,LBN,3.586 +2005,ALSO,1.017 +2005,LBR,0.199 +2005,LBY,10.392 +2005,LIE,6.613 +2005,LTU,4.109 +2005,LUX,25.985 +2005,MAC,3.742 +2005,MDG,0.093 +2005,MWI,0.067 +2005,MYS,6.556 +2005,MDV,1.957 +2005,MLI,0.11 +2005,MLT,6.476 +2005,MHL,2.09 +2005,MRT,0.477 +2005,MUS,2.618 +2005,MEX,4.397 +2005,FSM,1.057 +2005,MDA,1.234 +2005,MCO,0 +2005,MNG,3.327 +2005,MNE,2.766 +2005,MSR,8.544 +2005,MAR,1.468 +2005,MOZ,0.088 +2005,MMR,0.242 +2005,NAME,1.178 +2005,NRU,6.026 +2005,NPL,0.114 +2005,NLD,10.937 +2005,NCL,11.603 +2005,NZL,9.055 +2005,NIC,0.783 +2005,NER,0.05 +2005,NGA,0.722 +2005,NIU,1.948 +2005,PRK,3.112 +2005,MKD,4.135 +2005,NOR,9.357 +2005,OMN,12.562 +2005,PAK,0.772 +2005,PLW,11.073 +2005,PSE,0.774 +2005,PAN,2.109 +2005,PNG,0.766 +2005,PRY,0.69 +2005,PER,1.077 +2005,PHL,0.85 +2005,POL,8.367 +2005,PRT,6.617 +2005,QAT,56.027 +2005,ROU,4.828 +2005,RUS,10.866 +2005,RWA,0.057 +2005,SHN,1.978 +2005,KNA,4.232 +2005,LCA,2.348 +2005,SPM,10.153 +2005,VCT,1.962 +2005,WSM,0.893 +2005,SMR,0 +2005,STP,0.476 +2005,SAU,16.512 +2005,SEN,0.505 +2005,SRB,6.594 +2005,SYC,4.436 +2005,SLE,0.075 +2005,SGP,9.572 +2005,SXM,17.908 +2005,SVK,7.961 +2005,SVN,8.446 +2005,SLB,0.585 +2005,SOME,0.054 +2005,ZAF,8.491 +2005,KOR,10.413 +2005,SSD,0.119 +2005,ESP,8.432 +2005,LKA,0.607 +2005,SDN,0.356 +2005,SURE,3.077 +2005,SWE,5.952 +2005,CHE,6.163 +2005,SYR,2.731 +2005,TWN,11.689 +2005,TJK,0.352 +2005,TZA,0.136 +2005,THA,3.256 +2005,TGO,0.301 +2005,TON,1.075 +2005,TO,27.922 +2005,TUN,2.171 +2005,TUR,3.855 +2005,TKM,9.81 +2005,TCA,7.932 +2005,TUV,1.107 +2005,UGA,0.072 +2005,UKR,6.683 +2005,ARE,26.723 +2005,GBR,9.445 +2005,USA,20.658 +2005,URY,1.726 +2005,UZB,4.576 +2005,VUT,0.269 +2005,VAT,0 +2005,VEN,5.561 +2005,VNM,1.153 +2005,WLF,2.011 +2005,YEM,1.016 +2005,ZMB,0.194 +2005,ZWE,0.875 +2006,AFG,0.085 +2006,,4.608 +2006,ALB,1.302 +2006,DZA,3.167 +2006,AND,6.804 +2006,AGO,0.85 +2006,AIA,9.793 +2006,ATA,0 +2006,ATG,6.385 +2006,ARG,4.413 +2006,ARM,1.454 +2006,ABW,28.394 +2006,AUS,19.174 +2006,AUT,9.291 +2006,AZE,3.939 +2006,BHS,5.257 +2006,BHR,20.001 +2006,BGD,0.292 +2006,BRB,6.564 +2006,BLR,6.253 +2006,BEL,11.705 +2006,BLZ,1.535 +2006,BEN,0.399 +2006,BMU,10.337 +2006,BTN,0.584 +2006,BOL,1.292 +2006,BES,4.086 +2006,BIH,4.284 +2006,BWA,2.143 +2006,BRA,1.954 +2006,VGB,7.375 +2006,BRN,19.162 +2006,BGR,6.675 +2006,BFA,0.095 +2006,BDI,0.024 +2006,KHM,0.222 +2006,CMR,0.309 +2006,CAN,17.478 +2006,CPV,0.991 +2006,CALF,0.053 +2006,TCD,0.093 +2006,CHL,3.921 +2006,CHN,4.946 +2006,CXR,0 +2006,COL,1.457 +2006,COM,0.273 +2006,COG,1.255 +2006,COK,4.332 +2006,CRI,1.6 +2006,CIV,0.355 +2006,HRV,5.33 +2006,CUB,2.382 +2006,CUW,35.497 +2006,CYP,7.755 +2006,CZE,12.281 +2006,COD,0.04 +2006,DNK,10.902 +2006,DJI,0.489 +2006,DMA,2.078 +2006,DOM,2.059 +2006,TLS,0.318 +2006,ECU,2.119 +2006,EGY,2.222 +2006,SLV,1.113 +2006,GNQ,6.827 +2006,ERI,0.191 +2006,EST,12.211 +2006,SWZ,0.942 +2006,ETH,0.067 +2006,FRO,14.023 +2006,FJI,1.362 +2006,FIN,12.983 +2006,FRA,6.67 +2006,PYF,2.985 +2006,GAB,3.689 +2006,GMB,0.203 +2006,GEO,1.549 +2006,DEU,10.81 +2006,GHA,0.369 +2006,GRC,10.122 +2006,GRL,11.663 +2006,GRD,2.079 +2006,GTM,0.908 +2006,GIN,0.203 +2006,GNB,0.153 +2006,GUY,1.991 +2006,HTI,0.189 +2006,HND,0.994 +2006,HKG,6.022 +2006,HUN,5.937 +2006,ISL,10.378 +2006,IND,1.102 +2006,IDN,1.495 +2006,IRN,6.96 +2006,IRQ,3.41 +2006,IRL,11.241 +2006,ISR,9.097 +2006,ITA,8.505 +2006,JAM,4.304 +2006,JPN,9.907 +2006,JOR,3.379 +2006,KAZ,13.94 +2006,KEN,0.259 +2006,KIR,0.476 +2006,KWT,32.177 +2006,KGZ,1.041 +2006,LAO,0.296 +2006,LVA,3.771 +2006,LBN,3.134 +2006,ALSO,1.031 +2006,LBR,0.195 +2006,LBY,9.332 +2006,LIE,6.62 +2006,LTU,4.272 +2006,LUX,25.214 +2006,MAC,3.227 +2006,MDG,0.087 +2006,MWI,0.065 +2006,MYS,6.419 +2006,MDV,2.366 +2006,MLI,0.113 +2006,MLT,6.487 +2006,MHL,2.23 +2006,MRT,0.503 +2006,MUS,2.868 +2006,MEX,4.442 +2006,FSM,1.03 +2006,MDA,1.276 +2006,MCO,0 +2006,MNG,3.596 +2006,MNE,3.261 +2006,MSR,8.474 +2006,MAR,1.5 +2006,MOZ,0.092 +2006,MMR,0.266 +2006,NAME,1.177 +2006,NRU,4.261 +2006,NPL,0.093 +2006,NLD,10.596 +2006,NCL,11.018 +2006,NZL,8.931 +2006,NIC,0.799 +2006,NER,0.047 +2006,NGA,0.623 +2006,NIU,1.973 +2006,PRK,3.143 +2006,MKD,4.157 +2006,NOR,9.406 +2006,OMN,15.686 +2006,PAK,0.806 +2006,PLW,11.564 +2006,PSE,0.624 +2006,PAN,2.234 +2006,PNG,0.753 +2006,PRY,0.706 +2006,PER,0.994 +2006,PHL,0.757 +2006,POL,8.731 +2006,PRT,6.15 +2006,QAT,58.745 +2006,ROU,4.988 +2006,RUS,11.323 +2006,RWA,0.055 +2006,SHN,2.018 +2006,KNA,4.297 +2006,LCA,2.465 +2006,SPM,10.803 +2006,VCT,1.94 +2006,WSM,0.909 +2006,SMR,0 +2006,STP,0.508 +2006,SAU,17.3 +2006,SEN,0.393 +2006,SRB,7.805 +2006,SYC,4.416 +2006,SLE,0.1 +2006,SGP,9.43 +2006,SXM,18.479 +2006,SVK,7.916 +2006,SVN,8.531 +2006,SLB,0.587 +2006,SOME,0.052 +2006,ZAF,9.027 +2006,KOR,10.478 +2006,SSD,0.124 +2006,ESP,8.096 +2006,LKA,0.593 +2006,SDN,0.38 +2006,SURE,3.33 +2006,SWE,5.902 +2006,CHE,6.069 +2006,SYR,2.762 +2006,TWN,12.073 +2006,TJK,0.376 +2006,TZA,0.145 +2006,THA,3.254 +2006,TGO,0.255 +2006,TON,1.208 +2006,TO,31.031 +2006,TUN,2.191 +2006,TUR,4.057 +2006,TKM,9.992 +2006,TCA,8.595 +2006,TUV,1.093 +2006,UGA,0.083 +2006,UKR,7.148 +2006,ARE,24.843 +2006,GBR,9.339 +2006,USA,20.192 +2006,URY,1.986 +2006,UZB,4.639 +2006,VUT,0.214 +2006,VAT,0 +2006,VEN,5.741 +2006,VNM,1.187 +2006,WLF,2.052 +2006,YEM,1.083 +2006,ZMB,0.186 +2006,ZWE,0.841 +2007,AFG,0.108 +2007,,4.683 +2007,ALB,1.327 +2007,DZA,3.234 +2007,AND,6.889 +2007,AGO,0.841 +2007,AIA,9.93 +2007,ATA,0 +2007,ATG,6.521 +2007,ARG,4.351 +2007,ARM,1.698 +2007,ABW,29.146 +2007,AUS,19.187 +2007,AUT,8.936 +2007,AZE,3.383 +2007,BHS,5.235 +2007,BHR,25.728 +2007,BGD,0.296 +2007,BRB,6.639 +2007,BLR,6.128 +2007,BEL,11.307 +2007,BLZ,1.603 +2007,BEN,0.455 +2007,BMU,11.54 +2007,BTN,0.577 +2007,BOL,1.307 +2007,BES,4.423 +2007,BIH,4.36 +2007,BWA,2.15 +2007,BRA,2.047 +2007,VGB,7.411 +2007,BRN,21.324 +2007,BGR,7.214 +2007,BFA,0.108 +2007,BDI,0.025 +2007,KHM,0.253 +2007,CMR,0.406 +2007,CAN,18.068 +2007,CPV,1.002 +2007,CALF,0.053 +2007,TCD,0.102 +2007,CHL,4.272 +2007,CHN,5.285 +2007,CXR,0 +2007,COL,1.397 +2007,COM,0.172 +2007,COG,1.077 +2007,COK,4.246 +2007,CRI,1.793 +2007,CIV,0.336 +2007,HRV,5.643 +2007,CUB,2.325 +2007,CUW,39.314 +2007,CYP,7.919 +2007,CZE,12.419 +2007,COD,0.044 +2007,DNK,9.984 +2007,DJI,0.534 +2007,DMA,2.503 +2007,DOM,2.126 +2007,TLS,0.283 +2007,ECU,2.405 +2007,EGY,2.296 +2007,SLV,1.133 +2007,GNQ,6.033 +2007,ERI,0.193 +2007,EST,14.876 +2007,SWZ,0.96 +2007,ETH,0.071 +2007,FRO,14.237 +2007,FJI,1.28 +2007,FIN,12.623 +2007,FRA,6.46 +2007,PYF,2.924 +2007,GAB,3.251 +2007,GMB,0.197 +2007,GEO,1.628 +2007,DEU,10.473 +2007,GHA,0.379 +2007,GRC,10.327 +2007,GRL,11.525 +2007,GRD,2.131 +2007,GTM,0.887 +2007,GIN,0.206 +2007,GNB,0.159 +2007,GUY,2.354 +2007,HTI,0.188 +2007,HND,1.037 +2007,HKG,6.223 +2007,HUN,5.839 +2007,ISL,11.207 +2007,IND,1.17 +2007,IDN,1.651 +2007,IRN,6.949 +2007,IRQ,2.131 +2007,IRL,10.933 +2007,ISR,9.033 +2007,ITA,8.347 +2007,JAM,3.979 +2007,JPN,10.178 +2007,JOR,3.311 +2007,KAZ,14.166 +2007,KEN,0.258 +2007,KIR,0.467 +2007,KWT,30.78 +2007,KGZ,1.232 +2007,LAO,0.304 +2007,LVA,3.97 +2007,LBN,2.858 +2007,ALSO,1.046 +2007,LBR,0.172 +2007,LBY,8.462 +2007,LIE,5.708 +2007,LTU,4.729 +2007,LUX,23.562 +2007,MAC,2.645 +2007,MDG,0.087 +2007,MWI,0.068 +2007,MYS,6.476 +2007,MDV,2.4 +2007,MLI,0.127 +2007,MLT,6.628 +2007,MHL,2.304 +2007,MRT,0.557 +2007,MUS,2.901 +2007,MEX,4.374 +2007,FSM,1.204 +2007,MDA,1.284 +2007,MCO,0 +2007,MNG,4.589 +2007,MNE,3.253 +2007,MSR,8.409 +2007,MAR,1.566 +2007,MOZ,0.103 +2007,MMR,0.265 +2007,NAME,1.126 +2007,NRU,4.273 +2007,NPL,0.096 +2007,NLD,10.539 +2007,NCL,11.736 +2007,NZL,8.625 +2007,NIC,0.812 +2007,NER,0.048 +2007,NGA,0.553 +2007,NIU,1.985 +2007,PRK,2.606 +2007,MKD,4.326 +2007,NOR,9.68 +2007,OMN,17.296 +2007,PAK,0.854 +2007,PLW,13.043 +2007,PSE,0.625 +2007,PAN,2.142 +2007,PNG,0.951 +2007,PRY,0.723 +2007,PER,1.199 +2007,PHL,0.79 +2007,POL,8.72 +2007,PRT,5.902 +2007,QAT,48.204 +2007,ROU,5.255 +2007,RUS,11.346 +2007,RWA,0.057 +2007,SHN,2.059 +2007,KNA,4.596 +2007,LCA,2.537 +2007,SPM,10.83 +2007,VCT,2.116 +2007,WSM,0.942 +2007,SMR,0 +2007,STP,0.496 +2007,SAU,14.928 +2007,SEN,0.42 +2007,SRB,7.676 +2007,SYC,4.562 +2007,SLE,0.082 +2007,SGP,6.601 +2007,SXM,20.594 +2007,SVK,7.618 +2007,SVN,8.572 +2007,SLB,0.595 +2007,SOME,0.055 +2007,ZAF,9.302 +2007,KOR,10.819 +2007,SSD,0.139 +2007,ESP,8.12 +2007,LKA,0.605 +2007,SDN,0.434 +2007,SURE,3.327 +2007,SWE,5.783 +2007,CHE,5.75 +2007,SYR,3.22 +2007,TWN,12.196 +2007,TJK,0.449 +2007,TZA,0.137 +2007,THA,3.353 +2007,TGO,0.251 +2007,TON,1.065 +2007,TO,32.915 +2007,TUN,2.324 +2007,TUR,4.452 +2007,TKM,9.788 +2007,TCA,9.616 +2007,TUV,1.081 +2007,UGA,0.091 +2007,UKR,7.273 +2007,ARE,22.669 +2007,GBR,9.134 +2007,USA,20.249 +2007,URY,1.787 +2007,UZB,4.534 +2007,VUT,0.433 +2007,VAT,0 +2007,VEN,5.594 +2007,VNM,1.197 +2007,WLF,2.096 +2007,YEM,1.041 +2007,ZMB,0.185 +2007,ZWE,0.79 +2008,AFG,0.161 +2008,,4.704 +2008,ALB,1.49 +2008,DZA,3.213 +2008,AND,7.08 +2008,AGO,0.864 +2008,AIA,9.768 +2008,ATA,0 +2008,ATG,6.468 +2008,ARG,4.649 +2008,ARM,1.869 +2008,ABW,27.104 +2008,AUS,19.026 +2008,AUT,8.832 +2008,AZE,3.671 +2008,BHS,5.282 +2008,BHR,26.731 +2008,BGD,0.312 +2008,BRB,7.634 +2008,BLR,6.42 +2008,BEL,11.203 +2008,BLZ,1.426 +2008,BEN,0.44 +2008,BMU,10.249 +2008,BTN,0.612 +2008,BOL,1.312 +2008,BES,4.096 +2008,BIH,5.061 +2008,BWA,2.243 +2008,BRA,2.142 +2008,VGB,7.429 +2008,BRN,23.833 +2008,BGR,7.039 +2008,BFA,0.114 +2008,BDI,0.025 +2008,KHM,0.277 +2008,CMR,0.39 +2008,CAN,17.364 +2008,CPV,0.919 +2008,CALF,0.037 +2008,TCD,0.079 +2008,CHL,4.234 +2008,CHN,5.64 +2008,CXR,0 +2008,COL,1.528 +2008,COM,0.175 +2008,COG,1.044 +2008,COK,4.099 +2008,CRI,1.775 +2008,CIV,0.332 +2008,HRV,5.37 +2008,CUB,2.493 +2008,CUW,37.212 +2008,CYP,7.978 +2008,CZE,11.847 +2008,COD,0.043 +2008,DNK,9.315 +2008,DJI,0.564 +2008,DMA,2.45 +2008,DOM,2.135 +2008,TLS,0.273 +2008,ECU,2.091 +2008,EGY,2.358 +2008,SLV,1.055 +2008,GNQ,5.972 +2008,ERI,0.136 +2008,EST,13.33 +2008,SWZ,0.938 +2008,ETH,0.076 +2008,FRO,13.011 +2008,FJI,0.943 +2008,FIN,11.033 +2008,FRA,6.309 +2008,PYF,2.978 +2008,GAB,3.362 +2008,GMB,0.199 +2008,GEO,1.368 +2008,DEU,10.501 +2008,GHA,0.342 +2008,GRC,10.031 +2008,GRL,11.968 +2008,GRD,2.247 +2008,GTM,0.779 +2008,GIN,0.204 +2008,GNB,0.153 +2008,GUY,2.264 +2008,HTI,0.184 +2008,HND,1.053 +2008,HKG,6.069 +2008,HUN,5.719 +2008,ISL,12.0 +2008,IND,1.234 +2008,IDN,1.537 +2008,IRN,7.127 +2008,IRQ,3.215 +2008,IRL,10.638 +2008,ISR,9.609 +2008,ITA,8.089 +2008,JAM,3.976 +2008,JPN,9.619 +2008,JOR,3.124 +2008,KAZ,14.005 +2008,KEN,0.261 +2008,KIR,0.458 +2008,KWT,31.793 +2008,KGZ,1.399 +2008,LAO,0.347 +2008,LVA,3.813 +2008,LBN,3.564 +2008,ALSO,1.065 +2008,LBR,0.14 +2008,LBY,8.59 +2008,LIE,6.197 +2008,LTU,4.627 +2008,LUX,22.864 +2008,MAC,2.14 +2008,MDG,0.087 +2008,MWI,0.072 +2008,MYS,6.971 +2008,MDV,2.501 +2008,MLI,0.134 +2008,MLT,6.508 +2008,MHL,2.382 +2008,MRT,0.567 +2008,MUS,2.953 +2008,MEX,4.313 +2008,FSM,1.011 +2008,MDA,1.361 +2008,MCO,0 +2008,MNG,4.525 +2008,MNE,4.125 +2008,MSR,9.849 +2008,MAR,1.624 +2008,MOZ,0.1 +2008,MMR,0.201 +2008,NAME,1.326 +2008,NRU,4.282 +2008,NPL,0.125 +2008,NLD,10.675 +2008,NCL,11.253 +2008,NZL,8.804 +2008,NIC,0.768 +2008,NER,0.051 +2008,NGA,0.573 +2008,NIU,3.968 +2008,PRK,2.874 +2008,MKD,4.284 +2008,NOR,9.368 +2008,OMN,17.797 +2008,PAK,0.828 +2008,PLW,10.731 +2008,PSE,0.539 +2008,PAN,2.098 +2008,PNG,0.777 +2008,PRY,0.758 +2008,PER,1.225 +2008,PHL,0.847 +2008,POL,8.555 +2008,PRT,5.673 +2008,QAT,42.676 +2008,ROU,5.238 +2008,RUS,11.546 +2008,RWA,0.054 +2008,SHN,2.081 +2008,KNA,4.582 +2008,LCA,2.521 +2008,SPM,10.838 +2008,VCT,1.993 +2008,WSM,0.841 +2008,SMR,0 +2008,STP,0.484 +2008,SAU,16.049 +2008,SEN,0.399 +2008,SRB,6.705 +2008,SYC,4.618 +2008,SLE,0.083 +2008,SGP,9.653 +2008,SXM,19.614 +2008,SVK,7.686 +2008,SVN,8.986 +2008,SLB,0.596 +2008,SOME,0.053 +2008,ZAF,9.793 +2008,KOR,11.005 +2008,SSD,0.14 +2008,ESP,7.307 +2008,LKA,0.591 +2008,SDN,0.441 +2008,SURE,3.618 +2008,SWE,5.511 +2008,CHE,5.853 +2008,SYR,3.155 +2008,TWN,11.592 +2008,TJK,0.395 +2008,TZA,0.138 +2008,THA,3.331 +2008,TGO,0.245 +2008,TON,1.131 +2008,TO,31.924 +2008,TUN,2.391 +2008,TUR,4.355 +2008,TKM,11.654 +2008,TCA,9.61 +2008,TUV,1.068 +2008,UGA,0.091 +2008,UKR,7.077 +2008,ARE,21.99 +2008,GBR,8.826 +2008,USA,19.35 +2008,URY,2.458 +2008,UZB,4.63 +2008,VUT,0.407 +2008,VAT,0 +2008,VEN,5.567 +2008,VNM,1.335 +2008,WLF,1.606 +2008,YEM,1.046 +2008,ZMB,0.199 +2008,ZWE,0.615 +2009,AFG,0.233 +2009,,4.565 +2009,ALB,1.504 +2009,DZA,3.377 +2009,AND,6.993 +2009,AGO,0.918 +2009,AIA,9.605 +2009,ATA,0 +2009,ATG,6.587 +2009,ARG,4.378 +2009,ARM,1.472 +2009,ABW,26.477 +2009,AUS,18.812 +2009,AUT,8.069 +2009,AZE,3.191 +2009,BHS,5.853 +2009,BHR,23.823 +2009,BGD,0.335 +2009,BRB,7.305 +2009,BLR,6.208 +2009,BEL,9.977 +2009,BLZ,1.644 +2009,BEN,0.461 +2009,BMU,7.516 +2009,BTN,0.559 +2009,BOL,1.347 +2009,BES,4.039 +2009,BIH,5.296 +2009,BWA,1.882 +2009,BRA,2.004 +2009,VGB,7.174 +2009,BRN,19.842 +2009,BGR,5.994 +2009,BFA,0.117 +2009,BDI,0.019 +2009,KHM,0.322 +2009,CMR,0.443 +2009,CAN,16.198 +2009,CPV,1.009 +2009,CALF,0.035 +2009,TCD,0.106 +2009,CHL,3.918 +2009,CHN,5.893 +2009,CXR,0 +2009,COL,1.635 +2009,COM,0.211 +2009,COG,1.161 +2009,COK,3.959 +2009,CRI,1.698 +2009,CIV,0.274 +2009,HRV,4.976 +2009,CUB,2.517 +2009,CUW,37.489 +2009,CYP,7.624 +2009,CZE,11.035 +2009,COD,0.038 +2009,DNK,8.84 +2009,DJI,0.492 +2009,DMA,2.716 +2009,DOM,2.05 +2009,TLS,0.302 +2009,ECU,2.279 +2009,EGY,2.415 +2009,SLV,1.036 +2009,GNQ,4.696 +2009,ERI,0.161 +2009,EST,10.803 +2009,SWZ,0.967 +2009,ETH,0.074 +2009,FRO,11.872 +2009,FJI,0.823 +2009,FIN,10.472 +2009,FRA,5.974 +2009,PYF,2.972 +2009,GAB,3.208 +2009,GMB,0.199 +2009,GEO,1.592 +2009,DEU,9.701 +2009,GHA,0.275 +2009,GRC,9.436 +2009,GRL,10.494 +2009,GRD,2.232 +2009,GTM,0.802 +2009,GIN,0.211 +2009,GNB,0.154 +2009,GUY,2.541 +2009,HTI,0.194 +2009,HND,0.97 +2009,HKG,5.861 +2009,HUN,5.15 +2009,ISL,11.703 +2009,IND,1.318 +2009,IDN,1.655 +2009,IRN,7.22 +2009,IRQ,3.461 +2009,IRL,9.374 +2009,ISR,8.87 +2009,ITA,7.135 +2009,JAM,2.914 +2009,JPN,9.078 +2009,JOR,3.147 +2009,KAZ,13.69 +2009,KEN,0.306 +2009,KIR,0.449 +2009,KWT,31.735 +2009,KGZ,1.239 +2009,LAO,0.428 +2009,LVA,3.507 +2009,LBN,4.233 +2009,ALSO,1.103 +2009,LBR,0.121 +2009,LBY,8.396 +2009,LIE,5.753 +2009,LTU,4.034 +2009,LUX,21.351 +2009,MAC,3.482 +2009,MDG,0.081 +2009,MWI,0.071 +2009,MYS,6.809 +2009,MDV,2.53 +2009,MLI,0.126 +2009,MLT,6.08 +2009,MHL,2.46 +2009,MRT,0.606 +2009,MUS,2.897 +2009,MEX,4.146 +2009,FSM,1.359 +2009,MDA,1.211 +2009,MCO,0 +2009,MNG,4.885 +2009,MNE,2.664 +2009,MSR,8.98 +2009,MAR,1.587 +2009,MOZ,0.109 +2009,MMR,0.208 +2009,NAME,1.336 +2009,NRU,3.93 +2009,NPL,0.153 +2009,NLD,10.301 +2009,NCL,11.813 +2009,NZL,8.046 +2009,NIC,0.77 +2009,NER,0.059 +2009,NGA,0.493 +2009,NIU,1.99 +2009,PRK,2.175 +2009,MKD,3.941 +2009,NOR,8.921 +2009,OMN,16.405 +2009,PAK,0.817 +2009,PLW,10.497 +2009,PSE,0.536 +2009,PAN,2.377 +2009,PNG,0.715 +2009,PRY,0.795 +2009,PER,1.362 +2009,PHL,0.824 +2009,POL,8.197 +2009,PRT,5.395 +2009,QAT,40.411 +2009,ROU,4.323 +2009,RUS,10.792 +2009,RWA,0.056 +2009,SHN,2.072 +2009,KNA,4.724 +2009,LCA,2.505 +2009,SPM,10.853 +2009,VCT,2.535 +2009,WSM,0.872 +2009,SMR,0 +2009,STP,0.514 +2009,SAU,16.613 +2009,SEN,0.431 +2009,SRB,5.97 +2009,SYC,4.829 +2009,SLE,0.08 +2009,SGP,9.031 +2009,SXM,19.881 +2009,SVK,6.982 +2009,SVN,7.922 +2009,SLB,0.604 +2009,SOME,0.051 +2009,ZAF,9.392 +2009,KOR,11.069 +2009,SSD,0.14 +2009,ESP,6.395 +2009,LKA,0.632 +2009,SDN,0.448 +2009,SURE,3.687 +2009,SWE,5.079 +2009,CHE,5.628 +2009,SYR,2.885 +2009,TWN,10.957 +2009,TJK,0.327 +2009,TZA,0.131 +2009,THA,3.374 +2009,TGO,0.428 +2009,TON,1.231 +2009,TO,31.717 +2009,TUN,2.357 +2009,TUR,4.381 +2009,TKM,10.126 +2009,TCA,9.608 +2009,TUV,1.054 +2009,UGA,0.095 +2009,UKR,6.053 +2009,ARE,21.021 +2009,GBR,7.938 +2009,USA,17.765 +2009,URY,2.376 +2009,UZB,3.878 +2009,VUT,0.504 +2009,VAT,0 +2009,VEN,5.36 +2009,VNM,1.44 +2009,WLF,2.185 +2009,YEM,1.114 +2009,ZMB,0.221 +2009,ZWE,0.651 +2010,AFG,0.297 +2010,,4.768 +2010,ALB,1.642 +2010,DZA,3.301 +2010,AND,7.221 +2010,AGO,0.984 +2010,AIA,9.999 +2010,ATA,0 +2010,ATG,6.412 +2010,ARG,4.522 +2010,ARM,1.443 +2010,ABW,24.973 +2010,AUS,18.416 +2010,AUT,8.612 +2010,AZE,2.982 +2010,BHS,5.968 +2010,BHR,23.957 +2010,BGD,0.364 +2010,BRB,6.716 +2010,BLR,6.417 +2010,BEL,10.535 +2010,BLZ,1.672 +2010,BEN,0.495 +2010,BMU,9.583 +2010,BTN,0.691 +2010,BOL,1.44 +2010,BES,2.684 +2010,BIH,5.548 +2010,BWA,2.169 +2010,BRA,2.242 +2010,VGB,7.706 +2010,BRN,20.243 +2010,BGR,6.297 +2010,BFA,0.126 +2010,BDI,0.033 +2010,KHM,0.354 +2010,CMR,0.422 +2010,CAN,16.372 +2010,CPV,1.069 +2010,CALF,0.036 +2010,TCD,0.104 +2010,CHL,4.196 +2010,CHN,6.394 +2010,CXR,0 +2010,COL,1.702 +2010,COM,0.246 +2010,COG,1.239 +2010,COK,4.251 +2010,CRI,1.621 +2010,CIV,0.295 +2010,HRV,4.811 +2010,CUB,3.029 +2010,CUW,25.439 +2010,CYP,7.171 +2010,CZE,11.227 +2010,COD,0.041 +2010,DNK,8.862 +2010,DJI,0.562 +2010,DMA,2.504 +2010,DOM,2.102 +2010,TLS,0.288 +2010,ECU,2.42 +2010,EGY,2.334 +2010,SLV,1.033 +2010,GNQ,5.849 +2010,ERI,0.158 +2010,EST,14.251 +2010,SWZ,0.906 +2010,ETH,0.071 +2010,FRO,13.012 +2010,FJI,1.206 +2010,FIN,11.948 +2010,FRA,6.03 +2010,PYF,3.254 +2010,GAB,3.411 +2010,GMB,0.219 +2010,GEO,1.618 +2010,DEU,10.22 +2010,GHA,0.35 +2010,GRC,8.823 +2010,GRL,12.052 +2010,GRD,2.281 +2010,GTM,0.763 +2010,GIN,0.242 +2010,GNB,0.152 +2010,GUY,2.523 +2010,HTI,0.217 +2010,HND,0.946 +2010,HKG,5.617 +2010,HUN,5.216 +2010,ISL,11.395 +2010,IND,1.352 +2010,IDN,1.827 +2010,IRN,7.336 +2010,IRQ,3.631 +2010,IRL,9.237 +2010,ISR,9.318 +2010,ITA,7.297 +2010,JAM,2.808 +2010,JPN,9.482 +2010,JOR,2.974 +2010,KAZ,14.963 +2010,KEN,0.293 +2010,KIR,0.543 +2010,KWT,30.873 +2010,KGZ,1.159 +2010,LAO,0.475 +2010,LVA,4.071 +2010,LBN,4.003 +2010,ALSO,1.125 +2010,LBR,0.188 +2010,LBY,9.409 +2010,LIE,5.309 +2010,LTU,4.397 +2010,LUX,22.083 +2010,MAC,2.216 +2010,MDG,0.086 +2010,MWI,0.066 +2010,MYS,6.942 +2010,MDV,2.584 +2010,MLI,0.134 +2010,MLT,6.216 +2010,MHL,2.537 +2010,MRT,0.612 +2010,MUS,3.049 +2010,MEX,4.057 +2010,FSM,0.953 +2010,MDA,1.313 +2010,MCO,0 +2010,MNG,5.096 +2010,MNE,3.838 +2010,MSR,11.819 +2010,MAR,1.675 +2010,MOZ,0.114 +2010,MMR,0.266 +2010,NAME,1.353 +2010,NRU,4.285 +2010,NPL,0.178 +2010,NLD,10.974 +2010,NCL,13.991 +2010,NZL,8.009 +2010,NIC,0.763 +2010,NER,0.07 +2010,NGA,0.692 +2010,NIU,1.993 +2010,PRK,2.029 +2010,MKD,3.909 +2010,NOR,9.33 +2010,OMN,17.748 +2010,PAK,0.791 +2010,PLW,11.251 +2010,PSE,0.509 +2010,PAN,2.504 +2010,PNG,0.622 +2010,PRY,0.871 +2010,PER,1.454 +2010,PHL,0.878 +2010,POL,8.659 +2010,PRT,4.999 +2010,QAT,42.849 +2010,ROU,4.234 +2010,RUS,11.399 +2010,RWA,0.056 +2010,SHN,2.059 +2010,KNA,4.635 +2010,LCA,2.851 +2010,SPM,10.856 +2010,VCT,2.011 +2010,WSM,0.941 +2010,SMR,0 +2010,STP,0.563 +2010,SAU,17.836 +2010,SEN,0.548 +2010,SRB,5.97 +2010,SYC,4.797 +2010,SLE,0.084 +2010,SGP,8.337 +2010,SXM,13.581 +2010,SVK,7.117 +2010,SVN,8.001 +2010,SLB,0.61 +2010,SOME,0.051 +2010,ZAF,8.941 +2010,KOR,12.176 +2010,SSD,0.135 +2010,ESP,6.075 +2010,LKA,0.631 +2010,SDN,0.445 +2010,SURE,4.34 +2010,SWE,5.662 +2010,CHE,5.758 +2010,SYR,2.763 +2010,TWN,11.703 +2010,TJK,0.333 +2010,TZA,0.153 +2010,THA,3.53 +2010,TGO,0.395 +2010,TON,1.092 +2010,TO,33.406 +2010,TUN,2.583 +2010,TUR,4.32 +2010,TKM,11.235 +2010,TCA,9.483 +2010,TUV,1.04 +2010,UGA,0.11 +2010,UKR,6.444 +2010,ARE,21.793 +2010,GBR,8.157 +2010,USA,18.252 +2010,URY,1.879 +2010,UZB,3.763 +2010,VUT,0.493 +2010,VAT,0 +2010,VEN,6.593 +2010,VNM,1.597 +2010,WLF,2.226 +2010,YEM,1.039 +2010,ZMB,0.226 +2010,ZWE,0.682 +2011,AFG,0.405 +2011,,4.869 +2011,ALB,1.832 +2011,DZA,3.423 +2011,AND,6.956 +2011,AGO,0.964 +2011,AIA,9.322 +2011,ATA,0 +2011,ATG,6.293 +2011,ARG,4.568 +2011,ARM,1.696 +2011,ABW,24.667 +2011,AUS,18.082 +2011,AUT,8.331 +2011,AZE,3.228 +2011,BHS,5.594 +2011,BHR,23.486 +2011,BGD,0.377 +2011,BRB,7.047 +2011,BLR,6.316 +2011,BEL,9.59 +2011,BLZ,1.679 +2011,BEN,0.461 +2011,BMU,6.976 +2011,BTN,1.026 +2011,BOL,1.534 +2011,BES,3.318 +2011,BIH,6.348 +2011,BWA,1.888 +2011,BRA,2.334 +2011,VGB,7.594 +2011,BRN,23.798 +2011,BGR,7.043 +2011,BFA,0.128 +2011,BDI,0.036 +2011,KHM,0.362 +2011,CMR,0.396 +2011,CAN,16.468 +2011,CPV,1.167 +2011,CALF,0.039 +2011,TCD,0.103 +2011,CHL,4.546 +2011,CHN,7.024 +2011,CXR,0 +2011,COL,1.682 +2011,COM,0.213 +2011,COG,1.101 +2011,COK,4.53 +2011,CRI,1.562 +2011,CIV,0.303 +2011,HRV,4.745 +2011,CUB,2.605 +2011,CUW,31.993 +2011,CYP,6.801 +2011,CZE,10.975 +2011,COD,0.044 +2011,DNK,7.933 +2011,DJI,0.505 +2011,DMA,2.238 +2011,DOM,2.153 +2011,TLS,0.39 +2011,ECU,2.511 +2011,EGY,2.432 +2011,SLV,1.059 +2011,GNQ,5.325 +2011,ERI,0.18 +2011,EST,14.28 +2011,SWZ,0.912 +2011,ETH,0.081 +2011,FRO,11.731 +2011,FJI,1.099 +2011,FIN,10.512 +2011,FRA,5.641 +2011,PYF,3.108 +2011,GAB,3.288 +2011,GMB,0.222 +2011,GEO,2.042 +2011,DEU,9.919 +2011,GHA,0.394 +2011,GRC,8.588 +2011,GRL,12.894 +2011,GRD,2.2 +2011,GTM,0.758 +2011,GIN,0.246 +2011,GNB,0.15 +2011,GUY,2.634 +2011,HTI,0.228 +2011,HND,1.028 +2011,HKG,6.014 +2011,HUN,5.049 +2011,ISL,10.976 +2011,IND,1.403 +2011,IDN,2.026 +2011,IRN,7.418 +2011,IRQ,3.797 +2011,IRL,8.374 +2011,ISR,9.195 +2011,ITA,7.076 +2011,JAM,3.006 +2011,JPN,9.879 +2011,JOR,2.988 +2011,KAZ,14.171 +2011,KEN,0.315 +2011,KIR,0.5 +2011,KWT,27.965 +2011,KGZ,1.372 +2011,LAO,0.494 +2011,LVA,3.761 +2011,LBN,4.017 +2011,ALSO,1.503 +2011,LBR,0.203 +2011,LBY,9.027 +2011,LIE,4.882 +2011,LTU,4.504 +2011,LUX,21.425 +2011,MAC,2.265 +2011,MDG,0.106 +2011,MWI,0.07 +2011,MYS,6.971 +2011,MDV,2.632 +2011,MLI,0.142 +2011,MLT,6.1 +2011,MHL,2.627 +2011,MRT,0.633 +2011,MUS,3.045 +2011,MEX,4.235 +2011,FSM,1.087 +2011,MDA,1.375 +2011,MCO,0 +2011,MNG,7.787 +2011,MNE,3.81 +2011,MSR,7.344 +2011,MAR,1.668 +2011,MOZ,0.131 +2011,MMR,0.304 +2011,NAME,1.305 +2011,NRU,3.912 +2011,NPL,0.191 +2011,NLD,10.126 +2011,NCL,13.873 +2011,NZL,7.821 +2011,NIC,0.809 +2011,NER,0.076 +2011,NGA,0.758 +2011,NIU,3.998 +2011,PRK,1.46 +2011,MKD,4.214 +2011,NOR,9.033 +2011,OMN,17.769 +2011,PAK,0.779 +2011,PLW,11.839 +2011,PSE,0.549 +2011,PAN,2.698 +2011,PNG,0.679 +2011,PRY,0.89 +2011,PER,1.431 +2011,PHL,0.871 +2011,POL,8.638 +2011,PRT,4.897 +2011,QAT,45.169 +2011,ROU,4.59 +2011,RUS,11.76 +2011,RWA,0.061 +2011,SHN,2.044 +2011,KNA,4.849 +2011,LCA,2.831 +2011,SPM,10.872 +2011,VCT,1.988 +2011,WSM,0.97 +2011,SMR,0 +2011,STP,0.532 +2011,SAU,16.68 +2011,SEN,0.607 +2011,SRB,6.424 +2011,SYC,4.334 +2011,SLE,0.107 +2011,SGP,6.441 +2011,SXM,17.181 +2011,SVK,7.031 +2011,SVN,7.92 +2011,SLB,0.622 +2011,SOME,0.05 +2011,ZAF,8.884 +2011,KOR,12.679 +2011,SSD,0.125 +2011,ESP,6.073 +2011,LKA,0.714 +2011,SDN,0.426 +2011,SURE,4.99 +2011,SWE,5.198 +2011,CHE,5.18 +2011,SYR,2.588 +2011,TWN,11.938 +2011,TJK,0.301 +2011,TZA,0.162 +2011,THA,3.564 +2011,TGO,0.371 +2011,TON,0.953 +2011,TO,33.146 +2011,TUN,2.396 +2011,TUR,4.612 +2011,TKM,12.154 +2011,TCA,9.148 +2011,TUV,1.025 +2011,UGA,0.115 +2011,UKR,6.775 +2011,ARE,23.04 +2011,GBR,7.422 +2011,USA,17.67 +2011,URY,2.276 +2011,UZB,4.018 +2011,VUT,0.539 +2011,VAT,0 +2011,VEN,5.788 +2011,VNM,1.727 +2011,WLF,1.984 +2011,YEM,0.884 +2011,ZMB,0.236 +2011,ZWE,0.796 +2012,AFG,0.329 +2012,,4.878 +2012,ALB,1.677 +2012,DZA,3.641 +2012,AND,6.86 +2012,AGO,1.03 +2012,AIA,8.97 +2012,ATA,0 +2012,ATG,6.351 +2012,ARG,4.56 +2012,ARM,1.972 +2012,ABW,13.203 +2012,AUS,17.888 +2012,AUT,7.982 +2012,AZE,3.455 +2012,BHS,5.37 +2012,BHR,22.139 +2012,BGD,0.399 +2012,BRB,6.529 +2012,BLR,6.452 +2012,BEL,9.283 +2012,BLZ,1.337 +2012,BEN,0.44 +2012,BMU,5.763 +2012,BTN,1.158 +2012,BOL,1.659 +2012,BES,3.925 +2012,BIH,6.024 +2012,BWA,2.334 +2012,BRA,2.492 +2012,VGB,7.472 +2012,BRN,23.362 +2012,BGR,6.449 +2012,BFA,0.153 +2012,BDI,0.036 +2012,KHM,0.377 +2012,CMR,0.362 +2012,CAN,16.309 +2012,CPV,0.94 +2012,CALF,0.04 +2012,TCD,0.137 +2012,CHL,4.591 +2012,CHN,7.156 +2012,CXR,0 +2012,COL,1.753 +2012,COM,0.225 +2012,COG,1.065 +2012,COK,4.674 +2012,CRI,1.533 +2012,CIV,0.388 +2012,HRV,4.405 +2012,CUB,2.638 +2012,CUW,38.347 +2012,CYP,6.28 +2012,CZE,10.589 +2012,COD,0.039 +2012,DNK,7.117 +2012,DJI,0.511 +2012,DMA,2.393 +2012,DOM,2.119 +2012,TLS,0.52 +2012,ECU,2.416 +2012,EGY,2.343 +2012,SLV,1.05 +2012,GNQ,5.781 +2012,ERI,0.187 +2012,EST,13.436 +2012,SWZ,1.052 +2012,ETH,0.086 +2012,FRO,12.186 +2012,FJI,1.063 +2012,FIN,9.447 +2012,FRA,5.651 +2012,PYF,3.056 +2012,GAB,3.065 +2012,GMB,0.217 +2012,GEO,2.17 +2012,DEU,9.968 +2012,GHA,0.471 +2012,GRC,8.336 +2012,GRL,10.313 +2012,GRD,2.339 +2012,GTM,0.765 +2012,GIN,0.224 +2012,GNB,0.149 +2012,GUY,2.635 +2012,HTI,0.224 +2012,HND,1.048 +2012,HKG,5.908 +2012,HUN,4.719 +2012,ISL,10.908 +2012,IND,1.511 +2012,IDN,2.062 +2012,IRN,7.609 +2012,IRQ,3.94 +2012,IRL,8.375 +2012,ISR,9.85 +2012,ITA,6.716 +2012,JAM,2.867 +2012,JPN,10.214 +2012,JOR,3.302 +2012,KAZ,14.404 +2012,KEN,0.286 +2012,KIR,0.46 +2012,KWT,29.978 +2012,KGZ,1.791 +2012,LAO,0.522 +2012,LVA,3.662 +2012,LBN,4.313 +2012,ALSO,1.505 +2012,LBR,0.222 +2012,LBY,9.417 +2012,LIE,5.074 +2012,LTU,4.588 +2012,LUX,20.49 +2012,MAC,1.943 +2012,MDG,0.119 +2012,MWI,0.068 +2012,MYS,7.181 +2012,MDV,2.865 +2012,MLI,0.148 +2012,MLT,6.32 +2012,MHL,2.596 +2012,MRT,0.673 +2012,MUS,3.076 +2012,MEX,4.333 +2012,FSM,1.151 +2012,MDA,1.359 +2012,MCO,0 +2012,MNG,12.534 +2012,MNE,3.496 +2012,MSR,8.062 +2012,MAR,1.708 +2012,MOZ,0.125 +2012,MMR,0.235 +2012,NAME,1.548 +2012,NRU,4.202 +2012,NPL,0.2 +2012,NLD,9.859 +2012,NCL,13.658 +2012,NZL,8.15 +2012,NIC,0.754 +2012,NER,0.103 +2012,NGA,0.641 +2012,NIU,3.998 +2012,PRK,1.509 +2012,MKD,4.042 +2012,NOR,8.812 +2012,OMN,17.738 +2012,PAK,0.766 +2012,PLW,12.435 +2012,PSE,0.526 +2012,PAN,2.601 +2012,PNG,0.631 +2012,PRY,0.873 +2012,PER,1.53 +2012,PHL,0.904 +2012,POL,8.439 +2012,PRT,4.742 +2012,QAT,48.97 +2012,ROU,4.525 +2012,RUS,11.845 +2012,RWA,0.066 +2012,SHN,2.032 +2012,KNA,4.604 +2012,LCA,2.836 +2012,SPM,10.905 +2012,VCT,2.135 +2012,WSM,0.943 +2012,SMR,0 +2012,STP,0.579 +2012,SAU,18.391 +2012,SEN,0.556 +2012,SRB,5.762 +2012,SYC,4.459 +2012,SLE,0.123 +2012,SGP,9.423 +2012,SXM,19.796 +2012,SVK,6.639 +2012,SVN,7.616 +2012,SLB,0.613 +2012,SOME,0.049 +2012,ZAF,8.636 +2012,KOR,12.651 +2012,SSD,0.124 +2012,ESP,5.947 +2012,LKA,0.752 +2012,SDN,0.433 +2012,SURE,4.465 +2012,SWE,4.888 +2012,CHE,5.284 +2012,SYR,2.028 +2012,TWN,11.739 +2012,TJK,0.369 +2012,TZA,0.184 +2012,THA,3.794 +2012,TGO,0.32 +2012,TON,0.988 +2012,TO,32.282 +2012,TUN,2.547 +2012,TUR,4.731 +2012,TKM,12.263 +2012,TCA,8.788 +2012,TUV,1.011 +2012,UGA,0.106 +2012,UKR,6.704 +2012,ARE,23.935 +2012,GBR,7.64 +2012,USA,16.877 +2012,URY,2.549 +2012,UZB,4.029 +2012,VUT,0.456 +2012,VAT,0 +2012,VEN,5.963 +2012,VNM,1.616 +2012,WLF,2.02 +2012,YEM,0.776 +2012,ZMB,0.283 +2012,ZWE,0.848 +2013,AFG,0.293 +2013,,4.859 +2013,ALB,1.831 +2013,DZA,3.705 +2013,AND,6.673 +2013,AGO,0.983 +2013,AIA,8.541 +2013,ATA,0 +2013,ATG,6.292 +2013,ARG,4.471 +2013,ARM,1.908 +2013,ABW,8.368 +2013,AUS,17.276 +2013,AUT,7.993 +2013,AZE,3.49 +2013,BHS,5.263 +2013,BHR,24.695 +2013,BGD,0.401 +2013,BRB,6.581 +2013,BLR,6.617 +2013,BEL,9.251 +2013,BLZ,1.276 +2013,BEN,0.443 +2013,BMU,8.421 +2013,BTN,1.26 +2013,BOL,1.654 +2013,BES,3.826 +2013,BIH,6.026 +2013,BWA,2.554 +2013,BRA,2.639 +2013,VGB,7.409 +2013,BRN,18.523 +2013,BGR,5.731 +2013,BFA,0.162 +2013,BDI,0.035 +2013,KHM,0.376 +2013,CMR,0.363 +2013,CAN,16.243 +2013,CPV,0.916 +2013,CALF,0.024 +2013,TCD,0.157 +2013,CHL,4.668 +2013,CHN,7.235 +2013,CXR,0 +2013,COL,1.905 +2013,COM,0.262 +2013,COG,1.22 +2013,COK,4.697 +2013,CRI,1.593 +2013,CIV,0.431 +2013,HRV,4.278 +2013,CUB,2.484 +2013,CUW,35.681 +2013,CYP,5.641 +2013,CZE,10.151 +2013,COD,0.053 +2013,DNK,7.424 +2013,DJI,0.573 +2013,DMA,2.289 +2013,DOM,2.094 +2013,TLS,0.533 +2013,ECU,2.522 +2013,EGY,2.264 +2013,SLV,0.985 +2013,GNQ,6.589 +2013,ERI,0.173 +2013,EST,14.917 +2013,SWZ,1.252 +2013,ETH,0.101 +2013,FRO,13.387 +2013,FJI,1.211 +2013,FIN,9.508 +2013,FRA,5.66 +2013,PYF,3.053 +2013,GAB,3.116 +2013,GMB,0.2 +2013,GEO,2.116 +2013,DEU,10.208 +2013,GHA,0.458 +2013,GRC,7.487 +2013,GRL,10.032 +2013,GRD,2.6 +2013,GTM,0.831 +2013,GIN,0.191 +2013,GNB,0.145 +2013,GUY,2.549 +2013,HTI,0.264 +2013,HND,1.045 +2013,HKG,6.072 +2013,HUN,4.413 +2013,ISL,10.771 +2013,IND,1.545 +2013,IDN,1.931 +2013,IRN,7.621 +2013,IRQ,3.975 +2013,IRL,8.124 +2013,ISR,8.166 +2013,ITA,6.139 +2013,JAM,3.062 +2013,JPN,10.301 +2013,JOR,3.095 +2013,KAZ,14.711 +2013,KEN,0.3 +2013,KIR,0.453 +2013,KWT,22.96 +2013,KGZ,1.702 +2013,LAO,0.646 +2013,LVA,3.628 +2013,LBN,3.903 +2013,ALSO,1.125 +2013,LBR,0.197 +2013,LBY,7.261 +2013,LIE,5.227 +2013,LTU,4.344 +2013,LUX,19.038 +2013,MAC,1.667 +2013,MDG,0.136 +2013,MWI,0.072 +2013,MYS,8.02 +2013,MDV,2.725 +2013,MLI,0.161 +2013,MLT,5.438 +2013,MHL,2.71 +2013,MRT,0.579 +2013,MUS,3.151 +2013,MEX,4.224 +2013,FSM,1.248 +2013,MDA,1.434 +2013,MCO,0 +2013,MNG,15.281 +2013,MNE,3.393 +2013,MSR,9.46 +2013,MAR,1.675 +2013,MOZ,0.142 +2013,MMR,0.251 +2013,NAME,1.165 +2013,NRU,4.444 +2013,NPL,0.227 +2013,NLD,9.775 +2013,NCL,14.364 +2013,NZL,7.918 +2013,NIC,0.753 +2013,NER,0.111 +2013,NGA,0.666 +2013,NIU,3.97 +2013,PRK,1.08 +2013,MKD,3.563 +2013,NOR,8.762 +2013,OMN,17.096 +2013,PAK,0.738 +2013,PLW,12.741 +2013,PSE,0.569 +2013,PAN,2.751 +2013,PNG,0.658 +2013,PRY,0.861 +2013,PER,1.432 +2013,PHL,0.963 +2013,POL,8.338 +2013,PRT,4.597 +2013,QAT,40.682 +2013,ROU,3.955 +2013,RUS,11.395 +2013,RWA,0.07 +2013,SHN,2.018 +2013,KNA,4.677 +2013,LCA,2.801 +2013,SPM,10.943 +2013,VCT,2.046 +2013,WSM,0.971 +2013,SMR,0 +2013,STP,0.567 +2013,SAU,17.297 +2013,SEN,0.578 +2013,SRB,5.89 +2013,SYC,4.28 +2013,SLE,0.15 +2013,SGP,10.023 +2013,SXM,18.941 +2013,SVK,6.569 +2013,SVN,7.3 +2013,SLB,0.642 +2013,SOME,0.049 +2013,ZAF,8.513 +2013,KOR,12.667 +2013,SSD,0.13 +2013,ESP,5.406 +2013,LKA,0.687 +2013,SDN,0.473 +2013,SURE,5.581 +2013,SWE,4.685 +2013,CHE,5.339 +2013,SYR,1.584 +2013,TWN,11.739 +2013,TJK,0.357 +2013,TZA,0.197 +2013,THA,3.791 +2013,TGO,0.231 +2013,TON,1.06 +2013,TO,31.806 +2013,TUN,2.507 +2013,TUR,4.536 +2013,TKM,11.572 +2013,TCA,8.501 +2013,TUV,1.005 +2013,UGA,0.106 +2013,UKR,6.566 +2013,ARE,24.376 +2013,GBR,7.428 +2013,USA,17.159 +2013,URY,2.216 +2013,UZB,3.747 +2013,VUT,0.417 +2013,VAT,0 +2013,VEN,6.65 +2013,VNM,1.678 +2013,WLF,1.759 +2013,YEM,1.007 +2013,ZMB,0.29 +2013,ZWE,0.861 +2014,AFG,0.28 +2014,,4.833 +2014,ALB,2.08 +2014,DZA,3.903 +2014,AND,6.444 +2014,AGO,0.913 +2014,AIA,8.549 +2014,ATA,0 +2014,ATG,6.363 +2014,ARG,4.399 +2014,ARM,1.939 +2014,ABW,8.417 +2014,AUS,16.747 +2014,AUT,7.509 +2014,AZE,3.526 +2014,BHS,5.489 +2014,BHR,23.567 +2014,BGD,0.423 +2014,BRB,6.09 +2014,BLR,6.566 +2014,BEL,8.681 +2014,BLZ,1.331 +2014,BEN,0.452 +2014,BMU,10.58 +2014,BTN,1.396 +2014,BOL,1.822 +2014,BES,3.729 +2014,BIH,5.408 +2014,BWA,3.031 +2014,BRA,2.742 +2014,VGB,7.454 +2014,BRN,21.194 +2014,BGR,6.126 +2014,BFA,0.16 +2014,BDI,0.033 +2014,KHM,0.449 +2014,CMR,0.393 +2014,CAN,15.997 +2014,CPV,0.892 +2014,CALF,0.026 +2014,TCD,0.159 +2014,CHL,4.387 +2014,CHN,7.218 +2014,CXR,0 +2014,COL,2.106 +2014,COM,0.226 +2014,COG,1.106 +2014,COK,4.518 +2014,CRI,1.599 +2014,CIV,0.431 +2014,HRV,4.127 +2014,CUB,2.436 +2014,CUW,40.448 +2014,CYP,5.906 +2014,CZE,9.913 +2014,COD,0.067 +2014,DNK,6.649 +2014,DJI,0.393 +2014,DMA,2.376 +2014,DOM,2.12 +2014,TLS,0.527 +2014,ECU,2.74 +2014,EGY,2.374 +2014,SLV,0.991 +2014,GNQ,5.924 +2014,ERI,0.171 +2014,EST,14.323 +2014,SWZ,0.683 +2014,ETH,0.12 +2014,FRO,12.316 +2014,FJI,1.367 +2014,FIN,8.719 +2014,FRA,5.143 +2014,PYF,2.932 +2014,GAB,3.238 +2014,GMB,0.231 +2014,GEO,2.368 +2014,DEU,9.709 +2014,GHA,0.47 +2014,GRC,7.239 +2014,GRL,9.337 +2014,GRD,1.987 +2014,GTM,0.88 +2014,GIN,0.191 +2014,GNB,0.149 +2014,GUY,2.625 +2014,HTI,0.26 +2014,HND,1.039 +2014,HKG,6.188 +2014,HUN,4.438 +2014,ISL,10.52 +2014,IND,1.643 +2014,IDN,1.904 +2014,IRN,7.912 +2014,IRQ,3.729 +2014,IRL,7.973 +2014,ISR,7.666 +2014,ITA,5.804 +2014,JAM,2.764 +2014,JPN,9.916 +2014,JOR,2.991 +2014,KAZ,15.591 +2014,KEN,0.317 +2014,KIR,0.51 +2014,KWT,20.104 +2014,KGZ,1.761 +2014,LAO,0.665 +2014,LVA,3.567 +2014,LBN,3.824 +2014,ALSO,1.18 +2014,LBR,0.182 +2014,LBY,9.938 +2014,LIE,4.344 +2014,LTU,4.292 +2014,LUX,17.666 +2014,MAC,2.141 +2014,MDG,0.13 +2014,MWI,0.063 +2014,MYS,8.006 +2014,MDV,3.156 +2014,MLI,0.179 +2014,MLT,5.296 +2014,MHL,2.833 +2014,MRT,0.668 +2014,MUS,3.255 +2014,MEX,4.077 +2014,FSM,1.243 +2014,MDA,1.426 +2014,MCO,0 +2014,MNG,10.206 +2014,MNE,3.321 +2014,MSR,9.386 +2014,MAR,1.669 +2014,MOZ,0.312 +2014,MMR,0.31 +2014,NAME,1.64 +2014,NRU,4.678 +2014,NPL,0.276 +2014,NLD,9.288 +2014,NCL,17.838 +2014,NZL,7.85 +2014,NIC,0.764 +2014,NER,0.116 +2014,NGA,0.686 +2014,NIU,5.916 +2014,PRK,1.216 +2014,MKD,3.422 +2014,NOR,8.75 +2014,OMN,16.436 +2014,PAK,0.752 +2014,PLW,12.336 +2014,PSE,0.648 +2014,PAN,2.791 +2014,PNG,0.787 +2014,PRY,0.896 +2014,PER,1.627 +2014,PHL,1.002 +2014,POL,8.024 +2014,PRT,4.601 +2014,QAT,41.174 +2014,ROU,3.957 +2014,RUS,11.366 +2014,RWA,0.073 +2014,SHN,2.006 +2014,KNA,4.828 +2014,LCA,2.767 +2014,SPM,10.968 +2014,VCT,2.364 +2014,WSM,0.999 +2014,SMR,0 +2014,STP,0.649 +2014,SAU,19.076 +2014,SEN,0.624 +2014,SRB,4.932 +2014,SYC,4.673 +2014,SLE,0.158 +2014,SGP,9.355 +2014,SXM,18.543 +2014,SVK,6.211 +2014,SVN,6.536 +2014,SLB,0.552 +2014,SOME,0.047 +2014,ZAF,8.622 +2014,KOR,12.448 +2014,SSD,0.136 +2014,ESP,5.47 +2014,LKA,0.82 +2014,SDN,0.474 +2014,SURE,5.618 +2014,SWE,4.46 +2014,CHE,4.791 +2014,SYR,1.542 +2014,TWN,11.799 +2014,TJK,0.552 +2014,TZA,0.181 +2014,THA,3.895 +2014,TGO,0.212 +2014,TON,1.065 +2014,TO,32.324 +2014,TUN,2.598 +2014,TUR,4.66 +2014,TKM,11.089 +2014,TCA,8.79 +2014,TUV,1.006 +2014,UGA,0.113 +2014,UKR,5.707 +2014,ARE,23.765 +2014,GBR,6.774 +2014,USA,17.168 +2014,URY,1.971 +2014,UZB,3.527 +2014,VUT,0.584 +2014,VAT,0 +2014,VEN,5.828 +2014,VNM,1.993 +2014,WLF,1.782 +2014,YEM,0.954 +2014,ZMB,0.31 +2014,ZWE,0.862 +2015,AFG,0.29 +2015,,4.775 +2015,ALB,1.635 +2015,DZA,4.048 +2015,AND,6.484 +2015,AGO,0.925 +2015,AIA,9.567 +2015,ATA,0 +2015,ATG,6.476 +2015,ARG,4.433 +2015,ARM,1.9 +2015,ABW,8.609 +2015,AUS,16.85 +2015,AUT,7.679 +2015,AZE,3.516 +2015,BHS,5.719 +2015,BHR,23.83 +2015,BGD,0.464 +2015,BRB,6.064 +2015,BLR,6.061 +2015,BEL,8.992 +2015,BLZ,1.782 +2015,BEN,0.487 +2015,BMU,8.527 +2015,BTN,1.421 +2015,BOL,1.793 +2015,BES,4.107 +2015,BIH,5.238 +2015,BWA,2.352 +2015,BRA,2.58 +2015,VGB,7.728 +2015,BRN,16.451 +2015,BGR,6.584 +2015,BFA,0.198 +2015,BDI,0.033 +2015,KHM,0.543 +2015,CMR,0.428 +2015,CAN,15.971 +2015,CPV,0.889 +2015,CALF,0.037 +2015,TCD,0.166 +2015,CHL,4.576 +2015,CHN,7.08 +2015,CXR,0 +2015,COL,2.054 +2015,COM,0.246 +2015,COG,1.112 +2015,COK,4.549 +2015,CRI,1.513 +2015,CIV,0.405 +2015,HRV,4.189 +2015,CUB,2.599 +2015,CUW,43.211 +2015,CYP,5.872 +2015,CZE,9.979 +2015,COD,0.042 +2015,DNK,6.184 +2015,DJI,0.429 +2015,DMA,2.511 +2015,DOM,2.258 +2015,TLS,0.489 +2015,ECU,2.549 +2015,EGY,2.297 +2015,SLV,1.07 +2015,GNQ,5.222 +2015,ERI,0.166 +2015,EST,12.037 +2015,SWZ,0.766 +2015,ETH,0.124 +2015,FRO,12.451 +2015,FJI,1.439 +2015,FIN,8.064 +2015,FRA,5.194 +2015,PYF,2.901 +2015,GAB,3.192 +2015,GMB,0.259 +2015,GEO,2.587 +2015,DEU,9.724 +2015,GHA,0.505 +2015,GRC,6.933 +2015,GRL,9.402 +2015,GRD,2.186 +2015,GTM,0.997 +2015,GIN,0.211 +2015,GNB,0.156 +2015,GUY,2.653 +2015,HTI,0.252 +2015,HND,1.113 +2015,HKG,5.73 +2015,HUN,4.746 +2015,ISL,10.704 +2015,IND,1.689 +2015,IDN,2.081 +2015,IRN,7.722 +2015,IRQ,3.76 +2015,IRL,8.298 +2015,ISR,7.86 +2015,ITA,6.009 +2015,JAM,2.862 +2015,JPN,9.612 +2015,JOR,2.691 +2015,KAZ,15.624 +2015,KEN,0.363 +2015,KIR,0.471 +2015,KWT,23.962 +2015,KGZ,1.735 +2015,LAO,1.36 +2015,LVA,3.646 +2015,LBN,4.022 +2015,ALSO,1.058 +2015,LBR,0.101 +2015,LBY,8.81 +2015,LIE,4.275 +2015,LTU,4.405 +2015,LUX,16.422 +2015,MAC,2.876 +2015,MDG,0.13 +2015,MWI,0.064 +2015,MYS,7.58 +2015,MDV,2.936 +2015,MLI,0.181 +2015,MLT,3.647 +2015,MHL,2.891 +2015,MRT,0.761 +2015,MUS,3.256 +2015,MEX,3.991 +2015,FSM,1.305 +2015,MDA,1.459 +2015,MCO,0 +2015,MNG,7.853 +2015,MNE,3.531 +2015,MSR,10.813 +2015,MAR,1.687 +2015,MOZ,0.251 +2015,MMR,0.425 +2015,NAME,1.687 +2015,NRU,4.904 +2015,NPL,0.25 +2015,NLD,9.612 +2015,NCL,17.23 +2015,NZL,7.801 +2015,NIC,0.862 +2015,NER,0.11 +2015,NGA,0.594 +2015,NIU,3.917 +2015,PRK,0.975 +2015,MKD,3.205 +2015,NOR,8.765 +2015,OMN,16.073 +2015,PAK,0.789 +2015,PLW,11.513 +2015,PSE,0.67 +2015,PAN,2.726 +2015,PNG,0.765 +2015,PRY,0.979 +2015,PER,1.603 +2015,PHL,1.091 +2015,POL,8.111 +2015,PRT,5.036 +2015,QAT,37.768 +2015,ROU,3.92 +2015,RUS,11.327 +2015,RWA,0.083 +2015,SHN,1.991 +2015,KNA,4.904 +2015,LCA,2.733 +2015,SPM,10.99 +2015,VCT,2.202 +2015,WSM,1.116 +2015,SMR,0 +2015,STP,0.674 +2015,SAU,20.728 +2015,SEN,0.686 +2015,SRB,5.821 +2015,SYC,5.02 +2015,SLE,0.148 +2015,SGP,10.96 +2015,SXM,18.489 +2015,SVK,6.355 +2015,SVN,6.558 +2015,SLB,0.478 +2015,SOME,0.046 +2015,ZAF,7.986 +2015,KOR,12.436 +2015,SSD,0.171 +2015,ESP,5.832 +2015,LKA,0.923 +2015,SDN,0.555 +2015,SURE,4.715 +2015,SWE,4.408 +2015,CHE,4.676 +2015,SYR,1.489 +2015,TWN,11.733 +2015,TJK,0.622 +2015,TZA,0.185 +2015,THA,3.942 +2015,TGO,0.249 +2015,TON,1.105 +2015,TO,31.201 +2015,TUN,2.718 +2015,TUR,4.833 +2015,TKM,11.155 +2015,TCA,8.52 +2015,TUV,1.009 +2015,UGA,0.121 +2015,UKR,4.975 +2015,ARE,25.31 +2015,GBR,6.477 +2015,USA,16.563 +2015,URY,1.981 +2015,UZB,3.35 +2015,VUT,0.49 +2015,VAT,0 +2015,VEN,5.433 +2015,VNM,2.337 +2015,WLF,1.801 +2015,YEM,0.494 +2015,ZMB,0.312 +2015,ZWE,0.866 +2016,AFG,0.262 +2016,,4.72 +2016,ALB,1.608 +2016,DZA,3.929 +2016,AND,6.463 +2016,AGO,0.823 +2016,AIA,9.381 +2016,ATA,0 +2016,ATG,6.594 +2016,ARG,4.349 +2016,ARM,1.816 +2016,ABW,8.419 +2016,AUS,16.956 +2016,AUT,7.695 +2016,AZE,3.481 +2016,BHS,5.579 +2016,BHR,22.388 +2016,BGD,0.477 +2016,BRB,6.078 +2016,BLR,5.988 +2016,BEL,8.803 +2016,BLZ,1.706 +2016,BEN,0.577 +2016,BMU,9.815 +2016,BTN,1.682 +2016,BOL,1.886 +2016,BES,4.312 +2016,BIH,6.243 +2016,BWA,2.699 +2016,BRA,2.382 +2016,VGB,7.756 +2016,BRN,17.918 +2016,BGR,6.257 +2016,BFA,0.203 +2016,BDI,0.039 +2016,KHM,0.699 +2016,CMR,0.42 +2016,CAN,15.443 +2016,CPV,0.886 +2016,CALF,0.04 +2016,TCD,0.163 +2016,CHL,4.659 +2016,CHN,6.966 +2016,CXR,0 +2016,COL,2.093 +2016,COM,0.29 +2016,COG,1.105 +2016,COK,4.17 +2016,CRI,1.589 +2016,CIV,0.495 +2016,HRV,4.287 +2016,CUB,2.484 +2016,CUW,36.264 +2016,CYP,6.156 +2016,CZE,10.133 +2016,COD,0.031 +2016,DNK,6.469 +2016,DJI,0.371 +2016,DMA,2.457 +2016,DOM,2.341 +2016,TLS,0.525 +2016,ECU,2.394 +2016,EGY,2.399 +2016,SLV,1.055 +2016,GNQ,5.403 +2016,ERI,0.166 +2016,EST,13.29 +2016,SWZ,0.924 +2016,ETH,0.137 +2016,FRO,12.726 +2016,FJI,1.372 +2016,FIN,8.598 +2016,FRA,5.22 +2016,PYF,2.958 +2016,GAB,3.126 +2016,GMB,0.255 +2016,GEO,2.67 +2016,DEU,9.738 +2016,GHA,0.471 +2016,GRC,6.638 +2016,GRL,9.413 +2016,GRD,2.229 +2016,GTM,1.039 +2016,GIN,0.23 +2016,GNB,0.166 +2016,GUY,3.128 +2016,HTI,0.278 +2016,HND,1.028 +2016,HKG,5.832 +2016,HUN,4.807 +2016,ISL,10.413 +2016,IND,1.759 +2016,IDN,2.063 +2016,IRN,7.608 +2016,IRQ,4.106 +2016,IRL,8.561 +2016,ISR,7.496 +2016,ITA,5.968 +2016,JAM,2.913 +2016,JPN,9.469 +2016,JOR,2.479 +2016,KAZ,15.361 +2016,KEN,0.379 +2016,KIR,0.464 +2016,KWT,26.957 +2016,KGZ,1.599 +2016,LAO,2.388 +2016,LVA,3.654 +2016,LBN,4.225 +2016,ALSO,1.032 +2016,LBR,0.106 +2016,LBY,8.222 +2016,LIE,3.982 +2016,LTU,4.473 +2016,LUX,15.589 +2016,MAC,2.841 +2016,MDG,0.128 +2016,MWI,0.069 +2016,MYS,7.512 +2016,MDV,3.162 +2016,MLI,0.226 +2016,MLT,2.9 +2016,MHL,2.955 +2016,MRT,0.648 +2016,MUS,3.358 +2016,MEX,3.948 +2016,FSM,1.3 +2016,MDA,1.52 +2016,MCO,0 +2016,MNG,8.46 +2016,MNE,3.182 +2016,MSR,5.738 +2016,MAR,1.665 +2016,MOZ,0.309 +2016,MMR,0.407 +2016,NAME,1.744 +2016,NRU,4.475 +2016,NPL,0.362 +2016,NLD,9.617 +2016,NCL,19.319 +2016,NZL,7.316 +2016,NIC,0.846 +2016,NER,0.103 +2016,NGA,0.62 +2016,NIU,3.892 +2016,PRK,1.086 +2016,MKD,3.153 +2016,NOR,8.532 +2016,OMN,14.947 +2016,PAK,0.917 +2016,PLW,11.912 +2016,PSE,0.704 +2016,PAN,2.593 +2016,PNG,0.791 +2016,PRY,1.148 +2016,PER,1.694 +2016,PHL,1.161 +2016,POL,8.397 +2016,PRT,4.874 +2016,QAT,33.672 +2016,ROU,3.875 +2016,RUS,11.267 +2016,RWA,0.088 +2016,SHN,2.649 +2016,KNA,4.981 +2016,LCA,2.7 +2016,SPM,11.01 +2016,VCT,2.282 +2016,WSM,1.159 +2016,SMR,0 +2016,STP,0.734 +2016,SAU,20.871 +2016,SEN,0.679 +2016,SRB,6.011 +2016,SYC,5.504 +2016,SLE,0.152 +2016,SGP,7.054 +2016,SXM,17.981 +2016,SVK,6.428 +2016,SVN,6.918 +2016,SLB,0.461 +2016,SOME,0.045 +2016,ZAF,8.099 +2016,KOR,12.433 +2016,SSD,0.154 +2016,ESP,5.587 +2016,LKA,1.073 +2016,SDN,0.509 +2016,SURE,5.023 +2016,SWE,4.362 +2016,CHE,4.679 +2016,SYR,1.42 +2016,TWN,11.856 +2016,TJK,0.652 +2016,TZA,0.188 +2016,THA,4.023 +2016,TGO,0.302 +2016,TON,1.178 +2016,TO,27.15 +2016,TUN,2.618 +2016,TUR,5.011 +2016,TKM,10.982 +2016,TCA,8.617 +2016,TUV,1.011 +2016,UGA,0.124 +2016,UKR,5.218 +2016,ARE,25.194 +2016,GBR,6.084 +2016,USA,16.054 +2016,URY,1.91 +2016,UZB,3.502 +2016,VUT,0.53 +2016,VAT,0 +2016,VEN,4.957 +2016,VNM,2.397 +2016,WLF,2.122 +2016,YEM,0.342 +2016,ZMB,0.35 +2016,ZWE,0.729 +2017,AFG,0.277 +2017,,4.74 +2017,ALB,1.838 +2017,DZA,4.013 +2017,AND,6.301 +2017,AGO,0.749 +2017,AIA,8.527 +2017,ATA,0 +2017,ATG,6.634 +2017,ARG,4.242 +2017,ARM,1.943 +2017,ABW,8.443 +2017,AUS,16.822 +2017,AUT,7.912 +2017,AZE,3.436 +2017,BHS,6.29 +2017,BHR,22.539 +2017,BGD,0.499 +2017,BRB,5.89 +2017,BLR,6.117 +2017,BEL,8.701 +2017,BLZ,1.643 +2017,BEN,0.587 +2017,BMU,10.015 +2017,BTN,1.774 +2017,BOL,1.939 +2017,BES,4.206 +2017,BIH,6.441 +2017,BWA,2.947 +2017,BRA,2.384 +2017,VGB,5.968 +2017,BRN,21.635 +2017,BGR,6.604 +2017,BFA,0.228 +2017,BDI,0.046 +2017,KHM,0.788 +2017,CMR,0.393 +2017,CAN,15.502 +2017,CPV,0.901 +2017,CALF,0.043 +2017,TCD,0.155 +2017,CHL,4.581 +2017,CHN,7.099 +2017,CXR,0 +2017,COL,1.908 +2017,COM,0.351 +2017,COG,1.064 +2017,COK,4.628 +2017,CRI,1.608 +2017,CIV,0.483 +2017,HRV,4.471 +2017,CUB,2.201 +2017,CUW,28.942 +2017,CYP,6.209 +2017,CZE,10.234 +2017,COD,0.037 +2017,DNK,6.042 +2017,DJI,0.377 +2017,DMA,2.185 +2017,DOM,2.256 +2017,TLS,0.555 +2017,ECU,2.346 +2017,EGY,2.555 +2017,SLV,0.953 +2017,GNQ,5.42 +2017,ERI,0.164 +2017,EST,14.222 +2017,SWZ,0.856 +2017,ETH,0.144 +2017,FRO,14.07 +2017,FJI,1.482 +2017,FIN,8.107 +2017,FRA,5.252 +2017,PYF,2.914 +2017,GAB,2.747 +2017,GMB,0.249 +2017,GEO,2.697 +2017,DEU,9.513 +2017,GHA,0.412 +2017,GRC,7.0 +2017,GRL,9.72 +2017,GRD,2.303 +2017,GTM,1.045 +2017,GIN,0.265 +2017,GNB,0.162 +2017,GUY,3.049 +2017,HTI,0.289 +2017,HND,1.063 +2017,HKG,5.687 +2017,HUN,5.065 +2017,ISL,10.512 +2017,IND,1.792 +2017,IDN,2.106 +2017,IRN,8.111 +2017,IRQ,4.466 +2017,IRL,8.189 +2017,ISR,7.081 +2017,ITA,5.89 +2017,JAM,2.777 +2017,JPN,9.37 +2017,JOR,2.527 +2017,KAZ,16.024 +2017,KEN,0.362 +2017,KIR,0.548 +2017,KWT,25.046 +2017,KGZ,1.529 +2017,LAO,2.813 +2017,LVA,3.691 +2017,LBN,4.554 +2017,ALSO,1.158 +2017,LBR,0.186 +2017,LBY,8.378 +2017,LIE,4.108 +2017,LTU,4.574 +2017,LUX,15.543 +2017,MAC,3.018 +2017,MDG,0.157 +2017,MWI,0.069 +2017,MYS,7.634 +2017,MDV,3.203 +2017,MLI,0.241 +2017,MLT,3.193 +2017,MHL,3.105 +2017,MRT,0.822 +2017,MUS,3.503 +2017,MEX,3.79 +2017,FSM,1.294 +2017,MDA,1.641 +2017,MCO,0 +2017,MNG,11.026 +2017,MNE,3.325 +2017,MSR,6.206 +2017,MAR,1.72 +2017,MOZ,0.242 +2017,MMR,0.452 +2017,NAME,1.783 +2017,NRU,4.694 +2017,NPL,0.441 +2017,NLD,9.426 +2017,NCL,19.636 +2017,NZL,7.519 +2017,NIC,0.843 +2017,NER,0.103 +2017,NGA,0.583 +2017,NIU,3.875 +2017,PRK,2.028 +2017,MKD,3.386 +2017,NOR,8.369 +2017,OMN,15.243 +2017,PAK,0.999 +2017,PLW,12.106 +2017,PSE,0.695 +2017,PAN,2.74 +2017,PNG,0.731 +2017,PRY,1.248 +2017,PER,1.736 +2017,PHL,1.266 +2017,POL,8.74 +2017,PRT,5.346 +2017,QAT,36.917 +2017,ROU,4.043 +2017,RUS,11.455 +2017,RWA,0.094 +2017,SHN,1.991 +2017,KNA,5.058 +2017,LCA,2.771 +2017,SPM,11.029 +2017,VCT,2.048 +2017,WSM,1.182 +2017,SMR,0 +2017,STP,0.669 +2017,SAU,19.912 +2017,SEN,0.71 +2017,SRB,6.099 +2017,SYC,5.649 +2017,SLE,0.142 +2017,SGP,6.942 +2017,SXM,16.877 +2017,SVK,6.639 +2017,SVN,6.966 +2017,SLB,0.444 +2017,SOME,0.043 +2017,ZAF,7.757 +2017,KOR,12.707 +2017,SSD,0.136 +2017,ESP,5.873 +2017,LKA,1.064 +2017,SDN,0.504 +2017,SURE,4.13 +2017,SWE,4.221 +2017,CHE,4.517 +2017,SYR,1.498 +2017,TWN,12.054 +2017,TJK,0.768 +2017,TZA,0.2 +2017,THA,3.997 +2017,TGO,0.254 +2017,TON,1.286 +2017,TO,27.267 +2017,TUN,2.648 +2017,TUR,5.249 +2017,TKM,10.783 +2017,TCA,8.823 +2017,TUV,1.013 +2017,UGA,0.134 +2017,UKR,4.995 +2017,ARE,23.432 +2017,GBR,5.863 +2017,USA,15.804 +2017,URY,1.801 +2017,UZB,3.41 +2017,VUT,0.492 +2017,VAT,0 +2017,VEN,4.523 +2017,VNM,2.442 +2017,WLF,2.144 +2017,YEM,0.323 +2017,ZMB,0.396 +2017,ZWE,0.63 +2018,AFG,0.295 +2018,,4.785 +2018,ALB,1.701 +2018,DZA,4.085 +2018,AND,6.592 +2018,AGO,0.685 +2018,AIA,8.677 +2018,ATA,0 +2018,ATG,6.717 +2018,ARG,4.066 +2018,ARM,2.054 +2018,ABW,8.228 +2018,AUS,16.628 +2018,AUT,7.53 +2018,AZE,3.406 +2018,BHS,6.026 +2018,BHR,21.943 +2018,BGD,0.504 +2018,BRB,5.919 +2018,BLR,6.411 +2018,BEL,8.732 +2018,BLZ,1.582 +2018,BEN,0.623 +2018,BMU,10.947 +2018,BTN,1.939 +2018,BOL,1.939 +2018,BES,3.958 +2018,BIH,6.492 +2018,BWA,3.061 +2018,BRA,2.274 +2018,VGB,5.31 +2018,BRN,21.517 +2018,BGR,6.109 +2018,BFA,0.246 +2018,BDI,0.056 +2018,KHM,0.869 +2018,CMR,0.385 +2018,CAN,15.582 +2018,CPV,0.943 +2018,CALF,0.043 +2018,TCD,0.153 +2018,CHL,4.515 +2018,CHN,7.307 +2018,CXR,0 +2018,COL,1.768 +2018,COM,0.378 +2018,COG,1.203 +2018,COK,4.666 +2018,CRI,1.59 +2018,CIV,0.406 +2018,HRV,4.26 +2018,CUB,2.042 +2018,CUW,23.345 +2018,CYP,6.007 +2018,CZE,10.096 +2018,COD,0.039 +2018,DNK,6.002 +2018,DJI,0.411 +2018,DMA,2.276 +2018,DOM,2.392 +2018,TLS,0.489 +2018,ECU,2.258 +2018,EGY,2.351 +2018,SLV,0.998 +2018,GNQ,4.0 +2018,ERI,0.181 +2018,EST,13.527 +2018,SWZ,0.89 +2018,ETH,0.144 +2018,FRO,14.231 +2018,FJI,1.534 +2018,FIN,8.291 +2018,FRA,5.011 +2018,PYF,2.942 +2018,GAB,2.497 +2018,GMB,0.265 +2018,GEO,2.667 +2018,DEU,9.105 +2018,GHA,0.492 +2018,GRC,6.751 +2018,GRL,9.734 +2018,GRD,2.526 +2018,GTM,1.098 +2018,GIN,0.252 +2018,GNB,0.162 +2018,GUY,3.186 +2018,HTI,0.31 +2018,HND,1.007 +2018,HKG,5.694 +2018,HUN,5.067 +2018,ISL,10.395 +2018,IND,1.894 +2018,IDN,2.225 +2018,IRN,8.295 +2018,IRQ,4.527 +2018,IRL,8.07 +2018,ISR,7.093 +2018,ITA,5.842 +2018,JAM,2.876 +2018,JPN,9.043 +2018,JOR,2.404 +2018,KAZ,16.57 +2018,KEN,0.377 +2018,KIR,0.539 +2018,KWT,24.448 +2018,KGZ,1.795 +2018,LAO,2.893 +2018,LVA,4.062 +2018,LBN,4.396 +2018,ALSO,1.079 +2018,LBR,0.086 +2018,LBY,8.339 +2018,LIE,3.742 +2018,LTU,4.671 +2018,LUX,15.749 +2018,MAC,1.908 +2018,MDG,0.141 +2018,MWI,0.094 +2018,MYS,8.093 +2018,MDV,3.553 +2018,MLI,0.246 +2018,MLT,3.146 +2018,MHL,3.185 +2018,MRT,0.858 +2018,MUS,3.442 +2018,MEX,3.792 +2018,FSM,1.288 +2018,MDA,1.733 +2018,MCO,0 +2018,MNG,14.322 +2018,MNE,3.801 +2018,MSR,6.691 +2018,MAR,1.734 +2018,MOZ,0.238 +2018,MMR,0.646 +2018,NAME,1.727 +2018,NRU,4.6 +2018,NPL,0.519 +2018,NLD,9.148 +2018,NCL,20.756 +2018,NZL,7.379 +2018,NIC,0.791 +2018,NER,0.08 +2018,NGA,0.532 +2018,NIU,5.764 +2018,PRK,1.858 +2018,MKD,3.157 +2018,NOR,8.359 +2018,OMN,14.374 +2018,PAK,0.933 +2018,PLW,11.881 +2018,PSE,0.616 +2018,PAN,2.582 +2018,PNG,0.815 +2018,PRY,1.304 +2018,PER,1.734 +2018,PHL,1.303 +2018,POL,8.725 +2018,PRT,4.993 +2018,QAT,34.504 +2018,ROU,4.094 +2018,RUS,11.757 +2018,RWA,0.103 +2018,SHN,2.001 +2018,KNA,5.137 +2018,LCA,2.863 +2018,SPM,9.821 +2018,VCT,2.297 +2018,WSM,1.136 +2018,SMR,0 +2018,STP,0.659 +2018,SAU,19.615 +2018,SEN,0.727 +2018,SRB,5.962 +2018,SYC,5.827 +2018,SLE,0.131 +2018,SGP,8.605 +2018,SXM,16.297 +2018,SVK,6.629 +2018,SVN,6.895 +2018,SLB,0.439 +2018,SOME,0.041 +2018,ZAF,7.591 +2018,KOR,12.968 +2018,SSD,0.167 +2018,ESP,5.74 +2018,LKA,0.931 +2018,SDN,0.518 +2018,SURE,3.519 +2018,SWE,4.137 +2018,CHE,4.33 +2018,SYR,1.677 +2018,TWN,11.945 +2018,TJK,0.883 +2018,TZA,0.204 +2018,THA,4.054 +2018,TGO,0.269 +2018,TON,1.289 +2018,TO,26.801 +2018,TUN,2.609 +2018,TUR,5.097 +2018,TKM,10.513 +2018,TCA,8.737 +2018,TUV,1.01 +2018,UGA,0.133 +2018,UKR,5.212 +2018,ARE,22.897 +2018,GBR,5.716 +2018,USA,16.191 +2018,URY,1.917 +2018,UZB,3.152 +2018,VUT,0.604 +2018,VAT,0 +2018,VEN,3.377 +2018,VNM,2.715 +2018,WLF,2.166 +2018,YEM,0.376 +2018,ZMB,0.416 +2018,ZWE,0.712 +2019,AFG,0.293 +2019,,4.77 +2019,ALB,1.68 +2019,DZA,4.182 +2019,AND,6.334 +2019,AGO,0.594 +2019,AIA,9.512 +2019,ATA,0 +2019,ATG,6.96 +2019,ARG,3.989 +2019,ARM,2.228 +2019,ABW,8.363 +2019,AUS,16.398 +2019,AUT,7.653 +2019,AZE,3.688 +2019,BHS,6.068 +2019,BHR,25.153 +2019,BGD,0.609 +2019,BRB,5.948 +2019,BLR,6.419 +2019,BEL,8.642 +2019,BLZ,1.817 +2019,BEN,0.579 +2019,BMU,9.299 +2019,BTN,1.901 +2019,BOL,1.919 +2019,BES,4.294 +2019,BIH,6.192 +2019,BWA,2.713 +2019,BRA,2.236 +2019,VGB,5.383 +2019,BRN,24.501 +2019,BGR,5.988 +2019,BFA,0.263 +2019,BDI,0.058 +2019,KHM,1.124 +2019,CMR,0.372 +2019,CAN,15.42 +2019,CPV,0.972 +2019,CALF,0.044 +2019,TCD,0.143 +2019,CHL,4.831 +2019,CHN,7.54 +2019,CXR,0 +2019,COL,1.883 +2019,COM,0.394 +2019,COG,1.309 +2019,COK,4.919 +2019,CRI,1.514 +2019,CIV,0.403 +2019,HRV,4.324 +2019,CUB,1.999 +2019,CUW,11.611 +2019,CYP,5.975 +2019,CZE,9.588 +2019,COD,0.04 +2019,DNK,5.34 +2019,DJI,0.418 +2019,DMA,2.41 +2019,DOM,2.608 +2019,TLS,0.527 +2019,ECU,2.31 +2019,EGY,2.425 +2019,SLV,1.202 +2019,GNQ,2.986 +2019,ERI,0.185 +2019,EST,9.287 +2019,SWZ,0.959 +2019,ETH,0.146 +2019,FRO,14.314 +2019,FJI,1.533 +2019,FIN,7.689 +2019,FRA,4.898 +2019,PYF,2.922 +2019,GAB,2.419 +2019,GMB,0.283 +2019,GEO,2.887 +2019,DEU,8.509 +2019,GHA,0.521 +2019,GRC,6.219 +2019,GRL,9.926 +2019,GRD,2.687 +2019,GTM,1.14 +2019,GIN,0.304 +2019,GNB,0.164 +2019,GUY,3.45 +2019,HTI,0.277 +2019,HND,1.127 +2019,HKG,5.59 +2019,HUN,5.046 +2019,ISL,9.864 +2019,IND,1.889 +2019,IDN,2.414 +2019,IRN,8.025 +2019,IRQ,4.562 +2019,IRL,7.624 +2019,ISR,6.815 +2019,ITA,5.699 +2019,JAM,2.78 +2019,JPN,8.781 +2019,JOR,2.311 +2019,KAZ,14.679 +2019,KEN,0.389 +2019,KIR,0.531 +2019,KWT,24.519 +2019,KGZ,1.431 +2019,LAO,2.709 +2019,LVA,3.992 +2019,LBN,4.438 +2019,ALSO,1.073 +2019,LBR,0.205 +2019,LBY,10.736 +2019,LIE,3.871 +2019,LTU,4.801 +2019,LUX,15.758 +2019,MAC,1.827 +2019,MDG,0.155 +2019,MWI,0.087 +2019,MYS,8.154 +2019,MDV,3.871 +2019,MLI,0.284 +2019,MLT,3.274 +2019,MHL,3.275 +2019,MRT,0.861 +2019,MUS,3.488 +2019,MEX,3.744 +2019,FSM,1.316 +2019,MDA,1.767 +2019,MCO,0 +2019,MNG,14.619 +2019,MNE,3.929 +2019,MSR,4.855 +2019,MAR,1.894 +2019,MOZ,0.234 +2019,MMR,0.634 +2019,NAME,1.68 +2019,NRU,4.521 +2019,NPL,0.471 +2019,NLD,8.783 +2019,NCL,19.879 +2019,NZL,7.437 +2019,NIC,0.784 +2019,NER,0.107 +2019,NGA,0.626 +2019,NIU,3.771 +2019,PRK,2.049 +2019,MKD,3.617 +2019,NOR,8.001 +2019,OMN,12.867 +2019,PAK,0.923 +2019,PLW,12.255 +2019,PSE,0.665 +2019,PAN,3.161 +2019,PNG,0.819 +2019,PRY,1.246 +2019,PER,1.778 +2019,PHL,1.306 +2019,POL,8.253 +2019,PRT,4.616 +2019,QAT,35.985 +2019,ROU,3.938 +2019,RUS,11.699 +2019,RWA,0.107 +2019,SHN,2.679 +2019,KNA,5.296 +2019,LCA,2.811 +2019,SPM,11.081 +2019,VCT,2.165 +2019,WSM,1.279 +2019,SMR,0 +2019,STP,0.666 +2019,SAU,19.737 +2019,SEN,0.795 +2019,SRB,5.983 +2019,SYC,5.686 +2019,SLE,0.125 +2019,SGP,5.758 +2019,SXM,16.019 +2019,SVK,6.193 +2019,SVN,6.645 +2019,SLB,0.434 +2019,SOME,0.04 +2019,ZAF,8.013 +2019,KOR,12.472 +2019,SSD,0.17 +2019,ESP,5.318 +2019,LKA,0.981 +2019,SDN,0.504 +2019,SURE,4.391 +2019,SWE,3.994 +2019,CHE,4.283 +2019,SYR,1.427 +2019,TWN,11.521 +2019,TJK,0.949 +2019,TZA,0.23 +2019,THA,3.953 +2019,TGO,0.293 +2019,TON,1.536 +2019,TO,26.832 +2019,TUN,2.519 +2019,TUR,4.824 +2019,TKM,10.607 +2019,TCA,8.415 +2019,TUV,1.001 +2019,UGA,0.133 +2019,UKR,5.02 +2019,ARE,23.835 +2019,GBR,5.462 +2019,USA,15.74 +2019,URY,1.893 +2019,UZB,3.288 +2019,VUT,0.542 +2019,VAT,0 +2019,VEN,3.024 +2019,VNM,3.569 +2019,WLF,2.186 +2019,YEM,0.375 +2019,ZMB,0.422 +2019,ZWE,0.637 +2020,AFG,0.305 +2020,,4.465 +2020,ALB,1.751 +2020,DZA,3.91 +2020,AND,4.808 +2020,AGO,0.502 +2020,AIA,9.153 +2020,ATA,0 +2020,ATG,6.682 +2020,ARG,3.705 +2020,ARM,2.423 +2020,ABW,8.353 +2020,AUS,15.453 +2020,AUT,6.974 +2020,AZE,3.515 +2020,BHS,5.372 +2020,BHR,25.311 +2020,BGD,0.559 +2020,BRB,4.394 +2020,BLR,6.13 +2020,BEL,7.88 +2020,BLZ,1.553 +2020,BEN,0.613 +2020,BMU,7.151 +2020,BTN,1.191 +2020,BOL,1.535 +2020,BES,4.339 +2020,BIH,6.278 +2020,BWA,2.18 +2020,BRA,2.085 +2020,VGB,5.212 +2020,BRN,25.132 +2020,BGR,5.235 +2020,BFA,0.241 +2020,BDI,0.06 +2020,KHM,1.161 +2020,CMR,0.366 +2020,CAN,13.8 +2020,CPV,0.887 +2020,CALF,0.045 +2020,TCD,0.137 +2020,CHL,4.154 +2020,CHN,7.659 +2020,CXR,0 +2020,COL,1.762 +2020,COM,0.473 +2020,COG,1.296 +2020,COK,3.868 +2020,CRI,1.32 +2020,CIV,0.411 +2020,HRV,4.118 +2020,CUB,1.883 +2020,CUW,9.542 +2020,CYP,5.584 +2020,CZE,8.707 +2020,COD,0.04 +2020,DNK,4.858 +2020,DJI,0.351 +2020,DMA,2.188 +2020,DOM,2.137 +2020,TLS,0.497 +2020,ECU,1.871 +2020,EGY,2.118 +2020,SLV,1.048 +2020,GNQ,3.462 +2020,ERI,0.18 +2020,EST,6.949 +2020,SWZ,0.925 +2020,ETH,0.152 +2020,FRO,14.463 +2020,FJI,1.131 +2020,FIN,6.824 +2020,FRA,4.366 +2020,PYF,2.803 +2020,GAB,2.586 +2020,GMB,0.272 +2020,GEO,2.808 +2020,DEU,7.767 +2020,GHA,0.596 +2020,GRC,5.291 +2020,GRL,9.584 +2020,GRD,2.341 +2020,GTM,0.999 +2020,GIN,0.341 +2020,GNB,0.147 +2020,GUY,3.999 +2020,HTI,0.222 +2020,HND,0.941 +2020,HKG,4.466 +2020,HUN,4.855 +2020,ISL,9.108 +2020,IND,1.734 +2020,IDN,2.229 +2020,IRN,7.779 +2020,IRQ,3.756 +2020,IRL,7.101 +2020,ISR,6.36 +2020,ITA,5.097 +2020,JAM,2.291 +2020,JPN,8.302 +2020,JOR,1.933 +2020,KAZ,13.461 +2020,KEN,0.423 +2020,KIR,0.521 +2020,KWT,22.409 +2020,KGZ,1.299 +2020,LAO,2.688 +2020,LVA,3.69 +2020,LBN,3.934 +2020,ALSO,0.964 +2020,LBR,0.151 +2020,LBY,7.074 +2020,LIE,3.66 +2020,LTU,4.8 +2020,LUX,12.798 +2020,MAC,1.528 +2020,MDG,0.14 +2020,MWI,0.097 +2020,MYS,8.11 +2020,MDV,3.205 +2020,MLI,0.302 +2020,MLT,3.099 +2020,MHL,3.374 +2020,MRT,0.915 +2020,MUS,2.933 +2020,MEX,3.51 +2020,FSM,1.307 +2020,MDA,1.699 +2020,MCO,0 +2020,MNG,11.255 +2020,MNE,3.827 +2020,MSR,4.857 +2020,MAR,1.76 +2020,MOZ,0.205 +2020,MMR,0.648 +2020,NAME,1.479 +2020,NRU,4.157 +2020,NPL,0.508 +2020,NLD,7.84 +2020,NCL,17.839 +2020,NZL,6.765 +2020,NIC,0.703 +2020,NER,0.116 +2020,NGA,0.594 +2020,NIU,3.733 +2020,PRK,1.926 +2020,MKD,3.294 +2020,NOR,7.664 +2020,OMN,14.401 +2020,PAK,0.883 +2020,PLW,11.807 +2020,PSE,0.663 +2020,PAN,2.404 +2020,PNG,0.792 +2020,PRY,1.113 +2020,PER,1.41 +2020,PHL,1.184 +2020,POL,7.87 +2020,PRT,4.049 +2020,QAT,37.133 +2020,ROU,3.808 +2020,RUS,11.214 +2020,RWA,0.104 +2020,SHN,3.363 +2020,KNA,4.843 +2020,LCA,2.698 +2020,SPM,10.504 +2020,VCT,2.346 +2020,WSM,1.125 +2020,SMR,0 +2020,STP,0.637 +2020,SAU,16.967 +2020,SEN,0.658 +2020,SRB,6.139 +2020,SYC,5.763 +2020,SLE,0.125 +2020,SGP,9.275 +2020,SXM,14.946 +2020,SVK,5.699 +2020,SVN,6.07 +2020,SLB,0.419 +2020,SOME,0.036 +2020,ZAF,7.395 +2020,KOR,11.527 +2020,SSD,0.16 +2020,ESP,4.51 +2020,LKA,0.95 +2020,SDN,0.462 +2020,SURE,4.807 +2020,SWE,3.538 +2020,CHE,3.963 +2020,SYR,1.267 +2020,TWN,11.409 +2020,TJK,0.974 +2020,TZA,0.23 +2020,THA,3.803 +2020,TGO,0.282 +2020,TON,1.74 +2020,TO,23.074 +2020,TUN,2.343 +2020,TUR,4.908 +2020,TKM,10.831 +2020,TCA,8.106 +2020,TUV,0.991 +2020,UGA,0.125 +2020,UKR,4.71 +2020,ARE,23.34 +2020,GBR,4.865 +2020,USA,14.034 +2020,URY,1.889 +2020,UZB,3.27 +2020,VUT,0.647 +2020,VAT,0 +2020,VEN,2.175 +2020,VNM,3.759 +2020,WLF,2.196 +2020,YEM,0.337 +2020,ZMB,0.43 +2020,ZWE,0.501 +2021,AFG,0.306 +2021,,4.655 +2021,ALB,1.718 +2021,DZA,4.08 +2021,AND,4.592 +2021,AGO,0.507 +2021,AIA,8.727 +2021,ATA,0 +2021,ATG,6.4 +2021,ARG,4.191 +2021,ARM,2.531 +2021,ABW,8.053 +2021,AUS,14.915 +2021,AUT,7.399 +2021,AZE,3.717 +2021,BHS,5.15 +2021,BHR,26.053 +2021,BGD,0.583 +2021,BRB,4.361 +2021,BLR,6.362 +2021,BEL,8.239 +2021,BLZ,1.755 +2021,BEN,0.64 +2021,BMU,6.873 +2021,BTN,1.366 +2021,BOL,1.799 +2021,BES,4.095 +2021,BIH,6.095 +2021,BWA,2.371 +2021,BRA,2.32 +2021,VGB,4.988 +2021,BRN,25.376 +2021,BGR,6.14 +2021,BFA,0.266 +2021,BDI,0.064 +2021,KHM,1.213 +2021,CMR,0.379 +2021,CAN,14.079 +2021,CPV,0.951 +2021,CALF,0.046 +2021,TCD,0.144 +2021,CHL,4.568 +2021,CHN,7.95 +2021,CXR,0 +2021,COL,1.859 +2021,COM,0.505 +2021,COG,1.243 +2021,COK,4.042 +2021,CRI,1.485 +2021,CIV,0.427 +2021,HRV,4.288 +2021,CUB,1.86 +2021,CUW,9.145 +2021,CYP,5.65 +2021,CZE,9.197 +2021,COD,0.041 +2021,DNK,5.058 +2021,DJI,0.411 +2021,DMA,2.096 +2021,DOM,2.139 +2021,TLS,0.502 +2021,ECU,2.232 +2021,EGY,2.26 +2021,SLV,1.186 +2021,GNQ,3.444 +2021,ERI,0.193 +2021,EST,7.842 +2021,SWZ,0.949 +2021,ETH,0.157 +2021,FRO,13.924 +2021,FJI,1.174 +2021,FIN,6.855 +2021,FRA,4.754 +2021,PYF,2.905 +2021,GAB,2.512 +2021,GMB,0.287 +2021,GEO,2.942 +2021,DEU,8.138 +2021,GHA,0.639 +2021,GRC,5.51 +2021,GRL,10.072 +2021,GRD,2.647 +2021,GTM,1.118 +2021,GIN,0.36 +2021,GNB,0.156 +2021,GUY,4.43 +2021,HTI,0.212 +2021,HND,1.054 +2021,HKG,4.373 +2021,HUN,5.002 +2021,ISL,9.478 +2021,IND,1.9 +2021,IDN,2.25 +2021,IRN,7.826 +2021,IRQ,3.873 +2021,IRL,7.53 +2021,ISR,6.163 +2021,ITA,5.693 +2021,JAM,2.337 +2021,JPN,8.523 +2021,JOR,2.033 +2021,KAZ,13.291 +2021,KEN,0.461 +2021,KIR,0.534 +2021,KWT,24.301 +2021,KGZ,1.445 +2021,LAO,3.147 +2021,LVA,3.864 +2021,LBN,4.197 +2021,ALSO,1.087 +2021,LBR,0.167 +2021,LBY,9.491 +2021,LIE,3.735 +2021,LTU,4.963 +2021,LUX,13.185 +2021,MAC,1.557 +2021,MDG,0.148 +2021,MWI,0.104 +2021,MYS,8.306 +2021,MDV,3.299 +2021,MLI,0.316 +2021,MLT,3.05 +2021,MHL,3.635 +2021,MRT,0.965 +2021,MUS,3.149 +2021,MEX,3.7 +2021,FSM,1.352 +2021,MDA,1.828 +2021,MCO,0 +2021,MNG,11.427 +2021,MNE,3.671 +2021,MSR,4.773 +2021,MAR,1.959 +2021,MOZ,0.224 +2021,MMR,0.662 +2021,NAME,1.475 +2021,NRU,4.271 +2021,NPL,0.52 +2021,NLD,7.994 +2021,NCL,15.763 +2021,NZL,6.69 +2021,NIC,0.786 +2021,NER,0.12 +2021,NGA,0.614 +2021,NIU,3.908 +2021,PRK,1.977 +2021,MKD,3.656 +2021,NOR,7.594 +2021,OMN,15.677 +2021,PAK,0.966 +2021,PLW,12.285 +2021,PSE,0.668 +2021,PAN,2.691 +2021,PNG,0.799 +2021,PRY,1.382 +2021,PER,1.675 +2021,PHL,1.254 +2021,POL,8.643 +2021,PRT,3.881 +2021,QAT,39.884 +2021,ROU,3.994 +2021,RUS,11.798 +2021,RWA,0.115 +2021,SHN,3.252 +2021,KNA,4.67 +2021,LCA,2.594 +2021,SPM,10.165 +2021,VCT,2.267 +2021,WSM,1.153 +2021,SMR,0 +2021,STP,0.653 +2021,SAU,17.564 +2021,SEN,0.684 +2021,SRB,6.017 +2021,SYC,6.223 +2021,SLE,0.132 +2021,SGP,9.397 +2021,SXM,14.264 +2021,SVK,6.455 +2021,SVN,6.162 +2021,SLB,0.427 +2021,SOME,0.038 +2021,ZAF,7.166 +2021,KOR,11.886 +2021,SSD,0.172 +2021,ESP,4.849 +2021,LKA,0.924 +2021,SDN,0.486 +2021,SURE,6.008 +2021,SWE,3.681 +2021,CHE,4.118 +2021,SYR,1.276 +2021,TWN,12.205 +2021,TJK,1.024 +2021,TZA,0.242 +2021,THA,3.732 +2021,TGO,0.296 +2021,TON,1.803 +2021,TO,23.29 +2021,TUN,2.874 +2021,TUR,5.34 +2021,TKM,11.034 +2021,TCA,7.665 +2021,TUV,1.021 +2021,UGA,0.132 +2021,UKR,4.828 +2021,ARE,25.333 +2021,GBR,5.164 +2021,USA,14.932 +2021,URY,2.365 +2021,UZB,3.415 +2021,VUT,0.659 +2021,VAT,0 +2021,VEN,2.539 +2021,VNM,3.617 +2021,WLF,2.296 +2021,YEM,0.351 +2021,ZMB,0.445 +2021,ZWE,0.525 +2022,AFG,0.295 +2022,,4.658 +2022,ALB,1.743 +2022,DZA,3.927 +2022,AND,4.617 +2022,AGO,0.452 +2022,AIA,8.753 +2022,ATA,0 +2022,ATG,6.422 +2022,ARG,4.238 +2022,ARM,2.305 +2022,ABW,8.133 +2022,AUS,14.985 +2022,AUT,6.878 +2022,AZE,3.675 +2022,BHS,5.171 +2022,BHR,25.672 +2022,BGD,0.596 +2022,BRB,4.377 +2022,BLR,6.167 +2022,BEL,7.688 +2022,BLZ,1.789 +2022,BEN,0.631 +2022,BMU,6.937 +2022,BTN,1.349 +2022,BOL,1.758 +2022,BES,4.083 +2022,BIH,6.103 +2022,BWA,2.839 +2022,BRA,2.245 +2022,VGB,5.004 +2022,BRN,23.95 +2022,BGR,6.804 +2022,BFA,0.263 +2022,BDI,0.062 +2022,KHM,1.19 +2022,CMR,0.343 +2022,CAN,14.249 +2022,CPV,0.959 +2022,CALF,0.041 +2022,TCD,0.134 +2022,CHL,4.304 +2022,CHN,7.993 +2022,CXR,0 +2022,COL,1.922 +2022,COM,0.493 +2022,COG,1.245 +2022,COK,3.995 +2022,CRI,1.523 +2022,CIV,0.417 +2022,HRV,4.349 +2022,CUB,1.866 +2022,CUW,9.189 +2022,CYP,5.617 +2022,CZE,9.336 +2022,COD,0.036 +2022,DNK,4.94 +2022,DJI,0.404 +2022,DMA,2.106 +2022,DOM,2.105 +2022,TLS,0.499 +2022,ECU,2.312 +2022,EGY,2.333 +2022,SLV,1.217 +2022,GNQ,3.031 +2022,ERI,0.189 +2022,EST,7.776 +2022,SWZ,1.053 +2022,ETH,0.155 +2022,FRO,14.085 +2022,FJI,1.155 +2022,FIN,6.527 +2022,FRA,4.604 +2022,PYF,2.851 +2022,GAB,2.388 +2022,GMB,0.285 +2022,GEO,2.963 +2022,DEU,7.984 +2022,GHA,0.622 +2022,GRC,5.745 +2022,GRL,10.474 +2022,GRD,2.713 +2022,GTM,1.076 +2022,GIN,0.357 +2022,GNB,0.155 +2022,GUY,4.374 +2022,HTI,0.211 +2022,HND,1.07 +2022,HKG,4.082 +2022,HUN,4.45 +2022,ISL,9.5 +2022,IND,1.997 +2022,IDN,2.646 +2022,IRN,7.799 +2022,IRQ,4.025 +2022,IRL,7.721 +2022,ISR,6.209 +2022,ITA,5.727 +2022,JAM,2.295 +2022,JPN,8.502 +2022,JOR,2.03 +2022,KAZ,13.98 +2022,KEN,0.46 +2022,KIR,0.518 +2022,KWT,25.578 +2022,KGZ,1.425 +2022,LAO,3.08 +2022,LVA,3.562 +2022,LBN,4.354 +2022,ALSO,1.359 +2022,LBR,0.165 +2022,LBY,9.242 +2022,LIE,3.81 +2022,LTU,4.606 +2022,LUX,11.618 +2022,MAC,1.513 +2022,MDG,0.149 +2022,MWI,0.103 +2022,MYS,8.577 +2022,MDV,3.248 +2022,MLI,0.312 +2022,MLT,3.104 +2022,MHL,3.635 +2022,MRT,0.957 +2022,MUS,3.27 +2022,MEX,4.015 +2022,FSM,1.324 +2022,MDA,1.657 +2022,MCO,0 +2022,MNG,11.151 +2022,MNE,3.656 +2022,MSR,4.845 +2022,MAR,1.826 +2022,MOZ,0.243 +2022,MMR,0.645 +2022,NAME,1.54 +2022,NRU,4.17 +2022,NPL,0.507 +2022,NLD,7.137 +2022,NCL,17.641 +2022,NZL,6.212 +2022,NIC,0.799 +2022,NER,0.117 +2022,NGA,0.589 +2022,NIU,3.873 +2022,PRK,1.951 +2022,MKD,3.625 +2022,NOR,7.509 +2022,OMN,15.73 +2022,PAK,0.849 +2022,PLW,12.124 +2022,PSE,0.666 +2022,PAN,2.699 +2022,PNG,0.771 +2022,PRY,1.33 +2022,PER,1.789 +2022,PHL,1.301 +2022,POL,8.107 +2022,PRT,4.051 +2022,QAT,37.601 +2022,ROU,3.74 +2022,RUS,11.417 +2022,RWA,0.112 +2022,SHN,3.299 +2022,KNA,4.708 +2022,LCA,2.615 +2022,SPM,10.293 +2022,VCT,2.296 +2022,WSM,1.122 +2022,SMR,0 +2022,STP,0.582 +2022,SAU,18.197 +2022,SEN,0.674 +2022,SRB,6.025 +2022,SYC,6.15 +2022,SLE,0.131 +2022,SGP,8.912 +2022,SXM,14.352 +2022,SVK,6.052 +2022,SVN,5.998 +2022,SLB,0.412 +2022,SOME,0.037 +2022,ZAF,6.746 +2022,KOR,11.599 +2022,SSD,0.168 +2022,ESP,5.164 +2022,LKA,0.794 +2022,SDN,0.47 +2022,SURE,5.803 +2022,SWE,3.607 +2022,CHE,4.048 +2022,SYR,1.249 +2022,TWN,11.631 +2022,TJK,1.006 +2022,TZA,0.238 +2022,THA,3.776 +2022,TGO,0.291 +2022,TON,1.769 +2022,TO,22.424 +2022,TUN,2.879 +2022,TUR,5.105 +2022,TKM,11.034 +2022,TCA,7.637 +2022,TUV,1.0 +2022,UGA,0.127 +2022,UKR,3.558 +2022,ARE,25.833 +2022,GBR,4.72 +2022,USA,14.95 +2022,URY,2.306 +2022,UZB,3.483 +2022,VUT,0.636 +2022,VAT,0 +2022,VEN,2.717 +2022,VNM,3.5 +2022,WLF,2.282 +2022,YEM,0.337 +2022,ZMB,0.446 +2022,ZWE,0.543 diff --git a/examples/ReadKml.py b/examples/read_kml.py old mode 100644 new mode 100755 similarity index 97% rename from examples/ReadKml.py rename to examples/read_kml.py index 77f67266..d8d78a32 --- a/examples/ReadKml.py +++ b/examples/read_kml.py @@ -30,7 +30,7 @@ # Create the KML object to store the parsed result # Read in the KML string -k = kml.KML.class_from_string(doc.encode("utf-8")) +k = kml.KML.from_string(doc.encode("utf-8")) # Next we perform some simple sanity checks diff --git a/examples/shp2kml.py b/examples/shp2kml.py new file mode 100755 index 00000000..1b0a1634 --- /dev/null +++ b/examples/shp2kml.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python +import csv +import pathlib +import random + +import shapefile +from pygeoif.factories import force_3d +from pygeoif.factories import shape + +import fastkml +import fastkml.containers +import fastkml.features +import fastkml.styles +from fastkml.enums import AltitudeMode +from fastkml.enums import ColorMode +from fastkml.geometry import create_kml_geometry + +examples_dir = pathlib.Path(__file__).parent + +shp = shapefile.Reader(examples_dir / "ne_110m_admin_0_countries.shp") + +co2_csv = pathlib.Path(examples_dir / "owid-co2-data.csv") + +co2_data = {} + +with co2_csv.open() as csvfile: + reader = csv.DictReader(csvfile) + for row in reader: + if row["year"] == "2020": + co2_data[row["iso_code"]] = ( + float(row["co2_per_capita"]) if row["co2_per_capita"] else 0 + ) + +document = fastkml.containers.Document() + +for feature in shp.__geo_interface__["features"]: + geometry = shape(feature["geometry"]) + co2_emission = co2_data.get(feature["properties"]["ADM0_A3"], 0) + geometry = force_3d(geometry, co2_emission * 100_000) + kml_geometry = create_kml_geometry( + geometry, + extrude=True, + altitude_mode=AltitudeMode.relative_to_ground, + ) + color = random.randint(0, 0xFFFFFF) + style = fastkml.styles.Style( + id=feature["properties"]["ADM0_A3"], + styles=[ + fastkml.styles.LineStyle(color=f"55{color:06X}", width=2), + fastkml.styles.PolyStyle( + color_mode=ColorMode.random, + color=f"88{color:06X}", + fill=True, + outline=True, + ), + ], + ) + + style_url = fastkml.styles.StyleUrl(url=f"#{feature['properties']['ADM0_A3']}") + placemark = fastkml.features.Placemark( + name=feature["properties"]["NAME"], + description=feature["properties"]["FORMAL_EN"], + kml_geometry=kml_geometry, + styles=[style], + ) + document.append(placemark) + +kml = fastkml.KML(features=[document]) + +outfile = pathlib.Path("co2_per_capita_2020.kml") +with outfile.open("w") as f: + f.write(kml.to_string(prettyprint=True, precision=3)) diff --git a/examples/shp2kml_timed.py b/examples/shp2kml_timed.py new file mode 100755 index 00000000..2f7dd343 --- /dev/null +++ b/examples/shp2kml_timed.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python +import csv +import datetime +import pathlib +import random + +import shapefile +from pygeoif.factories import force_3d +from pygeoif.factories import shape + +import fastkml +import fastkml.containers +import fastkml.features +import fastkml.styles +import fastkml.times +from fastkml.enums import AltitudeMode +from fastkml.enums import DateTimeResolution +from fastkml.geometry import create_kml_geometry + +examples_dir = pathlib.Path(__file__).parent + +shp = shapefile.Reader(examples_dir / "ne_110m_admin_0_countries.shp") + +co2_csv = pathlib.Path(examples_dir / "owid-co2-data.csv") + +co2_pa = {str(i): {} for i in range(1995, 2023)} + +with co2_csv.open() as csvfile: + reader = csv.DictReader(csvfile) + for row in reader: + if row["year"] >= "1995": + co2_pa[row["year"]][row["iso_code"]] = ( + float(row["co2_per_capita"]) if row["co2_per_capita"] else 0 + ) + +styles = [] +folders = [] +for feature in shp.__geo_interface__["features"]: + iso3_code = feature["properties"]["ADM0_A3"] + geometry = shape(feature["geometry"]) + color = random.randint(0, 0xFFFFFF) + styles.append( + fastkml.styles.Style( + id=iso3_code, + styles=[ + fastkml.styles.LineStyle(color=f"33{color:06X}", width=2), + fastkml.styles.PolyStyle( + color=f"88{color:06X}", + fill=True, + outline=True, + ), + ], + ), + ) + style_url = fastkml.styles.StyleUrl(url=f"#{iso3_code}") + folder = fastkml.containers.Folder(name=feature["properties"]["NAME"]) + co2_growth = 0 + for year in range(1995, 2023): + co2_year = co2_pa[str(year)].get(iso3_code, 0) + co2_growth += co2_year + + kml_geometry = create_kml_geometry( + force_3d(geometry, co2_growth * 5_000), + extrude=True, + altitude_mode=AltitudeMode.relative_to_ground, + ) + timespan = fastkml.times.TimeSpan( + begin=fastkml.times.KmlDateTime( + datetime.date(year, 1, 1), + resolution=DateTimeResolution.year_month, + ), + end=fastkml.times.KmlDateTime( + datetime.date(year, 12, 31), + resolution=DateTimeResolution.year_month, + ), + ) + placemark = fastkml.features.Placemark( + name=f"{feature['properties']['NAME']} - {year}", + description=feature["properties"]["FORMAL_EN"], + kml_geometry=kml_geometry, + style_url=style_url, + times=timespan, + ) + folder.features.append(placemark) + folders.append(folder) + +document = fastkml.containers.Document(features=folders, styles=styles) +kml = fastkml.KML(features=[document]) + +outfile = pathlib.Path("co2_growth_1995_2022.kml") +with outfile.open("w") as f: + f.write(kml.to_string(prettyprint=True, precision=3)) diff --git a/examples/UsageExamples.py b/examples/simple_example.py old mode 100644 new mode 100755 similarity index 57% rename from examples/UsageExamples.py rename to examples/simple_example.py index 82b98379..c5b9e139 --- a/examples/UsageExamples.py +++ b/examples/simple_example.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +import pathlib from fastkml import kml @@ -8,14 +9,15 @@ def print_child_features(element, depth=0): if not getattr(element, "features", None): return for feature in element.features: - print(" " * depth + feature.name) + print(" " * depth, feature.name) print_child_features(feature, depth + 1) if __name__ == "__main__": - fname = "KML_Samples.kml" + examples_dir = pathlib.Path(__file__).parent + fname = pathlib.Path(examples_dir / "KML_Samples.kml") - with open(fname, encoding="utf-8") as kml_file: - k = kml.KML.class_from_string(kml_file.read().encode("utf-8")) + with fname.open(encoding="utf-8") as kml_file: + k = kml.KML.from_string(kml_file.read().encode("utf-8")) print_child_features(k) diff --git a/examples/transform_cascading_style.py b/examples/transform_cascading_style.py new file mode 100755 index 00000000..42d7df89 --- /dev/null +++ b/examples/transform_cascading_style.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python +import pathlib +from typing import Any +from typing import Dict +from typing import Optional + +from fastkml import KML +from fastkml import Document +from fastkml import Style +from fastkml import config +from fastkml.helpers import xml_subelement +from fastkml.helpers import xml_subelement_kwarg +from fastkml.helpers import xml_subelement_list +from fastkml.helpers import xml_subelement_list_kwarg +from fastkml.kml_base import _BaseObject +from fastkml.registry import RegistryItem +from fastkml.registry import registry +from fastkml.utils import find + +examples_dir = pathlib.Path(__file__).parent + + +class CascadingStyle(_BaseObject): + """ + CascadingStyle. + + The ```` is an undocumented element that is created in + Google Earth Web that is unsupported by Google Earth Pro + """ + + _default_nsid = config.GX + + def __init__( + self, + ns: Optional[str] = None, + name_spaces: Optional[Dict[str, str]] = None, + id: Optional[str] = None, + target_id: Optional[str] = None, + style: Optional[Style] = None, + **kwargs: Any, + ) -> None: + """Initialize the CascadingStyle object.""" + self.style = style + super().__init__(ns, name_spaces, id, target_id, **kwargs) + + +registry.register( + CascadingStyle, + RegistryItem( + ns_ids=("kml",), + attr_name="style", + node_name="Style", + classes=(Style,), + get_kwarg=xml_subelement_kwarg, + set_element=xml_subelement, + ), +) + + +registry.register( + Document, + RegistryItem( + ns_ids=("gx",), + attr_name="gx_cascading_style", + node_name="CascadingStyle", + classes=(CascadingStyle,), + get_kwarg=xml_subelement_list_kwarg, + set_element=xml_subelement_list, + ), +) + +cs_kml = KML.parse(examples_dir / "gx_cascading_style.kml") +document = find(cs_kml, of_type=Document) +for cascading_style in document.gx_cascading_style: + kml_style = cascading_style.style + kml_style.id = cascading_style.id + document.styles.append(kml_style) + +document.gx_cascading_style = [] +print(cs_kml.to_string(prettyprint=True)) diff --git a/fastkml/__init__.py b/fastkml/__init__.py index a6ca8b7e..d0cb773f 100644 --- a/fastkml/__init__.py +++ b/fastkml/__init__.py @@ -25,9 +25,9 @@ functionality that KML on google earth provides. """ from fastkml.about import __version__ # noqa: F401 -from fastkml.atom import Author -from fastkml.atom import Contributor -from fastkml.atom import Link +from fastkml.atom import Author as AtomAuthor +from fastkml.atom import Contributor as AtomContributor +from fastkml.atom import Link as AtomLink from fastkml.containers import Document from fastkml.containers import Folder from fastkml.data import Data @@ -35,7 +35,14 @@ from fastkml.data import Schema from fastkml.data import SchemaData from fastkml.features import Placemark +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.kml import KML +from fastkml.links import Icon +from fastkml.links import Link from fastkml.overlays import GroundOverlay from fastkml.overlays import PhotoOverlay from fastkml.styles import BalloonStyle @@ -72,9 +79,16 @@ "PolyStyle", "LabelStyle", "BalloonStyle", + "AtomLink", + "Icon", "Link", - "Author", - "Contributor", + "Point", + "LineString", + "LinearRing", + "Polygon", + "MultiGeometry", + "AtomAuthor", + "AtomContributor", "Camera", "LookAt", ] diff --git a/fastkml/about.py b/fastkml/about.py index ee30bf21..c3030e31 100644 --- a/fastkml/about.py +++ b/fastkml/about.py @@ -18,4 +18,4 @@ The only purpose of this module is to provide a version number for the package. """ -__version__ = "1.0.a13" +__version__ = "1.0.0b1" diff --git a/fastkml/atom.py b/fastkml/atom.py index 1a75b8dd..fde10d0b 100644 --- a/fastkml/atom.py +++ b/fastkml/atom.py @@ -40,7 +40,6 @@ from fastkml import config from fastkml.base import _XMLObject -from fastkml.config import ATOMNS as NS from fastkml.helpers import attribute_int_kwarg from fastkml.helpers import attribute_text_kwarg from fastkml.helpers import int_attribute @@ -83,11 +82,11 @@ class Link(_AtomObject): title, and length. """ - href: Optional[str] - rel: Optional[str] - type: Optional[str] - hreflang: Optional[str] - title: Optional[str] + href: str + rel: str + type: str + hreflang: str + title: str length: Optional[int] def __init__( @@ -134,11 +133,11 @@ def __init__( """ super().__init__(ns=ns, name_spaces=name_spaces, **kwargs) - self.href = href - self.rel = rel - self.type = type - self.hreflang = hreflang - self.title = title + self.href = href or "" + self.rel = rel or "" + self.type = type or "" + self.hreflang = hreflang or "" + self.title = title or "" self.length = length def __repr__(self) -> str: @@ -259,9 +258,9 @@ class _Person(_AtomObject): """ - name: Optional[str] - uri: Optional[str] - email: Optional[str] + name: str + uri: str + email: str def __init__( self, @@ -286,9 +285,9 @@ def __init__( """ super().__init__(ns=ns, name_spaces=name_spaces, **kwargs) - self.name = name - self.uri = uri - self.email = email + self.name = name.strip() if name else "" + self.uri = uri.strip() if uri else "" + self.email = email.strip() if email else "" def __repr__(self) -> str: """ @@ -373,4 +372,4 @@ class Contributor(_Person): """ -__all__ = ["Author", "Contributor", "Link", "NS"] +__all__ = ["Author", "Contributor", "Link"] diff --git a/fastkml/base.py b/fastkml/base.py index 15c00da7..f67f7de1 100644 --- a/fastkml/base.py +++ b/fastkml/base.py @@ -115,10 +115,7 @@ def __eq__(self, other: object) -> bool: True if the objects are equal, False otherwise. """ - if type(self) is not type(other): - return False - assert isinstance(other, type(self)) # noqa: S101 - return self.__dict__ == other.__dict__ + return self.__dict__ == other.__dict__ if type(self) is type(other) else False def etree_element( self, @@ -152,6 +149,7 @@ def etree_element( node_name=item.node_name, precision=precision, verbosity=verbosity, + default=item.default, ) return element @@ -345,7 +343,7 @@ def class_from_element( ) @classmethod - def class_from_string( + def from_string( cls, string: str, *, diff --git a/fastkml/containers.py b/fastkml/containers.py index bf1fa398..95dee533 100644 --- a/fastkml/containers.py +++ b/fastkml/containers.py @@ -44,6 +44,7 @@ from fastkml.styles import StyleUrl from fastkml.times import TimeSpan from fastkml.times import TimeStamp +from fastkml.utils import find_all from fastkml.views import Camera from fastkml.views import LookAt from fastkml.views import Region @@ -306,7 +307,14 @@ def get_style_by_url(self, style_url: str) -> Optional[Union[Style, StyleMap]]: """ id_ = urlparse.urlparse(style_url).fragment - return next((style for style in self.styles if style.id == id_), None) + return next( + find_all( # type: ignore[arg-type] + self, + of_type=(Style, StyleMap), + id=id_, + ), + None, + ) registry.register( diff --git a/fastkml/data.py b/fastkml/data.py index 23dafdfb..9d02a80b 100644 --- a/fastkml/data.py +++ b/fastkml/data.py @@ -156,6 +156,17 @@ def __bool__(self) -> bool: return bool(self.name) and bool(self.type) +registry.register( + SimpleField, + RegistryItem( + ns_ids=("kml",), + attr_name="display_name", + node_name="displayName", + classes=(str,), + get_kwarg=subelement_text_kwarg, + set_element=text_subelement, + ), +) registry.register( SimpleField, RegistryItem( @@ -178,17 +189,6 @@ def __bool__(self) -> bool: set_element=enum_attribute, ), ) -registry.register( - SimpleField, - RegistryItem( - ns_ids=("kml",), - attr_name="display_name", - node_name="displayName", - classes=(str,), - get_kwarg=subelement_text_kwarg, - set_element=text_subelement, - ), -) class Schema(_BaseObject): @@ -409,13 +409,12 @@ def __bool__(self) -> bool: set_element=text_attribute, ), ) - registry.register( Data, RegistryItem( - ns_ids=("", "kml"), - attr_name="value", - node_name="value", + ns_ids=("kml",), + attr_name="display_name", + node_name="displayName", classes=(str,), get_kwarg=subelement_text_kwarg, set_element=text_subelement, @@ -424,9 +423,9 @@ def __bool__(self) -> bool: registry.register( Data, RegistryItem( - ns_ids=("kml",), - attr_name="display_name", - node_name="displayName", + ns_ids=("", "kml"), + attr_name="value", + node_name="value", classes=(str,), get_kwarg=subelement_text_kwarg, set_element=text_subelement, diff --git a/fastkml/enums.py b/fastkml/enums.py index 407efed5..d75ff298 100644 --- a/fastkml/enums.py +++ b/fastkml/enums.py @@ -77,7 +77,7 @@ def _missing_(cls, value: object) -> "RelaxedEnum": class Verbosity(Enum): """Enum to represent the different verbosity levels.""" - quiet = 0 + terse = 0 normal = 1 verbose = 2 @@ -99,34 +99,35 @@ class AltitudeMode(RelaxedEnum): 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. + + - 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, @@ -139,6 +140,15 @@ class AltitudeMode(RelaxedEnum): clamp_to_sea_floor = "clampToSeaFloor" relative_to_sea_floor = "relativeToSeaFloor" + def get_ns_id(self) -> str: + """Get the namespace for the altitude mode.""" + if self in ( + AltitudeMode.clamp_to_sea_floor, + AltitudeMode.relative_to_sea_floor, + ): + return "gx" + return "kml" + @unique class DataType(RelaxedEnum): @@ -216,9 +226,9 @@ class Shape(RelaxedEnum): The PhotoOverlay is projected onto the . The can be one of the following: - - rectangle (default) - for an ordinary photo - - cylinder - for panoramas, which can be either partial or full cylinders - - sphere - for spherical panoramas + - rectangle (default) - for an ordinary photo + - cylinder - for panoramas, which can be either partial or full cylinders + - sphere - for spherical panoramas """ rectangle = "rectangle" diff --git a/fastkml/features.py b/fastkml/features.py index 4f809f0f..08e6ed24 100644 --- a/fastkml/features.py +++ b/fastkml/features.py @@ -185,6 +185,7 @@ def __bool__(self) -> bool: classes=(int,), get_kwarg=attribute_int_kwarg, set_element=int_attribute, + default=2, ), ) @@ -353,6 +354,7 @@ def __repr__(self) -> str: classes=(bool,), get_kwarg=subelement_bool_kwarg, set_element=bool_subelement, + default=True, ), ) registry.register( @@ -364,15 +366,16 @@ def __repr__(self) -> str: classes=(bool,), get_kwarg=subelement_bool_kwarg, set_element=bool_subelement, + default=False, ), ) registry.register( _Feature, RegistryItem( ns_ids=("atom",), - attr_name="atom_link", - node_name="atom:link", - classes=(atom.Link,), + attr_name="atom_author", + node_name="atom:author", + classes=(atom.Author,), get_kwarg=xml_subelement_kwarg, set_element=xml_subelement, ), @@ -381,9 +384,9 @@ def __repr__(self) -> str: _Feature, RegistryItem( ns_ids=("atom",), - attr_name="atom_author", - node_name="atom:author", - classes=(atom.Author,), + attr_name="atom_link", + node_name="atom:link", + classes=(atom.Link,), get_kwarg=xml_subelement_kwarg, set_element=xml_subelement, ), @@ -628,7 +631,7 @@ def __init__( msg = "You can only specify one of kml_geometry or geometry" raise ValueError(msg) if geometry: - kml_geometry = create_kml_geometry( # type: ignore[assignment] + kml_geometry = create_kml_geometry( geometry=geometry, ns=ns, name_spaces=name_spaces, @@ -745,7 +748,8 @@ class NetworkLink(_Feature): For example, Google Earth would fly to the view of the parent Document, not the of the Placemarks contained within the Document. (required) - https://developers.google.com/kml/documentation/kmlreference#link + + https://developers.google.com/kml/documentation/kmlreference#networklink """ refresh_visibility: Optional[bool] @@ -921,6 +925,7 @@ def __bool__(self) -> bool: classes=(bool,), get_kwarg=subelement_bool_kwarg, set_element=bool_subelement, + default=False, ), ) registry.register( @@ -932,6 +937,7 @@ def __bool__(self) -> bool: classes=(bool,), get_kwarg=subelement_bool_kwarg, set_element=bool_subelement, + default=False, ), ) registry.register( diff --git a/fastkml/geometry.py b/fastkml/geometry.py index 6b314140..50dd48e5 100644 --- a/fastkml/geometry.py +++ b/fastkml/geometry.py @@ -32,9 +32,11 @@ from typing import Final from typing import Iterable from typing import List +from typing import NoReturn from typing import Optional from typing import Sequence from typing import Tuple +from typing import Type from typing import Union from typing import cast @@ -63,7 +65,6 @@ from fastkml.helpers import xml_subelement_list_kwarg from fastkml.kml_base import _BaseObject from fastkml.registry import RegistryItem -from fastkml.registry import known_types from fastkml.registry import registry from fastkml.types import Element @@ -93,6 +94,8 @@ MsgMutualExclusive: Final = "Geometry and kml coordinates are mutually exclusive" +xml_attrs = {"ns", "name_spaces", "id", "target_id"} + def handle_invalid_geometry_error( *, @@ -138,6 +141,7 @@ def coordinates_subelement( node_name: str, # noqa: ARG001 precision: Optional[int], verbosity: Optional[Verbosity], # noqa: ARG001 + default: Any, # noqa: ARG001 ) -> None: """ Set the value of an attribute from a subelement with a text node. @@ -150,6 +154,7 @@ def coordinates_subelement( node_name (str): The name of the subelement to create. precision (Optional[int]): The precision of the attribute value. verbosity (Optional[Verbosity]): The verbosity level. + default (Any): The default value of the attribute (unused). Returns: ------- @@ -157,15 +162,14 @@ def coordinates_subelement( """ if getattr(obj, attr_name, None): - p = precision if precision is not None else 6 coords = getattr(obj, attr_name) - if len(coords[0]) == 2: # noqa: PLR2004 - tuples = (f"{c[0]:.{p}f},{c[1]:.{p}f}" for c in coords) - elif len(coords[0]) == 3: # noqa: PLR2004 - tuples = (f"{c[0]:.{p}f},{c[1]:.{p}f},{c[2]:.{p}f}" for c in coords) - else: + if not coords or len(coords[0]) not in (2, 3): msg = f"Invalid dimensions in coordinates '{coords}'" raise KMLWriteError(msg) + if precision is None: + tuples = (",".join(str(c) for c in coord) for coord in coords) + else: + tuples = (",".join(f"{c:.{precision}f}" for c in coord) for coord in coords) element.text = " ".join(tuples) @@ -176,7 +180,7 @@ def subelement_coordinates_kwarg( name_spaces: Dict[str, str], # noqa: ARG001 node_name: str, # noqa: ARG001 kwarg: str, - classes: Tuple[known_types, ...], # noqa: ARG001 + classes: Tuple[Type[object], ...], # noqa: ARG001 strict: bool, ) -> Dict[str, LineType]: """ @@ -189,7 +193,7 @@ def subelement_coordinates_kwarg( name_spaces (Dict[str, str]): A dictionary mapping namespace prefixes to URIs. node_name (str): The name of the XML node containing the coordinates. kwarg (str): The name of the keyword argument to store the coordinates. - classes (Tuple[known_types, ...]): A tuple of known types for validation. + classes (Tuple[Type[object], ...]): A tuple of known types for validation. strict (bool): A flag indicating whether to raise an error for invalid geometry. Returns: @@ -318,8 +322,6 @@ class _Geometry(_BaseObject): """ - extrude: Optional[bool] - tessellate: Optional[bool] altitude_mode: Optional[AltitudeMode] def __init__( @@ -329,8 +331,6 @@ def __init__( name_spaces: Optional[Dict[str, str]] = None, id: Optional[str] = None, target_id: Optional[str] = None, - extrude: Optional[bool] = None, - tessellate: Optional[bool] = None, altitude_mode: Optional[AltitudeMode] = None, **kwargs: Any, ) -> None: @@ -357,8 +357,6 @@ def __init__( target_id=target_id, **kwargs, ) - self.extrude = extrude - self.tessellate = tessellate self.altitude_mode = altitude_mode def __repr__(self) -> str: @@ -369,49 +367,12 @@ def __repr__(self) -> str: f"name_spaces={self.name_spaces!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"**{self._get_splat()!r}," ")" ) -registry.register( - _Geometry, - item=RegistryItem( - ns_ids=("kml",), - classes=(bool,), - attr_name="extrude", - node_name="extrude", - get_kwarg=subelement_bool_kwarg, - set_element=bool_subelement, - ), -) -registry.register( - _Geometry, - item=RegistryItem( - ns_ids=("kml",), - classes=(bool,), - attr_name="tessellate", - node_name="tessellate", - get_kwarg=subelement_bool_kwarg, - set_element=bool_subelement, - ), -) -registry.register( - _Geometry, - item=RegistryItem( - ns_ids=("kml", "gx"), - classes=(AltitudeMode,), - attr_name="altitude_mode", - node_name="altitudeMode", - get_kwarg=subelement_enum_kwarg, - set_element=enum_subelement, - ), -) - - class Point(_Geometry): """ A geographic location defined by longitude, latitude, and (optional) altitude. @@ -424,6 +385,7 @@ class Point(_Geometry): https://developers.google.com/kml/documentation/kmlreference#point """ + extrude: Optional[bool] kml_coordinates: Optional[Coordinates] def __init__( @@ -434,7 +396,6 @@ def __init__( id: Optional[str] = None, target_id: Optional[str] = None, extrude: Optional[bool] = None, - tessellate: Optional[bool] = None, altitude_mode: Optional[AltitudeMode] = None, geometry: Optional[geo.Point] = None, kml_coordinates: Optional[Coordinates] = None, @@ -471,13 +432,13 @@ def __init__( else None ) self.kml_coordinates = kml_coordinates + self.extrude = extrude + kwargs.pop("tessellate", None) super().__init__( ns=ns, id=id, name_spaces=name_spaces, target_id=target_id, - extrude=extrude, - tessellate=tessellate, altitude_mode=altitude_mode, **kwargs, ) @@ -498,7 +459,6 @@ def __repr__(self) -> str: 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"kml_coordinates={self.kml_coordinates!r}, " f"**{self._get_splat()!r}," @@ -516,6 +476,21 @@ def __bool__(self) -> bool: """ return bool(self.geometry) + def __eq__(self, other: object) -> bool: + """Check if the Point objects are equal.""" + if isinstance(other, Point): + return all( + getattr(self, attr) == getattr(other, attr) + for attr in ( + "extrude", + "altitude_mode", + "geometry", + *xml_attrs, + *self._get_splat(), + ) + ) + return super().__eq__(other) + @property def geometry(self) -> Optional[geo.Point]: """ @@ -535,6 +510,30 @@ def geometry(self) -> Optional[geo.Point]: return None +registry.register( + Point, + item=RegistryItem( + ns_ids=("kml",), + classes=(bool,), + attr_name="extrude", + node_name="extrude", + get_kwarg=subelement_bool_kwarg, + set_element=bool_subelement, + default=False, + ), +) +registry.register( + Point, + item=RegistryItem( + ns_ids=("kml", "gx"), + classes=(AltitudeMode,), + attr_name="altitude_mode", + node_name="altitudeMode", + get_kwarg=subelement_enum_kwarg, + set_element=enum_subelement, + default=AltitudeMode.clamp_to_ground, + ), +) registry.register( Point, item=RegistryItem( @@ -562,6 +561,10 @@ class LineString(_Geometry): https://developers.google.com/kml/documentation/kmlreference#linestring """ + extrude: Optional[bool] + tessellate: Optional[bool] + kml_coordinates: Optional[Coordinates] + def __init__( self, *, @@ -602,13 +605,13 @@ def __init__( if kml_coordinates is None: kml_coordinates = Coordinates(coords=geometry.coords) if geometry else None self.kml_coordinates = kml_coordinates + self.extrude = extrude + self.tessellate = tessellate super().__init__( ns=ns, name_spaces=name_spaces, id=id, target_id=target_id, - extrude=extrude, - tessellate=tessellate, altitude_mode=altitude_mode, **kwargs, ) @@ -640,6 +643,21 @@ def __bool__(self) -> bool: """ return bool(self.geometry) + def __eq__(self, other: object) -> bool: + """Check if the LineString objects is equal.""" + if isinstance(other, LineString): + return all( + getattr(self, attr) == getattr(other, attr) + for attr in ( + "extrude", + "tessellate", + "geometry", + *xml_attrs, + *self._get_splat(), + ) + ) + return super().__eq__(other) + @property def geometry(self) -> Optional[geo.LineString]: """ @@ -659,6 +677,42 @@ def geometry(self) -> Optional[geo.LineString]: return None +registry.register( + LineString, + item=RegistryItem( + ns_ids=("kml",), + classes=(bool,), + attr_name="extrude", + node_name="extrude", + get_kwarg=subelement_bool_kwarg, + set_element=bool_subelement, + default=False, + ), +) +registry.register( + LineString, + item=RegistryItem( + ns_ids=("kml",), + classes=(bool,), + attr_name="tessellate", + node_name="tessellate", + get_kwarg=subelement_bool_kwarg, + set_element=bool_subelement, + default=False, + ), +) +registry.register( + LineString, + item=RegistryItem( + ns_ids=("kml", "gx"), + classes=(AltitudeMode,), + attr_name="altitude_mode", + node_name="altitudeMode", + get_kwarg=subelement_enum_kwarg, + set_element=enum_subelement, + default=AltitudeMode.clamp_to_ground, + ), +) registry.register( LineString, item=RegistryItem( @@ -741,22 +795,6 @@ def __init__( **kwargs, ) - def __repr__(self) -> str: - """Create a string (c)representation for LinearRing.""" - return ( - f"{self.__class__.__module__}.{self.__class__.__name__}(" - f"ns={self.ns!r}, " - f"name_spaces={self.name_spaces!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"**{self._get_splat()!r}," - ")" - ) - @property def geometry(self) -> Optional[geo.LinearRing]: """ @@ -779,9 +817,9 @@ def geometry(self) -> Optional[geo.LinearRing]: return None -class OuterBoundaryIs(_XMLObject): +class BoundaryIs(_XMLObject): """ - Represents the outer boundary of a polygon in KML. + Represents the inner or outer boundary of a polygon in KML. Attributes ---------- @@ -867,19 +905,6 @@ def __repr__(self) -> str: ")" ) - @classmethod - def get_tag_name(cls) -> str: - """ - Get the tag name for the OuterBoundaryIs object. - - Returns - ------- - str - The tag name. - - """ - return "outerBoundaryIs" - @property def geometry(self) -> Optional[geo.LinearRing]: """ @@ -894,106 +919,34 @@ def geometry(self) -> Optional[geo.LinearRing]: return self.kml_geometry.geometry if self.kml_geometry else None -registry.register( - OuterBoundaryIs, - item=RegistryItem( - ns_ids=("kml", ""), - classes=(LinearRing,), - attr_name="kml_geometry", - node_name="LinearRing", - get_kwarg=xml_subelement_kwarg, - set_element=xml_subelement, - ), -) - +class OuterBoundaryIs(BoundaryIs): + """Represents the outer boundary of a polygon in KML.""" -class InnerBoundaryIs(_XMLObject): - """Represents the inner boundary of a polygon in KML.""" - - _default_nsid = config.KML - kml_geometry: Optional[LinearRing] - - def __init__( - self, - *, - ns: Optional[str] = None, - name_spaces: Optional[Dict[str, str]] = None, - geometry: Optional[geo.LinearRing] = None, - kml_geometry: Optional[LinearRing] = None, - **kwargs: Any, - ) -> None: + @classmethod + def get_tag_name(cls) -> str: """ - Initialize a Geometry object. - - Parameters - ---------- - ns : Optional[str], optional - The namespace for the KML element, by default None. - name_spaces : Optional[Dict[str, str]], optional - The namespace dictionary for the KML element, by default None. - geometry : Optional[geo.LinearRing], optional - The geometry to be converted to a KML geometry, by default None. - kml_geometry : Optional[LinearRing], optional - The KML geometry, by default None. - **kwargs : Any - Additional keyword arguments. - - Raises - ------ - GeometryError - If both `geometry` and `kml_geometry` are provided. + Get the tag name for the OuterBoundaryIs object. - Notes - ----- - - If `geometry` is provided, it will be converted to KML geometries and - stored in `kml_geometry`. - - If `geometry` and `kml_geometry` are both provided, a GeometryError will be - raised. + Returns + ------- + str + The tag name. """ - if geometry is not None and kml_geometry is not None: - raise GeometryError(MsgMutualExclusive) - if kml_geometry is None: - kml_geometry = LinearRing(ns=ns, name_spaces=name_spaces, geometry=geometry) - self.kml_geometry = kml_geometry - super().__init__( - ns=ns, - name_spaces=name_spaces, - **kwargs, - ) + return "outerBoundaryIs" - def __bool__(self) -> bool: - """Return True if any of the inner boundary geometries exist.""" - return bool(self.kml_geometry) - def __repr__(self) -> str: - """Create a string (c)representation for InnerBoundaryIs.""" - return ( - f"{self.__class__.__module__}.{self.__class__.__name__}(" - f"ns={self.ns!r}, " - f"name_spaces={self.name_spaces!r}, " - f"kml_geometry={self.kml_geometry!r}, " - f"**{self._get_splat()}," - ")" - ) +class InnerBoundaryIs(BoundaryIs): + """Represents the inner boundary of a polygon in KML.""" @classmethod def get_tag_name(cls) -> str: """Return the tag name of the element.""" return "innerBoundaryIs" - @property - def geometry(self) -> Optional[geo.LinearRing]: - """ - Return the list of LinearRing objects representing the inner boundary. - - If no inner boundary geometries exist, returns None. - """ - return self.kml_geometry.geometry if self.kml_geometry else None - registry.register( - InnerBoundaryIs, + BoundaryIs, item=RegistryItem( ns_ids=("kml",), classes=(LinearRing,), @@ -1024,16 +977,18 @@ class Polygon(_Geometry): The `geometry` property returns a `geo.Polygon` object representing the geometry of the Polygon. - Example usage: - ``` - polygon = Polygon(outer_boundary_is=outer_boundary, - inner_boundary_is=inner_boundary) - print(polygon.geometry) - ``` + Example usage:: + + polygon = Polygon(outer_boundary_is=outer_boundary, + inner_boundary_is=inner_boundary) + print(polygon.geometry) + https://developers.google.com/kml/documentation/kmlreference#polygon """ + extrude: Optional[bool] + tessellate: Optional[bool] outer_boundary: Optional[OuterBoundaryIs] inner_boundaries: List[InnerBoundaryIs] @@ -1082,7 +1037,7 @@ def __init__( Raises ------ - GeometryError + GeometryError: If both outer_boundary_is and geometry are provided. Returns @@ -1099,13 +1054,13 @@ def __init__( ] self.outer_boundary = outer_boundary self.inner_boundaries = list(inner_boundaries) if inner_boundaries else [] + self.extrude = extrude + self.tessellate = tessellate super().__init__( ns=ns, name_spaces=name_spaces, id=id, target_id=target_id, - extrude=extrude, - tessellate=tessellate, altitude_mode=altitude_mode, **kwargs, ) @@ -1167,13 +1122,63 @@ def __repr__(self) -> str: f"extrude={self.extrude!r}, " f"tessellate={self.tessellate!r}, " f"altitude_mode={self.altitude_mode}, " - f"outer_boundary={self.outer_boundary!r}, " - f"inner_boundaries={self.inner_boundaries!r}, " + f"geometry={self.geometry!r}, " f"**{self._get_splat()!r}," ")" ) + def __eq__(self, other: object) -> bool: + """Check if the Polygon objects are equal.""" + if isinstance(other, Polygon): + return all( + getattr(self, attr) == getattr(other, attr) + for attr in ( + "extrude", + "tessellate", + "geometry", + *xml_attrs, + *self._get_splat(), + ) + ) + return super().__eq__(other) + +registry.register( + Polygon, + item=RegistryItem( + ns_ids=("kml",), + classes=(bool,), + attr_name="extrude", + node_name="extrude", + get_kwarg=subelement_bool_kwarg, + set_element=bool_subelement, + default=False, + ), +) +registry.register( + Polygon, + item=RegistryItem( + ns_ids=("kml",), + classes=(bool,), + attr_name="tessellate", + node_name="tessellate", + get_kwarg=subelement_bool_kwarg, + set_element=bool_subelement, + default=False, + ), +) +registry.register( + Polygon, + item=RegistryItem( + ns_ids=("kml", "gx"), + classes=(AltitudeMode,), + attr_name="altitude_mode", + node_name="altitudeMode", + get_kwarg=subelement_enum_kwarg, + set_element=enum_subelement, + default=AltitudeMode.clamp_to_ground, + ), +) registry.register( Polygon, item=RegistryItem( @@ -1232,69 +1237,7 @@ def create_multigeometry( return geo.GeometryCollection(geometries) -def create_kml_geometry( - geometry: Union[GeoType, GeoCollectionType], - *, - ns: Optional[str] = None, - name_spaces: Optional[Dict[str, str]] = None, - id: Optional[str] = None, - target_id: Optional[str] = None, - extrude: Optional[bool] = None, - tessellate: Optional[bool] = None, - altitude_mode: Optional[AltitudeMode] = None, -) -> _Geometry: - """ - Create a KML geometry from a geometry object. - - Args: - ---- - geometry: Geometry object. - ns: Namespace of the object - name_spaces: Name spaces 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. - - Returns: - ------- - KML geometry object. - - """ - _map_to_kml = { - geo.Point: Point, - geo.Polygon: Polygon, - geo.LinearRing: LinearRing, - geo.LineString: LineString, - geo.MultiPoint: MultiGeometry, - geo.MultiLineString: MultiGeometry, - geo.MultiPolygon: MultiGeometry, - geo.GeometryCollection: MultiGeometry, - } - geom = shape(geometry) - for geometry_class, kml_class in _map_to_kml.items(): - if isinstance(geom, geometry_class): - return cast( - _Geometry, - kml_class( - ns=ns, - name_spaces=name_spaces, - id=id, - target_id=target_id, - extrude=extrude, - tessellate=tessellate, - altitude_mode=altitude_mode, - geometry=geom, - ), - ) - # this should be unreachable, but mypy doesn't know that - msg = f"Unsupported geometry type {type(geometry)}" # pragma: no cover - raise KMLWriteError(msg) # pragma: no cover - - -class MultiGeometry(_Geometry): +class MultiGeometry(_BaseObject): """A container for zero or more geometry primitives.""" kml_geometries: List[Union[Point, LineString, Polygon, LinearRing, Self]] @@ -1328,17 +1271,26 @@ def __init__( The ID of the KML element. target_id : str, optional The target ID of the KML element. - extrude : bool, optional - Specifies whether to extend the geometry to the ground. - tessellate : bool, optional - Specifies whether to allow the geometry to follow the terrain. - altitude_mode : AltitudeMode, optional - The altitude mode of the geometry. kml_geometries : iterable of Point, LineString, Polygon, LinearRing, MultiGeometry A collection of KML geometries. geometry : MultiGeometryType, optional A multi-geometry object. + Parameters for geometry and kml_geometries are mutually exclusive. + When geometry is provided, kml_geometries will be created from it and + you can specify additional parameters like extrude, tessellate, and + altitude_mode which will be set on the individual geometries. + extrude : bool, optional + Specifies whether to extend the geometry to the ground. + This is not set on the multi-geometry itself, but on the individual + geometries. + tessellate : bool, optional + Specifies whether to allow the geometry to follow the terrain. + This is not set on the multi-geometry itself, but on the individual + geometries. + altitude_mode : AltitudeMode, optional + The altitude mode of the geometry. This is not set on the multi-geometry + itself, but on the individual geometries. **kwargs : any Additional keyword arguments. @@ -1373,9 +1325,6 @@ def __init__( name_spaces=name_spaces, id=id, target_id=target_id, - extrude=extrude, - tessellate=tessellate, - altitude_mode=altitude_mode, **kwargs, ) @@ -1391,9 +1340,6 @@ def __repr__(self) -> str: f"name_spaces={self.name_spaces!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"kml_geometries={self.kml_geometries!r}, " f"**{self._get_splat()!r}," ")" @@ -1418,3 +1364,84 @@ def geometry(self) -> Optional[MultiGeometryType]: set_element=xml_subelement_list, ), ) + + +KMLGeometryType = Union[Point, LineString, Polygon, LinearRing, MultiGeometry] + + +def _unknown_geometry_type(geometry: Union[GeoType, GeoCollectionType]) -> NoReturn: + """ + Raise an error for an unknown geometry type. + + Args: + ---- + geometry: The geometry object. + + Raises: + ------ + KMLWriteError: If the geometry type is unknown. + + """ + msg = f"Unsupported geometry type {type(geometry)}" # pragma: no cover + raise KMLWriteError(msg) # pragma: no cover + + +def create_kml_geometry( + geometry: Union[GeoType, GeoCollectionType], + *, + ns: Optional[str] = None, + name_spaces: Optional[Dict[str, str]] = None, + id: Optional[str] = None, + target_id: Optional[str] = None, + extrude: Optional[bool] = None, + tessellate: Optional[bool] = None, + altitude_mode: Optional[AltitudeMode] = None, +) -> KMLGeometryType: + """ + Create a KML geometry from a geometry object. + + Args: + ---- + geometry: Geometry object. + ns: Namespace of the object + name_spaces: Name spaces 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. + + Returns: + ------- + KML geometry object. + + """ + _map_to_kml: Dict[ + Union[Type[GeoType], Type[GeoCollectionType]], + Type[KMLGeometryType], + ] = { + geo.Point: Point, + geo.Polygon: Polygon, + geo.LinearRing: LinearRing, + geo.LineString: LineString, + geo.MultiPoint: MultiGeometry, + geo.MultiLineString: MultiGeometry, + geo.MultiPolygon: MultiGeometry, + geo.GeometryCollection: MultiGeometry, + } + geom = shape(geometry) + for geometry_class, kml_class in _map_to_kml.items(): + if isinstance(geom, geometry_class): + return kml_class( + ns=ns, + name_spaces=name_spaces, + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geom, # type: ignore[arg-type] + ) + + _unknown_geometry_type(geometry) # pragma: no cover diff --git a/fastkml/gx.py b/fastkml/gx.py index 071e1321..736814a7 100644 --- a/fastkml/gx.py +++ b/fastkml/gx.py @@ -77,39 +77,42 @@ The complete XML schema for elements in this extension namespace is located at http://developers.google.com/kml/schema/kml22gx.xsd. """ -import datetime import logging from dataclasses import dataclass from itertools import zip_longest from typing import Any from typing import Dict from typing import Iterable -from typing import Iterator from typing import List from typing import Optional +from typing import Tuple +from typing import cast -import arrow import pygeoif.geometry as geo +from pygeoif.types import PointType from fastkml import config from fastkml.enums import AltitudeMode -from fastkml.enums import Verbosity from fastkml.geometry import _Geometry from fastkml.helpers import bool_subelement +from fastkml.helpers import coords_subelement_list +from fastkml.helpers import coords_subelement_list_kwarg +from fastkml.helpers import datetime_subelement_list +from fastkml.helpers import datetime_subelement_list_kwarg +from fastkml.helpers import enum_subelement from fastkml.helpers import subelement_bool_kwarg +from fastkml.helpers import subelement_enum_kwarg from fastkml.helpers import xml_subelement_list from fastkml.helpers import xml_subelement_list_kwarg from fastkml.registry import RegistryItem from fastkml.registry import registry -from fastkml.types import Element +from fastkml.times import KmlDateTime __all__ = [ "Angle", "MultiTrack", "Track", "TrackItem", - "linestring_to_track_items", - "multilinestring_to_tracks", "track_items_to_geometry", "tracks_to_geometry", ] @@ -131,62 +134,27 @@ class Angle: tilt: float = 0.0 roll: float = 0.0 + @property + def coords(self) -> PointType: + """ + Get the coordinates of the angle. -@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 + Returns + ------- + PointType + The coordinates of the angle. - def etree_elements( - self, - *, - precision: Optional[int] = None, # noqa: ARG002 - verbosity: Verbosity = Verbosity.normal, # noqa: ARG002 - name_spaces: Optional[Dict[str, str]] = None, - ) -> Iterator[Element]: """ - Generate XML elements for the gx:when, gx:coord, and gx:angles elements. + return (self.heading, self.tilt, self.roll) - Args: - ---- - precision (Optional[int]): The precision of the coordinates. - verbosity (Verbosity): The verbosity level. Defaults to Verbosity.normal. - name_spaces (Optional[Dict[str, str]]): Additional XML namespaces. - Yields: - ------ - Element: The generated XML elements. +@dataclass(frozen=True) +class TrackItem: + """A track item describes an objects position and heading at a specific time.""" - """ - name_spaces = name_spaces or {} - name_spaces = {**config.NAME_SPACES, **name_spaces} - element: Element = config.etree.Element( - f"{name_spaces.get('kml', '')}when", - ) - if self.when: - element.text = self.when.isoformat() - yield element - element = config.etree.Element( - f"{name_spaces.get('gx', '')}coord", - ) - if self.coord: - element.text = " ".join( - [ - str(c) for c in self.coord.coords[0] # type:ignore[misc] - ], - ) - yield element - element = config.etree.Element( - 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 + when: KmlDateTime + coord: geo.Point + angle: Optional[Angle] = None def track_items_to_geometry(track_items: Iterable[TrackItem]) -> geo.LineString: @@ -209,22 +177,6 @@ def track_items_to_geometry(track_items: Iterable[TrackItem]) -> geo.LineString: ) -def linestring_to_track_items(linestring: geo.LineString) -> List[TrackItem]: - """ - Convert a LineString to a list of TrackItems. - - Args: - ---- - linestring (LineString): The LineString to convert. - - Returns: - ------- - List[TrackItem]: A list of TrackItems representing the points in the LineString. - - """ - 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. @@ -237,10 +189,11 @@ class Track(_Geometry): 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. + + https://developers.google.com/kml/documentation/kmlreference#gxtrack """ _default_nsid = config.GX - track_items: List[TrackItem] def __init__( @@ -250,11 +203,11 @@ def __init__( name_spaces: Optional[Dict[str, str]] = None, id: Optional[str] = None, target_id: Optional[str] = None, - extrude: Optional[bool] = None, - tessellate: Optional[bool] = None, altitude_mode: Optional[AltitudeMode] = None, - geometry: Optional[geo.LineString] = None, track_items: Optional[Iterable[TrackItem]] = None, + whens: Optional[Iterable[KmlDateTime]] = None, + coords: Optional[Iterable[PointType]] = None, + angles: Optional[Iterable[PointType]] = None, **kwargs: Any, ) -> None: """ @@ -270,16 +223,16 @@ def __init__( The ID of the GX object, by default None target_id : Optional[str], optional The target ID of the GX object, by default None - extrude : Optional[bool], optional - Whether to extrude the GX object, by default None - tessellate : Optional[bool], optional - Whether to tessellate the GX object, by default None altitude_mode : Optional[AltitudeMode], optional The altitude mode of the GX object, by default None - geometry : Optional[geo.LineString], optional - The geometry of the GX object, by default None track_items : Optional[Iterable[TrackItem]], optional The track items of the GX object, by default None + whens : Optional[Iterable[KmlDateTime]], optional + The timestamps of the track items, by default None + coords : Optional[Iterable[PointType]], optional + The coordinates of the track items, by default None + angles : Optional[Iterable[PointType]], optional + The angles of the track items, by default None **kwargs : Any, optional Additional keyword arguments. @@ -289,19 +242,30 @@ def __init__( If both `geometry` and `track_items` are specified. """ - if geometry and track_items: + angles = list(angles) if angles else [] + if (whens or coords) and track_items: msg = "Cannot specify both geometry and track_items" raise ValueError(msg) - if geometry: - track_items = linestring_to_track_items(geometry) + if not track_items and whens and coords: + track_items = [ + TrackItem( + when=cast(KmlDateTime, when), + coord=geo.Point(*coord), + angle=Angle(*angle), + ) + for when, coord, angle in zip_longest( + whens, + coords, + angles, + fillvalue=(), + ) + ] self.track_items = list(track_items) if track_items else [] super().__init__( ns=ns, name_spaces=name_spaces, id=id, target_id=target_id, - extrude=extrude, - tessellate=tessellate, altitude_mode=altitude_mode, **kwargs, ) @@ -322,10 +286,7 @@ def __repr__(self) -> str: f"name_spaces={self.name_spaces!r}, " f"id={self.id!r}, " f"target_id={self.target_id!r}, " - f"extrude={self.extrude!r}, " - f"tessellate={self.tessellate!r}, " f"altitude_mode={self.altitude_mode}, " - f"geometry={self.geometry!r}, " f"track_items={self.track_items!r}, " f"**{self._get_splat()!r}," ")" @@ -344,225 +305,108 @@ def geometry(self) -> Optional[geo.LineString]: """ return track_items_to_geometry(self.track_items) - def __bool__(self) -> bool: - """ - Check if the track has any track items. - - Returns - ------- - bool - True if the track has track items, False otherwise. - - """ - return bool(self.track_items) - - def etree_element( - self, - precision: Optional[int] = None, - verbosity: Verbosity = Verbosity.normal, - name_spaces: Optional[Dict[str, str]] = None, - ) -> Element: + @property + def whens(self) -> Tuple[KmlDateTime, ...]: """ - Get the ElementTree element representation of the track. - - Parameters - ---------- - precision : Optional[int], optional - The precision for floating-point values, by default None - verbosity : Verbosity, optional - The verbosity level for the element, by default Verbosity.normal - name_spaces : Optional[Dict[str, str]], optional - A dictionary of namespace prefixes and URIs, by default None + Get the timestamps of the track items. Returns ------- - Element - The ElementTree element representation of the track. + Tuple[KmlDateTime] + The timestamps of the track items. """ - 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 _get_timestamps(cls, element: Element) -> List[Optional[datetime.datetime]]: - """ - Get the timestamps from the XML element. + return tuple(item.when for item in self.track_items) - Parameters - ---------- - element : Element - The XML element. - - Returns - ------- - List[Optional[datetime.datetime]] - The list of timestamps. - - """ - 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(arrow.get(time_stamp.text).datetime) - else: - time_stamps.append(None) - return time_stamps - - @classmethod - def _get_coords(cls, element: Element) -> List[Optional[geo.Point]]: + @property + def coords(self) -> Tuple[PointType, ...]: """ - Get the coordinates from the XML element. - - Parameters - ---------- - element : Element - The XML element. + Get the coordinates of the track items. Returns ------- - List[Optional[geo.Point]] - The list of coordinates. + Tuple[PointType] + The coordinates of the track items. """ - 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) - return coords + return tuple( + item.coord.coords[0] # type: ignore[misc] + for item in self.track_items + if item.coord + ) - @classmethod - def _get_angles(cls, element: Element) -> List[Optional[Angle]]: + @property + def angles(self) -> Tuple[PointType, ...]: """ - Get the angles from the XML element. - - Parameters - ---------- - element : Element - The XML element. + Get the angles of the track items. Returns ------- - List[Optional[Angle]] - The list of angles. + Tuple[Angle] + The angles of the track items. """ - 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 angles - - @classmethod - def track_items_kwargs_from_element( - cls, - *, - ns: str, # noqa: ARG003 - element: Element, - strict: bool, # noqa: ARG003 - ) -> List[TrackItem]: - """ - Get the track item keyword arguments from the XML element. - - Parameters - ---------- - ns : str - The namespace for the GX object. - element : Element - The XML element. - strict : bool - Whether to enforce strict parsing. - - Returns - ------- - List[TrackItem] - The list of track items. + return tuple(item.angle.coords for item in self.track_items if item.angle) + def __bool__(self) -> bool: """ - time_stamps = cls._get_timestamps(element) - coords = cls._get_coords(element) - angles = cls._get_angles(element) - return [ - TrackItem(when=when, coord=coord, angle=angle) - for when, coord, angle in zip_longest(time_stamps, coords, angles) - ] - - @classmethod - def _get_kwargs( - cls, - *, - ns: str, - name_spaces: Optional[Dict[str, str]] = None, - element: Element, - strict: bool, - ) -> Dict[str, Any]: - """ - Get the keyword arguments for the track. - - Parameters - ---------- - ns : str - The namespace for the GX object. - name_spaces : Optional[Dict[str, str]], optional - A dictionary of namespace prefixes and URIs, by default None - element : Element - The XML element. - strict : bool - Whether to enforce strict parsing. + Check if the track has any track items. Returns ------- - Dict[str, Any] - The keyword arguments for the track. + bool + True if the track has track items, False otherwise. """ - kwargs = super()._get_kwargs( - ns=ns, - name_spaces=name_spaces, - 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]: - """ - Convert a MultiLineString to a list of Track objects. - - Args: - ---- - multilinestring : geo.MultiLineString: - The MultiLineString to convert. - ns : str, optional: - The namespace for the Track objects. + return bool(self.track_items) - Returns: - ------- - List[Track]: - A list of Track objects. - """ - return [Track(ns=ns, geometry=linestring) for linestring in multilinestring.geoms] +registry.register( + Track, + item=RegistryItem( + ns_ids=("gx", "kml", ""), + classes=(AltitudeMode,), + attr_name="altitude_mode", + node_name="altitudeMode", + get_kwarg=subelement_enum_kwarg, + set_element=enum_subelement, + default=AltitudeMode.clamp_to_ground, + ), +) +registry.register( + Track, + item=RegistryItem( + ns_ids=("kml", "gx", ""), + classes=(KmlDateTime,), + attr_name="whens", + node_name="when", + get_kwarg=datetime_subelement_list_kwarg, + set_element=datetime_subelement_list, + ), +) +registry.register( + Track, + item=RegistryItem( + ns_ids=("gx", ""), + classes=(tuple,), + attr_name="coords", + node_name="coord", + get_kwarg=coords_subelement_list_kwarg, + set_element=coords_subelement_list, + ), +) +registry.register( + Track, + item=RegistryItem( + ns_ids=("gx", ""), + classes=(tuple,), + attr_name="angles", + node_name="angles", + get_kwarg=coords_subelement_list_kwarg, + set_element=coords_subelement_list, + default=(0.0, 0.0, 0.0), + ), +) def tracks_to_geometry(tracks: Iterable[Track]) -> geo.MultiLineString: @@ -613,10 +457,7 @@ def __init__( name_spaces: Optional[Dict[str, str]] = None, id: Optional[str] = None, target_id: Optional[str] = None, - extrude: Optional[bool] = None, - tessellate: Optional[bool] = None, altitude_mode: Optional[AltitudeMode] = None, - geometry: Optional[geo.MultiLineString] = None, tracks: Optional[Iterable[Track]] = None, interpolate: Optional[bool] = None, **kwargs: Any, @@ -631,8 +472,6 @@ def __init__( and URIs. id (Optional[str]): The ID of the GX object. target_id (Optional[str]): The target ID of the GX object. - extrude (Optional[bool]): The extrude flag of the GX object. - tessellate (Optional[bool]): The tessellate flag of the GX object. altitude_mode (Optional[AltitudeMode]): The altitude mode of the GX object. geometry (Optional[geo.MultiLineString]): The geometry of the GX object. tracks (Optional[Iterable[Track]]): The tracks of the GX object. @@ -644,20 +483,13 @@ def __init__( ValueError: If both geometry and tracks are specified. """ - if geometry and tracks: - msg = "Cannot specify both geometry and track_items" - raise ValueError(msg) - if geometry: - tracks = multilinestring_to_tracks(geometry, ns=ns) - self.tracks = list(tracks) if tracks else [] + self.tracks = [t for t in tracks if t] if tracks else [] self.interpolate = interpolate super().__init__( ns=ns, name_spaces=name_spaces, id=id, target_id=target_id, - extrude=extrude, - tessellate=tessellate, altitude_mode=altitude_mode, **kwargs, ) @@ -670,10 +502,7 @@ def __repr__(self) -> str: f"name_spaces={self.name_spaces!r}, " f"id={self.id!r}, " f"target_id={self.target_id!r}, " - f"extrude={self.extrude!r}, " - f"tessellate={self.tessellate!r}, " f"altitude_mode={self.altitude_mode}, " - f"geometry={self.geometry!r}, " f"tracks={self.tracks!r}, " f"interpolate={self.interpolate!r}, " f"**{self._get_splat()!r}," @@ -705,6 +534,18 @@ def __bool__(self) -> bool: return bool(self.tracks) +registry.register( + MultiTrack, + item=RegistryItem( + ns_ids=("gx", "kml"), + classes=(AltitudeMode,), + attr_name="altitude_mode", + node_name="altitudeMode", + get_kwarg=subelement_enum_kwarg, + set_element=enum_subelement, + default=AltitudeMode.clamp_to_ground, + ), +) registry.register( MultiTrack, item=RegistryItem( diff --git a/fastkml/helpers.py b/fastkml/helpers.py index 193365ec..51d8791e 100644 --- a/fastkml/helpers.py +++ b/fastkml/helpers.py @@ -17,19 +17,27 @@ import logging from enum import Enum +from typing import TYPE_CHECKING +from typing import Any from typing import Dict from typing import List from typing import Optional from typing import Tuple from typing import Type +from typing import cast + +from pygeoif.types import PointType from fastkml import config -from fastkml.base import _XMLObject from fastkml.enums import Verbosity from fastkml.exceptions import KMLParseError -from fastkml.registry import known_types from fastkml.types import Element +if TYPE_CHECKING: + from fastkml.base import _XMLObject + from fastkml.times import KmlDateTime + + logger = logging.getLogger(__name__) @@ -79,21 +87,59 @@ def handle_error( logger.warning("%s, %s", error, msg) +def get_ns(obj: "_XMLObject", value: object) -> str: + """Get the namespace of an attribute, fall back on the objects namespace.""" + try: + return obj.name_spaces.get(value.get_ns_id(), "") # type: ignore[attr-defined] + except AttributeError: + return obj.ns + + +def get_value( + obj: "_XMLObject", + *, + attr_name: str, + verbosity: Verbosity, + default: Optional[Any], +) -> Optional[Any]: + """ + Get the value of an attribute from an object. + + If the verbosity is set to `Verbosity.terse`, the function returns `None` if the + attribute value is equal to the default value. If the verbosity is set to + `Verbosity.verbose`, the function returns the default value if the attribute value + is `None`. + + Args: + ---- + obj ("_XMLObject"): The object to get the attribute value from. + attr_name (str): The name of the attribute to retrieve. + verbosity (Optional[Verbosity]): The verbosity. + default (Optional[Any]): The default value. + + """ + value = getattr(obj, attr_name, None) + if value is None and default is not None and verbosity == Verbosity.verbose: + return default + return None if value == default and verbosity == Verbosity.terse else value + + def node_text( - obj: _XMLObject, + obj: "_XMLObject", *, element: Element, attr_name: str, node_name: str, precision: Optional[int], - verbosity: Optional[Verbosity], + verbosity: Verbosity, + default: Optional[str], ) -> None: """ Set the text of an XML element based on the attribute value in the given object. Parameters ---------- - obj : _XMLObject + obj : "_XMLObject" The object containing the attribute value. element : Element The XML element to set the text content for. @@ -105,6 +151,8 @@ def node_text( The precision to use when converting numeric values to text (unused). verbosity : Optional[Verbosity] The verbosity level for logging (unused). + default : Optional[str] + The default value for the attribute. Returns ------- @@ -112,256 +160,375 @@ def node_text( This function does not return anything. """ - if getattr(obj, attr_name, None): - element.text = getattr(obj, attr_name) + if value := get_value( + obj, + attr_name=attr_name, + verbosity=verbosity, + default=default, + ): + element.text = value def text_subelement( - obj: _XMLObject, + obj: "_XMLObject", *, element: Element, attr_name: str, node_name: str, precision: Optional[int], - verbosity: Optional[Verbosity], + verbosity: Verbosity, + default: Optional[str], ) -> None: """ Set the value of an attribute from a subelement with a text node. Args: ---- - obj (_XMLObject): The object from which to retrieve the attribute value. + obj ("_XMLObject"): The object from which to retrieve the attribute value. element (Element): The parent element to add the subelement to. attr_name (str): The name of the attribute to retrieve the value from. node_name (str): The name of the subelement to create. precision (Optional[int]): The precision of the attribute value. verbosity (Optional[Verbosity]): The verbosity level. + default (Optional[str]): The default value for the attribute. Returns: ------- None """ - if getattr(obj, attr_name, None): + if value := get_value( + obj, + attr_name=attr_name, + verbosity=verbosity, + default=default, + ): subelement = config.etree.SubElement( element, f"{obj.ns}{node_name}", ) - subelement.text = getattr(obj, attr_name) + subelement.text = value def text_attribute( - obj: _XMLObject, + obj: "_XMLObject", *, element: Element, attr_name: str, node_name: str, precision: Optional[int], - verbosity: Optional[Verbosity], + verbosity: Verbosity, + default: Optional[str], ) -> None: """ Set the value of an attribute from a subelement with a text node. Args: ---- - obj (_XMLObject): The object from which to retrieve the attribute value. + obj ("_XMLObject"): The object from which to retrieve the attribute value. element (Element): The parent element to add the subelement to. attr_name (str): The name of the attribute to retrieve the value from. node_name (str): The name of the attribute to be set. precision (Optional[int]): The precision of the attribute value. verbosity (Optional[Verbosity]): The verbosity level. + default (Optional[str]): The default value for the attribute. Returns: ------- None """ - if getattr(obj, attr_name, None): - element.set(node_name, getattr(obj, attr_name)) + if value := get_value( + obj, + attr_name=attr_name, + verbosity=verbosity, + default=default, + ): + element.set(node_name, value) def bool_subelement( - obj: _XMLObject, + obj: "_XMLObject", *, element: Element, attr_name: str, node_name: str, precision: Optional[int], - verbosity: Optional[Verbosity], + verbosity: Verbosity, + default: Optional[bool], ) -> None: """ Set the value of an attribute from a subelement with a text node. Args: ---- - obj (_XMLObject): The object from which to retrieve the attribute value. + obj ("_XMLObject"): The object from which to retrieve the attribute value. element (Element): The parent element to add the subelement to. attr_name (str): The name of the attribute to retrieve the value from. node_name (str): The name of the subelement to create. precision (Optional[int]): The precision of the attribute value. verbosity (Optional[Verbosity]): The verbosity level. + default (Optional[bool]): The default value for the attribute. Returns: ------- None """ - if getattr(obj, attr_name, None) is not None: + value = get_value(obj, attr_name=attr_name, verbosity=verbosity, default=default) + if value is not None: subelement = config.etree.SubElement( element, f"{obj.ns}{node_name}", ) - subelement.text = str(int(getattr(obj, attr_name))) + subelement.text = str(int(value)) def int_subelement( - obj: _XMLObject, + obj: "_XMLObject", *, element: Element, attr_name: str, node_name: str, precision: Optional[int], - verbosity: Optional[Verbosity], + verbosity: Verbosity, + default: Optional[int], ) -> None: """ Set the value of an attribute from a subelement with a text node. Args: ---- - obj (_XMLObject): The object from which to retrieve the attribute value. + obj ("_XMLObject"): The object from which to retrieve the attribute value. element (Element): The parent element to add the subelement to. attr_name (str): The name of the attribute to retrieve the value from. node_name (str): The name of the subelement to create. precision (Optional[int]): The precision of the attribute value. verbosity (Optional[Verbosity]): The verbosity level. + default (Optional[int]): The default value for the attribute. Returns: ------- None: This function does not return anything. """ - if getattr(obj, attr_name, None) is not None: + value = get_value(obj, attr_name=attr_name, verbosity=verbosity, default=default) + if value is not None: subelement = config.etree.SubElement( element, f"{obj.ns}{node_name}", ) - subelement.text = str(getattr(obj, attr_name)) + subelement.text = str(value) def int_attribute( - obj: _XMLObject, + obj: "_XMLObject", *, element: Element, attr_name: str, node_name: str, precision: Optional[int], - verbosity: Optional[Verbosity], + verbosity: Verbosity, + default: Optional[int], ) -> None: """ Set the value of an attribute. Args: ---- - obj (_XMLObject): The object from which to retrieve the attribute value. + obj ("_XMLObject"): The object from which to retrieve the attribute value. element (Element): The parent element to add the subelement to. attr_name (str): The name of the attribute to retrieve the value from. node_name (str): The name of the attribute to be set. precision (Optional[int]): The precision of the attribute value. verbosity (Optional[Verbosity]): The verbosity level. + default (Optional[int]): The default value for the attribute. Returns: ------- None: This function does not return anything. """ - if getattr(obj, attr_name, None) is not None: - element.set(node_name, str(getattr(obj, attr_name))) + value = get_value(obj, attr_name=attr_name, verbosity=verbosity, default=default) + if value is not None: + element.set(node_name, str(value)) def float_subelement( - obj: _XMLObject, + obj: "_XMLObject", *, element: Element, attr_name: str, node_name: str, precision: Optional[int], - verbosity: Optional[Verbosity], + verbosity: Verbosity, + default: Optional[float], ) -> None: """Set the value of an attribute from a subelement with a text node.""" - if getattr(obj, attr_name, None) is not None: + value = get_value(obj, attr_name=attr_name, verbosity=verbosity, default=default) + if value is not None: subelement = config.etree.SubElement( element, f"{obj.ns}{node_name}", ) - subelement.text = str(getattr(obj, attr_name)) + subelement.text = str(value) def float_attribute( - obj: _XMLObject, + obj: "_XMLObject", *, element: Element, attr_name: str, node_name: str, precision: Optional[int], - verbosity: Optional[Verbosity], + verbosity: Verbosity, + default: Optional[float], ) -> None: """Set the value of an attribute.""" - if getattr(obj, attr_name, None) is not None: - element.set(node_name, str(getattr(obj, attr_name))) + value = get_value(obj, attr_name=attr_name, verbosity=verbosity, default=default) + if value is not None: + element.set(node_name, str(value)) def enum_subelement( - obj: _XMLObject, + obj: "_XMLObject", *, element: Element, attr_name: str, node_name: str, precision: Optional[int], - verbosity: Optional[Verbosity], + verbosity: Verbosity, + default: Optional[Enum], ) -> None: """Set the value of an attribute from a subelement with a text node.""" - if getattr(obj, attr_name, None): + value = get_value(obj, attr_name=attr_name, verbosity=verbosity, default=default) + if value is not None: + ns = get_ns(obj, value) subelement = config.etree.SubElement( element, - f"{obj.ns}{node_name}", + f"{ns or ''}{node_name}", ) - subelement.text = getattr(obj, attr_name).value + subelement.text = value.value def enum_attribute( - obj: _XMLObject, + obj: "_XMLObject", *, element: Element, attr_name: str, node_name: str, precision: Optional[int], - verbosity: Optional[Verbosity], + verbosity: Verbosity, + default: Optional[Enum], ) -> None: """Set the value of an attribute.""" - if getattr(obj, attr_name, None): - element.set(node_name, getattr(obj, attr_name).value) + value = get_value(obj, attr_name=attr_name, verbosity=verbosity, default=default) + if value is not None: + element.set(node_name, value.value) + + +def datetime_subelement( + obj: "_XMLObject", + *, + element: Element, + attr_name: str, + node_name: str, + precision: Optional[int], + verbosity: Verbosity, + default: Optional[str], +) -> None: + """Create the subelement for a KML datetime values.""" + if value := get_value( + obj, + attr_name=attr_name, + verbosity=verbosity, + default=default, + ): + ns = get_ns(obj, value) + subelement = config.etree.SubElement( + element, + f"{ns}{node_name}", + ) + subelement.text = str(value) + + +def datetime_subelement_list( + obj: "_XMLObject", + *, + element: Element, + attr_name: str, + node_name: str, + precision: Optional[int], + verbosity: Verbosity, + default: Optional[str], +) -> None: + """Create the subelements for a list of KML datetime values.""" + if value := get_value( + obj, + attr_name=attr_name, + verbosity=verbosity, + default=default, + ): + for item in value: + ns = get_ns(obj, item) + subelement = config.etree.SubElement( + element, + f"{ns}{node_name}", + ) + subelement.text = str(item) + + +def coords_subelement_list( + obj: "_XMLObject", + *, + element: Element, + attr_name: str, + node_name: str, + precision: Optional[int], + verbosity: Verbosity, + default: Optional[str], +) -> None: + """Create the subelements for a list of KML coordinate values.""" + if value := get_value( + obj, + attr_name=attr_name, + verbosity=verbosity, + default=default, + ): + for coord in value: + ns = get_ns(obj, coord) + subelement = config.etree.SubElement( + element, + f"{ns}{node_name}", + ) + if precision is None: + subelement.text = " ".join(str(c) for c in coord) + else: + subelement.text = " ".join(f"{c:.{precision}f}" for c in coord) def xml_subelement( - obj: _XMLObject, + obj: "_XMLObject", *, element: Element, attr_name: str, node_name: str, precision: Optional[int], - verbosity: Optional[Verbosity], + verbosity: Verbosity, + default: Optional["_XMLObject"], ) -> None: """ Add a subelement to an XML element based on the value of an attribute of an object. Args: ---- - obj (_XMLObject): The object containing the attribute. + obj ("_XMLObject"): The object containing the attribute. element (Element): The XML element to which the subelement will be added. attr_name (str): The name of the attribute in the object. node_name (str): The name of the XML node for the subelement (unused). precision (Optional[int]): The precision for formatting numerical values. verbosity (Optional[Verbosity]): The verbosity level for the subelement. + default (Optional["_XMLObject"]): The default value for the attribute (unused). Returns: ------- @@ -378,25 +545,27 @@ def xml_subelement( def xml_subelement_list( - obj: _XMLObject, + obj: "_XMLObject", *, element: Element, attr_name: str, node_name: str, precision: Optional[int], - verbosity: Optional[Verbosity], + verbosity: Verbosity, + default: Optional[List["_XMLObject"]], ) -> None: """ Add subelements to an XML element based on a list attribute of an object. Args: ---- - obj (_XMLObject): The object containing the list attribute. + obj ("_XMLObject"): The object containing the list attribute. element (Element): The XML element to which the subelements will be added. attr_name (str): The name of the list attribute in the object. node_name (str): The name of the XML node for each subelement (unused). precision (Optional[int]): The precision for floating-point values. verbosity (Optional[Verbosity]): The verbosity level for the XML output. + default (Optional[List["_XMLObject"]]): The default value for the attribute. Returns: ------- @@ -418,7 +587,7 @@ def node_text_kwarg( name_spaces: Dict[str, str], node_name: str, kwarg: str, - classes: Tuple[known_types, ...], + classes: Tuple[Type[object], ...], strict: bool, ) -> Dict[str, str]: """ @@ -428,11 +597,11 @@ def node_text_kwarg( ---- element (Element): The XML element to extract the text content from. ns (str): The namespace of the XML element. - name_spaces (Dict[str, str]): - A dictionary mapping namespace prefixes to their URIs. + name_spaces (Dict[str, str]): A dictionary mapping namespace prefixes to their + URIs. node_name (str): The name of the XML node. kwarg (str): The name of the keyword argument to store the text content in. - classes (Tuple[known_types, ...]): A tuple of known types. + classes (Tuple[Type[object], ...]): A tuple of known types. strict (bool): A flag indicating whether to enforce strict parsing rules. Returns: @@ -453,7 +622,7 @@ def subelement_text_kwarg( name_spaces: Dict[str, str], node_name: str, kwarg: str, - classes: Tuple[known_types, ...], + classes: Tuple[Type[object], ...], strict: bool, ) -> Dict[str, str]: """ @@ -466,7 +635,7 @@ def subelement_text_kwarg( name_spaces (Dict[str, str]): A dictionary of namespace prefixes and URIs. node_name (str): The name of the subelement. kwarg (str): The key to use in the returned dictionary. - classes (Tuple[known_types, ...]): A tuple of known types. + classes (Tuple[Type[object], ...]): A tuple of known types. strict (bool): A flag indicating whether to enforce strict parsing. Returns: @@ -488,7 +657,7 @@ def attribute_text_kwarg( name_spaces: Dict[str, str], node_name: str, kwarg: str, - classes: Tuple[known_types, ...], + classes: Tuple[Type[object], ...], strict: bool, ) -> Dict[str, str]: """ @@ -501,7 +670,7 @@ def attribute_text_kwarg( name_spaces (Dict[str, str]): A dictionary mapping namespace prefixes to URIs. node_name (str): The name of the XML node. kwarg (str): The name of the keyword argument. - classes (Tuple[known_types, ...]): A tuple of known types. + classes (Tuple[Type[object], ...]): A tuple of known types. strict (bool): A flag indicating whether to enforce strict parsing. Returns: @@ -533,7 +702,7 @@ def subelement_bool_kwarg( name_spaces: Dict[str, str], node_name: str, kwarg: str, - classes: Tuple[known_types, ...], + classes: Tuple[Type[object], ...], strict: bool, ) -> Dict[str, bool]: """ @@ -546,7 +715,7 @@ def subelement_bool_kwarg( name_spaces (Dict[str, str]): A dictionary mapping namespace prefixes to URIs. node_name (str): The name of the subelement. kwarg (str): The name of the keyword argument to store the boolean value. - classes (Tuple[known_types, ...]): A tuple of known types. + classes (Tuple[Type[object], ...]): A tuple of known types. strict (bool): A flag indicating whether to enforce strict parsing. Returns: @@ -584,7 +753,7 @@ def subelement_int_kwarg( name_spaces: Dict[str, str], node_name: str, kwarg: str, - classes: Tuple[known_types, ...], + classes: Tuple[Type[object], ...], strict: bool, ) -> Dict[str, int]: """ @@ -597,7 +766,7 @@ def subelement_int_kwarg( name_spaces (Dict[str, str]): A dictionary mapping namespace prefixes to URIs. node_name (str): The name of the subelement. kwarg (str): The key to use in the returned dictionary. - classes (Tuple[known_types, ...]): A tuple of known types for error handling. + classes (Tuple[Type[object], ...]): A tuple of known types for error handling. strict (bool): A flag indicating whether to enforce strict parsing. Returns: @@ -633,7 +802,7 @@ def attribute_int_kwarg( name_spaces: Dict[str, str], node_name: str, kwarg: str, - classes: Tuple[known_types, ...], + classes: Tuple[Type[object], ...], strict: bool, ) -> Dict[str, int]: """ @@ -646,7 +815,7 @@ def attribute_int_kwarg( name_spaces (Dict[str, str]): A dictionary mapping namespace prefixes to URIs. node_name (str): The name of the XML node containing the attribute. kwarg (str): The name of the keyword argument to store the extracted attribute. - classes (Tuple[known_types, ...]): A tuple of known types (unused). + classes (Tuple[Type[object], ...]): A tuple of known types (unused). strict (bool): A flag indicating whether to raise an exception (unused). Returns: @@ -665,7 +834,7 @@ def subelement_float_kwarg( name_spaces: Dict[str, str], node_name: str, kwarg: str, - classes: Tuple[known_types, ...], + classes: Tuple[Type[object], ...], strict: bool, ) -> Dict[str, float]: """ @@ -678,7 +847,7 @@ def subelement_float_kwarg( name_spaces (Dict[str, str]): A dictionary of namespace prefixes and URIs. node_name (str): The name of the subelement. kwarg (str): The name of the keyword argument to store the float value. - classes (Tuple[known_types, ...]): A tuple of known types for error handling. + classes (Tuple[Type[object], ...]): A tuple of known types for error handling. strict (bool): A flag indicating whether to raise an error. Returns: @@ -714,7 +883,7 @@ def attribute_float_kwarg( name_spaces: Dict[str, str], node_name: str, kwarg: str, - classes: Tuple[known_types, ...], + classes: Tuple[Type[object], ...], strict: bool, ) -> Dict[str, float]: """ @@ -727,7 +896,7 @@ def attribute_float_kwarg( name_spaces (Dict[str, str]): A dictionary of namespace prefixes and URIs. node_name (str): The name of the attribute. kwarg (str): The name of the keyword argument to store the converted float. - classes (Tuple[known_types, ...]): A tuple of known types for error handling. + classes (Tuple[Type[object], ...]): A tuple of known types for error handling. strict (bool): A flag indicating whether to raise an error for invalid values. Returns: @@ -768,7 +937,7 @@ def subelement_enum_kwarg( name_spaces: Dict[str, str], node_name: str, kwarg: str, - classes: Tuple[known_types, ...], + classes: Tuple[Type[object], ...], strict: bool, ) -> Dict[str, Enum]: """ @@ -781,7 +950,7 @@ def subelement_enum_kwarg( name_spaces (Dict[str, str]): A dictionary of namespace prefixes and URIs. node_name (str): The name of the subelement. kwarg (str): The name of the keyword argument to store the extracted value. - classes (Tuple[known_types, ...]): A tuple of enumerated value classes. + classes (Tuple[Type[object], ...]): A tuple of enumerated value classes. strict (bool): A flag indicating whether to raise an exception. Returns: @@ -826,7 +995,7 @@ def attribute_enum_kwarg( name_spaces: Dict[str, str], node_name: str, kwarg: str, - classes: Tuple[known_types, ...], + classes: Tuple[Type[object], ...], strict: bool, ) -> Dict[str, Enum]: """ @@ -839,7 +1008,7 @@ def attribute_enum_kwarg( name_spaces (Dict[str, str]): A dictionary of namespace prefixes and their URIs. node_name (str): The name of the XML node. kwarg (str): The name of the keyword argument. - classes (Tuple[known_types, ...]): A tuple of enum classes. + classes (Tuple[Type[object], ...]): A tuple of enum classes. strict (bool): A flag indicating whether to raise an error for invalid values. Returns: @@ -869,6 +1038,98 @@ def attribute_enum_kwarg( return {} +def datetime_subelement_kwarg( + *, + element: Element, + ns: str, + name_spaces: Dict[str, str], + node_name: str, + kwarg: str, + classes: Tuple[Type[object], ...], + strict: bool, +) -> Dict[str, List["KmlDateTime"]]: + """Extract a KML datetime from a subelement of an XML element.""" + cls = classes[0] + node = element.find(f"{ns}{node_name}") + if node is None: + return {} + node_text = node.text.strip() if node.text else "" + if node_text: + try: + return {kwarg: cls.parse(node_text)} # type: ignore[attr-defined] + except ValueError as exc: + handle_error( + error=exc, + strict=strict, + element=element, + node=node, + expected="DateTime", + ) + return {} + + +def datetime_subelement_list_kwarg( + *, + element: Element, + ns: str, + name_spaces: Dict[str, str], + node_name: str, + kwarg: str, + classes: Tuple[Type[object], ...], + strict: bool, +) -> Dict[str, List["KmlDateTime"]]: + """Extract a list of KML datetime values from subelements of an XML element.""" + args_list: List[KmlDateTime] = [] + cls = classes[0] + if subelements := element.findall(f"{ns}{node_name}"): + for subelement in subelements: + try: + args_list.append( + cls.parse(subelement.text), # type: ignore[attr-defined] + ) + except ValueError as exc: # noqa: PERF203 + handle_error( + error=exc, + strict=strict, + element=element, + node=subelement, + expected="DateTime", + ) + return {kwarg: args_list} if args_list else {} + + +def coords_subelement_list_kwarg( + *, + element: Element, + ns: str, + name_spaces: Dict[str, str], + node_name: str, + kwarg: str, + classes: Tuple[Type[object], ...], + strict: bool, +) -> Dict[str, List[PointType]]: + """Extract a list of KML coordinate values from subelements of an XML element.""" + args_list: List[PointType] = [] + if subelements := element.findall(f"{ns}{node_name}"): + for subelement in subelements: + if subelement.text: + try: + coords = cast( + PointType, + tuple(float(coord) for coord in subelement.text.split()), + ) + args_list.append(coords) + except ValueError as exc: + handle_error( + error=exc, + strict=strict, + element=element, + node=subelement, + expected="Coordinates", + ) + return {kwarg: args_list} if args_list else {} + + def xml_subelement_kwarg( *, element: Element, @@ -876,9 +1137,9 @@ def xml_subelement_kwarg( name_spaces: Dict[str, str], node_name: str, kwarg: str, - classes: Tuple[known_types, ...], + classes: Tuple[Type[object], ...], strict: bool, -) -> Dict[str, _XMLObject]: +) -> Dict[str, "_XMLObject"]: """ Return the subelement of the given XML element based on the provided parameters. @@ -889,21 +1150,22 @@ def xml_subelement_kwarg( name_spaces (Dict[str, str]): A dictionary mapping namespace prefixes to their URIs. node_name (str): The name of the XML node to search for. kwarg (str): The name of the keyword argument to store the found subelement. - classes (Tuple[known_types, ...]): A tuple of classes that represent the types. + classes (Tuple[Type[object], ...]): A tuple of classes that represent the types. strict (bool): A flag indicating whether to enforce strict parsing rules. Returns: ------- - Dict[str, _XMLObject]: A dictionary containing the found subelement as the value + Dict[str, "_XMLObject"]: A dictionary containing the found subelement as the value of the specified keyword argument. """ for cls in classes: - assert issubclass(cls, _XMLObject) # noqa: S101 - subelement = element.find(f"{ns}{cls.get_tag_name()}") + subelement = element.find( + f"{ns}{cls.get_tag_name()}", # type: ignore[attr-defined] + ) if subelement is not None: return { - kwarg: cls.class_from_element( + kwarg: cls.class_from_element( # type: ignore[attr-defined] ns=ns, name_spaces=name_spaces, element=subelement, @@ -920,9 +1182,9 @@ def xml_subelement_list_kwarg( name_spaces: Dict[str, str], node_name: str, kwarg: str, - classes: Tuple[known_types, ...], + classes: Tuple[Type[object], ...], strict: bool, -) -> Dict[str, List[_XMLObject]]: +) -> Dict[str, List["_XMLObject"]]: """ Return a dictionary with the specified keyword argument and its list of subelements. @@ -933,12 +1195,12 @@ def xml_subelement_list_kwarg( name_spaces (Dict[str, str]): A dictionary mapping namespace prefixes to URIs. node_name (str): The name of the XML node to search for. kwarg (str): The name of the keyword argument to store the found subelements. - classes (Tuple[known_types, ...]): A tuple of classes that represent the types. + classes (Tuple[Type[object], ...]): A tuple of classes that represent the types. strict (bool): A flag indicating whether to enforce strict parsing rules. Returns: ------- - Dict[str, List[_XMLObject]]: A dictionary containing the specified keyword + Dict[str, List["_XMLObject"]]: A dictionary containing the specified keyword argument and its list of subelements. """ @@ -946,11 +1208,12 @@ def xml_subelement_list_kwarg( assert node_name is not None # noqa: S101 assert name_spaces is not None # noqa: S101 for obj_class in classes: - assert issubclass(obj_class, _XMLObject) # noqa: S101 - if subelements := element.findall(f"{ns}{obj_class.get_tag_name()}"): + if subelements := element.findall( + f"{ns}{obj_class.get_tag_name()}", # type: ignore[attr-defined] + ): args_list.extend( [ - obj_class.class_from_element( + obj_class.class_from_element( # type: ignore[attr-defined] ns=ns, name_spaces=name_spaces, element=subelement, diff --git a/fastkml/kml.py b/fastkml/kml.py index 08687679..7c5dab89 100644 --- a/fastkml/kml.py +++ b/fastkml/kml.py @@ -144,6 +144,7 @@ def etree_element( node_name="", precision=precision, verbosity=verbosity, + default=None, ) return cast(Element, root) @@ -152,9 +153,6 @@ def append( kmlobj: kml_children, ) -> None: """Append a feature.""" - if kmlobj is self: - msg = "Cannot append self" - raise ValueError(msg) self.features.append(kmlobj) @classmethod diff --git a/fastkml/links.py b/fastkml/links.py index 53486e10..62bd4bc8 100644 --- a/fastkml/links.py +++ b/fastkml/links.py @@ -44,14 +44,14 @@ class Link(_BaseObject): https://developers.google.com/kml/documentation/kmlreference#link """ - href: Optional[str] + href: str refresh_mode: Optional[RefreshMode] refresh_interval: Optional[float] view_refresh_mode: Optional[ViewRefreshMode] view_refresh_time: Optional[float] view_bound_scale: Optional[float] - view_format: Optional[str] - http_query: Optional[str] + view_format: str + http_query: str def __init__( self, @@ -77,14 +77,14 @@ def __init__( target_id=target_id, **kwargs, ) - self.href = href + self.href = href or "" self.refresh_mode = refresh_mode self.refresh_interval = refresh_interval self.view_refresh_mode = view_refresh_mode self.view_refresh_time = view_refresh_time self.view_bound_scale = view_bound_scale - self.view_format = view_format - self.http_query = http_query + self.view_format = view_format or "" + self.http_query = http_query or "" def __repr__(self) -> str: """Create a string (c)representation for Link.""" @@ -134,33 +134,24 @@ def __bool__(self) -> bool: Link, RegistryItem( ns_ids=("kml",), - attr_name="view_format", - node_name="viewFormat", - classes=(str,), - get_kwarg=subelement_text_kwarg, - set_element=text_subelement, - ), -) -registry.register( - Link, - RegistryItem( - ns_ids=("kml",), - attr_name="http_query", - node_name="httpQuery", - classes=(str,), - get_kwarg=subelement_text_kwarg, - set_element=text_subelement, + attr_name="refresh_mode", + node_name="refreshMode", + classes=(RefreshMode,), + get_kwarg=subelement_enum_kwarg, + set_element=enum_subelement, + default=RefreshMode.on_change, ), ) registry.register( Link, RegistryItem( ns_ids=("kml",), - attr_name="refresh_mode", - node_name="refreshMode", - classes=(RefreshMode,), - get_kwarg=subelement_enum_kwarg, - set_element=enum_subelement, + attr_name="refresh_interval", + node_name="refreshInterval", + classes=(float,), + get_kwarg=subelement_float_kwarg, + set_element=float_subelement, + default=4.0, ), ) registry.register( @@ -172,39 +163,54 @@ def __bool__(self) -> bool: classes=(ViewRefreshMode,), get_kwarg=subelement_enum_kwarg, set_element=enum_subelement, + default=ViewRefreshMode.never, ), ) registry.register( Link, RegistryItem( ns_ids=("kml",), - attr_name="refresh_interval", - node_name="refreshInterval", + attr_name="view_refresh_time", + node_name="viewRefreshTime", classes=(float,), get_kwarg=subelement_float_kwarg, set_element=float_subelement, + default=4.0, ), ) registry.register( Link, RegistryItem( ns_ids=("kml",), - attr_name="view_refresh_time", - node_name="viewRefreshTime", + attr_name="view_bound_scale", + node_name="viewBoundScale", classes=(float,), get_kwarg=subelement_float_kwarg, set_element=float_subelement, + default=1.0, ), ) registry.register( Link, RegistryItem( ns_ids=("kml",), - attr_name="view_bound_scale", - node_name="viewBoundScale", - classes=(float,), - get_kwarg=subelement_float_kwarg, - set_element=float_subelement, + attr_name="view_format", + node_name="viewFormat", + classes=(str,), + get_kwarg=subelement_text_kwarg, + set_element=text_subelement, + default="BBOX=[bboxWest],[bboxSouth],[bboxEast],[bboxNorth]", + ), +) +registry.register( + Link, + RegistryItem( + ns_ids=("kml",), + attr_name="http_query", + node_name="httpQuery", + classes=(str,), + get_kwarg=subelement_text_kwarg, + set_element=text_subelement, ), ) diff --git a/fastkml/overlays.py b/fastkml/overlays.py index b450c1e7..d5ea5ed3 100644 --- a/fastkml/overlays.py +++ b/fastkml/overlays.py @@ -245,6 +245,7 @@ def __repr__(self) -> str: classes=(str,), get_kwarg=subelement_text_kwarg, set_element=text_subelement, + default="ffffffff", ), ) registry.register( @@ -256,6 +257,7 @@ def __repr__(self) -> str: classes=(int,), get_kwarg=subelement_int_kwarg, set_element=int_subelement, + default=0, ), ) registry.register( @@ -394,6 +396,7 @@ def __bool__(self) -> bool: classes=(float,), get_kwarg=subelement_float_kwarg, set_element=float_subelement, + default=0.0, ), ) registry.register( @@ -405,6 +408,7 @@ def __bool__(self) -> bool: classes=(float,), get_kwarg=subelement_float_kwarg, set_element=float_subelement, + default=0.0, ), ) registry.register( @@ -416,6 +420,7 @@ def __bool__(self) -> bool: classes=(float,), get_kwarg=subelement_float_kwarg, set_element=float_subelement, + default=0.0, ), ) registry.register( @@ -427,6 +432,7 @@ def __bool__(self) -> bool: classes=(float,), get_kwarg=subelement_float_kwarg, set_element=float_subelement, + default=0.0, ), ) registry.register( @@ -438,6 +444,7 @@ def __bool__(self) -> bool: classes=(float,), get_kwarg=subelement_float_kwarg, set_element=float_subelement, + default=0.0, ), ) @@ -562,6 +569,7 @@ def __bool__(self) -> bool: classes=(int,), get_kwarg=subelement_int_kwarg, set_element=int_subelement, + default=256, ), ) registry.register( @@ -595,6 +603,7 @@ def __bool__(self) -> bool: classes=(GridOrigin,), get_kwarg=subelement_enum_kwarg, set_element=enum_subelement, + default=GridOrigin.lower_left, ), ) @@ -831,6 +840,7 @@ def __repr__(self) -> str: classes=(float,), get_kwarg=subelement_float_kwarg, set_element=float_subelement, + default=0.0, ), ) registry.register( @@ -875,6 +885,7 @@ def __repr__(self) -> str: classes=(Shape,), get_kwarg=subelement_enum_kwarg, set_element=enum_subelement, + default=Shape.rectangle, ), ) @@ -900,6 +911,8 @@ class LatLonBox(_XMLObject): Specifies a rotation of the overlay about its center, in degrees. Values can be ±180. The default is 0 (north). Rotations are specified in a counterclockwise direction. + + https://developers.google.com/kml/documentation/kmlreference#latlonbox """ _default_nsid = config.KML @@ -1042,6 +1055,7 @@ def __bool__(self) -> bool: classes=(float,), get_kwarg=subelement_float_kwarg, set_element=float_subelement, + default=0.0, ), ) @@ -1246,6 +1260,7 @@ def __repr__(self) -> str: classes=(float,), get_kwarg=subelement_float_kwarg, set_element=float_subelement, + default=0.0, ), ) registry.register( @@ -1257,6 +1272,7 @@ def __repr__(self) -> str: classes=(AltitudeMode,), get_kwarg=subelement_enum_kwarg, set_element=enum_subelement, + default=AltitudeMode.clamp_to_ground, ), ) registry.register( diff --git a/fastkml/registry.py b/fastkml/registry.py index d80aaff0..6dd36bb5 100644 --- a/fastkml/registry.py +++ b/fastkml/registry.py @@ -15,7 +15,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA """Registry for XML objects.""" from dataclasses import dataclass -from enum import Enum from typing import TYPE_CHECKING from typing import Any from typing import Dict @@ -23,7 +22,6 @@ from typing import Optional from typing import Tuple from typing import Type -from typing import Union from typing_extensions import Protocol @@ -34,16 +32,6 @@ from fastkml.base import _XMLObject -known_types = Union[ - Type["_XMLObject"], - Type[Enum], - Type[bool], - Type[int], - Type[str], - Type[float], -] - - class GetKWArgs(Protocol): def __call__( self, @@ -53,7 +41,7 @@ def __call__( name_spaces: Dict[str, str], node_name: str, kwarg: str, - classes: Tuple[known_types, ...], + classes: Tuple[Type[object], ...], strict: bool, ) -> Dict[str, Any]: ... @@ -67,7 +55,8 @@ def __call__( attr_name: str, node_name: str, precision: Optional[int], - verbosity: Optional[Verbosity], + verbosity: Verbosity, + default: Any, ) -> None: ... @@ -76,11 +65,12 @@ class RegistryItem: """A registry item.""" ns_ids: Tuple[str, ...] - classes: Tuple[known_types, ...] + classes: Tuple[Type[object], ...] attr_name: str get_kwarg: GetKWArgs set_element: SetElement node_name: str + default: Any = None class Registry: diff --git a/fastkml/schema/atom-author-link.xsd b/fastkml/schema/atom-author-link.xsd new file mode 100644 index 00000000..b3d77ade --- /dev/null +++ b/fastkml/schema/atom-author-link.xsd @@ -0,0 +1,66 @@ + + + + + atom-author-link.xsd 2008-01-23 + There is no official atom XSD. This XSD is created based on: + http://atompub.org/2005/08/17/atom.rnc. A subset of Atom as used in the + ogckml22.xsd is defined here. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fastkml/schema/kml22gx.xsd b/fastkml/schema/kml22gx.xsd new file mode 100644 index 00000000..6e8e74a2 --- /dev/null +++ b/fastkml/schema/kml22gx.xsd @@ -0,0 +1,329 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/schema/ogckml22.xsd b/fastkml/schema/ogckml22.xsd similarity index 99% rename from schema/ogckml22.xsd rename to fastkml/schema/ogckml22.xsd index 3496aef5..1a8086d4 100644 --- a/schema/ogckml22.xsd +++ b/fastkml/schema/ogckml22.xsd @@ -1,6 +1,7 @@ + schemaLocation="xAL.xsd"/> + + + diff --git a/fastkml/schema/xAL.xsd b/fastkml/schema/xAL.xsd new file mode 100644 index 00000000..53eb7428 --- /dev/null +++ b/fastkml/schema/xAL.xsd @@ -0,0 +1,1680 @@ + + + + + xAL: eXtensible Address Language +This is an XML document type definition (DTD) for +defining addresses. +Original Date of Creation: 1 March 2001 +Copyright(c) 2000, OASIS. All Rights Reserved [http://www.oasis-open.org] +Contact: Customer Information Quality Technical Committee, OASIS +http://www.oasis-open.org/committees/ciq +VERSION: 2.0 [MAJOR RELEASE] Date of Creation: 01 May 2002 +Last Update: 24 July 2002 +Previous Version: 1.3 + + + Common Attributes:Type - If not documented then it means, possible values of Type not limited to: Official, Unique, Abbreviation, OldName, Synonym +Code:Address element codes are used by groups like postal groups like ECCMA, ADIS, UN/PROLIST for postal services + + + + + Used by postal services to encode the name of the element. + + + + + + Root element for a list of addresses + + + + + + + + + Specific to DTD to specify the version number of DTD + + + + + + + + This container defines the details of the address. Can define multiple addresses including tracking address history + + + + + + + Postal authorities use specific postal service data to expedient delivery of mail + + + + + + A unique identifier of an address assigned by postal authorities. Example: DPID in Australia + + + + + Type of identifier. eg. DPID as in Australia + + + + + + + + + + Directly affects postal service distribution + + + + + Specific to postal service + + + + + + + + + Required for some postal services + + + + + Specific to postal service + + + + + + + + + Required for some postal services + + + + + Specific to postal service + + + + + + + + + Used for sorting addresses. Values may for example be CEDEX 16 (France) + + + + + Specific to postal service + + + + + + + + Latitude of delivery address + + + + + Specific to postal service + + + + + + + + + Latitude direction of delivery address;N = North and S = South + + + + Specific to postal service + + + + + + + + + Longtitude of delivery address + + + + + Specific to postal service + + + + + + + + + Longtitude direction of delivery address;N=North and S=South + + + + + Specific to postal service + + + + + + + + + any postal service elements not covered by the container can be represented using this element + + + + + Specific to postal service + + + + + + + + + + + USPS, ECMA, UN/PROLIST, etc + + + + + + + + Use the most suitable option. Country contains the most detailed information while Locality is missing Country and AdminArea + + + + Address as one line of free text + + + + + Postal, residential, corporate, etc + + + + + + + + + Container for Address lines + + + + + Specification of a country + + + + + + + A country code according to the specified scheme + + + + + Country code scheme possible values, but not limited to: iso.3166-2, iso.3166-3 for two and three character country codes. + + + + + + + + + + + + + + + + + + + + + + + + + + Type of address. Example: Postal, residential,business, primary, secondary, etc + + + + + Moved, Living, Investment, Deceased, etc.. + + + + + Start Date of the validity of address + + + + + End date of the validity of address + + + + + Communication, Contact, etc. + + + + + + Key identifier for the element for not reinforced references from other elements. Not required to be unique for the document to be valid, but application may get confused if not unique. Extend this schema adding unique constraint if needed. + + + + + + + + + + + + + + + + Occurrence of the building name before/after the type. eg. EGIS BUILDING where name appears before type + + + + + + + + + + + + + + + + + Name of the dependent locality + + + + + + + + + + Number of the dependent locality. Some areas are numbered. Eg. SECTOR 5 in a Suburb as in India or SOI SUKUMVIT 10 as in Thailand + + + + + Eg. SECTOR occurs before 5 in SECTOR 5 + + + + + + + + + + + + + + + + + Specification of a large mail user address. Examples of large mail users are postal companies, companies in France with a cedex number, hospitals and airports with their own post code. Large mail user addresses do not have a street name with premise name or premise number in countries like Netherlands. But they have a POBox and street also in countries like France + + + + + + A Postal van is specific for a route as in Is`rael, Rural route + + + + + + + + Dependent localities are Districts within cities/towns, locality divisions, postal +divisions of cities, suburbs, etc. DependentLocality is a recursive element, but no nesting deeper than two exists (Locality-DependentLocality-DependentLocality). + + + + + + + + City or IndustrialEstate, etc + + + + + Postal or Political - Sometimes locations must be distinguished between postal system, and physical locations as defined by a political system + + + + + "VIA" as in Hill Top VIA Parish where Parish is a locality and Hill Top is a dependent locality + + + + + Eg. Erode (Dist) where (Dist) is the Indicator + + + + + + + + + + Name of the firm + + + + + + + + + + + A MailStop is where the the mail is delivered to within a premise/subpremise/firm or a facility. + + + + + + + + + + + + + + Name of the large mail user. eg. Smith Ford International airport + + + + + Airport, Hospital, etc + + + + + + + + + Specification of the identification number of a large mail user. An example are the Cedex codes in France. + + + + + CEDEX Code + + + + + eg. Building 429 in which Building is the Indicator + + + + + + + + + Name of the building + + + + + + + + + + + + + + + + + Name of the the Mail Stop. eg. MSP, MS, etc + + + + + + + + + + Number of the Mail stop. eg. 123 in MS 123 + + + + + "-" in MS-123 + + + + + + + + + + + + + + + + + + Name of the Postal Route + + + + + + + + + + Number of the Postal Route + + + + + + + + + + + + + + + + + + + Name of the SubPremise + + + + + + EGIS Building where EGIS occurs before Building + + + + + + + + + + + + + + + + Name of the SubPremise Location. eg. LOBBY, BASEMENT, GROUND FLOOR, etc... + + + + + + + + Specification of the identifier of a sub-premise. Examples of sub-premises are apartments and suites. sub-premises in a building are often uniquely identified by means of consecutive +identifiers. The identifier can be a number, a letter or any combination of the two. In the latter case, the identifier includes exactly one variable (range) part, which is either a +number or a single letter that is surrounded by fixed parts at the left (prefix) or the right (postfix). + + + + + "TH" in 12TH which is a floor number, "NO." in NO.1, "#" in APT #12, etc. + + + + + "No." occurs before 1 in No.1, or TH occurs after 12 in 12TH + + + + + + + + + + + 12TH occurs "before" FLOOR (a type of subpremise) in 12TH FLOOR + + + + + + + + + + + "/" in 12/14 Archer Street where 12 is sub-premise number and 14 is premise number + + + + + + + + + + + Prefix of the sub premise number. eg. A in A-12 + + + + + A-12 where 12 is number and A is prefix and "-" is the separator + + + + + + + + + + Suffix of the sub premise number. eg. A in 12A + + + + + 12-A where 12 is number and A is suffix and "-" is the separator + + + + + + + + + + Name of the building + + + + + Specification of a firm, company, organization, etc. It can be specified as part of an address that contains a street or a postbox. It is therefore different from a large mail user address, which contains no street. + + + + + A MailStop is where the the mail is delivered to within a premise/subpremise/firm or a facility. + + + + + + Specification of a single sub-premise. Examples of sub-premises are apartments and suites. +Each sub-premise should be uniquely identifiable. SubPremiseType: Specification of the name of a sub-premise type. Possible values not limited to: Suite, Apartment, Floor, Unknown +Multiple levels within a premise by recursively calling SubPremise Eg. Level 4, Suite 2, Block C + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Free format address representation. An address can have more than one line. The order of the AddressLine elements must be preserved. + + + + + Defines the type of address line. eg. Street, Address Line 1, etc. + + + + + + + + + Locality is one level lower than adminisstrative area. Eg.: cities, reservations and any other built-up areas. + + + + + + + Name of the locality + + + + + + + + + + + + Specification of a large mail user address. Examples of large mail users are postal companies, companies in France with a cedex number, hospitals and airports with their own post code. Large mail user addresses do not have a street name with premise name or premise number in countries like Netherlands. But they have a POBox and street also in countries like France + + + + + + A Postal van is specific for a route as in Is`rael, Rural route + + + + + + + + Dependent localities are Districts within cities/towns, locality divisions, postal +divisions of cities, suburbs, etc. DependentLocality is a recursive element, but no nesting deeper than two exists (Locality-DependentLocality-DependentLocality). + + + + + + + + Possible values not limited to: City, IndustrialEstate, etc + + + + + Postal or Political - Sometimes locations must be distinguished between postal system, and physical locations as defined by a political system + + + + + Erode (Dist) where (Dist) is the Indicator + + + + + + + + Specification of a thoroughfare. A thoroughfare could be a rd, street, canal, river, etc. Note dependentlocality in a street. For example, in some countries, a large street will +have many subdivisions with numbers. Normally the subdivision name is the same as the road name, but with a number to identify it. Eg. SOI SUKUMVIT 3, SUKUMVIT RD, BANGKOK + + + + + + + + + A container to represent a range of numbers (from x thru y)for a thoroughfare. eg. 1-2 Albert Av + + + + + + + Starting number in the range + + + + + + + + + + + + + + + Ending number in the range + + + + + + + + + + + + + + + + Thoroughfare number ranges are odd or even + + + + + + + + + + + "No." No.12-13 + + + + + "-" in 12-14 or "Thru" in 12 Thru 14 etc. + + + + + No.12-14 where "No." is before actual street number + + + + + + + + + + + 23-25 Archer St, where number appears before name + + + + + + + + + + + + + + + + + + + + + North Baker Street, where North is the pre-direction. The direction appears before the name. + + + + + Appears before the thoroughfare name. Ed. Spanish: Avenida Aurora, where Avenida is the leading type / French: Rue Moliere, where Rue is the leading type. + + + + + Specification of the name of a Thoroughfare (also dependant street name): street name, canal name, etc. + + + + + Appears after the thoroughfare name. Ed. British: Baker Lane, where Lane is the trailing type. + + + + + 221-bis Baker Street North, where North is the post-direction. The post-direction appears after the name. + + + + + DependentThroughfare is related to a street; occurs in GB, IE, ES, PT + + + + + + + North Baker Street, where North is the pre-direction. The direction appears before the name. + + + + + Appears before the thoroughfare name. Ed. Spanish: Avenida Aurora, where Avenida is the leading type / French: Rue Moliere, where Rue is the leading type. + + + + + Specification of the name of a Thoroughfare (also dependant street name): street name, canal name, etc. + + + + + Appears after the thoroughfare name. Ed. British: Baker Lane, where Lane is the trailing type. + + + + + 221-bis Baker Street North, where North is the post-direction. The post-direction appears after the name. + + + + + + + + + + + + Dependent localities are Districts within cities/towns, locality divisions, postal +divisions of cities, suburbs, etc. DependentLocality is a recursive element, but no nesting deeper than two exists (Locality-DependentLocality-DependentLocality). + + + + + + Specification of a firm, company, organization, etc. It can be specified as part of an address that contains a street or a postbox. It is therefore different from +a large mail user address, which contains no street. + + + + + + + + + + Does this thoroughfare have a a dependent thoroughfare? Corner of street X, etc + + + + + + + + + + + Corner of, Intersection of + + + + + Corner of Street1 AND Street 2 where AND is the Connector + + + + + STS in GEORGE and ADELAIDE STS, RDS IN A and B RDS, etc. Use only when both the street types are the same + + + + + + + + Examples of administrative areas are provinces counties, special regions (such as "Rijnmond"), etc. + + + + + + + Name of the administrative area. eg. MI in USA, NSW in Australia + + + + + + + + + + Specification of a sub-administrative area. An example of a sub-administrative areas is a county. There are two places where the name of an administrative +area can be specified and in this case, one becomes sub-administrative area. + + + + + + + Name of the sub-administrative area + + + + + + + + + + + + + + + + + Province or State or County or Kanton, etc + + + + + Postal or Political - Sometimes locations must be distinguished between postal system, and physical locations as defined by a political system + + + + + Erode (Dist) where (Dist) is the Indicator + + + + + + + + + + + + + + + Province or State or County or Kanton, etc + + + + + Postal or Political - Sometimes locations must be distinguished between postal system, and physical locations as defined by a political system + + + + + Erode (Dist) where (Dist) is the Indicator + + + + + + + + Specification of a post office. Examples are a rural post office where post is delivered and a post office containing post office boxes. + + + + + + + + Specification of the name of the post office. This can be a rural postoffice where post is delivered or a post office containing post office boxes. + + + + + + + + + + Specification of the number of the postoffice. Common in rural postoffices + + + + + MS in MS 62, # in MS # 12, etc. + + + + + MS occurs before 62 in MS 62 + + + + + + + + + + + + + + + + A Postal van is specific for a route as in Is`rael, Rural route + + + + + + + + + Could be a Mobile Postoffice Van as in Isreal + + + + + eg. Kottivakkam (P.O) here (P.O) is the Indicator + + + + + + + + PostalCode is the container element for either simple or complex (extended) postal codes. Type: Area Code, Postcode, etc. + + + + + + + Specification of a postcode. The postcode is formatted according to country-specific rules. Example: SW3 0A8-1A, 600074, 2067 + + + + + Old Postal Code, new code, etc + + + + + + + + + Examples are: 1234 (USA), 1G (UK), etc. + + + + + Delivery Point Suffix, New Postal Code, etc.. + + + + + The separator between postal code number and the extension. Eg. "-" + + + + + + + + + A post town is not the same as a locality. A post town can encompass a collection of (small) localities. It can also be a subpart of a locality. An actual post town in Norway is "Bergen". + + + + + + + Name of the post town + + + + + + + + + + GENERAL PO in MIAMI GENERAL PO + + + + + + + + + + eg. village, town, suburb, etc + + + + + + + + + + Area Code, Postcode, Delivery code as in NZ, etc + + + + + + + + Specification of a postbox like mail delivery point. Only a single postbox number can be specified. Examples of postboxes are POBox, free mail numbers, etc. + + + + + + + Specification of the number of a postbox + + + + + + + + + Specification of the prefix of the post box number. eg. A in POBox:A-123 + + + + + A-12 where 12 is number and A is prefix and "-" is the separator + + + + + + + + + Specification of the suffix of the post box number. eg. A in POBox:123A + + + + + 12-A where 12 is number and A is suffix and "-" is the separator + + + + + + + + + Some countries like USA have POBox as 12345-123 + + + + + "-" is the NumberExtensionSeparator in POBOX:12345-123 + + + + + + + + Specification of a firm, company, organization, etc. It can be specified as part of an address that contains a street or a postbox. It is therefore different from +a large mail user address, which contains no street. + + + + + + + + Possible values are, not limited to: POBox and Freepost. + + + + + LOCKED BAG NO:1234 where the Indicator is NO: and Type is LOCKED BAG + + + + + + + + Subdivision in the firm: School of Physics at Victoria University (School of Physics is the department) + + + + + + + Specification of the name of a department. + + + + + + + + + + A MailStop is where the the mail is delivered to within a premise/subpremise/firm or a facility. + + + + + + + + School in Physics School, Division in Radiology division of school of physics + + + + + + + + Specification of a single premise, for example a house or a building. The premise as a whole has a unique premise (house) number or a premise name. There could be more than +one premise in a street referenced in an address. For example a building address near a major shopping centre or raiwlay station + + + + + + + Specification of the name of the premise (house, building, park, farm, etc). A premise name is specified when the premise cannot be addressed using a street name plus premise (house) number. + + + + + + EGIS Building where EGIS occurs before Building, DES JARDINS occurs after COMPLEX DES JARDINS + + + + + + + + + + + + + + + + LOBBY, BASEMENT, GROUND FLOOR, etc... + + + + + + + + + + + Specification for defining the premise number range. Some premises have number as Building C1-C7 + + + + + + Start number details of the premise number range + + + + + + + + + + + + + End number details of the premise number range + + + + + + + + + + + + + + Eg. Odd or even number range + + + + + Eg. No. in Building No:C1-C5 + + + + + "-" in 12-14 or "Thru" in 12 Thru 14 etc. + + + + + + No.12-14 where "No." is before actual street number + + + + + + + + + + + Building 23-25 where the number occurs after building name + + + + + + + + + + + + + + + + + + + Specification of the name of a building. + + + + + + Specification of a single sub-premise. Examples of sub-premises are apartments and suites. Each sub-premise should be uniquely identifiable. + + + + + Specification of a firm, company, organization, etc. It can be specified as part of an address that contains a street or a postbox. It is therefore different from a large mail user address, which contains no street. + + + + + + A MailStop is where the the mail is delivered to within a premise/subpremise/firm or a facility. + + + + + + + + + COMPLEX in COMPLEX DES JARDINS, A building, station, etc + + + + + STREET, PREMISE, SUBPREMISE, PARK, FARM, etc + + + + + NEAR, ADJACENT TO, etc + + + + + DES, DE, LA, LA, DU in RUE DU BOIS. These terms connect a premise/thoroughfare type and premise/thoroughfare name. Terms may appear with names AVE DU BOIS + + + + + + + + Prefix before the number. A in A12 Archer Street + + + + A-12 where 12 is number and A is prefix and "-" is the separator + + + + + + + + + + Suffix after the number. A in 12A Archer Street + + + + + NEAR, ADJACENT TO, etc + 12-A where 12 is number and A is suffix and "-" is the separator + + + + + + + + + + Eg.: 23 Archer street or 25/15 Zero Avenue, etc + + + + + 12 Archer Street is "Single" and 12-14 Archer Street is "Range" + + + + + + + + + + + + No. in Street No.12 or "#" in Street # 12, etc. + + + + + No.12 where "No." is before actual street number + + + + + + + + + + + 23 Archer St, Archer Street 23, St Archer 23 + + + + + + + + + + + + + + + + + Specification of the identifier of the premise (house, building, etc). Premises in a street are often uniquely identified by means of consecutive identifiers. The identifier can be a number, a letter or any combination of the two. + + + + + Building 12-14 is "Range" and Building 12 is "Single" + + + + + + + + + + + + No. in House No.12, # in #12, etc. + + + + + No. occurs before 12 No.12 + + + + + + + + + + + 12 in BUILDING 12 occurs "after" premise type BUILDING + + + + + + + + + + + + + + + A in A12 + + + + + + + A-12 where 12 is number and A is prefix and "-" is the separator + + + + + + + + + + + + A in 12A + + + + + 12-A where 12 is number and A is suffix and "-" is the separator + + + + + + + + + + Specification of the name of a country. + + + + + Old name, new name, etc + + + + + + + diff --git a/fastkml/styles.py b/fastkml/styles.py index d6a4c930..c70de2c7 100644 --- a/fastkml/styles.py +++ b/fastkml/styles.py @@ -183,8 +183,7 @@ class _ColorStyle(_BaseObject): https://developers.google.com/kml/documentation/kmlreference#colorstyle """ - id = None - color = None + color: Optional[str] = None # Color and opacity (alpha) values are expressed in hexadecimal notation. # The range of values for any one color is 0 to 255 (00 to ff). # For alpha, 00 is fully transparent and ff is fully opaque. @@ -257,6 +256,7 @@ def __repr__(self) -> str: classes=(str,), get_kwarg=subelement_text_kwarg, set_element=text_subelement, + default="ffffffff", ), ) registry.register( @@ -268,6 +268,7 @@ def __repr__(self) -> str: classes=(ColorMode,), get_kwarg=subelement_enum_kwarg, set_element=enum_subelement, + default=ColorMode.normal, ), ) @@ -371,6 +372,7 @@ def get_tag_name(cls) -> str: classes=(float,), get_kwarg=attribute_float_kwarg, set_element=float_attribute, + default=0.5, ), ) registry.register( @@ -382,6 +384,7 @@ def get_tag_name(cls) -> str: classes=(float,), get_kwarg=attribute_float_kwarg, set_element=float_attribute, + default=0.5, ), ) registry.register( @@ -393,6 +396,7 @@ def get_tag_name(cls) -> str: classes=(Units,), get_kwarg=attribute_enum_kwarg, set_element=enum_attribute, + default=Units.fraction, ), ) registry.register( @@ -404,6 +408,7 @@ def get_tag_name(cls) -> str: classes=(Units,), get_kwarg=attribute_enum_kwarg, set_element=enum_attribute, + default=Units.fraction, ), ) @@ -521,6 +526,7 @@ def __bool__(self) -> bool: classes=(float,), get_kwarg=subelement_float_kwarg, set_element=float_subelement, + default=1.0, ), ) registry.register( @@ -532,6 +538,7 @@ def __bool__(self) -> bool: classes=(float,), get_kwarg=subelement_float_kwarg, set_element=float_subelement, + default=0.0, ), ) registry.register( @@ -652,6 +659,7 @@ def __bool__(self) -> bool: classes=(float,), get_kwarg=subelement_float_kwarg, set_element=float_subelement, + default=1.0, ), ) @@ -760,6 +768,7 @@ def __bool__(self) -> bool: classes=(bool,), get_kwarg=subelement_bool_kwarg, set_element=bool_subelement, + default=True, ), ) registry.register( @@ -771,6 +780,7 @@ def __bool__(self) -> bool: classes=(bool,), get_kwarg=subelement_bool_kwarg, set_element=bool_subelement, + default=True, ), ) @@ -872,6 +882,7 @@ def __bool__(self) -> bool: classes=(float,), get_kwarg=subelement_float_kwarg, set_element=float_subelement, + default=1.0, ), ) @@ -1023,6 +1034,7 @@ def __bool__(self) -> bool: classes=(str,), get_kwarg=subelement_text_kwarg, set_element=text_subelement, + default="ffffffff", ), ) registry.register( @@ -1034,6 +1046,7 @@ def __bool__(self) -> bool: classes=(str,), get_kwarg=subelement_text_kwarg, set_element=text_subelement, + default="ff000000", ), ) registry.register( @@ -1056,6 +1069,7 @@ def __bool__(self) -> bool: classes=(DisplayMode,), get_kwarg=subelement_enum_kwarg, set_element=enum_subelement, + default=DisplayMode.default, ), ) diff --git a/fastkml/times.py b/fastkml/times.py index 54c4d0bc..62b06fd9 100644 --- a/fastkml/times.py +++ b/fastkml/times.py @@ -13,7 +13,15 @@ # 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 -"""Date and time handling in KML.""" +""" +Date and time handling in KML. + +Any Feature in KML can have time data associated with it. +This time data has the effect of restricting the visibility of the data set to a given +time period or point in time. + +https://developers.google.com/kml/documentation/time +""" import re from datetime import date from datetime import datetime @@ -26,12 +34,14 @@ from fastkml import config from fastkml.enums import DateTimeResolution -from fastkml.enums import Verbosity +from fastkml.helpers import datetime_subelement +from fastkml.helpers import datetime_subelement_kwarg from fastkml.kml_base import _BaseObject -from fastkml.types import Element +from fastkml.registry import RegistryItem +from fastkml.registry import registry # regular expression to parse a gYearMonth string -# year and month may be separated by a dash or not +# year and month may be separated by an optional dash # year is always 4 digits, month is always 2 digits year_month_day = re.compile( r"^(?P\d{4})(?:-)?(?P\d{2})?(?:-)?(?P\d{2})?$", @@ -143,6 +153,11 @@ def parse(cls, datestr: str) -> Optional["KmlDateTime"]: resolution = DateTimeResolution.datetime return cls(dt, resolution) if dt else None + @classmethod + def get_ns_id(cls) -> str: + """Return the namespace ID.""" + return config.KML + class _TimePrimitive(_BaseObject): """ @@ -154,7 +169,11 @@ class _TimePrimitive(_BaseObject): class TimeStamp(_TimePrimitive): - """Represents a single moment in time.""" + """ + Represents a single moment in time. + + https://developers.google.com/kml/documentation/kmlreference#timestamp + """ def __init__( self, @@ -214,55 +233,26 @@ def __bool__(self) -> bool: """Return True if the timestamp is valid.""" return bool(self.timestamp) - def etree_element( - self, - precision: Optional[int] = None, - verbosity: Verbosity = Verbosity.normal, - ) -> Element: - """ - Create an ElementTree element representing the TimeStamp object. - - Args: - ---- - precision (Optional[int]): The precision of the timestamp. - verbosity (Verbosity): The verbosity level of the element. - - Returns: - ------- - Element: The ElementTree element representing the TimeStamp object. - """ - element = super().etree_element(precision=precision, verbosity=verbosity) - when = config.etree.SubElement( - element, - f"{self.ns}when", - ) - when.text = str(self.timestamp) - return element - - @classmethod - def _get_kwargs( - cls, - *, - ns: str, - name_spaces: Optional[Dict[str, str]] = None, - element: Element, - strict: bool, - ) -> Dict[str, Any]: - kwargs = super()._get_kwargs( - ns=ns, - name_spaces=name_spaces, - element=element, - strict=strict, - ) - when = element.find(f"{ns}when") - if when is not None: - kwargs["timestamp"] = KmlDateTime.parse(when.text) - return kwargs +registry.register( + TimeStamp, + item=RegistryItem( + ns_ids=("kml", "gx", ""), + classes=(KmlDateTime,), + attr_name="timestamp", + node_name="when", + get_kwarg=datetime_subelement_kwarg, + set_element=datetime_subelement, + ), +) class TimeSpan(_TimePrimitive): - """Represents an extent in time bounded by begin and end dateTimes.""" + """ + Represents an extent in time bounded by begin and end dateTimes. + + https://developers.google.com/kml/documentation/kmlreference#timespan + """ def __init__( self, @@ -321,60 +311,26 @@ def __bool__(self) -> bool: """Return True if the begin or end date is valid.""" return bool(self.begin) or bool(self.end) - def etree_element( - self, - precision: Optional[int] = None, - verbosity: Verbosity = Verbosity.normal, - ) -> Element: - """ - Create an Element object representing the time interval. - - Args: - ---- - precision (Optional[int]): The precision of the time values. - verbosity (Verbosity): The verbosity level for the element. - - Returns: - ------- - Element: The created Element object. - - """ - element = super().etree_element(precision=precision, verbosity=verbosity) - if self.begin is not None: # noqa: SIM102 - if text := str(self.begin): - begin = config.etree.SubElement( - element, - f"{self.ns}begin", - ) - begin.text = text - if self.end is not None: # noqa: SIM102 - if text := str(self.end): - end = config.etree.SubElement( - element, - f"{self.ns}end", - ) - end.text = text - return element - @classmethod - def _get_kwargs( - cls, - *, - ns: str, - name_spaces: Optional[Dict[str, str]] = None, - element: Element, - strict: bool, - ) -> Dict[str, Any]: - kwargs = super()._get_kwargs( - ns=ns, - name_spaces=name_spaces, - element=element, - strict=strict, - ) - begin = element.find(f"{ns}begin") - if begin is not None: - kwargs["begin"] = KmlDateTime.parse(begin.text) - end = element.find(f"{ns}end") - if end is not None: - kwargs["end"] = KmlDateTime.parse(end.text) - return kwargs +registry.register( + TimeSpan, + item=RegistryItem( + ns_ids=("kml", "gx", ""), + classes=(KmlDateTime,), + attr_name="begin", + node_name="begin", + get_kwarg=datetime_subelement_kwarg, + set_element=datetime_subelement, + ), +) +registry.register( + TimeSpan, + item=RegistryItem( + ns_ids=("kml", "gx", ""), + classes=(KmlDateTime,), + attr_name="end", + node_name="end", + get_kwarg=datetime_subelement_kwarg, + set_element=datetime_subelement, + ), +) diff --git a/fastkml/utils.py b/fastkml/utils.py new file mode 100644 index 00000000..2851cb5c --- /dev/null +++ b/fastkml/utils.py @@ -0,0 +1,91 @@ +"""Fastkml utility functions.""" + +from typing import Any +from typing import Generator +from typing import Optional +from typing import Tuple +from typing import Type +from typing import Union + +__all__ = ["find_all", "has_attribute_values"] + + +def has_attribute_values(obj: object, **kwargs: Any) -> bool: + """ + Check if an object has all of the given attribute values. + + Args: + ---- + obj: The object to check. + **kwargs: Attributes of the object to match. + + Returns: + ------- + True if the object has the given attribute values, False otherwise. + + """ + try: + return all(getattr(obj, key) == value for key, value in kwargs.items()) + except AttributeError: + return False + + +def find_all( + obj: object, + *, + of_type: Optional[Union[Type[object], Tuple[Type[object], ...]]] = None, + **kwargs: Any, +) -> Generator[object, None, None]: + """ + Find all instances of a given type in a given object. + + Args: + ---- + obj: The object to search. + of_type: The type(s) to search for or None for any type. + **kwargs: Attributes of the object to match. + + Returns: + ------- + An iterable of all instances of the given type in the given object. + + """ + if (of_type is None or isinstance(obj, of_type)) and has_attribute_values( + obj, + **kwargs, + ): + yield obj + try: + attrs = (attr for attr in obj.__dict__ if not attr.startswith("_")) + except AttributeError: + return + for attr_name in attrs: + attr = getattr(obj, attr_name) + try: + for item in attr: + yield from find_all(item, of_type=of_type, **kwargs) + except TypeError: + yield from find_all(attr, of_type=of_type, **kwargs) + + +def find( + obj: object, + *, + of_type: Optional[Union[Type[object], Tuple[Type[object], ...]]] = None, + **kwargs: Any, +) -> Optional[object]: + """ + Find the first instance of a given type in a given object. + + Args: + ---- + obj: The object to search. + of_type: The type(s) to search for or None for any type. + **kwargs: Attributes of the object to match. + + Returns: + ------- + The first instance of the given type in the given object or None if not found. + + """ + return next(find_all(obj, of_type=of_type, **kwargs), None) diff --git a/fastkml/validator.py b/fastkml/validator.py new file mode 100644 index 00000000..c40fdf82 --- /dev/null +++ b/fastkml/validator.py @@ -0,0 +1,114 @@ +# Copyright (C) 2024 Christian Ledermann +# +# This library is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +"""Validate KML files against the XML schema.""" +import logging +import pathlib +from functools import lru_cache +from typing import Final +from typing import Optional + +from fastkml import config +from fastkml.types import Element + +logger = logging.getLogger(__name__) + +MUTUAL_EXCLUSIVE: Final = "Only one of element and file_to_validate can be provided." +REQUIRE_ONE_OF: Final = "Either element or file_to_validate must be provided." + + +@lru_cache(maxsize=16) +def get_schema_parser( + schema: Optional[pathlib.Path] = None, +) -> "config.etree.XMLSchema": + """ + Parse the XML schema. + + Args: + ---- + schema: The path to the XML schema file. + + Returns: + ------- + The parsed XML schema. + + To clear the cache call get_schema_parser.cache_clear(). + + """ + if schema is None: + schema = pathlib.Path(__file__).parent / "schema" / "ogckml22.xsd" + return config.etree.XMLSchema(config.etree.parse(schema)) + + +def validate( + *, + schema: Optional[pathlib.Path] = None, + element: Optional[Element] = None, + file_to_validate: Optional[pathlib.Path] = None, +) -> Optional[bool]: + """ + Validate a KML file against the XML schema. + + Args: + ---- + schema: The path to the XML schema file. + element: The element to validate. + file_to_validate: The file to validate. + + Returns: + ------- + True if the file or element is valid. + Raises an AssertionError if validation fails. + Returns None if the schema parser is unavailable. + + """ + if element is None and file_to_validate is None: + raise ValueError(REQUIRE_ONE_OF) + if element is not None and file_to_validate is not None: + raise ValueError(MUTUAL_EXCLUSIVE) + + try: + schema_parser = get_schema_parser(schema) + except AttributeError: + return None + + if file_to_validate is not None: + element = config.etree.parse(file_to_validate) + + try: + schema_parser.assert_(element) # noqa: PT009 + except AssertionError: + log = schema_parser.error_log + for e in log: + try: + parent = element.xpath(e.path)[ # type: ignore[union-attr] + 0 + ].getparent() + except config.etree.XPathEvalError: + parent = element + error_in_xml = config.etree.tostring( + parent, + encoding="UTF-8", + pretty_print=True, + ).decode( + "UTF-8", + ) + logger.error( # noqa: TRY400 + "Error <%s> in XML:\n %s", + e.message, + error_in_xml, + ) + raise + return True diff --git a/fastkml/views.py b/fastkml/views.py index 45ffbd89..5e56d545 100644 --- a/fastkml/views.py +++ b/fastkml/views.py @@ -25,8 +25,10 @@ from fastkml.enums import AltitudeMode from fastkml.helpers import enum_subelement from fastkml.helpers import float_subelement +from fastkml.helpers import int_subelement from fastkml.helpers import subelement_enum_kwarg from fastkml.helpers import subelement_float_kwarg +from fastkml.helpers import subelement_int_kwarg from fastkml.helpers import xml_subelement from fastkml.helpers import xml_subelement_kwarg from fastkml.kml_base import _BaseObject @@ -180,6 +182,7 @@ def __repr__(self) -> str: classes=(float,), get_kwarg=subelement_float_kwarg, set_element=float_subelement, + default=0.0, ), ) registry.register( @@ -191,6 +194,7 @@ def __repr__(self) -> str: classes=(float,), get_kwarg=subelement_float_kwarg, set_element=float_subelement, + default=0.0, ), ) registry.register( @@ -202,6 +206,7 @@ def __repr__(self) -> str: classes=(float,), get_kwarg=subelement_float_kwarg, set_element=float_subelement, + default=0.0, ), ) registry.register( @@ -213,6 +218,7 @@ def __repr__(self) -> str: classes=(float,), get_kwarg=subelement_float_kwarg, set_element=float_subelement, + default=0.0, ), ) registry.register( @@ -224,6 +230,7 @@ def __repr__(self) -> str: classes=(float,), get_kwarg=subelement_float_kwarg, set_element=float_subelement, + default=0.0, ), ) registry.register( @@ -235,6 +242,7 @@ def __repr__(self) -> str: classes=(AltitudeMode,), get_kwarg=subelement_enum_kwarg, set_element=enum_subelement, + default=AltitudeMode.clamp_to_ground, ), ) registry.register( @@ -271,6 +279,8 @@ class Camera(_AbstractView): Time values in Camera affect historical imagery, sunlight, and the display of time-stamped features. For more information, read Time with AbstractViews in the Time and Animation chapter of the Developer's Guide. + + https://developers.google.com/kml/documentation/kmlreference#camera """ roll: Optional[float] @@ -289,7 +299,7 @@ def __init__( heading: Optional[float] = None, tilt: Optional[float] = None, roll: Optional[float] = None, - altitude_mode: AltitudeMode = AltitudeMode.relative_to_ground, + altitude_mode: Optional[AltitudeMode] = None, time_primitive: Union[TimeSpan, TimeStamp, None] = None, **kwargs: Any, ) -> None: @@ -364,6 +374,7 @@ def __repr__(self) -> str: classes=(float,), get_kwarg=subelement_float_kwarg, set_element=float_subelement, + default=0.0, ), ) @@ -395,7 +406,7 @@ def __init__( heading: Optional[float] = None, tilt: Optional[float] = None, range: Optional[float] = None, - altitude_mode: AltitudeMode = AltitudeMode.relative_to_ground, + altitude_mode: Optional[AltitudeMode] = None, time_primitive: Union[TimeSpan, TimeStamp, None] = None, **kwargs: Any, ) -> None: @@ -624,6 +635,7 @@ def __bool__(self) -> bool: classes=(float,), get_kwarg=subelement_float_kwarg, set_element=float_subelement, + default=0.0, ), ) registry.register( @@ -635,6 +647,7 @@ def __bool__(self) -> bool: classes=(float,), get_kwarg=subelement_float_kwarg, set_element=float_subelement, + default=0.0, ), ) registry.register( @@ -646,6 +659,7 @@ def __bool__(self) -> bool: classes=(AltitudeMode,), get_kwarg=subelement_enum_kwarg, set_element=enum_subelement, + default=AltitudeMode.clamp_to_ground, ), ) @@ -664,19 +678,19 @@ class Lod(_XMLObject): _default_nsid = config.KML - min_lod_pixels: Optional[float] - max_lod_pixels: Optional[float] - min_fade_extent: Optional[float] - max_fade_extent: Optional[float] + min_lod_pixels: Optional[int] + max_lod_pixels: Optional[int] + min_fade_extent: Optional[int] + max_fade_extent: Optional[int] def __init__( self, ns: Optional[str] = None, name_spaces: Optional[Dict[str, str]] = None, - min_lod_pixels: Optional[float] = None, - max_lod_pixels: Optional[float] = None, - min_fade_extent: Optional[float] = None, - max_fade_extent: Optional[float] = None, + min_lod_pixels: Optional[int] = None, + max_lod_pixels: Optional[int] = None, + min_fade_extent: Optional[int] = None, + max_fade_extent: Optional[int] = None, **kwargs: Any, ) -> None: """ @@ -737,8 +751,9 @@ def __bool__(self) -> bool: attr_name="min_lod_pixels", node_name="minLodPixels", classes=(float,), - get_kwarg=subelement_float_kwarg, - set_element=float_subelement, + get_kwarg=subelement_int_kwarg, + set_element=int_subelement, + default=256, ), ) registry.register( @@ -748,8 +763,9 @@ def __bool__(self) -> bool: attr_name="max_lod_pixels", node_name="maxLodPixels", classes=(float,), - get_kwarg=subelement_float_kwarg, - set_element=float_subelement, + get_kwarg=subelement_int_kwarg, + set_element=int_subelement, + default=-1, ), ) registry.register( @@ -761,6 +777,7 @@ def __bool__(self) -> bool: classes=(float,), get_kwarg=subelement_float_kwarg, set_element=float_subelement, + default=0, ), ) registry.register( @@ -772,6 +789,7 @@ def __bool__(self) -> bool: classes=(float,), get_kwarg=subelement_float_kwarg, set_element=float_subelement, + default=0, ), ) diff --git a/pyproject.toml b/pyproject.toml index d8a2cfbb..dc3c71e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ authors = [ { email = "christian.ledermann@gmail.com", name = "Christian Ledermann" }, ] classifiers = [ - "Development Status :: 3 - Alpha", + "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)", "Operating System :: OS Independent", @@ -21,6 +21,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Scientific/Engineering :: GIS", "Topic :: Text Processing :: Markup :: XML", "Typing :: Typed", @@ -46,17 +47,13 @@ complexity = [ "radon", ] dev = [ - "fastkml[complexity]", - "fastkml[docs]", - "fastkml[linting]", - "fastkml[lxml]", - "fastkml[tests]", - "fastkml[typing]", + "fastkml[complexity,docs,linting,lxml,tests,typing]", "pre-commit", "shapely", ] docs = [ "Sphinx", + "pyshp", "sphinx-autodoc-typehints", "sphinx-rtd-theme", ] @@ -77,8 +74,11 @@ lxml = [ "lxml", ] tests = [ + "hypothesis[dateutil]", "pytest", "pytest-cov", + "pytz", + "tzdata", ] typing = [ "mypy", @@ -105,7 +105,6 @@ ignore = [ ".*", "examples/*", "mutmut_config.py", - "test-requirements.txt", "tox.ini", ] @@ -118,6 +117,7 @@ source = [ [tool.coverage.report] exclude_also = [ "^\\s*\\.\\.\\.$", + "class \\w+\\(Protocol\\)\\:", "except AssertionError:", "except ImportError:", "if TYPE_CHECKING:", @@ -146,10 +146,6 @@ ignore_errors = false ignore_missing_imports = true implicit_reexport = false no_implicit_optional = true -overrides = [ - { disable_error_code = "attr-defined, union-attr", module = "tests.oldunit_test" }, - { disable_error_code = "union-attr", module = "tests.*" }, -] show_error_codes = true strict_equality = true strict_optional = true @@ -160,6 +156,18 @@ warn_unreachable = true warn_unused_configs = true warn_unused_ignores = true +[[tool.mypy.overrides]] +disable_error_code = "attr-defined, union-attr" +module = "tests.oldunit_test" + +[[tool.mypy.overrides]] +disable_error_code = "attr-defined, union-attr, no-untyped-def, no-untyped-call, arg-type" +module = "examples.*" + +[[tool.mypy.overrides]] +disable_error_code = "union-attr" +module = "tests.*" + [tool.pyright] exclude = [ "**/__pycache__", @@ -187,67 +195,21 @@ ignore = [ "PLR0913", ] select = [ - "A", - "AIR", - "ANN", - "ARG", - "ASYNC", - "B", - "BLE", - "C4", - "C90", - "COM", - "CPY", - "D", - "DJ", - "DTZ", - "E", - "EM", - "ERA", - "EXE", - "F", - "FA", - "FBT", - "FIX", - "FLY", - "FURB", - "G", - "I", - "ICN", - "INP", - "INT", - "ISC", - "LOG", - "N", - "NPY", - "PD", - "PERF", - "PGH", - "PIE", - "PL", - "PT", - "PTH", - "PYI", - "Q", - "RET", - "RSE", - "RUF", - "S", - "SIM", - "SLF", - "SLOT", - "T10", - "T20", - "TCH", - "TD", - "TID", - "TRY", - "UP", - "W", - "YTT", + "ALL", ] [tool.ruff.lint.extend-per-file-ignores] +"examples/*.py" = [ + "ANN001", + "ANN201", + "D100", + "D104", + "D401", + "ICN001", + "INP001", + "S311", + "T201", +] "fastkml/helpers.py" = [ "ARG001", "PLR0913", @@ -262,7 +224,13 @@ select = [ "SLF001", ] "tests/oldunit_test.py" = [ + "D100", + "D200", + "D401", "E501", + "FIX", + "ICN001", + "TD", ] "tests/repr_eq_test.py" = [ "E501", diff --git a/tests/atom_test.py b/tests/atom_test.py index 92c2e968..56635fc5 100644 --- a/tests/atom_test.py +++ b/tests/atom_test.py @@ -64,14 +64,14 @@ def test_atom_link_round_trip(self) -> None: length=3456, ) - link2 = atom.Link.class_from_string(link.to_string()) + link2 = atom.Link.from_string(link.to_string()) assert link == link2 assert link.to_string() == link2.to_string() assert repr(link) == repr(link2) def test_atom_link_read(self) -> None: - link = atom.Link.class_from_string( + link = atom.Link.from_string( '', @@ -84,12 +84,12 @@ def test_atom_link_read(self) -> None: assert link.length == 3456 def test_atom_link_read_no_href(self) -> None: - link = atom.Link.class_from_string( + link = atom.Link.from_string( '', ) - assert link.href is None + assert link.href == "" def test_atom_person_ns(self) -> None: ns = "{http://www.opengis.net/kml/2.2}" @@ -112,7 +112,7 @@ def test_atom_author(self) -> None: assert "" in serialized def test_atom_author_read(self) -> None: - a = atom.Author.class_from_string( + a = atom.Author.from_string( '' "Nobodyhttp://localhost" "cl@donotreply.com", @@ -131,28 +131,28 @@ def test_atom_author_round_trip(self) -> None: email="cl@donotreply.com", ) - a2 = atom.Author.class_from_string(a.to_string()) + a2 = atom.Author.from_string(a.to_string()) assert a == a2 assert a.to_string() == a2.to_string() assert repr(a) == repr(a2) def test_atom_contributor_read_no_name(self) -> None: - a = atom.Contributor.class_from_string( + a = atom.Contributor.from_string( '' "http://localhost" "cl@donotreply.com", ns="{http://www.w3.org/2005/Atom}", ) - assert a.name is None + assert a.name == "" assert a.uri == "http://localhost" assert a.email == "cl@donotreply.com" def test_atom_contributor_no_name(self) -> None: a = atom.Contributor(uri="http://localhost", email="cl@donotreply.com") - assert a.name is None + assert a.name == "" assert "atom:name" not in a.to_string() def test_atom_contributor_roundtrip(self) -> None: @@ -163,7 +163,7 @@ def test_atom_contributor_roundtrip(self) -> None: email="cl@donotreply.com", ) - a2 = atom.Contributor.class_from_string(a.to_string()) + a2 = atom.Contributor.from_string(a.to_string()) assert a == a2 assert a.to_string() == a2.to_string() diff --git a/tests/base.py b/tests/base.py index 5327191f..250b2480 100644 --- a/tests/base.py +++ b/tests/base.py @@ -15,7 +15,7 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA """Base classes to run the tests both with the std library and lxml.""" -import xml.etree.ElementTree +import xml.etree.ElementTree as ET import pytest @@ -27,14 +27,15 @@ LXML = False from fastkml import config +from fastkml.validator import get_schema_parser class StdLibrary: """Configure test to run with the standard library.""" def setup_method(self) -> None: - """Always test with the same parser.""" - config.set_etree_implementation(xml.etree.ElementTree) + """Ensure to always test with the standard library xml ElementTree parser.""" + config.set_etree_implementation(ET) config.set_default_namespaces() @@ -47,6 +48,7 @@ class Lxml: """ def setup_method(self) -> None: - """Always test with the same parser.""" + """Ensure to always test with the lxml parse.""" config.set_etree_implementation(lxml.etree) config.set_default_namespaces() + get_schema_parser() diff --git a/tests/base_test.py b/tests/base_test.py index 5f1b742f..8388c665 100644 --- a/tests/base_test.py +++ b/tests/base_test.py @@ -88,7 +88,7 @@ def test_to_str_empty_ns(self) -> None: ) == '<_BaseObject id="id-0" targetId="target-id-0" />'.replace(" ", "") def test_from_string(self) -> None: - be = kml_base._BaseObject.class_from_string( + be = kml_base._BaseObject.from_string( string=( '' @@ -98,7 +98,7 @@ def test_from_string(self) -> None: assert be.target_id == "target-id-0" def test_from_string_attr_ns_prefix(self) -> None: - be = kml_base._BaseObject.class_from_string( + be = kml_base._BaseObject.from_string( string=( '' @@ -108,7 +108,7 @@ def test_from_string_attr_ns_prefix(self) -> None: assert be.target_id == "target-id-0" def test_base_class_from_string(self) -> None: - be = kml_base._BaseObject.class_from_string( + be = kml_base._BaseObject.from_string( '', ) @@ -117,7 +117,7 @@ def test_base_class_from_string(self) -> None: assert be.ns == "{http://www.opengis.net/kml/2.2}" def test_base_class_from_empty_string(self) -> None: - be = kml_base._BaseObject.class_from_string("") + be = kml_base._BaseObject.from_string("") assert be.id == "" assert be.target_id == "" @@ -125,7 +125,7 @@ def test_base_class_from_empty_string(self) -> None: def test_xml_object_roundtrip(self) -> None: obj = base._XMLObject() - obj2 = base._XMLObject.class_from_string(obj.to_string(), ns="") + obj2 = base._XMLObject.from_string(obj.to_string(), ns="") assert obj == obj2 assert str(obj) == obj2.to_string() @@ -134,7 +134,7 @@ def test_xml_object_roundtrip(self) -> None: def test_base_object_roundtrip(self) -> None: obj = kml_base._BaseObject(id="id-0", target_id="target-id-0") - obj2 = kml_base._BaseObject.class_from_string(obj.to_string()) + obj2 = kml_base._BaseObject.from_string(obj.to_string()) assert obj == obj2 assert str(obj) == obj2.to_string() @@ -153,7 +153,7 @@ def test_to_string(self) -> None: ) def test_from_string(self) -> None: - be = kml_base._BaseObject.class_from_string( + be = kml_base._BaseObject.from_string( string=( '\n' diff --git a/tests/config_test.py b/tests/config_test.py index a7e722aa..c1ab10ca 100644 --- a/tests/config_test.py +++ b/tests/config_test.py @@ -16,7 +16,7 @@ """Test the configuration options.""" -import xml.etree.ElementTree as ET +from xml.etree import ElementTree as ET import pytest diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..b54dbc5a --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,21 @@ +""" +Configure the tests. + +Register the hypothesis 'exhaustive' profile to run 10 thousand examples. +Run this profile with ``pytest --hypothesis-profile=exhaustive`` +""" + +from hypothesis import HealthCheck +from hypothesis import settings + +settings.register_profile( + "exhaustive", + max_examples=10_000, + suppress_health_check=[HealthCheck.too_slow], +) +settings.register_profile( + "coverage", + max_examples=10, + suppress_health_check=[HealthCheck.too_slow], +) +settings.register_profile("ci", suppress_health_check=[HealthCheck.too_slow]) diff --git a/tests/containers_test.py b/tests/containers_test.py index 645c79d3..c7890253 100644 --- a/tests/containers_test.py +++ b/tests/containers_test.py @@ -17,7 +17,12 @@ """Test the kml classes.""" +import pytest + +from fastkml import containers +from fastkml import features from fastkml import kml +from fastkml import styles from tests.base import Lxml from tests.base import StdLibrary @@ -34,7 +39,7 @@ def test_document_boolean_visibility(self) -> None: """ - k = kml.KML.class_from_string(doc, strict=False) + k = kml.KML.from_string(doc, strict=False) assert k.features[0].visibility assert k.features[0].isopen @@ -47,7 +52,7 @@ def test_document_boolean_open(self) -> None: """ - k = kml.KML.class_from_string(doc, strict=False) + k = kml.KML.from_string(doc, strict=False) assert k.features[0].visibility == 0 assert k.features[0].isopen is False @@ -60,11 +65,96 @@ def test_document_boolean_visibility_invalid(self) -> None: """ - d = kml.KML.class_from_string(doc, strict=False) + d = kml.KML.from_string(doc, strict=False) assert d.features[0].visibility is None assert d.features[0].isopen + def test_container_creation(self) -> None: + container = containers._Container( + ns="ns", + id="id", + target_id="target_id", + name="name", + ) + assert container.ns == "ns" + assert container.name == "name" + + def test_container_feature_append(self) -> None: + container = containers._Container( + ns="ns", + id="id", + target_id="target_id", + name="name", + ) + feature = features._Feature(name="new_feature") + container.append(feature) + assert feature in container.features + with pytest.raises(ValueError, match="Cannot append self"): + container.append(container) + + def test_document_container_get_style_url(self) -> None: + document = containers.Document( + name="Document", + ns="ns", + style_url=styles.StyleUrl(url="www.styleurl.com"), + ) + assert document.get_style_by_url(style_url="www.styleurl.com") is None + + def test_document_container_get_style_url_id(self) -> None: + style = styles.Style(id="style-0") + document = containers.Document( + name="Document", + ns="ns", + styles=[style], + ) + assert document.get_style_by_url(style_url="#style-0") == style + + def test_get_style_by_url(self) -> None: + doc = """ + + Document.kml + 1 + + + + normal + #normalState + + + highlight + #highlightState + + + + + """ + k = kml.KML.from_string(doc) + assert len(k.features) == 1 + document = k.features[0] + + style0 = document.get_style_by_url( + "http://localhost:8080/somepath#exampleStyleDocument", + ) + style1 = document.get_style_by_url("somepath#linestyleExample") + style2 = document.get_style_by_url("#styleMapExample") + + assert isinstance(style0.styles[0], styles.LabelStyle) + assert style0.id == "exampleStyleDocument" + assert isinstance(style1.styles[0], styles.LineStyle) + assert style1.id == "linestyleExample" + assert isinstance(style2, styles.StyleMap) + assert style2.id == "styleMapExample" + class TestLxml(Lxml, TestStdLibrary): """Test with lxml.""" diff --git a/tests/data_test.py b/tests/data_test.py index f820eb04..b142aa95 100644 --- a/tests/data_test.py +++ b/tests/data_test.py @@ -65,7 +65,7 @@ def test_schema_from_string(self) -> None: """ - s = kml.Schema.class_from_string(doc, ns=None) + s = kml.Schema.from_string(doc, ns=None) assert len(s.fields) == 3 assert s.fields[0].type == DataType("string") @@ -77,7 +77,7 @@ def test_schema_from_string(self) -> None: assert s.fields[0].display_name == "Trail Head Name" assert s.fields[1].display_name == "The length in miles" assert s.fields[2].display_name == "change in altitude" - s1 = kml.Schema.class_from_string(s.to_string(), ns=None) + s1 = kml.Schema.from_string(s.to_string(), ns=None) assert len(s1.fields) == 3 assert s1.fields[0].type == DataType("string") assert s1.fields[1].name == "TrailLength" @@ -87,11 +87,11 @@ def test_schema_from_string(self) -> None: '' f"{doc}" ) - k = kml.KML.class_from_string(doc1, ns=None) + k = kml.KML.from_string(doc1, ns=None) d = k.features[0] s2 = d.schemata[0] assert s.to_string() == s2.to_string() - k1 = kml.KML.class_from_string(k.to_string()) + k1 = kml.KML.from_string(k.to_string()) assert "Schema" in k1.to_string() assert "SimpleField" in k1.to_string() assert k1.to_string().replace("kml:", "").replace( @@ -145,7 +145,7 @@ def test_untyped_extended_data(self) -> None: assert len(p.extended_data.elements) == 2 k.append(p) - k2 = kml.KML.class_from_string(k.to_string()) + k2 = kml.KML.from_string(k.to_string()) extended_data = k2.features[0].extended_data assert extended_data is not None @@ -176,7 +176,7 @@ def test_untyped_extended_data_nested(self) -> None: k.append(d) d.append(f) - k2 = kml.KML.class_from_string(k.to_string()) + k2 = kml.KML.from_string(k.to_string()) document_data = k2.features[0].extended_data folder_data = k2.features[0].features[0].extended_data @@ -217,7 +217,7 @@ def test_extended_data(self) -> None: """ - k = kml.KML.class_from_string(doc) + k = kml.KML.from_string(doc) extended_data = k.features[0].extended_data @@ -248,7 +248,7 @@ def test_schema_data_from_str(self) -> None: 10 """ - sd = data.SchemaData.class_from_string(doc) + sd = data.SchemaData.from_string(doc) assert sd.schema_url == "#TrailHeadTypeId" assert sd.data[0].name == "TrailHeadName" assert sd.data[0].value == "Pi in the sky" @@ -256,7 +256,7 @@ def test_schema_data_from_str(self) -> None: assert sd.data[1].value == "3.14159" assert sd.data[2].name == "ElevationGain" assert sd.data[2].value == "10" - sd1 = data.SchemaData.class_from_string(sd.to_string()) + sd1 = data.SchemaData.from_string(sd.to_string()) assert sd1.schema_url == "#TrailHeadTypeId" assert sd.to_string() == sd1.to_string() @@ -268,12 +268,12 @@ def test_data_from_string(self) -> None: 1 """ - d = data.Data.class_from_string(doc) + d = data.Data.from_string(doc) assert d.name == "holeNumber" assert d.value == "1" assert isinstance(d.display_name, str) assert "This is hole " in d.display_name - d1 = data.Data.class_from_string(d.to_string()) + d1 = data.Data.from_string(d.to_string()) assert d1.name == "holeNumber" assert d.to_string() == d1.to_string() diff --git a/tests/features_test.py b/tests/features_test.py index 07d2690d..8384ae5d 100644 --- a/tests/features_test.py +++ b/tests/features_test.py @@ -71,11 +71,14 @@ def test_placemark_geometry_and_kml_geometry_parameter_set(self) -> None: pt = geo.Point(10, 20) point = geometry.Point(geometry=pt) - with pytest.raises(ValueError): + with pytest.raises( + ValueError, + match="^You can only specify one of kml_geometry or geometry$", + ): features.Placemark(geometry=pt, kml_geometry=point) def test_network_link_with_link_parameter_only(self) -> None: - """NetworkLink object with Link parameter only""" + """Test NetworkLink object with Link parameter only.""" network_link = features.NetworkLink( link=links.Link(href="http://example.com/kml_file.kml"), ) @@ -155,7 +158,7 @@ def test_network_link_read(self) -> None: "" ) - network_link = features.NetworkLink.class_from_string(doc) + network_link = features.NetworkLink.from_string(doc) assert network_link.name == "My NetworkLink" assert network_link.visibility @@ -170,6 +173,7 @@ def test_network_link_read(self) -> None: assert network_link.address == "123 Main St" assert network_link.phone_number == "555-1234" assert network_link.snippet.text == "This is a snippet" + assert bool(network_link.snippet) assert network_link.description == "This is a description" assert network_link.view.latitude == 37.0 assert network_link.view.longitude == -122.0 diff --git a/tests/geometries/boundaries_test.py b/tests/geometries/boundaries_test.py index 4b413f08..8e422a68 100644 --- a/tests/geometries/boundaries_test.py +++ b/tests/geometries/boundaries_test.py @@ -16,8 +16,14 @@ """Test the Outer and Inner Boundary classes.""" +from typing import Type +from typing import Union + import pygeoif.geometry as geo +import pytest +import fastkml +from fastkml.exceptions import GeometryError from fastkml.geometry import Coordinates from fastkml.geometry import InnerBoundaryIs from fastkml.geometry import LinearRing @@ -35,7 +41,7 @@ def test_outer_boundary(self) -> None: ) assert outer_boundary.geometry == geo.LinearRing(coords) - assert outer_boundary.to_string(prettyprint=False).strip() == ( + assert outer_boundary.to_string(prettyprint=False, precision=6).strip() == ( '' "" "1.000000,2.000000 2.000000,0.000000 0.000000,0.000000 1.000000,2.000000" @@ -44,7 +50,7 @@ def test_outer_boundary(self) -> None: def test_read_outer_boundary(self) -> None: """Test the from_string method.""" - outer_boundary = OuterBoundaryIs.class_from_string( + outer_boundary = OuterBoundaryIs.from_string( '' "" "1.0,4.0 2.0,0.0 0.0,0.0 1.0,4.0" @@ -66,13 +72,34 @@ def test_inner_boundary(self) -> None: assert inner_boundary.geometry == geo.LinearRing(coords) assert bool(inner_boundary) - assert inner_boundary.to_string(prettyprint=False).strip() == ( + assert inner_boundary.to_string(prettyprint=False, precision=6).strip() == ( '' "" "1.000000,2.000000 2.000000,0.000000 0.000000,0.000000 1.000000,2.000000" "" ) + def _test_boundary_geometry_error( + self, + boundary_class: Union[Type[InnerBoundaryIs], Type[OuterBoundaryIs]], + ) -> None: + p = geo.LinearRing(((1, 2), (2, 0))) + coords = ((1, 2), (2, 0), (0, 0), (1, 2)) + + with pytest.raises(GeometryError): + boundary_class( + kml_geometry=LinearRing(kml_coordinates=Coordinates(coords=coords)), + geometry=p, + ) + + def test_outer_boundary_geometry_error(self) -> None: + """Test that OuterBoundaryIs raises GeometryError with invalid geometry.""" + self._test_boundary_geometry_error(OuterBoundaryIs) + + def test_inner_boundary_geometry_error(self) -> None: + """Test that InnerBoundaryIs raises GeometryError with invalid geometry.""" + self._test_boundary_geometry_error(InnerBoundaryIs) + def test_read_inner_boundary_multiple_linestrings(self) -> None: """ Test the from_string method. @@ -80,7 +107,7 @@ def test_read_inner_boundary_multiple_linestrings(self) -> None: When there are multiple LinearRings in the innerBoundaryIs element only the first one is used. """ - inner_boundary = InnerBoundaryIs.class_from_string( + inner_boundary = InnerBoundaryIs.from_string( '' "" "1.0,4.0 2.0,0.0 0.0,0.0 1.0,4.0" @@ -96,6 +123,22 @@ def test_read_inner_boundary_multiple_linestrings(self) -> None: ((1, 4), (2, 0), (0, 0), (1, 4)), ) + def test_inner_boundary_repr_roundtrip(self) -> None: + """Test that repr(obj) can be eval'd back to obj.""" + coords = ((1, 2), (2, 0), (0, 0), (1, 2)) + inner_boundary = InnerBoundaryIs( + kml_geometry=LinearRing(kml_coordinates=Coordinates(coords=coords)), + ) + + assert inner_boundary == eval( # noqa: S307 + repr(inner_boundary), + {}, + { + "fastkml": fastkml, + "LinearRing": geo.LinearRing, + }, + ) + class TestBoundariesLxml(Lxml, TestBoundaries): pass diff --git a/tests/geometries/coordinates_test.py b/tests/geometries/coordinates_test.py index 9b8074e1..745186e2 100644 --- a/tests/geometries/coordinates_test.py +++ b/tests/geometries/coordinates_test.py @@ -29,7 +29,7 @@ def test_coordinates(self) -> None: coordinates = Coordinates(coords=coords) - assert coordinates.to_string().strip() == ( + assert coordinates.to_string(precision=6).strip() == ( '' "0.000000,0.000000 0.000000,1.000000 1.000000,1.000000 " "1.000000,0.000000 0.000000,0.000000" @@ -38,7 +38,7 @@ def test_coordinates(self) -> None: def test_coordinates_from_string(self) -> None: """Test the from_string method.""" - coordinates = Coordinates.class_from_string( + coordinates = Coordinates.from_string( '' "0.000000,0.000000 1.000000,0.000000 1.0,1.0 0.000000,0.000000" "", @@ -48,7 +48,7 @@ def test_coordinates_from_string(self) -> None: def test_coordinates_from_string_with_whitespace(self) -> None: """Test the from_string method with whitespace.""" - coordinates = Coordinates.class_from_string( + coordinates = Coordinates.from_string( '\n' "-123.9404499372,49.169275246690,17 -123.940493701601,49.1694596207446,17 " "-123.940356261489,49.16947180231761,17 -123.940306243,49.169291706171,17 " diff --git a/tests/geometries/functions_test.py b/tests/geometries/functions_test.py new file mode 100644 index 00000000..ae89da26 --- /dev/null +++ b/tests/geometries/functions_test.py @@ -0,0 +1,88 @@ +# Copyright (C) 2024 Rishit Chaudhary, 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 error handling.""" +from typing import Callable +from unittest.mock import Mock +from unittest.mock import patch + +import pytest + +from fastkml.enums import Verbosity +from fastkml.exceptions import KMLParseError +from fastkml.exceptions import KMLWriteError +from fastkml.geometry import coordinates_subelement +from fastkml.geometry import handle_invalid_geometry_error +from tests.base import StdLibrary + + +class TestGeometryFunctions(StdLibrary): + """Test functions in Geometry.""" + + @patch("fastkml.config.etree.tostring", return_value=b"") + def test_handle_invalid_geometry_error_true( + self, + mock_to_string: Callable[..., str], + ) -> None: + mock_element = Mock() + with pytest.raises( + KMLParseError, + match=mock_to_string.return_value.decode(), # type: ignore[attr-defined] + ): + handle_invalid_geometry_error( + error=ValueError(), + element=mock_element, + strict=True, + ) + mock_to_string.assert_called_once_with( # type: ignore[attr-defined] + mock_element, + encoding="UTF-8", + ) + + @patch("fastkml.config.etree.tostring", return_value=b"") + def test_handle_invalid_geometry_error_false( + self, + mock_to_string: Callable[..., str], + ) -> None: + mock_element = Mock() + handle_invalid_geometry_error( + error=ValueError(), + element=mock_element, + strict=False, + ) + mock_to_string.assert_called_once_with( # type: ignore[attr-defined] + mock_element, + encoding="UTF-8", + ) + + def test_coordinates_subelement_exception(self) -> None: + obj = Mock() + obj.coordinates = [(1.123456, 2.654321, 3.111111, 4.222222)] + + element = Mock() + + precision = 9 + attr_name = "coordinates" + + with pytest.raises(KMLWriteError): + coordinates_subelement( + obj=obj, + attr_name=attr_name, + node_name="", + element=element, + precision=precision, + verbosity=Verbosity.terse, + default=None, + ) diff --git a/tests/geometries/geometry_test.py b/tests/geometries/geometry_test.py index 7e96699f..224b9a1c 100644 --- a/tests/geometries/geometry_test.py +++ b/tests/geometries/geometry_test.py @@ -1,4 +1,4 @@ -# Copyright (C) 2021 - 2023 Christian Ledermann +# Copyright (C) 2021 - 2024 Christian Ledermann # # This library is free software; you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free @@ -18,7 +18,6 @@ import pytest from pygeoif import geometry as geo -from fastkml import exceptions from fastkml.enums import AltitudeMode from fastkml.geometry import LinearRing from fastkml.geometry import LineString @@ -38,7 +37,7 @@ def test_altitude_mode(self) -> None: clampToGround """ - g = Point.class_from_string(doc) + g = Point.from_string(doc) assert g.altitude_mode == AltitudeMode("clampToGround") @@ -50,7 +49,7 @@ def test_gx_altitude_mode(self) -> None: "clampToSeaFloor" "" ) - g = Point.class_from_string(doc) + g = Point.from_string(doc) assert g.altitude_mode == AltitudeMode("clampToSeaFloor") @@ -60,7 +59,7 @@ def test_extrude(self) -> None: 1 """ - g = Point.class_from_string(doc) + g = Point.from_string(doc) assert g.extrude is True @@ -70,17 +69,18 @@ def test_tessellate(self) -> None: 1 """ - g = Point.class_from_string(doc) + g = Point.from_string(doc) - assert g.tessellate is True + assert not hasattr(g, "tessellate") def test_point(self) -> None: doc = """ 0.000000,1.000000 """ - g = Point.class_from_string(doc) + g = Point.from_string(doc) + assert g.geometry assert g.geometry.__geo_interface__ == { "type": "Point", "bbox": (0.0, 1.0, 0.0, 1.0), @@ -92,8 +92,9 @@ def test_linestring(self) -> None: 0.000000,0.000000 1.000000,1.000000 """ - g = LineString.class_from_string(doc) + g = LineString.from_string(doc) + assert g.geometry assert g.geometry.__geo_interface__ == { "type": "LineString", "bbox": (0.0, 0.0, 1.0, 1.0), @@ -107,8 +108,9 @@ def test_linearring(self) -> None: """ - g = LinearRing.class_from_string(doc) + g = LinearRing.from_string(doc) + assert g.geometry assert g.geometry.__geo_interface__ == { "type": "LinearRing", "bbox": (0.0, 0.0, 1.0, 1.0), @@ -126,8 +128,9 @@ def test_polygon(self) -> None: """ - g = Polygon.class_from_string(doc) + g = Polygon.from_string(doc) + assert g.geometry assert g.geometry.__geo_interface__ == { "type": "Polygon", "bbox": (0.0, 0.0, 1.0, 1.0), @@ -151,8 +154,9 @@ def test_polygon_with_inner_boundary(self) -> None: """ - g = Polygon.class_from_string(doc) + g = Polygon.from_string(doc) + assert g.geometry assert g.geometry.__geo_interface__ == { "type": "Polygon", "bbox": (-1.0, -1.0, 2.0, 2.0), @@ -174,7 +178,7 @@ def test_multipoint(self) -> None: """ - g = MultiGeometry.class_from_string(doc) + g = MultiGeometry.from_string(doc) assert len(g.geometry) == 2 # type: ignore[arg-type] @@ -190,7 +194,7 @@ def test_multilinestring(self) -> None: """ - g = MultiGeometry.class_from_string(doc) + g = MultiGeometry.from_string(doc) assert len(g.geometry) == 2 # type: ignore[arg-type] @@ -222,7 +226,7 @@ def test_multipolygon(self) -> None: """ - g = MultiGeometry.class_from_string(doc) + g = MultiGeometry.from_string(doc) assert g.geometry is not None assert len(g.geometry) == 2 @@ -249,7 +253,7 @@ def test_geometrycollection(self) -> None: """ - g = MultiGeometry.class_from_string(doc) + g = MultiGeometry.from_string(doc) assert len(g.geometry) == 4 # type: ignore[arg-type] @@ -265,9 +269,10 @@ def test_geometrycollection_with_linearring(self) -> None: """ - g = MultiGeometry.class_from_string(doc) + g = MultiGeometry.from_string(doc) - assert len(g.geometry) == 2 # type: ignore[arg-type] + assert g.geometry + assert len(g.geometry) == 2 assert g.geometry.geom_type == "GeometryCollection" @@ -281,9 +286,7 @@ def test_init(self) -> None: assert g.ns == "{http://www.opengis.net/kml/2.2}" 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_init_with_args(self) -> None: """Test the init method with arguments.""" @@ -291,17 +294,13 @@ def test_init_with_args(self) -> None: 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.""" @@ -328,13 +327,13 @@ def test_to_string_with_args(self) -> None: 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 "extrude" not in g.to_string() + assert "altitudeMode>relativeToGround<" not in g.to_string() + assert "tessellate" not in g.to_string() def test_from_string(self) -> None: """Test the from_string method.""" - g = _Geometry.class_from_string( + g = _Geometry.from_string( '<_Geometry id="my-id" targetId="target_id" ' 'xmlns="http://www.opengis.net/kml/2.2">' "1" @@ -346,61 +345,23 @@ def test_from_string(self) -> None: 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 - - def test_from_string_invalid_altitude_mode_strict(self) -> None: - """Test the from_string method.""" - with pytest.raises( - exceptions.KMLParseError, - ): - _Geometry.class_from_string( - '<_Geometry id="my-id" targetId="target_id" ' - 'xmlns="http://www.opengis.net/kml/2.2">' - "invalid" - "", - ) - - def test_from_string_invalid_altitude_mode_relaxed(self) -> None: - """Test the from_string method.""" - geom = _Geometry.class_from_string( - '<_Geometry id="my-id" targetId="target_id" ' - 'xmlns="http://www.opengis.net/kml/2.2">' - "invalid" - "", - strict=False, - ) - - assert geom.altitude_mode is None - - def test_from_string_invalid_extrude(self) -> None: - """Test the from_string method.""" - with pytest.raises( - exceptions.KMLParseError, - ): - _Geometry.class_from_string( - '<_Geometry id="my-id" targetId="target_id" ' - 'xmlns="http://www.opengis.net/kml/2.2">' - "invalid" - "", - ) + assert g.altitude_mode is None + assert not hasattr(g, "tessellate") + assert not hasattr(g, "extrude") def test_from_minimal_string(self) -> None: - g = _Geometry.class_from_string( + g = _Geometry.from_string( '<_Geometry xmlns="http://www.opengis.net/kml/2.2/" />', ) assert g.ns == "{http://www.opengis.net/kml/2.2}" 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( + g = _Geometry.from_string( '' "1" @@ -412,9 +373,9 @@ def test_from_string_omitting_ns(self) -> None: 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 + assert g.altitude_mode is None + assert not hasattr(g, "tessellate") + assert not hasattr(g, "extrude") class TestCreateKmlGeometry(StdLibrary): @@ -423,32 +384,37 @@ def test_create_kml_geometry_point(self) -> None: g = create_kml_geometry(geo.Point(0, 1)) assert isinstance(g, Point) + assert g.geometry assert g.geometry.__geo_interface__ == { "type": "Point", "bbox": (0.0, 1.0, 0.0, 1.0), "coordinates": (0.0, 1.0), } assert "Point>" in g.to_string() - assert "coordinates>0.000000,1.0000000.000000,1.000000 None: """Test the create_kml_geometry function.""" g = create_kml_geometry(geo.LineString([(0, 0), (1, 1)])) assert isinstance(g, LineString) + assert g.geometry 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)), } assert "LineString>" in g.to_string() - assert "coordinates>0.000000,0.000000 1.000000,1.0000000.000000,0.000000 1.000000,1.000000 None: """Test the create_kml_geometry function.""" g = create_kml_geometry(geo.LinearRing([(0, 0), (1, 1), (1, 0), (0, 0)])) assert isinstance(g, LinearRing) + assert g.geometry assert g.geometry.__geo_interface__ == { "type": "LinearRing", "bbox": (0.0, 0.0, 1.0, 1.0), @@ -458,14 +424,14 @@ def test_create_kml_geometry_linearring(self) -> None: assert ( "coordinates>0.000000,0.000000 1.000000,1.000000 1.000000,0.000000 " "0.000000,0.000000 None: """Test the create_kml_geometry function.""" g = create_kml_geometry(geo.Polygon([(0, 0), (1, 1), (1, 0), (0, 0)])) assert isinstance(g, Polygon) - assert g.geometry is not None + assert g.geometry assert g.geometry.__geo_interface__ == { "type": "Polygon", "bbox": (0.0, 0.0, 1.0, 1.0), @@ -475,21 +441,22 @@ def test_create_kml_geometry_polygon(self) -> None: assert ( "coordinates>0.000000,0.000000 1.000000,1.000000 1.000000,0.000000 " "0.000000,0.000000 None: """Test the create_kml_geometry function.""" g = create_kml_geometry(geo.MultiPoint([(0, 0), (1, 1), (1, 0), (2, 2)])) assert isinstance(g, MultiGeometry) - assert g.geometry is not None + assert g.geometry assert len(g.geometry) == 4 - assert "MultiGeometry>" in g.to_string() - assert "Point>" in g.to_string() - assert "coordinates>0.000000,0.0000001.000000,1.0000001.000000,0.0000002.000000,2.000000" in xml + assert "Point>" in xml + assert "coordinates>0.000000,0.0000001.000000,1.0000001.000000,0.0000002.000000,2.000000 None: """Test the create_kml_geometry function.""" @@ -498,12 +465,13 @@ def test_create_kml_geometry_multilinestring(self) -> None: ) assert isinstance(g, MultiGeometry) - assert g.geometry is not None + assert g.geometry assert len(g.geometry) == 2 - assert "MultiGeometry>" in g.to_string() - assert "LineString>" in g.to_string() - assert "coordinates>0.000000,0.000000 1.000000,1.0000000.000000,0.000000 1.000000,1.000000" in xml + assert "LineString>" in xml + assert "coordinates>0.000000,0.000000 1.000000,1.0000000.000000,0.000000 1.000000,1.000000 None: """Test the create_kml_geometry function.""" @@ -520,22 +488,23 @@ def test_create_kml_geometry_multipolygon(self) -> None: ) assert isinstance(g, MultiGeometry) - assert g.geometry is not None + assert g.geometry assert len(g.geometry) == 2 - assert "MultiGeometry>" in g.to_string() - assert "Polygon>" in g.to_string() + xml = g.to_string(precision=6) + assert "MultiGeometry>" in xml + assert "Polygon>" in xml 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,1.000000 1.000000,1.000000 " "1.000000,0.000000 0.000000,0.000000 None: multipoint = geo.MultiPoint([(0, 0), (1, 1), (1, 2), (2, 2)]) @@ -555,16 +524,17 @@ def test_create_kml_geometry_geometrycollection(self) -> None: g = create_kml_geometry(gc) assert isinstance(g, MultiGeometry) - assert g.geometry is not None + assert g.geometry assert len(g.geometry) == 7 - assert "MultiGeometry>" in g.to_string() - assert "LineString>" in g.to_string() - assert "LinearRing>" in g.to_string() - assert "Polygon>" in g.to_string() - assert "outerBoundaryIs>" in g.to_string() - assert "innerBoundaryIs>" in g.to_string() - assert "Point>" in g.to_string() - assert "coordinates>0.000000,0.000000" in xml + assert "LineString>" in xml + assert "LinearRing>" in xml + assert "Polygon>" in xml + assert "outerBoundaryIs>" in xml + assert "innerBoundaryIs>" in xml + assert "Point>" in xml + assert "coordinates>0.000000,0.000000 None: @@ -593,7 +563,7 @@ def test_create_kml_geometry_geometrycollection_roundtrip(self) -> None: ) g = create_kml_geometry(gc) - mg = MultiGeometry.class_from_string(g.to_string()) + mg = MultiGeometry.from_string(g.to_string()) assert mg.geometry == gc diff --git a/tests/geometries/linearring_test.py b/tests/geometries/linearring_test.py index bd3ff0a1..f6a91057 100644 --- a/tests/geometries/linearring_test.py +++ b/tests/geometries/linearring_test.py @@ -45,12 +45,13 @@ def test_to_string(self) -> None: 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 = LinearRing.class_from_string( + linear_ring = LinearRing.from_string( '' "0.000000,0.000000 1.000000,0.000000 1.0,1.0 " "0.000000,0.000000" @@ -61,7 +62,7 @@ def test_from_string(self) -> None: def test_empty_from_string(self) -> None: """Test the from_string method with an empty LinearRing.""" - linear_ring = LinearRing.class_from_string( + linear_ring = LinearRing.from_string( '' "" "", @@ -71,7 +72,7 @@ def test_empty_from_string(self) -> None: def test_no_coordinates_from_string(self) -> None: """Test the from_string method with an empty LinearRing.""" - linear_ring = LinearRing.class_from_string( + linear_ring = LinearRing.from_string( '' "", ) @@ -84,7 +85,7 @@ def test_from_string_invalid_coordinates_non_numerical(self) -> None: KMLParseError, match=r"^Invalid coordinates in", ): - LinearRing.class_from_string( + LinearRing.from_string( '' "0.000000,0.000000 1.000000,0.000000 1.0,1.0 " "0.000000,0.000000 1.000000,a" @@ -93,7 +94,7 @@ def test_from_string_invalid_coordinates_non_numerical(self) -> None: def test_mixed_2d_3d_coordinates_from_string_relaxed(self) -> None: """Test the from_string method with mixed 2D and 3D coordinates.""" - linear_ring = LinearRing.class_from_string( + linear_ring = LinearRing.from_string( '' "0.000000,0.000000 1.000000,0.000000 1.0,1.0 " "0.000000,0.000000 1.000000,2.000000,3.000000" diff --git a/tests/geometries/linestring_test.py b/tests/geometries/linestring_test.py index bfc773f8..13e60b76 100644 --- a/tests/geometries/linestring_test.py +++ b/tests/geometries/linestring_test.py @@ -19,7 +19,11 @@ import pygeoif.geometry as geo import pytest +from fastkml import exceptions +from fastkml.enums import Verbosity +from fastkml.exceptions import GeometryError from fastkml.exceptions import KMLParseError +from fastkml.geometry import Coordinates from fastkml.geometry import LineString from tests.base import Lxml from tests.base import StdLibrary @@ -36,6 +40,14 @@ def test_init(self) -> None: assert line_string.altitude_mode is None assert line_string.extrude is None + def test_geometry_error(self) -> None: + """Test GeometryError.""" + p = geo.LineString(((1, 2), (2, 0))) + q = Coordinates(ns="ns") + + with pytest.raises(GeometryError): + LineString(geometry=p, kml_coordinates=q) + def test_to_string(self) -> None: """Test the to_string method.""" ls = geo.LineString(((1, 2), (2, 0))) @@ -45,12 +57,12 @@ def test_to_string(self) -> None: assert "LineString" in line_string.to_string() assert ( "coordinates>1.000000,2.000000 2.000000,0.000000 None: """Test the from_string method.""" - linestring = LineString.class_from_string( + linestring = LineString.from_string( '' "1" "1" @@ -65,8 +77,7 @@ def test_from_string(self) -> None: ) def test_mixed_2d_3d_coordinates_from_string(self) -> None: - - linestring = LineString.class_from_string( + linestring = LineString.from_string( '' "1" "1" @@ -79,7 +90,7 @@ def test_mixed_2d_3d_coordinates_from_string(self) -> None: assert not linestring def test_mixed_2d_3d_coordinates_from_string_relaxed(self) -> None: - line_string = LineString.class_from_string( + line_string = LineString.from_string( '' "1" "1" @@ -94,7 +105,7 @@ def test_mixed_2d_3d_coordinates_from_string_relaxed(self) -> None: def test_empty_from_string(self) -> None: """Test the from_string method with an empty LineString.""" - linestring = LineString.class_from_string( + linestring = LineString.from_string( '' "1" "1" @@ -107,7 +118,7 @@ def test_empty_from_string(self) -> None: def test_no_coordinates_from_string(self) -> None: """Test the from_string method with no coordinates.""" - linestring = LineString.class_from_string( + linestring = LineString.from_string( '' "1" "1" @@ -122,7 +133,7 @@ def test_from_string_invalid_coordinates_non_numerical(self) -> None: KMLParseError, match=r"^Invalid coordinates in", ): - LineString.class_from_string( + LineString.from_string( '' "1" "1" @@ -133,7 +144,7 @@ def test_from_string_invalid_coordinates_non_numerical(self) -> None: ) def test_from_string_invalid_coordinates_nan(self) -> None: - line_string = LineString.class_from_string( + line_string = LineString.from_string( '' "false" "true" @@ -145,9 +156,79 @@ def test_from_string_invalid_coordinates_nan(self) -> None: "", ) + assert line_string.geometry assert len(line_string.geometry.coords) == 5 assert line_string.to_string() + def test_from_string_invalid_extrude(self) -> None: + """Test the from_string method.""" + with pytest.raises( + exceptions.KMLParseError, + ): + LineString.from_string( + '' + "invalid" + "", + ) + + def test_from_string_invalid_tessellate(self) -> None: + """Test the from_string method.""" + with pytest.raises( + exceptions.KMLParseError, + ): + LineString.from_string( + '' + "invalid" + "", + ) + + def test_to_string_terse_default(self) -> None: + ls = geo.LineString(((1, 2), (2, 0))) + line_string = LineString(geometry=ls, extrude=False, tessellate=False) + + xml = line_string.to_string(verbosity=Verbosity.terse) + + assert "tessellate" not in xml + assert "extrude" not in xml + + def test_to_string_terse(self) -> None: + ls = geo.LineString(((1, 2), (2, 0))) + line_string = LineString(geometry=ls, extrude=True, tessellate=True) + + xml = line_string.to_string(verbosity=Verbosity.terse) + + assert "tessellate>11 None: + ls = geo.LineString(((1, 2), (2, 0))) + line_string = LineString(geometry=ls, extrude=False, tessellate=False) + + xml = line_string.to_string(verbosity=Verbosity.verbose) + + assert "tessellate>00 None: + ls = geo.LineString(((1, 2), (2, 0))) + line_string = LineString(geometry=ls, extrude=True, tessellate=True) + + xml = line_string.to_string(verbosity=Verbosity.verbose) + + assert "tessellate>11 None: + ls = geo.LineString(((1, 2), (2, 0))) + line_string = LineString(geometry=ls) + + xml = line_string.to_string(verbosity=Verbosity.verbose) + + assert "tessellate>00 None: mg = MultiGeometry(geometry=p) - assert "coordinates>1.000000,2.0000001.000000,2.000000" in mg.to_string() assert "Point>" in mg.to_string() @@ -41,8 +44,8 @@ def test_2_points(self) -> None: mg = MultiGeometry(geometry=p) - assert "coordinates>1.000000,2.0000003.000000,4.0000001.000000,2.0000003.000000,4.000000" in mg.to_string() assert "Point>" in mg.to_string() @@ -55,7 +58,7 @@ def test_2_points_read(self) -> None: "" ) - mg = MultiGeometry.class_from_string(xml) + mg = MultiGeometry.from_string(xml) assert mg.geometry == geo.MultiPoint([(1, 2), (3, 4)]) @@ -67,7 +70,9 @@ def test_1_linestring(self) -> None: mg = MultiGeometry(geometry=p) - assert "coordinates>1.000000,2.000000 3.000000,4.0000001.000000,2.000000 3.000000,4.000000" in mg.to_string() assert "LineString>" in mg.to_string() @@ -77,8 +82,12 @@ def test_2_linestrings(self) -> None: mg = MultiGeometry(geometry=p) - assert "coordinates>1.000000,2.000000 3.000000,4.0000005.000000,6.000000 7.000000,8.0000001.000000,2.000000 3.000000,4.0000005.000000,6.000000 7.000000,8.000000" in mg.to_string() assert "LineString>" in mg.to_string() @@ -92,7 +101,7 @@ def test_2_linestrings_read(self) -> None: "" ) - mg = MultiGeometry.class_from_string(xml, ns="") + mg = MultiGeometry.from_string(xml, ns="") assert mg.geometry == geo.MultiLineString([[(1, 2), (3, 4)], [(5, 6), (7, 8)]]) @@ -108,7 +117,7 @@ def test_1_polygon(self) -> None: 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() @@ -129,11 +138,11 @@ def test_1_polygons_with_holes(self) -> None: 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() @@ -156,15 +165,15 @@ def test_2_polygons(self) -> None: 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() @@ -189,7 +198,7 @@ def test_2_polygons_read(self) -> None: "" ) - mg = MultiGeometry.class_from_string(xml) + mg = MultiGeometry.from_string(xml) assert mg.geometry == geo.MultiPolygon( [ @@ -211,7 +220,7 @@ def test_1_point(self) -> None: mg = MultiGeometry(geometry=p) - assert "coordinates>1.000000,2.0000001.000000,2.000000" in mg.to_string() assert "Point>" in mg.to_string() @@ -261,6 +270,41 @@ def test_multi_geometries(self) -> None: assert "Polygon>" in mg.to_string() assert "MultiGeometry>" in mg.to_string() + def test_multi_geometries_verbose(self) -> None: + 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) + + xml = mg.to_string(verbosity=Verbosity.verbose) + assert xml.count("tessellate>0<") == 12 # points do not have tessellate + assert xml.count("extrude>0<") == 13 + assert xml.count("altitudeMode") == 26 + assert xml.count(">clampToGround<") == 13 + + def test_geometry_error(self) -> None: + """Test GeometryError.""" + p = geo.MultiPoint(((1.0, 2.0),)) + + with pytest.raises(GeometryError): + MultiGeometry(geometry=p, kml_geometries=(MultiGeometry(geometry=p),)) + def test_multi_geometries_read(self) -> None: xml = ( '' @@ -292,7 +336,7 @@ def test_multi_geometries_read(self) -> None: "" ) - mg = MultiGeometry.class_from_string(xml) + mg = MultiGeometry.from_string(xml) assert mg.geometry == geo.GeometryCollection( ( @@ -365,13 +409,13 @@ def test_empty_multi_geometries_read(self) -> None: "" ) - mg = MultiGeometry.class_from_string(xml) + mg = MultiGeometry.from_string(xml) assert mg.geometry is None - assert "MultiGeometry>" in mg.to_string() - assert "coordinates>" not in mg.to_string() - assert mg.extrude is False - assert mg.tessellate is False + assert "MultiGeometry" in mg.to_string() + assert "coordinates" not in mg.to_string() + assert not hasattr(mg, "extrude") + assert not hasattr(mg, "tessellate") class TestMultiPointLxml(Lxml, TestMultiPointStdLibrary): diff --git a/tests/geometries/point_test.py b/tests/geometries/point_test.py index d0389647..32fb08df 100644 --- a/tests/geometries/point_test.py +++ b/tests/geometries/point_test.py @@ -19,7 +19,10 @@ import pygeoif.geometry as geo import pytest +from fastkml.enums import Verbosity +from fastkml.exceptions import GeometryError from fastkml.exceptions import KMLParseError +from fastkml.geometry import Coordinates from fastkml.geometry import Point from tests.base import Lxml from tests.base import StdLibrary @@ -38,6 +41,14 @@ def test_init(self) -> None: assert point.altitude_mode is None assert point.extrude is None + def test_geometry_error(self) -> None: + """Test GeometryError.""" + p = geo.Point(1, 2) + q = Coordinates(ns="ns") + + with pytest.raises(GeometryError): + Point(geometry=p, kml_coordinates=q) + def test_to_string_2d(self) -> None: """Test the to_string method.""" p = geo.Point(1, 2) @@ -45,7 +56,7 @@ def test_to_string_2d(self) -> None: point = Point(geometry=p) assert "Point" in point.to_string() - assert "coordinates>1.000000,2.0000001.000000,2.000000 None: """Test the to_string method.""" @@ -54,7 +65,54 @@ def test_to_string_3d(self) -> None: point = Point(geometry=p) assert "Point" in point.to_string() - assert "coordinates>1.000000,2.000000,3.0000001.000000,2.000000,3.000000 None: + """Test the to_string method, exclude default for extrude in terse mode.""" + p = geo.Point(1, 2) + + point = Point(geometry=p, extrude=False) + + assert "coordinates>" in point.to_string(verbosity=Verbosity.terse) + assert "extrude" not in point.to_string(verbosity=Verbosity.terse) + + def test_to_string_terse_non_default(self) -> None: + """Test the to_string method, include extrude when true in terse mode.""" + p = geo.Point(1, 2) + + point = Point(geometry=p, extrude=True) + + assert "coordinates>" in point.to_string(verbosity=Verbosity.terse) + assert "extrude>1 None: + """Test the to_string method, include default for extrude in verbose mode.""" + p = geo.Point(1, 2) + + point = Point(geometry=p, extrude=False) + + assert "coordinates>" in point.to_string(verbosity=Verbosity.verbose) + assert "extrude>0 None: + """Test the to_string method, include extrude when true in verbose mode.""" + p = geo.Point(1, 2) + + point = Point(geometry=p, extrude=True) + + assert "coordinates>" in point.to_string(verbosity=Verbosity.verbose) + assert "extrude>1 None: + """Test the to_string method, include extrude when true in verbose mode.""" + p = geo.Point(1, 2) + + point = Point(geometry=p, extrude=False) + + assert "coordinates>" in point.to_string(verbosity=Verbosity.verbose) + assert "extrude>0 None: """Test the to_string method.""" @@ -101,7 +159,7 @@ def test_to_string_empty_geometry(self) -> None: def test_from_string_2d(self) -> None: """Test the from_string method for a 2 dimensional point.""" - point = Point.class_from_string( + point = Point.from_string( '' "1.000000,2.000000" "", @@ -110,11 +168,10 @@ def test_from_string_2d(self) -> None: 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_from_string_uppercase_altitude_mode_relaxed(self) -> None: """Test the from_string method for an uppercase altitude mode.""" - point = Point.class_from_string( + point = Point.from_string( '' "RELATIVETOGROUND" "1.000000,2.000000" @@ -123,6 +180,7 @@ def test_from_string_uppercase_altitude_mode_relaxed(self) -> None: ) assert point.geometry == geo.Point(1, 2) + assert point.altitude_mode assert point.altitude_mode.value == "relativeToGround" def test_from_string_uppercase_altitude_mode_strict(self) -> None: @@ -131,16 +189,40 @@ def test_from_string_uppercase_altitude_mode_strict(self) -> None: KMLParseError, match=r"Value RELATIVETOGROUND is not a valid value for Enum AltitudeMode$", ): - assert Point.class_from_string( + assert Point.from_string( '' "RELATIVETOGROUND" "1.000000,2.000000" "", ) + def test_from_string_invalid_altitude_mode_strict(self) -> None: + with pytest.raises( + KMLParseError, + match=r"^Error parsing '<", + ): + assert Point.from_string( + '' + "INVALID" + "1.000000,2.000000" + "", + ) + + def test_from_string_invalid_altitude_mode_relaxed(self) -> None: + point = Point.from_string( + '' + "invalid" + "1.000000,2.000000" + "", + strict=False, + ) + + assert point.geometry == geo.Point(1, 2) + assert not point.altitude_mode + def test_from_string_3d(self) -> None: """Test the from_string method for a 3 dimensional point.""" - point = Point.class_from_string( + point = Point.from_string( '' "1" "1" @@ -150,13 +232,13 @@ def test_from_string_3d(self) -> None: ) assert point.geometry == geo.Point(1, 2, 3) + assert point.altitude_mode assert point.altitude_mode.value == "absolute" assert point.extrude - assert point.tessellate def test_empty_from_string(self) -> None: """Test the from_string method.""" - point = Point.class_from_string( + point = Point.from_string( "", ns="", ) @@ -165,7 +247,7 @@ def test_empty_from_string(self) -> None: def test_empty_from_string_relaxed(self) -> None: """Test that no error is raised when the geometry is empty and not strict.""" - point = Point.class_from_string( + point = Point.from_string( "", ns="", strict=False, @@ -174,7 +256,7 @@ def test_empty_from_string_relaxed(self) -> None: assert point.geometry is None def test_from_string_empty_coordinates(self) -> None: - point = Point.class_from_string( + point = Point.from_string( '', ) @@ -182,8 +264,7 @@ def test_from_string_empty_coordinates(self) -> None: assert point.geometry is None def test_from_string_invalid_coordinates(self) -> None: - - point = Point.class_from_string( + point = Point.from_string( '' "1", ) @@ -191,8 +272,7 @@ def test_from_string_invalid_coordinates(self) -> None: assert not point def test_from_string_invalid_coordinates_4d(self) -> None: - - point = Point.class_from_string( + point = Point.from_string( '' "1,2,3,4", ) @@ -203,21 +283,11 @@ def test_from_string_invalid_coordinates_non_numerical(self) -> None: KMLParseError, match=r"^Invalid coordinates in", ): - Point.class_from_string( + Point.from_string( '' "a,b,c", ) - def test_from_string_invalid_coordinates_nan(self) -> None: - with pytest.raises( - KMLParseError, - match=r"^Invalid coordinates in", - ): - Point.class_from_string( - '' - "a,b", - ) - class TestPointLxml(Lxml, TestPoint): """Test with lxml.""" diff --git a/tests/geometries/polygon_test.py b/tests/geometries/polygon_test.py index 13763494..ed9ee2e1 100644 --- a/tests/geometries/polygon_test.py +++ b/tests/geometries/polygon_test.py @@ -17,7 +17,11 @@ """Test the geometry classes.""" import pygeoif.geometry as geo +import pytest +from fastkml.enums import AltitudeMode +from fastkml.enums import Verbosity +from fastkml.exceptions import GeometryError from fastkml.geometry import OuterBoundaryIs from fastkml.geometry import Polygon from tests.base import Lxml @@ -39,7 +43,7 @@ def test_exterior_only(self) -> None: 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() + ) in polygon.to_string(precision=6) def test_exterior_interior(self) -> None: """Test exterior and interior.""" @@ -56,11 +60,83 @@ def test_exterior_interior(self) -> None: 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() + ) in polygon.to_string(precision=6) 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() + ) in polygon.to_string(precision=6) + + def test_exterior_interior_tessellate_extrude_altitude_mode(self) -> None: + """ + Test exterior and interior with tessellate, extrude and altitude mode. + + This should be set on the Polygon level, not on the LinearRing level. + """ + 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, + extrude=True, + tessellate=True, + altitude_mode=AltitudeMode.relative_to_ground, + ) + + xml = polygon.to_string() + assert xml.count("extrude>11relativeToGround None: + """Test the to_string method, exclude default for extrude in terse mode.""" + poly = geo.Polygon([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)]) + + polygon = Polygon(ns="", geometry=poly, extrude=False) + + assert "extrude>0 None: + """Test the to_string method, include extrude when true in terse mode.""" + poly = geo.Polygon([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)]) + + polygon = Polygon(ns="", geometry=poly, extrude=True) + + assert "extrude>1 None: + """Test the to_string method, include default for extrude in verbose mode.""" + poly = geo.Polygon([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)]) + + polygon = Polygon(ns="", geometry=poly, extrude=False) + + assert "extrude>0 None: + """Test the to_string method, include extrude when true in verbose mode.""" + poly = geo.Polygon([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)]) + + polygon = Polygon(ns="", geometry=poly, extrude=True) + + assert "extrude>1 None: + """Test the to_string method, include extrude when true in verbose mode.""" + poly = geo.Polygon([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)]) + + polygon = Polygon(ns="", geometry=poly) + + assert "extrude>0 None: + """Test GeometryError.""" + poly = geo.Polygon([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)]) + ob = OuterBoundaryIs(ns="") + + with pytest.raises(GeometryError): + Polygon(geometry=poly, outer_boundary=ob) def test_from_string_exterior_only(self) -> None: """Test exterior only.""" @@ -73,7 +149,7 @@ def test_from_string_exterior_only(self) -> None: """ - polygon2 = Polygon.class_from_string(doc) + polygon2 = Polygon.from_string(doc) assert polygon2.geometry == geo.Polygon([(0, 0), (1, 0), (1, 1), (0, 0)]) @@ -88,7 +164,7 @@ def test_from_string_interiors_only(self) -> None: """ - assert not Polygon.class_from_string(doc) + assert not Polygon.from_string(doc) def test_from_string_exterior_wo_linearring(self) -> None: """Test exterior when no LinearRing in outer boundary.""" @@ -99,7 +175,7 @@ def test_from_string_exterior_wo_linearring(self) -> None: """ - assert not Polygon.class_from_string(doc) + assert not Polygon.from_string(doc) def test_from_string_interior_wo_linearring(self) -> None: """Test interior when no LinearRing in inner boundary.""" @@ -116,7 +192,7 @@ def test_from_string_interior_wo_linearring(self) -> None: """ - poly = Polygon.class_from_string(doc) + poly = Polygon.from_string(doc) assert poly.geometry == geo.Polygon( ((0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 0.0)), @@ -139,7 +215,7 @@ def test_from_string_exterior_interior(self) -> None: """ - polygon = Polygon.class_from_string(doc) + polygon = Polygon.from_string(doc) assert polygon.geometry == geo.Polygon( [(-1, -1), (2, -1), (2, 2), (-1, -1)], @@ -163,7 +239,7 @@ def test_from_string_exterior_mixed_interior_relaxed(self) -> None: """ - polygon = Polygon.class_from_string(doc, strict=False) + polygon = Polygon.from_string(doc, strict=False) assert polygon.geometry == geo.Polygon( ((-1.0, -1.0), (2.0, -1.0), (2.0, 2.0), (-1.0, -1.0)), @@ -177,7 +253,7 @@ def test_empty_polygon(self) -> None: "" ) - polygon = Polygon.class_from_string(doc) + polygon = Polygon.from_string(doc) assert not polygon.geometry assert polygon.outer_boundary is not None diff --git a/tests/gx_test.py b/tests/gx_test.py index 90d67580..8ad064d6 100644 --- a/tests/gx_test.py +++ b/tests/gx_test.py @@ -19,13 +19,14 @@ import pygeoif.geometry as geo import pytest +from dateutil.tz import tzoffset 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 fastkml.times import KmlDateTime from tests.base import Lxml from tests.base import StdLibrary @@ -43,7 +44,7 @@ def test_track(self) -> None: 0.000000 0.000000 1.000000 1.000000 """ - g = Track.class_from_string(doc, ns="") + g = Track.from_string(doc, ns="") assert g.geometry.__geo_interface__ == { "type": "LineString", @@ -51,6 +52,13 @@ def test_track(self) -> None: "coordinates": ((0.0, 0.0), (1.0, 1.0)), } + def test_track_etree_element(self) -> None: + g = Track() + + g.etree_element() + + assert g.track_items == [] + def test_multitrack(self) -> None: doc = """ None: """ - mt = MultiTrack.class_from_string(doc, ns="") + mt = MultiTrack.from_string(doc) 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( + assert mt == MultiTrack( + tracks=[ + Track( + track_items=[ + TrackItem( + when=KmlDateTime( + dt=datetime.datetime( 2020, 1, 1, 0, 0, - tzinfo=tzutc(), + tzinfo=tzoffset(None, 0), ), - coord=geo.Point(0.0, 0.0), - angle=None, ), - TrackItem( - when=datetime.datetime( + coord=geo.Point(0.0, 0.0), + angle=Angle(heading=0.0, tilt=0.0, roll=0.0), + ), + TrackItem( + when=KmlDateTime( + dt=datetime.datetime( 2020, 1, 1, 0, 10, - tzinfo=tzutc(), + tzinfo=tzoffset(None, 0), ), - 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( + coord=geo.Point(1.0, 0.0), + angle=Angle(heading=0.0, tilt=0.0, roll=0.0), + ), + ], + ), + Track( + track_items=[ + TrackItem( + when=KmlDateTime( + dt=datetime.datetime( 2020, 1, 1, 0, 10, - tzinfo=tzutc(), + tzinfo=tzoffset(None, 0), ), - coord=geo.Point(0.0, 1.0), - angle=None, ), - TrackItem( - when=datetime.datetime( + coord=geo.Point(0.0, 1.0), + angle=Angle(heading=0.0, tilt=0.0, roll=0.0), + ), + TrackItem( + when=KmlDateTime( + dt=datetime.datetime( 2020, 1, 1, 0, 20, - tzinfo=tzutc(), + tzinfo=tzoffset(None, 0), ), - coord=geo.Point(1.0, 1.0), - angle=None, ), - ], - ), - ], - interpolate=None, - ).to_string() + coord=geo.Point(1.0, 1.0), + angle=Angle(heading=0.0, tilt=0.0, roll=0.0), + ), + ], + ), + ], ) 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() - def test_track_from_track_items(self) -> None: - time1 = datetime.datetime(2023, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc) + time1 = KmlDateTime( + datetime.datetime(2023, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc), + ) angle = Angle() track_items = [TrackItem(when=time1, coord=geo.Point(1, 2), angle=angle)] @@ -205,34 +176,76 @@ def test_track_from_track_items(self) -> None: assert "angles>" in track.to_string() assert ">0.0 0.0 0.0 None: - ls = geo.LineString(((1, 2), (2, 0))) - time1 = datetime.datetime(2023, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc) + def test_track_from_whens_and_coords(self) -> None: + whens = [ + KmlDateTime( + datetime.datetime(2023, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc), + ), + ] + coords = [(1, 2)] + + track = Track( + whens=whens, + coords=coords, + ) + + assert "when>" in track.to_string() + assert ">2023-01-01T00:00:00+00:00" in track.to_string() + assert ">1 2 None: + whens = [ + KmlDateTime( + datetime.datetime(2023, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc), + ), + ] + coords = [(1, 2)] + time1 = KmlDateTime( + datetime.datetime(2023, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc), + ) angle = Angle() track_items = [TrackItem(when=time1, coord=geo.Point(1, 2), angle=angle)] - with pytest.raises(ValueError): + with pytest.raises( + ValueError, + match="^Cannot specify both geometry and track_items$", + ): Track( + whens=whens, + coords=coords, track_items=track_items, - geometry=ls, ) - def test_track_from_track_items_no_coord(self) -> 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)] - + def test_track_precision(self) -> None: track = Track( - ns="", - track_items=track_items, + id="x", + target_id="y", + altitude_mode=None, + track_items=[ + TrackItem( + when=KmlDateTime( + dt=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=KmlDateTime( + dt=datetime.datetime(2010, 5, 28, 2, 2, 35, tzinfo=tzutc()), + ), + coord=geo.Point(-122.205712, 37.373288, 152.0), + angle=Angle(heading=1.0, tilt=2.0, roll=3.0), + ), + ], ) - 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.045.55 66.23 77.001.00 2.00 3.00-122.21 37.37 156.00-122.21 37.37 152.00 None: doc = """ @@ -245,7 +258,7 @@ def test_track_from_str(self) -> None: 2010-05-28T02:02:54Z 2010-05-28T02:02:55Z 2010-05-28T02:02:56Z - + 2010-05-28T02:02:57Z 45.54676 66.2342 77.0 1 2 3 @@ -258,64 +271,83 @@ def test_track_from_str(self) -> None: -122.205712 37.373288 152.000000 -122.204678 37.373939 147.000000 -122.203572 37.374630 142.199997 - + -112.203451 37.374690 141.800000 -122.203451 37.374706 141.800003 -122.203329 37.374780 141.199997 -122.203207 37.374857 140.199997 """ expected_track = Track( - ns="", + ns="{http://www.google.com/kml/ext/2.2}", + name_spaces={ + "kml": "{http://www.opengis.net/kml/2.2}", + "atom": "{http://www.w3.org/2005/Atom}", + "gx": "{http://www.google.com/kml/ext/2.2}", + }, id="", target_id="", - extrude=None, - tessellate=None, altitude_mode=None, track_items=[ TrackItem( - when=datetime.datetime(2010, 5, 28, 2, 2, 9, tzinfo=tzutc()), + when=KmlDateTime( + dt=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()), + when=KmlDateTime( + dt=datetime.datetime(2010, 5, 28, 2, 2, 35, tzinfo=tzutc()), + ), coord=geo.Point(-122.205712, 37.373288, 152.0), - angle=None, + angle=Angle(heading=1.0, tilt=2.0, roll=3.0), ), TrackItem( - when=datetime.datetime(2010, 5, 28, 2, 2, 44, tzinfo=tzutc()), + when=KmlDateTime( + dt=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()), + when=KmlDateTime( + dt=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, + when=KmlDateTime( + dt=datetime.datetime(2010, 5, 28, 2, 2, 54, tzinfo=tzutc()), + ), + coord=geo.Point(-112.203451, 37.37469, 141.8), angle=Angle(heading=1.0, tilt=2.0, roll=3.0), ), TrackItem( - when=datetime.datetime(2010, 5, 28, 2, 2, 55, tzinfo=tzutc()), + when=KmlDateTime( + dt=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()), + when=KmlDateTime( + dt=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, + when=KmlDateTime( + dt=datetime.datetime(2010, 5, 28, 2, 2, 57, tzinfo=tzutc()), + ), coord=geo.Point(-122.203207, 37.374857, 140.199997), - angle=Angle(heading=1.0, tilt=2.0, roll=3.0), + angle=Angle(heading=0.0, tilt=0.0, roll=0.0), ), ], ) - track = Track.class_from_string(doc, ns="") + track = Track.from_string(doc) assert track.geometry == geo.LineString( ( @@ -323,61 +355,46 @@ def test_track_from_str(self) -> None: (-122.205712, 37.373288, 152.0), (-122.204678, 37.373939, 147.0), (-122.203572, 37.37463, 142.199997), + (-112.203451, 37.37469, 141.8), (-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() + def test_track_from_str_invalid_when(self) -> None: + doc = """ + + 2010-02-32T02:02:09Z + 45.54676 66.2342 77.0 + -122.207881 37.371915 156.000000 + + """ + + track = Track.from_string(doc, strict=False) -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))), - ) + assert track.track_items == [] - mt = MultiTrack(geometry=lines, ns="") - - assert ( - mt.to_string() - == MultiTrack( - ns="", - id=None, - target_id=None, - extrude=None, - tessellate=None, - altitude_mode=None, - tracks=[ - Track( - ns="", - id=None, - target_id=None, - extrude=None, - tessellate=None, - 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=None, - tessellate=None, - 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), - ], - ), - ], - ).to_string() - ) + def test_track_from_str_invalid_coord(self) -> None: + doc = """ + + 2010-02-14T02:02:09Z + 45.54676 66.2342 77.0 + XYZ 37.371915 156.000000 + + """ + + track = Track.from_string(doc, strict=False) + + assert track.track_items == [] + + +class TestMultiTrack(StdLibrary): + """Test gx.MultiTrack.""" def test_multitrack(self) -> None: track = MultiTrack( @@ -387,37 +404,97 @@ def test_multitrack(self) -> None: 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), + TrackItem( + when=KmlDateTime( + datetime.datetime( + 2010, + 5, + 28, + 2, + 2, + 55, + tzinfo=tzutc(), + ), + ), + coord=geo.Point(0, 0), + angle=None, + ), + TrackItem( + when=KmlDateTime( + datetime.datetime( + 2010, + 5, + 28, + 2, + 2, + 56, + tzinfo=tzutc(), + ), + ), + coord=geo.Point(1, 1), + angle=None, + ), + TrackItem( + when=KmlDateTime( + datetime.datetime( + 2010, + 5, + 28, + 2, + 2, + 57, + tzinfo=tzutc(), + ), + ), + coord=geo.Point(1, 2), + angle=None, + ), + TrackItem( + when=KmlDateTime( + datetime.datetime( + 2010, + 5, + 28, + 2, + 2, + 58, + tzinfo=tzutc(), + ), + ), + coord=geo.Point(2, 2), + angle=None, + ), ], ), Track( ns="", track_items=[ TrackItem( - when=datetime.datetime( - 2010, - 5, - 28, - 2, - 2, - 55, - tzinfo=tzutc(), + when=KmlDateTime( + 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(), + when=KmlDateTime( + 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), @@ -443,29 +520,6 @@ def test_multitrack(self) -> None: assert "angles>" in track.to_string() assert "when>" in track.to_string() - def test_from_multilinestring_and_tracks(self) -> None: - lines = geo.MultiLineString( - (((0, 0), (1, 1), (1, 2), (2, 2)), ((0.0, 0.0), (1.0, 2.0))), - ) - 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), - ), - ] - - with pytest.raises(ValueError): - MultiTrack(geometry=lines, tracks=[Track(track_items=track_items)]) - class TestLxml(Lxml, TestStdLibrary): """Test with lxml.""" diff --git a/tests/helper_test.py b/tests/helper_test.py new file mode 100644 index 00000000..cd1ae8b7 --- /dev/null +++ b/tests/helper_test.py @@ -0,0 +1,147 @@ +# Copyright (C) 2024 Rishit Chaudhary, 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 helper functions edge cases.""" +from enum import Enum +from typing import Callable +from unittest.mock import Mock +from unittest.mock import patch + +from fastkml.helpers import attribute_enum_kwarg +from fastkml.helpers import attribute_float_kwarg +from fastkml.helpers import subelement_bool_kwarg +from fastkml.helpers import subelement_enum_kwarg +from fastkml.helpers import subelement_float_kwarg +from fastkml.helpers import subelement_int_kwarg +from tests.base import StdLibrary + + +class Node: + text: str + + +class Color(Enum): + RED = 1 + + +class TestStdLibrary(StdLibrary): + """Test with the standard library.""" + + def test_subelement_int_kwarg(self) -> None: + node = Node() + node.text = "" + element = Mock() + element.find.return_value = node + res = subelement_int_kwarg( + element=element, + ns="ns", + name_spaces={"name": "uri"}, + node_name="node", + kwarg="kwarg", + classes=(int,), + strict=False, + ) + assert res == {} + + def test_subelement_float_kwarg(self) -> None: + node = Node() + node.text = "" + element = Mock() + element.find.return_value = node + res = subelement_float_kwarg( + element=element, + ns="ns", + name_spaces={"name": "uri"}, + node_name="node", + kwarg="kwarg", + classes=(float,), + strict=False, + ) + assert res == {} + + @patch("fastkml.helpers.handle_error") + def test_attribute_float_kwarg( + self, + mock_handle_error: Callable[..., None], + ) -> None: + element = Mock() + element.get.return_value = "abcd" + + res = attribute_float_kwarg( + element=element, + ns="ns", + name_spaces={"name": "uri"}, + node_name="node", + kwarg="a", + classes=(float,), + strict=True, + ) + + assert res == {} + mock_handle_error.assert_called_once() # type: ignore[attr-defined] + + def test_subelement_enum_kwarg(self) -> None: + node = Node() + node.text = "" + element = Mock() + element.find.return_value = node + + res = subelement_enum_kwarg( + element=element, + ns="ns", + name_spaces={"name": "uri"}, + node_name="node", + kwarg="a", + classes=(Color,), + strict=True, + ) + + assert res == {} + element.find.assert_called_once_with("nsnode") + + def test_attribute_enum_kwarg(self) -> None: + element = Mock() + element.get.return_value = None + + res = attribute_enum_kwarg( + element=element, + ns="ns", + name_spaces={"name": "uri"}, + node_name="node", + kwarg="a", + classes=(Color,), + strict=True, + ) + + assert res == {} + element.get.assert_called_once_with("nsnode") + + def test_subelement_bool_kwarg(self) -> None: + node = Node() + node.text = "" + element = Mock() + element.find.return_value = node + res = subelement_bool_kwarg( + element=element, + ns="ns", + name_spaces={"name": "uri"}, + node_name="node", + kwarg="a", + classes=(bool,), + strict=True, + ) + + assert res == {} + element.find.assert_called_once_with("nsnode") diff --git a/tests/hypothesis/__init__.py b/tests/hypothesis/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/hypothesis/atom_test.py b/tests/hypothesis/atom_test.py new file mode 100644 index 00000000..4cf0cbe8 --- /dev/null +++ b/tests/hypothesis/atom_test.py @@ -0,0 +1,91 @@ +# Copyright (C) 2024 Christian Ledermann +# +# This library is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +""" +Property-based tests for Link and Author classes using Hypothesis. + +This module implements fuzz testing to verify the serialization/deserialization +roundtrip and string representation of Link and Author classes under various +input conditions. +""" + +import typing + +from hypothesis import given +from hypothesis import strategies as st +from hypothesis.provisional import urls + +import fastkml +import fastkml.enums +from tests.base import Lxml +from tests.hypothesis.common import assert_repr_roundtrip +from tests.hypothesis.common import assert_str_roundtrip +from tests.hypothesis.common import assert_str_roundtrip_terse +from tests.hypothesis.common import assert_str_roundtrip_verbose +from tests.hypothesis.strategies import href_langs +from tests.hypothesis.strategies import media_types +from tests.hypothesis.strategies import xml_text + + +class TestLxml(Lxml): + + @given( + href=urls(), + rel=st.one_of(st.none(), xml_text()), + type=st.one_of(st.none(), media_types()), + hreflang=st.one_of(st.none(), href_langs()), + title=st.one_of(st.none(), xml_text()), + length=st.one_of(st.none(), st.integers()), + ) + def test_fuzz_link( + self, + href: typing.Optional[str], + rel: typing.Optional[str], + type: typing.Optional[str], + hreflang: typing.Optional[str], + title: typing.Optional[str], + length: typing.Optional[int], + ) -> None: + link = fastkml.atom.Link( + href=href, + rel=rel, + type=type, + hreflang=hreflang, + title=title, + length=length, + ) + + assert_repr_roundtrip(link) + assert_str_roundtrip(link) + assert_str_roundtrip_terse(link) + assert_str_roundtrip_verbose(link) + + @given( + name=st.one_of(st.none(), xml_text()), + uri=st.one_of(st.none(), urls()), + email=st.one_of(st.none(), st.emails()), + ) + def test_fuzz_author( + self, + name: typing.Optional[str], + uri: typing.Optional[str], + email: typing.Optional[str], + ) -> None: + author = fastkml.atom.Author(name=name, uri=uri, email=email) + + assert_repr_roundtrip(author) + assert_str_roundtrip(author) + assert_str_roundtrip_terse(author) + assert_str_roundtrip_verbose(author) diff --git a/tests/hypothesis/common.py b/tests/hypothesis/common.py new file mode 100644 index 00000000..53dee19e --- /dev/null +++ b/tests/hypothesis/common.py @@ -0,0 +1,107 @@ +# Copyright (C) 2024 Christian Ledermann +# +# This library is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +"""Common functionality for property based tests.""" +import datetime +import logging + +from dateutil.tz import tzfile +from dateutil.tz import tzutc +from pygeoif import GeometryCollection +from pygeoif import MultiLineString +from pygeoif import MultiPoint +from pygeoif import MultiPolygon +from pygeoif.geometry import LinearRing +from pygeoif.geometry import LineString +from pygeoif.geometry import Point +from pygeoif.geometry import Polygon + +import fastkml +from fastkml.base import _XMLObject +from fastkml.enums import AltitudeMode +from fastkml.enums import DateTimeResolution +from fastkml.enums import RefreshMode +from fastkml.enums import Verbosity +from fastkml.enums import ViewRefreshMode +from fastkml.gx import Angle +from fastkml.gx import TrackItem +from fastkml.validator import validate + +logger = logging.getLogger(__name__) + +eval_locals = { + "Point": Point, + "Polygon": Polygon, + "LineString": LineString, + "LinearRing": LinearRing, + "MultiPoint": MultiPoint, + "MultiLineString": MultiLineString, + "MultiPolygon": MultiPolygon, + "GeometryCollection": GeometryCollection, + "AltitudeMode": AltitudeMode, + "fastkml": fastkml, + "ViewRefreshMode": ViewRefreshMode, + "RefreshMode": RefreshMode, + "TrackItem": TrackItem, + "Angle": Angle, + "datetime": datetime, + "DateTimeResolution": DateTimeResolution, + "tzutc": tzutc, + "tzfile": tzfile, +} + + +def assert_repr_roundtrip(obj: _XMLObject) -> None: + """Test that repr(obj) can be eval'd back to obj.""" + try: + assert obj == eval(repr(obj), {}, eval_locals) # noqa: S307 + except FileNotFoundError: + # The timezone file may not be available on all systems. + logger.exception("Failed to eval repr(obj).") + + +def assert_str_roundtrip(obj: _XMLObject) -> None: + """ + Test that an XML object can be serialized and deserialized without changes. + + Uses default verbosity settings and validates the resulting XML structure. + """ + new_object = type(obj).from_string(obj.to_string()) + + assert obj.to_string() == new_object.to_string() + assert obj == new_object + assert validate(element=new_object.etree_element()) + + +def assert_str_roundtrip_terse(obj: _XMLObject) -> None: + new_object = type(obj).from_string( + obj.to_string(verbosity=Verbosity.terse), + ) + + assert obj.to_string(verbosity=Verbosity.verbose) == new_object.to_string( + verbosity=Verbosity.verbose, + ) + assert validate(element=new_object.etree_element()) + + +def assert_str_roundtrip_verbose(obj: _XMLObject) -> None: + new_object = type(obj).from_string( + obj.to_string(verbosity=Verbosity.verbose), + ) + + assert obj.to_string(verbosity=Verbosity.terse) == new_object.to_string( + verbosity=Verbosity.terse, + ) + assert validate(element=new_object.etree_element()) diff --git a/tests/hypothesis/geometry_test.py b/tests/hypothesis/geometry_test.py new file mode 100644 index 00000000..0e85323d --- /dev/null +++ b/tests/hypothesis/geometry_test.py @@ -0,0 +1,484 @@ +# Copyright (C) 2024 Christian Ledermann +# +# This library is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +"""Property based tests of the Geometry classes.""" +import typing +from functools import partial + +from hypothesis import given +from hypothesis import settings +from hypothesis import strategies as st +from pygeoif.geometry import LinearRing +from pygeoif.geometry import LineString +from pygeoif.geometry import Point +from pygeoif.geometry import Polygon +from pygeoif.hypothesis.strategies import epsg4326 +from pygeoif.hypothesis.strategies import line_coords +from pygeoif.hypothesis.strategies import line_strings +from pygeoif.hypothesis.strategies import points +from pygeoif.hypothesis.strategies import polygons + +import fastkml.geometry +from fastkml.enums import AltitudeMode +from fastkml.enums import Verbosity +from fastkml.validator import validate +from tests.base import Lxml +from tests.hypothesis.common import assert_repr_roundtrip +from tests.hypothesis.common import assert_str_roundtrip +from tests.hypothesis.strategies import nc_name + +eval_locals = { + "Point": Point, + "Polygon": Polygon, + "LineString": LineString, + "LinearRing": LinearRing, + "AltitudeMode": AltitudeMode, + "fastkml": fastkml, +} + +kml_geometry = typing.Union[ + fastkml.geometry.Point, + fastkml.geometry.LineString, + fastkml.geometry.Polygon, +] + +coordinates = partial( + given, + coords=st.one_of(st.none(), line_coords(srs=epsg4326, min_points=1)), +) + +common_geometry = partial( + given, + id=st.one_of(st.none(), nc_name()), + target_id=st.one_of(st.none(), nc_name()), + extrude=st.one_of(st.none(), st.booleans()), + tessellate=st.one_of(st.none(), st.booleans()), + altitude_mode=st.one_of( + st.none(), + st.sampled_from( + ( + AltitudeMode.absolute, + AltitudeMode.clamp_to_ground, + AltitudeMode.relative_to_ground, + ), + ), + ), +) + + +def _test_repr_roundtrip(geometry: kml_geometry) -> None: + assert_repr_roundtrip(geometry) + + +def _test_geometry_str_roundtrip(geometry: kml_geometry) -> None: + assert_str_roundtrip(geometry) + + +def _test_geometry_str_roundtrip_terse(geometry: kml_geometry) -> None: + new_g = type(geometry).from_string( + geometry.to_string(verbosity=Verbosity.terse), + ) + + assert validate(element=new_g.etree_element()) + assert geometry.to_string(verbosity=Verbosity.verbose) == new_g.to_string( + verbosity=Verbosity.verbose, + ) + assert geometry.geometry == new_g.geometry + if geometry.altitude_mode == AltitudeMode.clamp_to_ground: + assert new_g.altitude_mode is None + else: + assert new_g.altitude_mode == geometry.altitude_mode + if geometry.extrude: + assert new_g.extrude is True + else: + assert new_g.extrude is None + if hasattr(geometry, "tessellate"): + assert not isinstance(geometry, fastkml.geometry.Point) + if geometry.tessellate: + assert new_g.tessellate is True + else: + assert new_g.tessellate is None + + +def _test_geometry_str_roundtrip_verbose(geometry: kml_geometry) -> None: + new_g = type(geometry).from_string( + geometry.to_string(verbosity=Verbosity.verbose), + ) + + assert validate(element=new_g.etree_element()) + assert geometry.to_string(verbosity=Verbosity.terse) == new_g.to_string( + verbosity=Verbosity.terse, + ) + assert geometry.geometry == new_g.geometry + assert new_g.altitude_mode is not None + if geometry.altitude_mode is None: + assert new_g.altitude_mode == AltitudeMode.clamp_to_ground + if geometry.extrude is None: + assert new_g.extrude is False + else: + assert new_g.extrude == geometry.extrude + if hasattr(geometry, "tessellate"): + assert not isinstance(geometry, fastkml.geometry.Point) + if geometry.tessellate is None: + assert new_g.tessellate is False + else: + assert new_g.tessellate == geometry.tessellate + + +class TestLxml(Lxml): + + @coordinates() + @settings(deadline=None) + def test_coordinates_str_roundtrip( + self, + coords: typing.Union[ + typing.Sequence[typing.Tuple[float, float]], + typing.Sequence[typing.Tuple[float, float, float]], + None, + ], + ) -> None: + coordinate = fastkml.geometry.Coordinates(coords=coords) + + new_c = fastkml.geometry.Coordinates.from_string( + coordinate.to_string(precision=20), + ) + + assert coordinate.to_string(precision=10) == new_c.to_string(precision=10) + assert validate(element=new_c.etree_element()) + + @coordinates() + def test_coordinates_repr_roundtrip( + self, + coords: typing.Union[ + typing.Sequence[typing.Tuple[float, float]], + typing.Sequence[typing.Tuple[float, float, float]], + None, + ], + ) -> None: + coordinate = fastkml.geometry.Coordinates(coords=coords) + + new_c = eval(repr(coordinate), {}, eval_locals) # noqa: S307 + + assert coordinate == new_c + assert validate(element=new_c.etree_element()) + + @common_geometry( + geometry=st.one_of( + st.none(), + points(srs=epsg4326), + ), + ) + def test_point_repr_roundtrip( + self, + id: typing.Optional[str], + target_id: typing.Optional[str], + extrude: typing.Optional[bool], + altitude_mode: typing.Optional[AltitudeMode], + tessellate: typing.Optional[bool], # noqa: ARG002 + geometry: typing.Optional[Point], + ) -> None: + point = fastkml.geometry.Point( + id=id, + target_id=target_id, + extrude=extrude, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_repr_roundtrip(point) + + @common_geometry( + geometry=st.one_of( + st.none(), + points(srs=epsg4326), + ), + ) + def test_point_str_roundtrip( + self, + id: typing.Optional[str], + target_id: typing.Optional[str], + extrude: typing.Optional[bool], + tessellate: typing.Optional[bool], # noqa: ARG002 + altitude_mode: typing.Optional[AltitudeMode], + geometry: typing.Optional[Point], + ) -> None: + point = fastkml.geometry.Point( + id=id, + target_id=target_id, + extrude=extrude, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_geometry_str_roundtrip(point) + + @common_geometry( + geometry=st.one_of( + st.none(), + points(srs=epsg4326), + ), + ) + def test_point_str_roundtrip_terse( + self, + id: typing.Optional[str], + target_id: typing.Optional[str], + extrude: typing.Optional[bool], + tessellate: typing.Optional[bool], # noqa: ARG002 + altitude_mode: typing.Optional[AltitudeMode], + geometry: typing.Optional[Point], + ) -> None: + point = fastkml.geometry.Point( + id=id, + target_id=target_id, + extrude=extrude, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_geometry_str_roundtrip_terse(point) + + @common_geometry( + geometry=st.one_of( + st.none(), + points(srs=epsg4326), + ), + ) + def test_point_str_roundtrip_verbose( + self, + id: typing.Optional[str], + target_id: typing.Optional[str], + extrude: typing.Optional[bool], + tessellate: typing.Optional[bool], # noqa: ARG002 + altitude_mode: typing.Optional[AltitudeMode], + geometry: typing.Optional[Point], + ) -> None: + point = fastkml.geometry.Point( + id=id, + target_id=target_id, + extrude=extrude, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_geometry_str_roundtrip_verbose(point) + + @common_geometry( + geometry=st.one_of( + st.none(), + line_strings(srs=epsg4326), + ), + ) + def test_linestring_repr_roundtrip( + self, + id: typing.Optional[str], + target_id: typing.Optional[str], + extrude: typing.Optional[bool], + tessellate: typing.Optional[bool], + altitude_mode: typing.Optional[AltitudeMode], + geometry: typing.Optional[LineString], + ) -> None: + line = fastkml.geometry.LineString( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_repr_roundtrip(line) + + @common_geometry( + geometry=st.one_of( + st.none(), + line_strings(srs=epsg4326), + ), + ) + def test_linestring_str_roundtrip( + self, + id: typing.Optional[str], + target_id: typing.Optional[str], + extrude: typing.Optional[bool], + tessellate: typing.Optional[bool], + altitude_mode: typing.Optional[AltitudeMode], + geometry: typing.Optional[LineString], + ) -> None: + line = fastkml.geometry.LineString( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_geometry_str_roundtrip(line) + + @common_geometry( + geometry=st.one_of( + st.none(), + line_strings(srs=epsg4326), + ), + ) + def test_linestring_str_roundtrip_terse( + self, + id: typing.Optional[str], + target_id: typing.Optional[str], + extrude: typing.Optional[bool], + tessellate: typing.Optional[bool], + altitude_mode: typing.Optional[AltitudeMode], + geometry: typing.Optional[LineString], + ) -> None: + line = fastkml.geometry.LineString( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_geometry_str_roundtrip_terse(line) + + @common_geometry( + geometry=st.one_of( + st.none(), + line_strings(srs=epsg4326), + ), + ) + def test_linestring_str_roundtrip_verbose( + self, + id: typing.Optional[str], + target_id: typing.Optional[str], + extrude: typing.Optional[bool], + tessellate: typing.Optional[bool], + altitude_mode: typing.Optional[AltitudeMode], + geometry: typing.Optional[LineString], + ) -> None: + line = fastkml.geometry.LineString( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_geometry_str_roundtrip_verbose(line) + + @common_geometry( + geometry=st.one_of( + st.none(), + polygons(srs=epsg4326), + ), + ) + def test_polygon_repr_roundtrip( + self, + id: typing.Optional[str], + target_id: typing.Optional[str], + extrude: typing.Optional[bool], + tessellate: typing.Optional[bool], + altitude_mode: typing.Optional[AltitudeMode], + geometry: typing.Optional[Polygon], + ) -> None: + polygon = fastkml.geometry.Polygon( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_repr_roundtrip(polygon) + + @common_geometry( + geometry=st.one_of( + st.none(), + polygons(srs=epsg4326), + ), + ) + def test_polygon_str_roundtrip( + self, + id: typing.Optional[str], + target_id: typing.Optional[str], + extrude: typing.Optional[bool], + tessellate: typing.Optional[bool], + altitude_mode: typing.Optional[AltitudeMode], + geometry: typing.Optional[Polygon], + ) -> None: + polygon = fastkml.geometry.Polygon( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_geometry_str_roundtrip(polygon) + + @common_geometry( + geometry=st.one_of( + st.none(), + polygons(srs=epsg4326), + ), + ) + def test_polygon_str_roundtrip_terse( + self, + id: typing.Optional[str], + target_id: typing.Optional[str], + extrude: typing.Optional[bool], + tessellate: typing.Optional[bool], + altitude_mode: typing.Optional[AltitudeMode], + geometry: typing.Optional[Polygon], + ) -> None: + polygon = fastkml.geometry.Polygon( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_geometry_str_roundtrip_terse(polygon) + + @common_geometry( + geometry=st.one_of( + st.none(), + polygons(srs=epsg4326), + ), + ) + def test_polygon_str_roundtrip_verbose( + self, + id: typing.Optional[str], + target_id: typing.Optional[str], + extrude: typing.Optional[bool], + tessellate: typing.Optional[bool], + altitude_mode: typing.Optional[AltitudeMode], + geometry: typing.Optional[Polygon], + ) -> None: + polygon = fastkml.geometry.Polygon( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_geometry_str_roundtrip_verbose(polygon) diff --git a/tests/hypothesis/gx_test.py b/tests/hypothesis/gx_test.py new file mode 100644 index 00000000..a9c184cd --- /dev/null +++ b/tests/hypothesis/gx_test.py @@ -0,0 +1,141 @@ +# Copyright (C) 2024 Christian Ledermann +# +# This library is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +"""Test gx Track and MultiTrack.""" +import datetime +import typing + +from hypothesis import given +from hypothesis import strategies as st +from hypothesis.extra.dateutil import timezones +from pygeoif.hypothesis.strategies import epsg4326 +from pygeoif.hypothesis.strategies import points + +import fastkml +import fastkml.enums +import fastkml.gx +import fastkml.types +from fastkml.gx import Angle +from fastkml.gx import TrackItem +from fastkml.times import KmlDateTime +from tests.base import Lxml +from tests.hypothesis.common import assert_repr_roundtrip +from tests.hypothesis.common import assert_str_roundtrip +from tests.hypothesis.common import assert_str_roundtrip_terse +from tests.hypothesis.common import assert_str_roundtrip_verbose +from tests.hypothesis.strategies import nc_name + +track_items = st.builds( + TrackItem, + angle=st.one_of( + st.one_of( + st.builds(Angle), + st.builds( + Angle, + heading=st.floats(allow_nan=False, allow_infinity=False), + roll=st.floats(allow_nan=False, allow_infinity=False), + tilt=st.floats(allow_nan=False, allow_infinity=False), + ), + ), + ), + coord=points(srs=epsg4326), + when=st.builds( + KmlDateTime, + dt=st.datetimes( + allow_imaginary=False, + timezones=timezones(), + min_value=datetime.datetime(2000, 1, 1), # noqa: DTZ001 + max_value=datetime.datetime(2050, 1, 1), # noqa: DTZ001 + ), + ), +) + + +class TestGx(Lxml): + + @given( + id=st.one_of(st.none(), nc_name()), + target_id=st.one_of(st.none(), nc_name()), + altitude_mode=st.one_of(st.none(), st.sampled_from(fastkml.enums.AltitudeMode)), + track_items=st.one_of( + st.none(), + st.lists( + track_items, + ), + ), + ) + def test_fuzz_track_track_items( + self, + id: typing.Optional[str], + target_id: typing.Optional[str], + altitude_mode: typing.Optional[fastkml.enums.AltitudeMode], + track_items: typing.Optional[typing.Iterable[fastkml.gx.TrackItem]], + ) -> None: + track = fastkml.gx.Track( + id=id, + target_id=target_id, + altitude_mode=altitude_mode, + track_items=track_items, + ) + + assert_repr_roundtrip(track) + assert_str_roundtrip(track) + assert_str_roundtrip_terse(track) + assert_str_roundtrip_verbose(track) + + @given( + id=st.one_of(st.none(), nc_name()), + target_id=st.one_of(st.none(), nc_name()), + altitude_mode=st.one_of(st.none(), st.sampled_from(fastkml.enums.AltitudeMode)), + tracks=st.one_of( + st.none(), + st.lists( + st.builds( + fastkml.gx.Track, + altitude_mode=st.one_of( + st.none(), + st.sampled_from(fastkml.enums.AltitudeMode), + ), + track_items=st.one_of( + st.none(), + st.lists( + track_items, + ), + ), + ), + ), + ), + interpolate=st.one_of(st.none(), st.booleans()), + ) + def test_fuzz_multi_track( + self, + id: typing.Optional[str], + target_id: typing.Optional[str], + altitude_mode: typing.Optional[fastkml.enums.AltitudeMode], + tracks: typing.Optional[typing.Iterable[fastkml.gx.Track]], + interpolate: typing.Optional[bool], + ) -> None: + multi_track = fastkml.gx.MultiTrack( + id=id, + target_id=target_id, + altitude_mode=altitude_mode, + tracks=tracks, + interpolate=interpolate, + ) + + assert_repr_roundtrip(multi_track) + assert_str_roundtrip(multi_track) + assert_str_roundtrip_terse(multi_track) + assert_str_roundtrip_verbose(multi_track) diff --git a/tests/hypothesis/links_test.py b/tests/hypothesis/links_test.py new file mode 100644 index 00000000..b965783a --- /dev/null +++ b/tests/hypothesis/links_test.py @@ -0,0 +1,101 @@ +# Copyright (C) 2024 Christian Ledermann +# +# This library is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +"""Test Link and Icon.""" +import string +import typing +from functools import partial + +import pytest +from hypothesis import given +from hypothesis import strategies as st +from hypothesis.provisional import urls + +import fastkml +import fastkml.enums +from fastkml.validator import validate +from tests.base import Lxml +from tests.hypothesis.common import assert_repr_roundtrip +from tests.hypothesis.common import assert_str_roundtrip +from tests.hypothesis.common import assert_str_roundtrip_terse +from tests.hypothesis.common import assert_str_roundtrip_verbose +from tests.hypothesis.strategies import nc_name +from tests.hypothesis.strategies import query_strings + +common_link = partial( + given, + id=st.one_of(st.none(), nc_name()), + target_id=st.one_of(st.none(), nc_name()), + href=st.one_of(st.none(), urls()), + refresh_mode=st.one_of(st.none(), st.sampled_from(fastkml.enums.RefreshMode)), + refresh_interval=st.one_of( + st.none(), + st.floats(allow_infinity=False, allow_nan=False), + ), + view_refresh_mode=st.one_of( + st.none(), + st.sampled_from(fastkml.enums.ViewRefreshMode), + ), + view_refresh_time=st.one_of( + st.none(), + st.floats(allow_infinity=False, allow_nan=False), + ), + view_bound_scale=st.one_of( + st.none(), + st.floats(allow_infinity=False, allow_nan=False), + ), + view_format=st.one_of( + st.none(), + st.text(string.ascii_letters + string.punctuation), + ), + http_query=st.one_of(st.none(), query_strings()), +) + + +class TestLxml(Lxml): + + @pytest.mark.parametrize("cls", [fastkml.Link, fastkml.Icon]) + @common_link() + def test_fuzz_link( + self, + cls: typing.Union[typing.Type[fastkml.Link], typing.Type[fastkml.Icon]], + id: typing.Optional[str], + target_id: typing.Optional[str], + href: typing.Optional[str], + refresh_mode: typing.Optional[fastkml.enums.RefreshMode], + refresh_interval: typing.Optional[float], + view_refresh_mode: typing.Optional[fastkml.enums.ViewRefreshMode], + view_refresh_time: typing.Optional[float], + view_bound_scale: typing.Optional[float], + view_format: typing.Optional[str], + http_query: typing.Optional[str], + ) -> None: + link = cls( + id=id, + target_id=target_id, + href=href, + refresh_mode=refresh_mode, + refresh_interval=refresh_interval, + view_refresh_mode=view_refresh_mode, + view_refresh_time=view_refresh_time, + view_bound_scale=view_bound_scale, + view_format=view_format, + http_query=http_query, + ) + assert validate(element=link.etree_element()) + assert_repr_roundtrip(link) + assert_str_roundtrip(link) + assert_str_roundtrip_terse(link) + assert_str_roundtrip_verbose(link) diff --git a/tests/hypothesis/multi_geometry_test.py b/tests/hypothesis/multi_geometry_test.py new file mode 100644 index 00000000..f54a56f6 --- /dev/null +++ b/tests/hypothesis/multi_geometry_test.py @@ -0,0 +1,704 @@ +# Copyright (C) 2024 Christian Ledermann +# +# This library is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +"""Property based tests of the Geometry classes.""" +from __future__ import annotations + +from functools import partial + +from hypothesis import given +from hypothesis import settings +from hypothesis import strategies as st +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 +from pygeoif.hypothesis.strategies import epsg4326 +from pygeoif.hypothesis.strategies import geometry_collections +from pygeoif.hypothesis.strategies import multi_line_strings +from pygeoif.hypothesis.strategies import multi_points +from pygeoif.hypothesis.strategies import multi_polygons + +import fastkml.geometry +from fastkml.enums import AltitudeMode +from fastkml.enums import Verbosity +from fastkml.validator import validate +from tests.base import Lxml +from tests.hypothesis.strategies import nc_name + +eval_locals = { + "Point": Point, + "Polygon": Polygon, + "LineString": LineString, + "LinearRing": LinearRing, + "AltitudeMode": AltitudeMode, + "MultiPoint": MultiPoint, + "MultiLineString": MultiLineString, + "MultiPolygon": MultiPolygon, + "GeometryCollection": GeometryCollection, + "fastkml": fastkml, +} + + +common_geometry = partial( + given, + id=st.one_of(st.none(), nc_name()), + target_id=st.one_of(st.none(), nc_name()), + extrude=st.one_of(st.none(), st.booleans()), + tessellate=st.one_of(st.none(), st.booleans()), + altitude_mode=st.one_of( + st.none(), + st.sampled_from( + ( + AltitudeMode.absolute, + AltitudeMode.clamp_to_ground, + AltitudeMode.relative_to_ground, + ), + ), + ), +) + + +def _test_repr_roundtrip( + geometry: fastkml.geometry.MultiGeometry, + cls: type[MultiPoint | MultiLineString | MultiPolygon | GeometryCollection], +) -> None: + new_g = eval(repr(geometry), {}, eval_locals) # noqa: S307 + + assert geometry == new_g + if geometry: + assert type(new_g.geometry) is cls + assert validate(element=new_g.etree_element()) + + +def _test_geometry_str_roundtrip( + geometry: fastkml.geometry.MultiGeometry, + *, + cls: type[MultiPoint | MultiLineString | MultiPolygon], + extrude: bool | None, + tessellate: bool | None, + altitude_mode: AltitudeMode | None, +) -> None: + new_g = fastkml.geometry.MultiGeometry.from_string(geometry.to_string()) + + assert geometry.to_string() == new_g.to_string() + assert geometry == new_g + if not geometry: + return + assert new_g.geometry + assert geometry.geometry + assert type(new_g.geometry) is cls + for g1, g2 in zip(new_g.kml_geometries, geometry.kml_geometries): + assert g1.extrude == g2.extrude == extrude + assert g1.altitude_mode == g2.altitude_mode == altitude_mode + if not isinstance(g1, fastkml.geometry.Point): + assert g1.tessellate == g2.tessellate == tessellate + assert validate(element=new_g.etree_element()) + + +def _test_geometry_str_roundtrip_terse( + geometry: fastkml.geometry.MultiGeometry, + *, + cls: type[MultiPoint | MultiLineString | MultiPolygon], + extrude: bool | None, + tessellate: bool | None, + altitude_mode: AltitudeMode | None, +) -> None: + new_g = fastkml.geometry.MultiGeometry.from_string( + geometry.to_string(verbosity=Verbosity.terse), + ) + + assert geometry.to_string(verbosity=Verbosity.verbose) == new_g.to_string( + verbosity=Verbosity.verbose, + ) + if not geometry: + return + assert new_g.geometry + assert geometry.geometry + assert type(new_g.geometry) is cls + for new, orig in zip(new_g.kml_geometries, geometry.kml_geometries): + if extrude: + assert new.extrude == orig.extrude == extrude + else: + assert new.extrude is None + if altitude_mode == AltitudeMode.clamp_to_ground: + assert new.altitude_mode is None + else: + assert new.altitude_mode == orig.altitude_mode == altitude_mode + if not isinstance(new, fastkml.geometry.Point): + if tessellate: + assert new.tessellate == orig.tessellate == tessellate + else: + assert new.tessellate is None + assert validate(element=new_g.etree_element()) + + +def _test_geometry_str_roundtrip_verbose( + geometry: fastkml.geometry.MultiGeometry, + *, + cls: type[MultiPoint | MultiLineString | MultiPolygon], + extrude: bool | None, + tessellate: bool | None, + altitude_mode: AltitudeMode | None, +) -> None: + new_g = fastkml.geometry.MultiGeometry.from_string( + geometry.to_string(verbosity=Verbosity.verbose), + ) + + assert geometry.to_string(verbosity=Verbosity.terse) == new_g.to_string( + verbosity=Verbosity.terse, + ) + if not geometry: + return + assert new_g.geometry + assert geometry.geometry + assert type(new_g.geometry) is cls + for new, orig in zip(new_g.kml_geometries, geometry.kml_geometries): + if isinstance(new, fastkml.geometry.MultiGeometry): + continue + assert not isinstance(orig, fastkml.geometry.MultiGeometry) + if extrude: + assert new.extrude == orig.extrude == extrude + else: + assert new.extrude is False + if altitude_mode is None: + assert new.altitude_mode == AltitudeMode.clamp_to_ground + else: + assert new.altitude_mode == orig.altitude_mode == altitude_mode + if not isinstance(new, fastkml.geometry.Point): + if tessellate: + assert new.tessellate == orig.tessellate == tessellate + else: + assert new.tessellate is False + validate(element=new_g.etree_element()) + + +class TestLxml(Lxml): + """Validation requires lxml.""" + + @common_geometry( + geometry=st.one_of( + st.none(), + multi_points(srs=epsg4326), + ), + ) + @settings(deadline=1_000) + def test_multipoint_repr_roundtrip( + self, + id: str | None, + target_id: str | None, + extrude: bool | None, + tessellate: bool | None, + altitude_mode: AltitudeMode | None, + geometry: MultiPoint | None, + ) -> None: + multi_geometry = fastkml.geometry.MultiGeometry( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_repr_roundtrip(multi_geometry, MultiPoint) + + @common_geometry( + geometry=st.one_of( + st.none(), + multi_points(srs=epsg4326), + ), + ) + def test_multipoint_str_roundtrip( + self, + id: str | None, + target_id: str | None, + extrude: bool | None, + tessellate: bool | None, + altitude_mode: AltitudeMode | None, + geometry: MultiPoint | None, + ) -> None: + multi_geometry = fastkml.geometry.MultiGeometry( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_geometry_str_roundtrip( + multi_geometry, + cls=MultiPoint, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + ) + + @common_geometry( + geometry=st.one_of( + st.none(), + multi_points(srs=epsg4326), + ), + ) + def test_multipoint_str_roundtrip_terse( + self, + id: str | None, + target_id: str | None, + extrude: bool | None, + tessellate: bool | None, + altitude_mode: AltitudeMode | None, + geometry: MultiPoint | None, + ) -> None: + multi_geometry = fastkml.geometry.MultiGeometry( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_geometry_str_roundtrip_terse( + multi_geometry, + cls=MultiPoint, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + ) + + @common_geometry( + geometry=st.one_of( + st.none(), + multi_points(srs=epsg4326), + ), + ) + def test_multipoint_str_roundtrip_verbose( + self, + id: str | None, + target_id: str | None, + extrude: bool | None, + tessellate: bool | None, + altitude_mode: AltitudeMode | None, + geometry: MultiPoint | None, + ) -> None: + multi_geometry = fastkml.geometry.MultiGeometry( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_geometry_str_roundtrip_verbose( + multi_geometry, + cls=MultiPoint, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + ) + + @common_geometry( + geometry=st.one_of( + st.none(), + multi_line_strings(srs=epsg4326), + ), + ) + def test_multilinestring_repr_roundtrip( + self, + id: str | None, + target_id: str | None, + extrude: bool | None, + tessellate: bool | None, + altitude_mode: AltitudeMode | None, + geometry: MultiLineString | None, + ) -> None: + multi_geometry = fastkml.geometry.MultiGeometry( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_repr_roundtrip(multi_geometry, MultiLineString) + + @common_geometry( + geometry=st.one_of( + st.none(), + multi_line_strings(srs=epsg4326), + ), + ) + def test_multilinestring_str_roundtrip( + self, + id: str | None, + target_id: str | None, + extrude: bool | None, + tessellate: bool | None, + altitude_mode: AltitudeMode | None, + geometry: MultiLineString | None, + ) -> None: + multi_geometry = fastkml.geometry.MultiGeometry( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_geometry_str_roundtrip( + multi_geometry, + cls=MultiLineString, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + ) + + @common_geometry( + geometry=st.one_of( + st.none(), + multi_line_strings(srs=epsg4326), + ), + ) + def test_multilinestring_str_roundtrip_terse( + self, + id: str | None, + target_id: str | None, + extrude: bool | None, + tessellate: bool | None, + altitude_mode: AltitudeMode | None, + geometry: MultiLineString | None, + ) -> None: + multi_geometry = fastkml.geometry.MultiGeometry( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_geometry_str_roundtrip_terse( + multi_geometry, + cls=MultiLineString, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + ) + + @common_geometry( + geometry=st.one_of( + st.none(), + multi_line_strings(srs=epsg4326), + ), + ) + def test_multilinestring_str_roundtrip_verbose( + self, + id: str | None, + target_id: str | None, + extrude: bool | None, + tessellate: bool | None, + altitude_mode: AltitudeMode | None, + geometry: MultiLineString | None, + ) -> None: + multi_geometry = fastkml.geometry.MultiGeometry( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_geometry_str_roundtrip_verbose( + multi_geometry, + cls=MultiLineString, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + ) + + @common_geometry( + geometry=st.one_of( + st.none(), + multi_polygons(srs=epsg4326), + ), + ) + def test_multipolygon_repr_roundtrip( + self, + id: str | None, + target_id: str | None, + extrude: bool | None, + tessellate: bool | None, + altitude_mode: AltitudeMode | None, + geometry: MultiPolygon | None, + ) -> None: + multi_geometry = fastkml.geometry.MultiGeometry( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_repr_roundtrip(multi_geometry, MultiPolygon) + + @common_geometry( + geometry=st.one_of( + st.none(), + multi_polygons(srs=epsg4326), + ), + ) + def test_multipolygon_str_roundtrip( + self, + id: str | None, + target_id: str | None, + extrude: bool | None, + tessellate: bool | None, + altitude_mode: AltitudeMode | None, + geometry: MultiPolygon | None, + ) -> None: + multi_geometry = fastkml.geometry.MultiGeometry( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_geometry_str_roundtrip( + multi_geometry, + cls=MultiPolygon, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + ) + + @common_geometry( + geometry=st.one_of( + st.none(), + multi_polygons(srs=epsg4326), + ), + ) + def test_multipolygon_str_roundtrip_terse( + self, + id: str | None, + target_id: str | None, + extrude: bool | None, + tessellate: bool | None, + altitude_mode: AltitudeMode | None, + geometry: MultiPolygon | None, + ) -> None: + multi_geometry = fastkml.geometry.MultiGeometry( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_geometry_str_roundtrip_terse( + multi_geometry, + cls=MultiPolygon, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + ) + + @common_geometry( + geometry=st.one_of( + st.none(), + multi_polygons(srs=epsg4326), + ), + ) + def test_multipolygon_str_roundtrip_verbose( + self, + id: str | None, + target_id: str | None, + extrude: bool | None, + tessellate: bool | None, + altitude_mode: AltitudeMode | None, + geometry: MultiPolygon | None, + ) -> None: + multi_geometry = fastkml.geometry.MultiGeometry( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + _test_geometry_str_roundtrip_verbose( + multi_geometry, + cls=MultiPolygon, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + ) + + @common_geometry( + geometry=st.one_of( + st.none(), + geometry_collections(srs=epsg4326), + ), + ) + def test_geometrycollection_repr_roundtrip( + self, + id: str | None, + target_id: str | None, + extrude: bool | None, + tessellate: bool | None, + altitude_mode: AltitudeMode | None, + geometry: GeometryCollection | None, + ) -> None: + multi_geometry = fastkml.geometry.MultiGeometry( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + new_mg = eval(repr(multi_geometry), {}, eval_locals) # noqa: S307 + + assert multi_geometry == new_mg + if geometry: + assert isinstance( + new_mg.geometry, + (GeometryCollection, MultiPolygon, MultiLineString, MultiPoint), + ) + else: + assert not new_mg + + @common_geometry( + geometry=st.one_of( + st.none(), + geometry_collections(srs=epsg4326), + ), + ) + def test_geometrycollection_str_roundtrip( + self, + id: str | None, + target_id: str | None, + extrude: bool | None, + tessellate: bool | None, + altitude_mode: AltitudeMode | None, + geometry: GeometryCollection | None, + ) -> None: + multi_geometry = fastkml.geometry.MultiGeometry( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + new_mg = fastkml.geometry.MultiGeometry.from_string( + multi_geometry.to_string(), + ) + + if geometry: + assert isinstance( + new_mg.geometry, + (GeometryCollection, MultiPolygon, MultiLineString, MultiPoint), + ) + else: + assert not new_mg + + @common_geometry( + geometry=st.one_of( + st.none(), + geometry_collections(srs=epsg4326), + ), + ) + def test_geometrycollection_str_roundtrip_terse( + self, + id: str | None, + target_id: str | None, + extrude: bool | None, + tessellate: bool | None, + altitude_mode: AltitudeMode | None, + geometry: GeometryCollection | None, + ) -> None: + multi_geometry = fastkml.geometry.MultiGeometry( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + new_mg = fastkml.geometry.MultiGeometry.from_string( + multi_geometry.to_string(verbosity=Verbosity.terse), + ) + + if geometry: + assert isinstance( + new_mg.geometry, + (GeometryCollection, MultiPolygon, MultiLineString, MultiPoint), + ) + else: + assert not new_mg + + @common_geometry( + geometry=st.one_of( + st.none(), + geometry_collections(srs=epsg4326), + ), + ) + def test_geometrycollection_str_roundtrip_verbose( + self, + id: str | None, + target_id: str | None, + extrude: bool | None, + tessellate: bool | None, + altitude_mode: AltitudeMode | None, + geometry: GeometryCollection | None, + ) -> None: + multi_geometry = fastkml.geometry.MultiGeometry( + id=id, + target_id=target_id, + extrude=extrude, + tessellate=tessellate, + altitude_mode=altitude_mode, + geometry=geometry, + ) + + new_mg = fastkml.geometry.MultiGeometry.from_string( + multi_geometry.to_string(verbosity=Verbosity.verbose), + ) + + if geometry: + assert isinstance( + new_mg.geometry, + (GeometryCollection, MultiPolygon, MultiLineString, MultiPoint), + ) + else: + assert not new_mg diff --git a/tests/hypothesis/strategies.py b/tests/hypothesis/strategies.py new file mode 100644 index 00000000..3e154cea --- /dev/null +++ b/tests/hypothesis/strategies.py @@ -0,0 +1,60 @@ +# Copyright (C) 2024 Christian Ledermann +# +# This library is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +"""Custom hypothesis strategies for testing.""" +import re +import string +from functools import partial +from typing import Final +from urllib.parse import urlencode + +from hypothesis import strategies as st + +ID_TEXT: Final = string.ascii_letters + string.digits + ".-_" +nc_name = partial( + st.from_regex, + regex=re.compile(r"^[A-Za-z_][\w.-]*$"), + alphabet=ID_TEXT, + fullmatch=True, +) + +href_langs = partial( + st.from_regex, + regex=re.compile(r"^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})?$"), + alphabet=f"{string.ascii_letters}-{string.digits}", + fullmatch=True, +) +media_types = partial( + st.from_regex, + regex=re.compile(r"^[a-zA-Z0-9-]+/[a-zA-Z0-9-]+$"), + alphabet=f"{string.ascii_letters}/-{string.digits}", + fullmatch=True, +) +xml_text = partial( + st.text, + alphabet=st.characters(min_codepoint=1, blacklist_categories=("Cc", "Cs")), +) + + +@st.composite +def query_strings(draw: st.DrawFn) -> str: + params = draw( + st.dictionaries( + keys=st.text(alphabet=string.ascii_letters, min_size=1), + values=st.text(alphabet=string.printable), + ), + ) + return urlencode(params) diff --git a/tests/kml_test.py b/tests/kml_test.py index e9f73188..e7189417 100644 --- a/tests/kml_test.py +++ b/tests/kml_test.py @@ -19,6 +19,7 @@ import pathlib import pygeoif as geo +from pygeoif.geometry import Polygon from fastkml import containers from fastkml import features @@ -36,7 +37,7 @@ class TestStdLibrary(StdLibrary): """Test with the standard library.""" def test_linarring_placemark(self) -> None: - doc = kml.KML.class_from_string( + doc = kml.KML.from_string( """ @@ -44,7 +45,7 @@ def test_linarring_placemark(self) -> None: """, ) - doc2 = kml.KML.class_from_string(doc.to_string()) + doc2 = kml.KML.from_string(doc.to_string()) assert isinstance(doc.features[0].geometry, geo.LinearRing) assert doc.to_string() == doc2.to_string() @@ -56,7 +57,7 @@ def test_kml(self) -> None: k.to_string().strip().replace(" ", "") == '' ) - k2 = kml.KML.class_from_string(k.to_string(), ns="") + k2 = kml.KML.from_string(k.to_string(), ns="") assert k.to_string() == k2.to_string() def test_kml_with_document(self) -> None: @@ -92,7 +93,7 @@ def test_kml_with_document(self) -> None: "" "" ) - k = kml.KML.class_from_string(doc) + k = kml.KML.from_string(doc) assert len(k.features) == 1 assert isinstance(k.features[0], Document) assert len(k.features[0].features) == 1 @@ -174,6 +175,15 @@ def test_parse_kml_fileobject(self) -> None: ) +class TestParseKMLNone(StdLibrary): + def test_kml_parse(self) -> None: + empty_placemark = KMLFILEDIR / "emptyPlacemarkWithoutId.xml" + + doc = kml.KML.parse(file=empty_placemark, ns="None") + + assert doc.ns == "None" + + class TestLxml(Lxml, TestStdLibrary): """Test with lxml.""" @@ -192,3 +202,360 @@ def test_from_string_with_unbound_prefix(self) -> None: k = kml.KML.parse(doc, ns="{http://www.opengis.net/kml/2.2}") assert len(k.features) == 1 assert isinstance(k.features[0], features.Placemark) + + +class TestKmlFromString: + def test_document(self) -> None: + doc = """ + + Document.kml + 1 + + + Document Feature 1 + #exampleStyleDocument + + -122.371,37.816,0 + + + + Document Feature 2 + #exampleStyleDocument + + -122.370,37.817,0 + + + + """ + + k = kml.KML.from_string(doc) + assert len(k.features) == 1 + assert len(k.features[0].features) == 2 + k2 = kml.KML.from_string(k.to_string()) + assert k.to_string() == k2.to_string() + + def test_folders(self) -> None: + doc = """ + + Folder.kml + 1 + + A folder is a container that can hold multiple other objects + + + Folder object 1 (Placemark) + + -122.377588,37.830266,0 + + + + Folder object 2 (Polygon) + + + + + -122.377830,37.830445,0 + -122.377576,37.830631,0 + -122.377840,37.830642,0 + -122.377830,37.830445,0 + + + + + + + Folder object 3 (Path) + + 1 + + -122.378009,37.830128,0 -122.377885,37.830379,0 + + + + + """ + + k = kml.KML.from_string(doc) + assert len(k.features) == 1 + assert len(k.features[0].features) == 3 + k2 = kml.KML.from_string(k.to_string()) + assert k.to_string() == k2.to_string() + + def test_placemark(self) -> None: + doc = """ + + Simple placemark + Attached to the ground. Intelligently places itself + at the height of the underlying terrain. + + -122.0822035425683,37.42228990140251,0 + + + """ + + k = kml.KML.from_string(doc) + assert len(k.features) == 1 + assert k.features[0].name == "Simple placemark" + k2 = kml.KML.from_string(k.to_string()) + assert k.to_string() == k2.to_string() + + def test_polygon(self) -> None: + doc = """ + + + South Africa + + + + + 31.521,-29.257,0 + 31.326,-29.402,0 + 30.902,-29.91,0 + 30.623,-30.424,0 + 30.056,-31.14,0 + 28.926,-32.172,0 + 28.22,-32.772,0 + 27.465,-33.227,0 + 26.419,-33.615,0 + 25.91,-33.667,0 + 25.781,-33.945,0 + 25.173,-33.797,0 + 24.678,-33.987,0 + 23.594,-33.794,0 + 22.988,-33.916,0 + 22.574,-33.864,0 + 21.543,-34.259,0 + 20.689,-34.417,0 + 20.071,-34.795,0 + 19.616,-34.819,0 + 19.193,-34.463,0 + 18.855,-34.444,0 + 18.425,-33.998,0 + 18.377,-34.137,0 + 18.244,-33.868,0 + 18.25,-33.281,0 + 17.925,-32.611,0 + 18.248,-32.429,0 + 18.222,-31.662,0 + 17.567,-30.726,0 + 17.064,-29.879,0 + 17.063,-29.876,0 + 16.345,-28.577,0 + 16.824,-28.082,0 + 17.219,-28.356,0 + 17.387,-28.784,0 + 17.836,-28.856,0 + 18.465,-29.045,0 + 19.002,-28.972,0 + 19.895,-28.461,0 + 19.896,-24.768,0 + 20.166,-24.918,0 + 20.759,-25.868,0 + 20.666,-26.477,0 + 20.89,-26.829,0 + 21.606,-26.727,0 + 22.106,-26.28,0 + 22.58,-25.979,0 + 22.824,-25.5,0 + 23.312,-25.269,0 + 23.734,-25.39,0 + 24.211,-25.67,0 + 25.025,-25.72,0 + 25.665,-25.487,0 + 25.766,-25.175,0 + 25.942,-24.696,0 + 26.486,-24.616,0 + 26.786,-24.241,0 + 27.119,-23.574,0 + 28.017,-22.828,0 + 29.432,-22.091,0 + 29.839,-22.102,0 + 30.323,-22.272,0 + 30.66,-22.152,0 + 31.191,-22.252,0 + 31.67,-23.659,0 + 31.931,-24.369,0 + 31.752,-25.484,0 + 31.838,-25.843,0 + 31.333,-25.66,0 + 31.044,-25.731,0 + 30.95,-26.023,0 + 30.677,-26.398,0 + 30.686,-26.744,0 + 31.283,-27.286,0 + 31.868,-27.178,0 + 32.072,-26.734,0 + 32.83,-26.742,0 + 32.58,-27.47,0 + 32.462,-28.301,0 + 32.203,-28.752,0 + 31.521,-29.257,0 + + + + + + + 28.978,-28.956,0 + 28.542,-28.648,0 + 28.074,-28.851,0 + 27.533,-29.243,0 + 26.999,-29.876,0 + 27.749,-30.645,0 + 28.107,-30.546,0 + 28.291,-30.226,0 + 28.848,-30.07,0 + 29.018,-29.744,0 + 29.325,-29.257,0 + 28.978,-28.956,0 + + + + + + """ + + k = kml.KML.from_string(doc) + assert len(k.features) == 1 + assert isinstance(k.features[0].geometry, Polygon) + k2 = kml.KML.from_string(k.to_string()) + assert k.to_string() == k2.to_string() + + def test_multipoints(self) -> None: + doc = """ + + MultiPoint + #stylesel_9 + + + 16,-35,0.0 + + + 16,-33,0.0 + + + 16,-31,0.0 + + + 16,-29,0.0 + + + 16,-27,0.0 + + + 16,-25,0.0 + + + 16,-23,0.0 + + + 16,-21,0.0 + + + 18,-35,0.0 + + + 18,-33,0.0 + + + 18,-31,0.0 + + + 18,-29,0.0 + + + """ + + k = kml.KML.from_string(doc) + assert len(k.features) == 1 + assert isinstance(k.features[0].geometry, geo.MultiPoint) + assert len(list(k.features[0].geometry.geoms)) == 12 + k2 = kml.KML.from_string(k.to_string()) + assert k.to_string() == k2.to_string() + + def test_snippet(self) -> None: + doc = """ + + Short Desc + """ + + k = kml.KML.from_string(doc) + assert k.features[0].snippet.text == "Short Desc" + assert k.features[0].snippet.max_lines == 2 + k.features[0].snippet = features.Snippet( + text="Another Snippet", + max_lines=3, + ) + assert 'maxLines="3"' in k.to_string() + k.features[0].snippet = features.Snippet(text="Another Snippet") + assert "maxLines" not in k.to_string() + assert "Another Snippet" in k.to_string() + k.features[0].snippet = features.Snippet(text="Different Snippet") + assert "maxLines" not in k.to_string() + assert "Different Snippet" in k.to_string() + k.features[0].snippet = features.Snippet(text="", max_lines=4) + assert "Snippet" not in k.to_string() + + def test_address(self) -> None: + doc = Document.from_string( + """ + + pm-name + pm-description + 1 + 1600 Amphitheatre Parkway,... + + """, + ) + + doc2 = Document.from_string(doc.to_string()) + assert doc.to_string() == doc2.to_string() + + def test_phone_number(self) -> None: + doc = Document.from_string( + """ + + pm-name + pm-description + 1 + +1 234 567 8901 + + """, + ) + + doc2 = Document.from_string(doc.to_string()) + assert doc.to_string() == doc2.to_string() + + def test_groundoverlay(self) -> None: + doc = kml.KML.from_string( + """ + + + Ground Overlays + Examples of ground overlays + + Large-scale overlay on terrain + Overlay shows Mount Etna erupting + on July 13th, 2001. + + http://developers.google.com/kml/etna.jpg + + + 37.91904192681665 + 37.46543388598137 + 15.35832653742206 + 14.60128369746704 + -0.1556640799496235 + + + + + """, + ) + + doc2 = kml.KML.from_string(doc.to_string()) + assert doc.to_string() == doc2.to_string() diff --git a/tests/links_test.py b/tests/links_test.py index 39486630..b7628f63 100644 --- a/tests/links_test.py +++ b/tests/links_test.py @@ -39,7 +39,7 @@ def test_icon(self) -> None: http_query="clientName=fastkml", ) - icon = links.Icon.class_from_string(icon_0.to_string()) + icon = links.Icon.from_string(icon_0.to_string()) assert icon.id == "icon-01" assert icon.href == "http://maps.google.com/mapfiles/kml/paddle/red-circle.png" @@ -53,7 +53,7 @@ def test_icon(self) -> None: def test_icon_read(self) -> None: """Test the Icon class.""" - icon = links.Icon.class_from_string( + icon = links.Icon.from_string( """ http://maps.google.com/mapfiles/kml/paddle/red-circle.png @@ -78,7 +78,7 @@ def test_icon_read(self) -> None: assert icon.view_format == "BBOX=[bboxWest],[bboxSouth],[bboxEast],[bboxNorth]" assert icon.http_query == "clientName=fastkml" - icon2 = links.Icon.class_from_string(icon.to_string()) + icon2 = links.Icon.from_string(icon.to_string()) assert icon2.to_string() == icon.to_string() diff --git a/tests/ogc_conformance/data/kml/Document-places.kml b/tests/ogc_conformance/data/kml/Document-places.kml index b35dd2ed..97129097 100644 --- a/tests/ogc_conformance/data/kml/Document-places.kml +++ b/tests/ogc_conformance/data/kml/Document-places.kml @@ -4,13 +4,13 @@ place123 - -95.44,40.42,0 + -95.44,40.42,0.00 place456 - -95.43,40.42,0 + -95.43,40.42,0.00 diff --git a/tests/oldunit_test.py b/tests/oldunit_test.py deleted file mode 100644 index 80a56fa4..00000000 --- a/tests/oldunit_test.py +++ /dev/null @@ -1,1141 +0,0 @@ -# Copyright (C) 2012 -2022 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 -import xml.etree.ElementTree - -from pygeoif.geometry import MultiPoint -from pygeoif.geometry import Polygon - -from fastkml import atom -from fastkml import config -from fastkml import features -from fastkml import kml -from fastkml import kml_base -from fastkml import styles -from fastkml.enums import ColorMode -from fastkml.enums import DisplayMode - - -class TestBaseClasses: - """ - BaseClasses must raise a NotImplementedError on etree_element. - """ - - def setup_method(self) -> None: - """Always test with the same parser.""" - config.set_etree_implementation(xml.etree.ElementTree) - config.set_default_namespaces() - - def test_base_object(self) -> None: - bo = kml_base._BaseObject(id="id0") - assert bo.id == "id0" - assert bo.ns == config.KMLNS - assert bo.target_id == "" - bo.target_id = "target" - assert bo.target_id == "target" - bo.ns = "" - bo.id = None - assert bo.id is None - assert not bo.ns - - -class TestBuildKml: - """Build a simple KML File.""" - - def setup_method(self) -> None: - """Always test with the same parser.""" - config.set_etree_implementation(xml.etree.ElementTree) - config.set_default_namespaces() - - def test_folder(self) -> None: - """KML file with folders.""" - ns = "{http://www.opengis.net/kml/2.2}" - k = kml.KML(ns=ns) - f = kml.Folder(ns=ns, id="id", name="name", description="description") - nf = kml.Folder( - ns=ns, - id="nested-id", - name="nested-name", - description="nested-description", - ) - f.append(nf) - k.append(f) - f2 = kml.Folder(ns, id="id2", name="name2", description="description2") - k.append(f2) - assert len(k.features) == 2 - assert len(k.features[0].features) == 1 - s = k.to_string() - k2 = kml.KML.class_from_string(s, ns=ns) - assert s == k2.to_string() - - def test_placemark(self) -> None: - ns = "{http://www.opengis.net/kml/2.2}" - k = kml.KML(ns=ns) - p = kml.Placemark(ns, id="id", name="name", description="description") - # XXX p.geometry = Point(0.0, 0.0, 0.0) - p2 = kml.Placemark(ns, id="id2", name="name2", description="description2") - # XXX p2.geometry = LineString([(0, 0, 0), (1, 1, 1)]) - k.append(p) - k.append(p2) - assert len(k.features) == 2 - k2 = kml.KML.class_from_string(k.to_string(prettyprint=True), ns=ns) - assert k.to_string() == k2.to_string() - - def test_document(self) -> None: - ns = "{http://www.opengis.net/kml/2.2}" - k = kml.KML(ns=ns) - d = kml.Document(ns, id="docid", name="doc name", description="doc description") - f = kml.Folder(ns, id="fid", name="f name", description="f description") - k.append(d) - d.append(f) - nf = kml.Folder( - ns, - id="nested-fid", - name="nested f name", - description="nested f description", - ) - f.append(nf) - f2 = kml.Folder(ns, id="id2", name="name2", description="description2") - d.append(f2) - p = kml.Placemark(ns, id="id", name="name", description="description") - # XXX p.geometry = Polygon([(0, 0, 0), (1, 1, 0), (1, 0, 1)]) - p2 = kml.Placemark(ns, id="id2", name="name2", description="description2") - # p2 does not have a geometry! - f2.append(p) - nf.append(p2) - assert len(k.features) == 1 - assert len(k.features[0].features) == 2 - k2 = kml.KML.class_from_string(k.to_string()) - assert k.to_string() == k2.to_string() - - def test_author(self) -> None: - d = kml.Document() - d.atom_author = atom.Author( - ns="{http://www.w3.org/2005/Atom}", - name="Christian Ledermann", - ) - assert "Christian Ledermann" in str(d.to_string()) - a = atom.Author( - ns="{http://www.w3.org/2005/Atom}", - name="Nobody", - uri="http://localhost", - email="cl@donotreply.com", - ) - d.atom_author = a - assert "Christian Ledermann" not in str(d.to_string()) - assert "Nobody" in str(d.to_string()) - assert "http://localhost" in str(d.to_string()) - assert "cl@donotreply.com" in str(d.to_string()) - d2 = kml.Document.class_from_string(d.to_string()) - assert d.to_string() == d2.to_string() - - def test_link(self) -> None: - d = kml.Document() - d.atom_link = atom.Link(ns=config.ATOMNS, href="http://localhost") - assert "http://localhost" in str(d.to_string()) - d.atom_link = atom.Link(ns=config.ATOMNS, href="#here") - assert "#here" in str(d.to_string()) - d2 = kml.Document.class_from_string(d.to_string()) - assert d.to_string() == d2.to_string() - - 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) -> None: - phone = "+1 234 567 8901" - d = kml.Document() - d.phone_number = phone - assert phone in str(d.to_string()) - assert "phoneNumber>" in str(d.to_string()) - - -class TestKmlFromString: - def test_document(self) -> None: - doc = """ - - Document.kml - 1 - - - Document Feature 1 - #exampleStyleDocument - - -122.371,37.816,0 - - - - Document Feature 2 - #exampleStyleDocument - - -122.370,37.817,0 - - - - """ - - k = kml.KML.class_from_string(doc) - assert len(k.features) == 1 - assert len(k.features[0].features) == 2 - k2 = kml.KML.class_from_string(k.to_string()) - assert k.to_string() == k2.to_string() - - def test_folders(self) -> None: - doc = """ - - Folder.kml - 1 - - A folder is a container that can hold multiple other objects - - - Folder object 1 (Placemark) - - -122.377588,37.830266,0 - - - - Folder object 2 (Polygon) - - - - - -122.377830,37.830445,0 - -122.377576,37.830631,0 - -122.377840,37.830642,0 - -122.377830,37.830445,0 - - - - - - - Folder object 3 (Path) - - 1 - - -122.378009,37.830128,0 -122.377885,37.830379,0 - - - - - """ - - k = kml.KML.class_from_string(doc) - assert len(k.features) == 1 - assert len(k.features[0].features) == 3 - k2 = kml.KML.class_from_string(k.to_string()) - assert k.to_string() == k2.to_string() - - def test_placemark(self) -> None: - doc = """ - - Simple placemark - Attached to the ground. Intelligently places itself - at the height of the underlying terrain. - - -122.0822035425683,37.42228990140251,0 - - - """ - - k = kml.KML.class_from_string(doc) - assert len(k.features) == 1 - assert k.features[0].name == "Simple placemark" - k2 = kml.KML.class_from_string(k.to_string()) - assert k.to_string() == k2.to_string() - - def test_polygon(self) -> None: - doc = """ - - - South Africa - - - - - 31.521,-29.257,0 - 31.326,-29.402,0 - 30.902,-29.91,0 - 30.623,-30.424,0 - 30.056,-31.14,0 - 28.926,-32.172,0 - 28.22,-32.772,0 - 27.465,-33.227,0 - 26.419,-33.615,0 - 25.91,-33.667,0 - 25.781,-33.945,0 - 25.173,-33.797,0 - 24.678,-33.987,0 - 23.594,-33.794,0 - 22.988,-33.916,0 - 22.574,-33.864,0 - 21.543,-34.259,0 - 20.689,-34.417,0 - 20.071,-34.795,0 - 19.616,-34.819,0 - 19.193,-34.463,0 - 18.855,-34.444,0 - 18.425,-33.998,0 - 18.377,-34.137,0 - 18.244,-33.868,0 - 18.25,-33.281,0 - 17.925,-32.611,0 - 18.248,-32.429,0 - 18.222,-31.662,0 - 17.567,-30.726,0 - 17.064,-29.879,0 - 17.063,-29.876,0 - 16.345,-28.577,0 - 16.824,-28.082,0 - 17.219,-28.356,0 - 17.387,-28.784,0 - 17.836,-28.856,0 - 18.465,-29.045,0 - 19.002,-28.972,0 - 19.895,-28.461,0 - 19.896,-24.768,0 - 20.166,-24.918,0 - 20.759,-25.868,0 - 20.666,-26.477,0 - 20.89,-26.829,0 - 21.606,-26.727,0 - 22.106,-26.28,0 - 22.58,-25.979,0 - 22.824,-25.5,0 - 23.312,-25.269,0 - 23.734,-25.39,0 - 24.211,-25.67,0 - 25.025,-25.72,0 - 25.665,-25.487,0 - 25.766,-25.175,0 - 25.942,-24.696,0 - 26.486,-24.616,0 - 26.786,-24.241,0 - 27.119,-23.574,0 - 28.017,-22.828,0 - 29.432,-22.091,0 - 29.839,-22.102,0 - 30.323,-22.272,0 - 30.66,-22.152,0 - 31.191,-22.252,0 - 31.67,-23.659,0 - 31.931,-24.369,0 - 31.752,-25.484,0 - 31.838,-25.843,0 - 31.333,-25.66,0 - 31.044,-25.731,0 - 30.95,-26.023,0 - 30.677,-26.398,0 - 30.686,-26.744,0 - 31.283,-27.286,0 - 31.868,-27.178,0 - 32.072,-26.734,0 - 32.83,-26.742,0 - 32.58,-27.47,0 - 32.462,-28.301,0 - 32.203,-28.752,0 - 31.521,-29.257,0 - - - - - - - 28.978,-28.956,0 - 28.542,-28.648,0 - 28.074,-28.851,0 - 27.533,-29.243,0 - 26.999,-29.876,0 - 27.749,-30.645,0 - 28.107,-30.546,0 - 28.291,-30.226,0 - 28.848,-30.07,0 - 29.018,-29.744,0 - 29.325,-29.257,0 - 28.978,-28.956,0 - - - - - - """ - - k = kml.KML.class_from_string(doc) - assert len(k.features) == 1 - assert isinstance(k.features[0].geometry, Polygon) - k2 = kml.KML.class_from_string(k.to_string()) - assert k.to_string() == k2.to_string() - - def test_multipoints(self) -> None: - doc = """ - - MultiPoint - #stylesel_9 - - - 16,-35,0.0 - - - 16,-33,0.0 - - - 16,-31,0.0 - - - 16,-29,0.0 - - - 16,-27,0.0 - - - 16,-25,0.0 - - - 16,-23,0.0 - - - 16,-21,0.0 - - - 18,-35,0.0 - - - 18,-33,0.0 - - - 18,-31,0.0 - - - 18,-29,0.0 - - - """ - - k = kml.KML.class_from_string(doc) - assert len(k.features) == 1 - assert isinstance(k.features[0].geometry, MultiPoint) - assert len(list(k.features[0].geometry.geoms)) == 12 - k2 = kml.KML.class_from_string(k.to_string()) - assert k.to_string() == k2.to_string() - - def test_atom(self) -> None: - pass - - def test_snippet(self) -> None: - doc = """ - - Short Desc - """ - - k = kml.KML.class_from_string(doc) - assert k.features[0].snippet.text == "Short Desc" - assert k.features[0].snippet.max_lines == 2 - k.features[0].snippet = features.Snippet( - text="Another Snippet", - max_lines=3, - ) - assert 'maxLines="3"' in k.to_string() - k.features[0].snippet = features.Snippet(text="Another Snippet") - assert "maxLines" not in k.to_string() - assert "Another Snippet" in k.to_string() - k.features[0].snippet = features.Snippet(text="Different Snippet") - assert "maxLines" not in k.to_string() - assert "Different Snippet" in k.to_string() - k.features[0].snippet = features.Snippet(text="", max_lines=4) - assert "Snippet" not in k.to_string() - - def test_address(self) -> None: - doc = kml.Document.class_from_string( - """ - - pm-name - pm-description - 1 - 1600 Amphitheatre Parkway,... - - """, - ) - - doc2 = kml.Document.class_from_string(doc.to_string()) - assert doc.to_string() == doc2.to_string() - - def test_phone_number(self) -> None: - doc = kml.Document.class_from_string( - """ - - pm-name - pm-description - 1 - +1 234 567 8901 - - """, - ) - - doc2 = kml.Document.class_from_string(doc.to_string()) - assert doc.to_string() == doc2.to_string() - - def test_groundoverlay(self) -> None: - doc = kml.KML.class_from_string( - """ - - - Ground Overlays - Examples of ground overlays - - Large-scale overlay on terrain - Overlay shows Mount Etna erupting - on July 13th, 2001. - - http://developers.google.com/kml/etna.jpg - - - 37.91904192681665 - 37.46543388598137 - 15.35832653742206 - 14.60128369746704 - -0.1556640799496235 - - - - - """, - ) - - doc2 = kml.KML.class_from_string(doc.to_string()) - assert doc.to_string() == doc2.to_string() - - -class TestStyle: - def test_styleurl(self) -> None: - f = kml.Document() - s = styles.StyleUrl(config.KMLNS, url="#otherstyle") - f.style_url = s - f2 = kml.Document.class_from_string(f.to_string()) - assert f.to_string() == f2.to_string() - assert isinstance(f2.style_url, styles.StyleUrl) - assert f2.style_url.url == "#otherstyle" - - def test_style(self) -> None: - lstyle = styles.LineStyle(color="red", width=2.0) - style = styles.Style(styles=[lstyle]) - f = kml.Document(styles=[style]) - f2 = kml.Document.class_from_string(f.to_string(prettyprint=True)) - assert f.to_string() == f2.to_string() - - def test_polystyle_fill(self) -> None: - styles.PolyStyle() - - def test_polystyle_outline(self) -> None: - styles.PolyStyle() - - -class TestStyleUsage: - def test_create_document_style(self) -> None: - style = styles.Style( - styles=[ - styles.PolyStyle( - color="7f000000", - fill=True, - outline=True, - ), - ], - ) - - doc = kml.Document(styles=[style]) - - doc2 = kml.Document() - doc2.styles.append(style) - - expected = """ - - - - - 7f000000 - 1 - 1 - - - - """ - - doc3 = kml.Document.class_from_string(expected) - - assert doc.to_string() == doc2.to_string() - assert doc2.to_string() == doc3.to_string() - assert doc.to_string() == doc3.to_string() - - def test_create_placemark_style(self) -> None: - style = styles.Style( - styles=[ - styles.PolyStyle( - color="7f000000", - fill=True, - outline=True, - ), - ], - ) - - place = kml.Placemark(styles=[style]) - - place2 = kml.Placemark() - place2.styles.append(style) - - expected = """ - - - - 7f000000 - 1 - 1 - - - - """ - - place3 = kml.Placemark.class_from_string(expected) - assert place.to_string() == place2.to_string() - assert place2.to_string() == place3.to_string() - assert place.to_string() == place3.to_string() - - -class TestStyleFromString: - def test_styleurl(self) -> None: - doc = """ - - Document.kml - 1 - #default - - """ - - k = kml.KML.class_from_string(doc) - assert len(k.features) == 1 - assert k.features[0].style_url.url == "#default" - k2 = kml.KML.class_from_string(k.to_string()) - assert k.to_string() == k2.to_string() - - def test_balloonstyle(self) -> None: - doc = """ - - Document.kml - - - """ - - k = kml.KML.class_from_string(doc) - features = k.features - assert len(features) == 1 - style = features[0].styles[0] - assert isinstance(style, styles.Style) - style_1 = style.styles[0] - assert isinstance(style_1, styles.BalloonStyle) - assert style_1.bg_color == "ffffffbb" - assert style_1.text_color == "ff000000" - assert style_1.display_mode == DisplayMode.default - assert style_1.text - assert "$[geDirections]" in style_1.text - assert "$[description]" in style_1.text - k2 = kml.KML.class_from_string(k.to_string()) - assert k2.to_string() == k.to_string() - - def test_balloonstyle_old_color(self) -> None: - doc = """ - - Document.kml - - - """ - - k = kml.KML.class_from_string(doc) - assert len(k.features) == 1 - assert isinstance(k.features[0].styles[0], styles.Style) - style = k.features[0].styles[0].styles[0] - assert isinstance(style, styles.BalloonStyle) - assert style.bg_color is None - assert not style - k2 = kml.KML.class_from_string(k.to_string()) - assert k2.to_string() == k.to_string() - - def test_labelstyle(self) -> None: - doc = """ - - Document.kml - 1 - - - """ - - k = kml.KML.class_from_string(doc) - assert len(k.features) == 1 - assert isinstance(k.features[0].styles[0], styles.Style) - style = k.features[0].styles[0].styles[0] - assert isinstance(style, styles.LabelStyle) - assert style.color == "ff0000cc" - assert style.color_mode is None - k2 = kml.KML.class_from_string(k.to_string()) - assert k.to_string() == k2.to_string() - - def test_iconstyle(self) -> None: - doc = """ - - - - """ - - k = kml.KML.class_from_string(doc) - assert len(k.features) == 1 - assert isinstance(k.features[0].styles[0], styles.Style) - style = k.features[0].styles[0].styles[0] - assert isinstance(style, styles.IconStyle) - assert style.color == "ff00ff00" - assert style.scale == 1.1 - assert style.color_mode == ColorMode.random - assert style.heading == 0.0 - assert style.icon.href == "http://maps.google.com/icon21.png" - k2 = kml.KML.class_from_string(k.to_string()) - assert k.to_string() == k2.to_string() - - def test_linestyle(self) -> None: - doc = """ - - LineStyle.kml - 1 - - - """ - - k = kml.KML.class_from_string(doc) - assert len(k.features) == 1 - assert isinstance(k.features[0].styles[0], styles.Style) - style = k.features[0].styles[0].styles[0] - assert isinstance(style, styles.LineStyle) - assert style.color == "7f0000ff" - assert style.width == 4 - k2 = kml.KML.class_from_string(k.to_string()) - assert k.to_string() == k2.to_string() - - def test_polystyle(self) -> None: - doc = """ - - PolygonStyle.kml - 1 - - - """ - - # XXX fill and outline - k = kml.KML.class_from_string(doc) - assert len(k.features) == 1 - assert isinstance(k.features[0].styles[0], styles.Style) - style = k.features[0].styles[0].styles[0] - assert isinstance(style, styles.PolyStyle) - assert style.color == "ff0000cc" - assert style.color_mode == ColorMode.random - k2 = kml.KML.class_from_string(k.to_string()) - assert k.to_string() == k2.to_string() - - def test_polystyle_boolean_fill(self) -> None: - doc = """ - - PolygonStyle.kml - 1 - - - """ - - k = kml.KML.class_from_string(doc, strict=False) - style = k.features[0].styles[0].styles[0] - assert isinstance(style, styles.PolyStyle) - assert style.fill == 0 - k2 = kml.KML.class_from_string(k.to_string()) - assert k.to_string() == k2.to_string() - - def test_polystyle_boolean_outline(self) -> None: - doc = """ - - PolygonStyle.kml - 1 - - - """ - - k = kml.KML.class_from_string(doc, strict=False) - style = k.features[0].styles[0].styles[0] - assert isinstance(style, styles.PolyStyle) - assert style.outline == 0 - k2 = kml.KML.class_from_string(k.to_string()) - assert k.to_string() == k2.to_string() - - def test_polystyle_float_fill(self) -> None: - doc = """ - - PolygonStyle.kml - 1 - - - """ - - k = kml.KML.class_from_string(doc, strict=False) - style = k.features[0].styles[0].styles[0] - assert isinstance(style, styles.PolyStyle) - assert style.fill == 0 - k2 = kml.KML.class_from_string(k.to_string()) - assert k.to_string() == k2.to_string() - - def test_polystyle_float_outline(self) -> None: - doc = """ - - PolygonStyle.kml - 1 - - - """ - - k = kml.KML.class_from_string(doc, strict=False) - style = k.features[0].styles[0].styles[0] - assert isinstance(style, styles.PolyStyle) - assert style.outline == 0 - k2 = kml.KML.class_from_string(k.to_string()) - assert k.to_string() == k2.to_string() - - def test_styles(self) -> None: - doc = """ - - - - - - """ - - k = kml.KML.class_from_string(doc) - assert len(k.features) == 1 - assert isinstance(k.features[0].styles[0], styles.Style) - style = k.features[0].styles[0].styles - assert len(style) == 4 - k2 = kml.KML.class_from_string(k.to_string()) - assert k.to_string() == k2.to_string() - - def test_stylemapurl(self) -> None: - doc = """ - - - - normal - #normalState - - - highlight - #highlightState - - - - """ - - k = kml.KML.class_from_string(doc) - features = k.features - assert len(features) == 1 - feature_styles = features[0].styles - assert isinstance( - feature_styles[0], - styles.StyleMap, - ) - sm = feature_styles[0] - - assert isinstance(sm.normal, styles.StyleUrl) - assert sm.normal.url == "#normalState" - assert isinstance(sm.highlight, styles.StyleUrl) - assert sm.highlight.url == "#highlightState" - k2 = kml.KML.class_from_string(k.to_string()) - assert k.to_string() == k2.to_string() - - def test_stylemapstyles(self) -> None: - doc = """ - - - - normal - - - - highlight - - - - - """ - - k = kml.KML.class_from_string(doc) - assert len(k.features) == 1 - assert isinstance( - k.features[0].styles[0], - styles.StyleMap, - ) - sm = k.features[0].styles[0] - assert isinstance(sm.normal, styles.Style) - assert len(sm.normal.styles) == 1 - assert isinstance(sm.normal.styles[0], styles.LabelStyle) - assert isinstance(sm.highlight, styles.Style) - assert isinstance(sm.highlight, styles.Style) - assert len(sm.highlight.styles) == 2 - assert isinstance(sm.highlight.styles[0], styles.LineStyle) - assert isinstance(sm.highlight.styles[1], styles.PolyStyle) - k2 = kml.KML.class_from_string(k.to_string()) - assert k.to_string() == k2.to_string() - - def test_get_style_by_url(self) -> None: - doc = """ - - Document.kml - 1 - - - - normal - #normalState - - - highlight - #highlightState - - - - - """ - - k = kml.KML.class_from_string(doc) - assert len(k.features) == 1 - document = k.features[0] - style = document.get_style_by_url( - "http://localhost:8080/somepath#exampleStyleDocument", - ) - assert isinstance(style.styles[0], styles.LabelStyle) - style = document.get_style_by_url("somepath#linestyleExample") - assert isinstance(style.styles[0], styles.LineStyle) - style = document.get_style_by_url("#styleMapExample") - assert isinstance(style, styles.StyleMap) - - -def test_nested_multigeometry() -> None: - doc = """ - - - - - - -122.366278,37.818844,0 -122.365248,37.819267,0 - -122.365640,37.819875,0 -122.366278,37.818844,0 - - - - - - -122.365,37.819,0 - - - - - -122.365278,37.819000,0 -122.365248,37.819267,0 - - - - - - - -122.365248,37.819267,0 -122.365640,37.819875,0 - -122.366278,37.818844,0 -122.365248,37.819267,0 - - - - - - - - """ - - k = kml.KML.class_from_string(doc) - placemark = k.features[0].features[0] - - first_multigeometry = placemark.geometry - assert len(list(first_multigeometry.geoms)) == 3 - - second_multigeometry = next( - g for g in first_multigeometry.geoms if g.geom_type == "GeometryCollection" - ) - assert len(list(second_multigeometry.geoms)) == 2 - - -class TestGetGeometry: - def test_nested_multigeometry(self) -> None: - doc = """ - - - - - - -122.366278,37.818844,0 -122.365248,37.819267,0 - -122.365640,37.819875,0 -122.366278,37.818844,0 - - - - - - -122.365,37.819,0 - - - - - -122.365278,37.819000,0 -122.365248,37.819267,0 - - - - - - - -122.365248,37.819267,0 -122.365640,37.819875,0 - -122.366278,37.818844,0 -122.365248,37.819267,0 - - - - - - - - """ - - k = kml.KML.class_from_string(doc) - placemark = k.features[0].features[0] - - first_multigeometry = placemark.geometry - assert len(list(first_multigeometry.geoms)) == 3 - - second_multigeometry = next( - g for g in first_multigeometry.geoms if g.geom_type == "GeometryCollection" - ) - assert len(list(second_multigeometry.geoms)) == 2 diff --git a/tests/overlays_test.py b/tests/overlays_test.py index a21c2339..3a7a71cf 100644 --- a/tests/overlays_test.py +++ b/tests/overlays_test.py @@ -36,7 +36,7 @@ class TestGroundOverlayString(StdLibrary): def test_default_to_string(self) -> None: g = overlays.GroundOverlay() - expected = overlays.GroundOverlay.class_from_string( + expected = overlays.GroundOverlay.from_string( '' "", ) @@ -49,7 +49,7 @@ def test_to_string(self) -> None: g.draw_order = 1 g.color = "00010203" - expected = overlays.GroundOverlay.class_from_string( + expected = overlays.GroundOverlay.from_string( '' "00010203" "1" @@ -65,7 +65,7 @@ def test_altitude_from_int(self) -> None: g = overlays.GroundOverlay() g.altitude = 123.0 - expected = overlays.GroundOverlay.class_from_string( + expected = overlays.GroundOverlay.from_string( '' "123" "", @@ -77,7 +77,7 @@ def test_altitude_from_float(self) -> None: g = overlays.GroundOverlay() g.altitude = 123.4 - expected = overlays.GroundOverlay.class_from_string( + expected = overlays.GroundOverlay.from_string( '' "123.4" "", @@ -86,7 +86,7 @@ def test_altitude_from_float(self) -> None: assert g.to_string() == expected.to_string() def test_altitude_invalid(self) -> None: - g = overlays.GroundOverlay.class_from_string( + g = overlays.GroundOverlay.from_string( '' " one two" "", @@ -96,7 +96,7 @@ def test_altitude_invalid(self) -> None: assert g.altitude is None def test_draw_order_from_invalid(self) -> None: - g = overlays.GroundOverlay.class_from_string( + g = overlays.GroundOverlay.from_string( '' "nan" "", @@ -111,7 +111,7 @@ def test_altitude_from_string(self) -> None: altitude_mode=AltitudeMode.clamp_to_ground, ) - expected = overlays.GroundOverlay.class_from_string( + expected = overlays.GroundOverlay.from_string( '' "123.4" "clampToGround" @@ -122,7 +122,7 @@ def test_altitude_from_string(self) -> None: def test_altitude_mode_absolute(self) -> None: g = overlays.GroundOverlay(altitude=123.4, altitude_mode=AltitudeMode.absolute) - expected = overlays.GroundOverlay.class_from_string( + expected = overlays.GroundOverlay.from_string( '' "123.4" "absolute" @@ -141,7 +141,7 @@ def test_latlonbox_no_rotation(self) -> None: ) g = overlays.GroundOverlay(lat_lon_box=llb) - expected = overlays.GroundOverlay.class_from_string( + expected = overlays.GroundOverlay.from_string( '' "" "10" @@ -165,7 +165,7 @@ def test_latlonbox_rotation(self) -> None: ) g = overlays.GroundOverlay(lat_lon_box=llb) - expected = overlays.GroundOverlay.class_from_string( + expected = overlays.GroundOverlay.from_string( '' "" "10" @@ -190,7 +190,7 @@ def test_latlonbox_nswer(self) -> None: ) g = overlays.GroundOverlay() g.lat_lon_box = llb - expected = overlays.GroundOverlay.class_from_string( + expected = overlays.GroundOverlay.from_string( '' "" "10" @@ -249,6 +249,9 @@ def test_create_photo_overlay_with_all_optional_parameters(self) -> None: assert photo_overlay.description == "This is a photo overlay" assert photo_overlay.shape == enums.Shape.rectangle assert photo_overlay.rotation == 0 + assert photo_overlay.view_volume.__bool__() is True + assert bool(photo_overlay.view_volume) + assert bool(photo_overlay.image_pyramid) def test_read_photo_overlay(self) -> None: """Read a PhotoOverlay object from a KML file.""" @@ -275,7 +278,7 @@ def test_read_photo_overlay(self) -> None: "rectangle" ) - p_overlay = overlays.PhotoOverlay.class_from_string(doc) + p_overlay = overlays.PhotoOverlay.from_string(doc) assert p_overlay.id == "photo_overlay_1" assert p_overlay.name == "Photo Overlay" @@ -329,7 +332,7 @@ def test_camera_altitude_none(self) -> None: def test_camera_altitude_mode_default(self) -> None: po = overlays.PhotoOverlay(view=views.Camera()) - assert po.view.altitude_mode == AltitudeMode("relativeToGround") + assert po.view.altitude_mode is None def test_camera_altitude_mode_clamp(self) -> None: po = overlays.PhotoOverlay(view=views.Camera()) @@ -357,7 +360,6 @@ def test_camera_initialization(self) -> None: assert po.view.heading == 40 assert po.view.tilt == 50 assert po.view.roll == 60 - assert po.view.altitude_mode == AltitudeMode("relativeToGround") class TestGroundOverlayLxml(Lxml, TestGroundOverlay): diff --git a/tests/registry_test.py b/tests/registry_test.py index 26ac2cb0..083b3fcc 100644 --- a/tests/registry_test.py +++ b/tests/registry_test.py @@ -20,7 +20,6 @@ from typing import Optional from typing import Tuple from typing import Type -from typing import Union from fastkml.base import _XMLObject from fastkml.enums import Verbosity @@ -28,15 +27,6 @@ from fastkml.registry import RegistryItem from fastkml.types import Element -known_types = Union[ - Type[_XMLObject], - Type[Enum], - Type[bool], - Type[int], - Type[str], - Type[float], -] - class A(_XMLObject): """A test class.""" @@ -66,6 +56,7 @@ def set_element( node_name: str, precision: Optional[int], verbosity: Optional[Verbosity], + default: Any, ) -> None: """Get an attribute from an XML object.""" @@ -77,7 +68,7 @@ def get_kwarg( # type: ignore[empty-body] name_spaces: Dict[str, str], node_name: str, kwarg: str, - classes: Tuple[known_types, ...], + classes: Tuple[Type[object], ...], strict: bool, ) -> Dict[str, Any]: """Get the kwarg for the constructor from the element.""" diff --git a/tests/repr_eq_test.py b/tests/repr_eq_test.py index 5f88e0c0..65d827bf 100644 --- a/tests/repr_eq_test.py +++ b/tests/repr_eq_test.py @@ -1946,7 +1946,7 @@ def test_str(self) -> None: def test_eq_str_round_trip(self) -> None: """Test the equality of the original and the round-tripped document.""" - new_doc = fastkml.KML.class_from_string(self.clean_doc.to_string(precision=15)) + new_doc = fastkml.KML.from_string(self.clean_doc.to_string(precision=15)) assert str(self.clean_doc) == str(new_doc) assert repr(new_doc) == repr(self.clean_doc) diff --git a/tests/styles_test.py b/tests/styles_test.py index 144bdc97..e8c2e348 100644 --- a/tests/styles_test.py +++ b/tests/styles_test.py @@ -19,11 +19,13 @@ from fastkml import links from fastkml import styles +from fastkml.containers import Document from fastkml.enums import ColorMode from fastkml.enums import DisplayMode from fastkml.enums import PairKey from fastkml.enums import Units from fastkml.exceptions import KMLParseError +from fastkml.features import Placemark from tests.base import Lxml from tests.base import StdLibrary @@ -42,7 +44,7 @@ def test_style_url(self) -> None: assert ">#style-0" in serialized def test_style_url_read(self) -> None: - url = styles.StyleUrl.class_from_string( + url = styles.StyleUrl.from_string( '#style-0', ) @@ -111,7 +113,7 @@ def test_icon_style_with_hot_spot(self) -> None: assert "href" not in serialized def test_icon_style_read(self) -> None: - icons = styles.IconStyle.class_from_string( + icons = styles.IconStyle.from_string( '' "ff2200ffrandom" @@ -134,7 +136,7 @@ def test_icon_style_read(self) -> None: assert icons.hot_spot.yunits.value == "insetPixels" def test_icon_style_with_hot_spot_enum_relaxed(self) -> None: - icons = styles.IconStyle.class_from_string( + icons = styles.IconStyle.from_string( '' "ff2200ffrandom" @@ -149,7 +151,7 @@ def test_icon_style_with_hot_spot_enum_relaxed(self) -> None: def test_icon_style_with_hot_spot_enum_strict(self) -> None: with pytest.raises(KMLParseError): - styles.IconStyle.class_from_string( + styles.IconStyle.from_string( '' "ff2200ffrandom" @@ -179,7 +181,7 @@ def test_line_style(self) -> None: assert "" in serialized def test_line_style_read(self) -> None: - lines = styles.LineStyle.class_from_string( + lines = styles.LineStyle.from_string( '\n' " ffaa00ff\n" @@ -216,7 +218,7 @@ def test_poly_style(self) -> None: assert "" in serialized def test_poly_style_read(self) -> None: - ps = styles.PolyStyle.class_from_string( + ps = styles.PolyStyle.from_string( '' "ffaabbff" @@ -255,7 +257,7 @@ def test_label_style(self) -> None: assert "" in serialized def test_label_style_read(self) -> None: - ls = styles.LabelStyle.class_from_string( + ls = styles.LabelStyle.from_string( '' "ff001122" @@ -294,7 +296,7 @@ def test_balloon_style(self) -> None: assert "" in serialized def test_balloon_style_read(self) -> None: - bs = styles.BalloonStyle.class_from_string( + bs = styles.BalloonStyle.from_string( '' "7fff1144" @@ -374,7 +376,7 @@ def test_style(self) -> None: assert "" in serialized def test_style_read(self) -> None: - style = styles.Style.class_from_string( + style = styles.Style.from_string( '' '' @@ -555,7 +557,7 @@ def test_stylemap(self) -> None: # noqa: PLR0915 assert "" in serialized def test_stylemap_read(self) -> None: - sm = styles.StyleMap.class_from_string( + sm = styles.StyleMap.from_string( """ @@ -610,6 +612,82 @@ def test_stylemap_read(self) -> None: assert sm.highlight.id == "id-u0" assert sm.highlight.target_id == "target-u0" + def test_style_map_none_case(self) -> None: + sm = styles.StyleMap() + + assert sm.normal is None + assert sm.highlight is None + + +class TestStyleUsage: + def test_create_document_style(self) -> None: + style = styles.Style( + styles=[ + styles.PolyStyle( + color="7f000000", + fill=True, + outline=True, + ), + ], + ) + + doc = Document(styles=[style]) + + doc2 = Document() + doc2.styles.append(style) + + expected = """ + + + + + 7f000000 + 1 + 1 + + + + """ + + doc3 = Document.from_string(expected) + + assert doc.to_string() == doc2.to_string() + assert doc2.to_string() == doc3.to_string() + assert doc.to_string() == doc3.to_string() + + def test_create_placemark_style(self) -> None: + style = styles.Style( + styles=[ + styles.PolyStyle( + color="7f000000", + fill=True, + outline=True, + ), + ], + ) + + place = Placemark(styles=[style]) + + place2 = Placemark() + place2.styles.append(style) + + expected = """ + + + + 7f000000 + 1 + 1 + + + + """ + + place3 = Placemark.from_string(expected) + assert place.to_string() == place2.to_string() + assert place2.to_string() == place3.to_string() + assert place.to_string() == place3.to_string() + class TestLxml(Lxml, TestStdLibrary): """Test with lxml.""" diff --git a/tests/times_test.py b/tests/times_test.py index e32ca6a1..1876a003 100644 --- a/tests/times_test.py +++ b/tests/times_test.py @@ -1,4 +1,4 @@ -# Copyright (C) 2022 - 2023 Christian Ledermann +# Copyright (C) 2022 - 2024 Christian Ledermann # # This library is free software; you can redistribute it and/or modify it under # the terms of the GNU Lesser General Public License as published by the Free @@ -32,7 +32,7 @@ class TestDateTime(StdLibrary): """KmlDateTime implementation is independent of XML parser.""" def test_kml_datetime_year(self) -> None: - dt = datetime.datetime(2000, 1, 1) + dt = datetime.datetime(2000, 1, 1) # noqa: DTZ001 kdt = KmlDateTime(dt, DateTimeResolution.year) @@ -41,7 +41,7 @@ def test_kml_datetime_year(self) -> None: assert bool(kdt) def test_kml_datetime_year_month(self) -> None: - dt = datetime.datetime(2000, 3, 1) + dt = datetime.datetime(2000, 3, 1) # noqa: DTZ001 kdt = KmlDateTime(dt, DateTimeResolution.year_month) @@ -50,7 +50,7 @@ def test_kml_datetime_year_month(self) -> None: assert bool(kdt) def test_kml_datetime_date(self) -> None: - dt = datetime.datetime.now() + dt = datetime.datetime.now() # noqa: DTZ005 kdt = KmlDateTime(dt, DateTimeResolution.date) @@ -59,7 +59,7 @@ def test_kml_datetime_date(self) -> None: assert bool(kdt) def test_kml_datetime_date_implicit(self) -> None: - dt = datetime.date.today() + dt = datetime.date.today() # noqa: DTZ011 kdt = KmlDateTime(dt) @@ -68,7 +68,7 @@ def test_kml_datetime_date_implicit(self) -> None: assert bool(kdt) def test_kml_datetime_datetime(self) -> None: - dt = datetime.datetime.now() + dt = datetime.datetime.now() # noqa: DTZ005 kdt = KmlDateTime(dt, DateTimeResolution.datetime) @@ -77,7 +77,7 @@ def test_kml_datetime_datetime(self) -> None: assert bool(kdt) def test_kml_datetime_datetime_implicit(self) -> None: - dt = datetime.datetime.now() + dt = datetime.datetime.now() # noqa: DTZ005 kdt = KmlDateTime(dt) @@ -91,64 +91,80 @@ def test_kml_datetime_no_datetime(self) -> None: assert kdt.resolution == DateTimeResolution.date assert not bool(kdt) - with pytest.raises(AttributeError): + with pytest.raises( + AttributeError, + match="^'NoneType' object has no attribute 'isoformat'$", + ): str(kdt) def test_parse_year(self) -> None: dt = KmlDateTime.parse("2000") + assert dt assert dt.resolution == DateTimeResolution.year assert dt.dt == datetime.datetime(2000, 1, 1, tzinfo=tzutc()) def test_parse_year_0(self) -> None: - with pytest.raises(ValueError): + with pytest.raises( + ValueError, + match="^year 0 is out of range$|year must be in 1..9999", + ): KmlDateTime.parse("0000") def test_parse_year_month(self) -> None: dt = KmlDateTime.parse("2000-03") + assert dt assert dt.resolution == DateTimeResolution.year_month assert dt.dt == datetime.datetime(2000, 3, 1, tzinfo=tzutc()) def test_parse_year_month_no_dash(self) -> None: dt = KmlDateTime.parse("200004") + assert dt assert dt.resolution == DateTimeResolution.year_month assert dt.dt == datetime.datetime(2000, 4, 1, tzinfo=tzutc()) def test_parse_year_month_0(self) -> None: - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="month must be in 1..12"): KmlDateTime.parse("2000-00") def test_parse_year_month_13(self) -> None: - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="month must be in 1..12"): KmlDateTime.parse("2000-13") def test_parse_year_month_day(self) -> None: dt = KmlDateTime.parse("2000-03-01") + assert dt assert dt.resolution == DateTimeResolution.date assert dt.dt == datetime.datetime(2000, 3, 1, tzinfo=tzutc()) def test_parse_year_month_day_no_dash(self) -> None: dt = KmlDateTime.parse("20000401") + assert dt assert dt.resolution == DateTimeResolution.date assert dt.dt == datetime.datetime(2000, 4, 1, tzinfo=tzutc()) def test_parse_year_month_day_0(self) -> None: - with pytest.raises(ValueError): + with pytest.raises( + ValueError, + match="^day is out of range for month$|day must be in 1..31", + ): KmlDateTime.parse("2000-05-00") def test_parse_datetime_utc(self) -> None: dt = KmlDateTime.parse("1997-07-16T07:30:15Z") + assert dt assert dt.resolution == DateTimeResolution.datetime assert dt.dt == datetime.datetime(1997, 7, 16, 7, 30, 15, tzinfo=tzutc()) def test_parse_datetime_with_tz(self) -> None: dt = KmlDateTime.parse("1997-07-16T07:30:15+01:00") + assert dt assert dt.resolution == DateTimeResolution.datetime assert dt.dt == datetime.datetime( 1997, @@ -163,6 +179,7 @@ def test_parse_datetime_with_tz(self) -> None: def test_parse_datetime_with_tz_no_colon(self) -> None: dt = KmlDateTime.parse("1997-07-16T07:30:15+0100") + assert dt assert dt.resolution == DateTimeResolution.datetime assert dt.dt == datetime.datetime( 1997, @@ -177,6 +194,7 @@ def test_parse_datetime_with_tz_no_colon(self) -> None: def test_parse_datetime_no_tz(self) -> None: dt = KmlDateTime.parse("1997-07-16T07:30:15") + assert dt assert dt.resolution == DateTimeResolution.datetime assert dt.dt == datetime.datetime(1997, 7, 16, 7, 30, 15, tzinfo=tzutc()) @@ -192,9 +210,12 @@ class TestStdLibrary(StdLibrary): """Test with the standard library.""" def test_timestamp(self) -> None: - now = datetime.datetime.now() + now = datetime.datetime.now() # noqa: DTZ005 dt = KmlDateTime(now) + ts = kml.TimeStamp(timestamp=dt) + + assert ts.timestamp assert ts.timestamp.dt == now assert ts.timestamp.resolution == DateTimeResolution.datetime assert "TimeStamp>" in str(ts.to_string()) @@ -206,9 +227,11 @@ def test_timestamp(self) -> None: assert "2000-01-01" in str(ts.to_string()) def test_timespan(self) -> None: - now = KmlDateTime(datetime.datetime.now()) - y2k = KmlDateTime(datetime.datetime(2000, 1, 1)) + now = KmlDateTime(datetime.datetime.now()) # noqa: DTZ005 + y2k = KmlDateTime(datetime.datetime(2000, 1, 1)) # noqa: DTZ001 + ts = kml.TimeSpan(end=now, begin=y2k) + assert ts.end == now assert ts.begin == y2k assert "TimeSpan>" in str(ts.to_string()) @@ -224,9 +247,12 @@ def test_timespan(self) -> None: assert not ts def test_feature_timestamp(self) -> None: - now = datetime.datetime.now() + now = datetime.datetime.now() # noqa: DTZ005 f = kml.Document() + f.times = kml.TimeStamp(timestamp=KmlDateTime(now)) + + assert f.time_stamp assert f.time_stamp.dt == now assert now.isoformat() in str(f.to_string()) assert "TimeStamp>" in str(f.to_string()) @@ -238,8 +264,8 @@ def test_feature_timestamp(self) -> None: assert "TimeStamp>" not in str(f.to_string()) def test_feature_timespan(self) -> None: - now = datetime.datetime.now() - y2k = datetime.datetime(2000, 1, 1) + now = datetime.datetime.now() # noqa: DTZ005 + y2k = datetime.datetime(2000, 1, 1) # noqa: DTZ001 f = kml.Document() f.times = kml.TimeSpan(begin=KmlDateTime(y2k), end=KmlDateTime(now)) assert f.begin == KmlDateTime(y2k) @@ -265,8 +291,9 @@ def test_read_timestamp_year(self) -> None: """ - ts = kml.TimeStamp.class_from_string(doc, ns="") + ts = kml.TimeStamp.from_string(doc, ns="") + assert ts.timestamp assert ts.timestamp.resolution == DateTimeResolution.year assert ts.timestamp.dt == datetime.datetime(1997, 1, 1, 0, 0, tzinfo=tzutc()) @@ -277,8 +304,9 @@ def test_read_timestamp_year_month(self) -> None: """ - ts = kml.TimeStamp.class_from_string(doc, ns="") + ts = kml.TimeStamp.from_string(doc, ns="") + assert ts.timestamp assert ts.timestamp.resolution == DateTimeResolution.year_month assert ts.timestamp.dt == datetime.datetime(1997, 7, 1, 0, 0, tzinfo=tzutc()) @@ -289,8 +317,9 @@ def test_read_timestamp_ym_no_hyphen(self) -> None: """ - ts = kml.TimeStamp.class_from_string(doc, ns="") + ts = kml.TimeStamp.from_string(doc, ns="") + assert ts.timestamp assert ts.timestamp.resolution == DateTimeResolution.year_month assert ts.timestamp.dt == datetime.datetime(1998, 8, 1, 0, 0, tzinfo=tzutc()) @@ -301,8 +330,9 @@ def test_read_timestamp_ymd(self) -> None: """ - ts = kml.TimeStamp.class_from_string(doc, ns="") + ts = kml.TimeStamp.from_string(doc, ns="") + assert ts.timestamp assert ts.timestamp.resolution == DateTimeResolution.date assert ts.timestamp.dt == datetime.datetime(1997, 7, 16, 0, 0, tzinfo=tzutc()) @@ -316,8 +346,9 @@ def test_read_timestamp_utc(self) -> None: """ - ts = kml.TimeStamp.class_from_string(doc, ns="") + ts = kml.TimeStamp.from_string(doc, ns="") + assert ts.timestamp assert ts.timestamp.resolution == DateTimeResolution.datetime assert ts.timestamp.dt == datetime.datetime( 1997, @@ -336,8 +367,9 @@ def test_read_timestamp_utc_offset(self) -> None: """ - ts = kml.TimeStamp.class_from_string(doc, ns="") + ts = kml.TimeStamp.from_string(doc, ns="") + assert ts.timestamp assert ts.timestamp.resolution == DateTimeResolution.datetime assert ts.timestamp.dt == datetime.datetime( 1997, @@ -357,10 +389,12 @@ def test_read_timespan(self) -> None: """ - ts = kml.TimeSpan.class_from_string(doc, ns="") + ts = kml.TimeSpan.from_string(doc, ns="") + assert ts.begin assert ts.begin.resolution == DateTimeResolution.date assert ts.begin.dt == datetime.datetime(1876, 8, 1, 0, 0, tzinfo=tzutc()) + assert ts.end assert ts.end.resolution == DateTimeResolution.datetime assert ts.end.dt == datetime.datetime(1997, 7, 16, 7, 30, 15, tzinfo=tzutc()) @@ -377,10 +411,12 @@ def test_feature_fromstring(self) -> None: """ - d = kml.Document.class_from_string(doc, ns="") + d = kml.Document.from_string(doc, ns="") assert d.time_stamp is None + assert d.begin assert d.begin.dt == datetime.datetime(1876, 8, 1, 0, 0, tzinfo=tzutc()) + assert d.end assert d.end.dt == datetime.datetime(1997, 7, 16, 7, 30, 15, tzinfo=tzutc()) diff --git a/tests/utils_test.py b/tests/utils_test.py new file mode 100644 index 00000000..b7bb79a6 --- /dev/null +++ b/tests/utils_test.py @@ -0,0 +1,215 @@ +# 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 utils module.""" +from typing import List + +from fastkml import Schema +from fastkml import SchemaData +from fastkml import kml +from fastkml.utils import find +from fastkml.utils import find_all +from tests.base import Lxml +from tests.base import StdLibrary + + +class TestFindAll(StdLibrary): + """Test the find_all function.""" + + def test_find_all(self) -> None: + + class A: + def __init__(self, x: int) -> None: + self.x = x + + class B: + def __init__(self, y: int) -> None: + self.y = y + + class C: + def __init__(self, z: int) -> None: + self.z = z + + class D: + def __init__(self, a: A, b: B, c: C) -> None: + self.a = a + self.b = b + self.c = c + + a1 = A(1) + a2 = A(2) + b1 = B(1) + b2 = B(2) + c1 = C(1) + c2 = C(2) + d1 = D(a1, b1, c1) + d2 = D(a2, b2, c2) + + result = list(find_all(d1, of_type=A)) + assert result == [a1] + + result = list(find_all(d1, of_type=B)) + assert result == [b1] + + result = list(find_all(d1, of_type=C)) + assert result == [c1] + + result = list(find_all(d1, of_type=D)) + assert result == [d1] + + result = list(find_all(d1, of_type=A, x=1)) + assert result == [a1] + + result = list(find_all(d1, of_type=A, x=2)) + assert not result + + result = list(find_all(d2, of_type=A, x=2)) + assert result == [a2] + + def test_find_all_empty(self) -> None: + result = list(find_all(None, of_type=None)) + assert result == [None] + + def test_find_all_no_type(self) -> None: + class A: + def __init__(self, x: int) -> None: + self.x = x + + a1 = A(1) + + result = list(find_all(a1, of_type=None)) + assert result == [a1, 1] + + def test_find_all_no_type_attr(self) -> None: + class A: + def __init__(self, x: int, y: int) -> None: + self.x = x + self.y = y + + class B: + def __init__(self, a: List[A]) -> None: + self.a = a + + a1 = A(1, 0) + a2 = A(0, 1) + a3 = A(1, 1) + b = B([a1, a2, a3]) + + assert list(find_all(b, x=1)) == [a1, a3] + assert list(find_all(b, y=1)) == [a2, a3] + assert list(find_all(b, x=0)) == [a2] + assert list(find_all(b, x=1, y=1)) == [a3] + + def test_find_no_type_attr(self) -> None: + class A: + def __init__(self, x: int, y: int) -> None: + self.x = x + self.y = y + + class B: + def __init__(self, a: List[A]) -> None: + self.a = a + + a1 = A(1, 0) + a2 = A(0, 1) + a3 = A(1, 1) + b = B([a1, a2, a3]) + + assert find(b, x=1) == a1 + assert find(b, y=1) == a2 + assert find(b, x=0) == a2 + assert find(b, x=1, y=1) == a3 + + def test_find_schema_by_url(self) -> None: + doc = ( + '' + "" + "ExtendedData+SchemaData" + "1" + "" + '" + '' + '' + '' + "Trail Head Name]]>" + "" + '' + "The length in miles]]>" + "" + '' + "change in altitude]]>" + "" + "" + "" + "" + "Easy trail" + "#trailhead-balloon-template" + "" + '' + 'Pi in the sky' + '3.14159' + '10' + "" + "" + "" + "-122.000,37.002" + "" + "" + "" + "Difficult trail" + "#trailhead-balloon-template" + "" + '' + 'Mount Everest' + '347.45' + '10000' + "" + "" + "" + "-121.998,37.0078" + "" + "" + "" + "" + ) + k = kml.KML.from_string(doc, strict=False) + + schema = find(k, of_type=Schema, id="TrailHeadTypeId") + schema_data = list(find_all(k, of_type=SchemaData)) + + assert isinstance(schema, Schema) + assert schema.name == "TrailHeadType" + assert schema.id == "TrailHeadTypeId" + assert len(schema_data) == 2 + for data in schema_data: + assert isinstance(data, SchemaData) + assert data.schema_url == "#TrailHeadTypeId" + + +class TestFindAllLxml(Lxml): + """Run the tests using lxml.""" diff --git a/tests/validator_test.py b/tests/validator_test.py new file mode 100644 index 00000000..13a5d99c --- /dev/null +++ b/tests/validator_test.py @@ -0,0 +1,108 @@ +# Copyright (C) 2024 Christian Ledermann +# +# This library is free software; you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +"""Test the validator module.""" +from pathlib import Path +from typing import Final + +import pytest + +from fastkml import atom +from fastkml.validator import get_schema_parser +from fastkml.validator import validate +from tests.base import Lxml +from tests.base import StdLibrary + +TEST_DIR: Final = Path(__file__).parent + + +class TestStdLibrary(StdLibrary): + + def setup_method(self) -> None: + """Invalidate the cache before each test.""" + get_schema_parser.cache_clear() + super().setup_method() + + def test_validate(self) -> None: + assert ( + validate( + file_to_validate=TEST_DIR + / "ogc_conformance" + / "data" + / "kml" + / "Document-clean.kml", + ) + is None + ) + + def test_validate_require_element_or_path(self) -> None: + with pytest.raises( + ValueError, + match="^Either element or file_to_validate must be provided.$", + ): + validate() + + def test_validate_mutual_exclusive_element_and_path(self) -> None: + with pytest.raises( + ValueError, + match="^Only one of element and file_to_validate can be provided.$", + ): + validate( + element=atom.Link().etree_element(), + file_to_validate=TEST_DIR / "test.xml", + ) + + +class TestLxml(Lxml): + + def setup_method(self) -> None: + """Invalidate the cache before each test.""" + get_schema_parser.cache_clear() + super().setup_method() + + def test_validate(self) -> None: + assert validate( + file_to_validate=TEST_DIR + / "ogc_conformance" + / "data" + / "kml" + / "Document-clean.kml", + ) + + def test_validate_element(self) -> None: + link = atom.Link( + ns="{http://www.w3.org/2005/Atom}", + href="#here", + rel="alternate", + type="text/html", + hreflang="en", + title="Title", + length=3456, + ) + assert validate(element=link.etree_element()) + + def test_validate_invalid_element(self) -> None: + link = atom.Link( + ns="{http://www.w3.org/2005/Atom}", + href="", + rel="alternate", + type="text/html", + hreflang="en", + title="Title", + length=3456, + ) + + with pytest.raises(AssertionError): + validate(element=link.etree_element()) diff --git a/tests/views_test.py b/tests/views_test.py index bd185b84..097dfaae 100644 --- a/tests/views_test.py +++ b/tests/views_test.py @@ -87,7 +87,7 @@ def test_camera_read(self) -> None: "" ) - camera = views.Camera.class_from_string(camera_xml) + camera = views.Camera.from_string(camera_xml) assert camera.heading == 10 assert camera.tilt == 20 @@ -156,7 +156,7 @@ def test_look_at_read(self) -> None: "30" "" ) - look_at = views.LookAt.class_from_string(look_at_xml) + look_at = views.LookAt.from_string(look_at_xml) assert look_at.heading == 10 assert look_at.tilt == 20 @@ -209,6 +209,7 @@ def test_region_with_all_optional_parameters(self) -> None: assert region.lod.min_fade_extent == 0 assert region.lod.max_fade_extent == 512 assert region + assert bool(region) def test_region_read(self) -> None: doc = ( @@ -224,7 +225,7 @@ def test_region_read(self) -> None: "512" ) - region = views.Region.class_from_string(doc) + region = views.Region.from_string(doc) assert region.id == "region1" assert region.lat_lon_alt_box.north == 37.85