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