001    package edu.nrao.sss.measure;
002    
003    import java.math.BigDecimal;
004    import java.math.MathContext;
005    import java.math.RoundingMode;
006    
007    import edu.nrao.sss.util.StringUtil;
008    
009    /**
010     * The latitudinal coordinate of a point on a sphere.
011     * The other coordinate is {@link Longitude}.
012     * <p>
013     * The major usage of this class is as a <i>declination</i>.
014     * See <a href="http://en.wikipedia.org/wiki/Declination">Wikipedia</a>
015     * for an explanation of declination.</p> 
016     * <p>
017     * <b>Version Info:</b>
018     * <table style="margin-left:2em">
019     *   <tr><td>$Revision: 161 $</td>
020     *   <tr><td>$Date: 2006-12-15 18:48:34 +0000 (Fri, 15 Dec 2006) $</td>
021     *   <tr><td>$Author: btruitt $ (last person to update)</td>
022     * </table></p>
023     * 
024     * @author David M. Harland
025     * @since 2006-02-27
026     */
027    public class Latitude
028      extends EquatorialArc<Latitude>
029    {
030      //===========================================================================
031      // CLASS & INSTANCE VARIABLES
032      //===========================================================================
033    
034      private static final MathContext PRECISION =
035        new MathContext(MathContext.DECIMAL128.getPrecision(),RoundingMode.HALF_UP);
036    
037      private static final BigDecimal DEFAULT_VALUE =
038        new BigDecimal("0.0", PRECISION);
039      
040      private static final ArcUnits DEFAULT_UNITS = ArcUnits.DEGREE;
041      
042      private boolean increasesNorthward = true;
043      
044      //===========================================================================
045      // CONSTRUCTORS & VALIDATION
046      //===========================================================================
047      
048      /** Creates a new latitude of 0.0 degrees. */
049      public Latitude()
050      {
051        super(DEFAULT_VALUE, DEFAULT_UNITS);
052      }
053      
054      /**
055       * Creates a new latitude from the given angle.
056       * 
057       * @param latitude
058       *   an angle of latitude.
059       *   
060       * @throws NullPointerException
061       *   if <tt>latitude</tt> is <i>null</i>
062       */
063      public Latitude(Angle latitude)
064      {
065        super(latitude);
066      }
067      
068      /**
069       * Creates a new latitude of {@code degrees} degrees.
070       * If {@code degrees} is not a valid value<sup>1</sup> for
071       * latitude, it will be normalized in a way that will
072       * transform it to a legal value.
073       * <p>
074       * <sup>1</sup> <i>To be legal, {@code degrees} must be greater
075       * than or equal to -90.0 and less than or equal to +90.0.</i></p>
076       */
077      public Latitude(BigDecimal degrees)
078      {
079        super(degrees, ArcUnits.DEGREE);
080      }
081      
082      /**
083       * Creates a new latitude of {@code degrees} degrees.
084       * If {@code degrees} is not a valid value<sup>1</sup> for
085       * latitude, it will be normalized in a way that will
086       * transform it to a legal value.
087       * <p>
088       * <sup>1</sup> <i>To be legal, {@code degrees} must be greater
089       * than or equal to -90.0 and less than or equal to +90.0.</i></p>
090       */
091      public Latitude(String degrees)
092      {
093        this(new BigDecimal(degrees));
094      }
095    
096      /**
097       * Creates a new latitude with the given magnitude and units.
098       * If {@code magnitude} is not a valid value<sup>1</sup> for
099       * latitude, it will be normalized in a way that will
100       * transform it to a legal value.
101       * <p>
102       * <sup>1</sup> <i>To be legal, {@code magnitude} must be greater
103       * than or equal to the negative of one-quarter of a circle and
104       * less than or equal to one-quarter of a circle, in the given
105       * units.</i></p>
106       */
107      public Latitude(BigDecimal magnitude, ArcUnits units)
108      {
109        super(magnitude, units);
110      }
111    
112      /**
113       * Creates a new latitude with the given magnitude and units.
114       * If {@code magnitude} is not a valid value<sup>1</sup> for
115       * latitude, it will be normalized in a way that will
116       * transform it to a legal value.
117       * <p>
118       * <sup>1</sup> <i>To be legal, {@code magnitude} must be greater
119       * than or equal to the negative of one-quarter of a circle and
120       * less than or equal to one-quarter of a circle, in the given
121       * units.</i></p>
122       */
123      public Latitude(String magnitude, ArcUnits units)
124      {
125        this(new BigDecimal(magnitude), units);
126      }
127      
128      /**
129       * Converts the incoming angle so that it is
130       * less than or equal to one-quarter circle and 
131       * greater than or equal to the negative of one-quarter circle.
132       */
133      void forceCompliance(Angle angle)
134      {
135        //Make absolute value of angle less than one full circle
136        angle.normalize();
137        
138        //Abs value is now less than one full circle
139        //Make abs val of angle less than one-half circle
140        ArcUnits   units      = angle.getUnits(); 
141        BigDecimal value      = angle.getValue();
142        BigDecimal halfCircle = units.toHalfCircle();
143        BigDecimal fullCircle = units.toFullCircle();
144        
145        int posComp = value.compareTo(halfCircle);
146        int negComp = value.compareTo(halfCircle.negate());
147        
148        if (posComp == 0 || negComp == 0)  //value == half circle
149        {
150          value = BigDecimal.ZERO;
151        }
152        else if (posComp > 0)              //value > half circle
153        {
154          value = value.subtract(fullCircle);
155        }
156        else if (negComp < 0)              //value < negative half circle
157        {
158          value = value.add(fullCircle);
159        }
160      //else abs val < 1/2 circle already
161    
162        //Abs value is now less than one half circle
163        //Make abs val of angle less than or equal to one-quarter circle
164        BigDecimal quarterCircle = units.toQuarterCircle();
165    
166        if (value.compareTo(quarterCircle) > 0)                //value > +1/4 circle
167        {
168          value = halfCircle.subtract(value);
169        }
170        else if (value.compareTo(quarterCircle.negate()) < 0)  //value < -1/4 circle
171        {
172          value = halfCircle.negate().subtract(value);
173        }
174      //else abs val <= 1/4 circle already
175        
176        angle.set(value, units);
177      }
178    
179      /**
180       * Resets this latitude so that it is equal to a latitude created
181       * via the no-argument constructor.
182       */
183      public void reset()
184      {
185        set(DEFAULT_VALUE, DEFAULT_UNITS);
186      }
187    
188      //===========================================================================
189      // LATITUDE-SPECIFIC METHODS
190      //===========================================================================
191    
192      /**
193       * Configures the directional convention for this latitude.
194       * If {@code northIsUp} is <i>true</i>, then latitude values become
195       * more positive in a northward direction.  If it is <i>false</i>, then they
196       * become more positive in a southward direction.
197       * 
198       * @param northIsUp <i>true</i> if latitude increases when traveling north.
199       */
200      public void setIncreasesNorthward(boolean northIsUp)
201      {
202        increasesNorthward = northIsUp;
203      }
204      
205      /**
206       * Returns <i>true</i> if latitude increases when traveling north.
207       * @return <i>true</i> if latitude increases when traveling north.
208       */
209      public boolean getIncreasesNorthward()
210      {
211        return increasesNorthward;
212      }
213      
214      /**
215       * Returns <i>true</i> if this latitude is north of {@code other}.
216       * @param other the latitude to be tested.
217       * @return <i>true</i> if this latitude is north of {@code other}.
218       */
219      public boolean isNorthOf(Latitude other)
220      {
221        int comp = this.getValue().compareTo(other.getValue());
222        
223        return increasesNorthward ? comp > 0 : comp < 0;
224      }
225      
226      /**
227       * Returns <i>true</i> if this latitude is south of {@code other}.
228       * @param other the latitude to be tested.
229       * @return <i>true</i> if this latitude is south of {@code other}.
230       */
231      public boolean isSouthOf(Latitude other)
232      {
233        int comp = this.getValue().compareTo(other.getValue());
234        
235        return increasesNorthward ? comp < 0 : comp > 0;
236      }
237    
238      /**
239       * Returns <i>true</i> if this latitude is north of the equator.
240       * @return <i>true</i> if this latitude is north of the equator.
241       */
242      public boolean isNorthOfEquator()
243      {
244        int signum = this.getValue().signum();
245        
246        return increasesNorthward ? signum > 0 : signum < 0;
247      }
248      
249      /**
250       * Returns <i>true</i> if this latitude is south of the equator.
251       * @return <i>true</i> if this latitude is south of the equator.
252       */
253      public boolean isSouthOfEquator()
254      {
255        int signum = this.getValue().signum();
256        
257        return increasesNorthward ? signum < 0 : signum > 0;
258      }
259      
260      /**
261       * Returns <i>true</i> if this latitude is one of the poles.
262       * @return <i>true</i> if this latitude is one of the poles.
263       * @since 2008-10-15
264       */
265      public boolean isPole()
266      {
267        BigDecimal qtrCircle = this.getUnits().toQuarterCircle();
268        
269        return this.getValue().abs().compareTo(qtrCircle) == 0;
270      }
271    
272      /**
273       * Returns <i>true</i> if this latitude is the north pole.
274       * @return <i>true</i> if this latitude is the north pole.
275       * @since 2008-10-15
276       */
277      public boolean isNorthPole()
278      {
279        return isPole() && isNorthOfEquator();
280      }
281      
282      /**
283       * Returns <i>true</i> if this latitude is the north pole.
284       * @return <i>true</i> if this latitude is the north pole.
285       * @since 2008-10-15
286       */
287      public boolean isSouthPole()
288      {
289        return isPole() && isSouthOfEquator();
290      }
291      
292      /**
293       * Returns <i>true</i> if this latitude is the equator.
294       * @return <i>true</i> if this latitude is the equator.
295       * @since 2008-10-15
296       */
297      public boolean isEquator()
298      {
299        return this.getValue().signum() == 0;
300      }
301    
302      //===========================================================================
303      // TEXT
304      //===========================================================================
305    
306      /**
307       * Returns a string where the degrees, minutes, and seconds are separated
308       * by the given string.
309       * 
310       * @param separator the separator to use between the degrees and minutes,
311       *                  and minutes and seconds, fields.
312       *                  
313       * @return a text representation of this latitude.
314       */
315      public String toString(String separator)
316      {
317        return toString(separator, 0, 999);
318      }
319    
320      /**
321       * Returns a string where the degrees, minutes, and seconds are separated
322       * by the given string.
323       * 
324       * @param separator the separator to use between the degrees and minutes,
325       *                  and minutes and seconds, fields.
326       * 
327       * @param minFracDigits the minimum number of places after the decimal point
328       *                      for the seconds field.
329       *                      
330       * @param maxFracDigits the maximum number of places after the decimal point
331       *                      for the seconds field.
332       *                  
333       * @return a text representation of this latitude.
334       */
335      public String toString(String separator, int minFracDigits, int maxFracDigits)
336      {
337        Number[] dms = toDms();
338        
339        int        degrees = dms[0].intValue();
340        int        minutes = dms[1].intValue();
341        BigDecimal seconds = new BigDecimal(dms[2].toString());
342    
343        
344        StringBuilder buff = new StringBuilder();
345        
346        //Deal with the sign.  Only one of {d, m, s} can be negative,
347        //but it could be any one of them.  (If m is < 0, d must be 0;
348        //if s < 0.0, both d & m must be 0.)
349        char sign = '+';
350        
351        if (degrees < 0)
352        {
353          sign = '-';
354          degrees = -degrees;
355        }
356        else if (degrees == 0)
357        {
358          if (minutes < 0)
359          {
360            sign = '-';
361            minutes = -minutes;
362          }
363          else if (minutes == 0)
364          {
365            if (seconds.signum() < 0)
366            {
367              sign = '-';
368              seconds = seconds.negate();
369            }
370          }
371        }
372        
373        buff.append(sign);
374        
375        if (degrees < 10)
376          buff.append('0');
377        
378        buff.append(degrees).append(separator);
379        
380        if (minutes < 10)
381          buff.append('0');
382        
383        buff.append(minutes).append(separator);
384        
385        if (seconds.compareTo(BigDecimal.TEN) < 0)
386          buff.append('0');
387    
388        buff.append(StringUtil.getInstance().formatNoScientificNotation(
389                    seconds, minFracDigits, maxFracDigits));
390        
391        return buff.toString();
392      }
393    
394      /**
395       * Returns a new latitude based on the given text.
396       * <p>
397       * See the {@link edu.nrao.sss.measure.Angle#parse(String) parse}
398       * method of {@code Angle} for information on the format of
399       * {@code text}.  This {@code Latitude} class offers two other
400       * formats:
401       * <ol>
402       *   <li>dd:mm:ss.sss</li>
403       *   <li>dd mm ss.sss</li>
404       * </ol>
405       * Both of the above are in degrees, arc-minutes, and arc-seconds.
406       * For the first alternative form, whitespace is permitted around the
407       * colon characters.  For the second alternative form, any type and
408       * number of whitespace characters may be used in between the three
409       * parts.</p>
410       * <p>
411       * The parsed value, if not a legal value for latitude, will be
412       * normalized in such a way that it is transformed to a legal value.
413       * To be legal, {@code magnitude} must be greater
414       * than or equal to the negative of one-quarter of a circle and
415       * less than or equal to one-quarter of a circle, in the given
416       * units.</p>
417       * 
418       * @param text a string that will be converted into a latitude.
419       * 
420       * @return a new latitude.  If parsing was successful, the value of the
421       *         latitude will be based on the parameter string.  If it was
422       *         not, the returned latitude will be of zero degrees.
423       * 
424       * @throws IllegalArgumentException if {@code text} is not in
425       *                                  the expected form.
426       */
427      public static Latitude parse(String text)
428      {
429        Latitude newLatitude = new Latitude();
430        
431        Angle angle = newLatitude.getAngle();
432        
433        //First see if we have "dd mm ss.sss", where whitespace is used
434        //to delimit the three parts.  If we do, the returned text will
435        //be colon-delimited; otherwise it will be returned unchanged.
436        text = delimitWithColonIfTextHasThreeParts(text);
437        
438        //For latitude, convert 99:99:99.999 to degrees, arc-minutes, arc-seconds
439        if (text.contains(":"))
440          text = convertColonToXms(text, ArcUnits.DEGREE,
441                                         ArcUnits.ARC_MINUTE, ArcUnits.ARC_SECOND); 
442        angle.set(text);
443        
444        newLatitude.forceCompliance(angle);
445    
446        return newLatitude;
447      }
448      
449      /**
450       * Sets the value and units of this latitude base on the given text.
451       * See {@link #parse(String)} for the expected format of
452       * {@code text}.
453       * <p>
454       * See the {@link edu.nrao.sss.measure.Angle#parse(String) parse}
455       * method of {@code Angle} for information on the format of
456       * {@code text}.</p>
457       * <p>
458       * The parsed value, if not a legal value for latitude, will be
459       * normalized in such a way that it is transformed to a legal value.
460       * To be legal, {@code magnitude} must be greater
461       * than or equal to the negative of one-quarter of a circle and
462       * less than or equal to one-quarter of a circle, in the given
463       * units.</p>
464       *                                  
465       * @param text a string that will be converted into
466       *                  a latitude.
467       * 
468       * @throws IllegalArgumentException if {@code text} is not in
469       *                                  the expected form.
470       */
471      public void set(String text)
472      {
473        Latitude temp = Latitude.parse(text); 
474    
475        this.set(temp.getValue(), temp.getUnits());
476      }
477      
478      /*
479      public static void main(String[] args)
480      {
481        for (String arg : args)
482        {
483          System.out.println("INPUT:  " + arg);
484          System.out.println("OUTPUT: " + Latitude.parse(arg).toString());
485          System.out.println();
486        }
487      }
488      */
489    }