diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index d3629c79..d3b17031 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -43,7 +43,7 @@ repos:
hooks:
- id: absolufy-imports
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: 'v0.7.4'
+ rev: 'v0.8.0'
hooks:
- id: ruff
- id: ruff-format
diff --git a/README.rst b/README.rst
index 3fe99453..55ecf1bc 100644
--- a/README.rst
+++ b/README.rst
@@ -3,13 +3,15 @@ Introduction
.. inclusion-marker-do-not-remove
-KML is an XML geospatial data format and an OGC_ standard that deserves a canonical python implementation.
+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 `Marble `_,
+`NASA WorldWind `_,
`Cesium JS `_, `OpenLayers `_,
`Google Maps `_, and
`Google Earth `_ support.
@@ -18,9 +20,12 @@ For more details about the KML Specification, check out the `KML Reference
`_ on the Google
developers site.
-Geometries are handled as pygeoif_ objects.
+Geometries are handled as pygeoif_ objects, which are compatible with any geometry that
+implements the ``__geo_interface__`` protocol, such as shapely_.
-Fastkml is continually tested
+Fastkml is tested on `CPython `_ and
+`PyPy `_, but it should work on alternative
+Python implementations (that implement the language specification *>=3.8*) as well.
|test| |hypothesis| |cov| |black| |mypy| |commit|
@@ -48,9 +53,9 @@ Fastkml is continually tested
:target: https://github.com/pre-commit/pre-commit
:alt: pre-commit
-Is Maintained and documented:
+Is maintained and documented:
-|pypi| |status| |license| |doc| |stats| |pyversion| |pyimpl| |dependencies| |downloads|
+|pypi| |conda-forge| |status| |license| |doc| |stats| |pyversion| |pyimpl| |dependencies| |downloads|
.. |pypi| image:: https://img.shields.io/pypi/v/fastkml.svg
:target: https://pypi.python.org/pypi/fastkml
@@ -88,6 +93,9 @@ Is Maintained and documented:
:target: https://pepy.tech/project/fastkml
:alt: Downloads
+.. |conda-forge| image:: https://img.shields.io/conda/vn/conda-forge/fastkml.svg
+ :target: https://anaconda.org/conda-forge/fastkml
+ :alt: Conda-Forge
Documentation
=============
@@ -130,3 +138,4 @@ Please submit a PR with the features you'd like to see implemented.
.. _lxml: https://pypi.python.org/pypi/lxml
.. _arrow: https://pypi.python.org/pypi/arrow
.. _OGC: https://www.ogc.org/standard/kml/
+.. _shapely: https://shapely.readthedocs.io/
diff --git a/_typos.toml b/_typos.toml
index a1cb930e..269e7f28 100644
--- a/_typos.toml
+++ b/_typos.toml
@@ -1,3 +1,12 @@
+[default]
+extend-ignore-identifiers-re = [
+ "04AFE6060F147CE66FBD",
+ "Lod",
+ "lod",
+]
+
+
+
[default.extend-words]
lod = "lod"
Lod = "Lod"
diff --git a/docs/HISTORY.rst b/docs/HISTORY.rst
index 4d742416..4e171fa8 100644
--- a/docs/HISTORY.rst
+++ b/docs/HISTORY.rst
@@ -1,7 +1,15 @@
Changelog
=========
-1.0 (unreleased)
+1.1.0 (unreleased)
+----------------------
+
+- Add support for ScreenOverlay and Model.
+- allow parsing kml files without namespace declarations.
+- Add support for NetworkLinkControl. [Apurva Banka]
+
+
+1.0 (2024/11/19)
-----------------
- Drop Python 2 support
@@ -11,6 +19,7 @@ Changelog
- refactor
- Use arrow instead of dateutil
- Add an informative ``__repr__``
+- Change the ``from_string`` method to a class method which returns a new instance.
0.12 (2020/09/23)
-----------------
diff --git a/docs/Makefile b/docs/Makefile
index 38879a67..d4bb2cbb 100644
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -1,177 +1,20 @@
-# Makefile for Sphinx documentation
+# Minimal makefile for Sphinx documentation
#
-# You can set these variables from the command line.
-SPHINXOPTS =
-SPHINXBUILD = sphinx-build
-PAPER =
+# You can set these variables from the command line, and also
+# from the environment for the first two.
+SPHINXOPTS ?=
+SPHINXBUILD ?= sphinx-build
+SOURCEDIR = .
BUILDDIR = _build
-# User-friendly check for sphinx-build
-ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
-$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
-endif
-
-# Internal variables.
-PAPEROPT_a4 = -D latex_paper_size=a4
-PAPEROPT_letter = -D latex_paper_size=letter
-ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
-# the i18n builder cannot share the environment and doctrees with the others
-I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
-
-.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
-
+# Put it first so that "make" without argument is like "make help".
help:
- @echo "Please use \`make ' where is one of"
- @echo " html to make standalone HTML files"
- @echo " dirhtml to make HTML files named index.html in directories"
- @echo " singlehtml to make a single large HTML file"
- @echo " pickle to make pickle files"
- @echo " json to make JSON files"
- @echo " htmlhelp to make HTML files and a HTML help project"
- @echo " qthelp to make HTML files and a qthelp project"
- @echo " devhelp to make HTML files and a Devhelp project"
- @echo " epub to make an epub"
- @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
- @echo " latexpdf to make LaTeX files and run them through pdflatex"
- @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
- @echo " text to make text files"
- @echo " man to make manual pages"
- @echo " texinfo to make Texinfo files"
- @echo " info to make Texinfo files and run them through makeinfo"
- @echo " gettext to make PO message catalogs"
- @echo " changes to make an overview of all changed/added/deprecated items"
- @echo " xml to make Docutils-native XML files"
- @echo " pseudoxml to make pseudoxml-XML files for display purposes"
- @echo " linkcheck to check all external links for integrity"
- @echo " doctest to run all doctests embedded in the documentation (if enabled)"
-
-clean:
- rm -rf $(BUILDDIR)/*
-
-html:
- $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
- @echo
- @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
-
-dirhtml:
- $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
- @echo
- @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
-
-singlehtml:
- $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
- @echo
- @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
-
-pickle:
- $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
- @echo
- @echo "Build finished; now you can process the pickle files."
-
-json:
- $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
- @echo
- @echo "Build finished; now you can process the JSON files."
-
-htmlhelp:
- $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
- @echo
- @echo "Build finished; now you can run HTML Help Workshop with the" \
- ".hhp project file in $(BUILDDIR)/htmlhelp."
-
-qthelp:
- $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
- @echo
- @echo "Build finished; now you can run "qcollectiongenerator" with the" \
- ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
- @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/FastKML.qhcp"
- @echo "To view the help file:"
- @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/FastKML.qhc"
-
-devhelp:
- $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
- @echo
- @echo "Build finished."
- @echo "To view the help file:"
- @echo "# mkdir -p $$HOME/.local/share/devhelp/FastKML"
- @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/FastKML"
- @echo "# devhelp"
-
-epub:
- $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
- @echo
- @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
-
-latex:
- $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
- @echo
- @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
- @echo "Run \`make' in that directory to run these through (pdf)latex" \
- "(use \`make latexpdf' here to do that automatically)."
-
-latexpdf:
- $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
- @echo "Running LaTeX files through pdflatex..."
- $(MAKE) -C $(BUILDDIR)/latex all-pdf
- @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
-
-latexpdfja:
- $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
- @echo "Running LaTeX files through platex and dvipdfmx..."
- $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
- @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
-
-text:
- $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
- @echo
- @echo "Build finished. The text files are in $(BUILDDIR)/text."
-
-man:
- $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
- @echo
- @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
-
-texinfo:
- $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
- @echo
- @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
- @echo "Run \`make' in that directory to run these through makeinfo" \
- "(use \`make info' here to do that automatically)."
-
-info:
- $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
- @echo "Running Texinfo files through makeinfo..."
- make -C $(BUILDDIR)/texinfo info
- @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
-
-gettext:
- $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
- @echo
- @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
-
-changes:
- $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
- @echo
- @echo "The overview file is in $(BUILDDIR)/changes."
-
-linkcheck:
- $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
- @echo
- @echo "Link check complete; look for any errors in the above output " \
- "or in $(BUILDDIR)/linkcheck/output.txt."
-
-doctest:
- $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
- @echo "Testing of doctests in the sources finished, look at the " \
- "results in $(BUILDDIR)/doctest/output.txt."
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
-xml:
- $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
- @echo
- @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
+.PHONY: help Makefile
-pseudoxml:
- $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
- @echo
- @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/docs/conf.py b/docs/conf.py
index 53caa139..07251bbd 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -1,33 +1,36 @@
+# noqa: INP001, D100
+# Configuration file for the Sphinx documentation builder.
#
-# FastKML documentation build configuration file, created by
-# sphinx-quickstart on Mon Oct 13 22:24:07 2014.
-#
-# This file is execfile()d with the current directory set to its
-# containing dir.
-#
-# Note that not all possible configuration values are present in this
-# autogenerated file.
-#
-# All configuration values have a default; values that are commented out
-# serve to show the default.
+# For the full list of built-in configuration values, see the documentation:
+# https://www.sphinx-doc.org/en/master/usage/configuration.html
-import os
+# -- Project information -----------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
+
+import pathlib
import sys
+DOC_ROOT = pathlib.Path(__file__).parent
+PROJECT_ROOT = DOC_ROOT.parent
+
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
-sys.path.insert(0, os.path.abspath(".."))
-from fastkml import about
+sys.path.insert(0, str(PROJECT_ROOT))
+from fastkml import about # noqa: E402
-# -- General configuration ------------------------------------------------
+# General information about the project.
+project = "FastKML"
+copyright = "2014 -2024, Christian Ledermann & Ian Lee" # noqa: A001
+author = "Christian Ledermann"
+# The short X.Y version.
+version = ".".join(about.__version__.split(".")[:2])
+# The full version, including alpha/beta/rc tags.
+release = about.__version__
-# If your documentation needs a minimal Sphinx version, state it here.
-# needs_sphinx = '1.0'
+# -- General configuration ---------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
-# Add any Sphinx extension module names here, as strings. They can be
-# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
-# ones.
extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.doctest",
@@ -38,75 +41,21 @@
]
autosummary_generate = True
-# Add any paths that contain templates here, relative to this directory.
-templates_path = ["_templates"]
-
-# The suffix of source filenames.
-source_suffix = ".rst"
-
-# The encoding of source files.
-# source_encoding = 'utf-8-sig'
-
-# The master toctree document.
-master_doc = "index"
-# General information about the project.
-project = "FastKML"
-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 = ".".join(about.__version__.split(".")[:2])
-# The full version, including alpha/beta/rc tags.
-release = about.__version__
-
-# The language for content autogenerated by Sphinx. Refer to documentation
-# for a list of supported languages.
-# language = None
-
-# There are two options for replacing |today|: either, you set today to some
-# non-false value, then it is used:
-# today = ''
-# Else, today_fmt is used as the format for a strftime call.
-# today_fmt = '%B %d, %Y'
-
-# List of patterns, relative to source directory, that match files and
-# directories to ignore when looking for source files.
-exclude_patterns = ["_build"]
-
-# The reST default role (used for this markup: `text`) to use for all
-# documents.
-# default_role = None
+templates_path = ["_templates"]
+exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
-# If true, '()' will be appended to :func: etc. cross-reference text.
-# add_function_parentheses = True
+root_doc = "index"
-# If true, the current module name will be prepended to all description
-# unit titles (such as .. function::).
-# add_module_names = True
-# If true, sectionauthor and moduleauthor directives will be shown in the
-# output. They are ignored by default.
-# show_authors = False
+# -- Options for HTML output -------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = "sphinx"
-# A list of ignored prefixes for module index sorting.
-# modindex_common_prefix = []
-
-# If true, keep warnings as "system message" paragraphs in the built documents.
-# keep_warnings = False
-
-
-# -- Options for HTML output ----------------------------------------------
-
-# The theme to use for HTML and HTML Help pages. See the documentation for
-# a list of builtin themes.
-html_theme = "default"
+html_theme = "alabaster"
+html_static_path = ["_static"]
try:
import sphinx_rtd_theme
@@ -115,67 +64,6 @@
except ImportError:
pass
-# Theme options are theme-specific and customize the look and feel of a theme
-# further. For a list of options available for each theme, see the
-# documentation.
-# html_theme_options = {}
-
-# Add any paths that contain custom themes here, relative to this directory.
-# html_theme_path = []
-
-# The name for this set of Sphinx documents. If None, it defaults to
-# " v documentation".
-# html_title = None
-
-# A shorter title for the navigation bar. Default is the same as html_title.
-# html_short_title = None
-
-# The name of an image file (relative to this directory) to place at the top
-# of the sidebar.
-# html_logo = None
-
-# The name of an image file (within the static path) to use as favicon of the
-# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
-# pixels large.
-# html_favicon = None
-
-# Add any paths that contain custom static files (such as style sheets) here,
-# relative to this directory. They are copied after the builtin static files,
-# so a file named "default.css" will overwrite the builtin "default.css".
-# html_static_path = ['_static']
-
-# Add any extra paths that contain custom files (such as robots.txt or
-# .htaccess) here, relative to this directory. These files are copied
-# directly to the root of the documentation.
-# html_extra_path = []
-
-# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
-# using the given strftime format.
-# html_last_updated_fmt = '%b %d, %Y'
-
-# If true, SmartyPants will be used to convert quotes and dashes to
-# typographically correct entities.
-# html_use_smartypants = True
-
-# Custom sidebar templates, maps document names to template names.
-# html_sidebars = {}
-
-# Additional templates that should be rendered to pages, maps page names to
-# template names.
-# html_additional_pages = {}
-
-# If false, no module index is generated.
-# html_domain_indices = True
-
-# If false, no index is generated.
-# html_use_index = True
-
-# If true, the index is split into individual pages for each letter.
-# html_split_index = False
-
-# If true, links to the reST sources are added to the pages.
-# html_show_sourcelink = True
-
html_context = {
"display_github": True, # Integrate GitHub
"github_user": "cleder", # Username
@@ -183,107 +71,3 @@
"github_version": "main", # Version
"conf_py_path": "/docs/", # Path in the checkout to the docs root
}
-
-# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
-# html_show_sphinx = True
-
-# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
-# html_show_copyright = True
-
-# If true, an OpenSearch description file will be output, and all pages will
-# contain a tag referring to it. The value of this option must be the
-# base URL from which the finished HTML is served.
-# html_use_opensearch = ''
-
-# This is the file name suffix for HTML files (e.g. ".xhtml").
-# html_file_suffix = None
-
-# Output file base name for HTML help builder.
-htmlhelp_basename = "FastKMLdoc"
-
-
-# -- Options for LaTeX output ---------------------------------------------
-
-latex_elements = {
- # The paper size ('letterpaper' or 'a4paper').
- # 'papersize': 'letterpaper',
- # The font size ('10pt', '11pt' or '12pt').
- # 'pointsize': '10pt',
- # Additional stuff for the LaTeX preamble.
- # 'preamble': '',
-}
-
-# Grouping the document tree into LaTeX files. List of tuples
-# (source start file, target name, title,
-# author, documentclass [howto, manual, or own class]).
-latex_documents = [
- (
- "index",
- "FastKML.tex",
- "FastKML Documentation",
- r"Christian Ledermann \& Ian Lee",
- "manual",
- ),
-]
-
-# The name of an image file (relative to this directory) to place at the top of
-# the title page.
-# latex_logo = None
-
-# For "manual" documents, if this is true, then toplevel headings are parts,
-# not chapters.
-# latex_use_parts = False
-
-# If true, show page references after internal links.
-# latex_show_pagerefs = False
-
-# If true, show URL addresses after external links.
-# latex_show_urls = False
-
-# Documents to append as an appendix to all manuals.
-# latex_appendices = []
-
-# If false, no module index is generated.
-# latex_domain_indices = True
-
-
-# -- Options for manual page output ---------------------------------------
-
-# One entry per manual page. List of tuples
-# (source start file, name, description, authors, manual section).
-man_pages = [
- ("index", "fastkml", "FastKML Documentation", ["Christian Ledermann & Ian Lee"], 1),
-]
-
-# If true, show URL addresses after external links.
-# man_show_urls = False
-
-
-# -- Options for Texinfo output -------------------------------------------
-
-# Grouping the document tree into Texinfo files. List of tuples
-# (source start file, target name, title, author,
-# dir menu entry, description, category)
-texinfo_documents = [
- (
- "index",
- "FastKML",
- "FastKML Documentation",
- "Christian Ledermann & Ian Lee",
- "FastKML",
- "One line description of project.",
- "Miscellaneous",
- ),
-]
-
-# Documents to append as an appendix to all manuals.
-# texinfo_appendices = []
-
-# If false, no module index is generated.
-# texinfo_domain_indices = True
-
-# How to display URL addresses: 'footnote', 'no', or 'inline'.
-# texinfo_show_urls = 'footnote'
-
-# If true, do not generate a @detailmenu in the "Top" node's menu.
-# texinfo_no_detailmenu = False
diff --git a/docs/create_kml_files.rst b/docs/create_kml_files.rst
index f722aa20..d608a0ee 100644
--- a/docs/create_kml_files.rst
+++ b/docs/create_kml_files.rst
@@ -109,10 +109,8 @@ Finally, we create the KML object and write it to a file:
>>> document = fastkml.containers.Document(features=placemarks)
>>> 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)) # doctest: +ELLIPSIS
- ...
- 4...
+ >>> kml.write(outfile, prettyprint=True, precision=3) # doctest: +ELLIPSIS
+
The resulting KML file can be opened in Google Earth or any other KML viewer.
@@ -232,10 +230,7 @@ Finally, we create the KML object and write it to a file:
>>> 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)) # doctest: +ELLIPSIS
- ...
- 1...
+ >>> kml.write(outfile, prettyprint=True, precision=3)
You can open the resulting KML file in Google Earth Desktop and use the time slider to
diff --git a/docs/fastkml.rst b/docs/fastkml.rst
index 96553566..d4a13808 100644
--- a/docs/fastkml.rst
+++ b/docs/fastkml.rst
@@ -30,14 +30,13 @@ fastkml.registry
-----------------------
.. automodule:: fastkml.registry
- :members:
+ :members: RegistryItem,Registry
:undoc-members:
:show-inheritance:
-.. autoclass:: fastkml.registry::Registry
- :members: register, get
- :undoc-members:
- :show-inheritance:
+ .. autodata:: registry
+ :no-value:
+
fastkml.kml\_base
------------------------
@@ -158,6 +157,15 @@ fastkml.mixins
:undoc-members:
:show-inheritance:
+fastkml.model
+--------------------
+
+.. automodule:: fastkml.model
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+
fastkml.overlays
-----------------------
diff --git a/docs/index.rst b/docs/index.rst
index acdaf036..d438380c 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -26,7 +26,9 @@ requirements, namely:
create_kml_files
working_with_kml
configuration
+ upgrading
fastkml
contributing
+ kml
alternatives
HISTORY
diff --git a/docs/kml.rst b/docs/kml.rst
new file mode 100644
index 00000000..29f8133b
--- /dev/null
+++ b/docs/kml.rst
@@ -0,0 +1,13 @@
+KML Resources and Tutorials
+===========================
+
+Learning KML can be straightforward with the right resources.
+Here are some of the best sources:
+
+
+`Google Developers - KML `_ provides a comprehensive
+guide to KML, including tutorials, reference, and examples.
+
+
+The `FME Support Center's section on OGC and Google KML `_
+provides detailed guidance for creating, styling, and optimizing KML files.
diff --git a/docs/network.kml b/docs/network.kml
new file mode 100644
index 00000000..48f6004d
--- /dev/null
+++ b/docs/network.kml
@@ -0,0 +1,16 @@
+
+
+
+ 43200
+ -1
+
+ A snippet of XHTML
+ ]]>
+
+ 2008-05-30
+
+
\ No newline at end of file
diff --git a/docs/quickstart.rst b/docs/quickstart.rst
index 34b2355e..fb837a58 100644
--- a/docs/quickstart.rst
+++ b/docs/quickstart.rst
@@ -17,25 +17,24 @@ First we import the necessary modules:
>>> from fastkml import kml
>>> from pygeoif.geometry import Polygon
-Create a KML object and set the namespace:
+Create a KML object:
.. 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")
+ >>> d = kml.Document(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")
+ >>> f = kml.Folder(id="fid", name="f name", description="f description")
>>> d.append(f)
Create a KML Folder and nest it in the first Folder:
@@ -43,7 +42,7 @@ 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"
+ ... id="nested-fid", name="nested f name", description="nested f description"
... )
>>> f.append(nf)
@@ -51,7 +50,7 @@ Create a second KML Folder within the Document:
.. code-block:: pycon
- >>> f2 = kml.Folder(ns=ns, id="id2", name="name2", description="description2")
+ >>> f2 = kml.Folder(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:
@@ -59,9 +58,7 @@ Create a KML Placemark with a simple polygon geometry and add it to the second F
.. 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
- ... )
+ >>> p = kml.Placemark(id="id", name="name", description="description", geometry=polygon)
>>> f2.append(p)
Finally, print out the KML object as a string:
@@ -141,6 +138,17 @@ Read in the KML string
>>> k = kml.KML.from_string(doc)
+
+.. note::
+
+ To read a KML file directly, you can use the parse method:
+
+ .. code-block:: Python
+
+ k = kml.KML.parse("path/to/file.kml")
+
+
+
Next we perform some simple sanity checks, such as checking the number of features.
.. code-block:: pycon
@@ -206,3 +214,11 @@ Finally, print out the KML object as a string:
+
+.. note::
+
+ To save the KML object to a file, you can use the write method:
+
+ .. code-block:: Python
+
+ k.write("path/to/file.kml")
diff --git a/docs/upgrading.rst b/docs/upgrading.rst
new file mode 100644
index 00000000..82a002ac
--- /dev/null
+++ b/docs/upgrading.rst
@@ -0,0 +1,28 @@
+Upgrading from older versions of FastKML
+========================================
+
+Q: I updated from 0.12 to 1.0.0 and now getting the following errors when using
+``parse()``::
+
+ File "src/lxml/etree.pyx", line 3701, in lxml.etree._Validator.assert_
+ AssertionError: Element ...
+
+A: Your KML does not validate against the XML Schema.
+You can read it without validations by passing ``validate=False`` or ``strict=False``
+to the parse method::
+
+ from fastkml.kml import KML
+ doc = KML.parse('path/to/your/file.kml', strict=False)
+ # or
+ doc = KML.parse('path/to/your/file.kml', validate=False)
+
+With version 1.0, ``.from_string()`` is a class method that returns a new object.
+
+In fastkml 0.x::
+
+ postcode_kml = kml.KML()
+ postcode_kml.from_string(kml_file.read())
+
+Becomes in 1.0::
+
+ postcode_kml = kml.KML.from_string(kml_file.read())
diff --git a/docs/working_with_kml.rst b/docs/working_with_kml.rst
index 3da5942e..b810f79f 100644
--- a/docs/working_with_kml.rst
+++ b/docs/working_with_kml.rst
@@ -50,8 +50,12 @@ We could also search for all Points, which will also return the Points inside th
``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.
+
+.. note::
+
+ ``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
@@ -123,7 +127,7 @@ We need to register the attributes of the KML object to be able to parse it:
>>> registry.register(
... CascadingStyle,
... RegistryItem(
- ... ns_ids=("kml",),
+ ... ns_ids=("kml", ""),
... attr_name="style",
... node_name="Style",
... classes=(Style,),
@@ -152,8 +156,10 @@ And register the new element with the KML Document object:
The CascadingStyle object is now part of the KML document and can be accessed like any
other element.
-When parsing the document we have to skip the validation as the ``gx:CascadingStyle`` is
-not in the XSD Schema.
+
+.. note::
+ When parsing the document we have to skip the validation by passing ``validate=False``
+ to ``KML.parse`` as the ``gx:CascadingStyle`` is not in the XSD Schema.
Create a new KML object and confirm that the new element is parsed correctly:
diff --git a/examples/owid-co2-data.csv b/examples/owid-co2-data.csv
index f6f5e1a3..65c696f6 100644
--- a/examples/owid-co2-data.csv
+++ b/examples/owid-co2-data.csv
@@ -197,7 +197,7 @@ year,iso_code,co2_per_capita
1995,THA,2.586
1995,TGO,0.274
1995,TON,0.953
-1995,TO,11.265
+1995,TTO,11.265
1995,TUN,1.751
1995,TUR,3.058
1995,TKM,8.179
@@ -416,7 +416,7 @@ year,iso_code,co2_per_capita
1996,THA,2.883
1996,TGO,0.285
1996,TON,0.768
-1996,TO,14.053
+1996,TTO,14.053
1996,TUN,1.762
1996,TUR,3.311
1996,TKM,7.36
@@ -635,7 +635,7 @@ year,iso_code,co2_per_capita
1997,THA,2.971
1997,TGO,0.191
1997,TON,0.983
-1997,TO,14.346
+1997,TTO,14.346
1997,TUN,1.801
1997,TUR,3.461
1997,TKM,7.149
@@ -854,7 +854,7 @@ year,iso_code,co2_per_capita
1998,THA,2.587
1998,TGO,0.273
1998,TON,0.868
-1998,TO,15.05
+1998,TTO,15.05
1998,TUN,1.838
1998,TUR,3.408
1998,TKM,7.547
@@ -1073,7 +1073,7 @@ year,iso_code,co2_per_capita
1999,THA,2.678
1999,TGO,0.38
1999,TON,1.078
-1999,TO,17.026
+1999,TTO,17.026
1999,TUN,1.904
1999,TUR,3.291
1999,TKM,8.842
@@ -1292,7 +1292,7 @@ year,iso_code,co2_per_capita
2000,THA,2.654
2000,TGO,0.266
2000,TON,0.928
-2000,TO,18.29
+2000,TTO,18.29
2000,TUN,1.977
2000,TUR,3.586
2000,TKM,8.615
@@ -1511,7 +1511,7 @@ year,iso_code,co2_per_capita
2001,THA,2.706
2001,TGO,0.225
2001,TON,0.852
-2001,TO,19.989
+2001,TTO,19.989
2001,TUN,2.044
2001,TUR,3.282
2001,TKM,7.343
@@ -1730,7 +1730,7 @@ year,iso_code,co2_per_capita
2002,THA,2.875
2002,TGO,0.25
2002,TON,0.988
-2002,TO,21.324
+2002,TTO,21.324
2002,TUN,2.049
2002,TUR,3.352
2002,TKM,6.412
@@ -1949,7 +1949,7 @@ year,iso_code,co2_per_capita
2003,THA,2.95
2003,TGO,0.333
2003,TON,1.123
-2003,TO,23.931
+2003,TTO,23.931
2003,TUN,2.074
2003,TUR,3.541
2003,TKM,8.524
@@ -2168,7 +2168,7 @@ year,iso_code,co2_per_capita
2004,THA,3.175
2004,TGO,0.313
2004,TON,1.046
-2004,TO,24.032
+2004,TTO,24.032
2004,TUN,2.147
2004,TUR,3.611
2004,TKM,10.24
@@ -2387,7 +2387,7 @@ year,iso_code,co2_per_capita
2005,THA,3.256
2005,TGO,0.301
2005,TON,1.075
-2005,TO,27.922
+2005,TTO,27.922
2005,TUN,2.171
2005,TUR,3.855
2005,TKM,9.81
@@ -2606,7 +2606,7 @@ year,iso_code,co2_per_capita
2006,THA,3.254
2006,TGO,0.255
2006,TON,1.208
-2006,TO,31.031
+2006,TTO,31.031
2006,TUN,2.191
2006,TUR,4.057
2006,TKM,9.992
@@ -2825,7 +2825,7 @@ year,iso_code,co2_per_capita
2007,THA,3.353
2007,TGO,0.251
2007,TON,1.065
-2007,TO,32.915
+2007,TTO,32.915
2007,TUN,2.324
2007,TUR,4.452
2007,TKM,9.788
@@ -3044,7 +3044,7 @@ year,iso_code,co2_per_capita
2008,THA,3.331
2008,TGO,0.245
2008,TON,1.131
-2008,TO,31.924
+2008,TTO,31.924
2008,TUN,2.391
2008,TUR,4.355
2008,TKM,11.654
@@ -3263,7 +3263,7 @@ year,iso_code,co2_per_capita
2009,THA,3.374
2009,TGO,0.428
2009,TON,1.231
-2009,TO,31.717
+2009,TTO,31.717
2009,TUN,2.357
2009,TUR,4.381
2009,TKM,10.126
@@ -3482,7 +3482,7 @@ year,iso_code,co2_per_capita
2010,THA,3.53
2010,TGO,0.395
2010,TON,1.092
-2010,TO,33.406
+2010,TTO,33.406
2010,TUN,2.583
2010,TUR,4.32
2010,TKM,11.235
@@ -3701,7 +3701,7 @@ year,iso_code,co2_per_capita
2011,THA,3.564
2011,TGO,0.371
2011,TON,0.953
-2011,TO,33.146
+2011,TTO,33.146
2011,TUN,2.396
2011,TUR,4.612
2011,TKM,12.154
@@ -3920,7 +3920,7 @@ year,iso_code,co2_per_capita
2012,THA,3.794
2012,TGO,0.32
2012,TON,0.988
-2012,TO,32.282
+2012,TTO,32.282
2012,TUN,2.547
2012,TUR,4.731
2012,TKM,12.263
@@ -4139,7 +4139,7 @@ year,iso_code,co2_per_capita
2013,THA,3.791
2013,TGO,0.231
2013,TON,1.06
-2013,TO,31.806
+2013,TTO,31.806
2013,TUN,2.507
2013,TUR,4.536
2013,TKM,11.572
@@ -4358,7 +4358,7 @@ year,iso_code,co2_per_capita
2014,THA,3.895
2014,TGO,0.212
2014,TON,1.065
-2014,TO,32.324
+2014,TTO,32.324
2014,TUN,2.598
2014,TUR,4.66
2014,TKM,11.089
@@ -4577,7 +4577,7 @@ year,iso_code,co2_per_capita
2015,THA,3.942
2015,TGO,0.249
2015,TON,1.105
-2015,TO,31.201
+2015,TTO,31.201
2015,TUN,2.718
2015,TUR,4.833
2015,TKM,11.155
@@ -4796,7 +4796,7 @@ year,iso_code,co2_per_capita
2016,THA,4.023
2016,TGO,0.302
2016,TON,1.178
-2016,TO,27.15
+2016,TTO,27.15
2016,TUN,2.618
2016,TUR,5.011
2016,TKM,10.982
@@ -5015,7 +5015,7 @@ year,iso_code,co2_per_capita
2017,THA,3.997
2017,TGO,0.254
2017,TON,1.286
-2017,TO,27.267
+2017,TTO,27.267
2017,TUN,2.648
2017,TUR,5.249
2017,TKM,10.783
@@ -5234,7 +5234,7 @@ year,iso_code,co2_per_capita
2018,THA,4.054
2018,TGO,0.269
2018,TON,1.289
-2018,TO,26.801
+2018,TTO,26.801
2018,TUN,2.609
2018,TUR,5.097
2018,TKM,10.513
@@ -5453,7 +5453,7 @@ year,iso_code,co2_per_capita
2019,THA,3.953
2019,TGO,0.293
2019,TON,1.536
-2019,TO,26.832
+2019,TTO,26.832
2019,TUN,2.519
2019,TUR,4.824
2019,TKM,10.607
@@ -5672,7 +5672,7 @@ year,iso_code,co2_per_capita
2020,THA,3.803
2020,TGO,0.282
2020,TON,1.74
-2020,TO,23.074
+2020,TTO,23.074
2020,TUN,2.343
2020,TUR,4.908
2020,TKM,10.831
@@ -5891,7 +5891,7 @@ year,iso_code,co2_per_capita
2021,THA,3.732
2021,TGO,0.296
2021,TON,1.803
-2021,TO,23.29
+2021,TTO,23.29
2021,TUN,2.874
2021,TUR,5.34
2021,TKM,11.034
@@ -6110,7 +6110,7 @@ year,iso_code,co2_per_capita
2022,THA,3.776
2022,TGO,0.291
2022,TON,1.769
-2022,TO,22.424
+2022,TTO,22.424
2022,TUN,2.879
2022,TUR,5.105
2022,TKM,11.034
diff --git a/examples/shp2kml.py b/examples/shp2kml.py
index 1b0a1634..f5edb6a7 100755
--- a/examples/shp2kml.py
+++ b/examples/shp2kml.py
@@ -35,7 +35,7 @@
for feature in shp.__geo_interface__["features"]:
geometry = shape(feature["geometry"])
- co2_emission = co2_data.get(feature["properties"]["ADM0_A3"], 0)
+ co2_emission = co2_data.get(feature["properties"]["ADM0_ISO"], 0)
geometry = force_3d(geometry, co2_emission * 100_000)
kml_geometry = create_kml_geometry(
geometry,
@@ -44,7 +44,7 @@
)
color = random.randint(0, 0xFFFFFF)
style = fastkml.styles.Style(
- id=feature["properties"]["ADM0_A3"],
+ id=feature["properties"]["ADM0_ISO"],
styles=[
fastkml.styles.LineStyle(color=f"55{color:06X}", width=2),
fastkml.styles.PolyStyle(
@@ -56,7 +56,7 @@
],
)
- style_url = fastkml.styles.StyleUrl(url=f"#{feature['properties']['ADM0_A3']}")
+ style_url = fastkml.styles.StyleUrl(url=f"#{feature['properties']['ADM0_ISO']}")
placemark = fastkml.features.Placemark(
name=feature["properties"]["NAME"],
description=feature["properties"]["FORMAL_EN"],
@@ -68,5 +68,4 @@
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))
+kml.write(outfile, prettyprint=True, precision=3)
diff --git a/examples/shp2kml_timed.py b/examples/shp2kml_timed.py
index 2f7dd343..cc5c9cc3 100755
--- a/examples/shp2kml_timed.py
+++ b/examples/shp2kml_timed.py
@@ -1,4 +1,6 @@
#!/usr/bin/env python
+from __future__ import annotations
+
import csv
import datetime
import pathlib
@@ -23,7 +25,7 @@
co2_csv = pathlib.Path(examples_dir / "owid-co2-data.csv")
-co2_pa = {str(i): {} for i in range(1995, 2023)}
+co2_pa: dict[str, dict[str, float]] = {str(i): {} for i in range(1995, 2023)}
with co2_csv.open() as csvfile:
reader = csv.DictReader(csvfile)
@@ -36,7 +38,7 @@
styles = []
folders = []
for feature in shp.__geo_interface__["features"]:
- iso3_code = feature["properties"]["ADM0_A3"]
+ iso3_code = feature["properties"]["ADM0_ISO"]
geometry = shape(feature["geometry"])
color = random.randint(0, 0xFFFFFF)
styles.append(
@@ -54,9 +56,9 @@
)
style_url = fastkml.styles.StyleUrl(url=f"#{iso3_code}")
folder = fastkml.containers.Folder(name=feature["properties"]["NAME"])
- co2_growth = 0
+ co2_growth = 0.0
for year in range(1995, 2023):
- co2_year = co2_pa[str(year)].get(iso3_code, 0)
+ co2_year = co2_pa[str(year)].get(iso3_code, 0.0)
co2_growth += co2_year
kml_geometry = create_kml_geometry(
@@ -87,6 +89,5 @@
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))
+outfile = pathlib.Path("co2_growth_1995_2022.kmz")
+kml.write(outfile, prettyprint=True, precision=3)
diff --git a/examples/simple_example.py b/examples/simple_example.py
index c5b9e139..183d856a 100755
--- a/examples/simple_example.py
+++ b/examples/simple_example.py
@@ -17,7 +17,6 @@ def print_child_features(element, depth=0):
examples_dir = pathlib.Path(__file__).parent
fname = pathlib.Path(examples_dir / "KML_Samples.kml")
- with fname.open(encoding="utf-8") as kml_file:
- k = kml.KML.from_string(kml_file.read().encode("utf-8"))
+ k = kml.KML.parse(fname)
print_child_features(k)
diff --git a/fastkml/__init__.py b/fastkml/__init__.py
index b607e08f..769799dd 100644
--- a/fastkml/__init__.py
+++ b/fastkml/__init__.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2012 -2022 Christian Ledermann
+# Copyright (C) 2012 -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
@@ -35,61 +35,121 @@
from fastkml.data import ExtendedData
from fastkml.data import Schema
from fastkml.data import SchemaData
+from fastkml.data import SimpleData
+from fastkml.data import SimpleField
+from fastkml.features import NetworkLink
from fastkml.features import Placemark
+from fastkml.features import Snippet
+from fastkml.geometry import Coordinates
+from fastkml.geometry import InnerBoundaryIs
from fastkml.geometry import LinearRing
from fastkml.geometry import LineString
from fastkml.geometry import MultiGeometry
+from fastkml.geometry import OuterBoundaryIs
from fastkml.geometry import Point
from fastkml.geometry import Polygon
+from fastkml.geometry import create_kml_geometry
from fastkml.kml import KML
from fastkml.links import Icon
from fastkml.links import Link
+from fastkml.model import Alias
+from fastkml.model import Location
+from fastkml.model import Model
+from fastkml.model import Orientation
+from fastkml.model import ResourceMap
+from fastkml.model import Scale
+from fastkml.network_link_control import NetworkLinkControl
from fastkml.overlays import GroundOverlay
+from fastkml.overlays import ImagePyramid
+from fastkml.overlays import LatLonBox
+from fastkml.overlays import OverlayXY
from fastkml.overlays import PhotoOverlay
+from fastkml.overlays import RotationXY
+from fastkml.overlays import ScreenOverlay
+from fastkml.overlays import ScreenXY
+from fastkml.overlays import Size
+from fastkml.overlays import ViewVolume
from fastkml.styles import BalloonStyle
+from fastkml.styles import HotSpot
from fastkml.styles import IconStyle
from fastkml.styles import LabelStyle
from fastkml.styles import LineStyle
+from fastkml.styles import Pair
from fastkml.styles import PolyStyle
from fastkml.styles import Style
from fastkml.styles import StyleMap
from fastkml.styles import StyleUrl
+from fastkml.times import KmlDateTime
from fastkml.times import TimeSpan
from fastkml.times import TimeStamp
+from fastkml.utils import find
+from fastkml.utils import find_all
+from fastkml.validator import get_schema_parser
+from fastkml.validator import validate
from fastkml.views import Camera
from fastkml.views import LookAt
__all__ = [
"KML",
+ "Alias",
+ "AtomAuthor",
+ "AtomContributor",
+ "AtomLink",
+ "BalloonStyle",
+ "Camera",
+ "Coordinates",
+ "Data",
"Document",
+ "ExtendedData",
"Folder",
"GroundOverlay",
- "Placemark",
- "TimeSpan",
- "TimeStamp",
- "ExtendedData",
- "Data",
- "PhotoOverlay",
- "Schema",
- "SchemaData",
- "StyleUrl",
- "Style",
- "StyleMap",
+ "HotSpot",
+ "Icon",
"IconStyle",
- "LineStyle",
- "PolyStyle",
+ "ImagePyramid",
+ "InnerBoundaryIs",
+ "KmlDateTime",
"LabelStyle",
- "BalloonStyle",
- "AtomLink",
- "Icon",
- "Link",
- "Point",
+ "LatLonBox",
"LineString",
+ "LineStyle",
"LinearRing",
- "Polygon",
- "MultiGeometry",
- "AtomAuthor",
- "AtomContributor",
- "Camera",
+ "Link",
+ "Location",
"LookAt",
+ "Model",
+ "MultiGeometry",
+ "NetworkLink",
+ "NetworkLinkControl",
+ "Orientation",
+ "OuterBoundaryIs",
+ "OverlayXY",
+ "Pair",
+ "PhotoOverlay",
+ "Placemark",
+ "Point",
+ "PolyStyle",
+ "Polygon",
+ "ResourceMap",
+ "RotationXY",
+ "Scale",
+ "Schema",
+ "SchemaData",
+ "ScreenOverlay",
+ "ScreenXY",
+ "SimpleData",
+ "SimpleField",
+ "Size",
+ "Snippet",
+ "Style",
+ "StyleMap",
+ "StyleUrl",
+ "TimeSpan",
+ "TimeStamp",
+ "ViewVolume",
+ "create_kml_geometry",
+ "find",
+ "find_all",
+ "get_schema_parser",
+ "validate",
]
diff --git a/fastkml/about.py b/fastkml/about.py
index ce656dad..aa006175 100644
--- a/fastkml/about.py
+++ b/fastkml/about.py
@@ -19,7 +19,7 @@
The only purpose of this module is to provide a version number for the package.
"""
-__version__ = "1.0.0"
+__version__ = "1.1.0"
"""Fastkml version number."""
__all__ = ["__version__"]
diff --git a/fastkml/base.py b/fastkml/base.py
index ca2c7c21..8fa9d875 100644
--- a/fastkml/base.py
+++ b/fastkml/base.py
@@ -83,8 +83,8 @@ def __init__(
self.ns: str = (
self.name_spaces.get(self._default_nsid, "") if ns is None else ns
)
- for arg in kwargs:
- setattr(self, arg, kwargs[arg])
+ for arg, val in kwargs.items():
+ setattr(self, arg, val)
self.__kwarg_keys = tuple(kwargs.keys())
def __repr__(self) -> str:
diff --git a/fastkml/containers.py b/fastkml/containers.py
index 5041d219..1a3e7737 100644
--- a/fastkml/containers.py
+++ b/fastkml/containers.py
@@ -41,6 +41,7 @@
from fastkml.helpers import xml_subelement_list_kwarg
from fastkml.overlays import GroundOverlay
from fastkml.overlays import PhotoOverlay
+from fastkml.overlays import ScreenOverlay
from fastkml.registry import RegistryItem
from fastkml.registry import registry
from fastkml.styles import Style
@@ -55,6 +56,8 @@
logger = logging.getLogger(__name__)
+__all__ = ["Document", "Folder"]
+
KmlGeometry = Union[
Point,
LineString,
@@ -326,10 +329,21 @@ def get_style_by_url(self, style_url: str) -> Optional[Union[Style, StyleMap]]:
registry.register(
_Container,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="features",
- node_name="Folder,Placemark,Document,GroundOverlay,PhotoOverlay,NetworkLink",
- classes=(Document, Folder, Placemark, GroundOverlay, PhotoOverlay, NetworkLink),
+ node_name=(
+ "Folder,Placemark,Document,GroundOverlay,PhotoOverlay,ScreenOverlay,"
+ "NetworkLink"
+ ),
+ classes=(
+ Document,
+ Folder,
+ Placemark,
+ GroundOverlay,
+ PhotoOverlay,
+ ScreenOverlay,
+ NetworkLink,
+ ),
get_kwarg=xml_subelement_list_kwarg,
set_element=xml_subelement_list,
),
@@ -337,7 +351,7 @@ def get_style_by_url(self, style_url: str) -> Optional[Union[Style, StyleMap]]:
registry.register(
Document,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="schemata",
node_name="Schema",
classes=(Schema,),
diff --git a/fastkml/data.py b/fastkml/data.py
index baea3587..573ca074 100644
--- a/fastkml/data.py
+++ b/fastkml/data.py
@@ -50,6 +50,7 @@
"ExtendedData",
"Schema",
"SchemaData",
+ "SimpleData",
"SimpleField",
]
@@ -310,7 +311,7 @@ def append(self, field: SimpleField) -> None:
registry.register(
Schema,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="fields",
node_name="SimpleField",
classes=(SimpleField,),
@@ -441,6 +442,14 @@ def __bool__(self) -> bool:
class SimpleData(_XMLObject):
+ """
+ A SimpleData element is a custom data field.
+
+ This element assigns a value to the custom data field identified by the name
+ attribute. The type and name of this custom data field are declared in the
+ ```` element.
+ """
+
_default_nsid = "kml"
name: Optional[str]
@@ -635,7 +644,7 @@ def append_data(self, data: SimpleData) -> None:
registry.register(
SchemaData,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="data",
node_name="SimpleData",
classes=(SimpleData,),
@@ -716,7 +725,7 @@ def __bool__(self) -> bool:
registry.register(
ExtendedData,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="elements",
node_name="Data,SchemaData",
classes=(
diff --git a/fastkml/features.py b/fastkml/features.py
index f8d091a5..af535ec2 100644
--- a/fastkml/features.py
+++ b/fastkml/features.py
@@ -58,6 +58,7 @@
from fastkml.kml_base import _BaseObject
from fastkml.links import Link
from fastkml.mixins import TimeMixin
+from fastkml.model import Model
from fastkml.registry import RegistryItem
from fastkml.registry import registry
from fastkml.styles import Style
@@ -69,7 +70,7 @@
from fastkml.views import LookAt
from fastkml.views import Region
-__all__ = ["KmlGeometry", "NetworkLink", "Placemark", "Snippet"]
+__all__ = ["NetworkLink", "Placemark", "Snippet"]
logger = logging.getLogger(__name__)
@@ -78,6 +79,7 @@
LineString,
LinearRing,
Polygon,
+ Model,
MultiGeometry,
gx.MultiTrack,
gx.Track,
@@ -309,7 +311,7 @@ def __init__(
registry.register(
_Feature,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="name",
node_name="name",
classes=(str,),
@@ -320,7 +322,7 @@ def __init__(
registry.register(
_Feature,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="visibility",
node_name="visibility",
classes=(bool,),
@@ -332,7 +334,7 @@ def __init__(
registry.register(
_Feature,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="isopen",
node_name="open",
classes=(bool,),
@@ -366,7 +368,7 @@ def __init__(
registry.register(
_Feature,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="address",
node_name="address",
classes=(str,),
@@ -377,7 +379,7 @@ def __init__(
registry.register(
_Feature,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="phone_number",
node_name="phoneNumber",
classes=(str,),
@@ -388,7 +390,7 @@ def __init__(
registry.register(
_Feature,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="snippet",
node_name="Snippet",
classes=(Snippet,),
@@ -399,7 +401,7 @@ def __init__(
registry.register(
_Feature,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="description",
node_name="description",
classes=(str,),
@@ -410,7 +412,7 @@ def __init__(
registry.register(
_Feature,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="view",
node_name="Camera,LookAt",
classes=(
@@ -424,7 +426,7 @@ def __init__(
registry.register(
_Feature,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="times",
node_name="TimeSpan,TimeStamp",
classes=(
@@ -438,7 +440,7 @@ def __init__(
registry.register(
_Feature,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="style_url",
node_name="styleUrl",
classes=(StyleUrl,),
@@ -449,7 +451,7 @@ def __init__(
registry.register(
_Feature,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="styles",
node_name="Style,StyleMap",
classes=(
@@ -463,7 +465,7 @@ def __init__(
registry.register(
_Feature,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="region",
node_name="region",
classes=(Region,),
@@ -474,7 +476,7 @@ def __init__(
registry.register(
_Feature,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="extended_data",
node_name="ExtendedData",
classes=(ExtendedData,),
@@ -662,10 +664,10 @@ def geometry(self) -> Optional[AnyGeometryType]:
registry.register(
Placemark,
RegistryItem(
- ns_ids=("kml", "gx"),
+ ns_ids=("kml", "gx", ""),
attr_name="kml_geometry",
node_name=(
- "Point,LineString,LinearRing,Polygon,MultiGeometry,"
+ "Point,LineString,LinearRing,Polygon,MultiGeometry,Model,"
"gx:MultiTrack,gx:Track"
),
classes=(
@@ -674,6 +676,7 @@ def geometry(self) -> Optional[AnyGeometryType]:
LinearRing,
Polygon,
MultiGeometry,
+ Model,
gx.MultiTrack,
gx.Track,
),
@@ -891,7 +894,7 @@ def __bool__(self) -> bool:
registry.register(
NetworkLink,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="refresh_visibility",
node_name="refreshVisibility",
classes=(bool,),
@@ -903,7 +906,7 @@ def __bool__(self) -> bool:
registry.register(
NetworkLink,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="fly_to_view",
node_name="flyToView",
classes=(bool,),
@@ -915,7 +918,7 @@ def __bool__(self) -> bool:
registry.register(
NetworkLink,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="link",
node_name="Link",
classes=(Link,),
diff --git a/fastkml/geometry.py b/fastkml/geometry.py
index 3d920a90..bb14f78c 100644
--- a/fastkml/geometry.py
+++ b/fastkml/geometry.py
@@ -69,18 +69,19 @@
from fastkml.types import Element
__all__ = [
- "AnyGeometryType",
"Coordinates",
- "GeometryType",
+ "InnerBoundaryIs",
"LineString",
"LinearRing",
"MultiGeometry",
- "MultiGeometryType",
+ "OuterBoundaryIs",
"Point",
"Polygon",
+ "create_kml_geometry",
"create_multigeometry",
]
+
logger = logging.getLogger(__name__)
GeometryType = Union[geo.Polygon, geo.LineString, geo.LinearRing, geo.Point]
@@ -299,7 +300,7 @@ def get_tag_name(cls) -> str:
registry.register(
Coordinates,
item=RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
classes=(LineType,), # type: ignore[arg-type]
attr_name="coords",
node_name="coordinates",
@@ -500,7 +501,7 @@ def geometry(self) -> Optional[geo.Point]:
registry.register(
Point,
item=RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
classes=(bool,),
attr_name="extrude",
node_name="extrude",
@@ -512,7 +513,7 @@ def geometry(self) -> Optional[geo.Point]:
registry.register(
Point,
item=RegistryItem(
- ns_ids=("kml", "gx"),
+ ns_ids=("kml", "gx", ""),
classes=(AltitudeMode,),
attr_name="altitude_mode",
node_name="altitudeMode",
@@ -524,7 +525,7 @@ def geometry(self) -> Optional[geo.Point]:
registry.register(
Point,
item=RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
classes=(Coordinates,),
attr_name="kml_coordinates",
node_name="coordinates",
@@ -667,7 +668,7 @@ def geometry(self) -> Optional[geo.LineString]:
registry.register(
LineString,
item=RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
classes=(bool,),
attr_name="extrude",
node_name="extrude",
@@ -679,7 +680,7 @@ def geometry(self) -> Optional[geo.LineString]:
registry.register(
LineString,
item=RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
classes=(bool,),
attr_name="tessellate",
node_name="tessellate",
@@ -691,7 +692,7 @@ def geometry(self) -> Optional[geo.LineString]:
registry.register(
LineString,
item=RegistryItem(
- ns_ids=("kml", "gx"),
+ ns_ids=("kml", "gx", ""),
classes=(AltitudeMode,),
attr_name="altitude_mode",
node_name="altitudeMode",
@@ -703,7 +704,7 @@ def geometry(self) -> Optional[geo.LineString]:
registry.register(
LineString,
item=RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
classes=(Coordinates,),
attr_name="kml_coordinates",
node_name="coordinates",
@@ -935,7 +936,7 @@ def get_tag_name(cls) -> str:
registry.register(
BoundaryIs,
item=RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
classes=(LinearRing,),
attr_name="kml_geometry",
node_name="LinearRing",
@@ -1133,7 +1134,7 @@ def __eq__(self, other: object) -> bool:
registry.register(
Polygon,
item=RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
classes=(bool,),
attr_name="extrude",
node_name="extrude",
@@ -1145,7 +1146,7 @@ def __eq__(self, other: object) -> bool:
registry.register(
Polygon,
item=RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
classes=(bool,),
attr_name="tessellate",
node_name="tessellate",
@@ -1157,7 +1158,7 @@ def __eq__(self, other: object) -> bool:
registry.register(
Polygon,
item=RegistryItem(
- ns_ids=("kml", "gx"),
+ ns_ids=("kml", "gx", ""),
classes=(AltitudeMode,),
attr_name="altitude_mode",
node_name="altitudeMode",
@@ -1169,7 +1170,7 @@ def __eq__(self, other: object) -> bool:
registry.register(
Polygon,
item=RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
classes=(OuterBoundaryIs,),
attr_name="outer_boundary",
node_name="outerBoundaryIs",
@@ -1180,7 +1181,7 @@ def __eq__(self, other: object) -> bool:
registry.register(
Polygon,
item=RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
classes=(InnerBoundaryIs,),
attr_name="inner_boundaries",
node_name="innerBoundaryIs",
@@ -1343,7 +1344,7 @@ def geometry(self) -> Optional[MultiGeometryType]:
registry.register(
MultiGeometry,
item=RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
classes=(Point, LineString, Polygon, LinearRing, MultiGeometry),
attr_name="kml_geometries",
node_name="(Point|LineString|Polygon|LinearRing|MultiGeometry)",
diff --git a/fastkml/kml.py b/fastkml/kml.py
index 0886c6ba..aee6f9cd 100644
--- a/fastkml/kml.py
+++ b/fastkml/kml.py
@@ -51,6 +51,7 @@
from fastkml.features import Placemark
from fastkml.helpers import xml_subelement_list
from fastkml.helpers import xml_subelement_list_kwarg
+from fastkml.network_link_control import NetworkLinkControl
from fastkml.overlays import GroundOverlay
from fastkml.overlays import PhotoOverlay
from fastkml.registry import RegistryItem
@@ -59,7 +60,14 @@
logger = logging.getLogger(__name__)
-kml_children = Union[Folder, Document, Placemark, GroundOverlay, PhotoOverlay]
+kml_children = Union[
+ Folder,
+ Document,
+ Placemark,
+ GroundOverlay,
+ PhotoOverlay,
+ NetworkLinkControl,
+]
def lxml_parse_and_validate(
@@ -286,8 +294,19 @@ def write(
KML,
RegistryItem(
ns_ids=("kml",),
- classes=(Document, Folder, Placemark, GroundOverlay, PhotoOverlay, NetworkLink),
- node_name="Document,Folder,Placemark,GroundOverlay,PhotoOverlay,NetworkLink",
+ classes=(
+ Document,
+ Folder,
+ Placemark,
+ GroundOverlay,
+ PhotoOverlay,
+ NetworkLink,
+ NetworkLinkControl,
+ ),
+ node_name=(
+ "Document,Folder,Placemark,GroundOverlay,PhotoOverlay,NetworkLink,"
+ "NetworkLinkControl"
+ ),
attr_name="features",
get_kwarg=xml_subelement_list_kwarg,
set_element=xml_subelement_list,
diff --git a/fastkml/links.py b/fastkml/links.py
index a0180bf7..8347f069 100644
--- a/fastkml/links.py
+++ b/fastkml/links.py
@@ -32,6 +32,8 @@
from fastkml.registry import RegistryItem
from fastkml.registry import registry
+__all__ = ["Icon", "Link"]
+
class Link(_BaseObject):
"""
@@ -124,7 +126,7 @@ def __bool__(self) -> bool:
registry.register(
Link,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="href",
node_name="href",
classes=(str,),
@@ -135,7 +137,7 @@ def __bool__(self) -> bool:
registry.register(
Link,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="refresh_mode",
node_name="refreshMode",
classes=(RefreshMode,),
@@ -147,7 +149,7 @@ def __bool__(self) -> bool:
registry.register(
Link,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="refresh_interval",
node_name="refreshInterval",
classes=(float,),
@@ -159,7 +161,7 @@ def __bool__(self) -> bool:
registry.register(
Link,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="view_refresh_mode",
node_name="viewRefreshMode",
classes=(ViewRefreshMode,),
@@ -171,7 +173,7 @@ def __bool__(self) -> bool:
registry.register(
Link,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="view_refresh_time",
node_name="viewRefreshTime",
classes=(float,),
@@ -183,7 +185,7 @@ def __bool__(self) -> bool:
registry.register(
Link,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="view_bound_scale",
node_name="viewBoundScale",
classes=(float,),
@@ -195,7 +197,7 @@ def __bool__(self) -> bool:
registry.register(
Link,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="view_format",
node_name="viewFormat",
classes=(str,),
@@ -207,7 +209,7 @@ def __bool__(self) -> bool:
registry.register(
Link,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="http_query",
node_name="httpQuery",
classes=(str,),
diff --git a/fastkml/model.py b/fastkml/model.py
new file mode 100644
index 00000000..9387b98f
--- /dev/null
+++ b/fastkml/model.py
@@ -0,0 +1,547 @@
+# 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
+"""
+Model element.
+
+The Model element defines a 3D model that is attached to a Placemark.
+
+https://developers.google.com/kml/documentation/models
+https://developers.google.com/kml/documentation/kmlreference#model
+
+"""
+
+from typing import Any
+from typing import Dict
+from typing import Iterable
+from typing import List
+from typing import Optional
+
+from pygeoif.geometry import Point
+
+from fastkml import config
+from fastkml.base import _XMLObject
+from fastkml.enums import AltitudeMode
+from fastkml.helpers import clean_string
+from fastkml.helpers import enum_subelement
+from fastkml.helpers import float_subelement
+from fastkml.helpers import subelement_enum_kwarg
+from fastkml.helpers import subelement_float_kwarg
+from fastkml.helpers import subelement_text_kwarg
+from fastkml.helpers import text_subelement
+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.links import Link
+from fastkml.registry import RegistryItem
+from fastkml.registry import registry
+
+__all__ = ["Alias", "Location", "Model", "Orientation", "ResourceMap", "Scale"]
+
+
+class Location(_XMLObject):
+ """Represents a location in KML."""
+
+ _default_nsid = config.KML
+
+ latitude: Optional[float]
+ longitude: Optional[float]
+ altitude: Optional[float]
+
+ def __init__(
+ self,
+ ns: Optional[str] = None,
+ name_spaces: Optional[Dict[str, str]] = None,
+ altitude: Optional[float] = None,
+ latitude: Optional[float] = None,
+ longitude: Optional[float] = None,
+ **kwargs: Any,
+ ) -> None:
+ """Create a new Location."""
+ super().__init__(ns=ns, name_spaces=name_spaces, **kwargs)
+ self.altitude = altitude
+ self.latitude = latitude
+ self.longitude = longitude
+
+ def __bool__(self) -> bool:
+ """Return True if latitude and longitude are set."""
+ return all((self.latitude is not None, self.longitude is not None))
+
+ def __repr__(self) -> str:
+ """Create a string (c)representation for Location."""
+ return (
+ f"{self.__class__.__module__}.{self.__class__.__name__}("
+ f"ns={self.ns!r}, "
+ f"name_spaces={self.name_spaces!r}, "
+ f"altitude={self.altitude!r}, "
+ f"latitude={self.latitude!r}, "
+ f"longitude={self.longitude!r}, "
+ f"**{self._get_splat()!r},"
+ ")"
+ )
+
+ @property
+ def geometry(self) -> Optional[Point]:
+ """Return a Point representation of the geometry."""
+ if not self:
+ return None
+ assert self.longitude is not None # noqa: S101
+ assert self.latitude is not None # noqa: S101
+ return Point(self.longitude, self.latitude, self.altitude)
+
+
+registry.register(
+ Location,
+ RegistryItem(
+ ns_ids=("kml", ""),
+ attr_name="longitude",
+ node_name="longitude",
+ classes=(float,),
+ get_kwarg=subelement_float_kwarg,
+ set_element=float_subelement,
+ ),
+)
+registry.register(
+ Location,
+ RegistryItem(
+ ns_ids=("kml", ""),
+ attr_name="latitude",
+ node_name="latitude",
+ classes=(float,),
+ get_kwarg=subelement_float_kwarg,
+ set_element=float_subelement,
+ ),
+)
+registry.register(
+ Location,
+ RegistryItem(
+ ns_ids=("kml", ""),
+ attr_name="altitude",
+ node_name="altitude",
+ classes=(float,),
+ get_kwarg=subelement_float_kwarg,
+ set_element=float_subelement,
+ default=0.0,
+ ),
+)
+
+
+class Orientation(_XMLObject):
+ """Represents an orientation in KML."""
+
+ _default_nsid = config.KML
+
+ heading: Optional[float]
+ tilt: Optional[float]
+ roll: Optional[float]
+
+ def __init__(
+ self,
+ ns: Optional[str] = None,
+ name_spaces: Optional[Dict[str, str]] = None,
+ heading: Optional[float] = None,
+ tilt: Optional[float] = None,
+ roll: Optional[float] = None,
+ **kwargs: Any,
+ ) -> None:
+ """Create a new Orientation."""
+ super().__init__(ns=ns, name_spaces=name_spaces, **kwargs)
+ self.heading = heading
+ self.tilt = tilt
+ self.roll = roll
+
+ def __bool__(self) -> bool:
+ """Return True if heading, tilt, or roll are set."""
+ return any(
+ (self.heading is not None, self.tilt is not None, self.roll is not None),
+ )
+
+ def __repr__(self) -> str:
+ """Create a string (c)representation for Orientation."""
+ return (
+ f"{self.__class__.__module__}.{self.__class__.__name__}("
+ f"ns={self.ns!r}, "
+ f"name_spaces={self.name_spaces!r}, "
+ f"heading={self.heading!r}, "
+ f"tilt={self.tilt!r}, "
+ f"roll={self.roll!r}, "
+ f"**{self._get_splat()!r},"
+ ")"
+ )
+
+
+registry.register(
+ Orientation,
+ RegistryItem(
+ ns_ids=("kml", ""),
+ attr_name="heading",
+ node_name="heading",
+ classes=(float,),
+ get_kwarg=subelement_float_kwarg,
+ set_element=float_subelement,
+ default=0.0,
+ ),
+)
+registry.register(
+ Orientation,
+ RegistryItem(
+ ns_ids=("kml", ""),
+ attr_name="tilt",
+ node_name="tilt",
+ classes=(float,),
+ get_kwarg=subelement_float_kwarg,
+ set_element=float_subelement,
+ default=0.0,
+ ),
+)
+registry.register(
+ Orientation,
+ RegistryItem(
+ ns_ids=("kml", ""),
+ attr_name="roll",
+ node_name="roll",
+ classes=(float,),
+ get_kwarg=subelement_float_kwarg,
+ set_element=float_subelement,
+ default=0.0,
+ ),
+)
+
+
+class Scale(_XMLObject):
+ """Represents a scale in KML."""
+
+ _default_nsid = config.KML
+
+ x: Optional[float]
+ y: Optional[float]
+ z: Optional[float]
+
+ def __init__(
+ self,
+ ns: Optional[str] = None,
+ name_spaces: Optional[Dict[str, str]] = None,
+ x: Optional[float] = None,
+ y: Optional[float] = None,
+ z: Optional[float] = None,
+ **kwargs: Any,
+ ) -> None:
+ """Create a new Scale."""
+ super().__init__(ns=ns, name_spaces=name_spaces, **kwargs)
+ self.x = x
+ self.y = y
+ self.z = z
+
+ def __bool__(self) -> bool:
+ """Return True if x, y, or z are set."""
+ return any((self.x is not None, self.y is not None, self.z is not None))
+
+ def __repr__(self) -> str:
+ """Create a string (c)representation for Scale."""
+ return (
+ f"{self.__class__.__module__}.{self.__class__.__name__}("
+ f"ns={self.ns!r}, "
+ f"name_spaces={self.name_spaces!r}, "
+ f"x={self.x!r}, "
+ f"y={self.y!r}, "
+ f"z={self.z!r}, "
+ f"**{self._get_splat()!r},"
+ ")"
+ )
+
+
+registry.register(
+ Scale,
+ RegistryItem(
+ ns_ids=("kml", ""),
+ attr_name="x",
+ node_name="x",
+ classes=(float,),
+ get_kwarg=subelement_float_kwarg,
+ set_element=float_subelement,
+ default=1.0,
+ ),
+)
+registry.register(
+ Scale,
+ RegistryItem(
+ ns_ids=("kml", ""),
+ attr_name="y",
+ node_name="y",
+ classes=(float,),
+ get_kwarg=subelement_float_kwarg,
+ set_element=float_subelement,
+ default=1.0,
+ ),
+)
+registry.register(
+ Scale,
+ RegistryItem(
+ ns_ids=("kml", ""),
+ attr_name="z",
+ node_name="z",
+ classes=(float,),
+ get_kwarg=subelement_float_kwarg,
+ set_element=float_subelement,
+ default=1.0,
+ ),
+)
+
+
+class Alias(_XMLObject):
+ """Represents an alias in KML."""
+
+ _default_nsid = config.KML
+
+ target_href: Optional[str]
+ source_href: Optional[str]
+
+ def __init__(
+ self,
+ ns: Optional[str] = None,
+ name_spaces: Optional[Dict[str, str]] = None,
+ target_href: Optional[str] = None,
+ source_href: Optional[str] = None,
+ **kwargs: Any,
+ ) -> None:
+ """Create a new Alias."""
+ super().__init__(ns=ns, name_spaces=name_spaces, **kwargs)
+ self.target_href = clean_string(target_href)
+ self.source_href = clean_string(source_href)
+
+ def __bool__(self) -> bool:
+ """Return True if target_href or source_href are set."""
+ return any((self.target_href is not None, self.source_href is not None))
+
+ def __repr__(self) -> str:
+ """Create a string (c)representation for Alias."""
+ return (
+ f"{self.__class__.__module__}.{self.__class__.__name__}("
+ f"ns={self.ns!r}, "
+ f"name_spaces={self.name_spaces!r}, "
+ f"target_href={self.target_href!r}, "
+ f"source_href={self.source_href!r}, "
+ f"**{self._get_splat()!r},"
+ ")"
+ )
+
+
+registry.register(
+ Alias,
+ RegistryItem(
+ ns_ids=("kml", ""),
+ attr_name="target_href",
+ node_name="targetHref",
+ classes=(str,),
+ get_kwarg=subelement_text_kwarg,
+ set_element=text_subelement,
+ ),
+)
+registry.register(
+ Alias,
+ RegistryItem(
+ ns_ids=("kml", ""),
+ attr_name="source_href",
+ node_name="sourceHref",
+ classes=(str,),
+ get_kwarg=subelement_text_kwarg,
+ set_element=text_subelement,
+ ),
+)
+
+
+class ResourceMap(_XMLObject):
+ """Represents a resource map in KML."""
+
+ _default_nsid = config.KML
+
+ aliases: List[Alias]
+
+ def __init__(
+ self,
+ ns: Optional[str] = None,
+ name_spaces: Optional[Dict[str, str]] = None,
+ aliases: Optional[Iterable[Alias]] = None,
+ **kwargs: Any,
+ ) -> None:
+ """Create a new ResourceMap."""
+ super().__init__(ns=ns, name_spaces=name_spaces, **kwargs)
+ self.aliases = list(aliases) if aliases is not None else []
+
+ def __bool__(self) -> bool:
+ """Return True if aliases are set."""
+ return bool(self.aliases)
+
+ def __repr__(self) -> str:
+ """Create a string (c)representation for ResourceMap."""
+ return (
+ f"{self.__class__.__module__}.{self.__class__.__name__}("
+ f"ns={self.ns!r}, "
+ f"name_spaces={self.name_spaces!r}, "
+ f"aliases={self.aliases!r}, "
+ f"**{self._get_splat()!r},"
+ ")"
+ )
+
+
+registry.register(
+ ResourceMap,
+ RegistryItem(
+ ns_ids=("kml", ""),
+ attr_name="aliases",
+ node_name="Alias",
+ classes=(Alias,),
+ get_kwarg=xml_subelement_list_kwarg,
+ set_element=xml_subelement_list,
+ ),
+)
+
+
+class Model(_BaseObject):
+ """Represents a model in KML."""
+
+ altitude_mode: Optional[AltitudeMode]
+ location: Optional[Location]
+ orientation: Optional[Orientation]
+ scale: Optional[Scale]
+ link: Optional[Link]
+ resource_map: Optional[ResourceMap]
+
+ def __init__(
+ self,
+ ns: Optional[str] = None,
+ name_spaces: Optional[Dict[str, str]] = None,
+ id: Optional[str] = None,
+ target_id: Optional[str] = None,
+ altitude_mode: Optional[AltitudeMode] = None,
+ location: Optional[Location] = None,
+ orientation: Optional[Orientation] = None,
+ scale: Optional[Scale] = None,
+ link: Optional[Link] = None,
+ resource_map: Optional[ResourceMap] = None,
+ **kwargs: Any,
+ ) -> None:
+ """Create a new Model."""
+ super().__init__(
+ ns=ns,
+ name_spaces=name_spaces,
+ id=id,
+ target_id=target_id,
+ **kwargs,
+ )
+ self.altitude_mode = altitude_mode
+ self.location = location
+ self.orientation = orientation
+ self.scale = scale
+ self.link = link
+ self.resource_map = resource_map
+
+ def __bool__(self) -> bool:
+ """Return True if link and location are set."""
+ return all((self.link, self.location))
+
+ def __repr__(self) -> str:
+ """Create a string representation for Model."""
+ 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"altitude_mode={self.altitude_mode}, "
+ f"location={self.location!r}, "
+ f"orientation={self.orientation!r}, "
+ f"scale={self.scale!r}, "
+ f"link={self.link!r}, "
+ f"resource_map={self.resource_map!r}, "
+ f"**{self._get_splat()!r},"
+ ")"
+ )
+
+ @property
+ def geometry(self) -> Optional[Point]:
+ """Return a Point representation of the geometry."""
+ return self.location.geometry if self.location else None
+
+
+registry.register(
+ Model,
+ RegistryItem(
+ ns_ids=("kml", "gx", ""),
+ attr_name="altitude_mode",
+ node_name="altitudeMode",
+ classes=(AltitudeMode,),
+ get_kwarg=subelement_enum_kwarg,
+ set_element=enum_subelement,
+ default=AltitudeMode.clamp_to_ground,
+ ),
+)
+registry.register(
+ Model,
+ RegistryItem(
+ ns_ids=("kml", ""),
+ attr_name="location",
+ node_name="Location",
+ classes=(Location,),
+ get_kwarg=xml_subelement_kwarg,
+ set_element=xml_subelement,
+ ),
+)
+registry.register(
+ Model,
+ RegistryItem(
+ ns_ids=("kml", ""),
+ attr_name="orientation",
+ node_name="Orientation",
+ classes=(Orientation,),
+ get_kwarg=xml_subelement_kwarg,
+ set_element=xml_subelement,
+ ),
+)
+registry.register(
+ Model,
+ RegistryItem(
+ ns_ids=("kml", ""),
+ attr_name="scale",
+ node_name="Scale",
+ classes=(Scale,),
+ get_kwarg=xml_subelement_kwarg,
+ set_element=xml_subelement,
+ ),
+)
+registry.register(
+ Model,
+ RegistryItem(
+ ns_ids=("kml", ""),
+ attr_name="link",
+ node_name="Link",
+ classes=(Link,),
+ get_kwarg=xml_subelement_kwarg,
+ set_element=xml_subelement,
+ ),
+)
+registry.register(
+ Model,
+ RegistryItem(
+ ns_ids=("kml", ""),
+ attr_name="resource_map",
+ node_name="ResourceMap",
+ classes=(ResourceMap,),
+ get_kwarg=xml_subelement_kwarg,
+ set_element=xml_subelement,
+ ),
+)
diff --git a/fastkml/network_link_control.py b/fastkml/network_link_control.py
new file mode 100644
index 00000000..01d2d066
--- /dev/null
+++ b/fastkml/network_link_control.py
@@ -0,0 +1,261 @@
+# 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
+"""
+NetworkLinkControl class.
+
+Controls the behavior of files fetched by a .
+
+https://developers.google.com/kml/documentation/kmlreference#networklinkcontrol
+"""
+
+import logging
+from typing import Any
+from typing import Dict
+from typing import Optional
+from typing import Union
+
+from fastkml import config
+from fastkml.base import _XMLObject
+from fastkml.helpers import clean_string
+from fastkml.helpers import datetime_subelement
+from fastkml.helpers import datetime_subelement_kwarg
+from fastkml.helpers import float_subelement
+from fastkml.helpers import subelement_float_kwarg
+from fastkml.helpers import subelement_text_kwarg
+from fastkml.helpers import text_subelement
+from fastkml.helpers import xml_subelement
+from fastkml.helpers import xml_subelement_kwarg
+from fastkml.registry import RegistryItem
+from fastkml.registry import registry
+from fastkml.times import KmlDateTime
+from fastkml.views import Camera
+from fastkml.views import LookAt
+
+__all__ = [
+ "NetworkLinkControl",
+]
+
+logger = logging.getLogger(__name__)
+
+
+class NetworkLinkControl(_XMLObject):
+ """Controls the behavior of files fetched by a ."""
+
+ _default_nsid = config.KML
+
+ min_refresh_period: Optional[float]
+ max_session_length: Optional[float]
+ cookie: Optional[str]
+ message: Optional[str]
+ link_name: Optional[str]
+ link_description: Optional[str]
+ link_snippet: Optional[str]
+ expires: Optional[KmlDateTime]
+ view: Union[Camera, LookAt, None]
+
+ def __init__(
+ self,
+ ns: Optional[str] = None,
+ name_spaces: Optional[Dict[str, str]] = None,
+ min_refresh_period: Optional[float] = None,
+ max_session_length: Optional[float] = None,
+ cookie: Optional[str] = None,
+ message: Optional[str] = None,
+ link_name: Optional[str] = None,
+ link_description: Optional[str] = None,
+ link_snippet: Optional[str] = None,
+ expires: Optional[KmlDateTime] = None,
+ view: Optional[Union[Camera, LookAt]] = None,
+ **kwargs: Any,
+ ) -> None:
+ """
+ Create a NetworkLinkControl object.
+
+ Parameters
+ ----------
+ ns : str, optional
+ The namespace to use for the NetworkLinkControl object.
+ name_spaces : dict, optional
+ A dictionary of namespaces to use for the NetworkLinkControl object.
+ min_refresh_period : float, optional
+ The minimum number of seconds between fetches. A value of -1 indicates that
+ the NetworkLinkControl object should be fetched only once.
+ max_session_length : float, optional
+ The maximum number of seconds that the link should be followed.
+ cookie : str, optional
+ A string value that can be used to identify the client request.
+ message : str, optional
+ A message to be displayed to the user in case of a failure.
+ link_name : str, optional
+ The name of the link.
+ link_description : str, optional
+ A description of the link.
+ link_snippet : str, optional
+ A snippet of text to be displayed in the link.
+ expires : KmlDateTime, optional
+ The time at which the link should expire.
+ view : Camera or LookAt, optional
+ The view to be used when the link is followed.
+ **kwargs : Any, optional
+ Additional keyword arguments.
+
+ """
+ super().__init__(
+ ns=ns,
+ name_spaces=name_spaces,
+ **kwargs,
+ )
+ self.min_refresh_period = min_refresh_period
+ self.max_session_length = max_session_length
+ self.cookie = clean_string(cookie)
+ self.message = clean_string(message)
+ self.link_name = clean_string(link_name)
+ self.link_description = clean_string(link_description)
+ self.link_snippet = clean_string(link_snippet)
+ self.expires = expires
+ self.view = view
+
+ def __repr__(self) -> str:
+ """
+ Return a string representation of the NetworkLinkControl object.
+
+ Returns
+ -------
+ str: A string representation of the NetworkLinkControl object.
+
+ """
+ return (
+ f"{self.__class__.__module__}.{self.__class__.__name__}("
+ f"ns={self.ns!r}, "
+ f"name_spaces={self.name_spaces!r}, "
+ f"min_refresh_period={self.min_refresh_period!r}, "
+ f"max_session_length={self.max_session_length!r}, "
+ f"cookie={self.cookie!r}, "
+ f"message={self.message!r}, "
+ f"link_name={self.link_name!r}, "
+ f"link_description={self.link_description!r}, "
+ f"link_snippet={self.link_snippet!r}, "
+ f"expires={self.expires!r}, "
+ f"view={self.view!r}, "
+ f"**{self._get_splat()!r},"
+ ")"
+ )
+
+
+registry.register(
+ NetworkLinkControl,
+ RegistryItem(
+ ns_ids=("kml",),
+ attr_name="min_refresh_period",
+ node_name="minRefreshPeriod",
+ classes=(float,),
+ get_kwarg=subelement_float_kwarg,
+ set_element=float_subelement,
+ default=0,
+ ),
+)
+registry.register(
+ NetworkLinkControl,
+ RegistryItem(
+ ns_ids=("kml",),
+ attr_name="max_session_length",
+ node_name="maxSessionLength",
+ classes=(float,),
+ get_kwarg=subelement_float_kwarg,
+ set_element=float_subelement,
+ default=-1,
+ ),
+)
+registry.register(
+ NetworkLinkControl,
+ RegistryItem(
+ ns_ids=("kml",),
+ attr_name="cookie",
+ node_name="cookie",
+ classes=(str,),
+ get_kwarg=subelement_text_kwarg,
+ set_element=text_subelement,
+ ),
+)
+registry.register(
+ NetworkLinkControl,
+ RegistryItem(
+ ns_ids=("kml",),
+ attr_name="message",
+ node_name="message",
+ classes=(str,),
+ get_kwarg=subelement_text_kwarg,
+ set_element=text_subelement,
+ ),
+)
+registry.register(
+ NetworkLinkControl,
+ RegistryItem(
+ ns_ids=("kml",),
+ attr_name="link_name",
+ node_name="linkName",
+ classes=(str,),
+ get_kwarg=subelement_text_kwarg,
+ set_element=text_subelement,
+ ),
+)
+registry.register(
+ NetworkLinkControl,
+ RegistryItem(
+ ns_ids=("kml",),
+ attr_name="link_description",
+ node_name="linkDescription",
+ classes=(str,),
+ get_kwarg=subelement_text_kwarg,
+ set_element=text_subelement,
+ ),
+)
+registry.register(
+ NetworkLinkControl,
+ RegistryItem(
+ ns_ids=("kml",),
+ attr_name="link_snippet",
+ node_name="linkSnippet",
+ classes=(str,),
+ get_kwarg=subelement_text_kwarg,
+ set_element=text_subelement,
+ ),
+)
+registry.register(
+ NetworkLinkControl,
+ item=RegistryItem(
+ ns_ids=("kml",),
+ classes=(KmlDateTime,),
+ attr_name="expires",
+ node_name="expires",
+ get_kwarg=datetime_subelement_kwarg,
+ set_element=datetime_subelement,
+ ),
+)
+registry.register(
+ NetworkLinkControl,
+ RegistryItem(
+ ns_ids=("kml",),
+ attr_name="view",
+ node_name="Camera,LookAt",
+ classes=(
+ Camera,
+ LookAt,
+ ),
+ get_kwarg=xml_subelement_kwarg,
+ set_element=xml_subelement,
+ ),
+)
diff --git a/fastkml/overlays.py b/fastkml/overlays.py
index ab6a9551..48bfcdd9 100644
--- a/fastkml/overlays.py
+++ b/fastkml/overlays.py
@@ -30,6 +30,7 @@
from fastkml.enums import AltitudeMode
from fastkml.enums import GridOrigin
from fastkml.enums import Shape
+from fastkml.enums import Units
from fastkml.features import Snippet
from fastkml.features import _Feature
from fastkml.geometry import LinearRing
@@ -37,8 +38,12 @@
from fastkml.geometry import MultiGeometry
from fastkml.geometry import Point
from fastkml.geometry import Polygon
+from fastkml.helpers import attribute_enum_kwarg
+from fastkml.helpers import attribute_float_kwarg
from fastkml.helpers import clean_string
+from fastkml.helpers import enum_attribute
from fastkml.helpers import enum_subelement
+from fastkml.helpers import float_attribute
from fastkml.helpers import float_subelement
from fastkml.helpers import int_subelement
from fastkml.helpers import subelement_enum_kwarg
@@ -63,9 +68,13 @@
__all__ = [
"GroundOverlay",
"ImagePyramid",
- "KmlGeometry",
"LatLonBox",
+ "OverlayXY",
"PhotoOverlay",
+ "RotationXY",
+ "ScreenOverlay",
+ "ScreenXY",
+ "Size",
"ViewVolume",
]
@@ -220,7 +229,7 @@ def __init__(
registry.register(
_Overlay,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="color",
node_name="color",
classes=(str,),
@@ -232,7 +241,7 @@ def __init__(
registry.register(
_Overlay,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="draw_order",
node_name="drawOrder",
classes=(int,),
@@ -244,7 +253,7 @@ def __init__(
registry.register(
_Overlay,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="icon",
node_name="Icon",
classes=(Icon,),
@@ -371,7 +380,7 @@ def __bool__(self) -> bool:
registry.register(
ViewVolume,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="left_fov",
node_name="leftFov",
classes=(float,),
@@ -383,7 +392,7 @@ def __bool__(self) -> bool:
registry.register(
ViewVolume,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="right_fov",
node_name="rightFov",
classes=(float,),
@@ -395,7 +404,7 @@ def __bool__(self) -> bool:
registry.register(
ViewVolume,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="bottom_fov",
node_name="bottomFov",
classes=(float,),
@@ -407,7 +416,7 @@ def __bool__(self) -> bool:
registry.register(
ViewVolume,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="top_fov",
node_name="topFov",
classes=(float,),
@@ -419,7 +428,7 @@ def __bool__(self) -> bool:
registry.register(
ViewVolume,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="near",
node_name="near",
classes=(float,),
@@ -541,7 +550,7 @@ def __bool__(self) -> bool:
registry.register(
ImagePyramid,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="tile_size",
node_name="tileSize",
classes=(int,),
@@ -553,7 +562,7 @@ def __bool__(self) -> bool:
registry.register(
ImagePyramid,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="max_width",
node_name="maxWidth",
classes=(int,),
@@ -564,7 +573,7 @@ def __bool__(self) -> bool:
registry.register(
ImagePyramid,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="max_height",
node_name="maxHeight",
classes=(int,),
@@ -575,7 +584,7 @@ def __bool__(self) -> bool:
registry.register(
ImagePyramid,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="grid_origin",
node_name="gridOrigin",
classes=(GridOrigin,),
@@ -812,7 +821,7 @@ def __repr__(self) -> str:
registry.register(
PhotoOverlay,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="rotation",
node_name="rotation",
classes=(float,),
@@ -824,7 +833,7 @@ def __repr__(self) -> str:
registry.register(
PhotoOverlay,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="view_volume",
node_name="ViewVolume",
classes=(ViewVolume,),
@@ -835,7 +844,7 @@ def __repr__(self) -> str:
registry.register(
PhotoOverlay,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="image_pyramid",
node_name="ImagePyramid",
classes=(ImagePyramid,),
@@ -846,7 +855,7 @@ def __repr__(self) -> str:
registry.register(
PhotoOverlay,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="point",
node_name="Point",
classes=(Point,),
@@ -857,7 +866,7 @@ def __repr__(self) -> str:
registry.register(
PhotoOverlay,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="shape",
node_name="shape",
classes=(Shape,),
@@ -983,7 +992,7 @@ def __bool__(self) -> bool:
registry.register(
LatLonBox,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="north",
node_name="north",
classes=(float,),
@@ -994,7 +1003,7 @@ def __bool__(self) -> bool:
registry.register(
LatLonBox,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="south",
node_name="south",
classes=(float,),
@@ -1005,7 +1014,7 @@ def __bool__(self) -> bool:
registry.register(
LatLonBox,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="east",
node_name="east",
classes=(float,),
@@ -1016,7 +1025,7 @@ def __bool__(self) -> bool:
registry.register(
LatLonBox,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="west",
node_name="west",
classes=(float,),
@@ -1027,7 +1036,7 @@ def __bool__(self) -> bool:
registry.register(
LatLonBox,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="rotation",
node_name="rotation",
classes=(float,),
@@ -1232,7 +1241,7 @@ def __repr__(self) -> str:
registry.register(
GroundOverlay,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="altitude",
node_name="altitude",
classes=(float,),
@@ -1256,7 +1265,7 @@ def __repr__(self) -> str:
registry.register(
GroundOverlay,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="lat_lon_box",
node_name="LatLonBox",
classes=(LatLonBox,),
@@ -1264,3 +1273,405 @@ def __repr__(self) -> str:
set_element=xml_subelement,
),
)
+
+
+class _XY(_XMLObject):
+ """Specifies a point relative to the screen origin in pixels."""
+
+ _default_nsid = config.KML
+
+ x: Optional[float]
+ y: Optional[float]
+ x_units: Optional[Units]
+
+ y_units: Optional[Units]
+
+ def __init__(
+ self,
+ ns: Optional[str] = None,
+ name_spaces: Optional[Dict[str, str]] = None,
+ x: Optional[float] = None,
+ y: Optional[float] = None,
+ x_units: Optional[Units] = None,
+ y_units: Optional[Units] = None,
+ **kwargs: Any,
+ ) -> None:
+ """
+ Initialize a new _XY object.
+
+ Args:
+ ----
+ ns : Optional[str]
+ The namespace for the element.
+ name_spaces : Optional[Dict[str, str]]
+ A dictionary of namespace prefixes and URIs.
+ x : Optional[float]
+ The horizontal position of the point relative to the left edge.
+ y : Optional[float]
+ The vertical position of the point relative to the bottom edge.
+ x_units : Optional[Units]
+ The horizontal units of the point.
+ y_units : Optional[Units]
+ The vertical units of the point
+ kwargs : Any
+ Additional keyword arguments.
+
+ """
+ super().__init__(ns=ns, name_spaces=name_spaces, **kwargs)
+ self.x = x
+ self.y = y
+ self.x_units = x_units
+ self.y_units = y_units
+
+ def __repr__(self) -> str:
+ """Create a string (c)representation for _XY."""
+ return (
+ f"{self.__class__.__module__}.{self.__class__.__name__}("
+ f"ns={self.ns!r}, "
+ f"name_spaces={self.name_spaces!r}, "
+ f"x={self.x!r}, "
+ f"y={self.y!r}, "
+ f"x_units={self.x_units}, "
+ f"y_units={self.y_units}, "
+ f"**{self._get_splat()!r},"
+ ")"
+ )
+
+ def __bool__(self) -> bool:
+ """
+ Check if all the attributes necessary are not None.
+
+ Returns
+ -------
+ bool: True if all attributes (x, y) are not None.
+
+ """
+ return all([self.x is not None, self.y is not None])
+
+
+registry.register(
+ _XY,
+ RegistryItem(
+ ns_ids=("", "kml"),
+ attr_name="x",
+ node_name="x",
+ classes=(float,),
+ get_kwarg=attribute_float_kwarg,
+ set_element=float_attribute,
+ ),
+)
+registry.register(
+ _XY,
+ RegistryItem(
+ ns_ids=("", "kml"),
+ attr_name="y",
+ node_name="y",
+ classes=(float,),
+ get_kwarg=attribute_float_kwarg,
+ set_element=float_attribute,
+ ),
+)
+registry.register(
+ _XY,
+ RegistryItem(
+ ns_ids=("", "kml"),
+ attr_name="x_units",
+ node_name="xunits",
+ classes=(Units,),
+ get_kwarg=attribute_enum_kwarg,
+ set_element=enum_attribute,
+ default=Units.fraction,
+ ),
+)
+registry.register(
+ _XY,
+ RegistryItem(
+ ns_ids=("", "kml"),
+ attr_name="y_units",
+ node_name="yunits",
+ classes=(Units,),
+ get_kwarg=attribute_enum_kwarg,
+ set_element=enum_attribute,
+ default=Units.fraction,
+ ),
+)
+
+
+class OverlayXY(_XY):
+ """Specifies the placement of the overlay on the screen."""
+
+ @classmethod
+ def get_tag_name(cls) -> str:
+ """Return the tag name."""
+ return "overlayXY"
+
+
+class ScreenXY(_XY):
+ """Specifies the placement of the overlay on the screen."""
+
+ @classmethod
+ def get_tag_name(cls) -> str:
+ """Return the tag name."""
+ return "screenXY"
+
+
+class RotationXY(_XY):
+ """Specifies the rotation of the overlay on the screen."""
+
+ @classmethod
+ def get_tag_name(cls) -> str:
+ """Return the tag name."""
+ return "rotationXY"
+
+
+class Size(_XY):
+ """Specifies the size of the overlay on the screen."""
+
+ @classmethod
+ def get_tag_name(cls) -> str:
+ """Return the tag name."""
+ return "size"
+
+
+class ScreenOverlay(_Overlay):
+ """
+ A ScreenOverlay draws an image overlay fixed to the screen.
+
+ This element draws an image overlay fixed to the screen. Sample uses include
+ watermarking the map with an image, such as a company logo, or adding a
+ heads-up display (HUD) to show real-time information.
+
+ The child of specifies the image to be used as the overlay.
+ This file can be either on a local file system or on a web server.
+
+ https://developers.google.com/kml/documentation/kmlreference#screenoverlay
+ """
+
+ def __init__(
+ self,
+ ns: Optional[str] = None,
+ name_spaces: Optional[Dict[str, str]] = None,
+ id: Optional[str] = None,
+ target_id: Optional[str] = None,
+ name: Optional[str] = None,
+ visibility: Optional[bool] = None,
+ isopen: Optional[bool] = None,
+ atom_link: Optional[atom.Link] = None,
+ atom_author: Optional[atom.Author] = None,
+ address: Optional[str] = None,
+ phone_number: Optional[str] = None,
+ snippet: Optional[Snippet] = None,
+ description: Optional[str] = None,
+ view: Optional[Union[Camera, LookAt]] = None,
+ times: Optional[Union[TimeSpan, TimeStamp]] = None,
+ style_url: Optional[StyleUrl] = None,
+ styles: Optional[Iterable[Union[Style, StyleMap]]] = None,
+ region: Optional[Region] = None,
+ extended_data: Optional[ExtendedData] = None,
+ color: Optional[str] = None,
+ draw_order: Optional[int] = None,
+ icon: Optional[Icon] = None,
+ # Screen Overlay specific
+ overlay_xy: Optional[OverlayXY] = None,
+ screen_xy: Optional[ScreenXY] = None,
+ rotation_xy: Optional[RotationXY] = None,
+ size: Optional[Size] = None,
+ rotation: Optional[float] = None,
+ **kwargs: Any,
+ ) -> None:
+ """
+ Initialize a new ScreenOverlay object.
+
+ Args:
+ ----
+ ns : Optional[str]
+ The namespace of the element.
+ name_spaces : Optional[Dict[str, str]]
+ The dictionary of namespace prefixes and URIs.
+ id : Optional[str]
+ The ID of the element.
+ target_id : Optional[str]
+ The target ID of the element.
+ name : Optional[str]
+ The name of the element.
+ visibility : Optional[bool]
+ The visibility of the element.
+ isopen : Optional[bool]
+ The open state of the element.
+ atom_link : Optional[atom.Link]
+ The Atom link associated with the element.
+ atom_author : Optional[atom.Author]
+ The Atom author associated with the element.
+ address : Optional[str]
+ The address of the element.
+ phone_number : Optional[str]
+ The phone number of the element.
+ snippet : Optional[Snippet]
+ The snippet associated with the element.
+ description : Optional[str]
+ The description of the element.
+ view : Optional[Union[Camera, LookAt]]
+ The view associated with the element.
+ times : Optional[Union[TimeSpan, TimeStamp]]
+ The times associated with the element.
+ style_url : Optional[StyleUrl]
+ The style URL of the element.
+ styles : Optional[Iterable[Union[Style, StyleMap]]]
+ The styles associated with the element.
+ region : Optional[Region]
+ The region associated with the element.
+ extended_data : Optional[ExtendedData]
+ The extended data associated with the element.
+ color : Optional[str]
+ The color of the element.
+ draw_order : Optional[int]
+ The draw order of the element.
+ icon : Optional[Icon]
+ The icon associated with the element.
+ altitude : Optional[float]
+ The altitude of the element.
+ altitude_mode : Optional[AltitudeMode]
+ The altitude mode of the element.
+ lat_lon_box : Optional[LatLonBox]
+ The latitude-longitude box associated with the element.
+ overlay_xy : Optional[OverlayXY]
+ The overlay XY associated with the element.
+ screen_xy : Optional[ScreenXY]
+ The screen XY associated with the element.
+ rotation_xy : Optional[RotationXY]
+ The rotation XY associated with the element.
+ size : Optional[Size]
+ The size associated with the element.
+ rotation : Optional[float]
+ The rotation of the element.
+ kwargs : Any
+ Additional keyword arguments.
+
+ Returns:
+ -------
+ None
+
+ """
+ super().__init__(
+ ns=ns,
+ name_spaces=name_spaces,
+ id=id,
+ target_id=target_id,
+ name=name,
+ visibility=visibility,
+ isopen=isopen,
+ atom_link=atom_link,
+ atom_author=atom_author,
+ address=address,
+ phone_number=phone_number,
+ snippet=snippet,
+ description=description,
+ view=view,
+ times=times,
+ style_url=style_url,
+ styles=styles,
+ region=region,
+ extended_data=extended_data,
+ color=color,
+ draw_order=draw_order,
+ icon=icon,
+ **kwargs,
+ )
+ self.overlay_xy = overlay_xy
+ self.screen_xy = screen_xy
+ self.rotation_xy = rotation_xy
+ self.size = size
+ self.rotation = rotation
+
+ def __repr__(self) -> str:
+ """Create a string (c)representation for ScreenOverlay."""
+ 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"name={self.name!r}, "
+ f"visibility={self.visibility!r}, "
+ f"isopen={self.isopen!r}, "
+ f"atom_link={self.atom_link!r}, "
+ f"atom_author={self.atom_author!r}, "
+ f"address={self.address!r}, "
+ f"phone_number={self.phone_number!r}, "
+ f"snippet={self.snippet!r}, "
+ f"description={self.description!r}, "
+ f"view={self.view!r}, "
+ f"times={self.times!r}, "
+ f"style_url={self.style_url!r}, "
+ f"styles={self.styles!r}, "
+ f"region={self.region!r}, "
+ f"extended_data={self.extended_data!r}, "
+ f"color={self.color!r}, "
+ f"draw_order={self.draw_order!r}, "
+ f"icon={self.icon!r}, "
+ f"overlay_xy={self.overlay_xy!r}, "
+ f"screen_xy={self.screen_xy!r}, "
+ f"rotation_xy={self.rotation_xy!r}, "
+ f"size={self.size!r}, "
+ f"rotation={self.rotation!r}, "
+ f"**{self._get_splat()!r},"
+ ")"
+ )
+
+
+registry.register(
+ ScreenOverlay,
+ RegistryItem(
+ ns_ids=("kml", ""),
+ attr_name="overlay_xy",
+ node_name="overlayXY",
+ classes=(OverlayXY,),
+ get_kwarg=xml_subelement_kwarg,
+ set_element=xml_subelement,
+ ),
+)
+registry.register(
+ ScreenOverlay,
+ RegistryItem(
+ ns_ids=("kml", ""),
+ attr_name="screen_xy",
+ node_name="screenXY",
+ classes=(ScreenXY,),
+ get_kwarg=xml_subelement_kwarg,
+ set_element=xml_subelement,
+ ),
+)
+registry.register(
+ ScreenOverlay,
+ RegistryItem(
+ ns_ids=("kml", ""),
+ attr_name="rotation_xy",
+ node_name="rotationXY",
+ classes=(RotationXY,),
+ get_kwarg=xml_subelement_kwarg,
+ set_element=xml_subelement,
+ ),
+)
+registry.register(
+ ScreenOverlay,
+ RegistryItem(
+ ns_ids=("kml", ""),
+ attr_name="size",
+ node_name="size",
+ classes=(Size,),
+ get_kwarg=xml_subelement_kwarg,
+ set_element=xml_subelement,
+ ),
+)
+registry.register(
+ ScreenOverlay,
+ RegistryItem(
+ ns_ids=("kml", ""),
+ attr_name="rotation",
+ node_name="rotation",
+ classes=(float,),
+ get_kwarg=subelement_float_kwarg,
+ set_element=float_subelement,
+ default=0.0,
+ ),
+)
diff --git a/fastkml/registry.py b/fastkml/registry.py
index 96e53bb1..d603a80b 100644
--- a/fastkml/registry.py
+++ b/fastkml/registry.py
@@ -20,14 +20,6 @@
This approach allows for flexible, declarative mapping between XML and Python objects,
with the registry acting as a central configuration for these mappings.
-Direct ``Registry`` class use is typically only for library internals or advanced
-customization. For normal usage, stick with the ``registry`` instance:
-
-- The library is designed around this global instance.
-- Ensures all parts of the library use the same registry.
-- Pre-populated with standard KML mappings.
-- Singleton pattern: Avoids multiple conflicting registries.
-
"""
from dataclasses import dataclass
@@ -89,6 +81,7 @@ class RegistryItem:
- ``attr_name``: The name of the attribute on the Python object that corresponds to
the XML object.
- ``get_kwarg``: A function that retrieves keyword arguments for the Python object.
+ - ``set_element``: A function that sets the XML element for the Python object.
- ``type``: The type of the XML object.
- ``node_name``: The name of the XML node that the mapping applies to.
- ``default``: An optional default value for the Python object attribute.
@@ -187,5 +180,18 @@ def get(self, cls: Type["_XMLObject"]) -> List[RegistryItem]:
registry = Registry()
+"""
+Global Registry.
+
+You should use ``registry.registry`` instead of instantiating ``registry.Registry()``
+because ``registry.registry`` is a pre-instantiated global instance, ensuring a single,
+shared registry across the entire application.
+It is pre-populated with all the necessary KML element registrations.
+If you need to add custom elements, you can extend the existing registry without
+creating a new one.
+Using the pre-defined ``registry.registry`` ensures you're working with the complete,
+properly initialized registry for the fastkml library.
+
+"""
-__all__ = ["registry", "RegistryItem"]
+__all__ = ["RegistryItem", "registry"]
diff --git a/fastkml/styles.py b/fastkml/styles.py
index 671a7943..b5b9292a 100644
--- a/fastkml/styles.py
+++ b/fastkml/styles.py
@@ -62,6 +62,19 @@
logger = logging.getLogger(__name__)
+__all__ = [
+ "BalloonStyle",
+ "HotSpot",
+ "IconStyle",
+ "LabelStyle",
+ "LineStyle",
+ "Pair",
+ "PolyStyle",
+ "Style",
+ "StyleMap",
+ "StyleUrl",
+]
+
class StyleUrl(_XMLObject):
"""
@@ -143,7 +156,7 @@ def get_tag_name(cls) -> str:
registry.register(
StyleUrl,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="url",
node_name="styleUrl",
classes=(str,),
@@ -229,7 +242,7 @@ def __init__(
registry.register(
_ColorStyle,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="color",
node_name="color",
classes=(str,),
@@ -241,7 +254,7 @@ def __init__(
registry.register(
_ColorStyle,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="color_mode",
node_name="colorMode",
classes=(ColorMode,),
@@ -513,7 +526,7 @@ def icon_href(self) -> Optional[str]:
registry.register(
IconStyle,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="scale",
node_name="scale",
classes=(float,),
@@ -525,7 +538,7 @@ def icon_href(self) -> Optional[str]:
registry.register(
IconStyle,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="heading",
node_name="heading",
classes=(float,),
@@ -537,7 +550,7 @@ def icon_href(self) -> Optional[str]:
registry.register(
IconStyle,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="icon",
node_name="Icon",
classes=(Icon,),
@@ -548,7 +561,7 @@ def icon_href(self) -> Optional[str]:
registry.register(
IconStyle,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="hot_spot",
node_name="hotSpot",
classes=(HotSpot,),
@@ -646,7 +659,7 @@ def __bool__(self) -> bool:
registry.register(
LineStyle,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="width",
node_name="width",
classes=(float,),
@@ -755,7 +768,7 @@ def __bool__(self) -> bool:
registry.register(
PolyStyle,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="fill",
node_name="fill",
classes=(bool,),
@@ -767,7 +780,7 @@ def __bool__(self) -> bool:
registry.register(
PolyStyle,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="outline",
node_name="outline",
classes=(bool,),
@@ -869,7 +882,7 @@ def __bool__(self) -> bool:
registry.register(
LabelStyle,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="scale",
node_name="scale",
classes=(float,),
@@ -1021,7 +1034,7 @@ def __bool__(self) -> bool:
registry.register(
BalloonStyle,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="bg_color",
node_name="bgColor",
classes=(str,),
@@ -1033,7 +1046,7 @@ def __bool__(self) -> bool:
registry.register(
BalloonStyle,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="text_color",
node_name="textColor",
classes=(str,),
@@ -1045,7 +1058,7 @@ def __bool__(self) -> bool:
registry.register(
BalloonStyle,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="text",
node_name="text",
classes=(str,),
@@ -1056,7 +1069,7 @@ def __bool__(self) -> bool:
registry.register(
BalloonStyle,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="display_mode",
node_name="displayMode",
classes=(DisplayMode,),
@@ -1148,7 +1161,7 @@ def __bool__(self) -> bool:
registry.register(
Style,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="styles",
node_name="Style",
classes=(
@@ -1258,7 +1271,7 @@ def __bool__(self) -> bool:
registry.register(
Pair,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="key",
node_name="key",
classes=(PairKey,),
@@ -1269,7 +1282,7 @@ def __bool__(self) -> bool:
registry.register(
Pair,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="style",
node_name="Style",
classes=(
@@ -1391,7 +1404,7 @@ def highlight(self) -> Optional[Union[StyleUrl, Style]]:
registry.register(
StyleMap,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="pairs",
node_name="Pair",
classes=(Pair,),
@@ -1399,15 +1412,3 @@ def highlight(self) -> Optional[Union[StyleUrl, Style]]:
set_element=xml_subelement_list,
),
)
-
-
-__all__ = [
- "BalloonStyle",
- "IconStyle",
- "LabelStyle",
- "LineStyle",
- "PolyStyle",
- "Style",
- "StyleMap",
- "StyleUrl",
-]
diff --git a/fastkml/times.py b/fastkml/times.py
index a2f35b5f..8f7a451e 100644
--- a/fastkml/times.py
+++ b/fastkml/times.py
@@ -41,7 +41,11 @@
from fastkml.registry import RegistryItem
from fastkml.registry import registry
-__all__ = ["KmlDateTime", "TimeSpan", "TimeStamp", "adjust_date_to_resolution"]
+__all__ = [
+ "KmlDateTime",
+ "TimeSpan",
+ "TimeStamp",
+]
# regular expression to parse a gYearMonth string
# year and month may be separated by an optional dash
diff --git a/fastkml/views.py b/fastkml/views.py
index 16ac27fe..57b30481 100644
--- a/fastkml/views.py
+++ b/fastkml/views.py
@@ -153,7 +153,7 @@ def __init__(
registry.register(
_AbstractView,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="longitude",
node_name="longitude",
classes=(float,),
@@ -165,7 +165,7 @@ def __init__(
registry.register(
_AbstractView,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="latitude",
node_name="latitude",
classes=(float,),
@@ -177,7 +177,7 @@ def __init__(
registry.register(
_AbstractView,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="altitude",
node_name="altitude",
classes=(float,),
@@ -189,7 +189,7 @@ def __init__(
registry.register(
_AbstractView,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="heading",
node_name="heading",
classes=(float,),
@@ -201,7 +201,7 @@ def __init__(
registry.register(
_AbstractView,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="tilt",
node_name="tilt",
classes=(float,),
@@ -317,7 +317,7 @@ def __repr__(self) -> str:
registry.register(
Camera,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="roll",
node_name="roll",
classes=(float,),
@@ -432,7 +432,7 @@ def __repr__(self) -> str:
registry.register(
LookAt,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="range",
node_name="range",
classes=(float,),
@@ -554,7 +554,7 @@ def __bool__(self) -> bool:
registry.register(
LatLonAltBox,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="north",
node_name="north",
classes=(float,),
@@ -565,7 +565,7 @@ def __bool__(self) -> bool:
registry.register(
LatLonAltBox,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="south",
node_name="south",
classes=(float,),
@@ -576,7 +576,7 @@ def __bool__(self) -> bool:
registry.register(
LatLonAltBox,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="east",
node_name="east",
classes=(float,),
@@ -587,7 +587,7 @@ def __bool__(self) -> bool:
registry.register(
LatLonAltBox,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="west",
node_name="west",
classes=(float,),
@@ -598,7 +598,7 @@ def __bool__(self) -> bool:
registry.register(
LatLonAltBox,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="min_altitude",
node_name="minAltitude",
classes=(float,),
@@ -610,7 +610,7 @@ def __bool__(self) -> bool:
registry.register(
LatLonAltBox,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="max_altitude",
node_name="maxAltitude",
classes=(float,),
@@ -716,7 +716,7 @@ def __bool__(self) -> bool:
registry.register(
Lod,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="min_lod_pixels",
node_name="minLodPixels",
classes=(float,),
@@ -728,7 +728,7 @@ def __bool__(self) -> bool:
registry.register(
Lod,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="max_lod_pixels",
node_name="maxLodPixels",
classes=(float,),
@@ -740,7 +740,7 @@ def __bool__(self) -> bool:
registry.register(
Lod,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="min_fade_extent",
node_name="minFadeExtent",
classes=(float,),
@@ -752,7 +752,7 @@ def __bool__(self) -> bool:
registry.register(
Lod,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="max_fade_extent",
node_name="maxFadeExtent",
classes=(float,),
@@ -852,7 +852,7 @@ def __bool__(self) -> bool:
registry.register(
Region,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="lat_lon_alt_box",
node_name="LatLonAltBox",
classes=(LatLonAltBox,),
@@ -863,7 +863,7 @@ def __bool__(self) -> bool:
registry.register(
Region,
RegistryItem(
- ns_ids=("kml",),
+ ns_ids=("kml", ""),
attr_name="lod",
node_name="Lod",
classes=(Lod,),
diff --git a/pyproject.toml b/pyproject.toml
index c525ae04..50f8d47d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -22,7 +22,11 @@ classifiers = [
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
+ "Programming Language :: Python :: Implementation :: CPython",
+ "Programming Language :: Python :: Implementation :: PyPy",
"Topic :: Scientific/Engineering :: GIS",
+ "Topic :: Software Development :: Libraries",
+ "Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Text Processing :: Markup :: XML",
"Typing :: Typed",
]
@@ -191,8 +195,6 @@ target-version = "py38"
[tool.ruff.lint]
ignore = [
"A002",
- "ANN101",
- "ANN102",
"ANN401",
"D203",
"D212",
diff --git a/tests/hypothesis/feature_test.py b/tests/hypothesis/feature_test.py
index 6f289c45..f04bc8e8 100644
--- a/tests/hypothesis/feature_test.py
+++ b/tests/hypothesis/feature_test.py
@@ -29,6 +29,7 @@
import fastkml.features
import fastkml.gx
import fastkml.links
+import fastkml.model
import fastkml.styles
import fastkml.views
from tests.base import Lxml
@@ -419,6 +420,44 @@ def test_fuzz_placemark_styles(
assert_str_roundtrip_terse(placemark)
assert_str_roundtrip_verbose(placemark)
+ @given(
+ model=st.builds(
+ fastkml.model.Model,
+ altitude_mode=st.sampled_from(fastkml.enums.AltitudeMode),
+ location=st.builds(
+ fastkml.model.Location,
+ latitude=st.floats(
+ allow_nan=False,
+ allow_infinity=False,
+ min_value=-90,
+ max_value=90,
+ ).filter(lambda x: x != 0),
+ longitude=st.floats(
+ allow_nan=False,
+ allow_infinity=False,
+ min_value=-180,
+ max_value=180,
+ ).filter(lambda x: x != 0),
+ altitude=st.floats(allow_nan=False, allow_infinity=False).filter(
+ lambda x: x != 0,
+ ),
+ ),
+ link=st.builds(fastkml.Link, href=urls()),
+ ),
+ )
+ def test_fuzz_placemark_model(
+ self,
+ model: fastkml.model.Model,
+ ) -> None:
+ placemark = fastkml.Placemark(
+ kml_geometry=model,
+ )
+
+ assert_repr_roundtrip(placemark)
+ assert_str_roundtrip(placemark)
+ assert_str_roundtrip_terse(placemark)
+ assert_str_roundtrip_verbose(placemark)
+
@given(
refresh_visibility=st.one_of(st.none(), st.booleans()),
fly_to_view=st.one_of(st.none(), st.booleans()),
diff --git a/tests/hypothesis/model_test.py b/tests/hypothesis/model_test.py
new file mode 100644
index 00000000..ee318107
--- /dev/null
+++ b/tests/hypothesis/model_test.py
@@ -0,0 +1,288 @@
+# 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
+"""Hypothesis tests for the fastkml.model module."""
+
+import typing
+
+from hypothesis import given
+from hypothesis import strategies as st
+from hypothesis.provisional import urls
+
+import fastkml
+import fastkml.enums
+import fastkml.links
+import fastkml.model
+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
+
+
+class TestLxml(Lxml):
+ @given(
+ altitude=st.one_of(
+ st.none(),
+ st.just(0.0),
+ st.floats(allow_nan=False, allow_infinity=False).filter(lambda x: x != 0),
+ ),
+ latitude=st.one_of(
+ st.none(),
+ st.just(0.0),
+ st.floats(
+ allow_nan=False,
+ allow_infinity=False,
+ min_value=-90,
+ max_value=90,
+ ),
+ ),
+ longitude=st.one_of(
+ st.none(),
+ st.just(0.0),
+ st.floats(
+ allow_nan=False,
+ allow_infinity=False,
+ min_value=-180,
+ max_value=180,
+ ),
+ ),
+ )
+ def test_fuzz_location(
+ self,
+ altitude: typing.Optional[float],
+ latitude: typing.Optional[float],
+ longitude: typing.Optional[float],
+ ) -> None:
+ location = fastkml.model.Location(
+ altitude=altitude,
+ latitude=latitude,
+ longitude=longitude,
+ )
+
+ assert_repr_roundtrip(location)
+ assert_str_roundtrip(location)
+ assert_str_roundtrip_terse(location)
+ assert_str_roundtrip_verbose(location)
+
+ @given(
+ heading=st.one_of(
+ st.none(),
+ st.just(0.0),
+ st.floats(
+ allow_nan=False,
+ allow_infinity=False,
+ min_value=-360,
+ max_value=360,
+ ).filter(lambda x: x != 0),
+ ),
+ tilt=st.one_of(
+ st.none(),
+ st.just(0.0),
+ st.floats(
+ allow_nan=False,
+ allow_infinity=False,
+ min_value=0,
+ max_value=180,
+ ).filter(lambda x: x != 0),
+ ),
+ roll=st.one_of(
+ st.none(),
+ st.just(0.0),
+ st.floats(
+ allow_nan=False,
+ allow_infinity=False,
+ min_value=-180,
+ max_value=180,
+ ).filter(lambda x: x != 0),
+ ),
+ )
+ def test_fuzz_orientation(
+ self,
+ heading: typing.Optional[float],
+ tilt: typing.Optional[float],
+ roll: typing.Optional[float],
+ ) -> None:
+ orientation = fastkml.model.Orientation(heading=heading, tilt=tilt, roll=roll)
+
+ assert_repr_roundtrip(orientation)
+ assert_str_roundtrip(orientation)
+ assert_str_roundtrip_terse(orientation)
+ assert_str_roundtrip_verbose(orientation)
+
+ @given(
+ x=st.one_of(st.none(), st.floats(allow_nan=False, allow_infinity=False)),
+ y=st.one_of(st.none(), st.floats(allow_nan=False, allow_infinity=False)),
+ z=st.one_of(st.none(), st.floats(allow_nan=False, allow_infinity=False)),
+ )
+ def test_fuzz_scale(
+ self,
+ x: typing.Optional[float],
+ y: typing.Optional[float],
+ z: typing.Optional[float],
+ ) -> None:
+ scale = fastkml.model.Scale(x=x, y=y, z=z)
+
+ assert_repr_roundtrip(scale)
+ assert_str_roundtrip(scale)
+ assert_str_roundtrip_terse(scale)
+ assert_str_roundtrip_verbose(scale)
+
+ @given(
+ target_href=st.one_of(st.none(), urls()),
+ source_href=st.one_of(st.none(), urls()),
+ )
+ def test_fuzz_alias(
+ self,
+ target_href: typing.Optional[str],
+ source_href: typing.Optional[str],
+ ) -> None:
+ alias = fastkml.model.Alias(target_href=target_href, source_href=source_href)
+
+ assert_repr_roundtrip(alias)
+ assert_str_roundtrip(alias)
+ assert_str_roundtrip_terse(alias)
+ assert_str_roundtrip_verbose(alias)
+
+ @given(
+ aliases=st.one_of(
+ st.none(),
+ st.lists(
+ st.builds(
+ fastkml.model.Alias,
+ source_href=urls(),
+ target_href=urls(),
+ ),
+ ),
+ ),
+ )
+ def test_fuzz_resource_map(
+ self,
+ aliases: typing.Optional[typing.Iterable[fastkml.model.Alias]],
+ ) -> None:
+ resource_map = fastkml.model.ResourceMap(aliases=aliases)
+
+ assert_repr_roundtrip(resource_map)
+ assert_str_roundtrip(resource_map)
+ assert_str_roundtrip_terse(resource_map)
+ assert_str_roundtrip_verbose(resource_map)
+
+ @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)),
+ location=st.one_of(
+ st.none(),
+ st.builds(
+ fastkml.model.Location,
+ altitude=st.floats(allow_nan=False, allow_infinity=False).filter(
+ lambda x: x != 0,
+ ),
+ latitude=st.floats(
+ allow_nan=False,
+ allow_infinity=False,
+ min_value=-90,
+ max_value=90,
+ ),
+ longitude=st.floats(
+ allow_nan=False,
+ allow_infinity=False,
+ min_value=-180,
+ max_value=180,
+ ),
+ ),
+ ),
+ orientation=st.one_of(
+ st.none(),
+ st.builds(
+ fastkml.model.Orientation,
+ heading=st.floats(
+ allow_nan=False,
+ allow_infinity=False,
+ min_value=-360,
+ max_value=360,
+ ).filter(lambda x: x != 0),
+ tilt=st.floats(
+ allow_nan=False,
+ allow_infinity=False,
+ min_value=0,
+ max_value=180,
+ ).filter(lambda x: x != 0),
+ roll=st.floats(
+ allow_nan=False,
+ allow_infinity=False,
+ min_value=-180,
+ max_value=180,
+ ).filter(lambda x: x != 0),
+ ),
+ ),
+ scale=st.one_of(
+ st.none(),
+ st.builds(
+ fastkml.model.Scale,
+ x=st.floats(allow_nan=False, allow_infinity=False).filter(
+ lambda x: x != 0,
+ ),
+ y=st.floats(allow_nan=False, allow_infinity=False).filter(
+ lambda x: x != 0,
+ ),
+ z=st.floats(allow_nan=False, allow_infinity=False).filter(
+ lambda x: x != 0,
+ ),
+ ),
+ ),
+ link=st.one_of(st.none(), st.builds(fastkml.Link, href=urls())),
+ resource_map=st.one_of(
+ st.none(),
+ st.builds(
+ fastkml.model.ResourceMap,
+ aliases=st.lists(
+ st.builds(
+ fastkml.model.Alias,
+ source_href=urls(),
+ target_href=urls(),
+ ),
+ min_size=1,
+ ),
+ ),
+ ),
+ )
+ def test_fuzz_model(
+ self,
+ id: typing.Optional[str],
+ target_id: typing.Optional[str],
+ altitude_mode: typing.Optional[fastkml.enums.AltitudeMode],
+ location: typing.Optional[fastkml.model.Location],
+ orientation: typing.Optional[fastkml.model.Orientation],
+ scale: typing.Optional[fastkml.model.Scale],
+ link: typing.Optional[fastkml.Link],
+ resource_map: typing.Optional[fastkml.model.ResourceMap],
+ ) -> None:
+ model = fastkml.model.Model(
+ id=id,
+ target_id=target_id,
+ altitude_mode=altitude_mode,
+ location=location,
+ orientation=orientation,
+ scale=scale,
+ link=link,
+ resource_map=resource_map,
+ )
+
+ assert_repr_roundtrip(model)
+ assert_str_roundtrip(model)
+ assert_str_roundtrip_terse(model)
+ assert_str_roundtrip_verbose(model)
diff --git a/tests/hypothesis/network_link_control_test.py b/tests/hypothesis/network_link_control_test.py
new file mode 100644
index 00000000..3ff1d963
--- /dev/null
+++ b/tests/hypothesis/network_link_control_test.py
@@ -0,0 +1,122 @@
+# 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
+"""Hypothesis tests for the fastkml.network_link_control module."""
+
+import typing
+
+from hypothesis import given
+from hypothesis import strategies as st
+
+import fastkml
+import fastkml.enums
+import fastkml.model
+import fastkml.views
+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 kml_datetimes
+from tests.hypothesis.strategies import xml_text
+
+
+class TestLxml(Lxml):
+ @given(
+ min_refresh_period=st.one_of(
+ st.none(),
+ st.floats(allow_nan=False, allow_infinity=False).filter(lambda x: x != 0),
+ ),
+ max_session_length=st.one_of(
+ st.none(),
+ st.floats(allow_nan=False, allow_infinity=False).filter(lambda x: x != -1),
+ ),
+ cookie=st.one_of(st.none(), xml_text()),
+ message=st.one_of(st.none(), xml_text()),
+ link_name=st.one_of(st.none(), xml_text()),
+ link_description=st.one_of(st.none(), xml_text()),
+ link_snippet=st.one_of(st.none(), xml_text()),
+ expires=st.one_of(
+ st.none(),
+ kml_datetimes(),
+ ),
+ view=st.one_of(
+ st.none(),
+ st.builds(
+ fastkml.views.Camera,
+ longitude=st.floats(
+ allow_nan=False,
+ allow_infinity=False,
+ min_value=-180,
+ max_value=180,
+ ).filter(lambda x: x != 0),
+ latitude=st.floats(
+ allow_nan=False,
+ allow_infinity=False,
+ min_value=-90,
+ max_value=90,
+ ).filter(lambda x: x != 0),
+ altitude=st.floats(allow_nan=False, allow_infinity=False).filter(
+ lambda x: x != 0,
+ ),
+ ),
+ st.builds(
+ fastkml.views.LookAt,
+ longitude=st.floats(
+ allow_nan=False,
+ allow_infinity=False,
+ min_value=-180,
+ max_value=180,
+ ).filter(lambda x: x != 0),
+ latitude=st.floats(
+ allow_nan=False,
+ allow_infinity=False,
+ min_value=-90,
+ max_value=90,
+ ).filter(lambda x: x != 0),
+ altitude=st.floats(allow_nan=False, allow_infinity=False).filter(
+ lambda x: x != 0,
+ ),
+ ),
+ ),
+ )
+ def test_fuzz_network_link_control(
+ self,
+ min_refresh_period: typing.Optional[float],
+ max_session_length: typing.Optional[float],
+ cookie: typing.Optional[str],
+ message: typing.Optional[str],
+ link_name: typing.Optional[str],
+ link_description: typing.Optional[str],
+ link_snippet: typing.Optional[str],
+ expires: typing.Optional[fastkml.KmlDateTime],
+ view: typing.Union[fastkml.Camera, fastkml.LookAt, None],
+ ) -> None:
+ nlc = fastkml.NetworkLinkControl(
+ min_refresh_period=min_refresh_period,
+ max_session_length=max_session_length,
+ cookie=cookie,
+ message=message,
+ link_name=link_name,
+ link_description=link_description,
+ link_snippet=link_snippet,
+ expires=expires,
+ view=view,
+ )
+
+ assert_repr_roundtrip(nlc)
+ assert_str_roundtrip(nlc)
+ assert_str_roundtrip_terse(nlc)
+ assert_str_roundtrip_verbose(nlc)
diff --git a/tests/hypothesis/overlay_test.py b/tests/hypothesis/overlay_test.py
index 5d7762ef..807e50bb 100644
--- a/tests/hypothesis/overlay_test.py
+++ b/tests/hypothesis/overlay_test.py
@@ -17,6 +17,7 @@
import typing
+import pytest
from hypothesis import given
from hypothesis import strategies as st
from pygeoif.hypothesis.strategies import epsg4326
@@ -31,6 +32,7 @@
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 xy
class TestLxml(Lxml):
@@ -242,3 +244,68 @@ def test_fuzz_ground_overlay(
assert_str_roundtrip(ground_overlay)
assert_str_roundtrip_terse(ground_overlay)
assert_str_roundtrip_verbose(ground_overlay)
+
+ @pytest.mark.parametrize(
+ "cls",
+ [
+ fastkml.overlays.OverlayXY,
+ fastkml.overlays.RotationXY,
+ fastkml.overlays.ScreenXY,
+ fastkml.overlays.Size,
+ ],
+ )
+ @given(
+ x=st.one_of(st.none(), st.floats(allow_nan=False, allow_infinity=False)),
+ y=st.one_of(st.none(), st.floats(allow_nan=False, allow_infinity=False)),
+ x_units=st.one_of(st.none(), st.sampled_from(fastkml.enums.Units)),
+ y_units=st.one_of(st.none(), st.sampled_from(fastkml.enums.Units)),
+ )
+ def test_fuzz_xy(
+ self,
+ cls: typing.Union[
+ typing.Type[fastkml.overlays.OverlayXY],
+ typing.Type[fastkml.overlays.RotationXY],
+ typing.Type[fastkml.overlays.ScreenXY],
+ typing.Type[fastkml.overlays.Size],
+ ],
+ x: typing.Optional[float],
+ y: typing.Optional[float],
+ x_units: typing.Optional[fastkml.enums.Units],
+ y_units: typing.Optional[fastkml.enums.Units],
+ ) -> None:
+ xy = cls(x=x, y=y, x_units=x_units, y_units=y_units)
+
+ assert_repr_roundtrip(xy)
+ assert_str_roundtrip(xy)
+ assert_str_roundtrip_terse(xy)
+ assert_str_roundtrip_verbose(xy)
+
+ @given(
+ overlay_xy=xy(fastkml.overlays.OverlayXY),
+ screen_xy=xy(fastkml.overlays.ScreenXY),
+ rotation_xy=xy(fastkml.overlays.RotationXY),
+ size=xy(fastkml.overlays.Size),
+ rotation=st.floats(min_value=-180, max_value=180).filter(lambda x: x != 0),
+ )
+ def test_screen_overlay(
+ self,
+ overlay_xy: typing.Optional[fastkml.overlays.OverlayXY],
+ screen_xy: typing.Optional[fastkml.overlays.ScreenXY],
+ rotation_xy: typing.Optional[fastkml.overlays.RotationXY],
+ size: typing.Optional[fastkml.overlays.Size],
+ rotation: typing.Optional[float],
+ ) -> None:
+ screen_overlay = fastkml.overlays.ScreenOverlay(
+ id="screen_overlay1",
+ name="screen_overlay",
+ overlay_xy=overlay_xy,
+ screen_xy=screen_xy,
+ rotation_xy=rotation_xy,
+ size=size,
+ rotation=rotation,
+ )
+
+ assert_repr_roundtrip(screen_overlay)
+ assert_str_roundtrip(screen_overlay)
+ assert_str_roundtrip_terse(screen_overlay)
+ assert_str_roundtrip_verbose(screen_overlay)
diff --git a/tests/hypothesis/strategies.py b/tests/hypothesis/strategies.py
index 4390510f..e070c450 100644
--- a/tests/hypothesis/strategies.py
+++ b/tests/hypothesis/strategies.py
@@ -178,6 +178,14 @@
when=kml_datetimes(),
)
+xy = partial(
+ st.builds,
+ x=st.floats(allow_nan=False, allow_infinity=False),
+ y=st.floats(allow_nan=False, allow_infinity=False),
+ x_units=st.sampled_from(fastkml.enums.Units),
+ y_units=st.sampled_from(fastkml.enums.Units),
+)
+
@st.composite
def query_strings(draw: st.DrawFn) -> str:
diff --git a/tests/model_test.py b/tests/model_test.py
new file mode 100644
index 00000000..fb10c150
--- /dev/null
+++ b/tests/model_test.py
@@ -0,0 +1,148 @@
+# Copyright (C) 2021 - 2023 Christian Ledermann
+#
+# This library is free software; you can redistribute it and/or modify it under
+# the terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation; either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# This library is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this library; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+"""Test the kml overlay classes."""
+
+from pygeoif.geometry import Point
+
+import fastkml.links
+import fastkml.model
+from fastkml.enums import AltitudeMode
+from tests.base import Lxml
+from tests.base import StdLibrary
+
+
+class TestModel(StdLibrary):
+ def test_from_string(self) -> None:
+ doc = (
+ ''
+ "absolute"
+ ""
+ "-123.115776547816"
+ "49.279804095564"
+ "21.614010375743"
+ ""
+ "111"
+ "http://barcelona.galdos.local/files/PublicLibrary.dae"
+ ''
+ ""
+ "http://barcelona.galdos.local/images/Concrete2.jpg"
+ "../images/Concrete.jpg"
+ ""
+ ""
+ ""
+ )
+
+ model = fastkml.model.Model.from_string(doc)
+
+ assert model.altitude_mode == AltitudeMode.absolute
+ assert model.geometry == Point(
+ -123.115776547816,
+ 49.279804095564,
+ 21.614010375743,
+ )
+ assert model == fastkml.model.Model(
+ altitude_mode=AltitudeMode.absolute,
+ location=fastkml.model.Location(
+ altitude=21.614010375743,
+ latitude=49.279804095564,
+ longitude=-123.115776547816,
+ ),
+ orientation=None,
+ scale=fastkml.model.Scale(
+ x=1.0,
+ y=1.0,
+ z=1.0,
+ ),
+ link=fastkml.links.Link(
+ href="http://barcelona.galdos.local/files/PublicLibrary.dae",
+ ),
+ resource_map=fastkml.model.ResourceMap(
+ aliases=[
+ fastkml.model.Alias(
+ target_href="http://barcelona.galdos.local/images/Concrete2.jpg",
+ source_href="../images/Concrete.jpg",
+ ),
+ ],
+ ),
+ )
+
+ def test_from_string_no_location(self) -> None:
+ doc = (
+ ''
+ "absolute"
+ "111"
+ "http://barcelona.galdos.local/files/PublicLibrary.dae"
+ ''
+ ""
+ "http://barcelona.galdos.local/images/Concrete2.jpg"
+ "../images/Concrete.jpg"
+ ""
+ ""
+ ""
+ )
+
+ model = fastkml.model.Model.from_string(doc)
+
+ assert model.altitude_mode == AltitudeMode.absolute
+ assert model.geometry is None
+ assert not model
+
+ def test_from_string_invalid_location(self) -> None:
+ doc = (
+ ''
+ "absolute"
+ ""
+ ""
+ "49.279804095564"
+ "21.614010375743"
+ ""
+ "111"
+ "http://barcelona.galdos.local/files/PublicLibrary.dae"
+ ''
+ ""
+ "http://barcelona.galdos.local/images/Concrete2.jpg"
+ "../images/Concrete.jpg"
+ ""
+ ""
+ ""
+ )
+
+ model = fastkml.model.Model.from_string(doc)
+
+ assert model.altitude_mode == AltitudeMode.absolute
+ assert model.geometry is None
+ assert not model
+
+ def test_location_from_string_invalid_location(self) -> None:
+ doc = (
+ ''
+ ""
+ "49.279804095564"
+ "21.614010375743"
+ ""
+ )
+
+ location = fastkml.model.Location.from_string(doc)
+
+ assert location.altitude == 21.614010375743
+ assert location.latitude == 49.279804095564
+ assert location.geometry is None
+ assert not location
+
+
+class TestModelLxml(TestModel, Lxml):
+ pass
diff --git a/tests/network_link_control_test.py b/tests/network_link_control_test.py
new file mode 100644
index 00000000..5cfa0ac4
--- /dev/null
+++ b/tests/network_link_control_test.py
@@ -0,0 +1,81 @@
+# Copyright (C) 2021 - 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
+
+"""Test the Network Link Control classes."""
+
+import datetime
+
+from dateutil.tz import tzutc
+
+from fastkml import views
+from fastkml.network_link_control import NetworkLinkControl
+from fastkml.times import KmlDateTime
+from tests.base import StdLibrary
+
+
+class TestStdLibrary(StdLibrary):
+ """Test with the standard library."""
+
+ def test_network_link_control_obj(self) -> None:
+ dt = datetime.datetime.now(tz=tzutc())
+ kml_datetime = KmlDateTime(dt=dt)
+ view = views.Camera()
+
+ network_control_obj = NetworkLinkControl(
+ min_refresh_period=1.1,
+ max_session_length=100.1,
+ cookie="cookie",
+ message="message",
+ link_name="link_name",
+ link_description="link_description",
+ link_snippet="link_snippet",
+ expires=kml_datetime,
+ view=view,
+ )
+
+ assert network_control_obj.min_refresh_period == 1.1
+ assert network_control_obj.max_session_length == 100.1
+ assert network_control_obj.cookie == "cookie"
+ assert network_control_obj.message == "message"
+ assert network_control_obj.link_name == "link_name"
+ assert network_control_obj.link_description == "link_description"
+ assert network_control_obj.link_snippet == "link_snippet"
+ assert str(network_control_obj.expires) == str(kml_datetime)
+ assert str(network_control_obj.view) == str(view)
+
+ def test_network_link_control_kml(self) -> None:
+ doc = (
+ ''
+ "432000"
+ "-1"
+ "A Snippet"
+ "2008-05-30"
+ ""
+ )
+
+ nc = NetworkLinkControl.from_string(doc)
+
+ dt = datetime.date(2008, 5, 30)
+ kml_datetime = KmlDateTime(dt=dt)
+
+ nc_obj = NetworkLinkControl(
+ min_refresh_period=432000,
+ max_session_length=-1,
+ link_snippet="A Snippet",
+ expires=kml_datetime,
+ )
+
+ assert nc == nc_obj
diff --git a/tests/overlays_test.py b/tests/overlays_test.py
index 3a7a71cf..0209e6ed 100644
--- a/tests/overlays_test.py
+++ b/tests/overlays_test.py
@@ -14,7 +14,9 @@
# along with this library; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-"""Test the kml classes."""
+"""Test the kml overlay classes."""
+
+import contextlib
from pygeoif import geometry as geo
@@ -24,15 +26,74 @@
from fastkml import overlays
from fastkml import views
from fastkml.enums import AltitudeMode
+from fastkml.enums import Units
from tests.base import Lxml
from tests.base import StdLibrary
-class TestGroundOverlay(StdLibrary):
- pass
+class TestScreenOverlay(StdLibrary):
+ def test_screen_overlay_from_string(self) -> None:
+ """Create a ScreenOverlay object with all optional parameters."""
+ doc = (
+ ''
+ "Simple crosshairs"
+ "0"
+ "This screen overlay uses fractional positioning to put the "
+ "image in the exact center of the screen"
+ ""
+ "http://developers.google.com/kml/images/crosshairs.png"
+ ""
+ ''
+ ''
+ ''
+ ''
+ "-45"
+ ""
+ )
+
+ screen_overlay = overlays.ScreenOverlay.from_string(doc)
+
+ assert screen_overlay == overlays.ScreenOverlay(
+ name="Simple crosshairs",
+ visibility=False,
+ description=(
+ "This screen overlay uses fractional positioning to put the image "
+ "in the exact center of the screen"
+ ),
+ icon=links.Icon(
+ href="http://developers.google.com/kml/images/crosshairs.png",
+ ),
+ overlay_xy=overlays.OverlayXY(
+ x=0.5,
+ y=0.5,
+ x_units=Units.fraction,
+ y_units=Units.fraction,
+ ),
+ screen_xy=overlays.ScreenXY(
+ x=0.5,
+ y=0.5,
+ x_units=Units.fraction,
+ y_units=Units.fraction,
+ ),
+ rotation_xy=overlays.RotationXY(
+ x=0.5,
+ y=0.5,
+ x_units=Units.fraction,
+ y_units=Units.fraction,
+ ),
+ size=overlays.Size(
+ x=0.0,
+ y=0.0,
+ x_units=Units.pixels,
+ y_units=Units.pixels,
+ ),
+ rotation=-45,
+ )
+ with contextlib.suppress(TypeError):
+ screen_overlay.validate()
-class TestGroundOverlayString(StdLibrary):
+class TestGroundOverlay(StdLibrary):
def test_default_to_string(self) -> None:
g = overlays.GroundOverlay()
@@ -362,11 +423,11 @@ def test_camera_initialization(self) -> None:
assert po.view.roll == 60
-class TestGroundOverlayLxml(Lxml, TestGroundOverlay):
+class TestScreenOverlayLxml(Lxml, TestScreenOverlay):
"""Test with lxml."""
-class TestGroundOverlayStringLxml(Lxml, TestGroundOverlay):
+class TestGroundOverlayLxml(Lxml, TestGroundOverlay):
"""Test with lxml."""