#!/usr/local/bin/python # coding: iso-8859-1 # Copyright © 2008 by Amos Newcombe # # This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along with this program. If not, see . '''A library class for angles. You can specify either radians or degrees for an angle, and the constructor calculates the other and saves them both. Trig functions are methods in the angle class; the major inverse trig functions are Python functions that return angles. Both 1- and 2-argument versions are included for arctan. >>> arcsin(.866) angle(rad=1.047147, deg=59d59'49.521") >>> arctan2(1, 0) angle(rad=1.570796, deg=90d0'0.000") ''' from math import pi, degrees, radians, ldexp, modf, sin, cos, tan, asin, acos, atan, atan2 class angle: '''A class to deal with angles in their various units. The constructor expects either a radian value (rad=x), a decimal degree value (deg=x), or a degree-minute-second tuple (deg=(d, m, s), but it can have more or fewer components). If the argument name is missing it defaults to radians, unless the value is a tuple, when it defaults to degrees. >>> angle(3.14) angle(rad=3.140000, deg=179d54'31.492") >>> angle(deg=(23, 56, 4, 6)) angle(rad=0.417735, deg=23d56'4.100") >>> angle((30,)) angle(rad=0.523599, deg=30d0'0.000") You can add and subtract angles with each other; you can multiply an angle by a scalar and vice versa; you can divide to angles to get a scalar, and divide an angle by a scalar to get an angle. You can't divide a scalar by an angle. >>> angle(rad=pi) - angle(deg=90) angle(rad=1.570796, deg=90d0'0.000") >>> 1/angle(pi) Traceback (most recent call last): ... TypeError: unsupported operand type(s) for /: 'int' and 'instance' Each trigonometric function has its own method. ''' # ¤ Creation def __init__(self, rad=None, deg=None): '''Initialize an angle with a value in either radians (default) or degrees. You can specify both, but the values must be compatible (to about 10 decimal places in radians) or you will raise a 'ConflictingAngle' ValueError. See SettleConflict() for details. You can also specify neither, but such an instance will raise an 'Uninitialized' ValueError on any call to getDeg() or getRad(). >>> angle(pi/6, 30) angle(rad=0.523599, deg=29d59'60.000") >>> angle(1.571, 90) Traceback (most recent call last): ... ConflictingAngle: rad = 1.571, deg = 90.0 >>> angle() + angle() Traceback (most recent call last): ... UninitializedAngle ''' if deg != None: if isinstance(deg, tuple): deg = Reduce(deg) else : deg = float(deg) if rad != None: if deg == None and isinstance(rad, tuple): deg = Reduce(rad) rad = None else: rad = float(rad) if (deg != None) and (rad != None): deg, rad = self.SettleConflict(deg, rad) self.setDeg(deg) self.setRad(rad) def SettleConflict(self, deg, rad): xDiff = rad - (deg * pi / 180.) if abs(xDiff) < ConflictingAngle.xThreshhold: # OK, they're practically the same, rad = (rad + radians(deg)) / 2. # so use the average. deg = degrees(rad) else: # Nope, they're different, raise ConflictingAngle, (rad, deg) # so bitch and moan. return deg, rad def setDeg(self, deg): self.deg = deg def getDeg(self): if self.deg == None: if self.rad == None: raise UninitializedAngle else : self.deg = degrees(self.rad) return self.deg def setRad(self, rad): self.rad = rad def getRad(self): if self.rad == None: if self.deg == None: raise UninitializedAngle else : self.rad = radians(self.deg) return self.rad # ¤ Description def __repr__(self): return '%s(rad=%f, deg=%s)' % (self.__class__.__name__, self.getRad(), self.ToDms()) def ToDms(self): tpn = Expand(self.getDeg(), 2) # tpn of length 3 return '%dd%d\'%.3f"' % tpn # ¤ Arithmetic # add and subtract angles from each other def __add__(self, other): return angle(rad=self.getRad() + other.getRad()) def __sub__(self, other): return angle(rad=self.getRad() - other.getRad()) # reversals should never be called # def __radd__(self, other): return other + self # def __rsub__(self, other): return other - self def __iadd__(self, other): self.setRad(self.getRad() + other.getRad()) def __isub__(self, other): self.setRad(self.getRad() - other.getRad()) # multiply angles by scalars def __mul__ (self, other): return angle(rad=self.getRad() * other) def __rmul__(self, other): return angle(rad=self.getRad() * other) def __imul__(self, other): self.setRad(self.getRad() * other) # divide angles by angles to return a scalar, or by scalars to return an angle # but don't divide a scalar by an angle def __div__(self, other): if isinstance(other, angle): return angle(self.getRad() / other.getRad()) else : return angle(self.getRad() / other) def __idiv__(self, other): self.setRad(self.getRad() / other) def __truediv__(self, other): return self.__div__(other) def __itruediv__(self, other): self.__idev__(other) def __floordiv__(self, other): return angle(self.getRad() // other) def __ifloordiv__(self, other): self.setRad(self.getRad() // other) def __mod__(self, other): if isinstance(other, angle): return angle(self.getRad() % other.getRad()) else : return angle(self.getRad() % other) def __imod__(self, other): self.setRad(self.getRad() % other) # unary operations def __neg__(self): return angle(rad = - self.getRad()) def __pos__(self): return angle(rad = + self.getRad()) def __float__(self): return self.getRad() # ¤ Trigonometry def cos(self): return cos(self.getRad()) def sin(self): return sin(self.getRad()) def tan(self): return tan(self.getRad()) def sec(self): return 1. / self.cos() def csc(self): return 1. / self.sin() def cot(self): return 1. / self.tan() def arcsin(x): return angle(rad=asin(x)) def arccos(x): return angle(rad=acos(x)) def arctan(x): return angle(rad=atan(x)) def arctan2(y, x): return angle(rad=atan2(y, x)) def Expand(x, cLevel=2, nBase=60.): '''Expand a number into a base-60 (default) tuple. The first component can be any integer; subsequent ones are base 60. Parameter cLevel counts the accuracy of the representation: it defaults to 2, which gives degrees, minutes and seconds, but it can be more or less. Parameter nBase chooses the base. ''' l = [] while 0 < cLevel: xF, x = modf(x) l.append(int(x)) x = xF * nBase cLevel -= 1 l.append(x) return tuple(l) def Reduce(tpn, nBase=60.): '''Reduce a base-60 (default) tuple to a single number. This is the inverse of Expand(). ''' xSum, xFactor = 0., 1. for n in tpn: xSum += n / xFactor xFactor *= nBase return xSum class ConflictingAngle(ValueError): xThreshhold = ldexp(1., -32) # 2**-32 ~= 2.3e-10 def __init__(self, rad, deg): self.sMsg = 'rad = %s, deg = %s' % (rad, deg) def __str__(self): return self.sMsg class UninitializedAngle(ValueError): pass if __name__ == '__main__': # Unit tests import doctest print '%d tests failed out of %d' % doctest.testmod() # Command line tool from commander.commander import evaluator import sys class anglecommander(evaluator): # ¤ Metadata sVersion = '%prog 2008.09.04' # ¤ Creation def options(self): 'populate the option parser (self.optParser) with options' evaluator.options(self) self.optParser.add_option('-r', '--rad', action='append_const', const='r', dest='lchPut', help='output radians' ) self.optParser.add_option('-d', '--deg', action='append_const', const='d', dest='lchPut', help='output decimal degrees' ) self.optParser.add_option('-b', '--b60', action='append_const', const='b', dest='lchPut', help='output degrees, minutes, seconds in base 60' ) def function(self, sArg): ang = ParseAngle(sArg) for ch in self.opt.lchPut: if ch == 'r': print ang.rad if ch == 'd': print ang.deg if ch == 'b': print ang.ToDms() anglecommander(sys.argv).main()