Ticket #17754: measure.py

File measure.py, 15.9 KB (added by Riccardo Di Virgilio, 12 years ago)

full .py file

Line 
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', 'V', 'Volume', 'W', 'Weight']
39from decimal import Decimal
40
41def is_number(obj):
42 return isinstance(obj, (int, float, long, Decimal))
43
44def pretty_name(obj):
45 if obj.__class__ == type:
46 return obj.__name__
47 return obj.__class__.__name__
48
49class MeasureBase(object):
50
51 STANDARD_UNIT = None
52 ALIAS = {}
53 UNITS = {}
54 LALIAS = {}
55
56 def __init__(self, default_unit=None, **kwargs):
57 # The base unit is in meters.
58 value, self._default_unit = self.default_units(kwargs)
59 setattr(self, self.STANDARD_UNIT, value)
60 if default_unit and isinstance(default_unit, str):
61 self._default_unit = default_unit
62
63 def _get_standard(self):
64 return getattr(self, self.STANDARD_UNIT)
65
66 def _set_standard(self, value):
67 setattr(self, self.STANDARD_UNIT, value)
68
69 standard = property(_get_standard, _set_standard)
70
71 def __len__(self):
72 return len(str(self))
73
74 def __getattr__(self, name):
75 if name in self.UNITS:
76 return self.standard / self.UNITS[name]
77 else:
78 raise AttributeError('Unknown unit type: %s' % name)
79
80 def __repr__(self):
81 return '%s(%s=%s)' % (pretty_name(self), self._default_unit, getattr(self, self._default_unit))
82
83 def __str__(self):
84 val = getattr(self, self._default_unit)
85 if val == int(val):
86 val = int(val)
87 return '%s %s' % (val, self._default_unit)
88
89 def __cmp__(self, other):
90 if isinstance(other, self.__class__):
91 return cmp(self.standard, other.standard)
92 else:
93 return NotImplemented
94
95 def __add__(self, other):
96 if isinstance(other, self.__class__):
97 return self.__class__(default_unit=self._default_unit,
98 **{self.STANDARD_UNIT: (self.standard + other.standard)})
99 else:
100 raise TypeError('%(class)s must be added with %(class)s' % {"class":pretty_name(self)})
101
102 def __iadd__(self, other):
103 if isinstance(other, self.__class__):
104 self.standard += other.standard
105 return self
106 else:
107 raise TypeError('%(class)s must be added with %(class)s' % {"class":pretty_name(self)})
108
109 def __sub__(self, other):
110 if isinstance(other, self.__class__):
111 return self.__class__(default_unit=self._default_unit,
112 **{self.STANDARD_UNIT: (self.standard - other.standard)})
113 else:
114 raise TypeError('%(class)s must be subtracted from %(class)s' % {"class":pretty_name(self)})
115
116 def __isub__(self, other):
117 if isinstance(other, self.__class__):
118 self.standard -= other.standard
119 return self
120 else:
121 raise TypeError('%(class)s must be subtracted from %(class)s' % {"class":pretty_name(self)})
122
123 def __mul__(self, other):
124 if is_number(other):
125 return self.__class__(default_unit=self._default_unit,
126 **{self.STANDARD_UNIT: (self.standard * other)})
127 else:
128 raise TypeError('%(class)s must be multiplied with number' % {"class":pretty_name(self)})
129
130 def __imul__(self, other):
131 if is_number(other):
132 self.standard *= float(other)
133 return self
134 else:
135 raise TypeError('%(class)s must be multiplied with number' % {"class":pretty_name(self)})
136
137 def __rmul__(self, other):
138 return self * other
139
140 def __div__(self, other):
141 if isinstance(other, self.__class__):
142 return self.standard / other.standard
143 if is_number(other):
144 return self.__class__(default_unit=self._default_unit,
145 **{self.STANDARD_UNIT: (self.standard / other)})
146 else:
147 raise TypeError('%(class)s must be divided with number or %(class)s' % {"class":pretty_name(self)})
148
149 def __idiv__(self, other):
150 if is_number(other):
151 self.standard /= float(other)
152 return self
153 else:
154 raise TypeError('%(class)s must be divided with number' % {"class":pretty_name(self)})
155
156 def __nonzero__(self):
157 return bool(self.standard)
158
159 def default_units(self, kwargs):
160 """
161 Return the unit value and the default units specified
162 from the given keyword arguments dictionary.
163 """
164 val = 0.0
165 default_unit = self.STANDARD_UNIT
166 for unit, value in kwargs.iteritems():
167 if not isinstance(value, float): value = float(value)
168 if unit in self.UNITS:
169 val += self.UNITS[unit] * value
170 default_unit = unit
171 elif unit in self.ALIAS:
172 u = self.ALIAS[unit]
173 val += self.UNITS[u] * value
174 default_unit = u
175 else:
176 lower = unit.lower()
177 if lower in self.UNITS:
178 val += self.UNITS[lower] * value
179 default_unit = lower
180 elif lower in self.LALIAS:
181 u = self.LALIAS[lower]
182 val += self.UNITS[u] * value
183 default_unit = u
184 else:
185 raise AttributeError('Unknown unit type: %s' % unit)
186 return val, default_unit
187
188 @classmethod
189 def unit_attname(cls, unit_str):
190 """
191 Retrieves the unit attribute name for the given unit string.
192 For example, if the given unit string is 'metre', 'm' would be returned.
193 An exception is raised if an attribute cannot be found.
194 """
195 lower = unit_str.lower()
196 if unit_str in cls.UNITS:
197 return unit_str
198 elif lower in cls.UNITS:
199 return lower
200 elif lower in cls.LALIAS:
201 return cls.LALIAS[lower]
202 else:
203 raise Exception('Could not find a unit keyword associated with "%s"' % unit_str)
204
205class Distance(MeasureBase):
206 STANDARD_UNIT = "m"
207 UNITS = {
208 'chain' : 20.1168,
209 'chain_benoit' : 20.116782,
210 'chain_sears' : 20.1167645,
211 'british_chain_benoit' : 20.1167824944,
212 'british_chain_sears' : 20.1167651216,
213 'british_chain_sears_truncated' : 20.116756,
214 'cm' : 0.01,
215 'british_ft' : 0.304799471539,
216 'british_yd' : 0.914398414616,
217 'clarke_ft' : 0.3047972654,
218 'clarke_link' : 0.201166195164,
219 'fathom' : 1.8288,
220 'ft': 0.3048,
221 'german_m' : 1.0000135965,
222 'gold_coast_ft' : 0.304799710181508,
223 'indian_yd' : 0.914398530744,
224 'inch' : 0.0254,
225 'km': 1000.0,
226 'link' : 0.201168,
227 'link_benoit' : 0.20116782,
228 'link_sears' : 0.20116765,
229 'm': 1.0,
230 'mi': 1609.344,
231 'mm' : 0.001,
232 'nm': 1852.0,
233 'nm_uk' : 1853.184,
234 'rod' : 5.0292,
235 'sears_yd' : 0.91439841,
236 'survey_ft' : 0.304800609601,
237 'um' : 0.000001,
238 'yd': 0.9144,
239 'pt': 0.0254 / 72,
240 }
241
242 # Unit aliases for `UNIT` terms encountered in Spatial Reference WKT.
243 ALIAS = {
244 'centimeter' : 'cm',
245 'foot' : 'ft',
246 'inches' : 'inch',
247 'kilometer' : 'km',
248 'kilometre' : 'km',
249 'meter' : 'm',
250 'metre' : 'm',
251 'micrometer' : 'um',
252 'micrometre' : 'um',
253 'millimeter' : 'mm',
254 'millimetre' : 'mm',
255 'mile' : 'mi',
256 'yard' : 'yd',
257 'British chain (Benoit 1895 B)' : 'british_chain_benoit',
258 'British chain (Sears 1922)' : 'british_chain_sears',
259 'British chain (Sears 1922 truncated)' : 'british_chain_sears_truncated',
260 'British foot (Sears 1922)' : 'british_ft',
261 'British foot' : 'british_ft',
262 'British yard (Sears 1922)' : 'british_yd',
263 'British yard' : 'british_yd',
264 "Clarke's Foot" : 'clarke_ft',
265 "Clarke's link" : 'clarke_link',
266 'Chain (Benoit)' : 'chain_benoit',
267 'Chain (Sears)' : 'chain_sears',
268 'Foot (International)' : 'ft',
269 'German legal metre' : 'german_m',
270 'Gold Coast foot' : 'gold_coast_ft',
271 'Indian yard' : 'indian_yd',
272 'Link (Benoit)': 'link_benoit',
273 'Link (Sears)': 'link_sears',
274 'Nautical Mile' : 'nm',
275 'Nautical Mile (UK)' : 'nm_uk',
276 'US survey foot' : 'survey_ft',
277 'U.S. Foot' : 'survey_ft',
278 'Yard (Indian)' : 'indian_yd',
279 'Yard (Sears)' : 'sears_yd',
280 'Point': 'pt',
281 'Pixel': 'pt',
282 }
283 LALIAS = dict([(k.lower(), v) for k, v in ALIAS.items()])
284
285 def __mul__(self, other):
286 if isinstance(other, Area):
287 return Volume(default_unit=VOLUME_PREFIX + self._default_unit,
288 **{VOLUME_PREFIX + self.STANDARD_UNIT: (self.standard * other.standard)})
289 elif isinstance(other, self.__class__):
290 return Area(default_unit=AREA_PREFIX + self._default_unit,
291 **{AREA_PREFIX + self.STANDARD_UNIT: (self.standard * other.standard)})
292 elif is_number(other):
293 return self.__class__(default_unit=self._default_unit,
294 **{self.STANDARD_UNIT: (self.standard * other)})
295 else:
296 raise TypeError('%(distance)s must be multiplied with number, %(distance)s or %(area)s' % {
297 "distance" : pretty_name(self.__class__),
298 "area" : pretty_name(Area),
299 })
300
301AREA_PREFIX = "sq_"
302
303class Area(MeasureBase):
304 STANDARD_UNIT = AREA_PREFIX + Distance.STANDARD_UNIT
305 # Getting the square units values and the alias dictionary.
306 UNITS = dict([(AREA_PREFIX + k, v ** 2) for k, v in Distance.UNITS.items()])
307 ALIAS = dict([(k, AREA_PREFIX + v) for k, v in Distance.ALIAS.items()])
308 LALIAS = dict([(k.lower(), v) for k, v in ALIAS.items()])
309
310 def __div__(self, other):
311 if isinstance(other, self.__class__):
312 return self.standard / other.standard
313 if isinstance(other, Distance):
314 return Distance(default_unit=self._default_unit[len(AREA_PREFIX):],
315 **{Distance.STANDARD_UNIT: (self.standard / other.standard)})
316 elif is_number(other):
317 return self.__class__(default_unit=self._default_unit,
318 **{self.STANDARD_UNIT: (self.standard / other)})
319 else:
320 raise TypeError('%(area)s must be divided with number, %(distance)s or %(area)s' % {
321 "distance" : pretty_name(Distance),
322 "area" : pretty_name(self.__class__),
323 })
324
325 def __mul__(self, other):
326 if isinstance(other, Distance):
327 return Volume(default_unit=VOLUME_PREFIX+self._default_unit[len(AREA_PREFIX):],
328 **{Volume.STANDARD_UNIT: (self.standard * other.standard)})
329 elif is_number(other):
330 return self.__class__(default_unit=self._default_unit,
331 **{self.STANDARD_UNIT: (self.standard * other)})
332 else:
333 raise TypeError('%(area)s must be multiplied with number or %(distance)s' % {
334 "distance" : pretty_name(Distance),
335 "area" : pretty_name(self.__class__),
336 })
337
338VOLUME_PREFIX = "vol_"
339
340class Volume(MeasureBase):
341 STANDARD_UNIT = VOLUME_PREFIX + Distance.STANDARD_UNIT
342 # Getting the cube units values and the alias dictionary.
343 UNITS = dict([(VOLUME_PREFIX + k, v ** 3) for k, v in Distance.UNITS.items()])
344 ALIAS = dict([(k, VOLUME_PREFIX + v) for k, v in Distance.ALIAS.items()])
345 LALIAS = dict([(k.lower(), v) for k, v in ALIAS.items()])
346
347 def __div__(self, other):
348 if isinstance(other, self.__class__):
349 return self.standard / other.standard
350 if isinstance(other, Area):
351 return Distance(default_unit=self._default_unit[len(VOLUME_PREFIX):],
352 **{Distance.STANDARD_UNIT: (self.standard / other.standard)})
353 if isinstance(other, Distance):
354 return Area(default_unit=AREA_PREFIX+self._default_unit[len(VOLUME_PREFIX):],
355 **{Area.STANDARD_UNIT: (self.standard / other.standard)})
356 elif is_number(other):
357 return self.__class__(default_unit=self._default_unit,
358 **{self.STANDARD_UNIT: (self.standard / other)})
359 else:
360 raise TypeError('%(volume)s must be divided with number, %(distance)s, %(area)s or %(volume)s' % {
361 "distance" : pretty_name(Distance),
362 "area" : pretty_name(Area),
363 "volume" : pretty_name(self.__class__),
364 })
365
366class Weight(MeasureBase):
367
368 STANDARD_UNIT = "gr"
369
370 UNITS = {
371 'mg': 1.0 / 1000,
372 'gr': 1.0,
373 'kg': 1.0 * 1000,
374 'q' : 1.0 * 1000 * 100,
375 'ton': 1.0 * 1000 * 1000,
376 }
377
378 # Unit aliases for `UNIT` terms encountered in Spatial Reference WKT.
379 ALIAS = {
380 'Milligram': 'mg',
381 'Gram': 'gr',
382 'Kilogram': 'kg',
383 'Quintal' : 'q',
384 'Ton' : 'ton',
385 }
386 LALIAS = dict([(k.lower(), v) for k, v in ALIAS.items()])
387
388# Shortcuts
389D = Distance
390A = Area
391V = Volume
392W = Weight
393
394
395if __name__ == '__main__':
396
397 from itertools import product
398
399 CONSTRUCTORS = {
400 float: lambda cls, val: cls(val),
401 A: lambda cls, val: cls(**{cls.STANDARD_UNIT:val}),
402 D: lambda cls, val: cls(**{cls.STANDARD_UNIT:val}),
403 V: lambda cls, val: cls(**{cls.STANDARD_UNIT:val}),
404 }
405
406 classes = [D, A, V, float]
407
408 combs = product(classes, repeat = 2)
409
410 a_val = 10
411 b_val = 5
412
413 for a_class, b_class in combs:
414
415 print "-----------%s-vs-%s-----------" % (a_class.__name__.title(), b_class.__name__.title())
416
417 a = CONSTRUCTORS[a_class](a_class, a_val)
418 b = CONSTRUCTORS[b_class](b_class, b_val)
419
420 for verbose, operator in (("+", "__add__"), ("-", "__sub__"), ("*", "__mul__"), ("/", "__div__")):
421
422 desc = "%s %s %s" % (a, verbose, b)
423
424 print desc.ljust(25), "> ",
425
426 try:
427 print getattr(a, operator)(b).__repr__()
428 except Exception, e:
429 print e
430
Back to Top