|
1 from django.contrib.gis import forms |
|
2 # Getting the SpatialBackend container and the geographic quoting method. |
|
3 from django.contrib.gis.db.backend import SpatialBackend, gqn |
|
4 # GeometryProxy, GEOS, and Distance imports. |
|
5 from django.contrib.gis.db.models.proxy import GeometryProxy |
|
6 from django.contrib.gis.measure import Distance |
|
7 # The `get_srid_info` function gets SRID information from the spatial |
|
8 # reference system table w/o using the ORM. |
|
9 from django.contrib.gis.models import get_srid_info |
|
10 |
|
11 #TODO: Flesh out widgets; consider adding support for OGR Geometry proxies. |
|
12 class GeometryField(SpatialBackend.Field): |
|
13 "The base GIS field -- maps to the OpenGIS Specification Geometry type." |
|
14 |
|
15 # The OpenGIS Geometry name. |
|
16 _geom = 'GEOMETRY' |
|
17 |
|
18 # Geodetic units. |
|
19 geodetic_units = ('Decimal Degree', 'degree') |
|
20 |
|
21 def __init__(self, verbose_name=None, srid=4326, spatial_index=True, dim=2, **kwargs): |
|
22 """ |
|
23 The initialization function for geometry fields. Takes the following |
|
24 as keyword arguments: |
|
25 |
|
26 srid: |
|
27 The spatial reference system identifier, an OGC standard. |
|
28 Defaults to 4326 (WGS84). |
|
29 |
|
30 spatial_index: |
|
31 Indicates whether to create a spatial index. Defaults to True. |
|
32 Set this instead of 'db_index' for geographic fields since index |
|
33 creation is different for geometry columns. |
|
34 |
|
35 dim: |
|
36 The number of dimensions for this geometry. Defaults to 2. |
|
37 """ |
|
38 |
|
39 # Setting the index flag with the value of the `spatial_index` keyword. |
|
40 self._index = spatial_index |
|
41 |
|
42 # Setting the SRID and getting the units. Unit information must be |
|
43 # easily available in the field instance for distance queries. |
|
44 self._srid = srid |
|
45 self._unit, self._unit_name, self._spheroid = get_srid_info(srid) |
|
46 |
|
47 # Setting the dimension of the geometry field. |
|
48 self._dim = dim |
|
49 |
|
50 # Setting the verbose_name keyword argument with the positional |
|
51 # first parameter, so this works like normal fields. |
|
52 kwargs['verbose_name'] = verbose_name |
|
53 |
|
54 super(GeometryField, self).__init__(**kwargs) # Calling the parent initializtion function |
|
55 |
|
56 ### Routines specific to GeometryField ### |
|
57 @property |
|
58 def geodetic(self): |
|
59 """ |
|
60 Returns true if this field's SRID corresponds with a coordinate |
|
61 system that uses non-projected units (e.g., latitude/longitude). |
|
62 """ |
|
63 return self._unit_name in self.geodetic_units |
|
64 |
|
65 def get_distance(self, dist_val, lookup_type): |
|
66 """ |
|
67 Returns a distance number in units of the field. For example, if |
|
68 `D(km=1)` was passed in and the units of the field were in meters, |
|
69 then 1000 would be returned. |
|
70 """ |
|
71 # Getting the distance parameter and any options. |
|
72 if len(dist_val) == 1: dist, option = dist_val[0], None |
|
73 else: dist, option = dist_val |
|
74 |
|
75 if isinstance(dist, Distance): |
|
76 if self.geodetic: |
|
77 # Won't allow Distance objects w/DWithin lookups on PostGIS. |
|
78 if SpatialBackend.postgis and lookup_type == 'dwithin': |
|
79 raise TypeError('Only numeric values of degree units are allowed on geographic DWithin queries.') |
|
80 # Spherical distance calculation parameter should be in meters. |
|
81 dist_param = dist.m |
|
82 else: |
|
83 dist_param = getattr(dist, Distance.unit_attname(self._unit_name)) |
|
84 else: |
|
85 # Assuming the distance is in the units of the field. |
|
86 dist_param = dist |
|
87 |
|
88 if SpatialBackend.postgis and self.geodetic and lookup_type != 'dwithin' and option == 'spheroid': |
|
89 # On PostGIS, by default `ST_distance_sphere` is used; but if the |
|
90 # accuracy of `ST_distance_spheroid` is needed than the spheroid |
|
91 # needs to be passed to the SQL stored procedure. |
|
92 return [gqn(self._spheroid), dist_param] |
|
93 else: |
|
94 return [dist_param] |
|
95 |
|
96 def get_geometry(self, value): |
|
97 """ |
|
98 Retrieves the geometry, setting the default SRID from the given |
|
99 lookup parameters. |
|
100 """ |
|
101 if isinstance(value, (tuple, list)): |
|
102 geom = value[0] |
|
103 else: |
|
104 geom = value |
|
105 |
|
106 # When the input is not a GEOS geometry, attempt to construct one |
|
107 # from the given string input. |
|
108 if isinstance(geom, SpatialBackend.Geometry): |
|
109 pass |
|
110 elif isinstance(geom, basestring): |
|
111 try: |
|
112 geom = SpatialBackend.Geometry(geom) |
|
113 except SpatialBackend.GeometryException: |
|
114 raise ValueError('Could not create geometry from lookup value: %s' % str(value)) |
|
115 else: |
|
116 raise TypeError('Cannot use parameter of `%s` type as lookup parameter.' % type(value)) |
|
117 |
|
118 # Assigning the SRID value. |
|
119 geom.srid = self.get_srid(geom) |
|
120 |
|
121 return geom |
|
122 |
|
123 def get_srid(self, geom): |
|
124 """ |
|
125 Returns the default SRID for the given geometry, taking into account |
|
126 the SRID set for the field. For example, if the input geometry |
|
127 has no SRID, then that of the field will be returned. |
|
128 """ |
|
129 gsrid = geom.srid # SRID of given geometry. |
|
130 if gsrid is None or self._srid == -1 or (gsrid == -1 and self._srid != -1): |
|
131 return self._srid |
|
132 else: |
|
133 return gsrid |
|
134 |
|
135 ### Routines overloaded from Field ### |
|
136 def contribute_to_class(self, cls, name): |
|
137 super(GeometryField, self).contribute_to_class(cls, name) |
|
138 |
|
139 # Setup for lazy-instantiated Geometry object. |
|
140 setattr(cls, self.attname, GeometryProxy(SpatialBackend.Geometry, self)) |
|
141 |
|
142 def formfield(self, **kwargs): |
|
143 defaults = {'form_class' : forms.GeometryField, |
|
144 'geom_type' : self._geom, |
|
145 'null' : self.null, |
|
146 } |
|
147 defaults.update(kwargs) |
|
148 return super(GeometryField, self).formfield(**defaults) |
|
149 |
|
150 def get_db_prep_lookup(self, lookup_type, value): |
|
151 """ |
|
152 Returns the spatial WHERE clause and associated parameters for the |
|
153 given lookup type and value. The value will be prepared for database |
|
154 lookup (e.g., spatial transformation SQL will be added if necessary). |
|
155 """ |
|
156 if lookup_type in SpatialBackend.gis_terms: |
|
157 # special case for isnull lookup |
|
158 if lookup_type == 'isnull': return [], [] |
|
159 |
|
160 # Get the geometry with SRID; defaults SRID to that of the field |
|
161 # if it is None. |
|
162 geom = self.get_geometry(value) |
|
163 |
|
164 # Getting the WHERE clause list and the associated params list. The params |
|
165 # list is populated with the Adaptor wrapping the Geometry for the |
|
166 # backend. The WHERE clause list contains the placeholder for the adaptor |
|
167 # (e.g. any transformation SQL). |
|
168 where = [self.get_placeholder(geom)] |
|
169 params = [SpatialBackend.Adaptor(geom)] |
|
170 |
|
171 if isinstance(value, (tuple, list)): |
|
172 if lookup_type in SpatialBackend.distance_functions: |
|
173 # Getting the distance parameter in the units of the field. |
|
174 where += self.get_distance(value[1:], lookup_type) |
|
175 elif lookup_type in SpatialBackend.limited_where: |
|
176 pass |
|
177 else: |
|
178 # Otherwise, making sure any other parameters are properly quoted. |
|
179 where += map(gqn, value[1:]) |
|
180 return where, params |
|
181 else: |
|
182 raise TypeError("Field has invalid lookup: %s" % lookup_type) |
|
183 |
|
184 def get_db_prep_save(self, value): |
|
185 "Prepares the value for saving in the database." |
|
186 if value is None: |
|
187 return None |
|
188 else: |
|
189 return SpatialBackend.Adaptor(self.get_geometry(value)) |
|
190 |
|
191 # The OpenGIS Geometry Type Fields |
|
192 class PointField(GeometryField): |
|
193 _geom = 'POINT' |
|
194 |
|
195 class LineStringField(GeometryField): |
|
196 _geom = 'LINESTRING' |
|
197 |
|
198 class PolygonField(GeometryField): |
|
199 _geom = 'POLYGON' |
|
200 |
|
201 class MultiPointField(GeometryField): |
|
202 _geom = 'MULTIPOINT' |
|
203 |
|
204 class MultiLineStringField(GeometryField): |
|
205 _geom = 'MULTILINESTRING' |
|
206 |
|
207 class MultiPolygonField(GeometryField): |
|
208 _geom = 'MULTIPOLYGON' |
|
209 |
|
210 class GeometryCollectionField(GeometryField): |
|
211 _geom = 'GEOMETRYCOLLECTION' |