001    /* ============================================================
002     * JRobin : Pure java implementation of RRDTool's functionality
003     * ============================================================
004     *
005     * Project Info:  http://www.jrobin.org
006     * Project Lead:  Sasa Markovic (saxon@jrobin.org)
007     *
008     * Developers:    Sasa Markovic (saxon@jrobin.org)
009     *
010     *
011     * (C) Copyright 2003-2005, by Sasa Markovic.
012     *
013     * This library is free software; you can redistribute it and/or modify it under the terms
014     * of the GNU Lesser General Public License as published by the Free Software Foundation;
015     * either version 2.1 of the License, or (at your option) any later version.
016     *
017     * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
018     * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
019     * See the GNU Lesser General Public License for more details.
020     *
021     * You should have received a copy of the GNU Lesser General Public License along with this
022     * library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330,
023     * Boston, MA 02111-1307, USA.
024     */
025    package org.jrobin.data;
026    
027    import org.jrobin.core.RrdException;
028    import org.jrobin.core.Util;
029    
030    import java.util.Calendar;
031    import java.util.Date;
032    
033    /**
034     * Class used to interpolate datasource values from the collection of (timestamp, values)
035     * points. This class is suitable for linear interpolation only. <p>
036     * <p/>
037     * Interpolation algorithm returns different values based on the value passed to
038     * {@link #setInterpolationMethod(int) setInterpolationMethod()}. If not set, interpolation
039     * method defaults to standard linear interpolation ({@link #INTERPOLATE_LINEAR}).
040     * Interpolation method handles NaN datasource
041     * values gracefully.<p>
042     */
043    public class LinearInterpolator extends Plottable {
044            /**
045             * constant used to specify LEFT interpolation.
046             * See {@link #setInterpolationMethod(int) setInterpolationMethod()} for explanation.
047             */
048            public static final int INTERPOLATE_LEFT = 0;
049            /**
050             * constant used to specify RIGHT interpolation.
051             * See {@link #setInterpolationMethod(int) setInterpolationMethod()} for explanation.
052             */
053            public static final int INTERPOLATE_RIGHT = 1;
054            /**
055             * constant used to specify LINEAR interpolation (default interpolation method).
056             * See {@link #setInterpolationMethod(int) setInterpolationMethod()} for explanation.
057             */
058            public static final int INTERPOLATE_LINEAR = 2;
059            /**
060             * constant used to specify LINEAR REGRESSION as interpolation method.
061             * See {@link #setInterpolationMethod(int) setInterpolationMethod()} for explanation.
062             */
063            public static final int INTERPOLATE_REGRESSION = 3;
064    
065            private int lastIndexUsed = 0;
066            private int interpolationMethod = INTERPOLATE_LINEAR;
067    
068            private long[] timestamps;
069            private double[] values;
070    
071            // used only if INTERPOLATE_BESTFIT is specified
072            double b0 = Double.NaN, b1 = Double.NaN;
073    
074            /**
075             * Creates LinearInterpolator from arrays of timestamps and corresponding datasource values.
076             *
077             * @param timestamps timestamps in seconds
078             * @param values         corresponding datasource values
079             * @throws RrdException Thrown if supplied arrays do not contain at least two values, or if
080             *                      timestamps are not ordered, or array lengths are not equal.
081             */
082            public LinearInterpolator(long[] timestamps, double[] values) throws RrdException {
083                    this.timestamps = timestamps;
084                    this.values = values;
085                    validate();
086            }
087    
088            /**
089             * Creates LinearInterpolator from arrays of timestamps and corresponding datasource values.
090             *
091             * @param dates  Array of Date objects
092             * @param values corresponding datasource values
093             * @throws RrdException Thrown if supplied arrays do not contain at least two values, or if
094             *                      timestamps are not ordered, or array lengths are not equal.
095             */
096            public LinearInterpolator(Date[] dates, double[] values) throws RrdException {
097                    this.values = values;
098                    timestamps = new long[dates.length];
099                    for (int i = 0; i < dates.length; i++) {
100                            timestamps[i] = Util.getTimestamp(dates[i]);
101                    }
102                    validate();
103            }
104    
105            /**
106             * Creates LinearInterpolator from arrays of timestamps and corresponding datasource values.
107             *
108             * @param dates  array of GregorianCalendar objects
109             * @param values corresponding datasource values
110             * @throws RrdException Thrown if supplied arrays do not contain at least two values, or if
111             *                      timestamps are not ordered, or array lengths are not equal.
112             */
113            public LinearInterpolator(Calendar[] dates, double[] values) throws RrdException {
114                    this.values = values;
115                    timestamps = new long[dates.length];
116                    for (int i = 0; i < dates.length; i++) {
117                            timestamps[i] = Util.getTimestamp(dates[i]);
118                    }
119                    validate();
120            }
121    
122            private void validate() throws RrdException {
123                    boolean ok = true;
124                    if (timestamps.length != values.length || timestamps.length < 2) {
125                            ok = false;
126                    }
127                    for (int i = 0; i < timestamps.length - 1 && ok; i++) {
128                            if (timestamps[i] >= timestamps[i + 1]) {
129                                    ok = false;
130                            }
131                    }
132                    if (!ok) {
133                            throw new RrdException("Invalid plottable data supplied");
134                    }
135            }
136    
137            /**
138             * Sets interpolation method to be used. Suppose that we have two timestamp/value pairs:<br>
139             * <code>(t, 100)</code> and <code>(t + 100, 300)</code>. Here are the results interpolator
140             * returns for t + 50 seconds, for various <code>interpolationMethods</code>:<p>
141             * <ul>
142             * <li><code>INTERPOLATE_LEFT:   100</code>
143             * <li><code>INTERPOLATE_RIGHT:  300</code>
144             * <li><code>INTERPOLATE_LINEAR: 200</code>
145             * </ul>
146             * If not set, interpolation method defaults to <code>INTERPOLATE_LINEAR</code>.<p>
147             * <p/>
148             * The fourth available interpolation method is INTERPOLATE_REGRESSION. This method uses
149             * simple linear regression to interpolate supplied data with a simple straight line which does not
150             * necessarily pass through all data points. The slope of the best-fit line will be chosen so that the
151             * total square distance of real data points from from the best-fit line is at minimum.<p>
152             * <p/>
153             * The full explanation of this inteprolation method can be found
154             * <a href="http://www.tufts.edu/~gdallal/slr.htm">here</a>.<p>
155             *
156             * @param interpolationMethod Should be <code>INTERPOLATE_LEFT</code>,
157             *                            <code>INTERPOLATE_RIGHT</code>, <code>INTERPOLATE_LINEAR</code> or
158             *                            <code>INTERPOLATE_REGRESSION</code>. Any other value will be interpreted as
159             *                            INTERPOLATE_LINEAR (default).
160             */
161            public void setInterpolationMethod(int interpolationMethod) {
162                    switch (interpolationMethod) {
163                            case INTERPOLATE_REGRESSION:
164                                    calculateBestFitLine();
165                            case INTERPOLATE_LEFT:
166                            case INTERPOLATE_RIGHT:
167                            case INTERPOLATE_LINEAR:
168                                    this.interpolationMethod = interpolationMethod;
169                                    break;
170                            default:
171                                    this.interpolationMethod = INTERPOLATE_LINEAR;
172                    }
173            }
174    
175            private void calculateBestFitLine() {
176                    int count = timestamps.length, validCount = 0;
177                    double ts = 0.0, vs = 0.0;
178                    for (int i = 0; i < count; i++) {
179                            if (!Double.isNaN(values[i])) {
180                                    ts += timestamps[i];
181                                    vs += values[i];
182                                    validCount++;
183                            }
184                    }
185                    if (validCount <= 1) {
186                            // just one not-NaN point
187                            b0 = b1 = Double.NaN;
188                            return;
189                    }
190                    ts /= validCount;
191                    vs /= validCount;
192                    double s1 = 0, s2 = 0;
193                    for (int i = 0; i < count; i++) {
194                            if (!Double.isNaN(values[i])) {
195                                    double dt = timestamps[i] - ts;
196                                    double dv = values[i] - vs;
197                                    s1 += dt * dv;
198                                    s2 += dt * dt;
199                            }
200                    }
201                    b1 = s1 / s2;
202                    b0 = vs - b1 * ts;
203            }
204    
205            /**
206             * Method overriden from the base class. This method will be called by the framework. Call
207             * this method only if you need interpolated values in your code.
208             *
209             * @param timestamp timestamp in seconds
210             * @return inteprolated datasource value
211             */
212            public double getValue(long timestamp) {
213                    if (interpolationMethod == INTERPOLATE_REGRESSION) {
214                            return b0 + b1 * timestamp;
215                    }
216                    int count = timestamps.length;
217                    // check if out of range
218                    if (timestamp < timestamps[0] || timestamp > timestamps[count - 1]) {
219                            return Double.NaN;
220                    }
221                    // find matching segment
222                    int startIndex = lastIndexUsed;
223                    if (timestamp < timestamps[lastIndexUsed]) {
224                            // backward reading, shift to the first timestamp
225                            startIndex = 0;
226                    }
227                    for (int i = startIndex; i < count; i++) {
228                            if (timestamps[i] == timestamp) {
229                                    return values[i];
230                            }
231                            if (i < count - 1 && timestamps[i] < timestamp && timestamp < timestamps[i + 1]) {
232                                    // matching segment found
233                                    lastIndexUsed = i;
234                                    switch (interpolationMethod) {
235                                            case INTERPOLATE_LEFT:
236                                                    return values[i];
237                                            case INTERPOLATE_RIGHT:
238                                                    return values[i + 1];
239                                            case INTERPOLATE_LINEAR:
240                                                    double slope = (values[i + 1] - values[i]) /
241                                                                    (timestamps[i + 1] - timestamps[i]);
242                                                    return values[i] + slope * (timestamp - timestamps[i]);
243                                            default:
244                                                    return Double.NaN;
245                                    }
246                            }
247                    }
248                    // should not be here ever, but let's satisfy the compiler
249                    return Double.NaN;
250            }
251    }