mirror of
https://github.com/django/django.git
synced 2025-07-04 01:39:20 +00:00
gis: Fixed 6414, and applied DRY to spatial backend internals. Changes include:
(1) Support for distance calculations on geometry fields with geodetic coordinate systems (e.g., WGS84, the default). (2) The `get_db_prep_save` and `get_db_prep_lookup` have been moved from the spatial backends to common implementations in `GeometryField`. (3) Simplified SQL construction for `GeoQuerySet` methods. (4) `SpatialBackend` now contains all spatial backend dependent settings. git-svn-id: http://code.djangoproject.com/svn/django/branches/gis@7104 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
parent
d06e33a54d
commit
21a019681c
@ -9,66 +9,101 @@
|
||||
the backend.
|
||||
(3) The `parse_lookup` function, used for spatial SQL construction by
|
||||
the GeoQuerySet.
|
||||
(4) The `create_spatial_db`, and `get_geo_where_clause`
|
||||
routines (needed by `parse_lookup`).
|
||||
(4) The `create_spatial_db`, and `get_geo_where_clause`
|
||||
(needed by `parse_lookup`) functions.
|
||||
(5) The `SpatialBackend` object, which contains information specific
|
||||
to the spatial backend.
|
||||
"""
|
||||
from types import StringType, UnicodeType
|
||||
from django.conf import settings
|
||||
from django.db import connection
|
||||
from django.db.models.query import field_choices, find_field, get_where_clause, \
|
||||
FieldFound, LOOKUP_SEPARATOR, QUERY_TERMS
|
||||
from django.utils.datastructures import SortedDict
|
||||
from django.contrib.gis.geos import GEOSGeometry
|
||||
from django.contrib.gis.db.backend.util import gqn
|
||||
|
||||
# These routines (needed by GeoManager), default to False.
|
||||
ASGML, ASKML, DISTANCE, EXTENT, TRANSFORM, UNION, VERSION = (False, False, False, False, False, False, False)
|
||||
ASGML, ASKML, DISTANCE, DISTANCE_SPHEROID, EXTENT, TRANSFORM, UNION, VERSION = tuple(False for i in range(8))
|
||||
|
||||
# Lookup types in which the rest of the parameters are not
|
||||
# needed to be substitute in the WHERE SQL (e.g., the 'relate'
|
||||
# operation on Oracle does not need the mask substituted back
|
||||
# into the query SQL.).
|
||||
LIMITED_WHERE = []
|
||||
|
||||
# Retrieving the necessary settings from the backend.
|
||||
if settings.DATABASE_ENGINE == 'postgresql_psycopg2':
|
||||
# PostGIS is the spatial database, getting the rquired modules,
|
||||
# renaming as necessary.
|
||||
from django.contrib.gis.db.backend.postgis import \
|
||||
PostGISField as GeoBackendField, POSTGIS_TERMS as GIS_TERMS, \
|
||||
create_spatial_db, get_geo_where_clause, \
|
||||
ASGML, ASKML, DISTANCE, EXTENT, GEOM_SELECT, TRANSFORM, UNION, \
|
||||
from django.contrib.gis.db.backend.postgis.adaptor import \
|
||||
PostGISAdaptor as GeoAdaptor
|
||||
from django.contrib.gis.db.backend.postgis.field import \
|
||||
PostGISField as GeoBackendField
|
||||
from django.contrib.gis.db.backend.postgis.creation import create_spatial_db
|
||||
from django.contrib.gis.db.backend.postgis.query import \
|
||||
get_geo_where_clause, POSTGIS_TERMS as GIS_TERMS, \
|
||||
ASGML, ASKML, DISTANCE, DISTANCE_SPHEROID, DISTANCE_FUNCTIONS, \
|
||||
EXTENT, GEOM_SELECT, TRANSFORM, UNION, \
|
||||
MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2
|
||||
# PostGIS version info is needed to determine calling order of some
|
||||
# stored procedures (e.g., AsGML()).
|
||||
VERSION = (MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2)
|
||||
SPATIAL_BACKEND = 'postgis'
|
||||
elif settings.DATABASE_ENGINE == 'oracle':
|
||||
from django.contrib.gis.db.backend.oracle import \
|
||||
OracleSpatialField as GeoBackendField, \
|
||||
ORACLE_SPATIAL_TERMS as GIS_TERMS, \
|
||||
create_spatial_db, get_geo_where_clause, \
|
||||
ASGML, DISTANCE, GEOM_SELECT, TRANSFORM, UNION
|
||||
from django.contrib.gis.db.backend.oracle.adaptor import \
|
||||
OracleSpatialAdaptor as GeoAdaptor
|
||||
from django.contrib.gis.db.backend.oracle.field import \
|
||||
OracleSpatialField as GeoBackendField
|
||||
from django.contrib.gis.db.backend.oracle.creation import create_spatial_db
|
||||
from django.contrib.gis.db.backend.oracle.query import \
|
||||
get_geo_where_clause, ORACLE_SPATIAL_TERMS as GIS_TERMS, \
|
||||
ASGML, DISTANCE, DISTANCE_FUNCTIONS, GEOM_SELECT, TRANSFORM, UNION
|
||||
SPATIAL_BACKEND = 'oracle'
|
||||
LIMITED_WHERE = ['relate']
|
||||
elif settings.DATABASE_ENGINE == 'mysql':
|
||||
from django.contrib.gis.db.backend.mysql import \
|
||||
MySQLGeoField as GeoBackendField, \
|
||||
MYSQL_GIS_TERMS as GIS_TERMS, \
|
||||
create_spatial_db, get_geo_where_clause, \
|
||||
GEOM_SELECT
|
||||
from django.contrib.gis.db.backend.mysql.adaptor import \
|
||||
MySQLAdaptor as GeoAdaptor
|
||||
from django.contrib.gis.db.backend.mysql.field import \
|
||||
MySQLGeoField as GeoBackendField
|
||||
from django.contrib.gis.db.backend.mysql.creation import create_spatial_db
|
||||
from django.contrib.gis.db.backend.mysql.query import \
|
||||
get_geo_where_clause, MYSQL_GIS_TERMS as GIS_TERMS, GEOM_SELECT
|
||||
DISTANCE_FUNCTIONS = {}
|
||||
SPATIAL_BACKEND = 'mysql'
|
||||
else:
|
||||
raise NotImplementedError('No Geographic Backend exists for %s' % settings.DATABASE_ENGINE)
|
||||
|
||||
class SpatialBackend(object):
|
||||
"A container for properties of the Spatial Backend."
|
||||
"A container for properties of the SpatialBackend."
|
||||
# Stored procedure names used by the `GeoManager`.
|
||||
as_kml = ASKML
|
||||
as_gml = ASGML
|
||||
distance = DISTANCE
|
||||
distance_spheroid = DISTANCE_SPHEROID
|
||||
extent = EXTENT
|
||||
name = SPATIAL_BACKEND
|
||||
select = GEOM_SELECT
|
||||
transform = TRANSFORM
|
||||
union = UNION
|
||||
|
||||
# Version information, if defined.
|
||||
version = VERSION
|
||||
|
||||
# All valid GIS lookup terms, and distance functions.
|
||||
gis_terms = GIS_TERMS
|
||||
distance_functions = DISTANCE_FUNCTIONS
|
||||
|
||||
# Lookup types where additional WHERE parameters are excluded.
|
||||
limited_where = LIMITED_WHERE
|
||||
|
||||
# Class for the backend field.
|
||||
Field = GeoBackendField
|
||||
|
||||
# Adaptor class used for quoting GEOS geometries in the database.
|
||||
Adaptor = GeoAdaptor
|
||||
|
||||
#### query.py overloaded functions ####
|
||||
# parse_lookup() and lookup_inner() are modified from their django/db/models/query.py
|
||||
# counterparts to support constructing SQL for geographic queries.
|
||||
#
|
||||
# Status: Synced with r5982.
|
||||
# Status: Synced with r7098.
|
||||
#
|
||||
def parse_lookup(kwarg_items, opts):
|
||||
# Helper function that handles converting API kwargs
|
||||
@ -290,16 +325,17 @@ def lookup_inner(path, lookup_type, value, opts, table, column):
|
||||
# If the field is a geometry field, then the WHERE clause will need to be obtained
|
||||
# with the get_geo_where_clause()
|
||||
if hasattr(field, '_geom'):
|
||||
# Getting the preparation SQL object from the field.
|
||||
geo_prep = field.get_db_prep_lookup(lookup_type, value)
|
||||
# Getting additional SQL WHERE and params arrays associated with
|
||||
# the geographic field.
|
||||
geo_where, geo_params = field.get_db_prep_lookup(lookup_type, value)
|
||||
|
||||
# Getting the adapted geometry from the field.
|
||||
gwc = get_geo_where_clause(lookup_type, current_table, column, value)
|
||||
# Getting the geographic WHERE clause.
|
||||
gwc = get_geo_where_clause(lookup_type, current_table, field, value)
|
||||
|
||||
# Substituting in the the where parameters into the geographic where
|
||||
# clause, and extending the parameters.
|
||||
where.append(gwc % tuple(geo_prep.where))
|
||||
params.extend(geo_prep.params)
|
||||
# Appending the geographic WHERE componnents and parameters onto
|
||||
# the where and params arrays.
|
||||
where.append(gwc % tuple(geo_where))
|
||||
params.extend(geo_params)
|
||||
else:
|
||||
where.append(get_where_clause(lookup_type, current_table + '.', column, value, db_type))
|
||||
params.extend(field.get_db_prep_lookup(lookup_type, value))
|
||||
|
@ -1,12 +1 @@
|
||||
"""
|
||||
The MySQL spatial database backend module.
|
||||
|
||||
Please note that MySQL only supports bounding box queries, also
|
||||
known as MBRs (Minimum Bounding Rectangles). Moreover, spatial
|
||||
indices may only be used on MyISAM tables -- if you need
|
||||
transactions, take a look at PostGIS.
|
||||
"""
|
||||
|
||||
from django.contrib.gis.db.backend.mysql.creation import create_spatial_db
|
||||
from django.contrib.gis.db.backend.mysql.field import MySQLGeoField, gqn
|
||||
from django.contrib.gis.db.backend.mysql.query import get_geo_where_clause, MYSQL_GIS_TERMS, GEOM_SELECT
|
||||
|
10
django/contrib/gis/db/backend/mysql/adaptor.py
Normal file
10
django/contrib/gis/db/backend/mysql/adaptor.py
Normal file
@ -0,0 +1,10 @@
|
||||
"""
|
||||
This object provides quoting for GEOS geometries into MySQL.
|
||||
"""
|
||||
class MySQLAdaptor(object):
|
||||
def __init__(self, geom):
|
||||
self.wkt = geom.wkt
|
||||
|
||||
def __str__(self):
|
||||
"WKT is used as for the substitution value for the geometry."
|
||||
return self.wkt
|
@ -1,16 +1,9 @@
|
||||
import re
|
||||
from types import StringType, UnicodeType
|
||||
from django.db import connection
|
||||
from django.db.models.fields import Field # Django base Field class
|
||||
from django.contrib.gis.geos import GEOSGeometry
|
||||
from django.contrib.gis.db.backend.util import GeoFieldSQL
|
||||
from django.contrib.gis.db.backend.mysql.query import MYSQL_GIS_TERMS, GEOM_FROM_TEXT
|
||||
from django.contrib.gis.db.backend.mysql.query import GEOM_FROM_TEXT
|
||||
|
||||
# Quotename & geographic quotename, respectively.
|
||||
qn = connection.ops.quote_name
|
||||
def gqn(value):
|
||||
if isinstance(value, UnicodeType): value = value.encode('ascii')
|
||||
return "'%s'" % value
|
||||
|
||||
class MySQLGeoField(Field):
|
||||
"""
|
||||
@ -23,7 +16,7 @@ class MySQLGeoField(Field):
|
||||
used an R-Tree index is created, otherwise a B-Tree index is created.
|
||||
Thus, for best spatial performance, you should use MyISAM tables
|
||||
(which do not support transactions). For more information, see Ch.
|
||||
17.6.1 of the MySQL 5.0 documentation.
|
||||
16.6.1 of the MySQL 5.0 documentation.
|
||||
"""
|
||||
|
||||
# Getting the index name.
|
||||
@ -50,43 +43,11 @@ class MySQLGeoField(Field):
|
||||
def db_type(self):
|
||||
"The OpenGIS name is returned for the MySQL database column type."
|
||||
return self._geom
|
||||
|
||||
def get_db_prep_lookup(self, lookup_type, value):
|
||||
"""
|
||||
Returns field's value prepared for database lookup, accepts WKT and
|
||||
GEOS Geometries for the value.
|
||||
"""
|
||||
if lookup_type in MYSQL_GIS_TERMS:
|
||||
# special case for isnull lookup
|
||||
if lookup_type == 'isnull': return GeoFieldSQL([], [])
|
||||
|
||||
# When the input is not a GEOS geometry, attempt to construct one
|
||||
# from the given string input.
|
||||
if isinstance(value, GEOSGeometry):
|
||||
pass
|
||||
elif isinstance(value, (StringType, UnicodeType)):
|
||||
try:
|
||||
value = GEOSGeometry(value)
|
||||
except GEOSException:
|
||||
raise TypeError("Could not create geometry from lookup value: %s" % str(value))
|
||||
else:
|
||||
raise TypeError('Cannot use parameter of %s type as lookup parameter.' % type(value))
|
||||
|
||||
return GeoFieldSQL(['%s(%%s)' % GEOM_FROM_TEXT], [value])
|
||||
|
||||
else:
|
||||
raise TypeError("Field has invalid lookup: %s" % lookup_type)
|
||||
|
||||
def get_db_prep_save(self, value):
|
||||
"Prepares the value for saving in the database."
|
||||
if not bool(value): return None
|
||||
if isinstance(value, GEOSGeometry):
|
||||
return value
|
||||
else:
|
||||
raise TypeError('Geometry Proxy should only return GEOSGeometry objects.')
|
||||
|
||||
def get_placeholder(self, value):
|
||||
"""
|
||||
Nothing special happens here because MySQL does not support transformations.
|
||||
The placeholder here has to include MySQL's WKT constructor. Because
|
||||
MySQL does not support spatial transformations, there is no need to
|
||||
modify the placeholder based on the contents of the given value.
|
||||
"""
|
||||
return '%s(%%s)' % GEOM_FROM_TEXT
|
||||
|
@ -1,6 +1,11 @@
|
||||
"""
|
||||
This module contains the spatial lookup types, and the get_geo_where_clause()
|
||||
routine for MySQL
|
||||
This module contains the spatial lookup types, and the `get_geo_where_clause`
|
||||
routine for MySQL.
|
||||
|
||||
Please note that MySQL only supports bounding box queries, also
|
||||
known as MBRs (Minimum Bounding Rectangles). Moreover, spatial
|
||||
indices may only be used on MyISAM tables -- if you need
|
||||
transactions, take a look at PostGIS.
|
||||
"""
|
||||
from django.db import connection
|
||||
qn = connection.ops.quote_name
|
||||
@ -34,10 +39,10 @@ MYSQL_GIS_TERMS = MYSQL_GIS_FUNCTIONS.keys()
|
||||
MYSQL_GIS_TERMS += MISC_TERMS
|
||||
MYSQL_GIS_TERMS = tuple(MYSQL_GIS_TERMS) # Making immutable
|
||||
|
||||
def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
|
||||
def get_geo_where_clause(lookup_type, table_prefix, field, value):
|
||||
"Returns the SQL WHERE clause for use in MySQL spatial SQL construction."
|
||||
# Getting the quoted field as `geo_col`.
|
||||
geo_col = '%s.%s' % (qn(table_prefix), qn(field_name))
|
||||
geo_col = '%s.%s' % (qn(table_prefix), qn(field.column))
|
||||
|
||||
# See if a MySQL Geometry function matches the lookup type next
|
||||
lookup_info = MYSQL_GIS_FUNCTIONS.get(lookup_type, False)
|
||||
|
@ -1,14 +0,0 @@
|
||||
"""
|
||||
The Oracle spatial database backend module.
|
||||
|
||||
Please note that WKT support is broken on the XE version, and thus
|
||||
this backend will not work on such platforms. Specifically, XE lacks
|
||||
support for an internal JVM, and Java libraries are required to use
|
||||
the WKT constructors.
|
||||
"""
|
||||
from django.contrib.gis.db.backend.oracle.creation import create_spatial_db
|
||||
from django.contrib.gis.db.backend.oracle.field import OracleSpatialField, gqn
|
||||
from django.contrib.gis.db.backend.oracle.query import \
|
||||
get_geo_where_clause, ORACLE_SPATIAL_TERMS, \
|
||||
ASGML, DISTANCE, GEOM_SELECT, TRANSFORM, UNION
|
||||
|
@ -1,8 +1,6 @@
|
||||
"""
|
||||
This object provides the database adaptor for Oracle geometries.
|
||||
"""
|
||||
from cx_Oracle import CLOB
|
||||
|
||||
class OracleSpatialAdaptor(object):
|
||||
def __init__(self, geom):
|
||||
"Initializes only on the geometry object."
|
||||
@ -11,11 +9,3 @@ class OracleSpatialAdaptor(object):
|
||||
def __str__(self):
|
||||
"WKT is used for the substitution value of the geometry."
|
||||
return self.wkt
|
||||
|
||||
def oracle_type(self):
|
||||
"""
|
||||
The parameter type is a CLOB because no string (VARCHAR2) greater
|
||||
than 4000 characters will be accepted through the Oracle database
|
||||
API and/or SQL*Plus.
|
||||
"""
|
||||
return CLOB
|
||||
|
@ -1,18 +1,11 @@
|
||||
import re
|
||||
from types import StringType, UnicodeType
|
||||
from django.db import connection
|
||||
from django.db.backends.util import truncate_name
|
||||
from django.db.models.fields import Field # Django base Field class
|
||||
from django.contrib.gis.geos import GEOSGeometry
|
||||
from django.contrib.gis.db.backend.util import GeoFieldSQL
|
||||
from django.contrib.gis.db.backend.oracle.adaptor import OracleSpatialAdaptor
|
||||
from django.contrib.gis.db.backend.oracle.query import ORACLE_SPATIAL_TERMS, DISTANCE_FUNCTIONS, TRANSFORM
|
||||
from django.contrib.gis.db.backend.util import gqn
|
||||
from django.contrib.gis.db.backend.oracle.query import TRANSFORM
|
||||
|
||||
# Quotename & geographic quotename, respectively.
|
||||
qn = connection.ops.quote_name
|
||||
def gqn(value):
|
||||
if isinstance(value, UnicodeType): value = value.encode('ascii')
|
||||
return "'%s'" % value
|
||||
|
||||
class OracleSpatialField(Field):
|
||||
"""
|
||||
@ -95,64 +88,16 @@ class OracleSpatialField(Field):
|
||||
"The Oracle geometric data type is MDSYS.SDO_GEOMETRY."
|
||||
return 'MDSYS.SDO_GEOMETRY'
|
||||
|
||||
def get_db_prep_lookup(self, lookup_type, value):
|
||||
"""
|
||||
Returns field's value prepared for database lookup, accepts WKT and
|
||||
GEOS Geometries for the value.
|
||||
"""
|
||||
if lookup_type in ORACLE_SPATIAL_TERMS:
|
||||
# special case for isnull lookup
|
||||
if lookup_type == 'isnull': return GeoFieldSQL([], [])
|
||||
|
||||
# Get the geometry with SRID; defaults SRID to that
|
||||
# of the field if it is None
|
||||
geom = self.get_geometry(value)
|
||||
|
||||
# The adaptor will be used by psycopg2 for quoting the WKT.
|
||||
adapt = OracleSpatialAdaptor(geom)
|
||||
|
||||
if geom.srid != self._srid:
|
||||
# Adding the necessary string substitutions and parameters
|
||||
# to perform a geometry transformation.
|
||||
where = ['%s(SDO_GEOMETRY(%%s, %s), %%s)' % (TRANSFORM, geom.srid)]
|
||||
params = [adapt, self._srid]
|
||||
else:
|
||||
where = ['SDO_GEOMETRY(%%s, %s)' % geom.srid]
|
||||
params = [adapt]
|
||||
|
||||
if isinstance(value, tuple):
|
||||
if lookup_type in DISTANCE_FUNCTIONS or lookup_type == 'dwithin':
|
||||
# Getting the distance parameter in the units of the field
|
||||
where += [self.get_distance(value[1])]
|
||||
elif lookup_type == 'relate':
|
||||
# No extra where parameters for SDO_RELATE queries.
|
||||
pass
|
||||
else:
|
||||
where += map(gqn, value[1:])
|
||||
return GeoFieldSQL(where, params)
|
||||
else:
|
||||
raise TypeError("Field has invalid lookup: %s" % lookup_type)
|
||||
|
||||
def get_db_prep_save(self, value):
|
||||
"Prepares the value for saving in the database."
|
||||
if not bool(value):
|
||||
# Return an empty string for NULL -- but this doesn't work yet.
|
||||
return ''
|
||||
if isinstance(value, GEOSGeometry):
|
||||
return OracleSpatialAdaptor(value)
|
||||
else:
|
||||
raise TypeError('Geometry Proxy should only return GEOSGeometry objects.')
|
||||
|
||||
def get_placeholder(self, value):
|
||||
"""
|
||||
Provides a proper substitution value for Geometries that are not in the
|
||||
SRID of the field. Specifically, this routine will substitute in the
|
||||
SDO_CS.TRANSFORM() function call.
|
||||
"""
|
||||
if isinstance(value, GEOSGeometry) and value.srid != self._srid:
|
||||
if value is None:
|
||||
return '%s'
|
||||
elif value.srid != self._srid:
|
||||
# Adding Transform() to the SQL placeholder.
|
||||
return '%s(SDO_GEOMETRY(%%s, %s), %s)' % (TRANSFORM, value.srid, self._srid)
|
||||
elif value is None:
|
||||
return '%s'
|
||||
else:
|
||||
return 'SDO_GEOMETRY(%%s, %s)' % self._srid
|
||||
|
@ -1,6 +1,11 @@
|
||||
"""
|
||||
This module contains the spatial lookup types, and the get_geo_where_clause()
|
||||
This module contains the spatial lookup types, and the `get_geo_where_clause`
|
||||
routine for Oracle Spatial.
|
||||
|
||||
Please note that WKT support is broken on the XE version, and thus
|
||||
this backend will not work on such platforms. Specifically, XE lacks
|
||||
support for an internal JVM, and Java libraries are required to use
|
||||
the WKT constructors.
|
||||
"""
|
||||
import re
|
||||
from decimal import Decimal
|
||||
@ -25,8 +30,11 @@ GEOM_SELECT = 'SDO_UTIL.TO_WKTGEOMETRY(%s)'
|
||||
#### Classes used in constructing Oracle spatial SQL ####
|
||||
class SDOOperation(SpatialFunction):
|
||||
"Base class for SDO* Oracle operations."
|
||||
def __init__(self, func, end_subst=") %s '%s'"):
|
||||
super(SDOOperation, self).__init__(func, end_subst=end_subst, operator='=', result='TRUE')
|
||||
def __init__(self, func, **kwargs):
|
||||
kwargs.setdefault('operator', '=')
|
||||
kwargs.setdefault('result', 'TRUE')
|
||||
kwargs.setdefault('end_subst', ") %s '%s'")
|
||||
super(SDOOperation, self).__init__(func, **kwargs)
|
||||
|
||||
class SDODistance(SpatialFunction):
|
||||
"Class for Distance queries."
|
||||
@ -55,12 +63,14 @@ class SDORelate(SpatialFunction):
|
||||
#### Lookup type mapping dictionaries of Oracle spatial operations ####
|
||||
|
||||
# Valid distance types and substitutions
|
||||
dtypes = (Decimal, Distance, float, int)
|
||||
dtypes = (Decimal, Distance, float, int, long)
|
||||
DISTANCE_FUNCTIONS = {
|
||||
'distance_gt' : (SDODistance('>'), dtypes),
|
||||
'distance_gte' : (SDODistance('>='), dtypes),
|
||||
'distance_lt' : (SDODistance('<'), dtypes),
|
||||
'distance_lte' : (SDODistance('<='), dtypes),
|
||||
'dwithin' : (SDOOperation('SDO_WITHIN_DISTANCE',
|
||||
beg_subst="%s(%s, %%s, 'distance=%%s'"), dtypes),
|
||||
}
|
||||
|
||||
ORACLE_GEOMETRY_FUNCTIONS = {
|
||||
@ -68,7 +78,6 @@ ORACLE_GEOMETRY_FUNCTIONS = {
|
||||
'coveredby' : SDOOperation('SDO_COVEREDBY'),
|
||||
'covers' : SDOOperation('SDO_COVERS'),
|
||||
'disjoint' : SDOGeomRelate('DISJOINT'),
|
||||
'dwithin' : (SDOOperation('SDO_WITHIN_DISTANCE', end_subst=", %%s, 'distance=%%s') %s '%s'"), dtypes),
|
||||
'intersects' : SDOOperation('SDO_OVERLAPBDYINTERSECT'), # TODO: Is this really the same as ST_Intersects()?
|
||||
'equals' : SDOOperation('SDO_EQUAL'),
|
||||
'exact' : SDOOperation('SDO_EQUAL'),
|
||||
@ -89,10 +98,10 @@ ORACLE_SPATIAL_TERMS += MISC_TERMS
|
||||
ORACLE_SPATIAL_TERMS = tuple(ORACLE_SPATIAL_TERMS) # Making immutable
|
||||
|
||||
#### The `get_geo_where_clause` function for Oracle ####
|
||||
def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
|
||||
def get_geo_where_clause(lookup_type, table_prefix, field, value):
|
||||
"Returns the SQL WHERE clause for use in Oracle spatial SQL construction."
|
||||
# Getting the quoted table name as `geo_col`.
|
||||
geo_col = '%s.%s' % (qn(table_prefix), qn(field_name))
|
||||
geo_col = '%s.%s' % (qn(table_prefix), qn(field.column))
|
||||
|
||||
# See if a Oracle Geometry function matches the lookup type next
|
||||
lookup_info = ORACLE_GEOMETRY_FUNCTIONS.get(lookup_type, False)
|
||||
@ -122,7 +131,7 @@ def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
|
||||
# Otherwise, just call the `as_sql` method on the SDOOperation instance.
|
||||
return sdo_op.as_sql(geo_col)
|
||||
else:
|
||||
# Lookup info is a SDOOperation instance, whos `as_sql` method returns
|
||||
# Lookup info is a SDOOperation instance, whose `as_sql` method returns
|
||||
# the SQL necessary for the geometry function call. For example:
|
||||
# SDO_CONTAINS("geoapp_country"."poly", SDO_GEOMTRY('POINT(5 23)', 4326)) = 'TRUE'
|
||||
return lookup_info.as_sql(geo_col)
|
||||
|
@ -1,9 +0,0 @@
|
||||
"""
|
||||
The PostGIS spatial database backend module.
|
||||
"""
|
||||
from django.contrib.gis.db.backend.postgis.creation import create_spatial_db
|
||||
from django.contrib.gis.db.backend.postgis.field import PostGISField, gqn
|
||||
from django.contrib.gis.db.backend.postgis.query import \
|
||||
get_geo_where_clause, GEOM_FUNC_PREFIX, POSTGIS_TERMS, \
|
||||
MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2, \
|
||||
ASKML, ASGML, DISTANCE, EXTENT, GEOM_FROM_TEXT, UNION, TRANSFORM, GEOM_SELECT
|
@ -1,20 +1,11 @@
|
||||
from types import UnicodeType
|
||||
from django.db import connection
|
||||
from django.db.models.fields import Field # Django base Field class
|
||||
from django.contrib.gis.geos import GEOSGeometry
|
||||
from django.contrib.gis.db.backend.util import GeoFieldSQL
|
||||
from django.contrib.gis.db.backend.postgis.adaptor import PostGISAdaptor
|
||||
from django.contrib.gis.db.backend.postgis.query import \
|
||||
DISTANCE_FUNCTIONS, POSTGIS_TERMS, TRANSFORM
|
||||
from django.contrib.gis.db.backend.util import gqn
|
||||
from django.contrib.gis.db.backend.postgis.query import TRANSFORM
|
||||
|
||||
# Quotename & geographic quotename, respectively
|
||||
qn = connection.ops.quote_name
|
||||
def gqn(value):
|
||||
if isinstance(value, basestring):
|
||||
if isinstance(value, UnicodeType): value = value.encode('ascii')
|
||||
return "'%s'" % value
|
||||
else:
|
||||
return str(value)
|
||||
|
||||
class PostGISField(Field):
|
||||
"""
|
||||
@ -92,59 +83,14 @@ class PostGISField(Field):
|
||||
"""
|
||||
return None
|
||||
|
||||
def get_db_prep_lookup(self, lookup_type, value):
|
||||
"""
|
||||
Returns field's value prepared for database lookup, accepts WKT and
|
||||
GEOS Geometries for the value.
|
||||
"""
|
||||
if lookup_type in POSTGIS_TERMS:
|
||||
# special case for isnull lookup
|
||||
if lookup_type == 'isnull': return GeoFieldSQL([], [])
|
||||
|
||||
# Get the geometry with SRID; defaults SRID to
|
||||
# that of the field if it is None.
|
||||
geom = self.get_geometry(value)
|
||||
|
||||
# The adaptor will be used by psycopg2 for quoting the WKB.
|
||||
adapt = PostGISAdaptor(geom)
|
||||
|
||||
if geom.srid != self._srid:
|
||||
# Adding the necessary string substitutions and parameters
|
||||
# to perform a geometry transformation.
|
||||
where = ['%s(%%s,%%s)' % TRANSFORM]
|
||||
params = [adapt, self._srid]
|
||||
else:
|
||||
# Otherwise, the adaptor will take care of everything.
|
||||
where = ['%s']
|
||||
params = [adapt]
|
||||
|
||||
if isinstance(value, tuple):
|
||||
if lookup_type in DISTANCE_FUNCTIONS or lookup_type == 'dwithin':
|
||||
# Getting the distance parameter in the units of the field.
|
||||
where += [self.get_distance(value[1])]
|
||||
else:
|
||||
where += map(gqn, value[1:])
|
||||
return GeoFieldSQL(where, params)
|
||||
else:
|
||||
raise TypeError("Field has invalid lookup: %s" % lookup_type)
|
||||
|
||||
|
||||
def get_db_prep_save(self, value):
|
||||
"Prepares the value for saving in the database."
|
||||
if not bool(value): return None
|
||||
if isinstance(value, GEOSGeometry):
|
||||
return PostGISAdaptor(value)
|
||||
else:
|
||||
raise TypeError('Geometry Proxy should only return GEOSGeometry objects.')
|
||||
|
||||
def get_placeholder(self, value):
|
||||
"""
|
||||
Provides a proper substitution value for Geometries that are not in the
|
||||
SRID of the field. Specifically, this routine will substitute in the
|
||||
ST_Transform() function call.
|
||||
"""
|
||||
if isinstance(value, GEOSGeometry) and value.srid != self._srid:
|
||||
if value is None or value.srid == self._srid:
|
||||
return '%s'
|
||||
else:
|
||||
# Adding Transform() to the SQL placeholder.
|
||||
return '%s(%%s, %s)' % (TRANSFORM, self._srid)
|
||||
else:
|
||||
return '%s'
|
||||
|
@ -40,6 +40,7 @@ if MAJOR_VERSION >= 1:
|
||||
ASKML = get_func('AsKML')
|
||||
ASGML = get_func('AsGML')
|
||||
DISTANCE = get_func('Distance')
|
||||
DISTANCE_SPHEROID = get_func('distance_spheroid')
|
||||
EXTENT = get_func('extent')
|
||||
GEOM_FROM_TEXT = get_func('GeomFromText')
|
||||
GEOM_FROM_WKB = get_func('GeomFromWKB')
|
||||
@ -74,8 +75,20 @@ class PostGISFunctionParam(PostGISFunction):
|
||||
|
||||
class PostGISDistance(PostGISFunction):
|
||||
"For PostGIS distance operations."
|
||||
dist_func = 'Distance'
|
||||
def __init__(self, operator):
|
||||
super(PostGISDistance, self).__init__('Distance', end_subst=') %s %s', operator=operator, result='%%s')
|
||||
super(PostGISDistance, self).__init__(self.dist_func, end_subst=') %s %s',
|
||||
operator=operator, result='%%s')
|
||||
|
||||
class PostGISSphereDistance(PostGISFunction):
|
||||
"For PostGIS spherical distance operations."
|
||||
dist_func = 'distance_spheroid'
|
||||
def __init__(self, operator):
|
||||
# An extra parameter in `end_subst` is needed for the spheroid string.
|
||||
super(PostGISSphereDistance, self).__init__(self.dist_func,
|
||||
beg_subst='%s(%s, %%s, %%s',
|
||||
end_subst=') %s %s',
|
||||
operator=operator, result='%%s')
|
||||
|
||||
class PostGISRelate(PostGISFunctionParam):
|
||||
"For PostGIS Relate(<geom>, <pattern>) calls."
|
||||
@ -148,21 +161,24 @@ POSTGIS_GEOMETRY_FUNCTIONS = {
|
||||
}
|
||||
|
||||
# Valid distance types and substitutions
|
||||
dtypes = (Decimal, Distance, float, int)
|
||||
dtypes = (Decimal, Distance, float, int, long)
|
||||
def get_dist_ops(operator):
|
||||
"Returns operations for both regular and spherical distances."
|
||||
return (PostGISDistance(operator), PostGISSphereDistance(operator))
|
||||
DISTANCE_FUNCTIONS = {
|
||||
'distance_gt' : (PostGISDistance('>'), dtypes),
|
||||
'distance_gte' : (PostGISDistance('>='), dtypes),
|
||||
'distance_lt' : (PostGISDistance('<'), dtypes),
|
||||
'distance_lte' : (PostGISDistance('<='), dtypes),
|
||||
'distance_gt' : (get_dist_ops('>'), dtypes),
|
||||
'distance_gte' : (get_dist_ops('>='), dtypes),
|
||||
'distance_lt' : (get_dist_ops('<'), dtypes),
|
||||
'distance_lte' : (get_dist_ops('<='), dtypes),
|
||||
}
|
||||
|
||||
if GEOM_FUNC_PREFIX == 'ST_':
|
||||
# The ST_DWithin, ST_CoveredBy, and ST_Covers routines become available in 1.2.2+
|
||||
POSTGIS_GEOMETRY_FUNCTIONS.update(
|
||||
{'dwithin' : (PostGISFunctionParam('DWithin'), dtypes),
|
||||
'coveredby' : PostGISFunction('CoveredBy'),
|
||||
{'coveredby' : PostGISFunction('CoveredBy'),
|
||||
'covers' : PostGISFunction('Covers'),
|
||||
})
|
||||
DISTANCE_FUNCTIONS['dwithin'] = (PostGISFunctionParam('DWithin'), dtypes)
|
||||
|
||||
# Distance functions are a part of PostGIS geometry functions.
|
||||
POSTGIS_GEOMETRY_FUNCTIONS.update(DISTANCE_FUNCTIONS)
|
||||
@ -178,10 +194,10 @@ POSTGIS_TERMS += MISC_TERMS # Adding any other miscellaneous terms (e.g., 'isnul
|
||||
POSTGIS_TERMS = tuple(POSTGIS_TERMS) # Making immutable
|
||||
|
||||
#### The `get_geo_where_clause` function for PostGIS. ####
|
||||
def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
|
||||
def get_geo_where_clause(lookup_type, table_prefix, field, value):
|
||||
"Returns the SQL WHERE clause for use in PostGIS SQL construction."
|
||||
# Getting the quoted field as `geo_col`.
|
||||
geo_col = '%s.%s' % (qn(table_prefix), qn(field_name))
|
||||
geo_col = '%s.%s' % (qn(table_prefix), qn(field.column))
|
||||
if lookup_type in POSTGIS_OPERATORS:
|
||||
# See if a PostGIS operator matches the lookup type.
|
||||
return POSTGIS_OPERATORS[lookup_type].as_sql(geo_col)
|
||||
@ -198,7 +214,7 @@ def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
|
||||
op, arg_type = tmp
|
||||
|
||||
# Ensuring that a tuple _value_ was passed in from the user
|
||||
if not isinstance(value, tuple):
|
||||
if not isinstance(value, (tuple, list)):
|
||||
raise TypeError('Tuple required for `%s` lookup type.' % lookup_type)
|
||||
if len(value) != 2:
|
||||
raise ValueError('2-element tuple required or `%s` lookup type.' % lookup_type)
|
||||
@ -209,7 +225,18 @@ def get_geo_where_clause(lookup_type, table_prefix, field_name, value):
|
||||
|
||||
# For lookup type `relate`, the op instance is not yet created (has
|
||||
# to be instantiated here to check the pattern parameter).
|
||||
if lookup_type == 'relate': op = op(value[1])
|
||||
if lookup_type == 'relate':
|
||||
op = op(value[1])
|
||||
elif lookup_type in DISTANCE_FUNCTIONS and lookup_type != 'dwithin':
|
||||
if field._unit_name == 'degree':
|
||||
# Geodetic distances are only availble from Points to PointFields.
|
||||
if field._geom != 'POINT':
|
||||
raise TypeError('PostGIS spherical operations are only valid on PointFields.')
|
||||
if value[0].geom_typeid != 0:
|
||||
raise TypeError('PostGIS geometry distance parameter is required to be of type Point.')
|
||||
op = op[1]
|
||||
else:
|
||||
op = op[0]
|
||||
else:
|
||||
op = tmp
|
||||
# Calling the `as_sql` function on the operation instance.
|
||||
|
@ -1,23 +1,16 @@
|
||||
class GeoFieldSQL(object):
|
||||
"""
|
||||
Container for passing values to `parse_lookup` from the various
|
||||
backend geometry fields.
|
||||
"""
|
||||
def __init__(self, where=[], params=[]):
|
||||
self.where = where
|
||||
self.params = params
|
||||
from types import UnicodeType
|
||||
|
||||
def __str__(self):
|
||||
return self.as_sql()
|
||||
|
||||
def as_sql(self, quote=False):
|
||||
if not quote:
|
||||
return self.where[0] % tuple(self.params)
|
||||
else:
|
||||
# Used for quoting WKT on certain backends.
|
||||
tmp_params = ["'%s'" % self.params[0]]
|
||||
tmp_params.extend(self.params[1:])
|
||||
return self.where[0] % tuple(tmp_params)
|
||||
def gqn(val):
|
||||
"""
|
||||
The geographic quote name function; used for quoting tables and
|
||||
geometries (they use single rather than the double quotes of the
|
||||
backend quotename function).
|
||||
"""
|
||||
if isinstance(val, basestring):
|
||||
if isinstance(val, UnicodeType): val = val.encode('ascii')
|
||||
return "'%s'" % val
|
||||
else:
|
||||
return str(val)
|
||||
|
||||
class SpatialOperation(object):
|
||||
"""
|
||||
|
@ -1,7 +1,9 @@
|
||||
from decimal import Decimal
|
||||
from django.conf import settings
|
||||
from django.db import connection
|
||||
from django.contrib.gis.db.backend import GeoBackendField # these depend on the spatial database backend.
|
||||
# Getting the SpatialBackend container and the geographic quoting method.
|
||||
from django.contrib.gis.db.backend import SpatialBackend, gqn
|
||||
# GeometryProxy, GEOS, Distance, and oldforms imports.
|
||||
from django.contrib.gis.db.models.proxy import GeometryProxy
|
||||
from django.contrib.gis.geos import GEOSException, GEOSGeometry
|
||||
from django.contrib.gis.measure import Distance
|
||||
@ -14,7 +16,7 @@ except NotImplementedError:
|
||||
SpatialRefSys = None
|
||||
|
||||
#TODO: Flesh out widgets; consider adding support for OGR Geometry proxies.
|
||||
class GeometryField(GeoBackendField):
|
||||
class GeometryField(SpatialBackend.Field):
|
||||
"The base GIS field -- maps to the OpenGIS Specification Geometry type."
|
||||
|
||||
# The OpenGIS Geometry name.
|
||||
@ -38,21 +40,19 @@ class GeometryField(GeoBackendField):
|
||||
The number of dimensions for this geometry. Defaults to 2.
|
||||
"""
|
||||
|
||||
# Backward-compatibility notice, this will disappear in future revisions.
|
||||
if 'index' in kwargs:
|
||||
from warnings import warn
|
||||
warn('The `index` keyword has been deprecated, please use the `spatial_index` keyword instead.')
|
||||
self._index = kwargs['index']
|
||||
else:
|
||||
self._index = spatial_index
|
||||
# Setting the index flag with the value of the `spatial_index` keyword.
|
||||
self._index = spatial_index
|
||||
|
||||
# Setting the SRID and getting the units.
|
||||
# Setting the SRID and getting the units. Unit information must be
|
||||
# easily available in the field instance for distance queries.
|
||||
self._srid = srid
|
||||
if SpatialRefSys:
|
||||
# This doesn't work when we actually use: SpatialRefSys.objects.get(srid=srid)
|
||||
# Getting the spatial reference WKT associated with the SRID from the
|
||||
# `spatial_ref_sys` (or equivalent) spatial database table.
|
||||
#
|
||||
# The following doesn't work: SpatialRefSys.objects.get(srid=srid)
|
||||
# Why? `syncdb` fails to recognize installed geographic models when there's
|
||||
# an ORM query instantiated within a model field. No matter, this works fine
|
||||
# too.
|
||||
# an ORM query instantiated within a model field.
|
||||
cur = connection.cursor()
|
||||
qn = connection.ops.quote_name
|
||||
stmt = 'SELECT %(table)s.%(wkt_col)s FROM %(table)s WHERE (%(table)s.%(srid_col)s = %(srid)s)'
|
||||
@ -62,27 +62,48 @@ class GeometryField(GeoBackendField):
|
||||
'srid' : srid,
|
||||
}
|
||||
cur.execute(stmt)
|
||||
row = cur.fetchone()
|
||||
self._unit, self._unit_name = SpatialRefSys.get_units(row[0])
|
||||
srs_wkt = cur.fetchone()[0]
|
||||
|
||||
# Getting metadata associated with the spatial reference system identifier.
|
||||
# Specifically, getting the unit information and spheroid information
|
||||
# (both required for distance queries).
|
||||
self._unit, self._unit_name = SpatialRefSys.get_units(srs_wkt)
|
||||
self._spheroid = SpatialRefSys.get_spheroid(srs_wkt)
|
||||
|
||||
# Setting the dimension of the geometry field.
|
||||
self._dim = dim
|
||||
|
||||
super(GeometryField, self).__init__(**kwargs) # Calling the parent initializtion function
|
||||
|
||||
### Routines specific to GeometryField ###
|
||||
def get_distance(self, dist):
|
||||
"""
|
||||
Returns a distance number in units of the field. For example, if
|
||||
`D(km=1)` was passed in and the units of the field were in meters,
|
||||
then 1000 would be returned.
|
||||
"""
|
||||
if isinstance(dist, Distance):
|
||||
return getattr(dist, Distance.unit_attname(self._unit_name))
|
||||
elif isinstance(dist, (int, float, Decimal)):
|
||||
if self._unit_name in ('Decimal Degree', 'degree'):
|
||||
# Spherical distance calculation parameter should be in meters.
|
||||
dist_param = dist.m
|
||||
else:
|
||||
dist_param = getattr(dist, Distance.unit_attname(self._unit_name))
|
||||
else:
|
||||
# Assuming the distance is in the units of the field.
|
||||
return dist
|
||||
dist_param = dist
|
||||
|
||||
# Sphereical distance query; returning meters.
|
||||
if SpatialBackend.name == 'postgis' and self._unit_name == 'degree':
|
||||
return [gqn(self._spheroid), dist_param]
|
||||
else:
|
||||
return [dist_param]
|
||||
|
||||
def get_geometry(self, value):
|
||||
"""
|
||||
Retrieves the geometry, setting the default SRID from the given
|
||||
lookup parameters.
|
||||
"""
|
||||
if isinstance(value, tuple):
|
||||
if isinstance(value, (tuple, list)):
|
||||
geom = value[0]
|
||||
else:
|
||||
geom = value
|
||||
@ -121,6 +142,49 @@ class GeometryField(GeoBackendField):
|
||||
# Setup for lazy-instantiated GEOSGeometry object.
|
||||
setattr(cls, self.attname, GeometryProxy(GEOSGeometry, self))
|
||||
|
||||
def get_db_prep_lookup(self, lookup_type, value):
|
||||
"""
|
||||
Returns the spatial WHERE clause and associated parameters for the
|
||||
given lookup type and value. The value will be prepared for database
|
||||
lookup (e.g., spatial transformation SQL will be added if necessary).
|
||||
"""
|
||||
if lookup_type in SpatialBackend.gis_terms:
|
||||
# special case for isnull lookup
|
||||
if lookup_type == 'isnull': return [], []
|
||||
|
||||
# Get the geometry with SRID; defaults SRID to that of the field
|
||||
# if it is None.
|
||||
geom = self.get_geometry(value)
|
||||
|
||||
# Getting the WHERE clause list and the associated params list. The params
|
||||
# list is populated with the Adaptor wrapping the GEOSGeometry for the
|
||||
# backend. The WHERE clause list contains the placeholder for the adaptor
|
||||
# (e.g. any transformation SQL).
|
||||
where = [self.get_placeholder(geom)]
|
||||
params = [SpatialBackend.Adaptor(geom)]
|
||||
|
||||
if isinstance(value, (tuple, list)):
|
||||
if lookup_type in SpatialBackend.distance_functions:
|
||||
# Getting the distance parameter in the units of the field.
|
||||
where += self.get_distance(value[1])
|
||||
elif lookup_type in SpatialBackend.limited_where:
|
||||
pass
|
||||
else:
|
||||
# Otherwise, making sure any other parameters are properly quoted.
|
||||
where += map(gqn, value[1:])
|
||||
return where, params
|
||||
else:
|
||||
raise TypeError("Field has invalid lookup: %s" % lookup_type)
|
||||
|
||||
def get_db_prep_save(self, value):
|
||||
"Prepares the value for saving in the database."
|
||||
if isinstance(value, GEOSGeometry):
|
||||
return SpatialBackend.Adaptor(value)
|
||||
elif value is None:
|
||||
return None
|
||||
else:
|
||||
raise TypeError('Geometry Proxy should only return GEOSGeometry objects or None.')
|
||||
|
||||
def get_manipulator_field_objs(self):
|
||||
"Using the WKTField (defined above) to be our manipulator."
|
||||
return [WKTField]
|
||||
|
@ -6,7 +6,7 @@ from django.db.models.fields import FieldDoesNotExist
|
||||
from django.utils.datastructures import SortedDict
|
||||
from django.contrib.gis.db.models.fields import GeometryField
|
||||
# parse_lookup depends on the spatial database backend.
|
||||
from django.contrib.gis.db.backend import parse_lookup, SpatialBackend
|
||||
from django.contrib.gis.db.backend import gqn, parse_lookup, SpatialBackend
|
||||
from django.contrib.gis.geos import GEOSGeometry
|
||||
|
||||
# Shortcut booleans for determining the backend.
|
||||
@ -233,25 +233,20 @@ class GeoQuerySet(QuerySet):
|
||||
return "%s.%s" % (qn(self.model._meta.db_table),
|
||||
qn(field.column))
|
||||
|
||||
def _geo_column(self, field_name):
|
||||
def _geo_field(self, field_name=None):
|
||||
"""
|
||||
Helper function that returns False when the given field name is not an
|
||||
instance of a GeographicField, otherwise, the database column for the
|
||||
geographic field is returned.
|
||||
Returns the first Geometry field encountered; or specified via the
|
||||
`field_name` keyword.
|
||||
"""
|
||||
field = self.model._meta.get_field(field_name)
|
||||
if isinstance(field, GeometryField):
|
||||
return self._field_column(field)
|
||||
else:
|
||||
return False
|
||||
|
||||
def _get_geofield(self):
|
||||
"Returns the name of the first Geometry field encountered."
|
||||
for field in self.model._meta.fields:
|
||||
if isinstance(field, GeometryField):
|
||||
return field.name
|
||||
raise Exception('No GeometryFields in the model.')
|
||||
|
||||
fname = field.name
|
||||
if field_name:
|
||||
if field_name == field.name: return field
|
||||
else:
|
||||
return field
|
||||
raise False
|
||||
|
||||
def distance(self, *args, **kwargs):
|
||||
"""
|
||||
Returns the distance from the given geographic field name to the
|
||||
@ -266,7 +261,7 @@ class GeoQuerySet(QuerySet):
|
||||
# calculations from.
|
||||
nargs = len(args)
|
||||
if nargs == 1:
|
||||
field_name = self._get_geofield()
|
||||
field_name = None
|
||||
geom = args[0]
|
||||
elif nargs == 2:
|
||||
field_name, geom = args
|
||||
@ -274,29 +269,33 @@ class GeoQuerySet(QuerySet):
|
||||
raise ValueError('Maximum two arguments allowed for `distance` aggregate.')
|
||||
|
||||
# Getting the quoted column.
|
||||
field_col = self._geo_column(field_name)
|
||||
if not field_col:
|
||||
geo_field = self._geo_field(field_name)
|
||||
if not geo_field:
|
||||
raise TypeError('Distance output only available on GeometryFields.')
|
||||
geo_col = self._field_column(geo_field)
|
||||
|
||||
# Getting the geographic field instance.
|
||||
geo_field = self.model._meta.get_field(field_name)
|
||||
|
||||
# Using the field's get_db_prep_lookup() to get any needed
|
||||
# transformation SQL -- we pass in a 'dummy' `contains` lookup
|
||||
# type.
|
||||
geom_sql = geo_field.get_db_prep_lookup('contains', geom)
|
||||
# Using the field's get_db_prep_lookup() to get any needed
|
||||
# transformation and distance SQL -- we pass in a 'dummy'
|
||||
# `distance_lte` lookup type.
|
||||
where, params = geo_field.get_db_prep_lookup('distance_lte', (geom, 0))
|
||||
if oracle:
|
||||
# The `tolerance` keyword may be used for Oracle.
|
||||
tolerance = kwargs.get('tolerance', 0.05)
|
||||
|
||||
# More legwork here because the OracleSpatialAdaptor doesn't do
|
||||
# quoting of the WKT.
|
||||
params = ["'%s'" % geom_sql.params[0]]
|
||||
params.extend(geom_sql.params[1:])
|
||||
gsql = geom_sql.where[0] % tuple(params)
|
||||
dist_select = {'distance' : '%s(%s, %s, %s)' % (DISTANCE, field_col, gsql, tolerance)}
|
||||
tmp_params = [gqn(str(params[0]))]
|
||||
tmp_params.extend(params[1:])
|
||||
dsql = where[0] % tuple(tmp_params)
|
||||
dist_select = {'distance' : '%s(%s, %s, %s)' % (DISTANCE, geo_col, dsql, tolerance)}
|
||||
else:
|
||||
dist_select = {'distance' : '%s(%s, %s)' % (DISTANCE, field_col, geom_sql)}
|
||||
dsql = where[0] % tuple(params)
|
||||
if len(where) == 3:
|
||||
# Call to distance_spheroid() requires the spheroid as well.
|
||||
dist_sql = '%s(%s, %s, %s)' % (SpatialBackend.distance_spheroid, geo_col, dsql, where[1])
|
||||
else:
|
||||
dist_sql = '%s(%s, %s)' % (DISTANCE, geo_col, dsql)
|
||||
dist_select = {'distance' : dist_sql}
|
||||
return self.extra(select=dist_select)
|
||||
|
||||
def extent(self, field_name=None):
|
||||
@ -308,12 +307,10 @@ class GeoQuerySet(QuerySet):
|
||||
if not EXTENT:
|
||||
raise ImproperlyConfigured('Extent stored procedure not available.')
|
||||
|
||||
if not field_name:
|
||||
field_name = self._get_geofield()
|
||||
|
||||
field_col = self._geo_column(field_name)
|
||||
if not field_col:
|
||||
geo_field = self._geo_field(field_name)
|
||||
if not geo_field:
|
||||
raise TypeError('Extent information only available on GeometryFields.')
|
||||
geo_col = self._field_column(geo_field)
|
||||
|
||||
# Getting the SQL for the query.
|
||||
try:
|
||||
@ -322,7 +319,7 @@ class GeoQuerySet(QuerySet):
|
||||
return None
|
||||
|
||||
# Constructing the query that will select the extent.
|
||||
extent_sql = ('SELECT %s(%s)' % (EXTENT, field_col)) + sql
|
||||
extent_sql = ('SELECT %s(%s)' % (EXTENT, geo_col)) + sql
|
||||
|
||||
# Getting a cursor, executing the query, and extracting the returned
|
||||
# value from the extent function.
|
||||
@ -353,23 +350,21 @@ class GeoQuerySet(QuerySet):
|
||||
|
||||
# If no field name explicitly given, get the first GeometryField from
|
||||
# the model.
|
||||
if not field_name:
|
||||
field_name = self._get_geofield()
|
||||
|
||||
field_col = self._geo_column(field_name)
|
||||
if not field_col:
|
||||
geo_field = self._geo_field(field_name)
|
||||
if not geo_field:
|
||||
raise TypeError('GML output only available on GeometryFields.')
|
||||
|
||||
geo_col = self._field_column(geo_field)
|
||||
|
||||
if oracle:
|
||||
gml_select = {'gml':'%s(%s)' % (ASGML, field_col)}
|
||||
gml_select = {'gml':'%s(%s)' % (ASGML, geo_col)}
|
||||
elif postgis:
|
||||
# PostGIS AsGML() aggregate function parameter order depends on the
|
||||
# version -- uggh.
|
||||
major, minor1, minor2 = SpatialBackend.version
|
||||
if major >= 1 and (minor1 > 3 or (minor1 == 3 and minor2 > 1)):
|
||||
gml_select = {'gml':'%s(%s,%s,%s)' % (ASGML, version, field_col, precision)}
|
||||
gml_select = {'gml':'%s(%s,%s,%s)' % (ASGML, version, geo_col, precision)}
|
||||
else:
|
||||
gml_select = {'gml':'%s(%s,%s,%s)' % (ASGML, field_col, precision, version)}
|
||||
gml_select = {'gml':'%s(%s,%s,%s)' % (ASGML, geo_col, precision, version)}
|
||||
|
||||
# Adding GML function call to SELECT part of the SQL.
|
||||
return self.extra(select=gml_select)
|
||||
@ -385,48 +380,49 @@ class GeoQuerySet(QuerySet):
|
||||
raise ImproperlyConfigured('AsKML() stored procedure not available.')
|
||||
|
||||
# Getting the geographic field.
|
||||
if not field_name:
|
||||
field_name = self._get_geofield()
|
||||
|
||||
field_col = self._geo_column(field_name)
|
||||
if not field_col:
|
||||
geo_field = self._geo_field(field_name)
|
||||
if not geo_field:
|
||||
raise TypeError('KML output only available on GeometryFields.')
|
||||
|
||||
geo_col = self._field_column(geo_field)
|
||||
|
||||
# Adding the AsKML function call to SELECT part of the SQL.
|
||||
return self.extra(select={'kml':'%s(%s,%s)' % (ASKML, field_col, precision)})
|
||||
return self.extra(select={'kml':'%s(%s,%s)' % (ASKML, geo_col, precision)})
|
||||
|
||||
def transform(self, field_name=None, srid=4326):
|
||||
"""
|
||||
Transforms the given geometry field to the given SRID. If no SRID is
|
||||
provided, the transformation will default to using 4326 (WGS84).
|
||||
"""
|
||||
# Getting the geographic field.
|
||||
if not field_name:
|
||||
field_name = self._get_geofield()
|
||||
elif isinstance(field_name, int):
|
||||
srid = field_name
|
||||
field_name = self._get_geofield()
|
||||
TRANSFORM = SpatialBackend.transform
|
||||
if not TRANSFORM:
|
||||
raise ImproperlyConfigured('Transform stored procedure not available.')
|
||||
|
||||
field = self.model._meta.get_field(field_name)
|
||||
if not isinstance(field, GeometryField):
|
||||
# `field_name` is first for backwards compatibility; but we want to
|
||||
# be able to take integer srid as first parameter.
|
||||
if isinstance(field_name, (int, long)):
|
||||
srid = field_name
|
||||
field_name = None
|
||||
|
||||
# Getting the geographic field.
|
||||
geo_field = self._geo_field(field_name)
|
||||
if not geo_field:
|
||||
raise TypeError('%s() only available for GeometryFields' % TRANSFORM)
|
||||
|
||||
# Why cascading substitutions? Because spatial backends like
|
||||
# Oracle and MySQL already require a function call to convert to text, thus
|
||||
# when there's also a transformation we need to cascade the substitutions.
|
||||
# For example, 'SDO_UTIL.TO_WKTGEOMETRY(SDO_CS.TRANSFORM( ... )'
|
||||
col = self._custom_select.get(field.column, self._field_column(field))
|
||||
geo_col = self._custom_select.get(geo_field.column, self._field_column(geo_field))
|
||||
|
||||
# Setting the key for the field's column with the custom SELECT SQL to
|
||||
# override the geometry column returned from the database.
|
||||
TRANSFORM = SpatialBackend.transform
|
||||
if oracle:
|
||||
custom_sel = '%s(%s, %s)' % (TRANSFORM, col, srid)
|
||||
custom_sel = '%s(%s, %s)' % (TRANSFORM, geo_col, srid)
|
||||
self._ewkt = srid
|
||||
else:
|
||||
custom_sel = '(%s(%s, %s)) AS %s' % \
|
||||
(TRANSFORM, col, srid, connection.ops.quote_name(field.column))
|
||||
self._custom_select[field.column] = custom_sel
|
||||
(TRANSFORM, geo_col, srid, connection.ops.quote_name(geo_field.column))
|
||||
self._custom_select[geo_field.column] = custom_sel
|
||||
return self._clone()
|
||||
|
||||
def union(self, field_name=None, tolerance=0.0005):
|
||||
@ -441,12 +437,10 @@ class GeoQuerySet(QuerySet):
|
||||
raise ImproperlyConfigured('Union stored procedure not available.')
|
||||
|
||||
# Getting the geographic field column
|
||||
if not field_name:
|
||||
field_name = self._get_geofield()
|
||||
|
||||
field_col = self._geo_column(field_name)
|
||||
if not field_col:
|
||||
geo_field = self._geo_field(field_name)
|
||||
if not geo_field:
|
||||
raise TypeError('Aggregate Union only available on GeometryFields.')
|
||||
geo_col = self._field_column(geo_field)
|
||||
|
||||
# Getting the SQL for the query.
|
||||
try:
|
||||
@ -458,10 +452,10 @@ class GeoQuerySet(QuerySet):
|
||||
# on the geographic field column.
|
||||
if oracle:
|
||||
union_sql = 'SELECT %s' % self._geo_fmt
|
||||
union_sql = union_sql % ('%s(SDOAGGRTYPE(%s,%s))' % (UNION, field_col, tolerance))
|
||||
union_sql = union_sql % ('%s(SDOAGGRTYPE(%s,%s))' % (UNION, geo_col, tolerance))
|
||||
union_sql += sql
|
||||
else:
|
||||
union_sql = ('SELECT %s(%s)' % (UNION, field_col)) + sql
|
||||
union_sql = ('SELECT %s(%s)' % (UNION, geo_col)) + sql
|
||||
|
||||
# Getting a cursor, executing the query.
|
||||
cursor = connection.cursor()
|
||||
|
@ -21,7 +21,7 @@ class SpatialRefSysMixin(object):
|
||||
# TODO: Flattening not used in all ellipsoids, could also be a minor axis, or 'b'
|
||||
# parameter.
|
||||
spheroid_regex = re.compile(r'.+SPHEROID\[\"(?P<name>.+)\",(?P<major>\d+(\.\d+)?),(?P<flattening>\d{3}\.\d+),')
|
||||
|
||||
|
||||
# For pulling out the units on platforms w/o GDAL installed.
|
||||
# TODO: Figure out how to pull out angular units of projected coordinate system and
|
||||
# fix for LOCAL_CS types. GDAL should be highly recommended for performing
|
||||
@ -71,7 +71,7 @@ class SpatialRefSysMixin(object):
|
||||
|
||||
@property
|
||||
def spheroid(self):
|
||||
"Returns the spheroid for this spatial reference."
|
||||
"Returns the spheroid name for this spatial reference."
|
||||
return self.srs['spheroid']
|
||||
|
||||
@property
|
||||
@ -170,6 +170,34 @@ class SpatialRefSysMixin(object):
|
||||
m = cls.units_regex.match(wkt)
|
||||
return m.group('unit'), m.group('unit_name')
|
||||
|
||||
@classmethod
|
||||
def get_spheroid(cls, wkt, string=True):
|
||||
"""
|
||||
Class method used by GeometryField on initialization to
|
||||
retrieve the `SPHEROID[..]` parameters from the given WKT.
|
||||
"""
|
||||
if HAS_GDAL:
|
||||
srs = SpatialReference(wkt)
|
||||
sphere_params = srs.ellipsoid
|
||||
sphere_name = srs['spheroid']
|
||||
else:
|
||||
m = cls.spheroid_regex.match(wkt)
|
||||
if m:
|
||||
sphere_params = (float(m.group('major')), float(m.group('flattening')))
|
||||
sphere_name = m.group('name')
|
||||
else:
|
||||
return None
|
||||
|
||||
if not string:
|
||||
return sphere_name, sphere_params
|
||||
else:
|
||||
# `string` parameter used to place in format acceptable by PostGIS
|
||||
if len(sphere_params) == 3:
|
||||
radius, flattening = sphere_params[0], sphere_params[2]
|
||||
else:
|
||||
radius, flattening = sphere_params
|
||||
return 'SPHEROID["%s",%s,%s]' % (sphere_name, radius, flattening)
|
||||
|
||||
def __unicode__(self):
|
||||
"""
|
||||
Returns the string representation. If GDAL is installed,
|
||||
|
Binary file not shown.
@ -1 +0,0 @@
|
||||
PROJCS["NAD83 / Texas South Central",GEOGCS["GCS_North_American_1983",DATUM["D_North_American_1983",SPHEROID["GRS_1980",6378137,298.257222101]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]],PROJECTION["Lambert_Conformal_Conic"],PARAMETER["standard_parallel_1",30.28333333333334],PARAMETER["standard_parallel_2",28.38333333333333],PARAMETER["latitude_of_origin",27.83333333333333],PARAMETER["central_meridian",-99],PARAMETER["false_easting",600000],PARAMETER["false_northing",4000000],UNIT["Meter",1]]
|
Binary file not shown.
Binary file not shown.
25
django/contrib/gis/tests/distapp/data.py
Normal file
25
django/contrib/gis/tests/distapp/data.py
Normal file
@ -0,0 +1,25 @@
|
||||
au_cities = (('Wollongong', 150.902, -34.4245),
|
||||
('Shellharbour', 150.87, -34.5789),
|
||||
('Thirroul', 150.924, -34.3147),
|
||||
('Mittagong', 150.449, -34.4509),
|
||||
('Batemans Bay', 150.175, -35.7082),
|
||||
('Canberra', 144.963, -37.8143),
|
||||
('Melbourne', 145.963, -37.8143),
|
||||
('Sydney', 151.26071, -33.887034),
|
||||
('Hobart', 147.33, -42.8827),
|
||||
('Adelaide', 138.6, -34.9258),
|
||||
)
|
||||
|
||||
stx_cities = (('Downtown Houston', 951640.547328, 4219369.26172),
|
||||
('West University Place', 943547.922328, 4213623.65345),
|
||||
('Southside Place', 944704.643307, 4212768.87617),
|
||||
('Bellaire', 942595.669129, 4212686.72583),
|
||||
('Pearland', 959674.616506, 4197465.6526),
|
||||
('Galveston', 1008151.16007, 4170027.47655),
|
||||
('Sealy', 874859.286808, 4219186.8641),
|
||||
('San Antonio', 649173.910483, 4176413.27786),
|
||||
('Round Rock', 726846.03695, 4297160.99715),
|
||||
('Saint Hedwig', 677644.649952, 4175467.06744),
|
||||
)
|
||||
|
||||
|
@ -1,20 +1,20 @@
|
||||
from django.contrib.gis.db import models
|
||||
|
||||
class City(models.Model):
|
||||
class SouthTexasCity(models.Model):
|
||||
"City model on projected coordinate system for South Texas."
|
||||
name = models.CharField(max_length=30)
|
||||
point = models.PointField(srid=32140)
|
||||
objects = models.GeoManager()
|
||||
def __unicode__(self): return self.name
|
||||
|
||||
class AustraliaCity(models.Model):
|
||||
"City model for Australia, using WGS84."
|
||||
name = models.CharField(max_length=30)
|
||||
point = models.PointField()
|
||||
objects = models.GeoManager()
|
||||
def __unicode__(self): return self.name
|
||||
|
||||
#class County(models.Model):
|
||||
# name = models.CharField(max_length=30)
|
||||
# mpoly = models.MultiPolygonField(srid=32140)
|
||||
# objects = models.GeoManager()
|
||||
|
||||
city_mapping = {'name' : 'Name',
|
||||
'point' : 'POINT',
|
||||
}
|
||||
|
||||
#county_mapping = {'name' : 'Name',
|
||||
# 'mpoly' : 'MULTIPOLYGON',
|
||||
# }
|
||||
|
@ -1,53 +1,68 @@
|
||||
import os, unittest
|
||||
from decimal import Decimal
|
||||
from models import *
|
||||
from django.contrib.gis.utils import LayerMapping
|
||||
|
||||
from django.contrib.gis.gdal import DataSource
|
||||
from django.contrib.gis.geos import GEOSGeometry
|
||||
from django.contrib.gis.geos import GEOSGeometry, Point
|
||||
from django.contrib.gis.measure import D # alias for Distance
|
||||
from django.contrib.gis.db.models import GeoQ
|
||||
from django.contrib.gis.tests.utils import oracle
|
||||
|
||||
shp_path = os.path.dirname(__file__)
|
||||
city_shp = os.path.join(shp_path, 'cities/cities.shp')
|
||||
#county_shp = os.path.join(shp_path, 'counties/counties.shp')
|
||||
from models import SouthTexasCity, AustraliaCity
|
||||
from data import au_cities, stx_cities
|
||||
|
||||
class DistanceTest(unittest.TestCase):
|
||||
|
||||
# A point we are testing distances with -- using a WGS84
|
||||
# coordinate that'll be implicitly transormed to that to
|
||||
# the coordinate system of the field, EPSG:32140 (Texas South Central
|
||||
# w/units in meters)
|
||||
stx_pnt = GEOSGeometry('POINT (-95.370401017314293 29.704867409475465)', 4326)
|
||||
|
||||
def get_cities(self, qs):
|
||||
cities = [c.name for c in qs]
|
||||
cities.sort()
|
||||
return cities
|
||||
|
||||
def test01_init(self):
|
||||
"LayerMapping initialization of distance models."
|
||||
"Initialization of distance models."
|
||||
|
||||
city_lm = LayerMapping(City, city_shp, city_mapping, transform=False)
|
||||
city_lm.save()
|
||||
|
||||
# TODO: complete tests with distance from multipolygons.
|
||||
#county_lm = LayerMapping(County, county_shp, county_mapping, transform=False)
|
||||
#county_lm.save()
|
||||
def load_cities(city_model, srid, data_tup):
|
||||
for name, x, y in data_tup:
|
||||
c = city_model(name=name, point=Point(x, y, srid=srid))
|
||||
c.save()
|
||||
|
||||
self.assertEqual(12, City.objects.count())
|
||||
#self.assertEqual(60, County.objects.count())
|
||||
load_cities(SouthTexasCity, 32140, stx_cities)
|
||||
load_cities(AustraliaCity, 4326, au_cities)
|
||||
|
||||
# TODO: Complete tests for `dwithin` lookups.
|
||||
#def test02_dwithin(self):
|
||||
# "Testing the `dwithin` lookup type."
|
||||
# pass
|
||||
self.assertEqual(10, SouthTexasCity.objects.count())
|
||||
self.assertEqual(10, AustraliaCity.objects.count())
|
||||
|
||||
def test02_dwithin(self):
|
||||
"Testing the `dwithin` lookup type."
|
||||
pnt = self.stx_pnt
|
||||
dists = [7000, D(km=7), D(mi=4.349)]
|
||||
for dist in dists:
|
||||
qs = SouthTexasCity.objects.filter(point__dwithin=(self.stx_pnt, dist))
|
||||
cities = self.get_cities(qs)
|
||||
self.assertEqual(cities, ['Downtown Houston', 'Southside Place'])
|
||||
|
||||
def test03_distance_aggregate(self):
|
||||
"Testing the `distance` GeoQuerySet method."
|
||||
# The point for La Grange, TX
|
||||
lagrange = GEOSGeometry('POINT(-96.876369 29.905320)', 4326)
|
||||
# Got these from using the raw SQL statement:
|
||||
# SELECT ST_Distance(point, ST_Transform(ST_GeomFromText('POINT(-96.876369 29.905320)', 4326),32140)) FROM distapp_city;
|
||||
distances = [147075.069813436, 139630.198056286, 140888.552826286,
|
||||
138809.684197415, 158309.246259353, 212183.594374882,
|
||||
70870.1889675217, 319225.965633536, 165337.758878256,
|
||||
92630.7446925393, 102128.654360872, 139196.085105372]
|
||||
dist1 = City.objects.distance('point', lagrange)
|
||||
dist2 = City.objects.distance(lagrange)
|
||||
# SELECT ST_Distance(point, ST_Transform(ST_GeomFromText('POINT(-96.876369 29.905320)', 4326),32140)) FROM distapp_southtexascity;
|
||||
distances = [147075.069813, 139630.198056, 140888.552826,
|
||||
138809.684197, 158309.246259, 212183.594374,
|
||||
70870.188967, 165337.758878, 102128.654360,
|
||||
139196.085105]
|
||||
dist1 = SouthTexasCity.objects.distance('point', lagrange)
|
||||
dist2 = SouthTexasCity.objects.distance(lagrange)
|
||||
|
||||
# Original query done on PostGIS, have to adjust AlmostEqual tolerance
|
||||
# for Oracle.
|
||||
if oracle: tol = 3
|
||||
else: tol = 7
|
||||
else: tol = 5
|
||||
|
||||
for qs in [dist1, dist2]:
|
||||
for i, c in enumerate(qs):
|
||||
@ -55,30 +70,50 @@ class DistanceTest(unittest.TestCase):
|
||||
|
||||
def test04_distance_lookups(self):
|
||||
"Testing the `distance_lt`, `distance_gt`, `distance_lte`, and `distance_gte` lookup types."
|
||||
# The point we are testing distances with -- using a WGS84
|
||||
# coordinate that'll be implicitly transormed to that to
|
||||
# the coordinate system of the field, EPSG:32140 (Texas South Central
|
||||
# w/units in meters)
|
||||
pnt = GEOSGeometry('POINT (-95.370401017314293 29.704867409475465)', 4326)
|
||||
|
||||
# Only two cities (Houston and Southside Place) should be
|
||||
# within 7km of the given point.
|
||||
qs1 = City.objects.filter(point__distance_lte=(pnt, D(km=7))) # Query w/Distance instance.
|
||||
qs2 = City.objects.filter(point__distance_lte=(pnt, 7000)) # Query w/int (units are assumed to be that of the field)
|
||||
qs3 = City.objects.filter(point__distance_lte=(pnt, 7000.0)) # Query w/float
|
||||
qs4 = City.objects.filter(point__distance_lte=(pnt, Decimal(7000))) # Query w/Decimal
|
||||
dists = [D(km=7), D(mi=4.349), # Distance instances in different units.
|
||||
7000, 7000.0, Decimal(7000), # int, float, Decimal parameters.
|
||||
]
|
||||
|
||||
for qs in [qs1, qs2, qs3, qs4]:
|
||||
for dist in dists:
|
||||
qs = SouthTexasCity.objects.filter(point__dwithin=(self.stx_pnt, dist))
|
||||
for c in qs:
|
||||
self.assertEqual(2, qs.count())
|
||||
self.failIf(not c.name in ['Downtown Houston', 'Southside Place'])
|
||||
cities = self.get_cities(qs)
|
||||
self.assertEqual(cities, ['Downtown Houston', 'Southside Place'])
|
||||
|
||||
# Now only retrieving the cities within a 20km 'donut' w/a 7km radius 'hole'
|
||||
# (thus, Houston and Southside place will be excluded)
|
||||
qs = City.objects.filter(point__distance_gte=(pnt, D(km=7))).filter(point__distance_lte=(pnt, D(km=20)))
|
||||
self.assertEqual(3, qs.count())
|
||||
for c in qs:
|
||||
self.failIf(not c.name in ['Pearland', 'Bellaire', 'West University Place'])
|
||||
qs = SouthTexasCity.objects.filter(point__distance_gte=(self.stx_pnt, D(km=7))).filter(point__distance_lte=(self.stx_pnt, D(km=20)))
|
||||
cities = self.get_cities(qs)
|
||||
self.assertEqual(cities, ['Bellaire', 'Pearland', 'West University Place'])
|
||||
|
||||
def test05_geodetic_distance(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.
|
||||
mp = GEOSGeometry('MULTIPOINT(0 0, 5 23)')
|
||||
self.assertRaises(TypeError,
|
||||
AustraliaCity.objects.filter(point__distance_lte=(mp, D(km=100))))
|
||||
|
||||
hobart = AustraliaCity.objects.get(name='Hobart')
|
||||
|
||||
# Getting all cities w/in 550 miles of Hobart.
|
||||
qs = AustraliaCity.objects.exclude(name='Hobart').filter(point__distance_lte=(hobart.point, D(mi=550)))
|
||||
cities = self.get_cities(qs)
|
||||
self.assertEqual(cities, ['Batemans Bay', 'Canberra', 'Melbourne'])
|
||||
|
||||
# Cities that are either really close or really far from Wollongong --
|
||||
# and using different units of distance.
|
||||
wollongong = AustraliaCity.objects.get(name='Wollongong')
|
||||
gq1 = GeoQ(point__distance_lte=(wollongong.point, D(yd=19500))) # Yards (~17km)
|
||||
gq2 = GeoQ(point__distance_gte=(wollongong.point, D(nm=400))) # Nautical Miles
|
||||
qs = AustraliaCity.objects.exclude(name='Wollongong').filter(gq1 | gq2)
|
||||
cities = self.get_cities(qs)
|
||||
self.assertEqual(cities, ['Adelaide', 'Hobart', 'Shellharbour', 'Thirroul'])
|
||||
|
||||
|
||||
def suite():
|
||||
s = unittest.TestSuite()
|
||||
|
Loading…
x
Reference in New Issue
Block a user