summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2018-09-11 19:47:02 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2018-09-11 19:47:02 +0000
commit07fc060ddfc10509ca76910885fc6c74bbe39f68 (patch)
tree26c3d78f2bb7be8faecb8a2ae3f94c89495c1090
parentInitial commit. (diff)
downloadpython-geojson-07fc060ddfc10509ca76910885fc6c74bbe39f68.zip
python-geojson-07fc060ddfc10509ca76910885fc6c74bbe39f68.tar.xz
Adding upstream version 1.3.4.upstream/1.3.4
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
-rw-r--r--.gitignore8
-rw-r--r--.travis.yml18
-rw-r--r--CHANGELOG.rst149
-rw-r--r--LICENSE.rst11
-rw-r--r--MANIFEST.in2
-rw-r--r--README.rst327
-rw-r--r--geojson/__init__.py19
-rw-r--r--geojson/_version.py2
-rw-r--r--geojson/base.py127
-rw-r--r--geojson/codec.py59
-rw-r--r--geojson/crs.py39
-rw-r--r--geojson/examples.py70
-rw-r--r--geojson/factory.py15
-rw-r--r--geojson/feature.py48
-rw-r--r--geojson/geometry.py88
-rw-r--r--geojson/mapping.py41
-rw-r--r--geojson/utils.py157
-rw-r--r--geojson/validation.py100
-rw-r--r--setup.cfg2
-rw-r--r--setup.py75
-rw-r--r--tests/__init__.py12
-rw-r--r--tests/data.geojson7
-rw-r--r--tests/test_base.py57
-rw-r--r--tests/test_coords.py59
-rw-r--r--tests/test_crs.py30
-rw-r--r--tests/test_features.py116
-rw-r--r--tests/test_geo_interface.py131
-rw-r--r--tests/test_null_geometries.py27
-rw-r--r--tests/test_strict_json.py52
-rw-r--r--tests/test_validation.py130
-rw-r--r--tox.ini10
31 files changed, 1988 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..969b14d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+__pycache__/
+*.py[cod]
+build/
+dist/
+sdist/
+*.egg-info/
+*.egg
+.tox/
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..f3c75a1
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,18 @@
+language: "python"
+python:
+ - "2.7"
+ - "3.3"
+ - "3.4"
+ - "3.5"
+ - "pypy"
+ - "pypy3"
+install:
+ - pip install flake8
+ - pip install codecov
+before_script:
+ flake8 .
+script:
+ - coverage run --branch --source=geojson setup.py test
+ - coverage xml
+after_success:
+ - codecov
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
new file mode 100644
index 0000000..9b9d4cd
--- /dev/null
+++ b/CHANGELOG.rst
@@ -0,0 +1,149 @@
+Changes
+=======
+
+1.3.4 (2017-02-11)
+------------------
+
+- Remove runtime dependency on setuptools
+
+ - https://github.com/frewsxcv/python-geojson/pull/90
+
+1.3.3 (2016-07-21)
+------------------
+
+- Add validate parameter to GeoJSON constructors
+
+ - https://github.com/frewsxcv/python-geojson/pull/78
+
+1.3.2 (2016-01-28)
+------------------
+
+- Add __version__ and __version_info__ attributes
+
+ - https://github.com/frewsxcv/python-geojson/pull/74
+
+1.3.1 (2015-10-12)
+------------------
+
+- Fix validation bug for MultiPolygons
+
+ - https://github.com/frewsxcv/python-geojson/pull/63
+
+1.3.0 (2015-08-11)
+------------------
+
+- Add utility to generate geometries with random data
+
+ - https://github.com/frewsxcv/python-geojson/pull/60
+
+1.2.2 (2015-07-13)
+------------------
+
+- Fix tests by including test file into build
+
+ - https://github.com/frewsxcv/python-geojson/issues/61
+
+- Build universal wheels
+
+ - https://packaging.python.org/en/latest/distributing.html#universal-wheels
+
+1.2.1 (2015-06-25)
+------------------
+
+- Encode long types correctly with Python 2.x
+
+ - https://github.com/frewsxcv/python-geojson/pull/57
+
+1.2.0 (2015-06-19)
+------------------
+
+- Utility function to validate GeoJSON objects
+
+ - https://github.com/frewsxcv/python-geojson/pull/56
+
+1.1.0 (2015-06-08)
+------------------
+
+- Stop outputting invalid GeoJSON value id=null on Features
+
+ - https://github.com/frewsxcv/python-geojson/pull/53
+
+1.0.9 (2014-10-05)
+------------------
+
+- Fix bug where unicode/non-string properties with a 'type' key cause a crash
+
+1.0.8 (2014-09-30)
+------------------
+
+- Fix bug where unicode keys don't get decoded properly
+- Add coords and map_coords utilities
+
+1.0.7 (2014-04-19)
+------------------
+
+- Compatibility with Python 3.4
+- Remove nose dependency
+- Convert doctests to unittests
+- Run tests using runtests.sh
+
+1.0.6 (2014-01-18)
+------------------
+
+- Update README.rst documentation (fix errors, add examples)
+- Allow simplejson to be used again
+
+1.0.5 (2013-11-16)
+------------------
+
+- Remove warning about RSTs in test/ upon install
+
+1.0.4 (2013-11-16)
+------------------
+
+- Flake8 everything
+- Transition all documentation to reStructuredText
+- Start using Travis CI
+- Support Python 3
+- Fix broken testcase when run using Python 2.6
+
+1.0.3 (2009-11-25)
+------------------
+
+- Fixed #186
+- Internal code simplification
+
+1.0.2 (2009-11-24)
+------------------
+
+- Use nose test framework instead of rolling our own.
+
+1.0.1 (2008-12-19)
+------------------
+
+- Handle features with null geometries (#174).
+
+1.0 (2008-08-01)
+----------------
+
+- Final 1.0 release.
+- Rename PyGFPEncoder to GeoJSONEncoder and expose it from the geojson module.
+
+1.0rc1 (2008-07-11)
+-------------------
+
+- Release candidate.
+
+1.0b1 (2008-07-02)
+------------------
+
+- Rename encoding module to codec.
+
+1.0a4 (2008-04-27)
+------------------
+
+- Get in step with GeoJSON draft version 6.
+- Made all code work with Python 2.4.3, 2.5.1, will test with all variations.
+ (see tests/rundoctests.dist)
+- Made tests use ELLIPSIS to avoid output transmogification due to floating
+ point representation.
diff --git a/LICENSE.rst b/LICENSE.rst
new file mode 100644
index 0000000..0caa10c
--- /dev/null
+++ b/LICENSE.rst
@@ -0,0 +1,11 @@
+Copyright © 2014, contributors of python-geojson
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+- Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+- 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.
+- Neither the name of the python-geojson nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+
+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 PYTHON-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.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..2407291
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,2 @@
+include *.rst
+recursive-include tests *.txt *.py *.geojson
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..ebc7ffd
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,327 @@
+python-geojson
+==============
+
+.. image:: https://img.shields.io/travis/frewsxcv/python-geojson.svg
+ :target: https://travis-ci.org/frewsxcv/python-geojson
+.. image:: https://img.shields.io/codecov/c/github/frewsxcv/python-geojson.svg
+ :target: https://codecov.io/github/frewsxcv/python-geojson?branch=master
+
+This library contains:
+
+- Functions for encoding and decoding GeoJSON_ formatted data
+- Classes for all GeoJSON Objects
+- An implementation of the Python `__geo_interface__ Specification`_
+
+**Table of Contents**
+
+.. contents::
+ :backlinks: none
+ :local:
+
+Installation
+------------
+
+python-geojson is compatible with Python 2.6, 2.7, 3.2, 3.3, 3.4 and 3.5. It is listed on `PyPi as 'geojson'`_. The recommended way to install is via pip_:
+
+.. code::
+
+ pip install geojson
+
+.. _PyPi as 'geojson': https://pypi.python.org/pypi/geojson/
+.. _pip: http://www.pip-installer.org
+
+GeoJSON Objects
+---------------
+
+This library implements all the `GeoJSON Objects`_ described in `The GeoJSON Format Specification`_.
+
+.. _GeoJSON Objects: http://www.geojson.org/geojson-spec.html#geojson-objects
+
+Point
+~~~~~
+
+.. code:: python
+
+ >>> from geojson import Point
+
+ >>> Point((-115.81, 37.24)) # doctest: +ELLIPSIS
+ {"coordinates": [-115.8..., 37.2...], "type": "Point"}
+
+Visualize the result of the example above `here <https://gist.github.com/frewsxcv/b5768a857f5598e405fa>`__. General information about Point can be found in `Section 2.1.2`_ and `Appendix A: Point`_ within `The GeoJSON Format Specification`_.
+
+.. _Section 2.1.2: http://www.geojson.org/geojson-spec.html#point
+.. _Appendix A\: Point: http://www.geojson.org/geojson-spec.html#id2
+
+MultiPoint
+~~~~~~~~~~
+
+.. code:: python
+
+ >>> from geojson import MultiPoint
+
+ >>> MultiPoint([(-155.52, 19.61), (-156.22, 20.74), (-157.97, 21.46)]) # doctest: +ELLIPSIS
+ {"coordinates": [[-155.5..., 19.6...], [-156.2..., 20.7...], [-157.9..., 21.4...]], "type": "MultiPoint"}
+
+Visualize the result of the example above `here <https://gist.github.com/frewsxcv/be02025c1eb3aa2040ee>`__. General information about MultiPoint can be found in `Section 2.1.3`_ and `Appendix A: MultiPoint`_ within `The GeoJSON Format Specification`_.
+
+.. _Section 2.1.3: http://www.geojson.org/geojson-spec.html#multipoint
+.. _Appendix A\: MultiPoint: http://www.geojson.org/geojson-spec.html#id5
+
+
+LineString
+~~~~~~~~~~
+
+.. code:: python
+
+ >>> from geojson import LineString
+
+ >>> LineString([(8.919, 44.4074), (8.923, 44.4075)]) # doctest: +ELLIPSIS
+ {"coordinates": [[8.91..., 44.407...], [8.92..., 44.407...]], "type": "LineString"}
+
+Visualize the result of the example above `here <https://gist.github.com/frewsxcv/758563182ca49ce8e8bb>`__. General information about LineString can be found in `Section 2.1.4`_ and `Appendix A: LineString`_ within `The GeoJSON Format Specification`_.
+
+.. _Section 2.1.4: http://www.geojson.org/geojson-spec.html#linestring
+.. _Appendix A\: LineString: http://www.geojson.org/geojson-spec.html#id3
+
+MultiLineString
+~~~~~~~~~~~~~~~
+
+.. code:: python
+
+ >>> from geojson import MultiLineString
+
+ >>> MultiLineString([
+ ... [(3.75, 9.25), (-130.95, 1.52)],
+ ... [(23.15, -34.25), (-1.35, -4.65), (3.45, 77.95)]
+ ... ]) # doctest: +ELLIPSIS
+ {"coordinates": [[[3.7..., 9.2...], [-130.9..., 1.52...]], [[23.1..., -34.2...], [-1.3..., -4.6...], [3.4..., 77.9...]]], "type": "MultiLineString"}
+
+Visualize the result of the example above `here <https://gist.github.com/frewsxcv/20b6522d8242ede00bb3>`__. General information about MultiLineString can be found in `Section 2.1.5`_ and `Appendix A: MultiLineString`_ within `The GeoJSON Format Specification`_.
+
+.. _Section 2.1.5: http://www.geojson.org/geojson-spec.html#multilinestring
+.. _Appendix A\: MultiLineString: http://www.geojson.org/geojson-spec.html#id6
+
+Polygon
+~~~~~~~
+
+.. code:: python
+
+ >>> from geojson import Polygon
+
+ >>> # no hole within polygon
+ >>> Polygon([[(2.38, 57.322), (23.194, -20.28), (-120.43, 19.15), (2.38, 57.322)]]) # doctest: +ELLIPSIS
+ {"coordinates": [[[2.3..., 57.32...], [23.19..., -20.2...], [-120.4..., 19.1...]]], "type": "Polygon"}
+
+ >>> # hole within polygon
+ >>> Polygon([
+ ... [(2.38, 57.322), (23.194, -20.28), (-120.43, 19.15), (2.38, 57.322)],
+ ... [(-5.21, 23.51), (15.21, -10.81), (-20.51, 1.51), (-5.21, 23.51)]
+ ... ]) # doctest: +ELLIPSIS
+ {"coordinates": [[[2.3..., 57.32...], [23.19..., -20.2...], [-120.4..., 19.1...]], [[-5.2..., 23.5...], [15.2..., -10.8...], [-20.5..., 1.5...], [-5.2..., 23.5...]]], "type": "Polygon"}
+
+Visualize the results of the example above `here <https://gist.github.com/frewsxcv/b2f5c31c10e399a63679>`__. General information about Polygon can be found in `Section 2.1.6`_ and `Appendix A: Polygon`_ within `The GeoJSON Format Specification`_.
+
+.. _Section 2.1.6: http://www.geojson.org/geojson-spec.html#polygon
+.. _Appendix A\: Polygon: http://www.geojson.org/geojson-spec.html#id4
+
+MultiPolygon
+~~~~~~~~~~~~
+
+.. code:: python
+
+ >>> from geojson import MultiPolygon
+
+ >>> MultiPolygon([
+ ... ([(3.78, 9.28), (-130.91, 1.52), (35.12, 72.234), (3.78, 9.28)],),
+ ... ([(23.18, -34.29), (-1.31, -4.61), (3.41, 77.91), (23.18, -34.29)],)
+ ... ]) # doctest: +ELLIPSIS
+ {"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"}
+
+Visualize the result of the example above `here <https://gist.github.com/frewsxcv/e0388485e28392870b74>`__. General information about MultiPolygon can be found in `Section 2.1.7`_ and `Appendix A: MultiPolygon`_ within `The GeoJSON Format Specification`_.
+
+.. _Section 2.1.7: http://www.geojson.org/geojson-spec.html#multipolygon
+.. _Appendix A\: MultiPolygon: http://www.geojson.org/geojson-spec.html#id7
+
+GeometryCollection
+~~~~~~~~~~~~~~~~~~
+
+.. code:: python
+
+ >>> from geojson import GeometryCollection, Point, LineString
+
+ >>> my_point = Point((23.532, -63.12))
+
+ >>> my_line = LineString([(-152.62, 51.21), (5.21, 10.69)])
+
+ >>> GeometryCollection([my_point, my_line]) # doctest: +ELLIPSIS
+ {"geometries": [{"coordinates": [23.53..., -63.1...], "type": "Point"}, {"coordinates": [[-152.6..., 51.2...], [5.2..., 10.6...]], "type": "LineString"}], "type": "GeometryCollection"}
+
+Visualize the result of the example above `here <https://gist.github.com/frewsxcv/6ec8422e97d338a101b0>`__. General information about GeometryCollection can be found in `Section 2.1.8`_ and `Appendix A: GeometryCollection`_ within `The GeoJSON Format Specification`_.
+
+.. _Section 2.1.8: http://www.geojson.org/geojson-spec.html#geometry-collection
+.. _Appendix A\: GeometryCollection: http://www.geojson.org/geojson-spec.html#geometrycollection
+
+Feature
+~~~~~~~
+
+.. code:: python
+
+ >>> from geojson import Feature, Point
+
+ >>> my_point = Point((-3.68, 40.41))
+
+ >>> Feature(geometry=my_point) # doctest: +ELLIPSIS
+ {"geometry": {"coordinates": [-3.68..., 40.4...], "type": "Point"}, "properties": {}, "type": "Feature"}
+
+ >>> Feature(geometry=my_point, properties={"country": "Spain"}) # doctest: +ELLIPSIS
+ {"geometry": {"coordinates": [-3.68..., 40.4...], "type": "Point"}, "properties": {"country": "Spain"}, "type": "Feature"}
+
+ >>> Feature(geometry=my_point, id=27) # doctest: +ELLIPSIS
+ {"geometry": {"coordinates": [-3.68..., 40.4...], "type": "Point"}, "id": 27, "properties": {}, "type": "Feature"}
+
+Visualize the results of the examples above `here <https://gist.github.com/frewsxcv/4488d30209d22685c075>`__. General information about Feature can be found in `Section 2.2`_ within `The GeoJSON Format Specification`_.
+
+.. _Section 2.2: http://geojson.org/geojson-spec.html#feature-objects
+
+FeatureCollection
+~~~~~~~~~~~~~~~~~
+
+.. code:: python
+
+ >>> from geojson import Feature, Point, FeatureCollection
+
+ >>> my_feature = Feature(geometry=Point((1.6432, -19.123)))
+
+ >>> my_other_feature = Feature(geometry=Point((-80.234, -22.532)))
+
+ >>> FeatureCollection([my_feature, my_other_feature]) # doctest: +ELLIPSIS
+ {"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"}
+
+Visualize the result of the example above `here <https://gist.github.com/frewsxcv/34513be6fb492771ef7b>`__. General information about FeatureCollection can be found in `Section 2.3`_ within `The GeoJSON Format Specification`_.
+
+.. _Section 2.3: http://geojson.org/geojson-spec.html#feature-collection-objects
+
+GeoJSON encoding/decoding
+-------------------------
+
+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.
+
+.. code:: python
+
+ >>> import geojson
+
+ >>> my_point = geojson.Point((43.24, -1.532))
+
+ >>> my_point # doctest: +ELLIPSIS
+ {"coordinates": [43.2..., -1.53...], "type": "Point"}
+
+ >>> dump = geojson.dumps(my_point, sort_keys=True)
+
+ >>> dump # doctest: +ELLIPSIS
+ '{"coordinates": [43.2..., -1.53...], "type": "Point"}'
+
+ >>> geojson.loads(dump) # doctest: +ELLIPSIS
+ {"coordinates": [43.2..., -1.53...], "type": "Point"}
+
+Custom classes
+~~~~~~~~~~~~~~
+
+This encoding/decoding functionality shown in the previous can be extended to custom classes using the interface described by the `__geo_interface__ Specification`_.
+
+.. code:: python
+
+ >>> import geojson
+
+ >>> class MyPoint():
+ ... def __init__(self, x, y):
+ ... self.x = x
+ ... self.y = y
+ ...
+ ... @property
+ ... def __geo_interface__(self):
+ ... return {'type': 'Point', 'coordinates': (self.x, self.y)}
+
+ >>> point_instance = MyPoint(52.235, -19.234)
+
+ >>> geojson.dumps(point_instance, sort_keys=True) # doctest: +ELLIPSIS
+ '{"coordinates": [52.23..., -19.23...], "type": "Point"}'
+
+Helpful utilities
+-----------------
+
+coords
+~~~~~~
+
+:code:`geojson.utils.coords` yields all coordinate tuples from a geometry or feature object.
+
+.. code:: python
+
+ >>> import geojson
+
+ >>> my_line = LineString([(-152.62, 51.21), (5.21, 10.69)])
+
+ >>> my_feature = geojson.Feature(geometry=my_line)
+
+ >>> list(geojson.utils.coords(my_feature)) # doctest: +ELLIPSIS
+ [(-152.62..., 51.21...), (5.21..., 10.69...)]
+
+map_coords
+~~~~~~~~~~
+
+:code:`geojson.utils.map_coords` maps a function over all coordinate tuples and returns a geometry of the same type. Useful for translating a geometry in space or flipping coordinate order.
+
+.. code:: python
+
+ >>> import geojson
+
+ >>> new_point = geojson.utils.map_coords(lambda x: x/2, geojson.Point((-115.81, 37.24)))
+
+ >>> geojson.dumps(new_point, sort_keys=True) # doctest: +ELLIPSIS
+ '{"coordinates": [-57.905..., 18.62...], "type": "Point"}'
+
+validation
+~~~~~~~~~~
+:code:`geojson.is_valid` provides validation of GeoJSON objects.
+
+.. code:: python
+
+ >>> import geojson
+
+ >>> validation = geojson.is_valid(geojson.Point((-3.68,40.41,25.14)))
+ >>> validation['valid']
+ 'no'
+ >>> validation['message']
+ 'the "coordinates" member must be a single position'
+
+generate_random
+~~~~~~~~~~~~~~~
+
+:code:`geojson.utils.generate_random` yields a geometry type with random data
+
+.. code:: python
+
+ >>> import geojson
+
+ >>> geojson.utils.generate_random("LineString") # doctest: +ELLIPSIS
+ {"coordinates": [...], "type": "LineString"}
+
+
+Development
+-----------
+
+To build this project, run :code:`python setup.py build`. To run the unit tests, run :code:`python setup.py test`.
+
+Credits
+-------
+
+* Sean Gillies <sgillies@frii.com>
+* Matthew Russell <matt@sanoodi.com>
+* Corey Farwell <coreyf@rwell.org>
+* Blake Grotewold <hello@grotewold.me>
+* Zsolt Ero <zsolt.ero@gmail.com>
+* Sergey Romanov <xxsmotur@gmail.com>
+
+
+.. _GeoJSON: http://geojson.org/
+.. _The GeoJSON Format Specification: http://www.geojson.org/geojson-spec.html
+.. _\_\_geo\_interface\_\_ Specification: https://gist.github.com/sgillies/2217756
diff --git a/geojson/__init__.py b/geojson/__init__.py
new file mode 100644
index 0000000..887f9db
--- /dev/null
+++ b/geojson/__init__.py
@@ -0,0 +1,19 @@
+from geojson.codec import dump, dumps, load, loads, GeoJSONEncoder
+from geojson.utils import coords, map_coords
+from geojson.geometry import Point, LineString, Polygon
+from geojson.geometry import MultiLineString, MultiPoint, MultiPolygon
+from geojson.geometry import GeometryCollection
+from geojson.feature import Feature, FeatureCollection
+from geojson.base import GeoJSON
+from geojson.validation import is_valid
+from geojson._version import __version__, __version_info__
+
+__all__ = ([dump, dumps, load, loads, GeoJSONEncoder] +
+ [coords, map_coords] +
+ [Point, LineString, Polygon] +
+ [MultiLineString, MultiPoint, MultiPolygon] +
+ [GeometryCollection] +
+ [Feature, FeatureCollection] +
+ [GeoJSON] +
+ [is_valid] +
+ [__version__, __version_info__])
diff --git a/geojson/_version.py b/geojson/_version.py
new file mode 100644
index 0000000..35531d9
--- /dev/null
+++ b/geojson/_version.py
@@ -0,0 +1,2 @@
+__version__ = "1.3.4"
+__version_info__ = tuple(map(int, __version__.split(".")))
diff --git a/geojson/base.py b/geojson/base.py
new file mode 100644
index 0000000..692141f
--- /dev/null
+++ b/geojson/base.py
@@ -0,0 +1,127 @@
+from __future__ import unicode_literals
+
+import geojson
+from geojson.mapping import to_mapping
+
+
+class GeoJSON(dict):
+ """
+ A class representing a GeoJSON object.
+ """
+
+ def __init__(self, iterable=(), **extra):
+ """
+ Initialises a GeoJSON object
+
+ :param iterable: iterable from which to draw the content of the GeoJSON
+ object.
+ :type iterable: dict, array, tuple
+ :return: a GeoJSON object
+ :rtype: GeoJSON
+ """
+ super(GeoJSON, self).__init__(iterable)
+ self["type"] = getattr(self, "type", type(self).__name__)
+ self.update(extra)
+
+ def __repr__(self):
+ return geojson.dumps(self, sort_keys=True)
+
+ __str__ = __repr__
+
+ def __getattr__(self, name):
+ """
+ Permit dictionary items to be retrieved like object attributes
+
+ :param name: attribute name
+ :type name: str, int
+ :return: dictionary value
+ """
+ try:
+ return self[name]
+ except KeyError:
+ raise AttributeError(name)
+
+ def __setattr__(self, name, value):
+ """
+ Permit dictionary items to be set like object attributes.
+
+ :param name: key of item to be set
+ :type name: str
+ :param value: value to set item to
+ """
+
+ self[name] = value
+
+ def __delattr__(self, name):
+ """
+ Permit dictionary items to be deleted like object attributes
+
+ :param name: key of item to be deleted
+ :type name: str
+ """
+
+ del self[name]
+
+ @property
+ def __geo_interface__(self):
+ if self.type != "GeoJSON":
+ return self
+
+ @classmethod
+ def to_instance(cls, ob, default=None, strict=False):
+ """Encode a GeoJSON dict into an GeoJSON object.
+ Assumes the caller knows that the dict should satisfy a GeoJSON type.
+
+ :param cls: Dict containing the elements to be encoded into a GeoJSON
+ object.
+ :type cls: dict
+ :param ob: GeoJSON object into which to encode the dict provided in
+ `cls`.
+ :type ob: GeoJSON
+ :param default: A default instance to append the content of the dict
+ to if none is provided.
+ :type default: GeoJSON
+ :param strict: Raise error if unable to coerce particular keys or
+ attributes to a valid GeoJSON structure.
+ :type strict: bool
+ :return: A GeoJSON object with the dict's elements as its constituents.
+ :rtype: GeoJSON
+ :raises TypeError: If the input dict contains items that are not valid
+ GeoJSON types.
+ :raises UnicodeEncodeError: If the input dict contains items of a type
+ that contain non-ASCII characters.
+ :raises AttributeError: If the input dict contains items that are not
+ valid GeoJSON types.
+ """
+ if ob is None and default is not None:
+ instance = default()
+ elif isinstance(ob, GeoJSON):
+ instance = ob
+ else:
+ mapping = to_mapping(ob)
+ d = {}
+ for k in mapping:
+ d[k] = mapping[k]
+ try:
+ type_ = d.pop("type")
+ try:
+ type_ = str(type_)
+ except UnicodeEncodeError:
+ # If the type contains non-ascii characters, we can assume
+ # it's not a valid GeoJSON type
+ raise AttributeError(
+ "{0} is not a GeoJSON type").format(type_)
+ geojson_factory = getattr(geojson.factory, type_)
+ if not issubclass(geojson_factory, GeoJSON):
+ raise TypeError("""\
+ Not a valid GeoJSON type:
+ %r (geojson_factory: %r, cls: %r)
+ """ % (type_, geojson_factory, cls))
+ instance = geojson_factory(**d)
+ except (AttributeError, KeyError) as invalid:
+ if strict:
+ msg = "Cannot coerce %r into a valid GeoJSON structure: %s"
+ msg %= (ob, invalid)
+ raise ValueError(msg)
+ instance = ob
+ return instance
diff --git a/geojson/codec.py b/geojson/codec.py
new file mode 100644
index 0000000..eb8808b
--- /dev/null
+++ b/geojson/codec.py
@@ -0,0 +1,59 @@
+try:
+ import simplejson as json
+except ImportError:
+ import json
+
+import geojson
+import geojson.factory
+from geojson.mapping import to_mapping
+
+
+class GeoJSONEncoder(json.JSONEncoder):
+
+ def default(self, obj):
+ return geojson.factory.GeoJSON.to_instance(obj)
+
+
+# Wrap the functions from json, providing encoder, decoders, and
+# object creation hooks.
+# Here the defaults are set to only permit valid JSON as per RFC 4267
+
+def _enforce_strict_numbers(obj):
+ if isinstance(obj, (int, float)):
+ raise ValueError("Number %r is not JSON compliant" % obj)
+
+
+def dump(obj, fp, cls=GeoJSONEncoder, allow_nan=False, **kwargs):
+ return json.dump(to_mapping(obj),
+ fp, cls=cls, allow_nan=allow_nan, **kwargs)
+
+
+def dumps(obj, cls=GeoJSONEncoder, allow_nan=False, **kwargs):
+ return json.dumps(to_mapping(obj),
+ cls=cls, allow_nan=allow_nan, **kwargs)
+
+
+def load(fp,
+ cls=json.JSONDecoder,
+ parse_constant=_enforce_strict_numbers,
+ object_hook=geojson.base.GeoJSON.to_instance,
+ **kwargs):
+ return json.load(fp,
+ cls=cls, object_hook=object_hook,
+ parse_constant=parse_constant,
+ **kwargs)
+
+
+def loads(s,
+ cls=json.JSONDecoder,
+ parse_constant=_enforce_strict_numbers,
+ object_hook=geojson.base.GeoJSON.to_instance,
+ **kwargs):
+ return json.loads(s,
+ cls=cls, object_hook=object_hook,
+ parse_constant=parse_constant,
+ **kwargs)
+
+
+# Backwards compatibility
+PyGFPEncoder = GeoJSONEncoder
diff --git a/geojson/crs.py b/geojson/crs.py
new file mode 100644
index 0000000..41a60e2
--- /dev/null
+++ b/geojson/crs.py
@@ -0,0 +1,39 @@
+from geojson.base import GeoJSON
+
+
+class CoordinateReferenceSystem(GeoJSON):
+ """
+ Represents a CRS.
+ """
+
+ def __init__(self, properties=None, **extra):
+ super(CoordinateReferenceSystem, self).__init__(**extra)
+ self["properties"] = properties or {}
+
+
+class Named(CoordinateReferenceSystem):
+ """
+ Represents a named CRS.
+ """
+
+ def __init__(self, properties=None, **extra):
+ super(Named, self).__init__(properties=properties, **extra)
+ self["type"] = "name"
+
+ def __repr__(self):
+ return super(Named, self).__repr__()
+
+
+class Linked(CoordinateReferenceSystem):
+ """
+ Represents a linked CRS.
+ """
+
+ def __init__(self, properties=None, **extra):
+ super(Linked, self).__init__(properties=properties, **extra)
+ self["type"] = "link"
+
+
+class Default(object):
+
+ """GeoJSON default, long/lat WGS84, is not serialized."""
diff --git a/geojson/examples.py b/geojson/examples.py
new file mode 100644
index 0000000..4b8a857
--- /dev/null
+++ b/geojson/examples.py
@@ -0,0 +1,70 @@
+
+class SimpleWebFeature(object):
+
+ """
+ A simple, Atom-ish, single geometry (WGS84) GIS feature.
+ """
+
+ def __init__(self, id=None, geometry=None, title=None, summary=None,
+ link=None):
+ """
+ Initialises a SimpleWebFeature from the parameters provided.
+
+ :param id: Identifier assigned to the object.
+ :type id: int, str
+ :param geometry: The geometry on which the object is based.
+ :type geometry: Geometry
+ :param title: Name of the object
+ :type title: str
+ :param summary: Short summary associated with the object.
+ :type summary: str
+ :param link: Link associated with the object.
+ :type link: str
+ :return: A SimpleWebFeature object
+ :rtype: SimpleWebFeature
+ """
+ self.id = id
+ self.geometry = geometry
+ self.properties = {'title': title, 'summary': summary, 'link': link}
+
+ def as_dict(self):
+ return {
+ "type": "Feature",
+ "id": self.id,
+ "properties": self.properties,
+ "geometry": self.geometry
+ }
+
+ __geo_interface__ = property(as_dict)
+
+ """
+ Create an instance of SimpleWebFeature from a dict, o. If o does not
+ match a Python feature object, simply return o. This function serves as a
+ json decoder hook. See coding.load().
+ """
+
+
+def create_simple_web_feature(o):
+ """
+ Create an instance of SimpleWebFeature from a dict, o. If o does not
+ match a Python feature object, simply return o. This function serves as a
+ json decoder hook. See coding.load().
+
+ :param o: A dict to create the SimpleWebFeature from.
+ :type o: dict
+ :return: A SimpleWebFeature from the dict provided.
+ :rtype: SimpleWebFeature
+ """
+ try:
+ id = o['id']
+ g = o['geometry']
+ p = o['properties']
+ return SimpleWebFeature(str(id), {
+ 'type': str(g.get('type')),
+ 'coordinates': g.get('coordinates', [])},
+ title=p.get('title'),
+ summary=p.get('summary'),
+ link=str(p.get('link')))
+ except (KeyError, TypeError):
+ pass
+ return o
diff --git a/geojson/factory.py b/geojson/factory.py
new file mode 100644
index 0000000..bd0493e
--- /dev/null
+++ b/geojson/factory.py
@@ -0,0 +1,15 @@
+from geojson.geometry import Point, LineString, Polygon
+from geojson.geometry import MultiLineString, MultiPoint, MultiPolygon
+from geojson.geometry import GeometryCollection
+from geojson.feature import Feature, FeatureCollection
+from geojson.base import GeoJSON
+from geojson.crs import Named, Linked
+
+__all__ = ([Point, LineString, Polygon] +
+ [MultiLineString, MultiPoint, MultiPolygon] +
+ [GeometryCollection] +
+ [Feature, FeatureCollection] +
+ [GeoJSON])
+
+name = Named
+link = Linked
diff --git a/geojson/feature.py b/geojson/feature.py
new file mode 100644
index 0000000..dc9625d
--- /dev/null
+++ b/geojson/feature.py
@@ -0,0 +1,48 @@
+"""
+SimpleWebFeature is a working example of a class that satisfies the Python geo
+interface.
+"""
+
+from geojson.base import GeoJSON
+
+
+class Feature(GeoJSON):
+ """
+ Represents a WGS84 GIS feature.
+ """
+
+ def __init__(self, id=None, geometry=None, properties=None, **extra):
+ """
+ Initialises a Feature object with the given parameters.
+
+ :param id: Feature identifier, such as a sequential number.
+ :type id: str, int
+ :param geometry: Geometry corresponding to the feature.
+ :param properties: Dict containing properties of the feature.
+ :type properties: dict
+ :return: Feature object
+ :rtype: Feature
+ """
+ super(Feature, self).__init__(**extra)
+ if id is not None:
+ self["id"] = id
+ self["geometry"] = (self.to_instance(geometry, strict=True)
+ if geometry else None)
+ self["properties"] = properties or {}
+
+
+class FeatureCollection(GeoJSON):
+ """
+ Represents a FeatureCollection, a set of multiple Feature objects.
+ """
+
+ def __init__(self, features, **extra):
+ """
+ Initialises a FeatureCollection object from the
+ :param features: List of features to constitute the FeatureCollection.
+ :type features: list
+ :return: FeatureCollection object
+ :rtype: FeatureCollection
+ """
+ super(FeatureCollection, self).__init__(**extra)
+ self["features"] = features
diff --git a/geojson/geometry.py b/geojson/geometry.py
new file mode 100644
index 0000000..931eccd
--- /dev/null
+++ b/geojson/geometry.py
@@ -0,0 +1,88 @@
+import sys
+from decimal import Decimal
+
+from geojson.base import GeoJSON
+from geojson.validation import is_valid
+
+
+class Geometry(GeoJSON):
+ """
+ Represents an abstract base class for a WGS84 geometry.
+ """
+
+ if sys.version_info[0] == 3:
+ # Python 3.x has no long type
+ __JSON_compliant_types = (float, int, Decimal)
+ else:
+ __JSON_compliant_types = (float, int, Decimal, long) # noqa
+
+ def __init__(self, coordinates=None, crs=None, validate=False, **extra):
+ """
+ Initialises a Geometry object.
+
+ :param coordinates: Coordinates of the Geometry object.
+ :type coordinates: tuple
+ :param crs: CRS
+ :type crs: CRS object
+ """
+
+ super(Geometry, self).__init__(**extra)
+ self["coordinates"] = coordinates or []
+ self.clean_coordinates(self["coordinates"])
+ if validate:
+ validation = is_valid(self)
+ if validation['valid'] == 'no':
+ raise ValueError('{}: {}'.format(
+ validation['message'], coordinates))
+ if crs:
+ self["crs"] = self.to_instance(crs, strict=True)
+
+ @classmethod
+ def clean_coordinates(cls, coords):
+ for coord in coords:
+ if isinstance(coord, (list, tuple)):
+ cls.clean_coordinates(coord)
+ elif not isinstance(coord, cls.__JSON_compliant_types):
+ raise ValueError("%r is not JSON compliant number" % coord)
+
+
+class GeometryCollection(GeoJSON):
+ """
+ Represents an abstract base class for collections of WGS84 geometries.
+ """
+
+ def __init__(self, geometries=None, **extra):
+ super(GeometryCollection, self).__init__(**extra)
+ self["geometries"] = geometries or []
+
+
+# Marker classes.
+
+class Point(Geometry):
+ pass
+
+
+class MultiPoint(Geometry):
+ pass
+
+
+class LineString(MultiPoint):
+ pass
+
+
+class MultiLineString(Geometry):
+ pass
+
+
+class Polygon(Geometry):
+ pass
+
+
+class MultiPolygon(Geometry):
+ pass
+
+
+class Default(object):
+ """
+ GeoJSON default object.
+ """
diff --git a/geojson/mapping.py b/geojson/mapping.py
new file mode 100644
index 0000000..efee073
--- /dev/null
+++ b/geojson/mapping.py
@@ -0,0 +1,41 @@
+from collections import MutableMapping
+try:
+ import simplejson as json
+except ImportError:
+ import json
+
+import geojson
+
+
+mapping_base = MutableMapping
+
+
+GEO_INTERFACE_MARKER = "__geo_interface__"
+
+
+def is_mapping(obj):
+ """
+ Checks if the object is an instance of MutableMapping.
+
+ :param obj: Object to be checked.
+ :return: Truth value of whether the object is an instance of
+ MutableMapping.
+ :rtype: bool
+ """
+ return isinstance(obj, MutableMapping)
+
+
+def to_mapping(obj):
+
+ mapping = getattr(obj, GEO_INTERFACE_MARKER, None)
+
+ if mapping is not None:
+ return mapping
+
+ if is_mapping(obj):
+ return obj
+
+ if isinstance(obj, geojson.GeoJSON):
+ return dict(obj)
+
+ return json.loads(json.dumps(obj))
diff --git a/geojson/utils.py b/geojson/utils.py
new file mode 100644
index 0000000..15ff320
--- /dev/null
+++ b/geojson/utils.py
@@ -0,0 +1,157 @@
+"""Coordinate utility functions."""
+
+
+def coords(obj):
+ """
+ Yields the coordinates from a Feature or Geometry.
+
+ :param obj: A geometry or feature to extract the coordinates from."
+ :type obj: Feature, Geometry
+ :return: A generator with coordinate tuples from the geometry or feature.
+ :rtype: generator
+ """
+
+ if isinstance(obj, (tuple, list)):
+ coordinates = obj
+ elif 'geometry' in obj:
+ coordinates = obj['geometry']['coordinates']
+ else:
+ coordinates = obj.get('coordinates', obj)
+ for e in coordinates:
+ if isinstance(e, (float, int)):
+ yield tuple(coordinates)
+ break
+ for f in coords(e):
+ yield f
+
+
+def map_coords(func, obj):
+ """
+ Returns the mapped coordinates from a Geometry after applying the provided
+ function to each dimension in tuples list (ie, linear scaling).
+
+ :param func: Function to apply to tuples
+ :type func: function
+ :param obj: A geometry or feature to extract the coordinates from.
+ :type obj: Point, LineString, MultiPoint, MultiLineString, Polygon,
+ MultiPolygon
+ :return: The result of applying the function to each dimension in the
+ array.
+ :rtype: list
+ :raises ValueError: if the provided object is not a Geometry.
+ """
+
+ if obj['type'] == 'Point':
+ coordinates = tuple(map(func, obj['coordinates']))
+ elif obj['type'] in ['LineString', 'MultiPoint']:
+ coordinates = [tuple(map(func, c)) for c in obj['coordinates']]
+ elif obj['type'] in ['MultiLineString', 'Polygon']:
+ coordinates = [[
+ tuple(map(func, c)) for c in curve]
+ for curve in obj['coordinates']]
+ elif obj['type'] == 'MultiPolygon':
+ coordinates = [[[
+ tuple(map(func, c)) for c in curve]
+ for curve in part]
+ for part in obj['coordinates']]
+ else:
+ raise ValueError("Invalid geometry object %s" % repr(obj))
+ return {'type': obj['type'], 'coordinates': coordinates}
+
+
+def generate_random(featureType, numberVertices=3,
+ boundingBox=[-180.0, -90.0, 180.0, 90.0]):
+ """
+ Generates random geojson features depending on the parameters
+ passed through.
+ The bounding box defaults to the world - [-180.0, -90.0, 180.0, 90.0].
+ The number of vertices defaults to 3.
+
+ :param featureType: A geometry type
+ :type featureType: Point, LineString, Polygon
+ :param numberVertices: The number vertices that a linestring or polygon
+ will have
+ :type numberVertices: int
+ :param boundingBox: A bounding box in which features will be restricted to
+ :type boundingBox: list
+ :return: The resulting random geojson object or geometry collection.
+ :rtype: object
+ :raises ValueError: if there is no featureType provided.
+ """
+ from geojson import Point, LineString, Polygon
+ import random
+ import math
+
+ lonMin = boundingBox[0]
+ lonMax = boundingBox[2]
+
+ def randomLon():
+ return random.uniform(lonMin, lonMax)
+
+ latMin = boundingBox[1]
+ latMax = boundingBox[3]
+
+ def randomLat():
+ return random.uniform(latMin, latMax)
+
+ def createPoint():
+ return Point((randomLon(), randomLat()))
+
+ def createLine():
+ coords = []
+ for i in range(0, numberVertices):
+ coords.append((randomLon(), randomLat()))
+ return LineString(coords)
+
+ def createPoly():
+ aveRadius = 60
+ ctrX = 0.1
+ ctrY = 0.2
+ irregularity = clip(0.1, 0, 1) * 2 * math.pi / numberVertices
+ spikeyness = clip(0.5, 0, 1) * aveRadius
+
+ angleSteps = []
+ lower = (2 * math.pi / numberVertices) - irregularity
+ upper = (2 * math.pi / numberVertices) + irregularity
+ sum = 0
+ for i in range(numberVertices):
+ tmp = random.uniform(lower, upper)
+ angleSteps.append(tmp)
+ sum = sum + tmp
+
+ k = sum / (2 * math.pi)
+ for i in range(numberVertices):
+ angleSteps[i] = angleSteps[i] / k
+
+ points = []
+ angle = random.uniform(0, 2 * math.pi)
+
+ for i in range(numberVertices):
+ r_i = clip(random.gauss(aveRadius, spikeyness), 0, 2 * aveRadius)
+ x = ctrX + r_i * math.cos(angle)
+ y = ctrY + r_i * math.sin(angle)
+ points.append((int(x), int(y)))
+ angle = angle + angleSteps[i]
+
+ firstVal = points[0]
+ points.append(firstVal)
+ return Polygon([points])
+
+ def clip(x, min, max):
+ if(min > max):
+ return x
+ elif(x < min):
+ return min
+ elif(x > max):
+ return max
+ else:
+ return x
+
+ if featureType == 'Point':
+ return createPoint()
+
+ if featureType == 'LineString':
+ return createLine()
+
+ if featureType == 'Polygon':
+ return createPoly()
diff --git a/geojson/validation.py b/geojson/validation.py
new file mode 100644
index 0000000..e1afa37
--- /dev/null
+++ b/geojson/validation.py
@@ -0,0 +1,100 @@
+import geojson
+
+
+def is_valid(obj):
+ """ IsValid provides validation for GeoJSON objects
+ All of error messages obtained from the official site
+ http://geojson.org/geojson-spec.html
+
+ Args:
+ obj(geoJSON object): check validation
+
+ Returns:
+ dict of two parameters 'valid' and 'message'.
+ In the case if geoJSON object is valid, returns
+ {valid: 'yes', message: ''}
+ If json objects is not valid, returns
+ {valid: 'no', message:'explanation, why this object is not valid'}
+
+ Example:
+ >> point = Point((10,20), (30,40))
+ >> IsValid(point)
+ >> {valid: 'yes', message: ''}
+ """
+
+ if not isinstance(obj, geojson.GeoJSON):
+ return output('this is not GeoJSON object')
+
+ if isinstance(obj, geojson.Point):
+ if len(obj['coordinates']) != 2:
+ return output('the "coordinates" member must be a single position')
+
+ if isinstance(obj, geojson.MultiPoint):
+ if checkListOfObjects(obj['coordinates'], lambda x: len(x) == 2):
+ return output(
+ 'the "coordinates" member must be an array of positions'
+ )
+
+ if isinstance(obj, geojson.LineString):
+ if len(obj['coordinates']) < 2:
+ return output('the "coordinates" member must be an array '
+ 'of two or more positions')
+
+ if isinstance(obj, geojson.MultiLineString):
+ coord = obj['coordinates']
+ if checkListOfObjects(coord, lambda x: len(x) >= 2):
+ return output('the "coordinates" member must be an array '
+ 'of LineString coordinate arrays')
+
+ if isinstance(obj, geojson.Polygon):
+ coord = obj['coordinates']
+ lengths = all([len(elem) >= 4 for elem in coord])
+ if lengths is False:
+ return output('LinearRing must contain with 4 or more positions')
+
+ isring = all([elem[0] == elem[-1] for elem in coord])
+ if isring is False:
+ return output('The first and last positions in LinearRing'
+ 'must be equivalent')
+
+ if isinstance(obj, geojson.MultiPolygon):
+ if checkListOfObjects(obj['coordinates'], lambda x: is_polygon(x)):
+ return output('the "coordinates" member must be an array'
+ 'of Polygon coordinate arrays')
+
+ return output('')
+
+
+def is_polygon(coords):
+ lengths = all(len(elem) >= 4 for elem in coords)
+ isring = all(elem[0] == elem[-1] for elem in coords)
+ return lengths and isring
+
+
+def checkListOfObjects(coord, pred):
+ """ This method provides checking list of geojson objects such Multipoint or
+ MultiLineString that each element of the list is valid geojson object.
+ This is helpful method for IsValid.
+
+ :param coord: List of coordinates
+ :type coord: list
+ :param pred: Predicate to check validation of each member in the coord
+ :type pred: function
+ :return: True if list contains valid objects, False otherwise
+ :rtype: bool
+ """
+ return not isinstance(coord, list) or not all([pred(ls) for ls in coord])
+
+
+def output(message):
+ """ Output result for IsValid
+
+ :param message: If message is not empty, object is not valid
+ :type message: str
+ """
+ result = {'valid': 'no', 'message': ''}
+ if message != '':
+ result['message'] = message
+ return result
+ result['valid'] = 'yes'
+ return result
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..2a9acf1
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,2 @@
+[bdist_wheel]
+universal = 1
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..3166430
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,75 @@
+import io
+from setuptools import setup
+import sys
+import re
+
+
+with io.open("README.rst") as readme_file:
+ readme_text = readme_file.read()
+
+VERSIONFILE = "geojson/_version.py"
+verstrline = open(VERSIONFILE, "rt").read()
+VSRE = r"^__version__ = ['\"]([^'\"]*)['\"]"
+mo = re.search(VSRE, verstrline, re.M)
+if mo:
+ verstr = mo.group(1)
+else:
+ raise RuntimeError("Unable to find version string in %s." % (VERSIONFILE,))
+
+
+def test_suite():
+ import doctest
+ try:
+ import unittest2 as unittest
+ except:
+ import unittest
+
+ suite = unittest.TestLoader().discover("tests")
+ suite.addTest(doctest.DocFileSuite("README.rst"))
+ return suite
+
+
+if sys.version_info[:2] not in [(2, 6), (2, 7)] and \
+ sys.version_info[:1] not in [(3, )]:
+ sys.stderr.write("Sorry, only Python 2.7, and 3.x are supported "
+ "at this time.\n")
+ exit(1)
+
+# Get around this issue: http://bugs.python.org/issue15881
+# Appears to be a problem in older versions of Python 2.7
+import multiprocessing # NOQA
+
+setup(
+ name="geojson",
+ version=verstr,
+ description="Python bindings and utilities for GeoJSON",
+ license="BSD",
+ keywords="gis geography json",
+ author="Sean Gillies",
+ author_email="sgillies@frii.com",
+ maintainer="Corey Farwell",
+ maintainer_email="coreyf@rwell.org",
+ url="https://github.com/frewsxcv/python-geojson",
+ long_description=readme_text,
+ packages=["geojson"],
+ package_dir={"geojson": "geojson"},
+ package_data={"geojson": ["*.rst"]},
+ install_requires=[],
+ test_suite="setup.test_suite",
+ classifiers=[
+ "Development Status :: 5 - Production/Stable",
+ "Intended Audience :: Developers",
+ "Intended Audience :: Science/Research",
+ "License :: OSI Approved :: BSD License",
+ "Operating System :: OS Independent",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 2",
+ "Programming Language :: Python :: 2.7",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.2",
+ "Programming Language :: Python :: 3.3",
+ "Programming Language :: Python :: 3.4",
+ "Programming Language :: Python :: 3.5",
+ "Topic :: Scientific/Engineering :: GIS",
+ ]
+)
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..4efcb7b
--- /dev/null
+++ b/tests/__init__.py
@@ -0,0 +1,12 @@
+import doctest
+import glob
+import os
+
+optionflags = (doctest.REPORT_ONLY_FIRST_FAILURE |
+ doctest.NORMALIZE_WHITESPACE |
+ doctest.ELLIPSIS)
+
+_basedir = os.path.dirname(__file__)
+paths = glob.glob("%s/*.txt" % _basedir)
+test_suite = doctest.DocFileSuite(*paths, **dict(module_relative=False,
+ optionflags=optionflags))
diff --git a/tests/data.geojson b/tests/data.geojson
new file mode 100644
index 0000000..f78bd62
--- /dev/null
+++ b/tests/data.geojson
@@ -0,0 +1,7 @@
+{
+ "properties": {
+ "Ã": "Ã"
+ },
+ "type": "Feature",
+ "geometry": null
+}
diff --git a/tests/test_base.py b/tests/test_base.py
new file mode 100644
index 0000000..a80355b
--- /dev/null
+++ b/tests/test_base.py
@@ -0,0 +1,57 @@
+# -*- coding: utf-8 -*-
+"""
+Tests for geojson/base.py
+"""
+
+import unittest
+
+import geojson
+
+
+class TypePropertyTestCase(unittest.TestCase):
+ def test_type_property(self):
+ json_str = ('{"type": "Feature",'
+ ' "geometry": null,'
+ ' "id": 1,'
+ ' "properties": {"type": "é"}}')
+ geojson_obj = geojson.loads(json_str)
+ self.assertTrue(isinstance(geojson_obj, geojson.GeoJSON))
+ self.assertTrue("type" in geojson_obj.properties)
+
+ json_str = ('{"type": "Feature",'
+ ' "geometry": null,'
+ ' "id": 1,'
+ ' "properties": {"type": null}}')
+ geojson_obj = geojson.loads(json_str)
+ self.assertTrue(isinstance(geojson_obj, geojson.GeoJSON))
+ self.assertTrue("type" in geojson_obj.properties)
+
+ json_str = ('{"type": "Feature",'
+ ' "geometry": null,'
+ ' "id": 1,'
+ ' "properties": {"type": "meow"}}')
+ geojson_obj = geojson.loads(json_str)
+ self.assertTrue(isinstance(geojson_obj, geojson.GeoJSON))
+ self.assertTrue("type" in geojson_obj.properties)
+
+
+class OperatorOverloadingTestCase(unittest.TestCase):
+ """
+ Tests for operator overloading
+ """
+
+ def setUp(self):
+ self.coords = (12, -5)
+ self.point = geojson.Point(self.coords)
+
+ def test_setattr(self):
+ new_coords = (27, 42)
+ self.point.coordinates = new_coords
+ self.assertEqual(self.point['coordinates'], new_coords)
+
+ def test_getattr(self):
+ self.assertEqual(self.point['coordinates'], self.point.coordinates)
+
+ def test_delattr(self):
+ del self.point.coordinates
+ self.assertFalse(hasattr(self.point, 'coordinates'))
diff --git a/tests/test_coords.py b/tests/test_coords.py
new file mode 100644
index 0000000..3435786
--- /dev/null
+++ b/tests/test_coords.py
@@ -0,0 +1,59 @@
+import unittest
+
+import geojson
+from geojson.utils import coords, map_coords
+
+
+class CoordsTestCase(unittest.TestCase):
+ def test_point(self):
+ itr = coords(geojson.Point((-115.81, 37.24)))
+ self.assertEqual(next(itr), (-115.81, 37.24))
+
+ def test_dict(self):
+ itr = coords({'type': 'Point', 'coordinates': [-115.81, 37.24]})
+ self.assertEqual(next(itr), (-115.81, 37.24))
+
+ def test_point_feature(self):
+ itr = coords(geojson.Feature(geometry=geojson.Point((-115.81, 37.24))))
+ self.assertEqual(next(itr), (-115.81, 37.24))
+
+ def test_multipolygon(self):
+ g = geojson.MultiPolygon([
+ ([(3.78, 9.28), (-130.91, 1.52), (35.12, 72.234), (3.78, 9.28)],),
+ ([(23.18, -34.29), (-1.31, -4.61),
+ (3.41, 77.91), (23.18, -34.29)],)])
+ itr = coords(g)
+ pairs = list(itr)
+ self.assertEqual(pairs[0], (3.78, 9.28))
+ self.assertEqual(pairs[-1], (23.18, -34.29))
+
+ def test_map_point(self):
+ result = map_coords(lambda x: x, geojson.Point((-115.81, 37.24)))
+ self.assertEqual(result['type'], 'Point')
+ self.assertEqual(result['coordinates'], (-115.81, 37.24))
+
+ def test_map_linestring(self):
+ g = geojson.LineString(
+ [(3.78, 9.28), (-130.91, 1.52), (35.12, 72.234), (3.78, 9.28)])
+ result = map_coords(lambda x: x, g)
+ self.assertEqual(result['type'], 'LineString')
+ self.assertEqual(result['coordinates'][0], (3.78, 9.28))
+ self.assertEqual(result['coordinates'][-1], (3.78, 9.28))
+
+ def test_map_polygon(self):
+ g = geojson.Polygon([
+ [(3.78, 9.28), (-130.91, 1.52), (35.12, 72.234), (3.78, 9.28)], ])
+ result = map_coords(lambda x: x, g)
+ self.assertEqual(result['type'], 'Polygon')
+ self.assertEqual(result['coordinates'][0][0], (3.78, 9.28))
+ self.assertEqual(result['coordinates'][0][-1], (3.78, 9.28))
+
+ def test_map_multipolygon(self):
+ g = geojson.MultiPolygon([
+ ([(3.78, 9.28), (-130.91, 1.52), (35.12, 72.234), (3.78, 9.28)],),
+ ([(23.18, -34.29), (-1.31, -4.61),
+ (3.41, 77.91), (23.18, -34.29)],)])
+ result = map_coords(lambda x: x, g)
+ self.assertEqual(result['type'], 'MultiPolygon')
+ self.assertEqual(result['coordinates'][0][0][0], (3.78, 9.28))
+ self.assertEqual(result['coordinates'][-1][-1][-1], (23.18, -34.29))
diff --git a/tests/test_crs.py b/tests/test_crs.py
new file mode 100644
index 0000000..cbcc106
--- /dev/null
+++ b/tests/test_crs.py
@@ -0,0 +1,30 @@
+import unittest
+
+import geojson
+
+
+class CRSTest(unittest.TestCase):
+
+ def setUp(self):
+ self.crs = geojson.crs.Named(
+ properties={
+ "name": "urn:ogc:def:crs:EPSG::3785",
+ }
+ )
+
+ def test_crs_repr(self):
+ actual = repr(self.crs)
+ expected = ('{"properties": {"name": "urn:ogc:def:crs:EPSG::3785"},'
+ ' "type": "name"}')
+ self.assertEqual(actual, expected)
+
+ def test_crs_encode(self):
+ actual = geojson.dumps(self.crs, sort_keys=True)
+ expected = ('{"properties": {"name": "urn:ogc:def:crs:EPSG::3785"},'
+ ' "type": "name"}')
+ self.assertEqual(actual, expected)
+
+ def test_crs_decode(self):
+ dumped = geojson.dumps(self.crs)
+ actual = geojson.loads(dumped)
+ self.assertEqual(actual, self.crs)
diff --git a/tests/test_features.py b/tests/test_features.py
new file mode 100644
index 0000000..fe295d4
--- /dev/null
+++ b/tests/test_features.py
@@ -0,0 +1,116 @@
+from io import BytesIO # NOQA
+import unittest
+
+import geojson
+
+
+class FeaturesTest(unittest.TestCase):
+ def test_protocol(self):
+ """
+ A dictionary can satisfy the protocol
+ """
+ f = {
+ 'type': 'Feature',
+ 'id': '1',
+ 'geometry': {'type': 'Point', 'coordinates': [53, -4]},
+ 'properties': {'title': 'Dict 1'},
+ }
+
+ json = geojson.dumps(f, sort_keys=True)
+ self.assertEqual(json, '{"geometry":'
+ ' {"coordinates": [53, -4],'
+ ' "type": "Point"},'
+ ' "id": "1",'
+ ' "properties": {"title": "Dict 1"},'
+ ' "type": "Feature"}')
+
+ o = geojson.loads(json)
+ output = geojson.dumps(o, sort_keys=True)
+ self.assertEqual(output, '{"geometry":'
+ ' {"coordinates": [53, -4],'
+ ' "type": "Point"},'
+ ' "id": "1",'
+ ' "properties": {"title": "Dict 1"},'
+ ' "type": "Feature"}')
+
+ def test_unicode_properties(self):
+ with open("tests/data.geojson") as file_:
+ obj = geojson.load(file_)
+ geojson.dumps(obj)
+
+ def test_feature_class(self):
+ """
+ Test the Feature class
+ """
+
+ from geojson.examples import SimpleWebFeature
+ feature = SimpleWebFeature(
+ id='1',
+ geometry={'type': 'Point', 'coordinates': [53, -4]},
+ title='Feature 1', summary='The first feature',
+ link='http://example.org/features/1'
+ )
+
+ # It satisfies the feature protocol
+ self.assertEqual(feature.id, '1')
+ self.assertEqual(feature.properties['title'], 'Feature 1')
+ self.assertEqual(feature.properties['summary'], 'The first feature')
+ self.assertEqual(feature.properties['link'],
+ 'http://example.org/features/1')
+ self.assertEqual(geojson.dumps(feature.geometry, sort_keys=True),
+ '{"coordinates": [53, -4], "type": "Point"}')
+
+ # Encoding
+ json = ('{"geometry": {"coordinates": [53, -4],'
+ ' "type": "Point"},'
+ ' "id": "1",'
+ ' "properties":'
+ ' {"link": "http://example.org/features/1",'
+ ' "summary": "The first feature",'
+ ' "title": "Feature 1"},'
+ ' "type": "Feature"}')
+ self.assertEqual(geojson.dumps(feature, sort_keys=True), json)
+
+ # Decoding
+ factory = geojson.examples.create_simple_web_feature
+ json = ('{"geometry": {"type": "Point",'
+ ' "coordinates": [53, -4]},'
+ ' "id": "1",'
+ ' "properties": {"summary": "The first feature",'
+ ' "link": "http://example.org/features/1",'
+ ' "title": "Feature 1"}}')
+ feature = geojson.loads(json, object_hook=factory, encoding="utf-8")
+ self.assertEqual(repr(type(feature)),
+ "<class 'geojson.examples.SimpleWebFeature'>")
+ self.assertEqual(feature.id, '1')
+ self.assertEqual(feature.properties['title'], 'Feature 1')
+ self.assertEqual(feature.properties['summary'], 'The first feature')
+ self.assertEqual(feature.properties['link'],
+ 'http://example.org/features/1')
+ self.assertEqual(geojson.dumps(feature.geometry, sort_keys=True),
+ '{"coordinates": [53, -4], "type": "Point"}')
+
+ def test_geo_interface(self):
+ class Thingy(object):
+ def __init__(self, id, title, x, y):
+ self.id = id
+ self.title = title
+ self.x = x
+ self.y = y
+
+ @property
+ def __geo_interface__(self):
+ return ({"id": self.id,
+ "properties": {"title": self.title},
+ "geometry": {"type": "Point",
+ "coordinates": (self.x, self.y)}})
+
+ ob = Thingy('1', 'thingy one', -106, 40)
+ self.assertEqual(geojson.dumps(ob.__geo_interface__['geometry'],
+ sort_keys=True),
+ '{"coordinates": [-106, 40], "type": "Point"}')
+ self.assertEqual(geojson.dumps(ob, sort_keys=True),
+ ('{"geometry": {"coordinates": [-106, 40],'
+ ' "type": "Point"},'
+ ' "id": "1",'
+ ' "properties": {"title": "thingy one"}}'))
diff --git a/tests/test_geo_interface.py b/tests/test_geo_interface.py
new file mode 100644
index 0000000..7e7cb54
--- /dev/null
+++ b/tests/test_geo_interface.py
@@ -0,0 +1,131 @@
+"""
+Encoding/decoding custom objects with __geo_interface__
+"""
+import unittest
+
+import geojson
+
+
+class EncodingDecodingTest(unittest.TestCase):
+
+ def setUp(self):
+ class Restaurant(object):
+ """
+ Basic Restaurant class
+ """
+ def __init__(self, name, latlng):
+ super(Restaurant, self).__init__()
+ self.name = name
+ self.latlng = latlng
+
+ class Restaurant1(Restaurant):
+ """
+ Extends Restaurant with __geo_interface__ returning dict
+ """
+ @property
+ def __geo_interface__(self):
+ return {'type': "Point", 'coordinates': self.latlng}
+
+ class Restaurant2(Restaurant):
+ """
+ Extends Restaurant with __geo_interface__ returning another
+ __geo_interface__ object
+ """
+ @property
+ def __geo_interface__(self):
+ return geojson.Point(self.latlng)
+
+ class RestaurantFeature1(Restaurant):
+ """
+ Extends Restaurant with __geo_interface__ returning dict
+ """
+ @property
+ def __geo_interface__(self):
+ return {
+ 'geometry': {
+ 'type': "Point",
+ 'coordinates': self.latlng,
+ },
+ 'type': "Feature",
+ 'properties': {
+ 'name': self.name,
+ },
+ }
+
+ class RestaurantFeature2(Restaurant):
+ """
+ Extends Restaurant with __geo_interface__ returning another
+ __geo_interface__ object
+ """
+ @property
+ def __geo_interface__(self):
+ return geojson.Feature(
+ geometry=geojson.Point(self.latlng),
+ properties={'name': self.name})
+
+ self.name = "In N Out Burger"
+ self.latlng = [-54, 4]
+
+ self.restaurant_nogeo = Restaurant(self.name, self.latlng)
+
+ self.restaurant1 = Restaurant1(self.name, self.latlng)
+ self.restaurant2 = Restaurant2(self.name, self.latlng)
+
+ self.restaurant_str = ('{"coordinates": [-54, 4],'
+ ' "type": "Point"}')
+
+ self.restaurant_feature1 = RestaurantFeature1(self.name, self.latlng)
+ self.restaurant_feature2 = RestaurantFeature2(self.name, self.latlng)
+
+ self.restaurant_feature_str = ('{"geometry":'
+ ' {"coordinates": [-54, 4],'
+ ' "type": "Point"},'
+ ' "properties":'
+ ' {"name": "In N Out Burger"},'
+ ' "type": "Feature"}')
+
+ def test_encode(self):
+ """
+ Ensure objects that implement __geo_interface__ can be encoded into
+ GeoJSON strings
+ """
+ actual = geojson.dumps(self.restaurant1, sort_keys=True)
+ self.assertEqual(actual, self.restaurant_str)
+
+ actual = geojson.dumps(self.restaurant2, sort_keys=True)
+ self.assertEqual(actual, self.restaurant_str)
+
+ # Classes that don't implement geo interface should raise TypeError
+ self.assertRaises(
+ TypeError, geojson.dumps, self.restaurant_nogeo, sort_keys=True)
+
+ def test_encode_nested(self):
+ """
+ Ensure nested objects that implement __geo_interface__ can be encoded
+ into GeoJSON strings
+ """
+ actual = geojson.dumps(self.restaurant_feature1, sort_keys=True)
+ self.assertEqual(actual, self.restaurant_feature_str)
+
+ actual = geojson.dumps(self.restaurant_feature2, sort_keys=True)
+ self.assertEqual(actual, self.restaurant_feature_str)
+
+ def test_decode(self):
+ """
+ Ensure a GeoJSON string can be decoded into GeoJSON objects
+ """
+ actual = geojson.loads(self.restaurant_str)
+ expected = self.restaurant1.__geo_interface__
+ self.assertEqual(expected, actual)
+
+ def test_decode_nested(self):
+ """
+ Ensure a GeoJSON string can be decoded into nested GeoJSON objects
+ """
+ actual = geojson.loads(self.restaurant_feature_str)
+ expected = self.restaurant_feature1.__geo_interface__
+ self.assertEqual(expected, actual)
+
+ nested = expected['geometry']
+ expected = self.restaurant1.__geo_interface__
+ self.assertEqual(nested, expected)
diff --git a/tests/test_null_geometries.py b/tests/test_null_geometries.py
new file mode 100644
index 0000000..5d90cac
--- /dev/null
+++ b/tests/test_null_geometries.py
@@ -0,0 +1,27 @@
+import unittest
+
+import geojson
+
+
+class NullGeometriesTest(unittest.TestCase):
+
+ def test_null_geometry_explicit(self):
+ feature = geojson.Feature(
+ id=12,
+ geometry=None,
+ properties={'foo': 'bar'}
+ )
+ actual = geojson.dumps(feature, sort_keys=True)
+ expected = ('{"geometry": null, "id": 12, "properties": {"foo": '
+ '"bar"}, "type": "Feature"}')
+ self.assertEqual(actual, expected)
+
+ def test_null_geometry_implicit(self):
+ feature = geojson.Feature(
+ id=12,
+ properties={'foo': 'bar'}
+ )
+ actual = geojson.dumps(feature, sort_keys=True)
+ expected = ('{"geometry": null, "id": 12, "properties": {"foo": '
+ '"bar"}, "type": "Feature"}')
+ self.assertEqual(actual, expected)
diff --git a/tests/test_strict_json.py b/tests/test_strict_json.py
new file mode 100644
index 0000000..2dfdfd9
--- /dev/null
+++ b/tests/test_strict_json.py
@@ -0,0 +1,52 @@
+"""
+GeoJSON produces and consumes only strict JSON. NAN and Infinity are not
+permissible values according to the JSON specification.
+"""
+import unittest
+
+import geojson
+
+
+class StrictJsonTest(unittest.TestCase):
+ def test_encode_nan(self):
+ """
+ Ensure Error is raised when encoding nan
+ """
+ unstrict = {
+ "type": "Point",
+ "coordinates": (float("nan"), 1.0),
+ }
+ self.assertRaises(ValueError, geojson.dumps, unstrict)
+
+ def test_encode_inf(self):
+ """
+ Ensure Error is raised when encoding inf or -inf
+ """
+ unstrict = {
+ "type": "Point",
+ "coordinates": (float("inf"), 1.0),
+ }
+ self.assertRaises(ValueError, geojson.dumps, unstrict)
+
+ unstrict = {
+ "type": "Point",
+ "coordinates": (float("-inf"), 1.0),
+ }
+ self.assertRaises(ValueError, geojson.dumps, unstrict)
+
+ def test_decode_nan(self):
+ """
+ Ensure Error is raised when decoding NaN
+ """
+ unstrict = '{"type": "Point", "coordinates": [1.0, NaN]}'
+ self.assertRaises(ValueError, geojson.loads, unstrict)
+
+ def test_decode_inf(self):
+ """
+ Ensure Error is raised when decoding Infinity or -Infinity
+ """
+ unstrict = '{"type": "Point", "coordinates": [1.0, Infinity]}'
+ self.assertRaises(ValueError, geojson.loads, unstrict)
+
+ unstrict = '{"type": "Point", "coordinates": [1.0, -Infinity]}'
+ self.assertRaises(ValueError, geojson.loads, unstrict)
diff --git a/tests/test_validation.py b/tests/test_validation.py
new file mode 100644
index 0000000..45d47d4
--- /dev/null
+++ b/tests/test_validation.py
@@ -0,0 +1,130 @@
+# -*- coding: utf-8 -*-
+"""
+Tests for geojson/validation
+"""
+
+import unittest
+
+import geojson
+
+is_valid = geojson.is_valid
+YES = 'yes'
+NO = 'no'
+
+
+class TestValidationGeometry(unittest.TestCase):
+
+ def test_invalid_geometry_with_validate(self):
+ self.assertRaises(
+ ValueError, geojson.Point, (10, 20, 30), validate=True)
+
+ def test_invalid_geometry_without_validate(self):
+ try:
+ geojson.Point((10, 20, 30))
+ geojson.Point((10, 20, 30), validate=False)
+ except ValueError:
+ self.fail("Point raised ValueError unexpectedly")
+
+ def test_valid_geometry(self):
+ try:
+ geojson.Point((10, 20), validate=True)
+ geojson.Point((10, 20), validate=False)
+ except ValueError:
+ self.fail("Point raised ValueError unexpectedly")
+
+
+class TestValidationGeoJSONObject(unittest.TestCase):
+
+ def test_invalid_jsonobject(self):
+ obj = [1, 2, 3]
+ self.assertEqual(is_valid(obj)['valid'], NO)
+
+ def test_valid_jsonobject(self):
+ point = geojson.Point((-10.52, 2.33))
+ self.assertEqual(is_valid(point)['valid'], YES)
+
+
+class TestValidationPoint(unittest.TestCase):
+
+ def test_invalid_point(self):
+ point = geojson.Point((10, 20, 30))
+ self.assertEqual(is_valid(point)['valid'], NO)
+
+ def test_valid_point(self):
+ point = geojson.Point((-3.68, 40.41))
+ self.assertEqual(is_valid(point)['valid'], YES)
+
+
+class TestValidationMultipoint(unittest.TestCase):
+
+ def test_invalid_multipoint(self):
+ mpoint = geojson.MultiPoint(
+ [(3.5887, 10.44558), (2.5555, 3.887), (2.44, 3.44, 2.555)])
+ self.assertEqual(is_valid(mpoint)['valid'], NO)
+
+ def test_valid_multipoint(self):
+ mpoint = geojson.MultiPoint([(10, 20), (30, 40)])
+ self.assertEqual(is_valid(mpoint)['valid'], YES)
+
+
+class TestValidationLineString(unittest.TestCase):
+
+ def test_invalid_linestring(self):
+ ls = geojson.LineString([(8.919, 44.4074)])
+ self.assertEqual(is_valid(ls)['valid'], NO)
+
+ def test_valid_linestring(self):
+ ls = geojson.LineString([(10, 5), (4, 3)])
+ self.assertEqual(is_valid(ls)['valid'], YES)
+
+
+class TestValidationMultiLineString(unittest.TestCase):
+
+ def test_invalid_multilinestring(self):
+ mls = geojson.MultiLineString([[(10, 5), (20, 1)], []])
+ self.assertEqual(is_valid(mls)['valid'], NO)
+
+ def test_valid_multilinestring(self):
+ ls1 = [(3.75, 9.25), (-130.95, 1.52)]
+ ls2 = [(23.15, -34.25), (-1.35, -4.65), (3.45, 77.95)]
+ mls = geojson.MultiLineString([ls1, ls2])
+ self.assertEqual(is_valid(mls)['valid'], YES)
+
+
+class TestValidationPolygon(unittest.TestCase):
+
+ def test_invalid_polygon(self):
+ poly1 = geojson.Polygon(
+ [[(2.38, 57.322), (23.194, -20.28), (-120.43, 19.15)]])
+ self.assertEqual(is_valid(poly1)['valid'], NO)
+ poly2 = geojson.Polygon(
+ [[(2.38, 57.322), (23.194, -20.28),
+ (-120.43, 19.15), (2.38, 57.323)]])
+ self.assertEqual(is_valid(poly2)['valid'], NO)
+
+ def test_valid_polygon(self):
+ poly = geojson.Polygon(
+ [[(2.38, 57.322), (23.194, -20.28),
+ (-120.43, 19.15), (2.38, 57.322)]])
+ self.assertEqual(is_valid(poly)['valid'], YES)
+
+
+class TestValidationMultiPolygon(unittest.TestCase):
+
+ def test_invalid_multipolygon(self):
+ poly1 = [(2.38, 57.322), (23.194, -20.28),
+ (-120.43, 19.15), (25.44, -17.91)]
+ poly2 = [(2.38, 57.322), (23.194, -20.28),
+ (-120.43, 19.15), (2.38, 57.322)]
+ multipoly = geojson.MultiPolygon([poly1, poly2])
+ self.assertEqual(is_valid(multipoly)['valid'], NO)
+
+ def test_valid_multipolygon(self):
+ poly1 = [[(2.38, 57.322), (23.194, -20.28),
+ (-120.43, 19.15), (2.38, 57.322)]]
+ poly2 = [[(-5.34, 3.71), (28.74, 31.44),
+ (28.55, 19.10), (-5.34, 3.71)]]
+ poly3 = [[(3.14, 23.17), (51.34, 27.14),
+ (22, -18.11), (3.14, 23.17)]]
+ multipoly = geojson.MultiPolygon([poly1, poly2, poly3])
+ self.assertEqual(is_valid(multipoly)['valid'], YES)
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..e4608b3
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,10 @@
+# Tox (http://tox.testrun.org/) is a tool for running tests
+# in multiple virtualenvs. This configuration file will run the
+# test suite on all supported python versions. To use it, "pip install tox"
+# and then run "tox" from this directory.
+
+[tox]
+envlist = py{27,33,34,35}, pypy, pypy3
+
+[testenv]
+commands = {envpython} setup.py test