Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[fix] Make GeoJSON output valid by transforming to WGS84 #326

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"If any other" this line is not clear, can you please rephrase it?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have rephrased these docs after setting the default value to None. I hope it is easier to understand now. What do you think?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ping @auvipy

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
15 changes: 14 additions & 1 deletion tests/django_restframework_gis_tests/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from rest_framework import pagination, serializers

from rest_framework_gis import serializers as gis_serializers
from rest_framework_gis.fields import GeometrySerializerMethodField
from rest_framework_gis.fields import GeometryField, GeometrySerializerMethodField

from .models import (
BoxedLocation,
Expand All @@ -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,17 @@ class Meta:
fields = '__all__'


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

geometry = GeometryField(auto_bbox=True)

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


class PaginatedLocationGeoSerializer(pagination.PageNumberPagination):
page_size_query_param = 'limit'
page_size = 40
Expand Down
97 changes: 97 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,102 @@ 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)

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

expected_coords = (
16.372500007573713,
48.20833306345481,
16.372500007573713,
48.20833306345481,
)
for received, expected in zip(
data["geometry"]["bbox"],
expected_coords,
):
self.assertAlmostEqual(received, expected, places=5)


class TestPrecision(BaseTestCase):
def test_precision_Point(self):
model = self.get_instance(Point)
Expand Down
29 changes: 28 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,33 @@ 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)
expected_coords_bbox = (
16.372500007573713,
48.20833306345481,
16.372500007573713,
48.20833306345481,
)
self.assertEqual(response.data['properties']['name'], 'other SRID location')
for received, expected in zip(
response.data["geometry"]["coordinates"],
expected_coords,
):
self.assertAlmostEqual(received, expected, places=5)

for received, expected in zip(
response.data["geometry"]["bbox"],
expected_coords_bbox,
):
self.assertAlmostEqual(received, expected, 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