summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorClaude Paroz <claude@2xlibre.net>2014-11-08 17:08:12 +0100
committerClaude Paroz <claude@2xlibre.net>2014-11-15 18:07:18 +0100
commit35dac5070bc3422cd2c6c2f46a28d1a1af2a2c50 (patch)
treec7223989f443096a3a1882777cef46ed06963622
parentc5132382f081bd1b5a3618bbf23fa0cf720af14b (diff)
Added a new GeoJSON serialization format for GeoDjango
Thanks Reinout van Rees for the review.
-rw-r--r--django/contrib/gis/apps.py5
-rw-r--r--django/contrib/gis/serializers/__init__.py0
-rw-r--r--django/contrib/gis/serializers/geojson.py68
-rw-r--r--django/contrib/gis/tests/geoapp/models.py6
-rw-r--r--django/contrib/gis/tests/geoapp/test_serializers.py81
-rw-r--r--django/core/serializers/json.py5
-rw-r--r--docs/ref/contrib/gis/serializers.txt69
-rw-r--r--docs/ref/contrib/gis/utils.txt1
-rw-r--r--docs/releases/1.8.txt7
-rw-r--r--docs/topics/serialization.txt5
-rw-r--r--tests/serializers_regress/tests.py2
11 files changed, 245 insertions, 4 deletions
diff --git a/django/contrib/gis/apps.py b/django/contrib/gis/apps.py
index 225efb8bb5..6ca51e9b42 100644
--- a/django/contrib/gis/apps.py
+++ b/django/contrib/gis/apps.py
@@ -1,4 +1,5 @@
from django.apps import AppConfig
+from django.core import serializers
from django.utils.translation import ugettext_lazy as _
@@ -6,3 +7,7 @@ from django.utils.translation import ugettext_lazy as _
class GISConfig(AppConfig):
name = 'django.contrib.gis'
verbose_name = _("GIS")
+
+ def ready(self):
+ if 'geojson' not in serializers.BUILTIN_SERIALIZERS:
+ serializers.BUILTIN_SERIALIZERS['geojson'] = "django.contrib.gis.serializers.geojson"
diff --git a/django/contrib/gis/serializers/__init__.py b/django/contrib/gis/serializers/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/django/contrib/gis/serializers/__init__.py
diff --git a/django/contrib/gis/serializers/geojson.py b/django/contrib/gis/serializers/geojson.py
new file mode 100644
index 0000000000..573f85c045
--- /dev/null
+++ b/django/contrib/gis/serializers/geojson.py
@@ -0,0 +1,68 @@
+from __future__ import unicode_literals
+
+from django.contrib.gis.gdal import HAS_GDAL
+from django.core.serializers.base import SerializerDoesNotExist, SerializationError
+from django.core.serializers.json import Serializer as JSONSerializer
+
+if HAS_GDAL:
+ from django.contrib.gis.gdal import CoordTransform, SpatialReference
+
+
+class Serializer(JSONSerializer):
+ """
+ Convert a queryset to GeoJSON, http://geojson.org/
+ """
+ def _init_options(self):
+ super(Serializer, self)._init_options()
+ self.geometry_field = self.json_kwargs.pop('geometry_field', None)
+ self.srs = SpatialReference(self.json_kwargs.pop('srid', 4326))
+
+ def start_serialization(self):
+ if not HAS_GDAL:
+ # GDAL is needed for the geometry.geojson call
+ raise SerializationError("The geojson serializer requires the GDAL library.")
+ self._init_options()
+ self._cts = {} # cache of CoordTransform's
+ self.stream.write(
+ '{"type": "FeatureCollection", "crs": {"type": "name", "properties": {"name": "EPSG:%d"}},'
+ ' "features": [' % self.srs.srid)
+
+ def end_serialization(self):
+ self.stream.write(']}')
+
+ def start_object(self, obj):
+ super(Serializer, self).start_object(obj)
+ self._geometry = None
+ if self.geometry_field is None:
+ # Find the first declared geometry field
+ for field in obj._meta.fields:
+ if hasattr(field, 'geom_type'):
+ self.geometry_field = field.name
+ break
+
+ def get_dump_object(self, obj):
+ data = {
+ "type": "Feature",
+ "properties": self._current,
+ }
+ if self._geometry:
+ if self._geometry.srid != self.srs.srid:
+ # If needed, transform the geometry in the srid of the global geojson srid
+ if self._geometry.srid not in self._cts:
+ self._cts[self._geometry.srid] = CoordTransform(self._geometry.srs, self.srs)
+ self._geometry.transform(self._cts[self._geometry.srid])
+ data["geometry"] = eval(self._geometry.geojson)
+ else:
+ data["geometry"] = None
+ return data
+
+ def handle_field(self, obj, field):
+ if field.name == self.geometry_field:
+ self._geometry = field._get_val_from_obj(obj)
+ else:
+ super(Serializer, self).handle_field(obj, field)
+
+
+class Deserializer(object):
+ def __init__(self, *args, **kwargs):
+ raise SerializerDoesNotExist("geojson is a serialization-only serializer")
diff --git a/django/contrib/gis/tests/geoapp/models.py b/django/contrib/gis/tests/geoapp/models.py
index 59e54272fd..38e19cdea6 100644
--- a/django/contrib/gis/tests/geoapp/models.py
+++ b/django/contrib/gis/tests/geoapp/models.py
@@ -46,6 +46,12 @@ class Track(NamedModel):
line = models.LineStringField()
+class MultiFields(NamedModel):
+ city = models.ForeignKey(City)
+ point = models.PointField()
+ poly = models.PolygonField()
+
+
class Truth(models.Model):
val = models.BooleanField(default=False)
diff --git a/django/contrib/gis/tests/geoapp/test_serializers.py b/django/contrib/gis/tests/geoapp/test_serializers.py
new file mode 100644
index 0000000000..18809264f8
--- /dev/null
+++ b/django/contrib/gis/tests/geoapp/test_serializers.py
@@ -0,0 +1,81 @@
+from __future__ import unicode_literals
+
+import json
+
+from django.contrib.gis.geos import HAS_GEOS
+from django.core import serializers
+from django.test import TestCase, skipUnlessDBFeature
+
+from .models import City, MultiFields, PennsylvaniaCity
+
+if HAS_GEOS:
+ from django.contrib.gis.geos import LinearRing, Point, Polygon
+
+
+@skipUnlessDBFeature("gis_enabled")
+class GeoJSONSerializerTests(TestCase):
+ fixtures = ['initial']
+
+ def test_builtin_serializers(self):
+ """
+ 'geojson' should be listed in available serializers.
+ """
+ all_formats = set(serializers.get_serializer_formats())
+ public_formats = set(serializers.get_public_serializer_formats())
+
+ self.assertIn('geojson', all_formats),
+ self.assertIn('geojson', public_formats)
+
+ def test_serialization_base(self):
+ geojson = serializers.serialize('geojson', City.objects.all().order_by('name'))
+ try:
+ geodata = json.loads(geojson)
+ except Exception:
+ self.fail("Serialized output is not valid JSON")
+ self.assertEqual(len(geodata['features']), len(City.objects.all()))
+ self.assertEqual(geodata['features'][0]['geometry']['type'], 'Point')
+ self.assertEqual(geodata['features'][0]['properties']['name'], 'Chicago')
+
+ def test_geometry_field_option(self):
+ """
+ When a model has several geometry fields, the 'geometry_field' option
+ can be used to specify the field to use as the 'geometry' key.
+ """
+ MultiFields.objects.create(
+ city=City.objects.first(), name='Name', point=Point(5, 23),
+ poly=Polygon(LinearRing((0, 0), (0, 5), (5, 5), (5, 0), (0, 0))))
+
+ geojson = serializers.serialize('geojson', MultiFields.objects.all())
+ geodata = json.loads(geojson)
+ self.assertEqual(geodata['features'][0]['geometry']['type'], 'Point')
+
+ geojson = serializers.serialize('geojson', MultiFields.objects.all(),
+ geometry_field='poly')
+ geodata = json.loads(geojson)
+ self.assertEqual(geodata['features'][0]['geometry']['type'], 'Polygon')
+
+ def test_fields_option(self):
+ """
+ The fields option allows to define a subset of fields to be present in
+ the 'properties' of the generated output.
+ """
+ PennsylvaniaCity.objects.create(name='Mansfield', county='Tioga', point='POINT(-77.071445 41.823881)')
+ geojson = serializers.serialize('geojson', PennsylvaniaCity.objects.all(),
+ fields=('county', 'point'))
+ geodata = json.loads(geojson)
+ self.assertIn('county', geodata['features'][0]['properties'])
+ self.assertNotIn('founded', geodata['features'][0]['properties'])
+
+ def test_srid_option(self):
+ geojson = serializers.serialize('geojson', City.objects.all().order_by('name'), srid=2847)
+ geodata = json.loads(geojson)
+ self.assertEqual(
+ [int(c) for c in geodata['features'][0]['geometry']['coordinates']],
+ [1564802, 5613214])
+
+ def test_deserialization_exception(self):
+ """
+ GeoJSON cannot be deserialized.
+ """
+ with self.assertRaises(serializers.base.SerializerDoesNotExist):
+ serializers.deserialize('geojson', '{}')
diff --git a/django/core/serializers/json.py b/django/core/serializers/json.py
index 72053e1d49..614427f9a7 100644
--- a/django/core/serializers/json.py
+++ b/django/core/serializers/json.py
@@ -24,7 +24,7 @@ class Serializer(PythonSerializer):
"""
internal_use_only = False
- def start_serialization(self):
+ def _init_options(self):
if json.__version__.split('.') >= ['2', '1', '3']:
# Use JS strings to represent Python Decimal instances (ticket #16850)
self.options.update({'use_decimal': False})
@@ -35,6 +35,9 @@ class Serializer(PythonSerializer):
if self.options.get('indent'):
# Prevent trailing spaces
self.json_kwargs['separators'] = (',', ': ')
+
+ def start_serialization(self):
+ self._init_options()
self.stream.write("[")
def end_serialization(self):
diff --git a/docs/ref/contrib/gis/serializers.txt b/docs/ref/contrib/gis/serializers.txt
new file mode 100644
index 0000000000..4e445f03ad
--- /dev/null
+++ b/docs/ref/contrib/gis/serializers.txt
@@ -0,0 +1,69 @@
+.. _ref-geojson-serializer:
+
+==================
+GeoJSON Serializer
+==================
+
+.. versionadded:: 1.8
+
+.. module:: django.contrib.gis.serializers.geojson
+ :synopsis: Serialization of GeoDjango models in the GeoJSON format.
+
+GeoDjango provides a specific serializer for the `GeoJSON`__ format. The GDAL
+library is required for this serializer. See :doc:`/topics/serialization` for
+more information on serialization.
+
+__ http://geojson.org/
+
+The ``geojson`` serializer is not meant for round-tripping data, as it has no
+deserializer equivalent. For example, you cannot use :djadmin:`loaddata` to
+reload the output produced by this serializer. If you plan to reload the
+outputted data, use the plain :ref:`json serializer <serialization-formats-json>`
+instead.
+
+In addition to the options of the ``json`` serializer, the ``geojson``
+serializer accepts the following additional option when it is called by
+``serializers.serialize()``:
+
+* ``geometry_field``: A string containing the name of a geometry field to use
+ for the ``geometry`` key of the GeoJSON feature. This is only needed when you
+ have a model with more than one geometry field and you don't want to use the
+ first defined geometry field (by default, the first geometry field is picked).
+
+* ``srid``: The SRID to use for the ``geometry`` content. Defaults to 4326
+ (WGS 84).
+
+The :ref:`fields <subset-of-fields>` option can be used to limit fields that
+will be present in the ``properties`` key, as it works with all other
+serializers.
+
+Example::
+
+ from django.core.serializers import serialize
+ from my_app.models import City
+
+ serialize('geojson', City.objects.all(),
+ geometry_field='point',
+ fields=('name',))
+
+Would output::
+
+ {
+ 'type': 'FeatureCollection',
+ 'crs': {
+ 'type': 'name',
+ 'properties': {'name': 'EPSG:4326'}
+ },
+ 'features': [
+ {
+ 'type': 'Feature',
+ 'geometry': {
+ 'type': 'Point',
+ 'coordinates': [-87.650175, 41.850385]
+ },
+ 'properties': {
+ 'name': 'Chicago'
+ }
+ }
+ ]
+ }
diff --git a/docs/ref/contrib/gis/utils.txt b/docs/ref/contrib/gis/utils.txt
index fee6325437..539a4ca9a2 100644
--- a/docs/ref/contrib/gis/utils.txt
+++ b/docs/ref/contrib/gis/utils.txt
@@ -15,3 +15,4 @@ useful in creating geospatial Web applications.
layermapping
ogrinspect
+ serializers
diff --git a/docs/releases/1.8.txt b/docs/releases/1.8.txt
index 6bfe9435a6..dc3b4211c8 100644
--- a/docs/releases/1.8.txt
+++ b/docs/releases/1.8.txt
@@ -131,12 +131,15 @@ Minor features
:mod:`django.contrib.gis`
^^^^^^^^^^^^^^^^^^^^^^^^^^
-* Compatibility shims for ``SpatialRefSys`` and ``GeometryColumns`` changed in
- Django 1.2 have been removed.
+* A new :doc:`GeoJSON serializer </ref/contrib/gis/serializers>` is now
+ available.
* The Spatialite backend now supports ``Collect`` and ``Extent`` aggregates
when the database version is 3.0 or later.
+* Compatibility shims for ``SpatialRefSys`` and ``GeometryColumns`` changed in
+ Django 1.2 have been removed.
+
:mod:`django.contrib.messages`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/docs/topics/serialization.txt b/docs/topics/serialization.txt
index 8ddf6a6b57..514b472b01 100644
--- a/docs/topics/serialization.txt
+++ b/docs/topics/serialization.txt
@@ -47,6 +47,8 @@ This is useful if you want to serialize data directly to a file-like object
:ref:`format <serialization-formats>` will raise a
``django.core.serializers.SerializerDoesNotExist`` exception.
+.. _subset-of-fields:
+
Subset of fields
~~~~~~~~~~~~~~~~
@@ -257,6 +259,9 @@ In particular, :ref:`lazy translation objects <lazy-translations>` need a
return force_text(obj)
return super(LazyEncoder, self).default(obj)
+Also note that GeoDjango provides a :doc:`customized GeoJSON serializer
+</ref/contrib/gis/serializers>`.
+
.. _special encoder: http://docs.python.org/library/json.html#encoders-and-decoders
.. _ecma-262: http://www.ecma-international.org/ecma-262/5.1/#sec-15.9.1.15
diff --git a/tests/serializers_regress/tests.py b/tests/serializers_regress/tests.py
index 655fbe32a0..86b03c26f4 100644
--- a/tests/serializers_regress/tests.py
+++ b/tests/serializers_regress/tests.py
@@ -578,7 +578,7 @@ def naturalKeyTest(format, self):
for format in [f for f in serializers.get_serializer_formats()
- if not isinstance(serializers.get_serializer(f), serializers.BadSerializer)]:
+ if not isinstance(serializers.get_serializer(f), serializers.BadSerializer) and not f == 'geojson']:
setattr(SerializerTests, 'test_' + format + '_serializer', curry(serializerTest, format))
setattr(SerializerTests, 'test_' + format + '_natural_key_serializer', curry(naturalKeySerializerTest, format))
setattr(SerializerTests, 'test_' + format + '_serializer_fields', curry(fieldsTest, format))