|
1 """ |
|
2 Base/mixin classes for the spatial backend database operations and the |
|
3 `SpatialRefSys` model the backend. |
|
4 """ |
|
5 import re |
|
6 from django.conf import settings |
|
7 from django.contrib.gis import gdal |
|
8 |
|
9 class BaseSpatialOperations(object): |
|
10 """ |
|
11 This module holds the base `BaseSpatialBackend` object, which is |
|
12 instantiated by each spatial database backend with the features |
|
13 it has. |
|
14 """ |
|
15 distance_functions = {} |
|
16 geometry_functions = {} |
|
17 geometry_operators = {} |
|
18 geography_operators = {} |
|
19 geography_functions = {} |
|
20 gis_terms = {} |
|
21 truncate_params = {} |
|
22 |
|
23 # Quick booleans for the type of this spatial backend, and |
|
24 # an attribute for the spatial database version tuple (if applicable) |
|
25 postgis = False |
|
26 spatialite = False |
|
27 mysql = False |
|
28 oracle = False |
|
29 spatial_version = None |
|
30 |
|
31 # How the geometry column should be selected. |
|
32 select = None |
|
33 |
|
34 # Does the spatial database have a geography type? |
|
35 geography = False |
|
36 |
|
37 area = False |
|
38 centroid = False |
|
39 difference = False |
|
40 distance = False |
|
41 distance_sphere = False |
|
42 distance_spheroid = False |
|
43 envelope = False |
|
44 force_rhr = False |
|
45 mem_size = False |
|
46 bounding_circle = False |
|
47 num_geom = False |
|
48 num_points = False |
|
49 perimeter = False |
|
50 perimeter3d = False |
|
51 point_on_surface = False |
|
52 polygonize = False |
|
53 reverse = False |
|
54 scale = False |
|
55 snap_to_grid = False |
|
56 sym_difference = False |
|
57 transform = False |
|
58 translate = False |
|
59 union = False |
|
60 |
|
61 # Aggregates |
|
62 collect = False |
|
63 extent = False |
|
64 extent3d = False |
|
65 make_line = False |
|
66 unionagg = False |
|
67 |
|
68 # Serialization |
|
69 geohash = False |
|
70 geojson = False |
|
71 gml = False |
|
72 kml = False |
|
73 svg = False |
|
74 |
|
75 # Constructors |
|
76 from_text = False |
|
77 from_wkb = False |
|
78 |
|
79 # Default conversion functions for aggregates; will be overridden if implemented |
|
80 # for the spatial backend. |
|
81 def convert_extent(self, box): |
|
82 raise NotImplementedError('Aggregate extent not implemented for this spatial backend.') |
|
83 |
|
84 def convert_extent3d(self, box): |
|
85 raise NotImplementedError('Aggregate 3D extent not implemented for this spatial backend.') |
|
86 |
|
87 def convert_geom(self, geom_val, geom_field): |
|
88 raise NotImplementedError('Aggregate method not implemented for this spatial backend.') |
|
89 |
|
90 # For quoting column values, rather than columns. |
|
91 def geo_quote_name(self, name): |
|
92 if isinstance(name, unicode): |
|
93 name = name.encode('ascii') |
|
94 return "'%s'" % name |
|
95 |
|
96 # GeometryField operations |
|
97 def geo_db_type(self, f): |
|
98 """ |
|
99 Returns the database column type for the geometry field on |
|
100 the spatial backend. |
|
101 """ |
|
102 raise NotImplementedError |
|
103 |
|
104 def get_distance(self, f, value, lookup_type): |
|
105 """ |
|
106 Returns the distance parameters for the given geometry field, |
|
107 lookup value, and lookup type. |
|
108 """ |
|
109 raise NotImplementedError('Distance operations not available on this spatial backend.') |
|
110 |
|
111 def get_geom_placeholder(self, f, value): |
|
112 """ |
|
113 Returns the placeholder for the given geometry field with the given |
|
114 value. Depending on the spatial backend, the placeholder may contain a |
|
115 stored procedure call to the transformation function of the spatial |
|
116 backend. |
|
117 """ |
|
118 raise NotImplementedError |
|
119 |
|
120 # Spatial SQL Construction |
|
121 def spatial_aggregate_sql(self, agg): |
|
122 raise NotImplementedError('Aggregate support not implemented for this spatial backend.') |
|
123 |
|
124 def spatial_lookup_sql(self, lvalue, lookup_type, value, field): |
|
125 raise NotImplmentedError |
|
126 |
|
127 # Routines for getting the OGC-compliant models. |
|
128 def geometry_columns(self): |
|
129 raise NotImplementedError |
|
130 |
|
131 def spatial_ref_sys(self): |
|
132 raise NotImplementedError |
|
133 |
|
134 class SpatialRefSysMixin(object): |
|
135 """ |
|
136 The SpatialRefSysMixin is a class used by the database-dependent |
|
137 SpatialRefSys objects to reduce redundnant code. |
|
138 """ |
|
139 # For pulling out the spheroid from the spatial reference string. This |
|
140 # regular expression is used only if the user does not have GDAL installed. |
|
141 # TODO: Flattening not used in all ellipsoids, could also be a minor axis, |
|
142 # or 'b' parameter. |
|
143 spheroid_regex = re.compile(r'.+SPHEROID\[\"(?P<name>.+)\",(?P<major>\d+(\.\d+)?),(?P<flattening>\d{3}\.\d+),') |
|
144 |
|
145 # For pulling out the units on platforms w/o GDAL installed. |
|
146 # TODO: Figure out how to pull out angular units of projected coordinate system and |
|
147 # fix for LOCAL_CS types. GDAL should be highly recommended for performing |
|
148 # distance queries. |
|
149 units_regex = re.compile(r'.+UNIT ?\["(?P<unit_name>[\w \'\(\)]+)", ?(?P<unit>[\d\.]+)(,AUTHORITY\["(?P<unit_auth_name>[\w \'\(\)]+)","(?P<unit_auth_val>\d+)"\])?\]([\w ]+)?(,AUTHORITY\["(?P<auth_name>[\w \'\(\)]+)","(?P<auth_val>\d+)"\])?\]$') |
|
150 |
|
151 @property |
|
152 def srs(self): |
|
153 """ |
|
154 Returns a GDAL SpatialReference object, if GDAL is installed. |
|
155 """ |
|
156 if gdal.HAS_GDAL: |
|
157 # TODO: Is caching really necessary here? Is complexity worth it? |
|
158 if hasattr(self, '_srs'): |
|
159 # Returning a clone of the cached SpatialReference object. |
|
160 return self._srs.clone() |
|
161 else: |
|
162 # Attempting to cache a SpatialReference object. |
|
163 |
|
164 # Trying to get from WKT first. |
|
165 try: |
|
166 self._srs = gdal.SpatialReference(self.wkt) |
|
167 return self.srs |
|
168 except Exception, msg: |
|
169 pass |
|
170 |
|
171 try: |
|
172 self._srs = gdal.SpatialReference(self.proj4text) |
|
173 return self.srs |
|
174 except Exception, msg: |
|
175 pass |
|
176 |
|
177 raise Exception('Could not get OSR SpatialReference from WKT: %s\nError:\n%s' % (self.wkt, msg)) |
|
178 else: |
|
179 raise Exception('GDAL is not installed.') |
|
180 |
|
181 @property |
|
182 def ellipsoid(self): |
|
183 """ |
|
184 Returns a tuple of the ellipsoid parameters: |
|
185 (semimajor axis, semiminor axis, and inverse flattening). |
|
186 """ |
|
187 if gdal.HAS_GDAL: |
|
188 return self.srs.ellipsoid |
|
189 else: |
|
190 m = self.spheroid_regex.match(self.wkt) |
|
191 if m: return (float(m.group('major')), float(m.group('flattening'))) |
|
192 else: return None |
|
193 |
|
194 @property |
|
195 def name(self): |
|
196 "Returns the projection name." |
|
197 return self.srs.name |
|
198 |
|
199 @property |
|
200 def spheroid(self): |
|
201 "Returns the spheroid name for this spatial reference." |
|
202 return self.srs['spheroid'] |
|
203 |
|
204 @property |
|
205 def datum(self): |
|
206 "Returns the datum for this spatial reference." |
|
207 return self.srs['datum'] |
|
208 |
|
209 @property |
|
210 def projected(self): |
|
211 "Is this Spatial Reference projected?" |
|
212 if gdal.HAS_GDAL: |
|
213 return self.srs.projected |
|
214 else: |
|
215 return self.wkt.startswith('PROJCS') |
|
216 |
|
217 @property |
|
218 def local(self): |
|
219 "Is this Spatial Reference local?" |
|
220 if gdal.HAS_GDAL: |
|
221 return self.srs.local |
|
222 else: |
|
223 return self.wkt.startswith('LOCAL_CS') |
|
224 |
|
225 @property |
|
226 def geographic(self): |
|
227 "Is this Spatial Reference geographic?" |
|
228 if gdal.HAS_GDAL: |
|
229 return self.srs.geographic |
|
230 else: |
|
231 return self.wkt.startswith('GEOGCS') |
|
232 |
|
233 @property |
|
234 def linear_name(self): |
|
235 "Returns the linear units name." |
|
236 if gdal.HAS_GDAL: |
|
237 return self.srs.linear_name |
|
238 elif self.geographic: |
|
239 return None |
|
240 else: |
|
241 m = self.units_regex.match(self.wkt) |
|
242 return m.group('unit_name') |
|
243 |
|
244 @property |
|
245 def linear_units(self): |
|
246 "Returns the linear units." |
|
247 if gdal.HAS_GDAL: |
|
248 return self.srs.linear_units |
|
249 elif self.geographic: |
|
250 return None |
|
251 else: |
|
252 m = self.units_regex.match(self.wkt) |
|
253 return m.group('unit') |
|
254 |
|
255 @property |
|
256 def angular_name(self): |
|
257 "Returns the name of the angular units." |
|
258 if gdal.HAS_GDAL: |
|
259 return self.srs.angular_name |
|
260 elif self.projected: |
|
261 return None |
|
262 else: |
|
263 m = self.units_regex.match(self.wkt) |
|
264 return m.group('unit_name') |
|
265 |
|
266 @property |
|
267 def angular_units(self): |
|
268 "Returns the angular units." |
|
269 if gdal.HAS_GDAL: |
|
270 return self.srs.angular_units |
|
271 elif self.projected: |
|
272 return None |
|
273 else: |
|
274 m = self.units_regex.match(self.wkt) |
|
275 return m.group('unit') |
|
276 |
|
277 @property |
|
278 def units(self): |
|
279 "Returns a tuple of the units and the name." |
|
280 if self.projected or self.local: |
|
281 return (self.linear_units, self.linear_name) |
|
282 elif self.geographic: |
|
283 return (self.angular_units, self.angular_name) |
|
284 else: |
|
285 return (None, None) |
|
286 |
|
287 @classmethod |
|
288 def get_units(cls, wkt): |
|
289 """ |
|
290 Class method used by GeometryField on initialization to |
|
291 retrive the units on the given WKT, without having to use |
|
292 any of the database fields. |
|
293 """ |
|
294 if gdal.HAS_GDAL: |
|
295 return gdal.SpatialReference(wkt).units |
|
296 else: |
|
297 m = cls.units_regex.match(wkt) |
|
298 return m.group('unit'), m.group('unit_name') |
|
299 |
|
300 @classmethod |
|
301 def get_spheroid(cls, wkt, string=True): |
|
302 """ |
|
303 Class method used by GeometryField on initialization to |
|
304 retrieve the `SPHEROID[..]` parameters from the given WKT. |
|
305 """ |
|
306 if gdal.HAS_GDAL: |
|
307 srs = gdal.SpatialReference(wkt) |
|
308 sphere_params = srs.ellipsoid |
|
309 sphere_name = srs['spheroid'] |
|
310 else: |
|
311 m = cls.spheroid_regex.match(wkt) |
|
312 if m: |
|
313 sphere_params = (float(m.group('major')), float(m.group('flattening'))) |
|
314 sphere_name = m.group('name') |
|
315 else: |
|
316 return None |
|
317 |
|
318 if not string: |
|
319 return sphere_name, sphere_params |
|
320 else: |
|
321 # `string` parameter used to place in format acceptable by PostGIS |
|
322 if len(sphere_params) == 3: |
|
323 radius, flattening = sphere_params[0], sphere_params[2] |
|
324 else: |
|
325 radius, flattening = sphere_params |
|
326 return 'SPHEROID["%s",%s,%s]' % (sphere_name, radius, flattening) |
|
327 |
|
328 def __unicode__(self): |
|
329 """ |
|
330 Returns the string representation. If GDAL is installed, |
|
331 it will be 'pretty' OGC WKT. |
|
332 """ |
|
333 try: |
|
334 return unicode(self.srs) |
|
335 except: |
|
336 return unicode(self.wkt) |