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 }