001    package edu.nrao.sss.measure;
002    
003    import java.math.BigDecimal;
004    import java.util.regex.Pattern;
005    
006    import javax.xml.bind.annotation.XmlElement;
007    import javax.xml.bind.annotation.XmlType;
008    
009    /**
010     * An arc of either latitude or longitude on a sphere.
011     * <p>
012     * <b>Version Info:</b>
013     * <table style="margin-left:2em">
014     *   <tr><td>$Revision: 1707 $</td>
015     *   <tr><td>$Date: 2008-11-14 10:23:59 -0700 (Fri, 14 Nov 2008) $</td>
016     *   <tr><td>$Author: dharland $ (last person to modify)</td>
017     * </table></p>
018     *
019     * @author David M. Harland
020     * @since 2006-05-25
021     */
022    @XmlType(propOrder={"xmlValue", "units"})
023    public abstract class EquatorialArc<A extends EquatorialArc<A>>
024      implements Cloneable, Comparable<A>
025    {
026      //The fact that this arc uses an Angle is hidden from clients
027      private Angle angle;
028      
029      //These are here to track whether JAXB has set both the units and the value
030      //so that we can call forceCompliance.
031      private boolean jaxbSetValue;
032      private boolean jaxbSetUnits;
033    
034      //===========================================================================
035      // CONSTRUCTORS & VALIDATION
036      //===========================================================================
037      
038      private EquatorialArc()
039      {
040        jaxbSetUnits = false;
041        jaxbSetValue = false;
042      }
043      
044      /** Helps create a new arc with the given magnitude and units. */
045      EquatorialArc(BigDecimal magnitude, ArcUnits units)
046      {
047        this();
048        
049        units = nullCheck(units);
050        
051        angle = new Angle(magnitude, units);
052        
053        forceCompliance(angle);
054      }
055      
056      /** Helps create a new arc of the given angle. */
057      EquatorialArc(Angle arc)
058      {
059        this();
060        
061        angle = arc.clone();
062        
063        forceCompliance(angle);
064      }
065      
066      /**
067       * Forces the given angle into a form dictated by the extending
068       * subclasses.  For example, a specific implementation may not
069       * want to convert negative angles to positive angles.
070       */
071      abstract void forceCompliance(Angle angle);
072      
073      /**
074       * Converts units of <i>null</i> or {@code ArcUnits.UNKNOWN}
075       * to {@code ArcUnits.DEGREE}.
076       */
077      private ArcUnits nullCheck(ArcUnits units)
078      {
079        if (units == null)
080          units = ArcUnits.DEGREE;
081        
082        return units;
083      }
084    
085      //===========================================================================
086      // GETTING & SETTING THE PROPERTIES
087      //===========================================================================
088      
089      /**
090       * Returns the magnitude of this arc.
091       * @return the magnitude of this arc.
092       */
093      public BigDecimal getValue()
094      {
095        return angle.getValue();
096      }
097      
098      /**
099       * Returns the units of this arc.
100       * @return the units of this arc.
101       */
102      @XmlElement
103      public ArcUnits getUnits()
104      {
105        return angle.getUnits();
106      }
107      
108      /**
109       * Sets the magnitude and units of this arc.
110       * 
111       * @param newValue the new magnitude for this arc.
112       *                 
113       * @param newUnits the new units for this declination.
114       *              
115       * @throws IllegalArgumentException if the new value is outside of
116       *                 its legal range.
117       */
118      public void set(BigDecimal newValue, ArcUnits newUnits)
119      {
120        newUnits = nullCheck(newUnits);
121        
122        angle.set(newValue, newUnits);
123        
124        forceCompliance(angle);
125      }
126      
127      /**
128       * Sets the magnitude and units of this arc.
129       * 
130       * @param newValue the new magnitude for this arc.
131       *                 
132       * @param newUnits the new units for this declination.
133       *              
134       * @throws IllegalArgumentException if the new value is outside of
135       *                 its legal range.
136       */
137      public void set(String newValue, ArcUnits newUnits)
138      {
139        set(new BigDecimal(newValue), newUnits);
140      }
141      
142      //JAXB was having trouble with the overloaded setValue methods.
143      //These methods work around that trouble.
144      @XmlElement(name="value")
145      @SuppressWarnings("unused")
146      private BigDecimal getXmlValue()  { return getValue().stripTrailingZeros(); }
147      @SuppressWarnings("unused")
148      private void setXmlValue(BigDecimal v)  { setValue(v); }
149    
150      //This is here for persistence mechanisms
151      private void setValue(BigDecimal newValue)
152      {
153        angle.setValue(newValue);
154        
155        jaxbSetValue = true;
156        
157        jaxbForceCompliance();
158      }
159      
160      //This is here for persistence mechanisms
161      @SuppressWarnings("unused")
162      private void setUnits(ArcUnits newUnits)
163      {
164        newUnits = nullCheck(newUnits);
165        
166        angle.setUnits(newUnits);
167        
168        jaxbSetUnits = true;
169        
170        jaxbForceCompliance();
171      }
172    
173      //Once JAXB has set both the value & the units, we can then get
174      //the angle into normalized form.
175      private void jaxbForceCompliance()
176      {
177        if (jaxbSetValue && jaxbSetUnits)
178        {
179          forceCompliance(angle);
180    
181          jaxbSetValue = false;
182          jaxbSetUnits = false;
183        }
184      }
185      
186      //===========================================================================
187      // ARITHMETIC
188      //===========================================================================
189    
190      /**
191       * Adds {@code amount}, assumed to be in the same units as this
192       * arc, to this arc.
193       * <p>
194       * If the addition of {@code amount} leads to a value that is
195       * out of bounds, the result will be normalized in such a way
196       * that it is the equivalent in-bounds value.</p>
197       *  
198       * @param amount an amount to be added to this arc.
199       * @return this arc, after the addition.
200       */
201      @SuppressWarnings("unchecked")
202      public A add(BigDecimal amount)
203      {
204        Angle angle = this.getAngle().add(amount);
205        
206        forceCompliance(angle);
207        
208        //We know that "this" has to be of type A, since A is the subclass
209        //of this one, and "this" refers to the fully-formed class.
210        //The SuppressWarnings statement is for this line.
211        return (A)this;
212      }
213    
214      /**
215       * Adds {@code amount}, assumed to be in the same units as this
216       * arc, to this arc.
217       * <p>
218       * If the addition of {@code amount} leads to a value that is
219       * out of bounds, the result will be normalized in such a way
220       * that it is the equivalent in-bounds value.</p>
221       *  
222       * @param amount an amount to be added to this arc.
223       * @return this arc, after the addition.
224       */
225      @SuppressWarnings("unchecked")
226      public A add(String amount)
227      {
228        Angle angle = this.getAngle().add(amount);
229        
230        forceCompliance(angle);
231        
232        //We know that "this" has to be of type A, since A is the subclass
233        //of this one, and "this" refers to the fully-formed class.
234        //The SuppressWarnings statement is for this line.
235        return (A)this;
236      }
237    
238      /**
239       * Adds {@code amount} {@code units} to this arc.
240       * <p>
241       * If the addition of {@code amount} leads to a value that is
242       * out of bounds, the result will be normalized in such a way
243       * that it is the equivalent in-bounds value.</p>
244       *  
245       * @param amount an amount to be added to this arc.
246       * @param units the units in which {@code amount} is expressed.
247       * @return this arc, after the addition.
248       */
249      public A add(BigDecimal amount, ArcUnits units)
250      {
251        return add(new Angle(amount, units));
252      }
253    
254      /**
255       * Adds {@code amount} {@code units} to this arc.
256       * <p>
257       * If the addition of {@code amount} leads to a value that is
258       * out of bounds, the result will be normalized in such a way
259       * that it is the equivalent in-bounds value.</p>
260       *  
261       * @param amount an amount to be added to this arc.
262       * @param units the units in which {@code amount} is expressed.
263       * @return this arc, after the addition.
264       */
265      public A add(String amount, ArcUnits units)
266      {
267        return add(new Angle(amount, units));
268      }
269    
270      /**
271       * Adds the given angle to this arc.
272       * <p>
273       * If the addition of {@code addend} leads to a value that is
274       * out of bounds, the result will be normalized in such a way
275       * that it is the equivalent in-bounds value.</p>
276       * 
277       * @param addend an angle to be added to this one.
278       * @return this arc, after the addition.
279       */
280      @SuppressWarnings("unchecked")
281      public A add(Angle addend)
282      {
283        Angle thisAngle = this.getAngle().add(addend);
284        
285        forceCompliance(thisAngle);
286        
287        //We know that "this" has to be of type A, since A is the subclass
288        //of this one, and "this" refers to the fully-formed class.
289        //The SuppressWarnings statement is for this line.
290        return (A)this;
291      }
292    
293      /**
294       * Adds the other arc to this one.
295       * <p>
296       * If the addition of {@code otherArc} leads to a value that is
297       * out of bounds, the result will be normalized in such a way
298       * that it is the equivalent in-bounds value.</p>
299       * 
300       * @param otherArc an arc to be added to this one.
301       * @return this arc, after the addition.
302       */
303      public A add(A otherArc)
304      {
305        return add(otherArc.getAngle());
306      }
307    
308      /**
309       * Subtracts {@code amount}, assumed to be in the same units as this
310       * declination, from this arc.
311       * <p>
312       * If the subtraction of {@code amount} leads to a value that is
313       * out of bounds, the result will be normalized in such a way
314       * that it is the equivalent in-bounds value.</p>
315       *  
316       * @param amount an amount to be subtracted from this arc.
317       * @return this arc, after the subtraction.
318       */
319      public A subtract(BigDecimal amount)
320      {
321        return add(amount.negate());
322      }
323    
324      /**
325       * Subtracts {@code amount}, assumed to be in the same units as this
326       * declination, from this arc.
327       * <p>
328       * If the subtraction of {@code amount} leads to a value that is
329       * out of bounds, the result will be normalized in such a way
330       * that it is the equivalent in-bounds value.</p>
331       *  
332       * @param amount an amount to be subtracted from this arc.
333       * @return this arc, after the subtraction.
334       */
335      public A subtract(String amount)
336      {
337        return subtract(new BigDecimal(amount));
338      }
339    
340      /**
341       * Subtracts {@code amount} {@code units} from this arc.
342       * <p>
343       * If the subtraction of {@code amount} leads to a value that is
344       * out of bounds, the result will be normalized in such a way
345       * that it is the equivalent in-bounds value.</p>
346       *  
347       * @param amount an amount to be subtracted from this arc.
348       * @param units the units in which {@code amount} is expressed.
349       * @return this arc, after the subtraction.
350       */
351      public A subtract(BigDecimal amount, ArcUnits units)
352      {
353        return add(amount.negate(), units);
354      }
355    
356      /**
357       * Subtracts {@code amount} {@code units} from this arc.
358       * <p>
359       * If the subtraction of {@code amount} leads to a value that is
360       * out of bounds, the result will be normalized in such a way
361       * that it is the equivalent in-bounds value.</p>
362       *  
363       * @param amount an amount to be subtracted from this arc.
364       * @param units the units in which {@code amount} is expressed.
365       * @return this arc, after the subtraction.
366       */
367      public A subtract(String amount, ArcUnits units)
368      {
369        return subtract(new BigDecimal(amount), units);
370      }
371    
372      /**
373       * Subtracts the given angle from this arc.
374       * <p>
375       * If the subtraction of {@code subtrahend} leads to a value that is
376       * out of bounds, the result will be normalized in such a way
377       * that it is the equivalent in-bounds value.</p>
378       * 
379       * @param subtrahend an angle to be subtracted from this one.
380       * @return this arc, after the subtraction.
381       */
382      @SuppressWarnings("unchecked")
383      public A subtract(Angle subtrahend)
384      {
385        Angle thisAngle = this.getAngle().subtract(subtrahend);
386        
387        forceCompliance(thisAngle);
388        
389        //We know that "this" has to be of type A, since A is the subclass
390        //of this one, and "this" refers to the fully-formed class.
391        //The SuppressWarnings statement is for this line.
392        return (A)this;
393      }
394    
395      /**
396       * Subtracts the other arc from this one.
397       * <p>
398       * If the subtraction of {@code otherArc} leads to a value that is
399       * out of bounds, the result will be normalized in such a way
400       * that it is the equivalent in-bounds value.</p>
401       * 
402       * @param otherArc an arc to be subtracted from this one.
403       * @return this arc, after the subtraction.
404       */
405      public A subtract(A otherArc)
406      {
407        return subtract(otherArc.getAngle());
408      }
409    
410      //===========================================================================
411      // CONVERSION TO, AND EXPRESSION IN, OTHER UNITS
412      //===========================================================================
413      
414      /**
415       * Converts this arc to the new units.
416       * <p>
417       * After this method is complete this arc will have units of
418       * {@code units} and its <tt>value</tt> will have been converted
419       * accordingly.</p>
420       *  
421       * @param newUnits the new units for this arc.  If {@code newUnits}
422       *                 is <i>null</i> it will be treated as
423       *                 {@link ArcUnits#DEGREE}.
424       * 
425       * @return this arc.  The reason for this return type is to allow
426       *         code of this nature:
427       *         {@code double radians = 
428       *         myArc.convertTo(ArcUnits.RADIAN).getValue();}
429       */
430      @SuppressWarnings("unchecked")
431      public A convertTo(ArcUnits newUnits)
432      {
433        angle.convertTo(newUnits);
434        
435        //We know that "this" has to be of type A, since A is the subclass
436        //of this one, and "this" refers to the fully-formed class.
437        //The SuppressWarnings statement is for this line.
438        return (A)this;
439      }
440      
441      /**
442       * Returns the magnitude of this arc in {@code otherUnits}.
443       * <p>
444       * Note that this method does not alter the state of this arc.
445       * Contrast this with {@link #convertTo(ArcUnits)}.</p>
446       * 
447       * @param otherUnits the units in which to express this arc's magnitude.
448       * 
449       * @return this arc's value converted to {@code otherUnits}.
450       */
451      public BigDecimal toUnits(ArcUnits otherUnits)
452      {
453        return angle.toUnits(otherUnits);
454      }
455      
456      /**
457       * Returns this arc as an <tt>Angle</tt>.  The returned angle is
458       * not referenced by this arc, so any changes made to it will not
459       * affect this object.  Clients may manipulate the returned angle
460       * in ways that might not have been legal for this arc.
461       * 
462       * @return this arc expressed as an angle.
463       */
464      public Angle toAngle()
465      {
466        return angle.clone();
467      }
468      
469      /**
470       * Returns a representation of this arc in degrees, minutes, and seconds.
471       * 
472       * @return an array of size three in this order:
473       *         <ol start="0">
474       *           <li>An integral number of degrees.</li>
475       *           <li>An integral number of arc minutes.</li>
476       *           <li>A real number of arc seconds.</li>
477       *         </ol>
478       */
479      public Number[] toDms()
480      {
481        return angle.toDms();
482      }
483      
484      /**
485       * Returns a representation of this arc in hours, minutes, and seconds.
486       * 
487       * @return an array of size three in this order:
488       *         <ol start="0">
489       *           <li>An integral number of hours.</li>
490       *           <li>An integral number of minutes.</li>
491       *           <li>A real number of seconds.</li>
492       *         </ol>
493       */
494      public Number[] toHms()
495      {
496        return angle.toHms();
497      }
498    
499      //===========================================================================
500      // TEXT
501      //===========================================================================
502      
503      /** Returns a text representation of this arc. */
504      public String toString()
505      {
506        return angle.toString();
507      }
508      
509      /**
510       * Returns a text representation of this arc.
511       * 
512       * @param minFracDigits the minimum number of places after the decimal point.
513       *                      
514       * @param maxFracDigits the maximum number of places after the decimal point.
515       */
516      public String toString(int minFracDigits, int maxFracDigits)
517      {
518        return angle.toString(minFracDigits, maxFracDigits);
519      }
520      
521      /**
522       * Returns a text representation of this angle in
523       * hours, minutes, and seconds.
524       */
525      public String toStringHms()
526      {
527        return angle.toStringHms();
528      }
529      
530      /**
531       * Returns a text representation of this angle in
532       * hours, minutes, and seconds.
533       * 
534       * @param minFracDigits the minimum number of places after the decimal point
535       *                      for the seconds field.
536       *                      
537       * @param maxFracDigits the maximum number of places after the decimal point
538       *                      for the seconds field.
539       */
540      public String toStringHms(int minFracDigits, int maxFracDigits)
541      {
542        return angle.toStringHms(minFracDigits, maxFracDigits);
543      }
544    
545      /**
546       * Returns a text representation of this angle in
547       * degrees, arc-minutes, and arc-seconds.
548       */
549      public String toStringDms()
550      {
551        return angle.toStringDms();
552      }
553    
554      /**
555       * Returns a text representation of this angle in
556       * degrees, arc-minutes, and arc-seconds.
557       * 
558       * @param minFracDigits the minimum number of places after the decimal point
559       *                      for the seconds field.
560       *                      
561       * @param maxFracDigits the maximum number of places after the decimal point
562       *                      for the seconds field.
563       */
564      public String toStringDms(int minFracDigits, int maxFracDigits)
565      {
566        return angle.toStringDms(minFracDigits, maxFracDigits);
567      }
568      
569      /**
570       * Returns a text representation of this angle in
571       * degrees, arc-minutes, and arc-seconds, with HTML-friendly symbols.
572       * 
573       * @param minFracDigits the minimum number of places after the decimal point
574       *                      for the seconds field.
575       *                      
576       * @param maxFracDigits the maximum number of places after the decimal point
577       *                      for the seconds field.
578       */
579      public String toStringDmsHtml(int minFracDigits, int maxFracDigits)
580      {
581        return angle.toStringDmsHtml(minFracDigits, maxFracDigits);
582      }
583    
584      /**
585       * Takes a string either of form hh:mm:ss or dd:mm:ss and replaces
586       * the first colon with x, the second colon with m, and appends s.
587       */
588      static String convertColonToXms(String xmsString,
589                                      ArcUnits x, ArcUnits m, ArcUnits s)
590      {
591        StringBuilder buff = new StringBuilder(xmsString);
592        
593        int colonPosition = buff.indexOf(":");
594        if (colonPosition < 0)
595          throwColonParseError(xmsString);
596        buff.replace(colonPosition, colonPosition+1, x.getSymbol());
597        
598        colonPosition = buff.indexOf(":");
599        if (colonPosition < 0)
600          throwColonParseError(xmsString);
601        buff.replace(colonPosition, colonPosition+1, m.getSymbol());
602        
603        buff.append(s.getSymbol());
604        
605        return buff.toString();
606      }
607      
608      private static void throwColonParseError(String xmsString)
609      {
610        throw new IllegalArgumentException("Cannot parse " + xmsString +
611          ". Expected either hh:mm:ss or dd:mm:ss.");
612      }
613    
614      private static final Pattern ANY_ALPHA = Pattern.compile(".*[a-zA-Z].*");
615      
616      private static final String WHITESPACE = "\\s+";
617      
618      /**
619       * If text is delimited by whitespace into three parts, replace
620       * whitespace with colons.  Otherwise, just return text as is.
621       * (Actually, leading and trailing whitespace may be removed.)
622       */
623      static String delimitWithColonIfTextHasThreeParts(String text)
624      {
625        //Quick exit if text already has colons, or if it has
626        //alpha chars (which signify presence of units).
627        if (ANY_ALPHA.matcher(text).matches() ||
628            text.contains(":") ||
629            text.contains(ArcUnits.ARC_MINUTE.getSymbol()) ||
630            text.contains(ArcUnits.ARC_SECOND.getSymbol()))
631          return text;
632    
633        //See if whitespace delimits this into 3 parts
634        text = text.trim();
635        
636        String[] parts = text.split(WHITESPACE);
637        
638        if (parts.length == 3)
639        {
640          StringBuilder buff = new StringBuilder(parts[0]);
641          buff.append(':').append(parts[1]);
642          buff.append(':').append(parts[2]);
643          
644          text = buff.toString();
645        }
646        
647        return text;
648      }
649      
650      //===========================================================================
651      // UTILITY METHODS
652      //===========================================================================
653    
654      /** Returns an arc that is equal to this one. */
655      @SuppressWarnings("unchecked")
656      public A clone()
657      {
658        A clone = null;
659        
660        try
661        {
662          clone = (A)super.clone();
663          clone.angle = this.angle.clone();
664        }
665        catch (CloneNotSupportedException ex)
666        {
667          throw new RuntimeException(ex);
668        }
669        
670        return clone;
671      }
672    
673      /** Returns <i>true</i> if {@code o} is equal to this arc. */
674      @SuppressWarnings("unchecked")
675      @Override
676      public boolean equals(Object o)
677      {
678        //Quick exit if o is this
679        if (o == this)
680          return true;
681        
682        //Quick exit if o is null
683        if (o == null)
684          return false;
685        
686        //Quick exit if classes are different
687        if (!o.getClass().equals(this.getClass()))
688          return false;
689        
690        EquatorialArc other = (EquatorialArc)o;
691        
692        return this.angle.equals(other.angle);
693      }
694      
695      /** Returns a hash code value for this arc. */
696      public int hashCode()
697      {
698        return angle.hashCode();
699      }
700      
701      /** Compares this arc with the {@code otherArc} for order. */
702      public int compareTo(A otherArc)
703      {
704        return this.angle.compareTo(otherArc.angle);
705      }
706    
707      //===========================================================================
708      // 
709      //===========================================================================
710    
711      /**
712       * The subclasses sometimes need to work directly with angle.
713       * Note that the returned angle is the one actually used by
714       * this class, so do not change its value unless you absolutely
715       * know what you're doing.
716       */
717      Angle getAngle()  { return angle; }
718    }