001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2008, 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, 2008, 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    package org.jfree.chart.renderer.xy;
046    
047    import java.awt.Graphics2D;
048    import java.awt.geom.Rectangle2D;
049    import java.util.Vector;
050    
051    import org.jfree.chart.axis.ValueAxis;
052    import org.jfree.chart.event.RendererChangeEvent;
053    import org.jfree.chart.plot.PlotOrientation;
054    import org.jfree.chart.plot.PlotRenderingInfo;
055    import org.jfree.chart.plot.XYPlot;
056    import org.jfree.data.xy.XYDataset;
057    import org.jfree.ui.RectangleEdge;
058    
059    /**
060     * A renderer that connects data points with natural cubic splines and/or
061     * draws shapes at each data point.  This renderer is designed for use with
062     * the {@link XYPlot} class. The example shown here is generated by the
063     * <code>XYSplineRendererDemo1.java</code> program included in the JFreeChart
064     * demo collection:
065     * <br><br>
066     * <img src="../../../../../images/XYSplineRendererSample.png"
067     * alt="XYSplineRendererSample.png" />
068     *
069     * @since 1.0.7
070     */
071    public class XYSplineRenderer extends XYLineAndShapeRenderer {
072    
073        /**
074         * To collect data points for later splining.
075         */
076        private Vector points;
077    
078        /**
079         * Resolution of splines (number of line segments between points)
080         */
081        private int precision;
082    
083        /**
084         * Creates a new instance with the 'precision' attribute defaulting to
085         * 5.
086         */
087        public XYSplineRenderer() {
088            this(5);
089        }
090    
091        /**
092         * Creates a new renderer with the specified precision.
093         *
094         * @param precision  the number of points between data items.
095         */
096        public XYSplineRenderer(int precision) {
097            super();
098            if (precision <= 0) {
099                throw new IllegalArgumentException("Requires precision > 0.");
100            }
101            this.precision = precision;
102        }
103    
104        /**
105         * Get the resolution of splines.
106         *
107         * @return Number of line segments between points.
108         *
109         * @see #setPrecision(int)
110         */
111        public int getPrecision() {
112            return this.precision;
113        }
114    
115        /**
116         * Set the resolution of splines and sends a {@link RendererChangeEvent}
117         * to all registered listeners.
118         *
119         * @param p  number of line segments between points (must be > 0).
120         *
121         * @see #getPrecision()
122         */
123        public void setPrecision(int p) {
124            if (p <= 0) {
125                throw new IllegalArgumentException("Requires p > 0.");
126            }
127            this.precision = p;
128            fireChangeEvent();
129        }
130    
131        /**
132         * Initialises the renderer.
133         * <P>
134         * This method will be called before the first item is rendered, giving the
135         * renderer an opportunity to initialise any state information it wants to
136         * maintain.  The renderer can do nothing if it chooses.
137         *
138         * @param g2  the graphics device.
139         * @param dataArea  the area inside the axes.
140         * @param plot  the plot.
141         * @param data  the data.
142         * @param info  an optional info collection object to return data back to
143         *              the caller.
144         *
145         * @return The renderer state.
146         */
147        public XYItemRendererState initialise(Graphics2D g2, Rectangle2D dataArea,
148                XYPlot plot, XYDataset data, PlotRenderingInfo info) {
149    
150            State state = (State) super.initialise(g2, dataArea, plot, data, info);
151            state.setProcessVisibleItemsOnly(false);
152            this.points = new Vector();
153            setDrawSeriesLineAsPath(true);
154            return state;
155        }
156    
157        /**
158         * Draws the item (first pass). This method draws the lines
159         * connecting the items. Instead of drawing separate lines,
160         * a GeneralPath is constructed and drawn at the end of
161         * the series painting.
162         *
163         * @param g2  the graphics device.
164         * @param state  the renderer state.
165         * @param plot  the plot (can be used to obtain standard color information
166         *              etc).
167         * @param dataset  the dataset.
168         * @param pass  the pass.
169         * @param series  the series index (zero-based).
170         * @param item  the item index (zero-based).
171         * @param domainAxis  the domain axis.
172         * @param rangeAxis  the range axis.
173         * @param dataArea  the area within which the data is being drawn.
174         */
175        protected void drawPrimaryLineAsPath(XYItemRendererState state,
176                Graphics2D g2, XYPlot plot, XYDataset dataset, int pass,
177                int series, int item, ValueAxis domainAxis, ValueAxis rangeAxis,
178                Rectangle2D dataArea) {
179    
180            RectangleEdge xAxisLocation = plot.getDomainAxisEdge();
181            RectangleEdge yAxisLocation = plot.getRangeAxisEdge();
182    
183            // get the data points
184            double x1 = dataset.getXValue(series, item);
185            double y1 = dataset.getYValue(series, item);
186            double transX1 = domainAxis.valueToJava2D(x1, dataArea, xAxisLocation);
187            double transY1 = rangeAxis.valueToJava2D(y1, dataArea, yAxisLocation);
188    
189            // collect points
190            if (!Double.isNaN(transX1) && !Double.isNaN(transY1)) {
191                ControlPoint p = new ControlPoint(plot.getOrientation()
192                                    == PlotOrientation.HORIZONTAL ? (float) transY1
193                                    : (float) transX1, plot.getOrientation()
194                                    == PlotOrientation.HORIZONTAL ? (float) transX1
195                                            : (float) transY1);
196                if (!this.points.contains(p)) {
197                    this.points.add(p);
198                }
199            }
200            if (item == dataset.getItemCount(series) - 1) {
201                State s = (State) state;
202                // construct path
203                if (this.points.size() > 1) {
204                    // we need at least two points to draw something
205                    ControlPoint cp0 = (ControlPoint) this.points.get(0);
206                    s.seriesPath.moveTo(cp0.x, cp0.y);
207                    if (this.points.size() == 2) {
208                        // we need at least 3 points to spline. Draw simple line
209                        // for two points
210                        ControlPoint cp1 = (ControlPoint) this.points.get(1);
211                        s.seriesPath.lineTo(cp1.x, cp1.y);
212                    }
213                    else {
214                        // construct spline
215                        int np = this.points.size(); // number of points
216                        float[] d = new float[np]; // Newton form coefficients
217                        float[] x = new float[np]; // x-coordinates of nodes
218                        float y;
219                        float t;
220                        float oldy = 0;
221                        float oldt = 0;
222    
223                        float[] a = new float[np];
224                        float t1;
225                        float t2;
226                        float[] h = new float[np];
227    
228                        for (int i = 0; i < np; i++) {
229                            ControlPoint cpi = (ControlPoint) this.points.get(i);
230                            x[i] = cpi.x;
231                            d[i] = cpi.y;
232                        }
233    
234                        for (int i = 1; i <= np - 1; i++) {
235                            h[i] = x[i] - x[i - 1];
236                        }
237                        float[] sub = new float[np - 1];
238                        float[] diag = new float[np - 1];
239                        float[] sup = new float[np - 1];
240    
241                        for (int i = 1; i <= np - 2; i++) {
242                            diag[i] = (h[i] + h[i + 1]) / 3;
243                            sup[i] = h[i + 1] / 6;
244                            sub[i] = h[i] / 6;
245                            a[i] = (d[i + 1] - d[i]) / h[i + 1]
246                                       - (d[i] - d[i - 1]) / h[i];
247                        }
248                        solveTridiag(sub, diag, sup, a, np - 2);
249    
250                        // note that a[0]=a[np-1]=0
251                        // draw
252                        oldt = x[0];
253                        oldy = d[0];
254                        s.seriesPath.moveTo(oldt, oldy);
255                        for (int i = 1; i <= np - 1; i++) {
256                            // loop over intervals between nodes
257                            for (int j = 1; j <= this.precision; j++) {
258                                t1 = (h[i] * j) / this.precision;
259                                t2 = h[i] - t1;
260                                y = ((-a[i - 1] / 6 * (t2 + h[i]) * t1 + d[i - 1])
261                                        * t2 + (-a[i] / 6 * (t1 + h[i]) * t2
262                                        + d[i]) * t1) / h[i];
263                                t = x[i - 1] + t1;
264                                s.seriesPath.lineTo(t, y);
265                                oldt = t;
266                                oldy = y;
267                            }
268                        }
269                    }
270                    // draw path
271                    drawFirstPassShape(g2, pass, series, item, s.seriesPath);
272                }
273    
274                // reset points vector
275                this.points = new Vector();
276            }
277        }
278    
279        private void solveTridiag(float[] sub, float[] diag, float[] sup,
280                float[] b, int n) {
281    /*      solve linear system with tridiagonal n by n matrix a
282            using Gaussian elimination *without* pivoting
283            where   a(i,i-1) = sub[i]  for 2<=i<=n
284            a(i,i)   = diag[i] for 1<=i<=n
285            a(i,i+1) = sup[i]  for 1<=i<=n-1
286            (the values sub[1], sup[n] are ignored)
287            right hand side vector b[1:n] is overwritten with solution
288            NOTE: 1...n is used in all arrays, 0 is unused */
289            int i;
290    /*                  factorization and forward substitution */
291            for (i = 2; i <= n; i++) {
292                sub[i] = sub[i] / diag[i - 1];
293                diag[i] = diag[i] - sub[i] * sup[i - 1];
294                b[i] = b[i] - sub[i] * b[i - 1];
295            }
296            b[n] = b[n] / diag[n];
297            for (i = n - 1; i >= 1; i--) {
298                b[i] = (b[i] - sup[i] * b[i + 1]) / diag[i];
299            }
300        }
301    
302        /**
303         * Tests this renderer for equality with an arbitrary object.
304         *
305         * @param obj  the object (<code>null</code> permitted).
306         *
307         * @return A boolean.
308         */
309        public boolean equals(Object obj) {
310            if (obj == this) {
311                return true;
312            }
313            if (!(obj instanceof XYSplineRenderer)) {
314                return false;
315            }
316            XYSplineRenderer that = (XYSplineRenderer) obj;
317            if (this.precision != that.precision) {
318                return false;
319            }
320            return super.equals(obj);
321        }
322    
323        /**
324         * Represents a control point.
325         */
326        class ControlPoint {
327    
328            /** The x-coordinate. */
329            public float x;
330    
331            /** The y-coordinate. */
332            public float y;
333    
334            /**
335             * Creates a new control point.
336             *
337             * @param x  the x-coordinate.
338             * @param y  the y-coordinate.
339             */
340            public ControlPoint(float x, float y) {
341                this.x = x;
342                this.y = y;
343            }
344    
345            /**
346             * Tests this point for equality with an arbitrary object.
347             *
348             * @param obj  the object (<code>null</code> permitted.
349             *
350             * @return A boolean.
351             */
352            public boolean equals(Object obj) {
353                if (obj == this) {
354                    return true;
355                }
356                if (!(obj instanceof ControlPoint)) {
357                    return false;
358                }
359                ControlPoint that = (ControlPoint) obj;
360                if (this.x != that.x) {
361                    return false;
362                }
363                /*&& y == ((ControlPoint) obj).y*/;
364                return true;
365            }
366    
367        }
368    }