001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2014, 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 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 025 * Other names may be trademarks of their respective owners.] 026 * 027 * ------------------------- 028 * XYDifferenceRenderer.java 029 * ------------------------- 030 * (C) Copyright 2003-2014, 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 * Patrick Schlott 036 * Christoph Schroeder 037 * Martin Hoeller 038 * 039 * Changes: 040 * -------- 041 * 30-Apr-2003 : Version 1 (DG); 042 * 30-Jul-2003 : Modified entity constructor (CZ); 043 * 20-Aug-2003 : Implemented Cloneable and PublicCloneable (DG); 044 * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG); 045 * 09-Feb-2004 : Updated to support horizontal plot orientation (DG); 046 * 10-Feb-2004 : Added default constructor, setter methods and updated 047 * Javadocs (DG); 048 * 25-Feb-2004 : Replaced CrosshairInfo with CrosshairState (DG); 049 * 30-Mar-2004 : Fixed bug in getNegativePaint() method (DG); 050 * 15-Jul-2004 : Switched getX() with getXValue() and getY() with 051 * getYValue() (DG); 052 * 25-Aug-2004 : Fixed a bug preventing the use of crosshairs (DG); 053 * 11-Nov-2004 : Now uses ShapeUtilities to translate shapes (DG); 054 * 19-Jan-2005 : Now accesses only primitive values from dataset (DG); 055 * 22-Feb-2005 : Override getLegendItem(int, int) to return "line" items (DG); 056 * 13-Apr-2005 : Fixed shape positioning bug (id = 1182062) (DG); 057 * 20-Apr-2005 : Use generators for legend tooltips and URLs (DG); 058 * 04-May-2005 : Override equals() method, renamed get/setPlotShapes() --> 059 * get/setShapesVisible (DG); 060 * 09-Jun-2005 : Updated equals() to handle GradientPaint (DG); 061 * 16-Jun-2005 : Fix bug (1221021) affecting stroke used for each series (DG); 062 * ------------- JFREECHART 1.0.x --------------------------------------------- 063 * 24-Jan-2007 : Added flag to allow rounding of x-coordinates, and fixed 064 * bug in clone() (DG); 065 * 05-Feb-2007 : Added an extra call to updateCrosshairValues() in 066 * drawItemPass1(), to fix bug 1564967 (DG); 067 * 06-Feb-2007 : Fixed bug 1086307, crosshairs with multiple axes (DG); 068 * 08-Mar-2007 : Fixed entity generation (DG); 069 * 20-Apr-2007 : Updated getLegendItem() for renderer change (DG); 070 * 23-Apr-2007 : Rewrite of difference drawing algorithm to allow use of 071 * series with disjoint x-values (RW); 072 * 04-May-2007 : Set processVisibleItemsOnly flag to false (DG); 073 * 17-May-2007 : Set datasetIndex and seriesIndex in getLegendItem() (DG); 074 * 18-May-2007 : Set dataset and seriesKey for LegendItem (DG); 075 * 05-Nov-2007 : Draw item labels if visible (RW); 076 * 17-Jun-2008 : Apply legend shape, font and paint attributes (DG); 077 * 13-Feb-2012 : Applied patch 3450234 for bug 3425881 by Patrick Schlott and 078 * Christoph Schroeder (MH); 079 * 03-Jul-2013 : Use ParamChecks (DG); 080 * 081 */ 082 083package org.jfree.chart.renderer.xy; 084 085import java.awt.Color; 086import java.awt.Graphics2D; 087import java.awt.Paint; 088import java.awt.Shape; 089import java.awt.Stroke; 090import java.awt.geom.GeneralPath; 091import java.awt.geom.Line2D; 092import java.awt.geom.Rectangle2D; 093import java.io.IOException; 094import java.io.ObjectInputStream; 095import java.io.ObjectOutputStream; 096import java.util.Collections; 097import java.util.LinkedList; 098 099import org.jfree.chart.LegendItem; 100import org.jfree.chart.axis.ValueAxis; 101import org.jfree.chart.entity.EntityCollection; 102import org.jfree.chart.entity.XYItemEntity; 103import org.jfree.chart.event.RendererChangeEvent; 104import org.jfree.chart.labels.XYToolTipGenerator; 105import org.jfree.chart.plot.CrosshairState; 106import org.jfree.chart.plot.PlotOrientation; 107import org.jfree.chart.plot.PlotRenderingInfo; 108import org.jfree.chart.plot.XYPlot; 109import org.jfree.chart.urls.XYURLGenerator; 110import org.jfree.chart.util.ParamChecks; 111import org.jfree.data.xy.XYDataset; 112import org.jfree.io.SerialUtilities; 113import org.jfree.ui.RectangleEdge; 114import org.jfree.util.PaintUtilities; 115import org.jfree.util.PublicCloneable; 116import org.jfree.util.ShapeUtilities; 117 118/** 119 * A renderer for an {@link XYPlot} that highlights the differences between two 120 * series. The example shown here is generated by the 121 * <code>DifferenceChartDemo1.java</code> program included in the JFreeChart 122 * demo collection: 123 * <br><br> 124 * <img src="../../../../../images/XYDifferenceRendererSample.png" 125 * alt="XYDifferenceRendererSample.png"> 126 */ 127public class XYDifferenceRenderer extends AbstractXYItemRenderer 128 implements XYItemRenderer, PublicCloneable { 129 130 /** For serialization. */ 131 private static final long serialVersionUID = -8447915602375584857L; 132 133 /** The paint used to highlight positive differences (y(0) > y(1)). */ 134 private transient Paint positivePaint; 135 136 /** The paint used to highlight negative differences (y(0) < y(1)). */ 137 private transient Paint negativePaint; 138 139 /** Display shapes at each point? */ 140 private boolean shapesVisible; 141 142 /** The shape to display in the legend item. */ 143 private transient Shape legendLine; 144 145 /** 146 * This flag controls whether or not the x-coordinates (in Java2D space) 147 * are rounded to integers. When set to true, this can avoid the vertical 148 * striping that anti-aliasing can generate. However, the rounding may not 149 * be appropriate for output in high resolution formats (for example, 150 * vector graphics formats such as SVG and PDF). 151 * 152 * @since 1.0.4 153 */ 154 private boolean roundXCoordinates; 155 156 /** 157 * Creates a new renderer with default attributes. 158 */ 159 public XYDifferenceRenderer() { 160 this(Color.green, Color.red, false); 161 } 162 163 /** 164 * Creates a new renderer. 165 * 166 * @param positivePaint the highlight color for positive differences 167 * (<code>null</code> not permitted). 168 * @param negativePaint the highlight color for negative differences 169 * (<code>null</code> not permitted). 170 * @param shapes draw shapes? 171 */ 172 public XYDifferenceRenderer(Paint positivePaint, Paint negativePaint, 173 boolean shapes) { 174 ParamChecks.nullNotPermitted(positivePaint, "positivePaint"); 175 ParamChecks.nullNotPermitted(negativePaint, "negativePaint"); 176 this.positivePaint = positivePaint; 177 this.negativePaint = negativePaint; 178 this.shapesVisible = shapes; 179 this.legendLine = new Line2D.Double(-7.0, 0.0, 7.0, 0.0); 180 this.roundXCoordinates = false; 181 } 182 183 /** 184 * Returns the paint used to highlight positive differences. 185 * 186 * @return The paint (never <code>null</code>). 187 * 188 * @see #setPositivePaint(Paint) 189 */ 190 public Paint getPositivePaint() { 191 return this.positivePaint; 192 } 193 194 /** 195 * Sets the paint used to highlight positive differences and sends a 196 * {@link RendererChangeEvent} to all registered listeners. 197 * 198 * @param paint the paint (<code>null</code> not permitted). 199 * 200 * @see #getPositivePaint() 201 */ 202 public void setPositivePaint(Paint paint) { 203 ParamChecks.nullNotPermitted(paint, "paint"); 204 this.positivePaint = paint; 205 fireChangeEvent(); 206 } 207 208 /** 209 * Returns the paint used to highlight negative differences. 210 * 211 * @return The paint (never <code>null</code>). 212 * 213 * @see #setNegativePaint(Paint) 214 */ 215 public Paint getNegativePaint() { 216 return this.negativePaint; 217 } 218 219 /** 220 * Sets the paint used to highlight negative differences. 221 * 222 * @param paint the paint (<code>null</code> not permitted). 223 * 224 * @see #getNegativePaint() 225 */ 226 public void setNegativePaint(Paint paint) { 227 ParamChecks.nullNotPermitted(paint, "paint"); 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, and sends a {@link RendererChangeEvent} to all registered 247 * listeners. 248 * 249 * @param flag the flag. 250 * 251 * @see #getShapesVisible() 252 */ 253 public void setShapesVisible(boolean flag) { 254 this.shapesVisible = flag; 255 fireChangeEvent(); 256 } 257 258 /** 259 * Returns the shape used to represent a line in the legend. 260 * 261 * @return The legend line (never <code>null</code>). 262 * 263 * @see #setLegendLine(Shape) 264 */ 265 public Shape getLegendLine() { 266 return this.legendLine; 267 } 268 269 /** 270 * Sets the shape used as a line in each legend item and sends a 271 * {@link RendererChangeEvent} to all registered listeners. 272 * 273 * @param line the line (<code>null</code> not permitted). 274 * 275 * @see #getLegendLine() 276 */ 277 public void setLegendLine(Shape line) { 278 ParamChecks.nullNotPermitted(line, "line"); 279 this.legendLine = line; 280 fireChangeEvent(); 281 } 282 283 /** 284 * Returns the flag that controls whether or not the x-coordinates (in 285 * Java2D space) are rounded to integer values. 286 * 287 * @return The flag. 288 * 289 * @since 1.0.4 290 * 291 * @see #setRoundXCoordinates(boolean) 292 */ 293 public boolean getRoundXCoordinates() { 294 return this.roundXCoordinates; 295 } 296 297 /** 298 * Sets the flag that controls whether or not the x-coordinates (in 299 * Java2D space) are rounded to integer values, and sends a 300 * {@link RendererChangeEvent} to all registered listeners. 301 * 302 * @param round the new flag value. 303 * 304 * @since 1.0.4 305 * 306 * @see #getRoundXCoordinates() 307 */ 308 public void setRoundXCoordinates(boolean round) { 309 this.roundXCoordinates = round; 310 fireChangeEvent(); 311 } 312 313 /** 314 * Initialises the renderer and returns a state object that should be 315 * passed to subsequent calls to the drawItem() method. This method will 316 * be called before the first item is rendered, giving the renderer an 317 * opportunity to initialise any state information it wants to maintain. 318 * The renderer can do nothing if it chooses. 319 * 320 * @param g2 the graphics device. 321 * @param dataArea the area inside the axes. 322 * @param plot the plot. 323 * @param data the data. 324 * @param info an optional info collection object to return data back to 325 * the caller. 326 * 327 * @return A state object. 328 */ 329 @Override 330 public XYItemRendererState initialise(Graphics2D g2, Rectangle2D dataArea, 331 XYPlot plot, XYDataset data, PlotRenderingInfo info) { 332 XYItemRendererState state = super.initialise(g2, dataArea, plot, data, 333 info); 334 state.setProcessVisibleItemsOnly(false); 335 return state; 336 } 337 338 /** 339 * Returns <code>2</code>, the number of passes required by the renderer. 340 * The {@link XYPlot} will run through the dataset this number of times. 341 * 342 * @return The number of passes required by the renderer. 343 */ 344 @Override 345 public int getPassCount() { 346 return 2; 347 } 348 349 /** 350 * Draws the visual representation of a single data item. 351 * 352 * @param g2 the graphics device. 353 * @param state the renderer state. 354 * @param dataArea the area within which the data is being drawn. 355 * @param info collects information about the drawing. 356 * @param plot the plot (can be used to obtain standard color 357 * information etc). 358 * @param domainAxis the domain (horizontal) axis. 359 * @param rangeAxis the range (vertical) axis. 360 * @param dataset the dataset. 361 * @param series the series index (zero-based). 362 * @param item the item index (zero-based). 363 * @param crosshairState crosshair information for the plot 364 * (<code>null</code> permitted). 365 * @param pass the pass index. 366 */ 367 @Override 368 public void drawItem(Graphics2D g2, XYItemRendererState state, 369 Rectangle2D dataArea, PlotRenderingInfo info, XYPlot plot, 370 ValueAxis domainAxis, ValueAxis rangeAxis, XYDataset dataset, 371 int series, int item, CrosshairState crosshairState, int pass) { 372 373 if (pass == 0) { 374 drawItemPass0(g2, dataArea, info, plot, domainAxis, rangeAxis, 375 dataset, series, item, crosshairState); 376 } 377 else if (pass == 1) { 378 drawItemPass1(g2, dataArea, info, plot, domainAxis, rangeAxis, 379 dataset, series, item, crosshairState); 380 } 381 382 } 383 384 /** 385 * Draws the visual representation of a single data item, first pass. 386 * 387 * @param x_graphics the graphics device. 388 * @param x_dataArea the area within which the data is being drawn. 389 * @param x_info collects information about the drawing. 390 * @param x_plot the plot (can be used to obtain standard color 391 * information etc). 392 * @param x_domainAxis the domain (horizontal) axis. 393 * @param x_rangeAxis the range (vertical) axis. 394 * @param x_dataset the dataset. 395 * @param x_series the series index (zero-based). 396 * @param x_item the item index (zero-based). 397 * @param x_crosshairState crosshair information for the plot 398 * (<code>null</code> permitted). 399 */ 400 protected void drawItemPass0(Graphics2D x_graphics, 401 Rectangle2D x_dataArea, 402 PlotRenderingInfo x_info, 403 XYPlot x_plot, 404 ValueAxis x_domainAxis, 405 ValueAxis x_rangeAxis, 406 XYDataset x_dataset, 407 int x_series, 408 int x_item, 409 CrosshairState x_crosshairState) { 410 411 if (!((0 == x_series) && (0 == x_item))) { 412 return; 413 } 414 415 boolean b_impliedZeroSubtrahend = (1 == x_dataset.getSeriesCount()); 416 417 // check if either series is a degenerate case (i.e. less than 2 points) 418 if (isEitherSeriesDegenerate(x_dataset, b_impliedZeroSubtrahend)) { 419 return; 420 } 421 422 // check if series are disjoint (i.e. domain-spans do not overlap) 423 if (!b_impliedZeroSubtrahend && areSeriesDisjoint(x_dataset)) { 424 return; 425 } 426 427 // polygon definitions 428 LinkedList l_minuendXs = new LinkedList(); 429 LinkedList l_minuendYs = new LinkedList(); 430 LinkedList l_subtrahendXs = new LinkedList(); 431 LinkedList l_subtrahendYs = new LinkedList(); 432 LinkedList l_polygonXs = new LinkedList(); 433 LinkedList l_polygonYs = new LinkedList(); 434 435 // state 436 int l_minuendItem = 0; 437 int l_minuendItemCount = x_dataset.getItemCount(0); 438 Double l_minuendCurX = null; 439 Double l_minuendNextX = null; 440 Double l_minuendCurY = null; 441 Double l_minuendNextY = null; 442 double l_minuendMaxY = Double.NEGATIVE_INFINITY; 443 double l_minuendMinY = Double.POSITIVE_INFINITY; 444 445 int l_subtrahendItem = 0; 446 int l_subtrahendItemCount = 0; // actual value set below 447 Double l_subtrahendCurX = null; 448 Double l_subtrahendNextX = null; 449 Double l_subtrahendCurY = null; 450 Double l_subtrahendNextY = null; 451 double l_subtrahendMaxY = Double.NEGATIVE_INFINITY; 452 double l_subtrahendMinY = Double.POSITIVE_INFINITY; 453 454 // if a subtrahend is not specified, assume it is zero 455 if (b_impliedZeroSubtrahend) { 456 l_subtrahendItem = 0; 457 l_subtrahendItemCount = 2; 458 l_subtrahendCurX = new Double(x_dataset.getXValue(0, 0)); 459 l_subtrahendNextX = new Double(x_dataset.getXValue(0, 460 (l_minuendItemCount - 1))); 461 l_subtrahendCurY = new Double(0.0); 462 l_subtrahendNextY = new Double(0.0); 463 l_subtrahendMaxY = 0.0; 464 l_subtrahendMinY = 0.0; 465 466 l_subtrahendXs.add(l_subtrahendCurX); 467 l_subtrahendYs.add(l_subtrahendCurY); 468 } 469 else { 470 l_subtrahendItemCount = x_dataset.getItemCount(1); 471 } 472 473 boolean b_minuendDone = false; 474 boolean b_minuendAdvanced = true; 475 boolean b_minuendAtIntersect = false; 476 boolean b_minuendFastForward = false; 477 boolean b_subtrahendDone = false; 478 boolean b_subtrahendAdvanced = true; 479 boolean b_subtrahendAtIntersect = false; 480 boolean b_subtrahendFastForward = false; 481 boolean b_colinear = false; 482 483 boolean b_positive; 484 485 // coordinate pairs 486 double l_x1 = 0.0, l_y1 = 0.0; // current minuend point 487 double l_x2 = 0.0, l_y2 = 0.0; // next minuend point 488 double l_x3 = 0.0, l_y3 = 0.0; // current subtrahend point 489 double l_x4 = 0.0, l_y4 = 0.0; // next subtrahend point 490 491 // fast-forward through leading tails 492 boolean b_fastForwardDone = false; 493 while (!b_fastForwardDone) { 494 // get the x and y coordinates 495 l_x1 = x_dataset.getXValue(0, l_minuendItem); 496 l_y1 = x_dataset.getYValue(0, l_minuendItem); 497 l_x2 = x_dataset.getXValue(0, l_minuendItem + 1); 498 l_y2 = x_dataset.getYValue(0, l_minuendItem + 1); 499 500 l_minuendCurX = new Double(l_x1); 501 l_minuendCurY = new Double(l_y1); 502 l_minuendNextX = new Double(l_x2); 503 l_minuendNextY = new Double(l_y2); 504 505 if (b_impliedZeroSubtrahend) { 506 l_x3 = l_subtrahendCurX.doubleValue(); 507 l_y3 = l_subtrahendCurY.doubleValue(); 508 l_x4 = l_subtrahendNextX.doubleValue(); 509 l_y4 = l_subtrahendNextY.doubleValue(); 510 } 511 else { 512 l_x3 = x_dataset.getXValue(1, l_subtrahendItem); 513 l_y3 = x_dataset.getYValue(1, l_subtrahendItem); 514 l_x4 = x_dataset.getXValue(1, l_subtrahendItem + 1); 515 l_y4 = x_dataset.getYValue(1, l_subtrahendItem + 1); 516 517 l_subtrahendCurX = new Double(l_x3); 518 l_subtrahendCurY = new Double(l_y3); 519 l_subtrahendNextX = new Double(l_x4); 520 l_subtrahendNextY = new Double(l_y4); 521 } 522 523 if (l_x2 <= l_x3) { 524 // minuend needs to be fast forwarded 525 l_minuendItem++; 526 b_minuendFastForward = true; 527 continue; 528 } 529 530 if (l_x4 <= l_x1) { 531 // subtrahend needs to be fast forwarded 532 l_subtrahendItem++; 533 b_subtrahendFastForward = true; 534 continue; 535 } 536 537 // check if initial polygon needs to be clipped 538 if ((l_x3 < l_x1) && (l_x1 < l_x4)) { 539 // project onto subtrahend 540 double l_slope = (l_y4 - l_y3) / (l_x4 - l_x3); 541 l_subtrahendCurX = l_minuendCurX; 542 l_subtrahendCurY = new Double((l_slope * l_x1) 543 + (l_y3 - (l_slope * l_x3))); 544 545 l_subtrahendXs.add(l_subtrahendCurX); 546 l_subtrahendYs.add(l_subtrahendCurY); 547 } 548 549 if ((l_x1 < l_x3) && (l_x3 < l_x2)) { 550 // project onto minuend 551 double l_slope = (l_y2 - l_y1) / (l_x2 - l_x1); 552 l_minuendCurX = l_subtrahendCurX; 553 l_minuendCurY = new Double((l_slope * l_x3) 554 + (l_y1 - (l_slope * l_x1))); 555 556 l_minuendXs.add(l_minuendCurX); 557 l_minuendYs.add(l_minuendCurY); 558 } 559 560 l_minuendMaxY = l_minuendCurY.doubleValue(); 561 l_minuendMinY = l_minuendCurY.doubleValue(); 562 l_subtrahendMaxY = l_subtrahendCurY.doubleValue(); 563 l_subtrahendMinY = l_subtrahendCurY.doubleValue(); 564 565 b_fastForwardDone = true; 566 } 567 568 // start of algorithm 569 while (!b_minuendDone && !b_subtrahendDone) { 570 if (!b_minuendDone && !b_minuendFastForward && b_minuendAdvanced) { 571 l_x1 = x_dataset.getXValue(0, l_minuendItem); 572 l_y1 = x_dataset.getYValue(0, l_minuendItem); 573 l_minuendCurX = new Double(l_x1); 574 l_minuendCurY = new Double(l_y1); 575 576 if (!b_minuendAtIntersect) { 577 l_minuendXs.add(l_minuendCurX); 578 l_minuendYs.add(l_minuendCurY); 579 } 580 581 l_minuendMaxY = Math.max(l_minuendMaxY, l_y1); 582 l_minuendMinY = Math.min(l_minuendMinY, l_y1); 583 584 l_x2 = x_dataset.getXValue(0, l_minuendItem + 1); 585 l_y2 = x_dataset.getYValue(0, l_minuendItem + 1); 586 l_minuendNextX = new Double(l_x2); 587 l_minuendNextY = new Double(l_y2); 588 } 589 590 // never updated the subtrahend if it is implied to be zero 591 if (!b_impliedZeroSubtrahend && !b_subtrahendDone 592 && !b_subtrahendFastForward && b_subtrahendAdvanced) { 593 l_x3 = x_dataset.getXValue(1, l_subtrahendItem); 594 l_y3 = x_dataset.getYValue(1, l_subtrahendItem); 595 l_subtrahendCurX = new Double(l_x3); 596 l_subtrahendCurY = new Double(l_y3); 597 598 if (!b_subtrahendAtIntersect) { 599 l_subtrahendXs.add(l_subtrahendCurX); 600 l_subtrahendYs.add(l_subtrahendCurY); 601 } 602 603 l_subtrahendMaxY = Math.max(l_subtrahendMaxY, l_y3); 604 l_subtrahendMinY = Math.min(l_subtrahendMinY, l_y3); 605 606 l_x4 = x_dataset.getXValue(1, l_subtrahendItem + 1); 607 l_y4 = x_dataset.getYValue(1, l_subtrahendItem + 1); 608 l_subtrahendNextX = new Double(l_x4); 609 l_subtrahendNextY = new Double(l_y4); 610 } 611 612 // deassert b_*FastForward (only matters for 1st time through loop) 613 b_minuendFastForward = false; 614 b_subtrahendFastForward = false; 615 616 Double l_intersectX = null; 617 Double l_intersectY = null; 618 boolean b_intersect = false; 619 620 b_minuendAtIntersect = false; 621 b_subtrahendAtIntersect = false; 622 623 // check for intersect 624 if ((l_x2 == l_x4) && (l_y2 == l_y4)) { 625 // check if line segments are colinear 626 if ((l_x1 == l_x3) && (l_y1 == l_y3)) { 627 b_colinear = true; 628 } 629 else { 630 // the intersect is at the next point for both the minuend 631 // and subtrahend 632 l_intersectX = new Double(l_x2); 633 l_intersectY = new Double(l_y2); 634 635 b_intersect = true; 636 b_minuendAtIntersect = true; 637 b_subtrahendAtIntersect = true; 638 } 639 } 640 else { 641 // compute common denominator 642 double l_denominator = ((l_y4 - l_y3) * (l_x2 - l_x1)) 643 - ((l_x4 - l_x3) * (l_y2 - l_y1)); 644 645 // compute common deltas 646 double l_deltaY = l_y1 - l_y3; 647 double l_deltaX = l_x1 - l_x3; 648 649 // compute numerators 650 double l_numeratorA = ((l_x4 - l_x3) * l_deltaY) 651 - ((l_y4 - l_y3) * l_deltaX); 652 double l_numeratorB = ((l_x2 - l_x1) * l_deltaY) 653 - ((l_y2 - l_y1) * l_deltaX); 654 655 // check if line segments are colinear 656 if ((0 == l_numeratorA) && (0 == l_numeratorB) 657 && (0 == l_denominator)) { 658 b_colinear = true; 659 } 660 else { 661 // check if previously colinear 662 if (b_colinear) { 663 // clear colinear points and flag 664 l_minuendXs.clear(); 665 l_minuendYs.clear(); 666 l_subtrahendXs.clear(); 667 l_subtrahendYs.clear(); 668 l_polygonXs.clear(); 669 l_polygonYs.clear(); 670 671 b_colinear = false; 672 673 // set new starting point for the polygon 674 boolean b_useMinuend = ((l_x3 <= l_x1) 675 && (l_x1 <= l_x4)); 676 l_polygonXs.add(b_useMinuend ? l_minuendCurX 677 : l_subtrahendCurX); 678 l_polygonYs.add(b_useMinuend ? l_minuendCurY 679 : l_subtrahendCurY); 680 } 681 } 682 683 // compute slope components 684 double l_slopeA = l_numeratorA / l_denominator; 685 double l_slopeB = l_numeratorB / l_denominator; 686 687 // test if both grahphs have a vertical rise at the same x-value 688 boolean b_vertical = (l_x1 == l_x2) && (l_x3 == l_x4) && (l_x2 == l_x4); 689 690 // check if the line segments intersect 691 if (((0 < l_slopeA) && (l_slopeA <= 1) && (0 < l_slopeB) 692 && (l_slopeB <= 1))|| b_vertical) { 693 694 // compute the point of intersection 695 double l_xi; 696 double l_yi; 697 if(b_vertical){ 698 b_colinear = false; 699 l_xi = l_x2; 700 l_yi = l_x4; 701 } 702 else{ 703 l_xi = l_x1 + (l_slopeA * (l_x2 - l_x1)); 704 l_yi = l_y1 + (l_slopeA * (l_y2 - l_y1)); 705 } 706 707 l_intersectX = new Double(l_xi); 708 l_intersectY = new Double(l_yi); 709 b_intersect = true; 710 b_minuendAtIntersect = ((l_xi == l_x2) 711 && (l_yi == l_y2)); 712 b_subtrahendAtIntersect = ((l_xi == l_x4) 713 && (l_yi == l_y4)); 714 715 // advance minuend and subtrahend to intesect 716 l_minuendCurX = l_intersectX; 717 l_minuendCurY = l_intersectY; 718 l_subtrahendCurX = l_intersectX; 719 l_subtrahendCurY = l_intersectY; 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 @Override 1118 public LegendItem getLegendItem(int datasetIndex, int series) { 1119 LegendItem result = null; 1120 XYPlot p = getPlot(); 1121 if (p != null) { 1122 XYDataset dataset = p.getDataset(datasetIndex); 1123 if (dataset != null) { 1124 if (getItemVisible(series, 0)) { 1125 String label = getLegendItemLabelGenerator().generateLabel( 1126 dataset, series); 1127 String description = label; 1128 String toolTipText = null; 1129 if (getLegendItemToolTipGenerator() != null) { 1130 toolTipText 1131 = getLegendItemToolTipGenerator().generateLabel( 1132 dataset, series); 1133 } 1134 String urlText = null; 1135 if (getLegendItemURLGenerator() != null) { 1136 urlText = getLegendItemURLGenerator().generateLabel( 1137 dataset, series); 1138 } 1139 Paint paint = lookupSeriesPaint(series); 1140 Stroke stroke = lookupSeriesStroke(series); 1141 Shape line = getLegendLine(); 1142 result = new LegendItem(label, description, 1143 toolTipText, urlText, line, stroke, paint); 1144 result.setLabelFont(lookupLegendTextFont(series)); 1145 Paint labelPaint = lookupLegendTextPaint(series); 1146 if (labelPaint != null) { 1147 result.setLabelPaint(labelPaint); 1148 } 1149 result.setDataset(dataset); 1150 result.setDatasetIndex(datasetIndex); 1151 result.setSeriesKey(dataset.getSeriesKey(series)); 1152 result.setSeriesIndex(series); 1153 } 1154 } 1155 1156 } 1157 1158 return result; 1159 1160 } 1161 1162 /** 1163 * Tests this renderer for equality with an arbitrary object. 1164 * 1165 * @param obj the object (<code>null</code> permitted). 1166 * 1167 * @return A boolean. 1168 */ 1169 @Override 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 @Override 1207 public Object clone() throws CloneNotSupportedException { 1208 XYDifferenceRenderer clone = (XYDifferenceRenderer) super.clone(); 1209 clone.legendLine = ShapeUtilities.clone(this.legendLine); 1210 return clone; 1211 } 1212 1213 /** 1214 * Provides serialization support. 1215 * 1216 * @param stream the output stream. 1217 * 1218 * @throws IOException if there is an I/O error. 1219 */ 1220 private void writeObject(ObjectOutputStream stream) throws IOException { 1221 stream.defaultWriteObject(); 1222 SerialUtilities.writePaint(this.positivePaint, stream); 1223 SerialUtilities.writePaint(this.negativePaint, stream); 1224 SerialUtilities.writeShape(this.legendLine, stream); 1225 } 1226 1227 /** 1228 * Provides serialization support. 1229 * 1230 * @param stream the input stream. 1231 * 1232 * @throws IOException if there is an I/O error. 1233 * @throws ClassNotFoundException if there is a classpath problem. 1234 */ 1235 private void readObject(ObjectInputStream stream) 1236 throws IOException, ClassNotFoundException { 1237 stream.defaultReadObject(); 1238 this.positivePaint = SerialUtilities.readPaint(stream); 1239 this.negativePaint = SerialUtilities.readPaint(stream); 1240 this.legendLine = SerialUtilities.readShape(stream); 1241 } 1242 1243}