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