|
1 from django.db import connections |
|
2 from django.db.models.query import QuerySet, Q, ValuesQuerySet, ValuesListQuerySet |
|
3 |
|
4 from django.contrib.gis.db.models import aggregates |
|
5 from django.contrib.gis.db.models.fields import get_srid_info, GeometryField, PointField, LineStringField |
|
6 from django.contrib.gis.db.models.sql import AreaField, DistanceField, GeomField, GeoQuery, GeoWhereNode |
|
7 from django.contrib.gis.geometry.backend import Geometry |
|
8 from django.contrib.gis.measure import Area, Distance |
|
9 |
|
10 class GeoQuerySet(QuerySet): |
|
11 "The Geographic QuerySet." |
|
12 |
|
13 ### Methods overloaded from QuerySet ### |
|
14 def __init__(self, model=None, query=None, using=None): |
|
15 super(GeoQuerySet, self).__init__(model=model, query=query, using=using) |
|
16 self.query = query or GeoQuery(self.model) |
|
17 |
|
18 def values(self, *fields): |
|
19 return self._clone(klass=GeoValuesQuerySet, setup=True, _fields=fields) |
|
20 |
|
21 def values_list(self, *fields, **kwargs): |
|
22 flat = kwargs.pop('flat', False) |
|
23 if kwargs: |
|
24 raise TypeError('Unexpected keyword arguments to values_list: %s' |
|
25 % (kwargs.keys(),)) |
|
26 if flat and len(fields) > 1: |
|
27 raise TypeError("'flat' is not valid when values_list is called with more than one field.") |
|
28 return self._clone(klass=GeoValuesListQuerySet, setup=True, flat=flat, |
|
29 _fields=fields) |
|
30 |
|
31 ### GeoQuerySet Methods ### |
|
32 def area(self, tolerance=0.05, **kwargs): |
|
33 """ |
|
34 Returns the area of the geographic field in an `area` attribute on |
|
35 each element of this GeoQuerySet. |
|
36 """ |
|
37 # Peforming setup here rather than in `_spatial_attribute` so that |
|
38 # we can get the units for `AreaField`. |
|
39 procedure_args, geo_field = self._spatial_setup('area', field_name=kwargs.get('field_name', None)) |
|
40 s = {'procedure_args' : procedure_args, |
|
41 'geo_field' : geo_field, |
|
42 'setup' : False, |
|
43 } |
|
44 connection = connections[self.db] |
|
45 backend = connection.ops |
|
46 if backend.oracle: |
|
47 s['procedure_fmt'] = '%(geo_col)s,%(tolerance)s' |
|
48 s['procedure_args']['tolerance'] = tolerance |
|
49 s['select_field'] = AreaField('sq_m') # Oracle returns area in units of meters. |
|
50 elif backend.postgis or backend.spatialite: |
|
51 if not geo_field.geodetic(connection): |
|
52 # Getting the area units of the geographic field. |
|
53 s['select_field'] = AreaField(Area.unit_attname(geo_field.units_name(connection))) |
|
54 else: |
|
55 # TODO: Do we want to support raw number areas for geodetic fields? |
|
56 raise Exception('Area on geodetic coordinate systems not supported.') |
|
57 return self._spatial_attribute('area', s, **kwargs) |
|
58 |
|
59 def centroid(self, **kwargs): |
|
60 """ |
|
61 Returns the centroid of the geographic field in a `centroid` |
|
62 attribute on each element of this GeoQuerySet. |
|
63 """ |
|
64 return self._geom_attribute('centroid', **kwargs) |
|
65 |
|
66 def collect(self, **kwargs): |
|
67 """ |
|
68 Performs an aggregate collect operation on the given geometry field. |
|
69 This is analagous to a union operation, but much faster because |
|
70 boundaries are not dissolved. |
|
71 """ |
|
72 return self._spatial_aggregate(aggregates.Collect, **kwargs) |
|
73 |
|
74 def difference(self, geom, **kwargs): |
|
75 """ |
|
76 Returns the spatial difference of the geographic field in a `difference` |
|
77 attribute on each element of this GeoQuerySet. |
|
78 """ |
|
79 return self._geomset_attribute('difference', geom, **kwargs) |
|
80 |
|
81 def distance(self, geom, **kwargs): |
|
82 """ |
|
83 Returns the distance from the given geographic field name to the |
|
84 given geometry in a `distance` attribute on each element of the |
|
85 GeoQuerySet. |
|
86 |
|
87 Keyword Arguments: |
|
88 `spheroid` => If the geometry field is geodetic and PostGIS is |
|
89 the spatial database, then the more accurate |
|
90 spheroid calculation will be used instead of the |
|
91 quicker sphere calculation. |
|
92 |
|
93 `tolerance` => Used only for Oracle. The tolerance is |
|
94 in meters -- a default of 5 centimeters (0.05) |
|
95 is used. |
|
96 """ |
|
97 return self._distance_attribute('distance', geom, **kwargs) |
|
98 |
|
99 def envelope(self, **kwargs): |
|
100 """ |
|
101 Returns a Geometry representing the bounding box of the |
|
102 Geometry field in an `envelope` attribute on each element of |
|
103 the GeoQuerySet. |
|
104 """ |
|
105 return self._geom_attribute('envelope', **kwargs) |
|
106 |
|
107 def extent(self, **kwargs): |
|
108 """ |
|
109 Returns the extent (aggregate) of the features in the GeoQuerySet. The |
|
110 extent will be returned as a 4-tuple, consisting of (xmin, ymin, xmax, ymax). |
|
111 """ |
|
112 return self._spatial_aggregate(aggregates.Extent, **kwargs) |
|
113 |
|
114 def extent3d(self, **kwargs): |
|
115 """ |
|
116 Returns the aggregate extent, in 3D, of the features in the |
|
117 GeoQuerySet. It is returned as a 6-tuple, comprising: |
|
118 (xmin, ymin, zmin, xmax, ymax, zmax). |
|
119 """ |
|
120 return self._spatial_aggregate(aggregates.Extent3D, **kwargs) |
|
121 |
|
122 def force_rhr(self, **kwargs): |
|
123 """ |
|
124 Returns a modified version of the Polygon/MultiPolygon in which |
|
125 all of the vertices follow the Right-Hand-Rule. By default, |
|
126 this is attached as the `force_rhr` attribute on each element |
|
127 of the GeoQuerySet. |
|
128 """ |
|
129 return self._geom_attribute('force_rhr', **kwargs) |
|
130 |
|
131 def geojson(self, precision=8, crs=False, bbox=False, **kwargs): |
|
132 """ |
|
133 Returns a GeoJSON representation of the geomtry field in a `geojson` |
|
134 attribute on each element of the GeoQuerySet. |
|
135 |
|
136 The `crs` and `bbox` keywords may be set to True if the users wants |
|
137 the coordinate reference system and the bounding box to be included |
|
138 in the GeoJSON representation of the geometry. |
|
139 """ |
|
140 backend = connections[self.db].ops |
|
141 if not backend.geojson: |
|
142 raise NotImplementedError('Only PostGIS 1.3.4+ supports GeoJSON serialization.') |
|
143 |
|
144 if not isinstance(precision, (int, long)): |
|
145 raise TypeError('Precision keyword must be set with an integer.') |
|
146 |
|
147 # Setting the options flag -- which depends on which version of |
|
148 # PostGIS we're using. |
|
149 if backend.spatial_version >= (1, 4, 0): |
|
150 options = 0 |
|
151 if crs and bbox: options = 3 |
|
152 elif bbox: options = 1 |
|
153 elif crs: options = 2 |
|
154 else: |
|
155 options = 0 |
|
156 if crs and bbox: options = 3 |
|
157 elif crs: options = 1 |
|
158 elif bbox: options = 2 |
|
159 s = {'desc' : 'GeoJSON', |
|
160 'procedure_args' : {'precision' : precision, 'options' : options}, |
|
161 'procedure_fmt' : '%(geo_col)s,%(precision)s,%(options)s', |
|
162 } |
|
163 return self._spatial_attribute('geojson', s, **kwargs) |
|
164 |
|
165 def geohash(self, precision=20, **kwargs): |
|
166 """ |
|
167 Returns a GeoHash representation of the given field in a `geohash` |
|
168 attribute on each element of the GeoQuerySet. |
|
169 |
|
170 The `precision` keyword may be used to custom the number of |
|
171 _characters_ used in the output GeoHash, the default is 20. |
|
172 """ |
|
173 s = {'desc' : 'GeoHash', |
|
174 'procedure_args': {'precision': precision}, |
|
175 'procedure_fmt': '%(geo_col)s,%(precision)s', |
|
176 } |
|
177 return self._spatial_attribute('geohash', s, **kwargs) |
|
178 |
|
179 def gml(self, precision=8, version=2, **kwargs): |
|
180 """ |
|
181 Returns GML representation of the given field in a `gml` attribute |
|
182 on each element of the GeoQuerySet. |
|
183 """ |
|
184 backend = connections[self.db].ops |
|
185 s = {'desc' : 'GML', 'procedure_args' : {'precision' : precision}} |
|
186 if backend.postgis: |
|
187 # PostGIS AsGML() aggregate function parameter order depends on the |
|
188 # version -- uggh. |
|
189 if backend.spatial_version > (1, 3, 1): |
|
190 procedure_fmt = '%(version)s,%(geo_col)s,%(precision)s' |
|
191 else: |
|
192 procedure_fmt = '%(geo_col)s,%(precision)s,%(version)s' |
|
193 s['procedure_args'] = {'precision' : precision, 'version' : version} |
|
194 |
|
195 return self._spatial_attribute('gml', s, **kwargs) |
|
196 |
|
197 def intersection(self, geom, **kwargs): |
|
198 """ |
|
199 Returns the spatial intersection of the Geometry field in |
|
200 an `intersection` attribute on each element of this |
|
201 GeoQuerySet. |
|
202 """ |
|
203 return self._geomset_attribute('intersection', geom, **kwargs) |
|
204 |
|
205 def kml(self, **kwargs): |
|
206 """ |
|
207 Returns KML representation of the geometry field in a `kml` |
|
208 attribute on each element of this GeoQuerySet. |
|
209 """ |
|
210 s = {'desc' : 'KML', |
|
211 'procedure_fmt' : '%(geo_col)s,%(precision)s', |
|
212 'procedure_args' : {'precision' : kwargs.pop('precision', 8)}, |
|
213 } |
|
214 return self._spatial_attribute('kml', s, **kwargs) |
|
215 |
|
216 def length(self, **kwargs): |
|
217 """ |
|
218 Returns the length of the geometry field as a `Distance` object |
|
219 stored in a `length` attribute on each element of this GeoQuerySet. |
|
220 """ |
|
221 return self._distance_attribute('length', None, **kwargs) |
|
222 |
|
223 def make_line(self, **kwargs): |
|
224 """ |
|
225 Creates a linestring from all of the PointField geometries in the |
|
226 this GeoQuerySet and returns it. This is a spatial aggregate |
|
227 method, and thus returns a geometry rather than a GeoQuerySet. |
|
228 """ |
|
229 return self._spatial_aggregate(aggregates.MakeLine, geo_field_type=PointField, **kwargs) |
|
230 |
|
231 def mem_size(self, **kwargs): |
|
232 """ |
|
233 Returns the memory size (number of bytes) that the geometry field takes |
|
234 in a `mem_size` attribute on each element of this GeoQuerySet. |
|
235 """ |
|
236 return self._spatial_attribute('mem_size', {}, **kwargs) |
|
237 |
|
238 def num_geom(self, **kwargs): |
|
239 """ |
|
240 Returns the number of geometries if the field is a |
|
241 GeometryCollection or Multi* Field in a `num_geom` |
|
242 attribute on each element of this GeoQuerySet; otherwise |
|
243 the sets with None. |
|
244 """ |
|
245 return self._spatial_attribute('num_geom', {}, **kwargs) |
|
246 |
|
247 def num_points(self, **kwargs): |
|
248 """ |
|
249 Returns the number of points in the first linestring in the |
|
250 Geometry field in a `num_points` attribute on each element of |
|
251 this GeoQuerySet; otherwise sets with None. |
|
252 """ |
|
253 return self._spatial_attribute('num_points', {}, **kwargs) |
|
254 |
|
255 def perimeter(self, **kwargs): |
|
256 """ |
|
257 Returns the perimeter of the geometry field as a `Distance` object |
|
258 stored in a `perimeter` attribute on each element of this GeoQuerySet. |
|
259 """ |
|
260 return self._distance_attribute('perimeter', None, **kwargs) |
|
261 |
|
262 def point_on_surface(self, **kwargs): |
|
263 """ |
|
264 Returns a Point geometry guaranteed to lie on the surface of the |
|
265 Geometry field in a `point_on_surface` attribute on each element |
|
266 of this GeoQuerySet; otherwise sets with None. |
|
267 """ |
|
268 return self._geom_attribute('point_on_surface', **kwargs) |
|
269 |
|
270 def reverse_geom(self, **kwargs): |
|
271 """ |
|
272 Reverses the coordinate order of the geometry, and attaches as a |
|
273 `reverse` attribute on each element of this GeoQuerySet. |
|
274 """ |
|
275 s = {'select_field' : GeomField(),} |
|
276 kwargs.setdefault('model_att', 'reverse_geom') |
|
277 if connections[self.db].ops.oracle: |
|
278 s['geo_field_type'] = LineStringField |
|
279 return self._spatial_attribute('reverse', s, **kwargs) |
|
280 |
|
281 def scale(self, x, y, z=0.0, **kwargs): |
|
282 """ |
|
283 Scales the geometry to a new size by multiplying the ordinates |
|
284 with the given x,y,z scale factors. |
|
285 """ |
|
286 if connections[self.db].ops.spatialite: |
|
287 if z != 0.0: |
|
288 raise NotImplementedError('SpatiaLite does not support 3D scaling.') |
|
289 s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s', |
|
290 'procedure_args' : {'x' : x, 'y' : y}, |
|
291 'select_field' : GeomField(), |
|
292 } |
|
293 else: |
|
294 s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s,%(z)s', |
|
295 'procedure_args' : {'x' : x, 'y' : y, 'z' : z}, |
|
296 'select_field' : GeomField(), |
|
297 } |
|
298 return self._spatial_attribute('scale', s, **kwargs) |
|
299 |
|
300 def snap_to_grid(self, *args, **kwargs): |
|
301 """ |
|
302 Snap all points of the input geometry to the grid. How the |
|
303 geometry is snapped to the grid depends on how many arguments |
|
304 were given: |
|
305 - 1 argument : A single size to snap both the X and Y grids to. |
|
306 - 2 arguments: X and Y sizes to snap the grid to. |
|
307 - 4 arguments: X, Y sizes and the X, Y origins. |
|
308 """ |
|
309 if False in [isinstance(arg, (float, int, long)) for arg in args]: |
|
310 raise TypeError('Size argument(s) for the grid must be a float or integer values.') |
|
311 |
|
312 nargs = len(args) |
|
313 if nargs == 1: |
|
314 size = args[0] |
|
315 procedure_fmt = '%(geo_col)s,%(size)s' |
|
316 procedure_args = {'size' : size} |
|
317 elif nargs == 2: |
|
318 xsize, ysize = args |
|
319 procedure_fmt = '%(geo_col)s,%(xsize)s,%(ysize)s' |
|
320 procedure_args = {'xsize' : xsize, 'ysize' : ysize} |
|
321 elif nargs == 4: |
|
322 xsize, ysize, xorigin, yorigin = args |
|
323 procedure_fmt = '%(geo_col)s,%(xorigin)s,%(yorigin)s,%(xsize)s,%(ysize)s' |
|
324 procedure_args = {'xsize' : xsize, 'ysize' : ysize, |
|
325 'xorigin' : xorigin, 'yorigin' : yorigin} |
|
326 else: |
|
327 raise ValueError('Must provide 1, 2, or 4 arguments to `snap_to_grid`.') |
|
328 |
|
329 s = {'procedure_fmt' : procedure_fmt, |
|
330 'procedure_args' : procedure_args, |
|
331 'select_field' : GeomField(), |
|
332 } |
|
333 |
|
334 return self._spatial_attribute('snap_to_grid', s, **kwargs) |
|
335 |
|
336 def svg(self, relative=False, precision=8, **kwargs): |
|
337 """ |
|
338 Returns SVG representation of the geographic field in a `svg` |
|
339 attribute on each element of this GeoQuerySet. |
|
340 |
|
341 Keyword Arguments: |
|
342 `relative` => If set to True, this will evaluate the path in |
|
343 terms of relative moves (rather than absolute). |
|
344 |
|
345 `precision` => May be used to set the maximum number of decimal |
|
346 digits used in output (defaults to 8). |
|
347 """ |
|
348 relative = int(bool(relative)) |
|
349 if not isinstance(precision, (int, long)): |
|
350 raise TypeError('SVG precision keyword argument must be an integer.') |
|
351 s = {'desc' : 'SVG', |
|
352 'procedure_fmt' : '%(geo_col)s,%(rel)s,%(precision)s', |
|
353 'procedure_args' : {'rel' : relative, |
|
354 'precision' : precision, |
|
355 } |
|
356 } |
|
357 return self._spatial_attribute('svg', s, **kwargs) |
|
358 |
|
359 def sym_difference(self, geom, **kwargs): |
|
360 """ |
|
361 Returns the symmetric difference of the geographic field in a |
|
362 `sym_difference` attribute on each element of this GeoQuerySet. |
|
363 """ |
|
364 return self._geomset_attribute('sym_difference', geom, **kwargs) |
|
365 |
|
366 def translate(self, x, y, z=0.0, **kwargs): |
|
367 """ |
|
368 Translates the geometry to a new location using the given numeric |
|
369 parameters as offsets. |
|
370 """ |
|
371 if connections[self.db].ops.spatialite: |
|
372 if z != 0.0: |
|
373 raise NotImplementedError('SpatiaLite does not support 3D translation.') |
|
374 s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s', |
|
375 'procedure_args' : {'x' : x, 'y' : y}, |
|
376 'select_field' : GeomField(), |
|
377 } |
|
378 else: |
|
379 s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s,%(z)s', |
|
380 'procedure_args' : {'x' : x, 'y' : y, 'z' : z}, |
|
381 'select_field' : GeomField(), |
|
382 } |
|
383 return self._spatial_attribute('translate', s, **kwargs) |
|
384 |
|
385 def transform(self, srid=4326, **kwargs): |
|
386 """ |
|
387 Transforms the given geometry field to the given SRID. If no SRID is |
|
388 provided, the transformation will default to using 4326 (WGS84). |
|
389 """ |
|
390 if not isinstance(srid, (int, long)): |
|
391 raise TypeError('An integer SRID must be provided.') |
|
392 field_name = kwargs.get('field_name', None) |
|
393 tmp, geo_field = self._spatial_setup('transform', field_name=field_name) |
|
394 |
|
395 # Getting the selection SQL for the given geographic field. |
|
396 field_col = self._geocol_select(geo_field, field_name) |
|
397 |
|
398 # Why cascading substitutions? Because spatial backends like |
|
399 # Oracle and MySQL already require a function call to convert to text, thus |
|
400 # when there's also a transformation we need to cascade the substitutions. |
|
401 # For example, 'SDO_UTIL.TO_WKTGEOMETRY(SDO_CS.TRANSFORM( ... )' |
|
402 geo_col = self.query.custom_select.get(geo_field, field_col) |
|
403 |
|
404 # Setting the key for the field's column with the custom SELECT SQL to |
|
405 # override the geometry column returned from the database. |
|
406 custom_sel = '%s(%s, %s)' % (connections[self.db].ops.transform, geo_col, srid) |
|
407 # TODO: Should we have this as an alias? |
|
408 # custom_sel = '(%s(%s, %s)) AS %s' % (SpatialBackend.transform, geo_col, srid, qn(geo_field.name)) |
|
409 self.query.transformed_srid = srid # So other GeoQuerySet methods |
|
410 self.query.custom_select[geo_field] = custom_sel |
|
411 return self._clone() |
|
412 |
|
413 def union(self, geom, **kwargs): |
|
414 """ |
|
415 Returns the union of the geographic field with the given |
|
416 Geometry in a `union` attribute on each element of this GeoQuerySet. |
|
417 """ |
|
418 return self._geomset_attribute('union', geom, **kwargs) |
|
419 |
|
420 def unionagg(self, **kwargs): |
|
421 """ |
|
422 Performs an aggregate union on the given geometry field. Returns |
|
423 None if the GeoQuerySet is empty. The `tolerance` keyword is for |
|
424 Oracle backends only. |
|
425 """ |
|
426 return self._spatial_aggregate(aggregates.Union, **kwargs) |
|
427 |
|
428 ### Private API -- Abstracted DRY routines. ### |
|
429 def _spatial_setup(self, att, desc=None, field_name=None, geo_field_type=None): |
|
430 """ |
|
431 Performs set up for executing the spatial function. |
|
432 """ |
|
433 # Does the spatial backend support this? |
|
434 connection = connections[self.db] |
|
435 func = getattr(connection.ops, att, False) |
|
436 if desc is None: desc = att |
|
437 if not func: |
|
438 raise NotImplementedError('%s stored procedure not available on ' |
|
439 'the %s backend.' % |
|
440 (desc, connection.ops.name)) |
|
441 |
|
442 # Initializing the procedure arguments. |
|
443 procedure_args = {'function' : func} |
|
444 |
|
445 # Is there a geographic field in the model to perform this |
|
446 # operation on? |
|
447 geo_field = self.query._geo_field(field_name) |
|
448 if not geo_field: |
|
449 raise TypeError('%s output only available on GeometryFields.' % func) |
|
450 |
|
451 # If the `geo_field_type` keyword was used, then enforce that |
|
452 # type limitation. |
|
453 if not geo_field_type is None and not isinstance(geo_field, geo_field_type): |
|
454 raise TypeError('"%s" stored procedures may only be called on %ss.' % (func, geo_field_type.__name__)) |
|
455 |
|
456 # Setting the procedure args. |
|
457 procedure_args['geo_col'] = self._geocol_select(geo_field, field_name) |
|
458 |
|
459 return procedure_args, geo_field |
|
460 |
|
461 def _spatial_aggregate(self, aggregate, field_name=None, |
|
462 geo_field_type=None, tolerance=0.05): |
|
463 """ |
|
464 DRY routine for calling aggregate spatial stored procedures and |
|
465 returning their result to the caller of the function. |
|
466 """ |
|
467 # Getting the field the geographic aggregate will be called on. |
|
468 geo_field = self.query._geo_field(field_name) |
|
469 if not geo_field: |
|
470 raise TypeError('%s aggregate only available on GeometryFields.' % aggregate.name) |
|
471 |
|
472 # Checking if there are any geo field type limitations on this |
|
473 # aggregate (e.g. ST_Makeline only operates on PointFields). |
|
474 if not geo_field_type is None and not isinstance(geo_field, geo_field_type): |
|
475 raise TypeError('%s aggregate may only be called on %ss.' % (aggregate.name, geo_field_type.__name__)) |
|
476 |
|
477 # Getting the string expression of the field name, as this is the |
|
478 # argument taken by `Aggregate` objects. |
|
479 agg_col = field_name or geo_field.name |
|
480 |
|
481 # Adding any keyword parameters for the Aggregate object. Oracle backends |
|
482 # in particular need an additional `tolerance` parameter. |
|
483 agg_kwargs = {} |
|
484 if connections[self.db].ops.oracle: agg_kwargs['tolerance'] = tolerance |
|
485 |
|
486 # Calling the QuerySet.aggregate, and returning only the value of the aggregate. |
|
487 return self.aggregate(geoagg=aggregate(agg_col, **agg_kwargs))['geoagg'] |
|
488 |
|
489 def _spatial_attribute(self, att, settings, field_name=None, model_att=None): |
|
490 """ |
|
491 DRY routine for calling a spatial stored procedure on a geometry column |
|
492 and attaching its output as an attribute of the model. |
|
493 |
|
494 Arguments: |
|
495 att: |
|
496 The name of the spatial attribute that holds the spatial |
|
497 SQL function to call. |
|
498 |
|
499 settings: |
|
500 Dictonary of internal settings to customize for the spatial procedure. |
|
501 |
|
502 Public Keyword Arguments: |
|
503 |
|
504 field_name: |
|
505 The name of the geographic field to call the spatial |
|
506 function on. May also be a lookup to a geometry field |
|
507 as part of a foreign key relation. |
|
508 |
|
509 model_att: |
|
510 The name of the model attribute to attach the output of |
|
511 the spatial function to. |
|
512 """ |
|
513 # Default settings. |
|
514 settings.setdefault('desc', None) |
|
515 settings.setdefault('geom_args', ()) |
|
516 settings.setdefault('geom_field', None) |
|
517 settings.setdefault('procedure_args', {}) |
|
518 settings.setdefault('procedure_fmt', '%(geo_col)s') |
|
519 settings.setdefault('select_params', []) |
|
520 |
|
521 connection = connections[self.db] |
|
522 backend = connection.ops |
|
523 |
|
524 # Performing setup for the spatial column, unless told not to. |
|
525 if settings.get('setup', True): |
|
526 default_args, geo_field = self._spatial_setup(att, desc=settings['desc'], field_name=field_name, |
|
527 geo_field_type=settings.get('geo_field_type', None)) |
|
528 for k, v in default_args.iteritems(): settings['procedure_args'].setdefault(k, v) |
|
529 else: |
|
530 geo_field = settings['geo_field'] |
|
531 |
|
532 # The attribute to attach to the model. |
|
533 if not isinstance(model_att, basestring): model_att = att |
|
534 |
|
535 # Special handling for any argument that is a geometry. |
|
536 for name in settings['geom_args']: |
|
537 # Using the field's get_placeholder() routine to get any needed |
|
538 # transformation SQL. |
|
539 geom = geo_field.get_prep_value(settings['procedure_args'][name]) |
|
540 params = geo_field.get_db_prep_lookup('contains', geom, connection=connection) |
|
541 geom_placeholder = geo_field.get_placeholder(geom, connection) |
|
542 |
|
543 # Replacing the procedure format with that of any needed |
|
544 # transformation SQL. |
|
545 old_fmt = '%%(%s)s' % name |
|
546 new_fmt = geom_placeholder % '%%s' |
|
547 settings['procedure_fmt'] = settings['procedure_fmt'].replace(old_fmt, new_fmt) |
|
548 settings['select_params'].extend(params) |
|
549 |
|
550 # Getting the format for the stored procedure. |
|
551 fmt = '%%(function)s(%s)' % settings['procedure_fmt'] |
|
552 |
|
553 # If the result of this function needs to be converted. |
|
554 if settings.get('select_field', False): |
|
555 sel_fld = settings['select_field'] |
|
556 if isinstance(sel_fld, GeomField) and backend.select: |
|
557 self.query.custom_select[model_att] = backend.select |
|
558 if connection.ops.oracle: |
|
559 sel_fld.empty_strings_allowed = False |
|
560 self.query.extra_select_fields[model_att] = sel_fld |
|
561 |
|
562 # Finally, setting the extra selection attribute with |
|
563 # the format string expanded with the stored procedure |
|
564 # arguments. |
|
565 return self.extra(select={model_att : fmt % settings['procedure_args']}, |
|
566 select_params=settings['select_params']) |
|
567 |
|
568 def _distance_attribute(self, func, geom=None, tolerance=0.05, spheroid=False, **kwargs): |
|
569 """ |
|
570 DRY routine for GeoQuerySet distance attribute routines. |
|
571 """ |
|
572 # Setting up the distance procedure arguments. |
|
573 procedure_args, geo_field = self._spatial_setup(func, field_name=kwargs.get('field_name', None)) |
|
574 |
|
575 # If geodetic defaulting distance attribute to meters (Oracle and |
|
576 # PostGIS spherical distances return meters). Otherwise, use the |
|
577 # units of the geometry field. |
|
578 connection = connections[self.db] |
|
579 geodetic = geo_field.geodetic(connection) |
|
580 geography = geo_field.geography |
|
581 |
|
582 if geodetic: |
|
583 dist_att = 'm' |
|
584 else: |
|
585 dist_att = Distance.unit_attname(geo_field.units_name(connection)) |
|
586 |
|
587 # Shortcut booleans for what distance function we're using and |
|
588 # whether the geometry field is 3D. |
|
589 distance = func == 'distance' |
|
590 length = func == 'length' |
|
591 perimeter = func == 'perimeter' |
|
592 if not (distance or length or perimeter): |
|
593 raise ValueError('Unknown distance function: %s' % func) |
|
594 geom_3d = geo_field.dim == 3 |
|
595 |
|
596 # The field's get_db_prep_lookup() is used to get any |
|
597 # extra distance parameters. Here we set up the |
|
598 # parameters that will be passed in to field's function. |
|
599 lookup_params = [geom or 'POINT (0 0)', 0] |
|
600 |
|
601 # Getting the spatial backend operations. |
|
602 backend = connection.ops |
|
603 |
|
604 # If the spheroid calculation is desired, either by the `spheroid` |
|
605 # keyword or when calculating the length of geodetic field, make |
|
606 # sure the 'spheroid' distance setting string is passed in so we |
|
607 # get the correct spatial stored procedure. |
|
608 if spheroid or (backend.postgis and geodetic and |
|
609 (not geography) and length): |
|
610 lookup_params.append('spheroid') |
|
611 lookup_params = geo_field.get_prep_value(lookup_params) |
|
612 params = geo_field.get_db_prep_lookup('distance_lte', lookup_params, connection=connection) |
|
613 |
|
614 # The `geom_args` flag is set to true if a geometry parameter was |
|
615 # passed in. |
|
616 geom_args = bool(geom) |
|
617 |
|
618 if backend.oracle: |
|
619 if distance: |
|
620 procedure_fmt = '%(geo_col)s,%(geom)s,%(tolerance)s' |
|
621 elif length or perimeter: |
|
622 procedure_fmt = '%(geo_col)s,%(tolerance)s' |
|
623 procedure_args['tolerance'] = tolerance |
|
624 else: |
|
625 # Getting whether this field is in units of degrees since the field may have |
|
626 # been transformed via the `transform` GeoQuerySet method. |
|
627 if self.query.transformed_srid: |
|
628 u, unit_name, s = get_srid_info(self.query.transformed_srid, connection) |
|
629 geodetic = unit_name in geo_field.geodetic_units |
|
630 |
|
631 if backend.spatialite and geodetic: |
|
632 raise ValueError('SQLite does not support linear distance calculations on geodetic coordinate systems.') |
|
633 |
|
634 if distance: |
|
635 if self.query.transformed_srid: |
|
636 # Setting the `geom_args` flag to false because we want to handle |
|
637 # transformation SQL here, rather than the way done by default |
|
638 # (which will transform to the original SRID of the field rather |
|
639 # than to what was transformed to). |
|
640 geom_args = False |
|
641 procedure_fmt = '%s(%%(geo_col)s, %s)' % (backend.transform, self.query.transformed_srid) |
|
642 if geom.srid is None or geom.srid == self.query.transformed_srid: |
|
643 # If the geom parameter srid is None, it is assumed the coordinates |
|
644 # are in the transformed units. A placeholder is used for the |
|
645 # geometry parameter. `GeomFromText` constructor is also needed |
|
646 # to wrap geom placeholder for SpatiaLite. |
|
647 if backend.spatialite: |
|
648 procedure_fmt += ', %s(%%%%s, %s)' % (backend.from_text, self.query.transformed_srid) |
|
649 else: |
|
650 procedure_fmt += ', %%s' |
|
651 else: |
|
652 # We need to transform the geom to the srid specified in `transform()`, |
|
653 # so wrapping the geometry placeholder in transformation SQL. |
|
654 # SpatiaLite also needs geometry placeholder wrapped in `GeomFromText` |
|
655 # constructor. |
|
656 if backend.spatialite: |
|
657 procedure_fmt += ', %s(%s(%%%%s, %s), %s)' % (backend.transform, backend.from_text, |
|
658 geom.srid, self.query.transformed_srid) |
|
659 else: |
|
660 procedure_fmt += ', %s(%%%%s, %s)' % (backend.transform, self.query.transformed_srid) |
|
661 else: |
|
662 # `transform()` was not used on this GeoQuerySet. |
|
663 procedure_fmt = '%(geo_col)s,%(geom)s' |
|
664 |
|
665 if not geography and geodetic: |
|
666 # Spherical distance calculation is needed (because the geographic |
|
667 # field is geodetic). However, the PostGIS ST_distance_sphere/spheroid() |
|
668 # procedures may only do queries from point columns to point geometries |
|
669 # some error checking is required. |
|
670 if not backend.geography: |
|
671 if not isinstance(geo_field, PointField): |
|
672 raise ValueError('Spherical distance calculation only supported on PointFields.') |
|
673 if not str(Geometry(buffer(params[0].ewkb)).geom_type) == 'Point': |
|
674 raise ValueError('Spherical distance calculation only supported with Point Geometry parameters') |
|
675 # The `function` procedure argument needs to be set differently for |
|
676 # geodetic distance calculations. |
|
677 if spheroid: |
|
678 # Call to distance_spheroid() requires spheroid param as well. |
|
679 procedure_fmt += ",'%(spheroid)s'" |
|
680 procedure_args.update({'function' : backend.distance_spheroid, 'spheroid' : params[1]}) |
|
681 else: |
|
682 procedure_args.update({'function' : backend.distance_sphere}) |
|
683 elif length or perimeter: |
|
684 procedure_fmt = '%(geo_col)s' |
|
685 if not geography and geodetic and length: |
|
686 # There's no `length_sphere`, and `length_spheroid` also |
|
687 # works on 3D geometries. |
|
688 procedure_fmt += ",'%(spheroid)s'" |
|
689 procedure_args.update({'function' : backend.length_spheroid, 'spheroid' : params[1]}) |
|
690 elif geom_3d and backend.postgis: |
|
691 # Use 3D variants of perimeter and length routines on PostGIS. |
|
692 if perimeter: |
|
693 procedure_args.update({'function' : backend.perimeter3d}) |
|
694 elif length: |
|
695 procedure_args.update({'function' : backend.length3d}) |
|
696 |
|
697 # Setting up the settings for `_spatial_attribute`. |
|
698 s = {'select_field' : DistanceField(dist_att), |
|
699 'setup' : False, |
|
700 'geo_field' : geo_field, |
|
701 'procedure_args' : procedure_args, |
|
702 'procedure_fmt' : procedure_fmt, |
|
703 } |
|
704 if geom_args: |
|
705 s['geom_args'] = ('geom',) |
|
706 s['procedure_args']['geom'] = geom |
|
707 elif geom: |
|
708 # The geometry is passed in as a parameter because we handled |
|
709 # transformation conditions in this routine. |
|
710 s['select_params'] = [backend.Adapter(geom)] |
|
711 return self._spatial_attribute(func, s, **kwargs) |
|
712 |
|
713 def _geom_attribute(self, func, tolerance=0.05, **kwargs): |
|
714 """ |
|
715 DRY routine for setting up a GeoQuerySet method that attaches a |
|
716 Geometry attribute (e.g., `centroid`, `point_on_surface`). |
|
717 """ |
|
718 s = {'select_field' : GeomField(),} |
|
719 if connections[self.db].ops.oracle: |
|
720 s['procedure_fmt'] = '%(geo_col)s,%(tolerance)s' |
|
721 s['procedure_args'] = {'tolerance' : tolerance} |
|
722 return self._spatial_attribute(func, s, **kwargs) |
|
723 |
|
724 def _geomset_attribute(self, func, geom, tolerance=0.05, **kwargs): |
|
725 """ |
|
726 DRY routine for setting up a GeoQuerySet method that attaches a |
|
727 Geometry attribute and takes a Geoemtry parameter. This is used |
|
728 for geometry set-like operations (e.g., intersection, difference, |
|
729 union, sym_difference). |
|
730 """ |
|
731 s = {'geom_args' : ('geom',), |
|
732 'select_field' : GeomField(), |
|
733 'procedure_fmt' : '%(geo_col)s,%(geom)s', |
|
734 'procedure_args' : {'geom' : geom}, |
|
735 } |
|
736 if connections[self.db].ops.oracle: |
|
737 s['procedure_fmt'] += ',%(tolerance)s' |
|
738 s['procedure_args']['tolerance'] = tolerance |
|
739 return self._spatial_attribute(func, s, **kwargs) |
|
740 |
|
741 def _geocol_select(self, geo_field, field_name): |
|
742 """ |
|
743 Helper routine for constructing the SQL to select the geographic |
|
744 column. Takes into account if the geographic field is in a |
|
745 ForeignKey relation to the current model. |
|
746 """ |
|
747 opts = self.model._meta |
|
748 if not geo_field in opts.fields: |
|
749 # Is this operation going to be on a related geographic field? |
|
750 # If so, it'll have to be added to the select related information |
|
751 # (e.g., if 'location__point' was given as the field name). |
|
752 self.query.add_select_related([field_name]) |
|
753 compiler = self.query.get_compiler(self.db) |
|
754 compiler.pre_sql_setup() |
|
755 rel_table, rel_col = self.query.related_select_cols[self.query.related_select_fields.index(geo_field)] |
|
756 return compiler._field_column(geo_field, rel_table) |
|
757 elif not geo_field in opts.local_fields: |
|
758 # This geographic field is inherited from another model, so we have to |
|
759 # use the db table for the _parent_ model instead. |
|
760 tmp_fld, parent_model, direct, m2m = opts.get_field_by_name(geo_field.name) |
|
761 return self.query.get_compiler(self.db)._field_column(geo_field, parent_model._meta.db_table) |
|
762 else: |
|
763 return self.query.get_compiler(self.db)._field_column(geo_field) |
|
764 |
|
765 class GeoValuesQuerySet(ValuesQuerySet): |
|
766 def __init__(self, *args, **kwargs): |
|
767 super(GeoValuesQuerySet, self).__init__(*args, **kwargs) |
|
768 # This flag tells `resolve_columns` to run the values through |
|
769 # `convert_values`. This ensures that Geometry objects instead |
|
770 # of string values are returned with `values()` or `values_list()`. |
|
771 self.query.geo_values = True |
|
772 |
|
773 class GeoValuesListQuerySet(GeoValuesQuerySet, ValuesListQuerySet): |
|
774 pass |