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 }