diff --git a/django/contrib/gis/db/backends/base/operations.py b/django/contrib/gis/db/backends/base/operations.py index ba3d3bdf67..0732d517c6 100644 --- a/django/contrib/gis/db/backends/base/operations.py +++ b/django/contrib/gis/db/backends/base/operations.py @@ -25,7 +25,7 @@ class BaseSpatialOperations: # Blacklist/set of known unsupported functions of the backend unsupported_functions = { - 'Area', 'AsGeoJSON', 'AsGML', 'AsKML', 'AsSVG', + 'Area', 'AsGeoJSON', 'AsGML', 'AsKML', 'AsSVG', 'Azimuth', 'BoundingCircle', 'Centroid', 'Difference', 'Distance', 'Envelope', 'ForceRHR', 'GeoHash', 'Intersection', 'IsValid', 'Length', 'LineLocatePoint', 'MakeValid', 'MemSize', 'NumGeometries', diff --git a/django/contrib/gis/db/backends/mysql/operations.py b/django/contrib/gis/db/backends/mysql/operations.py index 33f148f5f6..0ea9f8f274 100644 --- a/django/contrib/gis/db/backends/mysql/operations.py +++ b/django/contrib/gis/db/backends/mysql/operations.py @@ -72,7 +72,7 @@ class MySQLOperations(BaseSpatialOperations, DatabaseOperations): @cached_property def unsupported_functions(self): unsupported = { - 'AsGML', 'AsKML', 'AsSVG', 'BoundingCircle', 'ForceRHR', + 'AsGML', 'AsKML', 'AsSVG', 'Azimuth', 'BoundingCircle', 'ForceRHR', 'LineLocatePoint', 'MakeValid', 'MemSize', 'Perimeter', 'PointOnSurface', 'Reverse', 'Scale', 'SnapToGrid', 'Transform', 'Translate', diff --git a/django/contrib/gis/db/backends/oracle/operations.py b/django/contrib/gis/db/backends/oracle/operations.py index 649449b160..cbe90d6f53 100644 --- a/django/contrib/gis/db/backends/oracle/operations.py +++ b/django/contrib/gis/db/backends/oracle/operations.py @@ -111,9 +111,9 @@ class OracleOperations(BaseSpatialOperations, DatabaseOperations): } unsupported_functions = { - 'AsGeoJSON', 'AsKML', 'AsSVG', 'Envelope', 'ForceRHR', 'GeoHash', - 'LineLocatePoint', 'MakeValid', 'MemSize', 'Scale', 'SnapToGrid', - 'Translate', + 'AsGeoJSON', 'AsKML', 'AsSVG', 'Azimuth', 'Envelope', 'ForceRHR', + 'GeoHash', 'LineLocatePoint', 'MakeValid', 'MemSize', 'Scale', + 'SnapToGrid', 'Translate', } def geo_quote_name(self, name): diff --git a/django/contrib/gis/db/backends/spatialite/operations.py b/django/contrib/gis/db/backends/spatialite/operations.py index 23b38694f8..42b079bbfa 100644 --- a/django/contrib/gis/db/backends/spatialite/operations.py +++ b/django/contrib/gis/db/backends/spatialite/operations.py @@ -93,7 +93,7 @@ class SpatiaLiteOperations(BaseSpatialOperations, DatabaseOperations): def unsupported_functions(self): unsupported = {'BoundingCircle', 'ForceRHR', 'MemSize'} if not self.lwgeom_version(): - unsupported |= {'GeoHash', 'IsValid', 'MakeValid'} + unsupported |= {'Azimuth', 'GeoHash', 'IsValid', 'MakeValid'} return unsupported @cached_property diff --git a/django/contrib/gis/db/models/functions.py b/django/contrib/gis/db/models/functions.py index ee2df7cfb4..3dd8efaa22 100644 --- a/django/contrib/gis/db/models/functions.py +++ b/django/contrib/gis/db/models/functions.py @@ -162,6 +162,12 @@ class Area(OracleToleranceMixin, GeoFunc): return self.as_sql(compiler, connection, **extra_context) +class Azimuth(GeoFunc): + output_field_class = FloatField + arity = 2 + geom_param_pos = (0, 1) + + class AsGeoJSON(GeoFunc): output_field_class = TextField diff --git a/docs/ref/contrib/gis/db-api.txt b/docs/ref/contrib/gis/db-api.txt index c3d7003636..cce38a3711 100644 --- a/docs/ref/contrib/gis/db-api.txt +++ b/docs/ref/contrib/gis/db-api.txt @@ -376,6 +376,7 @@ Function PostGIS Oracle MySQL Spat :class:`AsGML` X X X :class:`AsKML` X X :class:`AsSVG` X X +:class:`Azimuth` X X (LWGEOM) :class:`BoundingCircle` X X :class:`Centroid` X X X X :class:`Difference` X X X (≥ 5.6.1) X diff --git a/docs/ref/contrib/gis/functions.txt b/docs/ref/contrib/gis/functions.txt index 4bf1994ea6..6b3dc28593 100644 --- a/docs/ref/contrib/gis/functions.txt +++ b/docs/ref/contrib/gis/functions.txt @@ -23,11 +23,11 @@ Function's summary: ================== ======================= ====================== =================== ================== ===================== Measurement Relationships Operations Editors Output format Miscellaneous ================== ======================= ====================== =================== ================== ===================== -:class:`Area` :class:`BoundingCircle` :class:`Difference` :class:`ForceRHR` :class:`AsGeoJSON` :class:`IsValid` -:class:`Distance` :class:`Centroid` :class:`Intersection` :class:`MakeValid` :class:`AsGML` :class:`MemSize` -:class:`Length` :class:`Envelope` :class:`SymDifference` :class:`Reverse` :class:`AsKML` :class:`NumGeometries` -:class:`Perimeter` :class:`PointOnSurface` :class:`Union` :class:`Scale` :class:`AsSVG` :class:`NumPoints` -.. :class:`SnapToGrid` :class:`GeoHash` +:class:`Area` :class:`Azimuth` :class:`Difference` :class:`ForceRHR` :class:`AsGeoJSON` :class:`IsValid` +:class:`Distance` :class:`BoundingCircle` :class:`Intersection` :class:`MakeValid` :class:`AsGML` :class:`MemSize` +:class:`Length` :class:`Centroid` :class:`SymDifference` :class:`Reverse` :class:`AsKML` :class:`NumGeometries` +:class:`Perimeter` :class:`Envelope` :class:`Union` :class:`Scale` :class:`AsSVG` :class:`NumPoints` +.. :class:`PointOnSurface` :class:`SnapToGrid` :class:`GeoHash` .. :class:`Transform` .. :class:`Translate` ================== ======================= ====================== =================== ================== ===================== @@ -173,6 +173,21 @@ Keyword Argument Description __ http://www.w3.org/Graphics/SVG/ +``Azimuth`` +=========== + +.. class:: Azimuth(point_a, point_b, **extra) + +.. versionadded:: 2.0 + +*Availability*: `PostGIS `__, +SpatiaLite (LWGEOM) + +Returns the azimuth in radians of the segment defined by the given point +geometries, or ``None`` if the two points are coincident. The azimuth is angle +referenced from north and is positive clockwise: north = ``0``; east = ``π/2``; +south = ``π``; west = ``3π/2``. + ``BoundingCircle`` ================== diff --git a/docs/releases/2.0.txt b/docs/releases/2.0.txt index bff9e76c88..f21d53cd8a 100644 --- a/docs/releases/2.0.txt +++ b/docs/releases/2.0.txt @@ -68,8 +68,9 @@ Minor features :class:`~django.contrib.gis.db.models.functions.IsValid` function, and :lookup:`isvalid` lookup. -* Added the :class:`~django.contrib.gis.db.models.functions.LineLocatePoint` - function, supported on PostGIS and SpatiaLite. +* Added the :class:`~django.contrib.gis.db.models.functions.Azimuth` and + :class:`~django.contrib.gis.db.models.functions.LineLocatePoint` functions, + supported on PostGIS and SpatiaLite. * Any :class:`~django.contrib.gis.geos.GEOSGeometry` imported from GeoJSON now has its SRID set. diff --git a/tests/gis_tests/geoapp/test_functions.py b/tests/gis_tests/geoapp/test_functions.py index ff6d1db9a8..6e4a87f212 100644 --- a/tests/gis_tests/geoapp/test_functions.py +++ b/tests/gis_tests/geoapp/test_functions.py @@ -1,4 +1,5 @@ import json +import math import re from decimal import Decimal @@ -145,6 +146,15 @@ class GISFunctionsTests(TestCase): self.assertEqual(svg1, City.objects.annotate(svg=functions.AsSVG('point')).get(name='Pueblo').svg) self.assertEqual(svg2, City.objects.annotate(svg=functions.AsSVG('point', relative=5)).get(name='Pueblo').svg) + @skipUnlessDBFeature("has_Azimuth_function") + def test_azimuth(self): + # Returns the azimuth in radians. + azimuth_expr = functions.Azimuth(Point(0, 0, srid=4326), Point(1, 1, srid=4326)) + self.assertAlmostEqual(City.objects.annotate(azimuth=azimuth_expr).first().azimuth, math.pi / 4) + # Returns None if the two points are coincident. + azimuth_expr = functions.Azimuth(Point(0, 0, srid=4326), Point(0, 0, srid=4326)) + self.assertIsNone(City.objects.annotate(azimuth=azimuth_expr).first().azimuth) + @skipUnlessDBFeature("has_BoundingCircle_function") def test_bounding_circle(self): def circle_num_points(num_seg):