pax_global_header00006660000000000000000000000064141603554410014515gustar00rootroot0000000000000052 comment=cbe85285ab6f05afe9beabc66a86e05a4d125c43 geopython-pygml-5ce8eb0/000077500000000000000000000000001416035544100153545ustar00rootroot00000000000000geopython-pygml-5ce8eb0/.bumpversion.cfg000066400000000000000000000006271416035544100204710ustar00rootroot00000000000000[bumpversion] current_version = 0.2.2 commit = True tag = True tag_name = release-{new_version} [bumpversion:file:pygml/__init__.py] search = __version__ = '{current_version}' replace = __version__ = '{new_version}' [bumpversion:file:.bumpversion.cfg] search = current_version = {current_version} [bumpversion:file:docs/conf.py] search = release = '{current_version}' replace = release = '{new_version}' geopython-pygml-5ce8eb0/.github/000077500000000000000000000000001416035544100167145ustar00rootroot00000000000000geopython-pygml-5ce8eb0/.github/workflows/000077500000000000000000000000001416035544100207515ustar00rootroot00000000000000geopython-pygml-5ce8eb0/.github/workflows/publish.yaml000066400000000000000000000011711416035544100233030ustar00rootroot00000000000000name: publish on: push: tags: - release-* jobs: publish: runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 name: Setup Python with: python-version: "3.x" - name: Install build dependency run: pip install wheel - name: Build package run: python setup.py sdist bdist_wheel --universal - name: Publish package if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') uses: pypa/gh-action-pypi-publish@release/v1 with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }}geopython-pygml-5ce8eb0/.github/workflows/test.yaml000066400000000000000000000010241416035544100226110ustar00rootroot00000000000000name: test on: [ push, pull_request ] jobs: test: runs-on: ubuntu-20.04 strategy: matrix: python-version: [3.6, 3.7, 3.8, 3.9] steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 name: Setup Python ${{ matrix.python-version }} with: python-version: ${{ matrix.python-version }} - name: Install requirements 📦 run: | pip install -r requirements-test.txt pip install . - name: Run unit tests ⚙️ run: | pytest geopython-pygml-5ce8eb0/.gitignore000066400000000000000000000034171416035544100173510ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ .vscodegeopython-pygml-5ce8eb0/.readthedocs.yaml000066400000000000000000000006561416035544100206120ustar00rootroot00000000000000# .readthedocs.yaml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/conf.py # Optionally set requirements required to build your docs python: version: "3.8" install: - requirements: docs/requirements.txt - requirements: requirements-test.txt geopython-pygml-5ce8eb0/CHANGELOG.md000066400000000000000000000004221416035544100171630ustar00rootroot00000000000000# 0.2.1 (2021-08-31) - Fixing case handling when parsing CRS strings # 0.2.0 (2021-08-09) - Adding parsing and encoding of GML 3.x (< 3.2) and GML 3.3 compact encoding - Adding GeoRSS encoding support # 0.1.0 - Adding GeoRSS parsing support # 0.0.1 - Initial release geopython-pygml-5ce8eb0/CONTRIBUTING.md000066400000000000000000000114621416035544100176110ustar00rootroot00000000000000# Contributing We welcome contributions to pygml, in the form of issues, bug fixes, documentation or suggestions for enhancements. This document sets out our guidelines and best practices for such contributions. It's based on the [Contributing to pygeoapi](https://github.com/geopython/pygeoapi/blob/master/CONTRIBUTING.md) guide which is based on the [Contributing to Open Source Projects Guide](https://contribution-guide-org.readthedocs.io/). pygml has the following modes of contribution: - GitHub Commit Access - GitHub Pull Requests ## Code of Conduct Contributors to this project are expected to act respectfully toward others in accordance with the [OSGeo Code of Conduct](https://www.osgeo.org/code_of_conduct). ## Submitting Bugs ### Due Diligence Before submitting a bug, please do the following: * Perform __basic troubleshooting__ steps: * Make sure you're on the latest version. If you're not on the most recent version, your problem may have been solved already! Upgrading is always the best first step. * [Search the issue tracker](https://github.com/geopython/pygml/issues) to make sure it's not a known issue. ### What to put in your bug report Make sure your report gets the attention it deserves: bug reports with missing information may be ignored or punted back to you, delaying a fix. The below constitutes a bare minimum; more info is almost always better: * __What version of Python are you using?__ For example, are you using Python 2.7, Python 3.7, PyPy 2.0? * __What operating system are you using?__ Windows (7, 8, 10, 32-bit, 64-bit), Mac OS X, (10.7.4, 10.9.0), GNU/Linux (which distribution, which version?) Again, more detail is better. * __Which version or versions of the software are you using?__ Ideally, you've followed the advice above and are on the latest version, but please confirm this. * __How can the we recreate your problem?__ Imagine that we have never used pygml before and have downloaded it for the first time. Exactly what steps do we need to take to reproduce your problem? ## Contributions and Licensing ### Contributor License Agreement Your contribution will be under our [license](https://github.com/geopython/pygml/blob/main/LICENSE) as per [GitHub's terms of service](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license). ### GitHub Commit Access * Proposals to provide developers with GitHub commit access shall be raised on the pygml [discussions page](https://github.com/geopython/pygml/discussions). Committers shall be added by the project admin. * Removal of commit access shall be handled in the same manner. ### GitHub Pull Requests * Pull requests may include copyright in the source code header by the contributor if the contribution is significant or the contributor wants to claim copyright on their contribution. * All contributors shall be listed at https://github.com/geopython/pygml/graphs/contributors * Unclaimed copyright, by default, is assigned to the main copyright holders as specified in https://github.com/geopython/pygml/blob/main/LICENSE ### Version Control Branching * Always __make a new branch__ for your work, no matter how small. This makes it easy for others to take just that one set of changes from your repository, in case you have multiple unrelated changes floating around. * __Don't submit unrelated changes in the same branch/pull request!__ If it is not possible to review your changes quickly and easily, we may reject your request. * __Base your new branch off of the appropriate branch__ on the main repository: * In general the released version of pygml is based on the ``main`` (default) branch whereas development work is done under other non-default branches. Unless you are sure that your issue affects a non-default branch, __base your branch off the ``main`` one__. * Note that depending on how long it takes for the dev team to merge your patch, the copy of ``main`` you worked off of may get out of date! * If you find yourself 'bumping' a pull request that's been sidelined for a while, __make sure you rebase or merge to latest ``main``__ to ensure a speedier resolution. ### Documentation * documentation is managed in `docs/`, in reStructuredText format * [Sphinx](https://www.sphinx-doc.org) is used to generate the documentation * See the [reStructuredText Primer](https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html) on rST markup and syntax ### Code Formatting * __Please follow the coding conventions and style used in the pygml repository.__ * pygml follows the [PEP-8](http://www.python.org/dev/peps/pep-0008/) guidelines * 80 characters * spaces, not tabs * pygml, instead of PyGML, pyGml, etc. ## Suggesting Enhancements We welcome suggestions for enhancements, but reserve the right to reject them if they do not follow future plans for pygml. geopython-pygml-5ce8eb0/LICENSE000066400000000000000000000020521416035544100163600ustar00rootroot00000000000000MIT License Copyright (c) 2021 geopython Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. geopython-pygml-5ce8eb0/README.md000066400000000000000000000026341416035544100166400ustar00rootroot00000000000000# pygml A pure python parser and encoder for OGC GML Geometries. [![PyPI version](https://badge.fury.io/py/pygml.svg)](https://badge.fury.io/py/pygml) [![CI](https://github.com/geopython/pygml/actions/workflows/test.yaml/badge.svg)](https://github.com/geopython/pygml/actions/workflows/test.yaml) [![Documentation Status](https://readthedocs.org/projects/pygml/badge/?version=latest)](https://pygml.readthedocs.io/en/latest/?badge=latest) ## Installation ```bash $ pip install pygml ``` ## Features Parse GML 3.1, 3.2, compact encoded GML 3.3 and GeoRSS geometries to a [Geo Interface](https://gist.github.com/sgillies/2217756) compliant class. ```python >>> import pygml >>> geom = pygml.parse(""" ... ... 1.0 1.0 ... ... """) >>> print(geom) Geometry(geometry={'type': 'Point', 'coordinates': (1.0, 1.0)}) >>> print(geom.__geo_interface__) {'type': 'Point', 'coordinates': (1.0, 1.0)} ``` Conversely, it is possible to encode GeoJSON or Geo Interfaces to GML ```python >>> from pygml.v32 import encode_v32 >>> from lxml import etree >>> tree = encode_v32({'type': 'Point', 'coordinates': (1.0, 1.0)}, 'ID') >>> print(etree.tostring(tree, pretty_print=True).decode()) 1.0 1.0 >>> ```geopython-pygml-5ce8eb0/SECURITY.md000066400000000000000000000010521416035544100171430ustar00rootroot00000000000000# pygml Security Policy ## Supported Versions Security/vulnerability reports **should not** be submitted through GitHub issues or public discussions, but instead please send your report to **geopython-security nospam @ lists.osgeo.org** - (remove the blanks and 'nospam'). ## Supported Versions pygml developers will release patches for security vulnerabilities for the following versions: | Version | Supported | | ------- | ------------------ | | latest stable version | :white_check_mark: | | previous versions | :x: | geopython-pygml-5ce8eb0/docs/000077500000000000000000000000001416035544100163045ustar00rootroot00000000000000geopython-pygml-5ce8eb0/docs/.gitignore000066400000000000000000000000041416035544100202660ustar00rootroot00000000000000api geopython-pygml-5ce8eb0/docs/Makefile000066400000000000000000000011041416035544100177400ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # 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)geopython-pygml-5ce8eb0/docs/conf.py000066400000000000000000000130601416035544100176030ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Configuration file for the Sphinx documentation builder. # # This file does only contain a selection of the most common options. For a # full list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Path setup -------------------------------------------------------------- # 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. # import os import sys sys.path.insert(0, os.path.abspath('..')) # -- Project information ----------------------------------------------------- project = 'pygml' copyright = '2021, Fabian Schindler' author = 'Fabian Schindler' # The short X.Y version version = '' # The full version, including alpha/beta/rc tags release = '0.2.2' # -- General configuration --------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # 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.intersphinx', 'sphinxcontrib.apidoc', 'm2r2', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The master toctree document. master_doc = 'index' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # The name of the Pygments (syntax highlighting) style to use. pygments_style = None # -- 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 = 'pydata_sphinx_theme' html_theme_options = { "github_url": "https://github.com/geopython/pygml", } # 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 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'] # Custom sidebar templates, must be a dictionary that maps document names # to template names. # # The default sidebars (for documents that don't match any pattern) are # defined by theme itself. Builtin themes are using these templates by # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', # 'searchbox.html']``. # # html_sidebars = {} # -- Options for HTMLHelp output --------------------------------------------- # Output file base name for HTML help builder. htmlhelp_basename = 'pygmldoc' # -- 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': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # 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 = [ (master_doc, 'pygml.tex', 'pygml Documentation', 'Fabian Schindler', 'manual'), ] # -- Options for manual page output ------------------------------------------ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'pygml', 'pygml Documentation', [author], 1) ] # -- 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 = [ (master_doc, 'pygml', 'pygml Documentation', author, 'pygml', 'One line description of project.', 'Miscellaneous'), ] # -- Options for Epub output ------------------------------------------------- # Bibliographic Dublin Core info. epub_title = project # The unique identifier of the text. This can be a ISBN number # or the project homepage. # # epub_identifier = '' # A unique identification for the text. # # epub_uid = '' # A list of files that should not be packed into the epub file. epub_exclude_files = ['search.html'] # -- Extension configuration ------------------------------------------------- intersphinx_mapping = { 'python': ('https://python.readthedocs.org/en/latest/', None), } # apidoc configs: apidoc_module_dir = '../pygml' apidoc_output_dir = 'api' # apidoc_excluded_paths = ['tests'] # apidoc_separate_modules = True # apidoc_module_first = True geopython-pygml-5ce8eb0/docs/contributing.rst000066400000000000000000000000411416035544100215400ustar00rootroot00000000000000.. mdinclude:: ../CONTRIBUTING.mdgeopython-pygml-5ce8eb0/docs/index.rst000066400000000000000000000003301416035544100201410ustar00rootroot00000000000000.. mdinclude:: ../README.md .. toctree:: :maxdepth: 2 :caption: Contents: license contributing api/modules Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` geopython-pygml-5ce8eb0/docs/license.rst000066400000000000000000000000651416035544100204610ustar00rootroot00000000000000License ======= .. include:: ../LICENSE :literal:geopython-pygml-5ce8eb0/docs/make.bat000066400000000000000000000014271416035544100177150ustar00rootroot00000000000000@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=. set BUILDDIR=_build if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.https://www.sphinx-doc.org exit /b 1 ) %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% :end popd geopython-pygml-5ce8eb0/docs/requirements.txt000066400000000000000000000000561416035544100215710ustar00rootroot00000000000000sphinxcontrib-apidoc pydata-sphinx-theme m2r2 geopython-pygml-5ce8eb0/pygml/000077500000000000000000000000001416035544100165045ustar00rootroot00000000000000geopython-pygml-5ce8eb0/pygml/__init__.py000066400000000000000000000030021416035544100206100ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # # Project: pygml # Authors: Fabian Schindler # # ------------------------------------------------------------------------------ # Copyright (C) 2021 EOX IT Services GmbH # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies of this Software or works derived from this Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # ------------------------------------------------------------------------------ from .parse import parse __version__ = '0.2.2' __all__ = ['parse'] geopython-pygml-5ce8eb0/pygml/axisorder.py000066400000000000000000000342631416035544100210660ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # # Project: pygml # Authors: Fabian Schindler # # ------------------------------------------------------------------------------ # Copyright (C) 2021 EOX IT Services GmbH # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies of this Software or works derived from this Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # ------------------------------------------------------------------------------ import re from typing import Union # copied from: # https://github.com/geopython/OWSLib/blob/a9c1be25676ab530fd0327c2450922f288ca25f4/owslib/crs.py AXISORDER_YX = { 4326, 4258, 31466, 31467, 31468, 31469, 2166, 2167, 2168, 2036, 2044, 2045, 2065, 2081, 2082, 2083, 2085, 2086, 2091, 2092, 2093, 2096, 2097, 2098, 2105, 2106, 2107, 2108, 2109, 2110, 2111, 2112, 2113, 2114, 2115, 2116, 2117, 2118, 2119, 2120, 2121, 2122, 2123, 2124, 2125, 2126, 2127, 2128, 2129, 2130, 2131, 2132, 2169, 2170, 2171, 2172, 2173, 2174, 2175, 2176, 2177, 2178, 2179, 2180, 2193, 2199, 2200, 2206, 2207, 2208, 2209, 2210, 2211, 2212, 2319, 2320, 2321, 2322, 2323, 2324, 2325, 2326, 2327, 2328, 2329, 2330, 2331, 2332, 2333, 2334, 2335, 2336, 2337, 2338, 2339, 2340, 2341, 2342, 2343, 2344, 2345, 2346, 2347, 2348, 2349, 2350, 2351, 2352, 2353, 2354, 2355, 2356, 2357, 2358, 2359, 2360, 2361, 2362, 2363, 2364, 2365, 2366, 2367, 2368, 2369, 2370, 2371, 2372, 2373, 2374, 2375, 2376, 2377, 2378, 2379, 2380, 2381, 2382, 2383, 2384, 2385, 2386, 2387, 2388, 2389, 2390, 2391, 2392, 2393, 2394, 2395, 2396, 2397, 2398, 2399, 2400, 2401, 2402, 2403, 2404, 2405, 2406, 2407, 2408, 2409, 2410, 2411, 2412, 2413, 2414, 2415, 2416, 2417, 2418, 2419, 2420, 2421, 2422, 2423, 2424, 2425, 2426, 2427, 2428, 2429, 2430, 2431, 2432, 2433, 2434, 2435, 2436, 2437, 2438, 2439, 2440, 2441, 2442, 2443, 2444, 2445, 2446, 2447, 2448, 2449, 2450, 2451, 2452, 2453, 2454, 2455, 2456, 2457, 2458, 2459, 2460, 2461, 2462, 2463, 2464, 2465, 2466, 2467, 2468, 2469, 2470, 2471, 2472, 2473, 2474, 2475, 2476, 2477, 2478, 2479, 2480, 2481, 2482, 2483, 2484, 2485, 2486, 2487, 2488, 2489, 2490, 2491, 2492, 2493, 2494, 2495, 2496, 2497, 2498, 2499, 2500, 2501, 2502, 2503, 2504, 2505, 2506, 2507, 2508, 2509, 2510, 2511, 2512, 2513, 2514, 2515, 2516, 2517, 2518, 2519, 2520, 2521, 2522, 2523, 2524, 2525, 2526, 2527, 2528, 2529, 2530, 2531, 2532, 2533, 2534, 2535, 2536, 2537, 2538, 2539, 2540, 2541, 2542, 2543, 2544, 2545, 2546, 2547, 2548, 2549, 2551, 2552, 2553, 2554, 2555, 2556, 2557, 2558, 2559, 2560, 2561, 2562, 2563, 2564, 2565, 2566, 2567, 2568, 2569, 2570, 2571, 2572, 2573, 2574, 2575, 2576, 2577, 2578, 2579, 2580, 2581, 2582, 2583, 2584, 2585, 2586, 2587, 2588, 2589, 2590, 2591, 2592, 2593, 2594, 2595, 2596, 2597, 2598, 2599, 2600, 2601, 2602, 2603, 2604, 2605, 2606, 2607, 2608, 2609, 2610, 2611, 2612, 2613, 2614, 2615, 2616, 2617, 2618, 2619, 2620, 2621, 2622, 2623, 2624, 2625, 2626, 2627, 2628, 2629, 2630, 2631, 2632, 2633, 2634, 2635, 2636, 2637, 2638, 2639, 2640, 2641, 2642, 2643, 2644, 2645, 2646, 2647, 2648, 2649, 2650, 2651, 2652, 2653, 2654, 2655, 2656, 2657, 2658, 2659, 2660, 2661, 2662, 2663, 2664, 2665, 2666, 2667, 2668, 2669, 2670, 2671, 2672, 2673, 2674, 2675, 2676, 2677, 2678, 2679, 2680, 2681, 2682, 2683, 2684, 2685, 2686, 2687, 2688, 2689, 2690, 2691, 2692, 2693, 2694, 2695, 2696, 2697, 2698, 2699, 2700, 2701, 2702, 2703, 2704, 2705, 2706, 2707, 2708, 2709, 2710, 2711, 2712, 2713, 2714, 2715, 2716, 2717, 2718, 2719, 2720, 2721, 2722, 2723, 2724, 2725, 2726, 2727, 2728, 2729, 2730, 2731, 2732, 2733, 2734, 2735, 2738, 2739, 2740, 2741, 2742, 2743, 2744, 2745, 2746, 2747, 2748, 2749, 2750, 2751, 2752, 2753, 2754, 2755, 2756, 2757, 2758, 2935, 2936, 2937, 2938, 2939, 2940, 2941, 2953, 2963, 3006, 3007, 3008, 3009, 3010, 3011, 3012, 3013, 3014, 3015, 3016, 3017, 3018, 3019, 3020, 3021, 3022, 3023, 3024, 3025, 3026, 3027, 3028, 3029, 3030, 3034, 3035, 3038, 3039, 3040, 3041, 3042, 3043, 3044, 3045, 3046, 3047, 3048, 3049, 3050, 3051, 3058, 3059, 3068, 3114, 3115, 3116, 3117, 3118, 3120, 3126, 3127, 3128, 3129, 3130, 3131, 3132, 3133, 3134, 3135, 3136, 3137, 3138, 3139, 3140, 3146, 3147, 3150, 3151, 3152, 3300, 3301, 3328, 3329, 3330, 3331, 3332, 3333, 3334, 3335, 3346, 3350, 3351, 3352, 3366, 3386, 3387, 3388, 3389, 3390, 3396, 3397, 3398, 3399, 3407, 3414, 3416, 3764, 3788, 3789, 3790, 3791, 3793, 3795, 3796, 3819, 3821, 3823, 3824, 3833, 3834, 3835, 3836, 3837, 3838, 3839, 3840, 3841, 3842, 3843, 3844, 3845, 3846, 3847, 3848, 3849, 3850, 3851, 3852, 3854, 3873, 3874, 3875, 3876, 3877, 3878, 3879, 3880, 3881, 3882, 3883, 3884, 3885, 3888, 3889, 3906, 3907, 3908, 3909, 3910, 3911, 4001, 4002, 4003, 4004, 4005, 4006, 4007, 4008, 4009, 4010, 4011, 4012, 4013, 4014, 4015, 4016, 4017, 4018, 4019, 4020, 4021, 4022, 4023, 4024, 4025, 4026, 4027, 4028, 4029, 4030, 4031, 4032, 4033, 4034, 4035, 4036, 4037, 4038, 4040, 4041, 4042, 4043, 4044, 4045, 4046, 4047, 4052, 4053, 4054, 4055, 4074, 4075, 4080, 4081, 4120, 4121, 4122, 4123, 4124, 4125, 4126, 4127, 4128, 4129, 4130, 4131, 4132, 4133, 4134, 4135, 4136, 4137, 4138, 4139, 4140, 4141, 4142, 4143, 4144, 4145, 4146, 4147, 4148, 4149, 4150, 4151, 4152, 4153, 4154, 4155, 4156, 4157, 4158, 4159, 4160, 4161, 4162, 4163, 4164, 4165, 4166, 4167, 4168, 4169, 4170, 4171, 4172, 4173, 4174, 4175, 4176, 4178, 4179, 4180, 4181, 4182, 4183, 4184, 4185, 4188, 4189, 4190, 4191, 4192, 4193, 4194, 4195, 4196, 4197, 4198, 4199, 4200, 4201, 4202, 4203, 4204, 4205, 4206, 4207, 4208, 4209, 4210, 4211, 4212, 4213, 4214, 4215, 4216, 4218, 4219, 4220, 4221, 4222, 4223, 4224, 4225, 4226, 4227, 4228, 4229, 4230, 4231, 4232, 4233, 4234, 4235, 4236, 4237, 4238, 4239, 4240, 4241, 4242, 4243, 4244, 4245, 4246, 4247, 4248, 4249, 4250, 4251, 4252, 4253, 4254, 4255, 4256, 4257, 4259, 4260, 4261, 4262, 4263, 4264, 4265, 4266, 4267, 4268, 4269, 4270, 4271, 4272, 4273, 4274, 4275, 4276, 4277, 4278, 4279, 4280, 4281, 4282, 4283, 4284, 4285, 4286, 4287, 4288, 4289, 4291, 4292, 4293, 4294, 4295, 4296, 4297, 4298, 4299, 4300, 4301, 4302, 4303, 4304, 4306, 4307, 4308, 4309, 4310, 4311, 4312, 4313, 4314, 4315, 4316, 4317, 4318, 4319, 4322, 4324, 4327, 4329, 4339, 4341, 4343, 4345, 4347, 4349, 4351, 4353, 4355, 4357, 4359, 4361, 4363, 4365, 4367, 4369, 4371, 4373, 4375, 4377, 4379, 4381, 4383, 4386, 4388, 4417, 4434, 4463, 4466, 4469, 4470, 4472, 4475, 4480, 4482, 4483, 4490, 4491, 4492, 4493, 4494, 4495, 4496, 4497, 4498, 4499, 4500, 4501, 4502, 4503, 4504, 4505, 4506, 4507, 4508, 4509, 4510, 4511, 4512, 4513, 4514, 4515, 4516, 4517, 4518, 4519, 4520, 4521, 4522, 4523, 4524, 4525, 4526, 4527, 4528, 4529, 4530, 4531, 4532, 4533, 4534, 4535, 4536, 4537, 4538, 4539, 4540, 4541, 4542, 4543, 4544, 4545, 4546, 4547, 4548, 4549, 4550, 4551, 4552, 4553, 4554, 4555, 4557, 4558, 4568, 4569, 4570, 4571, 4572, 4573, 4574, 4575, 4576, 4577, 4578, 4579, 4580, 4581, 4582, 4583, 4584, 4585, 4586, 4587, 4588, 4589, 4600, 4601, 4602, 4603, 4604, 4605, 4606, 4607, 4608, 4609, 4610, 4611, 4612, 4613, 4614, 4615, 4616, 4617, 4618, 4619, 4620, 4621, 4622, 4623, 4624, 4625, 4626, 4627, 4628, 4629, 4630, 4631, 4632, 4633, 4634, 4635, 4636, 4637, 4638, 4639, 4640, 4641, 4642, 4643, 4644, 4645, 4646, 4652, 4653, 4654, 4655, 4656, 4657, 4658, 4659, 4660, 4661, 4662, 4663, 4664, 4665, 4666, 4667, 4668, 4669, 4670, 4671, 4672, 4673, 4674, 4675, 4676, 4677, 4678, 4679, 4680, 4681, 4682, 4683, 4684, 4685, 4686, 4687, 4688, 4689, 4690, 4691, 4692, 4693, 4694, 4695, 4696, 4697, 4698, 4699, 4700, 4701, 4702, 4703, 4704, 4705, 4706, 4707, 4708, 4709, 4710, 4711, 4712, 4713, 4714, 4715, 4716, 4717, 4718, 4719, 4720, 4721, 4722, 4723, 4724, 4725, 4726, 4727, 4728, 4729, 4730, 4731, 4732, 4733, 4734, 4735, 4736, 4737, 4738, 4739, 4740, 4741, 4742, 4743, 4744, 4745, 4746, 4747, 4748, 4749, 4750, 4751, 4752, 4753, 4754, 4755, 4756, 4757, 4758, 4759, 4760, 4761, 4762, 4763, 4764, 4765, 4766, 4767, 4768, 4769, 4770, 4771, 4772, 4773, 4774, 4775, 4776, 4777, 4778, 4779, 4780, 4781, 4782, 4783, 4784, 4785, 4786, 4787, 4788, 4789, 4790, 4791, 4792, 4793, 4794, 4795, 4796, 4797, 4798, 4799, 4800, 4801, 4802, 4803, 4804, 4805, 4806, 4807, 4808, 4809, 4810, 4811, 4812, 4813, 4814, 4815, 4816, 4817, 4818, 4819, 4820, 4821, 4822, 4823, 4824, 4839, 4855, 4856, 4857, 4858, 4859, 4860, 4861, 4862, 4863, 4864, 4865, 4866, 4867, 4868, 4869, 4870, 4871, 4872, 4873, 4874, 4875, 4876, 4877, 4878, 4879, 4880, 4883, 4885, 4887, 4889, 4891, 4893, 4895, 4898, 4900, 4901, 4902, 4903, 4904, 4907, 4909, 4921, 4923, 4925, 4927, 4929, 4931, 4933, 4935, 4937, 4939, 4941, 4943, 4945, 4947, 4949, 4951, 4953, 4955, 4957, 4959, 4961, 4963, 4965, 4967, 4969, 4971, 4973, 4975, 4977, 4979, 4981, 4983, 4985, 4987, 4989, 4991, 4993, 4995, 4997, 4999, 5012, 5013, 5017, 5048, 5105, 5106, 5107, 5108, 5109, 5110, 5111, 5112, 5113, 5114, 5115, 5116, 5117, 5118, 5119, 5120, 5121, 5122, 5123, 5124, 5125, 5126, 5127, 5128, 5129, 5130, 5132, 5167, 5168, 5169, 5170, 5171, 5172, 5173, 5174, 5175, 5176, 5177, 5178, 5179, 5180, 5181, 5182, 5183, 5184, 5185, 5186, 5187, 5188, 5224, 5228, 5229, 5233, 5245, 5246, 5251, 5252, 5253, 5254, 5255, 5256, 5257, 5258, 5259, 5263, 5264, 5269, 5270, 5271, 5272, 5273, 5274, 5275, 5801, 5802, 5803, 5804, 5808, 5809, 5810, 5811, 5812, 5813, 5814, 5815, 5816, 20004, 20005, 20006, 20007, 20008, 20009, 20010, 20011, 20012, 20013, 20014, 20015, 20016, 20017, 20018, 20019, 20020, 20021, 20022, 20023, 20024, 20025, 20026, 20027, 20028, 20029, 20030, 20031, 20032, 20064, 20065, 20066, 20067, 20068, 20069, 20070, 20071, 20072, 20073, 20074, 20075, 20076, 20077, 20078, 20079, 20080, 20081, 20082, 20083, 20084, 20085, 20086, 20087, 20088, 20089, 20090, 20091, 20092, 21413, 21414, 21415, 21416, 21417, 21418, 21419, 21420, 21421, 21422, 21423, 21453, 21454, 21455, 21456, 21457, 21458, 21459, 21460, 21461, 21462, 21463, 21473, 21474, 21475, 21476, 21477, 21478, 21479, 21480, 21481, 21482, 21483, 21896, 21897, 21898, 21899, 22171, 22172, 22173, 22174, 22175, 22176, 22177, 22181, 22182, 22183, 22184, 22185, 22186, 22187, 22191, 22192, 22193, 22194, 22195, 22196, 22197, 25884, 27205, 27206, 27207, 27208, 27209, 27210, 27211, 27212, 27213, 27214, 27215, 27216, 27217, 27218, 27219, 27220, 27221, 27222, 27223, 27224, 27225, 27226, 27227, 27228, 27229, 27230, 27231, 27232, 27391, 27392, 27393, 27394, 27395, 27396, 27397, 27398, 27492, 28402, 28403, 28404, 28405, 28406, 28407, 28408, 28409, 28410, 28411, 28412, 28413, 28414, 28415, 28416, 28417, 28418, 28419, 28420, 28421, 28422, 28423, 28424, 28425, 28426, 28427, 28428, 28429, 28430, 28431, 28432, 28462, 28463, 28464, 28465, 28466, 28467, 28468, 28469, 28470, 28471, 28472, 28473, 28474, 28475, 28476, 28477, 28478, 28479, 28480, 28481, 28482, 28483, 28484, 28485, 28486, 28487, 28488, 28489, 28490, 28491, 28492, 29701, 29702, 30161, 30162, 30163, 30164, 30165, 30166, 30167, 30168, 30169, 30170, 30171, 30172, 30173, 30174, 30175, 30176, 30177, 30178, 30179, 30800, 31251, 31252, 31253, 31254, 31255, 31256, 31257, 31258, 31259, 31275, 31276, 31277, 31278, 31279, 31281, 31282, 31283, 31284, 31285, 31286, 31287, 31288, 31289, 31290, 31700, } RE_CRS_CODE = re.compile( r'(EPSG:|' r'http://www\.opengis\.net/def/crs/epsg/0/|' r'http://www\.opengis\.net/gml/srs/epsg\.xml\#|' r'urn:EPSG:geographicCRS:|' r'urn:ogc:def:crs:EPSG::|' r'urn:ogc:def:crs:OGC::|' r'urn:ogc:def:crs:EPSG:)([0-9]+|CRS84)', re.IGNORECASE, ) def get_crs_code(crs: str) -> Union[int, str]: """ Extract the CRS code from the given CRS identifier string, which can be one of: * ``EPSG:`` * ``http://www.opengis.net/def/crs/EPSG/0/`` * ``http://www.opengis.net/gml/srs/epsg.xml#`` * ``urn:EPSG:geographicCRS:`` * ``urn:ogc:def:crs:EPSG::`` * ``urn:ogc:def:crs:OGC::`` * ``urn:ogc:def:crs:EPSG:`` Returns the code as an integer in case of EPSG code or as the string ``'CRS84'``. >>> get_crs_code('EPSG:4326') 4326 >>> get_crs_code('urn:ogc:def:crs:OGC::CRS84') 'CRS84' >>> get_crs_code('something') Traceback (most recent call last): ... ValueError: Failed to retrieve CRS code """ match = RE_CRS_CODE.match(crs) if not match: raise ValueError('Failed to retrieve CRS code') value_group = match.groups()[1] try: return int(value_group) except ValueError: return value_group def is_crs_yx(crs: str) -> bool: """ Determines whether the given CRS uses Y/X (or latitude/longitude) axis order. >>> is_crs_yx('EPSG:4326') True >>> is_crs_yx('EPSG:3857') False >>> is_crs_yx('urn:ogc:def:crs:OGC::CRS84') False """ code = get_crs_code(crs) return code in AXISORDER_YX geopython-pygml-5ce8eb0/pygml/basics.py000066400000000000000000000120401416035544100203170ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # # Project: pygml # Authors: Fabian Schindler # # ------------------------------------------------------------------------------ # Copyright (C) 2021 EOX IT Services GmbH # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies of this Software or works derived from this Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # ------------------------------------------------------------------------------ from typing import Callable from .types import Coordinates, Coordinate def _make_number_parser(decimal: str) -> Callable[[str], float]: """ Helper to create a number parser with a potentially custom decimal separator. When this is not the '.' character, each number will replace the given decimal separator with '.' before calling the built-in `float` function. """ if decimal == '.': return float def inner(value: str) -> float: return float(value.replace(decimal, '.')) return inner def parse_coordinates(value: str, cs: str = ',', ts: str = ' ', decimal: str = '.') -> Coordinates: """ Parses the the values of a gml:coordinates node to a list of lists of floats. Takes the coordinate separator and tuple separator into account, and also custom decimal separators. >>> parse_coordinates('12.34 56.7,89.10 11.12') [(12.34, 56.7), (89.1, 11.12)] >>> parse_coordinates('12.34 56.7;89.10 11.12', cs=';') [(12.34, 56.7), (89.1, 11.12)] >>> parse_coordinates('12.34:56.7,89.10:11.12', ts=':') [(12.34, 56.7), (89.1, 11.12)] >>> parse_coordinates('12.34:56.7;89.10:11.12', cs=';', ts=':') [(12.34, 56.7), (89.1, 11.12)] >>> parse_coordinates( ... '12,34:56,7;89,10:11,12', cs=';', ts=':', decimal=',' ... ) [(12.34, 56.7), (89.1, 11.12)] """ number_parser = _make_number_parser(decimal) return [ tuple( number_parser(number) for number in coordinate.strip().split(ts) ) for coordinate in value.strip().split(cs) ] def parse_poslist(value: str, dimensions: int = 2) -> Coordinates: """ Parses the value of a single gml:posList to a `Coordinates` structure. >>> parse_poslist('12.34 56.7 89.10 11.12') [(12.34, 56.7), (89.1, 11.12)] >>> parse_poslist('12.34 56.7 89.10 11.12 13.14 15.16', dimensions=3) [(12.34, 56.7, 89.1), (11.12, 13.14, 15.16)] >>> parse_poslist('12.34 56.7 89.10 11.12', dimensions=3) Traceback (most recent call last): ... ValueError: Invalid dimensionality of pos list """ raw = [float(v) for v in value.split()] if len(raw) % dimensions > 0: raise ValueError('Invalid dimensionality of pos list') return [ tuple(raw[i:i + dimensions]) for i in range(0, len(raw), dimensions) ] def parse_pos(value: str) -> Coordinate: """ Parses a single gml:pos to a `Coordinate` structure. >>> parse_pos('12.34 56.7') (12.34, 56.7) >>> parse_pos('12.34 56.7 89.10') (12.34, 56.7, 89.1) """ return tuple(float(v) for v in value.split()) def swap_coordinate_xy(coordinate: Coordinate) -> Coordinate: """ Swaps the X and Y coordinates of a given coordinate >>> swap_coordinate_xy((12.34, 56.7)) (56.7, 12.34) >>> swap_coordinate_xy((12.34, 56.7, 89.10)) (56.7, 12.34, 89.1) """ return (coordinate[1], coordinate[0], *coordinate[2:]) def swap_coordinates_xy(coordinates: Coordinates) -> Coordinates: """ Swaps the X and Y coordinates of a given coordinates list >>> swap_coordinates_xy( ... [(12.34, 56.7), (89.10, 11.12)] ... ) [(56.7, 12.34), (11.12, 89.1)] >>> swap_coordinates_xy( ... [(12.34, 56.7, 89.10), (11.12, 13.14, 15.16)] ... ) [(56.7, 12.34, 89.1), (13.14, 11.12, 15.16)] """ return [ (coordinate[1], coordinate[0], *coordinate[2:]) for coordinate in coordinates ] geopython-pygml-5ce8eb0/pygml/dimensionality.py000066400000000000000000000062771416035544100221220ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # # Project: pygml # Authors: Fabian Schindler # # ------------------------------------------------------------------------------ # Copyright (C) 2021 EOX IT Services GmbH # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies of this Software or works derived from this Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # ------------------------------------------------------------------------------ from typing import Optional from collections.abc import Sequence from .types import GeomDict def get_dimensionality(geometry: GeomDict) -> Optional[int]: """ Returns the dimensionality of a given GeoJSON geometry. This is obtained by descending into the first coordinate and using its length. When no coordinates can be retrieved (e.g: in case of GeometryCollections) None is returned. >>> get_dimensionality({ ... 'type': 'Polygon', ... 'coordinates': [ ... [ ... (0.5, 1.0), ... (0.5, 2.0), ... (1.5, 2.0), ... (1.5, 1.0), ... (0.5, 1.0) ... ] ... ] ... }) 2 >>> get_dimensionality({ ... 'type': 'MultiPoint', ... 'coordinates': [ ... (1.0, 1.0, 1.0), ... (2.0, 2.0, 1.0), ... ] ... }) 3 >>> get_dimensionality({ ... 'type': 'GeometryCollection', ... 'geometries': [ ... { ... 'type': 'Point', ... 'coordinates': (1.0, 1.0) ... }, ... { ... 'type': 'Polygon', ... 'coordinates': [ ... [(1.0, 1.0)], ... [(1.0, 1.0)], ... ] ... }, ... ] ... }) """ coordinates = geometry.get('coordinates') if coordinates: # drill down into nested coordinates while isinstance(coordinates[0], Sequence): coordinates = coordinates[0] return len(coordinates) return None geopython-pygml-5ce8eb0/pygml/georss.py000066400000000000000000000154721416035544100203710ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # # Project: pygml # Authors: Fabian Schindler # # ------------------------------------------------------------------------------ # Copyright (C) 2021 EOX IT Services GmbH # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies of this Software or works derived from this Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # ------------------------------------------------------------------------------ from pygml.axisorder import get_crs_code from typing import Callable, List from lxml import etree from lxml.builder import ElementMaker from .basics import ( parse_pos, parse_poslist, swap_coordinate_xy, swap_coordinates_xy ) from .dimensionality import get_dimensionality from .types import GeomDict from .pre_v32 import ( NAMESPACE as NAMESPACE_PRE32, parse_pre_v32, encode_pre_v32 ) from .v32 import NAMESPACE as NAMESPACE_32, parse_v32 from .v33 import NAMESPACE as NAMESPACE_33_CE, parse_v33_ce NAMESPACE = 'http://www.georss.org/georss' NSMAP = {'georss': NAMESPACE} Element = etree._Element Elements = List[Element] def parse_georss(element: Element) -> GeomDict: """ Parses the GeoRSS basic elements to their respective GeoJSON representation. As all coordinates in GeoRSS are expressed in WGS84 and in Latitude/Longitude order, the coordinates are swapped to XY order. In case of georss:where, it is expected that it contains a single GML element which is parsed as either GML 3.1.1, GML 3.2 or GML 3.3 CE. """ qname = etree.QName(element.tag) if qname.namespace != NAMESPACE: raise ValueError(f'Unsupported namespace {qname.namespace}') bbox = None localname = qname.localname if localname == 'point': type_ = 'Point' coordinates = swap_coordinate_xy(parse_pos(element.text)) elif localname == 'line': type_ = 'LineString' coordinates = swap_coordinates_xy(parse_poslist(element.text)) elif localname == 'box': # boxes are expanded to Polygons, but store the 'bbox' value type_ = 'Polygon' low, high = swap_coordinates_xy(parse_poslist(element.text)) lx, ly = low hx, hy = high coordinates = [ [ (lx, ly), (lx, hy), (hx, hy), (hx, ly), (lx, ly), ] ] bbox = (lx, ly, hx, hy) elif localname == 'polygon': type_ = 'Polygon' coordinates = [swap_coordinates_xy(parse_poslist(element.text))] elif localname == 'where': # special handling here: defer to the gml definition. Although, # only GML 3.1.1 is officially supported, we also allow GML 3.2 and 3.3 if not len(element) == 1: raise ValueError( 'Invalid number of child elements in georss:where' ) child = element[0] child_namespace = etree.QName(child.tag).namespace if child_namespace == NAMESPACE_PRE32: return parse_pre_v32(child) elif child_namespace == NAMESPACE_32: return parse_v32(child) elif child_namespace == NAMESPACE_33_CE: return parse_v33_ce(child) else: raise ValueError( f'Unsupported child element in georss:where: {child.tag}' ) else: raise ValueError(f'Unsupported georss element: {localname}') result = { 'type': type_, 'coordinates': coordinates, } if bbox: result['bbox'] = bbox return result GEORSS = ElementMaker(namespace=NAMESPACE, nsmap=NSMAP) GmlEncoder = Callable[[GeomDict, str], Element] def encode_georss(geometry: GeomDict, gml_encoder: GmlEncoder = encode_pre_v32) -> Element: """ Encodes a GeoJSON geometry as a GeoRSS ``lxml.etree.Element``. Tries to use the native GeoRSS elements ``point``, ``line``, or ``polygon`` when possible. Falls back to ``georss:where`` with using the ``gml_encoder`` function (defaulting to GML 3.2): - MultiPoint, MultiLineString, MultiPolygon geometries - Polygons with interiors - GeometryCollections - any geometry with CRS other than CRS84 or EPSG:4326 - when dealing with >2D geometries """ type_ = geometry['type'] coordinates = geometry.get('coordinates') crs = geometry.get('crs') dims = get_dimensionality(geometry) code = None if crs: crs_name = crs.get('properties', {}).get('name') code = get_crs_code(crs_name) if code in (None, 4326, 'CRS84') and dims == 2: if type_ == 'Point': return GEORSS( 'point', ' '.join( str(v) for v in swap_coordinate_xy(coordinates) ) ) elif type_ == 'LineString': return GEORSS( 'line', ' '.join( ' '.join( str(v) for v in coordinate ) for coordinate in swap_coordinates_xy(coordinates) ) ) elif type_ == 'Polygon': # only exterior if len(coordinates) == 1: return GEORSS( 'polygon', ' '.join( ' '.join( str(v) for v in coordinate ) for coordinate in swap_coordinates_xy(coordinates[0]) ) ) # fall back to GML encoding when we have: # - MultiPoint, MultiLineString, MultiPolygon geometries # - Polygons with interiors # - GeometryCollections # - any geometry with CRS other than CRS84 or EPSG4326 # - when dealing with >2D geometries return GEORSS( 'where', gml_encoder(geometry, 'ID') ) geopython-pygml-5ce8eb0/pygml/parse.py000066400000000000000000000045001416035544100201670ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # # Project: pygml # Authors: Fabian Schindler # # ------------------------------------------------------------------------------ # Copyright (C) 2021 EOX IT Services GmbH # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies of this Software or works derived from this Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # ------------------------------------------------------------------------------ from typing import Union from lxml import etree from .georss import NAMESPACE as NAMESPACE_GEORSS, parse_georss from .pre_v32 import NAMESPACE as NAMESPACE_PRE_v32, parse_pre_v32 from .v32 import NAMESPACE as NAMESPACE_32, parse_v32 from .v33 import NAMESPACE as NAMESPACE_33_CE, parse_v33_ce from .types import Geometry def parse(source: Union[etree._Element, str]) -> Geometry: """ """ if etree.iselement(source): element = source else: element = etree.fromstring(source) namespace = etree.QName(element.tag).namespace if namespace == NAMESPACE_PRE_v32: result = parse_pre_v32(element) elif namespace == NAMESPACE_32: result = parse_v32(element) elif namespace == NAMESPACE_33_CE: result = parse_v33_ce(element) elif namespace == NAMESPACE_GEORSS: result = parse_georss(element) return Geometry(result) geopython-pygml-5ce8eb0/pygml/pre_v32.py000066400000000000000000000114331416035544100203400ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # # Project: pygml # Authors: Fabian Schindler # # ------------------------------------------------------------------------------ # Copyright (C) 2021 EOX IT Services GmbH # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies of this Software or works derived from this Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # ------------------------------------------------------------------------------ from typing import List from lxml import etree from .types import GeomDict from .v3_common import ( GML3Encoder, GML3Parser, parse_envelope, parse_multi_linestring, parse_multi_polygon, parse_point, parse_linestring_or_linear_ring, parse_multi_curve, parse_polygon, parse_multi_point, parse_multi_surface, parse_multi_geometry, ) NAMESPACE = 'http://www.opengis.net/gml' NSMAP = {'gml': NAMESPACE} Element = etree._Element Elements = List[Element] # set up a parser GML_PRE32_PARSER = GML3Parser(NAMESPACE, NSMAP, { 'Point': parse_point, 'MultiPoint': parse_multi_point, 'LineString': parse_linestring_or_linear_ring, 'MultiLineString': parse_multi_linestring, 'MultiCurve': parse_multi_curve, 'Polygon': parse_polygon, 'Envelope': parse_envelope, 'MultiPolygon': parse_multi_polygon, 'MultiSurface': parse_multi_surface, 'MultiGeometry': parse_multi_geometry, }) def parse_pre_v32(element: Element) -> GeomDict: """ Main parsing function for GML 3.0 and 3.1 XML structures. The following XML tags can be parsed to their respective GeoJSON counterpart: - gml:Point -> Point - gml:MultiPoint -> MultiPoint - gml:LineString -> LineString - gml:MultiCurve (with only gml:LineString curve members) -> MultiLineString - gml:MultiLineString -> MultiLineString - gml:Polygon -> Polygon - gml:MultiPolygon -> MultiPolygon - gml:MultiSurface (with only gml:Polygon surface members) -> MultiPolygon - gml:MultiGeometry (with any of the aforementioned types as geometry members) -> GeometryCollection The SRS of the geometry is determined and the coordinates are flipped to XY order in GeoJSON when they are in YX order in GML. Returns: the parsed GeoJSON geometry as a dict. Contains a 'type' field, a 'coordinates' field and potentially a 'crs' field when the geometries SRS could be determined. This field follows the structure laid out in the `draft for GeoJSON `_. """ return GML_PRE32_PARSER.parse(element) GML_PRE32_ENCODER = GML3Encoder(NAMESPACE, NSMAP, True) def encode_pre_v32(geometry: GeomDict, identifier: str = None) -> Element: """ Encodes the given GeoJSON dict to its most simple GML 3 representation. In preparation of the encoding, the coordinates may have to be swapped from XY order to YX order, depending on the used CRS. This includes the case when no CRS is specified, as this means the default WGS84 in GeoJSON, which in turn uses latitude/longitude ordering GML. This function returns an ``lxml.etree._Element`` which can be altered or serialized. >>> from pygml.pre_v32 import encode_pre_v32 >>> from lxml import etree >>> tree = encode_pre_v32({ ... 'type': 'Point', ... 'coordinates': (1.0, 1.0) ... }, 'ID') >>> print(etree.tostring(tree, pretty_print=True).decode()) 1.0 1.0 """ return GML_PRE32_ENCODER.encode(geometry, identifier) geopython-pygml-5ce8eb0/pygml/types.py000066400000000000000000000041071416035544100202240ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # # Project: pygml # Authors: Fabian Schindler # # ------------------------------------------------------------------------------ # Copyright (C) 2021 EOX IT Services GmbH # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies of this Software or works derived from this Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # ------------------------------------------------------------------------------ from dataclasses import dataclass from typing import List, Optional, Tuple, Union try: from typing import TypedDict class GeomDict(TypedDict, total=True): type: str coordinates: Union[Tuple, List] crs: Optional[dict] except ImportError: GeomDict = dict # Definition of a coordinate list Coordinate = Tuple[float, ...] Coordinates = List[Coordinate] @dataclass(frozen=True) class Geometry: """ Simple container class to hold a geometry and expose it via the ``__geo_interface__`` property """ geometry: GeomDict @property def __geo_interface__(self): return self.geometry geopython-pygml-5ce8eb0/pygml/v32.py000066400000000000000000000111541416035544100174720ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # # Project: pygml # Authors: Fabian Schindler # # ------------------------------------------------------------------------------ # Copyright (C) 2021 EOX IT Services GmbH # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies of this Software or works derived from this Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # ------------------------------------------------------------------------------ from typing import List from lxml import etree from .types import GeomDict from .v3_common import ( GML3Encoder, GML3Parser, parse_envelope, parse_point, parse_multi_point, parse_linestring_or_linear_ring, parse_multi_curve, parse_polygon, parse_multi_surface, parse_multi_geometry ) NAMESPACE = 'http://www.opengis.net/gml/3.2' NSMAP = {'gml': NAMESPACE} Element = etree._Element Elements = List[Element] # set up a parser GML32_PARSER = GML3Parser([NAMESPACE], NSMAP, { 'Point': parse_point, 'MultiPoint': parse_multi_point, 'LineString': parse_linestring_or_linear_ring, 'MultiCurve': parse_multi_curve, 'Polygon': parse_polygon, 'Envelope': parse_envelope, 'MultiSurface': parse_multi_surface, 'MultiGeometry': parse_multi_geometry, }) def parse_v32(element: Element) -> GeomDict: """ Main parsing function for GML 3.2 XML structures. The following XML tags can be parsed to their respective GeoJSON counterpart: - gml:Point -> Point - gml:MultiPoint -> MultiPoint - gml:LineString -> LineString - gml:MultiCurve (with only gml:LineString curve members) -> MultiLineString - gml:Polygon -> Polygon - gml:MultiSurface (with only gml:Polygon surface members) -> MultiPolygon - gml:MultiGeometry (with any of the aforementioned types as geometry members) -> GeometryCollection The SRS of the geometry is determined and the coordinates are flipped to XY order in GeoJSON when they are in YX order in GML. Returns: the parsed GeoJSON geometry as a dict. Contains a 'type' field, a 'coordinates' field and potentially a 'crs' field when the geometries SRS could be determined. This field follows the structure laid out in the `draft for GeoJSON `_. """ return GML32_PARSER.parse(element) GML32_ENCODER = GML3Encoder(NAMESPACE, NSMAP, True) def encode_v32(geometry: GeomDict, identifier: str) -> Element: """ Encodes the given GeoJSON dict to its most simple GML 3.2 representation. As in GML 3.2 the gml:id attribute is mandatory, the identifier must be passed as well. In preparation of the encoding, the coordinates may have to be swapped from XY order to YX order, depending on the used CRS. This includes the case when no CRS is specified, as this means the default WGS84 in GeoJSON, which in turn uses latitude/longitude ordering GML. This function returns an ``lxml.etree._Element`` which can be altered or serialized. >>> from pygml.v32 import encode_v32 >>> from lxml import etree >>> tree = encode_v32({ ... 'type': 'Point', ... 'coordinates': (1.0, 1.0) ... }, 'ID') >>> print(etree.tostring(tree, pretty_print=True).decode()) 1.0 1.0 """ return GML32_ENCODER.encode(geometry, identifier) geopython-pygml-5ce8eb0/pygml/v33.py000066400000000000000000000173511416035544100175000ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # # Project: pygml # Authors: Fabian Schindler # # ------------------------------------------------------------------------------ # Copyright (C) 2021 EOX IT Services GmbH # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies of this Software or works derived from this Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # ------------------------------------------------------------------------------ from lxml import etree from lxml.builder import ElementMaker from .types import Coordinates, GeomDict from .v3_common import ( GML3Encoder, GML3Parser, determine_srs, parse_envelope, parse_point, parse_multi_point, parse_linestring_or_linear_ring, parse_multi_curve, parse_polygon, parse_multi_surface, parse_multi_geometry, NameSpaceMap, Element, ParseResult ) from .v32 import NAMESPACE as NAMESPACE_32, GML32_ENCODER NAMESPACE = 'http://www.opengis.net/gml/3.3/ce' NSMAP: NameSpaceMap = { 'gmlce': NAMESPACE, 'gml': NAMESPACE_32 } def parse_simple_triangle_or_rectangle(element: Element, nsmap: NameSpaceMap) -> ParseResult: exterior, srs = parse_linestring_or_linear_ring( element, nsmap ) exterior = exterior['coordinates'] exterior.append(exterior[0]) return { 'type': 'Polygon', 'coordinates': [exterior] }, srs def parse_simple_polygon(element: Element, nsmap: NameSpaceMap) -> ParseResult: exterior, srs = parse_linestring_or_linear_ring( element, nsmap ) exterior = exterior['coordinates'] exterior.append(exterior[0]) return { 'type': 'Polygon', 'coordinates': [exterior] }, srs def parse_simple_multi_point(element: Element, nsmap: NameSpaceMap) -> ParseResult: sub_elements = element.xpath( 'gml:MultiPoint|gmlce:SimpleMultiPoint', namespaces=nsmap ) multi_points, srss = zip(*( parse_multi_point(sub_elem) if etree.QName().localname == 'MultiPoint' else parse_simple_multi_point(sub_elem) for sub_elem in sub_elements )) srs = determine_srs(*srss) # merge the possibly nested multi points into a single list of coordinates coordinates = [ coord for multi_point in multi_points for coord in multi_point['coordinates'] ] return { 'type': 'MultiPoint', 'coordinates': coordinates }, srs GML33_CE_PARSER = GML3Parser([NAMESPACE, NAMESPACE_32], NSMAP, { 'Point': parse_point, 'MultiPoint': parse_multi_point, 'LineString': parse_linestring_or_linear_ring, 'MultiCurve': parse_multi_curve, 'Polygon': parse_polygon, 'Envelope': parse_envelope, 'MultiSurface': parse_multi_surface, 'MultiGeometry': parse_multi_geometry, 'SimpleTriangle': parse_simple_triangle_or_rectangle, 'SimpleRectangle': parse_simple_triangle_or_rectangle, 'SimplePolygon': parse_simple_polygon, 'SimpleMultiPoint': parse_simple_multi_point, }) def parse_v33_ce(element: Element) -> GeomDict: """ Main parsing function for GML 3.3 CE XML structures. The following XML tags can be parsed to their respective GeoJSON counterpart: - gmlce:SimpleTriangle -> Polygon - gmlce:SimpleRectangel -> Polygon - gmlce:SimplePolygon -> Polygon - TODO: gmlce:SimpleMultiPoint -> MultiPoint - gml:Point -> Point - gml:MultiPoint -> MultiPoint - gml:LineString -> LineString - gml:MultiCurve (with only gml:LineString curve members) -> MultiLineString - gml:Polygon -> Polygon - gml:MultiSurface (with only gml:Polygon surface members) -> MultiPolygon - gml:MultiGeometry (with any of the aforementioned types as geometry members) -> GeometryCollection The SRS of the geometry is determined and the coordinates are flipped to XY order in GeoJSON when they are in YX order in GML. Returns: the parsed GeoJSON geometry as a dict. Contains a 'type' field, a 'coordinates' field and potentially a 'crs' field when the geometries SRS could be determined. This field follows the structure laid out in the `draft for GeoJSON `_. """ return GML33_CE_PARSER.parse(element) class GML33CEEncoder(GML3Encoder): def __init__(self): super().__init__(NAMESPACE_32, NSMAP, True) self.gmlce = ElementMaker(namespace=NAMESPACE, nsmap=NSMAP) def encode_polygon(self, coordinates: Coordinates, attrs: dict) -> Element: if len(coordinates) == 1: exterior = coordinates[0] tag_name = None if len(exterior) == 4: tag_name = 'SimpleTriangle' elif len(exterior) == 5: tag_name = 'SimpleRectangle' else: tag_name = 'SimplePolygon' return self.gmlce( tag_name, GML32_ENCODER._encode_pos_list(exterior[:-1]), **attrs ) return super().encode_polygon(coordinates, attrs) GML33CE_ENCODER = GML33CEEncoder() def encode_v33_ce(geometry: GeomDict, identifier: str) -> Element: """ Encodes the given GeoJSON dict to its most simple GML 3.3 CE representation, with a fallback to encoding it GML 3.2 when the compact encoding is not possible. As in GML 3.2 the gml:id attribute is mandatory, the identifier must be passed as well. In preparation of the encoding, the coordinates may have to be swapped from XY order to YX order, depending on the used CRS. This includes the case when no CRS is specified, as this means the default WGS84 in GeoJSON, which in turn uses latitude/longitude ordering GML. This function returns an ``lxml.etree._Element`` which can be altered or serialized. >>> from pygml.v33 import encode_v33_ce >>> from lxml import etree >>> tree = encode_v33_ce({ ... 'type': 'Polygon', ... 'coordinates': [ ... [ ... (1.0, 2.0), ... (1.0, 3.0), ... (2.0, 2.0), ... (1.0, 2.0), ... ], ... ], ... }, 'ID') >>> print(etree.tostring(tree, pretty_print=True).decode()) 1.0 2.0 1.0 3.0 2.0 2.0 """ return GML33CE_ENCODER.encode(geometry, identifier) geopython-pygml-5ce8eb0/pygml/v3_common.py000066400000000000000000000503101416035544100207550ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # # Project: pygml # Authors: Fabian Schindler # # ------------------------------------------------------------------------------ # Copyright (C) 2021 EOX IT Services GmbH # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies of this Software or works derived from this Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # ------------------------------------------------------------------------------ from typing import Callable, List, Optional, Tuple, Dict from lxml import etree from lxml.builder import ElementMaker from .axisorder import is_crs_yx from .basics import ( parse_coordinates, parse_pos, parse_poslist, swap_coordinates_xy ) from .types import Coordinate, Coordinates, GeomDict # type aliases NameSpaceMap = Dict[str, str] Element = etree._Element Elements = List[Element] ParseResult = Tuple[GeomDict, str] HandlerFunc = Callable[[Element, NameSpaceMap], ParseResult] class GML3Parser: def __init__(self, namespaces: List[str], nsmap: NameSpaceMap, handlers: Dict[str, HandlerFunc]): self.namespaces = namespaces self.nsmap = nsmap self.handlers = handlers def parse(self, element: Element) -> GeomDict: qname = etree.QName(element.tag) if qname.namespace not in self.namespaces: raise ValueError(f'Namespace {qname.namespace} is not supported') # get a registered handler function handler = self.handlers.get(qname.localname) if not handler: raise ValueError( f'XML nodes of type {qname.localname} are not supported.' ) # parse the geometry if qname.localname == 'MultiGeometry': geometry, srs = handler(element, self.nsmap, self.parse) else: geometry, srs = handler(element, self.nsmap) # handle SRS: maybe swap YX ordered coordinates to XY # and store the SRS as a crs field in the geometry if srs: geometry = maybe_swap_coordinates(geometry, srs) geometry['crs'] = { 'type': 'name', 'properties': { 'name': srs } } return geometry def maybe_swap_coordinates(geometry: GeomDict, srs: str) -> GeomDict: if is_crs_yx(srs): type_ = geometry['type'] coordinates = geometry['coordinates'] if type_ == 'Point': coordinates = (coordinates[1], coordinates[0], *coordinates[2:]) elif type_ in ('MultiPoint', 'LineString'): coordinates = swap_coordinates_xy(coordinates) elif type_ in ('MultiLineString', 'Polygon'): coordinates = [ swap_coordinates_xy(line) for line in coordinates ] elif type_ == 'MultiPolygon': coordinates = [ [ swap_coordinates_xy(line) for line in polygon ] for polygon in coordinates ] geometry['coordinates'] = coordinates return geometry else: return geometry def determine_srs(*srss: List[Optional[str]]) -> Optional[str]: srss = set(srss) if None in srss: srss.remove(None) if len(srss) > 1: raise ValueError(f'Conflicting SRS definitions: {", ".join(srss)}') try: return srss.pop() except KeyError: return None def parse_coord(element: Element, nsmap: NameSpaceMap) -> Coordinate: x = float(element.xpath('gml:X/text()')[0]) y = element.xpath('gml:X/text()') z = element.xpath('gml:X/text()') if y and z: return (x, float(y[0]), float(z[0])) elif y: return (x, float(y[0])) return (x,) def parse_point(element: Element, nsmap: NameSpaceMap) -> ParseResult: positions = element.xpath('gml:pos', namespaces=nsmap) coordinates = element.xpath('gml:coordinates', namespaces=nsmap) coord_elemss = element.xpath('gml:coord', namespaces=nsmap) srs = None if positions: if len(positions) > 1: raise ValueError('Too many gml:pos elements') coords = parse_pos(positions[0].text) srs = positions[0].attrib.get('srsName') elif coordinates: if len(coordinates) > 1: raise ValueError('Too many gml:coordinates elements') coordinates0 = coordinates[0] coords = parse_coordinates( coordinates0.text, cs=coordinates0.attrib.get('cs', ','), ts=coordinates0.attrib.get('ts', ' '), decimal=coordinates0.attrib.get('decimal', '.'), )[0] elif coord_elemss: if len(coord_elemss) > 1: raise ValueError('Too many gml:coord elements') coords = parse_coord(coord_elemss[0]) else: raise ValueError( 'Neither gml:pos nor gml:coordinates found' ) srs = determine_srs(element.attrib.get('srsName'), srs) return { 'type': 'Point', 'coordinates': coords }, srs def parse_multi_point(element: Element, nsmap: NameSpaceMap) -> ParseResult: points, srss = zip(*( parse_point(point_elem, nsmap) for point_elem in element.xpath( '(gml:pointMember|gml:pointMembers)/*', namespaces=nsmap ) )) srs = determine_srs(element.attrib.get('srsName'), *srss) return { 'type': 'MultiPoint', 'coordinates': [ point['coordinates'] for point in points ] }, srs def parse_linestring_or_linear_ring(element: Element, nsmap: NameSpaceMap) -> ParseResult: pos_lists = element.xpath('gml:posList', namespaces=nsmap) poss = element.xpath('gml:pos', namespaces=nsmap) coordinates_elems = element.xpath('gml:coordinates', namespaces=nsmap) coords = element.xpath('gml:coord', namespaces=nsmap) if pos_lists: if len(pos_lists) > 1: raise ValueError('Too many gml:posList elements') pos_list0 = pos_lists[0] coordinates = parse_poslist( pos_list0.text, int(pos_list0.attrib.get('srsDimension', 2)) ) srs = pos_list0.attrib.get('srsName') elif poss: coordinates = [ parse_pos(pos.text) for pos in poss ] srs = determine_srs( *element.xpath('gml:pos/@srsName', namespaces=nsmap) ) elif coordinates_elems: if len(coordinates_elems) > 1: raise ValueError('Too many gml:coordinates elements') coordinates0 = coordinates_elems[0] coordinates = parse_coordinates( coordinates0.text, cs=coordinates0.attrib.get('cs', ','), ts=coordinates0.attrib.get('ts', ' '), decimal=coordinates0.attrib.get('decimal', '.'), ) srs = None elif coords: coordinates = [ parse_coord(coord) for coord in coords ] srs = None else: raise ValueError('No gml:posList, gml:pos or gml:coordinates found') srs = determine_srs(element.attrib.get('srsName'), srs) return { 'type': 'LineString', 'coordinates': coordinates }, srs def parse_multi_curve(element: Element, nsmap: NameSpaceMap) -> ParseResult: linestring_elements = element.xpath( '(gml:curveMember|gml:curveMembers)/gml:LineString', namespaces=nsmap ) are_not_linestrings = ( etree.QName(e.tag).localname != 'LineString' for e in linestring_elements ) if any(are_not_linestrings): raise ValueError( 'Only gml:LineString elements are supported for gml:MultiCurves' ) linestrings, srss = zip(*( parse_linestring_or_linear_ring(linestring_element, nsmap) for linestring_element in linestring_elements )) srs = determine_srs(element.attrib.get('srsName'), *srss) return { 'type': 'MultiLineString', 'coordinates': [ linestring['coordinates'] for linestring in linestrings ] }, srs def parse_multi_linestring(element: Element, nsmap: NameSpaceMap) -> ParseResult: linestring_elements = element.xpath( 'gml:lineStringMember/gml:LineString', namespaces=nsmap ) linestrings, srss = zip(*( parse_linestring_or_linear_ring(linestring_element, nsmap) for linestring_element in linestring_elements )) srs = determine_srs(element.attrib.get('srsName'), *srss) return { 'type': 'MultiLineString', 'coordinates': [ linestring['coordinates'] for linestring in linestrings ] }, srs def parse_polygon(element: Element, nsmap: NameSpaceMap) -> ParseResult: exterior_rings = element.xpath( 'gml:exterior/gml:LinearRing', namespaces=nsmap ) if not exterior_rings: raise ValueError('No gml:exterior/gml:LinearRing') elif len(exterior_rings) > 1: raise ValueError('Too many gml:exterior/gml:LinearRing elements') exterior, ext_srs = parse_linestring_or_linear_ring( exterior_rings[0], nsmap ) exterior = exterior['coordinates'] interior_elems = element.xpath( 'gml:interior/gml:LinearRing', namespaces=nsmap ) if len(interior_elems) > 0: interior_rings, int_srss = zip(*( parse_linestring_or_linear_ring(linear_ring, nsmap) for linear_ring in interior_elems )) interiors = [ ring['coordinates'] for ring in interior_rings ] else: interiors = [] int_srss = [] srs = determine_srs(element.attrib.get('srsName'), ext_srs, *int_srss) return { 'type': 'Polygon', 'coordinates': [exterior, *interiors] }, srs def parse_multi_surface(element: Element, nsmap: NameSpaceMap) -> ParseResult: polygon_elements = element.xpath( '(gml:surfaceMember|gml:surfaceMembers)/gml:Polygon', namespaces=nsmap ) are_not_polygons = ( etree.QName(e.tag).localname != 'Polygon' for e in polygon_elements ) if any(are_not_polygons): raise ValueError( 'Only gml:Polygon elements are supported for gml:MultiSurfaces' ) polygons, srss = zip(*( parse_polygon(polygon_element, nsmap) for polygon_element in polygon_elements )) srs = determine_srs(element.attrib.get('srsName'), *srss) return { 'type': 'MultiPolygon', 'coordinates': [ polygon['coordinates'] for polygon in polygons ] }, srs def parse_multi_polygon(element: Element, nsmap: NameSpaceMap) -> ParseResult: polygon_elements = element.xpath( 'gml:polygonMember/gml:Polygon', namespaces=nsmap ) polygons, srss = zip(*( parse_polygon(polygon_element, nsmap) for polygon_element in polygon_elements )) srs = determine_srs(element.attrib.get('srsName'), *srss) return { 'type': 'MultiPolygon', 'coordinates': [ polygon['coordinates'] for polygon in polygons ] }, srs def parse_envelope(element: Element, nsmap: NameSpaceMap) -> ParseResult: lower = element.xpath('gml:lowerCorner', namespaces=nsmap) upper = element.xpath('gml:upperCorner', namespaces=nsmap) pos_elems = element.xpath('gml:pos', namespaces=nsmap) coordinates = element.xpath('gml:coordinates', namespaces=nsmap) coords = element.xpath('gml:coord', namespaces=nsmap) if lower and upper: lower = lower[0] upper = upper[0] srs = determine_srs( lower.attrib.get('srsName'), upper.attrib.get('srsName') ) lower = parse_pos(lower.text) upper = parse_pos(upper.text) elif pos_elems: lower, upper = [ parse_pos(pos_elem.text) for pos_elem in pos_elems ] srs = determine_srs(*( pos_elem.attrib.get('srsName') for pos_elem in pos_elems )) elif coordinates: coordinates0 = coordinates[0] lower, upper = parse_coordinates( coordinates0.text, cs=coordinates0.attrib.get('cs', ','), ts=coordinates0.attrib.get('ts', ' '), decimal=coordinates0.attrib.get('decimal', '.'), ) srs = None elif coords: lower, upper = [ parse_coord(coord) for coord in coords ] srs = None else: raise ValueError( 'Missing gml:lowerCorner, gml:upperCorner, gml:pos or ' 'gml:coordinates.' ) lx, ly = lower hx, hy = upper return { 'type': 'Polygon', 'coordinates': [ [ (lx, ly), (lx, hy), (hx, hy), (hx, ly), (lx, ly), ] ] }, srs SubParser = Callable[[Element], GeomDict] def parse_multi_geometry(element: Element, nsmap: NameSpaceMap, geometry_parser: SubParser) -> ParseResult: sub_elements = element.xpath( '(gml:geometryMember|gml:geometryMembers)/*', namespaces=nsmap ) return { 'type': 'GeometryCollection', 'geometries': [ geometry_parser(sub_element) for sub_element in sub_elements ] }, element.attrib.get('srsName') class GML3Encoder: def __init__(self, namespace: str, nsmap: NameSpaceMap, id_required: bool): self.namespace = namespace self.nsmap = nsmap self.id_required = id_required self.gml = ElementMaker(namespace=namespace, nsmap=nsmap) def encode(self, geometry: GeomDict, identifier: str = None) -> Element: if not identifier and self.id_required: raise TypeError( "Missing 1 required positional argument: 'identifier'" ) gml = self.gml crs = geometry.get('crs') srs = None if identifier: id_attr = {f'{{{self.namespace}}}id': identifier} else: id_attr = {} if crs: srs = crs.get('properties', {}).get('name') else: # GeoJSON is by default in CRS84 srs = 'urn:ogc:def:crs:OGC::CRS84' attrs = { 'srsName': srs, **id_attr } geometry = maybe_swap_coordinates(geometry, srs) type_ = geometry['type'] # GeometryCollections have no coordinates coordinates = geometry.get('coordinates') if type_ == 'Point': return self.encode_point(coordinates, attrs) elif type_ == 'MultiPoint': return self.encode_multi_point(coordinates, identifier, attrs) elif type_ == 'LineString': return self.encode_line_string(coordinates, attrs) elif type_ == 'MultiLineString': return self.encode_multi_line_string( coordinates, identifier, attrs ) elif type_ == 'Polygon': return self.encode_polygon(coordinates, attrs) elif type_ == 'MultiPolygon': return self.encode_multi_polygon(coordinates, identifier, attrs) elif type_ == 'GeometryCollection': geometries = geometry['geometries'] return gml( 'MultiGeometry', gml( 'geometryMembers', *[ self.encode(sub_geometry, f'{identifier}_{i}') for i, sub_geometry in enumerate(geometries) ] ), **id_attr ) raise ValueError(f'Unable to encode geometry of type {type_}') def encode_point(self, coordinates: Coordinates, attrs: dict) -> Element: return self.gml( 'Point', self.gml('pos', ' '.join(str(c) for c in coordinates)), **attrs ) def encode_multi_point(self, coordinates: Coordinates, identifier: str, attrs: dict) -> Element: return self.gml( 'MultiPoint', self.gml('geometryMembers', *[ self.gml( 'Point', self.gml('pos', ' '.join(str(c) for c in coordinate)), **{f'{{{self.namespace}}}id': f'{identifier}_{i}'} ) for i, coordinate in enumerate(coordinates) ]), **attrs ) def encode_line_string(self, coordinates: Coordinates, attrs: dict) -> Element: return self.gml( 'LineString', self._encode_pos_list(coordinates), **attrs ) def encode_multi_line_string(self, coordinates: Coordinates, identifier: str, attrs: dict) -> Element: return self.gml( 'MultiCurve', self.gml( 'curveMembers', *[ self.gml( 'LineString', self._encode_pos_list(linestring), **{f'{{{self.namespace}}}id': f'{identifier}_{i}'} ) for i, linestring in enumerate(coordinates) ] ), **attrs ) def encode_polygon(self, coordinates: Coordinates, attrs: dict) -> Element: return self.gml( 'Polygon', self.gml( 'exterior', self.gml( 'LinearRing', self._encode_pos_list(coordinates[0]), ) ), *[ self.gml( 'interior', self.gml( 'LinearRing', self._encode_pos_list(linear_ring), ) ) for linear_ring in coordinates[1:] ], **attrs ) def encode_multi_polygon(self, coordinates: Coordinates, identifier: str, attrs: dict) -> Element: return self.gml( 'MultiSurface', self.gml( 'surfaceMembers', *[ self.gml( 'Polygon', self.gml( 'exterior', self.gml( 'LinearRing', self._encode_pos_list(polygon[0]), ) ), *[ self.gml( 'interior', self.gml( 'LinearRing', self._encode_pos_list(linear_ring), ) ) for linear_ring in polygon[1:] ], **{f'{{{self.namespace}}}id': f'{identifier}_{i}'} ) for i, polygon in enumerate(coordinates) ] ), **attrs ) def _encode_pos_list(self, coordinates: Coordinates) -> Element: return self.gml( 'posList', ' '.join( ' '.join(str(c) for c in coordinate) for coordinate in coordinates ) ) geopython-pygml-5ce8eb0/requirements-test.txt000066400000000000000000000000131416035544100216070ustar00rootroot00000000000000pytest lxmlgeopython-pygml-5ce8eb0/setup.py000066400000000000000000000057661416035544100171040ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # # Project: pygml # Authors: Fabian Schindler # # ------------------------------------------------------------------------------ # Copyright (C) 2021 EOX IT Services GmbH # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies of this Software or works derived from this Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # ------------------------------------------------------------------------------ """Install pygml.""" from setuptools import find_packages, setup import os import os.path # don't install dependencies when building win readthedocs on_rtd = os.environ.get('READTHEDOCS') == 'True' # get version number # from https://github.com/mapbox/rasterio/blob/master/setup.py#L55 with open(os.path.join(os.path.dirname(__file__), 'pygml/__init__.py')) as f: for line in f: if line.find("__version__") >= 0: version = line.split("=")[1].strip() version = version.strip('"') version = version.strip("'") break # use README.md for project long_description with open('README.md') as f: readme = f.read() setup( name='pygml', version=version, description='Parsing GML geometries', long_description=readme, long_description_content_type="text/markdown", author='Fabian Schindler', author_email='fabian.schindler@eox.at', url='https://github.com/geopython/pygml', license='MIT', packages=find_packages(), include_package_data=True, install_requires=[ "dataclasses;python_version<'3.7'", "lxml", ] if not on_rtd else [], classifiers=[ 'Development Status :: 3 - Alpha', 'Intended Audience :: Developers', 'Topic :: Scientific/Engineering :: GIS', 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', ], tests_require=['pytest'] ) geopython-pygml-5ce8eb0/tests/000077500000000000000000000000001416035544100165165ustar00rootroot00000000000000geopython-pygml-5ce8eb0/tests/__init__.py000066400000000000000000000000001416035544100206150ustar00rootroot00000000000000geopython-pygml-5ce8eb0/tests/test_axisorder.py000066400000000000000000000030601416035544100221260ustar00rootroot00000000000000import pytest from pygml.axisorder import is_crs_yx, get_crs_code def test_get_crs_code(): # test with a reversed code assert get_crs_code('EPSG:4326') == 4326 assert get_crs_code('http://www.opengis.net/def/crs/EPSG/0/4326') == 4326 assert get_crs_code('http://www.opengis.net/gml/srs/epsg.xml#4326') == 4326 assert get_crs_code('urn:EPSG:geographicCRS:4326') == 4326 assert get_crs_code('urn:ogc:def:crs:EPSG::4326') == 4326 assert get_crs_code('urn:ogc:def:crs:EPSG:4326') == 4326 assert get_crs_code('urn:ogc:def:crs:OGC::CRS84') == 'CRS84' # test with some garbage format with pytest.raises(ValueError): get_crs_code('abcd:4326') def test_is_crs_yx(): # test with a reversed code assert is_crs_yx('EPSG:4326') assert is_crs_yx('http://www.opengis.net/def/crs/EPSG/0/4326') assert is_crs_yx('http://www.opengis.net/gml/srs/epsg.xml#4326') assert is_crs_yx('urn:EPSG:geographicCRS:4326') assert is_crs_yx('urn:ogc:def:crs:EPSG::4326') assert is_crs_yx('urn:ogc:def:crs:EPSG:4326') # test with a non-reversed code assert is_crs_yx('EPSG:3857') is False assert is_crs_yx('http://www.opengis.net/def/crs/EPSG/0/3857') is False assert is_crs_yx('http://www.opengis.net/gml/srs/epsg.xml#3857') is False assert is_crs_yx('urn:EPSG:geographicCRS:3857') is False assert is_crs_yx('urn:ogc:def:crs:EPSG::3857') is False assert is_crs_yx('urn:ogc:def:crs:EPSG:3857') is False # test with some garbage format with pytest.raises(ValueError): is_crs_yx('abcd:4326') geopython-pygml-5ce8eb0/tests/test_basics.py000066400000000000000000000045141416035544100213770ustar00rootroot00000000000000import pytest from pygml.basics import ( parse_coordinates, parse_poslist, parse_pos, swap_coordinate_xy, swap_coordinates_xy ) def test_parse_coordinates(): # basic test result = parse_coordinates('12.34 56.7,89.10 11.12') assert result == [(12.34, 56.7), (89.10, 11.12)] # ignore some whitespace result = parse_coordinates('12.34 56.7, 89.10 11.12') assert result == [(12.34, 56.7), (89.10, 11.12)] # custom cs result = parse_coordinates('12.34 56.7;89.10 11.12', cs=';') assert result == [(12.34, 56.7), (89.10, 11.12)] # custom ts result = parse_coordinates('12.34:56.7,89.10:11.12', ts=':') assert result == [(12.34, 56.7), (89.10, 11.12)] # custom cs/ts result = parse_coordinates('12.34:56.7;89.10:11.12', cs=';', ts=':') assert result == [(12.34, 56.7), (89.10, 11.12)] # custom cs/ts and decimal result = parse_coordinates( '12,34:56,7;89,10:11,12', cs=';', ts=':', decimal=',' ) assert result == [(12.34, 56.7), (89.10, 11.12)] def test_parse_poslist(): # basic test result = parse_poslist('12.34 56.7 89.10 11.12') assert result == [(12.34, 56.7), (89.10, 11.12)] # 3D coordinates result = parse_poslist('12.34 56.7 89.10 11.12 13.14 15.16', dimensions=3) assert result == [(12.34, 56.7, 89.10), (11.12, 13.14, 15.16)] # exception on wrong dimensionality with pytest.raises(ValueError): parse_poslist('12.34 56.7 89.10 11.12', dimensions=3) def test_parse_pos(): # basic test result = parse_pos('12.34 56.7') assert result == (12.34, 56.7) # 3D pos result = parse_pos('12.34 56.7 89.10') assert result == (12.34, 56.7, 89.10) def test_swap_coordinate_xy(): # basic test swapped = swap_coordinate_xy((12.34, 56.7)) assert swapped == (56.7, 12.34) # 3D coords, only X/Y are to be swapped swapped = swap_coordinate_xy((12.34, 56.7, 89.10)) assert swapped == (56.7, 12.34, 89.10) def test_swap_coordinates_xy(): # basic test swapped = swap_coordinates_xy( [(12.34, 56.7), (89.10, 11.12)] ) assert swapped == [(56.7, 12.34), (11.12, 89.10)] # 3D coords, only X/Y are to be swapped swapped = swap_coordinates_xy( [(12.34, 56.7, 89.10), (11.12, 13.14, 15.16)] ) assert swapped == [(56.7, 12.34, 89.10), (13.14, 11.12, 15.16)] geopython-pygml-5ce8eb0/tests/test_dimensionality.py000066400000000000000000000135231416035544100231630ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # # Project: pygml # Authors: Fabian Schindler # # ------------------------------------------------------------------------------ # Copyright (C) 2021 EOX IT Services GmbH # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies of this Software or works derived from this Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # ------------------------------------------------------------------------------ from pygml.dimensionality import get_dimensionality def test_dimensionality_point(): assert 2 == get_dimensionality({ 'type': 'Point', 'coordinates': (1.0, 1.0) }) assert 3 == get_dimensionality({ 'type': 'Point', 'coordinates': (1.0, 1.0, 1.0) }) def test_dimensionality_multi_point(): assert 2 == get_dimensionality({ 'type': 'MultiPoint', 'coordinates': [ (1.0, 1.0), (2.0, 2.0), ] }) assert 3 == get_dimensionality({ 'type': 'MultiPoint', 'coordinates': [ (1.0, 1.0, 1.0), (2.0, 2.0, 1.0), ] }) def test_dimensionality_linestring(): assert 2 == get_dimensionality({ 'type': 'LineString', 'coordinates': [ (2.0, 1.0), (1.0, 2.0) ] }) assert 3 == get_dimensionality({ 'type': 'LineString', 'coordinates': [ (2.0, 1.0, 1.0), (1.0, 2.0, 1.0) ] }) def test_dimensionality_multi_linestring(): assert 2 == get_dimensionality({ 'type': 'MultiLineString', 'coordinates': [ [ (1.0, 1.0), (2.0, 2.0) ], [ (3.0, 3.0), (4.0, 4.0) ], ] }) assert 3 == get_dimensionality({ 'type': 'MultiLineString', 'coordinates': [ [ (1.0, 1.0, 1.0), (2.0, 2.0, 1.0) ], [ (3.0, 3.0, 1.0), (4.0, 4.0, 1.0) ], ] }) def test_dimensionality_polygon(): assert 2 == get_dimensionality({ 'type': 'Polygon', 'coordinates': [ [ (0.5, 1.0), (0.5, 2.0), (1.5, 2.0), (1.5, 1.0), (0.5, 1.0) ] ] }) assert 3 == get_dimensionality({ 'type': 'Polygon', 'coordinates': [ [ (0.5, 1.0, 1.0), (0.5, 2.0, 1.0), (1.5, 2.0, 1.0), (1.5, 1.0, 1.0), (0.5, 1.0, 1.0) ] ] }) def test_dimensionality_multi_polygon(): assert 2 == get_dimensionality({ 'type': 'MultiPolygon', 'coordinates': [ [ [ (0.0, 0.0), (1.0, 0.0), (0.0, 1.0), (0.0, 0.0) ], [ (0.2, 0.2), (0.5, 0.2), (0.2, 0.5), (0.2, 0.2) ], ], [ [ (10.0, 10.0), (11.0, 10.0), (10.0, 11.0), (10.0, 10.0) ], [ (10.2, 10.2), (10.5, 10.2), (10.2, 10.5), (10.2, 10.2) ], ] ] }) assert 3 == get_dimensionality({ 'type': 'MultiPolygon', 'coordinates': [ [ [ (0.0, 0.0, 1.0), (1.0, 0.0, 1.0), (0.0, 1.0, 1.0), (0.0, 0.0, 1.0) ], [ (0.2, 0.2, 1.0), (0.5, 0.2, 1.0), (0.2, 0.5, 1.0), (0.2, 0.2, 1.0) ], ], [ [ (10.0, 10.0, 1.0), (11.0, 10.0, 1.0), (10.0, 11.0, 1.0), (10.0, 10.0, 1.0) ], [ (10.2, 10.2, 1.0), (10.5, 10.2, 1.0), (10.2, 10.5, 1.0), (10.2, 10.2, 1.0) ], ] ] }) def test_dimensionality_geometrycollection(): assert None is get_dimensionality({ 'type': 'GeometryCollection', 'geometries': [ { 'type': 'Point', 'coordinates': (1.0, 1.0) }, { 'type': 'Polygon', 'coordinates': [ [(1.0, 1.0)], [(1.0, 1.0)], ] }, ] }) geopython-pygml-5ce8eb0/tests/test_georss.py000066400000000000000000000250551416035544100214400ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # # Project: pygml # Authors: Fabian Schindler # # ------------------------------------------------------------------------------ # Copyright (C) 2021 EOX IT Services GmbH # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies of this Software or works derived from this Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # ------------------------------------------------------------------------------ from lxml import etree from pygml.georss import encode_georss, parse_georss from .util import compare_trees def test_parse_point(): # basic test result = parse_georss( etree.fromstring(""" 1.0 1.0 """) ) assert result == {'type': 'Point', 'coordinates': (1.0, 1.0)} def test_parse_line(): # basic test result = parse_georss( etree.fromstring(""" 1.0 2.0 2.0 1.0 """) ) assert result == { 'type': 'LineString', 'coordinates': [ (2.0, 1.0), (1.0, 2.0) ] } def test_parse_box(): # basic test result = parse_georss( etree.fromstring(""" 1.0 0.5 2.0 1.5 """) ) assert result == { 'type': 'Polygon', 'bbox': (0.5, 1.0, 1.5, 2.0), 'coordinates': [ [ (0.5, 1.0), (0.5, 2.0), (1.5, 2.0), (1.5, 1.0), (0.5, 1.0) ] ] } def test_parse_polygon(): # basic test result = parse_georss( etree.fromstring(""" 1.0 0.5 2.0 0.5 2.0 1.5 1.0 1.5 1.0 0.5 """) ) assert result == { 'type': 'Polygon', 'coordinates': [ [ (0.5, 1.0), (0.5, 2.0), (1.5, 2.0), (1.5, 1.0), (0.5, 1.0) ] ] } def test_parse_where(): # TODO: add tests as soon as gml 3.1 is done pass def test_encode_point(): # test that simple points can be encoded result = encode_georss({'type': 'Point', 'coordinates': (1.0, 2.0)}) expected = etree.fromstring(""" 2.0 1.0 """) assert compare_trees( expected, result ), f'{etree.tostring(expected)} != {etree.tostring(result)}' # test that >2D geometries or with a specific CRS # can only be encoded using georss:where and GML result = encode_georss({'type': 'Point', 'coordinates': (1.0, 2.0, 1.0)}) assert result.tag == '{http://www.georss.org/georss}where' assert etree.QName(result[0].tag).localname == 'Point' result = encode_georss({ 'type': 'Point', 'coordinates': (1.0, 2.0), 'crs': { 'type': 'name', 'properties': { 'name': 'EPSG:3857' } } }) assert result.tag == '{http://www.georss.org/georss}where' assert etree.QName(result[0].tag).localname == 'Point' def test_encode_multi_point(): result = encode_georss({ 'type': 'MultiPoint', 'coordinates': [ (1.0, 2.0), (3.0, 4.0), ] }) assert result.tag == '{http://www.georss.org/georss}where' assert etree.QName(result[0].tag).localname == 'MultiPoint' def test_encode_linestring(): # test that simple points can be encoded result = encode_georss({ 'type': 'LineString', 'coordinates': [ (1.0, 2.0), (3.0, 4.0), ] }) expected = etree.fromstring(""" 2.0 1.0 4.0 3.0 """) assert compare_trees( expected, result ), f'{etree.tostring(expected)} != {etree.tostring(result)}' # test that >2D geometries or with a specific CRS # can only be encoded using georss:where and GML result = encode_georss({ 'type': 'LineString', 'coordinates': [ (1.0, 2.0, 1.0), (3.0, 4.0, 1.0), ] }) assert result.tag == '{http://www.georss.org/georss}where' assert etree.QName(result[0].tag).localname == 'LineString' result = encode_georss({ 'type': 'LineString', 'coordinates': [ (1.0, 2.0), (3.0, 4.0), ], 'crs': { 'type': 'name', 'properties': { 'name': 'EPSG:3857' } } }) assert result.tag == '{http://www.georss.org/georss}where' assert etree.QName(result[0].tag).localname == 'LineString' def test_encode_multi_linestring(): result = encode_georss({ 'type': 'MultiLineString', 'coordinates': [ [ (1.0, 2.0), (3.0, 4.0), ], [ (11.0, 12.0), (13.0, 14.0), ] ] }) assert result.tag == '{http://www.georss.org/georss}where' assert etree.QName(result[0].tag).localname == 'MultiCurve' def test_encode_polygon(): result = encode_georss({ 'type': 'Polygon', 'coordinates': [ [ (0.5, 1.0), (0.5, 2.0), (1.5, 2.0), (1.5, 1.0), (0.5, 1.0) ] ] }) expected = etree.fromstring(""" 1.0 0.5 2.0 0.5 2.0 1.5 1.0 1.5 1.0 0.5 """) assert compare_trees( expected, result ), f'{etree.tostring(expected)} != {etree.tostring(result)}' # test that >2D geometries or with a specific CRS or polygons with holes # can only be encoded using georss:where and GML result = encode_georss({ 'type': 'Polygon', 'coordinates': [ [ (0.5, 1.0), (0.5, 2.0), (1.5, 2.0), (1.5, 1.0), (0.5, 1.0) ], [ (0.6, 1.1), (0.6, 1.9), (1.4, 1.9), (1.4, 1.1), (0.6, 1.1) ] ] }) assert result.tag == '{http://www.georss.org/georss}where' assert etree.QName(result[0].tag).localname == 'Polygon' result = encode_georss({ 'type': 'Polygon', 'coordinates': [ [ (0.5, 1.0, 1.0), (0.5, 2.0, 1.0), (1.5, 2.0, 1.0), (1.5, 1.0, 1.0), (0.5, 1.0, 1.0) ] ] }) assert result.tag == '{http://www.georss.org/georss}where' assert etree.QName(result[0].tag).localname == 'Polygon' result = encode_georss({ 'type': 'Polygon', 'coordinates': [ [ (0.5, 1.0), (0.5, 2.0), (1.5, 2.0), (1.5, 1.0), (0.5, 1.0) ] ], 'crs': { 'type': 'name', 'properties': { 'name': 'EPSG:3857' } } }) assert result.tag == '{http://www.georss.org/georss}where' assert etree.QName(result[0].tag).localname == 'Polygon' def test_encode_multi_polygon(): result = encode_georss({ 'type': 'MultiPolygon', 'coordinates': [ [ [ (1.0, 2.0), (1.0, 3.0), (2.0, 3.0), (2.0, 2.0), (1.0, 2.0), ], [ (1.4, 2.4), (1.4, 2.6), (1.6, 2.6), (1.6, 2.4), (1.4, 2.4), ], ], [ [ (11.0, 12.0), (11.0, 13.0), (12.0, 13.0), (12.0, 12.0), (11.0, 12.0), ], [ (11.4, 12.4), (11.4, 12.6), (11.6, 12.6), (11.6, 12.4), (11.4, 12.4), ], ], ], }) assert result.tag == '{http://www.georss.org/georss}where' assert etree.QName(result[0].tag).localname == 'MultiSurface' def test_encode_geometry_collection(): result = encode_georss({ 'type': 'GeometryCollection', 'geometries': [ { 'type': 'Point', 'coordinates': (1.0, 2.0), }, { 'type': 'Polygon', 'coordinates': [ [ (1.0, 2.0), (1.0, 3.0), (2.0, 3.0), (2.0, 2.0), (1.0, 2.0), ], [ (1.4, 2.4), (1.4, 2.6), (1.6, 2.6), (1.6, 2.4), (1.4, 2.4), ], ], }, ] }) assert result.tag == '{http://www.georss.org/georss}where' assert etree.QName(result[0].tag).localname == 'MultiGeometry' geopython-pygml-5ce8eb0/tests/test_pre_v32.py000066400000000000000000001511471416035544100214200ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # # Project: pygml # Authors: Fabian Schindler # # ------------------------------------------------------------------------------ # Copyright (C) 2021 EOX IT Services GmbH # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies of this Software or works derived from this Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # ------------------------------------------------------------------------------ from lxml import etree import pytest from pygml.pre_v32 import encode_pre_v32, parse_pre_v32 from .util import compare_trees def test_parse_point(): # basic test result = parse_pre_v32( etree.fromstring(""" 1.0 1.0 """) ) assert result == {'type': 'Point', 'coordinates': (1.0, 1.0)} # using gml:coordinates instead result = parse_pre_v32( etree.fromstring(""" 1.0 1.0 """) ) assert result == {'type': 'Point', 'coordinates': (1.0, 1.0)} # axis order swapping with srsName in pos or Point result = parse_pre_v32( etree.fromstring(""" 2.0 1.0 """) ) assert result == { 'type': 'Point', 'coordinates': (1.0, 2.0), 'crs': { 'type': 'name', 'properties': { 'name': 'EPSG:4326' } } } result = parse_pre_v32( etree.fromstring(""" 2.0 1.0 """) ) assert result == { 'type': 'Point', 'coordinates': (1.0, 2.0), 'crs': { 'type': 'name', 'properties': { 'name': 'EPSG:4326' } } } # conflicting srsName with pytest.raises(ValueError): parse_pre_v32( etree.fromstring(""" 2.0 1.0 """) ) def test_parse_multi_point(): # using gml:pointMember result = parse_pre_v32( etree.fromstring(""" 1.0 1.0 2.0 2.0 """) ) assert result == { 'type': 'MultiPoint', 'coordinates': [ (1.0, 1.0), (2.0, 2.0), ] } # using gml:pointMembers result = parse_pre_v32( etree.fromstring(""" 1.0 1.0 2.0 2.0 """) ) assert result == { 'type': 'MultiPoint', 'coordinates': [ (1.0, 1.0), (2.0, 2.0), ] } # using gml:pointMember and gml:pointMembers result = parse_pre_v32( etree.fromstring(""" 1.0 1.0 2.0 2.0 3.0 3.0 """) ) assert result == { 'type': 'MultiPoint', 'coordinates': [ (1.0, 1.0), (2.0, 2.0), (3.0, 3.0), ] } # conflicting srsName with pytest.raises(ValueError): parse_pre_v32( etree.fromstring(""" 1.0 1.0 2.0 2.0 """) ) def test_parse_linestring(): # from gml:posList result = parse_pre_v32( etree.fromstring(""" 1.0 1.0 2.0 2.0 """) ) assert result == { 'type': 'LineString', 'coordinates': [ (1.0, 1.0), (2.0, 2.0), ] } # from gml:pos elements result = parse_pre_v32( etree.fromstring(""" 1.0 1.0 2.0 2.0 """) ) assert result == { 'type': 'LineString', 'coordinates': [ (1.0, 1.0), (2.0, 2.0), ] } # from gml:coordinates result = parse_pre_v32( etree.fromstring(""" 1.0 1.0,2.0 2.0 """) ) assert result == { 'type': 'LineString', 'coordinates': [ (1.0, 1.0), (2.0, 2.0), ] } # from gml:pos elements with srsName result = parse_pre_v32( etree.fromstring(""" 1.0 1.0 2.0 2.0 """) ) assert result == { 'type': 'LineString', 'coordinates': [ (1.0, 1.0), (2.0, 2.0), ], 'crs': { 'type': 'name', 'properties': { 'name': 'EPSG:4326' } } } # from gml:posList element with srsName result = parse_pre_v32( etree.fromstring(""" 1.0 1.0 2.0 2.0 """) ) assert result == { 'type': 'LineString', 'coordinates': [ (1.0, 1.0), (2.0, 2.0), ], 'crs': { 'type': 'name', 'properties': { 'name': 'EPSG:4326' } } } # from gml:coordinates element with srsName result = parse_pre_v32( etree.fromstring(""" 1.0 1.0,2.0 2.0 """) ) assert result == { 'type': 'LineString', 'coordinates': [ (1.0, 1.0), (2.0, 2.0), ], 'crs': { 'type': 'name', 'properties': { 'name': 'EPSG:4326' } } } # srsName conflict with pytest.raises(ValueError): parse_pre_v32( etree.fromstring(""" 1.0 1.0 2.0 2.0 """) ) def test_parse_multi_curve(): # using gml:curveMember elements result = parse_pre_v32( etree.fromstring(""" 1.0 1.0 2.0 2.0 3.0 3.0 4.0 4.0 """) ) assert result == { 'type': 'MultiLineString', 'coordinates': [ [(1.0, 1.0), (2.0, 2.0)], [(3.0, 3.0), (4.0, 4.0)], ] } # using gml:curveMembers element result = parse_pre_v32( etree.fromstring(""" 1.0 1.0 2.0 2.0 3.0 3.0 4.0 4.0 """) ) assert result == { 'type': 'MultiLineString', 'coordinates': [ [(1.0, 1.0), (2.0, 2.0)], [(3.0, 3.0), (4.0, 4.0)], ] } # determine srsName from MultiCurve result = parse_pre_v32( etree.fromstring(""" 1.0 1.0 2.0 2.0 3.0 3.0 4.0 4.0 """) ) assert result == { 'type': 'MultiLineString', 'coordinates': [ [(1.0, 1.0), (2.0, 2.0)], [(3.0, 3.0), (4.0, 4.0)], ], 'crs': { 'type': 'name', 'properties': { 'name': 'EPSG:4326' } } } # determine srsName from first LineString result = parse_pre_v32( etree.fromstring(""" 1.0 1.0 2.0 2.0 3.0 3.0 4.0 4.0 """) ) assert result == { 'type': 'MultiLineString', 'coordinates': [ [(1.0, 1.0), (2.0, 2.0)], [(3.0, 3.0), (4.0, 4.0)], ], 'crs': { 'type': 'name', 'properties': { 'name': 'EPSG:4326' } } } # srsName conflict with pytest.raises(ValueError): parse_pre_v32( etree.fromstring(""" 1.0 1.0 2.0 2.0 3.0 3.0 4.0 4.0 """) ) with pytest.raises(ValueError): parse_pre_v32( etree.fromstring(""" 1.0 1.0 2.0 2.0 3.0 3.0 4.0 4.0 """) ) def test_parse_polygon(): # using gml:posList result = parse_pre_v32( etree.fromstring(""" 0.0 0.0 1.0 0.0 0.0 1.0 0.0 0.0 0.2 0.2 0.5 0.2 0.2 0.5 0.2 0.2 """) ) assert result == { 'type': 'Polygon', 'coordinates': [ [ (0.0, 0.0), (1.0, 0.0), (0.0, 1.0), (0.0, 0.0) ], [ (0.2, 0.2), (0.5, 0.2), (0.2, 0.5), (0.2, 0.2) ], ] } # using gml:posList with no interiors result = parse_pre_v32( etree.fromstring(""" 0.0 0.0 1.0 0.0 0.0 1.0 0.0 0.0 """) ) assert result == { 'type': 'Polygon', 'coordinates': [ [ (0.0, 0.0), (1.0, 0.0), (0.0, 1.0), (0.0, 0.0) ], ] } # using gml:pos elements result = parse_pre_v32( etree.fromstring(""" 0.0 0.0 1.0 0.0 0.0 1.0 0.0 0.0 0.2 0.2 0.5 0.2 0.2 0.5 0.2 0.2 """) ) assert result == { 'type': 'Polygon', 'coordinates': [ [ (0.0, 0.0), (1.0, 0.0), (0.0, 1.0), (0.0, 0.0) ], [ (0.2, 0.2), (0.5, 0.2), (0.2, 0.5), (0.2, 0.2) ], ] } # using gml:coordinates result = parse_pre_v32( etree.fromstring(""" 0.0 0.0,1.0 0.0,0.0 1.0,0.0 0.0 0.2 0.2,0.5 0.2,0.2 0.5,0.2 0.2 """) ) assert result == { 'type': 'Polygon', 'coordinates': [ [ (0.0, 0.0), (1.0, 0.0), (0.0, 1.0), (0.0, 0.0) ], [ (0.2, 0.2), (0.5, 0.2), (0.2, 0.5), (0.2, 0.2) ], ] } # using gml:posList with srsName result = parse_pre_v32( etree.fromstring(""" 0.0 0.0 1.0 0.0 0.0 1.0 0.0 0.0 0.2 0.2 0.5 0.2 0.2 0.5 0.2 0.2 """) ) assert result == { 'type': 'Polygon', 'coordinates': [ [ (0.0, 0.0), (0.0, 1.0), (1.0, 0.0), (0.0, 0.0) ], [ (0.2, 0.2), (0.2, 0.5), (0.5, 0.2), (0.2, 0.2) ], ], 'crs': { 'type': 'name', 'properties': { 'name': 'EPSG:4326' } } } def test_parse_envelope(): # using gml:lowerCorner/gml:upperCorner result = parse_pre_v32( etree.fromstring(""" 0.0 1.0 2.0 3.0 """) ) assert result == { 'type': 'Polygon', 'coordinates': [ [ (0.0, 1.0), (0.0, 3.0), (2.0, 3.0), (2.0, 1.0), (0.0, 1.0), ], ] } # using gml:pos elements result = parse_pre_v32( etree.fromstring(""" 0.0 1.0 2.0 3.0 """) ) assert result == { 'type': 'Polygon', 'coordinates': [ [ (0.0, 1.0), (0.0, 3.0), (2.0, 3.0), (2.0, 1.0), (0.0, 1.0), ], ] } # using gml:coordinates result = parse_pre_v32( etree.fromstring(""" 0.0 1.0,2.0 3.0 """) ) assert result == { 'type': 'Polygon', 'coordinates': [ [ (0.0, 1.0), (0.0, 3.0), (2.0, 3.0), (2.0, 1.0), (0.0, 1.0), ], ] } # using gml:lowerCorner/gml:upperCorner with srsName result = parse_pre_v32( etree.fromstring(""" 0.0 1.0 2.0 3.0 """) ) assert result == { 'type': 'Polygon', 'coordinates': [ [ (1.0, 0.0), (3.0, 0.0), (3.0, 2.0), (1.0, 2.0), (1.0, 0.0), ], ], 'crs': { 'type': 'name', 'properties': { 'name': 'EPSG:4326' } } } def test_parse_multi_polygon(): # using gml:surfaceMember elements result = parse_pre_v32( etree.fromstring(""" 0.0 0.0 1.0 0.0 0.0 1.0 0.0 0.0 0.2 0.2 0.5 0.2 0.2 0.5 0.2 0.2 10.0 10.0 11.0 10.0 10.0 11.0 10.0 10.0 10.2 10.2 10.5 10.2 10.2 10.5 10.2 10.2 """) ) assert result == { 'type': 'MultiPolygon', 'coordinates': [ [ [ (0.0, 0.0), (1.0, 0.0), (0.0, 1.0), (0.0, 0.0) ], [ (0.2, 0.2), (0.5, 0.2), (0.2, 0.5), (0.2, 0.2) ], ], [ [ (10.0, 10.0), (11.0, 10.0), (10.0, 11.0), (10.0, 10.0) ], [ (10.2, 10.2), (10.5, 10.2), (10.2, 10.5), (10.2, 10.2) ], ] ] } # using gml:surfaceMember elements with no interiors result = parse_pre_v32( etree.fromstring(""" 0.0 0.0 1.0 0.0 0.0 1.0 0.0 0.0 10.0 10.0 11.0 10.0 10.0 11.0 10.0 10.0 """) ) assert result == { 'type': 'MultiPolygon', 'coordinates': [ [ [ (0.0, 0.0), (1.0, 0.0), (0.0, 1.0), (0.0, 0.0) ], ], [ [ (10.0, 10.0), (11.0, 10.0), (10.0, 11.0), (10.0, 10.0) ], ] ] } # using gml:surfaceMembers result = parse_pre_v32( etree.fromstring(""" 0.0 0.0 1.0 0.0 0.0 1.0 0.0 0.0 0.2 0.2 0.5 0.2 0.2 0.5 0.2 0.2 10.0 10.0 11.0 10.0 10.0 11.0 10.0 10.0 10.2 10.2 10.5 10.2 10.2 10.5 10.2 10.2 """) ) assert result == { 'type': 'MultiPolygon', 'coordinates': [ [ [ (0.0, 0.0), (1.0, 0.0), (0.0, 1.0), (0.0, 0.0) ], [ (0.2, 0.2), (0.5, 0.2), (0.2, 0.5), (0.2, 0.2) ], ], [ [ (10.0, 10.0), (11.0, 10.0), (10.0, 11.0), (10.0, 10.0) ], [ (10.2, 10.2), (10.5, 10.2), (10.2, 10.5), (10.2, 10.2) ], ] ] } # using gml:surfaceMembers with srsName result = parse_pre_v32( etree.fromstring(""" 0.0 0.0 1.0 0.0 0.0 1.0 0.0 0.0 0.2 0.2 0.5 0.2 0.2 0.5 0.2 0.2 10.0 10.0 11.0 10.0 10.0 11.0 10.0 10.0 10.2 10.2 10.5 10.2 10.2 10.5 10.2 10.2 """) ) assert result == { 'type': 'MultiPolygon', 'coordinates': [ [ [ (0.0, 0.0), (0.0, 1.0), (1.0, 0.0), (0.0, 0.0) ], [ (0.2, 0.2), (0.2, 0.5), (0.5, 0.2), (0.2, 0.2) ], ], [ [ (10.0, 10.0), (10.0, 11.0), (11.0, 10.0), (10.0, 10.0) ], [ (10.2, 10.2), (10.2, 10.5), (10.5, 10.2), (10.2, 10.2) ], ] ], 'crs': { 'type': 'name', 'properties': { 'name': 'EPSG:4326' } } } with pytest.raises(ValueError): parse_pre_v32( etree.fromstring(""" 0.0 0.0 1.0 0.0 0.0 1.0 0.0 0.0 0.2 0.2 0.5 0.2 0.2 0.5 0.2 0.2 10.0 10.0 11.0 10.0 10.0 11.0 10.0 10.0 10.2 10.2 10.5 10.2 10.2 10.5 10.2 10.2 """) ) def test_parse_multi_geometry(): # using geometryMembers result = parse_pre_v32( etree.fromstring(""" 1.0 1.0 1.0 1.0 1.0 1.0 """) ) assert result == { 'type': 'GeometryCollection', 'geometries': [ { 'type': 'Point', 'coordinates': (1.0, 1.0) }, { 'type': 'Polygon', 'coordinates': [ [(1.0, 1.0)], [(1.0, 1.0)], ] }, ] } # using geometryMember result = parse_pre_v32( etree.fromstring(""" 1.0 1.0 1.0 1.0 1.0 1.0 """) ) assert result == { 'type': 'GeometryCollection', 'geometries': [ { 'type': 'Point', 'coordinates': (1.0, 1.0) }, { 'type': 'Polygon', 'coordinates': [ [(1.0, 1.0)], [(1.0, 1.0)], ] }, ] } # allow varying srsNames result = parse_pre_v32( etree.fromstring(""" 1.0 1.0 1.0 1.0 1.0 1.0 """) ) assert result == { 'type': 'GeometryCollection', 'geometries': [ { 'type': 'Point', 'coordinates': (1.0, 1.0), 'crs': { 'type': 'name', 'properties': { 'name': 'EPSG:4326' } } }, { 'type': 'Polygon', 'coordinates': [ [(1.0, 1.0)], [(1.0, 1.0)], ], 'crs': { 'type': 'name', 'properties': { 'name': 'EPSG:3857' } } }, ] } def test_encode_pre_v32_point(): # encode Point result = encode_pre_v32({'type': 'Point', 'coordinates': (1.0, 2.0)}, 'ID') expected = etree.fromstring(""" 1.0 2.0 """) assert compare_trees( expected, result ), f'{etree.tostring(expected)} != {etree.tostring(result)}' # encode Point with EPSG:4326 result = encode_pre_v32({ 'type': 'Point', 'coordinates': (1.0, 2.0), 'crs': { 'type': 'name', 'properties': { 'name': 'EPSG:4326' } } }, 'ID') expected = etree.fromstring(""" 2.0 1.0 """) assert compare_trees( expected, result ), f'{etree.tostring(expected)} != {etree.tostring(result)}' def test_encode_pre_v32_multi_point(): # encode MultiPoint result = encode_pre_v32({ 'type': 'MultiPoint', 'coordinates': [ (1.0, 2.0), (3.0, 4.0), ] }, 'ID') expected = etree.fromstring(""" 1.0 2.0 3.0 4.0 """) assert compare_trees( expected, result ), f'{etree.tostring(expected)} != {etree.tostring(result)}' # encode MultiPoint with EPSG:4326 result = encode_pre_v32({ 'type': 'MultiPoint', 'coordinates': [ (1.0, 2.0), (3.0, 4.0), ], 'crs': { 'type': 'name', 'properties': { 'name': 'EPSG:4326' } } }, 'ID') expected = etree.fromstring(""" 2.0 1.0 4.0 3.0 """) assert compare_trees( expected, result ), f'{etree.tostring(expected)} != {etree.tostring(result)}' def test_encode_pre_v32_linestring(): # encode LineString result = encode_pre_v32({ 'type': 'LineString', 'coordinates': [ (1.0, 2.0), (3.0, 4.0), ], }, 'ID') expected = etree.fromstring(""" 1.0 2.0 3.0 4.0 """) assert compare_trees( expected, result ), f'{etree.tostring(expected)} != {etree.tostring(result)}' # encode LineString with EPSG:4326 result = encode_pre_v32({ 'type': 'LineString', 'coordinates': [ (1.0, 2.0), (3.0, 4.0), ], 'crs': { 'type': 'name', 'properties': { 'name': 'EPSG:4326' } } }, 'ID') expected = etree.fromstring(""" 2.0 1.0 4.0 3.0 """) assert compare_trees( expected, result ), f'{etree.tostring(expected)} != {etree.tostring(result)}' def test_encode_pre_v32_polygon(): # encode Polygon result = encode_pre_v32({ 'type': 'Polygon', 'coordinates': [ [ (1.0, 2.0), (1.0, 3.0), (2.0, 3.0), (2.0, 2.0), (1.0, 2.0), ], [ (1.4, 2.4), (1.4, 2.6), (1.6, 2.6), (1.6, 2.4), (1.4, 2.4), ], ], }, 'ID') expected = etree.fromstring(""" 1.0 2.0 1.0 3.0 2.0 3.0 2.0 2.0 1.0 2.0 1.4 2.4 1.4 2.6 1.6 2.6 1.6 2.4 1.4 2.4 """) assert compare_trees( expected, result ), f'{etree.tostring(expected)} != {etree.tostring(result)}' # encode Polygon with EPSG:4326 result = encode_pre_v32({ 'type': 'Polygon', 'coordinates': [ [ (1.0, 2.0), (1.0, 3.0), (2.0, 3.0), (2.0, 2.0), (1.0, 2.0), ], [ (1.4, 2.4), (1.4, 2.6), (1.6, 2.6), (1.6, 2.4), (1.4, 2.4), ], ], 'crs': { 'type': 'name', 'properties': { 'name': 'EPSG:4326' } } }, 'ID') expected = etree.fromstring(""" 2.0 1.0 3.0 1.0 3.0 2.0 2.0 2.0 2.0 1.0 2.4 1.4 2.6 1.4 2.6 1.6 2.4 1.6 2.4 1.4 """) assert compare_trees( expected, result ), f'{etree.tostring(expected)} != {etree.tostring(result)}' def test_encode_pre_v32_multi_polygon(): # encode MultiPolygon result = encode_pre_v32({ 'type': 'MultiPolygon', 'coordinates': [ [ [ (1.0, 2.0), (1.0, 3.0), (2.0, 3.0), (2.0, 2.0), (1.0, 2.0), ], [ (1.4, 2.4), (1.4, 2.6), (1.6, 2.6), (1.6, 2.4), (1.4, 2.4), ], ], [ [ (11.0, 12.0), (11.0, 13.0), (12.0, 13.0), (12.0, 12.0), (11.0, 12.0), ], [ (11.4, 12.4), (11.4, 12.6), (11.6, 12.6), (11.6, 12.4), (11.4, 12.4), ], ], ], }, 'ID') expected = etree.fromstring(""" 1.0 2.0 1.0 3.0 2.0 3.0 2.0 2.0 1.0 2.0 1.4 2.4 1.4 2.6 1.6 2.6 1.6 2.4 1.4 2.4 11.0 12.0 11.0 13.0 12.0 13.0 12.0 12.0 11.0 12.0 11.4 12.4 11.4 12.6 11.6 12.6 11.6 12.4 11.4 12.4 """) assert compare_trees( expected, result ), f'{etree.tostring(expected)} != {etree.tostring(result)}' # encode MultiPolygon with EPSG:4326 result = encode_pre_v32({ 'type': 'MultiPolygon', 'coordinates': [ [ [ (1.0, 2.0), (1.0, 3.0), (2.0, 3.0), (2.0, 2.0), (1.0, 2.0), ], [ (1.4, 2.4), (1.4, 2.6), (1.6, 2.6), (1.6, 2.4), (1.4, 2.4), ], ], [ [ (11.0, 12.0), (11.0, 13.0), (12.0, 13.0), (12.0, 12.0), (11.0, 12.0), ], [ (11.4, 12.4), (11.4, 12.6), (11.6, 12.6), (11.6, 12.4), (11.4, 12.4), ], ], ], 'crs': { 'type': 'name', 'properties': { 'name': 'EPSG:4326' } } }, 'ID') expected = etree.fromstring(""" 2.0 1.0 3.0 1.0 3.0 2.0 2.0 2.0 2.0 1.0 2.4 1.4 2.6 1.4 2.6 1.6 2.4 1.6 2.4 1.4 12.0 11.0 13.0 11.0 13.0 12.0 12.0 12.0 12.0 11.0 12.4 11.4 12.6 11.4 12.6 11.6 12.4 11.6 12.4 11.4 """) assert compare_trees( expected, result ), f'{etree.tostring(expected)} != {etree.tostring(result)}' def test_encode_pre_v32_geometry_collection(): result = encode_pre_v32({ 'type': 'GeometryCollection', 'geometries': [ { 'type': 'Point', 'coordinates': (1.0, 2.0), }, { 'type': 'Polygon', 'coordinates': [ [ (1.0, 2.0), (1.0, 3.0), (2.0, 3.0), (2.0, 2.0), (1.0, 2.0), ], [ (1.4, 2.4), (1.4, 2.6), (1.6, 2.6), (1.6, 2.4), (1.4, 2.4), ], ], }, ] }, 'ID') expected = etree.fromstring(""" 1.0 2.0 1.0 2.0 1.0 3.0 2.0 3.0 2.0 2.0 1.0 2.0 1.4 2.4 1.4 2.6 1.6 2.6 1.6 2.4 1.4 2.4 """) assert compare_trees( expected, result ), f'{etree.tostring(expected)} != {etree.tostring(result)}' result = encode_pre_v32({ 'type': 'GeometryCollection', 'geometries': [ { 'type': 'Point', 'coordinates': (1.0, 2.0), 'crs': { 'type': 'name', 'properties': { 'name': 'EPSG:4326' } } }, { 'type': 'Polygon', 'coordinates': [ [ (1.0, 2.0), (1.0, 3.0), (2.0, 3.0), (2.0, 2.0), (1.0, 2.0), ], [ (1.4, 2.4), (1.4, 2.6), (1.6, 2.6), (1.6, 2.4), (1.4, 2.4), ], ], 'crs': { 'type': 'name', 'properties': { 'name': 'EPSG:4326' } } }, ] }, 'ID') expected = etree.fromstring(""" 2.0 1.0 2.0 1.0 3.0 1.0 3.0 2.0 2.0 2.0 2.0 1.0 2.4 1.4 2.6 1.4 2.6 1.6 2.4 1.6 2.4 1.4 """) assert compare_trees( expected, result ), f'{etree.tostring(expected)} != {etree.tostring(result)}' geopython-pygml-5ce8eb0/tests/test_v32.py000066400000000000000000001511201416035544100205410ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # # Project: pygml # Authors: Fabian Schindler # # ------------------------------------------------------------------------------ # Copyright (C) 2021 EOX IT Services GmbH # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies of this Software or works derived from this Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # ------------------------------------------------------------------------------ from lxml import etree import pytest from pygml.v32 import encode_v32, parse_v32 from .util import compare_trees def test_parse_point(): # basic test result = parse_v32( etree.fromstring(""" 1.0 1.0 """) ) assert result == {'type': 'Point', 'coordinates': (1.0, 1.0)} # using gml:coordinates instead result = parse_v32( etree.fromstring(""" 1.0 1.0 """) ) assert result == {'type': 'Point', 'coordinates': (1.0, 1.0)} # axis order swapping with srsName in pos or Point result = parse_v32( etree.fromstring(""" 2.0 1.0 """) ) assert result == { 'type': 'Point', 'coordinates': (1.0, 2.0), 'crs': { 'type': 'name', 'properties': { 'name': 'EPSG:4326' } } } result = parse_v32( etree.fromstring(""" 2.0 1.0 """) ) assert result == { 'type': 'Point', 'coordinates': (1.0, 2.0), 'crs': { 'type': 'name', 'properties': { 'name': 'EPSG:4326' } } } # conflicting srsName with pytest.raises(ValueError): parse_v32( etree.fromstring(""" 2.0 1.0 """) ) def test_parse_multi_point(): # using gml:pointMember result = parse_v32( etree.fromstring(""" 1.0 1.0 2.0 2.0 """) ) assert result == { 'type': 'MultiPoint', 'coordinates': [ (1.0, 1.0), (2.0, 2.0), ] } # using gml:pointMembers result = parse_v32( etree.fromstring(""" 1.0 1.0 2.0 2.0 """) ) assert result == { 'type': 'MultiPoint', 'coordinates': [ (1.0, 1.0), (2.0, 2.0), ] } # using gml:pointMember and gml:pointMembers result = parse_v32( etree.fromstring(""" 1.0 1.0 2.0 2.0 3.0 3.0 """) ) assert result == { 'type': 'MultiPoint', 'coordinates': [ (1.0, 1.0), (2.0, 2.0), (3.0, 3.0), ] } # conflicting srsName with pytest.raises(ValueError): parse_v32( etree.fromstring(""" 1.0 1.0 2.0 2.0 """) ) def test_parse_linestring(): # from gml:posList result = parse_v32( etree.fromstring(""" 1.0 1.0 2.0 2.0 """) ) assert result == { 'type': 'LineString', 'coordinates': [ (1.0, 1.0), (2.0, 2.0), ] } # from gml:pos elements result = parse_v32( etree.fromstring(""" 1.0 1.0 2.0 2.0 """) ) assert result == { 'type': 'LineString', 'coordinates': [ (1.0, 1.0), (2.0, 2.0), ] } # from gml:coordinates result = parse_v32( etree.fromstring(""" 1.0 1.0,2.0 2.0 """) ) assert result == { 'type': 'LineString', 'coordinates': [ (1.0, 1.0), (2.0, 2.0), ] } # from gml:pos elements with srsName result = parse_v32( etree.fromstring(""" 1.0 1.0 2.0 2.0 """) ) assert result == { 'type': 'LineString', 'coordinates': [ (1.0, 1.0), (2.0, 2.0), ], 'crs': { 'type': 'name', 'properties': { 'name': 'EPSG:4326' } } } # from gml:posList element with srsName result = parse_v32( etree.fromstring(""" 1.0 1.0 2.0 2.0 """) ) assert result == { 'type': 'LineString', 'coordinates': [ (1.0, 1.0), (2.0, 2.0), ], 'crs': { 'type': 'name', 'properties': { 'name': 'EPSG:4326' } } } # from gml:coordinates element with srsName result = parse_v32( etree.fromstring(""" 1.0 1.0,2.0 2.0 """) ) assert result == { 'type': 'LineString', 'coordinates': [ (1.0, 1.0), (2.0, 2.0), ], 'crs': { 'type': 'name', 'properties': { 'name': 'EPSG:4326' } } } # srsName conflict with pytest.raises(ValueError): parse_v32( etree.fromstring(""" 1.0 1.0 2.0 2.0 """) ) def test_parse_multi_curve(): # using gml:curveMember elements result = parse_v32( etree.fromstring(""" 1.0 1.0 2.0 2.0 3.0 3.0 4.0 4.0 """) ) assert result == { 'type': 'MultiLineString', 'coordinates': [ [(1.0, 1.0), (2.0, 2.0)], [(3.0, 3.0), (4.0, 4.0)], ] } # using gml:curveMembers element result = parse_v32( etree.fromstring(""" 1.0 1.0 2.0 2.0 3.0 3.0 4.0 4.0 """) ) assert result == { 'type': 'MultiLineString', 'coordinates': [ [(1.0, 1.0), (2.0, 2.0)], [(3.0, 3.0), (4.0, 4.0)], ] } # determine srsName from MultiCurve result = parse_v32( etree.fromstring(""" 1.0 1.0 2.0 2.0 3.0 3.0 4.0 4.0 """) ) assert result == { 'type': 'MultiLineString', 'coordinates': [ [(1.0, 1.0), (2.0, 2.0)], [(3.0, 3.0), (4.0, 4.0)], ], 'crs': { 'type': 'name', 'properties': { 'name': 'EPSG:4326' } } } # determine srsName from first LineString result = parse_v32( etree.fromstring(""" 1.0 1.0 2.0 2.0 3.0 3.0 4.0 4.0 """) ) assert result == { 'type': 'MultiLineString', 'coordinates': [ [(1.0, 1.0), (2.0, 2.0)], [(3.0, 3.0), (4.0, 4.0)], ], 'crs': { 'type': 'name', 'properties': { 'name': 'EPSG:4326' } } } # srsName conflict with pytest.raises(ValueError): parse_v32( etree.fromstring(""" 1.0 1.0 2.0 2.0 3.0 3.0 4.0 4.0 """) ) with pytest.raises(ValueError): parse_v32( etree.fromstring(""" 1.0 1.0 2.0 2.0 3.0 3.0 4.0 4.0 """) ) def test_parse_polygon(): # using gml:posList result = parse_v32( etree.fromstring(""" 0.0 0.0 1.0 0.0 0.0 1.0 0.0 0.0 0.2 0.2 0.5 0.2 0.2 0.5 0.2 0.2 """) ) assert result == { 'type': 'Polygon', 'coordinates': [ [ (0.0, 0.0), (1.0, 0.0), (0.0, 1.0), (0.0, 0.0) ], [ (0.2, 0.2), (0.5, 0.2), (0.2, 0.5), (0.2, 0.2) ], ] } # using gml:posList with only exterior result = parse_v32( etree.fromstring(""" 0.0 0.0 1.0 0.0 0.0 1.0 0.0 0.0 """) ) assert result == { 'type': 'Polygon', 'coordinates': [ [ (0.0, 0.0), (1.0, 0.0), (0.0, 1.0), (0.0, 0.0) ], ] } # using gml:pos elements result = parse_v32( etree.fromstring(""" 0.0 0.0 1.0 0.0 0.0 1.0 0.0 0.0 0.2 0.2 0.5 0.2 0.2 0.5 0.2 0.2 """) ) assert result == { 'type': 'Polygon', 'coordinates': [ [ (0.0, 0.0), (1.0, 0.0), (0.0, 1.0), (0.0, 0.0) ], [ (0.2, 0.2), (0.5, 0.2), (0.2, 0.5), (0.2, 0.2) ], ] } # using gml:coordinates result = parse_v32( etree.fromstring(""" 0.0 0.0,1.0 0.0,0.0 1.0,0.0 0.0 0.2 0.2,0.5 0.2,0.2 0.5,0.2 0.2 """) ) assert result == { 'type': 'Polygon', 'coordinates': [ [ (0.0, 0.0), (1.0, 0.0), (0.0, 1.0), (0.0, 0.0) ], [ (0.2, 0.2), (0.5, 0.2), (0.2, 0.5), (0.2, 0.2) ], ] } # using gml:posList with srsName result = parse_v32( etree.fromstring(""" 0.0 0.0 1.0 0.0 0.0 1.0 0.0 0.0 0.2 0.2 0.5 0.2 0.2 0.5 0.2 0.2 """) ) assert result == { 'type': 'Polygon', 'coordinates': [ [ (0.0, 0.0), (0.0, 1.0), (1.0, 0.0), (0.0, 0.0) ], [ (0.2, 0.2), (0.2, 0.5), (0.5, 0.2), (0.2, 0.2) ], ], 'crs': { 'type': 'name', 'properties': { 'name': 'EPSG:4326' } } } def test_parse_envelope(): # using gml:lowerCorner/gml:upperCorner result = parse_v32( etree.fromstring(""" 0.0 1.0 2.0 3.0 """) ) assert result == { 'type': 'Polygon', 'coordinates': [ [ (0.0, 1.0), (0.0, 3.0), (2.0, 3.0), (2.0, 1.0), (0.0, 1.0), ], ] } # using gml:pos elements result = parse_v32( etree.fromstring(""" 0.0 1.0 2.0 3.0 """) ) assert result == { 'type': 'Polygon', 'coordinates': [ [ (0.0, 1.0), (0.0, 3.0), (2.0, 3.0), (2.0, 1.0), (0.0, 1.0), ], ] } # using gml:coordinates result = parse_v32( etree.fromstring(""" 0.0 1.0,2.0 3.0 """) ) assert result == { 'type': 'Polygon', 'coordinates': [ [ (0.0, 1.0), (0.0, 3.0), (2.0, 3.0), (2.0, 1.0), (0.0, 1.0), ], ] } # using gml:lowerCorner/gml:upperCorner with srsName result = parse_v32( etree.fromstring(""" 0.0 1.0 2.0 3.0 """) ) assert result == { 'type': 'Polygon', 'coordinates': [ [ (1.0, 0.0), (3.0, 0.0), (3.0, 2.0), (1.0, 2.0), (1.0, 0.0), ], ], 'crs': { 'type': 'name', 'properties': { 'name': 'EPSG:4326' } } } def test_parse_multi_polygon(): # using gml:surfaceMember elements result = parse_v32( etree.fromstring(""" 0.0 0.0 1.0 0.0 0.0 1.0 0.0 0.0 0.2 0.2 0.5 0.2 0.2 0.5 0.2 0.2 10.0 10.0 11.0 10.0 10.0 11.0 10.0 10.0 10.2 10.2 10.5 10.2 10.2 10.5 10.2 10.2 """) ) assert result == { 'type': 'MultiPolygon', 'coordinates': [ [ [ (0.0, 0.0), (1.0, 0.0), (0.0, 1.0), (0.0, 0.0) ], [ (0.2, 0.2), (0.5, 0.2), (0.2, 0.5), (0.2, 0.2) ], ], [ [ (10.0, 10.0), (11.0, 10.0), (10.0, 11.0), (10.0, 10.0) ], [ (10.2, 10.2), (10.5, 10.2), (10.2, 10.5), (10.2, 10.2) ], ] ] } # using gml:surfaceMember elements with no interiors result = parse_v32( etree.fromstring(""" 0.0 0.0 1.0 0.0 0.0 1.0 0.0 0.0 10.0 10.0 11.0 10.0 10.0 11.0 10.0 10.0 """) ) assert result == { 'type': 'MultiPolygon', 'coordinates': [ [ [ (0.0, 0.0), (1.0, 0.0), (0.0, 1.0), (0.0, 0.0) ], ], [ [ (10.0, 10.0), (11.0, 10.0), (10.0, 11.0), (10.0, 10.0) ], ] ] } # using gml:surfaceMembers result = parse_v32( etree.fromstring(""" 0.0 0.0 1.0 0.0 0.0 1.0 0.0 0.0 0.2 0.2 0.5 0.2 0.2 0.5 0.2 0.2 10.0 10.0 11.0 10.0 10.0 11.0 10.0 10.0 10.2 10.2 10.5 10.2 10.2 10.5 10.2 10.2 """) ) assert result == { 'type': 'MultiPolygon', 'coordinates': [ [ [ (0.0, 0.0), (1.0, 0.0), (0.0, 1.0), (0.0, 0.0) ], [ (0.2, 0.2), (0.5, 0.2), (0.2, 0.5), (0.2, 0.2) ], ], [ [ (10.0, 10.0), (11.0, 10.0), (10.0, 11.0), (10.0, 10.0) ], [ (10.2, 10.2), (10.5, 10.2), (10.2, 10.5), (10.2, 10.2) ], ] ] } # using gml:surfaceMembers with srsName result = parse_v32( etree.fromstring(""" 0.0 0.0 1.0 0.0 0.0 1.0 0.0 0.0 0.2 0.2 0.5 0.2 0.2 0.5 0.2 0.2 10.0 10.0 11.0 10.0 10.0 11.0 10.0 10.0 10.2 10.2 10.5 10.2 10.2 10.5 10.2 10.2 """) ) assert result == { 'type': 'MultiPolygon', 'coordinates': [ [ [ (0.0, 0.0), (0.0, 1.0), (1.0, 0.0), (0.0, 0.0) ], [ (0.2, 0.2), (0.2, 0.5), (0.5, 0.2), (0.2, 0.2) ], ], [ [ (10.0, 10.0), (10.0, 11.0), (11.0, 10.0), (10.0, 10.0) ], [ (10.2, 10.2), (10.2, 10.5), (10.5, 10.2), (10.2, 10.2) ], ] ], 'crs': { 'type': 'name', 'properties': { 'name': 'EPSG:4326' } } } with pytest.raises(ValueError): parse_v32( etree.fromstring(""" 0.0 0.0 1.0 0.0 0.0 1.0 0.0 0.0 0.2 0.2 0.5 0.2 0.2 0.5 0.2 0.2 10.0 10.0 11.0 10.0 10.0 11.0 10.0 10.0 10.2 10.2 10.5 10.2 10.2 10.5 10.2 10.2 """) ) def test_parse_multi_geometry(): # using geometryMembers result = parse_v32( etree.fromstring(""" 1.0 1.0 1.0 1.0 1.0 1.0 """) ) assert result == { 'type': 'GeometryCollection', 'geometries': [ { 'type': 'Point', 'coordinates': (1.0, 1.0) }, { 'type': 'Polygon', 'coordinates': [ [(1.0, 1.0)], [(1.0, 1.0)], ] }, ] } # using geometryMember result = parse_v32( etree.fromstring(""" 1.0 1.0 1.0 1.0 1.0 1.0 """) ) assert result == { 'type': 'GeometryCollection', 'geometries': [ { 'type': 'Point', 'coordinates': (1.0, 1.0) }, { 'type': 'Polygon', 'coordinates': [ [(1.0, 1.0)], [(1.0, 1.0)], ] }, ] } # allow varying srsNames result = parse_v32( etree.fromstring(""" 1.0 1.0 1.0 1.0 1.0 1.0 """) ) assert result == { 'type': 'GeometryCollection', 'geometries': [ { 'type': 'Point', 'coordinates': (1.0, 1.0), 'crs': { 'type': 'name', 'properties': { 'name': 'EPSG:4326' } } }, { 'type': 'Polygon', 'coordinates': [ [(1.0, 1.0)], [(1.0, 1.0)], ], 'crs': { 'type': 'name', 'properties': { 'name': 'EPSG:3857' } } }, ] } def test_encode_v32_point(): # encode Point result = encode_v32({'type': 'Point', 'coordinates': (1.0, 2.0)}, 'ID') expected = etree.fromstring(""" 1.0 2.0 """) assert compare_trees( expected, result ), f'{etree.tostring(expected)} != {etree.tostring(result)}' # encode Point with EPSG:4326 result = encode_v32({ 'type': 'Point', 'coordinates': (1.0, 2.0), 'crs': { 'type': 'name', 'properties': { 'name': 'EPSG:4326' } } }, 'ID') expected = etree.fromstring(""" 2.0 1.0 """) assert compare_trees( expected, result ), f'{etree.tostring(expected)} != {etree.tostring(result)}' def test_encode_v32_multi_point(): # encode MultiPoint result = encode_v32({ 'type': 'MultiPoint', 'coordinates': [ (1.0, 2.0), (3.0, 4.0), ] }, 'ID') expected = etree.fromstring(""" 1.0 2.0 3.0 4.0 """) assert compare_trees( expected, result ), f'{etree.tostring(expected)} != {etree.tostring(result)}' # encode MultiPoint with EPSG:4326 result = encode_v32({ 'type': 'MultiPoint', 'coordinates': [ (1.0, 2.0), (3.0, 4.0), ], 'crs': { 'type': 'name', 'properties': { 'name': 'EPSG:4326' } } }, 'ID') expected = etree.fromstring(""" 2.0 1.0 4.0 3.0 """) assert compare_trees( expected, result ), f'{etree.tostring(expected)} != {etree.tostring(result)}' def test_encode_v32_linestring(): # encode LineString result = encode_v32({ 'type': 'LineString', 'coordinates': [ (1.0, 2.0), (3.0, 4.0), ], }, 'ID') expected = etree.fromstring(""" 1.0 2.0 3.0 4.0 """) assert compare_trees( expected, result ), f'{etree.tostring(expected)} != {etree.tostring(result)}' # encode LineString with EPSG:4326 result = encode_v32({ 'type': 'LineString', 'coordinates': [ (1.0, 2.0), (3.0, 4.0), ], 'crs': { 'type': 'name', 'properties': { 'name': 'EPSG:4326' } } }, 'ID') expected = etree.fromstring(""" 2.0 1.0 4.0 3.0 """) assert compare_trees( expected, result ), f'{etree.tostring(expected)} != {etree.tostring(result)}' def test_encode_v32_polygon(): # encode Polygon result = encode_v32({ 'type': 'Polygon', 'coordinates': [ [ (1.0, 2.0), (1.0, 3.0), (2.0, 3.0), (2.0, 2.0), (1.0, 2.0), ], [ (1.4, 2.4), (1.4, 2.6), (1.6, 2.6), (1.6, 2.4), (1.4, 2.4), ], ], }, 'ID') expected = etree.fromstring(""" 1.0 2.0 1.0 3.0 2.0 3.0 2.0 2.0 1.0 2.0 1.4 2.4 1.4 2.6 1.6 2.6 1.6 2.4 1.4 2.4 """) assert compare_trees( expected, result ), f'{etree.tostring(expected)} != {etree.tostring(result)}' # encode Polygon with EPSG:4326 result = encode_v32({ 'type': 'Polygon', 'coordinates': [ [ (1.0, 2.0), (1.0, 3.0), (2.0, 3.0), (2.0, 2.0), (1.0, 2.0), ], [ (1.4, 2.4), (1.4, 2.6), (1.6, 2.6), (1.6, 2.4), (1.4, 2.4), ], ], 'crs': { 'type': 'name', 'properties': { 'name': 'EPSG:4326' } } }, 'ID') expected = etree.fromstring(""" 2.0 1.0 3.0 1.0 3.0 2.0 2.0 2.0 2.0 1.0 2.4 1.4 2.6 1.4 2.6 1.6 2.4 1.6 2.4 1.4 """) assert compare_trees( expected, result ), f'{etree.tostring(expected)} != {etree.tostring(result)}' def test_encode_v32_multi_polygon(): # encode MultiPolygon result = encode_v32({ 'type': 'MultiPolygon', 'coordinates': [ [ [ (1.0, 2.0), (1.0, 3.0), (2.0, 3.0), (2.0, 2.0), (1.0, 2.0), ], [ (1.4, 2.4), (1.4, 2.6), (1.6, 2.6), (1.6, 2.4), (1.4, 2.4), ], ], [ [ (11.0, 12.0), (11.0, 13.0), (12.0, 13.0), (12.0, 12.0), (11.0, 12.0), ], [ (11.4, 12.4), (11.4, 12.6), (11.6, 12.6), (11.6, 12.4), (11.4, 12.4), ], ], ], }, 'ID') expected = etree.fromstring(""" 1.0 2.0 1.0 3.0 2.0 3.0 2.0 2.0 1.0 2.0 1.4 2.4 1.4 2.6 1.6 2.6 1.6 2.4 1.4 2.4 11.0 12.0 11.0 13.0 12.0 13.0 12.0 12.0 11.0 12.0 11.4 12.4 11.4 12.6 11.6 12.6 11.6 12.4 11.4 12.4 """) assert compare_trees( expected, result ), f'{etree.tostring(expected)} != {etree.tostring(result)}' # encode MultiPolygon with EPSG:4326 result = encode_v32({ 'type': 'MultiPolygon', 'coordinates': [ [ [ (1.0, 2.0), (1.0, 3.0), (2.0, 3.0), (2.0, 2.0), (1.0, 2.0), ], [ (1.4, 2.4), (1.4, 2.6), (1.6, 2.6), (1.6, 2.4), (1.4, 2.4), ], ], [ [ (11.0, 12.0), (11.0, 13.0), (12.0, 13.0), (12.0, 12.0), (11.0, 12.0), ], [ (11.4, 12.4), (11.4, 12.6), (11.6, 12.6), (11.6, 12.4), (11.4, 12.4), ], ], ], 'crs': { 'type': 'name', 'properties': { 'name': 'EPSG:4326' } } }, 'ID') expected = etree.fromstring(""" 2.0 1.0 3.0 1.0 3.0 2.0 2.0 2.0 2.0 1.0 2.4 1.4 2.6 1.4 2.6 1.6 2.4 1.6 2.4 1.4 12.0 11.0 13.0 11.0 13.0 12.0 12.0 12.0 12.0 11.0 12.4 11.4 12.6 11.4 12.6 11.6 12.4 11.6 12.4 11.4 """) assert compare_trees( expected, result ), f'{etree.tostring(expected)} != {etree.tostring(result)}' def test_encode_v32_geometry_collection(): result = encode_v32({ 'type': 'GeometryCollection', 'geometries': [ { 'type': 'Point', 'coordinates': (1.0, 2.0), }, { 'type': 'Polygon', 'coordinates': [ [ (1.0, 2.0), (1.0, 3.0), (2.0, 3.0), (2.0, 2.0), (1.0, 2.0), ], [ (1.4, 2.4), (1.4, 2.6), (1.6, 2.6), (1.6, 2.4), (1.4, 2.4), ], ], }, ] }, 'ID') expected = etree.fromstring(""" 1.0 2.0 1.0 2.0 1.0 3.0 2.0 3.0 2.0 2.0 1.0 2.0 1.4 2.4 1.4 2.6 1.6 2.6 1.6 2.4 1.4 2.4 """) assert compare_trees( expected, result ), f'{etree.tostring(expected)} != {etree.tostring(result)}' result = encode_v32({ 'type': 'GeometryCollection', 'geometries': [ { 'type': 'Point', 'coordinates': (1.0, 2.0), 'crs': { 'type': 'name', 'properties': { 'name': 'EPSG:4326' } } }, { 'type': 'Polygon', 'coordinates': [ [ (1.0, 2.0), (1.0, 3.0), (2.0, 3.0), (2.0, 2.0), (1.0, 2.0), ], [ (1.4, 2.4), (1.4, 2.6), (1.6, 2.6), (1.6, 2.4), (1.4, 2.4), ], ], 'crs': { 'type': 'name', 'properties': { 'name': 'EPSG:4326' } } }, ] }, 'ID') expected = etree.fromstring(""" 2.0 1.0 2.0 1.0 3.0 1.0 3.0 2.0 2.0 2.0 2.0 1.0 2.4 1.4 2.6 1.4 2.6 1.6 2.4 1.6 2.4 1.4 """) assert compare_trees( expected, result ), f'{etree.tostring(expected)} != {etree.tostring(result)}' geopython-pygml-5ce8eb0/tests/test_v33.py000066400000000000000000000304361416035544100205500ustar00rootroot00000000000000# ------------------------------------------------------------------------------ # # Project: pygml # Authors: Fabian Schindler # # ------------------------------------------------------------------------------ # Copyright (C) 2021 EOX IT Services GmbH # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies of this Software or works derived from this Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # ------------------------------------------------------------------------------ from lxml import etree # import pytest from pygml.v33 import encode_v33_ce, parse_v33_ce from .util import compare_trees def test_parse_simple_triangle(): # using gml:pos elements result = parse_v33_ce( etree.fromstring(""" 1.0 1.0 1.0 2.0 2.0 1.0 """) ) assert result == { 'type': 'Polygon', 'coordinates': [ [ (1.0, 1.0), (1.0, 2.0), (2.0, 1.0), (1.0, 1.0), ] ] } # using gml:posList element result = parse_v33_ce( etree.fromstring(""" 1.0 1.0 1.0 2.0 2.0 1.0 """) ) assert result == { 'type': 'Polygon', 'coordinates': [ [ (1.0, 1.0), (1.0, 2.0), (2.0, 1.0), (1.0, 1.0), ] ] } # swapped coordinates with EPSG:4326 result = parse_v33_ce( etree.fromstring(""" 0.0 1.0 0.0 2.0 1.0 1.0 """) ) assert result == { 'type': 'Polygon', 'coordinates': [ [ (1.0, 0.0), (2.0, 0.0), (1.0, 1.0), (1.0, 0.0), ] ], 'crs': { 'type': 'name', 'properties': { 'name': 'EPSG:4326' } } } def test_parse_simple_rectangle(): # using gml:pos elements result = parse_v33_ce( etree.fromstring(""" 1.0 1.0 1.0 2.0 2.0 2.0 2.0 1.0 """) ) assert result == { 'type': 'Polygon', 'coordinates': [ [ (1.0, 1.0), (1.0, 2.0), (2.0, 2.0), (2.0, 1.0), (1.0, 1.0), ] ] } # using gml:posList element result = parse_v33_ce( etree.fromstring(""" 1.0 1.0 1.0 2.0 2.0 2.0 2.0 1.0 """) ) assert result == { 'type': 'Polygon', 'coordinates': [ [ (1.0, 1.0), (1.0, 2.0), (2.0, 2.0), (2.0, 1.0), (1.0, 1.0), ] ] } # swapped coordinates with EPSG:4326 result = parse_v33_ce( etree.fromstring(""" 0.0 1.0 0.0 2.0 1.0 2.0 1.0 1.0 """) ) assert result == { 'type': 'Polygon', 'coordinates': [ [ (1.0, 0.0), (2.0, 0.0), (2.0, 1.0), (1.0, 1.0), (1.0, 0.0), ] ], 'crs': { 'type': 'name', 'properties': { 'name': 'EPSG:4326' } } } def test_parse_simple_polygon(): # using gml:pos elements result = parse_v33_ce( etree.fromstring(""" 1.0 1.0 1.0 2.0 2.0 2.0 2.0 1.0 """) ) assert result == { 'type': 'Polygon', 'coordinates': [ [ (1.0, 1.0), (1.0, 2.0), (2.0, 2.0), (2.0, 1.0), (1.0, 1.0), ] ] } # using gml:posList element result = parse_v33_ce( etree.fromstring(""" 1.0 1.0 1.0 2.0 2.0 2.0 2.0 1.0 """) ) assert result == { 'type': 'Polygon', 'coordinates': [ [ (1.0, 1.0), (1.0, 2.0), (2.0, 2.0), (2.0, 1.0), (1.0, 1.0), ] ] } # swapped coordinates with EPSG:4326 result = parse_v33_ce( etree.fromstring(""" 0.0 1.0 0.0 2.0 1.0 2.0 1.0 1.0 """) ) assert result == { 'type': 'Polygon', 'coordinates': [ [ (1.0, 0.0), (2.0, 0.0), (2.0, 1.0), (1.0, 1.0), (1.0, 0.0), ] ], 'crs': { 'type': 'name', 'properties': { 'name': 'EPSG:4326' } } } def test_encode_v32_polygon(): # encode Polygon as SimpleTriangle result = encode_v33_ce({ 'type': 'Polygon', 'coordinates': [ [ (1.0, 2.0), (1.0, 3.0), (2.0, 2.0), (1.0, 2.0), ], ], }, 'ID') expected = etree.fromstring(""" 1.0 2.0 1.0 3.0 2.0 2.0 """) assert compare_trees( expected, result ), f'{etree.tostring(expected)} != {etree.tostring(result)}' # encode Polygon as SimpleRectangle result = encode_v33_ce({ 'type': 'Polygon', 'coordinates': [ [ (1.0, 2.0), (1.0, 3.0), (2.0, 3.0), (2.0, 2.0), (1.0, 2.0), ], ], }, 'ID') expected = etree.fromstring(""" 1.0 2.0 1.0 3.0 2.0 3.0 2.0 2.0 """) assert compare_trees( expected, result ), f'{etree.tostring(expected)} != {etree.tostring(result)}' # encode Polygon as SimplePolygon when more than 4 distinct # coordinates and no interiors (with EPSG:4326) result = encode_v33_ce({ 'type': 'Polygon', 'coordinates': [ [ (1.0, 2.0), (1.0, 3.0), (1.5, 3.5), (2.0, 3.0), (2.0, 2.0), (1.0, 2.0), ], ], 'crs': { 'type': 'name', 'properties': { 'name': 'EPSG:4326' } } }, 'ID') expected = etree.fromstring(""" 2.0 1.0 3.0 1.0 3.5 1.5 3.0 2.0 2.0 2.0 """) assert compare_trees( expected, result ), f'{etree.tostring(expected)} != {etree.tostring(result)}' # encode Polygon (fallback to gml 3.2 Polygon when interiors) result = encode_v33_ce({ 'type': 'Polygon', 'coordinates': [ [ (1.0, 2.0), (1.0, 3.0), (2.0, 3.0), (2.0, 2.0), (1.0, 2.0), ], [ (1.4, 2.4), (1.4, 2.6), (1.6, 2.6), (1.6, 2.4), (1.4, 2.4), ], ], }, 'ID') expected = etree.fromstring(""" 1.0 2.0 1.0 3.0 2.0 3.0 2.0 2.0 1.0 2.0 1.4 2.4 1.4 2.6 1.6 2.6 1.6 2.4 1.4 2.4 """) assert compare_trees( expected, result ), f'{etree.tostring(expected)} != {etree.tostring(result)}' geopython-pygml-5ce8eb0/tests/util.py000066400000000000000000000012471416035544100200510ustar00rootroot00000000000000def compare_trees(e1, e2): # from https://stackoverflow.com/a/24349916/746961 if e1.tag != e2.tag: raise AssertionError(f'Tag: {e1.tag} != {e2.tag}') if (e1.text or '').strip() != (e2.text or '').strip(): raise AssertionError(f'Text: {e1.text} != {e2.text}') if (e1.tail or '').strip() != (e2.tail or '').strip(): raise AssertionError(f'Tail: {e1.tail} != {e2.tail}') if e1.attrib != e2.attrib: raise AssertionError(f'Attributes: {e1.attrib} != {e2.attrib}') if len(e1) != len(e2): raise AssertionError(f'Child nodes: {len(e1.tag)} != {len(e2.tag)}') return all(compare_trees(c1, c2) for c1, c2 in zip(e1, e2))