001    package edu.nrao.sss.geom;
002    
003    import java.math.BigDecimal;
004    import java.util.Date;
005    import java.util.TimeZone;
006    
007    import uk.ac.starlink.pal.AngleDR;
008    import uk.ac.starlink.pal.Pal;
009    
010    import static edu.nrao.sss.astronomy.CelestialCoordinateSystem.EQUATORIAL;
011    import static edu.nrao.sss.astronomy.VelocityFrame.*;
012    import static edu.nrao.sss.math.MathUtil.MC_INTERM_CALCS;
013    import static edu.nrao.sss.measure.ArcUnits.RADIAN;
014    import static edu.nrao.sss.measure.LinearVelocityUnits.KILOMETERS_PER_SECOND;
015    
016    import edu.nrao.sss.astronomy.CoordinateConversionException;
017    import edu.nrao.sss.astronomy.Epoch;
018    import edu.nrao.sss.astronomy.SkyPosition;
019    import edu.nrao.sss.astronomy.VelocityFrame;
020    import edu.nrao.sss.measure.Angle;
021    import edu.nrao.sss.measure.ArcUnits;
022    import edu.nrao.sss.measure.Distance;
023    import edu.nrao.sss.measure.DistanceUnits;
024    import edu.nrao.sss.measure.JulianDate;
025    import edu.nrao.sss.measure.Latitude;
026    import edu.nrao.sss.measure.LinearVelocity;
027    import edu.nrao.sss.measure.LinearVelocityUnits;
028    import edu.nrao.sss.measure.LocalSiderealTime;
029    import edu.nrao.sss.measure.Longitude;
030    import edu.nrao.sss.measure.TimeDuration;
031    import edu.nrao.sss.measure.TimeUnits;
032    
033    /**
034     * A position on, in, or above the earth.
035     * The most common use of this class is for surface positions, where
036     * the position is given in terms of longitude, latitude, and elevation
037     * above mean sea level.  The time zone for this position may also be
038     * specified.
039     * <p>
040     * Instances of this class are immutable.  The mutable objects contained
041     * within this class are always cloned before being accepted or passed
042     * out.</p>
043     * <p>
044     * <b>Version Info:</b>
045     * <table style="margin-left:2em">
046     *   <tr><td>$Revision: 1625 $</td></tr>
047     *   <tr><td>$Date: 2008-10-16 09:28:49 -0600 (Thu, 16 Oct 2008) $</td></tr>
048     *   <tr><td>$Author: dharland $ (last person to modify)</td></tr>
049     * </table></p>
050     * 
051     * @author David M. Harland
052     * @since 2008-02-25
053     */
054    public class EarthPosition
055      implements SphericalPosition
056    {
057      private static final BigDecimal BD2 = new BigDecimal("2.0", MC_INTERM_CALCS);
058    
059      private Longitude longitude;
060      private Latitude  latitude;
061      private Distance  distance;
062      
063      private Longitude longitudeUncertainty;
064      private Latitude  latitudeUncertainty;
065      private Distance  distanceUncertainty;
066      
067      private TimeZone  timeZone;
068      
069      private static Pal SLALIB = new Pal();
070      
071      //============================================================================
072      // 
073      //============================================================================
074    
075      /**
076       * Creates a new position at the given location.
077       * <p>
078       * Any parameter that is <i>null</i> will be treated as a value of zero.
079       * Values that are not <i>null</i> will be copied into this class, not
080       * referenced.  This means changes made to a parameter object after
081       * creation of an instance of this class will <i>not</i> be reflected
082       * herein.</p> 
083       * 
084       * @param lon
085       *   the longitude of this position.
086       *   A value of <i>null</i> will be treated as a value of zero.
087       * @param lat
088       *   the latitude of this position.
089       *   A value of <i>null</i> will be treated as a value of zero.
090       * @param dist
091       *   the distance (normally a height above or below sea level) of this
092       *   position.
093       *   A value of <i>null</i> will be treated as a value of zero.
094       * @param tz
095       *   the time zone of this position.
096       *   If this value is <i>null</i>, a simple algorithm based on the
097       *   longitude will be used to create a time zone value.  It is
098       *   very likely that this calculated time zone will not be the
099       *   true time zone of this position.
100       * @param lonUncertainty
101       *   the uncertainty in the longitude of this position.
102       *   A value of <i>null</i> will be treated as a value of zero.
103       * @param latUncertainty
104       *   the uncertainty in the latitude of this position.
105       *   A value of <i>null</i> will be treated as a value of zero.
106       * @param distUncertainty
107       *   the uncertainty in the distance of this position.
108       *   A value of <i>null</i> will be treated as a value of zero.
109       */
110      public EarthPosition(Longitude lon, Latitude lat, Distance dist, TimeZone  tz,
111                           Longitude lonUncertainty, Latitude  latUncertainty,
112                           Distance  distUncertainty)
113      {
114        longitude = (lon  == null) ? new Longitude() : lon.clone();
115        latitude  = (lat  == null) ? new Latitude()  : lat.clone();
116        distance  = (dist == null) ? new Distance()  : dist.clone();
117        
118        longitudeUncertainty = (lonUncertainty == null) ? new Longitude()
119                                                        : lonUncertainty.clone();
120        
121        latitudeUncertainty = (latUncertainty == null) ? new Latitude()
122                                                       : latUncertainty.clone();
123        
124        if (distUncertainty != null)
125        {
126          distanceUncertainty = distUncertainty.clone();
127        }
128        else
129        {
130          DistanceUnits units = (distance == null) ? DistanceUnits.METER
131                                                   : distance.getUnits();
132          distanceUncertainty = new Distance("0.0", units);
133        }
134        
135        if (tz != null)
136        {
137          timeZone = (TimeZone)tz.clone();
138        }
139        else
140        {
141          int hours = (int)Math.floor(longitude.toUnits(ArcUnits.HOUR).doubleValue());
142          
143          StringBuilder tzCode = new StringBuilder("GMT+");
144          if (hours < 10)
145            tzCode.append('0');
146          tzCode.append(hours).append(":00");
147          
148          timeZone = TimeZone.getTimeZone(tzCode.toString());
149        }
150      }
151      
152      /**
153       * Creates a new position at the given location.
154       * 
155       * @param longitude
156       *   the longitude of this position.
157       *   A value of <i>null</i> will be treated as a value of zero.
158       * @param latitude
159       *   the latitude of this position.
160       *   A value of <i>null</i> will be treated as a value of zero.
161       * @param distance
162       *   the distance (normally a height above or below sea level) of this
163       *   position.
164       *   A value of <i>null</i> will be treated as a value of zero.
165       * @param timeZone
166       *   the time zone of this position.
167       *   If this value is <i>null</i>, a simple algorithm based on the
168       *   longitude will be used to create a time zone value.  It is
169       *   very likely that this calculated time zone will not be the
170       *   true time zone of this position.
171       */
172      public EarthPosition(Longitude longitude, Latitude latitude,
173                           Distance  distance,  TimeZone timeZone)
174      {
175        this(longitude, latitude, distance, timeZone, null, null, null);
176      }
177      
178      /**
179       * Creates a new position at the given location.
180       * The distance value of this position will be zero.
181       * 
182       * @param longitude
183       *   the longitude of this position.
184       *   A value of <i>null</i> will be treated as a value of zero.
185       * @param latitude
186       *   the latitude of this position.
187       *   A value of <i>null</i> will be treated as a value of zero.
188       * @param timeZone
189       *   the time zone of this position.
190       *   If this value is <i>null</i>, a simple algorithm based on the
191       *   longitude will be used to create a time zone value.  It is
192       *   very likely that this calculated time zone will not be the
193       *   true time zone of this position.
194       */
195      public EarthPosition(Longitude longitude,
196                           Latitude  latitude, TimeZone timeZone)
197      {
198        this(longitude, latitude, null, timeZone, null, null, null);
199      }
200      
201      /**
202       * Creates a null-like position in which all component values are zero.
203       */
204      public EarthPosition()
205      {
206        this(null, null, null, null, null, null, null);
207      }
208      
209      //============================================================================
210      // INTERFACE SphericalPosition 
211      //============================================================================
212      
213      /** Returns a copy of this position's longitude. */
214      public Longitude getLongitude()
215      {
216        return longitude.clone();
217      }
218    
219      /** Returns a copy of this position's latitude. */
220      public Latitude getLatitude()
221      {
222        return latitude.clone();
223      }
224    
225      /**
226       * Returns a copy of this position's distance.
227       * What this distance represents is the responsibility of the client.
228       * By convention, however, the distance for this class usually represents
229       * a distance above mean sea level.
230       */
231      public Distance getDistance()
232      {
233        return distance.clone();
234      }
235      
236      /**
237       * Returns a copy of the time zone in effect at this position.
238       * Note that this value is only as accurate as the value that was
239       * passed into the constructor of this object.  This class does not
240       * know which time zones are associated with which positions on the
241       * earth.
242       * 
243       * @return a copy of the time zone in effect at this position.
244       */
245      public TimeZone getTimeZone()
246      {
247        return (TimeZone)timeZone.clone();
248      }
249    
250      /** Returns a copy of the uncertainty in this position's longitude. */
251      public Longitude getLongitudeUncertainty()
252      {
253        return longitudeUncertainty.clone();
254      }
255      
256      /** Returns a copy of the uncertainty in this position's latitude. */
257      public Latitude getLatitudeUncertainty()
258      {
259        return latitudeUncertainty.clone();
260      }
261    
262      /** Returns a copy of the uncertainty in this position's distance. */
263      public Distance getDistanceUncertainty()
264      {
265        return distanceUncertainty.clone();
266      }
267    
268      public Angle getAngularSeparation(SphericalPosition other)
269      {
270        return calculateAngularSeparation( this.getLatitude(),
271                                          other.getLatitude(),
272                                           this.getLongitude(),
273                                          other.getLongitude());
274      }
275      
276      //============================================================================
277      // CALCULATIONS & MISC
278      //============================================================================
279    
280      /**
281       * Calculates the angular separation between two positions.
282       * <p>
283       * The preferred means of getting the angular separation between one
284       * position and another is {@link #getAngularSeparation(SphericalPosition)}.
285       * This method is made available to other classes that might need
286       * to embed this algorithm into their own logic.</p>
287       * <p>
288       * Note that this method does not consider distance, but only the
289       * angular components of a spherical position.</p>
290       * 
291       * @param latitude1
292       *   the latitude of the first position.
293       * @param latitude2
294       *   the latitude of the second position.
295       * @param longitude1
296       *   the longitude of the first position.
297       * @param longitude2
298       *   the longitude of the second position.
299       * @return
300       *   the angle of separation between the two positions.
301       */
302      public static Angle calculateAngularSeparation(Latitude  latitude1,
303                                                     Latitude  latitude2,
304                                                     Longitude longitude1,
305                                                     Longitude longitude2)
306      {
307        final double ALMOST_ONE = 1.0 - Math.ulp(1.0);
308    
309        Angle answer;
310        
311        //SPECIAL CASE A: POINTS HAVE SAME LONGITUDE
312        //Remember that longitude spans only half the sphere (terminates at poles),
313        //so we can just use difference in latitudes.
314        //if (lon1.compareTo(lon2) == 0)
315        if (longitude1.compareTo(longitude2) == 0)
316        {
317          answer = latitude1.toAngle().subtract(latitude2.toAngle())
318                                      .convertToMinAbsValueNormal();
319          
320          if (answer.getValue().signum() < 0)
321            answer.negate();
322        }
323        //SPECIAL CASE B: ONE OR BOTH POINTS IS A POLE
324        else if (latitude1.isPole() || latitude2.isPole())
325        {
326          answer = latitude1.toAngle().subtract(latitude2.toAngle())
327                                      .convertToMinAbsValueNormal();
328          
329          if (answer.getValue().signum() < 0)
330            answer.negate();
331        }
332        //SPECIAL CASE C: POINTS ARE ON EQUATOR
333        else if (latitude1.isEquator() && latitude2.isEquator())
334        {
335          answer = longitude1.toAngle().subtract(longitude2.toAngle())
336                                       .convertToMinAbsValueNormal();
337          
338          if (answer.getValue().signum() < 0)
339            answer.negate();
340        }
341        //GENERAL CASE
342        else
343        {
344          //Algorithm found on web.  Several sources cite:
345          //R.W. Sinnott, "Virtues of the Haversine", Sky and Telescope,
346          //vol. 68, no. 2, 1984, p. 159
347          BigDecimal lat1 =  latitude1.toUnits(ArcUnits.RADIAN);
348          BigDecimal lat2 =  latitude2.toUnits(ArcUnits.RADIAN);
349          BigDecimal lon1 = longitude1.toUnits(ArcUnits.RADIAN);
350          BigDecimal lon2 = longitude2.toUnits(ArcUnits.RADIAN);
351          
352          BigDecimal separationRadians;
353          
354          BigDecimal temp = lat2.subtract(lat1).divide(BD2, MC_INTERM_CALCS);
355          BigDecimal sinSqLat = BigDecimal.valueOf(Math.sin(temp.doubleValue()));
356          sinSqLat = sinSqLat.multiply(sinSqLat);
357          
358          temp = lon2.subtract(lon1).divide(BD2, MC_INTERM_CALCS);
359          BigDecimal sinSqLon = BigDecimal.valueOf(Math.sin(temp.doubleValue()));
360          sinSqLon = sinSqLon.multiply(sinSqLon);
361          
362          BigDecimal cosLat1 = new BigDecimal(Math.cos(lat1.doubleValue()));
363          BigDecimal cosLat2 = new BigDecimal(Math.cos(lat2.doubleValue()));
364          BigDecimal a = cosLat1.multiply(cosLat2).multiply(sinSqLon).add(sinSqLat);
365    
366          double sqrtA = Math.sqrt(a.doubleValue());
367          
368          double sepRad = (sqrtA > ALMOST_ONE) ? Math.PI : 2.0 * Math.asin(sqrtA);
369          
370          separationRadians = BigDecimal.valueOf(sepRad);
371          
372          answer = new Angle(separationRadians, ArcUnits.RADIAN);
373        }
374        
375        return answer;
376      }
377      
378      /**
379       * Returns the velocity of this position to the given sky position at a
380       * particular point in time and with respect to the given frame of reference.
381       * A positive velocity indicates that this earth position is moving <i>away
382       * from</i> the given sky position.
383       * 
384       * @param skyPosition
385       *   a position in the sky toward or from which velocity is to be calculated.
386       *     
387       * @param calcTime
388       *   the point in time at which the velocity is calculated.
389       *   
390       * @param referenceFrame
391       *   the frame of reference used in the velocity calculation.  The 
392       *   {@code skyPosition} is assumed to be motionless relative to this frame.
393       *   The <tt>UNKNOWN</tt> and <tt>COSMIC_MICROWAVE_BACKGROUND</tt> types
394       *   are unsupported and will result in an {@code IllegalArgumentException}.
395       *   
396       * @return
397       *   the radial velocity of this position toward or away from a point
398       *   in space.
399       *   
400       * @throws CoordinateConversionException
401       *   if {@code skyPosition} cannot be converted to an equatorial
402       *   RA / Dec position.
403       *   
404       * @throws IllegalArgumentException
405       *   if the {@code referenceFrame} is either 
406       *   <tt>UNKNOWN</tt> or <tt>COSMIC_MICROWAVE_BACKGROUND</tt>.
407       *   
408       * @since 2008-07-29
409       */
410      public LinearVelocity calcVelocityRelativeTo(SkyPosition   skyPosition,
411                                                   Date          calcTime,
412                                                   VelocityFrame referenceFrame)
413        throws CoordinateConversionException
414      {
415        LinearVelocity velocity = new LinearVelocity();
416        
417        if (referenceFrame.equals(UNKNOWN) ||
418            referenceFrame.equals(COSMIC_MICROWAVE_BACKGROUND))
419          throw new IllegalArgumentException("The referenceFrame '" + referenceFrame +
420            "' is one of two unsupported values (see javadoc).");
421        
422        //If ref frame is topocentric, answer is zero, so do no more.
423        if (!referenceFrame.equals(TOPOCENTRIC))
424        {
425          LocalSiderealTime lst = getLocalSiderealTime().setSolarTime(calcTime);
426          
427          //Make sure position is in RA/Dec
428          SkyPosition j2000Position = skyPosition;
429          if (!skyPosition.getCoordinateSystem().equals(EQUATORIAL))
430            j2000Position = skyPosition.toPosition(EQUATORIAL, Epoch.J2000, this, lst);
431    
432          //Keep two versions of position on hand, both in radians, one in J2000.0,
433          //one precessed to time of calc.
434          AngleDR j2000Pos =
435            new AngleDR(j2000Position.getLongitude(calcTime).toUnits(RADIAN).doubleValue(),
436                        j2000Position.getLatitude(calcTime).toUnits(RADIAN).doubleValue());
437        
438          double  calcEpoch    = new JulianDate(calcTime).toEpoch().doubleValue();
439          AngleDR precessedPos = SLALIB.Preces("FK5", 2000.0, calcEpoch, j2000Pos);
440          
441          //Start with contribution from rotation of earth
442          double obsLatRadians = latitude.toAngle().toUnits(RADIAN).doubleValue();
443          double lstRadians    = lst.toTimeOfDay().toAngle().toUnits(RADIAN).doubleValue();
444          
445          BigDecimal earthRotation =
446            BigDecimal.valueOf(SLALIB.Drverot(obsLatRadians, precessedPos, lstRadians));
447        
448          //All frames covered below get contribution from earth rotation
449          velocity.set(earthRotation, KILOMETERS_PER_SECOND);
450    
451          final boolean HELIO = true;
452          final boolean BARY  = false;
453          
454          switch (referenceFrame)
455          {
456            case GEOCENTRIC:
457              break; //nothing more to add to earth rotation
458              
459            case BARYCENTRIC:
460              velocity.add(calcRevolutionVelocityToward(precessedPos, calcTime, BARY));
461              break;
462    
463            default:
464              //Remaining frames all get contribution from motion of earth relative to sun
465              velocity.add(calcRevolutionVelocityToward(precessedPos, calcTime, HELIO));
466              
467              double kms = 0.0;
468              
469              switch (referenceFrame)
470              {
471                case HELIOCENTRIC:
472                  break; //nothing more to add
473    
474                //All of the SLALIB methods used later in this block have
475                //precomputed  velocities using epoch=2000.0, so we feed
476                //them the unprecessed positions.
477                //TODO: should we write our own methods in order to handle precession?
478                    
479                case LSR_KINEMATIC:
480                  kms = SLALIB.Drvlsrk(j2000Pos);
481                  break;
482                    
483                case LSR_DYNAMIC:
484                  kms = SLALIB.Drvlsrd(j2000Pos);
485                  break;
486    
487                case GALACTOCENTRIC:
488                  kms = SLALIB.Drvlsrd(j2000Pos) + SLALIB.Drvgalc(j2000Pos);
489                  break;
490                  
491                case LOCAL_GROUP:
492                  kms = SLALIB.Drvlsrd(j2000Pos) + SLALIB.Drvlg(j2000Pos);
493                  break;
494                  
495                case TOPOCENTRIC:
496                case GEOCENTRIC:
497                case BARYCENTRIC:
498                case COSMIC_MICROWAVE_BACKGROUND:
499                case UNKNOWN:
500                  throw new RuntimeException(
501                    "PROGRAMMER ERROR: should not have encountered refrenceFrame" +
502                    referenceFrame);
503                  
504                default:
505                  throw new RuntimeException(
506                    "PROGRAMMER ERROR: unrecognized refrenceFrame" + referenceFrame);
507              }
508              
509              velocity.add(new LinearVelocity(BigDecimal.valueOf(kms), KILOMETERS_PER_SECOND));
510              
511              break;
512           } //end outer switch
513        } //end if-TOPOCENTRIC
514        
515        return velocity;
516      }
517      
518      private static final LinearVelocityUnits AU_PER_SEC =
519        LinearVelocityUnits.from(DistanceUnits.ASTRONOMICAL_UNIT, TimeUnits.SECOND);
520      
521      /**
522       * Calculates the velocity in the direction of the given sky position
523       * due to the revolution of the earth around the sun or barycenter.
524       * <p>
525       * This method was originally intended for the private use of this class.
526       * It was made public so that a test program could display the value.</p>
527       */
528      public static LinearVelocity
529        calcRevolutionVelocityToward(AngleDR positionOfDate, Date calcDate, boolean useHelio)
530      {
531        //For now we use only J2000; later we should be able to take arbitrary epoch
532        double deqx = 0.0;
533        
534        //The EVP method does not use UTC; need to adjust the date
535        double evpDate = getEvpDate(calcDate);
536        
537        //Output from EVP
538        double[] barycentricPosition  = new double[3];
539        double[] barycentricVelocity  = new double[3];
540        double[] heliocentricPosition = new double[3];
541        double[] heliocentricVelocity = new double[3];
542        
543        //EVP fills the double arrays
544        SLALIB.Evp(evpDate, deqx,  barycentricVelocity,  barycentricPosition,
545                                  heliocentricVelocity, heliocentricPosition);
546        
547        //Choose either the heliocentric or barycentric velocity vector
548        double[] earthVelocityVector = useHelio ? heliocentricVelocity : barycentricVelocity;
549        
550        //We want the component of the velocity in the direction of an RA/Dec point.
551        //First convert from RA / Dec to x,y,z.
552        double[] skyPositionVector = SLALIB.Dcs2c(positionOfDate);
553        
554        //Get the component of velocity in direction of RA/Dec point.
555        //Take dot-product over size of RA / Dec vector (which s/b 1.0).
556        //We use flipped signs for the earth's velocity vector.  This is
557        //because we want a positive value to represent motion of one body
558        //AWAY from another. 
559        double dotProduct = 0.0;
560        for (int i=0; i < 3; i++)
561          dotProduct += (-earthVelocityVector[i] * skyPositionVector[i]);
562    
563        //This should be unnecessary, since skyPositionVector s/b a unit vector,
564        //but we play it cautiously.
565        double directionVectorMagnitude = 0.0;
566        for (int i=0; i < 3; i++)
567          directionVectorMagnitude += (skyPositionVector[i] * skyPositionVector[i]);
568        directionVectorMagnitude = Math.sqrt(directionVectorMagnitude);
569    
570        double directionalVelocity = dotProduct / directionVectorMagnitude;
571        
572        return new LinearVelocity(BigDecimal.valueOf(directionalVelocity), AU_PER_SEC);
573      }
574      
575      /** SLALIB's EVP method wants TDB, not UTC. */
576      private static double getEvpDate(Date javaDate)
577      {
578        //Start w/ Julian Date
579        JulianDate jd = new JulianDate(javaDate);
580        
581        //Get the MJD for use w/ SLALIB
582        double utcMjd = jd.modifiedValue().doubleValue();
583        
584        //Find out how far TT is ahead of UTC
585        double ttMinusUtc = SLALIB.Dtt(utcMjd);
586        
587        //Move the Julian Date by TT - UTC
588        jd.add(new TimeDuration(Double.toString(ttMinusUtc), TimeUnits.SECOND));
589    
590        //Report the adjusted MJD
591        return jd.modifiedValue().doubleValue();
592      }
593      
594      /**
595       * Returns the current local sidereal time at this location.
596       * The returned object is newly created and is not referenced
597       * by this position, so clients are free to manipulate it.
598       * 
599       * @return the current local sidereal time at this location.
600       * 
601       * @since 2008-09-04
602       */
603      public LocalSiderealTime getLocalSiderealTime()
604      {
605        return new LocalSiderealTime(null, longitude, timeZone);
606      }
607      
608      /**
609       * Returns the parallactic angle to the given sky coordinates from this
610       * location at the specified time.
611       * <p>
612       * Some links:</p>
613       * <ul>
614       *   <li><a href="http://www.petermeadows.com/html/parallactic.html">Parallactic Angle</a></li>
615       *   <li><a href="http://www.gb.nrao.edu/GBT/memos/memo52.ps">GBT Memo 52</a></li>
616       * </ul>
617       * 
618       * @param rightAscension
619       *   the right ascension of the sky position for which the parallactic
620       *   angle is desired.
621       *    
622       * @param declination
623       *   the declination of the sky position for which the parallactic
624       *   angle is desired.
625       *   
626       * @param dateTime
627       *   the time for which the parallactic angle is desired.
628       * 
629       * @return
630       *   the parallactic angle to the given sky position at the given time.
631       * 
632       * @since 2008-09-04
633       */
634      public Angle getParallacticAngle(Longitude rightAscension,
635                                       Latitude  declination,
636                                       Date      dateTime)
637      {
638        Longitude hourAngle =
639          getLocalSiderealTime().setSolarTime(dateTime).getHourAngle(rightAscension);
640    
641        //This algorithm was taken from the SLA_LIB PA routine, as found at
642        //http://stsdas.stsci.edu/cgi-bin/gethelp.cgi?pa.src
643        //With the exception of letter case, the variable names were preserved.
644        
645        Angle phi = latitude.toAngle();
646        Angle ha  = hourAngle.toAngle();
647        Angle dec = declination.toAngle();
648        
649        double cp   = phi.cosine();
650        double sqsz = cp * ha.sine();
651        double cqsz = phi.sine() * dec.cosine() - cp * dec.sine() * ha.cosine();
652        
653        if (sqsz == 0.0 && cqsz == 0.0)
654          cqsz = 1.0;
655    
656        double slPA = Math.atan2(sqsz, cqsz);
657        
658        return new Angle(BigDecimal.valueOf(slPA), ArcUnits.RADIAN);
659      }
660      
661      //============================================================================
662      // 
663      //============================================================================
664      
665      @Override
666      public String toString()
667      {
668        StringBuilder buff = new StringBuilder();
669        
670        buff.append(longitude).append("+/-").append(longitudeUncertainty);
671        buff.append(',');
672        buff.append(latitude).append("+/-").append(latitudeUncertainty);
673        buff.append(',');
674        buff.append(distance).append("+/-").append(distanceUncertainty);
675        buff.append(',');
676        buff.append(timeZone.toString());
677        
678        return buff.toString();
679      }
680      
681      /**
682       *  Returns a copy of this position.
683       *  <p>
684       *  If anything goes wrong during the cloning procedure,
685       *  a {@code RuntimeException} will be thrown.</p>
686       */
687      @Override
688      public EarthPosition clone()
689      {
690        EarthPosition clone = null;
691    
692        try
693        {
694          //This line takes care of the primitive & immutable fields properly
695          clone = (EarthPosition)super.clone();
696          
697          clone.longitude = this.longitude.clone();
698          clone.latitude  = this.latitude.clone();
699          clone.distance  = this.distance.clone();
700          
701          clone.longitudeUncertainty = this.longitudeUncertainty.clone();
702          clone.latitudeUncertainty  = this.latitudeUncertainty.clone();
703          clone.distanceUncertainty  = this.distanceUncertainty.clone();
704          
705          clone.timeZone = (TimeZone)this.timeZone.clone();
706        }
707        catch (Exception ex)
708        {
709          throw new RuntimeException(ex);
710        }
711        
712        return clone;
713      }
714      
715      /**
716       * Returns <i>true</i> if {@code o} is equal to this position.
717       * The time zone attribute is not taken into account by this method.
718       */
719      @Override
720      public boolean equals(Object o)
721      {
722        //Quick exit if o is this
723        if (o == this)
724          return true;
725        
726        //Quick exit if super class determines not equal
727        if (!super.equals(o))
728          return false;
729        
730        //Super class determined o is non-null & of same class
731        EarthPosition other = (EarthPosition)o;
732    
733        return other.longitude.equals(this.longitude) &&
734               other.latitude.equals (this.latitude ) &&
735               other.distance.equals (this.distance ) &&
736    
737               other.longitudeUncertainty.equals(this.longitudeUncertainty) &&
738               other.latitudeUncertainty.equals (this.latitudeUncertainty ) &&
739               other.distanceUncertainty.equals (this.distanceUncertainty );
740      }
741    
742      /** Returns a hash code value for this position. */
743      @Override
744      public int hashCode()
745      {
746        //Taken from the Effective Java book by Joshua Bloch.
747        //The constants 17 & 37 are arbitrary & carry no meaning.
748        
749        //You MUST keep this method in synch with equals()
750        
751        int result = super.hashCode();
752    
753        result = 37 * result + longitude.hashCode();
754        result = 37 * result + latitude.hashCode();
755        result = 37 * result + distance.hashCode();
756    
757        result = 37 * result + longitudeUncertainty.hashCode();
758        result = 37 * result + latitudeUncertainty.hashCode();
759        result = 37 * result + distanceUncertainty.hashCode();
760        
761        //Intentionally left out time zone
762        
763        return result;
764      }
765      
766      //============================================================================
767      // 
768      //============================================================================
769      /*
770      public static void main(String... args) throws Exception
771      {
772        Longitude ra = new Longitude("90.0");
773        Latitude dec = new Latitude("75.0");
774        
775        EarthPosition ep = new EarthPosition(Longitude.parse("-107d 37' 03.819\""), 
776                                             Latitude.parse("34d 04' 43.497\""),
777                                             TimeZone.getTimeZone("America/Denver"));
778        
779        LocalSiderealTime lst = ep.getLocalSiderealTime();
780        
781        java.util.Calendar cal = java.util.Calendar.getInstance();
782        cal.clear();
783        cal.set(2008, 5, 21, 13, 10);
784        
785        for (int h=0; h <= 24; h++)
786        {
787          Date date = cal.getTime();
788          lst.setSolarTime(date);
789          Angle pa = ep.getParallacticAngle(ra, dec, date);
790          System.out.println(lst.getHourAngle(ra).convertTo(ArcUnits.HOUR).toString(1, 1) +
791                             ", " + pa.convertTo(ArcUnits.DEGREE).toString(1, 1));
792          cal.add(java.util.Calendar.HOUR_OF_DAY, 1);
793        }
794      }
795      */
796    }