diff --git a/README.rst b/README.rst index fc6af5d2..949a5a3f 100644 --- a/README.rst +++ b/README.rst @@ -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 `_), rounding values to @@ -97,6 +97,15 @@ This field takes three optional arguments: - ``auto_bbox``: If ``True``, the GeoJSON object will include a `bounding box `_, which is the smallest possible rectangle enclosing the geometry. +- ``transform`` (defaults to ``None``): Can be set to any value that is accepted by + |GEOSGeometry.transform|_. Set to `4326` if your input geometries are in another + projection and you want to produce output according to the `GeoJSON standard + `_. If ``None`` (or the + input geometries do not have a SRID), the output coordinates will not be + transformed. + +.. |GEOSGeometry.transform| replace:: ``GEOSGeometry.transform`` +.. _GEOSGeometry.transform: https://example.org **Note:** While ``precision`` and ``remove_duplicates`` are designed to reduce the byte size of the API response, they will also increase the processing time diff --git a/rest_framework_gis/fields.py b/rest_framework_gis/fields.py index bb0faa65..d228283f 100644 --- a/rest_framework_gis/fields.py +++ b/rest_framework_gis/fields.py @@ -18,7 +18,12 @@ 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=None, + **kwargs, ): """ :param auto_bbox: Whether the GeoJSON object should include a bounding box @@ -26,6 +31,7 @@ def __init__( 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') @@ -34,6 +40,10 @@ 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: + value.transform(self.transform) + geojson = GeoJsonDict(value.geojson) # in this case we're dealing with an empty point else: diff --git a/tests/django_restframework_gis_tests/migrations/0005_othersridlocation.py b/tests/django_restframework_gis_tests/migrations/0005_othersridlocation.py new file mode 100644 index 00000000..bf33c966 --- /dev/null +++ b/tests/django_restframework_gis_tests/migrations/0005_othersridlocation.py @@ -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, + }, + ), + ] diff --git a/tests/django_restframework_gis_tests/models.py b/tests/django_restframework_gis_tests/models.py index e9c044af..80a73c9e 100644 --- a/tests/django_restframework_gis_tests/models.py +++ b/tests/django_restframework_gis_tests/models.py @@ -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) diff --git a/tests/django_restframework_gis_tests/serializers.py b/tests/django_restframework_gis_tests/serializers.py index 6d4f7874..21cd7a53 100644 --- a/tests/django_restframework_gis_tests/serializers.py +++ b/tests/django_restframework_gis_tests/serializers.py @@ -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, @@ -13,6 +13,7 @@ MultiPointModel, MultiPolygonModel, Nullable, + OtherSridLocation, PointModel, PolygonModel, ) @@ -40,6 +41,7 @@ 'GeometrySerializerMethodFieldSerializer', 'GeometrySerializer', 'BoxedLocationGeoFeatureWithBBoxGeoFieldSerializer', + 'OtherSridLocationGeoSerializer', ] @@ -53,6 +55,17 @@ class Meta: fields = '__all__' +class OtherSridLocationGeoSerializer(gis_serializers.GeoFeatureModelSerializer): + """Other SRID location geo serializer""" + + geometry = GeometryField(auto_bbox=True, transform=4326) + + class Meta: + model = OtherSridLocation + geo_field = 'geometry' + fields = '__all__' + + class PaginatedLocationGeoSerializer(pagination.PageNumberPagination): page_size_query_param = 'limit' page_size = 40 diff --git a/tests/django_restframework_gis_tests/test_fields.py b/tests/django_restframework_gis_tests/test_fields.py index b081529d..72b31c8c 100644 --- a/tests/django_restframework_gis_tests/test_fields.py +++ b/tests/django_restframework_gis_tests/test_fields.py @@ -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", @@ -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(transform=4326) + 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, transform=4326) + 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) diff --git a/tests/django_restframework_gis_tests/tests.py b/tests/django_restframework_gis_tests/tests.py index 4a3a05e9..82e0d58a 100644 --- a/tests/django_restframework_gis_tests/tests.py +++ b/tests/django_restframework_gis_tests/tests.py @@ -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 @@ -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 = { diff --git a/tests/django_restframework_gis_tests/urls.py b/tests/django_restframework_gis_tests/urls.py index 17c61eba..b5201ae7 100644 --- a/tests/django_restframework_gis_tests/urls.py +++ b/tests/django_restframework_gis_tests/urls.py @@ -17,6 +17,11 @@ views.geojson_location_details, name='api_geojson_location_details', ), + path( + 'geojson-other-srid//', + views.other_srid_location_details, + name='api_other_srid_location_details', + ), path( 'geojson-nullable//', views.geojson_nullable_details, diff --git a/tests/django_restframework_gis_tests/views.py b/tests/django_restframework_gis_tests/views.py index cc96337b..8ce2694d 100644 --- a/tests/django_restframework_gis_tests/views.py +++ b/tests/django_restframework_gis_tests/views.py @@ -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, @@ -25,6 +32,7 @@ LocationGeoSerializer, NoGeoFeatureMethodSerializer, NoneGeoFeatureMethodSerializer, + OtherSridLocationGeoSerializer, PaginatedLocationGeoSerializer, PolygonModelSerializer, ) @@ -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