Ticket #17754: measure.py

File measure.py, 15.9 KB (added by Riccardo Di Virgilio, 6 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