001 package edu.nrao.sss.measure; 002 003 import java.math.MathContext; 004 import java.math.RoundingMode; 005 import java.util.ArrayList; 006 import java.util.Collection; 007 import java.util.SortedSet; 008 import java.util.TreeSet; 009 010 /** 011 * A collection of disjoint (non-overlapping, non-contiguous) frequency ranges. 012 * These ranges are said to be "covered" portions of the spectrum. 013 * <p> 014 * The span of this spectrum goes from the low frequency of its lowest covered 015 * range through the high frequency of its highest covered range. This spectrum 016 * contains only covered frequency <i>ranges</i> -- it does not handle 017 * covered frequency <i>points</i>. (An approximation of a covered single 018 * frequency can be made by using an extremely narrow range.)</p> 019 * <p> 020 * <b>Version Info:</b> 021 * <table style="margin-left:2em"> 022 * <tr><td>$Revision$</td></tr> 023 * <tr><td>$Date$</td></tr> 024 * <tr><td>$Author$</td></tr> 025 * </table></p> 026 * 027 * @author David M. Harland 028 * @since 2007-01-11 029 */ 030 public class FrequencySpectrum 031 implements Cloneable 032 { 033 private static FrequencyUnits STD_UNITS = FrequencyUnits.HERTZ; 034 035 private static final MathContext PRECISION = 036 new MathContext(MathContext.DECIMAL128.getPrecision(),RoundingMode.HALF_UP); 037 038 private SortedSet<FrequencyRange> coveredRanges; 039 040 /** 041 * Creates a new spectrum with no covered ranges and, therefore, a span of 042 * zero Hertz. 043 */ 044 public FrequencySpectrum() 045 { 046 coveredRanges = new TreeSet<FrequencyRange>(); 047 } 048 049 /** 050 * Creates a new spectrum using the given collection of covered ranges. 051 * @param ranges the portions of this spectrum that are covered. 052 */ 053 public FrequencySpectrum(Collection<FrequencyRange> ranges) 054 { 055 this(); 056 057 for (FrequencyRange range : ranges) 058 addCoveredRange(range); 059 } 060 061 //============================================================================ 062 // 063 //============================================================================ 064 065 /** 066 * Returns the regions of this spectrum that are covered. 067 * The returned ranges are copies of those held by this spectrum, so they 068 * may be altered by the client without affecting this object. 069 * 070 * @return the regions of this spectrum that are covered. 071 */ 072 public SortedSet<FrequencyRange> getCoveredRanges() 073 { 074 SortedSet<FrequencyRange> result = new TreeSet<FrequencyRange>(); 075 076 for (FrequencyRange coveredRange : coveredRanges) 077 result.add(coveredRange.clone()); 078 079 return result; 080 } 081 082 //============================================================================ 083 // DERIVED QUERIES 084 //============================================================================ 085 086 /** 087 * Returns a range whose low frequency is that of the lowest covered range 088 * in this spectrum and whose high frequency is that of the highest covered 089 * range. 090 * <p> 091 * For example if this spectrum has these covered ranges: 092 * <tt>10GHz-15GHz</tt> and <tt>50GHz-60GHz</tt>, 093 * then the span of this spectrum is <tt>10GHz-60GHz</tt>.</p> 094 * 095 * @return a range representing the lowest and highest frequencies covered 096 * herein. 097 */ 098 public FrequencyRange getSpan() 099 { 100 if (coveredRanges.size() > 0) 101 return new FrequencyRange(coveredRanges.first().getLowFrequency(), 102 coveredRanges.last().getHighFrequency()); 103 else 104 return new FrequencyRange(new Frequency(), new Frequency()); 105 } 106 107 /** 108 * Returns <i>true</i> if this spectrum covers the given frequency. 109 * 110 * @param frequency 111 * the frequency to be tested for containment. 112 * 113 * @return <i>true</i> if this spectrum covers the given frequency. 114 */ 115 public boolean covers(Frequency frequency) 116 { 117 if (frequency == null) 118 return false; 119 120 for (FrequencyRange range : coveredRanges) 121 if (range.contains(frequency)) 122 return true; 123 124 return false; 125 } 126 127 /** 128 * Returns the amount of this spectrum that is covered. 129 * <p> 130 * For example if this spectrum has these covered ranges: 131 * <tt>10GHz-15GHz</tt> and <tt>50GHz-60GHz</tt>, 132 * then the amount covered is <tt>15GHz</tt> 133 * (<tt>5GHz + 10GHz</tt>).</p> 134 * 135 * @return the amount of this spectrum that is covered. 136 */ 137 public Frequency getAmountCovered() 138 { 139 Frequency amountCovered = new Frequency("0.0"); 140 141 for (FrequencyRange r : coveredRanges) 142 amountCovered.add(r.getWidth()); 143 144 return amountCovered; 145 } 146 147 /** 148 * Returns the amount of the target range that is covered by the 149 * covered portions of this spectrum. In other words, the amount 150 * returned is the size of the intersection between the covered 151 * portions of this spectrum and the target range. 152 * <p> 153 * This method is useful when the range you want to test for coverage 154 * has a different span than the natural span of this spectrum. 155 * The span of this spectrum is defined by the low frequency of the 156 * lowest covered portion and the high frequency of the highest 157 * covered portion of this spectrum.</p> 158 * <p> 159 * For example if this spectrum has these covered ranges: 160 * <tt>10GHz-15GHz</tt> and <tt>50GHz-60GHz</tt>, 161 * and if the target range is <tt>0GHz-100GHz</tt>, 162 * then the amount covered is <tt>15GHz</tt>. 163 * If, however, the target range is <tt>35GHz-55GHz</tt>, 164 * then the amount covered is <tt>5GHz</tt>.</p> 165 * 166 * @param targetRange a range to be tested for coverage. 167 * 168 * @return the amount of the target range that is covered by the 169 * covered portions of this spectrum. 170 */ 171 public Frequency getAmountCovered(FrequencyRange targetRange) 172 { 173 Frequency amountCovered; 174 175 //If the target range completely contains this spectrum, get a quick result 176 if (targetRange.contains(getSpan())) 177 { 178 amountCovered = getAmountCovered(); 179 } 180 //Otherwise, sum the overlapping regions 181 else 182 { 183 amountCovered = new Frequency("0.0"); 184 185 for (FrequencyRange r : coveredRanges) 186 if (targetRange.overlaps(r)) 187 amountCovered.add(targetRange.getOverlapWith(r).getWidth()); 188 } 189 190 return amountCovered; 191 } 192 193 /** 194 * Returns a number that represents the portion of this spectrum 195 * that is covered. 196 * <p> 197 * For example if this spectrum has these covered ranges: 198 * <tt>10GHz-15GHz</tt> and <tt>50GHz-60GHz</tt>, 199 * then the portion of this spectrum that is covered is <tt>0.30</tt>. 200 * (The covered range is <tt>15GHz</tt> and the total span is 201 * <tt>50GHz</tt>.)</p> 202 * 203 * @return the fraction of this spectrum that is covered. 204 * The value returned is between zero and one, inclusive 205 * (i.e., [0-1]). 206 */ 207 public double getFractionCovered() 208 { 209 return getAmountCovered().toUnits(STD_UNITS) 210 .divide(getSpan().getWidth().toUnits(STD_UNITS), 211 PRECISION).doubleValue(); 212 } 213 214 /** 215 * Returns a number that represents the portion of the target range 216 * that is covered by the covered portions of this spectrum. 217 * <p> 218 * For example if this spectrum has these covered ranges: 219 * <tt>10GHz-15GHz</tt> and <tt>50GHz-60GHz</tt>, 220 * and if the target range is <tt>0GHz-100GHz</tt>, 221 * then the portion of this spectrum that is covered is <tt>0.15</tt>. 222 * If, however, the target range is <tt>35GHz-55GHz</tt>, 223 * then the portion of this spectrum that is covered is <tt>0.25</tt> 224 * (<tt>5GHz</tt> of overlap with the <tt>20GHz</tt>-wide target range).</p> 225 * 226 * @param targetRange a range to be tested for coverage. 227 * 228 * @return the fraction of the target range that is covered by the 229 * covered portions of this spectrum. 230 * The value returned is between zero and one, inclusive 231 * (i.e., [0-1]). 232 */ 233 public double getAmountCoveredAsFractionOf(FrequencyRange targetRange) 234 { 235 return 236 getAmountCovered(targetRange).toUnits(STD_UNITS) 237 .divide(targetRange.getWidth().toUnits(STD_UNITS), 238 PRECISION).doubleValue(); 239 } 240 241 /** 242 * Returns a quantity that represents the smallest gap between 243 * {@code targetRange} and the nearest of the covered ranges of this 244 * spectrum. 245 * <p> 246 * If the target range overlaps one or more of this spectrum's covered 247 * ranges, the returned range will have a width of zero and an arbitrary 248 * center frequency. If this spectrum has no covered ranges, the size 249 * of the range returned will be infinite.</p> 250 * 251 * @param targetRange the range for which a gap is calculated. 252 * 253 * @return the smallest gap between {@code targetRange} and the nearest 254 * of this spectrum's covered ranges. 255 */ 256 public FrequencyRange getSmallestGapTo(FrequencyRange targetRange) 257 { 258 FrequencyRange smallestGap = new FrequencyRange(); //an infinite range 259 260 for (FrequencyRange coveredRange : coveredRanges) 261 { 262 if (coveredRange.overlaps(targetRange)) 263 { 264 Frequency zeroFreq = new Frequency(); 265 smallestGap.setCenterAndWidth(zeroFreq, zeroFreq); 266 break; 267 } 268 269 FrequencyRange gap = coveredRange.getGapBetween(targetRange); 270 271 if (gap.getWidth().compareTo(smallestGap.getWidth()) < 0) 272 smallestGap = gap; 273 } 274 275 return smallestGap; 276 } 277 278 //============================================================================ 279 // ADDING & REMOVING RANGES 280 //============================================================================ 281 282 /** 283 * Removes all covered ranges from this spectrum. 284 */ 285 public void clear() 286 { 287 coveredRanges.clear(); 288 } 289 290 /** @deprecated Use {@link #set(String)}. */ 291 @Deprecated public void setCoveredRanges(String spectrumText) 292 { 293 set(spectrumText); 294 } 295 296 /** 297 * Removes all covered ranges from this spectrum and adds new ranges found 298 * in {@code spectrumText}. 299 * See {@link #parse(String)} for details about the parsing methodology. 300 * 301 * @param spectrumText text representation of a {@code FrequencySpectrum}. 302 */ 303 public void set(String spectrumText) 304 { 305 if (spectrumText == null || spectrumText.equals("")) 306 { 307 clear(); 308 } 309 else 310 { 311 TreeSet<FrequencyRange> previousRanges = new TreeSet<FrequencyRange>(coveredRanges); 312 clear(); 313 try { 314 parseSpectrum(spectrumText, ",", "-"); 315 } 316 catch (IllegalArgumentException ex) { 317 coveredRanges = previousRanges; 318 throw ex; 319 } 320 } 321 } 322 323 /** 324 * Adds new covered ranges to this spectrum by parsing {@code spectrumText}. 325 * See {@link #parse(String)} for details about the parsing methodology. 326 * 327 * @param spectrumText text representation of a {@code FrequencySpectrum}. 328 */ 329 public void addCoveredRanges(String spectrumText) 330 { 331 parseSpectrum(spectrumText, ",", "-"); 332 } 333 334 /** 335 * Adds a new frequency range to our set of covered ranges. 336 * <p> 337 * The addition of a new range can take one of several tracks:</p> 338 * <ol> 339 * <li>If {@code newRange} is <i>null</i>, this object is not changed.</li> 340 * <li>If {@code newRange} has no overlap with any existing range, nor is 341 * contiguous with any existing range, then a copy of it is added to 342 * this object.</li> 343 * <li>If {@code newRange} overlaps, or is continguous, with one or more 344 * existing ranges, the single union of all those ranges replaces 345 * those ranges.</li> 346 * </ol> 347 * <p><u>Examples:</u></br> 348 * <pre> 349 * Existing Ranges: ~~~~~~ ~~~~ ~~~~~~~~~~~~ ~~~~~~~ ~~~~~~~~~ 350 * New Range: +++ 351 * After Addition: ~~~~~~~~ ~~~~ ~~~ ~~~~~~~~~~~~ ~~~~~~~ ~~~~~~~~~ 352 * </pre> 353 * <pre> 354 * Existing Ranges: ~~~~~~ ~~~~ ~~~~~~~~~~~~ ~~~~~~~ ~~~~~~~~~ 355 * New Range: +++ 356 * After Addition: ~~~~~~~~ ~~~~ ~~~~~~~~~~~~ ~~~~~~~ ~~~~~~~~~ 357 * </pre> 358 * <pre> 359 * Existing Ranges: ~~~~~~ ~~~~ ~~~~~~~~~~~~ ~~~~~~~ ~~~~~~~~~ 360 * New Range: +++++ 361 * After Addition: ~~~~~~~~ ~~~~ ~~~~~~~~~~~~ ~~~~~~~ ~~~~~~~~~ 362 * </pre> 363 * <pre> 364 * Existing Ranges: ~~~~~~ ~~~~ ~~~~~~~~~~~~ ~~~~~~~ ~~~~~~~~~ 365 * New Range: +++++++++ 366 * After Addition: ~~~~~~~~~~~~~~ ~~~~~~~~~~~~ ~~~~~~~ ~~~~~~~~~ 367 * </pre> 368 * <pre> 369 * Existing Ranges: ~~~~~~ ~~~~ ~~~~~~~~~~~~ ~~~~~~~ ~~~~~~~~~ 370 * New Range: ++++++++++++++++++++++++++++++++++++ 371 * After Addition: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~ 372 * </pre> 373 * 374 * @param newRange the range to be added to our set of covered ranges. 375 * @return this instance. 376 */ 377 public FrequencySpectrum addCoveredRange(FrequencyRange newRange) 378 { 379 //Quick exit if range is null 380 if (newRange == null) 381 return this; 382 383 FrequencyRange copyOfNewRange = newRange.clone(); 384 385 ArrayList<FrequencyRange> rangesToRemove = new ArrayList<FrequencyRange>(); 386 387 //Because the incoming range may overlap several of the existing ranges, 388 //we choose to merge the existing ranges into the (copy of) the new one. 389 //We also note which ranges overlapped and were contiguous with the 390 //new range and remove them before we leave this method. 391 for (FrequencyRange coveredRange : coveredRanges) 392 { 393 if (newRange.overlaps(coveredRange) || 394 newRange.isContiguousWith(coveredRange)) 395 { 396 copyOfNewRange.mergeWith(coveredRange); 397 rangesToRemove.add(coveredRange); 398 } 399 } 400 401 //If we did any mergers, remove the now redundant ranges. 402 for (FrequencyRange unwantedRange : rangesToRemove) 403 coveredRanges.remove(unwantedRange); 404 405 //Add the new range, which may have been merged with 0+ covered ranges 406 coveredRanges.add(copyOfNewRange); 407 408 return this; 409 } 410 411 /** 412 * Removes a frequency range from our set of covered ranges. 413 * </ol> 414 * <p><u>Examples:</u></br> 415 * <pre> 416 * Existing Ranges: ~~~~~~ ~~~~ ~~~~~~~~~~~~ ~~~~~~~ ~~~~~~~~~ 417 * Unwanted Range: --- 418 * After Removal: ~~~~~~~~ ~~~~ ~~~~~~~~~~~~ ~~~~~~~ ~~~~~~~~~ 419 * </pre> 420 * <pre> 421 * Existing Ranges: ~~~~~~ ~~~~ ~~~~~~~~~~~~ ~~~~~~~ ~~~~~~~~~ 422 * Unwanted Range: --- 423 * After Removal: ~~~~~~~~ ~~~~ ~~ ~~~~~~~ ~~~~~~~ ~~~~~~~~~ 424 * </pre> 425 * <pre> 426 * Existing Ranges: ~~~~~~ ~~~~ ~~~~~~~~~~~~ ~~~~~~~ ~~~~~~~~~ 427 * Unwanted Range: ----- 428 * After Removal: ~~~ ~~~~ ~~~~~~~~~~~~ ~~~~~~~ ~~~~~~~~~ 429 * </pre> 430 * <pre> 431 * Existing Ranges: ~~~~~~ ~~~~ ~~~~~~~~~~~~ ~~~~~~~ ~~~~~~~~~ 432 * Unwanted Range: --------- 433 * After Removal: ~~~ ~~ ~~~~~~~~~~~~ ~~~~~~~ ~~~~~~~~~ 434 * </pre> 435 * <pre> 436 * Existing Ranges: ~~~~~~ ~~~~ ~~~~~~~~~~~~ ~~~~~~~ ~~~~~~~~~ 437 * Unwanted Range: ------------------------------------ 438 * After Removal: ~~~ ~~~~ ~~~~~~~~~ 439 * </pre> 440 * 441 * @param unwantedRange the range to be removed from our set of covered ranges. 442 * @return this instance. 443 */ 444 public FrequencySpectrum removeCoveredRange(FrequencyRange unwantedRange) 445 { 446 //Quick exit if range is null 447 if (unwantedRange == null) 448 return this; 449 450 ArrayList<FrequencyRange> rangesToRemove = new ArrayList<FrequencyRange>(); 451 ArrayList<FrequencyRange> rangesToTrim = new ArrayList<FrequencyRange>(); 452 FrequencyRange rangeToSplit = null; 453 454 //Identify the ranges to be removed and those to be altered 455 for (FrequencyRange coveredRange : coveredRanges) 456 { 457 //Completely contained ranges will be removed. 458 if (unwantedRange.contains(coveredRange)) 459 { 460 rangesToRemove.add(coveredRange); 461 } 462 //Unwanted ranges is not same as covered range, but IS contained by it. 463 //The covered range needs to be split into two disjoint ranges. 464 else if (coveredRange.contains(unwantedRange)) 465 { 466 if (rangeToSplit != null) 467 throw new RuntimeException( 468 "PROGRAMMER ERROR: Only one range can contain the unwanted range."); 469 470 rangeToSplit = coveredRange; 471 //we could break the loop here, but we keep going for Q/A 472 } 473 //Partially overlapping ranges will be trimmed. 474 else if (unwantedRange.overlaps(coveredRange)) 475 { 476 rangesToTrim.add(coveredRange); 477 } 478 } 479 480 //Check the above algorithm for errors. Runtime exception if faulty. 481 //We could handle instead with an assertion that is deactivated in production. 482 validateRemoveCoveredRange(rangesToRemove, rangesToTrim, rangeToSplit); 483 484 //Perform the needed actions 485 coveredRanges.removeAll(rangesToRemove); 486 trimRanges(rangesToTrim, unwantedRange); 487 splitRange(rangeToSplit, unwantedRange); 488 489 return this; 490 } 491 492 /** 493 * Modifies this spectrum to be the intersection of its covered ranges 494 * with {@code targetRange}. 495 * If this range does not intersect with {@code other}, it will have 496 * no covered ranges after the intersection. 497 * 498 * @param targetRange the frequency range with which this spectrum 499 * should be intersected. 500 * 501 * @return this spectrum after the intersection. 502 */ 503 public FrequencySpectrum intersectWith(FrequencyRange targetRange) 504 { 505 ArrayList<FrequencyRange> overlappingRanges = 506 new ArrayList<FrequencyRange>(); 507 508 //Find all the overlapping ranges 509 for (FrequencyRange coveredRange : coveredRanges) 510 { 511 if (coveredRange.overlaps(targetRange)) 512 overlappingRanges.add(coveredRange.getOverlapWith(targetRange)); 513 } 514 515 //Remove all existing ranges 516 coveredRanges.clear(); 517 518 //Add all the overlapping ranges. 519 //Note: we could probably safely add these directly to the underlying list. 520 // However, we take the safer, though slower, approach here. 521 for (FrequencyRange overlappingRange : overlappingRanges) 522 addCoveredRange(overlappingRange); 523 524 return this; 525 } 526 527 /** Aids removeCoveredRange. */ 528 private void trimRanges(ArrayList<FrequencyRange> rangesToTrim, 529 FrequencyRange unwantedRange) 530 { 531 //Trim the 0, 1, or 2 partially overlapping ranges 532 for (FrequencyRange rangeToTrim : rangesToTrim) 533 { 534 //We remove the high end of this range 535 if (rangeToTrim.getLowFrequency() 536 .compareTo(unwantedRange.getLowFrequency()) < 0) 537 { 538 rangeToTrim.set( rangeToTrim.getLowFrequency(), 539 unwantedRange.getLowFrequency()); 540 } 541 //We remove the low end of this range 542 else if (rangeToTrim.getHighFrequency() 543 .compareTo(unwantedRange.getHighFrequency()) > 0) 544 { 545 rangeToTrim.set(unwantedRange.getHighFrequency(), 546 rangeToTrim.getHighFrequency()); 547 } 548 } 549 } 550 551 /** Aids removeCoveredRange. */ 552 private void splitRange(FrequencyRange rangeToSplit, 553 FrequencyRange unwantedRange) 554 { 555 if (rangeToSplit != null) 556 { 557 FrequencyRange newLow = 558 new FrequencyRange(rangeToSplit.getLowFrequency(), 559 unwantedRange.getLowFrequency()); 560 FrequencyRange newHigh = 561 new FrequencyRange(unwantedRange.getHighFrequency(), 562 rangeToSplit.getHighFrequency()); 563 564 coveredRanges.remove(rangeToSplit); 565 coveredRanges.add(newLow); 566 coveredRanges.add(newHigh); 567 } 568 } 569 570 /** 571 * Applies some Q/A to removeCoveredRange intermediate results 572 * and throws runtime exception if algorithm's logic is faulty. 573 */ 574 private 575 void validateRemoveCoveredRange(ArrayList<FrequencyRange> rangesToRemove, 576 ArrayList<FrequencyRange> rangesToTrim, 577 FrequencyRange rangeToSplit) 578 { 579 //If we're splitting a range, we cannot be removing or trimming any other 580 if (rangeToSplit != null) 581 { 582 if (rangesToRemove.size() > 0 || rangesToTrim.size() > 0) 583 throw new RuntimeException( 584 "PROGRAMMER ERROR: if we're splitting a range," + 585 " there should be no ranges to remove or trim." + 586 " ToRemove=" + rangesToRemove.size() + 587 ", ToTrim=" + rangesToTrim.size()); 588 } 589 else //no ranges to split 590 { 591 //Ranges to split s/b 0, 1, or 2 592 int trimCount = rangesToTrim.size(); 593 if (trimCount > 2) 594 throw new RuntimeException( 595 "PROGRAMMER ERROR: the maximum number of ranges to trim is 2. " + 596 " The algorithm produced " + trimCount); 597 } 598 } 599 600 //============================================================================ 601 // TEXT 602 //============================================================================ 603 604 /** Creates a text representation of this spectrum. */ 605 @Override 606 public String toString() 607 { 608 if (coveredRanges.size() > 0) 609 { 610 StringBuilder buff = new StringBuilder(); 611 612 FrequencyRange lastRange = coveredRanges.last(); 613 614 for (FrequencyRange coveredRange : coveredRanges) 615 { 616 buff.append(coveredRange.toString()); 617 618 if (coveredRange != lastRange) 619 buff.append(", "); 620 } 621 622 return buff.toString(); 623 } 624 else 625 { 626 return ""; 627 } 628 } 629 630 /** 631 * Creates and returns a new frequency spectrum, based on 632 * {@code spectrumText}. 633 * <p> 634 * This is a convenience method that is equivalent to calling 635 * {@link #parse(String, String, String) 636 * FrequencySpectrum.parse(spectrumText, ",", "-")}.</p> 637 * 638 * @param spectrumText 639 * text representation of a {@code FrequencySpectrum}. 640 * 641 * @return 642 * a new spectrum, based on {@code spectrumText}. 643 */ 644 public static FrequencySpectrum parse(String spectrumText) 645 { 646 return FrequencySpectrum.parse(spectrumText, ",", "-"); 647 } 648 649 /** 650 * Creates and returns a new frequency spectrum, based on 651 * {@code spectrumText}. 652 * <p> 653 * This is a convenience method that is equivalent to calling 654 * {@link #parse(String, String, String) 655 * FrequencySpectrum.parse(spectrumText, rangeSeparator, "-")}.</p> 656 * 657 * @param spectrumText 658 * text representation of a {@code FrequencySpectrum}. 659 * 660 * @param rangeSeparator 661 * text used to separate the endpoints of a frequency range. 662 * 663 * @return a new spectrum, based on {@code spectrumText}. 664 */ 665 public static FrequencySpectrum parse(String spectrumText, 666 String rangeSeparator) 667 { 668 return FrequencySpectrum.parse(spectrumText, rangeSeparator, "-"); 669 } 670 671 /** 672 * Creates and returns a new frequency spectrum, based on 673 * {@code spectrumText}. 674 * <p> 675 * The {@code spectrumText} is nothing more than a list of delimited 676 * frequency ranges. To learn more about how frequency ranges are parsed, 677 * see {@link FrequencyRange#parse(String)}. 678 * Each of these ranges is delimited by {@code rangeSeparator}.</p> 679 * <p> 680 * This method will attempt to parse the entire text and make use of 681 * any ranges that are successfully interpreted. If there are any parsing 682 * errors, an {@code IllegalArgumentException} will be thrown, listing 683 * each of the errors in its message.</p> 684 * <p> 685 * If {@code spectrumText} is <i>null</i> or the empty string (<tt>""</tt>), 686 * the returned range will be equal to one created via the no-argument 687 * constructor.</p> 688 * 689 * @param spectrumText 690 * text representation of a {@code FrequencySpectrum}. 691 * 692 * @param rangeSeparator 693 * text used to separate the endpoints of a frequency range. 694 * 695 * @param endPointSeparator 696 * text that separates one frequency range from another in 697 * {@code spectrumText}. 698 * 699 * @return a new spectrum, based on {@code spectrumText}. 700 */ 701 public static FrequencySpectrum parse(String spectrumText, 702 String rangeSeparator, 703 String endPointSeparator) 704 { 705 FrequencySpectrum spectrum = new FrequencySpectrum(); 706 707 if ((spectrumText != null) && !spectrumText.equals("")) 708 { 709 try { 710 spectrum.parseSpectrum(spectrumText, rangeSeparator, endPointSeparator); 711 } 712 catch (Exception ex) { 713 throw new IllegalArgumentException("Could not parse " + 714 spectrumText, ex); 715 } 716 } 717 718 return spectrum; 719 } 720 721 /** 722 * Adds new covered ranges to this spectrum by parsing {@code spectrumText}. 723 * Each successfully parsed range is added to this spectrum. 724 * If one or more parsing errors were detected, an 725 * {@code IllegalArgumentException} will be thrown, with each of the 726 * errors listed in its message. 727 * 728 * @param spectrumText text representation of a {@code FrequencySpectrum}. 729 * 730 * @param rangeSeparator 731 * text used to separate the endpoints of a frequency range. 732 * 733 * @param endPointSeparator text that separates one frequency range from 734 * another in {@code spectrumText}. 735 */ 736 private void parseSpectrum(String spectrumText, 737 String rangeSeparator, String endPointSeparator) 738 { 739 final String EOL = System.getProperty("line.separator"); 740 741 StringBuilder errBuff = new StringBuilder(); 742 743 String[] ranges = spectrumText.split(rangeSeparator, -1); 744 745 for (String rangeText : ranges) 746 { 747 try { 748 addCoveredRange(FrequencyRange.parse(rangeText, endPointSeparator)); 749 } 750 catch (Exception ex) { 751 errBuff.append(ex.getMessage()).append(EOL); 752 } 753 } 754 755 if (errBuff.length() > 0) 756 throw new IllegalArgumentException(errBuff.toString()); 757 } 758 759 //============================================================================ 760 // 761 //============================================================================ 762 763 /** 764 * Returns a copy of this spectrum. 765 * <p> 766 * If anything goes wrong during the cloning procedure, 767 * a {@code RuntimeException} will be thrown.</p> 768 */ 769 @Override 770 public FrequencySpectrum clone() 771 { 772 FrequencySpectrum clone = null; 773 774 try 775 { 776 clone = (FrequencySpectrum)super.clone(); 777 778 clone.coveredRanges = new TreeSet<FrequencyRange>(); 779 for (FrequencyRange r : this.coveredRanges) 780 clone.coveredRanges.add(r.clone()); 781 } 782 catch (CloneNotSupportedException ex) 783 { 784 throw new RuntimeException(ex); 785 } 786 787 return clone; 788 } 789 790 /** Returns <i>true</i> if {@code o} is equal to this spectrum. */ 791 @Override 792 public boolean equals(Object o) 793 { 794 //Quick exit if o is this 795 if (o == this) 796 return true; 797 798 //Quick exit if o is null 799 if (o == null) 800 return false; 801 802 //Quick exit if classes are different 803 if (!o.getClass().equals(this.getClass())) 804 return false; 805 806 FrequencySpectrum other = (FrequencySpectrum)o; 807 808 SortedSet<FrequencyRange> these = 809 new TreeSet<FrequencyRange>(this.coveredRanges); 810 811 SortedSet<FrequencyRange> those = 812 new TreeSet<FrequencyRange>(other.coveredRanges); 813 814 return those.equals(these); 815 } 816 817 /** Returns a hash code value for this spectrum. */ 818 public int hashCode() 819 { 820 return coveredRanges.hashCode(); 821 } 822 }