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."""