/django/contrib/gis/measure.py
Python | 336 lines | 300 code | 0 blank | 36 comment | 15 complexity | fc0142a37292eb9c41893aabb9a7b12f MD5 | raw file
Possible License(s): BSD-3-Clause
1# Copyright (c) 2007, Robert Coup <robert.coup@onetrackmind.co.nz>
2# All rights reserved.
3#
4# Redistribution and use in source and binary forms, with or without modification,
5# are permitted provided that the following conditions are met:
6#
7# 1. Redistributions of source code must retain the above copyright notice,
8# this list of conditions and the following disclaimer.
9#
10# 2. Redistributions in binary form must reproduce the above copyright
11# notice, this list of conditions and the following disclaimer in the
12# documentation and/or other materials provided with the distribution.
13#
14# 3. Neither the name of Distance nor the names of its contributors may be used
15# to endorse or promote products derived from this software without
16# specific prior written permission.
17#
18# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
22# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
25# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28#
29"""
30Distance and Area objects to allow for sensible and convienient calculation
31and conversions.
32
33Authors: Robert Coup, Justin Bronn
34
35Inspired by GeoPy (http://exogen.case.edu/projects/geopy/)
36and Geoff Biggs' PhD work on dimensioned units for robotics.
37"""
38__all__ = ['A', 'Area', 'D', 'Distance']
39from decimal import Decimal
40
41class MeasureBase(object):
42 def default_units(self, kwargs):
43 """
44 Return the unit value and the default units specified
45 from the given keyword arguments dictionary.
46 """
47 val = 0.0
48 for unit, value in kwargs.iteritems():
49 if not isinstance(value, float): value = float(value)
50 if unit in self.UNITS:
51 val += self.UNITS[unit] * value
52 default_unit = unit
53 elif unit in self.ALIAS:
54 u = self.ALIAS[unit]
55 val += self.UNITS[u] * value
56 default_unit = u
57 else:
58 lower = unit.lower()
59 if lower in self.UNITS:
60 val += self.UNITS[lower] * value
61 default_unit = lower
62 elif lower in self.LALIAS:
63 u = self.LALIAS[lower]
64 val += self.UNITS[u] * value
65 default_unit = u
66 else:
67 raise AttributeError('Unknown unit type: %s' % unit)
68 return val, default_unit
69
70 @classmethod
71 def unit_attname(cls, unit_str):
72 """
73 Retrieves the unit attribute name for the given unit string.
74 For example, if the given unit string is 'metre', 'm' would be returned.
75 An exception is raised if an attribute cannot be found.
76 """
77 lower = unit_str.lower()
78 if unit_str in cls.UNITS:
79 return unit_str
80 elif lower in cls.UNITS:
81 return lower
82 elif lower in cls.LALIAS:
83 return cls.LALIAS[lower]
84 else:
85 raise Exception('Could not find a unit keyword associated with "%s"' % unit_str)
86
87class Distance(MeasureBase):
88 UNITS = {
89 'chain' : 20.1168,
90 'chain_benoit' : 20.116782,
91 'chain_sears' : 20.1167645,
92 'british_chain_benoit' : 20.1167824944,
93 'british_chain_sears' : 20.1167651216,
94 'british_chain_sears_truncated' : 20.116756,
95 'cm' : 0.01,
96 'british_ft' : 0.304799471539,
97 'british_yd' : 0.914398414616,
98 'clarke_ft' : 0.3047972654,
99 'clarke_link' : 0.201166195164,
100 'fathom' : 1.8288,
101 'ft': 0.3048,
102 'german_m' : 1.0000135965,
103 'gold_coast_ft' : 0.304799710181508,
104 'indian_yd' : 0.914398530744,
105 'inch' : 0.0254,
106 'km': 1000.0,
107 'link' : 0.201168,
108 'link_benoit' : 0.20116782,
109 'link_sears' : 0.20116765,
110 'm': 1.0,
111 'mi': 1609.344,
112 'mm' : 0.001,
113 'nm': 1852.0,
114 'nm_uk' : 1853.184,
115 'rod' : 5.0292,
116 'sears_yd' : 0.91439841,
117 'survey_ft' : 0.304800609601,
118 'um' : 0.000001,
119 'yd': 0.9144,
120 }
121
122 # Unit aliases for `UNIT` terms encountered in Spatial Reference WKT.
123 ALIAS = {
124 'centimeter' : 'cm',
125 'foot' : 'ft',
126 'inches' : 'inch',
127 'kilometer' : 'km',
128 'kilometre' : 'km',
129 'meter' : 'm',
130 'metre' : 'm',
131 'micrometer' : 'um',
132 'micrometre' : 'um',
133 'millimeter' : 'mm',
134 'millimetre' : 'mm',
135 'mile' : 'mi',
136 'yard' : 'yd',
137 'British chain (Benoit 1895 B)' : 'british_chain_benoit',
138 'British chain (Sears 1922)' : 'british_chain_sears',
139 'British chain (Sears 1922 truncated)' : 'british_chain_sears_truncated',
140 'British foot (Sears 1922)' : 'british_ft',
141 'British foot' : 'british_ft',
142 'British yard (Sears 1922)' : 'british_yd',
143 'British yard' : 'british_yd',
144 "Clarke's Foot" : 'clarke_ft',
145 "Clarke's link" : 'clarke_link',
146 'Chain (Benoit)' : 'chain_benoit',
147 'Chain (Sears)' : 'chain_sears',
148 'Foot (International)' : 'ft',
149 'German legal metre' : 'german_m',
150 'Gold Coast foot' : 'gold_coast_ft',
151 'Indian yard' : 'indian_yd',
152 'Link (Benoit)': 'link_benoit',
153 'Link (Sears)': 'link_sears',
154 'Nautical Mile' : 'nm',
155 'Nautical Mile (UK)' : 'nm_uk',
156 'US survey foot' : 'survey_ft',
157 'U.S. Foot' : 'survey_ft',
158 'Yard (Indian)' : 'indian_yd',
159 'Yard (Sears)' : 'sears_yd'
160 }
161 LALIAS = dict([(k.lower(), v) for k, v in ALIAS.items()])
162
163 def __init__(self, default_unit=None, **kwargs):
164 # The base unit is in meters.
165 self.m, self._default_unit = self.default_units(kwargs)
166 if default_unit and isinstance(default_unit, str):
167 self._default_unit = default_unit
168
169 def __getattr__(self, name):
170 if name in self.UNITS:
171 return self.m / self.UNITS[name]
172 else:
173 raise AttributeError('Unknown unit type: %s' % name)
174
175 def __repr__(self):
176 return 'Distance(%s=%s)' % (self._default_unit, getattr(self, self._default_unit))
177
178 def __str__(self):
179 return '%s %s' % (getattr(self, self._default_unit), self._default_unit)
180
181 def __cmp__(self, other):
182 if isinstance(other, Distance):
183 return cmp(self.m, other.m)
184 else:
185 return NotImplemented
186
187 def __add__(self, other):
188 if isinstance(other, Distance):
189 return Distance(default_unit=self._default_unit, m=(self.m + other.m))
190 else:
191 raise TypeError('Distance must be added with Distance')
192
193 def __iadd__(self, other):
194 if isinstance(other, Distance):
195 self.m += other.m
196 return self
197 else:
198 raise TypeError('Distance must be added with Distance')
199
200 def __sub__(self, other):
201 if isinstance(other, Distance):
202 return Distance(default_unit=self._default_unit, m=(self.m - other.m))
203 else:
204 raise TypeError('Distance must be subtracted from Distance')
205
206 def __isub__(self, other):
207 if isinstance(other, Distance):
208 self.m -= other.m
209 return self
210 else:
211 raise TypeError('Distance must be subtracted from Distance')
212
213 def __mul__(self, other):
214 if isinstance(other, (int, float, long, Decimal)):
215 return Distance(default_unit=self._default_unit, m=(self.m * float(other)))
216 elif isinstance(other, Distance):
217 return Area(default_unit='sq_' + self._default_unit, sq_m=(self.m * other.m))
218 else:
219 raise TypeError('Distance must be multiplied with number or Distance')
220
221 def __imul__(self, other):
222 if isinstance(other, (int, float, long, Decimal)):
223 self.m *= float(other)
224 return self
225 else:
226 raise TypeError('Distance must be multiplied with number')
227
228 def __rmul__(self, other):
229 return self * other
230
231 def __div__(self, other):
232 if isinstance(other, (int, float, long, Decimal)):
233 return Distance(default_unit=self._default_unit, m=(self.m / float(other)))
234 else:
235 raise TypeError('Distance must be divided with number')
236
237 def __idiv__(self, other):
238 if isinstance(other, (int, float, long, Decimal)):
239 self.m /= float(other)
240 return self
241 else:
242 raise TypeError('Distance must be divided with number')
243
244 def __nonzero__(self):
245 return bool(self.m)
246
247class Area(MeasureBase):
248 # Getting the square units values and the alias dictionary.
249 UNITS = dict([('sq_%s' % k, v ** 2) for k, v in Distance.UNITS.items()])
250 ALIAS = dict([(k, 'sq_%s' % v) for k, v in Distance.ALIAS.items()])
251 LALIAS = dict([(k.lower(), v) for k, v in ALIAS.items()])
252
253 def __init__(self, default_unit=None, **kwargs):
254 self.sq_m, self._default_unit = self.default_units(kwargs)
255 if default_unit and isinstance(default_unit, str):
256 self._default_unit = default_unit
257
258 def __getattr__(self, name):
259 if name in self.UNITS:
260 return self.sq_m / self.UNITS[name]
261 else:
262 raise AttributeError('Unknown unit type: ' + name)
263
264 def __repr__(self):
265 return 'Area(%s=%s)' % (self._default_unit, getattr(self, self._default_unit))
266
267 def __str__(self):
268 return '%s %s' % (getattr(self, self._default_unit), self._default_unit)
269
270 def __cmp__(self, other):
271 if isinstance(other, Area):
272 return cmp(self.sq_m, other.sq_m)
273 else:
274 return NotImplemented
275
276 def __add__(self, other):
277 if isinstance(other, Area):
278 return Area(default_unit=self._default_unit, sq_m=(self.sq_m + other.sq_m))
279 else:
280 raise TypeError('Area must be added with Area')
281
282 def __iadd__(self, other):
283 if isinstance(other, Area):
284 self.sq_m += other.sq_m
285 return self
286 else:
287 raise TypeError('Area must be added with Area')
288
289 def __sub__(self, other):
290 if isinstance(other, Area):
291 return Area(default_unit=self._default_unit, sq_m=(self.sq_m - other.sq_m))
292 else:
293 raise TypeError('Area must be subtracted from Area')
294
295 def __isub__(self, other):
296 if isinstance(other, Area):
297 self.sq_m -= other.sq_m
298 return self
299 else:
300 raise TypeError('Area must be subtracted from Area')
301
302 def __mul__(self, other):
303 if isinstance(other, (int, float, long, Decimal)):
304 return Area(default_unit=self._default_unit, sq_m=(self.sq_m * float(other)))
305 else:
306 raise TypeError('Area must be multiplied with number')
307
308 def __imul__(self, other):
309 if isinstance(other, (int, float, long, Decimal)):
310 self.sq_m *= float(other)
311 return self
312 else:
313 raise TypeError('Area must be multiplied with number')
314
315 def __rmul__(self, other):
316 return self * other
317
318 def __div__(self, other):
319 if isinstance(other, (int, float, long, Decimal)):
320 return Area(default_unit=self._default_unit, sq_m=(self.sq_m / float(other)))
321 else:
322 raise TypeError('Area must be divided with number')
323
324 def __idiv__(self, other):
325 if isinstance(other, (int, float, long, Decimal)):
326 self.sq_m /= float(other)
327 return self
328 else:
329 raise TypeError('Area must be divided with number')
330
331 def __nonzero__(self):
332 return bool(self.sq_m)
333
334# Shortcuts
335D = Distance
336A = Area