Fixed #25588 -- Added spatial lookups to RasterField.

Thanks Tim Graham for the review.
This commit is contained in:
Daniel Wiesmann 2016-04-21 17:03:14 +01:00 committed by Tim Graham
parent 03efa304bc
commit bbfad84dd9
11 changed files with 743 additions and 158 deletions

View File

@ -6,16 +6,27 @@ from __future__ import unicode_literals
from psycopg2 import Binary from psycopg2 import Binary
from psycopg2.extensions import ISQLQuote from psycopg2.extensions import ISQLQuote
from django.contrib.gis.db.backends.postgis.pgraster import to_pgraster
from django.contrib.gis.geometry.backend import Geometry
class PostGISAdapter(object): class PostGISAdapter(object):
def __init__(self, geom, geography=False): def __init__(self, obj, geography=False):
"Initializes on the geometry." """
Initialize on the spatial object.
"""
self.is_geometry = isinstance(obj, Geometry)
# Getting the WKB (in string form, to allow easy pickling of # Getting the WKB (in string form, to allow easy pickling of
# the adaptor) and the SRID from the geometry. # the adaptor) and the SRID from the geometry or raster.
self.ewkb = bytes(geom.ewkb) if self.is_geometry:
self.srid = geom.srid self.ewkb = bytes(obj.ewkb)
self.geography = geography
self._adapter = Binary(self.ewkb) self._adapter = Binary(self.ewkb)
else:
self.ewkb = to_pgraster(obj)
self.srid = obj.srid
self.geography = geography
def __conform__(self, proto): def __conform__(self, proto):
# Does the given protocol conform to what Psycopg2 expects? # Does the given protocol conform to what Psycopg2 expects?
@ -40,12 +51,19 @@ class PostGISAdapter(object):
This method allows escaping the binary in the style required by the This method allows escaping the binary in the style required by the
server's `standard_conforming_string` setting. server's `standard_conforming_string` setting.
""" """
if self.is_geometry:
self._adapter.prepare(conn) self._adapter.prepare(conn)
def getquoted(self): def getquoted(self):
"Returns a properly quoted string for use in PostgreSQL/PostGIS." """
# psycopg will figure out whether to use E'\\000' or '\000' Return a properly quoted string for use in PostgreSQL/PostGIS.
"""
if self.is_geometry:
# Psycopg will figure out whether to use E'\\000' or '\000'.
return str('%s(%s)' % ( return str('%s(%s)' % (
'ST_GeogFromWKB' if self.geography else 'ST_GeomFromEWKB', 'ST_GeogFromWKB' if self.geography else 'ST_GeomFromEWKB',
self._adapter.getquoted().decode()) self._adapter.getquoted().decode())
) )
else:
# For rasters, add explicit type cast to WKB string.
return "'%s'::raster" % self.ewkb

View File

@ -4,30 +4,83 @@ from django.conf import settings
from django.contrib.gis.db.backends.base.operations import \ from django.contrib.gis.db.backends.base.operations import \
BaseSpatialOperations BaseSpatialOperations
from django.contrib.gis.db.backends.utils import SpatialOperator from django.contrib.gis.db.backends.utils import SpatialOperator
from django.contrib.gis.gdal import GDALRaster
from django.contrib.gis.geometry.backend import Geometry from django.contrib.gis.geometry.backend import Geometry
from django.contrib.gis.measure import Distance from django.contrib.gis.measure import Distance
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.db.backends.postgresql.operations import DatabaseOperations from django.db.backends.postgresql.operations import DatabaseOperations
from django.db.utils import ProgrammingError from django.db.utils import ProgrammingError
from django.utils import six
from django.utils.functional import cached_property from django.utils.functional import cached_property
from .adapter import PostGISAdapter from .adapter import PostGISAdapter
from .models import PostGISGeometryColumns, PostGISSpatialRefSys from .models import PostGISGeometryColumns, PostGISSpatialRefSys
from .pgraster import from_pgraster, get_pgraster_srid, to_pgraster from .pgraster import from_pgraster, get_pgraster_srid, to_pgraster
# Identifier to mark raster lookups as bilateral.
BILATERAL = 'bilateral'
class PostGISOperator(SpatialOperator): class PostGISOperator(SpatialOperator):
def __init__(self, geography=False, **kwargs): def __init__(self, geography=False, raster=False, **kwargs):
# Only a subset of the operators and functions are available # Only a subset of the operators and functions are available for the
# for the geography type. # geography type.
self.geography = geography self.geography = geography
# Only a subset of the operators and functions are available for the
# raster type. Lookups that don't suport raster will be converted to
# polygons. If the raster argument is set to BILATERAL, then the
# operator cannot handle mixed geom-raster lookups.
self.raster = raster
super(PostGISOperator, self).__init__(**kwargs) super(PostGISOperator, self).__init__(**kwargs)
def as_sql(self, connection, lookup, *args): def as_sql(self, connection, lookup, template_params, *args):
if lookup.lhs.output_field.geography and not self.geography: if lookup.lhs.output_field.geography and not self.geography:
raise ValueError('PostGIS geography does not support the "%s" ' raise ValueError('PostGIS geography does not support the "%s" '
'function/operator.' % (self.func or self.op,)) 'function/operator.' % (self.func or self.op,))
return super(PostGISOperator, self).as_sql(connection, lookup, *args)
template_params = self.check_raster(lookup, template_params)
return super(PostGISOperator, self).as_sql(connection, lookup, template_params, *args)
def check_raster(self, lookup, template_params):
# Get rhs value.
if isinstance(lookup.rhs, (tuple, list)):
rhs_val = lookup.rhs[0]
spheroid = lookup.rhs[-1] == 'spheroid'
else:
rhs_val = lookup.rhs
spheroid = False
# Check which input is a raster.
lhs_is_raster = lookup.lhs.field.geom_type == 'RASTER'
rhs_is_raster = isinstance(rhs_val, GDALRaster)
# Look for band indices and inject them if provided.
if lookup.band_lhs is not None and lhs_is_raster:
if not self.func:
raise ValueError('Band indices are not allowed for this operator, it works on bbox only.')
template_params['lhs'] = '%s, %s' % (template_params['lhs'], lookup.band_lhs)
if lookup.band_rhs is not None and rhs_is_raster:
if not self.func:
raise ValueError('Band indices are not allowed for this operator, it works on bbox only.')
template_params['rhs'] = '%s, %s' % (template_params['rhs'], lookup.band_rhs)
# Convert rasters to polygons if necessary.
if not self.raster or spheroid:
# Operators without raster support.
if lhs_is_raster:
template_params['lhs'] = 'ST_Polygon(%s)' % template_params['lhs']
if rhs_is_raster:
template_params['rhs'] = 'ST_Polygon(%s)' % template_params['rhs']
elif self.raster == BILATERAL:
# Operators with raster support but don't support mixed (rast-geom)
# lookups.
if lhs_is_raster and not rhs_is_raster:
template_params['lhs'] = 'ST_Polygon(%s)' % template_params['lhs']
elif rhs_is_raster and not lhs_is_raster:
template_params['rhs'] = 'ST_Polygon(%s)' % template_params['rhs']
return template_params
class PostGISDistanceOperator(PostGISOperator): class PostGISDistanceOperator(PostGISOperator):
@ -35,6 +88,7 @@ class PostGISDistanceOperator(PostGISOperator):
def as_sql(self, connection, lookup, template_params, sql_params): def as_sql(self, connection, lookup, template_params, sql_params):
if not lookup.lhs.output_field.geography and lookup.lhs.output_field.geodetic(connection): if not lookup.lhs.output_field.geography and lookup.lhs.output_field.geodetic(connection):
template_params = self.check_raster(lookup, template_params)
sql_template = self.sql_template sql_template = self.sql_template
if len(lookup.rhs) == 3 and lookup.rhs[-1] == 'spheroid': if len(lookup.rhs) == 3 and lookup.rhs[-1] == 'spheroid':
template_params.update({'op': self.op, 'func': 'ST_Distance_Spheroid'}) template_params.update({'op': self.op, 'func': 'ST_Distance_Spheroid'})
@ -58,33 +112,33 @@ class PostGISOperations(BaseSpatialOperations, DatabaseOperations):
Adapter = PostGISAdapter Adapter = PostGISAdapter
gis_operators = { gis_operators = {
'bbcontains': PostGISOperator(op='~'), 'bbcontains': PostGISOperator(op='~', raster=True),
'bboverlaps': PostGISOperator(op='&&', geography=True), 'bboverlaps': PostGISOperator(op='&&', geography=True, raster=True),
'contained': PostGISOperator(op='@'), 'contained': PostGISOperator(op='@', raster=True),
'contains': PostGISOperator(func='ST_Contains'), 'overlaps_left': PostGISOperator(op='&<', raster=BILATERAL),
'overlaps_left': PostGISOperator(op='&<'), 'overlaps_right': PostGISOperator(op='&>', raster=BILATERAL),
'overlaps_right': PostGISOperator(op='&>'),
'overlaps_below': PostGISOperator(op='&<|'), 'overlaps_below': PostGISOperator(op='&<|'),
'overlaps_above': PostGISOperator(op='|&>'), 'overlaps_above': PostGISOperator(op='|&>'),
'left': PostGISOperator(op='<<'), 'left': PostGISOperator(op='<<'),
'right': PostGISOperator(op='>>'), 'right': PostGISOperator(op='>>'),
'strictly_below': PostGISOperator(op='<<|'), 'strictly_below': PostGISOperator(op='<<|'),
'strictly_above': PostGISOperator(op='|>>'), 'strictly_above': PostGISOperator(op='|>>'),
'same_as': PostGISOperator(op='~='), 'same_as': PostGISOperator(op='~=', raster=BILATERAL),
'exact': PostGISOperator(op='~='), # alias of same_as 'exact': PostGISOperator(op='~=', raster=BILATERAL), # alias of same_as
'contains_properly': PostGISOperator(func='ST_ContainsProperly'), 'contains': PostGISOperator(func='ST_Contains', raster=BILATERAL),
'coveredby': PostGISOperator(func='ST_CoveredBy', geography=True), 'contains_properly': PostGISOperator(func='ST_ContainsProperly', raster=BILATERAL),
'covers': PostGISOperator(func='ST_Covers', geography=True), 'coveredby': PostGISOperator(func='ST_CoveredBy', geography=True, raster=BILATERAL),
'covers': PostGISOperator(func='ST_Covers', geography=True, raster=BILATERAL),
'crosses': PostGISOperator(func='ST_Crosses'), 'crosses': PostGISOperator(func='ST_Crosses'),
'disjoint': PostGISOperator(func='ST_Disjoint'), 'disjoint': PostGISOperator(func='ST_Disjoint', raster=BILATERAL),
'equals': PostGISOperator(func='ST_Equals'), 'equals': PostGISOperator(func='ST_Equals'),
'intersects': PostGISOperator(func='ST_Intersects', geography=True), 'intersects': PostGISOperator(func='ST_Intersects', geography=True, raster=BILATERAL),
'isvalid': PostGISOperator(func='ST_IsValid'), 'isvalid': PostGISOperator(func='ST_IsValid'),
'overlaps': PostGISOperator(func='ST_Overlaps'), 'overlaps': PostGISOperator(func='ST_Overlaps', raster=BILATERAL),
'relate': PostGISOperator(func='ST_Relate'), 'relate': PostGISOperator(func='ST_Relate'),
'touches': PostGISOperator(func='ST_Touches'), 'touches': PostGISOperator(func='ST_Touches', raster=BILATERAL),
'within': PostGISOperator(func='ST_Within'), 'within': PostGISOperator(func='ST_Within', raster=BILATERAL),
'dwithin': PostGISOperator(func='ST_DWithin', geography=True), 'dwithin': PostGISOperator(func='ST_DWithin', geography=True, raster=BILATERAL),
'distance_gt': PostGISDistanceOperator(func='ST_Distance', op='>', geography=True), 'distance_gt': PostGISDistanceOperator(func='ST_Distance', op='>', geography=True),
'distance_gte': PostGISDistanceOperator(func='ST_Distance', op='>=', geography=True), 'distance_gte': PostGISDistanceOperator(func='ST_Distance', op='>=', geography=True),
'distance_lt': PostGISDistanceOperator(func='ST_Distance', op='<', geography=True), 'distance_lt': PostGISDistanceOperator(func='ST_Distance', op='<', geography=True),
@ -272,14 +326,14 @@ class PostGISOperations(BaseSpatialOperations, DatabaseOperations):
def get_geom_placeholder(self, f, value, compiler): def get_geom_placeholder(self, f, value, compiler):
""" """
Provides a proper substitution value for Geometries that are not in the Provide a proper substitution value for Geometries or rasters that are
SRID of the field. Specifically, this routine will substitute in the not in the SRID of the field. Specifically, this routine will
ST_Transform() function call. substitute in the ST_Transform() function call.
""" """
# Get the srid for this object # Get the srid for this object
if value is None: if value is None:
value_srid = None value_srid = None
elif f.geom_type == 'RASTER': elif f.geom_type == 'RASTER' and isinstance(value, six.string_types):
value_srid = get_pgraster_srid(value) value_srid = get_pgraster_srid(value)
else: else:
value_srid = value.srid value_srid = value.srid
@ -288,7 +342,7 @@ class PostGISOperations(BaseSpatialOperations, DatabaseOperations):
# is not equal to the field srid. # is not equal to the field srid.
if value_srid is None or value_srid == f.srid: if value_srid is None or value_srid == f.srid:
placeholder = '%s' placeholder = '%s'
elif f.geom_type == 'RASTER': elif f.geom_type == 'RASTER' and isinstance(value, six.string_types):
placeholder = '%s((%%s)::raster, %s)' % (self.transform, f.srid) placeholder = '%s((%%s)::raster, %s)' % (self.transform, f.srid)
else: else:
placeholder = '%s(%%s, %s)' % (self.transform, f.srid) placeholder = '%s(%%s, %s)' % (self.transform, f.srid)

View File

@ -1,7 +1,10 @@
from django.contrib.gis import forms from django.contrib.gis import forms
from django.contrib.gis.db.models.lookups import gis_lookups from django.contrib.gis.db.models.lookups import (
RasterBandTransform, gis_lookups,
)
from django.contrib.gis.db.models.proxy import SpatialProxy from django.contrib.gis.db.models.proxy import SpatialProxy
from django.contrib.gis.gdal import HAS_GDAL from django.contrib.gis.gdal import HAS_GDAL
from django.contrib.gis.gdal.error import GDALException
from django.contrib.gis.geometry.backend import Geometry, GeometryException from django.contrib.gis.geometry.backend import Geometry, GeometryException
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.db.models.expressions import Expression from django.db.models.expressions import Expression
@ -157,6 +160,82 @@ class BaseSpatialField(Field):
""" """
return connection.ops.get_geom_placeholder(self, value, compiler) return connection.ops.get_geom_placeholder(self, value, compiler)
def get_srid(self, obj):
"""
Return the default SRID for the given geometry or raster, taking into
account the SRID set for the field. For example, if the input geometry
or raster doesn't have an SRID, then the SRID of the field will be
returned.
"""
srid = obj.srid # SRID of given geometry.
if srid is None or self.srid == -1 or (srid == -1 and self.srid != -1):
return self.srid
else:
return srid
def get_db_prep_save(self, value, connection):
"""
Prepare the value for saving in the database.
"""
if not value:
return None
else:
return connection.ops.Adapter(self.get_prep_value(value))
def get_prep_value(self, value):
"""
Spatial lookup values are either a parameter that is (or may be
converted to) a geometry or raster, or a sequence of lookup values
that begins with a geometry or raster. This routine sets up the
geometry or raster value properly and preserves any other lookup
parameters.
"""
from django.contrib.gis.gdal import GDALRaster
value = super(BaseSpatialField, self).get_prep_value(value)
# For IsValid lookups, boolean values are allowed.
if isinstance(value, (Expression, bool)):
return value
elif isinstance(value, (tuple, list)):
obj = value[0]
seq_value = True
else:
obj = value
seq_value = False
# When the input is not a geometry or raster, attempt to construct one
# from the given string input.
if isinstance(obj, (Geometry, GDALRaster)):
pass
elif isinstance(obj, (bytes, six.string_types)) or hasattr(obj, '__geo_interface__'):
try:
obj = Geometry(obj)
except (GeometryException, GDALException):
try:
obj = GDALRaster(obj)
except GDALException:
raise ValueError("Couldn't create spatial object from lookup value '%s'." % obj)
elif isinstance(obj, dict):
try:
obj = GDALRaster(obj)
except GDALException:
raise ValueError("Couldn't create spatial object from lookup value '%s'." % obj)
else:
raise ValueError('Cannot use object with type %s for a spatial lookup parameter.' % type(obj).__name__)
# Assigning the SRID value.
obj.srid = self.get_srid(obj)
if seq_value:
lookup_val = [obj]
lookup_val.extend(value[1:])
return tuple(lookup_val)
else:
return obj
for klass in gis_lookups.values():
BaseSpatialField.register_lookup(klass)
class GeometryField(GeoSelectFormatMixin, BaseSpatialField): class GeometryField(GeoSelectFormatMixin, BaseSpatialField):
""" """
@ -224,6 +303,8 @@ class GeometryField(GeoSelectFormatMixin, BaseSpatialField):
value properly, and preserve any other lookup parameters before value properly, and preserve any other lookup parameters before
returning to the caller. returning to the caller.
""" """
from django.contrib.gis.gdal import GDALRaster
value = super(GeometryField, self).get_prep_value(value) value = super(GeometryField, self).get_prep_value(value)
if isinstance(value, (Expression, bool)): if isinstance(value, (Expression, bool)):
return value return value
@ -236,7 +317,7 @@ class GeometryField(GeoSelectFormatMixin, BaseSpatialField):
# 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, Geometry): if isinstance(geom, (Geometry, GDALRaster)):
pass pass
elif isinstance(geom, (bytes, six.string_types)) or hasattr(geom, '__geo_interface__'): elif isinstance(geom, (bytes, six.string_types)) or hasattr(geom, '__geo_interface__'):
try: try:
@ -265,18 +346,6 @@ class GeometryField(GeoSelectFormatMixin, BaseSpatialField):
value.srid = self.srid value.srid = self.srid
return value return value
def get_srid(self, geom):
"""
Returns the default SRID for the given geometry, taking into account
the SRID set for the field. For example, if the input geometry
has no SRID, then that of the field will be returned.
"""
gsrid = geom.srid # SRID of given geometry.
if gsrid is None or self.srid == -1 or (gsrid == -1 and self.srid != -1):
return self.srid
else:
return gsrid
# ### Routines overloaded from Field ### # ### Routines overloaded from Field ###
def contribute_to_class(self, cls, name, **kwargs): def contribute_to_class(self, cls, name, **kwargs):
super(GeometryField, self).contribute_to_class(cls, name, **kwargs) super(GeometryField, self).contribute_to_class(cls, name, **kwargs)
@ -316,17 +385,6 @@ class GeometryField(GeoSelectFormatMixin, BaseSpatialField):
params = [connection.ops.Adapter(value)] params = [connection.ops.Adapter(value)]
return params return params
def get_db_prep_save(self, value, connection):
"Prepares the value for saving in the database."
if not value:
return None
else:
return connection.ops.Adapter(self.get_prep_value(value))
for klass in gis_lookups.values():
GeometryField.register_lookup(klass)
# The OpenGIS Geometry Type Fields # The OpenGIS Geometry Type Fields
class PointField(GeometryField): class PointField(GeometryField):
@ -387,6 +445,7 @@ class RasterField(BaseSpatialField):
description = _("Raster Field") description = _("Raster Field")
geom_type = 'RASTER' geom_type = 'RASTER'
geography = False
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
if not HAS_GDAL: if not HAS_GDAL:
@ -421,3 +480,15 @@ class RasterField(BaseSpatialField):
# delays the instantiation of the objects to the moment of evaluation # delays the instantiation of the objects to the moment of evaluation
# of the raster attribute. # of the raster attribute.
setattr(cls, self.attname, SpatialProxy(GDALRaster, self)) setattr(cls, self.attname, SpatialProxy(GDALRaster, self))
def get_transform(self, name):
try:
band_index = int(name)
return type(
'SpecificRasterBandTransform',
(RasterBandTransform, ),
{'band_index': band_index}
)
except ValueError:
pass
return super(RasterField, self).get_transform(name)

View File

@ -5,16 +5,23 @@ import re
from django.core.exceptions import FieldDoesNotExist from django.core.exceptions import FieldDoesNotExist
from django.db.models.constants import LOOKUP_SEP from django.db.models.constants import LOOKUP_SEP
from django.db.models.expressions import Col, Expression from django.db.models.expressions import Col, Expression
from django.db.models.lookups import BuiltinLookup, Lookup from django.db.models.lookups import BuiltinLookup, Lookup, Transform
from django.utils import six from django.utils import six
gis_lookups = {} gis_lookups = {}
class RasterBandTransform(Transform):
def as_sql(self, compiler, connection):
return compiler.compile(self.lhs)
class GISLookup(Lookup): class GISLookup(Lookup):
sql_template = None sql_template = None
transform_func = None transform_func = None
distance = False distance = False
band_rhs = None
band_lhs = None
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(GISLookup, self).__init__(*args, **kwargs) super(GISLookup, self).__init__(*args, **kwargs)
@ -28,10 +35,10 @@ class GISLookup(Lookup):
'point, 'the_geom', or a related lookup on a geographic field like 'point, 'the_geom', or a related lookup on a geographic field like
'address__point'. 'address__point'.
If a GeometryField exists according to the given lookup on the model If a BaseSpatialField exists according to the given lookup on the model
options, it will be returned. Otherwise returns None. options, it will be returned. Otherwise return None.
""" """
from django.contrib.gis.db.models.fields import GeometryField from django.contrib.gis.db.models.fields import BaseSpatialField
# This takes into account the situation where the lookup is a # This takes into account the situation where the lookup is a
# lookup to a related geographic field, e.g., 'address__point'. # lookup to a related geographic field, e.g., 'address__point'.
field_list = lookup.split(LOOKUP_SEP) field_list = lookup.split(LOOKUP_SEP)
@ -55,11 +62,34 @@ class GISLookup(Lookup):
return False return False
# Finally, make sure we got a Geographic field and return. # Finally, make sure we got a Geographic field and return.
if isinstance(geo_fld, GeometryField): if isinstance(geo_fld, BaseSpatialField):
return geo_fld return geo_fld
else: else:
return False return False
def process_band_indices(self, only_lhs=False):
"""
Extract the lhs band index from the band transform class and the rhs
band index from the input tuple.
"""
# PostGIS band indices are 1-based, so the band index needs to be
# increased to be consistent with the GDALRaster band indices.
if only_lhs:
self.band_rhs = 1
self.band_lhs = self.lhs.band_index + 1
return
if isinstance(self.lhs, RasterBandTransform):
self.band_lhs = self.lhs.band_index + 1
else:
self.band_lhs = 1
self.band_rhs = self.rhs[1]
if len(self.rhs) == 1:
self.rhs = self.rhs[0]
else:
self.rhs = (self.rhs[0], ) + self.rhs[2:]
def get_db_prep_lookup(self, value, connection): def get_db_prep_lookup(self, value, connection):
# get_db_prep_lookup is called by process_rhs from super class # get_db_prep_lookup is called by process_rhs from super class
if isinstance(value, (tuple, list)): if isinstance(value, (tuple, list)):
@ -70,10 +100,9 @@ class GISLookup(Lookup):
return ('%s', params) return ('%s', params)
def process_rhs(self, compiler, connection): def process_rhs(self, compiler, connection):
rhs, rhs_params = super(GISLookup, self).process_rhs(compiler, connection)
if hasattr(self.rhs, '_as_sql'): if hasattr(self.rhs, '_as_sql'):
# If rhs is some QuerySet, don't touch it # If rhs is some QuerySet, don't touch it
return rhs, rhs_params return super(GISLookup, self).process_rhs(compiler, connection)
geom = self.rhs geom = self.rhs
if isinstance(self.rhs, Col): if isinstance(self.rhs, Col):
@ -85,9 +114,19 @@ class GISLookup(Lookup):
raise ValueError('No geographic field found in expression.') raise ValueError('No geographic field found in expression.')
self.rhs.srid = geo_fld.srid self.rhs.srid = geo_fld.srid
elif isinstance(self.rhs, Expression): elif isinstance(self.rhs, Expression):
raise ValueError('Complex expressions not supported for GeometryField') raise ValueError('Complex expressions not supported for spatial fields.')
elif isinstance(self.rhs, (list, tuple)): elif isinstance(self.rhs, (list, tuple)):
geom = self.rhs[0] geom = self.rhs[0]
# Check if a band index was passed in the query argument.
if ((len(self.rhs) == 2 and not self.lookup_name == 'relate') or
(len(self.rhs) == 3 and self.lookup_name == 'relate')):
self.process_band_indices()
elif len(self.rhs) > 2:
raise ValueError('Tuple too long for lookup %s.' % self.lookup_name)
elif isinstance(self.lhs, RasterBandTransform):
self.process_band_indices(only_lhs=True)
rhs, rhs_params = super(GISLookup, self).process_rhs(compiler, connection)
rhs = connection.ops.get_geom_placeholder(self.lhs.output_field, geom, compiler) rhs = connection.ops.get_geom_placeholder(self.lhs.output_field, geom, compiler)
return rhs, rhs_params return rhs, rhs_params
@ -274,6 +313,8 @@ class IsValidLookup(BuiltinLookup):
lookup_name = 'isvalid' lookup_name = 'isvalid'
def as_sql(self, compiler, connection): def as_sql(self, compiler, connection):
if self.lhs.field.geom_type == 'RASTER':
raise ValueError('The isvalid lookup is only available on geometry fields.')
gis_op = connection.ops.gis_operators[self.lookup_name] gis_op = connection.ops.gis_operators[self.lookup_name]
sql, params = self.process_lhs(compiler, connection) sql, params = self.process_lhs(compiler, connection)
sql = '%(func)s(%(lhs)s)' % {'func': gis_op.func, 'lhs': sql} sql = '%(func)s(%(lhs)s)' % {'func': gis_op.func, 'lhs': sql}
@ -323,9 +364,17 @@ class DistanceLookupBase(GISLookup):
sql_template = '%(func)s(%(lhs)s, %(rhs)s) %(op)s %(value)s' sql_template = '%(func)s(%(lhs)s, %(rhs)s) %(op)s %(value)s'
def process_rhs(self, compiler, connection): def process_rhs(self, compiler, connection):
if not isinstance(self.rhs, (tuple, list)) or not 2 <= len(self.rhs) <= 3: if not isinstance(self.rhs, (tuple, list)) or not 2 <= len(self.rhs) <= 4:
raise ValueError("2 or 3-element tuple required for '%s' lookup." % self.lookup_name) raise ValueError("2, 3, or 4-element tuple required for '%s' lookup." % self.lookup_name)
elif len(self.rhs) == 4 and not self.rhs[3] == 'spheroid':
raise ValueError("For 4-element tuples the last argument must be the 'speroid' directive.")
# Check if the second parameter is a band index.
if len(self.rhs) > 2 and not self.rhs[2] == 'spheroid':
self.process_band_indices()
params = [connection.ops.Adapter(self.rhs[0])] params = [connection.ops.Adapter(self.rhs[0])]
# Getting the distance parameter in the units of the field. # Getting the distance parameter in the units of the field.
dist_param = self.rhs[1] dist_param = self.rhs[1]
if hasattr(dist_param, 'resolve_expression'): if hasattr(dist_param, 'resolve_expression'):

View File

@ -53,7 +53,12 @@ Raster Support
-------------- --------------
``RasterField`` is currently only implemented for the PostGIS backend. Spatial ``RasterField`` is currently only implemented for the PostGIS backend. Spatial
queries (such as lookups and distance) are not yet available for raster fields. lookups are available for raster fields, but spatial database functions and
aggregates aren't implemented for raster fields.
.. versionchanged:: 1.10
``RasterField`` now supports spatial lookups.
Creating and Saving Models with Geometry Fields Creating and Saving Models with Geometry Fields
=============================================== ===============================================
@ -136,11 +141,20 @@ Spatial Lookups
GeoDjango's lookup types may be used with any manager method like GeoDjango's lookup types may be used with any manager method like
``filter()``, ``exclude()``, etc. However, the lookup types unique to ``filter()``, ``exclude()``, etc. However, the lookup types unique to
GeoDjango are only available on geometry fields. GeoDjango are only available on spatial fields.
Filters on 'normal' fields (e.g. :class:`~django.db.models.CharField`) Filters on 'normal' fields (e.g. :class:`~django.db.models.CharField`)
may be chained with those on geographic fields. Thus, geographic queries may be chained with those on geographic fields. Geographic lookups accept
take the following general form (assuming the ``Zipcode`` model used in the geometry and raster input on both sides and input types can be mixed freely.
:doc:`model-api`)::
The general structure of geographic lookups is described below. A complete
reference can be found in the :ref:`spatial lookup reference<spatial-lookups>`.
Geometry Lookups
----------------
Geographic queries with geometries take the following general form (assuming
the ``Zipcode`` model used in the :doc:`model-api`)::
>>> qs = Zipcode.objects.filter(<field>__<lookup_type>=<parameter>) >>> qs = Zipcode.objects.filter(<field>__<lookup_type>=<parameter>)
>>> qs = Zipcode.objects.exclude(...) >>> qs = Zipcode.objects.exclude(...)
@ -148,14 +162,60 @@ take the following general form (assuming the ``Zipcode`` model used in the
For example:: For example::
>>> qs = Zipcode.objects.filter(poly__contains=pnt) >>> qs = Zipcode.objects.filter(poly__contains=pnt)
>>> qs = Elevation.objects.filter(poly__contains=rst)
In this case, ``poly`` is the geographic field, :lookup:`contains <gis-contains>` In this case, ``poly`` is the geographic field, :lookup:`contains <gis-contains>`
is the spatial lookup type, and ``pnt`` is the parameter (which may be a is the spatial lookup type, ``pnt`` is the parameter (which may be a
:class:`~django.contrib.gis.geos.GEOSGeometry` object or a string of :class:`~django.contrib.gis.geos.GEOSGeometry` object or a string of
GeoJSON , WKT, or HEXEWKB). GeoJSON , WKT, or HEXEWKB), and ``rst`` is a
:class:`~django.contrib.gis.gdal.GDALRaster` object.
A complete reference can be found in the :ref:`spatial lookup reference .. _spatial-lookup-raster:
<spatial-lookups>`.
Raster Lookups
--------------
.. versionadded:: 1.10
The raster lookup syntax is similar to the syntax for geometries. The only
difference is that a band index can specified as additional input. If no band
index is specified, the first band is used by default (index ``0``). In that
case the syntax is identical to the syntax for geometry lookups.
To specify the band index, an additional parameter can be specified on both
sides of the lookup. On the left hand side, the double underscore syntax is
used to pass a band index. On the right hand side, a tuple of the raster and
band index can be specified.
This results in the following general form for lookups involving rasters
(assuming the ``Elevation`` model used in the :doc:`model-api`)::
>>> qs = Elevation.objects.filter(<field>__<lookup_type>=<parameter>)
>>> qs = Elevation.objects.filter(<field>__<band_index>__<lookup_type>=<parameter>)
>>> qs = Elevation.objects.filter(<field>__<lookup_type>=(<raster_input, <band_index>)
Fore example::
>>> qs = Elevation.objects.filter(rast__contains=geom)
>>> qs = Elevation.objects.filter(rast__contains=rst)
>>> qs = Elevation.objects.filter(rast__1__contains=geom)
>>> qs = Elevation.objects.filter(rast__contains=(rst, 1))
>>> qs = Elevation.objects.filter(rast__1__contains=(rst, 1))
On the left hand side of the example, ``rast`` is the geographic raster field
and :lookup:`contains <gis-contains>` is the spatial lookup type. On the right
hand side, ``geom`` is a geometry input and ``rst`` is a
:class:`~django.contrib.gis.gdal.GDALRaster` object. The band index defaults to
``0`` in the first two queries and is set to ``1`` on the others.
While all spatial lookups can be used with raster objects on both sides, not all
underlying operators natively accept raster input. For cases where the operator
expects geometry input, the raster is automatically converted to a geometry.
It's important to keep this in mind when interpreting the lookup results.
The type of raster support is listed for all lookups in the :ref:`compatibility
table <spatial-lookup-compatibility>`. Lookups involving rasters are currently
only available for the PostGIS backend.
.. _distance-queries: .. _distance-queries:
@ -176,7 +236,7 @@ in the :doc:`model-api` documentation for more details.
Distance Lookups Distance Lookups
---------------- ----------------
*Availability*: PostGIS, Oracle, SpatiaLite *Availability*: PostGIS, Oracle, SpatiaLite, PGRaster (Native)
The following distance lookups are available: The following distance lookups are available:
@ -193,7 +253,7 @@ The following distance lookups are available:
Distance lookups take a tuple parameter comprising: Distance lookups take a tuple parameter comprising:
#. A geometry to base calculations from; and #. A geometry or raster to base calculations from; and
#. A number or :class:`~django.contrib.gis.measure.Distance` object containing the distance. #. A number or :class:`~django.contrib.gis.measure.Distance` object containing the distance.
If a :class:`~django.contrib.gis.measure.Distance` object is used, If a :class:`~django.contrib.gis.measure.Distance` object is used,
@ -241,6 +301,16 @@ Then distance queries may be performed as follows::
>>> qs = SouthTexasCity.objects.filter(point__distance_gte=(pnt, D(mi=20))) >>> qs = SouthTexasCity.objects.filter(point__distance_gte=(pnt, D(mi=20)))
>>> qs = SouthTexasCity.objects.filter(point__distance_gte=(pnt, D(chain=100))) >>> qs = SouthTexasCity.objects.filter(point__distance_gte=(pnt, D(chain=100)))
Raster queries work the same way by simply replacing the geometry field
``point`` with a raster field, or the ``pnt`` object with a raster object, or
both. To specify the band index of a raster input on the right hand side, a
3-tuple can be passed to the lookup as follows::
>>> qs = SouthTexasCity.objects.filter(point__distance_gte=(rst, 2, D(km=7)))
Where the band with index 2 (the third band) of the raster ``rst`` would be
used for the lookup.
__ https://github.com/django/django/blob/master/tests/gis_tests/distapp/models.py __ https://github.com/django/django/blob/master/tests/gis_tests/distapp/models.py
.. _compatibility-table: .. _compatibility-table:
@ -254,43 +324,46 @@ Spatial Lookups
--------------- ---------------
The following table provides a summary of what spatial lookups are available The following table provides a summary of what spatial lookups are available
for each spatial database backend. for each spatial database backend. The PostGIS Raster (PGRaster) lookups are
divided into the three categories described in the :ref:`raster lookup details
<spatial-lookup-raster>`: native support ``N``, bilateral native support ``B``,
and geometry conversion support ``C``.
================================= ========= ======== ============ ========== ================================= ========= ======== ============ ========== ========
Lookup Type PostGIS Oracle MySQL [#]_ SpatiaLite Lookup Type PostGIS Oracle MySQL [#]_ SpatiaLite PGRaster
================================= ========= ======== ============ ========== ================================= ========= ======== ============ ========== ========
:lookup:`bbcontains` X X X :lookup:`bbcontains` X X X N
:lookup:`bboverlaps` X X X :lookup:`bboverlaps` X X X N
:lookup:`contained` X X X :lookup:`contained` X X X N
:lookup:`contains <gis-contains>` X X X X :lookup:`contains <gis-contains>` X X X X B
:lookup:`contains_properly` X :lookup:`contains_properly` X B
:lookup:`coveredby` X X :lookup:`coveredby` X X B
:lookup:`covers` X X :lookup:`covers` X X B
:lookup:`crosses` X X :lookup:`crosses` X X C
:lookup:`disjoint` X X X X :lookup:`disjoint` X X X X B
:lookup:`distance_gt` X X X :lookup:`distance_gt` X X X N
:lookup:`distance_gte` X X X :lookup:`distance_gte` X X X N
:lookup:`distance_lt` X X X :lookup:`distance_lt` X X X N
:lookup:`distance_lte` X X X :lookup:`distance_lte` X X X N
:lookup:`dwithin` X X :lookup:`dwithin` X X B
:lookup:`equals` X X X X :lookup:`equals` X X X X C
:lookup:`exact` X X X X :lookup:`exact` X X X X B
:lookup:`intersects` X X X X :lookup:`intersects` X X X X B
:lookup:`isvalid` X :lookup:`isvalid` X
:lookup:`overlaps` X X X X :lookup:`overlaps` X X X X B
:lookup:`relate` X X X :lookup:`relate` X X X C
:lookup:`same_as` X X X X :lookup:`same_as` X X X X B
:lookup:`touches` X X X X :lookup:`touches` X X X X B
:lookup:`within` X X X X :lookup:`within` X X X X B
:lookup:`left` X :lookup:`left` X C
:lookup:`right` X :lookup:`right` X C
:lookup:`overlaps_left` X :lookup:`overlaps_left` X B
:lookup:`overlaps_right` X :lookup:`overlaps_right` X B
:lookup:`overlaps_above` X :lookup:`overlaps_above` X C
:lookup:`overlaps_below` X :lookup:`overlaps_below` X C
:lookup:`strictly_above` X :lookup:`strictly_above` X C
:lookup:`strictly_below` X :lookup:`strictly_below` X C
================================= ========= ======== ============ ========== ================================= ========= ======== ============ ========== ========
.. _database-functions-compatibility: .. _database-functions-compatibility:

View File

@ -11,22 +11,70 @@ GeoQuerySet API Reference
Spatial Lookups Spatial Lookups
=============== ===============
The spatial lookups in this section are available for :class:`GeometryField`. The spatial lookups in this section are available for :class:`GeometryField`
and :class:`RasterField`.
For an introduction, see the :ref:`spatial lookups introduction For an introduction, see the :ref:`spatial lookups introduction
<spatial-lookups-intro>`. For an overview of what lookups are <spatial-lookups-intro>`. For an overview of what lookups are
compatible with a particular spatial backend, refer to the compatible with a particular spatial backend, refer to the
:ref:`spatial lookup compatibility table <spatial-lookup-compatibility>`. :ref:`spatial lookup compatibility table <spatial-lookup-compatibility>`.
.. versionchanged:: 1.10
Spatial lookups now support raster input.
Lookups with rasters
--------------------
All examples in the reference below are given for geometry fields and inputs,
but the lookups can be used the same way with rasters on both sides. Whenever
a lookup doesn't support raster input, the input is automatically
converted to a geometry where necessary using the `ST_Polygon
<http://postgis.net/docs/RT_ST_Polygon.html>`_ function. See also the
:ref:`introduction to raster lookups <spatial-lookup-raster>`.
The database operators used by the lookups can be divided into three categories:
- Native raster support ``N``: the operator accepts rasters natively on both
sides of the lookup, and raster input can be mixed with geometry inputs.
- Bilateral raster support ``B``: the operator supports rasters only if both
sides of the lookup receive raster inputs. Raster data is automatically
converted to geometries for mixed lookups.
- Geometry conversion support ``C``. The lookup does not have native raster
support, all raster data is automatically converted to geometries.
The examples below show the SQL equivalent for the lookups in the different
types of raster support. The same pattern applies to all spatial lookups.
==== ============================== =======================================================
Case Lookup SQL Equivalent
==== ============================== =======================================================
N, B ``rast__contains=rst`` ``ST_Contains(rast, rst)``
N, B ``rast__1__contains=(rst, 2)`` ``ST_Contains(rast, 1, rst, 2)``
B, C ``rast__contains=geom`` ``ST_Contains(ST_Polygon(rast), geom)``
B, C ``rast__1__contains=geom`` ``ST_Contains(ST_Polygon(rast, 1), geom)``
B, C ``poly__contains=rst`` ``ST_Contains(poly, ST_Polygon(rst))``
B, C ``poly__contains=(rst, 1)`` ``ST_Contains(poly, ST_Polygon(rst, 1))``
C ``rast__crosses=rst`` ``ST_Crosses(ST_Polygon(rast), ST_Polygon(rst))``
C ``rast__1__crosses=(rst, 2)`` ``ST_Crosses(ST_Polygon(rast, 1), ST_Polygon(rst, 2))``
C ``rast__crosses=geom`` ``ST_Crosses(ST_Polygon(rast), geom)``
C ``poly__crosses=rst`` ``ST_Crosses(poly, ST_Polygon(rst))``
==== ============================== =======================================================
Spatial lookups with rasters are only supported for PostGIS backends
(denominated as PGRaster in this section).
.. fieldlookup:: bbcontains .. fieldlookup:: bbcontains
``bbcontains`` ``bbcontains``
-------------- --------------
*Availability*: PostGIS, MySQL, SpatiaLite *Availability*: PostGIS, MySQL, SpatiaLite, PGRaster (Native)
Tests if the geometry field's bounding box completely contains the lookup Tests if the geometry or raster field's bounding box completely contains the
geometry's bounding box. lookup geometry's bounding box.
Example:: Example::
@ -45,7 +93,7 @@ SpatiaLite ``MbrContains(poly, geom)``
``bboverlaps`` ``bboverlaps``
-------------- --------------
*Availability*: PostGIS, MySQL, SpatiaLite *Availability*: PostGIS, MySQL, SpatiaLite, PGRaster (Native)
Tests if the geometry field's bounding box overlaps the lookup geometry's Tests if the geometry field's bounding box overlaps the lookup geometry's
bounding box. bounding box.
@ -67,7 +115,7 @@ SpatiaLite ``MbrOverlaps(poly, geom)``
``contained`` ``contained``
------------- -------------
*Availability*: PostGIS, MySQL, SpatiaLite *Availability*: PostGIS, MySQL, SpatiaLite, PGRaster (Native)
Tests if the geometry field's bounding box is completely contained by the Tests if the geometry field's bounding box is completely contained by the
lookup geometry's bounding box. lookup geometry's bounding box.
@ -89,7 +137,7 @@ SpatiaLite ``MbrWithin(poly, geom)``
``contains`` ``contains``
------------ ------------
*Availability*: PostGIS, Oracle, MySQL, SpatiaLite *Availability*: PostGIS, Oracle, MySQL, SpatiaLite, PGRaster (Bilateral)
Tests if the geometry field spatially contains the lookup geometry. Tests if the geometry field spatially contains the lookup geometry.
@ -111,7 +159,7 @@ SpatiaLite ``Contains(poly, geom)``
``contains_properly`` ``contains_properly``
--------------------- ---------------------
*Availability*: PostGIS *Availability*: PostGIS, PGRaster (Bilateral)
Returns true if the lookup geometry intersects the interior of the Returns true if the lookup geometry intersects the interior of the
geometry field, but not the boundary (or exterior). [#fncontainsproperly]_ geometry field, but not the boundary (or exterior). [#fncontainsproperly]_
@ -131,7 +179,7 @@ PostGIS ``ST_ContainsProperly(poly, geom)``
``coveredby`` ``coveredby``
------------- -------------
*Availability*: PostGIS, Oracle *Availability*: PostGIS, Oracle, PGRaster (Bilateral)
Tests if no point in the geometry field is outside the lookup geometry. Tests if no point in the geometry field is outside the lookup geometry.
[#fncovers]_ [#fncovers]_
@ -152,7 +200,7 @@ Oracle ``SDO_COVEREDBY(poly, geom)``
``covers`` ``covers``
---------- ----------
*Availability*: PostGIS, Oracle *Availability*: PostGIS, Oracle, PGRaster (Bilateral)
Tests if no point in the lookup geometry is outside the geometry field. Tests if no point in the lookup geometry is outside the geometry field.
[#fncovers]_ [#fncovers]_
@ -173,7 +221,7 @@ Oracle ``SDO_COVERS(poly, geom)``
``crosses`` ``crosses``
----------- -----------
*Availability*: PostGIS, SpatiaLite *Availability*: PostGIS, SpatiaLite, PGRaster (Conversion)
Tests if the geometry field spatially crosses the lookup geometry. Tests if the geometry field spatially crosses the lookup geometry.
@ -193,7 +241,7 @@ SpatiaLite ``Crosses(poly, geom)``
``disjoint`` ``disjoint``
------------ ------------
*Availability*: PostGIS, Oracle, MySQL, SpatiaLite *Availability*: PostGIS, Oracle, MySQL, SpatiaLite, PGRaster (Bilateral)
Tests if the geometry field is spatially disjoint from the lookup geometry. Tests if the geometry field is spatially disjoint from the lookup geometry.
@ -215,7 +263,7 @@ SpatiaLite ``Disjoint(poly, geom)``
``equals`` ``equals``
---------- ----------
*Availability*: PostGIS, Oracle, MySQL, SpatiaLite *Availability*: PostGIS, Oracle, MySQL, SpatiaLite, PGRaster (Conversion)
.. fieldlookup:: exact .. fieldlookup:: exact
.. fieldlookup:: same_as .. fieldlookup:: same_as
@ -223,14 +271,14 @@ SpatiaLite ``Disjoint(poly, geom)``
``exact``, ``same_as`` ``exact``, ``same_as``
---------------------- ----------------------
*Availability*: PostGIS, Oracle, MySQL, SpatiaLite *Availability*: PostGIS, Oracle, MySQL, SpatiaLite, PGRaster (Bilateral)
.. fieldlookup:: intersects .. fieldlookup:: intersects
``intersects`` ``intersects``
-------------- --------------
*Availability*: PostGIS, Oracle, MySQL, SpatiaLite *Availability*: PostGIS, Oracle, MySQL, SpatiaLite, PGRaster (Bilateral)
Tests if the geometry field spatially intersects the lookup geometry. Tests if the geometry field spatially intersects the lookup geometry.
@ -271,14 +319,14 @@ PostGIS equivalent::
``overlaps`` ``overlaps``
------------ ------------
*Availability*: PostGIS, Oracle, MySQL, SpatiaLite *Availability*: PostGIS, Oracle, MySQL, SpatiaLite, PGRaster (Bilateral)
.. fieldlookup:: relate .. fieldlookup:: relate
``relate`` ``relate``
---------- ----------
*Availability*: PostGIS, Oracle, SpatiaLite *Availability*: PostGIS, Oracle, SpatiaLite, PGRaster (Conversion)
Tests if the geometry field is spatially related to the lookup geometry by Tests if the geometry field is spatially related to the lookup geometry by
the values given in the given pattern. This lookup requires a tuple parameter, the values given in the given pattern. This lookup requires a tuple parameter,
@ -293,7 +341,7 @@ The intersection pattern matrix may only use the following characters:
``1``, ``2``, ``T``, ``F``, or ``*``. This lookup type allows users to "fine tune" ``1``, ``2``, ``T``, ``F``, or ``*``. This lookup type allows users to "fine tune"
a specific geometric relationship consistent with the DE-9IM model. [#fnde9im]_ a specific geometric relationship consistent with the DE-9IM model. [#fnde9im]_
Example:: Geometry example::
# A tuple lookup parameter is used to specify the geometry and # A tuple lookup parameter is used to specify the geometry and
# the intersection pattern (the pattern here is for 'contains'). # the intersection pattern (the pattern here is for 'contains').
@ -307,6 +355,16 @@ SpatiaLite SQL equivalent::
SELECT ... WHERE Relate(poly, geom, 'T*T***FF*') SELECT ... WHERE Relate(poly, geom, 'T*T***FF*')
Raster example::
Zipcode.objects.filter(poly__relate=(rast, 1, 'T*T***FF*'))
Zipcode.objects.filter(rast__2__relate=(rast, 1, 'T*T***FF*'))
PostGIS SQL equivalent::
SELECT ... WHERE ST_Relate(poly, ST_Polygon(rast, 1), 'T*T***FF*')
SELECT ... WHERE ST_Relate(ST_Polygon(rast, 2), ST_Polygon(rast, 1), 'T*T***FF*')
Oracle Oracle
~~~~~~ ~~~~~~
@ -352,7 +410,7 @@ SpatiaLite ``Touches(poly, geom)``
``within`` ``within``
---------- ----------
*Availability*: PostGIS, Oracle, MySQL, SpatiaLite *Availability*: PostGIS, Oracle, MySQL, SpatiaLite, PGRaster (Bilateral)
Tests if the geometry field is spatially within the lookup geometry. Tests if the geometry field is spatially within the lookup geometry.
@ -374,7 +432,7 @@ SpatiaLite ``Within(poly, geom)``
``left`` ``left``
-------- --------
*Availability*: PostGIS *Availability*: PostGIS, PGRaster (Conversion)
Tests if the geometry field's bounding box is strictly to the left of the Tests if the geometry field's bounding box is strictly to the left of the
lookup geometry's bounding box. lookup geometry's bounding box.
@ -392,7 +450,7 @@ PostGIS equivalent::
``right`` ``right``
--------- ---------
*Availability*: PostGIS *Availability*: PostGIS, PGRaster (Conversion)
Tests if the geometry field's bounding box is strictly to the right of the Tests if the geometry field's bounding box is strictly to the right of the
lookup geometry's bounding box. lookup geometry's bounding box.
@ -410,7 +468,7 @@ PostGIS equivalent::
``overlaps_left`` ``overlaps_left``
----------------- -----------------
*Availability*: PostGIS *Availability*: PostGIS, PGRaster (Bilateral)
Tests if the geometry field's bounding box overlaps or is to the left of the lookup Tests if the geometry field's bounding box overlaps or is to the left of the lookup
geometry's bounding box. geometry's bounding box.
@ -429,7 +487,7 @@ PostGIS equivalent::
``overlaps_right`` ``overlaps_right``
------------------ ------------------
*Availability*: PostGIS *Availability*: PostGIS, PGRaster (Bilateral)
Tests if the geometry field's bounding box overlaps or is to the right of the lookup Tests if the geometry field's bounding box overlaps or is to the right of the lookup
geometry's bounding box. geometry's bounding box.
@ -447,7 +505,7 @@ PostGIS equivalent::
``overlaps_above`` ``overlaps_above``
------------------ ------------------
*Availability*: PostGIS *Availability*: PostGIS, PGRaster (Conversion)
Tests if the geometry field's bounding box overlaps or is above the lookup Tests if the geometry field's bounding box overlaps or is above the lookup
geometry's bounding box. geometry's bounding box.
@ -465,7 +523,7 @@ PostGIS equivalent::
``overlaps_below`` ``overlaps_below``
------------------ ------------------
*Availability*: PostGIS *Availability*: PostGIS, PGRaster (Conversion)
Tests if the geometry field's bounding box overlaps or is below the lookup Tests if the geometry field's bounding box overlaps or is below the lookup
geometry's bounding box. geometry's bounding box.
@ -483,7 +541,7 @@ PostGIS equivalent::
``strictly_above`` ``strictly_above``
------------------ ------------------
*Availability*: PostGIS *Availability*: PostGIS, PGRaster (Conversion)
Tests if the geometry field's bounding box is strictly above the lookup Tests if the geometry field's bounding box is strictly above the lookup
geometry's bounding box. geometry's bounding box.
@ -501,7 +559,7 @@ PostGIS equivalent::
``strictly_below`` ``strictly_below``
------------------ ------------------
*Availability*: PostGIS *Availability*: PostGIS, PGRaster (Conversion)
Tests if the geometry field's bounding box is strictly below the lookup Tests if the geometry field's bounding box is strictly below the lookup
geometry's bounding box. geometry's bounding box.
@ -520,27 +578,31 @@ PostGIS equivalent::
Distance Lookups Distance Lookups
================ ================
*Availability*: PostGIS, Oracle, SpatiaLite *Availability*: PostGIS, Oracle, SpatiaLite, PGRaster (Native)
For an overview on performing distance queries, please refer to For an overview on performing distance queries, please refer to
the :ref:`distance queries introduction <distance-queries>`. the :ref:`distance queries introduction <distance-queries>`.
Distance lookups take the following form:: Distance lookups take the following form::
<field>__<distance lookup>=(<geometry>, <distance value>[, 'spheroid']) <field>__<distance lookup>=(<geometry/raster>, <distance value>[, 'spheroid'])
<field>__<distance lookup>=(<raster>, <band_index>, <distance value>[, 'spheroid'])
<field>__<band_index>__<distance lookup>=(<raster>, <band_index>, <distance value>[, 'spheroid'])
The value passed into a distance lookup is a tuple; the first two The value passed into a distance lookup is a tuple; the first two
values are mandatory, and are the geometry to calculate distances to, values are mandatory, and are the geometry to calculate distances to,
and a distance value (either a number in units of the field, a and a distance value (either a number in units of the field, a
:class:`~django.contrib.gis.measure.Distance` object, or a `query expression :class:`~django.contrib.gis.measure.Distance` object, or a `query expression
<ref/models/expressions>`). <ref/models/expressions>`). To pass a band index to the lookup, use a 3-tuple
where the second entry is the band index.
With PostGIS, on every distance lookup but :lookup:`dwithin`, an optional With PostGIS, on every distance lookup but :lookup:`dwithin`, an optional
third element, ``'spheroid'``, may be included to tell GeoDjango element, ``'spheroid'``, may be included to tell GeoDjango to use the more
to use the more accurate spheroid distance calculation functions on accurate spheroid distance calculation functions on fields with a geodetic
fields with a geodetic coordinate system (e.g., ``ST_Distance_Spheroid`` coordinate system (e.g., ``ST_Distance_Spheroid`` would be used instead of
would be used instead of ``ST_Distance_Sphere``). The simpler ``ST_Distance`` ``ST_Distance_Sphere``). The simpler ``ST_Distance`` function is used with
function is used with projected coordinate systems. projected coordinate systems. Rasters are converted to geometries for spheroid
based lookups.
.. versionadded:: 1.10 .. versionadded:: 1.10

View File

@ -160,6 +160,9 @@ Minor features
:lookup:`isvalid` lookup, all for PostGIS. This allows filtering and :lookup:`isvalid` lookup, all for PostGIS. This allows filtering and
repairing invalid geometries on the database side. repairing invalid geometries on the database side.
* Added raster support for all :doc:`spatial lookups
</ref/contrib/gis/geoquerysets>`.
:mod:`django.contrib.messages` :mod:`django.contrib.messages`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -558,6 +558,7 @@ pessimization
Petri Petri
Peucker Peucker
pgAdmin pgAdmin
PGRaster
phishing phishing
php php
picklable picklable

View File

@ -82,9 +82,9 @@ class OperationTests(TransactionTestCase):
operation = migration_class(*args) operation = migration_class(*args)
new_state = project_state.clone() new_state = project_state.clone()
operation.state_forwards('gis', new_state) operation.state_forwards('gis', new_state)
self.current_state = new_state
with connection.schema_editor() as editor: with connection.schema_editor() as editor:
operation.database_forwards('gis', editor, project_state, new_state) operation.database_forwards('gis', editor, project_state, new_state)
self.current_state = new_state
def test_add_geom_field(self): def test_add_geom_field(self):
""" """

View File

@ -3,6 +3,18 @@ from ..models import models
class RasterModel(models.Model): class RasterModel(models.Model):
rast = models.RasterField('A Verbose Raster Name', null=True, srid=4326, spatial_index=True, blank=True) rast = models.RasterField('A Verbose Raster Name', null=True, srid=4326, spatial_index=True, blank=True)
rastprojected = models.RasterField('A Projected Raster Table', srid=3086, null=True)
geom = models.PointField(null=True)
class Meta:
required_db_features = ['supports_raster']
def __str__(self):
return str(self.id)
class RasterRelatedModel(models.Model):
rastermodel = models.ForeignKey(RasterModel, models.CASCADE)
class Meta: class Meta:
required_db_features = ['supports_raster'] required_db_features = ['supports_raster']

View File

@ -1,20 +1,48 @@
import json import json
from django.contrib.gis.db.models.lookups import (
DistanceLookupBase, gis_lookups,
)
from django.contrib.gis.gdal import HAS_GDAL
from django.contrib.gis.geos import GEOSGeometry
from django.contrib.gis.measure import D
from django.contrib.gis.shortcuts import numpy from django.contrib.gis.shortcuts import numpy
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.db.models import Q
from django.test import ( from django.test import (
TestCase, TransactionTestCase, mock, skipUnlessDBFeature, TestCase, TransactionTestCase, mock, skipUnlessDBFeature,
) )
from ..data.rasters.textrasters import JSON_RASTER from ..data.rasters.textrasters import JSON_RASTER
from ..models import models from ..models import models
from .models import RasterModel from .models import RasterModel, RasterRelatedModel
if HAS_GDAL:
from django.contrib.gis.gdal import GDALRaster
@skipUnlessDBFeature('supports_raster') @skipUnlessDBFeature('supports_raster')
class RasterFieldTest(TransactionTestCase): class RasterFieldTest(TransactionTestCase):
available_apps = ['gis_tests.rasterapp'] available_apps = ['gis_tests.rasterapp']
def setUp(self):
rast = GDALRaster({
"srid": 4326,
"origin": [0, 0],
"scale": [-1, 1],
"skew": [0, 0],
"width": 5,
"height": 5,
"nr_of_bands": 2,
"bands": [{"data": range(25)}, {"data": range(25, 50)}],
})
model_instance = RasterModel.objects.create(
rast=rast,
rastprojected=rast,
geom="POINT (-95.37040 29.70486)",
)
RasterRelatedModel.objects.create(rastermodel=model_instance)
def test_field_null_value(self): def test_field_null_value(self):
""" """
Test creating a model where the RasterField has a null value. Test creating a model where the RasterField has a null value.
@ -89,6 +117,220 @@ class RasterFieldTest(TransactionTestCase):
'A Verbose Raster Name' 'A Verbose Raster Name'
) )
def test_all_gis_lookups_with_rasters(self):
"""
Evaluate all possible lookups for all input combinations (i.e.
raster-raster, raster-geom, geom-raster) and for projected and
unprojected coordinate systems. This test just checks that the lookup
can be called, but doesn't check if the result makes logical sense.
"""
from django.contrib.gis.db.backends.postgis.operations import PostGISOperations
# Create test raster and geom.
rast = GDALRaster(json.loads(JSON_RASTER))
stx_pnt = GEOSGeometry('POINT (-95.370401017314293 29.704867409475465)', 4326)
stx_pnt.transform(3086)
# Loop through all the GIS lookups.
for name, lookup in gis_lookups.items():
# Construct lookup filter strings.
combo_keys = [
field + name for field in [
'rast__', 'rast__', 'rastprojected__0__', 'rast__',
'rastprojected__', 'geom__', 'rast__',
]
]
if issubclass(lookup, DistanceLookupBase):
# Set lookup values for distance lookups.
combo_values = [
(rast, 50, 'spheroid'),
(rast, 0, 50, 'spheroid'),
(rast, 0, D(km=1)),
(stx_pnt, 0, 500),
(stx_pnt, D(km=1000)),
(rast, 500),
(json.loads(JSON_RASTER), 500),
]
elif name == 'relate':
# Set lookup values for the relate lookup.
combo_values = [
(rast, 'T*T***FF*'),
(rast, 0, 'T*T***FF*'),
(rast, 0, 'T*T***FF*'),
(stx_pnt, 0, 'T*T***FF*'),
(stx_pnt, 'T*T***FF*'),
(rast, 'T*T***FF*'),
(json.loads(JSON_RASTER), 'T*T***FF*'),
]
elif name == 'isvalid':
# The isvalid lookup doesn't make sense for rasters.
continue
elif PostGISOperations.gis_operators[name].func:
# Set lookup values for all function based operators.
combo_values = [
rast, (rast, 0), (rast, 0), (stx_pnt, 0), stx_pnt,
rast, rast, json.loads(JSON_RASTER)
]
else:
# Override band lookup for these, as it's not supported.
combo_keys[2] = 'rastprojected__' + name
# Set lookup values for all other operators.
combo_values = [rast, rast, rast, stx_pnt, stx_pnt, rast, rast, json.loads(JSON_RASTER)]
# Create query filter combinations.
combos = [{x[0]: x[1]} for x in zip(combo_keys, combo_values)]
for combo in combos:
# Apply this query filter.
qs = RasterModel.objects.filter(**combo)
# Evaluate normal filter qs.
self.assertTrue(qs.count() in [0, 1])
# Evaluate on conditional Q expressions.
qs = RasterModel.objects.filter(Q(**combos[0]) & Q(**combos[1]))
self.assertTrue(qs.count() in [0, 1])
def test_dwithin_gis_lookup_ouptut_with_rasters(self):
"""
Check the logical functionality of the dwithin lookup for different
input parameters.
"""
# Create test raster and geom.
rast = GDALRaster(json.loads(JSON_RASTER))
stx_pnt = GEOSGeometry('POINT (-95.370401017314293 29.704867409475465)', 4326)
stx_pnt.transform(3086)
# Filter raster with different lookup raster formats.
qs = RasterModel.objects.filter(rastprojected__dwithin=(rast, D(km=1)))
self.assertEqual(qs.count(), 1)
qs = RasterModel.objects.filter(rastprojected__dwithin=(json.loads(JSON_RASTER), D(km=1)))
self.assertEqual(qs.count(), 1)
qs = RasterModel.objects.filter(rastprojected__dwithin=(JSON_RASTER, D(km=1)))
self.assertEqual(qs.count(), 1)
# Filter in an unprojected coordinate system.
qs = RasterModel.objects.filter(rast__dwithin=(rast, 40))
self.assertEqual(qs.count(), 1)
# Filter with band index transform.
qs = RasterModel.objects.filter(rast__1__dwithin=(rast, 1, 40))
self.assertEqual(qs.count(), 1)
qs = RasterModel.objects.filter(rast__1__dwithin=(rast, 40))
self.assertEqual(qs.count(), 1)
qs = RasterModel.objects.filter(rast__dwithin=(rast, 1, 40))
self.assertEqual(qs.count(), 1)
# Filter raster by geom.
qs = RasterModel.objects.filter(rast__dwithin=(stx_pnt, 500))
self.assertEqual(qs.count(), 1)
qs = RasterModel.objects.filter(rastprojected__dwithin=(stx_pnt, D(km=10000)))
self.assertEqual(qs.count(), 1)
qs = RasterModel.objects.filter(rast__dwithin=(stx_pnt, 5))
self.assertEqual(qs.count(), 0)
qs = RasterModel.objects.filter(rastprojected__dwithin=(stx_pnt, D(km=100)))
self.assertEqual(qs.count(), 0)
# Filter geom by raster.
qs = RasterModel.objects.filter(geom__dwithin=(rast, 500))
self.assertEqual(qs.count(), 1)
# Filter through related model.
qs = RasterRelatedModel.objects.filter(rastermodel__rast__dwithin=(rast, 40))
self.assertEqual(qs.count(), 1)
# Filter through related model with band index transform
qs = RasterRelatedModel.objects.filter(rastermodel__rast__1__dwithin=(rast, 40))
self.assertEqual(qs.count(), 1)
# Filter through conditional statements.
qs = RasterModel.objects.filter(Q(rast__dwithin=(rast, 40)) & Q(rastprojected__dwithin=(stx_pnt, D(km=10000))))
self.assertEqual(qs.count(), 1)
# Filter through different lookup.
qs = RasterModel.objects.filter(rastprojected__bbcontains=rast)
self.assertEqual(qs.count(), 1)
def test_lookup_input_tuple_too_long(self):
rast = GDALRaster(json.loads(JSON_RASTER))
qs = RasterModel.objects.filter(rast__bbcontains=(rast, 1, 2))
msg = 'Tuple too long for lookup bbcontains.'
with self.assertRaisesMessage(ValueError, msg):
qs.count()
def test_lookup_input_band_not_allowed(self):
rast = GDALRaster(json.loads(JSON_RASTER))
qs = RasterModel.objects.filter(rast__bbcontains=(rast, 1))
msg = 'Band indices are not allowed for this operator, it works on bbox only.'
with self.assertRaisesMessage(ValueError, msg):
qs.count()
def test_isvalid_lookup_with_raster_error(self):
qs = RasterModel.objects.filter(rast__isvalid=True)
msg = 'The isvalid lookup is only available on geometry fields.'
with self.assertRaisesMessage(ValueError, msg):
qs.count()
def test_result_of_gis_lookup_with_rasters(self):
# Point is in the interior
qs = RasterModel.objects.filter(rast__contains=GEOSGeometry('POINT (-0.5 0.5)', 4326))
self.assertEqual(qs.count(), 1)
# Point is in the exterior
qs = RasterModel.objects.filter(rast__contains=GEOSGeometry('POINT (0.5 0.5)', 4326))
self.assertEqual(qs.count(), 0)
# A point on the boundary is not contained properly
qs = RasterModel.objects.filter(rast__contains_properly=GEOSGeometry('POINT (0 0)', 4326))
self.assertEqual(qs.count(), 0)
# Raster is located left of the point
qs = RasterModel.objects.filter(rast__left=GEOSGeometry('POINT (1 0)', 4326))
self.assertEqual(qs.count(), 1)
def test_lookup_with_raster_bbox(self):
rast = GDALRaster(json.loads(JSON_RASTER))
# Shift raster upwards
rast.origin.y = 2
# The raster in the model is not strictly below
qs = RasterModel.objects.filter(rast__strictly_below=rast)
self.assertEqual(qs.count(), 0)
# Shift raster further upwards
rast.origin.y = 6
# The raster in the model is strictly below
qs = RasterModel.objects.filter(rast__strictly_below=rast)
self.assertEqual(qs.count(), 1)
def test_lookup_with_polygonized_raster(self):
rast = GDALRaster(json.loads(JSON_RASTER))
# Move raster to overlap with the model point on the left side
rast.origin.x = -95.37040 + 1
rast.origin.y = 29.70486
# Raster overlaps with point in model
qs = RasterModel.objects.filter(geom__intersects=rast)
self.assertEqual(qs.count(), 1)
# Change left side of raster to be nodata values
rast.bands[0].data(data=[0, 0, 0, 1, 1], shape=(5, 1))
rast.bands[0].nodata_value = 0
qs = RasterModel.objects.filter(geom__intersects=rast)
# Raster does not overlap anymore after polygonization
# where the nodata zone is not included.
self.assertEqual(qs.count(), 0)
def test_lookup_value_error(self):
# Test with invalid dict lookup parameter
obj = dict()
msg = "Couldn't create spatial object from lookup value '%s'." % obj
with self.assertRaisesMessage(ValueError, msg):
RasterModel.objects.filter(geom__intersects=obj)
# Test with invalid string lookup parameter
obj = '00000'
msg = "Couldn't create spatial object from lookup value '%s'." % obj
with self.assertRaisesMessage(ValueError, msg):
RasterModel.objects.filter(geom__intersects=obj)
@mock.patch('django.contrib.gis.db.models.fields.HAS_GDAL', False) @mock.patch('django.contrib.gis.db.models.fields.HAS_GDAL', False)
class RasterFieldWithoutGDALTest(TestCase): class RasterFieldWithoutGDALTest(TestCase):