001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2009, 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 * BarRenderer.java 029 * ---------------- 030 * (C) Copyright 2002-2009, by Object Refinery Limited. 031 * 032 * Original Author: David Gilbert (for Object Refinery Limited); 033 * Contributor(s): Christian W. Zuckschwerdt; 034 * Peter Kolb (patch 2497611); 035 * 036 * Changes 037 * ------- 038 * 14-Mar-2002 : Version 1 (DG); 039 * 23-May-2002 : Added tooltip generator to renderer (DG); 040 * 29-May-2002 : Moved tooltip generator to abstract super-class (DG); 041 * 25-Jun-2002 : Changed constructor to protected and removed redundant 042 * code (DG); 043 * 26-Jun-2002 : Added axis to initialise method, and record upper and lower 044 * clip values (DG); 045 * 24-Sep-2002 : Added getLegendItem() method (DG); 046 * 09-Oct-2002 : Modified constructor to include URL generator (DG); 047 * 05-Nov-2002 : Base dataset is now TableDataset not CategoryDataset (DG); 048 * 10-Jan-2003 : Moved get/setItemMargin() method up from subclasses (DG); 049 * 17-Jan-2003 : Moved plot classes into a separate package (DG); 050 * 25-Mar-2003 : Implemented Serializable (DG); 051 * 01-May-2003 : Modified clipping to allow for dual axes and datasets (DG); 052 * 12-May-2003 : Merged horizontal and vertical bar renderers (DG); 053 * 12-Jun-2003 : Updates for item labels (DG); 054 * 30-Jul-2003 : Modified entity constructor (CZ); 055 * 02-Sep-2003 : Changed initialise method to fix bug 790407 (DG); 056 * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG); 057 * 07-Oct-2003 : Added renderer state (DG); 058 * 27-Oct-2003 : Merged drawHorizontalItem() and drawVerticalItem() 059 * methods (DG); 060 * 28-Oct-2003 : Added support for gradient paint on bars (DG); 061 * 14-Nov-2003 : Added 'maxBarWidth' attribute (DG); 062 * 10-Feb-2004 : Small changes inside drawItem() method to ease cut-and-paste 063 * overriding (DG); 064 * 19-Mar-2004 : Fixed bug introduced with separation of tool tip and item 065 * label generators. Fixed equals() method (DG); 066 * 11-May-2004 : Fix for null pointer exception (bug id 951127) (DG); 067 * 05-Nov-2004 : Modified drawItem() signature (DG); 068 * 26-Jan-2005 : Provided override for getLegendItem() method (DG); 069 * 20-Apr-2005 : Generate legend labels, tooltips and URLs (DG); 070 * 18-May-2005 : Added configurable base value (DG); 071 * 09-Jun-2005 : Use addItemEntity() method from superclass (DG); 072 * 01-Dec-2005 : Update legend item to use/not use outline (DG); 073 * ------------: JFreeChart 1.0.x --------------------------------------------- 074 * 06-Dec-2005 : Fixed bug 1374222 (JDK 1.4 specific code) (DG); 075 * 11-Jan-2006 : Fixed bug 1401856 (bad rendering for non-zero base) (DG); 076 * 04-Aug-2006 : Fixed bug 1467706 (missing item labels for zero value 077 * bars) (DG); 078 * 04-Dec-2006 : Fixed bug in rendering to non-primary axis (DG); 079 * 13-Dec-2006 : Add support for GradientPaint display in legend items (DG); 080 * 20-Apr-2007 : Updated getLegendItem() for renderer change (DG); 081 * 11-May-2007 : Check for visibility in getLegendItem() (DG); 082 * 17-May-2007 : Set datasetIndex and seriesIndex in getLegendItem() (DG); 083 * 18-May-2007 : Set dataset and seriesKey for LegendItem (DG); 084 * 07-May-2008 : If minimumBarLength is > 0.0, extend the non-base end of the 085 * bar (DG); 086 * 17-Jun-2008 : Apply legend shape, font and paint attributes (DG); 087 * 24-Jun-2008 : Added barPainter mechanism (DG); 088 * 26-Jun-2008 : Added crosshair support (DG); 089 * 13-Aug-2008 : Added shadowPaint attribute (DG); 090 * 14-Jan-2009 : Added support for seriesVisible flags (PK); 091 * 03-Feb-2009 : Added defaultShadowsVisible flag - see patch 2511330 (PK); 092 * 093 */ 094 095 package org.jfree.chart.renderer.category; 096 097 import java.awt.BasicStroke; 098 import java.awt.Color; 099 import java.awt.Font; 100 import java.awt.Graphics2D; 101 import java.awt.Paint; 102 import java.awt.Shape; 103 import java.awt.Stroke; 104 import java.awt.geom.Line2D; 105 import java.awt.geom.Point2D; 106 import java.awt.geom.Rectangle2D; 107 import java.io.IOException; 108 import java.io.ObjectInputStream; 109 import java.io.ObjectOutputStream; 110 import java.io.Serializable; 111 112 import org.jfree.chart.LegendItem; 113 import org.jfree.chart.axis.CategoryAxis; 114 import org.jfree.chart.axis.ValueAxis; 115 import org.jfree.chart.entity.EntityCollection; 116 import org.jfree.chart.event.RendererChangeEvent; 117 import org.jfree.chart.labels.CategoryItemLabelGenerator; 118 import org.jfree.chart.labels.ItemLabelAnchor; 119 import org.jfree.chart.labels.ItemLabelPosition; 120 import org.jfree.chart.plot.CategoryPlot; 121 import org.jfree.chart.plot.PlotOrientation; 122 import org.jfree.chart.plot.PlotRenderingInfo; 123 import org.jfree.data.Range; 124 import org.jfree.data.category.CategoryDataset; 125 import org.jfree.data.general.DatasetUtilities; 126 import org.jfree.io.SerialUtilities; 127 import org.jfree.text.TextUtilities; 128 import org.jfree.ui.GradientPaintTransformer; 129 import org.jfree.ui.RectangleEdge; 130 import org.jfree.ui.StandardGradientPaintTransformer; 131 import org.jfree.util.ObjectUtilities; 132 import org.jfree.util.PaintUtilities; 133 import org.jfree.util.PublicCloneable; 134 135 /** 136 * A {@link CategoryItemRenderer} that draws individual data items as bars. 137 * The example shown here is generated by the <code>BarChartDemo1.java</code> 138 * program included in the JFreeChart Demo Collection: 139 * <br><br> 140 * <img src="../../../../../images/BarRendererSample.png" 141 * alt="BarRendererSample.png" /> 142 */ 143 public class BarRenderer extends AbstractCategoryItemRenderer 144 implements Cloneable, PublicCloneable, Serializable { 145 146 /** For serialization. */ 147 private static final long serialVersionUID = 6000649414965887481L; 148 149 /** The default item margin percentage. */ 150 public static final double DEFAULT_ITEM_MARGIN = 0.20; 151 152 /** 153 * Constant that controls the minimum width before a bar has an outline 154 * drawn. 155 */ 156 public static final double BAR_OUTLINE_WIDTH_THRESHOLD = 3.0; 157 158 /** 159 * The default bar painter assigned to each new instance of this renderer. 160 * 161 * @since 1.0.11 162 */ 163 private static BarPainter defaultBarPainter = new GradientBarPainter(); 164 165 /** 166 * Returns the default bar painter. 167 * 168 * @return The default bar painter. 169 * 170 * @since 1.0.11 171 */ 172 public static BarPainter getDefaultBarPainter() { 173 return BarRenderer.defaultBarPainter; 174 } 175 176 /** 177 * Sets the default bar painter. 178 * 179 * @param painter the painter (<code>null</code> not permitted). 180 * 181 * @since 1.0.11 182 */ 183 public static void setDefaultBarPainter(BarPainter painter) { 184 if (painter == null) { 185 throw new IllegalArgumentException("Null 'painter' argument."); 186 } 187 BarRenderer.defaultBarPainter = painter; 188 } 189 190 /** 191 * The default value for the initialisation of the shadowsVisible flag. 192 */ 193 private static boolean defaultShadowsVisible = true; 194 195 /** 196 * Returns the default value for the <code>shadowsVisible</code> flag. 197 * 198 * @return A boolean. 199 * 200 * @see #setDefaultShadowsVisible(boolean) 201 * 202 * @since 1.0.13 203 */ 204 public static boolean getDefaultShadowsVisible() { 205 return BarRenderer.defaultShadowsVisible; 206 } 207 208 /** 209 * Sets the default value for the shadows visible flag. 210 * 211 * @param visible the new value for the default. 212 * 213 * @see #getDefaultShadowsVisible() 214 * 215 * @since 1.0.13 216 */ 217 public static void setDefaultShadowsVisible(boolean visible) { 218 BarRenderer.defaultShadowsVisible = visible; 219 } 220 221 /** The margin between items (bars) within a category. */ 222 private double itemMargin; 223 224 /** A flag that controls whether or not bar outlines are drawn. */ 225 private boolean drawBarOutline; 226 227 /** The maximum bar width as a percentage of the available space. */ 228 private double maximumBarWidth; 229 230 /** The minimum bar length (in Java2D units). */ 231 private double minimumBarLength; 232 233 /** 234 * An optional class used to transform gradient paint objects to fit each 235 * bar. 236 */ 237 private GradientPaintTransformer gradientPaintTransformer; 238 239 /** 240 * The fallback position if a positive item label doesn't fit inside the 241 * bar. 242 */ 243 private ItemLabelPosition positiveItemLabelPositionFallback; 244 245 /** 246 * The fallback position if a negative item label doesn't fit inside the 247 * bar. 248 */ 249 private ItemLabelPosition negativeItemLabelPositionFallback; 250 251 /** The upper clip (axis) value for the axis. */ 252 private double upperClip; 253 // TODO: this needs to move into the renderer state 254 255 /** The lower clip (axis) value for the axis. */ 256 private double lowerClip; 257 // TODO: this needs to move into the renderer state 258 259 /** The base value for the bars (defaults to 0.0). */ 260 private double base; 261 262 /** 263 * A flag that controls whether the base value is included in the range 264 * returned by the findRangeBounds() method. 265 */ 266 private boolean includeBaseInRange; 267 268 /** 269 * The bar painter (never <code>null</code>). 270 * 271 * @since 1.0.11 272 */ 273 private BarPainter barPainter; 274 275 /** 276 * The flag that controls whether or not shadows are drawn for the bars. 277 * 278 * @since 1.0.11 279 */ 280 private boolean shadowsVisible; 281 282 /** 283 * The shadow paint. 284 * 285 * @since 1.0.11 286 */ 287 private transient Paint shadowPaint; 288 289 /** 290 * The x-offset for the shadow effect. 291 * 292 * @since 1.0.11 293 */ 294 private double shadowXOffset; 295 296 /** 297 * The y-offset for the shadow effect. 298 * 299 * @since 1.0.11 300 */ 301 private double shadowYOffset; 302 303 /** 304 * Creates a new bar renderer with default settings. 305 */ 306 public BarRenderer() { 307 super(); 308 this.base = 0.0; 309 this.includeBaseInRange = true; 310 this.itemMargin = DEFAULT_ITEM_MARGIN; 311 this.drawBarOutline = false; 312 this.maximumBarWidth = 1.0; 313 // 100 percent, so it will not apply unless changed 314 this.positiveItemLabelPositionFallback = null; 315 this.negativeItemLabelPositionFallback = null; 316 this.gradientPaintTransformer = new StandardGradientPaintTransformer(); 317 this.minimumBarLength = 0.0; 318 setBaseLegendShape(new Rectangle2D.Double(-4.0, -4.0, 8.0, 8.0)); 319 this.barPainter = getDefaultBarPainter(); 320 this.shadowsVisible = getDefaultShadowsVisible(); 321 this.shadowPaint = Color.gray; 322 this.shadowXOffset = 4.0; 323 this.shadowYOffset = 4.0; 324 } 325 326 /** 327 * Returns the base value for the bars. The default value is 328 * <code>0.0</code>. 329 * 330 * @return The base value for the bars. 331 * 332 * @see #setBase(double) 333 */ 334 public double getBase() { 335 return this.base; 336 } 337 338 /** 339 * Sets the base value for the bars and sends a {@link RendererChangeEvent} 340 * to all registered listeners. 341 * 342 * @param base the new base value. 343 * 344 * @see #getBase() 345 */ 346 public void setBase(double base) { 347 this.base = base; 348 fireChangeEvent(); 349 } 350 351 /** 352 * Returns the item margin as a percentage of the available space for all 353 * bars. 354 * 355 * @return The margin percentage (where 0.10 is ten percent). 356 * 357 * @see #setItemMargin(double) 358 */ 359 public double getItemMargin() { 360 return this.itemMargin; 361 } 362 363 /** 364 * Sets the item margin and sends a {@link RendererChangeEvent} to all 365 * registered listeners. The value is expressed as a percentage of the 366 * available width for plotting all the bars, with the resulting amount to 367 * be distributed between all the bars evenly. 368 * 369 * @param percent the margin (where 0.10 is ten percent). 370 * 371 * @see #getItemMargin() 372 */ 373 public void setItemMargin(double percent) { 374 this.itemMargin = percent; 375 fireChangeEvent(); 376 } 377 378 /** 379 * Returns a flag that controls whether or not bar outlines are drawn. 380 * 381 * @return A boolean. 382 * 383 * @see #setDrawBarOutline(boolean) 384 */ 385 public boolean isDrawBarOutline() { 386 return this.drawBarOutline; 387 } 388 389 /** 390 * Sets the flag that controls whether or not bar outlines are drawn and 391 * sends a {@link RendererChangeEvent} to all registered listeners. 392 * 393 * @param draw the flag. 394 * 395 * @see #isDrawBarOutline() 396 */ 397 public void setDrawBarOutline(boolean draw) { 398 this.drawBarOutline = draw; 399 fireChangeEvent(); 400 } 401 402 /** 403 * Returns the maximum bar width, as a percentage of the available drawing 404 * space. 405 * 406 * @return The maximum bar width. 407 * 408 * @see #setMaximumBarWidth(double) 409 */ 410 public double getMaximumBarWidth() { 411 return this.maximumBarWidth; 412 } 413 414 /** 415 * Sets the maximum bar width, which is specified as a percentage of the 416 * available space for all bars, and sends a {@link RendererChangeEvent} to 417 * all registered listeners. 418 * 419 * @param percent the percent (where 0.05 is five percent). 420 * 421 * @see #getMaximumBarWidth() 422 */ 423 public void setMaximumBarWidth(double percent) { 424 this.maximumBarWidth = percent; 425 fireChangeEvent(); 426 } 427 428 /** 429 * Returns the minimum bar length (in Java2D units). The default value is 430 * 0.0. 431 * 432 * @return The minimum bar length. 433 * 434 * @see #setMinimumBarLength(double) 435 */ 436 public double getMinimumBarLength() { 437 return this.minimumBarLength; 438 } 439 440 /** 441 * Sets the minimum bar length and sends a {@link RendererChangeEvent} to 442 * all registered listeners. The minimum bar length is specified in Java2D 443 * units, and can be used to prevent bars that represent very small data 444 * values from disappearing when drawn on the screen. Typically you would 445 * set this to (say) 0.5 or 1.0 Java 2D units. Use this attribute with 446 * caution, however, because setting it to a non-zero value will 447 * artificially increase the length of bars representing small values, 448 * which may misrepresent your data. 449 * 450 * @param min the minimum bar length (in Java2D units, must be >= 0.0). 451 * 452 * @see #getMinimumBarLength() 453 */ 454 public void setMinimumBarLength(double min) { 455 if (min < 0.0) { 456 throw new IllegalArgumentException("Requires 'min' >= 0.0"); 457 } 458 this.minimumBarLength = min; 459 fireChangeEvent(); 460 } 461 462 /** 463 * Returns the gradient paint transformer (an object used to transform 464 * gradient paint objects to fit each bar). 465 * 466 * @return A transformer (<code>null</code> possible). 467 * 468 * @see #setGradientPaintTransformer(GradientPaintTransformer) 469 */ 470 public GradientPaintTransformer getGradientPaintTransformer() { 471 return this.gradientPaintTransformer; 472 } 473 474 /** 475 * Sets the gradient paint transformer and sends a 476 * {@link RendererChangeEvent} to all registered listeners. 477 * 478 * @param transformer the transformer (<code>null</code> permitted). 479 * 480 * @see #getGradientPaintTransformer() 481 */ 482 public void setGradientPaintTransformer( 483 GradientPaintTransformer transformer) { 484 this.gradientPaintTransformer = transformer; 485 fireChangeEvent(); 486 } 487 488 /** 489 * Returns the fallback position for positive item labels that don't fit 490 * within a bar. 491 * 492 * @return The fallback position (<code>null</code> possible). 493 * 494 * @see #setPositiveItemLabelPositionFallback(ItemLabelPosition) 495 */ 496 public ItemLabelPosition getPositiveItemLabelPositionFallback() { 497 return this.positiveItemLabelPositionFallback; 498 } 499 500 /** 501 * Sets the fallback position for positive item labels that don't fit 502 * within a bar, and sends a {@link RendererChangeEvent} to all registered 503 * listeners. 504 * 505 * @param position the position (<code>null</code> permitted). 506 * 507 * @see #getPositiveItemLabelPositionFallback() 508 */ 509 public void setPositiveItemLabelPositionFallback( 510 ItemLabelPosition position) { 511 this.positiveItemLabelPositionFallback = position; 512 fireChangeEvent(); 513 } 514 515 /** 516 * Returns the fallback position for negative item labels that don't fit 517 * within a bar. 518 * 519 * @return The fallback position (<code>null</code> possible). 520 * 521 * @see #setPositiveItemLabelPositionFallback(ItemLabelPosition) 522 */ 523 public ItemLabelPosition getNegativeItemLabelPositionFallback() { 524 return this.negativeItemLabelPositionFallback; 525 } 526 527 /** 528 * Sets the fallback position for negative item labels that don't fit 529 * within a bar, and sends a {@link RendererChangeEvent} to all registered 530 * listeners. 531 * 532 * @param position the position (<code>null</code> permitted). 533 * 534 * @see #getNegativeItemLabelPositionFallback() 535 */ 536 public void setNegativeItemLabelPositionFallback( 537 ItemLabelPosition position) { 538 this.negativeItemLabelPositionFallback = position; 539 fireChangeEvent(); 540 } 541 542 /** 543 * Returns the flag that controls whether or not the base value for the 544 * bars is included in the range calculated by 545 * {@link #findRangeBounds(CategoryDataset)}. 546 * 547 * @return <code>true</code> if the base is included in the range, and 548 * <code>false</code> otherwise. 549 * 550 * @since 1.0.1 551 * 552 * @see #setIncludeBaseInRange(boolean) 553 */ 554 public boolean getIncludeBaseInRange() { 555 return this.includeBaseInRange; 556 } 557 558 /** 559 * Sets the flag that controls whether or not the base value for the bars 560 * is included in the range calculated by 561 * {@link #findRangeBounds(CategoryDataset)}. If the flag is changed, 562 * a {@link RendererChangeEvent} is sent to all registered listeners. 563 * 564 * @param include the new value for the flag. 565 * 566 * @since 1.0.1 567 * 568 * @see #getIncludeBaseInRange() 569 */ 570 public void setIncludeBaseInRange(boolean include) { 571 if (this.includeBaseInRange != include) { 572 this.includeBaseInRange = include; 573 fireChangeEvent(); 574 } 575 } 576 577 /** 578 * Returns the bar painter. 579 * 580 * @return The bar painter (never <code>null</code>). 581 * 582 * @see #setBarPainter(BarPainter) 583 * 584 * @since 1.0.11 585 */ 586 public BarPainter getBarPainter() { 587 return this.barPainter; 588 } 589 590 /** 591 * Sets the bar painter for this renderer and sends a 592 * {@link RendererChangeEvent} to all registered listeners. 593 * 594 * @param painter the painter (<code>null</code> not permitted). 595 * 596 * @see #getBarPainter() 597 * 598 * @since 1.0.11 599 */ 600 public void setBarPainter(BarPainter painter) { 601 if (painter == null) { 602 throw new IllegalArgumentException("Null 'painter' argument."); 603 } 604 this.barPainter = painter; 605 fireChangeEvent(); 606 } 607 608 /** 609 * Returns the flag that controls whether or not shadows are drawn for 610 * the bars. 611 * 612 * @return A boolean. 613 * 614 * @since 1.0.11 615 */ 616 public boolean getShadowsVisible() { 617 return this.shadowsVisible; 618 } 619 620 /** 621 * Sets the flag that controls whether or not shadows are 622 * drawn by the renderer. 623 * 624 * @param visible the new flag value. 625 * 626 * @since 1.0.11 627 */ 628 public void setShadowVisible(boolean visible) { 629 this.shadowsVisible = visible; 630 fireChangeEvent(); 631 } 632 633 /** 634 * Returns the shadow paint. 635 * 636 * @return The shadow paint. 637 * 638 * @see #setShadowPaint(Paint) 639 * 640 * @since 1.0.11 641 */ 642 public Paint getShadowPaint() { 643 return this.shadowPaint; 644 } 645 646 /** 647 * Sets the shadow paint and sends a {@link RendererChangeEvent} to all 648 * registered listeners. 649 * 650 * @param paint the paint (<code>null</code> not permitted). 651 * 652 * @see #getShadowPaint() 653 * 654 * @since 1.0.11 655 */ 656 public void setShadowPaint(Paint paint) { 657 if (paint == null) { 658 throw new IllegalArgumentException("Null 'paint' argument."); 659 } 660 this.shadowPaint = paint; 661 fireChangeEvent(); 662 } 663 664 /** 665 * Returns the shadow x-offset. 666 * 667 * @return The shadow x-offset. 668 * 669 * @since 1.0.11 670 */ 671 public double getShadowXOffset() { 672 return this.shadowXOffset; 673 } 674 675 /** 676 * Sets the x-offset for the bar shadow and sends a 677 * {@link RendererChangeEvent} to all registered listeners. 678 * 679 * @param offset the offset. 680 * 681 * @since 1.0.11 682 */ 683 public void setShadowXOffset(double offset) { 684 this.shadowXOffset = offset; 685 fireChangeEvent(); 686 } 687 688 /** 689 * Returns the shadow y-offset. 690 * 691 * @return The shadow y-offset. 692 * 693 * @since 1.0.11 694 */ 695 public double getShadowYOffset() { 696 return this.shadowYOffset; 697 } 698 699 /** 700 * Sets the y-offset for the bar shadow and sends a 701 * {@link RendererChangeEvent} to all registered listeners. 702 * 703 * @param offset the offset. 704 * 705 * @since 1.0.11 706 */ 707 public void setShadowYOffset(double offset) { 708 this.shadowYOffset = offset; 709 fireChangeEvent(); 710 } 711 712 /** 713 * Returns the lower clip value. This value is recalculated in the 714 * initialise() method. 715 * 716 * @return The value. 717 */ 718 public double getLowerClip() { 719 // TODO: this attribute should be transferred to the renderer state. 720 return this.lowerClip; 721 } 722 723 /** 724 * Returns the upper clip value. This value is recalculated in the 725 * initialise() method. 726 * 727 * @return The value. 728 */ 729 public double getUpperClip() { 730 // TODO: this attribute should be transferred to the renderer state. 731 return this.upperClip; 732 } 733 734 /** 735 * Initialises the renderer and returns a state object that will be passed 736 * to subsequent calls to the drawItem method. This method gets called 737 * once at the start of the process of drawing a chart. 738 * 739 * @param g2 the graphics device. 740 * @param dataArea the area in which the data is to be plotted. 741 * @param plot the plot. 742 * @param rendererIndex the renderer index. 743 * @param info collects chart rendering information for return to caller. 744 * 745 * @return The renderer state. 746 */ 747 public CategoryItemRendererState initialise(Graphics2D g2, 748 Rectangle2D dataArea, 749 CategoryPlot plot, 750 int rendererIndex, 751 PlotRenderingInfo info) { 752 753 CategoryItemRendererState state = super.initialise(g2, dataArea, plot, 754 rendererIndex, info); 755 756 // get the clipping values... 757 ValueAxis rangeAxis = plot.getRangeAxisForDataset(rendererIndex); 758 this.lowerClip = rangeAxis.getRange().getLowerBound(); 759 this.upperClip = rangeAxis.getRange().getUpperBound(); 760 761 // calculate the bar width 762 calculateBarWidth(plot, dataArea, rendererIndex, state); 763 764 return state; 765 766 } 767 768 /** 769 * Calculates the bar width and stores it in the renderer state. 770 * 771 * @param plot the plot. 772 * @param dataArea the data area. 773 * @param rendererIndex the renderer index. 774 * @param state the renderer state. 775 */ 776 protected void calculateBarWidth(CategoryPlot plot, 777 Rectangle2D dataArea, 778 int rendererIndex, 779 CategoryItemRendererState state) { 780 781 CategoryAxis domainAxis = getDomainAxis(plot, rendererIndex); 782 CategoryDataset dataset = plot.getDataset(rendererIndex); 783 if (dataset != null) { 784 int columns = dataset.getColumnCount(); 785 int rows = state.getVisibleSeriesCount() >= 0 786 ? state.getVisibleSeriesCount() : dataset.getRowCount(); 787 double space = 0.0; 788 PlotOrientation orientation = plot.getOrientation(); 789 if (orientation == PlotOrientation.HORIZONTAL) { 790 space = dataArea.getHeight(); 791 } 792 else if (orientation == PlotOrientation.VERTICAL) { 793 space = dataArea.getWidth(); 794 } 795 double maxWidth = space * getMaximumBarWidth(); 796 double categoryMargin = 0.0; 797 double currentItemMargin = 0.0; 798 if (columns > 1) { 799 categoryMargin = domainAxis.getCategoryMargin(); 800 } 801 if (rows > 1) { 802 currentItemMargin = getItemMargin(); 803 } 804 double used = space * (1 - domainAxis.getLowerMargin() 805 - domainAxis.getUpperMargin() 806 - categoryMargin - currentItemMargin); 807 if ((rows * columns) > 0) { 808 state.setBarWidth(Math.min(used / (rows * columns), maxWidth)); 809 } 810 else { 811 state.setBarWidth(Math.min(used, maxWidth)); 812 } 813 } 814 } 815 816 /** 817 * Calculates the coordinate of the first "side" of a bar. This will be 818 * the minimum x-coordinate for a vertical bar, and the minimum 819 * y-coordinate for a horizontal bar. 820 * 821 * @param plot the plot. 822 * @param orientation the plot orientation. 823 * @param dataArea the data area. 824 * @param domainAxis the domain axis. 825 * @param state the renderer state (has the bar width precalculated). 826 * @param row the row index. 827 * @param column the column index. 828 * 829 * @return The coordinate. 830 */ 831 protected double calculateBarW0(CategoryPlot plot, 832 PlotOrientation orientation, 833 Rectangle2D dataArea, 834 CategoryAxis domainAxis, 835 CategoryItemRendererState state, 836 int row, 837 int column) { 838 // calculate bar width... 839 double space = 0.0; 840 if (orientation == PlotOrientation.HORIZONTAL) { 841 space = dataArea.getHeight(); 842 } 843 else { 844 space = dataArea.getWidth(); 845 } 846 double barW0 = domainAxis.getCategoryStart(column, getColumnCount(), 847 dataArea, plot.getDomainAxisEdge()); 848 int seriesCount = state.getVisibleSeriesCount() >= 0 849 ? state.getVisibleSeriesCount() : getRowCount(); 850 int categoryCount = getColumnCount(); 851 if (seriesCount > 1) { 852 double seriesGap = space * getItemMargin() 853 / (categoryCount * (seriesCount - 1)); 854 double seriesW = calculateSeriesWidth(space, domainAxis, 855 categoryCount, seriesCount); 856 barW0 = barW0 + row * (seriesW + seriesGap) 857 + (seriesW / 2.0) - (state.getBarWidth() / 2.0); 858 } 859 else { 860 barW0 = domainAxis.getCategoryMiddle(column, getColumnCount(), 861 dataArea, plot.getDomainAxisEdge()) - state.getBarWidth() 862 / 2.0; 863 } 864 return barW0; 865 } 866 867 /** 868 * Calculates the coordinates for the length of a single bar. 869 * 870 * @param value the value represented by the bar. 871 * 872 * @return The coordinates for each end of the bar (or <code>null</code> if 873 * the bar is not visible for the current axis range). 874 */ 875 protected double[] calculateBarL0L1(double value) { 876 double lclip = getLowerClip(); 877 double uclip = getUpperClip(); 878 double barLow = Math.min(this.base, value); 879 double barHigh = Math.max(this.base, value); 880 if (barHigh < lclip) { // bar is not visible 881 return null; 882 } 883 if (barLow > uclip) { // bar is not visible 884 return null; 885 } 886 barLow = Math.max(barLow, lclip); 887 barHigh = Math.min(barHigh, uclip); 888 return new double[] {barLow, barHigh}; 889 } 890 891 /** 892 * Returns the range of values the renderer requires to display all the 893 * items from the specified dataset. This takes into account the range 894 * of values in the dataset, plus the flag that determines whether or not 895 * the base value for the bars should be included in the range. 896 * 897 * @param dataset the dataset (<code>null</code> permitted). 898 * 899 * @return The range (or <code>null</code> if the dataset is 900 * <code>null</code> or empty). 901 */ 902 public Range findRangeBounds(CategoryDataset dataset) { 903 if (dataset == null) { 904 return null; 905 } 906 Range result = DatasetUtilities.findRangeBounds(dataset); 907 if (result != null) { 908 if (this.includeBaseInRange) { 909 result = Range.expandToInclude(result, this.base); 910 } 911 } 912 return result; 913 } 914 915 /** 916 * Returns a legend item for a series. 917 * 918 * @param datasetIndex the dataset index (zero-based). 919 * @param series the series index (zero-based). 920 * 921 * @return The legend item (possibly <code>null</code>). 922 */ 923 public LegendItem getLegendItem(int datasetIndex, int series) { 924 925 CategoryPlot cp = getPlot(); 926 if (cp == null) { 927 return null; 928 } 929 930 // check that a legend item needs to be displayed... 931 if (!isSeriesVisible(series) || !isSeriesVisibleInLegend(series)) { 932 return null; 933 } 934 935 CategoryDataset dataset = cp.getDataset(datasetIndex); 936 String label = getLegendItemLabelGenerator().generateLabel(dataset, 937 series); 938 String description = label; 939 String toolTipText = null; 940 if (getLegendItemToolTipGenerator() != null) { 941 toolTipText = getLegendItemToolTipGenerator().generateLabel( 942 dataset, series); 943 } 944 String urlText = null; 945 if (getLegendItemURLGenerator() != null) { 946 urlText = getLegendItemURLGenerator().generateLabel(dataset, 947 series); 948 } 949 Shape shape = lookupLegendShape(series); 950 Paint paint = lookupSeriesPaint(series); 951 Paint outlinePaint = lookupSeriesOutlinePaint(series); 952 Stroke outlineStroke = lookupSeriesOutlineStroke(series); 953 954 LegendItem result = new LegendItem(label, description, toolTipText, 955 urlText, true, shape, true, paint, isDrawBarOutline(), 956 outlinePaint, outlineStroke, false, new Line2D.Float(), 957 new BasicStroke(1.0f), Color.black); 958 result.setLabelFont(lookupLegendTextFont(series)); 959 Paint labelPaint = lookupLegendTextPaint(series); 960 if (labelPaint != null) { 961 result.setLabelPaint(labelPaint); 962 } 963 result.setDataset(dataset); 964 result.setDatasetIndex(datasetIndex); 965 result.setSeriesKey(dataset.getRowKey(series)); 966 result.setSeriesIndex(series); 967 if (this.gradientPaintTransformer != null) { 968 result.setFillPaintTransformer(this.gradientPaintTransformer); 969 } 970 return result; 971 } 972 973 /** 974 * Draws the bar for a single (series, category) data item. 975 * 976 * @param g2 the graphics device. 977 * @param state the renderer state. 978 * @param dataArea the data area. 979 * @param plot the plot. 980 * @param domainAxis the domain axis. 981 * @param rangeAxis the range axis. 982 * @param dataset the dataset. 983 * @param row the row index (zero-based). 984 * @param column the column index (zero-based). 985 * @param pass the pass index. 986 */ 987 public void drawItem(Graphics2D g2, 988 CategoryItemRendererState state, 989 Rectangle2D dataArea, 990 CategoryPlot plot, 991 CategoryAxis domainAxis, 992 ValueAxis rangeAxis, 993 CategoryDataset dataset, 994 int row, 995 int column, 996 int pass) { 997 998 // nothing is drawn if the row index is not included in the list with 999 // the indices of the visible rows... 1000 int visibleRow = state.getVisibleSeriesIndex(row); 1001 if (visibleRow < 0) { 1002 return; 1003 } 1004 // nothing is drawn for null values... 1005 Number dataValue = dataset.getValue(row, column); 1006 if (dataValue == null) { 1007 return; 1008 } 1009 1010 final double value = dataValue.doubleValue(); 1011 PlotOrientation orientation = plot.getOrientation(); 1012 double barW0 = calculateBarW0(plot, orientation, dataArea, domainAxis, 1013 state, visibleRow, column); 1014 double[] barL0L1 = calculateBarL0L1(value); 1015 if (barL0L1 == null) { 1016 return; // the bar is not visible 1017 } 1018 1019 RectangleEdge edge = plot.getRangeAxisEdge(); 1020 double transL0 = rangeAxis.valueToJava2D(barL0L1[0], dataArea, edge); 1021 double transL1 = rangeAxis.valueToJava2D(barL0L1[1], dataArea, edge); 1022 1023 // in the following code, barL0 is (in Java2D coordinates) the LEFT 1024 // end of the bar for a horizontal bar chart, and the TOP end of the 1025 // bar for a vertical bar chart. Whether this is the BASE of the bar 1026 // or not depends also on (a) whether the data value is 'negative' 1027 // relative to the base value and (b) whether or not the range axis is 1028 // inverted. This only matters if/when we apply the minimumBarLength 1029 // attribute, because we should extend the non-base end of the bar 1030 boolean positive = (value >= this.base); 1031 boolean inverted = rangeAxis.isInverted(); 1032 double barL0 = Math.min(transL0, transL1); 1033 double barLength = Math.abs(transL1 - transL0); 1034 double barLengthAdj = 0.0; 1035 if (barLength > 0.0 && barLength < getMinimumBarLength()) { 1036 barLengthAdj = getMinimumBarLength() - barLength; 1037 } 1038 double barL0Adj = 0.0; 1039 RectangleEdge barBase; 1040 if (orientation == PlotOrientation.HORIZONTAL) { 1041 if (positive && inverted || !positive && !inverted) { 1042 barL0Adj = barLengthAdj; 1043 barBase = RectangleEdge.RIGHT; 1044 } 1045 else { 1046 barBase = RectangleEdge.LEFT; 1047 } 1048 } 1049 else { 1050 if (positive && !inverted || !positive && inverted) { 1051 barL0Adj = barLengthAdj; 1052 barBase = RectangleEdge.BOTTOM; 1053 } 1054 else { 1055 barBase = RectangleEdge.TOP; 1056 } 1057 } 1058 1059 // draw the bar... 1060 Rectangle2D bar = null; 1061 if (orientation == PlotOrientation.HORIZONTAL) { 1062 bar = new Rectangle2D.Double(barL0 - barL0Adj, barW0, 1063 barLength + barLengthAdj, state.getBarWidth()); 1064 } 1065 else { 1066 bar = new Rectangle2D.Double(barW0, barL0 - barL0Adj, 1067 state.getBarWidth(), barLength + barLengthAdj); 1068 } 1069 if (getShadowsVisible()) { 1070 this.barPainter.paintBarShadow(g2, this, row, column, bar, barBase, 1071 true); 1072 } 1073 this.barPainter.paintBar(g2, this, row, column, bar, barBase); 1074 1075 CategoryItemLabelGenerator generator = getItemLabelGenerator(row, 1076 column); 1077 if (generator != null && isItemLabelVisible(row, column)) { 1078 drawItemLabel(g2, dataset, row, column, plot, generator, bar, 1079 (value < 0.0)); 1080 } 1081 1082 // submit the current data point as a crosshair candidate 1083 int datasetIndex = plot.indexOf(dataset); 1084 updateCrosshairValues(state.getCrosshairState(), 1085 dataset.getRowKey(row), dataset.getColumnKey(column), value, 1086 datasetIndex, barW0, barL0, orientation); 1087 1088 // add an item entity, if this information is being collected 1089 EntityCollection entities = state.getEntityCollection(); 1090 if (entities != null) { 1091 addItemEntity(entities, dataset, row, column, bar); 1092 } 1093 1094 } 1095 1096 /** 1097 * Calculates the available space for each series. 1098 * 1099 * @param space the space along the entire axis (in Java2D units). 1100 * @param axis the category axis. 1101 * @param categories the number of categories. 1102 * @param series the number of series. 1103 * 1104 * @return The width of one series. 1105 */ 1106 protected double calculateSeriesWidth(double space, CategoryAxis axis, 1107 int categories, int series) { 1108 double factor = 1.0 - getItemMargin() - axis.getLowerMargin() 1109 - axis.getUpperMargin(); 1110 if (categories > 1) { 1111 factor = factor - axis.getCategoryMargin(); 1112 } 1113 return (space * factor) / (categories * series); 1114 } 1115 1116 /** 1117 * Draws an item label. This method is overridden so that the bar can be 1118 * used to calculate the label anchor point. 1119 * 1120 * @param g2 the graphics device. 1121 * @param data the dataset. 1122 * @param row the row. 1123 * @param column the column. 1124 * @param plot the plot. 1125 * @param generator the label generator. 1126 * @param bar the bar. 1127 * @param negative a flag indicating a negative value. 1128 */ 1129 protected void drawItemLabel(Graphics2D g2, 1130 CategoryDataset data, 1131 int row, 1132 int column, 1133 CategoryPlot plot, 1134 CategoryItemLabelGenerator generator, 1135 Rectangle2D bar, 1136 boolean negative) { 1137 1138 String label = generator.generateLabel(data, row, column); 1139 if (label == null) { 1140 return; // nothing to do 1141 } 1142 1143 Font labelFont = getItemLabelFont(row, column); 1144 g2.setFont(labelFont); 1145 Paint paint = getItemLabelPaint(row, column); 1146 g2.setPaint(paint); 1147 1148 // find out where to place the label... 1149 ItemLabelPosition position = null; 1150 if (!negative) { 1151 position = getPositiveItemLabelPosition(row, column); 1152 } 1153 else { 1154 position = getNegativeItemLabelPosition(row, column); 1155 } 1156 1157 // work out the label anchor point... 1158 Point2D anchorPoint = calculateLabelAnchorPoint( 1159 position.getItemLabelAnchor(), bar, plot.getOrientation()); 1160 1161 if (isInternalAnchor(position.getItemLabelAnchor())) { 1162 Shape bounds = TextUtilities.calculateRotatedStringBounds(label, 1163 g2, (float) anchorPoint.getX(), (float) anchorPoint.getY(), 1164 position.getTextAnchor(), position.getAngle(), 1165 position.getRotationAnchor()); 1166 1167 if (bounds != null) { 1168 if (!bar.contains(bounds.getBounds2D())) { 1169 if (!negative) { 1170 position = getPositiveItemLabelPositionFallback(); 1171 } 1172 else { 1173 position = getNegativeItemLabelPositionFallback(); 1174 } 1175 if (position != null) { 1176 anchorPoint = calculateLabelAnchorPoint( 1177 position.getItemLabelAnchor(), bar, 1178 plot.getOrientation()); 1179 } 1180 } 1181 } 1182 1183 } 1184 1185 if (position != null) { 1186 TextUtilities.drawRotatedString(label, g2, 1187 (float) anchorPoint.getX(), (float) anchorPoint.getY(), 1188 position.getTextAnchor(), position.getAngle(), 1189 position.getRotationAnchor()); 1190 } 1191 } 1192 1193 /** 1194 * Calculates the item label anchor point. 1195 * 1196 * @param anchor the anchor. 1197 * @param bar the bar. 1198 * @param orientation the plot orientation. 1199 * 1200 * @return The anchor point. 1201 */ 1202 private Point2D calculateLabelAnchorPoint(ItemLabelAnchor anchor, 1203 Rectangle2D bar, 1204 PlotOrientation orientation) { 1205 1206 Point2D result = null; 1207 double offset = getItemLabelAnchorOffset(); 1208 double x0 = bar.getX() - offset; 1209 double x1 = bar.getX(); 1210 double x2 = bar.getX() + offset; 1211 double x3 = bar.getCenterX(); 1212 double x4 = bar.getMaxX() - offset; 1213 double x5 = bar.getMaxX(); 1214 double x6 = bar.getMaxX() + offset; 1215 1216 double y0 = bar.getMaxY() + offset; 1217 double y1 = bar.getMaxY(); 1218 double y2 = bar.getMaxY() - offset; 1219 double y3 = bar.getCenterY(); 1220 double y4 = bar.getMinY() + offset; 1221 double y5 = bar.getMinY(); 1222 double y6 = bar.getMinY() - offset; 1223 1224 if (anchor == ItemLabelAnchor.CENTER) { 1225 result = new Point2D.Double(x3, y3); 1226 } 1227 else if (anchor == ItemLabelAnchor.INSIDE1) { 1228 result = new Point2D.Double(x4, y4); 1229 } 1230 else if (anchor == ItemLabelAnchor.INSIDE2) { 1231 result = new Point2D.Double(x4, y4); 1232 } 1233 else if (anchor == ItemLabelAnchor.INSIDE3) { 1234 result = new Point2D.Double(x4, y3); 1235 } 1236 else if (anchor == ItemLabelAnchor.INSIDE4) { 1237 result = new Point2D.Double(x4, y2); 1238 } 1239 else if (anchor == ItemLabelAnchor.INSIDE5) { 1240 result = new Point2D.Double(x4, y2); 1241 } 1242 else if (anchor == ItemLabelAnchor.INSIDE6) { 1243 result = new Point2D.Double(x3, y2); 1244 } 1245 else if (anchor == ItemLabelAnchor.INSIDE7) { 1246 result = new Point2D.Double(x2, y2); 1247 } 1248 else if (anchor == ItemLabelAnchor.INSIDE8) { 1249 result = new Point2D.Double(x2, y2); 1250 } 1251 else if (anchor == ItemLabelAnchor.INSIDE9) { 1252 result = new Point2D.Double(x2, y3); 1253 } 1254 else if (anchor == ItemLabelAnchor.INSIDE10) { 1255 result = new Point2D.Double(x2, y4); 1256 } 1257 else if (anchor == ItemLabelAnchor.INSIDE11) { 1258 result = new Point2D.Double(x2, y4); 1259 } 1260 else if (anchor == ItemLabelAnchor.INSIDE12) { 1261 result = new Point2D.Double(x3, y4); 1262 } 1263 else if (anchor == ItemLabelAnchor.OUTSIDE1) { 1264 result = new Point2D.Double(x5, y6); 1265 } 1266 else if (anchor == ItemLabelAnchor.OUTSIDE2) { 1267 result = new Point2D.Double(x6, y5); 1268 } 1269 else if (anchor == ItemLabelAnchor.OUTSIDE3) { 1270 result = new Point2D.Double(x6, y3); 1271 } 1272 else if (anchor == ItemLabelAnchor.OUTSIDE4) { 1273 result = new Point2D.Double(x6, y1); 1274 } 1275 else if (anchor == ItemLabelAnchor.OUTSIDE5) { 1276 result = new Point2D.Double(x5, y0); 1277 } 1278 else if (anchor == ItemLabelAnchor.OUTSIDE6) { 1279 result = new Point2D.Double(x3, y0); 1280 } 1281 else if (anchor == ItemLabelAnchor.OUTSIDE7) { 1282 result = new Point2D.Double(x1, y0); 1283 } 1284 else if (anchor == ItemLabelAnchor.OUTSIDE8) { 1285 result = new Point2D.Double(x0, y1); 1286 } 1287 else if (anchor == ItemLabelAnchor.OUTSIDE9) { 1288 result = new Point2D.Double(x0, y3); 1289 } 1290 else if (anchor == ItemLabelAnchor.OUTSIDE10) { 1291 result = new Point2D.Double(x0, y5); 1292 } 1293 else if (anchor == ItemLabelAnchor.OUTSIDE11) { 1294 result = new Point2D.Double(x1, y6); 1295 } 1296 else if (anchor == ItemLabelAnchor.OUTSIDE12) { 1297 result = new Point2D.Double(x3, y6); 1298 } 1299 1300 return result; 1301 1302 } 1303 1304 /** 1305 * Returns <code>true</code> if the specified anchor point is inside a bar. 1306 * 1307 * @param anchor the anchor point. 1308 * 1309 * @return A boolean. 1310 */ 1311 private boolean isInternalAnchor(ItemLabelAnchor anchor) { 1312 return anchor == ItemLabelAnchor.CENTER 1313 || anchor == ItemLabelAnchor.INSIDE1 1314 || anchor == ItemLabelAnchor.INSIDE2 1315 || anchor == ItemLabelAnchor.INSIDE3 1316 || anchor == ItemLabelAnchor.INSIDE4 1317 || anchor == ItemLabelAnchor.INSIDE5 1318 || anchor == ItemLabelAnchor.INSIDE6 1319 || anchor == ItemLabelAnchor.INSIDE7 1320 || anchor == ItemLabelAnchor.INSIDE8 1321 || anchor == ItemLabelAnchor.INSIDE9 1322 || anchor == ItemLabelAnchor.INSIDE10 1323 || anchor == ItemLabelAnchor.INSIDE11 1324 || anchor == ItemLabelAnchor.INSIDE12; 1325 } 1326 1327 /** 1328 * Tests this instance for equality with an arbitrary object. 1329 * 1330 * @param obj the object (<code>null</code> permitted). 1331 * 1332 * @return A boolean. 1333 */ 1334 public boolean equals(Object obj) { 1335 if (obj == this) { 1336 return true; 1337 } 1338 if (!(obj instanceof BarRenderer)) { 1339 return false; 1340 } 1341 BarRenderer that = (BarRenderer) obj; 1342 if (this.base != that.base) { 1343 return false; 1344 } 1345 if (this.itemMargin != that.itemMargin) { 1346 return false; 1347 } 1348 if (this.drawBarOutline != that.drawBarOutline) { 1349 return false; 1350 } 1351 if (this.maximumBarWidth != that.maximumBarWidth) { 1352 return false; 1353 } 1354 if (this.minimumBarLength != that.minimumBarLength) { 1355 return false; 1356 } 1357 if (!ObjectUtilities.equal(this.gradientPaintTransformer, 1358 that.gradientPaintTransformer)) { 1359 return false; 1360 } 1361 if (!ObjectUtilities.equal(this.positiveItemLabelPositionFallback, 1362 that.positiveItemLabelPositionFallback)) { 1363 return false; 1364 } 1365 if (!ObjectUtilities.equal(this.negativeItemLabelPositionFallback, 1366 that.negativeItemLabelPositionFallback)) { 1367 return false; 1368 } 1369 if (!this.barPainter.equals(that.barPainter)) { 1370 return false; 1371 } 1372 if (this.shadowsVisible != that.shadowsVisible) { 1373 return false; 1374 } 1375 if (!PaintUtilities.equal(this.shadowPaint, that.shadowPaint)) { 1376 return false; 1377 } 1378 if (this.shadowXOffset != that.shadowXOffset) { 1379 return false; 1380 } 1381 if (this.shadowYOffset != that.shadowYOffset) { 1382 return false; 1383 } 1384 return super.equals(obj); 1385 } 1386 1387 /** 1388 * Provides serialization support. 1389 * 1390 * @param stream the output stream. 1391 * 1392 * @throws IOException if there is an I/O error. 1393 */ 1394 private void writeObject(ObjectOutputStream stream) throws IOException { 1395 stream.defaultWriteObject(); 1396 SerialUtilities.writePaint(this.shadowPaint, stream); 1397 } 1398 1399 /** 1400 * Provides serialization support. 1401 * 1402 * @param stream the input stream. 1403 * 1404 * @throws IOException if there is an I/O error. 1405 * @throws ClassNotFoundException if there is a classpath problem. 1406 */ 1407 private void readObject(ObjectInputStream stream) 1408 throws IOException, ClassNotFoundException { 1409 stream.defaultReadObject(); 1410 this.shadowPaint = SerialUtilities.readPaint(stream); 1411 } 1412 1413 }