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