|
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 """ |
|
30 Distance and Area objects to allow for sensible and convienient calculation |
|
31 and conversions. |
|
32 |
|
33 Authors: Robert Coup, Justin Bronn |
|
34 |
|
35 Inspired by GeoPy (http://exogen.case.edu/projects/geopy/) |
|
36 and Geoff Biggs' PhD work on dimensioned units for robotics. |
|
37 """ |
|
38 __all__ = ['A', 'Area', 'D', 'Distance'] |
|
39 from decimal import Decimal |
|
40 |
|
41 class 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 |
|
87 class 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 |
|
247 class 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 |
|
335 D = Distance |
|
336 A = Area |