mirror of
https://github.com/django/django.git
synced 2025-07-04 01:39:20 +00:00
gis: Refactor of the GeoQuerySet
; new features include:
(1) Creation of internal API that eases generation of `GeoQuerySet` methods. (2) `GeoQuerySet.distance` now returns `Distance` objects instead of floats. (3) Added the new `GeoQuerySet` methods: `area`, `centroid`, `difference`, `envelope`, `intersection`, `length`, `make_line`, `mem_size`, `num_geom`, `num_points`, `perimeter`, `point_on_surface`, `scale`, `svg`, `sym_difference`, `translate`, `union`. (4) The `model_att` keyword may be used to customize the attribute that `GeoQuerySet` methods attach output to. (5) Geographic distance lookups and `GeoQuerySet.distance` calls now use `ST_distance_sphere` by default (performance benefits far outweigh small loss in accuracy); `ST_distance_spheroid` may still be used by specifying an option. (6) `GeoQuerySet` methods may now operate accross ForeignKey relations specified via the `field_name` keyword (but this does not work on Oracle). (7) `Area` now has the same units of measure as `Distance`. Backward Incompatibilites: * The aggregate union method is now known as `unionagg`. * The `field_name` keyword used for `GeoQuerySet` methods may no longer be specified via positional arguments. * `Distance` objects returned instead of floats from `GeoQuerySet.distance`. * `ST_Distance_sphere` used by default for geographic distance calculations. git-svn-id: http://code.djangoproject.com/svn/django/branches/gis@7641 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
parent
eb30cad66a
commit
4ec80c4333
@ -3,97 +3,16 @@
|
|||||||
|
|
||||||
Specifically, this module will import the correct routines and modules
|
Specifically, this module will import the correct routines and modules
|
||||||
needed for GeoDjango to interface with the spatial database.
|
needed for GeoDjango to interface with the spatial database.
|
||||||
|
|
||||||
Some of the more important classes and routines from the spatial backend
|
|
||||||
include:
|
|
||||||
|
|
||||||
(1) `GeoBackEndField`, a base class needed for GeometryField.
|
|
||||||
(2) `get_geo_where_clause`, a routine used by `GeoWhereNode`.
|
|
||||||
(3) `GIS_TERMS`, a listing of all valid GeoDjango lookup types.
|
|
||||||
(4) `SpatialBackend`, a container object for information specific to the
|
|
||||||
spatial backend.
|
|
||||||
"""
|
"""
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db.models.sql.query import QUERY_TERMS
|
|
||||||
from django.contrib.gis.db.backend.util import gqn
|
from django.contrib.gis.db.backend.util import gqn
|
||||||
|
|
||||||
# These routines (needed by GeoManager), default to 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.
|
# Retrieving the necessary settings from the backend.
|
||||||
if settings.DATABASE_ENGINE == 'postgresql_psycopg2':
|
if settings.DATABASE_ENGINE == 'postgresql_psycopg2':
|
||||||
from django.contrib.gis.db.backend.postgis.adaptor import \
|
from django.contrib.gis.db.backend.postgis import create_spatial_db, get_geo_where_clause, SpatialBackend
|
||||||
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':
|
elif settings.DATABASE_ENGINE == 'oracle':
|
||||||
from django.contrib.gis.db.backend.adaptor import WKTAdaptor as GeoAdaptor
|
from django.contrib.gis.db.backend.oracle import create_spatial_db, get_geo_where_clause, SpatialBackend
|
||||||
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':
|
elif settings.DATABASE_ENGINE == 'mysql':
|
||||||
from django.contrib.gis.db.backend.adaptor import WKTAdaptor as GeoAdaptor
|
from django.contrib.gis.db.backend.mysql import create_spatial_db, get_geo_where_clause, SpatialBackend
|
||||||
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:
|
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):
|
|
||||||
"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
|
|
||||||
|
|
||||||
# Shortcut booleans.
|
|
||||||
mysql = SPATIAL_BACKEND == 'mysql'
|
|
||||||
oracle = SPATIAL_BACKEND == 'oracle'
|
|
||||||
postgis = SPATIAL_BACKEND == 'postgis'
|
|
||||||
|
|
||||||
# Class for the backend field.
|
|
||||||
Field = GeoBackendField
|
|
||||||
|
|
||||||
# Adaptor class used for quoting GEOS geometries in the database.
|
|
||||||
Adaptor = GeoAdaptor
|
|
||||||
|
29
django/contrib/gis/db/backend/base.py
Normal file
29
django/contrib/gis/db/backend/base.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
"""
|
||||||
|
This module holds the base `SpatialBackend` object, which is
|
||||||
|
instantiated by each spatial backend with the features it has.
|
||||||
|
"""
|
||||||
|
# TODO: Create a `Geometry` protocol and allow user to use
|
||||||
|
# different Geometry objects -- for now we just use GEOSGeometry.
|
||||||
|
from django.contrib.gis.geos import GEOSGeometry, GEOSException
|
||||||
|
|
||||||
|
class BaseSpatialBackend(object):
|
||||||
|
Geometry = GEOSGeometry
|
||||||
|
GeometryException = GEOSException
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
kwargs.setdefault('distance_functions', {})
|
||||||
|
kwargs.setdefault('limited_where', {})
|
||||||
|
for k, v in kwargs.iteritems(): setattr(self, k, v)
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
"""
|
||||||
|
All attributes of the spatial backend return False by default.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return self.__dict__[name]
|
||||||
|
except KeyError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1 +1,13 @@
|
|||||||
|
__all__ = ['create_spatial_db', 'get_geo_where_clause', 'SpatialBackend']
|
||||||
|
|
||||||
|
from django.contrib.gis.db.backend.base import BaseSpatialBackend
|
||||||
|
from django.contrib.gis.db.backend.adaptor import WKTAdaptor
|
||||||
|
from django.contrib.gis.db.backend.mysql.creation import create_spatial_db
|
||||||
|
from django.contrib.gis.db.backend.mysql.field import MySQLGeoField
|
||||||
|
from django.contrib.gis.db.backend.mysql.query import *
|
||||||
|
|
||||||
|
SpatialBackend = BaseSpatialBackend(name='mysql', mysql=True,
|
||||||
|
gis_terms=MYSQL_GIS_TERMS,
|
||||||
|
select=GEOM_SELECT,
|
||||||
|
Adaptor=WKTAdaptor,
|
||||||
|
Field=MySQLGeoField)
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
__all__ = ['create_spatial_db', 'get_geo_where_clause', 'SpatialBackend']
|
||||||
|
|
||||||
|
from django.contrib.gis.db.backend.base import BaseSpatialBackend
|
||||||
|
from django.contrib.gis.db.backend.oracle.adaptor import OracleSpatialAdaptor
|
||||||
|
from django.contrib.gis.db.backend.oracle.creation import create_spatial_db
|
||||||
|
from django.contrib.gis.db.backend.oracle.field import OracleSpatialField
|
||||||
|
from django.contrib.gis.db.backend.oracle.query import *
|
||||||
|
|
||||||
|
SpatialBackend = BaseSpatialBackend(name='oracle', oracle=True,
|
||||||
|
area=AREA,
|
||||||
|
centroid=CENTROID,
|
||||||
|
difference=DIFFERENCE,
|
||||||
|
distance=DISTANCE,
|
||||||
|
distance_functions=DISTANCE_FUNCTIONS,
|
||||||
|
gis_terms=ORACLE_SPATIAL_TERMS,
|
||||||
|
gml=ASGML,
|
||||||
|
intersection=INTERSECTION,
|
||||||
|
length=LENGTH,
|
||||||
|
limited_where = {'relate' : None},
|
||||||
|
num_geom=NUM_GEOM,
|
||||||
|
num_points=NUM_POINTS,
|
||||||
|
perimeter=LENGTH,
|
||||||
|
point_on_surface=POINT_ON_SURFACE,
|
||||||
|
select=GEOM_SELECT,
|
||||||
|
sym_difference=SYM_DIFFERENCE,
|
||||||
|
transform=TRANSFORM,
|
||||||
|
unionagg=UNIONAGG,
|
||||||
|
union=UNION,
|
||||||
|
Adaptor=OracleSpatialAdaptor,
|
||||||
|
Field=OracleSpatialField,
|
||||||
|
)
|
5
django/contrib/gis/db/backend/oracle/adaptor.py
Normal file
5
django/contrib/gis/db/backend/oracle/adaptor.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from cx_Oracle import CLOB
|
||||||
|
from django.contrib.gis.db.backend.adaptor import WKTAdaptor
|
||||||
|
|
||||||
|
class OracleSpatialAdaptor(WKTAdaptor):
|
||||||
|
input_size = CLOB
|
@ -15,10 +15,21 @@ from django.contrib.gis.measure import Distance
|
|||||||
qn = connection.ops.quote_name
|
qn = connection.ops.quote_name
|
||||||
|
|
||||||
# The GML, distance, transform, and union procedures.
|
# The GML, distance, transform, and union procedures.
|
||||||
|
AREA = 'SDO_GEOM.SDO_AREA'
|
||||||
ASGML = 'SDO_UTIL.TO_GMLGEOMETRY'
|
ASGML = 'SDO_UTIL.TO_GMLGEOMETRY'
|
||||||
|
CENTROID = 'SDO_GEOM.SDO_CENTROID'
|
||||||
|
DIFFERENCE = 'SDO_GEOM.SDO_DIFFERENCE'
|
||||||
DISTANCE = 'SDO_GEOM.SDO_DISTANCE'
|
DISTANCE = 'SDO_GEOM.SDO_DISTANCE'
|
||||||
|
EXTENT = 'SDO_AGGR_MBR'
|
||||||
|
INTERSECTION = 'SDO_GEOM.SDO_INTERSECTION'
|
||||||
|
LENGTH = 'SDO_GEOM.SDO_LENGTH'
|
||||||
|
NUM_GEOM = 'SDO_UTIL.GETNUMELEM'
|
||||||
|
NUM_POINTS = 'SDO_UTIL.GETNUMVERTICES'
|
||||||
|
POINT_ON_SURFACE = 'SDO_GEOM.SDO_POINTONSURFACE'
|
||||||
|
SYM_DIFFERENCE = 'SDO_GEOM.SDO_XOR'
|
||||||
TRANSFORM = 'SDO_CS.TRANSFORM'
|
TRANSFORM = 'SDO_CS.TRANSFORM'
|
||||||
UNION = 'SDO_AGGR_UNION'
|
UNION = 'SDO_GEOM.SDO_UNION'
|
||||||
|
UNIONAGG = 'SDO_AGGR_UNION'
|
||||||
|
|
||||||
# We want to get SDO Geometries as WKT because it is much easier to
|
# We want to get SDO Geometries as WKT because it is much easier to
|
||||||
# instantiate GEOS proxies from WKT than SDO_GEOMETRY(...) strings.
|
# instantiate GEOS proxies from WKT than SDO_GEOMETRY(...) strings.
|
||||||
|
@ -0,0 +1,42 @@
|
|||||||
|
__all__ = ['create_spatial_db', 'get_geo_where_clause', 'SpatialBackend']
|
||||||
|
|
||||||
|
from django.contrib.gis.db.backend.base import BaseSpatialBackend
|
||||||
|
from django.contrib.gis.db.backend.postgis.adaptor import PostGISAdaptor
|
||||||
|
from django.contrib.gis.db.backend.postgis.creation import create_spatial_db
|
||||||
|
from django.contrib.gis.db.backend.postgis.field import PostGISField
|
||||||
|
from django.contrib.gis.db.backend.postgis.query import *
|
||||||
|
|
||||||
|
SpatialBackend = BaseSpatialBackend(name='postgis', postgis=True,
|
||||||
|
area=AREA,
|
||||||
|
centroid=CENTROID,
|
||||||
|
difference=DIFFERENCE,
|
||||||
|
distance=DISTANCE,
|
||||||
|
distance_functions=DISTANCE_FUNCTIONS,
|
||||||
|
distance_sphere=DISTANCE_SPHERE,
|
||||||
|
distance_spheroid=DISTANCE_SPHEROID,
|
||||||
|
envelope=ENVELOPE,
|
||||||
|
extent=EXTENT,
|
||||||
|
gis_terms=POSTGIS_TERMS,
|
||||||
|
gml=ASGML,
|
||||||
|
intersection=INTERSECTION,
|
||||||
|
kml=ASKML,
|
||||||
|
length=LENGTH,
|
||||||
|
length_spheroid=LENGTH_SPHEROID,
|
||||||
|
make_line=MAKE_LINE,
|
||||||
|
mem_size=MEM_SIZE,
|
||||||
|
num_geom=NUM_GEOM,
|
||||||
|
num_points=NUM_POINTS,
|
||||||
|
perimeter=PERIMETER,
|
||||||
|
point_on_surface=POINT_ON_SURFACE,
|
||||||
|
scale=SCALE,
|
||||||
|
select=GEOM_SELECT,
|
||||||
|
svg=ASSVG,
|
||||||
|
sym_difference=SYM_DIFFERENCE,
|
||||||
|
transform=TRANSFORM,
|
||||||
|
translate=TRANSLATE,
|
||||||
|
union=UNION,
|
||||||
|
unionagg=UNIONAGG,
|
||||||
|
version=(MAJOR_VERSION, MINOR_VERSION1, MINOR_VERSION2),
|
||||||
|
Adaptor=PostGISAdaptor,
|
||||||
|
Field=PostGISField,
|
||||||
|
)
|
@ -1,6 +1,5 @@
|
|||||||
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.util import gqn
|
from django.contrib.gis.db.backend.util import gqn
|
||||||
from django.contrib.gis.db.backend.postgis.query import TRANSFORM
|
from django.contrib.gis.db.backend.postgis.query import TRANSFORM
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ if MAJOR_VERSION != 1 or (MAJOR_VERSION == 1 and MINOR_VERSION1 < 1):
|
|||||||
|
|
||||||
# Versions of PostGIS >= 1.2.2 changed their naming convention to be
|
# Versions of PostGIS >= 1.2.2 changed their naming convention to be
|
||||||
# 'SQL-MM-centric' to conform with the ISO standard. Practically, this
|
# 'SQL-MM-centric' to conform with the ISO standard. Practically, this
|
||||||
# means that 'ST_' is prefixes geometry function names.
|
# means that 'ST_' prefixes geometry function names.
|
||||||
GEOM_FUNC_PREFIX = ''
|
GEOM_FUNC_PREFIX = ''
|
||||||
if MAJOR_VERSION >= 1:
|
if MAJOR_VERSION >= 1:
|
||||||
if (MINOR_VERSION1 > 2 or
|
if (MINOR_VERSION1 > 2 or
|
||||||
@ -30,26 +30,46 @@ if MAJOR_VERSION >= 1:
|
|||||||
|
|
||||||
def get_func(func): return '%s%s' % (GEOM_FUNC_PREFIX, func)
|
def get_func(func): return '%s%s' % (GEOM_FUNC_PREFIX, func)
|
||||||
|
|
||||||
# Custom selection not needed for PostGIS since GEOS geometries may be
|
# Custom selection not needed for PostGIS because GEOS geometries are
|
||||||
# instantiated directly from the HEXEWKB returned by default. If
|
# instantiated directly from the HEXEWKB returned by default. If
|
||||||
# WKT is needed for some reason in the future, this value may be changed,
|
# WKT is needed for some reason in the future, this value may be changed,
|
||||||
# 'AsText(%s)'
|
# e.g,, 'AsText(%s)'.
|
||||||
GEOM_SELECT = None
|
GEOM_SELECT = None
|
||||||
|
|
||||||
# Functions used by the GeoManager & GeoQuerySet
|
# Functions used by the GeoManager & GeoQuerySet
|
||||||
|
AREA = get_func('Area')
|
||||||
ASKML = get_func('AsKML')
|
ASKML = get_func('AsKML')
|
||||||
ASGML = get_func('AsGML')
|
ASGML = get_func('AsGML')
|
||||||
|
ASSVG = get_func('AsSVG')
|
||||||
|
CENTROID = get_func('Centroid')
|
||||||
|
DIFFERENCE = get_func('Difference')
|
||||||
DISTANCE = get_func('Distance')
|
DISTANCE = get_func('Distance')
|
||||||
|
DISTANCE_SPHERE = get_func('distance_sphere')
|
||||||
DISTANCE_SPHEROID = get_func('distance_spheroid')
|
DISTANCE_SPHEROID = get_func('distance_spheroid')
|
||||||
|
ENVELOPE = get_func('Envelope')
|
||||||
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')
|
||||||
|
INTERSECTION = get_func('Intersection')
|
||||||
|
LENGTH = get_func('Length')
|
||||||
|
LENGTH_SPHEROID = get_func('length_spheroid')
|
||||||
|
MAKE_LINE = get_func('MakeLine')
|
||||||
|
MEM_SIZE = get_func('mem_size')
|
||||||
|
NUM_GEOM = get_func('NumGeometries')
|
||||||
|
NUM_POINTS = get_func('npoints')
|
||||||
|
PERIMETER = get_func('Perimeter')
|
||||||
|
POINT_ON_SURFACE = get_func('PointOnSurface')
|
||||||
|
SCALE = get_func('Scale')
|
||||||
|
SYM_DIFFERENCE = get_func('SymDifference')
|
||||||
TRANSFORM = get_func('Transform')
|
TRANSFORM = get_func('Transform')
|
||||||
|
TRANSLATE = get_func('Translate')
|
||||||
|
|
||||||
# Special cases for union and KML methods.
|
# Special cases for union and KML methods.
|
||||||
if MINOR_VERSION1 < 3:
|
if MINOR_VERSION1 < 3:
|
||||||
UNION = 'GeomUnion'
|
UNIONAGG = 'GeomUnion'
|
||||||
|
UNION = 'Union'
|
||||||
else:
|
else:
|
||||||
|
UNIONAGG = 'ST_Union'
|
||||||
UNION = 'ST_Union'
|
UNION = 'ST_Union'
|
||||||
|
|
||||||
if MINOR_VERSION1 == 1:
|
if MINOR_VERSION1 == 1:
|
||||||
@ -80,16 +100,23 @@ class PostGISDistance(PostGISFunction):
|
|||||||
super(PostGISDistance, self).__init__(self.dist_func, end_subst=') %s %s',
|
super(PostGISDistance, self).__init__(self.dist_func, end_subst=') %s %s',
|
||||||
operator=operator, result='%%s')
|
operator=operator, result='%%s')
|
||||||
|
|
||||||
class PostGISSphereDistance(PostGISFunction):
|
class PostGISSpheroidDistance(PostGISFunction):
|
||||||
"For PostGIS spherical distance operations."
|
"For PostGIS spherical distance operations (using the spheroid)."
|
||||||
dist_func = 'distance_spheroid'
|
dist_func = 'distance_spheroid'
|
||||||
def __init__(self, operator):
|
def __init__(self, operator):
|
||||||
# An extra parameter in `end_subst` is needed for the spheroid string.
|
# An extra parameter in `end_subst` is needed for the spheroid string.
|
||||||
super(PostGISSphereDistance, self).__init__(self.dist_func,
|
super(PostGISSpheroidDistance, self).__init__(self.dist_func,
|
||||||
beg_subst='%s(%s, %%s, %%s',
|
beg_subst='%s(%s, %%s, %%s',
|
||||||
end_subst=') %s %s',
|
end_subst=') %s %s',
|
||||||
operator=operator, result='%%s')
|
operator=operator, result='%%s')
|
||||||
|
|
||||||
|
class PostGISSphereDistance(PostGISFunction):
|
||||||
|
"For PostGIS spherical distance operations."
|
||||||
|
dist_func = 'distance_sphere'
|
||||||
|
def __init__(self, operator):
|
||||||
|
super(PostGISSphereDistance, self).__init__(self.dist_func, 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."
|
||||||
pattern_regex = re.compile(r'^[012TF\*]{9}$')
|
pattern_regex = re.compile(r'^[012TF\*]{9}$')
|
||||||
@ -164,7 +191,7 @@ POSTGIS_GEOMETRY_FUNCTIONS = {
|
|||||||
dtypes = (Decimal, Distance, float, int, long)
|
dtypes = (Decimal, Distance, float, int, long)
|
||||||
def get_dist_ops(operator):
|
def get_dist_ops(operator):
|
||||||
"Returns operations for both regular and spherical distances."
|
"Returns operations for both regular and spherical distances."
|
||||||
return (PostGISDistance(operator), PostGISSphereDistance(operator))
|
return (PostGISDistance(operator), PostGISSphereDistance(operator), PostGISSpheroidDistance(operator))
|
||||||
DISTANCE_FUNCTIONS = {
|
DISTANCE_FUNCTIONS = {
|
||||||
'distance_gt' : (get_dist_ops('>'), dtypes),
|
'distance_gt' : (get_dist_ops('>'), dtypes),
|
||||||
'distance_gte' : (get_dist_ops('>='), dtypes),
|
'distance_gte' : (get_dist_ops('>='), dtypes),
|
||||||
@ -193,6 +220,13 @@ POSTGIS_TERMS += POSTGIS_GEOMETRY_FUNCTIONS.keys() # Adding on the Geometry Func
|
|||||||
POSTGIS_TERMS += MISC_TERMS # Adding any other miscellaneous terms (e.g., 'isnull')
|
POSTGIS_TERMS += MISC_TERMS # Adding any other miscellaneous terms (e.g., 'isnull')
|
||||||
POSTGIS_TERMS = tuple(POSTGIS_TERMS) # Making immutable
|
POSTGIS_TERMS = tuple(POSTGIS_TERMS) # Making immutable
|
||||||
|
|
||||||
|
# For checking tuple parameters -- not very pretty but gets job done.
|
||||||
|
def exactly_two(val): return val == 2
|
||||||
|
def two_to_three(val): return val >= 2 and val <=3
|
||||||
|
def num_params(lookup_type, val):
|
||||||
|
if lookup_type in DISTANCE_FUNCTIONS and lookup_type != 'dwithin': return two_to_three(val)
|
||||||
|
else: return exactly_two(val)
|
||||||
|
|
||||||
#### 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, 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."
|
||||||
@ -216,8 +250,10 @@ def get_geo_where_clause(lookup_type, table_prefix, field, value):
|
|||||||
# 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, list)):
|
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:
|
# Number of valid tuple parameters depends on the lookup type.
|
||||||
raise ValueError('2-element tuple required or `%s` lookup type.' % lookup_type)
|
nparams = len(value)
|
||||||
|
if not num_params(lookup_type, nparams):
|
||||||
|
raise ValueError('Incorrect number of parameters given for `%s` lookup type.' % lookup_type)
|
||||||
|
|
||||||
# Ensuring the argument type matches what we expect.
|
# Ensuring the argument type matches what we expect.
|
||||||
if not isinstance(value[1], arg_type):
|
if not isinstance(value[1], arg_type):
|
||||||
@ -234,7 +270,9 @@ def get_geo_where_clause(lookup_type, table_prefix, field, value):
|
|||||||
raise TypeError('PostGIS spherical operations are only valid on PointFields.')
|
raise TypeError('PostGIS spherical operations are only valid on PointFields.')
|
||||||
if value[0].geom_typeid != 0:
|
if value[0].geom_typeid != 0:
|
||||||
raise TypeError('PostGIS geometry distance parameter is required to be of type Point.')
|
raise TypeError('PostGIS geometry distance parameter is required to be of type Point.')
|
||||||
op = op[1]
|
# Setting up the geodetic operation appropriately.
|
||||||
|
if nparams == 3 and value[2] == 'spheroid': op = op[2]
|
||||||
|
else: op = op[1]
|
||||||
else:
|
else:
|
||||||
op = op[0]
|
op = op[0]
|
||||||
else:
|
else:
|
||||||
|
@ -3,15 +3,12 @@ from django.db import connection
|
|||||||
from django.contrib.gis.db.backend import SpatialBackend, gqn
|
from django.contrib.gis.db.backend import SpatialBackend, gqn
|
||||||
# GeometryProxy, GEOS, Distance, and oldforms imports.
|
# 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.measure import Distance
|
from django.contrib.gis.measure import Distance
|
||||||
from django.contrib.gis.oldforms import WKTField
|
from django.contrib.gis.oldforms import WKTField
|
||||||
|
|
||||||
# Attempting to get the spatial reference system.
|
# The `get_srid_info` function gets SRID information from the spatial
|
||||||
try:
|
# reference system table w/o using the ORM.
|
||||||
from django.contrib.gis.models import SpatialRefSys
|
from django.contrib.gis.models import get_srid_info
|
||||||
except ImportError:
|
|
||||||
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(SpatialBackend.Field):
|
class GeometryField(SpatialBackend.Field):
|
||||||
@ -47,29 +44,7 @@ class GeometryField(SpatialBackend.Field):
|
|||||||
# Setting the SRID and getting the units. Unit information must be
|
# Setting the SRID and getting the units. Unit information must be
|
||||||
# easily available in the field instance for distance queries.
|
# easily available in the field instance for distance queries.
|
||||||
self._srid = srid
|
self._srid = srid
|
||||||
if SpatialRefSys:
|
self._unit, self._unit_name, self._spheroid = get_srid_info(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.
|
|
||||||
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)'
|
|
||||||
stmt = stmt % {'table' : qn(SpatialRefSys._meta.db_table),
|
|
||||||
'wkt_col' : qn(SpatialRefSys.wkt_col()),
|
|
||||||
'srid_col' : qn('srid'),
|
|
||||||
'srid' : srid,
|
|
||||||
}
|
|
||||||
cur.execute(stmt)
|
|
||||||
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.
|
# Setting the dimension of the geometry field.
|
||||||
self._dim = dim
|
self._dim = dim
|
||||||
@ -79,19 +54,26 @@ class GeometryField(SpatialBackend.Field):
|
|||||||
### Routines specific to GeometryField ###
|
### Routines specific to GeometryField ###
|
||||||
@property
|
@property
|
||||||
def geodetic(self):
|
def geodetic(self):
|
||||||
|
"""
|
||||||
|
Returns true if this field's SRID corresponds with a coordinate
|
||||||
|
system that uses non-projected units (e.g., latitude/longitude).
|
||||||
|
"""
|
||||||
return self._unit_name in self.geodetic_units
|
return self._unit_name in self.geodetic_units
|
||||||
|
|
||||||
def get_distance(self, dist, lookup_type):
|
def get_distance(self, dist_val, lookup_type):
|
||||||
"""
|
"""
|
||||||
Returns a distance number in units of the field. For example, if
|
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,
|
`D(km=1)` was passed in and the units of the field were in meters,
|
||||||
then 1000 would be returned.
|
then 1000 would be returned.
|
||||||
"""
|
"""
|
||||||
postgis = SpatialBackend.name == 'postgis'
|
# Getting the distance parameter and any options.
|
||||||
|
if len(dist_val) == 1: dist, option = dist_val[0], None
|
||||||
|
else: dist, option = dist_val
|
||||||
|
|
||||||
if isinstance(dist, Distance):
|
if isinstance(dist, Distance):
|
||||||
if self.geodetic:
|
if self.geodetic:
|
||||||
# Won't allow Distance objects w/DWithin lookups on PostGIS.
|
# Won't allow Distance objects w/DWithin lookups on PostGIS.
|
||||||
if postgis and lookup_type == 'dwithin':
|
if SpatialBackend.postgis and lookup_type == 'dwithin':
|
||||||
raise TypeError('Only numeric values of degree units are allowed on geographic DWithin queries.')
|
raise TypeError('Only numeric values of degree units are allowed on geographic DWithin queries.')
|
||||||
# Spherical distance calculation parameter should be in meters.
|
# Spherical distance calculation parameter should be in meters.
|
||||||
dist_param = dist.m
|
dist_param = dist.m
|
||||||
@ -101,8 +83,10 @@ class GeometryField(SpatialBackend.Field):
|
|||||||
# Assuming the distance is in the units of the field.
|
# Assuming the distance is in the units of the field.
|
||||||
dist_param = dist
|
dist_param = dist
|
||||||
|
|
||||||
# Sphereical distance query; returning meters.
|
if SpatialBackend.postgis and self.geodetic and lookup_type != 'dwithin' and option == 'spheroid':
|
||||||
if postgis and self.geodetic and lookup_type != 'dwithin':
|
# On PostGIS, by default `ST_distance_sphere` is used; but if the
|
||||||
|
# accuracy of `ST_distance_spheroid` is needed than the spheroid
|
||||||
|
# needs to be passed to the SQL stored procedure.
|
||||||
return [gqn(self._spheroid), dist_param]
|
return [gqn(self._spheroid), dist_param]
|
||||||
else:
|
else:
|
||||||
return [dist_param]
|
return [dist_param]
|
||||||
@ -119,12 +103,12 @@ class GeometryField(SpatialBackend.Field):
|
|||||||
|
|
||||||
# When the input is not a GEOS geometry, attempt to construct one
|
# When the input is not a GEOS geometry, attempt to construct one
|
||||||
# from the given string input.
|
# from the given string input.
|
||||||
if isinstance(geom, GEOSGeometry):
|
if isinstance(geom, SpatialBackend.Geometry):
|
||||||
pass
|
pass
|
||||||
elif isinstance(geom, basestring):
|
elif isinstance(geom, basestring):
|
||||||
try:
|
try:
|
||||||
geom = GEOSGeometry(geom)
|
geom = SpatialBackend.Geometry(geom)
|
||||||
except GEOSException:
|
except SpatialBackend.GeometryException:
|
||||||
raise ValueError('Could not create geometry from lookup value: %s' % str(value))
|
raise ValueError('Could not create geometry from lookup value: %s' % str(value))
|
||||||
else:
|
else:
|
||||||
raise TypeError('Cannot use parameter of `%s` type as lookup parameter.' % type(value))
|
raise TypeError('Cannot use parameter of `%s` type as lookup parameter.' % type(value))
|
||||||
@ -148,8 +132,8 @@ class GeometryField(SpatialBackend.Field):
|
|||||||
def contribute_to_class(self, cls, name):
|
def contribute_to_class(self, cls, name):
|
||||||
super(GeometryField, self).contribute_to_class(cls, name)
|
super(GeometryField, self).contribute_to_class(cls, name)
|
||||||
|
|
||||||
# Setup for lazy-instantiated GEOSGeometry object.
|
# Setup for lazy-instantiated Geometry object.
|
||||||
setattr(cls, self.attname, GeometryProxy(GEOSGeometry, self))
|
setattr(cls, self.attname, GeometryProxy(SpatialBackend.Geometry, self))
|
||||||
|
|
||||||
def get_db_prep_lookup(self, lookup_type, value):
|
def get_db_prep_lookup(self, lookup_type, value):
|
||||||
"""
|
"""
|
||||||
@ -166,7 +150,7 @@ class GeometryField(SpatialBackend.Field):
|
|||||||
geom = self.get_geometry(value)
|
geom = self.get_geometry(value)
|
||||||
|
|
||||||
# Getting the WHERE clause list and the associated params list. The params
|
# Getting the WHERE clause list and the associated params list. The params
|
||||||
# list is populated with the Adaptor wrapping the GEOSGeometry for the
|
# list is populated with the Adaptor wrapping the Geometry for the
|
||||||
# backend. The WHERE clause list contains the placeholder for the adaptor
|
# backend. The WHERE clause list contains the placeholder for the adaptor
|
||||||
# (e.g. any transformation SQL).
|
# (e.g. any transformation SQL).
|
||||||
where = [self.get_placeholder(geom)]
|
where = [self.get_placeholder(geom)]
|
||||||
@ -175,7 +159,7 @@ class GeometryField(SpatialBackend.Field):
|
|||||||
if isinstance(value, (tuple, list)):
|
if isinstance(value, (tuple, list)):
|
||||||
if lookup_type in SpatialBackend.distance_functions:
|
if lookup_type in SpatialBackend.distance_functions:
|
||||||
# Getting the distance parameter in the units of the field.
|
# Getting the distance parameter in the units of the field.
|
||||||
where += self.get_distance(value[1], lookup_type)
|
where += self.get_distance(value[1:], lookup_type)
|
||||||
elif lookup_type in SpatialBackend.limited_where:
|
elif lookup_type in SpatialBackend.limited_where:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
@ -187,15 +171,15 @@ class GeometryField(SpatialBackend.Field):
|
|||||||
|
|
||||||
def get_db_prep_save(self, value):
|
def get_db_prep_save(self, value):
|
||||||
"Prepares the value for saving in the database."
|
"Prepares the value for saving in the database."
|
||||||
if isinstance(value, GEOSGeometry):
|
if isinstance(value, SpatialBackend.Geometry):
|
||||||
return SpatialBackend.Adaptor(value)
|
return SpatialBackend.Adaptor(value)
|
||||||
elif value is None:
|
elif value is None:
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
raise TypeError('Geometry Proxy should only return GEOSGeometry objects or None.')
|
raise TypeError('Geometry Proxy should only return Geometry 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 (oldforms) to be our manipulator."
|
||||||
return [WKTField]
|
return [WKTField]
|
||||||
|
|
||||||
# The OpenGIS Geometry Type Fields
|
# The OpenGIS Geometry Type Fields
|
||||||
|
@ -7,20 +7,71 @@ class GeoManager(Manager):
|
|||||||
def get_query_set(self):
|
def get_query_set(self):
|
||||||
return GeoQuerySet(model=self.model)
|
return GeoQuerySet(model=self.model)
|
||||||
|
|
||||||
|
def area(self, *args, **kwargs):
|
||||||
|
return self.get_query_set().area(*args, **kwargs)
|
||||||
|
|
||||||
|
def centroid(self, *args, **kwargs):
|
||||||
|
return self.get_query_set().centroid(*args, **kwargs)
|
||||||
|
|
||||||
|
def difference(self, *args, **kwargs):
|
||||||
|
return self.get_query_set().difference(*args, **kwargs)
|
||||||
|
|
||||||
def distance(self, *args, **kwargs):
|
def distance(self, *args, **kwargs):
|
||||||
return self.get_query_set().distance(*args, **kwargs)
|
return self.get_query_set().distance(*args, **kwargs)
|
||||||
|
|
||||||
|
def envelope(self, *args, **kwargs):
|
||||||
|
return self.get_query_set().envelope(*args, **kwargs)
|
||||||
|
|
||||||
def extent(self, *args, **kwargs):
|
def extent(self, *args, **kwargs):
|
||||||
return self.get_query_set().extent(*args, **kwargs)
|
return self.get_query_set().extent(*args, **kwargs)
|
||||||
|
|
||||||
def gml(self, *args, **kwargs):
|
def gml(self, *args, **kwargs):
|
||||||
return self.get_query_set().gml(*args, **kwargs)
|
return self.get_query_set().gml(*args, **kwargs)
|
||||||
|
|
||||||
|
def intersection(self, *args, **kwargs):
|
||||||
|
return self.get_query_set().intersection(*args, **kwargs)
|
||||||
|
|
||||||
def kml(self, *args, **kwargs):
|
def kml(self, *args, **kwargs):
|
||||||
return self.get_query_set().kml(*args, **kwargs)
|
return self.get_query_set().kml(*args, **kwargs)
|
||||||
|
|
||||||
|
def length(self, *args, **kwargs):
|
||||||
|
return self.get_query_set().length(*args, **kwargs)
|
||||||
|
|
||||||
|
def make_line(self, *args, **kwargs):
|
||||||
|
return self.get_query_set().make_line(*args, **kwargs)
|
||||||
|
|
||||||
|
def mem_size(self, *args, **kwargs):
|
||||||
|
return self.get_query_set().mem_size(*args, **kwargs)
|
||||||
|
|
||||||
|
def num_geom(self, *args, **kwargs):
|
||||||
|
return self.get_query_set().num_geom(*args, **kwargs)
|
||||||
|
|
||||||
|
def num_points(self, *args, **kwargs):
|
||||||
|
return self.get_query_set().num_points(*args, **kwargs)
|
||||||
|
|
||||||
|
def perimeter(self, *args, **kwargs):
|
||||||
|
return self.get_query_set().perimeter(*args, **kwargs)
|
||||||
|
|
||||||
|
def point_on_surface(self, *args, **kwargs):
|
||||||
|
return self.get_query_set().point_on_surface(*args, **kwargs)
|
||||||
|
|
||||||
|
def scale(self, *args, **kwargs):
|
||||||
|
return self.get_query_set().scale(*args, **kwargs)
|
||||||
|
|
||||||
|
def svg(self, *args, **kwargs):
|
||||||
|
return self.get_query_set().svg(*args, **kwargs)
|
||||||
|
|
||||||
|
def sym_difference(self, *args, **kwargs):
|
||||||
|
return self.get_query_set().sym_difference(*args, **kwargs)
|
||||||
|
|
||||||
def transform(self, *args, **kwargs):
|
def transform(self, *args, **kwargs):
|
||||||
return self.get_query_set().transform(*args, **kwargs)
|
return self.get_query_set().transform(*args, **kwargs)
|
||||||
|
|
||||||
|
def translate(self, *args, **kwargs):
|
||||||
|
return self.get_query_set().translate(*args, **kwargs)
|
||||||
|
|
||||||
def union(self, *args, **kwargs):
|
def union(self, *args, **kwargs):
|
||||||
return self.get_query_set().union(*args, **kwargs)
|
return self.get_query_set().union(*args, **kwargs)
|
||||||
|
|
||||||
|
def unionagg(self, *args, **kwargs):
|
||||||
|
return self.get_query_set().unionagg(*args, **kwargs)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
"""
|
"""
|
||||||
The GeometryProxy object, allows for lazy-geometries. The proxy uses
|
The GeometryProxy object, allows for lazy-geometries. The proxy uses
|
||||||
Python descriptors for instantiating and setting GEOS Geometry objects
|
Python descriptors for instantiating and setting Geometry objects
|
||||||
corresponding to geographic model fields.
|
corresponding to geographic model fields.
|
||||||
|
|
||||||
Thanks to Robert Coup for providing this functionality (see #4322).
|
Thanks to Robert Coup for providing this functionality (see #4322).
|
||||||
@ -31,7 +31,7 @@ class GeometryProxy(object):
|
|||||||
elif (geom_value is None) or (geom_value==''):
|
elif (geom_value is None) or (geom_value==''):
|
||||||
geom = None
|
geom = None
|
||||||
else:
|
else:
|
||||||
# Otherwise, a GEOSGeometry object is built using the field's contents,
|
# Otherwise, a Geometry object is built using the field's contents,
|
||||||
# and the model's corresponding attribute is set.
|
# and the model's corresponding attribute is set.
|
||||||
geom = self._klass(geom_value)
|
geom = self._klass(geom_value)
|
||||||
setattr(obj, self._field.attname, geom)
|
setattr(obj, self._field.attname, geom)
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
from itertools import izip
|
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
from django.db.models.query import sql, QuerySet, Q
|
from django.db.models.query import sql, QuerySet, Q
|
||||||
|
|
||||||
from django.contrib.gis.db.backend import SpatialBackend
|
from django.contrib.gis.db.backend import SpatialBackend
|
||||||
from django.contrib.gis.db.models.fields import GeometryField, PointField
|
from django.contrib.gis.db.models.fields import GeometryField, PointField
|
||||||
from django.contrib.gis.db.models.sql import GeoQuery, GeoWhereNode
|
from django.contrib.gis.db.models.sql import AreaField, DistanceField, GeomField, GeoQuery, GeoWhereNode
|
||||||
from django.contrib.gis.geos import GEOSGeometry, Point
|
from django.contrib.gis.measure import Area, Distance
|
||||||
|
from django.contrib.gis.models import get_srid_info
|
||||||
qn = connection.ops.quote_name
|
qn = connection.ops.quote_name
|
||||||
|
|
||||||
# For backwards-compatibility; Q object should work just fine
|
# For backwards-compatibility; Q object should work just fine
|
||||||
@ -28,92 +28,79 @@ class GeoQuerySet(QuerySet):
|
|||||||
super(GeoQuerySet, self).__init__(model=model, query=query)
|
super(GeoQuerySet, self).__init__(model=model, query=query)
|
||||||
self.query = query or GeoQuery(self.model, connection)
|
self.query = query or GeoQuery(self.model, connection)
|
||||||
|
|
||||||
def distance(self, *args, **kwargs):
|
def area(self, tolerance=0.05, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns the area of the geographic field in an `area` attribute on
|
||||||
|
each element of this GeoQuerySet.
|
||||||
|
"""
|
||||||
|
# Peforming setup here rather than in `_spatial_attribute` so that
|
||||||
|
# we can get the units for `AreaField`.
|
||||||
|
procedure_args, geo_field = self._spatial_setup('area', field_name=kwargs.get('field_name', None))
|
||||||
|
s = {'procedure_args' : procedure_args,
|
||||||
|
'geo_field' : geo_field,
|
||||||
|
'setup' : False,
|
||||||
|
}
|
||||||
|
if SpatialBackend.oracle:
|
||||||
|
s['procedure_fmt'] = '%(geo_col)s,%(tolerance)s'
|
||||||
|
s['procedure_args']['tolerance'] = tolerance
|
||||||
|
s['select_field'] = AreaField('sq_m') # Oracle returns area in units of meters.
|
||||||
|
elif SpatialBackend.postgis:
|
||||||
|
if not geo_field.geodetic:
|
||||||
|
# Getting the area units of the geographic field.
|
||||||
|
s['select_field'] = AreaField(Area.unit_attname(geo_field._unit_name))
|
||||||
|
else:
|
||||||
|
# TODO: Do we want to support raw number areas for geodetic fields?
|
||||||
|
raise Exception('Area on geodetic coordinate systems not supported.')
|
||||||
|
return self._spatial_attribute('area', s, **kwargs)
|
||||||
|
|
||||||
|
def centroid(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns the centroid of the geographic field in a `centroid`
|
||||||
|
attribute on each element of this GeoQuerySet.
|
||||||
|
"""
|
||||||
|
return self._geom_attribute('centroid', **kwargs)
|
||||||
|
|
||||||
|
def difference(self, geom, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns the spatial difference of the geographic field in a `difference`
|
||||||
|
attribute on each element of this GeoQuerySet.
|
||||||
|
"""
|
||||||
|
return self._geomset_attribute('difference', geom, **kwargs)
|
||||||
|
|
||||||
|
def distance(self, geom, **kwargs):
|
||||||
"""
|
"""
|
||||||
Returns the distance from the given geographic field name to the
|
Returns the distance from the given geographic field name to the
|
||||||
given geometry in a `distance` attribute on each element of the
|
given geometry in a `distance` attribute on each element of the
|
||||||
GeoQuerySet.
|
GeoQuerySet.
|
||||||
|
|
||||||
|
Keyword Arguments:
|
||||||
|
`spheroid` => If the geometry field is geodetic and PostGIS is
|
||||||
|
the spatial database, then the more accurate
|
||||||
|
spheroid calculation will be used instead of the
|
||||||
|
quicker sphere calculation.
|
||||||
|
|
||||||
|
`tolerance` => Used only for Oracle. The tolerance is
|
||||||
|
in meters -- a default of 5 centimeters (0.05)
|
||||||
|
is used.
|
||||||
"""
|
"""
|
||||||
DISTANCE = SpatialBackend.distance
|
return self._distance_attribute('distance', geom, **kwargs)
|
||||||
if not DISTANCE:
|
|
||||||
raise ImproperlyConfigured('Distance() stored proecedure not available.')
|
|
||||||
|
|
||||||
# Getting the geometry field and GEOSGeometry object to base distance
|
def envelope(self, **kwargs):
|
||||||
# calculations from.
|
"""
|
||||||
nargs = len(args)
|
Returns a Geometry representing the bounding box of the
|
||||||
if nargs == 1:
|
Geometry field in an `envelope` attribute on each element of
|
||||||
field_name = None
|
the GeoQuerySet.
|
||||||
geom = args[0]
|
"""
|
||||||
elif nargs == 2:
|
return self._geom_attribute('envelope', **kwargs)
|
||||||
field_name, geom = args
|
|
||||||
else:
|
|
||||||
raise ValueError('Maximum two arguments allowed for `distance` aggregate.')
|
|
||||||
|
|
||||||
# Getting the GeometryField and quoted column.
|
def extent(self, **kwargs):
|
||||||
geo_field = self.query._geo_field(field_name)
|
|
||||||
if not geo_field:
|
|
||||||
raise TypeError('Distance output only available on GeometryFields.')
|
|
||||||
geo_col = self.query._field_column(geo_field)
|
|
||||||
|
|
||||||
# Using the field's get_db_prep_lookup() to get any needed
|
|
||||||
# transformation SQL -- we pass in a 'dummy' `contains`
|
|
||||||
# `distance_lte` lookup type.
|
|
||||||
where, params = geo_field.get_db_prep_lookup('distance_lte', (geom, 0))
|
|
||||||
if SpatialBackend.oracle:
|
|
||||||
# The `tolerance` keyword may be used for Oracle; the tolerance is
|
|
||||||
# in meters -- a default of 5 centimeters is used.
|
|
||||||
tolerance = kwargs.get('tolerance', 0.05)
|
|
||||||
dist_select = {'distance' : '%s(%s, %s, %s)' % (DISTANCE, geo_col, where[0], tolerance)}
|
|
||||||
else:
|
|
||||||
if len(where) == 3:
|
|
||||||
# Spherical distance calculation was requested (b/c spheroid
|
|
||||||
# parameter was attached) However, the PostGIS ST_distance_spheroid()
|
|
||||||
# procedure may only do queries from point columns to point geometries
|
|
||||||
# some error checking is required.
|
|
||||||
if not isinstance(geo_field, PointField):
|
|
||||||
raise TypeError('Spherical distance calculation only supported on PointFields.')
|
|
||||||
if not isinstance(GEOSGeometry(buffer(params[0].wkb)), Point):
|
|
||||||
raise TypeError('Spherical distance calculation only supported with Point Geometry parameters')
|
|
||||||
|
|
||||||
# Call to distance_spheroid() requires the spheroid as well.
|
|
||||||
dist_sql = '%s(%s, %s, %s)' % (SpatialBackend.distance_spheroid, geo_col, where[0], where[1])
|
|
||||||
else:
|
|
||||||
dist_sql = '%s(%s, %s)' % (DISTANCE, geo_col, where[0])
|
|
||||||
dist_select = {'distance' : dist_sql}
|
|
||||||
return self.extra(select=dist_select, select_params=params)
|
|
||||||
|
|
||||||
def extent(self, field_name=None):
|
|
||||||
"""
|
"""
|
||||||
Returns the extent (aggregate) of the features in the GeoQuerySet. The
|
Returns the extent (aggregate) of the features in the GeoQuerySet. The
|
||||||
extent will be returned as a 4-tuple, consisting of (xmin, ymin, xmax, ymax).
|
extent will be returned as a 4-tuple, consisting of (xmin, ymin, xmax, ymax).
|
||||||
"""
|
"""
|
||||||
EXTENT = SpatialBackend.extent
|
convert_extent = None
|
||||||
if not EXTENT:
|
if SpatialBackend.postgis:
|
||||||
raise ImproperlyConfigured('Extent stored procedure not available.')
|
def convert_extent(box, geo_field):
|
||||||
|
|
||||||
# Getting the GeometryField and quoted column.
|
|
||||||
geo_field = self.query._geo_field(field_name)
|
|
||||||
if not geo_field:
|
|
||||||
raise TypeError('Extent information only available on GeometryFields.')
|
|
||||||
geo_col = self.query._field_column(geo_field)
|
|
||||||
|
|
||||||
# Constructing the query that will select the extent.
|
|
||||||
extent_sql = '%s(%s)' % (EXTENT, geo_col)
|
|
||||||
|
|
||||||
self.query.select = [GeomSQL(extent_sql)]
|
|
||||||
self.query.select_fields = [None]
|
|
||||||
try:
|
|
||||||
esql, params = self.query.as_sql()
|
|
||||||
except sql.datastructures.EmptyResultSet:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Getting a cursor, executing the query, and extracting the returned
|
|
||||||
# value from the extent function.
|
|
||||||
cursor = connection.cursor()
|
|
||||||
cursor.execute(esql, params)
|
|
||||||
box = cursor.fetchone()[0]
|
|
||||||
|
|
||||||
if box:
|
|
||||||
# TODO: Parsing of BOX3D, Oracle support (patches welcome!)
|
# TODO: Parsing of BOX3D, Oracle support (patches welcome!)
|
||||||
# Box text will be something like "BOX(-90.0 30.0, -85.0 40.0)";
|
# Box text will be something like "BOX(-90.0 30.0, -85.0 40.0)";
|
||||||
# parsing out and returning as a 4-tuple.
|
# parsing out and returning as a 4-tuple.
|
||||||
@ -121,81 +108,156 @@ class GeoQuerySet(QuerySet):
|
|||||||
xmin, ymin = map(float, ll.split())
|
xmin, ymin = map(float, ll.split())
|
||||||
xmax, ymax = map(float, ur.split())
|
xmax, ymax = map(float, ur.split())
|
||||||
return (xmin, ymin, xmax, ymax)
|
return (xmin, ymin, xmax, ymax)
|
||||||
else:
|
elif SpatialBackend.oracle:
|
||||||
return None
|
def convert_extent(wkt, geo_field):
|
||||||
|
raise NotImplementedError
|
||||||
|
return self._spatial_aggregate('extent', convert_func=convert_extent, **kwargs)
|
||||||
|
|
||||||
def gml(self, field_name=None, precision=8, version=2):
|
def gml(self, precision=8, version=2, **kwargs):
|
||||||
"""
|
"""
|
||||||
Returns GML representation of the given field in a `gml` attribute
|
Returns GML representation of the given field in a `gml` attribute
|
||||||
on each element of the GeoQuerySet.
|
on each element of the GeoQuerySet.
|
||||||
"""
|
"""
|
||||||
# Is GML output supported?
|
s = {'desc' : 'GML', 'procedure_args' : {'precision' : precision}}
|
||||||
ASGML = SpatialBackend.as_gml
|
if SpatialBackend.postgis:
|
||||||
if not ASGML:
|
|
||||||
raise ImproperlyConfigured('AsGML() stored procedure not available.')
|
|
||||||
|
|
||||||
# Getting the GeometryField and quoted column.
|
|
||||||
geo_field = self.query._geo_field(field_name)
|
|
||||||
if not geo_field:
|
|
||||||
raise TypeError('GML output only available on GeometryFields.')
|
|
||||||
geo_col = self.query._field_column(geo_field)
|
|
||||||
|
|
||||||
if SpatialBackend.oracle:
|
|
||||||
gml_select = {'gml':'%s(%s)' % (ASGML, geo_col)}
|
|
||||||
elif SpatialBackend.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, geo_col, precision)}
|
procedure_fmt = '%(version)s,%(geo_col)s,%(precision)s'
|
||||||
else:
|
else:
|
||||||
gml_select = {'gml':'%s(%s,%s,%s)' % (ASGML, geo_col, precision, version)}
|
procedure_fmt = '%(geo_col)s,%(precision)s,%(version)s'
|
||||||
|
s['procedure_args'] = {'precision' : precision, 'version' : version}
|
||||||
|
|
||||||
# Adding GML function call to SELECT part of the SQL.
|
return self._spatial_attribute('gml', s, **kwargs)
|
||||||
return self.extra(select=gml_select)
|
|
||||||
|
|
||||||
def kml(self, field_name=None, precision=8):
|
def intersection(self, geom, **kwargs):
|
||||||
"""
|
"""
|
||||||
Returns KML representation of the given field name in a `kml`
|
Returns the spatial intersection of the Geometry field in
|
||||||
attribute on each element of the GeoQuerySet.
|
an `intersection` attribute on each element of this
|
||||||
|
GeoQuerySet.
|
||||||
"""
|
"""
|
||||||
# Is KML output supported?
|
return self._geomset_attribute('intersection', geom, **kwargs)
|
||||||
ASKML = SpatialBackend.as_kml
|
|
||||||
if not ASKML:
|
|
||||||
raise ImproperlyConfigured('AsKML() stored procedure not available.')
|
|
||||||
|
|
||||||
# Getting the GeometryField and quoted column.
|
def kml(self, **kwargs):
|
||||||
geo_field = self.query._geo_field(field_name)
|
"""
|
||||||
if not geo_field:
|
Returns KML representation of the geometry field in a `kml`
|
||||||
raise TypeError('KML output only available on GeometryFields.')
|
attribute on each element of this GeoQuerySet.
|
||||||
|
"""
|
||||||
|
s = {'desc' : 'KML',
|
||||||
|
'procedure_fmt' : '%(geo_col)s,%(precision)s',
|
||||||
|
'procedure_args' : {'precision' : kwargs.pop('precision', 8)},
|
||||||
|
}
|
||||||
|
return self._spatial_attribute('kml', s, **kwargs)
|
||||||
|
|
||||||
geo_col = self.query._field_column(geo_field)
|
def length(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns the length of the geometry field as a `Distance` object
|
||||||
|
stored in a `length` attribute on each element of this GeoQuerySet.
|
||||||
|
"""
|
||||||
|
return self._distance_attribute('length', None, **kwargs)
|
||||||
|
|
||||||
# Adding the AsKML function call to SELECT part of the SQL.
|
def make_line(self, **kwargs):
|
||||||
return self.extra(select={'kml':'%s(%s,%s)' % (ASKML, geo_col, precision)})
|
"""
|
||||||
|
Creates a linestring from all of the PointField geometries in the
|
||||||
|
this GeoQuerySet and returns it. This is a spatial aggregate
|
||||||
|
method, and thus returns a geometry rather than a GeoQuerySet.
|
||||||
|
"""
|
||||||
|
kwargs['geo_field_type'] = PointField
|
||||||
|
kwargs['agg_field'] = GeometryField
|
||||||
|
return self._spatial_aggregate('make_line', **kwargs)
|
||||||
|
|
||||||
def transform(self, field_name=None, srid=4326):
|
def mem_size(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns the memory size (number of bytes) that the geometry field takes
|
||||||
|
in a `mem_size` attribute on each element of this GeoQuerySet.
|
||||||
|
"""
|
||||||
|
return self._spatial_attribute('mem_size', {}, **kwargs)
|
||||||
|
|
||||||
|
def num_geom(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns the number of geometries if the field is a
|
||||||
|
GeometryCollection or Multi* Field in a `num_geom`
|
||||||
|
attribute on each element of this GeoQuerySet; otherwise
|
||||||
|
the sets with None.
|
||||||
|
"""
|
||||||
|
return self._spatial_attribute('num_geom', {}, **kwargs)
|
||||||
|
|
||||||
|
def num_points(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns the number of points in the first linestring in the
|
||||||
|
Geometry field in a `num_points` attribute on each element of
|
||||||
|
this GeoQuerySet; otherwise sets with None.
|
||||||
|
"""
|
||||||
|
return self._spatial_attribute('num_points', {}, **kwargs)
|
||||||
|
|
||||||
|
def perimeter(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns the perimeter of the geometry field as a `Distance` object
|
||||||
|
stored in a `perimeter` attribute on each element of this GeoQuerySet.
|
||||||
|
"""
|
||||||
|
return self._distance_attribute('perimeter', None, **kwargs)
|
||||||
|
|
||||||
|
def point_on_surface(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns a Point geometry guaranteed to lie on the surface of the
|
||||||
|
Geometry field in a `point_on_surface` attribute on each element
|
||||||
|
of this GeoQuerySet; otherwise sets with None.
|
||||||
|
"""
|
||||||
|
return self._geom_attribute('point_on_surface', **kwargs)
|
||||||
|
|
||||||
|
def scale(self, x, y, z=0.0, **kwargs):
|
||||||
|
"""
|
||||||
|
Scales the geometry to a new size by multiplying the ordinates
|
||||||
|
with the given x,y,z scale factors.
|
||||||
|
"""
|
||||||
|
s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s,%(z)s',
|
||||||
|
'procedure_args' : {'x' : x, 'y' : y, 'z' : z},
|
||||||
|
'select_field' : GeomField(),
|
||||||
|
}
|
||||||
|
return self._spatial_attribute('scale', s, **kwargs)
|
||||||
|
|
||||||
|
def svg(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns SVG representation of the geographic field in a `svg`
|
||||||
|
attribute on each element of this GeoQuerySet.
|
||||||
|
"""
|
||||||
|
s = {'desc' : 'SVG',
|
||||||
|
'procedure_fmt' : '%(geo_col)s,%(rel)s,%(precision)s',
|
||||||
|
'procedure_args' : {'rel' : int(kwargs.pop('relative', 0)),
|
||||||
|
'precision' : kwargs.pop('precision', 8)},
|
||||||
|
}
|
||||||
|
return self._spatial_attribute('svg', s, **kwargs)
|
||||||
|
|
||||||
|
def sym_difference(self, geom, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns the symmetric difference of the geographic field in a
|
||||||
|
`sym_difference` attribute on each element of this GeoQuerySet.
|
||||||
|
"""
|
||||||
|
return self._geomset_attribute('sym_difference', geom, **kwargs)
|
||||||
|
|
||||||
|
def translate(self, x, y, z=0.0, **kwargs):
|
||||||
|
"""
|
||||||
|
Translates the geometry to a new location using the given numeric
|
||||||
|
parameters as offsets.
|
||||||
|
"""
|
||||||
|
s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s,%(z)s',
|
||||||
|
'procedure_args' : {'x' : x, 'y' : y, 'z' : z},
|
||||||
|
'select_field' : GeomField(),
|
||||||
|
}
|
||||||
|
return self._spatial_attribute('translate', s, **kwargs)
|
||||||
|
|
||||||
|
def transform(self, srid=4326, **kwargs):
|
||||||
"""
|
"""
|
||||||
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.
|
if not isinstance(srid, (int, long)):
|
||||||
TRANSFORM = SpatialBackend.transform
|
raise TypeError('An integer SRID must be provided.')
|
||||||
if not TRANSFORM:
|
field_name = kwargs.get('field_name', None)
|
||||||
raise ImproperlyConfigured('Transform stored procedure not available.')
|
tmp, geo_field = self._spatial_setup('transform', field_name=field_name)
|
||||||
|
|
||||||
# `field_name` is first for backwards compatibility; but we want to
|
# Getting the selection SQL for the given geographic field.
|
||||||
# be able to take integer srid as first parameter.
|
|
||||||
if isinstance(field_name, (int, long)):
|
|
||||||
srid = field_name
|
|
||||||
field_name = None
|
|
||||||
|
|
||||||
# Getting the GeometryField and quoted column.
|
|
||||||
geo_field = self.query._geo_field(field_name)
|
|
||||||
if not geo_field:
|
|
||||||
raise TypeError('%s() only available for GeometryFields' % TRANSFORM)
|
|
||||||
|
|
||||||
# Getting the selection SQL for the given geograph
|
|
||||||
field_col = self._geocol_select(geo_field, field_name)
|
field_col = self._geocol_select(geo_field, field_name)
|
||||||
|
|
||||||
# Why cascading substitutions? Because spatial backends like
|
# Why cascading substitutions? Because spatial backends like
|
||||||
@ -206,63 +268,334 @@ class GeoQuerySet(QuerySet):
|
|||||||
|
|
||||||
# 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.
|
||||||
if SpatialBackend.oracle:
|
custom_sel = '%s(%s, %s)' % (SpatialBackend.transform, geo_col, srid)
|
||||||
custom_sel = '%s(%s, %s)' % (TRANSFORM, geo_col, srid)
|
# TODO: Should we have this as an alias?
|
||||||
self.query.ewkt = srid
|
# custom_sel = '(%s(%s, %s)) AS %s' % (SpatialBackend.transform, geo_col, srid, qn(geo_field.name))
|
||||||
else:
|
self.query.transformed_srid = srid # So other GeoQuerySet methods
|
||||||
custom_sel = '%s(%s, %s)' % (TRANSFORM, geo_col, srid)
|
|
||||||
self.query.custom_select[geo_field] = custom_sel
|
self.query.custom_select[geo_field] = custom_sel
|
||||||
return self._clone()
|
return self._clone()
|
||||||
|
|
||||||
def union(self, field_name=None, tolerance=0.0005):
|
def union(self, geom, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns the union of the geographic field with the given
|
||||||
|
Geometry in a `union` attribute on each element of this GeoQuerySet.
|
||||||
|
"""
|
||||||
|
return self._geomset_attribute('union', geom, **kwargs)
|
||||||
|
|
||||||
|
def unionagg(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Performs an aggregate union on the given geometry field. Returns
|
Performs an aggregate union on the given geometry field. Returns
|
||||||
None if the GeoQuerySet is empty. The `tolerance` keyword is for
|
None if the GeoQuerySet is empty. The `tolerance` keyword is for
|
||||||
Oracle backends only.
|
Oracle backends only.
|
||||||
"""
|
"""
|
||||||
# Making sure backend supports the Union stored procedure
|
kwargs['agg_field'] = GeometryField
|
||||||
UNION = SpatialBackend.union
|
return self._spatial_aggregate('unionagg', **kwargs)
|
||||||
if not UNION:
|
|
||||||
raise ImproperlyConfigured('Union stored procedure not available.')
|
|
||||||
|
|
||||||
# Getting the GeometryField and quoted column.
|
### Private API -- Abstracted DRY routines. ###
|
||||||
|
def _spatial_setup(self, att, aggregate=False, desc=None, field_name=None, geo_field_type=None):
|
||||||
|
"""
|
||||||
|
Performs set up for executing the spatial function.
|
||||||
|
"""
|
||||||
|
# Does the spatial backend support this?
|
||||||
|
func = getattr(SpatialBackend, att, False)
|
||||||
|
if desc is None: desc = att
|
||||||
|
if not func: raise ImproperlyConfigured('%s stored procedure not available.' % desc)
|
||||||
|
|
||||||
|
# Initializing the procedure arguments.
|
||||||
|
procedure_args = {'function' : func}
|
||||||
|
|
||||||
|
# Is there a geographic field in the model to perform this
|
||||||
|
# operation on?
|
||||||
geo_field = self.query._geo_field(field_name)
|
geo_field = self.query._geo_field(field_name)
|
||||||
if not geo_field:
|
if not geo_field:
|
||||||
raise TypeError('Aggregate Union only available on GeometryFields.')
|
raise TypeError('%s output only available on GeometryFields.' % func)
|
||||||
geo_col = self.query._field_column(geo_field)
|
|
||||||
|
# If the `geo_field_type` keyword was used, then enforce that
|
||||||
|
# type limitation.
|
||||||
|
if not geo_field_type is None and not isinstance(geo_field, geo_field_type):
|
||||||
|
raise TypeError('"%s" stored procedures may only be called on %ss.' % (func, geo_field_type.__name__))
|
||||||
|
|
||||||
|
# Setting the procedure args.
|
||||||
|
procedure_args['geo_col'] = self._geocol_select(geo_field, field_name, aggregate)
|
||||||
|
|
||||||
|
return procedure_args, geo_field
|
||||||
|
|
||||||
|
def _spatial_aggregate(self, att, field_name=None,
|
||||||
|
agg_field=None, convert_func=None,
|
||||||
|
geo_field_type=None, tolerance=0.0005):
|
||||||
|
"""
|
||||||
|
DRY routine for calling aggregate spatial stored procedures and
|
||||||
|
returning their result to the caller of the function.
|
||||||
|
"""
|
||||||
|
# Constructing the setup keyword arguments.
|
||||||
|
setup_kwargs = {'aggregate' : True,
|
||||||
|
'field_name' : field_name,
|
||||||
|
'geo_field_type' : geo_field_type,
|
||||||
|
}
|
||||||
|
procedure_args, geo_field = self._spatial_setup(att, **setup_kwargs)
|
||||||
|
|
||||||
# Replacing the select with a call to the ST_Union stored procedure
|
|
||||||
# on the geographic field column.
|
|
||||||
if SpatialBackend.oracle:
|
if SpatialBackend.oracle:
|
||||||
union_sql = '%s' % SpatialBackend.select
|
procedure_args['tolerance'] = tolerance
|
||||||
union_sql = union_sql % ('%s(SDOAGGRTYPE(%s,%s))' % (UNION, geo_col, tolerance))
|
# Adding in selection SQL for Oracle geometry columns.
|
||||||
|
if agg_field is GeometryField:
|
||||||
|
agg_sql = '%s' % SpatialBackend.select
|
||||||
else:
|
else:
|
||||||
union_sql = '%s(%s)' % (UNION, geo_col)
|
agg_sql = '%s'
|
||||||
|
agg_sql = agg_sql % ('%(function)s(SDOAGGRTYPE(%(geo_col)s,%(tolerance)s))' % procedure_args)
|
||||||
|
else:
|
||||||
|
agg_sql = '%(function)s(%(geo_col)s)' % procedure_args
|
||||||
|
|
||||||
|
# Wrapping our selection SQL in `GeomSQL` to bypass quoting, and
|
||||||
|
# specifying the type of the aggregate field.
|
||||||
|
self.query.select = [GeomSQL(agg_sql)]
|
||||||
|
self.query.select_fields = [agg_field]
|
||||||
|
|
||||||
# Only want the union SQL to be selected.
|
|
||||||
self.query.select = [GeomSQL(union_sql)]
|
|
||||||
self.query.select_fields = [GeometryField]
|
|
||||||
try:
|
try:
|
||||||
usql, params = self.query.as_sql()
|
# `asql` => not overriding `sql` module.
|
||||||
|
asql, params = self.query.as_sql()
|
||||||
except sql.datastructures.EmptyResultSet:
|
except sql.datastructures.EmptyResultSet:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Getting a cursor, executing the query.
|
# Getting a cursor, executing the query, and extracting the returned
|
||||||
|
# value from the aggregate function.
|
||||||
cursor = connection.cursor()
|
cursor = connection.cursor()
|
||||||
cursor.execute(usql, params)
|
cursor.execute(asql, params)
|
||||||
if SpatialBackend.oracle:
|
result = cursor.fetchone()[0]
|
||||||
# On Oracle have to read out WKT from CLOB first.
|
|
||||||
clob = cursor.fetchone()[0]
|
|
||||||
if clob: u = clob.read()
|
|
||||||
else: u = None
|
|
||||||
else:
|
|
||||||
u = cursor.fetchone()[0]
|
|
||||||
|
|
||||||
if u: return GEOSGeometry(u)
|
# If the `agg_field` is specified as a GeometryField, then autmatically
|
||||||
|
# set up the conversion function.
|
||||||
|
if agg_field is GeometryField and not callable(convert_func):
|
||||||
|
if SpatialBackend.postgis:
|
||||||
|
def convert_geom(hex, geo_field):
|
||||||
|
if hex: return SpatialBackend.Geometry(hex)
|
||||||
else: return None
|
else: return None
|
||||||
|
elif SpatialBackend.oracle:
|
||||||
|
def convert_geom(clob, geo_field):
|
||||||
|
if clob: return SpatialBackend.Geometry(clob.read(), geo_field._srid)
|
||||||
|
else: return None
|
||||||
|
convert_func = convert_geom
|
||||||
|
|
||||||
# Private API utilities, subject to change.
|
# Returning the callback function evaluated on the result culled
|
||||||
def _geocol_select(self, geo_field, field_name):
|
# from the executed cursor.
|
||||||
|
if callable(convert_func):
|
||||||
|
return convert_func(result, geo_field)
|
||||||
|
else:
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _spatial_attribute(self, att, settings, field_name=None, model_att=None):
|
||||||
|
"""
|
||||||
|
DRY routine for calling a spatial stored procedure on a geometry column
|
||||||
|
and attaching its output as an attribute of the model.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
att:
|
||||||
|
The name of the spatial attribute that holds the spatial
|
||||||
|
SQL function to call.
|
||||||
|
|
||||||
|
settings:
|
||||||
|
Dictonary of internal settings to customize for the spatial procedure.
|
||||||
|
|
||||||
|
Public Keyword Arguments:
|
||||||
|
|
||||||
|
field_name:
|
||||||
|
The name of the geographic field to call the spatial
|
||||||
|
function on. May also be a lookup to a geometry field
|
||||||
|
as part of a foreign key relation.
|
||||||
|
|
||||||
|
model_att:
|
||||||
|
The name of the model attribute to attach the output of
|
||||||
|
the spatial function to.
|
||||||
|
"""
|
||||||
|
# Default settings.
|
||||||
|
settings.setdefault('desc', None)
|
||||||
|
settings.setdefault('geom_args', ())
|
||||||
|
settings.setdefault('geom_field', None)
|
||||||
|
settings.setdefault('procedure_args', {})
|
||||||
|
settings.setdefault('procedure_fmt', '%(geo_col)s')
|
||||||
|
settings.setdefault('select_params', [])
|
||||||
|
|
||||||
|
# Performing setup for the spatial column, unless told not to.
|
||||||
|
if settings.get('setup', True):
|
||||||
|
default_args, geo_field = self._spatial_setup(att, desc=settings['desc'], field_name=field_name)
|
||||||
|
for k, v in default_args.iteritems(): settings['procedure_args'].setdefault(k, v)
|
||||||
|
else:
|
||||||
|
geo_field = settings['geo_field']
|
||||||
|
|
||||||
|
# The attribute to attach to the model.
|
||||||
|
if not isinstance(model_att, basestring): model_att = att
|
||||||
|
|
||||||
|
# Special handling for any argument that is a geometry.
|
||||||
|
for name in settings['geom_args']:
|
||||||
|
# Using the field's get_db_prep_lookup() to get any needed
|
||||||
|
# transformation SQL -- we pass in a 'dummy' `contains` lookup.
|
||||||
|
where, params = geo_field.get_db_prep_lookup('contains', settings['procedure_args'][name])
|
||||||
|
# Replacing the procedure format with that of any needed
|
||||||
|
# transformation SQL.
|
||||||
|
old_fmt = '%%(%s)s' % name
|
||||||
|
new_fmt = where[0] % '%%s'
|
||||||
|
settings['procedure_fmt'] = settings['procedure_fmt'].replace(old_fmt, new_fmt)
|
||||||
|
settings['select_params'].extend(params)
|
||||||
|
|
||||||
|
# Getting the format for the stored procedure.
|
||||||
|
fmt = '%%(function)s(%s)' % settings['procedure_fmt']
|
||||||
|
|
||||||
|
# If the result of this function needs to be converted.
|
||||||
|
if settings.get('select_field', False):
|
||||||
|
sel_fld = settings['select_field']
|
||||||
|
if isinstance(sel_fld, GeomField) and SpatialBackend.select:
|
||||||
|
self.query.custom_select[model_att] = SpatialBackend.select
|
||||||
|
self.query.extra_select_fields[model_att] = sel_fld
|
||||||
|
|
||||||
|
# Finally, setting the extra selection attribute with
|
||||||
|
# the format string expanded with the stored procedure
|
||||||
|
# arguments.
|
||||||
|
return self.extra(select={model_att : fmt % settings['procedure_args']},
|
||||||
|
select_params=settings['select_params'])
|
||||||
|
|
||||||
|
def _distance_attribute(self, func, geom=None, tolerance=0.05, spheroid=False, **kwargs):
|
||||||
|
"""
|
||||||
|
DRY routine for GeoQuerySet distance attribute routines.
|
||||||
|
"""
|
||||||
|
# Setting up the distance procedure arguments.
|
||||||
|
procedure_args, geo_field = self._spatial_setup(func, field_name=kwargs.get('field_name', None))
|
||||||
|
|
||||||
|
# If geodetic defaulting distance attribute to meters (Oracle and
|
||||||
|
# PostGIS spherical distances return meters). Otherwise, use the
|
||||||
|
# units of the geometry field.
|
||||||
|
if geo_field.geodetic:
|
||||||
|
dist_att = 'm'
|
||||||
|
else:
|
||||||
|
dist_att = Distance.unit_attname(geo_field._unit_name)
|
||||||
|
|
||||||
|
# Shortcut booleans for what distance function we're using.
|
||||||
|
distance = func == 'distance'
|
||||||
|
length = func == 'length'
|
||||||
|
perimeter = func == 'perimeter'
|
||||||
|
if not (distance or length or perimeter):
|
||||||
|
raise ValueError('Unknown distance function: %s' % func)
|
||||||
|
|
||||||
|
# The field's get_db_prep_lookup() is used to get any
|
||||||
|
# extra distance parameters. Here we set up the
|
||||||
|
# parameters that will be passed in to field's function.
|
||||||
|
lookup_params = [geom or 'POINT (0 0)', 0]
|
||||||
|
|
||||||
|
# If the spheroid calculation is desired, either by the `spheroid`
|
||||||
|
# keyword or wehn calculating the length of geodetic field, make
|
||||||
|
# sure the 'spheroid' distance setting string is passed in so we
|
||||||
|
# get the correct spatial stored procedure.
|
||||||
|
if spheroid or (SpatialBackend.postgis and geo_field.geodetic and length):
|
||||||
|
lookup_params.append('spheroid')
|
||||||
|
where, params = geo_field.get_db_prep_lookup('distance_lte', lookup_params)
|
||||||
|
|
||||||
|
# The `geom_args` flag is set to true if a geometry parameter was
|
||||||
|
# passed in.
|
||||||
|
geom_args = bool(geom)
|
||||||
|
|
||||||
|
if SpatialBackend.oracle:
|
||||||
|
if distance:
|
||||||
|
procedure_fmt = '%(geo_col)s,%(geom)s,%(tolerance)s'
|
||||||
|
elif length or perimeter:
|
||||||
|
procedure_fmt = '%(geo_col)s,%(tolerance)s'
|
||||||
|
procedure_args['tolerance'] = tolerance
|
||||||
|
else:
|
||||||
|
# Getting whether this field is in units of degrees since the field may have
|
||||||
|
# been transformed via the `transform` GeoQuerySet method.
|
||||||
|
if self.query.transformed_srid:
|
||||||
|
u, unit_name, s = get_srid_info(self.query.transformed_srid)
|
||||||
|
geodetic = unit_name in geo_field.geodetic_units
|
||||||
|
else:
|
||||||
|
geodetic = geo_field.geodetic
|
||||||
|
|
||||||
|
if distance:
|
||||||
|
if self.query.transformed_srid:
|
||||||
|
# Setting the `geom_args` flag to false because we want to handle
|
||||||
|
# transformation SQL here, rather than the way done by default
|
||||||
|
# (which will transform to the original SRID of the field rather
|
||||||
|
# than to what was transformed to).
|
||||||
|
geom_args = False
|
||||||
|
procedure_fmt = '%s(%%(geo_col)s, %s)' % (SpatialBackend.transform, self.query.transformed_srid)
|
||||||
|
if geom.srid is None or geom.srid == self.query.transformed_srid:
|
||||||
|
# If the geom parameter srid is None, it is assumed the coordinates
|
||||||
|
# are in the transformed units. A placeholder is used for the
|
||||||
|
# geometry parameter.
|
||||||
|
procedure_fmt += ', %%s'
|
||||||
|
else:
|
||||||
|
# We need to transform the geom to the srid specified in `transform()`,
|
||||||
|
# so wrapping the geometry placeholder in transformation SQL.
|
||||||
|
procedure_fmt += ', %s(%%%%s, %s)' % (SpatialBackend.transform, self.query.transformed_srid)
|
||||||
|
else:
|
||||||
|
# `transform()` was not used on this GeoQuerySet.
|
||||||
|
procedure_fmt = '%(geo_col)s,%(geom)s'
|
||||||
|
|
||||||
|
if geodetic:
|
||||||
|
# Spherical distance calculation is needed (because the geographic
|
||||||
|
# field is geodetic). However, the PostGIS ST_distance_sphere/spheroid()
|
||||||
|
# procedures may only do queries from point columns to point geometries
|
||||||
|
# some error checking is required.
|
||||||
|
if not isinstance(geo_field, PointField):
|
||||||
|
raise TypeError('Spherical distance calculation only supported on PointFields.')
|
||||||
|
if not str(SpatialBackend.Geometry(buffer(params[0].wkb)).geom_type) == 'Point':
|
||||||
|
raise TypeError('Spherical distance calculation only supported with Point Geometry parameters')
|
||||||
|
# The `function` procedure argument needs to be set differently for
|
||||||
|
# geodetic distance calculations.
|
||||||
|
if spheroid:
|
||||||
|
# Call to distance_spheroid() requires spheroid param as well.
|
||||||
|
procedure_fmt += ',%(spheroid)s'
|
||||||
|
procedure_args.update({'function' : SpatialBackend.distance_spheroid, 'spheroid' : where[1]})
|
||||||
|
else:
|
||||||
|
procedure_args.update({'function' : SpatialBackend.distance_sphere})
|
||||||
|
elif length or perimeter:
|
||||||
|
procedure_fmt = '%(geo_col)s'
|
||||||
|
if geodetic and length:
|
||||||
|
# There's no `length_sphere`
|
||||||
|
procedure_fmt += ',%(spheroid)s'
|
||||||
|
procedure_args.update({'function' : SpatialBackend.length_spheroid, 'spheroid' : where[1]})
|
||||||
|
|
||||||
|
# Setting up the settings for `_spatial_attribute`.
|
||||||
|
s = {'select_field' : DistanceField(dist_att),
|
||||||
|
'setup' : False,
|
||||||
|
'geo_field' : geo_field,
|
||||||
|
'procedure_args' : procedure_args,
|
||||||
|
'procedure_fmt' : procedure_fmt,
|
||||||
|
}
|
||||||
|
if geom_args:
|
||||||
|
s['geom_args'] = ('geom',)
|
||||||
|
s['procedure_args']['geom'] = geom
|
||||||
|
elif geom:
|
||||||
|
# The geometry is passed in as a parameter because we handled
|
||||||
|
# transformation conditions in this routine.
|
||||||
|
s['select_params'] = [SpatialBackend.Adaptor(geom)]
|
||||||
|
return self._spatial_attribute(func, s, **kwargs)
|
||||||
|
|
||||||
|
def _geom_attribute(self, func, tolerance=0.05, **kwargs):
|
||||||
|
"""
|
||||||
|
DRY routine for setting up a GeoQuerySet method that attaches a
|
||||||
|
Geometry attribute (e.g., `centroid`, `point_on_surface`).
|
||||||
|
"""
|
||||||
|
s = {'select_field' : GeomField(),}
|
||||||
|
if SpatialBackend.oracle:
|
||||||
|
s['procedure_fmt'] = '%(geo_col)s,%(tolerance)s'
|
||||||
|
s['procedure_args'] = {'tolerance' : tolerance}
|
||||||
|
return self._spatial_attribute(func, s, **kwargs)
|
||||||
|
|
||||||
|
def _geomset_attribute(self, func, geom, tolerance=0.05, **kwargs):
|
||||||
|
"""
|
||||||
|
DRY routine for setting up a GeoQuerySet method that attaches a
|
||||||
|
Geometry attribute and takes a Geoemtry parameter. This is used
|
||||||
|
for geometry set-like operations (e.g., intersection, difference,
|
||||||
|
union, sym_difference).
|
||||||
|
"""
|
||||||
|
s = {'geom_args' : ('geom',),
|
||||||
|
'select_field' : GeomField(),
|
||||||
|
'procedure_fmt' : '%(geo_col)s,%(geom)s',
|
||||||
|
'procedure_args' : {'geom' : geom},
|
||||||
|
}
|
||||||
|
if SpatialBackend.oracle:
|
||||||
|
s['procedure_fmt'] += ',%(tolerance)s'
|
||||||
|
s['procedure_args']['tolerance'] = tolerance
|
||||||
|
return self._spatial_attribute(func, s, **kwargs)
|
||||||
|
|
||||||
|
def _geocol_select(self, geo_field, field_name, aggregate=False):
|
||||||
"""
|
"""
|
||||||
Helper routine for constructing the SQL to select the geographic
|
Helper routine for constructing the SQL to select the geographic
|
||||||
column. Takes into account if the geographic field is in a
|
column. Takes into account if the geographic field is in a
|
||||||
@ -274,6 +607,8 @@ class GeoQuerySet(QuerySet):
|
|||||||
# (e.g., if 'location__point' was given as the field name).
|
# (e.g., if 'location__point' was given as the field name).
|
||||||
self.query.add_select_related([field_name])
|
self.query.add_select_related([field_name])
|
||||||
self.query.pre_sql_setup()
|
self.query.pre_sql_setup()
|
||||||
|
# Can't non-aggregate and aggregate selections together.
|
||||||
|
if aggregate: self.query.aggregate = True
|
||||||
rel_table, rel_col = self.query.related_select_cols[self.query.related_select_fields.index(geo_field)]
|
rel_table, rel_col = self.query.related_select_cols[self.query.related_select_fields.index(geo_field)]
|
||||||
return self.query._field_column(geo_field, rel_table)
|
return self.query._field_column(geo_field, rel_table)
|
||||||
else:
|
else:
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
from django.contrib.gis.db.models.sql.query import GeoQuery
|
from django.contrib.gis.db.models.sql.query import AreaField, DistanceField, GeomField, GeoQuery
|
||||||
from django.contrib.gis.db.models.sql.where import GeoWhereNode
|
from django.contrib.gis.db.models.sql.where import GeoWhereNode
|
||||||
|
@ -6,6 +6,7 @@ from django.db.models.fields.related import ForeignKey
|
|||||||
from django.contrib.gis.db.backend import SpatialBackend
|
from django.contrib.gis.db.backend import SpatialBackend
|
||||||
from django.contrib.gis.db.models.fields import GeometryField
|
from django.contrib.gis.db.models.fields import GeometryField
|
||||||
from django.contrib.gis.db.models.sql.where import GeoWhereNode
|
from django.contrib.gis.db.models.sql.where import GeoWhereNode
|
||||||
|
from django.contrib.gis.measure import Area, Distance
|
||||||
|
|
||||||
# Valid GIS query types.
|
# Valid GIS query types.
|
||||||
ALL_TERMS = sql.constants.QUERY_TERMS.copy()
|
ALL_TERMS = sql.constants.QUERY_TERMS.copy()
|
||||||
@ -24,14 +25,19 @@ class GeoQuery(sql.Query):
|
|||||||
# The following attributes are customized for the GeoQuerySet.
|
# The following attributes are customized for the GeoQuerySet.
|
||||||
# The GeoWhereNode and SpatialBackend classes contain backend-specific
|
# The GeoWhereNode and SpatialBackend classes contain backend-specific
|
||||||
# routines and functions.
|
# routines and functions.
|
||||||
|
self.aggregate = False
|
||||||
self.custom_select = {}
|
self.custom_select = {}
|
||||||
self.ewkt = None
|
self.transformed_srid = None
|
||||||
|
self.extra_select_fields = {}
|
||||||
|
|
||||||
def clone(self, *args, **kwargs):
|
def clone(self, *args, **kwargs):
|
||||||
obj = super(GeoQuery, self).clone(*args, **kwargs)
|
obj = super(GeoQuery, self).clone(*args, **kwargs)
|
||||||
# Customized selection dictionary and EWKT flag have to be added to obj.
|
# Customized selection dictionary and transformed srid flag have
|
||||||
|
# to also be added to obj.
|
||||||
|
obj.aggregate = self.aggregate
|
||||||
obj.custom_select = self.custom_select.copy()
|
obj.custom_select = self.custom_select.copy()
|
||||||
obj.ewkt = self.ewkt
|
obj.transformed_srid = self.transformed_srid
|
||||||
|
obj.extra_select_fields = self.extra_select_fields.copy()
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
def get_columns(self, with_aliases=False):
|
def get_columns(self, with_aliases=False):
|
||||||
@ -49,7 +55,8 @@ class GeoQuery(sql.Query):
|
|||||||
"""
|
"""
|
||||||
qn = self.quote_name_unless_alias
|
qn = self.quote_name_unless_alias
|
||||||
qn2 = self.connection.ops.quote_name
|
qn2 = self.connection.ops.quote_name
|
||||||
result = ['(%s) AS %s' % (col, qn2(alias)) for alias, col in self.extra_select.iteritems()]
|
result = ['(%s) AS %s' % (self.get_extra_select_format(alias) % col, qn2(alias))
|
||||||
|
for alias, col in self.extra_select.iteritems()]
|
||||||
aliases = set(self.extra_select.keys())
|
aliases = set(self.extra_select.keys())
|
||||||
if with_aliases:
|
if with_aliases:
|
||||||
col_aliases = aliases.copy()
|
col_aliases = aliases.copy()
|
||||||
@ -80,6 +87,7 @@ class GeoQuery(sql.Query):
|
|||||||
result.extend(cols)
|
result.extend(cols)
|
||||||
aliases.update(new_aliases)
|
aliases.update(new_aliases)
|
||||||
# This loop customized for GeoQuery.
|
# This loop customized for GeoQuery.
|
||||||
|
if not self.aggregate:
|
||||||
for (table, col), field in izip(self.related_select_cols, self.related_select_fields):
|
for (table, col), field in izip(self.related_select_cols, self.related_select_fields):
|
||||||
r = self.get_field_select(field, table)
|
r = self.get_field_select(field, table)
|
||||||
if with_aliases and col in col_aliases:
|
if with_aliases and col in col_aliases:
|
||||||
@ -139,7 +147,51 @@ class GeoQuery(sql.Query):
|
|||||||
col_aliases.add(field.column)
|
col_aliases.add(field.column)
|
||||||
return result, aliases
|
return result, aliases
|
||||||
|
|
||||||
|
def resolve_columns(self, row, fields=()):
|
||||||
|
"""
|
||||||
|
This routine is necessary so that distances and geometries returned
|
||||||
|
from extra selection SQL get resolved appropriately into Python
|
||||||
|
objects.
|
||||||
|
"""
|
||||||
|
values = []
|
||||||
|
aliases = self.extra_select.keys()
|
||||||
|
index_start = len(aliases)
|
||||||
|
values = [self.convert_values(v, self.extra_select_fields.get(a, None))
|
||||||
|
for v, a in izip(row[:index_start], aliases)]
|
||||||
|
if SpatialBackend.oracle:
|
||||||
|
# This is what happens normally in Oracle's `resolve_columns`.
|
||||||
|
for value, field in izip(row[index_start:], fields):
|
||||||
|
values.append(self.convert_values(value, field))
|
||||||
|
else:
|
||||||
|
values.extend(row[index_start:])
|
||||||
|
return values
|
||||||
|
|
||||||
|
def convert_values(self, value, field):
|
||||||
|
"""
|
||||||
|
Using the same routines that Oracle does we can convert our
|
||||||
|
extra selection objects into Geometry and Distance objects.
|
||||||
|
TODO: Laziness.
|
||||||
|
"""
|
||||||
|
if SpatialBackend.oracle:
|
||||||
|
# Running through Oracle's first.
|
||||||
|
value = super(GeoQuery, self).convert_values(value, field)
|
||||||
|
if isinstance(field, DistanceField):
|
||||||
|
# Using the field's distance attribute, can instantiate
|
||||||
|
# `Distance` with the right context.
|
||||||
|
value = Distance(**{field.distance_att : value})
|
||||||
|
elif isinstance(field, AreaField):
|
||||||
|
value = Area(**{field.area_att : value})
|
||||||
|
elif isinstance(field, GeomField):
|
||||||
|
value = SpatialBackend.Geometry(value)
|
||||||
|
return value
|
||||||
|
|
||||||
#### Routines unique to GeoQuery ####
|
#### Routines unique to GeoQuery ####
|
||||||
|
def get_extra_select_format(self, alias):
|
||||||
|
sel_fmt = '%s'
|
||||||
|
if alias in self.custom_select:
|
||||||
|
sel_fmt = sel_fmt % self.custom_select[alias]
|
||||||
|
return sel_fmt
|
||||||
|
|
||||||
def get_field_select(self, fld, alias=None):
|
def get_field_select(self, fld, alias=None):
|
||||||
"""
|
"""
|
||||||
Returns the SELECT SQL string for the given field. Figures out
|
Returns the SELECT SQL string for the given field. Figures out
|
||||||
@ -173,8 +225,8 @@ class GeoQuery(sql.Query):
|
|||||||
# the SRID is prefixed to the returned WKT to ensure that the
|
# the SRID is prefixed to the returned WKT to ensure that the
|
||||||
# transformed geometries have an SRID different than that of the
|
# transformed geometries have an SRID different than that of the
|
||||||
# field -- this is only used by `transform` for Oracle backends.
|
# field -- this is only used by `transform` for Oracle backends.
|
||||||
if self.ewkt and SpatialBackend.oracle:
|
if self.transformed_srid and SpatialBackend.oracle:
|
||||||
sel_fmt = "'SRID=%d;'||%s" % (self.ewkt, sel_fmt)
|
sel_fmt = "'SRID=%d;'||%s" % (self.transformed_srid, sel_fmt)
|
||||||
else:
|
else:
|
||||||
sel_fmt = '%s'
|
sel_fmt = '%s'
|
||||||
return sel_fmt
|
return sel_fmt
|
||||||
@ -188,8 +240,8 @@ class GeoQuery(sql.Query):
|
|||||||
Related model field strings like 'address__point', may also be
|
Related model field strings like 'address__point', may also be
|
||||||
used.
|
used.
|
||||||
|
|
||||||
If a GeometryField exists according to the given name
|
If a GeometryField exists according to the given name parameter
|
||||||
parameter it will be returned, otherwise returns False.
|
it will be returned, otherwise returns False.
|
||||||
"""
|
"""
|
||||||
if isinstance(name_param, basestring):
|
if isinstance(name_param, basestring):
|
||||||
# This takes into account the situation where the name is a
|
# This takes into account the situation where the name is a
|
||||||
@ -242,3 +294,17 @@ class GeoQuery(sql.Query):
|
|||||||
# Otherwise, check by the given field name -- which may be
|
# Otherwise, check by the given field name -- which may be
|
||||||
# a lookup to a _related_ geographic field.
|
# a lookup to a _related_ geographic field.
|
||||||
return self._check_geo_field(self.model, field_name)
|
return self._check_geo_field(self.model, field_name)
|
||||||
|
|
||||||
|
### Field Classes for `convert_values` ####
|
||||||
|
class AreaField(object):
|
||||||
|
def __init__(self, area_att):
|
||||||
|
self.area_att = area_att
|
||||||
|
|
||||||
|
class DistanceField(object):
|
||||||
|
def __init__(self, distance_att):
|
||||||
|
self.distance_att = distance_att
|
||||||
|
|
||||||
|
# Rather than use GeometryField (which requires a SQL query
|
||||||
|
# upon instantiation), use this lighter weight class.
|
||||||
|
class GeomField(object):
|
||||||
|
pass
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from django.db.models.sql.where import WhereNode
|
from django.db.models.sql.where import WhereNode
|
||||||
from django.contrib.gis.db.backend import get_geo_where_clause, GIS_TERMS
|
from django.contrib.gis.db.backend import get_geo_where_clause, SpatialBackend
|
||||||
|
|
||||||
class GeoWhereNode(WhereNode):
|
class GeoWhereNode(WhereNode):
|
||||||
"""
|
"""
|
||||||
@ -9,7 +9,7 @@ class GeoWhereNode(WhereNode):
|
|||||||
def make_atom(self, child, qn):
|
def make_atom(self, child, qn):
|
||||||
table_alias, name, field, lookup_type, value = child
|
table_alias, name, field, lookup_type, value = child
|
||||||
if hasattr(field, '_geom'):
|
if hasattr(field, '_geom'):
|
||||||
if lookup_type in GIS_TERMS:
|
if lookup_type in SpatialBackend.gis_terms:
|
||||||
# Getting the geographic where clause; substitution parameters
|
# Getting the geographic where clause; substitution parameters
|
||||||
# will be populated in the GeoFieldSQL object returned by the
|
# will be populated in the GeoFieldSQL object returned by the
|
||||||
# GeometryField.
|
# GeometryField.
|
||||||
|
@ -30,14 +30,60 @@
|
|||||||
Distance and Area objects to allow for sensible and convienient calculation
|
Distance and Area objects to allow for sensible and convienient calculation
|
||||||
and conversions.
|
and conversions.
|
||||||
|
|
||||||
Author: Robert Coup
|
Author: Robert Coup, Justin Bronn
|
||||||
|
|
||||||
Inspired by GeoPy (http://exogen.case.edu/projects/geopy/)
|
Inspired by GeoPy (http://exogen.case.edu/projects/geopy/)
|
||||||
and Geoff Biggs' PhD work on dimensioned units for robotics.
|
and Geoff Biggs' PhD work on dimensioned units for robotics.
|
||||||
"""
|
"""
|
||||||
|
__all__ = ['A', 'Area', 'D', 'Distance']
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
class Distance(object):
|
class MeasureBase(object):
|
||||||
|
def default_units(self, kwargs):
|
||||||
|
"""
|
||||||
|
Return the unit value and the the default units specified
|
||||||
|
from the given keyword arguments dictionary.
|
||||||
|
"""
|
||||||
|
val = 0.0
|
||||||
|
for unit, value in kwargs.iteritems():
|
||||||
|
if unit in self.UNITS:
|
||||||
|
val += self.UNITS[unit] * value
|
||||||
|
default_unit = unit
|
||||||
|
elif unit in self.ALIAS:
|
||||||
|
u = self.ALIAS[unit]
|
||||||
|
val += self.UNITS[u] * value
|
||||||
|
default_unit = u
|
||||||
|
else:
|
||||||
|
lower = unit.lower()
|
||||||
|
if lower in self.UNITS:
|
||||||
|
val += self.UNITS[lower] * value
|
||||||
|
default_unit = lower
|
||||||
|
elif lower in self.LALIAS:
|
||||||
|
u = self.LALIAS[lower]
|
||||||
|
val += self.UNITS[u] * value
|
||||||
|
default_unit = u
|
||||||
|
else:
|
||||||
|
raise AttributeError('Unknown unit type: %s' % unit)
|
||||||
|
return val, default_unit
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def unit_attname(cls, unit_str):
|
||||||
|
"""
|
||||||
|
Retrieves the unit attribute name for the given unit string.
|
||||||
|
For example, if the given unit string is 'metre', 'm' would be returned.
|
||||||
|
An exception is raised if an attribute cannot be found.
|
||||||
|
"""
|
||||||
|
lower = unit_str.lower()
|
||||||
|
if unit_str in cls.UNITS:
|
||||||
|
return unit_str
|
||||||
|
elif lower in cls.UNITS:
|
||||||
|
return lower
|
||||||
|
elif lower in cls.LALIAS:
|
||||||
|
return cls.LALIAS[lower]
|
||||||
|
else:
|
||||||
|
raise Exception('Could not find a unit keyword associated with "%s"' % unit_str)
|
||||||
|
|
||||||
|
class Distance(MeasureBase):
|
||||||
UNITS = {
|
UNITS = {
|
||||||
'chain' : 20.1168,
|
'chain' : 20.1168,
|
||||||
'chain_benoit' : 20.116782,
|
'chain_benoit' : 20.116782,
|
||||||
@ -53,7 +99,6 @@ class Distance(object):
|
|||||||
'fathom' : 1.8288,
|
'fathom' : 1.8288,
|
||||||
'ft': 0.3048,
|
'ft': 0.3048,
|
||||||
'german_m' : 1.0000135965,
|
'german_m' : 1.0000135965,
|
||||||
'grad' : 0.0157079632679,
|
|
||||||
'gold_coast_ft' : 0.304799710181508,
|
'gold_coast_ft' : 0.304799710181508,
|
||||||
'indian_yd' : 0.914398530744,
|
'indian_yd' : 0.914398530744,
|
||||||
'in' : 0.0254,
|
'in' : 0.0254,
|
||||||
@ -92,9 +137,10 @@ class Distance(object):
|
|||||||
'British chain (Sears 1922)' : 'british_chain_sears',
|
'British chain (Sears 1922)' : 'british_chain_sears',
|
||||||
'British chain (Sears 1922 truncated)' : 'british_chain_sears_truncated',
|
'British chain (Sears 1922 truncated)' : 'british_chain_sears_truncated',
|
||||||
'British foot (Sears 1922)' : 'british_ft',
|
'British foot (Sears 1922)' : 'british_ft',
|
||||||
|
'British foot' : 'british_ft',
|
||||||
'British yard (Sears 1922)' : 'british_yd',
|
'British yard (Sears 1922)' : 'british_yd',
|
||||||
|
'British yard' : 'british_yd',
|
||||||
"Clarke's Foot" : 'clarke_ft',
|
"Clarke's Foot" : 'clarke_ft',
|
||||||
"Clarke's foot" : 'clarke_ft',
|
|
||||||
"Clarke's link" : 'clarke_link',
|
"Clarke's link" : 'clarke_link',
|
||||||
'Chain (Benoit)' : 'chain_benoit',
|
'Chain (Benoit)' : 'chain_benoit',
|
||||||
'Chain (Sears)' : 'chain_sears',
|
'Chain (Sears)' : 'chain_sears',
|
||||||
@ -111,33 +157,11 @@ class Distance(object):
|
|||||||
'Yard (Indian)' : 'indian_yd',
|
'Yard (Indian)' : 'indian_yd',
|
||||||
'Yard (Sears)' : 'sears_yd'
|
'Yard (Sears)' : 'sears_yd'
|
||||||
}
|
}
|
||||||
REV_ALIAS = dict((value, key) for key, value in ALIAS.items())
|
LALIAS = dict([(k.lower(), v) for k, v in ALIAS.items()])
|
||||||
|
|
||||||
def __init__(self, default_unit=None, **kwargs):
|
def __init__(self, default_unit=None, **kwargs):
|
||||||
# The base unit is in meters.
|
# The base unit is in meters.
|
||||||
self.m = 0.0
|
self.m, self._default_unit = self.default_units(kwargs)
|
||||||
self._default_unit = 'm'
|
|
||||||
|
|
||||||
for unit,value in kwargs.items():
|
|
||||||
if unit in self.UNITS:
|
|
||||||
self.m += self.UNITS[unit] * value
|
|
||||||
self._default_unit = unit
|
|
||||||
elif unit in self.ALIAS:
|
|
||||||
u = self.ALIAS[unit]
|
|
||||||
self.m += self.UNITS[u] * value
|
|
||||||
self._default_unit = u
|
|
||||||
else:
|
|
||||||
lower = unit.lower()
|
|
||||||
if lower in self.UNITS:
|
|
||||||
self.m += self.UNITS[lower] * value
|
|
||||||
self._default_unit = lower
|
|
||||||
elif lower in self.ALIAS:
|
|
||||||
u = self.ALIAS[lower]
|
|
||||||
self.m += self.UNITS[u] * value
|
|
||||||
self._default_unit = u
|
|
||||||
else:
|
|
||||||
raise AttributeError('Unknown unit type: %s' % unit)
|
|
||||||
|
|
||||||
if default_unit and isinstance(default_unit, str):
|
if default_unit and isinstance(default_unit, str):
|
||||||
self._default_unit = default_unit
|
self._default_unit = default_unit
|
||||||
|
|
||||||
@ -216,49 +240,15 @@ class Distance(object):
|
|||||||
def __nonzero__(self):
|
def __nonzero__(self):
|
||||||
return bool(self.m)
|
return bool(self.m)
|
||||||
|
|
||||||
@classmethod
|
class Area(MeasureBase):
|
||||||
def unit_attname(cls, unit_str):
|
# Getting the square units values and the alias dictionary.
|
||||||
"""
|
UNITS = dict([('sq_%s' % k, v ** 2) for k, v in Distance.UNITS.items()])
|
||||||
Retrieves the unit attribute name for the given unit string.
|
ALIAS = dict([(k, 'sq_%s' % v) for k, v in Distance.ALIAS.items()])
|
||||||
For example, if the given unit string is 'metre', 'm' would be returned.
|
LALIAS = dict([(k.lower(), v) for k, v in ALIAS.items()])
|
||||||
An exception is raised if an attribute cannot be found.
|
|
||||||
"""
|
|
||||||
lower = unit_str.lower()
|
|
||||||
|
|
||||||
if unit_str in cls.UNITS:
|
|
||||||
return unit_str
|
|
||||||
elif lower in cls.UNITS:
|
|
||||||
return lower
|
|
||||||
elif unit_str in cls.ALIAS:
|
|
||||||
return cls.ALIAS[unit_str]
|
|
||||||
elif lower in cls.ALIAS:
|
|
||||||
return cls.ALIAS[lower]
|
|
||||||
else:
|
|
||||||
raise Exception('Could not find a unit keyword associated with "%s"' % unit_str)
|
|
||||||
|
|
||||||
class Area(object):
|
|
||||||
# TODO: Add units from above.
|
|
||||||
UNITS = {
|
|
||||||
'sq_m': 1.0,
|
|
||||||
'sq_km': 1000000.0,
|
|
||||||
'sq_mi': 2589988.110336,
|
|
||||||
'sq_ft': 0.09290304,
|
|
||||||
'sq_yd': 0.83612736,
|
|
||||||
'sq_nm': 3429904.0,
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, default_unit=None, **kwargs):
|
def __init__(self, default_unit=None, **kwargs):
|
||||||
self.sq_m = 0.0
|
self.sq_m, self._default_unit = self.default_units(kwargs)
|
||||||
self._default_unit = 'sq_m'
|
if default_unit and isinstance(default_unit, str):
|
||||||
|
|
||||||
for unit,value in kwargs.items():
|
|
||||||
if unit in self.UNITS:
|
|
||||||
self.sq_m += self.UNITS[unit] * value
|
|
||||||
self._default_unit = unit
|
|
||||||
else:
|
|
||||||
raise AttributeError('Unknown unit type: ' + unit)
|
|
||||||
|
|
||||||
if default_unit:
|
|
||||||
self._default_unit = default_unit
|
self._default_unit = default_unit
|
||||||
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
@ -334,7 +324,6 @@ class Area(object):
|
|||||||
def __nonzero__(self):
|
def __nonzero__(self):
|
||||||
return bool(self.sq_m)
|
return bool(self.sq_m)
|
||||||
|
|
||||||
|
|
||||||
# Shortcuts
|
# Shortcuts
|
||||||
D = Distance
|
D = Distance
|
||||||
A = Area
|
A = Area
|
||||||
|
@ -209,9 +209,54 @@ class SpatialRefSysMixin(object):
|
|||||||
return unicode(self.wkt)
|
return unicode(self.wkt)
|
||||||
|
|
||||||
# The SpatialRefSys and GeometryColumns models
|
# The SpatialRefSys and GeometryColumns models
|
||||||
|
_srid_info = True
|
||||||
if settings.DATABASE_ENGINE == 'postgresql_psycopg2':
|
if settings.DATABASE_ENGINE == 'postgresql_psycopg2':
|
||||||
from django.contrib.gis.db.backend.postgis.models import GeometryColumns, SpatialRefSys
|
from django.contrib.gis.db.backend.postgis.models import GeometryColumns, SpatialRefSys
|
||||||
elif settings.DATABASE_ENGINE == 'oracle':
|
elif settings.DATABASE_ENGINE == 'oracle':
|
||||||
from django.contrib.gis.db.backend.oracle.models import GeometryColumns, SpatialRefSys
|
from django.contrib.gis.db.backend.oracle.models import GeometryColumns, SpatialRefSys
|
||||||
else:
|
else:
|
||||||
pass
|
_srid_info = False
|
||||||
|
|
||||||
|
if _srid_info:
|
||||||
|
def get_srid_info(srid):
|
||||||
|
"""
|
||||||
|
Returns the units, unit name, and spheroid WKT associated with the
|
||||||
|
given SRID from the `spatial_ref_sys` (or equivalent) spatial database
|
||||||
|
table. We use a database cursor to execute the query because this
|
||||||
|
function is used when it is not possible to use the ORM (for example,
|
||||||
|
during field initialization).
|
||||||
|
"""
|
||||||
|
from django.db import connection
|
||||||
|
# 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.
|
||||||
|
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)'
|
||||||
|
stmt = stmt % {'table' : qn(SpatialRefSys._meta.db_table),
|
||||||
|
'wkt_col' : qn(SpatialRefSys.wkt_col()),
|
||||||
|
'srid_col' : qn('srid'),
|
||||||
|
'srid' : srid,
|
||||||
|
}
|
||||||
|
cur.execute(stmt)
|
||||||
|
srs_wkt = cur.fetchone()[0]
|
||||||
|
if srs_wkt is None:
|
||||||
|
raise ValueError('Failed to find Spatial Reference System entry corresponding to SRID=%s' % srid)
|
||||||
|
|
||||||
|
# Getting metadata associated with the spatial reference system identifier.
|
||||||
|
# Specifically, getting the unit information and spheroid information
|
||||||
|
# (both required for distance queries).
|
||||||
|
unit, unit_name = SpatialRefSys.get_units(srs_wkt)
|
||||||
|
spheroid = SpatialRefSys.get_spheroid(srs_wkt)
|
||||||
|
return unit, unit_name, spheroid
|
||||||
|
else:
|
||||||
|
def get_srid_info(srid):
|
||||||
|
"""
|
||||||
|
Dummy routine for the backends that do not have the OGC required
|
||||||
|
spatial metadata tables (like MySQL).
|
||||||
|
"""
|
||||||
|
return None, None, None
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ from django.conf import settings
|
|||||||
if not settings._target: settings.configure()
|
if not settings._target: settings.configure()
|
||||||
|
|
||||||
# Tests that require use of a spatial database (e.g., creation of models)
|
# Tests that require use of a spatial database (e.g., creation of models)
|
||||||
test_models = ['geoapp', 'relatedapp']
|
test_models = ['geoapp',]
|
||||||
|
|
||||||
# Tests that do not require setting up and tearing down a spatial database.
|
# Tests that do not require setting up and tearing down a spatial database.
|
||||||
test_suite_names = [
|
test_suite_names = [
|
||||||
@ -20,11 +20,14 @@ test_suite_names = [
|
|||||||
]
|
]
|
||||||
if HAS_GDAL:
|
if HAS_GDAL:
|
||||||
if oracle:
|
if oracle:
|
||||||
# TODO: There is a problem with the `syncdb` SQL for the LayerMapping
|
# TODO: There's a problem with `select_related` and GeoQuerySet on
|
||||||
# tests on Oracle.
|
# Oracle -- e.g., GeoModel.objects.distance(geom, field_name='fk__point')
|
||||||
test_models += ['distapp']
|
# doesn't work so we don't test `relatedapp`.
|
||||||
elif postgis:
|
|
||||||
test_models += ['distapp', 'layermap']
|
test_models += ['distapp', 'layermap']
|
||||||
|
elif postgis:
|
||||||
|
test_models += ['distapp', 'layermap', 'relatedapp']
|
||||||
|
elif mysql:
|
||||||
|
test_models += ['relatedapp']
|
||||||
|
|
||||||
test_suite_names += [
|
test_suite_names += [
|
||||||
'test_gdal_driver',
|
'test_gdal_driver',
|
||||||
|
@ -11,16 +11,23 @@ au_cities = (('Wollongong', 150.902, -34.4245),
|
|||||||
('Hillsdale', 151.231341, -33.952685),
|
('Hillsdale', 151.231341, -33.952685),
|
||||||
)
|
)
|
||||||
|
|
||||||
stx_cities = (('Downtown Houston', 951640.547328, 4219369.26172),
|
stx_cities = (('Downtown Houston', -95.363151, 29.763374),
|
||||||
('West University Place', 943547.922328, 4213623.65345),
|
('West University Place', -95.448601, 29.713803),
|
||||||
('Southside Place', 944704.643307, 4212768.87617),
|
('Southside Place', -95.436920, 29.705777),
|
||||||
('Bellaire', 942595.669129, 4212686.72583),
|
('Bellaire', -95.458732, 29.705614),
|
||||||
('Pearland', 959674.616506, 4197465.6526),
|
('Pearland', -95.287303, 29.563568),
|
||||||
('Galveston', 1008151.16007, 4170027.47655),
|
('Galveston', -94.797489, 29.301336),
|
||||||
('Sealy', 874859.286808, 4219186.8641),
|
('Sealy', -96.156952, 29.780918),
|
||||||
('San Antonio', 649173.910483, 4176413.27786),
|
('San Antonio', -98.493183, 29.424170),
|
||||||
('Round Rock', 726846.03695, 4297160.99715),
|
('Saint Hedwig', -98.199820, 29.414197),
|
||||||
('Saint Hedwig', 677644.649952, 4175467.06744),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Data from U.S. Census ZCTA cartographic boundary file for Texas (`zt48_d00.shp`).
|
||||||
|
stx_zips = (('77002', 'POLYGON ((-95.365015 29.772327, -95.362415 29.772327, -95.360915 29.771827, -95.354615 29.771827, -95.351515 29.772527, -95.350915 29.765327, -95.351015 29.762436, -95.350115 29.760328, -95.347515 29.758528, -95.352315 29.753928, -95.356415 29.756328, -95.358215 29.754028, -95.360215 29.756328, -95.363415 29.757128, -95.364014 29.75638, -95.363415 29.753928, -95.360015 29.751828, -95.361815 29.749528, -95.362715 29.750028, -95.367516 29.744128, -95.369316 29.745128, -95.373916 29.744128, -95.380116 29.738028, -95.387916 29.727929, -95.388516 29.729629, -95.387916 29.732129, -95.382916 29.737428, -95.376616 29.742228, -95.372616 29.747228, -95.378601 29.750846, -95.378616 29.752028, -95.378616 29.754428, -95.376016 29.754528, -95.374616 29.759828, -95.373616 29.761128, -95.371916 29.763928, -95.372316 29.768727, -95.365884 29.76791, -95.366015 29.767127, -95.358715 29.765327, -95.358615 29.766327, -95.359115 29.767227, -95.360215 29.767027, -95.362783 29.768267, -95.365315 29.770527, -95.365015 29.772327))'),
|
||||||
|
('77005', 'POLYGON ((-95.447918 29.727275, -95.428017 29.728729, -95.421117 29.729029, -95.418617 29.727629, -95.418517 29.726429, -95.402117 29.726629, -95.402117 29.725729, -95.395316 29.725729, -95.391916 29.726229, -95.389716 29.725829, -95.396517 29.715429, -95.397517 29.715929, -95.400917 29.711429, -95.411417 29.715029, -95.418417 29.714729, -95.418317 29.70623, -95.440818 29.70593, -95.445018 29.70683, -95.446618 29.70763, -95.447418 29.71003, -95.447918 29.727275))'),
|
||||||
|
('77025', 'POLYGON ((-95.418317 29.70623, -95.414717 29.706129, -95.414617 29.70533, -95.418217 29.70533, -95.419817 29.69533, -95.419484 29.694196, -95.417166 29.690901, -95.414517 29.69433, -95.413317 29.69263, -95.412617 29.68973, -95.412817 29.68753, -95.414087 29.685055, -95.419165 29.685428, -95.421617 29.68513, -95.425717 29.67983, -95.425017 29.67923, -95.424517 29.67763, -95.427418 29.67763, -95.438018 29.664631, -95.436713 29.664411, -95.440118 29.662231, -95.439218 29.661031, -95.437718 29.660131, -95.435718 29.659731, -95.431818 29.660331, -95.441418 29.656631, -95.441318 29.656331, -95.441818 29.656131, -95.441718 29.659031, -95.441118 29.661031, -95.446718 29.656431, -95.446518 29.673431, -95.446918 29.69013, -95.447418 29.71003, -95.446618 29.70763, -95.445018 29.70683, -95.440818 29.70593, -95.418317 29.70623))'),
|
||||||
|
('77401', 'POLYGON ((-95.447918 29.727275, -95.447418 29.71003, -95.446918 29.69013, -95.454318 29.68893, -95.475819 29.68903, -95.475819 29.69113, -95.484419 29.69103, -95.484519 29.69903, -95.480419 29.70133, -95.480419 29.69833, -95.474119 29.69833, -95.474119 29.70453, -95.472719 29.71283, -95.468019 29.71293, -95.468219 29.720229, -95.464018 29.720229, -95.464118 29.724529, -95.463018 29.725929, -95.459818 29.726129, -95.459918 29.720329, -95.451418 29.720429, -95.451775 29.726303, -95.451318 29.727029, -95.447918 29.727275))'),
|
||||||
|
)
|
||||||
|
|
||||||
|
interstates = (('I-25', 'LINESTRING(-104.4780170766108 36.66698791870694, -104.4468522338495 36.79925409393386, -104.46212692626 36.9372149776075, -104.5126119783768 37.08163268820887, -104.5247764602161 37.29300499892048, -104.7084397427668 37.49150259925398, -104.8126599016282 37.69514285621863, -104.8452887035466 37.87613395659479, -104.7160169341003 38.05951763337799, -104.6165437927668 38.30432045855106, -104.6437227858174 38.53979986564737, -104.7596170387259 38.7322907594295, -104.8380078676822 38.89998460604341, -104.8501253693506 39.09980189213358, -104.8791648316464 39.24368776457503, -104.8635041274215 39.3785278162751, -104.8894471170052 39.5929228239605, -104.9721242843344 39.69528482419685, -105.0112104500356 39.7273080432394, -105.0010368577104 39.76677607811571, -104.981835619 39.81466504121967, -104.9858891550477 39.88806911250832, -104.9873548059578 39.98117234571016, -104.9766220487419 40.09796423450692, -104.9818565932953 40.36056530662884, -104.9912746373997 40.74904484447656)'),
|
||||||
|
)
|
||||||
|
@ -7,6 +7,13 @@ class SouthTexasCity(models.Model):
|
|||||||
objects = models.GeoManager()
|
objects = models.GeoManager()
|
||||||
def __unicode__(self): return self.name
|
def __unicode__(self): return self.name
|
||||||
|
|
||||||
|
class SouthTexasCityFt(models.Model):
|
||||||
|
"Same City model as above, but U.S. survey feet are the units."
|
||||||
|
name = models.CharField(max_length=30)
|
||||||
|
point = models.PointField(srid=2278)
|
||||||
|
objects = models.GeoManager()
|
||||||
|
def __unicode__(self): return self.name
|
||||||
|
|
||||||
class AustraliaCity(models.Model):
|
class AustraliaCity(models.Model):
|
||||||
"City model for Australia, using WGS84."
|
"City model for Australia, using WGS84."
|
||||||
name = models.CharField(max_length=30)
|
name = models.CharField(max_length=30)
|
||||||
@ -14,7 +21,22 @@ class AustraliaCity(models.Model):
|
|||||||
objects = models.GeoManager()
|
objects = models.GeoManager()
|
||||||
def __unicode__(self): return self.name
|
def __unicode__(self): return self.name
|
||||||
|
|
||||||
#class County(models.Model):
|
class CensusZipcode(models.Model):
|
||||||
# name = models.CharField(max_length=30)
|
"Model for a few South Texas ZIP codes (in original Census NAD83)."
|
||||||
# mpoly = models.MultiPolygonField(srid=32140)
|
name = models.CharField(max_length=5)
|
||||||
# objects = models.GeoManager()
|
poly = models.PolygonField(srid=4269)
|
||||||
|
objects = models.GeoManager()
|
||||||
|
|
||||||
|
class SouthTexasZipcode(models.Model):
|
||||||
|
"Model for a few South Texas ZIP codes."
|
||||||
|
name = models.CharField(max_length=5)
|
||||||
|
poly = models.PolygonField(srid=32140)
|
||||||
|
objects = models.GeoManager()
|
||||||
|
def __unicode__(self): return self.name
|
||||||
|
|
||||||
|
class Interstate(models.Model):
|
||||||
|
"Geodetic model for U.S. Interstates."
|
||||||
|
name = models.CharField(max_length=10)
|
||||||
|
line = models.LineStringField()
|
||||||
|
objects = models.GeoManager()
|
||||||
|
def __unicode__(self): return self.name
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
import os, unittest
|
import os, unittest
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from django.db.models import Q
|
||||||
from django.contrib.gis.gdal import DataSource
|
from django.contrib.gis.gdal import DataSource
|
||||||
from django.contrib.gis.geos import GEOSGeometry, Point, LineString
|
from django.contrib.gis.geos import GEOSGeometry, Point, LineString
|
||||||
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.db.models import GeoQ
|
||||||
from django.contrib.gis.tests.utils import oracle
|
from django.contrib.gis.tests.utils import oracle, postgis, no_oracle
|
||||||
|
|
||||||
from models import SouthTexasCity, AustraliaCity
|
from models import AustraliaCity, Interstate, SouthTexasCity, SouthTexasCityFt, CensusZipcode, SouthTexasZipcode
|
||||||
from data import au_cities, stx_cities
|
from data import au_cities, interstates, stx_cities, stx_zips
|
||||||
|
|
||||||
class DistanceTest(unittest.TestCase):
|
class DistanceTest(unittest.TestCase):
|
||||||
|
|
||||||
@ -20,7 +21,7 @@ class DistanceTest(unittest.TestCase):
|
|||||||
# Another one for Australia
|
# Another one for Australia
|
||||||
au_pnt = GEOSGeometry('POINT (150.791 -34.4919)', 4326)
|
au_pnt = GEOSGeometry('POINT (150.791 -34.4919)', 4326)
|
||||||
|
|
||||||
def get_cities(self, qs):
|
def get_names(self, qs):
|
||||||
cities = [c.name for c in qs]
|
cities = [c.name for c in qs]
|
||||||
cities.sort()
|
cities.sort()
|
||||||
return cities
|
return cities
|
||||||
@ -28,33 +29,56 @@ class DistanceTest(unittest.TestCase):
|
|||||||
def test01_init(self):
|
def test01_init(self):
|
||||||
"Initialization of distance models."
|
"Initialization of distance models."
|
||||||
|
|
||||||
def load_cities(city_model, srid, data_tup):
|
# Loading up the cities.
|
||||||
|
def load_cities(city_model, data_tup):
|
||||||
for name, x, y in data_tup:
|
for name, x, y in data_tup:
|
||||||
c = city_model(name=name, point=Point(x, y, srid=srid))
|
c = city_model(name=name, point=Point(x, y, srid=4326))
|
||||||
c.save()
|
c.save()
|
||||||
|
|
||||||
load_cities(SouthTexasCity, 32140, stx_cities)
|
load_cities(SouthTexasCity, stx_cities)
|
||||||
load_cities(AustraliaCity, 4326, au_cities)
|
load_cities(SouthTexasCityFt, stx_cities)
|
||||||
|
load_cities(AustraliaCity, au_cities)
|
||||||
|
|
||||||
self.assertEqual(10, SouthTexasCity.objects.count())
|
self.assertEqual(9, SouthTexasCity.objects.count())
|
||||||
|
self.assertEqual(9, SouthTexasCityFt.objects.count())
|
||||||
self.assertEqual(11, AustraliaCity.objects.count())
|
self.assertEqual(11, AustraliaCity.objects.count())
|
||||||
|
|
||||||
|
# Loading up the South Texas Zip Codes.
|
||||||
|
for name, wkt in stx_zips:
|
||||||
|
poly = GEOSGeometry(wkt, srid=4269)
|
||||||
|
SouthTexasZipcode(name=name, poly=poly).save()
|
||||||
|
CensusZipcode(name=name, poly=poly).save()
|
||||||
|
self.assertEqual(4, SouthTexasZipcode.objects.count())
|
||||||
|
self.assertEqual(4, CensusZipcode.objects.count())
|
||||||
|
|
||||||
|
# Loading up the Interstates.
|
||||||
|
for name, wkt in interstates:
|
||||||
|
Interstate(name=name, line=GEOSGeometry(wkt, srid=4326)).save()
|
||||||
|
self.assertEqual(1, Interstate.objects.count())
|
||||||
|
|
||||||
def test02_dwithin(self):
|
def test02_dwithin(self):
|
||||||
"Testing the `dwithin` lookup type."
|
"Testing the `dwithin` lookup type."
|
||||||
# Distances -- all should be equal (except for the
|
# Distances -- all should be equal (except for the
|
||||||
# degree/meter pair in au_cities, that's somewhat
|
# degree/meter pair in au_cities, that's somewhat
|
||||||
# approximate).
|
# approximate).
|
||||||
tx_dists = [7000, D(km=7), D(mi=4.349)]
|
tx_dists = [(7000, 22965.83), D(km=7), D(mi=4.349)]
|
||||||
au_dists = [(0.5, 32000), D(km=32), D(mi=19.884)]
|
au_dists = [(0.5, 32000), D(km=32), D(mi=19.884)]
|
||||||
|
|
||||||
# Expected cities for Australia and Texas.
|
# Expected cities for Australia and Texas.
|
||||||
tx_cities = ['Downtown Houston', 'Southside Place']
|
tx_cities = ['Downtown Houston', 'Southside Place']
|
||||||
au_cities = ['Mittagong', 'Shellharbour', 'Thirroul', 'Wollongong']
|
au_cities = ['Mittagong', 'Shellharbour', 'Thirroul', 'Wollongong']
|
||||||
|
|
||||||
|
# Performing distance queries on two projected coordinate systems one
|
||||||
|
# with units in meters and the other in units of U.S. survey feet.
|
||||||
for dist in tx_dists:
|
for dist in tx_dists:
|
||||||
qs = SouthTexasCity.objects.filter(point__dwithin=(self.stx_pnt, dist))
|
if isinstance(dist, tuple): dist1, dist2 = dist
|
||||||
self.assertEqual(tx_cities, self.get_cities(qs))
|
else: dist1 = dist2 = dist
|
||||||
|
qs1 = SouthTexasCity.objects.filter(point__dwithin=(self.stx_pnt, dist1))
|
||||||
|
qs2 = SouthTexasCityFt.objects.filter(point__dwithin=(self.stx_pnt, dist2))
|
||||||
|
for qs in qs1, qs2:
|
||||||
|
self.assertEqual(tx_cities, self.get_names(qs))
|
||||||
|
|
||||||
|
# Now performing the `dwithin` queries on a geodetic coordinate system.
|
||||||
for dist in au_dists:
|
for dist in au_dists:
|
||||||
if isinstance(dist, D) and not oracle: type_error = True
|
if isinstance(dist, D) and not oracle: type_error = True
|
||||||
else: type_error = False
|
else: type_error = False
|
||||||
@ -70,23 +94,29 @@ class DistanceTest(unittest.TestCase):
|
|||||||
# Distance objects into a DWithin query using a geodetic field.
|
# Distance objects into a DWithin query using a geodetic field.
|
||||||
self.assertRaises(TypeError, qs.count)
|
self.assertRaises(TypeError, qs.count)
|
||||||
else:
|
else:
|
||||||
self.assertEqual(au_cities, self.get_cities(qs))
|
self.assertEqual(au_cities, self.get_names(qs))
|
||||||
|
|
||||||
def test03_distance_aggregate(self):
|
def test03a_distance_method(self):
|
||||||
"Testing the `distance` GeoQuerySet method."
|
"Testing the `distance` GeoQuerySet method on projected coordinate systems."
|
||||||
# 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:
|
# Reference distances in feet and in meters. Got these values from
|
||||||
|
# using the provided raw SQL statements.
|
||||||
# SELECT ST_Distance(point, ST_Transform(ST_GeomFromText('POINT(-96.876369 29.905320)', 4326), 32140)) FROM distapp_southtexascity;
|
# 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,
|
m_distances = [147075.069813, 139630.198056, 140888.552826,
|
||||||
138809.684197, 158309.246259, 212183.594374,
|
138809.684197, 158309.246259, 212183.594374,
|
||||||
70870.188967, 165337.758878, 102128.654360,
|
70870.188967, 165337.758878, 139196.085105]
|
||||||
139196.085105]
|
# SELECT ST_Distance(point, ST_Transform(ST_GeomFromText('POINT(-96.876369 29.905320)', 4326), 2278)) FROM distapp_southtexascityft;
|
||||||
|
ft_distances = [482528.79154625, 458103.408123001, 462231.860397575,
|
||||||
|
455411.438904354, 519386.252102563, 696139.009211594,
|
||||||
|
232513.278304279, 542445.630586414, 456679.155883207]
|
||||||
|
|
||||||
# Testing when the field name is explicitly set.
|
# Testing using different variations of parameters and using models
|
||||||
dist1 = SouthTexasCity.objects.distance('point', lagrange)
|
# with different projected coordinate systems.
|
||||||
|
dist1 = SouthTexasCity.objects.distance(lagrange, field_name='point')
|
||||||
dist2 = SouthTexasCity.objects.distance(lagrange) # Using GEOSGeometry parameter
|
dist2 = SouthTexasCity.objects.distance(lagrange) # Using GEOSGeometry parameter
|
||||||
dist3 = SouthTexasCity.objects.distance(lagrange.ewkt) # Using EWKT string parameter.
|
dist3 = SouthTexasCityFt.objects.distance(lagrange.ewkt) # Using EWKT string parameter.
|
||||||
|
dist4 = SouthTexasCityFt.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.
|
||||||
@ -94,9 +124,15 @@ class DistanceTest(unittest.TestCase):
|
|||||||
else: tol = 5
|
else: tol = 5
|
||||||
|
|
||||||
# Ensuring expected distances are returned for each distance queryset.
|
# Ensuring expected distances are returned for each distance queryset.
|
||||||
for qs in [dist1, dist2, dist3]:
|
for qs in [dist1, dist2, dist3, dist4]:
|
||||||
for i, c in enumerate(qs):
|
for i, c in enumerate(qs):
|
||||||
self.assertAlmostEqual(distances[i], c.distance, tol)
|
self.assertAlmostEqual(m_distances[i], c.distance.m, tol)
|
||||||
|
self.assertAlmostEqual(ft_distances[i], c.distance.survey_ft, tol)
|
||||||
|
|
||||||
|
def test03b_distance_method(self):
|
||||||
|
"Testing the `distance` GeoQuerySet method on geodetic coordnate systems."
|
||||||
|
if oracle: tol = 2
|
||||||
|
else: tol = 5
|
||||||
|
|
||||||
# Now testing geodetic distance aggregation.
|
# Now testing geodetic distance aggregation.
|
||||||
hillsdale = AustraliaCity.objects.get(name='Hillsdale')
|
hillsdale = AustraliaCity.objects.get(name='Hillsdale')
|
||||||
@ -106,61 +142,153 @@ class DistanceTest(unittest.TestCase):
|
|||||||
self.assertRaises(TypeError, AustraliaCity.objects.distance, 'LINESTRING(0 0, 1 1)')
|
self.assertRaises(TypeError, AustraliaCity.objects.distance, 'LINESTRING(0 0, 1 1)')
|
||||||
self.assertRaises(TypeError, AustraliaCity.objects.distance, LineString((0, 0), (1, 1)))
|
self.assertRaises(TypeError, AustraliaCity.objects.distance, LineString((0, 0), (1, 1)))
|
||||||
|
|
||||||
# Got these distances using the raw SQL statement:
|
# Got the reference distances using the raw SQL statements:
|
||||||
# SELECT ST_distance_spheroid(point, ST_GeomFromText('POINT(151.231341 -33.952685)', 4326), 'SPHEROID["WGS 84",6378137.0,298.257223563]') FROM distapp_australiacity WHERE (NOT (id = 11));
|
# SELECT ST_distance_spheroid(point, ST_GeomFromText('POINT(151.231341 -33.952685)', 4326), 'SPHEROID["WGS 84",6378137.0,298.257223563]') FROM distapp_australiacity WHERE (NOT (id = 11));
|
||||||
geodetic_distances = [60504.0628825298, 77023.948962654, 49154.8867507115, 90847.435881812, 217402.811862568, 709599.234619957, 640011.483583758, 7772.00667666425, 1047861.7859506, 1165126.55237647]
|
spheroid_distances = [60504.0628825298, 77023.948962654, 49154.8867507115, 90847.435881812, 217402.811862568, 709599.234619957, 640011.483583758, 7772.00667666425, 1047861.7859506, 1165126.55237647]
|
||||||
|
# SELECT ST_distance_sphere(point, ST_GeomFromText('POINT(151.231341 -33.952685)', 4326)) FROM distapp_australiacity WHERE (NOT (id = 11)); st_distance_sphere
|
||||||
|
sphere_distances = [60580.7612632291, 77143.7785056615, 49199.2725132184, 90804.4414289463, 217712.63666124, 709131.691061906, 639825.959074112, 7786.80274606706, 1049200.46122281, 1162619.7297006]
|
||||||
|
|
||||||
# Ensuring the expected distances are returned.
|
# Testing with spheroid distances first.
|
||||||
|
qs = AustraliaCity.objects.exclude(id=hillsdale.id).distance(hillsdale.point, spheroid=True)
|
||||||
|
for i, c in enumerate(qs):
|
||||||
|
self.assertAlmostEqual(spheroid_distances[i], c.distance.m, tol)
|
||||||
|
if postgis:
|
||||||
|
# PostGIS uses sphere-only distances by default, testing these as well.
|
||||||
qs = AustraliaCity.objects.exclude(id=hillsdale.id).distance(hillsdale.point)
|
qs = AustraliaCity.objects.exclude(id=hillsdale.id).distance(hillsdale.point)
|
||||||
for i, c in enumerate(qs):
|
for i, c in enumerate(qs):
|
||||||
self.assertAlmostEqual(geodetic_distances[i], c.distance, tol)
|
self.assertAlmostEqual(sphere_distances[i], c.distance.m, tol)
|
||||||
|
|
||||||
|
@no_oracle # Oracle already handles geographic distance calculation.
|
||||||
|
def test03c_distance_method(self):
|
||||||
|
"Testing the `distance` GeoQuerySet method used with `transform` on a geographic field."
|
||||||
|
# Normally you can't compute distances from a geometry field
|
||||||
|
# that is not a PointField (on PostGIS).
|
||||||
|
self.assertRaises(TypeError, CensusZipcode.objects.distance, self.stx_pnt)
|
||||||
|
|
||||||
|
# We'll be using a Polygon (created by buffering the centroid
|
||||||
|
# of 77005 to 100m) -- which aren't allowed in geographic distance
|
||||||
|
# queries normally, however our field has been transformed to
|
||||||
|
# a non-geographic system.
|
||||||
|
z = SouthTexasZipcode.objects.get(name='77005')
|
||||||
|
|
||||||
|
# Reference query:
|
||||||
|
# SELECT ST_Distance(ST_Transform("distapp_censuszipcode"."poly", 32140), ST_GeomFromText('<buffer_wkt>', 32140)) FROM "distapp_censuszipcode";
|
||||||
|
dists_m = [3553.30384972258, 1243.18391525602, 2186.15439472242]
|
||||||
|
|
||||||
|
# Having our buffer in the SRID of the transformation and of the field
|
||||||
|
# -- should get the same results. The first buffer has no need for
|
||||||
|
# transformation SQL because it is the same SRID as what was given
|
||||||
|
# to `transform()`. The second buffer will need to be transformed,
|
||||||
|
# however.
|
||||||
|
buf1 = z.poly.centroid.buffer(100)
|
||||||
|
buf2 = buf1.transform(4269, clone=True)
|
||||||
|
for buf in [buf1, buf2]:
|
||||||
|
qs = CensusZipcode.objects.exclude(name='77005').transform(32140).distance(buf)
|
||||||
|
self.assertEqual(['77002', '77025', '77401'], self.get_names(qs))
|
||||||
|
for i, z in enumerate(qs):
|
||||||
|
self.assertAlmostEqual(z.distance.m, dists_m[i], 5)
|
||||||
|
|
||||||
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."
|
||||||
# Only two cities (Houston and Southside Place) should be
|
# Retrieving the cities within a 20km 'donut' w/a 7km radius 'hole'
|
||||||
# within 7km of the given point.
|
# (thus, Houston and Southside place will be excluded as tested in
|
||||||
dists = [D(km=7), D(mi=4.349), # Distance instances in different units.
|
# the `test02_dwithin` above).
|
||||||
7000, 7000.0, Decimal(7000), # int, float, Decimal parameters.
|
qs1 = SouthTexasCity.objects.filter(point__distance_gte=(self.stx_pnt, D(km=7))).filter(point__distance_lte=(self.stx_pnt, D(km=20)))
|
||||||
]
|
qs2 = SouthTexasCityFt.objects.filter(point__distance_gte=(self.stx_pnt, D(km=7))).filter(point__distance_lte=(self.stx_pnt, D(km=20)))
|
||||||
|
for qs in qs1, qs2:
|
||||||
for dist in dists:
|
cities = self.get_names(qs)
|
||||||
qs = SouthTexasCity.objects.filter(point__dwithin=(self.stx_pnt, dist))
|
|
||||||
for c in qs:
|
|
||||||
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 = 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'])
|
self.assertEqual(cities, ['Bellaire', 'Pearland', 'West University Place'])
|
||||||
|
|
||||||
def test05_geodetic_distance(self):
|
# Doing a distance query using Polygons instead of a Point.
|
||||||
"Testing distance lookups on geodetic coordinate systems."
|
z = SouthTexasZipcode.objects.get(name='77005')
|
||||||
|
qs = SouthTexasZipcode.objects.exclude(name='77005').filter(poly__distance_lte=(z.poly, D(m=275)))
|
||||||
|
self.assertEqual(['77025', '77401'], self.get_names(qs))
|
||||||
|
# If we add a little more distance 77002 should be included.
|
||||||
|
qs = SouthTexasZipcode.objects.exclude(name='77005').filter(poly__distance_lte=(z.poly, D(m=300)))
|
||||||
|
self.assertEqual(['77002', '77025', '77401'], self.get_names(qs))
|
||||||
|
|
||||||
|
def test05_geodetic_distance_lookups(self):
|
||||||
|
"Testing distance lookups on geodetic coordinate systems."
|
||||||
if not oracle:
|
if not oracle:
|
||||||
# Oracle doesn't have this limitation -- PostGIS only allows geodetic
|
# Oracle doesn't have this limitation -- PostGIS only allows geodetic
|
||||||
# distance queries from Points to PointFields.
|
# distance queries from Points to PointFields.
|
||||||
mp = GEOSGeometry('MULTIPOINT(0 0, 5 23)')
|
mp = GEOSGeometry('MULTIPOINT(0 0, 5 23)')
|
||||||
self.assertRaises(TypeError,
|
self.assertRaises(TypeError,
|
||||||
AustraliaCity.objects.filter(point__distance_lte=(mp, D(km=100))))
|
AustraliaCity.objects.filter(point__distance_lte=(mp, D(km=100))))
|
||||||
|
# Too many params (4 in this case) should raise a ValueError.
|
||||||
|
self.assertRaises(ValueError,
|
||||||
|
AustraliaCity.objects.filter(point__distance_lte=('POINT(5 23)', D(km=100), 'spheroid', '4')).count)
|
||||||
|
|
||||||
hobart = AustraliaCity.objects.get(name='Hobart')
|
# Not enough params should raise a ValueError.
|
||||||
|
self.assertRaises(ValueError,
|
||||||
|
AustraliaCity.objects.filter(point__distance_lte=('POINT(5 23)',)).count)
|
||||||
|
|
||||||
# Getting all cities w/in 550 miles of Hobart.
|
# Getting all cities w/in 550 miles of Hobart.
|
||||||
|
hobart = AustraliaCity.objects.get(name='Hobart')
|
||||||
qs = AustraliaCity.objects.exclude(name='Hobart').filter(point__distance_lte=(hobart.point, D(mi=550)))
|
qs = AustraliaCity.objects.exclude(name='Hobart').filter(point__distance_lte=(hobart.point, D(mi=550)))
|
||||||
cities = self.get_cities(qs)
|
cities = self.get_names(qs)
|
||||||
self.assertEqual(cities, ['Batemans Bay', 'Canberra', 'Melbourne'])
|
self.assertEqual(cities, ['Batemans Bay', 'Canberra', 'Melbourne'])
|
||||||
|
|
||||||
# Cities that are either really close or really far from Wollongong --
|
# Cities that are either really close or really far from Wollongong --
|
||||||
# and using different units of distance.
|
# and using different units of distance.
|
||||||
wollongong = AustraliaCity.objects.get(name='Wollongong')
|
wollongong = AustraliaCity.objects.get(name='Wollongong')
|
||||||
gq1 = GeoQ(point__distance_lte=(wollongong.point, D(yd=19500))) # Yards (~17km)
|
d1, d2 = D(yd=19500), D(nm=400) # Yards (~17km) & Nautical miles.
|
||||||
gq2 = GeoQ(point__distance_gte=(wollongong.point, D(nm=400))) # Nautical Miles
|
|
||||||
qs = AustraliaCity.objects.exclude(name='Wollongong').filter(gq1 | gq2)
|
# Normal geodetic distance lookup (uses `distance_sphere` on PostGIS.
|
||||||
cities = self.get_cities(qs)
|
gq1 = GeoQ(point__distance_lte=(wollongong.point, d1))
|
||||||
|
gq2 = GeoQ(point__distance_gte=(wollongong.point, d2))
|
||||||
|
qs1 = AustraliaCity.objects.exclude(name='Wollongong').filter(gq1 | gq2)
|
||||||
|
|
||||||
|
# Geodetic distance lookup but telling GeoDjango to use `distance_spheroid`
|
||||||
|
# instead (we should get the same results b/c accuracy variance won't matter
|
||||||
|
# in this test case). Using `Q` instead of `GeoQ` to be different (post-qsrf
|
||||||
|
# it doesn't matter).
|
||||||
|
if postgis:
|
||||||
|
gq3 = Q(point__distance_lte=(wollongong.point, d1, 'spheroid'))
|
||||||
|
gq4 = Q(point__distance_gte=(wollongong.point, d2, 'spheroid'))
|
||||||
|
qs2 = AustraliaCity.objects.exclude(name='Wollongong').filter(gq3 | gq4)
|
||||||
|
querysets = [qs1, qs2]
|
||||||
|
else:
|
||||||
|
querysets = [qs1]
|
||||||
|
|
||||||
|
for qs in querysets:
|
||||||
|
cities = self.get_names(qs)
|
||||||
self.assertEqual(cities, ['Adelaide', 'Hobart', 'Shellharbour', 'Thirroul'])
|
self.assertEqual(cities, ['Adelaide', 'Hobart', 'Shellharbour', 'Thirroul'])
|
||||||
|
|
||||||
|
def test06_area(self):
|
||||||
|
"Testing the `area` GeoQuerySet method."
|
||||||
|
# Reference queries:
|
||||||
|
# SELECT ST_Area(poly) FROM distapp_southtexaszipcode;
|
||||||
|
area_sq_m = [5437908.90234375, 10183031.4389648, 11254471.0073242, 9881708.91772461]
|
||||||
|
# Tolerance has to be lower for Oracle and differences
|
||||||
|
# with GEOS 3.0.0RC4
|
||||||
|
tol = 2
|
||||||
|
for i, z in enumerate(SouthTexasZipcode.objects.area()):
|
||||||
|
self.assertAlmostEqual(area_sq_m[i], z.area.sq_m, tol)
|
||||||
|
|
||||||
|
def test07_length(self):
|
||||||
|
"Testing the `length` GeoQuerySet method."
|
||||||
|
# Reference query (should use `length_spheroid`).
|
||||||
|
# SELECT ST_length_spheroid(ST_GeomFromText('<wkt>', 4326) 'SPHEROID["WGS 84",6378137,298.257223563, AUTHORITY["EPSG","7030"]]');
|
||||||
|
len_m = 473504.769553813
|
||||||
|
qs = Interstate.objects.length()
|
||||||
|
if oracle: tol = 2
|
||||||
|
else: tol = 7
|
||||||
|
self.assertAlmostEqual(len_m, qs[0].length.m, tol)
|
||||||
|
|
||||||
|
def test08_perimeter(self):
|
||||||
|
"Testing the `perimeter` GeoQuerySet method."
|
||||||
|
# Reference query:
|
||||||
|
# SELECT ST_Perimeter(distapp_southtexaszipcode.poly) FROM distapp_southtexaszipcode;
|
||||||
|
perim_m = [18404.3550889361, 15627.2108551001, 20632.5588368978, 17094.5996143697]
|
||||||
|
if oracle: tol = 2
|
||||||
|
else: tol = 7
|
||||||
|
for i, z in enumerate(SouthTexasZipcode.objects.perimeter()):
|
||||||
|
self.assertAlmostEqual(perim_m[i], z.perimeter.m, tol)
|
||||||
|
|
||||||
|
# Running on points; should return 0.
|
||||||
|
for i, c in enumerate(SouthTexasCity.objects.perimeter(model_att='perim')):
|
||||||
|
self.assertEqual(0, c.perim.m)
|
||||||
|
|
||||||
def suite():
|
def suite():
|
||||||
s = unittest.TestSuite()
|
s = unittest.TestSuite()
|
||||||
|
@ -28,14 +28,10 @@ class GeoModelTest(unittest.TestCase):
|
|||||||
def get_file(wkt_file):
|
def get_file(wkt_file):
|
||||||
return os.path.join(data_dir, wkt_file)
|
return os.path.join(data_dir, wkt_file)
|
||||||
|
|
||||||
co = State(name='Colorado', poly=fromfile(get_file('co.wkt')))
|
State(name='Colorado', poly=fromfile(get_file('co.wkt'))).save()
|
||||||
co.save()
|
State(name='Kansas', poly=fromfile(get_file('ks.wkt'))).save()
|
||||||
ks = State(name='Kansas', poly=fromfile(get_file('ks.wkt')))
|
Country(name='Texas', mpoly=fromfile(get_file('tx.wkt'))).save()
|
||||||
ks.save()
|
Country(name='New Zealand', mpoly=fromfile(get_file('nz.wkt'))).save()
|
||||||
tx = Country(name='Texas', mpoly=fromfile(get_file('tx.wkt')))
|
|
||||||
tx.save()
|
|
||||||
nz = Country(name='New Zealand', mpoly=fromfile(get_file('nz.wkt')))
|
|
||||||
nz.save()
|
|
||||||
|
|
||||||
# Ensuring that data was loaded from initial SQL.
|
# Ensuring that data was loaded from initial SQL.
|
||||||
self.assertEqual(2, Country.objects.count())
|
self.assertEqual(2, Country.objects.count())
|
||||||
@ -49,7 +45,7 @@ class GeoModelTest(unittest.TestCase):
|
|||||||
def test02_proxy(self):
|
def test02_proxy(self):
|
||||||
"Testing Lazy-Geometry support (using the GeometryProxy)."
|
"Testing Lazy-Geometry support (using the GeometryProxy)."
|
||||||
if DISABLE: return
|
if DISABLE: return
|
||||||
#### Testing on a Point
|
## Testing on a Point
|
||||||
pnt = Point(0, 0)
|
pnt = Point(0, 0)
|
||||||
nullcity = City(name='NullCity', point=pnt)
|
nullcity = City(name='NullCity', point=pnt)
|
||||||
nullcity.save()
|
nullcity.save()
|
||||||
@ -86,7 +82,7 @@ class GeoModelTest(unittest.TestCase):
|
|||||||
self.assertEqual(Point(23, 5), City.objects.get(name='NullCity').point)
|
self.assertEqual(Point(23, 5), City.objects.get(name='NullCity').point)
|
||||||
nullcity.delete()
|
nullcity.delete()
|
||||||
|
|
||||||
#### Testing on a Polygon
|
## Testing on a Polygon
|
||||||
shell = LinearRing((0, 0), (0, 100), (100, 100), (100, 0), (0, 0))
|
shell = LinearRing((0, 0), (0, 100), (100, 100), (100, 0), (0, 0))
|
||||||
inner = LinearRing((40, 40), (40, 60), (60, 60), (60, 40), (40, 40))
|
inner = LinearRing((40, 40), (40, 60), (60, 60), (60, 40), (40, 40))
|
||||||
|
|
||||||
@ -136,7 +132,7 @@ class GeoModelTest(unittest.TestCase):
|
|||||||
ref_kml = ref_kml2
|
ref_kml = ref_kml2
|
||||||
|
|
||||||
# Ensuring the KML is as expected.
|
# Ensuring the KML is as expected.
|
||||||
ptown1 = City.objects.kml('point', precision=9).get(name='Pueblo')
|
ptown1 = City.objects.kml(field_name='point', precision=9).get(name='Pueblo')
|
||||||
ptown2 = City.objects.kml(precision=9).get(name='Pueblo')
|
ptown2 = City.objects.kml(precision=9).get(name='Pueblo')
|
||||||
for ptown in [ptown1, ptown2]:
|
for ptown in [ptown1, ptown2]:
|
||||||
self.assertEqual(ref_kml, ptown.kml)
|
self.assertEqual(ref_kml, ptown.kml)
|
||||||
@ -147,8 +143,8 @@ class GeoModelTest(unittest.TestCase):
|
|||||||
# Should throw a TypeError when tyring to obtain GML from a
|
# Should throw a TypeError when tyring to obtain GML from a
|
||||||
# non-geometry field.
|
# non-geometry field.
|
||||||
qs = City.objects.all()
|
qs = City.objects.all()
|
||||||
self.assertRaises(TypeError, qs.gml, 'name')
|
self.assertRaises(TypeError, qs.gml, field_name='name')
|
||||||
ptown1 = City.objects.gml('point', precision=9).get(name='Pueblo')
|
ptown1 = City.objects.gml(field_name='point', precision=9).get(name='Pueblo')
|
||||||
ptown2 = City.objects.gml(precision=9).get(name='Pueblo')
|
ptown2 = City.objects.gml(precision=9).get(name='Pueblo')
|
||||||
|
|
||||||
if oracle:
|
if oracle:
|
||||||
@ -172,12 +168,12 @@ class GeoModelTest(unittest.TestCase):
|
|||||||
# Asserting the result of the transform operation with the values in
|
# Asserting the result of the transform operation with the values in
|
||||||
# the pre-transformed points. Oracle does not have the 3084 SRID.
|
# the pre-transformed points. Oracle does not have the 3084 SRID.
|
||||||
if not oracle:
|
if not oracle:
|
||||||
h = City.objects.transform('point', srid=htown.srid).get(name='Houston')
|
h = City.objects.transform(htown.srid).get(name='Houston')
|
||||||
self.assertEqual(3084, h.point.srid)
|
self.assertEqual(3084, h.point.srid)
|
||||||
self.assertAlmostEqual(htown.x, h.point.x, prec)
|
self.assertAlmostEqual(htown.x, h.point.x, prec)
|
||||||
self.assertAlmostEqual(htown.y, h.point.y, prec)
|
self.assertAlmostEqual(htown.y, h.point.y, prec)
|
||||||
|
|
||||||
p1 = City.objects.transform('point', srid=ptown.srid).get(name='Pueblo')
|
p1 = City.objects.transform(ptown.srid, field_name='point').get(name='Pueblo')
|
||||||
p2 = City.objects.transform(srid=ptown.srid).get(name='Pueblo')
|
p2 = City.objects.transform(srid=ptown.srid).get(name='Pueblo')
|
||||||
for p in [p1, p2]:
|
for p in [p1, p2]:
|
||||||
self.assertEqual(2774, p.point.srid)
|
self.assertEqual(2774, p.point.srid)
|
||||||
@ -186,7 +182,7 @@ class GeoModelTest(unittest.TestCase):
|
|||||||
|
|
||||||
@no_oracle # Most likely can do this in Oracle, however, it is not yet implemented (patches welcome!)
|
@no_oracle # Most likely can do this in Oracle, however, it is not yet implemented (patches welcome!)
|
||||||
def test05_extent(self):
|
def test05_extent(self):
|
||||||
"Testing the extent() GeoManager method."
|
"Testing the `extent` GeoQuerySet method."
|
||||||
if DISABLE: return
|
if DISABLE: return
|
||||||
# Reference query:
|
# Reference query:
|
||||||
# `SELECT ST_extent(point) FROM geoapp_city WHERE (name='Houston' or name='Dallas');`
|
# `SELECT ST_extent(point) FROM geoapp_city WHERE (name='Houston' or name='Dallas');`
|
||||||
@ -199,6 +195,17 @@ class GeoModelTest(unittest.TestCase):
|
|||||||
for val, exp in zip(extent, expected):
|
for val, exp in zip(extent, expected):
|
||||||
self.assertAlmostEqual(exp, val, 8)
|
self.assertAlmostEqual(exp, val, 8)
|
||||||
|
|
||||||
|
@no_oracle
|
||||||
|
def test06_make_line(self):
|
||||||
|
"Testing the `make_line` GeoQuerySet method."
|
||||||
|
# Ensuring that a `TypeError` is raised on models without PointFields.
|
||||||
|
self.assertRaises(TypeError, State.objects.make_line)
|
||||||
|
self.assertRaises(TypeError, Country.objects.make_line)
|
||||||
|
# Reference query:
|
||||||
|
# SELECT AsText(ST_MakeLine(geoapp_city.point)) FROM geoapp_city;
|
||||||
|
self.assertEqual(GEOSGeometry('LINESTRING(-95.363151 29.763374,-96.801611 32.782057,-97.521157 34.464642,174.783117 -41.315268,-104.609252 38.255001,-95.23506 38.971823,-87.650175 41.850385,-123.305196 48.462611)', srid=4326),
|
||||||
|
City.objects.make_line())
|
||||||
|
|
||||||
def test09_disjoint(self):
|
def test09_disjoint(self):
|
||||||
"Testing the `disjoint` lookup type."
|
"Testing the `disjoint` lookup type."
|
||||||
if DISABLE: return
|
if DISABLE: return
|
||||||
@ -317,8 +324,7 @@ class GeoModelTest(unittest.TestCase):
|
|||||||
# Saving another commonwealth w/a NULL geometry.
|
# Saving another commonwealth w/a NULL geometry.
|
||||||
if not oracle:
|
if not oracle:
|
||||||
# TODO: Fix saving w/NULL geometry on Oracle.
|
# TODO: Fix saving w/NULL geometry on Oracle.
|
||||||
nmi = State(name='Northern Mariana Islands', poly=None)
|
State(name='Northern Mariana Islands', poly=None).save()
|
||||||
nmi.save()
|
|
||||||
|
|
||||||
@no_oracle # No specific `left` or `right` operators in Oracle.
|
@no_oracle # No specific `left` or `right` operators in Oracle.
|
||||||
def test13_left_right(self):
|
def test13_left_right(self):
|
||||||
@ -416,34 +422,36 @@ class GeoModelTest(unittest.TestCase):
|
|||||||
c = City()
|
c = City()
|
||||||
self.assertEqual(c.point, None)
|
self.assertEqual(c.point, None)
|
||||||
|
|
||||||
def test17_union(self):
|
def test17_unionagg(self):
|
||||||
"Testing the union() GeoManager method."
|
"Testing the `unionagg` (aggregate union) GeoManager method."
|
||||||
if DISABLE: return
|
if DISABLE: return
|
||||||
tx = Country.objects.get(name='Texas').mpoly
|
tx = Country.objects.get(name='Texas').mpoly
|
||||||
# Houston, Dallas, San Antonio
|
# Houston, Dallas, San Antonio -- Oracle has different order.
|
||||||
union = fromstr('MULTIPOINT(-98.493183 29.424170,-96.801611 32.782057,-95.363151 29.763374)')
|
union1 = fromstr('MULTIPOINT(-98.493183 29.424170,-96.801611 32.782057,-95.363151 29.763374)')
|
||||||
|
union2 = fromstr('MULTIPOINT(-96.801611 32.782057,-95.363151 29.763374,-98.493183 29.424170)')
|
||||||
qs = City.objects.filter(point__within=tx)
|
qs = City.objects.filter(point__within=tx)
|
||||||
self.assertRaises(TypeError, qs.union, 'name')
|
self.assertRaises(TypeError, qs.unionagg, 'name')
|
||||||
u1 = qs.union('point')
|
u1 = qs.unionagg(field_name='point')
|
||||||
u2 = qs.union()
|
u2 = qs.unionagg()
|
||||||
self.assertEqual(True, union.equals_exact(u1, 10)) # Going up to 10 digits of precision.
|
tol = 0.00001
|
||||||
self.assertEqual(True, union.equals_exact(u2, 10))
|
if SpatialBackend.oracle:
|
||||||
|
union = union2
|
||||||
|
else:
|
||||||
|
union = union1
|
||||||
|
self.assertEqual(True, union.equals_exact(u1, tol))
|
||||||
|
self.assertEqual(True, union.equals_exact(u2, tol))
|
||||||
qs = City.objects.filter(name='NotACity')
|
qs = City.objects.filter(name='NotACity')
|
||||||
self.assertEqual(None, qs.union('point'))
|
self.assertEqual(None, qs.unionagg(field_name='point'))
|
||||||
|
|
||||||
def test18_geometryfield(self):
|
def test18_geometryfield(self):
|
||||||
"Testing GeometryField."
|
"Testing GeometryField."
|
||||||
if DISABLE: return
|
if DISABLE: return
|
||||||
f1 = Feature(name='Point', geom=Point(1, 1))
|
Feature(name='Point', geom=Point(1, 1)).save()
|
||||||
f2 = Feature(name='LineString', geom=LineString((0, 0), (1, 1), (5, 5)))
|
Feature(name='LineString', geom=LineString((0, 0), (1, 1), (5, 5))).save()
|
||||||
f3 = Feature(name='Polygon', geom=Polygon(LinearRing((0, 0), (0, 5), (5, 5), (5, 0), (0, 0))))
|
Feature(name='Polygon', geom=Polygon(LinearRing((0, 0), (0, 5), (5, 5), (5, 0), (0, 0)))).save()
|
||||||
f4 = Feature(name='GeometryCollection',
|
Feature(name='GeometryCollection',
|
||||||
geom=GeometryCollection(Point(2, 2), LineString((0, 0), (2, 2)),
|
geom=GeometryCollection(Point(2, 2), LineString((0, 0), (2, 2)),
|
||||||
Polygon(LinearRing((0, 0), (0, 5), (5, 5), (5, 0), (0, 0)))))
|
Polygon(LinearRing((0, 0), (0, 5), (5, 5), (5, 0), (0, 0))))).save()
|
||||||
f1.save()
|
|
||||||
f2.save()
|
|
||||||
f3.save()
|
|
||||||
f4.save()
|
|
||||||
|
|
||||||
f_1 = Feature.objects.get(name='Point')
|
f_1 = Feature.objects.get(name='Point')
|
||||||
self.assertEqual(True, isinstance(f_1.geom, Point))
|
self.assertEqual(True, isinstance(f_1.geom, Point))
|
||||||
@ -458,6 +466,82 @@ class GeoModelTest(unittest.TestCase):
|
|||||||
self.assertEqual(True, isinstance(f_4.geom, GeometryCollection))
|
self.assertEqual(True, isinstance(f_4.geom, GeometryCollection))
|
||||||
self.assertEqual(f_3.geom, f_4.geom[2])
|
self.assertEqual(f_3.geom, f_4.geom[2])
|
||||||
|
|
||||||
|
def test19_centroid(self):
|
||||||
|
"Testing the `centroid` GeoQuerySet method."
|
||||||
|
qs = State.objects.exclude(poly__isnull=True).centroid()
|
||||||
|
if oracle: tol = 0.1
|
||||||
|
else: tol = 0.000000001
|
||||||
|
for s in qs:
|
||||||
|
self.assertEqual(True, s.poly.centroid.equals_exact(s.centroid, tol))
|
||||||
|
|
||||||
|
def test20_pointonsurface(self):
|
||||||
|
"Testing the `point_on_surface` GeoQuerySet method."
|
||||||
|
# Reference values.
|
||||||
|
if SpatialBackend.oracle:
|
||||||
|
# SELECT SDO_UTIL.TO_WKTGEOMETRY(SDO_GEOM.SDO_POINTONSURFACE(GEOAPP_COUNTRY.MPOLY, 0.05)) FROM GEOAPP_COUNTRY;
|
||||||
|
ref = {'New Zealand' : fromstr('POINT (174.616364 -36.100861)', srid=4326),
|
||||||
|
'Texas' : fromstr('POINT (-103.002434 36.500397)', srid=4326),
|
||||||
|
}
|
||||||
|
elif SpatialBackend.postgis:
|
||||||
|
# Using GEOSGeometry to compute the reference point on surface values
|
||||||
|
# -- since PostGIS also uses GEOS these should be the same.
|
||||||
|
ref = {'New Zealand' : Country.objects.get(name='New Zealand').mpoly.point_on_surface,
|
||||||
|
'Texas' : Country.objects.get(name='Texas').mpoly.point_on_surface
|
||||||
|
}
|
||||||
|
for cntry in Country.objects.point_on_surface():
|
||||||
|
self.assertEqual(ref[cntry.name], cntry.point_on_surface)
|
||||||
|
|
||||||
|
@no_oracle
|
||||||
|
def test21_scale(self):
|
||||||
|
"Testing the `scale` GeoQuerySet method."
|
||||||
|
xfac, yfac = 2, 3
|
||||||
|
qs = Country.objects.scale(xfac, yfac, model_att='scaled')
|
||||||
|
for c in qs:
|
||||||
|
for p1, p2 in zip(c.mpoly, c.scaled):
|
||||||
|
for r1, r2 in zip(p1, p2):
|
||||||
|
for c1, c2 in zip(r1.coords, r2.coords):
|
||||||
|
self.assertEqual(c1[0] * xfac, c2[0])
|
||||||
|
self.assertEqual(c1[1] * yfac, c2[1])
|
||||||
|
|
||||||
|
@no_oracle
|
||||||
|
def test22_translate(self):
|
||||||
|
"Testing the `translate` GeoQuerySet method."
|
||||||
|
xfac, yfac = 5, -23
|
||||||
|
qs = Country.objects.translate(xfac, yfac, model_att='translated')
|
||||||
|
for c in qs:
|
||||||
|
for p1, p2 in zip(c.mpoly, c.translated):
|
||||||
|
for r1, r2 in zip(p1, p2):
|
||||||
|
for c1, c2 in zip(r1.coords, r2.coords):
|
||||||
|
self.assertEqual(c1[0] + xfac, c2[0])
|
||||||
|
self.assertEqual(c1[1] + yfac, c2[1])
|
||||||
|
|
||||||
|
def test23_numgeom(self):
|
||||||
|
"Testing the `num_geom` GeoQuerySet method."
|
||||||
|
# Both 'countries' only have two geometries.
|
||||||
|
for c in Country.objects.num_geom(): self.assertEqual(2, c.num_geom)
|
||||||
|
for c in City.objects.filter(point__isnull=False).num_geom():
|
||||||
|
# Oracle will return 1 for the number of geometries on non-collections,
|
||||||
|
# whereas PostGIS will return None.
|
||||||
|
if postgis: self.assertEqual(None, c.num_geom)
|
||||||
|
else: self.assertEqual(1, c.num_geom)
|
||||||
|
|
||||||
|
def test24_numpoints(self):
|
||||||
|
"Testing the `num_points` GeoQuerySet method."
|
||||||
|
for c in Country.objects.num_points(): self.assertEqual(c.mpoly.num_points, c.num_points)
|
||||||
|
if postgis:
|
||||||
|
# Oracle cannot count vertices in Point geometries.
|
||||||
|
for c in City.objects.num_points(): self.assertEqual(1, c.num_points)
|
||||||
|
|
||||||
|
@no_oracle
|
||||||
|
def test25_geoset(self):
|
||||||
|
"Testing the `difference`, `intersection`, `sym_difference`, and `union` GeoQuerySet methods."
|
||||||
|
geom = Point(5, 23)
|
||||||
|
for c in Country.objects.all().intersection(geom).difference(geom).sym_difference(geom).union(geom):
|
||||||
|
self.assertEqual(c.mpoly.difference(geom), c.difference)
|
||||||
|
self.assertEqual(c.mpoly.intersection(geom), c.intersection)
|
||||||
|
self.assertEqual(c.mpoly.sym_difference(geom), c.sym_difference)
|
||||||
|
self.assertEqual(c.mpoly.union(geom), c.union)
|
||||||
|
|
||||||
def suite():
|
def suite():
|
||||||
s = unittest.TestSuite()
|
s = unittest.TestSuite()
|
||||||
s.addTest(unittest.makeSuite(GeoModelTest))
|
s.addTest(unittest.makeSuite(GeoModelTest))
|
||||||
|
@ -169,9 +169,9 @@ class GeoModelTest(unittest.TestCase):
|
|||||||
|
|
||||||
def test07_mysql_limitations(self):
|
def test07_mysql_limitations(self):
|
||||||
"Testing that union(), kml(), gml() raise exceptions."
|
"Testing that union(), kml(), gml() raise exceptions."
|
||||||
self.assertRaises(ImproperlyConfigured, City.objects.union, 'point')
|
self.assertRaises(ImproperlyConfigured, City.objects.union, Point(5, 23), field_name='point')
|
||||||
self.assertRaises(ImproperlyConfigured, State.objects.all().kml, 'poly')
|
self.assertRaises(ImproperlyConfigured, State.objects.all().kml, field_name='poly')
|
||||||
self.assertRaises(ImproperlyConfigured, Country.objects.all().gml, 'mpoly')
|
self.assertRaises(ImproperlyConfigured, Country.objects.all().gml, field_name='mpoly')
|
||||||
|
|
||||||
def suite():
|
def suite():
|
||||||
s = unittest.TestSuite()
|
s = unittest.TestSuite()
|
||||||
|
@ -19,7 +19,7 @@ class City(models.Model):
|
|||||||
name = models.CharField(max_length=25)
|
name = models.CharField(max_length=25)
|
||||||
population = models.IntegerField()
|
population = models.IntegerField()
|
||||||
density = models.DecimalField(max_digits=7, decimal_places=1)
|
density = models.DecimalField(max_digits=7, decimal_places=1)
|
||||||
date = models.DateField()
|
dt = models.DateField()
|
||||||
point = models.PointField()
|
point = models.PointField()
|
||||||
objects = models.GeoManager()
|
objects = models.GeoManager()
|
||||||
|
|
||||||
@ -42,7 +42,7 @@ cofeat_mapping = {'name' : 'Name',
|
|||||||
city_mapping = {'name' : 'Name',
|
city_mapping = {'name' : 'Name',
|
||||||
'population' : 'Population',
|
'population' : 'Population',
|
||||||
'density' : 'Density',
|
'density' : 'Density',
|
||||||
'date' : 'Created',
|
'dt' : 'Created',
|
||||||
'point' : 'POINT',
|
'point' : 'POINT',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,7 +69,7 @@ class LayerMapTest(unittest.TestCase):
|
|||||||
city = City.objects.get(name=feat['Name'].value)
|
city = City.objects.get(name=feat['Name'].value)
|
||||||
self.assertEqual(feat['Population'].value, city.population)
|
self.assertEqual(feat['Population'].value, city.population)
|
||||||
self.assertEqual(Decimal(str(feat['Density'])), city.density)
|
self.assertEqual(Decimal(str(feat['Density'])), city.density)
|
||||||
self.assertEqual(feat['Created'].value, city.date)
|
self.assertEqual(feat['Created'].value, city.dt)
|
||||||
|
|
||||||
# Comparing the geometries.
|
# Comparing the geometries.
|
||||||
pnt1, pnt2 = feat.geom, city.point
|
pnt1, pnt2 = feat.geom, city.point
|
||||||
|
@ -34,7 +34,7 @@ class RelatedGeoModelTest(unittest.TestCase):
|
|||||||
|
|
||||||
@no_mysql
|
@no_mysql
|
||||||
def test03_transform_related(self):
|
def test03_transform_related(self):
|
||||||
"Testing the `transform` GeoManager method on related geographic models."
|
"Testing the `transform` GeoQuerySet method on related geographic models."
|
||||||
# All the transformations are to state plane coordinate systems using
|
# All the transformations are to state plane coordinate systems using
|
||||||
# US Survey Feet (thus a tolerance of 0 implies error w/in 1 survey foot).
|
# US Survey Feet (thus a tolerance of 0 implies error w/in 1 survey foot).
|
||||||
if postgis:
|
if postgis:
|
||||||
@ -47,6 +47,7 @@ class RelatedGeoModelTest(unittest.TestCase):
|
|||||||
def check_pnt(ref, pnt):
|
def check_pnt(ref, pnt):
|
||||||
self.assertAlmostEqual(ref.x, pnt.x, tol)
|
self.assertAlmostEqual(ref.x, pnt.x, tol)
|
||||||
self.assertAlmostEqual(ref.y, pnt.y, tol)
|
self.assertAlmostEqual(ref.y, pnt.y, tol)
|
||||||
|
self.assertEqual(ref.srid, pnt.srid)
|
||||||
|
|
||||||
# Turning on debug so we can manually verify the number of SQL queries issued.
|
# Turning on debug so we can manually verify the number of SQL queries issued.
|
||||||
# DISABLED: the number of queries count testing mechanism is way too brittle.
|
# DISABLED: the number of queries count testing mechanism is way too brittle.
|
||||||
@ -62,13 +63,35 @@ class RelatedGeoModelTest(unittest.TestCase):
|
|||||||
|
|
||||||
for name, srid, wkt in transformed:
|
for name, srid, wkt in transformed:
|
||||||
# Doing this implicitly sets `select_related` select the location.
|
# Doing this implicitly sets `select_related` select the location.
|
||||||
qs = list(City.objects.filter(name=name).transform('location__point', srid))
|
qs = list(City.objects.filter(name=name).transform(srid, field_name='location__point'))
|
||||||
check_pnt(GEOSGeometry(wkt), qs[0].location.point)
|
check_pnt(GEOSGeometry(wkt, srid), qs[0].location.point)
|
||||||
#settings.DEBUG= dbg
|
#settings.DEBUG= dbg
|
||||||
|
|
||||||
# Verifying the number of issued SQL queries.
|
# Verifying the number of issued SQL queries.
|
||||||
#self.assertEqual(nqueries, len(connection.queries))
|
#self.assertEqual(nqueries, len(connection.queries))
|
||||||
|
|
||||||
|
@no_mysql
|
||||||
|
def test04_related_aggregate(self):
|
||||||
|
"Testing the `extent` and `unionagg` GeoQuerySet aggregates on related geographic models."
|
||||||
|
if postgis:
|
||||||
|
# One for all locations, one that excludes Roswell.
|
||||||
|
all_extent = (-104.528060913086, 33.0583305358887,-79.4607315063477, 40.1847610473633)
|
||||||
|
txpa_extent = (-97.51611328125, 33.0583305358887,-79.4607315063477, 40.1847610473633)
|
||||||
|
e1 = City.objects.extent(field_name='location__point')
|
||||||
|
e2 = City.objects.exclude(name='Roswell').extent(field_name='location__point')
|
||||||
|
for ref, e in [(all_extent, e1), (txpa_extent, e2)]:
|
||||||
|
for ref_val, e_val in zip(ref, e): self.assertAlmostEqual(ref_val, e_val)
|
||||||
|
|
||||||
|
# The second union is for a query that has something in the WHERE clause.
|
||||||
|
ref_u1 = GEOSGeometry('MULTIPOINT(-104.528056 33.387222,-97.516111 33.058333,-79.460734 40.18476)', 4326)
|
||||||
|
ref_u2 = GEOSGeometry('MULTIPOINT(-97.516111 33.058333,-79.460734 40.18476)', 4326)
|
||||||
|
u1 = City.objects.unionagg(field_name='location__point')
|
||||||
|
u2 = City.objects.exclude(name='Roswell').unionagg(field_name='location__point')
|
||||||
|
self.assertEqual(ref_u1, u1)
|
||||||
|
self.assertEqual(ref_u2, u2)
|
||||||
|
|
||||||
|
# TODO: Related tests for KML, GML, and distance lookups.
|
||||||
|
|
||||||
def suite():
|
def suite():
|
||||||
s = unittest.TestSuite()
|
s = unittest.TestSuite()
|
||||||
s.addTest(unittest.makeSuite(RelatedGeoModelTest))
|
s.addTest(unittest.makeSuite(RelatedGeoModelTest))
|
||||||
|
Loading…
x
Reference in New Issue
Block a user