001    package edu.nrao.sss.measure;
002    
003    import java.math.BigDecimal;
004    import java.math.RoundingMode;
005    import java.text.ParseException;
006    import java.util.Arrays;
007    import java.util.Comparator;
008    
009    import javax.xml.bind.annotation.XmlAccessType;
010    import javax.xml.bind.annotation.XmlAccessorType;
011    import javax.xml.bind.annotation.XmlElement;
012    import javax.xml.bind.annotation.XmlType;
013    
014    import static edu.nrao.sss.math.MathUtil.MC_FINAL_CALC;
015    import static edu.nrao.sss.math.MathUtil.MC_INTERM_CALCS;
016    
017    import edu.nrao.sss.math.MathUtil;
018    import edu.nrao.sss.util.StringUtil;
019    
020    //TODO work on infinity.  Make private static +/-INFINITY BigDecimal variables
021    //   Whenever a value becomes infinite, use the private statics
022    
023    /**
024     * An angle, or measure of arc.
025     * <p>
026     * Positive angles are measured in a counter-clockwise direction;
027     * negative angles in a clockwise direction.</p>
028     * <p>
029     * <b><u>Note About Accuracy</u></b><br/>
030     * This class originally used java's primitive <tt>double</tt> type
031     * for storage and calculation.  Certain transformations, though, 
032     * led to results that where not accurate enough for many purposes.
033     * Because of that, the internal references to <tt>double</tt>
034     * have been replaced with references to {@link BigDecimal}.</p>
035     * <p>
036     * <b>Version Info:</b>
037     * <table style="margin-left:2em">
038     *   <tr><td>$Revision: 1816 $</td></tr>
039     *   <tr><td>$Date: 2008-12-23 10:21:00 -0700 (Tue, 23 Dec 2008) $</td></tr>
040     *   <tr><td>$Author: dharland $</td></tr>
041     * </table></p>
042     *  
043     * @author David M. Harland
044     * @since 2006-05-23
045     */
046    @XmlAccessorType(XmlAccessType.NONE)
047    @XmlType(propOrder= {"xmlValue","units"})
048    public class Angle
049      implements Cloneable, Comparable<Angle>, java.io.Serializable
050    {
051            private static final long serialVersionUID = 1L;
052    
053      private static final BigDecimal DEFAULT_VALUE = BigDecimal.ZERO;
054      private static final ArcUnits   DEFAULT_UNITS = ArcUnits.DEGREE;
055    
056      //Used by equals, hashCode, and compareTo methods
057      //private static ArcUnits STD_UNITS = ArcUnits.ARC_SECOND;
058    
059      private static final int PRECISION = MC_FINAL_CALC.getPrecision(); 
060      
061      private BigDecimal value;
062      private ArcUnits   units;
063    
064      //===========================================================================
065      // CONSTRUCTORS
066      //===========================================================================
067     
068      /** Creates a new angle of zero degrees. */
069      public Angle()
070      {
071        set(DEFAULT_VALUE, DEFAULT_UNITS);
072      }
073      
074      /** Creates a new angle of {@code degrees} degrees. */
075      public Angle(BigDecimal degrees)
076      {
077        set(degrees, ArcUnits.DEGREE);
078      }
079      
080      /** Creates a new angle of {@code degrees} degrees. */
081      public Angle(String degrees)
082      {
083        set(degrees, ArcUnits.DEGREE);
084      }
085      
086      /** Creates a new angle with the given magnitude and units. */
087      public Angle(BigDecimal magnitude, ArcUnits units)
088      {
089        set(magnitude, units);
090      }
091      
092      /** Creates a new angle with the given magnitude and units. */
093      public Angle(String magnitude, ArcUnits units)
094      {
095        set(magnitude, units);
096      }
097    
098      /**
099       * Resets this angle so that it is equal to a angle created
100       * via the no-argument constructor.
101       */
102      public void reset()
103      {
104        set(DEFAULT_VALUE, DEFAULT_UNITS);
105      }
106    
107      //===========================================================================
108      // GETTING & SETTING THE PROPERTIES
109      //===========================================================================
110      
111      /**
112       * Returns the magnitude of this angle.
113       * @return the magnitude of this angle.
114       */
115      public BigDecimal getValue()
116      {
117        return value;
118      }
119      
120      /**
121       * Returns the units of this angle.
122       * @return the units of this angle.
123       */
124      @XmlElement
125      public ArcUnits getUnits()
126      {
127        return units;
128      }
129      
130      /**
131       * Sets the magnitude and units of this angle.
132       * 
133       * @param value the new magnitude for this angle.
134       * @param units the new units for this angle.
135       */
136      public final void set(BigDecimal value, ArcUnits units)
137      {
138        setValue(value);
139        setUnits(units);
140      }
141      
142      /**
143       * Sets the magnitude and units of this angle.
144       * 
145       * @param value the new magnitude for this angle.
146       * @param units the new units for this angle.
147       */
148      public final void set(String value, ArcUnits units)
149      {
150        setValue(value);
151        setUnits(units);
152      }
153      
154      /**
155       * Sets the magnitude of this angle to {@code newValue}.
156       * <p>
157       * Note that the <tt>units</tt> of this angle are unaffected by
158       * this method.</p>
159       * 
160       * @param newValue the new magnitude for this angle.
161        *        
162       * @throws NumberFormatException
163       *   if {@code newValue} is <i>null</i>.
164      */
165      public final void setValue(BigDecimal newValue)
166      {
167        if (newValue == null)
168          throw new NumberFormatException("newValue may not be null.");
169        
170        setAndRescale(newValue);
171      }
172      
173      private void setAndRescale(BigDecimal newValue)
174      {
175        int precision = newValue.precision();
176        
177        if (precision < PRECISION)
178        {
179          int newScale =
180            (newValue.signum() == 0) ? 1 : PRECISION - precision + newValue.scale();
181          
182          value = newValue.setScale(newScale);
183        }
184        else if (precision > PRECISION)
185        {
186          value = newValue.round(MC_FINAL_CALC);
187        }
188        else
189        {
190          value = newValue;
191        }
192      }
193    
194      /**
195       * Sets the magnitude of this angle to {@code newValue}.
196       * <p>
197       * Note that the <tt>units</tt> of this angle are unaffected by
198       * this method.</p>
199       * 
200       * @param newValue the new magnitude for this angle.
201       *        
202       * @throws NumberFormatException
203       *   if {@code newValue} is <i>null</i>.
204       */
205      public final void setValue(String newValue)
206      {
207        if (newValue == null)
208          throw new NumberFormatException("newValue may not be null.");
209        
210        BigDecimal newBD;
211        
212        newValue = newValue.trim().toLowerCase();
213        
214        if (newValue.equals("infinity") ||
215            newValue.equals("+infinity") || newValue.equals("-infinity"))
216        {
217          newBD = MathUtil.getInfiniteValue(newValue.startsWith("-") ? -1 : +1);
218        }
219        else
220        {
221          newBD = new BigDecimal(newValue);
222        }
223        
224        setAndRescale(newBD);
225      }
226    
227      /**
228       * Sets the units of this angle to {@code newUnits}.
229       * <p>
230       * Note that the <tt>value</tt> of this angle is unaffected by
231       * this method.  Contrast this with {@link #convertTo(ArcUnits)}.</p>
232       * 
233       * @param newUnits
234       *   the new units for this angle.  If {@code newUnits} is <i>null</i>
235       *   it will be treated as {@link ArcUnits#getDefault()}.
236       */
237      public final void setUnits(ArcUnits newUnits)
238      {
239        units = (newUnits == null) ? ArcUnits.getDefault() : newUnits;
240      }
241      
242      /**
243       * Sets the value and units of this angle based on {@code angleString}.
244       * See {@link #parse(String)} for the expected format of
245       * {@code angleString}.
246       * <p>
247       * If the parsing fails, this angle will be kept in its current
248       * state.</p>
249       *                                  
250       * @param angleString a string that will be converted into
251       *                    an angle.
252       * 
253       * @throws IllegalArgumentException if {@code angleString} is not in
254       *                                  the expected form.
255       */
256      public void set(String angleString)
257      {
258        if (angleString == null || angleString.equals(""))
259        {
260          this.reset();
261        }
262        else
263        {
264          ArcUnits   oldUnits = units;
265          BigDecimal oldValue = value;
266        
267          try {
268            this.parseAngle(angleString);
269          }
270          catch (Exception ex) {
271            set(oldValue, oldUnits);
272            throw new IllegalArgumentException("Could not parse " + angleString, ex);
273          }
274        }
275      }
276    
277      //===========================================================================
278      // HELPERS FOR PERSISTENCE MECHANISMS
279      //===========================================================================
280      
281      //JAXB was having trouble with the overloaded setValue methods.
282      //These methods work around that trouble.
283      @XmlElement(name="value")
284      @SuppressWarnings("unused")
285      private BigDecimal getXmlValue()  { return getValue().stripTrailingZeros(); }
286    
287      @SuppressWarnings("unused")
288      private void setXmlValue(BigDecimal v)  { setValue(v); }
289    
290      //===========================================================================
291      // DERIVED QUERIES
292      //===========================================================================
293     
294      /**
295       * Returns the number of full circles made by this angle.
296       * The value returned is never negative.
297       * <p>
298       * For example, an angle of +60.0&#x00B0; has made zero
299       * full circles, an angle of -400.0&#x00B0; one full circle,
300       * and an angle of +900.0&#x00B0; two full circles.</p>
301       * 
302       * @return the number of full circles made by this angle.
303       */
304      public int getFullCircles()
305      {
306        //Don't use one of the MC_* MathContext objects here.
307        //We used to have MC_FINAL_CALC here and had problems with
308        //radians.  We'd get something like 18.9999....999, whereas
309        //the code below will give us 19.0.
310        BigDecimal numberOfCircles =
311          value.abs().divide(units.toFullCircle(), RoundingMode.HALF_UP);
312    
313        return numberOfCircles.intValue();
314      }
315      
316      /**
317       * Returns <i>true</i> if the value of this angle is less than zero.
318       * @return <i>true</i> if the value of this angle is less than zero.
319       */
320      public boolean isNegative()
321      {
322        return value.signum() < 0;
323      }
324      
325      /**
326       * Returns <i>true</i> if the value of this angle is greater than zero.
327       * @return <i>true</i> if the value of this angle is greater than zero.
328       */
329      public boolean isPositive()
330      {
331        return value.signum() > 0;
332      }
333      
334      /**
335       * Returns <i>true</i> if this angle is traversed in a clockwise direction.
336       * The convention of this class is that negative angles are considered
337       * to be clockwise.
338       * @return <i>true</i> if this angle is traversed in a clockwise direction.
339       */
340      public boolean isClockwise()
341      {
342        return isNegative();
343      }
344    
345      /**
346       * Returns <i>true</i> if this angle is in its default state,
347       * no matter how it got there.
348       * <p>
349       * An angle is in its <i>default state</i> if both its value and
350       * its units are the same as those of an angle newly created via
351       * the {@link #Angle() no-argument constructor}.
352       * An angle whose most recent update came via the
353       * {@link #reset() reset} method is also in its default state.</p>
354       * 
355       * @return <i>true</i> if this angle is in its default state.
356       */
357      public boolean isInDefaultState()
358      {
359        return (value == DEFAULT_VALUE) && units.equals(DEFAULT_UNITS);
360      }
361    
362      /**
363       * Returns <i>true</i> if the magnitude of this angle approaches infinity.
364       * @return <i>true</i> if the magnitude of this angle approaches infinity.
365       */
366      public boolean isInfinite()
367      {
368        return MathUtil.doubleValueIsInfinite(value);
369      }
370    
371      //===========================================================================
372      // INDIRECT MANIPULATIONS
373      //===========================================================================
374     
375      /**
376       * Negates the value of this angle.
377       * <p>
378       * For example, if this angle is currently -45.0&#x00B0;,
379       * it will be +45.0&#x00B0; after this call.  The negation
380       * here is a simple sign flip.
381       * Contrast this with {@link #reverseDirection()}.</p>
382       * 
383       * @return this angle, after the negation.
384       */
385      public Angle negate()
386      {
387        if (value.signum() != 0)
388          setValue(value.negate());
389        
390        return this;
391      }
392      
393      /**
394       * Reverses the direction of this angle.  The reversal results in
395       * a sign change, unless this angle has a value of zero.
396       * <p>
397       * For example, if this angle is currently -45.0&#x00B0;,
398       * it will be +315.0&#x00B0; after this call.
399       * Contrast this with {@link #negate()}.</p>
400       * 
401       * @return this angle, after the reversal of direction.
402       */
403      public Angle reverseDirection()
404      {
405        BigDecimal fullCircle = units.toFullCircle();
406        BigDecimal rotations  = new BigDecimal(getFullCircles());
407        
408        if (rotations.signum() > 0) //a non-normalized angle
409        {
410          Angle temp = this.clone();
411          boolean originallyNegative = temp.isNegative();
412          temp.normalize();
413          temp.reverseDirection();
414          BigDecimal angle = rotations.multiply(fullCircle);
415          if (originallyNegative)
416            setValue(temp.value.add(angle));
417          else //Can't be zero if rotations > 0, so this is positive
418            setValue(temp.value.subtract(angle));
419        }
420        else //already normalized
421        {
422          if (value.equals(fullCircle))
423          {
424            setValue(fullCircle.negate());
425          }
426          else if (value.signum() != 0)
427          {
428            boolean    negate   = (value.signum() > 0);
429            BigDecimal absValue = fullCircle.subtract(value.abs());
430            setValue(negate ? absValue.negate() : absValue);
431          }
432        }
433        
434        return this;
435      }
436      
437      /**
438       * Normalizes this angle so that its absolute value is less
439       * than that of a full circle.  The method does <i>not</i>
440       * change the direction, or sign, of the angle.
441       * <p>
442       * For example, if this angle is currently 765.0&#x00B0;,
443       * it will be 45.0&#x00B0; after this call.
444       * Likewise, if this angle is currently -765.0&#x00B0;,
445       * it will be -45.0&#x00B0; after this call.</p>
446       * 
447       * @return this angle, after the normalization.
448       */
449      public Angle normalize()
450      {
451        int fullCircles = getFullCircles();
452        
453        if (fullCircles > 0)
454        {
455          BigDecimal circleSize = units.toFullCircle();
456          BigDecimal excess     = circleSize.multiply(new BigDecimal(fullCircles));
457          
458          if (value.signum() < 0)
459            excess = excess.negate();
460          
461          setValue(value.subtract(excess));
462        }
463        
464        return this;
465      }
466    
467      /**
468       * Converts this angle to a positive normalized angle.
469       * The conversion is done first by normalizing this angle
470       * (see {@link #normalize()})
471       * and then by creating a positive angle, <i>not</i> by a
472       * simple sign flip, but instead by reversing direction
473       * to complete the circle.
474       * <p>
475       * For example, if this angle is currently -765.0&#x00B0;,
476       * it is first normalized to -45.0&#x00B0;.  The result
477       * is then converted to a positive number by traveling the
478       * circle in the opposite direction, for a final result
479       * of +315&#x00B0;.</p>
480       * 
481       * @return this angle, after the conversion.
482       */
483      public Angle convertToPositiveNormal()
484      {
485        this.normalize();
486        
487        if (value.signum() < 0)
488          setValue(value.add(units.toFullCircle()));
489        
490        return this;
491      }
492      
493      /**
494       * Converts this angle to a negative normalized angle.
495       * The conversion is done first by normalizing this angle
496       * (see {@link #normalize()})
497       * and then by creating a negative angle <i>not</i> by a
498       * simple sign flip, but instead by reversing direction
499       * to complete the circle.
500       * <p>
501       * For example, if this angle is currently +765.0&#x00B0;,
502       * it is first normalized to +45.0&#x00B0;.  The result
503       * is then converted to a negative number by traveling the
504       * circle in the opposite direction, for a final result
505       * of -315&#x00B0;.</p>
506       * 
507       * @return this angle, after the conversion.
508       */
509      public Angle convertToNegativeNormal()
510      {
511        this.normalize();
512        
513        if (value.signum() > 0)
514          setValue(value.subtract(units.toFullCircle()));
515        
516        return this;
517      }
518      
519      /**
520       * Converts this angle to the normalized value that has the
521       * smaller absolute value.  This may involve a reversal of
522       * direction.
523       * <p>
524       * For example, if this angle is currently +920.0&#x00B0;,
525       * it is first normalized to +200.0&#x00B0;.  By reversing
526       * direction, we wind up with an angle of -160.0&#x00B0;,
527       * which has a smaller absolute value than +200.0&#x00B0;,
528       * so that is our end result.  For the special situations
529       * where the normalized value is either +180.0&#x00B0;
530       * or -180.0&#x00B0;, the original direction is preserved.</p>
531       * 
532       * @return this angle, after the conversion.
533       */
534      public Angle convertToMinAbsValueNormal()
535      {
536        this.normalize();
537        
538        if (value.abs().compareTo(units.toHalfCircle()) > 0)
539          this.reverseDirection();
540        
541        return this;
542      }
543    
544      //===========================================================================
545      // CONVERSION TO, AND EXPRESSION IN, OTHER UNITS
546      //===========================================================================
547    
548      /**
549       * Converts this angle to the new units.
550       * <p>
551       * After this method is complete this angle will have units of
552       * {@code units} and its <tt>value</tt> will have been converted
553       * accordingly.</p>
554       *  
555       * @param newUnits the new units for this angle.
556       *                 If {@code newUnits} is <i>null</i> an
557       *                 {@code IllegalArgumentException} will be thrown.
558       * 
559       * @return this angle.  The reason for this return type is to allow
560       *         code of this nature:
561       *         {@code BigDecimal radians = 
562       *         myAngle.convertTo(ArcUnits.RADIAN).getValue();}
563       */
564      public Angle convertTo(ArcUnits newUnits)
565      {
566        if (newUnits == null)
567          throw new IllegalArgumentException("May not convert to NULL units.");
568      
569        if (!newUnits.equals(units))
570        {
571          set(toUnits(newUnits), newUnits);
572        }
573        
574        return this;
575      }
576      
577      /**
578       * Returns the magnitude of this angle in {@code otherUnits}.
579       * <p>
580       * Note that this method does not alter the state of this angle.
581       * Contrast this with {@link #convertTo(ArcUnits)}.</p>
582       * 
583       * @param otherUnits the units in which to express this angle's magnitude.
584       * 
585       * @return this angle's value converted to {@code otherUnits}.
586       */
587      public BigDecimal toUnits(ArcUnits otherUnits)
588      {
589        BigDecimal answer = value;
590        
591        if (otherUnits == null)
592          throw new IllegalArgumentException("May not convert to NULL units.");
593        
594        //No conversion for zero, infinite, or if no change of units
595        if (!otherUnits.equals(units) && 
596            value.compareTo(BigDecimal.ZERO) != 0.0 && !isInfinite())
597        {
598          answer = units.convertTo(otherUnits, value);
599        }
600        
601        return answer;
602      }
603      
604      /**
605       * Returns a representation of this angle in degrees, minutes, and seconds.
606       * 
607       * @return an array of size three in this order:
608       *         <ol start="0">
609       *           <li>An integral number of degrees.</li>
610       *           <li>An integral number of arc minutes.</li>
611       *           <li>A real number of arc seconds.</li>
612       *         </ol>
613       */
614      public Number[] toDms()
615      {
616        return units.convertToDms(value);
617      }
618      
619      /**
620       * Returns a representation of this angle in hours, minutes, and seconds.
621       * 
622       * @return an array of size three in this order:
623       *         <ol start="0">
624       *           <li>An integral number of hours.</li>
625       *           <li>An integral number of minutes.</li>
626       *           <li>A real number of seconds.</li>
627       *         </ol>
628       */
629      public Number[] toHms()
630      {
631        return units.convertToHms(value);
632      }
633    
634      //===========================================================================
635      // ARITHMETIC
636      //===========================================================================
637     
638      /**
639       * Adds the {@code other} angle to this one.
640       * @param other an angle to be added to this one.
641       * @return this angle, after the addition.
642       */
643      public Angle add(Angle other)
644      {
645        //TODO when we work on INFINITY, we need to consider other being infinite
646        //     and the result being inf, too
647        if (!isInfinite())
648          setAndRescale(value.add(other.toUnits(this.units)));
649        
650        return this;
651      }
652      
653      /**
654       * Subtracts the {@code other} angle from this one.
655       * @param other an angle to be subtracted from this one.
656       * @return this angle, after the subtraction.
657       */
658      public Angle subtract(Angle other)
659      {
660        //TODO when we work on INFINITY, we need to consider other being infinite
661        //     and the result being inf, too
662        if (!isInfinite())
663          setAndRescale(value.subtract(other.toUnits(this.units)));
664        
665        return this;
666      }
667      
668      private Angle mathHelper = null;
669      
670      /**
671       * Adds the {@code amount}, of the same units as this angle, to this angle.
672       * @param amount the amount to add to this angle.
673       * @return this angle, after the addition.
674       */
675      public Angle add(BigDecimal amount)
676      {
677        if (mathHelper == null)
678          mathHelper = new Angle();
679        mathHelper.set(amount, this.units);
680        return add(mathHelper);
681      }
682      
683      /**
684       * Adds the {@code amount}, of the same units as this angle, to this angle.
685       * @param amount the amount to add to this angle.
686       * @return this angle, after the addition.
687       */
688      public Angle add(String amount)
689      {
690        if (mathHelper == null)
691          mathHelper = new Angle();
692        mathHelper.set(amount, this.units);
693        return add(mathHelper);
694      }
695    
696      /**
697       * Subtracts the {@code amount}, of the same units as this angle, from this
698       * angle.
699       * @param amount the amount to subtract from this angle.
700       * @return this angle, after the subtraction.
701       */
702      public Angle subtract(BigDecimal amount)
703      {
704        if (mathHelper == null)
705          mathHelper = new Angle();
706        mathHelper.set(amount, this.units);
707        return subtract(mathHelper);
708      }
709    
710      /**
711       * Subtracts the {@code amount}, of the same units as this angle, from this
712       * angle.
713       * @param amount the amount to subtract from this angle.
714       * @return this angle, after the subtraction.
715       */
716      public Angle subtract(String amount)
717      {
718        if (mathHelper == null)
719          mathHelper = new Angle();
720        mathHelper.set(amount, this.units);
721        return subtract(mathHelper);
722      }
723      
724      /**
725       * Multiplies this angle by {@code multiplier}.
726       * 
727       * @param multiplier the number by which this angle should be multiplied.
728       *        
729       * @return this angle, after the multiplication.
730       */
731      public Angle multiplyBy(BigDecimal multiplier)
732      {
733        if (!isInfinite())
734          setAndRescale(value.multiply(multiplier));
735    
736        return this;
737      }
738      
739      /**
740       * Multiplies this angle by {@code multiplier}.
741       * 
742       * @param multiplier the number by which this angle should be multiplied.
743       *        
744       * @return this angle, after the multiplication.
745       */
746      public Angle multiplyBy(String multiplier)
747      {
748        return multiplyBy(new BigDecimal(multiplier));
749      }
750    
751      /**
752       * Divides this angle by {@code divisor}.
753       * 
754       * @param divisor the number by which this angle should be divided.
755       *        
756       * @return this angle, after the division.
757       */
758      public Angle divideBy(BigDecimal divisor)
759      {
760        setAndRescale(value.divide(divisor, MC_INTERM_CALCS));
761        
762        return this;
763      }
764    
765      /**
766       * Divides this angle by {@code divisor}.
767       * 
768       * @param divisor the number by which this angle should be divided.
769       *        
770       * @return this angle, after the division.
771       */
772      public Angle divideBy(String divisor)
773      {
774        return divideBy(new BigDecimal(divisor));
775      }
776    
777      //===========================================================================
778      // TRIGONOMETRY
779      //===========================================================================
780    
781      /**
782       * Returns the cosine of this angle.
783       * @return the cosine of this angle.
784       */
785      public double cosine()
786      {
787        return Math.cos(toUnits(ArcUnits.RADIAN).doubleValue());
788      }
789    
790      /**
791       * Returns the sine of this angle.
792       * @return the sine of this angle.
793       */
794      public double sine()
795      {
796        return Math.sin(toUnits(ArcUnits.RADIAN).doubleValue());
797      }
798    
799      /**
800       * Returns the tangent of this angle.
801       * @return the tangent of this angle.
802       */
803      public double tangent()
804      {
805        return Math.tan(toUnits(ArcUnits.RADIAN).doubleValue());
806      }
807      
808      /**
809       * Returns the secant of this angle.
810       * @return the secant of this angle.
811       */
812      public double secant()
813      {
814        return 1.0 / cosine();
815      }
816      
817      /**
818       * Returns the cosecant of this angle.
819       * @return the cosecant of this angle.
820       */
821      public double cosecant()
822      {
823        return 1.0 / sine();
824      }
825      
826      /**
827       * Returns the cotangent of this angle.
828       * @return the cotangent of this angle.
829       */
830      public double cotangent()
831      {
832        return 1.0 / tangent();
833      }
834      
835      /**
836       * Returns a new angle whose cosine is <tt>cosine</tt>.
837       * @param cosine
838       *   the value whose arc cosine is to be returned.
839       * @return a new angle whose cosine is <tt>cosine</tt>.
840       */
841      public static Angle arcCosine(double cosine)
842      {
843        return new Angle(BigDecimal.valueOf(Math.acos(cosine)),
844                         ArcUnits.RADIAN);
845      }
846    
847      /**
848       * Returns a new angle whose sine is <tt>sine</tt>.
849       * @param sine
850       *   the value whose arc sine is to be returned.
851       * @return a new angle whose sine is <tt>sine</tt>.
852       */
853      public static Angle arcSine(double sine)
854      {
855        return new Angle(BigDecimal.valueOf(Math.asin(sine)),
856                         ArcUnits.RADIAN);
857      }
858    
859      /**
860       * Returns a new angle whose tangent is <tt>tangent</tt>.
861       * @param tangent
862       *   the value whose arc tangent is to be returned.
863       * @return a new angle whose tangent is <tt>tangent</tt>.
864       */
865      public static Angle arcTangent(double tangent)
866      {
867        return new Angle(BigDecimal.valueOf(Math.atan(tangent)),
868                         ArcUnits.RADIAN);
869      }
870    
871      //===========================================================================
872      // PARSING
873      //===========================================================================
874    
875      /**
876       * Returns a new angle based on {@code angleString}.
877       * <p>
878       * <b><u>Valid Formats</u></b><br/>
879       * Let I be the text representation of an integer.<br/>
880       * Let R be the text representation of a real number.<br/>
881       * Let w represent zero or more whitespace characters.<br/>
882       * Let S be a valid {@link ArcUnits units} symbol.<br/>
883       * <br/>
884       * <i>Format One</i>: <tt>wRw</tt>.  The given number will be defined to be
885       * in units of {@link ArcUnits#DEGREE degrees}. Examples:
886       * <ul>
887       *   <li>12.345</li>
888       * </ul>
889       * <i>Format Two</i>: <tt>wRwSw</tt>
890       * <ul>
891       *   <li>12.345d</li>
892       *   <li>&nbsp;&nbsp;12.345&nbsp;&nbsp;d</li>
893       *   <li>1.234 rad</li>
894       * </ul>
895       * <i>Format Three</i>: <tt>wIwSwIwSwRwSw</tt>.  The first S must be the symbol
896       * for either {@link ArcUnits#HOUR hours} or {@link ArcUnits#DEGREE degrees}.
897       * The second S must be the symbol for either
898       * {@link ArcUnits#MINUTE minutes} or {@link ArcUnits#ARC_MINUTE arc minutes}.
899       * The final S must be the symbol for either
900       * {@link ArcUnits#SECOND seconds} or {@link ArcUnits#ARC_SECOND arc seconds}.
901       * <ul>
902       *   <li>12h34m56.789s</li>
903       *   <li>12d 34' 56.789"</li>
904       * </ul>
905       * This format has been updated so that only two of the three number/units
906       * pairs are required.  For example, each of the following is valid:
907       * <ul>
908       *   <li>12h 34m</li>
909       *   <li>12h 56.789s</li>
910       *   <li>34m 56.789s</li>
911       * </ul>
912       * </p><p>
913       * <b><u>Special Cases</u></b><br/>
914       * An {@code angleString} of <i>null</i> or <tt>""</tt> (the empty
915       * string) will <i>not</i> result in an {@code IllegalArgumentException},
916       * but will instead return an angle of zero degrees.</p>
917       * 
918       * @param angleString a string that will be converted into
919       *                    an angle.
920       * 
921       * @throws IllegalArgumentException if {@code angleString} is not in
922       *                                  the expected form.
923       */
924      public static Angle parse(String angleString)
925      {
926        Angle newAngle = new Angle();
927        
928        if ((angleString != null) && !angleString.equals(""))
929        {
930          try {
931            newAngle.parseAngle(angleString);
932          }
933          catch (Exception ex) {
934            throw new IllegalArgumentException("Could not parse '" + angleString + "'.", ex);
935          }
936        }
937        return newAngle;
938      }
939      
940      /**
941       * If parsing was successful, this angle's units & value will have been
942       * valued.  Otherwise an exception is thrown.
943       */
944      private void parseAngle(String angleString)
945      {
946        //Eliminate whitespace
947        angleString = angleString.replaceAll("\\s", "");
948    
949        //If successful parsing either hr-min-sec or degrees-min-sec, return
950        if (parseXms(angleString))
951          return;
952        
953        //Now assume we have a number followed (optionally) by a symbol
954        units = null;
955        
956        int unitsPos = -1;
957    
958        //Sort units by length of symbol, longer symbols before shorter.
959        //This helps w/ discovering which unit is contained in angleString.
960        ArcUnits[] sortedUnits = ArcUnits.values();
961        Arrays.sort(sortedUnits,
962                    new Comparator<ArcUnits>() {
963                      public int compare(ArcUnits a, ArcUnits b) {
964                        return b.getSymbol().length() - a.getSymbol().length();
965                      }
966                    });
967    
968        //Figure out what kind of units we have
969        for (ArcUnits u : sortedUnits) 
970        {
971          if (angleString.endsWith(u.getSymbol()))
972          {
973            units = u;
974            unitsPos = angleString.lastIndexOf(u.getSymbol());
975            break;
976          }
977        }
978        
979        //If units, simply parse the number
980        if (units != null)
981        {
982          String numberString = angleString.substring(0, unitsPos);
983          setValue(numberString);
984        }
985        //Otherwise, see if we have just a number.  Declare this to be degrees.
986        else
987        {
988          set(angleString, ArcUnits.DEGREE);
989        }
990      }
991      
992      /**
993       * Returns true if parsing was successful, false otherwise.
994       * If successful, this angle's units & value will have been set.
995       */
996      private boolean parseXms(String xmsString)
997      {
998        //Consider as HMS or DMS if it has two or more of the unit symbols.
999        //If it has only one, eg 12.345 h, let normally parsing occur.
1000        int hmsCount = 0;
1001        
1002        if (xmsString.contains(ArcUnits.HOUR.getSymbol()))    hmsCount++;
1003        if (xmsString.contains(ArcUnits.MINUTE.getSymbol()))  hmsCount++;
1004        if (xmsString.contains(ArcUnits.SECOND.getSymbol()))  hmsCount++;
1005        
1006        int dmsCount = 0;
1007        
1008        if (xmsString.contains(ArcUnits.DEGREE.getSymbol()))      dmsCount++;
1009        if (xmsString.contains(ArcUnits.ARC_MINUTE.getSymbol()))  dmsCount++;
1010        if (xmsString.contains(ArcUnits.ARC_SECOND.getSymbol()))  dmsCount++;
1011        
1012        boolean isHms = hmsCount > 1;
1013        boolean isDms = dmsCount > 1;
1014        
1015        //Quick exit if don't have HMS or DMS, or if have both
1016        if (isHms == isDms) 
1017          return false;
1018        
1019        //It looks like we have either HMS or DMS.
1020        //Aug 2007: We now allow any 2 or 3 parts for HMS and DMS.
1021        //Eg, "34m 56s" is now acceptable, as are "12h 56s" and "12h 34m".
1022        try
1023        {
1024          //The Integer class, for some odd reason, handles "-" signs, but not "+"
1025          xmsString = xmsString.replaceAll("\\+", "");
1026          
1027          if (isHms)
1028          {
1029            Number[] parts = parseXms(xmsString, ArcUnits.HOUR.getSymbol(),
1030                                                 ArcUnits.MINUTE.getSymbol(),
1031                                                 ArcUnits.SECOND.getSymbol());
1032            units = ArcUnits.SECOND;
1033            setValue(ArcUnits.convertHmsTo(units, parts[0].intValue(),
1034                                                  parts[1].intValue(),
1035                                                  new BigDecimal(parts[2].toString())));
1036          }
1037          else //isDms
1038          {
1039            Number[] parts = parseXms(xmsString, ArcUnits.DEGREE.getSymbol(),
1040                                                 ArcUnits.ARC_MINUTE.getSymbol(),
1041                                                 ArcUnits.ARC_SECOND.getSymbol());
1042            units = ArcUnits.ARC_SECOND;
1043            setValue(ArcUnits.convertDmsTo(units, parts[0].intValue(),
1044                                                  parts[1].intValue(),
1045                                                  new BigDecimal(parts[2].toString())));
1046          }
1047        }
1048        //Signal any kind of failure with a return value of false;
1049        catch (Exception ex)
1050        {
1051          return false;
1052        }
1053        
1054        return true;  //success
1055      }
1056      
1057      /**
1058       * Expects xmsString to have form:
1059       *  INTEGER symbol1 INTEGER minutesSymbol DOUBLE secondsSymbol
1060       *  
1061       * As of Aug 2007, we can now have any two or three of the above
1062       * number/symbol pairs.
1063       * 
1064       * Throws the following exceptions if xmsString is non-conformant:
1065       *   + IndexOutOfBoundsException
1066       *   + NumberFormatException
1067       */
1068      private Number[] parseXms(String xmsString, String symbol1, String minutesSymbol,
1069                                                                  String secondsSymbol)
1070        throws ParseException
1071      {
1072        Number[] answer = new Number[3];
1073        
1074        int        DorH = 0;   //Degrees or Hours
1075        int        M    = 0;   //Arc-minutes or Minutes
1076        BigDecimal S    = BigDecimal.ZERO; //Arc-seconds or Seconds
1077        
1078        String  numberStr         = null;
1079        int     finalSymbolIndex  = -1;
1080        boolean foundPreviousPart = false;
1081        boolean negative          = xmsString.contains("-");
1082    
1083        //Get the degrees or HOURS PART, if we have that part
1084        int subStrStart = 0;
1085        int subStrEnd   = xmsString.indexOf(symbol1);
1086        
1087        if (subStrEnd < 0)
1088        {
1089          foundPreviousPart = false;
1090        }
1091        else //we found either degrees or hours symbol
1092        {
1093          numberStr         = xmsString.substring(subStrStart, subStrEnd);
1094          DorH              = Math.abs(Integer.parseInt(numberStr));
1095          finalSymbolIndex  = subStrEnd;
1096          foundPreviousPart = true;
1097        }
1098    
1099        //Get the MINUTES PART, if we have that part
1100        if (foundPreviousPart)
1101          subStrStart = subStrEnd + symbol1.length();
1102          //else leave at current value
1103        
1104        subStrEnd = xmsString.indexOf(minutesSymbol);
1105        
1106        if (subStrEnd < 0)
1107        {
1108          foundPreviousPart = false;
1109        }
1110        else //we found minutes symbol
1111        {
1112          numberStr         = xmsString.substring(subStrStart, subStrEnd);
1113          M                 = Math.abs(Integer.parseInt(numberStr));
1114          finalSymbolIndex  = subStrEnd;
1115          foundPreviousPart = true;
1116        }
1117    
1118        //Get the SECONDS PART, if we have that part
1119        if (foundPreviousPart)
1120          subStrStart = subStrEnd + minutesSymbol.length();
1121          //else leave at current value
1122        
1123        subStrEnd   = xmsString.indexOf(secondsSymbol);
1124        
1125        if (subStrEnd >= 0) //we found seconds symbol
1126        {
1127          numberStr        = xmsString.substring(subStrStart, subStrEnd);
1128          finalSymbolIndex = subStrEnd;
1129          S                = new BigDecimal(numberStr).abs();
1130        }
1131        
1132        //Ensure that we have no more characters after the position of the last
1133        //units symbol.  (Code assumes trailing whitespace was already stripped.)
1134        if (finalSymbolIndex != (xmsString.length()-1))
1135          throw new ParseException("Found extra characters at end of text '" +
1136                                   xmsString + "'.", subStrEnd);
1137        
1138        //Negate the largest non-zero field
1139        if (negative)
1140        {
1141          if (DorH != 0)
1142          {
1143            DorH = -DorH;
1144          }
1145          else if (M != 0)
1146          {
1147            M = -M;
1148          }
1149          else //M == 0
1150          {
1151            S = S.negate();
1152          }
1153        }
1154        
1155        answer[0] = DorH;
1156        answer[1] = M;
1157        answer[2] = S;
1158    
1159        return answer;
1160      }
1161    
1162      //===========================================================================
1163      // UTILITY METHODS
1164      //===========================================================================
1165      
1166      /** Returns a text representation of this angle in its native units. */
1167      public String toString()
1168      {
1169        return StringUtil.getInstance()
1170                         .formatNoScientificNotation(getValue()) +
1171                                                     getUnits().getSymbol();
1172      }
1173      
1174      /** Returns a text representation of this angle in its native units. */
1175      public String toString(int minFracDigits, int maxFracDigits)
1176      {
1177        return StringUtil.getInstance().formatNoScientificNotation(
1178                 getValue(), minFracDigits, maxFracDigits) +
1179                 getUnits().getSymbol();
1180      }
1181      
1182      /**
1183       * Returns a text representation of this angle in its native units, using
1184       * an HTML-friendly symbol.
1185       */
1186      public String toStringHtml(int minFracDigits, int maxFracDigits)
1187      {
1188        return StringUtil.getInstance().formatNoScientificNotation(
1189                 getValue(), minFracDigits, maxFracDigits) +
1190                 getUnits().getHtmlSymbol();
1191      }
1192    
1193      /**
1194       * Returns a text representation of this angle in
1195       * hours, minutes, and seconds.
1196       */
1197      public String toStringHms()
1198      {
1199        return toStringHms(0, -1);
1200      }
1201      
1202      /**
1203       * Returns a text representation of this angle in
1204       * hours, minutes, and seconds.
1205       * 
1206       * @param minFracDigits
1207       *   the minimum number of places after the decimal point
1208       *   for the seconds field.
1209       *                      
1210       * @param maxFracDigits
1211       *   the maximum number of places after the decimal point
1212       *   for the seconds field.  If this value is less than zero,
1213       *   no rounding or truncating will be performed.
1214       */
1215      public String toStringHms(int minFracDigits, int maxFracDigits)
1216      {
1217        Number[] hms = toHms();
1218        
1219        int        hours   = hms[0].intValue();
1220        int        minutes = hms[1].intValue();
1221        BigDecimal seconds = new BigDecimal(hms[2].toString());
1222        
1223        //Deal with the sign.  Only one of {d, m, s} can be negative,
1224        //but it could be any one of them.  (If m is < 0, d must be 0;
1225        //if s < 0.0, both d & m must be 0.)
1226        String sign = "";
1227        
1228        if (hours < 0)
1229        {
1230          sign  = "-";
1231          hours = -hours;
1232        }
1233        else if (hours == 0)
1234        {
1235          if (minutes < 0)
1236          {
1237            sign    = "-";
1238            minutes = -minutes;
1239          }
1240          else if (minutes == 0)
1241          {
1242            if (seconds.signum() < 0)
1243            {
1244              sign    = "-";
1245              seconds = seconds.negate();
1246            }
1247          }
1248        }
1249        
1250        //Rollover logic
1251        //We have to worry about things like 59.999 being rounded to 60.0
1252        if (maxFracDigits >= 0)
1253        {
1254          double s = seconds.setScale(maxFracDigits, RoundingMode.HALF_UP).doubleValue();
1255          
1256          if (s == 60.0 || s == -60.0)
1257          {
1258            seconds = BigDecimal.ZERO;
1259            minutes++;
1260          }
1261          
1262          if (minutes == 60 || minutes == -60)
1263          {
1264            minutes = 0;
1265            hours++;
1266          }
1267        }
1268        
1269        //Formatting logic
1270        StringBuilder buff = new StringBuilder();
1271    
1272        buff.append(sign);
1273        
1274        if (hours < 10)
1275          buff.append('0');
1276        
1277        buff.append(hours).append(ArcUnits.HOUR.getSymbol()).append(' ');
1278        
1279        if (minutes < 10)
1280          buff.append('0');
1281        
1282        buff.append(minutes).append(ArcUnits.MINUTE.getSymbol()).append(' ');
1283        
1284        if (seconds.compareTo(BigDecimal.TEN) < 0)
1285          buff.append('0');
1286    
1287        if (maxFracDigits >= 0)
1288          buff.append(StringUtil.getInstance().formatNoScientificNotation(
1289                      seconds, minFracDigits, maxFracDigits));
1290        else //no rounding or truncation
1291          buff.append(StringUtil.getInstance().formatNoScientificNotation(seconds));
1292        
1293        buff.append(ArcUnits.SECOND.getSymbol());
1294        
1295        return buff.toString();
1296      }
1297     
1298      /**
1299       * Returns a text representation of this angle in
1300       * degrees, arc-minutes, and arc-seconds.
1301       */
1302      public String toStringDms()
1303      {
1304        return toStringDms(0, -1);
1305      }
1306      
1307      /**
1308       * Returns a text representation of this angle in
1309       * degrees, arc-minutes, and arc-seconds.
1310       * 
1311       * @param minFracDigits
1312       *   the minimum number of places after the decimal point
1313       *   for the seconds field.
1314       *                      
1315       * @param maxFracDigits
1316       *   the maximum number of places after the decimal point
1317       *   for the seconds field.  If this value is less than zero,
1318       *   no rounding or truncating will be performed.
1319       */
1320      public String toStringDms(int minFracDigits, int maxFracDigits)
1321      {
1322        return toStringDms(minFracDigits, maxFracDigits, false);
1323      }
1324      
1325      /**
1326       * Returns a text representation of this angle in
1327       * degrees, arc-minutes, and arc-seconds, with HTML-friendly symbols.
1328       * 
1329       * @param minFracDigits
1330       *   the minimum number of places after the decimal point
1331       *   for the seconds field.
1332       *                      
1333       * @param maxFracDigits
1334       *   the maximum number of places after the decimal point
1335       *   for the seconds field.  If this value is less than zero,
1336       *   no rounding or truncating will be performed.
1337       */
1338      public String toStringDmsHtml(int minFracDigits, int maxFracDigits)
1339      {
1340        return toStringDms(minFracDigits, maxFracDigits, true);
1341      }
1342      
1343      private String toStringDms(int minFracDigits,
1344                                 int maxFracDigits, boolean htmlFriendly)
1345      {
1346        Number[] dms = toDms();
1347        
1348        int        degrees = dms[0].intValue();
1349        int        minutes = dms[1].intValue();
1350        BigDecimal seconds = new BigDecimal(dms[2].toString());
1351    
1352        //Deal with the sign.  Only one of {d, m, s} can be negative,
1353        //but it could be any one of them.  (If m is < 0, d must be 0;
1354        //if s < 0.0, both d & m must be 0.)
1355        char sign = '+';
1356        
1357        if (degrees < 0)
1358        {
1359          sign    = '-';
1360          degrees = -degrees;
1361        }
1362        else if (degrees == 0)
1363        {
1364          if (minutes < 0)
1365          {
1366            sign    = '-';
1367            minutes = -minutes;
1368          }
1369          else if (minutes == 0)
1370          {
1371            if (seconds.signum() < 0)
1372            {
1373              sign    = '-';
1374              seconds = seconds.negate();
1375            }
1376          }
1377        }
1378        
1379        //Rollover logic
1380        //We have to worry about things like 59.999 being rounded to 60.0
1381        if (maxFracDigits >= 0)
1382        {
1383          double s = seconds.setScale(maxFracDigits, RoundingMode.HALF_UP).doubleValue();
1384          
1385          if (s == 60.0 || s == -60.0)
1386          {
1387            seconds = BigDecimal.ZERO;
1388            minutes++;
1389          }
1390          
1391          if (minutes == 60 || minutes == -60)
1392          {
1393            minutes = 0;
1394            degrees++;
1395          }
1396        }
1397    
1398        //Formatting logic
1399        StringBuilder buff = new StringBuilder();
1400        
1401        buff.append(sign);
1402        
1403        if (degrees < 10)
1404          buff.append('0');
1405        
1406        String symbol = htmlFriendly ? ArcUnits.DEGREE.getHtmlSymbol()
1407                                     : ArcUnits.DEGREE.getSymbol();
1408    
1409        buff.append(degrees).append(symbol).append(' ');
1410        
1411        if (minutes < 10)
1412          buff.append('0');
1413        
1414        symbol = htmlFriendly ? ArcUnits.ARC_MINUTE.getHtmlSymbol()
1415                              : ArcUnits.ARC_MINUTE.getSymbol();
1416    
1417        buff.append(minutes).append(symbol).append(' ');
1418        
1419        if (seconds.compareTo(BigDecimal.TEN) < 0)
1420          buff.append('0');
1421    
1422        if (maxFracDigits >= 0)
1423          buff.append(StringUtil.getInstance().formatNoScientificNotation(
1424                      seconds, minFracDigits, maxFracDigits));
1425        else //no rounding or truncation
1426          buff.append(StringUtil.getInstance().formatNoScientificNotation(seconds));
1427        
1428        symbol = htmlFriendly ? ArcUnits.ARC_SECOND.getHtmlSymbol()
1429                              : ArcUnits.ARC_SECOND.getSymbol();
1430    
1431        buff.append(symbol);
1432        
1433        return buff.toString();
1434      }
1435    
1436      /** Returns an angle that is equal to this one. */
1437      @Override
1438      public Angle clone()
1439      {
1440        //Since this class has only primitive (& immutable) attributes,
1441        //the clone in Object is all we need.
1442        try
1443        {
1444          return (Angle)super.clone();
1445        }
1446        catch (CloneNotSupportedException ex)
1447        {
1448          //We'll never get here, but just in case...
1449          throw new RuntimeException(ex);
1450        }
1451      }
1452    
1453      /** Returns <i>true</i> if {@code o} is equal to this angle. */
1454      @Override
1455      public boolean equals(Object o)
1456      {
1457        //Quick exit if o is this
1458        if (o == this)
1459          return true;
1460        
1461        //Quick exit if o is null
1462        if (o == null)
1463          return false;
1464        
1465        //Quick exit if classes are different
1466        if (!o.getClass().equals(this.getClass()))
1467          return false;
1468        
1469        Angle other = (Angle)o;
1470    
1471        //Treat two infinite values of same sign as equal,
1472        //regardless of actual BigDecimal values
1473        if (isInfinite() && other.isInfinite())
1474          return value.signum() == other.value.signum();
1475    
1476        //Ignore stored units; equality is based purely on magnitude in std units
1477        return compareTo(other) == 0;
1478      }
1479      
1480      /** Returns a hash code value for this angle. */
1481      @Override
1482      public int hashCode()
1483      {
1484        if (isInfinite())
1485          return value.signum() > 0 ? "+infinity".hashCode() : "-infinity".hashCode();
1486          
1487        String crude = value.toPlainString() + units.getSymbol();
1488        return crude.hashCode();
1489      }
1490      
1491      /** Compares this angle with the {@code otherAngle} for order. */
1492      public int compareTo(Angle otherAngle)
1493      {
1494        //Treat two infinite values of same sign as equal,
1495        //regardless of actual BigDecimal values
1496        if (isInfinite() && otherAngle.isInfinite())
1497          return value.signum() - otherAngle.value.signum();
1498        
1499        //Avoid doing two unit conversions
1500        return getValue().compareTo(otherAngle.toUnits(units));
1501      }
1502      
1503      /*
1504      //This is here for quick testing.
1505      public static void main(String[] args)
1506      {
1507        double[] angles = {0.0, 60.0, 120.0, 180.0, 300.0, 360.0, 720.0, 765.0, 900.0,
1508                               -60.0,-120.0,-180.0,-300.0,-360.0,-720.0,-765.0,-900.0};
1509        
1510        Angle angle = new Angle();
1511        for (double a : angles)
1512        {
1513          angle.setValue(a);
1514          System.out.println("ANGLE = " + angle);
1515          System.out.println("  positive   = " + angle.isPositive());
1516          System.out.println("  negative   = " + angle.isNegative());
1517          System.out.println("  clockwise  = " + angle.isClockwise());
1518          System.out.println("  rotations  = " + angle.getFullCircles());
1519          System.out.println("  negated    = " + angle.negate());
1520          angle.setValue(a);
1521          System.out.println("  normalized = " + angle.normalize());
1522          angle.setValue(a);
1523          System.out.println("  reversed   = " + angle.reverseDirection());
1524          angle.setValue(a);
1525          System.out.println("  posNormal  = " + angle.convertToPositiveNormal());
1526          angle.setValue(a);
1527          System.out.println("  negNormal  = " + angle.convertToNegativeNormal());
1528          angle.setValue(a);
1529          System.out.println("  minNormal  = " + angle.convertToMinAbsValueNormal());
1530          angle.setValue(a);
1531    
1532          System.out.println("  CONVERSIONS:");
1533          for (ArcUnits unit : ArcUnits.values())
1534            System.out.println("    to"+unit.name()+ " = " + angle.toUnits(unit));
1535          System.out.println("    toDMS     = "+angle.toStringDms());
1536          System.out.println("    toHMS     = "+angle.toStringHms());
1537    
1538          System.out.println("  TRIGONOMETRY:");
1539          System.out.println("    sine      = "+angle.sine());
1540          System.out.println("    cosine    = "+angle.cosine());
1541          System.out.println("    tangent   = "+angle.tangent());
1542          System.out.println("    cosecant  = "+angle.cosecant());
1543          System.out.println("    secant    = "+angle.secant());
1544          System.out.println("    cotangent = "+angle.cotangent());
1545          
1546          System.out.println();
1547        }
1548      }
1549      */
1550      /*
1551      public static void main(String[] args) throws Exception
1552      {
1553        JaxbUtility jaxb = JaxbUtility.getSharedInstance();
1554        jaxb.setLookForDefaultSchema(false);
1555        
1556        for (String arg : args)
1557        {
1558          System.out.println("INPUT:  " + arg);
1559    
1560          Angle a = Angle.parse(arg);
1561          
1562          System.out.print("OUTPUT: " + a.toString());
1563          System.out.print(", " + a.toStringHms());
1564          System.out.print(", " + a.toStringDms());
1565          System.out.println();
1566          
1567          System.out.println();
1568        }
1569      }
1570      */
1571      /*
1572      public static void main(String args[])
1573      {
1574        Angle a1 = new Angle(1.2, ArcUnits.RADIAN);
1575        System.out.println("a1 = " + a1);
1576        for (ArcUnits units : ArcUnits.values())
1577        {
1578          a1.convertTo(units);
1579          System.out.println("a1 = " + a1);
1580        }
1581        
1582        System.out.println();
1583    
1584        Angle a2 = new Angle(123.654321, ArcUnits.DEGREE);
1585        System.out.println("a2 = " + a2);
1586        for (ArcUnits units : ArcUnits.values())
1587        {
1588          a2.convertTo(units);
1589          System.out.println("a2 = " + a2);
1590        }
1591        
1592        System.out.println();
1593    
1594        Angle a3 = new Angle(100.0, ArcUnits.SECOND);
1595        System.out.println("a3 = " + a3);
1596        for (ArcUnits units : ArcUnits.values())
1597        {
1598          a3.convertTo(units);
1599          System.out.println("a3 = " + a3);
1600        }
1601        
1602        System.out.println();
1603    
1604        Angle a4 = new Angle(100.0, ArcUnits.SECOND);
1605        System.out.println("a4 = " + a4);
1606        for (ArcUnits units : ArcUnits.values())
1607        {
1608          System.out.println("a4 = " + a4.toUnits(units));
1609        }
1610      }
1611      */
1612      /*
1613      public static void main(String args[])
1614      {
1615        //Deal w/ proper display when rounding HMS/DMS (ie, no 60 mins or secs)
1616        String[] hmsText =
1617        {
1618         "0h 0m 59.95s", "0h 59m 59.95s",
1619         "-59.95s", "-59m 59.95s"
1620        };
1621        
1622        Angle a;
1623    
1624        for (int t=0; t < hmsText.length; t++)
1625        {
1626          a = Angle.parse(hmsText[t]);
1627          System.out.print("text = " + hmsText[t]);
1628          System.out.print(", ...Hms() = " + a.toStringHms());
1629          System.out.print(", ...Hms(1,1) = " + a.toStringHms(1,1));
1630          System.out.print(", toString() = " + a.toString());
1631          System.out.println();
1632        }
1633        
1634        System.out.println();
1635    
1636        String[] dmsText =
1637        {
1638         "0d 0' 59.95\"", "0d 59' 59.95\"",
1639         "-59.95\"", "-59' 59.95\""
1640        };
1641    
1642        for (int t=0; t < dmsText.length; t++)
1643        {
1644          a = Angle.parse(dmsText[t]);
1645          System.out.print("text = " + dmsText[t]);
1646          System.out.print(", ...Dms() = " + a.toStringDms());
1647          System.out.print(", ...Dms(1,1) = " + a.toStringDms(1,1));
1648          System.out.print(", toString() = " + a.toString());
1649          System.out.println();
1650        }
1651      }
1652      */
1653    }