001 package edu.nrao.sss.measure; 002 003 import java.math.BigDecimal; 004 import java.util.ArrayList; 005 import java.util.List; 006 007 import javax.xml.bind.annotation.XmlElement; 008 009 import edu.nrao.sss.util.FormatString; 010 011 /** 012 * A range of frequencies. The range is a <i>closed</i> interval; that is, 013 * both endpoints are themselves part of the range. 014 * <p> 015 * TODO: <i>Discuss if that's a bad idea. It's often nice to 016 * have half-open intervals so that one can create multiple 017 * ranges that fit together perfectly w/ no overlap. On the other hand, 018 * if the normal usage pattern is not to set up these kinds of 019 * ranges, the closed interval can be more intuitive.</p> 020 * <p> 021 * <u>Jan 2007 Note:</u> Now that we've added more methods to this class, 022 * the closed-interval choice seems less wise; half-open intervals 023 * lead to better algorithms. Eg, if you want the gap between 024 * the lower range [l<sub>1</sub>-h<sub>1</sub>] and the higher range 025 * [l<sub>2</sub>-h<sub>2</sub>], the natural gap range is 026 * [h<sub>1</sub>-l<sub>2</sub>]. If the range were half-open, we now 027 * have 3 contiguous, non-overlapping ranges. By keeping the range 028 * closed, we must either have overlap on the endpoints, or say that the 029 * gap-range is [(h<sub>1</sub>+epsilon)-(l<sub>2</sub>-epsilon)]. 030 * The main reason for having a closed interval was to allow someone to 031 * say "this receiver has a range from 40GHz to 50GHz", where "to" is 032 * usually intended to be "through".</i></p> 033 * <p> 034 * <b>Version Info:</b> 035 * <table style="margin-left:2em"> 036 * <tr><td>$Revision: 2295 $</td></tr> 037 * <tr><td>$Date: 2009-05-12 12:59:07 -0600 (Tue, 12 May 2009) $</td></tr> 038 * <tr><td>$Author: dharland $</td></tr> 039 * </table></p> 040 * 041 * @author David M. Harland 042 * @since 2006-03-31 043 */ 044 public class FrequencyRange 045 implements Cloneable, Comparable<FrequencyRange> 046 { 047 private static final FrequencyUnits DEFAULT_UNITS = Frequency.DEFAULT_UNITS; 048 049 private static final Frequency DEFAULT_LOW_FREQ = 050 new Frequency("0.0", DEFAULT_UNITS); 051 052 private static final Frequency DEFAULT_HIGH_FREQ = 053 new Frequency("infinity", DEFAULT_UNITS); 054 055 @XmlElement private Frequency lowFrequency; 056 @XmlElement private Frequency highFrequency; 057 058 //=========================================================================== 059 // CONSTRUCTORS & INITALIZERS 060 //=========================================================================== 061 062 /** 063 * Creates a new instance that contains all positive frequencies. 064 * The default unit of frequency used is GHz. 065 */ 066 public FrequencyRange() 067 { 068 lowFrequency = DEFAULT_LOW_FREQ.clone(); 069 highFrequency = DEFAULT_HIGH_FREQ.clone(); 070 } 071 072 /** 073 * Creates a new instance with the given endpoints. 074 * <p> 075 * This method will set the lower value of its range to the lesser of the 076 * two parameter values. If either parameter is <i>null</i>, it will be 077 * interpreted as a signal to create a new default frequency.</p> 078 * <p> 079 * Note that this method makes copies of the parameters; it 080 * does not maintain a reference to either parameter. This 081 * is done in order to maintain the integrity of the relationship 082 * between the starting and ending points of this interval.</p> 083 * 084 * @param frequency1 one endpoint of this range. 085 * @param frequency2 the other endpoint of this range. 086 */ 087 public FrequencyRange(Frequency frequency1, Frequency frequency2) 088 { 089 set(frequency1, frequency2); 090 } 091 092 /** 093 * Creates a new frequency range with the given center and an extremely 094 * narrow width. 095 * 096 * @param center 097 * the center of this frequency range. 098 * 099 * @since 2008-10-30 100 */ 101 public FrequencyRange(Frequency center) 102 { 103 //Start with a half-width of one "ulp" 104 BigDecimal halfWidth = 105 BigDecimal.valueOf(Math.ulp(center.getValue().doubleValue())); 106 107 //Let's try a width of 100 ULPs, pending outcome of unit tests 108 Frequency width = new Frequency(halfWidth.multiply(new BigDecimal("100")), 109 center.getUnits()); 110 //Instantiate inst vars 111 lowFrequency = new Frequency(); 112 highFrequency = new Frequency(); 113 114 setCenterAndWidth(center, width); 115 } 116 117 /** 118 * Resets this range so that it contains all positive frequencies. 119 * The unit of frequency is also reset to GHz. 120 */ 121 public void reset() 122 { 123 lowFrequency.set(DEFAULT_LOW_FREQ.getValue(), DEFAULT_UNITS); 124 highFrequency.set(DEFAULT_HIGH_FREQ.getValue(), DEFAULT_UNITS); 125 } 126 127 private void makeThisRangeInfinite(FrequencyUnits units) 128 { 129 if (units == null) 130 units = DEFAULT_UNITS; 131 132 lowFrequency.set("0.0", units); 133 highFrequency.set("+infinity", units); 134 } 135 136 //=========================================================================== 137 // GETTING & SETTING THE PROPERTIES 138 //=========================================================================== 139 140 /** 141 * Sets the frequencies of this range. 142 * <p> 143 * This method will set the lower value of its range to the lesser of the 144 * two parameter values. If either parameter is <i>null</i>, it will be 145 * interpreted as a signal to create a new default frequency.</p> 146 * <p> 147 * Note that this method makes copies of the parameters; it 148 * does not maintain a reference to either parameter. This 149 * is done in order to maintain the integrity of the relationship 150 * between the starting and ending points of this interval.</p> 151 * 152 * @param frequency1 one endpoint of this range. 153 * @param frequency2 the other endpoint of this range. 154 */ 155 public final void set(Frequency frequency1, Frequency frequency2) 156 { 157 //Clone parameters, or create new if null 158 Frequency f1 = (frequency1 == null) ? new Frequency() : frequency1.clone(); 159 Frequency f2 = (frequency2 == null) ? new Frequency() : frequency2.clone(); 160 161 //Save references to the clones (or new freqs) 162 if (f1.compareTo(f2) <= 0) 163 { 164 lowFrequency = f1; 165 highFrequency = f2; 166 } 167 else 168 { 169 lowFrequency = f2; 170 highFrequency = f1; 171 } 172 } 173 174 /** 175 * Sets the endpoints of this range based on {@code rangeText}. 176 * See {@link #parse(String)} for the expected format of 177 * {@code rangeText}. 178 * <p> 179 * If the parsing fails, this range will be kept in its current 180 * state.</p> 181 * 182 * @param rangeText 183 * a string that will be converted into a frequency range. 184 * 185 * @throws IllegalArgumentException 186 * if {@code rangeText} is not in the expected form. 187 * 188 * @since 2008-10-01 189 */ 190 public void set(String rangeText) 191 { 192 if (rangeText == null || rangeText.equals("")) 193 { 194 this.reset(); 195 } 196 else 197 { 198 try { 199 this.parseRange(rangeText, FormatString.ENDPOINT_SEPARATOR); 200 } 201 catch (IllegalArgumentException ex) { 202 //If the parseRange method threw an exception it never reached 203 //the point of updating this range, so we don't need to restore 204 //to pre-parsing values. 205 throw ex; 206 } 207 } 208 } 209 210 /** 211 * Returns the high endpoint of this range. 212 * <p> 213 * Note that the value returned is a copy of the one held 214 * internally by this range. This means that any changes 215 * made by a client to the returned frequency will 216 * <i>not</i> be reflected in this range.</p> 217 * 218 * @return a copy of the high endpoint of this range. 219 */ 220 public Frequency getHighFrequency() 221 { 222 return highFrequency.clone(); 223 } 224 225 /** 226 * Returns the low endpoint of this range. 227 * <p> 228 * Note that the value returned is a copy of the one held 229 * internally by this range. This means that any changes 230 * made by a client to the returned frequency will 231 * <i>not</i> be reflected in this range.</p> 232 * 233 * @return a copy of the low endpoint of this range. 234 */ 235 public Frequency getLowFrequency() 236 { 237 return lowFrequency.clone(); 238 } 239 240 //=========================================================================== 241 // DERIVED QUERIES 242 //=========================================================================== 243 244 /** 245 * Returns the frequency that is midway 246 * between the endpoints of this range. 247 * <p> 248 * The units for the returned frequency will be the same as those 249 * of the high frequency of this range.</p> 250 * 251 * @return the center of this range. 252 */ 253 public Frequency getCenterFrequency() 254 { 255 return getWidth().multiplyBy("0.5").add(lowFrequency); 256 } 257 258 /** 259 * Returns the width of this range. 260 * <p> 261 * The units for the returned frequency will be the same as those 262 * of the high frequency of this range.</p> 263 * 264 * @return the width of this range. 265 */ 266 public Frequency getWidth() 267 { 268 Frequency width; 269 270 //Handling the equality case separately makes it convenient in the 271 //"else" condition for dealing with infinity. 272 if (highFrequency.equals(lowFrequency)) 273 { 274 width = new Frequency("0.0", highFrequency.getUnits()); 275 } 276 else 277 { 278 width = highFrequency.clone(); 279 if (!highFrequency.isInfinite()) 280 width.subtract(lowFrequency); 281 } 282 283 return width; 284 } 285 286 /** 287 * Returns <i>true</i> if this range contains {@code frequency}. 288 * @param frequency the frequency to test for inclusion in this range. 289 * @return <i>true</i> if this range contains {@code frequency}. 290 * If {@code frequency} is <i>null</i>, the return value 291 * will be <i>false</i>. 292 * 293 * @see Frequency#isEndPointOf(FrequencyRange) 294 */ 295 public boolean contains(Frequency frequency) 296 { 297 if (frequency == null) 298 return false; 299 300 //Rem: this is a closed interval (includes both endpoints) 301 return (frequency.compareTo(this.lowFrequency ) >= 0) && 302 (frequency.compareTo(this.highFrequency) <= 0); 303 } 304 305 /** 306 * Returns <i>true</i> if this range contains {@code other}. 307 * <p> 308 * Range A is said to contain range B if A's low frequency 309 * is less than or equal to B's low frequency and A's high frequency 310 * is greater than or each to B's high frequency. 311 * Notice that this means that if A equals B, it also contains B.</p> 312 * 313 * @param other the range to test for inclusion in this range. 314 * @return <i>true</i> if this range contains {@code other}. 315 * If {@code other} is <i>null</i>, the return value 316 * will be <i>false</i>. 317 */ 318 public boolean contains(FrequencyRange other) 319 { 320 if (other == null) 321 return false; 322 323 return ( this.lowFrequency.compareTo(other.lowFrequency ) <= 0) && 324 (this.highFrequency.compareTo(other.highFrequency) >= 0); 325 } 326 327 /** 328 * Returns <i>true</i> if this frequency range overlaps with {@code other}. 329 * Remember that this range is a closed interval, that is, one that 330 * contains both of its endpoints. 331 * <p> 332 * If {@code other} is <i>null</i>, the return value is <i>false</i>.</p> 333 * 334 * @param other another range that may overlap this one. 335 * @return <i>true</i> if this range overlaps with {@code other}. 336 */ 337 public boolean overlaps(FrequencyRange other) 338 { 339 //Quick exit on null 340 if (other == null) 341 return false; 342 343 //This logic is dependent on the fact that we're using a closed 344 //interval (range includes both endpoints). If you switch to 345 //a half-open interval, you must change this code. 346 347 //Easier to test for non-overlap 348 return !((other.highFrequency.compareTo(this.lowFrequency) < 0) || 349 (other.lowFrequency.compareTo(this.highFrequency) > 0)); 350 } 351 352 /** 353 * Returns <i>true</i> if this range overlaps any of the covered ranges 354 * of {@code spectrum}. 355 * 356 * @param spectrum the covered ranges of which are tested for overlap 357 * with this range. 358 * 359 * @return <i>true</i> if this range overlaps any of the covered ranges 360 * of {@code spectrum}. 361 */ 362 public boolean overlaps(FrequencySpectrum spectrum) 363 { 364 for (FrequencyRange coveredRange : spectrum.getCoveredRanges()) 365 if (coveredRange.overlaps(this)) 366 return true; 367 368 return false; 369 } 370 371 /** 372 * Returns <i>true</i> if this frequency range is contiguous with 373 * {@code other}. Two ranges are contiguous if one's low frequency 374 * is equal to the other's high frequency. 375 * If {@code other} is <i>null</i>, the return value is <i>false</i>. 376 * <p> 377 * <i>Note: we are using this definition even though this class 378 * currently defines the range as inclusive of both endpoints. 379 * So, at this time, continguous ranges technically overlap on 380 * their endpoints. If this class is changed to a half-open 381 * interval, the behavior of this method will </i>not<i> change.</i></p> 382 * 383 * @param other another range that may be contiguous this one. 384 * @return <i>true</i> if this range is contiguous with {@code other}. 385 */ 386 public boolean isContiguousWith(FrequencyRange other) 387 { 388 return (other != null) && 389 (this.lowFrequency.equals(other.highFrequency) || 390 this.highFrequency.equals(other.lowFrequency)); 391 } 392 393 /** 394 * Returns a frequency spectrum that represents the overlap of this range 395 * with {@code spectrum}. If {@code spectrum} is <i>null</i>, or has no 396 * covered regions that overlap with this range, the returned spectrum 397 * will have no covered ranges. 398 * 399 * @param spectrum the spectrum to check for covered ranges that overlap 400 * with this range. 401 * 402 * @return a frequency spectrum that represents the overlap of this range 403 * with {@code spectrum}. 404 */ 405 public FrequencySpectrum getOverlapWith(FrequencySpectrum spectrum) 406 { 407 FrequencySpectrum result = new FrequencySpectrum(); 408 409 if (spectrum != null) 410 { 411 for (FrequencyRange coveredRange : spectrum.getCoveredRanges()) 412 { 413 if (this.overlaps(coveredRange)) 414 result.addCoveredRange(this.getOverlapWith(coveredRange)); 415 } 416 } 417 418 return result; 419 } 420 421 /** 422 * Returns a new range that represents the region of overlap between this 423 * range and {@code other}. If there is no overlap, <i>null</i> is returned. 424 * <p> 425 * See {@link #overlaps(FrequencyRange)} for how the closed-interval 426 * definition of this class impacts the results of this method.</p> 427 * 428 * @param other another range that may overlap this one. 429 * @return the overlapping region of this range and {@code other}. 430 */ 431 public FrequencyRange getOverlapWith(FrequencyRange other) 432 { 433 FrequencyRange result = null; 434 435 if (this.overlaps(other)) 436 { 437 //Choose the greater of the lows 438 Frequency low = (this.lowFrequency.compareTo(other.lowFrequency) > 0) ? 439 this.lowFrequency.clone() : other.lowFrequency.clone(); 440 441 //Choose the lesser of the highs 442 Frequency high = (this.highFrequency.compareTo(other.highFrequency) < 0) ? 443 this.highFrequency.clone() : other.highFrequency.clone(); 444 445 result = new FrequencyRange(low, high); 446 } 447 448 return result; 449 } 450 451 /** 452 * Returns a new range that represents the region of frequency space between 453 * this range and {@code other}. If the other range is coincident with, or 454 * overlaps, this range, <i>null</i> is returned. If the other range is 455 * <i>null</i>, <i>null</i> is returned. 456 * <p> 457 * See {@link #overlaps(FrequencyRange)} for how the closed-interval 458 * definition of this class impacts the results of this method.</p> 459 * 460 * @param other another range that might not overlap this one. 461 * @return the frequency gap between this range and {@code other}. 462 */ 463 public FrequencyRange getGapBetween(FrequencyRange other) 464 { 465 FrequencyRange result = null; 466 467 if ((other != null) && !this.overlaps(other)) 468 { 469 //Choose the greater of the lows 470 Frequency high = (this.lowFrequency.compareTo(other.lowFrequency) > 0) ? 471 this.lowFrequency.clone() : other.lowFrequency.clone(); 472 473 //Choose the lesser of the highs 474 Frequency low = (this.highFrequency.compareTo(other.highFrequency) < 0) ? 475 this.highFrequency.clone() : other.highFrequency.clone(); 476 477 result = new FrequencyRange(low, high); 478 } 479 480 return result; 481 } 482 483 /** 484 * Returns zero, one, or two ranges that represent the portions of 485 * <tt>other</tt> that are not also found in this range. Some scenarios: 486 * <ol> 487 * <li> 488 * If <tt>other</tt> is: 489 * <ol type="a"><li><i>null</i>,</li> 490 * <li>equal to this range, or</li> 491 * <li>contained within this range</li></ol> 492 * the returned list will be empty.</li> 493 * <li> 494 * If <tt>other</tt> has no overlap with this range, the returned 495 * list will contain a single element whose range is a copy 496 * of <tt>other</tt>.</li> 497 * <li> 498 * If <tt>other</tt> and this range overlap, but neither contains 499 * the other, the returned list will contain one element.</li> 500 * <li> 501 * If <tt>other</tt> contains this range the returned list 502 * will contain two elements.</li> 503 * </ol> 504 * 505 * @param other 506 * the provider of frequencies that are in the returned range 507 * but not in this one. 508 * 509 * @return 510 * the complement of this range in <tt>other</tt>. 511 */ 512 public List<FrequencyRange> getComplementIn(FrequencyRange other) 513 { 514 List<FrequencyRange> result = new ArrayList<FrequencyRange>(); 515 516 if (other == null || other.equals(this) || this.contains(other)) 517 { 518 ; //result will have zero elements 519 } 520 else if (!other.overlaps(this)) 521 { 522 result.add(other.clone()); 523 } 524 else if (other.contains(this)) 525 { 526 //Freq'Range constructor makes clones, so it's OK to use variables here 527 result.add(new FrequencyRange(other.lowFrequency, this.lowFrequency)); 528 result.add(new FrequencyRange( this.highFrequency, other.highFrequency)); 529 } 530 else //overlap w/out containment 531 { 532 //If other.low >= this.low, use this.high & other.high 533 if (other.lowFrequency.compareTo(this.lowFrequency) >= 0) 534 { 535 result.add(new FrequencyRange(this.highFrequency, other.highFrequency)); 536 } 537 //If other.low < this.low, use other.low and this.low 538 else 539 { 540 result.add(new FrequencyRange(other.lowFrequency, this.lowFrequency)); 541 } 542 } 543 544 return result; 545 } 546 547 /** 548 * Returns <i>true</i> if this frequency range is in its default state, 549 * no matter how it got there. 550 * <p> 551 * A frequency range is in its <i>default state</i> if both the values 552 * and units for the low and high points are the same as those of a range 553 * newly created via 554 * the {@link #FrequencyRange() no-argument constructor}. 555 * A frequency range whose most recent update came via the 556 * {@link #reset() reset} method is also in its default state.</p> 557 * 558 * @return <i>true</i> if this frequency range is in its default state. 559 */ 560 public boolean isInDefaultState() 561 { 562 return lowFrequency.equals(DEFAULT_LOW_FREQ) && 563 highFrequency.equals(DEFAULT_HIGH_FREQ); 564 } 565 566 //=========================================================================== 567 // INDIRECT MANIPULATIONS 568 //=========================================================================== 569 570 /** 571 * Converts both endpoints of this range to the given units. 572 * <p> 573 * After this method is complete both endpoints of this range will have units 574 * of {@code units}, and their <tt>values</tt> will have been converted 575 * accordingly.</p> 576 * 577 * @param newUnits the new units for the endpoints of this range. 578 * If {@code newUnits} 579 * is <i>null</i>, it will be treated as 580 * {@link FrequencyUnits#GIGAHERTZ}. 581 * 582 * @return this range. 583 */ 584 public FrequencyRange convertTo(FrequencyUnits newUnits) 585 { 586 lowFrequency.convertTo(newUnits); 587 highFrequency.convertTo(newUnits); 588 589 return this; 590 } 591 592 /** 593 * Recasts each endpoint separately by calling its 594 * {@link Frequency#normalize() normalize} method. 595 * The one exception to the above process is when the low frequency 596 * is zero and the high is not. In that case the low frequency 597 * will be converted to the same units as the normalized high. 598 * 599 * @return this range, after normalization. 600 */ 601 public FrequencyRange normalize() 602 { 603 highFrequency.normalize(); 604 605 if (lowFrequency.getValue().signum() != 0) 606 lowFrequency.normalize(); 607 else 608 lowFrequency.convertTo(highFrequency.getUnits()); 609 610 return this; 611 } 612 613 /** 614 * Modifies this range to be the union of this range and {@code other}, 615 * but only if the two ranges overlap or are contiguous. 616 * If the two ranges are disjoint, this range is unaltered. 617 * 618 * @param other the range to merge into this range. 619 * 620 * @return this range after merger with {@code other}. 621 */ 622 public FrequencyRange mergeWith(FrequencyRange other) 623 { 624 //Only ranges that overlap or butt up against each other may be merged 625 if (this.overlaps(other) || this.isContiguousWith(other)) 626 { 627 //Update our low frequency if other's low is lower 628 if (other.lowFrequency.compareTo(this.lowFrequency) < 0) 629 { 630 this.lowFrequency.set(other.lowFrequency.getValue(), 631 other.lowFrequency.getUnits()); 632 } 633 //Update our high frequency if other's high is higher 634 if (other.highFrequency.compareTo(this.highFrequency) > 0) 635 { 636 this.highFrequency.set(other.highFrequency.getValue(), 637 other.highFrequency.getUnits()); 638 } 639 } 640 641 return this; 642 } 643 644 /** 645 * Modifies this range to be the intersection of this range and {@code other}. 646 * If this range does not intersect with {@code other}, it is modified 647 * so that the value of both its low and high frequencies is zero, while 648 * leaving their units unchanged. 649 * 650 * @param other the range to intersect with this one. 651 * 652 * @return this range after intersection with {@code other}. 653 */ 654 public FrequencyRange intersectWith(FrequencyRange other) 655 { 656 if (this.overlaps(other)) 657 { 658 //Choose the greater of the lows 659 if (other.lowFrequency.compareTo(this.lowFrequency) > 0) 660 this.lowFrequency.set(other.lowFrequency.getValue(), 661 other.lowFrequency.getUnits()); 662 663 //Choose the lesser of the highs 664 if (other.highFrequency.compareTo(this.highFrequency) < 0) 665 this.highFrequency.set(other.highFrequency.getValue(), 666 other.highFrequency.getUnits()); 667 } 668 else //there is no intersection 669 { 670 this.lowFrequency.setValue( "0.0"); 671 this.highFrequency.setValue("0.0"); 672 } 673 674 return this; 675 } 676 677 /** 678 * Sets the endpoints of this range based on the given center frequency 679 * and bandwidth. The units of both endpoints will be the same as those 680 * of the {@code center} frequency. 681 * <p> 682 * If either parameter is <i>null</i> this method will take no action 683 * and this range will have the same state as immediately before the call.</p> 684 * <p> 685 * If either {@code center} or {@code width} is infinite, this range will 686 * be reset so that its low frequency has a value of zero and its high 687 * frequency has an infinite value.</p> 688 * <p> 689 * If neither of the two conditions above exist, an attempt will be made 690 * to honor the new center and width. However, if doing so would lead to 691 * a negative value for the low endpoint of this range, the width will 692 * be honored, but the center will not. The center would instead be 693 * a frequency equal to one half the width, as the low frequency would be 694 * zero and the high frequency equal to the width.</p> 695 * 696 * @param center the new center frequency of this range. 697 * 698 * @param width the new bandwidth of this range. 699 * 700 * @return this range after changing its endpoints. 701 */ 702 public FrequencyRange setCenterAndWidth(Frequency center, Frequency width) 703 { 704 if ((center == null) || (width == null)) 705 { 706 //do nothing 707 } 708 else if (center.isInfinite() || width.isInfinite()) 709 { 710 makeThisRangeInfinite(center.getUnits()); 711 } 712 else //finite width 713 { 714 Frequency halfWidth = width.clone().divideBy("2.0"); 715 716 //If the incoming parameters would lead to a negative value for the 717 //low endpoint, preserve the desired width, but not the center frequency 718 if (center.compareTo(halfWidth) < 0) 719 { 720 FrequencyUnits units = center.getUnits(); 721 722 lowFrequency.set("0.0", units); 723 highFrequency.set(width.toUnits(units), units); 724 } 725 else //we have enough room to honor request 726 { 727 lowFrequency = center.clone().subtract(halfWidth); 728 highFrequency = center.clone().add(halfWidth); 729 } 730 } 731 732 return this; 733 } 734 735 /** 736 * Moves both the low and high endpoints of this range down by 737 * {@code shiftSize}. 738 * <p> 739 * If {@code shiftSize} is <i>null</i> this method will take no action 740 * and this range will have the same state as immediately before the call.</p> 741 * <p> 742 * If the size of the shift is infinite, this range will 743 * be reset so that its both its low and high frequencies have a value 744 * of zero.</p> 745 * <p> 746 * If neither of the two conditions above exist, an attempt will be made 747 * to honor the shift while leaving the width of this range unchanged. 748 * If, however, doing so would lead to 749 * a negative value for the low endpoint of this range, this range's 750 * width will be preserved and the value of the low frequency will be 751 * set to zero. In otherwords, this method will make the biggest legal 752 * shift that is less than or equal than the requested shift.</p> 753 * 754 * @param shiftSize the amount by which to decrease both the high and 755 * low frequencies of this range. 756 * 757 * @return this range after the frequency shift. 758 */ 759 public FrequencyRange shiftDownBy(Frequency shiftSize) 760 { 761 if (shiftSize == null) 762 { 763 //do nothing 764 } 765 else if (shiftSize.isInfinite()) 766 { 767 lowFrequency.setValue( "0.0"); 768 highFrequency.setValue("0.0"); 769 } 770 else //finite shift 771 { 772 setCenterAndWidth(getCenterFrequency().subtract(shiftSize), getWidth()); 773 } 774 775 return this; 776 } 777 778 /** 779 * Moves both the low and high endpoints of this range up by 780 * {@code shiftSize}. 781 * <p> 782 * If {@code shiftSize} is <i>null</i> this method will take no action 783 * and this range will have the same state as immediately before the call.</p> 784 * <p> 785 * If the size of the shift is infinite, this range will 786 * be reset so that its both its low and high frequencies have a value 787 * of {@code Double.POSITIVE_INFINITY}.</p> 788 * <p> 789 * If neither of the two conditions above exist, the requested shift 790 * will be made.</p> 791 * 792 * @param shiftSize the amount by which to increase both the high and 793 * low frequencies of this range. 794 * 795 * @return this range after the frequency shift. 796 */ 797 public FrequencyRange shiftUpBy(Frequency shiftSize) 798 { 799 if (shiftSize == null) 800 { 801 //do nothing 802 } 803 else if (shiftSize.isInfinite()) 804 { 805 lowFrequency.setValue("+infinity"); 806 highFrequency.setValue("+infinity"); 807 } 808 else 809 { 810 setCenterAndWidth(getCenterFrequency().add(shiftSize), getWidth()); 811 } 812 813 return this; 814 } 815 816 /** 817 * Multiplies the low and high frequencies of this range by {@code multiplier}. 818 * 819 * @param multiplier 820 * the number by which the end points of this range should be multiplied. 821 * This value must not result in a frequency magnitude that is 822 * negative. 823 * 824 * @return this range, after the multiplication. 825 * 826 * @throws ArithmeticException 827 * if the multiplication results in a negative value for an end point, and 828 * that end point is not allowed to be negative. 829 * 830 * @throws NullPointerException 831 * if <tt>multiplier</tt> is <i>null</i>. 832 * 833 * @since 2008-11-06 834 */ 835 public FrequencyRange multiplyBy(String multiplier) 836 { 837 return multiplyBy(new BigDecimal(multiplier)); 838 } 839 840 /** 841 * Multiplies the low and high frequencies of this range by {@code multiplier}. 842 * 843 * @param multiplier 844 * the number by which the end points of this range should be multiplied. 845 * This value must not result in a frequency magnitude that is 846 * negative. 847 * 848 * @return this range, after the multiplication. 849 * 850 * @throws ArithmeticException 851 * if the multiplication results in a negative value for an end point, and 852 * that end point is not allowed to be negative. 853 * 854 * @throws NullPointerException 855 * if <tt>multiplier</tt> is <i>null</i>. 856 * 857 * @since 2008-11-06 858 */ 859 public FrequencyRange multiplyBy(BigDecimal multiplier) 860 { 861 lowFrequency.multiplyBy(multiplier); 862 highFrequency.multiplyBy(multiplier); 863 864 return this; 865 } 866 867 /** 868 * Moves this range so that its new center frequency is {@code newCenter}. 869 * <p> 870 * If {@code newCenter} is <i>null</i> this method will take no action 871 * and this range will have the same state as immediately before the call.</p> 872 * <p> 873 * If the new center is infinite, this range will 874 * be reset so that its both its low and high frequencies have a value 875 * of {@code Double.POSITIVE_INFINITY}.</p> 876 * <p> 877 * If neither of the two conditions above exist, an attempt will be made 878 * to honor the shift to the new center while leaving the width of this 879 * range unchanged. 880 * If, however, doing so would lead to 881 * a negative value for the low endpoint of this range, this range's 882 * width will be preserved and the value of the low frequency will be 883 * set to zero. In otherwords, this method will make the biggest legal 884 * shift that is less than or equal than the requested shift, and the 885 * resulting center might not be equal to {@code newCenter}.</p> 886 * 887 * @param newCenter the new center frequency of this range. 888 * 889 * @return this range after its center has been shifted. 890 */ 891 public FrequencyRange moveCenterTo(Frequency newCenter) 892 { 893 if (newCenter == null) 894 { 895 //do nothing 896 } 897 else 898 { 899 Frequency currentCenter = getCenterFrequency(); 900 901 if (newCenter.compareTo(currentCenter) > 0) 902 { 903 shiftUpBy(newCenter.clone().subtract(currentCenter)); 904 } 905 else 906 { 907 shiftDownBy(currentCenter.subtract(newCenter)); 908 } 909 } 910 911 return this; 912 } 913 914 /** 915 * Changes the width of this range to {@code newWidth}. 916 * <p> 917 * If {@code newWidth} is <i>null</i> this method will take no action 918 * and this range will have the same state as immediately before the call.</p> 919 * <p> 920 * If the new width is infinite, this range will 921 * be reset so that its low frequency has a value of zero and its high 922 * frequency has an infinite value.</p> 923 * <p> 924 * If neither of the two conditions above exist, an attempt will be made 925 * to honor the new width while leaving the center frequency unchanged. 926 * However, if doing so would lead to 927 * a negative value for the low endpoint of this range, the new width will 928 * be honored, but the center will be altered. The new center will be 929 * a frequency equal to one half the width, as the low frequency would be 930 * zero and the high frequency equal to the width.</p> 931 * 932 * @param newWidth the new bandwidth of this range. 933 * 934 * @return this range after the change in bandwidth 935 */ 936 public FrequencyRange changeWidthTo(Frequency newWidth) 937 { 938 return setCenterAndWidth(getCenterFrequency(), newWidth); 939 } 940 941 //=========================================================================== 942 // TEXT METHODS 943 //=========================================================================== 944 945 /** Returns a text representation of this frequency range. */ 946 public String toString() 947 { 948 return toString(true); 949 } 950 951 /** 952 * Returns a text representation of this frequency range. 953 954 * @param lowToHigh 955 * if <true>, the low frequency is displayed before the high frequency. 956 * 957 * @return 958 * a text representation of this frequency range. 959 */ 960 public String toString(boolean lowToHigh) 961 { 962 StringBuilder buff = new StringBuilder(); 963 964 Frequency left = lowToHigh ? lowFrequency : highFrequency; 965 Frequency right = lowToHigh ? highFrequency : lowFrequency; 966 967 buff.append(left) 968 .append(FormatString.ENDPOINT_SEPARATOR) 969 .append(right); 970 971 return buff.toString(); 972 973 } 974 975 /** 976 * Returns a text representation of this frequency range. 977 * 978 * @param minFracDigits the minimum number of places after the decimal point. 979 * @param maxFracDigits the maximum number of places after the decimal point. 980 * 981 * @return a text representation of this frequency range. 982 */ 983 public String toString(int minFracDigits, int maxFracDigits) 984 { 985 return toString(minFracDigits, maxFracDigits, true); 986 } 987 988 /** 989 * Returns a text representation of this frequency range. 990 * 991 * @param minFracDigits the minimum number of places after the decimal point. 992 * @param maxFracDigits the maximum number of places after the decimal point. 993 * @param lowToHigh 994 * if <true>, the low frequency is displayed before the high frequency. 995 * 996 * @return a text representation of this frequency range. 997 */ 998 public String toString(int minFracDigits, int maxFracDigits, boolean lowToHigh) 999 { 1000 StringBuilder buff = new StringBuilder(); 1001 1002 Frequency left = lowToHigh ? lowFrequency : highFrequency; 1003 Frequency right = lowToHigh ? highFrequency : lowFrequency; 1004 1005 buff.append(left.toString(minFracDigits, maxFracDigits)); 1006 buff.append(FormatString.ENDPOINT_SEPARATOR); 1007 buff.append(right.toString(minFracDigits, maxFracDigits)); 1008 1009 return buff.toString(); 1010 } 1011 1012 /** 1013 * Creates and returns a new frequency range, based on {@code rangeText} 1014 * and a separator string of {@link FormatString#ENDPOINT_SEPARATOR}. 1015 * <p> 1016 * See {@link #parse(String, String)} for more details.</p> 1017 * 1018 * @param rangeText text that represents a frequency range. 1019 * 1020 * @return a new frequency range, based on {@code rangeText} and a separator 1021 * string of {@link FormatString#ENDPOINT_SEPARATOR}. 1022 */ 1023 public static FrequencyRange parse(String rangeText) 1024 { 1025 return parse(rangeText, FormatString.ENDPOINT_SEPARATOR); 1026 } 1027 1028 /** 1029 * Creates and returns a new frequency range, based on {@code rangeText}. 1030 * <p> 1031 * The general form {@code rangeText} is 1032 * <i style="color:blue">frequencyOne</i> 1033 * <i style="color:red">EndPointSeparator</i> 1034 * <i style="color:blue">frequencyTwo</i>, 1035 * where <i>frequencyOne</i> and <i>frequencyTwo</i> follow the rules set 1036 * forth in {@link Frequency#parse(String)} and <i>endPointSeparator</i> 1037 * is text that does not collide with text that is normally found in 1038 * the frequency text. (That is, it would be unwise to use a number, 1039 * period, or any of the alpha characters that might be present in 1040 * the units portion of a frequency in the endpoint separator. 1041 * Typical separators might be <tt>" - "</tt> or <tt>","</tt>.) 1042 * Note: using "-" (without spaces) as a separator will cause problems if either 1043 * endpoint is negative.</p> 1044 * <p> 1045 * If {@code rangeText} is <i>null</i> or the empty string (<tt>""</tt>), 1046 * the returned range will be equal to one created via the no-argument 1047 * constructor.</p> 1048 * 1049 * @param rangeText text that represents a frequency range. 1050 * 1051 * @param endPointSeparator the text that separates one endpoint of the range 1052 * from the other. 1053 * 1054 * @return a new frequency range, based on {@code rangeText}. 1055 */ 1056 public static FrequencyRange parse(String rangeText, String endPointSeparator) 1057 { 1058 FrequencyRange range = new FrequencyRange(); 1059 1060 if ((rangeText != null) && !rangeText.equals("")) 1061 { 1062 try { 1063 range.parseRange(rangeText, endPointSeparator); 1064 } 1065 catch (Exception ex) { 1066 throw new IllegalArgumentException("Could not parse " + rangeText, ex); 1067 } 1068 } 1069 1070 return range; 1071 } 1072 1073 /** 1074 * If parsing was successful, this range's endpoints will have been 1075 * valued. Otherwise an exception is thrown. 1076 */ 1077 private void parseRange(String rangeText, String endPointSeparator) 1078 { 1079 String errMsg = null; 1080 1081 String[] endPoints = rangeText.split(endPointSeparator, -1); 1082 1083 if (endPoints.length == 2) 1084 { 1085 try 1086 { 1087 Frequency f0 = Frequency.parse(endPoints[0]); 1088 Frequency f1 = Frequency.parse(endPoints[1]); 1089 1090 set(f0, f1); 1091 } 1092 catch (IllegalArgumentException ex) 1093 { 1094 errMsg = ex.getMessage(); 1095 } 1096 } 1097 else //bad # of endpoints 1098 { 1099 errMsg = "Found " + endPoints.length + " endpoints. There should be 2."; 1100 } 1101 1102 //Error message 1103 if (errMsg != null) 1104 { 1105 errMsg = errMsg + " [Params: rangeText=" + rangeText + 1106 ", endPointSeparator=" + endPointSeparator + "]"; 1107 1108 throw new IllegalArgumentException(errMsg); 1109 } 1110 } 1111 1112 //=========================================================================== 1113 // UTILITY METHODS 1114 //=========================================================================== 1115 1116 /** Creates a copy of this frequency range. */ 1117 public FrequencyRange clone() 1118 { 1119 FrequencyRange clone = null; 1120 1121 try 1122 { 1123 clone = (FrequencyRange)super.clone(); 1124 clone.highFrequency = this.highFrequency.clone(); 1125 clone.lowFrequency = this.lowFrequency.clone(); 1126 } 1127 catch (CloneNotSupportedException ex) 1128 { 1129 throw new RuntimeException(ex); 1130 } 1131 1132 return clone; 1133 } 1134 1135 /** Returns <i>true</i> if {@code o} is equal to this frequency range. */ 1136 public boolean equals(Object o) 1137 { 1138 //Quick exit if o is this 1139 if (o == this) 1140 return true; 1141 1142 //Quick exit if o is null 1143 if (o == null) 1144 return false; 1145 1146 //Quick exit if classes are different 1147 if (!o.getClass().equals(this.getClass())) 1148 return false; 1149 1150 FrequencyRange otherRange = (FrequencyRange)o; 1151 1152 return otherRange.highFrequency.equals(this.highFrequency) && 1153 otherRange.lowFrequency.equals(this.lowFrequency); 1154 } 1155 1156 /** Returns a hash code value for this frequency range. */ 1157 public int hashCode() 1158 { 1159 return toString().hashCode(); 1160 } 1161 1162 /** 1163 * Compares this range to {@code other} for order. 1164 * <p> 1165 * One range is deemed to be "less than" the other if it begins 1166 * before the other. In the case that two ranges start at the 1167 * same point, the one with a lesser endpoint is considered to be less 1168 * than the other.</p> 1169 * 1170 * @param other the range to which this one is compared. 1171 * 1172 * @return a negative integer, zero, or a positive integer as this range 1173 * is less than, equal to, or greater than the other range. 1174 */ 1175 public int compareTo(FrequencyRange other) 1176 { 1177 int answer = this.lowFrequency.compareTo(other.lowFrequency); 1178 1179 //If the ranges start at the same point, compare the end points 1180 if (answer == 0) 1181 answer = this.highFrequency.compareTo(other.highFrequency); 1182 1183 return answer; 1184 } 1185 }