Browse Source

Added a new GeoJSON serialization format for GeoDjango

Thanks Reinout van Rees for the review.
Claude Paroz 10 years ago
parent
commit
35dac5070b

+ 5 - 0
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"

+ 0 - 0
django/contrib/gis/serializers/__init__.py


+ 68 - 0
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")

+ 6 - 0
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)
 

+ 81 - 0
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', '{}')

+ 4 - 1
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):

+ 69 - 0
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'
+          }
+        }
+      ]
+    }

+ 1 - 0
docs/ref/contrib/gis/utils.txt

@@ -15,3 +15,4 @@ useful in creating geospatial Web applications.
 
    layermapping
    ogrinspect
+   serializers

+ 5 - 2
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`
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 

+ 5 - 0
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
 

+ 1 - 1
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))