diff options
Diffstat (limited to 'django/contrib/gis/db/backends/postgis/operations.py')
-rw-r--r-- | django/contrib/gis/db/backends/postgis/operations.py | 444 |
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 |