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    }