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