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
,
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
,
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'
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.
81 if hasattr(settings
, 'POSTGIS_VERSION'):
82 vtup
= settings
.POSTGIS_VERSION
84 # The user-supplied PostGIS version.
87 # This was the old documented way, but it's stupid to
91 vtup
= self
.postgis_version_tuple()
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):
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']
111 # TODO: Raise helpful exceptions as they become known.
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
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):
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
200 if version
< (1, 3, 0):
201 UNIONAGG
= 'GeomUnion'
205 UNIONAGG
= 'ST_Union'
207 MAKELINE
= 'ST_MakeLine'
209 # Only PostGIS versions 1.3.4+ have GeoJSON serialization support.
210 if version
< (1, 3, 4):
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')
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'
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'
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.
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.
331 if not self
.geography
:
332 raise NotImplementedError('PostGIS 1.5 required for geography column support.')
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
)
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
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
):
366 if lookup_type
== 'dwithin':
367 raise ValueError('Only numeric values of degree units are '
368 'allowed on geographic DWithin queries.')
371 dist_param
= getattr(value
, Distance
.unit_attname(f
.units_name(self
.connection
)))
373 # Assuming the distance is in the units of the field.
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
380 return [f
._spheroid
, 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
:
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
400 placeholder
= placeholder
% '%s.%s' % tuple(map(self
.quote_name
, value
.cols
[value
.expression
]))
404 def _get_postgis_func(self
, func
):
406 Helper routine for calling PostGIS functions and returning their result.
408 cursor
= self
.connection
._cursor
()
411 cursor
.execute('SELECT %s()' % func
)
412 row
= cursor
.fetchone()
414 # Responsibility of callers to perform error handling.
417 # Close out the connection. See #9437.
418 self
.connection
.close()
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,
446 # Getting the PostGIS version
447 version
= self
.postgis_lib_version()
448 m
= self
.version_regex
.match(version
)
451 major
= int(m
.group('major'))
452 minor1
= int(m
.group('minor1'))
453 minor2
= int(m
.group('minor2'))
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
)
468 return tuple(map(int, [m
.group(1), m
.group(2), m
.group(3)]))
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
)
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
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
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.
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.
526 # Number of valid tuple parameters depends on the lookup type.
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':
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