Add Django-1.2.1
[frozenviper.git] / Django-1.2.1 / build / lib.linux-i686-2.6 / django / contrib / gis / db / backends / postgis / operations.py
blobb1a9e315292efed124debe513cf119f082ca662b
1 import re
2 from decimal import Decimal
4 from django.conf import settings
5 from django.contrib.gis.db.backends.base import BaseSpatialOperations
6 from django.contrib.gis.db.backends.util import SpatialOperation, SpatialFunction
7 from django.contrib.gis.db.backends.postgis.adapter import PostGISAdapter
8 from django.contrib.gis.geometry.backend import Geometry
9 from django.contrib.gis.measure import Distance
10 from django.core.exceptions import ImproperlyConfigured
11 from django.db.backends.postgresql_psycopg2.base import DatabaseOperations
12 from django.db.utils import DatabaseError
14 #### Classes used in constructing PostGIS spatial SQL ####
15 class PostGISOperator(SpatialOperation):
16 "For PostGIS operators (e.g. `&&`, `~`)."
17 def __init__(self, operator):
18 super(PostGISOperator, self).__init__(operator=operator)
20 class PostGISFunction(SpatialFunction):
21 "For PostGIS function calls (e.g., `ST_Contains(table, geom)`)."
22 def __init__(self, prefix, function, **kwargs):
23 super(PostGISFunction, self).__init__(prefix + function, **kwargs)
25 class PostGISFunctionParam(PostGISFunction):
26 "For PostGIS functions that take another parameter (e.g. DWithin, Relate)."
27 sql_template = '%(function)s(%(geo_col)s, %(geometry)s, %%s)'
29 class PostGISDistance(PostGISFunction):
30 "For PostGIS distance operations."
31 dist_func = 'Distance'
32 sql_template = '%(function)s(%(geo_col)s, %(geometry)s) %(operator)s %%s'
34 def __init__(self, prefix, operator):
35 super(PostGISDistance, self).__init__(prefix, self.dist_func,
36 operator=operator)
38 class PostGISSpheroidDistance(PostGISFunction):
39 "For PostGIS spherical distance operations (using the spheroid)."
40 dist_func = 'distance_spheroid'
41 sql_template = '%(function)s(%(geo_col)s, %(geometry)s, %%s) %(operator)s %%s'
42 def __init__(self, prefix, operator):
43 # An extra parameter in `end_subst` is needed for the spheroid string.
44 super(PostGISSpheroidDistance, self).__init__(prefix, self.dist_func,
45 operator=operator)
47 class PostGISSphereDistance(PostGISDistance):
48 "For PostGIS spherical distance operations."
49 dist_func = 'distance_sphere'
51 class PostGISRelate(PostGISFunctionParam):
52 "For PostGIS Relate(<geom>, <pattern>) calls."
53 pattern_regex = re.compile(r'^[012TF\*]{9}$')
54 def __init__(self, prefix, pattern):
55 if not self.pattern_regex.match(pattern):
56 raise ValueError('Invalid intersection matrix pattern "%s".' % pattern)
57 super(PostGISRelate, self).__init__(prefix, 'Relate')
60 class PostGISOperations(DatabaseOperations, BaseSpatialOperations):
61 compiler_module = 'django.contrib.gis.db.models.sql.compiler'
62 name = 'postgis'
63 postgis = True
64 version_regex = re.compile(r'^(?P<major>\d)\.(?P<minor1>\d)\.(?P<minor2>\d+)')
65 valid_aggregates = dict([(k, None) for k in
66 ('Collect', 'Extent', 'Extent3D', 'MakeLine', 'Union')])
68 Adapter = PostGISAdapter
69 Adaptor = Adapter # Backwards-compatibility alias.
71 def __init__(self, connection):
72 super(PostGISOperations, self).__init__(connection)
74 # Trying to get the PostGIS version because the function
75 # signatures will depend on the version used. The cost
76 # here is a database query to determine the version, which
77 # can be mitigated by setting `POSTGIS_VERSION` with a 3-tuple
78 # comprising user-supplied values for the major, minor, and
79 # subminor revision of PostGIS.
80 try:
81 if hasattr(settings, 'POSTGIS_VERSION'):
82 vtup = settings.POSTGIS_VERSION
83 if len(vtup) == 3:
84 # The user-supplied PostGIS version.
85 version = vtup
86 else:
87 # This was the old documented way, but it's stupid to
88 # include the string.
89 version = vtup[1:4]
90 else:
91 vtup = self.postgis_version_tuple()
92 version = vtup[1:]
94 # Getting the prefix -- even though we don't officially support
95 # PostGIS 1.2 anymore, keeping it anyway in case a prefix change
96 # for something else is necessary.
97 if version >= (1, 2, 2):
98 prefix = 'ST_'
99 else:
100 prefix = ''
102 self.geom_func_prefix = prefix
103 self.spatial_version = version
104 except DatabaseError:
105 raise ImproperlyConfigured('Cannot determine PostGIS version for database "%s". '
106 'GeoDjango requires at least PostGIS version 1.3. '
107 'Was the database created from a spatial database '
108 'template?' % self.connection.settings_dict['NAME']
110 except Exception, e:
111 # TODO: Raise helpful exceptions as they become known.
112 raise
114 # PostGIS-specific operators. The commented descriptions of these
115 # operators come from Section 7.6 of the PostGIS 1.4 documentation.
116 self.geometry_operators = {
117 # The "&<" operator returns true if A's bounding box overlaps or
118 # is to the left of B's bounding box.
119 'overlaps_left' : PostGISOperator('&<'),
120 # The "&>" operator returns true if A's bounding box overlaps or
121 # is to the right of B's bounding box.
122 'overlaps_right' : PostGISOperator('&>'),
123 # The "<<" operator returns true if A's bounding box is strictly
124 # to the left of B's bounding box.
125 'left' : PostGISOperator('<<'),
126 # The ">>" operator returns true if A's bounding box is strictly
127 # to the right of B's bounding box.
128 'right' : PostGISOperator('>>'),
129 # The "&<|" operator returns true if A's bounding box overlaps or
130 # is below B's bounding box.
131 'overlaps_below' : PostGISOperator('&<|'),
132 # The "|&>" operator returns true if A's bounding box overlaps or
133 # is above B's bounding box.
134 'overlaps_above' : PostGISOperator('|&>'),
135 # The "<<|" operator returns true if A's bounding box is strictly
136 # below B's bounding box.
137 'strictly_below' : PostGISOperator('<<|'),
138 # The "|>>" operator returns true if A's bounding box is strictly
139 # above B's bounding box.
140 'strictly_above' : PostGISOperator('|>>'),
141 # The "~=" operator is the "same as" operator. It tests actual
142 # geometric equality of two features. So if A and B are the same feature,
143 # vertex-by-vertex, the operator returns true.
144 'same_as' : PostGISOperator('~='),
145 'exact' : PostGISOperator('~='),
146 # The "@" operator returns true if A's bounding box is completely contained
147 # by B's bounding box.
148 'contained' : PostGISOperator('@'),
149 # The "~" operator returns true if A's bounding box completely contains
150 # by B's bounding box.
151 'bbcontains' : PostGISOperator('~'),
152 # The "&&" operator returns true if A's bounding box overlaps
153 # B's bounding box.
154 'bboverlaps' : PostGISOperator('&&'),
157 self.geometry_functions = {
158 'equals' : PostGISFunction(prefix, 'Equals'),
159 'disjoint' : PostGISFunction(prefix, 'Disjoint'),
160 'touches' : PostGISFunction(prefix, 'Touches'),
161 'crosses' : PostGISFunction(prefix, 'Crosses'),
162 'within' : PostGISFunction(prefix, 'Within'),
163 'overlaps' : PostGISFunction(prefix, 'Overlaps'),
164 'contains' : PostGISFunction(prefix, 'Contains'),
165 'intersects' : PostGISFunction(prefix, 'Intersects'),
166 'relate' : (PostGISRelate, basestring),
169 # Valid distance types and substitutions
170 dtypes = (Decimal, Distance, float, int, long)
171 def get_dist_ops(operator):
172 "Returns operations for both regular and spherical distances."
173 return {'cartesian' : PostGISDistance(prefix, operator),
174 'sphere' : PostGISSphereDistance(prefix, operator),
175 'spheroid' : PostGISSpheroidDistance(prefix, operator),
177 self.distance_functions = {
178 'distance_gt' : (get_dist_ops('>'), dtypes),
179 'distance_gte' : (get_dist_ops('>='), dtypes),
180 'distance_lt' : (get_dist_ops('<'), dtypes),
181 'distance_lte' : (get_dist_ops('<='), dtypes),
184 # Versions 1.2.2+ have KML serialization support.
185 if version < (1, 2, 2):
186 ASKML = False
187 else:
188 ASKML = 'ST_AsKML'
189 self.geometry_functions.update(
190 {'coveredby' : PostGISFunction(prefix, 'CoveredBy'),
191 'covers' : PostGISFunction(prefix, 'Covers'),
193 self.distance_functions['dwithin'] = (PostGISFunctionParam(prefix, 'DWithin'), dtypes)
195 # Adding the distance functions to the geometries lookup.
196 self.geometry_functions.update(self.distance_functions)
198 # The union aggregate and topology operation use the same signature
199 # in versions 1.3+.
200 if version < (1, 3, 0):
201 UNIONAGG = 'GeomUnion'
202 UNION = 'Union'
203 MAKELINE = False
204 else:
205 UNIONAGG = 'ST_Union'
206 UNION = 'ST_Union'
207 MAKELINE = 'ST_MakeLine'
209 # Only PostGIS versions 1.3.4+ have GeoJSON serialization support.
210 if version < (1, 3, 4):
211 GEOJSON = False
212 else:
213 GEOJSON = prefix + 'AsGeoJson'
215 # ST_ContainsProperly ST_MakeLine, and ST_GeoHash added in 1.4.
216 if version >= (1, 4, 0):
217 GEOHASH = 'ST_GeoHash'
218 BOUNDINGCIRCLE = 'ST_MinimumBoundingCircle'
219 self.geometry_functions['contains_properly'] = PostGISFunction(prefix, 'ContainsProperly')
220 else:
221 GEOHASH, BOUNDINGCIRCLE = False, False
223 # Geography type support added in 1.5.
224 if version >= (1, 5, 0):
225 self.geography = True
226 # Only a subset of the operators and functions are available
227 # for the geography type.
228 self.geography_functions = self.distance_functions.copy()
229 self.geography_functions.update({
230 'coveredby' : self.geometry_functions['coveredby'],
231 'covers' : self.geometry_functions['covers'],
232 'intersects' : self.geometry_functions['intersects'],
234 self.geography_operators = {
235 'bboverlaps' : PostGISOperator('&&'),
236 'exact' : PostGISOperator('~='),
237 'same_as' : PostGISOperator('~='),
240 # Creating a dictionary lookup of all GIS terms for PostGIS.
241 gis_terms = ['isnull']
242 gis_terms += self.geometry_operators.keys()
243 gis_terms += self.geometry_functions.keys()
244 self.gis_terms = dict([(term, None) for term in gis_terms])
246 self.area = prefix + 'Area'
247 self.bounding_circle = BOUNDINGCIRCLE
248 self.centroid = prefix + 'Centroid'
249 self.collect = prefix + 'Collect'
250 self.difference = prefix + 'Difference'
251 self.distance = prefix + 'Distance'
252 self.distance_sphere = prefix + 'distance_sphere'
253 self.distance_spheroid = prefix + 'distance_spheroid'
254 self.envelope = prefix + 'Envelope'
255 self.extent = prefix + 'Extent'
256 self.extent3d = prefix + 'Extent3D'
257 self.force_rhr = prefix + 'ForceRHR'
258 self.geohash = GEOHASH
259 self.geojson = GEOJSON
260 self.gml = prefix + 'AsGML'
261 self.intersection = prefix + 'Intersection'
262 self.kml = ASKML
263 self.length = prefix + 'Length'
264 self.length3d = prefix + 'Length3D'
265 self.length_spheroid = prefix + 'length_spheroid'
266 self.makeline = MAKELINE
267 self.mem_size = prefix + 'mem_size'
268 self.num_geom = prefix + 'NumGeometries'
269 self.num_points =prefix + 'npoints'
270 self.perimeter = prefix + 'Perimeter'
271 self.perimeter3d = prefix + 'Perimeter3D'
272 self.point_on_surface = prefix + 'PointOnSurface'
273 self.polygonize = prefix + 'Polygonize'
274 self.reverse = prefix + 'Reverse'
275 self.scale = prefix + 'Scale'
276 self.snap_to_grid = prefix + 'SnapToGrid'
277 self.svg = prefix + 'AsSVG'
278 self.sym_difference = prefix + 'SymDifference'
279 self.transform = prefix + 'Transform'
280 self.translate = prefix + 'Translate'
281 self.union = UNION
282 self.unionagg = UNIONAGG
284 def check_aggregate_support(self, aggregate):
286 Checks if the given aggregate name is supported (that is, if it's
287 in `self.valid_aggregates`).
289 agg_name = aggregate.__class__.__name__
290 return agg_name in self.valid_aggregates
292 def convert_extent(self, box):
294 Returns a 4-tuple extent for the `Extent` aggregate by converting
295 the bounding box text returned by PostGIS (`box` argument), for
296 example: "BOX(-90.0 30.0, -85.0 40.0)".
298 ll, ur = box[4:-1].split(',')
299 xmin, ymin = map(float, ll.split())
300 xmax, ymax = map(float, ur.split())
301 return (xmin, ymin, xmax, ymax)
303 def convert_extent3d(self, box3d):
305 Returns a 6-tuple extent for the `Extent3D` aggregate by converting
306 the 3d bounding-box text returnded by PostGIS (`box3d` argument), for
307 example: "BOX3D(-90.0 30.0 1, -85.0 40.0 2)".
309 ll, ur = box3d[6:-1].split(',')
310 xmin, ymin, zmin = map(float, ll.split())
311 xmax, ymax, zmax = map(float, ur.split())
312 return (xmin, ymin, zmin, xmax, ymax, zmax)
314 def convert_geom(self, hex, geo_field):
316 Converts the geometry returned from PostGIS aggretates.
318 if hex:
319 return Geometry(hex)
320 else:
321 return None
323 def geo_db_type(self, f):
325 Return the database field type for the given geometry field.
326 Typically this is `None` because geometry columns are added via
327 the `AddGeometryColumn` stored procedure, unless the field
328 has been specified to be of geography type instead.
330 if f.geography:
331 if not self.geography:
332 raise NotImplementedError('PostGIS 1.5 required for geography column support.')
334 if f.srid != 4326:
335 raise NotImplementedError('PostGIS 1.5 supports geography columns '
336 'only with an SRID of 4326.')
338 return 'geography(%s,%d)'% (f.geom_type, f.srid)
339 else:
340 return None
342 def get_distance(self, f, dist_val, lookup_type):
344 Retrieve the distance parameters for the given geometry field,
345 distance lookup value, and the distance lookup type.
347 This is the most complex implementation of the spatial backends due to
348 what is supported on geodetic geometry columns vs. what's available on
349 projected geometry columns. In addition, it has to take into account
350 the newly introduced geography column type introudced in PostGIS 1.5.
352 # Getting the distance parameter and any options.
353 if len(dist_val) == 1:
354 value, option = dist_val[0], None
355 else:
356 value, option = dist_val
358 # Shorthand boolean flags.
359 geodetic = f.geodetic(self.connection)
360 geography = f.geography and self.geography
362 if isinstance(value, Distance):
363 if geography:
364 dist_param = value.m
365 elif geodetic:
366 if lookup_type == 'dwithin':
367 raise ValueError('Only numeric values of degree units are '
368 'allowed on geographic DWithin queries.')
369 dist_param = value.m
370 else:
371 dist_param = getattr(value, Distance.unit_attname(f.units_name(self.connection)))
372 else:
373 # Assuming the distance is in the units of the field.
374 dist_param = value
376 if (not geography and geodetic and lookup_type != 'dwithin'
377 and option == 'spheroid'):
378 # using distance_spheroid requires the spheroid of the field as
379 # a parameter.
380 return [f._spheroid, dist_param]
381 else:
382 return [dist_param]
384 def get_geom_placeholder(self, f, value):
386 Provides a proper substitution value for Geometries that are not in the
387 SRID of the field. Specifically, this routine will substitute in the
388 ST_Transform() function call.
390 if value is None or value.srid == f.srid:
391 placeholder = '%s'
392 else:
393 # Adding Transform() to the SQL placeholder.
394 placeholder = '%s(%%s, %s)' % (self.transform, f.srid)
396 if hasattr(value, 'expression'):
397 # If this is an F expression, then we don't really want
398 # a placeholder and instead substitute in the column
399 # of the expression.
400 placeholder = placeholder % '%s.%s' % tuple(map(self.quote_name, value.cols[value.expression]))
402 return placeholder
404 def _get_postgis_func(self, func):
406 Helper routine for calling PostGIS functions and returning their result.
408 cursor = self.connection._cursor()
409 try:
410 try:
411 cursor.execute('SELECT %s()' % func)
412 row = cursor.fetchone()
413 except:
414 # Responsibility of callers to perform error handling.
415 raise
416 finally:
417 # Close out the connection. See #9437.
418 self.connection.close()
419 return row[0]
421 def postgis_geos_version(self):
422 "Returns the version of the GEOS library used with PostGIS."
423 return self._get_postgis_func('postgis_geos_version')
425 def postgis_lib_version(self):
426 "Returns the version number of the PostGIS library used with PostgreSQL."
427 return self._get_postgis_func('postgis_lib_version')
429 def postgis_proj_version(self):
430 "Returns the version of the PROJ.4 library used with PostGIS."
431 return self._get_postgis_func('postgis_proj_version')
433 def postgis_version(self):
434 "Returns PostGIS version number and compile-time options."
435 return self._get_postgis_func('postgis_version')
437 def postgis_full_version(self):
438 "Returns PostGIS version number and compile-time options."
439 return self._get_postgis_func('postgis_full_version')
441 def postgis_version_tuple(self):
443 Returns the PostGIS version as a tuple (version string, major,
444 minor, subminor).
446 # Getting the PostGIS version
447 version = self.postgis_lib_version()
448 m = self.version_regex.match(version)
450 if m:
451 major = int(m.group('major'))
452 minor1 = int(m.group('minor1'))
453 minor2 = int(m.group('minor2'))
454 else:
455 raise Exception('Could not parse PostGIS version string: %s' % version)
457 return (version, major, minor1, minor2)
459 def proj_version_tuple(self):
461 Return the version of PROJ.4 used by PostGIS as a tuple of the
462 major, minor, and subminor release numbers.
464 proj_regex = re.compile(r'(\d+)\.(\d+)\.(\d+)')
465 proj_ver_str = self.postgis_proj_version()
466 m = proj_regex.search(proj_ver_str)
467 if m:
468 return tuple(map(int, [m.group(1), m.group(2), m.group(3)]))
469 else:
470 raise Exception('Could not determine PROJ.4 version from PostGIS.')
472 def num_params(self, lookup_type, num_param):
474 Helper routine that returns a boolean indicating whether the number of
475 parameters is correct for the lookup type.
477 def exactly_two(np): return np == 2
478 def two_to_three(np): return np >= 2 and np <=3
479 if (lookup_type in self.distance_functions and
480 lookup_type != 'dwithin'):
481 return two_to_three(num_param)
482 else:
483 return exactly_two(num_param)
485 def spatial_lookup_sql(self, lvalue, lookup_type, value, field, qn):
487 Constructs spatial SQL from the given lookup value tuple a
488 (alias, col, db_type), the lookup type string, lookup value, and
489 the geometry field.
491 alias, col, db_type = lvalue
493 # Getting the quoted geometry column.
494 geo_col = '%s.%s' % (qn(alias), qn(col))
496 if lookup_type in self.geometry_operators:
497 if field.geography and not lookup_type in self.geography_operators:
498 raise ValueError('PostGIS geography does not support the '
499 '"%s" lookup.' % lookup_type)
500 # Handling a PostGIS operator.
501 op = self.geometry_operators[lookup_type]
502 return op.as_sql(geo_col, self.get_geom_placeholder(field, value))
503 elif lookup_type in self.geometry_functions:
504 if field.geography and not lookup_type in self.geography_functions:
505 raise ValueError('PostGIS geography type does not support the '
506 '"%s" lookup.' % lookup_type)
508 # See if a PostGIS geometry function matches the lookup type.
509 tmp = self.geometry_functions[lookup_type]
511 # Lookup types that are tuples take tuple arguments, e.g., 'relate' and
512 # distance lookups.
513 if isinstance(tmp, tuple):
514 # First element of tuple is the PostGISOperation instance, and the
515 # second element is either the type or a tuple of acceptable types
516 # that may passed in as further parameters for the lookup type.
517 op, arg_type = tmp
519 # Ensuring that a tuple _value_ was passed in from the user
520 if not isinstance(value, (tuple, list)):
521 raise ValueError('Tuple required for `%s` lookup type.' % lookup_type)
523 # Geometry is first element of lookup tuple.
524 geom = value[0]
526 # Number of valid tuple parameters depends on the lookup type.
527 nparams = len(value)
528 if not self.num_params(lookup_type, nparams):
529 raise ValueError('Incorrect number of parameters given for `%s` lookup type.' % lookup_type)
531 # Ensuring the argument type matches what we expect.
532 if not isinstance(value[1], arg_type):
533 raise ValueError('Argument type should be %s, got %s instead.' % (arg_type, type(value[1])))
535 # For lookup type `relate`, the op instance is not yet created (has
536 # to be instantiated here to check the pattern parameter).
537 if lookup_type == 'relate':
538 op = op(self.geom_func_prefix, value[1])
539 elif lookup_type in self.distance_functions and lookup_type != 'dwithin':
540 if not field.geography and field.geodetic(self.connection):
541 # Geodetic distances are only availble from Points to
542 # PointFields on PostGIS 1.4 and below.
543 if not self.connection.ops.geography:
544 if field.geom_type != 'POINT':
545 raise ValueError('PostGIS spherical operations are only valid on PointFields.')
547 if str(geom.geom_type) != 'Point':
548 raise ValueError('PostGIS geometry distance parameter is required to be of type Point.')
550 # Setting up the geodetic operation appropriately.
551 if nparams == 3 and value[2] == 'spheroid':
552 op = op['spheroid']
553 else:
554 op = op['sphere']
555 else:
556 op = op['cartesian']
557 else:
558 op = tmp
559 geom = value
561 # Calling the `as_sql` function on the operation instance.
562 return op.as_sql(geo_col, self.get_geom_placeholder(field, geom))
564 elif lookup_type == 'isnull':
565 # Handling 'isnull' lookup type
566 return "%s IS %sNULL" % (geo_col, (not value and 'NOT ' or ''))
568 raise TypeError("Got invalid lookup_type: %s" % repr(lookup_type))
570 def spatial_aggregate_sql(self, agg):
572 Returns the spatial aggregate SQL template and function for the
573 given Aggregate instance.
575 agg_name = agg.__class__.__name__
576 if not self.check_aggregate_support(agg):
577 raise NotImplementedError('%s spatial aggregate is not implmented for this backend.' % agg_name)
578 agg_name = agg_name.lower()
579 if agg_name == 'union': agg_name += 'agg'
580 sql_template = '%(function)s(%(field)s)'
581 sql_function = getattr(self, agg_name)
582 return sql_template, sql_function
584 # Routines for getting the OGC-compliant models.
585 def geometry_columns(self):
586 from django.contrib.gis.db.backends.postgis.models import GeometryColumns
587 return GeometryColumns
589 def spatial_ref_sys(self):
590 from django.contrib.gis.db.backends.postgis.models import SpatialRefSys
591 return SpatialRefSys