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 &quot;AS IS&quot;, 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