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 * CyclicXYItemRenderer.java
029 * ---------------------------
030 * (C) Copyright 2003-2008, by Nicolas Brodu and Contributors.
031 *
032 * Original Author:  Nicolas Brodu;
033 * Contributor(s):   David Gilbert (for Object Refinery Limited);
034 *
035 * Changes
036 * -------
037 * 19-Nov-2003 : Initial import to JFreeChart from the JSynoptic project (NB);
038 * 23-Dec-2003 : Added missing Javadocs (DG);
039 * 25-Feb-2004 : Replaced CrosshairInfo with CrosshairState (DG);
040 * 15-Jul-2004 : Switched getX() with getXValue() and getY() with
041 *               getYValue() (DG);
042 * ------------- JFREECHART 1.0.0 ---------------------------------------------
043 * 06-Jul-2006 : Modified to call only dataset methods that return double
044 *               primitives (DG);
045 *
046 */
047
048package org.jfree.chart.renderer.xy;
049
050import java.awt.Graphics2D;
051import java.awt.geom.Rectangle2D;
052import java.io.Serializable;
053
054import org.jfree.chart.axis.CyclicNumberAxis;
055import org.jfree.chart.axis.ValueAxis;
056import org.jfree.chart.labels.XYToolTipGenerator;
057import org.jfree.chart.plot.CrosshairState;
058import org.jfree.chart.plot.PlotRenderingInfo;
059import org.jfree.chart.plot.XYPlot;
060import org.jfree.chart.urls.XYURLGenerator;
061import org.jfree.data.DomainOrder;
062import org.jfree.data.general.DatasetChangeListener;
063import org.jfree.data.general.DatasetGroup;
064import org.jfree.data.xy.XYDataset;
065
066/**
067 * The Cyclic XY item renderer is specially designed to handle cyclic axis.
068 * While the standard renderer would draw a line across the plot when a cycling
069 * occurs, the cyclic renderer splits the line at each cycle end instead. This
070 * is done by interpolating new points at cycle boundary. Thus, correct
071 * appearance is restored.
072 *
073 * The Cyclic XY item renderer works exactly like a standard XY item renderer
074 * with non-cyclic axis.
075 */
076public class CyclicXYItemRenderer extends StandardXYItemRenderer
077                                  implements Serializable {
078
079    /** For serialization. */
080    private static final long serialVersionUID = 4035912243303764892L;
081
082    /**
083     * Default constructor.
084     */
085    public CyclicXYItemRenderer() {
086        super();
087    }
088
089    /**
090     * Creates a new renderer.
091     *
092     * @param type  the renderer type.
093     */
094    public CyclicXYItemRenderer(int type) {
095        super(type);
096    }
097
098    /**
099     * Creates a new renderer.
100     *
101     * @param type  the renderer type.
102     * @param labelGenerator  the tooltip generator.
103     */
104    public CyclicXYItemRenderer(int type, XYToolTipGenerator labelGenerator) {
105        super(type, labelGenerator);
106    }
107
108    /**
109     * Creates a new renderer.
110     *
111     * @param type  the renderer type.
112     * @param labelGenerator  the tooltip generator.
113     * @param urlGenerator  the url generator.
114     */
115    public CyclicXYItemRenderer(int type,
116                                XYToolTipGenerator labelGenerator,
117                                XYURLGenerator urlGenerator) {
118        super(type, labelGenerator, urlGenerator);
119    }
120
121
122    /**
123     * Draws the visual representation of a single data item.
124     * When using cyclic axis, do not draw a line from right to left when
125     * cycling as would a standard XY item renderer, but instead draw a line
126     * from the previous point to the cycle bound in the last cycle, and a line
127     * from the cycle bound to current point in the current cycle.
128     *
129     * @param g2  the graphics device.
130     * @param state  the renderer state.
131     * @param dataArea  the data area.
132     * @param info  the plot rendering info.
133     * @param plot  the plot.
134     * @param domainAxis  the domain axis.
135     * @param rangeAxis  the range axis.
136     * @param dataset  the dataset.
137     * @param series  the series index.
138     * @param item  the item index.
139     * @param crosshairState  crosshair information for the plot
140     *                        (<code>null</code> permitted).
141     * @param pass  the current pass index.
142     */
143    public void drawItem(Graphics2D g2,
144                         XYItemRendererState state,
145                         Rectangle2D dataArea,
146                         PlotRenderingInfo info,
147                         XYPlot plot,
148                         ValueAxis domainAxis,
149                         ValueAxis rangeAxis,
150                         XYDataset dataset,
151                         int series,
152                         int item,
153                         CrosshairState crosshairState,
154                         int pass) {
155
156        if ((!getPlotLines()) || ((!(domainAxis instanceof CyclicNumberAxis))
157                && (!(rangeAxis instanceof CyclicNumberAxis))) || (item <= 0)) {
158            super.drawItem(g2, state, dataArea, info, plot, domainAxis,
159                    rangeAxis, dataset, series, item, crosshairState, pass);
160            return;
161        }
162
163        // get the previous data point...
164        double xn = dataset.getXValue(series, item - 1);
165        double yn = dataset.getYValue(series, item - 1);
166        // If null, don't draw line => then delegate to parent
167        if (Double.isNaN(yn)) {
168            super.drawItem(g2, state, dataArea, info, plot, domainAxis,
169                    rangeAxis, dataset, series, item, crosshairState, pass);
170            return;
171        }
172        double[] x = new double[2];
173        double[] y = new double[2];
174        x[0] = xn;
175        y[0] = yn;
176
177        // get the data point...
178        xn = dataset.getXValue(series, item);
179        yn = dataset.getYValue(series, item);
180        // If null, don't draw line at all
181        if (Double.isNaN(yn)) {
182            return;
183        }
184        x[1] = xn;
185        y[1] = yn;
186
187        // Now split the segment as needed
188        double xcycleBound = Double.NaN;
189        double ycycleBound = Double.NaN;
190        boolean xBoundMapping = false, yBoundMapping = false;
191        CyclicNumberAxis cnax = null, cnay = null;
192
193        if (domainAxis instanceof CyclicNumberAxis) {
194            cnax = (CyclicNumberAxis) domainAxis;
195            xcycleBound = cnax.getCycleBound();
196            xBoundMapping = cnax.isBoundMappedToLastCycle();
197            // If the segment must be splitted, insert a new point
198            // Strict test forces to have real segments (not 2 equal points)
199            // and avoids division by 0
200            if ((x[0] != x[1])
201                    && ((xcycleBound >= x[0])
202                    && (xcycleBound <= x[1])
203                    || (xcycleBound >= x[1])
204                    && (xcycleBound <= x[0]))) {
205                double[] nx = new double[3];
206                double[] ny = new double[3];
207                nx[0] = x[0]; nx[2] = x[1]; ny[0] = y[0]; ny[2] = y[1];
208                nx[1] = xcycleBound;
209                ny[1] = (y[1] - y[0]) * (xcycleBound - x[0])
210                        / (x[1] - x[0]) + y[0];
211                x = nx; y = ny;
212            }
213        }
214
215        if (rangeAxis instanceof CyclicNumberAxis) {
216            cnay = (CyclicNumberAxis) rangeAxis;
217            ycycleBound = cnay.getCycleBound();
218            yBoundMapping = cnay.isBoundMappedToLastCycle();
219            // The split may occur in either x splitted segments, if any, but
220            // not in both
221            if ((y[0] != y[1]) && ((ycycleBound >= y[0])
222                    && (ycycleBound <= y[1])
223                    || (ycycleBound >= y[1]) && (ycycleBound <= y[0]))) {
224                double[] nx = new double[x.length + 1];
225                double[] ny = new double[y.length + 1];
226                nx[0] = x[0]; nx[2] = x[1]; ny[0] = y[0]; ny[2] = y[1];
227                ny[1] = ycycleBound;
228                nx[1] = (x[1] - x[0]) * (ycycleBound - y[0])
229                        / (y[1] - y[0]) + x[0];
230                if (x.length == 3) {
231                    nx[3] = x[2]; ny[3] = y[2];
232                }
233                x = nx; y = ny;
234            }
235            else if ((x.length == 3) && (y[1] != y[2]) && ((ycycleBound >= y[1])
236                    && (ycycleBound <= y[2])
237                    || (ycycleBound >= y[2]) && (ycycleBound <= y[1]))) {
238                double[] nx = new double[4];
239                double[] ny = new double[4];
240                nx[0] = x[0]; nx[1] = x[1]; nx[3] = x[2];
241                ny[0] = y[0]; ny[1] = y[1]; ny[3] = y[2];
242                ny[2] = ycycleBound;
243                nx[2] = (x[2] - x[1]) * (ycycleBound - y[1])
244                        / (y[2] - y[1]) + x[1];
245                x = nx; y = ny;
246            }
247        }
248
249        // If the line is not wrapping, then parent is OK
250        if (x.length == 2) {
251            super.drawItem(g2, state, dataArea, info, plot, domainAxis,
252                    rangeAxis, dataset, series, item, crosshairState, pass);
253            return;
254        }
255
256        OverwriteDataSet newset = new OverwriteDataSet(x, y, dataset);
257
258        if (cnax != null) {
259            if (xcycleBound == x[0]) {
260                cnax.setBoundMappedToLastCycle(x[1] <= xcycleBound);
261            }
262            if (xcycleBound == x[1]) {
263                cnax.setBoundMappedToLastCycle(x[0] <= xcycleBound);
264            }
265        }
266        if (cnay != null) {
267            if (ycycleBound == y[0]) {
268                cnay.setBoundMappedToLastCycle(y[1] <= ycycleBound);
269            }
270            if (ycycleBound == y[1]) {
271                cnay.setBoundMappedToLastCycle(y[0] <= ycycleBound);
272            }
273        }
274        super.drawItem(
275            g2, state, dataArea, info, plot, domainAxis, rangeAxis,
276            newset, series, 1, crosshairState, pass
277        );
278
279        if (cnax != null) {
280            if (xcycleBound == x[1]) {
281                cnax.setBoundMappedToLastCycle(x[2] <= xcycleBound);
282            }
283            if (xcycleBound == x[2]) {
284                cnax.setBoundMappedToLastCycle(x[1] <= xcycleBound);
285            }
286        }
287        if (cnay != null) {
288            if (ycycleBound == y[1]) {
289                cnay.setBoundMappedToLastCycle(y[2] <= ycycleBound);
290            }
291            if (ycycleBound == y[2]) {
292                cnay.setBoundMappedToLastCycle(y[1] <= ycycleBound);
293            }
294        }
295        super.drawItem(g2, state, dataArea, info, plot, domainAxis, rangeAxis,
296                newset, series, 2, crosshairState, pass);
297
298        if (x.length == 4) {
299            if (cnax != null) {
300                if (xcycleBound == x[2]) {
301                    cnax.setBoundMappedToLastCycle(x[3] <= xcycleBound);
302                }
303                if (xcycleBound == x[3]) {
304                    cnax.setBoundMappedToLastCycle(x[2] <= xcycleBound);
305                }
306            }
307            if (cnay != null) {
308                if (ycycleBound == y[2]) {
309                    cnay.setBoundMappedToLastCycle(y[3] <= ycycleBound);
310                }
311                if (ycycleBound == y[3]) {
312                    cnay.setBoundMappedToLastCycle(y[2] <= ycycleBound);
313                }
314            }
315            super.drawItem(g2, state, dataArea, info, plot, domainAxis,
316                    rangeAxis, newset, series, 3, crosshairState, pass);
317        }
318
319        if (cnax != null) {
320            cnax.setBoundMappedToLastCycle(xBoundMapping);
321        }
322        if (cnay != null) {
323            cnay.setBoundMappedToLastCycle(yBoundMapping);
324        }
325    }
326
327    /**
328     * A dataset to hold the interpolated points when drawing new lines.
329     */
330    protected static class OverwriteDataSet implements XYDataset {
331
332        /** The delegate dataset. */
333        protected XYDataset delegateSet;
334
335        /** Storage for the x and y values. */
336        Double[] x, y;
337
338        /**
339         * Creates a new dataset.
340         *
341         * @param x  the x values.
342         * @param y  the y values.
343         * @param delegateSet  the dataset.
344         */
345        public OverwriteDataSet(double [] x, double[] y,
346                                XYDataset delegateSet) {
347            this.delegateSet = delegateSet;
348            this.x = new Double[x.length]; this.y = new Double[y.length];
349            for (int i = 0; i < x.length; ++i) {
350                this.x[i] = new Double(x[i]);
351                this.y[i] = new Double(y[i]);
352            }
353        }
354
355        /**
356         * Returns the order of the domain (X) values.
357         *
358         * @return The domain order.
359         */
360        public DomainOrder getDomainOrder() {
361            return DomainOrder.NONE;
362        }
363
364        /**
365         * Returns the number of items for the given series.
366         *
367         * @param series  the series index (zero-based).
368         *
369         * @return The item count.
370         */
371        public int getItemCount(int series) {
372            return this.x.length;
373        }
374
375        /**
376         * Returns the x-value.
377         *
378         * @param series  the series index (zero-based).
379         * @param item  the item index (zero-based).
380         *
381         * @return The x-value.
382         */
383        public Number getX(int series, int item) {
384            return this.x[item];
385        }
386
387        /**
388         * Returns the x-value (as a double primitive) for an item within a
389         * series.
390         *
391         * @param series  the series (zero-based index).
392         * @param item  the item (zero-based index).
393         *
394         * @return The x-value.
395         */
396        public double getXValue(int series, int item) {
397            double result = Double.NaN;
398            Number x = getX(series, item);
399            if (x != null) {
400                result = x.doubleValue();
401            }
402            return result;
403        }
404
405        /**
406         * Returns the y-value.
407         *
408         * @param series  the series index (zero-based).
409         * @param item  the item index (zero-based).
410         *
411         * @return The y-value.
412         */
413        public Number getY(int series, int item) {
414            return this.y[item];
415        }
416
417        /**
418         * Returns the y-value (as a double primitive) for an item within a
419         * series.
420         *
421         * @param series  the series (zero-based index).
422         * @param item  the item (zero-based index).
423         *
424         * @return The y-value.
425         */
426        public double getYValue(int series, int item) {
427            double result = Double.NaN;
428            Number y = getY(series, item);
429            if (y != null) {
430                result = y.doubleValue();
431            }
432            return result;
433        }
434
435        /**
436         * Returns the number of series in the dataset.
437         *
438         * @return The series count.
439         */
440        public int getSeriesCount() {
441            return this.delegateSet.getSeriesCount();
442        }
443
444        /**
445         * Returns the name of the given series.
446         *
447         * @param series  the series index (zero-based).
448         *
449         * @return The series name.
450         */
451        public Comparable getSeriesKey(int series) {
452            return this.delegateSet.getSeriesKey(series);
453        }
454
455        /**
456         * Returns the index of the named series, or -1.
457         *
458         * @param seriesName  the series name.
459         *
460         * @return The index.
461         */
462        public int indexOf(Comparable seriesName) {
463            return this.delegateSet.indexOf(seriesName);
464        }
465
466        /**
467         * Does nothing.
468         *
469         * @param listener  ignored.
470         */
471        public void addChangeListener(DatasetChangeListener listener) {
472            // unused in parent
473        }
474
475        /**
476         * Does nothing.
477         *
478         * @param listener  ignored.
479         */
480        public void removeChangeListener(DatasetChangeListener listener) {
481            // unused in parent
482        }
483
484        /**
485         * Returns the dataset group.
486         *
487         * @return The dataset group.
488         */
489        public DatasetGroup getGroup() {
490            // unused but must return something, so while we are at it...
491            return this.delegateSet.getGroup();
492        }
493
494        /**
495         * Does nothing.
496         *
497         * @param group  ignored.
498         */
499        public void setGroup(DatasetGroup group) {
500            // unused in parent
501        }
502
503    }
504
505}
506
507