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