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