mirror of https://github.com/django/django.git
Fixed #25588 -- Added spatial lookups to RasterField.
Thanks Tim Graham for the review.
This commit is contained in:
parent
03efa304bc
commit
bbfad84dd9
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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'):
|
||||||
|
|
|
@ -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:
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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`
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -558,6 +558,7 @@ pessimization
|
||||||
Petri
|
Petri
|
||||||
Peucker
|
Peucker
|
||||||
pgAdmin
|
pgAdmin
|
||||||
|
PGRaster
|
||||||
phishing
|
phishing
|
||||||
php
|
php
|
||||||
picklable
|
picklable
|
||||||
|
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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']
|
||||||
|
|
|
@ -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):
|
||||||
|
|
Loading…
Reference in New Issue