Skip to content

Commit 9bcf56b

Browse files
committed
[fix] Make GeoJSON output valid by transforming to WGS84
1 parent da5acef commit 9bcf56b

File tree

9 files changed

+223
-5
lines changed

9 files changed

+223
-5
lines changed

README.rst

+5-1
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ Provides a ``GeometryField``, which is a subclass of Django Rest Framework
8383
geometry fields, providing custom ``to_native`` and ``from_native``
8484
methods for GeoJSON input/output.
8585

86-
This field takes three optional arguments:
86+
This field takes four optional arguments:
8787

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

101105
**Note:** While ``precision`` and ``remove_duplicates`` are designed to reduce the
102106
byte size of the API response, they will also increase the processing time

rest_framework_gis/fields.py

+15-1
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,20 @@ class GeometryField(Field):
1818
type_name = 'GeometryField'
1919

2020
def __init__(
21-
self, precision=None, remove_duplicates=False, auto_bbox=False, **kwargs
21+
self,
22+
precision=None,
23+
remove_duplicates=False,
24+
auto_bbox=False,
25+
transform=4326,
26+
**kwargs,
2227
):
2328
"""
2429
:param auto_bbox: Whether the GeoJSON object should include a bounding box
2530
"""
2631
self.precision = precision
2732
self.auto_bbox = auto_bbox
2833
self.remove_dupes = remove_duplicates
34+
self.transform = transform
2935
super().__init__(**kwargs)
3036
self.style.setdefault('base_template', 'textarea.html')
3137

@@ -34,6 +40,14 @@ def to_representation(self, value):
3440
return value
3541
# we expect value to be a GEOSGeometry instance
3642
if value.geojson:
43+
# NOTE: For repeated transformations a gdal.CoordTransform is recommended
44+
if (
45+
self.transform is not None
46+
and value.srid is not None
47+
and value.srid != 4326
48+
):
49+
value.transform(self.transform)
50+
3751
geojson = GeoJsonDict(value.geojson)
3852
# in this case we're dealing with an empty point
3953
else:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Generated by Django 5.1.5 on 2025-01-24 17:38
2+
3+
import django.contrib.gis.db.models.fields
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
dependencies = [
9+
("django_restframework_gis_tests", "0004_auto_20240228_2357"),
10+
]
11+
12+
operations = [
13+
migrations.CreateModel(
14+
name="OtherSridLocation",
15+
fields=[
16+
(
17+
"id",
18+
models.AutoField(
19+
auto_created=True,
20+
primary_key=True,
21+
serialize=False,
22+
verbose_name="ID",
23+
),
24+
),
25+
("name", models.CharField(max_length=32)),
26+
("slug", models.SlugField(blank=True, max_length=128, unique=True)),
27+
("timestamp", models.DateTimeField(blank=True, null=True)),
28+
(
29+
"geometry",
30+
django.contrib.gis.db.models.fields.GeometryField(srid=31287),
31+
),
32+
],
33+
options={
34+
"abstract": False,
35+
},
36+
),
37+
]

tests/django_restframework_gis_tests/models.py

+4
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ class Location(BaseModelGeometry):
5454
pass
5555

5656

57+
class OtherSridLocation(BaseModelGeometry):
58+
geometry = models.GeometryField(srid=31287)
59+
60+
5761
class LocatedFile(BaseModelGeometry):
5862
file = models.FileField(upload_to='located_files', blank=True, null=True)
5963

tests/django_restframework_gis_tests/serializers.py

+14-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from rest_framework import pagination, serializers
33

44
from rest_framework_gis import serializers as gis_serializers
5-
from rest_framework_gis.fields import GeometrySerializerMethodField
5+
from rest_framework_gis.fields import GeometryField, GeometrySerializerMethodField
66

77
from .models import (
88
BoxedLocation,
@@ -13,6 +13,7 @@
1313
MultiPointModel,
1414
MultiPolygonModel,
1515
Nullable,
16+
OtherSridLocation,
1617
PointModel,
1718
PolygonModel,
1819
)
@@ -40,6 +41,7 @@
4041
'GeometrySerializerMethodFieldSerializer',
4142
'GeometrySerializer',
4243
'BoxedLocationGeoFeatureWithBBoxGeoFieldSerializer',
44+
'OtherSridLocationGeoSerializer',
4345
]
4446

4547

@@ -53,6 +55,17 @@ class Meta:
5355
fields = '__all__'
5456

5557

58+
class OtherSridLocationGeoSerializer(gis_serializers.GeoFeatureModelSerializer):
59+
"""Other SRID location geo serializer"""
60+
61+
geometry = GeometryField(auto_bbox=True)
62+
63+
class Meta:
64+
model = OtherSridLocation
65+
geo_field = 'geometry'
66+
fields = '__all__'
67+
68+
5669
class PaginatedLocationGeoSerializer(pagination.PageNumberPagination):
5770
page_size_query_param = 'limit'
5871
page_size = 40

tests/django_restframework_gis_tests/test_fields.py

+97
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from rest_framework_gis import serializers as gis_serializers
88

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

1112
MultiPoint = {
1213
"type": "MultiPoint",
@@ -141,6 +142,102 @@ def normalize(self, data):
141142
return data
142143

143144

145+
class TestTransform(BaseTestCase):
146+
def test_no_transform_4326_Point_no_srid(self):
147+
model = self.get_instance(Point)
148+
Serializer = self.create_serializer()
149+
data = Serializer(model).data
150+
151+
expected_coords = (-105.0162, 39.5742)
152+
for lat, lon in zip(
153+
data["geometry"]["coordinates"],
154+
expected_coords,
155+
):
156+
self.assertAlmostEqual(lat, lon, places=5)
157+
158+
def test_no_transform_4326_Point_set_srid(self):
159+
model = self.get_instance(Point)
160+
model.geometry.srid = 4326
161+
Serializer = self.create_serializer()
162+
data = Serializer(model).data
163+
164+
expected_coords = (-105.0162, 39.5742)
165+
for lat, lon in zip(
166+
data["geometry"]["coordinates"],
167+
expected_coords,
168+
):
169+
self.assertAlmostEqual(lat, lon, places=5)
170+
171+
def test_transform_Point_no_transform(self):
172+
model = self.get_instance(Point31287)
173+
model.geometry.srid = 31287
174+
Serializer = self.create_serializer(transform=None)
175+
data = Serializer(model).data
176+
177+
expected_coords = (625826.2376404074, 483198.2074507246)
178+
for lat, lon in zip(
179+
data["geometry"]["coordinates"],
180+
expected_coords,
181+
):
182+
self.assertAlmostEqual(lat, lon, places=5)
183+
184+
def test_transform_Point_no_srid(self):
185+
model = self.get_instance(Point31287)
186+
Serializer = self.create_serializer()
187+
data = Serializer(model).data
188+
189+
expected_coords = (625826.2376404074, 483198.2074507246)
190+
for lat, lon in zip(
191+
data["geometry"]["coordinates"],
192+
expected_coords,
193+
):
194+
self.assertAlmostEqual(lat, lon, places=5)
195+
196+
def test_transform_Point_to_4326(self):
197+
model = self.get_instance(Point31287)
198+
model.geometry.srid = 31287
199+
Serializer = self.create_serializer()
200+
data = Serializer(model).data
201+
202+
expected_coords = (16.372500007573713, 48.20833306345481)
203+
for lat, lon in zip(
204+
data["geometry"]["coordinates"],
205+
expected_coords,
206+
):
207+
self.assertAlmostEqual(lat, lon, places=5)
208+
209+
def test_transform_Point_to_3857(self):
210+
model = self.get_instance(Point31287)
211+
model.geometry.srid = 31287
212+
Serializer = self.create_serializer(transform=3857)
213+
data = Serializer(model).data
214+
215+
expected_coords = (1822578.363856016, 6141584.271938089)
216+
for lat, lon in zip(
217+
data["geometry"]["coordinates"],
218+
expected_coords,
219+
):
220+
self.assertAlmostEqual(lat, lon, places=1)
221+
222+
def test_transform_Point_bbox_to_4326(self):
223+
model = self.get_instance(Point31287)
224+
model.geometry.srid = 31287
225+
Serializer = self.create_serializer(auto_bbox=True)
226+
data = Serializer(model).data
227+
228+
expected_coords = (
229+
16.372500007573713,
230+
48.20833306345481,
231+
16.372500007573713,
232+
48.20833306345481,
233+
)
234+
for received, expected in zip(
235+
data["geometry"]["bbox"],
236+
expected_coords,
237+
):
238+
self.assertAlmostEqual(received, expected, places=5)
239+
240+
144241
class TestPrecision(BaseTestCase):
145242
def test_precision_Point(self):
146243
model = self.get_instance(Point)

tests/django_restframework_gis_tests/tests.py

+28-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from rest_framework_gis import serializers as gis_serializers
1515
from rest_framework_gis.fields import GeoJsonDict
1616

17-
from .models import LocatedFile, Location, Nullable
17+
from .models import LocatedFile, Location, Nullable, OtherSridLocation
1818
from .serializers import LocationGeoSerializer
1919

2020

@@ -310,6 +310,33 @@ def test_geojson_false_id_attribute_slug(self):
310310
with self.assertRaises(KeyError):
311311
response.data['id']
312312

313+
def test_geojson_srid_transforms_to_wgs84(self):
314+
location = OtherSridLocation.objects.create(
315+
name="other SRID location",
316+
geometry='POINT(625826.2376404074 483198.2074507246)',
317+
)
318+
url = reverse('api_other_srid_location_details', args=[location.id])
319+
response = self.client.get(url)
320+
expected_coords = (16.372500007573713, 48.20833306345481)
321+
expected_coords_bbox = (
322+
16.372500007573713,
323+
48.20833306345481,
324+
16.372500007573713,
325+
48.20833306345481,
326+
)
327+
self.assertEqual(response.data['properties']['name'], 'other SRID location')
328+
for received, expected in zip(
329+
response.data["geometry"]["coordinates"],
330+
expected_coords,
331+
):
332+
self.assertAlmostEqual(received, expected, places=5)
333+
334+
for received, expected in zip(
335+
response.data["geometry"]["bbox"],
336+
expected_coords_bbox,
337+
):
338+
self.assertAlmostEqual(received, expected, places=5)
339+
313340
def test_post_geojson_id_attribute(self):
314341
self.assertEqual(Location.objects.count(), 0)
315342
data = {

tests/django_restframework_gis_tests/urls.py

+5
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@
1717
views.geojson_location_details,
1818
name='api_geojson_location_details',
1919
),
20+
path(
21+
'geojson-other-srid/<int:pk>/',
22+
views.other_srid_location_details,
23+
name='api_other_srid_location_details',
24+
),
2025
path(
2126
'geojson-nullable/<int:pk>/',
2227
views.geojson_nullable_details,

tests/django_restframework_gis_tests/views.py

+18-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,14 @@
1111
)
1212
from rest_framework_gis.pagination import GeoJsonPagination
1313

14-
from .models import BoxedLocation, LocatedFile, Location, Nullable, PolygonModel
14+
from .models import (
15+
BoxedLocation,
16+
LocatedFile,
17+
Location,
18+
Nullable,
19+
OtherSridLocation,
20+
PolygonModel,
21+
)
1522
from .serializers import (
1623
BoxedLocationGeoFeatureSerializer,
1724
LocatedFileGeoFeatureSerializer,
@@ -25,6 +32,7 @@
2532
LocationGeoSerializer,
2633
NoGeoFeatureMethodSerializer,
2734
NoneGeoFeatureMethodSerializer,
35+
OtherSridLocationGeoSerializer,
2836
PaginatedLocationGeoSerializer,
2937
PolygonModelSerializer,
3038
)
@@ -49,6 +57,15 @@ class LocationDetails(generics.RetrieveUpdateDestroyAPIView):
4957
location_details = LocationDetails.as_view()
5058

5159

60+
class OtherSridLocationDetails(generics.RetrieveUpdateDestroyAPIView):
61+
model = OtherSridLocation
62+
serializer_class = OtherSridLocationGeoSerializer
63+
queryset = OtherSridLocation.objects.all()
64+
65+
66+
other_srid_location_details = OtherSridLocationDetails.as_view()
67+
68+
5269
class GeojsonLocationList(generics.ListCreateAPIView):
5370
model = Location
5471
serializer_class = LocationGeoFeatureSerializer

0 commit comments

Comments
 (0)