summaryrefslogtreecommitdiff
path: root/django/contrib/gis/db/backends/postgis/operations.py
diff options
context:
space:
mode:
Diffstat (limited to 'django/contrib/gis/db/backends/postgis/operations.py')
-rw-r--r--django/contrib/gis/db/backends/postgis/operations.py444
1 files changed, 444 insertions, 0 deletions
diff --git a/django/contrib/gis/db/backends/postgis/operations.py b/django/contrib/gis/db/backends/postgis/operations.py
new file mode 100644
index 0000000000..4cc0aec256
--- /dev/null
+++ b/django/contrib/gis/db/backends/postgis/operations.py
@@ -0,0 +1,444 @@
+import re
+from decimal import Decimal
+
+from django.db.backends.postgresql.operations import DatabaseOperations
+from django.contrib.gis.db.backends.base import BaseSpatialOperations
+from django.contrib.gis.db.backends.util import SpatialOperation, SpatialFunction
+from django.contrib.gis.db.backends.postgis.adapter import PostGISAdapter
+from django.contrib.gis.geometry import Geometry
+from django.contrib.gis.measure import Distance
+
+#### Classes used in constructing PostGIS spatial SQL ####
+class PostGISOperator(SpatialOperation):
+ "For PostGIS operators (e.g. `&&`, `~`)."
+ def __init__(self, operator):
+ super(PostGISOperator, self).__init__(operator=operator)
+
+class PostGISFunction(SpatialFunction):
+ "For PostGIS function calls (e.g., `ST_Contains(table, geom)`)."
+ def __init__(self, prefix, function, **kwargs):
+ super(PostGISFunction, self).__init__(prefix + function, **kwargs)
+
+class PostGISFunctionParam(PostGISFunction):
+ "For PostGIS functions that take another parameter (e.g. DWithin, Relate)."
+ sql_template = '%(function)s(%(geo_col)s, %(geometry)s, %%s)'
+
+class PostGISDistance(PostGISFunction):
+ "For PostGIS distance operations."
+ dist_func = 'Distance'
+ sql_template = '%(function)s(%(geo_col)s, %(geometry)s) %(operator)s %%s'
+
+ def __init__(self, prefix, operator):
+ super(PostGISDistance, self).__init__(prefix, self.dist_func,
+ operator=operator)
+
+class PostGISSpheroidDistance(PostGISFunction):
+ "For PostGIS spherical distance operations (using the spheroid)."
+ dist_func = 'distance_spheroid'
+ sql_template = '%(function)s(%(geo_col)s, %(geometry)s, %%s) %(operator)s %%s'
+ def __init__(self, prefix, operator):
+ # An extra parameter in `end_subst` is needed for the spheroid string.
+ super(PostGISSpheroidDistance, self).__init__(prefix, self.dist_func,
+ operator=operator)
+
+class PostGISSphereDistance(PostGISDistance):
+ "For PostGIS spherical distance operations."
+ dist_func = 'distance_sphere'
+
+class PostGISRelate(PostGISFunctionParam):
+ "For PostGIS Relate(<geom>, <pattern>) calls."
+ pattern_regex = re.compile(r'^[012TF\*]{9}$')
+ def __init__(self, prefix, pattern):
+ if not self.pattern_regex.match(pattern):
+ raise ValueError('Invalid intersection matrix pattern "%s".' % pattern)
+ super(PostGISRelate, self).__init__(prefix, 'Relate')
+
+
+class PostGISOperations(DatabaseOperations, BaseSpatialOperations):
+ compiler_module = 'django.contrib.gis.db.models.sql.compiler'
+ name = 'postgis'
+ postgis = True
+ version_regex = re.compile(r'^(?P<major>\d)\.(?P<minor1>\d)\.(?P<minor2>\d+)')
+ valid_aggregates = dict([(k, None) for k in
+ ('Collect', 'Extent', 'Extent3D', 'MakeLine', 'Union')])
+
+ Adapter = PostGISAdapter
+
+ def __init__(self, connection):
+ super(PostGISOperations, self).__init__(connection)
+
+ # Trying to get the PostGIS version because the function
+ # signatures will depend on the version used.
+ try:
+ vtup = self.postgis_version_tuple()
+ version = vtup[1:]
+ if version >= (1, 2, 2):
+ prefix = 'ST_'
+ else:
+ prefix = ''
+ self.geom_func_prefix = prefix
+ self.spatial_version = version
+ except Exception, e:
+ # TODO: Plain raising right now.
+ raise
+
+ # PostGIS-specific operators. The commented descriptions of these
+ # operators come from Section 7.6 of the PostGIS 1.4 documentation.
+ self.spatial_operators = {
+ # The "&<" operator returns true if A's bounding box overlaps or
+ # is to the left of B's bounding box.
+ 'overlaps_left' : PostGISOperator('&<'),
+ # The "&>" operator returns true if A's bounding box overlaps or
+ # is to the right of B's bounding box.
+ 'overlaps_right' : PostGISOperator('&>'),
+ # The "<<" operator returns true if A's bounding box is strictly
+ # to the left of B's bounding box.
+ 'left' : PostGISOperator('<<'),
+ # The ">>" operator returns true if A's bounding box is strictly
+ # to the right of B's bounding box.
+ 'right' : PostGISOperator('>>'),
+ # The "&<|" operator returns true if A's bounding box overlaps or
+ # is below B's bounding box.
+ 'overlaps_below' : PostGISOperator('&<|'),
+ # The "|&>" operator returns true if A's bounding box overlaps or
+ # is above B's bounding box.
+ 'overlaps_above' : PostGISOperator('|&>'),
+ # The "<<|" operator returns true if A's bounding box is strictly
+ # below B's bounding box.
+ 'strictly_below' : PostGISOperator('<<|'),
+ # The "|>>" operator returns true if A's bounding box is strictly
+ # above B's bounding box.
+ 'strictly_above' : PostGISOperator('|>>'),
+ # The "~=" operator is the "same as" operator. It tests actual
+ # geometric equality of two features. So if A and B are the same feature,
+ # vertex-by-vertex, the operator returns true.
+ 'same_as' : PostGISOperator('~='),
+ 'exact' : PostGISOperator('~='),
+ # The "@" operator returns true if A's bounding box is completely contained
+ # by B's bounding box.
+ 'contained' : PostGISOperator('@'),
+ # The "~" operator returns true if A's bounding box completely contains
+ # by B's bounding box.
+ 'bbcontains' : PostGISOperator('~'),
+ # The "&&" operator returns true if A's bounding box overlaps
+ # B's bounding box.
+ 'bboverlaps' : PostGISOperator('&&'),
+ }
+
+ self.geometry_functions = {
+ 'equals' : PostGISFunction(prefix, 'Equals'),
+ 'disjoint' : PostGISFunction(prefix, 'Disjoint'),
+ 'touches' : PostGISFunction(prefix, 'Touches'),
+ 'crosses' : PostGISFunction(prefix, 'Crosses'),
+ 'within' : PostGISFunction(prefix, 'Within'),
+ 'overlaps' : PostGISFunction(prefix, 'Overlaps'),
+ 'contains' : PostGISFunction(prefix, 'Contains'),
+ 'intersects' : PostGISFunction(prefix, 'Intersects'),
+ 'relate' : (PostGISRelate, basestring),
+ }
+
+ # Valid distance types and substitutions
+ dtypes = (Decimal, Distance, float, int, long)
+ def get_dist_ops(operator):
+ "Returns operations for both regular and spherical distances."
+ return {'cartesian' : PostGISDistance(prefix, operator),
+ 'sphere' : PostGISSphereDistance(prefix, operator),
+ 'spheroid' : PostGISSpheroidDistance(prefix, operator),
+ }
+ self.distance_functions = {
+ 'distance_gt' : (get_dist_ops('>'), dtypes),
+ 'distance_gte' : (get_dist_ops('>='), dtypes),
+ 'distance_lt' : (get_dist_ops('<'), dtypes),
+ 'distance_lte' : (get_dist_ops('<='), dtypes),
+ }
+
+ # Versions 1.2.2+ have KML serialization support.
+ if version < (1, 2, 2):
+ ASKML = False
+ else:
+ ASKML = 'ST_AsKML'
+ self.geometry_functions.update(
+ {'coveredby' : PostGISFunction(prefix, 'CoveredBy'),
+ 'covers' : PostGISFunction(prefix, 'Covers'),
+ })
+ self.distance_functions['dwithin'] = (PostGISFunctionParam(prefix, 'DWithin'), dtypes)
+
+ # Adding the distance functions to the geometries lookup.
+ self.geometry_functions.update(self.distance_functions)
+
+ # ST_ContainsProperly and GeoHash serialization added in 1.4.
+ if version >= (1, 4, 0):
+ GEOHASH = 'ST_GeoHash'
+ self.geometry_functions['contains_properly'] = PostGISFunction(prefix, 'ContainsProperly')
+ else:
+ GEOHASH = False
+
+ # Creating a dictionary lookup of all GIS terms for PostGIS.
+ gis_terms = ['isnull']
+ gis_terms += self.spatial_operators.keys()
+ gis_terms += self.geometry_functions.keys()
+ self.gis_terms = dict([(term, None) for term in gis_terms])
+
+ # The union aggregate and topology operation use the same signature
+ # in versions 1.3+.
+ if version < (1, 3, 0):
+ UNIONAGG = 'GeomUnion'
+ UNION = 'Union'
+ else:
+ UNIONAGG = 'ST_Union'
+ UNION = 'ST_Union'
+
+ # Only PostGIS versions 1.3.4+ have GeoJSON serialization support.
+ if version < (1, 3, 4):
+ GEOJSON = False
+ else:
+ GEOJSON = prefix + 'AsGeoJson'
+
+ self.area = prefix + 'Area'
+ self.centroid = prefix + 'Centroid'
+ self.collect = prefix + 'Collect'
+ self.difference = prefix + 'Difference'
+ self.distance = prefix + 'Distance'
+ self.distance_sphere = prefix + 'distance_sphere'
+ self.distance_spheroid = prefix + 'distance_spheroid'
+ self.envelope = prefix + 'Envelope'
+ self.extent = prefix + 'Extent'
+ self.extent3d = prefix + 'Extent3D'
+ self.geohash = GEOHASH
+ self.geojson = GEOJSON
+ self.gml = prefix + 'AsGML'
+ self.intersection = prefix + 'Intersection'
+ self.kml = ASKML
+ self.length = prefix + 'Length'
+ self.length3d = prefix + 'Length3D'
+ self.length_spheroid = prefix + 'length_spheroid'
+ self.makeline = prefix + 'MakeLine'
+ self.mem_size = prefix + 'mem_size'
+ self.num_geom = prefix + 'NumGeometries'
+ self.num_points =prefix + 'npoints'
+ self.perimeter = prefix + 'Perimeter'
+ self.perimeter3d = prefix + 'Perimeter3D'
+ self.point_on_surface = prefix + 'PointOnSurface'
+ self.scale = prefix + 'Scale'
+ self.snap_to_grid = prefix + 'SnapToGrid'
+ self.svg = prefix + 'AsSVG'
+ self.sym_difference = prefix + 'SymDifference'
+ self.transform = prefix + 'Transform'
+ self.translate = prefix + 'Translate'
+ self.union = UNION
+ self.unionagg = UNIONAGG
+
+ def check_aggregate_support(self, aggregate):
+ """
+ Checks if the given aggregate name is supported (that is, if it's
+ in `self.valid_aggregates`).
+ """
+ agg_name = aggregate.__class__.__name__
+ return agg_name in self.valid_aggregates
+
+ def convert_extent(self, box):
+ # Box text will be something like "BOX(-90.0 30.0, -85.0 40.0)";
+ # parsing out and returning as a 4-tuple.
+ ll, ur = box[4:-1].split(',')
+ xmin, ymin = map(float, ll.split())
+ xmax, ymax = map(float, ur.split())
+ return (xmin, ymin, xmax, ymax)
+
+ def convert_extent3d(self, box3d):
+ # Box text will be something like "BOX3D(-90.0 30.0 1, -85.0 40.0 2)";
+ # parsing out and returning as a 4-tuple.
+ ll, ur = box3d[6:-1].split(',')
+ xmin, ymin, zmin = map(float, ll.split())
+ xmax, ymax, zmax = map(float, ur.split())
+ return (xmin, ymin, zmin, xmax, ymax, zmax)
+
+ def convert_geom(self, hex, geo_field):
+ """
+ Converts the geometry returned from PostGIS aggretates.
+ """
+ if hex:
+ return Geometry(hex)
+ else:
+ return None
+
+ def get_geom_placeholder(self, value, srid):
+ """
+ Provides a proper substitution value for Geometries that are not in the
+ SRID of the field. Specifically, this routine will substitute in the
+ ST_Transform() function call.
+ """
+ if value is None or value.srid == srid:
+ placeholder = '%s'
+ else:
+ # Adding Transform() to the SQL placeholder.
+ placeholder = '%s(%%s, %s)' % (self.transform, srid)
+
+ if hasattr(value, 'expression'):
+ # If this is an F expression, then we don't really want
+ # a placeholder and instead substitute in the column
+ # of the expression.
+ placeholder = placeholder % '%s.%s' % tuple(map(self.quote_name, value.cols[value.expression]))
+
+ return placeholder
+
+ def _get_postgis_func(self, func):
+ """
+ Helper routine for calling PostGIS functions and returning their result.
+ """
+ cursor = self.connection._cursor()
+ try:
+ cursor.execute('SELECT %s()' % func)
+ row = cursor.fetchone()
+ except:
+ # TODO: raise helpful exception here.
+ raise
+ finally:
+ cursor.close()
+ return row[0]
+
+ def postgis_geos_version(self):
+ "Returns the version of the GEOS library used with PostGIS."
+ return self._get_postgis_func('postgis_geos_version')
+
+ def postgis_lib_version(self):
+ "Returns the version number of the PostGIS library used with PostgreSQL."
+ return self._get_postgis_func('postgis_lib_version')
+
+ def postgis_proj_version(self):
+ "Returns the version of the PROJ.4 library used with PostGIS."
+ return self._get_postgis_func('postgis_proj_version')
+
+ def postgis_version(self):
+ "Returns PostGIS version number and compile-time options."
+ return self._get_postgis_func('postgis_version')
+
+ def postgis_full_version(self):
+ "Returns PostGIS version number and compile-time options."
+ return self._get_postgis_func('postgis_full_version')
+
+ def postgis_version_tuple(self):
+ """
+ Returns the PostGIS version as a tuple (version string, major,
+ minor, subminor).
+ """
+ # Getting the PostGIS version
+ version = self.postgis_lib_version()
+ m = self.version_regex.match(version)
+
+ if m:
+ major = int(m.group('major'))
+ minor1 = int(m.group('minor1'))
+ minor2 = int(m.group('minor2'))
+ else:
+ raise Exception('Could not parse PostGIS version string: %s' % version)
+
+ return (version, major, minor1, minor2)
+
+ def num_params(self, lookup_type, val):
+ def exactly_two(val): return val == 2
+ def two_to_three(val): return val >= 2 and val <=3
+ if (lookup_type in self.distance_functions and
+ lookup_type != 'dwithin'):
+ return two_to_three(val)
+ else:
+ return exactly_two(val)
+
+ def spatial_lookup_sql(self, lvalue, lookup_type, value, field):
+ """
+ Constructs spatial SQL from the given lookup value tuple a
+ (alias, col, db_type), the lookup type string, lookup value, and
+ the geometry field.
+ """
+ qn = self.quote_name
+ alias, col, db_type = lvalue
+
+ # Getting the quoted geometry column.
+ geo_col = '%s.%s' % (qn(alias), qn(col))
+
+ if lookup_type in self.spatial_operators:
+ # Handling a PostGIS operator.
+ op = self.spatial_operators[lookup_type]
+ return op.as_sql(geo_col, self.get_geom_placeholder(value, field.srid))
+ elif lookup_type in self.geometry_functions:
+ # See if a PostGIS geometry function matches the lookup type.
+ tmp = self.geometry_functions[lookup_type]
+
+ # Lookup types that are tuples take tuple arguments, e.g., 'relate' and
+ # distance lookups.
+ if isinstance(tmp, tuple):
+ # First element of tuple is the PostGISOperation instance, and the
+ # second element is either the type or a tuple of acceptable types
+ # that may passed in as further parameters for the lookup type.
+ op, arg_type = tmp
+
+ # Ensuring that a tuple _value_ was passed in from the user
+ if not isinstance(value, (tuple, list)):
+ raise ValueError('Tuple required for `%s` lookup type.' % lookup_type)
+
+ # Geometry is first element of lookup tuple.
+ geom = value[0]
+
+ # Number of valid tuple parameters depends on the lookup type.
+ nparams = len(value)
+ if not self.num_params(lookup_type, nparams):
+ raise ValueError('Incorrect number of parameters given for `%s` lookup type.' % lookup_type)
+
+ # Ensuring the argument type matches what we expect.
+ if not isinstance(value[1], arg_type):
+ raise ValueError('Argument type should be %s, got %s instead.' % (arg_type, type(value[1])))
+
+ # For lookup type `relate`, the op instance is not yet created (has
+ # to be instantiated here to check the pattern parameter).
+ if lookup_type == 'relate':
+ op = op(self.geom_func_prefix, value[1])
+ elif lookup_type in self.distance_functions and lookup_type != 'dwithin':
+ if field.geodetic(self.connection):
+ # Geodetic distances are only availble from Points to PointFields.
+ if field.geom_type != 'POINT':
+ raise ValueError('PostGIS spherical operations are only valid on PointFields.')
+
+ if str(geom.geom_type) != 'Point':
+ raise ValueError('PostGIS geometry distance parameter is required to be of type Point.')
+
+ # Setting up the geodetic operation appropriately.
+ if nparams == 3 and value[2] == 'spheroid':
+ op = op['spheroid']
+ else:
+ op = op['sphere']
+ else:
+ op = op['cartesian']
+ else:
+ op = tmp
+ geom = value
+
+ # Calling the `as_sql` function on the operation instance.
+ return op.as_sql(geo_col, self.get_geom_placeholder(geom, field.srid))
+
+ elif lookup_type == 'isnull':
+ # Handling 'isnull' lookup type
+ return "%s IS %sNULL" % (geo_col, (not value and 'NOT ' or ''))
+
+ raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type))
+
+ def spatial_aggregate_sql(self, agg):
+ """
+ Returns the spatial aggregate SQL template and function for the
+ given Aggregate instance.
+ """
+ agg_name = agg.__class__.__name__
+ if not self.check_aggregate_support(agg):
+ raise NotImplementedError('%s spatial aggregate is not implmented for this backend.' % agg_name)
+ agg_name = agg_name.lower()
+ if agg_name == 'union': agg_name += 'agg'
+ sql_template = '%(function)s(%(field)s)'
+ sql_function = getattr(self, agg_name)
+ return sql_template, sql_function
+
+ # Routines for getting the OGC-compliant models.
+ def geometry_columns(self):
+ from django.contrib.gis.db.backends.postgis.models import GeometryColumns
+ return GeometryColumns
+
+ def spatial_ref_sys(self):
+ from django.contrib.gis.db.backends.postgis.models import SpatialRefSys
+ return SpatialRefSys