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,v 1.12 2007/04/01 18:49:32 taqua Exp $
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(ObjectOutputStream out)
761 throws IOException
762 {
763 out.defaultWriteObject();
764 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(ObjectInputStream in)
782 throws IOException, ClassNotFoundException
783 {
784 in.defaultReadObject();
785 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 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 }