From 4884472447db11c57405edd5bdff36a171decd28 Mon Sep 17 00:00:00 2001 From: Sergey Fedoseev Date: Fri, 16 Dec 2016 03:36:18 +0600 Subject: [PATCH] Fixed #27576 -- Made get_srid_info() fallback to GDAL if SpatialRefSys is unavailable. --- django/contrib/gis/db/models/fields.py | 33 +++++++++++++------------- docs/ref/contrib/gis/functions.txt | 25 ++++++++++++++----- tests/gis_tests/distapp/tests.py | 7 +++--- 3 files changed, 39 insertions(+), 26 deletions(-) diff --git a/django/contrib/gis/db/models/fields.py b/django/contrib/gis/db/models/fields.py index 4ded5b1b4e..8dc272aaa4 100644 --- a/django/contrib/gis/db/models/fields.py +++ b/django/contrib/gis/db/models/fields.py @@ -1,8 +1,11 @@ +from collections import defaultdict + from django.contrib.gis import forms, gdal from django.contrib.gis.db.models.lookups import ( RasterBandTransform, gis_lookups, ) from django.contrib.gis.db.models.proxy import SpatialProxy +from django.contrib.gis.gdal import SpatialReference from django.contrib.gis.gdal.error import GDALException from django.contrib.gis.geometry.backend import Geometry, GeometryException from django.core.exceptions import ImproperlyConfigured @@ -14,7 +17,7 @@ from django.utils.translation import ugettext_lazy as _ # Local cache of the spatial_ref_sys table, which holds SRID data for each # spatial database alias. This cache exists so that the database isn't queried # for SRID info each time a distance query is constructed. -_srid_cache = {} +_srid_cache = defaultdict(dict) def get_srid_info(srid, connection): @@ -29,21 +32,21 @@ def get_srid_info(srid, connection): # The SpatialRefSys model for the spatial backend. SpatialRefSys = connection.ops.spatial_ref_sys() except NotImplementedError: - # No `spatial_ref_sys` table in spatial backend (e.g., MySQL). - return None, None, None + SpatialRefSys = None - if connection.alias not in _srid_cache: - # Initialize SRID dictionary for database if it doesn't exist. - _srid_cache[connection.alias] = {} + alias, get_srs = ( + (connection.alias, lambda srid: SpatialRefSys.objects.using(connection.alias).get(srid=srid).srs) + if SpatialRefSys else + (None, SpatialReference) + ) + if srid not in _srid_cache[alias]: + srs = get_srs(srid) + units, units_name = srs.units + sphere_name = srs['spheroid'] + spheroid = 'SPHEROID["%s",%s,%s]' % (sphere_name, srs.semi_major, srs.inverse_flattening) + _srid_cache[alias][srid] = (units, units_name, spheroid) - if srid not in _srid_cache[connection.alias]: - # Use `SpatialRefSys` model to query for spatial reference info. - sr = SpatialRefSys.objects.using(connection.alias).get(srid=srid) - units, units_name = sr.units - spheroid = SpatialRefSys.get_spheroid(sr.wkt) - _srid_cache[connection.alias][srid] = (units, units_name, spheroid) - - return _srid_cache[connection.alias][srid] + return _srid_cache[alias][srid] class GeoSelectFormatMixin(object): @@ -149,8 +152,6 @@ class BaseSpatialField(Field): system that uses non-projected units (e.g., latitude/longitude). """ units_name = self.units_name(connection) - # Some backends like MySQL cannot determine units name. In that case, - # test if srid is 4326 (WGS84), even if this is over-simplification. return units_name.lower() in self.geodetic_units if units_name else self.srid == 4326 def get_placeholder(self, value, compiler, connection): diff --git a/docs/ref/contrib/gis/functions.txt b/docs/ref/contrib/gis/functions.txt index 2917a658af..1fbe1fa212 100644 --- a/docs/ref/contrib/gis/functions.txt +++ b/docs/ref/contrib/gis/functions.txt @@ -41,8 +41,12 @@ Measurement Relationships Operations Editors Accepts a single geographic field or expression and returns the area of the field as an :class:`~django.contrib.gis.measure.Area` measure. On MySQL, a raw -float value is returned, as it's not possible to automatically determine the -unit of the field. +float value is returned when the coordinates are geodetic. + +.. versionchanged:: 1.11 + + In older versions, a raw value was returned on MySQL when used on + projected SRS. ``AsGeoJSON`` ============= @@ -211,8 +215,7 @@ geometry B. Accepts two geographic fields or expressions and returns the distance between them, as a :class:`~django.contrib.gis.measure.Distance` object. On MySQL, a raw -float value is returned, as it's not possible to automatically determine the -unit of the field. +float value is returned when the coordinates are geodetic. On backends that support distance calculation on geodetic coordinates, the proper backend function is automatically chosen depending on the SRID value of @@ -246,6 +249,11 @@ queryset is calculated:: in kilometers. See :doc:`measure` for usage details and the list of :ref:`supported_units`. +.. versionchanged:: 1.11 + + In older versions, a raw value was returned on MySQL when used on + projected SRS. + ``Envelope`` ============ @@ -325,14 +333,19 @@ Returns ``True`` if its value is a valid geometry and ``False`` otherwise. Accepts a single geographic linestring or multilinestring field or expression and returns its length as an :class:`~django.contrib.gis.measure.Distance` -measure. On MySQL, a raw float value is returned, as it's not possible to -automatically determine the unit of the field. +measure. On MySQL, a raw float value is returned when the coordinates +are geodetic. On PostGIS and SpatiaLite, when the coordinates are geodetic (angular), you can specify if the calculation should be based on a simple sphere (less accurate, less resource-intensive) or on a spheroid (more accurate, more resource-intensive) with the ``spheroid`` keyword argument. +.. versionchanged:: 1.11 + + In older versions, a raw value was returned on MySQL when used on + projected SRS. + ``MakeValid`` ============= diff --git a/tests/gis_tests/distapp/tests.py b/tests/gis_tests/distapp/tests.py index a62ea46280..6060f00461 100644 --- a/tests/gis_tests/distapp/tests.py +++ b/tests/gis_tests/distapp/tests.py @@ -475,8 +475,7 @@ class DistanceFunctionsTests(TestCase): # Tolerance has to be lower for Oracle tol = 2 for i, z in enumerate(SouthTexasZipcode.objects.annotate(area=Area('poly')).order_by('name')): - # MySQL is returning a raw float value - self.assertAlmostEqual(area_sq_m[i], z.area.sq_m if hasattr(z.area, 'sq_m') else z.area, tol) + self.assertAlmostEqual(area_sq_m[i], z.area.sq_m, tol) @skipUnlessDBFeature("has_Distance_function") def test_distance_simple(self): @@ -488,7 +487,7 @@ class DistanceFunctionsTests(TestCase): houston = SouthTexasCity.objects.annotate(dist=Distance('point', lagrange)).order_by('id').first() tol = 2 if oracle else 5 self.assertAlmostEqual( - houston.dist.m if hasattr(houston.dist, 'm') else houston.dist, + houston.dist.m, 147075.069813, tol ) @@ -656,7 +655,7 @@ class DistanceFunctionsTests(TestCase): # Now doing length on a projected coordinate system. i10 = SouthTexasInterstate.objects.annotate(length=Length('path')).get(name='I-10') - self.assertAlmostEqual(len_m2, i10.length.m if isinstance(i10.length, D) else i10.length, 2) + self.assertAlmostEqual(len_m2, i10.length.m, 2) self.assertTrue( SouthTexasInterstate.objects.annotate(length=Length('path')).filter(length__gt=4000).exists() )