001 /* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2007, by Object Refinery Limited 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 * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
025 * in the United States and other countries.]
026 *
027 * -------------------------
028 * XYDifferenceRenderer.java
029 * -------------------------
030 * (C) Copyright 2003-2007, by Object Refinery Limited and Contributors.
031 *
032 * Original Author: David Gilbert (for Object Refinery Limited);
033 * Contributor(s): Richard West, Advanced Micro Devices, Inc. (major rewrite
034 * of difference drawing algorithm);
035 *
036 * $Id: XYDifferenceRenderer.java,v 1.12.2.15 2007/05/18 10:28:31 mungady Exp $
037 *
038 * Changes:
039 * --------
040 * 30-Apr-2003 : Version 1 (DG);
041 * 30-Jul-2003 : Modified entity constructor (CZ);
042 * 20-Aug-2003 : Implemented Cloneable and PublicCloneable (DG);
043 * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG);
044 * 09-Feb-2004 : Updated to support horizontal plot orientation (DG);
045 * 10-Feb-2004 : Added default constructor, setter methods and updated
046 * Javadocs (DG);
047 * 25-Feb-2004 : Replaced CrosshairInfo with CrosshairState (DG);
048 * 30-Mar-2004 : Fixed bug in getNegativePaint() method (DG);
049 * 15-Jul-2004 : Switched getX() with getXValue() and getY() with
050 * getYValue() (DG);
051 * 25-Aug-2004 : Fixed a bug preventing the use of crosshairs (DG);
052 * 11-Nov-2004 : Now uses ShapeUtilities to translate shapes (DG);
053 * 19-Jan-2005 : Now accesses only primitive values from dataset (DG);
054 * 22-Feb-2005 : Override getLegendItem(int, int) to return "line" items (DG);
055 * 13-Apr-2005 : Fixed shape positioning bug (id = 1182062) (DG);
056 * 20-Apr-2005 : Use generators for legend tooltips and URLs (DG);
057 * 04-May-2005 : Override equals() method, renamed get/setPlotShapes() -->
058 * get/setShapesVisible (DG);
059 * 09-Jun-2005 : Updated equals() to handle GradientPaint (DG);
060 * 16-Jun-2005 : Fix bug (1221021) affecting stroke used for each series (DG);
061 * ------------- JFREECHART 1.0.x ---------------------------------------------
062 * 24-Jan-2007 : Added flag to allow rounding of x-coordinates, and fixed
063 * bug in clone() (DG);
064 * 05-Feb-2007 : Added an extra call to updateCrosshairValues() in
065 * drawItemPass1(), to fix bug 1564967 (DG);
066 * 06-Feb-2007 : Fixed bug 1086307, crosshairs with multiple axes (DG);
067 * 08-Mar-2007 : Fixed entity generation (DG);
068 * 20-Apr-2007 : Updated getLegendItem() for renderer change (DG);
069 * 23-Apr-2007 : Rewrite of difference drawing algorithm to allow use of
070 * series with disjoint x-values (RW);
071 * 04-May-2007 : Set processVisibleItemsOnly flag to false (DG);
072 * 17-May-2007 : Set datasetIndex and seriesIndex in getLegendItem() (DG);
073 * 18-May-2007 : Set dataset and seriesKey for LegendItem (DG);
074 *
075 */
076
077 package org.jfree.chart.renderer.xy;
078
079 import java.awt.Color;
080 import java.awt.Graphics2D;
081 import java.awt.Paint;
082 import java.awt.Shape;
083 import java.awt.Stroke;
084 import java.awt.geom.GeneralPath;
085 import java.awt.geom.Line2D;
086 import java.awt.geom.Rectangle2D;
087 import java.io.IOException;
088 import java.io.ObjectInputStream;
089 import java.io.ObjectOutputStream;
090 import java.io.Serializable;
091 import java.util.Collections;
092 import java.util.LinkedList;
093
094 import org.jfree.chart.LegendItem;
095 import org.jfree.chart.axis.ValueAxis;
096 import org.jfree.chart.entity.EntityCollection;
097 import org.jfree.chart.entity.XYItemEntity;
098 import org.jfree.chart.event.RendererChangeEvent;
099 import org.jfree.chart.labels.XYToolTipGenerator;
100 import org.jfree.chart.plot.CrosshairState;
101 import org.jfree.chart.plot.PlotOrientation;
102 import org.jfree.chart.plot.PlotRenderingInfo;
103 import org.jfree.chart.plot.XYPlot;
104 import org.jfree.chart.urls.XYURLGenerator;
105 import org.jfree.data.xy.XYDataset;
106 import org.jfree.io.SerialUtilities;
107 import org.jfree.ui.RectangleEdge;
108 import org.jfree.util.PaintUtilities;
109 import org.jfree.util.PublicCloneable;
110 import org.jfree.util.ShapeUtilities;
111
112 /**
113 * A renderer for an {@link XYPlot} that highlights the differences between two
114 * series.
115 */
116 public class XYDifferenceRenderer extends AbstractXYItemRenderer
117 implements XYItemRenderer,
118 Cloneable,
119 PublicCloneable,
120 Serializable {
121
122 /** For serialization. */
123 private static final long serialVersionUID = -8447915602375584857L;
124
125 /** The paint used to highlight positive differences (y(0) > y(1)). */
126 private transient Paint positivePaint;
127
128 /** The paint used to highlight negative differences (y(0) < y(1)). */
129 private transient Paint negativePaint;
130
131 /** Display shapes at each point? */
132 private boolean shapesVisible;
133
134 /** The shape to display in the legend item. */
135 private transient Shape legendLine;
136
137 /**
138 * This flag controls whether or not the x-coordinates (in Java2D space)
139 * are rounded to integers. When set to true, this can avoid the vertical
140 * striping that anti-aliasing can generate. However, the rounding may not
141 * be appropriate for output in high resolution formats (for example,
142 * vector graphics formats such as SVG and PDF).
143 *
144 * @since 1.0.4
145 */
146 private boolean roundXCoordinates;
147
148 /**
149 * Creates a new renderer with default attributes.
150 */
151 public XYDifferenceRenderer() {
152 this(Color.green, Color.red, false);
153 }
154
155 /**
156 * Creates a new renderer.
157 *
158 * @param positivePaint the highlight color for positive differences
159 * (<code>null</code> not permitted).
160 * @param negativePaint the highlight color for negative differences
161 * (<code>null</code> not permitted).
162 * @param shapes draw shapes?
163 */
164 public XYDifferenceRenderer(Paint positivePaint, Paint negativePaint,
165 boolean shapes) {
166 if (positivePaint == null) {
167 throw new IllegalArgumentException(
168 "Null 'positivePaint' argument.");
169 }
170 if (negativePaint == null) {
171 throw new IllegalArgumentException(
172 "Null 'negativePaint' argument.");
173 }
174 this.positivePaint = positivePaint;
175 this.negativePaint = negativePaint;
176 this.shapesVisible = shapes;
177 this.legendLine = new Line2D.Double(-7.0, 0.0, 7.0, 0.0);
178 this.roundXCoordinates = false;
179 }
180
181 /**
182 * Returns the paint used to highlight positive differences.
183 *
184 * @return The paint (never <code>null</code>).
185 *
186 * @see #setPositivePaint(Paint)
187 */
188 public Paint getPositivePaint() {
189 return this.positivePaint;
190 }
191
192 /**
193 * Sets the paint used to highlight positive differences.
194 *
195 * @param paint the paint (<code>null</code> not permitted).
196 *
197 * @see #getPositivePaint()
198 */
199 public void setPositivePaint(Paint paint) {
200 if (paint == null) {
201 throw new IllegalArgumentException("Null 'paint' argument.");
202 }
203 this.positivePaint = paint;
204 notifyListeners(new RendererChangeEvent(this));
205 }
206
207 /**
208 * Returns the paint used to highlight negative differences.
209 *
210 * @return The paint (never <code>null</code>).
211 *
212 * @see #setNegativePaint(Paint)
213 */
214 public Paint getNegativePaint() {
215 return this.negativePaint;
216 }
217
218 /**
219 * Sets the paint used to highlight negative differences.
220 *
221 * @param paint the paint (<code>null</code> not permitted).
222 *
223 * @see #getNegativePaint()
224 */
225 public void setNegativePaint(Paint paint) {
226 if (paint == null) {
227 throw new IllegalArgumentException("Null 'paint' argument.");
228 }
229 this.negativePaint = paint;
230 notifyListeners(new RendererChangeEvent(this));
231 }
232
233 /**
234 * Returns a flag that controls whether or not shapes are drawn for each
235 * data value.
236 *
237 * @return A boolean.
238 *
239 * @see #setShapesVisible(boolean)
240 */
241 public boolean getShapesVisible() {
242 return this.shapesVisible;
243 }
244
245 /**
246 * Sets a flag that controls whether or not shapes are drawn for each
247 * data value.
248 *
249 * @param flag the flag.
250 *
251 * @see #getShapesVisible()
252 */
253 public void setShapesVisible(boolean flag) {
254 this.shapesVisible = flag;
255 notifyListeners(new RendererChangeEvent(this));
256 }
257
258 /**
259 * Returns the shape used to represent a line in the legend.
260 *
261 * @return The legend line (never <code>null</code>).
262 *
263 * @see #setLegendLine(Shape)
264 */
265 public Shape getLegendLine() {
266 return this.legendLine;
267 }
268
269 /**
270 * Sets the shape used as a line in each legend item and sends a
271 * {@link RendererChangeEvent} to all registered listeners.
272 *
273 * @param line the line (<code>null</code> not permitted).
274 *
275 * @see #getLegendLine()
276 */
277 public void setLegendLine(Shape line) {
278 if (line == null) {
279 throw new IllegalArgumentException("Null 'line' argument.");
280 }
281 this.legendLine = line;
282 notifyListeners(new RendererChangeEvent(this));
283 }
284
285 /**
286 * Returns the flag that controls whether or not the x-coordinates (in
287 * Java2D space) are rounded to integer values.
288 *
289 * @return The flag.
290 *
291 * @since 1.0.4
292 *
293 * @see #setRoundXCoordinates(boolean)
294 */
295 public boolean getRoundXCoordinates() {
296 return this.roundXCoordinates;
297 }
298
299 /**
300 * Sets the flag that controls whether or not the x-coordinates (in
301 * Java2D space) are rounded to integer values, and sends a
302 * {@link RendererChangeEvent} to all registered listeners.
303 *
304 * @param round the new flag value.
305 *
306 * @since 1.0.4
307 *
308 * @see #getRoundXCoordinates()
309 */
310 public void setRoundXCoordinates(boolean round) {
311 this.roundXCoordinates = round;
312 notifyListeners(new RendererChangeEvent(this));
313 }
314
315 /**
316 * Initialises the renderer and returns a state object that should be
317 * passed to subsequent calls to the drawItem() method. This method will
318 * be called before the first item is rendered, giving the renderer an
319 * opportunity to initialise any state information it wants to maintain.
320 * The renderer can do nothing if it chooses.
321 *
322 * @param g2 the graphics device.
323 * @param dataArea the area inside the axes.
324 * @param plot the plot.
325 * @param data the data.
326 * @param info an optional info collection object to return data back to
327 * the caller.
328 *
329 * @return A state object.
330 */
331 public XYItemRendererState initialise(Graphics2D g2,
332 Rectangle2D dataArea,
333 XYPlot plot,
334 XYDataset data,
335 PlotRenderingInfo info) {
336
337 XYItemRendererState state = super.initialise(g2, dataArea, plot, data,
338 info);
339 state.setProcessVisibleItemsOnly(false);
340 return state;
341
342 }
343
344 /**
345 * Returns <code>2</code>, the number of passes required by the renderer.
346 * The {@link XYPlot} will run through the dataset this number of times.
347 *
348 * @return The number of passes required by the renderer.
349 */
350 public int getPassCount() {
351 return 2;
352 }
353
354 /**
355 * Draws the visual representation of a single data item.
356 *
357 * @param g2 the graphics device.
358 * @param state the renderer state.
359 * @param dataArea the area within which the data is being drawn.
360 * @param info collects information about the drawing.
361 * @param plot the plot (can be used to obtain standard color
362 * information etc).
363 * @param domainAxis the domain (horizontal) axis.
364 * @param rangeAxis the range (vertical) axis.
365 * @param dataset the dataset.
366 * @param series the series index (zero-based).
367 * @param item the item index (zero-based).
368 * @param crosshairState crosshair information for the plot
369 * (<code>null</code> permitted).
370 * @param pass the pass index.
371 */
372 public void drawItem(Graphics2D g2,
373 XYItemRendererState state,
374 Rectangle2D dataArea,
375 PlotRenderingInfo info,
376 XYPlot plot,
377 ValueAxis domainAxis,
378 ValueAxis rangeAxis,
379 XYDataset dataset,
380 int series,
381 int item,
382 CrosshairState crosshairState,
383 int pass) {
384
385 if (pass == 0) {
386 drawItemPass0(g2, dataArea, info, plot, domainAxis, rangeAxis,
387 dataset, series, item, crosshairState);
388 }
389 else if (pass == 1) {
390 drawItemPass1(g2, dataArea, info, plot, domainAxis, rangeAxis,
391 dataset, series, item, crosshairState);
392 }
393
394 }
395
396 /**
397 * Draws the visual representation of a single data item, first pass.
398 *
399 * @param x_graphics the graphics device.
400 * @param x_dataArea the area within which the data is being drawn.
401 * @param x_info collects information about the drawing.
402 * @param x_plot the plot (can be used to obtain standard color
403 * information etc).
404 * @param x_domainAxis the domain (horizontal) axis.
405 * @param x_rangeAxis the range (vertical) axis.
406 * @param x_dataset the dataset.
407 * @param x_series the series index (zero-based).
408 * @param x_item the item index (zero-based).
409 * @param x_crosshairState crosshair information for the plot
410 * (<code>null</code> permitted).
411 */
412 protected void drawItemPass0(Graphics2D x_graphics,
413 Rectangle2D x_dataArea,
414 PlotRenderingInfo x_info,
415 XYPlot x_plot,
416 ValueAxis x_domainAxis,
417 ValueAxis x_rangeAxis,
418 XYDataset x_dataset,
419 int x_series,
420 int x_item,
421 CrosshairState x_crosshairState) {
422
423 if (!((0 == x_series) && (0 == x_item))) {
424 return;
425 }
426
427 boolean b_impliedZeroSubtrahend = (1 == x_dataset.getSeriesCount());
428
429 // check if either series is a degenerate case (i.e. less than 2 points)
430 if (isEitherSeriesDegenerate(x_dataset, b_impliedZeroSubtrahend)) {
431 return;
432 }
433
434 // check if series are disjoint (i.e. domain-spans do not overlap)
435 if (!b_impliedZeroSubtrahend && areSeriesDisjoint(x_dataset)) {
436 return;
437 }
438
439 // polygon definitions
440 LinkedList l_minuendXs = new LinkedList();
441 LinkedList l_minuendYs = new LinkedList();
442 LinkedList l_subtrahendXs = new LinkedList();
443 LinkedList l_subtrahendYs = new LinkedList();
444 LinkedList l_polygonXs = new LinkedList();
445 LinkedList l_polygonYs = new LinkedList();
446
447 // state
448 int l_minuendItem = 0;
449 int l_minuendItemCount = x_dataset.getItemCount(0);
450 Double l_minuendCurX = null;
451 Double l_minuendNextX = null;
452 Double l_minuendCurY = null;
453 Double l_minuendNextY = null;
454 double l_minuendMaxY = Double.NEGATIVE_INFINITY;
455 double l_minuendMinY = Double.POSITIVE_INFINITY;
456
457 int l_subtrahendItem = 0;
458 int l_subtrahendItemCount = 0; // actual value set below
459 Double l_subtrahendCurX = null;
460 Double l_subtrahendNextX = null;
461 Double l_subtrahendCurY = null;
462 Double l_subtrahendNextY = null;
463 double l_subtrahendMaxY = Double.NEGATIVE_INFINITY;
464 double l_subtrahendMinY = Double.POSITIVE_INFINITY;
465
466 // if a subtrahend is not specified, assume it is zero
467 if (b_impliedZeroSubtrahend) {
468 l_subtrahendItem = 0;
469 l_subtrahendItemCount = 2;
470 l_subtrahendCurX = new Double(x_dataset.getXValue(0, 0));
471 l_subtrahendNextX = new Double(x_dataset.getXValue(0,
472 (l_minuendItemCount - 1)));
473 l_subtrahendCurY = new Double(0.0);
474 l_subtrahendNextY = new Double(0.0);
475 l_subtrahendMaxY = 0.0;
476 l_subtrahendMinY = 0.0;
477
478 l_subtrahendXs.add(l_subtrahendCurX);
479 l_subtrahendYs.add(l_subtrahendCurY);
480 }
481 else {
482 l_subtrahendItemCount = x_dataset.getItemCount(1);
483 }
484
485 boolean b_minuendDone = false;
486 boolean b_minuendAdvanced = true;
487 boolean b_minuendAtIntersect = false;
488 boolean b_minuendFastForward = false;
489 boolean b_subtrahendDone = false;
490 boolean b_subtrahendAdvanced = true;
491 boolean b_subtrahendAtIntersect = false;
492 boolean b_subtrahendFastForward = false;
493 boolean b_colinear = false;
494
495 boolean b_positive;
496
497 // coordinate pairs
498 double l_x1 = 0.0, l_y1 = 0.0; // current minuend point
499 double l_x2 = 0.0, l_y2 = 0.0; // next minuend point
500 double l_x3 = 0.0, l_y3 = 0.0; // current subtrahend point
501 double l_x4 = 0.0, l_y4 = 0.0; // next subtrahend point
502
503 // fast-forward through leading tails
504 boolean b_fastForwardDone = false;
505 while (!b_fastForwardDone) {
506 // get the x and y coordinates
507 l_x1 = x_dataset.getXValue(0, l_minuendItem);
508 l_y1 = x_dataset.getYValue(0, l_minuendItem);
509 l_x2 = x_dataset.getXValue(0, l_minuendItem + 1);
510 l_y2 = x_dataset.getYValue(0, l_minuendItem + 1);
511
512 l_minuendCurX = new Double(l_x1);
513 l_minuendCurY = new Double(l_y1);
514 l_minuendNextX = new Double(l_x2);
515 l_minuendNextY = new Double(l_y2);
516
517 if (b_impliedZeroSubtrahend) {
518 l_x3 = l_subtrahendCurX.doubleValue();
519 l_y3 = l_subtrahendCurY.doubleValue();
520 l_x4 = l_subtrahendNextX.doubleValue();
521 l_y4 = l_subtrahendNextY.doubleValue();
522 }
523 else {
524 l_x3 = x_dataset.getXValue(1, l_subtrahendItem);
525 l_y3 = x_dataset.getYValue(1, l_subtrahendItem);
526 l_x4 = x_dataset.getXValue(1, l_subtrahendItem + 1);
527 l_y4 = x_dataset.getYValue(1, l_subtrahendItem + 1);
528
529 l_subtrahendCurX = new Double(l_x3);
530 l_subtrahendCurY = new Double(l_y3);
531 l_subtrahendNextX = new Double(l_x4);
532 l_subtrahendNextY = new Double(l_y4);
533 }
534
535 if (l_x2 <= l_x3) {
536 // minuend needs to be fast forwarded
537 l_minuendItem++;
538 b_minuendFastForward = true;
539 continue;
540 }
541
542 if (l_x4 <= l_x1) {
543 // subtrahend needs to be fast forwarded
544 l_subtrahendItem++;
545 b_subtrahendFastForward = true;
546 continue;
547 }
548
549 // check if initial polygon needs to be clipped
550 if ((l_x3 < l_x1) && (l_x1 < l_x4)) {
551 // project onto subtrahend
552 double l_slope = (l_y4 - l_y3) / (l_x4 - l_x3);
553 l_subtrahendCurX = l_minuendCurX;
554 l_subtrahendCurY = new Double((l_slope * l_x1)
555 + (l_y3 - (l_slope * l_x3)));
556
557 l_subtrahendXs.add(l_subtrahendCurX);
558 l_subtrahendYs.add(l_subtrahendCurY);
559 }
560
561 if ((l_x1 < l_x3) && (l_x3 < l_x2)) {
562 // project onto minuend
563 double l_slope = (l_y2 - l_y1) / (l_x2 - l_x1);
564 l_minuendCurX = l_subtrahendCurX;
565 l_minuendCurY = new Double((l_slope * l_x3)
566 + (l_y1 - (l_slope * l_x1)));
567
568 l_minuendXs.add(l_minuendCurX);
569 l_minuendYs.add(l_minuendCurY);
570 }
571
572 l_minuendMaxY = l_minuendCurY.doubleValue();
573 l_minuendMinY = l_minuendCurY.doubleValue();
574 l_subtrahendMaxY = l_subtrahendCurY.doubleValue();
575 l_subtrahendMinY = l_subtrahendCurY.doubleValue();
576
577 b_fastForwardDone = true;
578 }
579
580 // start of algorithm
581 while (!b_minuendDone && !b_subtrahendDone) {
582 if (!b_minuendDone && !b_minuendFastForward && b_minuendAdvanced) {
583 l_x1 = x_dataset.getXValue(0, l_minuendItem);
584 l_y1 = x_dataset.getYValue(0, l_minuendItem);
585 l_minuendCurX = new Double(l_x1);
586 l_minuendCurY = new Double(l_y1);
587
588 if (!b_minuendAtIntersect) {
589 l_minuendXs.add(l_minuendCurX);
590 l_minuendYs.add(l_minuendCurY);
591 }
592
593 l_minuendMaxY = Math.max(l_minuendMaxY, l_y1);
594 l_minuendMinY = Math.min(l_minuendMinY, l_y1);
595
596 l_x2 = x_dataset.getXValue(0, l_minuendItem + 1);
597 l_y2 = x_dataset.getYValue(0, l_minuendItem + 1);
598 l_minuendNextX = new Double(l_x2);
599 l_minuendNextY = new Double(l_y2);
600 }
601
602 // never updated the subtrahend if it is implied to be zero
603 if (!b_impliedZeroSubtrahend && !b_subtrahendDone
604 && !b_subtrahendFastForward && b_subtrahendAdvanced) {
605 l_x3 = x_dataset.getXValue(1, l_subtrahendItem);
606 l_y3 = x_dataset.getYValue(1, l_subtrahendItem);
607 l_subtrahendCurX = new Double(l_x3);
608 l_subtrahendCurY = new Double(l_y3);
609
610 if (!b_subtrahendAtIntersect) {
611 l_subtrahendXs.add(l_subtrahendCurX);
612 l_subtrahendYs.add(l_subtrahendCurY);
613 }
614
615 l_subtrahendMaxY = Math.max(l_subtrahendMaxY, l_y3);
616 l_subtrahendMinY = Math.min(l_subtrahendMinY, l_y3);
617
618 l_x4 = x_dataset.getXValue(1, l_subtrahendItem + 1);
619 l_y4 = x_dataset.getYValue(1, l_subtrahendItem + 1);
620 l_subtrahendNextX = new Double(l_x4);
621 l_subtrahendNextY = new Double(l_y4);
622 }
623
624 // deassert b_*FastForward (only matters for 1st time through loop)
625 b_minuendFastForward = false;
626 b_subtrahendFastForward = false;
627
628 Double l_intersectX = null;
629 Double l_intersectY = null;
630 boolean b_intersect = false;
631
632 b_minuendAtIntersect = false;
633 b_subtrahendAtIntersect = false;
634
635 // check for intersect
636 if ((l_x2 == l_x4) && (l_y2 == l_y4)) {
637 // check if line segments are colinear
638 if ((l_x1 == l_x3) && (l_y1 == l_y3)) {
639 b_colinear = true;
640 }
641 else {
642 // the intersect is at the next point for both the minuend
643 // and subtrahend
644 l_intersectX = new Double(l_x2);
645 l_intersectY = new Double(l_y2);
646
647 b_intersect = true;
648 b_minuendAtIntersect = true;
649 b_subtrahendAtIntersect = true;
650 }
651 }
652 else {
653 // compute common denominator
654 double l_denominator = ((l_y4 - l_y3) * (l_x2 - l_x1))
655 - ((l_x4 - l_x3) * (l_y2 - l_y1));
656
657 // compute common deltas
658 double l_deltaY = l_y1 - l_y3;
659 double l_deltaX = l_x1 - l_x3;
660
661 // compute numerators
662 double l_numeratorA = ((l_x4 - l_x3) * l_deltaY)
663 - ((l_y4 - l_y3) * l_deltaX);
664 double l_numeratorB = ((l_x2 - l_x1) * l_deltaY)
665 - ((l_y2 - l_y1) * l_deltaX);
666
667 // check if line segments are colinear
668 if ((0 == l_numeratorA) && (0 == l_numeratorB)
669 && (0 == l_denominator)) {
670 b_colinear = true;
671 }
672 else {
673 // check if previously colinear
674 if (b_colinear) {
675 // clear colinear points and flag
676 l_minuendXs.clear();
677 l_minuendYs.clear();
678 l_subtrahendXs.clear();
679 l_subtrahendYs.clear();
680 l_polygonXs.clear();
681 l_polygonYs.clear();
682
683 b_colinear = false;
684
685 // set new starting point for the polygon
686 boolean b_useMinuend = ((l_x3 <= l_x1)
687 && (l_x1 <= l_x4));
688 l_polygonXs.add(b_useMinuend ? l_minuendCurX
689 : l_subtrahendCurX);
690 l_polygonYs.add(b_useMinuend ? l_minuendCurY
691 : l_subtrahendCurY);
692 }
693
694 // compute slope components
695 double l_slopeA = l_numeratorA / l_denominator;
696 double l_slopeB = l_numeratorB / l_denominator;
697
698 // check if the line segments intersect
699 if ((0 < l_slopeA) && (l_slopeA <= 1) && (0 < l_slopeB)
700 && (l_slopeB <= 1)) {
701 // compute the point of intersection
702 double l_xi = l_x1 + (l_slopeA * (l_x2 - l_x1));
703 double l_yi = l_y1 + (l_slopeA * (l_y2 - l_y1));
704
705 l_intersectX = new Double(l_xi);
706 l_intersectY = new Double(l_yi);
707 b_intersect = true;
708 b_minuendAtIntersect = ((l_xi == l_x2)
709 && (l_yi == l_y2));
710 b_subtrahendAtIntersect = ((l_xi == l_x4)
711 && (l_yi == l_y4));
712
713 // advance minuend and subtrahend to intesect
714 l_minuendCurX = l_intersectX;
715 l_minuendCurY = l_intersectY;
716 l_subtrahendCurX = l_intersectX;
717 l_subtrahendCurY = l_intersectY;
718 }
719 }
720 }
721
722 if (b_intersect) {
723 // create the polygon
724 // add the minuend's points to polygon
725 l_polygonXs.addAll(l_minuendXs);
726 l_polygonYs.addAll(l_minuendYs);
727
728 // add intersection point to the polygon
729 l_polygonXs.add(l_intersectX);
730 l_polygonYs.add(l_intersectY);
731
732 // add the subtrahend's points to the polygon in reverse
733 Collections.reverse(l_subtrahendXs);
734 Collections.reverse(l_subtrahendYs);
735 l_polygonXs.addAll(l_subtrahendXs);
736 l_polygonYs.addAll(l_subtrahendYs);
737
738 // create an actual polygon
739 b_positive = (l_subtrahendMaxY <= l_minuendMaxY)
740 && (l_subtrahendMinY <= l_minuendMinY);
741 createPolygon(x_graphics, x_dataArea, x_plot, x_domainAxis,
742 x_rangeAxis, b_positive, l_polygonXs, l_polygonYs);
743
744 // clear the point vectors
745 l_minuendXs.clear();
746 l_minuendYs.clear();
747 l_subtrahendXs.clear();
748 l_subtrahendYs.clear();
749 l_polygonXs.clear();
750 l_polygonYs.clear();
751
752 // set the maxY and minY values to intersect y-value
753 double l_y = l_intersectY.doubleValue();
754 l_minuendMaxY = l_y;
755 l_subtrahendMaxY = l_y;
756 l_minuendMinY = l_y;
757 l_subtrahendMinY = l_y;
758
759 // add interection point to new polygon
760 l_polygonXs.add(l_intersectX);
761 l_polygonYs.add(l_intersectY);
762 }
763
764 // advance the minuend if needed
765 if (l_x2 <= l_x4) {
766 l_minuendItem++;
767 b_minuendAdvanced = true;
768 }
769 else {
770 b_minuendAdvanced = false;
771 }
772
773 // advance the subtrahend if needed
774 if (l_x4 <= l_x2) {
775 l_subtrahendItem++;
776 b_subtrahendAdvanced = true;
777 }
778 else {
779 b_subtrahendAdvanced = false;
780 }
781
782 b_minuendDone = (l_minuendItem == (l_minuendItemCount - 1));
783 b_subtrahendDone = (l_subtrahendItem == (l_subtrahendItemCount
784 - 1));
785 }
786
787 // check if the final polygon needs to be clipped
788 if (b_minuendDone && (l_x3 < l_x2) && (l_x2 < l_x4)) {
789 // project onto subtrahend
790 double l_slope = (l_y4 - l_y3) / (l_x4 - l_x3);
791 l_subtrahendNextX = l_minuendNextX;
792 l_subtrahendNextY = new Double((l_slope * l_x2)
793 + (l_y3 - (l_slope * l_x3)));
794 }
795
796 if (b_subtrahendDone && (l_x1 < l_x4) && (l_x4 < l_x2)) {
797 // project onto minuend
798 double l_slope = (l_y2 - l_y1) / (l_x2 - l_x1);
799 l_minuendNextX = l_subtrahendNextX;
800 l_minuendNextY = new Double((l_slope * l_x4)
801 + (l_y1 - (l_slope * l_x1)));
802 }
803
804 // consider last point of minuend and subtrahend for determining positivity
805 l_minuendMaxY = Math.max(l_minuendMaxY,
806 l_minuendNextY.doubleValue());
807 l_subtrahendMaxY = Math.max(l_subtrahendMaxY,
808 l_subtrahendNextY.doubleValue());
809 l_minuendMinY = Math.min(l_minuendMinY,
810 l_minuendNextY.doubleValue());
811 l_subtrahendMinY = Math.min(l_subtrahendMinY,
812 l_subtrahendNextY.doubleValue());
813
814 // add the last point of the minuned and subtrahend
815 l_minuendXs.add(l_minuendNextX);
816 l_minuendYs.add(l_minuendNextY);
817 l_subtrahendXs.add(l_subtrahendNextX);
818 l_subtrahendYs.add(l_subtrahendNextY);
819
820 // create the polygon
821 // add the minuend's points to polygon
822 l_polygonXs.addAll(l_minuendXs);
823 l_polygonYs.addAll(l_minuendYs);
824
825 // add the subtrahend's points to the polygon in reverse
826 Collections.reverse(l_subtrahendXs);
827 Collections.reverse(l_subtrahendYs);
828 l_polygonXs.addAll(l_subtrahendXs);
829 l_polygonYs.addAll(l_subtrahendYs);
830
831 // create an actual polygon
832 b_positive = (l_subtrahendMaxY <= l_minuendMaxY)
833 && (l_subtrahendMinY <= l_minuendMinY);
834 createPolygon(x_graphics, x_dataArea, x_plot, x_domainAxis,
835 x_rangeAxis, b_positive, l_polygonXs, l_polygonYs);
836 }
837
838 /**
839 * Draws the visual representation of a single data item, second pass. In
840 * the second pass, the renderer draws the lines and shapes for the
841 * individual points in the two series.
842 *
843 * @param x_graphics the graphics device.
844 * @param x_dataArea the area within which the data is being drawn.
845 * @param x_info collects information about the drawing.
846 * @param x_plot the plot (can be used to obtain standard color
847 * information etc).
848 * @param x_domainAxis the domain (horizontal) axis.
849 * @param x_rangeAxis the range (vertical) axis.
850 * @param x_dataset the dataset.
851 * @param x_series the series index (zero-based).
852 * @param x_item the item index (zero-based).
853 * @param x_crosshairState crosshair information for the plot
854 * (<code>null</code> permitted).
855 */
856 protected void drawItemPass1(Graphics2D x_graphics,
857 Rectangle2D x_dataArea,
858 PlotRenderingInfo x_info,
859 XYPlot x_plot,
860 ValueAxis x_domainAxis,
861 ValueAxis x_rangeAxis,
862 XYDataset x_dataset,
863 int x_series,
864 int x_item,
865 CrosshairState x_crosshairState) {
866
867 Shape l_entityArea = null;
868 EntityCollection l_entities = null;
869 if (null != x_info) {
870 l_entities = x_info.getOwner().getEntityCollection();
871 }
872
873 Paint l_seriesPaint = getItemPaint(x_series, x_item);
874 Stroke l_seriesStroke = getItemStroke(x_series, x_item);
875 x_graphics.setPaint(l_seriesPaint);
876 x_graphics.setStroke(l_seriesStroke);
877
878 PlotOrientation l_orientation = x_plot.getOrientation();
879 RectangleEdge l_domainAxisLocation = x_plot.getDomainAxisEdge();
880 RectangleEdge l_rangeAxisLocation = x_plot.getRangeAxisEdge();
881
882 double l_x0 = x_dataset.getXValue(x_series, x_item);
883 double l_y0 = x_dataset.getYValue(x_series, x_item);
884 double l_x1 = x_domainAxis.valueToJava2D(l_x0, x_dataArea,
885 l_domainAxisLocation);
886 double l_y1 = x_rangeAxis.valueToJava2D(l_y0, x_dataArea,
887 l_rangeAxisLocation);
888
889 if (getShapesVisible()) {
890 Shape l_shape = getItemShape(x_series, x_item);
891 if (l_orientation == PlotOrientation.HORIZONTAL) {
892 l_shape = ShapeUtilities.createTranslatedShape(l_shape,
893 l_y1, l_x1);
894 }
895 else {
896 l_shape = ShapeUtilities.createTranslatedShape(l_shape,
897 l_x1, l_y1);
898 }
899 if (l_shape.intersects(x_dataArea)) {
900 x_graphics.setPaint(getItemPaint(x_series, x_item));
901 x_graphics.fill(l_shape);
902 }
903 l_entityArea = l_shape;
904 }
905
906 // add an entity for the item...
907 if (null != l_entities) {
908 if (null == l_entityArea) {
909 l_entityArea = new Rectangle2D.Double((l_x1 - 2), (l_y1 - 2),
910 4, 4);
911 }
912 String l_tip = null;
913 XYToolTipGenerator l_tipGenerator = getToolTipGenerator(x_series,
914 x_item);
915 if (null != l_tipGenerator) {
916 l_tip = l_tipGenerator.generateToolTip(x_dataset, x_series,
917 x_item);
918 }
919 String l_url = null;
920 XYURLGenerator l_urlGenerator = getURLGenerator();
921 if (null != l_urlGenerator) {
922 l_url = l_urlGenerator.generateURL(x_dataset, x_series,
923 x_item);
924 }
925 XYItemEntity l_entity = new XYItemEntity(l_entityArea, x_dataset,
926 x_series, x_item, l_tip, l_url);
927 l_entities.add(l_entity);
928 }
929
930 int l_domainAxisIndex = x_plot.getDomainAxisIndex(x_domainAxis);
931 int l_rangeAxisIndex = x_plot.getRangeAxisIndex(x_rangeAxis);
932 updateCrosshairValues(x_crosshairState, l_x0, l_y0, l_domainAxisIndex,
933 l_rangeAxisIndex, l_x1, l_y1, l_orientation);
934
935 if (0 == x_item) {
936 return;
937 }
938
939 double l_x2 = x_domainAxis.valueToJava2D(x_dataset.getXValue(x_series,
940 (x_item - 1)), x_dataArea, l_domainAxisLocation);
941 double l_y2 = x_rangeAxis.valueToJava2D(x_dataset.getYValue(x_series,
942 (x_item - 1)), x_dataArea, l_rangeAxisLocation);
943
944 Line2D l_line = null;
945 if (PlotOrientation.HORIZONTAL == l_orientation) {
946 l_line = new Line2D.Double(l_y1, l_x1, l_y2, l_x2);
947 }
948 else if (PlotOrientation.VERTICAL == l_orientation) {
949 l_line = new Line2D.Double(l_x1, l_y1, l_x2, l_y2);
950 }
951
952 if ((null != l_line) && l_line.intersects(x_dataArea)) {
953 x_graphics.setPaint(getItemPaint(x_series, x_item));
954 x_graphics.setStroke(getItemStroke(x_series, x_item));
955 x_graphics.draw(l_line);
956 }
957 }
958
959 /**
960 * Determines if a dataset is degenerate. A degenerate dataset is a
961 * dataset where either series has less than two (2) points.
962 *
963 * @param x_dataset the dataset.
964 * @param x_impliedZeroSubtrahend if false, do not check the subtrahend
965 *
966 * @return true if the dataset is degenerate.
967 */
968 private boolean isEitherSeriesDegenerate(XYDataset x_dataset,
969 boolean x_impliedZeroSubtrahend) {
970
971 if (x_impliedZeroSubtrahend) {
972 return (x_dataset.getItemCount(0) < 2);
973 }
974
975 return ((x_dataset.getItemCount(0) < 2)
976 || (x_dataset.getItemCount(1) < 2));
977 }
978
979 /**
980 * Determines if the two (2) series are disjoint.
981 * Disjoint series do not overlap in the domain space.
982 *
983 * @param x_dataset the dataset.
984 *
985 * @return true if the dataset is degenerate.
986 */
987 private boolean areSeriesDisjoint(XYDataset x_dataset) {
988
989 int l_minuendItemCount = x_dataset.getItemCount(0);
990 double l_minuendFirst = x_dataset.getXValue(0, 0);
991 double l_minuendLast = x_dataset.getXValue(0, l_minuendItemCount - 1);
992
993 int l_subtrahendItemCount = x_dataset.getItemCount(1);
994 double l_subtrahendFirst = x_dataset.getXValue(1, 0);
995 double l_subtrahendLast = x_dataset.getXValue(1,
996 l_subtrahendItemCount - 1);
997
998 return ((l_minuendLast < l_subtrahendFirst)
999 || (l_subtrahendLast < l_minuendFirst));
1000 }
1001
1002 /**
1003 * Draws the visual representation of a polygon
1004 *
1005 * @param x_graphics the graphics device.
1006 * @param x_dataArea the area within which the data is being drawn.
1007 * @param x_plot the plot (can be used to obtain standard color
1008 * information etc).
1009 * @param x_domainAxis the domain (horizontal) axis.
1010 * @param x_rangeAxis the range (vertical) axis.
1011 * @param x_positive indicates if the polygon is positive (true) or
1012 * negative (false).
1013 * @param x_xValues a linked list of the x values (expects values to be
1014 * of type Double).
1015 * @param x_yValues a linked list of the y values (expects values to be
1016 * of type Double).
1017 */
1018 private void createPolygon (Graphics2D x_graphics,
1019 Rectangle2D x_dataArea,
1020 XYPlot x_plot,
1021 ValueAxis x_domainAxis,
1022 ValueAxis x_rangeAxis,
1023 boolean x_positive,
1024 LinkedList x_xValues,
1025 LinkedList x_yValues) {
1026
1027 PlotOrientation l_orientation = x_plot.getOrientation();
1028 RectangleEdge l_domainAxisLocation = x_plot.getDomainAxisEdge();
1029 RectangleEdge l_rangeAxisLocation = x_plot.getRangeAxisEdge();
1030
1031 Object[] l_xValues = x_xValues.toArray();
1032 Object[] l_yValues = x_yValues.toArray();
1033
1034 GeneralPath l_path = new GeneralPath();
1035
1036 if (PlotOrientation.VERTICAL == l_orientation) {
1037 double l_x = x_domainAxis.valueToJava2D((
1038 (Double) l_xValues[0]).doubleValue(), x_dataArea,
1039 l_domainAxisLocation);
1040 if (this.roundXCoordinates) {
1041 l_x = Math.rint(l_x);
1042 }
1043
1044 double l_y = x_rangeAxis.valueToJava2D((
1045 (Double) l_yValues[0]).doubleValue(), x_dataArea,
1046 l_rangeAxisLocation);
1047
1048 l_path.moveTo((float)l_x, (float)l_y);
1049 for (int i = 1; i < l_xValues.length; i++) {
1050 l_x = x_domainAxis.valueToJava2D((
1051 (Double) l_xValues[i]).doubleValue(), x_dataArea,
1052 l_domainAxisLocation);
1053 if (this.roundXCoordinates) {
1054 l_x = Math.rint(l_x);
1055 }
1056
1057 l_y = x_rangeAxis.valueToJava2D((
1058 (Double)l_yValues[i]).doubleValue(), x_dataArea,
1059 l_rangeAxisLocation);
1060 l_path.lineTo((float)l_x, (float)l_y);
1061 }
1062 l_path.closePath();
1063 }
1064 else {
1065 double l_x = x_domainAxis.valueToJava2D((
1066 (Double)l_xValues[0]).doubleValue(), x_dataArea,
1067 l_domainAxisLocation);
1068 if (this.roundXCoordinates) {
1069 l_x = Math.rint(l_x);
1070 }
1071
1072 double l_y = x_rangeAxis.valueToJava2D((
1073 (Double)l_yValues[0]).doubleValue(), x_dataArea,
1074 l_rangeAxisLocation);
1075
1076 l_path.moveTo((float)l_y, (float)l_x);
1077 for (int i = 1; i < l_xValues.length; i++) {
1078 l_x = x_domainAxis.valueToJava2D((
1079 (Double) l_xValues[i]).doubleValue(), x_dataArea,
1080 l_domainAxisLocation);
1081 if (this.roundXCoordinates) {
1082 l_x = Math.rint(l_x);
1083 }
1084
1085 l_y = x_rangeAxis.valueToJava2D((
1086 (Double) l_yValues[i]).doubleValue(), x_dataArea,
1087 l_rangeAxisLocation);
1088 l_path.lineTo((float)l_y, (float)l_x);
1089 }
1090 l_path.closePath();
1091 }
1092
1093 if (l_path.intersects(x_dataArea)) {
1094 x_graphics.setPaint(x_positive ? getPositivePaint()
1095 : getNegativePaint());
1096 x_graphics.fill(l_path);
1097 }
1098 }
1099
1100 /**
1101 * Returns a default legend item for the specified series. Subclasses
1102 * should override this method to generate customised items.
1103 *
1104 * @param datasetIndex the dataset index (zero-based).
1105 * @param series the series index (zero-based).
1106 *
1107 * @return A legend item for the series.
1108 */
1109 public LegendItem getLegendItem(int datasetIndex, int series) {
1110 LegendItem result = null;
1111 XYPlot p = getPlot();
1112 if (p != null) {
1113 XYDataset dataset = p.getDataset(datasetIndex);
1114 if (dataset != null) {
1115 if (getItemVisible(series, 0)) {
1116 String label = getLegendItemLabelGenerator().generateLabel(
1117 dataset, series);
1118 String description = label;
1119 String toolTipText = null;
1120 if (getLegendItemToolTipGenerator() != null) {
1121 toolTipText
1122 = getLegendItemToolTipGenerator().generateLabel(
1123 dataset, series);
1124 }
1125 String urlText = null;
1126 if (getLegendItemURLGenerator() != null) {
1127 urlText = getLegendItemURLGenerator().generateLabel(
1128 dataset, series);
1129 }
1130 Paint paint = lookupSeriesPaint(series);
1131 Stroke stroke = lookupSeriesStroke(series);
1132 // TODO: the following hard-coded line needs generalising
1133 Line2D line = new Line2D.Double(-7.0, 0.0, 7.0, 0.0);
1134 result = new LegendItem(label, description,
1135 toolTipText, urlText, line, stroke, paint);
1136 result.setDataset(dataset);
1137 result.setDatasetIndex(datasetIndex);
1138 result.setSeriesKey(dataset.getSeriesKey(series));
1139 result.setSeriesIndex(series);
1140 }
1141 }
1142
1143 }
1144
1145 return result;
1146
1147 }
1148
1149 /**
1150 * Tests this renderer for equality with an arbitrary object.
1151 *
1152 * @param obj the object (<code>null</code> permitted).
1153 *
1154 * @return A boolean.
1155 */
1156 public boolean equals(Object obj) {
1157 if (obj == this) {
1158 return true;
1159 }
1160 if (!(obj instanceof XYDifferenceRenderer)) {
1161 return false;
1162 }
1163 if (!super.equals(obj)) {
1164 return false;
1165 }
1166 XYDifferenceRenderer that = (XYDifferenceRenderer) obj;
1167 if (!PaintUtilities.equal(this.positivePaint, that.positivePaint)) {
1168 return false;
1169 }
1170 if (!PaintUtilities.equal(this.negativePaint, that.negativePaint)) {
1171 return false;
1172 }
1173 if (this.shapesVisible != that.shapesVisible) {
1174 return false;
1175 }
1176 if (!ShapeUtilities.equal(this.legendLine, that.legendLine)) {
1177 return false;
1178 }
1179 if (this.roundXCoordinates != that.roundXCoordinates) {
1180 return false;
1181 }
1182 return true;
1183 }
1184
1185 /**
1186 * Returns a clone of the renderer.
1187 *
1188 * @return A clone.
1189 *
1190 * @throws CloneNotSupportedException if the renderer cannot be cloned.
1191 */
1192 public Object clone() throws CloneNotSupportedException {
1193 XYDifferenceRenderer clone = (XYDifferenceRenderer) super.clone();
1194 clone.legendLine = ShapeUtilities.clone(this.legendLine);
1195 return clone;
1196 }
1197
1198 /**
1199 * Provides serialization support.
1200 *
1201 * @param stream the output stream.
1202 *
1203 * @throws IOException if there is an I/O error.
1204 */
1205 private void writeObject(ObjectOutputStream stream) throws IOException {
1206 stream.defaultWriteObject();
1207 SerialUtilities.writePaint(this.positivePaint, stream);
1208 SerialUtilities.writePaint(this.negativePaint, stream);
1209 SerialUtilities.writeShape(this.legendLine, stream);
1210 }
1211
1212 /**
1213 * Provides serialization support.
1214 *
1215 * @param stream the input stream.
1216 *
1217 * @throws IOException if there is an I/O error.
1218 * @throws ClassNotFoundException if there is a classpath problem.
1219 */
1220 private void readObject(ObjectInputStream stream)
1221 throws IOException, ClassNotFoundException {
1222 stream.defaultReadObject();
1223 this.positivePaint = SerialUtilities.readPaint(stream);
1224 this.negativePaint = SerialUtilities.readPaint(stream);
1225 this.legendLine = SerialUtilities.readShape(stream);
1226 }
1227
1228 }