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