├── .flake8 ├── .github └── workflows │ ├── lint.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── CHANGELOG.rst ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.rst ├── LICENSE.rst ├── MANIFEST.in ├── README.rst ├── docs └── source │ ├── conf.py │ ├── index.rst │ └── theme.toml ├── geojson ├── __init__.py ├── _version.py ├── base.py ├── codec.py ├── examples.py ├── factory.py ├── feature.py ├── geometry.py ├── mapping.py └── utils.py ├── setup.py ├── tests ├── __init__.py ├── data.geojson ├── test_base.py ├── test_constructor.py ├── test_coords.py ├── test_features.py ├── test_geo_interface.py ├── test_null_geometries.py ├── test_strict_json.py ├── test_utils.py └── test_validation.py └── tox.ini /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 100 3 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: [push, pull_request, workflow_dispatch] 4 | 5 | permissions: 6 | contents: read 7 | 8 | jobs: 9 | lint: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | 15 | - uses: actions/setup-python@v4 16 | with: 17 | python-version: "3.x" 18 | 19 | - uses: pre-commit/action@v3.0.0 20 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | build: 10 | if: github.repository_owner == 'jazzband' 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Set up Python 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: "3.x" 22 | cache: pip 23 | cache-dependency-path: setup.py 24 | 25 | - name: Install dependencies 26 | run: | 27 | python -m pip install -U pip 28 | python -m pip install -U setuptools twine wheel 29 | 30 | - name: Build package 31 | run: | 32 | python setup.py --version 33 | python setup.py sdist --format=gztar bdist_wheel 34 | twine check dist/* 35 | 36 | - name: Upload packages to Jazzband 37 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 38 | uses: pypa/gh-action-pypi-publish@release/v1 39 | with: 40 | user: jazzband 41 | password: ${{ secrets.JAZZBAND_RELEASE_KEY }} 42 | repository_url: https://jazzband.co/projects/geojson/upload 43 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request, workflow_dispatch] 4 | 5 | env: 6 | FORCE_COLOR: 1 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | python-version: ['pypy3.9', '3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Set up Python ${{ matrix.python-version }} 20 | uses: actions/setup-python@v4 21 | with: 22 | python-version: ${{ matrix.python-version }} 23 | allow-prereleases: true 24 | cache: pip 25 | cache-dependency-path: tox.ini 26 | 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | python -m pip install --upgrade tox 31 | 32 | - name: Tests 33 | run: | 34 | tox -e py 35 | 36 | - name: Upload coverage 37 | uses: codecov/codecov-action@v3 38 | with: 39 | name: Python ${{ matrix.python-version }} 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.py[cod] 3 | build/ 4 | dist/ 5 | sdist/ 6 | *.egg-info/ 7 | *.egg 8 | .tox/ 9 | .coverage 10 | coverage.xml 11 | .idea 12 | .venv* 13 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: check-case-conflict 6 | - id: check-merge-conflict 7 | - id: check-yaml 8 | - id: end-of-file-fixer 9 | - id: trailing-whitespace 10 | 11 | - repo: https://github.com/tox-dev/tox-ini-fmt 12 | rev: 1.4.1 13 | hooks: 14 | - id: tox-ini-fmt 15 | 16 | - repo: https://github.com/PyCQA/flake8 17 | rev: 7.1.1 18 | hooks: 19 | - id: flake8 20 | 21 | ci: 22 | autoupdate_schedule: quarterly 23 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | version: 2 6 | 7 | build: 8 | os: ubuntu-22.04 9 | tools: 10 | python: "3.10" 11 | 12 | sphinx: 13 | configuration: docs/source/conf.py 14 | 15 | python: 16 | install: 17 | - method: pip 18 | path: . 19 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Changes 2 | ======= 3 | 4 | 3.2.0 5 | ---------- 6 | 7 | - Add support for Python 3.13 8 | 9 | - https://github.com/jazzband/geojson/pull/228 10 | 11 | - Code modernization 12 | 13 | - https://github.com/jazzband/geojson/pull/218 14 | - https://github.com/jazzband/geojson/pull/229 15 | 16 | - RtD bugfix 17 | 18 | - https://github.com/jazzband/geojson/pull/227 19 | 20 | 3.1.0 21 | ---------- 22 | 23 | - Add support for Python 3.12 24 | 25 | - https://github.com/jazzband/geojson/pull/222 26 | - https://github.com/jazzband/geojson/pull/211 27 | 28 | - CI improvements 29 | 30 | - https://github.com/jazzband/geojson/pull/217 31 | - https://github.com/jazzband/geojson/pull/212 32 | - https://github.com/jazzband/geojson/pull/207 33 | 34 | - Unit test improvements 35 | 36 | - https://github.com/jazzband/geojson/pull/215 37 | - https://github.com/jazzband/geojson/pull/210 38 | - https://github.com/jazzband/geojson/pull/209 39 | 40 | 3.0.1 (2023-02-15) 41 | ------------------ 42 | 43 | - Add Support for Python 3.11.x minor revisions 44 | 45 | - https://github.com/jazzband/geojson/pull/198 46 | 47 | 3.0.0 (2023-01-26) 48 | ------------------ 49 | 50 | - Support for Python versions 3.7-3.11 (Python 2 no longer supported) 51 | 52 | - Primary development branch renamed from `master` to `main` 53 | 54 | - Handle all real numbers as coordinates 55 | 56 | - https://github.com/jazzband/geojson/pull/188 57 | 58 | - Default precision improvements 59 | 60 | - https://github.com/jazzband/geojson/pull/177 61 | 62 | - CI improvements 63 | 64 | - https://github.com/jazzband/geojson/pull/172 65 | - https://github.com/jazzband/geojson/pull/155 66 | 67 | - utf-8 support added to `geojson.dumps()` 68 | 69 | - https://github.com/jazzband/geojson/pull/165 70 | 71 | - Polygons now constrained to bounding box 72 | 73 | - https://github.com/jazzband/geojson/pull/147 74 | 75 | - Better GeometryCollection handling in `util.coords()` 76 | 77 | - https://github.com/jazzband/geojson/pull/146 78 | 79 | - Improved point validation 80 | 81 | - https://github.com/jazzband/geojson/pull/144 82 | 83 | 2.5.0 (2019-07-18) 84 | ------------------ 85 | 86 | - Add "precision" parameter to GeoJSON Object classes with default precision of 6 (0.1m) 87 | 88 | - https://github.com/jazzband/geojson/pull/131 89 | 90 | - Fix bug where `map_geometries()` util was not preserving Feature IDs 91 | 92 | - https://github.com/jazzband/geojson/pull/128 93 | - https://github.com/jazzband/geojson/pull/130 94 | 95 | - Remove `crs` module and features to conform to official WGS84-only GeoJSON spec 96 | 97 | - https://github.com/jazzband/geojson/pull/124 98 | 99 | - Set up semi-automatic PyPi releases via Travis/Jazzband 100 | 101 | - https://github.com/jazzband/geojson/pull/123 102 | 103 | 2.4.2 (2019-03-12) 104 | ------------------ 105 | 106 | - Tie Travis CI to jazzband instance 107 | - Remove EOL 3.3 and 3.4 version support 108 | 109 | - https://github.com/jazzband/geojson/pull/120 110 | 111 | 2.4.1 (2018-10-17) 112 | ------------------ 113 | 114 | - Allow ``FeatureCollections`` to be passed to ``coords`` 115 | 116 | - https://github.com/jazzband/geojson/pull/117 117 | 118 | 2.4.0 (2018-05-21) 119 | ------------------ 120 | 121 | - Additional functional maps for GeoJSON entities 122 | 123 | - https://github.com/jazzband/geojson/pull/112 124 | 125 | 2.3.0 (2017-09-18) 126 | ------------------ 127 | 128 | - Add ``__getitem__`` methods to sequence-like objects 129 | 130 | - https://github.com/jazzband/geojson/pull/103 131 | 132 | 133 | 2.2.0 (2017-09-17) 134 | ------------------ 135 | 136 | - Allow constructing geojson objects from geojson objects 137 | 138 | - https://github.com/jazzband/geojson/pull/104 139 | 140 | 2.1.0 (2017-08-29) 141 | ------------------ 142 | 143 | - Implement validation for GeometryCollection 144 | 145 | - https://github.com/jazzband/geojson/pull/102 146 | 147 | 2.0.0 (2017-07-28) 148 | ------------------ 149 | 150 | - Rewrite of validation mechanism (breaking change). 151 | 152 | - https://github.com/jazzband/geojson/pull/98 153 | 154 | 1.3.5 (2017-04-24) 155 | ------------------ 156 | 157 | - Changed the validator to allow elevation 158 | 159 | - https://github.com/jazzband/geojson/pull/92 160 | 161 | 1.3.4 (2017-02-11) 162 | ------------------ 163 | 164 | - Remove runtime dependency on setuptools 165 | 166 | - https://github.com/jazzband/geojson/pull/90 167 | 168 | 1.3.3 (2016-07-21) 169 | ------------------ 170 | 171 | - Add validate parameter to GeoJSON constructors 172 | 173 | - https://github.com/jazzband/geojson/pull/78 174 | 175 | 1.3.2 (2016-01-28) 176 | ------------------ 177 | 178 | - Add __version__ and __version_info__ attributes 179 | 180 | - https://github.com/jazzband/geojson/pull/74 181 | 182 | 1.3.1 (2015-10-12) 183 | ------------------ 184 | 185 | - Fix validation bug for MultiPolygons 186 | 187 | - https://github.com/jazzband/geojson/pull/63 188 | 189 | 1.3.0 (2015-08-11) 190 | ------------------ 191 | 192 | - Add utility to generate geometries with random data 193 | 194 | - https://github.com/jazzband/geojson/pull/60 195 | 196 | 1.2.2 (2015-07-13) 197 | ------------------ 198 | 199 | - Fix tests by including test file into build 200 | 201 | - https://github.com/jazzband/geojson/issues/61 202 | 203 | - Build universal wheels 204 | 205 | - https://packaging.python.org/en/latest/distributing.html#universal-wheels 206 | 207 | 1.2.1 (2015-06-25) 208 | ------------------ 209 | 210 | - Encode long types correctly with Python 2.x 211 | 212 | - https://github.com/jazzband/geojson/pull/57 213 | 214 | 1.2.0 (2015-06-19) 215 | ------------------ 216 | 217 | - Utility function to validate GeoJSON objects 218 | 219 | - https://github.com/jazzband/geojson/pull/56 220 | 221 | 1.1.0 (2015-06-08) 222 | ------------------ 223 | 224 | - Stop outputting invalid GeoJSON value id=null on Features 225 | 226 | - https://github.com/jazzband/geojson/pull/53 227 | 228 | 1.0.9 (2014-10-05) 229 | ------------------ 230 | 231 | - Fix bug where unicode/non-string properties with a 'type' key cause a crash 232 | 233 | 1.0.8 (2014-09-30) 234 | ------------------ 235 | 236 | - Fix bug where unicode keys don't get decoded properly 237 | - Add coords and map_coords utilities 238 | 239 | 1.0.7 (2014-04-19) 240 | ------------------ 241 | 242 | - Compatibility with Python 3.4 243 | - Remove nose dependency 244 | - Convert doctests to unittests 245 | - Run tests using runtests.sh 246 | 247 | 1.0.6 (2014-01-18) 248 | ------------------ 249 | 250 | - Update README.rst documentation (fix errors, add examples) 251 | - Allow simplejson to be used again 252 | 253 | 1.0.5 (2013-11-16) 254 | ------------------ 255 | 256 | - Remove warning about RSTs in test/ upon install 257 | 258 | 1.0.4 (2013-11-16) 259 | ------------------ 260 | 261 | - Flake8 everything 262 | - Transition all documentation to reStructuredText 263 | - Start using Travis CI 264 | - Support Python 3 265 | - Fix broken testcase when run using Python 2.6 266 | 267 | 1.0.3 (2009-11-25) 268 | ------------------ 269 | 270 | - Fixed #186 271 | - Internal code simplification 272 | 273 | 1.0.2 (2009-11-24) 274 | ------------------ 275 | 276 | - Use nose test framework instead of rolling our own. 277 | 278 | 1.0.1 (2008-12-19) 279 | ------------------ 280 | 281 | - Handle features with null geometries (#174). 282 | 283 | 1.0 (2008-08-01) 284 | ---------------- 285 | 286 | - Final 1.0 release. 287 | - Rename PyGFPEncoder to GeoJSONEncoder and expose it from the geojson module. 288 | 289 | 1.0rc1 (2008-07-11) 290 | ------------------- 291 | 292 | - Release candidate. 293 | 294 | 1.0b1 (2008-07-02) 295 | ------------------ 296 | 297 | - Rename encoding module to codec. 298 | 299 | 1.0a4 (2008-04-27) 300 | ------------------ 301 | 302 | - Get in step with GeoJSON draft version 6. 303 | - Made all code work with Python 2.4.3, 2.5.1, will test with all variations. 304 | (see tests/rundoctests.dist) 305 | - Made tests use ELLIPSIS to avoid output transmogification due to floating 306 | point representation. 307 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | As contributors and maintainers of the Jazzband projects, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating documentation, 6 | submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in the Jazzband a harassment-free experience 9 | for everyone, regardless of the level of experience, gender, gender identity and 10 | expression, sexual orientation, disability, personal appearance, body size, race, 11 | ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | - The use of sexualized language or imagery 16 | - Personal attacks 17 | - Trolling or insulting/derogatory comments 18 | - Public or private harassment 19 | - Publishing other's private information, such as physical or electronic addresses, 20 | without explicit permission 21 | - Other unethical or unprofessional conduct 22 | 23 | The Jazzband roadies have the right and responsibility to remove, edit, or reject 24 | comments, commits, code, wiki edits, issues, and other contributions that are not 25 | aligned to this Code of Conduct, or to ban temporarily or permanently any contributor 26 | for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 27 | 28 | By adopting this Code of Conduct, the roadies commit themselves to fairly and 29 | consistently applying these principles to every aspect of managing the jazzband 30 | projects. Roadies who do not follow or enforce the Code of Conduct may be permanently 31 | removed from the Jazzband roadies. 32 | 33 | This code of conduct applies both within project spaces and in public spaces when an 34 | individual is representing the project or its community. 35 | 36 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by 37 | contacting the roadies at `roadies@jazzband.co`. All complaints will be reviewed and 38 | investigated and will result in a response that is deemed necessary and appropriate to 39 | the circumstances. Roadies are obligated to maintain confidentiality with regard to the 40 | reporter of an incident. 41 | 42 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 43 | 1.3.0, available at [https://contributor-covenant.org/version/1/3/0/][version] 44 | 45 | [homepage]: https://contributor-covenant.org 46 | [version]: https://contributor-covenant.org/version/1/3/0/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://jazzband.co/static/img/jazzband.svg 2 | :target: https://jazzband.co/ 3 | :alt: Jazzband 4 | 5 | This is a `Jazzband `_ project. By contributing you agree to abide by the `Contributor Code of Conduct `_ and follow the `guidelines `_. 6 | -------------------------------------------------------------------------------- /LICENSE.rst: -------------------------------------------------------------------------------- 1 | Copyright © 2007-2019, contributors of geojson 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | 7 | - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | - Neither the name of the geojson nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS OF GEOJSON BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.rst 2 | recursive-include tests *.txt *.py *.geojson 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | geojson 2 | ============== 3 | 4 | .. image:: https://github.com/jazzband/geojson/actions/workflows/test.yml/badge.svg 5 | :target: https://github.com/jazzband/geojson/actions/workflows/test.yml 6 | :alt: GitHub Actions 7 | .. image:: https://img.shields.io/codecov/c/github/jazzband/geojson.svg 8 | :target: https://codecov.io/github/jazzband/geojson?branch=main 9 | :alt: Codecov 10 | .. image:: https://jazzband.co/static/img/badge.svg 11 | :target: https://jazzband.co/ 12 | :alt: Jazzband 13 | .. image:: https://img.shields.io/pypi/dm/geojson.svg 14 | :target: https://pypi.org/project/geojson/ 15 | :alt: PyPI 16 | 17 | This Python library contains: 18 | 19 | - Functions for encoding and decoding GeoJSON_ formatted data 20 | - Classes for all GeoJSON Objects 21 | - An implementation of the Python `__geo_interface__ Specification`_ 22 | 23 | **Table of Contents** 24 | 25 | .. contents:: 26 | :backlinks: none 27 | :local: 28 | 29 | Installation 30 | ------------ 31 | 32 | geojson is compatible with Python 3.7 - 3.13. The recommended way to install is via pip_: 33 | 34 | .. code:: 35 | 36 | pip install geojson 37 | 38 | .. _PyPi as 'geojson': https://pypi.python.org/pypi/geojson/ 39 | .. _pip: https://www.pip-installer.org 40 | 41 | GeoJSON Objects 42 | --------------- 43 | 44 | This library implements all the `GeoJSON Objects`_ described in `The GeoJSON Format Specification`_. 45 | 46 | .. _GeoJSON Objects: https://tools.ietf.org/html/rfc7946#section-3 47 | 48 | All object keys can also be used as attributes. 49 | 50 | The objects contained in GeometryCollection and FeatureCollection can be indexed directly. 51 | 52 | Point 53 | ~~~~~ 54 | 55 | .. code:: python 56 | 57 | >>> from geojson import Point 58 | 59 | >>> Point((-115.81, 37.24)) # doctest: +ELLIPSIS 60 | {"coordinates": [-115.8..., 37.2...], "type": "Point"} 61 | 62 | Visualize the result of the example above `here `__. General information about Point can be found in `Section 3.1.2`_ and `Appendix A: Points`_ within `The GeoJSON Format Specification`_. 63 | 64 | .. _Section 3.1.2: https://tools.ietf.org/html/rfc7946#section-3.1.2 65 | .. _Appendix A\: Points: https://tools.ietf.org/html/rfc7946#appendix-A.1 66 | 67 | MultiPoint 68 | ~~~~~~~~~~ 69 | 70 | .. code:: python 71 | 72 | >>> from geojson import MultiPoint 73 | 74 | >>> MultiPoint([(-155.52, 19.61), (-156.22, 20.74), (-157.97, 21.46)]) # doctest: +ELLIPSIS 75 | {"coordinates": [[-155.5..., 19.6...], [-156.2..., 20.7...], [-157.9..., 21.4...]], "type": "MultiPoint"} 76 | 77 | Visualize the result of the example above `here `__. General information about MultiPoint can be found in `Section 3.1.3`_ and `Appendix A: MultiPoints`_ within `The GeoJSON Format Specification`_. 78 | 79 | .. _Section 3.1.3: https://tools.ietf.org/html/rfc7946#section-3.1.3 80 | .. _Appendix A\: MultiPoints: https://tools.ietf.org/html/rfc7946#appendix-A.4 81 | 82 | 83 | LineString 84 | ~~~~~~~~~~ 85 | 86 | .. code:: python 87 | 88 | >>> from geojson import LineString 89 | 90 | >>> LineString([(8.919, 44.4074), (8.923, 44.4075)]) # doctest: +ELLIPSIS 91 | {"coordinates": [[8.91..., 44.407...], [8.92..., 44.407...]], "type": "LineString"} 92 | 93 | Visualize the result of the example above `here `__. General information about LineString can be found in `Section 3.1.4`_ and `Appendix A: LineStrings`_ within `The GeoJSON Format Specification`_. 94 | 95 | .. _Section 3.1.4: https://tools.ietf.org/html/rfc7946#section-3.1.4 96 | .. _Appendix A\: LineStrings: https://tools.ietf.org/html/rfc7946#appendix-A.2 97 | 98 | MultiLineString 99 | ~~~~~~~~~~~~~~~ 100 | 101 | .. code:: python 102 | 103 | >>> from geojson import MultiLineString 104 | 105 | >>> MultiLineString([ 106 | ... [(3.75, 9.25), (-130.95, 1.52)], 107 | ... [(23.15, -34.25), (-1.35, -4.65), (3.45, 77.95)] 108 | ... ]) # doctest: +ELLIPSIS 109 | {"coordinates": [[[3.7..., 9.2...], [-130.9..., 1.52...]], [[23.1..., -34.2...], [-1.3..., -4.6...], [3.4..., 77.9...]]], "type": "MultiLineString"} 110 | 111 | Visualize the result of the example above `here `__. General information about MultiLineString can be found in `Section 3.1.5`_ and `Appendix A: MultiLineStrings`_ within `The GeoJSON Format Specification`_. 112 | 113 | .. _Section 3.1.5: https://tools.ietf.org/html/rfc7946#section-3.1.5 114 | .. _Appendix A\: MultiLineStrings: https://tools.ietf.org/html/rfc7946#appendix-A.5 115 | 116 | Polygon 117 | ~~~~~~~ 118 | 119 | .. code:: python 120 | 121 | >>> from geojson import Polygon 122 | 123 | >>> # no hole within polygon 124 | >>> Polygon([[(2.38, 57.322), (-120.43, 19.15), (23.194, -20.28), (2.38, 57.322)]]) # doctest: +ELLIPSIS 125 | {"coordinates": [[[2.3..., 57.32...], [-120.4..., 19.1...], [23.19..., -20.2...]]], "type": "Polygon"} 126 | 127 | >>> # hole within polygon 128 | >>> Polygon([ 129 | ... [(2.38, 57.322), (-120.43, 19.15), (23.194, -20.28), (2.38, 57.322)], 130 | ... [(-5.21, 23.51), (15.21, -10.81), (-20.51, 1.51), (-5.21, 23.51)] 131 | ... ]) # doctest: +ELLIPSIS 132 | {"coordinates": [[[2.3..., 57.32...], [-120.4..., 19.1...], [23.19..., -20.2...]], [[-5.2..., 23.5...], [15.2..., -10.8...], [-20.5..., 1.5...], [-5.2..., 23.5...]]], "type": "Polygon"} 133 | 134 | Visualize the results of the example above `here `__. General information about Polygon can be found in `Section 3.1.6`_ and `Appendix A: Polygons`_ within `The GeoJSON Format Specification`_. 135 | 136 | .. _Section 3.1.6: https://tools.ietf.org/html/rfc7946#section-3.1.6 137 | .. _Appendix A\: Polygons: https://tools.ietf.org/html/rfc7946#appendix-A.3 138 | 139 | MultiPolygon 140 | ~~~~~~~~~~~~ 141 | 142 | .. code:: python 143 | 144 | >>> from geojson import MultiPolygon 145 | 146 | >>> MultiPolygon([ 147 | ... ([(3.78, 9.28), (-130.91, 1.52), (35.12, 72.234), (3.78, 9.28)],), 148 | ... ([(23.18, -34.29), (-1.31, -4.61), (3.41, 77.91), (23.18, -34.29)],) 149 | ... ]) # doctest: +ELLIPSIS 150 | {"coordinates": [[[[3.7..., 9.2...], [-130.9..., 1.5...], [35.1..., 72.23...]]], [[[23.1..., -34.2...], [-1.3..., -4.6...], [3.4..., 77.9...]]]], "type": "MultiPolygon"} 151 | 152 | Visualize the result of the example above `here `__. General information about MultiPolygon can be found in `Section 3.1.7`_ and `Appendix A: MultiPolygons`_ within `The GeoJSON Format Specification`_. 153 | 154 | .. _Section 3.1.7: https://tools.ietf.org/html/rfc7946#section-3.1.7 155 | .. _Appendix A\: MultiPolygons: https://tools.ietf.org/html/rfc7946#appendix-A.6 156 | 157 | GeometryCollection 158 | ~~~~~~~~~~~~~~~~~~ 159 | 160 | .. code:: python 161 | 162 | >>> from geojson import GeometryCollection, Point, LineString 163 | 164 | >>> my_point = Point((23.532, -63.12)) 165 | 166 | >>> my_line = LineString([(-152.62, 51.21), (5.21, 10.69)]) 167 | 168 | >>> geo_collection = GeometryCollection([my_point, my_line]) 169 | 170 | >>> geo_collection # doctest: +ELLIPSIS 171 | {"geometries": [{"coordinates": [23.53..., -63.1...], "type": "Point"}, {"coordinates": [[-152.6..., 51.2...], [5.2..., 10.6...]], "type": "LineString"}], "type": "GeometryCollection"} 172 | 173 | >>> geo_collection[1] 174 | {"coordinates": [[-152.62, 51.21], [5.21, 10.69]], "type": "LineString"} 175 | 176 | >>> geo_collection[0] == geo_collection.geometries[0] 177 | True 178 | 179 | Visualize the result of the example above `here `__. General information about GeometryCollection can be found in `Section 3.1.8`_ and `Appendix A: GeometryCollections`_ within `The GeoJSON Format Specification`_. 180 | 181 | .. _Section 3.1.8: https://tools.ietf.org/html/rfc7946#section-3.1.8 182 | .. _Appendix A\: GeometryCollections: https://tools.ietf.org/html/rfc7946#appendix-A.7 183 | 184 | Feature 185 | ~~~~~~~ 186 | 187 | .. code:: python 188 | 189 | >>> from geojson import Feature, Point 190 | 191 | >>> my_point = Point((-3.68, 40.41)) 192 | 193 | >>> Feature(geometry=my_point) # doctest: +ELLIPSIS 194 | {"geometry": {"coordinates": [-3.68..., 40.4...], "type": "Point"}, "properties": {}, "type": "Feature"} 195 | 196 | >>> Feature(geometry=my_point, properties={"country": "Spain"}) # doctest: +ELLIPSIS 197 | {"geometry": {"coordinates": [-3.68..., 40.4...], "type": "Point"}, "properties": {"country": "Spain"}, "type": "Feature"} 198 | 199 | >>> Feature(geometry=my_point, id=27) # doctest: +ELLIPSIS 200 | {"geometry": {"coordinates": [-3.68..., 40.4...], "type": "Point"}, "id": 27, "properties": {}, "type": "Feature"} 201 | 202 | Visualize the results of the examples above `here `__. General information about Feature can be found in `Section 3.2`_ within `The GeoJSON Format Specification`_. 203 | 204 | .. _Section 3.2: https://tools.ietf.org/html/rfc7946#section-3.2 205 | 206 | FeatureCollection 207 | ~~~~~~~~~~~~~~~~~ 208 | 209 | .. code:: python 210 | 211 | >>> from geojson import Feature, Point, FeatureCollection 212 | 213 | >>> my_feature = Feature(geometry=Point((1.6432, -19.123))) 214 | 215 | >>> my_other_feature = Feature(geometry=Point((-80.234, -22.532))) 216 | 217 | >>> feature_collection = FeatureCollection([my_feature, my_other_feature]) 218 | 219 | >>> feature_collection # doctest: +ELLIPSIS 220 | {"features": [{"geometry": {"coordinates": [1.643..., -19.12...], "type": "Point"}, "properties": {}, "type": "Feature"}, {"geometry": {"coordinates": [-80.23..., -22.53...], "type": "Point"}, "properties": {}, "type": "Feature"}], "type": "FeatureCollection"} 221 | 222 | >>> feature_collection.errors() 223 | [] 224 | 225 | >>> (feature_collection[0] == feature_collection['features'][0], feature_collection[1] == my_other_feature) 226 | (True, True) 227 | 228 | Visualize the result of the example above `here `__. General information about FeatureCollection can be found in `Section 3.3`_ within `The GeoJSON Format Specification`_. 229 | 230 | .. _Section 3.3: https://tools.ietf.org/html/rfc7946#section-3.3 231 | 232 | GeoJSON encoding/decoding 233 | ------------------------- 234 | 235 | All of the GeoJSON Objects implemented in this library can be encoded and decoded into raw GeoJSON with the ``geojson.dump``, ``geojson.dumps``, ``geojson.load``, and ``geojson.loads`` functions. Note that each of these functions is a wrapper around the core `json` function with the same name, and will pass through any additional arguments. This allows you to control the JSON formatting or parsing behavior with the underlying core `json` functions. 236 | 237 | .. code:: python 238 | 239 | >>> import geojson 240 | 241 | >>> my_point = geojson.Point((43.24, -1.532)) 242 | 243 | >>> my_point # doctest: +ELLIPSIS 244 | {"coordinates": [43.2..., -1.53...], "type": "Point"} 245 | 246 | >>> dump = geojson.dumps(my_point, sort_keys=True) 247 | 248 | >>> dump # doctest: +ELLIPSIS 249 | '{"coordinates": [43.2..., -1.53...], "type": "Point"}' 250 | 251 | >>> geojson.loads(dump) # doctest: +ELLIPSIS 252 | {"coordinates": [43.2..., -1.53...], "type": "Point"} 253 | 254 | Custom classes 255 | ~~~~~~~~~~~~~~ 256 | 257 | This encoding/decoding functionality shown in the previous can be extended to custom classes using the interface described by the `__geo_interface__ Specification`_. 258 | 259 | .. code:: python 260 | 261 | >>> import geojson 262 | 263 | >>> class MyPoint(): 264 | ... def __init__(self, x, y): 265 | ... self.x = x 266 | ... self.y = y 267 | ... 268 | ... @property 269 | ... def __geo_interface__(self): 270 | ... return {'type': 'Point', 'coordinates': (self.x, self.y)} 271 | 272 | >>> point_instance = MyPoint(52.235, -19.234) 273 | 274 | >>> geojson.dumps(point_instance, sort_keys=True) # doctest: +ELLIPSIS 275 | '{"coordinates": [52.23..., -19.23...], "type": "Point"}' 276 | 277 | Default and custom precision 278 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 279 | 280 | GeoJSON Object-based classes in this package have an additional `precision` attribute which rounds off 281 | coordinates to 6 decimal places (roughly 0.1 meters) by default and can be customized per object instance. 282 | 283 | .. code:: python 284 | 285 | >>> from geojson import Point 286 | 287 | >>> Point((-115.123412341234, 37.123412341234)) # rounded to 6 decimal places by default 288 | {"coordinates": [-115.123412, 37.123412], "type": "Point"} 289 | 290 | >>> Point((-115.12341234, 37.12341234), precision=8) # rounded to 8 decimal places 291 | {"coordinates": [-115.12341234, 37.12341234], "type": "Point"} 292 | 293 | 294 | Precision can be set at the package level by setting `geojson.geometry.DEFAULT_PRECISION` 295 | 296 | 297 | .. code:: python 298 | 299 | >>> import geojson 300 | 301 | >>> geojson.geometry.DEFAULT_PRECISION = 5 302 | 303 | >>> from geojson import Point 304 | 305 | >>> Point((-115.12341234, 37.12341234)) # rounded to 8 decimal places 306 | {"coordinates": [-115.12341, 37.12341], "type": "Point"} 307 | 308 | 309 | After setting the DEFAULT_PRECISION, coordinates will be rounded off to that precision with `geojson.load` or `geojson.loads`. Following one of those with `geojson.dump` is a quick and easy way to scale down the precision of excessively precise, arbitrarily-sized GeoJSON data. 310 | 311 | 312 | Helpful utilities 313 | ----------------- 314 | 315 | coords 316 | ~~~~~~ 317 | 318 | :code:`geojson.utils.coords` yields all coordinate tuples from a geometry or feature object. 319 | 320 | .. code:: python 321 | 322 | >>> import geojson 323 | 324 | >>> my_line = LineString([(-152.62, 51.21), (5.21, 10.69)]) 325 | 326 | >>> my_feature = geojson.Feature(geometry=my_line) 327 | 328 | >>> list(geojson.utils.coords(my_feature)) # doctest: +ELLIPSIS 329 | [(-152.62..., 51.21...), (5.21..., 10.69...)] 330 | 331 | map_coords 332 | ~~~~~~~~~~ 333 | 334 | :code:`geojson.utils.map_coords` maps a function over all coordinate values and returns a geometry of the same type. Useful for scaling a geometry. 335 | 336 | .. code:: python 337 | 338 | >>> import geojson 339 | 340 | >>> new_point = geojson.utils.map_coords(lambda x: x/2, geojson.Point((-115.81, 37.24))) 341 | 342 | >>> geojson.dumps(new_point, sort_keys=True) # doctest: +ELLIPSIS 343 | '{"coordinates": [-57.905..., 18.62...], "type": "Point"}' 344 | 345 | map_tuples 346 | ~~~~~~~~~~ 347 | 348 | :code:`geojson.utils.map_tuples` maps a function over all coordinates and returns a geometry of the same type. Useful for changing coordinate order or applying coordinate transforms. 349 | 350 | .. code:: python 351 | 352 | >>> import geojson 353 | 354 | >>> new_point = geojson.utils.map_tuples(lambda c: (c[1], c[0]), geojson.Point((-115.81, 37.24))) 355 | 356 | >>> geojson.dumps(new_point, sort_keys=True) # doctest: +ELLIPSIS 357 | '{"coordinates": [37.24..., -115.81], "type": "Point"}' 358 | 359 | map_geometries 360 | ~~~~~~~~~~~~~~ 361 | 362 | :code:`geojson.utils.map_geometries` maps a function over each geometry in the input. 363 | 364 | .. code:: python 365 | 366 | >>> import geojson 367 | 368 | >>> new_point = geojson.utils.map_geometries(lambda g: geojson.MultiPoint([g["coordinates"]]), geojson.GeometryCollection([geojson.Point((-115.81, 37.24))])) 369 | 370 | >>> geojson.dumps(new_point, sort_keys=True) 371 | '{"geometries": [{"coordinates": [[-115.81, 37.24]], "type": "MultiPoint"}], "type": "GeometryCollection"}' 372 | 373 | validation 374 | ~~~~~~~~~~ 375 | 376 | :code:`is_valid` property provides simple validation of GeoJSON objects. 377 | 378 | .. code:: python 379 | 380 | >>> import geojson 381 | 382 | >>> obj = geojson.Point((-3.68,40.41,25.14,10.34)) 383 | >>> obj.is_valid 384 | False 385 | 386 | :code:`errors` method provides collection of errors when validation GeoJSON objects. 387 | 388 | .. code:: python 389 | 390 | >>> import geojson 391 | 392 | >>> obj = geojson.Point((-3.68,40.41,25.14,10.34)) 393 | >>> obj.errors() 394 | 'a position must have exactly 2 or 3 values' 395 | 396 | generate_random 397 | ~~~~~~~~~~~~~~~ 398 | 399 | :code:`geojson.utils.generate_random` yields a geometry type with random data 400 | 401 | .. code:: python 402 | 403 | >>> import geojson 404 | 405 | >>> geojson.utils.generate_random("LineString") # doctest: +ELLIPSIS 406 | {"coordinates": [...], "type": "LineString"} 407 | 408 | >>> geojson.utils.generate_random("Polygon") # doctest: +ELLIPSIS 409 | {"coordinates": [...], "type": "Polygon"} 410 | 411 | 412 | Development 413 | ----------- 414 | 415 | To build this project, run :code:`python setup.py build`. 416 | To run the unit tests, run :code:`python -m pip install tox && tox`. 417 | To run the style checks, run :code:`flake8` (install `flake8` if needed). 418 | 419 | Credits 420 | ------- 421 | 422 | * Sean Gillies 423 | * Matthew Russell 424 | * Corey Farwell 425 | * Blake Grotewold 426 | * Zsolt Ero 427 | * Sergey Romanov 428 | * Ray Riga 429 | 430 | 431 | .. _GeoJSON: https://geojson.org/ 432 | .. _The GeoJSON Format Specification: https://tools.ietf.org/html/rfc7946 433 | .. _\_\_geo\_interface\_\_ Specification: https://gist.github.com/sgillies/2217756 434 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Project information ----------------------------------------------------- 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 8 | 9 | project = 'geojson' 10 | copyright = '2024, Sean Gillies, Matthew Russell, Corey Farwell, Blake Grotewold, \ 11 | Zsolt Ero, Sergey Romaov, Ray Riga' 12 | author = 'Sean Gillies, Matthew Russell, Corey Farwell, Blake Grotewold, Zsolt Ero, \ 13 | Sergey Romaov, Ray Riga' 14 | release = '3.1.0' 15 | 16 | # -- General configuration --------------------------------------------------- 17 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 18 | 19 | extensions = ['sphinxcontrib.jquery'] 20 | 21 | templates_path = ['_templates'] 22 | exclude_patterns = [] 23 | 24 | # -- Options for HTML output ------------------------------------------------- 25 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 26 | 27 | html_theme = 'sphinx_rtd_theme' 28 | html_theme_path = ['_theme'] 29 | html_static_path = ['_static'] 30 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. geojson documentation master file, created by 2 | sphinx-quickstart on Tue Aug 6 21:00:20 2024. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | :caption: Contents: 9 | 10 | .. include:: ../../README.rst 11 | -------------------------------------------------------------------------------- /docs/source/theme.toml: -------------------------------------------------------------------------------- 1 | [theme] 2 | name = "sphinx_rtd_theme" 3 | base_url = "https://readthedocs.org/projects/sphinx-rtd-theme/" 4 | -------------------------------------------------------------------------------- /geojson/__init__.py: -------------------------------------------------------------------------------- 1 | from geojson.codec import dump, dumps, load, loads, GeoJSONEncoder 2 | from geojson.utils import coords, map_coords 3 | from geojson.geometry import Point, LineString, Polygon 4 | from geojson.geometry import MultiLineString, MultiPoint, MultiPolygon 5 | from geojson.geometry import GeometryCollection 6 | from geojson.feature import Feature, FeatureCollection 7 | from geojson.base import GeoJSON 8 | from geojson._version import __version__, __version_info__ 9 | 10 | __all__ = ([dump, dumps, load, loads, GeoJSONEncoder] + 11 | [coords, map_coords] + 12 | [Point, LineString, Polygon] + 13 | [MultiLineString, MultiPoint, MultiPolygon] + 14 | [GeometryCollection] + 15 | [Feature, FeatureCollection] + 16 | [GeoJSON] + 17 | [__version__, __version_info__]) 18 | -------------------------------------------------------------------------------- /geojson/_version.py: -------------------------------------------------------------------------------- 1 | __version__ = "3.2.0" 2 | __version_info__ = tuple(map(int, __version__.split("."))) 3 | -------------------------------------------------------------------------------- /geojson/base.py: -------------------------------------------------------------------------------- 1 | import geojson 2 | from geojson.mapping import to_mapping 3 | 4 | 5 | class GeoJSON(dict): 6 | """ 7 | A class representing a GeoJSON object. 8 | """ 9 | 10 | def __init__(self, iterable=(), **extra): 11 | """ 12 | Initialises a GeoJSON object 13 | 14 | :param iterable: iterable from which to draw the content of the GeoJSON 15 | object. 16 | :type iterable: dict, array, tuple 17 | :return: a GeoJSON object 18 | :rtype: GeoJSON 19 | """ 20 | super().__init__(iterable) 21 | self["type"] = getattr(self, "type", type(self).__name__) 22 | self.update(extra) 23 | 24 | def __repr__(self): 25 | return geojson.dumps(self, sort_keys=True) 26 | 27 | __str__ = __repr__ 28 | 29 | def __getattr__(self, name): 30 | """ 31 | Permit dictionary items to be retrieved like object attributes 32 | 33 | :param name: attribute name 34 | :type name: str, int 35 | :return: dictionary value 36 | """ 37 | try: 38 | return self[name] 39 | except KeyError: 40 | raise AttributeError(name) 41 | 42 | def __setattr__(self, name, value): 43 | """ 44 | Permit dictionary items to be set like object attributes. 45 | 46 | :param name: key of item to be set 47 | :type name: str 48 | :param value: value to set item to 49 | """ 50 | 51 | self[name] = value 52 | 53 | def __delattr__(self, name): 54 | """ 55 | Permit dictionary items to be deleted like object attributes 56 | 57 | :param name: key of item to be deleted 58 | :type name: str 59 | """ 60 | 61 | del self[name] 62 | 63 | @property 64 | def __geo_interface__(self): 65 | if self.type != "GeoJSON": 66 | return self 67 | 68 | @classmethod 69 | def to_instance(cls, ob, default=None, strict=False): 70 | """Encode a GeoJSON dict into an GeoJSON object. 71 | Assumes the caller knows that the dict should satisfy a GeoJSON type. 72 | 73 | :param cls: Dict containing the elements to be encoded into a GeoJSON 74 | object. 75 | :type cls: dict 76 | :param ob: GeoJSON object into which to encode the dict provided in 77 | `cls`. 78 | :type ob: GeoJSON 79 | :param default: A default instance to append the content of the dict 80 | to if none is provided. 81 | :type default: GeoJSON 82 | :param strict: Raise error if unable to coerce particular keys or 83 | attributes to a valid GeoJSON structure. 84 | :type strict: bool 85 | :return: A GeoJSON object with the dict's elements as its constituents. 86 | :rtype: GeoJSON 87 | :raises TypeError: If the input dict contains items that are not valid 88 | GeoJSON types. 89 | :raises UnicodeEncodeError: If the input dict contains items of a type 90 | that contain non-ASCII characters. 91 | :raises AttributeError: If the input dict contains items that are not 92 | valid GeoJSON types. 93 | """ 94 | if ob is None and default is not None: 95 | instance = default() 96 | elif isinstance(ob, GeoJSON): 97 | instance = ob 98 | else: 99 | mapping = to_mapping(ob) 100 | d = {} 101 | for k in mapping: 102 | d[k] = mapping[k] 103 | try: 104 | type_ = d.pop("type") 105 | try: 106 | type_ = str(type_) 107 | except UnicodeEncodeError: 108 | # If the type contains non-ascii characters, we can assume 109 | # it's not a valid GeoJSON type 110 | raise AttributeError(f"{type_} is not a GeoJSON type") 111 | geojson_factory = getattr(geojson.factory, type_) 112 | instance = geojson_factory(**d) 113 | except (AttributeError, KeyError) as invalid: 114 | if strict: 115 | raise ValueError( 116 | f"Cannot coerce {ob!r} into " 117 | f"a valid GeoJSON structure: {invalid}" 118 | ) 119 | instance = ob 120 | return instance 121 | 122 | @property 123 | def is_valid(self): 124 | return not self.errors() 125 | 126 | def check_list_errors(self, checkFunc, lst): 127 | """Validation helper function.""" 128 | # check for errors on each subitem, filter only subitems with errors 129 | results = (checkFunc(i) for i in lst) 130 | return [err for err in results if err] 131 | 132 | def errors(self): 133 | """Return validation errors (if any). 134 | Implement in each subclass. 135 | """ 136 | 137 | # make sure that each subclass implements it's own validation function 138 | if self.__class__ != GeoJSON: 139 | raise NotImplementedError(self.__class__) 140 | -------------------------------------------------------------------------------- /geojson/codec.py: -------------------------------------------------------------------------------- 1 | try: 2 | import simplejson as json 3 | except ImportError: 4 | import json 5 | 6 | import geojson 7 | import geojson.factory 8 | from geojson.mapping import to_mapping 9 | 10 | 11 | class GeoJSONEncoder(json.JSONEncoder): 12 | 13 | def default(self, obj): 14 | return geojson.factory.GeoJSON.to_instance(obj) # NOQA 15 | 16 | 17 | # Wrap the functions from json, providing encoder, decoders, and 18 | # object creation hooks. 19 | # Here the defaults are set to only permit valid JSON as per RFC 4267 20 | 21 | def _enforce_strict_numbers(obj): 22 | raise ValueError(f"Number {obj!r} is not JSON compliant") 23 | 24 | 25 | def dump(obj, fp, cls=GeoJSONEncoder, allow_nan=False, **kwargs): 26 | return json.dump(to_mapping(obj), 27 | fp, cls=cls, allow_nan=allow_nan, **kwargs) 28 | 29 | 30 | def dumps(obj, cls=GeoJSONEncoder, allow_nan=False, ensure_ascii=False, **kwargs): 31 | return json.dumps(to_mapping(obj), 32 | cls=cls, allow_nan=allow_nan, ensure_ascii=ensure_ascii, **kwargs) 33 | 34 | 35 | def load(fp, 36 | cls=json.JSONDecoder, 37 | parse_constant=_enforce_strict_numbers, 38 | object_hook=geojson.base.GeoJSON.to_instance, 39 | **kwargs): 40 | return json.load(fp, 41 | cls=cls, object_hook=object_hook, 42 | parse_constant=parse_constant, 43 | **kwargs) 44 | 45 | 46 | def loads(s, 47 | cls=json.JSONDecoder, 48 | parse_constant=_enforce_strict_numbers, 49 | object_hook=geojson.base.GeoJSON.to_instance, 50 | **kwargs): 51 | return json.loads(s, 52 | cls=cls, object_hook=object_hook, 53 | parse_constant=parse_constant, 54 | **kwargs) 55 | 56 | 57 | # Backwards compatibility 58 | PyGFPEncoder = GeoJSONEncoder 59 | -------------------------------------------------------------------------------- /geojson/examples.py: -------------------------------------------------------------------------------- 1 | """ 2 | SimpleWebFeature is a working example of a class that satisfies the Python geo 3 | interface. 4 | """ 5 | 6 | 7 | class SimpleWebFeature: 8 | 9 | """ 10 | A simple, Atom-ish, single geometry (WGS84) GIS feature. 11 | """ 12 | 13 | def __init__(self, id=None, geometry=None, title=None, summary=None, 14 | link=None): 15 | """ 16 | Initialises a SimpleWebFeature from the parameters provided. 17 | 18 | :param id: Identifier assigned to the object. 19 | :type id: int, str 20 | :param geometry: The geometry on which the object is based. 21 | :type geometry: Geometry 22 | :param title: Name of the object 23 | :type title: str 24 | :param summary: Short summary associated with the object. 25 | :type summary: str 26 | :param link: Link associated with the object. 27 | :type link: str 28 | :return: A SimpleWebFeature object 29 | :rtype: SimpleWebFeature 30 | """ 31 | self.id = id 32 | self.geometry = geometry 33 | self.properties = {'title': title, 'summary': summary, 'link': link} 34 | 35 | def as_dict(self): 36 | return { 37 | "type": "Feature", 38 | "id": self.id, 39 | "properties": self.properties, 40 | "geometry": self.geometry 41 | } 42 | 43 | __geo_interface__ = property(as_dict) 44 | 45 | 46 | def create_simple_web_feature(o): 47 | """ 48 | Create an instance of SimpleWebFeature from a dict, o. If o does not 49 | match a Python feature object, simply return o. This function serves as a 50 | json decoder hook. See coding.load(). 51 | 52 | :param o: A dict to create the SimpleWebFeature from. 53 | :type o: dict 54 | :return: A SimpleWebFeature from the dict provided. 55 | :rtype: SimpleWebFeature 56 | """ 57 | try: 58 | id = o['id'] 59 | g = o['geometry'] 60 | p = o['properties'] 61 | return SimpleWebFeature(str(id), { 62 | 'type': str(g.get('type')), 63 | 'coordinates': g.get('coordinates', [])}, 64 | title=p.get('title'), 65 | summary=p.get('summary'), 66 | link=str(p.get('link'))) 67 | except (KeyError, TypeError): 68 | pass 69 | return o 70 | -------------------------------------------------------------------------------- /geojson/factory.py: -------------------------------------------------------------------------------- 1 | from geojson.geometry import Point, LineString, Polygon 2 | from geojson.geometry import MultiLineString, MultiPoint, MultiPolygon 3 | from geojson.geometry import GeometryCollection 4 | from geojson.feature import Feature, FeatureCollection 5 | from geojson.base import GeoJSON 6 | 7 | __all__ = ([Point, LineString, Polygon] + 8 | [MultiLineString, MultiPoint, MultiPolygon] + 9 | [GeometryCollection] + 10 | [Feature, FeatureCollection] + 11 | [GeoJSON]) 12 | -------------------------------------------------------------------------------- /geojson/feature.py: -------------------------------------------------------------------------------- 1 | from geojson.base import GeoJSON 2 | 3 | 4 | class Feature(GeoJSON): 5 | """ 6 | Represents a WGS84 GIS feature. 7 | """ 8 | 9 | def __init__(self, id=None, geometry=None, properties=None, **extra): 10 | """ 11 | Initialises a Feature object with the given parameters. 12 | 13 | :param id: Feature identifier, such as a sequential number. 14 | :type id: str, int 15 | :param geometry: Geometry corresponding to the feature. 16 | :param properties: Dict containing properties of the feature. 17 | :type properties: dict 18 | :return: Feature object 19 | :rtype: Feature 20 | """ 21 | super().__init__(**extra) 22 | if id is not None: 23 | self["id"] = id 24 | self["geometry"] = (self.to_instance(geometry, strict=True) 25 | if geometry else None) 26 | self["properties"] = properties or {} 27 | 28 | def errors(self): 29 | geo = self.get('geometry') 30 | return geo.errors() if geo else None 31 | 32 | 33 | class FeatureCollection(GeoJSON): 34 | """ 35 | Represents a FeatureCollection, a set of multiple Feature objects. 36 | """ 37 | 38 | def __init__(self, features, **extra): 39 | """ 40 | Initialises a FeatureCollection object from the 41 | :param features: List of features to constitute the FeatureCollection. 42 | :type features: list 43 | :return: FeatureCollection object 44 | :rtype: FeatureCollection 45 | """ 46 | super().__init__(**extra) 47 | self["features"] = features 48 | 49 | def errors(self): 50 | return self.check_list_errors(lambda x: x.errors(), self.features) 51 | 52 | def __getitem__(self, key): 53 | try: 54 | return self.get("features", ())[key] 55 | except (KeyError, TypeError, IndexError): 56 | return super(GeoJSON, self).__getitem__(key) 57 | -------------------------------------------------------------------------------- /geojson/geometry.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | from numbers import Number, Real 3 | 4 | from geojson.base import GeoJSON 5 | 6 | 7 | DEFAULT_PRECISION = 6 8 | 9 | 10 | class Geometry(GeoJSON): 11 | """ 12 | Represents an abstract base class for a WGS84 geometry. 13 | """ 14 | 15 | def __init__(self, coordinates=None, validate=False, precision=None, **extra): 16 | """ 17 | Initialises a Geometry object. 18 | 19 | :param coordinates: Coordinates of the Geometry object. 20 | :type coordinates: tuple or list of tuple 21 | :param validate: Raise exception if validation errors are present? 22 | :type validate: boolean 23 | :param precision: Number of decimal places for lat/lon coords. 24 | :type precision: integer 25 | """ 26 | super().__init__(**extra) 27 | if precision is None: 28 | precision = DEFAULT_PRECISION 29 | self["coordinates"] = self.clean_coordinates( 30 | coordinates or [], precision) 31 | 32 | if validate: 33 | errors = self.errors() 34 | if errors: 35 | raise ValueError(f'{errors}: {coordinates}') 36 | 37 | @classmethod 38 | def clean_coordinates(cls, coords, precision): 39 | if isinstance(coords, cls): 40 | return coords['coordinates'] 41 | 42 | new_coords = [] 43 | if isinstance(coords, Geometry): 44 | coords = [coords] 45 | for coord in coords: 46 | if isinstance(coord, (list, tuple)): 47 | new_coords.append(cls.clean_coordinates(coord, precision)) 48 | elif isinstance(coord, Geometry): 49 | new_coords.append(coord['coordinates']) 50 | elif isinstance(coord, (Real, Decimal)): 51 | new_coords.append(round(coord, precision)) 52 | else: 53 | raise ValueError(f"{coord!r} is not a JSON compliant number") 54 | return new_coords 55 | 56 | 57 | class GeometryCollection(GeoJSON): 58 | """ 59 | Represents an abstract base class for collections of WGS84 geometries. 60 | """ 61 | 62 | def __init__(self, geometries=None, **extra): 63 | super().__init__(**extra) 64 | self["geometries"] = geometries or [] 65 | 66 | def errors(self): 67 | errors = [geom.errors() for geom in self['geometries']] 68 | return [err for err in errors if err] 69 | 70 | def __getitem__(self, key): 71 | try: 72 | return self.get("geometries", ())[key] 73 | except (KeyError, TypeError, IndexError): 74 | return super(GeoJSON, self).__getitem__(key) 75 | 76 | 77 | # Marker classes. 78 | 79 | def check_point(coord): 80 | if not isinstance(coord, list): 81 | return 'each position must be a list' 82 | if len(coord) not in (2, 3): 83 | return 'a position must have exactly 2 or 3 values' 84 | for number in coord: 85 | if not isinstance(number, Number): 86 | return 'a position cannot have inner positions' 87 | 88 | 89 | class Point(Geometry): 90 | def errors(self): 91 | return check_point(self['coordinates']) 92 | 93 | 94 | class MultiPoint(Geometry): 95 | def errors(self): 96 | return self.check_list_errors(check_point, self['coordinates']) 97 | 98 | 99 | def check_line_string(coord): 100 | if not isinstance(coord, list): 101 | return 'each line must be a list of positions' 102 | if len(coord) < 2: 103 | return ('the "coordinates" member must be an array of ' 104 | 'two or more positions') 105 | for pos in coord: 106 | error = check_point(pos) 107 | if error: 108 | return error 109 | 110 | 111 | class LineString(MultiPoint): 112 | def errors(self): 113 | return check_line_string(self['coordinates']) 114 | 115 | 116 | class MultiLineString(Geometry): 117 | def errors(self): 118 | return self.check_list_errors(check_line_string, self['coordinates']) 119 | 120 | 121 | def check_polygon(coord): 122 | if not isinstance(coord, list): 123 | return 'Each polygon must be a list of linear rings' 124 | 125 | if not all(isinstance(elem, list) for elem in coord): 126 | return "Each element of a polygon's coordinates must be a list" 127 | 128 | lengths = all(len(elem) >= 4 for elem in coord) 129 | if lengths is False: 130 | return 'Each linear ring must contain at least 4 positions' 131 | 132 | isring = all(elem[0] == elem[-1] for elem in coord) 133 | if isring is False: 134 | return 'Each linear ring must end where it started' 135 | 136 | 137 | class Polygon(Geometry): 138 | def errors(self): 139 | return check_polygon(self['coordinates']) 140 | 141 | 142 | class MultiPolygon(Geometry): 143 | def errors(self): 144 | return self.check_list_errors(check_polygon, self['coordinates']) 145 | 146 | 147 | class Default: 148 | """ 149 | GeoJSON default object. 150 | """ 151 | -------------------------------------------------------------------------------- /geojson/mapping.py: -------------------------------------------------------------------------------- 1 | from collections.abc import MutableMapping 2 | 3 | try: 4 | import simplejson as json 5 | except ImportError: 6 | import json 7 | 8 | import geojson 9 | 10 | 11 | GEO_INTERFACE_MARKER = "__geo_interface__" 12 | 13 | 14 | def is_mapping(obj): 15 | """ 16 | Checks if the object is an instance of MutableMapping. 17 | 18 | :param obj: Object to be checked. 19 | :return: Truth value of whether the object is an instance of 20 | MutableMapping. 21 | :rtype: bool 22 | """ 23 | return isinstance(obj, MutableMapping) 24 | 25 | 26 | def to_mapping(obj): 27 | 28 | mapping = getattr(obj, GEO_INTERFACE_MARKER, None) 29 | 30 | if mapping is not None: 31 | return mapping 32 | 33 | if is_mapping(obj): 34 | return obj 35 | 36 | if isinstance(obj, geojson.GeoJSON): 37 | return dict(obj) 38 | 39 | return json.loads(json.dumps(obj)) 40 | -------------------------------------------------------------------------------- /geojson/utils.py: -------------------------------------------------------------------------------- 1 | """Coordinate utility functions.""" 2 | 3 | 4 | def coords(obj): 5 | """ 6 | Yields the coordinates from a Feature or Geometry. 7 | 8 | :param obj: A geometry or feature to extract the coordinates from. 9 | :type obj: Feature, Geometry 10 | :return: A generator with coordinate tuples from the geometry or feature. 11 | :rtype: generator 12 | """ 13 | # Handle recursive case first 14 | if 'features' in obj: # FeatureCollection 15 | for f in obj['features']: 16 | yield from coords(f) 17 | elif 'geometry' in obj: # Feature 18 | yield from coords(obj['geometry']) 19 | elif 'geometries' in obj: # GeometryCollection 20 | for g in obj['geometries']: 21 | yield from coords(g) 22 | else: 23 | if isinstance(obj, (tuple, list)): 24 | coordinates = obj 25 | else: 26 | coordinates = obj.get('coordinates', obj) 27 | for e in coordinates: 28 | if isinstance(e, (float, int)): 29 | yield tuple(coordinates) 30 | break 31 | for f in coords(e): 32 | yield f 33 | 34 | 35 | def map_coords(func, obj): 36 | """ 37 | Returns the mapped coordinates from a Geometry after applying the provided 38 | function to each dimension in tuples list (ie, linear scaling). 39 | 40 | :param func: Function to apply to individual coordinate values 41 | independently 42 | :type func: function 43 | :param obj: A geometry or feature to extract the coordinates from. 44 | :type obj: Point, LineString, MultiPoint, MultiLineString, Polygon, 45 | MultiPolygon 46 | :return: The result of applying the function to each dimension in the 47 | array. 48 | :rtype: list 49 | :raises ValueError: if the provided object is not GeoJSON. 50 | """ 51 | 52 | def tuple_func(coord): 53 | return (func(coord[0]), func(coord[1])) 54 | 55 | return map_tuples(tuple_func, obj) 56 | 57 | 58 | def map_tuples(func, obj): 59 | """ 60 | Returns the mapped coordinates from a Geometry after applying the provided 61 | function to each coordinate. 62 | 63 | :param func: Function to apply to tuples 64 | :type func: function 65 | :param obj: A geometry or feature to extract the coordinates from. 66 | :type obj: Point, LineString, MultiPoint, MultiLineString, Polygon, 67 | MultiPolygon 68 | :return: The result of applying the function to each dimension in the 69 | array. 70 | :rtype: list 71 | :raises ValueError: if the provided object is not GeoJSON. 72 | """ 73 | 74 | if obj['type'] == 'Point': 75 | coordinates = tuple(func(obj['coordinates'])) 76 | elif obj['type'] in ['LineString', 'MultiPoint']: 77 | coordinates = [tuple(func(c)) for c in obj['coordinates']] 78 | elif obj['type'] in ['MultiLineString', 'Polygon']: 79 | coordinates = [[ 80 | tuple(func(c)) for c in curve] 81 | for curve in obj['coordinates']] 82 | elif obj['type'] == 'MultiPolygon': 83 | coordinates = [[[ 84 | tuple(func(c)) for c in curve] 85 | for curve in part] 86 | for part in obj['coordinates']] 87 | elif obj['type'] in ['Feature', 'FeatureCollection', 'GeometryCollection']: 88 | return map_geometries(lambda g: map_tuples(func, g), obj) 89 | else: 90 | raise ValueError(f"Invalid geometry object {obj!r}") 91 | return {'type': obj['type'], 'coordinates': coordinates} 92 | 93 | 94 | def map_geometries(func, obj): 95 | """ 96 | Returns the result of passing every geometry in the given geojson object 97 | through func. 98 | 99 | :param func: Function to apply to tuples 100 | :type func: function 101 | :param obj: A geometry or feature to extract the coordinates from. 102 | :type obj: GeoJSON 103 | :return: The result of applying the function to each geometry 104 | :rtype: list 105 | :raises ValueError: if the provided object is not geojson. 106 | """ 107 | simple_types = [ 108 | 'Point', 109 | 'LineString', 110 | 'MultiPoint', 111 | 'MultiLineString', 112 | 'Polygon', 113 | 'MultiPolygon', 114 | ] 115 | 116 | if obj['type'] in simple_types: 117 | return func(obj) 118 | elif obj['type'] == 'GeometryCollection': 119 | geoms = [func(geom) if geom else None for geom in obj['geometries']] 120 | return {'type': obj['type'], 'geometries': geoms} 121 | elif obj['type'] == 'Feature': 122 | obj['geometry'] = func(obj['geometry']) if obj['geometry'] else None 123 | return obj 124 | elif obj['type'] == 'FeatureCollection': 125 | feats = [map_geometries(func, feat) for feat in obj['features']] 126 | return {'type': obj['type'], 'features': feats} 127 | else: 128 | raise ValueError(f"Invalid GeoJSON object {obj!r}") 129 | 130 | 131 | def generate_random(featureType, numberVertices=3, 132 | boundingBox=[-180.0, -90.0, 180.0, 90.0]): 133 | """ 134 | Generates random geojson features depending on the parameters 135 | passed through. 136 | The bounding box defaults to the world - [-180.0, -90.0, 180.0, 90.0]. 137 | The number of vertices defaults to 3. 138 | 139 | :param featureType: A geometry type 140 | :type featureType: Point, LineString, Polygon 141 | :param numberVertices: The number vertices that a linestring or polygon 142 | will have 143 | :type numberVertices: int 144 | :param boundingBox: A bounding box in which features will be restricted to 145 | :type boundingBox: list 146 | :return: The resulting random geojson object or geometry collection. 147 | :rtype: object 148 | :raises ValueError: if there is no featureType provided. 149 | """ 150 | 151 | from geojson import Point, LineString, Polygon 152 | import random 153 | import math 154 | 155 | lon_min, lat_min, lon_max, lat_max = boundingBox 156 | 157 | def random_lon(): 158 | return random.uniform(lon_min, lon_max) 159 | 160 | def random_lat(): 161 | return random.uniform(lat_min, lat_max) 162 | 163 | def create_point(): 164 | return Point((random_lon(), random_lat())) 165 | 166 | def create_line(): 167 | return LineString([create_point() for _ in range(numberVertices)]) 168 | 169 | def create_poly(): 170 | ave_radius = 60 171 | ctr_x = 0.1 172 | ctr_y = 0.2 173 | irregularity = clip(0.1, 0, 1) * math.tau / numberVertices 174 | spikeyness = clip(0.5, 0, 1) * ave_radius 175 | 176 | lower = (math.tau / numberVertices) - irregularity 177 | upper = (math.tau / numberVertices) + irregularity 178 | angle_steps = [] 179 | for _ in range(numberVertices): 180 | angle_steps.append(random.uniform(lower, upper)) 181 | sum_angle = sum(angle_steps) 182 | 183 | k = sum_angle / math.tau 184 | angle_steps = [x / k for x in angle_steps] 185 | 186 | points = [] 187 | angle = random.uniform(0, math.tau) 188 | 189 | for angle_step in angle_steps: 190 | r_i = clip(random.gauss(ave_radius, spikeyness), 0, 2 * ave_radius) 191 | x = ctr_x + r_i * math.cos(angle) 192 | y = ctr_y + r_i * math.sin(angle) 193 | x = (x + 180.0) * (abs(lon_min - lon_max) / 360.0) + lon_min 194 | y = (y + 90.0) * (abs(lat_min - lat_max) / 180.0) + lat_min 195 | x = clip(x, lon_min, lon_max) 196 | y = clip(y, lat_min, lat_max) 197 | points.append((x, y)) 198 | angle += angle_step 199 | 200 | points.append(points[0]) # append first point to the end 201 | return Polygon([points]) 202 | 203 | def clip(x, min_val, max_val): 204 | if min_val > max_val: 205 | return x 206 | else: 207 | return min(max(min_val, x), max_val) 208 | 209 | if featureType == 'Point': 210 | return create_point() 211 | 212 | if featureType == 'LineString': 213 | return create_line() 214 | 215 | if featureType == 'Polygon': 216 | return create_poly() 217 | 218 | raise ValueError(f"featureType: {featureType} is not supported.") 219 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | import sys 3 | import re 4 | 5 | 6 | with open("README.rst") as readme_file: 7 | readme_text = readme_file.read() 8 | 9 | VERSIONFILE = "geojson/_version.py" 10 | verstrline = open(VERSIONFILE).read() 11 | VSRE = r"^__version__ = ['\"]([^'\"]*)['\"]" 12 | mo = re.search(VSRE, verstrline, re.M) 13 | if mo: 14 | verstr = mo.group(1) 15 | else: 16 | raise RuntimeError(f"Unable to find version string in {VERSIONFILE}.") 17 | 18 | 19 | major_version, minor_version = sys.version_info[:2] 20 | if not (major_version == 3 and 7 <= minor_version <= 13): 21 | sys.stderr.write("Sorry, only Python 3.7 - 3.13 are " 22 | "supported at this time.\n") 23 | exit(1) 24 | 25 | setup( 26 | name="geojson", 27 | version=verstr, 28 | description="Python bindings and utilities for GeoJSON", 29 | license="BSD", 30 | keywords="gis geography json", 31 | author="Sean Gillies", 32 | author_email="sgillies@frii.com", 33 | maintainer="Ray Riga", 34 | maintainer_email="ray.maintainer@gmail.com", 35 | url="https://github.com/jazzband/geojson", 36 | long_description=readme_text, 37 | packages=["geojson"], 38 | package_dir={"geojson": "geojson"}, 39 | package_data={"geojson": ["*.rst"]}, 40 | install_requires=[], 41 | python_requires=">=3.7", 42 | classifiers=[ 43 | "Development Status :: 5 - Production/Stable", 44 | "Intended Audience :: Developers", 45 | "Intended Audience :: Science/Research", 46 | "License :: OSI Approved :: BSD License", 47 | "Operating System :: OS Independent", 48 | "Programming Language :: Python", 49 | "Programming Language :: Python :: 3", 50 | "Programming Language :: Python :: 3.7", 51 | "Programming Language :: Python :: 3.8", 52 | "Programming Language :: Python :: 3.9", 53 | "Programming Language :: Python :: 3.10", 54 | "Programming Language :: Python :: 3.11", 55 | "Programming Language :: Python :: 3.12", 56 | "Programming Language :: Python :: 3.13", 57 | "Topic :: Scientific/Engineering :: GIS", 58 | ] 59 | ) 60 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import doctest 2 | import glob 3 | import os 4 | 5 | optionflags = (doctest.REPORT_ONLY_FIRST_FAILURE | 6 | doctest.NORMALIZE_WHITESPACE | 7 | doctest.ELLIPSIS) 8 | 9 | _basedir = os.path.dirname(__file__) 10 | paths = glob.glob(f"{_basedir}/*.txt") 11 | test_suite = doctest.DocFileSuite(*paths, **dict(module_relative=False, 12 | optionflags=optionflags)) 13 | -------------------------------------------------------------------------------- /tests/data.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "properties": { 3 | "Ã": "Ã" 4 | }, 5 | "type": "Feature", 6 | "geometry": null 7 | } 8 | -------------------------------------------------------------------------------- /tests/test_base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for geojson/base.py 3 | """ 4 | 5 | import unittest 6 | 7 | import geojson 8 | 9 | 10 | class TypePropertyTestCase(unittest.TestCase): 11 | def test_type_property(self): 12 | json_str = ('{"type": "Feature",' 13 | ' "geometry": null,' 14 | ' "id": 1,' 15 | ' "properties": {"type": "é"}}') 16 | geojson_obj = geojson.loads(json_str) 17 | self.assertTrue(isinstance(geojson_obj, geojson.GeoJSON)) 18 | self.assertTrue("type" in geojson_obj.properties) 19 | 20 | json_str = ('{"type": "Feature",' 21 | ' "geometry": null,' 22 | ' "id": 1,' 23 | ' "properties": {"type": null}}') 24 | geojson_obj = geojson.loads(json_str) 25 | self.assertTrue(isinstance(geojson_obj, geojson.GeoJSON)) 26 | self.assertTrue("type" in geojson_obj.properties) 27 | 28 | json_str = ('{"type": "Feature",' 29 | ' "geometry": null,' 30 | ' "id": 1,' 31 | ' "properties": {"type": "meow"}}') 32 | geojson_obj = geojson.loads(json_str) 33 | self.assertTrue(isinstance(geojson_obj, geojson.GeoJSON)) 34 | self.assertTrue("type" in geojson_obj.properties) 35 | 36 | 37 | class OperatorOverloadingTestCase(unittest.TestCase): 38 | """ 39 | Tests for operator overloading 40 | """ 41 | 42 | def setUp(self): 43 | self.coords = (12, -5) 44 | self.point = geojson.Point(self.coords) 45 | 46 | def test_setattr(self): 47 | new_coords = (27, 42) 48 | self.point.coordinates = new_coords 49 | self.assertEqual(self.point['coordinates'], new_coords) 50 | 51 | def test_getattr(self): 52 | self.assertEqual(self.point['coordinates'], self.point.coordinates) 53 | 54 | def test_delattr(self): 55 | del self.point.coordinates 56 | self.assertFalse(hasattr(self.point, 'coordinates')) 57 | 58 | 59 | class BaseTestCase(unittest.TestCase): 60 | 61 | def test_to_instance(self): 62 | FAKE = 'fake' 63 | self.assertEqual(FAKE, geojson.GeoJSON.to_instance( 64 | None, (lambda: FAKE))) 65 | 66 | with self.assertRaises(ValueError): 67 | geojson.GeoJSON.to_instance({"type": "Not GeoJSON"}, strict=True) 68 | 69 | def test_errors(self): 70 | class Fake(geojson.GeoJSON): 71 | pass 72 | 73 | with self.assertRaises(NotImplementedError): 74 | Fake().errors() 75 | -------------------------------------------------------------------------------- /tests/test_constructor.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for geojson object constructor 3 | """ 4 | 5 | import unittest 6 | 7 | import geojson 8 | 9 | 10 | class TestGeoJSONConstructor(unittest.TestCase): 11 | 12 | def test_copy_construction(self): 13 | coords = [1, 2] 14 | pt = geojson.Point(coords) 15 | self.assertEqual(geojson.Point(pt), pt) 16 | 17 | def test_nested_constructors(self): 18 | a = [5, 6] 19 | b = [9, 10] 20 | c = [-5, 12] 21 | mp = geojson.MultiPoint([geojson.Point(a), b]) 22 | self.assertEqual(mp.coordinates, [a, b]) 23 | 24 | mls = geojson.MultiLineString([geojson.LineString([a, b]), [a, c]]) 25 | self.assertEqual(mls.coordinates, [[a, b], [a, c]]) 26 | 27 | outer = [a, b, c, a] 28 | poly = geojson.Polygon(geojson.MultiPoint(outer)) 29 | other = [[1, 1], [1, 2], [2, 1], [1, 1]] 30 | poly2 = geojson.Polygon([outer, other]) 31 | self.assertEqual(geojson.MultiPolygon([poly, poly2]).coordinates, 32 | [[outer], [outer, other]]) 33 | -------------------------------------------------------------------------------- /tests/test_coords.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import geojson 4 | from geojson.utils import coords, map_coords 5 | 6 | TOO_PRECISE = (1.12341234, -2.12341234) 7 | 8 | 9 | class CoordsTestCase(unittest.TestCase): 10 | def test_point(self): 11 | itr = coords(geojson.Point((-115.81, 37.24))) 12 | self.assertEqual(next(itr), (-115.81, 37.24)) 13 | 14 | def test_point_rounding(self): 15 | itr = coords(geojson.Point(TOO_PRECISE)) 16 | self.assertEqual(next(itr), tuple([round(c, 6) for c in TOO_PRECISE])) 17 | 18 | def test_dict(self): 19 | itr = coords({'type': 'Point', 'coordinates': [-115.81, 37.24]}) 20 | self.assertEqual(next(itr), (-115.81, 37.24)) 21 | 22 | def test_point_feature(self): 23 | itr = coords(geojson.Feature(geometry=geojson.Point((-115.81, 37.24)))) 24 | self.assertEqual(next(itr), (-115.81, 37.24)) 25 | 26 | def test_multipolygon(self): 27 | g = geojson.MultiPolygon([ 28 | ([(3.78, 9.28), (-130.91, 1.52), (35.12, 72.234), (3.78, 9.28)],), 29 | ([(23.18, -34.29), (-1.31, -4.61), 30 | (3.41, 77.91), (23.18, -34.29)],)]) 31 | itr = coords(g) 32 | pairs = list(itr) 33 | self.assertEqual(pairs[0], (3.78, 9.28)) 34 | self.assertEqual(pairs[-1], (23.18, -34.29)) 35 | 36 | def test_featurecollection(self): 37 | p1 = geojson.Feature(geometry=geojson.Point((-115.11, 37.11))) 38 | p2 = geojson.Feature(geometry=geojson.Point((-115.22, 37.22))) 39 | itr = coords(geojson.FeatureCollection([p1, p2])) 40 | pairs = list(itr) 41 | self.assertEqual(pairs[0], (-115.11, 37.11)) 42 | self.assertEqual(pairs[1], (-115.22, 37.22)) 43 | 44 | def test_geometrycollection(self): 45 | p1 = geojson.Point((-115.11, 37.11)) 46 | p2 = geojson.Point((-115.22, 37.22)) 47 | ln = geojson.LineString( 48 | [(-115.3, 37.3), (-115.4, 37.4), (-115.5, 37.5)]) 49 | g = geojson.MultiPolygon([ 50 | ([(3.78, 9.28), (-130.91, 1.52), (35.12, 72.234), (3.78, 9.28)],), 51 | ([(23.18, -34.29), (-1.31, -4.61), 52 | (3.41, 77.91), (23.18, -34.29)],)]) 53 | itr = coords(geojson.GeometryCollection([p1, p2, ln, g])) 54 | pairs = set(itr) 55 | self.assertEqual(pairs, { 56 | (-115.11, 37.11), (-115.22, 37.22), 57 | (-115.3, 37.3), (-115.4, 37.4), (-115.5, 37.5), 58 | (3.78, 9.28), (-130.91, 1.52), (35.12, 72.234), (3.78, 9.28), 59 | (23.18, -34.29), (-1.31, -4.61), (3.41, 77.91), (23.18, -34.29) 60 | }) 61 | 62 | def test_map_point(self): 63 | result = map_coords(lambda x: x, geojson.Point((-115.81, 37.24))) 64 | self.assertEqual(result['type'], 'Point') 65 | self.assertEqual(result['coordinates'], (-115.81, 37.24)) 66 | 67 | def test_map_linestring(self): 68 | g = geojson.LineString( 69 | [(3.78, 9.28), (-130.91, 1.52), (35.12, 72.234), (3.78, 9.28)]) 70 | result = map_coords(lambda x: x, g) 71 | self.assertEqual(result['type'], 'LineString') 72 | self.assertEqual(result['coordinates'][0], (3.78, 9.28)) 73 | self.assertEqual(result['coordinates'][-1], (3.78, 9.28)) 74 | 75 | def test_map_polygon(self): 76 | g = geojson.Polygon([ 77 | [(3.78, 9.28), (-130.91, 1.52), (35.12, 72.234), (3.78, 9.28)], ]) 78 | result = map_coords(lambda x: x, g) 79 | self.assertEqual(result['type'], 'Polygon') 80 | self.assertEqual(result['coordinates'][0][0], (3.78, 9.28)) 81 | self.assertEqual(result['coordinates'][0][-1], (3.78, 9.28)) 82 | 83 | def test_map_multipolygon(self): 84 | g = geojson.MultiPolygon([ 85 | ([(3.78, 9.28), (-130.91, 1.52), (35.12, 72.234), (3.78, 9.28)],), 86 | ([(23.18, -34.29), (-1.31, -4.61), 87 | (3.41, 77.91), (23.18, -34.29)],)]) 88 | result = map_coords(lambda x: x, g) 89 | self.assertEqual(result['type'], 'MultiPolygon') 90 | self.assertEqual(result['coordinates'][0][0][0], (3.78, 9.28)) 91 | self.assertEqual(result['coordinates'][-1][-1][-1], (23.18, -34.29)) 92 | 93 | def test_map_feature(self): 94 | g = geojson.Feature( 95 | id='123', 96 | geometry=geojson.Point([-115.81, 37.24]) 97 | ) 98 | result = map_coords(lambda x: x, g) 99 | self.assertEqual(result['type'], 'Feature') 100 | self.assertEqual(result['id'], '123') 101 | self.assertEqual(result['geometry']['coordinates'], (-115.81, 37.24)) 102 | 103 | def test_map_invalid(self): 104 | with self.assertRaises(ValueError): 105 | map_coords(lambda x: x, {"type": ""}) 106 | -------------------------------------------------------------------------------- /tests/test_features.py: -------------------------------------------------------------------------------- 1 | from io import StringIO 2 | import unittest 3 | 4 | import geojson 5 | 6 | 7 | class FeaturesTest(unittest.TestCase): 8 | def test_protocol(self): 9 | """ 10 | A dictionary can satisfy the protocol 11 | """ 12 | f = { 13 | 'type': 'Feature', 14 | 'id': '1', 15 | 'geometry': {'type': 'Point', 'coordinates': [53.0, -4.0]}, 16 | 'properties': {'title': 'Dict 1'}, 17 | } 18 | 19 | json = geojson.dumps(f, sort_keys=True) 20 | self.assertEqual(json, '{"geometry":' 21 | ' {"coordinates": [53.0, -4.0],' 22 | ' "type": "Point"},' 23 | ' "id": "1",' 24 | ' "properties": {"title": "Dict 1"},' 25 | ' "type": "Feature"}') 26 | 27 | o = geojson.loads(json) 28 | output = geojson.dumps(o, sort_keys=True) 29 | self.assertEqual(output, '{"geometry":' 30 | ' {"coordinates": [53.0, -4.0],' 31 | ' "type": "Point"},' 32 | ' "id": "1",' 33 | ' "properties": {"title": "Dict 1"},' 34 | ' "type": "Feature"}') 35 | 36 | def test_unicode_properties(self): 37 | with open("tests/data.geojson") as file_: 38 | obj = geojson.load(file_) 39 | geojson.dump(obj, StringIO()) 40 | 41 | def test_feature_class(self): 42 | """ 43 | Test the Feature class 44 | """ 45 | 46 | from geojson.examples import SimpleWebFeature 47 | feature = SimpleWebFeature( 48 | id='1', 49 | geometry={'type': 'Point', 'coordinates': [53.0, -4.0]}, 50 | title='Feature 1', summary='The first feature', 51 | link='http://example.org/features/1' 52 | ) 53 | 54 | # It satisfies the feature protocol 55 | self.assertEqual(feature.id, '1') 56 | self.assertEqual(feature.properties['title'], 'Feature 1') 57 | self.assertEqual(feature.properties['summary'], 'The first feature') 58 | self.assertEqual(feature.properties['link'], 59 | 'http://example.org/features/1') 60 | self.assertEqual(geojson.dumps(feature.geometry, sort_keys=True), 61 | '{"coordinates": [53.0, -4.0], "type": "Point"}') 62 | 63 | # Encoding 64 | json = ('{"geometry": {"coordinates": [53.0, -4.0],' 65 | ' "type": "Point"},' 66 | ' "id": "1",' 67 | ' "properties":' 68 | ' {"link": "http://example.org/features/1",' 69 | ' "summary": "The first feature",' 70 | ' "title": "Feature 1"},' 71 | ' "type": "Feature"}') 72 | self.assertEqual(geojson.dumps(feature, sort_keys=True), json) 73 | 74 | # Decoding 75 | factory = geojson.examples.create_simple_web_feature 76 | json = ('{"geometry": {"type": "Point",' 77 | ' "coordinates": [53.0, -4.0]},' 78 | ' "id": "1",' 79 | ' "properties": {"summary": "The first feature",' 80 | ' "link": "http://example.org/features/1",' 81 | ' "title": "Feature 1"}}') 82 | feature = geojson.loads(json, object_hook=factory) 83 | self.assertEqual(repr(type(feature)), 84 | "") 85 | self.assertEqual(feature.id, '1') 86 | self.assertEqual(feature.properties['title'], 'Feature 1') 87 | self.assertEqual(feature.properties['summary'], 'The first feature') 88 | self.assertEqual(feature.properties['link'], 89 | 'http://example.org/features/1') 90 | self.assertEqual(geojson.dumps(feature.geometry, sort_keys=True), 91 | '{"coordinates": [53.0, -4.0], "type": "Point"}') 92 | 93 | def test_geo_interface(self): 94 | class Thingy: 95 | def __init__(self, id, title, x, y): 96 | self.id = id 97 | self.title = title 98 | self.x = x 99 | self.y = y 100 | 101 | @property 102 | def __geo_interface__(self): 103 | return ({"id": self.id, 104 | "properties": {"title": self.title}, 105 | "geometry": {"type": "Point", 106 | "coordinates": (self.x, self.y)}}) 107 | 108 | ob = Thingy('1', 'thingy one', -106.0, 40.0) 109 | self.assertEqual(geojson.dumps(ob.__geo_interface__['geometry'], 110 | sort_keys=True), 111 | '{"coordinates": [-106.0, 40.0], "type": "Point"}') 112 | self.assertEqual(geojson.dumps(ob, sort_keys=True), 113 | ('{"geometry": {"coordinates": [-106.0, 40.0],' 114 | ' "type": "Point"},' 115 | ' "id": "1",' 116 | ' "properties": {"title": "thingy one"}}')) 117 | -------------------------------------------------------------------------------- /tests/test_geo_interface.py: -------------------------------------------------------------------------------- 1 | """ 2 | Encoding/decoding custom objects with __geo_interface__ 3 | """ 4 | import unittest 5 | 6 | import geojson 7 | from geojson.mapping import to_mapping 8 | 9 | 10 | class EncodingDecodingTest(unittest.TestCase): 11 | 12 | def setUp(self): 13 | class Restaurant: 14 | """ 15 | Basic Restaurant class 16 | """ 17 | def __init__(self, name, latlng): 18 | super().__init__() 19 | self.name = name 20 | self.latlng = latlng 21 | 22 | class Restaurant1(Restaurant): 23 | """ 24 | Extends Restaurant with __geo_interface__ returning dict 25 | """ 26 | @property 27 | def __geo_interface__(self): 28 | return {'type': "Point", 'coordinates': self.latlng} 29 | 30 | class Restaurant2(Restaurant): 31 | """ 32 | Extends Restaurant with __geo_interface__ returning another 33 | __geo_interface__ object 34 | """ 35 | @property 36 | def __geo_interface__(self): 37 | return geojson.Point(self.latlng) 38 | 39 | class RestaurantFeature1(Restaurant): 40 | """ 41 | Extends Restaurant with __geo_interface__ returning dict 42 | """ 43 | @property 44 | def __geo_interface__(self): 45 | return { 46 | 'geometry': { 47 | 'type': "Point", 48 | 'coordinates': self.latlng, 49 | }, 50 | 'type': "Feature", 51 | 'properties': { 52 | 'name': self.name, 53 | }, 54 | } 55 | 56 | class RestaurantFeature2(Restaurant): 57 | """ 58 | Extends Restaurant with __geo_interface__ returning another 59 | __geo_interface__ object 60 | """ 61 | @property 62 | def __geo_interface__(self): 63 | return geojson.Feature( 64 | geometry=geojson.Point(self.latlng), 65 | properties={'name': self.name}) 66 | 67 | self.name = "In N Out Burger" 68 | self.latlng = [-54.0, 4.0] 69 | 70 | self.restaurant_nogeo = Restaurant(self.name, self.latlng) 71 | 72 | self.restaurant1 = Restaurant1(self.name, self.latlng) 73 | self.restaurant2 = Restaurant2(self.name, self.latlng) 74 | 75 | self.restaurant_str = ('{"coordinates": [-54.0, 4.0],' 76 | ' "type": "Point"}') 77 | 78 | self.restaurant_feature1 = RestaurantFeature1(self.name, self.latlng) 79 | self.restaurant_feature2 = RestaurantFeature2(self.name, self.latlng) 80 | 81 | self.restaurant_feature_str = ('{"geometry":' 82 | ' {"coordinates": [-54.0, 4.0],' 83 | ' "type": "Point"},' 84 | ' "properties":' 85 | ' {"name": "In N Out Burger"},' 86 | ' "type": "Feature"}') 87 | 88 | def test_encode(self): 89 | """ 90 | Ensure objects that implement __geo_interface__ can be encoded into 91 | GeoJSON strings 92 | """ 93 | actual = geojson.dumps(self.restaurant1, sort_keys=True) 94 | self.assertEqual(actual, self.restaurant_str) 95 | 96 | actual = geojson.dumps(self.restaurant2, sort_keys=True) 97 | self.assertEqual(actual, self.restaurant_str) 98 | 99 | # Classes that don't implement geo interface should raise TypeError 100 | with self.assertRaises(TypeError): 101 | geojson.dumps(self.restaurant_nogeo) 102 | 103 | def test_encode_nested(self): 104 | """ 105 | Ensure nested objects that implement __geo_interface__ can be encoded 106 | into GeoJSON strings 107 | """ 108 | actual = geojson.dumps(self.restaurant_feature1, sort_keys=True) 109 | self.assertEqual(actual, self.restaurant_feature_str) 110 | 111 | actual = geojson.dumps(self.restaurant_feature2, sort_keys=True) 112 | self.assertEqual(actual, self.restaurant_feature_str) 113 | 114 | def test_decode(self): 115 | """ 116 | Ensure a GeoJSON string can be decoded into GeoJSON objects 117 | """ 118 | actual = geojson.loads(self.restaurant_str) 119 | expected = self.restaurant1.__geo_interface__ 120 | self.assertEqual(expected, actual) 121 | 122 | def test_decode_nested(self): 123 | """ 124 | Ensure a GeoJSON string can be decoded into nested GeoJSON objects 125 | """ 126 | actual = geojson.loads(self.restaurant_feature_str) 127 | expected = self.restaurant_feature1.__geo_interface__ 128 | self.assertEqual(expected, actual) 129 | 130 | nested = expected['geometry'] 131 | expected = self.restaurant1.__geo_interface__ 132 | self.assertEqual(nested, expected) 133 | 134 | def test_invalid(self): 135 | with self.assertRaises(ValueError) as cm: 136 | geojson.loads('{"type":"Point", "coordinates":[[-Infinity, 4.0]]}') 137 | 138 | self.assertIn('is not JSON compliant', str(cm.exception)) 139 | 140 | def test_mapping(self): 141 | self.assertEqual(to_mapping(geojson.Point([1.0, 2.0])), 142 | {"coordinates": [1.0, 2.0], "type": "Point"}) 143 | 144 | def test_GeoJSON(self): 145 | self.assertEqual(None, geojson.GeoJSON().__geo_interface__) 146 | 147 | self.assertEqual({"type": "GeoJSON"}, to_mapping(geojson.GeoJSON())) 148 | -------------------------------------------------------------------------------- /tests/test_null_geometries.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import geojson 4 | 5 | 6 | class NullGeometriesTest(unittest.TestCase): 7 | 8 | def test_null_geometry_explicit(self): 9 | feature = geojson.Feature( 10 | id=12, 11 | geometry=None, 12 | properties={'foo': 'bar'} 13 | ) 14 | actual = geojson.dumps(feature, sort_keys=True) 15 | expected = ('{"geometry": null, "id": 12, "properties": {"foo": ' 16 | '"bar"}, "type": "Feature"}') 17 | self.assertEqual(actual, expected) 18 | 19 | def test_null_geometry_implicit(self): 20 | feature = geojson.Feature( 21 | id=12, 22 | properties={'foo': 'bar'} 23 | ) 24 | actual = geojson.dumps(feature, sort_keys=True) 25 | expected = ('{"geometry": null, "id": 12, "properties": {"foo": ' 26 | '"bar"}, "type": "Feature"}') 27 | self.assertEqual(actual, expected) 28 | -------------------------------------------------------------------------------- /tests/test_strict_json.py: -------------------------------------------------------------------------------- 1 | """ 2 | GeoJSON produces and consumes only strict JSON. NAN and Infinity are not 3 | permissible values according to the JSON specification. 4 | """ 5 | import unittest 6 | 7 | import geojson 8 | 9 | 10 | class StrictJsonTest(unittest.TestCase): 11 | def test_encode_nan(self): 12 | """ 13 | Ensure Error is raised when encoding nan 14 | """ 15 | self._raises_on_dump({ 16 | "type": "Point", 17 | "coordinates": (float("nan"), 1.0), 18 | }) 19 | 20 | def test_encode_inf(self): 21 | """ 22 | Ensure Error is raised when encoding inf or -inf 23 | """ 24 | self._raises_on_dump({ 25 | "type": "Point", 26 | "coordinates": (float("inf"), 1.0), 27 | }) 28 | 29 | self._raises_on_dump({ 30 | "type": "Point", 31 | "coordinates": (float("-inf"), 1.0), 32 | }) 33 | 34 | def _raises_on_dump(self, unstrict): 35 | with self.assertRaises(ValueError): 36 | geojson.dumps(unstrict) 37 | 38 | def test_decode_nan(self): 39 | """ 40 | Ensure Error is raised when decoding NaN 41 | """ 42 | self._raises_on_load('{"type": "Point", "coordinates": [1.0, NaN]}') 43 | 44 | def test_decode_inf(self): 45 | """ 46 | Ensure Error is raised when decoding Infinity or -Infinity 47 | """ 48 | self._raises_on_load( 49 | '{"type": "Point", "coordinates": [1.0, Infinity]}') 50 | 51 | self._raises_on_load( 52 | '{"type": "Point", "coordinates": [1.0, -Infinity]}') 53 | 54 | def _raises_on_load(self, unstrict): 55 | with self.assertRaises(ValueError): 56 | geojson.loads(unstrict) 57 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for geojson generation 3 | """ 4 | 5 | import random 6 | import unittest 7 | 8 | import geojson 9 | from geojson.utils import generate_random, map_geometries 10 | 11 | 12 | def generate_bbox(): 13 | min_lat = random.random() * 180.0 - 90.0 14 | max_lat = random.random() * 180.0 - 90.0 15 | if min_lat > max_lat: 16 | min_lat, max_lat = max_lat, min_lat 17 | min_lon = random.random() * 360.0 - 180.0 18 | max_lon = random.random() * 360.0 - 180.0 19 | if min_lon > max_lon: 20 | min_lon, max_lon = max_lon, min_lon 21 | return [min_lon, min_lat, max_lon, max_lat] 22 | 23 | 24 | def check_polygon_bbox(polygon, bbox): 25 | min_lon, min_lat, max_lon, max_lat = bbox 26 | eps = 1e-3 27 | for linear_ring in polygon['coordinates']: 28 | for coordinate in linear_ring: 29 | if not (min_lon-eps <= coordinate[0] <= max_lon+eps 30 | and min_lat-eps <= coordinate[1] <= max_lat+eps): 31 | return False 32 | return True 33 | 34 | 35 | def check_point_bbox(point, bbox): 36 | min_lon, min_lat, max_lon, max_lat = bbox 37 | eps = 1e-3 38 | if ( 39 | min_lon - eps <= point["coordinates"][0] <= max_lon + eps 40 | and min_lat - eps <= point["coordinates"][1] <= max_lat + eps 41 | ): 42 | return True 43 | return False 44 | 45 | 46 | class TestGenerateRandom(unittest.TestCase): 47 | def test_simple_polygon(self): 48 | for _ in range(5000): 49 | bbox = [-180.0, -90.0, 180.0, 90.0] 50 | result = generate_random('Polygon') 51 | self.assertIsInstance(result, geojson.geometry.Polygon) 52 | self.assertTrue(geojson.geometry.check_polygon(result)) 53 | self.assertTrue(check_polygon_bbox(result, bbox)) 54 | 55 | def test_bbox_polygon(self): 56 | for _ in range(5000): 57 | bbox = generate_bbox() 58 | result = generate_random('Polygon', boundingBox=bbox) 59 | self.assertIsInstance(result, geojson.geometry.Polygon) 60 | self.assertTrue(geojson.geometry.check_polygon(result)) 61 | self.assertTrue(check_polygon_bbox(result, bbox)) 62 | 63 | def test_bbox_point(self): 64 | for _ in range(5000): 65 | bbox = generate_bbox() 66 | result = generate_random("Point", boundingBox=bbox) 67 | self.assertIsInstance(result, geojson.geometry.Point) 68 | self.assertTrue(geojson.geometry.check_point(result)) 69 | self.assertTrue(check_point_bbox(result, bbox)) 70 | 71 | def test_raise_value_error(self): 72 | with self.assertRaises(ValueError): 73 | generate_random("MultiPolygon") 74 | 75 | 76 | class TestMapGeometries(unittest.TestCase): 77 | def test_with_simple_type(self): 78 | new_point = map_geometries( 79 | lambda g: geojson.MultiPoint([g["coordinates"]]), 80 | geojson.Point((-115.81, 37.24)), 81 | ) 82 | self.assertEqual(new_point, geojson.MultiPoint([(-115.81, 37.24)])) 83 | 84 | def test_with_feature_collection(self): 85 | new_point = map_geometries( 86 | lambda g: geojson.MultiPoint([g["coordinates"]]), 87 | geojson.FeatureCollection([geojson.Point((-115.81, 37.24))]), 88 | ) 89 | self.assertEqual( 90 | new_point, 91 | geojson.FeatureCollection( 92 | [geojson.MultiPoint([geojson.Point((-115.81, 37.24))])] 93 | ), 94 | ) 95 | 96 | def test_raise_value_error(self): 97 | invalid_object = geojson.Feature(type="InvalidType") 98 | with self.assertRaises(ValueError): 99 | map_geometries(lambda g: g, invalid_object) 100 | -------------------------------------------------------------------------------- /tests/test_validation.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for geojson/validation 3 | """ 4 | 5 | import unittest 6 | 7 | import geojson 8 | 9 | INVALID_POS = (10, 20, 30, 40) 10 | VALID_POS = (10, 20) 11 | VALID_POS_WITH_ELEVATION = (10, 20, 30) 12 | 13 | 14 | class TestValidationGeometry(unittest.TestCase): 15 | 16 | def test_invalid_geometry_with_validate(self): 17 | with self.assertRaises(ValueError): 18 | geojson.Point(INVALID_POS, validate=True) 19 | 20 | def test_invalid_geometry_without_validate(self): 21 | geojson.Point(INVALID_POS) 22 | geojson.Point(INVALID_POS, validate=False) 23 | 24 | def test_valid_geometry(self): 25 | geojson.Point(VALID_POS, validate=True) 26 | 27 | def test_valid_geometry_with_elevation(self): 28 | geojson.Point(VALID_POS_WITH_ELEVATION, validate=True) 29 | 30 | def test_not_sequence(self): 31 | with self.assertRaises(ValueError) as cm: 32 | geojson.MultiPoint([5], validate=True) 33 | 34 | self.assertIn('each position must be a list', str(cm.exception)) 35 | 36 | def test_not_number(self): 37 | with self.assertRaises(ValueError) as cm: 38 | geojson.MultiPoint([[1, '?']], validate=False) 39 | 40 | self.assertIn('is not a JSON compliant number', str(cm.exception)) 41 | 42 | 43 | class TestValidationGeoJSONObject(unittest.TestCase): 44 | 45 | def test_valid_jsonobject(self): 46 | point = geojson.Point((-10.52, 2.33)) 47 | self.assertEqual(point.is_valid, True) 48 | 49 | 50 | class TestValidationPoint(unittest.TestCase): 51 | 52 | def test_invalid_point(self): 53 | point = geojson.Point((10, 20, 30, 40)) 54 | self.assertEqual(point.is_valid, False) 55 | 56 | point = geojson.Point([(10, 20), (30, 40)]) 57 | self.assertEqual(point.is_valid, False) 58 | 59 | def test_valid_point(self): 60 | point = geojson.Point((-3.68, 40.41)) 61 | self.assertEqual(point.is_valid, True) 62 | 63 | def test_valid_point_with_elevation(self): 64 | point = geojson.Point((-3.68, 40.41, 3.45)) 65 | self.assertEqual(point.is_valid, True) 66 | 67 | 68 | class TestValidationMultipoint(unittest.TestCase): 69 | 70 | def test_invalid_multipoint(self): 71 | mpoint = geojson.MultiPoint( 72 | [(3.5887,), (3.5887, 10.44558), 73 | (2.5555, 3.887, 4.56), (2.44, 3.44, 2.555, 4.56)]) 74 | self.assertEqual(mpoint.is_valid, False) 75 | 76 | def test_valid_multipoint(self): 77 | mpoint = geojson.MultiPoint([(10, 20), (30, 40)]) 78 | self.assertEqual(mpoint.is_valid, True) 79 | 80 | def test_valid_multipoint_with_elevation(self): 81 | mpoint = geojson.MultiPoint([(10, 20, 30), (30, 40, 50)]) 82 | self.assertEqual(mpoint.is_valid, True) 83 | 84 | 85 | class TestValidationLineString(unittest.TestCase): 86 | 87 | def test_invalid_linestring(self): 88 | with self.assertRaises(ValueError) as cm: 89 | geojson.LineString([(8.919, 44.4074)], validate=True) 90 | 91 | self.assertIn('must be an array of two or more positions', 92 | str(cm.exception)) 93 | 94 | with self.assertRaises(ValueError) as cm: 95 | geojson.LineString([(8.919, 44.4074), [3]], validate=True) 96 | 97 | self.assertIn('a position must have exactly 2 or 3 values', 98 | str(cm.exception)) 99 | 100 | def test_valid_linestring(self): 101 | ls = geojson.LineString([(10, 5), (4, 3)]) 102 | self.assertEqual(ls.is_valid, True) 103 | 104 | 105 | class TestValidationMultiLineString(unittest.TestCase): 106 | 107 | def test_invalid_multilinestring(self): 108 | with self.assertRaises(ValueError) as cm: 109 | geojson.MultiLineString([1], validate=True) 110 | 111 | self.assertIn('each line must be a list of positions', 112 | str(cm.exception)) 113 | 114 | mls = geojson.MultiLineString([[(10, 5), (20, 1)], []]) 115 | self.assertEqual(mls.is_valid, False) 116 | 117 | def test_valid_multilinestring(self): 118 | ls1 = [(3.75, 9.25), (-130.95, 1.52)] 119 | ls2 = [(23.15, -34.25), (-1.35, -4.65), (3.45, 77.95)] 120 | mls = geojson.MultiLineString([ls1, ls2]) 121 | self.assertEqual(mls.is_valid, True) 122 | 123 | 124 | class TestValidationPolygon(unittest.TestCase): 125 | 126 | def test_invalid_polygon(self): 127 | with self.assertRaises(ValueError) as cm: 128 | geojson.Polygon([1], validate=True) 129 | 130 | self.assertIn("Each element of a polygon's coordinates must be a list", 131 | str(cm.exception)) 132 | 133 | poly1 = geojson.Polygon( 134 | [[(2.38, 57.322), (23.194, -20.28), (-120.43, 19.15)]]) 135 | self.assertEqual(poly1.is_valid, False) 136 | poly2 = geojson.Polygon( 137 | [[(2.38, 57.322), (23.194, -20.28), 138 | (-120.43, 19.15), (2.38, 57.323)]]) 139 | self.assertEqual(poly2.is_valid, False) 140 | 141 | def test_valid_polygon(self): 142 | poly = geojson.Polygon( 143 | [[(2.38, 57.322), (23.194, -20.28), 144 | (-120.43, 19.15), (2.38, 57.322)]]) 145 | self.assertEqual(poly.is_valid, True) 146 | 147 | 148 | class TestValidationMultiPolygon(unittest.TestCase): 149 | 150 | def test_invalid_multipolygon(self): 151 | with self.assertRaises(ValueError) as cm: 152 | geojson.MultiPolygon([1], validate=True) 153 | 154 | self.assertIn("Each polygon must be a list of linear rings", 155 | str(cm.exception)) 156 | 157 | poly1 = [(2.38, 57.322), (23.194, -20.28), 158 | (-120.43, 19.15), (25.44, -17.91)] 159 | poly2 = [(2.38, 57.322), (23.194, -20.28), 160 | (-120.43, 19.15), (2.38, 57.322)] 161 | multipoly = geojson.MultiPolygon([poly1, poly2]) 162 | self.assertEqual(multipoly.is_valid, False) 163 | 164 | def test_valid_multipolygon(self): 165 | poly1 = [[(2.38, 57.322), (23.194, -20.28), 166 | (-120.43, 19.15), (2.38, 57.322)]] 167 | poly2 = [[(-5.34, 3.71), (28.74, 31.44), 168 | (28.55, 19.10), (-5.34, 3.71)]] 169 | poly3 = [[(3.14, 23.17), (51.34, 27.14), 170 | (22, -18.11), (3.14, 23.17)]] 171 | multipoly = geojson.MultiPolygon([poly1, poly2, poly3]) 172 | self.assertEqual(multipoly.is_valid, True) 173 | 174 | 175 | class TestValidationGeometryCollection(unittest.TestCase): 176 | 177 | def test_invalid_geometrycollection(self): 178 | point = geojson.Point((10, 20)) 179 | bad_poly = geojson.Polygon([[(2.38, 57.322), (23.194, -20.28), 180 | (-120.43, 19.15), (25.44, -17.91)]]) 181 | 182 | geom_collection = geojson.GeometryCollection( 183 | geometries=[point, bad_poly] 184 | ) 185 | self.assertFalse(geom_collection.is_valid) 186 | 187 | def test_valid_geometrycollection(self): 188 | point = geojson.Point((10, 20)) 189 | poly = geojson.Polygon([[(2.38, 57.322), (23.194, -20.28), 190 | (-120.43, 19.15), (2.38, 57.322)]]) 191 | 192 | geom_collection = geojson.GeometryCollection( 193 | geometries=[point, poly] 194 | ) 195 | self.assertTrue(geom_collection.is_valid) 196 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | requires = 3 | tox>=4.2 4 | env_list = 5 | py{py3, 313, 312, 311, 310, 39, 38, 37} 6 | 7 | [testenv] 8 | deps = 9 | pytest 10 | pytest-cov 11 | pass_env = 12 | FORCE_COLOR 13 | commands = 14 | {envpython} -m pytest --cov geojson --cov tests --cov-report xml {posargs} 15 | --------------------------------------------------------------------------------