001 package edu.nrao.sss.measure; 002 003 import java.math.BigDecimal; 004 005 import edu.nrao.sss.util.FormatString; 006 007 /** 008 * An interval from one line of longitude to another. 009 * <p> 010 * This interval is half-open; it includes the starting point 011 * but does <i>not</i> include the ending point.</p> 012 * <p> 013 * The motivation for this class is the desire to test, for example, 014 * if a given point is between a longitude of 355 degrees and 015 * 10 degrees, which is different that testing if that point is 016 * between 10 degrees and 355 degrees. Because there is a discontinuity 017 * where once we reach the maximum longitude we proceed to the minimum, 018 * we cannot use the typical range concept that allows us to test 019 * whether a point is at or above a minimum and below a maximum, as 020 * the example above shows. Instead, we need to know if a point 021 * is on or after a <em>starting</em> point and before an 022 * <em>ending</em>, where the starting point could be numerically 023 * greater than the ending point. The {@link TimeInterval} class 024 * encapsulates the same concept for the time of day.</p> 025 * <p> 026 * <b>Version Info:</b> 027 * <table style="margin-left:2em"> 028 * <tr><td>$Revision: 1707 $</td></tr> 029 * <tr><td>$Date: 2008-11-14 10:23:59 -0700 (Fri, 14 Nov 2008) $</td></tr> 030 * <tr><td>$Author: dharland $ (last person to modify)</td></tr> 031 * </table></p> 032 * 033 * @author David M. Harland 034 * @since 2007-06-13 035 */ 036 public class LongitudeInterval 037 implements Cloneable 038 { 039 private Longitude start; 040 private Longitude end; 041 042 /** Creates a new interval that encompasses the entire circle. */ 043 public LongitudeInterval() 044 { 045 start = new Longitude(); 046 end = new Longitude(); 047 048 end.setToFullCircle(); 049 } 050 051 /** 052 * Creates a new interval using the given longitudes. 053 * <p> 054 * The description of the {@link #set(Longitude, Longitude)} method applies 055 * to this constructor as well.</p> 056 * 057 * @param from the starting point of this interval. The starting 058 * point is included in the interval. 059 * 060 * @param to the ending point of this interval. The ending 061 * point is <i>not</i> included in the interval. 062 */ 063 public LongitudeInterval(Longitude from, Longitude to) 064 { 065 setInterval(from, to); 066 } 067 068 /** 069 * Resets this interval to its default state. 070 * <p> 071 * A reset interval has the same state as one newly created by 072 * the {@link #LongitudeInterval() no-argument constructor}. 073 * Specifically, it is an interval that encompasses a full circle.</p> 074 */ 075 public void reset() 076 { 077 start.reset(); 078 end.setToFullCircle(); 079 } 080 081 /** 082 * Sets the starting and ending points of this interval. 083 * <p> 084 * It is acceptable for the starting longitude to be 085 * later than the ending longitude. The interval still 086 * starts at {@code from}; it is just that, in order to 087 * reach {@code to}, it must cross the "top" of the circle 088 * (0, or 360, degrees).</p> 089 * <p> 090 * If either parameter is <i>null</i>, an 091 * {@code IllegalArgumentException} will be thrown.</p> 092 * <p> 093 * This class will maintain references to {@code from} and 094 * {@code to}; it will not make copies. This means that 095 * any changes made by clients to the parameter objects after 096 * calling this method will be reflected in this object.</p> 097 * 098 * @param from the starting point of this interval. The starting 099 * point is included in the interval. 100 * 101 * @param to the ending point of this interval. The ending 102 * point is <i>not</i> included in the interval. 103 */ 104 public void set(Longitude from, Longitude to) 105 { 106 setInterval(from, to); 107 } 108 109 /** Called from constructor & public set method. */ 110 private void setInterval(Longitude startLon, Longitude endLon) 111 { 112 if ((startLon == null) || (endLon == null)) 113 throw new IllegalArgumentException( 114 "Cannot configure LongitudeInterval with null Longitude."); 115 116 //Since we don't care about the NUMERICAL ordering of these two 117 //elements, we do NOT need to make clones. Contrast this to 118 //what we needed to do in the TimeInterval class. 119 start = startLon; 120 end = endLon; 121 } 122 123 /** 124 * Sets the endpoints of this interval based on {@code intervalText}. 125 * If {@code intervalText} is <i>null</i> or <tt>""</tt> (the empty string), 126 * the {@link #reset()} method is called. Otherwise, the parsing is 127 * delegated to {@link #parse(String)}. See that method 128 * for details related to parsing. 129 */ 130 public void set(String intervalText) 131 { 132 if (intervalText == null || intervalText.equals("")) 133 { 134 reset(); 135 } 136 else 137 { 138 try { 139 parseInterval(intervalText, FormatString.ENDPOINT_SEPARATOR); 140 } 141 catch (IllegalArgumentException ex) { 142 //If the parseInterval method threw an exception it never reached 143 //the point of updating this interval, so we don't need to restore 144 //to pre-parsing values. 145 throw ex; 146 } 147 } 148 } 149 150 /** 151 * Exchanges the starting and ending points of this interval. 152 */ 153 public void switchEndpoints() 154 { 155 Longitude temp = start; 156 start = end; 157 end = temp; 158 } 159 160 /** 161 * Creates a new interval whose starting point is this interval's ending 162 * point and whose ending point is this interval's starting point. 163 * 164 * @return a new interval with endpoints opposite to those of this interval. 165 */ 166 public LongitudeInterval toComplement() 167 { 168 LongitudeInterval result = this.clone(); 169 170 result.switchEndpoints(); 171 172 return result; 173 } 174 175 /** 176 * Returns two new intervals that were formed by splitting this one at the 177 * given point. This interval is not altered by this method. 178 * <p> 179 * <b>Scenario One.</b> If this interval contains {@code pointOfSplit}, then 180 * the first interval in the array runs from this interval's starting point 181 * to {@code pointOfSplit}, and the second interval runs from 182 * {@code pointOfSplit} to this interval's ending point.</p> 183 * <p> 184 * <b>Scenario Two.</b> If this interval does not contain 185 * {@code pointOfSplit}, then the first interval in the array will be equal 186 * to this one, and the second interval will be a zero length interval whose 187 * starting and ending endpoints are both {@code pointOfSplit}.</p> 188 * 189 * @param pointOfSplit the point at which to split this interval in two. 190 * 191 * @return an array of two intervals whose combined length equals this 192 * interval's length. 193 */ 194 public LongitudeInterval[] split(Longitude pointOfSplit) 195 { 196 LongitudeInterval[] result = new LongitudeInterval[2]; 197 198 result[0] = new LongitudeInterval(); 199 result[1] = new LongitudeInterval(); 200 201 if (this.contains(pointOfSplit)) 202 { 203 result[0].set(this.start, pointOfSplit.clone()); 204 result[1].set(pointOfSplit.clone(), this.end); 205 206 //Special logic for splitting at the top of the circle 207 BigDecimal splitVal = pointOfSplit.getValue(); 208 if (splitVal.compareTo(BigDecimal.ZERO) == 0) 209 { 210 result[0].end.setToFullCircle(); 211 } 212 else if (splitVal.compareTo(pointOfSplit.getUnits().toFullCircle()) == 0) 213 { 214 result[1].start.set("0.0", result[1].start.getUnits()); 215 } 216 } 217 else //can't use point to split, as its not in this interval 218 { 219 result[0].set(this.start, this.end); 220 result[1].set(pointOfSplit.clone(), pointOfSplit.clone()); 221 } 222 223 return result; 224 } 225 226 //============================================================================ 227 // QUERIES 228 //============================================================================ 229 230 /** 231 * Returns this interval's starting longitude. 232 * Note that the starting longitude is included in this interval. 233 * <p> 234 * The returned longitude, which is guaranteed to be non-null, 235 * is the actual longitude held by this interval, 236 * so changes made to it will be reflected in this object.</p> 237 * 238 * @return this interval's starting longitude. 239 * 240 * @see #set(Longitude, Longitude) 241 */ 242 public Longitude getStart() 243 { 244 return start; 245 } 246 247 /** 248 * Returns this interval's ending longitude. 249 * Note that the end longitude is <i>not</i> included in this interval. 250 * <p> 251 * The returned longitude, which is guaranteed to be non-null, 252 * is the actual longitude held by this interval, 253 * so changes made to it will be reflected in this object.</p> 254 * 255 * @return this interval's ending longitude. 256 * 257 * @see #set(Longitude, Longitude) 258 */ 259 public Longitude getEnd() 260 { 261 return end; 262 } 263 264 /** 265 * Returns the longitude that is midway between the endpoints of this 266 * interval. 267 * <p> 268 * Understand that while the returned value will always be between 269 * the endpoints (or coincident with them, if they are identical), 270 * the center may be numerically smaller than the starting point 271 * of this interval. For example, if this interval starts at 272 * 340 degrees and ends at 60 degrees, the length of this interval 273 * is 80 degrees, and the center is at 20 degrees, which is a smaller 274 * value than that of -- but is not "before" -- the starting point.</p> 275 * 276 * @return the center of this interval. 277 */ 278 public Longitude getCenter() 279 { 280 Longitude center = start.clone(); 281 282 Angle halfLength = getLength().divideBy("2.0"); 283 284 return center.add(halfLength); 285 } 286 287 /** 288 * Returns the length of this this interval. 289 * @return the length of this this interval. 290 */ 291 public Angle getLength() 292 { 293 Angle endPoint = end.getAngle().clone(); 294 Angle startPoint = start.getAngle(); 295 296 if (endPoint.compareTo(startPoint) < 0) 297 endPoint.add(endPoint.getUnits().toFullCircle()); 298 299 return endPoint.subtract(startPoint); 300 } 301 302 /** 303 * Returns <i>true</i> if {@code longitude} is contained in this interval. 304 * <p> 305 * Note that this interval is half-open; it includes the starting point, 306 * but not the ending point.</p> 307 * 308 * @param longitude the longitude to be tested for containment. 309 * 310 * @return <i>true</i> if {@code longitude} is contained in this interval. 311 */ 312 public boolean contains(Longitude longitude) 313 { 314 boolean result; 315 316 int startComparedToEnd = start.compareTo(end); 317 318 //"Normal" order: end > start 319 if (startComparedToEnd < 0) 320 { 321 result = (start.compareTo(longitude) <= 0) && (longitude.compareTo(end) < 0); 322 } 323 //Crossing top of circle: start > end 324 else if (startComparedToEnd > 0) 325 { 326 result = (start.compareTo(longitude) <= 0) || (longitude.compareTo(end) < 0); 327 } 328 //A half-open interval with identical endpoints contains nothing 329 else //start==end 330 { 331 result = false; 332 } 333 334 return result; 335 } 336 337 //See TimeOfDayInterval for more methods we could add. 338 339 //============================================================================ 340 // TRANSLATION TO & FROM TEXT 341 //============================================================================ 342 343 /** 344 * Returns a string representation of this interval. 345 * The separator of the endpoints is {@link FormatString#ENDPOINT_SEPARATOR}. 346 * 347 * @return a string representation of this interval. 348 */ 349 public String toString() 350 { 351 return toString(FormatString.ENDPOINT_SEPARATOR); 352 } 353 354 /** 355 * Returns a string representation of this interval. 356 * See the {@link Longitude#toString() toString} method in 357 * {@code Longitude} for information about how the endpoints 358 * are formatted. The {@code endPointSeparator} is used to 359 * separate the two endpoints in the returned string. 360 * 361 * @param endPointSeparator text that separates one endpoint from another 362 * in the returned string. Using a <i>null</i> 363 * or empty-string value here is a bad idea. 364 * 365 * @return a string representation of this interval. 366 */ 367 public String toString(String endPointSeparator) 368 { 369 StringBuilder buff = new StringBuilder(); 370 371 buff.append(start.toString()); 372 buff.append(endPointSeparator); 373 buff.append(end.toString()); 374 375 return buff.toString(); 376 } 377 378 /** 379 * Creates a new longitude interval by parsing {@code intervalText}. 380 * <p> 381 * This is a convenience method that is equivalent to:<pre> 382 * parse(intervalText, FormatString.ENDPOINT_SEPARATOR);</pre> 383 * That is, the string separating the two endpoints is assumed to 384 * be {@link FormatString#ENDPOINT_SEPARATOR}. 385 * 386 * @param intervalText 387 * the text to be parsed and converted into a longitude 388 * interval. If this value is <i>null</i> or <tt>""</tt> 389 * (the empty string), a new interval of length zero is returned. 390 * 391 * @return 392 * a new time interval based on {@code intervalText}.\ 393 * If {@code intervalText} is <i>null</i> or <tt>""</tt> 394 * (the empty string), a new interval of length zero is returned. 395 * 396 * @throws IllegalArgumentException if {@code intervalText} cannot be parsed. 397 */ 398 public static LongitudeInterval parse(String intervalText) 399 { 400 return LongitudeInterval.parse(intervalText, FormatString.ENDPOINT_SEPARATOR); 401 } 402 403 /** 404 * Creates a new longitude interval by parsing {@code intervalText}. 405 * The general form of {@code intervalText} is 406 * <tt>StartTimeOfDaySeparatorEndTimeOfDay</tt>, 407 * with the particulars of the longitude format described by 408 * the {@link Longitude#parse(String)} method of {@code Longitude}. 409 * 410 * @param intervalText 411 * the text to be parsed and converted into a longitude interval. 412 * If this value is <i>null</i> or <tt>""</tt> (the empty string), 413 * a new interval of length zero is returned. 414 * 415 * @param endPointSeparator 416 * the text that separates the starting longitude 417 * from the ending longitude in {@code intervalText}. 418 * 419 * @return 420 * a new longitude interval based on {@code intervalText}. 421 * 422 * @throws IllegalArgumentException 423 * if the combination {@code intervalText} and {@code endPointSeparator} 424 * cannot be parsed successfully. 425 */ 426 public static LongitudeInterval parse(String intervalText, 427 String endPointSeparator) 428 { 429 LongitudeInterval newInterval = new LongitudeInterval(); 430 431 //null & "" are permissible 432 if ((intervalText != null) && !intervalText.equals("")) 433 { 434 try { 435 newInterval.parseInterval(intervalText, endPointSeparator); 436 } 437 catch (IllegalArgumentException ex) { 438 throw ex; 439 } 440 } 441 442 return newInterval; 443 } 444 445 /** Does the actual parsing. */ 446 private void parseInterval(String intervalText, String endPointSeparator) 447 { 448 //Find the endpoints of the interval 449 String[] endPoints = intervalText.split(endPointSeparator, -1); 450 451 if (endPoints.length == 2) 452 { 453 setInterval(Longitude.parse(endPoints[0]), 454 Longitude.parse(endPoints[1])); 455 } 456 else //bad # of endpoints 457 { 458 throw new IllegalArgumentException("Could not parse " + intervalText + 459 " using " + endPointSeparator + ". Found + endPoints.length +" + 460 " endpoints. There should be 2."); 461 } 462 } 463 464 //============================================================================ 465 // 466 //============================================================================ 467 468 /** 469 * Returns a copy of this interval. 470 * <p> 471 * If anything goes wrong during the cloning procedure, 472 * a {@code RuntimeException} will be thrown.</p> 473 */ 474 @Override 475 public LongitudeInterval clone() 476 { 477 LongitudeInterval clone = null; 478 479 try 480 { 481 clone = (LongitudeInterval)super.clone(); 482 483 clone.start = this.start.clone(); 484 clone.end = this.end.clone(); 485 } 486 catch (Exception ex) 487 { 488 throw new RuntimeException(ex); 489 } 490 491 return clone; 492 } 493 494 /** Returns <i>true</i> if {@code o} is equal to this interval. */ 495 @Override 496 public boolean equals(Object o) 497 { 498 //Quick exit if o is this 499 if (o == this) 500 return true; 501 502 //Quick exit if o is null 503 if (o == null) 504 return false; 505 506 //Quick exit if classes are different 507 if (!o.getClass().equals(this.getClass())) 508 return false; 509 510 LongitudeInterval otherTime = (LongitudeInterval)o; 511 512 return otherTime.start.equals(this.start) && 513 otherTime.end.equals(this.end); 514 } 515 516 /** Returns a hash code value for this interval. */ 517 @Override 518 public int hashCode() 519 { 520 //Taken from the Effective Java book by Joshua Bloch. 521 //The constants 17 & 37 are arbitrary & carry no meaning. 522 int result = 17; 523 524 result = 37 * result + start.hashCode(); 525 result = 37 * result + end.hashCode(); 526 527 return result; 528 } 529 }