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