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