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