001/* ======================================================
002 * JFreeChart : a chart library for the Java(tm) platform
003 * ======================================================
004 *
005 * (C) Copyright 2000-present, by David Gilbert and Contributors.
006 *
007 * Project Info:  https://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 * XYBezierRenderer.java
029 * ---------------------
030 * (C) Copyright 2021-present, by Javier Robes and Contributors.
031 *
032 * Original Author:  Javier Robes;
033 *
034 */
035
036package org.jfree.chart.renderer.xy;
037
038import java.awt.GradientPaint;
039import java.awt.Graphics2D;
040import java.awt.Paint;
041import java.awt.geom.GeneralPath;
042import java.awt.geom.Point2D;
043import java.awt.geom.Rectangle2D;
044import java.util.ArrayList;
045import java.util.List;
046import java.util.Objects;
047
048import org.jfree.chart.axis.ValueAxis;
049import org.jfree.chart.event.RendererChangeEvent;
050import org.jfree.chart.plot.PlotOrientation;
051import org.jfree.chart.plot.PlotRenderingInfo;
052import org.jfree.chart.plot.XYPlot;
053import org.jfree.chart.ui.GradientPaintTransformer;
054import org.jfree.chart.ui.RectangleEdge;
055import org.jfree.chart.ui.StandardGradientPaintTransformer;
056import org.jfree.chart.util.Args;
057import org.jfree.data.xy.XYDataset;
058
059
060/**
061 * A renderer that connects data points with Bezier cubic curves and/or
062 * draws shapes at each data point.  This renderer is designed for use with
063 * the {@link XYPlot} class.
064 */
065public class XYBezierRenderer extends XYLineAndShapeRenderer {
066
067    /**
068     * An enumeration of the fill types for the renderer.
069     *
070     * @since 1.0.17
071     */
072    public static enum FillType {
073
074        /** No fill. */
075        NONE,
076
077        /** Fill down to zero. */
078        TO_ZERO,
079
080        /** Fill to the lower bound. */
081        TO_LOWER_BOUND,
082
083        /** Fill to the upper bound. */
084        TO_UPPER_BOUND
085    }
086
087    /**
088     * Represents state information that applies to a single rendering of
089     * a chart.
090     */
091    public static class XYBezierState extends State {
092
093        /** The area to fill under the curve. */
094        public GeneralPath fillArea;
095
096        /** The points. */
097        public List<Point2D> points;
098
099        /**
100         * Creates a new state instance.
101         *
102         * @param info  the plot rendering info. 
103         */
104        public XYBezierState(PlotRenderingInfo info) {
105            super(info);
106            this.fillArea = new GeneralPath();
107            this.points = new ArrayList<>();
108        }
109    }
110
111    /**
112     * Resolution of Bezier curves (number of line segments between points)
113     */
114    private int precision;
115
116    /**
117     *  Tension defines how sharply does the curve bends
118     */
119    private double tension;
120
121    /**
122     * A flag that can be set to specify 
123     * to fill the area under the Bezier curve.
124     */
125    private FillType fillType;
126
127    /** The gradient transformer. */
128    private GradientPaintTransformer gradientPaintTransformer;
129
130    /**
131     * Creates a new instance with the precision attribute defaulting to 5,
132     * the tension attribute defaulting to 2  
133     * and no fill of the area 'under' the Bezier curve.
134     */
135    public XYBezierRenderer() {
136        this(5, 25, FillType.NONE);
137    }
138
139    /**
140     * Creates a new renderer with the specified precision and tension
141     * and no fill of the area 'under' (between '0' and) the Bezier curve.
142     *
143     * @param precision  the number of points between data items.
144     * @param tension  value to define how sharply the curve bends
145     */
146    public XYBezierRenderer(int precision, double tension) {
147        this(precision, tension ,FillType.NONE);
148    }
149
150    /**
151     * Creates a new renderer with the specified precision
152     * and specified fill of the area 'under' (between '0' and) the Bezier curve.
153     *
154     * @param precision  the number of points between data items.
155     * @param tension  value to define how sharply the Bezier curve bends
156     * @param fillType  the type of fill beneath the curve ({@code null}
157     *     not permitted).
158     *
159     * @since 1.0.17
160     */
161    public XYBezierRenderer(int precision, double tension, FillType fillType) {
162        super();
163        if (precision <= 0) {
164            throw new IllegalArgumentException("Requires precision > 0.");
165        }
166        if (tension <= 0) {
167            throw new IllegalArgumentException("Requires precision > 0.");
168        }
169        Args.nullNotPermitted(fillType, "fillType");
170        this.precision = precision;
171        this.tension = tension;
172        this.fillType = fillType;
173        this.gradientPaintTransformer = new StandardGradientPaintTransformer();
174    }
175
176    /**
177     * Returns the number of line segments used to approximate the Bezier
178     * curve between data points.
179     *
180     * @return The number of line segments.
181     *
182     * @see #setPrecision(int)
183     */
184    public int getPrecision() {
185        return this.precision;
186    }
187
188    /**
189     * Set the resolution of Bezier curves and sends a {@link RendererChangeEvent}
190     * to all registered listeners.
191     *
192     * @param p  number of line segments between points (must be &gt; 0).
193     *
194     * @see #getPrecision()
195     */
196    public void setPrecision(int p) {
197        if (p <= 0) {
198            throw new IllegalArgumentException("Requires p > 0.");
199        }
200        this.precision = p;
201        fireChangeEvent();
202    }
203
204    /**
205     * Returns the value of the tension which defines how sharply 
206     * does the curve bends
207     *
208     * @return The value of tesion.
209     *
210     * @see #setTension(double)
211     */
212    public double getTension() {
213        return this.tension;
214    }
215
216    /**
217     * Set the value of the tension which defines how sharply 
218     * does the curve bends and sends a {@link RendererChangeEvent}
219     * to all registered listeners.
220     *
221     * @param t  value of tension (must be &gt; 0).
222     *
223     * @see #getTension()
224     */
225    public void setTension(double t) {
226        if (t <= 0) {
227            throw new IllegalArgumentException("Requires tension > 0.");
228        }
229        this.tension = t;
230        fireChangeEvent();
231    }
232
233    /**
234     * Returns the type of fill that the renderer draws beneath the curve.
235     *
236     * @return The type of fill (never {@code null}).
237     *
238     * @see #setFillType(FillType)
239     *
240     * @since 1.0.17
241     */
242    public FillType getFillType() {
243        return this.fillType;
244    }
245
246    /**
247     * Set the fill type and sends a {@link RendererChangeEvent}
248     * to all registered listeners.
249     *
250     * @param fillType   the fill type ({@code null} not permitted).
251     *
252     * @see #getFillType()
253     *
254     * @since 1.0.17
255     */
256    public void setFillType(FillType fillType) {
257        this.fillType = fillType;
258        fireChangeEvent();
259    }
260
261    /**
262     * Returns the gradient paint transformer, or {@code null}.
263     *
264     * @return The gradient paint transformer (possibly {@code null}).
265     *
266     * @since 1.0.17
267     */
268    public GradientPaintTransformer getGradientPaintTransformer() {
269        return this.gradientPaintTransformer;
270    }
271
272    /**
273     * Sets the gradient paint transformer and sends a 
274     * {@link RendererChangeEvent} to all registered listeners.
275     *
276     * @param gpt  the transformer ({@code null} permitted).
277     *
278     * @since 1.0.17
279     */
280    public void setGradientPaintTransformer(GradientPaintTransformer gpt) {
281        this.gradientPaintTransformer = gpt;
282        fireChangeEvent();
283    }
284
285    /**
286     * Initialises the renderer.
287     * <P>
288     * This method will be called before the first item is rendered, giving the
289     * renderer an opportunity to initialise any state information it wants to
290     * maintain.  The renderer can do nothing if it chooses.
291     *
292     * @param g2  the graphics device.
293     * @param dataArea  the area inside the axes.
294     * @param plot  the plot.
295     * @param data  the data.
296     * @param info  an optional info collection object to return data back to
297     *              the caller.
298     *
299     * @return The renderer state.
300     */
301    @Override
302    public XYItemRendererState initialise(Graphics2D g2, Rectangle2D dataArea,
303                                          XYPlot plot, XYDataset data, PlotRenderingInfo info) {
304
305        setDrawSeriesLineAsPath(true);
306        XYBezierState state = new XYBezierState(info);
307        state.setProcessVisibleItemsOnly(false);
308        return state;
309    }
310
311
312    /**
313     * Draws the item (first pass). This method draws the lines
314     * connecting the items. Instead of drawing separate lines,
315     * a GeneralPath is constructed and drawn at the end of
316     * the series painting.
317     *
318     * @param g2  the graphics device.
319     * @param state  the renderer state.
320     * @param plot  the plot (can be used to obtain standard color information
321     *              etc).
322     * @param dataset  the dataset.
323     * @param pass  the pass.
324     * @param series  the series index (zero-based).
325     * @param item  the item index (zero-based).
326     * @param xAxis  the domain axis.
327     * @param yAxis  the range axis.
328     * @param dataArea  the area within which the data is being drawn.
329     */
330    @Override
331    protected void drawPrimaryLineAsPath(XYItemRendererState state,
332                                         Graphics2D g2, XYPlot plot, XYDataset dataset, int pass,
333                                         int series, int item, ValueAxis xAxis, ValueAxis yAxis,
334                                         Rectangle2D dataArea) {
335
336        XYBezierState s = (XYBezierState) state;
337        RectangleEdge xAxisLocation = plot.getDomainAxisEdge();
338        RectangleEdge yAxisLocation = plot.getRangeAxisEdge();
339
340        // get the data points
341        double x1 = dataset.getXValue(series, item);
342        double y1 = dataset.getYValue(series, item);
343        double transX1 = xAxis.valueToJava2D(x1, dataArea, xAxisLocation);
344        double transY1 = yAxis.valueToJava2D(y1, dataArea, yAxisLocation);
345
346        // Collect points
347        if (!Double.isNaN(transX1) && !Double.isNaN(transY1)) {
348            Point2D p = plot.getOrientation() == PlotOrientation.HORIZONTAL
349                    ? new Point2D.Float((float) transY1, (float) transX1)
350                    : new Point2D.Float((float) transX1, (float) transY1);
351            if (!s.points.contains(p))
352                s.points.add(p);
353        }
354
355        if (item == dataset.getItemCount(series) - 1) {     // construct path
356            if (s.points.size() > 1) {
357                Point2D origin;
358                if (this.fillType == FillType.TO_ZERO) {
359                    float xz = (float) xAxis.valueToJava2D(0, dataArea,
360                            yAxisLocation);
361                    float yz = (float) yAxis.valueToJava2D(0, dataArea,
362                            yAxisLocation);
363                    origin = plot.getOrientation() == PlotOrientation.HORIZONTAL
364                            ? new Point2D.Float(yz, xz)
365                            : new Point2D.Float(xz, yz);
366                } else if (this.fillType == FillType.TO_LOWER_BOUND) {
367                    float xlb = (float) xAxis.valueToJava2D(
368                            xAxis.getLowerBound(), dataArea, xAxisLocation);
369                    float ylb = (float) yAxis.valueToJava2D(
370                            yAxis.getLowerBound(), dataArea, yAxisLocation);
371                    origin = plot.getOrientation() == PlotOrientation.HORIZONTAL
372                            ? new Point2D.Float(ylb, xlb)
373                            : new Point2D.Float(xlb, ylb);
374                } else {// fillType == TO_UPPER_BOUND
375                    float xub = (float) xAxis.valueToJava2D(
376                            xAxis.getUpperBound(), dataArea, xAxisLocation);
377                    float yub = (float) yAxis.valueToJava2D(
378                            yAxis.getUpperBound(), dataArea, yAxisLocation);
379                    origin = plot.getOrientation() == PlotOrientation.HORIZONTAL
380                            ? new Point2D.Float(yub, xub)
381                            : new Point2D.Float(xub, yub);
382                }
383
384                // we need at least two points to draw something
385                Point2D cp0 = s.points.get(0);
386                s.seriesPath.moveTo(cp0.getX(), cp0.getY());
387                if (this.fillType != FillType.NONE) {
388                    if (plot.getOrientation() == PlotOrientation.HORIZONTAL) {
389                        s.fillArea.moveTo(origin.getX(), cp0.getY());
390                    } else {
391                        s.fillArea.moveTo(cp0.getX(), origin.getY());
392                    }
393                    s.fillArea.lineTo(cp0.getX(), cp0.getY());
394                }
395                if (s.points.size() == 2) {
396                    // we need at least 3 points to Bezier. Draw simple line
397                    // for two points
398                    Point2D cp1 = s.points.get(1);
399                    if (this.fillType != FillType.NONE) {
400                        s.fillArea.lineTo(cp1.getX(), cp1.getY());
401                        s.fillArea.lineTo(cp1.getX(), origin.getY());
402                        s.fillArea.closePath();
403                    }
404                    s.seriesPath.lineTo(cp1.getX(), cp1.getY());
405                }
406                else if (s.points.size() == 3) {
407                    // with 3 points only initial and end Bezier curves are required.
408
409                    Point2D[] pInitial = getInitalPoints(s);
410                    addBezierPointsToSeriesPath(pInitial, s);
411                    Point2D[] pFinal = getFinalPoints(s);
412                    addBezierPointsToSeriesPath(pFinal, s);
413
414                }
415                else {
416                    // construct Bezier curve
417                    int np = s.points.size(); // number of points
418                    for(int i = 0; i < np - 1; i++) {
419                        if(i == 0) {
420                            // 3 points, 2 lines (initial an final Bezier curves)
421                            Point2D[] initial3Points = new Point2D[3];
422                            initial3Points[0] = s.points.get(0);
423                            initial3Points[1] = s.points.get(1);
424                            initial3Points[2] = s.points.get(2);
425                            Point2D[] pInitial = calcSegmentPointsInitial(initial3Points);
426                            addBezierPointsToSeriesPath(pInitial, s);
427                        }
428                        if(i == np - 2) {
429                            Point2D[] final3Points = new Point2D[4];
430                            final3Points[1] = s.points.get(np-3);
431                            final3Points[2] = s.points.get(np-2);
432                            final3Points[3] = s.points.get(np-1);
433                            // No need for final3Points[0]. Not required
434                            Point2D[] pFinal = calcSegmentPointsFinal(final3Points);
435                            addBezierPointsToSeriesPath(pFinal, s);
436                        }
437                        if ((i != 0) && (i != (np - 2))){
438                            Point2D[] original4Points = new Point2D[4];
439                            original4Points[0] = s.points.get(i - 1);
440                            original4Points[1] = s.points.get(i);
441                            original4Points[2] = s.points.get(i + 1);
442                            original4Points[3] = s.points.get(i + 2);
443                            Point2D[] pMedium = calculateSegmentPoints(original4Points);
444                            addBezierPointsToSeriesPath(pMedium, s);
445                        }
446                    }
447                }
448                // Add last point @ y=0 for fillPath and close path
449                if (this.fillType != FillType.NONE) {
450                    if (plot.getOrientation() == PlotOrientation.HORIZONTAL) {
451                        s.fillArea.lineTo(origin.getX(), s.points.get(
452                                s.points.size() - 1).getY());
453                    } else {
454                        s.fillArea.lineTo(s.points.get(
455                                s.points.size() - 1).getX(), origin.getY());
456                    }
457                    s.fillArea.closePath();
458                }
459                // fill under the curve...
460                if (this.fillType != FillType.NONE) {
461                    Paint fp = getSeriesFillPaint(series);
462                    if (this.gradientPaintTransformer != null
463                            && fp instanceof GradientPaint) {
464                        GradientPaint gp = this.gradientPaintTransformer
465                                .transform((GradientPaint) fp, s.fillArea);
466                        g2.setPaint(gp);
467                    } else {
468                        g2.setPaint(fp);
469                    }
470                    g2.fill(s.fillArea);
471                    s.fillArea.reset();
472                }
473                // then draw the line...
474                drawFirstPassShape(g2, pass, series, item, s.seriesPath);
475            }
476            // reset points vector
477            s.points = new ArrayList<>();
478        }
479    }
480
481    private void addBezierPointsToSeriesPath(Point2D[] segmentPoints, XYBezierState s) {
482        double x;
483        double y;
484        for (int t = 0 ; t <= this.precision; t++) {
485            double k = (double)t / this.precision;
486            double r = 1- k;
487
488            x = Math.pow(r, 3) * segmentPoints[0].getX() + 3 * k * Math.pow(r, 2) * segmentPoints[1].getX()
489                    + 3 * Math.pow(k, 2) * (1 - k) * segmentPoints[2].getX() + Math.pow(k, 3) * segmentPoints[3].getX();
490            y = Math.pow(r, 3) * segmentPoints[0].getY() + 3 * k * Math.pow(r, 2) * segmentPoints[1].getY()
491                    + 3 * Math.pow(k, 2) * (1 - k) * segmentPoints[2].getY() + Math.pow(k, 3) * segmentPoints[3].getY();
492            s.seriesPath.lineTo(x, y);
493            if (this.fillType != FillType.NONE) {
494                s.fillArea.lineTo(x, y);
495            }
496        }
497    }
498
499    private Point2D[] getFinalPoints(XYBezierState s) {
500        Point2D[] final3Points = new Point2D[4];
501        final3Points[1] = s.points.get(0);
502        final3Points[2] = s.points.get(1);
503        final3Points[3] = s.points.get(2);
504        // No need for final3Points[0]. Not required
505        Point2D[] pFinal = calcSegmentPointsFinal(final3Points);//TENSION = 1.5
506        return pFinal;
507    }
508
509    private Point2D[] getInitalPoints(XYBezierState s) {
510        Point2D[] initial3Points = new Point2D[3];
511        initial3Points[0] = s.points.get(0);
512        initial3Points[1] = s.points.get(1);
513        initial3Points[2] = s.points.get(2);
514        Point2D[] pInitial = calcSegmentPointsInitial(initial3Points);// TENSION = 1.5
515        return pInitial;
516    }
517
518    private Point2D[] calculateSegmentPoints(Point2D[] original4Points) {
519        Point2D[] points = new Point2D[4];
520        points[0] = original4Points[1];
521        points[3] = original4Points[2];
522        for(int i = 1; i < 3; i++) {
523            Point2D aux1 = calcUnitaryVector(original4Points[i-1], original4Points[i]);
524            Point2D aux2 = calcUnitaryVector(original4Points[i+1], original4Points[i]);
525            Point2D aux3 = calcUnitaryVector(aux2, aux1);
526
527            double x = original4Points[i].getX() + Math.pow(-1.0, i+1) * tension * aux3.getX();
528            double y = original4Points[i].getY() + Math.pow(-1.0, i+1) * tension * aux3.getY();
529            points[i] = new Point2D.Double(x, y);
530        }
531        return points;
532    }
533
534    private Point2D[] calcSegmentPointsInitial(Point2D[] original3P) {
535        Point2D[] points = new Point2D[4];
536        points[0] = original3P[0];// Endpoint 1
537        points[3] = original3P[1];// Endpoint 2
538        // Control point 1
539        Point2D auxInitial = calcUnitaryVector(original3P[0], original3P[1]);
540        points[1] = original3P[0];// new Point2D.Double(x0, y0);
541        // Control point 2
542        Point2D aux2 = calcUnitaryVector(original3P[2], original3P[1]);
543        Point2D aux3 = calcUnitaryVector(auxInitial, aux2);
544        double x = original3P[1].getX() + tension * aux3.getX();
545        double y = original3P[1].getY() + tension * aux3.getY();
546        points[2] = new Point2D.Double(x, y);
547        return points;
548    }
549
550    private Point2D[] calcSegmentPointsFinal(Point2D[] original3P) {
551        /*
552         * Each segment is defined by its two endpoints and two control points. A
553         * control point determines the tangent at the corresponding endpoint.
554         */
555        Point2D[] points = new Point2D[4];
556        points[0] = original3P[2];// Endpoint 1
557        points[3] = original3P[3];// Endpoint 2
558        // Control point 2: points[2]
559        Point2D auxInitial = calcUnitaryVector(original3P[3], original3P[2]);
560        points[2] = original3P[3];// new Point2D.Double(x0, y0);
561        // Control point 1
562        Point2D aux1 = calcUnitaryVector(original3P[3], original3P[2]);
563        Point2D aux2 = calcUnitaryVector(original3P[1], original3P[2]);
564        Point2D aux3 = calcUnitaryVector(aux1, aux2);
565        double x = original3P[2].getX() + tension * aux3.getX();
566        double y = original3P[2].getY() + tension * aux3.getY();
567        points[1] = new Point2D.Double(x, y);
568        return points;
569    }
570
571    private Point2D calcUnitaryVector(Point2D pOrigin, Point2D pEnd) {
572        double module = Math.sqrt(Math.pow(pEnd.getX() - pOrigin.getX(), 2) +
573                Math.pow(pEnd.getY() - pOrigin.getY(), 2));
574        if (module == 0) {
575            return null;
576        }
577        return new Point2D.Double((pEnd.getX() - pOrigin.getX()) / module,
578                (pEnd.getY() - pOrigin.getY()) /module);
579    }
580
581
582    /**
583     * Tests this renderer for equality with an arbitrary object.
584     *
585     * @param obj  the object ({@code null} permitted).
586     *
587     * @return A boolean.
588     */
589    @Override
590    public boolean equals(Object obj) {
591        if (obj == this) {
592            return true;
593        }
594        if (!(obj instanceof XYBezierRenderer)) {
595            return false;
596        }
597        XYBezierRenderer that = (XYBezierRenderer) obj;
598        if (this.precision != that.precision) {
599            return false;
600        }
601        if (this.fillType != that.fillType) {
602            return false;
603        }
604        if (!Objects.equals(this.gradientPaintTransformer, that.gradientPaintTransformer)) {
605            return false;
606        }
607        return super.equals(obj);
608    }
609}