001 /** 002 * ======================================== 003 * JFreeReport : a free Java report library 004 * ======================================== 005 * 006 * Project Info: http://reporting.pentaho.org/ 007 * 008 * (C) Copyright 2000-2007, by Object Refinery Limited, Pentaho Corporation and Contributors. 009 * 010 * This library is free software; you can redistribute it and/or modify it under the terms 011 * of the GNU Lesser General Public License as published by the Free Software Foundation; 012 * either version 2.1 of the License, or (at your option) any later version. 013 * 014 * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 015 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 016 * See the GNU Lesser General Public License for more details. 017 * 018 * You should have received a copy of the GNU Lesser General Public License along with this 019 * library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, 020 * Boston, MA 02111-1307, USA. 021 * 022 * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 023 * in the United States and other countries.] 024 * 025 * ------------ 026 * $Id: SurveyScale.java 3525 2007-10-16 11:43:48Z tmorgner $ 027 * ------------ 028 * (C) Copyright 2000-2005, by Object Refinery Limited. 029 * (C) Copyright 2005-2007, by Pentaho Corporation. 030 */ 031 032 package org.jfree.report.modules.misc.survey; 033 034 import java.awt.BasicStroke; 035 import java.awt.Color; 036 import java.awt.Font; 037 import java.awt.Graphics2D; 038 import java.awt.Paint; 039 import java.awt.Shape; 040 import java.awt.Stroke; 041 import java.awt.geom.Ellipse2D; 042 import java.awt.geom.Line2D; 043 import java.awt.geom.Rectangle2D; 044 import java.io.IOException; 045 import java.io.ObjectInputStream; 046 import java.io.ObjectOutputStream; 047 import java.io.Serializable; 048 049 import org.jfree.serializer.SerializerHelper; 050 import org.jfree.text.TextUtilities; 051 import org.jfree.ui.Drawable; 052 import org.jfree.ui.TextAnchor; 053 import org.jfree.util.BooleanList; 054 import org.jfree.util.BooleanUtilities; 055 import org.jfree.util.ShapeList; 056 import org.jfree.util.ShapeUtilities; 057 058 /** 059 * Draws a survey scale. By implementing the {@link Drawable} interface, 060 * instances can be displayed within a report using the {@link 061 * org.jfree.report.DrawableElement} class. 062 * 063 * @author David Gilbert 064 */ 065 public class SurveyScale implements Drawable, Serializable 066 { 067 private static final Number[] EMPTY_VALUES = new Number[0]; 068 069 /** The lowest response value on the scale. */ 070 private int lowest; 071 072 /** The highest response value on the scale. */ 073 private int highest; 074 075 /** The lower margin. */ 076 private double lowerMargin = 0.10; 077 078 /** The upper margin. */ 079 private double upperMargin = 0.10; 080 081 /** A list of flags that control whether or not the shapes are filled. */ 082 private BooleanList fillShapes; 083 084 /** The values to display. */ 085 private Number[] values; 086 087 /** The lower bound of the highlighted range. */ 088 private Number rangeLowerBound; 089 090 /** The upper bound of the highlighted range. */ 091 private Number rangeUpperBound; 092 093 /** Draw a border? */ 094 private boolean drawBorder = false; 095 096 /** Draw the tick marks? */ 097 private boolean drawTickMarks; 098 099 /** Draw the scale values. */ 100 private boolean drawScaleValues = false; 101 102 /** The font used to display the scale values. */ 103 private Font scaleValueFont; 104 105 /** The paint used to draw the scale values. */ 106 private transient Paint scaleValuePaint; 107 108 /** The range paint. */ 109 private transient Paint rangePaint; 110 111 /** The shapes to display. */ 112 private transient ShapeList shapes; 113 114 /** The fill paint. */ 115 private transient Paint fillPaint; 116 117 /** The outline stroke for the shapes. */ 118 private transient Stroke outlineStroke; 119 120 /** 121 * The default shape, if no shape is defined in the shapeList for the given 122 * value. 123 */ 124 private transient Shape defaultShape; 125 126 /** The tick mark paint. */ 127 private transient Paint tickMarkPaint; 128 129 private transient Paint borderPaint; 130 131 private int range; 132 private double lowerBound; 133 private double upperBound; 134 135 /** Creates a new default instance. */ 136 public SurveyScale() 137 { 138 this(1, 5, EMPTY_VALUES); 139 } 140 141 /** 142 * Creates a new instance. 143 * 144 * @param lowest the lowest response value on the scale. 145 * @param highest the highest response value on the scale. 146 * @param values the values to display. 147 */ 148 public SurveyScale(final int lowest, final int highest, 149 final Number[] values) 150 { 151 152 this.lowest = lowest; 153 this.highest = highest; 154 if (values == null) 155 { 156 this.values = EMPTY_VALUES; 157 } 158 else 159 { 160 this.values = (Number[]) values.clone(); 161 } 162 163 this.drawTickMarks = true; 164 this.tickMarkPaint = Color.gray; 165 166 this.scaleValueFont = new Font("Serif", Font.ITALIC, 10); 167 this.scaleValuePaint = Color.black; 168 this.defaultShape = new Ellipse2D.Double(-3.0, -3.0, 6.0, 6.0); 169 170 this.rangeLowerBound = null; 171 this.rangeUpperBound = null; 172 this.rangePaint = Color.lightGray; 173 174 this.shapes = createShapeList(); 175 this.fillShapes = new BooleanList(); 176 this.fillShapes.setBoolean(0, Boolean.TRUE); 177 //this.fillShapes.setBoolean(5, Boolean.TRUE); 178 this.fillPaint = Color.black; 179 this.outlineStroke = new BasicStroke(0.5f); 180 recompute(); 181 } 182 183 public int getLowest() 184 { 185 return lowest; 186 } 187 188 public void setLowest(final int lowest) 189 { 190 this.lowest = lowest; 191 recompute(); 192 } 193 194 public int getHighest() 195 { 196 return highest; 197 } 198 199 public void setHighest(final int highest) 200 { 201 this.highest = highest; 202 recompute(); 203 } 204 205 /** 206 * This method is called whenever lowest or highest has changed. It will 207 * recompute the range and upper and lower bounds. 208 */ 209 protected void recompute() 210 { 211 this.range = Math.max(0, this.highest - this.lowest); 212 this.lowerBound = this.lowest - (range * this.lowerMargin); 213 this.upperBound = this.highest + (range * this.upperMargin); 214 } 215 216 protected int getRange() 217 { 218 return range; 219 } 220 221 protected void setRange(final int range) 222 { 223 this.range = range; 224 } 225 226 protected double getLowerBound() 227 { 228 return lowerBound; 229 } 230 231 protected void setLowerBound(final double lowerBound) 232 { 233 this.lowerBound = lowerBound; 234 } 235 236 protected double getUpperBound() 237 { 238 return upperBound; 239 } 240 241 protected void setUpperBound(final double upperBound) 242 { 243 this.upperBound = upperBound; 244 } 245 246 /** 247 * Creates the shape list used when drawing the scale. The list returned must 248 * contain exactly 6 elements. 249 * 250 * @return 251 */ 252 protected ShapeList createShapeList() 253 { 254 final ShapeList shapes = new ShapeList(); 255 //this.shapes.setShape(0, createDiagonalCross(3.0f, 0.5f)); 256 shapes.setShape(0, new Ellipse2D.Double(-3.0, -3.0, 6.0, 6.0)); 257 shapes.setShape(1, ShapeUtilities.createDownTriangle(4.0f)); 258 shapes.setShape(2, ShapeUtilities.createUpTriangle(4.0f)); 259 shapes.setShape(3, ShapeUtilities.createDiamond(4.0f)); 260 shapes.setShape(4, new Rectangle2D.Double(-4.0, -4.0, 8.0, 8.0)); 261 shapes.setShape(5, new Ellipse2D.Double(-4.0, -4.0, 8.0, 8.0)); 262 //this.shapes.setShape(5, createDiagonalCross(3.0f, 0.5f)); 263 return shapes; 264 } 265 266 /** 267 * Returns the lower bound of the highlighted range. A <code>null</code> 268 * value indicates that no range is set for highlighting. 269 * 270 * @return The lower bound (possibly <code>null</code>). 271 */ 272 public Number getRangeLowerBound() 273 { 274 return this.rangeLowerBound; 275 } 276 277 /** 278 * Sets the lower bound for the range that is highlighted on the scale. 279 * 280 * @param bound the lower bound (<code>null</code> permitted). 281 */ 282 public void setRangeLowerBound(final Number bound) 283 { 284 this.rangeLowerBound = bound; 285 } 286 287 /** 288 * Returns the upper bound of the highlighted range. A <code>null</code> 289 * value indicates that no range is set for highlighting. 290 * 291 * @return The upper bound (possibly <code>null</code>). 292 */ 293 public Number getRangeUpperBound() 294 { 295 return this.rangeUpperBound; 296 } 297 298 /** 299 * Sets the upper bound for the range that is highlighted on the scale. 300 * 301 * @param bound the upper bound (<code>null</code> permitted). 302 */ 303 public void setRangeUpperBound(final Number bound) 304 { 305 this.rangeUpperBound = bound; 306 } 307 308 /** 309 * Returns a flag that controls whether or not a border is drawn around the 310 * scale. 311 * 312 * @return A boolean. 313 */ 314 public boolean isDrawBorder() 315 { 316 return this.drawBorder; 317 } 318 319 /** 320 * Sets a flag that controls whether or not a border is drawn around the 321 * scale. 322 * 323 * @param flag the flag. 324 */ 325 public void setDrawBorder(final boolean flag) 326 { 327 this.drawBorder = flag; 328 } 329 330 /** 331 * Returns the flag that controls whether the tick marks are drawn. 332 * 333 * @return A boolean. 334 */ 335 public boolean isDrawTickMarks() 336 { 337 return this.drawTickMarks; 338 } 339 340 /** 341 * Sets the flag that controls whether the tick marks are drawn. 342 * 343 * @param flag a boolean. 344 */ 345 public void setDrawTickMarks(final boolean flag) 346 { 347 this.drawTickMarks = flag; 348 } 349 350 /** 351 * Returns a flag that controls whether or not scale values are drawn. 352 * 353 * @return a boolean. 354 */ 355 public boolean isDrawScaleValues() 356 { 357 return this.drawScaleValues; 358 } 359 360 /** 361 * Sets a flag that controls whether or not scale values are drawn. 362 * 363 * @param flag the flag. 364 */ 365 public void setDrawScaleValues(final boolean flag) 366 { 367 this.drawScaleValues = flag; 368 } 369 370 /** 371 * Returns the font used to display the scale values. 372 * 373 * @return A font (never <code>null</code>). 374 */ 375 public Font getScaleValueFont() 376 { 377 return this.scaleValueFont; 378 } 379 380 /** 381 * Sets the font used to display the scale values. 382 * 383 * @param font the font (<code>null</code> not permitted). 384 */ 385 public void setScaleValueFont(final Font font) 386 { 387 if (font == null) 388 { 389 throw new IllegalArgumentException("Null 'font' argument."); 390 } 391 this.scaleValueFont = font; 392 } 393 394 /** 395 * Returns the color used to draw the scale values (if they are visible). 396 * 397 * @return A paint (never <code>null</code>). 398 */ 399 public Paint getScaleValuePaint() 400 { 401 return this.scaleValuePaint; 402 } 403 404 /** 405 * Sets the color used to draw the scale values. 406 * 407 * @param paint the paint (<code>null</code> not permitted). 408 */ 409 public void setScaleValuePaint(final Paint paint) 410 { 411 if (paint == null) 412 { 413 throw new IllegalArgumentException("Null 'paint' argument."); 414 } 415 this.scaleValuePaint = paint; 416 } 417 418 /** 419 * Returns the shape used to indicate the value of a response. 420 * 421 * @param index the value index (zero-based). 422 * @return The shape. 423 */ 424 public Shape getShape(final int index) 425 { 426 return this.shapes.getShape(index); 427 } 428 429 /** 430 * Sets the shape used to mark a particular value in the dataset. 431 * 432 * @param index the value index (zero-based). 433 * @param shape the shape (<code>null</code> not permitted). 434 */ 435 public void setShape(final int index, final Shape shape) 436 { 437 this.shapes.setShape(index, shape); 438 } 439 440 /** 441 * Returns a flag that controls whether the shape for a particular value 442 * should be filled. 443 * 444 * @param index the value index (zero-based). 445 * @return A boolean. 446 */ 447 public boolean isShapeFilled(final int index) 448 { 449 boolean result = false; 450 final Boolean b = this.fillShapes.getBoolean(index); 451 if (b != null) 452 { 453 result = b.booleanValue(); 454 } 455 return result; 456 } 457 458 /** 459 * Sets the flag that controls whether the shape for a particular value should 460 * be filled. 461 * 462 * @param index the value index (zero-based). 463 * @param fill the flag. 464 */ 465 public void setShapeFilled(final int index, final boolean fill) 466 { 467 this.fillShapes.setBoolean(index, BooleanUtilities.valueOf(fill)); 468 } 469 470 /** 471 * Returns the paint used to highlight the range. 472 * 473 * @return A {@link Paint} object (never <code>null</code>). 474 */ 475 public Paint getRangePaint() 476 { 477 return this.rangePaint; 478 } 479 480 /** 481 * Sets the paint used to highlight the range (if one is specified). 482 * 483 * @param paint the paint (<code>null</code> not permitted). 484 */ 485 public void setRangePaint(final Paint paint) 486 { 487 if (paint == null) 488 { 489 throw new IllegalArgumentException("Null 'paint' argument."); 490 } 491 this.rangePaint = paint; 492 } 493 494 public Paint getBorderPaint() 495 { 496 return borderPaint; 497 } 498 499 public void setBorderPaint(final Paint borderPaint) 500 { 501 if (borderPaint == null) 502 { 503 throw new IllegalArgumentException("Null 'paint' argument."); 504 } 505 this.borderPaint = borderPaint; 506 } 507 508 /** 509 * Returns the default shape, which is used, if a shape for a certain value is 510 * not defined. 511 * 512 * @return the default shape, never null. 513 */ 514 public Shape getDefaultShape() 515 { 516 return defaultShape; 517 } 518 519 /** 520 * Redefines the default shape. 521 * 522 * @param defaultShape the default shape 523 * @throws NullPointerException if the given shape is null. 524 */ 525 public void setDefaultShape(final Shape defaultShape) 526 { 527 if (defaultShape == null) 528 { 529 throw new NullPointerException("The default shape must not be null."); 530 } 531 this.defaultShape = defaultShape; 532 } 533 534 public Paint getTickMarkPaint() 535 { 536 return tickMarkPaint; 537 } 538 539 public void setTickMarkPaint(final Paint tickMarkPaint) 540 { 541 if (tickMarkPaint == null) 542 { 543 throw new NullPointerException(); 544 } 545 this.tickMarkPaint = tickMarkPaint; 546 } 547 548 public Number[] getValues() 549 { 550 return (Number[]) values.clone(); 551 } 552 553 public Paint getFillPaint() 554 { 555 return fillPaint; 556 } 557 558 public void setFillPaint(final Paint fillPaint) 559 { 560 if (fillPaint == null) 561 { 562 throw new NullPointerException(); 563 } 564 this.fillPaint = fillPaint; 565 } 566 567 public Stroke getOutlineStroke() 568 { 569 return outlineStroke; 570 } 571 572 public void setOutlineStroke(final Stroke outlineStroke) 573 { 574 if (outlineStroke == null) 575 { 576 throw new NullPointerException(); 577 } 578 this.outlineStroke = outlineStroke; 579 } 580 581 public double getUpperMargin() 582 { 583 return upperMargin; 584 } 585 586 public void setUpperMargin(final double upperMargin) 587 { 588 this.upperMargin = upperMargin; 589 } 590 591 public double getLowerMargin() 592 { 593 return lowerMargin; 594 } 595 596 public void setLowerMargin(final double lowerMargin) 597 { 598 this.lowerMargin = lowerMargin; 599 } 600 601 /** 602 * Draws the survey scale. 603 * 604 * @param g2 the graphics device. 605 * @param area the area. 606 */ 607 public void draw(final Graphics2D g2, final Rectangle2D area) 608 { 609 610 if (isDrawBorder()) 611 { 612 drawBorder(g2, area); 613 } 614 615 drawRangeArea(area, g2); 616 617 // draw tick marks... 618 if (isDrawTickMarks()) 619 { 620 drawTickMarks(g2, area); 621 } 622 623 // draw scale values... 624 if (isDrawScaleValues()) 625 { 626 drawScaleValues(g2, area); 627 } 628 629 drawValues(g2, area); 630 } 631 632 protected void drawValues(final Graphics2D g2, 633 final Rectangle2D area) 634 { 635 636 // draw data values... 637 final Number[] values = getValues(); 638 if (values.length == 0) 639 { 640 return; 641 } 642 643 final double y = area.getCenterY(); 644 645 final Stroke outlineStroke = getOutlineStroke(); 646 final Shape defaultShape = getDefaultShape(); 647 648 g2.setPaint(getFillPaint()); 649 for (int i = 0; i < values.length; i++) 650 { 651 final Number n = values[i]; 652 if (n == null) 653 { 654 continue; 655 } 656 657 final double v = n.doubleValue(); 658 final double x = valueToJava2D(v, area); 659 Shape valueShape = getShape(i); 660 if (valueShape == null) 661 { 662 valueShape = defaultShape; 663 } 664 if (isShapeFilled(i)) 665 { 666 g2.translate(x, y); 667 g2.fill(valueShape); 668 g2.translate(-x, -y); 669 } 670 else 671 { 672 g2.setStroke(outlineStroke); 673 g2.translate(x, y); 674 g2.draw(valueShape); 675 g2.translate(-x, -y); 676 } 677 } 678 } 679 680 protected void drawScaleValues(final Graphics2D g2, final Rectangle2D area) 681 { 682 g2.setPaint(getScaleValuePaint()); 683 g2.setFont(getScaleValueFont()); 684 685 final int highest = getHighest(); 686 for (int i = getLowest(); i <= highest; i++) 687 { 688 final double x = valueToJava2D(i, area); 689 final double y = area.getCenterY(); 690 TextUtilities.drawAlignedString(String.valueOf(i), g2, (float) x, 691 (float) y, TextAnchor.CENTER); 692 } 693 } 694 695 protected void drawTickMarks(final Graphics2D g2, final Rectangle2D area) 696 { 697 g2.setPaint(getTickMarkPaint()); 698 g2.setStroke(new BasicStroke(0.1f)); 699 700 final int highest = getHighest(); 701 for (int i = getLowest(); i <= highest; i++) 702 { 703 for (int j = 0; j < 10; j++) 704 { 705 final double xx = valueToJava2D(i + j / 10.0, area); 706 final Line2D mark = new Line2D.Double(xx, area.getCenterY() - 2.0, xx, 707 area.getCenterY() + 2.0); 708 g2.draw(mark); 709 } 710 } 711 final double xx = valueToJava2D(highest, area); 712 final Line2D mark = new Line2D.Double(xx, area.getCenterY() - 2.0, xx, 713 area.getCenterY() + 2.0); 714 g2.draw(mark); 715 } 716 717 protected void drawRangeArea(final Rectangle2D area, final Graphics2D g2) 718 { 719 final Number rangeUpperBound = getRangeUpperBound(); 720 final Number rangeLowerBound = getRangeLowerBound(); 721 if (rangeLowerBound == null || rangeUpperBound == null) 722 { 723 return; 724 } 725 final double x0 = valueToJava2D(rangeLowerBound.doubleValue(), area); 726 final double x1 = valueToJava2D(rangeUpperBound.doubleValue(), area); 727 final Rectangle2D rangeArea = new Rectangle2D.Double(x0, area.getY(), 728 (x1 - x0), area.getHeight()); 729 g2.setPaint(getRangePaint()); 730 g2.fill(rangeArea); 731 } 732 733 protected void drawBorder(final Graphics2D g2, final Rectangle2D area) 734 { 735 g2.setStroke(getOutlineStroke()); 736 g2.setPaint(getBorderPaint()); 737 g2.draw(area); 738 } 739 740 /** 741 * Translates a data value to Java2D coordinates. 742 * 743 * @param value the value. 744 * @param area the area. 745 * @param lowerBound the lower bound. 746 * @param upperBound the upper bound. 747 * @return The Java2D coordinate. 748 */ 749 private double valueToJava2D(final double value, 750 final Rectangle2D area) 751 { 752 753 final double upperBound = getUpperBound(); 754 final double lowerBound = getLowerBound(); 755 return area.getMinX() + ((value - lowerBound) / 756 (upperBound - lowerBound) * area .getWidth()); 757 758 } 759 760 private void writeObject(final ObjectOutputStream out) 761 throws IOException 762 { 763 out.defaultWriteObject(); 764 final SerializerHelper helper = SerializerHelper.getInstance(); 765 helper.writeObject(scaleValuePaint, out); 766 helper.writeObject(rangePaint, out); 767 helper.writeObject(fillPaint, out); 768 helper.writeObject(outlineStroke, out); 769 helper.writeObject(defaultShape, out); 770 helper.writeObject(tickMarkPaint, out); 771 helper.writeObject(borderPaint, out); 772 final int size = shapes.size(); 773 out.writeInt(size); 774 for (int i = 0; i < size; i++) 775 { 776 final Shape s = shapes.getShape(i); 777 helper.writeObject(s, out); 778 } 779 } 780 781 private void readObject(final ObjectInputStream in) 782 throws IOException, ClassNotFoundException 783 { 784 in.defaultReadObject(); 785 final SerializerHelper helper = SerializerHelper.getInstance(); 786 scaleValuePaint = (Paint) helper.readObject(in); 787 rangePaint = (Paint) helper.readObject(in); 788 fillPaint = (Paint) helper.readObject(in); 789 outlineStroke = (Stroke) helper.readObject(in); 790 defaultShape = (Shape) helper.readObject(in); 791 tickMarkPaint = (Paint) helper.readObject(in); 792 borderPaint = (Paint) helper.readObject(in); 793 shapes = new ShapeList(); 794 795 final int size = in.readInt(); 796 for (int i = 0; i < size; i++) 797 { 798 final Shape s = (Shape) helper.readObject(in); 799 shapes.setShape(i, s); 800 } 801 802 } 803 804 }