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 076package org.jfree.chart.renderer.xy; 077 078import java.awt.Color; 079import java.awt.Graphics2D; 080import java.awt.Paint; 081import java.awt.Shape; 082import java.awt.Stroke; 083import java.awt.geom.GeneralPath; 084import java.awt.geom.Line2D; 085import java.awt.geom.Rectangle2D; 086import java.io.IOException; 087import java.io.ObjectInputStream; 088import java.io.ObjectOutputStream; 089import java.io.Serializable; 090import java.util.Collections; 091import java.util.LinkedList; 092 093import org.jfree.chart.LegendItem; 094import org.jfree.chart.axis.ValueAxis; 095import org.jfree.chart.entity.EntityCollection; 096import org.jfree.chart.entity.XYItemEntity; 097import org.jfree.chart.event.RendererChangeEvent; 098import org.jfree.chart.labels.XYToolTipGenerator; 099import org.jfree.chart.plot.CrosshairState; 100import org.jfree.chart.plot.PlotOrientation; 101import org.jfree.chart.plot.PlotRenderingInfo; 102import org.jfree.chart.plot.XYPlot; 103import org.jfree.chart.urls.XYURLGenerator; 104import org.jfree.data.xy.XYDataset; 105import org.jfree.io.SerialUtilities; 106import org.jfree.ui.RectangleEdge; 107import org.jfree.util.PaintUtilities; 108import org.jfree.util.PublicCloneable; 109import org.jfree.util.ShapeUtilities; 110 111/** 112 * A renderer for an {@link XYPlot} that highlights the differences between two 113 * series. 114 */ 115public 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. 193 * 194 * @param paint the paint (<code>null</code> not permitted). 195 * 196 * @see #getPositivePaint() 197 */ 198 public void setPositivePaint(Paint paint) { 199 if (paint == null) { 200 throw new IllegalArgumentException("Null 'paint' argument."); 201 } 202 this.positivePaint = paint; 203 notifyListeners(new RendererChangeEvent(this)); 204 } 205 206 /** 207 * Returns the paint used to highlight negative differences. 208 * 209 * @return The paint (never <code>null</code>). 210 * 211 * @see #setNegativePaint(Paint) 212 */ 213 public Paint getNegativePaint() { 214 return this.negativePaint; 215 } 216 217 /** 218 * Sets the paint used to highlight negative differences. 219 * 220 * @param paint the paint (<code>null</code> not permitted). 221 * 222 * @see #getNegativePaint() 223 */ 224 public void setNegativePaint(Paint paint) { 225 if (paint == null) { 226 throw new IllegalArgumentException("Null 'paint' argument."); 227 } 228 this.negativePaint = paint; 229 notifyListeners(new RendererChangeEvent(this)); 230 } 231 232 /** 233 * Returns a flag that controls whether or not shapes are drawn for each 234 * data value. 235 * 236 * @return A boolean. 237 * 238 * @see #setShapesVisible(boolean) 239 */ 240 public boolean getShapesVisible() { 241 return this.shapesVisible; 242 } 243 244 /** 245 * Sets a flag that controls whether or not shapes are drawn for each 246 * data value. 247 * 248 * @param flag the flag. 249 * 250 * @see #getShapesVisible() 251 */ 252 public void setShapesVisible(boolean flag) { 253 this.shapesVisible = flag; 254 notifyListeners(new RendererChangeEvent(this)); 255 } 256 257 /** 258 * Returns the shape used to represent a line in the legend. 259 * 260 * @return The legend line (never <code>null</code>). 261 * 262 * @see #setLegendLine(Shape) 263 */ 264 public Shape getLegendLine() { 265 return this.legendLine; 266 } 267 268 /** 269 * Sets the shape used as a line in each legend item and sends a 270 * {@link RendererChangeEvent} to all registered listeners. 271 * 272 * @param line the line (<code>null</code> not permitted). 273 * 274 * @see #getLegendLine() 275 */ 276 public void setLegendLine(Shape line) { 277 if (line == null) { 278 throw new IllegalArgumentException("Null 'line' argument."); 279 } 280 this.legendLine = line; 281 notifyListeners(new RendererChangeEvent(this)); 282 } 283 284 /** 285 * Returns the flag that controls whether or not the x-coordinates (in 286 * Java2D space) are rounded to integer values. 287 * 288 * @return The flag. 289 * 290 * @since 1.0.4 291 * 292 * @see #setRoundXCoordinates(boolean) 293 */ 294 public boolean getRoundXCoordinates() { 295 return this.roundXCoordinates; 296 } 297 298 /** 299 * Sets the flag that controls whether or not the x-coordinates (in 300 * Java2D space) are rounded to integer values, and sends a 301 * {@link RendererChangeEvent} to all registered listeners. 302 * 303 * @param round the new flag value. 304 * 305 * @since 1.0.4 306 * 307 * @see #getRoundXCoordinates() 308 */ 309 public void setRoundXCoordinates(boolean round) { 310 this.roundXCoordinates = round; 311 notifyListeners(new RendererChangeEvent(this)); 312 } 313 314 /** 315 * Initialises the renderer and returns a state object that should be 316 * passed to subsequent calls to the drawItem() method. This method will 317 * be called before the first item is rendered, giving the renderer an 318 * opportunity to initialise any state information it wants to maintain. 319 * The renderer can do nothing if it chooses. 320 * 321 * @param g2 the graphics device. 322 * @param dataArea the area inside the axes. 323 * @param plot the plot. 324 * @param data the data. 325 * @param info an optional info collection object to return data back to 326 * the caller. 327 * 328 * @return A state object. 329 */ 330 public XYItemRendererState initialise(Graphics2D g2, 331 Rectangle2D dataArea, 332 XYPlot plot, 333 XYDataset data, 334 PlotRenderingInfo info) { 335 336 XYItemRendererState state = super.initialise(g2, dataArea, plot, data, 337 info); 338 state.setProcessVisibleItemsOnly(false); 339 return state; 340 341 } 342 343 /** 344 * Returns <code>2</code>, the number of passes required by the renderer. 345 * The {@link XYPlot} will run through the dataset this number of times. 346 * 347 * @return The number of passes required by the renderer. 348 */ 349 public int getPassCount() { 350 return 2; 351 } 352 353 /** 354 * Draws the visual representation of a single data item. 355 * 356 * @param g2 the graphics device. 357 * @param state the renderer state. 358 * @param dataArea the area within which the data is being drawn. 359 * @param info collects information about the drawing. 360 * @param plot the plot (can be used to obtain standard color 361 * information etc). 362 * @param domainAxis the domain (horizontal) axis. 363 * @param rangeAxis the range (vertical) axis. 364 * @param dataset the dataset. 365 * @param series the series index (zero-based). 366 * @param item the item index (zero-based). 367 * @param crosshairState crosshair information for the plot 368 * (<code>null</code> permitted). 369 * @param pass the pass index. 370 */ 371 public void drawItem(Graphics2D g2, 372 XYItemRendererState state, 373 Rectangle2D dataArea, 374 PlotRenderingInfo info, 375 XYPlot plot, 376 ValueAxis domainAxis, 377 ValueAxis rangeAxis, 378 XYDataset dataset, 379 int series, 380 int item, 381 CrosshairState crosshairState, 382 int pass) { 383 384 if (pass == 0) { 385 drawItemPass0(g2, dataArea, info, plot, domainAxis, rangeAxis, 386 dataset, series, item, crosshairState); 387 } 388 else if (pass == 1) { 389 drawItemPass1(g2, dataArea, info, plot, domainAxis, rangeAxis, 390 dataset, series, item, crosshairState); 391 } 392 393 } 394 395 /** 396 * Draws the visual representation of a single data item, first pass. 397 * 398 * @param x_graphics the graphics device. 399 * @param x_dataArea the area within which the data is being drawn. 400 * @param x_info collects information about the drawing. 401 * @param x_plot the plot (can be used to obtain standard color 402 * information etc). 403 * @param x_domainAxis the domain (horizontal) axis. 404 * @param x_rangeAxis the range (vertical) axis. 405 * @param x_dataset the dataset. 406 * @param x_series the series index (zero-based). 407 * @param x_item the item index (zero-based). 408 * @param x_crosshairState crosshair information for the plot 409 * (<code>null</code> permitted). 410 */ 411 protected void drawItemPass0(Graphics2D x_graphics, 412 Rectangle2D x_dataArea, 413 PlotRenderingInfo x_info, 414 XYPlot x_plot, 415 ValueAxis x_domainAxis, 416 ValueAxis x_rangeAxis, 417 XYDataset x_dataset, 418 int x_series, 419 int x_item, 420 CrosshairState x_crosshairState) { 421 422 if (!((0 == x_series) && (0 == x_item))) { 423 return; 424 } 425 426 boolean b_impliedZeroSubtrahend = (1 == x_dataset.getSeriesCount()); 427 428 // check if either series is a degenerate case (i.e. less than 2 points) 429 if (isEitherSeriesDegenerate(x_dataset, b_impliedZeroSubtrahend)) { 430 return; 431 } 432 433 // check if series are disjoint (i.e. domain-spans do not overlap) 434 if (!b_impliedZeroSubtrahend && areSeriesDisjoint(x_dataset)) { 435 return; 436 } 437 438 // polygon definitions 439 LinkedList l_minuendXs = new LinkedList(); 440 LinkedList l_minuendYs = new LinkedList(); 441 LinkedList l_subtrahendXs = new LinkedList(); 442 LinkedList l_subtrahendYs = new LinkedList(); 443 LinkedList l_polygonXs = new LinkedList(); 444 LinkedList l_polygonYs = new LinkedList(); 445 446 // state 447 int l_minuendItem = 0; 448 int l_minuendItemCount = x_dataset.getItemCount(0); 449 Double l_minuendCurX = null; 450 Double l_minuendNextX = null; 451 Double l_minuendCurY = null; 452 Double l_minuendNextY = null; 453 double l_minuendMaxY = Double.NEGATIVE_INFINITY; 454 double l_minuendMinY = Double.POSITIVE_INFINITY; 455 456 int l_subtrahendItem = 0; 457 int l_subtrahendItemCount = 0; // actual value set below 458 Double l_subtrahendCurX = null; 459 Double l_subtrahendNextX = null; 460 Double l_subtrahendCurY = null; 461 Double l_subtrahendNextY = null; 462 double l_subtrahendMaxY = Double.NEGATIVE_INFINITY; 463 double l_subtrahendMinY = Double.POSITIVE_INFINITY; 464 465 // if a subtrahend is not specified, assume it is zero 466 if (b_impliedZeroSubtrahend) { 467 l_subtrahendItem = 0; 468 l_subtrahendItemCount = 2; 469 l_subtrahendCurX = new Double(x_dataset.getXValue(0, 0)); 470 l_subtrahendNextX = new Double(x_dataset.getXValue(0, 471 (l_minuendItemCount - 1))); 472 l_subtrahendCurY = new Double(0.0); 473 l_subtrahendNextY = new Double(0.0); 474 l_subtrahendMaxY = 0.0; 475 l_subtrahendMinY = 0.0; 476 477 l_subtrahendXs.add(l_subtrahendCurX); 478 l_subtrahendYs.add(l_subtrahendCurY); 479 } 480 else { 481 l_subtrahendItemCount = x_dataset.getItemCount(1); 482 } 483 484 boolean b_minuendDone = false; 485 boolean b_minuendAdvanced = true; 486 boolean b_minuendAtIntersect = false; 487 boolean b_minuendFastForward = false; 488 boolean b_subtrahendDone = false; 489 boolean b_subtrahendAdvanced = true; 490 boolean b_subtrahendAtIntersect = false; 491 boolean b_subtrahendFastForward = false; 492 boolean b_colinear = false; 493 494 boolean b_positive; 495 496 // coordinate pairs 497 double l_x1 = 0.0, l_y1 = 0.0; // current minuend point 498 double l_x2 = 0.0, l_y2 = 0.0; // next minuend point 499 double l_x3 = 0.0, l_y3 = 0.0; // current subtrahend point 500 double l_x4 = 0.0, l_y4 = 0.0; // next subtrahend point 501 502 // fast-forward through leading tails 503 boolean b_fastForwardDone = false; 504 while (!b_fastForwardDone) { 505 // get the x and y coordinates 506 l_x1 = x_dataset.getXValue(0, l_minuendItem); 507 l_y1 = x_dataset.getYValue(0, l_minuendItem); 508 l_x2 = x_dataset.getXValue(0, l_minuendItem + 1); 509 l_y2 = x_dataset.getYValue(0, l_minuendItem + 1); 510 511 l_minuendCurX = new Double(l_x1); 512 l_minuendCurY = new Double(l_y1); 513 l_minuendNextX = new Double(l_x2); 514 l_minuendNextY = new Double(l_y2); 515 516 if (b_impliedZeroSubtrahend) { 517 l_x3 = l_subtrahendCurX.doubleValue(); 518 l_y3 = l_subtrahendCurY.doubleValue(); 519 l_x4 = l_subtrahendNextX.doubleValue(); 520 l_y4 = l_subtrahendNextY.doubleValue(); 521 } 522 else { 523 l_x3 = x_dataset.getXValue(1, l_subtrahendItem); 524 l_y3 = x_dataset.getYValue(1, l_subtrahendItem); 525 l_x4 = x_dataset.getXValue(1, l_subtrahendItem + 1); 526 l_y4 = x_dataset.getYValue(1, l_subtrahendItem + 1); 527 528 l_subtrahendCurX = new Double(l_x3); 529 l_subtrahendCurY = new Double(l_y3); 530 l_subtrahendNextX = new Double(l_x4); 531 l_subtrahendNextY = new Double(l_y4); 532 } 533 534 if (l_x2 <= l_x3) { 535 // minuend needs to be fast forwarded 536 l_minuendItem++; 537 b_minuendFastForward = true; 538 continue; 539 } 540 541 if (l_x4 <= l_x1) { 542 // subtrahend needs to be fast forwarded 543 l_subtrahendItem++; 544 b_subtrahendFastForward = true; 545 continue; 546 } 547 548 // check if initial polygon needs to be clipped 549 if ((l_x3 < l_x1) && (l_x1 < l_x4)) { 550 // project onto subtrahend 551 double l_slope = (l_y4 - l_y3) / (l_x4 - l_x3); 552 l_subtrahendCurX = l_minuendCurX; 553 l_subtrahendCurY = new Double((l_slope * l_x1) 554 + (l_y3 - (l_slope * l_x3))); 555 556 l_subtrahendXs.add(l_subtrahendCurX); 557 l_subtrahendYs.add(l_subtrahendCurY); 558 } 559 560 if ((l_x1 < l_x3) && (l_x3 < l_x2)) { 561 // project onto minuend 562 double l_slope = (l_y2 - l_y1) / (l_x2 - l_x1); 563 l_minuendCurX = l_subtrahendCurX; 564 l_minuendCurY = new Double((l_slope * l_x3) 565 + (l_y1 - (l_slope * l_x1))); 566 567 l_minuendXs.add(l_minuendCurX); 568 l_minuendYs.add(l_minuendCurY); 569 } 570 571 l_minuendMaxY = l_minuendCurY.doubleValue(); 572 l_minuendMinY = l_minuendCurY.doubleValue(); 573 l_subtrahendMaxY = l_subtrahendCurY.doubleValue(); 574 l_subtrahendMinY = l_subtrahendCurY.doubleValue(); 575 576 b_fastForwardDone = true; 577 } 578 579 // start of algorithm 580 while (!b_minuendDone && !b_subtrahendDone) { 581 if (!b_minuendDone && !b_minuendFastForward && b_minuendAdvanced) { 582 l_x1 = x_dataset.getXValue(0, l_minuendItem); 583 l_y1 = x_dataset.getYValue(0, l_minuendItem); 584 l_minuendCurX = new Double(l_x1); 585 l_minuendCurY = new Double(l_y1); 586 587 if (!b_minuendAtIntersect) { 588 l_minuendXs.add(l_minuendCurX); 589 l_minuendYs.add(l_minuendCurY); 590 } 591 592 l_minuendMaxY = Math.max(l_minuendMaxY, l_y1); 593 l_minuendMinY = Math.min(l_minuendMinY, l_y1); 594 595 l_x2 = x_dataset.getXValue(0, l_minuendItem + 1); 596 l_y2 = x_dataset.getYValue(0, l_minuendItem + 1); 597 l_minuendNextX = new Double(l_x2); 598 l_minuendNextY = new Double(l_y2); 599 } 600 601 // never updated the subtrahend if it is implied to be zero 602 if (!b_impliedZeroSubtrahend && !b_subtrahendDone 603 && !b_subtrahendFastForward && b_subtrahendAdvanced) { 604 l_x3 = x_dataset.getXValue(1, l_subtrahendItem); 605 l_y3 = x_dataset.getYValue(1, l_subtrahendItem); 606 l_subtrahendCurX = new Double(l_x3); 607 l_subtrahendCurY = new Double(l_y3); 608 609 if (!b_subtrahendAtIntersect) { 610 l_subtrahendXs.add(l_subtrahendCurX); 611 l_subtrahendYs.add(l_subtrahendCurY); 612 } 613 614 l_subtrahendMaxY = Math.max(l_subtrahendMaxY, l_y3); 615 l_subtrahendMinY = Math.min(l_subtrahendMinY, l_y3); 616 617 l_x4 = x_dataset.getXValue(1, l_subtrahendItem + 1); 618 l_y4 = x_dataset.getYValue(1, l_subtrahendItem + 1); 619 l_subtrahendNextX = new Double(l_x4); 620 l_subtrahendNextY = new Double(l_y4); 621 } 622 623 // deassert b_*FastForward (only matters for 1st time through loop) 624 b_minuendFastForward = false; 625 b_subtrahendFastForward = false; 626 627 Double l_intersectX = null; 628 Double l_intersectY = null; 629 boolean b_intersect = false; 630 631 b_minuendAtIntersect = false; 632 b_subtrahendAtIntersect = false; 633 634 // check for intersect 635 if ((l_x2 == l_x4) && (l_y2 == l_y4)) { 636 // check if line segments are colinear 637 if ((l_x1 == l_x3) && (l_y1 == l_y3)) { 638 b_colinear = true; 639 } 640 else { 641 // the intersect is at the next point for both the minuend 642 // and subtrahend 643 l_intersectX = new Double(l_x2); 644 l_intersectY = new Double(l_y2); 645 646 b_intersect = true; 647 b_minuendAtIntersect = true; 648 b_subtrahendAtIntersect = true; 649 } 650 } 651 else { 652 // compute common denominator 653 double l_denominator = ((l_y4 - l_y3) * (l_x2 - l_x1)) 654 - ((l_x4 - l_x3) * (l_y2 - l_y1)); 655 656 // compute common deltas 657 double l_deltaY = l_y1 - l_y3; 658 double l_deltaX = l_x1 - l_x3; 659 660 // compute numerators 661 double l_numeratorA = ((l_x4 - l_x3) * l_deltaY) 662 - ((l_y4 - l_y3) * l_deltaX); 663 double l_numeratorB = ((l_x2 - l_x1) * l_deltaY) 664 - ((l_y2 - l_y1) * l_deltaX); 665 666 // check if line segments are colinear 667 if ((0 == l_numeratorA) && (0 == l_numeratorB) 668 && (0 == l_denominator)) { 669 b_colinear = true; 670 } 671 else { 672 // check if previously colinear 673 if (b_colinear) { 674 // clear colinear points and flag 675 l_minuendXs.clear(); 676 l_minuendYs.clear(); 677 l_subtrahendXs.clear(); 678 l_subtrahendYs.clear(); 679 l_polygonXs.clear(); 680 l_polygonYs.clear(); 681 682 b_colinear = false; 683 684 // set new starting point for the polygon 685 boolean b_useMinuend = ((l_x3 <= l_x1) 686 && (l_x1 <= l_x4)); 687 l_polygonXs.add(b_useMinuend ? l_minuendCurX 688 : l_subtrahendCurX); 689 l_polygonYs.add(b_useMinuend ? l_minuendCurY 690 : l_subtrahendCurY); 691 } 692 693 // compute slope components 694 double l_slopeA = l_numeratorA / l_denominator; 695 double l_slopeB = l_numeratorB / l_denominator; 696 697 // check if the line segments intersect 698 if ((0 < l_slopeA) && (l_slopeA <= 1) && (0 < l_slopeB) 699 && (l_slopeB <= 1)) { 700 // compute the point of intersection 701 double l_xi = l_x1 + (l_slopeA * (l_x2 - l_x1)); 702 double l_yi = l_y1 + (l_slopeA * (l_y2 - l_y1)); 703 704 l_intersectX = new Double(l_xi); 705 l_intersectY = new Double(l_yi); 706 b_intersect = true; 707 b_minuendAtIntersect = ((l_xi == l_x2) 708 && (l_yi == l_y2)); 709 b_subtrahendAtIntersect = ((l_xi == l_x4) 710 && (l_yi == l_y4)); 711 712 // advance minuend and subtrahend to intesect 713 l_minuendCurX = l_intersectX; 714 l_minuendCurY = l_intersectY; 715 l_subtrahendCurX = l_intersectX; 716 l_subtrahendCurY = l_intersectY; 717 } 718 } 719 } 720 721 if (b_intersect) { 722 // create the polygon 723 // add the minuend's points to polygon 724 l_polygonXs.addAll(l_minuendXs); 725 l_polygonYs.addAll(l_minuendYs); 726 727 // add intersection point to the polygon 728 l_polygonXs.add(l_intersectX); 729 l_polygonYs.add(l_intersectY); 730 731 // add the subtrahend's points to the polygon in reverse 732 Collections.reverse(l_subtrahendXs); 733 Collections.reverse(l_subtrahendYs); 734 l_polygonXs.addAll(l_subtrahendXs); 735 l_polygonYs.addAll(l_subtrahendYs); 736 737 // create an actual polygon 738 b_positive = (l_subtrahendMaxY <= l_minuendMaxY) 739 && (l_subtrahendMinY <= l_minuendMinY); 740 createPolygon(x_graphics, x_dataArea, x_plot, x_domainAxis, 741 x_rangeAxis, b_positive, l_polygonXs, l_polygonYs); 742 743 // clear the point vectors 744 l_minuendXs.clear(); 745 l_minuendYs.clear(); 746 l_subtrahendXs.clear(); 747 l_subtrahendYs.clear(); 748 l_polygonXs.clear(); 749 l_polygonYs.clear(); 750 751 // set the maxY and minY values to intersect y-value 752 double l_y = l_intersectY.doubleValue(); 753 l_minuendMaxY = l_y; 754 l_subtrahendMaxY = l_y; 755 l_minuendMinY = l_y; 756 l_subtrahendMinY = l_y; 757 758 // add interection point to new polygon 759 l_polygonXs.add(l_intersectX); 760 l_polygonYs.add(l_intersectY); 761 } 762 763 // advance the minuend if needed 764 if (l_x2 <= l_x4) { 765 l_minuendItem++; 766 b_minuendAdvanced = true; 767 } 768 else { 769 b_minuendAdvanced = false; 770 } 771 772 // advance the subtrahend if needed 773 if (l_x4 <= l_x2) { 774 l_subtrahendItem++; 775 b_subtrahendAdvanced = true; 776 } 777 else { 778 b_subtrahendAdvanced = false; 779 } 780 781 b_minuendDone = (l_minuendItem == (l_minuendItemCount - 1)); 782 b_subtrahendDone = (l_subtrahendItem == (l_subtrahendItemCount 783 - 1)); 784 } 785 786 // check if the final polygon needs to be clipped 787 if (b_minuendDone && (l_x3 < l_x2) && (l_x2 < l_x4)) { 788 // project onto subtrahend 789 double l_slope = (l_y4 - l_y3) / (l_x4 - l_x3); 790 l_subtrahendNextX = l_minuendNextX; 791 l_subtrahendNextY = new Double((l_slope * l_x2) 792 + (l_y3 - (l_slope * l_x3))); 793 } 794 795 if (b_subtrahendDone && (l_x1 < l_x4) && (l_x4 < l_x2)) { 796 // project onto minuend 797 double l_slope = (l_y2 - l_y1) / (l_x2 - l_x1); 798 l_minuendNextX = l_subtrahendNextX; 799 l_minuendNextY = new Double((l_slope * l_x4) 800 + (l_y1 - (l_slope * l_x1))); 801 } 802 803 // consider last point of minuend and subtrahend for determining 804 // 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 // draw the item label if there is one... 931 if (isItemLabelVisible(x_series, x_item)) { 932 drawItemLabel(x_graphics, l_orientation, x_dataset, x_series, 933 x_item, l_x1, l_y1, (l_y1 < 0.0)); 934 } 935 936 int l_domainAxisIndex = x_plot.getDomainAxisIndex(x_domainAxis); 937 int l_rangeAxisIndex = x_plot.getRangeAxisIndex(x_rangeAxis); 938 updateCrosshairValues(x_crosshairState, l_x0, l_y0, l_domainAxisIndex, 939 l_rangeAxisIndex, l_x1, l_y1, l_orientation); 940 941 if (0 == x_item) { 942 return; 943 } 944 945 double l_x2 = x_domainAxis.valueToJava2D(x_dataset.getXValue(x_series, 946 (x_item - 1)), x_dataArea, l_domainAxisLocation); 947 double l_y2 = x_rangeAxis.valueToJava2D(x_dataset.getYValue(x_series, 948 (x_item - 1)), x_dataArea, l_rangeAxisLocation); 949 950 Line2D l_line = null; 951 if (PlotOrientation.HORIZONTAL == l_orientation) { 952 l_line = new Line2D.Double(l_y1, l_x1, l_y2, l_x2); 953 } 954 else if (PlotOrientation.VERTICAL == l_orientation) { 955 l_line = new Line2D.Double(l_x1, l_y1, l_x2, l_y2); 956 } 957 958 if ((null != l_line) && l_line.intersects(x_dataArea)) { 959 x_graphics.setPaint(getItemPaint(x_series, x_item)); 960 x_graphics.setStroke(getItemStroke(x_series, x_item)); 961 x_graphics.draw(l_line); 962 } 963 } 964 965 /** 966 * Determines if a dataset is degenerate. A degenerate dataset is a 967 * dataset where either series has less than two (2) points. 968 * 969 * @param x_dataset the dataset. 970 * @param x_impliedZeroSubtrahend if false, do not check the subtrahend 971 * 972 * @return true if the dataset is degenerate. 973 */ 974 private boolean isEitherSeriesDegenerate(XYDataset x_dataset, 975 boolean x_impliedZeroSubtrahend) { 976 977 if (x_impliedZeroSubtrahend) { 978 return (x_dataset.getItemCount(0) < 2); 979 } 980 981 return ((x_dataset.getItemCount(0) < 2) 982 || (x_dataset.getItemCount(1) < 2)); 983 } 984 985 /** 986 * Determines if the two (2) series are disjoint. 987 * Disjoint series do not overlap in the domain space. 988 * 989 * @param x_dataset the dataset. 990 * 991 * @return true if the dataset is degenerate. 992 */ 993 private boolean areSeriesDisjoint(XYDataset x_dataset) { 994 995 int l_minuendItemCount = x_dataset.getItemCount(0); 996 double l_minuendFirst = x_dataset.getXValue(0, 0); 997 double l_minuendLast = x_dataset.getXValue(0, l_minuendItemCount - 1); 998 999 int l_subtrahendItemCount = x_dataset.getItemCount(1); 1000 double l_subtrahendFirst = x_dataset.getXValue(1, 0); 1001 double l_subtrahendLast = x_dataset.getXValue(1, 1002 l_subtrahendItemCount - 1); 1003 1004 return ((l_minuendLast < l_subtrahendFirst) 1005 || (l_subtrahendLast < l_minuendFirst)); 1006 } 1007 1008 /** 1009 * Draws the visual representation of a polygon 1010 * 1011 * @param x_graphics the graphics device. 1012 * @param x_dataArea the area within which the data is being drawn. 1013 * @param x_plot the plot (can be used to obtain standard color 1014 * information etc). 1015 * @param x_domainAxis the domain (horizontal) axis. 1016 * @param x_rangeAxis the range (vertical) axis. 1017 * @param x_positive indicates if the polygon is positive (true) or 1018 * negative (false). 1019 * @param x_xValues a linked list of the x values (expects values to be 1020 * of type Double). 1021 * @param x_yValues a linked list of the y values (expects values to be 1022 * of type Double). 1023 */ 1024 private void createPolygon (Graphics2D x_graphics, 1025 Rectangle2D x_dataArea, 1026 XYPlot x_plot, 1027 ValueAxis x_domainAxis, 1028 ValueAxis x_rangeAxis, 1029 boolean x_positive, 1030 LinkedList x_xValues, 1031 LinkedList x_yValues) { 1032 1033 PlotOrientation l_orientation = x_plot.getOrientation(); 1034 RectangleEdge l_domainAxisLocation = x_plot.getDomainAxisEdge(); 1035 RectangleEdge l_rangeAxisLocation = x_plot.getRangeAxisEdge(); 1036 1037 Object[] l_xValues = x_xValues.toArray(); 1038 Object[] l_yValues = x_yValues.toArray(); 1039 1040 GeneralPath l_path = new GeneralPath(); 1041 1042 if (PlotOrientation.VERTICAL == l_orientation) { 1043 double l_x = x_domainAxis.valueToJava2D(( 1044 (Double) l_xValues[0]).doubleValue(), x_dataArea, 1045 l_domainAxisLocation); 1046 if (this.roundXCoordinates) { 1047 l_x = Math.rint(l_x); 1048 } 1049 1050 double l_y = x_rangeAxis.valueToJava2D(( 1051 (Double) l_yValues[0]).doubleValue(), x_dataArea, 1052 l_rangeAxisLocation); 1053 1054 l_path.moveTo((float) l_x, (float) l_y); 1055 for (int i = 1; i < l_xValues.length; i++) { 1056 l_x = x_domainAxis.valueToJava2D(( 1057 (Double) l_xValues[i]).doubleValue(), x_dataArea, 1058 l_domainAxisLocation); 1059 if (this.roundXCoordinates) { 1060 l_x = Math.rint(l_x); 1061 } 1062 1063 l_y = x_rangeAxis.valueToJava2D(( 1064 (Double) l_yValues[i]).doubleValue(), x_dataArea, 1065 l_rangeAxisLocation); 1066 l_path.lineTo((float) l_x, (float) l_y); 1067 } 1068 l_path.closePath(); 1069 } 1070 else { 1071 double l_x = x_domainAxis.valueToJava2D(( 1072 (Double) l_xValues[0]).doubleValue(), x_dataArea, 1073 l_domainAxisLocation); 1074 if (this.roundXCoordinates) { 1075 l_x = Math.rint(l_x); 1076 } 1077 1078 double l_y = x_rangeAxis.valueToJava2D(( 1079 (Double) l_yValues[0]).doubleValue(), x_dataArea, 1080 l_rangeAxisLocation); 1081 1082 l_path.moveTo((float) l_y, (float) l_x); 1083 for (int i = 1; i < l_xValues.length; i++) { 1084 l_x = x_domainAxis.valueToJava2D(( 1085 (Double) l_xValues[i]).doubleValue(), x_dataArea, 1086 l_domainAxisLocation); 1087 if (this.roundXCoordinates) { 1088 l_x = Math.rint(l_x); 1089 } 1090 1091 l_y = x_rangeAxis.valueToJava2D(( 1092 (Double) l_yValues[i]).doubleValue(), x_dataArea, 1093 l_rangeAxisLocation); 1094 l_path.lineTo((float) l_y, (float) l_x); 1095 } 1096 l_path.closePath(); 1097 } 1098 1099 if (l_path.intersects(x_dataArea)) { 1100 x_graphics.setPaint(x_positive ? getPositivePaint() 1101 : getNegativePaint()); 1102 x_graphics.fill(l_path); 1103 } 1104 } 1105 1106 /** 1107 * Returns a default legend item for the specified series. Subclasses 1108 * should override this method to generate customised items. 1109 * 1110 * @param datasetIndex the dataset index (zero-based). 1111 * @param series the series index (zero-based). 1112 * 1113 * @return A legend item for the series. 1114 */ 1115 public LegendItem getLegendItem(int datasetIndex, int series) { 1116 LegendItem result = null; 1117 XYPlot p = getPlot(); 1118 if (p != null) { 1119 XYDataset dataset = p.getDataset(datasetIndex); 1120 if (dataset != null) { 1121 if (getItemVisible(series, 0)) { 1122 String label = getLegendItemLabelGenerator().generateLabel( 1123 dataset, series); 1124 String description = label; 1125 String toolTipText = null; 1126 if (getLegendItemToolTipGenerator() != null) { 1127 toolTipText 1128 = getLegendItemToolTipGenerator().generateLabel( 1129 dataset, series); 1130 } 1131 String urlText = null; 1132 if (getLegendItemURLGenerator() != null) { 1133 urlText = getLegendItemURLGenerator().generateLabel( 1134 dataset, series); 1135 } 1136 Paint paint = lookupSeriesPaint(series); 1137 Stroke stroke = lookupSeriesStroke(series); 1138 // TODO: the following hard-coded line needs generalising 1139 Line2D line = new Line2D.Double(-7.0, 0.0, 7.0, 0.0); 1140 result = new LegendItem(label, description, 1141 toolTipText, urlText, line, stroke, paint); 1142 result.setDataset(dataset); 1143 result.setDatasetIndex(datasetIndex); 1144 result.setSeriesKey(dataset.getSeriesKey(series)); 1145 result.setSeriesIndex(series); 1146 } 1147 } 1148 1149 } 1150 1151 return result; 1152 1153 } 1154 1155 /** 1156 * Tests this renderer for equality with an arbitrary object. 1157 * 1158 * @param obj the object (<code>null</code> permitted). 1159 * 1160 * @return A boolean. 1161 */ 1162 public boolean equals(Object obj) { 1163 if (obj == this) { 1164 return true; 1165 } 1166 if (!(obj instanceof XYDifferenceRenderer)) { 1167 return false; 1168 } 1169 if (!super.equals(obj)) { 1170 return false; 1171 } 1172 XYDifferenceRenderer that = (XYDifferenceRenderer) obj; 1173 if (!PaintUtilities.equal(this.positivePaint, that.positivePaint)) { 1174 return false; 1175 } 1176 if (!PaintUtilities.equal(this.negativePaint, that.negativePaint)) { 1177 return false; 1178 } 1179 if (this.shapesVisible != that.shapesVisible) { 1180 return false; 1181 } 1182 if (!ShapeUtilities.equal(this.legendLine, that.legendLine)) { 1183 return false; 1184 } 1185 if (this.roundXCoordinates != that.roundXCoordinates) { 1186 return false; 1187 } 1188 return true; 1189 } 1190 1191 /** 1192 * Returns a clone of the renderer. 1193 * 1194 * @return A clone. 1195 * 1196 * @throws CloneNotSupportedException if the renderer cannot be cloned. 1197 */ 1198 public Object clone() throws CloneNotSupportedException { 1199 XYDifferenceRenderer clone = (XYDifferenceRenderer) super.clone(); 1200 clone.legendLine = ShapeUtilities.clone(this.legendLine); 1201 return clone; 1202 } 1203 1204 /** 1205 * Provides serialization support. 1206 * 1207 * @param stream the output stream. 1208 * 1209 * @throws IOException if there is an I/O error. 1210 */ 1211 private void writeObject(ObjectOutputStream stream) throws IOException { 1212 stream.defaultWriteObject(); 1213 SerialUtilities.writePaint(this.positivePaint, stream); 1214 SerialUtilities.writePaint(this.negativePaint, stream); 1215 SerialUtilities.writeShape(this.legendLine, stream); 1216 } 1217 1218 /** 1219 * Provides serialization support. 1220 * 1221 * @param stream the input stream. 1222 * 1223 * @throws IOException if there is an I/O error. 1224 * @throws ClassNotFoundException if there is a classpath problem. 1225 */ 1226 private void readObject(ObjectInputStream stream) 1227 throws IOException, ClassNotFoundException { 1228 stream.defaultReadObject(); 1229 this.positivePaint = SerialUtilities.readPaint(stream); 1230 this.negativePaint = SerialUtilities.readPaint(stream); 1231 this.legendLine = SerialUtilities.readShape(stream); 1232 } 1233 1234}