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