001 package org.LiveGraph.dataCache; 002 003 import java.io.PrintWriter; 004 import java.io.StringWriter; 005 import java.util.ArrayList; 006 import java.util.Arrays; 007 import java.util.Collections; 008 import java.util.EnumSet; 009 import java.util.Iterator; 010 import java.util.List; 011 import java.util.Set; 012 013 import org.LiveGraph.LiveGraph; 014 import org.LiveGraph.events.Event; 015 import org.LiveGraph.events.EventProcessingException; 016 import org.LiveGraph.events.EventProducer; 017 import org.LiveGraph.events.EventType; 018 019 020 import com.softnetConsult.utils.collections.NullIterator; 021 import com.softnetConsult.utils.collections.ReadOnlyIterator; 022 import com.softnetConsult.utils.exceptions.UnexpectedSwitchCase; 023 024 025 /** 026 * An instance of this class caches datasets previously read from a data file in memory. 027 * The cache applies a smart procedure to cache just enough data in order to plot a graph 028 * on the screen. Two cache modes are currently possible: {@code CacheTailData} and 029 * {@code CacheAllData}. In the first case the data sets added most recently are 030 * cached (and ultimately displayed bythe plotter). In the latter case all datasets are 031 * cached. If the number of datasets grows too large, the datasets located at odd indices in 032 * the original data file will be deleted from the cache. 033 * After this only datasets located at even indices in the original file will be cached. 034 * If the cache grows too large again, this procedure is re-applied such that only datasets 035 * at indices divisible by 4 in the original file are cached. As more datasets are added to the 036 * cache, this procedure can be re-applied again making sure that at any time the original data 037 * file is sampled at equal intervals.<br /> 038 * The maximum cache size is {@code CACHE_SIZE}. 039 * 040 * <p> 041 * <strong>LiveGraph</strong> 042 * (<a href="http://www.live-graph.org" target="_blank">http://www.live-graph.org</a>). 043 * </p> 044 * <p>Copyright (c) 2007 by G. Paperin.</p> 045 * <p>File: DataCache.java</p> 046 * <p style="font-size:smaller;">Redistribution and use in source and binary forms, with or 047 * without modification, are permitted provided that the following terms and conditions are met: 048 * </p> 049 * <p style="font-size:smaller;">1. Redistributions of source code must retain the above 050 * acknowledgement of the LiveGraph project and its web-site, the above copyright notice, 051 * this list of conditions and the following disclaimer.<br /> 052 * 2. Redistributions in binary form must reproduce the above acknowledgement of the 053 * LiveGraph project and its web-site, the above copyright notice, this list of conditions 054 * and the following disclaimer in the documentation and/or other materials provided with 055 * the distribution.<br /> 056 * 3. All advertising materials mentioning features or use of this software or any derived 057 * software must display the following acknowledgement:<br /> 058 * <em>This product includes software developed by the LiveGraph project and its 059 * contributors.<br />(http://www.live-graph.org)</em><br /> 060 * 4. All advertising materials distributed in form of HTML pages or any other technology 061 * permitting active hyper-links that mention features or use of this software or any 062 * derived software must display the acknowledgment specified in condition 3 of this 063 * agreement, and in addition, include a visible and working hyper-link to the LiveGraph 064 * homepage (http://www.live-graph.org). 065 * </p> 066 * <p style="font-size:smaller;">THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY 067 * OF ANY KIND, EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 068 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 069 * THE AUTHORS, CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 070 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 071 * IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 072 * </p> 073 * 074 * @author Greg Paperin (<a href="http://www.paperin.org" target="_blank">http://www.paperin.org</a>) 075 * @version {@value org.LiveGraph.LiveGraph#version} 076 */ 077 public class DataCache implements EventProducer { 078 079 /** 080 * Maximum number if datasets to be held in memory. 081 */ 082 public static final int CACHE_SIZE = 500; 083 084 085 /** 086 * Maximum number of delayed events during a bulk operation before the listeners are brought up to date. 087 */ 088 public static final int MAX_DELAYED_EVENTS = 1000; 089 090 /** 091 * Number of datasets to always keep in memory when operating in {@code CacheTailData}-mode. 092 */ 093 public static final int TAIL_BALANCE_SIZE = (int) (0.75 * (double) CACHE_SIZE); 094 095 /** 096 * Defines possible cache modes. 097 * {@code CacheAllData} specifies that all datasets ever added to this cache must be sampled 098 * at equal intervals. {@code CacheTailData} specifies that only the most recently datasets 099 * are to be kept in memory. 100 */ 101 public static enum CacheMode { CacheAllData, CacheTailData }; 102 103 104 /** 105 * Stores the desctibtion of the data series in this cache. A data series corresponds to a column 106 * in a data file. 107 */ 108 private List<DataSeries> dataSeries = null; 109 110 /** 111 * Stores the data in this cache. 112 */ 113 private List<DataSet> dataSets = null; 114 115 /** 116 * Current operating mode. 117 */ 118 private CacheMode currentMode = null; 119 120 /** 121 * When working in {@code CacheAllData}-mode this value determines which datasets are kept in memory. 122 * At any time, exactly the datasets for which {@link DataSet#getDataFileIndex()} returns a value that 123 * can be divided by {@code dispersalFactor} without remainder will be kept in the cache. 124 */ 125 private int dispersalFactor = 1; 126 127 /** 128 * Caches the data file info lines. 129 */ 130 private List<String> dataFileInfo = null; 131 132 /** 133 * Caches the smallest data value currently in the cache. 134 */ 135 private double minValueCached = Double.NaN; 136 137 /** 138 * Caches the largest data value currently in the cache. 139 */ 140 private double maxValueCached = Double.NaN; 141 142 /** 143 * Stores occured cache events when operating in the delayed mode. 144 */ 145 private Set<CacheEvent> delayedEvents = null; 146 147 /** 148 * Whether the cache events are being delayed. 149 */ 150 private boolean delayEvents = false; 151 152 /** 153 * Number of events that has been delayed during the current bulk operation. 154 */ 155 private int countDelayedEvents = 0; 156 157 /** 158 * Creates a new data cache in the {@code CacheAllData}-mode. 159 * 160 */ 161 public DataCache() { 162 this.delayedEvents = EnumSet.noneOf(CacheEvent.class); 163 this.delayEvents = false; 164 this.countDelayedEvents = 0; 165 this.resetCache(CacheMode.CacheAllData); 166 } 167 168 /** 169 * Creates a new cache in the specified mode. 170 * 171 * @param mode Mode of the new cache. 172 */ 173 public DataCache(CacheMode mode) { 174 this(); 175 this.resetCache(mode); 176 } 177 178 /** 179 * Creates a new cache in a specified mode and initilises it for the specified data series. 180 * 181 * @param mode Mode to use. 182 * @param seriesLabels Names of the data series. 183 */ 184 public DataCache(CacheMode mode, List<String> seriesLabels) { 185 this(); 186 resetCache(mode, seriesLabels); 187 } 188 189 /** 190 * Creates a new cache in a specified mode and initilises it for the specified data series. 191 * 192 * @param mode Mode to use. 193 * @param seriesLabels Names of the data series. 194 */ 195 public DataCache(CacheMode mode, String [] seriesLabels) { 196 this(mode, Arrays.asList(seriesLabels)); 197 } 198 199 /** 200 * Removes all data from this cache and resets is to the empty state. 201 */ 202 public void resetCache() { 203 resetLabels(); 204 resetData(); 205 resetDataFileInfo(); 206 } 207 208 /** 209 * Removes all data from this cache and resets is to the empty state. 210 * 211 * @param mode The mode the cache will have after the reset. 212 */ 213 public void resetCache(CacheMode mode) { 214 resetLabels(); 215 resetData(mode); 216 resetDataFileInfo(); 217 } 218 219 /** 220 * Removes all data from this cache and resets is to the empty state. 221 * New data series are set up according to the specified labels. 222 * 223 * @param mode The mode the cache will have after the reset. 224 * @param seriesLabels The data series labels for the reset cache 225 */ 226 public void resetCache(CacheMode mode, List<String> seriesLabels) { 227 resetLabels(seriesLabels); 228 resetData(mode); 229 resetDataFileInfo(); 230 } 231 232 /** 233 * Removes all data series informatioon from the cache without deleting the actual data. 234 */ 235 public void resetLabels() { 236 List<String> l = Collections.emptyList(); 237 resetLabels(l); 238 } 239 240 /** 241 * Removes all data series informatioon from the cache and replaces is with new empty series. 242 * Actual data is not affected. 243 * 244 * @param seriesLabels Labels for the new data series. 245 */ 246 public void resetLabels(List<String> seriesLabels) { 247 248 if (null == seriesLabels) 249 throw new NullPointerException("Series labels array cannot be null"); 250 251 dataSeries = new ArrayList<DataSeries>(seriesLabels.size()); 252 int index = 0; 253 for (String label : seriesLabels) { 254 dataSeries.add(new DataSeries(label, this, index)); 255 index++; 256 } 257 258 fireEvent(CacheEvent.CACHE_UpdatedLabels); 259 } 260 261 /** 262 * Resets the cache while keeping the same operating mode. 263 * All data is deleted. 264 */ 265 public void resetData() { 266 resetData(currentMode); 267 } 268 269 /** 270 * Resets the cache to the specified mode. All data is deleted. 271 * 272 * @param mode New operating mode. 273 */ 274 public void resetData(CacheMode mode) { 275 276 if (null == mode) 277 throw new NullPointerException("Cache mode cannot be null"); 278 279 if (null != dataSets) { 280 dataSets.clear(); 281 dataSets = null; 282 } 283 284 currentMode = mode; 285 286 resetExtremeValues(); 287 288 switch (currentMode) { 289 case CacheAllData: dataSets = new RemoveRangeArrayList<DataSet>(CACHE_SIZE); 290 dispersalFactor = 1; 291 break; 292 293 case CacheTailData: dataSets = new RemoveRangeArrayList<DataSet>(CACHE_SIZE); 294 dispersalFactor = 1; 295 break; 296 297 default: 298 throw new UnexpectedSwitchCase(currentMode); 299 } 300 301 fireEvent(CacheEvent.CACHE_ChangedMode); 302 fireEvent(CacheEvent.CACHE_UpdatedData); 303 } 304 305 /** 306 * Delets the information of min and max values held by this cache and any of its data series. 307 * 308 */ 309 private void resetExtremeValues() { 310 minValueCached = Double.NaN; 311 maxValueCached = Double.NaN; 312 for (DataSeries s : dataSeries) 313 s.resetExtremeValues(); 314 } 315 316 /** 317 * Delets all data file info strings held by this cache. 318 * 319 */ 320 public void resetDataFileInfo() { 321 this.dataFileInfo = new ArrayList<String>(); 322 fireEvent(CacheEvent.CACHE_UpdatedDataFileInfo); 323 } 324 325 /** 326 * @return Current operating mode. 327 */ 328 public CacheMode getCacheMode() { 329 return currentMode; 330 } 331 332 /** 333 * @return Number of data series in the cache (i.e. data columns in the data file). 334 */ 335 public int countDataSeries() { 336 return dataSeries.size(); 337 } 338 339 /** 340 * @return a Read-olny iterator over this cache's data series. 341 */ 342 public ReadOnlyIterator<DataSeries> iterateDataSeries() { 343 return new ReadOnlyIterator<DataSeries>(dataSeries.iterator()); 344 } 345 346 /** 347 * @param index Data series number (0-based). 348 * @return The data series at the specified index. 349 */ 350 public DataSeries getDataSeries(int index) { 351 return dataSeries.get(index); 352 } 353 354 /** 355 * @return An read-only iterator over the labels of the data series in this cache. 356 */ 357 public ReadOnlyIterator<String> iterateDataSeriesLabels() { 358 return new DataSeriesLabelIterator(dataSeries.iterator()); 359 } 360 361 /** 362 * @param label A data series label. 363 * @return The index of the series with the specified label or -1 if not found. 364 */ 365 public int findDataSeriesIndex(String label) { 366 int index = 0; 367 ReadOnlyIterator<String> it = iterateDataSeriesLabels(); 368 while (it.hasNext()) { 369 if (it.next().equals(label)) 370 return index; 371 index++; 372 } 373 return -1; 374 } 375 376 /** 377 * @param label A data series label. 378 * @param ignoreCase Whether case shuld be ignore in string comparison. 379 * @return The index of the series with the specified label or -1 if not found. 380 */ 381 public int findDataSeriesIndex(String label, boolean ignoreCase) { 382 int index = 0; 383 ReadOnlyIterator<String> it = iterateDataSeriesLabels(); 384 while (it.hasNext()) { 385 if (ignoreCase && it.next().equalsIgnoreCase(label)) 386 return index; 387 if (!ignoreCase && it.next().equals(label)) 388 return index; 389 index++; 390 } 391 return -1; 392 } 393 394 /** 395 * @return Number of datasets currently in cache. 396 */ 397 public int countDataSets() { 398 return dataSets.size(); 399 } 400 401 /** 402 * @return Read-only iterator over the datasets in this cache. 403 */ 404 public ReadOnlyIterator<DataSet> iterateDataSets() { 405 return new ReadOnlyIterator<DataSet>(dataSets.iterator()); 406 } 407 408 /** 409 * @param cacheIndex Cache-index of a dataset. 410 * @return Dataset at the specified index. 411 */ 412 public DataSet getDataSet(int cacheIndex) { 413 return dataSets.get(cacheIndex); 414 } 415 416 /** 417 * @return Smallest value currently in the cache or {@code Double.NaN} if the cache is empty. 418 */ 419 public double getMinValueCached() { 420 return minValueCached; 421 } 422 423 /** 424 * @return Largest value currently in the cache or {@code Double.NaN} if the cache is empty. 425 */ 426 public double getMaxValueCached() { 427 return maxValueCached; 428 } 429 430 /** 431 * @return The index which the first dataset in this chache had in the original datafile. 432 */ 433 public int getMinDataFileIndex() { 434 try { 435 return dataSets.get(0).getDataFileIndex(); 436 } catch (IndexOutOfBoundsException e) { 437 return 0; 438 } catch (NullPointerException e) { 439 return 0; 440 } 441 } 442 443 /** 444 * @return The index that the last dataset in this chache had in the original datafile. 445 */ 446 public int getMaxDataFileIndex() { 447 try { 448 return dataSets.get(dataSets.size() - 1).getDataFileIndex(); 449 } catch (IndexOutOfBoundsException e) { 450 return 0; 451 } catch (NullPointerException e) { 452 return 0; 453 } 454 } 455 456 /** 457 * @param dataFileIndex An index in the original datafile. 458 * @return A dataset which was located at the specified index in the original data file, or {@code null} 459 * if there is no such dataset in the cache. 460 */ 461 public DataSet findDataSet(int dataFileIndex) { 462 int cacheIndex = Collections.binarySearch(dataSets, dataFileIndex); 463 if (cacheIndex < 0) 464 return null; 465 if (cacheIndex >= dataSets.size()) 466 return null; 467 DataSet ds = getDataSet(cacheIndex); 468 if (ds.getDataFileIndex() != dataFileIndex) 469 return null; 470 return ds; 471 } 472 473 /** 474 * Adds a specified dataset to this cache. 475 * @param ds A dataset. 476 */ 477 public void addDataSet(DataSet ds) { 478 479 // Ignore null datasets: 480 if (null == ds) 481 return; 482 483 // Add dataset according to the current cache mode: 484 boolean reallyAdded = false; 485 switch (currentMode) { 486 case CacheAllData: reallyAdded = addDataSet_AllDataMode(ds); 487 break; 488 case CacheTailData: reallyAdded = addDataSet_TailDataMode(ds); 489 break; 490 } 491 492 // If the dataset has not actually been cached, there is nothing more to so: 493 if (!reallyAdded) 494 return; 495 496 // Update min- and max-caches: 497 includeExtremeValues(ds); 498 499 // Notify listeners: 500 fireEvent(CacheEvent.CACHE_UpdatedData); 501 } 502 503 /** 504 * Updates the internal state of this cache and its data series to include the min and max 505 * values of the specified dataset. 506 * @param ds A dataset. 507 */ 508 private void includeExtremeValues(DataSet ds) { 509 510 for (int s = 0; s < dataSeries.size(); s++) { 511 512 double val = ds.getValue(s); 513 514 if (Double.isNaN(val) || Double.isInfinite(val)) 515 continue; 516 517 if (val < minValueCached || Double.isNaN(minValueCached)) 518 minValueCached = val; 519 520 if (val > maxValueCached || Double.isNaN(maxValueCached)) 521 maxValueCached = val; 522 523 dataSeries.get(s).includeExtremeValue(val); 524 } 525 } 526 527 /** 528 * Adds a dataset when cache is in {@code AllDataMode}. 529 * @param ds A dataset. 530 * @return Whether the dataset was actually added. 531 */ 532 private boolean addDataSet_AllDataMode(DataSet ds) { 533 534 if (0 != (ds.getDataFileIndex() % dispersalFactor)) 535 return false; 536 537 if (CACHE_SIZE > dataSets.size()) { 538 dataSets.add(ds); 539 return true; 540 } 541 542 increaseDispersalFactor(); 543 return addDataSet_AllDataMode(ds); 544 } 545 546 /** 547 * Increases the value which must divide datafile indices of all cached datasets without remainder. 548 * Datasets with the wrong datafile indices are removed from the cache and the cache indices are updated. 549 * This method is used to compact the cache in {@code AllDataMode}-mode. 550 */ 551 private void increaseDispersalFactor() { 552 553 // Remove every second dataset: 554 int i = 0; 555 boolean remove = false; 556 while (i < dataSets.size()) { 557 if (remove) 558 dataSets.remove(i); 559 else 560 i++; 561 remove = !remove; 562 } 563 564 if (dataSets instanceof ArrayList) 565 ((ArrayList) dataSets).ensureCapacity(CACHE_SIZE); 566 567 // Increase dispersal factor: 568 dispersalFactor *= 2; 569 570 // Rebuild the extreme values cache: 571 resetExtremeValues(); 572 for (DataSet ds : dataSets) 573 includeExtremeValues(ds); 574 } 575 576 /** 577 * Adds a dataset when cache is in {@code TailDataMode}. 578 * @param ds A dataset. 579 * @return {@code true}. 580 */ 581 private boolean addDataSet_TailDataMode(DataSet ds) { 582 583 if (CACHE_SIZE > dataSets.size()) { 584 dataSets.add(ds); 585 return true; 586 } 587 588 removeDatalistHead(); 589 return addDataSet_TailDataMode(ds); 590 } 591 592 /** 593 * Removes the oldest datasets in this cache. 594 * This method is used to compact the cache in {@code AllDataMode}-mode. 595 */ 596 private void removeDatalistHead() { 597 598 // Remove datasets wich were cached the longes time ago: 599 600 if (dataSets instanceof RemoveRangeArrayList) { 601 ((RemoveRangeArrayList) dataSets).removeRangeint(0, CACHE_SIZE - TAIL_BALANCE_SIZE); 602 603 } else { 604 while (TAIL_BALANCE_SIZE > dataSets.size()) 605 dataSets.remove(0); 606 } 607 608 if (dataSets instanceof ArrayList) 609 ((ArrayList) dataSets).ensureCapacity(CACHE_SIZE); 610 611 // Rebuild the extreme values cache: 612 resetExtremeValues(); 613 for (DataSet ds : dataSets) 614 includeExtremeValues(ds); 615 } 616 617 /** 618 * Caches info on the data file. 619 * @param info File info. 620 */ 621 public void addDataFileInfo(String info) { 622 this.dataFileInfo.add(info); 623 fireEvent(CacheEvent.CACHE_UpdatedDataFileInfo); 624 } 625 626 /** 627 * @return A list of all caches data file info strings. 628 */ 629 public List<String> listDataFileInfo() { 630 return Collections.unmodifiableList(dataFileInfo); 631 } 632 633 /** 634 * @return The data file info where all cached info strings are separated by new-lines and 635 * concatenated into a single string. 636 */ 637 public String getDataFileInfo() { 638 639 StringWriter s = new StringWriter(); 640 PrintWriter w = new PrintWriter(s); 641 for (String infoLine : dataFileInfo) 642 w.println(infoLine); 643 644 w.close(); 645 return s.toString(); 646 } 647 648 /** 649 * Objects of this class do not handle {@code eventProcessingFinished} notifications. 650 * 651 * @param event Ignored. 652 */ 653 public void eventProcessingFinished(Event<? extends EventType> event) { } 654 655 /** 656 * Objects of this class do not handle {@code eventProcessingException} notofications. 657 * 658 * @param event Ignored. 659 * @param exception Never actually thrown. 660 * @return {@code false}. 661 */ 662 public boolean eventProcessingException(Event<? extends EventType> event, EventProcessingException exception) { 663 return false; 664 } 665 666 667 /** 668 * Notifies the observers of a specified event. If this cache is currently in {@code delayEvents} 669 * mode, the observers are not notified and the event is cached. However, even in the 670 * {@code delayEvents}-mode, if the number of events delayed so far exceeds {@link #MAX_DELAYED_EVENTS}, 671 * all events delayed so far <em>are</em> fired - this ensured that listeners are brought up to date 672 * from time to time during very long bulk operations. 673 * @param eventType An event type. 674 */ 675 private void fireEvent(CacheEvent eventType) { 676 677 if (null == eventType) 678 return; 679 680 if (!delayEvents) { 681 raiseEvent(eventType); 682 return; 683 } 684 685 delayedEvents.add(eventType); 686 countDelayedEvents++; 687 if (countDelayedEvents > MAX_DELAYED_EVENTS) 688 raiseDelayedEvents(); 689 } 690 691 /** 692 * Notifies the observers of an event of a specified type. 693 * 694 * @param eventType Type of event to raise; 695 */ 696 private void raiseEvent(CacheEvent eventType) { 697 Event<CacheEvent> event = new Event<CacheEvent>(this, CacheEvent.class, eventType); 698 LiveGraph.application().eventManager().raiseEvent(event); 699 } 700 701 /** 702 * When this method is invoked the cache enters the {@code delayEvents}-mode; 703 * while in this mode events are not supplied to observers, instead they are cached 704 * and fired only when {@code fireDelayedEvents} is invoked. This is can be useful 705 * when the cache is modified several times in one go. In such case the notification 706 * of observers can be consolidated which might save processing similar events many times. 707 * 708 * @see #bulkOperationCompleted() 709 */ 710 public void bulkOperationStart() { 711 if (!delayedEvents.isEmpty()) 712 raiseDelayedEvents(); 713 delayEvents = true; 714 } 715 716 /** 717 * Ends the {@code delayEvents}-mode and returns in the normal observable mode; 718 * all events cached while in that mode are fired. However, each type of event 719 * is fired at most once. The order in which the events are fires is unspecified 720 * and might not correspond to the order in which the events actually occured. 721 */ 722 public void bulkOperationCompleted() { 723 if (!delayEvents) 724 return; 725 delayEvents = false; 726 raiseDelayedEvents(); 727 } 728 729 /** 730 * Raises all events delayed so far during a buld operation: 731 */ 732 private void raiseDelayedEvents() { 733 734 // We want to fire the events in this particular order: 735 736 if (delayedEvents.contains(CacheEvent.CACHE_ChangedMode)) 737 raiseEvent(CacheEvent.CACHE_ChangedMode); 738 739 if (delayedEvents.contains(CacheEvent.CACHE_UpdatedDataFileInfo)) 740 raiseEvent(CacheEvent.CACHE_UpdatedDataFileInfo); 741 742 if (delayedEvents.contains(CacheEvent.CACHE_UpdatedLabels)) 743 raiseEvent(CacheEvent.CACHE_UpdatedLabels); 744 745 if (delayedEvents.contains(CacheEvent.CACHE_UpdatedData)) 746 raiseEvent(CacheEvent.CACHE_UpdatedData); 747 748 // This is only required in case we forgot an event type: 749 for (CacheEvent eventType : delayedEvents) { 750 if (CacheEvent.CACHE_ChangedMode != eventType 751 && CacheEvent.CACHE_UpdatedDataFileInfo != eventType 752 && CacheEvent.CACHE_UpdatedLabels != eventType 753 && CacheEvent.CACHE_UpdatedData != eventType) { 754 raiseEvent(eventType); 755 } 756 } 757 758 delayedEvents.clear(); 759 countDelayedEvents = 0; 760 } 761 762 /** 763 * A {@code ArrayList} which publicly publishes its {@code removeRangeint} method. 764 * @param <E> Any class. 765 */ 766 private class RemoveRangeArrayList<E> extends ArrayList<E> { 767 public RemoveRangeArrayList() { super(); } 768 public RemoveRangeArrayList(int initialCapacity) { super(initialCapacity); } 769 public void removeRangeint(int fromIndex, int toIndex) { super.removeRange(fromIndex, toIndex); } 770 } // private class RemoveRangeArrayList<E> 771 772 /** 773 * A read-only iterator for data series labels. 774 */ 775 private class DataSeriesLabelIterator extends ReadOnlyIterator<String> { 776 private Iterator<DataSeries> iterator = null; 777 778 public DataSeriesLabelIterator(Iterator<DataSeries> iter) { 779 super(NullIterator.getSingeltonInstance(String.class)); 780 iterator = iter; 781 } 782 783 @Override 784 public boolean hasNext() { 785 return iterator.hasNext(); 786 } 787 788 @Override 789 public String next() { 790 return iterator.next().getLabel(); 791 } 792 793 @Override 794 public void remove() { 795 throw new UnsupportedOperationException("Cannot use this iterator to remove items."); 796 } 797 } // class DataSeriesLabelIterator 798 799 } // class DataCache