001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2007, by Object Refinery Limited and Contributors.
006 *
007 * Project Info:  http://www.jfree.org/jfreechart/index.html
008 *
009 * This library is free software; you can redistribute it and/or modify it 
010 * under the terms of the GNU Lesser General Public License as published by 
011 * the Free Software Foundation; either version 2.1 of the License, or 
012 * (at your option) any later version.
013 *
014 * This library is distributed in the hope that it will be useful, but 
015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 
017 * License for more details.
018 *
019 * You should have received a copy of the GNU Lesser General Public
020 * License along with this library; if not, write to the Free Software
021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, 
022 * USA.  
023 *
024 * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 
025 * in the United States and other countries.]
026 *
027 * ---------------------
028 * XYSplineRenderer.java
029 * ---------------------
030 * (C) Copyright 2007, by Klaus Rheinwald and Contributors.
031 *
032 * Original Author:  Klaus Rheinwald;
033 * Contributor(s):   Tobias von Petersdorff (tvp@math.umd.edu, 
034 *                       http://www.wam.umd.edu/~petersd/);
035 *                   David Gilbert (for Object Refinery Limited);
036 *
037 * Changes:
038 * --------
039 * 25-Jul-2007 : Version 1, contributed by Klaus Rheinwald (DG);
040 * 03-Aug-2007 : Added new constructor (KR);
041 * 25-Oct-2007 : Prevent duplicate control points (KR);
042 *
043 */
044
045
046package org.jfree.chart.renderer.xy;
047
048import java.awt.Graphics2D;
049import java.awt.geom.Rectangle2D;
050import java.util.Vector;
051
052import org.jfree.chart.axis.ValueAxis;
053import org.jfree.chart.event.RendererChangeEvent;
054import org.jfree.chart.plot.PlotOrientation;
055import org.jfree.chart.plot.PlotRenderingInfo;
056import org.jfree.chart.plot.XYPlot;
057import org.jfree.data.xy.XYDataset;
058import org.jfree.ui.RectangleEdge;
059
060
061/**
062 * A renderer that connects data points with natural cubic splines and/or 
063 * draws shapes at each data point.  This renderer is designed for use with 
064 * the {@link XYPlot} class.
065 * 
066 * @since 1.0.7
067 */
068public class XYSplineRenderer extends XYLineAndShapeRenderer {
069
070    /**
071     * To collect data points for later splining.
072     */
073    private Vector points;
074
075    /**
076     * Resolution of splines (number of line segments between points)
077     */
078    private int precision;
079
080    /**
081     * Creates a new instance with the 'precision' attribute defaulting to 
082     * 5.
083     */
084    public XYSplineRenderer() {
085        this(5);
086    }
087    
088    /**
089     * Creates a new renderer with the specified precision.
090     * 
091     * @param precision  the number of points between data items.
092     */
093    public XYSplineRenderer(int precision) {
094        super();
095        if (precision <= 0) {
096            throw new IllegalArgumentException("Requires precision > 0.");
097        }
098        this.precision = precision;
099    }    
100    
101    /**
102     * Get the resolution of splines.
103     *
104     * @return Number of line segments between points.
105     * 
106     * @see #setPrecision(int)
107     */
108    public int getPrecision() {
109        return this.precision;
110    }
111
112    /**
113     * Set the resolution of splines and sends a {@link RendererChangeEvent}
114     * to all registered listeners.
115     *
116     * @param p  number of line segments between points (must be > 0).
117     * 
118     * @see #getPrecision()
119     */
120    public void setPrecision(int p) {
121        if (p <= 0) {
122            throw new IllegalArgumentException("Requires p > 0.");
123        }
124        this.precision = p;
125        notifyListeners(new RendererChangeEvent(this));
126    }
127
128    /**
129     * Initialises the renderer.
130     * <P>
131     * This method will be called before the first item is rendered, giving the
132     * renderer an opportunity to initialise any state information it wants to
133     * maintain.  The renderer can do nothing if it chooses.
134     *
135     * @param g2  the graphics device.
136     * @param dataArea  the area inside the axes.
137     * @param plot  the plot.
138     * @param data  the data.
139     * @param info  an optional info collection object to return data back to
140     *              the caller.
141     *
142     * @return The renderer state.
143     */
144    public XYItemRendererState initialise(Graphics2D g2, Rectangle2D dataArea, 
145            XYPlot plot, XYDataset data, PlotRenderingInfo info) {
146
147        State state = (State) super.initialise(g2, dataArea, plot, data, info);
148        state.setProcessVisibleItemsOnly(false);
149        this.points = new Vector();
150        setDrawSeriesLineAsPath(true);
151        return state;
152    }
153
154    /**
155     * Draws the item (first pass). This method draws the lines
156     * connecting the items. Instead of drawing separate lines,
157     * a GeneralPath is constructed and drawn at the end of
158     * the series painting.
159     *
160     * @param g2  the graphics device.
161     * @param state  the renderer state.
162     * @param plot  the plot (can be used to obtain standard color information
163     *              etc).
164     * @param dataset  the dataset.
165     * @param pass  the pass.
166     * @param series  the series index (zero-based).
167     * @param item  the item index (zero-based).
168     * @param domainAxis  the domain axis.
169     * @param rangeAxis  the range axis.
170     * @param dataArea  the area within which the data is being drawn.
171     */
172    protected void drawPrimaryLineAsPath(XYItemRendererState state, 
173            Graphics2D g2, XYPlot plot, XYDataset dataset, int pass, 
174            int series, int item, ValueAxis domainAxis, ValueAxis rangeAxis, 
175            Rectangle2D dataArea) {
176
177        RectangleEdge xAxisLocation = plot.getDomainAxisEdge();
178        RectangleEdge yAxisLocation = plot.getRangeAxisEdge();
179
180        // get the data points
181        double x1 = dataset.getXValue(series, item);
182        double y1 = dataset.getYValue(series, item);
183        double transX1 = domainAxis.valueToJava2D(x1, dataArea, xAxisLocation);
184        double transY1 = rangeAxis.valueToJava2D(y1, dataArea, yAxisLocation);
185
186        // collect points
187        if (!Double.isNaN(transX1) && !Double.isNaN(transY1)) {
188            ControlPoint p = new ControlPoint(plot.getOrientation() 
189                                == PlotOrientation.HORIZONTAL ? (float) transY1 
190                                : (float) transX1, plot.getOrientation() 
191                                == PlotOrientation.HORIZONTAL ? (float) transX1 
192                                        : (float) transY1);
193            if (!this.points.contains(p)) {
194                this.points.add(p);
195            }
196        }
197        if (item == dataset.getItemCount(series) - 1) {
198            State s = (State) state;
199            // construct path
200            if (this.points.size() > 1) {
201                // we need at least two points to draw something
202                ControlPoint cp0 = (ControlPoint) this.points.get(0);
203                s.seriesPath.moveTo(cp0.x, cp0.y);
204                if (this.points.size() == 2) {
205                    // we need at least 3 points to spline. Draw simple line 
206                    // for two points
207                    ControlPoint cp1 = (ControlPoint) this.points.get(1);
208                    s.seriesPath.lineTo(cp1.x, cp1.y);
209                } 
210                else {
211                    // construct spline
212                    int np = this.points.size(); // number of points
213                    float[] d = new float[np]; // Newton form coefficients
214                    float[] x = new float[np]; // x-coordinates of nodes
215                    float y;
216                    float t;
217                    float oldy = 0;
218                    float oldt = 0;
219
220                    float[] a = new float[np];
221                    float t1;
222                    float t2;
223                    float[] h = new float[np];
224
225                    for (int i = 0; i < np; i++) {
226                        ControlPoint cpi = (ControlPoint) this.points.get(i);
227                        x[i] = cpi.x;
228                        d[i] = cpi.y;
229                    }
230
231                    for (int i = 1; i <= np - 1; i++) {
232                        h[i] = x[i] - x[i - 1];
233                    }
234                    float[] sub = new float[np - 1];
235                    float[] diag = new float[np - 1];
236                    float[] sup = new float[np - 1];
237
238                    for (int i = 1; i <= np - 2; i++) {
239                        diag[i] = (h[i] + h[i + 1]) / 3;
240                        sup[i] = h[i + 1] / 6;
241                        sub[i] = h[i] / 6;
242                        a[i] = (d[i + 1] - d[i]) / h[i + 1] 
243                                   - (d[i] - d[i - 1]) / h[i];
244                    }
245                    solveTridiag(sub, diag, sup, a, np - 2);
246
247                    // note that a[0]=a[np-1]=0
248                    // draw
249                    oldt = x[0];
250                    oldy = d[0];
251                    s.seriesPath.moveTo(oldt, oldy);
252                    for (int i = 1; i <= np - 1; i++) {
253                        // loop over intervals between nodes
254                        for (int j = 1; j <= this.precision; j++) {
255                            t1 = (h[i] * j) / this.precision;
256                            t2 = h[i] - t1;
257                            y = ((-a[i - 1] / 6 * (t2 + h[i]) * t1 + d[i - 1]) 
258                                    * t2 + (-a[i] / 6 * (t1 + h[i]) * t2 
259                                    + d[i]) * t1) / h[i];
260                            t = x[i - 1] + t1;
261                            s.seriesPath.lineTo(t, y);
262                            oldt = t;
263                            oldy = y;
264                        }
265                    }
266                }
267                // draw path
268                drawFirstPassShape(g2, pass, series, item, s.seriesPath);
269            }
270
271            // reset points vector
272            this.points = new Vector();
273        }
274    }
275
276    /**
277     * Document me!
278     * 
279     * @param sub
280     * @param diag
281     * @param sup
282     * @param b
283     * @param n
284     */
285    private void solveTridiag(float[] sub, float[] diag, float[] sup, 
286            float[] b, int n) {
287/*      solve linear system with tridiagonal n by n matrix a
288        using Gaussian elimination *without* pivoting
289        where   a(i,i-1) = sub[i]  for 2<=i<=n
290        a(i,i)   = diag[i] for 1<=i<=n
291        a(i,i+1) = sup[i]  for 1<=i<=n-1
292        (the values sub[1], sup[n] are ignored)
293        right hand side vector b[1:n] is overwritten with solution
294        NOTE: 1...n is used in all arrays, 0 is unused */
295        int i;
296/*                  factorization and forward substitution */
297        for (i = 2; i <= n; i++) {
298            sub[i] = sub[i] / diag[i - 1];
299            diag[i] = diag[i] - sub[i] * sup[i - 1];
300            b[i] = b[i] - sub[i] * b[i - 1];
301        }
302        b[n] = b[n] / diag[n];
303        for (i = n - 1; i >= 1; i--) {
304            b[i] = (b[i] - sup[i] * b[i + 1]) / diag[i];
305        }
306    }
307    
308    /**
309     * Tests this renderer for equality with an arbitrary object.
310     * 
311     * @param obj  the object (<code>null</code> permitted).
312     * 
313     * @return A boolean.
314     */
315    public boolean equals(Object obj) {
316        if (obj == this) {
317            return true;
318        }
319        if (!(obj instanceof XYSplineRenderer)) {
320            return false;
321        }
322        XYSplineRenderer that = (XYSplineRenderer) obj;
323        if (this.precision != that.precision) {
324            return false;
325        }
326        return super.equals(obj);
327    }
328
329    /**
330     * Represents a control point.
331     */
332    class ControlPoint {
333        
334        /** The x-coordinate. */
335        public float x;
336        
337        /** The y-coordinate. */
338        public float y;
339
340        /**
341         * Creates a new control point.
342         * 
343         * @param x  the x-coordinate.
344         * @param y  the y-coordinate.
345         */
346        public ControlPoint(float x, float y) {
347            this.x = x;
348            this.y = y;
349        }
350
351        /**
352         * Tests this point for equality with an arbitrary object.
353         * 
354         * @param obj  the object (<code>null</code> permitted.
355         * 
356         * @return A boolean.
357         */
358        public boolean equals(Object obj) {
359            if (obj == this) {
360                return true;
361            }
362            if (!(obj instanceof ControlPoint)) {
363                return false;
364            }
365            ControlPoint that = (ControlPoint) obj;
366            if (this.x != that.x) {
367                return false;
368            }
369            /*&& y == ((ControlPoint) obj).y*/;
370            return true;
371        }
372
373    }
374}