View Javadoc
1   package org.opentrafficsim.graphs;
2   
3   import java.awt.BorderLayout;
4   import java.awt.Color;
5   import java.awt.event.ActionEvent;
6   import java.awt.event.ActionListener;
7   import java.awt.geom.Line2D;
8   import java.util.ArrayList;
9   import java.util.HashMap;
10  import java.util.List;
11  
12  import javax.swing.JFrame;
13  import javax.swing.JLabel;
14  import javax.swing.JPopupMenu;
15  import javax.swing.SwingConstants;
16  import javax.swing.event.EventListenerList;
17  
18  import org.djunits.unit.LengthUnit;
19  import org.djunits.value.ValueException;
20  import org.djunits.value.vdouble.scalar.DoubleScalar;
21  import org.djunits.value.vdouble.vector.DoubleVector;
22  import org.jfree.chart.ChartFactory;
23  import org.jfree.chart.ChartPanel;
24  import org.jfree.chart.JFreeChart;
25  import org.jfree.chart.StandardChartTheme;
26  import org.jfree.chart.axis.NumberAxis;
27  import org.jfree.chart.axis.ValueAxis;
28  import org.jfree.chart.plot.PlotOrientation;
29  import org.jfree.chart.plot.XYPlot;
30  import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
31  import org.jfree.data.DomainOrder;
32  import org.jfree.data.general.DatasetChangeEvent;
33  import org.jfree.data.general.DatasetChangeListener;
34  import org.jfree.data.general.DatasetGroup;
35  import org.jfree.data.xy.XYDataset;
36  import org.opentrafficsim.core.OTS_SCALAR;
37  import org.opentrafficsim.core.network.NetworkException;
38  import org.opentrafficsim.road.gtu.lane.AbstractLaneBasedGTU;
39  import org.opentrafficsim.road.network.lane.Lane;
40  
41  /**
42   * Trajectory plot.
43   * <p>
44   * Copyright (c) 2013-2015 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
45   * BSD-style license. See <a href="http://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
46   * <p>
47   * $LastChangedDate: 2015-09-14 01:33:02 +0200 (Mon, 14 Sep 2015) $, @version $Revision: 1401 $, by $Author: averbraeck $,
48   * initial version Jul 24, 2014 <br>
49   * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
50   */
51  public class TrajectoryPlot extends JFrame implements ActionListener, XYDataset, MultipleViewerChart, LaneBasedGTUSampler,
52      OTS_SCALAR
53  {
54      /** */
55      private static final long serialVersionUID = 20140724L;
56  
57      /** Sample interval of this TrajectoryPlot. */
58      private final Time.Rel sampleInterval;
59  
60      /**
61       * @return sampleInterval
62       */
63      public final Time.Rel getSampleInterval()
64      {
65          return this.sampleInterval;
66      }
67  
68      /** The series of Lanes that provide the data for this TrajectoryPlot. */
69      private final ArrayList<Lane> path;
70  
71      /** The cumulative lengths of the elements of path. */
72      private final DoubleVector.Rel.Dense<LengthUnit> cumulativeLengths;
73  
74      /**
75       * Retrieve the cumulative length of the sampled path at the end of a path element.
76       * @param index int; the index of the path element; if -1, the total length of the path is returned
77       * @return DoubleScalar.Rel&lt;LengthUnit&gt;; the cumulative length at the end of the specified path element
78       */
79      public final Length.Rel getCumulativeLength(final int index)
80      {
81          int useIndex = -1 == index ? this.cumulativeLengths.size() - 1 : index;
82          try
83          {
84              return new Length.Rel(this.cumulativeLengths.get(useIndex));
85          }
86          catch (ValueException exception)
87          {
88              exception.printStackTrace();
89          }
90          return null; // NOTREACHED
91      }
92  
93      /** Maximum of the time axis. */
94      private Time.Abs maximumTime = new Time.Abs(300, SECOND);
95  
96      /**
97       * @return maximumTime
98       */
99      public final Time.Abs getMaximumTime()
100     {
101         return this.maximumTime;
102     }
103 
104     /**
105      * @param maximumTime set maximumTime
106      */
107     public final void setMaximumTime(final Time.Abs maximumTime)
108     {
109         this.maximumTime = maximumTime;
110     }
111 
112     /** List of parties interested in changes of this ContourPlot. */
113     private transient EventListenerList listenerList = new EventListenerList();
114 
115     /** Not used internally. */
116     private DatasetGroup datasetGroup = null;
117 
118     /** Name of the chart. */
119     private final String caption;
120 
121     /**
122      * Create a new TrajectoryPlot.
123      * @param caption String; the text to show above the TrajectoryPlot
124      * @param sampleInterval DoubleScalarRel&lt;TimeUnit&gt;; the time between samples of this TrajectoryPlot
125      * @param path ArrayList&lt;Lane&gt;; the series of Lanes that will provide the data for this TrajectoryPlot
126      */
127     public TrajectoryPlot(final String caption, final Time.Rel sampleInterval, final List<Lane> path)
128     {
129         this.sampleInterval = sampleInterval;
130         this.path = new ArrayList<Lane>(path); // make a copy
131         double[] endLengths = new double[path.size()];
132         double cumulativeLength = 0;
133         DoubleVector.Rel.Dense<LengthUnit> lengths = null;
134         for (int i = 0; i < path.size(); i++)
135         {
136             Lane lane = path.get(i);
137             lane.addSampler(this);
138             cumulativeLength += lane.getLength().getSI();
139             endLengths[i] = cumulativeLength;
140         }
141         try
142         {
143             lengths = new DoubleVector.Rel.Dense<LengthUnit>(endLengths, LengthUnit.SI);
144         }
145         catch (ValueException exception)
146         {
147             exception.printStackTrace();
148         }
149         this.cumulativeLengths = lengths;
150         this.caption = caption;
151         createChart(this);
152         this.reGraph(); // fixes the domain axis
153     }
154 
155     /**
156      * Create the visualization.
157      * @param container JFrame; the JFrame that will be filled with chart and the status label
158      * @return JFreeChart; the visualization
159      */
160     private JFreeChart createChart(final JFrame container)
161     {
162         final JLabel statusLabel = new JLabel(" ", SwingConstants.CENTER);
163         container.add(statusLabel, BorderLayout.SOUTH);
164         ChartFactory.setChartTheme(new StandardChartTheme("JFree/Shadow", false));
165         final JFreeChart result =
166             ChartFactory.createXYLineChart(this.caption, "", "", this, PlotOrientation.VERTICAL, false, false, false);
167         // Overrule the default background paint because some of the lines are invisible on top of this default.
168         result.getPlot().setBackgroundPaint(new Color(0.9f, 0.9f, 0.9f));
169         FixCaption.fixCaption(result);
170         NumberAxis xAxis = new NumberAxis("\u2192 " + "time [s]");
171         xAxis.setLowerMargin(0.0);
172         xAxis.setUpperMargin(0.0);
173         NumberAxis yAxis = new NumberAxis("\u2192 " + "Distance [m]");
174         yAxis.setAutoRangeIncludesZero(false);
175         yAxis.setLowerMargin(0.0);
176         yAxis.setUpperMargin(0.0);
177         yAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits());
178         result.getXYPlot().setDomainAxis(xAxis);
179         result.getXYPlot().setRangeAxis(yAxis);
180         Length.Rel minimumPosition = new Length.Rel(0, LengthUnit.SI);
181         Length.Rel maximumPosition = getCumulativeLength(-1);
182         configureAxis(result.getXYPlot().getRangeAxis(), DoubleScalar.minus(maximumPosition, minimumPosition).getSI());
183         final XYLineAndShapeRenderer renderer = (XYLineAndShapeRenderer) result.getXYPlot().getRenderer();
184         renderer.setBaseLinesVisible(true);
185         renderer.setBaseShapesVisible(false);
186         renderer.setBaseShape(new Line2D.Float(0, 0, 0, 0));
187         final ChartPanel cp = new ChartPanel(result);
188         cp.setMouseWheelEnabled(true);
189         final PointerHandler ph = new PointerHandler()
190         {
191             /** {@inheritDoc} */
192             @Override
193             void updateHint(final double domainValue, final double rangeValue)
194             {
195                 if (Double.isNaN(domainValue))
196                 {
197                     statusLabel.setText(" ");
198                     return;
199                 }
200                 String value = "";
201                 /*-
202                 XYDataset dataset = plot.getDataset();
203                 double bestDistance = Double.MAX_VALUE;
204                 Trajectory bestTrajectory = null;
205                 final int mousePrecision = 5;
206                 java.awt.geom.Point2D.Double mousePoint = new java.awt.geom.Point2D.Double(t, distance);
207                 double lowTime =
208                         plot.getDomainAxis().java2DToValue(p.getX() - mousePrecision, pi.getDataArea(),
209                                 plot.getDomainAxisEdge()) - 1;
210                 double highTime =
211                         plot.getDomainAxis().java2DToValue(p.getX() + mousePrecision, pi.getDataArea(),
212                                 plot.getDomainAxisEdge()) + 1;
213                 double lowDistance =
214                         plot.getRangeAxis().java2DToValue(p.getY() + mousePrecision, pi.getDataArea(),
215                                 plot.getRangeAxisEdge()) - 20;
216                 double highDistance =
217                         plot.getRangeAxis().java2DToValue(p.getY() - mousePrecision, pi.getDataArea(),
218                                 plot.getRangeAxisEdge()) + 20;
219                 // System.out.println(String.format("Searching area t[%.1f-%.1f], x[%.1f,%.1f]", lowTime, highTime,
220                 // lowDistance, highDistance));
221                 for (Trajectory trajectory : this.trajectories)
222                 {
223                     java.awt.geom.Point2D.Double[] clippedTrajectory =
224                             trajectory.clipTrajectory(lowTime, highTime, lowDistance, highDistance);
225                     if (null == clippedTrajectory)
226                         continue;
227                     java.awt.geom.Point2D.Double prevPoint = null;
228                     for (java.awt.geom.Point2D.Double trajectoryPoint : clippedTrajectory)
229                     {
230                         if (null != prevPoint)
231                         {
232                             double thisDistance = Planar.distancePolygonToPoint(clippedTrajectory, mousePoint);
233                             if (thisDistance < bestDistance)
234                             {
235                                 bestDistance = thisDistance;
236                                 bestTrajectory = trajectory;
237                             }
238                         }
239                         prevPoint = trajectoryPoint;
240                     }
241                 }
242                 if (null != bestTrajectory)
243                 {
244                     for (SimulatedObject so : indices.keySet())
245                         if (this.trajectories.get(indices.get(so)) == bestTrajectory)
246                         {
247                             Point2D.Double bestPosition = bestTrajectory.getEstimatedPosition(t);
248                             if (null == bestPosition)
249                                 continue;
250                             value =
251                                     String.format(
252                                             Main.locale,
253                                             ": vehicle %s; location on measurement path at t=%.1fs: "
254                                             + "longitudinal %.1fm, lateral %.1fm",
255                                             so.toString(), t, bestPosition.x, bestPosition.y);
256                         }
257                 }
258                 else
259                     value = "";
260                  */
261                 statusLabel.setText(String.format("t=%.0fs, distance=%.0fm%s", domainValue, rangeValue, value));
262             }
263         };
264         cp.addMouseMotionListener(ph);
265         cp.addMouseListener(ph);
266         container.add(cp, BorderLayout.CENTER);
267         // TODO ensure that shapes for all the data points don't get allocated.
268         // Currently JFreeChart allocates many megabytes of memory for Ellipses that are never drawn.
269         JPopupMenu popupMenu = cp.getPopupMenu();
270         popupMenu.add(new JPopupMenu.Separator());
271         popupMenu.add(StandAloneChartWindow.createMenuItem(this));
272         return result;
273     }
274 
275     /**
276      * Redraw this TrajectoryGraph (after the underlying data has been changed).
277      */
278     public final void reGraph()
279     {
280         for (DatasetChangeListener dcl : this.listenerList.getListeners(DatasetChangeListener.class))
281         {
282             if (dcl instanceof XYPlot)
283             {
284                 configureAxis(((XYPlot) dcl).getDomainAxis(), this.maximumTime.getSI());
285             }
286         }
287         notifyListeners(new DatasetChangeEvent(this, null)); // This guess work actually works!
288     }
289 
290     /**
291      * Notify interested parties of an event affecting this TrajectoryPlot.
292      * @param event DatasetChangedEvent
293      */
294     private void notifyListeners(final DatasetChangeEvent event)
295     {
296         for (DatasetChangeListener dcl : this.listenerList.getListeners(DatasetChangeListener.class))
297         {
298             dcl.datasetChanged(event);
299         }
300     }
301 
302     /**
303      * Configure the range of an axis.
304      * @param valueAxis ValueAxis
305      * @param range double; the upper bound of the axis
306      */
307     private static void configureAxis(final ValueAxis valueAxis, final double range)
308     {
309         valueAxis.setUpperBound(range);
310         valueAxis.setLowerMargin(0);
311         valueAxis.setUpperMargin(0);
312         valueAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits());
313         valueAxis.setAutoRange(true);
314         valueAxis.setAutoRangeMinimumSize(range);
315         valueAxis.centerRange(range / 2);
316     }
317 
318     /** {@inheritDoc} */
319     @Override
320     public void actionPerformed(final ActionEvent e)
321     {
322         // not yet
323     }
324 
325     /** All stored trajectories. */
326     private HashMap<String, Trajectory> trajectories = new HashMap<String, Trajectory>();
327 
328     /** Quick access to the Nth trajectory. */
329     private ArrayList<Trajectory> trajectoryIndices = new ArrayList<Trajectory>();
330 
331     /** {@inheritDoc} */
332     public final void addData(final AbstractLaneBasedGTU car, final Lane lane) throws NetworkException
333     {
334         // final Time.Abs startTime = car.getLastEvaluationTime();
335         // System.out.println("addData car: " + car + ", lastEval: " + startTime);
336         // Convert the position of the car to a position on path.
337         // Find a (the first) lane that car is on that is in our path.
338         double lengthOffset = 0;
339         int index = this.path.indexOf(lane);
340         if (index >= 0)
341         {
342             if (index > 0)
343             {
344                 try
345                 {
346                     lengthOffset = this.cumulativeLengths.getSI(index - 1);
347                 }
348                 catch (ValueException exception)
349                 {
350                     exception.printStackTrace();
351                 }
352             }
353         }
354         else
355         {
356             throw new Error("Car is not on any lane in the path");
357         }
358         // System.out.println("lane index is " + index + " car is " + car);
359         // final Length.Rel startPosition =
360         // DoubleScalar.plus(new Length.Rel(lengthOffset, LengthUnit.SI),
361         // car.position(lane, car.getReference(), startTime));
362         String key = car.getId().toString();
363         Trajectory carTrajectory = this.trajectories.get(key);
364         if (null == carTrajectory)
365         {
366             // Create a new Trajectory for this GTU
367             carTrajectory = new Trajectory(key);
368             this.trajectoryIndices.add(carTrajectory);
369             this.trajectories.put(key, carTrajectory);
370             // System.out.println("Creating new trajectory");
371         }
372         carTrajectory.addSegment(car, lane, lengthOffset);
373     }
374 
375     /**
376      * Store trajectory data.
377      * <p>
378      * Copyright (c) 2013-2015 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
379      * <p>
380      * See for project information <a href="http://www.simulation.tudelft.nl/"> www.simulation.tudelft.nl</a>.
381      * <p>
382      * The OpenTrafficSim project is distributed under the following BSD-style license:<br>
383      * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
384      * following conditions are met:
385      * <ul>
386      * <li>Redistributions of source code must retain the above copyright notice, this list of conditions and the following
387      * disclaimer.</li>
388      * <li>Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
389      * disclaimer in the documentation and/or other materials provided with the distribution.</li>
390      * <li>Neither the name of Delft University of Technology, nor the names of its contributors may be used to endorse or
391      * promote products derived from this software without specific prior written permission.</li>
392      * </ul>
393      * This software is provided by the copyright holders and contributors "as is" and any express or implied warranties,
394      * including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose are
395      * disclaimed. In no event shall the copyright holder or contributors be liable for any direct, indirect, incidental,
396      * special, exemplary, or consequential damages (including, but not limited to, procurement of substitute goods or services;
397      * loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in
398      * contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this
399      * software, even if advised of the possibility of such damage. $LastChangedDate: 2015-07-15 11:18:39 +0200 (Wed, 15 Jul
400      * 2015) $, @version $Revision: 1401 $, by $Author: averbraeck $, initial versionJul 24, 2014 <br>
401      * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
402      */
403     class Trajectory
404     {
405         /** Time of (current) end of trajectory. */
406         private Time.Abs currentEndTime;
407 
408         /**
409          * Retrieve the current end time of this Trajectory.
410          * @return currentEndTime
411          */
412         public final Time.Abs getCurrentEndTime()
413         {
414             return this.currentEndTime;
415         }
416 
417         /** Position of (current) end of trajectory. */
418         private Length.Rel currentEndPosition;
419 
420         /**
421          * Retrieve the current end position of this Trajectory.
422          * @return currentEndPosition
423          */
424         public final Length.Rel getCurrentEndPosition()
425         {
426             return this.currentEndPosition;
427         }
428 
429         /** ID of the GTU. */
430         private final Object id;
431 
432         /**
433          * Retrieve the id of this Trajectory.
434          * @return Object; the id of this Trajectory
435          */
436         public final Object getId()
437         {
438             return this.id;
439         }
440 
441         /** Storage for the position of the car. */
442         private ArrayList<Double> positions = new ArrayList<Double>();
443 
444         /** Time sample of first sample in positions (successive entries will each be one sampleTime later). */
445         private int firstSample;
446 
447         /**
448          * Construct a Trajectory.
449          * @param id Object; Id of the new Trajectory
450          */
451         public Trajectory(final Object id)
452         {
453             this.id = id;
454         }
455 
456         /**
457          * Add a trajectory segment and update the currentEndTime and currentEndPosition.
458          * @param car AbstractLaneBasedGTU&lt;>&gt;; the GTU whose currently committed trajectory segment must be added
459          * @param lane Lane; the Lane that the positionOffset is valid for
460          * @param positionOffset double; offset needed to convert the position in the current Lane to a position on the
461          *            trajectory
462          * @throws NetworkException when car is not on lane anymore
463          */
464         public final void addSegment(final AbstractLaneBasedGTU car, final Lane lane, final double positionOffset)
465             throws NetworkException
466         {
467             final int startSample = (int) Math.ceil(car.getLastEvaluationTime().getSI() / getSampleInterval().getSI());
468             final int endSample = (int) (Math.ceil(car.getNextEvaluationTime().getSI() / getSampleInterval().getSI()));
469             for (int sample = startSample; sample < endSample; sample++)
470             {
471                 Time.Abs sampleTime = new Time.Abs(sample * getSampleInterval().getSI(), SECOND);
472                 Double position = car.position(lane, car.getReference(), sampleTime).getSI() + positionOffset;
473                 if (this.positions.size() > 0 && position < this.currentEndPosition.getSI() - 0.001)
474                 {
475                     if (0 != positionOffset)
476                     {
477                         // System.out.println("Already added " + car);
478                         break;
479                     }
480                     // System.out.println("inserting null for " + car);
481                     position = null; // Wrapping on circular path?
482                 }
483                 if (this.positions.size() == 0)
484                 {
485                     this.firstSample = sample;
486                 }
487                 /*-
488                 if (sample - this.firstSample > this.positions.size())
489                 {
490                     System.out.println("Inserting " + (sample - this.positions.size()) 
491                             + " nulls; this is trajectory number " + trajectoryIndices.indexOf(this));
492                 }
493                  */
494                 while (sample - this.firstSample > this.positions.size())
495                 {
496                     // System.out.println("Inserting nulls");
497                     this.positions.add(null); // insert nulls as place holders for unsampled data (usually because
498                                               // vehicle was temporarily in a parallel Lane)
499                 }
500                 if (null != position && this.positions.size() > sample - this.firstSample)
501                 {
502                     // System.out.println("Skipping sample " + car);
503                     continue;
504                 }
505                 this.positions.add(position);
506             }
507             this.currentEndTime = car.getNextEvaluationTime();
508             this.currentEndPosition =
509                 new Length.Rel(car.position(lane, car.getReference(), this.currentEndTime).getSI() + positionOffset, METER);
510             if (car.getNextEvaluationTime().gt(getMaximumTime()))
511             {
512                 setMaximumTime(car.getNextEvaluationTime());
513             }
514         }
515 
516         /**
517          * Retrieve the number of samples in this Trajectory.
518          * @return Integer; number of positions in this Trajectory
519          */
520         public int size()
521         {
522             return this.positions.size();
523         }
524 
525         /**
526          * @param item Integer; the sample number
527          * @return Double; the time of the sample indexed by item
528          */
529         public double getTime(final int item)
530         {
531             return (item + this.firstSample) * getSampleInterval().getSI();
532         }
533 
534         /**
535          * @param item Integer; the sample number
536          * @return Double; the position indexed by item
537          */
538         public double getDistance(final int item)
539         {
540             Double distance = this.positions.get(item);
541             if (null == distance)
542             {
543                 return Double.NaN;
544             }
545             return this.positions.get(item);
546         }
547     }
548 
549     /** {@inheritDoc} */
550     @Override
551     public final int getSeriesCount()
552     {
553         return this.trajectories.size();
554     }
555 
556     /** {@inheritDoc} */
557     @Override
558     public final Comparable<Integer> getSeriesKey(final int series)
559     {
560         return series;
561     }
562 
563     /** {@inheritDoc} */
564     @SuppressWarnings("rawtypes")
565     @Override
566     public final int indexOf(final Comparable seriesKey)
567     {
568         if (seriesKey instanceof Integer)
569         {
570             return (Integer) seriesKey;
571         }
572         return -1;
573     }
574 
575     /** {@inheritDoc} */
576     @Override
577     public final void addChangeListener(final DatasetChangeListener listener)
578     {
579         this.listenerList.add(DatasetChangeListener.class, listener);
580     }
581 
582     /** {@inheritDoc} */
583     @Override
584     public final void removeChangeListener(final DatasetChangeListener listener)
585     {
586         this.listenerList.remove(DatasetChangeListener.class, listener);
587     }
588 
589     /** {@inheritDoc} */
590     @Override
591     public final DatasetGroup getGroup()
592     {
593         return this.datasetGroup;
594     }
595 
596     /** {@inheritDoc} */
597     @Override
598     public final void setGroup(final DatasetGroup group)
599     {
600         this.datasetGroup = group;
601     }
602 
603     /** {@inheritDoc} */
604     @Override
605     public final DomainOrder getDomainOrder()
606     {
607         return DomainOrder.ASCENDING;
608     }
609 
610     /** {@inheritDoc} */
611     @Override
612     public final int getItemCount(final int series)
613     {
614         return this.trajectoryIndices.get(series).size();
615     }
616 
617     /** {@inheritDoc} */
618     @Override
619     public final Number getX(final int series, final int item)
620     {
621         double v = getXValue(series, item);
622         if (Double.isNaN(v))
623         {
624             return null;
625         }
626         return v;
627     }
628 
629     /** {@inheritDoc} */
630     @Override
631     public final double getXValue(final int series, final int item)
632     {
633         return this.trajectoryIndices.get(series).getTime(item);
634     }
635 
636     /** {@inheritDoc} */
637     @Override
638     public final Number getY(final int series, final int item)
639     {
640         double v = getYValue(series, item);
641         if (Double.isNaN(v))
642         {
643             return null;
644         }
645         return v;
646     }
647 
648     /** {@inheritDoc} */
649     @Override
650     public final double getYValue(final int series, final int item)
651     {
652         return this.trajectoryIndices.get(series).getDistance(item);
653     }
654 
655     /** {@inheritDoc} */
656     @Override
657     public final JFrame addViewer()
658     {
659         JFrame result = new JFrame(this.caption);
660         result.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
661         JFreeChart newChart = createChart(result);
662         newChart.setTitle((String) null);
663         addChangeListener(newChart.getPlot());
664         return result;
665     }
666 
667 }