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 * XYAreaRenderer.java
029 * -------------------
030 * (C) Copyright 2002-present, by Hari and Contributors.
031 *
032 * Original Author:  Hari (ourhari@hotmail.com);
033 * Contributor(s):   David Gilbert;
034 *                   Richard Atkinson;
035 *                   Christian W. Zuckschwerdt;
036 *                   Martin Krauskopf;
037 *                   Ulrich Voigt (patch #312);
038 */
039
040package org.jfree.chart.renderer.xy;
041
042import java.awt.BasicStroke;
043import java.awt.GradientPaint;
044import java.awt.Graphics2D;
045import java.awt.Paint;
046import java.awt.Shape;
047import java.awt.Stroke;
048import java.awt.geom.Area;
049import java.awt.geom.GeneralPath;
050import java.awt.geom.Line2D;
051import java.awt.geom.Rectangle2D;
052import java.io.IOException;
053import java.io.ObjectInputStream;
054import java.io.ObjectOutputStream;
055
056import org.jfree.chart.HashUtils;
057import org.jfree.chart.LegendItem;
058import org.jfree.chart.axis.ValueAxis;
059import org.jfree.chart.entity.EntityCollection;
060import org.jfree.chart.event.RendererChangeEvent;
061import org.jfree.chart.labels.XYSeriesLabelGenerator;
062import org.jfree.chart.labels.XYToolTipGenerator;
063import org.jfree.chart.plot.CrosshairState;
064import org.jfree.chart.plot.PlotOrientation;
065import org.jfree.chart.plot.PlotRenderingInfo;
066import org.jfree.chart.plot.XYPlot;
067import org.jfree.chart.ui.GradientPaintTransformer;
068import org.jfree.chart.ui.StandardGradientPaintTransformer;
069import org.jfree.chart.urls.XYURLGenerator;
070import org.jfree.chart.util.Args;
071import org.jfree.chart.util.PublicCloneable;
072import org.jfree.chart.util.SerialUtils;
073import org.jfree.chart.util.ShapeUtils;
074import org.jfree.data.xy.XYDataset;
075
076/**
077 * Area item renderer for an {@link XYPlot}.  This class can draw (a) shapes at
078 * each point, or (b) lines between points, or (c) both shapes and lines,
079 * or (d) filled areas, or (e) filled areas and shapes. The example shown here
080 * is generated by the {@code XYAreaRendererDemo1.java} program included
081 * in the JFreeChart demo collection:
082 * <br><br>
083 * <img src="doc-files/XYAreaRendererSample.png" alt="XYAreaRendererSample.png">
084 */
085public class XYAreaRenderer extends AbstractXYItemRenderer
086        implements XYItemRenderer, PublicCloneable {
087
088    /** For serialization. */
089    private static final long serialVersionUID = -4481971353973876747L;
090
091    /**
092     * A state object used by this renderer.
093     */
094    static class XYAreaRendererState extends XYItemRendererState {
095
096        /** Working storage for the area under one series. */
097        public GeneralPath area;
098
099        /** Working line that can be recycled. */
100        public Line2D line;
101
102        /**
103         * Creates a new state.
104         *
105         * @param info  the plot rendering info.
106         */
107        public XYAreaRendererState(PlotRenderingInfo info) {
108            super(info);
109            this.area = new GeneralPath();
110            this.line = new Line2D.Double();
111        }
112
113    }
114
115    /** Useful constant for specifying the type of rendering (shapes only). */
116    public static final int SHAPES = 1;
117
118    /** Useful constant for specifying the type of rendering (lines only). */
119    public static final int LINES = 2;
120
121    /**
122     * Useful constant for specifying the type of rendering (shapes and lines).
123     */
124    public static final int SHAPES_AND_LINES = 3;
125
126    /** Useful constant for specifying the type of rendering (area only). */
127    public static final int AREA = 4;
128
129    /**
130     * Useful constant for specifying the type of rendering (area and shapes).
131     */
132    public static final int AREA_AND_SHAPES = 5;
133
134    /** A flag indicating whether or not shapes are drawn at each XY point. */
135    private boolean plotShapes;
136
137    /** A flag indicating whether or not lines are drawn between XY points. */
138    private boolean plotLines;
139
140    /** A flag indicating whether or not Area are drawn at each XY point. */
141    private boolean plotArea;
142
143    /** A flag that controls whether or not the outline is shown. */
144    private boolean showOutline;
145
146    /**
147     * The shape used to represent an area in each legend item (this should
148     * never be {@code null}).
149     */
150    private transient Shape legendArea;
151
152    /**
153     * A flag that can be set to specify that the fill paint should be used
154     * to fill the area under the renderer.
155     */
156    private boolean useFillPaint;
157
158    /**
159     * A transformer that is applied to the paint used to fill under the
160     * area *if* it is an instance of GradientPaint.
161     */
162    private GradientPaintTransformer gradientTransformer;
163
164    /**
165     * Constructs a new renderer.
166     */
167    public XYAreaRenderer() {
168        this(AREA);
169    }
170
171    /**
172     * Constructs a new renderer.
173     *
174     * @param type  the type of the renderer.
175     */
176    public XYAreaRenderer(int type) {
177        this(type, null, null);
178    }
179
180    /**
181     * Constructs a new renderer.  To specify the type of renderer, use one of
182     * the constants: {@code SHAPES}, {@code LINES}, {@code SHAPES_AND_LINES}, 
183     * {@code AREA} or {@code AREA_AND_SHAPES}.
184     *
185     * @param type  the type of renderer.
186     * @param toolTipGenerator  the tool tip generator ({@code null} permitted).
187     * @param urlGenerator  the URL generator ({@code null} permitted).
188     */
189    public XYAreaRenderer(int type, XYToolTipGenerator toolTipGenerator,
190                          XYURLGenerator urlGenerator) {
191
192        super();
193        setDefaultToolTipGenerator(toolTipGenerator);
194        setURLGenerator(urlGenerator);
195
196        if (type == SHAPES) {
197            this.plotShapes = true;
198        }
199        if (type == LINES) {
200            this.plotLines = true;
201        }
202        if (type == SHAPES_AND_LINES) {
203            this.plotShapes = true;
204            this.plotLines = true;
205        }
206        if (type == AREA) {
207            this.plotArea = true;
208        }
209        if (type == AREA_AND_SHAPES) {
210            this.plotArea = true;
211            this.plotShapes = true;
212        }
213        this.showOutline = false;
214        GeneralPath area = new GeneralPath();
215        area.moveTo(0.0f, -4.0f);
216        area.lineTo(3.0f, -2.0f);
217        area.lineTo(4.0f, 4.0f);
218        area.lineTo(-4.0f, 4.0f);
219        area.lineTo(-3.0f, -2.0f);
220        area.closePath();
221        this.legendArea = area;
222        this.useFillPaint = false;
223        this.gradientTransformer = new StandardGradientPaintTransformer();
224    }
225
226    /**
227     * Returns true if shapes are being plotted by the renderer.
228     *
229     * @return {@code true} if shapes are being plotted by the renderer.
230     */
231    public boolean getPlotShapes() {
232        return this.plotShapes;
233    }
234
235    /**
236     * Returns true if lines are being plotted by the renderer.
237     *
238     * @return {@code true} if lines are being plotted by the renderer.
239     */
240    public boolean getPlotLines() {
241        return this.plotLines;
242    }
243
244    /**
245     * Returns true if Area is being plotted by the renderer.
246     *
247     * @return {@code true} if Area is being plotted by the renderer.
248     */
249    public boolean getPlotArea() {
250        return this.plotArea;
251    }
252
253    /**
254     * Returns a flag that controls whether or not outlines of the areas are
255     * drawn.
256     *
257     * @return The flag.
258     *
259     * @see #setOutline(boolean)
260     */
261    public boolean isOutline() {
262        return this.showOutline;
263    }
264
265    /**
266     * Sets a flag that controls whether or not outlines of the areas are drawn
267     * and sends a {@link RendererChangeEvent} to all registered listeners.
268     *
269     * @param show  the flag.
270     *
271     * @see #isOutline()
272     */
273    public void setOutline(boolean show) {
274        this.showOutline = show;
275        fireChangeEvent();
276    }
277
278    /**
279     * Returns the shape used to represent an area in the legend.
280     *
281     * @return The legend area (never {@code null}).
282     */
283    public Shape getLegendArea() {
284        return this.legendArea;
285    }
286
287    /**
288     * Sets the shape used as an area in each legend item and sends a
289     * {@link RendererChangeEvent} to all registered listeners.
290     *
291     * @param area  the area ({@code null} not permitted).
292     */
293    public void setLegendArea(Shape area) {
294        Args.nullNotPermitted(area, "area");
295        this.legendArea = area;
296        fireChangeEvent();
297    }
298
299    /**
300     * Returns the flag that controls whether the series fill paint is used to
301     * fill the area under the line.
302     *
303     * @return A boolean.
304     */
305    public boolean getUseFillPaint() {
306        return this.useFillPaint;
307    }
308
309    /**
310     * Sets the flag that controls whether or not the series fill paint is
311     * used to fill the area under the line and sends a
312     * {@link RendererChangeEvent} to all listeners.
313     *
314     * @param use  the new flag value.
315     */
316    public void setUseFillPaint(boolean use) {
317        this.useFillPaint = use;
318        fireChangeEvent();
319    }
320
321    /**
322     * Returns the gradient paint transformer.
323     *
324     * @return The gradient paint transformer (never {@code null}).
325     */
326    public GradientPaintTransformer getGradientTransformer() {
327        return this.gradientTransformer;
328    }
329
330    /**
331     * Sets the gradient paint transformer and sends a
332     * {@link RendererChangeEvent} to all registered listeners.
333     *
334     * @param transformer  the transformer ({@code null} not permitted).
335     */
336    public void setGradientTransformer(GradientPaintTransformer transformer) {
337        Args.nullNotPermitted(transformer, "transformer");
338        this.gradientTransformer = transformer;
339        fireChangeEvent();
340    }
341
342    /**
343     * Initialises the renderer and returns a state object that should be
344     * passed to all subsequent calls to the drawItem() method.
345     *
346     * @param g2  the graphics device.
347     * @param dataArea  the area inside the axes.
348     * @param plot  the plot.
349     * @param data  the data.
350     * @param info  an optional info collection object to return data back to
351     *              the caller.
352     *
353     * @return A state object for use by the renderer.
354     */
355    @Override
356    public XYItemRendererState initialise(Graphics2D g2, Rectangle2D dataArea,
357            XYPlot plot, XYDataset data, PlotRenderingInfo info) {
358        XYAreaRendererState state = new XYAreaRendererState(info);
359
360        // in the rendering process, there is special handling for item
361        // zero, so we can't support processing of visible data items only
362        state.setProcessVisibleItemsOnly(false);
363        return state;
364    }
365
366    /**
367     * Returns a default legend item for the specified series.  Subclasses
368     * should override this method to generate customised items.
369     *
370     * @param datasetIndex  the dataset index (zero-based).
371     * @param series  the series index (zero-based).
372     *
373     * @return A legend item for the series.
374     */
375    @Override
376    public LegendItem getLegendItem(int datasetIndex, int series) {
377        LegendItem result = null;
378        XYPlot xyplot = getPlot();
379        if (xyplot != null) {
380            XYDataset dataset = xyplot.getDataset(datasetIndex);
381            if (dataset != null) {
382                XYSeriesLabelGenerator lg = getLegendItemLabelGenerator();
383                String label = lg.generateLabel(dataset, series);
384                String description = label;
385                String toolTipText = null;
386                if (getLegendItemToolTipGenerator() != null) {
387                    toolTipText = getLegendItemToolTipGenerator().generateLabel(
388                            dataset, series);
389                }
390                String urlText = null;
391                if (getLegendItemURLGenerator() != null) {
392                    urlText = getLegendItemURLGenerator().generateLabel(
393                            dataset, series);
394                }
395                Paint paint = lookupSeriesPaint(series);
396                result = new LegendItem(label, description, toolTipText,
397                        urlText, this.legendArea, paint);
398                result.setLabelFont(lookupLegendTextFont(series));
399                Paint labelPaint = lookupLegendTextPaint(series);
400                if (labelPaint != null) {
401                    result.setLabelPaint(labelPaint);
402                }
403                result.setDataset(dataset);
404                result.setDatasetIndex(datasetIndex);
405                result.setSeriesKey(dataset.getSeriesKey(series));
406                result.setSeriesIndex(series);
407            }
408        }
409        return result;
410    }
411
412    /**
413     * Draws the visual representation of a single data item.
414     *
415     * @param g2  the graphics device.
416     * @param state  the renderer state.
417     * @param dataArea  the area within which the data is being drawn.
418     * @param info  collects information about the drawing.
419     * @param plot  the plot (can be used to obtain standard color information
420     *              etc).
421     * @param domainAxis  the domain axis.
422     * @param rangeAxis  the range axis.
423     * @param dataset  the dataset.
424     * @param series  the series index (zero-based).
425     * @param item  the item index (zero-based).
426     * @param crosshairState  crosshair information for the plot
427     *                        ({@code null} permitted).
428     * @param pass  the pass index.
429     */
430    @Override
431    public void drawItem(Graphics2D g2, XYItemRendererState state,
432            Rectangle2D dataArea, PlotRenderingInfo info, XYPlot plot,
433            ValueAxis domainAxis, ValueAxis rangeAxis, XYDataset dataset,
434            int series, int item, CrosshairState crosshairState, int pass) {
435
436        if (!getItemVisible(series, item)) {
437            return;
438        }
439        XYAreaRendererState areaState = (XYAreaRendererState) state;
440
441        // get the data point...
442        double x1 = dataset.getXValue(series, item);
443        double y1 = dataset.getYValue(series, item);
444        if (Double.isNaN(y1)) {
445            y1 = 0.0;
446        }
447        double transX1 = domainAxis.valueToJava2D(x1, dataArea,
448                plot.getDomainAxisEdge());
449        double transY1 = rangeAxis.valueToJava2D(y1, dataArea,
450                plot.getRangeAxisEdge());
451
452        // get the previous point and the next point so we can calculate a
453        // "hot spot" for the area (used by the chart entity)...
454        int itemCount = dataset.getItemCount(series);
455        double x0 = dataset.getXValue(series, Math.max(item - 1, 0));
456        double y0 = dataset.getYValue(series, Math.max(item - 1, 0));
457        if (Double.isNaN(y0)) {
458            y0 = 0.0;
459        }
460        double transX0 = domainAxis.valueToJava2D(x0, dataArea,
461                plot.getDomainAxisEdge());
462        double transY0 = rangeAxis.valueToJava2D(y0, dataArea,
463                plot.getRangeAxisEdge());
464
465        double x2 = dataset.getXValue(series, Math.min(item + 1,
466                itemCount - 1));
467        double y2 = dataset.getYValue(series, Math.min(item + 1,
468                itemCount - 1));
469        if (Double.isNaN(y2)) {
470            y2 = 0.0;
471        }
472        double transX2 = domainAxis.valueToJava2D(x2, dataArea,
473                plot.getDomainAxisEdge());
474        double transY2 = rangeAxis.valueToJava2D(y2, dataArea,
475                plot.getRangeAxisEdge());
476
477        double transZero = rangeAxis.valueToJava2D(0.0, dataArea,
478                plot.getRangeAxisEdge());
479
480        if (item == 0) {  // create a new area polygon for the series
481            areaState.area = new GeneralPath();
482            // the first point is (x, 0)
483            double zero = rangeAxis.valueToJava2D(0.0, dataArea,
484                    plot.getRangeAxisEdge());
485            if (plot.getOrientation().isVertical()) {
486                moveTo(areaState.area, transX1, zero);
487            } else if (plot.getOrientation().isHorizontal()) {
488                moveTo(areaState.area, zero, transX1);
489            }
490        }
491
492        // Add each point to Area (x, y)
493        if (plot.getOrientation().isVertical()) {
494            lineTo(areaState.area, transX1, transY1);
495        } else if (plot.getOrientation().isHorizontal()) {
496            lineTo(areaState.area, transY1, transX1);
497        }
498
499        PlotOrientation orientation = plot.getOrientation();
500        Paint paint = getItemPaint(series, item);
501        Stroke stroke = getItemStroke(series, item);
502        g2.setPaint(paint);
503        g2.setStroke(stroke);
504
505        Shape shape;
506        if (getPlotShapes()) {
507            shape = getItemShape(series, item);
508            if (orientation == PlotOrientation.VERTICAL) {
509                shape = ShapeUtils.createTranslatedShape(shape, transX1,
510                        transY1);
511            } else if (orientation == PlotOrientation.HORIZONTAL) {
512                shape = ShapeUtils.createTranslatedShape(shape, transY1,
513                        transX1);
514            }
515            g2.draw(shape);
516        }
517
518        if (getPlotLines()) {
519            if (item > 0) {
520                if (plot.getOrientation() == PlotOrientation.VERTICAL) {
521                    areaState.line.setLine(transX0, transY0, transX1, transY1);
522                } else if (plot.getOrientation() == PlotOrientation.HORIZONTAL) {
523                    areaState.line.setLine(transY0, transX0, transY1, transX1);
524                }
525                g2.draw(areaState.line);
526            }
527        }
528
529        // Check if the item is the last item for the series.
530        // and number of items > 0.  We can't draw an area for a single point.
531        if (getPlotArea() && item > 0 && item == (itemCount - 1)) {
532
533            if (orientation == PlotOrientation.VERTICAL) {
534                // Add the last point (x,0)
535                lineTo(areaState.area, transX1, transZero);
536                areaState.area.closePath();
537            } else if (orientation == PlotOrientation.HORIZONTAL) {
538                // Add the last point (x,0)
539                lineTo(areaState.area, transZero, transX1);
540                areaState.area.closePath();
541            }
542
543            if (this.useFillPaint) {
544                paint = lookupSeriesFillPaint(series);
545            }
546            if (paint instanceof GradientPaint) {
547                GradientPaint gp = (GradientPaint) paint;
548                GradientPaint adjGP = this.gradientTransformer.transform(gp,
549                        dataArea);
550                g2.setPaint(adjGP);
551            }
552            g2.fill(areaState.area);
553
554            // draw an outline around the Area.
555            if (isOutline()) {
556                Shape area = areaState.area;
557
558                // Java2D has some issues drawing dashed lines around "large"
559                // geometrical shapes - for example, see bug 6620013 in the
560                // Java bug database.  So, we'll check if the outline is
561                // dashed and, if it is, do our own clipping before drawing
562                // the outline...
563                Stroke outlineStroke = lookupSeriesOutlineStroke(series);
564                if (outlineStroke instanceof BasicStroke) {
565                    BasicStroke bs = (BasicStroke) outlineStroke;
566                    if (bs.getDashArray() != null) {
567                        Area poly = new Area(areaState.area);
568                        // we make the clip region slightly larger than the
569                        // dataArea so that the clipped edges don't show lines
570                        // on the chart
571                        Area clip = new Area(new Rectangle2D.Double(
572                                dataArea.getX() - 5.0, dataArea.getY() - 5.0,
573                                dataArea.getWidth() + 10.0,
574                                dataArea.getHeight() + 10.0));
575                        poly.intersect(clip);
576                        area = poly;
577                    }
578                } // end of workaround
579
580                g2.setStroke(outlineStroke);
581                g2.setPaint(lookupSeriesOutlinePaint(series));
582                g2.draw(area);
583            }
584        }
585
586        int datasetIndex = plot.indexOf(dataset);
587        updateCrosshairValues(crosshairState, x1, y1, datasetIndex,
588                transX1, transY1, orientation);
589
590        // collect entity and tool tip information...
591        EntityCollection entities = state.getEntityCollection();
592        if (entities != null) {
593            GeneralPath hotspot = new GeneralPath();
594            if (plot.getOrientation() == PlotOrientation.HORIZONTAL) {
595                moveTo(hotspot, transZero, ((transX0 + transX1) / 2.0));
596                lineTo(hotspot, ((transY0 + transY1) / 2.0), ((transX0 + transX1) / 2.0));
597                lineTo(hotspot, transY1, transX1);
598                lineTo(hotspot, ((transY1 + transY2) / 2.0), ((transX1 + transX2) / 2.0));
599                lineTo(hotspot, transZero, ((transX1 + transX2) / 2.0));
600            } else { // vertical orientation
601                moveTo(hotspot, ((transX0 + transX1) / 2.0), transZero);
602                lineTo(hotspot, ((transX0 + transX1) / 2.0), ((transY0 + transY1) / 2.0));
603                lineTo(hotspot, transX1, transY1);
604                lineTo(hotspot, ((transX1 + transX2) / 2.0), ((transY1 + transY2) / 2.0));
605                lineTo(hotspot, ((transX1 + transX2) / 2.0), transZero);
606            }
607            hotspot.closePath();
608
609            // limit the entity hotspot area to the data area
610            Area dataAreaHotspot = new Area(hotspot);
611            dataAreaHotspot.intersect(new Area(dataArea));
612
613            if (!dataAreaHotspot.isEmpty()) {
614                addEntity(entities, dataAreaHotspot, dataset, series, item, 
615                        0.0, 0.0);
616            }
617        }
618
619    }
620
621    /**
622     * Returns a clone of the renderer.
623     *
624     * @return A clone.
625     *
626     * @throws CloneNotSupportedException  if the renderer cannot be cloned.
627     */
628    @Override
629    public Object clone() throws CloneNotSupportedException {
630        XYAreaRenderer clone = (XYAreaRenderer) super.clone();
631        clone.legendArea = ShapeUtils.clone(this.legendArea);
632        return clone;
633    }
634
635    /**
636     * Tests this renderer for equality with an arbitrary object.
637     *
638     * @param obj  the object ({@code null} permitted).
639     *
640     * @return A boolean.
641     */
642    @Override
643    public boolean equals(Object obj) {
644        if (obj == this) {
645            return true;
646        }
647        if (!(obj instanceof XYAreaRenderer)) {
648            return false;
649        }
650        XYAreaRenderer that = (XYAreaRenderer) obj;
651        if (this.plotArea != that.plotArea) {
652            return false;
653        }
654        if (this.plotLines != that.plotLines) {
655            return false;
656        }
657        if (this.plotShapes != that.plotShapes) {
658            return false;
659        }
660        if (this.showOutline != that.showOutline) {
661            return false;
662        }
663        if (this.useFillPaint != that.useFillPaint) {
664            return false;
665        }
666        if (!this.gradientTransformer.equals(that.gradientTransformer)) {
667            return false;
668        }
669        if (!ShapeUtils.equal(this.legendArea, that.legendArea)) {
670            return false;
671        }
672        return true;
673    }
674
675    /**
676     * Returns a hash code for this instance.
677     *
678     * @return A hash code.
679     */
680    @Override
681    public int hashCode() {
682        int result = super.hashCode();
683        result = HashUtils.hashCode(result, this.plotArea);
684        result = HashUtils.hashCode(result, this.plotLines);
685        result = HashUtils.hashCode(result, this.plotShapes);
686        result = HashUtils.hashCode(result, this.useFillPaint);
687        return result;
688    }
689
690    /**
691     * Provides serialization support.
692     *
693     * @param stream  the input stream.
694     *
695     * @throws IOException  if there is an I/O error.
696     * @throws ClassNotFoundException  if there is a classpath problem.
697     */
698    private void readObject(ObjectInputStream stream)
699            throws IOException, ClassNotFoundException {
700        stream.defaultReadObject();
701        this.legendArea = SerialUtils.readShape(stream);
702    }
703
704    /**
705     * Provides serialization support.
706     *
707     * @param stream  the output stream.
708     *
709     * @throws IOException  if there is an I/O error.
710     */
711    private void writeObject(ObjectOutputStream stream) throws IOException {
712        stream.defaultWriteObject();
713        SerialUtils.writeShape(this.legendArea, stream);
714    }
715}