|
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 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 unit in self.UNITS: |
|
50 val += self.UNITS[unit] * value |
|
51 default_unit = unit |
|
52 elif unit in self.ALIAS: |
|
53 u = self.ALIAS[unit] |
|
54 val += self.UNITS[u] * value |
|
55 default_unit = u |
|
56 else: |
|
57 lower = unit.lower() |
|
58 if lower in self.UNITS: |
|
59 val += self.UNITS[lower] * value |
|
60 default_unit = lower |
|
61 elif lower in self.LALIAS: |
|
62 u = self.LALIAS[lower] |
|
63 val += self.UNITS[u] * value |
|
64 default_unit = u |
|
65 else: |
|
66 raise AttributeError('Unknown unit type: %s' % unit) |
|
67 return val, default_unit |
|
68 |
|
69 @classmethod |
|
70 def unit_attname(cls, unit_str): |
|
71 """ |
|
72 Retrieves the unit attribute name for the given unit string. |
|
73 For example, if the given unit string is 'metre', 'm' would be returned. |
|
74 An exception is raised if an attribute cannot be found. |
|
75 """ |
|
76 lower = unit_str.lower() |
|
77 if unit_str in cls.UNITS: |
|
78 return unit_str |
|
79 elif lower in cls.UNITS: |
|
80 return lower |
|
81 elif lower in cls.LALIAS: |
|
82 return cls.LALIAS[lower] |
|
83 else: |
|
84 raise Exception('Could not find a unit keyword associated with "%s"' % unit_str) |
|
85 |
|
86 class Distance(MeasureBase): |
|
87 UNITS = { |
|
88 'chain' : 20.1168, |
|
89 'chain_benoit' : 20.116782, |
|
90 'chain_sears' : 20.1167645, |
|
91 'british_chain_benoit' : 20.1167824944, |
|
92 'british_chain_sears' : 20.1167651216, |
|
93 'british_chain_sears_truncated' : 20.116756, |
|
94 'cm' : 0.01, |
|
95 'british_ft' : 0.304799471539, |
|
96 'british_yd' : 0.914398414616, |
|
97 'clarke_ft' : 0.3047972654, |
|
98 'clarke_link' : 0.201166195164, |
|
99 'fathom' : 1.8288, |
|
100 'ft': 0.3048, |
|
101 'german_m' : 1.0000135965, |
|
102 'gold_coast_ft' : 0.304799710181508, |
|
103 'indian_yd' : 0.914398530744, |
|
104 'inch' : 0.0254, |
|
105 'km': 1000.0, |
|
106 'link' : 0.201168, |
|
107 'link_benoit' : 0.20116782, |
|
108 'link_sears' : 0.20116765, |
|
109 'm': 1.0, |
|
110 'mi': 1609.344, |
|
111 'mm' : 0.001, |
|
112 'nm': 1852.0, |
|
113 'nm_uk' : 1853.184, |
|
114 'rod' : 5.0292, |
|
115 'sears_yd' : 0.91439841, |
|
116 'survey_ft' : 0.304800609601, |
|
117 'um' : 0.000001, |
|
118 'yd': 0.9144, |
|
119 } |
|
120 |
|
121 # Unit aliases for `UNIT` terms encountered in Spatial Reference WKT. |
|
122 ALIAS = { |
|
123 'centimeter' : 'cm', |
|
124 'foot' : 'ft', |
|
125 'inches' : 'inch', |
|
126 'kilometer' : 'km', |
|
127 'kilometre' : 'km', |
|
128 'meter' : 'm', |
|
129 'metre' : 'm', |
|
130 'micrometer' : 'um', |
|
131 'micrometre' : 'um', |
|
132 'millimeter' : 'mm', |
|
133 'millimetre' : 'mm', |
|
134 'mile' : 'mi', |
|
135 'yard' : 'yd', |
|
136 'British chain (Benoit 1895 B)' : 'british_chain_benoit', |
|
137 'British chain (Sears 1922)' : 'british_chain_sears', |
|
138 'British chain (Sears 1922 truncated)' : 'british_chain_sears_truncated', |
|
139 'British foot (Sears 1922)' : 'british_ft', |
|
140 'British foot' : 'british_ft', |
|
141 'British yard (Sears 1922)' : 'british_yd', |
|
142 'British yard' : 'british_yd', |
|
143 "Clarke's Foot" : 'clarke_ft', |
|
144 "Clarke's link" : 'clarke_link', |
|
145 'Chain (Benoit)' : 'chain_benoit', |
|
146 'Chain (Sears)' : 'chain_sears', |
|
147 'Foot (International)' : 'ft', |
|
148 'German legal metre' : 'german_m', |
|
149 'Gold Coast foot' : 'gold_coast_ft', |
|
150 'Indian yard' : 'indian_yd', |
|
151 'Link (Benoit)': 'link_benoit', |
|
152 'Link (Sears)': 'link_sears', |
|
153 'Nautical Mile' : 'nm', |
|
154 'Nautical Mile (UK)' : 'nm_uk', |
|
155 'US survey foot' : 'survey_ft', |
|
156 'U.S. Foot' : 'survey_ft', |
|
157 'Yard (Indian)' : 'indian_yd', |
|
158 'Yard (Sears)' : 'sears_yd' |
|
159 } |
|
160 LALIAS = dict([(k.lower(), v) for k, v in ALIAS.items()]) |
|
161 |
|
162 def __init__(self, default_unit=None, **kwargs): |
|
163 # The base unit is in meters. |
|
164 self.m, self._default_unit = self.default_units(kwargs) |
|
165 if default_unit and isinstance(default_unit, str): |
|
166 self._default_unit = default_unit |
|
167 |
|
168 def __getattr__(self, name): |
|
169 if name in self.UNITS: |
|
170 return self.m / self.UNITS[name] |
|
171 else: |
|
172 raise AttributeError('Unknown unit type: %s' % name) |
|
173 |
|
174 def __repr__(self): |
|
175 return 'Distance(%s=%s)' % (self._default_unit, getattr(self, self._default_unit)) |
|
176 |
|
177 def __str__(self): |
|
178 return '%s %s' % (getattr(self, self._default_unit), self._default_unit) |
|
179 |
|
180 def __cmp__(self, other): |
|
181 if isinstance(other, Distance): |
|
182 return cmp(self.m, other.m) |
|
183 else: |
|
184 return NotImplemented |
|
185 |
|
186 def __add__(self, other): |
|
187 if isinstance(other, Distance): |
|
188 return Distance(default_unit=self._default_unit, m=(self.m + other.m)) |
|
189 else: |
|
190 raise TypeError('Distance must be added with Distance') |
|
191 |
|
192 def __iadd__(self, other): |
|
193 if isinstance(other, Distance): |
|
194 self.m += other.m |
|
195 return self |
|
196 else: |
|
197 raise TypeError('Distance must be added with Distance') |
|
198 |
|
199 def __sub__(self, other): |
|
200 if isinstance(other, Distance): |
|
201 return Distance(default_unit=self._default_unit, m=(self.m - other.m)) |
|
202 else: |
|
203 raise TypeError('Distance must be subtracted from Distance') |
|
204 |
|
205 def __isub__(self, other): |
|
206 if isinstance(other, Distance): |
|
207 self.m -= other.m |
|
208 return self |
|
209 else: |
|
210 raise TypeError('Distance must be subtracted from Distance') |
|
211 |
|
212 def __mul__(self, other): |
|
213 if isinstance(other, (int, float, long, Decimal)): |
|
214 return Distance(default_unit=self._default_unit, m=(self.m * float(other))) |
|
215 elif isinstance(other, Distance): |
|
216 return Area(default_unit='sq_' + self._default_unit, sq_m=(self.m * other.m)) |
|
217 else: |
|
218 raise TypeError('Distance must be multiplied with number or Distance') |
|
219 |
|
220 def __imul__(self, other): |
|
221 if isinstance(other, (int, float, long, Decimal)): |
|
222 self.m *= float(other) |
|
223 return self |
|
224 else: |
|
225 raise TypeError('Distance must be multiplied with number') |
|
226 |
|
227 def __div__(self, other): |
|
228 if isinstance(other, (int, float, long, Decimal)): |
|
229 return Distance(default_unit=self._default_unit, m=(self.m / float(other))) |
|
230 else: |
|
231 raise TypeError('Distance must be divided with number') |
|
232 |
|
233 def __idiv__(self, other): |
|
234 if isinstance(other, (int, float, long, Decimal)): |
|
235 self.m /= float(other) |
|
236 return self |
|
237 else: |
|
238 raise TypeError('Distance must be divided with number') |
|
239 |
|
240 def __nonzero__(self): |
|
241 return bool(self.m) |
|
242 |
|
243 class Area(MeasureBase): |
|
244 # Getting the square units values and the alias dictionary. |
|
245 UNITS = dict([('sq_%s' % k, v ** 2) for k, v in Distance.UNITS.items()]) |
|
246 ALIAS = dict([(k, 'sq_%s' % v) for k, v in Distance.ALIAS.items()]) |
|
247 LALIAS = dict([(k.lower(), v) for k, v in ALIAS.items()]) |
|
248 |
|
249 def __init__(self, default_unit=None, **kwargs): |
|
250 self.sq_m, self._default_unit = self.default_units(kwargs) |
|
251 if default_unit and isinstance(default_unit, str): |
|
252 self._default_unit = default_unit |
|
253 |
|
254 def __getattr__(self, name): |
|
255 if name in self.UNITS: |
|
256 return self.sq_m / self.UNITS[name] |
|
257 else: |
|
258 raise AttributeError('Unknown unit type: ' + name) |
|
259 |
|
260 def __repr__(self): |
|
261 return 'Area(%s=%s)' % (self._default_unit, getattr(self, self._default_unit)) |
|
262 |
|
263 def __str__(self): |
|
264 return '%s %s' % (getattr(self, self._default_unit), self._default_unit) |
|
265 |
|
266 def __cmp__(self, other): |
|
267 if isinstance(other, Area): |
|
268 return cmp(self.sq_m, other.sq_m) |
|
269 else: |
|
270 return NotImplemented |
|
271 |
|
272 def __add__(self, other): |
|
273 if isinstance(other, Area): |
|
274 return Area(default_unit=self._default_unit, sq_m=(self.sq_m + other.sq_m)) |
|
275 else: |
|
276 raise TypeError('Area must be added with Area') |
|
277 |
|
278 def __iadd__(self, other): |
|
279 if isinstance(other, Area): |
|
280 self.sq_m += other.sq_m |
|
281 return self |
|
282 else: |
|
283 raise TypeError('Area must be added with Area') |
|
284 |
|
285 def __sub__(self, other): |
|
286 if isinstance(other, Area): |
|
287 return Area(default_unit=self._default_unit, sq_m=(self.sq_m - other.sq_m)) |
|
288 else: |
|
289 raise TypeError('Area must be subtracted from Area') |
|
290 |
|
291 def __isub__(self, other): |
|
292 if isinstance(other, Area): |
|
293 self.sq_m -= other.sq_m |
|
294 return self |
|
295 else: |
|
296 raise TypeError('Area must be subtracted from Area') |
|
297 |
|
298 def __mul__(self, other): |
|
299 if isinstance(other, (int, float, long, Decimal)): |
|
300 return Area(default_unit=self._default_unit, sq_m=(self.sq_m * float(other))) |
|
301 else: |
|
302 raise TypeError('Area must be multiplied with number') |
|
303 |
|
304 def __imul__(self, other): |
|
305 if isinstance(other, (int, float, long, Decimal)): |
|
306 self.sq_m *= float(other) |
|
307 return self |
|
308 else: |
|
309 raise TypeError('Area must be multiplied with number') |
|
310 |
|
311 def __div__(self, other): |
|
312 if isinstance(other, (int, float, long, Decimal)): |
|
313 return Area(default_unit=self._default_unit, sq_m=(self.sq_m / float(other))) |
|
314 else: |
|
315 raise TypeError('Area must be divided with number') |
|
316 |
|
317 def __idiv__(self, other): |
|
318 if isinstance(other, (int, float, long, Decimal)): |
|
319 self.sq_m /= float(other) |
|
320 return self |
|
321 else: |
|
322 raise TypeError('Area must be divided with number') |
|
323 |
|
324 def __nonzero__(self): |
|
325 return bool(self.sq_m) |
|
326 |
|
327 # Shortcuts |
|
328 D = Distance |
|
329 A = Area |