Added a new GeoJSON serialization format for GeoDjango

Thanks Reinout van Rees for the review.
This commit is contained in:
Claude Paroz 2014-11-08 17:08:12 +01:00
parent c5132382f0
commit 35dac5070b
11 changed files with 245 additions and 4 deletions

View File

@ -1,4 +1,5 @@
from django.apps import AppConfig from django.apps import AppConfig
from django.core import serializers
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -6,3 +7,7 @@ from django.utils.translation import ugettext_lazy as _
class GISConfig(AppConfig): class GISConfig(AppConfig):
name = 'django.contrib.gis' name = 'django.contrib.gis'
verbose_name = _("GIS") verbose_name = _("GIS")
def ready(self):
if 'geojson' not in serializers.BUILTIN_SERIALIZERS:
serializers.BUILTIN_SERIALIZERS['geojson'] = "django.contrib.gis.serializers.geojson"

View File

@ -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")

View File

@ -46,6 +46,12 @@ class Track(NamedModel):
line = models.LineStringField() line = models.LineStringField()
class MultiFields(NamedModel):
city = models.ForeignKey(City)
point = models.PointField()
poly = models.PolygonField()
class Truth(models.Model): class Truth(models.Model):
val = models.BooleanField(default=False) val = models.BooleanField(default=False)

View File

@ -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', '{}')

View File

@ -24,7 +24,7 @@ class Serializer(PythonSerializer):
""" """
internal_use_only = False internal_use_only = False
def start_serialization(self): def _init_options(self):
if json.__version__.split('.') >= ['2', '1', '3']: if json.__version__.split('.') >= ['2', '1', '3']:
# Use JS strings to represent Python Decimal instances (ticket #16850) # Use JS strings to represent Python Decimal instances (ticket #16850)
self.options.update({'use_decimal': False}) self.options.update({'use_decimal': False})
@ -35,6 +35,9 @@ class Serializer(PythonSerializer):
if self.options.get('indent'): if self.options.get('indent'):
# Prevent trailing spaces # Prevent trailing spaces
self.json_kwargs['separators'] = (',', ': ') self.json_kwargs['separators'] = (',', ': ')
def start_serialization(self):
self._init_options()
self.stream.write("[") self.stream.write("[")
def end_serialization(self): def end_serialization(self):

View File

@ -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'
}
}
]
}

View File

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

View File

@ -131,12 +131,15 @@ Minor features
:mod:`django.contrib.gis` :mod:`django.contrib.gis`
^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^
* Compatibility shims for ``SpatialRefSys`` and ``GeometryColumns`` changed in * A new :doc:`GeoJSON serializer </ref/contrib/gis/serializers>` is now
Django 1.2 have been removed. available.
* The Spatialite backend now supports ``Collect`` and ``Extent`` aggregates * The Spatialite backend now supports ``Collect`` and ``Extent`` aggregates
when the database version is 3.0 or later. 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` :mod:`django.contrib.messages`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -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 :ref:`format <serialization-formats>` will raise a
``django.core.serializers.SerializerDoesNotExist`` exception. ``django.core.serializers.SerializerDoesNotExist`` exception.
.. _subset-of-fields:
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 force_text(obj)
return super(LazyEncoder, self).default(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 .. _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 .. _ecma-262: http://www.ecma-international.org/ecma-262/5.1/#sec-15.9.1.15

View File

@ -578,7 +578,7 @@ def naturalKeyTest(format, self):
for format in [f for f in serializers.get_serializer_formats() 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 + '_serializer', curry(serializerTest, format))
setattr(SerializerTests, 'test_' + format + '_natural_key_serializer', curry(naturalKeySerializerTest, format)) setattr(SerializerTests, 'test_' + format + '_natural_key_serializer', curry(naturalKeySerializerTest, format))
setattr(SerializerTests, 'test_' + format + '_serializer_fields', curry(fieldsTest, format)) setattr(SerializerTests, 'test_' + format + '_serializer_fields', curry(fieldsTest, format))