|
1 from django.core.exceptions import ImproperlyConfigured |
|
2 from django.db import connection |
|
3 from django.db.models.query import sql, QuerySet, Q |
|
4 |
|
5 from django.contrib.gis.db.backend import SpatialBackend |
|
6 from django.contrib.gis.db.models.fields import GeometryField, PointField |
|
7 from django.contrib.gis.db.models.sql import AreaField, DistanceField, GeomField, GeoQuery, GeoWhereNode |
|
8 from django.contrib.gis.measure import Area, Distance |
|
9 from django.contrib.gis.models import get_srid_info |
|
10 qn = connection.ops.quote_name |
|
11 |
|
12 # For backwards-compatibility; Q object should work just fine |
|
13 # after queryset-refactor. |
|
14 class GeoQ(Q): pass |
|
15 |
|
16 class GeomSQL(object): |
|
17 "Simple wrapper object for geometric SQL." |
|
18 def __init__(self, geo_sql): |
|
19 self.sql = geo_sql |
|
20 |
|
21 def as_sql(self, *args, **kwargs): |
|
22 return self.sql |
|
23 |
|
24 class GeoQuerySet(QuerySet): |
|
25 "The Geographic QuerySet." |
|
26 |
|
27 def __init__(self, model=None, query=None): |
|
28 super(GeoQuerySet, self).__init__(model=model, query=query) |
|
29 self.query = query or GeoQuery(self.model, connection) |
|
30 |
|
31 def area(self, tolerance=0.05, **kwargs): |
|
32 """ |
|
33 Returns the area of the geographic field in an `area` attribute on |
|
34 each element of this GeoQuerySet. |
|
35 """ |
|
36 # Peforming setup here rather than in `_spatial_attribute` so that |
|
37 # we can get the units for `AreaField`. |
|
38 procedure_args, geo_field = self._spatial_setup('area', field_name=kwargs.get('field_name', None)) |
|
39 s = {'procedure_args' : procedure_args, |
|
40 'geo_field' : geo_field, |
|
41 'setup' : False, |
|
42 } |
|
43 if SpatialBackend.oracle: |
|
44 s['procedure_fmt'] = '%(geo_col)s,%(tolerance)s' |
|
45 s['procedure_args']['tolerance'] = tolerance |
|
46 s['select_field'] = AreaField('sq_m') # Oracle returns area in units of meters. |
|
47 elif SpatialBackend.postgis: |
|
48 if not geo_field.geodetic: |
|
49 # Getting the area units of the geographic field. |
|
50 s['select_field'] = AreaField(Area.unit_attname(geo_field._unit_name)) |
|
51 else: |
|
52 # TODO: Do we want to support raw number areas for geodetic fields? |
|
53 raise Exception('Area on geodetic coordinate systems not supported.') |
|
54 return self._spatial_attribute('area', s, **kwargs) |
|
55 |
|
56 def centroid(self, **kwargs): |
|
57 """ |
|
58 Returns the centroid of the geographic field in a `centroid` |
|
59 attribute on each element of this GeoQuerySet. |
|
60 """ |
|
61 return self._geom_attribute('centroid', **kwargs) |
|
62 |
|
63 def difference(self, geom, **kwargs): |
|
64 """ |
|
65 Returns the spatial difference of the geographic field in a `difference` |
|
66 attribute on each element of this GeoQuerySet. |
|
67 """ |
|
68 return self._geomset_attribute('difference', geom, **kwargs) |
|
69 |
|
70 def distance(self, geom, **kwargs): |
|
71 """ |
|
72 Returns the distance from the given geographic field name to the |
|
73 given geometry in a `distance` attribute on each element of the |
|
74 GeoQuerySet. |
|
75 |
|
76 Keyword Arguments: |
|
77 `spheroid` => If the geometry field is geodetic and PostGIS is |
|
78 the spatial database, then the more accurate |
|
79 spheroid calculation will be used instead of the |
|
80 quicker sphere calculation. |
|
81 |
|
82 `tolerance` => Used only for Oracle. The tolerance is |
|
83 in meters -- a default of 5 centimeters (0.05) |
|
84 is used. |
|
85 """ |
|
86 return self._distance_attribute('distance', geom, **kwargs) |
|
87 |
|
88 def envelope(self, **kwargs): |
|
89 """ |
|
90 Returns a Geometry representing the bounding box of the |
|
91 Geometry field in an `envelope` attribute on each element of |
|
92 the GeoQuerySet. |
|
93 """ |
|
94 return self._geom_attribute('envelope', **kwargs) |
|
95 |
|
96 def extent(self, **kwargs): |
|
97 """ |
|
98 Returns the extent (aggregate) of the features in the GeoQuerySet. The |
|
99 extent will be returned as a 4-tuple, consisting of (xmin, ymin, xmax, ymax). |
|
100 """ |
|
101 convert_extent = None |
|
102 if SpatialBackend.postgis: |
|
103 def convert_extent(box, geo_field): |
|
104 # TODO: Parsing of BOX3D, Oracle support (patches welcome!) |
|
105 # Box text will be something like "BOX(-90.0 30.0, -85.0 40.0)"; |
|
106 # parsing out and returning as a 4-tuple. |
|
107 ll, ur = box[4:-1].split(',') |
|
108 xmin, ymin = map(float, ll.split()) |
|
109 xmax, ymax = map(float, ur.split()) |
|
110 return (xmin, ymin, xmax, ymax) |
|
111 elif SpatialBackend.oracle: |
|
112 def convert_extent(wkt, geo_field): |
|
113 raise NotImplementedError |
|
114 return self._spatial_aggregate('extent', convert_func=convert_extent, **kwargs) |
|
115 |
|
116 def gml(self, precision=8, version=2, **kwargs): |
|
117 """ |
|
118 Returns GML representation of the given field in a `gml` attribute |
|
119 on each element of the GeoQuerySet. |
|
120 """ |
|
121 s = {'desc' : 'GML', 'procedure_args' : {'precision' : precision}} |
|
122 if SpatialBackend.postgis: |
|
123 # PostGIS AsGML() aggregate function parameter order depends on the |
|
124 # version -- uggh. |
|
125 major, minor1, minor2 = SpatialBackend.version |
|
126 if major >= 1 and (minor1 > 3 or (minor1 == 3 and minor2 > 1)): |
|
127 procedure_fmt = '%(version)s,%(geo_col)s,%(precision)s' |
|
128 else: |
|
129 procedure_fmt = '%(geo_col)s,%(precision)s,%(version)s' |
|
130 s['procedure_args'] = {'precision' : precision, 'version' : version} |
|
131 |
|
132 return self._spatial_attribute('gml', s, **kwargs) |
|
133 |
|
134 def intersection(self, geom, **kwargs): |
|
135 """ |
|
136 Returns the spatial intersection of the Geometry field in |
|
137 an `intersection` attribute on each element of this |
|
138 GeoQuerySet. |
|
139 """ |
|
140 return self._geomset_attribute('intersection', geom, **kwargs) |
|
141 |
|
142 def kml(self, **kwargs): |
|
143 """ |
|
144 Returns KML representation of the geometry field in a `kml` |
|
145 attribute on each element of this GeoQuerySet. |
|
146 """ |
|
147 s = {'desc' : 'KML', |
|
148 'procedure_fmt' : '%(geo_col)s,%(precision)s', |
|
149 'procedure_args' : {'precision' : kwargs.pop('precision', 8)}, |
|
150 } |
|
151 return self._spatial_attribute('kml', s, **kwargs) |
|
152 |
|
153 def length(self, **kwargs): |
|
154 """ |
|
155 Returns the length of the geometry field as a `Distance` object |
|
156 stored in a `length` attribute on each element of this GeoQuerySet. |
|
157 """ |
|
158 return self._distance_attribute('length', None, **kwargs) |
|
159 |
|
160 def make_line(self, **kwargs): |
|
161 """ |
|
162 Creates a linestring from all of the PointField geometries in the |
|
163 this GeoQuerySet and returns it. This is a spatial aggregate |
|
164 method, and thus returns a geometry rather than a GeoQuerySet. |
|
165 """ |
|
166 kwargs['geo_field_type'] = PointField |
|
167 kwargs['agg_field'] = GeometryField |
|
168 return self._spatial_aggregate('make_line', **kwargs) |
|
169 |
|
170 def mem_size(self, **kwargs): |
|
171 """ |
|
172 Returns the memory size (number of bytes) that the geometry field takes |
|
173 in a `mem_size` attribute on each element of this GeoQuerySet. |
|
174 """ |
|
175 return self._spatial_attribute('mem_size', {}, **kwargs) |
|
176 |
|
177 def num_geom(self, **kwargs): |
|
178 """ |
|
179 Returns the number of geometries if the field is a |
|
180 GeometryCollection or Multi* Field in a `num_geom` |
|
181 attribute on each element of this GeoQuerySet; otherwise |
|
182 the sets with None. |
|
183 """ |
|
184 return self._spatial_attribute('num_geom', {}, **kwargs) |
|
185 |
|
186 def num_points(self, **kwargs): |
|
187 """ |
|
188 Returns the number of points in the first linestring in the |
|
189 Geometry field in a `num_points` attribute on each element of |
|
190 this GeoQuerySet; otherwise sets with None. |
|
191 """ |
|
192 return self._spatial_attribute('num_points', {}, **kwargs) |
|
193 |
|
194 def perimeter(self, **kwargs): |
|
195 """ |
|
196 Returns the perimeter of the geometry field as a `Distance` object |
|
197 stored in a `perimeter` attribute on each element of this GeoQuerySet. |
|
198 """ |
|
199 return self._distance_attribute('perimeter', None, **kwargs) |
|
200 |
|
201 def point_on_surface(self, **kwargs): |
|
202 """ |
|
203 Returns a Point geometry guaranteed to lie on the surface of the |
|
204 Geometry field in a `point_on_surface` attribute on each element |
|
205 of this GeoQuerySet; otherwise sets with None. |
|
206 """ |
|
207 return self._geom_attribute('point_on_surface', **kwargs) |
|
208 |
|
209 def scale(self, x, y, z=0.0, **kwargs): |
|
210 """ |
|
211 Scales the geometry to a new size by multiplying the ordinates |
|
212 with the given x,y,z scale factors. |
|
213 """ |
|
214 s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s,%(z)s', |
|
215 'procedure_args' : {'x' : x, 'y' : y, 'z' : z}, |
|
216 'select_field' : GeomField(), |
|
217 } |
|
218 return self._spatial_attribute('scale', s, **kwargs) |
|
219 |
|
220 def svg(self, **kwargs): |
|
221 """ |
|
222 Returns SVG representation of the geographic field in a `svg` |
|
223 attribute on each element of this GeoQuerySet. |
|
224 """ |
|
225 s = {'desc' : 'SVG', |
|
226 'procedure_fmt' : '%(geo_col)s,%(rel)s,%(precision)s', |
|
227 'procedure_args' : {'rel' : int(kwargs.pop('relative', 0)), |
|
228 'precision' : kwargs.pop('precision', 8)}, |
|
229 } |
|
230 return self._spatial_attribute('svg', s, **kwargs) |
|
231 |
|
232 def sym_difference(self, geom, **kwargs): |
|
233 """ |
|
234 Returns the symmetric difference of the geographic field in a |
|
235 `sym_difference` attribute on each element of this GeoQuerySet. |
|
236 """ |
|
237 return self._geomset_attribute('sym_difference', geom, **kwargs) |
|
238 |
|
239 def translate(self, x, y, z=0.0, **kwargs): |
|
240 """ |
|
241 Translates the geometry to a new location using the given numeric |
|
242 parameters as offsets. |
|
243 """ |
|
244 s = {'procedure_fmt' : '%(geo_col)s,%(x)s,%(y)s,%(z)s', |
|
245 'procedure_args' : {'x' : x, 'y' : y, 'z' : z}, |
|
246 'select_field' : GeomField(), |
|
247 } |
|
248 return self._spatial_attribute('translate', s, **kwargs) |
|
249 |
|
250 def transform(self, srid=4326, **kwargs): |
|
251 """ |
|
252 Transforms the given geometry field to the given SRID. If no SRID is |
|
253 provided, the transformation will default to using 4326 (WGS84). |
|
254 """ |
|
255 if not isinstance(srid, (int, long)): |
|
256 raise TypeError('An integer SRID must be provided.') |
|
257 field_name = kwargs.get('field_name', None) |
|
258 tmp, geo_field = self._spatial_setup('transform', field_name=field_name) |
|
259 |
|
260 # Getting the selection SQL for the given geographic field. |
|
261 field_col = self._geocol_select(geo_field, field_name) |
|
262 |
|
263 # Why cascading substitutions? Because spatial backends like |
|
264 # Oracle and MySQL already require a function call to convert to text, thus |
|
265 # when there's also a transformation we need to cascade the substitutions. |
|
266 # For example, 'SDO_UTIL.TO_WKTGEOMETRY(SDO_CS.TRANSFORM( ... )' |
|
267 geo_col = self.query.custom_select.get(geo_field, field_col) |
|
268 |
|
269 # Setting the key for the field's column with the custom SELECT SQL to |
|
270 # override the geometry column returned from the database. |
|
271 custom_sel = '%s(%s, %s)' % (SpatialBackend.transform, geo_col, srid) |
|
272 # TODO: Should we have this as an alias? |
|
273 # custom_sel = '(%s(%s, %s)) AS %s' % (SpatialBackend.transform, geo_col, srid, qn(geo_field.name)) |
|
274 self.query.transformed_srid = srid # So other GeoQuerySet methods |
|
275 self.query.custom_select[geo_field] = custom_sel |
|
276 return self._clone() |
|
277 |
|
278 def union(self, geom, **kwargs): |
|
279 """ |
|
280 Returns the union of the geographic field with the given |
|
281 Geometry in a `union` attribute on each element of this GeoQuerySet. |
|
282 """ |
|
283 return self._geomset_attribute('union', geom, **kwargs) |
|
284 |
|
285 def unionagg(self, **kwargs): |
|
286 """ |
|
287 Performs an aggregate union on the given geometry field. Returns |
|
288 None if the GeoQuerySet is empty. The `tolerance` keyword is for |
|
289 Oracle backends only. |
|
290 """ |
|
291 kwargs['agg_field'] = GeometryField |
|
292 return self._spatial_aggregate('unionagg', **kwargs) |
|
293 |
|
294 ### Private API -- Abstracted DRY routines. ### |
|
295 def _spatial_setup(self, att, aggregate=False, desc=None, field_name=None, geo_field_type=None): |
|
296 """ |
|
297 Performs set up for executing the spatial function. |
|
298 """ |
|
299 # Does the spatial backend support this? |
|
300 func = getattr(SpatialBackend, att, False) |
|
301 if desc is None: desc = att |
|
302 if not func: raise ImproperlyConfigured('%s stored procedure not available.' % desc) |
|
303 |
|
304 # Initializing the procedure arguments. |
|
305 procedure_args = {'function' : func} |
|
306 |
|
307 # Is there a geographic field in the model to perform this |
|
308 # operation on? |
|
309 geo_field = self.query._geo_field(field_name) |
|
310 if not geo_field: |
|
311 raise TypeError('%s output only available on GeometryFields.' % func) |
|
312 |
|
313 # If the `geo_field_type` keyword was used, then enforce that |
|
314 # type limitation. |
|
315 if not geo_field_type is None and not isinstance(geo_field, geo_field_type): |
|
316 raise TypeError('"%s" stored procedures may only be called on %ss.' % (func, geo_field_type.__name__)) |
|
317 |
|
318 # Setting the procedure args. |
|
319 procedure_args['geo_col'] = self._geocol_select(geo_field, field_name, aggregate) |
|
320 |
|
321 return procedure_args, geo_field |
|
322 |
|
323 def _spatial_aggregate(self, att, field_name=None, |
|
324 agg_field=None, convert_func=None, |
|
325 geo_field_type=None, tolerance=0.0005): |
|
326 """ |
|
327 DRY routine for calling aggregate spatial stored procedures and |
|
328 returning their result to the caller of the function. |
|
329 """ |
|
330 # Constructing the setup keyword arguments. |
|
331 setup_kwargs = {'aggregate' : True, |
|
332 'field_name' : field_name, |
|
333 'geo_field_type' : geo_field_type, |
|
334 } |
|
335 procedure_args, geo_field = self._spatial_setup(att, **setup_kwargs) |
|
336 |
|
337 if SpatialBackend.oracle: |
|
338 procedure_args['tolerance'] = tolerance |
|
339 # Adding in selection SQL for Oracle geometry columns. |
|
340 if agg_field is GeometryField: |
|
341 agg_sql = '%s' % SpatialBackend.select |
|
342 else: |
|
343 agg_sql = '%s' |
|
344 agg_sql = agg_sql % ('%(function)s(SDOAGGRTYPE(%(geo_col)s,%(tolerance)s))' % procedure_args) |
|
345 else: |
|
346 agg_sql = '%(function)s(%(geo_col)s)' % procedure_args |
|
347 |
|
348 # Wrapping our selection SQL in `GeomSQL` to bypass quoting, and |
|
349 # specifying the type of the aggregate field. |
|
350 self.query.select = [GeomSQL(agg_sql)] |
|
351 self.query.select_fields = [agg_field] |
|
352 |
|
353 try: |
|
354 # `asql` => not overriding `sql` module. |
|
355 asql, params = self.query.as_sql() |
|
356 except sql.datastructures.EmptyResultSet: |
|
357 return None |
|
358 |
|
359 # Getting a cursor, executing the query, and extracting the returned |
|
360 # value from the aggregate function. |
|
361 cursor = connection.cursor() |
|
362 cursor.execute(asql, params) |
|
363 result = cursor.fetchone()[0] |
|
364 |
|
365 # If the `agg_field` is specified as a GeometryField, then autmatically |
|
366 # set up the conversion function. |
|
367 if agg_field is GeometryField and not callable(convert_func): |
|
368 if SpatialBackend.postgis: |
|
369 def convert_geom(hex, geo_field): |
|
370 if hex: return SpatialBackend.Geometry(hex) |
|
371 else: return None |
|
372 elif SpatialBackend.oracle: |
|
373 def convert_geom(clob, geo_field): |
|
374 if clob: return SpatialBackend.Geometry(clob.read(), geo_field._srid) |
|
375 else: return None |
|
376 convert_func = convert_geom |
|
377 |
|
378 # Returning the callback function evaluated on the result culled |
|
379 # from the executed cursor. |
|
380 if callable(convert_func): |
|
381 return convert_func(result, geo_field) |
|
382 else: |
|
383 return result |
|
384 |
|
385 def _spatial_attribute(self, att, settings, field_name=None, model_att=None): |
|
386 """ |
|
387 DRY routine for calling a spatial stored procedure on a geometry column |
|
388 and attaching its output as an attribute of the model. |
|
389 |
|
390 Arguments: |
|
391 att: |
|
392 The name of the spatial attribute that holds the spatial |
|
393 SQL function to call. |
|
394 |
|
395 settings: |
|
396 Dictonary of internal settings to customize for the spatial procedure. |
|
397 |
|
398 Public Keyword Arguments: |
|
399 |
|
400 field_name: |
|
401 The name of the geographic field to call the spatial |
|
402 function on. May also be a lookup to a geometry field |
|
403 as part of a foreign key relation. |
|
404 |
|
405 model_att: |
|
406 The name of the model attribute to attach the output of |
|
407 the spatial function to. |
|
408 """ |
|
409 # Default settings. |
|
410 settings.setdefault('desc', None) |
|
411 settings.setdefault('geom_args', ()) |
|
412 settings.setdefault('geom_field', None) |
|
413 settings.setdefault('procedure_args', {}) |
|
414 settings.setdefault('procedure_fmt', '%(geo_col)s') |
|
415 settings.setdefault('select_params', []) |
|
416 |
|
417 # Performing setup for the spatial column, unless told not to. |
|
418 if settings.get('setup', True): |
|
419 default_args, geo_field = self._spatial_setup(att, desc=settings['desc'], field_name=field_name) |
|
420 for k, v in default_args.iteritems(): settings['procedure_args'].setdefault(k, v) |
|
421 else: |
|
422 geo_field = settings['geo_field'] |
|
423 |
|
424 # The attribute to attach to the model. |
|
425 if not isinstance(model_att, basestring): model_att = att |
|
426 |
|
427 # Special handling for any argument that is a geometry. |
|
428 for name in settings['geom_args']: |
|
429 # Using the field's get_db_prep_lookup() to get any needed |
|
430 # transformation SQL -- we pass in a 'dummy' `contains` lookup. |
|
431 where, params = geo_field.get_db_prep_lookup('contains', settings['procedure_args'][name]) |
|
432 # Replacing the procedure format with that of any needed |
|
433 # transformation SQL. |
|
434 old_fmt = '%%(%s)s' % name |
|
435 new_fmt = where[0] % '%%s' |
|
436 settings['procedure_fmt'] = settings['procedure_fmt'].replace(old_fmt, new_fmt) |
|
437 settings['select_params'].extend(params) |
|
438 |
|
439 # Getting the format for the stored procedure. |
|
440 fmt = '%%(function)s(%s)' % settings['procedure_fmt'] |
|
441 |
|
442 # If the result of this function needs to be converted. |
|
443 if settings.get('select_field', False): |
|
444 sel_fld = settings['select_field'] |
|
445 if isinstance(sel_fld, GeomField) and SpatialBackend.select: |
|
446 self.query.custom_select[model_att] = SpatialBackend.select |
|
447 self.query.extra_select_fields[model_att] = sel_fld |
|
448 |
|
449 # Finally, setting the extra selection attribute with |
|
450 # the format string expanded with the stored procedure |
|
451 # arguments. |
|
452 return self.extra(select={model_att : fmt % settings['procedure_args']}, |
|
453 select_params=settings['select_params']) |
|
454 |
|
455 def _distance_attribute(self, func, geom=None, tolerance=0.05, spheroid=False, **kwargs): |
|
456 """ |
|
457 DRY routine for GeoQuerySet distance attribute routines. |
|
458 """ |
|
459 # Setting up the distance procedure arguments. |
|
460 procedure_args, geo_field = self._spatial_setup(func, field_name=kwargs.get('field_name', None)) |
|
461 |
|
462 # If geodetic defaulting distance attribute to meters (Oracle and |
|
463 # PostGIS spherical distances return meters). Otherwise, use the |
|
464 # units of the geometry field. |
|
465 if geo_field.geodetic: |
|
466 dist_att = 'm' |
|
467 else: |
|
468 dist_att = Distance.unit_attname(geo_field._unit_name) |
|
469 |
|
470 # Shortcut booleans for what distance function we're using. |
|
471 distance = func == 'distance' |
|
472 length = func == 'length' |
|
473 perimeter = func == 'perimeter' |
|
474 if not (distance or length or perimeter): |
|
475 raise ValueError('Unknown distance function: %s' % func) |
|
476 |
|
477 # The field's get_db_prep_lookup() is used to get any |
|
478 # extra distance parameters. Here we set up the |
|
479 # parameters that will be passed in to field's function. |
|
480 lookup_params = [geom or 'POINT (0 0)', 0] |
|
481 |
|
482 # If the spheroid calculation is desired, either by the `spheroid` |
|
483 # keyword or wehn calculating the length of geodetic field, make |
|
484 # sure the 'spheroid' distance setting string is passed in so we |
|
485 # get the correct spatial stored procedure. |
|
486 if spheroid or (SpatialBackend.postgis and geo_field.geodetic and length): |
|
487 lookup_params.append('spheroid') |
|
488 where, params = geo_field.get_db_prep_lookup('distance_lte', lookup_params) |
|
489 |
|
490 # The `geom_args` flag is set to true if a geometry parameter was |
|
491 # passed in. |
|
492 geom_args = bool(geom) |
|
493 |
|
494 if SpatialBackend.oracle: |
|
495 if distance: |
|
496 procedure_fmt = '%(geo_col)s,%(geom)s,%(tolerance)s' |
|
497 elif length or perimeter: |
|
498 procedure_fmt = '%(geo_col)s,%(tolerance)s' |
|
499 procedure_args['tolerance'] = tolerance |
|
500 else: |
|
501 # Getting whether this field is in units of degrees since the field may have |
|
502 # been transformed via the `transform` GeoQuerySet method. |
|
503 if self.query.transformed_srid: |
|
504 u, unit_name, s = get_srid_info(self.query.transformed_srid) |
|
505 geodetic = unit_name in geo_field.geodetic_units |
|
506 else: |
|
507 geodetic = geo_field.geodetic |
|
508 |
|
509 if distance: |
|
510 if self.query.transformed_srid: |
|
511 # Setting the `geom_args` flag to false because we want to handle |
|
512 # transformation SQL here, rather than the way done by default |
|
513 # (which will transform to the original SRID of the field rather |
|
514 # than to what was transformed to). |
|
515 geom_args = False |
|
516 procedure_fmt = '%s(%%(geo_col)s, %s)' % (SpatialBackend.transform, self.query.transformed_srid) |
|
517 if geom.srid is None or geom.srid == self.query.transformed_srid: |
|
518 # If the geom parameter srid is None, it is assumed the coordinates |
|
519 # are in the transformed units. A placeholder is used for the |
|
520 # geometry parameter. |
|
521 procedure_fmt += ', %%s' |
|
522 else: |
|
523 # We need to transform the geom to the srid specified in `transform()`, |
|
524 # so wrapping the geometry placeholder in transformation SQL. |
|
525 procedure_fmt += ', %s(%%%%s, %s)' % (SpatialBackend.transform, self.query.transformed_srid) |
|
526 else: |
|
527 # `transform()` was not used on this GeoQuerySet. |
|
528 procedure_fmt = '%(geo_col)s,%(geom)s' |
|
529 |
|
530 if geodetic: |
|
531 # Spherical distance calculation is needed (because the geographic |
|
532 # field is geodetic). However, the PostGIS ST_distance_sphere/spheroid() |
|
533 # procedures may only do queries from point columns to point geometries |
|
534 # some error checking is required. |
|
535 if not isinstance(geo_field, PointField): |
|
536 raise TypeError('Spherical distance calculation only supported on PointFields.') |
|
537 if not str(SpatialBackend.Geometry(buffer(params[0].wkb)).geom_type) == 'Point': |
|
538 raise TypeError('Spherical distance calculation only supported with Point Geometry parameters') |
|
539 # The `function` procedure argument needs to be set differently for |
|
540 # geodetic distance calculations. |
|
541 if spheroid: |
|
542 # Call to distance_spheroid() requires spheroid param as well. |
|
543 procedure_fmt += ',%(spheroid)s' |
|
544 procedure_args.update({'function' : SpatialBackend.distance_spheroid, 'spheroid' : where[1]}) |
|
545 else: |
|
546 procedure_args.update({'function' : SpatialBackend.distance_sphere}) |
|
547 elif length or perimeter: |
|
548 procedure_fmt = '%(geo_col)s' |
|
549 if geodetic and length: |
|
550 # There's no `length_sphere` |
|
551 procedure_fmt += ',%(spheroid)s' |
|
552 procedure_args.update({'function' : SpatialBackend.length_spheroid, 'spheroid' : where[1]}) |
|
553 |
|
554 # Setting up the settings for `_spatial_attribute`. |
|
555 s = {'select_field' : DistanceField(dist_att), |
|
556 'setup' : False, |
|
557 'geo_field' : geo_field, |
|
558 'procedure_args' : procedure_args, |
|
559 'procedure_fmt' : procedure_fmt, |
|
560 } |
|
561 if geom_args: |
|
562 s['geom_args'] = ('geom',) |
|
563 s['procedure_args']['geom'] = geom |
|
564 elif geom: |
|
565 # The geometry is passed in as a parameter because we handled |
|
566 # transformation conditions in this routine. |
|
567 s['select_params'] = [SpatialBackend.Adaptor(geom)] |
|
568 return self._spatial_attribute(func, s, **kwargs) |
|
569 |
|
570 def _geom_attribute(self, func, tolerance=0.05, **kwargs): |
|
571 """ |
|
572 DRY routine for setting up a GeoQuerySet method that attaches a |
|
573 Geometry attribute (e.g., `centroid`, `point_on_surface`). |
|
574 """ |
|
575 s = {'select_field' : GeomField(),} |
|
576 if SpatialBackend.oracle: |
|
577 s['procedure_fmt'] = '%(geo_col)s,%(tolerance)s' |
|
578 s['procedure_args'] = {'tolerance' : tolerance} |
|
579 return self._spatial_attribute(func, s, **kwargs) |
|
580 |
|
581 def _geomset_attribute(self, func, geom, tolerance=0.05, **kwargs): |
|
582 """ |
|
583 DRY routine for setting up a GeoQuerySet method that attaches a |
|
584 Geometry attribute and takes a Geoemtry parameter. This is used |
|
585 for geometry set-like operations (e.g., intersection, difference, |
|
586 union, sym_difference). |
|
587 """ |
|
588 s = {'geom_args' : ('geom',), |
|
589 'select_field' : GeomField(), |
|
590 'procedure_fmt' : '%(geo_col)s,%(geom)s', |
|
591 'procedure_args' : {'geom' : geom}, |
|
592 } |
|
593 if SpatialBackend.oracle: |
|
594 s['procedure_fmt'] += ',%(tolerance)s' |
|
595 s['procedure_args']['tolerance'] = tolerance |
|
596 return self._spatial_attribute(func, s, **kwargs) |
|
597 |
|
598 def _geocol_select(self, geo_field, field_name, aggregate=False): |
|
599 """ |
|
600 Helper routine for constructing the SQL to select the geographic |
|
601 column. Takes into account if the geographic field is in a |
|
602 ForeignKey relation to the current model. |
|
603 """ |
|
604 # If this is an aggregate spatial query, the flag needs to be |
|
605 # set on the `GeoQuery` object of this queryset. |
|
606 if aggregate: self.query.aggregate = True |
|
607 |
|
608 # Is this operation going to be on a related geographic field? |
|
609 if not geo_field in self.model._meta.fields: |
|
610 # If so, it'll have to be added to the select related information |
|
611 # (e.g., if 'location__point' was given as the field name). |
|
612 self.query.add_select_related([field_name]) |
|
613 self.query.pre_sql_setup() |
|
614 rel_table, rel_col = self.query.related_select_cols[self.query.related_select_fields.index(geo_field)] |
|
615 return self.query._field_column(geo_field, rel_table) |
|
616 else: |
|
617 return self.query._field_column(geo_field) |