Skip to content

Commit

Permalink
[fix] Make GeoJSON output valid by transforming to WGS84
Browse files Browse the repository at this point in the history
  • Loading branch information
StefanBrand committed Jan 24, 2025
1 parent da5acef commit e95eddc
Show file tree
Hide file tree
Showing 9 changed files with 190 additions and 4 deletions.
6 changes: 5 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ Provides a ``GeometryField``, which is a subclass of Django Rest Framework
geometry fields, providing custom ``to_native`` and ``from_native``
methods for GeoJSON input/output.

This field takes three optional arguments:
This field takes four optional arguments:

- ``precision``: Passes coordinates through Python's builtin ``round()`` function (`docs
<https://docs.python.org/3/library/functions.html#round>`_), rounding values to
Expand All @@ -97,6 +97,10 @@ This field takes three optional arguments:
- ``auto_bbox``: If ``True``, the GeoJSON object will include
a `bounding box <https://datatracker.ietf.org/doc/html/rfc7946#section-5>`_,
which is the smallest possible rectangle enclosing the geometry.
- ``transform`` (defaults to ``4326``): If ``None`` (or the input geometry does not have
a SRID), the GeoJSON's coordinates will not be transformed. If any other `spatial
reference <https://docs.djangoproject.com/en/5.0/ref/contrib/gis/geos/#django.contrib.gis.geos.GEOSGeometry.transform>`,
the GeoJSON's coordinates will be transformed correspondingly.

**Note:** While ``precision`` and ``remove_duplicates`` are designed to reduce the
byte size of the API response, they will also increase the processing time
Expand Down
16 changes: 15 additions & 1 deletion rest_framework_gis/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,20 @@ class GeometryField(Field):
type_name = 'GeometryField'

def __init__(
self, precision=None, remove_duplicates=False, auto_bbox=False, **kwargs
self,
precision=None,
remove_duplicates=False,
auto_bbox=False,
transform=4326,
**kwargs,
):
"""
:param auto_bbox: Whether the GeoJSON object should include a bounding box
"""
self.precision = precision
self.auto_bbox = auto_bbox
self.remove_dupes = remove_duplicates
self.transform = transform
super().__init__(**kwargs)
self.style.setdefault('base_template', 'textarea.html')

Expand All @@ -34,6 +40,14 @@ def to_representation(self, value):
return value
# we expect value to be a GEOSGeometry instance
if value.geojson:
# NOTE: For repeated transformations a gdal.CoordTransform is recommended
if (
self.transform is not None
and value.srid is not None
and value.srid != 4326
):
value.transform(self.transform)

geojson = GeoJsonDict(value.geojson)
# in this case we're dealing with an empty point
else:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Generated by Django 5.1.5 on 2025-01-24 17:38

import django.contrib.gis.db.models.fields
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("django_restframework_gis_tests", "0004_auto_20240228_2357"),
]

operations = [
migrations.CreateModel(
name="OtherSridLocation",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=32)),
("slug", models.SlugField(blank=True, max_length=128, unique=True)),
("timestamp", models.DateTimeField(blank=True, null=True)),
(
"geometry",
django.contrib.gis.db.models.fields.GeometryField(srid=31287),
),
],
options={
"abstract": False,
},
),
]
4 changes: 4 additions & 0 deletions tests/django_restframework_gis_tests/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ class Location(BaseModelGeometry):
pass


class OtherSridLocation(BaseModelGeometry):
geometry = models.GeometryField(srid=31287)


class LocatedFile(BaseModelGeometry):
file = models.FileField(upload_to='located_files', blank=True, null=True)

Expand Down
11 changes: 11 additions & 0 deletions tests/django_restframework_gis_tests/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
MultiPointModel,
MultiPolygonModel,
Nullable,
OtherSridLocation,
PointModel,
PolygonModel,
)
Expand Down Expand Up @@ -40,6 +41,7 @@
'GeometrySerializerMethodFieldSerializer',
'GeometrySerializer',
'BoxedLocationGeoFeatureWithBBoxGeoFieldSerializer',
'OtherSridLocationGeoSerializer',
]


Expand All @@ -53,6 +55,15 @@ class Meta:
fields = '__all__'


class OtherSridLocationGeoSerializer(gis_serializers.GeoFeatureModelSerializer):
"""Other SRID location geo serializer"""

class Meta:
model = OtherSridLocation
geo_field = 'geometry'
fields = '__all__'


class PaginatedLocationGeoSerializer(pagination.PageNumberPagination):
page_size_query_param = 'limit'
page_size = 40
Expand Down
79 changes: 79 additions & 0 deletions tests/django_restframework_gis_tests/test_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from rest_framework_gis import serializers as gis_serializers

Point = {"type": "Point", "coordinates": [-105.0162, 39.5742]}
Point31287 = {"type": "Point", "coordinates": [625826.2376404074, 483198.2074507246]}

MultiPoint = {
"type": "MultiPoint",
Expand Down Expand Up @@ -141,6 +142,84 @@ def normalize(self, data):
return data


class TestTransform(BaseTestCase):
def test_no_transform_4326_Point_no_srid(self):
model = self.get_instance(Point)
Serializer = self.create_serializer()
data = Serializer(model).data

expected_coords = (-105.0162, 39.5742)
for lat, lon in zip(
data["geometry"]["coordinates"],
expected_coords,
):
self.assertAlmostEqual(lat, lon, places=5)

def test_no_transform_4326_Point_set_srid(self):
model = self.get_instance(Point)
model.geometry.srid = 4326
Serializer = self.create_serializer()
data = Serializer(model).data

expected_coords = (-105.0162, 39.5742)
for lat, lon in zip(
data["geometry"]["coordinates"],
expected_coords,
):
self.assertAlmostEqual(lat, lon, places=5)

def test_transform_Point_no_transform(self):
model = self.get_instance(Point31287)
model.geometry.srid = 31287
Serializer = self.create_serializer(transform=None)
data = Serializer(model).data

expected_coords = (625826.2376404074, 483198.2074507246)
for lat, lon in zip(
data["geometry"]["coordinates"],
expected_coords,
):
self.assertAlmostEqual(lat, lon, places=5)

def test_transform_Point_no_srid(self):
model = self.get_instance(Point31287)
Serializer = self.create_serializer()
data = Serializer(model).data

expected_coords = (625826.2376404074, 483198.2074507246)
for lat, lon in zip(
data["geometry"]["coordinates"],
expected_coords,
):
self.assertAlmostEqual(lat, lon, places=5)

def test_transform_Point_to_4326(self):
model = self.get_instance(Point31287)
model.geometry.srid = 31287
Serializer = self.create_serializer()
data = Serializer(model).data

expected_coords = (16.372500007573713, 48.20833306345481)
for lat, lon in zip(
data["geometry"]["coordinates"],
expected_coords,
):
self.assertAlmostEqual(lat, lon, places=5)

def test_transform_Point_to_3857(self):
model = self.get_instance(Point31287)
model.geometry.srid = 31287
Serializer = self.create_serializer(transform=3857)
data = Serializer(model).data

expected_coords = (1822578.363856016, 6141584.271938089)
for lat, lon in zip(
data["geometry"]["coordinates"],
expected_coords,
):
self.assertAlmostEqual(lat, lon, places=1)


class TestPrecision(BaseTestCase):
def test_precision_Point(self):
model = self.get_instance(Point)
Expand Down
17 changes: 16 additions & 1 deletion tests/django_restframework_gis_tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from rest_framework_gis import serializers as gis_serializers
from rest_framework_gis.fields import GeoJsonDict

from .models import LocatedFile, Location, Nullable
from .models import LocatedFile, Location, Nullable, OtherSridLocation
from .serializers import LocationGeoSerializer


Expand Down Expand Up @@ -310,6 +310,21 @@ def test_geojson_false_id_attribute_slug(self):
with self.assertRaises(KeyError):
response.data['id']

def test_geojson_srid_transforms_to_wgs84(self):
location = OtherSridLocation.objects.create(
name="other SRID location",
geometry='POINT(625826.2376404074 483198.2074507246)',
)
url = reverse('api_other_srid_location_details', args=[location.id])
response = self.client.get(url)
expected_coords = (16.372500007573713, 48.20833306345481)
self.assertEqual(response.data['properties']['name'], 'other SRID location')
for lat, lon in zip(
response.data["geometry"]["coordinates"],
expected_coords,
):
self.assertAlmostEqual(lat, lon, places=5)

def test_post_geojson_id_attribute(self):
self.assertEqual(Location.objects.count(), 0)
data = {
Expand Down
5 changes: 5 additions & 0 deletions tests/django_restframework_gis_tests/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@
views.geojson_location_details,
name='api_geojson_location_details',
),
path(
'geojson-other-srid/<int:pk>/',
views.other_srid_location_details,
name='api_other_srid_location_details',
),
path(
'geojson-nullable/<int:pk>/',
views.geojson_nullable_details,
Expand Down
19 changes: 18 additions & 1 deletion tests/django_restframework_gis_tests/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,14 @@
)
from rest_framework_gis.pagination import GeoJsonPagination

from .models import BoxedLocation, LocatedFile, Location, Nullable, PolygonModel
from .models import (
BoxedLocation,
LocatedFile,
Location,
Nullable,
OtherSridLocation,
PolygonModel,
)
from .serializers import (
BoxedLocationGeoFeatureSerializer,
LocatedFileGeoFeatureSerializer,
Expand All @@ -25,6 +32,7 @@
LocationGeoSerializer,
NoGeoFeatureMethodSerializer,
NoneGeoFeatureMethodSerializer,
OtherSridLocationGeoSerializer,
PaginatedLocationGeoSerializer,
PolygonModelSerializer,
)
Expand All @@ -49,6 +57,15 @@ class LocationDetails(generics.RetrieveUpdateDestroyAPIView):
location_details = LocationDetails.as_view()


class OtherSridLocationDetails(generics.RetrieveUpdateDestroyAPIView):
model = OtherSridLocation
serializer_class = OtherSridLocationGeoSerializer
queryset = OtherSridLocation.objects.all()


other_srid_location_details = OtherSridLocationDetails.as_view()


class GeojsonLocationList(generics.ListCreateAPIView):
model = Location
serializer_class = LocationGeoFeatureSerializer
Expand Down

0 comments on commit e95eddc

Please sign in to comment.