/* =========================================================== * JFreeChart : a free chart library for the Java(tm) platform * =========================================================== * * (C) Copyright 2000-2014, by Object Refinery Limited and Contributors. * * Project Info: http://www.jfree.org/jfreechart/index.html * * This library is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 2.1 of the License, or * (at your option) any later version. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public * License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, * USA. * * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. * Other names may be trademarks of their respective owners.] * * -------------------------------- * DynamicTimeSeriesCollection.java * -------------------------------- * (C) Copyright 2002-2014, by I. H. Thomae and Contributors. * * Original Author: I. H. Thomae (ithomae@ists.dartmouth.edu); * Contributor(s): David Gilbert (for Object Refinery Limited); * Ricardo JL Rufino (patch #310); * * Changes * ------- * 22-Nov-2002 : Initial version completed * Jan 2003 : Optimized advanceTime(), added implemnt'n of RangeInfo intfc * (using cached values for min, max, and range); also added * getOldestIndex() and getNewestIndex() ftns so client classes * can use this class as the master "index authority". * 22-Jan-2003 : Made this class stand on its own, rather than extending * class FastTimeSeriesCollection * 31-Jan-2003 : Changed TimePeriod --> RegularTimePeriod (DG); * 13-Mar-2003 : Moved to com.jrefinery.data.time package (DG); * 29-Apr-2003 : Added small change to appendData method, from Irv Thomae (DG); * 19-Sep-2003 : Added new appendData method, from Irv Thomae (DG); * 05-May-2004 : Now extends AbstractIntervalXYDataset. This also required a * change to the return type of the getY() method - I'm slightly * unsure of the implications of this, so it might require some * further amendment (DG); * 15-Jul-2004 : Switched getX() with getXValue() and getY() with * getYValue() (DG); * 11-Jan-2004 : Removed deprecated code in preparation for the 1.0.0 * release (DG); * 02-Feb-2007 : Removed author tags all over JFreeChart sources (DG); * */ package org.jfree.data.time; import java.util.Calendar; import java.util.TimeZone; import org.jfree.data.DomainInfo; import org.jfree.data.Range; import org.jfree.data.RangeInfo; import org.jfree.data.general.SeriesChangeEvent; import org.jfree.data.xy.AbstractIntervalXYDataset; import org.jfree.data.xy.IntervalXYDataset; /** * A dynamic dataset. * <p> * Like FastTimeSeriesCollection, this class is a functional replacement * for JFreeChart's TimeSeriesCollection _and_ TimeSeries classes. * FastTimeSeriesCollection is appropriate for a fixed time range; for * real-time applications this subclass adds the ability to append new * data and discard the oldest. * In this class, the arrays used in FastTimeSeriesCollection become FIFO's. * NOTE:As presented here, all data is assumed >= 0, an assumption which is * embodied only in methods associated with interface RangeInfo. */ public class DynamicTimeSeriesCollection extends AbstractIntervalXYDataset implements IntervalXYDataset, DomainInfo, RangeInfo { /** * Useful constant for controlling the x-value returned for a time * period. */ public static final int START = 0; /** * Useful constant for controlling the x-value returned for a time period. */ public static final int MIDDLE = 1; /** * Useful constant for controlling the x-value returned for a time period. */ public static final int END = 2; /** The maximum number of items for each series (can be overridden). */ private int maximumItemCount = 2000; // an arbitrary safe default value /** The history count. */ protected int historyCount; /** Storage for the series keys. */ private Comparable[] seriesKeys; /** The time period class - barely used, and could be removed (DG). */ private Class timePeriodClass = Minute.class; // default value; /** Storage for the x-values. */ protected RegularTimePeriod[] pointsInTime; /** The number of series. */ private int seriesCount; /** * A wrapper for a fixed array of float values. */ protected class ValueSequence { /** Storage for the float values. */ float[] dataPoints; /** * Default constructor: */ public ValueSequence() { this(DynamicTimeSeriesCollection.this.maximumItemCount); } /** * Creates a sequence with the specified length. * * @param length the length. */ public ValueSequence(int length) { this.dataPoints = new float[length]; for (int i = 0; i < length; i++) { this.dataPoints[i] = 0.0f; } } /** * Enters data into the storage array. * * @param index the index. * @param value the value. */ public void enterData(int index, float value) { this.dataPoints[index] = value; } /** * Returns a value from the storage array. * * @param index the index. * * @return The value. */ public float getData(int index) { return this.dataPoints[index]; } } /** An array for storing the objects that represent each series. */ protected ValueSequence[] valueHistory; /** A working calendar (to recycle) */ protected Calendar workingCalendar; /** * The position within a time period to return as the x-value (START, * MIDDLE or END). */ private int position; /** * A flag that indicates that the domain is 'points in time'. If this flag * is true, only the x-value is used to determine the range of values in * the domain, the start and end x-values are ignored. */ private boolean domainIsPointsInTime; /** index for mapping: points to the oldest valid time and data. */ private int oldestAt; // as a class variable, initializes == 0 /** Index of the newest data item. */ private int newestAt; // cached values used for interface DomainInfo: /** the # of msec by which time advances. */ private long deltaTime; /** Cached domain start (for use by DomainInfo). */ private Long domainStart; /** Cached domain end (for use by DomainInfo). */ private Long domainEnd; /** Cached domain range (for use by DomainInfo). */ private Range domainRange; // Cached values used for interface RangeInfo: (note minValue pinned at 0) // A single set of extrema covers the entire SeriesCollection /** The minimum value. */ private Float minValue = 0.0f; /** The maximum value. */ private Float maxValue = null; /** The value range. */ private Range valueRange; // autoinit's to null. /** * Constructs a dataset with capacity for N series, tied to default * timezone. * * @param nSeries the number of series to be accommodated. * @param nMoments the number of TimePeriods to be spanned. */ public DynamicTimeSeriesCollection(int nSeries, int nMoments) { this(nSeries, nMoments, new Millisecond(), TimeZone.getDefault()); this.newestAt = nMoments - 1; } /** * Constructs an empty dataset, tied to a specific timezone. * * @param nSeries the number of series to be accommodated * @param nMoments the number of TimePeriods to be spanned * @param zone the timezone. */ public DynamicTimeSeriesCollection(int nSeries, int nMoments, TimeZone zone) { this(nSeries, nMoments, new Millisecond(), zone); this.newestAt = nMoments - 1; } /** * Creates a new dataset. * * @param nSeries the number of series. * @param nMoments the number of items per series. * @param timeSample a time period sample. */ public DynamicTimeSeriesCollection(int nSeries, int nMoments, RegularTimePeriod timeSample) { this(nSeries, nMoments, timeSample, TimeZone.getDefault()); } /** * Creates a new dataset. * * @param nSeries the number of series. * @param nMoments the number of items per series. * @param timeSample a time period sample. * @param zone the time zone. */ public DynamicTimeSeriesCollection(int nSeries, int nMoments, RegularTimePeriod timeSample, TimeZone zone) { // the first initialization must precede creation of the ValueSet array: this.maximumItemCount = nMoments; // establishes length of each array this.historyCount = nMoments; this.seriesKeys = new Comparable[nSeries]; // initialize the members of "seriesNames" array so they won't be null: for (int i = 0; i < nSeries; i++) { this.seriesKeys[i] = ""; } this.newestAt = nMoments - 1; this.valueHistory = new ValueSequence[nSeries]; this.timePeriodClass = timeSample.getClass(); /// Expand the following for all defined TimePeriods: if (this.timePeriodClass == Millisecond.class) { this.pointsInTime = new Millisecond[nMoments]; } else if (this.timePeriodClass == Second.class) { this.pointsInTime = new Second[nMoments]; } else if (this.timePeriodClass == Minute.class) { this.pointsInTime = new Minute[nMoments]; } else if (this.timePeriodClass == Hour.class) { this.pointsInTime = new Hour[nMoments]; } /// .. etc.... this.workingCalendar = Calendar.getInstance(zone); this.position = START; this.domainIsPointsInTime = true; } /** * Fill the pointsInTime with times using TimePeriod.next(): * Will silently return if the time array was already populated. * * Also computes the data cached for later use by * methods implementing the DomainInfo interface: * * @param start the start. * * @return ??. */ public synchronized long setTimeBase(RegularTimePeriod start) { if (this.pointsInTime[0] == null) { this.pointsInTime[0] = start; for (int i = 1; i < this.historyCount; i++) { this.pointsInTime[i] = this.pointsInTime[i - 1].next(); } } long oldestL = this.pointsInTime[0].getFirstMillisecond( this.workingCalendar); long nextL = this.pointsInTime[1].getFirstMillisecond( this.workingCalendar); this.deltaTime = nextL - oldestL; this.oldestAt = 0; this.newestAt = this.historyCount - 1; findDomainLimits(); return this.deltaTime; } /** * Finds the domain limits. Note: this doesn't need to be synchronized * because it's called from within another method that already is. */ protected void findDomainLimits() { long startL = getOldestTime().getFirstMillisecond(this.workingCalendar); long endL; if (this.domainIsPointsInTime) { endL = getNewestTime().getFirstMillisecond(this.workingCalendar); } else { endL = getNewestTime().getLastMillisecond(this.workingCalendar); } this.domainStart = startL; this.domainEnd = endL; this.domainRange = new Range(startL, endL); } /** * Returns the x position type (START, MIDDLE or END). * * @return The x position type. */ public int getPosition() { return this.position; } /** * Sets the x position type (START, MIDDLE or END). * * @param position The x position type. */ public void setPosition(int position) { this.position = position; } /** * Adds a series to the dataset. Only the y-values are supplied, the * x-values are specified elsewhere. * * @param values the y-values. * @param seriesNumber the series index (zero-based). * @param seriesKey the series key. * * Use this as-is during setup only, or add the synchronized keyword around * the copy loop. */ public void addSeries(float[] values, int seriesNumber, Comparable seriesKey) { invalidateRangeInfo(); int i; if (values == null) { throw new IllegalArgumentException("TimeSeriesDataset.addSeries(): " + "cannot add null array of values."); } if (seriesNumber >= this.valueHistory.length) { throw new IllegalArgumentException("TimeSeriesDataset.addSeries(): " + "cannot add more series than specified in c'tor"); } if (this.valueHistory[seriesNumber] == null) { this.valueHistory[seriesNumber] = new ValueSequence(this.historyCount); this.seriesCount++; } // But if that series array already exists, just overwrite its contents // Avoid IndexOutOfBoundsException: int srcLength = values.length; int copyLength = this.historyCount; boolean fillNeeded = false; if (srcLength < this.historyCount) { fillNeeded = true; copyLength = srcLength; } //{ for (i = 0; i < copyLength; i++) { // deep copy from values[], caller // can safely discard that array this.valueHistory[seriesNumber].enterData(i, values[i]); } if (fillNeeded) { for (i = copyLength; i < this.historyCount; i++) { this.valueHistory[seriesNumber].enterData(i, 0.0f); } } //} if (seriesKey != null) { this.seriesKeys[seriesNumber] = seriesKey; } fireSeriesChanged(); } /** * Sets the name of a series. If planning to add values individually. * * @param seriesNumber the series. * @param key the new key. */ public void setSeriesKey(int seriesNumber, Comparable key) { this.seriesKeys[seriesNumber] = key; } /** * Adds a value to a series. * * @param seriesNumber the series index. * @param index ??. * @param value the value. */ public void addValue(int seriesNumber, int index, float value) { invalidateRangeInfo(); if (seriesNumber >= this.valueHistory.length) { throw new IllegalArgumentException( "TimeSeriesDataset.addValue(): series #" + seriesNumber + "unspecified in c'tor" ); } if (this.valueHistory[seriesNumber] == null) { this.valueHistory[seriesNumber] = new ValueSequence(this.historyCount); this.seriesCount++; } // But if that series array already exists, just overwrite its contents //synchronized(this) //{ this.valueHistory[seriesNumber].enterData(index, value); //} fireSeriesChanged(); } /** * Returns the number of series in the collection. * * @return The series count. */ @Override public int getSeriesCount() { return this.seriesCount; } /** * Returns the number of items in a series. * <p> * For this implementation, all series have the same number of items. * * @param series the series index (zero-based). * * @return The item count. */ @Override public int getItemCount(int series) { // all arrays equal length, // so ignore argument: return this.historyCount; } // Methods for managing the FIFO's: /** * Re-map an index, for use in retrieving data. * * @param toFetch the index. * * @return The translated index. */ protected int translateGet(int toFetch) { if (this.oldestAt == 0) { return toFetch; // no translation needed } // else [implicit here] int newIndex = toFetch + this.oldestAt; if (newIndex >= this.historyCount) { newIndex -= this.historyCount; } return newIndex; } /** * Returns the actual index to a time offset by "delta" from newestAt. * * @param delta the delta. * * @return The offset. */ public int offsetFromNewest(int delta) { return wrapOffset(this.newestAt + delta); } /** * ?? * * @param delta ?? * * @return The offset. */ public int offsetFromOldest(int delta) { return wrapOffset(this.oldestAt + delta); } /** * ?? * * @param protoIndex the index. * * @return The offset. */ protected int wrapOffset(int protoIndex) { int tmp = protoIndex; if (tmp >= this.historyCount) { tmp -= this.historyCount; } else if (tmp < 0) { tmp += this.historyCount; } return tmp; } /** * Adjust the array offset as needed when a new time-period is added: * Increments the indices "oldestAt" and "newestAt", mod(array length), * zeroes the series values at newestAt, returns the new TimePeriod. * * @return The new time period. */ public synchronized RegularTimePeriod advanceTime() { RegularTimePeriod nextInstant = this.pointsInTime[this.newestAt].next(); this.newestAt = this.oldestAt; // newestAt takes value previously held // by oldestAT /*** * The next 10 lines or so should be expanded if data can be negative ***/ // if the oldest data contained a maximum Y-value, invalidate the stored // Y-max and Y-range data: boolean extremaChanged = false; float oldMax = 0.0f; if (this.maxValue != null) { oldMax = this.maxValue; } for (int s = 0; s < getSeriesCount(); s++) { if (this.valueHistory[s].getData(this.oldestAt) == oldMax) { extremaChanged = true; } if (extremaChanged) { break; } } /*** If data can be < 0, add code here to check the minimum **/ if (extremaChanged) { invalidateRangeInfo(); } // wipe the next (about to be used) set of data slots float wiper = (float) 0.0; for (int s = 0; s < getSeriesCount(); s++) { this.valueHistory[s].enterData(this.newestAt, wiper); } // Update the array of TimePeriods: this.pointsInTime[this.newestAt] = nextInstant; // Now advance "oldestAt", wrapping at end of the array this.oldestAt++; if (this.oldestAt >= this.historyCount) { this.oldestAt = 0; } // Update the domain limits: long startL = this.domainStart; //(time is kept in msec) this.domainStart = startL + this.deltaTime; long endL = this.domainEnd; this.domainEnd = endL + this.deltaTime; this.domainRange = new Range(startL, endL); fireSeriesChanged(); return nextInstant; } // If data can be < 0, the next 2 methods should be modified /** * Invalidates the range info. */ public void invalidateRangeInfo() { this.maxValue = null; this.valueRange = null; } /** * Returns the maximum value. * * @return The maximum value. */ protected double findMaxValue() { double max = 0.0f; for (int s = 0; s < getSeriesCount(); s++) { for (int i = 0; i < this.historyCount; i++) { double tmp = getYValue(s, i); if (tmp > max) { max = tmp; } } } return max; } /** End, positive-data-only code **/ /** * Returns the index of the oldest data item. * * @return The index. */ public int getOldestIndex() { return this.oldestAt; } /** * Returns the index of the newest data item. * * @return The index. */ public int getNewestIndex() { return this.newestAt; } // appendData() writes new data at the index position given by newestAt/ // When adding new data dynamically, use advanceTime(), followed by this: /** * Appends new data. * * @param newData the data. */ public void appendData(float[] newData) { int nDataPoints = newData.length; if (nDataPoints > this.valueHistory.length) { throw new IllegalArgumentException( "More data than series to put them in"); } int s; // index to select the "series" for (s = 0; s < nDataPoints; s++) { // check whether the "valueHistory" array member exists; if not, // create them: if (this.valueHistory[s] == null) { this.valueHistory[s] = new ValueSequence(this.historyCount); } this.valueHistory[s].enterData(this.newestAt, newData[s]); } fireSeriesChanged(); } /** * Appends data at specified index, for loading up with data from file(s). * * @param newData the data * @param insertionIndex the index value at which to put it * @param refresh value of n in "refresh the display on every nth call" * (ignored if <= 0) */ public void appendData(float[] newData, int insertionIndex, int refresh) { int nDataPoints = newData.length; if (nDataPoints > this.valueHistory.length) { throw new IllegalArgumentException( "More data than series to put them in"); } for (int s = 0; s < nDataPoints; s++) { if (this.valueHistory[s] == null) { this.valueHistory[s] = new ValueSequence(this.historyCount); } this.valueHistory[s].enterData(insertionIndex, newData[s]); } if (refresh > 0) { insertionIndex++; if (insertionIndex % refresh == 0) { fireSeriesChanged(); } } } /** * Returns the newest time. * * @return The newest time. */ public RegularTimePeriod getNewestTime() { return this.pointsInTime[this.newestAt]; } /** * Returns the oldest time. * * @return The oldest time. */ public RegularTimePeriod getOldestTime() { return this.pointsInTime[this.oldestAt]; } /** * Returns the x-value. * * @param series the series index (zero-based). * @param item the item index (zero-based). * * @return The value. */ // getXxx() ftns can ignore the "series" argument: // Don't synchronize this!! Instead, synchronize the loop that calls it. @Override public Number getX(int series, int item) { RegularTimePeriod tp = this.pointsInTime[translateGet(item)]; return getX(tp); } /** * Returns the y-value. * * @param series the series index (zero-based). * @param item the item index (zero-based). * * @return The value. */ @Override public double getYValue(int series, int item) { // Don't synchronize this!! // Instead, synchronize the loop that calls it. ValueSequence values = this.valueHistory[series]; return values.getData(translateGet(item)); } /** * Returns the y-value. * * @param series the series index (zero-based). * @param item the item index (zero-based). * * @return The value. */ @Override public Number getY(int series, int item) { return new Float(getYValue(series, item)); } /** * Returns the start x-value. * * @param series the series index (zero-based). * @param item the item index (zero-based). * * @return The value. */ @Override public Number getStartX(int series, int item) { RegularTimePeriod tp = this.pointsInTime[translateGet(item)]; return tp.getFirstMillisecond(this.workingCalendar); } /** * Returns the end x-value. * * @param series the series index (zero-based). * @param item the item index (zero-based). * * @return The value. */ @Override public Number getEndX(int series, int item) { RegularTimePeriod tp = this.pointsInTime[translateGet(item)]; return tp.getLastMillisecond(this.workingCalendar); } /** * Returns the start y-value. * * @param series the series index (zero-based). * @param item the item index (zero-based). * * @return The value. */ @Override public Number getStartY(int series, int item) { return getY(series, item); } /** * Returns the end y-value. * * @param series the series index (zero-based). * @param item the item index (zero-based). * * @return The value. */ @Override public Number getEndY(int series, int item) { return getY(series, item); } /* // "Extras" found useful when analyzing/verifying class behavior: public Number getUntranslatedXValue(int series, int item) { return super.getXValue(series, item); } public float getUntranslatedY(int series, int item) { return super.getY(series, item); } */ /** * Returns the key for a series. * * @param series the series index (zero-based). * * @return The key. */ @Override public Comparable getSeriesKey(int series) { return this.seriesKeys[series]; } /** * Sends a {@link SeriesChangeEvent} to all registered listeners. */ protected void fireSeriesChanged() { seriesChanged(new SeriesChangeEvent(this)); } // The next 3 functions override the base-class implementation of // the DomainInfo interface. Using saved limits (updated by // each updateTime() call), improves performance. // /** * Returns the minimum x-value in the dataset. * * @param includeInterval a flag that determines whether or not the * x-interval is taken into account. * * @return The minimum value. */ @Override public double getDomainLowerBound(boolean includeInterval) { return this.domainStart.doubleValue(); // a Long kept updated by advanceTime() } /** * Returns the maximum x-value in the dataset. * * @param includeInterval a flag that determines whether or not the * x-interval is taken into account. * * @return The maximum value. */ @Override public double getDomainUpperBound(boolean includeInterval) { return this.domainEnd.doubleValue(); // a Long kept updated by advanceTime() } /** * Returns the range of the values in this dataset's domain. * * @param includeInterval a flag that determines whether or not the * x-interval is taken into account. * * @return The range. */ @Override public Range getDomainBounds(boolean includeInterval) { if (this.domainRange == null) { findDomainLimits(); } return this.domainRange; } /** * Returns the x-value for a time period. * * @param period the period. * * @return The x-value. */ private long getX(RegularTimePeriod period) { switch (this.position) { case (START) : return period.getFirstMillisecond(this.workingCalendar); case (MIDDLE) : return period.getMiddleMillisecond(this.workingCalendar); case (END) : return period.getLastMillisecond(this.workingCalendar); default: return period.getMiddleMillisecond(this.workingCalendar); } } // The next 3 functions implement the RangeInfo interface. // Using saved limits (updated by each updateTime() call) significantly // improves performance. WARNING: this code makes the simplifying // assumption that data is never negative. Expand as needed for the // general case. /** * Returns the minimum range value. * * @param includeInterval a flag that determines whether or not the * y-interval is taken into account. * * @return The minimum range value. */ @Override public double getRangeLowerBound(boolean includeInterval) { double result = Double.NaN; if (this.minValue != null) { result = this.minValue.doubleValue(); } return result; } /** * Returns the maximum range value. * * @param includeInterval a flag that determines whether or not the * y-interval is taken into account. * * @return The maximum range value. */ @Override public double getRangeUpperBound(boolean includeInterval) { double result = Double.NaN; if (this.maxValue != null) { result = this.maxValue.doubleValue(); } return result; } /** * Returns the value range. * * @param includeInterval a flag that determines whether or not the * y-interval is taken into account. * * @return The range. */ @Override public Range getRangeBounds(boolean includeInterval) { if (this.valueRange == null) { double max = getRangeUpperBound(includeInterval); this.valueRange = new Range(0.0, max); } return this.valueRange; } }