From 7bdb9a90d06a955d0ad099f9b72e6c2c30cb89dc Mon Sep 17 00:00:00 2001
From: Justin Bronn <jbronn@gmail.com>
Date: Tue, 30 Mar 2010 23:15:43 +0000
Subject: [PATCH] PostGIS 1.5 allows distance queries on non-point geographic
 geometry columns with `ST_Distance_Sphere`, enabled this functionality.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@12890 bcc190cf-cafb-0310-a4f2-bffc1f526a37
---
 .../gis/db/backends/postgis/operations.py     | 12 ++++----
 django/contrib/gis/tests/distapp/tests.py     | 30 ++++++++++++-------
 docs/ref/contrib/gis/db-api.txt               | 24 +++++++++------
 docs/ref/contrib/gis/model-api.txt            | 22 +++++++-------
 4 files changed, 53 insertions(+), 35 deletions(-)

diff --git a/django/contrib/gis/db/backends/postgis/operations.py b/django/contrib/gis/db/backends/postgis/operations.py
index c4576a3574..eeb9105002 100644
--- a/django/contrib/gis/db/backends/postgis/operations.py
+++ b/django/contrib/gis/db/backends/postgis/operations.py
@@ -536,12 +536,14 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations):
                     op = op(self.geom_func_prefix, value[1])
                 elif lookup_type in self.distance_functions and lookup_type != 'dwithin':
                     if not field.geography and field.geodetic(self.connection):
-                        # Geodetic distances are only availble from Points to PointFields.
-                        if field.geom_type != 'POINT':
-                            raise ValueError('PostGIS spherical operations are only valid on PointFields.')
+                        # Geodetic distances are only availble from Points to
+                        # PointFields on PostGIS 1.4 and below.
+                        if not self.connection.ops.geography:
+                            if field.geom_type != 'POINT':
+                                raise ValueError('PostGIS spherical operations are only valid on PointFields.')
 
-                        if str(geom.geom_type) != 'Point':
-                            raise ValueError('PostGIS geometry distance parameter is required to be of type Point.')
+                            if str(geom.geom_type) != 'Point':
+                                raise ValueError('PostGIS geometry distance parameter is required to be of type Point.')
 
                         # Setting up the geodetic operation appropriately.
                         if nparams == 3 and value[2] == 'spheroid':
diff --git a/django/contrib/gis/tests/distapp/tests.py b/django/contrib/gis/tests/distapp/tests.py
index 28e50a3571..aacb610dcc 100644
--- a/django/contrib/gis/tests/distapp/tests.py
+++ b/django/contrib/gis/tests/distapp/tests.py
@@ -265,21 +265,31 @@ class DistanceTest(unittest.TestCase):
 
     def test05_geodetic_distance_lookups(self):
         "Testing distance lookups on geodetic coordinate systems."
-        if not oracle:
-            # Oracle doesn't have this limitation -- PostGIS only allows geodetic
-            # distance queries from Points to PointFields on geometry columns (geography
-            # columns don't have that limitation).
-            mp = GEOSGeometry('MULTIPOINT(0 0, 5 23)')
-            self.assertRaises(ValueError, len,
-                              AustraliaCity.objects.filter(point__distance_lte=(mp, D(km=100))))
+        # Line is from Canberra to Sydney.  Query is for all other cities within
+        # a 100km of that line (which should exclude only Hobart & Adelaide).
+        line = GEOSGeometry('LINESTRING(144.9630 -37.8143,151.2607 -33.8870)', 4326)
+        dist_qs = AustraliaCity.objects.filter(point__distance_lte=(line, D(km=100)))
+
+        if oracle or connection.ops.geography:
+            # Oracle and PostGIS 1.5 can do distance lookups on arbitrary geometries.
+            self.assertEqual(9, dist_qs.count())
+            self.assertEqual(['Batemans Bay', 'Canberra', 'Hillsdale',
+                              'Melbourne', 'Mittagong', 'Shellharbour',
+                              'Sydney', 'Thirroul', 'Wollongong'],
+                             self.get_names(dist_qs))
+        else:
+            # PostGIS 1.4 and below only allows geodetic distance queries (utilizing
+            # ST_Distance_Sphere/ST_Distance_Spheroid) from Points to PointFields
+            # on geometry columns.
+            self.assertRaises(ValueError, dist_qs.count)
 
             # Ensured that a ValueError was raised, none of the rest of the test is
             # support on this backend, so bail now.
             if spatialite: return
 
-            # Too many params (4 in this case) should raise a ValueError.
-            self.assertRaises(ValueError, len,
-                              AustraliaCity.objects.filter(point__distance_lte=('POINT(5 23)', D(km=100), 'spheroid', '4')))
+        # Too many params (4 in this case) should raise a ValueError.
+        self.assertRaises(ValueError, len,
+                          AustraliaCity.objects.filter(point__distance_lte=('POINT(5 23)', D(km=100), 'spheroid', '4')))
 
         # Not enough params should raise a ValueError.
         self.assertRaises(ValueError, len,
diff --git a/docs/ref/contrib/gis/db-api.txt b/docs/ref/contrib/gis/db-api.txt
index 017c9ec142..0959156c43 100644
--- a/docs/ref/contrib/gis/db-api.txt
+++ b/docs/ref/contrib/gis/db-api.txt
@@ -160,21 +160,26 @@ to be in the units of the field.
 
 .. note::
 
-    For PostGIS users, the routine ``ST_distance_sphere`` 
+    For users of PostGIS 1.4 and below, the routine ``ST_Distance_Sphere`` 
     is used by default for calculating distances on geographic coordinate systems 
-    -- which may only be called with point geometries [#fndistsphere]_.  Thus, 
-    geographic distance lookups on traditional PostGIS geometry columns are
+    (e.g., WGS84) -- which may only be called with point geometries [#fndistsphere14]_.
+    Thus, geographic distance lookups on traditional PostGIS geometry columns are
     only allowed on :class:`PointField` model fields using a point for the
     geometry parameter.
 
 .. note::
 
-    PostGIS 1.5 introduced :ref:`geography columns <geography-type>`, which
-    is limited on what geometry types distance queries are performed with.  In
-    other words, if you have ``geography=True`` in your geometry field
-    definition you'll be allowed to peform arbitrary distance queries with your
-    data in geodetic units of WGS84.
+    In PostGIS 1.5, ``ST_Distance_Sphere`` does *not* limit the geometry types
+    geographic distance queries are performed with. [#fndistsphere15]_  However,
+    these queries may take a long time, as great-circle distances must be
+    calculated on the fly for *every* row in the query.  This is because the
+    spatial index on traditional geometry fields cannot be used.
 
+    For much better performance on WGS84 distance queries, consider using
+    :ref:`geography columns <geography-type>` in your database instead because
+    they are able to use their spatial index in distance queries.
+    You can tell GeoDjango to use a geography column by setting ``geography=True``
+    in your field definition.
 
 For example, let's say we have a ``SouthTexasCity`` model (from the 
 `GeoDjango distance tests`__ ) on a *projected* coordinate system valid for cities 
@@ -300,5 +305,6 @@ Method                                PostGIS  Oracle  SpatiaLite
 .. [#fnwkt] *See* Open Geospatial Consortium, Inc., `OpenGIS Simple Feature Specification For SQL <http://www.opengis.org/docs/99-049.pdf>`_, Document 99-049 (May 5, 1999), at  Ch. 3.2.5, p. 3-11 (SQL Textual Representation of Geometry).
 .. [#fnewkb] *See* `PostGIS EWKB, EWKT and Canonical Forms <http://postgis.refractions.net/documentation/manual-1.5/ch04.html#EWKB_EWKT>`_, PostGIS documentation at Ch. 4.1.2.
 .. [#fngeojson] *See* Howard Butler, Martin Daly, Allan Doyle, Tim Schaub, & Christopher Schmidt, `The GeoJSON Format Specification <http://geojson.org/geojson-spec.html>`_, Revision 1.0 (June 16, 2008).
-.. [#fndistsphere] *See* PostGIS 1.5 ``ST_distance_sphere`` `documentation <http://postgis.refractions.net/documentation/manual-1.5/ST_Distance_Sphere.html>`_.
+.. [#fndistsphere14] *See* `PostGIS 1.4 documentation <http://postgis.refractions.net/documentation/manual-1.4/ST_Distance_Sphere.html>`_ on ``ST_distance_sphere``.
+.. [#fndistsphere15] *See* `PostGIS 1.5 documentation <http://postgis.refractions.net/documentation/manual-1.5/ST_Distance_Sphere.html>`_ on ``ST_distance_sphere``.
 .. [#] MySQL only supports bounding box operations (known as minimum bounding rectangles, or MBR, in MySQL).  Thus, spatial lookups such as :lookup:`contains <gis-contains>` are really equivalent to :lookup:`bbcontains`.
diff --git a/docs/ref/contrib/gis/model-api.txt b/docs/ref/contrib/gis/model-api.txt
index 6b7b20d478..7c83a7e267 100644
--- a/docs/ref/contrib/gis/model-api.txt
+++ b/docs/ref/contrib/gis/model-api.txt
@@ -107,10 +107,11 @@ a flat surface is a straight line, the shortest path between two points on a cur
 surface (such as the earth) is an *arc* of a `great circle`__. [#fnthematic]_  Thus,
 additional computation is required to obtain distances in planar units (e.g., 
 kilometers and miles).  Using a geographic coordinate system may introduce
-complications for the developer later on.  For example, PostGIS does not
-have the capability to perform distance calculations between non-point
-geometries using geographic coordinate systems, e.g., constructing a query to 
-find all points within 5 miles of a county boundary stored as WGS84. [#fndist]_
+complications for the developer later on.  For example, PostGIS versions 1.4
+and below do not have the capability to perform distance calculations between
+non-point geometries using geographic coordinate systems, e.g., constructing a
+query to  find all points within 5 miles of a county boundary stored as WGS84.
+[#fndist]_
 
 Portions of the earth's surface may projected onto a two-dimensional, or 
 Cartesian, plane.  Projected coordinate systems are especially convenient
@@ -123,9 +124,10 @@ calculations.
 .. note::
 
     If you wish to peform arbitrary distance queries using non-point
-    geometries, consider using PostGIS 1.5 and enabling the
-    :attr:`GeometryField.geography` keyword to use the
-    :ref:`geography database type <geography-type>` instead.
+    geometries in WGS84, consider upgrading to PostGIS 1.5. For
+    better performance, enable the :attr:`GeometryField.geography`
+    keyword so that :ref:`geography database type <geography-type>`
+    is used instead.
 
 Additional Resources:
 
@@ -182,7 +184,7 @@ three-dimensonal support.
 
 .. attribute:: GeometryField.geography
 
-If set to ``True``, this option will use create a database column of
+If set to ``True``, this option will create a database column of
 type geography, rather than geometry.  Please refer to the 
 :ref:`geography type <geography-type>` section below for more
 details.
@@ -223,8 +225,6 @@ For more information, the PostGIS documentation contains a helpful section on
 determining `when to use geography data type over geometry data type
 <http://postgis.refractions.net/documentation/manual-1.5/ch04.html#PostGIS_GeographyVSGeometry>`_.
 
-
-
 ``GeoManager``
 ==============
 
@@ -262,5 +262,5 @@ for example::
 .. [#fnsrid] Typically, SRID integer corresponds to an EPSG (`European Petroleum Survey Group <http://www.epsg.org>`_) identifier.  However, it may also be associated with custom projections defined in spatial database's spatial reference systems table.
 .. [#fnharvard] Harvard Graduate School of Design, `An Overview of Geodesy and Geographic Referencing Systems <http://www.gsd.harvard.edu/gis/manual/projections/fundamentals/>`_.  This is an excellent resource for an overview of principles relating to geographic and Cartesian coordinate systems. 
 .. [#fnthematic] Terry A. Slocum, Robert B. McMaster, Fritz C. Kessler, & Hugh H. Howard, *Thematic Cartography and Geographic Visualization* (Prentice Hall, 2nd edition), at Ch. 7.1.3.
-.. [#fndist] This isn't impossible using GeoDjango; you could for example, take a known point in a projected coordinate system, buffer it to the appropriate radius, and then perform an intersection operation with the buffer transformed to the geographic coordinate system.
+.. [#fndist] This limitation does not apply to PostGIS 1.5.  It should be noted that even in previous versions of PostGIS, this isn't impossible using GeoDjango; you could for example, take a known point in a projected coordinate system, buffer it to the appropriate radius, and then perform an intersection operation with the buffer transformed to the geographic coordinate system.
 .. [#fngeography] Please refer to the `PostGIS Geography Type <http://postgis.refractions.net/documentation/manual-1.5/ch04.html#PostGIS_Geography>`_ documentation for more details.