mirror of
https://github.com/django/django.git
synced 2025-07-04 17:59:13 +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
@ -10,65 +10,100 @@
|
|||||||
(3) The `parse_lookup` function, used for spatial SQL construction by
|
(3) The `parse_lookup` function, used for spatial SQL construction by
|
||||||
the GeoQuerySet.
|
the GeoQuerySet.
|
||||||
(4) The `create_spatial_db`, and `get_geo_where_clause`
|
(4) The `create_spatial_db`, and `get_geo_where_clause`
|
||||||
routines (needed by `parse_lookup`).
|
(needed by `parse_lookup`) functions.
|
||||||
(5) The `SpatialBackend` object, which contains information specific
|
(5) The `SpatialBackend` object, which contains information specific
|
||||||
to the spatial backend.
|
to the spatial backend.
|
||||||
"""
|
"""
|
||||||
from types import StringType, UnicodeType
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
from django.db.models.query import field_choices, find_field, get_where_clause, \
|
from django.db.models.query import field_choices, find_field, get_where_clause, \
|
||||||
FieldFound, LOOKUP_SEPARATOR, QUERY_TERMS
|
FieldFound, LOOKUP_SEPARATOR, QUERY_TERMS
|
||||||
from django.utils.datastructures import SortedDict
|
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.
|
# 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':
|
if settings.DATABASE_ENGINE == 'postgresql_psycopg2':
|
||||||
# PostGIS is the spatial database, getting the rquired modules,
|
from django.contrib.gis.db.backend.postgis.adaptor import \
|
||||||
# renaming as necessary.
|
PostGISAdaptor as GeoAdaptor
|
||||||
from django.contrib.gis.db.backend.postgis import \
|
from django.contrib.gis.db.backend.postgis.field import \
|
||||||
PostGISField as GeoBackendField, POSTGIS_TERMS as GIS_TERMS, \
|
PostGISField as GeoBackendField
|
||||||
create_spatial_db, get_geo_where_clause, \
|
from django.contrib.gis.db.backend.postgis.creation import create_spatial_db
|
||||||
ASGML, ASKML, DISTANCE, EXTENT, GEOM_SELECT, TRANSFORM, UNION, \
|
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
|
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)
|
VERSION = (MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2)
|
||||||
SPATIAL_BACKEND = 'postgis'
|
SPATIAL_BACKEND = 'postgis'
|
||||||
elif settings.DATABASE_ENGINE == 'oracle':
|
elif settings.DATABASE_ENGINE == 'oracle':
|
||||||
from django.contrib.gis.db.backend.oracle import \
|
from django.contrib.gis.db.backend.oracle.adaptor import \
|
||||||
OracleSpatialField as GeoBackendField, \
|
OracleSpatialAdaptor as GeoAdaptor
|
||||||
ORACLE_SPATIAL_TERMS as GIS_TERMS, \
|
from django.contrib.gis.db.backend.oracle.field import \
|
||||||
create_spatial_db, get_geo_where_clause, \
|
OracleSpatialField as GeoBackendField
|
||||||
ASGML, DISTANCE, GEOM_SELECT, TRANSFORM, UNION
|
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'
|
SPATIAL_BACKEND = 'oracle'
|
||||||
|
LIMITED_WHERE = ['relate']
|
||||||
elif settings.DATABASE_ENGINE == 'mysql':
|
elif settings.DATABASE_ENGINE == 'mysql':
|
||||||
from django.contrib.gis.db.backend.mysql import \
|
from django.contrib.gis.db.backend.mysql.adaptor import \
|
||||||
MySQLGeoField as GeoBackendField, \
|
MySQLAdaptor as GeoAdaptor
|
||||||
MYSQL_GIS_TERMS as GIS_TERMS, \
|
from django.contrib.gis.db.backend.mysql.field import \
|
||||||
create_spatial_db, get_geo_where_clause, \
|
MySQLGeoField as GeoBackendField
|
||||||
GEOM_SELECT
|
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'
|
SPATIAL_BACKEND = 'mysql'
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError('No Geographic Backend exists for %s' % settings.DATABASE_ENGINE)
|
raise NotImplementedError('No Geographic Backend exists for %s' % settings.DATABASE_ENGINE)
|
||||||
|
|
||||||
class SpatialBackend(object):
|
class SpatialBackend(object):
|
||||||
"A container for properties of the SpatialBackend."
|
"A container for properties of the SpatialBackend."
|
||||||
|
# Stored procedure names used by the `GeoManager`.
|
||||||
as_kml = ASKML
|
as_kml = ASKML
|
||||||
as_gml = ASGML
|
as_gml = ASGML
|
||||||
distance = DISTANCE
|
distance = DISTANCE
|
||||||
|
distance_spheroid = DISTANCE_SPHEROID
|
||||||
extent = EXTENT
|
extent = EXTENT
|
||||||
name = SPATIAL_BACKEND
|
name = SPATIAL_BACKEND
|
||||||
select = GEOM_SELECT
|
select = GEOM_SELECT
|
||||||
transform = TRANSFORM
|
transform = TRANSFORM
|
||||||
union = UNION
|
union = UNION
|
||||||
|
|
||||||
|
# Version information, if defined.
|
||||||
version = VERSION
|
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 ####
|
#### query.py overloaded functions ####
|
||||||
# parse_lookup() and lookup_inner() are modified from their django/db/models/query.py
|
# parse_lookup() and lookup_inner() are modified from their django/db/models/query.py
|
||||||
# counterparts to support constructing SQL for geographic queries.
|
# counterparts to support constructing SQL for geographic queries.
|
||||||
#
|
#
|
||||||
# Status: Synced with r5982.
|
# Status: Synced with r7098.
|
||||||
#
|
#
|
||||||
def parse_lookup(kwarg_items, opts):
|
def parse_lookup(kwarg_items, opts):
|
||||||
# Helper function that handles converting API kwargs
|
# 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
|
# If the field is a geometry field, then the WHERE clause will need to be obtained
|
||||||
# with the get_geo_where_clause()
|
# with the get_geo_where_clause()
|
||||||
if hasattr(field, '_geom'):
|
if hasattr(field, '_geom'):
|
||||||
# Getting the preparation SQL object from the field.
|
# Getting additional SQL WHERE and params arrays associated with
|
||||||
geo_prep = field.get_db_prep_lookup(lookup_type, value)
|
# the geographic field.
|
||||||
|
geo_where, geo_params = field.get_db_prep_lookup(lookup_type, value)
|
||||||
|
|
||||||
# Getting the adapted geometry from the field.
|
# Getting the geographic WHERE clause.
|
||||||
gwc = get_geo_where_clause(lookup_type, current_table, column, value)
|
gwc = get_geo_where_clause(lookup_type, current_table, field, value)
|
||||||
|
|
||||||
# Substituting in the the where parameters into the geographic where
|
# Appending the geographic WHERE componnents and parameters onto
|
||||||
# clause, and extending the parameters.
|
# the where and params arrays.
|
||||||
where.append(gwc % tuple(geo_prep.where))
|
where.append(gwc % tuple(geo_where))
|
||||||
params.extend(geo_prep.params)
|
params.extend(geo_params)
|
||||||
else:
|
else:
|
||||||
where.append(get_where_clause(lookup_type, current_table + '.', column, value, db_type))
|
where.append(get_where_clause(lookup_type, current_table + '.', column, value, db_type))
|
||||||
params.extend(field.get_db_prep_lookup(lookup_type, value))
|
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 import connection
|
||||||
from django.db.models.fields import Field # Django base Field class
|
from django.db.models.fields import Field # Django base Field class
|
||||||
from django.contrib.gis.geos import GEOSGeometry
|
from django.contrib.gis.db.backend.mysql.query import GEOM_FROM_TEXT
|
||||||
from django.contrib.gis.db.backend.util import GeoFieldSQL
|
|
||||||
from django.contrib.gis.db.backend.mysql.query import MYSQL_GIS_TERMS, GEOM_FROM_TEXT
|
|
||||||
|
|
||||||
# Quotename & geographic quotename, respectively.
|
# Quotename & geographic quotename, respectively.
|
||||||
qn = connection.ops.quote_name
|
qn = connection.ops.quote_name
|
||||||
def gqn(value):
|
|
||||||
if isinstance(value, UnicodeType): value = value.encode('ascii')
|
|
||||||
return "'%s'" % value
|
|
||||||
|
|
||||||
class MySQLGeoField(Field):
|
class MySQLGeoField(Field):
|
||||||
"""
|
"""
|
||||||
@ -23,7 +16,7 @@ class MySQLGeoField(Field):
|
|||||||
used an R-Tree index is created, otherwise a B-Tree index is created.
|
used an R-Tree index is created, otherwise a B-Tree index is created.
|
||||||
Thus, for best spatial performance, you should use MyISAM tables
|
Thus, for best spatial performance, you should use MyISAM tables
|
||||||
(which do not support transactions). For more information, see Ch.
|
(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.
|
# Getting the index name.
|
||||||
@ -51,42 +44,10 @@ class MySQLGeoField(Field):
|
|||||||
"The OpenGIS name is returned for the MySQL database column type."
|
"The OpenGIS name is returned for the MySQL database column type."
|
||||||
return self._geom
|
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):
|
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
|
return '%s(%%s)' % GEOM_FROM_TEXT
|
||||||
|
@ -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 MySQL
|
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
|
from django.db import connection
|
||||||
qn = connection.ops.quote_name
|
qn = connection.ops.quote_name
|
||||||
@ -34,10 +39,10 @@ MYSQL_GIS_TERMS = MYSQL_GIS_FUNCTIONS.keys()
|
|||||||
MYSQL_GIS_TERMS += MISC_TERMS
|
MYSQL_GIS_TERMS += MISC_TERMS
|
||||||
MYSQL_GIS_TERMS = tuple(MYSQL_GIS_TERMS) # Making immutable
|
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."
|
"Returns the SQL WHERE clause for use in MySQL spatial SQL construction."
|
||||||
# Getting the quoted field as `geo_col`.
|
# 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
|
# See if a MySQL Geometry function matches the lookup type next
|
||||||
lookup_info = MYSQL_GIS_FUNCTIONS.get(lookup_type, False)
|
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.
|
This object provides the database adaptor for Oracle geometries.
|
||||||
"""
|
"""
|
||||||
from cx_Oracle import CLOB
|
|
||||||
|
|
||||||
class OracleSpatialAdaptor(object):
|
class OracleSpatialAdaptor(object):
|
||||||
def __init__(self, geom):
|
def __init__(self, geom):
|
||||||
"Initializes only on the geometry object."
|
"Initializes only on the geometry object."
|
||||||
@ -11,11 +9,3 @@ class OracleSpatialAdaptor(object):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
"WKT is used for the substitution value of the geometry."
|
"WKT is used for the substitution value of the geometry."
|
||||||
return self.wkt
|
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 import connection
|
||||||
from django.db.backends.util import truncate_name
|
from django.db.backends.util import truncate_name
|
||||||
from django.db.models.fields import Field # Django base Field class
|
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 gqn
|
||||||
from django.contrib.gis.db.backend.util import GeoFieldSQL
|
from django.contrib.gis.db.backend.oracle.query import TRANSFORM
|
||||||
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
|
|
||||||
|
|
||||||
# Quotename & geographic quotename, respectively.
|
# Quotename & geographic quotename, respectively.
|
||||||
qn = connection.ops.quote_name
|
qn = connection.ops.quote_name
|
||||||
def gqn(value):
|
|
||||||
if isinstance(value, UnicodeType): value = value.encode('ascii')
|
|
||||||
return "'%s'" % value
|
|
||||||
|
|
||||||
class OracleSpatialField(Field):
|
class OracleSpatialField(Field):
|
||||||
"""
|
"""
|
||||||
@ -95,64 +88,16 @@ class OracleSpatialField(Field):
|
|||||||
"The Oracle geometric data type is MDSYS.SDO_GEOMETRY."
|
"The Oracle geometric data type is MDSYS.SDO_GEOMETRY."
|
||||||
return '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):
|
def get_placeholder(self, value):
|
||||||
"""
|
"""
|
||||||
Provides a proper substitution value for Geometries that are not in the
|
Provides a proper substitution value for Geometries that are not in the
|
||||||
SRID of the field. Specifically, this routine will substitute in the
|
SRID of the field. Specifically, this routine will substitute in the
|
||||||
SDO_CS.TRANSFORM() function call.
|
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.
|
# Adding Transform() to the SQL placeholder.
|
||||||
return '%s(SDO_GEOMETRY(%%s, %s), %s)' % (TRANSFORM, value.srid, self._srid)
|
return '%s(SDO_GEOMETRY(%%s, %s), %s)' % (TRANSFORM, value.srid, self._srid)
|
||||||
elif value is None:
|
|
||||||
return '%s'
|
|
||||||
else:
|
else:
|
||||||
return 'SDO_GEOMETRY(%%s, %s)' % self._srid
|
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.
|
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
|
import re
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
@ -25,8 +30,11 @@ GEOM_SELECT = 'SDO_UTIL.TO_WKTGEOMETRY(%s)'
|
|||||||
#### Classes used in constructing Oracle spatial SQL ####
|
#### Classes used in constructing Oracle spatial SQL ####
|
||||||
class SDOOperation(SpatialFunction):
|
class SDOOperation(SpatialFunction):
|
||||||
"Base class for SDO* Oracle operations."
|
"Base class for SDO* Oracle operations."
|
||||||
def __init__(self, func, end_subst=") %s '%s'"):
|
def __init__(self, func, **kwargs):
|
||||||
super(SDOOperation, self).__init__(func, end_subst=end_subst, operator='=', result='TRUE')
|
kwargs.setdefault('operator', '=')
|
||||||
|
kwargs.setdefault('result', 'TRUE')
|
||||||
|
kwargs.setdefault('end_subst', ") %s '%s'")
|
||||||
|
super(SDOOperation, self).__init__(func, **kwargs)
|
||||||
|
|
||||||
class SDODistance(SpatialFunction):
|
class SDODistance(SpatialFunction):
|
||||||
"Class for Distance queries."
|
"Class for Distance queries."
|
||||||
@ -55,12 +63,14 @@ class SDORelate(SpatialFunction):
|
|||||||
#### Lookup type mapping dictionaries of Oracle spatial operations ####
|
#### Lookup type mapping dictionaries of Oracle spatial operations ####
|
||||||
|
|
||||||
# Valid distance types and substitutions
|
# Valid distance types and substitutions
|
||||||
dtypes = (Decimal, Distance, float, int)
|
dtypes = (Decimal, Distance, float, int, long)
|
||||||
DISTANCE_FUNCTIONS = {
|
DISTANCE_FUNCTIONS = {
|
||||||
'distance_gt' : (SDODistance('>'), dtypes),
|
'distance_gt' : (SDODistance('>'), dtypes),
|
||||||
'distance_gte' : (SDODistance('>='), dtypes),
|
'distance_gte' : (SDODistance('>='), dtypes),
|
||||||
'distance_lt' : (SDODistance('<'), dtypes),
|
'distance_lt' : (SDODistance('<'), dtypes),
|
||||||
'distance_lte' : (SDODistance('<='), dtypes),
|
'distance_lte' : (SDODistance('<='), dtypes),
|
||||||
|
'dwithin' : (SDOOperation('SDO_WITHIN_DISTANCE',
|
||||||
|
beg_subst="%s(%s, %%s, 'distance=%%s'"), dtypes),
|
||||||
}
|
}
|
||||||
|
|
||||||
ORACLE_GEOMETRY_FUNCTIONS = {
|
ORACLE_GEOMETRY_FUNCTIONS = {
|
||||||
@ -68,7 +78,6 @@ ORACLE_GEOMETRY_FUNCTIONS = {
|
|||||||
'coveredby' : SDOOperation('SDO_COVEREDBY'),
|
'coveredby' : SDOOperation('SDO_COVEREDBY'),
|
||||||
'covers' : SDOOperation('SDO_COVERS'),
|
'covers' : SDOOperation('SDO_COVERS'),
|
||||||
'disjoint' : SDOGeomRelate('DISJOINT'),
|
'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()?
|
'intersects' : SDOOperation('SDO_OVERLAPBDYINTERSECT'), # TODO: Is this really the same as ST_Intersects()?
|
||||||
'equals' : SDOOperation('SDO_EQUAL'),
|
'equals' : SDOOperation('SDO_EQUAL'),
|
||||||
'exact' : 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
|
ORACLE_SPATIAL_TERMS = tuple(ORACLE_SPATIAL_TERMS) # Making immutable
|
||||||
|
|
||||||
#### The `get_geo_where_clause` function for Oracle ####
|
#### 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."
|
"Returns the SQL WHERE clause for use in Oracle spatial SQL construction."
|
||||||
# Getting the quoted table name as `geo_col`.
|
# 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
|
# See if a Oracle Geometry function matches the lookup type next
|
||||||
lookup_info = ORACLE_GEOMETRY_FUNCTIONS.get(lookup_type, False)
|
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.
|
# Otherwise, just call the `as_sql` method on the SDOOperation instance.
|
||||||
return sdo_op.as_sql(geo_col)
|
return sdo_op.as_sql(geo_col)
|
||||||
else:
|
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:
|
# the SQL necessary for the geometry function call. For example:
|
||||||
# SDO_CONTAINS("geoapp_country"."poly", SDO_GEOMTRY('POINT(5 23)', 4326)) = 'TRUE'
|
# SDO_CONTAINS("geoapp_country"."poly", SDO_GEOMTRY('POINT(5 23)', 4326)) = 'TRUE'
|
||||||
return lookup_info.as_sql(geo_col)
|
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 import connection
|
||||||
from django.db.models.fields import Field # Django base Field class
|
from django.db.models.fields import Field # Django base Field class
|
||||||
from django.contrib.gis.geos import GEOSGeometry
|
from django.contrib.gis.geos import GEOSGeometry
|
||||||
from django.contrib.gis.db.backend.util import GeoFieldSQL
|
from django.contrib.gis.db.backend.util import gqn
|
||||||
from django.contrib.gis.db.backend.postgis.adaptor import PostGISAdaptor
|
from django.contrib.gis.db.backend.postgis.query import TRANSFORM
|
||||||
from django.contrib.gis.db.backend.postgis.query import \
|
|
||||||
DISTANCE_FUNCTIONS, POSTGIS_TERMS, TRANSFORM
|
|
||||||
|
|
||||||
# Quotename & geographic quotename, respectively
|
# Quotename & geographic quotename, respectively
|
||||||
qn = connection.ops.quote_name
|
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):
|
class PostGISField(Field):
|
||||||
"""
|
"""
|
||||||
@ -92,59 +83,14 @@ class PostGISField(Field):
|
|||||||
"""
|
"""
|
||||||
return None
|
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):
|
def get_placeholder(self, value):
|
||||||
"""
|
"""
|
||||||
Provides a proper substitution value for Geometries that are not in the
|
Provides a proper substitution value for Geometries that are not in the
|
||||||
SRID of the field. Specifically, this routine will substitute in the
|
SRID of the field. Specifically, this routine will substitute in the
|
||||||
ST_Transform() function call.
|
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.
|
# Adding Transform() to the SQL placeholder.
|
||||||
return '%s(%%s, %s)' % (TRANSFORM, self._srid)
|
return '%s(%%s, %s)' % (TRANSFORM, self._srid)
|
||||||
else:
|
|
||||||
return '%s'
|
|
||||||
|
@ -40,6 +40,7 @@ if MAJOR_VERSION >= 1:
|
|||||||
ASKML = get_func('AsKML')
|
ASKML = get_func('AsKML')
|
||||||
ASGML = get_func('AsGML')
|
ASGML = get_func('AsGML')
|
||||||
DISTANCE = get_func('Distance')
|
DISTANCE = get_func('Distance')
|
||||||
|
DISTANCE_SPHEROID = get_func('distance_spheroid')
|
||||||
EXTENT = get_func('extent')
|
EXTENT = get_func('extent')
|
||||||
GEOM_FROM_TEXT = get_func('GeomFromText')
|
GEOM_FROM_TEXT = get_func('GeomFromText')
|
||||||
GEOM_FROM_WKB = get_func('GeomFromWKB')
|
GEOM_FROM_WKB = get_func('GeomFromWKB')
|
||||||
@ -74,8 +75,20 @@ class PostGISFunctionParam(PostGISFunction):
|
|||||||
|
|
||||||
class PostGISDistance(PostGISFunction):
|
class PostGISDistance(PostGISFunction):
|
||||||
"For PostGIS distance operations."
|
"For PostGIS distance operations."
|
||||||
|
dist_func = 'Distance'
|
||||||
def __init__(self, operator):
|
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):
|
class PostGISRelate(PostGISFunctionParam):
|
||||||
"For PostGIS Relate(<geom>, <pattern>) calls."
|
"For PostGIS Relate(<geom>, <pattern>) calls."
|
||||||
@ -148,21 +161,24 @@ POSTGIS_GEOMETRY_FUNCTIONS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Valid distance types and substitutions
|
# 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_FUNCTIONS = {
|
||||||
'distance_gt' : (PostGISDistance('>'), dtypes),
|
'distance_gt' : (get_dist_ops('>'), dtypes),
|
||||||
'distance_gte' : (PostGISDistance('>='), dtypes),
|
'distance_gte' : (get_dist_ops('>='), dtypes),
|
||||||
'distance_lt' : (PostGISDistance('<'), dtypes),
|
'distance_lt' : (get_dist_ops('<'), dtypes),
|
||||||
'distance_lte' : (PostGISDistance('<='), dtypes),
|
'distance_lte' : (get_dist_ops('<='), dtypes),
|
||||||
}
|
}
|
||||||
|
|
||||||
if GEOM_FUNC_PREFIX == 'ST_':
|
if GEOM_FUNC_PREFIX == 'ST_':
|
||||||
# The ST_DWithin, ST_CoveredBy, and ST_Covers routines become available in 1.2.2+
|
# The ST_DWithin, ST_CoveredBy, and ST_Covers routines become available in 1.2.2+
|
||||||
POSTGIS_GEOMETRY_FUNCTIONS.update(
|
POSTGIS_GEOMETRY_FUNCTIONS.update(
|
||||||
{'dwithin' : (PostGISFunctionParam('DWithin'), dtypes),
|
{'coveredby' : PostGISFunction('CoveredBy'),
|
||||||
'coveredby' : PostGISFunction('CoveredBy'),
|
|
||||||
'covers' : PostGISFunction('Covers'),
|
'covers' : PostGISFunction('Covers'),
|
||||||
})
|
})
|
||||||
|
DISTANCE_FUNCTIONS['dwithin'] = (PostGISFunctionParam('DWithin'), dtypes)
|
||||||
|
|
||||||
# Distance functions are a part of PostGIS geometry functions.
|
# Distance functions are a part of PostGIS geometry functions.
|
||||||
POSTGIS_GEOMETRY_FUNCTIONS.update(DISTANCE_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
|
POSTGIS_TERMS = tuple(POSTGIS_TERMS) # Making immutable
|
||||||
|
|
||||||
#### The `get_geo_where_clause` function for PostGIS. ####
|
#### 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."
|
"Returns the SQL WHERE clause for use in PostGIS SQL construction."
|
||||||
# Getting the quoted field as `geo_col`.
|
# 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:
|
if lookup_type in POSTGIS_OPERATORS:
|
||||||
# See if a PostGIS operator matches the lookup type.
|
# See if a PostGIS operator matches the lookup type.
|
||||||
return POSTGIS_OPERATORS[lookup_type].as_sql(geo_col)
|
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
|
op, arg_type = tmp
|
||||||
|
|
||||||
# Ensuring that a tuple _value_ was passed in from the user
|
# 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)
|
raise TypeError('Tuple required for `%s` lookup type.' % lookup_type)
|
||||||
if len(value) != 2:
|
if len(value) != 2:
|
||||||
raise ValueError('2-element tuple required or `%s` lookup type.' % lookup_type)
|
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
|
# For lookup type `relate`, the op instance is not yet created (has
|
||||||
# to be instantiated here to check the pattern parameter).
|
# 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:
|
else:
|
||||||
op = tmp
|
op = tmp
|
||||||
# Calling the `as_sql` function on the operation instance.
|
# Calling the `as_sql` function on the operation instance.
|
||||||
|
@ -1,23 +1,16 @@
|
|||||||
class GeoFieldSQL(object):
|
from types import UnicodeType
|
||||||
"""
|
|
||||||
Container for passing values to `parse_lookup` from the various
|
|
||||||
backend geometry fields.
|
|
||||||
"""
|
|
||||||
def __init__(self, where=[], params=[]):
|
|
||||||
self.where = where
|
|
||||||
self.params = params
|
|
||||||
|
|
||||||
def __str__(self):
|
def gqn(val):
|
||||||
return self.as_sql()
|
"""
|
||||||
|
The geographic quote name function; used for quoting tables and
|
||||||
def as_sql(self, quote=False):
|
geometries (they use single rather than the double quotes of the
|
||||||
if not quote:
|
backend quotename function).
|
||||||
return self.where[0] % tuple(self.params)
|
"""
|
||||||
|
if isinstance(val, basestring):
|
||||||
|
if isinstance(val, UnicodeType): val = val.encode('ascii')
|
||||||
|
return "'%s'" % val
|
||||||
else:
|
else:
|
||||||
# Used for quoting WKT on certain backends.
|
return str(val)
|
||||||
tmp_params = ["'%s'" % self.params[0]]
|
|
||||||
tmp_params.extend(self.params[1:])
|
|
||||||
return self.where[0] % tuple(tmp_params)
|
|
||||||
|
|
||||||
class SpatialOperation(object):
|
class SpatialOperation(object):
|
||||||
"""
|
"""
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import connection
|
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.db.models.proxy import GeometryProxy
|
||||||
from django.contrib.gis.geos import GEOSException, GEOSGeometry
|
from django.contrib.gis.geos import GEOSException, GEOSGeometry
|
||||||
from django.contrib.gis.measure import Distance
|
from django.contrib.gis.measure import Distance
|
||||||
@ -14,7 +16,7 @@ except NotImplementedError:
|
|||||||
SpatialRefSys = None
|
SpatialRefSys = None
|
||||||
|
|
||||||
#TODO: Flesh out widgets; consider adding support for OGR Geometry proxies.
|
#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 base GIS field -- maps to the OpenGIS Specification Geometry type."
|
||||||
|
|
||||||
# The OpenGIS Geometry name.
|
# The OpenGIS Geometry name.
|
||||||
@ -38,21 +40,19 @@ class GeometryField(GeoBackendField):
|
|||||||
The number of dimensions for this geometry. Defaults to 2.
|
The number of dimensions for this geometry. Defaults to 2.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Backward-compatibility notice, this will disappear in future revisions.
|
# Setting the index flag with the value of the `spatial_index` keyword.
|
||||||
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
|
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
|
self._srid = srid
|
||||||
if SpatialRefSys:
|
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
|
# Why? `syncdb` fails to recognize installed geographic models when there's
|
||||||
# an ORM query instantiated within a model field. No matter, this works fine
|
# an ORM query instantiated within a model field.
|
||||||
# too.
|
|
||||||
cur = connection.cursor()
|
cur = connection.cursor()
|
||||||
qn = connection.ops.quote_name
|
qn = connection.ops.quote_name
|
||||||
stmt = 'SELECT %(table)s.%(wkt_col)s FROM %(table)s WHERE (%(table)s.%(srid_col)s = %(srid)s)'
|
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,
|
'srid' : srid,
|
||||||
}
|
}
|
||||||
cur.execute(stmt)
|
cur.execute(stmt)
|
||||||
row = cur.fetchone()
|
srs_wkt = cur.fetchone()[0]
|
||||||
self._unit, self._unit_name = SpatialRefSys.get_units(row[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.
|
# Setting the dimension of the geometry field.
|
||||||
self._dim = dim
|
self._dim = dim
|
||||||
|
|
||||||
super(GeometryField, self).__init__(**kwargs) # Calling the parent initializtion function
|
super(GeometryField, self).__init__(**kwargs) # Calling the parent initializtion function
|
||||||
|
|
||||||
### Routines specific to GeometryField ###
|
### Routines specific to GeometryField ###
|
||||||
def get_distance(self, dist):
|
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):
|
if isinstance(dist, Distance):
|
||||||
return getattr(dist, Distance.unit_attname(self._unit_name))
|
if self._unit_name in ('Decimal Degree', 'degree'):
|
||||||
elif isinstance(dist, (int, float, Decimal)):
|
# 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.
|
# 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):
|
def get_geometry(self, value):
|
||||||
"""
|
"""
|
||||||
Retrieves the geometry, setting the default SRID from the given
|
Retrieves the geometry, setting the default SRID from the given
|
||||||
lookup parameters.
|
lookup parameters.
|
||||||
"""
|
"""
|
||||||
if isinstance(value, tuple):
|
if isinstance(value, (tuple, list)):
|
||||||
geom = value[0]
|
geom = value[0]
|
||||||
else:
|
else:
|
||||||
geom = value
|
geom = value
|
||||||
@ -121,6 +142,49 @@ class GeometryField(GeoBackendField):
|
|||||||
# Setup for lazy-instantiated GEOSGeometry object.
|
# Setup for lazy-instantiated GEOSGeometry object.
|
||||||
setattr(cls, self.attname, GeometryProxy(GEOSGeometry, self))
|
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):
|
def get_manipulator_field_objs(self):
|
||||||
"Using the WKTField (defined above) to be our manipulator."
|
"Using the WKTField (defined above) to be our manipulator."
|
||||||
return [WKTField]
|
return [WKTField]
|
||||||
|
@ -6,7 +6,7 @@ from django.db.models.fields import FieldDoesNotExist
|
|||||||
from django.utils.datastructures import SortedDict
|
from django.utils.datastructures import SortedDict
|
||||||
from django.contrib.gis.db.models.fields import GeometryField
|
from django.contrib.gis.db.models.fields import GeometryField
|
||||||
# parse_lookup depends on the spatial database backend.
|
# 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
|
from django.contrib.gis.geos import GEOSGeometry
|
||||||
|
|
||||||
# Shortcut booleans for determining the backend.
|
# Shortcut booleans for determining the backend.
|
||||||
@ -233,24 +233,19 @@ class GeoQuerySet(QuerySet):
|
|||||||
return "%s.%s" % (qn(self.model._meta.db_table),
|
return "%s.%s" % (qn(self.model._meta.db_table),
|
||||||
qn(field.column))
|
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
|
Returns the first Geometry field encountered; or specified via the
|
||||||
instance of a GeographicField, otherwise, the database column for the
|
`field_name` keyword.
|
||||||
geographic field is returned.
|
|
||||||
"""
|
"""
|
||||||
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:
|
for field in self.model._meta.fields:
|
||||||
if isinstance(field, GeometryField):
|
if isinstance(field, GeometryField):
|
||||||
return field.name
|
fname = field.name
|
||||||
raise Exception('No GeometryFields in the model.')
|
if field_name:
|
||||||
|
if field_name == field.name: return field
|
||||||
|
else:
|
||||||
|
return field
|
||||||
|
raise False
|
||||||
|
|
||||||
def distance(self, *args, **kwargs):
|
def distance(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
@ -266,7 +261,7 @@ class GeoQuerySet(QuerySet):
|
|||||||
# calculations from.
|
# calculations from.
|
||||||
nargs = len(args)
|
nargs = len(args)
|
||||||
if nargs == 1:
|
if nargs == 1:
|
||||||
field_name = self._get_geofield()
|
field_name = None
|
||||||
geom = args[0]
|
geom = args[0]
|
||||||
elif nargs == 2:
|
elif nargs == 2:
|
||||||
field_name, geom = args
|
field_name, geom = args
|
||||||
@ -274,29 +269,33 @@ class GeoQuerySet(QuerySet):
|
|||||||
raise ValueError('Maximum two arguments allowed for `distance` aggregate.')
|
raise ValueError('Maximum two arguments allowed for `distance` aggregate.')
|
||||||
|
|
||||||
# Getting the quoted column.
|
# Getting the quoted column.
|
||||||
field_col = self._geo_column(field_name)
|
geo_field = self._geo_field(field_name)
|
||||||
if not field_col:
|
if not geo_field:
|
||||||
raise TypeError('Distance output only available on GeometryFields.')
|
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
|
# Using the field's get_db_prep_lookup() to get any needed
|
||||||
# transformation SQL -- we pass in a 'dummy' `contains` lookup
|
# transformation and distance SQL -- we pass in a 'dummy'
|
||||||
# type.
|
# `distance_lte` lookup type.
|
||||||
geom_sql = geo_field.get_db_prep_lookup('contains', geom)
|
where, params = geo_field.get_db_prep_lookup('distance_lte', (geom, 0))
|
||||||
if oracle:
|
if oracle:
|
||||||
# The `tolerance` keyword may be used for Oracle.
|
# The `tolerance` keyword may be used for Oracle.
|
||||||
tolerance = kwargs.get('tolerance', 0.05)
|
tolerance = kwargs.get('tolerance', 0.05)
|
||||||
|
|
||||||
# More legwork here because the OracleSpatialAdaptor doesn't do
|
# More legwork here because the OracleSpatialAdaptor doesn't do
|
||||||
# quoting of the WKT.
|
# quoting of the WKT.
|
||||||
params = ["'%s'" % geom_sql.params[0]]
|
tmp_params = [gqn(str(params[0]))]
|
||||||
params.extend(geom_sql.params[1:])
|
tmp_params.extend(params[1:])
|
||||||
gsql = geom_sql.where[0] % tuple(params)
|
dsql = where[0] % tuple(tmp_params)
|
||||||
dist_select = {'distance' : '%s(%s, %s, %s)' % (DISTANCE, field_col, gsql, tolerance)}
|
dist_select = {'distance' : '%s(%s, %s, %s)' % (DISTANCE, geo_col, dsql, tolerance)}
|
||||||
else:
|
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)
|
return self.extra(select=dist_select)
|
||||||
|
|
||||||
def extent(self, field_name=None):
|
def extent(self, field_name=None):
|
||||||
@ -308,12 +307,10 @@ class GeoQuerySet(QuerySet):
|
|||||||
if not EXTENT:
|
if not EXTENT:
|
||||||
raise ImproperlyConfigured('Extent stored procedure not available.')
|
raise ImproperlyConfigured('Extent stored procedure not available.')
|
||||||
|
|
||||||
if not field_name:
|
geo_field = self._geo_field(field_name)
|
||||||
field_name = self._get_geofield()
|
if not geo_field:
|
||||||
|
|
||||||
field_col = self._geo_column(field_name)
|
|
||||||
if not field_col:
|
|
||||||
raise TypeError('Extent information only available on GeometryFields.')
|
raise TypeError('Extent information only available on GeometryFields.')
|
||||||
|
geo_col = self._field_column(geo_field)
|
||||||
|
|
||||||
# Getting the SQL for the query.
|
# Getting the SQL for the query.
|
||||||
try:
|
try:
|
||||||
@ -322,7 +319,7 @@ class GeoQuerySet(QuerySet):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# Constructing the query that will select the extent.
|
# 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
|
# Getting a cursor, executing the query, and extracting the returned
|
||||||
# value from the extent function.
|
# value from the extent function.
|
||||||
@ -353,23 +350,21 @@ class GeoQuerySet(QuerySet):
|
|||||||
|
|
||||||
# If no field name explicitly given, get the first GeometryField from
|
# If no field name explicitly given, get the first GeometryField from
|
||||||
# the model.
|
# the model.
|
||||||
if not field_name:
|
geo_field = self._geo_field(field_name)
|
||||||
field_name = self._get_geofield()
|
if not geo_field:
|
||||||
|
|
||||||
field_col = self._geo_column(field_name)
|
|
||||||
if not field_col:
|
|
||||||
raise TypeError('GML output only available on GeometryFields.')
|
raise TypeError('GML output only available on GeometryFields.')
|
||||||
|
geo_col = self._field_column(geo_field)
|
||||||
|
|
||||||
if oracle:
|
if oracle:
|
||||||
gml_select = {'gml':'%s(%s)' % (ASGML, field_col)}
|
gml_select = {'gml':'%s(%s)' % (ASGML, geo_col)}
|
||||||
elif postgis:
|
elif postgis:
|
||||||
# PostGIS AsGML() aggregate function parameter order depends on the
|
# PostGIS AsGML() aggregate function parameter order depends on the
|
||||||
# version -- uggh.
|
# version -- uggh.
|
||||||
major, minor1, minor2 = SpatialBackend.version
|
major, minor1, minor2 = SpatialBackend.version
|
||||||
if major >= 1 and (minor1 > 3 or (minor1 == 3 and minor2 > 1)):
|
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:
|
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.
|
# Adding GML function call to SELECT part of the SQL.
|
||||||
return self.extra(select=gml_select)
|
return self.extra(select=gml_select)
|
||||||
@ -385,48 +380,49 @@ class GeoQuerySet(QuerySet):
|
|||||||
raise ImproperlyConfigured('AsKML() stored procedure not available.')
|
raise ImproperlyConfigured('AsKML() stored procedure not available.')
|
||||||
|
|
||||||
# Getting the geographic field.
|
# Getting the geographic field.
|
||||||
if not field_name:
|
geo_field = self._geo_field(field_name)
|
||||||
field_name = self._get_geofield()
|
if not geo_field:
|
||||||
|
|
||||||
field_col = self._geo_column(field_name)
|
|
||||||
if not field_col:
|
|
||||||
raise TypeError('KML output only available on GeometryFields.')
|
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.
|
# 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):
|
def transform(self, field_name=None, srid=4326):
|
||||||
"""
|
"""
|
||||||
Transforms the given geometry field to the given SRID. If no SRID is
|
Transforms the given geometry field to the given SRID. If no SRID is
|
||||||
provided, the transformation will default to using 4326 (WGS84).
|
provided, the transformation will default to using 4326 (WGS84).
|
||||||
"""
|
"""
|
||||||
# Getting the geographic field.
|
TRANSFORM = SpatialBackend.transform
|
||||||
if not field_name:
|
if not TRANSFORM:
|
||||||
field_name = self._get_geofield()
|
raise ImproperlyConfigured('Transform stored procedure not available.')
|
||||||
elif isinstance(field_name, int):
|
|
||||||
srid = field_name
|
|
||||||
field_name = self._get_geofield()
|
|
||||||
|
|
||||||
field = self.model._meta.get_field(field_name)
|
# `field_name` is first for backwards compatibility; but we want to
|
||||||
if not isinstance(field, GeometryField):
|
# 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)
|
raise TypeError('%s() only available for GeometryFields' % TRANSFORM)
|
||||||
|
|
||||||
# Why cascading substitutions? Because spatial backends like
|
# Why cascading substitutions? Because spatial backends like
|
||||||
# Oracle and MySQL already require a function call to convert to text, thus
|
# 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.
|
# when there's also a transformation we need to cascade the substitutions.
|
||||||
# For example, 'SDO_UTIL.TO_WKTGEOMETRY(SDO_CS.TRANSFORM( ... )'
|
# 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
|
# Setting the key for the field's column with the custom SELECT SQL to
|
||||||
# override the geometry column returned from the database.
|
# override the geometry column returned from the database.
|
||||||
TRANSFORM = SpatialBackend.transform
|
|
||||||
if oracle:
|
if oracle:
|
||||||
custom_sel = '%s(%s, %s)' % (TRANSFORM, col, srid)
|
custom_sel = '%s(%s, %s)' % (TRANSFORM, geo_col, srid)
|
||||||
self._ewkt = srid
|
self._ewkt = srid
|
||||||
else:
|
else:
|
||||||
custom_sel = '(%s(%s, %s)) AS %s' % \
|
custom_sel = '(%s(%s, %s)) AS %s' % \
|
||||||
(TRANSFORM, col, srid, connection.ops.quote_name(field.column))
|
(TRANSFORM, geo_col, srid, connection.ops.quote_name(geo_field.column))
|
||||||
self._custom_select[field.column] = custom_sel
|
self._custom_select[geo_field.column] = custom_sel
|
||||||
return self._clone()
|
return self._clone()
|
||||||
|
|
||||||
def union(self, field_name=None, tolerance=0.0005):
|
def union(self, field_name=None, tolerance=0.0005):
|
||||||
@ -441,12 +437,10 @@ class GeoQuerySet(QuerySet):
|
|||||||
raise ImproperlyConfigured('Union stored procedure not available.')
|
raise ImproperlyConfigured('Union stored procedure not available.')
|
||||||
|
|
||||||
# Getting the geographic field column
|
# Getting the geographic field column
|
||||||
if not field_name:
|
geo_field = self._geo_field(field_name)
|
||||||
field_name = self._get_geofield()
|
if not geo_field:
|
||||||
|
|
||||||
field_col = self._geo_column(field_name)
|
|
||||||
if not field_col:
|
|
||||||
raise TypeError('Aggregate Union only available on GeometryFields.')
|
raise TypeError('Aggregate Union only available on GeometryFields.')
|
||||||
|
geo_col = self._field_column(geo_field)
|
||||||
|
|
||||||
# Getting the SQL for the query.
|
# Getting the SQL for the query.
|
||||||
try:
|
try:
|
||||||
@ -458,10 +452,10 @@ class GeoQuerySet(QuerySet):
|
|||||||
# on the geographic field column.
|
# on the geographic field column.
|
||||||
if oracle:
|
if oracle:
|
||||||
union_sql = 'SELECT %s' % self._geo_fmt
|
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
|
union_sql += sql
|
||||||
else:
|
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.
|
# Getting a cursor, executing the query.
|
||||||
cursor = connection.cursor()
|
cursor = connection.cursor()
|
||||||
|
@ -71,7 +71,7 @@ class SpatialRefSysMixin(object):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def spheroid(self):
|
def spheroid(self):
|
||||||
"Returns the spheroid for this spatial reference."
|
"Returns the spheroid name for this spatial reference."
|
||||||
return self.srs['spheroid']
|
return self.srs['spheroid']
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -170,6 +170,34 @@ class SpatialRefSysMixin(object):
|
|||||||
m = cls.units_regex.match(wkt)
|
m = cls.units_regex.match(wkt)
|
||||||
return m.group('unit'), m.group('unit_name')
|
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):
|
def __unicode__(self):
|
||||||
"""
|
"""
|
||||||
Returns the string representation. If GDAL is installed,
|
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
|
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)
|
name = models.CharField(max_length=30)
|
||||||
point = models.PointField(srid=32140)
|
point = models.PointField(srid=32140)
|
||||||
objects = models.GeoManager()
|
objects = models.GeoManager()
|
||||||
def __unicode__(self): return self.name
|
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):
|
#class County(models.Model):
|
||||||
# name = models.CharField(max_length=30)
|
# name = models.CharField(max_length=30)
|
||||||
# mpoly = models.MultiPolygonField(srid=32140)
|
# mpoly = models.MultiPolygonField(srid=32140)
|
||||||
# objects = models.GeoManager()
|
# objects = models.GeoManager()
|
||||||
|
|
||||||
city_mapping = {'name' : 'Name',
|
|
||||||
'point' : 'POINT',
|
|
||||||
}
|
|
||||||
|
|
||||||
#county_mapping = {'name' : 'Name',
|
|
||||||
# 'mpoly' : 'MULTIPOLYGON',
|
|
||||||
# }
|
|
||||||
|
@ -1,53 +1,68 @@
|
|||||||
import os, unittest
|
import os, unittest
|
||||||
from decimal import Decimal
|
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.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.measure import D # alias for Distance
|
||||||
|
from django.contrib.gis.db.models import GeoQ
|
||||||
from django.contrib.gis.tests.utils import oracle
|
from django.contrib.gis.tests.utils import oracle
|
||||||
|
|
||||||
shp_path = os.path.dirname(__file__)
|
from models import SouthTexasCity, AustraliaCity
|
||||||
city_shp = os.path.join(shp_path, 'cities/cities.shp')
|
from data import au_cities, stx_cities
|
||||||
#county_shp = os.path.join(shp_path, 'counties/counties.shp')
|
|
||||||
|
|
||||||
class DistanceTest(unittest.TestCase):
|
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):
|
def test01_init(self):
|
||||||
"LayerMapping initialization of distance models."
|
"Initialization of distance models."
|
||||||
|
|
||||||
city_lm = LayerMapping(City, city_shp, city_mapping, transform=False)
|
def load_cities(city_model, srid, data_tup):
|
||||||
city_lm.save()
|
for name, x, y in data_tup:
|
||||||
|
c = city_model(name=name, point=Point(x, y, srid=srid))
|
||||||
|
c.save()
|
||||||
|
|
||||||
# TODO: complete tests with distance from multipolygons.
|
load_cities(SouthTexasCity, 32140, stx_cities)
|
||||||
#county_lm = LayerMapping(County, county_shp, county_mapping, transform=False)
|
load_cities(AustraliaCity, 4326, au_cities)
|
||||||
#county_lm.save()
|
|
||||||
|
|
||||||
self.assertEqual(12, City.objects.count())
|
self.assertEqual(10, SouthTexasCity.objects.count())
|
||||||
#self.assertEqual(60, County.objects.count())
|
self.assertEqual(10, AustraliaCity.objects.count())
|
||||||
|
|
||||||
# TODO: Complete tests for `dwithin` lookups.
|
def test02_dwithin(self):
|
||||||
#def test02_dwithin(self):
|
"Testing the `dwithin` lookup type."
|
||||||
# "Testing the `dwithin` lookup type."
|
pnt = self.stx_pnt
|
||||||
# pass
|
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):
|
def test03_distance_aggregate(self):
|
||||||
"Testing the `distance` GeoQuerySet method."
|
"Testing the `distance` GeoQuerySet method."
|
||||||
# The point for La Grange, TX
|
# The point for La Grange, TX
|
||||||
lagrange = GEOSGeometry('POINT(-96.876369 29.905320)', 4326)
|
lagrange = GEOSGeometry('POINT(-96.876369 29.905320)', 4326)
|
||||||
# Got these from using the raw SQL statement:
|
# 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;
|
# SELECT ST_Distance(point, ST_Transform(ST_GeomFromText('POINT(-96.876369 29.905320)', 4326),32140)) FROM distapp_southtexascity;
|
||||||
distances = [147075.069813436, 139630.198056286, 140888.552826286,
|
distances = [147075.069813, 139630.198056, 140888.552826,
|
||||||
138809.684197415, 158309.246259353, 212183.594374882,
|
138809.684197, 158309.246259, 212183.594374,
|
||||||
70870.1889675217, 319225.965633536, 165337.758878256,
|
70870.188967, 165337.758878, 102128.654360,
|
||||||
92630.7446925393, 102128.654360872, 139196.085105372]
|
139196.085105]
|
||||||
dist1 = City.objects.distance('point', lagrange)
|
dist1 = SouthTexasCity.objects.distance('point', lagrange)
|
||||||
dist2 = City.objects.distance(lagrange)
|
dist2 = SouthTexasCity.objects.distance(lagrange)
|
||||||
|
|
||||||
# Original query done on PostGIS, have to adjust AlmostEqual tolerance
|
# Original query done on PostGIS, have to adjust AlmostEqual tolerance
|
||||||
# for Oracle.
|
# for Oracle.
|
||||||
if oracle: tol = 3
|
if oracle: tol = 3
|
||||||
else: tol = 7
|
else: tol = 5
|
||||||
|
|
||||||
for qs in [dist1, dist2]:
|
for qs in [dist1, dist2]:
|
||||||
for i, c in enumerate(qs):
|
for i, c in enumerate(qs):
|
||||||
@ -55,30 +70,50 @@ class DistanceTest(unittest.TestCase):
|
|||||||
|
|
||||||
def test04_distance_lookups(self):
|
def test04_distance_lookups(self):
|
||||||
"Testing the `distance_lt`, `distance_gt`, `distance_lte`, and `distance_gte` lookup types."
|
"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
|
# Only two cities (Houston and Southside Place) should be
|
||||||
# within 7km of the given point.
|
# within 7km of the given point.
|
||||||
qs1 = City.objects.filter(point__distance_lte=(pnt, D(km=7))) # Query w/Distance instance.
|
dists = [D(km=7), D(mi=4.349), # Distance instances in different units.
|
||||||
qs2 = City.objects.filter(point__distance_lte=(pnt, 7000)) # Query w/int (units are assumed to be that of the field)
|
7000, 7000.0, Decimal(7000), # int, float, Decimal parameters.
|
||||||
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
|
|
||||||
|
|
||||||
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:
|
for c in qs:
|
||||||
self.assertEqual(2, qs.count())
|
cities = self.get_cities(qs)
|
||||||
self.failIf(not c.name in ['Downtown Houston', 'Southside Place'])
|
self.assertEqual(cities, ['Downtown Houston', 'Southside Place'])
|
||||||
|
|
||||||
# Now only retrieving the cities within a 20km 'donut' w/a 7km radius 'hole'
|
# Now only retrieving the cities within a 20km 'donut' w/a 7km radius 'hole'
|
||||||
# (thus, Houston and Southside place will be excluded)
|
# (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)))
|
qs = SouthTexasCity.objects.filter(point__distance_gte=(self.stx_pnt, D(km=7))).filter(point__distance_lte=(self.stx_pnt, D(km=20)))
|
||||||
self.assertEqual(3, qs.count())
|
cities = self.get_cities(qs)
|
||||||
for c in qs:
|
self.assertEqual(cities, ['Bellaire', 'Pearland', 'West University Place'])
|
||||||
self.failIf(not c.name in ['Pearland', 'Bellaire', '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():
|
def suite():
|
||||||
s = unittest.TestSuite()
|
s = unittest.TestSuite()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user