Browse Source

Fixed #28738 -- Added the GeometryDistance function.

Francisco Couzo 6 years ago
parent
commit
0193bf874f

+ 1 - 0
AUTHORS

@@ -282,6 +282,7 @@ answer newbie questions, and generally made Django that much better:
     Florian Apolloner <florian@apolloner.eu>
     Florian Moussous <florian.moussous@gmail.com>
     Francisco Albarran Cristobal <pahko.xd@gmail.com>
+    Francisco Couzo <franciscouzo@gmail.com>
     François Freitag <mail@franek.fr>
     Frank Tegtmeyer <fte@fte.to>
     Frank Wierzbicki

+ 4 - 4
django/contrib/gis/db/backends/base/operations.py

@@ -40,10 +40,10 @@ class BaseSpatialOperations:
     unsupported_functions = {
         'Area', 'AsGeoJSON', 'AsGML', 'AsKML', 'AsSVG', 'Azimuth',
         'BoundingCircle', 'Centroid', 'Difference', 'Distance', 'Envelope',
-        'GeoHash', 'Intersection', 'IsValid', 'Length', 'LineLocatePoint',
-        'MakeValid', 'MemSize', 'NumGeometries', 'NumPoints', 'Perimeter',
-        'PointOnSurface', 'Reverse', 'Scale', 'SnapToGrid', 'SymDifference',
-        'Transform', 'Translate', 'Union',
+        'GeoHash', 'GeometryDistance', 'Intersection', 'IsValid', 'Length',
+        'LineLocatePoint', 'MakeValid', 'MemSize', 'NumGeometries',
+        'NumPoints', 'Perimeter', 'PointOnSurface', 'Reverse', 'Scale',
+        'SnapToGrid', 'SymDifference', 'Transform', 'Translate', 'Union',
     }
 
     # Constructors

+ 3 - 3
django/contrib/gis/db/backends/mysql/operations.py

@@ -54,9 +54,9 @@ class MySQLOperations(BaseSpatialOperations, DatabaseOperations):
     def unsupported_functions(self):
         unsupported = {
             'AsGML', 'AsKML', 'AsSVG', 'Azimuth', 'BoundingCircle',
-            'ForcePolygonCW', 'LineLocatePoint', 'MakeValid', 'MemSize',
-            'Perimeter', 'PointOnSurface', 'Reverse', 'Scale', 'SnapToGrid',
-            'Transform', 'Translate',
+            'ForcePolygonCW', 'GeometryDistance', 'LineLocatePoint',
+            'MakeValid', 'MemSize', 'Perimeter', 'PointOnSurface', 'Reverse',
+            'Scale', 'SnapToGrid', 'Transform', 'Translate',
         }
         if self.connection.mysql_is_mariadb:
             unsupported.update({'GeoHash', 'IsValid'})

+ 2 - 2
django/contrib/gis/db/backends/oracle/operations.py

@@ -107,8 +107,8 @@ class OracleOperations(BaseSpatialOperations, DatabaseOperations):
 
     unsupported_functions = {
         'AsGeoJSON', 'AsKML', 'AsSVG', 'Azimuth', 'ForcePolygonCW', 'GeoHash',
-        'LineLocatePoint', 'MakeValid', 'MemSize', 'Scale', 'SnapToGrid',
-        'Translate',
+        'GeometryDistance', 'LineLocatePoint', 'MakeValid', 'MemSize',
+        'Scale', 'SnapToGrid', 'Translate',
     }
 
     def geo_quote_name(self, name):

+ 1 - 1
django/contrib/gis/db/backends/spatialite/operations.py

@@ -79,7 +79,7 @@ class SpatiaLiteOperations(BaseSpatialOperations, DatabaseOperations):
 
     @cached_property
     def unsupported_functions(self):
-        unsupported = {'BoundingCircle', 'MemSize'}
+        unsupported = {'BoundingCircle', 'GeometryDistance', 'MemSize'}
         if not self.lwgeom_version():
             unsupported |= {'Azimuth', 'GeoHash', 'IsValid', 'MakeValid'}
         return unsupported

+ 9 - 1
django/contrib/gis/db/models/functions.py

@@ -48,7 +48,7 @@ class GeoFuncMixin:
         return self.source_expressions[self.geom_param_pos[0]].field
 
     def as_sql(self, compiler, connection, function=None, **extra_context):
-        if not self.function and not function:
+        if self.function is None and function is None:
             function = connection.ops.spatial_function_name(self.name)
         return super().as_sql(compiler, connection, function=function, **extra_context)
 
@@ -299,6 +299,14 @@ class GeoHash(GeoFunc):
         return clone.as_sql(compiler, connection, **extra_context)
 
 
+class GeometryDistance(GeoFunc):
+    output_field = FloatField()
+    arity = 2
+    function = ''
+    arg_joiner = ' <-> '
+    geom_param_pos = (0, 1)
+
+
 class Intersection(OracleToleranceMixin, GeomOutputGeoFunc):
     arity = 2
     geom_param_pos = (0, 1)

+ 24 - 11
docs/ref/contrib/gis/functions.txt

@@ -20,17 +20,17 @@ get a ``NotImplementedError`` exception.
 
 Function's summary:
 
-==================  ========================   ======================  =======================  ==================  =====================
-Measurement         Relationships              Operations              Editors                  Output format       Miscellaneous
-==================  ========================   ======================  =======================  ==================  =====================
-:class:`Area`       :class:`Azimuth`           :class:`Difference`     :class:`ForcePolygonCW`  :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:`LineLocatePoint`                           :class:`SnapToGrid`      :class:`GeoHash`
-..                  :class:`PointOnSurface`                            :class:`Transform`
-..                                                                     :class:`Translate`
-==================  ========================   ======================  =======================  ==================  =====================
+=========================  ========================  ======================  =======================  ==================  =====================
+Measurement                Relationships             Operations              Editors                  Output format       Miscellaneous
+=========================  ========================  ======================  =======================  ==================  =====================
+:class:`Area`              :class:`Azimuth`          :class:`Difference`     :class:`ForcePolygonCW`  :class:`AsGeoJSON`  :class:`IsValid`
+:class:`Distance`          :class:`BoundingCircle`   :class:`Intersection`   :class:`MakeValid`       :class:`AsGML`      :class:`MemSize`
+:class:`GeometryDistance`  :class:`Centroid`         :class:`SymDifference`  :class:`Reverse`         :class:`AsKML`      :class:`NumGeometries`
+:class:`Length`            :class:`Envelope`         :class:`Union`          :class:`Scale`           :class:`AsSVG`      :class:`NumPoints`
+:class:`Perimeter`         :class:`LineLocatePoint`                          :class:`SnapToGrid`      :class:`GeoHash`
+..                         :class:`PointOnSurface`                           :class:`Transform`
+..                                                                           :class:`Translate`
+=========================  ========================  ======================  =======================  ==================  =====================
 
 ``Area``
 ========
@@ -308,6 +308,19 @@ result.
 
 __ https://en.wikipedia.org/wiki/Geohash
 
+``GeometryDistance``
+====================
+
+.. class:: GeometryDistance(expr1, expr2, **extra)
+
+.. versionadded:: 3.0
+
+*Availability*: `PostGIS <https://postgis.net/docs/geometry_distance_knn.html>`__
+
+Accepts two geographic fields or expressions and returns the distance between
+them. When used in an :meth:`~django.db.models.query.QuerySet.order_by` clause,
+it provides index-assisted nearest-neighbor result sets.
+
 ``Intersection``
 ================
 

+ 3 - 0
docs/releases/3.0.txt

@@ -67,6 +67,9 @@ Minor features
 * Allowed MySQL spatial lookup functions to operate on real geometries.
   Previous support was limited to bounding boxes.
 
+* Added the :class:`~django.contrib.gis.db.models.functions.GeometryDistance`
+  function, supported on PostGIS.
+
 :mod:`django.contrib.messages`
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 

+ 15 - 0
tests/gis_tests/geoapp/test_functions.py

@@ -240,6 +240,21 @@ class GISFunctionsTests(FuncTestMixin, TestCase):
         self.assertEqual(ref_hash, h1.geohash[:len(ref_hash)])
         self.assertEqual(ref_hash[:5], h2.geohash)
 
+    @skipUnlessDBFeature('has_GeometryDistance_function')
+    def test_geometry_distance(self):
+        point = Point(-90, 40, srid=4326)
+        qs = City.objects.annotate(distance=functions.GeometryDistance('point', point)).order_by('distance')
+        self.assertEqual([city.distance for city in qs], [
+            2.99091995527296,
+            5.33507274054713,
+            9.33852187483721,
+            9.91769193646233,
+            11.556465744884,
+            14.713098433352,
+            34.3635252198568,
+            276.987855073372,
+        ])
+
     @skipUnlessDBFeature("has_Intersection_function")
     def test_intersection(self):
         geom = Point(5, 23, srid=4326)